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>
+ * @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;
+ }
+}