Skip to content

Fix instrumenting metric probes for Kotlin#11890

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
masterfrom
jpbempel/fix-metric-suspend-fun
Jul 9, 2026
Merged

Fix instrumenting metric probes for Kotlin#11890
gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
masterfrom
jpbempel/fix-metric-suspend-fun

Conversation

@jpbempel

@jpbempel jpbempel commented Jul 8, 2026

Copy link
Copy Markdown
Member

What Does This Do

track objects on the stack with frame for each instruction using ASM analyzer. based on this generate POP/POP2 bytecode instruction to leave the stack clean before inserting metric calls.

Motivation

Kotlin compiler can, especially for suspend functions, generates a state machine with duplicate values on the stack while a return instruction will clear them (dropping the frame).

we are previously relying on the fact that the number of values on the stack are precise even before a return instruction which leads to introduce jump to a same location but without the same number of values on the stack, triggering the verifier.

Additional Notes

Claude analysis:

 Look at the original, uninstrumented download() bytecode right before the first areturn (the orig.txt dump, bci 160–192):                                                     
                                                                                                                                                                                
  160: invokestatic  DelayKt.delay(...)   // stack: [R]           R = delay() result                                                                                            
  163: dup                                // stack: [R, R]                                                                                                                      
  164: aload         7                    // stack: [R, R, S]     S = COROUTINE_SUSPENDED sentinel (var7)                                                                       
  166: if_acmpeq     178                  // pops R,S; if equal -> stack: [R]   (one dup of R survives)                                                                         
  ...                                                                                                                                                                           
  178: aload         4  ...  183: bastore // jacoco bookkeeping, stack-neutral -> still [R]                                                                                     
  184: aload         7                    // stack: [R, S]        pushes the sentinel AGAIN                                                                                     
  186: aload         4  ...  191: bastore // jacoco bookkeeping, stack-neutral -> still [R, S]                                                                                  
  192: areturn                            // returns S (top of stack); R is silently discarded                                                                                  
                                                                                                                                                                                
  This is completely legal, ordinary bytecode: kotlinc's suspend-dispatch pattern leaves a redundant duplicate value (R) underneath the actual return value (S), relying on the 
  fact that areturn is a terminal instruction — the JVM only cares about the top-of-stack value at return time and simply throws away everything else below it. There's no      
  verification problem in the original class because nothing executes after that areturn that would need a merged stack-map frame.                                              
                                                                                                                                                                                
  Now look at what MetricInstrumenter.getBeforeReturnInsnList (MetricInstrumenter.java:192-208) does to this exact ARETURN:                                                     
                                                                                                                                                                                
  int tmpIdx = newVar(size);                                                                                                                                                    
  InsnList insnList = wrapTryCatch(callMetric(metricProbe, node, new ReturnContext(tmpIdx, loadOpCode, returnType)));                                                           
  insnList.insert(new VarInsnNode(storeOpCode, tmpIdx));  // ASTORE tmpIdx  — prepended                                                                                         
  insnList.add(new VarInsnNode(loadOpCode, tmpIdx));      // ALOAD  tmpIdx  — appended                                                                                          

  It assumes the stack at an ARETURN site contains exactly one value — the thing being returned — and replaces the bare areturn with ASTORE tmp → [try{ metric-emission code    
  }catch] → ALOAD tmp → GOTO sharedReturn. That assumption is false for this call site: it pops only the top value (S, into var 9), but the redundant R sitting underneath is   
  not consumed by anything anymore, because the code that follows the store is no longer a terminal areturn — it's non-terminal metric-emission code (ldc/getstatic/invokestatic
  DebuggerContext.metric) that pushes and pops its own operands from scratch, oblivious to the stale R still sitting beneath it.                                                

   So in the instrumented class, at the point corresponding to bci 230 (aload 9, right after the invokestatic metric(...) call returns):                                         
  - Via normal fall-through (227 → 230): the real stack is [R] — one leftover item, never cleaned up.                                                                           
  - Via the inner exception handler (bci 320 pop; 321 goto 230): when an exception is thrown out of the metric-emission code, the JVM's exception-dispatch semantics clear the  
  entire operand stack down to just the thrown exception before entering the handler (per the JVM spec — this is mandatory, not optional). So after pop removes the exception,  
  the real stack there is genuinely [] — the leftover R is gone, wiped out by the exception dispatch, not by any instruction in the instrumentation's own code.                 
                                                                                                                                                                                
  Two different real stack depths (1 vs. 0) converge on the same label (bci 230) — that is exactly what "Inconsistent stackmap frames at branch target 230" is reporting. It's  
  not really a "bad/stale StackMapTable entry" bug so much as the instrumentation genuinely producing divergent stack depths on the two converging paths.                       

Contributor Checklist

  • Format the title according to the contribution guidelines
  • Assign the type: and (comp: or inst:) labels in addition to any other useful labels
  • Avoid using close, fix, or any linking keywords when referencing an issue
    Use solves instead, and assign the PR milestone to the issue
  • Update the CODEOWNERS file on source file addition, migration, or deletion
  • Update public documentation with any new configuration flags or behaviors
  • Add your completed PR to the merge queue by commenting /merge. You can also:
    • Customize the commit message associated with the merge with /merge --commit-message "..."
    • Remove your PR from the merge queue with /merge -c
    • Skip all merge queue checks with /merge -f --reason "reason"; please use this judiciously, as some checks do not run at the PR-level (note: the PR still needs to be mergeable, this will only skip the pre-merge build)
    • Get more information in this doc

Jira ticket: DEBUG-5816

track objects on the stack with frame for each instruction using
ASM analyzer. based on this generate POP/POP2 bytecode instruction
to leave the stack clean before inserting metric calls.

Kotlin compiler can, especially for suspend functions, generates
a state machine with duplicate values on the stack while a return
instruction will clear them (dropping the frame).

we are previously relying on the fact that the number of values on the
stack are precise even before a return instruction which leads to
introduce jump to a same location but without the same number of
values on the stack, triggering the verifier.
@jpbempel jpbempel requested a review from a team as a code owner July 8, 2026 16:20
@jpbempel jpbempel requested review from andreimatei and removed request for a team July 8, 2026 16:20
@dd-octo-sts

dd-octo-sts Bot commented Jul 8, 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.

@jpbempel jpbempel added comp: debugger Dynamic Instrumentation type: bug fix Bug fix labels Jul 8, 2026

@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: b063fcae74

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

when(config.isDynamicInstrumentationEnabled()).thenReturn(true);
when(config.isDynamicInstrumentationClassFileDumpEnabled()).thenReturn(true);
when(config.isDynamicInstrumentationVerifyByteCode()).thenReturn(true);
// when(config.isDynamicInstrumentationVerifyByteCode()).thenReturn(true);

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 Keep bytecode verification enabled in metric tests

Because Mockito returns false for unstubbed booleans, commenting out this stub disables DebuggerTransformer.verifyByteCode for every metric-probe instrumentation test, while the product default for DD_DYNAMIC_INSTRUMENTATION_VERIFY_BYTECODE is enabled. The Kotlin exit-probe change is specifically verifier-sensitive, so these tests can now pass with bytecode that would be rejected in the default production path; keep verification enabled or scope any opt-out to a documented exceptional case.

Useful? React with 👍 / 👎.

@datadog-datadog-us1-prod

This comment has been minimized.

@pr-commenter

pr-commenter Bot commented Jul 8, 2026

Copy link
Copy Markdown

Debugger benchmarks

Parameters

Baseline Candidate
baseline_or_candidate baseline candidate
ci_job_date 1783585303 1783585650
end_time 2026-07-09T08:23:09 2026-07-09T08:28:56
git_branch master jpbempel/fix-metric-suspend-fun
git_commit_sha bd65ef4 61454ee
start_time 2026-07-09T08:21:44 2026-07-09T08:27:31
See matching parameters
Baseline Candidate
ci_job_id 1844258894 1844258894
ci_pipeline_id 123700996 123700996
cpu_model Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz
git_commit_date 1783584369 1783584369

Summary

Found 0 performance improvements and 0 performance regressions! Performance is the same for 10 metrics, 5 unstable metrics.

