Skip to content

Commit dfc29ef

Browse files
authored
fix: prevent duplicate trace_id after CallTraceHashTable expansion (#659)
1 parent 0c8985a commit dfc29ef

5 files changed

Lines changed: 322 additions & 11 deletions

File tree

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

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@
1717
static const u32 INITIAL_CAPACITY = 65536; // 64K initial table size (matches upstream)
1818
static const u32 CALL_TRACE_CHUNK = 8 * 1024 * 1024;
1919
static const u64 OVERFLOW_TRACE_ID = 0x7fffffffffffffffULL; // Max 64-bit signed value
20+
// slot_base + local_slot must stay within a u32 so it never carries into
21+
// instance_id's bits of trace_id = (instance_id << 32) | (slot_base + slot).
22+
static const u64 SLOT_ID_RANGE = 0x100000000ull; // 2^32
23+
24+
// Pure, allocation-free helpers for the expansion-overflow guard below;
25+
// exposed via the header so tests can exercise the 2^32 slot-id boundary
26+
// directly instead of needing billions of real put() calls to reach it.
27+
u64 CallTraceHashTable::nextGenerationCapacity(u32 capacity) {
28+
return (u64)capacity * 2;
29+
}
30+
31+
bool CallTraceHashTable::wouldExceedSlotIdRange(u64 slot_base, u32 capacity) {
32+
u64 prospective_base = slot_base + capacity;
33+
return prospective_base + nextGenerationCapacity(capacity) > SLOT_ID_RANGE;
34+
}
2035

2136
// Define the sentinel value for CallTraceSample
2237
CallTrace* const CallTraceSample::PREPARING = reinterpret_cast<CallTrace*>(1);
@@ -26,7 +41,8 @@ class LongHashTable {
2641
LongHashTable *_prev;
2742
void *_padding0;
2843
u32 _capacity;
29-
u32 _padding1[15];
44+
u32 _slot_base;
45+
u32 _padding1[14];
3046
volatile u32 _size;
3147
u32 _padding2[15];
3248

@@ -37,22 +53,25 @@ class LongHashTable {
3753
}
3854

3955
public:
40-
LongHashTable(LongHashTable *prev = nullptr, u32 capacity = 0, bool should_clean = true)
41-
: _prev(prev), _padding0(nullptr), _capacity(capacity), _size(0) {
56+
LongHashTable(LongHashTable *prev = nullptr, u32 capacity = 0,
57+
u32 slot_base = 0, bool should_clean = true)
58+
: _prev(prev), _padding0(nullptr), _capacity(capacity),
59+
_slot_base(slot_base), _size(0) {
4260
memset(_padding1, 0, sizeof(_padding1));
4361
memset(_padding2, 0, sizeof(_padding2));
4462
if (should_clean) {
4563
clear();
4664
}
4765
}
4866

49-
static LongHashTable *allocate(LongHashTable *prev, u32 capacity, LinearAllocator* allocator) {
67+
static LongHashTable *allocate(LongHashTable *prev, u32 capacity,
68+
u32 slot_base, LinearAllocator* allocator) {
5069
void *memory = allocator->alloc(getSize(capacity));
5170
if (memory != nullptr) {
5271
// Use placement new to invoke constructor in-place with parameters
5372
// LinearAllocator doesn't zero memory like OS::safeAlloc with anon mmap
5473
// so we need to explicitly clear the keys and values (should_clean = true)
55-
LongHashTable *table = new (memory) LongHashTable(prev, capacity, true);
74+
LongHashTable *table = new (memory) LongHashTable(prev, capacity, slot_base, true);
5675
return table;
5776
}
5877
return nullptr;
@@ -63,6 +82,8 @@ class LongHashTable {
6382

6483
u32 capacity() { return _capacity; }
6584

85+
u32 slotBase() { return _slot_base; }
86+
6687
u32 size() { return _size; }
6788

6889
u32 incSize() { return __sync_add_and_fetch(&_size, 1); }
@@ -89,10 +110,14 @@ CallTraceHashTable::CallTraceHashTable() : _instance_id(0), _parent_storage(null
89110
// Instance ID will be set externally via setInstanceId()
90111

91112
// Start with initial capacity, allowing expansion as needed
92-
_table = LongHashTable::allocate(nullptr, INITIAL_CAPACITY, &_allocator);
113+
_table = LongHashTable::allocate(nullptr, INITIAL_CAPACITY, 0, &_allocator);
93114
_overflow = 0;
94115
}
95116

117+
void CallTraceHashTable::seedTableForTesting(u32 slot_base, u32 capacity) {
118+
_table = LongHashTable::allocate(nullptr, capacity, slot_base, &_allocator);
119+
}
120+
96121
CallTraceHashTable::~CallTraceHashTable() {
97122
// LinearAllocator handles all memory cleanup automatically
98123
// No need to explicitly destroy tables since they're allocated from LinearAllocator
@@ -178,7 +203,7 @@ ChunkList CallTraceHashTable::clearTableOnly() {
178203
// RELEASE: pairs with ACQUIRE loads in collect() and put() to ensure the
179204
// freshly-initialised table is visible on weakly-ordered architectures (aarch64).
180205
__atomic_store_n(&_table,
181-
LongHashTable::allocate(nullptr, INITIAL_CAPACITY, &_allocator),
206+
LongHashTable::allocate(nullptr, INITIAL_CAPACITY, 0, &_allocator),
182207
__ATOMIC_RELEASE);
183208
_overflow = 0;
184209

@@ -280,8 +305,23 @@ void CallTraceHashTable::expandTableIfNeeded(LongHashTable* table, u32 size) {
280305
// EXPANSION LOGIC: Check if load ratio reached after incrementing size
281306
if (size >= (u32) (capacity * LOAD_RATIO) &&
282307
table == __atomic_load_n(&_table, __ATOMIC_RELAXED)) { // quick check, if other thread already expanded the table
308+
if (wouldExceedSlotIdRange(table->slotBase(), capacity)) {
309+
// Expanding would push slot_base + local_slot past 2^32, carrying
310+
// into instance_id's bit range. Skip expansion; put() keeps working
311+
// on the current table at a higher load factor. This degraded state
312+
// is bounded, not permanent: the next processTraces() rotation resets
313+
// slot_base to 0 via clearTableOnly(), so normal JFR-flush cadence
314+
// recovers it.
315+
Counters::increment(CALLTRACE_STORAGE_EXPANSION_SKIPPED);
316+
return;
317+
}
318+
319+
u64 prospective_base = (u64)table->slotBase() + capacity;
320+
u64 prospective_capacity = nextGenerationCapacity(capacity);
321+
283322
// Allocate new table with double capacity using LinearAllocator
284-
LongHashTable* new_table = LongHashTable::allocate(table, capacity * 2, &_allocator);
323+
LongHashTable* new_table = LongHashTable::allocate(
324+
table, (u32)prospective_capacity, (u32)prospective_base, &_allocator);
285325
if (new_table != nullptr) {
286326
// Atomic table swap - only one thread succeeds
287327
__atomic_compare_exchange_n(&_table, &table, new_table, false, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED);
@@ -384,11 +424,13 @@ u64 CallTraceHashTable::put(int num_frames, ASGCT_CallFrame *frames,
384424
}
385425

386426
if (trace == nullptr) {
387-
// Generate unique trace ID: upper 32 bits = instance_id, lower 32 bits = slot
427+
// Generate unique trace ID: upper 32 bits = instance_id, lower 32 bits =
428+
// slot_base + local slot (slot_base makes the low bits unique across
429+
// all LongHashTable generations of one active tenure)
388430
// ACQUIRE ordering synchronizes with RELEASE store in setInstanceId() to ensure
389431
// visibility of new instance_id on weakly-ordered architectures (aarch64, POWER)
390432
u64 instance_id = _instance_id.load(std::memory_order_acquire);
391-
u64 trace_id = (instance_id << 32) | slot;
433+
u64 trace_id = (instance_id << 32) | (table->slotBase() + slot);
392434
trace = storeCallTrace(num_frames, frames, truncated, trace_id);
393435
if (trace == nullptr) {
394436
// Allocation failure - reset trace first, then clear key

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,24 @@ class CallTraceStorage;
5656
class CallTraceHashTable {
5757
static constexpr double LOAD_RATIO = 3.0 / 4.0;
5858

59+
friend class CallTraceHashTableTestAccessor;
60+
// Test-only: grants CallTraceHashTableOverflowGuardTestAccessor
61+
// (test_callTraceStorage.cpp) access to the private overflow-guard
62+
// helpers below, so CallTraceHashTableOverflowGuardTest can exercise the
63+
// 2^32 slot-id boundary and the capacity-doubling behaviour directly,
64+
// without needing billions of real put() calls to reach them.
65+
friend class CallTraceHashTableOverflowGuardTestAccessor;
66+
5967
public:
6068
static CallTrace _overflow_trace;
6169

6270
private:
71+
// Pure, allocation-free helpers backing the expansion-overflow guard in
72+
// expandTableIfNeeded(); kept private and reached in tests only via
73+
// CallTraceHashTableOverflowGuardTestAccessor (see friend declaration above).
74+
static u64 nextGenerationCapacity(u32 capacity);
75+
static bool wouldExceedSlotIdRange(u64 slot_base, u32 capacity);
76+
6377
std::atomic<u64> _instance_id; // 64-bit instance ID for this hash table - atomic for thread-safe access
6478
CallTraceStorage* _parent_storage; // Parent storage for RefCountGuard access
6579

@@ -83,6 +97,13 @@ class CallTraceHashTable {
8397

8498
void expandTableIfNeeded(LongHashTable* table, u32 size);
8599

100+
// Test-only seam: replaces the live table with a synthetic one seeded at
101+
// an explicit slot_base/capacity, so gtest can drive expandTableIfNeeded()
102+
// through its real call path at the 2^32 slot-id boundary without
103+
// billions of real put() calls to get there. Only ever invoked via
104+
// CallTraceHashTableTestAccessor (test_callTraceStorage.cpp).
105+
void seedTableForTesting(u32 slot_base, u32 capacity);
106+
86107
public:
87108
CallTraceHashTable();
88109
~CallTraceHashTable();

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ typedef std::function<void(std::unordered_set<u64>&)> LivenessChecker;
3131
class CallTraceStorage {
3232
public:
3333
// Reserved trace ID for dropped samples due to contention
34-
// Real trace IDs are generated as (instance_id << 32) | slot, where instance_id starts from 1
34+
// Real trace IDs are generated as (instance_id << 32) | (slot_base + slot),
35+
// where instance_id starts from 1 and slot_base makes the low bits unique
36+
// across all LongHashTable generations of one active tenure
3537
// Any ID with 0 in upper 32 bits is guaranteed to not clash with real trace IDs
3638
static constexpr u64 DROPPED_TRACE_ID = 1ULL;
3739

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
X(UNWINDING_TIME_ASYNC, "unwinding_ticks_async") \
7474
X(UNWINDING_TIME_JVMTI, "unwinding_ticks_jvmti") \
7575
X(CALLTRACE_STORAGE_DROPPED, "calltrace_storage_dropped_traces") \
76+
X(CALLTRACE_STORAGE_EXPANSION_SKIPPED, "calltrace_storage_expansion_skipped") \
7677
X(LINE_NUMBER_TABLES, "line_number_tables") \
7778
X(LINE_NUMBER_TABLE_UNREADABLE, "line_number_table_unreadable") \
7879
X(REMOTE_SYMBOLICATION_FRAMES, "remote_symbolication_frames") \

0 commit comments

Comments
 (0)