Skip to content
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
14 changes: 14 additions & 0 deletions ddprof-lib/src/main/cpp/dictionary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "arch.h"
#include "counters.h"
#include "signalSafety.h"
#include <cassert>
#include <climits>
#include <stdlib.h>
#include <string.h>
Expand All @@ -26,6 +27,12 @@ static inline char *allocateKey(const char *key, size_t length) {
char *result = (char *)malloc(length + 1);
memcpy(result, key, length);
result[length] = 0;
// NM_DICTIONARY accounting recovers a freed key's size via strlen at clear()
// time, which requires the key to be a NUL-free string of exactly `length`.
// Pin that assumption here so a future caller passing an embedded NUL trips in
// debug/gtest rather than silently under-counting the free. Stripped under
// NDEBUG.
assert(strlen(result) == length);
return result;
}

Expand All @@ -37,6 +44,7 @@ static inline bool keyEquals(const char *candidate, const char *key,
Dictionary::~Dictionary() {
clear(_table, _id);
free(_table);
NativeMem::record(NM_DICTIONARY, -(long long)sizeof(DictTable));
Counters::set(DICTIONARY_BYTES, 0, _id);
Counters::set(DICTIONARY_PAGES, 0, _id);
}
Expand All @@ -58,13 +66,17 @@ void Dictionary::clear(DictTable *table, int id) {
DictRow *row = &table->rows[i];
for (int j = 0; j < CELLS; j++) {
if (row->keys[j]) {
// Keys are null-terminated, so the malloc'd size (length + 1) is
// recoverable at free time without tracking it per key.
NativeMem::record(NM_DICTIONARY, -(long long)(strlen(row->keys[j]) + 1));
free(row->keys[j]); // content is zeroed en-mass in the clear() function
}
}
if (row->next != NULL) {
clear(row->next, id);
DictTable *tmp = row->next;
row->next = NULL;
NativeMem::record(NM_DICTIONARY, -(long long)sizeof(DictTable));
free(tmp);
}
}
Expand Down Expand Up @@ -110,6 +122,7 @@ unsigned int Dictionary::lookup(const char *key, size_t length, bool for_insert,
if (__sync_bool_compare_and_swap(&row->keys[c], NULL, new_key)) {
Counters::increment(DICTIONARY_KEYS, 1, _id);
Counters::increment(DICTIONARY_KEYS_BYTES, length + 1, _id);
NativeMem::record(NM_DICTIONARY, (long long)(length + 1));
atomicInc(_size);
return table->index(h % ROWS, c);
}
Expand All @@ -130,6 +143,7 @@ unsigned int Dictionary::lookup(const char *key, size_t length, bool for_insert,
} else {
Counters::increment(DICTIONARY_PAGES, 1, _id);
Counters::increment(DICTIONARY_BYTES, sizeof(DictTable), _id);
NativeMem::record(NM_DICTIONARY, (long long)sizeof(DictTable));
}
} else {
return sentinel;
Expand Down
2 changes: 2 additions & 0 deletions ddprof-lib/src/main/cpp/dictionary.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#define _DICTIONARY_H

#include "counters.h"
#include "nativeMem.h"
#include <map>
#include <stddef.h>
#include <stdlib.h>
Expand Down Expand Up @@ -67,6 +68,7 @@ class Dictionary {
_table = (DictTable *)calloc(1, sizeof(DictTable));
Counters::set(DICTIONARY_PAGES, 1, id);
Counters::set(DICTIONARY_BYTES, sizeof(DictTable), id);
NativeMem::record(NM_DICTIONARY, (long long)sizeof(DictTable));
_table->base_index = _base_index = 1;
_size = 0;
}
Expand Down
69 changes: 69 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 @@ -73,6 +74,10 @@ SharedLineNumberTable::~SharedLineNumberTable() {
if (_ptr != nullptr) {
free(_ptr);
Counters::decrement(LINE_NUMBER_TABLES);
// _size is the JVMTI entry count passed at construction (see
// fillJavaMethodInfo), so the byte size matches the allocation.
NativeMem::record(NM_LINE_TABLES, -(long long)((size_t)_size *
sizeof(jvmtiLineNumberEntry)));
}
}

Expand Down Expand Up @@ -420,6 +425,8 @@ void Lookup::fillJavaMethodInfo(MethodInfo *mi, jmethodID method,
line_number_table_size, owned_table);
// Increment counter for tracking live line number tables
Counters::increment(LINE_NUMBER_TABLES);
NativeMem::record(NM_LINE_TABLES, (long long)((size_t)line_number_table_size *
sizeof(jvmtiLineNumberEntry)));
}
}

