Skip to content

[perf-improver] perf: eliminate string allocations in ANSI render hot path#8769

Merged
Evangelink merged 1 commit into
mainfrom
perf-assist/render-alloc-reduction-e7a4ef3abf2cdf8d
Jun 2, 2026
Merged

[perf-improver] perf: eliminate string allocations in ANSI render hot path#8769
Evangelink merged 1 commit into
mainfrom
perf-assist/render-alloc-reduction-e7a4ef3abf2cdf8d

Conversation

@Evangelink
Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

The live-progress render loop runs every 500 ms while tests are executing. Three allocation sites in this path were building temporary strings from compile-time-known constants:

  1. $"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}" — used 4× in AnsiTerminalTestProgressFrame.Render (once per changed progress or detail line). The result is always the literal "\x1b[K".
  2. $"{AnsiCodes.CSI}{AnsiCodes.EraseInDisplay}" — used in AnsiTerminalTestProgressFrame.Render and AnsiTerminal.EraseProgress. Always "\x1b[J".
  3. MoveCursorUp string allocationAnsiTerminal.MoveCursorUp built a temporary string even when batching mode was active and the result went straight into a StringBuilder.

Approach

  1. Added AnsiCodes.CsiEraseInLine (= CSI + EraseInLine) and AnsiCodes.CsiEraseInDisplay (= CSI + EraseInDisplay) as const string fields. The C# compiler evaluates these at compile time; no runtime allocation.
  2. Replaced all five $"{AnsiCodes.CSI}{AnsiCodes.EraseIn*}" interpolations with the new constants.
  3. In AnsiTerminal.MoveCursorUp, for the hot batching path, replaced string interpolation with direct StringBuilder.Append chaining (Append(CSI).Append(lineCount).Append(MoveUpToLineStart).AppendLine()). The non-batching path retains the interpolation since it only runs on the very first call after progress is erased.

Performance Evidence

Site Before After
CsiEraseInLine (per changed line) string allocation ~5 bytes 0 — compile-time constant
CsiEraseInDisplay (erase / render) string allocation ~4 bytes 0 — compile-time constant
MoveCursorUp in batching path string allocation ~8 bytes 0 — direct StringBuilder append
Total per 500 ms frame (worst case: all lines changed + erase) ≥6 short-lived string allocations 0 for these sites

Methodology: code inspection + diff review. The new constants are const string fields initialised from other const string fields; the C# compiler folds them at compile time.

Trade-offs

  • AnsiCodes gains two constants (+12 lines). Naming follows the existing SetDefaultColor pattern (a shortcut that combines prefix + suffix into one constant).
  • The non-batching branch of MoveCursorUp still builds a string — it's invoked only once per erase cycle (not per frame), so the impact is negligible.

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1003 passed, 0 failed, 3 skipped
  • Build: 0 warnings, 0 errors

Reproducibility

./build.sh
artifacts/bin/Microsoft.Testing.Platform.UnitTests/Debug/net8.0/Microsoft.Testing.Platform.UnitTests

Generated by Perf Improver

Generated by Perf Improver · sonnet46 3.2M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/perf-improver.md@main

Add CsiEraseInLine and CsiEraseInDisplay constants to AnsiCodes,
replacing runtime string interpolations that ran every 500ms render cycle.
Optimize MoveCursorUp to write directly to StringBuilder in batching mode,
avoiding a per-call string allocation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 2, 2026 15:03
@Evangelink Evangelink added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 2, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes the Microsoft.Testing.Platform ANSI terminal live-progress rendering path by removing avoidable short-lived string allocations in frequently executed ANSI escape sequence writes.

Changes:

  • Introduced AnsiCodes.CsiEraseInLine and AnsiCodes.CsiEraseInDisplay constants to avoid runtime string construction for common CSI+erase sequences.
  • Replaced repeated interpolations in AnsiTerminalTestProgressFrame.Render and AnsiTerminal.EraseProgress with the new constants.
  • Updated AnsiTerminal.MoveCursorUp to append cursor-movement sequences directly to the batching StringBuilder (avoiding intermediate strings).
Show a summary per file
File Description
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs Uses new precomputed CSI erase constants in the render hot path.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminal.cs Avoids intermediate string allocation when batching cursor moves; uses CSI erase constant for progress erase.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiCodes.cs Adds const string shortcuts for CSI+erase sequences to enable allocation-free reuse.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0

@Evangelink Evangelink marked this pull request as ready for review June 2, 2026 15:37
@Evangelink Evangelink enabled auto-merge (squash) June 2, 2026 15:38
@Evangelink Evangelink merged commit 79499fa into main Jun 2, 2026
38 checks passed
@Evangelink Evangelink deleted the perf-assist/render-alloc-reduction-e7a4ef3abf2cdf8d branch June 2, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants