Skip to content

Cache regex assertion patterns safely - #10661

Open
Amaury Levé (Evangelink) wants to merge 2 commits into
mainfrom
dev/amauryleve/optimize-regex-assertions
Open

Cache regex assertion patterns safely#10661
Amaury Levé (Evangelink) wants to merge 2 commits into
mainfrom
dev/amauryleve/optimize-regex-assertions

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

  • reuse string-pattern regular expressions through a private, bounded cache
  • keep cache hits lock-free while synchronizing only duplicate-check and insertion
  • preserve the caller-supplied Regex overload path and existing exception/telemetry ordering

Memory and behavior safety

The cache is a fixed 15-slot FIFO ring, matching the runtime's default static regex cache size. Only patterns up to 512 UTF-16 code units are admitted, so retained pattern text and regex count are both bounded; larger patterns are constructed per call and never retained. High-cardinality input overwrites old slots rather than growing process-lifetime state.

Entries are keyed by ordinal pattern text and the current culture name because default case-insensitive regex behavior captures culture when the Regex is constructed. Default options and timeout semantics remain those of new Regex(pattern). Construction remains in ToRegex, before assertion telemetry and value validation, preserving invalid-pattern/null ordering and exception stack shape. The overloads accepting a caller-created Regex bypass this cache unchanged.

Concurrent hits use volatile reads. Misses construct outside the lock, then perform a synchronized second lookup and fixed-slot insertion, so unrelated regex parsing is not serialized and concurrent callers converge on one cached instance.

Benchmarks

Independent Release microbenchmark, seven runs with median reported, telemetry opted out. The baseline mirrors the previous new Regex(pattern).IsMatch(value) path; the candidate invokes the public string assertion overload.

Runtime / scenario Baseline Candidate Allocated baseline Allocated candidate
net8.0, 500k repeated 1,221 ms 128 ms 1,684 MB 0 B
net9.0, 500k repeated 1,227 ms 134 ms 1,692 MB 0 B
net8.0, 50k unique 60 ms 91 ms 114.4 MB 116.4 MB
net9.0, 50k unique 59 ms 87 ms 115.2 MB 117.2 MB

The repeated-pattern case is about 9–10x faster and allocation-free after warmup. The deliberately adversarial unique-pattern case pays the expected bounded lookup/insertion cost (about 27–31 ms and 2 MB across 50,000 calls) while a second 50,000-pattern sweep retained only the fixed cache footprint (approximately 240 bytes net measured growth after full GC).

Validation

  • full TestFramework.UnitTests build and execution on net48, net8.0, net9.0, and net8.0-windows10.0.18362.0
  • focused coverage for reuse, FIFO eviction, long-pattern bypass, culture separation, concurrent convergence, invalid-pattern/null ordering, and caller-supplied Regex bypass
  • two independent memory/concurrency/API reviews, followed by two post-fix reviews
  • binary log captured at artifacts/log/Debug/Build.binlog

Closes #10659

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 21, 2026 02:46
@Evangelink Amaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Aug 21, 2026
Comment thread src/TestFramework/TestFramework/Assertions/Assert.Matches.cs Fixed
Comment thread src/TestFramework/TestFramework/Assertions/Assert.Matches.cs Fixed

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

Adds a bounded, culture-aware regex cache to improve repeated string-pattern assertion performance.

Changes:

  • Adds a lock-free-read, synchronized-write FIFO cache.
  • Adds tests for reuse, eviction, culture, concurrency, and bypass behavior.
Show a summary per file
File Description
src/TestFramework/TestFramework/Assertions/Assert.Matches.cs Implements bounded regex caching.
test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.MatchesRegex.cs Adds cache behavior tests.

