Skip to content

[efficiency-improver] perf: cache ExecutionId and single-pass property scan in DotnetTestDataConsumer#8866

Merged
Evangelink merged 2 commits into
mainfrom
efficiency/cache-execution-id-single-pass-dotnet-test-bb739c4ef42e0ddd
Jun 7, 2026
Merged

[efficiency-improver] perf: cache ExecutionId and single-pass property scan in DotnetTestDataConsumer#8866
Evangelink merged 2 commits into
mainfrom
efficiency/cache-execution-id-single-pass-dotnet-test-bb739c4ef42e0ddd

Conversation

@Evangelink

Copy link
Copy Markdown
Member

🤖 This is a draft PR from Efficiency Improver, an automated AI assistant focused on reducing energy consumption and computational footprint.


Goal & Rationale

Two per-test inefficiencies in DotnetTestDataConsumer (the consumer that ships every test result to dotnet test):

  1. ExecutionId was a computed property — it called GetEnvironmentVariable() on every single access. The value is set once at process startup and never changes during a test run, yet N test result events each paid the cost of a native environment-variable lookup + a new string allocation. Caching it as a readonly field removes all N redundant lookups.

  2. GetTestNodeDetails walked the PropertyBag linked list 3× per test — once for StandardOutputProperty, once for StandardErrorProperty, and once (conditionally) for TimingProperty. These three separate passes are replaced with a single pass using PropertyBag.GetStructEnumerator(), a zero-allocation struct enumerator that is internal to the Microsoft.Testing.Platform assembly.

Focus Area

Code-Level Efficiency — unnecessary computation / redundant allocation per test result.

Approach

Change 1 — Cache ExecutionId

// Before (computed property — called once per test event, allocates a new string each time)
private string? ExecutionId => _environment.GetEnvironmentVariable(...);

// After (cached at construction time — one lookup, one string, zero per-test cost)
private readonly string? _executionId;
public DotnetTestDataConsumer(DotnetTestConnection conn, IEnvironment env)
{
    _executionId = env.GetEnvironmentVariable(...);
}

_environment is no longer needed as a field, so it is removed.

Change 2 — Single-pass property scan in GetTestNodeDetails

// Before — 3 separate PropertyBag linked-list walks per test
string? standardOutput = ...Properties.SingleOrDefault<StandardOutputProperty>()?.StandardOutput;
string? standardError  = ...Properties.SingleOrDefault<StandardErrorProperty>()?.StandardError;
// (inside switch cases:)
duration = ...Properties.SingleOrDefault<TimingProperty>()?.GlobalTiming.Duration.Ticks;

// After — one pass, zero enumerator allocation (struct enumerator)
TimingProperty? timingProperty = null;
string? standardOutput = null;
string? standardError  = null;
PropertyBag.PropertyBagEnumerator enumerator = ...Properties.GetStructEnumerator();
while (enumerator.MoveNext())
{
    switch (enumerator.Current)
    {
        case TimingProperty tp:       timingProperty = tp; break;
        case StandardOutputProperty s: standardOutput = s.StandardOutput; break;
        case StandardErrorProperty e:  standardError  = e.StandardError;  break;
    }
}

Energy Efficiency Evidence

Proxy metric used: CPU instruction count + heap allocation count
(Direct energy measurement unavailable; these proxies are standard Green Software proxies for energy reduction.)

Metric Before After
GetEnvironmentVariable calls / test 1 0 (cached once at startup)
String allocations for ExecutionId / test 1 0
PropertyBag linked-list walks per test in GetTestNodeDetails 3 1
Enumerator heap allocations per GetTestNodeDetails call 0 (LINQ SingleOrDefault is generic but struct-inlined) 0 (struct enumerator)

For a 10 000-test dotnet test run:

  • 10 000 fewer environment-variable lookups (native interop + string marshal)
  • 10 000 fewer short-lived string allocations (GC Gen0 pressure reduced)
  • ~20 000 fewer linked-list node dereferences in property scanning

These are small absolute numbers but occur on every test result in every dotnet test run. They are consistent, compounding, and require no trade-off in correctness or readability.

Green Software Foundation Context

Hardware Efficiency — eliminating redundant work (native calls, string allocations, list traversals) lets the same hardware do more testing per unit of energy. Per the GSF SCI model, reducing instructions/allocations per functional unit (one test result) directly lowers the Energy component.

Trade-offs

  • The single-pass loop is slightly more verbose than the three separate SingleOrDefault calls, but the trade-off is clearly documented in an inline comment.
  • The PropertyBag.PropertyBagEnumerator type is internal. This pattern is already used elsewhere in the platform assembly (e.g. SerializerUtilities) and is the canonical low-allocation iteration path within the assembly.
  • Behavior is identical: TestNodeStateProperty was already retrieved via the O(1) fast-path field, unchanged.

Test Status

✅ Full solution build: ./build.sh -buildBuild succeeded. 0 Warning(s). 0 Error(s).

Reproducibility

# Build baseline (no changes)
git stash
./build.sh -build
# Time the DotnetTestDataConsumer code path in a test run

# Build with changes
git stash pop
./build.sh -build

For quantitative comparison, profile with dotnet-trace or BenchmarkDotNet targeting DotnetTestDataConsumer.ConsumeAsync with a synthetic TestNodeUpdateMessage workload.

Generated by Efficiency Improver · sonnet46 12.3M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

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

…stDataConsumer

Previously, DotnetTestDataConsumer had two per-test inefficiencies:

1. ExecutionId was a computed property calling GetEnvironmentVariable() on
   every access. For N tests, this produced N environment-variable lookups
   and N redundant string allocations (once per TestNodeUpdateMessage, plus
   once each for session start/end events). The environment variable is set
   once at process startup and never changes, so the value is now cached in
   a readonly field in the constructor.

2. GetTestNodeDetails() walked the PropertyBag linked list separately for
   each of StandardOutputProperty, StandardErrorProperty, and TimingProperty
   (3 walks per test). These are replaced with a single pass using
   PropertyBag.GetStructEnumerator() (a zero-allocation struct enumerator),
   reducing linked-list traversals from 3N to N for a run with N tests.

Proxy metrics (CPU cycles / memory allocation) map to energy reduction:
fewer object allocations reduce GC pressure; fewer linked-list walks reduce
instruction count. For a 10,000-test run these savings are small but
consistent, and they compound across every process that uses dotnet test.

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

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

This PR targets per-test execution efficiency in the Dotnet test IPC consumer (DotnetTestDataConsumer) within Microsoft.Testing.Platform by removing redundant environment-variable lookups and reducing repeated property-bag scans.

Changes:

  • Cache the dotnet-test ExecutionId at construction time instead of recomputing it per message.
  • Replace multiple PropertyBag.SingleOrDefault<T>() traversals in GetTestNodeDetails with a single struct-enumerator pass that gathers required properties.
Show a summary per file
File Description
src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/DotnetTestDataConsumer.cs Caches execution id and performs single-pass property extraction for per-test result message creation.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

@Evangelink Evangelink marked this pull request as ready for review June 6, 2026 14:05
Keep the single-pass scan but throw the same InvalidOperationException as PropertyBag.SingleOrDefault when duplicate timing/output/error properties are present.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink Evangelink merged commit 3d30712 into main Jun 7, 2026
40 checks passed
@Evangelink Evangelink deleted the efficiency/cache-execution-id-single-pass-dotnet-test-bb739c4ef42e0ddd branch June 7, 2026 11:10
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.

2 participants