[perf-improver] perf: fast path in HumanReadableDurationFormatter.Render for sub-hour durations#8861
Open
Evangelink wants to merge 1 commit into
Open
Conversation
… durations On .NET 8+, add a fast path that uses string.Create with a stackalloc buffer for the most common case (duration < 1 hour, no milliseconds). Before: each Render call allocated a StringBuilder + 1-2 intermediate strings from GetFormattedPart + the final result string (3-4 allocations). After (fast path): only the final result string is allocated (1 allocation). This method is called on every progress-frame render tick (roughly 5 times per frame) to format durations for each visible test-worker line. The savings accumulate quickly during long-running parallel test runs. All progress-frame callers (AnsiTerminalTestProgressFrame, SimpleTerminalBase) use the default parameters (wrapInParentheses=true, showMilliseconds=false), so they all benefit from the fast path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR optimizes HumanReadableDurationFormatter.Render in Microsoft.Testing.Platform’s terminal output path by adding a .NET 8+ fast path for the common “sub-hour, no milliseconds” case, reducing per-call allocations during frequent progress-frame rendering.
Changes:
- Added a
#if NET8_0_OR_GREATERfast path usingstring.Createwith astackallocscratch buffer for durations whereDays == 0 && Hours == 0andshowMilliseconds == false. - Kept the existing
StringBuilder-based formatting as the unchanged slow path for longer durations or when milliseconds are requested.
Show a summary per file
| File | Description |
|---|---|
| src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/HumanReadableDurationFormatter.cs | Adds a .NET 8+ allocation-reducing formatting fast path for the most common progress-duration rendering scenario. |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 0
azat-msft
approved these changes
Jun 5, 2026
Youssef1313
reviewed
Jun 5, 2026
| int minutes = duration.Value.Minutes; | ||
| return wrapInParentheses | ||
| ? (minutes == 0 | ||
| ? string.Create(CultureInfo.InvariantCulture, stackalloc char[8], $"({seconds}s)") |
Member
There was a problem hiding this comment.
I don't think this is going to be significantly faster compared to string interpolation. This also seem to add complications without any proven gains.
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 This is an automated contribution from Perf Improver.
Goal and Rationale
HumanReadableDurationFormatter.Renderis called multiple times per terminal progress-frame render tick — once per visible test-worker line for the "duration unchanged" fast-check, and once more insideAppendTestWorkerProgress/AppendTestWorkerDetailwhen the line needs a full re-render. For a 4-assembly run refreshing at ~5 fps, this adds up to ~40+ calls per second over the lifetime of the test run.Each call currently allocates:
new StringBuilder()GetFormattedPart(e.g."5s","59s"," 05s")stringBuilder.ToString()resultThat is 3–4 heap allocations per call — all for a tiny string like
"(5s)"or"(2m 30s)".Approach
On .NET 8+, use
string.Create(IFormatProvider, Span<char>, ref DefaultInterpolatedStringHandler)with astackallocbuffer. This overload uses the span as a scratch buffer and produces the final heap string in a single allocation — noStringBuilder, no intermediateGetFormattedPartstrings.The fast path activates when:
Days == 0 && Hours == 0(covers virtually all test runs)showMilliseconds == false(the default for all progress-frame callers)Both conditions are true for every caller in
AnsiTerminalTestProgressFrameandSimpleTerminalBase.The slow path (days, hours, or
showMilliseconds=true) is unchanged; it is rarely reached and is not in the render hot path.Performance Evidence
Rendercall (typical: < 1 min)Rendercall (> 1 hour)string.Createheap allocationsMethodology: code inspection + allocation analysis.
HumanReadableDurationFormatter.Renderis called ~5× per render frame; at 5 fps over a 5-minute run that is ~7 500 calls, saving ~15 000–22 500 small string allocations.The change is
#if NET8_0_OR_GREATER-guarded, so netstandard2.0 behaviour is completely unchanged.Trade-offs
wrapInParentheses ? (minutes == 0 ? ... : ...) : (minutes == 0 ? ... : ...)) is slightly dense but self-contained. The logic is simple and the four resulting strings are easy to validate visually.showMilliseconds=true, durations with hours, or durations with days.netstandard2.0uses the existing slow path as before.Test Status
Microsoft.Testing.Platform.UnitTests(net8.0): 1086 passed, 0 failed, 3 skipped ✅Reproducibility
Add this agentic workflows to your repo
To install this agentic workflow, run