From 0b6195a8715d9f27c11d395638271b37e99e563f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 28 May 2026 19:31:40 +0100 Subject: [PATCH] perf(engine): use MemoryExtensions.Split for path parsing in MetadataFilterMatcher Replace filterString.Split('/') with a span-based Split into a stackalloc Span on net8+, eliminating the string[] and only materializing the literal segments actually retained by FilterHints. netstandard2.0 keeps string.Split since MemoryExtensions.Split(Span) is net8+ only. Closes #6037 --- .../Services/MetadataFilterMatcher.cs | 85 ++++++++++++------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/TUnit.Engine/Services/MetadataFilterMatcher.cs b/TUnit.Engine/Services/MetadataFilterMatcher.cs index 17c012d34f..28ee3729e7 100644 --- a/TUnit.Engine/Services/MetadataFilterMatcher.cs +++ b/TUnit.Engine/Services/MetadataFilterMatcher.cs @@ -153,53 +153,71 @@ public static FilterHints ExtractFilterHints(ITestExecutionFilter? filter) } // Parse path: /{assembly}/{namespace}/{className}/{methodName} - var parts = filterString.Split('/'); - // Expected format: "", assembly, namespace, className, methodName - // parts[0] is empty (before first /), parts[1] is assembly, etc. - if (parts.Length < 2) - { - return default; - } + // segment[0] is empty (before first /), segment[1] is assembly, etc. + // FilterHints only retains segments 1-4, so we only materialize those. +#if NET8_0_OR_GREATER + var filterSpan = filterString.AsSpan(); - string? assemblyName = null; - string? namespaceName = null; - string? className = null; - string? methodName = null; + // Up to 5 useful segments (empty, assembly, namespace, className, methodName); + // a 6th range absorbs any trailing path so the earlier segments stay intact. + Span ranges = stackalloc Range[6]; + var count = filterSpan.Split(ranges, '/'); - // Extract assembly name (parts[1]) - if (parts.Length > 1 && !IsNonLiteralSegment(parts[1])) + // Need at least the leading empty segment plus the assembly segment. + if (count < 2) { - assemblyName = parts[1]; + return default; } - // Extract namespace (parts[2]) - if (parts.Length > 2 && !IsNonLiteralSegment(parts[2])) + return new FilterHints { - namespaceName = parts[2]; - } + AssemblyName = ExtractSegment(filterSpan, ranges, count, 1), + Namespace = ExtractSegment(filterSpan, ranges, count, 2), + ClassName = ExtractSegment(filterSpan, ranges, count, 3), + MethodName = ExtractSegment(filterSpan, ranges, count, 4) + }; + } - // Extract class name (parts[3]) - if (parts.Length > 3 && !IsNonLiteralSegment(parts[3])) + private static string? ExtractSegment(ReadOnlySpan filterSpan, ReadOnlySpan ranges, int count, int index) + { + if (index >= count) { - className = parts[3]; + return null; } - // Extract method name (parts[4]) - if (parts.Length > 4 && !IsNonLiteralSegment(parts[4])) + var segment = filterSpan[ranges[index]]; + return IsNonLiteralSegment(segment) ? null : segment.ToString(); + } +#else + var parts = filterString.Split('/'); + + if (parts.Length < 2) { - methodName = parts[4]; + return default; } return new FilterHints { - AssemblyName = assemblyName, - Namespace = namespaceName, - ClassName = className, - MethodName = methodName + AssemblyName = ExtractSegment(parts, 1), + Namespace = ExtractSegment(parts, 2), + ClassName = ExtractSegment(parts, 3), + MethodName = ExtractSegment(parts, 4) }; } + private static string? ExtractSegment(string[] parts, int index) + { + if (index >= parts.Length) + { + return null; + } + + var segment = parts[index]; + return IsNonLiteralSegment(segment) ? null : segment; + } +#endif + // Characters that make a TreeNodeFilter path segment non-literal: // * ? wildcards // ( ) | & ! grouping / logical operators @@ -217,14 +235,17 @@ public static FilterHints ExtractFilterHints(ITestExecutionFilter? filter) private static readonly char[] _filterOperatorChars = { '*', '?', '(', ')', '|', '&', '!', '\\' }; #endif - private static bool IsNonLiteralSegment(string value) - { #if NET8_0_OR_GREATER - return string.IsNullOrEmpty(value) || value.AsSpan().IndexOfAny(_filterOperatorChars) >= 0; + private static bool IsNonLiteralSegment(ReadOnlySpan value) + { + return value.IsEmpty || value.IndexOfAny(_filterOperatorChars) >= 0; + } #else + private static bool IsNonLiteralSegment(string value) + { return string.IsNullOrEmpty(value) || value.IndexOfAny(_filterOperatorChars) >= 0; -#endif } +#endif #pragma warning restore TPEXP public bool CouldMatchFilter(TestMetadata metadata, ITestExecutionFilter? filter)