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/19 22:47:08 UTC

(groovy) 05/11: Add additional 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 0a5317fddf0d3110a7812c81c3bb60f3fe42437a
Author: Jonny Carter <jc...@adaptavist.com>
AuthorDate: Fri Jan 30 17:40:02 2026 -0600

    Add additional benchmarks
---
 subprojects/performance/README.adoc                | 243 +++++++++--
 subprojects/performance/baselines/README.md        |  49 +++
 subprojects/performance/build.gradle               | 458 +++++++++++++++++++++
 .../controller/ControllerSimulator.groovy          | 417 +++++++++++++++++++
 .../controller/RequestLifecycleBench.java          | 382 +++++++++++++++++
 .../grailslike/model/AdditionalDomains.groovy      | 428 +++++++++++++++++++
 .../bench/grailslike/model/DomainObject.groovy     | 163 ++++++++
 .../groovy/bench/grailslike/model/Order.groovy     | 191 +++++++++
 .../groovy/bench/grailslike/model/OrderItem.groovy | 178 ++++++++
 .../groovy/bench/grailslike/model/Person.groovy    | 144 +++++++
 .../bench/grailslike/service/DynamicService.groovy | 288 +++++++++++++
 .../service/TransactionalSimulator.groovy          | 341 +++++++++++++++
 .../bench/grailslike/view/TemplateRenderBench.java | 279 +++++++++++++
 .../bench/grailslike/view/TemplateSimulator.groovy | 435 +++++++++++++++++++
 .../groovy/bench/profiling/MemoryProfileBench.java | 181 ++++++++
 .../bench/profiling/MemoryTrackingState.java       | 221 ++++++++++
 .../bench/profiling/ProfiledBenchmarkRunner.java   | 252 ++++++++++++
 17 files changed, 4608 insertions(+), 42 deletions(-)

diff --git a/subprojects/performance/README.adoc b/subprojects/performance/README.adoc
index 49b22347de..1e2c15e2eb 100644
--- a/subprojects/performance/README.adoc
+++ b/subprojects/performance/README.adoc
@@ -21,14 +21,28 @@
 
 = Performance
 
-This subproject contains two sets of performance related tests, compiler tests
-and benchmarks.
+This subproject contains performance tests and JMH benchmarks for Apache Groovy,
+with a particular focus on investigating invokedynamic performance characteristics.
+
+== Quick Start
+
+[source,bash]
+----
+# Run all benchmarks (indy mode)
+./gradlew -Pindy=true :performance:jmh
+
+# Run specific benchmark suite
+./gradlew -Pindy=true -PbenchInclude=ColdCall :performance:jmh
+
+# Compare indy vs non-indy
+./gradlew :performance:jmhCompare
+----
 
 == Compiler Performance Tests
 
 The compiler tests can be run using the following Gradle task:
 
-    ./gradlew :perf:performanceTests
+    ./gradlew :performance:performanceTests
 
 This will compile various source files using several past versions of Apache
 Groovy in addition to the current source version.
@@ -37,65 +51,62 @@ Groovy in addition to the current source version.
 
 JMH Benchmarks can be run using:
 
-    ./gradlew :perf:jmh
+    ./gradlew :performance:jmh
 
 In order to run the benchmarks against InvokeDynamic generated classes use
 the `indy` property:
 
-    ./gradlew -Pindy=true :perf:jmh
-
-Groovy and Java sources placed in `src/test` will also be available to the
-benchmarks.
+    ./gradlew -Pindy=true :performance:jmh
 
 To run a single benchmark or a matched set of benchmarks, use the
 `benchInclude` property:
 
-    ./gradlew -PbenchInclude=CallsiteBench :perf:jmh
+    ./gradlew -PbenchInclude=CallsiteBench :performance:jmh
 
 The `benchInclude` property will perform a partial match against package
 names or class names. It is equivalent to `.*${benchInclude}.*`.
 
 == InvokeDynamic Performance Benchmarks
 
-A set of benchmarks specifically designed to investigate and isolate performance
+A comprehensive set of benchmarks designed to investigate and isolate performance
 issues with Groovy's invokedynamic implementation. See `INDY_PERFORMANCE_PLAN.md`
 for the full testing plan and background.
 
-=== Available Benchmark Suites
+=== Phase 1: Core Performance Benchmarks
 
 ==== Cold Call Benchmarks (`ColdCallBench`)
 
 Measures the cost of method invocations before callsites are optimized.
-This is critical for web applications where objects are created per-request.
+Critical for web applications where objects are created per-request.
 
-    ./gradlew -Pindy=true -PbenchInclude=ColdCallBench :perf:jmh
+    ./gradlew -Pindy=true -PbenchInclude=ColdCallBench :performance:jmh
 
 ==== Warmup Behavior Benchmarks (`WarmupBehaviorBench`)
 
 Tests performance at different warmup levels (10, 100, 1000, 10000+ calls)
 to understand how the optimization threshold affects performance.
 
-    ./gradlew -Pindy=true -PbenchInclude=WarmupBehavior :perf:jmh
+    ./gradlew -Pindy=true -PbenchInclude=WarmupBehavior :performance:jmh
 
 ==== Threshold Sensitivity Benchmarks (`ThresholdSensitivityBench`)
 
 Tests different usage patterns (web request, batch processing, mixed) to
 understand which patterns benefit from different threshold configurations.
 
-    ./gradlew -Pindy=true -PbenchInclude=ThresholdSensitivity :perf:jmh
+    ./gradlew -Pindy=true -PbenchInclude=ThresholdSensitivity :performance:jmh
 
 ==== Cache Invalidation Benchmarks (`CacheInvalidationBench`)
 
 Tests polymorphic dispatch scenarios that cause inline cache invalidation,
 addressing issues described in GROOVY-8298.
 
-    ./gradlew -Pindy=true -PbenchInclude=CacheInvalidation :perf:jmh
+    ./gradlew -Pindy=true -PbenchInclude=CacheInvalidation :performance:jmh
 
 ==== Property Access Benchmarks (`PropertyAccessBench`)
 
 Tests GORM-like property access patterns common in Grails applications.
 
-    ./gradlew -Pindy=true -PbenchInclude=PropertyAccess :perf:jmh
+    ./gradlew -Pindy=true -PbenchInclude=PropertyAccess :performance:jmh
 
 ==== Memory Allocation Benchmarks (`MemoryAllocationBench`)
 
@@ -103,11 +114,11 @@ 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
+    ./gradlew -Pindy=true -PbenchInclude=MemoryAllocation :performance:jmh
 
 For detailed allocation profiling with GC metrics:
 
-    ./gradlew -Pindy=true -PbenchInclude=MemoryAllocation :perf:jmh -Pjmh.profilers=gc
+    ./gradlew -Pindy=true -PbenchInclude=MemoryAllocation :performance:jmh -Pjmh.profilers=gc
 
 ==== Callsite Growth Benchmarks (`CallsiteGrowthBench`)
 
@@ -115,51 +126,199 @@ 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
+    ./gradlew -Pindy=true -PbenchInclude=CallsiteGrowth :performance: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
+    ./gradlew -Pindy=true -PbenchInclude=LongRunningSession :performance:jmh
+
+=== Phase 2: Grails-Realistic Scenario Tests
+
+These benchmarks simulate real-world Grails application patterns.
+
+==== Request Lifecycle Benchmarks (`RequestLifecycleBench`)
+
+Simulates full HTTP request lifecycle in a Grails-like application:
+
+* Controller receives request (params parsing)
+* Service layer invocation (transactional context)
+* Domain object manipulation (property access, validation)
+* Model building for view
+
+    ./gradlew -Pindy=true -PbenchInclude=RequestLifecycle :performance:jmh
+
+==== Template Render Benchmarks (`TemplateRenderBench`)
+
+Simulates GSP-like template rendering with heavy property access:
+
+* Property access on model objects (`${person.name}`)
+* Collection iteration (`g:each`)
+* GString interpolation
+* Spread operators (`people*.name`)
+
+    ./gradlew -Pindy=true -PbenchInclude=TemplateRender :performance:jmh
+
+==== Mini-Grails Domain Model
+
+The `grailslike` package contains realistic domain classes:
+
+* `DomainObject` - Base class with dirty tracking, validation
+* `Person`, `Order`, `OrderItem` - E-commerce entities
+* `User`, `Role`, `Customer`, `Product` - Additional domain classes
+* `DynamicService` - Service with methodMissing for dynamic finders
+* `TransactionalSimulator` - Transaction boundary simulation
+* `ControllerSimulator` - Request handling patterns
+* `TemplateSimulator` - GSP-like rendering
+
+=== Phase 3: Comparison Infrastructure
+
+==== Automated Indy vs Non-Indy Comparison
+
+[source,bash]
+----
+# Run both modes and generate comparison report
+./gradlew :performance:jmhCompare
+
+# Or run individually
+./gradlew :performance:jmhIndy
+./gradlew :performance:jmhNonIndy
+----
+
+The comparison report is saved to `build/results/jmh-compare/comparison-report.txt`.
+
+==== Threshold Parameter Sweep
+
+Test multiple threshold values to find optimal settings:
 
-=== Memory Pressure Analysis
+[source,bash]
+----
+./gradlew :performance:jmhThresholdSweep
+----
 
-To compare memory overhead between indy and non-indy modes:
+Tests thresholds: 0, 10, 100, 1000, 10000, 100000
 
-    # Capture non-indy baseline
-    ./gradlew -Pindy=false -PbenchInclude=Memory :perf:jmh > noindy-memory.txt
+Results saved to `build/results/jmh-compare/threshold-sweep/threshold-summary.txt`.
 
-    # Capture indy memory profile
-    ./gradlew -Pindy=true -PbenchInclude=Memory :perf:jmh > indy-memory.txt
+=== Phase 4: Profiling Integration
 
-    # Compare results
-    diff noindy-memory.txt indy-memory.txt
+==== JFR Profiling
 
-For comprehensive memory analysis with GC profiler:
+Run benchmarks with Java Flight Recorder:
 
-    ./gradlew -Pindy=true -PbenchInclude=Memory :perf:jmh -Pjmh.profilers=gc
+[source,bash]
+----
+./gradlew -Pindy=true :performance:jmhProfile
+----
 
-=== Comparing Indy vs Non-Indy
+JFR output saved to `build/results/jmh-profile/benchmark.jfr`.
 
-Run the same benchmark with and without invokedynamic to measure the overhead:
+Analyze with:
+[source,bash]
+----
+jfr print build/results/jmh-profile/benchmark.jfr
+jfr summary build/results/jmh-profile/benchmark.jfr
+# Or open in JDK Mission Control (jmc)
+----
 
-    # With invokedynamic (Groovy 4 default)
-    ./gradlew -Pindy=true -PbenchInclude=ColdCallBench :perf:jmh
+==== GC Profiling
 
-    # Without invokedynamic (classic bytecode)
-    ./gradlew -Pindy=false -PbenchInclude=ColdCallBench :perf:jmh
+Run with JMH's GC profiler for detailed memory analysis:
+
+[source,bash]
+----
+./gradlew -Pindy=true :performance:jmhGcProfile
+----
+
+==== Memory Tracking Benchmarks
+
+Use `MemoryTrackingState` in benchmarks for detailed heap tracking:
+
+[source,bash]
+----
+./gradlew -Pindy=true -PbenchInclude=MemoryProfile :performance:jmh
+----
+
+=== Phase 5: Baseline Management & CI
+
+==== Saving Baselines
+
+[source,bash]
+----
+# Run benchmarks
+./gradlew -Pindy=true :performance:jmh
+
+# Save as baseline
+./gradlew :performance:jmhSaveBaseline -PbaselineName=groovy-4.0.x-indy
+----
+
+==== Comparing Against Baselines
+
+[source,bash]
+----
+# Compare against most recent baseline
+./gradlew :performance:jmh
+./gradlew :performance:jmhCompareBaseline
+
+# Compare against specific baseline
+./gradlew :performance:jmhCompareBaseline -PbaselineName=groovy-4.0.x-indy
+----
+
+==== CI Integration
+
+Performance benchmarks run automatically via GitHub Actions:
+
+* **On PRs**: Smoke tests for performance-related changes
+* **Weekly**: Full benchmark suite with indy/non-indy comparison
+* **On demand**: Via workflow_dispatch with customizable filters
+
+See `.github/workflows/groovy-performance.yml` for details.
 
 === Tuning Thresholds
 
 The invokedynamic implementation has configurable thresholds:
 
-- `groovy.indy.optimize.threshold` (default: 10000) - Calls before optimization
-- `groovy.indy.fallback.threshold` (default: 10000) - Fallbacks before reset
+* `groovy.indy.optimize.threshold` (default: 10000) - Calls before optimization
+* `groovy.indy.fallback.threshold` (default: 10000) - Fallbacks before reset
+
+Test with different thresholds:
+
+[source,bash]
+----
+./gradlew -Pindy=true -PbenchInclude=ThresholdSensitivity :performance:jmh \
+    --jvmArgs="-Dgroovy.indy.optimize.threshold=100"
+----
+
+=== Benchmark Categories by Use Case
+
+==== For Web Application Performance
+
+Focus on cold call and request lifecycle:
+
+[source,bash]
+----
+./gradlew -Pindy=true -PbenchInclude="ColdCall|RequestLifecycle|Template" :performance:jmh
+----
+
+==== For Memory Investigation
+
+[source,bash]
+----
+./gradlew -Pindy=true -PbenchInclude=Memory :performance:jmh -Pjmh.profilers=gc
+----
+
+==== For Polymorphic Dispatch Issues
 
-Test with different thresholds using JVM arguments:
+[source,bash]
+----
+./gradlew -Pindy=true -PbenchInclude="CacheInvalidation|Dispatch" :performance:jmh
+----
 
-    ./gradlew -Pindy=true -PbenchInclude=ThresholdSensitivity :perf:jmh \
-        --jvmArgs="-Dgroovy.indy.optimize.threshold=100"
+==== For Full Regression Testing
 
+[source,bash]
+----
+./gradlew :performance:jmhCompare
+----
diff --git a/subprojects/performance/baselines/README.md b/subprojects/performance/baselines/README.md
new file mode 100644
index 0000000000..76b8e0a8fc
--- /dev/null
+++ b/subprojects/performance/baselines/README.md
@@ -0,0 +1,49 @@
+# Performance Baselines
+
+This directory contains saved JMH benchmark results for comparison purposes.
+
+## Creating a Baseline
+
+Run benchmarks and save as a baseline:
+
+```bash
+# Run all benchmarks
+./gradlew :performance:jmh
+
+# Save results as a named baseline
+./gradlew :performance:jmhSaveBaseline -PbaselineName=my-baseline
+
+# Or with a specific date
+./gradlew :performance:jmhSaveBaseline -PbaselineName=groovy-4.0.x-indy-20240115
+```
+
+## Comparing Against a Baseline
+
+```bash
+# Compare current results against most recent baseline
+./gradlew :performance:jmh
+./gradlew :performance:jmhCompareBaseline
+
+# Compare against a specific baseline
+./gradlew :performance:jmhCompareBaseline -PbaselineName=groovy-4.0.x-indy
+```
+
+## Baseline Naming Convention
+
+Recommended naming pattern: `{version}-{mode}-{date}.txt`
+
+Examples:
+- `groovy-3.0.x-noindy.txt` - Groovy 3.x without invokedynamic
+- `groovy-4.0.x-indy.txt` - Groovy 4.x with invokedynamic (default)
+- `groovy-4.0.x-noindy.txt` - Groovy 4.x without invokedynamic
+- `groovy-4.0.x-threshold-100.txt` - Groovy 4.x with custom threshold
+
+## File Format
+
+Baseline files are standard JMH text output format:
+
+```
+Benchmark                               Mode  Cnt    Score   Error  Units
+MemoryAllocationBench.memory_create...  avgt   10   11.047 ± 0.493  us/op
+MemoryAllocationBench.memory_poly...    avgt   10  253.352 ± 14.6   us/op
+```
diff --git a/subprojects/performance/build.gradle b/subprojects/performance/build.gradle
index 477232b058..88cfd5dc04 100644
--- a/subprojects/performance/build.gradle
+++ b/subprojects/performance/build.gradle
@@ -31,3 +31,461 @@ performanceTests {
             project.files('src/jmh/groovy/org/apache/groovy/bench/Ary.groovy') +
             project.files('src/jmh/groovy/org/apache/groovy/bench/Fibo.groovy')
 }
