diff --git a/ddprof-lib/src/main/cpp/codeCache.cpp b/ddprof-lib/src/main/cpp/codeCache.cpp index 5203d5c5d..a18d8c2e6 100644 --- a/ddprof-lib/src/main/cpp/codeCache.cpp +++ b/ddprof-lib/src/main/cpp/codeCache.cpp @@ -144,6 +144,33 @@ CodeCache::~CodeCache() { free(_build_id); // Free build-id memory } +long long CodeCache::memoryUsage() const { + // The blob array: _capacity entries of CodeBlob. + long long total = (long long)_capacity * sizeof(CodeBlob); + + // This cache's own name, plus each symbol's name string. Each is a + // variable-length allocation whose size depends on the name length + // (see NativeFunc::allocSize). Both the name pointers and the array are fixed + // once the library is published (the same read the symbolication fast path + // does), so this is safe to read at dump time without extra synchronization. + total += (long long)NativeFunc::allocSize(_name); + for (int i = 0; i < _count; i++) { + total += (long long)NativeFunc::allocSize(_blobs[i]._name); + } + + // The DWARF unwind table, when present (length only — no pointer deref). + total += (long long)_dwarf_table_length * sizeof(FrameDesc); + + // The build-id string is intentionally NOT counted here: the background + // library refresher (Libraries::updateBuildIds) frees and replaces _build_id + // on already-published caches under _build_id_lock, which dump does not hold, + // so dereferencing it here would race. It is negligible (~tens of bytes per + // library) next to the symbol tables, so excluding it costs no meaningful + // accuracy while keeping this read lock-free. + + return total; +} + void CodeCache::expand() { CodeBlob *old_blobs = _blobs; CodeBlob *new_blobs = new CodeBlob[_capacity * 2]; diff --git a/ddprof-lib/src/main/cpp/codeCache.h b/ddprof-lib/src/main/cpp/codeCache.h index 92b45bf47..bfa65c660 100644 --- a/ddprof-lib/src/main/cpp/codeCache.h +++ b/ddprof-lib/src/main/cpp/codeCache.h @@ -74,6 +74,15 @@ class NativeFunc { static char *create(const char *name, short lib_index); static void destroy(char *name); + // Size of the heap allocation backing a name string produced by create(). + // Mirrors the size computed there so callers can account for it. 0 if null. + static size_t allocSize(const char *name) { + if (name == nullptr) { + return 0; + } + return align_up(sizeof(NativeFunc) + 1 + strlen(name), sizeof(NativeFunc *)); + } + static short libIndex(const char *name) { if (name == nullptr) { return -1; @@ -251,9 +260,14 @@ class CodeCache { void setDwarfTable(FrameDesc *table, int length, const FrameDesc &default_frame = FrameDesc::default_frame); FrameDesc findFrameDesc(const void *pc); - long long memoryUsage() { - return _capacity * sizeof(CodeBlob *) + _count * sizeof(NativeFunc); - } + // Live size of what this CodeCache owns on the heap: the blob array, the + // per-symbol name strings (variable length), this cache's own name, and the + // DWARF unwind table. Recomputed on demand (called only at dump time), so it + // reflects the current contents. The build-id string is deliberately excluded + // — it is mutated by the background refresher and negligible in size (see the + // definition). Const and lock-free: reads only fields that are stable once the + // library is published. + long long memoryUsage() const; int count() { return _count; } CodeBlob* blob(int idx) { @@ -266,11 +280,10 @@ class CodeCacheArray { CodeCache *_libs[MAX_NATIVE_LIBS]; volatile int _reserved; // next slot to reserve (CAS by writers) volatile int _count; // published count (all indices < _count have non-NULL pointers) - volatile size_t _used_memory; bool _overflow_reported; public: - CodeCacheArray() : _reserved(0), _count(0), _used_memory(0), _overflow_reported(false) { + CodeCacheArray() : _reserved(0), _count(0), _overflow_reported(false) { memset(_libs, 0, MAX_NATIVE_LIBS * sizeof(CodeCache *)); } @@ -296,7 +309,6 @@ class CodeCacheArray { } while (!__atomic_compare_exchange_n(&_reserved, &slot, slot + 1, true, __ATOMIC_RELAXED, __ATOMIC_RELAXED)); assert(__atomic_load_n(&_libs[slot], __ATOMIC_RELAXED) == nullptr); - __atomic_fetch_add(&_used_memory, lib->memoryUsage(), __ATOMIC_RELAXED); // Store pointer before publishing count. The RELEASE here pairs with // the ACQUIRE load in operator[]/at() and count(). __atomic_store_n(&_libs[slot], lib, __ATOMIC_RELEASE); @@ -316,8 +328,21 @@ class CodeCacheArray { return __atomic_load_n(&_libs[index], __ATOMIC_ACQUIRE); } + // Sum the live memory of all registered libraries. Recomputed on demand + // (called only at dump time) so it reflects each library's current size, + // including symbols added after the library was registered. The array is + // append-only, so iterating the published prefix is safe alongside + // concurrent add()s. size_t memoryUsage() const { - return __atomic_load_n(&_used_memory, __ATOMIC_RELAXED); + size_t total = 0; + int n = count(); + for (int i = 0; i < n; i++) { + CodeCache *lib = at(i); + if (lib != nullptr) { + total += (size_t)lib->memoryUsage(); + } + } + return total; } }; diff --git a/ddprof-lib/src/test/cpp/codeCache_ut.cpp b/ddprof-lib/src/test/cpp/codeCache_ut.cpp new file mode 100644 index 000000000..a67964557 --- /dev/null +++ b/ddprof-lib/src/test/cpp/codeCache_ut.cpp @@ -0,0 +1,48 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "codeCache.h" + +// memoryUsage() must reflect the actual per-symbol name length, not a fixed +// per-symbol constant: adding one symbol grows the reported usage by exactly +// the allocation size of its name string. +TEST(CodeCacheTest, MemoryUsageCountsSymbolNameLength) { + CodeCache cc("testlib"); + long long base = cc.memoryUsage(); + + const char *name = + "a_long_symbol_name_well_beyond_sizeof_NativeFunc_padding_xxxxxxxxxxxx"; + cc.add(reinterpret_cast(0x1000), 16, name); + + EXPECT_EQ(NativeFunc::allocSize(name), (size_t)(cc.memoryUsage() - base)); +} + +// A longer name costs more than a shorter one — the reported size scales with +// the actual symbol-name length. +TEST(CodeCacheTest, MemoryUsageGrowsWithNameLength) { + CodeCache shortc("lib"); + CodeCache longc("lib"); + long long short_base = shortc.memoryUsage(); + long long long_base = longc.memoryUsage(); + + shortc.add(reinterpret_cast(0x1000), 8, "f"); + longc.add(reinterpret_cast(0x1000), 8, + "an_extremely_long_symbol_name_padded_out_further_and_further_1234"); + + EXPECT_GT(longc.memoryUsage() - long_base, shortc.memoryUsage() - short_base); +} + +// The blob array is counted at the full CodeBlob size (a CodeBlob is several +// pointers wide): an empty cache already accounts for +// capacity * sizeof(CodeBlob) plus its own name. +TEST(CodeCacheTest, MemoryUsageCountsFullBlobArray) { + CodeCache cc("lib"); + long long usage = cc.memoryUsage(); + EXPECT_GT(usage, (long long)(INITIAL_CODE_CACHE_CAPACITY * sizeof(CodeBlob))); + // Sanity: a pointer-sized count would be far smaller than the real array. + EXPECT_GT((long long)(INITIAL_CODE_CACHE_CAPACITY * sizeof(CodeBlob)), + (long long)(INITIAL_CODE_CACHE_CAPACITY * sizeof(CodeBlob *))); +}