Skip to content

Skip the now binding when no program references it#461

Open
snuderl wants to merge 1 commit intobufbuild:mainfrom
snuderl:skip-now-binding
Open

Skip the now binding when no program references it#461
snuderl wants to merge 1 commit intobufbuild:mainfrom
snuderl:skip-now-binding

Conversation

@snuderl
Copy link
Copy Markdown
Contributor

@snuderl snuderl commented Apr 26, 2026

Summary

Variable.newThisVariable always allocated three objects per field evaluation: a NowVariable, the this Variable, and a hierarchicalVariableResolver wrapping them. Almost no rule expression references the now identifier, so the wrapper layer is pure waste in the common case.

This PR walks each compiled CEL AST once at build time (via CelNavigableAst) and records whether it references now. CelPrograms aggregates that flag across its program list at construction; when nothing in the list needs now, Variable.newThisVariable returns a single bare Variable("this", val) instead of the three-object stack — three allocations become one per field eval.

The flag lives on CompiledProgram so future construction sites (e.g. user-defined cel rules) get the same treatment automatically.

Benchmark results

Steady-state validate() benchmarks, measured with a standalone ThreadMXBean-based runner (5 forks × 200K iterations each, after a 250K-iteration warmup) so we can report CPU time alongside wall time and confirm the savings are real CPU work, not just GC offload.

benchmark               wall_ns/op      cpu_ns/op   wall Δ   cpu Δ
validateSimple        1227.91 → 1193.03  1215.65 → 1178.89  -2.8%  -3.0%
validateManyUnruled    535.17 →  484.19   528.04 →  480.21  -9.5%  -9.1%
validateRepeatedRule 10643.32 → 10419.12 10574.64 → 10373.57 -2.1%  -1.9%
validateRegexPattern  1645.76 → 1582.47  1639.84 → 1579.18  -3.9%  -3.7%
validateNumericRange 17806.97 → 17206.64 17731.53 → 17137.54 -3.4%  -3.3%

cpu/wall ≈ 0.99 across the board (these benchmarks are fully CPU-bound), and the CPU delta tracks the wall delta within ~0.2pp. The largest relative win is on validateManyUnruled — schemas with many fields and few rules are common, and the per-field wrapper overhead is a bigger fraction of total work there.

Test plan

  • ./gradlew test — full unit + conformance suite passes.
  • Benchmarks above were run on JDK 21 against the same upstream main baseline.

Variable.newThisVariable always allocated three objects per field
evaluation (a NowVariable, the this Variable, and a hierarchical
wrapper). Almost no rule expression references the now identifier,
so the wrapping is pure waste in the common case.

Walk each compiled AST once at build time to record whether it
references now. CelPrograms aggregates that flag across its program
list and, when nothing needs now, asks Variable for a single
unwrapped resolver — three allocations become one per field eval.
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