-
Notifications
You must be signed in to change notification settings - Fork 203
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This adds a C# program that compares and summarizes ETW heap snapshots. Heap snapshots are described here: https://randomascii.wordpress.com/2019/10/27/heap-snapshots-tracing-all-heap-allocations/ TraceProcessor for programmatic analysis of ETW traces is described here: https://randomascii.wordpress.com/2020/01/05/bulk-etw-trace-analysis-in-c/
- Loading branch information
1 parent
677d404
commit b2bc3dd
Showing
6 changed files
with
413 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<?xml version="1.0" encoding="utf-8" ?> | ||
<configuration> | ||
<startup> | ||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /> | ||
</startup> | ||
</configuration> |
250 changes: 250 additions & 0 deletions
250
TraceProcessors/HeapSnapshotCompare/HeapSnapshotCompare.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
/* | ||
Copyright 2019 Google Inc. All Rights Reserved. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
// This program summarizes one or more heap snapshots. If multiple heap | ||
// snapshots are given as parameters then it tries to summarizes the | ||
// differences. See this post for discussion of recording heap snapshots: | ||
// https://randomascii.wordpress.com/2019/10/27/heap-snapshots-tracing-all-heap-allocations/ | ||
|
||
// See these blog posts for details of the Trace Processor package used to | ||
// drive this: | ||
// https://blogs.windows.com/windowsdeveloper/2019/05/09/announcing-traceprocessor-preview-0-1-0/ | ||
// https://blogs.windows.com/windowsdeveloper/2019/08/07/traceprocessor-0-2-0/#L2W90BVvLzJ8XwEY.97 | ||
// https://randomascii.wordpress.com/2020/01/05/bulk-etw-trace-analysis-in-c/ | ||
// This uses the Microsoft.Windows.EventTracing.Processing.All package from NuGet | ||
|
||
using Microsoft.Windows.EventTracing; | ||
using Microsoft.Windows.EventTracing.Memory; | ||
using Microsoft.Windows.EventTracing.Symbols; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
|
||
namespace HeapSnapshotCompare | ||
{ | ||
// A summary of a particular allocation call stack including the outstanding | ||
// bytes allocated, the number of outstanding allocations, and the call | ||
// stack. | ||
struct AllocDetails | ||
{ | ||
public DataSize Size; | ||
public long Count; | ||
public IReadOnlyList<IStackFrame> Stack; | ||
} | ||
|
||
// A summary of a heap snapshot. This includes AllocDetails by Stack Ref #, | ||
// which stack frames show up in the most allocation stacks, and the total | ||
// number of bytes and allocations outstanding. | ||
class SnapshotSummary | ||
{ | ||
public SnapshotSummary(Dictionary<ulong, AllocDetails> allocsByStackId, ulong pid) | ||
{ | ||
allocsByStackId_ = allocsByStackId; | ||
pid_ = pid; | ||
} | ||
// Dictionary of AllocDetails indexed by SnapshotUniqueStackId, aka the Stack Ref# | ||
// column. | ||
public Dictionary<ulong, AllocDetails> allocsByStackId_; | ||
public Dictionary<string, long> hotStackFrames_ = null; | ||
public ulong pid_; | ||
|
||
public DataSize totalBytes_; | ||
public long allocCount_; | ||
} | ||
|
||
class HeapSnapshotCompare | ||
{ | ||
// Process a trace and create a summary. | ||
static SnapshotSummary GetAllocSummary(ITraceProcessor trace) | ||
{ | ||
var pendingSnapshotData = trace.UseHeapSnapshots(); | ||
IPendingResult<ISymbolDataSource> pendingSymbols = null; | ||
pendingSymbols = trace.UseSymbols(); | ||
trace.Process(); | ||
|
||
var snapshotData = pendingSnapshotData.Result; | ||
|
||
ISymbolDataSource symbols = null; | ||
symbols = pendingSymbols.Result; | ||
symbols.LoadSymbolsAsync(new SymCachePath(@"c:\symcache")).GetAwaiter().GetResult(); | ||
|
||
if (snapshotData.Snapshots.Count != 1) | ||
{ | ||
Console.Error.WriteLine("Trace must contain exactly one heap snapshot - actually contained {0}.", | ||
snapshotData.Snapshots.Count); | ||
return new SnapshotSummary(null, 0); | ||
} | ||
|
||
// Scan through all of the allocations and collect them by | ||
// SnapshotUniqueStackId (which corresponds to Stack Ref#), | ||
// accumulating the bytes allocated, allocation count, and the | ||
// stack. | ||
var allocsByStackId = new Dictionary<ulong, AllocDetails>(); | ||
long highestCount = 0; | ||
var highestCountDetails = new AllocDetails(); | ||
foreach (IHeapAllocation row in snapshotData.Snapshots[0].Allocations) | ||
{ | ||
allocsByStackId.TryGetValue(row.SnapshotUniqueStackId, out AllocDetails value); | ||
value.Stack = row.Stack; | ||
value.Size += row.Size; | ||
value.Count += 1; | ||
if (value.Count > highestCount) | ||
{ | ||
highestCount = value.Count; | ||
highestCountDetails = value; | ||
} | ||
allocsByStackId[row.SnapshotUniqueStackId] = value; | ||
} | ||
|
||
// Count how many allocations each stack frame is part of. | ||
// RtlThreadStart will presumably be near the top, along with | ||
// RtlpAllocateHeapInternal, but some clues may be found. | ||
var hotStackFrames = new Dictionary<string, long>(); | ||
foreach (var data in allocsByStackId.Values) | ||
{ | ||
foreach (var entry in data.Stack) | ||
{ | ||
var analyzerString = entry.GetAnalyzerString(); | ||
hotStackFrames.TryGetValue(analyzerString, out long count); | ||
count += data.Count; | ||
hotStackFrames[analyzerString] = count; | ||
} | ||
} | ||
|
||
var result = new SnapshotSummary(allocsByStackId, snapshotData.Snapshots[0].ProcessId); | ||
|
||
// Create a summary of the alloc counts and byte counts. | ||
var totalAllocBytes = DataSize.Zero; | ||
long totalAllocCount = 0; | ||
foreach (var data in allocsByStackId.Values) | ||
{ | ||
totalAllocBytes += data.Size; | ||
totalAllocCount += data.Count; | ||
} | ||
|
||
result.hotStackFrames_ = hotStackFrames; | ||
result.totalBytes_ = totalAllocBytes; | ||
result.allocCount_ = totalAllocCount; | ||
|
||
return result; | ||
} | ||
|
||
static void Main(string[] args) | ||
{ | ||
if (args.Length == 0) | ||
{ | ||
Console.WriteLine("Use this to summarize a heap snapshot or compare multiple heap snapshots"); | ||
Console.WriteLine("from one run of a program."); | ||
return; | ||
} | ||
|
||
SnapshotSummary lastAllocs = null; | ||
string lastTracename = ""; | ||
foreach (var arg in args) | ||
{ | ||
if (!File.Exists(arg)) | ||
{ | ||
Console.Error.WriteLine("File '{0}' does not exist.", arg); | ||
continue; | ||
} | ||
using (ITraceProcessor trace = TraceProcessor.Create(arg)) | ||
{ | ||
Console.WriteLine("Summarizing '{0}'", Path.GetFileName(arg)); | ||
var allocs = GetAllocSummary(trace); | ||
if (allocs.allocsByStackId_ == null) | ||
{ | ||
Console.WriteLine("Ignoring trace {0}.", arg); | ||
continue; | ||
} | ||
Console.WriteLine("{0,7:F2} MB from {1,9:#,#} allocations on {2,7:#,#} stacks", | ||
allocs.totalBytes_.TotalMegabytes, allocs.allocCount_, allocs.allocsByStackId_.Count); | ||
|
||
const int maxPrinted = 40; | ||
|
||
Console.WriteLine("Hottest stack frames:"); | ||
// Display a summary of the first (possibly only) heap snapshot trace. | ||
var sortedHotStackEntries = new List<KeyValuePair<string, long>>(allocs.hotStackFrames_); | ||
sortedHotStackEntries.Sort((x, y) => y.Value.CompareTo(x.Value)); | ||
for (int i = 0; i < sortedHotStackEntries.Count && i < maxPrinted; ++i) | ||
{ | ||
var data = sortedHotStackEntries[i]; | ||
Console.WriteLine("{0,5} allocs cross {1}", data.Value, data.Key); | ||
} | ||
|
||
if (lastAllocs != null) | ||
{ | ||
Console.WriteLine("Comparing old ({0}) to new ({1}) snapshots.", Path.GetFileName(lastTracename), Path.GetFileName(arg)); | ||
if (allocs.pid_ != lastAllocs.pid_) | ||
{ | ||
Console.WriteLine("WARNING: process IDs are different ({0} and {1}) so stack IDs may not be comparable.", lastAllocs.pid_, allocs.pid_); | ||
} | ||
|
||
var hotStackFramesDelta = new Dictionary<string, long>(allocs.hotStackFrames_); | ||
// Subtract the lastAllocs stack frame counts fomr the current stack frame counts. | ||
foreach (var entry in lastAllocs.hotStackFrames_) | ||
{ | ||
hotStackFramesDelta.TryGetValue(entry.Key, out long count); | ||
count -= entry.Value; | ||
hotStackFramesDelta[entry.Key] = count; | ||
} | ||
|
||
Console.WriteLine("Hottest stack frame deltas:"); | ||
// Print the biggest deltas, positive then negative. | ||
var sortedHotStackFramesDelta = new List<KeyValuePair<string, long>>(hotStackFramesDelta); | ||
sortedHotStackFramesDelta.Sort((x, y) => y.Value.CompareTo(x.Value)); | ||
// Print the first half... | ||
for (int i = 0; i < sortedHotStackFramesDelta.Count && i < maxPrinted / 2; ++i) | ||
{ | ||
var data = sortedHotStackFramesDelta[i]; | ||
Console.WriteLine("{0,5} allocs cross {1}", data.Value, data.Key); | ||
} | ||
Console.WriteLine("..."); | ||
int start = sortedHotStackFramesDelta.Count - maxPrinted / 2; | ||
if (start < 0) | ||
start = 0; | ||
for (int i = start; i < sortedHotStackFramesDelta.Count - 1; ++i) | ||
{ | ||
var data = sortedHotStackFramesDelta[i]; | ||
Console.WriteLine("{0,5} allocs cross {1}", data.Value, data.Key); | ||
} | ||
|
||
ulong newOnlyStacks = 0; | ||
ulong oldOnlyStacks = 0; | ||
foreach (var tag in allocs.allocsByStackId_.Keys) | ||
{ | ||
if (!lastAllocs.allocsByStackId_.ContainsKey(tag)) | ||
{ | ||
newOnlyStacks++; | ||
} | ||
} | ||
foreach (var tag in lastAllocs.allocsByStackId_.Keys) | ||
{ | ||
if (!allocs.allocsByStackId_.ContainsKey(tag)) | ||
{ | ||
oldOnlyStacks++; | ||
} | ||
} | ||
Console.WriteLine(" Old snapshot had {0} unique-to-it stacks, new trace had {1} unique-to-it stacks.", | ||
oldOnlyStacks, newOnlyStacks); | ||
} | ||
|
||
lastAllocs = allocs; | ||
lastTracename = arg; | ||
} | ||
} | ||
} | ||
} | ||
} |
100 changes: 100 additions & 0 deletions
100
TraceProcessors/HeapSnapshotCompare/HeapSnapshotCompare.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> | ||
<PropertyGroup> | ||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | ||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | ||
<ProjectGuid>{96201BE4-B511-49A3-A98D-537B77E054C6}</ProjectGuid> | ||
<OutputType>Exe</OutputType> | ||
<RootNamespace>HeapSnapshotCompare</RootNamespace> | ||
<AssemblyName>HeapSnapshotCompare</AssemblyName> | ||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion> | ||
<FileAlignment>512</FileAlignment> | ||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> | ||
<Deterministic>true</Deterministic> | ||
<NuGetPackageImportStamp> | ||
</NuGetPackageImportStamp> | ||
</PropertyGroup> | ||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | ||
<PlatformTarget>AnyCPU</PlatformTarget> | ||
<DebugSymbols>true</DebugSymbols> | ||
<DebugType>full</DebugType> | ||
<Optimize>false</Optimize> | ||
<OutputPath>bin\Debug\</OutputPath> | ||
<DefineConstants>DEBUG;TRACE</DefineConstants> | ||
<ErrorReport>prompt</ErrorReport> | ||
<WarningLevel>4</WarningLevel> | ||
<Prefer32Bit>false</Prefer32Bit> | ||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
</PropertyGroup> | ||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | ||
<PlatformTarget>AnyCPU</PlatformTarget> | ||
<DebugType>pdbonly</DebugType> | ||
<Optimize>true</Optimize> | ||
<OutputPath>bin\Release\</OutputPath> | ||
<DefineConstants>TRACE</DefineConstants> | ||
<ErrorReport>prompt</ErrorReport> | ||
<WarningLevel>4</WarningLevel> | ||
<Prefer32Bit>false</Prefer32Bit> | ||
</PropertyGroup> | ||
<ItemGroup> | ||
<Reference Include="Microsoft.Windows.EventTracing.Cpu, Version=0.2.1.0, Culture=neutral, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\Microsoft.Windows.EventTracing.Processing.All.0.2.1\lib\netstandard2.0\Microsoft.Windows.EventTracing.Cpu.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Microsoft.Windows.EventTracing.GenericEvents, Version=0.2.1.0, Culture=neutral, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\Microsoft.Windows.EventTracing.Processing.All.0.2.1\lib\netstandard2.0\Microsoft.Windows.EventTracing.GenericEvents.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Microsoft.Windows.EventTracing.HyperV, Version=0.2.1.0, Culture=neutral, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\Microsoft.Windows.EventTracing.Processing.All.0.2.1\lib\netstandard2.0\Microsoft.Windows.EventTracing.HyperV.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Microsoft.Windows.EventTracing.Interop, Version=0.2.1.0, Culture=neutral, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\Microsoft.Windows.EventTracing.Processing.All.0.2.1\lib\netstandard2.0\Microsoft.Windows.EventTracing.Interop.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Microsoft.Windows.EventTracing.Power, Version=0.2.1.0, Culture=neutral, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\Microsoft.Windows.EventTracing.Processing.All.0.2.1\lib\netstandard2.0\Microsoft.Windows.EventTracing.Power.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Microsoft.Windows.EventTracing.Processing, Version=0.2.1.0, Culture=neutral, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\Microsoft.Windows.EventTracing.Processing.0.2.1\lib\netstandard2.0\Microsoft.Windows.EventTracing.Processing.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Microsoft.Windows.EventTracing.Processing.Community, Version=0.2.1.0, Culture=neutral, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\Microsoft.Windows.EventTracing.Processing.All.0.2.1\lib\netstandard2.0\Microsoft.Windows.EventTracing.Processing.Community.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Microsoft.Windows.EventTracing.ScheduledTasks, Version=0.2.1.0, Culture=neutral, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\Microsoft.Windows.EventTracing.Processing.All.0.2.1\lib\netstandard2.0\Microsoft.Windows.EventTracing.ScheduledTasks.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Microsoft.Windows.EventTracing.Syscalls, Version=0.2.1.0, Culture=neutral, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\Microsoft.Windows.EventTracing.Processing.All.0.2.1\lib\netstandard2.0\Microsoft.Windows.EventTracing.Syscalls.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Microsoft.Windows.EventTracing.WindowInFocus, Version=0.2.1.0, Culture=neutral, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\Microsoft.Windows.EventTracing.Processing.All.0.2.1\lib\netstandard2.0\Microsoft.Windows.EventTracing.WindowInFocus.dll</HintPath> | ||
</Reference> | ||
<Reference Include="System" /> | ||
<Reference Include="System.Core" /> | ||
<Reference Include="System.Security.Principal.Windows, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | ||
<HintPath>..\packages\System.Security.Principal.Windows.4.4.1\lib\net461\System.Security.Principal.Windows.dll</HintPath> | ||
</Reference> | ||
<Reference Include="System.Xml.Linq" /> | ||
<Reference Include="System.Data.DataSetExtensions" /> | ||
<Reference Include="Microsoft.CSharp" /> | ||
<Reference Include="System.Data" /> | ||
<Reference Include="System.Net.Http" /> | ||
<Reference Include="System.Xml" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<Compile Include="HeapSnapshotCompare.cs" /> | ||
<Compile Include="Properties\AssemblyInfo.cs" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<None Include="App.config" /> | ||
<None Include="packages.config" /> | ||
</ItemGroup> | ||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | ||
<Import Project="..\packages\Microsoft.Windows.EventTracing.Processing.Toolkit.0.2.0\build\Microsoft.Windows.EventTracing.Processing.Toolkit.targets" Condition="Exists('..\packages\Microsoft.Windows.EventTracing.Processing.Toolkit.0.2.0\build\Microsoft.Windows.EventTracing.Processing.Toolkit.targets')" /> | ||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> | ||
<PropertyGroup> | ||
<ErrorText>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}.</ErrorText> | ||
</PropertyGroup> | ||
<Error Condition="!Exists('..\packages\Microsoft.Windows.EventTracing.Processing.Toolkit.0.2.0\build\Microsoft.Windows.EventTracing.Processing.Toolkit.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.EventTracing.Processing.Toolkit.0.2.0\build\Microsoft.Windows.EventTracing.Processing.Toolkit.targets'))" /> | ||
</Target> | ||
</Project> |
36 changes: 36 additions & 0 deletions
36
TraceProcessors/HeapSnapshotCompare/Properties/AssemblyInfo.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
using System.Runtime.InteropServices; | ||
|
||
// General Information about an assembly is controlled through the following | ||
// set of attributes. Change these attribute values to modify the information | ||
// associated with an assembly. | ||
[assembly: AssemblyTitle("HeapSnapshotCompare")] | ||
[assembly: AssemblyDescription("")] | ||
[assembly: AssemblyConfiguration("")] | ||
[assembly: AssemblyCompany("")] | ||
[assembly: AssemblyProduct("HeapSnapshotCompare")] | ||
[assembly: AssemblyCopyright("Copyright © 2019")] | ||
[assembly: AssemblyTrademark("")] | ||
[assembly: AssemblyCulture("")] | ||
|
||
// Setting ComVisible to false makes the types in this assembly not visible | ||
// to COM components. If you need to access a type in this assembly from | ||
// COM, set the ComVisible attribute to true on that type. | ||
[assembly: ComVisible(false)] | ||
|
||
// The following GUID is for the ID of the typelib if this project is exposed to COM | ||
[assembly: Guid("96201be4-b511-49a3-a98d-537b77e054c6")] | ||
|
||
// Version information for an assembly consists of the following four values: | ||
// | ||
// Major Version | ||
// Minor Version | ||
// Build Number | ||
// Revision | ||
// | ||
// You can specify all the values or you can default the Build and Revision Numbers | ||
// by using the '*' as shown below: | ||
// [assembly: AssemblyVersion("1.0.*")] | ||
[assembly: AssemblyVersion("1.0.0.0")] | ||
[assembly: AssemblyFileVersion("1.0.0.0")] |
Oops, something went wrong.