Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ddprof-lib/src/main/cpp/callTraceHashTable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ u64 CallTraceHashTable::put(int num_frames, ASGCT_CallFrame *frames,
}
}

void CallTraceHashTable::collect(std::unordered_set<CallTrace *> &traces, std::function<void(CallTrace*)> trace_hook) {
void CallTraceHashTable::collect(CallTraceSet &traces, std::function<void(CallTrace*)> trace_hook) {
// Lock-free collection for read-only tables.
// Use ACQUIRE to pair with the ACQ_REL CAS in put()'s expansion path and the
// RELEASE store in clearTableOnly(); ensures we see the fully-initialised table
Expand Down
22 changes: 20 additions & 2 deletions ddprof-lib/src/main/cpp/callTraceHashTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
#define _CALLTRACEHASHTABLE_H

#include "arch.h"
#include "countingAllocator.h"
#include "linearAllocator.h"
#include "nativeMem.h"
#include "vmEntry.h"
#include <unordered_set>
#include <atomic>
Expand All @@ -21,11 +23,27 @@ struct CallTrace {
u64 trace_id; // 64-bit for JFR constant pool compatibility
ASGCT_CallFrame frames[1];

CallTrace(bool truncated, int num_frames, u64 trace_id)
CallTrace(bool truncated, int num_frames, u64 trace_id)
: truncated(truncated), num_frames(num_frames), trace_id(trace_id) {
}
};

// Traces collected for JFR/liveness processing. Uses CountingAllocator so the
// real per-node heap cost is attributed to NM_CALLTRACE, the same category the
// rest of the call-trace arena already accounts into. These sets are cleared
// and rebuilt on every processTraces() rotation, so the per-insert atomic
// increment/peak-CAS runs on a hot path; a microbenchmark against a plain
// std::allocator on a 50k-entry table measured ~5% added time per insert.
// Kept as-is: the cost is modest and switching to a periodic setLive() gauge
// update would trade the exact per-node byte count for an estimate.

using CallTraceSet =
std::unordered_set<CallTrace *, std::hash<CallTrace *>, std::equal_to<CallTrace *>,
CountingAllocator<CallTrace *, NM_CALLTRACE>>;
using CallTraceIdSet =
std::unordered_set<u64, std::hash<u64>, std::equal_to<u64>,
CountingAllocator<u64, NM_CALLTRACE>>;

struct CallTraceSample {
CallTrace *trace;

Expand Down Expand Up @@ -122,7 +140,7 @@ class CallTraceHashTable {
*/
ChunkList clearTableOnly();

void collect(std::unordered_set<CallTrace *> &traces, std::function<void(CallTrace*)> trace_hook = nullptr);
void collect(CallTraceSet &traces, std::function<void(CallTrace*)> trace_hook = nullptr);

u64 put(int num_frames, ASGCT_CallFrame *frames, bool truncated, u64 weight);
void putWithExistingId(CallTrace* trace, u64 weight); // For standby tables with no contention
Expand Down
2 changes: 1 addition & 1 deletion ddprof-lib/src/main/cpp/callTraceStorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ u64 CallTraceStorage::put(int num_frames, ASGCT_CallFrame* frames, bool truncate
* This function is safe to call concurrently with put() operations.
* It is not designed to be called concurrently with itself.
*/
void CallTraceStorage::processTraces(std::function<void(const std::unordered_set<CallTrace*>&)> processor) {
void CallTraceStorage::processTraces(std::function<void(const CallTraceSet&)> processor) {
// PHASE 1: Collect liveness information with simple lock (rare operation)
{
SharedLockGuard lock(&_liveness_lock);
Expand Down
8 changes: 4 additions & 4 deletions ddprof-lib/src/main/cpp/callTraceStorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class CallTraceHashTable;
// Liveness checker function type
// Fills the provided set with 64-bit call_trace_id values that should be preserved
// Using reference parameter avoids malloc() for vector creation and copying
typedef std::function<void(std::unordered_set<u64>&)> LivenessChecker;
typedef std::function<void(CallTraceIdSet&)> LivenessChecker;

class CallTraceStorage {
public:
Expand Down Expand Up @@ -65,8 +65,8 @@ class CallTraceStorage {

// Pre-allocated collections for processTraces (single-threaded operation)
// These collections are reused to eliminate malloc/free cycles
std::unordered_set<CallTrace*> _traces_buffer; // All traces for JFR processing
std::unordered_set<u64> _preserve_set_buffer; // Preserve set for current cycle
CallTraceSet _traces_buffer; // All traces for JFR processing
CallTraceIdSet _preserve_set_buffer; // Preserve set for current cycle

public:
CallTraceStorage();
Expand All @@ -85,7 +85,7 @@ class CallTraceStorage {
// Lock-free trace processing with RefCountGuard protection
// The callback receives traces that are guaranteed to be valid during execution
// Uses atomic table swapping with grace period for safe memory reclamation
void processTraces(std::function<void(const std::unordered_set<CallTrace*>&)> processor);
void processTraces(std::function<void(const CallTraceSet&)> processor);

// Enhanced clear with liveness preservation (rarely called - uses atomic operations)
void clear();
Expand Down
4 changes: 4 additions & 0 deletions ddprof-lib/src/main/cpp/codeCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,10 @@ void CodeCache::setDwarfTable(FrameDesc *table, int length, const FrameDesc &def
// _dwarf_table_length lock-free at dump time, so this must not run afterwards.
assert(!_published.load(std::memory_order_acquire) &&
"setDwarfTable() on a published CodeCache races memoryUsage()");
// The parser (SFrameParser/DwarfParser) already shrinks its capacity-doubled
// buffer to exactly `length` entries before returning it from table(), so
// memoryUsage()'s length-based formula matches the real allocation with no
// trim step needed here.
_dwarf_table = table;
_dwarf_table_length = length;
_default_frame = &default_frame;
Expand Down
55 changes: 55 additions & 0 deletions ddprof-lib/src/main/cpp/countingAllocator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef _COUNTINGALLOCATOR_H
#define _COUNTINGALLOCATOR_H

#include "nativeMem.h"
#include <cstddef>
#include <new>

// A stateless, C++11-Allocator-conformant wrapper around ::operator new /
// ::operator delete that records every allocation/deallocation into the given
// NativeMem category. STL containers rebind the supplied Allocator<value_type>
// to their actual node type before calling allocate(), so this yields the
// exact real per-node byte count the implementation uses -- not an estimate.
template <typename T, NativeMemCategory Cat>
class CountingAllocator {
public:
using value_type = T;

CountingAllocator() noexcept = default;
template <typename U>
CountingAllocator(const CountingAllocator<U, Cat> &) noexcept {}

T *allocate(std::size_t n) {
T *p = static_cast<T *>(::operator new(n * sizeof(T)));
NativeMem::record(Cat, (long long)(n * sizeof(T)));
return p;
}

void deallocate(T *p, std::size_t n) noexcept {
NativeMem::record(Cat, -(long long)(n * sizeof(T)));
::operator delete(p);
}

template <typename U>
struct rebind {
using other = CountingAllocator<U, Cat>;
};
};

template <typename T, NativeMemCategory Cat>
inline bool operator==(const CountingAllocator<T, Cat> &,
const CountingAllocator<T, Cat> &) {
return true;
}

template <typename T, NativeMemCategory Cat>
inline bool operator!=(const CountingAllocator<T, Cat> &,
const CountingAllocator<T, Cat> &) {
return false;
}

#endif // _COUNTINGALLOCATOR_H
13 changes: 12 additions & 1 deletion ddprof-lib/src/main/cpp/dwarf.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#define _DWARF_H

#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "arch.h"

Expand Down Expand Up @@ -264,7 +265,17 @@ class DwarfParser {
// Ownership of the returned pointer transfers to the caller.
// The caller is responsible for freeing it with free() (not delete[]).
// DwarfParser has no destructor; _table is left dangling after this call is used.
FrameDesc* table() const {
FrameDesc* table() {
// Shrink the capacity-doubled buffer to _count entries here, at the
// source, so every consumer of table()/count() already gets an
// exactly-sized allocation instead of relying on a shared trim step
// downstream.
if (_table != nullptr && _count > 0 && _count < _capacity) {
FrameDesc* trimmed = (FrameDesc*)realloc(_table, _count * sizeof(FrameDesc));
if (trimmed != nullptr) {
_table = trimmed;
}
}
return _table;
}

Expand Down
5 changes: 2 additions & 3 deletions ddprof-lib/src/main/cpp/flightRecorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1659,15 +1659,14 @@ int Recording::writeStackTraces(Buffer *buf, Lookup *lookup) {
// via processCallTraces, but no T_STACK_TRACE section is emitted in that case.
int trace_count = 0;
// Use safe trace processing with guaranteed lifetime during callback execution
Profiler::instance()->processCallTraces([this, buf, lookup, &trace_count](const std::unordered_set<CallTrace*>& traces) {
Profiler::instance()->processCallTraces([this, buf, lookup, &trace_count](const CallTraceSet& traces) {
if (traces.empty()) {
return;
}
trace_count = (int)traces.size();
buf->putVar64(T_STACK_TRACE);
buf->putVar64(traces.size());
for (std::unordered_set<CallTrace *>::const_iterator it = traces.begin();
it != traces.end(); ++it) {
for (auto it = traces.begin(); it != traces.end(); ++it) {
CallTrace *trace = *it;
buf->putVar64(trace->trace_id);
if (trace->num_frames > 0) {
Expand Down
11 changes: 9 additions & 2 deletions ddprof-lib/src/main/cpp/flightRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#ifndef _FLIGHTRECORDER_H
#define _FLIGHTRECORDER_H

#include <functional>
#include <map>
#include <unordered_map>
#include <unordered_set>
Expand All @@ -18,6 +19,7 @@
#include "arch.h"
#include "arguments.h"
#include "buffers.h"
#include "countingAllocator.h"
#include "counters.h"
#include "dictionary.h"
#include "stringDictionary.h"
Expand Down Expand Up @@ -106,7 +108,10 @@ class MethodInfo {
// 10 - void* address (native frame names)
// 01 - RemoteFrameInfo (packed remote symbolication)
// 11 - vtable_receiver class_id (BCI_VTABLE_RECEIVER frames)
class MethodMap : public std::map<unsigned long, MethodInfo> {
class MethodMap
: public std::map<unsigned long, MethodInfo, std::less<unsigned long>,
CountingAllocator<std::pair<const unsigned long, MethodInfo>,
NM_METHOD_MAP>> {
public:
static constexpr unsigned long ADDRESS_MARK = 0x8000000000000000ULL;
static constexpr unsigned long REMOTE_FRAME_MARK = 0x4000000000000000ULL;
Expand Down Expand Up @@ -365,7 +370,9 @@ class Lookup {
// as the MethodMap key, so distinct Symbol* addresses for the same
// class name (class unload/reload mid-chunk) collapse to a single
// MethodInfo row.
std::unordered_map<void*, u32> _vtable_receiver_cache;
std::unordered_map<void*, u32, std::hash<void*>, std::equal_to<void*>,
CountingAllocator<std::pair<void* const, u32>, NM_JFR_BUFFERS>>
_vtable_receiver_cache;
Dictionary _packages;
Dictionary _symbols;

Expand Down
10 changes: 8 additions & 2 deletions ddprof-lib/src/main/cpp/livenessTracker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "jniHelper.h"
#include "livenessTracker.h"
#include "log.h"
#include "nativeMem.h"
#include "os.h"
#include "profiler.h"
#include "threadLocalData.h"
Expand Down Expand Up @@ -193,7 +194,7 @@ Error LivenessTracker::start(Arguments &args) {
}

// Self-register with the profiler for liveness checking
Profiler::instance()->registerLivenessChecker([this](std::unordered_set<u64>& buffer) {
Profiler::instance()->registerLivenessChecker([this](CallTraceIdSet& buffer) {
this->getLiveTraceIds(buffer);
});

Expand Down Expand Up @@ -277,6 +278,9 @@ Error LivenessTracker::initialize(Arguments &args) {
std::min(2048, _table_max_cap); // with default 512k sampling interval, it's
// enough for 1G of heap
_table = (TrackingEntry *)malloc(sizeof(TrackingEntry) * _table_cap);
if (_table != NULL) {
NativeMem::record(NM_LIVENESS, (long long)sizeof(TrackingEntry) * _table_cap);
}

_gc_epoch = 0;
_last_gc_epoch = 0;
Expand Down Expand Up @@ -406,6 +410,8 @@ void LivenessTracker::track(JNIEnv *env, AllocEvent &event, jint tid,
TrackingEntry *tmp = (TrackingEntry *)realloc(
_table, sizeof(TrackingEntry) * newcap);
if (tmp != nullptr) {
NativeMem::record(NM_LIVENESS,
(long long)sizeof(TrackingEntry) * (newcap - _table_cap));
_table = tmp;
_table_cap = newcap;
Log::debug(
Expand Down Expand Up @@ -450,7 +456,7 @@ void LivenessTracker::onGC() {
}
}

void LivenessTracker::getLiveTraceIds(std::unordered_set<u64>& out_buffer) {
void LivenessTracker::getLiveTraceIds(CallTraceIdSet& out_buffer) {
out_buffer.clear();

if (!_enabled || !_initialized) {
Expand Down
3 changes: 2 additions & 1 deletion ddprof-lib/src/main/cpp/livenessTracker.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#define _LIVENESSTRACKER_H

#include "arch.h"
#include "callTraceHashTable.h"
#include "context.h"
#include "engine.h"
#include "event.h"
Expand Down Expand Up @@ -104,7 +105,7 @@ class alignas(alignof(SpinLock)) LivenessTracker {
static void JNICALL GarbageCollectionFinish(jvmtiEnv *jvmti_env);

private:
void getLiveTraceIds(std::unordered_set<u64>& out_buffer);
void getLiveTraceIds(CallTraceIdSet& out_buffer);
};

#endif // _LIVENESSTRACKER_H
4 changes: 4 additions & 0 deletions ddprof-lib/src/main/cpp/nativeMem.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
X(LINE_TABLES, "line_tables") \
X(PERF, "perf") \
X(THREAD_LOCAL, "thread_local") \
X(THREAD_INFO, "thread_info") \
X(JFR_BUFFERS, "jfr_buffers") \
X(METHOD_MAP, "method_map") \
X(LIVENESS, "liveness") \
X(WALLCLOCK, "wallclock") \
X(MISC, "misc")

#define X_NM_ENUM(a, b) NM_##a,
Expand Down
4 changes: 2 additions & 2 deletions ddprof-lib/src/main/cpp/profiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,12 @@ class alignas(alignof(SpinLock)) Profiler {

const char* cstack() const;
int lookupClass(const char *key, size_t length);
void processCallTraces(std::function<void(const std::unordered_set<CallTrace*>&)> processor) {
void processCallTraces(std::function<void(const CallTraceSet&)> processor) {
if (!_omit_stacktraces) {
_call_trace_storage.processTraces(processor);
} else {
// If stack traces are omitted, call processor with empty set
static std::unordered_set<CallTrace*> empty_traces;
static CallTraceSet empty_traces;
processor(empty_traces);
}
}
Expand Down
10 changes: 10 additions & 0 deletions ddprof-lib/src/main/cpp/sframe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ SFrameParser::~SFrameParser() {
}

FrameDesc* SFrameParser::table() {
// Shrink the capacity-doubled buffer to _count entries here, at the source,
// so every consumer of table()/count() already gets an exactly-sized
// allocation instead of relying on a shared trim step downstream.
if (_table != nullptr && _count > 0 && _count < _capacity) {
FrameDesc* trimmed = static_cast<FrameDesc*>(
realloc(_table, _count * sizeof(FrameDesc)));
if (trimmed != nullptr) {
_table = trimmed;
}
}
FrameDesc* t = _table;
_table = nullptr;
return t;
Expand Down
Loading
Loading