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") + } + } +}