Expand Down Expand Up @@ -810,7 +817,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 +1785,62 @@ void Recording::writeLogLevels(Buffer *buf) {
}
}

void Recording::updateNativeMemStats() {
// Refresh the moving-window averages and the observed total peak. Per-category
// peaks are maintained precisely at allocation time, so they are not sampled
// here; the total peak is bracketed instead (see writeNativeMem).
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). NATIVE_MEM_MAX_BYTES carries the upper bound on the total peak
// (sum of precise per-category peaks); the observed lower bound and the
// per-category values are emitted 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());
}
Comment on lines +1788 to +1802

void Recording::writeNativeMem(Buffer *buf) {
// Emit native-memory stats as counter events, reusing the counter event format
// so they land alongside the totals without needing a dedicated event type or
// a slot in the counter table.
auto emit = [&](const char *label, long long value) {
int start = buf->skip(1);
buf->putVar64(T_DATADOG_COUNTER);
buf->putVar64(_start_ticks);
buf->putUtf8(label);
buf->putVar64(value);
writeEventSizePrefix(buf, start);
flushIfNeeded(buf);
};

// Per-category live/avg/max, named "<metric>.<category>". The max here is the
// precise per-category peak tracked at allocation time.
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)},
};
Comment on lines +1818 to +1830
for (const auto &m : metrics) {
char label[64];
snprintf(label, sizeof(label), "%s%s", m.prefix, name);
emit(label, m.value);
}
}

// The total peak is bracketed: NATIVE_MEM_MAX_BYTES already carries the upper
// bound (sum of precise per-category peaks); here we also emit the observed
// lower bound (largest instantaneous total seen at a sampling tick).
emit("native_mem_max_observed_total_bytes", NativeMem::maxTotalObserved());
}

void Recording::writeCounters(Buffer *buf) {
long long *counters = Counters::getCounters();
if (counters) {
Expand Down Expand Up @@ -2064,6 +2129,9 @@ Error FlightRecorder::newRecording(bool reset) {
}

_rec = new Recording(fd, _args);
// The Recording embeds the JFR RecordingBuffer array and the cpu-monitor
// buffer, so its allocation size is the profiler's JFR buffer footprint.
NativeMem::record(NM_JFR_BUFFERS, (long long)sizeof(Recording));
return Error::OK;
}

Expand All @@ -2074,6 +2142,7 @@ void FlightRecorder::stop() {
if (rec != nullptr) {
// NULL first, deallocate later
_rec = nullptr;
NativeMem::record(NM_JFR_BUFFERS, -(long long)sizeof(Recording));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep JFR buffer bytes counted through final chunk

When stopping a recording, delete rec invokes Recording::~Recording(), which calls finishChunk(true) and emits the native-memory counters. This decrement runs before that destructor, so the final JFR chunk reports jfr_buffers and the total as if the Recording buffers were already freed even though they are still live during serialization; move the decrement after delete rec or after finishChunk to keep stop output accurate.

Useful? React with 👍 / 👎.

delete rec;
}
}
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
112 changes: 112 additions & 0 deletions ddprof-lib/src/main/cpp/nativeMem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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] = {};
volatile long long NativeMem::_max[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::_total_avg = 0;
long long NativeMem::_total_max_observed = 0;

long long NativeMem::liveTotal() {
long long total = 0;
for (int c = 0; c < NM_NUM_CATEGORIES; c++) {
total += load(_live[c]);
}
return total;
}
Comment on lines +28 to +34

long long NativeMem::maxTotal() {
long long total = 0;
for (int c = 0; c < NM_NUM_CATEGORIES; c++) {
total += load(_max[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]);
// A category's live bytes are never negative under correct pairing (asserted
// in record()). This clamp is a release-mode safety net: should an accounting
// bug slip past the assert under NDEBUG, it keeps a negative from skewing the
// window average and total rather than propagating garbage.
if (v < 0) {
v = 0;
}
_window[c][_window_pos] = v;
total += v;
}

// The per-category peaks are maintained precisely at allocation time by
// record(); here we only track the observed total (lower bound on the peak).
_total_window[_window_pos] = total;
if (total > _total_max_observed) {
_total_max_observed = 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);
store(_max[c], (long long)0);
_avg[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_observed = 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];
}
Loading
Loading