This is an automated email from the ASF dual-hosted git repository. jonnybot pushed a commit to branch INDY-PERF-EXPLORATION in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 0cfe14e76c09575cb3e6883ad581652ba63a6198 Author: Jonny Carter <[email protected]> AuthorDate: Thu Jan 22 17:55:39 2026 -0600 Create benchmarks around aspects potentially related to Grails performance issues --- subprojects/performance/README.adoc | 64 +++ .../bench/dispatch/CacheInvalidationBench.java | 339 ++++++++++++++++ .../groovy/bench/dispatch/DispatchHelper.groovy | 68 ++++ .../apache/groovy/bench/indy/ColdCallBench.java | 302 ++++++++++++++ .../groovy/bench/indy/ColdCallPatterns.groovy | 222 ++++++++++ .../bench/indy/ThresholdSensitivityBench.java | 316 +++++++++++++++ .../groovy/bench/indy/WarmupBehaviorBench.java | 268 ++++++++++++ .../apache/groovy/bench/orm/DomainObjects.groovy | 377 +++++++++++++++++ .../groovy/bench/orm/PropertyAccessBench.java | 447 +++++++++++++++++++++ .../groovy/bench/orm/PropertyAccessHelper.groovy | 175 ++++++++ 10 files changed, 2578 insertions(+) diff --git a/subprojects/performance/README.adoc b/subprojects/performance/README.adoc index e6e4aa15e5..2cd55fee9a 100644 --- a/subprojects/performance/README.adoc +++ b/subprojects/performance/README.adoc @@ -55,3 +55,67 @@ To run a single benchmark or a matched set of benchmarks, use the The `benchInclude` property will perform a partial match against package names or class names. It is equivalent to `.*${benchInclude}.*`. +== InvokeDynamic Performance Benchmarks + +A set of benchmarks specifically designed to investigate and isolate performance +issues with Groovy's invokedynamic implementation. See `INDY_PERFORMANCE_PLAN.md` +for the full testing plan and background. + +=== Available Benchmark Suites + +==== Cold Call Benchmarks (`ColdCallBench`) + +Measures the cost of method invocations before callsites are optimized. +This is critical for web applications where objects are created per-request. + + ./gradlew -Pindy=true -PbenchInclude=ColdCallBench :perf:jmh + +==== Warmup Behavior Benchmarks (`WarmupBehaviorBench`) + +Tests performance at different warmup levels (10, 100, 1000, 10000+ calls) +to understand how the optimization threshold affects performance. + + ./gradlew -Pindy=true -PbenchInclude=WarmupBehavior :perf:jmh + +==== Threshold Sensitivity Benchmarks (`ThresholdSensitivityBench`) + +Tests different usage patterns (web request, batch processing, mixed) to +understand which patterns benefit from different threshold configurations. + + ./gradlew -Pindy=true -PbenchInclude=ThresholdSensitivity :perf:jmh + +==== Cache Invalidation Benchmarks (`CacheInvalidationBench`) + +Tests polymorphic dispatch scenarios that cause inline cache invalidation, +addressing issues described in GROOVY-8298. + + ./gradlew -Pindy=true -PbenchInclude=CacheInvalidation :perf:jmh + +==== Property Access Benchmarks (`PropertyAccessBench`) + +Tests GORM-like property access patterns common in Grails applications. + + ./gradlew -Pindy=true -PbenchInclude=PropertyAccess :perf:jmh + +=== Comparing Indy vs Non-Indy + +Run the same benchmark with and without invokedynamic to measure the overhead: + + # With invokedynamic (Groovy 4 default) + ./gradlew -Pindy=true -PbenchInclude=ColdCallBench :perf:jmh + + # Without invokedynamic (classic bytecode) + ./gradlew -Pindy=false -PbenchInclude=ColdCallBench :perf:jmh + +=== Tuning Thresholds + +The invokedynamic implementation has configurable thresholds: + +- `groovy.indy.optimize.threshold` (default: 10000) - Calls before optimization +- `groovy.indy.fallback.threshold` (default: 10000) - Fallbacks before reset + +Test with different thresholds using JVM arguments: + + ./gradlew -Pindy=true -PbenchInclude=ThresholdSensitivity :perf:jmh \ + --jvmArgs="-Dgroovy.indy.optimize.threshold=100" + diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/CacheInvalidationBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/CacheInvalidationBench.java new file mode 100644 index 0000000000..6c37aaf9eb --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/CacheInvalidationBench.java @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.bench.dispatch; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for measuring inline cache invalidation overhead. + * <p> + * This addresses the issues described in GROOVY-8298 where polymorphic + * dispatch on collections with mixed types causes continuous cache + * invalidation and prevents JIT optimization. + * <p> + * Key scenarios tested: + * <ul> + * <li>Stable type pattern: same types in consistent order</li> + * <li>Rotating types: types change but predictably</li> + * <li>Random types: unpredictable type changes (cache thrashing)</li> + * <li>Growing polymorphism: gradually increasing type diversity</li> + * </ul> + * <p> + * Run with: ./gradlew -Pindy=true -PbenchInclude=CacheInvalidation :perf:jmh + */ +@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(3) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class CacheInvalidationBench { + + // ======================================================================== + // Receiver classes (same interface, different implementations) + // ======================================================================== + + interface Receiver { + String receive(); + } + + static class ReceiverA implements Receiver { + @Override public String receive() { return "A"; } + } + + static class ReceiverB implements Receiver { + @Override public String receive() { return "B"; } + } + + static class ReceiverC implements Receiver { + @Override public String receive() { return "C"; } + } + + static class ReceiverD implements Receiver { + @Override public String receive() { return "D"; } + } + + static class ReceiverE implements Receiver { + @Override public String receive() { return "E"; } + } + + static class ReceiverF implements Receiver { + @Override public String receive() { return "F"; } + } + + static class ReceiverG implements Receiver { + @Override public String receive() { return "G"; } + } + + static class ReceiverH implements Receiver { + @Override public String receive() { return "H"; } + } + + static class ReceiverI implements Receiver { + @Override public String receive() { return "I"; } + } + + static class ReceiverJ implements Receiver { + @Override public String receive() { return "J"; } + } + + private static final Receiver[] ALL_RECEIVERS = { + new ReceiverA(), new ReceiverB(), new ReceiverC(), new ReceiverD(), + new ReceiverE(), new ReceiverF(), new ReceiverG(), new ReceiverH(), + new ReceiverI(), new ReceiverJ() + }; + + // ======================================================================== + // State classes + // ======================================================================== + + private static final int COLLECTION_SIZE = 100; + + /** + * Monomorphic: all same type - best case for inline cache. + */ + @State(Scope.Thread) + public static class MonomorphicState { + List<Receiver> receivers; + + @Setup(Level.Trial) + public void setup() { + receivers = new ArrayList<>(COLLECTION_SIZE); + for (int i = 0; i < COLLECTION_SIZE; i++) { + receivers.add(new ReceiverA()); + } + } + } + + /** + * Bimorphic: two types - still optimizable by JIT. + */ + @State(Scope.Thread) + public static class BimorphicState { + List<Receiver> receivers; + + @Setup(Level.Trial) + public void setup() { + receivers = new ArrayList<>(COLLECTION_SIZE); + for (int i = 0; i < COLLECTION_SIZE; i++) { + receivers.add(i % 2 == 0 ? new ReceiverA() : new ReceiverB()); + } + } + } + + /** + * Polymorphic (3 types): getting harder for JIT. + */ + @State(Scope.Thread) + public static class Polymorphic3State { + List<Receiver> receivers; + + @Setup(Level.Trial) + public void setup() { + receivers = new ArrayList<>(COLLECTION_SIZE); + Receiver[] types = {new ReceiverA(), new ReceiverB(), new ReceiverC()}; + for (int i = 0; i < COLLECTION_SIZE; i++) { + receivers.add(types[i % 3]); + } + } + } + + /** + * Megamorphic (8 types): beyond inline cache capacity. + */ + @State(Scope.Thread) + public static class MegamorphicState { + List<Receiver> receivers; + + @Setup(Level.Trial) + public void setup() { + receivers = new ArrayList<>(COLLECTION_SIZE); + for (int i = 0; i < COLLECTION_SIZE; i++) { + receivers.add(ALL_RECEIVERS[i % 8]); + } + } + } + + /** + * Highly megamorphic (10 types). + */ + @State(Scope.Thread) + public static class HighlyMegamorphicState { + List<Receiver> receivers; + + @Setup(Level.Trial) + public void setup() { + receivers = new ArrayList<>(COLLECTION_SIZE); + for (int i = 0; i < COLLECTION_SIZE; i++) { + receivers.add(ALL_RECEIVERS[i % 10]); + } + } + } + + /** + * Random order: unpredictable types cause cache misses. + */ + @State(Scope.Thread) + public static class RandomOrderState { + List<Receiver> receivers; + final Random random = new Random(42); // Fixed seed for reproducibility + + @Setup(Level.Iteration) + public void setup() { + receivers = new ArrayList<>(COLLECTION_SIZE); + for (int i = 0; i < COLLECTION_SIZE; i++) { + receivers.add(ALL_RECEIVERS[random.nextInt(8)]); + } + // Shuffle to ensure random access pattern + Collections.shuffle(receivers, random); + } + } + + /** + * Changing types each iteration: simulates cache invalidation. + */ + @State(Scope.Thread) + public static class ChangingTypesState { + List<Receiver> receivers; + int iteration = 0; + + @Setup(Level.Iteration) + public void setup() { + receivers = new ArrayList<>(COLLECTION_SIZE); + // Each iteration uses different types + int offset = iteration % 5; + for (int i = 0; i < COLLECTION_SIZE; i++) { + receivers.add(ALL_RECEIVERS[(i + offset) % 8]); + } + iteration++; + } + } + + // ======================================================================== + // Groovy dispatch benchmarks + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void groovy_monomorphic(MonomorphicState state, Blackhole bh) { + DispatchHelper.dispatchAll(state.receivers, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void groovy_bimorphic(BimorphicState state, Blackhole bh) { + DispatchHelper.dispatchAll(state.receivers, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void groovy_polymorphic3(Polymorphic3State state, Blackhole bh) { + DispatchHelper.dispatchAll(state.receivers, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void groovy_megamorphic8(MegamorphicState state, Blackhole bh) { + DispatchHelper.dispatchAll(state.receivers, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void groovy_megamorphic10(HighlyMegamorphicState state, Blackhole bh) { + DispatchHelper.dispatchAll(state.receivers, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void groovy_randomOrder(RandomOrderState state, Blackhole bh) { + DispatchHelper.dispatchAll(state.receivers, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void groovy_changingTypes(ChangingTypesState state, Blackhole bh) { + DispatchHelper.dispatchAll(state.receivers, bh); + } + + // ======================================================================== + // Java baseline benchmarks + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void java_monomorphic(MonomorphicState state, Blackhole bh) { + for (Receiver r : state.receivers) { + bh.consume(r.receive()); + } + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void java_bimorphic(BimorphicState state, Blackhole bh) { + for (Receiver r : state.receivers) { + bh.consume(r.receive()); + } + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void java_polymorphic3(Polymorphic3State state, Blackhole bh) { + for (Receiver r : state.receivers) { + bh.consume(r.receive()); + } + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void java_megamorphic8(MegamorphicState state, Blackhole bh) { + for (Receiver r : state.receivers) { + bh.consume(r.receive()); + } + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void java_megamorphic10(HighlyMegamorphicState state, Blackhole bh) { + for (Receiver r : state.receivers) { + bh.consume(r.receive()); + } + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void java_randomOrder(RandomOrderState state, Blackhole bh) { + for (Receiver r : state.receivers) { + bh.consume(r.receive()); + } + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void java_changingTypes(ChangingTypesState state, Blackhole bh) { + for (Receiver r : state.receivers) { + bh.consume(r.receive()); + } + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/DispatchHelper.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/DispatchHelper.groovy new file mode 100644 index 0000000000..c5f477b600 --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/DispatchHelper.groovy @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.bench.dispatch + +import org.openjdk.jmh.infra.Blackhole + +/** + * Helper class for dispatch benchmarks. + * Methods here are compiled with Groovy's dynamic dispatch, + * allowing comparison with Java's static dispatch. + */ +class DispatchHelper { + + /** + * Dispatch to all receivers in the list. + * This uses Groovy's dynamic method dispatch. + */ + static void dispatchAll(List receivers, Blackhole bh) { + for (receiver in receivers) { + bh.consume(receiver.receive()) + } + } + + /** + * Dispatch using spread operator. + */ + static List spreadDispatch(List receivers) { + receivers*.receive() + } + + /** + * Dispatch with collect. + */ + static List collectDispatch(List receivers) { + receivers.collect { it.receive() } + } + + /** + * Dispatch with findAll + collect chain. + */ + static List chainedDispatch(List receivers) { + receivers.findAll { it != null } + .collect { it.receive() } + } + + /** + * Single dispatch call - for measuring individual call overhead. + */ + static Object singleDispatch(Object receiver) { + receiver.receive() + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/ColdCallBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/ColdCallBench.java new file mode 100644 index 0000000000..71687ca9d8 --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/ColdCallBench.java @@ -0,0 +1,302 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.bench.indy; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for measuring cold call overhead in Groovy's invokedynamic implementation. + * <p> + * Cold calls are method invocations that happen before the callsite has been optimized. + * This is particularly important for web applications where objects are created per-request + * and may only have their methods called a few times before being discarded. + * <p> + * Key areas tested: + * <ul> + * <li>First invocation cost (callsite bootstrap)</li> + * <li>Create-and-call patterns (new object + immediate method call)</li> + * <li>Property access on new objects</li> + * <li>Spread operator overhead</li> + * <li>Collection operations</li> + * </ul> + * <p> + * Run with: ./gradlew -Pindy=true -PbenchInclude=ColdCallBench :perf:jmh + * Compare: Run once with -Pindy=true and once without to see the difference. + */ +@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(3) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class ColdCallBench { + + // ======================================================================== + // State classes + // ======================================================================== + + @State(Scope.Thread) + public static class ServiceState { + SimpleService service; + Person person; + List<Person> people; + + @Setup(Level.Invocation) + public void setupPerInvocation() { + // Create fresh objects for each invocation to simulate cold calls + service = new SimpleService(); + person = new Person(); + person.setFirstName("John"); + person.setLastName("Doe"); + } + + @Setup(Level.Trial) + public void setupTrial() { + // Create a list of people for collection operations + people = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + Person p = new Person(); + p.setFirstName(i % 2 == 0 ? "Alice" : "Bob"); + p.setLastName("User" + i); + people.add(p); + } + } + } + + @State(Scope.Thread) + public static class WarmServiceState { + SimpleService service; + Person person; + + @Setup(Level.Trial) + public void setup() { + // Create objects once and reuse - tests "warm" callsite performance + service = new SimpleService(); + person = new Person(); + person.setFirstName("John"); + person.setLastName("Doe"); + + // Warm up the callsites + for (int i = 0; i < 20000; i++) { + service.getName(); + service.compute(1, 2); + person.getFullName(); + } + } + } + + // ======================================================================== + // Cold call benchmarks - new object each invocation + // ======================================================================== + + /** + * Baseline: Create object only (no method call). + */ + @Benchmark + public Object cold_01_createObjectOnly(Blackhole bh) { + SimpleService svc = new SimpleService(); + bh.consume(svc); + return svc; + } + + /** + * Cold call: Create object and call one method. + * This is the most common pattern in web apps. + */ + @Benchmark + public Object cold_02_createAndCallOne(ServiceState state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + /** + * Cold call: Create object and call multiple methods. + */ + @Benchmark + public Object cold_03_createAndCallMultiple(ServiceState state, Blackhole bh) { + Object r1 = state.service.getName(); + Object r2 = state.service.compute(1, 2); + state.service.doWork(); + bh.consume(r1); + bh.consume(r2); + return r2; + } + + /** + * Cold call: Property access on new object. + */ + @Benchmark + public Object cold_04_propertyAccess(ServiceState state, Blackhole bh) { + Object result = state.person.getFullName(); + bh.consume(result); + return result; + } + + /** + * Cold call via factory pattern (common in frameworks). + */ + @Benchmark + public Object cold_05_factoryCreateAndCall(Blackhole bh) { + Object result = ServiceFactory.createAndCall(); + bh.consume(result); + return result; + } + + /** + * Cold call via factory with multiple method calls. + */ + @Benchmark + public Object cold_06_factoryCreateAndCallMultiple(Blackhole bh) { + Object result = ServiceFactory.createAndCallMultiple(); + bh.consume(result); + return result; + } + + // ======================================================================== + // Warm call benchmarks - same object, reused callsite + // ======================================================================== + + /** + * Warm call: Same object, callsite should be optimized. + */ + @Benchmark + public Object warm_01_singleMethod(WarmServiceState state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + /** + * Warm call: Multiple methods on warmed object. + */ + @Benchmark + public Object warm_02_multipleMethods(WarmServiceState state, Blackhole bh) { + Object r1 = state.service.getName(); + Object r2 = state.service.compute(1, 2); + state.service.doWork(); + bh.consume(r1); + bh.consume(r2); + return r2; + } + + /** + * Warm call: Property access. + */ + @Benchmark + public Object warm_03_propertyAccess(WarmServiceState state, Blackhole bh) { + Object result = state.person.getFullName(); + bh.consume(result); + return result; + } + + // ======================================================================== + // Collection operation benchmarks + // ======================================================================== + + /** + * Spread operator on collection of objects. + * Common GORM pattern: domainObjects*.propertyName + */ + @Benchmark + public Object collection_01_spreadOperator(ServiceState state, Blackhole bh) { + List<String> result = ColdCallOperations.spreadOperator(state.people); + bh.consume(result); + return result; + } + + /** + * Collection operations: findAll + collect + join. + * Very common in Grails controllers and services. + */ + @Benchmark + public Object collection_02_chainedOperations(ServiceState state, Blackhole bh) { + Object result = ColdCallOperations.collectionOperations(state.people); + bh.consume(result); + return result; + } + + // ======================================================================== + // GString interpolation benchmark + // ======================================================================== + + /** + * GString with method call interpolation. + */ + @Benchmark + public Object gstring_01_interpolation(ServiceState state, Blackhole bh) { + String result = ColdCallOperations.gstringInterpolation(state.person); + bh.consume(result); + return result; + } + + // ======================================================================== + // Static method benchmarks (for comparison) + // ======================================================================== + + /** + * Static method call (should have less overhead). + */ + @Benchmark + public Object static_01_methodCall(Blackhole bh) { + Object result = SimpleService.staticMethod(); + bh.consume(result); + return result; + } + + // ======================================================================== + // Java equivalent benchmarks for comparison + // ======================================================================== + + /** + * Java equivalent: create and call. + */ + @Benchmark + public Object java_01_createAndCall(Blackhole bh) { + JavaService svc = new JavaService(); + Object result = svc.getName(); + bh.consume(result); + return result; + } + + /** + * Java equivalent: multiple calls. + */ + @Benchmark + public Object java_02_createAndCallMultiple(Blackhole bh) { + JavaService svc = new JavaService(); + Object r1 = svc.getName(); + int r2 = svc.compute(1, 2); + svc.doWork(); + bh.consume(r1); + bh.consume(r2); + return r2; + } + + // Java equivalent class + public static class JavaService { + public String getName() { return "JavaService"; } + public int compute(int a, int b) { return a + b; } + public void doWork() { /* no-op */ } + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/ColdCallPatterns.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/ColdCallPatterns.groovy new file mode 100644 index 0000000000..d0b81bc31a --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/ColdCallPatterns.groovy @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.bench.indy + +/** + * Groovy classes for cold call benchmarks. + * These simulate patterns common in web applications where objects are + * created, used briefly, and discarded - resulting in many "cold" callsites. + */ + +/** + * Simple service class with various method signatures. + * Used to test cold method invocation overhead. + */ +class SimpleService { + String getName() { "SimpleService" } + int compute(int a, int b) { a + b } + def process(Map params) { params.size() } + void doWork() { /* no-op */ } + static String staticMethod() { "static" } +} + +/** + * Factory that creates new instances for each call. + * Simulates request-scoped object creation in web apps. + */ +class ServiceFactory { + static SimpleService createService() { + new SimpleService() + } + + static Object createAndCall() { + def svc = new SimpleService() + svc.getName() + } + + static Object createAndCallMultiple() { + def svc = new SimpleService() + svc.getName() + svc.compute(1, 2) + svc.process([a: 1, b: 2]) + svc.doWork() + } +} + +/** + * Classes to test property access patterns (common in Grails domain objects). + */ +class DomainLikeObject { + String name + Integer age + Date createdAt + Map metadata = [:] + + def getProperty(String propName) { + if (metadata.containsKey(propName)) { + return metadata[propName] + } + return super.getProperty(propName) + } + + void setProperty(String propName, Object value) { + if (propName.startsWith('dynamic_')) { + metadata[propName] = value + } else { + super.setProperty(propName, value) + } + } +} + +/** + * Multiple domain classes to simulate polymorphic scenarios. + */ +class Person { + String firstName + String lastName + String getFullName() { "$firstName $lastName" } +} + +class Employee extends Person { + String department + BigDecimal salary +} + +class Customer extends Person { + String accountNumber + Date memberSince +} + +class Vendor extends Person { + String companyName + List<String> products = [] +} + +/** + * Interface-based dispatch testing. + */ +interface Processable { + Object process() + String getType() +} + +class TypeA implements Processable { + Object process() { "processed-A" } + String getType() { "A" } +} + +class TypeB implements Processable { + Object process() { "processed-B" } + String getType() { "B" } +} + +class TypeC implements Processable { + Object process() { "processed-C" } + String getType() { "C" } +} + +class TypeD implements Processable { + Object process() { "processed-D" } + String getType() { "D" } +} + +class TypeE implements Processable { + Object process() { "processed-E" } + String getType() { "E" } +} + +class TypeF implements Processable { + Object process() { "processed-F" } + String getType() { "F" } +} + +class TypeG implements Processable { + Object process() { "processed-G" } + String getType() { "G" } +} + +class TypeH implements Processable { + Object process() { "processed-H" } + String getType() { "H" } +} + +/** + * Utility class for cold call operations. + */ +class ColdCallOperations { + /** + * Simulates a typical web request: create object, access properties, call methods. + */ + static Object simulateRequest(int id) { + def person = new Person(firstName: "User", lastName: "Number$id") + def result = person.getFullName() + return result + } + + /** + * Call a method N times to test warmup behavior. + */ + static void callNTimes(Object target, String methodName, int n) { + for (int i = 0; i < n; i++) { + target."$methodName"() + } + } + + /** + * Access a property N times. + */ + static void accessPropertyNTimes(Object target, String propName, int n) { + for (int i = 0; i < n; i++) { + target."$propName" + } + } + + /** + * Create N different objects and call a method on each (all cold calls). + */ + static void coldCallsOnNewObjects(int n) { + for (int i = 0; i < n; i++) { + def svc = new SimpleService() + svc.getName() + } + } + + /** + * Simulate GString interpolation which involves method calls. + */ + static String gstringInterpolation(Person p) { + "Hello, ${p.getFullName()}! Welcome back." + } + + /** + * Spread operator on collection - common GORM pattern. + */ + static List<String> spreadOperator(List<Person> people) { + people*.getFullName() + } + + /** + * Collection operations - very common in Grails. + */ + static def collectionOperations(List<Person> people) { + people.findAll { it.firstName.startsWith('A') } + .collect { it.getFullName() } + .join(', ') + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/ThresholdSensitivityBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/ThresholdSensitivityBench.java new file mode 100644 index 0000000000..9cdb92d1f9 --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/ThresholdSensitivityBench.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.bench.indy; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks to understand threshold sensitivity for indy optimization. + * <p> + * This benchmark simulates different usage patterns that would benefit from + * different threshold configurations: + * <ul> + * <li>Web request pattern: Many short-lived objects with few calls each</li> + * <li>Batch processing pattern: Few objects with many calls each</li> + * <li>Mixed pattern: Combination of both</li> + * </ul> + * <p> + * Run with different threshold values to find optimal settings: + * <pre> + * # Default threshold (10000) + * ./gradlew -Pindy=true -PbenchInclude=ThresholdSensitivity :perf:jmh + * + * # Lower threshold (100) + * ./gradlew -Pindy=true -PbenchInclude=ThresholdSensitivity :perf:jmh \ + * --jvmArgs="-Dgroovy.indy.optimize.threshold=100" + * + * # Very low threshold (0 - immediate optimization) + * ./gradlew -Pindy=true -PbenchInclude=ThresholdSensitivity :perf:jmh \ + * --jvmArgs="-Dgroovy.indy.optimize.threshold=0 -Dgroovy.indy.fallback.threshold=0" + * </pre> + */ +@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(3) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class ThresholdSensitivityBench { + + // ======================================================================== + // Web Request Pattern: Many objects, few calls each + // ======================================================================== + + /** + * Simulates web request handling: create object, call 1-5 methods, discard. + * This pattern suffers most from high optimization thresholds. + */ + @Benchmark + public void webRequest_singleCall(Blackhole bh) { + SimpleService svc = new SimpleService(); + bh.consume(svc.getName()); + } + + @Benchmark + public void webRequest_fewCalls(Blackhole bh) { + SimpleService svc = new SimpleService(); + bh.consume(svc.getName()); + bh.consume(svc.compute(1, 2)); + svc.doWork(); + } + + @Benchmark + public void webRequest_typicalController(Blackhole bh) { + // Simulate a typical controller action + Person person = new Person(); + person.setFirstName("John"); + person.setLastName("Doe"); + + // Access properties (common in view rendering) + bh.consume(person.getFirstName()); + bh.consume(person.getLastName()); + bh.consume(person.getFullName()); + } + + // ======================================================================== + // Batch Processing Pattern: Few objects, many calls each + // ======================================================================== + + @State(Scope.Thread) + public static class BatchState { + SimpleService service; + List<Person> people; + + @Setup(Level.Trial) + public void setup() { + service = new SimpleService(); + people = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + Person p = new Person(); + p.setFirstName("User" + i); + p.setLastName("Batch"); + people.add(p); + } + } + } + + /** + * Batch processing: same method called many times on same object. + * This pattern benefits from optimization even with high thresholds. + */ + @Benchmark + @OperationsPerInvocation(1000) + public void batch_repeatMethodCall(BatchState state, Blackhole bh) { + for (int i = 0; i < 1000; i++) { + bh.consume(state.service.getName()); + } + } + + /** + * Batch: iterate over collection calling method on each. + */ + @Benchmark + @OperationsPerInvocation(1000) + public void batch_collectionIteration(BatchState state, Blackhole bh) { + for (Person p : state.people) { + bh.consume(p.getFullName()); + } + } + + // ======================================================================== + // Mixed Pattern: Combination of both + // ======================================================================== + + @State(Scope.Thread) + public static class MixedState { + List<Person> cachedPeople; + int requestCount = 0; + + @Setup(Level.Trial) + public void setup() { + // Some cached objects (like in a session or application scope) + cachedPeople = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Person p = new Person(); + p.setFirstName("Cached" + i); + p.setLastName("User"); + cachedPeople.add(p); + } + } + } + + /** + * Mixed: some cached objects + some new objects per request. + */ + @Benchmark + public void mixed_cachedAndNew(MixedState state, Blackhole bh) { + // Access cached object (warm callsite) + Person cached = state.cachedPeople.get(state.requestCount % 10); + bh.consume(cached.getFullName()); + + // Create new request-scoped object (cold callsite pattern) + Person requestScoped = new Person(); + requestScoped.setFirstName("Request"); + requestScoped.setLastName(String.valueOf(state.requestCount++)); + bh.consume(requestScoped.getFullName()); + } + + // ======================================================================== + // Polymorphic Call Patterns (affected by fallback threshold) + // ======================================================================== + + @State(Scope.Thread) + public static class PolymorphicState { + Processable[] processors; + + @Setup(Level.Trial) + public void setup() { + // Mix of different implementation types + processors = new Processable[] { + new TypeA(), new TypeB(), new TypeC(), new TypeD(), + new TypeE(), new TypeF(), new TypeG(), new TypeH() + }; + } + } + + /** + * Polymorphic dispatch: same interface, different implementations. + * This tests the inline cache and fallback behavior. + */ + @Benchmark + @OperationsPerInvocation(8) + public void polymorphic_interfaceDispatch(PolymorphicState state, Blackhole bh) { + for (Processable p : state.processors) { + bh.consume(p.process()); + } + } + + /** + * Polymorphic with random access pattern (worst case for inline cache). + */ + @Benchmark + public void polymorphic_randomAccess(PolymorphicState state, Blackhole bh) { + // Access in unpredictable order + bh.consume(state.processors[3].process()); + bh.consume(state.processors[7].process()); + bh.consume(state.processors[1].process()); + bh.consume(state.processors[5].process()); + bh.consume(state.processors[0].process()); + bh.consume(state.processors[6].process()); + bh.consume(state.processors[2].process()); + bh.consume(state.processors[4].process()); + } + + // ======================================================================== + // Property Access Patterns (very common in Grails) + // ======================================================================== + + @State(Scope.Thread) + public static class PropertyState { + DomainLikeObject domain; + + @Setup(Level.Trial) + public void setup() { + domain = new DomainLikeObject(); + domain.setName("TestDomain"); + domain.setAge(25); + } + } + + /** + * Property getter access. + */ + @Benchmark + public void property_getter(PropertyState state, Blackhole bh) { + bh.consume(state.domain.getName()); + bh.consume(state.domain.getAge()); + } + + /** + * Property setter access. + */ + @Benchmark + public void property_setter(PropertyState state, Blackhole bh) { + state.domain.setName("Updated"); + state.domain.setAge(26); + bh.consume(state.domain); + } + + /** + * Dynamic property access (uses getProperty/setProperty). + */ + @Benchmark + public void property_dynamic(PropertyState state, Blackhole bh) { + state.domain.setProperty("dynamic_key", "value"); + bh.consume(state.domain.getProperty("dynamic_key")); + } + + // ======================================================================== + // Java Baselines + // ======================================================================== + + public static class JavaPerson { + private String firstName; + private String lastName; + + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + public String getFullName() { return firstName + " " + lastName; } + } + + @Benchmark + public void java_webRequest(Blackhole bh) { + JavaPerson person = new JavaPerson(); + person.setFirstName("John"); + person.setLastName("Doe"); + bh.consume(person.getFirstName()); + bh.consume(person.getLastName()); + bh.consume(person.getFullName()); + } + + @State(Scope.Thread) + public static class JavaBatchState { + List<JavaPerson> people; + + @Setup(Level.Trial) + public void setup() { + people = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + JavaPerson p = new JavaPerson(); + p.setFirstName("User" + i); + p.setLastName("Batch"); + people.add(p); + } + } + } + + @Benchmark + @OperationsPerInvocation(1000) + public void java_batchIteration(JavaBatchState state, Blackhole bh) { + for (JavaPerson p : state.people) { + bh.consume(p.getFullName()); + } + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/WarmupBehaviorBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/WarmupBehaviorBench.java new file mode 100644 index 0000000000..204815ef56 --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/indy/WarmupBehaviorBench.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.bench.indy; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for measuring warmup behavior around the indy optimization threshold. + * <p> + * The default threshold is controlled by groovy.indy.optimize.threshold (default: 10,000). + * This benchmark tests performance at various call counts to understand: + * <ul> + * <li>The cost of calls below the threshold</li> + * <li>The transition behavior around the threshold</li> + * <li>The performance after optimization kicks in</li> + * </ul> + * <p> + * Run with different thresholds: + * <pre> + * ./gradlew -Pindy=true -PbenchInclude=WarmupBehavior :perf:jmh + * ./gradlew -Pindy=true -PbenchInclude=WarmupBehavior :perf:jmh -Dgroovy.indy.optimize.threshold=100 + * </pre> + */ +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(3) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class WarmupBehaviorBench { + + // ======================================================================== + // State classes with different warmup levels + // ======================================================================== + + @State(Scope.Thread) + public static class ColdState { + SimpleService service; + + @Setup(Level.Invocation) + public void setup() { + // Fresh object each time - completely cold callsite + service = new SimpleService(); + } + } + + @State(Scope.Thread) + public static class Warmed10State { + SimpleService service; + + @Setup(Level.Iteration) + public void setup() { + service = new SimpleService(); + // Warm with 10 calls + for (int i = 0; i < 10; i++) { + service.getName(); + } + } + } + + @State(Scope.Thread) + public static class Warmed100State { + SimpleService service; + + @Setup(Level.Iteration) + public void setup() { + service = new SimpleService(); + // Warm with 100 calls + for (int i = 0; i < 100; i++) { + service.getName(); + } + } + } + + @State(Scope.Thread) + public static class Warmed1000State { + SimpleService service; + + @Setup(Level.Iteration) + public void setup() { + service = new SimpleService(); + // Warm with 1,000 calls + for (int i = 0; i < 1000; i++) { + service.getName(); + } + } + } + + @State(Scope.Thread) + public static class Warmed5000State { + SimpleService service; + + @Setup(Level.Iteration) + public void setup() { + service = new SimpleService(); + // Warm with 5,000 calls (below default threshold) + for (int i = 0; i < 5000; i++) { + service.getName(); + } + } + } + + @State(Scope.Thread) + public static class Warmed10000State { + SimpleService service; + + @Setup(Level.Iteration) + public void setup() { + service = new SimpleService(); + // Warm with 10,000 calls (at default threshold) + for (int i = 0; i < 10000; i++) { + service.getName(); + } + } + } + + @State(Scope.Thread) + public static class Warmed15000State { + SimpleService service; + + @Setup(Level.Iteration) + public void setup() { + service = new SimpleService(); + // Warm with 15,000 calls (above default threshold) + for (int i = 0; i < 15000; i++) { + service.getName(); + } + } + } + + @State(Scope.Thread) + public static class Warmed50000State { + SimpleService service; + + @Setup(Level.Iteration) + public void setup() { + service = new SimpleService(); + // Warm with 50,000 calls (well above threshold) + for (int i = 0; i < 50000; i++) { + service.getName(); + } + } + } + + @State(Scope.Thread) + public static class FullyWarmedState { + SimpleService service; + + @Setup(Level.Trial) + public void setup() { + service = new SimpleService(); + // Warm with 200,000 calls (fully optimized) + for (int i = 0; i < 200000; i++) { + service.getName(); + } + } + } + + // ======================================================================== + // Benchmarks at different warmup levels + // ======================================================================== + + @Benchmark + public Object warmup_00_cold(ColdState state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + @Benchmark + public Object warmup_01_after10(Warmed10State state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + @Benchmark + public Object warmup_02_after100(Warmed100State state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + @Benchmark + public Object warmup_03_after1000(Warmed1000State state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + @Benchmark + public Object warmup_04_after5000(Warmed5000State state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + @Benchmark + public Object warmup_05_after10000(Warmed10000State state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + @Benchmark + public Object warmup_06_after15000(Warmed15000State state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + @Benchmark + public Object warmup_07_after50000(Warmed50000State state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + @Benchmark + public Object warmup_08_fullyWarmed(FullyWarmedState state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } + + // ======================================================================== + // Java baseline for comparison + // ======================================================================== + + @State(Scope.Thread) + public static class JavaState { + JavaService service; + + @Setup(Level.Trial) + public void setup() { + service = new JavaService(); + } + } + + public static class JavaService { + public String getName() { return "JavaService"; } + } + + @Benchmark + public Object java_baseline(JavaState state, Blackhole bh) { + Object result = state.service.getName(); + bh.consume(result); + return result; + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DomainObjects.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DomainObjects.groovy new file mode 100644 index 0000000000..573c4c3113 --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DomainObjects.groovy @@ -0,0 +1,377 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.bench.orm + +/** + * Domain object classes that simulate GORM/Grails domain patterns. + * These are used to benchmark property access, relationship traversal, + * and dynamic finder-like patterns. + */ + +/** + * Base trait for domain objects with common patterns. + */ +trait DomainEntity { + Long id + Long version + Date dateCreated + Date lastUpdated + + // Simulate dirty checking + private Set<String> dirtyProperties = [] + + void markDirty(String propertyName) { + dirtyProperties.add(propertyName) + } + + boolean isDirty() { + !dirtyProperties.isEmpty() + } + + Set<String> getDirtyPropertyNames() { + dirtyProperties + } + + void clearDirty() { + dirtyProperties.clear() + } +} + +/** + * Person domain class with relationships. + */ +class PersonDomain implements DomainEntity { + String firstName + String lastName + String email + Integer age + Boolean active = true + + AddressDomain address + List<OrderDomain> orders = [] + + String getFullName() { + "$firstName $lastName" + } + + String getDisplayName() { + active ? fullName : "[$fullName] (inactive)" + } + + // Simulate GORM dynamic finder + static PersonDomain findByEmail(String email, List<PersonDomain> all) { + all.find { it.email == email } + } + + static List<PersonDomain> findAllByActive(Boolean active, List<PersonDomain> all) { + all.findAll { it.active == active } + } + + static List<PersonDomain> findAllByLastNameLike(String pattern, List<PersonDomain> all) { + all.findAll { it.lastName?.contains(pattern) } + } +} + +/** + * Address domain class (embedded-like). + */ +class AddressDomain implements DomainEntity { + String street + String city + String state + String zipCode + String country + + String getFullAddress() { + "$street, $city, $state $zipCode, $country" + } + + String getShortAddress() { + "$city, $state" + } +} + +/** + * Order domain class with line items. + */ +class OrderDomain implements DomainEntity { + String orderNumber + Date orderDate + String status + PersonDomain customer + + List<OrderItemDomain> items = [] + + BigDecimal getTotal() { + items.sum { it.lineTotal } ?: 0.0 + } + + Integer getItemCount() { + items.size() + } + + // Simulate adding item + void addItem(ProductDomain product, Integer quantity) { + def item = new OrderItemDomain( + order: this, + product: product, + quantity: quantity, + unitPrice: product.price + ) + items.add(item) + } +} + +/** + * Order item domain class. + */ +class OrderItemDomain implements DomainEntity { + OrderDomain order + ProductDomain product + Integer quantity + BigDecimal unitPrice + + BigDecimal getLineTotal() { + (unitPrice ?: 0.0) * (quantity ?: 0) + } + + String getDescription() { + "${product?.name} x $quantity" + } +} + +/** + * Product domain class. + */ +class ProductDomain implements DomainEntity { + String sku + String name + String description + BigDecimal price + Integer stockQuantity + CategoryDomain category + + Boolean isInStock() { + stockQuantity > 0 + } + + String getDisplayPrice() { + "\$${price?.setScale(2)}" + } +} + +/** + * Category domain class with hierarchy. + */ +class CategoryDomain implements DomainEntity { + String name + String code + CategoryDomain parent + List<CategoryDomain> children = [] + + String getFullPath() { + parent ? "${parent.fullPath} > $name" : name + } + + List<CategoryDomain> getAncestors() { + def result = [] + def current = parent + while (current) { + result.add(0, current) + current = current.parent + } + result + } +} + +/** + * Factory for creating test domain objects. + */ +class DomainFactory { + + static PersonDomain createPerson(int index) { + def person = new PersonDomain( + id: index, + firstName: "First$index", + lastName: "Last$index", + email: "[email protected]", + age: 20 + (index % 50), + active: index % 10 != 0, + dateCreated: new Date(), + lastUpdated: new Date() + ) + + person.address = createAddress(index) + return person + } + + static AddressDomain createAddress(int index) { + new AddressDomain( + id: index, + street: "$index Main Street", + city: "City${index % 100}", + state: ['CA', 'NY', 'TX', 'FL', 'WA'][index % 5], + zipCode: String.format("%05d", index % 100000), + country: "USA", + dateCreated: new Date(), + lastUpdated: new Date() + ) + } + + static ProductDomain createProduct(int index) { + new ProductDomain( + id: index, + sku: "SKU-$index", + name: "Product $index", + description: "Description for product $index", + price: 10.0 + (index % 100), + stockQuantity: index % 2 == 0 ? index : 0, + dateCreated: new Date(), + lastUpdated: new Date() + ) + } + + static OrderDomain createOrder(PersonDomain customer, List<ProductDomain> products, int index) { + def order = new OrderDomain( + id: index, + orderNumber: "ORD-${String.format("%06d", index)}", + orderDate: new Date(), + status: ['PENDING', 'PROCESSING', 'SHIPPED', 'DELIVERED'][index % 4], + customer: customer, + dateCreated: new Date(), + lastUpdated: new Date() + ) + + // Add 1-5 items per order + int itemCount = 1 + (index % 5) + for (int i = 0; i < itemCount && i < products.size(); i++) { + order.addItem(products[(index + i) % products.size()], 1 + (i % 3)) + } + + return order + } + + static CategoryDomain createCategoryHierarchy(int depth, int breadth) { + def root = new CategoryDomain(id: 0, name: "Root", code: "ROOT") + createCategoryChildren(root, depth, breadth, 1) + return root + } + + private static int createCategoryChildren(CategoryDomain parent, int depth, int breadth, int startId) { + if (depth <= 0) return startId + + int currentId = startId + for (int i = 0; i < breadth; i++) { + def child = new CategoryDomain( + id: currentId, + name: "${parent.name}-Child$i", + code: "${parent.code}-C$i", + parent: parent + ) + parent.children.add(child) + currentId++ + currentId = createCategoryChildren(child, depth - 1, breadth, currentId) + } + return currentId + } +} + +/** + * Operations that simulate common GORM/Grails patterns. + */ +class DomainOperations { + + /** + * Simulate view rendering: access multiple properties. + */ + static Map renderPersonView(PersonDomain person) { + [ + fullName: person.fullName, + email: person.email, + age: person.age, + active: person.active, + address: person.address?.shortAddress, + orderCount: person.orders?.size() ?: 0 + ] + } + + /** + * Simulate list view: access properties on collection. + */ + static List<Map> renderPersonList(List<PersonDomain> people) { + people.collect { person -> + [ + id: person.id, + name: person.fullName, + email: person.email, + active: person.active + ] + } + } + + /** + * Simulate order summary calculation. + */ + static Map calculateOrderSummary(OrderDomain order) { + [ + orderNumber: order.orderNumber, + customerName: order.customer?.fullName, + itemCount: order.itemCount, + total: order.total, + status: order.status + ] + } + + /** + * Simulate report generation with aggregation. + */ + static Map generateCustomerReport(PersonDomain person) { + def orders = person.orders + [ + customer: person.fullName, + totalOrders: orders.size(), + totalSpent: orders.sum { it.total } ?: 0.0, + averageOrderValue: orders ? (orders.sum { it.total } / orders.size()) : 0.0, + lastOrderDate: orders.max { it.orderDate }?.orderDate + ] + } + + /** + * Traverse category hierarchy. + */ + static List<String> getCategoryPath(CategoryDomain category) { + category.ancestors*.name + [category.name] + } + + /** + * Deep graph traversal. + */ + static List<Map> getOrderDetails(OrderDomain order) { + order.items.collect { item -> + [ + product: item.product.name, + sku: item.product.sku, + category: item.product.category?.fullPath, + quantity: item.quantity, + unitPrice: item.unitPrice, + lineTotal: item.lineTotal + ] + } + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/PropertyAccessBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/PropertyAccessBench.java new file mode 100644 index 0000000000..a7b76f81dc --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/PropertyAccessBench.java @@ -0,0 +1,447 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.bench.orm; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for measuring property access patterns common in GORM/Grails. + * <p> + * Property access is one of the most frequent operations in Grails applications, + * especially in views (GSP) where domain object properties are rendered. + * This benchmark tests: + * <ul> + * <li>Simple property getters</li> + * <li>Computed properties (derived from other properties)</li> + * <li>Nested property access (object graphs)</li> + * <li>Collection property access (spread operator)</li> + * <li>Null-safe navigation</li> + * </ul> + * <p> + * Run with: ./gradlew -Pindy=true -PbenchInclude=PropertyAccess :perf:jmh + */ +@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(3) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class PropertyAccessBench { + + // ======================================================================== + // State classes + // ======================================================================== + + @State(Scope.Thread) + public static class SingleObjectState { + PersonDomain person; + OrderDomain order; + + @Setup(Level.Trial) + public void setup() { + person = DomainFactory.createPerson(1); + person.setAddress(DomainFactory.createAddress(1)); + + // Create products and order + List<ProductDomain> products = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + products.add(DomainFactory.createProduct(i)); + } + + order = DomainFactory.createOrder(person, products, 1); + person.getOrders().add(order); + } + } + + @State(Scope.Thread) + public static class CollectionState { + List<PersonDomain> people; + List<OrderDomain> orders; + List<ProductDomain> products; + + @Setup(Level.Trial) + public void setup() { + // Create products first + products = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + products.add(DomainFactory.createProduct(i)); + } + + // Create people with addresses + people = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + PersonDomain person = DomainFactory.createPerson(i); + person.setAddress(DomainFactory.createAddress(i)); + people.add(person); + } + + // Create orders + orders = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + OrderDomain order = DomainFactory.createOrder(people.get(i % people.size()), products, i); + orders.add(order); + people.get(i % people.size()).getOrders().add(order); + } + } + } + + @State(Scope.Thread) + public static class HierarchyState { + CategoryDomain rootCategory; + List<CategoryDomain> leafCategories; + + @Setup(Level.Trial) + public void setup() { + // Create a category hierarchy: 3 levels deep, 4 children per level + rootCategory = DomainFactory.createCategoryHierarchy(3, 4); + + // Collect leaf categories + leafCategories = new ArrayList<>(); + collectLeaves(rootCategory, leafCategories); + } + + private void collectLeaves(CategoryDomain category, List<CategoryDomain> leaves) { + if (category.getChildren().isEmpty()) { + leaves.add(category); + } else { + for (Object child : category.getChildren()) { + collectLeaves((CategoryDomain) child, leaves); + } + } + } + } + + // ======================================================================== + // Simple property access benchmarks + // ======================================================================== + + /** + * Simple getter access. + */ + @Benchmark + public void simple_singleGetter(SingleObjectState state, Blackhole bh) { + bh.consume(state.person.getFirstName()); + } + + /** + * Multiple getter access on same object. + */ + @Benchmark + public void simple_multipleGetters(SingleObjectState state, Blackhole bh) { + bh.consume(state.person.getFirstName()); + bh.consume(state.person.getLastName()); + bh.consume(state.person.getEmail()); + bh.consume(state.person.getAge()); + bh.consume(state.person.getActive()); + } + + /** + * Computed property (calls other getters internally). + */ + @Benchmark + public void simple_computedProperty(SingleObjectState state, Blackhole bh) { + bh.consume(state.person.getFullName()); + } + + /** + * Computed property with conditional logic. + */ + @Benchmark + public void simple_computedWithLogic(SingleObjectState state, Blackhole bh) { + bh.consume(state.person.getDisplayName()); + } + + // ======================================================================== + // Nested property access benchmarks + // ======================================================================== + + /** + * One level of nesting: person.address.city + */ + @Benchmark + public void nested_oneLevel(SingleObjectState state, Blackhole bh) { + bh.consume(state.person.getAddress().getCity()); + } + + /** + * Multiple nested accesses. + */ + @Benchmark + public void nested_multipleAccess(SingleObjectState state, Blackhole bh) { + AddressDomain addr = state.person.getAddress(); + bh.consume(addr.getStreet()); + bh.consume(addr.getCity()); + bh.consume(addr.getState()); + bh.consume(addr.getZipCode()); + } + + /** + * Computed nested property. + */ + @Benchmark + public void nested_computedProperty(SingleObjectState state, Blackhole bh) { + bh.consume(state.person.getAddress().getFullAddress()); + } + + /** + * Deep nesting: order -> items -> product -> category + */ + @Benchmark + public void nested_deepAccess(SingleObjectState state, Blackhole bh) { + for (Object item : state.order.getItems()) { + OrderItemDomain orderItem = (OrderItemDomain) item; + ProductDomain product = orderItem.getProduct(); + if (product != null && product.getCategory() != null) { + bh.consume(product.getCategory().getName()); + } + } + } + + // ======================================================================== + // Collection property access benchmarks + // ======================================================================== + + /** + * Iterate and access single property. + */ + @Benchmark + @OperationsPerInvocation(100) + public void collection_iterateSingleProp(CollectionState state, Blackhole bh) { + for (PersonDomain person : state.people) { + bh.consume(person.getFirstName()); + } + } + + /** + * Iterate and access multiple properties. + */ + @Benchmark + @OperationsPerInvocation(100) + public void collection_iterateMultipleProps(CollectionState state, Blackhole bh) { + for (PersonDomain person : state.people) { + bh.consume(person.getFirstName()); + bh.consume(person.getLastName()); + bh.consume(person.getEmail()); + } + } + + /** + * Spread operator access (Groovy specific). + */ + @Benchmark + public void collection_spreadOperator(CollectionState state, Blackhole bh) { + List<?> result = PropertyAccessHelper.spreadFirstName(state.people); + bh.consume(result); + } + + /** + * Collect with property access. + */ + @Benchmark + public void collection_collectProperty(CollectionState state, Blackhole bh) { + List<?> result = PropertyAccessHelper.collectFullNames(state.people); + bh.consume(result); + } + + /** + * Find with property comparison. + */ + @Benchmark + public void collection_findByProperty(CollectionState state, Blackhole bh) { + Object result = PropertyAccessHelper.findByEmail(state.people, "[email protected]"); + bh.consume(result); + } + + /** + * FindAll with property filter. + */ + @Benchmark + public void collection_findAllByProperty(CollectionState state, Blackhole bh) { + List<?> result = PropertyAccessHelper.findAllActive(state.people); + bh.consume(result); + } + + // ======================================================================== + // View rendering simulation benchmarks + // ======================================================================== + + /** + * Simulate rendering a single object view. + */ + @Benchmark + public void view_renderSingle(SingleObjectState state, Blackhole bh) { + Map<?, ?> result = DomainOperations.renderPersonView(state.person); + bh.consume(result); + } + + /** + * Simulate rendering a list view. + */ + @Benchmark + public void view_renderList(CollectionState state, Blackhole bh) { + List<?> result = DomainOperations.renderPersonList(state.people); + bh.consume(result); + } + + /** + * Simulate rendering order with nested data. + */ + @Benchmark + public void view_renderOrderDetails(SingleObjectState state, Blackhole bh) { + List<?> result = DomainOperations.getOrderDetails(state.order); + bh.consume(result); + } + + /** + * Simulate generating a report with aggregation. + */ + @Benchmark + public void view_generateReport(SingleObjectState state, Blackhole bh) { + Map<?, ?> result = DomainOperations.generateCustomerReport(state.person); + bh.consume(result); + } + + // ======================================================================== + // Hierarchy traversal benchmarks + // ======================================================================== + + /** + * Traverse up hierarchy (parent chain). + */ + @Benchmark + @OperationsPerInvocation(64) + public void hierarchy_traverseUp(HierarchyState state, Blackhole bh) { + for (CategoryDomain leaf : state.leafCategories) { + List<?> path = DomainOperations.getCategoryPath(leaf); + bh.consume(path); + } + } + + /** + * Computed property with hierarchy traversal. + */ + @Benchmark + @OperationsPerInvocation(64) + public void hierarchy_computedPath(HierarchyState state, Blackhole bh) { + for (CategoryDomain leaf : state.leafCategories) { + bh.consume(leaf.getFullPath()); + } + } + + // ======================================================================== + // Java baseline benchmarks + // ======================================================================== + + public static class JavaPerson { + private String firstName; + private String lastName; + private String email; + private JavaAddress address; + + public String getFirstName() { return firstName; } + public void setFirstName(String v) { firstName = v; } + public String getLastName() { return lastName; } + public void setLastName(String v) { lastName = v; } + public String getEmail() { return email; } + public void setEmail(String v) { email = v; } + public JavaAddress getAddress() { return address; } + public void setAddress(JavaAddress v) { address = v; } + public String getFullName() { return firstName + " " + lastName; } + } + + public static class JavaAddress { + private String city; + private String state; + + public String getCity() { return city; } + public void setCity(String v) { city = v; } + public String getState() { return state; } + public void setState(String v) { state = v; } + } + + @State(Scope.Thread) + public static class JavaState { + JavaPerson person; + List<JavaPerson> people; + + @Setup(Level.Trial) + public void setup() { + person = new JavaPerson(); + person.setFirstName("John"); + person.setLastName("Doe"); + person.setEmail("[email protected]"); + JavaAddress addr = new JavaAddress(); + addr.setCity("NYC"); + addr.setState("NY"); + person.setAddress(addr); + + people = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + JavaPerson p = new JavaPerson(); + p.setFirstName("First" + i); + p.setLastName("Last" + i); + p.setEmail("user" + i + "@example.com"); + JavaAddress a = new JavaAddress(); + a.setCity("City" + i); + a.setState("ST"); + p.setAddress(a); + people.add(p); + } + } + } + + @Benchmark + public void java_singleGetter(JavaState state, Blackhole bh) { + bh.consume(state.person.getFirstName()); + } + + @Benchmark + public void java_multipleGetters(JavaState state, Blackhole bh) { + bh.consume(state.person.getFirstName()); + bh.consume(state.person.getLastName()); + bh.consume(state.person.getEmail()); + } + + @Benchmark + public void java_nestedAccess(JavaState state, Blackhole bh) { + bh.consume(state.person.getAddress().getCity()); + } + + @Benchmark + @OperationsPerInvocation(100) + public void java_collectionIterate(JavaState state, Blackhole bh) { + for (JavaPerson p : state.people) { + bh.consume(p.getFirstName()); + } + } + + @Benchmark + public void java_collectionCollect(JavaState state, Blackhole bh) { + List<String> result = new ArrayList<>(); + for (JavaPerson p : state.people) { + result.add(p.getFullName()); + } + bh.consume(result); + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/PropertyAccessHelper.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/PropertyAccessHelper.groovy new file mode 100644 index 0000000000..9e16a852e3 --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/PropertyAccessHelper.groovy @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.bench.orm + +/** + * Helper class for property access benchmarks. + * Contains Groovy-specific operations like spread operator, collect, find, etc. + */ +class PropertyAccessHelper { + + /** + * Spread operator to get all firstNames. + * Equivalent to: people*.firstName + */ + static List<String> spreadFirstName(List<PersonDomain> people) { + people*.firstName + } + + /** + * Spread operator for computed property. + */ + static List<String> spreadFullName(List<PersonDomain> people) { + people*.fullName + } + + /** + * Collect with closure accessing property. + */ + static List<String> collectFullNames(List<PersonDomain> people) { + people.collect { it.fullName } + } + + /** + * Collect with multiple property accesses. + */ + static List<Map> collectMultipleProps(List<PersonDomain> people) { + people.collect { person -> + [ + name: person.fullName, + email: person.email, + city: person.address?.city + ] + } + } + + /** + * Find by property value. + */ + static PersonDomain findByEmail(List<PersonDomain> people, String email) { + people.find { it.email == email } + } + + /** + * FindAll with property filter. + */ + static List<PersonDomain> findAllActive(List<PersonDomain> people) { + people.findAll { it.active } + } + + /** + * FindAll with nested property access. + */ + static List<PersonDomain> findAllInState(List<PersonDomain> people, String state) { + people.findAll { it.address?.state == state } + } + + /** + * Chained collection operations. + */ + static List<String> chainedOperations(List<PersonDomain> people) { + people.findAll { it.active } + .collect { it.fullName } + .sort() + } + + /** + * Group by property. + */ + static Map<String, List<PersonDomain>> groupByState(List<PersonDomain> people) { + people.groupBy { it.address?.state } + } + + /** + * Sum with property access. + */ + static BigDecimal sumOrderTotals(List<OrderDomain> orders) { + orders.sum { it.total } ?: 0.0 + } + + /** + * Nested spread operator. + */ + static List<String> nestedSpread(List<OrderDomain> orders) { + orders*.customer*.fullName + } + + /** + * Deep property access with null safety. + */ + static List<String> safeNestedAccess(List<OrderDomain> orders) { + orders.collect { order -> + order?.customer?.address?.city ?: 'Unknown' + } + } + + /** + * Multiple levels of spread. + */ + static List<List<String>> multiLevelSpread(List<OrderDomain> orders) { + orders*.items*.product*.name + } + + /** + * Dynamic property access by name. + */ + static List<Object> dynamicPropertyAccess(List<PersonDomain> people, String propName) { + people.collect { it."$propName" } + } + + /** + * GString interpolation with property access. + */ + static List<String> formatWithGString(List<PersonDomain> people) { + people.collect { person -> + "Name: ${person.fullName}, Email: ${person.email}, Location: ${person.address?.shortAddress ?: 'N/A'}" + } + } + + /** + * Elvis operator for defaults. + */ + static List<String> withElvisDefaults(List<PersonDomain> people) { + people.collect { person -> + person.email ?: "${person.firstName}.${person.lastName}@default.com".toLowerCase() + } + } + + /** + * Simulate dynamic finder pattern. + */ + static List<PersonDomain> findAllByLastNameLike(List<PersonDomain> people, String pattern) { + people.findAll { it.lastName?.contains(pattern) } + } + + /** + * Simulate GORM criteria-like filtering. + */ + static List<PersonDomain> filterByCriteria(List<PersonDomain> people, Map criteria) { + people.findAll { person -> + criteria.every { key, value -> + if (value instanceof Closure) { + value(person."$key") + } else { + person."$key" == value + } + } + } + } +}
