Skip to content

Commit de12ae9

Browse files
jbachorikclaude
andcommitted
Add JFR tag encoding sidecar to ProfiledThread
Cache pre-computed Dictionary encodings and localRootSpanId in ProfiledThread so signal handlers read O(1) arrays instead of scanning attrs_data and probing the hash table on every sample. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4243a48 commit de12ae9

5 files changed

Lines changed: 40 additions & 48 deletions

File tree

ddprof-lib/src/main/cpp/context_api.cpp

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,13 @@ void ContextApi::setFull(u64 local_root_span_id, u64 span_id, u64 trace_id_high,
136136
// Write trace_id + span_id to OTEP record
137137
OtelContexts::set(trace_id_high, trace_id_low, span_id);
138138

139-
// Store local_root_span_id in ProfiledThread for O(1) read by signal handler
139+
// Store local_root_span_id as custom attribute at reserved index 0.
140+
// Readable by external OTEL profilers and decoded by writeCurrentContext.
141+
uint8_t lrs_bytes[8];
142+
OtelContexts::u64ToBytes(local_root_span_id, lrs_bytes);
143+
OtelContexts::setAttribute(LOCAL_ROOT_SPAN_ATTR_INDEX, (const char*)lrs_bytes, 8);
144+
145+
// Cache in sidecar for O(1) signal-handler reads
140146
thrd->setOtelLocalRootSpanId(local_root_span_id);
141147
} else {
142148
// Profiler mode: local_root_span_id maps to rootSpanId, trace_id_high ignored
@@ -185,7 +191,7 @@ bool ContextApi::get(u64& span_id, u64& root_span_id) {
185191
return false;
186192
}
187193

188-
// Read local_root_span_id from ProfiledThread (O(1), signal-safe)
194+
// Read local_root_span_id from sidecar (O(1), no attrs_data scan)
189195
root_span_id = 0;
190196
ProfiledThread* thrd = ProfiledThread::currentSignalSafe();
191197
if (thrd != nullptr) {
@@ -224,12 +230,11 @@ bool ContextApi::setAttribute(uint8_t key_index, const char* value, uint8_t valu
224230
initializeOtelTls(thrd);
225231
}
226232

227-
// Pre-register the value string in the Dictionary from this JNI thread.
228-
// The signal handler (writeCurrentContext) will later call bounded_lookup
229-
// to find the encoding — by pre-registering here, the signal handler
230-
// only does a read (no malloc), which is async-signal-safe.
231-
Profiler::instance()->contextValueMap()->bounded_lookup(
233+
// Pre-register the value string in the Dictionary and cache the encoding
234+
// in the sidecar for O(1) signal-handler reads (no hash lookup needed).
235+
u32 encoding = Profiler::instance()->contextValueMap()->bounded_lookup(
232236
value, value_len, 1 << 16);
237+
thrd->setOtelTagEncoding(key_index, encoding != INT_MAX ? encoding : 0);
233238

234239
return OtelContexts::setAttribute(key_index, value, value_len);
235240
} else {

ddprof-lib/src/main/cpp/context_api.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ class ContextApi {
151151

152152
static const int MAX_ATTRIBUTE_KEYS = 32;
153153

154+
// Reserved attribute index for local root span ID in OTEL attrs_data.
155+
// Stored as 8-byte big-endian value. Visible to external OTEL profilers.
156+
static const uint8_t LOCAL_ROOT_SPAN_ATTR_INDEX = 0;
157+
154158
private:
155159
static ContextStorageMode _mode;
156160
static bool _initialized;

ddprof-lib/src/main/cpp/flightRecorder.cpp

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
#include "callTraceHashTable.h"
1212
#include "context.h"
1313
#include "context_api.h"
14-
#include "otel_context.h"
1514
#include "counters.h"
1615
#include "dictionary.h"
1716
#include "flightRecorder.h"
@@ -1489,38 +1488,12 @@ void Recording::writeCurrentContext(Buffer *buf) {
14891488
size_t numAttrs = Profiler::instance()->numContextAttributes();
14901489

14911490
if (ContextApi::getMode() == CTX_STORAGE_OTEL) {
1492-
// In OTEL mode: decode attrs_data from the OTEP record only.
1493-
u32 tag_values[DD_TAGS_CAPACITY] = {};
1494-
1491+
// In OTEL mode: read pre-computed encodings from ProfiledThread sidecar.
1492+
// Encodings are cached by ContextApi::setAttribute() on the JNI thread —
1493+
// O(1) array read, no hash lookup or attrs_data scanning.
14951494
ProfiledThread* thrd = ProfiledThread::currentSignalSafe();
1496-
if (thrd != nullptr && thrd->isOtelContextInitialized()) {
1497-
OtelThreadContextRecord* record = thrd->getOtelContextRecord();
1498-
uint16_t attrs_size = 0;
1499-
const uint8_t* attrs = OtelContexts::readAttrsData(record, attrs_size);
1500-
if (attrs != nullptr && attrs_size > 0) {
1501-
uint16_t pos = 0;
1502-
while (pos + 2 <= attrs_size) {
1503-
uint8_t key_index = attrs[pos];
1504-
uint8_t val_len = attrs[pos + 1];
1505-
if (pos + 2 + val_len > attrs_size) break;
1506-
1507-
if (key_index < numAttrs) {
1508-
// Read-only lookup (size_limit=0 → for_insert=false → no malloc).
1509-
// O(1) amortized — hash table with 3-way associative rows.
1510-
// Values are pre-registered by ContextApi::setAttribute() from the JNI thread.
1511-
u32 encoding = Profiler::instance()->contextValueMap()->bounded_lookup(
1512-
(const char*)(attrs + pos + 2), val_len, 0);
1513-
if (encoding != INT_MAX) {
1514-
tag_values[key_index] = encoding;
1515-
}
1516-
}
1517-
pos += 2 + val_len;
1518-
}
1519-
}
1520-
}
1521-
15221495
for (size_t i = 0; i < numAttrs; i++) {
1523-
buf->putVar32(tag_values[i]);
1496+
buf->putVar32(thrd != nullptr ? thrd->getOtelTagEncoding(i) : 0);
15241497
}
15251498
} else {
15261499
// Profiler mode: read directly from Context.tags[]

ddprof-lib/src/main/cpp/thread.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "profiler.h"
55
#include "common.h"
66
#include "vmStructs.h"
7+
#include <cstring>
78
#include <time.h>
89

910
pthread_key_t ProfiledThread::_tls_key;
@@ -100,11 +101,14 @@ void ProfiledThread::releaseFromBuffer() {
100101
_filter_slot_id = -1;
101102
_unwind_failures.clear();
102103

104+
// Reset OTEL sidecar (before freeing the record, for signal safety)
105+
memset(_otel_tag_encodings, 0, sizeof(_otel_tag_encodings));
106+
_otel_local_root_span_id = 0;
107+
103108
// Free the OTEL context record (null first for signal safety)
104109
OtelThreadContextRecord* record = _otel_ctx_record;
105110
_otel_ctx_record = nullptr;
106111
_otel_ctx_initialized = false;
107-
_otel_local_root_span_id = 0;
108112
delete record;
109113

110114
// Put this ProfiledThread object back in the buffer for reuse

ddprof-lib/src/main/cpp/thread.h

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#ifndef _THREAD_H
77
#define _THREAD_H
88

9+
#include "context.h"
910
#include "os.h"
1011
#include "threadLocalData.h"
1112
#include "unwindStats.h"
@@ -74,11 +75,13 @@ class ProfiledThread : public ThreadLocalData {
7475
bool _crash_protection_active;
7576
Context* _ctx_tls_ptr;
7677
OtelThreadContextRecord* _otel_ctx_record;
77-
u64 _otel_local_root_span_id; // Cached local root span ID for OTEL mode (O(1) read)
78+
u32 _otel_tag_encodings[DD_TAGS_CAPACITY];
79+
u64 _otel_local_root_span_id;
7880

7981
ProfiledThread(int buffer_pos, int tid)
8082
: ThreadLocalData(), _pc(0), _sp(0), _span_id(0), _root_span_id(0), _crash_depth(0), _buffer_pos(buffer_pos), _tid(tid), _cpu_epoch(0),
81-
_wall_epoch(0), _call_trace_id(0), _recording_epoch(0), _misc_flags(0), _filter_slot_id(-1), _ctx_tls_initialized(false), _otel_ctx_initialized(false), _crash_protection_active(false), _ctx_tls_ptr(nullptr), _otel_ctx_record(nullptr), _otel_local_root_span_id(0) {};
83+
_wall_epoch(0), _call_trace_id(0), _recording_epoch(0), _misc_flags(0), _filter_slot_id(-1), _ctx_tls_initialized(false), _otel_ctx_initialized(false), _crash_protection_active(false), _ctx_tls_ptr(nullptr), _otel_ctx_record(nullptr),
84+
_otel_tag_encodings{}, _otel_local_root_span_id(0) {};
8285

8386
~ProfiledThread(); // Defined in thread.cpp (needs complete OtelThreadContextRecord type)
8487
void releaseFromBuffer();
@@ -208,13 +211,6 @@ class ProfiledThread : public ThreadLocalData {
208211
return _otel_ctx_record;
209212
}
210213

211-
inline void setOtelLocalRootSpanId(u64 id) {
212-
_otel_local_root_span_id = id;
213-
}
214-
215-
inline u64 getOtelLocalRootSpanId() {
216-
return _otel_local_root_span_id;
217-
}
218214

219215
// JavaThread status cache — avoids repeated vtable checks in VMThread::isJavaThread().
220216
// JVMTI ThreadStart only fires for application threads, not for JVM-internal
@@ -247,6 +243,16 @@ class ProfiledThread : public ThreadLocalData {
247243
inline bool isCrashProtectionActive() const { return _crash_protection_active; }
248244
inline void setCrashProtectionActive(bool active) { _crash_protection_active = active; }
249245

246+
// OTEL JFR tag encoding sidecar — populated by JNI thread, read by signal handler
247+
inline void setOtelTagEncoding(u32 idx, u32 val) {
248+
if (idx < DD_TAGS_CAPACITY) _otel_tag_encodings[idx] = val;
249+
}
250+
inline u32 getOtelTagEncoding(u32 idx) const {
251+
return idx < DD_TAGS_CAPACITY ? _otel_tag_encodings[idx] : 0;
252+
}
253+
inline void setOtelLocalRootSpanId(u64 id) { _otel_local_root_span_id = id; }
254+
inline u64 getOtelLocalRootSpanId() const { return _otel_local_root_span_id; }
255+
250256
private:
251257
// Atomic flag for signal handler reentrancy protection within the same thread
252258
// Must be atomic because a signal handler can interrupt normal execution mid-instruction,

0 commit comments

Comments
 (0)