Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d9138ec
Add benchmarks mirroring protovalidate-go for native-rules port baseline
jonbodner-buf Apr 30, 2026
40dc0ad
Add native-rules dispatcher infrastructure behind opt-in flag
jonbodner-buf Apr 30, 2026
00a4c21
Add native evaluator for bool.const
jonbodner-buf Apr 30, 2026
88a804c
Add native evaluator for the standard numeric rules
jonbodner-buf Apr 30, 2026
cd49558
Add native evaluator for enum const/in/not_in
jonbodner-buf Apr 30, 2026
d7662e6
Add native evaluator for the standard bytes rules
jonbodner-buf Apr 30, 2026
7e6dbfe
Add native evaluator for the standard string rules
jonbodner-buf Apr 30, 2026
a4e0568
Add native evaluators for repeated and map list/map-level rules
jonbodner-buf Apr 30, 2026
aa2840a
Add native-rules parity test and document the opt-in flag
jonbodner-buf Apr 30, 2026
a2bbd00
Add native wrapper unwrap for google.protobuf.{Bool,Int32,...}Value
jonbodner-buf Apr 30, 2026
d141626
Invert flag polarity from disableNativeRules to enableNativeRules
jonbodner-buf Apr 30, 2026
d865f59
Run conformance suite in both modes on every PR
jonbodner-buf Apr 30, 2026
3a7e5d4
Fix float comparator and formatter for ±0
jonbodner-buf May 1, 2026
58049a3
Address code-review action items in native rule evaluators
jonbodner-buf May 1, 2026
e1b394c
Pull violation-list helpers onto RuleBase, drop per-evaluator wrappers
jonbodner-buf May 1, 2026
0a6faec
Add unit tests for native rule coverage gaps
jonbodner-buf May 1, 2026
ad669ef
Add benchmarks for previously uncovered native rules
jonbodner-buf May 1, 2026
ea107a4
fix formatting and comments. Remove unused field. Add private constru…
jonbodner-buf May 4, 2026
b7fdc0f
default to native rules enabled. There are now two Builder methods, s…
jonbodner-buf May 4, 2026
9e45d6d
update the conformance workflow and the makefile to validate cel and …
jonbodner-buf May 4, 2026
ec8d659
fix validation message for string.not_contains
jonbodner-buf May 4, 2026
6374e26
remove the rules package, just put all the code in the protovalidate …
jonbodner-buf May 5, 2026
11c77c1
remove setDisableNativeRules and use a boolean flag on setEnableNativ…
jonbodner-buf May 5, 2026
a554ada
remove unneeded uses of Violation interface when RuleViolation will s…
jonbodner-buf May 5, 2026
7d7e3d9
minor code review issues addressed
jonbodner-buf May 5, 2026
e4f3463
fix numeric formatting and NaN comparisons. Native is now more correc…
jonbodner-buf May 5, 2026
5539a1a
fixed README
jonbodner-buf May 5, 2026
33f6c22
revert public API changes
jonbodner-buf May 5, 2026
da71616
address PR comments
jonbodner-buf May 5, 2026
819a9ce
update conformance workflow
jonbodner-buf May 5, 2026
2c66ede
fix header name regex
jonbodner-buf May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ clean: ## Delete intermediate build artifacts
$(GRADLE) clean

.PHONY: conformance
conformance: ## Execute conformance tests.
$(GRADLE) conformance:conformance
conformance: ## Execute conformance tests with native rule evaluators enabled and disabled.
ENABLE_NATIVE_RULES=true $(GRADLE) conformance:conformance
Comment thread
pkwarren marked this conversation as resolved.
ENABLE_NATIVE_RULES=false $(GRADLE) conformance:conformance

.PHONY: help
help: ## Describe useful make targets
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ Highlights for Java developers include:
* A comprehensive RPC quickstart for [Java and gRPC][grpc-java]
* A [migration guide for protoc-gen-validate][migration-guide] users

## Native rule evaluators (opt-out)

The standard rules can be evaluated either through CEL or through native Java code. Native evaluation is functionally identical (the conformance suite passes in both modes) but skips CEL compilation and runtime overhead for the rules it covers — a single `validate()` call on a complex message can run an order of magnitude faster and allocate ~10× less.

Native rules are **opt-out**. Disable them by configuring the validator:

