Skip to content

Instrument LambdaMetafactory and preserve Runnable lambda identity during context propagation - #12346

Open
amarziali wants to merge 23 commits into
masterfrom
andrea.marziali/lambda
Open

Instrument LambdaMetafactory and preserve Runnable lambda identity during context propagation#12346
amarziali wants to merge 23 commits into
masterfrom
andrea.marziali/lambda

Conversation

@amarziali

Copy link
Copy Markdown
Contributor

What Does This Do

Instrument Runnable lambda classes when they are generated by LambdaMetafactory, allowing executor context propagation to attach state directly to the lambda instead of creating a RunnableWrapper.

This is primarily a functional improvement: avoiding the wrapper preserves the submitted task’s identity while retaining the existing context-propagation behavior.

Notable changes

  • Pass eligible lambda classes through the existing instrumentation pipeline before class definition.
  • Use the ignored-class trie to select exact supported lambda interfaces efficiently.
  • Initially allowlist java.lang.Runnable; the mechanism can later be extended to interfaces such as Callable.
  • Apply the existing Runnable field injection and execution advice to eligible lambda classes.
  • Support both JDK 8 and the module-aware transformation path used by newer JDKs.
  • Bypass the matcher memoizer only for lambda transformations because hidden lambda classes may share the same symbolic name.
  • Preserve the existing fallback behavior if transformation fails or becomes reentrant.
  • Skip agent-owned lambda classes.
  • Enable lambda instrumentation by default, with dd.trace.lambda.enabled=false available as an opt-out.

Known limitation

Concurrent submissions of the same Runnable lambda instance share a single continuation slot. As with other field-injected Runnable implementations, only one pending context can be retained, so overlapping submissions may lose or misassociate context propagation.

Previously, wrapped lambdas had per-submission state. This limitation is most relevant to reusable singleton or stateless lambdas submitted concurrently; lambdas created per operation are unlikely to encounter it.

Benchmarks

Benchmarks were run with dd.benchmark.enabled=true.

Spring Boot startup

A Spring Boot 3.3 Petclinic-style application was measured on Temurin 17 across 15 interleaved runs:

Configuration Mean Median
Lambda instrumentation disabled 1665.8 ms 1656 ms
Lambda instrumentation enabled 1676.7 ms 1670 ms
Paired difference +10.9 ms

The 95% confidence interval for the paired difference was ±43.9 ms, so the measured startup difference was indistinguishable from run-to-run noise.

The application generated approximately 1,600 lambda classes. Of those, 22 exact-Runnable candidates entered the transformer and 8 were modified.

JMH results

Benchmark Disabled Enabled Difference
New Runnable call-site linkage ~14 µs / 27 KB ~126 µs / 295 KB +112 µs / 268 KB
New Supplier call-site linkage ~14–15 µs / 27 KB ~14–15 µs / 27 KB No change
Capturing Runnable allocation 16 B/op 24 B/op +8 B/op
Traced executor submission ~0.009 ms ~0.009 ms No material change
Direct untraced run(), JDK 17 0.723 ns/op 0.817 ns/op +0.094 ns/op
Direct untraced run(), JDK 25 0.776 ns/op 0.815 ns/op +0.039 ns/op

The linkage cost is paid once when a matching lambda call site is created. Unrelated lambda interfaces take the fast trie-rejection path. The direct-run benchmark intentionally uses a nearly empty Runnable, making the relative percentage appear larger than the very small absolute overhead.

GC profiling confirms an 8-byte increase for instrumented capturing Runnable lambda instances due to the injected state field. This is the expected heap tradeoff and may be more visible in applications that retain large numbers of Runnable objects.

Motivation

Additional Notes

Contributor Checklist

Jira ticket: [PROJ-IDENT]

@amarziali

Copy link
Copy Markdown
Contributor Author

@DataDog review

@datadog-prod-us1-6

This comment has been minimized.

@datadog-prod-us1-6 datadog-prod-us1-6 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Datadog Autotest: FAIL

On JDK 8 and JDK 11, the new structure matcher requires a field that does not exist. The lambda transformer does not run, and Runnable lambdas still use wrappers.

Open Bits AI session

