From eed61abf761ed5573109efcbe38468eae699ae19 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 12 Apr 2018 08:07:27 -0700 Subject: [PATCH 1/3] Add displayName to VSTS tasks --- .vsts-ci.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.vsts-ci.yml b/.vsts-ci.yml index 6f61723ee..20793844f 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -27,11 +27,13 @@ steps: condition: and(succeeded(), ne(variables['Hosted'], 'true')) - task: MicroBuildIBCMergePlugin@0 + displayName: MicroBuild IBCMerge Plugin inputs: branch: lab/vsuvscore condition: and(succeeded(), ne(variables['Hosted'], 'true')) - task: MicroBuildSigningPlugin@1 + displayName: MicroBuild Signing Plugin inputs: signType: $(SignType) esrpSigning: true @@ -53,6 +55,7 @@ steps: displayName: Build Visual Studio solution - task: MicroBuildCleanup@1 + displayName: MicroBuild Cleanup condition: succeededOrFailed() ## The rest of these steps are for deployment and skipped for PR builds @@ -65,6 +68,7 @@ steps: # condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - task: PublishBuildArtifacts@1 + displayName: Publish build_logs inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)/build_logs ArtifactName: build_logs @@ -72,6 +76,7 @@ steps: condition: succeededOrFailed() - task: CopyFiles@1 + displayName: Collecting symbols artifacts inputs: SourceFolder: bin Contents: | @@ -80,10 +85,10 @@ steps: Microsoft.VisualStudio.Threading.Analyzers/$(BuildConfiguration)/**/Microsoft.VisualStudio.Threading.Analyzers.dll Microsoft.VisualStudio.Threading.Analyzers/$(BuildConfiguration)/**/Microsoft.VisualStudio.Threading.Analyzers.pdb TargetFolder: $(Build.ArtifactStagingDirectory)/symbols - displayName: Collecting symbols artifacts condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - task: PublishBuildArtifacts@1 + displayName: Publish symbols inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)/symbols ArtifactName: symbols @@ -91,15 +96,16 @@ steps: condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - task: CopyFiles@1 + displayName: Collecting deployables inputs: Contents: | bin/**/$(BuildConfiguration)/**/*.nupkg TargetFolder: $(Build.ArtifactStagingDirectory)/deployables flattenFolders: true - displayName: Collecting deployables condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - task: PublishBuildArtifacts@1 + displayName: Publish deployables inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)/deployables ArtifactName: deployables @@ -107,6 +113,7 @@ steps: condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - task: PublishSymbols@2 + displayName: Archive symbols inputs: SymbolsFolder: $(Build.ArtifactStagingDirectory)/symbols SearchPattern: '**/*.pdb' @@ -115,6 +122,7 @@ steps: condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - task: NuGetCommand@2 + displayName: Push NuGet packages inputs: command: push searchPatternPush: '$(Build.SourcesDirectory)\bin\**\$(BuildConfiguration)\**\*.nupkg;!**\*.symbols.nupkg;!**/VS.*.nupkg' From db5d0e999b9969ec9d0eae4d1f1cd98fb2025edc Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 12 Apr 2018 09:37:41 -0700 Subject: [PATCH 2/3] Pin NuGet version --- .appveyor.yml | 4 +++- .vsts-ci.yml | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 59e028b5d..3a90d97f4 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -18,8 +18,10 @@ environment: VisualStudioVersion: 15.0 TreatWarningsAsErrors: true CodeAnalysisTreatWarningsAsErrors: true +install: + appveyor DownloadFile https://dist.nuget.org/win-x86-commandline/v4.6.1/nuget.exe before_build: - msbuild src\Microsoft.VisualStudio.Threading.sln /nologo /m /v:quiet /t:restore + nuget restore src build_script: - msbuild src\Microsoft.VisualStudio.Threading.sln /nologo /m /fl /v:minimal /t:build,pack test_script: diff --git a/.vsts-ci.yml b/.vsts-ci.yml index 20793844f..b28f17dc5 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -26,6 +26,11 @@ steps: displayName: Purge package caches condition: and(succeeded(), ne(variables['Hosted'], 'true')) +- task: NuGetToolInstaller@0 + displayName: Pin nuget.exe version + inputs: + versionSpec: 4.6.1 + - task: MicroBuildIBCMergePlugin@0 displayName: MicroBuild IBCMerge Plugin inputs: From 04a0d41d0f02bf65f4a47dfc83d222587a5d954e Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 12 Apr 2018 12:19:33 -0700 Subject: [PATCH 3/3] Add source and docs for !dumpasync extension --- .appveyor.yml | 12 +- .vsts-ci.yml | 35 +- doc/async_hang.md | 31 + doc/dumpasync.md | 71 ++ doc/index.md | 1 + src/AsyncDebugTools/AsyncDebugTools.def | 7 + src/AsyncDebugTools/AsyncDebugTools.vcxproj | 226 ++++++ .../AsyncDebugTools.vcxproj.filters | 81 ++ src/AsyncDebugTools/CpsNfeAnalyzer.cpp | 168 ++++ src/AsyncDebugTools/DML.cpp | 37 + src/AsyncDebugTools/DML.h | 5 + src/AsyncDebugTools/DbgExts.cpp | 40 + src/AsyncDebugTools/DbgExts.h | 7 + src/AsyncDebugTools/Helpers.cpp | 764 ++++++++++++++++++ src/AsyncDebugTools/Helpers.h | 112 +++ src/AsyncDebugTools/ThreadPoolExhausted.cpp | 143 ++++ src/AsyncDebugTools/ThreadPoolExhausted.h | 11 + src/AsyncDebugTools/dllmain.cpp | 18 + src/AsyncDebugTools/dumpasync.cpp | 313 +++++++ src/AsyncDebugTools/dumpversions.cpp | 143 ++++ src/AsyncDebugTools/outputcallbacks.cpp | 101 +++ src/AsyncDebugTools/outputcallbacks.h | 45 ++ src/AsyncDebugTools/packages.config | 7 + src/AsyncDebugTools/stdafx.cpp | 8 + src/AsyncDebugTools/stdafx.h | 26 + src/AsyncDebugTools/targetver.h | 8 + src/Directory.Build.props | 2 +- src/Microsoft.VisualStudio.Threading.sln | 42 +- 28 files changed, 2456 insertions(+), 8 deletions(-) create mode 100644 doc/async_hang.md create mode 100644 doc/dumpasync.md create mode 100644 src/AsyncDebugTools/AsyncDebugTools.def create mode 100644 src/AsyncDebugTools/AsyncDebugTools.vcxproj create mode 100644 src/AsyncDebugTools/AsyncDebugTools.vcxproj.filters create mode 100644 src/AsyncDebugTools/CpsNfeAnalyzer.cpp create mode 100644 src/AsyncDebugTools/DML.cpp create mode 100644 src/AsyncDebugTools/DML.h create mode 100644 src/AsyncDebugTools/DbgExts.cpp create mode 100644 src/AsyncDebugTools/DbgExts.h create mode 100644 src/AsyncDebugTools/Helpers.cpp create mode 100644 src/AsyncDebugTools/Helpers.h create mode 100644 src/AsyncDebugTools/ThreadPoolExhausted.cpp create mode 100644 src/AsyncDebugTools/ThreadPoolExhausted.h create mode 100644 src/AsyncDebugTools/dllmain.cpp create mode 100644 src/AsyncDebugTools/dumpasync.cpp create mode 100644 src/AsyncDebugTools/dumpversions.cpp create mode 100644 src/AsyncDebugTools/outputcallbacks.cpp create mode 100644 src/AsyncDebugTools/outputcallbacks.h create mode 100644 src/AsyncDebugTools/packages.config create mode 100644 src/AsyncDebugTools/stdafx.cpp create mode 100644 src/AsyncDebugTools/stdafx.h create mode 100644 src/AsyncDebugTools/targetver.h diff --git a/.appveyor.yml b/.appveyor.yml index 3a90d97f4..b48b8ebaf 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -23,7 +23,9 @@ install: before_build: nuget restore src build_script: -- msbuild src\Microsoft.VisualStudio.Threading.sln /nologo /m /fl /v:minimal /t:build,pack +- msbuild src\Microsoft.VisualStudio.Threading.sln /p:platform="Any CPU" /nologo /m /v:minimal /t:build,pack /bl:msbuild.binlog +- msbuild src\Microsoft.VisualStudio.Threading.sln /p:platform=x64 /nologo /m /v:minimal /bl:msbuild_x64.binlog +- msbuild src\Microsoft.VisualStudio.Threading.sln /p:platform=x86 /nologo /m /v:minimal /bl:msbuild_x86.binlog test_script: - >- md "bin\%configuration%" @@ -66,7 +68,11 @@ test_script: artifacts: - path: bin\**\*.nupkg name: NuGet Package -- path: msbuild.log - name: Build log +- path: bin\AsyncDebugTools\x64\$(Configuration)\AsyncDebugTools.dll + name: WinDbg extension (x64) +- path: bin\AsyncDebugTools\x86\$(Configuration)\AsyncDebugTools.dll + name: WinDbg extension (x86) +- path: msbuild*.binlog + name: Build logs - path: bin\**\testresults* name: Test results diff --git a/.vsts-ci.yml b/.vsts-ci.yml index b28f17dc5..8d1ab3f85 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -48,17 +48,32 @@ steps: inputs: restoreSolution: '**\*.sln' feedsToUse: config - nugetConfigPath: nuget.config displayName: Nuget restore packages - task: VSBuild@1 inputs: vsVersion: 15.0 msbuildArgs: /t:build,pack /m /bl:"$(Build.ArtifactStagingDirectory)/build_logs/msbuild.binlog" - platform: $(BuildPlatform) + platform: Any CPU configuration: $(BuildConfiguration) displayName: Build Visual Studio solution +- task: VSBuild@1 + inputs: + vsVersion: 15.0 + msbuildArgs: /t:build /m /bl:"$(Build.ArtifactStagingDirectory)/build_logs/msbuild_x86.binlog" + platform: x86 + configuration: $(BuildConfiguration) + displayName: Build AsyncDebugTools x86 + +- task: VSBuild@1 + inputs: + vsVersion: 15.0 + msbuildArgs: /t:build /m /bl:"$(Build.ArtifactStagingDirectory)/build_logs/msbuild_x64.binlog" + platform: x64 + configuration: $(BuildConfiguration) + displayName: Build AsyncDebugTools x64 + - task: MicroBuildCleanup@1 displayName: MicroBuild Cleanup condition: succeededOrFailed() @@ -89,6 +104,10 @@ steps: Microsoft.VisualStudio.Threading/$(BuildConfiguration)/**/Microsoft.VisualStudio.Threading.pdb Microsoft.VisualStudio.Threading.Analyzers/$(BuildConfiguration)/**/Microsoft.VisualStudio.Threading.Analyzers.dll Microsoft.VisualStudio.Threading.Analyzers/$(BuildConfiguration)/**/Microsoft.VisualStudio.Threading.Analyzers.pdb + AsyncDebugTools/x86/$(BuildConfiguration)/AsyncDebugTools.dll + AsyncDebugTools/x86/$(BuildConfiguration)/AsyncDebugTools.pdb + AsyncDebugTools/x64/$(BuildConfiguration)/AsyncDebugTools.dll + AsyncDebugTools/x64/$(BuildConfiguration)/AsyncDebugTools.pdb TargetFolder: $(Build.ArtifactStagingDirectory)/symbols condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) @@ -101,7 +120,7 @@ steps: condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - task: CopyFiles@1 - displayName: Collecting deployables + displayName: Collecting deployable packages inputs: Contents: | bin/**/$(BuildConfiguration)/**/*.nupkg @@ -109,6 +128,16 @@ steps: flattenFolders: true condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) +- task: CopyFiles@1 + displayName: Collecting deployable binaries + inputs: + Contents: | + x86/$(BuildConfiguration)/AsyncDebugTools.dll + x64/$(BuildConfiguration)/AsyncDebugTools.dll + SourceFolder: bin/AsyncDebugTools + TargetFolder: $(Build.ArtifactStagingDirectory)/deployables + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + - task: PublishBuildArtifacts@1 displayName: Publish deployables inputs: diff --git a/doc/async_hang.md b/doc/async_hang.md new file mode 100644 index 000000000..e82fea5ec --- /dev/null +++ b/doc/async_hang.md @@ -0,0 +1,31 @@ +# Async hang investigations + +An "asynchronous hang" is one in which the UI thread is synchronously blocked waiting for an asynchronous operation to complete. Because the operation is an async one, the callstack for operation that must complete to resolve the hang may not be evident on the UI thread or any other. + +Finding the cause for an asynchronous hang can be challenging, but it can be done with a live, hung process or with a DMP file that contains the application heap, using WinDbg. The following are the recommended steps for diagnosing the problem: + +1. Look at the application's UI thread to understand what scenario is blocking. + 1. Note whether the callstack includes a `JoinableTaskFactory.WaitSynchronously` frame. If it does not, but some other synchronously blocking frame does appear, this constitutes a violation of [rule #2][ThreadingRules]. + + **Resolution:** Replace uses of `Task.Wait()`, `Task.Result`, `WaitHandle.WaitOne()` or other sync blocking primitives with `JoinableTask.Join()` or `JoinableTaskFactory.Run(Func)`. + +1. Check background threads for any that are blocked on an STA COM call that's waiting to marshal to the UI thread. If you find any, and they are performing an operation that the UI thread is blocked waiting to complete, the STA switch is probably failing. + + **Resolution:** Modify the code running on the background thread to switch to the UI thread before making the call to the STA COM object, by using `await JoinableTaskFactory.SwitchToMainThreadAsync();` per [rule #1][ThreadingRules]. + +1. Use the [`!dumpasync` WinDbg extension](dumpasync.md) to reveal the async methods that do not appear on thread callstacks to identify why an async method did not complete. + +## Visual Studio specific considerations + +If the hang shows a JoinableTaskFactory.WaitSynchronously frame near the top of the callstack on the UI thread, and if the hung application is Visual Studio itself, and if CPS was loaded before the hang, there is probably a DGML file on the disk of the repro machine that contains a hang report that shows you what went wrong. That way you may be able to avoid any WinDBG heap scouring manual investigation. Look for directories with this pattern: `%temp%\CPS.*` where `*` is a random GUID. + If you don't have access to the TEMP directory, you can also get the hang report from the dump file itself: + + ``` + !dumpheap -stat -type CpsJoinableTaskContext + !dumpheap -mt + !do + !do
+ ``` + +[WinDbg]: https://aka.ms/windbg-direct-download +[ThreadingRules]: threading_rules.md diff --git a/doc/dumpasync.md b/doc/dumpasync.md new file mode 100644 index 000000000..41df3979d --- /dev/null +++ b/doc/dumpasync.md @@ -0,0 +1,71 @@ +# The `!dumpasync` WinDbg extension + +The `!dumpasync` command is a WinDbg extension that produces a list of async method callstacks +that may not appear on thread callstacks. This information is useful when diagnosing the cause +for an app hang that is waiting for async methods to complete. + +## Tool acquisition + +Using this tool requires that you download and install [WinDbg][WinDbg], which can attach to a running process or DMP file. The `!dumpasync` extension is only available for WinDbg. + +The `!dumpasync` extension itself is exported from the `AsyncDebugTools.dll` library, which you can acquire from [our releases page](https://github.com/Microsoft/vs-threading/releases). + +## Usage + +Use WinDbg to open your DMP file or attach to your process, then execute the following commands. +Be sure to use either the x86 or x64 version of the `AsyncDebugTools.dll` library, consistent with the version of WinDbg you are running. + +```windbg +.load path\to\AsyncDebugTools.dll +!dumpasync +``` + +The `!dumpasync` command will produce output such as: + +``` +07494c7c <0> Microsoft.Cascade.Rpc.RpcSession+d__49 +.07491d10 <1> Microsoft.Cascade.Agent.WorkspaceService+d__28 +..073c8be4 <5> Microsoft.Cascade.Agent.WorkspaceService+d__22 +...073b7e94 <0> Microsoft.Cascade.Rpc.RpcDispatcher`1+<>c__DisplayClass23_2+<b__2>d[[Microsoft.Cascade.Contracts.IWorkspaceService, Microsoft.Cascade.Common]] +....073b60e0 <0> Microsoft.Cascade.Rpc.RpcServiceUtil+d__3 +.....073b366c <0> Microsoft.Cascade.Rpc.RpcSession+d__42 +......073b815c <0> Microsoft.Cascade.Rpc.RpcSession+<>c__DisplayClass40_1+<b__0>d + +073cdb7c <0> StreamJsonRpc.ReadBufferingStream+d__27 +.073cdc6c <0> StreamJsonRpc.HeaderDelimitedMessageHandler+d__20 +..073cdd84 <1> StreamJsonRpc.DelimitedMessageHandler+d__22 +...073cd93c <0> Microsoft.Cascade.Rpc.Json.JsonRpcStream+d__11 +....073cd908 <0> Microsoft.Cascade.Rpc.RpcSession+d__39 + +0730c750 <0> Microsoft.Cascade.Rpc.NamedPipeServer+d__23 +.0730c17c <0> Microsoft.Cascade.Rpc.PipeRpcServer+d__2 +..0730bff4 <0> Microsoft.Cascade.Agent.Server+d__15 +...072ef4a8 <1> Microsoft.Cascade.Agent.Program+d__24 + +07496420 <0> System.Threading.SemaphoreSlim+d__32 +.07496208 <0> Microsoft.Cascade.Rpc.RpcSession+d__51 +..0731c7b4 <0> Microsoft.Cascade.Rpc.RpcSession+d__38 +...0730b6b4 <1> Microsoft.Cascade.Rpc.RpcServer+d__8 + +072fa138 <0> Microsoft.Cascade.Tracing.LogFileTraceListener+d__16 +0730a138 <0> Microsoft.Cascade.Rpc.NamedPipeServer+d__29 +``` + +The output above is a set of stacks – not exactly callstacks, but actually "continuation stacks". +A continuation stack is synthesized based on what code has 'awaited' the call to an async method. It's possible that the `Task` returned by an async method was awaited from multiple places (e.g. the `Task` was stored in a field, then awaited by multiple interested parties). When there are multiple awaiters, the stack can branch +and show multiple descendents of a given frame. The stacks above are therefore actually "trees", and the leading +dots at each frame helps recognize when trees have multiple branches. + +If an async method is invoked but not awaited on, the caller won't appear in the continuation stack. + +In the example output above, there are several stacks shown. The two frames at the bottom are two lone async method frames that have no continuations. +In the first stack above, `SendRequestAsync` method is on top, so it is the method that contains the last `await` that didn't resume. The top frame is usually where you want to investigate the cause of an async hang. The rest of the frames in the stack give you a clue as to why this top frame matters to the rest of your application. +​The `<0>` you see on the top frame is the state of the async state machine that tracks the async method on that frame. This state field should be interpreted based on the following table: + +| Value | Meaning | +| -- | -- | +| -2 | The async method has run to completion. This state machine is likely unrooted and subject to garbage collection. Observing this is rare because the `!dumpasync` extension filters these out as noise. +| -1 | The async method is currently executing (you should find it on a real thread's callstack somewhere). +| >= 0 | The 0-index into which "await" has most recently yielded. The list of awaits for the method are in strict syntax appearance order. That is, regardless of code execution, if branching, etc., it's the index into which await in code syntax order has yielded. For example, if you position the caret at the top of the method definition and search for "await ", and count how many times you hit a match, starting from 0, when you arrive at the number that you found in the state field, you've found the await that has most recently yielded. Note that when the code being debugged is compiled with certain Dev14 prerelease versions of the Roslyn compiler, this index is 1-based instead of 0-based. + +[WinDbg]: https://aka.ms/windbg-direct-download diff --git a/doc/index.md b/doc/index.md index 952c24054..7a9ac0398 100644 --- a/doc/index.md +++ b/doc/index.md @@ -9,4 +9,5 @@ ## Performance and responsiveness investigation techniques +* [Async hang investigations](async_hang.md) * [Investigating Threadpool starvation issues](threadpool_starvation.md) diff --git a/src/AsyncDebugTools/AsyncDebugTools.def b/src/AsyncDebugTools/AsyncDebugTools.def new file mode 100644 index 000000000..299ae6cba --- /dev/null +++ b/src/AsyncDebugTools/AsyncDebugTools.def @@ -0,0 +1,7 @@ +EXPORTS + cpsnfeanalyze + dumpasync + dumpversions + DebugExtensionNotify + DebugExtensionInitialize + DebugExtensionUninitialize diff --git a/src/AsyncDebugTools/AsyncDebugTools.vcxproj b/src/AsyncDebugTools/AsyncDebugTools.vcxproj new file mode 100644 index 000000000..57ed74fef --- /dev/null +++ b/src/AsyncDebugTools/AsyncDebugTools.vcxproj @@ -0,0 +1,226 @@ + + + + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924} + Win32Proj + AsyncDebugTools + true + + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + true + ..\..\bin\$(MSBuildProjectName)\x86\$(Configuration)\ + ..\..\obj\$(MSBuildProjectName)\x86\$(Configuration)\ + + + true + ..\..\bin\$(MSBuildProjectName)\$(Platform)\$(Configuration)\ + ..\..\obj\$(MSBuildProjectName)\$(Platform)\$(Configuration)\ + + + false + ..\..\bin\$(MSBuildProjectName)\x86\$(Configuration)\ + ..\..\obj\$(MSBuildProjectName)\x86\$(Configuration)\ + + + false + ..\..\bin\$(MSBuildProjectName)\$(Platform)\$(Configuration)\ + ..\..\obj\$(MSBuildProjectName)\$(Platform)\$(Configuration)\ + + + + Use + Level3 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + MultiThreadedDebug + + + Windows + true + AsyncDebugTools.def + + + + + Use + Level3 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + MultiThreadedDebug + + + Windows + true + AsyncDebugTools.def + + + + + Level3 + Use + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + MultiThreaded + + + Windows + true + true + true + AsyncDebugTools.def + + + + + Level3 + Use + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + MultiThreaded + + + Windows + true + true + true + AsyncDebugTools.def + + + + + + + + + + + + + + + + + + + false + false + + + + + false + false + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + \ No newline at end of file diff --git a/src/AsyncDebugTools/AsyncDebugTools.vcxproj.filters b/src/AsyncDebugTools/AsyncDebugTools.vcxproj.filters new file mode 100644 index 000000000..81515b8a3 --- /dev/null +++ b/src/AsyncDebugTools/AsyncDebugTools.vcxproj.filters @@ -0,0 +1,81 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + + + + Header Files + + + Header Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Source Files + + + + \ No newline at end of file diff --git a/src/AsyncDebugTools/CpsNfeAnalyzer.cpp b/src/AsyncDebugTools/CpsNfeAnalyzer.cpp new file mode 100644 index 000000000..43b296018 --- /dev/null +++ b/src/AsyncDebugTools/CpsNfeAnalyzer.cpp @@ -0,0 +1,168 @@ +#include "stdafx.h" +#include "dbgexts.h" +#include "Helpers.h" +#include "ThreadPoolExhausted.h" + +bool DetectHangFailureID( + PDEBUG_CLIENT pDebugClient, + const std::string &strSOS, + std::string &strFailureID); + +HRESULT CALLBACK +cpsnfeanalyze(PDEBUG_CLIENT pDebugClient, PCSTR args) +{ + UNREFERENCED_PARAMETER(args); + + CComQIPtr srpControl(pDebugClient); + + std::string strSOS; + std::string strOutput; + if (!EnsureLoadSOS(pDebugClient, strSOS, strOutput)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to load SOS.dll: %s\n", strOutput.c_str()); + return E_FAIL; + } + + if (IsThreadPoolExhausted(pDebugClient, strSOS)) + { + return OnThreadPoolExhausted(pDebugClient, strSOS); + } + + // !Threads + HRESULT hr = Execute(pDebugClient, GetFullCommand(strSOS, "Threads"), strOutput); + if (FAILED(hr)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to run !Threads: %s\n", strOutput.c_str()); + return hr; + } + + std::vector exceptionInfos; + ExtractExceptionInfosFromThreadsOutput(strOutput, exceptionInfos); + if (exceptionInfos.empty()) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Not found any exception objects.\n"); + return E_FAIL; + } + + ExceptionInfo blamedException; + std::string strBlamedThreadClrStack; + for (const ExceptionInfo &ex : exceptionInfos) + { + // It is not common but possible to find more than one exceptions. + // To blame the correct exception, we will dump the CLR stacks on each thread and find 'WatsonErrorReport.Submit'. + if (SUCCEEDED(Execute(pDebugClient, "~" + ex.m_strThreadId + "e " + GetFullCommand(strSOS, "ClrStack"), strOutput))) + { + if (strOutput.find("WatsonErrorReport.Submit") != std::string::npos) + { + blamedException = ex; + strBlamedThreadClrStack = strOutput; + break; + } + } + } + + if (blamedException.m_strThreadId.empty()) + { + // Fallback to the first exception which is usually correct. + blamedException = exceptionInfos.front(); + Execute(pDebugClient, "~" + blamedException.m_strThreadId + "e " + GetFullCommand(strSOS, "ClrStack"), strBlamedThreadClrStack); + } + + std::string strBlamedThreadMixedStack; + Execute(pDebugClient, "~" + blamedException.m_strThreadId + "k", strBlamedThreadMixedStack); + + std::vector exceptionDetails; + CollectExceptionDetails(pDebugClient, strSOS, blamedException.m_strExceptionObject, exceptionDetails); + if (exceptionDetails.empty()) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to parse the details of exception: %s\n", blamedException.m_strExceptionObject.c_str()); + return E_FAIL; + } + + std::string strFailureID; + + // Special logic: if the exception was thrown by OnHangDetected, then it is a hang, so that switch to a different code path to generate the failure id. + if (exceptionDetails.size() == 1 && IsDueToHangDetected(exceptionDetails.front())) + { + if (!DetectHangFailureID(pDebugClient, strSOS, strFailureID)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to detect the frame that is blocking the main thread."); + return E_FAIL; + } + + // Always blame main thread. + Execute(pDebugClient, "~0e " + GetFullCommand(strSOS, "ClrStack"), strBlamedThreadClrStack); + Execute(pDebugClient, "~0k", strBlamedThreadMixedStack); + } + else + { + const ExceptionDetail &finalInnerException = exceptionDetails.back(); + std::string strBlamedSymbol = GuessBlamedSymbol(finalInnerException); + strFailureID = finalInnerException.m_strExceptionType + "_" + finalInnerException.m_strHResult + "_" + strBlamedSymbol; + } + + // Print Failure ID + srpControl->Output(DEBUG_OUTPUT_NORMAL, "FAILURE_BUCKET_ID: %s\n\n", strFailureID.c_str()); + + // Print the exceptions from the inner to outer + for (auto riter = exceptionDetails.crbegin(); riter != exceptionDetails.crend(); ++riter) + { + // Print long string similar to the style in Perl. + // + // <m_strExceptionObject; + srpControl->Output(DEBUG_OUTPUT_NORMAL, "<<%s\n%s%s\n\n", strAnchor.c_str(), riter->m_strRawText.c_str(), strAnchor.c_str()); + } + + // Print CLR stack + { + std::string strAnchor = "ClrStack"; + srpControl->Output(DEBUG_OUTPUT_NORMAL, "<<%s\n%s%s\n\n", strAnchor.c_str(), strBlamedThreadClrStack.c_str(), strAnchor.c_str()); + } + + // Print Mixed stack + { + std::string strAnchor = "MixedStack"; + srpControl->Output(DEBUG_OUTPUT_NORMAL, "<<%s\n%s%s\n\n", strAnchor.c_str(), strBlamedThreadMixedStack.c_str(), strAnchor.c_str()); + } + + return S_OK; +} + +bool DetectHangFailureID( + PDEBUG_CLIENT pDebugClient, + const std::string &strSOS, + std::string &strFailureID) +{ + strFailureID.clear(); + + std::string strMainThreadClrStack; + Execute(pDebugClient, "~0e " + GetFullCommand(strSOS, "ClrStack"), strMainThreadClrStack); + + std::string strLine; + std::stringstream ss(strMainThreadClrStack); + bool foundOnHangDetected = false; + while (std::getline(ss, strLine)) + { + if (!foundOnHangDetected) + { + if (strLine.find("OnHangDetected") != std::string::npos) + { + foundOnHangDetected = true; + } + + continue; + } + + std::string strFunc; + if (ParseClrStackFrame(strLine, strFunc) && !IsImmunized(strFunc)) + { + strFailureID = GetSymbolFromFrame(strFunc); + break; + } + } + + return !strFailureID.empty(); +} diff --git a/src/AsyncDebugTools/DML.cpp b/src/AsyncDebugTools/DML.cpp new file mode 100644 index 000000000..a8a1a0e99 --- /dev/null +++ b/src/AsyncDebugTools/DML.cpp @@ -0,0 +1,37 @@ +#include "stdafx.h" + +BOOL PreferDML(PDEBUG_CLIENT pDebugClient) +{ + BOOL bPreferDML = FALSE; + IDebugControl* pDebugControl; + if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), + (void **)& pDebugControl))) + { + ULONG ulOptions = 0; + if (SUCCEEDED(pDebugControl->GetEngineOptions(&ulOptions))) + { + bPreferDML = (ulOptions & DEBUG_ENGOPT_PREFER_DML); + } + pDebugControl->Release(); + } + return bPreferDML; +} + +BOOL AbilityDML(PDEBUG_CLIENT pDebugClient) +{ + BOOL bAbilityDML = FALSE; + IDebugAdvanced2* pDebugAdvanced2; + if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugAdvanced2), + (void **)& pDebugAdvanced2))) + { + HRESULT hr = 0; + if (SUCCEEDED(hr = pDebugAdvanced2->Request( + DEBUG_REQUEST_CURRENT_OUTPUT_CALLBACKS_ARE_DML_AWARE, + NULL, 0, NULL, 0, NULL))) + { + if (hr == S_OK) bAbilityDML = TRUE; + } + pDebugAdvanced2->Release(); + } + return bAbilityDML; +} diff --git a/src/AsyncDebugTools/DML.h b/src/AsyncDebugTools/DML.h new file mode 100644 index 000000000..44bc17d65 --- /dev/null +++ b/src/AsyncDebugTools/DML.h @@ -0,0 +1,5 @@ +#pragma once +#include "stdafx.h" + +BOOL PreferDML(PDEBUG_CLIENT pDebugClient); +BOOL AbilityDML(PDEBUG_CLIENT pDebugClient); diff --git a/src/AsyncDebugTools/DbgExts.cpp b/src/AsyncDebugTools/DbgExts.cpp new file mode 100644 index 000000000..80c84e81a --- /dev/null +++ b/src/AsyncDebugTools/DbgExts.cpp @@ -0,0 +1,40 @@ +// dbgexts.cpp + +#include "stdafx.h" +#include "dbgexts.h" + +extern "C" HRESULT CALLBACK +DebugExtensionInitialize(PULONG Version, PULONG Flags) +{ + *Version = DEBUG_EXTENSION_VERSION(EXT_MAJOR_VER, EXT_MINOR_VER); + *Flags = 0; // Reserved for future use. + return S_OK; +} + +extern "C" void CALLBACK +DebugExtensionNotify(ULONG Notify, ULONG64 Argument) +{ + UNREFERENCED_PARAMETER(Argument); + switch (Notify) + { + // A debugging session is active. The session may not necessarily be suspended. + case DEBUG_NOTIFY_SESSION_ACTIVE: + break; + // No debugging session is active. + case DEBUG_NOTIFY_SESSION_INACTIVE: + break; + // The debugging session has suspended and is now accessible. + case DEBUG_NOTIFY_SESSION_ACCESSIBLE: + break; + // The debugging session has started running and is now inaccessible. + case DEBUG_NOTIFY_SESSION_INACCESSIBLE: + break; + } + return; +} + +extern "C" void CALLBACK +DebugExtensionUninitialize(void) +{ + return; +} diff --git a/src/AsyncDebugTools/DbgExts.h b/src/AsyncDebugTools/DbgExts.h new file mode 100644 index 000000000..027e9afd3 --- /dev/null +++ b/src/AsyncDebugTools/DbgExts.h @@ -0,0 +1,7 @@ +// dbgexts.h + +#include +#include + +#define EXT_MAJOR_VER 1 +#define EXT_MINOR_VER 0 diff --git a/src/AsyncDebugTools/Helpers.cpp b/src/AsyncDebugTools/Helpers.cpp new file mode 100644 index 000000000..f998013d1 --- /dev/null +++ b/src/AsyncDebugTools/Helpers.cpp @@ -0,0 +1,764 @@ +#include "stdafx.h" +#include "Helpers.h" +#include "outputcallbacks.h" + +bool TryFindSOS( + _In_ IDebugClient* pDebugClient, + _Out_ std::string &strSOS) +{ + strSOS.clear(); + + std::string strOutput; + if (SUCCEEDED(Execute(pDebugClient, ".extmatch /e *\\sos*.dll Threads", strOutput)) + && !strOutput.empty()) + { + size_t pos = strOutput.find_first_of("\r\n"); + if (pos != std::string::npos) + { + strOutput.erase(pos); + } + + size_t dot = strOutput.rfind('.'); + if (dot != std::string::npos) + { + strSOS = strOutput.substr(1, dot - 1); + } + } + + return !strSOS.empty(); +} + +bool EnsureLoadSOS( + _In_ IDebugClient* pDebugClient, + _Out_ std::string &strSOS, + _Out_ std::string &strOutput) +{ + strSOS.clear(); + strOutput.clear(); + + if (TryFindSOS(pDebugClient, strSOS)) + { + return true; + } + + if (SUCCEEDED(Execute(pDebugClient, ".loadby sos.dll clr", strOutput))) + { + return TryFindSOS(pDebugClient, strSOS); + } + + return false; +} + +std::string GetFullCommand( + _In_ const std::string &strSOS, + _In_ const std::string &strCommand) +{ + return "!" + strSOS + "." + strCommand; +} + +HRESULT Execute( + _In_ IDebugClient* pDebugClient, + _In_ const std::string &strCommand, + _Out_ std::string &strOutput) +{ + HRESULT hr = E_FAIL; + strOutput.clear(); + + CComPtr srpNewClient; + if (SUCCEEDED(pDebugClient->CreateClient(&srpNewClient))) + { + COutputCallbacks *pOutputCallbacks = new COutputCallbacks(); + if (SUCCEEDED(srpNewClient->SetOutputMask(pOutputCallbacks->SupportedMask())) + && SUCCEEDED(srpNewClient->SetOutputCallbacks(pOutputCallbacks))) + { + CComQIPtr srpNewControl(srpNewClient); + if (srpNewControl) + { + hr = srpNewControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED, strCommand.c_str(), DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT); + if (pOutputCallbacks->BufferError()) + { + strOutput = pOutputCallbacks->BufferError(); + if (SUCCEEDED(hr)) + { + hr = E_FAIL; // Ensure returning failure when there is error message. + } + } + else if (pOutputCallbacks->BufferNormal()) + { + strOutput = pOutputCallbacks->BufferNormal(); + } + + // Print the command outputs if enable verbose mode. + { + CComQIPtr srpControl(pDebugClient); + srpControl->Output(DEBUG_OUTPUT_VERBOSE, "Execute command: %s\n%s\n", strCommand.c_str(), strOutput.c_str()); + } + } + + srpNewClient->SetOutputCallbacks(nullptr); + } + + pOutputCallbacks->Release(); + } + + return hr; +} + +void SplitString( + _In_ const std::string &strText, + _In_ const std::string &strDelim, + _Out_ std::vector &tokens) +{ + tokens.clear(); + std::string strCopyText(strText); + char *pszContext = nullptr; + char *pszToken = strtok_s(const_cast(strCopyText.c_str()), strDelim.c_str(), &pszContext); + while (pszToken != nullptr) + { + tokens.push_back(pszToken); + pszToken = strtok_s(nullptr, strDelim.c_str(), &pszContext); + } +} + +static std::regex g_addressPattern("[0-9a-fA-F]{8}"); + +void ExtractExceptionInfosFromThreadsOutput( + _In_ const std::string &strOutput, + _Out_ std::vector &exceptionInfos) +{ + static std::string strDelim = " ()"; + + exceptionInfos.clear(); + + std::istringstream iss(strOutput); + std::string strLine; + std::vector tokens; + while (std::getline(iss, strLine)) + { + SplitString(strLine, strDelim, tokens); + for (auto i = tokens.size() - 1; i >= 10; --i) + { + if (std::regex_match(tokens[i], g_addressPattern)) + { + exceptionInfos.push_back(ExceptionInfo{tokens[0], tokens[i]}); + break; + } + } + } +} + +bool TryParsePrintExceptionOutput( + _In_ const std::string &strOutput, + _Out_ ExceptionDetail &detail) +{ + detail = ExceptionDetail{}; + + std::istringstream iss(strOutput); + std::string strLine; + + // Exception object + { + if (!std::getline(iss, strLine)) + { + return false; + } + + std::smatch match; + if (!std::regex_search(strLine, match, g_addressPattern)) + { + return false; + } + + detail.m_strExceptionObject = match.str(); + } + + // Exception type + { + if (!std::getline(iss, strLine)) + { + return false; + } + + size_t pos = strLine.find_last_of(" :"); + if (pos == std::string::npos) + { + return false; + } + + detail.m_strExceptionType = strLine.substr(pos + 1); + } + + // Skip Message + if (!std::getline(iss, strLine)) + { + return false; + } + + // InnerException [optional] + { + if (!std::getline(iss, strLine)) + { + return false; + } + + std::smatch match; + if (std::regex_search(strLine, match, g_addressPattern)) + { + detail.m_strInnerExceptionObject = match.str(); + } + } + + // StackTrace (generated) + { + if (!std::getline(iss, strLine)) + { + return false; + } + + // SP IP Function + if (!std::getline(iss, strLine)) + { + return false; + } + + size_t pos = strLine.rfind("Function"); + if (pos == std::string::npos) + { + return false; + } + + while (std::getline(iss, strLine) && !strLine.empty()) + { + detail.m_stackTraces.push_back(strLine.substr(pos)); + } + + if (detail.m_stackTraces.empty()) + { + return false; + } + } + + // Skip StackTraceString + if (!std::getline(iss, strLine)) + { + return false; + } + + // HResult + { + if (!std::getline(iss, strLine)) + { + return false; + } + + size_t pos = strLine.find_last_of(" :"); + if (pos == std::string::npos) + { + return false; + } + + detail.m_strHResult = strLine.substr(pos + 1); + } + + detail.m_strRawText = strOutput; + + return true; +} + +void CollectExceptionDetails( + _In_ IDebugClient* pDebugClient, + _In_ const std::string &strSOS, + _In_ const std::string &strExceptionObject, + _Out_ std::vector &exceptionDetailsIncludingInnerExceptions) +{ + std::string strPrintExceptionCommand = GetFullCommand(strSOS, "PrintException"); + std::string strException = strExceptionObject; + while (!strException.empty()) + { + std::string strCommand = strPrintExceptionCommand + " " + strException; + strException.clear(); + + std::string strOutput; + if (SUCCEEDED(Execute(pDebugClient, strCommand, strOutput))) + { + ExceptionDetail detail; + if (TryParsePrintExceptionOutput(strOutput, detail)) + { + exceptionDetailsIncludingInnerExceptions.push_back(detail); + strException = detail.m_strInnerExceptionObject; + } + } + } +} + +static std::vector g_immunizedSymbolsStartWithIgnoreCase = +{ + "mscorlib", + "System", + "Microsoft_VisualStudio_Validation", + "Microsoft_VisualStudio_Threading", + "Microsoft_Collections_Immutable", + + "Microsoft.VisualStudio.Validation", + "Microsoft.VisualStudio.Threading", + "Microsoft.Collections.Immutable", + "Microsoft.VisualStudio.Shell.VsTaskLibraryHelper", + "Microsoft.VisualStudio.Services.VsTask", + "Microsoft.VisualStudio.ProjectSystem.ThreadHandlingMultithreaded", + "Microsoft.VisualStudio.ProjectSystem.VS.HResult", + "Microsoft.VisualStudio.Project.VisualC.VCProjectEngine.ApartmentMarshaler.Invoke", +}; + +static std::vector g_immunizedSymbolsContain = +{ + "ErrorUtilities.", + "ErrorHandler.", + "ThrowOnFailure", + "ThrowIfNotOnUIThread", + "HrInvoke", +}; + +bool IsImmunized(_In_ const std::string &strFrame) +{ + for (const std::string &strImmunizedSymbolStartWithIgnoreCase : g_immunizedSymbolsStartWithIgnoreCase) + { + if (strFrame.length() > strImmunizedSymbolStartWithIgnoreCase.length() + && 0 == _strnicmp(strFrame.c_str(), strImmunizedSymbolStartWithIgnoreCase.c_str(), strImmunizedSymbolStartWithIgnoreCase.length())) + { + return true; + } + } + + for (const std::string &strImmunizedSymbolsContain : g_immunizedSymbolsContain) + { + if (strFrame.find(strImmunizedSymbolsContain) != std::string::npos) + { + return true; + } + } + + return false; +} + +std::string GetSymbolFromFrame(_In_ const std::string &strFrame) +{ + size_t pos = strFrame.find('('); + std::string strSymbol = strFrame.substr(0, pos); + + // Normalize NGENed image name. + pos = strSymbol.find("_ni!"); + if (pos != std::string::npos) + { + strSymbol.erase(pos, 3); + } + + return strSymbol; +} + +std::string GuessBlamedSymbol( + _In_ const ExceptionDetail &detail) +{ + for (const std::string &strFrame : detail.m_stackTraces) + { + if (!IsImmunized(strFrame)) + { + return GetSymbolFromFrame(strFrame); + } + } + + // Otherwise, fallback to the last frame. + return GetSymbolFromFrame(detail.m_stackTraces.back()); +} + +bool IsDueToHangDetected( + _In_ const ExceptionDetail &detail) +{ + for (const std::string &stack : detail.m_stackTraces) + { + if (stack.find("OnHangDetected") != std::string::npos) + { + return true; + } + } + + return false; +} + +static std::regex g_framePattern("^[0-9a-f]{8} [0-9a-f]{8} ([a-z].+)", std::regex_constants::icase); + +bool ParseClrStackFrame( + _In_ const std::string &strFrame, + _Out_ std::string &strFunc) +{ + std::smatch matches; + if (std::regex_match(strFrame, matches, g_framePattern)) + { + strFunc = matches[1].str(); + return true; + } + + return false; +} + +bool ParseKStackFrame( + _In_ const std::string &strFrame, + _Out_ std::string &strFunc) +{ + static std::regex s_funcPattern("[0-9a-fA-F-]{8} [0-9a-fA-F-]{8} (.+!.+\\+0x[0-9a-fA-F]+)"); + std::smatch match; + if (std::regex_search(strFrame, match, s_funcPattern)) + { + strFunc = match[1].str(); + return true; + } + + return false; +} + +/* !threadpool +CPU utilization: 3% +Worker Thread: Total: 129 Running: 110 Idle: 0 MaxLimit: 2047 MinLimit: 12 +Work Request in Queue: 0 +-------------------------------------- +Number of Timers: 1 +-------------------------------------- +Completion Port Thread:Total: 4 Free: 4 MaxFree: 24 CurrentLimit: 4 MaxLimit: 1000 MinLimit: 12 +*/ +bool GetThreadPoolStatus( + _In_ const std::string &strThreadPoolOutput, + _Out_ ThreadPoolStatus &status) +{ + static std::regex s_workerThreadPattern("^Worker Thread: Total: (\\d+) Running: (\\d+)", std::regex::icase); + + std::stringstream ss(strThreadPoolOutput); + std::string strLine; + while (std::getline(ss, strLine)) + { + std::smatch match; + if (std::regex_search(strLine, match, s_workerThreadPattern)) + { + status.m_iRunningWorkers = std::atoi(match[2].str().c_str()); + return true; + } + } + + return false; +} + +int GetNumberOfThreads( + _In_ PDEBUG_CLIENT pDebugClient) +{ + static std::regex s_threadIdPattern("^\\s*(\\d+)\\s*Id:", std::regex::icase); + + std::string strOutput; + if (SUCCEEDED(Execute(pDebugClient, "~*", strOutput))) + { + std::stringstream ss(strOutput); + std::string strLine; + int threadId = -1; + while (std::getline(ss, strLine)) + { + std::smatch match; + if (std::regex_search(strLine, match, s_threadIdPattern)) + { + threadId = std::atoi(match[1].str().c_str()); + } + } + + return threadId + 1; + } + + return 0; +} + +struct FieldHeaderInfo +{ + std::string m_strName; + size_t m_iStart; + size_t m_iLength; +}; + +bool ParseFieldHeaders( + _In_ const std::string &strHeaders, + _Out_ std::vector &headers) +{ + static std::regex s_fieldHeaderPattern("^\\s+(\\S+)"); + + headers.clear(); + std::smatch match; + std::string::const_iterator begin = strHeaders.begin(); + while (std::regex_search(begin, strHeaders.end(), match, s_fieldHeaderPattern)) + { + FieldHeaderInfo header{}; + header.m_strName.assign(match[1].first, match[1].second); + header.m_iStart = begin - strHeaders.begin(); + header.m_iLength = match.length(); + headers.push_back(std::move(header)); + begin += match.length(); + } + + if (!headers.empty()) + { + headers.back().m_iLength = std::string::npos; + } + + return headers.size() == 8; +} + +void Trim(_Inout_ std::string &strText) +{ + size_t start = strText.find_first_not_of(' '); + if (start == std::string::npos) + { + strText.clear(); + return; + } + + size_t end = strText.find_last_not_of(' '); + if (end - start + 1 < strText.length()) + { + strText.erase(end + 1); + strText.erase(0, start); + } +} + +void SetFieldValue( + _Inout_ FieldInfo &field, + _In_ const std::string &strName, + _Inout_ std::string &&strValue) +{ + Trim(strValue); + + if (strName == "MT") + { + field.m_strMethodTable = strValue; + } + else if (strName == "Offset") + { + field.m_iOffset = atoi(strValue.c_str()); + } + else if (strName == "VT") + { + field.m_fIsValueType = atoi(strValue.c_str()) != 0; + } + else if (strName == "Value") + { + field.m_strValue = strValue; + } + else if (strName == "Name") + { + field.m_strName = strValue; + } +} + +/* !do 032f2398 +Name: Microsoft.VisualStudio.Services.Settings.SettingsPackage+d__1 +MethodTable: 6f6a03fc +EEClass: 6f58da24 +Size: 32(0x20) bytes +File: C:\windows\Microsoft.Net\assembly\GAC_MSIL\Microsoft.VisualStudio.Shell.UI.Internal\v4.0_14.0.0.0__b03f5f7f11d50a3a\Microsoft.VisualStudio.Shell.UI.Internal.dll +Fields: + MT Field Offset Type VT Attr Value Name +6cc19158 4001249 8 System.Int32 1 instance 1 <>1__state +6cc223fc 400124a c ...TaskMethodBuilder 1 instance 032f23a4 <>t__builder +6cbe4fd0 400124b 18 ...olean, mscorlib]] 1 instance 032f23b0 <>u__1 +6cc17670 400124c 4 System.Object 0 instance 0a4862f4 <>u__2 +*/ +bool ParseDumpObjectOutput( + _In_ const std::string &strDumpObject, + _Out_ ObjectInfo &objectInfo) +{ + objectInfo.m_strType.clear(); + objectInfo.m_fields.clear(); + + std::stringstream ss(strDumpObject); + std::string strLine; + bool fFoundFields = false; + while (std::getline(ss, strLine)) + { + size_t sep = strLine.find(':'); + if (sep != std::string::npos) + { + if (strncmp(strLine.c_str(), "Name", sep) == 0) + { + size_t start = strLine.find_first_not_of(' ', sep + 1); + if (start != std::string::npos) + { + objectInfo.m_strType.assign(strLine, start); + } + } + if (strncmp(strLine.c_str(), "String", sep) == 0) + { + size_t start = strLine.find_first_not_of(' ', sep + 1); + if (start != std::string::npos) + { + objectInfo.m_string.assign(strLine, start); + } + } + else if (strncmp(strLine.c_str(), "Fields", sep) == 0) + { + fFoundFields = true; + break; + } + } + } + + if (fFoundFields && !objectInfo.m_strType.empty()) + { + if (std::getline(ss, strLine)) + { + std::vector headers; + if (ParseFieldHeaders(strLine, headers)) + { + while (std::getline(ss, strLine)) + { + if (strLine.empty() || isspace(strLine[0]) || (int)strLine.length() <= headers.back().m_iStart ) + { + continue; + } + + int start = 0; + int end = 0; + int fieldIndex = 0; + FieldInfo fieldInfo{}; + for (const FieldHeaderInfo &headerInfo : headers) + { + // SOS doesn't align all fields with head correctly, causing the code reads wrong values and names. + if (fieldIndex < 6) + { + std::string strValue(strLine, headerInfo.m_iStart, headerInfo.m_iLength); + SetFieldValue(fieldInfo, headerInfo.m_strName, std::move(strValue)); + start = headerInfo.m_iStart + headerInfo.m_iLength; + } + else + { + while (start < (int)strLine.length() && isspace(strLine[start])) + { + start++; + } + + end = start; + while (end < (int)strLine.length() && !isspace(strLine[end])) + { + end++; + } + + std::string strValue(strLine, start, end - start); + SetFieldValue(fieldInfo, headerInfo.m_strName, std::move(strValue)); + start = end; + } + + fieldIndex++; + } + + objectInfo.m_fields.push_back(std::move(fieldInfo)); + } + } + } + } + + return !objectInfo.m_strType.empty() && !objectInfo.m_fields.empty(); +} + +/* !name2ee mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner +Module: 6c7f1000 +Assembly: mscorlib.dll +Token: 020008be +MethodTable: 6cbe4dbc +EEClass: 6c855780 +Name: System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner +*/ +bool ParseName2EEOutput( + _In_ const std::string &strName2EE, + _Out_ std::string &strMethodTable) +{ + std::stringstream ss(strName2EE); + std::string strLine; + while (std::getline(ss, strLine)) + { + size_t sep = strLine.find(':'); + if (sep != std::string::npos) + { + size_t start = strLine.find_first_not_of(' ', sep + 1); + if (start != std::string::npos) + { + if (strncmp(strLine.c_str(), "MethodTable", sep) == 0) + { + strMethodTable.assign(strLine, start); + return true; + } + } + } + } + + return false; +} + +bool GetFieldInfo( + _In_ const ObjectInfo &objectInfo, + _In_ const std::string &strFieldName, + _Out_ FieldInfo &fieldInfo) +{ + for (const FieldInfo &fi: objectInfo.m_fields) + { + if (fi.m_strName == strFieldName) + { + fieldInfo = fi; + return true; + } + } + + return false; +} + +bool FindFieldAndGetObjectInfo( + _In_ PDEBUG_CLIENT pDebugClient, + _In_ const std::string &strSOS, + _In_ const ObjectInfo &objectInfo, + _In_ const std::string &strFieldName, + _Out_ ObjectInfo &fieldObjectInfo) +{ + FieldInfo fieldInfo{}; + if (!GetFieldInfo(objectInfo, strFieldName, fieldInfo) || fieldInfo.m_strValue == "00000000") + { + return false; + } + + std::string strCommand; + if (fieldInfo.m_fIsValueType) + { + if (fieldInfo.m_strMethodTable == "00000000") + { + return false; + } + + strCommand.append("dumpvc ") + .append(fieldInfo.m_strMethodTable) + .append(" ") + .append(fieldInfo.m_strValue); + } + else + { + strCommand.append("do ") + .append(fieldInfo.m_strValue); + } + + std::string strOutput; + HRESULT hr = Execute(pDebugClient, GetFullCommand(strSOS, strCommand), strOutput); + if (FAILED(hr)) + { + CComQIPtr srpControl(pDebugClient); + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to run !%s: %s\n", strCommand.c_str(), strOutput.c_str()); + return false; + } + + if (!ParseDumpObjectOutput(strOutput, fieldObjectInfo)) + { + CComQIPtr srpControl(pDebugClient); + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to parse the output of !%s: %s\n", strCommand.c_str(), strOutput.c_str()); + return false; + } + + fieldObjectInfo.m_strAddress = fieldInfo.m_strValue; + return true; +} diff --git a/src/AsyncDebugTools/Helpers.h b/src/AsyncDebugTools/Helpers.h new file mode 100644 index 000000000..949529410 --- /dev/null +++ b/src/AsyncDebugTools/Helpers.h @@ -0,0 +1,112 @@ +#pragma once + +#include "stdafx.h" + +struct ExceptionInfo +{ + std::string m_strThreadId; + std::string m_strExceptionObject; +}; + +struct ExceptionDetail +{ + std::string m_strExceptionObject; + std::string m_strExceptionType; + std::string m_strInnerExceptionObject; + std::vector m_stackTraces; + std::string m_strHResult; + std::string m_strRawText; +}; + +struct ThreadPoolStatus +{ + int m_iRunningWorkers = 0; +}; + +struct FieldInfo +{ + std::string m_strMethodTable; + int m_iOffset; + bool m_fIsValueType; + std::string m_strValue; + std::string m_strName; +}; + +struct ObjectInfo +{ + std::string m_strAddress; + std::string m_strType; + std::string m_string; + std::vector m_fields; +}; + +bool EnsureLoadSOS( + _In_ IDebugClient* pDebugClient, + _Out_ std::string &strSOS, + _Out_ std::string &strOutput); + +std::string GetFullCommand( + _In_ const std::string &strSOS, + _In_ const std::string &strCommand); + +HRESULT Execute( + _In_ IDebugClient* pDebugClient, + _In_ const std::string &strCommand, + _Out_ std::string &strOutput); + +void ExtractExceptionInfosFromThreadsOutput( + _In_ const std::string &strOutput, + _Out_ std::vector &exceptionInfos); + +void CollectExceptionDetails( + _In_ IDebugClient* pDebugClient, + _In_ const std::string &strSOS, + _In_ const std::string &strExceptionObject, + _Out_ std::vector &exceptionDetailsIncludingInnerExceptions); + +std::string GuessBlamedSymbol( + _In_ const ExceptionDetail &detail); + +bool IsDueToHangDetected( + _In_ const ExceptionDetail &detail); + +bool ParseClrStackFrame( + _In_ const std::string &strFrame, + _Out_ std::string &strFunc); + +bool ParseKStackFrame( + _In_ const std::string &strFrame, + _Out_ std::string &strFunc); + +bool IsImmunized( + _In_ const std::string &strFrame); + +std::string GetSymbolFromFrame( + _In_ const std::string &strFrame); + +bool GetThreadPoolStatus( + _In_ const std::string &strThreadPoolOutput, + _Out_ ThreadPoolStatus &status); + +int GetNumberOfThreads( + _In_ PDEBUG_CLIENT pDebugClient); + +bool ParseDumpObjectOutput( + _In_ const std::string &strDumpObject, + _Out_ ObjectInfo &objectInfo); + +bool ParseName2EEOutput( + _In_ const std::string &strName2EE, + _Out_ std::string &strMethodTable); + +bool GetFieldInfo( + _In_ const ObjectInfo &objectInfo, + _In_ const std::string &strFieldName, + _Out_ FieldInfo &fieldInfo); + +bool FindFieldAndGetObjectInfo( + _In_ PDEBUG_CLIENT pDebugClient, + _In_ const std::string &strSOS, + _In_ const ObjectInfo &objectInfo, + _In_ const std::string &strFieldName, + _Out_ ObjectInfo &fieldObjectInfo); diff --git a/src/AsyncDebugTools/ThreadPoolExhausted.cpp b/src/AsyncDebugTools/ThreadPoolExhausted.cpp new file mode 100644 index 000000000..7a5ee544e --- /dev/null +++ b/src/AsyncDebugTools/ThreadPoolExhausted.cpp @@ -0,0 +1,143 @@ +#include "stdafx.h" +#include "dbgexts.h" +#include "Helpers.h" + +static int s_iThresholdToDetectThreadPoolExhausted = 20; +static int s_iThresholdToBlameCallStack = 10; + +struct Thread +{ + int m_id; + std::vector m_frames; +}; + +struct LessThanFrames +{ + bool operator() (const std::vector *pLeft, const std::vector *pRight) const + { + if (pLeft->size() != pRight->size()) + { + return pLeft->size() < pRight->size(); + } + + for (size_t i = 0; i < pLeft->size(); ++i) + { + int result = pLeft->at(i).compare(pRight->at(i)); + if (result != 0) + { + return result < 0; + } + } + + return false; + } +}; + +bool IsThreadPoolExhausted( + PDEBUG_CLIENT pDebugClient, + const std::string &strSOS) +{ + // !ThreadPool + std::string strOutput; + ThreadPoolStatus threadPoolStatus; + if (SUCCEEDED(Execute(pDebugClient, GetFullCommand(strSOS, "ThreadPool"), strOutput)) + && GetThreadPoolStatus(strOutput, threadPoolStatus) + && threadPoolStatus.m_iRunningWorkers >= s_iThresholdToDetectThreadPoolExhausted) + { + return true; + } + + return false; +} + +bool IsClrThreadPoolWorkingThread(const std::string &strOutput) +{ + std::stringstream ss(strOutput); + std::string strLine; + while (std::getline(ss, strLine)) + { + if (strLine.find("clr!ThreadpoolMgr::ExecuteWorkRequest") != std::string::npos) + { + return true; + } + } + + return false; +} + +HRESULT OnThreadPoolExhausted( + PDEBUG_CLIENT pDebugClient, + const std::string &strSOS) +{ + CComQIPtr srpControl(pDebugClient); + HRESULT hr = S_OK; + + int iThreads = GetNumberOfThreads(pDebugClient); + std::string strOutput; + std::vector threads; + threads.reserve(iThreads); + for (int i = 1; i < iThreads; ++i) // Skip the main thread + { + char szCommand[0x20] = { 0 }; + if (sprintf_s(szCommand, "~%dk", i) > 0 + && SUCCEEDED(Execute(pDebugClient, szCommand, strOutput)) + && IsClrThreadPoolWorkingThread(strOutput)) + { + std::vector frames; + std::stringstream ss(strOutput); + std::string strLine; + std::string strFunc; + while (std::getline(ss, strLine)) + { + if (ParseKStackFrame(strLine, strFunc)) + { + frames.push_back(strFunc); + } + } + + if (!frames.empty()) + { + threads.push_back({ i, std::move(frames) }); + } + } + } + + for (const Thread &thread : threads) + { + srpControl->Output(DEBUG_OUTPUT_VERBOSE, "%d\n", thread.m_id); + for (const std::string &strFrame : thread.m_frames) + { + srpControl->Output(DEBUG_OUTPUT_VERBOSE, "\t%s\n", strFrame.c_str()); + } + } + + // Group the threads by the frames + std::map *, int, LessThanFrames> groups; + for (const Thread &thread : threads) + { + const std::vector *pKey = &thread.m_frames; + ++groups[pKey]; + } + + // Sort by count of the threads + std::vector *, int>> items(groups.size()); + std::copy(groups.cbegin(), groups.cend(), items.begin()); + std::sort(items.begin(), items.end(), [](const auto &x, const auto &y) { return x.second > y.second; }); + + for (const auto &item : items) + { + if (item.second >= s_iThresholdToBlameCallStack) + { + srpControl->Output(DEBUG_OUTPUT_NORMAL, "Detected thread pool exhausted due to this callstack: (%d threads)\n", item.second); + for (const std::string &strFrame : *item.first) + { + srpControl->Output(DEBUG_OUTPUT_NORMAL, "\t%s\n", strFrame.c_str()); + } + + srpControl->Output(DEBUG_OUTPUT_NORMAL, "\n"); + } + } + + return hr; +} + diff --git a/src/AsyncDebugTools/ThreadPoolExhausted.h b/src/AsyncDebugTools/ThreadPoolExhausted.h new file mode 100644 index 000000000..5aea930fc --- /dev/null +++ b/src/AsyncDebugTools/ThreadPoolExhausted.h @@ -0,0 +1,11 @@ +#pragma once + +#include "stdafx.h" + +bool IsThreadPoolExhausted( + PDEBUG_CLIENT pDebugClient, + const std::string &strSOS); + +HRESULT OnThreadPoolExhausted( + PDEBUG_CLIENT pDebugClient, + const std::string &strSOS); diff --git a/src/AsyncDebugTools/dllmain.cpp b/src/AsyncDebugTools/dllmain.cpp new file mode 100644 index 000000000..aefdadb3c --- /dev/null +++ b/src/AsyncDebugTools/dllmain.cpp @@ -0,0 +1,18 @@ +// dllmain.cpp : Defines the entry point for the DLL application. +#include "stdafx.h" + +BOOL APIENTRY DllMain(HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved +) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} diff --git a/src/AsyncDebugTools/dumpasync.cpp b/src/AsyncDebugTools/dumpasync.cpp new file mode 100644 index 000000000..7c6ad3098 --- /dev/null +++ b/src/AsyncDebugTools/dumpasync.cpp @@ -0,0 +1,313 @@ +#include "stdafx.h" +#include "dbgexts.h" +#include "Helpers.h" +#include "ThreadPoolExhausted.h" +#include "DML.h" +#include + +struct StateMachineNode +{ + ObjectInfo m_objectInfo; + StateMachineNode *m_pNext; + StateMachineNode *m_pPrevious; + int m_state; + int m_iDepth; + std::string m_task; +}; + +bool GetContinuation( + _In_ PDEBUG_CLIENT pDebugClient, + _In_ const std::string &strSOS, + _In_ const ObjectInfo &stateMachine, + _Out_ std::string &strContinuation); + +HRESULT CALLBACK +dumpasync(PDEBUG_CLIENT pDebugClient, PCSTR args) +{ + UNREFERENCED_PARAMETER(args); + + CComQIPtr srpControl(pDebugClient); + + std::string strSOS; + std::string strOutput; + if (!EnsureLoadSOS(pDebugClient, strSOS, strOutput)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to load SOS.dll: %s\n", strOutput.c_str()); + return E_FAIL; + } + + /* + if (IsThreadPoolExhausted(pDebugClient, strSOS)) + { + return OnThreadPoolExhausted(pDebugClient, strSOS); + } + */ + + // TODO: Support .NET Core by allowing AsyncMethodBuilderCore to be defined in System.Private.CoreLib.ni.dll + // and perhaps other modules. + HRESULT hr = Execute(pDebugClient, GetFullCommand(strSOS, "name2ee mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner"), strOutput); + if (FAILED(hr)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to run !name2ee: %s\n", strOutput.c_str()); + return hr; + } + + std::string strMethodTable; + if (!ParseName2EEOutput(strOutput, strMethodTable)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to parse the output of !name2ee: %s\n", strOutput.c_str()); + return hr; + } + + hr = Execute(pDebugClient, GetFullCommand(strSOS, "dumpheap -short -mt " + strMethodTable), strOutput); + if (FAILED(hr)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to run !dumpheap: %s\n", strOutput.c_str()); + return hr; + } + + // Extract the inner m_stateMachine. + std::stringstream ss(strOutput); + std::string strObject; + ObjectInfo objectInfo{}; + FieldInfo fieldInfo{}; + std::vector> stateMachineNodes; + while (std::getline(ss, strObject)) + { + // TODO: avoid calling !do 00000000 which will always error out. + hr = Execute(pDebugClient, GetFullCommand(strSOS, "do " + strObject), strOutput); + if (FAILED(hr)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to run !do: %s\n", strOutput.c_str()); + continue; + } + + if (!ParseDumpObjectOutput(strOutput, objectInfo)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to parse the output of !do: %s\n", strOutput.c_str()); + continue; + } + + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, objectInfo, "m_stateMachine", objectInfo)) + { + if (GetFieldInfo(objectInfo, "<>1__state", fieldInfo) + && atoi(fieldInfo.m_strValue.c_str()) >= -1) + { + auto found = std::find_if(stateMachineNodes.begin(), stateMachineNodes.end(), [&](const std::unique_ptr &node) + { + return node->m_objectInfo.m_strAddress == objectInfo.m_strAddress; + }); + + // state machine can be reported more than once... + if (found != stateMachineNodes.end()) + { + continue; + } + + stateMachineNodes.push_back(std::make_unique()); + stateMachineNodes.back()->m_objectInfo = std::move(objectInfo); + stateMachineNodes.back()->m_state = atoi(fieldInfo.m_strValue.c_str()); + + ObjectInfo asyncBuilder{}; + FieldInfo taskFieldInfo{}; + + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, stateMachineNodes.back()->m_objectInfo, "<>t__builder", asyncBuilder)) + { + ObjectInfo taskObject{}; + while (!GetFieldInfo(asyncBuilder, "m_task", taskFieldInfo) + && FindFieldAndGetObjectInfo(pDebugClient, strSOS, asyncBuilder, "m_builder", asyncBuilder)); + + if (!taskFieldInfo.m_strValue.empty()) + { + stateMachineNodes.back()->m_task = taskFieldInfo.m_strValue; + } + } + } + } + } + + // Link the state machines via the continuation. + std::string strContinuation; + for (size_t i = 0; i < stateMachineNodes.size(); ++i) + { + if (GetContinuation(pDebugClient, strSOS, stateMachineNodes[i]->m_objectInfo, strContinuation)) + { + auto found = std::find_if(stateMachineNodes.begin(), stateMachineNodes.end(), [&](const std::unique_ptr &node) + { + return node->m_objectInfo.m_strAddress == strContinuation; + }); + + if (found != stateMachineNodes.end()) + { + StateMachineNode *pContinuation = (*found).get(); + stateMachineNodes[i]->m_pNext = pContinuation; + pContinuation->m_pPrevious = (stateMachineNodes[i]).get(); + } + } + } + + // Fix Joinable Task chains + for (std::unique_ptr &node : stateMachineNodes) + { + if (node->m_pPrevious == nullptr) + { + ObjectInfo joinableTask{}; + FieldInfo wrappedTask{}; + + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, node->m_objectInfo, "<>4__this", joinableTask) + && GetFieldInfo(joinableTask, "wrappedTask", wrappedTask)) + { + std::string task = wrappedTask.m_strValue; + auto found = std::find_if(stateMachineNodes.begin(), stateMachineNodes.end(), [&](const std::unique_ptr &node) + { + return node->m_task == task; + }); + + if (found != stateMachineNodes.end()) + { + StateMachineNode *pPrevious = (*found).get(); + node->m_pPrevious = pPrevious; + pPrevious->m_pNext = node.get(); + } + } + } + } + + // Compute the depth + for (std::unique_ptr &node : stateMachineNodes) + { + node->m_iDepth = 0; + if (node->m_pPrevious == nullptr) + { + for (const StateMachineNode *p = node.get(); p != nullptr; p = p->m_pNext) + { + node->m_iDepth++; + } + } + } + + // Sort by depth DESC + std::sort(stateMachineNodes.begin(), stateMachineNodes.end(), [](const std::unique_ptr &x, const std::unique_ptr &y) + { + return x->m_iDepth > y->m_iDepth; + }); + + bool fOutputDML = PreferDML(pDebugClient) && AbilityDML(pDebugClient); + + // Dump the state machines + for (const std::unique_ptr &node : stateMachineNodes) + { + if (node->m_iDepth > 0) + { + std::string strIndent; + for (const StateMachineNode *p = node.get(); p != nullptr; p = p->m_pNext) + { + if (fOutputDML) + { + srpControl->Output(DEBUG_OUTPUT_NORMAL, strIndent.c_str()); + srpControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, DEBUG_OUTPUT_NORMAL, "%s", + p->m_objectInfo.m_strAddress.c_str(), + p->m_objectInfo.m_strAddress.c_str()); + srpControl->Output(DEBUG_OUTPUT_NORMAL, " <%d> %s\n", p->m_state, p->m_objectInfo.m_strType.c_str()); + } + else + { + srpControl->Output(DEBUG_OUTPUT_NORMAL, "%s%s <%d> %s\n", strIndent.c_str(), p->m_objectInfo.m_strAddress.c_str(), p->m_state, p->m_objectInfo.m_strType.c_str()); + } + + strIndent.append("."); + } + + if (node->m_iDepth > 1) + { + srpControl->Output(DEBUG_OUTPUT_NORMAL, "\n"); + } + } + } + + return S_OK; +} + +bool GetContinuationFromTarget( + _In_ PDEBUG_CLIENT pDebugClient, + _In_ const std::string &strSOS, + _In_ const ObjectInfo &targetObject, + _Out_ std::string &strContinuation) +{ + FieldInfo stateMachineField{}; + if (GetFieldInfo(targetObject, "m_stateMachine", stateMachineField)) + { + strContinuation = stateMachineField.m_strValue; + return true; + } + + ObjectInfo nextContinuationObject{}; + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, targetObject, "m_continuation", nextContinuationObject)) + { + ObjectInfo targetObject{}; + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, nextContinuationObject, "_target", targetObject) + && GetContinuationFromTarget(pDebugClient, strSOS, targetObject, strContinuation)) + { + return true; + } + } + + return false; +} + +bool GetContinuation( + _In_ PDEBUG_CLIENT pDebugClient, + _In_ const std::string &strSOS, + _In_ const ObjectInfo &stateMachine, + _Out_ std::string &strContinuation) +{ + ObjectInfo builderObject{}; + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, stateMachine, "<>t__builder", builderObject)) + { + ObjectInfo taskObject{}; + while (!FindFieldAndGetObjectInfo(pDebugClient, strSOS, builderObject, "m_task", taskObject) + && FindFieldAndGetObjectInfo(pDebugClient, strSOS, builderObject, "m_builder", builderObject)); + + if (!taskObject.m_strType.empty()) + { + ObjectInfo continuationObject{}; + ObjectInfo actionObject{}; + ObjectInfo targetObject{}; + while (FindFieldAndGetObjectInfo(pDebugClient, strSOS, taskObject, "m_continuationObject", continuationObject)) + { + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, continuationObject, "m_action", actionObject) + && FindFieldAndGetObjectInfo(pDebugClient, strSOS, actionObject, "_target", targetObject)) + { + if (GetContinuationFromTarget(pDebugClient, strSOS, targetObject, strContinuation)) + { + return true; + } + } + + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, continuationObject, "_target", targetObject)) + { + if (GetContinuationFromTarget(pDebugClient, strSOS, targetObject, strContinuation)) + { + return true; + } + } + + ObjectInfo continuationTaskObject{}; + ObjectInfo stateObject{}; + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, continuationObject, "m_task", continuationTaskObject) + && FindFieldAndGetObjectInfo(pDebugClient, strSOS, continuationTaskObject, "m_stateObject", stateObject) + && FindFieldAndGetObjectInfo(pDebugClient, strSOS, stateObject, "_target", targetObject)) + { + if (GetContinuationFromTarget(pDebugClient, strSOS, targetObject, strContinuation)) + { + return true; + } + } + + taskObject = continuationObject; + } + } + } + + return false; +} diff --git a/src/AsyncDebugTools/dumpversions.cpp b/src/AsyncDebugTools/dumpversions.cpp new file mode 100644 index 000000000..ff60a5b4d --- /dev/null +++ b/src/AsyncDebugTools/dumpversions.cpp @@ -0,0 +1,143 @@ +#include "stdafx.h" +#include "dbgexts.h" +#include "Helpers.h" +#include "ThreadPoolExhausted.h" +#include "DML.h" +#include + +HRESULT DumpNode( + _In_ PDEBUG_CLIENT pDebugClient, + _In_ const std::string &strSOS, + _In_ const ObjectInfo &node); + +HRESULT CALLBACK +dumpversions(PDEBUG_CLIENT pDebugClient, PCSTR args) +{ + CComQIPtr srpControl(pDebugClient); + + std::string strSOS; + std::string strOutput; + if (!EnsureLoadSOS(pDebugClient, strSOS, strOutput)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to load SOS.dll: %s\n", strOutput.c_str()); + return E_FAIL; + } + + std::string strObject(args, strlen(args)); + ObjectInfo objectInfo{}; + + HRESULT hr = Execute(pDebugClient, GetFullCommand(strSOS, "do " + strObject), strOutput); + if (FAILED(hr)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to run !do: %s\n", strOutput.c_str()); + return hr; + } + + if (!ParseDumpObjectOutput(strOutput, objectInfo)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "Failed to parse the output of !do: %s\n", strOutput.c_str()); + return E_FAIL; + } + + ObjectInfo node{}; + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, objectInfo, "_root", node)) + { + srpControl->Output(DEBUG_OUTPUT_NORMAL, "Id\tValue\tRequire\tName\n"); + hr = DumpNode(pDebugClient, strSOS, node); + if (FAILED(hr)) + { + return hr; + } + } + + return S_OK; +} + +HRESULT DumpNode( + _In_ PDEBUG_CLIENT pDebugClient, + _In_ const std::string &strSOS, + _In_ const ObjectInfo &node) +{ + HRESULT hr = S_OK; + FieldInfo heightInfo{}; + + CComQIPtr srpControl(pDebugClient); + + if (!GetFieldInfo(node, "_height", heightInfo)) + { + srpControl->Output(DEBUG_OUTPUT_ERROR, "No _height\n"); + return E_FAIL; + } + + int height = atoi(heightInfo.m_strValue.c_str()); + if (height == 0) + { + return S_OK; + } + + if (height > 1) + { + ObjectInfo left{}; + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, node, "_left", left)) + { + hr = DumpNode(pDebugClient, strSOS, left); + if (FAILED(hr)) + { + return hr; + } + } + } + + ObjectInfo valueInfo{}; + ObjectInfo firstValueInfo{}; + ObjectInfo keyInfo{}; + FieldInfo idInfo{}; + ObjectInfo nameInfo{}; + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, node, "_value", valueInfo) && + FindFieldAndGetObjectInfo(pDebugClient, strSOS, valueInfo, "_firstValue", firstValueInfo) && + FindFieldAndGetObjectInfo(pDebugClient, strSOS, firstValueInfo, "key", keyInfo) && + GetFieldInfo(keyInfo, "k__BackingField", idInfo) && + FindFieldAndGetObjectInfo(pDebugClient, strSOS, keyInfo, "k__BackingField", nameInfo) && + FindFieldAndGetObjectInfo(pDebugClient, strSOS, firstValueInfo, "value", valueInfo)) + { + FieldInfo intValueInfo{}; + ObjectInfo versionInfo{}; + FieldInfo allowMissingValueInfo{}; + ObjectInfo innerVersionInfo{}; + + if (GetFieldInfo(valueInfo, "m_value", intValueInfo)) + { + hr = srpControl->Output(DEBUG_OUTPUT_NORMAL, "%s\t%s\t\t\t%s\n", idInfo.m_strValue.c_str(), intValueInfo.m_strValue.c_str(), nameInfo.m_string.c_str()); + } + else if (GetFieldInfo(valueInfo, "k__BackingField", allowMissingValueInfo) && + FindFieldAndGetObjectInfo(pDebugClient, strSOS, valueInfo, "k__BackingField", innerVersionInfo) && + GetFieldInfo(innerVersionInfo, "m_value", intValueInfo)) + { + hr = srpControl->Output(DEBUG_OUTPUT_NORMAL, "%s\t%s\t%s\t\t%s\n", idInfo.m_strValue.c_str(), intValueInfo.m_strValue.c_str(), allowMissingValueInfo.m_strValue.c_str(), nameInfo.m_string.c_str()); + } + else + { + return E_FAIL; + } + + if (FAILED(hr)) + { + return hr; + } + } + + if (height > 1) + { + ObjectInfo right{}; + if (FindFieldAndGetObjectInfo(pDebugClient, strSOS, node, "_right", right)) + { + hr = DumpNode(pDebugClient, strSOS, right); + if (FAILED(hr)) + { + return hr; + } + } + } + + return hr; +} diff --git a/src/AsyncDebugTools/outputcallbacks.cpp b/src/AsyncDebugTools/outputcallbacks.cpp new file mode 100644 index 000000000..9c36ecdc4 --- /dev/null +++ b/src/AsyncDebugTools/outputcallbacks.cpp @@ -0,0 +1,101 @@ +#include "stdafx.h" +#include "dbgexts.h" +#include "outputcallbacks.h" + +#define MAX_OUTPUTCALLBACKS_BUFFER 0x1000000 // 1Mb +#define MAX_OUTPUTCALLBACKS_LENGTH 0x0FFFFFF // 1Mb - 1 + +STDMETHODIMP COutputCallbacks::QueryInterface(__in REFIID InterfaceId, __out PVOID* Interface) +{ + *Interface = NULL; + if (IsEqualIID(InterfaceId, __uuidof(IUnknown)) || IsEqualIID(InterfaceId, __uuidof(IDebugOutputCallbacks))) + { + *Interface = (IDebugOutputCallbacks *)this; + InterlockedIncrement(&m_ref); + return S_OK; + } + else + { + return E_NOINTERFACE; + } +} + +STDMETHODIMP_(ULONG) COutputCallbacks::AddRef() +{ + return InterlockedIncrement(&m_ref); +} + +STDMETHODIMP_(ULONG) COutputCallbacks::Release() +{ + if (InterlockedDecrement(&m_ref) == 0) + { + delete this; + return 0; + } + return m_ref; +} + +STDMETHODIMP COutputCallbacks::Output(__in ULONG Mask, __in PCSTR Text) +{ + if ((Mask & DEBUG_OUTPUT_NORMAL) == DEBUG_OUTPUT_NORMAL) + { + if (m_pBufferNormal == NULL) + { + m_nBufferNormal = 0; + m_pBufferNormal = (PCHAR)malloc(sizeof(CHAR)*(MAX_OUTPUTCALLBACKS_BUFFER)); + if (m_pBufferNormal == NULL) return E_OUTOFMEMORY; + m_pBufferNormal[0] = '\0'; + m_pBufferNormal[MAX_OUTPUTCALLBACKS_LENGTH] = '\0'; + } + size_t len = strlen(Text); + if (len > (MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferNormal)) + { + len = MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferNormal; + } + if (len > 0) + { + memcpy(&m_pBufferNormal[m_nBufferNormal], Text, len); + m_nBufferNormal += len; + m_pBufferNormal[m_nBufferNormal] = '\0'; + } + } + if ((Mask & DEBUG_OUTPUT_ERROR) == DEBUG_OUTPUT_ERROR) + { + if (m_pBufferError == NULL) + { + m_nBufferError = 0; + m_pBufferError = (PCHAR)malloc(sizeof(CHAR)*(MAX_OUTPUTCALLBACKS_BUFFER)); + if (m_pBufferError == NULL) return E_OUTOFMEMORY; + m_pBufferError[0] = '\0'; + m_pBufferError[MAX_OUTPUTCALLBACKS_LENGTH] = '\0'; + } + size_t len = strlen(Text); + if (len >= (MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferError)) + { + len = MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferError; + } + if (len > 0) + { + memcpy(&m_pBufferError[m_nBufferError], Text, len); + m_nBufferError += len; + m_pBufferError[m_nBufferError] = '\0'; + } + } + return S_OK; +} + +void COutputCallbacks::Clear() +{ + if (m_pBufferNormal) + { + free(m_pBufferNormal); + m_pBufferNormal = NULL; + m_nBufferNormal = 0; + } + if (m_pBufferError) + { + free(m_pBufferError); + m_pBufferError = NULL; + m_nBufferError = 0; + } +} diff --git a/src/AsyncDebugTools/outputcallbacks.h b/src/AsyncDebugTools/outputcallbacks.h new file mode 100644 index 000000000..ea6e8ea7b --- /dev/null +++ b/src/AsyncDebugTools/outputcallbacks.h @@ -0,0 +1,45 @@ +#ifndef __OUTPUTCALLBACKS_H__ +#define __OUTPUTCALLBACKS_H__ + +#include "dbgexts.h" + +class COutputCallbacks : public IDebugOutputCallbacks +{ +private: + long m_ref; + PCHAR m_pBufferNormal; + size_t m_nBufferNormal; + PCHAR m_pBufferError; + size_t m_nBufferError; + +public: + COutputCallbacks() + { + m_ref = 1; + m_pBufferNormal = NULL; + m_nBufferNormal = 0; + m_pBufferError = NULL; + m_nBufferError = 0; + } + + ~COutputCallbacks() + { + Clear(); + } + + // IUnknown + STDMETHOD(QueryInterface)(__in REFIID InterfaceId, __out PVOID* Interface); + STDMETHOD_(ULONG, AddRef)(); + STDMETHOD_(ULONG, Release)(); + + // IDebugOutputCallbacks + STDMETHOD(Output)(__in ULONG Mask, __in PCSTR Text); + + // Helpers + ULONG SupportedMask() { return DEBUG_OUTPUT_NORMAL | DEBUG_OUTPUT_ERROR; } + PCHAR BufferNormal() { return m_pBufferNormal; } + PCHAR BufferError() { return m_pBufferError; } + void Clear(); +}; + +#endif // __OUTPUTCALLBACKS_H__ diff --git a/src/AsyncDebugTools/packages.config b/src/AsyncDebugTools/packages.config new file mode 100644 index 000000000..8c50ceb57 --- /dev/null +++ b/src/AsyncDebugTools/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/AsyncDebugTools/stdafx.cpp b/src/AsyncDebugTools/stdafx.cpp new file mode 100644 index 000000000..9a8331031 --- /dev/null +++ b/src/AsyncDebugTools/stdafx.cpp @@ -0,0 +1,8 @@ +// stdafx.cpp : source file that includes just the standard includes +// WinDbgExt1.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/src/AsyncDebugTools/stdafx.h b/src/AsyncDebugTools/stdafx.h new file mode 100644 index 000000000..912d3ce5d --- /dev/null +++ b/src/AsyncDebugTools/stdafx.h @@ -0,0 +1,26 @@ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#include "targetver.h" + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files: +#include + +// TODO: reference additional headers your program requires here +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include diff --git a/src/AsyncDebugTools/targetver.h b/src/AsyncDebugTools/targetver.h new file mode 100644 index 000000000..87c0086de --- /dev/null +++ b/src/AsyncDebugTools/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d19d8c79b..c62dc7319 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,7 +6,7 @@ 2.0.54 - + diff --git a/src/Microsoft.VisualStudio.Threading.sln b/src/Microsoft.VisualStudio.Threading.sln index 4dc7580a4..6174ae659 100644 --- a/src/Microsoft.VisualStudio.Threading.sln +++ b/src/Microsoft.VisualStudio.Threading.sln @@ -23,38 +23,78 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Thre EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Tests", "Microsoft.VisualStudio.Threading.Tests\Microsoft.VisualStudio.Threading.Tests.csproj", "{CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IsolatedTestHost", "IsolatedTestHost\IsolatedTestHost.csproj", "{BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsolatedTestHost", "IsolatedTestHost\IsolatedTestHost.csproj", "{BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AsyncDebugTools", "AsyncDebugTools\AsyncDebugTools.vcxproj", "{CF348EC1-1E7D-47DE-8431-8C61D5CAF924}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {536F3F9A-B457-43B8-BC93-CE1C16959037}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {536F3F9A-B457-43B8-BC93-CE1C16959037}.Debug|Any CPU.Build.0 = Debug|Any CPU + {536F3F9A-B457-43B8-BC93-CE1C16959037}.Debug|x64.ActiveCfg = Debug|Any CPU + {536F3F9A-B457-43B8-BC93-CE1C16959037}.Debug|x86.ActiveCfg = Debug|Any CPU {536F3F9A-B457-43B8-BC93-CE1C16959037}.Release|Any CPU.ActiveCfg = Release|Any CPU {536F3F9A-B457-43B8-BC93-CE1C16959037}.Release|Any CPU.Build.0 = Release|Any CPU + {536F3F9A-B457-43B8-BC93-CE1C16959037}.Release|x64.ActiveCfg = Release|Any CPU + {536F3F9A-B457-43B8-BC93-CE1C16959037}.Release|x86.ActiveCfg = Release|Any CPU {620ED702-B6DA-4454-BF3E-5494D3652724}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {620ED702-B6DA-4454-BF3E-5494D3652724}.Debug|Any CPU.Build.0 = Debug|Any CPU + {620ED702-B6DA-4454-BF3E-5494D3652724}.Debug|x64.ActiveCfg = Debug|Any CPU + {620ED702-B6DA-4454-BF3E-5494D3652724}.Debug|x86.ActiveCfg = Debug|Any CPU {620ED702-B6DA-4454-BF3E-5494D3652724}.Release|Any CPU.ActiveCfg = Release|Any CPU {620ED702-B6DA-4454-BF3E-5494D3652724}.Release|Any CPU.Build.0 = Release|Any CPU + {620ED702-B6DA-4454-BF3E-5494D3652724}.Release|x64.ActiveCfg = Release|Any CPU + {620ED702-B6DA-4454-BF3E-5494D3652724}.Release|x86.ActiveCfg = Release|Any CPU {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|x86.ActiveCfg = Debug|Any CPU {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|Any CPU.Build.0 = Release|Any CPU + {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|x64.ActiveCfg = Release|Any CPU + {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|x86.ActiveCfg = Release|Any CPU {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|x86.ActiveCfg = Debug|Any CPU {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Release|Any CPU.Build.0 = Release|Any CPU + {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Release|x64.ActiveCfg = Release|Any CPU + {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Release|x86.ActiveCfg = Release|Any CPU {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Debug|x64.ActiveCfg = Debug|Any CPU + {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Debug|x86.ActiveCfg = Debug|Any CPU {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Release|Any CPU.ActiveCfg = Release|Any CPU {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Release|Any CPU.Build.0 = Release|Any CPU + {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Release|x64.ActiveCfg = Release|Any CPU + {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Release|x86.ActiveCfg = Release|Any CPU {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Debug|x64.ActiveCfg = Debug|Any CPU + {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Debug|x86.ActiveCfg = Debug|Any CPU {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Release|Any CPU.Build.0 = Release|Any CPU + {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Release|x64.ActiveCfg = Release|Any CPU + {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Release|x86.ActiveCfg = Release|Any CPU + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924}.Debug|x64.ActiveCfg = Debug|x64 + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924}.Debug|x64.Build.0 = Debug|x64 + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924}.Debug|x86.ActiveCfg = Debug|Win32 + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924}.Debug|x86.Build.0 = Debug|Win32 + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924}.Release|Any CPU.ActiveCfg = Release|Win32 + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924}.Release|x64.ActiveCfg = Release|x64 + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924}.Release|x64.Build.0 = Release|x64 + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924}.Release|x86.ActiveCfg = Release|Win32 + {CF348EC1-1E7D-47DE-8431-8C61D5CAF924}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE