Skip to content

[efficiency-improver] perf: avoid string[1] allocation in Condition.Evaluate for single-string properties - #16179

Merged
Jakub Jareš (nohwnd) merged 1 commit into
mainfrom
efficiency/condition-evaluate-no-single-string-alloc-69817e00fd657df6
Jun 29, 2026
Merged

[efficiency-improver] perf: avoid string[1] allocation in Condition.Evaluate for single-string properties#16179
Jakub Jareš (nohwnd) merged 1 commit into
mainfrom
efficiency/condition-evaluate-no-single-string-alloc-69817e00fd657df6

Conversation

@nohwnd

Copy link
Copy Markdown
Member

Goal and Rationale

When a test property value is a plain string (the common case for FullyQualifiedName, DisplayName, Source, etc.), Condition.Evaluate previously wrapped it in a new string[1] array before dispatching to EvaluateEqualOperation / EvaluateContainsOperation. This allocation occurred on every test-case evaluation in the slow filter path — i.e., filters using ~ (Contains), !~ (NotContains), or mixed boolean operators.

The ~ operator is the most commonly used developer filter (--filter "FullyQualifiedName~MyTest"), making this a high-frequency allocation.

Focus Area

Code-Level Efficiency — unnecessary object creation per evaluated test case.

Approach

Add a fast path in Condition.Evaluate that handles the string case directly, without wrapping:

// Fast path: single string value (most common case)
if (propertyValue is string singleValue)
{
    return Operation switch
    {
        Operation.Equal    => string.Equals(singleValue, Value, StringComparison.OrdinalIgnoreCase),
        Operation.NotEqual => !string.Equals(singleValue, Value, StringComparison.OrdinalIgnoreCase),
        Operation.Contains => singleValue.IndexOf(Value, StringComparison.OrdinalIgnoreCase) != -1,
        Operation.NotContains => singleValue.IndexOf(Value, StringComparison.OrdinalIgnoreCase) == -1,
        _ => false,
    };
}

The null and string[] cases continue through EvaluateEqualOperation / EvaluateContainsOperation unchanged. Non-string/non-array types retain the ToString() fallback for backward compatibility. The now-unused private GetPropertyValue helper is removed.

