Skip to content

Commit c89488c

Browse files
committed
Fix trace preservation across JFR dumps
1 parent 1223a7c commit c89488c

8 files changed

Lines changed: 84 additions & 11 deletions

File tree

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2025, Datadog, Inc.
2+
* Copyright 2025, 2026, Datadog, Inc.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -451,15 +451,34 @@ void CallTraceHashTable::putWithExistingId(CallTrace* source_trace, u64 weight)
451451

452452
u64 hash = calcHash(source_trace->num_frames, source_trace->frames, source_trace->truncated);
453453

454-
// First check if trace already exists in any table in the chain.
454+
// First check if this exact trace ID already exists in any table in the
455+
// chain. Different storage generations can assign different IDs to the same
456+
// stack. Both entries must survive preservation because recorded events
457+
// reference the ID, not the stack hash.
455458
// Use ACQUIRE to match the RELEASE store in clearTableOnly(); putWithExistingId()
456459
// is only called on scratch/standby tables with no concurrent writers, so the
457460
// load is safe, but consistent ordering prevents latent issues if callers change.
458461
for (LongHashTable *search_table = __atomic_load_n(&_table, __ATOMIC_ACQUIRE);
459462
search_table != nullptr; search_table = search_table->prev()) {
460-
CallTrace *existing_trace = findCallTrace(search_table, hash);
461-
if (existing_trace != nullptr) {
462-
return;
463+
u64 *search_keys = search_table->keys();
464+
HashProbe search_probe(hash, search_table->capacity());
465+
u32 search_slot = search_probe.slot();
466+
while (true) {
467+
u64 key = __atomic_load_n(&search_keys[search_slot], __ATOMIC_RELAXED);
468+
if (key == 0) {
469+
break;
470+
}
471+
if (key == hash) {
472+
CallTrace *existing_trace = search_table->values()[search_slot].acquireTrace();
473+
if (existing_trace != nullptr && existing_trace != CallTraceSample::PREPARING &&
474+
existing_trace->trace_id == source_trace->trace_id) {
475+
return;
476+
}
477+
}
478+
if (!search_probe.hasNext()) {
479+
break;
480+
}
481+
search_slot = search_probe.next();
463482
}
464483
}
465484

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) {
498498
jmethodID method_id = frame.method_id;
499499

500500
// Resolve native method
501-
if (FrameType::isRawPointer(bci)) {
501+
if (VM::isHotspot() && FrameType::isRawPointer(bci)) {
502502
method_id = JVMSupport::resolve(frame.method);
503503
}
504504

@@ -2107,6 +2107,7 @@ bool FlightRecorder::recordTaskBlock(int lock_index, int tid,
21072107
Recording* rec = _rec;
21082108
if (rec != nullptr) {
21092109
Buffer *buf = rec->buffer(lock_index);
2110+
rec->addThread(lock_index, tid);
21102111
rec->recordTaskBlock(buf, tid, event);
21112112
return true;
21122113
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,8 @@ void LivenessTracker::onGC() {
426426
}
427427

428428
void LivenessTracker::getLiveTraceIds(std::unordered_set<u64>& out_buffer) {
429-
out_buffer.clear();
430-
429+
// Liveness checkers share this buffer and must append their IDs. Clearing it
430+
// here would discard references contributed by checkers that ran earlier.
431431
if (!_enabled || !_initialized) {
432432
return;
433433
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021, 2025, Datadog, Inc.
2+
* Copyright 2021, 2026, Datadog, Inc.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -94,6 +94,12 @@ class alignas(alignof(SpinLock)) LivenessTracker {
9494
void track(JNIEnv *env, AllocEvent &event, jint tid, jobject object, u64 call_trace_id);
9595
void flush(std::set<int> &tracked_thread_ids);
9696

97+
#ifdef UNIT_TEST
98+
void getLiveTraceIdsForTest(std::unordered_set<u64>& out_buffer) {
99+
getLiveTraceIds(out_buffer);
100+
}
101+
#endif
102+
97103
static void JNICALL GarbageCollectionFinish(jvmtiEnv *jvmti_env);
98104

99105
private:

ddprof-lib/src/test/cpp/livenessTracker_ut.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include <gtest/gtest.h>
1818
#include "../../main/cpp/gtest_crash_handler.h"
19+
#include "../../main/cpp/livenessTracker.h"
1920
#include <cstdlib>
2021
#include <cstring>
2122

@@ -241,3 +242,12 @@ TEST_F(LivenessTrackerTest, CapacityDoesNotExceedMaxCap) {
241242
// In the actual code, this would trigger: if (_table_cap != newcap) { ... }
242243
// which would be false, so no resize would be attempted
243244
}
245+
246+
TEST_F(LivenessTrackerTest, LiveTraceCollectionPreservesIdsFromOtherCheckers) {
247+
LivenessTracker tracker;
248+
std::unordered_set<u64> trace_ids = {0x12345678ULL};
249+
250+
tracker.getLiveTraceIdsForTest(trace_ids);
251+
252+
EXPECT_EQ(trace_ids.count(0x12345678ULL), 1U);
253+
}

ddprof-lib/src/test/cpp/test_callTraceStorage.cpp

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2025, Datadog, Inc.
2+
* Copyright 2025, 2026, Datadog, Inc.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -727,6 +727,27 @@ TEST_F(CallTraceStorageTest, PutWithExistingIdNoInfiniteLoopWhenFull) {
727727
"when the scratch table was full";
728728
}
729729

730+
TEST_F(CallTraceStorageTest, PutWithExistingIdPreservesDuplicateStackIds) {
731+
CallTraceHashTable table;
732+
733+
alignas(alignof(CallTrace)) char first_buf[sizeof(CallTrace)];
734+
CallTrace* first = new (first_buf) CallTrace(false, 1, 0x100000001ULL);
735+
first->frames[0].bci = 17;
736+
first->frames[0].method_id = reinterpret_cast<jmethodID>(0x1234);
737+
738+
alignas(alignof(CallTrace)) char second_buf[sizeof(CallTrace)];
739+
CallTrace* second = new (second_buf) CallTrace(false, 1, 0x200000001ULL);
740+
second->frames[0] = first->frames[0];
741+
742+
table.putWithExistingId(first, 1);
743+
table.putWithExistingId(second, 1);
744+
745+
std::unordered_set<CallTrace*> traces;
746+
table.collect(traces);
747+
EXPECT_NE(findTraceById(traces, first->trace_id), nullptr);
748+
EXPECT_NE(findTraceById(traces, second->trace_id), nullptr);
749+
}
750+
730751
/**
731752
* Integration test: processTraces preserves live traces across rotation cycles
732753
* without hanging.

ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/JavaProfilerTaskBlockApiTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ public void taskBlockSpanningLiveDumpKeepsStackReference() throws Exception {
120120
assertTrue(recorded.get());
121121

122122
stopProfiler();
123-
TaskBlockAssertions.assertContainsStackTrace(verifyEvents("datadog.TaskBlock"));
123+
IItemCollection events = verifyEvents("datadog.TaskBlock");
124+
TaskBlockAssertions.assertContainsStackTrace(events);
125+
TaskBlockAssertions.assertContainsEventThread(events);
124126
}
125127

126128
@Override

ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/TaskBlockAssertions.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,20 @@ static void assertContainsStackTrace(IItemCollection taskBlockEvents) {
142142
assertTrue(checked > 0, "Expected at least one TaskBlock with a non-empty stackTrace");
143143
}
144144

145+
static void assertContainsEventThread(IItemCollection taskBlockEvents) {
146+
int checked = 0;
147+
for (IItemIterable iterable : taskBlockEvents) {
148+
IMemberAccessor<IMCThread, IItem> threadAccessor =
149+
JfrAttributes.EVENT_THREAD.getAccessor(iterable.getType());
150+
assertNotNull(threadAccessor, "TaskBlock must expose eventThread");
151+
for (IItem item : iterable) {
152+
checked++;
153+
assertNotNull(threadAccessor.getMember(item), "TaskBlock eventThread must not be null");
154+
}
155+
}
156+
assertTrue(checked > 0, "Expected at least one TaskBlock with an eventThread");
157+
}
158+
145159
static void assertContainsCorrelationId(IItemCollection taskBlockEvents) {
146160
Set<Long> correlationIds = nonZeroValues(taskBlockEvents, CORRELATION_ID);
147161
assertTrue(correlationIds.size() > 0, "Expected at least one non-zero TaskBlock correlationId");

0 commit comments

Comments
 (0)