See unchanged results
scenario Δ mean agg_http_req_duration_min Δ mean agg_http_req_duration_p50 Δ mean agg_http_req_duration_p75 Δ mean agg_http_req_duration_p99 Δ mean throughput
scenario:noprobe unstable
[-46.941µs; +17.850µs] or [-15.403%; +5.857%]
unstable
[-62.714µs; +28.709µs] or [-17.957%; +8.220%]
unstable
[-72.667µs; +36.687µs] or [-19.886%; +10.040%]
unstable
[-47.471µs; +338.352µs] or [-3.901%; +27.801%]
same
scenario:basic same same same unstable
[+167.530µs; +403.910µs] or [+16.974%; +40.923%]
same
scenario:loop unsure
[+3.068µs; +9.521µs] or [+0.035%; +0.107%]
same same same same
Request duration reports for reports
gantt
    title reports - request duration [CI 0.99] : candidate=None, baseline=None
    dateFormat X
    axisFormat %s
section baseline
noprobe (349.243 µs) : 293, 405
.   : milestone, 349,
basic (296.531 µs) : 289, 304
.   : milestone, 297,
loop (8.98 ms) : 8974, 8986
.   : milestone, 8980,
section candidate
noprobe (332.241 µs) : 307, 358
.   : milestone, 332,
basic (296.453 µs) : 290, 303
.   : milestone, 296,
loop (8.983 ms) : 8977, 8989
.   : milestone, 8983,
Loading
  • baseline results
Scenario Request median duration [CI 0.99]
noprobe 349.243 µs [293.368 µs, 405.118 µs]
basic 296.531 µs [289.262 µs, 303.8 µs]
loop 8.98 ms [8.974 ms, 8.986 ms]
  • candidate results
Scenario Request median duration [CI 0.99]
noprobe 332.241 µs [306.658 µs, 357.824 µs]
basic 296.453 µs [289.813 µs, 303.092 µs]
loop 8.983 ms [8.977 ms, 8.989 ms]

@dd-octo-sts

dd-octo-sts Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🟡 Java Benchmark SLOs — Performance SLO warning (near threshold)

Suite Status
Startup 🟡 warning

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 13.86 s 13.93 s [-1.4%; +0.4%] (no difference)
startup:insecure-bank:tracing:Agent 13.01 s 13.01 s [-0.6%; +0.7%] (no difference)
startup:petclinic:appsec:Agent 17.48 s 17.31 s [-0.0%; +1.9%] (no difference)
startup:petclinic:iast:Agent 17.38 s 17.50 s [-1.3%; -0.1%] (maybe better)
startup:petclinic:profiling:Agent 17.33 s 17.24 s [-0.4%; +1.4%] (no difference)
startup:petclinic:sca:Agent 17.53 s 17.35 s [+0.2%; +1.8%] (maybe worse)
startup:petclinic:tracing:Agent 16.52 s 16.63 s [-1.6%; +0.4%] (no difference)

Commit: 61454ee0 · 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.

}
} catch (AnalyzerException ex) {
LOGGER.debug("Failed to analyze method[%s::%s] instructions", owner, methodNode.name, ex);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can this produce a half-finished map? Would that be an issue?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No because the specific exception is thrown only by ASM's Analyzer.analyze method so when the exception is thrown, the map is untouchly empty


@Override
protected InsnList getBeforeReturnInsnList(AbstractInsnNode node) {
protected InsnList getBeforeReturnInsnList(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are we sure this is limited to metric probes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Until contrary proven, yes because this is specific to the code inserted for metric probe instrumentation.
Though, it is not excluded to manifest in other instrumentation cases.
This is why we will add exploration tests for Kotlin code

@jpbempel

jpbempel commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Jul 9, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-07-09 09:19:19 UTC ℹ️ Start processing command /merge


2026-07-09 09:19:24 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 2h (p90).


2026-07-09 10:17:20 UTC ℹ️ MergeQueue: This merge request was merged

@gh-worker-dd-mergequeue-cf854d gh-worker-dd-mergequeue-cf854d Bot merged commit a4087b6 into master Jul 9, 2026
593 of 596 checks passed
@gh-worker-dd-mergequeue-cf854d gh-worker-dd-mergequeue-cf854d Bot deleted the jpbempel/fix-metric-suspend-fun branch July 9, 2026 10:17
@github-actions github-actions Bot added this to the 1.65.0 milestone Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: debugger Dynamic Instrumentation type: bug fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants