Skip to content

Commit

Permalink
Add HeapSnapshotCompare project
Browse files Browse the repository at this point in the history
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
randomascii committed Jan 21, 2020
1 parent 677d404 commit b2bc3dd
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 0 deletions.
6 changes: 6 additions & 0 deletions TraceProcessors/HeapSnapshotCompare/App.config
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 TraceProcessors/HeapSnapshotCompare/HeapSnapshotCompare.cs
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 TraceProcessors/HeapSnapshotCompare/HeapSnapshotCompare.csproj
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 TraceProcessors/HeapSnapshotCompare/Properties/AssemblyInfo.cs
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")]
Loading

0 comments on commit b2bc3dd

Please sign in to comment.