Skip to content
Draft
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
3 changes: 3 additions & 0 deletions ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
X(CALLTRACE_STORAGE_TRACES, "calltrace_storage_traces") \
X(LINEAR_ALLOCATOR_BYTES, "linear_allocator_bytes") \
X(LINEAR_ALLOCATOR_CHUNKS, "linear_allocator_chunks") \
X(NATIVE_MEM_LIVE_BYTES, "native_mem_live_bytes") \
X(NATIVE_MEM_MAX_BYTES, "native_mem_max_bytes") \
X(NATIVE_MEM_AVG_BYTES, "native_mem_avg_bytes") \
X(THREAD_IDS_COUNT, "thread_ids_count") \
X(THREAD_NAMES_COUNT, "thread_names_count") \
X(THREAD_FILTER_PAGES, "thread_filter_pages") \
Expand Down
47 changes: 47 additions & 0 deletions ddprof-lib/src/main/cpp/flightRecorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "context.h"
#include "context_api.h"
#include "counters.h"
#include "nativeMem.h"
#include "dictionary.h"
#include "flightRecorder.inline.h"
#include "incbin.h"
Expand Down Expand Up @@ -810,7 +811,9 @@ off_t Recording::finishChunk(bool end_recording, bool do_cleanup) {
// dictionary) will reflect the previous serialization. That is, some level of
// familiarity with the code base will be required to use this diagnostic
// information for now.
updateNativeMemStats();
writeCounters(_buf);
writeNativeMem(_buf);

// Keep a simple stats for where we failed to unwind
// For the sakes of simplicity we are not keeping the count of failed unwinds which would also be
Expand Down Expand Up @@ -1776,6 +1779,50 @@ void Recording::writeLogLevels(Buffer *buf) {
}
}

void Recording::updateNativeMemStats() {
// Fold the per-category live gauges into their moving-window averages and
// running peaks. This is a sampled max: peaks that rise and fall entirely
// between two chunk finishes are not observed. A spike-accurate max would
// track the high-water mark at allocation time.
NativeMem::sample();

// Mirror the totals into the flat counter table so they flow out through the
// existing counter path (JFR T_DATADOG_COUNTER events and the JNI debug
// counters). Per-category values are emitted separately by writeNativeMem().
Counters::set(NATIVE_MEM_LIVE_BYTES, NativeMem::liveTotal());
Counters::set(NATIVE_MEM_AVG_BYTES, NativeMem::avgTotal());
Counters::set(NATIVE_MEM_MAX_BYTES, NativeMem::maxTotal());
}

void Recording::writeNativeMem(Buffer *buf) {
// Emit live/avg/max per category as counter events, reusing the counter event
// format with a "<metric>.<category>" name so they land alongside the totals
// without needing a dedicated event type or a slot in the counter table.
for (int c = 0; c < NM_NUM_CATEGORIES; c++) {
NativeMemCategory cat = (NativeMemCategory)c;
const char *name = NativeMem::categoryName(cat);
const struct {
const char *prefix;
long long value;
} metrics[] = {
{"native_mem_live_bytes.", NativeMem::live(cat)},
{"native_mem_avg_bytes.", NativeMem::avg(cat)},
{"native_mem_max_bytes.", NativeMem::max(cat)},
};
for (const auto &m : metrics) {
char label[64];
snprintf(label, sizeof(label), "%s%s", m.prefix, name);
int start = buf->skip(1);
buf->putVar64(T_DATADOG_COUNTER);
buf->putVar64(_start_ticks);
buf->putUtf8(label);
buf->putVar64(m.value);
writeEventSizePrefix(buf, start);
flushIfNeeded(buf);
}
}
}

