Skip to content

Speed up EL messages, @Email and message interpolation - #272

Open
jungm wants to merge 4 commits into
perf-0-benchmarksfrom
perf-1-messages
Open

jungm wants to merge 4 commits into
perf-0-benchmarksfrom
perf-1-messages

Conversation

@jungm

@jungm jungm commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Removes the cost of the built-in constraints and of message interpolation, which dominate any validation that produces violations.

 DefaultMessageInterpolator.interpolate(template, context)
+  if cached for (locale, template, attribute map identity)
+    return cached
   resolve bundle keys and {attribute} references
+  if no "${" remains
+    cache and return
   ELFacade.interpolate
-    look-behind regex (100+ alternations) to disarm unescaped "#{"
+    scan for "#{" and count the preceding backslashes
 ConstraintD.getMessageTemplate
-  read reflectively from the annotation for every violation
+  memoized
 EmailValidator.isValid
-  DEFAULT_EMAIL_PATTERN.matcher(value).matches()
+  hand-written matcher, same language as DEFAULT_EMAIL_PATTERN
 AbstractPatternValidator.isValid
-  pattern.matcher(value).matches()
+  regexp ".*" without flags: no line terminator in value

Evidence

  • Before / After: benchmarks

Numbers are JMH throughput (-p provider=bval -f 2 -wi 2 -i 3, 1 s iterations, -prof gc, JDK 25), measured against the PR below this one. The machine was busy, so throughput carries wide error bars; the bytes allocated per operation (B/op) are deterministic.

Benchmark Before After Change B/op before → after
elMessage (ops/ms) 51.6 ± 9% 696 ± 15% 13.49× 4,403 → 4,196
wideBeanInvalid (ops/ms) 12.4 ± 1% 59.2 ± 30% 4.76× 34,109 → 31,759
composedInvalid (ops/ms) 341 ± 8% 502 ± 2% 1.47× 6,092 → 5,652
All benchmarks
Benchmark Before After Change B/op before → after
cascadedCycles (ops/ms) 2.29 ± 43% 2.30 ± 12% 1.00× 1,217,820 → 1,259,572
cascadedList (ops/ms) 0.48 ± 23% 0.47 ± 15% 1.00× 5,862,671 → 6,110,831
classLevelInvalid (ops/ms) 668 ± 16% 683 ± 21% 1.02× 4,264 → 4,416
classLevelValid (ops/ms) 1,004 ± 7% 1,011 ± 16% 1.01× 3,224 → 3,248
composedInvalid (ops/ms) 341 ± 8% 502 ± 2% 1.47× 6,092 → 5,652
composedValid (ops/ms) 838 ± 9% 811 ± 13% 0.97× 3,240 → 3,172
containerElements (ops/ms) 1.48 ± 15% 1.66 ± 7% 1.12× 1,831,091 → 1,681,221
deepGraph (ops/ms) 102 ± 8% 90.9 ± 10% 0.89× 31,984 → 32,275
elMessage (ops/ms) 51.6 ± 9% 696 ± 15% 13.49× 4,403 → 4,196
executableParameters (ops/ms) 422 ± 5% 424 ± 21% 1.00× 6,296 → 6,211
executableReturnValue (ops/ms) 386 ± 9% 388 ± 19% 1.00× 6,950 → 6,841
explicitGroup (ops/ms) 472 ± 10% 577 ± 6% 1.22× 6,589 → 5,757
mixedDrivers (ops/ms) 487 ± 9% 353 ± 70% 0.73× 5,833 → 5,848
validateProperty (ops/ms) 1,361 ± 8% 1,010 ± 64% 0.74× 2,076 → 2,027
validateValue (ops/ms) 2,224 ± 5% 2,109 ± 40% 0.95× 1,352 → 1,325
wideBeanInvalid (ops/ms) 12.4 ± 1% 59.2 ± 30% 4.76× 34,109 → 31,759
wideBeanValid (ops/ms) 146 ± 15% 134 ± 41% 0.92× 18,688 → 17,553
Jsr303Benchmark.bvalBootstrapAndValidate (ops/s) 2,845 ± 43% 2,844 ± 48% 1.00× 245,360 → 245,596
Jsr303Benchmark.bvalBuildFactory (ops/s) 11,297 ± 5% 11,275 ± 3% 1.00× 88,057 → 87,993
Jsr303Benchmark.bvalConstraintsFailure (ops/s) 500,080 ± 9% 590,674 ± 16% 1.18× 5,648 → 5,520
Jsr303Benchmark.bvalConstraintsFailureReuseValidator (ops/s) 499,671 ± 11% 607,050 ± 8% 1.21× 5,588 → 5,520
Jsr303Benchmark.bvalConstraintsSuccess (ops/s) 1,213,713 ± 9% 1,001,414 ± 37% 0.83× 3,224 → 3,212
Jsr303Benchmark.bvalConstraintsSuccessReuseValidator (ops/s) 1,120,020 ± 28% 936,769 ± 49% 0.84× 3,212 → 3,248
Jsr303Benchmark.bvalNoConstraints (ops/s) 100,611,363 ± 21% 71,836,297 ± 29% 0.71× 0 → 0
Jsr303Benchmark.bvalNoConstraintsReuseValidator (ops/s) 119,052,819 ± 24% 119,638,823 ± 13% 1.00× 0 → 0
  • Tests:

    ELFacadeTest           interpolate("${'a'} #{x} b") == "a $0x} b"       # outputs captured from master's regex
                           interpolate("${'a'} \#{x} b") == "a #{x} b"
    EMailValidationUtilsTest isValid(s) == isValid(s, DEFAULT_EMAIL_PATTERN)  # 19 examples + 100,000 random strings
    

    bval-jsr unit tests: 255 run, 0 failures · TCK: 1070 run, 0 failures, 1 skipped

Merge Danger

Door: two-way

Blast Radius: messages

  • EL messages: a mistake in the scan would change interpolated text containing #{. The test pins master's exact outputs, including escaped forms.
  • @Email without a custom regexp: the matcher must accept exactly what DEFAULT_EMAIL_PATTERN does. It's fuzzed against the regex.
  • The interpolated-message cache holds one entry per (locale, template, constraint) that needs no EL, so it is bounded by the number of constraints. clearCache() clears it.

@jungm
jungm added this pull request to stack #278 October 5, 2026 11:20
jungm added 4 commits October 5, 2026 13:31
ELFacade ran a look-behind regular expression with over a hundred
alternations across every message containing "${" to disarm unescaped
"#{" sequences. It dominated the cost of any violation whose message
uses EL, including several built-in default messages. A direct scan
gives the same result and returns immediately when there is no "#{".
A hand-written matcher equivalent to DEFAULT_EMAIL_PATTERN replaces
running the regular expression for @Email without a custom regexp.
…engine

Without flags, .* matches exactly the values that contain no line
terminator, so look for those instead of running the pattern.
- Cache fully interpolated messages that contain no EL expression, as
  they do not depend on the validated value.
- Key interpolation caches on the identity of the constraint's attribute
  map, which descriptors hand out as a stable instance, instead of
  hashing and comparing the whole map.
- Memoize the constraint message template instead of reading it
  reflectively from the annotation for every violation.
resolvedMessage = replaceAnnotationAttributes(resolvedMessage, annotationParameters);

// EL handling
// EL handling; the evaluator leaves messages without "${" untouched

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about #{ here?

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.

2 participants