From c8a5ed9b9c1cf7dcee7776aa715cf541da8c2932 Mon Sep 17 00:00:00 2001 From: Blaz Snuderl Date: Sun, 26 Apr 2026 12:41:23 +0200 Subject: [PATCH 1/2] Skip tautological compiled programs in RuleCache Mirrors protovalidate-go's ReduceResiduals: when a rule program evaluates to true (or empty string) using only the bound rules/rule variables, the expression is tautological and can be eliminated at compile time so it never runs during validation. Programs that reference unbound variables (this, now) raise CelEvaluationException and are kept as before. --- .../build/buf/protovalidate/RuleCache.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/build/buf/protovalidate/RuleCache.java b/src/main/java/build/buf/protovalidate/RuleCache.java index 9e6962bf..1e7d6610 100644 --- a/src/main/java/build/buf/protovalidate/RuleCache.java +++ b/src/main/java/build/buf/protovalidate/RuleCache.java @@ -31,6 +31,7 @@ import dev.cel.common.types.StructTypeReference; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime.Program; +import dev.cel.runtime.CelVariableResolver; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -123,17 +124,41 @@ List compile( List programs = new ArrayList<>(); for (CelRule rule : completeProgramList) { Object fieldValue = message.getField(rule.field); + CelVariableResolver ruleResolver = + Variable.newRuleVariable(message, ProtoAdapter.toCel(rule.field, fieldValue)); + if (isTautological(rule.program, ruleResolver)) { + continue; + } programs.add( new CompiledProgram( rule.program, rule.astExpression.source, rule.rulePath, new ObjectValue(rule.field, fieldValue), - Variable.newRuleVariable(message, ProtoAdapter.toCel(rule.field, fieldValue)))); + ruleResolver)); } return Collections.unmodifiableList(programs); } + /** + * Evaluates the program with only rules/rule bound. If it resolves to true or "", the expression + * is tautological and can be eliminated (mirrors protovalidate-go's ReduceResiduals). + */ + private static boolean isTautological(Program program, CelVariableResolver ruleResolver) { + try { + Object result = program.eval(ruleResolver); + if (result instanceof Boolean) { + return (Boolean) result; + } + if (result instanceof String) { + return ((String) result).isEmpty(); + } + } catch (CelEvaluationException e) { + // Expression depends on unbound variables (this, now) — not tautological. + } + return false; + } + private @Nullable List compileRule( FieldDescriptor fieldDescriptor, boolean forItems, From 37c1516307b5b2d28776407ed552f8503c65e978 Mon Sep 17 00:00:00 2001 From: Blaz Snuderl Date: Sun, 26 Apr 2026 12:49:03 +0200 Subject: [PATCH 2/2] Add NumericRangeMessage benchmark for tautological-rule skip Adds a 20-field float message with only 'gt' set on each field. The standard float.gt rule expands into five CEL programs; four of them short-circuit to '' using only the bound rules variable when neither 'lt' nor 'lte' is set. This benchmark targets the residual-reduction skip in RuleCache. --- .../benchmarks/ValidationBenchmark.java | 14 +++++++++ benchmarks/src/jmh/proto/bench/v1/bench.proto | 29 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/ValidationBenchmark.java b/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/ValidationBenchmark.java index d058b20b..85608a45 100644 --- a/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/ValidationBenchmark.java +++ b/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/ValidationBenchmark.java @@ -17,6 +17,7 @@ import build.buf.protovalidate.Validator; import build.buf.protovalidate.ValidatorFactory; import build.buf.protovalidate.benchmarks.gen.ManyUnruledFieldsMessage; +import build.buf.protovalidate.benchmarks.gen.NumericRangeMessage; import build.buf.protovalidate.benchmarks.gen.RegexPatternMessage; import build.buf.protovalidate.benchmarks.gen.RepeatedRuleMessage; import build.buf.protovalidate.benchmarks.gen.SimpleStringMessage; @@ -42,6 +43,7 @@ public class ValidationBenchmark { private ManyUnruledFieldsMessage manyUnruled; private RepeatedRuleMessage repeatedRule; private RegexPatternMessage regexPattern; + private NumericRangeMessage numericRange; @Setup public void setup() throws ValidationException { @@ -71,11 +73,18 @@ public void setup() throws ValidationException { regexPattern = RegexPatternMessage.newBuilder().setName("Alice Example").build(); + NumericRangeMessage.Builder numericRangeBuilder = NumericRangeMessage.newBuilder(); + for (FieldDescriptor fd : NumericRangeMessage.getDescriptor().getFields()) { + numericRangeBuilder.setField(fd, 1.0f); + } + numericRange = numericRangeBuilder.build(); + // Warm evaluator cache for steady-state benchmarks. validator.validate(simple); validator.validate(manyUnruled); validator.validate(repeatedRule); validator.validate(regexPattern); + validator.validate(numericRange); } // Steady-state validate() benchmarks. These exercise the hot path after the @@ -100,4 +109,9 @@ public void validateRepeatedRule(Blackhole bh) throws ValidationException { public void validateRegexPattern(Blackhole bh) throws ValidationException { bh.consume(validator.validate(regexPattern)); } + + @Benchmark + public void validateNumericRange(Blackhole bh) throws ValidationException { + bh.consume(validator.validate(numericRange)); + } } diff --git a/benchmarks/src/jmh/proto/bench/v1/bench.proto b/benchmarks/src/jmh/proto/bench/v1/bench.proto index d6fde070..dc1b6617 100644 --- a/benchmarks/src/jmh/proto/bench/v1/bench.proto +++ b/benchmarks/src/jmh/proto/bench/v1/bench.proto @@ -79,3 +79,32 @@ message RegexPatternMessage { max_bytes: 256 }]; } + +// Twenty float fields, each with only `gt` set. The standard float.gt rule +// expands into five CEL programs (gt, gt_lt, gt_lt_exclusive, gt_lte, +// gt_lte_exclusive); when neither `lt` nor `lte` is set, four of those +// short-circuit to the empty string using only the bound `rules` variable. +// Targets the residual-reduction skip in RuleCache: those four programs +// can be eliminated at compile time so they never run during validate(). +message NumericRangeMessage { + float f01 = 1 [(buf.validate.field).float.gt = 0]; + float f02 = 2 [(buf.validate.field).float.gt = 0]; + float f03 = 3 [(buf.validate.field).float.gt = 0]; + float f04 = 4 [(buf.validate.field).float.gt = 0]; + float f05 = 5 [(buf.validate.field).float.gt = 0]; + float f06 = 6 [(buf.validate.field).float.gt = 0]; + float f07 = 7 [(buf.validate.field).float.gt = 0]; + float f08 = 8 [(buf.validate.field).float.gt = 0]; + float f09 = 9 [(buf.validate.field).float.gt = 0]; + float f10 = 10 [(buf.validate.field).float.gt = 0]; + float f11 = 11 [(buf.validate.field).float.gt = 0]; + float f12 = 12 [(buf.validate.field).float.gt = 0]; + float f13 = 13 [(buf.validate.field).float.gt = 0]; + float f14 = 14 [(buf.validate.field).float.gt = 0]; + float f15 = 15 [(buf.validate.field).float.gt = 0]; + float f16 = 16 [(buf.validate.field).float.gt = 0]; + float f17 = 17 [(buf.validate.field).float.gt = 0]; + float f18 = 18 [(buf.validate.field).float.gt = 0]; + float f19 = 19 [(buf.validate.field).float.gt = 0]; + float f20 = 20 [(buf.validate.field).float.gt = 0]; +}