- cpu profiling;
oncpu=Trueskips unwinding idle threads and uploads cpu only,oncpu=Falseuploads cpu+wall - fast copy (safe_memcpy) only, with no syscall copy fallback and no warmup; its SIGSEGV/SIGBUS handlers install from
stack::start, not at import, andconfigure()returnsFalsewhen they cannot be installed - no faulthandler patch: enable faulthandler before
configure(); a laterfaulthandler.enable()replaces our handlers and the sampler stops - thread registration via
threading - fork child and restart handling
configure_experimental_stack_profiler(max_nframe, max_threads, adaptive_*). - adaptive sampling, on by default;
sample_rateonly sets the starting interval
- Labels and thread/task info
- dropped-frame counts
samples/count- native frames
- GC frames
- free-threaded CPython.
- asyncio/uvloop task unwinding and
max_tasks - gevent/greenlets.
- A component that replaces our SIGSEGV/SIGBUS handlers after start stops the sampler, but a fault in
safe_memcpybefore the sampler loop sees the change reaches the foreign handler. - Embedded interpreters also install our SIGSEGV/SIGBUS handlers on top of the host's.
- Sampler::stop() timeout leads to a data race against reset_string_cache / interner::clear
- Thread registration releases the GIL, allowing thread ID reuse to overwrite a newer registration with a stale native ID and lose CPU samples on Linux; this port's startup snapshot widens the race window.
- Threads that pass
_set_native_idbefore patch installation but are still inthreading._limboduring the startup snapshot miss registration entirely and produce no CPU or wall samples.
Squashed from DataDog/dd-trace-py#18724 at e16880aa5845d5c9688dbb31a80edf1d6d25a233 (submodule commit 2f90793c1). Not in upstream main yet. Local changes in cpu_timer.cpp: oncpu is passed to render_cpu_sample_begin, and Engine::postfork_child() no longer re-arms the engine, since the child never restarts the sampler and at_fork_after_in_child shuts it down anyway.
- ASAN: the wall sampler, which still runs next to the timer, reads other threads' stale frames (heap-use-after-free in freed generators, stack-use-after-return on returned
entryframes, both inFrame::read), and both the wall path and the timer'sdrain_stateover-read short name strings inpyunicode_to_utf8(heap-buffer-overflow); the SIGPROF handler itself has no reports. - CPU timer state consumes more than 500 KiB of resident memory per registered thread: every
CaptureStateembeds a 64-slot ring and every slot reserves space for 512 frames, regardless ofmax_nframeormax_threads. Because the timer is enabled by default, thread-heavy processes can consume hundreds of MiB solely for these rings (roughly 515 MiB at 1,000 threads). - The CPU timer engine cannot restart:
Engine::shutdown()setsstopped_once, after whichEngine::start()always returnsfalse. Bothshutdown()followed byconfigure()andconfigure()in a fork child (at_fork_after_in_childcallsEngine::shutdown()) would otherwise report no CPU samples, so with the timer onSampler::start()now fails whenEngine::start()does, andconfigure()returnsFalsewith an error log. - Fixed:
Engine::postfork_child()matches the surviving thread by native TID, which changes across fork, so it used to unmap that thread's profiler-owned alternate signal stack while it was still registered, and an unrelatedSA_ONSTACKsignal then crashed the child. Each live, retired and leaked mapping is now unmapped only ifsigaltstackreports a registration outside it; the mapping the child still has registered, or every mapping if the query fails, is left mapped. Application-owned stacks are never touched. Remaining: the inherited stack stays registered and leaks in the child; the parent still leaks the stacks of threads retired from another thread;disable_current_thread_altstack()still ignoressigaltstackfailures.