Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fb5c4d2
docs(context): design note for all-native OTEL context storage
rkennke Jul 3, 2026
7f7ee99
feat(context): all-native OTEL context write JNI primitives (PROF-15271)
rkennke Jul 3, 2026
c51fd66
feat(context): all-native public write API + global value cache (PROF…
rkennke Jul 3, 2026
6424d55
refactor(context): deprecate the DirectByteBuffer context API (PROF-1…
rkennke Jul 3, 2026
a51c4f8
fix(context): native path self-initializes the LRS attrs_data entry (…
rkennke Jul 3, 2026
106df58
test(context): all-native context write API tests (PROF-15271)
rkennke Jul 3, 2026
ef767b7
docs(context): reference the preserved benchmark-experiment branch (P…
rkennke Jul 3, 2026
c4e784f
test(context): production perf-guard benchmark for the all-native cyc…
rkennke Jul 3, 2026
cc3705e
docs(context): add missing javadoc tags on new all-native context API
rkennke Jul 6, 2026
ddc946a
review: address pre-review findings on the all-native context API
rkennke Jul 7, 2026
7ee1b14
Merge branch 'main' into all-native-context-storage
rkennke Jul 7, 2026
aa4e7cb
Merge branch 'main' into all-native-context-storage
rkennke Jul 7, 2026
95351e6
review: address PR #631 review findings on the all-native context API
rkennke Jul 8, 2026
a4b8ea4
Merge remote-tracking branch 'origin/main' into all-native-context-st…
rkennke Jul 8, 2026
a71cac9
review: address kaahos findings on the all-native context API
rkennke Jul 8, 2026
6dd6efd
review: setContextValue0 preserves prior valid state (kaahos)
rkennke Jul 9, 2026
9908c58
feat(context): fail fast on illegal arguments in the all-native write…
rkennke Jul 9, 2026
5beb698
context: setContextValue0 publishes app context without a span
rkennke Jul 9, 2026
019e0f5
context: native copyContextTags read + phase 2 migration plan
rkennke Jul 9, 2026
0300aaf
context: add ContextSetter.size() (pure-Java slot count)
rkennke Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 311 additions & 0 deletions ddprof-lib/src/main/cpp/javaApi.cpp

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions ddprof-lib/src/main/cpp/profiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,9 @@ Error Profiler::start(Arguments &args, bool reset) {
}
_string_label_map.clearAll();
_context_value_map.clearAll();
// Signal the Java layer that context-value encodings have been reassigned so it can drop its
// process-wide ContextValueCache (consumed in JavaProfiler.execute after this start returns).
_context_value_dict_reset.store(true, std::memory_order_release);

// Reset call trace storage
if (!_omit_stacktraces) {
Expand Down
8 changes: 8 additions & 0 deletions ddprof-lib/src/main/cpp/profiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ class alignas(alignof(SpinLock)) Profiler {
StringDictionary _class_map{1};
StringDictionary _string_label_map{2};
StringDictionary _context_value_map{3};
// Set when a fresh start resets _context_value_map (clearAll), reassigning encodings. Consumed by
// the Java layer to drop its process-wide ContextValueCache so no stale encoding is reused. See
// JavaProfiler.execute / ContextValueCache.
std::atomic<bool> _context_value_dict_reset{false};
ThreadFilter _thread_filter;
CallTraceStorage _call_trace_storage;
FlightRecorder _jfr;
Expand Down Expand Up @@ -261,6 +265,10 @@ class alignas(alignof(SpinLock)) Profiler {
SharedLockGuard classMapSharedGuard() { return SharedLockGuard(&_class_map_lock); }
StringDictionary *stringLabelMap() { return &_string_label_map; }
StringDictionary *contextValueMap() { return &_context_value_map; }
// Atomically reads and clears the "context value dictionary was reset" flag (see the member).
bool consumeContextValueDictReset() {
return _context_value_dict_reset.exchange(false, std::memory_order_acq_rel);
}
u32 numContextAttributes() { return _num_context_attributes; }
ThreadFilter *threadFilter() { return &_thread_filter; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
import java.util.List;
import java.util.Set;

/**
* DirectByteBuffer context wrapper.
*
* @deprecated Superseded by {@link JavaProfiler#setTraceContext} /
* {@link JavaProfiler#setContextValue} (all-native). Removed in phase 3.
*/
@Deprecated
public class ContextSetter {

private final List<String> attributes;
Expand Down Expand Up @@ -51,7 +58,7 @@
* copying to prevent stale data from leaking to the caller.
* Use the no-arg {@link #snapshotTags()} overload to obtain a correctly sized array.
*/
public void snapshotTags(int[] snapshot) {

Check warning on line 61 in ddprof-lib/src/main/java/com/datadoghq/profiler/ContextSetter.java

View workflow job for this annotation

GitHub Actions / check-javadoc

no @param for snapshot
if (snapshot.length >= attributes.size()) {
profiler.copyTags(snapshot);
Arrays.fill(snapshot, attributes.size(), snapshot.length, 0);
Expand All @@ -62,6 +69,11 @@
return attributes.indexOf(attribute);
}

/** Number of (deduplicated, truncated) context attribute slots. Pure Java; no native/DBB read. */
public int size() {

Check warning on line 73 in ddprof-lib/src/main/java/com/datadoghq/profiler/ContextSetter.java

View workflow job for this annotation

GitHub Actions / check-javadoc

return attributes.size();
}

public boolean setContextValue(String attribute, String value) {
return setContextValue(offsetOf(attribute), value);
}
Expand All @@ -85,7 +97,7 @@
* slots are zeroed in both the sidecar and attrs_data views. Callers must not assume the record
* is unmodified when {@code false} is returned.
*/
public boolean setContextValuesByIdAndBytes(int[] constantIds, byte[][] utf8) {

Check warning on line 100 in ddprof-lib/src/main/java/com/datadoghq/profiler/ContextSetter.java

View workflow job for this annotation

GitHub Actions / check-javadoc

Check warning on line 100 in ddprof-lib/src/main/java/com/datadoghq/profiler/ContextSetter.java

View workflow job for this annotation

GitHub Actions / check-javadoc

no @param for utf8

Check warning on line 100 in ddprof-lib/src/main/java/com/datadoghq/profiler/ContextSetter.java

View workflow job for this annotation

GitHub Actions / check-javadoc

no @param for constantIds
return profiler.setContextAttributesByIdAndBytes(constantIds, utf8);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2026, Datadog, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.datadoghq.profiler;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReferenceArray;

/**
* Process-wide cache resolving an attribute value to its {@code (encoding, utf8)} pair for the
* all-native context write path, avoiding a JNI {@code registerConstant0} call on every write.
*
* <p>Within a single recording session the encoding is a <em>stable</em> ID: {@code registerConstant0}
* interns the value in the native Dictionary and returns the same ID for the life of that session.
* That stability is what makes a single shared, lock-free cache correct without per-thread copies:
* <ul>
* <li>Entries are immutable and published atomically (one array slot store), so a concurrent
* reader never sees a torn {@code (key, encoding, utf8)}.</li>
* <li>A miss just calls {@code registerConstant0} again — idempotent, returns the same encoding —
* and re-stores an equivalent entry; racing writers converge (last write wins, all correct).</li>
* <li>Direct-mapped by {@code value.hashCode()}; a hash collision evicts the previous value, which
* is simply re-resolved on its next use.</li>
* </ul>
*
* <p><b>Session boundary:</b> the encoding is stable only <em>within</em> a recording session. A
* fresh {@code start} (native {@code Profiler::start} with reset) calls
* {@code StringDictionary::clearAll()}, which resets the ID counter, so encodings from the previous
* session become stale. {@link JavaProfiler} therefore calls {@link #clear()} on every {@code start}
* command; a subsequent {@link #resolve} re-registers the value and re-caches its new encoding.
*
* <p>Replaces the per-{@link ThreadContext} value cache: in the all-native model there is no
* per-thread {@code ThreadContext} instance to host it, and a global cache avoids duplicating the
* table across every carrier / virtual thread.
*/
final class ContextValueCache {

/** Max UTF-8 byte length for a value (the OTEP attrs_data entry length field is one byte). */
static final int MAX_VALUE_BYTES = 255;

private static final int SIZE = 256;
private static final int MASK = SIZE - 1;

/** Immutable resolved value; published as a single atomic array-slot store. */
static final class Entry {
final String key;
final int encoding;
final byte[] utf8;

Entry(String key, int encoding, byte[] utf8) {
this.key = key;
this.encoding = encoding;
this.utf8 = utf8;
}
}

private final AtomicReferenceArray<Entry> table = new AtomicReferenceArray<>(SIZE);

/**
* Resolves {@code value} to its cached {@code (encoding, utf8)} pair, registering it on a miss.
*
* @return the entry, or {@code null} if the value cannot be represented — {@code null} input, a
* UTF-8 encoding longer than {@value #MAX_VALUE_BYTES} bytes, or a full native Dictionary
* (encoding {@code < 0}). Callers treat {@code null} as "no attribute" (skip / clear).
*/
Entry resolve(String value) {
if (value == null) {
return null;
}
int slot = value.hashCode() & MASK;
Entry e = table.get(slot);
if (e != null && value.equals(e.key)) {
return e; // hit — no JNI
}
// Miss: encode + validate size before touching the Dictionary (a rejected value must not
// create an orphan Dictionary entry).
byte[] utf8 = value.getBytes(StandardCharsets.UTF_8);
if (utf8.length > MAX_VALUE_BYTES) {
return null;
}
int encoding = ThreadContext.registerConstant0(value);
if (encoding < 0) {
return null; // Dictionary full
}
Entry ne = new Entry(value, encoding, utf8);
table.set(slot, ne); // benign race: converges on an equivalent entry
return ne;
}

/**
* Drops all cached entries. Called when a fresh recording session starts and the native
* Dictionary is reset, so stale encodings from the previous session are not reused. A concurrent
* {@link #resolve} racing this simply observes a miss and re-registers the value against the new
* Dictionary — correct, just an extra {@code registerConstant0} call.
*/
void clear() {
for (int i = 0; i < SIZE; i++) {
table.set(i, null);
}
}
}
Loading
Loading