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 77a2d2b371ffb638cf5a69236bc66c21873c0656
Author: Jonny Carter <[email protected]>
AuthorDate: Fri Jan 30 16:52:26 2026 -0600

    Implement memory pressure benchmarks
---
 subprojects/performance/README.adoc                |  44 +++
 .../groovy/bench/memory/CallsiteGrowthBench.java   | 255 ++++++++++++++++
 .../bench/memory/LongRunningSessionBench.java      | 336 +++++++++++++++++++++
 .../groovy/bench/memory/MemoryAllocationBench.java | 239 +++++++++++++++
 .../apache/groovy/bench/memory/MemoryHelper.groovy | 253 ++++++++++++++++
 5 files changed, 1127 insertions(+)

diff --git a/subprojects/performance/README.adoc 
b/subprojects/performance/README.adoc
index 2cd55fee9a..49b22347de 100644
--- a/subprojects/performance/README.adoc
+++ b/subprojects/performance/README.adoc
@@ -97,6 +97,50 @@ Tests GORM-like property access patterns common in Grails 
applications.
 
     ./gradlew -Pindy=true -PbenchInclude=PropertyAccess :perf:jmh
 
+==== Memory Allocation Benchmarks (`MemoryAllocationBench`)
+
+Measures heap growth and allocation rates during Groovy operations.
+Quantifies the memory overhead of MethodHandleWrapper AtomicLong objects
+and CacheableCallSite LRU cache entries.
+
+    ./gradlew -Pindy=true -PbenchInclude=MemoryAllocation :perf:jmh
+
+For detailed allocation profiling with GC metrics:
+
+    ./gradlew -Pindy=true -PbenchInclude=MemoryAllocation :perf:jmh 
-Pjmh.profilers=gc
+
+==== Callsite Growth Benchmarks (`CallsiteGrowthBench`)
+
+Measures how memory grows as unique callsites accumulate. Tests with
+parameterized callsite counts (100, 1000, 10000) to quantify the ~32 bytes
+per cached method handle overhead.
+
+    ./gradlew -Pindy=true -PbenchInclude=CallsiteGrowth :perf:jmh
+
+==== Long-Running Session Benchmarks (`LongRunningSessionBench`)
+
+Simulates web application memory patterns over time: request cycles,
+sustained load, and memory recovery after GC.
+
+    ./gradlew -Pindy=true -PbenchInclude=LongRunningSession :perf:jmh
+
+=== Memory Pressure Analysis
+
+To compare memory overhead between indy and non-indy modes:
+
+    # Capture non-indy baseline
+    ./gradlew -Pindy=false -PbenchInclude=Memory :perf:jmh > noindy-memory.txt
+
+    # Capture indy memory profile
+    ./gradlew -Pindy=true -PbenchInclude=Memory :perf:jmh > indy-memory.txt
+
+    # Compare results
+    diff noindy-memory.txt indy-memory.txt
+
+For comprehensive memory analysis with GC profiler:
+
+    ./gradlew -Pindy=true -PbenchInclude=Memory :perf:jmh -Pjmh.profilers=gc
+
 === Comparing Indy vs Non-Indy
 
 Run the same benchmark with and without invokedynamic to measure the overhead:
diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/CallsiteGrowthBench.java
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/CallsiteGrowthBench.java
new file mode 100644
index 0000000000..ccabece35a
--- /dev/null
+++ 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/CallsiteGrowthBench.java
@@ -0,0 +1,255 @@
+/*
+ *  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.memory;
+
+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 how memory grows as unique callsites accumulate.
+ * <p>
+ * With Groovy 4's invokedynamic implementation:
+ * <ul>
+ *   <li>Each callsite creates a CacheableCallSite with 4 cache entries</li>
+ *   <li>Each cache entry is wrapped in SoftReference with 
MethodHandleWrapper</li>
+ *   <li>MethodHandleWrapper contains AtomicLong (~32 bytes each)</li>
+ * </ul>
+ * <p>
+ * With millions of callsites × 4 cache entries = significant memory overhead.
+ * <p>
+ * Run with: ./gradlew -Pindy=true -PbenchInclude=CallsiteGrowth :perf:jmh
+ * <p>
+ * For GC profiling:
+ * ./gradlew -Pindy=true -PbenchInclude=CallsiteGrowth :perf:jmh 
-Pjmh.profilers=gc
+ */
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 2, jvmArgs = {"-Xms256m", "-Xmx256m"})
+@BenchmarkMode(Mode.SingleShotTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Benchmark)
+public class CallsiteGrowthBench {
+
+    // ========================================================================
+    // Parameterized callsite counts
+    // ========================================================================
+
+    @Param({"100", "1000", "10000"})
+    public int callsiteCount;
+
+    // ========================================================================
+    // State classes
+    // ========================================================================
+
+    @State(Scope.Benchmark)
+    public static class UniqueMethodState {
+        List<String> methodNames;
+
+        @Setup(Level.Trial)
+        public void setup() {
+            // Generate unique method names for dynamic dispatch testing
+            methodNames = new ArrayList<>();
+            for (int i = 0; i < 10000; i++) {
+                methodNames.add("method" + i);
+            }
+        }
+    }
+
+    @State(Scope.Benchmark)
+    public static class MultiTypeState {
+        List<List<MemoryReceiver>> receiverBatches;
+        final Random random = new Random(42);
+
+        @Setup(Level.Trial)
+        public void setup() {
+            // Create batches of receivers with varying type counts
+            receiverBatches = new ArrayList<>();
+
+            // Batch with 2 types
+            receiverBatches.add(MemoryHelper.createReceiversWithNTypes(100, 
2));
+            // Batch with 4 types
+            receiverBatches.add(MemoryHelper.createReceiversWithNTypes(100, 
4));
+            // Batch with 8 types
+            receiverBatches.add(MemoryHelper.createReceiversWithNTypes(100, 
8));
+        }
+    }
+
+    @State(Scope.Benchmark)
+    public static class CacheChurnState {
+        List<MemoryReceiver> receivers;
+        final Random random = new Random(42);
+
+        @Setup(Level.Iteration)
+        public void setup() {
+            // Create receivers in random order to maximize cache churn
+            receivers = MemoryHelper.createMixedReceivers(1000);
+            Collections.shuffle(receivers, random);
+        }
+    }
+
+    // ========================================================================
+    // Unique method callsite benchmarks
+    // ========================================================================
+
+    /**
+     * Call N unique method names dynamically.
+     * Each unique method name creates a distinct callsite.
+     * <p>
+     * Memory cost: ~N callsites × 4 cache entries × 32 bytes per AtomicLong
+     */
+    @Benchmark
+    public void callsite_uniqueMethods(UniqueMethodState state, Blackhole bh) {
+        MemoryTestService service = new MemoryTestService();
+        List<String> methodsToCall = state.methodNames.subList(0, 
callsiteCount);
+        MemoryHelper.callUniqueMethods(service, methodsToCall, bh);
+    }
+
+    // ========================================================================
+    // Unique receiver type callsite benchmarks
+    // ========================================================================
+
+    /**
+     * Call same method on objects of different types.
+     * Tests cache invalidation cost as types change.
+     * <p>
+     * With 4-entry LRU cache, types beyond 4 cause eviction and re-population.
+     */
+    @Benchmark
+    public void callsite_uniqueReceiverTypes2(MultiTypeState state, Blackhole 
bh) {
+        List<MemoryReceiver> receivers = state.receiverBatches.get(0);
+        for (int i = 0; i < callsiteCount && i < receivers.size(); i++) {
+            bh.consume(receivers.get(i % receivers.size()).receive());
+        }
+    }
+
+    @Benchmark
+    public void callsite_uniqueReceiverTypes4(MultiTypeState state, Blackhole 
bh) {
+        List<MemoryReceiver> receivers = state.receiverBatches.get(1);
+        for (int i = 0; i < callsiteCount && i < receivers.size(); i++) {
+            bh.consume(receivers.get(i % receivers.size()).receive());
+        }
+    }
+
+    @Benchmark
+    public void callsite_uniqueReceiverTypes8(MultiTypeState state, Blackhole 
bh) {
+        List<MemoryReceiver> receivers = state.receiverBatches.get(2);
+        for (int i = 0; i < callsiteCount && i < receivers.size(); i++) {
+            bh.consume(receivers.get(i % receivers.size()).receive());
+        }
+    }
+
+    // ========================================================================
+    // Cache churn benchmarks
+    // ========================================================================
+
+    /**
+     * Repeatedly invalidate caches with type changes.
+     * Simulates worst-case scenario: random type order maximizes cache misses.
+     */
+    @Benchmark
+    public void callsite_cacheChurn(CacheChurnState state, Blackhole bh) {
+        List<MemoryReceiver> receivers = state.receivers;
+        int iterations = Math.min(callsiteCount, receivers.size());
+        for (int round = 0; round < 10; round++) {
+            for (int i = 0; i < iterations; i++) {
+                bh.consume(receivers.get(i).receive());
+                bh.consume(receivers.get(i).getValue());
+            }
+        }
+    }
+
+    /**
+     * Alternating types to trigger continuous cache updates.
+     * Pattern: A, B, C, D, E, F, G, H, A, B, ... (8 types rotating)
+     */
+    @Benchmark
+    public void callsite_rotatingTypes(CacheChurnState state, Blackhole bh) {
+        List<MemoryReceiver> receivers = state.receivers;
+        for (int i = 0; i < callsiteCount; i++) {
+            // Access receivers in type-rotating order
+            int index = i % 8; // 8 different types
+            MemoryReceiver r = receivers.get(index * (receivers.size() / 8));
+            bh.consume(r.receive());
+        }
+    }
+
+    // ========================================================================
+    // Groovy collection operation callsite benchmarks
+    // ========================================================================
+
+    /**
+     * Spread operator creates callsites per element type.
+     */
+    @Benchmark
+    public void callsite_spreadGrowth(MultiTypeState state, Blackhole bh) {
+        for (int i = 0; i < callsiteCount / 100; i++) {
+            for (List<MemoryReceiver> batch : state.receiverBatches) {
+                MemoryHelper.spreadOperatorOverhead(batch, bh);
+            }
+        }
+    }
+
+    /**
+     * Closure operations create callsites within closure scope.
+     */
+    @Benchmark
+    public void callsite_closureGrowth(MultiTypeState state, Blackhole bh) {
+        for (int i = 0; i < callsiteCount / 100; i++) {
+            for (List<MemoryReceiver> batch : state.receiverBatches) {
+                MemoryHelper.closureOverhead(batch, bh);
+            }
+        }
+    }
+
+    // ========================================================================
+    // Java baseline benchmarks
+    // ========================================================================
+
+    /**
+     * Java baseline: interface dispatch (no callsite caching overhead).
+     */
+    @Benchmark
+    public void java_interfaceDispatch(MultiTypeState state, Blackhole bh) {
+        List<MemoryReceiver> receivers = state.receiverBatches.get(2); // 8 
types
+        for (int i = 0; i < callsiteCount && i < receivers.size(); i++) {
+            MemoryReceiver r = receivers.get(i % receivers.size());
+            bh.consume(r.receive());
+            bh.consume(r.getValue());
+        }
+    }
+
+    /**
+     * Java baseline: rotating types through interface.
+     */
+    @Benchmark
+    public void java_rotatingTypes(CacheChurnState state, Blackhole bh) {
+        List<MemoryReceiver> receivers = state.receivers;
+        for (int i = 0; i < callsiteCount; i++) {
+            int index = i % 8;
+            MemoryReceiver r = receivers.get(index * (receivers.size() / 8));
+            bh.consume(r.receive());
+        }
+    }
+}
diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/LongRunningSessionBench.java
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/LongRunningSessionBench.java
new file mode 100644
index 0000000000..24be9d1cd8
--- /dev/null
+++ 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/LongRunningSessionBench.java
@@ -0,0 +1,336 @@
+/*
+ *  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.memory;
+
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.Blackhole;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Benchmarks simulating web application memory patterns over time.
+ * <p>
+ * Web applications create many short-lived objects per request:
+ * <ul>
+ *   <li>Session objects created on request start</li>
+ *   <li>Domain objects fetched/created during request processing</li>
+ *   <li>All objects discarded at request end</li>
+ * </ul>
+ * <p>
+ * With Groovy 4's invokedynamic:
+ * <ul>
+ *   <li>Each request exercises callsites that may never be fully 
optimized</li>
+ *   <li>MethodHandleWrapper AtomicLong objects accumulate</li>
+ *   <li>ClassValue entries per class type persist</li>
+ * </ul>
+ * <p>
+ * Run with: ./gradlew -Pindy=true -PbenchInclude=LongRunningSession :perf:jmh
+ * <p>
+ * For memory profiling:
+ * ./gradlew -Pindy=true -PbenchInclude=LongRunningSession :perf:jmh 
-Pjmh.profilers=gc
+ */
+@Warmup(iterations = 2, time = 3, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 2, jvmArgs = {"-Xms512m", "-Xmx512m"})
+@BenchmarkMode(Mode.SingleShotTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+public class LongRunningSessionBench {
+
+    private static final int REQUESTS_PER_CYCLE = 100;
+
+    // ========================================================================
+    // State classes
+    // ========================================================================
+
+    @State(Scope.Thread)
+    public static class SessionState {
+        int requestCounter = 0;
+        List<MemoryReceiver> servicePool;
+
+        @Setup(Level.Trial)
+        public void setup() {
+            // Simulate a pool of service objects (like Spring beans)
+            servicePool = MemoryHelper.createMixedReceivers(50);
+        }
+
+        @Setup(Level.Iteration)
+        public void resetCounter() {
+            requestCounter = 0;
+        }
+
+        public String nextRequestId() {
+            return "req-" + (requestCounter++);
+        }
+    }
+
+    @State(Scope.Benchmark)
+    public static class SustainedLoadState {
+        long heapBeforeLoad;
+        long heapAfterLoad;
+
+        @Setup(Level.Trial)
+        public void captureInitialHeap() {
+            System.gc();
+            heapBeforeLoad = Runtime.getRuntime().totalMemory() - 
Runtime.getRuntime().freeMemory();
+        }
+    }
+
+    // ========================================================================
+    // Request cycle benchmarks
+    // ========================================================================
+
+    /**
+     * Single request cycle: create session, process, discard.
+     * This is the atomic unit of web application work.
+     */
+    @Benchmark
+    public void session_requestCycle(SessionState state, Blackhole bh) {
+        // Simulate single request
+        String requestId = state.nextRequestId();
+        Object result = MemoryHelper.simulateRequest(requestId, bh);
+        bh.consume(result);
+    }
+
+    /**
+     * Request cycle with service calls (more realistic).
+     * Simulates controller -> service -> repository pattern.
+     */
+    @Benchmark
+    public void session_requestWithServices(SessionState state, Blackhole bh) {
+        String requestId = state.nextRequestId();
+
+        // Controller creates session
+        Object sessionResult = MemoryHelper.simulateRequest(requestId, bh);
+
+        // Service layer calls
+        List<MemoryReceiver> services = state.servicePool;
+        for (int i = 0; i < 5; i++) {
+            MemoryReceiver service = services.get(i % services.size());
+            bh.consume(service.receive());
+            bh.consume(service.getValue());
+        }
+
+        bh.consume(sessionResult);
+    }
+
+    // ========================================================================
+    // Sustained load benchmarks
+    // ========================================================================
+
+    /**
+     * 1000 request cycles to measure memory accumulation.
+     * After 1000 requests, measure final heap to check for memory growth.
+     */
+    @Benchmark
+    public void session_sustainedLoad(SessionState state, SustainedLoadState 
loadState, Blackhole bh) {
+        // Process 1000 requests
+        for (int i = 0; i < 1000; i++) {
+            String requestId = state.nextRequestId();
+            Object result = MemoryHelper.simulateRequest(requestId, bh);
+
+            // Occasional service calls (like real apps)
+            if (i % 10 == 0) {
+                List<MemoryReceiver> services = state.servicePool;
+                for (int j = 0; j < 3; j++) {
+                    bh.consume(services.get(j % services.size()).receive());
+                }
+            }
+
+            bh.consume(result);
+        }
+
+        // Capture heap after load
+        System.gc();
+        loadState.heapAfterLoad = Runtime.getRuntime().totalMemory() - 
Runtime.getRuntime().freeMemory();
+    }
+
+    /**
+     * Heavy sustained load: 5000 requests with polymorphic dispatch.
+     */
+    @Benchmark
+    public void session_heavyLoad(SessionState state, Blackhole bh) {
+        List<MemoryReceiver> services = state.servicePool;
+
+        for (int i = 0; i < 5000; i++) {
+            // Create fresh session data each request
+            String requestId = "heavy-" + i;
+            SessionData session = new SessionData();
+            session.setSessionId(requestId);
+            session.setUserId("user-" + (i % 100));
+            session.setCreatedAt(System.currentTimeMillis());
+            session.setLastAccessed(System.currentTimeMillis());
+
+            // Process request with polymorphic service calls
+            session.setAttribute("iteration", i);
+            for (int j = 0; j < 5; j++) {
+                MemoryReceiver service = services.get((i + j) % 
services.size());
+                session.setAttribute("result-" + j, service.receive());
+            }
+
+            bh.consume(session.getSummary());
+        }
+    }
+
+    // ========================================================================
+    // Memory recovery benchmarks
+    // ========================================================================
+
+    /**
+     * Test if GC can reclaim memory after load.
+     * Runs load, forces GC, measures remaining heap.
+     */
+    @Benchmark
+    public void session_memoryRecovery(SessionState state, Blackhole bh) {
+        // Phase 1: Create load
+        for (int i = 0; i < 1000; i++) {
+            String requestId = state.nextRequestId();
+            Object result = MemoryHelper.simulateRequest(requestId, bh);
+            bh.consume(result);
+        }
+
+        // Phase 2: Let objects become eligible for GC
+        // (no references held - sessions should be collectible)
+
+        // Phase 3: Force GC
+        System.gc();
+
+        // Phase 4: More requests to see if memory stabilizes
+        for (int i = 0; i < 1000; i++) {
+            String requestId = state.nextRequestId();
+            Object result = MemoryHelper.simulateRequest(requestId, bh);
+            bh.consume(result);
+        }
+    }
+
+    /**
+     * Rapid object churn: create and discard objects quickly.
+     * Tests if SoftReferences in cache are cleared under memory pressure.
+     */
+    @Benchmark
+    public void session_rapidChurn(SessionState state, Blackhole bh) {
+        List<MemoryReceiver> services = state.servicePool;
+
+        for (int round = 0; round < 10; round++) {
+            // Create burst of activity
+            List<SessionData> sessions = new ArrayList<>();
+            for (int i = 0; i < 100; i++) {
+                SessionData session = new SessionData();
+                session.setSessionId(UUID.randomUUID().toString());
+                session.setUserId("user-" + i);
+                session.setCreatedAt(System.currentTimeMillis());
+                session.setLastAccessed(System.currentTimeMillis());
+
+                // Service calls
+                for (int j = 0; j < 3; j++) {
+                    session.setAttribute("data-" + j, services.get((i + j) % 
services.size()).receive());
+                }
+
+                sessions.add(session);
+            }
+
+            // Process all sessions
+            for (SessionData session : sessions) {
+                bh.consume(session.getSummary());
+                bh.consume(session.getAttribute("data-0"));
+            }
+
+            // Clear references
+            sessions.clear();
+
+            // Small pause between rounds
+            if (round < 9) {
+                System.gc();
+            }
+        }
+    }
+
+    // ========================================================================
+    // Java baseline benchmarks
+    // ========================================================================
+
+    /**
+     * Java baseline: request cycle without Groovy dispatch.
+     */
+    @Benchmark
+    public void java_requestCycle(SessionState state, Blackhole bh) {
+        String requestId = state.nextRequestId();
+        JavaSession session = new JavaSession(requestId, "user-" + 
requestId.hashCode() % 1000);
+        session.setAttribute("requestId", requestId);
+        session.setAttribute("timestamp", System.currentTimeMillis());
+        bh.consume(session.getSummary());
+    }
+
+    /**
+     * Java baseline: sustained load.
+     */
+    @Benchmark
+    public void java_sustainedLoad(SessionState state, Blackhole bh) {
+        for (int i = 0; i < 1000; i++) {
+            String requestId = state.nextRequestId();
+            JavaSession session = new JavaSession(requestId, "user-" + i % 
100);
+            session.setAttribute("iteration", i);
+
+            // Service calls through interface
+            List<MemoryReceiver> services = state.servicePool;
+            if (i % 10 == 0) {
+                for (int j = 0; j < 3; j++) {
+                    bh.consume(services.get(j % services.size()).receive());
+                }
+            }
+
+            bh.consume(session.getSummary());
+        }
+    }
+
+    // ========================================================================
+    // Java session equivalent
+    // ========================================================================
+
+    public static class JavaSession {
+        private final String sessionId;
+        private final String userId;
+        private final java.util.Map<String, Object> attributes = new 
java.util.HashMap<>();
+        private final long createdAt;
+        private long lastAccessed;
+
+        public JavaSession(String sessionId, String userId) {
+            this.sessionId = sessionId;
+            this.userId = userId;
+            this.createdAt = System.currentTimeMillis();
+            this.lastAccessed = this.createdAt;
+        }
+
+        public void setAttribute(String key, Object value) {
+            attributes.put(key, value);
+            lastAccessed = System.currentTimeMillis();
+        }
+
+        public Object getAttribute(String key) {
+            lastAccessed = System.currentTimeMillis();
+            return attributes.get(key);
+        }
+
+        public String getSummary() {
+            return "Session[" + sessionId + "]: user=" + userId + ", attrs=" + 
attributes.size();
+        }
+    }
+}
diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/MemoryAllocationBench.java
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/MemoryAllocationBench.java
new file mode 100644
index 0000000000..eafa0eee77
--- /dev/null
+++ 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/MemoryAllocationBench.java
@@ -0,0 +1,239 @@
+/*
+ *  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.memory;
+
+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 memory allocation and heap growth in Groovy 
operations.
+ * <p>
+ * This benchmark suite quantifies the memory overhead of Groovy's 
invokedynamic
+ * implementation, specifically:
+ * <ul>
+ *   <li>MethodHandleWrapper with AtomicLong latestHitCount per cached method 
handle</li>
+ *   <li>CacheableCallSite LRU cache with 4 entries per callsite 
(SoftReference wrapped)</li>
+ *   <li>GroovyObjectHelper ClassValue creating AtomicReference per class</li>
+ * </ul>
+ * <p>
+ * Run with: ./gradlew -Pindy=true -PbenchInclude=MemoryAllocation :perf:jmh
+ * <p>
+ * For detailed allocation profiling:
+ * ./gradlew -Pindy=true -PbenchInclude=MemoryAllocation :perf:jmh 
-Pjmh.profilers=gc
+ * <p>
+ * Compare indy vs non-indy:
+ * ./gradlew -Pindy=false -PbenchInclude=MemoryAllocation :perf:jmh
+ * ./gradlew -Pindy=true -PbenchInclude=MemoryAllocation :perf:jmh
+ */
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 2, jvmArgs = {"-Xms256m", "-Xmx256m"})
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class MemoryAllocationBench {
+
+    private static final int OBJECT_COUNT = 1000;
+    private static final int RECEIVER_COUNT = 100;
+
+    // ========================================================================
+    // State classes
+    // ========================================================================
+
+    @State(Scope.Thread)
+    public static class AllocationState {
+        List<MemoryReceiver> mixedReceivers;
+        List<MemoryReceiver> monomorphicReceivers;
+
+        @Setup(Level.Trial)
+        public void setup() {
+            mixedReceivers = MemoryHelper.createMixedReceivers(RECEIVER_COUNT);
+            monomorphicReceivers = new ArrayList<>(RECEIVER_COUNT);
+            for (int i = 0; i < RECEIVER_COUNT; i++) {
+                monomorphicReceivers.add(new MemoryReceiverA());
+            }
+        }
+    }
+
+    @State(Scope.Thread)
+    public static class FreshObjectState {
+        // Objects created fresh per invocation to simulate cold callsites
+
+        @Setup(Level.Invocation)
+        public void setup() {
+            // Force GC to get consistent memory measurements
+            // Note: This is a hint, not a guarantee
+            System.gc();
+        }
+    }
+
+    // ========================================================================
+    // Create and call benchmarks - measures cold callsite overhead
+    // ========================================================================
+
+    /**
+     * Create N objects and call one method on each.
+     * This is the most common web application pattern: object created per 
request,
+     * method called once or twice, then object discarded.
+     * <p>
+     * Each object creation exercises callsite bootstrap in indy mode.
+     */
+    @Benchmark
+    public void memory_createAndCallOnce(FreshObjectState freshState, 
Blackhole bh) {
+        MemoryHelper.createAndCallMany(OBJECT_COUNT, bh);
+    }
+
+    /**
+     * Create N objects and call multiple methods on each.
+     * Tests the memory cost of filling the 4-entry LRU cache per callsite.
+     */
+    @Benchmark
+    public void memory_createAndCallMultiple(FreshObjectState freshState, 
Blackhole bh) {
+        MemoryHelper.createAndCallMultiple(OBJECT_COUNT, bh);
+    }
+
+    /**
+     * Polymorphic dispatch to mixed types.
+     * Tests memory overhead when cache entries are invalidated due to type 
changes.
+     */
+    @Benchmark
+    public void memory_polymorphicDispatch(AllocationState state, Blackhole 
bh) {
+        MemoryHelper.polymorphicDispatch(state.mixedReceivers, bh);
+    }
+
+    /**
+     * Monomorphic dispatch (same type).
+     * Baseline for comparison - cache should stabilize.
+     */
+    @Benchmark
+    public void memory_monomorphicDispatch(AllocationState state, Blackhole 
bh) {
+        MemoryHelper.polymorphicDispatch(state.monomorphicReceivers, bh);
+    }
+
+    // ========================================================================
+    // Spread operator benchmarks - tests operator-specific allocation
+    // ========================================================================
+
+    /**
+     * Spread operator on mixed types.
+     * The spread operator creates intermediate lists and exercises callsites.
+     */
+    @Benchmark
+    public void memory_spreadOperatorMixed(AllocationState state, Blackhole 
bh) {
+        MemoryHelper.spreadOperatorOverhead(state.mixedReceivers, bh);
+    }
+
+    /**
+     * Spread operator on monomorphic types.
+     */
+    @Benchmark
+    public void memory_spreadOperatorMono(AllocationState state, Blackhole bh) 
{
+        MemoryHelper.spreadOperatorOverhead(state.monomorphicReceivers, bh);
+    }
+
+    // ========================================================================
+    // Closure benchmarks - tests closure allocation overhead
+    // ========================================================================
+
+    /**
+     * Closure operations on mixed types.
+     * Closures create additional objects and capture scope.
+     */
+    @Benchmark
+    public void memory_closureMixed(AllocationState state, Blackhole bh) {
+        MemoryHelper.closureOverhead(state.mixedReceivers, bh);
+    }
+
+    /**
+     * Closure operations on monomorphic types.
+     */
+    @Benchmark
+    public void memory_closureMono(AllocationState state, Blackhole bh) {
+        MemoryHelper.closureOverhead(state.monomorphicReceivers, bh);
+    }
+
+    // ========================================================================
+    // Java baseline benchmarks
+    // ========================================================================
+
+    /**
+     * Java baseline: create and call.
+     * Direct invokevirtual bytecode, no method handle overhead.
+     */
+    @Benchmark
+    public void java_createAndCallOnce(FreshObjectState freshState, Blackhole 
bh) {
+        for (int i = 0; i < OBJECT_COUNT; i++) {
+            JavaMemoryService svc = new JavaMemoryService();
+            bh.consume(svc.getName());
+        }
+    }
+
+    /**
+     * Java baseline: create and call multiple methods.
+     */
+    @Benchmark
+    public void java_createAndCallMultiple(FreshObjectState freshState, 
Blackhole bh) {
+        for (int i = 0; i < OBJECT_COUNT; i++) {
+            JavaMemoryService svc = new JavaMemoryService();
+            bh.consume(svc.getName());
+            bh.consume(svc.getCounter());
+            svc.increment();
+            bh.consume(svc.compute(i, i + 1));
+        }
+    }
+
+    /**
+     * Java baseline: polymorphic interface dispatch.
+     */
+    @Benchmark
+    public void java_polymorphicDispatch(AllocationState state, Blackhole bh) {
+        for (MemoryReceiver r : state.mixedReceivers) {
+            bh.consume(r.receive());
+            bh.consume(r.getValue());
+        }
+    }
+
+    /**
+     * Java baseline: monomorphic dispatch.
+     */
+    @Benchmark
+    public void java_monomorphicDispatch(AllocationState state, Blackhole bh) {
+        for (MemoryReceiver r : state.monomorphicReceivers) {
+            bh.consume(r.receive());
+            bh.consume(r.getValue());
+        }
+    }
+
+    // ========================================================================
+    // Java equivalent service class
+    // ========================================================================
+
+    public static class JavaMemoryService {
+        private String name = "TestService";
+        private int counter = 0;
+
+        public String getName() { return name; }
+        public int getCounter() { return counter; }
+        public void increment() { counter++; }
+        public int compute(int a, int b) { return a + b; }
+    }
+}
diff --git 
a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/MemoryHelper.groovy
 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/MemoryHelper.groovy
