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 ProgramMain(string[] args) { try { if (args.Length == 0) { Console.Error.WriteLine("Usage: DtpSnapshotExtractor "); 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[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(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(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 = "", 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 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 nodeMap, HashSet ancestry) { var source = nodeMap[offset]; var nextAncestry = new HashSet(ancestry) { offset }; var children = new List(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 _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("", "special"); if (fid == KnownFunctionId.NativeCallId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.ThreadStoppedByUnknownReasonCallId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.GcCallId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.AppDomainShutdownCallId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.CodePitchingCallId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.UnmanagedStackFrameCallId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.UnsafeStackFrameId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.ContinuationsId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.AwaitsId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.TaskScheduledId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.TaskExecutionId) return new SignatureResult("", "special"); if (fid == KnownFunctionId.TaskRecursionId) return new SignatureResult("", "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 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 Inclusive { get; init; } public required List 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 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 Children { get; init; } }