Review details

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 2/2 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment thread test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.MatchesRegex.cs Outdated
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 21, 2026 03:23

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.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/TestFramework/TestFramework/Assertions/Assert.Matches.cs:213

  • Keying only by CurrentCulture.Name does not fully identify the culture whose casing rules new Regex(pattern) captures. CultureInfo.Name and TextInfo are virtual on .NET Framework, so a valid derived/custom culture can retain the same name as a previously cached culture while supplying different casing rules; an inline (?i) pattern then reuses the wrong Regex, changing assertion results from the previous per-call construction. Preserve the bounded cache but key by the captured culture identity/casing semantics (or bypass caching for custom/derived cultures), and cover two same-name cultures with different TextInfo behavior.
        string cultureName = CultureInfo.CurrentCulture.Name;
        if (RegexCache.TryGet(pattern, cultureName, out Regex cachedRegex))
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

🧵 Parallel-safety audit — PR #10661

Parallelization

Test assembly Scope Workers Analyzer coverage
Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests off (TestContainer engine — no MSTest parallel scheduler) n/a n/a

⚠️ AssertTests (and its partials, including the changed AssertTests.MatchesRegex.cs) derives from TestContainer (test/Utilities/TestFramework.ForTestingMSTest), a bespoke engine with no parallel scheduler at all. Every finding below is readiness-only — what would matter if this suite were ever ported to MSTest and opted into [Parallelize]. Nothing here is a live race today.

Findings: A (global-state) 1 · B (paths) 0 · C (declaration) 0 · D (over-serialization) 0 — by severity: Critical 0 · High 0 · Warning 0 · Info 2.

Top actions (by expected value):

  1. No action required today; the notes below are forward-looking readiness observations only.

Info

  • [A · High confidence] test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.MatchesRegex.cs:82-99 (MatchesRegex_WithCultureSensitivePattern_DoesNotReuseRegexAcrossCultures) — sets the process-wide-flowing CultureInfo.CurrentCulture (not DefaultThreadCurrentCulture). On the suite's actual TFMs (net48, net8.0, net9.0 — all ≥ .NET Framework 4.6 / modern .NET) this value is carried via ExecutionContext and does not leak to concurrently-running siblings, so it is not a live-race candidate even in a hypothetical parallel port — and the test already restores the original culture in a finally block, which is the correct pattern. Recorded for completeness; no fix needed.
  • [A · Medium confidence] src/TestFramework/TestFramework/Assertions/Assert.Matches.cs — the new RegexCache is a genuine process-global mutable static (a fixed-size ring buffer) that every test calling Assert.MatchesRegex(string, ...) / the new ToRegex reads and writes, including several of the newly added tests (MatchesRegex_WithRepeatedStringPattern_ReusesRegex, ..._WhenOldestRegexIsReusedBeforeCapacityIsExceeded..., ..._WithMaximumLengthStringPattern_ReusesRegex, etc.). If this suite were ever moved onto MSTest with [Parallelize], concurrent writers could evict each other's entries — but eviction only causes a cache miss (a fresh, functionally-identical Regex is compiled and returned), never an incorrect result, and the guid/prefix-randomized patterns used by these tests avoid cross-test key collisions. So this is not a correctness hazard under parallelism, only a (currently moot) contention/perf note; no [ResourceLock] or isolation is warranted. Cross-ref detect-static-dependencies/test-anti-patterns if a static-coupling concern is separately of interest — this audit's read is purely about race-safety, and there is none here.

Advisory only — heuristic, non-blocking. Re-run with /parallel-audit. This audit answers "is it parallel-safe?"; for testability, smells, or flakiness see the detect-static-dependencies / test-smell-detection / test-anti-patterns analyses.

🤖 Automated content by GitHub Copilot. Generated by the Parallel-safety audit on PR (on open / sync) workflow. · auto · 90.2 AIC · ⌖ 3.82 AIC · ⊞ 24.8K · [◷]( · )

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Expert test review — PR #10661

