Skip to content

GROOVY-10307: add JMH benchmarks for Grails-like invokedynamic pain points#2385

Merged
paulk-asert merged 5 commits intoapache:masterfrom
jamesfredley:groovy-10307-grails-like-benchmarks
Feb 27, 2026
Merged

GROOVY-10307: add JMH benchmarks for Grails-like invokedynamic pain points#2385
paulk-asert merged 5 commits intoapache:masterfrom
jamesfredley:groovy-10307-grails-like-benchmarks

Conversation

@jamesfredley
Copy link
Contributor

https://issues.apache.org/jira/browse/GROOVY-10307

Follow-up to #2381 - adds 43 JMH benchmarks specifically targeting the metaclass invalidation patterns that cause performance regression in Grails applications under invokedynamic. While #2381 covers general Groovy language features, these benchmarks exercise the specific dynamic dispatch pain points identified during the optimization work in #2374 and #2377.

Run with: ./gradlew perf:jmh -PbenchInclude=perf

Benchmark Files

File Benchmarks Coverage
MetaclassChangeBench 11 ExpandoMetaClass method addition during dispatch, metaclass replacement cycles, multi-class invalidation cascade (change on ServiceA invalidates ServiceB/C call sites), burst-then-steady-state (Grails startup then request handling), property access and closure dispatch under metaclass churn
CategoryBench 10 use(Category) blocks inside vs outside loops, nested and simultaneous multi-category scopes, collateral invalidation damage on non-category call sites, category method shadowing
DynamicDispatchBench 12 methodMissing with single/rotating names (dynamic finders), propertyMissing read/write (params/session), GroovyInterceptable invokeMethod interception (transactional services), ExpandoMetaClass-injected methods mixed with real methods, def-typed polymorphic dispatch
GrailsLikePatternsBench 10 Service chain with validation/CRUD/collection processing, controller action dispatch with param binding and view rendering, domain validation with dynamic property access (this."$field"), configuration DSL with nested @DelegatesTo closures, markup builder with nested tag/closure rendering, full request cycle simulation

43 benchmarks total, all using proper Blackhole consumption and @Setup(Level.Iteration) metaclass cleanup where appropriate.

Pain Points Exercised

These benchmarks target the root cause identified in #2374 and #2377: global SwitchPoint invalidation when any metaclass changes. In baseline Groovy, every metaclass modification (ExpandoMetaClass method addition, category enter/exit, metaclass replacement) triggers invalidateSwitchPoints(), which invalidates ALL invokedynamic call sites across the entire application.

Metaclass Invalidation Patterns (MetaclassChangeBench)

The primary pain point. Each benchmark includes a baseline (no metaclass changes) for direct comparison:

  • expandoMethodAddition vs baselineNoMetaclassChanges - measures the cost of ExpandoMetaClass modifications on nearby call sites
  • frequentExpandoChanges - every 100 calls instead of 1000, simulating aggressive framework metaclass modification
  • multiClassMetaclassChurn vs baselineMultiClassNoChanges - demonstrates the cascade: modifying class A's metaclass invalidates call sites for classes B and C
  • burstThenSteadyState - simulates Grails startup (many metaclass changes) followed by request handling (stable dispatch)

Category Scope Patterns (CategoryBench)

Every use(Category) { } triggers two SwitchPoint invalidations (enter + exit):

  • categoryInLoop vs singleCategoryWrappingLoop - worst case (N invalidation pairs) vs best case (1 pair)
  • categoryWithOutsideCalls vs baselineEquivalentWithoutCategory - measures collateral damage on non-category code from nearby category enter/exit
  • nestedCategories / multipleCategoriesSimultaneous - Grails often has multiple category layers active

Dynamic Dispatch Patterns (DynamicDispatchBench)

The building blocks of Grails' convention-based model:

  • methodMissing - dynamic finders (findByName, findByAge), rotating names at same call site
  • propertyMissing - dynamic property injection (params, session attributes)
  • invokeMethod - method interception for transactions, security
  • expandoInjectedMethodCall / mixedRealAndInjectedCalls - runtime method injection (GORM save/delete/validate)

Composite Grails Patterns (GrailsLikePatternsBench)

Realistic multi-layer patterns combining closures, dynamic dispatch, property access, and delegation:

  • controllerActionPattern / controllerActionDuringMetaclassChurn - same work with and without metaclass changes, showing the invalidation cost
  • fullRequestCycleSimulation / fullRequestCycleDuringMetaclassChurn - controller + service + validation + rendering, the closest approximation to a real Grails request
  • configurationDsl / markupBuilderPattern - nested @DelegatesTo closures with DELEGATE_FIRST resolution

