Skip to content

Validate beans from cached per-group plans - #275

Open
jungm wants to merge 4 commits into
perf-3-allocationsfrom
perf-4-validation-plan
Open

jungm wants to merge 4 commits into
perf-3-allocationsfrom
perf-4-validation-plan

Conversation

@jungm

@jungm jungm commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Validates beans from plans cached per bean and group strategy, instead of working out what to validate on every call.

 BeanFrame.process(groups)
-  local = redefine Default for this bean
-  frames = read every constrained property
-  for each group in local
-    stream over all constraints, filter by group
+  plan = descriptor.getPlan(groups)                    # cached: local strategy + relevant properties
+  for each group in plan.localStrategy                 # single group: no applyTo callback
+    constraints = element.getConstraintsFor(group)    # cached per element and strategy
+    for each relevant property with work in this group
+      frame = created on first use                     # the property is read here

Relevant properties are those with constraints in scope, cascaded ones, and those with constrained container elements. Per-strategy caches first check up to eight already-seen strategy instances by identity (GroupStrategyMap), since validation keeps passing the same ones.

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
Jsr303Benchmark.bvalConstraintsSuccess (ops/s) 930,692 ± 50% 13,015,631 ± 35% 13.98× 3,032 → 432
mixedDrivers (ops/ms) 507 ± 62% 3,588 ± 4% 7.07× 4,332 → 1,041
explicitGroup (ops/ms) 703 ± 32% 4,462 ± 53% 6.34× 4,084 → 698
cascadedList (ops/ms) 0.29 ± 82% 1.57 ± 23% 5.42× 5,269,994 → 2,043,592
deepGraph (ops/ms) 97.0 ± 46% 345 ± 45% 3.56× 23,154 → 4,008
classLevelValid (ops/ms) 1,332 ± 11% 5,027 ± 34% 3.77× 2,592 → 568
All benchmarks
Benchmark Before After Change B/op before → after
cascadedCycles (ops/ms) 0.95 ± 66% 5.52 ± 74% 5.79× 1,098,127 → 443,567
cascadedList (ops/ms) 0.29 ± 82% 1.57 ± 23% 5.42× 5,269,994 → 2,043,592
classLevelInvalid (ops/ms) 729 ± 96% 2,780 ± 53% 3.81× 3,256 → 1,232
classLevelValid (ops/ms) 1,332 ± 11% 5,027 ± 34% 3.77× 2,592 → 568
composedInvalid (ops/ms) 1,192 ± 9% 2,016 ± 97% 1.69× 2,532 → 1,232
composedValid (ops/ms) 951 ± 38% 2,832 ± 83% 2.98× 2,152 → 800
containerElements (ops/ms) 1.92 ± 9% 4.99 ± 80% 2.59× 1,371,279 → 391,370
deepGraph (ops/ms) 97.0 ± 46% 345 ± 45% 3.56× 23,154 → 4,008
elMessage (ops/ms) 836 ± 45% 1,154 ± 44% 1.38× 3,412 → 2,088
executableParameters (ops/ms) 422 ± 87% 564 ± 52% 1.34× 5,423 → 3,689
executableReturnValue (ops/ms) 338 ± 46% 1,258 ± 60% 3.72× 5,460 → 1,817
explicitGroup (ops/ms) 703 ± 32% 4,462 ± 53% 6.34× 4,084 → 698
mixedDrivers (ops/ms) 507 ± 62% 3,588 ± 4% 7.07× 4,332 → 1,041
validateProperty (ops/ms) 1,615 ± 27% 2,983 ± 10% 1.85× 1,764 → 1,316
validateValue (ops/ms) 3,477 ± 10% 6,487 ± 9% 1.87× 1,088 → 648
wideBeanInvalid (ops/ms) 80.6 ± 150% 267 ± 11% 3.31× 20,600 → 9,568
wideBeanValid (ops/ms) 120 ± 68% 825 ± 10% 6.88× 13,737 → 2,816
Jsr303Benchmark.bvalBootstrapAndValidate (ops/s) 1,523 ± 43% 2,880 ± 56% 1.89× 251,542 → 247,317
Jsr303Benchmark.bvalBuildFactory (ops/s) 7,148 ± 34% 11,763 ± 3% 1.65× 88,164 → 88,397
Jsr303Benchmark.bvalConstraintsFailure (ops/s) 412,829 ± 89% 3,370,311 ± 10% 8.16× 4,236 → 1,352
Jsr303Benchmark.bvalConstraintsFailureReuseValidator (ops/s) 458,567 ± 38% 2,876,706 ± 16% 6.27× 4,248 → 1,376
Jsr303Benchmark.bvalConstraintsSuccess (ops/s) 930,692 ± 50% 13,015,631 ± 35% 13.98× 3,032 → 432
Jsr303Benchmark.bvalConstraintsSuccessReuseValidator (ops/s) 575,500 ± 101% 13,263,502 ± 19% 23.05× 2,900 → 432
Jsr303Benchmark.bvalNoConstraints (ops/s) 132,245,313 ± 93% 167,339,787 ± 11% 1.27× 0 → 0
Jsr303Benchmark.bvalNoConstraintsReuseValidator (ops/s) 160,105,673 ± 54% 187,581,916 ± 24% 1.17× 0 → 0
  • Tests: bval-jsr unit tests: 255 run, 0 failures · TCK: 1070 run, 0 failures, 1 skipped

Merge Danger

Door: two-way

Blast Radius: groups

  • Group sequences and Default group redefinition now go through cached plans; the TCK's group tests cover this.
  • Getters of properties with nothing to validate in the requested groups are no longer called. Code that relied on a getter's side effects during validation would notice.
  • The caches hold one entry per (descriptor, group strategy) that has been validated.

jungm added 4 commits October 5, 2026 13:32
Filter an element's constraints by the groups of a strategy once and
cache the result on the descriptor instead of streaming over all
constraints for every validation. The per-strategy caches first match
recently seen strategy instances by identity, as validation keeps
passing the same ones.
- Cache the Default group redefinition per bean descriptor and requested
  group strategy instead of computing it for every bean and property.
- Cache per bean and group strategy the properties that need a frame:
  those with constraints in scope, cascaded, or with constrained
  container elements. Properties with nothing to validate are no longer
  read.
- Precompute whether a container descriptor is cascaded deeply.
Build a bean's property frames on first use per group, and skip
properties without constraints in the current group, so that properties
relevant only to groups that are never validated (such as later steps
of a failing group sequence) are neither read nor resolved.
…oups directly

- Combine the Default group redefinition and the relevant properties of
  a bean for a requested group strategy into one cached plan.
- Validate a single-group strategy directly instead of through
  GroupStrategy#applyTo callbacks.
@jungm
jungm force-pushed the perf-4-validation-plan branch from ce9e63e to 245457c Compare October 5, 2026 11:42
@tandraschko

Copy link
Copy Markdown
Member

Otherwise +1
Really a great improvement

if (cached != null) {
return cached;
}
final GroupStrategy local = getLocalGroupStrategy(groups);

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.

I usally use computeIfAbsent in this case, saves a call to the method

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