From 0e17105912573788c3c20c23df13bdfe5c10f5b6 Mon Sep 17 00:00:00 2001 From: Abdelghani Moussaid <20841568+abdelghani-moussaid@users.noreply.github.com> Date: Thu, 14 May 2026 22:31:12 +0100 Subject: [PATCH] perf: reduce TreeNodeFilter allocations via IReadOnlyList and ReadOnlySpan - Widen SubExpressions to IReadOnlyList to enable optimized LINQ paths. - Implement ReadOnlySpan-based matching for .NET 8.0+ to eliminate string fragment allocations. - Use procedural loops in Span path to avoid ref-struct capture (CS9108). --- .../TreeNodeFilter/OperatorExpression.cs | 4 +- .../Requests/TreeNodeFilter/TreeNodeFilter.cs | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs index cf5409e595..0333f7e048 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs @@ -4,9 +4,9 @@ namespace Microsoft.Testing.Platform.Requests; // Allows to express an expression A & B or A | B -internal sealed class OperatorExpression(FilterOperator op, IReadOnlyCollection subExpressions) : FilterExpression +internal sealed class OperatorExpression(FilterOperator op, IReadOnlyList subExpressions) : FilterExpression { public FilterOperator Op { get; } = op; - public IReadOnlyCollection SubExpressions { get; } = subExpressions; + public IReadOnlyList SubExpressions { get; } = subExpressions; } diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs index da7178a629..c10e2bb94c 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs @@ -542,10 +542,60 @@ private static bool MatchFilterPattern( int endFragmentIndex, PropertyBag properties) { +#if NET8_0_OR_GREATER + return MatchFilterPattern( + filterExpression, + testNodeFullPath.AsSpan(startFragmentIndex, endFragmentIndex - startFragmentIndex), + properties); +#else string str = testNodeFullPath[startFragmentIndex..endFragmentIndex]; return MatchFilterPattern(filterExpression, str, properties); +#endif } +#if NET8_0_OR_GREATER + private static bool MatchFilterPattern( + FilterExpression filterExpression, + ReadOnlySpan testNodeFragment, + PropertyBag properties) + { + switch (filterExpression) + { + case ValueExpression vExpr: + return vExpr.Regex.IsMatch(testNodeFragment); + case OperatorExpression { Op: FilterOperator.Or, SubExpressions: var subexprs }: + foreach (FilterExpression expr in subexprs) + { + if (MatchFilterPattern(expr, testNodeFragment, properties)) + { + return true; + } + } + + return false; + case OperatorExpression { Op: FilterOperator.And, SubExpressions: var subexprs }: + foreach (FilterExpression expr in subexprs) + { + if (!MatchFilterPattern(expr, testNodeFragment, properties)) + { + return false; + } + } + + return true; + case OperatorExpression { Op: FilterOperator.Not, SubExpressions: var subexprs }: + return !MatchFilterPattern(subexprs.Single(), testNodeFragment, properties); + case ValueAndPropertyExpression { Value: var valueExpr, Properties: var propExpr }: + return MatchFilterPattern(valueExpr, testNodeFragment, properties) + && MatchProperties(propExpr, properties); + case NopExpression: + return true; + default: + throw ApplicationStateGuard.Unreachable(); + } + } +#endif + private static bool MatchFilterPattern( FilterExpression filterExpression, string testNodeFragment,