This is an automated email from the ASF dual-hosted git repository.

jonnybot pushed a commit to branch INDY-PERF-EXPLORATION
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 0cfe14e76c09575cb3e6883ad581652ba63a6198
Author: Jonny Carter <[email protected]>
AuthorDate: Thu Jan 22 17:55:39 2026 -0600

    Create benchmarks around aspects potentially related to Grails performance 
issues
---
 subprojects/performance/README.adoc                |  64 +++
 .../bench/dispatch/CacheInvalidationBench.java     | 339 ++++++++++++++++
 .../groovy/bench/dispatch/DispatchHelper.groovy    |  68 ++++
 .../apache/groovy/bench/indy/ColdCallBench.java    | 302 ++++++++++++++
 .../groovy/bench/indy/ColdCallPatterns.groovy      | 222 ++++++++++
 .../bench/indy/ThresholdSensitivityBench.java      | 316 +++++++++++++++
 .../groovy/bench/indy/WarmupBehaviorBench.java     | 268 ++++++++++++
 .../apache/groovy/bench/orm/DomainObjects.groovy   | 377 +++++++++++++++++
 .../groovy/bench/orm/PropertyAccessBench.java      | 447 +++++++++++++++++++++
 .../groovy/bench/orm/PropertyAccessHelper.groovy   | 175 ++++++++
 10 files changed, 2578 insertions(+)

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

Reply via email to