You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by jo...@apache.org on 2026/02/20 23:11:36 UTC
(groovy) 06/17: Add further benchmarks
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 <jc...@adaptavist.com>
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, "person42@example.com");
+ 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("person42@example.com");
+ 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"]
+ }
+}