🤖 Datadog Autotest · Commit 054841c · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@amarziali
amarziali force-pushed the andrea.marziali/lambda branch from 054841c to 5742c20 Compare August 31, 2026 12:41
@amarziali

Copy link
Copy Markdown
Contributor Author

@DataDog review

@amarziali
amarziali requested a review from mcculls August 31, 2026 13:04

@datadog-prod-us1-6 datadog-prod-us1-6 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Datadog Autotest: PASS

More details

The PR instruments generated Runnable lambdas so executor propagation keeps task identity, with supported JDK paths and a Graal build guard. This update fixes the earlier JDK 8 and 11 samBase mismatch and adds no new finding; the documented concurrent-reuse limitation remains, and no review finding is open.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Datadog Autotest · Commit 5655a6f · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@amarziali

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T15:19:51.381448Z a721063 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@amarziali
amarziali requested a review from bric3 August 31, 2026 13:09
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 5655a6ff32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@dd-octo-sts

dd-octo-sts Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.14 s 13.83 s [+1.5%; +3.0%] (significantly worse)
startup:insecure-bank:tracing:Agent 12.91 s 12.95 s [-0.9%; +0.3%] (no difference)
startup:petclinic:appsec:Agent 17.01 s 16.71 s [+0.8%; +2.8%] (maybe worse)
startup:petclinic:iast:Agent 16.90 s 17.03 s [-1.6%; +0.1%] (no difference)
startup:petclinic:profiling:Agent 16.83 s 16.75 s [-0.5%; +1.5%] (no difference)
startup:petclinic:sca:Agent 17.05 s 16.38 s [+3.1%; +5.0%] (significantly worse)
startup:petclinic:tracing:Agent 16.15 s 16.06 s [-0.3%; +1.5%] (no difference)

Commit: 0c051006 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@amarziali
amarziali marked this pull request as ready for review August 31, 2026 15:06
@amarziali
amarziali requested review from a team as code owners August 31, 2026 15:06
@amarziali
amarziali requested review from jordan-wong and removed request for a team August 31, 2026 15:06

@datadog-prod-us1-6 datadog-prod-us1-6 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Autotest was unable to start this review.

Please try again by commenting @autotest review.

@dd-octo-sts

dd-octo-sts Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Add at least one type, and one component or instrumentation label to the pull request

If you need help, please check our contributing guidelines.

@amarziali amarziali added inst: java Core Java language instrumentation tag: concurrency Virtual Threads, Coroutines, Async, RX, Executors type: feature Enhancements and improvements labels Aug 31, 2026

@datadog-prod-us1-6 datadog-prod-us1-6 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Autotest was unable to start this review.

Please try again by commenting @autotest review.

@amarziali

Copy link
Copy Markdown
Contributor Author

@DataDog review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a721063dff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +202 to +205
if ((opcode == Opcodes.INVOKEVIRTUAL
&& "toByteArray".equals(name)
&& "()[B".equals(descriptor)
&& owner.endsWith("/ClassWriter"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle CDS-provided Runnable proxy classes

When CDS/AppCDS already contains a lambda proxy, spinInnerClass() can return the class obtained from LambdaProxyClassArchive.find(...) before either byte-generation call matched here executes. Consequently the helper never transforms that Runnable; executor propagation falls back to wrapping it, so task identity is still changed in CDS-enabled applications—the behavior this change is intended to eliminate.

Useful? React with 👍 / 👎.

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.

That's true. Right now this is fine meaning that for CDS there will be a fallback to wrapping. I'm more incline to evaluate that in a separate PR.

Comment on lines +28 to +30
if (!(task instanceof RunnableWrapper)
&& !(task instanceof FieldBackedContextAccessor)
&& !ExcludeFilter.exclude(ExcludeType.RUNNABLE, task)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Retain per-submission context for reused lambdas

When the same lambda instance is submitted more than once before its first execution consumes the stored continuation—for example, a static or non-capturing Runnable queued concurrently by two requests—skipping the wrapper makes every submission share one State slot. The second capture is rejected, and overlapping executions can either lose a parent or both resume the first submission's continuation, producing missing or incorrectly parented traces; this path needs per-submission state rather than opting every field-backed lambda out of wrapping.

Useful? React with 👍 / 👎.

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.

As I explcitly already put in the PR description for reviewers:

Previously, wrapped lambdas had per-submission state. This limitation is most relevant to reusable singleton or stateless lambdas submitted concurrently; lambdas created per operation are unlikely to encounter it.

This is a known limitation and already existing for all the other field injected classes that are going throught threadpool. It's not introduced by this change and can be considered a trade-off we can perhaps live together for now.

@datadog-prod-us1-6 datadog-prod-us1-6 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Datadog Autotest: FAIL

A hidden Runnable lambda can put its type description in the shared name cache. A later real class with the same legal binary name can receive wrong instrumentation on JDK 21 and later.

Open Bits AI session

🤖 Datadog Autotest · Commit a721063 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

SharedTypeInfo<TypeDescription> sharedType = types.find(name);
// Same-owner lambdas share a symbolic name, so build their target from the supplied bytes.
SharedTypeInfo<TypeDescription> sharedType =
transformingLambda && name.equals(targetName) ? null : types.find(name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Do not cache hidden lambdas by symbolic name

The agent can apply wrong instrumentation to the normal class.

Assertion details
  • Input: On JDK 21 or later, transform a hidden Runnable lambda for Foo, then define Foo.$Lambda in the same class loader.
  • Expected: The transformer must parse the normal class from its own bytecode.
  • Actual: The normal transformation reuses the hidden lambda type description from the shared name cache.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

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.

it's a good catch. I initially wanted to try not to touch this part as well but it seems something that can happen. A dedicate lambda matcher and a cache correctness fix have been done in 1bc6bbc . the perf test looks untouched

@amarziali

Copy link
Copy Markdown
Contributor Author

@DataDog review

@datadog-prod-us1-6 datadog-prod-us1-6 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Datadog Autotest: FAIL

The new module has no Gradle lock file. Its dependency graph can change between builds.

Open Bits AI session

🤖 Datadog Autotest · Commit b11c8c6 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

Comment thread dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle Outdated
@amarziali

Copy link
Copy Markdown
Contributor Author

@DataDog review

@datadog-prod-us1-6 datadog-prod-us1-6 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Datadog Autotest: FAIL

Disabled Runnable instrumentation still gets the new lambda transform cost. AppCDS and restricted SecurityManager use also skip the identity-preserving path.

Open Bits AI session

🤖 Datadog Autotest · Commit 35be726 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
// Match repackaged JDK APIs while excluding unrelated byte-array producers. The generated
// byte[] remains on the operand stack after the original call.
if ((opcode == Opcodes.INVOKEVIRTUAL

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Handle Runnable lambdas from AppCDS

AppCDS applications keep the old Runnable wrapper and do not preserve task identity.

Assertion details
  • Input: Load a Runnable lambda from an AppCDS archive that was made without the agent.
  • Expected: The agent must preserve Runnable identity for saved lambda classes or state this limit.
  • Actual: The visitor only changes newly built class bytes. An AppCDS archive hit returns the saved lambda class before these calls.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest · Open Bits AI session

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.

Archived lambda classes can bypass the normal generation path. Forcing transformation would reduce CDS benefits. Wrapping preserves propagation, although identity changes. It's a known limitation already documented and I don't think we should fix now

TypePoolFacade.beginLambdaTransform(interfaceClassName);
try {
return classFileTransformer.transform(
targetClass.getClassLoader(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Use agent access for class data

Affected applications keep the old wrapper and do not preserve Runnable identity.

Assertion details
  • Input: Link an application Runnable lambda when an active SecurityManager denies getClassLoader or getProtectionDomain access.
  • Expected: The agent must read the class data with agent access rights.
  • Actual: The class access calls throw SecurityException. The catch returns the original bytes and stops lambda instrumentation.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest · Open Bits AI session

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.

Accessing the target class’s protection domain can be denied. The code safely catches this and falls back. SecurityManager usage is increasingly rare, and IMHO changing the protection domain handling would be a risky change. I would steer not to fix it

@amarziali

Copy link
Copy Markdown
Contributor Author

@DataDog review

@datadog-prod-us1-6 datadog-prod-us1-6 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Datadog Autotest: FAIL

This PR instruments Runnable lambdas before class definition to keep task identity.
The new commit removes full transform work when no lambda matcher is active.
AppCDS archive hits still bypass this instrumentation.
SecurityManager restrictions can still block class metadata access and cause wrapper fallback.
This increment adds no new reportable issue.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Datadog Autotest · Commit 0c05100 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@dougqh

dougqh commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Question on LambdaMetafactoryInstrumentation.isEnabled() returning !Platform.isNativeImageBuilder(): what's the specific failure mode this guards against?

My understanding is that Native Image's AOT compilation doesn't actually route an app's lambda call sites through the real InnerClassLambdaMetafactory at all — Substrate statically resolves invokedynamic lambda sites during analysis and synthesizes proxy classes directly, bypassing the JDK's dynamic metafactory entirely. So this instrumentation's hook would never fire for the bulk of an application's lambdas in a native image regardless of this guard.

The case where it seems like it would actually matter is narrower: the native-image builder is itself a real JVM, and things like heap snapshotting execute static-initializer side effects for real — so a Runnable lambda eagerly instantiated in a <clinit> would go through the genuine metafactory on the builder process, and (if the agent's attached there for build-time instrumentation) could hit this patch.

Is that the actual scenario driving this guard, or is there something else about how Native Image touches InnerClassLambdaMetafactory that I'm missing? Also, is there a test (e.g. against the spring-boot-3.0-native smoke test) that exercises this path, or was the guard added defensively without observing the failure directly?

@dougqh dougqh left a comment

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.

A couple of things I'd flag while reviewing the LambdaMetafactory instrumentation (see thread for context on how these were derived).

public void visitEnd() {
super.visitEnd();
if (!injected) {
log.debug(

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.

This whole feature depends on matching a specific bytecode shape inside spinInnerClass/generateInnerClass that is tied to JDK internals (the comment above already notes it changed for JDK 25). When the shape doesn't match on some future JDK, the only signal is a debug-level log line here — field-injection for Runnable lambdas silently falls back to the wrap-based path with nothing visible in normal operation. Given this is exactly the kind of JDK-version regression that's easy to miss until a customer notices missing spans, would it be worth a one-time higher-severity log (or a startup self-test against the running JDK) instead of log.debug?


@Override
public ElementMatcher<TypeDescription> lambdaMatcher() {
return notExcludedByName(RUNNABLE);

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.

lambdaMatcher() and hierarchyMatcher() both start from notExcludedByName(RUNNABLE) independently. If one is tightened or loosened later (e.g. to exclude another framework's Runnable implementations) without updating the other, the metafactory-injection path and the normal hierarchy-instrumentation path would silently diverge on which Runnables they cover. Might be worth deriving one from the other (or a shared base matcher) so they can't drift apart.

@dougqh dougqh left a comment

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.

Follow-up review with the findings from the earlier automated pass (recovered from local scrollback after the first set I posted was reconstructed from a fuzzier summary). All 10 are re-verified here against the current head commit. The dominant pattern: since hidden lambda classes can never be retransformed later (wouldIgnore explicitly excludes names containing /), several distinct races/re-entrancy edges cause a silent, permanent loss of field-injection for individual lambdas — they quietly fall back to RunnableWrapper, which is safe but defeats this PR's own goal of identity preservation for the affected lambdas, with only a debug log to notice it by.

ids.clear();

long fromTick = InstrumenterMetrics.tick();
String lambdaInterface = TypePoolFacade.lambdaInterface();

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.

A lambda linked while EXPERIMENTAL_DEFER_INTEGRATIONS_UNTIL deferral is active returns false here and permanently skips field-injection: wouldIgnore() (line 193) explicitly excludes names containing / ("don't retransform lambdas"), so the later resumeMatching() retransform sweep never revisits it. That's a permanent, silent miss for any lambda linked during the deferral window, not just a delayed one.

return classBytes;
}
if (Boolean.TRUE.equals(TRANSFORMING.get())) {
log.debug("Lambda {} skipped: re-entrant transform", lambdaClassName);

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.

The TRANSFORMING re-entrancy guard drops field-injection for any lambda whose class definition is triggered as a side effect of processing another lambda's transform on the same thread (e.g. a lambda captured while defining/loading another lambda's supporting classes). That lambda permanently falls back to RunnableWrapper with only this debug log as a trace.

try {
return transformerBuilder.installOn(inst);
ClassFileTransformer classFileTransformer = transformerBuilder.installOn(inst);
registerLambdaTransformer(classFileTransformer, transformerBuilder.lambdaInterfaces());

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.

Narrow startup race: transformerBuilder.installOn(inst) (which can trigger retransformation) runs before registerLambdaTransformer calls LambdaTransformerHolder.set(...). A lambda linked in that window gets a null transformer in LambdaTransformerHelper.transform and permanently misses injection — there's no retry once the window closes.

return deferredTypes.computeIfAbsent(name, deferType);
}

private boolean isLambdaTarget(String name) {

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.

isLambdaTarget compares against targetName, a per-thread field that's set for the duration of beginLambdaTransform/endLambdaTransform but isn't scoped to a single lookup call. If resolving the lambda's type triggers resolution of an unrelated class on the same thread while the lambda transform is still "open" (e.g. an advice helper class loaded as a side effect), that unrelated class could get routed onto the lambda-only matching path in CombiningMatcher and skip its own normal instrumentation.

LambdaMatchRecorder[] recorders = lambdaMatchers.get(lambdaInterface);
if (null != recorders) {
for (LambdaMatchRecorder recorder : recorders) {
recorder.record(target, classLoader, ids);

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.

recorder.record(target, classLoader, ids) here isn't wrapped in try/catch, unlike the equivalent loop over regular MatchRecorders a few lines below (line 111-112, which logs and continues on Throwable). An exception from a LambdaMatchRecorder propagates uncaught instead of being handled the same way.

// Anonymous classes have '/' in class name which is not allowed in 'normal' classes.
// Field-injected tasks are already instrumented and must retain their identity.
if (!(task instanceof RunnableWrapper)
&& !(task instanceof FieldBackedContextAccessor)

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.

instanceof FieldBackedContextAccessor is used here as a proxy for "already has propagation fields," but that marker isn't scoped to a specific context store. If a future second ForLambda instrumenter targets Runnable with a different context store, this check would treat its lambdas as already handled and skip wrapping them too, silently dropping propagation for that store with no fallback.

# matching and transformation pipeline. Keep this list narrow: the lookup runs for every lambda
# linkage in the application.

1 java.lang.Runnable

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.

This allowlist is duplicated between this trie and each ForLambda instrumenter's runtime lambdaInterface() (and the array built from them). Nothing enforces they stay in sync — forgetting to add a new interface here while adding a new ForLambda instrumenter would make LambdaInterfaceNameTrie.apply return something other than 1, silently no-oping the new instrumentation instead of failing loudly.

boolean isOutline = typeParser == outlineTypeParser;
long fromTick = InstrumenterMetrics.tick();
// Hidden lambda names may later be reused by an ordinary class definition.
boolean cacheable = !isLambdaTarget(name);

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.

"Don't cache under a lambda's transient hidden-class name" is reimplemented ad hoc at this call site and several others (line 394 and the corresponding guards in Memoizer/WithLocation) rather than enforced once at the cache boundary. It's currently consistent, but every new cache added to this area has to remember to add its own isLambdaTarget/isCacheable check rather than getting it for free.

return null;
}
return (className, targetClass, classBytes, interfaceName) -> {
for (String enabledInterface : lambdaInterfaces) {

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.

filterLambdaTransformer does a linear scan over lambdaInterfaces per lambda link, on exactly the hot path the PR's own JMH benchmark (LambdaExecutorBenchmark) measures. CombiningTransformerBuilder already has these as a Set/keyed map before it gets flattened to this array — reusing that instead of a per-call linear scan would avoid the O(n) lookup for every lambda linkage.

@SuppressWarnings("unchecked")
private static LambdaTransformer newLambdaTransformer(
final ClassFileTransformer classFileTransformer) {
if (JavaVirtualMachine.isJavaVersionAtLeast(9)) {

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.

This JDK9+ reflective-loading logic duplicates the existing idiom used elsewhere in this class/AgentStrategies for loading version-specific helper classes reflectively. Worth sharing that helper instead of a second bespoke reflection path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

inst: java Core Java language instrumentation tag: concurrency Virtual Threads, Coroutines, Async, RX, Executors type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants