Skip to content

GROOVY-12138: emit invokedynamic call sites for property writes (spike)#2674

Open
paulk-asert wants to merge 2 commits into
apache:masterfrom
paulk-asert:groovy12138
Open

GROOVY-12138: emit invokedynamic call sites for property writes (spike)#2674
paulk-asert wants to merge 2 commits into
apache:masterfrom
paulk-asert:groovy12138

Conversation

@paulk-asert

Copy link
Copy Markdown
Contributor

Property writes were the last un-cached dispatch surface: even in indy mode they compiled to static ScriptBytecodeAdapter.setProperty calls, paying getMetaClass() plus a full MetaClassImpl.setProperty resolution walk on every write, in both indy and classic modes.

Behind the compile-time flag groovy.indy.setproperty (default: off), AsmClassGenerator now diverts plain (non-safe, non-spread) property writes to a new CallSiteWriter.makeSetPropertySite hook. The default implementation is the classic adapter call, so classic mode and flag-off builds are unchanged; IndyCallSiteWriter overrides it to emit invokedynamic setProperty:(ReceiverType, ValueType)V with the receiver first, preserving the runtime's arguments[0]-is-receiver invariant.

The new SetPropertySelector (replacing the 'not supported' GroovyBugError for CallType.SET) fast-paths only shapes whose resolution is provably sender- and metaclass-independent: a public setter whose backing field is private or absent (GROOVY-8283 field-precedence is sender-dependent), or a public non-final field, each accepting the runtime value type directly. Everything else — Maps (GROOVY-8065/11367), Class receivers, categories, overridden/mixed-in/expando setProperty (detected by both a reflection probe and a metaclass pickMethod probe; ExpandoMetaClass is excluded wholesale, mirroring the GET selector), value coercion, listeners, propertyMissing — binds the sender-aware ScriptBytecodeAdapter.setProperty as the call-site handle, making non-fast-path behavior identical to the classic path by construction. Guards, PIC, and hit-count promotion come free from the existing call-site machinery; the value participates in the argument-class guards so type changes at a site re-select correctly.

Measured: ~42x on a hot property-write loop (6M mixed setter/field writes: ~435ms -> ~10ms, written state consumed and verified). Full root test suite (15678 tests) passes with the flag enabled for all test-code compilation; a 12-scenario semantic stress produces identical output compiled with and without the flag.

@codecov-commenter

codecov-commenter commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 9.90099% with 91 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.6439%. Comparing base (23780cf) to head (9ce62e0).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
...java/org/codehaus/groovy/vmplugin/v8/Selector.java 0.0000% 56 Missing ⚠️
.../groovy/classgen/asm/indy/InvokeDynamicWriter.java 0.0000% 19 Missing ⚠️
...va/org/codehaus/groovy/reflection/CachedField.java 0.0000% 10 Missing ⚠️
...s/groovy/classgen/asm/indy/IndyCallSiteWriter.java 42.8571% 3 Missing and 1 partial ⚠️
...vy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java 60.0000% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #2674        +/-   ##
==================================================
- Coverage     68.7019%   68.6439%   -0.0580%     
- Complexity      34123      34126         +3     
==================================================
  Files            1536       1536                
  Lines          128912     129018       +106     
  Branches        23369      23400        +31     
==================================================
- Hits            88565      88563         -2     
- Misses          32517      32619       +102     
- Partials         7830       7836         +6     
Files with missing lines Coverage Δ
...rg/codehaus/groovy/classgen/AsmClassGenerator.java 85.0616% <100.0000%> (+0.0217%) ⬆️
...g/codehaus/groovy/classgen/asm/CallSiteWriter.java 85.7143% <100.0000%> (+0.1374%) ⬆️
...vy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java 89.3939% <60.0000%> (-2.4093%) ⬇️
...s/groovy/classgen/asm/indy/IndyCallSiteWriter.java 63.6364% <42.8571%> (-9.6970%) ⬇️
...va/org/codehaus/groovy/reflection/CachedField.java 54.5454% <0.0000%> (-16.0428%) ⬇️
.../groovy/classgen/asm/indy/InvokeDynamicWriter.java 80.8743% <0.0000%> (-9.3696%) ⬇️
...java/org/codehaus/groovy/vmplugin/v8/Selector.java 72.9875% <0.0000%> (-7.9649%) ⬇️

... and 14 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.

Property writes were the last un-cached dispatch surface: even in indy
mode they compiled to static ScriptBytecodeAdapter.setProperty calls,
paying getMetaClass() plus a full MetaClassImpl.setProperty resolution
walk on every write, in both indy and classic modes.

Behind the compile-time flag groovy.indy.setproperty (default: off),
AsmClassGenerator now diverts plain (non-safe, non-spread) property
writes to a new CallSiteWriter.makeSetPropertySite hook. The default
implementation is the classic adapter call, so classic mode and
flag-off builds are unchanged; IndyCallSiteWriter overrides it to emit
invokedynamic setProperty:(ReceiverType, ValueType)V with the receiver
first, preserving the runtime's arguments[0]-is-receiver invariant.

The new SetPropertySelector (replacing the 'not supported' GroovyBugError
for CallType.SET) fast-paths only shapes whose resolution is provably
sender- and metaclass-independent: a public setter whose backing field
is private or absent (GROOVY-8283 field-precedence is sender-dependent),
or a public non-final field, each accepting the runtime value type
directly. Everything else — Maps (GROOVY-8065/11367), Class receivers,
categories, overridden/mixed-in/expando setProperty (detected by both a
reflection probe and a metaclass pickMethod probe; ExpandoMetaClass is
excluded wholesale, mirroring the GET selector), value coercion,
listeners, propertyMissing — binds the sender-aware
ScriptBytecodeAdapter.setProperty as the call-site handle, making
non-fast-path behavior identical to the classic path by construction.
Guards, PIC, and hit-count promotion come free from the existing
call-site machinery; the value participates in the argument-class
guards so type changes at a site re-select correctly.

Measured: ~42x on a hot property-write loop (6M mixed setter/field
writes: ~435ms -> ~10ms, written state consumed and verified). Full
root test suite (15678 tests) passes with the flag enabled for all
test-code compilation; a 12-scenario semantic stress produces identical
output compiled with and without the flag.
// otherwise (no meta property, listeners, expando, propertyMissing, ...): adapter path
}

private static final Class<?>[] SET_PROPERTY_PARAMS = {String.class, Object.class};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

just for style, this should be before the methods. Nothing blocking in my eyes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

moved

MethodHandle sba = MethodHandles.insertArguments(SBA_SET_PROPERTY, 1, sender);
sba = MethodHandles.insertArguments(sba, 2, name); // -> (Object value, Object receiver)void
handle = MethodHandles.permuteArguments(sba,
MethodType.methodType(void.class, Object.class, Object.class), 1, 0); // -> (receiver, value)void

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would consider not using SBA directly here. Instead I would make a method maybe even in the the selector, that has the arguments already in the right order and allows you to use a single insertArguments to inject sender and name and has the right remaining signature for the invocation. For example (name, receiverClass, receiver, value). Then one insertArguments call would be enough. You can still let that one call SBA... though I imagine we want to inline the remaining logic there in the long run.

@paulk-asert paulk-asert Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

changed if I understood you correctly

@testlens-app

testlens-app Bot commented Jul 10, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 9ce62e0
▶️ Tests: 40996 executed
⚪️ Checks: 31/31 completed


Learn more about TestLens at testlens.app.

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.

3 participants