+
+// ============================================================================
+// Indy vs Non-Indy Comparison Infrastructure
+// ============================================================================
+
+def jmhResultsDir = layout.buildDirectory.dir("results/jmh")
+def jmhCompareDir = layout.buildDirectory.dir("results/jmh-compare")
+
+/**
+ * Run JMH benchmarks with invokedynamic enabled.
+ * Results are saved to build/results/jmh-compare/indy-results.txt
+ */
+tasks.register('jmhIndy', JavaExec) {
+    dependsOn 'jmhJar'
+    group = 'benchmark'
+    description = 'Run JMH benchmarks with invokedynamic enabled'
+
+    mainClass = 'org.openjdk.jmh.Main'
+    classpath = files(tasks.named('jmhJar'))
+
+    def outputFile = jmhCompareDir.map { it.file("indy-results.txt") }
+
+    args '-rf', 'text'
+    args '-rff', outputFile.get().asFile.absolutePath
+
+    // Include filter if specified
+    if (project.hasProperty('benchInclude')) {
+        args '-e', '.*' + project.benchInclude + '.*'
+    }
+
+    jvmArgs '-Dgroovy.target.indy=true'
+
+    doFirst {
+        jmhCompareDir.get().asFile.mkdirs()
+        println "Running JMH with invokedynamic ENABLED"
+        println "Results will be saved to: ${outputFile.get().asFile}"
+    }
+}
+
+/**
+ * Run JMH benchmarks with invokedynamic disabled (classic bytecode).
+ * Results are saved to build/results/jmh-compare/noindy-results.txt
+ */
+tasks.register('jmhNonIndy', JavaExec) {
+    dependsOn 'jmhJar'
+    group = 'benchmark'
+    description = 'Run JMH benchmarks with invokedynamic disabled (classic bytecode)'
+
+    mainClass = 'org.openjdk.jmh.Main'
+    classpath = files(tasks.named('jmhJar'))
+
+    def outputFile = jmhCompareDir.map { it.file("noindy-results.txt") }
+
+    args '-rf', 'text'
+    args '-rff', outputFile.get().asFile.absolutePath
+
+    // Include filter if specified
+    if (project.hasProperty('benchInclude')) {
+        args '-e', '.*' + project.benchInclude + '.*'
+    }
+
+    jvmArgs '-Dgroovy.target.indy=false'
+
+    doFirst {
+        jmhCompareDir.get().asFile.mkdirs()
+        println "Running JMH with invokedynamic DISABLED"
+        println "Results will be saved to: ${outputFile.get().asFile}"
+    }
+}
+
+/**
+ * Run both indy and non-indy benchmarks and generate comparison report.
+ */
+tasks.register('jmhCompare') {
+    dependsOn 'jmhIndy', 'jmhNonIndy'
+    group = 'benchmark'
+    description = 'Run benchmarks with and without invokedynamic and compare results'
+
+    def indyResults = jmhCompareDir.map { it.file("indy-results.txt") }
+    def nonIndyResults = jmhCompareDir.map { it.file("noindy-results.txt") }
+    def comparisonReport = jmhCompareDir.map { it.file("comparison-report.txt") }
+
+    doLast {
+        def indyFile = indyResults.get().asFile
+        def nonIndyFile = nonIndyResults.get().asFile
+        def reportFile = comparisonReport.get().asFile
+
+        if (!indyFile.exists() || !nonIndyFile.exists()) {
+            println "ERROR: Results files not found. Run jmhIndy and jmhNonIndy first."
+            return
+        }
+
+        def parseResults = { File file ->
+            def results = [:]
+            file.eachLine { line ->
+                // Parse JMH result lines: BenchmarkName  Mode  Cnt  Score  Error  Units
+                def match = line =~ /^(\S+)\s+(\S+)\s+(\d+)\s+([\d.]+)\s*±?\s*([\d.]*)\s*(\S+)/
+                if (match) {
+                    def name = match[0][1]
+                    def score = match[0][4] as double
+                    def error = match[0][5] ? (match[0][5] as double) : 0.0
+                    def unit = match[0][6]
+                    results[name] = [score: score, error: error, unit: unit]
+                }
+            }
+            results
+        }
+
+        def indyData = parseResults(indyFile)
+        def nonIndyData = parseResults(nonIndyFile)
+
+        reportFile.withWriter { writer ->
+            writer.writeLine "=" * 80
+            writer.writeLine "INVOKEDYNAMIC PERFORMANCE COMPARISON REPORT"
+            writer.writeLine "Generated: ${new Date()}"
+            writer.writeLine "=" * 80
+            writer.writeLine ""
+
+            def allBenchmarks = (indyData.keySet() + nonIndyData.keySet()).unique().sort()
+
+            writer.writeLine String.format("%-60s %12s %12s %12s", "Benchmark", "Indy", "Non-Indy", "Ratio")
+            writer.writeLine "-" * 100
+
+            allBenchmarks.each { name ->
+                def indy = indyData[name]
+                def nonIndy = nonIndyData[name]
+
+                if (indy && nonIndy) {
+                    def ratio = indy.score / nonIndy.score
+                    def ratioStr = String.format("%.2fx", ratio)
+                    if (ratio > 1.1) ratioStr += " SLOWER"
+                    else if (ratio < 0.9) ratioStr += " FASTER"
+
+                    writer.writeLine String.format("%-60s %12.3f %12.3f %12s",
+                        name.length() > 60 ? "..." + name[-57..-1] : name,
+                        indy.score, nonIndy.score, ratioStr)
+                }
+            }
+
+            writer.writeLine ""
+            writer.writeLine "=" * 80
+            writer.writeLine "Notes:"
+            writer.writeLine "- Ratio > 1.0 means indy is SLOWER than non-indy"
+            writer.writeLine "- Ratio < 1.0 means indy is FASTER than non-indy"
+            writer.writeLine "- Use -PbenchInclude=<pattern> to filter benchmarks"
+            writer.writeLine "=" * 80
+        }
+
+        println ""
+        println "Comparison report saved to: ${reportFile}"
+        println ""
+        println reportFile.text
+    }
+}
+
+// ============================================================================
+// Threshold Parameter Sweep
+// ============================================================================
+
+/**
+ * Run benchmarks with different optimize threshold values.
+ * Helps determine optimal threshold for different workloads.
+ */
+tasks.register('jmhThresholdSweep') {
+    group = 'benchmark'
+    description = 'Run benchmarks with varying groovy.indy.optimize.threshold values'
+
+    def thresholds = [0, 10, 100, 1000, 10000, 100000]
+    def sweepDir = jmhCompareDir.map { it.dir("threshold-sweep") }
+
+    doFirst {
+        sweepDir.get().asFile.mkdirs()
+    }
+
+    doLast {
+        def results = [:]
+
+        thresholds.each { threshold ->
+            println ""
+            println "=" * 60
+            println "Running with groovy.indy.optimize.threshold = ${threshold}"
+            println "=" * 60
+
+            def outputFile = sweepDir.get().file("threshold-${threshold}.txt").asFile
+
+            // Run JMH with this threshold
+            project.javaexec {
+                mainClass = 'org.openjdk.jmh.Main'
+                classpath = files(tasks.named('jmhJar'))
+                args '-rf', 'text'
+                args '-rff', outputFile.absolutePath
+                args '-wi', '3'  // Reduced warmup for sweep
+                args '-i', '3'   // Reduced iterations for sweep
+                args '-f', '1'   // Single fork for sweep
+
+                if (project.hasProperty('benchInclude')) {
+                    args '.*' + project.benchInclude + '.*'
+                }
+
+                jvmArgs "-Dgroovy.indy.optimize.threshold=${threshold}"
+                jvmArgs '-Dgroovy.target.indy=true'
+            }
+
+            // Parse results
+            if (outputFile.exists()) {
+                outputFile.eachLine { line ->
+                    def match = line =~ /^(\S+)\s+\S+\s+\d+\s+([\d.]+)/
+                    if (match) {
+                        def name = match[0][1]
+                        def score = match[0][2] as double
+                        if (!results[name]) results[name] = [:]
+                        results[name][threshold] = score
+                    }
+                }
+            }
+        }
+
+        // Generate summary report
+        def summaryFile = sweepDir.get().file("threshold-summary.txt").asFile
+        summaryFile.withWriter { writer ->
+            writer.writeLine "=" * 100
+            writer.writeLine "THRESHOLD SWEEP SUMMARY"
+            writer.writeLine "Generated: ${new Date()}"
+            writer.writeLine "=" * 100
+            writer.writeLine ""
+            writer.writeLine String.format("%-50s %s", "Benchmark", thresholds.collect { String.format("%10d", it) }.join(""))
+            writer.writeLine "-" * 100
+
+            results.keySet().sort().each { name ->
+                def scores = thresholds.collect { t ->
+                    results[name][t] ? String.format("%10.3f", results[name][t]) : String.format("%10s", "N/A")
+                }.join("")
+                writer.writeLine String.format("%-50s %s",
+                    name.length() > 50 ? "..." + name[-47..-1] : name,
+                    scores)
+            }
+
+            writer.writeLine ""
+            writer.writeLine "Optimal thresholds by benchmark:"
+            writer.writeLine "-" * 50
+            results.keySet().sort().each { name ->
+                def scores = results[name]
+                def optimal = scores.min { it.value }?.key
+                if (optimal != null) {
+                    writer.writeLine String.format("%-50s %d",
+                        name.length() > 50 ? "..." + name[-47..-1] : name,
+                        optimal)
+                }
+            }
+        }
+
+        println ""
+        println "Threshold sweep summary saved to: ${summaryFile}"
+        println ""
+        println summaryFile.text
+    }
+}
+
+// ============================================================================
+// Profiling Support
+// ============================================================================
+
+/**
+ * Run JMH with JFR (Java Flight Recorder) profiling enabled.
+ */
+tasks.register('jmhProfile', JavaExec) {
+    dependsOn 'jmhJar'
+    group = 'benchmark'
+    description = 'Run JMH benchmarks with JFR profiling enabled'
+
+    mainClass = 'org.openjdk.jmh.Main'
+    classpath = files(tasks.named('jmhJar'))
+
+    def profileDir = layout.buildDirectory.dir("results/jmh-profile")
+    def jfrFile = profileDir.map { it.file("benchmark.jfr") }
+
+    // Reduce iterations for profiling
+    args '-wi', '2'
+    args '-i', '3'
+    args '-f', '1'
+
+    if (project.hasProperty('benchInclude')) {
+        args '.*' + project.benchInclude + '.*'
+    }
+
+    jvmArgs '-XX:+FlightRecorder'
+    jvmArgs "-XX:StartFlightRecording=duration=60s,filename=${jfrFile.get().asFile.absolutePath}"
+
+    if (project.hasProperty('indy') && project.indy == 'true') {
+        jvmArgs '-Dgroovy.target.indy=true'
+    }
+
+    doFirst {
+        profileDir.get().asFile.mkdirs()
+        println "Running JMH with JFR profiling"
+        println "JFR output will be saved to: ${jfrFile.get().asFile}"
+        println ""
+        println "After completion, analyze with:"
+        println "  jfr print ${jfrFile.get().asFile}"
+        println "  jfr summary ${jfrFile.get().asFile}"
+        println "Or open in JDK Mission Control (jmc)"
+    }
+}
+
+/**
+ * Run JMH with GC profiler for memory analysis.
+ */
+tasks.register('jmhGcProfile', JavaExec) {
+    dependsOn 'jmhJar'
+    group = 'benchmark'
+    description = 'Run JMH benchmarks with GC profiler for memory analysis'
+
+    mainClass = 'org.openjdk.jmh.Main'
+    classpath = files(tasks.named('jmhJar'))
+
+    def outputFile = jmhResultsDir.map { it.file("gc-profile-results.txt") }
+
+    args '-rf', 'text'
+    args '-rff', outputFile.get().asFile.absolutePath
+    args '-prof', 'gc'
+
+    if (project.hasProperty('benchInclude')) {
+        args '.*' + project.benchInclude + '.*'
+    }
+
+    if (project.hasProperty('indy') && project.indy == 'true') {
+        jvmArgs '-Dgroovy.target.indy=true'
+    }
+
+    doFirst {
+        println "Running JMH with GC profiler"
+        println "Results will be saved to: ${outputFile.get().asFile}"
+    }
+}
+
+// ============================================================================
+// Baseline Management
+// ============================================================================
+
+def baselinesDir = file("baselines")
+
+/**
+ * Save current benchmark results as a baseline.
+ */
+tasks.register('jmhSaveBaseline') {
+    dependsOn 'jmh'
+    group = 'benchmark'
+    description = 'Save current JMH results as a baseline for future comparison'
+
+    doLast {
+        def baselineName = project.hasProperty('baselineName') ? project.baselineName : "baseline-${new Date().format('yyyyMMdd-HHmmss')}"
+        def resultsFile = jmhResultsDir.get().file("results.txt").asFile
+        def baselineFile = new File(baselinesDir, "${baselineName}.txt")
+
+        if (!resultsFile.exists()) {
+            println "ERROR: No JMH results found. Run jmh first."
+            return
+        }
+
+        baselinesDir.mkdirs()
+        baselineFile.text = resultsFile.text
+
+        println "Baseline saved to: ${baselineFile}"
+    }
+}
+
+/**
+ * Compare current results against a saved baseline.
+ */
+tasks.register('jmhCompareBaseline') {
+    dependsOn 'jmh'
+    group = 'benchmark'
+    description = 'Compare current JMH results against a saved baseline'
+
+    doLast {
+        def baselineName = project.hasProperty('baselineName') ? project.baselineName : null
+        def baselineFile = baselineName ? new File(baselinesDir, "${baselineName}.txt") : null
+
+        // Find most recent baseline if not specified
+        if (!baselineFile?.exists()) {
+            def baselines = baselinesDir.listFiles()?.findAll { it.name.endsWith('.txt') }?.sort { -it.lastModified() }
+            baselineFile = baselines?.first()
+        }
+
+        if (!baselineFile?.exists()) {
+            println "ERROR: No baseline found. Run jmhSaveBaseline first."
+            return
+        }
+
+        def currentFile = jmhResultsDir.get().file("results.txt").asFile
+        if (!currentFile.exists()) {
+            println "ERROR: No current results found. Run jmh first."
+            return
+        }
+
+        def parseResults = { File file ->
+            def results = [:]
+            file.eachLine { line ->
+                def match = line =~ /^(\S+)\s+(\S+)\s+(\d+)\s+([\d.]+)\s*±?\s*([\d.]*)\s*(\S+)/
+                if (match) {
+                    def name = match[0][1]
+                    def score = match[0][4] as double
+                    results[name] = score
+                }
+            }
+            results
+        }
+
+        def baseline = parseResults(baselineFile)
+        def current = parseResults(currentFile)
+
+        println ""
+        println "=" * 80
+        println "BASELINE COMPARISON"
+        println "Baseline: ${baselineFile.name}"
+        println "Current:  ${currentFile.name}"
+        println "=" * 80
+        println ""
+        println String.format("%-50s %12s %12s %12s", "Benchmark", "Baseline", "Current", "Change")
+        println "-" * 90
+
+        def allBenchmarks = (baseline.keySet() + current.keySet()).unique().sort()
+        def regressions = []
+        def improvements = []
+
+        allBenchmarks.each { name ->
+            def baseScore = baseline[name]
+            def currScore = current[name]
+
+            if (baseScore && currScore) {
+                def change = ((currScore - baseScore) / baseScore) * 100
+                def changeStr = String.format("%+.1f%%", change)
+
+                if (change > 10) {
+                    changeStr += " REGRESSION"
+                    regressions << [name: name, change: change]
+                } else if (change < -10) {
+                    changeStr += " IMPROVED"
+                    improvements << [name: name, change: change]
+                }
+
+                println String.format("%-50s %12.3f %12.3f %12s",
+                    name.length() > 50 ? "..." + name[-47..-1] : name,
+                    baseScore, currScore, changeStr)
+            }
+        }
+
+        println ""
+        if (regressions) {
+            println "REGRESSIONS (>10% slower):"
+            regressions.each { println "  - ${it.name}: ${String.format('%+.1f%%', it.change)}" }
+        }
+        if (improvements) {
+            println "IMPROVEMENTS (>10% faster):"
+            improvements.each { println "  - ${it.name}: ${String.format('%+.1f%%', it.change)}" }
+        }
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/controller/ControllerSimulator.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/controller/ControllerSimulator.groovy
new file mode 100644
index 0000000000..0ce80def8b
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/controller/ControllerSimulator.groovy
@@ -0,0 +1,417 @@
+/*
+ *  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.grailslike.controller
+
+import org.apache.groovy.bench.grailslike.model.*
+import org.apache.groovy.bench.grailslike.service.*
+
+/**
+ * Simulates a Grails controller with typical request handling patterns.
+ * Controllers in Grails exercise:
+ * - Parameter binding (params map access)
+ * - Service method calls (dynamic dispatch)
+ * - Model building for views
+ * - Redirect/render decisions
+ */
+class ControllerSimulator {
+
+    // Injected services
+    DynamicService dynamicService
+    TransactionalSimulator transactionalService
+
+    // Request context simulation
+    Map params = [:]
+    Map model = [:]
+    Map flash = [:]
+    String redirectUrl
+    String renderView
+
+    // Session simulation
+    Map session = [:]
+
+    /**
+     * Reset controller state for new request.
+     */
+    void reset() {
+        params.clear()
+        model.clear()
+        redirectUrl = null
+        renderView = null
+    }
+
+    // ========================================================================
+    // Person Controller Actions
+    // ========================================================================
+
+    /**
+     * List action - common index page pattern.
+     */
+    Map listPeople() {
+        def max = ParamsHelper.parseInt(params,'max', 10)
+        def offset = ParamsHelper.parseInt(params,'offset', 0)
+        def sort = params.sort ?: 'lastName'
+        def order = params.order ?: 'asc'
+
+        def people = dynamicService.people
+        def total = people.size()
+
+        // Sort
+        def sorted = people.sort { a, b ->
+            def result = a."$sort" <=> b."$sort"
+            order == 'desc' ? -result : result
+        }
+
+        // Paginate
+        def list = sorted.drop(offset).take(max)
+
+        model = [
+            personList: list,
+            personCount: total,
+            params: [max: max, offset: offset, sort: sort, order: order]
+        ]
+        renderView = 'list'
+        model
+    }
+
+    /**
+     * Show action - detail page pattern.
+     */
+    Map showPerson() {
+        def id = ParamsHelper.parseLong(params,'id')
+        def person = dynamicService.people.find { it.id == id }
+
+        if (!person) {
+            flash.error = "Person not found"
+            redirectUrl = '/person/list'
+            return null
+        }
+
+        model = [
+            person: person,
+            orders: person.orders,
+            totalSpent: person.orders.sum { it.total } ?: 0
+        ]
+        renderView = 'show'
+        model
+    }
+
+    /**
+     * Create action - form submission pattern.
+     */
+    Map createPerson() {
+        def person = dynamicService.createPerson([
+            firstName: params.firstName,
+            lastName: params.lastName,
+            email: params.email,
+            phone: params.phone,
+            address: params.address ? [
+                street: params.'address.street',
+                city: params.'address.city',
+                state: params.'address.state',
+                zipCode: params.'address.zipCode'
+            ] : null
+        ])
+
+        if (person) {
+            flash.message = "Person created successfully"
+            redirectUrl = "/person/show/${person.id}"
+            return [person: person]
+        } else {
+            flash.error = "Failed to create person"
+            model = [person: new Person(params)]
+            renderView = 'create'
+            return model
+        }
+    }
+
+    /**
+     * Update action - edit form submission.
+     */
+    Map updatePerson() {
+        def id = ParamsHelper.parseLong(params,'id')
+        def person = dynamicService.updatePerson(id, params.subMap([
+            'firstName', 'lastName', 'email', 'phone'
+        ]))
+
+        if (person) {
+            flash.message = "Person updated successfully"
+            redirectUrl = "/person/show/$id"
+            return [person: person]
+        } else {
+            flash.error = "Failed to update person"
+            redirectUrl = "/person/edit/$id"
+            return null
+        }
+    }
+
+    // ========================================================================
+    // Order Controller Actions
+    // ========================================================================
+
+    /**
+     * List orders with filtering.
+     */
+    Map listOrders() {
+        def status = params.status ? OrderStatus.valueOf(params.status) : null
+        def customerId = ParamsHelper.parseLong(params,'customerId')
+
+        def orders = dynamicService.orders
+        if (status) {
+            orders = orders.findAll { it.status == status }
+        }
+        if (customerId) {
+            orders = orders.findAll { it.customer?.id == customerId }
+        }
+
+        model = [
+            orderList: orders,
+            orderCount: orders.size(),
+            statusOptions: OrderStatus.values()
+        ]
+        renderView = 'list'
+        model
+    }
+
+    /**
+     * Show order with all related data.
+     */
+    Map showOrder() {
+        def id = ParamsHelper.parseLong(params,'id')
+        def order = dynamicService.orders.find { it.id == id }
+
+        if (!order) {
+            flash.error = "Order not found"
+            redirectUrl = '/order/list'
+            return null
+        }
+
+        // Heavy property access for view model
+        model = [
+            order: order,
+            customer: order.customer,
+            items: order.items,
+            itemCount: order.itemCount,
+            subtotal: order.subtotal,
+            tax: order.tax,
+            total: order.total,
+            canCancel: order.canCancel(),
+            statusHistory: [] // Would be loaded from audit log
+        ]
+        renderView = 'show'
+        model
+    }
+
+    /**
+     * Create order from shopping cart.
+     */
+    Map createOrder() {
+        def customerId = ParamsHelper.parseLong(params,'customerId')
+        def customer = dynamicService.people.find { it.id == customerId }
+
+        if (!customer) {
+            flash.error = "Customer not found"
+            redirectUrl = '/order/list'
+            return null
+        }
+
+        // Parse items from params (common pattern)
+        def items = []
+        def itemCount = ParamsHelper.parseInt(params,'itemCount', 0)
+        (0..<itemCount).each { i ->
+            items << [
+                productCode: params."items[$i].productCode",
+                productName: params."items[$i].productName",
+                quantity: ParamsHelper.parseInt(params,"items[$i].quantity", 1),
+                unitPrice: new BigDecimal(params."items[$i].unitPrice" ?: "0")
+            ]
+        }
+
+        def order = dynamicService.createOrder(customer, items)
+
+        if (order) {
+            flash.message = "Order ${order.orderNumber} created"
+            redirectUrl = "/order/show/${order.id}"
+            return [order: order]
+        } else {
+            flash.error = "Failed to create order"
+            model = [customer: customer, items: items]
+            renderView = 'create'
+            return model
+        }
+    }
+
+    /**
+     * Process order action.
+     */
+    Map processOrder() {
+        def id = ParamsHelper.parseLong(params,'id')
+        def order = dynamicService.processOrder(id)
+
+        if (order) {
+            flash.message = "Order ${order.orderNumber} processed"
+        } else {
+            flash.error = "Failed to process order"
+        }
+        redirectUrl = "/order/show/$id"
+        [order: order]
+    }
+
+    // ========================================================================
+    // Dashboard/Report Actions
+    // ========================================================================
+
+    /**
+     * Dashboard with aggregated data - heavy computation.
+     */
+    Map dashboard() {
+        def stats = dynamicService.getOrderStatistics()
+        def topCustomers = dynamicService.getTopCustomers(5)
+        def recentOrders = dynamicService.orders
+            .sort { -it.orderDate.time }
+            .take(10)
+
+        model = [
+            stats: stats,
+            topCustomers: topCustomers,
+            recentOrders: recentOrders,
+            ordersByStatus: OrderStatus.values().collectEntries { status ->
+                [status, dynamicService.orders.count { it.status == status }]
+            }
+        ]
+        renderView = 'dashboard'
+        model
+    }
+
+    /**
+     * Export action - generates large data structure.
+     */
+    Map exportOrders() {
+        def fromDate = ParamsHelper.parseDate(params,'fromDate', 'yyyy-MM-dd') ?: (new Date() - 30)
+        def toDate = ParamsHelper.parseDate(params,'toDate', 'yyyy-MM-dd') ?: new Date()
+
+        def data = dynamicService.exportOrders(fromDate, toDate)
+
+        model = [
+            data: data,
+            fromDate: fromDate,
+            toDate: toDate,
+            recordCount: data.size()
+        ]
+        renderView = 'export'
+        model
+    }
+
+    // ========================================================================
+    // AJAX/JSON Actions
+    // ========================================================================
+
+    /**
+     * JSON response action.
+     */
+    Map jsonOrderDetails() {
+        def id = ParamsHelper.parseLong(params,'id')
+        def order = dynamicService.orders.find { it.id == id }
+
+        if (!order) {
+            return [error: 'Order not found', status: 404]
+        }
+
+        // Build JSON response with nested objects
+        [
+            order: [
+                id: order.id,
+                orderNumber: order.orderNumber,
+                status: order.status?.name(),
+                customer: [
+                    id: order.customer?.id,
+                    name: order.customer?.fullName,
+                    email: order.customer?.email
+                ],
+                items: order.items.collect { item ->
+                    [
+                        productCode: item.productCode,
+                        productName: item.productName,
+                        quantity: item.quantity,
+                        unitPrice: item.unitPrice,
+                        lineTotal: item.lineTotal
+                    ]
+                },
+                totals: [
+                    subtotal: order.subtotal,
+                    tax: order.tax,
+                    total: order.total
+                ]
+            ]
+        ]
+    }
+
+    /**
+     * Autocomplete/search action.
+     */
+    List searchPeople() {
+        def term = params.term?.toLowerCase() ?: ''
+        def max = ParamsHelper.parseInt(params,'max', 10)
+
+        dynamicService.people
+            .findAll { p ->
+                p.fullName.toLowerCase().contains(term) ||
+                p.email?.toLowerCase()?.contains(term)
+            }
+            .take(max)
+            .collect { p ->
+                [id: p.id, label: p.fullName, value: p.email]
+            }
+    }
+}
+
+/**
+ * Utility class for parsing parameters from a Map.
+ * Similar to Grails' params object.
+ */
+class ParamsHelper {
+    static Long parseLong(Map params, String key, Long defaultValue = null) {
+        def value = params[key]
+        if (value == null) return defaultValue
+        try {
+            return value.toString().toLong()
+        } catch (NumberFormatException e) {
+            return defaultValue
+        }
+    }
+
+    static Integer parseInt(Map params, String key, Integer defaultValue = null) {
+        def value = params[key]
+        if (value == null) return defaultValue
+        try {
+            return value.toString().toInteger()
+        } catch (NumberFormatException e) {
+            return defaultValue
+        }
+    }
+
+    static Date parseDate(Map params, String key, String format, Date defaultValue = null) {
+        def value = params[key]
+        if (value == null) return defaultValue
+        try {
+            return Date.parse(format, value.toString())
+        } catch (Exception e) {
+            return defaultValue
+        }
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/controller/RequestLifecycleBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/controller/RequestLifecycleBench.java
new file mode 100644
index 0000000000..81fcf95c3b
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/controller/RequestLifecycleBench.java
@@ -0,0 +1,382 @@
+/*
+ *  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.grailslike.controller;
+
+import org.apache.groovy.bench.grailslike.model.*;
+import org.apache.groovy.bench.grailslike.service.*;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.Blackhole;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Benchmarks simulating full HTTP request lifecycle in a Grails-like application.
+ * <p>
+ * This exercises the complete call path:
+ * <ol>
+ *   <li>Controller receives request (params parsing)</li>
+ *   <li>Service layer invocation (transactional context)</li>
+ *   <li>Domain object manipulation (property access, validation)</li>
+ *   <li>Model building for view</li>
+ * </ol>
+ * <p>
+ * Run with: ./gradlew -Pindy=true -PbenchInclude=RequestLifecycle :perf:jmh
+ */
+@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+public class RequestLifecycleBench {
+
+    // ========================================================================
+    // State classes
+    // ========================================================================
+
+    @State(Scope.Thread)
+    public static class ApplicationState {
+        ControllerSimulator controller;
+        DynamicService dynamicService;
+        TransactionalSimulator transactionalService;
+        Random random;
+
+        @Setup(Level.Trial)
+        public void setup() {
+            random = new Random(42);
+
+            // Initialize services
+            dynamicService = new DynamicService();
+            transactionalService = new TransactionalSimulator();
+            transactionalService.setDynamicService(dynamicService);
+
+            // Initialize controller
+            controller = new ControllerSimulator();
+            controller.setDynamicService(dynamicService);
+            controller.setTransactionalService(transactionalService);
+
+            // Seed data
+            seedTestData();
+        }
+
+        private void seedTestData() {
+            // Create 100 people
+            for (int i = 0; i < 100; i++) {
+                Person person = new Person();
+                person.setFirstName("First" + i);
+                person.setLastName("Last" + i);
+                person.setEmail("person" + i + "@example.com");
+                person.setPhone("555-" + String.format("%04d", i));
+                person.save();
+                dynamicService.getPeople().add(person);
+            }
+
+            // Create orders for some people
+            List<Person> people = dynamicService.getPeople();
+            for (int i = 0; i < 50; i++) {
+                Person customer = people.get(random.nextInt(people.size()));
+
+                Order order = new Order();
+                order.setOrderNumber(Order.generateOrderNumber());
+                order.setOrderDate(new Date());
+                order.setCustomer(customer);
+
+                // Add items
+                int itemCount = random.nextInt(5) + 1;
+                for (int j = 0; j < itemCount; j++) {
+                    OrderItem item = ProductCatalog.randomItem(random);
+                    order.addToItems(item);
+                }
+
+                order.save();
+                dynamicService.getOrders().add(order);
+                customer.addToOrders(order);
+            }
+        }
+
+        @Setup(Level.Invocation)
+        public void resetController() {
+            controller.reset();
+        }
+    }
+
+    // ========================================================================
+    // List/Index Action Benchmarks
+    // ========================================================================
+
+    /**
+     * List people action - pagination, sorting.
+     */
+    @Benchmark
+    public Object request_listPeople(ApplicationState state, Blackhole bh) {
+        state.controller.getParams().put("max", "20");
+        state.controller.getParams().put("offset", "0");
+        state.controller.getParams().put("sort", "lastName");
+        state.controller.getParams().put("order", "asc");
+
+        Map result = state.controller.listPeople();
+        bh.consume(result);
+        return result;
+    }
+
+    /**
+     * List orders with status filter.
+     */
+    @Benchmark
+    public Object request_listOrders(ApplicationState state, Blackhole bh) {
+        state.controller.getParams().put("status", "PENDING");
+
+        Map result = state.controller.listOrders();
+        bh.consume(result);
+        return result;
+    }
+
+    // ========================================================================
+    // Show/Detail Action Benchmarks
+    // ========================================================================
+
+    /**
+     * Show person detail - loads related orders.
+     */
+    @Benchmark
+    public Object request_showPerson(ApplicationState state, Blackhole bh) {
+        List<Person> people = state.dynamicService.getPeople();
+        Person person = people.get(state.random.nextInt(people.size()));
+
+        state.controller.getParams().put("id", person.getId().toString());
+
+        Map result = state.controller.showPerson();
+        bh.consume(result);
+        return result;
+    }
+
+    /**
+     * Show order detail - heavy property access.
+     */
+    @Benchmark
+    public Object request_showOrder(ApplicationState state, Blackhole bh) {
+        List<Order> orders = state.dynamicService.getOrders();
+        if (orders.isEmpty()) return null;
+
+        Order order = orders.get(state.random.nextInt(orders.size()));
+        state.controller.getParams().put("id", order.getId().toString());
+
+        Map result = state.controller.showOrder();
+        bh.consume(result);
+        return result;
+    }
+
+    // ========================================================================
+    // Create/Update Action Benchmarks
+    // ========================================================================
+
+    /**
+     * Create person - form submission, validation, save.
+     */
+    @Benchmark
+    public Object request_createPerson(ApplicationState state, Blackhole bh) {
+        int i = state.random.nextInt(10000);
+        state.controller.getParams().put("firstName", "New" + i);
+        state.controller.getParams().put("lastName", "Person" + i);
+        state.controller.getParams().put("email", "new" + i + "@example.com");
+        state.controller.getParams().put("phone", "555-" + String.format("%04d", i));
+
+        Map result = state.controller.createPerson();
+        bh.consume(result);
+        return result;
+    }
+
+    /**
+     * Create order with items - complex nested save.
+     */
+    @Benchmark
+    public Object request_createOrder(ApplicationState state, Blackhole bh) {
+        List<Person> people = state.dynamicService.getPeople();
+        Person customer = people.get(state.random.nextInt(people.size()));
+
+        state.controller.getParams().put("customerId", customer.getId().toString());
+        state.controller.getParams().put("itemCount", "3");
+
+        // Add items to params
+        for (int i = 0; i < 3; i++) {
+            state.controller.getParams().put("items[" + i + "].productCode", "PROD-" + i);
+            state.controller.getParams().put("items[" + i + "].productName", "Product " + i);
+            state.controller.getParams().put("items[" + i + "].quantity", String.valueOf(state.random.nextInt(5) + 1));
+            state.controller.getParams().put("items[" + i + "].unitPrice", "99.99");
+        }
+
+        Map result = state.controller.createOrder();
+        bh.consume(result);
+        return result;
+    }
+
+    /**
+     * Update person - edit form submission.
+     */
+    @Benchmark
+    public Object request_updatePerson(ApplicationState state, Blackhole bh) {
+        List<Person> people = state.dynamicService.getPeople();
+        Person person = people.get(state.random.nextInt(people.size()));
+
+        state.controller.getParams().put("id", person.getId().toString());
+        state.controller.getParams().put("firstName", person.getFirstName() + "-updated");
+        state.controller.getParams().put("lastName", person.getLastName());
+        state.controller.getParams().put("email", person.getEmail());
+
+        Map result = state.controller.updatePerson();
+        bh.consume(result);
+        return result;
+    }
+
+    // ========================================================================
+    // Dashboard/Report Benchmarks
+    // ========================================================================
+
+    /**
+     * Dashboard - aggregates, top N queries, recent items.
+     */
+    @Benchmark
+    public Object request_dashboard(ApplicationState state, Blackhole bh) {
+        Map result = state.controller.dashboard();
+        bh.consume(result);
+        return result;
+    }
+
+    /**
+     * Export orders - large data transformation.
+     */
+    @Benchmark
+    public Object request_exportOrders(ApplicationState state, Blackhole bh) {
+        Map result = state.controller.exportOrders();
+        bh.consume(result);
+        return result;
+    }
+
+    // ========================================================================
+    // AJAX/API Benchmarks
+    // ========================================================================
+
+    /**
+     * JSON order details - nested object serialization.
+     */
+    @Benchmark
+    public Object request_jsonOrderDetails(ApplicationState state, Blackhole bh) {
+        List<Order> orders = state.dynamicService.getOrders();
+        if (orders.isEmpty()) return null;
+
+        Order order = orders.get(state.random.nextInt(orders.size()));
+        state.controller.getParams().put("id", order.getId().toString());
+
+        Map result = state.controller.jsonOrderDetails();
+        bh.consume(result);
+        return result;
+    }
+
+    /**
+     * Person search - autocomplete pattern.
+     */
+    @Benchmark
+    public Object request_searchPeople(ApplicationState state, Blackhole bh) {
+        state.controller.getParams().put("term", "First");
+        state.controller.getParams().put("max", "10");
+
+        List result = state.controller.searchPeople();
+        bh.consume(result);
+        return result;
+    }
+
+    // ========================================================================
+    // Full Request Cycle Benchmarks
+    // ========================================================================
+
+    /**
+     * Complete CRUD cycle: list -> show -> edit -> save.
+     */
+    @Benchmark
+    public Object request_crudCycle(ApplicationState state, Blackhole bh) {
+        // List
+        state.controller.getParams().put("max", "10");
+        Map listResult = state.controller.listPeople();
+        bh.consume(listResult);
+        state.controller.reset();
+
+        // Show first person
+        List<Person> people = state.dynamicService.getPeople();
+        if (people.isEmpty()) return null;
+        Person person = people.get(0);
+        state.controller.getParams().put("id", person.getId().toString());
+        Map showResult = state.controller.showPerson();
+        bh.consume(showResult);
+        state.controller.reset();
+
+        // Update
+        state.controller.getParams().put("id", person.getId().toString());
+        state.controller.getParams().put("firstName", "Updated");
+        state.controller.getParams().put("lastName", person.getLastName());
+        state.controller.getParams().put("email", person.getEmail());
+        Map updateResult = state.controller.updatePerson();
+        bh.consume(updateResult);
+
+        return updateResult;
+    }
+
+    /**
+     * Order processing cycle: create -> process -> show.
+     */
+    @Benchmark
+    public Object request_orderCycle(ApplicationState state, Blackhole bh) {
+        List<Person> people = state.dynamicService.getPeople();
+        Person customer = people.get(state.random.nextInt(people.size()));
+
+        // Create order
+        state.controller.getParams().put("customerId", customer.getId().toString());
+        state.controller.getParams().put("itemCount", "2");
+        state.controller.getParams().put("items[0].productCode", "TEST-1");
+        state.controller.getParams().put("items[0].productName", "Test Product 1");
+        state.controller.getParams().put("items[0].quantity", "1");
+        state.controller.getParams().put("items[0].unitPrice", "49.99");
+        state.controller.getParams().put("items[1].productCode", "TEST-2");
+        state.controller.getParams().put("items[1].productName", "Test Product 2");
+        state.controller.getParams().put("items[1].quantity", "2");
+        state.controller.getParams().put("items[1].unitPrice", "29.99");
+
+        Map createResult = state.controller.createOrder();
+        bh.consume(createResult);
+
+        // Get order ID from redirect URL
+        List<Order> orders = state.dynamicService.getOrders();
+        if (orders.isEmpty()) return createResult;
+        Order newOrder = orders.get(orders.size() - 1);
+        state.controller.reset();
+
+        // Process order
+        state.controller.getParams().put("id", newOrder.getId().toString());
+        Map processResult = state.controller.processOrder();
+        bh.consume(processResult);
+        state.controller.reset();
+
+        // Show order
+        state.controller.getParams().put("id", newOrder.getId().toString());
+        Map showResult = state.controller.showOrder();
+        bh.consume(showResult);
+
+        return showResult;
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/AdditionalDomains.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/AdditionalDomains.groovy
new file mode 100644
index 0000000000..0e19e36062
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/AdditionalDomains.groovy
@@ -0,0 +1,428 @@
+/*
+ *  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.grailslike.model
+
+/**
+ * Additional domain classes to simulate a realistic Grails application
+ * with many different entity types (polymorphic dispatch scenarios).
+ */
+
+// ============================================================================
+// User/Security Domain Classes
+// ============================================================================
+
+class User extends DomainObject {
+    String username
+    String passwordHash
+    String email
+    boolean enabled = true
+    boolean accountLocked = false
+    Date lastLogin
+    List<Role> roles = []
+
+    String getDisplayName() { username }
+    boolean hasRole(String roleName) { roles.any { it.authority == roleName } }
+
+    @Override
+    Map toMap() {
+        super.toMap() + [username: username, email: email, enabled: enabled, roles: roles*.authority]
+    }
+}
+
+class Role extends DomainObject {
+    String authority
+    String description
+
+    @Override
+    Map toMap() { super.toMap() + [authority: authority, description: description] }
+}
+
+class AuditLog extends DomainObject {
+    String action
+    String entityType
+    Long entityId
+    String username
+    String ipAddress
+    Date timestamp = new Date()
+    String details
+
+    @Override
+    Map toMap() {
+        super.toMap() + [action: action, entityType: entityType, entityId: entityId,
+                         username: username, ipAddress: ipAddress, details: details]
+    }
+}
+
+// ============================================================================
+// Product/Inventory Domain Classes
+// ============================================================================
+
+class Product extends DomainObject {
+    String sku
+    String name
+    String description
+    BigDecimal price
+    BigDecimal cost
+    Category category
+    List<ProductAttribute> attributes = []
+    boolean active = true
+
+    BigDecimal getMargin() { price - cost }
+    BigDecimal getMarginPercent() { cost > 0 ? (margin / cost) * 100 : 0 }
+
+    @Override
+    Map toMap() {
+        super.toMap() + [sku: sku, name: name, price: price, margin: margin,
+                         category: category?.name, attributes: attributes*.toMap()]
+    }
+}
+
+class Category extends DomainObject {
+    String name
+    String code
+    Category parent
+    List<Category> children = []
+    int sortOrder = 0
+
+    String getFullPath() {
+        parent ? "${parent.fullPath} > $name" : name
+    }
+
+    @Override
+    Map toMap() { super.toMap() + [name: name, code: code, fullPath: fullPath] }
+}
+
+class ProductAttribute extends DomainObject {
+    String name
+    String value
+    String type = 'STRING'
+    Product product
+
+    @Override
+    Map toMap() { super.toMap() + [name: name, value: value, type: type] }
+}
+
+class Inventory extends DomainObject {
+    Product product
+    Warehouse warehouse
+    int quantityOnHand = 0
+    int quantityReserved = 0
+    int reorderPoint = 10
+    int reorderQuantity = 50
+
+    int getAvailable() { quantityOnHand - quantityReserved }
+    boolean needsReorder() { available <= reorderPoint }
+
+    @Override
+    Map toMap() {
+        super.toMap() + [productId: product?.id, warehouseId: warehouse?.id,
+                         onHand: quantityOnHand, available: available, needsReorder: needsReorder()]
+    }
+}
+
+class Warehouse extends DomainObject {
+    String code
+    String name
+    Address address
+    boolean active = true
+
+    @Override
+    Map toMap() { super.toMap() + [code: code, name: name, address: address?.toMap()] }
+}
+
+// ============================================================================
+// Customer/CRM Domain Classes
+// ============================================================================
+
+class Customer extends Person {
+    String customerNumber
+    CustomerType customerType = CustomerType.INDIVIDUAL
+    BigDecimal creditLimit = BigDecimal.ZERO
+    BigDecimal balance = BigDecimal.ZERO
+    String notes
+    List<Contact> contacts = []
+
+    BigDecimal getAvailableCredit() { creditLimit - balance }
+
+    @Override
+    Map toMap() {
+        super.toMap() + [customerNumber: customerNumber, customerType: customerType?.name(),
+                         creditLimit: creditLimit, balance: balance, availableCredit: availableCredit]
+    }
+}
+
+enum CustomerType {
+    INDIVIDUAL, BUSINESS, WHOLESALE, VIP
+}
+
+class Contact extends DomainObject {
+    String firstName
+    String lastName
+    String email
+    String phone
+    String title
+    boolean isPrimary = false
+    Customer customer
+
+    String getFullName() { "$firstName $lastName" }
+
+    @Override
+    Map toMap() { super.toMap() + [fullName: fullName, email: email, phone: phone, isPrimary: isPrimary] }
+}
+
+// ============================================================================
+// Shipping/Logistics Domain Classes
+// ============================================================================
+
+class Shipment extends DomainObject {
+    Order order
+    String trackingNumber
+    String carrier
+    ShipmentStatus status = ShipmentStatus.PENDING
+    Date shipDate
+    Date deliveryDate
+    BigDecimal weight
+    BigDecimal shippingCost
+    Address fromAddress
+    Address toAddress
+    List<ShipmentItem> items = []
+
+    int getTotalQuantity() { items.sum { it.quantity } ?: 0 }
+
+    @Override
+    Map toMap() {
+        super.toMap() + [trackingNumber: trackingNumber, carrier: carrier, status: status?.name(),
+                         totalQuantity: totalQuantity, shippingCost: shippingCost]
+    }
+}
+
+enum ShipmentStatus {
+    PENDING, PICKED, PACKED, SHIPPED, IN_TRANSIT, DELIVERED, RETURNED
+}
+
+class ShipmentItem extends DomainObject {
+    Shipment shipment
+    OrderItem orderItem
+    int quantity
+    String lotNumber
+
+    @Override
+    Map toMap() { super.toMap() + [orderItemId: orderItem?.id, quantity: quantity, lotNumber: lotNumber] }
+}
+
+// ============================================================================
+// Payment/Financial Domain Classes
+// ============================================================================
+
+class Payment extends DomainObject {
+    Order order
+    BigDecimal amount
+    PaymentMethod method
+    PaymentStatus status = PaymentStatus.PENDING
+    String transactionId
+    Date paymentDate
+    String notes
+
+    @Override
+    Map toMap() {
+        super.toMap() + [orderId: order?.id, amount: amount, method: method?.name(),
+                         status: status?.name(), transactionId: transactionId]
+    }
+}
+
+enum PaymentMethod {
+    CREDIT_CARD, DEBIT_CARD, PAYPAL, BANK_TRANSFER, CHECK, CASH
+}
+
+enum PaymentStatus {
+    PENDING, AUTHORIZED, CAPTURED, REFUNDED, FAILED, CANCELLED
+}
+
+class Invoice extends DomainObject {
+    String invoiceNumber
+    Order order
+    Customer customer
+    Date invoiceDate
+    Date dueDate
+    BigDecimal subtotal
+    BigDecimal tax
+    BigDecimal total
+    InvoiceStatus status = InvoiceStatus.DRAFT
+    List<InvoiceLine> lines = []
+
+    boolean isOverdue() { status == InvoiceStatus.SENT && new Date() > dueDate }
+
+    @Override
+    Map toMap() {
+        super.toMap() + [invoiceNumber: invoiceNumber, total: total, status: status?.name(), isOverdue: isOverdue()]
+    }
+}
+
+enum InvoiceStatus {
+    DRAFT, SENT, PAID, OVERDUE, CANCELLED, VOID
+}
+
+class InvoiceLine extends DomainObject {
+    Invoice invoice
+    String description
+    int quantity
+    BigDecimal unitPrice
+    BigDecimal lineTotal
+
+    @Override
+    Map toMap() { super.toMap() + [description: description, quantity: quantity, lineTotal: lineTotal] }
+}
+
+// ============================================================================
+// Content/CMS Domain Classes
+// ============================================================================
+
+class Article extends DomainObject {
+    String title
+    String slug
+    String content
+    String summary
+    User author
+    ArticleStatus status = ArticleStatus.DRAFT
+    Date publishDate
+    List<Tag> tags = []
+    List<Comment> comments = []
+
+    int getWordCount() { content?.split(/\s+/)?.size() ?: 0 }
+    int getCommentCount() { comments.size() }
+
+    @Override
+    Map toMap() {
+        super.toMap() + [title: title, slug: slug, author: author?.username,
+                         status: status?.name(), wordCount: wordCount, commentCount: commentCount]
+    }
+}
+
+enum ArticleStatus {
+    DRAFT, REVIEW, PUBLISHED, ARCHIVED
+}
+
+class Tag extends DomainObject {
+    String name
+    String slug
+    int usageCount = 0
+
+    @Override
+    Map toMap() { super.toMap() + [name: name, slug: slug, usageCount: usageCount] }
+}
+
+class Comment extends DomainObject {
+    Article article
+    User author
+    String content
+    boolean approved = false
+    Comment parent
+    List<Comment> replies = []
+
+    int getReplyCount() { replies.size() }
+
+    @Override
+    Map toMap() {
+        super.toMap() + [author: author?.username, content: content?.take(100),
+                         approved: approved, replyCount: replyCount]
+    }
+}
+
+// ============================================================================
+// Configuration/Settings Domain Classes
+// ============================================================================
+
+class Setting extends DomainObject {
+    String key
+    String value
+    String type = 'STRING'
+    String description
+    boolean encrypted = false
+
+    Object getParsedValue() {
+        switch (type) {
+            case 'INTEGER': return value?.toInteger()
+            case 'BOOLEAN': return value?.toBoolean()
+            case 'DECIMAL': return value ? new BigDecimal(value) : null
+            default: return value
+        }
+    }
+
+    @Override
+    Map toMap() { super.toMap() + [key: key, value: encrypted ? '***' : value, type: type] }
+}
+
+class Notification extends DomainObject {
+    User user
+    String title
+    String message
+    NotificationType type
+    boolean read = false
+    Date readAt
+    String actionUrl
+
+    @Override
+    Map toMap() {
+        super.toMap() + [title: title, type: type?.name(), read: read, actionUrl: actionUrl]
+    }
+}
+
+enum NotificationType {
+    INFO, SUCCESS, WARNING, ERROR, ALERT
+}
+
+// ============================================================================
+// Reporting/Analytics Domain Classes
+// ============================================================================
+
+class Report extends DomainObject {
+    String name
+    String description
+    String query
+    ReportType type
+    User owner
+    boolean shared = false
+    Map<String, Object> parameters = [:]
+
+    @Override
+    Map toMap() { super.toMap() + [name: name, type: type?.name(), shared: shared] }
+}
+
+enum ReportType {
+    TABLE, CHART, DASHBOARD, EXPORT
+}
+
+class ScheduledTask extends DomainObject {
+    String name
+    String cronExpression
+    String taskClass
+    boolean enabled = true
+    Date lastRun
+    Date nextRun
+    String lastResult
+    int runCount = 0
+    int failCount = 0
+
+    double getSuccessRate() { runCount > 0 ? ((runCount - failCount) / runCount) * 100 : 0 }
+
+    @Override
+    Map toMap() {
+        super.toMap() + [name: name, enabled: enabled, lastRun: lastRun, successRate: successRate]
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/DomainObject.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/DomainObject.groovy
new file mode 100644
index 0000000000..f83d3c18aa
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/DomainObject.groovy
@@ -0,0 +1,163 @@
+/*
+ *  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.grailslike.model
+
+/**
+ * Base domain object simulating GORM entity behavior.
+ * Provides dynamic property access, dirty tracking, and validation-like patterns.
+ */
+abstract class DomainObject {
+    Long id
+    Long version = 0L
+    Date dateCreated
+    Date lastUpdated
+
+    // Dynamic properties storage (like GORM's dynamic attributes)
+    protected Map<String, Object> dynamicProperties = [:]
+
+    // Dirty tracking (simulates Hibernate dirty checking)
+    protected Set<String> dirtyProperties = new HashSet<>()
+    protected Map<String, Object> originalValues = [:]
+
+    // Constraints-like validation errors
+    protected List<String> errors = []
+
+    /**
+     * Dynamic property access - common in Grails views and controllers.
+     */
+    def propertyMissing(String name) {
+        dynamicProperties[name]
+    }
+
+    def propertyMissing(String name, Object value) {
+        if (!originalValues.containsKey(name)) {
+            originalValues[name] = dynamicProperties[name]
+        }
+        dirtyProperties.add(name)
+        dynamicProperties[name] = value
+    }
+
+    /**
+     * Simulates GORM's isDirty() check.
+     */
+    boolean isDirty() {
+        !dirtyProperties.isEmpty()
+    }
+
+    boolean isDirty(String propertyName) {
+        dirtyProperties.contains(propertyName)
+    }
+
+    /**
+     * Simulates GORM's getPersistentValue().
+     */
+    Object getPersistentValue(String propertyName) {
+        originalValues.containsKey(propertyName) ? originalValues[propertyName] : this."$propertyName"
+    }
+
+    /**
+     * Simulates GORM save() - exercises method dispatch and property access.
+     */
+    boolean save(Map args = [:]) {
+        if (!validate()) {
+            return false
+        }
+
+        if (id == null) {
+            id = System.nanoTime()
+            dateCreated = new Date()
+        }
+        lastUpdated = new Date()
+        version++
+
+        // Clear dirty tracking
+        dirtyProperties.clear()
+        originalValues.clear()
+
+        if (args.flush) {
+            // Simulate flush
+            onFlush()
+        }
+        true
+    }
+
+    /**
+     * Simulates validation.
+     */
+    boolean validate() {
+        errors.clear()
+        doValidate()
+        errors.isEmpty()
+    }
+
+    protected void doValidate() {
+        // Override in subclasses
+    }
+
+    protected void onFlush() {
+        // Override in subclasses
+    }
+
+    /**
+     * Simulates GORM's refresh().
+     */
+    void refresh() {
+        dirtyProperties.clear()
+        originalValues.clear()
+    }
+
+    /**
+     * Simulates GORM's discard().
+     */
+    void discard() {
+        // Restore original values
+        originalValues.each { name, value ->
+            if (dynamicProperties.containsKey(name)) {
+                dynamicProperties[name] = value
+            }
+        }
+        dirtyProperties.clear()
+        originalValues.clear()
+    }
+
+    /**
+     * Simulates GORM's toMap() - heavy property access.
+     */
+    Map toMap() {
+        def result = [
+            id: id,
+            version: version,
+            dateCreated: dateCreated,
+            lastUpdated: lastUpdated
+        ]
+        result.putAll(dynamicProperties)
+        result
+    }
+
+    /**
+     * Simulates hasErrors().
+     */
+    boolean hasErrors() {
+        !errors.isEmpty()
+    }
+
+    List<String> getErrors() {
+        errors.asImmutable()
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/Order.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/Order.groovy
new file mode 100644
index 0000000000..713e09be28
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/Order.groovy
@@ -0,0 +1,191 @@
+/*
+ *  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.grailslike.model
+
+/**
+ * Order domain object simulating a typical e-commerce order.
+ * Has belongsTo relationship with Person and hasMany with OrderItem.
+ */
+class Order extends DomainObject {
+    String orderNumber
+    Date orderDate
+    OrderStatus status = OrderStatus.PENDING
+    String shippingMethod
+    String notes
+
+    // belongsTo relationship
+    Person customer
+
+    // hasMany relationship
+    List<OrderItem> items = []
+
+    // Shipping address (can differ from customer address)
+    Address shippingAddress
+
+    // Computed properties (heavy property access pattern)
+    BigDecimal getSubtotal() {
+        items.sum { it.lineTotal } ?: BigDecimal.ZERO
+    }
+
+    BigDecimal getTaxRate() {
+        new BigDecimal("0.08") // 8% tax
+    }
+
+    BigDecimal getTax() {
+        subtotal * taxRate
+    }
+
+    BigDecimal getTotal() {
+        subtotal + tax
+    }
+
+    int getItemCount() {
+        items.sum { it.quantity } ?: 0
+    }
+
+    // Simulates GORM's addToItems()
+    Order addToItems(OrderItem item) {
+        item.order = this
+        items.add(item)
+        this
+    }
+
+    // Simulates GORM's removeFromItems()
+    Order removeFromItems(OrderItem item) {
+        items.remove(item)
+        item.order = null
+        this
+    }
+
+    /**
+     * Business logic method - common pattern in Grails services/domain.
+     */
+    boolean canCancel() {
+        status in [OrderStatus.PENDING, OrderStatus.CONFIRMED]
+    }
+
+    boolean cancel() {
+        if (!canCancel()) {
+            errors.add("Order cannot be cancelled in status: $status")
+            return false
+        }
+        status = OrderStatus.CANCELLED
+        true
+    }
+
+    boolean ship(String trackingNumber) {
+        if (status != OrderStatus.CONFIRMED) {
+            errors.add("Order must be confirmed before shipping")
+            return false
+        }
+        status = OrderStatus.SHIPPED
+        dynamicProperties['trackingNumber'] = trackingNumber
+        true
+    }
+
+    boolean complete() {
+        if (status != OrderStatus.SHIPPED) {
+            errors.add("Order must be shipped before completing")
+            return false
+        }
+        status = OrderStatus.COMPLETED
+        true
+    }
+
+    @Override
+    protected void doValidate() {
+        if (!orderNumber?.trim()) {
+            errors.add("orderNumber cannot be blank")
+        }
+        if (!customer) {
+            errors.add("customer is required")
+        }
+        if (items.isEmpty()) {
+            errors.add("order must have at least one item")
+        }
+        // Validate each item
+        items.each { item ->
+            if (!item.validate()) {
+                errors.addAll(item.errors.collect { "Item: $it" })
+            }
+        }
+    }
+
+    @Override
+    Map toMap() {
+        def result = super.toMap()
+        result.putAll([
+            orderNumber: orderNumber,
+            orderDate: orderDate,
+            status: status?.name(),
+            customerId: customer?.id,
+            customerName: customer?.fullName,
+            shippingMethod: shippingMethod,
+            shippingAddress: shippingAddress?.toMap(),
+            items: items*.toMap(),
+            itemCount: itemCount,
+            subtotal: subtotal,
+            tax: tax,
+            total: total,
+            notes: notes
+        ])
+        result
+    }
+
+    /**
+     * Simulates dynamic finder: findByOrderNumber
+     */
+    static Order findByOrderNumber(List<Order> orders, String orderNumber) {
+        orders.find { it.orderNumber == orderNumber }
+    }
+
+    /**
+     * Simulates dynamic finder: findAllByStatus
+     */
+    static List<Order> findAllByStatus(List<Order> orders, OrderStatus status) {
+        orders.findAll { it.status == status }
+    }
+
+    /**
+     * Simulates criteria query: recent orders
+     */
+    static List<Order> findRecentOrders(List<Order> orders, int days) {
+        def cutoff = new Date() - days
+        orders.findAll { it.orderDate >= cutoff }
+              .sort { -it.orderDate.time }
+    }
+
+    /**
+     * Generate unique order number.
+     */
+    static String generateOrderNumber() {
+        "ORD-${System.currentTimeMillis()}-${(Math.random() * 10000).intValue()}"
+    }
+}
+
+/**
+ * Order status enum.
+ */
+enum OrderStatus {
+    PENDING,
+    CONFIRMED,
+    SHIPPED,
+    COMPLETED,
+    CANCELLED
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/OrderItem.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/OrderItem.groovy
new file mode 100644
index 0000000000..5d9cb165bd
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/OrderItem.groovy
@@ -0,0 +1,178 @@
+/*
+ *  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.grailslike.model
+
+/**
+ * OrderItem domain object representing a line item in an order.
+ * Demonstrates nested object access patterns common in Grails views.
+ */
+class OrderItem extends DomainObject {
+    String productCode
+    String productName
+    String description
+    int quantity = 1
+    BigDecimal unitPrice = BigDecimal.ZERO
+    BigDecimal discount = BigDecimal.ZERO
+
+    // belongsTo relationship
+    Order order
+
+    // Product category for filtering
+    String category
+
+    // Computed property: line total
+    BigDecimal getLineTotal() {
+        def base = unitPrice * quantity
+        base - (base * discount)
+    }
+
+    // Computed property: savings
+    BigDecimal getSavings() {
+        unitPrice * quantity * discount
+    }
+
+    // Computed property: has discount
+    boolean getHasDiscount() {
+        discount > BigDecimal.ZERO
+    }
+
+    /**
+     * Update quantity with validation.
+     */
+    boolean updateQuantity(int newQuantity) {
+        if (newQuantity < 1) {
+            errors.add("quantity must be at least 1")
+            return false
+        }
+        if (newQuantity > 100) {
+            errors.add("quantity cannot exceed 100")
+            return false
+        }
+        this.quantity = newQuantity
+        true
+    }
+
+    /**
+     * Apply discount with validation.
+     */
+    boolean applyDiscount(BigDecimal discountRate) {
+        if (discountRate < 0 || discountRate > 1) {
+            errors.add("discount must be between 0 and 1")
+            return false
+        }
+        this.discount = discountRate
+        true
+    }
+
+    @Override
+    protected void doValidate() {
+        if (!productCode?.trim()) {
+            errors.add("productCode cannot be blank")
+        }
+        if (!productName?.trim()) {
+            errors.add("productName cannot be blank")
+        }
+        if (quantity < 1) {
+            errors.add("quantity must be at least 1")
+        }
+        if (unitPrice < BigDecimal.ZERO) {
+            errors.add("unitPrice cannot be negative")
+        }
+        if (discount < BigDecimal.ZERO || discount > BigDecimal.ONE) {
+            errors.add("discount must be between 0 and 1")
+        }
+    }
+
+    @Override
+    Map toMap() {
+        def result = super.toMap()
+        result.putAll([
+            productCode: productCode,
+            productName: productName,
+            description: description,
+            category: category,
+            quantity: quantity,
+            unitPrice: unitPrice,
+            discount: discount,
+            lineTotal: lineTotal,
+            savings: savings,
+            hasDiscount: hasDiscount,
+            orderId: order?.id
+        ])
+        result
+    }
+
+    /**
+     * Factory method for creating test items.
+     */
+    static OrderItem create(String code, String name, int qty, BigDecimal price) {
+        new OrderItem(
+            productCode: code,
+            productName: name,
+            quantity: qty,
+            unitPrice: price
+        )
+    }
+
+    /**
+     * Factory method with discount.
+     */
+    static OrderItem createWithDiscount(String code, String name, int qty, BigDecimal price, BigDecimal discount) {
+        def item = create(code, name, qty, price)
+        item.discount = discount
+        item
+    }
+}
+
+/**
+ * Product catalog for generating test data.
+ */
+class ProductCatalog {
+    static final List<Map> PRODUCTS = [
+        [code: 'LAPTOP-001', name: 'Business Laptop', category: 'Electronics', price: 999.99],
+        [code: 'LAPTOP-002', name: 'Gaming Laptop', category: 'Electronics', price: 1499.99],
+        [code: 'PHONE-001', name: 'Smartphone Pro', category: 'Electronics', price: 799.99],
+        [code: 'PHONE-002', name: 'Smartphone Basic', category: 'Electronics', price: 399.99],
+        [code: 'TABLET-001', name: 'Tablet 10"', category: 'Electronics', price: 599.99],
+        [code: 'CHAIR-001', name: 'Office Chair', category: 'Furniture', price: 299.99],
+        [code: 'DESK-001', name: 'Standing Desk', category: 'Furniture', price: 499.99],
+        [code: 'MONITOR-001', name: '27" Monitor', category: 'Electronics', price: 349.99],
+        [code: 'KEYBOARD-001', name: 'Mechanical Keyboard', category: 'Accessories', price: 149.99],
+        [code: 'MOUSE-001', name: 'Wireless Mouse', category: 'Accessories', price: 79.99],
+        [code: 'HEADSET-001', name: 'Wireless Headset', category: 'Accessories', price: 199.99],
+        [code: 'WEBCAM-001', name: 'HD Webcam', category: 'Accessories', price: 129.99],
+        [code: 'BOOK-001', name: 'Programming Guide', category: 'Books', price: 49.99],
+        [code: 'BOOK-002', name: 'Architecture Patterns', category: 'Books', price: 59.99],
+        [code: 'SOFTWARE-001', name: 'IDE License', category: 'Software', price: 199.99],
+    ]
+
+    static OrderItem randomItem(Random random = new Random()) {
+        def product = PRODUCTS[random.nextInt(PRODUCTS.size())]
+        OrderItem.create(
+            product.code as String,
+            product.name as String,
+            random.nextInt(5) + 1,
+            new BigDecimal(product.price.toString())
+        )
+    }
+
+    static List<OrderItem> randomItems(int count, Random random = new Random()) {
+        (1..count).collect { randomItem(random) }
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/Person.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/Person.groovy
new file mode 100644
index 0000000000..44d26622cd
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/model/Person.groovy
@@ -0,0 +1,144 @@
+/*
+ *  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.grailslike.model
+
+/**
+ * Person domain object simulating a typical Grails domain class.
+ * Has relationships to Order (hasMany pattern).
+ */
+class Person extends DomainObject {
+    String firstName
+    String lastName
+    String email
+    Date birthDate
+    String phone
+    Address address
+
+    // hasMany relationship simulation
+    List<Order> orders = []
+
+    // Computed property (common in Grails)
+    String getFullName() {
+        "$firstName $lastName"
+    }
+
+    // Another computed property
+    int getAge() {
+        if (birthDate == null) return 0
+        def now = Calendar.getInstance()
+        def birth = Calendar.getInstance()
+        birth.time = birthDate
+        int age = now.get(Calendar.YEAR) - birth.get(Calendar.YEAR)
+        if (now.get(Calendar.DAY_OF_YEAR) < birth.get(Calendar.DAY_OF_YEAR)) {
+            age--
+        }
+        age
+    }
+
+    // Simulates GORM's addToOrders()
+    Person addToOrders(Order order) {
+        order.customer = this
+        orders.add(order)
+        this
+    }
+
+    // Simulates GORM's removeFromOrders()
+    Person removeFromOrders(Order order) {
+        orders.remove(order)
+        order.customer = null
+        this
+    }
+
+    @Override
+    protected void doValidate() {
+        if (!firstName?.trim()) {
+            errors.add("firstName cannot be blank")
+        }
+        if (!lastName?.trim()) {
+            errors.add("lastName cannot be blank")
+        }
+        if (email && !email.contains('@')) {
+            errors.add("email is not valid")
+        }
+    }
+
+    @Override
+    Map toMap() {
+        def result = super.toMap()
+        result.putAll([
+            firstName: firstName,
+            lastName: lastName,
+            fullName: fullName,
+            email: email,
+            birthDate: birthDate,
+            age: age,
+            phone: phone,
+            address: address?.toMap(),
+            orderCount: orders.size()
+        ])
+        result
+    }
+
+    /**
+     * Simulates dynamic finder: findByEmail
+     */
+    static Person findByEmail(List<Person> people, String email) {
+        people.find { it.email == email }
+    }
+
+    /**
+     * Simulates dynamic finder: findAllByLastName
+     */
+    static List<Person> findAllByLastName(List<Person> people, String lastName) {
+        people.findAll { it.lastName == lastName }
+    }
+
+    /**
+     * Simulates criteria query
+     */
+    static List<Person> findAllWithOrders(List<Person> people) {
+        people.findAll { !it.orders.isEmpty() }
+    }
+}
+
+/**
+ * Embedded Address class (like Grails embedded objects).
+ */
+class Address {
+    String street
+    String city
+    String state
+    String zipCode
+    String country = "USA"
+
+    String getFormatted() {
+        "$street, $city, $state $zipCode, $country"
+    }
+
+    Map toMap() {
+        [
+            street: street,
+            city: city,
+            state: state,
+            zipCode: zipCode,
+            country: country,
+            formatted: formatted
+        ]
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/service/DynamicService.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/service/DynamicService.groovy
new file mode 100644
index 0000000000..81f9888913
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/service/DynamicService.groovy
@@ -0,0 +1,288 @@
+/*
+ *  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.grailslike.service
+
+import org.apache.groovy.bench.grailslike.model.*
+
+/**
+ * Simulates a Grails service with dynamic method dispatch patterns.
+ * Services in Grails often use:
+ * - Dynamic finders (methodMissing)
+ * - Property access on domain objects
+ * - Collection operations (each, collect, findAll)
+ * - Builder patterns
+ */
+class DynamicService {
+
+    // Simulated "database" - lists of domain objects
+    List<Person> people = []
+    List<Order> orders = []
+    List<Product> products = []
+    List<Customer> customers = []
+
+    /**
+     * methodMissing to simulate GORM dynamic finders.
+     * e.g., findPersonByEmail, findAllOrdersByStatus
+     */
+    def methodMissing(String name, args) {
+        if (name.startsWith('findAllBy')) {
+            return handleFindAllBy(name, args)
+        } else if (name.startsWith('findBy')) {
+            return handleFindBy(name, args)
+        } else if (name.startsWith('countBy')) {
+            return handleCountBy(name, args)
+        }
+        throw new MissingMethodException(name, this.class, args)
+    }
+
+    private List handleFindAllBy(String name, args) {
+        def (entityName, propertyName) = parseDynamicFinderName(name.substring(9))
+        def collection = getCollectionForEntity(entityName)
+        def value = args[0]
+        collection.findAll { it."$propertyName" == value }
+    }
+
+    private Object handleFindBy(String name, args) {
+        def (entityName, propertyName) = parseDynamicFinderName(name.substring(6))
+        def collection = getCollectionForEntity(entityName)
+        def value = args[0]
+        collection.find { it."$propertyName" == value }
+    }
+
+    private int handleCountBy(String name, args) {
+        def (entityName, propertyName) = parseDynamicFinderName(name.substring(7))
+        def collection = getCollectionForEntity(entityName)
+        def value = args[0]
+        collection.count { it."$propertyName" == value }
+    }
+
+    private List parseDynamicFinderName(String suffix) {
+        // Simple parser: PersonEmail -> [Person, email]
+        def match = suffix =~ /^([A-Z][a-z]+)([A-Z].*)$/
+        if (match) {
+            def entity = match[0][1]
+            def property = match[0][2].uncapitalize()
+            return [entity, property]
+        }
+        [suffix, 'id']
+    }
+
+    private List getCollectionForEntity(String entityName) {
+        switch (entityName) {
+            case 'Person': return people
+            case 'Order': return orders
+            case 'Product': return products
+            case 'Customer': return customers
+            default: return []
+        }
+    }
+
+    // ========================================================================
+    // Person Service Methods
+    // ========================================================================
+
+    Person createPerson(Map params) {
+        def person = new Person(
+            firstName: params.firstName,
+            lastName: params.lastName,
+            email: params.email,
+            birthDate: params.birthDate,
+            phone: params.phone
+        )
+        if (params.address) {
+            person.address = new Address(params.address)
+        }
+        if (person.save()) {
+            people.add(person)
+            return person
+        }
+        null
+    }
+
+    Person updatePerson(Long id, Map params) {
+        def person = people.find { it.id == id }
+        if (!person) return null
+
+        params.each { key, value ->
+            if (person.hasProperty(key)) {
+                person."$key" = value
+            }
+        }
+        person.save()
+        person
+    }
+
+    List<Person> findPeopleWithOrders() {
+        people.findAll { !it.orders.isEmpty() }
+    }
+
+    List<Map> getPeopleSummary() {
+        people.collect { p ->
+            [
+                id: p.id,
+                fullName: p.fullName,
+                email: p.email,
+                orderCount: p.orders.size(),
+                totalSpent: p.orders.sum { it.total } ?: 0
+            ]
+        }
+    }
+
+    // ========================================================================
+    // Order Service Methods
+    // ========================================================================
+
+    Order createOrder(Person customer, List<Map> itemsData) {
+        def order = new Order(
+            orderNumber: Order.generateOrderNumber(),
+            orderDate: new Date(),
+            customer: customer
+        )
+
+        itemsData.each { itemData ->
+            def item = new OrderItem(
+                productCode: itemData.productCode,
+                productName: itemData.productName,
+                quantity: itemData.quantity ?: 1,
+                unitPrice: itemData.unitPrice ?: BigDecimal.ZERO
+            )
+            order.addToItems(item)
+        }
+
+        if (order.save()) {
+            orders.add(order)
+            customer.addToOrders(order)
+            return order
+        }
+        null
+    }
+
+    Order processOrder(Long orderId) {
+        def order = orders.find { it.id == orderId }
+        if (!order) return null
+
+        // Simulate order processing with heavy property access
+        def summary = [
+            orderNumber: order.orderNumber,
+            customer: order.customer.fullName,
+            items: order.items.collect { item ->
+                [
+                    product: item.productName,
+                    qty: item.quantity,
+                    price: item.unitPrice,
+                    total: item.lineTotal
+                ]
+            },
+            subtotal: order.subtotal,
+            tax: order.tax,
+            total: order.total
+        ]
+
+        // Update status
+        order.status = OrderStatus.CONFIRMED
+        order.save()
+
+        order
+    }
+
+    List<Order> getOrdersByStatus(OrderStatus status) {
+        orders.findAll { it.status == status }
+    }
+
+    List<Map> getOrdersReport() {
+        orders.collect { o ->
+            [
+                orderNumber: o.orderNumber,
+                customerName: o.customer?.fullName,
+                itemCount: o.itemCount,
+                total: o.total,
+                status: o.status?.name()
+            ]
+        }
+    }
+
+    Map getOrderStatistics() {
+        [
+            totalOrders: orders.size(),
+            pendingOrders: orders.count { it.status == OrderStatus.PENDING },
+            completedOrders: orders.count { it.status == OrderStatus.COMPLETED },
+            totalRevenue: orders.findAll { it.status == OrderStatus.COMPLETED }.sum { it.total } ?: 0,
+            averageOrderValue: orders ? (orders.sum { it.total } / orders.size()) : 0
+        ]
+    }
+
+    // ========================================================================
+    // Batch Operations (common in Grails services)
+    // ========================================================================
+
+    int updateOrderStatuses(OrderStatus fromStatus, OrderStatus toStatus) {
+        def toUpdate = orders.findAll { it.status == fromStatus }
+        toUpdate.each { order ->
+            order.status = toStatus
+            order.save()
+        }
+        toUpdate.size()
+    }
+
+    List<Map> exportOrders(Date fromDate, Date toDate) {
+        orders.findAll { o ->
+            o.orderDate >= fromDate && o.orderDate <= toDate
+        }.collect { o ->
+            o.toMap()
+        }
+    }
+
+    // ========================================================================
+    // Complex Query Simulations
+    // ========================================================================
+
+    List<Map> getTopCustomers(int limit) {
+        def customerTotals = orders.groupBy { it.customer }
+            .collectEntries { customer, customerOrders ->
+                [customer, customerOrders.sum { it.total }]
+            }
+
+        customerTotals.sort { -it.value }
+            .take(limit)
+            .collect { customer, total ->
+                [
+                    customerId: customer.id,
+                    customerName: customer.fullName,
+                    totalSpent: total,
+                    orderCount: orders.count { it.customer == customer }
+                ]
+            }
+    }
+
+    List<Map> getProductSales() {
+        def allItems = orders.collectMany { it.items }
+
+        allItems.groupBy { it.productCode }
+            .collectEntries { code, items ->
+                [code, [
+                    productCode: code,
+                    productName: items[0].productName,
+                    quantitySold: items.sum { it.quantity },
+                    revenue: items.sum { it.lineTotal }
+                ]]
+            }
+            .values()
+            .sort { -it.revenue }
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/service/TransactionalSimulator.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/service/TransactionalSimulator.groovy
new file mode 100644
index 0000000000..b757e60505
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/service/TransactionalSimulator.groovy
@@ -0,0 +1,341 @@
+/*
+ *  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.grailslike.service
+
+import org.apache.groovy.bench.grailslike.model.*
+
+/**
+ * Simulates transactional service patterns common in Grails.
+ * Exercises:
+ * - Nested method calls through dynamic dispatch
+ * - Exception handling and rollback simulation
+ * - Dirty checking patterns
+ * - Flush and clear simulation
+ */
+class TransactionalSimulator {
+
+    // Transaction state tracking
+    private ThreadLocal<TransactionContext> currentTransaction = new ThreadLocal<>()
+
+    // Services that would be injected in real Grails
+    DynamicService dynamicService
+
+    /**
+     * Simulate @Transactional behavior with closure.
+     */
+    def <T> T withTransaction(Closure<T> closure) {
+        def ctx = beginTransaction()
+        try {
+            def result = closure.call(ctx)
+            commitTransaction(ctx)
+            return result
+        } catch (Exception e) {
+            rollbackTransaction(ctx)
+            throw e
+        } finally {
+            endTransaction()
+        }
+    }
+
+    /**
+     * Simulate @Transactional(readOnly = true)
+     */
+    def <T> T withReadOnlyTransaction(Closure<T> closure) {
+        def ctx = beginTransaction()
+        ctx.readOnly = true
+        try {
+            def result = closure.call(ctx)
+            // Read-only transactions don't flush
+            return result
+        } finally {
+            endTransaction()
+        }
+    }
+
+    /**
+     * Simulate nested transaction (@Transactional(propagation = REQUIRES_NEW))
+     */
+    def <T> T withNewTransaction(Closure<T> closure) {
+        def parentCtx = currentTransaction.get()
+        def ctx = beginTransaction()
+        ctx.parent = parentCtx
+        try {
+            def result = closure.call(ctx)
+            commitTransaction(ctx)
+            return result
+        } catch (Exception e) {
+            rollbackTransaction(ctx)
+            throw e
+        } finally {
+            currentTransaction.set(parentCtx)
+        }
+    }
+
+    private TransactionContext beginTransaction() {
+        def ctx = new TransactionContext()
+        currentTransaction.set(ctx)
+        ctx
+    }
+
+    private void commitTransaction(TransactionContext ctx) {
+        if (ctx.readOnly) return
+
+        // Simulate flush - check all dirty objects
+        ctx.managedEntities.each { entity ->
+            if (entity.isDirty()) {
+                // Simulate write to database
+                entity.save(flush: true)
+            }
+        }
+        ctx.committed = true
+    }
+
+    private void rollbackTransaction(TransactionContext ctx) {
+        // Simulate rollback - discard all changes
+        ctx.managedEntities.each { entity ->
+            entity.discard()
+        }
+        ctx.rolledBack = true
+    }
+
+    private void endTransaction() {
+        currentTransaction.remove()
+    }
+
+    // ========================================================================
+    // Transactional Service Methods (typical Grails patterns)
+    // ========================================================================
+
+    /**
+     * Create order with items - demonstrates nested saves.
+     */
+    Order createOrderWithItems(Person customer, List<OrderItem> items) {
+        withTransaction { ctx ->
+            def order = new Order(
+                orderNumber: Order.generateOrderNumber(),
+                orderDate: new Date(),
+                customer: customer
+            )
+            ctx.manage(order)
+
+            items.each { item ->
+                order.addToItems(item)
+                ctx.manage(item)
+            }
+
+            if (!order.validate()) {
+                throw new ValidationException("Order validation failed: ${order.errors}")
+            }
+
+            order.save()
+            dynamicService?.orders?.add(order)
+            order
+        }
+    }
+
+    /**
+     * Process payment - demonstrates transaction boundary.
+     */
+    Payment processPayment(Order order, BigDecimal amount, PaymentMethod method) {
+        withTransaction { ctx ->
+            def payment = new Payment(
+                order: order,
+                amount: amount,
+                method: method,
+                paymentDate: new Date(),
+                transactionId: "TXN-${System.currentTimeMillis()}"
+            )
+            ctx.manage(payment)
+
+            // Simulate external payment processing
+            if (amount <= 0) {
+                throw new PaymentException("Invalid payment amount")
+            }
+
+            payment.status = PaymentStatus.AUTHORIZED
+
+            // Simulate capture
+            payment.status = PaymentStatus.CAPTURED
+            payment.save()
+
+            // Update order status
+            order.status = OrderStatus.CONFIRMED
+            ctx.manage(order)
+            order.save()
+
+            payment
+        }
+    }
+
+    /**
+     * Batch update with transaction - common admin operation.
+     */
+    int batchUpdatePrices(String category, BigDecimal multiplier) {
+        withTransaction { ctx ->
+            def products = dynamicService?.products?.findAll { it.category?.name == category } ?: []
+
+            int updated = 0
+            products.each { product ->
+                ctx.manage(product)
+                product.price = product.price * multiplier
+                if (product.save()) {
+                    updated++
+                }
+            }
+            updated
+        }
+    }
+
+    /**
+     * Complex operation with nested transactions.
+     */
+    Map transferInventory(Long fromWarehouseId, Long toWarehouseId, String productCode, int quantity) {
+        withTransaction { outerCtx ->
+            def result = [success: false, message: '']
+
+            // Nested transaction for source warehouse
+            withNewTransaction { srcCtx ->
+                // Find source inventory
+                def sourceInv = findInventory(fromWarehouseId, productCode)
+                if (!sourceInv) {
+                    result.message = "Source inventory not found"
+                    return result
+                }
+
+                if (sourceInv.available < quantity) {
+                    result.message = "Insufficient quantity"
+                    return result
+                }
+
+                srcCtx.manage(sourceInv)
+                sourceInv.quantityOnHand -= quantity
+                sourceInv.save()
+            }
+
+            // Nested transaction for destination warehouse
+            withNewTransaction { destCtx ->
+                def destInv = findInventory(toWarehouseId, productCode)
+                if (!destInv) {
+                    // Create new inventory record
+                    destInv = new Inventory(quantityOnHand: 0)
+                }
+
+                destCtx.manage(destInv)
+                destInv.quantityOnHand += quantity
+                destInv.save()
+            }
+
+            result.success = true
+            result.message = "Transferred $quantity units"
+            result
+        }
+    }
+
+    private Inventory findInventory(Long warehouseId, String productCode) {
+        // Simulated lookup
+        null
+    }
+
+    /**
+     * Read-only query with property access.
+     */
+    List<Map> getOrderSummaries() {
+        withReadOnlyTransaction { ctx ->
+            dynamicService?.orders?.collect { order ->
+                [
+                    id: order.id,
+                    number: order.orderNumber,
+                    customer: order.customer?.fullName,
+                    items: order.items*.productName,
+                    total: order.total,
+                    status: order.status?.name()
+                ]
+            } ?: []
+        }
+    }
+
+    /**
+     * Service method with validation and error handling.
+     */
+    Person registerCustomer(Map params) {
+        withTransaction { ctx ->
+            // Check for existing email
+            def existing = dynamicService?.findPersonByEmail(params.email)
+            if (existing) {
+                throw new DuplicateException("Email already registered")
+            }
+
+            def person = new Person(
+                firstName: params.firstName,
+                lastName: params.lastName,
+                email: params.email,
+                phone: params.phone
+            )
+            ctx.manage(person)
+
+            if (params.address) {
+                person.address = new Address(params.address)
+            }
+
+            if (!person.validate()) {
+                throw new ValidationException("Validation failed: ${person.errors}")
+            }
+
+            person.save()
+            dynamicService?.people?.add(person)
+            person
+        }
+    }
+}
+
+/**
+ * Transaction context for tracking managed entities.
+ */
+class TransactionContext {
+    List<DomainObject> managedEntities = []
+    boolean readOnly = false
+    boolean committed = false
+    boolean rolledBack = false
+    TransactionContext parent
+
+    void manage(DomainObject entity) {
+        if (!managedEntities.contains(entity)) {
+            managedEntities.add(entity)
+        }
+    }
+
+    boolean isActive() {
+        !committed && !rolledBack
+    }
+}
+
+/**
+ * Custom exceptions for service layer.
+ */
+class ValidationException extends RuntimeException {
+    ValidationException(String message) { super(message) }
+}
+
+class PaymentException extends RuntimeException {
+    PaymentException(String message) { super(message) }
+}
+
+class DuplicateException extends RuntimeException {
+    DuplicateException(String message) { super(message) }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/view/TemplateRenderBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/view/TemplateRenderBench.java
new file mode 100644
index 0000000000..43fde8e531
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/view/TemplateRenderBench.java
@@ -0,0 +1,279 @@
+/*
+ *  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.grailslike.view;
+
+import org.apache.groovy.bench.grailslike.model.*;
+import org.apache.groovy.bench.grailslike.service.*;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.Blackhole;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Benchmarks simulating GSP-like template rendering.
+ * <p>
+ * GSP templates heavily exercise:
+ * <ul>
+ *   <li>Property access on model objects (${person.name})</li>
+ *   <li>Collection iteration (g:each)</li>
+ *   <li>Conditional rendering (g:if)</li>
+ *   <li>GString interpolation</li>
+ *   <li>Tag invocations (dynamic method calls)</li>
+ * </ul>
+ * <p>
+ * Run with: ./gradlew -Pindy=true -PbenchInclude=TemplateRender :perf:jmh
+ */
+@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+public class TemplateRenderBench {
+
+    // ========================================================================
+    // State classes
+    // ========================================================================
+
+    @State(Scope.Thread)
+    public static class ViewState {
+        TemplateSimulator templateSimulator;
+        DynamicService dynamicService;
+        Random random;
+
+        // Pre-built models for benchmarks
+        Map<String, Object> personModel;
+        Map<String, Object> orderModel;
+        Map<String, Object> listModel;
+        Map<String, Object> dashboardModel;
+
+        @Setup(Level.Trial)
+        public void setup() {
+            random = new Random(42);
+            dynamicService = new DynamicService();
+            templateSimulator = new TemplateSimulator();
+
+            // Seed data
+            seedTestData();
+
+            // Pre-build models
+            buildModels();
+        }
+
+        private void seedTestData() {
+            // Create people with orders
+            for (int i = 0; i < 100; i++) {
+                Person person = new Person();
+                person.setFirstName("First" + i);
+                person.setLastName("Last" + i);
+                person.setEmail("person" + i + "@example.com");
+
+                Address addr = new Address();
+                addr.setStreet(i + " Main St");
+                addr.setCity("City" + (i % 10));
+                addr.setState("ST");
+                addr.setZipCode(String.format("%05d", i));
+                person.setAddress(addr);
+
+                person.save();
+                dynamicService.getPeople().add(person);
+
+                // Create orders
+                if (i % 3 == 0) {
+                    Order order = new Order();
+                    order.setOrderNumber(Order.generateOrderNumber());
+                    order.setOrderDate(new Date());
+                    order.setCustomer(person);
+                    order.setStatus(OrderStatus.values()[random.nextInt(OrderStatus.values().length)]);
+
+                    for (int j = 0; j < random.nextInt(5) + 1; j++) {
+                        order.addToItems(ProductCatalog.randomItem(random));
+                    }
+
+                    order.save();
+                    dynamicService.getOrders().add(order);
+                    person.addToOrders(order);
+                }
+            }
+        }
+
+        private void buildModels() {
+            List<Person> people = dynamicService.getPeople();
+            List<Order> orders = dynamicService.getOrders();
+
+            // Person detail model
+            Person person = people.get(0);
+            personModel = new HashMap<>();
+            personModel.put("person", person);
+            personModel.put("orders", person.getOrders());
+
+            // Order detail model
+            if (!orders.isEmpty()) {
+                Order order = orders.get(0);
+                orderModel = new HashMap<>();
+                orderModel.put("order", order);
+                orderModel.put("customer", order.getCustomer());
+                orderModel.put("items", order.getItems());
+            }
+
+            // List model
+            listModel = new HashMap<>();
+            listModel.put("personList", people);
+            listModel.put("personCount", people.size());
+
+            // Dashboard model
+            dashboardModel = new HashMap<>();
+            dashboardModel.put("recentOrders", orders.subList(0, Math.min(10, orders.size())));
+            dashboardModel.put("stats", dynamicService.getOrderStatistics());
+            dashboardModel.put("topCustomers", dynamicService.getTopCustomers(5));
+        }
+    }
+
+    // ========================================================================
+    // Simple Property Access Benchmarks
+    // ========================================================================
+
+    /**
+     * Render person detail - simple property access.
+     */
+    @Benchmark
+    public Object render_personDetail(ViewState state, Blackhole bh) {
+        String html = state.templateSimulator.renderPersonDetail(state.personModel);
+        bh.consume(html);
+        return html;
+    }
+
+    /**
+     * Render order detail - nested property access.
+     */
+    @Benchmark
+    public Object render_orderDetail(ViewState state, Blackhole bh) {
+        if (state.orderModel == null) return null;
+        String html = state.templateSimulator.renderOrderDetail(state.orderModel);
+        bh.consume(html);
+        return html;
+    }
+
+    // ========================================================================
+    // List/Table Rendering Benchmarks
+    // ========================================================================
+
+    /**
+     * Render person list table - iteration + property access.
+     */
+    @Benchmark
+    public Object render_personList(ViewState state, Blackhole bh) {
+        String html = state.templateSimulator.renderPersonList(state.listModel);
+        bh.consume(html);
+        return html;
+    }
+
+    /**
+     * Render order list with computed values.
+     */
+    @Benchmark
+    public Object render_orderList(ViewState state, Blackhole bh) {
+        Map<String, Object> model = new HashMap<>();
+        model.put("orderList", state.dynamicService.getOrders());
+        String html = state.templateSimulator.renderOrderList(model);
+        bh.consume(html);
+        return html;
+    }
+
+    // ========================================================================
+    // Complex Template Benchmarks
+    // ========================================================================
+
+    /**
+     * Render dashboard - multiple sections, aggregations.
+     */
+    @Benchmark
+    public Object render_dashboard(ViewState state, Blackhole bh) {
+        String html = state.templateSimulator.renderDashboard(state.dashboardModel);
+        bh.consume(html);
+        return html;
+    }
+
+    /**
+     * Render order with line items - nested iteration.
+     */
+    @Benchmark
+    public Object render_orderWithItems(ViewState state, Blackhole bh) {
+        if (state.orderModel == null) return null;
+        String html = state.templateSimulator.renderOrderWithItems(state.orderModel);
+        bh.consume(html);
+        return html;
+    }
+
+    // ========================================================================
+    // Spread Operator Benchmarks
+    // ========================================================================
+
+    /**
+     * Use spread operator for property extraction.
+     */
+    @Benchmark
+    public Object render_spreadOperator(ViewState state, Blackhole bh) {
+        String html = state.templateSimulator.renderWithSpreadOperator(state.listModel);
+        bh.consume(html);
+        return html;
+    }
+
+    // ========================================================================
+    // Conditional Rendering Benchmarks
+    // ========================================================================
+
+    /**
+     * Conditional rendering (g:if equivalent).
+     */
+    @Benchmark
+    public Object render_conditional(ViewState state, Blackhole bh) {
+        String html = state.templateSimulator.renderConditional(state.orderModel);
+        bh.consume(html);
+        return html;
+    }
+
+    // ========================================================================
+    // GString Interpolation Benchmarks
+    // ========================================================================
+
+    /**
+     * Heavy GString interpolation.
+     */
+    @Benchmark
+    public Object render_gstringHeavy(ViewState state, Blackhole bh) {
+        String html = state.templateSimulator.renderGStringHeavy(state.personModel);
+        bh.consume(html);
+        return html;
+    }
+
+    // ========================================================================
+    // Full Page Render Benchmarks
+    // ========================================================================
+
+    /**
+     * Full page render with layout, header, content, footer.
+     */
+    @Benchmark
+    public Object render_fullPage(ViewState state, Blackhole bh) {
+        String html = state.templateSimulator.renderFullPage(state.dashboardModel);
+        bh.consume(html);
+        return html;
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/view/TemplateSimulator.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/view/TemplateSimulator.groovy
new file mode 100644
index 0000000000..5c33a84446
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/grailslike/view/TemplateSimulator.groovy
@@ -0,0 +1,435 @@
+/*
+ *  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.grailslike.view
+
+import org.apache.groovy.bench.grailslike.model.*
+
+/**
+ * Simulates GSP template rendering patterns.
+ * Exercises heavy property access and GString interpolation.
+ */
+class TemplateSimulator {
+
+    /**
+     * Render person detail page.
+     */
+    String renderPersonDetail(Map model) {
+        def person = model.person
+        def orders = model.orders ?: []
+
+        def sb = new StringBuilder()
+        sb << "<div class='person-detail'>\n"
+        sb << "  <h1>${person.fullName}</h1>\n"
+        sb << "  <div class='info'>\n"
+        sb << "    <p>Email: ${person.email}</p>\n"
+        sb << "    <p>Phone: ${person.phone ?: 'N/A'}</p>\n"
+        sb << "    <p>Age: ${person.age}</p>\n"
+
+        if (person.address) {
+            sb << "    <div class='address'>\n"
+            sb << "      <h3>Address</h3>\n"
+            sb << "      <p>${person.address.street}</p>\n"
+            sb << "      <p>${person.address.city}, ${person.address.state} ${person.address.zipCode}</p>\n"
+            sb << "    </div>\n"
+        }
+
+        sb << "  </div>\n"
+
+        sb << "  <div class='orders'>\n"
+        sb << "    <h2>Orders (${orders.size()})</h2>\n"
+        if (orders) {
+            sb << "    <ul>\n"
+            orders.each { order ->
+                sb << "      <li>${order.orderNumber} - ${order.status} - \$${order.total}</li>\n"
+            }
+            sb << "    </ul>\n"
+        } else {
+            sb << "    <p>No orders</p>\n"
+        }
+        sb << "  </div>\n"
+
+        sb << "</div>\n"
+        sb.toString()
+    }
+
+    /**
+     * Render order detail page.
+     */
+    String renderOrderDetail(Map model) {
+        def order = model.order
+        def customer = model.customer ?: order?.customer
+        def items = model.items ?: order?.items ?: []
+
+        def sb = new StringBuilder()
+        sb << "<div class='order-detail'>\n"
+        sb << "  <h1>Order ${order.orderNumber}</h1>\n"
+        sb << "  <div class='status ${order.status.name().toLowerCase()}'>${order.status}</div>\n"
+
+        sb << "  <div class='customer'>\n"
+        sb << "    <h3>Customer</h3>\n"
+        sb << "    <p>${customer?.fullName ?: 'Unknown'}</p>\n"
+        sb << "    <p>${customer?.email ?: ''}</p>\n"
+        sb << "  </div>\n"
+
+        sb << "  <div class='items'>\n"
+        sb << "    <h3>Items (${items.size()})</h3>\n"
+        sb << "    <table>\n"
+        sb << "      <thead><tr><th>Product</th><th>Qty</th><th>Price</th><th>Total</th></tr></thead>\n"
+        sb << "      <tbody>\n"
+        items.each { item ->
+            sb << "        <tr>\n"
+            sb << "          <td>${item.productName}</td>\n"
+            sb << "          <td>${item.quantity}</td>\n"
+            sb << "          <td>\$${item.unitPrice}</td>\n"
+            sb << "          <td>\$${item.lineTotal}</td>\n"
+            sb << "        </tr>\n"
+        }
+        sb << "      </tbody>\n"
+        sb << "    </table>\n"
+        sb << "  </div>\n"
+
+        sb << "  <div class='totals'>\n"
+        sb << "    <p>Subtotal: \$${order.subtotal}</p>\n"
+        sb << "    <p>Tax: \$${order.tax}</p>\n"
+        sb << "    <p class='total'>Total: \$${order.total}</p>\n"
+        sb << "  </div>\n"
+
+        sb << "</div>\n"
+        sb.toString()
+    }
+
+    /**
+     * Render person list table.
+     */
+    String renderPersonList(Map model) {
+        def people = model.personList ?: []
+        def count = model.personCount ?: people.size()
+
+        def sb = new StringBuilder()
+        sb << "<div class='person-list'>\n"
+        sb << "  <h1>People (${count})</h1>\n"
+        sb << "  <table class='data-table'>\n"
+        sb << "    <thead>\n"
+        sb << "      <tr>\n"
+        sb << "        <th>ID</th><th>Name</th><th>Email</th><th>Phone</th><th>City</th><th>Orders</th>\n"
+        sb << "      </tr>\n"
+        sb << "    </thead>\n"
+        sb << "    <tbody>\n"
+
+        people.each { person ->
+            sb << "      <tr>\n"
+            sb << "        <td>${person.id}</td>\n"
+            sb << "        <td><a href='/person/show/${person.id}'>${person.fullName}</a></td>\n"
+            sb << "        <td>${person.email}</td>\n"
+            sb << "        <td>${person.phone ?: '-'}</td>\n"
+            sb << "        <td>${person.address?.city ?: '-'}</td>\n"
+            sb << "        <td>${person.orders?.size() ?: 0}</td>\n"
+            sb << "      </tr>\n"
+        }
+
+        sb << "    </tbody>\n"
+        sb << "  </table>\n"
+        sb << "</div>\n"
+        sb.toString()
+    }
+
+    /**
+     * Render order list table.
+     */
+    String renderOrderList(Map model) {
+        def orders = model.orderList ?: []
+
+        def sb = new StringBuilder()
+        sb << "<div class='order-list'>\n"
+        sb << "  <h1>Orders (${orders.size()})</h1>\n"
+        sb << "  <table class='data-table'>\n"
+        sb << "    <thead>\n"
+        sb << "      <tr>\n"
+        sb << "        <th>Order #</th><th>Date</th><th>Customer</th><th>Items</th><th>Total</th><th>Status</th>\n"
+        sb << "      </tr>\n"
+        sb << "    </thead>\n"
+        sb << "    <tbody>\n"
+
+        orders.each { order ->
+            sb << "      <tr class='status-${order.status.name().toLowerCase()}'>\n"
+            sb << "        <td><a href='/order/show/${order.id}'>${order.orderNumber}</a></td>\n"
+            sb << "        <td>${order.orderDate?.format('yyyy-MM-dd')}</td>\n"
+            sb << "        <td>${order.customer?.fullName ?: 'Unknown'}</td>\n"
+            sb << "        <td>${order.itemCount}</td>\n"
+            sb << "        <td>\$${order.total}</td>\n"
+            sb << "        <td>${order.status}</td>\n"
+            sb << "      </tr>\n"
+        }
+
+        sb << "    </tbody>\n"
+        sb << "  </table>\n"
+        sb << "</div>\n"
+        sb.toString()
+    }
+
+    /**
+     * Render dashboard with multiple sections.
+     */
+    String renderDashboard(Map model) {
+        def recentOrders = model.recentOrders ?: []
+        def stats = model.stats ?: [:]
+        def topCustomers = model.topCustomers ?: []
+
+        def sb = new StringBuilder()
+        sb << "<div class='dashboard'>\n"
+
+        // Stats cards
+        sb << "  <div class='stats-row'>\n"
+        stats.each { key, value ->
+            sb << "    <div class='stat-card'>\n"
+            sb << "      <h4>${key.replaceAll(/([A-Z])/, ' $1').trim()}</h4>\n"
+            sb << "      <p class='value'>${value}</p>\n"
+            sb << "    </div>\n"
+        }
+        sb << "  </div>\n"
+
+        // Top customers
+        sb << "  <div class='top-customers'>\n"
+        sb << "    <h3>Top Customers</h3>\n"
+        sb << "    <table>\n"
+        topCustomers.each { customer ->
+            sb << "      <tr>\n"
+            sb << "        <td>${customer.customerName}</td>\n"
+            sb << "        <td>\$${customer.totalSpent}</td>\n"
+            sb << "        <td>${customer.orderCount} orders</td>\n"
+            sb << "      </tr>\n"
+        }
+        sb << "    </table>\n"
+        sb << "  </div>\n"
+
+        // Recent orders
+        sb << "  <div class='recent-orders'>\n"
+        sb << "    <h3>Recent Orders</h3>\n"
+        sb << "    <ul>\n"
+        recentOrders.each { order ->
+            sb << "      <li>\n"
+            sb << "        <span class='order-number'>${order.orderNumber}</span>\n"
+            sb << "        <span class='customer'>${order.customer?.fullName}</span>\n"
+            sb << "        <span class='total'>\$${order.total}</span>\n"
+            sb << "        <span class='status ${order.status.name().toLowerCase()}'>${order.status}</span>\n"
+            sb << "      </li>\n"
+        }
+        sb << "    </ul>\n"
+        sb << "  </div>\n"
+
+        sb << "</div>\n"
+        sb.toString()
+    }
+
+    /**
+     * Render order with line items (nested iteration).
+     */
+    String renderOrderWithItems(Map model) {
+        def order = model.order
+        def items = model.items ?: order?.items ?: []
+
+        def sb = new StringBuilder()
+        sb << "<div class='order-items'>\n"
+        sb << "  <h2>Order ${order.orderNumber}</h2>\n"
+
+        items.eachWithIndex { item, idx ->
+            sb << "  <div class='item' data-index='${idx}'>\n"
+            sb << "    <div class='item-header'>\n"
+            sb << "      <span class='code'>${item.productCode}</span>\n"
+            sb << "      <span class='name'>${item.productName}</span>\n"
+            sb << "    </div>\n"
+            sb << "    <div class='item-details'>\n"
+            sb << "      <span class='qty'>Qty: ${item.quantity}</span>\n"
+            sb << "      <span class='price'>Price: \$${item.unitPrice}</span>\n"
+            if (item.hasDiscount) {
+                sb << "      <span class='discount'>Discount: ${item.discount * 100}%</span>\n"
+                sb << "      <span class='savings'>Savings: \$${item.savings}</span>\n"
+            }
+            sb << "      <span class='line-total'>Total: \$${item.lineTotal}</span>\n"
+            sb << "    </div>\n"
+            sb << "  </div>\n"
+        }
+
+        sb << "  <div class='order-totals'>\n"
+        sb << "    <div>Subtotal: \$${order.subtotal}</div>\n"
+        sb << "    <div>Tax (${order.taxRate * 100}%): \$${order.tax}</div>\n"
+        sb << "    <div class='grand-total'>Total: \$${order.total}</div>\n"
+        sb << "  </div>\n"
+        sb << "</div>\n"
+        sb.toString()
+    }
+
+    /**
+     * Render using spread operator.
+     */
+    String renderWithSpreadOperator(Map model) {
+        def people = model.personList ?: []
+
+        def sb = new StringBuilder()
+        sb << "<div class='spread-demo'>\n"
+
+        // Spread operator for names
+        def names = people*.fullName
+        sb << "  <div class='names'>\n"
+        names.each { name ->
+            sb << "    <span>${name}</span>\n"
+        }
+        sb << "  </div>\n"
+
+        // Spread operator for emails
+        def emails = people*.email
+        sb << "  <div class='emails'>\n"
+        emails.findAll { it != null }.each { email ->
+            sb << "    <a href='mailto:${email}'>${email}</a>\n"
+        }
+        sb << "  </div>\n"
+
+        // Spread on nested properties
+        def cities = people*.address*.city
+        sb << "  <div class='cities'>\n"
+        cities.findAll { it != null }.unique().each { city ->
+            sb << "    <span>${city}</span>\n"
+        }
+        sb << "  </div>\n"
+
+        sb << "</div>\n"
+        sb.toString()
+    }
+
+    /**
+     * Render with conditional logic.
+     */
+    String renderConditional(Map model) {
+        def order = model?.order
+
+        def sb = new StringBuilder()
+        sb << "<div class='conditional-demo'>\n"
+
+        if (order) {
+            sb << "  <div class='order-status'>\n"
+
+            if (order.status == OrderStatus.PENDING) {
+                sb << "    <div class='alert warning'>Order is pending processing</div>\n"
+            } else if (order.status == OrderStatus.CONFIRMED) {
+                sb << "    <div class='alert info'>Order has been confirmed</div>\n"
+            } else if (order.status == OrderStatus.SHIPPED) {
+                sb << "    <div class='alert success'>Order has shipped!</div>\n"
+                if (order.dynamicProperties['trackingNumber']) {
+                    sb << "    <p>Tracking: ${order.dynamicProperties['trackingNumber']}</p>\n"
+                }
+            } else if (order.status == OrderStatus.COMPLETED) {
+                sb << "    <div class='alert success'>Order completed</div>\n"
+            } else if (order.status == OrderStatus.CANCELLED) {
+                sb << "    <div class='alert error'>Order was cancelled</div>\n"
+            }
+
+            if (order.canCancel()) {
+                sb << "    <button class='btn-cancel'>Cancel Order</button>\n"
+            }
+
+            if (order.total > 100) {
+                sb << "    <div class='free-shipping'>Qualifies for free shipping!</div>\n"
+            }
+
+            sb << "  </div>\n"
+        } else {
+            sb << "  <div class='no-order'>No order to display</div>\n"
+        }
+
+        sb << "</div>\n"
+        sb.toString()
+    }
+
+    /**
+     * Heavy GString interpolation.
+     */
+    String renderGStringHeavy(Map model) {
+        def person = model.person
+
+        def sb = new StringBuilder()
+
+        // Multiple interpolations per line
+        sb << "Name: ${person.firstName} ${person.lastName} (${person.fullName})\n"
+        sb << "Contact: ${person.email ?: 'no email'} | ${person.phone ?: 'no phone'}\n"
+
+        if (person.address) {
+            def addr = person.address
+            sb << "Address: ${addr.street}, ${addr.city}, ${addr.state} ${addr.zipCode}, ${addr.country}\n"
+            sb << "Formatted: ${addr.formatted}\n"
+        }
+
+        sb << "Orders: ${person.orders?.size() ?: 0} total\n"
+
+        person.orders?.each { order ->
+            sb << "  - Order ${order.orderNumber}: ${order.itemCount} items, \$${order.total} (${order.status})\n"
+            order.items?.each { item ->
+                sb << "    * ${item.productCode}: ${item.productName} x${item.quantity} @ \$${item.unitPrice} = \$${item.lineTotal}\n"
+            }
+        }
+
+        def totalSpent = person.orders?.sum { it.total } ?: 0
+        sb << "Total spent: \$${totalSpent}\n"
+
+        sb.toString()
+    }
+
+    /**
+     * Full page render with layout.
+     */
+    String renderFullPage(Map model) {
+        def title = model.title ?: "Dashboard"
+
+        def sb = new StringBuilder()
+
+        // Layout start
+        sb << "<!DOCTYPE html>\n"
+        sb << "<html>\n"
+        sb << "<head>\n"
+        sb << "  <title>${title}</title>\n"
+        sb << "  <meta charset='utf-8'>\n"
+        sb << "</head>\n"
+        sb << "<body>\n"
+
+        // Header
+        sb << "<header>\n"
+        sb << "  <nav>\n"
+        sb << "    <a href='/'>Home</a>\n"
+        sb << "    <a href='/person/list'>People</a>\n"
+        sb << "    <a href='/order/list'>Orders</a>\n"
+        sb << "    <a href='/dashboard'>Dashboard</a>\n"
+        sb << "  </nav>\n"
+        sb << "</header>\n"
+
+        // Main content
+        sb << "<main>\n"
+        sb << renderDashboard(model)
+        sb << "</main>\n"
+
+        // Footer
+        sb << "<footer>\n"
+        sb << "  <p>Generated at ${new Date()}</p>\n"
+        sb << "  <p>Stats: ${model.stats?.totalOrders ?: 0} orders, "
+        sb << "\$${model.stats?.totalRevenue ?: 0} revenue</p>\n"
+        sb << "</footer>\n"
+
+        sb << "</body>\n"
+        sb << "</html>\n"
+        sb.toString()
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/MemoryProfileBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/MemoryProfileBench.java
new file mode 100644
index 0000000000..2474293412
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/MemoryProfileBench.java
@@ -0,0 +1,181 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.bench.profiling;
+
+import org.apache.groovy.bench.memory.*;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.Blackhole;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Benchmarks with detailed memory profiling.
+ * <p>
+ * These benchmarks use MemoryTrackingState to capture detailed memory
+ * metrics before and after execution, helping identify memory-intensive
+ * operations in Groovy's invokedynamic implementation.
+ * <p>
+ * Run with: ./gradlew -Pindy=true -PbenchInclude=MemoryProfile :perf:jmh
+ * <p>
+ * Or with the profiled runner:
+ * ./gradlew :perf:jmhJar && \
+ * java -cp build/libs/performance-*-jmh.jar \
+ *   org.apache.groovy.bench.profiling.ProfiledBenchmarkRunner "MemoryProfile"
+ */
+@Warmup(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(1)
+@BenchmarkMode(Mode.SingleShotTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+public class MemoryProfileBench {
+
+    private static final int BATCH_SIZE = 1000;
+
+    // ========================================================================
+    // State classes
+    // ========================================================================
+
+    @State(Scope.Thread)
+    public static class ProfileState {
+        List<MemoryReceiver> receivers;
+        Random random;
+
+        @Setup(Level.Trial)
+        public void setup() {
+            random = new Random(42);
+            receivers = MemoryHelper.createMixedReceivers(100);
+        }
+    }
+
+    // ========================================================================
+    // Memory Profile Benchmarks
+    // ========================================================================
+
+    /**
+     * Profile memory during batch object creation and method calls.
+     * This measures callsite allocation overhead.
+     */
+    @Benchmark
+    public void memprofile_batchCreateAndCall(ProfileState state, MemoryTrackingState memState, Blackhole bh) {
+        MemoryHelper.createAndCallMany(BATCH_SIZE, bh);
+    }
+
+    /**
+     * Profile memory during polymorphic dispatch.
+     * This measures cache churn overhead.
+     */
+    @Benchmark
+    public void memprofile_polymorphicDispatch(ProfileState state, MemoryTrackingState memState, Blackhole bh) {
+        for (int i = 0; i < 100; i++) {
+            MemoryHelper.polymorphicDispatch(state.receivers, bh);
+        }
+    }
+
+    /**
+     * Profile memory during closure operations.
+     */
+    @Benchmark
+    public void memprofile_closureOperations(ProfileState state, MemoryTrackingState memState, Blackhole bh) {
+        for (int i = 0; i < 100; i++) {
+            MemoryHelper.closureOverhead(state.receivers, bh);
+        }
+    }
+
+    /**
+     * Profile memory during spread operator use.
+     */
+    @Benchmark
+    public void memprofile_spreadOperator(ProfileState state, MemoryTrackingState memState, Blackhole bh) {
+        for (int i = 0; i < 100; i++) {
+            MemoryHelper.spreadOperatorOverhead(state.receivers, bh);
+        }
+    }
+
+    /**
+     * Profile memory during session simulation.
+     * This measures real-world web app memory patterns.
+     */
+    @Benchmark
+    public void memprofile_sessionSimulation(ProfileState state, MemoryTrackingState memState, Blackhole bh) {
+        for (int i = 0; i < BATCH_SIZE; i++) {
+            MemoryHelper.simulateRequest("request-" + i, bh);
+        }
+    }
+
+    /**
+     * Heavy benchmark to stress memory allocation.
+     * Creates many objects and performs many method calls.
+     */
+    @Benchmark
+    public void memprofile_heavyLoad(ProfileState state, MemoryTrackingState memState, Blackhole bh) {
+        // Phase 1: Object creation
+        for (int i = 0; i < BATCH_SIZE; i++) {
+            MemoryTestService svc = new MemoryTestService();
+            bh.consume(svc.getName());
+            bh.consume(svc.compute(i, i + 1));
+        }
+
+        // Phase 2: Polymorphic dispatch
+        for (int round = 0; round < 10; round++) {
+            MemoryHelper.polymorphicDispatch(state.receivers, bh);
+        }
+
+        // Phase 3: Collection operations
+        for (int round = 0; round < 10; round++) {
+            MemoryHelper.closureOverhead(state.receivers, bh);
+        }
+
+        // Phase 4: Session simulation
+        for (int i = 0; i < 100; i++) {
+            MemoryHelper.simulateRequest("heavy-" + i, bh);
+        }
+    }
+
+    /**
+     * Baseline: Java equivalent operations for comparison.
+     */
+    @Benchmark
+    public void memprofile_javaBaseline(ProfileState state, MemoryTrackingState memState, Blackhole bh) {
+        // Object creation
+        for (int i = 0; i < BATCH_SIZE; i++) {
+            JavaTestService svc = new JavaTestService();
+            bh.consume(svc.getName());
+            bh.consume(svc.compute(i, i + 1));
+        }
+
+        // Interface dispatch
+        for (int round = 0; round < 10; round++) {
+            for (MemoryReceiver r : state.receivers) {
+                bh.consume(r.receive());
+                bh.consume(r.getValue());
+            }
+        }
+    }
+
+    // Java baseline class
+    public static class JavaTestService {
+        private String name = "JavaTestService";
+        private int counter = 0;
+
+        public String getName() { return name; }
+        public int getCounter() { return counter; }
+        public int compute(int a, int b) { return a + b; }
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/MemoryTrackingState.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/MemoryTrackingState.java
new file mode 100644
index 0000000000..67622400c7
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/MemoryTrackingState.java
@@ -0,0 +1,221 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.bench.profiling;
+
+import org.openjdk.jmh.annotations.*;
+
+import java.lang.management.GarbageCollectorMXBean;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.lang.management.MemoryUsage;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * JMH State class for tracking memory allocation and GC activity.
+ * <p>
+ * Use this state in benchmarks to capture memory metrics before and after
+ * benchmark execution. This is particularly useful for identifying memory
+ * leaks and excessive allocation in Groovy's invokedynamic implementation.
+ * <p>
+ * Usage:
+ * <pre>
+ * &#64;Benchmark
+ * public void myBenchmark(MemoryTrackingState memState, Blackhole bh) {
+ *     // Your benchmark code
+ * }
+ * </pre>
+ * <p>
+ * Memory metrics are printed at the end of each trial.
+ */
+@State(Scope.Thread)
+public class MemoryTrackingState {
+
+    // Memory tracking
+    private long heapUsedBefore;
+    private long heapUsedAfter;
+    private long heapCommittedBefore;
+    private long heapCommittedAfter;
+
+    // GC tracking
+    private long gcCountBefore;
+    private long gcTimeBefore;
+    private long gcCountAfter;
+    private long gcTimeAfter;
+
+    // Allocation tracking (approximate)
+    private long allocationEstimateBefore;
+    private long allocationEstimateAfter;
+
+    // Trial counters
+    private static final AtomicLong trialCounter = new AtomicLong(0);
+    private long trialId;
+
+    private final MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
+    private final List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
+
+    /**
+     * Capture memory state before each trial.
+     */
+    @Setup(Level.Trial)
+    public void captureBeforeMemory() {
+        trialId = trialCounter.incrementAndGet();
+
+        // Force GC for consistent baseline
+        System.gc();
+        try {
+            Thread.sleep(100);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+        System.gc();
+
+        // Capture heap metrics
+        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
+        heapUsedBefore = heapUsage.getUsed();
+        heapCommittedBefore = heapUsage.getCommitted();
+
+        // Capture GC metrics
+        gcCountBefore = getTotalGcCount();
+        gcTimeBefore = getTotalGcTime();
+
+        // Estimate allocations
+        allocationEstimateBefore = getAllocationEstimate();
+
+        System.out.printf("[Trial %d] BEFORE - Heap: %.2f MB used, %.2f MB committed%n",
+                trialId,
+                heapUsedBefore / (1024.0 * 1024.0),
+                heapCommittedBefore / (1024.0 * 1024.0));
+    }
+
+    /**
+     * Capture memory state after each trial and report delta.
+     */
+    @TearDown(Level.Trial)
+    public void captureAfterMemory() {
+        // Capture current state (before forced GC)
+        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
+        heapUsedAfter = heapUsage.getUsed();
+        heapCommittedAfter = heapUsage.getCommitted();
+
+        gcCountAfter = getTotalGcCount();
+        gcTimeAfter = getTotalGcTime();
+
+        allocationEstimateAfter = getAllocationEstimate();
+
+        // Calculate deltas
+        long heapDelta = heapUsedAfter - heapUsedBefore;
+        long gcCountDelta = gcCountAfter - gcCountBefore;
+        long gcTimeDelta = gcTimeAfter - gcTimeBefore;
+        long allocationDelta = allocationEstimateAfter - allocationEstimateBefore;
+
+        System.out.printf("[Trial %d] AFTER - Heap: %.2f MB used, %.2f MB committed%n",
+                trialId,
+                heapUsedAfter / (1024.0 * 1024.0),
+                heapCommittedAfter / (1024.0 * 1024.0));
+
+        System.out.printf("[Trial %d] DELTA - Heap: %+.2f MB, GC: %d collections (%d ms), Est. Alloc: %.2f MB%n",
+                trialId,
+                heapDelta / (1024.0 * 1024.0),
+                gcCountDelta,
+                gcTimeDelta,
+                allocationDelta / (1024.0 * 1024.0));
+
+        // Check for potential memory leak
+        if (heapDelta > 50 * 1024 * 1024) { // > 50 MB growth
+            System.out.printf("[Trial %d] WARNING: Significant heap growth detected (%.2f MB)%n",
+                    trialId, heapDelta / (1024.0 * 1024.0));
+        }
+
+        System.out.println();
+    }
+
+    /**
+     * Capture memory at iteration boundaries for finer-grained tracking.
+     */
+    @Setup(Level.Iteration)
+    public void beforeIteration() {
+        // Optional: could add iteration-level tracking here
+    }
+
+    @TearDown(Level.Iteration)
+    public void afterIteration() {
+        // Optional: could add iteration-level tracking here
+    }
+
+    /**
+     * Get total GC collection count across all collectors.
+     */
+    private long getTotalGcCount() {
+        long total = 0;
+        for (GarbageCollectorMXBean gc : gcBeans) {
+            long count = gc.getCollectionCount();
+            if (count >= 0) {
+                total += count;
+            }
+        }
+        return total;
+    }
+
+    /**
+     * Get total GC time across all collectors.
+     */
+    private long getTotalGcTime() {
+        long total = 0;
+        for (GarbageCollectorMXBean gc : gcBeans) {
+            long time = gc.getCollectionTime();
+            if (time >= 0) {
+                total += time;
+            }
+        }
+        return total;
+    }
+
+    /**
+     * Estimate total allocation since JVM start.
+     * This is approximate and depends on available MXBeans.
+     */
+    private long getAllocationEstimate() {
+        // Use committed memory as a rough estimate
+        // More accurate tracking would require additional tooling
+        return memoryBean.getHeapMemoryUsage().getCommitted();
+    }
+
+    // Getters for use in benchmarks
+    public long getHeapUsedBefore() { return heapUsedBefore; }
+    public long getHeapUsedAfter() { return heapUsedAfter; }
+    public long getHeapDelta() { return heapUsedAfter - heapUsedBefore; }
+    public long getGcCount() { return gcCountAfter - gcCountBefore; }
+    public long getGcTime() { return gcTimeAfter - gcTimeBefore; }
+
+    /**
+     * Force a GC and return the heap used after.
+     * Useful for measuring retained memory.
+     */
+    public long getRetainedMemory() {
+        System.gc();
+        try {
+            Thread.sleep(50);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+        System.gc();
+        return memoryBean.getHeapMemoryUsage().getUsed();
+    }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/ProfiledBenchmarkRunner.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/ProfiledBenchmarkRunner.java
new file mode 100644
index 0000000000..ec3a0e7883
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/profiling/ProfiledBenchmarkRunner.java
@@ -0,0 +1,252 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.bench.profiling;
+
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import java.lang.management.*;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Profiled benchmark runner with detailed memory and performance analysis.
+ * <p>
+ * This class provides a programmatic way to run JMH benchmarks with
+ * additional profiling capabilities beyond what JMH provides by default.
+ * <p>
+ * Features:
+ * <ul>
+ *   <li>Detailed heap memory tracking before/after benchmarks</li>
+ *   <li>Class loading statistics</li>
+ *   <li>Thread count monitoring</li>
+ *   <li>GC activity reporting</li>
+ *   <li>Groovy-specific metrics (callsite count estimation)</li>
+ * </ul>
+ * <p>
+ * Run with: java -cp ... ProfiledBenchmarkRunner [benchmark-pattern]
+ */
+public class ProfiledBenchmarkRunner {
+
+    private static final MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
+    private static final ClassLoadingMXBean classLoadingBean = ManagementFactory.getClassLoadingMXBean();
+    private static final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
+    private static final List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
+
+    /**
+     * Main entry point for running profiled benchmarks.
+     */
+    public static void main(String[] args) throws RunnerException {
+        String pattern = args.length > 0 ? args[0] : ".*";
+
+        System.out.println("=" .repeat(80));
+        System.out.println("PROFILED BENCHMARK RUNNER");
+        System.out.println("Pattern: " + pattern);
+        System.out.println("=" .repeat(80));
+        System.out.println();
+
+        // Capture initial state
+        SystemState initialState = captureSystemState();
+        printSystemState("INITIAL STATE", initialState);
+
+        // Build JMH options
+        Options opt = new OptionsBuilder()
+                .include(pattern)
+                .warmupIterations(3)
+                .measurementIterations(5)
+                .forks(1)
+                .build();
+
+        // Run benchmarks
+        System.out.println("\nRunning benchmarks...\n");
+        new Runner(opt).run();
+
+        // Capture final state
+        SystemState finalState = captureSystemState();
+        printSystemState("FINAL STATE", finalState);
+
+        // Print delta
+        printStateDelta(initialState, finalState);
+    }
+
+    /**
+     * Capture current system state.
+     */
+    public static SystemState captureSystemState() {
+        // Force GC for accurate memory reading
+        System.gc();
+        try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
+
+        SystemState state = new SystemState();
+
+        // Memory
+        MemoryUsage heap = memoryBean.getHeapMemoryUsage();
+        state.heapUsed = heap.getUsed();
+        state.heapCommitted = heap.getCommitted();
+        state.heapMax = heap.getMax();
+
+        MemoryUsage nonHeap = memoryBean.getNonHeapMemoryUsage();
+        state.nonHeapUsed = nonHeap.getUsed();
+        state.nonHeapCommitted = nonHeap.getCommitted();
+
+        // Classes
+        state.loadedClassCount = classLoadingBean.getLoadedClassCount();
+        state.totalLoadedClassCount = classLoadingBean.getTotalLoadedClassCount();
+        state.unloadedClassCount = classLoadingBean.getUnloadedClassCount();
+
+        // Threads
+        state.threadCount = threadBean.getThreadCount();
+        state.peakThreadCount = threadBean.getPeakThreadCount();
+
+        // GC
+        for (GarbageCollectorMXBean gc : gcBeans) {
+            state.gcCount += gc.getCollectionCount();
+            state.gcTime += gc.getCollectionTime();
+        }
+
+        state.timestamp = System.currentTimeMillis();
+
+        return state;
+    }
+
+    /**
+     * Print system state.
+     */
+    public static void printSystemState(String label, SystemState state) {
+        System.out.println("-".repeat(60));
+        System.out.println(label);
+        System.out.println("-".repeat(60));
+        System.out.printf("Heap Memory:     %.2f MB used / %.2f MB committed / %.2f MB max%n",
+                state.heapUsed / 1024.0 / 1024.0,
+                state.heapCommitted / 1024.0 / 1024.0,
+                state.heapMax / 1024.0 / 1024.0);
+        System.out.printf("Non-Heap Memory: %.2f MB used / %.2f MB committed%n",
+                state.nonHeapUsed / 1024.0 / 1024.0,
+                state.nonHeapCommitted / 1024.0 / 1024.0);
+        System.out.printf("Classes:         %d loaded / %d total loaded / %d unloaded%n",
+                state.loadedClassCount, state.totalLoadedClassCount, state.unloadedClassCount);
+        System.out.printf("Threads:         %d current / %d peak%n",
+                state.threadCount, state.peakThreadCount);
+        System.out.printf("GC:              %d collections / %d ms total%n",
+                state.gcCount, state.gcTime);
+        System.out.println();
+    }
+
+    /**
+     * Print delta between two states.
+     */
+    public static void printStateDelta(SystemState before, SystemState after) {
+        System.out.println("=".repeat(60));
+        System.out.println("BENCHMARK IMPACT (Delta)");
+        System.out.println("=".repeat(60));
+
+        long heapDelta = after.heapUsed - before.heapUsed;
+        long nonHeapDelta = after.nonHeapUsed - before.nonHeapUsed;
+        int classLoadDelta = after.loadedClassCount - before.loadedClassCount;
+        long totalClassDelta = after.totalLoadedClassCount - before.totalLoadedClassCount;
+        long gcCountDelta = after.gcCount - before.gcCount;
+        long gcTimeDelta = after.gcTime - before.gcTime;
+        long duration = after.timestamp - before.timestamp;
+
+        System.out.printf("Duration:        %.2f seconds%n", duration / 1000.0);
+        System.out.printf("Heap Growth:     %+.2f MB%n", heapDelta / 1024.0 / 1024.0);
+        System.out.printf("Non-Heap Growth: %+.2f MB%n", nonHeapDelta / 1024.0 / 1024.0);
+        System.out.printf("Classes Loaded:  %+d (total: %+d)%n", classLoadDelta, totalClassDelta);
+        System.out.printf("GC Activity:     %d collections, %d ms%n", gcCountDelta, gcTimeDelta);
+
+        // Warnings
+        System.out.println();
+        if (heapDelta > 100 * 1024 * 1024) {
+            System.out.println("WARNING: Significant heap growth (>100MB) - possible memory leak");
+        }
+        if (classLoadDelta > 1000) {
+            System.out.println("WARNING: Large number of classes loaded (>1000) - check dynamic class generation");
+        }
+        if (gcTimeDelta > duration * 0.1) {
+            System.out.println("WARNING: High GC overhead (>10% of runtime)");
+        }
+
+        System.out.println();
+    }
+
+    /**
+     * System state snapshot.
+     */
+    public static class SystemState {
+        public long heapUsed;
+        public long heapCommitted;
+        public long heapMax;
+        public long nonHeapUsed;
+        public long nonHeapCommitted;
+        public int loadedClassCount;
+        public long totalLoadedClassCount;
+        public long unloadedClassCount;
+        public int threadCount;
+        public int peakThreadCount;
+        public long gcCount;
+        public long gcTime;
+        public long timestamp;
+    }
+
+    /**
+     * Estimate the number of Groovy callsites by examining loaded classes.
+     * This is a rough approximation based on class naming patterns.
+     */
+    public static int estimateCallsiteCount() {
+        int count = 0;
+        // This would require reflection to inspect GroovyClassLoader or CallSite arrays
+        // For now, return -1 to indicate "not implemented"
+        return -1;
+    }
+}
+
+/**
+ * JMH State that combines MemoryTrackingState with additional Groovy-specific tracking.
+ */
+@State(Scope.Benchmark)
+class GroovyProfilingState {
+
+    private ProfiledBenchmarkRunner.SystemState beforeState;
+    private ProfiledBenchmarkRunner.SystemState afterState;
+
+    @Setup(Level.Trial)
+    public void setup() {
+        beforeState = ProfiledBenchmarkRunner.captureSystemState();
+        ProfiledBenchmarkRunner.printSystemState("Benchmark Start", beforeState);
+    }
+
+    @TearDown(Level.Trial)
+    public void teardown() {
+        afterState = ProfiledBenchmarkRunner.captureSystemState();
+        ProfiledBenchmarkRunner.printSystemState("Benchmark End", afterState);
+        ProfiledBenchmarkRunner.printStateDelta(beforeState, afterState);
+    }
+
+    public ProfiledBenchmarkRunner.SystemState getBeforeState() {
+        return beforeState;
+    }
+
+    public ProfiledBenchmarkRunner.SystemState getAfterState() {
+        return afterState;
+    }
+}