Skip to content

Refactor Speedscope parsing and complete pre-PR verification - #55

Closed
rogeralsing wants to merge 1 commit into
mainfrom
codex/20260321-024754-69be06d8
Closed

rogeralsing wants to merge 1 commit into
mainfrom
codex/20260321-024754-69be06d8

Conversation

@rogeralsing

Copy link
Copy Markdown
Contributor

Summary

  • extracted Speedscope profile aggregation, call-tree construction, and result shaping into a dedicated SpeedscopeProfileAccumulator
  • reduced SpeedscopeParser to document/file loading plus top-level JSON shape validation
  • reran the pre-PR checks to confirm the repo still builds cleanly, has no reported duplication, and stays formatted

Testing

  • DOTNET_ROLL_FORWARD=Major MSBuildSDKsPath=/usr/local/share/dotnet/sdk/9.0.304/Sdks roslynator fix src/ProfilerCore/Asynkron.Profiler.Core.csproj
  • DOTNET_ROLL_FORWARD=Major MSBuildSDKsPath=/usr/local/share/dotnet/sdk/9.0.304/Sdks roslynator fix src/ProfileTool/ProfileTool.csproj
  • DOTNET_ROLL_FORWARD=Major MSBuildSDKsPath=/usr/local/share/dotnet/sdk/9.0.304/Sdks roslynator fix tests/Asynkron.Profiler.Tests/Asynkron.Profiler.Tests.csproj
  • dotnet build Asynkron.Profiler.sln -warnaserror
  • dotnet test Asynkron.Profiler.sln --no-build
  • quickdup -path . -ext .cs -select 0..20 -min 2 -exclude ".g.,.generated.,bin/,obj/"
  • dotnet format Asynkron.Profiler.sln --verify-no-changes

Notes

  • roslynator fix Asynkron.Profiler.sln is not usable in this environment because the installed Roslynator version fails when opening the solution directly against the current SDK, so the same check was run project-by-project instead.

Copilot AI review requested due to automatic review settings March 21, 2026 02:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors Speedscope CPU profile parsing by separating aggregation/call-tree construction from JSON document loading/validation, improving separation of concerns within the profiling pipeline.

Changes:

  • Added SpeedscopeProfileAccumulator to encapsulate frame aggregation, call-tree building, and CpuProfileResult shaping.
  • Simplified SpeedscopeParser to focus on IO + top-level JSON shape validation and delegate processing to the accumulator.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/ProfileTool/Cpu/SpeedscopeProfileAccumulator.cs New component that accumulates sampled/evented profile data into function samples + call tree.
src/ProfileTool/Cpu/SpeedscopeParser.cs Reduced to reading frames/profiles from JSON and delegating processing to the accumulator.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

{
var parentNode = stack.Count > 0 ? stack[^1].Node : _callTreeRoot;
var childNode = GetOrCreateChild(parentNode, frameIdx);
childNode.Calls += 1;

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CallTreeNode.Calls is incremented via += here, which bypasses the clamping logic provided by CallTreeNode.IncrementCalls() and can overflow on large profiles (leading to negative/incorrect call counts). Consider using IncrementCalls() for the +1 case, and for weighted increments add a clamped add helper (e.g., clamp at int.MaxValue) before assigning back to Calls.

Suggested change
childNode.Calls += 1;
childNode.IncrementCalls();

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +116
if (string.Equals(eventType, "O", StringComparison.Ordinal))
{
var parentNode = stack.Count > 0 ? stack[^1].Node : _callTreeRoot;
var childNode = GetOrCreateChild(parentNode, frameIdx);
childNode.Calls += 1;
IncrementFrameCount(frameIdx, 1);
stack.Add((childNode, at, frameIdx));

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Evented events can contain an invalid/negative frame index. Unlike the sampled path (which skips negative indices), this path will create a child keyed by a negative frame index (including -1, which is also used by the root) and pollute the call tree / function list. Add a frameIdx < 0 guard before GetOrCreateChild / IncrementFrameCount.

Copilot uses AI. Check for mistakes.
private void IncrementFrameCount(int frameIdx, int count)
{
_frameCounts.TryGetValue(frameIdx, out var existing);
_frameCounts[frameIdx] = existing + count;

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IncrementFrameCount uses unchecked existing + count, which can overflow for long-running profiles and produce negative/incorrect call/sample counts in FunctionSample. Consider clamping at int.MaxValue (or switching counts to long end-to-end if feasible) to keep results stable.

Suggested change
_frameCounts[frameIdx] = existing + count;
long sum = (long)existing + count;
if (sum > int.MaxValue)
{
_frameCounts[frameIdx] = int.MaxValue;
}
else if (sum < int.MinValue)
{
_frameCounts[frameIdx] = int.MinValue;
}
else
{
_frameCounts[frameIdx] = (int)sum;
}

Copilot uses AI. Check for mistakes.
Comment on lines +330 to +332
return weight <= 0
? 0
: Math.Max(1, (int)Math.Round(weight));

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResolveCallWeight casts Math.Round(weight) to int without range checking. If a speedscope weight exceeds int.MaxValue (or is NaN/Infinity), the cast can yield an unspecified/incorrect value. Clamp to [0, int.MaxValue] and handle non-finite values explicitly before converting.

Suggested change
return weight <= 0
? 0
: Math.Max(1, (int)Math.Round(weight));
// For sample units, weight represents a count of samples.
// Handle non-finite values explicitly and clamp to [0, int.MaxValue].
if (double.IsNaN(weight))
{
return 0;
}
if (double.IsPositiveInfinity(weight))
{
return int.MaxValue;
}
if (double.IsNegativeInfinity(weight) || weight <= 0d)
{
return 0;
}
var rounded = Math.Round(weight);
if (rounded <= 0d)
{
// Preserve previous behavior: any positive weight yields at least 1.
return 1;
}
if (rounded >= int.MaxValue)
{
return int.MaxValue;
}
return (int)rounded;

Copilot uses AI. Check for mistakes.
Comment on lines +368 to +374
var sum = 0;
foreach (var child in node.Children.Values)
{
sum += child.Calls;
}

return sum;

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SumChildCalls accumulates into an int with sum += child.Calls;, which can overflow and wrap for large call trees. Use a long accumulator and clamp the final result to int.MaxValue before assigning to CallTreeNode.Calls.

Suggested change
var sum = 0;
foreach (var child in node.Children.Values)
{
sum += child.Calls;
}
return sum;
long sum = 0;
foreach (var child in node.Children.Values)
{
sum += child.Calls;
}
return sum >= int.MaxValue ? int.MaxValue : (int)sum;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants