プロファイラーのアタッチとデタッチ
.NET Framework Version 4 より前のバージョンの .NET Framework では、アプリケーションとプロファイラーを同時に読み込む必要がありました。 アプリケーションの起動時に、ランタイムが環境変数とレジストリ設定を調べることによってアプリケーションをプロファイリングするかどうかを判断し、必要なプロファイラーを読み込んでいました。 読み込まれたプロファイラーは、アプリケーションが終了されるまでプロセス領域に残ったままでした。
.NET Framework 4 以降では、アプリケーションを起動した後にプロファイラーをアタッチできます。また、アプリケーションをプロファイリングした後、アプリケーションが終了する前にプロファイラーをデタッチできるようになっています。 プロファイラーがデタッチされた後、アプリケーションはプロファイラーがアタッチされる前と同じように実行されます。 つまり、一時的に読み込まれたプロファイラーのトレースはプロセス領域に残りません。
さらに、.NET Framework 4 以降では、プロファイリングのアタッチ メソッドとデタッチ メソッド、および関連するプロファイル API が強化され、プロファイラー ベースのツールを Just-In-Time の診断ツールとしてすぐにそのまま使用できるようになっています。
次の各セクションでは、これらの強化された機能について説明します。
プロファイラーのアタッチ
アタッチの詳細
アタッチの詳細なステップ
AttachProfiler メソッドの特定
ランタイムの読み込み前のターゲット アプリケーションへのアタッチ
アタッチの制限事項
プロファイラーのアタッチの例
アタッチ後のプロファイラーの初期化
プロファイラーのアタッチの完了
プロファイラーのデタッチ
デタッチ時のスレッドに関する考慮事項
デタッチの詳細なステップ
デタッチの制限事項
デバッグ
プロファイラーのアタッチ
実行中のアプリケーションにプロファイラーをアタッチするには、トリガー プロセスとターゲット プロセスという 2 つの異なるプロセスが必要です。
トリガー プロセスは、実行中のアプリケーションのプロセス領域にプロファイラー DLL を読み込むための実行可能ファイルです。 トリガー プロセスでは、ICLRProfiling::AttachProfiler メソッドを使用して、プロファイラー DLL を読み込むように共通言語ランタイム (CLR) に要求します。 プロファイラー DLL がターゲット プロセスに読み込まれた後にトリガー プロセスをそのまま残すか終了するかは、プロファイラー開発者が決定できます。
ターゲット プロセスには、プロファイリングするアプリケーションと CLR が含まれます。 AttachProfiler メソッドが呼び出されると、プロファイラー DLL がターゲット アプリケーションと共にこのプロセス領域に読み込まれます。
アタッチの詳細
AttachProfiler は、指定されたプロファイラーを指定されたプロセスにアタッチします。必要に応じて、一部のデータをプロファイラーに渡すこともできます。 このメソッドは、アタッチが完了するまで、指定されたタイムアウトの間待機します。
ターゲット プロセス内でアタッチ時にプロファイラーを読み込む手順は、起動時にプロファイラーを読み込む手順と似ています。CLR は、指定された CLSID の CoCreates を実行し、ICorProfilerCallback3 インターフェイスを照会し、ICorProfilerCallback3::InitializeForAttach メソッドを呼び出します。 割り当てられたタイムアウト内にこれらの操作が成功した場合、AttachProfiler は S_OK HRESULT を返します。 したがって、トリガー プロセスでは、プロファイラーが初期化を十分に完了できるタイムアウトを選択する必要があります。
メモ |
---|
ターゲット プロセスのファイナライザーの実行時間がタイムアウト値を超えたためにタイムアウトが発生すると、HRESULT_FROM_WIN32 (ERROR_TIMEOUT) が返されます。このエラーが発生した場合、アタッチ操作を再試行できます。 |
アタッチの完了前にタイムアウトが経過すると、AttachProfiler はエラーを示す HRESULT を返しますが、アタッチは成功している場合もあります。 このような場合は、別の方法で成功したかどうかを確認できます。 プロファイラー開発者は、トリガー プロセスとターゲット アプリケーションの間の独自の通信チャネルを確立していることがよくあります。 そのような通信チャネルを使用することで、成功したアタッチを検出できます。 トリガー プロセスで AttachProfiler をもう一度呼び出して CORPROF_E_PROFILER_ALREADY_ACTIVE を受けった場合も、成功したことを確認できます。
また、ターゲット プロセスでプロファイラーの DLL を読み込むには、十分なアクセス権が必要です。 これは、アクセス権が制限されたアカウント (NETWORK SERVICE など) で実行されている可能性があるサービス (W3wp.exe プロセスなど) にアタッチする場合に問題になることがあります。 このような場合、ファイル システムの一部のディレクトリに、ターゲット プロセスによるアクセスを禁止するセキュリティ制限が適用されている可能性があります。 そのため、プロファイラーの開発者は、適切なアクセス権が許可された場所にプロファイラー DLL をインストールする必要があります。
エラーはアプリケーション イベント ログに記録されます。
アタッチの詳細なステップ
トリガー プロセスで AttachProfiler を呼び出し、ターゲット プロセスとアタッチするプロファイラーを特定します。必要に応じて、アタッチ後にプロファイラーに提供するデータを渡すこともできます。
ターゲット プロセス内で、CLR がアタッチ要求を受け取ります。
ターゲット プロセス内で、CLR がプロファイラー オブジェクトを作成し、そのオブジェクトから ICorProfilerCallback3 インターフェイスを取得します。
CLR は、次に InitializeForAttach メソッドを呼び出し、トリガー プロセスでアタッチ要求に含められたデータを渡します。
プロファイラーがプロファイラー自体を初期化し、必要なコールバックを有効にした後、プロファイラーから正常に制御が戻ります。
トリガー プロセス内で、AttachProfiler が S_OK HRESULT を返し、プロファイラーが正常にアタッチされたことを示します。 トリガー プロセスの関連付けが解除されます。
この後にプロファイリングが行われます。以降のプロファイリングの処理は以前のバージョンと同じです。
AttachProfiler メソッドの特定
トリガー プロセスでは、AttachProfiler メソッドを次のステップで特定できます。
LoadLibrary メソッドを呼び出して、mscoree.dll を読み込み、CLRCreateInstance メソッドを探します。
CLSID_CLRMetaHost と IID_ICLRMetaHost を引数に指定して CLRCreateInstance メソッドを呼び出します。ICLRMetaHost インターフェイスが返されます。
ICLRMetaHost::EnumerateLoadedRuntimes メソッドを呼び出して、プロセスの各 CLR インスタンスの ICLRRuntimeInfo インターフェイスを取得します。
プロファイラーをアタッチする対象のインターフェイスが見つかるまで ICLRRuntimeInfo インターフェイスを反復処理します。
引数に IID_ICLRProfiling を指定して ICLRRuntimeInfo::GetInterface メソッドを呼び出し、ICLRProfiling インターフェイスを取得します。このインターフェイスから AttachProfiler メソッドが提供されます。
ランタイムの読み込み前のターゲット アプリケーションへのアタッチ
この機能はサポートされていません。 トリガー プロセスで、ランタイムがまだ読み込まれていないプロセスを指定して AttachProfiler メソッドを呼び出そうとすると、AttachProfiler メソッドからエラーを示す HRESULT が返されます。 ランタイムを読み込んだ時点からアプリケーションをプロファイリングする場合は、ターゲット アプリケーションを起動する前に、適切な環境変数を設定する (または、その設定を行うためにプロファイラー開発者が作成したアプリケーションを実行する) 必要があります。
アタッチの制限事項
アタッチするプロファイラーで使用できるのは、ICorProfilerCallback メソッドと ICorProfilerInfo メソッドのサブセットだけです。これらについて、以降のセクションで説明します。
ICorProfilerCallback の制限
プロファイラーで ICorProfilerInfo::SetEventMask メソッドを呼び出すときは、COR_PRF_ALLOWABLE_AFTER_ATTACH 列挙に含まれるイベント フラグだけを指定する必要があります。 その他のコールバックは、アタッチするプロファイラーで使用できません。 アタッチするプロファイラーで COR_PRF_ALLOWABLE_AFTER_ATTACH ビットマスクにないフラグを指定しようとすると、ICorProfilerInfo::SetEventMask から CORPROF_E_UNSUPPORTED_FOR_ATTACHING_PROFILER HRESULT が返されます。
ICorProfilerInfo の制限
AttachProfiler メソッドを呼び出して読み込まれたプロファイラーで、制限されている以下のいずれかの ICorProfilerInfo メソッドまたは ICorProfilerInfo2 メソッドを呼び出そうとすると、そのメソッドから CORPROF_E_UNSUPPORTED_FOR_ATTACHING_PROFILER HRESULT が返されます。
制限されている ICorProfilerInfo メソッド:
制限されている ICorProfilerInfo2 メソッド:
制限されている ICorProfilerInfo3 メソッド:
プロファイラーのアタッチの例
この例では、ターゲット プロセスのプロセス ID (PID)、タイムアウトまでのミリ秒数、読み込むプロファイラーの CLSID、およびプロファイラーの DLL ファイルの完全パスをトリガー プロセスが既に認識しているものとします。 また、プロファイラーの構成データを保持する MyProfilerConfigData という構造体と、その構造体に構成データを渡す PopulateMyProfilerConfigData という関数が定義されていることを前提としています。
HRESULT CallAttachProfiler(DWORD dwProfileeProcID, DWORD dwMillisecondsTimeout,
GUID profilerCLSID, LPCWSTR wszProfilerPath)
{
// This can be a data type of your own choosing for sending configuration data
// to your profiler:
MyProfilerConfigData profConfig;
PopulateMyProfilerConfigData(&profConfig);
LPVOID pvClientData = (LPVOID) &profConfig;
DWORD cbClientData = sizeof(profConfig);
ICLRMetaHost * pMetaHost = NULL;
IEnumUnknown * pEnum = NULL;
IUnknown * pUnk = NULL;
ICLRRuntimeInfo * pRuntime = NULL;
ICLRProfiling * pProfiling = NULL;
HRESULT hr = E_FAIL;
hModule = LoadLibrary(L"mscoree.dll");
if (hModule == NULL)
goto Cleanup;
CLRCreateInstanceFnPtr pfnCreateInterface =
(CLRCreateInstanceFnPtr)GetProcAddress(hModule, "CLRCreateInterface");
if (pfnCreateInterface == NULL)
goto Cleanup;
hr = (*pfnCreateInterface)(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID *)&pMetaHost);
if (FAILED(hr))
goto Cleanup;
hr = pMetaHost->EnumerateLoadedRuntimes(hProcess, &pEnum);
if (FAILED(hr))
goto Cleanup;
while (pEnum->Next(1, &pUnk, NULL) == S_OK)
{
hr = pUnk->QueryInterface(IID_ICLRRuntimeInfo, (LPVOID *) &pRuntime);
if (FAILED(hr))
{
pUnk->Release();
pUnk = NULL;
continue;
}
WCHAR wszVersion[30];
DWORD cchVersion = sizeof(wszVersion)/sizeof(wszVersion[0]);
hr = pRuntime->GetVersionString(wszVersion, &cchVersion);
if (SUCCEEDED(hr) &&
(cchVersion >= 3) &&
((wszVersion[0] == L'v') || (wszVersion[0] == L'V')) &&
((wszVersion[1] >= L'4') || (wszVersion[2] != L'.')))
{
hr = pRuntime->GetInterface(CLSID_CLRProfiling, IID_ICLRProfiling, (LPVOID *)&pProfiling);
if (SUCCEEDED(hr))
{
hr = pProfiling->AttachProfiler(
dwProfileeProcID,
dwMillisecondsTimeout,
profilerCLSID,
wszProfilerPath,
pvClientData,
cbClientData
);
pProfiling->Release();
pProfiling = NULL;
break;
}
}
pRuntime->Release();
pRuntime = NULL;
pUnk->Release();
pUnk = NULL;
}
Cleanup:
if (pProfiling != NULL)
{
pProfiling->Release();
pProfiling = NULL;
}
if (pRuntime != NULL)
{
pRuntime->Release();
pRuntime = NULL;
}
if (pUnk != NULL)
{
pUnk->Release();
pUnk = NULL;
}
if (pEnum != NULL)
{
pEnum->Release();
pEnum = NULL;
}
if (pMetaHost != NULL)
{
pMetaHost->Release();
pMetaHost = NULL;
}
if (hModule != NULL)
{
FreeLibrary(hModule);
hModule = NULL;
}
return hr;
}
アタッチ後のプロファイラーの初期化
.NET Framework 4 以降では、ICorProfilerCallback::Initialize メソッドに対応するアタッチ メソッドとして ICorProfilerCallback3::InitializeForAttach メソッドが用意されています。 InitializeForAttach は、アタッチ操作後にその状態を初期化する機会をプロファイラーに与えるために、CLR によって呼び出されます。 このコールバックで、プロファイラーは ICorProfilerInfo::SetEventMask メソッドを呼び出して 1 つ以上のイベントを要求します。 ICorProfilerCallback::Initialize メソッドで行われる呼び出しとは異なり、InitializeForAttach から行われる SetEventMask メソッドの呼び出しでは、COR_PRF_ALLOWABLE_AFTER_ATTACH ビットマスクにあるビットしか設定できません (「アタッチの制限事項」を参照してください)。
InitializeForAttach からエラー コードを受け取ると、CLR は Windows アプリケーション イベント ログにエラーを記録し、プロファイラー コールバック インターフェイスを解放してプロファイラーをアンロードします。 AttachProfiler も InitializeForAttach と同じエラー コードを返します。
プロファイラーのアタッチの完了
.NET Framework 4 以降では、InitializeForAttach メソッドの後に ICorProfilerCallback3::ProfilerAttachComplete コールバックが発行されます。 ProfilerAttachComplete は、InitializeForAttach メソッドでプロファイラーが要求したコールバックがアクティブになり、通知がなくても関連付けられた ID でプロファイラーがキャッチアップを実行できることを示します。
たとえば、プロファイラーが InitializeForAttach コールバックで COR_PRF_MONITOR_MODULE_LOADS を設定したとします。 InitializeForAttach からプロファイラーに制御が戻ると、CLR によって ModuleLoad コールバックが有効にされ、ProfilerAttachComplete コールバックが発行されます。 プロファイラーはその後、ICorProfilerInfo3::EnumModules メソッドを使用して、ProfilerAttachComplete コールバックで現在読み込まれているすべてのモジュールの ModuleID を列挙できます。 列挙時に読み込まれる新しいモジュールに対しても、ModuleLoad イベントが発行されます。 重複が発生した場合は、プロファイラーで適切に対処する必要があります。 たとえば、プロファイラーのアタッチ中にモジュールが読み込まれた場合、ICorProfilerInfo3::EnumModules から返される列挙と ModuleLoadFinished コールバックによって、ModuleID が 2 回取得される可能性があります。
プロファイラーのデタッチ
アタッチ要求はトリガー プロセスによってアウトプロセスで行われる必要がありますが、 デタッチ要求は ICorProfilerInfo3::RequestProfilerDetach メソッドを呼び出すときにプロファイラー DLL によってインプロセスで行われる必要があります。 デタッチ要求をアウトプロセスで (たとえば、カスタム UI から) 行う場合は、プロファイラー開発者はプロセス間通信メカニズムを作成して、RequestProfilerDetach を呼び出すように (ターゲット アプリケーションと共に実行される) プロファイラー DLL に指示する必要があります。
RequestProfilerDetach は、制御を返す前に、同期的にいくつかの処理 (プロファイラー DLL にイベントが送信されないようにするための内部状態の設定など) を行います。 残りの処理は、RequestProfilerDetach から制御が戻った後に非同期的に行われます。 この残りの処理は個別のスレッド (DetachThread) で実行されます。これには、ポーリングや、すべてのプロファイラー コードがすべてのアプリケーション スレッドのスタックからポップされたことの確認などが含まれます。 RequestProfilerDetach が完了すると、プロファイラーは 1 つの最後のコールバック (ICorProfilerCallback3::ProfilerDetachSucceeded) を受け取ります。その後に、CLR によってプロファイラーのインターフェイスとコードのヒープが解放され、プロファイラーの DLL がアンロードされます。
デタッチ時のスレッドに関する考慮事項
実行制御はさまざまな方法でプロファイラーに渡すことができます。 ただし、プロファイラーのアンロード後に制御を渡すことはできないため、そのような動作が発生しないようにプロファイラーとランタイムの両方で注意する必要があります。
ランタイムでは、スタックにプロファイラー コードを含む (またはすぐに含む可能性がある)、プロファイラーによって作成またはハイジャックされたスレッドを認識しません。 そのため、プロファイラーは、ICorProfilerInfo3::RequestProfilerDetach を呼び出す前に、作成したすべてのスレッドを確実に終了し、すべてのサンプリングまたはハイジャックを停止する必要があります。 この規則の例外として、ICorProfilerInfo3::RequestProfilerDetach を呼び出すために作成したスレッドは使用できます。 ただし、このスレッドでは、LoadLibrary 関数および FreeLibraryAndExitThread 関数を使用して、プロファイラー DLL 内へのスレッド自身の参照を保持する必要があります (詳細については次のセクションを参照してください)。
プロファイラーが RequestProfilerDetach を呼び出した後、ランタイムは、プロファイラーを完全にアンロードするときに、どのスレッドのスタックにも ICorProfilerCallback メソッドによってプロファイラー コードが残されないようにする必要があります。
デタッチの詳細なステップ
プロファイラーのデタッチは、次のステップで行われます。
プロファイラーで、作成したすべてのスレッドを終了し、すべてのサンプリングとハイジャックを停止してから、ICorProfilerInfo3::RequestProfilerDetach を呼び出します。ただし、次の例外があります。
プロファイラーでは、ICorProfilerInfo3::RequestProfilerDetach メソッドを呼び出すために、プロファイラー自身のスレッドの 1 つ (CLR が作成したスレッドではなく) を使用してデタッチを実装する場合があります。 プロファイラーでこの動作を実装する場合、このプロファイラー スレッドについては、RequestProfilerDetach メソッドを呼び出すとき (このメソッドを呼び出すスレッドであるため) に終了できます。 ただし、プロファイラーでこの実装を行う場合は、次のようにする必要があります。
RequestProfilerDetach を呼び出すために残すスレッドは、プロファイラー DLL 内へのスレッド自身の参照を保持する必要があります (自身の LoadLibrary 関数を呼び出します)。
スレッドが RequestProfilerDetach を呼び出した後、すぐに FreeLibraryAndExitThread 関数を呼び出してプロファイラー DLL の保持を解除し、終了します。
RequestProfilerDetach が、プロファイラーが無効になったと見なされるようにランタイムの内部状態を設定します。 これにより、以降、コールバック メソッドでプロファイラーが呼び出されなくなります。
RequestProfilerDetach が、残りのコールバック メソッドがすべてのスレッドのスタックからポップされたことを確認するように DetachThread に指示します。
RequestProfilerDetach から制御が戻り、デタッチが正常に開始されたかどうかを示すステータス コードが返されます。
この時点で、CLR によって、ICorProfilerInfo、ICorProfilerInfo2、および ICorProfilerInfo3 の各インターフェイス メソッドを使用したプロファイラーからの CLR の呼び出しが禁止されます。 このような呼び出しは直ちに失敗し、CORPROF_E_PROFILER_DETACHING HRESULT が返されます。
プロファイラーがスレッドを返すか終了します。 プロファイラーがそれ自身のスレッドを使用してこの RequestProfilerDetach を呼び出した場合、そのプロファイラーは、このスレッドから FreeLibraryAndExitThread を呼び出す必要があります。 プロファイラーが CLR スレッドを使用して (つまり、コールバック内から) RequestProfilerDetach を呼び出した場合、そのプロファイラーは CLR に制御を返します。
DetachThread はその間、残りのコールバック メソッドがすべてのスレッドのスタックからポップされたことを確認する処理を続けます。
DetachThread は、どのスレッドのスタックにもコールバックが残っていないことを確認すると、ICorProfilerCallback3::ProfilerDetachSucceeded メソッドを呼び出します。 プロファイラーは、ProfilerDetachSucceeded メソッドで最小限の処理だけ行って、すぐに制御を返します。
DetachThread が、最後の Release() をプロファイラーの ICorProfilerCallback インターフェイスで実行します。
DetachThread が、FreeLibrary 関数をプロファイラーの DLL で呼び出します。
デタッチの制限事項
次のシナリオではデタッチはサポートされません。
プロファイラーが変更できないイベント フラグを設定している場合。
プロファイラーが enter/leave/tailcall (ELT) プローブを使用している場合。
プロファイラーが (たとえば、SetILFunctionBody メソッドを呼び出して) Microsoft Intermediate Language (MSIL) インストルメンテーションを使用している場合。
これらのシナリオでプロファイラーのデタッチを実行しようとすると、RequestProfilerDetach からエラーを示す HRESULT が返されます。
デバッグ
プロファイラーのアタッチとデタッチの機能は、アプリケーションのデバッグの妨げになることはありません。 アプリケーションのデバッグ セッション中も、いつでもプロファイラーのアタッチとデタッチを行うことができます。 反対に、プロファイラーのアタッチを行っている (その後にデタッチを行う) アプリケーションでも、いつでもデバッガーのアタッチとデタッチを行うことができます。 ただし、デバッガーによって中断されているアプリケーションは、プロファイラーのアタッチ要求に応答できないためプロファイリングできません。