[perf-improver] perf: replace Regex array in AnsiDetector with direct string comparisons#8683
Conversation
Eliminates 17 Regex object allocations and their compilation cost by replacing regex patterns with equivalent StartsWith/Contains string operations. The terminal-type detection patterns are all simple prefix/substring checks that don't need regex. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Replaces a static readonly Regex[] of 17 patterns in AnsiDetector.IsAnsiSupported with a chain of StartsWith/Contains calls (all StringComparison.Ordinal) to remove the regex allocations/compilation that happen at class init for what are effectively prefix/substring checks.
Changes:
- Remove
TerminalsRegexesarray and.Any(regex => regex.IsMatch(...))loop. - Inline 17 equivalent string comparisons, including the negative-lookahead case for
etermvseterm-color.
Show a summary per file
| File | Description |
|---|---|
| src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiDetector.cs | Replaces the regex array with a chained StartsWith/Contains expression. |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 1
🔍 Build Failure AnalysisSummary — The Root cause:
|
| File | Line | Call |
|---|---|---|
AnsiDetector.cs:26 |
26 | termType.Contains("eterm", StringComparison.Ordinal) |
AnsiDetector.cs:29 |
29 | termType.Contains("tmux", StringComparison.Ordinal) |
AnsiDetector.cs:34 |
34 | termType.Contains("ansi", StringComparison.Ordinal) |
AnsiDetector.cs:35 |
35 | termType.Contains("scoansi", StringComparison.Ordinal) |
AnsiDetector.cs:36 |
36 | termType.Contains("cygwin", StringComparison.Ordinal) |
AnsiDetector.cs:37 |
37 | termType.Contains("linux", StringComparison.Ordinal) |
AnsiDetector.cs:38 |
38 | termType.Contains("konsole", StringComparison.Ordinal) |
AnsiDetector.cs:39 |
39 | termType.Contains("bvterm", StringComparison.Ordinal) |
AnsiDetector.cs:41 |
41 | termType.Contains("alacritty", StringComparison.Ordinal) |
Proposed fix
Replace every termType.Contains(value, StringComparison.Ordinal) with termType.IndexOf(value, StringComparison.Ordinal) >= 0. string.IndexOf(string, StringComparison) is available in netstandard2.0. The StartsWith calls are fine — that overload exists on netstandard2.0.
Example diff for all 9 affected lines:
- || (termType.Contains("eterm", StringComparison.Ordinal)
+ || (termType.IndexOf("eterm", StringComparison.Ordinal) >= 0
- || termType.Contains("tmux", StringComparison.Ordinal)
+ || termType.IndexOf("tmux", StringComparison.Ordinal) >= 0
- || termType.Contains("ansi", StringComparison.Ordinal)
+ || termType.IndexOf("ansi", StringComparison.Ordinal) >= 0
- || termType.Contains("scoansi", StringComparison.Ordinal)
+ || termType.IndexOf("scoansi", StringComparison.Ordinal) >= 0
- || termType.Contains("cygwin", StringComparison.Ordinal)
+ || termType.IndexOf("cygwin", StringComparison.Ordinal) >= 0
- || termType.Contains("linux", StringComparison.Ordinal)
+ || termType.IndexOf("linux", StringComparison.Ordinal) >= 0
- || termType.Contains("konsole", StringComparison.Ordinal)
+ || termType.IndexOf("konsole", StringComparison.Ordinal) >= 0
- || termType.Contains("bvterm", StringComparison.Ordinal)
+ || termType.IndexOf("bvterm", StringComparison.Ordinal) >= 0
- || termType.Contains("alacritty", StringComparison.Ordinal);
+ || termType.IndexOf("alacritty", StringComparison.Ordinal) >= 0;Build overview
- Project:
Microsoft.Testing.Platform.csproj - Target framework:
netstandard2.0 - Error:
CS1501— No overload for method 'Contains' takes 2 arguments (9 occurrences) - File:
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiDetector.cs
🤖 Generated by the Build Failure Analysis workflow · commit bc95b9f
Generated by Build Failure Analysis for issue #8683 · sonnet46 2.3M · ◷
Evangelink
left a comment
There was a problem hiding this comment.
Generated by Build Failure Analysis for issue #8683 · sonnet46 2.3M
…ingComparison) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🤖 This is an automated contribution from Perf Improver.
Goal and Rationale
AnsiDetectorheld astatic readonly Regex[] TerminalsRegexesfield initialised with 17new Regex(...)instances. These were allocated (and in some runtimes compiled) at class-initialisation time, purely to perform very simple prefix/substring checks against a terminal-type string. Every pattern maps directly to aStartsWithorContainscall, so no regex engine is needed.Approach
Replace the
Regex[]field and its.Any(regex => regex.IsMatch(...))loop with a singleboolexpression chainingstring.StartsWith/string.Contains(all withStringComparison.Ordinal), preserving each original pattern's semantics exactly — including the negative-lookahead case (^(?!eterm-color).*eterm.*→Contains("eterm") && !StartsWith("eterm-color")).Performance Evidence
Regexinstances allocated at startupRegex[]array allocationIsAnsiSupportedis called once per process (fromNativeMethods) so this is a startup-cost win. The method now allocates nothing and executes with pure string comparison instructions.Methodology: code inspection + BenchmarkDotNet would show reduced allocations on first call; the key claim (17 fewer allocations) is directly verifiable from the diff.
Trade-offs
Test Status
Microsoft.Testing.Platform.UnitTests(net8.0): 992 passed, 0 failed, 3 skipped ✅Reproducibility
./build.sh # restore + build artifacts/bin/Microsoft.Testing.Platform.UnitTests/Debug/net8.0/Microsoft.Testing.Platform.UnitTestsAdd this agentic workflows to your repo
To install this agentic workflow, run