Skip to content

perf(regex): build each unique pattern once at construction, not twice#6534

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf/regex-build-once
Jul 17, 2026
Merged

perf(regex): build each unique pattern once at construction, not twice#6534
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf/regex-build-once

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem

js_regexp_new validates an uncached pattern by fully NFA-compiling it (build_std_regex, falling back to build_fancy_regex) — and throws the result away; get_or_compile_regex then re-translates and re-compiles the same pattern to populate the cache. Every unique regex literal pays two Thompson-NFA constructions at construction time.

A bundled app evaluates every module-level literal eagerly at startup, so this doubles regex cost exactly where it hurts: startup profiles of a large compiled TUI app show regex_automata::nfa::thompson::compiler + regex_syntax::utf8::Utf8Sequences among the top frames during the multi-second launch, and Unicode-class-heavy patterns (the emoji-regex family) cost milliseconds per build.

Fix

compile_and_cache_regex_checked: one translate + one build, seeding REGEX_CACHE (or FANCY_CACHE + the never-match placeholder) and reporting validity. js_regexp_new's validation consumes that, making the subsequent get_or_compile_regex a guaranteed cache hit. get_or_compile_regex keeps its exact fallback (cache + return never-match when both engines reject).

One historical edge preserved: validation used to test the BARE translated pattern while the cache built the (?ims)-prefixed one, so a pattern that compiles bare but exceeds the size limit with the prefix was a silent never-match rather than a SyntaxError. On checked-compile failure the bare-pattern probe still runs, keeping that observable behavior unchanged.

Verification

regex unit tests: 42 passed / 0 failed. Full-app startup before/after numbers to follow in a comment once the big-binary rebuild completes.

Summary by CodeRabbit

  • Bug Fixes
    • Improved regular expression handling by reusing compiled patterns for faster, more consistent execution.
    • Added support for falling back to an alternate regex engine when standard compilation fails.
    • Improved validation and error reporting for invalid regular expressions while preserving compatibility for edge cases.
    • Added safeguards to prevent unbounded regex cache growth.

js_regexp_new validated an uncached pattern by fully NFA-compiling it
(build_std_regex, falling back to build_fancy_regex) and THREW AWAY the
result, then get_or_compile_regex re-translated and re-compiled the same
pattern to populate the cache. Every unique regex literal in a program
paid two Thompson-NFA constructions at startup — a bundle whose modules
construct their literals eagerly at load pays this across every literal,
and Unicode-class-heavy patterns (the emoji-regex family) cost
milliseconds per build.

Restructure into compile_and_cache_regex_checked: one translate + one
build, seeding REGEX_CACHE (or FANCY_CACHE + the never-match
placeholder) and reporting validity; js_regexp_new's validation consumes
that, so the get_or_compile_regex that follows is a guaranteed cache
hit. get_or_compile_regex keeps its exact fallback behavior (cache +
return never-match when both engines reject).

One historical edge preserved: validation used to test the BARE
translated pattern while the cache built the (?ims)-prefixed one, so a
pattern that compiles bare but exceeds the size limit with the prefix
was a silent never-match, not a SyntaxError. On checked-compile failure
the bare-pattern probe runs as before, keeping that behavior observable
unchanged.

regex unit tests 42/0.
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Regex compilation and caching are centralized in a shared helper with standard-engine and fancy-regex fallback support. Cache eviction is bounded, lookup uses the helper on misses, and js_regexp_new validation now follows the same path.

Changes

Regex runtime

Layer / File(s) Summary
Centralized compilation and caching
crates/perry-runtime/src/regex.rs
Translates patterns, applies supported flags, compiles with the standard engine, falls back to fancy-regex, caches results, and evicts entries when the cache limit is exceeded.
Lookup and RegExp validation
crates/perry-runtime/src/regex.rs
Uses centralized compilation on cache misses and updates js_regexp_new validation and failure handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RegExp
  participant REGEX_CACHE
  participant Compiler
  participant StandardRegex
  participant FancyRegex

  RegExp->>Compiler: validate pattern and flags
  Compiler->>StandardRegex: compile translated pattern
  StandardRegex-->>Compiler: success or failure
  Compiler->>FancyRegex: compile when standard engine fails
  FancyRegex-->>Compiler: success or failure
  Compiler->>REGEX_CACHE: store compiled result
  REGEX_CACHE-->>RegExp: cached regex or never-match fallback
Loading

Possibly related PRs

  • PerryTS/perry#5777: Updates the same regex compilation and cache paths to avoid redundant compilation.
  • PerryTS/perry#5808: Changes js_regexp_new validation and cache handling in the same runtime module.

Suggested reviewers: andrewtdiz

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change, but it does not follow the required template sections for Summary, Changes, Related issue, and Test plan. Rewrite the PR description to match the template: add Summary, Changes, Related issue, Test plan with commands, and the checklist sections.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately states the main regex optimization.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/regex.rs`:
- Around line 280-286: Update the cache handling around REGEX_CACHE and
FANCY_CACHE so evicting a fancy entry also removes its corresponding placeholder
from REGEX_CACHE, preventing stale placeholders from being treated as complete
matches. Preserve standard compiled entries, ensure a subsequent fancy lookup
reconstructs the program and sets fancy_ptr, and add a regression covering
asymmetric cache churn followed by reconstruction of a fancy-only pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 02418202-8a54-4e25-b663-ce4356920b0f

📥 Commits

Reviewing files that changed from the base of the PR and between 18c21e5 and 0cfa089.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/regex.rs

Comment on lines +280 to +286
let already = REGEX_CACHE.with(|cache| {
cache
.borrow()
.contains_key(&(pattern.to_string(), flags.to_string()))
});
if already {
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep fancy placeholders and compiled entries under the same eviction lifecycle.

FANCY_CACHE can clear while REGEX_CACHE retains its placeholder. A later lookup treats that placeholder as a complete hit, so a valid fancy-only pattern silently becomes never-match and fancy_ptr remains null.

Clear corresponding placeholders when evicting FANCY_CACHE, or use one cache entry enum holding either the standard or fancy program. Add a regression that reconstructs a fancy pattern after asymmetric cache churn.

Also applies to: 320-343, 348-356

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/regex.rs` around lines 280 - 286, Update the cache
handling around REGEX_CACHE and FANCY_CACHE so evicting a fancy entry also
removes its corresponding placeholder from REGEX_CACHE, preventing stale
placeholders from being treated as complete matches. Preserve standard compiled
entries, ensure a subsequent fancy lookup reconstructs the program and sets
fancy_ptr, and add a regression covering asymmetric cache churn followed by
reconstruction of a fancy-only pattern.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Full-app numbers (197MB compiled TUI bundle, hyperfine 5 runs, isolated HOME) — measured incrementally on top of #6533's link fix, so this is the regex-only delta:

metric before this PR (with #6533) after node
--help 4.390s ± 0.175 4.099s ± 0.136 (−0.29s) 0.715s
TUI first paint (PTY, same-load pairs) contributes to the combined −2.3s (10.9s → 8.4s) with #6532/#6533 0.76s

The eliminated work is exactly the discarded validation build of every unique literal at bundle startup. The remaining single eager build is the next lever (lazy/deferred NFA construction), tracked separately.

Verification: regex unit tests 42/0; full perry-runtime suite 1327/0 single-threaded; gap shards 5/8 + 7/8 locally green (only reds: pre-triaged + the known local zlib link artifact); 4/4 PTY login e2e runs clean on the rebuilt bundle.

@proggeramlug
proggeramlug merged commit ea017d7 into PerryTS:main Jul 17, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant