Improve XML documentation coverage across src modules and sync generated analysis artifacts.
This commit is contained in:
51
tools/DtpSnapshotExtractor/DtpSnapshotExtractor.csproj
Normal file
51
tools/DtpSnapshotExtractor/DtpSnapshotExtractor.csproj
Normal file
@@ -0,0 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<DotTraceAppDir Condition="'$(DotTraceAppDir)' == ''">$(HOME)/Applications/dotTrace.app/Contents/DotFiles</DotTraceAppDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="JetBrains.Platform.Core">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.Platform.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.Platform.Metadata">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.Platform.Metadata.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.Lifetimes">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.Lifetimes.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.Common.CallTreeStorage">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.Common.CallTreeStorage.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.Common.Util">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.Common.Util.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.Common.Util.Metadata">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.Common.Util.Metadata.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.DotTrace.Dal">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.DotTrace.Dal.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.DotTrace.Dal.Interface">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.DotTrace.Dal.Interface.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.DotTrace.Common.Dal.Interface">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.DotTrace.Common.Dal.Interface.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.DotTrace.Common.Dal">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.DotTrace.Common.Dal.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.DotTrace.DataStructures">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.DotTrace.DataStructures.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.Profiler.Snapshot">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.Profiler.Snapshot.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="JetBrains.Profiler.Snapshot.Interface">
|
||||
<HintPath>$(DotTraceAppDir)/JetBrains.Profiler.Snapshot.Interface.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
435
tools/DtpSnapshotExtractor/Program.cs
Normal file
435
tools/DtpSnapshotExtractor/Program.cs
Normal file
@@ -0,0 +1,435 @@
|
||||
using System.Text.Json;
|
||||
using JetBrains.Common.CallTreeStorage.Dfs;
|
||||
using JetBrains.Common.Util.Metadata;
|
||||
using JetBrains.DotTrace.Dal.Performance.RemoteTree.CoreLogic;
|
||||
using JetBrains.DotTrace.Dal.Performance.SnapshotComponents.BothSides;
|
||||
using JetBrains.DotTrace.Dal.Performance.SnapshotComponents.Remote;
|
||||
using JetBrains.DotTrace.Dal.Performance.SnapshotStorage;
|
||||
using JetBrains.DotTrace.Dal.Timeline.SnapshotComponents.Remote;
|
||||
using JetBrains.DotTrace.DalInterface.Performance.CallTree;
|
||||
using JetBrains.DotTrace.DalInterface.Performance.Fuids;
|
||||
using JetBrains.DotTrace.DataStructures.CallTree;
|
||||
using JetBrains.DotTrace.DataStructures.Payloads;
|
||||
using JetBrains.Lifetimes;
|
||||
using JetBrains.Metadata.Access;
|
||||
using JetBrains.Profiler.Snapshot.Converters;
|
||||
using JetBrains.Profiler.Snapshot.Interface.Section;
|
||||
using JetBrains.Profiler.Snapshot.Interface.Section.Metadata;
|
||||
using JetBrains.Profiler.Snapshot.Interface.Section.Metadata.Helpers;
|
||||
using JetBrains.Profiler.Snapshot.Section.Metadata;
|
||||
using JetBrains.Util;
|
||||
using JetBrains.Util.Logging;
|
||||
|
||||
return await ProgramMain(args);
|
||||
|
||||
static async Task<int> ProgramMain(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Console.Error.WriteLine("Usage: DtpSnapshotExtractor <snapshot.dtp>");
|
||||
return 2;
|
||||
}
|
||||
|
||||
var snapshotPath = Path.GetFullPath(args[0]);
|
||||
if (!File.Exists(snapshotPath))
|
||||
{
|
||||
Console.Error.WriteLine($"Snapshot not found: {snapshotPath}");
|
||||
return 2;
|
||||
}
|
||||
|
||||
var dotTraceAppDir = AppContext.GetData("APP_CONTEXT_DEPS_FILES") is not null
|
||||
? Environment.GetEnvironmentVariable("DOTTRACE_APP_DIR")
|
||||
: Environment.GetEnvironmentVariable("DOTTRACE_APP_DIR");
|
||||
RegisterAssemblyResolver(dotTraceAppDir);
|
||||
|
||||
using var lifetimeDef = Lifetime.Define();
|
||||
var lifetime = lifetimeDef.Lifetime;
|
||||
var logger = Logger.GetLogger("DtpSnapshotExtractor");
|
||||
var masks = new SnapshotMasksComponent();
|
||||
var snapshotFile = FileSystemPath.Parse(snapshotPath);
|
||||
var container = new SnapshotStorageContainer(lifetime, logger, masks, snapshotFile);
|
||||
var callTreeSections = new CallTreeSections(container, masks);
|
||||
var headerSections = new HeaderSections(container);
|
||||
var headerIndexSections = new HeaderIndexSections(container, masks);
|
||||
var headerCombinedSections = new HeaderCombinedSections(headerIndexSections, headerSections, masks);
|
||||
var singles = new HeaderIndexOptSections(container, masks);
|
||||
var accessor = new CallTreeAndIndexesSnapshotAccessor(callTreeSections, headerCombinedSections, singles);
|
||||
var dfsReaders = new DotTraceDfsReaders(accessor);
|
||||
var totalPayloadReader = new NodePayloadBatchReaderTotal(accessor);
|
||||
var ownPayloadReader = new NodePayloadBatchReaderOwn(accessor);
|
||||
|
||||
var metadataSection = (MetadataSection)new MetadataSectionContainer(container).Get(lifetime);
|
||||
var fuidConverter = new FuidToMetadataSectionContainer(container).Get();
|
||||
metadataSection.SetFuidConverter(fuidConverter);
|
||||
var assemblyProvider = metadataSection.GetMetadataAssemblyProvider(lifetime, loadReferencedAssemblies: false, guardMultiThreading: true);
|
||||
var resolver = new SignatureResolver(masks, fuidConverter, assemblyProvider);
|
||||
|
||||
var headers = callTreeSections.AllHeaders().ToArray();
|
||||
var totalNodeCount = headers.Sum(header =>
|
||||
checked((int)((header.HeaderFull.SectionSize - header.HeaderFull.SectionHeaderSize) / header.HeaderFull.RecordSize())));
|
||||
|
||||
var nodes = new DfsNode<CallTreeSectionOffset, FunctionUID>[totalNodeCount];
|
||||
var totals = new DotTracePayload[totalNodeCount];
|
||||
var owns = new DotTracePayload[totalNodeCount];
|
||||
|
||||
var readNodes = dfsReaders.GetNodesReaders(lifetime).ReadForwardOffsetsAscending(accessor.MinOffset, totalNodeCount, nodes, 0);
|
||||
var readTotals = totalPayloadReader.ReadForwardOffsetsAscending(accessor.MinOffset, totalNodeCount, totals, 0);
|
||||
var readOwns = ownPayloadReader.ReadForwardOffsetsAscending(accessor.MinOffset, totalNodeCount, owns, 0);
|
||||
|
||||
if (readNodes != totalNodeCount || readTotals != totalNodeCount || readOwns != totalNodeCount)
|
||||
throw new InvalidOperationException($"Snapshot read mismatch. nodes={readNodes}, totals={readTotals}, owns={readOwns}, expected={totalNodeCount}.");
|
||||
|
||||
var nodeMap = new Dictionary<CallTreeSectionOffset, MutableNode>(totalNodeCount);
|
||||
foreach (var index in Enumerable.Range(0, totalNodeCount))
|
||||
{
|
||||
var node = nodes[index];
|
||||
var signature = resolver.Resolve(node.Key);
|
||||
nodeMap[node.Offset] = new MutableNode
|
||||
{
|
||||
Id = node.Offset.ToString(),
|
||||
Name = signature.Name,
|
||||
Kind = signature.Kind,
|
||||
InclusiveTime = GetTotalTime(totals[index]),
|
||||
ExclusiveTime = GetTotalTime(owns[index]),
|
||||
CallCount = GetCallCount(totals[index]),
|
||||
Children = []
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var index in Enumerable.Range(0, totalNodeCount))
|
||||
{
|
||||
var node = nodes[index];
|
||||
if (!node.ParentOffset.IsValid)
|
||||
continue;
|
||||
|
||||
if (nodeMap.TryGetValue(node.ParentOffset, out var parent))
|
||||
parent.Children.Add(nodeMap[node.Offset]);
|
||||
}
|
||||
|
||||
var threadNodes = new List<SerializableNode>(headers.Length);
|
||||
foreach (var header in headers)
|
||||
{
|
||||
if (!nodeMap.TryGetValue(header.Root, out var rootNode))
|
||||
continue;
|
||||
|
||||
var threadName = string.IsNullOrWhiteSpace(header.ThreadName)
|
||||
? (string.IsNullOrWhiteSpace(header.GroupName) ? $"Thread {header.HeaderFull.GroupId}" : header.GroupName)
|
||||
: header.ThreadName;
|
||||
|
||||
threadNodes.Add(new SerializableNode
|
||||
{
|
||||
Id = $"thread:{header.HeaderFull.GroupId}:{rootNode.Id}",
|
||||
Name = threadName,
|
||||
Kind = "thread",
|
||||
ThreadName = threadName,
|
||||
InclusiveTime = rootNode.InclusiveTime,
|
||||
ExclusiveTime = 0,
|
||||
CallCount = rootNode.CallCount,
|
||||
Children = [CloneTree(header.Root, nodeMap, [])]
|
||||
});
|
||||
}
|
||||
|
||||
var syntheticRoot = new SerializableNode
|
||||
{
|
||||
Id = "root",
|
||||
Name = "<root>",
|
||||
Kind = "root",
|
||||
ThreadName = null,
|
||||
InclusiveTime = threadNodes.Sum(node => node.InclusiveTime),
|
||||
ExclusiveTime = 0,
|
||||
CallCount = threadNodes.Sum(node => node.CallCount),
|
||||
Children = threadNodes
|
||||
};
|
||||
|
||||
var hotspots = BuildHotspots(nodeMap.Values);
|
||||
var payload = new OutputDocument
|
||||
{
|
||||
Snapshot = new SnapshotInfo
|
||||
{
|
||||
Path = snapshotPath,
|
||||
PayloadType = "time",
|
||||
ThreadCount = threadNodes.Count,
|
||||
NodeCount = nodeMap.Count
|
||||
},
|
||||
ThreadRoots = threadNodes.Select(node => new ThreadRootInfo
|
||||
{
|
||||
Id = node.Id,
|
||||
Name = node.Name,
|
||||
InclusiveTime = node.InclusiveTime
|
||||
}).ToList(),
|
||||
Hotspots = hotspots,
|
||||
CallTree = syntheticRoot
|
||||
};
|
||||
|
||||
await JsonSerializer.SerializeAsync(Console.OpenStandardOutput(), payload, CreateJsonOptions());
|
||||
await Console.Out.WriteLineAsync();
|
||||
return 0;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine(exception);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static JsonSerializerOptions CreateJsonOptions() => new()
|
||||
{
|
||||
MaxDepth = 4096,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
static void RegisterAssemblyResolver(string? dotTraceAppDir)
|
||||
{
|
||||
var probeDir = string.IsNullOrWhiteSpace(dotTraceAppDir)
|
||||
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications", "dotTrace.app", "Contents", "DotFiles")
|
||||
: dotTraceAppDir;
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += (_, eventArgs) =>
|
||||
{
|
||||
var assemblyName = new System.Reflection.AssemblyName(eventArgs.Name).Name;
|
||||
if (string.IsNullOrWhiteSpace(assemblyName))
|
||||
return null;
|
||||
|
||||
var candidatePath = Path.Combine(probeDir, $"{assemblyName}.dll");
|
||||
return File.Exists(candidatePath) ? System.Reflection.Assembly.LoadFrom(candidatePath) : null;
|
||||
};
|
||||
}
|
||||
|
||||
static long GetTotalTime(DotTracePayload payload) => payload.PlusTime - payload.MinusTime;
|
||||
|
||||
static long GetCallCount(DotTracePayload payload) => payload.PlusCallCount - payload.MinusCallCount;
|
||||
|
||||
static HotspotLists BuildHotspots(IEnumerable<MutableNode> nodes)
|
||||
{
|
||||
var candidates = nodes
|
||||
.Where(node => node.Kind == "method")
|
||||
.Select(node => new HotspotEntry
|
||||
{
|
||||
Id = node.Id,
|
||||
Name = node.Name,
|
||||
Kind = node.Kind,
|
||||
InclusiveTime = node.InclusiveTime,
|
||||
ExclusiveTime = node.ExclusiveTime,
|
||||
CallCount = node.CallCount
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return new HotspotLists
|
||||
{
|
||||
Inclusive = candidates
|
||||
.OrderByDescending(node => node.InclusiveTime)
|
||||
.ThenBy(node => node.Name, StringComparer.Ordinal)
|
||||
.Take(50)
|
||||
.ToList(),
|
||||
Exclusive = candidates
|
||||
.OrderByDescending(node => node.ExclusiveTime)
|
||||
.ThenBy(node => node.Name, StringComparer.Ordinal)
|
||||
.Take(50)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
static SerializableNode CloneTree(
|
||||
CallTreeSectionOffset offset,
|
||||
IReadOnlyDictionary<CallTreeSectionOffset, MutableNode> nodeMap,
|
||||
HashSet<CallTreeSectionOffset> ancestry)
|
||||
{
|
||||
var source = nodeMap[offset];
|
||||
var nextAncestry = new HashSet<CallTreeSectionOffset>(ancestry) { offset };
|
||||
var children = new List<SerializableNode>(source.Children.Count);
|
||||
|
||||
foreach (var child in source.Children)
|
||||
{
|
||||
var childOffset = ParseOffset(child.Id);
|
||||
if (nextAncestry.Contains(childOffset))
|
||||
continue;
|
||||
|
||||
children.Add(CloneTree(childOffset, nodeMap, nextAncestry));
|
||||
}
|
||||
|
||||
return new SerializableNode
|
||||
{
|
||||
Id = source.Id,
|
||||
Name = source.Name,
|
||||
Kind = source.Kind,
|
||||
ThreadName = null,
|
||||
InclusiveTime = source.InclusiveTime,
|
||||
ExclusiveTime = source.ExclusiveTime,
|
||||
CallCount = source.CallCount,
|
||||
Children = children
|
||||
};
|
||||
}
|
||||
|
||||
static CallTreeSectionOffset ParseOffset(string value)
|
||||
{
|
||||
var parts = value.Split('/');
|
||||
return new CallTreeSectionOffset(Convert.ToInt64(parts[0], 16), int.Parse(parts[1]));
|
||||
}
|
||||
|
||||
sealed class SignatureResolver(
|
||||
SnapshotMasksComponent masks,
|
||||
FuidToMetadataIdConverter fuidConverter,
|
||||
IMetadataSectionAssemblyProvider assemblyProvider)
|
||||
{
|
||||
private readonly Dictionary<FunctionUID, SignatureResult> _cache = [];
|
||||
|
||||
public SignatureResult Resolve(FunctionUID fuid)
|
||||
{
|
||||
if (_cache.TryGetValue(fuid, out var cached))
|
||||
return cached;
|
||||
|
||||
var result = ResolveCore(fuid);
|
||||
_cache[fuid] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
private SignatureResult ResolveCore(FunctionUID fuid)
|
||||
{
|
||||
var fid = fuid.ToDotTraceFid(masks);
|
||||
if (fid == KnownFunctionId.RootId)
|
||||
return new SignatureResult("<thread-root>", "special");
|
||||
if (fid == KnownFunctionId.NativeCallId)
|
||||
return new SignatureResult("<native>", "special");
|
||||
if (fid == KnownFunctionId.ThreadStoppedByUnknownReasonCallId)
|
||||
return new SignatureResult("<thread-stopped>", "special");
|
||||
if (fid == KnownFunctionId.GcCallId)
|
||||
return new SignatureResult("<gc>", "special");
|
||||
if (fid == KnownFunctionId.AppDomainShutdownCallId)
|
||||
return new SignatureResult("<appdomain-shutdown>", "special");
|
||||
if (fid == KnownFunctionId.CodePitchingCallId)
|
||||
return new SignatureResult("<code-pitching>", "special");
|
||||
if (fid == KnownFunctionId.UnmanagedStackFrameCallId)
|
||||
return new SignatureResult("<unmanaged-stack-frame>", "special");
|
||||
if (fid == KnownFunctionId.UnsafeStackFrameId)
|
||||
return new SignatureResult("<unsafe-stack-frame>", "special");
|
||||
if (fid == KnownFunctionId.ContinuationsId)
|
||||
return new SignatureResult("<continuations>", "special");
|
||||
if (fid == KnownFunctionId.AwaitsId)
|
||||
return new SignatureResult("<awaits>", "special");
|
||||
if (fid == KnownFunctionId.TaskScheduledId)
|
||||
return new SignatureResult("<task-scheduled>", "special");
|
||||
if (fid == KnownFunctionId.TaskExecutionId)
|
||||
return new SignatureResult("<task-execution>", "special");
|
||||
if (fid == KnownFunctionId.TaskRecursionId)
|
||||
return new SignatureResult("<task-recursion>", "special");
|
||||
if (!KnownFunctionId.IsProcessable(fid))
|
||||
return new SignatureResult($"[special:0x{(uint)fid:X8}]", "special");
|
||||
|
||||
try
|
||||
{
|
||||
var metadataId = fuidConverter.Convert(fid);
|
||||
if (metadataId.IsSynthetic)
|
||||
return new SignatureResult($"[synthetic:{metadataId}]", "special");
|
||||
|
||||
var function = assemblyProvider.GetFunctionItemsLight(metadataId);
|
||||
var className = function.ClassToken != MetadataToken.Nil
|
||||
? assemblyProvider.GetClassItems(MetadataId.Create(metadataId.MetadataIndex, function.ClassToken)).FullName
|
||||
: null;
|
||||
var qualifiedMethod = string.IsNullOrWhiteSpace(className)
|
||||
? function.FunctionName
|
||||
: $"{className}.{function.FunctionName}";
|
||||
var methodName = string.IsNullOrEmpty(function.GenericParameters)
|
||||
? qualifiedMethod
|
||||
: $"{qualifiedMethod}<{function.GenericParameters}>";
|
||||
return new SignatureResult(methodName, "method");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new SignatureResult($"[unresolved:0x{(uint)fid:X8}]", "special");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly record struct SignatureResult(string Name, string Kind);
|
||||
|
||||
sealed class OutputDocument
|
||||
{
|
||||
public required SnapshotInfo Snapshot { get; init; }
|
||||
|
||||
public required List<ThreadRootInfo> ThreadRoots { get; init; }
|
||||
|
||||
public required HotspotLists Hotspots { get; init; }
|
||||
|
||||
public required SerializableNode CallTree { get; init; }
|
||||
}
|
||||
|
||||
sealed class SnapshotInfo
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
|
||||
public required string PayloadType { get; init; }
|
||||
|
||||
public required int ThreadCount { get; init; }
|
||||
|
||||
public required int NodeCount { get; init; }
|
||||
}
|
||||
|
||||
sealed class ThreadRootInfo
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
|
||||
public required string Name { get; init; }
|
||||
|
||||
public required long InclusiveTime { get; init; }
|
||||
}
|
||||
|
||||
sealed class HotspotLists
|
||||
{
|
||||
public required List<HotspotEntry> Inclusive { get; init; }
|
||||
|
||||
public required List<HotspotEntry> Exclusive { get; init; }
|
||||
}
|
||||
|
||||
sealed class HotspotEntry
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
|
||||
public required string Name { get; init; }
|
||||
|
||||
public required string Kind { get; init; }
|
||||
|
||||
public required long InclusiveTime { get; init; }
|
||||
|
||||
public required long ExclusiveTime { get; init; }
|
||||
|
||||
public required long CallCount { get; init; }
|
||||
}
|
||||
|
||||
class MutableNode
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
|
||||
public required string Name { get; init; }
|
||||
|
||||
public required string Kind { get; init; }
|
||||
|
||||
public required long InclusiveTime { get; init; }
|
||||
|
||||
public required long ExclusiveTime { get; init; }
|
||||
|
||||
public required long CallCount { get; init; }
|
||||
|
||||
public required List<MutableNode> Children { get; init; }
|
||||
}
|
||||
|
||||
sealed class SerializableNode
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
|
||||
public required string Name { get; init; }
|
||||
|
||||
public required string Kind { get; init; }
|
||||
|
||||
public string? ThreadName { get; init; }
|
||||
|
||||
public required long InclusiveTime { get; init; }
|
||||
|
||||
public required long ExclusiveTime { get; init; }
|
||||
|
||||
public required long CallCount { get; init; }
|
||||
|
||||
public required List<SerializableNode> Children { get; init; }
|
||||
}
|
||||
92
tools/dtp_parse.py
Normal file
92
tools/dtp_parse.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HELPER_PROJECT = ROOT / "tools" / "DtpSnapshotExtractor" / "DtpSnapshotExtractor.csproj"
|
||||
HELPER_DLL = ROOT / "tools" / "DtpSnapshotExtractor" / "bin" / "Debug" / "net10.0" / "DtpSnapshotExtractor.dll"
|
||||
DEFAULT_DOTTRACE_DIR = Path.home() / "Applications" / "dotTrace.app" / "Contents" / "DotFiles"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Parse a raw dotTrace .dtp snapshot family into JSON call-tree data."
|
||||
)
|
||||
parser.add_argument("snapshot", help="Path to the .dtp snapshot index file.")
|
||||
parser.add_argument("--out", help="Write JSON to this file.")
|
||||
parser.add_argument("--stdout", action="store_true", help="Write JSON to stdout.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def find_dottrace_dir() -> Path:
|
||||
value = os.environ.get("DOTTRACE_APP_DIR")
|
||||
if value:
|
||||
candidate = Path(value).expanduser()
|
||||
if candidate.is_dir():
|
||||
return candidate
|
||||
if DEFAULT_DOTTRACE_DIR.is_dir():
|
||||
return DEFAULT_DOTTRACE_DIR
|
||||
raise FileNotFoundError(
|
||||
f"dotTrace assemblies not found. Set DOTTRACE_APP_DIR or install dotTrace under {DEFAULT_DOTTRACE_DIR}."
|
||||
)
|
||||
|
||||
|
||||
def build_helper(dottrace_dir: Path) -> None:
|
||||
command = [
|
||||
"dotnet",
|
||||
"build",
|
||||
str(HELPER_PROJECT),
|
||||
"-nologo",
|
||||
"-clp:ErrorsOnly",
|
||||
f"-p:DotTraceAppDir={dottrace_dir}",
|
||||
]
|
||||
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "dotnet build failed")
|
||||
|
||||
|
||||
def run_helper(snapshot: Path, dottrace_dir: Path) -> dict:
|
||||
command = ["dotnet", str(HELPER_DLL), str(snapshot)]
|
||||
env = os.environ.copy()
|
||||
env["DOTTRACE_APP_DIR"] = str(dottrace_dir)
|
||||
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False, env=env)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "extractor failed")
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
snapshot = Path(args.snapshot).expanduser().resolve()
|
||||
if not snapshot.is_file():
|
||||
print(f"Snapshot not found: {snapshot}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if not args.stdout and not args.out:
|
||||
args.stdout = True
|
||||
|
||||
try:
|
||||
dottrace_dir = find_dottrace_dir()
|
||||
build_helper(dottrace_dir)
|
||||
payload = run_helper(snapshot, dottrace_dir)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
text = json.dumps(payload, indent=2)
|
||||
if args.out:
|
||||
out_path = Path(args.out).expanduser().resolve()
|
||||
out_path.write_text(text + "\n", encoding="utf-8")
|
||||
if args.stdout:
|
||||
sys.stdout.write(text + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
BIN
tools/tests/__pycache__/test_dtp_parser.cpython-314.pyc
Normal file
BIN
tools/tests/__pycache__/test_dtp_parser.cpython-314.pyc
Normal file
Binary file not shown.
41
tools/tests/test_dtp_parser.py
Normal file
41
tools/tests/test_dtp_parser.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import json
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SNAPSHOT = ROOT / "snapshots" / "js-ordered-consume.dtp"
|
||||
SCRIPT = ROOT / "tools" / "dtp_parse.py"
|
||||
|
||||
|
||||
def walk(node):
|
||||
yield node
|
||||
for child in node.get("children", []):
|
||||
yield from walk(child)
|
||||
|
||||
|
||||
class DtpParserTests(unittest.TestCase):
|
||||
def test_emits_machine_readable_call_tree(self):
|
||||
result = subprocess.run(
|
||||
["python3", str(SCRIPT), str(SNAPSHOT), "--stdout"],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
self.assertIn("callTree", payload)
|
||||
self.assertIn("hotspots", payload)
|
||||
self.assertTrue(payload["callTree"]["children"])
|
||||
self.assertTrue(payload["hotspots"]["inclusive"])
|
||||
|
||||
node_names = [node["name"] for node in walk(payload["callTree"])]
|
||||
self.assertTrue(any(not name.startswith("[special:") for name in node_names))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user