```java
Config config = Config.newBuilder().setEnableNativeRules(false).build();
Validator validator = ValidatorFactory.newBuilder().withConfig(config).build();
```

Forward compatibility is preserved by a clone-and-clear contract: when protovalidate adds a new rule that this codebase hasn't yet implemented natively, the rule remains on the residual `FieldRules` and CEL enforces it. Native evaluation is an optimization, never a replacement.

## Additional languages and repositories

Protovalidate isn't just for Java! You might be interested in sibling repositories for other languages:
Expand Down
20 changes: 20 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ compileValidatorForRepeated alloc 12950196.95 B/op 3262651.61 B/op -74.8%
`jmhCompare` diffs `results-before.json` against `results.json` by default.
Pass explicit paths with `-Pbefore=<path> -Pafter=<path>`.

## Comparing native rules vs CEL

Benchmarks A/B the `enableNativeRules` flag via `@Param({"false", "true"})`, so a single run produces both variants.
Diff them in place:

```
./gradlew :benchmarks:jmh
./gradlew :benchmarks:jmhCompareNativeRules
```

Output (`before` = CEL, `after` = native; negative delta means native is faster / allocates less):

```
benchmark metric cel native delta
buildBenchInt32GT time 1234567.89 ns/op 456789.01 ns/op -63.0%
buildBenchInt32GT alloc 123456.78 B/op 45678.90 B/op -63.0%
```

Override the input file with `-Presults=<path>`.

## Adding a new benchmark

Benchmarks live in `src/jmh/java/...` and target proto messages in `src/jmh/proto/...`.
Expand Down
23 changes: 23 additions & 0 deletions benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,26 @@ tasks.register<Exec>("jmhCompare") {
after, // $3
)
}

// Diffs the two enableNativeRules variants within a single results.json.
// `before` column is CEL (enableNativeRules=false), `after` is native
// (enableNativeRules=true), so a negative delta means native is faster /
// allocates less.
//
// Override the input file:
// ./gradlew :benchmarks:jmhCompareNative -Presults=path/to/results.json
tasks.register<Exec>("jmhCompareNativeRules") {
description = "Diffs enableNativeRules=true vs false from a single JMH results.json."
val results =
project.findProperty("results")?.toString()
?: jmhResults.get().asFile.absolutePath
val jqScript = file("jmh-compare-native-rules.jq").absolutePath
commandLine(
"bash",
"-c",
"jq --raw-output --from-file \"\$1\" \"\$2\" | column -t -s \$'\\t'",
"jmh-compare-native-rules", // $0
jqScript, // $1
results, // $2
)
}
32 changes: 32 additions & 0 deletions benchmarks/jmh-compare-native-rules.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# this script builds a comparison between runs that used the CEL interpreter for
# protovalidate rule evaluation vs runs that used native Java code for protovalidate
# rule evaluation. The differentiator is the value of the "native" field (true for
# native, false for CEL) and this script groups rows that have the same benchmark
# and metric name, but different values for native.
def pct(a; b):
Comment thread
pkwarren marked this conversation as resolved.
if a == null or b == null or b == 0 then "~"
else (((a - b) / b * 100) * 10 | round / 10) as $d
| if $d > 0 then "+\($d)%" elif $d == 0 then "~" else "\($d)%" end
end;
def num(x):
if x == null then "-"
else (x * 100 | round / 100 | tostring)
end;

def row: {
key: (.benchmark | split(".") | last),
native: .params.enableNativeRules,
time: .primaryMetric.score,
unit: .primaryMetric.scoreUnit,
alloc: (.secondaryMetrics["·gc.alloc.rate.norm"].score // null)
};

map(row)
| group_by(.key)
| (["benchmark", "metric", "cel", "native", "delta"] | @tsv),
(.[]
| (map(select(.native == "false"))[0]) as $b
| (map(select(.native == "true"))[0]) as $a
| select($b and $a)
| ([$b.key, "time", "\(num($b.time)) \($b.unit)", "\(num($a.time)) \($a.unit)", pct($a.time; $b.time)] | @tsv),
([$b.key, "alloc", "\(num($b.alloc)) B/op", "\(num($a.alloc)) B/op", pct($a.alloc; $b.alloc)] | @tsv))
Loading
Loading