Cache regex assertion patterns safely - #10661
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
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
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
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.Namedoes not fully identify the culture whose casing rulesnew Regex(pattern)captures.CultureInfo.NameandTextInfoare 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 wrongRegex, 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 differentTextInfobehavior.
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
🧵 Parallel-safety audit — PR #10661Parallelization
Findings: A (global-state) Top actions (by expected value):
Info
Advisory only — heuristic, non-blocking. Re-run with
|
🧪 Expert test review — PR #10661Reviewed the 12 new test methods added to
This advisory comment was generated automatically. Grades are heuristic
|
Summary
Regexoverload path and existing exception/telemetry orderingMemory 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
Regexis constructed. Default options and timeout semantics remain those ofnew Regex(pattern). Construction remains inToRegex, before assertion telemetry and value validation, preserving invalid-pattern/null ordering and exception stack shape. The overloads accepting a caller-createdRegexbypass 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.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
TestFramework.UnitTestsbuild and execution onnet48,net8.0,net9.0, andnet8.0-windows10.0.18362.0Regexbypassartifacts/log/Debug/Build.binlogCloses #10659