Skip to content

Commit f9ea8f2

Browse files
authored
Fix calltrace_storage counters accumulating unboundedly across rotations (#427)
1 parent 32f2e72 commit f9ea8f2

6 files changed

Lines changed: 87 additions & 11 deletions

File tree

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,48 @@ CallTraceHashTable::~CallTraceHashTable() {
102102
}
103103

104104

105+
void CallTraceHashTable::decrementCounters() {
106+
#ifdef COUNTERS
107+
// Compute and decrement the global counters for everything in this table.
108+
// Must only be called after waitForAllRefCountsToClear() so there are no
109+
// concurrent writers and plain iteration is safe.
110+
// Use a set to deduplicate: put() may store the same CallTrace* pointer in
111+
// both a newer and an older table (when findCallTrace finds it in prev()),
112+
// but the counter was only incremented once, so we must only count it once.
113+
const size_t header_size = sizeof(CallTrace) - sizeof(ASGCT_CallFrame);
114+
long long freed_bytes = 0;
115+
long long freed_traces = 0;
116+
size_t estimated_entries = 0;
117+
for (LongHashTable *t = _table; t != nullptr; t = t->prev()) {
118+
estimated_entries += t->size();
119+
}
120+
std::unordered_set<CallTrace*> seen;
121+
seen.reserve(estimated_entries);
122+
for (LongHashTable *t = _table; t != nullptr; t = t->prev()) {
123+
u64 *keys = t->keys();
124+
CallTraceSample *values = t->values();
125+
u32 capacity = t->capacity();
126+
for (u32 slot = 0; slot < capacity; slot++) {
127+
if (keys[slot] != 0) {
128+
CallTrace *trace = values[slot].acquireTrace();
129+
if (trace != nullptr && trace != CallTraceSample::PREPARING) {
130+
if (seen.insert(trace).second) {
131+
freed_bytes += header_size + trace->num_frames * sizeof(ASGCT_CallFrame);
132+
freed_traces++;
133+
}
134+
}
135+
}
136+
}
137+
}
138+
Counters::increment(CALLTRACE_STORAGE_BYTES, -freed_bytes);
139+
Counters::increment(CALLTRACE_STORAGE_TRACES, -freed_traces);
140+
#endif // COUNTERS
141+
}
142+
105143
ChunkList CallTraceHashTable::clearTableOnly() {
106144
// Wait for all refcount guards to clear before detaching chunks
107145
RefCountGuard::waitForAllRefCountsToClear();
146+
decrementCounters();
108147

109148
// Clear previous chain pointers to prevent traversal during deallocation
110149
for (LongHashTable *table = _table; table != nullptr; table = table->prev()) {
@@ -424,7 +463,7 @@ void CallTraceHashTable::putWithExistingId(CallTrace* source_trace, u64 weight)
424463
table->values()[slot].setTrace(copied_trace);
425464
Counters::increment(CALLTRACE_STORAGE_BYTES, total_size);
426465
Counters::increment(CALLTRACE_STORAGE_TRACES);
427-
466+
428467
// Increment table size
429468
u32 new_size = table->incSize();
430469
probe.updateCapacity(new_size);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class CallTraceHashTable {
7272
CallTrace *storeCallTrace(int num_frames, ASGCT_CallFrame *frames,
7373
bool truncated, u64 trace_id);
7474
CallTrace *findCallTrace(LongHashTable *table, u64 hash);
75+
void decrementCounters();
7576

7677

7778
public:

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,9 @@ u64 Profiler::recordJVMTISample(u64 counter, int tid, jthread thread, jint event
453453
}
454454
u64 call_trace_id = 0;
455455
if (!_omit_stacktraces) {
456+
#ifdef COUNTERS
456457
u64 startTime = TSC::ticks();
458+
#endif // COUNTERS
457459
ASGCT_CallFrame *frames = _calltrace_buffer[lock_index]->_asgct_frames;
458460
jvmtiFrameInfo *jvmti_frames = _calltrace_buffer[lock_index]->_jvmti_frames;
459461

@@ -473,10 +475,12 @@ u64 Profiler::recordJVMTISample(u64 counter, int tid, jthread thread, jint event
473475
}
474476

475477
call_trace_id = _call_trace_storage.put(num_frames, frames, false, counter);
478+
#ifdef COUNTERS
476479
u64 duration = TSC::ticks() - startTime;
477480
if (duration > 0) {
478-
Counters::increment(UNWINDING_TIME_JVMTI, duration); // increment the JVMTI specific counter
481+
Counters::increment(UNWINDING_TIME_JVMTI, duration);
479482
}
483+
#endif // COUNTERS
480484
}
481485
if (!deferred) {
482486
_jfr.recordEvent(lock_index, tid, call_trace_id, event_type, event);
@@ -529,7 +533,9 @@ void Profiler::recordSample(void *ucontext, u64 counter, int tid,
529533
// call_trace_id determined to be reusable at a higher level
530534

531535
if (!_omit_stacktraces && call_trace_id == 0) {
536+
#ifdef COUNTERS
532537
u64 startTime = TSC::ticks();
538+
#endif // COUNTERS
533539
ASGCT_CallFrame *frames = _calltrace_buffer[lock_index]->_asgct_frames;
534540

535541
int num_frames = 0;
@@ -558,10 +564,12 @@ void Profiler::recordSample(void *ucontext, u64 counter, int tid,
558564
if (thread != nullptr) {
559565
thread->recordCallTraceId(call_trace_id);
560566
}
567+
#ifdef COUNTERS
561568
u64 duration = TSC::ticks() - startTime;
562569
if (duration > 0) {
563-
Counters::increment(UNWINDING_TIME_ASYNC, duration); // increment the async specific counter
570+
Counters::increment(UNWINDING_TIME_ASYNC, duration);
564571
}
572+
#endif // COUNTERS
565573
}
566574
_jfr.recordEvent(lock_index, tid, call_trace_id, event_type, event);
567575

ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,4 +475,26 @@ protected void verifyStackTraces(Path recording, String eventType, String... pat
475475
assertNotEquals(0, cumulatedEvents, "no events found for " + eventType);
476476
assertTrue(unmatched.isEmpty(), "couldn't find " + eventType + " with " + unmatched);
477477
}
478+
479+
/**
480+
* Returns the value of a named counter from {@code datadog.ProfilerCounter} events in the JFR
481+
* recording. These events are written before the final cleanup ({@code processTraces}), so they
482+
* capture the pre-cleanup state.
483+
*
484+
* @return the counter value, or -1 if no matching event is found
485+
*/
486+
public long getRecordedCounterValue(String counterName) {
487+
IItemCollection events = verifyEvents("datadog.ProfilerCounter", false);
488+
for (IItemIterable iterable : events) {
489+
IMemberAccessor<String, IItem> nameAccessor = NAME.getAccessor(iterable.getType());
490+
IMemberAccessor<IQuantity, IItem> countAccessor = COUNT.getAccessor(iterable.getType());
491+
if (nameAccessor == null || countAccessor == null) continue;
492+
for (IItem item : iterable) {
493+
if (counterName.equals(nameAccessor.getMember(item))) {
494+
return countAccessor.getMember(item).longValue();
495+
}
496+
}
497+
}
498+
return -1;
499+
}
478500
}

ddprof-test/src/test/java/com/datadoghq/profiler/cpu/ContextCpuTest.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,14 @@ public void test(@CStack String cstack) throws ExecutionException, InterruptedEx
106106
assertInRange(method2Weight / (double) totalWeight, 0.1, 0.6);
107107
assertInRange(method3Weight / (double) totalWeight, 0.05, 0.6);
108108
}
109+
// The recording captures counter values before the final cleanup (before processTraces
110+
// runs and frees all traces). Verify the recording contains meaningful data.
111+
assertInRange(getRecordedCounterValue("calltrace_storage_traces"), 1, 100);
112+
assertInRange(getRecordedCounterValue("calltrace_storage_bytes"), 1024, 8 * 1024 * 1024);
113+
// live counters are 0 after stop (all traces freed - correct, non-leaking behaviour)
109114
Map<String, Long> debugCounters = profiler.getDebugCounters();
110-
// these are here to verify these counters produce reasonable values so they can be used for memory leak detection
111-
assertInRange(debugCounters.get("calltrace_storage_traces"), 1, 100);
112-
assertInRange(debugCounters.get("calltrace_storage_bytes"), 1024, 8 * 1024 * 1024);
113-
// this allocator is only used for calltrace storage and eagerly allocates chunks of 8MiB
115+
assertEquals(0, debugCounters.get("calltrace_storage_traces"));
116+
assertEquals(0, debugCounters.get("calltrace_storage_bytes"));
114117
assertEquals(0, debugCounters.get("linear_allocator_bytes"));
115118
assertEquals(0, debugCounters.get("linear_allocator_chunks"));
116119
}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,14 @@ void test(AbstractProfilerTest test, boolean assertContext, String cstack) throw
196196
assertWeight("method2Impl", totalWeight, method2Weight, 0.33, allowedError);
197197
// method3 has as much self time as method1, and should account for half the executor's thread's time
198198
assertWeight("method3Impl", totalWeight, method3Weight, 0.33, allowedError);
199+
// The recording captures counter values before the final cleanup (before processTraces
200+
// runs and frees all traces). Verify the recording contains meaningful data.
201+
assertInRange(test.getRecordedCounterValue("calltrace_storage_traces"), 1, 100);
202+
assertInRange(test.getRecordedCounterValue("calltrace_storage_bytes"), 1024, 8 * 1024 * 1024);
203+
// live counters are 0 after stop (all traces freed - correct, non-leaking behaviour)
199204
Map<String, Long> debugCounters = profiler.getDebugCounters();
200-
// these are here to verify these counters produce reasonable values so they can be used for memory leak detection
201-
assertInRange(debugCounters.get("calltrace_storage_traces"), 1, 100);
202-
assertInRange(debugCounters.get("calltrace_storage_bytes"), 1024, 8 * 1024 * 1024);
203-
// this allocator is only used for calltrace storage and eagerly allocates chunks of 8MiB
205+
assertEquals(0, debugCounters.get("calltrace_storage_traces"));
206+
assertEquals(0, debugCounters.get("calltrace_storage_bytes"));
204207
assertEquals(0, debugCounters.get("linear_allocator_bytes"));
205208
assertEquals(0, debugCounters.get("linear_allocator_chunks"));
206209
assertInRange(debugCounters.get("thread_ids_count"), 1, 100);

0 commit comments

Comments
 (0)