Skip to content

[Efficiency Improver] perf: eliminate LINQ closure allocations in TreeNodeFilter#8035

Open
abdelghani-moussaid wants to merge 9 commits intomicrosoft:mainfrom
abdelghani-moussaid:perf/eliminate-treenodefilter-allocations
Open

[Efficiency Improver] perf: eliminate LINQ closure allocations in TreeNodeFilter#8035
abdelghani-moussaid wants to merge 9 commits intomicrosoft:mainfrom
abdelghani-moussaid:perf/eliminate-treenodefilter-allocations

Conversation

@abdelghani-moussaid
Copy link
Copy Markdown

@abdelghani-moussaid abdelghani-moussaid commented May 5, 2026

Goal

Eradicate per-call heap allocations in the filter-matching hot path to improve energy proportionality and reduce GC pressure during large test suite executions.

Related Issue

Fixes #7998

Changes

  • OperatorExpression.SubExpressions: Changed the type from IReadOnlyCollection to IReadOnlyList. The property now safely reuses the input if it is already a list (subExpressions is IReadOnlyList ? ...), avoiding unnecessary array copies during parsing while still enabling zero-allocation indexed for loops during evaluation.
  • Direct Property Walk: Optimized MatchProperties to walk the PropertyBag directly (properties._property), removing the AsEnumerable() wrapper, its associated closures, and IEnumerator boxing.
  • MatchFilterPattern & MatchProperties: Replaced LINQ switch expressions and .Any() calls with procedural switch statements and index-based for loops.
  • Span-Based Matching & TFM Compatibility: Migrated path fragment matching to ReadOnlySpan to eliminate substring allocations, protected by #if NETSTANDARD2_0 guards to maintain legacy build stability.
  • Fail-Fast Validation: Added a constructor guard that immediately throws a localized ArgumentException if the filter parses to zero segments, preventing silent pipeline failures.

Performance Evidence

Ran via BenchmarkDotNet on .NET 10.

Method Mean Allocated
MatchFilterPattern (Baseline) 185.5 ns 128 B
MatchFilterPattern (Optimized) 111.8 ns 0 B

For a suite of 10,000 tests, this eliminates ~60,000–100,000 allocations per run.

Validation

  • Tests: Successfully ran test\UnitTests\Microsoft.Testing.Platform.UnitTests targeting TreeNodeFilterTests. Added EmptyFilter_Invalid to verify the new fail-fast constructor guard.
  • Result: 51/51 Tests Passed.
  • Style: Zero StyleCop or Roslyn analyzer warnings.

Copilot AI review requested due to automatic review settings May 5, 2026 21:24
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 TreeNodeFilter matching hot path to reduce runtime allocations and GC pressure during large test suite executions.

Changes:

  • Replaced several LINQ-based filter/property matching paths with procedural switch + index-based loops.
  • Changed OperatorExpression.SubExpressions to an array to enable allocation-free indexed iteration.
  • Avoided substring allocations in path matching by slicing the input path into fragments.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs Reworks filter matching to reduce allocations (procedural loops, span-based fragment slicing).
src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs Stores sub-expressions as an array to enable efficient indexed iteration.

@abdelghani-moussaid
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copilot AI review requested due to automatic review settings May 5, 2026 22:23
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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment thread src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 5, 2026 22:58
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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 5, 2026 23:07
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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

- Move empty filter validation to TreeNodeFilter constructor to fail fast with a localized ArgumentException.
- Optimize MatchProperties by replacing the foreach loop with a direct linked-list walk, eliminating IEnumerator boxing allocations.
- Expose OperatorExpression.SubExpressions as IReadOnlyList<T> and reuse the collection when possible to avoid array copy allocations.
- Implement ReadOnlySpan<char> fast-path for filter fragment matching with conditional #if fallback for netstandard2.0.
- Resolve merge conflicts.
Copilot AI review requested due to automatic review settings May 6, 2026 17:28
@abdelghani-moussaid abdelghani-moussaid marked this pull request as draft May 7, 2026 09:23
- Remove Dead Code: Removed the unreachable _testNodeStateProperty branch in TreeNodeFilter.MatchProperties.
- Add Unit Test: Added EmptyFilter_Invalid to TreeNodeFilterTests.cs to verify that passing an empty string to TreeNodeFilter correctly throws an ArgumentException.
@abdelghani-moussaid
Copy link
Copy Markdown
Author

PR Ready for Review: Zero-Allocation TreeNodeFilter

Fixes #7998

This PR successfully achieves the goal of eliminating per-call heap allocations in the TreeNodeFilter matching hot path, significantly reducing GC pressure during large test suite executions.

📊 Performance Verification

  • Allocated Memory: Reduced from 128 B to 0 B (100% reduction).

  • Execution Time: ~47% faster / 0.53x ratio.

✅ Key Architectural Updates

  • Zero-Allocation Hot Path: Replaced LINQ and IEnumerator boxing with a direct linked-list walk (properties._property) and index-based procedural loops.

  • TFM Compatibility: Implemented #if NETSTANDARD2_0 preprocessor guards to enable ReadOnlySpan<char> fast-paths for modern frameworks while maintaining legacy build stability.

  • Fail-Fast Validation: Moved empty filter validation to the constructor. Passing an empty string or / now immediately throws an ArgumentException using the localized TreeNodeFilterCannotBeEmptyErrorMessage resource.

  • Test Coverage: Added EmptyFilter_Invalid to TreeNodeFilterTests.cs to verify the new fail-fast constructor guard.

  • Upstream Sync: Merged latest upstream/main to resolve the SoftAssertionTests string casing regression.

💡 Note on OperatorExpression.SubExpressions

Automated reviews suggested forcing SubExpressions to materialize strictly as a FilterExpression[] array to avoid interface dispatch. I opted to maintain IReadOnlyList<FilterExpression> (subExpressions is IReadOnlyList<FilterExpression> list ? list : [.. subExpressions];). This safely avoids unnecessary array copying allocations during parsing when the caller already provides a list, which provides a better overall balance of performance and architectural cleanliness.

All CI checks are green. Ready for review and merge!

@abdelghani-moussaid abdelghani-moussaid marked this pull request as ready for review May 7, 2026 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Efficiency Improver] perf: eliminate LINQ closure allocations in TreeNodeFilter hot paths

2 participants