Skip to content

Commit d49e200

Browse files
authored
Merge pull request #539 from micronaut-projects/optimize
Validator optimizations
2 parents 1be55ba + 8146572 commit d49e200

File tree

9 files changed

+327
-141
lines changed

9 files changed

+327
-141
lines changed

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,6 @@ gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.
5757
gradle-kotlin-allopen = { module = "org.jetbrains.kotlin:kotlin-allopen", version.ref = "kotlin-gradle-plugin" }
5858
gradle-kotlin-noarg = { module = "org.jetbrains.kotlin:kotlin-noarg", version.ref = "kotlin-gradle-plugin" }
5959
gradle-ksp = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp-gradle-plugin" }
60+
61+
[plugins]
62+
jmh = { id = "me.champeau.jmh", version = "0.7.3" }

validation/build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
plugins {
22
id "io.micronaut.build.internal.validation-module"
3+
alias(libs.plugins.jmh)
34
}
45

56
dependencies {
@@ -33,6 +34,10 @@ dependencies {
3334

3435
testImplementation(mnTest.micronaut.test.junit5)
3536
testRuntimeOnly(libs.junit.jupiter.engine)
37+
38+
jmhAnnotationProcessor("org.openjdk.jmh:jmh-generator-annprocess:1.36")
39+
jmhAnnotationProcessor(projects.micronautValidationProcessor)
40+
jmhAnnotationProcessor(mn.micronaut.inject.java)
3641
}
3742

3843
spotless {
@@ -41,3 +46,7 @@ spotless {
4146
}
4247
}
4348

49+
tasks.getByName("checkstyleJmh").configure {
50+
enabled = false
51+
}
52+
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.micronaut.validation;
2+
3+
import io.micronaut.context.ApplicationContext;
4+
import io.micronaut.context.annotation.Requires;
5+
import jakarta.inject.Singleton;
6+
import jakarta.validation.constraints.NotBlank;
7+
import org.openjdk.jmh.annotations.Benchmark;
8+
import org.openjdk.jmh.annotations.BenchmarkMode;
9+
import org.openjdk.jmh.annotations.Fork;
10+
import org.openjdk.jmh.annotations.Measurement;
11+
import org.openjdk.jmh.annotations.Mode;
12+
import org.openjdk.jmh.annotations.OutputTimeUnit;
13+
import org.openjdk.jmh.annotations.Scope;
14+
import org.openjdk.jmh.annotations.Setup;
15+
import org.openjdk.jmh.annotations.State;
16+
import org.openjdk.jmh.annotations.TearDown;
17+
import org.openjdk.jmh.annotations.Warmup;
18+
import org.openjdk.jmh.infra.Blackhole;
19+
import org.openjdk.jmh.runner.Runner;
20+
import org.openjdk.jmh.runner.RunnerException;
21+
import org.openjdk.jmh.runner.options.Options;
22+
import org.openjdk.jmh.runner.options.OptionsBuilder;
23+
24+
import java.util.Map;
25+
import java.util.concurrent.TimeUnit;
26+
27+
@Fork(1)
28+
@Warmup(iterations = 10, time = 3, timeUnit = TimeUnit.SECONDS)
29+
@Measurement(iterations = 10, time = 3, timeUnit = TimeUnit.SECONDS)
30+
@BenchmarkMode(Mode.AverageTime)
31+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
32+
@State(Scope.Benchmark)
33+
public class ParameterBenchmark {
34+
private ApplicationContext ctx;
35+
private MyBean bean;
36+
37+
public static void main(String[] args) throws RunnerException {
38+
ParameterBenchmark test = new ParameterBenchmark();
39+
test.init();
40+
try {
41+
test.string(new Blackhole("Today's password is swordfish. I understand instantiating Blackholes directly is dangerous."));
42+
} finally {
43+
test.destroy();
44+
}
45+
46+
Options opt = new OptionsBuilder()
47+
.include(".*" + ParameterBenchmark.class.getSimpleName() + ".*")
48+
//.addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-4.1-linux-x64/lib/libasyncProfiler.so;output=flamegraph")
49+
.build();
50+
51+
new Runner(opt).run();
52+
}
53+
54+
@Setup
55+
public void init() {
56+
ctx = ApplicationContext.run(Map.of("spec.name", "ParameterBenchmark"));
57+
bean = ctx.getBean(MyBean.class);
58+
}
59+
60+
@TearDown
61+
public void destroy() {
62+
ctx.close();
63+
}
64+
65+
@Benchmark
66+
public void string(Blackhole blackhole) {
67+
bean.string(blackhole, "foo");
68+
}
69+
70+
@Singleton
71+
@Requires(property = "spec.name", value = "ParameterBenchmark")
72+
static class MyBean {
73+
void string(Blackhole blackhole, @NotBlank String string) {
74+
blackhole.consume(string);
75+
}
76+
}
77+
}

validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintValidatorContext.java

Lines changed: 99 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
import java.util.Set;
4949
import java.util.concurrent.ConcurrentHashMap;
5050
import java.util.stream.Collectors;
51-
import java.util.stream.Stream;
5251

5352
/**
5453
* The implementation of {@link ConstraintValidatorContext}.
@@ -83,7 +82,7 @@ public final class DefaultConstraintValidatorContext<R> implements ConstraintVal
8382
private Object executableReturnValue;
8483
private List<Class<?>> currentGroups;
8584
private Map<Class<?>, Class<?>> convertedGroups = Collections.emptyMap();
86-
private Set<ConstraintViolation<R>> currentViolations = new LinkedHashSet<>();
85+
private boolean hasCurrentViolations = false;
8786

8887
DefaultConstraintValidatorContext(DefaultValidator defaultValidator, BeanIntrospection<R> beanIntrospection, R rootBean, BeanValidationContext validationContext) {
8988
this(defaultValidator, beanIntrospection, validationContext, rootBean, null, new ValidationPath(), new LinkedHashSet<>(), null, Collections.emptyList());
@@ -144,15 +143,20 @@ private static void sanityCheckGroups(List<Class<?>> groups) {
144143
}
145144
}
146145

147-
public boolean hasDefaultGroup() {
146+
private static boolean hasDefaultGroup(List<Class<?>> definedGroups) {
148147
return definedGroups.equals(DEFAULT_GROUPS);
149148
}
150149

151150
public boolean containsGroup(Collection<Class<?>> constraintGroups) {
152151
if (currentGroups.contains(Default.class) && rootClass != null && constraintGroups.contains(rootClass)) {
153152
return true;
154153
}
155-
return currentGroups.stream().anyMatch(constraintGroups::contains);
154+
for (Class<?> group : currentGroups) {
155+
if (constraintGroups.contains(group)) {
156+
return true;
157+
}
158+
}
159+
return false;
156160
}
157161

158162
public Object[] getExecutableParameterValues() {
@@ -186,9 +190,9 @@ public ValidationCloseable withExecutableReturnValue(Object executableReturnValu
186190

187191
public GroupsValidation withGroupSequence(@NonNull ValidationGroup validationGroup) {
188192
List<Class<?>> prevGroups = currentGroups;
189-
Set<ConstraintViolation<R>> prevViolations = currentViolations;
193+
boolean prevViolations = hasCurrentViolations;
190194
currentGroups = validationGroup.groups();
191-
currentViolations = new LinkedHashSet<>();
195+
hasCurrentViolations = false;
192196

193197
return new GroupsValidation() {
194198

@@ -200,13 +204,13 @@ public boolean isFailed() {
200204
if (validationGroup.isRedefinedDefaultGroupSequence()) {
201205
return !overallViolations.isEmpty();
202206
}
203-
return !currentViolations.isEmpty();
207+
return hasCurrentViolations;
204208
}
205209

206210
@Override
207211
public void close() {
208212
currentGroups = prevGroups;
209-
currentViolations = prevViolations;
213+
hasCurrentViolations = prevViolations;
210214
}
211215
};
212216
}
@@ -226,87 +230,128 @@ public ValidationCloseable convertGroups(@NonNull AnnotationMetadata annotationM
226230
av -> av.classValue("to").orElseThrow())
227231
);
228232
convertedGroups.putAll(newConvertGroups);
229-
currentGroups = prevGroups.stream().<Class<?>>map(this::convertGroup).toList();
233+
currentGroups = prevGroups.stream().<Class<?>>map(c -> convertGroup(convertedGroups, c)).toList();
230234
return () -> {
231235
convertedGroups = prevConvertedGroups;
232236
currentGroups = prevGroups;
233237
};
234238
}
235239

240+
List<DefaultConstraintValidatorContext.ValidationGroup> findGroupSequences(@Nullable Object bean) {
241+
if (bean == null) {
242+
return findGroupSequences();
243+
} else {
244+
BeanIntrospection<?> beanIntrospection = defaultValidator.getBeanIntrospection(bean);
245+
if (beanIntrospection == null) {
246+
return findGroupSequences();
247+
} else {
248+
return findGroupSequences(beanIntrospection);
249+
}
250+
}
251+
}
252+
236253
public List<ValidationGroup> findGroupSequences(BeanIntrospection<?> beanIntrospection) {
237-
if (hasDefaultGroup()) {
254+
FindGroupContext ctx = new FindGroupContext(defaultValidator, convertedGroups, definedGroups);
255+
if (ctx.isDefault()) {
256+
return defaultValidator.findGroupSequencesCache.computeIfAbsent(beanIntrospection, bi -> List.copyOf(findGroupSequences(ctx, bi)));
257+
} else {
258+
return findGroupSequences(ctx, beanIntrospection);
259+
}
260+
}
261+
262+
private static List<ValidationGroup> findGroupSequences(FindGroupContext ctx, BeanIntrospection<?> beanIntrospection) {
263+
if (hasDefaultGroup(ctx.definedGroups)) {
238264
Class<Object>[] classGroupSequence = beanIntrospection.classValues(GroupSequence.class);
239265
if (classGroupSequence.length > 0) {
240266
if (Arrays.stream(classGroupSequence).noneMatch(c -> c == beanIntrospection.getBeanType())) {
241267
throw new GroupDefinitionException("Group sequence is missing default group defined by the class of: " + beanIntrospection.getBeanType());
242268
}
243-
return Arrays.stream(classGroupSequence)
244-
.flatMap(group -> {
245-
if (group == beanIntrospection.getBeanType()) {
246-
return Stream.of(new ValidationGroup(true, true, List.of(Default.class)));
247-
}
248-
return findGroupSequence(Collections.singletonList(group), new HashSet<>()).stream();
249-
})
250-
.toList();
269+
List<ValidationGroup> dest = new ArrayList<>();
270+
for (Class<Object> group : classGroupSequence) {
271+
if (group == beanIntrospection.getBeanType()) {
272+
dest.add(new ValidationGroup(true, true, List.of(Default.class)));
273+
} else {
274+
findGroups(ctx, dest, List.of(group), new HashSet<>());
275+
}
276+
}
277+
return dest;
251278
}
252279
}
253-
return findGroupSequence(definedGroups, new HashSet<>());
280+
return findGroupSequences(ctx);
254281
}
255282

256283
public List<ValidationGroup> findGroupSequences() {
257-
return findGroupSequence(definedGroups, new HashSet<>());
284+
FindGroupContext ctx = new FindGroupContext(defaultValidator, convertedGroups, definedGroups);
285+
if (ctx.isDefault()) {
286+
return defaultValidator.findGroupSequencesCache.computeIfAbsent(null, ignored -> List.copyOf(findGroupSequences(ctx)));
287+
} else {
288+
return findGroupSequences(ctx);
289+
}
258290
}
259291

260-
private List<ValidationGroup> findGroupSequence(List<Class<?>> groups, Set<Class<?>> processedGroups) {
261-
return findGroups(groups, processedGroups).stream().toList();
292+
private static List<ValidationGroup> findGroupSequences(FindGroupContext ctx) {
293+
List<ValidationGroup> dest = new ArrayList<>();
294+
findGroups(ctx, dest, ctx.definedGroups, new HashSet<>());
295+
return dest;
262296
}
263297

264-
private List<ValidationGroup> findGroups(Class<?> group, Set<Class<?>> processedGroups) {
265-
if (convertedGroups != null) {
266-
group = convertGroup(group);
298+
private static void findGroups(FindGroupContext ctx, List<ValidationGroup> dest, Class<?> group, Set<Class<?>> processedGroups) {
299+
if (ctx.convertedGroups != null) {
300+
group = convertGroup(ctx.convertedGroups, group);
267301
}
268302
if (!processedGroups.add(group)) {
269303
throw new GroupDefinitionException("Cyclical group: " + group);
270304
}
271305
Class<?> finalGroup = group;
272306
List<Class<?>> groupSequence = GROUP_SEQUENCES.computeIfAbsent(group, ignore -> {
273-
return defaultValidator.getBeanIntrospector().findIntrospection(finalGroup).stream()
307+
return ctx.defaultValidator.getBeanIntrospector().findIntrospection(finalGroup).stream()
274308
.<Class<?>>flatMap(introspection -> Arrays.stream(introspection.classValues(GroupSequence.class)))
275309
.toList();
276310
});
277311
if (groupSequence.isEmpty()) {
278-
return List.of(new ValidationGroup(false, false, List.of(group)));
312+
dest.add(new ValidationGroup(false, false, List.of(group)));
313+
return;
314+
}
315+
int start = dest.size();
316+
for (Class<?> g : groupSequence) {
317+
findGroups(ctx, dest, g, processedGroups);
318+
}
319+
for (int i = start; i < groupSequence.size(); i++) {
320+
ValidationGroup vg = dest.get(i);
321+
dest.set(i, new ValidationGroup(true, true, vg.groups));
279322
}
280-
return groupSequence.stream()
281-
.flatMap(g -> findGroups(g, processedGroups).stream().map(vg -> new ValidationGroup(true, true, vg.groups))).toList();
282323
}
283324

284-
private Class<?> convertGroup(Class<?> group) {
325+
private static Class<?> convertGroup(Map<Class<?>, Class<?>> convertedGroups, Class<?> group) {
285326
Class<?> newGroup = convertedGroups.get(group);
286327
if (newGroup == null) {
287328
return group;
288329
}
289330
return newGroup;
290331
}
291332

292-
private List<ValidationGroup> findGroups(List<Class<?>> groupSequence, Set<Class<?>> processedGroups) {
293-
List<ValidationGroup> innerGroups = groupSequence.stream().flatMap(g -> findGroups(g, processedGroups).stream()).toList();
294-
if (innerGroups.stream().noneMatch(validationGroup -> validationGroup.isSequence)) {
295-
return List.of(
296-
new ValidationGroup(
297-
false,
298-
false,
299-
innerGroups.stream().flatMap(validationGroup -> validationGroup.groups.stream()).toList()
300-
)
301-
);
333+
private static void findGroups(FindGroupContext ctx, List<ValidationGroup> dest, List<Class<?>> groupSequence, Set<Class<?>> processedGroups) {
334+
int start = dest.size();
335+
for (Class<?> g : groupSequence) {
336+
findGroups(ctx, dest, g, processedGroups);
337+
}
338+
boolean anySequence = false;
339+
for (int i = start; i < groupSequence.size() && !anySequence; i++) {
340+
anySequence |= dest.get(i).isSequence;
341+
}
342+
if (!anySequence) {
343+
List<ValidationGroup> subList = dest.subList(start, dest.size());
344+
List<Class<?>> copy = new ArrayList<>();
345+
for (ValidationGroup validationGroup : subList) {
346+
copy.addAll(validationGroup.groups);
347+
}
348+
subList.clear();
349+
dest.add(new ValidationGroup(false, false, copy));
302350
}
303-
return innerGroups;
304351
}
305352

306353
public void addViolation(DefaultConstraintViolation<R> violation) {
307-
if (currentViolations != null) {
308-
currentViolations.add(violation);
309-
}
354+
hasCurrentViolations = true;
310355
overallViolations.add(violation);
311356
}
312357

@@ -394,4 +439,14 @@ interface ValidationCloseable extends AutoCloseable {
394439
record ValidationGroup(boolean isSequence, boolean isRedefinedDefaultGroupSequence,
395440
List<Class<?>> groups) {
396441
}
442+
443+
private record FindGroupContext(
444+
DefaultValidator defaultValidator,
445+
Map<Class<?>, Class<?>> convertedGroups,
446+
List<Class<?>> definedGroups
447+
) {
448+
boolean isDefault() {
449+
return definedGroups == DEFAULT_GROUPS && convertedGroups.isEmpty();
450+
}
451+
}
397452
}

validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintViolationBuilder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ final class DefaultConstraintViolationBuilder<R> implements ConstraintValidatorC
4848
this.constraintValidatorContext = constraintValidatorContext;
4949
this.messageInterpolator = messageInterpolator;
5050
this.validationPath = new ValidationPath(constraintValidatorContext.getCurrentPath());
51-
Path.Node last = validationPath.nodes.peekLast();
51+
Path.Node last = validationPath.peekLast();
5252
ElementKind kind = last == null ? null : last.getKind();
5353
if (kind == ElementKind.CROSS_PARAMETER) {
54-
validationPath.nodes.pollLast();
54+
validationPath.removeLast();
5555
}
5656
if (kind == ElementKind.BEAN) {
57-
Path.Node node = validationPath.nodes.pollLast();
57+
Path.Node node = validationPath.removeLast();
5858
ValidationPath.DefaultNode defaultNode = (ValidationPath.DefaultNode) node;
5959
next = new ValidationPath.MutableContainerContext(defaultNode.containerContext);
6060
}
@@ -99,7 +99,7 @@ public ContainerElementNodeBuilderCustomizableContext addContainerElementNode(St
9999

100100
@Override
101101
public NodeBuilderDefinedContext addParameterNode(int index) {
102-
Path.Node node = validationPath.nodes.peekLast();
102+
Path.Node node = validationPath.peekLast();
103103
if (node == null || node.getKind() != ElementKind.METHOD) {
104104
throw new IllegalStateException("Cannot add parameter at path kind: " + (node == null ? "null" : node.getKind()));
105105
}

0 commit comments

Comments
 (0)