Reviewed the 12 new test methods added to AssertTests.MatchesRegex.cs covering the new bounded regex cache (BoundedRegexCache) in Assert.Matches.cs. All tests independently target a distinct, well-chosen mutation point (FIFO eviction vs. LRU, ordinal vs. ordinal-ignore-case key comparison, exact boundary at MaximumCachedRegexPatternLength, culture-sensitive key, concurrent insert convergence, and validation-order for null value vs. invalid pattern), and every one killed its target mutation in this review. Several tests (ReusesRegex, DoesNotReuseRegex, StillEvictsOldestRegex, boundary tests, culture test, concurrency test, BypassesStringPatternCache) reach into the private ToRegex/BoundedRegexCache implementation via reflection to observe cache identity, which is white-box coupling — but it's a reasonable trade-off here since cache reuse isn't observable through the public API any other way, so it does not pull any test below an A band. No high-confidence actionable findings were found, so no inline suggestions were posted.

GradeTestMutationNotesHow to improve
A (90–100) new AssertTests.
DoesNotMatchRegex_
WithInvalidStringPatternAndNullValue_
ThrowsPatternExceptionFirst
1/1 killed Pins that pattern validation runs before the null-value check via the public API.
A (90–100) new AssertTests.
DoesNotMatchRegex_
WithValidStringPatternAndNullValue_
ThrowsAssertFailedException
1/1 killed Kills a missing/mis-ordered null-value guard using only the public API.
A (90–100) new AssertTests.
MatchesRegex_
WithCaseDistinctStringPatterns_
DoesNotReuseRegex
1/1 killed Pins ordinal, case-sensitive cache-key comparison.
A (90–100) new AssertTests.
MatchesRegex_
WithConcurrentCandidateRegexes_
ConvergesOnSingleCachedRegex
1/1 killed Verifies the synchronized second lookup dedupes racing inserts; relies on reflection into the private cache type.
A (90–100) new AssertTests.
MatchesRegex_
WithCultureSensitivePattern_
DoesNotReuseRegexAcrossCultures
1/1 killed Uses the real tr-TR/en-US dotless-I distinction to prove culture is part of the cache key.
A (90–100) new AssertTests.
MatchesRegex_
WithInvalidStringPatternAndNullValue_
ThrowsPatternExceptionFirst
1/1 killed Pins parameter-validation order via the public API only.
A (90–100) new AssertTests.
MatchesRegex_
WithMaximumLengthStringPattern_
ReusesRegex
1/1 killed Exercises the exact 512-char boundary, complementing the over-limit test to pin the off-by-one.
A (90–100) new AssertTests.
MatchesRegex_
WithOverMaximumLengthStringPattern_
DoesNotCacheRegex
1/1 killed Confirms over-limit patterns bypass caching entirely.
A (90–100) new AssertTests.
MatchesRegex_
WithRegexPattern_
BypassesStringPatternCache
1/1 killed Proves the Regex-overload path neither reads nor writes the string-pattern cache.
A (90–100) new AssertTests.
MatchesRegex_
WithRepeatedStringPattern_
ReusesRegex
1/1 killed Kills the "cache never hits" mutation via reference-identity check.
A (90–100) new AssertTests.
MatchesRegex_
WithValidStringPatternAndNullValue_
ThrowsAssertFailedException
1/1 killed Confirms the null-value guard is not accidentally removed for the string-pattern overload.
A (90–100) new AssertTests.
MatchesRegex_
WhenOldestRegexIsReusedBeforeCapacityIsExceeded_
StillEvictsOldestRegex
1/1 killed Fills capacity, re-touches the oldest entry, then inserts once more, correctly distinguishing FIFO from LRU.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Suggestions on the Files
changed tab can be applied with one click. Re-run with
/review-tests.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 116.2 AIC · ⌖ 4.82 AIC · ⊞ 16.9K · [◷]( · )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-review Awaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[efficiency-improver] Cache compiled Regex instances in Assert.MatchesRegex/DoesNotMatchRegex

3 participants