Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions EtwToPprof.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>0.1.0</Version>
<Authors>Sunny Sachanandani</Authors>
<Company>Google LLC</Company>
Expand Down
139 changes: 129 additions & 10 deletions ProfileWriter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Google LLC
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -67,6 +67,16 @@ public ProfileWriter(Options options)

functions = new Dictionary<Function, ulong>();
nextFunctionId = 1;

mappings = new Dictionary<string, ulong>();
nextMappingId = 1;

unsymbolizedLocations = new Dictionary<ulong, ulong>();

mappingsWithUnsymbolized = new HashSet<ulong>();
// Maps mapping ID -> absolute base address of the loaded image.
// Used to convert absolute VAs to RVAs for Location.address.
mappingBaseAddresses = new Dictionary<ulong, ulong>();
}

public void AddSample(ICpuSample sample)
Expand All @@ -79,7 +89,7 @@ public void AddSample(ICpuSample sample)
if (timestamp < options.timeStart || timestamp > options.timeEnd)
return;

if (options.processFilterSet?.Count != 0)
if (options.processFilterSet?.Count > 0)
{
var processImage = sample.Process.Images.FirstOrDefault(
image => image.FileName == sample.Process.ImageName);
Expand All @@ -102,14 +112,12 @@ public void AddSample(ICpuSample sample)
{
if (stackFrame.HasValue && stackFrame.Symbol != null)
{
sampleProto.LocationId.Add(GetLocationId(stackFrame.Symbol));
sampleProto.LocationId.Add(GetLocationId(stackFrame.Symbol, stackFrame.Address));
}
else
else if (stackFrame.HasValue)
{
string imageName = stackFrame.Image?.FileName ?? "<unknown>";
string functionLabel = "<unknown>";
sampleProto.LocationId.Add(
GetPseudoLocationId(processId, imageName, null, functionLabel));
GetUnsymbolizedLocationId(stackFrame.Address, stackFrame.Image));
}
}
string processName = sample.Process.ImageName;
Expand All @@ -120,8 +128,12 @@ public void AddSample(ICpuSample sample)
{
threadLabel = String.Format("{0} ({1})", threadLabel, sample.Thread?.Id ?? 0);
}
// When process IDs are not included, use 0/null so that processes
// with the same label merge into a single flame graph entry.
int threadPseudoProcessId = (options.includeProcessIds || options.includeProcessAndThreadIds)
? processId : 0;
sampleProto.LocationId.Add(
GetPseudoLocationId(processId, processName, sample.Thread?.StartAddress, threadLabel));
GetPseudoLocationId(threadPseudoProcessId, processName, sample.Thread?.StartAddress, threadLabel));

string processLabel = processName;
if (options.splitChromeProcesses && processName == "chrome.exe" &&
Expand Down Expand Up @@ -156,8 +168,12 @@ public void AddSample(ICpuSample sample)
{
processLabel = processLabel + $" ({processId})";
}
// When process IDs are not included, use 0/null so that processes
// with the same label merge into a single flame graph entry.
int pseudoProcessId = (options.includeProcessIds || options.includeProcessAndThreadIds)
? processId : 0;
sampleProto.LocationId.Add(
GetPseudoLocationId(processId, processName, sample.Process.ObjectAddress, processLabel));
GetPseudoLocationId(pseudoProcessId, processName, null, processLabel));

