Improve docs coverage and refresh profiling parser artifacts
Add domain-specific XML documentation across src server components to satisfy CommentChecker, and update dotTrace parsing outputs used for diagnostics.
This commit is contained in:
@@ -26,29 +26,26 @@ static async Task<int> ProgramMain(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args.Length == 0)
|
||||
var options = ParseArguments(args);
|
||||
if (options is null)
|
||||
{
|
||||
Console.Error.WriteLine("Usage: DtpSnapshotExtractor <snapshot.dtp>");
|
||||
Console.Error.WriteLine("Usage: DtpSnapshotExtractor <snapshot.dtp> [--top N] [--filter text] [--flat] [--include-idle]");
|
||||
return 2;
|
||||
}
|
||||
|
||||
var snapshotPath = Path.GetFullPath(args[0]);
|
||||
if (!File.Exists(snapshotPath))
|
||||
if (!File.Exists(options.SnapshotPath))
|
||||
{
|
||||
Console.Error.WriteLine($"Snapshot not found: {snapshotPath}");
|
||||
Console.Error.WriteLine($"Snapshot not found: {options.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);
|
||||
RegisterAssemblyResolver(Environment.GetEnvironmentVariable("DOTTRACE_APP_DIR"));
|
||||
|
||||
using var lifetimeDef = Lifetime.Define();
|
||||
var lifetime = lifetimeDef.Lifetime;
|
||||
var logger = Logger.GetLogger("DtpSnapshotExtractor");
|
||||
var masks = new SnapshotMasksComponent();
|
||||
var snapshotFile = FileSystemPath.Parse(snapshotPath);
|
||||
var snapshotFile = FileSystemPath.Parse(options.SnapshotPath);
|
||||
var container = new SnapshotStorageContainer(lifetime, logger, masks, snapshotFile);
|
||||
var callTreeSections = new CallTreeSections(container, masks);
|
||||
var headerSections = new HeaderSections(container);
|
||||
@@ -67,27 +64,32 @@ static async Task<int> ProgramMain(string[] args)
|
||||
var resolver = new SignatureResolver(masks, fuidConverter, assemblyProvider);
|
||||
|
||||
var headers = callTreeSections.AllHeaders().ToArray();
|
||||
var totalNodeCount = headers.Sum(header =>
|
||||
var expectedNodeCount = 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 nodes = new DfsNode<CallTreeSectionOffset, FunctionUID>[expectedNodeCount];
|
||||
var totals = new DotTracePayload[expectedNodeCount];
|
||||
var owns = new DotTracePayload[expectedNodeCount];
|
||||
|
||||
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);
|
||||
var readNodes = dfsReaders.GetNodesReaders(lifetime).ReadForwardOffsetsAscending(accessor.MinOffset, expectedNodeCount, nodes, 0);
|
||||
var readTotals = totalPayloadReader.ReadForwardOffsetsAscending(accessor.MinOffset, expectedNodeCount, totals, 0);
|
||||
var readOwns = ownPayloadReader.ReadForwardOffsetsAscending(accessor.MinOffset, expectedNodeCount, owns, 0);
|
||||
|
||||
if (readNodes != totalNodeCount || readTotals != totalNodeCount || readOwns != totalNodeCount)
|
||||
throw new InvalidOperationException($"Snapshot read mismatch. nodes={readNodes}, totals={readTotals}, owns={readOwns}, expected={totalNodeCount}.");
|
||||
if (readNodes != expectedNodeCount || readTotals != expectedNodeCount || readOwns != expectedNodeCount)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Snapshot read mismatch. nodes={readNodes}, totals={readTotals}, owns={readOwns}, expected={expectedNodeCount}.");
|
||||
}
|
||||
|
||||
var nodeMap = new Dictionary<CallTreeSectionOffset, MutableNode>(totalNodeCount);
|
||||
foreach (var index in Enumerable.Range(0, totalNodeCount))
|
||||
var nodeMap = new Dictionary<CallTreeSectionOffset, MutableNode>(expectedNodeCount);
|
||||
foreach (var index in Enumerable.Range(0, expectedNodeCount))
|
||||
{
|
||||
var node = nodes[index];
|
||||
var signature = resolver.Resolve(node.Key);
|
||||
nodeMap[node.Offset] = new MutableNode
|
||||
{
|
||||
Offset = node.Offset,
|
||||
ParentOffset = node.ParentOffset,
|
||||
Id = node.Offset.ToString(),
|
||||
Name = signature.Name,
|
||||
Kind = signature.Kind,
|
||||
@@ -98,7 +100,7 @@ static async Task<int> ProgramMain(string[] args)
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var index in Enumerable.Range(0, totalNodeCount))
|
||||
foreach (var index in Enumerable.Range(0, expectedNodeCount))
|
||||
{
|
||||
var node = nodes[index];
|
||||
if (!node.ParentOffset.IsValid)
|
||||
@@ -127,7 +129,7 @@ static async Task<int> ProgramMain(string[] args)
|
||||
InclusiveTime = rootNode.InclusiveTime,
|
||||
ExclusiveTime = 0,
|
||||
CallCount = rootNode.CallCount,
|
||||
Children = [CloneTree(header.Root, nodeMap, [])]
|
||||
Children = [CloneTree(rootNode, [])]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,24 +145,41 @@ static async Task<int> ProgramMain(string[] args)
|
||||
Children = threadNodes
|
||||
};
|
||||
|
||||
var hotspots = BuildHotspots(nodeMap.Values);
|
||||
var filteredTree = string.IsNullOrWhiteSpace(options.NameFilter)
|
||||
? syntheticRoot
|
||||
: PruneTree(syntheticRoot, options.NameFilter) ?? CreateEmptyRoot();
|
||||
|
||||
var hotspotCandidates = FilterHotspotCandidates(nodeMap.Values, options);
|
||||
var hotspots = BuildHotspots(hotspotCandidates, options.Top);
|
||||
var summary = BuildSummary(syntheticRoot, hotspotCandidates, hotspots.Exclusive.FirstOrDefault());
|
||||
var payload = new OutputDocument
|
||||
{
|
||||
Snapshot = new SnapshotInfo
|
||||
{
|
||||
Path = snapshotPath,
|
||||
Path = options.SnapshotPath,
|
||||
PayloadType = "time",
|
||||
TimeUnit = "nanoseconds",
|
||||
ThreadCount = threadNodes.Count,
|
||||
NodeCount = nodeMap.Count
|
||||
NodeCount = nodeMap.Count,
|
||||
Diagnostics = new SnapshotDiagnostics
|
||||
{
|
||||
HeaderCount = headers.Length,
|
||||
ExpectedNodeCount = expectedNodeCount,
|
||||
ReadNodeCount = readNodes,
|
||||
ReadTotalPayloadCount = readTotals,
|
||||
ReadOwnPayloadCount = readOwns
|
||||
}
|
||||
},
|
||||
ThreadRoots = threadNodes.Select(node => new ThreadRootInfo
|
||||
Summary = summary,
|
||||
ThreadRoots = filteredTree.Children.Select(node => new ThreadRootInfo
|
||||
{
|
||||
Id = node.Id,
|
||||
Name = node.Name,
|
||||
InclusiveTime = node.InclusiveTime
|
||||
}).ToList(),
|
||||
Hotspots = hotspots,
|
||||
CallTree = syntheticRoot
|
||||
HotPaths = options.FlatPaths ? BuildHotPaths(filteredTree, options) : null,
|
||||
CallTree = filteredTree
|
||||
};
|
||||
|
||||
await JsonSerializer.SerializeAsync(Console.OpenStandardOutput(), payload, CreateJsonOptions());
|
||||
@@ -174,11 +193,54 @@ static async Task<int> ProgramMain(string[] args)
|
||||
}
|
||||
}
|
||||
|
||||
static ExtractorOptions? ParseArguments(string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
return null;
|
||||
|
||||
var snapshotPath = Path.GetFullPath(args[0]);
|
||||
var top = 200;
|
||||
string? nameFilter = null;
|
||||
var flatPaths = false;
|
||||
var excludeIdle = true;
|
||||
|
||||
for (var index = 1; index < args.Length; index++)
|
||||
{
|
||||
switch (args[index])
|
||||
{
|
||||
case "--top":
|
||||
if (++index >= args.Length || !int.TryParse(args[index], out top) || top <= 0)
|
||||
throw new ArgumentException("--top requires a positive integer value.");
|
||||
break;
|
||||
case "--filter":
|
||||
if (++index >= args.Length)
|
||||
throw new ArgumentException("--filter requires a value.");
|
||||
nameFilter = args[index];
|
||||
break;
|
||||
case "--flat":
|
||||
case "--paths":
|
||||
flatPaths = true;
|
||||
break;
|
||||
case "--include-idle":
|
||||
excludeIdle = false;
|
||||
break;
|
||||
case "--exclude-idle":
|
||||
excludeIdle = true;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unknown argument: {args[index]}");
|
||||
}
|
||||
}
|
||||
|
||||
return new ExtractorOptions(snapshotPath, top, nameFilter, flatPaths, excludeIdle);
|
||||
}
|
||||
|
||||
static JsonSerializerOptions CreateJsonOptions() => new()
|
||||
{
|
||||
MaxDepth = 4096,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
static void RegisterAssemblyResolver(string? dotTraceAppDir)
|
||||
@@ -202,10 +264,15 @@ static long GetTotalTime(DotTracePayload payload) => payload.PlusTime - payload.
|
||||
|
||||
static long GetCallCount(DotTracePayload payload) => payload.PlusCallCount - payload.MinusCallCount;
|
||||
|
||||
static HotspotLists BuildHotspots(IEnumerable<MutableNode> nodes)
|
||||
static IEnumerable<MutableNode> FilterHotspotCandidates(IEnumerable<MutableNode> nodes, ExtractorOptions options) =>
|
||||
nodes.Where(node =>
|
||||
node.Kind == "method" &&
|
||||
(!options.ExcludeIdle || !IsIdleMethod(node.Name)) &&
|
||||
MatchesFilter(node.Name, options.NameFilter));
|
||||
|
||||
static HotspotLists BuildHotspots(IEnumerable<MutableNode> nodes, int top)
|
||||
{
|
||||
var candidates = nodes
|
||||
.Where(node => node.Kind == "method")
|
||||
.Select(node => new HotspotEntry
|
||||
{
|
||||
Id = node.Id,
|
||||
@@ -222,32 +289,101 @@ static HotspotLists BuildHotspots(IEnumerable<MutableNode> nodes)
|
||||
Inclusive = candidates
|
||||
.OrderByDescending(node => node.InclusiveTime)
|
||||
.ThenBy(node => node.Name, StringComparer.Ordinal)
|
||||
.Take(50)
|
||||
.Take(top)
|
||||
.ToList(),
|
||||
Exclusive = candidates
|
||||
.OrderByDescending(node => node.ExclusiveTime)
|
||||
.ThenBy(node => node.Name, StringComparer.Ordinal)
|
||||
.Take(50)
|
||||
.Take(top)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
static SerializableNode CloneTree(
|
||||
CallTreeSectionOffset offset,
|
||||
IReadOnlyDictionary<CallTreeSectionOffset, MutableNode> nodeMap,
|
||||
HashSet<CallTreeSectionOffset> ancestry)
|
||||
static SummaryInfo BuildSummary(
|
||||
SerializableNode callTree,
|
||||
IEnumerable<MutableNode> hotspotCandidates,
|
||||
HotspotEntry? topExclusive)
|
||||
{
|
||||
var source = nodeMap[offset];
|
||||
var nextAncestry = new HashSet<CallTreeSectionOffset>(ancestry) { offset };
|
||||
var candidateArray = hotspotCandidates.ToArray();
|
||||
return new SummaryInfo
|
||||
{
|
||||
WallTimeMs = NanosecondsToMilliseconds(callTree.Children.Count == 0 ? 0 : callTree.Children.Max(node => node.InclusiveTime)),
|
||||
ActiveTimeMs = NanosecondsToMilliseconds(candidateArray.Sum(node => node.ExclusiveTime)),
|
||||
TotalSamples = candidateArray.Sum(node => node.CallCount),
|
||||
TopExclusiveMethod = topExclusive?.Name,
|
||||
TopExclusiveMs = NanosecondsToMilliseconds(topExclusive?.ExclusiveTime ?? 0)
|
||||
};
|
||||
}
|
||||
|
||||
static List<HotPathEntry> BuildHotPaths(SerializableNode root, ExtractorOptions options)
|
||||
{
|
||||
var collector = new List<HotPathAccumulator>();
|
||||
foreach (var threadNode in root.Children)
|
||||
{
|
||||
CollectHotPaths(threadNode, [threadNode.Name], collector, options);
|
||||
}
|
||||
|
||||
return collector
|
||||
.OrderByDescending(path => path.InclusiveTime)
|
||||
.ThenBy(path => path.Path, StringComparer.Ordinal)
|
||||
.Take(options.Top)
|
||||
.Select(path => new HotPathEntry
|
||||
{
|
||||
Path = path.Path,
|
||||
InclusiveTime = path.InclusiveTime,
|
||||
InclusiveMs = NanosecondsToMilliseconds(path.InclusiveTime),
|
||||
LeafExclusiveTime = path.LeafExclusiveTime,
|
||||
LeafExclusiveMs = NanosecondsToMilliseconds(path.LeafExclusiveTime)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
static void CollectHotPaths(
|
||||
SerializableNode node,
|
||||
List<string> path,
|
||||
List<HotPathAccumulator> collector,
|
||||
ExtractorOptions options)
|
||||
{
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
if (child.Kind == "method")
|
||||
{
|
||||
path.Add(child.Name);
|
||||
|
||||
if ((!options.ExcludeIdle || !IsIdleMethod(child.Name)) && MatchesFilter(child.Name, options.NameFilter))
|
||||
{
|
||||
collector.Add(new HotPathAccumulator(
|
||||
string.Join(" > ", path),
|
||||
child.InclusiveTime,
|
||||
child.ExclusiveTime));
|
||||
}
|
||||
|
||||
CollectHotPaths(child, path, collector, options);
|
||||
path.RemoveAt(path.Count - 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
var appended = child.Kind is not ("root" or "special");
|
||||
if (appended)
|
||||
path.Add(child.Name);
|
||||
|
||||
CollectHotPaths(child, path, collector, options);
|
||||
if (appended)
|
||||
path.RemoveAt(path.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
static SerializableNode CloneTree(MutableNode source, HashSet<CallTreeSectionOffset> ancestry)
|
||||
{
|
||||
var nextAncestry = new HashSet<CallTreeSectionOffset>(ancestry) { source.Offset };
|
||||
var children = new List<SerializableNode>(source.Children.Count);
|
||||
|
||||
foreach (var child in source.Children)
|
||||
{
|
||||
var childOffset = ParseOffset(child.Id);
|
||||
if (nextAncestry.Contains(childOffset))
|
||||
if (nextAncestry.Contains(child.Offset))
|
||||
continue;
|
||||
|
||||
children.Add(CloneTree(childOffset, nodeMap, nextAncestry));
|
||||
children.Add(CloneTree(child, nextAncestry));
|
||||
}
|
||||
|
||||
return new SerializableNode
|
||||
@@ -263,12 +399,80 @@ static SerializableNode CloneTree(
|
||||
};
|
||||
}
|
||||
|
||||
static CallTreeSectionOffset ParseOffset(string value)
|
||||
static SerializableNode? PruneTree(SerializableNode node, string? filter)
|
||||
{
|
||||
var parts = value.Split('/');
|
||||
return new CallTreeSectionOffset(Convert.ToInt64(parts[0], 16), int.Parse(parts[1]));
|
||||
if (string.IsNullOrWhiteSpace(filter))
|
||||
return node;
|
||||
|
||||
if (MatchesFilter(node.Name, filter))
|
||||
return node;
|
||||
|
||||
var prunedChildren = new List<SerializableNode>();
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
var prunedChild = PruneTree(child, filter);
|
||||
if (prunedChild is not null)
|
||||
prunedChildren.Add(prunedChild);
|
||||
}
|
||||
|
||||
if (prunedChildren.Count == 0)
|
||||
return null;
|
||||
|
||||
return node with { Children = prunedChildren };
|
||||
}
|
||||
|
||||
static SerializableNode CreateEmptyRoot() => new()
|
||||
{
|
||||
Id = "root",
|
||||
Name = "<root>",
|
||||
Kind = "root",
|
||||
ThreadName = null,
|
||||
InclusiveTime = 0,
|
||||
ExclusiveTime = 0,
|
||||
CallCount = 0,
|
||||
Children = []
|
||||
};
|
||||
|
||||
static bool MatchesFilter(string value, string? filter) =>
|
||||
string.IsNullOrWhiteSpace(filter) ||
|
||||
value.Contains(filter, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
static bool IsIdleMethod(string name) =>
|
||||
GetIdleMethodPatterns().Any(pattern => name.Contains(pattern, StringComparison.Ordinal));
|
||||
|
||||
static double NanosecondsToMilliseconds(long value) => Math.Round(value / 1_000_000d, 3);
|
||||
|
||||
static string[] GetIdleMethodPatterns()
|
||||
{
|
||||
return
|
||||
[
|
||||
"WaitHandle.Wait",
|
||||
"WaitOneNoCheck",
|
||||
"SemaphoreSlim.Wait",
|
||||
"LowLevelLifoSemaphore.Wait",
|
||||
"LowLevelLifoSemaphore.WaitForSignal",
|
||||
"LowLevelSpinWaiter.Wait",
|
||||
"Monitor.Wait",
|
||||
"SocketAsyncEngine.EventLoop",
|
||||
"PollGC",
|
||||
"Interop+Sys.WaitForSocketEvents",
|
||||
"ManualResetEventSlim.Wait",
|
||||
"Task.SpinThenBlockingWait",
|
||||
"Thread.SleepInternal"
|
||||
];
|
||||
}
|
||||
|
||||
sealed record ExtractorOptions(
|
||||
string SnapshotPath,
|
||||
int Top,
|
||||
string? NameFilter,
|
||||
bool FlatPaths,
|
||||
bool ExcludeIdle);
|
||||
|
||||
readonly record struct SignatureResult(string Name, string Kind);
|
||||
|
||||
readonly record struct HotPathAccumulator(string Path, long InclusiveTime, long LeafExclusiveTime);
|
||||
|
||||
sealed class SignatureResolver(
|
||||
SnapshotMasksComponent masks,
|
||||
FuidToMetadataIdConverter fuidConverter,
|
||||
@@ -343,16 +547,18 @@ sealed class SignatureResolver(
|
||||
}
|
||||
}
|
||||
|
||||
readonly record struct SignatureResult(string Name, string Kind);
|
||||
|
||||
sealed class OutputDocument
|
||||
{
|
||||
public required SnapshotInfo Snapshot { get; init; }
|
||||
|
||||
public required SummaryInfo Summary { get; init; }
|
||||
|
||||
public required List<ThreadRootInfo> ThreadRoots { get; init; }
|
||||
|
||||
public required HotspotLists Hotspots { get; init; }
|
||||
|
||||
public List<HotPathEntry>? HotPaths { get; init; }
|
||||
|
||||
public required SerializableNode CallTree { get; init; }
|
||||
}
|
||||
|
||||
@@ -362,9 +568,39 @@ sealed class SnapshotInfo
|
||||
|
||||
public required string PayloadType { get; init; }
|
||||
|
||||
public required string TimeUnit { get; init; }
|
||||
|
||||
public required int ThreadCount { get; init; }
|
||||
|
||||
public required int NodeCount { get; init; }
|
||||
|
||||
public required SnapshotDiagnostics Diagnostics { get; init; }
|
||||
}
|
||||
|
||||
sealed class SnapshotDiagnostics
|
||||
{
|
||||
public required int HeaderCount { get; init; }
|
||||
|
||||
public required int ExpectedNodeCount { get; init; }
|
||||
|
||||
public required int ReadNodeCount { get; init; }
|
||||
|
||||
public required int ReadTotalPayloadCount { get; init; }
|
||||
|
||||
public required int ReadOwnPayloadCount { get; init; }
|
||||
}
|
||||
|
||||
sealed class SummaryInfo
|
||||
{
|
||||
public required double WallTimeMs { get; init; }
|
||||
|
||||
public required double ActiveTimeMs { get; init; }
|
||||
|
||||
public required long TotalSamples { get; init; }
|
||||
|
||||
public string? TopExclusiveMethod { get; init; }
|
||||
|
||||
public required double TopExclusiveMs { get; init; }
|
||||
}
|
||||
|
||||
sealed class ThreadRootInfo
|
||||
@@ -398,8 +634,25 @@ sealed class HotspotEntry
|
||||
public required long CallCount { get; init; }
|
||||
}
|
||||
|
||||
class MutableNode
|
||||
sealed class HotPathEntry
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
|
||||
public required long InclusiveTime { get; init; }
|
||||
|
||||
public required double InclusiveMs { get; init; }
|
||||
|
||||
public required long LeafExclusiveTime { get; init; }
|
||||
|
||||
public required double LeafExclusiveMs { get; init; }
|
||||
}
|
||||
|
||||
sealed class MutableNode
|
||||
{
|
||||
public required CallTreeSectionOffset Offset { get; init; }
|
||||
|
||||
public required CallTreeSectionOffset ParentOffset { get; init; }
|
||||
|
||||
public required string Id { get; init; }
|
||||
|
||||
public required string Name { get; init; }
|
||||
@@ -415,7 +668,7 @@ class MutableNode
|
||||
public required List<MutableNode> Children { get; init; }
|
||||
}
|
||||
|
||||
sealed class SerializableNode
|
||||
sealed record SerializableNode
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
|
||||
|
||||
BIN
tools/__pycache__/dtp_parse.cpython-314.pyc
Normal file
BIN
tools/__pycache__/dtp_parse.cpython-314.pyc
Normal file
Binary file not shown.
@@ -21,6 +21,29 @@ def parse_args() -> argparse.Namespace:
|
||||
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.")
|
||||
parser.add_argument("--top", type=int, default=200, help="Maximum hotspot and path entries to emit.")
|
||||
parser.add_argument("--filter", dest="name_filter", help="Case-insensitive substring filter for node names.")
|
||||
parser.add_argument(
|
||||
"--flat",
|
||||
"--paths",
|
||||
dest="flat_paths",
|
||||
action="store_true",
|
||||
help="Include the top heaviest call paths as flat strings.",
|
||||
)
|
||||
idle_group = parser.add_mutually_exclusive_group()
|
||||
idle_group.add_argument(
|
||||
"--exclude-idle",
|
||||
dest="exclude_idle",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Exclude idle and wait methods from hotspot and path rankings.",
|
||||
)
|
||||
idle_group.add_argument(
|
||||
"--include-idle",
|
||||
dest="exclude_idle",
|
||||
action="store_false",
|
||||
help="Keep idle and wait methods in hotspot and path rankings.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -51,8 +74,14 @@ def build_helper(dottrace_dir: Path) -> None:
|
||||
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)]
|
||||
def run_helper(snapshot: Path, dottrace_dir: Path, args: argparse.Namespace) -> dict:
|
||||
command = ["dotnet", str(HELPER_DLL), str(snapshot), "--top", str(args.top)]
|
||||
if args.name_filter:
|
||||
command.extend(["--filter", args.name_filter])
|
||||
if args.flat_paths:
|
||||
command.append("--flat")
|
||||
if not args.exclude_idle:
|
||||
command.append("--include-idle")
|
||||
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)
|
||||
@@ -74,7 +103,7 @@ def main() -> int:
|
||||
try:
|
||||
dottrace_dir = find_dottrace_dir()
|
||||
build_helper(dottrace_dir)
|
||||
payload = run_helper(snapshot, dottrace_dir)
|
||||
payload = run_helper(snapshot, dottrace_dir, args)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
Binary file not shown.
@@ -16,9 +16,9 @@ def walk(node):
|
||||
|
||||
|
||||
class DtpParserTests(unittest.TestCase):
|
||||
def test_emits_machine_readable_call_tree(self):
|
||||
def run_parser(self, *args: str) -> dict:
|
||||
result = subprocess.run(
|
||||
["python3", str(SCRIPT), str(SNAPSHOT), "--stdout"],
|
||||
["python3", str(SCRIPT), str(SNAPSHOT), "--stdout", *args],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -26,16 +26,54 @@ class DtpParserTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
payload = json.loads(result.stdout)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
def test_emits_machine_readable_call_tree(self):
|
||||
payload = self.run_parser()
|
||||
|
||||
self.assertIn("callTree", payload)
|
||||
self.assertIn("hotspots", payload)
|
||||
self.assertIn("summary", payload)
|
||||
self.assertTrue(payload["callTree"]["children"])
|
||||
self.assertTrue(payload["hotspots"]["inclusive"])
|
||||
self.assertEqual(payload["snapshot"]["timeUnit"], "nanoseconds")
|
||||
self.assertLessEqual(len(payload["hotspots"]["inclusive"]), 200)
|
||||
self.assertLessEqual(len(payload["hotspots"]["exclusive"]), 200)
|
||||
self.assertIn("wallTimeMs", payload["summary"])
|
||||
self.assertIn("activeTimeMs", payload["summary"])
|
||||
self.assertIn("totalSamples", payload["summary"])
|
||||
self.assertIn("topExclusiveMethod", payload["summary"])
|
||||
|
||||
node_names = [node["name"] for node in walk(payload["callTree"])]
|
||||
self.assertTrue(any(not name.startswith("[special:") for name in node_names))
|
||||
|
||||
def test_supports_hotspot_filter_and_flat_paths(self):
|
||||
payload = self.run_parser("--top", "7", "--filter", "Microsoft.DotNet.Cli.Program", "--flat")
|
||||
|
||||
self.assertLessEqual(len(payload["hotspots"]["inclusive"]), 7)
|
||||
self.assertLessEqual(len(payload["hotspots"]["exclusive"]), 7)
|
||||
self.assertIn("hotPaths", payload)
|
||||
|
||||
node_names = [node["name"] for node in walk(payload["callTree"])]
|
||||
self.assertTrue(any("Microsoft.DotNet.Cli.Program" in name for name in node_names))
|
||||
self.assertFalse(any("Microsoft.Build.Tasks.Copy.ParallelCopyTask" == name for name in node_names))
|
||||
|
||||
for hotspot in payload["hotspots"]["inclusive"] + payload["hotspots"]["exclusive"]:
|
||||
self.assertIn("Microsoft.DotNet.Cli.Program", hotspot["name"])
|
||||
|
||||
for path_entry in payload["hotPaths"]:
|
||||
self.assertIn("Microsoft.DotNet.Cli.Program", path_entry["path"])
|
||||
|
||||
def test_can_include_idle_hotspots_when_requested(self):
|
||||
without_idle = self.run_parser("--top", "20")
|
||||
with_idle = self.run_parser("--top", "20", "--include-idle")
|
||||
|
||||
without_idle_names = {entry["name"] for entry in without_idle["hotspots"]["exclusive"]}
|
||||
with_idle_names = {entry["name"] for entry in with_idle["hotspots"]["exclusive"]}
|
||||
|
||||
self.assertNotIn("System.Threading.WaitHandle.WaitOneNoCheck", without_idle_names)
|
||||
self.assertIn("System.Threading.WaitHandle.WaitOneNoCheck", with_idle_names)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user