void Recording::writeCounters(Buffer *buf) {
long long *counters = Counters::getCounters();
if (counters) {
Expand Down
3 changes: 3 additions & 0 deletions ddprof-lib/src/main/cpp/flightRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ class Recording {

void writeCounters(Buffer *buf);

void updateNativeMemStats();
void writeNativeMem(Buffer *buf);

void writeUnwindFailures(Buffer *buf);

void writeContextSnapshot(Buffer *buf, Context &context);
Expand Down
6 changes: 6 additions & 0 deletions ddprof-lib/src/main/cpp/linearAllocator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include "linearAllocator.h"
#include "counters.h"
#include "nativeMem.h"
#include "os.h"
#include "common.h"
#include <stdio.h>
Expand Down Expand Up @@ -167,6 +168,9 @@ void LinearAllocator::freeChunks(ChunkList& chunks) {
OS::safeFree(current, chunks.chunk_size);
Counters::decrement(LINEAR_ALLOCATOR_BYTES, chunks.chunk_size);
Counters::decrement(LINEAR_ALLOCATOR_CHUNKS);
// The LinearAllocator's only user is call-trace storage, so all of its
// chunk memory is attributed to the CALLTRACE category.
NativeMem::record(NM_CALLTRACE, -(long long)chunks.chunk_size);
current = prev;
}

Expand Down Expand Up @@ -260,6 +264,7 @@ Chunk *LinearAllocator::allocateChunk(Chunk *current) {

Counters::increment(LINEAR_ALLOCATOR_BYTES, _chunk_size);
Counters::increment(LINEAR_ALLOCATOR_CHUNKS);
NativeMem::record(NM_CALLTRACE, (long long)_chunk_size);
}
return chunk;
}
Expand All @@ -275,6 +280,7 @@ void LinearAllocator::freeChunk(Chunk *current) {
OS::safeFree(current, _chunk_size);
Counters::decrement(LINEAR_ALLOCATOR_BYTES, _chunk_size);
Counters::decrement(LINEAR_ALLOCATOR_CHUNKS);
NativeMem::record(NM_CALLTRACE, -(long long)_chunk_size);
}

void LinearAllocator::reserveChunk(Chunk *current) {
Expand Down
103 changes: 103 additions & 0 deletions ddprof-lib/src/main/cpp/nativeMem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2026 Datadog, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "nativeMem.h"

volatile long long NativeMem::_live[NM_NUM_CATEGORIES] = {};
long long NativeMem::_window[NM_NUM_CATEGORIES][NativeMem::WINDOW] = {};
long long NativeMem::_total_window[NativeMem::WINDOW] = {};
int NativeMem::_window_pos = 0;
int NativeMem::_window_count = 0;
long long NativeMem::_avg[NM_NUM_CATEGORIES] = {};
long long NativeMem::_max[NM_NUM_CATEGORIES] = {};
long long NativeMem::_total_avg = 0;
long long NativeMem::_total_max = 0;

long long NativeMem::liveTotal() {
long long total = 0;
for (int c = 0; c < NM_NUM_CATEGORIES; c++) {
total += load(_live[c]);
}
return total;
}

void NativeMem::sample() {
long long total = 0;
for (int c = 0; c < NM_NUM_CATEGORIES; c++) {
long long v = load(_live[c]);
// Clamp transient negatives from not-yet-instrumented free sites so they do
// not skew the window; accounting is currently best-effort per category.
if (v < 0) {
v = 0;
}
_window[c][_window_pos] = v;
if (v > _max[c]) {
_max[c] = v;
}
total += v;
}

_total_window[_window_pos] = total;
if (total > _total_max) {
_total_max = total;
}

_window_pos = (_window_pos + 1) % WINDOW;
if (_window_count < WINDOW) {
_window_count++;
}

for (int c = 0; c < NM_NUM_CATEGORIES; c++) {
long long sum = 0;
for (int i = 0; i < _window_count; i++) {
sum += _window[c][i];
}
_avg[c] = sum / _window_count;
}

long long total_sum = 0;
for (int i = 0; i < _window_count; i++) {
total_sum += _total_window[i];
}
_total_avg = total_sum / _window_count;
}

void NativeMem::reset() {
for (int c = 0; c < NM_NUM_CATEGORIES; c++) {
store(_live[c], (long long)0);
_avg[c] = 0;
_max[c] = 0;
for (int i = 0; i < WINDOW; i++) {
_window[c][i] = 0;
}
}
for (int i = 0; i < WINDOW; i++) {
_total_window[i] = 0;
}
_window_pos = 0;
_window_count = 0;
_total_avg = 0;
_total_max = 0;
}

const char *NativeMem::categoryName(NativeMemCategory category) {
#define X_NM_NAME(a, b) b,
static const char *const names[] = {DD_NATIVE_MEM_CATEGORY_TABLE(X_NM_NAME)};
#undef X_NM_NAME
if (category < 0 || category >= NM_NUM_CATEGORIES) {
return "unknown";
}
return names[category];
}
99 changes: 99 additions & 0 deletions ddprof-lib/src/main/cpp/nativeMem.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2026 Datadog, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef _NATIVEMEM_H
#define _NATIVEMEM_H

#include "arch.h"

// Physical native-memory categories used by the profiler's own allocations.
// Every tracked backing allocation belongs to exactly one category, so the
// per-category live byte gauges partition the total: sum(category) == total,
// with no double counting.
//
// The "reserved vs used vs wasted" breakdowns exposed by the existing counters
// (CALLTRACE_STORAGE_BYTES is the used slice of the CALLTRACE arena;
// DICTIONARY_ARENA_WASTE_BYTES is the wasted slice of the DICTIONARY arena) are
// a separate, nested dimension. They are intentionally NOT summed in here.
#define DD_NATIVE_MEM_CATEGORY_TABLE(X) \
X(CALLTRACE, "calltrace") \
X(DICTIONARY, "dictionary") \
X(CONTEXT, "context") \
X(THREAD_FILTER, "thread_filter") \
X(CODECACHE, "codecache") \
X(LINE_TABLES, "line_tables") \
X(PERF, "perf") \
X(THREAD_LOCAL, "thread_local") \
X(JFR_BUFFERS, "jfr_buffers") \
X(MISC, "misc")

#define X_NM_ENUM(a, b) NM_##a,
typedef enum NativeMemCategory : int {
DD_NATIVE_MEM_CATEGORY_TABLE(X_NM_ENUM) NM_NUM_CATEGORIES
} NativeMemCategory;
#undef X_NM_ENUM

// Tracks the profiler's live native memory per category, plus a moving-window
// average and running peak. Live accounting is always-on and independent of the
// COUNTERS build flag: record() is a single relaxed atomic add, which is
// async-signal-safe and therefore usable from signal handlers.
//
// The moving average and peak are refreshed by sample(), which is expected to
// be called from a single thread (the JFR chunk-finish path).
class NativeMem {
private:
// Number of sample() ticks retained in the moving-average window.
static const int WINDOW = 64;

static volatile long long _live[NM_NUM_CATEGORIES];

// sample()-owned state; not touched from allocation sites.
static long long _window[NM_NUM_CATEGORIES][WINDOW];
static long long _total_window[WINDOW];
static int _window_pos;
static int _window_count;
static long long _avg[NM_NUM_CATEGORIES];
static long long _max[NM_NUM_CATEGORIES];
static long long _total_avg;
static long long _total_max;

public:
// Account for a backing allocation (delta > 0) or free (delta < 0).
static void record(NativeMemCategory category, long long delta) {
atomicIncRelaxed(_live[category], delta);
}

static long long live(NativeMemCategory category) {
return load(_live[category]);
}
static long long liveTotal();

// Fold the current live gauges into the moving-average window and the running
// peak. Call once per sampling tick from a single thread.
static void sample();

static long long avg(NativeMemCategory category) { return _avg[category]; }
static long long max(NativeMemCategory category) { return _max[category]; }
static long long avgTotal() { return _total_avg; }
static long long maxTotal() { return _total_max; }

// Clear all live gauges and window/peak state. Not thread-safe against
// concurrent record()/sample(); intended for process init and tests.
static void reset();

static const char *categoryName(NativeMemCategory category);
};

#endif // _NATIVEMEM_H
6 changes: 6 additions & 0 deletions ddprof-lib/src/main/cpp/profiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "guards.h"
#include "common.h"
#include "counters.h"
#include "nativeMem.h"
#include "ctimer.h"
#include "signalInflight.h"
#include "dwarf.h"
Expand Down Expand Up @@ -1321,6 +1322,7 @@ Error Profiler::start(Arguments &args, bool reset) {

// (Re-)allocate calltrace buffers
if (_max_stack_depth != args._jstackdepth) {
size_t prev_nelem = _max_stack_depth + RESERVED_FRAMES;
_max_stack_depth = args._jstackdepth;
size_t nelem = _max_stack_depth + RESERVED_FRAMES;

Expand All @@ -1334,13 +1336,17 @@ Error Profiler::start(Arguments &args, bool reset) {
return Error("Not enough memory to allocate stack trace buffers (try "
"smaller jstackdepth)");
}
NativeMem::record(NM_CALLTRACE, (long long)(nelem * sizeof(CallTraceBuffer)));
// Swap under the per-shard lock: all readers (recordJVMTISample,
// recordExternalSample) acquire this lock via tryLock before reading
// _calltrace_buffer, so no reader can observe a freed pointer mid-replacement.
_locks[i].lock();
CallTraceBuffer *prev = _calltrace_buffer[i];
_calltrace_buffer[i] = fresh;
_locks[i].unlock();
if (prev != NULL) {
NativeMem::record(NM_CALLTRACE, -(long long)(prev_nelem * sizeof(CallTraceBuffer)));
}
free(prev);
}
}
Expand Down
Loading
Loading