Skip to content

feat(context): all-native OTEL context write API — Phase 1 (PROF-15271)#631

Merged
rkennke merged 20 commits into
mainfrom
all-native-context-storage
Jul 10, 2026
Merged

feat(context): all-native OTEL context write API — Phase 1 (PROF-15271)#631
rkennke merged 20 commits into
mainfrom
all-native-context-storage

Conversation

@rkennke

@rkennke rkennke commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 1 of eliminating the virtual-thread use-after-free in OTEL trace-context storage
(PROF-15271), by moving context writes to an all-native path that resolves the current
carrier's OTEP record inside the JNI call — where the mounted virtual thread is pinned to its
carrier by the native frame, so the write is race-free by construction with no cached
DirectByteBuffer to dangle.

This PR is purely additive: it adds the new API alongside the existing DirectByteBuffer
(DBB) path (now @Deprecated) and changes no behavior. The crash fix itself lands in Phase 2,
when dd-trace-java switches to the new API; until then carrier-scoping (#625 / 94686da) remains
the mitigation.

Full rationale, options considered (A–E), the JMH campaign, the consumer audit, and the
expand → migrate → contract plan are in the design note:
doc/plans/2026-07-02-all-native-context-storage-design.md.

What's in this PR

  • Native write primitives (javaApi.cpp): setTraceContext0/clearTraceContext0 (combined
    per-scope activate/deactivate) and setContextValue0/clearContextValue0 (single attribute),
    using the same detach → mutate → attach valid-flag protocol as ThreadContext so the
    async-signal-handler sampler (ContextApi::get) stays coherent. The native path self-initializes
    the LRS attrs_data entry (no dependency on the ThreadContext ctor).
  • Public API + global value cache (JavaProfiler, ContextValueCache): provisional names
    setTraceContext/clearTraceContext/setContextValue/clearContextValue. The
    value → (encoding, utf8) memoization moves out of the per-thread ThreadContext into a
    process-wide lock-free cache (safe because the encoding is a process-global immutable ID).
  • Deprecations: the DBB write/read surface and the ThreadContext/ScopeStack/ContextSetter
    helper classes are marked @Deprecated (non-breaking) pointing at the new API.
  • Tests (AllNativeContextTest, 9): round-trip, DBB-equivalence, clear semantics,
    span-transition attribute reset, single-attr set/clear, overflow, rejected values,
    DBB↔native coexistence, and coherence of native writes from 512 mounted virtual threads.
  • Perf-guard benchmark (ContextCombinedBenchmark).

Results

  • Tests: all 9 new tests pass on JDK 21 (debug); the existing context suite (Otel / Carrier /
    Tag / ProcessContext) is unregressed.

  • Perf (JDK 21, production per-scope activate+deactivate cycle, carrier mode): all-native
    ~136 ns vs DBB ~189 ns — ~28% faster (~24% thread mode), a win on virtual threads too, and
    mode-independent. The DBB baseline is the faithful production cost (each of its 6 calls pays a
    currentContext()/CarrierThreadLocal lookup the 2-call native cycle avoids).

  • Perf on old runtimes (Java 8 & 11): same ContextCombinedBenchmark and JMH config as the
    JDK 21 campaign (3 forks × 3×1s warmup × 3×3s measurement, AverageTime, one op = one full
    activate+deactivate cycle). Only platform_* / thread applies on 8/11 — carrier mode and the
    vthread_* benchmarks are JDK 21+ only (they need jdk.internal.misc.CarrierThreadLocal /
    Thread.startVirtualThread).

    JVM native (setTraceContext+clearTraceContext) dbb (setContext+2×attr, ×2) delta
    Java 8 (1.8.0_492) 110.31 ± 1.89 ns/op 252.53 ± 1.39 ns/op native −56.3% (dbb ~2.29×)
    Java 11 (11.0.31) 168.85 ± 1.68 ns/op 417.90 ± 11.45 ns/op native −59.6% (dbb ~2.47×)

    No degradation on old runtimes — the all-native cycle wins by ~2.3–2.5×, consistent with JDK 21.
    (Reliable signal is the native-vs-dbb ratio within each JVM; the cross-JVM absolute gap is
    noisier, as JMH itself warns.)

Migration (expand → migrate → contract)

  1. Expand — this PR (java-profiler): add the new API alongside the DBB path. Additive.
  2. Migrate (dd-trace-java): switch DatadogProfilingIntegration to the combined API. Crash
    fix + perf win land here.
  3. Contract (java-profiler): delete the DBB / CarrierThreadLocal / OtelContextStorage
    subsystem. Breaking, gated on all-consumers-migrated.

Reviewer notes

  • Public API names are provisional — the one Phase-1 item to coordinate with the dd-trace-java
    team so Phase 2 is a clean swap.
  • Signal-safety is the key review area: the native detach/attach + release-fence protocol vs
    the sampler's acquire load of valid (ContextApi::get). Verify on aarch64 (trivial on x86).
  • The full JMH campaign + native prototypes are preserved for reproducibility on branch
    context-storage-benchmark-experiment.
  • Consumer audit: the DBB API's only production consumer is the dd-trace-java bridge; everything
    else is test/benchmark. Open item: confirm ddprof-lib standalone Maven publication (external
    consumers) before the Phase-3 removal.

🤖 Generated with Claude Code

rkennke and others added 8 commits July 3, 2026 08:35
Captures the UAF problem, options A-E, the JMH benchmark campaign and its
governing principle, the combined-API design, the consumer audit, and the
expand -> migrate -> contract migration plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add native setTraceContext0/clearTraceContext0 (combined per-scope
activate/deactivate) and setContextValue0/clearContextValue0 (single attribute),
plus attrs_data compaction and LRS helpers, in javaApi.cpp, with matching private
native declarations on JavaProfiler.

Each resolves the current carrier's OtelThreadContextRecord via
ProfiledThread::current(). A JNI native frame pins a mounted virtual thread to its
carrier for the duration of the call, so the resolved record is guaranteed live and
cannot migrate mid-write -- there is no cached per-thread DirectByteBuffer to
dangle. Writes follow the same detach -> mutate -> attach valid-flag protocol as
ThreadContext, keeping the async-signal-handler sampler (ContextApi::get) coherent.

Native layer only; the public Java API and value cache built on top follow in a
later commit. The DirectByteBuffer path is unchanged.

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

Add the public all-native context API on JavaProfiler (provisional names, pending
dd-trace-java coordination): setTraceContext/clearTraceContext (combined per-scope
activate/deactivate) and setContextValue/clearContextValue (single attribute),
each backed by the native primitives.

Relocate the value->(encoding,utf8) memoization out of the per-thread ThreadContext
into a new process-wide ContextValueCache. Because the encoding is a process-global
immutable ID (registerConstant0), a shared lock-free cache is correct: immutable
entries published via a single AtomicReferenceArray slot store (no torn reads), a
miss re-registers idempotently, and collisions self-heal. This avoids duplicating
the table across every carrier/virtual thread and needs no CarrierThreadLocal.
registerConstant0 is made package-private for reuse (relocates in phase 3).

Coexists with the DirectByteBuffer path (both write the same native record); the
DBB path is unchanged.

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

Mark the DBB write/read surface `@Deprecated` pointing at the all-native API:
JavaProfiler.setContext/clearContext/setContextAttribute/clearContextAttribute/
setContextAttributesByIdAndBytes/copyTags/getThreadContext, and the DBB-path helper
classes ThreadContext, ScopeStack, and ContextSetter.

Non-breaking (annotations only) -- signals the migration for dd-trace-java (phase 2)
ahead of removal (phase 3). Java has no -Werror, so the deprecation notes are
informational; the DBB path still functions unchanged during coexistence.

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

setTraceContext0/clearTraceContext0 now write the fixed LRS entry header
(key_index=0, length=16) in addition to its hex value, instead of relying on the
ThreadContext constructor to have initialized it. This makes the all-native path
correct on a record that only saw the ProfiledThread zero-init -- i.e. when no
DirectByteBuffer/ThreadContext was ever created (the phase-2 pure-native case),
where attrs_data[1] would otherwise be 0 and mis-frame the LRS entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AllNativeContextTest exercises setTraceContext/clearTraceContext/setContextValue/
clearContextValue: round-trip readback, equivalence with the DirectByteBuffer path,
clear semantics, span-transition attribute reset, single-attribute set/clear,
attrs_data overflow, rejected (null/oversized) values, DBB<->native coexistence on
one thread, and coherence of native writes from many mounted virtual threads (the
migration-safe path).

Read-back uses the ThreadContext DBB read methods as an oracle -- a view over the
same native record the all-native path writes. All 9 pass on JDK 21 (debug); the
existing context suite (Otel/Carrier/Tag/ProcessContext) is unregressed.

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

Point the design note appendix at branch context-storage-benchmark-experiment
(pushed to origin), which preserves the full JMH campaign + native prototypes for
reproducibility, rather than describing scaffolding that lives only on that branch.

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

ContextCombinedBenchmark compares the production per-scope activate+deactivate cycle:
the combined native calls (setTraceContext + clearTraceContext) vs the deprecated
DirectByteBuffer sequence as dd-trace-java actually drives it (profiler.setContext +
2x setContextAttribute + clear...), on platform + mounted virtual threads, both storage
modes.

Measured (JDK 21, debug, carrier): native ~136 ns vs DBB ~189 ns -- native ~28% faster,
mode-independent, and a win on vthreads too (~147 vs ~193 ns). The DBB baseline here is
the faithful production cost: each of its 6 calls pays a currentContext()/
CarrierThreadLocal lookup, which the 2-call combined native cycle avoids.

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

dd-octo-sts Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #29021930572 | Commit: 4a55e21 | Duration: 17m 3s (longest job)

All 32 test jobs passed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - - -
8-ibm - - -
8-j9 - -
8-librca - -
8-orcl - - -
11 - - -
11-j9 - -
11-librca - -
17 - -
17-graal - -
17-j9 - -
17-librca - -
21 - -
21-graal - -
21-librca - -
25 - -
25-graal - -
25-librca - -

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

Summary: Total: 32 | Passed: 32 | Failed: 0


Updated: 2026-07-09 13:56:02 UTC

Fixes check-javadoc "no comment" warnings for setTraceContext,
setContextValue, and clearContextValue introduced in this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Jul 6, 2026

Copy link
Copy Markdown

Pipelines

Fix all issues with BitsAI

⚠️ Warnings

🚦 2 Pipeline jobs failed

DataDog/java-profiler | reliability-chaos-aarch64: [profiler, tcmalloc, 21.0.3-tem]   View in Datadog   GitLab

DataDog/java-profiler | reliability-chaos-amd64: [profiler, tcmalloc, 21.0.3-tem]   View in Datadog   GitLab

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 0300aaf | Docs | Datadog PR Page | Give us feedback!

rkennke and others added 3 commits July 7, 2026 13:45
- Convert `// @deprecated` line comments into proper `@deprecated` javadoc
  tags on the deprecated JavaProfiler methods and the ThreadContext /
  ScopeStack / ContextSetter classes, so the rationale renders in javadoc
  and `-Xlint` stops warning about the missing tag.
- Reject out-of-range slots (`slot >= MAX_CONTEXT_SLOTS`) in setContextValue
  and resolveContextValue before resolving, so an invalid slot no longer
  registers an orphan entry in the permanent native Dictionary (matches the
  DBB ThreadContext.setContextAttribute guard and the existing javadoc).
- Reconcile the perf figures in the design note: explain that ~7-10% is the
  prototype microbench baseline and ~28% is the production ContextCombinedBenchmark
  baseline (six real currentContext()-resolving calls), so they no longer read
  as contradictory.
- Clarify the otelWriteLrsEntry comment: only setTraceContext0/clearTraceContext0
  establish the LRS entry; setContextValue0 does not, which is harmless.
- Add an assert(len <= 255) in otelReadUtf8 documenting the caller contract,
  keeping the clamp as release-build defense.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rkennke rkennke marked this pull request as ready for review July 7, 2026 15:31
@rkennke rkennke requested a review from a team as a code owner July 7, 2026 15:31

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

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

Comment thread ddprof-lib/src/main/cpp/javaApi.cpp
Comment thread ddprof-lib/src/main/cpp/javaApi.cpp
Comment thread ddprof-lib/src/main/java/com/datadoghq/profiler/ContextValueCache.java Outdated
Comment thread ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java Outdated
Comment thread ddprof-lib/src/main/cpp/javaApi.cpp
Comment thread ddprof-lib/src/main/cpp/javaApi.cpp
Comment thread ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java Outdated
- P1 (Codex/jbachorik): the all-native write entry points never initialized
  OTEL TLS, so a thread that used only setTraceContext/setContextValue wrote a
  record the sampler (ContextApi::get / wallClock) ignored and eBPF could not
  discover. setTraceContext0 / setContextValue0 now call
  ContextApi::initializeContextTLS(thrd) on first use, matching the DBB path.
  Added AllNativeContextSamplingTest: a wall-clock JFR test on a thread that
  never calls getThreadContext(), asserting a sample carries the natively
  written context (fails if the record is not published to the sampler).

- jbachorik: ContextValueCache assumed process-lifetime-immutable encodings,
  but a fresh start resets the native Dictionary (clearAll), reassigning IDs.
  Added ContextValueCache.clear(), invoked from execute() on a 'start' action,
  and corrected the class javadoc invariant.

- jbachorik: MAX_CONTEXT_SLOTS duplicated native DD_TAGS_CAPACITY with only a
  comment binding them. Added native accessor maxContextSlots0() and
  MaxContextSlotsTest to catch drift from DD_TAGS_CAPACITY /
  ThreadContext.MAX_CUSTOM_SLOTS at test time.

- jbachorik nits: added tests for the native null byte[] guard in otelReadUtf8,
  the otelCompactAttr 2-byte (zero-length value) boundary, and the
  setContextValue slot == MAX_CONTEXT_SLOTS boundary.

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

@kaahos kaahos 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.

really nice changes, thanks! I only have few minor questions you can find below.

Comment thread ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java
Comment thread ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java Outdated
Comment thread ddprof-lib/src/main/cpp/javaApi.cpp Outdated
rkennke and others added 2 commits July 8, 2026 20:10
- #1/#2 (ContextValueCache invalidation): replace the Java command-string
  parsing (isStartAction) — which didn't match arguments.cpp's tokenization —
  with a native, authoritative signal. Profiler::start sets a
  _context_value_dict_reset flag in the reset block where
  _context_value_map.clearAll() runs (reached only for ACTION_START), and
  JavaProfiler.execute consumes it via consumeContextDictionaryReset0() to drop
  the cache. No command re-parsing in Java. This does not fully close the
  concurrent race window (resolve() during the reset), which is no worse than
  the deprecated DirectByteBuffer path; fully closing it is tracked as a
  follow-up (see PR discussion / Jira).

- #3 (clearContextValue0 validity): capture and restore the prior valid flag
  instead of unconditionally re-attaching with valid=1, so clearing one
  attribute cannot resurrect a record that clearTraceContext0 intentionally
  left deactivated. Added AllNativeContextTest.clearContextValuePreservesInvalidState.

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

dd-octo-sts Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Reliability & Chaos Results

1 failure(s) detected Pipeline: https://gitlab.ddbuild.io/DataDog/java-profiler/-/pipelines/123758963

❌ chaos: profiler tcmalloc amd64 21 0 3 temXchaos
Chaos harness crashed (RC=124)
siginfo: si_signo: 11 (SIGSEGV), si_code: 1 (SEGV_MAPERR), si_addr: 0x0000000000000000
# C  [libjavaProfiler-dd-tmp4956539198133605165.so+0x207c6]  CTimer::signalHandler(int, siginfo_t*, void*)+0x26

Comment thread ddprof-lib/src/main/cpp/javaApi.cpp
Apply the same guard as clearContextValue0: capture the record's valid flag up
front and restore it instead of hardcoding valid=1. Writing a single attribute
must not resurrect a record that clearTraceContext0 intentionally deactivated —
republishing it would expose a span-less record (zero trace/span) as valid.
Mirrors the DBB path's guard in ThreadContext.setContextAttributesByIdAndBytes
("never resurrect a cleared, span-less record").

Added AllNativeContextTest.setContextValuePreservesInvalidState.

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

@kaahos kaahos 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.

Thanks for the modifications! it looks good to me.

… API

Distinguish caller programming errors (fail loud) from runtime/data conditions
(soft signal), instead of silently swallowing the former:

- setTraceContext rejects spanId==0 with IllegalArgumentException — it is the
  activation path and requires a real span; clearing is clearTraceContext. This
  drops the DBB path's conflation of setContext(0,0,0,0) with a clear and never
  publishes a span-less record. The native setTraceContext0 primitive asserts
  spanId!=0 as debug-only defense (the wrapper validates first).
- Out-of-range custom-attribute slots now throw IllegalArgumentException in
  setTraceContext (non-negative slot >= MAX_CONTEXT_SLOTS; negative stays the
  documented skip sentinel), setContextValue, and clearContextValue — instead of
  being silently skipped / returning false / no-op'ing.

Data conditions stay soft (not exceptions), matching normal control flow: a null
or oversized value, or a full Dictionary, still returns false (setContextValue)
or skips (setTraceContext). Native keeps its hard slot range guards (load-bearing
for memory safety and to defend direct-JNI calls).

Tests: added setTraceContextRejectsZeroSpanId; expanded slotBoundaryIsRejected to
assert IllegalArgumentException across setContextValue/clearContextValue/
setTraceContext and that a negative activation slot still skips without throwing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rkennke rkennke requested a review from jbachorik July 9, 2026 11:14
Reverse the earlier "preserve prior valid" change on setContextValue0. Setting a
value means "make it visible": an attribute must be observable to the sampler
even when no span is active (zero trace/span). This is dd-trace-java's
app-context-independent-of-span model (PR #11646) — app-owned attributes such as
http.route, and the reapply-after-deactivation path, must stay visible between
spans. It mirrors the DBB single-attribute setter
(ThreadContext.setContextAttributeDirect), which always re-attaches; the earlier
change had wrongly modeled it on the *bulk* setter's valid==0 guard.

clearContextValue0 keeps preserving the prior valid flag: removing a value must
not resurrect a deactivated record. So the asymmetry is intentional and matches
the DBB single-vs-bulk behavior the tracer's reapply logic was built around:
set publishes, clear does not.

Test renamed/rewritten: setContextValuePublishesAppContextWithoutSpan asserts a
value set after clearTraceContext republishes (valid=1, span=0, value observable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rkennke rkennke requested a review from kaahos July 9, 2026 11:48
@rkennke

rkennke commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@kaahos thanks for your review. I needed to partly revert the last changes to preserve the (correct) behaviour of the DBB implementation (see 5beb698), and I also tightened the argument validation (see 9908c58). Can you re-check those?

@jbachorik jbachorik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks really good.
Could you, for peace of my mind, run the benchmarks also on Java 8 and 11 and add the numbers to the PR?
I expect maybe slight degradation, but we won't try and optimize for old runtimes anyway ...

Comment thread doc/plans/2026-07-02-all-native-context-storage-design.md
rkennke and others added 2 commits July 9, 2026 14:52
Add JavaProfiler.copyContextTags(int[]) + native copyContextTags0: reads the
current thread's custom-attribute sidecar tag encodings directly from the record
(ProfiledThread::current()), with no ThreadContext / DirectByteBuffer. Unlike the
deprecated DBB copyTags — which goes through ThreadContext, whose ctor resets the
record — this observes encodings written via the all-native setContextValue path
and does not clobber them. This is the read the dd-trace-java Phase 2 migration
needs for its (test-only) snapshot() oracle once writes go all-native.

Also adds the Phase 2 plan doc (dd-trace-java migration onto the all-native API).

Test: AllNativeContextTest.copyContextTagsReadsNativeEncodings (never calls
getThreadContext, proving the native read observes native writes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dd-trace-java Phase 2 bridge sizes its app-offset arrays from the context
attribute count. It previously read that as snapshotTags().length — a DBB read
that creates a ThreadContext. Expose size() (attributes.size(), pure Java) so the
bridge can size without touching the DBB path; offsetOf + size are the only
ContextSetter methods it still uses.

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

rkennke commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@jbachorik ran the ContextCombinedBenchmark perf-guard on Java 8 and 11 as requested — same JMH config as the JDK 21 campaign (3 forks × 3×1s warmup × 3×3s measurement, AverageTime ns/op, one op = one full activate+deactivate cycle). Good news: no degradation on the old runtimes — the all-native path is a decisive win over the DBB path.

Only the platform_* methods and thread storage mode are applicable on 8/11:

  • vthread_* benchmarks are JDK 21+ only (Thread.startVirtualThread).
  • carrier storage mode is JDK 21+ only — it requires jdk.internal.misc.CarrierThreadLocal (Loom), which doesn't exist on 8/11, so it correctly errors out there (N/A, not a regression).

thread mode, ns/op (± 99.9% CI), 9 measurements each:

JVM native (setTraceContext+clearTraceContext) dbb (setContext+2×attr, ×2) delta
Java 8 (1.8.0_492) 110.31 ± 1.89 252.53 ± 1.39 native −56.3% (dbb ~2.29×)
Java 11 (11.0.31) 168.85 ± 1.68 417.90 ± 11.45 native −59.6% (dbb ~2.47×)

So the combined 2-JNI-call cycle beats the 6-call DBB sequence by ~2.3–2.5× on both — consistent with the JDK 21 result, no slight degradation observed. (Caveat: the reliable signal is the native-vs-dbb ratio within each JVM; the absolute 8-vs-11 gap is cross-JVM and noisier, as JMH itself warns.)

@jbachorik

Copy link
Copy Markdown
Collaborator

@rkennke Cool, thanks!

@rkennke rkennke merged commit 4011bf7 into main Jul 10, 2026
156 of 159 checks passed
@rkennke rkennke deleted the all-native-context-storage branch July 10, 2026 09:32
@github-actions github-actions Bot added this to the 1.47.0 milestone Jul 10, 2026
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