Skip to content

profiling(ddprof): migrate context bridge to the all-native API (Phase 2)#11899

Open
rkennke wants to merge 5 commits into
masterfrom
rkennke/profiler-all-native-context-phase2
Open

profiling(ddprof): migrate context bridge to the all-native API (Phase 2)#11899
rkennke wants to merge 5 commits into
masterfrom
rkennke/profiler-all-native-context-phase2

Conversation

@rkennke

@rkennke rkennke commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What

Migrates the profiler context bridge off the deprecated DirectByteBuffer (DBB) context API onto java-profiler's all-native API (setTraceContext / clearTraceContext / setContextValue / clearContextValue).

This is Phase 2 of the all-native context work (Phase 1 = DataDog/java-profiler#631).

Why

  • Fixes the virtual-thread use-after-free: the DBB path cached a DirectByteBuffer over the native OTEP record; on a carrier that a mounted virtual thread later migrated off, the record was freed while the cached buffer kept being written. The all-native API resolves the current carrier's record inside each JNI call — no cached buffer to dangle.
  • Per-activation perf: activation collapses from setContext + 2×setContextValue (3 JNI calls) into one setTraceContext (~28% faster on the combined cycle per the design-note benchmark).

Changes

  • DatadogProfilingIntegration.activate → one setTraceContext(...) carrying trace/span context + operation & resource attributes; close/clearContextclearTraceContext().
  • DatadogProfiler: setContextValue/clearContextValue/reapplyAppContext/syncNativeAppContext are now all-native. reapplyAppContext uses a per-slot native setContextValue loop — native setContextValue publishes the record (valid=1) even with no active span, so app context stays visible between spans (preserves Restore app context attributes wiped by ddprof setContext on span activation #11646). A native batch reapply is deferred to a measured follow-up (java-profiler PROF-15361).
  • AppContextSnapshot simplified to strings-only (the native path resolves each value's encoding via the process-wide cache — no cached id/utf8/snapshotTags DBB read).
  • snapshot() uses the new native copyContextTags read (no ThreadContext/DBB, so it observes native writes without resetting the record).
  • ContextSetter kept only for offsetOf + size (pure Java); no DBB usage remains.

Behavior notes

  • Per-slot reapply is not atomic across app attributes. reapplyAppContext publishes each app-managed attribute with its own native setContextValue (detach→attach), so a profiler sample landing mid-reapply can observe a partial app-attribute set on activation — a window the old batch fast-path (setContextValuesByIdAndBytes, single attach) did not have. This is deliberate for the expected small app-attribute cardinality (0–2); the single-call batch reapply is the measured follow-up (PROF-15361). Trace/span + operation/resource stay atomic (one setTraceContext).
  • Defensive zero-span guard. The native setTraceContext rejects spanId==0 (it is the activation path; clearing is clearTraceContext). Span ids are non-zero by construction (IdGenerationStrategy never yields 0, DDSpanId.ZERO means "no span", and DDSpanContext is the only ProfilerContext), so the bridge routes a stray zero span to clearTraceContext() rather than letting the IllegalArgumentException be swallowed over stale context.

Dependency

Requires java-profiler's all-native API, from DataDog/java-profiler#631. Master now pins ddprof = 1.47.0 (#12055), which includes it, so this branch has been rebased and builds against the published release — no local snapshot needed.

Status / verification

  • Rebased onto master with the published ddprof:1.47.0 (Bump java-profiler (ddprof) to 1.47.0 #12055); compiles clean (no cannot find symbol on the new calls) and the profiling-ddprof unit test suite is GREEN on Linux post-rebase.
  • Previously verified GREEN on Linux against a local 1.47.0-SNAPSHOT (pre-rebase, same code): dd-smoke-tests:profiling-integration-tests (CodeHotspotsTest, JFRBasedProfilingIntegrationTest) — 20 tests, 0 failures (end-to-end agent + profiler + JFR, exercising span/op/resource context). Will re-run against the published release before merge.

🤖 Generated with Claude Code

gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Jul 23, 2026
Bump java-profiler (ddprof) to 1.47.0

Required to continue work on the all-native profiler context bridge (#11899).

Merge branch 'master' into rkennke/bump-ddprof-1.47.0

Raise agent jar size budget for java-profiler 1.47.0

The ddprof 1.47.0 native library growth pushed the shadow jar past the
previous budget; raise it to unblock the verifyAgentJarContents check.

Merge branch 'master' into rkennke/bump-ddprof-1.47.0

Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
rkennke and others added 3 commits July 23, 2026 18:54
Switch the profiler context bridge off the deprecated DirectByteBuffer (DBB)
context API onto java-profiler's all-native API (setTraceContext /
clearTraceContext / setContextValue / clearContextValue). This eliminates the
virtual-thread use-after-free (the DBB cached buffer that dangled on carrier
migration) and folds the per-activation sequence (setContext + two
setContextValue) into a single native call.

- DatadogProfilingIntegration.activate: one setTraceContext(...) carrying
  trace/span context + operation and resource attributes (3 JNI calls -> 1);
  close/clearContext: clearTraceContext() (wipes op/resource slots too).
- DatadogProfiler: setContextValue/clearContextValue/reapplyAppContext/
  syncNativeAppContext are now all-native. reapplyAppContext uses a per-slot
  native setContextValue loop (native setContextValue publishes valid=1, so app
  context stays visible without an active span — preserves PR #11646). A native
  batch reapply is deferred to a measured follow-up (java-profiler PROF-15361).
- AppContextSnapshot simplified to strings-only (the native path resolves each
  value's encoding via the process-wide cache; no cached id/utf8/snapshotTags).
- snapshot() uses the new native copyContextTags read (no ThreadContext/DBB, so
  it observes native writes without resetting the record).
- ContextSetter kept only for offsetOf + size (pure Java); no DBB usage remains.

Requires java-profiler with the all-native API (ddprof >= the phase-1 release;
DataDog/java-profiler#631). Build/test with -PddprofUseSnapshot=true against a
local publishToMavenLocal 1.47.0-SNAPSHOT until that ships. ddprof suite is
Linux-gated (assumeTrue(isLinux)) — verify on Linux/CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ContextValue

The native setTraceContext rejects spanId==0 with IllegalArgumentException (it is
the activation path; clearing is clearTraceContext). The bridge's catch(Throwable)
would swallow that throw and leave the previous span's context stale on the thread.
Span ids are non-zero by construction (IdGenerationStrategy never yields 0,
DDSpanId.ZERO means "no span", and DDSpanContext is the only ProfilerContext), so
this is defensive: route a zero span to a clean clearTraceContext instead of a
silently-swallowed throw over stale state.

Also document clearContextValue(int)'s return contract (@param/@return) and add a
testContextRegistration scenario asserting a zero-span activation does not throw and
still reapplies app context.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ical references

Rewrite or remove comments that documented the superseded DirectByteBuffer context
API and the ddprof-version history of the bridge (e.g. "Replaces the previous
setContext...", "no DBB read", the 1.41.0/1.45.0 no-op history in
DatadogProfilingScope). Those explain how the code got here, not what it does now;
the commit history and PRs carry that evolution. Comment-only, behavior unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rkennke
rkennke force-pushed the rkennke/profiler-all-native-context-phase2 branch from b1d70ef to 07a20b2 Compare July 23, 2026 19:51
@datadog-prod-us1-5

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Jul 23, 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 13.97 s 13.91 s [-0.1%; +1.0%] (no difference)
startup:insecure-bank:tracing:Agent 12.88 s 12.96 s [-1.4%; +0.1%] (no difference)
startup:petclinic:appsec:Agent 16.91 s 16.79 s [-0.2%; +1.6%] (no difference)
startup:petclinic:iast:Agent 16.91 s 16.92 s [-1.1%; +0.9%] (no difference)
startup:petclinic:profiling:Agent 16.73 s 16.71 s [-1.4%; +1.6%] (no difference)
startup:petclinic:sca:Agent 16.31 s 16.86 s [-7.5%; +1.0%] (no difference)
startup:petclinic:tracing:Agent 16.05 s 16.15 s [-1.5%; +0.2%] (no difference)

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

@rkennke
rkennke marked this pull request as ready for review July 24, 2026 06:37
@rkennke
rkennke requested a review from a team as a code owner July 24, 2026 06:37
@dd-octo-sts

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

@dd-octo-sts dd-octo-sts Bot added the tag: ai generated Largely based on code generated by an AI or LLM label Jul 24, 2026
AppContextSnapshotBenchmark.setup() still called the old
record(int, int, byte[], String) signature; AppContextSnapshot.record is now
record(int, String) (strings-only, per the all-native context migration).
compileJmhJava was failing in CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@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: 07a20b2f98

ℹ️ 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".

// Native call first; snapshot updated only on success so Java and ddprof state stay in
// sync.
if (contextSetter.setContextValue(offset, value)) {
if (profiler.setContextValue(offset, value)) {

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 app snapshots in sync when native writes clear a slot

When an app context attribute already has a snapshot, a later write whose value is rejected by the all-native API (for example UTF-8 >255 bytes, dictionary full, or attrs_data overflow) returns false after clearing the native slot. This branch then leaves appContextValues untouched, so the next reapplyAppContext() on span activation/clear writes the previous value back and profiles regain a stale custom attribute instead of observing that the new value was not applied.

Useful? React with 👍 / 👎.

log.debug("Failed to clear context", e);
log.debug("Failed to set trace context", e);
}
reapplyAppContext();

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 Preserve span-derived attributes over app context

When profiling.context.attributes contains _dd.trace.operation or _dd.trace.resource and the corresponding span/resource context attribute is enabled, that slot is also marked app-owned. The previous activation order set operation/resource after setSpanContext, so the span-derived values won; now setTraceContext writes them first and this reapply immediately overwrites them with the saved app value, causing profiles to be tagged with stale/custom operation/resource names for every span on that thread.

Useful? React with 👍 / 👎.

@datadog-prod-us1-5 datadog-prod-us1-5 Bot 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.

Datadog Autotest: FAIL

Replacing an existing profiling context value with a >255-byte string now clears the native slot but retains its Java snapshot; the next span activation resurrects the stale prior value. The DBB base path retains the prior value continuously, so samples can lose and later regain incorrect context across this migration.

📊 Validated against 10 scenarios · Open Bits AI session

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

Comment on lines +554 to 557
if (profiler.setContextValue(offset, value)) {
recordAppContextValue(offset, value);
return true;
}

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.

P2 Rejected values resurrect stale app context

Profiler samples can temporarily lose an application context attribute and then be labeled with its stale prior value after the next span activation.

Assertion details
  • Input: Set a configured app profiling attribute to a valid value, replace it with a 256-byte UTF-8 string, then activate a span.
  • Expected: A rejected replacement should leave the native slot and Java snapshot consistent; the pre-PR DBB path retains the prior value continuously.
  • Actual: ddprof 1.47.0 clears the native slot and returns false for the oversized replacement, but this branch keeps the prior Java snapshot. The slot changes from encoding 1 to 0, then setSpanContext reapplies the stale snapshot and changes it back to 1.

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

Suggested change
if (profiler.setContextValue(offset, value)) {
recordAppContextValue(offset, value);
return true;
}
if (profiler.setContextValue(offset, value)) {
recordAppContextValue(offset, value);
return true;
}
reapplyAppContext();

- setContextValue: a rejected native write (e.g. >255-byte UTF-8) clears the
  native slot but left the Java snapshot untouched, so the attribute read as
  unset until the next span boundary silently resurrected the stale prior
  value. Reapply immediately on rejection so the prior value stays visible
  continuously, matching pre-migration DBB behavior.
- setTraceContext: reapplyAppContext() ran unconditionally after the native
  call, so when profiling.context.attributes also names
  _dd.trace.operation/resource (with span-name/resource-name context
  enabled), the trailing reapply clobbered the span-derived value that
  setTraceContext just wrote to the same offset with a stale app-recorded
  one. reapplyAppContext now takes the operation/resource offsets to skip.

Both were flagged independently by Codex and Datadog Autotest PR review bots
on #11899, with a concrete repro for the first. Added regression coverage to
DatadogProfilerTest#testContextRegistration for both (verified each new
assertion fails without its corresponding fix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: profiling Profiling tag: ai generated Largely based on code generated by an AI or LLM type: bug fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant