Uh oh!
There was an error while loading. Please reload this page.
Clear the perf-improver backlog: reporting and terminal allocations - #10384
Conversation
…ransport allocations) Handles the open perf-improver items tracked by #10376, #10381 and #9604. * ParsedManagedName.ToProperty() now caches the TestMethodIdentifierProperty for parameterless test methods. The property is fully immutable in that case (readonly strings/int plus an empty, unmutatable parameter array) and the ParsedManagedName is already cached per TestMethod, so a test that reports more than once (retries, re-runs) no longer allocates a property per report. Parameterized methods keep getting a fresh property and a fresh array copy. * AnsiTerminal.StopUpdate() no longer calls _stringBuilder.ToString() on every flush. IConsole gains a Write(StringBuilder) overload that SystemConsole forwards to TextWriter.Write(StringBuilder) on .NET, so the batched progress block (often several KB, flushed on every refresh tick) is written straight from the builder chunks. netstandard2.0 / .NET Framework keep the string copy because the overload does not exist there. * SilenceDrivenHeartbeatRenderer reuses a single StringBuilder for slow-test descriptions instead of allocating one per surfaced slow test. Like the backoff dictionary next to it, the buffer is only touched from the single rendering thread inside OnTick. * DotnetTestHttpClient reuses one instance-wide single-byte buffer for the trailing-frame probe instead of allocating a new byte[1] per request. Requests are serialized by _requestLock, which is held for the whole request/response exchange. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 938073d7-0181-42c6-a241-ab7b1faaf702
There was a problem hiding this comment.
Pull request overview
Reduces allocations across MSTest reporting, terminal rendering, and HTTP transport paths.
Changes:
- Caches parameterless test identifiers and adds reuse tests.
- Reuses terminal-rendering and HTTP probe buffers.
- Adds allocation-free
StringBuilderconsole output on modern .NET.
Show a summary per file
| File | Description |
|---|---|
MSTestTestNodeConverterTests.cs | Tests identifier sharing behavior. |
CTRLPlusCCancellationTokenSourceTests.cs | Updates the console fake. |
TerminalTestReporterTests.cs | Updates terminal console fakes. |
DotnetTestHttpClient.cs | Reuses the trailing-byte buffer. |
SilenceDrivenHeartbeatRenderer.cs | Reuses the description builder. |
AnsiTerminal.cs | Writes batched builders directly. |
InternalAPI.Unshipped.txt | Tracks new internal APIs. |
SystemConsole.cs | Implements builder-based output. |
IConsole.cs | Adds the builder write contract. |
MSTestTestNodeConverter.cs | Caches parameterless identifiers. |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 2
- Review effort level: Balanced
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Note
🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.
Expert Review — 22 Dimensions
All four changes are allocation-reduction optimizations with clear safety arguments. No issues found.
| # | Dimension | Verdict |
|---|---|---|
| 1 | Algorithmic Correctness | ✅ Caching gated on _parameterTypeFullNames.Length == 0 (immutable case); parameterized path still copies. StringBuilder reuse confined to single-thread tick callback. _trailingByteBuffer guarded by _requestLock. |
| 2 | Thread Safety & Concurrency | ✅ _cachedParameterlessProperty uses ??= which is safe (idempotent, immutable value). _slowTestDescriptionBuilder is single-thread (OnTick). _trailingByteBuffer is under _requestLock. |
| 3 | Public API Surface | ✅ No new public API. IConsole and SystemConsole are internal. Correctly added to InternalAPI.Unshipped.txt. |
| 4 | Backward Compatibility | ✅ No behavioral changes; purely allocation reductions. |
| 5 | Cross-TFM Correctness | ✅ SystemConsole.Write(StringBuilder) uses #if NETCOREAPP for the zero-copy path, falls back to .ToString() on netstandard2.0/.NET Framework. |
| 6 | Performance | ✅ This is the perf improvement — all four changes reduce allocations on hot paths. |
| 7 | Error Handling | N/A |
| 8 | Resource Management | ✅ No new disposables or streams. |
| 9 | Security | N/A |
| 10 | Naming & Conventions | ✅ Field names follow existing patterns (_cachedParameterlessProperty, _slowTestDescriptionBuilder, _trailingByteBuffer). |
| 11 | Code Style | ✅ Consistent with codebase conventions. |
| 12 | Documentation | ✅ Good inline comments explaining why each optimization is safe. |
| 13 | Localization | N/A |
| 14 | Test Coverage | ✅ Two new tests verify caching for parameterless and non-caching for parameterized. Existing DoesNotShareParameterTypeArray test still applies. |
| 15 | Test Quality | ✅ Tests use BeSameAs/NotBeSameAs — correct assertions for reference identity checks. |
| 16 | IPC Contract | N/A |
| 17 | Logging & Diagnostics | N/A |
| 18 | PublicAPI.*.txt | ✅ No public API changes. Internal API file updated correctly. |
| 19 | Analyzers | N/A |
| 20 | MSBuild / SDK | N/A |
| 21 | Scope Discipline | ✅ Four related perf items, all allocation reductions — reasonable to batch. |
| 22 | Follow-up Tracking | ✅ PR description references #10376, #10381, #9604. |
Summary: Clean performance PR. All optimizations have sound safety invariants documented in comments. No issues to flag.
This comment has been minimized.
This comment has been minimized.
… revert byte[1] reuse - ParsedManagedName builds the parameterless TestMethodIdentifierProperty in the constructor into a readonly field instead of a lazy ??=, so the shared instance no longer depends on call ordering or races between concurrent reporters. - Corrected the ParsedManagedNameCache remarks, which still claimed the property is never cached, and the ToProperty() comment, which overstated the scope (discovery and execution do not share a TestMethod, so 're-runs' was wrong). - SilenceDrivenHeartbeatRenderer creates its scratch StringBuilder on first use, so a healthy run that never surfaces a slow test allocates nothing. - Reverted the DotnetTestHttpClient byte[1] reuse. It saved one ~24 byte allocation per RPC on a path that already allocates streams, a linked CTS and HttpRequestMessage, and it coupled correctness to requests staying serialized. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 938073d7-0181-42c6-a241-ab7b1faaf702
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 938073d7-0181-42c6-a241-ab7b1faaf702
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/Platform/Microsoft.Testing.Platform/Helpers/System/SystemConsole.cs:124
CaptureConsoleOutWriteris configured withAutoFlush = true.TextWriter.Write(StringBuilder)callsWrite(ReadOnlySpan<char>)once perStringBuilderchunk, andStreamWriterauto-flushes after each such call. A multi-chunk progress frame therefore flushes the underlying stdout stream multiple times instead of once as the previousWrite(string)path did, potentially replacing the saved allocation with additional blocking I/O. Please use a chunk-writing path that suppresses auto-flush for the complete logical write and flushes once at the end; changing the shared writer'sAutoFlushstate also needs synchronization with the other write methods.
CaptureConsoleOutWriter.Write(value);
- Files reviewed: 9/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
🧪 Test quality grade — PR #10384
This advisory comment was generated automatically. Grades are heuristic and informational — they do not block merging. Re-run with
|
Amaury Levé (Evangelink)
commented
Aug 2, 2026
Good catch in principle, but I measured it and it does not hold beyond the first frame, so I am leaving the code as is.
So the extra auto-flushes are bounded to 11 additional The proposed fix would mean toggling |
Uh oh!
There was an error while loading. Please reload this page.
Handles the remaining open perf-improver backlog items: the filed ticket (#10376) plus the actionable entries listed in the monthly-activity backlogs (#10381, #9604).
Changes
1. Cache
TestMethodIdentifierPropertyfor parameterless tests (#10376)ParsedManagedName.ToProperty()is called once per node built from aTestMethod. For a parameterless method the property is fully immutable — readonly strings/int plus an empty array that cannot be mutated — andParsedManagedNameis already cached perTestMethod(#10366), so the several nodes one executed test produces (the in-progress node, then one result node per data row and per in-process retry attempt) now share a single property instance.The property is built eagerly in the
ParsedManagedNameconstructor into areadonlyfield rather than lazily via??=, so the shared instance doesn't depend on call ordering or on races between concurrent reporters.ToProperty()is always called on a fresh parse, so nothing is built that wouldn't have been built anyway.Parameterized methods are deliberately unchanged: they still get a fresh property carrying a fresh copy of
ParameterTypeFullNames, because that array is publicly exposed and must never be aliased across nodes.2. Unblock
AnsiTerminal.StopUpdate()(backlog item 3 — previously recorded as blocked by IConsole + netstandard2.0 compat)StopUpdate()called_stringBuilder.ToString()on every flush — twice a second for cursor rendering, plus around ordinary terminal writes — and the batched progress frame is routinely kilobyte-scale.IConsolenow has aWrite(StringBuilder)overload;SystemConsoleforwards it toTextWriter.Write(StringBuilder)on .NET, which walks the builder's chunks and writes each as a span, so no temporary string is materialised. netstandard2.0 / .NET Framework keep theToString()copy behind#if, since the overload doesn't exist there — nothing regresses on those TFMs.3. Reuse the slow-test description builder (backlog item 2)
SilenceDrivenHeartbeatRenderer.BuildSlowTestDescriptionallocated aStringBuilder(plus backingchar[]) per surfaced slow test. It now reuses one lazily-created instance field, so a healthy run that never surfaces a slow test still allocates nothing. Reuse is safe for the same reason as the_slowTestThresholdStatesdictionary next to it: both are only touched from the single rendering thread insideOnTick.Deliberately not done
DotnetTestHttpClientnew byte[1]probe buffer (backlog item 1). An earlier revision of this PR hoisted it to an instance field; that's been reverted. It saved one ~24-byte allocation per RPC on a path that already allocates a linked CTS, streams,ByteArrayContentand anHttpRequestMessage, and it silently coupled correctness to requests staying serialized by_requestLockforever. Not a good trade.OpenTelemetryResultHandler.GetFullyQualifiedName()(backlog item 4). One logical string build per test result, only on the OTel-enabled path.string.Concatwith 5 operands,string.Joinand aStringBuilderare all neutral-or-worse than what the compiler already emits for the interpolation.Both are recorded as won't-fix on the tracking issues rather than churning the code.
Tests
ToResultTestNode_ReusesTestMethodIdentifierInstance_ForParameterlessTestMethodandToResultTestNode_DoesNotReuseTestMethodIdentifierInstance_ForParameterizedTestMethodinMSTestTestNodeConverterTests, pinning both halves of the selective caching policy alongside the existing array-isolation test.TerminalTestReporterTestsconsole fakes now implementWrite(StringBuilder), so the whole terminal suite exercises the new flush path..\build.cmd -test— full solution build + unit tests, 0 warnings, 0 errors across net462, net48, net8.0, net9.0 and netstandard2.0.Review
Two independent review rounds were run over the diff (correctness/concurrency, plus a skeptical is-this-actually-a-win pass). Findings addressed: the lazy
??=identity race, a staleParsedManagedNameCachedoc block that still claimed the property is never cached, an overstated "re-runs" comment (discovery and execution don't share aTestMethod), eager builder allocation on healthy runs, and thebyte[1]trade-off above. The final round found no remaining issues.Closes#10376