Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ba99ac9
feat(nativemem): categorized native-memory accounting — first cut
rkennke Jul 17, 2026
1950e37
feat(nativemem): precise per-category max with bounded total
rkennke Jul 17, 2026
ab609a1
feat(nativemem): tag the big native-memory consumers
rkennke Jul 20, 2026
958de87
feat(nativemem): tag DICTIONARY, remove CONTEXT
rkennke Jul 20, 2026
40f6fcf
docs(nativemem): scope the async-signal-safety note to CALLTRACE
rkennke Jul 20, 2026
f6124f8
refactor(nativemem): rename CODECACHE category to NATIVE_SYMBOLS
rkennke Jul 21, 2026
5b7cc8a
docs(nativemem): correct record() description (add + high-water CAS)
rkennke Jul 21, 2026
279d97f
harden(nativemem): assert the non-negative and key-length invariants
rkennke Jul 21, 2026
0036067
fix(nativemem): clamp emitted values; fix JFR_BUFFERS decrement ordering
rkennke Jul 22, 2026
02a3c05
fix(profiler): two-phase calltrace resize; refresh native-lib counter…
rkennke Jul 22, 2026
f6cde90
feat(nativemem): account the perf _events array under NM_PERF
rkennke Jul 22, 2026
a2be664
test(nativemem): THREAD_LOCAL lifecycle coverage; copyright headers
rkennke Jul 22, 2026
53fc7e2
fix(nativemem): balance deleteForTest; account frees after they happen
rkennke Jul 22, 2026
d4cad78
style(nativemem): record calltrace-buffer decrement after free(prev)
rkennke Jul 23, 2026
9a8e4dd
style(nativemem): record perf _events decrement after free()
rkennke Jul 23, 2026
ae6358b
style(nativemem): record THREAD_LOCAL decrement after delete pt
rkennke Jul 23, 2026
1a4f8ee
style(nativemem): record dictionary/arena decrements after free()
rkennke Jul 23, 2026
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
16 changes: 16 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,14 +66,20 @@ 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 without tracking it per key. Capture it before free(),
// then record after, consistent with the other decrement sites.
long long key_bytes = (long long)(strlen(row->keys[j]) + 1);
free(row->keys[j]); // content is zeroed en-mass in the clear() function
NativeMem::record(NM_DICTIONARY, -key_bytes);
}
}
if (row->next != NULL) {
clear(row->next, id);
DictTable *tmp = row->next;
row->next = NULL;
free(tmp);
NativeMem::record(NM_DICTIONARY, -(long long)sizeof(DictTable));
}
}
}
Expand Down Expand Up @@ -110,6 +124,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 +145,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
80 changes: 80 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,69 @@ 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 sampled total 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 thread
rkennke marked this conversation as resolved.
Comment thread
rkennke marked this conversation as resolved.

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) {
// Clamp to 0 before encoding: the value is serialized as an unsigned varint
// (putVar64), so a negative live gauge would emit a huge value and corrupt
// the counter stream. avg/max are already non-negative; live is clamped
// here to match sample()/liveTotal().
if (value < 0) {
value = 0;
}
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 thread
rkennke marked this conversation as resolved.
for (const auto &m : metrics) {
char label[64];
snprintf(label, sizeof(label), "%s%s", m.prefix, name);
emit(label, m.value);
}
}

// NATIVE_MEM_MAX_BYTES already carries the upper bound on the total peak (sum
// of precise per-category peaks); here we also emit the largest observed
// sampled total (a non-atomic per-category sum; approximate).
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 +2136,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,7 +2149,12 @@ void FlightRecorder::stop() {
if (rec != nullptr) {
// NULL first, deallocate later
_rec = nullptr;
// Decrement AFTER delete: ~Recording() runs finishChunk(), which emits the
// native-memory counters for the final chunk. The Recording buffers are
// still live during that serialization, so account the free only once it
// has actually happened.
delete rec;
NativeMem::record(NM_JFR_BUFFERS, -(long long)sizeof(Recording));
}
}

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
120 changes: 120 additions & 0 deletions ddprof-lib/src/main/cpp/nativeMem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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++) {
// Clamp per-category negatives to 0 (see sample()): the total is exported
// as an unsigned varint, so a stray negative would otherwise serialize as a
// huge value and corrupt the counter stream.
long long v = load(_live[c]);
if (v > 0) {
total += v;
}
}
return total;
}

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;
Comment thread
rkennke marked this conversation as resolved.
}

// The per-category peaks are maintained precisely at allocation time by
// record(); here we only track the largest observed total. Note `total` is a
// non-atomic sum of the per-category gauges read moments apart, so it is an
// approximate sampled figure, not a strict instantaneous total.
_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