Skip to content
Open
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
65 changes: 40 additions & 25 deletions ddprof-lib/src/main/cpp/codeCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ CodeCache::~CodeCache() {
}

long long CodeCache::memoryUsage() const {
// O(1): _memory_usage is updated incrementally by the constructor, add(),
// expand(), and setDwarfTable() -- the same pre-publication mutators that
// O(1): _memory_usage is updated incrementally by the constructor,
// add()/commitAdd(), and setDwarfTable() -- the same pre-publication mutators that
// used to be re-summed here on every call. Safe to read lock-free once
// published (Libraries::native_libs) because those mutators never run
// afterwards (asserted via _published), same invariant the old per-call
Expand All @@ -191,55 +191,70 @@ long long CodeCache::memoryUsage() const {
return _memory_usage;
}

void CodeCache::expand() {
// Must not run after publication: memoryUsage() reads _blobs lock-free from
// the dump thread and a concurrent realloc would free it underneath.
assert(!_published.load(std::memory_order_acquire) &&
"expand() on a published CodeCache races memoryUsage()");
CodeBlob *old_blobs = _blobs;
CodeBlob *new_blobs = new CodeBlob[_capacity * 2];

memcpy(new_blobs, old_blobs, _count * sizeof(CodeBlob));

// Delta is exactly the current (pre-doubling) capacity's worth of blobs.
_memory_usage += (long long)_capacity * sizeof(CodeBlob);
_capacity *= 2;
_blobs = new_blobs;
delete[] old_blobs;
}

void CodeCache::add(const void *start, int length, const char *name,
bool update_bounds) {
void CodeCache::prepareAdd(const char *name, PreparedAdd &prepared) const {
// Symbols are added while parsing a library, before it is registered into a
// CodeCacheArray. Adding after publication would race memoryUsage() (which
// reads _blobs/_count lock-free at dump time). Unpublished standalone caches
// (e.g. _runtime_stubs) are exempt — they are never published.
assert(!_published.load(std::memory_order_acquire) &&
"add() on a published CodeCache races memoryUsage()");
char *name_copy = NativeFunc::create(name, _lib_index);
_memory_usage += (long long)NativeFunc::allocSize(name);
prepared.name = name_copy;
prepared.name_size = (long long)NativeFunc::allocSize(name);
// Measured off the block just handed back, while the pointer is in hand: the
// gauge is published as a total at add() time, with no pointer left to probe.
_name_overhead += (long long)NativeFunc::nameOverhead(name_copy);
prepared.name_overhead = (long long)NativeFunc::nameOverhead(name_copy);
// Replace non-printable characters
for (char *s = name_copy; *s != 0; s++) {
if (*s < ' ')
*s = '?';
}

prepared.capacity = _capacity;
prepared.grown = nullptr;
if (_count >= _capacity) {
expand();
prepared.grown = new CodeBlob[_capacity * 2];
memcpy(prepared.grown, _blobs, _count * sizeof(CodeBlob));
}
}

CodeBlob *CodeCache::commitAdd(const void *start, int length,
const PreparedAdd &prepared,
bool update_bounds) {
assert(!_published.load(std::memory_order_acquire) &&
"add() on a published CodeCache races memoryUsage()");
// A mutation between prepareAdd() and commitAdd() would make the grown copy
// stale or leave no free slot.
assert(prepared.capacity == _capacity &&
(prepared.grown != nullptr || _count < _capacity));
CodeBlob *retired = nullptr;
if (prepared.grown != nullptr) {
// Delta is exactly the current (pre-doubling) capacity's worth of blobs.
_memory_usage += (long long)_capacity * sizeof(CodeBlob);
_capacity *= 2;
retired = _blobs;
_blobs = prepared.grown;
}
_memory_usage += prepared.name_size;
_name_overhead += prepared.name_overhead;

const void *end = (const char *)start + length;
_blobs[_count]._start = start;
_blobs[_count]._end = end;
_blobs[_count]._name = name_copy;
_blobs[_count]._name = prepared.name;
_count++;

if (update_bounds) {
updateBounds(start, end);
}
return retired;
}

void CodeCache::add(const void *start, int length, const char *name,
bool update_bounds) {
PreparedAdd prepared;
prepareAdd(name, prepared);
delete[] commitAdd(start, length, prepared, update_bounds);
}

void CodeCache::updateBounds(const void *start, const void *end) {
Expand Down
38 changes: 30 additions & 8 deletions ddprof-lib/src/main/cpp/codeCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class CodeCache {
CodeBlob *_blobs;

// Running total of memoryUsage()'s formula, updated incrementally by the
// same pre-publication mutators (constructor, add(), expand(),
// same pre-publication mutators (constructor, add()/commitAdd(),
// setDwarfTable()) instead of recomputed by iterating _blobs. Safe because
// every contributing field is fixed before publication (see _published
// below) -- there is no point after construction-and-population where this
Expand All @@ -204,17 +204,17 @@ class CodeCache {

// Set once the cache is registered into a CodeCacheArray (see markPublished()).
// After that, memoryUsage() may be read lock-free from another thread (dump),
// so the mutators that touch the fields it reads (add()/expand()/
// setDwarfTable()) must not run — asserted in debug. Standalone caches that
// so the mutators that touch the fields it reads (add()/prepareAdd()/
// commitAdd()/setDwarfTable()) must not run — asserted in debug. Standalone caches that
// are never registered (e.g. JitCodeCache::_runtime_stubs, which is instead
// guarded by its own JitCodeCache::_stubs_lock) stay unpublished, so the
// guarded by its own JitCodeCache::_stubs_lock, with writers serialized by
// JitCodeCache::_stubs_write_lock) stay unpublished, so the
// asserts never fire for them. Atomic with release/acquire so the flag is
// observably true on any thread that reaches a published cache — the store is
// sequenced before CodeCacheArray::add()'s RELEASE publish of the pointer, and
// the assert loads it with acquire — without a formal data race.
std::atomic<bool> _published;

void expand();
void makeImportsPatchable();
void saveImport(ImportId id, void** entry);
void copyFrom(const CodeCache& other);
Expand Down Expand Up @@ -282,12 +282,34 @@ class CodeCache {
void setImageFingerprint(u64 fingerprint) { _image_fingerprint = fingerprint; }

// Mark this cache as published into a CodeCacheArray. Call before the array
// makes the pointer visible to readers; afterwards add()/expand()/
// setDwarfTable() must not be called (see _published).
// makes the pointer visible to readers; afterwards add()/prepareAdd()/
// commitAdd()/setDwarfTable() must not be called (see _published).
void markPublished() { _published.store(true, std::memory_order_release); }

void add(const void *start, int length, const char *name,
bool update_bounds = false);

// Heap allocations that prepareAdd() makes for exactly one later commitAdd().
struct PreparedAdd {
char *name; // owned NativeFunc name copy, non-printables replaced
long long name_size; // NativeFunc::allocSize() of the name
long long name_overhead; // NativeFunc::nameOverhead() of the copy
CodeBlob *grown; // filled copy of _blobs at twice the capacity, or
// nullptr when the current array has a free slot
int capacity; // _capacity that this preparation is valid for
};

// Two-phase add() for a cache whose readers run in signal handlers under a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional: up to you, not blocking.

A design point to consider. The bounded readers alone already break the deadlock, so prepareAdd()/commitAdd() plus the second mutex are about keeping the exclusive window short. I think that's worth keeping: without it, a writer blocked in malloc holds the lock exclusively for an unbounded time, every stub lookup fails during that time, and runtimeStubsMemoryUsage() waits behind it.

The rules, though, are enforced only by debug asserts: exactly one commitAdd() per prepareAdd(), nothing changing the cache in between, the caller deletes the returned array after unlocking, and writers are serialized externally. To make misuse harder, PreparedAdd could be an RAII type that frees its name copy and grown array if it is never committed.

// SpinLock (JitCodeCache::_runtime_stubs): prepareAdd() makes every heap
// allocation and can run outside the lock; commitAdd() only stores and
// copies pointers, so it is safe under the exclusive lock. commitAdd()
// returns the replaced blob array (or nullptr), which the caller must
// delete[] after it releases the lock. Every prepareAdd() must be followed by
// exactly one commitAdd(), with no other mutation of this cache in between;
// the caller serializes the pair against other writers.
void prepareAdd(const char *name, PreparedAdd &prepared) const;
CodeBlob *commitAdd(const void *start, int length,
const PreparedAdd &prepared, bool update_bounds = false);
void updateBounds(const void *start, const void *end);
void sort();

Expand Down Expand Up @@ -407,7 +429,7 @@ class CodeCacheArray {
// so it stays consistent with the logical bytes above.
NativeMem::recordOverhead(NM_NATIVE_SYMBOLS, lib->nameOverhead());
// Mark published before the RELEASE store makes the pointer visible, so any
// later add()/expand()/setDwarfTable() on this cache trips the assert (its
// later add()/prepareAdd()/commitAdd()/setDwarfTable() on this cache trips the assert (its
// _blobs would then be read lock-free by memoryUsage() at dump time).
lib->markPublished();
// Store pointer before publishing count. The RELEASE here pairs with
Expand Down
5 changes: 5 additions & 0 deletions ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@
X(WALKVM_STUB_INFO_HIT, "walkvm_stub_info_hit") \
X(WALKVM_STUB_INFO_FALLBACK, "walkvm_stub_info_fallback") \
X(WALKVM_STUB_INFO_REGISTRATION_FAILED, "walkvm_stub_info_registration_failed") \
/* Runtime-stub lookups (JitCodeCache::findRuntimeStub/findRuntimeStubInfo) \
* that returned "not found" without searching because the shared stubs \
* lock was held exclusively by a registration or the shared spin budget \
* ran out; the stack walk continued with its stub-not-found fallback. */ \
X(STUB_LOOKUP_LOCK_UNAVAILABLE, "stub_lookup_lock_unavailable") \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional: up to you, not blocking.

This counter combines "a writer held the lock" (an expected, short registration window) with "the spin budget ran out" (reader contention). If you switch to the no-argument tryLockShared(), only the first cause remains and this goes away. Otherwise, two counters would make the telemetry possible to act on.

X(WALKVM_FP_CHAIN_ATTEMPT, "walkvm_fp_chain_attempt") \
X(WALKVM_FP_CHAIN_REACHED_CODEHEAP, "walkvm_fp_chain_reached_codeheap") \
X(WALKVM_ANCHOR_NOT_IN_JAVA, "walkvm_anchor_not_in_java") \
Expand Down
18 changes: 8 additions & 10 deletions ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -761,9 +761,9 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
}
}

// One shared-lock acquisition returns the stub view plus the
// precomputed aarch64 unwind info (null on x64): the signal
// handler still takes _stubs_lock exactly once.
// At most one bounded shared-lock attempt returns the stub view
// plus the precomputed aarch64 unwind info (null on x64). When
// the lookup fails, the nmethod's own start and name are used.
const void* start;
const char* name;
const StubUnwindInfo* stub_info = nullptr;
Expand All @@ -773,9 +773,10 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
name = nm->name();
}
#else
CodeBlob* stub = JitCodeCache::findRuntimeStub(walk_pc.raw());
start = stub != NULL ? stub->_start : nm->code();
name = stub != NULL ? stub->_name : nm->name();
if (!JitCodeCache::findRuntimeStub(walk_pc.raw(), &start, &name)) {
start = nm->code();
name = nm->name();
}
#endif

if (details) {
Expand Down Expand Up @@ -1170,10 +1171,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
#ifdef __aarch64__
if (JitCodeCache::findRuntimeStubInfo((const void *)frame.pc(), &stub_start, &stub_name, &stub_info)) {
#else
CodeBlob *stub = JitCodeCache::findRuntimeStub((const void *)frame.pc());
if (stub != NULL) {
stub_start = stub->_start;
stub_name = stub->_name;
if (JitCodeCache::findRuntimeStub((const void *)frame.pc(), &stub_start, &stub_name)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A failed lookup also changes how the frame is unwound

When this returns false because the lock is busy, the code falls through to the CodeHeap::findNMethod() branch. That branch finds the stub blob and calls frame.unwindStub(NULL, nmethod->name()), with no stub start and no StubUnwindInfo. On aarch64 that means the old heuristics instead of the precomputed table, and jitCodeCache.cpp describes those heuristics as able to read a stale x30 slot. The walkVM path (L771) also continues with stub_info == nullptr.

So a busy lock can produce a wrong caller frame, not just a missing or less specific frame name. Inside the short registration window that is acceptable, but the reader-contention case in the other comment makes it happen more often. Fixing that comment is enough. It would still be worth saying in the PR description and the header comment that the fallback is a degraded unwind.

#endif
if (cstack != CSTACK_NO) {
max_depth -= makeFrame(trace.frames++, BCI_NATIVE_FRAME, stub_name);
Expand Down
Loading
Loading