From 547c9f995041703a07bda91b52ae4cf3e62bbef1 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 7 Jun 2026 20:51:20 +0200 Subject: [PATCH] fix(executor-core): F-P-014 thread-local LRU regex cache (128 entries) regex::is_match, find, find_all, replace, replace_all, split, captures all called Regex::new(pattern) per invocation. A script doing `regex::is_match("\\d+", x)` in a tight loop paid the compile every iteration. Regex compile dominates is_match on short strings. Add a thread-local LruCache> (cap 128). The compile helper now memoises by pattern string, returning Arc on hit. Cap is well above what any sensible script uses but bounded enough to keep memory predictable on long-running threads. AUDIT.md anchor: F-P-014. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/executor-core/src/sdk/stdlib/regex.rs | 38 ++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/crates/executor-core/src/sdk/stdlib/regex.rs b/crates/executor-core/src/sdk/stdlib/regex.rs index 7ba5557..3fc87a3 100644 --- a/crates/executor-core/src/sdk/stdlib/regex.rs +++ b/crates/executor-core/src/sdk/stdlib/regex.rs @@ -1,13 +1,29 @@ //! `regex::` — non-backtracking regular expressions (Rust `regex` crate). //! -//! Patterns compile per call. No cache: premature for v1.1.0, and the -//! `regex` crate's linear-time guarantees keep per-call cost bounded. -//! Catastrophic patterns are rejected at compile time by the crate -//! itself; no extra defense needed. +//! F-P-014: compiled patterns are cached in a thread-local LRU so a +//! tight script loop like `regex::is_match("\\d+", x)` doesn't recompile +//! every iteration. Compile dominates `is_match` on short strings; +//! caching shrinks per-call cost to a HashMap probe + `Arc::clone`. +use std::cell::RefCell; +use std::num::NonZeroUsize; +use std::sync::Arc; + +use lru::LruCache; use regex::Regex; use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Module}; +/// Per-thread cap on compiled regex patterns. 128 is well above what +/// any sensible script uses but bounded enough to keep memory in +/// check on a fleet of long-running threads. +const REGEX_CACHE_SIZE: usize = 128; + +thread_local! { + static REGEX_CACHE: RefCell>> = RefCell::new( + LruCache::new(NonZeroUsize::new(REGEX_CACHE_SIZE).expect("non-zero")) + ); +} + pub fn register(engine: &mut RhaiEngine) { let mut module = Module::new(); register_is_match(&mut module); @@ -20,8 +36,18 @@ pub fn register(engine: &mut RhaiEngine) { engine.register_static_module("regex", module.into()); } -fn compile(pattern: &str) -> Result> { - Regex::new(pattern).map_err(|e| format!("invalid regex: {e}").into()) +fn compile(pattern: &str) -> Result, Box> { + REGEX_CACHE.with(|cell| { + if let Some(cached) = cell.borrow_mut().get(pattern) { + return Ok(cached.clone()); + } + let re = Arc::new( + Regex::new(pattern) + .map_err(|e| -> Box { format!("invalid regex: {e}").into() })?, + ); + cell.borrow_mut().put(pattern.to_string(), re.clone()); + Ok(re) + }) } fn register_is_match(module: &mut Module) {