Skip to content

Commit 9bcd060

Browse files
authored
Follow up fixes to RuleCache (#458)
Switch from a global to a per-instance descriptor map now that we're caching the program. Make a few small improvements to the rule cache. Update benchmark to better represent intended use of a validator (creating one instance and re-using it across evaluations).
1 parent 6e4bb6f commit 9bcd060

3 files changed

Lines changed: 64 additions & 30 deletions

File tree

benchmarks/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import com.diffplug.gradle.spotless.SpotlessExtension
2+
13
plugins {
24
java
35
alias(libs.plugins.jmh)
@@ -10,6 +12,12 @@ java {
1012
targetCompatibility = JavaVersion.VERSION_21
1113
}
1214

15+
configure<SpotlessExtension> {
16+
java {
17+
targetExclude("build/generated/**/*.java")
18+
}
19+
}
20+
1321
val buf: Configuration by configurations.creating
1422

1523
tasks.register("configureBuf") {

benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/ValidationBenchmark.java

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@
1919
import build.buf.protovalidate.benchmarks.gen.ManyUnruledFieldsMessage;
2020
import build.buf.protovalidate.benchmarks.gen.RepeatedRuleMessage;
2121
import build.buf.protovalidate.benchmarks.gen.SimpleStringMessage;
22-
import build.buf.protovalidate.exceptions.CompilationException;
2322
import build.buf.protovalidate.exceptions.ValidationException;
24-
import com.google.protobuf.Descriptors.Descriptor;
25-
import java.util.Collections;
23+
import com.google.protobuf.Descriptors.FieldDescriptor;
2624
import java.util.concurrent.TimeUnit;
2725
import org.openjdk.jmh.annotations.Benchmark;
2826
import org.openjdk.jmh.annotations.BenchmarkMode;
@@ -41,9 +39,7 @@ public class ValidationBenchmark {
4139
private Validator validator;
4240
private SimpleStringMessage simple;
4341
private ManyUnruledFieldsMessage manyUnruled;
44-
45-
// Descriptor captured once; cheap to reference during benchmark.
46-
private static final Descriptor REPEATED_RULE_DESC = RepeatedRuleMessage.getDescriptor();
42+
private RepeatedRuleMessage repeatedRule;
4743

4844
@Setup
4945
public void setup() throws ValidationException {
@@ -65,13 +61,20 @@ public void setup() throws ValidationException {
6561
.setF9("v9")
6662
.build();
6763

64+
RepeatedRuleMessage.Builder repeatedRuleBuilder = RepeatedRuleMessage.newBuilder();
65+
for (FieldDescriptor fd : RepeatedRuleMessage.getDescriptor().getFields()) {
66+
repeatedRuleBuilder.setField(fd, "v");
67+
}
68+
repeatedRule = repeatedRuleBuilder.build();
69+
6870
// Warm evaluator cache for steady-state benchmarks.
6971
validator.validate(simple);
7072
validator.validate(manyUnruled);
73+
validator.validate(repeatedRule);
7174
}
7275

7376
// Steady-state validate() benchmarks. These exercise the hot path after the
74-
// evaluator cache is warm. PR #451 does not affect this path.
77+
// evaluator cache is warm.
7578

7679
@Benchmark
7780
public void validateSimple(Blackhole bh) throws ValidationException {
@@ -83,19 +86,8 @@ public void validateManyUnruled(Blackhole bh) throws ValidationException {
8386
bh.consume(validator.validate(manyUnruled));
8487
}
8588

86-
// Compile-path benchmark. Measures building a fresh validator and warming
87-
// its RuleCache for RepeatedRuleMessage (20 string fields, all min_len).
88-
// PR #451 affects exactly this path: without the fix, the AST is rebuilt
89-
// for every field; with the fix, fields 2..20 hit the cache.
90-
//
91-
// Time is dominated by Cel environment construction in newCel(); the #451
92-
// signal is the delta on top of that baseline.
9389
@Benchmark
94-
@OutputTimeUnit(TimeUnit.MILLISECONDS)
95-
public void compileValidatorForRepeated(Blackhole bh) throws CompilationException {
96-
Validator v =
97-
ValidatorFactory.newBuilder()
98-
.buildWithDescriptors(Collections.singletonList(REPEATED_RULE_DESC), false);
99-
bh.consume(v);
90+
public void validateRepeatedRule(Blackhole bh) throws ValidationException {
91+
bh.consume(validator.validate(repeatedRule));
10092
}
10193
}

src/main/java/build/buf/protovalidate/RuleCache.java

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,10 @@ private CelRule(
6262
}
6363

6464
/**
65-
* Concurrent map for caching {@link FieldDescriptor} and their associated List of {@link
66-
* AstExpression}.
65+
* Compiled rules keyed by rule field descriptor (e.g. {@code StringRules.min_len}), shared across
66+
* all user fields that reference the same rule. The rule value is bound per call at eval time.
6767
*/
68-
private static final Map<FieldDescriptor, List<CelRule>> descriptorMap =
69-
new ConcurrentHashMap<>();
68+
private final Map<FieldDescriptor, List<CelRule>> descriptorMap = new ConcurrentHashMap<>();
7069

7170
/** The environment to use for evaluation. */
7271
private final Cel cel;
@@ -123,14 +122,14 @@ List<CompiledProgram> compile(
123122
}
124123
List<CompiledProgram> programs = new ArrayList<>();
125124
for (CelRule rule : completeProgramList) {
125+
Object fieldValue = message.getField(rule.field);
126126
programs.add(
127127
new CompiledProgram(
128128
rule.program,
129129
rule.astExpression.source,
130130
rule.rulePath,
131-
new ObjectValue(rule.field, message.getField(rule.field)),
132-
Variable.newRuleVariable(
133-
message, ProtoAdapter.toCel(rule.field, message.getField(rule.field)))));
131+
new ObjectValue(rule.field, fieldValue),
132+
Variable.newRuleVariable(message, ProtoAdapter.toCel(rule.field, fieldValue))));
134133
}
135134
return Collections.unmodifiableList(programs);
136135
}
@@ -148,8 +147,31 @@ List<CompiledProgram> compile(
148147
}
149148
build.buf.validate.PredefinedRules rules = getFieldRules(ruleFieldDesc);
150149
if (rules == null) return null;
150+
try {
151+
return descriptorMap.computeIfAbsent(
152+
ruleFieldDesc,
153+
key -> {
154+
try {
155+
return buildCelRules(fieldDescriptor, forItems, setOneof, key, message, rules);
156+
} catch (CompilationException e) {
157+
throw new UncheckedCompilationException(e);
158+
}
159+
});
160+
} catch (UncheckedCompilationException e) {
161+
throw e.getCompilationException();
162+
}
163+
}
164+
165+
private List<CelRule> buildCelRules(
166+
FieldDescriptor fieldDescriptor,
167+
boolean forItems,
168+
FieldDescriptor setOneof,
169+
FieldDescriptor ruleFieldDesc,
170+
Message message,
171+
build.buf.validate.PredefinedRules rules)
172+
throws CompilationException {
151173
List<Expression> expressions = Expression.fromRules(rules.getCelList());
152-
celRules = new ArrayList<>(expressions.size());
174+
List<CelRule> celRules = new ArrayList<>(expressions.size());
153175
Cel ruleCel = getRuleCel(fieldDescriptor, message, ruleFieldDesc, forItems);
154176
for (Expression expression : expressions) {
155177
FieldPath rulePath =
@@ -167,10 +189,22 @@ List<CompiledProgram> compile(
167189
}
168190
celRules.add(new CelRule(astExpression, program, ruleFieldDesc, rulePath));
169191
}
170-
descriptorMap.put(ruleFieldDesc, celRules);
171192
return celRules;
172193
}
173194

195+
private static final class UncheckedCompilationException extends RuntimeException {
196+
private final CompilationException compilationException;
197+
198+
UncheckedCompilationException(CompilationException cause) {
199+
super(cause);
200+
this.compilationException = cause;
201+
}
202+
203+
CompilationException getCompilationException() {
204+
return compilationException;
205+
}
206+
}
207+
174208
private build.buf.validate.@Nullable PredefinedRules getFieldRules(FieldDescriptor ruleFieldDesc)
175209
throws CompilationException {
176210
DescriptorProtos.FieldOptions options = ruleFieldDesc.getOptions();
@@ -310,7 +344,7 @@ private ResolvedRule resolveRules(
310344
DynamicMessage.parseFrom(
311345
expectedRuleMessageDescriptor, typeRules.toByteString(), extensionRegistry);
312346
} catch (InvalidProtocolBufferException e) {
313-
throw new RuntimeException(e);
347+
throw new CompilationException("failed to reparse rules with extension registry", e);
314348
}
315349
}
316350
if (!allowUnknownFields && !typeRules.getUnknownFields().isEmpty()) {

0 commit comments

Comments
 (0)