This mirrors the approach already taken in FastFilter.TryGetPropertyValue (merged in #16160), which also avoids allocation for single-valued properties.

Energy Efficiency Evidence

Proxy metric: heap allocation count in the slow filter path (less memory churn → less GC pressure → less CPU energy for collection).

Before: GetPropertyValue allocates new string[1] + assigns [0] for every non-string[] property value.

After: string properties go through the fast path with zero allocation.

Estimated reduction: ~1 string[1] (~24 bytes) per test case evaluated when a Contains/NotContains filter is active. For a 10 K-test run with --filter "FullyQualifiedName~Test":

  • ~10 000 allocations eliminated → ~240 KB of short-lived GC garbage removed per filter evaluation pass.
  • For 100 K-test suites: ~2.4 MB per pass.

Green Software Foundation contextHardware Efficiency: reducing allocations makes better use of DRAM bandwidth and CPU cache by reducing GC scan pressure proportional to the working set size.

Trade-offs

  • Slight increase in code complexity (two code paths instead of one); offset by the removal of GetPropertyValue.
  • Readability is maintained: the fast path mirrors FastFilter's existing pattern and is clearly commented.

Reproducibility

# Build
./build.sh --restore --nobl

# Run condition evaluation tests
DOTNET_ROOT=.dotnet .dotnet/dotnet run \
  --project test/Microsoft.TestPlatform.Common.UnitTests/Microsoft.TestPlatform.Common.UnitTests.csproj \
  --no-build --framework net11.0 -- --filter "FullyQualifiedName~Condition"

# Run filter source tests
DOTNET_ROOT=.dotnet .dotnet/dotnet run \
  --project test/Microsoft.TestPlatform.Filter.Source.UnitTests/Microsoft.TestPlatform.Filter.Source.UnitTests.csproj \
  --no-build --framework net11.0

Test Status

  • ✅ 73 / 73 Condition-related tests passed (Microsoft.TestPlatform.Common.UnitTests, net11.0)
  • ✅ 45 / 45 Filter source tests passed (Microsoft.TestPlatform.Filter.Source.UnitTests, net11.0)

Generated by Efficiency Improver · 4K AIC · ⌖ 25.9 AIC · ⊞ 45.6K ·

…ing properties

When a test property value is a plain string (the common case for
FullyQualifiedName, DisplayName, Source, etc.), Condition.Evaluate
previously wrapped it in a new string[1] array before dispatching to
EvaluateEqualOperation / EvaluateContainsOperation. This allocation
happened on every test-case evaluation in the slow filter path (filters
using '~', '!~', or mixed operators).

Add a fast path that handles the string case inline, eliminating the
transient string[1] per evaluated test case.  The null and string[]
cases are unchanged; non-string/non-array types retain the ToString()
fallback for backward compatibility.

Proxy metric: heap allocation count in the slow filter path.
Expected reduction: ~1 string[1] (~24 bytes) per test case evaluated
when a Contains/NotContains filter is active.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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 improves the performance of the test filter evaluation slow path by avoiding a per-evaluation string[1] allocation when a filter property value is already a single string (common for properties like FullyQualifiedName, DisplayName, Source).

Changes:

  • Adds a fast path in Condition.Evaluate for propertyValue is string, performing the operation directly without wrapping into a one-element array.
  • Keeps existing behavior for null and string[] values, and preserves the ToString() fallback for other types for backward compatibility.
  • Removes the now-unneeded GetPropertyValue helper.

@nohwnd Jakub Jareš (nohwnd) left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Code Review — Condition.Evaluate fast path

Dimensions checked: Algorithmic Correctness · Performance & Allocations · Null Safety & Boundary Validation · Public API Surface Protection


Overall: ✅ Clean

The fast path is semantically equivalent to the removed GetPropertyValue helper across all cases. Verified against each code path:

Property type Old path New path Verdict
string new string[1] { value }EvaluateEqual/ContainsOperation([value]) Direct string.Equals / IndexOf Equivalent — single-element iteration is identical to direct comparison
null Returns null from helper propertyValue switch { null => null, ... } Equivalent
string[] Returns array as-is string[] arr => arr Equivalent
other (ToString coercion) new string[1] { x.ToString()! } new[] { x.ToString()! } Equivalent

NoneFilterValue semantics preserved: EvaluateEqualOperation's null or { Length: 0 } guard is only reachable for null/empty-array property values. A string-typed property value never satisfies those conditions in either old or new code — the fast path skips that method entirely, which is safe.

All four operators verified: Equal, NotEqual, Contains, NotContains in the fast path produce identical boolean results to the indirect path for any single string value.

Value null-safety: Value is annotated string (non-nullable), constructed via FilterHelper.Unescape which returns string. IndexOf(Value, ...) and string.Equals(..., Value, ...) are safe. ✓

No public API changes: Condition is internal sealed. No PublicAPI.Unshipped.txt changes needed. ✓


🧠 Reviewed by expert-reviewer — Algorithmic Correctness, Performance & Allocations, Null Safety, Public API Surface

🧠 Reviewed by Expert Code Reviewer 🧠

@nohwnd
Jakub Jareš (nohwnd) marked this pull request as ready for review June 29, 2026 13:08
@nohwnd
Jakub Jareš (nohwnd) enabled auto-merge (squash) June 29, 2026 13:08
@nohwnd Jakub Jareš (nohwnd) added the 🚢 Ship it! Add to PRs where owner approves automated PR, but cannot approve because they "wrote it". label Jun 29, 2026

@nohwnd Jakub Jareš (nohwnd) left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Code Review — Condition.Evaluate fast path

Dimensions checked: Algorithmic Correctness · Backward Compatibility & Rollback Safety · Null Safety & Boundary Validation · Performance & Allocations · Cross-TFM Compatibility · Public API Surface Protection


Overall: ✅ Clean

The fast path is semantically equivalent to the removed GetPropertyValue helper across all property value types and all operators. Verified:

Property type Old path New path Verdict
string new string[1] { value }EvaluateEqual/ContainsOperation([value]) Direct string.Equals / IndexOf Equivalent
null Returns null from helper propertyValue switch { null => null, ... } Equivalent
string[] Returns array as-is string[] arr => arr Equivalent
other (ToString coercion) new string[1] { x.ToString()! } new[] { x.ToString()! } Equivalent

NoneFilterValue semantics preserved: The null or { Length: 0 } guard in EvaluateEqualOperation is only reachable for null or empty-array property values. A non-null string typed property value never satisfies that guard in either the old or new code — the fast path is safe to skip EvaluateEqualOperation entirely for this case.

All four operators verified: Equal, NotEqual, Contains, NotContains in the fast path produce identical boolean results for any single string value.

Null safety: singleValue is non-null by pattern match precondition. Value is annotated string (non-nullable) and constructed via FilterHelper.Unescape. IndexOf and string.Equals calls are safe.

Cross-TFM: string.Equals(s1, s2, StringComparison) and string.IndexOf(s, StringComparison) are available on all targeted TFMs (net462, netstandard2.0, net8.0+). Switch expressions require C# 8+, already in use.

No public API changes: Condition is internal sealed. No PublicAPI.Unshipped.txt changes needed.

No binding redirect implications: No package changes; this is a pure logic optimization.

Description alignment: Title and description accurately describe the change. The approach mirrors FastFilter.TryGetPropertyValue from #16160 as stated.


🧠 Reviewed by Expert Code Reviewer 🧠

🧠 Reviewed by Expert Code Reviewer 🧠

@nohwnd
Jakub Jareš (nohwnd) merged commit e683cdc into main Jun 29, 2026
50 of 51 checks passed
@nohwnd
Jakub Jareš (nohwnd) deleted the efficiency/condition-evaluate-no-single-string-alloc-69817e00fd657df6 branch June 29, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentic-workflows Area: Performance 🚢 Ship it! Add to PRs where owner approves automated PR, but cannot approve because they "wrote it".

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants