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 cdab2aa5ffd6f129adc7eac836384829d58c4f5d Author: Jonny Carter <[email protected]> AuthorDate: Fri Jan 30 18:16:16 2026 -0600 Add further benchmarks --- .../bench/dispatch/InterfaceDispatchBench.java | 165 +++++++++ .../bench/dispatch/InterfaceDispatchHelper.groovy | 264 ++++++++++++++ .../bench/dispatch/MixedTypeCollectionBench.java | 213 ++++++++++++ .../groovy/bench/dispatch/MixedTypeHelper.groovy | 241 +++++++++++++ .../bench/orm/CollectionOperationsBench.java | 216 ++++++++++++ .../apache/groovy/bench/orm/CollectionOps.groovy | 195 +++++++++++ .../groovy/bench/orm/DynamicFinderBench.java | 166 +++++++++ .../groovy/bench/orm/DynamicFinderHelper.groovy | 108 ++++++ .../groovy/bench/orm/DynamicFinderService.groovy | 122 +++++++ .../org/apache/groovy/bench/orm/EntityGraph.groovy | 217 ++++++++++++ .../groovy/bench/orm/EntityTraversalBench.java | 128 +++++++ .../bench/profiling/FlameGraphGenerator.java | 385 +++++++++++++++++++++ .../src/jmh/resources/workloads/cold-startup.json | 42 +++ .../jmh/resources/workloads/dispatch-stress.json | 41 +++ .../jmh/resources/workloads/grails-typical.json | 48 +++ .../jmh/resources/workloads/memory-intensive.json | 44 +++ 16 files changed, 2595 insertions(+) diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/InterfaceDispatchBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/InterfaceDispatchBench.java new file mode 100644 index 0000000000..928541216f --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/InterfaceDispatchBench.java @@ -0,0 +1,165 @@ +/* + * 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.*; +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for interface-based polymorphic dispatch. + * <p> + * Tests scenarios where objects are accessed through interface types, + * which is common in service layers and plugin architectures. + * <p> + * Run with: ./gradlew -Pindy=true -PbenchInclude=InterfaceDispatch :perf:jmh + */ +@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(2) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class InterfaceDispatchBench { + + private static final int ITERATIONS = 1000; + + // ======================================================================== + // State classes + // ======================================================================== + + @State(Scope.Thread) + public static class InterfaceState { + List<InterfaceService> services; + List<InterfaceProcessor> processors; + List<InterfaceValidator> validators; + + @Setup(Level.Trial) + public void setup() { + services = InterfaceDispatchHelper.createServices(100); + processors = InterfaceDispatchHelper.createProcessors(100); + validators = InterfaceDispatchHelper.createValidators(100); + } + } + + // ======================================================================== + // Single interface dispatch + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(100) + public void interface_singleMethod(InterfaceState state, Blackhole bh) { + InterfaceDispatchHelper.callExecute(state.services, bh); + } + + @Benchmark + @OperationsPerInvocation(100) + public void interface_multipleMethodsSameInterface(InterfaceState state, Blackhole bh) { + InterfaceDispatchHelper.callMultipleMethods(state.services, bh); + } + + // ======================================================================== + // Multiple interfaces + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(300) + public void interface_multipleInterfaces(InterfaceState state, Blackhole bh) { + InterfaceDispatchHelper.callExecute(state.services, bh); + InterfaceDispatchHelper.callProcess(state.processors, bh); + InterfaceDispatchHelper.callValidate(state.validators, bh); + } + + // ======================================================================== + // Typed vs untyped dispatch + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(100) + public void interface_typedDispatch(InterfaceState state, Blackhole bh) { + InterfaceDispatchHelper.typedDispatch(state.services, bh); + } + + @Benchmark + @OperationsPerInvocation(100) + public void interface_untypedDispatch(InterfaceState state, Blackhole bh) { + InterfaceDispatchHelper.untypedDispatch(state.services, bh); + } + + // ======================================================================== + // Interface with default methods (Java 8+) + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(100) + public void interface_defaultMethod(InterfaceState state, Blackhole bh) { + InterfaceDispatchHelper.callDefaultMethod(state.services, bh); + } + + // ======================================================================== + // Chained interface calls + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(100) + public void interface_chainedCalls(InterfaceState state, Blackhole bh) { + InterfaceDispatchHelper.chainedInterfaceCalls(state.services, state.processors, bh); + } + + // ======================================================================== + // Collection operations through interface + // ======================================================================== + + @Benchmark + public Object interface_collectViaInterface(InterfaceState state, Blackhole bh) { + Object result = InterfaceDispatchHelper.collectResults(state.services); + bh.consume(result); + return result; + } + + @Benchmark + public Object interface_filterViaInterface(InterfaceState state, Blackhole bh) { + Object result = InterfaceDispatchHelper.filterByStatus(state.services); + bh.consume(result); + return result; + } + + // ======================================================================== + // Java baseline (pure invokeinterface) + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(100) + public void java_interfaceDispatch(InterfaceState state, Blackhole bh) { + for (InterfaceService svc : state.services) { + bh.consume(svc.execute("test")); + } + } + + @Benchmark + @OperationsPerInvocation(100) + public void java_multipleInterfaceMethods(InterfaceState state, Blackhole bh) { + for (InterfaceService svc : state.services) { + bh.consume(svc.execute("test")); + bh.consume(svc.getName()); + bh.consume(svc.isEnabled()); + } + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/InterfaceDispatchHelper.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/InterfaceDispatchHelper.groovy new file mode 100644 index 0000000000..4aa2f5eeda --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/InterfaceDispatchHelper.groovy @@ -0,0 +1,264 @@ +/* + * 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 for interface dispatch benchmarks. + * Provides interfaces and implementations that exercise polymorphic dispatch. + */ +class InterfaceDispatchHelper { + + /** + * Create a list of services with different implementations. + */ + static List<InterfaceService> createServices(int count) { + def result = [] + count.times { i -> + switch (i % 4) { + case 0: result << new ServiceImplA(name: "ServiceA$i", enabled: true); break + case 1: result << new ServiceImplB(name: "ServiceB$i", enabled: i % 2 == 0); break + case 2: result << new ServiceImplC(name: "ServiceC$i", enabled: true); break + case 3: result << new ServiceImplD(name: "ServiceD$i", enabled: i % 3 == 0); break + } + } + result + } + + /** + * Create a list of processors with different implementations. + */ + static List<InterfaceProcessor> createProcessors(int count) { + def result = [] + count.times { i -> + switch (i % 3) { + case 0: result << new ProcessorImplA(); break + case 1: result << new ProcessorImplB(); break + case 2: result << new ProcessorImplC(); break + } + } + result + } + + /** + * Create a list of validators with different implementations. + */ + static List<InterfaceValidator> createValidators(int count) { + def result = [] + count.times { i -> + switch (i % 2) { + case 0: result << new ValidatorImplA(); break + case 1: result << new ValidatorImplB(); break + } + } + result + } + + /** + * Call execute() on all services. + */ + static void callExecute(List<InterfaceService> services, Blackhole bh) { + for (svc in services) { + bh.consume(svc.execute("test")) + } + } + + /** + * Call multiple methods on same interface. + */ + static void callMultipleMethods(List<InterfaceService> services, Blackhole bh) { + for (svc in services) { + bh.consume(svc.execute("test")) + bh.consume(svc.getName()) + bh.consume(svc.isEnabled()) + } + } + + /** + * Call process() on all processors. + */ + static void callProcess(List<InterfaceProcessor> processors, Blackhole bh) { + for (proc in processors) { + bh.consume(proc.process([1, 2, 3])) + } + } + + /** + * Call validate() on all validators. + */ + static void callValidate(List<InterfaceValidator> validators, Blackhole bh) { + for (val in validators) { + bh.consume(val.validate("test")) + } + } + + /** + * Typed dispatch - explicitly typed parameter. + */ + static void typedDispatch(List<InterfaceService> services, Blackhole bh) { + for (InterfaceService svc in services) { + bh.consume(svc.execute("test")) + } + } + + /** + * Untyped dispatch - def parameter. + */ + static void untypedDispatch(List services, Blackhole bh) { + for (def svc in services) { + bh.consume(svc.execute("test")) + } + } + + /** + * Call default method on interface. + */ + static void callDefaultMethod(List<InterfaceService> services, Blackhole bh) { + for (svc in services) { + bh.consume(svc.getDescription()) + } + } + + /** + * Chained interface calls. + */ + static void chainedInterfaceCalls(List<InterfaceService> services, List<InterfaceProcessor> processors, Blackhole bh) { + services.eachWithIndex { svc, i -> + def result = svc.execute("input") + def processor = processors[i % processors.size()] + bh.consume(processor.process([result])) + } + } + + /** + * Collect results via interface. + */ + static List collectResults(List<InterfaceService> services) { + services.collect { it.execute("collect") } + } + + /** + * Filter by status via interface. + */ + static List filterByStatus(List<InterfaceService> services) { + services.findAll { it.isEnabled() } + } +} + +// ============================================================================ +// Interface definitions +// ============================================================================ + +interface InterfaceService { + String execute(String input) + String getName() + boolean isEnabled() + + // Default method (Java 8+) + default String getDescription() { + return "Service: " + getName() + } +} + +interface InterfaceProcessor { + Object process(List items) +} + +interface InterfaceValidator { + boolean validate(Object input) +} + +// ============================================================================ +// Service implementations +// ============================================================================ + +class ServiceImplA implements InterfaceService { + String name + boolean enabled + + String execute(String input) { "A:$input" } + String getName() { name } + boolean isEnabled() { enabled } +} + +class ServiceImplB implements InterfaceService { + String name + boolean enabled + + String execute(String input) { "B:${input.toUpperCase()}" } + String getName() { name } + boolean isEnabled() { enabled } +} + +class ServiceImplC implements InterfaceService { + String name + boolean enabled + + String execute(String input) { "C:${input.reverse()}" } + String getName() { name } + boolean isEnabled() { enabled } +} + +class ServiceImplD implements InterfaceService { + String name + boolean enabled + + String execute(String input) { "D:${input.length()}" } + String getName() { name } + boolean isEnabled() { enabled } +} + +// ============================================================================ +// Processor implementations +// ============================================================================ + +class ProcessorImplA implements InterfaceProcessor { + Object process(List items) { + items.collect { it.toString() }.join(',') + } +} + +class ProcessorImplB implements InterfaceProcessor { + Object process(List items) { + items.sum { it.toString().length() } + } +} + +class ProcessorImplC implements InterfaceProcessor { + Object process(List items) { + [count: items.size(), items: items] + } +} + +// ============================================================================ +// Validator implementations +// ============================================================================ + +class ValidatorImplA implements InterfaceValidator { + boolean validate(Object input) { + input != null && input.toString().length() > 0 + } +} + +class ValidatorImplB implements InterfaceValidator { + boolean validate(Object input) { + input != null && input.toString().matches(/\w+/) + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/MixedTypeCollectionBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/MixedTypeCollectionBench.java new file mode 100644 index 0000000000..3ae85e53ed --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/MixedTypeCollectionBench.java @@ -0,0 +1,213 @@ +/* + * 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.*; +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for collections containing mixed types. + * <p> + * This is common in Groovy/Grails where lists contain objects of + * different domain classes (e.g., a list of search results with + * Person, Order, Product objects). + * <p> + * Tests the callsite cache behavior when receiver types vary. + * <p> + * Run with: ./gradlew -Pindy=true -PbenchInclude=MixedTypeCollection :perf:jmh + */ +@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(2) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class MixedTypeCollectionBench { + + private static final int COLLECTION_SIZE = 100; + + // ======================================================================== + // State classes with varying type diversity + // ======================================================================== + + @State(Scope.Thread) + public static class Types2State { + List<Object> objects; + + @Setup(Level.Trial) + public void setup() { + objects = MixedTypeHelper.createMixedCollection(COLLECTION_SIZE, 2); + } + } + + @State(Scope.Thread) + public static class Types5State { + List<Object> objects; + + @Setup(Level.Trial) + public void setup() { + objects = MixedTypeHelper.createMixedCollection(COLLECTION_SIZE, 5); + } + } + + @State(Scope.Thread) + public static class Types10State { + List<Object> objects; + + @Setup(Level.Trial) + public void setup() { + objects = MixedTypeHelper.createMixedCollection(COLLECTION_SIZE, 10); + } + } + + @State(Scope.Thread) + public static class Types20State { + List<Object> objects; + + @Setup(Level.Trial) + public void setup() { + objects = MixedTypeHelper.createMixedCollection(COLLECTION_SIZE, 20); + } + } + + // ======================================================================== + // Benchmarks with 2 types + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_2types_getName(Types2State state, Blackhole bh) { + MixedTypeHelper.callGetName(state.objects, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_2types_getValue(Types2State state, Blackhole bh) { + MixedTypeHelper.callGetValue(state.objects, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_2types_process(Types2State state, Blackhole bh) { + MixedTypeHelper.callProcess(state.objects, bh); + } + + // ======================================================================== + // Benchmarks with 5 types + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_5types_getName(Types5State state, Blackhole bh) { + MixedTypeHelper.callGetName(state.objects, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_5types_getValue(Types5State state, Blackhole bh) { + MixedTypeHelper.callGetValue(state.objects, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_5types_process(Types5State state, Blackhole bh) { + MixedTypeHelper.callProcess(state.objects, bh); + } + + // ======================================================================== + // Benchmarks with 10 types + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_10types_getName(Types10State state, Blackhole bh) { + MixedTypeHelper.callGetName(state.objects, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_10types_getValue(Types10State state, Blackhole bh) { + MixedTypeHelper.callGetValue(state.objects, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_10types_process(Types10State state, Blackhole bh) { + MixedTypeHelper.callProcess(state.objects, bh); + } + + // ======================================================================== + // Benchmarks with 20 types (beyond typical cache size) + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_20types_getName(Types20State state, Blackhole bh) { + MixedTypeHelper.callGetName(state.objects, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_20types_getValue(Types20State state, Blackhole bh) { + MixedTypeHelper.callGetValue(state.objects, bh); + } + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void mixed_20types_process(Types20State state, Blackhole bh) { + MixedTypeHelper.callProcess(state.objects, bh); + } + + // ======================================================================== + // Collection operations on mixed types + // ======================================================================== + + @Benchmark + public Object mixed_5types_collect(Types5State state, Blackhole bh) { + Object result = MixedTypeHelper.collectNames(state.objects); + bh.consume(result); + return result; + } + + @Benchmark + public Object mixed_10types_collect(Types10State state, Blackhole bh) { + Object result = MixedTypeHelper.collectNames(state.objects); + bh.consume(result); + return result; + } + + @Benchmark + public Object mixed_20types_collect(Types20State state, Blackhole bh) { + Object result = MixedTypeHelper.collectNames(state.objects); + bh.consume(result); + return result; + } + + // ======================================================================== + // Java interface baseline + // ======================================================================== + + @Benchmark + @OperationsPerInvocation(COLLECTION_SIZE) + public void java_interface_getName(Types10State state, Blackhole bh) { + MixedTypeHelper.javaInterfaceCall(state.objects, bh); + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/MixedTypeHelper.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/MixedTypeHelper.groovy new file mode 100644 index 0000000000..af63333c0a --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/dispatch/MixedTypeHelper.groovy @@ -0,0 +1,241 @@ +/* + * 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 for mixed type collection benchmarks. + */ +class MixedTypeHelper { + + // All type classes implement this interface for Java baseline + static interface Nameable { + String getName() + int getValue() + Object process() + } + + // Generate 20 different classes + static final List<Class> TYPE_CLASSES = [ + MixedType01, MixedType02, MixedType03, MixedType04, MixedType05, + MixedType06, MixedType07, MixedType08, MixedType09, MixedType10, + MixedType11, MixedType12, MixedType13, MixedType14, MixedType15, + MixedType16, MixedType17, MixedType18, MixedType19, MixedType20 + ] + + /** + * Create a collection with N different types. + */ + static List<Object> createMixedCollection(int size, int numTypes) { + def typesToUse = TYPE_CLASSES.take(numTypes) + def result = [] + size.times { i -> + def typeClass = typesToUse[i % numTypes] + result << typeClass.newInstance([name: "Item$i", value: i]) + } + result + } + + /** + * Call getName() on all objects (exercises callsite cache). + */ + static void callGetName(List objects, Blackhole bh) { + for (obj in objects) { + bh.consume(obj.getName()) + } + } + + /** + * Call getValue() on all objects. + */ + static void callGetValue(List objects, Blackhole bh) { + for (obj in objects) { + bh.consume(obj.getValue()) + } + } + + /** + * Call process() on all objects. + */ + static void callProcess(List objects, Blackhole bh) { + for (obj in objects) { + bh.consume(obj.process()) + } + } + + /** + * Collect names using spread operator. + */ + static List collectNames(List objects) { + objects*.getName() + } + + /** + * Java interface call for baseline. + */ + static void javaInterfaceCall(List objects, Blackhole bh) { + for (Object obj : objects) { + if (obj instanceof Nameable) { + bh.consume(((Nameable) obj).getName()) + } + } + } +} + +// 20 different classes with same interface +class MixedType01 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "01:$name" } +} + +class MixedType02 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "02:$name" } +} + +class MixedType03 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "03:$name" } +} + +class MixedType04 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "04:$name" } +} + +class MixedType05 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "05:$name" } +} + +class MixedType06 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "06:$name" } +} + +class MixedType07 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "07:$name" } +} + +class MixedType08 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "08:$name" } +} + +class MixedType09 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "09:$name" } +} + +class MixedType10 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "10:$name" } +} + +class MixedType11 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "11:$name" } +} + +class MixedType12 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "12:$name" } +} + +class MixedType13 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "13:$name" } +} + +class MixedType14 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "14:$name" } +} + +class MixedType15 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "15:$name" } +} + +class MixedType16 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "16:$name" } +} + +class MixedType17 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "17:$name" } +} + +class MixedType18 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "18:$name" } +} + +class MixedType19 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "19:$name" } +} + +class MixedType20 implements MixedTypeHelper.Nameable { + String name; int value + String getName() { name } + int getValue() { value } + Object process() { "20:$name" } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/CollectionOperationsBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/CollectionOperationsBench.java new file mode 100644 index 0000000000..53c380cdae --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/CollectionOperationsBench.java @@ -0,0 +1,216 @@ +/* + * 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.*; +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for Groovy collection operations on domain objects. + * <p> + * Tests common GORM/Grails patterns: + * - .each { } iteration + * - .collect { } transformation + * - .findAll { } filtering + * - .groupBy { } aggregation + * - .sum { }, .max { }, .min { } + * <p> + * Run with: ./gradlew -Pindy=true -PbenchInclude=CollectionOperations :perf:jmh + */ +@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(2) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class CollectionOperationsBench { + + private static final int COLLECTION_SIZE = 1000; + + @State(Scope.Thread) + public static class CollectionState { + CollectionOps ops; + List<CollectionDomain> domains; + + @Setup(Level.Trial) + public void setup() { + ops = new CollectionOps(); + domains = ops.createDomains(COLLECTION_SIZE); + } + } + + // ======================================================================== + // Basic iteration + // ======================================================================== + + @Benchmark + public void collection_each(CollectionState state, Blackhole bh) { + state.ops.doEach(state.domains, bh); + } + + @Benchmark + public void collection_eachWithIndex(CollectionState state, Blackhole bh) { + state.ops.doEachWithIndex(state.domains, bh); + } + + // ======================================================================== + // Transformation + // ======================================================================== + + @Benchmark + public Object collection_collect(CollectionState state, Blackhole bh) { + Object result = state.ops.doCollect(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object collection_collectNested(CollectionState state, Blackhole bh) { + Object result = state.ops.doCollectNested(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object collection_collectMany(CollectionState state, Blackhole bh) { + Object result = state.ops.doCollectMany(state.domains); + bh.consume(result); + return result; + } + + // ======================================================================== + // Filtering + // ======================================================================== + + @Benchmark + public Object collection_findAll(CollectionState state, Blackhole bh) { + Object result = state.ops.doFindAll(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object collection_find(CollectionState state, Blackhole bh) { + Object result = state.ops.doFind(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object collection_grep(CollectionState state, Blackhole bh) { + Object result = state.ops.doGrep(state.domains); + bh.consume(result); + return result; + } + + // ======================================================================== + // Aggregation + // ======================================================================== + + @Benchmark + public Object collection_groupBy(CollectionState state, Blackhole bh) { + Object result = state.ops.doGroupBy(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object collection_countBy(CollectionState state, Blackhole bh) { + Object result = state.ops.doCountBy(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object collection_sum(CollectionState state, Blackhole bh) { + Object result = state.ops.doSum(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object collection_max(CollectionState state, Blackhole bh) { + Object result = state.ops.doMax(state.domains); + bh.consume(result); + return result; + } + + // ======================================================================== + // Chained operations (common in Grails) + // ======================================================================== + + @Benchmark + public Object collection_chainedOps(CollectionState state, Blackhole bh) { + Object result = state.ops.doChainedOperations(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object collection_complexChain(CollectionState state, Blackhole bh) { + Object result = state.ops.doComplexChain(state.domains); + bh.consume(result); + return result; + } + + // ======================================================================== + // Sorting + // ======================================================================== + + @Benchmark + public Object collection_sort(CollectionState state, Blackhole bh) { + Object result = state.ops.doSort(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object collection_sortBy(CollectionState state, Blackhole bh) { + Object result = state.ops.doSortBy(state.domains); + bh.consume(result); + return result; + } + + // ======================================================================== + // Java Stream baseline + // ======================================================================== + + @Benchmark + public Object java_streamCollect(CollectionState state, Blackhole bh) { + Object result = state.ops.javaStreamCollect(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object java_streamFilter(CollectionState state, Blackhole bh) { + Object result = state.ops.javaStreamFilter(state.domains); + bh.consume(result); + return result; + } + + @Benchmark + public Object java_streamChained(CollectionState state, Blackhole bh) { + Object result = state.ops.javaStreamChained(state.domains); + bh.consume(result); + return result; + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/CollectionOps.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/CollectionOps.groovy new file mode 100644 index 0000000000..290943a583 --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/CollectionOps.groovy @@ -0,0 +1,195 @@ +/* + * 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.infra.Blackhole +import java.util.stream.Collectors + +/** + * Collection operations helper for benchmarks. + */ +class CollectionOps { + + List<CollectionDomain> createDomains(int count) { + def random = new Random(42) + def categories = ['A', 'B', 'C', 'D', 'E'] + def statuses = ['ACTIVE', 'INACTIVE', 'PENDING', 'ARCHIVED'] + + (0..<count).collect { i -> + new CollectionDomain( + id: i, + name: "Domain $i", + category: categories[random.nextInt(categories.size())], + status: statuses[random.nextInt(statuses.size())], + value: random.nextInt(10000), + tags: (0..<(random.nextInt(5) + 1)).collect { "tag$it" } + ) + } + } + + // ======================================================================== + // Basic iteration + // ======================================================================== + + void doEach(List<CollectionDomain> domains, Blackhole bh) { + domains.each { domain -> + bh.consume(domain.name) + bh.consume(domain.value) + } + } + + void doEachWithIndex(List<CollectionDomain> domains, Blackhole bh) { + domains.eachWithIndex { domain, idx -> + bh.consume(domain.name) + bh.consume(idx) + } + } + + // ======================================================================== + // Transformation + // ======================================================================== + + List doCollect(List<CollectionDomain> domains) { + domains.collect { it.name } + } + + List doCollectNested(List<CollectionDomain> domains) { + domains.collect { domain -> + [ + id: domain.id, + name: domain.name, + upperName: domain.name.toUpperCase(), + valueDoubled: domain.value * 2 + ] + } + } + + List doCollectMany(List<CollectionDomain> domains) { + domains.collectMany { it.tags } + } + + // ======================================================================== + // Filtering + // ======================================================================== + + List doFindAll(List<CollectionDomain> domains) { + domains.findAll { it.status == 'ACTIVE' && it.value > 5000 } + } + + CollectionDomain doFind(List<CollectionDomain> domains) { + domains.find { it.value > 9000 } + } + + List doGrep(List<CollectionDomain> domains) { + domains.grep { it.category == 'A' } + } + + // ======================================================================== + // Aggregation + // ======================================================================== + + Map doGroupBy(List<CollectionDomain> domains) { + domains.groupBy { it.category } + } + + Map doCountBy(List<CollectionDomain> domains) { + domains.countBy { it.status } + } + + def doSum(List<CollectionDomain> domains) { + domains.sum { it.value } + } + + CollectionDomain doMax(List<CollectionDomain> domains) { + domains.max { it.value } + } + + // ======================================================================== + // Chained operations + // ======================================================================== + + List doChainedOperations(List<CollectionDomain> domains) { + domains.findAll { it.status == 'ACTIVE' } + .collect { it.name } + .unique() + .sort() + } + + Map doComplexChain(List<CollectionDomain> domains) { + domains.findAll { it.value > 1000 } + .groupBy { it.category } + .collectEntries { category, items -> + [category, [ + count: items.size(), + total: items.sum { it.value }, + avg: items.sum { it.value } / items.size(), + names: items*.name + ]] + } + } + + // ======================================================================== + // Sorting + // ======================================================================== + + List doSort(List<CollectionDomain> domains) { + domains.sort { it.name } + } + + List doSortBy(List<CollectionDomain> domains) { + domains.sort { a, b -> b.value <=> a.value } + } + + // ======================================================================== + // Java Stream baseline + // ======================================================================== + + List<String> javaStreamCollect(List<CollectionDomain> domains) { + domains.stream() + .map { it.name } + .collect(Collectors.toList()) + } + + List<CollectionDomain> javaStreamFilter(List<CollectionDomain> domains) { + domains.stream() + .filter { it.status == 'ACTIVE' && it.value > 5000 } + .collect(Collectors.toList()) + } + + List<String> javaStreamChained(List<CollectionDomain> domains) { + domains.stream() + .filter { it.status == 'ACTIVE' } + .map { it.name } + .distinct() + .sorted() + .collect(Collectors.toList()) + } +} + +/** + * Domain class for collection operations. + */ +class CollectionDomain { + long id + String name + String category + String status + int value + List<String> tags = [] +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DynamicFinderBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DynamicFinderBench.java new file mode 100644 index 0000000000..3ca5e02c5c --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DynamicFinderBench.java @@ -0,0 +1,166 @@ +/* + * 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.*; +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks simulating GORM dynamic finder patterns. + * <p> + * Dynamic finders like findByName(), findAllByStatus() use methodMissing + * which exercises Groovy's dynamic dispatch heavily. + * <p> + * Run with: ./gradlew -Pindy=true -PbenchInclude=DynamicFinder :perf:jmh + */ +@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(2) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class DynamicFinderBench { + + @State(Scope.Thread) + public static class FinderState { + DynamicFinderService service; + Random random; + + @Setup(Level.Trial) + public void setup() { + random = new Random(42); + service = new DynamicFinderService(); + + // Seed with test data + for (int i = 0; i < 1000; i++) { + service.addPerson("First" + i, "Last" + (i % 100), "person" + i + "@example.com", i % 50); + } + } + } + + // ======================================================================== + // Single property finders + // ======================================================================== + + @Benchmark + public Object finder_findByEmail(FinderState state, Blackhole bh) { + Object result = DynamicFinderHelper.findByEmail(state.service, "[email protected]"); + bh.consume(result); + return result; + } + + @Benchmark + public Object finder_findByLastName(FinderState state, Blackhole bh) { + Object result = DynamicFinderHelper.findByLastName(state.service, "Last25"); + bh.consume(result); + return result; + } + + @Benchmark + public Object finder_findByAge(FinderState state, Blackhole bh) { + Object result = DynamicFinderHelper.findByAge(state.service, 30); + bh.consume(result); + return result; + } + + // ======================================================================== + // FindAll finders (return lists) + // ======================================================================== + + @Benchmark + public Object finder_findAllByLastName(FinderState state, Blackhole bh) { + Object result = DynamicFinderHelper.findAllByLastName(state.service, "Last25"); + bh.consume(result); + return result; + } + + @Benchmark + public Object finder_findAllByAge(FinderState state, Blackhole bh) { + Object result = DynamicFinderHelper.findAllByAge(state.service, 30); + bh.consume(result); + return result; + } + + // ======================================================================== + // Compound finders + // ======================================================================== + + @Benchmark + public Object finder_findByFirstNameAndLastName(FinderState state, Blackhole bh) { + Object result = DynamicFinderHelper.findByFirstNameAndLastName(state.service, "First25", "Last25"); + bh.consume(result); + return result; + } + + @Benchmark + public Object finder_findAllByLastNameOrAge(FinderState state, Blackhole bh) { + Object result = DynamicFinderHelper.findAllByLastNameOrAge(state.service, "Last25", 30); + bh.consume(result); + return result; + } + + // ======================================================================== + // Count finders + // ======================================================================== + + @Benchmark + public Object finder_countByLastName(FinderState state, Blackhole bh) { + Object result = DynamicFinderHelper.countByLastName(state.service, "Last25"); + bh.consume(result); + return result; + } + + @Benchmark + public Object finder_countByAge(FinderState state, Blackhole bh) { + Object result = DynamicFinderHelper.countByAge(state.service, 30); + bh.consume(result); + return result; + } + + // ======================================================================== + // Mixed finder patterns (realistic usage) + // ======================================================================== + + @Benchmark + public Object finder_mixedQueries(FinderState state, Blackhole bh) { + int idx = state.random.nextInt(100); + DynamicFinderHelper.mixedFinderOperations(state.service, idx, bh); + return idx; + } + + // ======================================================================== + // Java baseline (direct method calls) + // ======================================================================== + + @Benchmark + public Object java_findByEmail(FinderState state, Blackhole bh) { + Object result = state.service.javaFindByEmail("[email protected]"); + bh.consume(result); + return result; + } + + @Benchmark + public Object java_findAllByLastName(FinderState state, Blackhole bh) { + Object result = state.service.javaFindAllByLastName("Last25"); + bh.consume(result); + return result; + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DynamicFinderHelper.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DynamicFinderHelper.groovy new file mode 100644 index 0000000000..7f354e9249 --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DynamicFinderHelper.groovy @@ -0,0 +1,108 @@ +/* + * 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.infra.Blackhole + +/** + * Helper for invoking dynamic finder methods from Java benchmarks. + * Groovy code can call methodMissing-based dynamic methods directly. + */ +class DynamicFinderHelper { + + // ======================================================================== + // findByX methods + // ======================================================================== + + static Object findByEmail(DynamicFinderService service, String email) { + service.findByEmail(email) + } + + static Object findByLastName(DynamicFinderService service, String lastName) { + service.findByLastName(lastName) + } + + static Object findByAge(DynamicFinderService service, int age) { + service.findByAge(age) + } + + static Object findByFirstNameAndLastName(DynamicFinderService service, String firstName, String lastName) { + service.findByFirstNameAndLastName(firstName, lastName) + } + + // ======================================================================== + // findAllByX methods + // ======================================================================== + + static Object findAllByLastName(DynamicFinderService service, String lastName) { + service.findAllByLastName(lastName) + } + + static Object findAllByAge(DynamicFinderService service, int age) { + service.findAllByAge(age) + } + + static Object findAllByLastNameOrAge(DynamicFinderService service, String lastName, int age) { + service.findAllByLastNameOrAge(lastName, age) + } + + // ======================================================================== + // countByX methods + // ======================================================================== + + static Object countByLastName(DynamicFinderService service, String lastName) { + service.countByLastName(lastName) + } + + static Object countByAge(DynamicFinderService service, int age) { + service.countByAge(age) + } + + // ======================================================================== + // Mixed operations for benchmarks + // ======================================================================== + + static void mixedFinderOperations(DynamicFinderService service, int idx, Blackhole bh) { + bh.consume(service.findByEmail("person" + idx + "@example.com")) + bh.consume(service.findAllByLastName("Last" + (idx % 100))) + bh.consume(service.countByAge(idx % 50)) + } + + // ======================================================================== + // Batch operations + // ======================================================================== + + static void batchFindByEmail(DynamicFinderService service, int count, Blackhole bh) { + for (int i = 0; i < count; i++) { + bh.consume(service.findByEmail("person" + i + "@example.com")) + } + } + + static void batchFindAllByLastName(DynamicFinderService service, int count, Blackhole bh) { + for (int i = 0; i < count; i++) { + bh.consume(service.findAllByLastName("Last" + (i % 100))) + } + } + + static void batchCountByAge(DynamicFinderService service, int count, Blackhole bh) { + for (int i = 0; i < count; i++) { + bh.consume(service.countByAge(i % 50)) + } + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DynamicFinderService.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DynamicFinderService.groovy new file mode 100644 index 0000000000..5a260f59a8 --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/DynamicFinderService.groovy @@ -0,0 +1,122 @@ +/* + * 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 + +/** + * Service that implements GORM-like dynamic finders via methodMissing. + * This exercises Groovy's dynamic method dispatch heavily. + */ +class DynamicFinderService { + + List<FinderPerson> people = [] + + void addPerson(String firstName, String lastName, String email, int age) { + people << new FinderPerson( + firstName: firstName, + lastName: lastName, + email: email, + age: age + ) + } + + /** + * methodMissing implements dynamic finders like: + * - findByX(value) + * - findAllByX(value) + * - countByX(value) + * - findByXAndY(value1, value2) + * - findAllByXOrY(value1, value2) + */ + def methodMissing(String name, args) { + if (name.startsWith('findAllBy')) { + return handleFindAllBy(name.substring(9), args) + } else if (name.startsWith('findBy')) { + return handleFindBy(name.substring(6), args) + } else if (name.startsWith('countBy')) { + return handleCountBy(name.substring(7), args) + } + throw new MissingMethodException(name, this.class, args as Object[]) + } + + private Object handleFindBy(String expression, args) { + def predicate = buildPredicate(expression, args) + people.find(predicate) + } + + private List handleFindAllBy(String expression, args) { + def predicate = buildPredicate(expression, args) + people.findAll(predicate) + } + + private int handleCountBy(String expression, args) { + def predicate = buildPredicate(expression, args) + people.count(predicate) + } + + private Closure buildPredicate(String expression, args) { + // Handle compound expressions: XAndY, XOrY + if (expression.contains('And')) { + def parts = expression.split('And') + def prop1 = parts[0].uncapitalize() + def prop2 = parts[1].uncapitalize() + return { it."$prop1" == args[0] && it."$prop2" == args[1] } + } else if (expression.contains('Or')) { + def parts = expression.split('Or') + def prop1 = parts[0].uncapitalize() + def prop2 = parts[1].uncapitalize() + return { it."$prop1" == args[0] || it."$prop2" == args[1] } + } else { + // Simple property + def prop = expression.uncapitalize() + return { it."$prop" == args[0] } + } + } + + // Java-style direct methods for baseline comparison + FinderPerson javaFindByEmail(String email) { + for (FinderPerson p : people) { + if (email.equals(p.email)) { + return p + } + } + return null + } + + List<FinderPerson> javaFindAllByLastName(String lastName) { + List<FinderPerson> result = new ArrayList<>() + for (FinderPerson p : people) { + if (lastName.equals(p.lastName)) { + result.add(p) + } + } + return result + } +} + +/** + * Simple person class for finder tests. + */ +class FinderPerson { + String firstName + String lastName + String email + int age + + String getFullName() { "$firstName $lastName" } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/EntityGraph.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/EntityGraph.groovy new file mode 100644 index 0000000000..fbbb537b90 --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/EntityGraph.groovy @@ -0,0 +1,217 @@ +/* + * 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.infra.Blackhole + +/** + * Entity graph for traversal benchmarks. + * Simulates GORM-style object relationships. + */ +class EntityGraph { + + List<GraphCustomer> customers = [] + + void buildGraph(int customerCount) { + def random = new Random(42) + def categories = ['Electronics', 'Clothing', 'Books', 'Home', 'Sports'].collect { + new GraphCategory(name: it, code: it.take(3).toUpperCase()) + } + + customerCount.times { i -> + def customer = new GraphCustomer( + name: "Customer $i", + email: "customer${i}@example.com", + address: new GraphAddress( + street: "${i * 10} Main St", + city: "City${i % 10}", + country: new GraphCountry( + name: "Country${i % 5}", + code: "C${i % 5}" + ) + ) + ) + + // Add 1-5 orders per customer + (random.nextInt(5) + 1).times { j -> + def order = new GraphOrder( + orderNumber: "ORD-$i-$j", + customer: customer + ) + + // Add 1-10 items per order + (random.nextInt(10) + 1).times { k -> + def product = new GraphProduct( + name: "Product $i-$j-$k", + sku: "SKU-$i-$j-$k", + category: categories[random.nextInt(categories.size())] + ) + order.items << new GraphOrderItem( + product: product, + quantity: random.nextInt(5) + 1, + price: (random.nextInt(10000) + 100) / 100.0 + ) + } + + customer.orders << order + } + + customers << customer + } + } + + // ======================================================================== + // Traversal methods for benchmarks + // ======================================================================== + + void traverseSimpleProperty(Blackhole bh) { + for (customer in customers) { + bh.consume(customer.name) + bh.consume(customer.email) + } + } + + void traverseNestedProperty(Blackhole bh) { + for (customer in customers) { + bh.consume(customer.address.city) + bh.consume(customer.address.street) + } + } + + void traverseDeepProperty(Blackhole bh) { + for (customer in customers) { + bh.consume(customer.address.country.name) + bh.consume(customer.address.country.code) + } + } + + void traverseVeryDeepProperty(Blackhole bh) { + for (customer in customers) { + for (order in customer.orders) { + for (item in order.items) { + bh.consume(item.product.category.name) + } + } + } + } + + void traverseWithSpread(Blackhole bh) { + for (customer in customers) { + def orderNumbers = customer.orders*.orderNumber + bh.consume(orderNumbers) + } + } + + void traverseNestedSpread(Blackhole bh) { + for (customer in customers) { + def productNames = customer.orders*.items.flatten()*.product*.name + bh.consume(productNames) + } + } + + void traverseNullSafe(Blackhole bh) { + for (customer in customers) { + // Some might be null in real scenarios + bh.consume(customer?.address?.country?.name) + bh.consume(customer?.orders?.first()?.items?.first()?.product?.name) + } + } + + void traverseMixedPatterns(Blackhole bh) { + for (customer in customers) { + // Simple + bh.consume(customer.name) + + // Nested + bh.consume(customer.address.city) + + // Deep + bh.consume(customer.address.country.name) + + // Spread + bh.consume(customer.orders*.orderNumber) + + // Collection with closure + def totals = customer.orders.collect { order -> + order.items.sum { it.price * it.quantity } + } + bh.consume(totals) + } + } + + // ======================================================================== + // Java baseline methods + // ======================================================================== + + void javaTraverseSimple(Blackhole bh) { + for (GraphCustomer customer : customers) { + bh.consume(customer.getName()) + bh.consume(customer.getEmail()) + } + } + + void javaTraverseDeep(Blackhole bh) { + for (GraphCustomer customer : customers) { + bh.consume(customer.getAddress().getCountry().getName()) + bh.consume(customer.getAddress().getCountry().getCode()) + } + } +} + +// Domain classes for graph traversal +class GraphCustomer { + String name + String email + GraphAddress address + List<GraphOrder> orders = [] +} + +class GraphAddress { + String street + String city + GraphCountry country +} + +class GraphCountry { + String name + String code +} + +class GraphOrder { + String orderNumber + GraphCustomer customer + List<GraphOrderItem> items = [] +} + +class GraphOrderItem { + GraphProduct product + int quantity + BigDecimal price +} + +class GraphProduct { + String name + String sku + GraphCategory category +} + +class GraphCategory { + String name + String code +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/EntityTraversalBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/EntityTraversalBench.java new file mode 100644 index 0000000000..3357013eba --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/orm/EntityTraversalBench.java @@ -0,0 +1,128 @@ +/* + * 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.*; +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for object graph navigation patterns common in GORM/Grails. + * <p> + * Tests patterns like: + * - person.address.city + * - order.customer.address.country + * - order.items*.product.category + * <p> + * Run with: ./gradlew -Pindy=true -PbenchInclude=EntityTraversal :perf:jmh + */ +@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(2) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class EntityTraversalBench { + + @State(Scope.Thread) + public static class TraversalState { + EntityGraph graph; + + @Setup(Level.Trial) + public void setup() { + graph = new EntityGraph(); + graph.buildGraph(100); // 100 customers with orders + } + } + + // ======================================================================== + // Simple traversal (2 levels) + // ======================================================================== + + @Benchmark + public void traversal_simpleProperty(TraversalState state, Blackhole bh) { + state.graph.traverseSimpleProperty(bh); + } + + @Benchmark + public void traversal_nestedProperty(TraversalState state, Blackhole bh) { + state.graph.traverseNestedProperty(bh); + } + + // ======================================================================== + // Deep traversal (3+ levels) + // ======================================================================== + + @Benchmark + public void traversal_deepProperty(TraversalState state, Blackhole bh) { + state.graph.traverseDeepProperty(bh); + } + + @Benchmark + public void traversal_veryDeepProperty(TraversalState state, Blackhole bh) { + state.graph.traverseVeryDeepProperty(bh); + } + + // ======================================================================== + // Collection traversal with spread + // ======================================================================== + + @Benchmark + public void traversal_spreadOnCollection(TraversalState state, Blackhole bh) { + state.graph.traverseWithSpread(bh); + } + + @Benchmark + public void traversal_nestedSpread(TraversalState state, Blackhole bh) { + state.graph.traverseNestedSpread(bh); + } + + // ======================================================================== + // Null-safe traversal + // ======================================================================== + + @Benchmark + public void traversal_nullSafe(TraversalState state, Blackhole bh) { + state.graph.traverseNullSafe(bh); + } + + // ======================================================================== + // Mixed traversal patterns + // ======================================================================== + + @Benchmark + public void traversal_mixedPatterns(TraversalState state, Blackhole bh) { + state.graph.traverseMixedPatterns(bh); + } + + // ======================================================================== + // Java baseline + // ======================================================================== + + @Benchmark + public void java_simpleTraversal(TraversalState state, Blackhole bh) { + state.graph.javaTraverseSimple(bh); + } + + @Benchmark + public void java_deepTraversal(TraversalState state, Blackhole bh) { + state.graph.javaTraverseDeep(bh); + } +} diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/FlameGraphGenerator.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/FlameGraphGenerator.java new file mode 100644 index 0000000000..8b1df6db7d --- /dev/null +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/FlameGraphGenerator.java @@ -0,0 +1,385 @@ +/* + * 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.profiling; + +import java.io.*; +import java.nio.file.*; +import java.util.*; +import java.util.regex.*; + +/** + * Post-processes JFR output to generate flame graph compatible output. + * <p> + * This class converts JFR recording data to the collapsed stack format + * that can be consumed by flame graph tools like async-profiler's + * converter or Brendan Gregg's flamegraph.pl. + * <p> + * Usage: + * <pre> + * # First, convert JFR to text using jfr tool + * jfr print --events jdk.ExecutionSample recording.jfr > stacks.txt + * + * # Then use this tool to convert to collapsed format + * java FlameGraphGenerator stacks.txt > collapsed.txt + * + * # Finally generate SVG using flamegraph.pl + * flamegraph.pl collapsed.txt > profile.svg + * </pre> + */ +public class FlameGraphGenerator { + + private static final Pattern STACK_FRAME_PATTERN = + Pattern.compile("^\\s+(.+)$"); + + private static final Pattern EVENT_SEPARATOR_PATTERN = + Pattern.compile("^jdk\\.(ExecutionSample|NativeMethodSample|ObjectAllocationInNewTLAB|ObjectAllocationOutsideTLAB)"); + + /** + * Main entry point for command-line usage. + */ + public static void main(String[] args) throws Exception { + if (args.length < 1) { + printUsage(); + System.exit(1); + } + + String command = args[0]; + + switch (command) { + case "collapse": + if (args.length < 2) { + System.err.println("Usage: FlameGraphGenerator collapse <jfr-text-file>"); + System.exit(1); + } + collapseStacks(args[1], System.out); + break; + + case "filter": + if (args.length < 3) { + System.err.println("Usage: FlameGraphGenerator filter <collapsed-file> <pattern>"); + System.exit(1); + } + filterStacks(args[1], args[2], System.out); + break; + + case "top": + if (args.length < 2) { + System.err.println("Usage: FlameGraphGenerator top <collapsed-file> [n]"); + System.exit(1); + } + int n = args.length > 2 ? Integer.parseInt(args[2]) : 20; + topMethods(args[1], n, System.out); + break; + + case "diff": + if (args.length < 3) { + System.err.println("Usage: FlameGraphGenerator diff <baseline-file> <current-file>"); + System.exit(1); + } + diffProfiles(args[1], args[2], System.out); + break; + + default: + printUsage(); + System.exit(1); + } + } + + private static void printUsage() { + System.err.println("FlameGraphGenerator - JFR profile analysis tool for Groovy benchmarks"); + System.err.println(); + System.err.println("Commands:"); + System.err.println(" collapse <jfr-text> Convert JFR text output to collapsed stacks"); + System.err.println(" filter <collapsed> <pattern> Filter stacks matching pattern"); + System.err.println(" top <collapsed> [n] Show top n methods by sample count"); + System.err.println(" diff <baseline> <current> Show difference between profiles"); + System.err.println(); + System.err.println("Workflow:"); + System.err.println(" 1. jfr print --events jdk.ExecutionSample recording.jfr > stacks.txt"); + System.err.println(" 2. java FlameGraphGenerator collapse stacks.txt > collapsed.txt"); + System.err.println(" 3. flamegraph.pl collapsed.txt > profile.svg"); + System.err.println(); + System.err.println("Analysis:"); + System.err.println(" java FlameGraphGenerator filter collapsed.txt 'groovy' > groovy-only.txt"); + System.err.println(" java FlameGraphGenerator top collapsed.txt 30"); + System.err.println(" java FlameGraphGenerator diff baseline.txt current.txt"); + } + + /** + * Convert JFR text output to collapsed stack format. + */ + public static void collapseStacks(String inputFile, PrintStream out) throws IOException { + Map<String, Long> stackCounts = new LinkedHashMap<>(); + List<String> currentStack = new ArrayList<>(); + + try (BufferedReader reader = Files.newBufferedReader(Paths.get(inputFile))) { + String line; + while ((line = reader.readLine()) != null) { + Matcher eventMatcher = EVENT_SEPARATOR_PATTERN.matcher(line); + if (eventMatcher.find()) { + // Start of new event - save previous stack if any + if (!currentStack.isEmpty()) { + String collapsed = collapseStack(currentStack); + stackCounts.merge(collapsed, 1L, Long::sum); + currentStack.clear(); + } + continue; + } + + Matcher frameMatcher = STACK_FRAME_PATTERN.matcher(line); + if (frameMatcher.matches()) { + String frame = frameMatcher.group(1).trim(); + // Clean up frame format + frame = cleanFrame(frame); + if (!frame.isEmpty()) { + currentStack.add(frame); + } + } + } + + // Don't forget the last stack + if (!currentStack.isEmpty()) { + String collapsed = collapseStack(currentStack); + stackCounts.merge(collapsed, 1L, Long::sum); + } + } + + // Output in collapsed format + for (Map.Entry<String, Long> entry : stackCounts.entrySet()) { + out.println(entry.getKey() + " " + entry.getValue()); + } + } + + private static String collapseStack(List<String> stack) { + // Reverse because JFR shows leaf first, but flame graphs want root first + StringBuilder sb = new StringBuilder(); + for (int i = stack.size() - 1; i >= 0; i--) { + if (sb.length() > 0) { + sb.append(";"); + } + sb.append(stack.get(i)); + } + return sb.toString(); + } + + private static String cleanFrame(String frame) { + // Remove line numbers and source file info + frame = frame.replaceAll("\\(.*\\)", ""); + // Remove module prefixes like java.base/ + frame = frame.replaceAll("^[\\w.]+/", ""); + return frame.trim(); + } + + /** + * Filter stacks to only those matching a pattern. + */ + public static void filterStacks(String inputFile, String pattern, PrintStream out) throws IOException { + Pattern p = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + + try (BufferedReader reader = Files.newBufferedReader(Paths.get(inputFile))) { + String line; + while ((line = reader.readLine()) != null) { + if (p.matcher(line).find()) { + out.println(line); + } + } + } + } + + /** + * Show top methods by sample count. + */ + public static void topMethods(String inputFile, int n, PrintStream out) throws IOException { + Map<String, Long> methodCounts = new HashMap<>(); + + try (BufferedReader reader = Files.newBufferedReader(Paths.get(inputFile))) { + String line; + while ((line = reader.readLine()) != null) { + int lastSpace = line.lastIndexOf(' '); + if (lastSpace < 0) continue; + + String stackPart = line.substring(0, lastSpace); + long count = Long.parseLong(line.substring(lastSpace + 1).trim()); + + // Extract individual methods from stack + String[] frames = stackPart.split(";"); + for (String frame : frames) { + methodCounts.merge(frame, count, Long::sum); + } + } + } + + // Sort by count descending + List<Map.Entry<String, Long>> sorted = new ArrayList<>(methodCounts.entrySet()); + sorted.sort((a, b) -> Long.compare(b.getValue(), a.getValue())); + + out.println("Top " + n + " methods by sample count:"); + out.println("===================================="); + int shown = 0; + for (Map.Entry<String, Long> entry : sorted) { + if (shown >= n) break; + out.printf("%8d %s%n", entry.getValue(), entry.getKey()); + shown++; + } + } + + /** + * Compare two profiles and show differences. + */ + public static void diffProfiles(String baselineFile, String currentFile, PrintStream out) throws IOException { + Map<String, Long> baseline = loadCollapsed(baselineFile); + Map<String, Long> current = loadCollapsed(currentFile); + + // Find methods with biggest changes + Map<String, Double> changes = new HashMap<>(); + + Set<String> allMethods = new HashSet<>(); + allMethods.addAll(baseline.keySet()); + allMethods.addAll(current.keySet()); + + long baselineTotal = baseline.values().stream().mapToLong(Long::longValue).sum(); + long currentTotal = current.values().stream().mapToLong(Long::longValue).sum(); + + for (String method : allMethods) { + long baseCount = baseline.getOrDefault(method, 0L); + long currCount = current.getOrDefault(method, 0L); + + double basePct = baselineTotal > 0 ? (baseCount * 100.0 / baselineTotal) : 0; + double currPct = currentTotal > 0 ? (currCount * 100.0 / currentTotal) : 0; + + double diff = currPct - basePct; + if (Math.abs(diff) > 0.1) { // Only show significant changes + changes.put(method, diff); + } + } + + // Sort by absolute change + List<Map.Entry<String, Double>> sorted = new ArrayList<>(changes.entrySet()); + sorted.sort((a, b) -> Double.compare(Math.abs(b.getValue()), Math.abs(a.getValue()))); + + out.println("Profile Comparison (positive = regression, negative = improvement)"); + out.println("================================================================"); + out.printf("Baseline total samples: %d%n", baselineTotal); + out.printf("Current total samples: %d%n", currentTotal); + out.println(); + + int shown = 0; + for (Map.Entry<String, Double> entry : sorted) { + if (shown >= 30) break; + String sign = entry.getValue() > 0 ? "+" : ""; + out.printf("%s%.2f%% %s%n", sign, entry.getValue(), entry.getKey()); + shown++; + } + } + + private static Map<String, Long> loadCollapsed(String file) throws IOException { + Map<String, Long> methodCounts = new HashMap<>(); + + try (BufferedReader reader = Files.newBufferedReader(Paths.get(file))) { + String line; + while ((line = reader.readLine()) != null) { + int lastSpace = line.lastIndexOf(' '); + if (lastSpace < 0) continue; + + String stackPart = line.substring(0, lastSpace); + long count = Long.parseLong(line.substring(lastSpace + 1).trim()); + + String[] frames = stackPart.split(";"); + for (String frame : frames) { + methodCounts.merge(frame, count, Long::sum); + } + } + } + + return methodCounts; + } + + /** + * Programmatic API for generating collapsed stacks from JFR. + */ + public static Map<String, Long> collapseFromJfr(Path jfrTextFile) throws IOException { + Map<String, Long> stackCounts = new LinkedHashMap<>(); + List<String> currentStack = new ArrayList<>(); + + try (BufferedReader reader = Files.newBufferedReader(jfrTextFile)) { + String line; + while ((line = reader.readLine()) != null) { + Matcher eventMatcher = EVENT_SEPARATOR_PATTERN.matcher(line); + if (eventMatcher.find()) { + if (!currentStack.isEmpty()) { + String collapsed = collapseStack(currentStack); + stackCounts.merge(collapsed, 1L, Long::sum); + currentStack.clear(); + } + continue; + } + + Matcher frameMatcher = STACK_FRAME_PATTERN.matcher(line); + if (frameMatcher.matches()) { + String frame = cleanFrame(frameMatcher.group(1).trim()); + if (!frame.isEmpty()) { + currentStack.add(frame); + } + } + } + + if (!currentStack.isEmpty()) { + String collapsed = collapseStack(currentStack); + stackCounts.merge(collapsed, 1L, Long::sum); + } + } + + return stackCounts; + } + + /** + * Filter collapsed stacks programmatically. + */ + public static Map<String, Long> filterByPattern(Map<String, Long> stacks, String pattern) { + Pattern p = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + Map<String, Long> filtered = new LinkedHashMap<>(); + + for (Map.Entry<String, Long> entry : stacks.entrySet()) { + if (p.matcher(entry.getKey()).find()) { + filtered.put(entry.getKey(), entry.getValue()); + } + } + + return filtered; + } + + /** + * Get top N methods from collapsed stacks. + */ + public static List<Map.Entry<String, Long>> getTopMethods(Map<String, Long> stacks, int n) { + Map<String, Long> methodCounts = new HashMap<>(); + + for (Map.Entry<String, Long> entry : stacks.entrySet()) { + String[] frames = entry.getKey().split(";"); + for (String frame : frames) { + methodCounts.merge(frame, entry.getValue(), Long::sum); + } + } + + List<Map.Entry<String, Long>> sorted = new ArrayList<>(methodCounts.entrySet()); + sorted.sort((a, b) -> Long.compare(b.getValue(), a.getValue())); + + return sorted.subList(0, Math.min(n, sorted.size())); + } +} diff --git a/subprojects/performance/src/jmh/resources/workloads/cold-startup.json b/subprojects/performance/src/jmh/resources/workloads/cold-startup.json new file mode 100644 index 0000000000..8247630114 --- /dev/null +++ b/subprojects/performance/src/jmh/resources/workloads/cold-startup.json @@ -0,0 +1,42 @@ +{ + "name": "Cold Startup Workload", + "description": "Measures performance of first-time method invocations before callsite caches warm up", + "benchmarks": [ + { + "pattern": ".*cold.*", + "weight": 0.40, + "description": "Cold callsite benchmarks" + }, + { + "pattern": ".*FirstCall.*", + "weight": 0.30, + "description": "First method call measurements" + }, + { + "pattern": "MemoryAllocationBench.*createAndCallOnce", + "weight": 0.30, + "description": "Create-call-discard patterns" + } + ], + "parameters": { + "callsiteWarmup": false, + "measureFirstInvocation": true + }, + "jvmArgs": [ + "-Xms128m", + "-Xmx128m", + "-XX:+UseG1GC", + "-XX:TieredStopAtLevel=1" + ], + "jmhArgs": { + "warmupIterations": 0, + "measurementIterations": 10, + "forks": 3, + "mode": "SingleShotTime", + "timeUnit": "MICROSECONDS" + }, + "analysis": { + "expectedPattern": "Single-shot times should be significantly higher than warmed-up throughput benchmarks", + "metrics": ["singleShot.time", "allocation_per_operation"] + } +} diff --git a/subprojects/performance/src/jmh/resources/workloads/dispatch-stress.json b/subprojects/performance/src/jmh/resources/workloads/dispatch-stress.json new file mode 100644 index 0000000000..04f4f7eaca --- /dev/null +++ b/subprojects/performance/src/jmh/resources/workloads/dispatch-stress.json @@ -0,0 +1,41 @@ +{ + "name": "Dispatch Stress Test", + "description": "Stress test focusing on method dispatch mechanisms - polymorphic dispatch, interface dispatch, and mixed-type collections", + "benchmarks": [ + { + "pattern": "MixedTypeCollectionBench", + "weight": 0.35, + "description": "Collections with varying type diversity (2-20 types)" + }, + { + "pattern": "InterfaceDispatchBench", + "weight": 0.35, + "description": "Interface-based polymorphic dispatch" + }, + { + "pattern": "PolymorphicDispatchBench", + "weight": 0.30, + "description": "General polymorphic dispatch scenarios" + } + ], + "parameters": { + "typeCount": [2, 5, 10, 20], + "collectionSize": 100, + "callsiteCount": 1000 + }, + "jvmArgs": [ + "-Xms256m", + "-Xmx256m", + "-XX:+UseG1GC" + ], + "jmhArgs": { + "warmupIterations": 5, + "measurementIterations": 5, + "forks": 2, + "timeUnit": "MICROSECONDS" + }, + "analysis": { + "expectedPattern": "Performance should degrade as type count increases beyond callsite cache size (typically 4-8)", + "metrics": ["throughput", "allocation_rate"] + } +} diff --git a/subprojects/performance/src/jmh/resources/workloads/grails-typical.json b/subprojects/performance/src/jmh/resources/workloads/grails-typical.json new file mode 100644 index 0000000000..460dc69c60 --- /dev/null +++ b/subprojects/performance/src/jmh/resources/workloads/grails-typical.json @@ -0,0 +1,48 @@ +{ + "name": "Grails Typical Web Application", + "description": "Simulates a typical Grails web application workload with CRUD operations, dynamic finders, and template rendering", + "benchmarks": [ + { + "pattern": "RequestLifecycleBench", + "weight": 0.30, + "description": "Controller request handling lifecycle" + }, + { + "pattern": "TemplateRenderBench", + "weight": 0.25, + "description": "GSP-like view rendering" + }, + { + "pattern": "DynamicFinderBench", + "weight": 0.20, + "description": "GORM dynamic finder methods" + }, + { + "pattern": "CollectionOperationsBench", + "weight": 0.15, + "description": "Groovy collection operations on domain objects" + }, + { + "pattern": "EntityTraversalBench", + "weight": 0.10, + "description": "Traversing domain object relationships" + } + ], + "parameters": { + "domainObjectCount": 1000, + "collectionSize": 100, + "templateComplexity": "medium", + "requestsPerSession": 50 + }, + "jvmArgs": [ + "-Xms512m", + "-Xmx512m", + "-XX:+UseG1GC" + ], + "jmhArgs": { + "warmupIterations": 5, + "measurementIterations": 5, + "forks": 2, + "timeUnit": "MILLISECONDS" + } +} diff --git a/subprojects/performance/src/jmh/resources/workloads/memory-intensive.json b/subprojects/performance/src/jmh/resources/workloads/memory-intensive.json new file mode 100644 index 0000000000..a026efa5e8 --- /dev/null +++ b/subprojects/performance/src/jmh/resources/workloads/memory-intensive.json @@ -0,0 +1,44 @@ +{ + "name": "Memory Intensive Workload", + "description": "Focuses on memory allocation patterns and callsite cache memory overhead", + "benchmarks": [ + { + "pattern": "MemoryAllocationBench", + "weight": 0.30, + "description": "Heap allocation during Groovy operations" + }, + { + "pattern": "CallsiteGrowthBench", + "weight": 0.35, + "description": "Memory cost of accumulating callsites" + }, + { + "pattern": "LongRunningSessionBench", + "weight": 0.35, + "description": "Memory patterns during sustained load" + } + ], + "parameters": { + "callsiteCount": [100, 1000, 10000], + "sessionDuration": "extended", + "gcMonitoring": true + }, + "jvmArgs": [ + "-Xms256m", + "-Xmx256m", + "-XX:+UseG1GC", + "-XX:+PrintGCDetails", + "-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=10M" + ], + "jmhArgs": { + "warmupIterations": 3, + "measurementIterations": 5, + "forks": 1, + "timeUnit": "MILLISECONDS", + "profilers": ["gc"] + }, + "analysis": { + "expectedPattern": "Memory should grow proportionally with callsite count; watch for retention issues", + "metrics": ["gc.alloc.rate", "gc.churn.PS_Eden_Space", "gc.count"] + } +}
