You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
RuntimeHelpers.matches() recompiles regex Pattern on every evaluation #1038
RuntimeHelpers.matches() calls Pattern.compile(regexp) on every CEL evaluation, even when the regex pattern is a constant string literal. This is a significant performance bottleneck for libraries like protovalidate-java that evaluate the same regex patterns millions of times.
Problem
In RuntimeHelpers.java, the matches method recompiles the regex on every call:
publicstaticbooleanmatches(Stringstring, Stringregexp, CelOptionscelOptions) {
Patternpattern = Pattern.compile(regexp); // called every evaluation
...
}
In protovalidate, CEL expressions like input.matches('^0x[0-9a-f]{64}$') are compiled into programs that are cached and reused. But the regex pattern itself is recompiled from the string on every eval() call. This applies to both runtime paths (DefaultInterpreter and ProgramPlanner), since both funnel through RuntimeHelpers.matches().
Impact
Benchmarks against real-world proto validation patterns show 38-82% end-to-end improvement when patterns are cached:
Pattern
Unpatched
Patched
Improvement
Hex hash ^0x[0-9a-f]{64}$
4,683 ns
2,897 ns
38%
Blockchain address (alternation pattern)
13,868 ns
2,455 ns
82%
UTXO pattern ^[0-9a-f]{64}:[0-9]+$
16,038 ns
8,403 ns
48%
The cost of Pattern.compile() scales with pattern complexity — alternation patterns like ^(0x[0-9a-fA-F]{40}|[1-9A-HJ-NP-Za-km-z]{26,64}|bc1[0-9a-zA-Z]{25,87})$ cost ~11μs per compile.
Context: cel-go already solves this
cel-go has MatchesRegexOptimization in interpreter/optimizations.go which precompiles regex patterns at program creation time. protovalidate-go enables it via cel.OptOptimize. cel-java has no equivalent mechanism.
Suggested fix
A minimal fix — add a ConcurrentHashMap cache to RuntimeHelpers.matches():
This is a 3-line change. The cache is unbounded, which is safe because:
In practice, regex patterns in CEL come from compiled proto definitions (finite, small set)
If bounded eviction is desired, a Caffeine or LRU cache could replace ConcurrentHashMap
A more sophisticated approach (matching cel-go's MatchesRegexOptimization) would precompile at program creation time in ProgramPlanner.planCall(), but that only helps the planner runtime path and doesn't fix DefaultInterpreter.
No concrete timeline I can share of yet, but this is an upcoming enhancement for the Program Planner. We'll be introducing the means to cache precompiled regexes within the resulting program.
Activity
l46kok commentedon Apr 29, 2026
No concrete timeline I can share of yet, but this is an upcoming enhancement for the Program Planner. We'll be introducing the means to cache precompiled regexes within the resulting program.
Precompile constant regex patterns in ProgramPlanner
Precompile constant regex patterns in ProgramPlanner
Precompile constant regex patterns in ProgramPlanner
Precompile constant regex patterns in ProgramPlanner
Precompile constant regex patterns in ProgramPlanner
Precompile constant regex patterns in ProgramPlanner