Commit b1eace6
feat(nativemem): categorized native-memory accounting — first cut (#669)
* feat(nativemem): categorized native-memory accounting — first cut
Add a NativeMem facility that tracks the profiler's own native memory
usage per category, with a moving-window average and a running peak.
- NativeMemCategory enum whose per-category live gauges partition the
total (each backing allocation belongs to exactly one category, so
there is no double counting).
- Live accounting is always-on and independent of the COUNTERS build
flag: record() is a single relaxed atomic add, async-signal-safe and
usable from signal handlers.
- sample() folds the live gauges into a moving-window average and a
high-water max; it is ticked once per JFR chunk finish.
- Totals mirror into the existing NATIVE_MEM_{LIVE,AVG,MAX}_BYTES
counters (JFR + JNI counter path); per-category values are emitted as
native_mem_{live,avg,max}_bytes.<category> counter events, reusing the
existing counter event format (no new event type).
Instrumented sites (tagged CALLTRACE, their sole use today): the
LinearAllocator chunk alloc/free (the call-trace arena) and the
per-shard calltrace buffers. Accounting lives with the semantic owner
rather than OS::safeAlloc, which stays category-agnostic.
First-cut limits, documented in code: only CALLTRACE sites are
instrumented so far (other categories read 0 until tagged); the max is
sampled rather than spike-accurate; transient negative live is clamped
to 0. The existing reserved/used/waste counters (CALLTRACE_STORAGE_BYTES,
DICTIONARY_ARENA_WASTE_BYTES) remain an independent nested dimension and
are intentionally not summed into the per-category total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(nativemem): precise per-category max with bounded total
Track each category's peak at allocation time instead of sampling it at
chunk finish, so a spike that rises and falls between two ticks is still
captured. record() updates the per-category high-water mark on positive
deltas via a relaxed CAS: the common (no new peak) path is a single load
plus a compare, and the CAS fires only when a genuinely higher peak is
set, which is rare since the peak is monotonic. Frees skip the check.
The total peak avoids a shared global counter (a contention hotspot on a
single cache line hit by every allocation) and is instead reported as a
bracket:
- NATIVE_MEM_MAX_BYTES = upper bound = sum of the precise per-category
peaks (exact when the peaks coincide, otherwise an overestimate).
- native_mem_max_observed_total_bytes = lower bound = the largest
instantaneous total seen at a sampling tick.
sample() no longer touches the per-category peaks; it only refreshes the
moving averages and the observed total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(nativemem): tag the big native-memory consumers
Extend per-category accounting beyond CALLTRACE to the other large,
cleanly-paired backing allocations:
- THREAD_LOCAL: `new`/`delete ProfiledThread` (sizeof, per thread).
- JFR_BUFFERS: `new`/`delete Recording` (embeds the RecordingBuffer array
and cpu-monitor buffer).
- LINE_TABLES: the malloc'd JVMTI line-number table copy, tracked beside
the existing LINE_NUMBER_TABLES counter and freed in
~SharedLineNumberTable (byte size recovered from the stored entry count).
- PERF: the perf ring mmap (2 * page_size), paired with its munmap.
- THREAD_FILTER: ChunkStorage chunks (bounded, tagged only on successful
CAS install) and the FreeListNode array.
- CODECACHE: a recomputed gauge, mirrored via the new NativeMem::setLive()
at the existing CodeCache size-set site. setLive() overwrites live and
still advances the peak.
Not tagged in this commit:
- CONTEXT has no separate allocation — the OTel context record is embedded
in ProfiledThread, so it is already counted under THREAD_LOCAL; tagging
it again would double-count. The category stays 0 by design.
- DICTIONARY is deferred to its own commit: it has two implementations
(the older per-key-malloc Dictionary used for symbols/packages, with
set()-based counters and bulk key-frees that lose per-item size, and the
arena-based StringDictionary where keys live inside chunks). A partial
number that tagged only one would mislead, so it needs dedicated
per-implementation handling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(nativemem): tag DICTIONARY, remove CONTEXT
Add DICTIONARY to the per-category accounting, and remove the CONTEXT
category.
CONTEXT had no separate backing allocation — the OTel context record is
embedded in ProfiledThread and already counted under THREAD_LOCAL — so it
would have been a permanently-zero line. Removed rather than left dangling.
DICTIONARY is tagged across both implementations, counting physical
backing allocations once each (single category; per-role breakdown, if
ever wanted, belongs in a nested dimension, not extra top-level
categories):
- Dictionary (older, per-key malloc; used for symbols/packages): root and
overflow DictTables (constant size) and key strings. Key frees are bulk
and lose per-item sizes, but keys are null-terminated so the malloc'd
size (strlen + 1) is recovered at free — no running total needed.
- StringDictionary / StringArena (arena-based): arena chunks and root /
overflow SBTables. Keys are bump-allocated inside chunks, so they are
NOT counted separately (that would double-count). Chunk and SBTable
accounting is unconditional, independent of the diagnostic counters'
_counter_offset gate, so anonymous dictionaries are covered too.
Tests: lifecycle invariants for both implementations assert the accounting
grows on insert, returns exactly to the construction baseline after
clear(), and to zero after destruction — directly proving inc/dec pairing
(including the strlen-at-free path and arena chunk growth). Full
gtestDebug suite green (356 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(nativemem): scope the async-signal-safety note to CALLTRACE
The class comment implied every record() call needs to be
async-signal-safe. In fact most categories allocate via malloc/new off
the signal path, where the property is irrelevant. Only the CALLTRACE
arena allocates from within the sampling signal handler (via
OS::safeAlloc's raw mmap syscall), so that is where record() staying a
relaxed atomic add actually matters. Reword accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(nativemem): rename CODECACHE category to NATIVE_SYMBOLS
The "CodeCache" name is async-profiler's, and collides with the JVM's
JIT code cache. This category measures something unrelated: the
profiler's own per-native-library symbol tables (used to symbolicate
native frames), not JVM-managed code. Rename the NativeMem category and
its JFR label to native_symbols so the metric is unambiguous.
The existing CODECACHE_NATIVE_SIZE_BYTES counter keeps its name for
continuity; only the new NativeMem category is renamed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(nativemem): correct record() description (add + high-water CAS)
The class comment still described record() as "a single relaxed atomic
add" from before precise per-category max was added. record() now also
does a conditional lock-free high-water update on allocation. Correct the
wording; the async-signal-safety guarantee still holds (lock-free atomics).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* harden(nativemem): assert the non-negative and key-length invariants
Two invariants the accounting relies on are now asserted (stripped under
NDEBUG, so no release cost and never in a real signal handler):
- Per-category live bytes never go negative: record() asserts the
post-update value >= 0. A negative means an unbalanced/oversized free.
This lets liveTotal() sum without clamping, since each term is >= 0.
- Dictionary keys are NUL-free strings of exactly `length`: allocateKey()
asserts strlen == length. The NM_DICTIONARY free path recovers a key's
size via strlen at clear(), so an embedded NUL would under-count; the
assert trips in debug/gtest instead of silently drifting.
The sample() clamp is retained as a release-mode safety net for the
asserted-impossible negative case, and its comment updated to say so. The
old NegativeLiveClampedInSample test is removed: it deliberately drove a
category negative, which now (correctly) trips the record() assert.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(nativemem): clamp emitted values; fix JFR_BUFFERS decrement ordering
Review fixes:
- Clamp per-category live to 0 at the emit boundary (liveTotal() and
writeNativeMem()). These values are serialized via putVar64(u64); a
negative gauge would otherwise emit a huge varint and corrupt the
counter stream. Matches the clamping sample() already does. The
record() invariant asserts non-negative in debug; this guards the
release path where the assert is stripped.
- Move the NM_JFR_BUFFERS decrement in FlightRecorder::stop() to after
`delete rec`: ~Recording() runs finishChunk(), which emits the counters
for the final chunk while the buffers are still live, so account the
free only once it has happened.
- Reword the "lower bound" description of the observed total: the sampled
per-category sum is not an atomic snapshot, so it can drift above or
below a true instantaneous total; it is an approximate sampled figure,
and maxTotal() remains the authoritative ceiling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(profiler): two-phase calltrace resize; refresh native-lib counters on stop
Review fixes:
- Reallocate the per-shard calltrace buffers in two phases: allocate all
CONCURRENCY_LEVEL replacements first, and only swap/free/account if all
succeed. A mid-loop allocation failure now leaves the profiler entirely
unchanged (old buffers, _max_stack_depth, and NM_CALLTRACE accounting
stay consistent), instead of the previous partial-swap + reset of
_max_stack_depth to 0 that mis-accounted a later resize.
- Extract updateNativeLibMemStats(): read native_libs.memoryUsage() once
(was called three times) and publish the CODECACHE counters plus the
NM_NATIVE_SYMBOLS gauge. Call it from both dump() and stop() (before
_jfr.stop()), so the final chunk reflects native-symbol memory even when
stopping without a preceding dump.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(nativemem): account the perf _events array under NM_PERF
The per-thread PerfEvent array (max_events * sizeof(PerfEvent)) is real
perf-engine memory that NM_PERF omitted — it counted only the ring
mappings. Add paired inc/dec around the calloc/free in start(), guarded
on non-null so a prior calloc failure is not mis-accounted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(nativemem): THREAD_LOCAL lifecycle coverage; copyright headers
- Add two tests proving NM_THREAD_LOCAL balances across the ProfiledThread
lifecycle: the decrement lives in freeValue(), reached both via
release() -> ThreadLocal::clear() and via the pthread-key destructor on
thread exit. Both paths return the gauge to baseline.
- dictionary_ut.cpp: update copyright to 2025, 2026.
- stringDictionary_ut.cpp: add the missing Datadog copyright header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(nativemem): balance deleteForTest; account frees after they happen
Two follow-up review fixes:
- ProfiledThread::deleteForTest() (UNIT_TEST helper) deleted the object
directly, bypassing freeValue()'s NM_THREAD_LOCAL decrement and leaking
live bytes across tests that use it. It stands in for freeValue()'s
delete, so mirror the decrement.
- In ~ThreadFilter(), record the decrements after the memory is actually
freed: delete the chunk before its decrement, and reset the _free_list
unique_ptr explicitly before recording (rather than letting it free the
array after the destructor body runs). Keeps the gauge from leading the
free during teardown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(nativemem): record calltrace-buffer decrement after free(prev)
Consistency: the two-phase resize recorded the NM_CALLTRACE decrement
before free(prev); the other decrement sites (FlightRecorder::stop,
~ThreadFilter) account the free after it happens. Reorder to match.
Functionally equivalent (free doesn't read the counter), purely for
uniformity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(nativemem): record perf _events decrement after free()
Consistency (same as the calltrace-buffer and ~ThreadFilter sites): record
the NM_PERF decrement for the old _events array after free(_events) rather
than before. Capture the old size first, since _max_events is overwritten
by the reallocation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(nativemem): record THREAD_LOCAL decrement after delete pt
Consistency with the other decrement sites (calltrace resize, perf
_events, ~ThreadFilter): record the NM_THREAD_LOCAL decrement after
delete pt rather than before. sizeof is a compile-time constant, so no
value is lost by deleting first.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(nativemem): record dictionary/arena decrements after free()
Sweep the remaining "record before free" decrement sites to record after
the free, consistent with the calltrace/perf/thread-local/ThreadFilter
sites (avoids a transient underreport if sampling races teardown):
- StringArena chunk frees (~StringArena, reset()).
- SBTable frees (freeOverflowNodes, ~StringDictionaryBuffer).
- Dictionary key strings (capture strlen+1 before free, record after) and
overflow DictTable frees.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(nativemem): smoke-test the aggregate NM accounting counters
Add an end-to-end smoke test asserting the always-on native-memory
accounting counters (native_mem_live_bytes / _avg_bytes / _max_bytes)
are emitted into the recording after a short profiling run and hold
their sanity invariants: the peak total (sum of precise per-category
peaks) brackets both the live total and the moving-window average.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(nativemem): use the short-form license header
Replace the full Apache-2.0 boilerplate in nativeMem.{h,cpp} and
nativeMem_ut.cpp with the short-form copyright + SPDX identifier used
across the tree, and normalize the dictionary_ut.cpp copyright line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent c57ba18 commit b1eace6
20 files changed
Lines changed: 757 additions & 35 deletions
File tree
- ddprof-lib/src
- main/cpp
- test/cpp
- ddprof-test/src/test/java/com/datadoghq/profiler/nativemem
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
56 | 59 | | |
57 | 60 | | |
58 | 61 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
29 | 36 | | |
30 | 37 | | |
31 | 38 | | |
| |||
37 | 44 | | |
38 | 45 | | |
39 | 46 | | |
| 47 | + | |
40 | 48 | | |
41 | 49 | | |
42 | 50 | | |
| |||
58 | 66 | | |
59 | 67 | | |
60 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
61 | 73 | | |
| 74 | + | |
62 | 75 | | |
63 | 76 | | |
64 | 77 | | |
65 | 78 | | |
66 | 79 | | |
67 | 80 | | |
68 | 81 | | |
| 82 | + | |
69 | 83 | | |
70 | 84 | | |
71 | 85 | | |
| |||
110 | 124 | | |
111 | 125 | | |
112 | 126 | | |
| 127 | + | |
113 | 128 | | |
114 | 129 | | |
115 | 130 | | |
| |||
130 | 145 | | |
131 | 146 | | |
132 | 147 | | |
| 148 | + | |
133 | 149 | | |
134 | 150 | | |
135 | 151 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
67 | 68 | | |
68 | 69 | | |
69 | 70 | | |
| 71 | + | |
70 | 72 | | |
71 | 73 | | |
72 | 74 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
73 | 74 | | |
74 | 75 | | |
75 | 76 | | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
76 | 81 | | |
77 | 82 | | |
78 | 83 | | |
| |||
420 | 425 | | |
421 | 426 | | |
422 | 427 | | |
| 428 | + | |
| 429 | + | |
423 | 430 | | |
424 | 431 | | |
425 | 432 | | |
| |||
810 | 817 | | |
811 | 818 | | |
812 | 819 | | |
| 820 | + | |
813 | 821 | | |
| 822 | + | |
814 | 823 | | |
815 | 824 | | |
816 | 825 | | |
| |||
1776 | 1785 | | |
1777 | 1786 | | |
1778 | 1787 | | |
| 1788 | + | |
| 1789 | + | |
| 1790 | + | |
| 1791 | + | |
| 1792 | + | |
| 1793 | + | |
| 1794 | + | |
| 1795 | + | |
| 1796 | + | |
| 1797 | + | |
| 1798 | + | |
| 1799 | + | |
| 1800 | + | |
| 1801 | + | |
| 1802 | + | |
| 1803 | + | |
| 1804 | + | |
| 1805 | + | |
| 1806 | + | |
| 1807 | + | |
| 1808 | + | |
| 1809 | + | |
| 1810 | + | |
| 1811 | + | |
| 1812 | + | |
| 1813 | + | |
| 1814 | + | |
| 1815 | + | |
| 1816 | + | |
| 1817 | + | |
| 1818 | + | |
| 1819 | + | |
| 1820 | + | |
| 1821 | + | |
| 1822 | + | |
| 1823 | + | |
| 1824 | + | |
| 1825 | + | |
| 1826 | + | |
| 1827 | + | |
| 1828 | + | |
| 1829 | + | |
| 1830 | + | |
| 1831 | + | |
| 1832 | + | |
| 1833 | + | |
| 1834 | + | |
| 1835 | + | |
| 1836 | + | |
| 1837 | + | |
| 1838 | + | |
| 1839 | + | |
| 1840 | + | |
| 1841 | + | |
| 1842 | + | |
| 1843 | + | |
| 1844 | + | |
| 1845 | + | |
| 1846 | + | |
| 1847 | + | |
| 1848 | + | |
| 1849 | + | |
| 1850 | + | |
1779 | 1851 | | |
1780 | 1852 | | |
1781 | 1853 | | |
| |||
2064 | 2136 | | |
2065 | 2137 | | |
2066 | 2138 | | |
| 2139 | + | |
| 2140 | + | |
| 2141 | + | |
2067 | 2142 | | |
2068 | 2143 | | |
2069 | 2144 | | |
| |||
2074 | 2149 | | |
2075 | 2150 | | |
2076 | 2151 | | |
| 2152 | + | |
| 2153 | + | |
| 2154 | + | |
| 2155 | + | |
2077 | 2156 | | |
| 2157 | + | |
2078 | 2158 | | |
2079 | 2159 | | |
2080 | 2160 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
304 | 304 | | |
305 | 305 | | |
306 | 306 | | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
307 | 310 | | |
308 | 311 | | |
309 | 312 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
167 | 168 | | |
168 | 169 | | |
169 | 170 | | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
170 | 174 | | |
171 | 175 | | |
172 | 176 | | |
| |||
260 | 264 | | |
261 | 265 | | |
262 | 266 | | |
| 267 | + | |
263 | 268 | | |
264 | 269 | | |
265 | 270 | | |
| |||
275 | 280 | | |
276 | 281 | | |
277 | 282 | | |
| 283 | + | |
278 | 284 | | |
279 | 285 | | |
280 | 286 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
0 commit comments