Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b507f77
PROF-16213: Account CTimer timer table in NativeMem
zhengyu123 Oct 27, 2026
d727893
PROF-16213: Harden ctimer_ut failure-path coverage
zhengyu123 Oct 27, 2026
83d5e21
PROF-16213: Clarify where NativeMem categories are defined
zhengyu123 Oct 27, 2026
0d93361
PROF-16213: Keep the native-thread hook off engines during start/stop
zhengyu123 Oct 27, 2026
54689c5
PROF-16213: Drain in-flight hook calls and open the gate after CPU start
zhengyu123 Oct 27, 2026
9d581c5
PROF-16213: Open thread registration before engines list threads
zhengyu123 Oct 27, 2026
20f2d28
PROF-16213: Keep PerfEvents unregistration open while idle; simplify …
zhengyu123 Oct 27, 2026
f79c1cc
PROF-16213: Fail PerfEvents start on table allocation failure; test r…
zhengyu123 Oct 27, 2026
eb6b11f
PROF-16213: Disable cancellation in hook calls; guard resetBuffer; te…
zhengyu123 Oct 27, 2026
ac310d7
PROF-16213: Restore thread cancellation outside destructor-bearing sc…
zhengyu123 Oct 27, 2026
ae8c8ed
PROF-16213: Gate JVMTI thread callbacks; prove cancellation disable; …
zhengyu123 Oct 28, 2026
3839410
Fix
zhengyu123 Oct 28, 2026
e4ecdd1
PROF-16213: Document the thread-registration contract in AGENTS.md
zhengyu123 Oct 28, 2026
08375d3
PROF-16213: Enable the CPU engine only if it started
zhengyu123 Oct 29, 2026
a24bc50
sphinx: address review feedback on PR #912
jbachorik Oct 29, 2026
f95195e
PROF-16213: Fix ctimer_ut compilation on Linux
zhengyu123 Oct 29, 2026
404fc14
PROF-16213: Fix ResizeReleasesOldSlots on musl
zhengyu123 Oct 29, 2026
fd7482f
PROF-16213: Skip calloc-overflow tests in perfEvents_ut under sanitizers
zhengyu123 Oct 29, 2026
2de3fa9
PROF-16213: Freeze thread unregistration only around a table replacement
zhengyu123 Oct 29, 2026
688e368
PROF-16213: Defer PerfEvents table changes to PROF-16359
zhengyu123 Oct 30, 2026
08c964e
PROF-16213: Restore perfEvents_ut.cpp from main
zhengyu123 Oct 30, 2026
9de032e
PROF-16213: Admit only the CPU engine through the registration gate
zhengyu123 Oct 30, 2026
3a00a33
PROF-16213: Keep check() off the engine slots; fix stale docs; test C…
zhengyu123 Oct 30, 2026
ad06a06
PROF-16213: Harden engine changes, gate timeouts and PerfEvents table…
zhengyu123 Oct 30, 2026
73f0255
PROF-16213: Correct check() preconditions; add check() JVM test
zhengyu123 Oct 31, 2026
e389e30
PROF-16213: Count every registration call; drop the registration pre-…
zhengyu123 Oct 31, 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
38 changes: 37 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ before working on a subsystem.
| String dictionary (arena chunks, rotation/clear protocols) | [doc/architecture/StringDictionary.md](doc/architecture/StringDictionary.md) |
| RefCountGuard (lock-free RAII reader-drain before reclamation) | [doc/architecture/RefCountGuard.md](doc/architecture/RefCountGuard.md) |
| TLS context (thread-local tracing context, distributed tracing) | [doc/architecture/TLSContext.md](doc/architecture/TLSContext.md) |
| Profiling engines (CPU, wall-clock, allocation, native memory, liveness) | [doc/architecture/ProfilingEngines.md](doc/architecture/ProfilingEngines.md) |
| Profiling engines (CPU, wall-clock, allocation, native memory, liveness; thread-registration gates) | [doc/architecture/ProfilingEngines.md](doc/architecture/ProfilingEngines.md) |
| JFR integration (FlightRecorder, metadata, constant pools, buffers) | [doc/architecture/JfrIntegration.md](doc/architecture/JfrIntegration.md) |
| Live-heap reference chains (implementation, signal scheduling, design) | [doc/architecture/LiveHeapReferenceChainsImplementation.md](doc/architecture/LiveHeapReferenceChainsImplementation.md), [doc/architecture/ReferenceChainsSignalsExplained.md](doc/architecture/ReferenceChainsSignalsExplained.md), [doc/ReferenceChainsDesign.md](doc/ReferenceChainsDesign.md), [doc/ReferenceChainsCollectionSummary.md](doc/ReferenceChainsCollectionSummary.md) |
| Native memory profiling (malloc/free interception, sampling) | [doc/architecture/NativeMemoryProfiling.md](doc/architecture/NativeMemoryProfiling.md) |
Expand Down Expand Up @@ -272,6 +272,42 @@ Otherwise a signal can sample a half-torn-down thread, or re-acquire a
thread that keeps running silently stops profiling that thread — use `SignalBlocker`
for a temporary window.