Copilot AI review requested due to automatic review settings February 26, 2026 17:21
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new set of JMH benchmarks under subprojects/performance to reproduce Grails-like invokedynamic performance regressions, focusing on metaclass invalidation patterns (ExpandoMetaClass churn, category enter/exit, dynamic dispatch mechanisms, and composite “request cycle” scenarios).

Changes:

  • Introduces MetaclassChangeBench to benchmark ExpandoMetaClass updates, metaclass replacement, and multi-class invalidation cascades.
  • Introduces CategoryBench to benchmark use(Category) scope patterns and their invalidation overhead.
  • Introduces DynamicDispatchBench and GrailsLikePatternsBench to cover Grails-style dynamic dispatch building blocks and composite application flows.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/MetaclassChangeBench.groovy Benchmarks metaclass churn and replacement patterns affecting indy call sites.
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/CategoryBench.groovy Benchmarks category scope usage patterns and their impact on dispatch/invalidation.
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/DynamicDispatchBench.groovy Benchmarks methodMissing/propertyMissing/invokeMethod/expando-injection and def-typed dispatch.
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/GrailsLikePatternsBench.groovy Benchmarks composite, Grails-like request/service/domain/view rendering patterns with/without churn.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…oints

Add 43 JMH benchmarks across 4 files targeting the metaclass invalidation
patterns that cause performance regression in Grails applications under
invokedynamic. These complement the general-purpose benchmarks from apache#2381
by exercising the specific dynamic dispatch patterns identified in apache#2374
and apache#2377.

New benchmark files:

MetaclassChangeBench (11 benchmarks)
- ExpandoMetaClass method addition during method dispatch
- Metaclass replacement cycles
- Multi-class invalidation cascade (change on ServiceA invalidates ServiceB/C)
- Burst-then-steady-state (simulates Grails startup then request handling)
- Property access and closure dispatch under metaclass churn

CategoryBench (10 benchmarks)
- use(Category) blocks inside vs outside loops
- Nested and simultaneous multi-category scopes
- Collateral invalidation damage on non-category call sites
- Category method shadowing existing methods

DynamicDispatchBench (12 benchmarks)
- methodMissing with single/rotating names (dynamic finders)
- propertyMissing read/write (Grails params/session)
- GroovyInterceptable invokeMethod interception (transactional services)
- ExpandoMetaClass-injected method calls mixed with real methods
- def-typed monomorphic and polymorphic dispatch

GrailsLikePatternsBench (10 benchmarks)
- Service chain: validation, CRUD, collection processing
- Controller action: param binding, service call, model/view rendering
- Domain validation with dynamic property access (this."$field")
- Configuration DSL with nested @DelegatesTo closures
- Markup builder with nested tag/closure rendering
- Full request cycle simulation with and without metaclass churn

Run with: ./gradlew perf:jmh -PbenchInclude=perf
@jamesfredley jamesfredley force-pushed the groovy-10307-grails-like-benchmarks branch from 35a08fd to aa5fdd3 Compare February 26, 2026 17:43
@codecov-commenter
Copy link

codecov-commenter commented Feb 26, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.7220%. Comparing base (b64ca55) to head (06137c7).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #2385        +/-   ##
==================================================
- Coverage     66.7263%   66.7220%   -0.0043%     
  Complexity      29863      29863                
==================================================
  Files            1382       1382                
  Lines          116215     116215                
  Branches        20508      20508                
==================================================
- Hits            77546      77541         -5     
- Misses          32321      32326         +5     
  Partials         6348       6348                

see 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Move the 4 Grails-like benchmark files into org.apache.groovy.perf.grails
subpackage and add a matrix strategy to the jmh-perf and jmh-perf-classic
workflows so core and grails benchmarks run as parallel CI jobs, each with
its own 60-minute budget. Restore original @fork(2) and @measurement(iterations=5)
settings now that the benchmarks have dedicated time slots.
@paulk-asert
Copy link
Contributor

LGTM

@paulk-asert paulk-asert merged commit cb35b63 into apache:master Feb 27, 2026
22 checks passed
@jamesfredley jamesfredley deleted the groovy-10307-grails-like-benchmarks branch February 27, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants