Skip to content

Commit 9fe9b40

Browse files
jbachorikclaude
andcommitted
Optimize liveness tracking with indexed call trace lookup and thread-safe data structures
This commit addresses critical performance and concurrency issues in the liveness tracking system: ## Race Condition Fixes - Fix race condition in LivenessTracker::countCallTraceReferences() by using exclusive locks instead of shared locks during concurrent write operations - Prevent reading partially-written tracking table entries during call trace reference counting ## Architecture Improvements - Extract TrackingTable class from LivenessTracker for better separation of concerns - Enable dependency injection and improved testability - Add comprehensive thread safety tests for concurrent operations ## Performance Optimizations - Replace O(N*M) call trace lookup complexity with O(N+M) using indexed approach - Implement incremental index maintenance during CallTraceStorage::put() operations - Eliminate expensive index rebuilding on every collection cycle - Add lock-striped ConcurrentHashTable providing 64-way parallelism with cache-friendly design ## Thread-Safe Data Structures - Create reusable ConcurrentHashTable template class inspired by Dictionary design - Use lock striping for minimal cache line contention and optimal L2/L3 cache performance - Support both exclusive writes and shared reads for optimal concurrent access patterns - Implement atomic operations and proper memory barriers for correctness ## Memory Management - Maintain call trace index incrementally during insertion (O(1) per operation) - Avoid std::map allocations in hot paths using callback-based APIs - Fixed-size data structures prevent memory fragmentation issues ## Testing - Add thread safety test for TrackingTable concurrent access - Add CallTraceStorage liveness reference preservation test - All existing tests pass with improved performance characteristics This optimization is critical for handling up to 256k liveness tracking entries efficiently while maintaining correctness under high concurrent load. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 45573d9 commit 9fe9b40

10 files changed

Lines changed: 739 additions & 160 deletions

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

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class LongHashTable {
7474

7575
CallTrace CallTraceStorage::_overflow_trace = {false, 1, {BCI_ERROR, LP64_ONLY(0 COMMA) (jmethodID)"storage_overflow"}};
7676

77-
CallTraceStorage::CallTraceStorage() : _allocator(CALL_TRACE_CHUNK), _lock(0) {
77+
CallTraceStorage::CallTraceStorage() : _allocator(CALL_TRACE_CHUNK), _lock(0), _call_trace_index() {
7878
_current_table = LongHashTable::allocate(NULL, INITIAL_CAPACITY);
7979
_overflow = 0;
8080
}
@@ -93,12 +93,18 @@ void CallTraceStorage::clear() {
9393
_current_table->clear();
9494
_allocator.clear();
9595
_overflow = 0;
96+
_call_trace_index.clear(); // Clear the concurrent hash table
9697
Counters::set(CALLTRACE_STORAGE_BYTES, 0);
9798
Counters::set(CALLTRACE_STORAGE_TRACES, 0);
9899
_lock.unlock();
99100
}
100101

101102
void CallTraceStorage::collectTraces(std::map<u32, CallTrace *> &map) {
103+
std::map<u32, u32> empty_liveness_counts;
104+
collectTraces(map, empty_liveness_counts);
105+
}
106+
107+
void CallTraceStorage::collectTraces(std::map<u32, CallTrace *> &map, const std::map<u32, u32> &liveness_counts) {
102108
for (LongHashTable *table = _current_table; table != NULL;
103109
table = table->prev()) {
104110
u64 *keys = table->keys();
@@ -107,12 +113,60 @@ void CallTraceStorage::collectTraces(std::map<u32, CallTrace *> &map) {
107113

108114
for (u32 slot = 0; slot < capacity; slot++) {
109115
if (keys[slot] != 0 && loadAcquire(values[slot].samples) != 0) {
110-
// Reset samples to avoid duplication of call traces between JFR chunks
111-
values[slot].samples = 0;
116+
u32 call_trace_id = capacity - (INITIAL_CAPACITY - 1) + slot;
117+
118+
// Instead of setting samples = 0, set it to the number of liveness events
119+
// that still reference this call trace
120+
auto liveness_it = liveness_counts.find(call_trace_id);
121+
u32 liveness_ref_count = (liveness_it != liveness_counts.end()) ? liveness_it->second : 0;
122+
values[slot].samples = liveness_ref_count;
123+
124+
CallTrace *trace = values[slot].acquireTrace();
125+
if (trace != NULL) {
126+
map[call_trace_id] = trace;
127+
}
128+
}
129+
}
130+
}
131+
if (_overflow > 0) {
132+
map[OVERFLOW_TRACE_ID] = &_overflow_trace;
133+
}
134+
}
135+
136+
137+
void CallTraceStorage::markLivenessReferencesIndexed(const std::map<u32, u32> &liveness_counts) {
138+
// Phase 1 optimization: Use index for O(1) call trace lookups
139+
for (const auto& entry : liveness_counts) {
140+
u32 call_trace_id = entry.first;
141+
u32 ref_count = entry.second;
142+
143+
// O(1) concurrent hash table lookup
144+
CallTraceLocation location;
145+
if (_call_trace_index.get(call_trace_id, location)) {
146+
CallTraceSample *values = location.table->values();
147+
storeRelease(values[location.slot].samples, ref_count);
148+
}
149+
}
150+
}
151+
152+
void CallTraceStorage::collectTracesWithMarkedLiveness(std::map<u32, CallTrace *> &map) {
153+
// Phase 1 optimization: Collect traces that were already marked with liveness counts
154+
// No lookups needed during this phase - liveness counts are already stored
155+
for (LongHashTable *table = _current_table; table != NULL; table = table->prev()) {
156+
u64 *keys = table->keys();
157+
CallTraceSample *values = table->values();
158+
u32 capacity = table->capacity();
159+
160+
for (u32 slot = 0; slot < capacity; slot++) {
161+
if (keys[slot] != 0 && loadAcquire(values[slot].samples) != 0) {
162+
u32 call_trace_id = capacity - (INITIAL_CAPACITY - 1) + slot;
163+
112164
CallTrace *trace = values[slot].acquireTrace();
113165
if (trace != NULL) {
114-
map[capacity - (INITIAL_CAPACITY - 1) + slot] = trace;
166+
map[call_trace_id] = trace;
115167
}
168+
169+
// Note: samples count is already set to liveness reference count by markLivenessReferences
116170
}
117171
}
118172
}
@@ -214,6 +268,9 @@ u32 CallTraceStorage::put(int num_frames, ASGCT_CallFrame *frames,
214268
while (true) {
215269
u64 key_value = __atomic_load_n(&keys[slot], __ATOMIC_RELAXED);
216270
if (key_value == hash) { // Hash matches, exit the loop
271+
// Update index for existing call trace
272+
u32 call_trace_id = capacity - (INITIAL_CAPACITY - 1) + slot;
273+
_call_trace_index.put(call_trace_id, CallTraceLocation(table, slot));
217274
break;
218275
}
219276
if (key_value == 0) {
@@ -243,6 +300,11 @@ u32 CallTraceStorage::put(int num_frames, ASGCT_CallFrame *frames,
243300
if (prev_table != NULL) {
244301
prev_table->keys()[slot] = 0;
245302
}
303+
304+
// Maintain call trace index incrementally during insertion
305+
u32 call_trace_id = capacity - (INITIAL_CAPACITY - 1) + slot;
306+
_call_trace_index.put(call_trace_id, CallTraceLocation(table, slot));
307+
246308
break;
247309
}
248310

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#define _CALLTRACESTORAGE_H
1919

2020
#include "arch_dd.h"
21+
#include "concurrentHashTable.h"
2122
#include "linearAllocator.h"
2223
#include "spinLock.h"
2324
#include "vmEntry.h"
@@ -78,8 +79,26 @@ class CallTraceStorage {
7879

7980
void clear();
8081
void collectTraces(std::map<u32, CallTrace *> &map);
82+
void collectTraces(std::map<u32, CallTrace *> &map, const std::map<u32, u32> &liveness_counts);
83+
84+
// Phase 1 optimization: Index-based liveness marking for O(1) lookups
85+
void markLivenessReferencesIndexed(const std::map<u32, u32> &liveness_counts);
86+
void collectTracesWithMarkedLiveness(std::map<u32, CallTrace *> &map);
8187

8288
u32 put(int num_frames, ASGCT_CallFrame *frames, bool truncated, u64 weight);
89+
90+
private:
91+
// Index: call_trace_id -> (table, slot) for O(1) marking
92+
struct CallTraceLocation {
93+
LongHashTable* table;
94+
u32 slot;
95+
96+
CallTraceLocation() : table(nullptr), slot(0) {}
97+
CallTraceLocation(LongHashTable* t, u32 s) : table(t), slot(s) {}
98+
};
99+
100+
// Thread-safe concurrent hash table for call trace index
101+
ConcurrentHashTable<u32, CallTraceLocation> _call_trace_index;
83102
};
84103

85104
#endif // _CALLTRACESTORAGE
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2025 Datadog, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef _CONCURRENTHASHTABLE_H
18+
#define _CONCURRENTHASHTABLE_H
19+
20+
#include "arch_dd.h"
21+
#include "spinLock.h"
22+
23+
#define STRIPE_BITS 6
24+
#define STRIPES (1 << STRIPE_BITS)
25+
#define STRIPE_CELLS 4
26+
#define STRIPE_MASK (STRIPES - 1)
27+
28+
// Lock-striped concurrent hash table inspired by Dictionary design
29+
// Provides true N-way parallelism with independent stripe locks
30+
template<typename K, typename V>
31+
class ConcurrentHashTable {
32+
private:
33+
struct StripeEntry {
34+
K key;
35+
V value;
36+
37+
StripeEntry() : key(0), value() {}
38+
};
39+
40+
struct Stripe {
41+
SpinLock lock; // Each stripe has its own lock for N-way parallelism
42+
StripeEntry entries[STRIPE_CELLS];
43+
};
44+
45+
Stripe _stripes[STRIPES];
46+
47+
static u32 hash(K key) {
48+
// Simple hash function for integer keys
49+
u32 h = (u32)key;
50+
h ^= h >> 16;
51+
h *= 0x85ebca6b;
52+
h ^= h >> 13;
53+
h *= 0xc2b2ae35;
54+
h ^= h >> 16;
55+
return h;
56+
}
57+
58+
public:
59+
ConcurrentHashTable() {
60+
clear();
61+
}
62+
63+
// Thread-safe insert/update - uses lock striping for N-way parallelism
64+
void put(K key, const V& value) {
65+
if (key == 0) return; // Reserve 0 as empty marker
66+
67+
u32 h = hash(key);
68+
u32 stripe_idx = h & STRIPE_MASK;
69+
Stripe& stripe = _stripes[stripe_idx];
70+
71+
// Lock this stripe - other stripes remain accessible
72+
stripe.lock.lock();
73+
74+
// Try to find existing entry or empty slot in this stripe
75+
for (u32 i = 0; i < STRIPE_CELLS; i++) {
76+
if (stripe.entries[i].key == key) {
77+
// Key exists, update value
78+
stripe.entries[i].value = value;
79+
stripe.lock.unlock();
80+
return;
81+
}
82+
83+
if (stripe.entries[i].key == 0) {
84+
// Empty slot, claim it
85+
stripe.entries[i].key = key;
86+
stripe.entries[i].value = value;
87+
stripe.lock.unlock();
88+
return;
89+
}
90+
}
91+
92+
stripe.lock.unlock();
93+
// Stripe full - drop entry (similar to Dictionary when row is full)
94+
}
95+
96+
// Thread-safe lookup with shared locking for better read concurrency
97+
bool get(K key, V& value) {
98+
if (key == 0) return false;
99+
100+
u32 h = hash(key);
101+
u32 stripe_idx = h & STRIPE_MASK;
102+
Stripe& stripe = _stripes[stripe_idx];
103+
104+
// Use shared lock for better read concurrency
105+
stripe.lock.lockShared();
106+
107+
for (u32 i = 0; i < STRIPE_CELLS; i++) {
108+
if (stripe.entries[i].key == key) {
109+
value = stripe.entries[i].value;
110+
stripe.lock.unlockShared();
111+
return true;
112+
}
113+
114+
if (stripe.entries[i].key == 0) {
115+
stripe.lock.unlockShared();
116+
return false; // Empty slot, key not found
117+
}
118+
}
119+
120+
stripe.lock.unlockShared();
121+
return false; // Not found in this stripe
122+
}
123+
124+
// Clear all entries (not thread-safe with put/get operations)
125+
void clear() {
126+
for (u32 s = 0; s < STRIPES; s++) {
127+
for (u32 i = 0; i < STRIPE_CELLS; i++) {
128+
_stripes[s].entries[i].key = 0;
129+
_stripes[s].entries[i].value = V{};
130+
}
131+
}
132+
}
133+
134+
u32 capacity() const { return STRIPES * STRIPE_CELLS; }
135+
};
136+
137+
#endif // _CONCURRENTHASHTABLE_H

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
* Copyright The async-profiler authors
3+
* Copyright 2025 Datadog, Inc
34
* SPDX-License-Identifier: Apache-2.0
45
*/
56

@@ -392,16 +393,16 @@ off_t Recording::finishChunk(bool end_recording) {
392393
if (oSampler->_record_liveness) {
393394
writeIntSetting(_buf, T_HEAP_LIVE_OBJECT, "interval", oSampler->_interval);
394395
writeIntSetting(_buf, T_HEAP_LIVE_OBJECT, "capacity",
395-
LivenessTracker::instance()->_table_cap);
396+
LivenessTracker::instance()->getTableCapacity());
396397
writeIntSetting(_buf, T_HEAP_LIVE_OBJECT, "maximum capacity",
397-
LivenessTracker::instance()->_table_max_cap);
398+
LivenessTracker::instance()->getMaxTableCapacity());
398399
}
399400
writeDatadogProfilerConfig(
400401
_buf, Profiler::instance()->cpuEngine()->interval() / 1000000,
401402
Profiler::instance()->wallEngine()->interval() / 1000000,
402403
oSampler->_record_allocations ? oSampler->_interval : 0L,
403404
oSampler->_record_liveness ? oSampler->_interval : 0L,
404-
oSampler->_record_liveness ? LivenessTracker::instance()->_table_cap : 0L,
405+
oSampler->_record_liveness ? LivenessTracker::instance()->getTableCapacity() : 0L,
405406
oSampler->_record_liveness ? LivenessTracker::instance()->_subsample_ratio
406407
: 0.0,
407408
oSampler->_gc_generations, Profiler::instance()->eventMask(),

0 commit comments

Comments
 (0)