new file mode 100644
index 0000000000..c9818560c1
--- /dev/null
+++ 
b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/memory/MemoryHelper.groovy
@@ -0,0 +1,253 @@
+/*
+ *  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.memory
+
+import org.openjdk.jmh.infra.Blackhole
+
+/**
+ * Groovy helper classes for memory pressure benchmarks.
+ * These classes exercise callsite creation and method handle caching.
+ */
+
+/**
+ * Simple service class for memory testing.
+ */
+class MemoryTestService {
+    String name = "TestService"
+    int counter = 0
+
+    String getName() { name }
+    int getCounter() { counter }
+    void increment() { counter++ }
+    int compute(int a, int b) { a + b }
+    String process(String input) { input.toUpperCase() }
+    def transform(Map data) { data.collect { k, v -> "$k=$v" }.join(', ') }
+}
+
+/**
+ * Multiple receiver types for polymorphic dispatch testing.
+ */
+interface MemoryReceiver {
+    String receive()
+    int getValue()
+}
+
+class MemoryReceiverA implements MemoryReceiver {
+    String receive() { "A" }
+    int getValue() { 1 }
+}
+
+class MemoryReceiverB implements MemoryReceiver {
+    String receive() { "B" }
+    int getValue() { 2 }
+}
+
+class MemoryReceiverC implements MemoryReceiver {
+    String receive() { "C" }
+    int getValue() { 3 }
+}
+
+class MemoryReceiverD implements MemoryReceiver {
+    String receive() { "D" }
+    int getValue() { 4 }
+}
+
+class MemoryReceiverE implements MemoryReceiver {
+    String receive() { "E" }
+    int getValue() { 5 }
+}
+
+class MemoryReceiverF implements MemoryReceiver {
+    String receive() { "F" }
+    int getValue() { 6 }
+}
+
+class MemoryReceiverG implements MemoryReceiver {
+    String receive() { "G" }
+    int getValue() { 7 }
+}
+
+class MemoryReceiverH implements MemoryReceiver {
+    String receive() { "H" }
+    int getValue() { 8 }
+}
+
+/**
+ * Domain-like object for session simulation.
+ */
+class SessionData {
+    String sessionId
+    String userId
+    Map<String, Object> attributes = [:]
+    long createdAt
+    long lastAccessed
+
+    void setAttribute(String key, Object value) {
+        attributes[key] = value
+        lastAccessed = System.currentTimeMillis()
+    }
+
+    Object getAttribute(String key) {
+        lastAccessed = System.currentTimeMillis()
+        attributes[key]
+    }
+
+    boolean isExpired(long timeout) {
+        System.currentTimeMillis() - lastAccessed > timeout
+    }
+
+    String getSummary() {
+        "Session[$sessionId]: user=$userId, attrs=${attributes.size()}"
+    }
+}
+
+/**
+ * Helper methods for memory benchmarks.
+ * All methods use Groovy's dynamic dispatch to exercise callsite creation.
+ */
+class MemoryHelper {
+
+    /**
+     * Create N objects and call one method on each (cold callsites).
+     * Each iteration creates new objects, triggering callsite population.
+     */
+    static void createAndCallMany(int count, Blackhole bh) {
+        for (int i = 0; i < count; i++) {
+            def svc = new MemoryTestService()
+            bh.consume(svc.getName())
+        }
+    }
+
+    /**
+     * Create N objects and call multiple methods on each (cache filling).
+     * This exercises the 4-entry LRU cache in CacheableCallSite.
+     */
+    static void createAndCallMultiple(int count, Blackhole bh) {
+        for (int i = 0; i < count; i++) {
+            def svc = new MemoryTestService()
+            bh.consume(svc.getName())
+            bh.consume(svc.getCounter())
+            svc.increment()
+            bh.consume(svc.compute(i, i + 1))
+        }
+    }
+
+    /**
+     * Polymorphic dispatch to mixed types (cache invalidation).
+     * Tests the cost of cache churn with multiple receiver types.
+     */
+    static void polymorphicDispatch(List receivers, Blackhole bh) {
+        for (receiver in receivers) {
+            bh.consume(receiver.receive())
+            bh.consume(receiver.getValue())
+        }
+    }
+
+    /**
+     * Exercise spread operator overhead.
+     * The spread operator creates callsites for each element.
+     */
+    static List spreadOperatorOverhead(List objects, Blackhole bh) {
+        def result = objects*.receive()
+        bh.consume(result)
+        result
+    }
+
+    /**
+     * Exercise closure allocation and invocation overhead.
+     * Closures capture the enclosing scope and create additional objects.
+     */
+    static void closureOverhead(List objects, Blackhole bh) {
+        def result = objects.collect { it.receive() }
+            .findAll { it != null }
+            .take(10)
+        bh.consume(result)
+    }
+
+    /**
+     * Simulate a single request cycle: create session, access data, discard.
+     * This is the pattern that exercises cold callsite performance.
+     */
+    static Object simulateRequest(String requestId, Blackhole bh) {
+        def session = new SessionData(
+            sessionId: requestId,
+            userId: "user_${requestId.hashCode() % 1000}",
+            createdAt: System.currentTimeMillis(),
+            lastAccessed: System.currentTimeMillis()
+        )
+        session.setAttribute("requestId", requestId)
+        session.setAttribute("timestamp", System.currentTimeMillis())
+        def summary = session.getSummary()
+        bh.consume(session)
+        bh.consume(summary)
+        summary
+    }
+
+    /**
+     * Create a mixed list of receiver types for polymorphic testing.
+     */
+    static List<MemoryReceiver> createMixedReceivers(int count) {
+        def types = [
+            MemoryReceiverA, MemoryReceiverB, MemoryReceiverC, MemoryReceiverD,
+            MemoryReceiverE, MemoryReceiverF, MemoryReceiverG, MemoryReceiverH
+        ]
+        def receivers = []
+        for (int i = 0; i < count; i++) {
+            receivers << types[i % types.size()].newInstance()
+        }
+        receivers
+    }
+
+    /**
+     * Create receivers with only N distinct types.
+     */
+    static List<MemoryReceiver> createReceiversWithNTypes(int count, int 
numTypes) {
+        def types = [
+            MemoryReceiverA, MemoryReceiverB, MemoryReceiverC, MemoryReceiverD,
+            MemoryReceiverE, MemoryReceiverF, MemoryReceiverG, MemoryReceiverH
+        ].take(numTypes)
+        def receivers = []
+        for (int i = 0; i < count; i++) {
+            receivers << types[i % types.size()].newInstance()
+        }
+        receivers
+    }
+
+    /**
+     * Call unique method names dynamically (exercises different callsites).
+     */
+    static void callUniqueMethods(Object target, List<String> methodNames, 
Blackhole bh) {
+        for (name in methodNames) {
+            try {
+                bh.consume(target."$name"())
+            } catch (MissingMethodException ignored) {
+                // Expected for non-existent methods
+            }
+        }
+    }
+
+    /**
+     * Access properties dynamically on multiple objects.
+     */
+    static void accessProperties(List objects, String propertyName, Blackhole 
bh) {
+        for (obj in objects) {
+            bh.consume(obj."$propertyName")
+        }
+    }
+}


Reply via email to