**Thread registration.** The `pthread_create` hook and the JVMTI `ThreadStart`/
`ThreadEnd` callbacks run without `_state_lock` and can overlap `Profiler::start()`/
`stop()`, while engines size, replace or tear down their per-thread tables (`CTimer`'s
`_timers`, `PerfEvents`' `_events`). So:
- reach the engines only through `Profiler::registerThread()`/`unregisterThread()`,
never `_cpu_engine->registerThread()` directly. These calls check a gate
(`_engines_ready` / `_thread_tables_stable`), count themselves in flight
(`HookCallScope`), and run with thread cancellation disabled;
- an engine with a per-thread table sizes it first, then calls
`Profiler::openThreadRegistration()`, then lists the existing threads. Free or
replace a table only while its gate is closed: closing a gate
(`closeThreadRegistration()`, `ThreadTablesFreeze`) waits until no call is inside an
engine. Each gate counts its calls separately: `closeThreadRegistration()` waits only
for registration calls, because unregistration stays open during `stop()`;
`ThreadTablesFreeze` waits for both. The wait is bounded; when it reports failure,
free or replace no table. A `ThreadTablesFreeze` timeout fails the operation; a
`closeThreadRegistration()` timeout puts the profiler in `ERROR` (`stop()` still
completes, since it frees no table), so no later `start()` replaces a table under
the stuck call. Engine
resources kept across `stop()` are released (`Engine::releaseThreads()`) when another
engine replaces the CPU engine. Known exception: `PerfEvents::start()` still
replaces `_events` without a gate (PROF-16359). `Profiler::startCpuEngine()` opens the
registration gate once any CPU engine started, so the gate is open iff it did, and it
admits only the CPU engine. A wall engine that needs `registerThread()` needs its own
gate: opening this one would also admit a CPU engine that failed;
- an engine's `stop()` must tolerate a concurrent `unregisterThread()`: `stop()` closes
only the registration gate, and unregistration stays open while the engines stop;
- close unregistration (`ThreadTablesFreeze`) only around an actual table replacement
or a CPU engine change, never for a whole `start()`: an exit dropped while it is closed leaves a stale slot
that a thread reusing the tid cannot claim, and `start()`/`stop()` cycles repeat;
- restore the cancellation state outside every destructor-bearing scope, never from a
destructor: current glibc acts on a pending asynchronous cancellation as soon as it
is re-enabled, and unwinding out of an implicitly `noexcept` destructor terminates.

Details: [ProfilingEngines.md](doc/architecture/ProfilingEngines.md#thread-registration).

**JVMTI callbacks.** Callbacks run on whichever JVM thread triggers the event —
including compiler, VM and GC threads that never went through our `pthread_create`
hook — so call `ProfiledThread::initCurrentThreadSignalSafe()` first.
Expand Down
10 changes: 10 additions & 0 deletions ddprof-lib/src/main/cpp/ctimer.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright 2017 Andrei Pangin
* 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.
Expand All @@ -25,6 +26,8 @@
#include <signal.h>

class CTimer : public Engine {
friend class CTimerTestAccessor;

private:
// cppcheck-suppress unusedPrivateFunction
static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext);
Expand All @@ -41,6 +44,13 @@ class CTimer : public Engine {
static int _max_timers;
static int *_timers;

static bool resizeTimers(int max_timers);
// Sizes the timer table for start(), replacing it under a
// Profiler::ThreadTablesFreeze only when the size changes.
static Error sizeTimerTable(int max_timers);
// start() with the timer table size passed in rather than read from
// pid_max, so a test can drive the allocation-failure path.
Error startWithTableSize(Arguments &args, int max_timers);
int registerThread(int tid);
void unregisterThread(int tid);

Expand Down
66 changes: 59 additions & 7 deletions ddprof-lib/src/main/cpp/ctimer_linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "debugSupport.h"
#include "libraries.h"
#include "log.h"
#include "nativeMem.h"
#include "profiler.h"
#include "samplerPerf.h"
#include "signalCookie.h"
Expand Down Expand Up @@ -54,7 +55,7 @@ bool CTimer::_enabled = false;
int CTimer::_signal;

int CTimer::registerThread(int tid) {
if (tid >= _max_timers) {
if (tid < 0 || tid >= _max_timers) {
Log::warn("tid[%d] > pid_max[%d]. Restart profiler after changing pid_max",
tid, _max_timers);
return -1;
Expand Down Expand Up @@ -119,7 +120,7 @@ int CTimer::registerThread(int tid) {
}

void CTimer::unregisterThread(int tid) {
if (tid >= _max_timers) {
if (tid < 0 || tid >= _max_timers) {
return;
}
// Atomic acquire to avoid possible leak when unregistering
Expand All @@ -131,6 +132,51 @@ void CTimer::unregisterThread(int tid) {
}

@jbachorik jbachorik Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Sphinx review — MEDIUM · ddprof-lib/src/main/cpp/ctimer_linux.cpp:123 · Fixed in a24bc50cc — verified against the full gtest suite

Finding — The bound checks added as the safety net for a failed/empty table reject only tids at or above the bound; a negative tid passes tid >= _max_timers and tid >= _max_events and indexes before the table. CTimer::registerThread (ctimer_linux.cpp:58) would CAS-write _timers[-k], CTimer::unregisterThread (line 129) would atomically load and CAS before the array, and PerfEvents::unregisterThread (perfEvents_linux.cpp:715/719) dereferences _events[negative]. The new comments explicitly claim these checks 'reject every tid' (ctimer_linux.cpp:140-142, perfEvents_linux.cpp:971-974), which is only true for tid >= 0. The new eventFor() does check tid < 0, so the PR itself treats negative tids as a real input class, but the register/unregister paths were not given the same half of the check.

Evidence

if (tid >= _max_timers) {
    return;
  }

Suggestion — Add the missing lower-bound half to all four one-sided checks (CTimer::registerThread ctimer_linux.cpp:58, CTimer::unregisterThread ctimer_linux.cpp:123, PerfEvents::registerThread, PerfEvents::unregisterThread perfEvents_linux.cpp:715), matching eventFor()'s tid < 0 guard.

Anchor note: the bound check (123) sits outside the diff context of the reviewed head.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a24bc50. CTimer::registerThread(), CTimer::unregisterThread(), PerfEvents::registerThread() and PerfEvents::unregisterThread() all check tid < 0 || tid >= bound. Covered by NegativeTidIsRejected in ctimer_ut.cpp and perfEvents_ut.cpp.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Update: the PerfEvents half is reverted in 688e368 and deferred to PROF-16359. The CTimer::registerThread()/unregisterThread() checks stay, covered by NegativeTidIsRejected in ctimer_ut.cpp.

}

// Sizes the tid-indexed timer table and accounts it under NM_CTIMER. Called
// from sizeTimerTable() under a Profiler::ThreadTablesFreeze, so no
// thread-hook call (pthread hook or JVMTI ThreadStart/ThreadEnd) can be inside
// the table while it is replaced.
//
// On allocation failure the table is left empty (_max_timers == 0), so the tid
// bound checks reject every tid instead of indexing a NULL table, and the next
// start() retries. The table currently held always has _max_timers slots, so
// that is the size released from NM_CTIMER.
bool CTimer::resizeTimers(int max_timers) {
if (max_timers == _max_timers) {
return true;
}
free(_timers);
NativeMem::record(NM_CTIMER, -(long long)((size_t)_max_timers * sizeof(int)));
_timers = (int *)calloc(max_timers, sizeof(int));
if (_timers == NULL) {
_max_timers = 0;
return false;
}
_max_timers = max_timers;
NativeMem::record(NM_CTIMER, (long long)((size_t)max_timers * sizeof(int)));
return true;
}

// A restart with an unchanged pid_max keeps the table and never closes thread
// unregistration, so an exit during start() still releases its timer. Only an
// actual replacement freezes the hook calls. If one is still inside the table
// after the wait budget, start() fails and keeps the old table; whether a
// later start() can succeed depends on the stuck call (see
// Profiler::startCpuEngine()).
Error CTimer::sizeTimerTable(int max_timers) {
if (max_timers == _max_timers) {
return Error::OK;
}
Profiler::ThreadTablesFreeze freeze(Profiler::instance());
if (!freeze.drained()) {
return Error("thread hook calls did not drain");
}
if (!resizeTimers(max_timers)) {
return Error("Failed to allocate CPU timer table");
}
return Error::OK;
}

Error CTimer::check(Arguments &args) {
timer_t timer;
if (timer_create(CLOCK_THREAD_CPUTIME_ID, NULL, &timer) < 0) {
Expand All @@ -142,6 +188,10 @@ Error CTimer::check(Arguments &args) {
}

Error CTimer::start(Arguments &args) {
return startWithTableSize(args, OS::getMaxThreadId());
}

Error CTimer::startWithTableSize(Arguments &args, int max_timers) {
if (args._interval < 0) {
return Error("interval must be positive");
}
Expand All @@ -150,11 +200,9 @@ Error CTimer::start(Arguments &args) {
_cstack = args._cstack;
_signal = SIGPROF;

int max_timers = OS::getMaxThreadId();
if (max_timers != _max_timers) {
free(_timers);
_timers = (int *)calloc(max_timers, sizeof(int));
_max_timers = max_timers;
Error error = sizeTimerTable(max_timers);
if (error) {
return error;
}

// Prime the origin-check cache from this non-signal context before any
Expand All @@ -165,6 +213,10 @@ Error CTimer::start(Arguments &args) {

OS::installSignalHandler(_signal, signalHandler);

// The table is sized: open the hook gate before listing threads (see
// Profiler::openThreadRegistration()).
Profiler::instance()->openThreadRegistration();

// Register all existing threads. Individual failures are benign — a thread
// may exit between listThreads() and registerThread(), and new threads
// will register themselves on creation. check() already validated that the
Expand Down
5 changes: 5 additions & 0 deletions ddprof-lib/src/main/cpp/engine.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright 2017 Andrei Pangin
* 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.
Expand Down Expand Up @@ -53,6 +54,10 @@ class Engine {

virtual int registerThread(int tid) { return -1; }
virtual void unregisterThread(int tid) {}
// Releases the per-thread resources the engine keeps across stop(). Called
// when another engine replaces it in its Profiler slot: thread exits no
// longer reach it after that, so nothing else would release them.
virtual void releaseThreads() {}

virtual void enableEvents(bool enabled) {
// do nothing
Expand Down
24 changes: 20 additions & 4 deletions ddprof-lib/src/main/cpp/nativeMem.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,32 @@
// per-category live byte gauges partition the total: sum(category) == total,
// with no double counting.
//
// These gauges track memory that is actually touched (and therefore resident),
// not address space that has merely been reserved. NM_CALLTRACE in particular
// counts the bytes LinearAllocator::alloc() has bump-allocated, NOT the
// capacity of the 8 MiB chunks backing them -- see linearAllocator.cpp.
// With the exceptions below, these gauges track memory that is actually touched
// (and therefore resident), not address space that has merely been reserved.
// NM_CALLTRACE in particular counts the bytes LinearAllocator::alloc() has
// bump-allocated, NOT the capacity of the 8 MiB chunks backing them -- see
// linearAllocator.cpp.
//
// Exceptions: the tid-indexed tables of CTimer (NM_CTIMER,
// pid_max * sizeof(int)) and PerfEvents (part of NM_PERF,
// pid_max * sizeof(PerfEvent), two cache lines per slot) are counted at their
// full calloc'd size. That memory is mostly untouched address space -- only the
// pages holding live tids become resident -- so for these two categories the
// gauge overstates RSS and should be read as an upper bound. NM_PERF's
// per-thread ring pages (2 pages each) are touched and resident.
//
// Consequently NM_CALLTRACE is no longer an "arena reserved" figure with
// CALLTRACE_STORAGE_BYTES as its used slice: both now count touched bytes, and
// their small difference is the call-trace hash tables (also bump-allocated),
// not chunk slack. Do not compute arena waste as the difference between them.
// DICTIONARY_ARENA_WASTE_BYTES remains a genuine nested waste figure for the
// DICTIONARY arena. None of these nested counters are summed in here.
//
// The exported per-category counter names (native_mem_{live,avg,max,
// chunk_overhead,post_flush_live,post_flush_max}_bytes.<category>, written by
// Recording::writeNativeMem()) derive from this table, and the set grows when
// a category is added, so consumers should match native_mem_*_bytes. counters
// by prefix rather than hard-code the category list.
#define DD_NATIVE_MEM_CATEGORY_TABLE(X) \
X(CALLTRACE, "calltrace") \
X(DICTIONARY, "dictionary") \
Expand All @@ -38,6 +53,7 @@
X(METHOD_MAP, "method_map") \
X(LIVENESS, "liveness") \
X(WALLCLOCK, "wallclock") \
X(CTIMER, "ctimer") \

@jbachorik jbachorik Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Sphinx review — MEDIUM · ddprof-lib/src/main/cpp/nativeMem.h:41 · Partially fixed in a24bc50cc

Status — In-repo, the new category is exported end-to-end; what remains is verification outside this repo: consumers that hard-code the native_mem_* category list (agent-side metric extraction, dashboards, alerting) were not checked — if they validate the tag set the new value may be rejected, if they expect a fixed list the new series is silently dropped. Also note the ctimer live gauge covers the full tid-indexed table (mostly untouched virtual address space), so it can overstate real RSS, unlike perf (ring pages, mostly resident).

Finding — The new NM_CTIMER category changes the externally observable counter-name set: every JFR chunk now exports native_mem_bytes.ctimer (live/avg/max), and the category count grows from 13 to 14. In-repo consumers that enumerate DD_NATIVE_MEM_CATEGORY_TABLE are updated automatically, but downstream consumers of the native_mem counter series that hard-code the category list (agent-side metric extraction, dashboards, or alerting keyed on the fixed category names) will silently miss the new series or, if they validate the tag set, see an unexpected value. The PR description acknowledges this ('Consumers with a fixed category list need the new name') but no verification of such consumers exists in this repository, so the compatibility surface is unverified. Additionally, unlike NM_PERF (which reports ring pages that are mostly resident), the full tid-indexed table is largely untouched virtual address space, so the live gauge for ctimer overstates real RSS; this is documented.

Evidence

X(CTIMER, "ctimer")                                                          \

Suggestion — Confirm with the agent/dashboard owners that native_mem_*_bytes consumers discover categories dynamically (or add the ctimer series to their fixed list) before merging, and document in ProfilingEngines.md that the ctimer gauge is allocated address space rather than resident RSS (already partially done in the PR body; mirror it in the NativeMem section of the doc).

Author decision needed — confirm with metric/dashboard/alert owners that the new ctimer name is handled.

X(MISC, "misc")

#define X_NM_ENUM(a, b) NM_##a,
Expand Down
1 change: 1 addition & 0 deletions ddprof-lib/src/main/cpp/perfEvents.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class PerfEvents : public Engine {

virtual int registerThread(int tid);
virtual void unregisterThread(int tid);
void releaseThreads();
long interval() const { return _interval; }

const char *name() { return "PerfEvents"; }
Expand Down
47 changes: 43 additions & 4 deletions ddprof-lib/src/main/cpp/perfEvents_linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -962,16 +962,29 @@ Error PerfEvents::start(Arguments &args) {
// sites); capture the old size first since _max_events is overwritten below.
long long old_bytes =
(_events != NULL) ? (long long)((size_t)_max_events * sizeof(PerfEvent)) : 0;
// The table is the only record of which fd and ring belong to which tid
// (stop() keeps them). Release them while it still exists; otherwise
// they leak, and stay armed, once it is freed.
releaseThreads();
free(_events);
if (old_bytes > 0) {
NativeMem::record(NM_PERF, -old_bytes);
}
_events = (PerfEvent *)calloc(max_events, sizeof(PerfEvent));
_max_events = max_events;
if (_events != NULL) {
NativeMem::record(NM_PERF,
(long long)((size_t)max_events * sizeof(PerfEvent)));
if (_events == NULL) {
// Back to the not-started value, so registerThread() and
// unregisterThread() reject every tid. Return before the signal
// handler, the hook and the registration gate are set up: start()
// fails, so the CPU engine is neither registered nor enabled for this
// recording. The next start() retries the allocation.
_max_events = -1;
Log::error("perfEvents: could not allocate the table for %d threads; "
"PerfEvents is disabled for this recording", max_events);
return Error("Failed to allocate perf event table");
}
_max_events = max_events;
NativeMem::record(NM_PERF,
(long long)((size_t)max_events * sizeof(PerfEvent)));
}

OS::installSignalHandler(SIGPROF, signalHandler);
Expand All @@ -980,6 +993,10 @@ Error PerfEvents::start(Arguments &args) {
__atomic_store_n(_pthread_entry, (void *)pthread_setspecific_hook,
__ATOMIC_RELEASE);

// _events is sized: open the hook gate before listing threads (see
// Profiler::openThreadRegistration()).
Profiler::instance()->openThreadRegistration();

// Create perf_events for all existing threads
int threads_sz = 0, threads_cap;
int *threads = (int *)malloc((threads_cap = 1024) * sizeof(int));
Comment thread
jbachorik marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -1052,12 +1069,28 @@ void PerfEvents::stop() {
// check whether the thread has been registered already on start.
}

// stop() keeps every fd and ring, and only unregisterThread() releases them
// when a thread exits. Releases them all when nothing else could:
// Profiler::replaceCpuEngine() calls this under a ThreadTablesFreeze once
// another CPU engine replaces this one, and start() calls it before it frees
// the table on a pid_max change. The start() call runs without a freeze, so a
// concurrent unregisterThread() of the same tid is still possible there
// (PROF-16359).
void PerfEvents::releaseThreads() {
for (int tid = 0; tid < _max_events; tid++) {
unregisterThread(tid);
}
}

int PerfEvents::walkKernel(int tid, const void **callchain, int max_depth,
StackContext *java_ctx) {
if (!(_ring & RING_KERNEL)) {
// we are not capturing kernel stacktraces
return 0;
}
if (_events == NULL || tid < 0 || tid >= _max_events) {
return 0; // no table: the allocation failed (see start())
}

PerfEvent *event = &_events[tid];
if (!event->tryLock()) {
Expand Down Expand Up @@ -1148,6 +1181,12 @@ int PerfEvents::walkKernel(int tid, const void **callchain, int max_depth,
}

void PerfEvents::resetBuffer(int tid) {
// Fds left armed by an earlier recording keep delivering SIGPROF, and the
// handler calls this on every exit, also after a failed table allocation
// left _events NULL (_max_events -1).
if (_events == NULL || tid < 0 || tid >= _max_events) {
return;
}
PerfEvent *event = &_events[tid];
if (!event->tryLock()) {
return; // the event is being destroyed
Expand Down
Loading
Loading