if (processThreadCpuTimes.ContainsKey(processLabel))
{
Expand Down Expand Up @@ -225,6 +241,16 @@ public long Write(string outputFileName)
{
profile.Comment.Add(GetStringId("No samples exported"));
}
// Set Has* flags on mappings: only claim symbolization for mappings
// where all locations were successfully resolved.
foreach (var mappingProto in profile.Mapping)
{
bool fullySymbolized = !mappingsWithUnsymbolized.Contains(mappingProto.Id);
mappingProto.HasFunctions = fullySymbolized;
mappingProto.HasFilenames = fullySymbolized;
mappingProto.HasLineNumbers = fullySymbolized;
mappingProto.HasInlineFrames = fullySymbolized && options.includeInlinedFunctions;
}
using (FileStream output = File.Create(outputFileName))
{
using (GZipStream gzip = new GZipStream(output, CompressionMode.Compress))
Expand Down Expand Up @@ -278,6 +304,8 @@ ulong GetPseudoLocationId(int processId, string imageName, Address? address, str

var locationProto = new pb.Location();
locationProto.Id = locationId;
if (address.HasValue)
locationProto.Address = unchecked((ulong)address.Value.Value);

var line = new pb.Line();
line.FunctionId = GetFunctionId(imageName, label);
Expand All @@ -288,7 +316,34 @@ ulong GetPseudoLocationId(int processId, string imageName, Address? address, str
return locationId;
}

ulong GetLocationId(IStackSymbol stackSymbol)
ulong GetUnsymbolizedLocationId(Address address, IImage image)
{
// Use the raw address as the dedup key for unsymbolized frames.
ulong addr = unchecked((ulong)address.Value);
if (!unsymbolizedLocations.TryGetValue(addr, out ulong locationId))
{
locationId = nextLocationId++;
unsymbolizedLocations.Add(addr, locationId);

var locationProto = new pb.Location();
locationProto.Id = locationId;
locationProto.Address = addr;
if (image != null)
{
ulong mid = GetMappingId(image);
locationProto.MappingId = mid;
// Convert absolute VA to RVA (see comment in GetMappingId).
locationProto.Address = addr - mappingBaseAddresses[mid];
mappingsWithUnsymbolized.Add(mid);
}

// No Line entries — leaves the location bare for offline symbolization.
profile.Location.Add(locationProto);
}
return locationId;
}

ulong GetLocationId(IStackSymbol stackSymbol, Address instructionAddress)
{
var processId = stackSymbol.Image?.ProcessId ?? 0;
var imageName = stackSymbol.Image?.FileName;
Expand All @@ -306,6 +361,15 @@ ulong GetLocationId(IStackSymbol stackSymbol)

var locationProto = new pb.Location();
locationProto.Id = locationId;
// Store the RVA (see comment in GetMappingId).
ulong absAddr = unchecked((ulong)instructionAddress.Value);
locationProto.Address = absAddr;
if (stackSymbol.Image != null)
{
ulong mid = GetMappingId(stackSymbol.Image);
locationProto.MappingId = mid;
locationProto.Address = absAddr - mappingBaseAddresses[mid];
}

pb.Line line;
if (options.includeInlinedFunctions && stackSymbol.InlinedFunctionNames != null)
Expand Down Expand Up @@ -396,11 +460,66 @@ long GetStringId(string str)
private readonly Options options;

Dictionary<Location, ulong> locations;
Dictionary<ulong, ulong> unsymbolizedLocations;
ulong nextLocationId;

Dictionary<Function, ulong> functions;
ulong nextFunctionId;

Dictionary<string, ulong> mappings;
ulong nextMappingId;
HashSet<ulong> mappingsWithUnsymbolized;
// Maps mapping ID -> absolute base address of the loaded image.
// Used to convert absolute VAs to RVAs for Location.address.
Dictionary<ulong, ulong> mappingBaseAddresses;

static string FormatBreakpadBuildId(IImage image)
{
if (image.Pdb == null)
return null;
return image.Pdb.Id.ToString("N").ToLowerInvariant()
+ image.Pdb.Age.ToString("x");
}

ulong GetMappingId(IImage image)
{
// Key by image path to deduplicate mappings for the same binary.
string key = image.Path ?? image.FileName ?? "<unknown>";
ulong mappingId;
if (!mappings.TryGetValue(key, out mappingId))
{
mappingId = nextMappingId++;
mappings.Add(key, mappingId);

var mappingProto = new pb.Mapping();
mappingProto.Id = mappingId;

// Workaround for pprof symbolization servers that assume ELF binaries:
// Some servers reject memory_start values that don't match standard
// Linux load addresses (0, 0x400000, 0x8048000) when ElfHeaders are
// absent. Windows PE/PDB binaries never have ElfHeaders, so any real
// Windows load address causes a symbolization failure.
// By setting memory_start=0 and memory_limit=module_size, and storing
// RVAs in Location.address, we ensure compatibility with servers that
// use memory_start==0 as a passthrough for RVA-based symbol lookup.
ulong baseAddr = unchecked((ulong)image.AddressRange.BaseAddress.Value);
mappingBaseAddresses.Add(mappingId, baseAddr);
mappingProto.MemoryStart = 0;
mappingProto.MemoryLimit = (ulong)image.Size.Bytes;
mappingProto.FileOffset = 0;
mappingProto.Filename = GetStringId(image.Path ?? image.FileName ?? "<unknown>");

string buildId = FormatBreakpadBuildId(image);
if (buildId != null)
mappingProto.BuildId = GetStringId(buildId);

// Has* flags are finalized in Write() after all samples are processed.

profile.Mapping.Add(mappingProto);
}
return mappingId;
}

Dictionary<string, long> strings;
long nextStringId;

Expand Down
60 changes: 59 additions & 1 deletion Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

using System;
using System.Collections.Generic;
using System.Linq;

using CommandLine;
using CommandLine.Text;
Expand Down Expand Up @@ -86,8 +87,16 @@ public static IEnumerable<Example> Examples
HelpText = "Whether chrome.exe processes are split by type (parsed from command line).")]
public bool splitChromeProcesses { get; set; }

[Option("noSplitChromeProcesses", Required = false, Default = false,
HelpText = "Merge all chrome.exe processes under a single heading.")]
public bool noSplitChromeProcesses { get; set; }

[Option("loadSymbols", Required = false, Default = true, HelpText = "Whether symbols should be loaded.")]
public bool? loadSymbols { get; set; }

[Option("listImageIds", Required = false, Default = false,
HelpText = "List all unique image (module) names found in the trace and exit.")]
public bool listImageIds { get; set; }
}

static void Main(string[] args)
Expand Down Expand Up @@ -123,12 +132,61 @@ static void RunWithOptions(Options opts)

ICpuSampleDataSource cpuSampleData = pendingCpuSampleData.Result;

// --listImageIds: list all unique images with their Breakpad build IDs and exit.
if (opts.listImageIds)
{
var images = new Dictionary<string, List<(string path, string buildId, long timestamp)>>();
var seenProcessIds = new HashSet<int>();
foreach (var sample in cpuSampleData.Samples)
{
if (sample.Process == null)
continue;
if (!seenProcessIds.Add(sample.Process.Id))
continue;
foreach (var image in sample.Process.Images)
{
string fileName = image.FileName;
if (fileName == null)
continue;
if (!images.ContainsKey(fileName))
images[fileName] = new List<(string, string, long)>();

string buildId = null;
if (image.Pdb != null)
{
buildId = image.Pdb.Id.ToString("N").ToLowerInvariant()
+ image.Pdb.Age.ToString("x");
}
long timestamp = image.Timestamp;
string path = image.Path;

if (!images[fileName].Any(e => e.buildId == buildId && e.timestamp == timestamp))
{
images[fileName].Add((path, buildId, timestamp));
}
}
}

foreach (var kvp in images.OrderBy(k => k.Key))
{
Console.WriteLine($"{kvp.Key}:");
foreach (var (path, buildId, timestamp) in kvp.Value)
{
Console.WriteLine($" Path: {path}");
Console.WriteLine($" BuildId: {buildId ?? "(none)"}");
Console.WriteLine($" Timestamp: {timestamp}");
Console.WriteLine();
}
}
return;
}

var profileOpts = new ProfileWriter.Options();
profileOpts.etlFileName = opts.etlFileName;
profileOpts.includeInlinedFunctions = opts.includeInlinedFunctions;
profileOpts.includeProcessIds = opts.includeProcessIds;
profileOpts.includeProcessAndThreadIds = opts.includeProcessAndThreadIds;
profileOpts.splitChromeProcesses = opts.splitChromeProcesses;
profileOpts.splitChromeProcesses = opts.splitChromeProcesses && !opts.noSplitChromeProcesses;
profileOpts.stripSourceFileNamePrefix = opts.stripSourceFileNamePrefix;
profileOpts.timeStart = opts.timeStart ?? 0;
profileOpts.timeEnd = opts.timeEnd ?? decimal.MaxValue;
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ symbolizing traces if set, otherwise it uses WPA defaults.

## Building

Build the provided Visual Studio Solution with VS 2019.
Build with the .NET 10 SDK:

dotnet build -c Release

### Nuget dependencies (included in solution)
- CommandLineParser v2.8.0
Expand All @@ -36,6 +38,14 @@ Export inlined functions and thread/process ids:

EtwToPprof --includeInlinedFunctions --includeProcessAndThreadIds trace.etl

Merge all chrome.exe processes under a single heading:

EtwToPprof --noSplitChromeProcesses trace.etl

List all loaded images with their Breakpad build IDs:

EtwToPprof --listImageIds trace.etl

## Command line flags

-o, --outputFileName (Default: profile.pb.gz) Output file name for gzipped pprof profile.
Expand All @@ -56,14 +66,25 @@ Export inlined functions and thread/process ids:

--splitChromeProcesses (Default: true) Whether chrome.exe processes are split by type (parsed from command line).

--noSplitChromeProcesses (Default: false) Merge all chrome.exe processes under a single heading.

--loadSymbols (Default: true) Whether symbols should be loaded.

--listImageIds (Default: false) List all unique image (module) names with Breakpad build IDs and exit.

--help Display this help screen.

--version Display version information.

etlFileName (pos. 0) Required. ETL trace file name.

## Offline Symbolization

Profiles include Mapping entries with Breakpad-format build IDs and RVA-based
addresses. Unsymbolized frames are emitted as bare locations (address + mapping
only) so that pprof symbolization servers can resolve them using symbol servers
without requiring local PDBs.

## Disclaimer:

**This is not an officially supported Google product.**