You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by jo...@apache.org on 2026/02/20 23:11:34 UTC
(groovy) 04/17: Implement memory pressure benchmarks
This is an automated email from the ASF dual-hosted git repository.
jonnybot pushed a commit to branch INDY-PERF-EXPLORATION
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 77a2d2b371ffb638cf5a69236bc66c21873c0656
Author: Jonny Carter <jc...@adaptavist.com>
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")
+ }
+ }
+}