refactor(profiling): Signal handler boilerplate - #756
Conversation
…indowIfNeeded Adds a narrowly-scoped RAII guard (guards.h) for the setSighandlerTid(tid)/setSighandlerTid(-1) span and a shared tickInitWindowIfNeeded() helper (jvmThread.h), then applies both to the five duplicated signal handlers in ctimer_linux.cpp, itimer.cpp and wallClock.cpp. Also fixes two pre-existing errno-restore gaps: CTimer::signalHandler's !cs.entered()/!_enabled early returns, and WallClockASGCT::signalHandler, which previously never saved/restored errno at all.
saved_errno was captured after the CriticalSection entry check, so the !cs.entered() bail-out path left errno unrestored. Move the save to the top of the handler, matching WallClockASGCT.
…ope, null assert) to ITimer and PerfEvents Brings ITimer::signalHandler and PerfEvents::signalHandler in line with CTimer/WallClock: save/restore errno across every return path, scope SighandlerTidScope narrowly around the recordSample span, and assert current != nullptr. ITimerJvmti was already fully migrated. Deliberately excludes tickInitWindowIfNeeded: unlike CTimer/WallClock, neither engine had this check before, and both lack the signal-origin validation those engines gate it behind, so adding it is a behavior change that needs separate review, not a boilerplate refactor.
Scan-Build Report
Bug Summary
Reports
|
||||||||||||||||||||||||||||||||||||
CI Test ResultsRun: #33885297947 | Commit:
Status Overview
Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled Summary: Total: 32 | Passed: 32 | Failed: 0 Updated: 2026-09-04 14:59:52 UTC |
This comment has been minimized.
This comment has been minimized.
|
One small typo to fix, otherwise looks good! |
Co-authored-by: Jaroslav Bachorik <jaroslav.bachorik@datadoghq.com>
There was a problem hiding this comment.
Pull request overview
Refactors duplicated boilerplate across multiple profiling signal handlers in the native (C++) profiler to reduce duplication and make handler cleanup (notably errno restore and sighandler TID bookkeeping) more consistent and less error-prone.
Changes:
- Introduces
SighandlerTidScope(RAII) to replace manualsetSighandlerTid(tid)/setSighandlerTid(-1)pairs in multiple handlers. - Extracts the “init-window tick-and-return” logic into
tickInitWindowIfNeeded(ProfiledThread*)and reuses it in the original CPU/wall handler set. - Expands and fixes
errnosave/restore coverage in several signal handlers and preventsPerfFdRearmGuarddestruction from clobbering restorederrno.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| ddprof-lib/src/main/cpp/wallClock.cpp | Adds errno save/restore and switches to SighandlerTidScope; uses shared init-window helper. |
| ddprof-lib/src/main/cpp/perfEvents_linux.cpp | Adds errno save/restore in the perf-events signal handler and fixes PerfFdRearmGuard destructor to preserve errno. |
| ddprof-lib/src/main/cpp/jvmThread.h | Adds tickInitWindowIfNeeded() helper and required include for ProfiledThread. |
| ddprof-lib/src/main/cpp/itimer.cpp | Adds errno save/restore and SighandlerTidScope in ITimer handler; uses shared init-window helper in JVMTI variant. |
| ddprof-lib/src/main/cpp/guards.h | Adds SighandlerTidScope RAII guard for sighandler TID management. |
| ddprof-lib/src/main/cpp/ctimer_linux.cpp | Uses shared init-window helper and SighandlerTidScope; fixes missing errno restore on early returns. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- guards.h: clarify SIGNAL_HANDLER_GUARD_OR_DROP() doc to note that guards needing cleanup on the drop path (e.g. PerfFdRearmGuard) must be declared before the macro too. - jvmThread_ut.cpp: assert the key-discovery scan finds the right key, and fix the resulting flaky test by using a monotonically unique marker instead of `this` (whose address gets reused across test instances, colliding with never-deleted stale keys from earlier tests). - jvmThread.h: drop the heavy threadLocalData.h include added only to support tickInitWindowIfNeeded(); move that helper to threadLocalData.inline.h instead, where ProfiledThread is already available, keeping jvmThread.h forward-declaration-only again.
There was a problem hiding this comment.
🟡 Changes recommended
The new gtest repeatedly creates pthread TLS keys without deletion, which diverges from established test conventions and can lead to pthread-key exhaustion over time.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 1
- Review effort level: Lite
TickInitWindowTest::SetUp() created a fresh pthread_key_create every test and never deleted it, diverging from the binary-lifetime-static key convention in threadLocal_ut.cpp and risking pthread-key exhaustion under a harness that repeats fixtures in-process (e.g. --gtest_repeat). Fix by creating one shared key once via sharedTestKey(), reused for the life of the binary; each test still gets its own unique marker value to drive/verify the key-discovery scan.
There was a problem hiding this comment.
🟡 Changes recommended
The new unit test currently generates “unique” TLS markers via integer-to-pointer casts, which is implementation-defined and can produce sanitizer/portability failures.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 1
- Review effort level: Lite
The unit test for tickInitWindowIfNeeded() had grown a rig -- a
binary-lifetime pthread key, a unique marker generator, a
JVMThreadTestAccessor, and a `friend` declaration in jvmThread.h -- whose
only purpose was to cover one line:
tickInitWindowIfNeededImpl(JVMThread::current() != nullptr, current)
That trade doesn't hold up. The Impl split already exists so the branch
logic is testable without a JVM; paying for the seam *and* for the fakery
the seam was meant to avoid is paying twice. The rig also mutated
process-global state (JVMThread::_jvm_thread ended up pointing at a
test-owned key for the binary's lifetime) and leaned on
ThreadLocal<JVMThread*>::initialize()'s scan over every live pthread key,
which is what forced the unique-marker machinery in the first place.
Keep four tests over tickInitWindowIfNeededImpl() -- all four
has_jvm_thread x inInitWindow() combinations, including the one that
catches && mutated to ||, plus the one-shot property -- and drop
everything else. The derivation from JVMThread::current() now rests on
compilation and inspection; no integration test exercises the
init-window path either, so the header says that outright rather than
implying coverage that doesn't exist.
jvmThread.h is byte-identical to origin/main again: the PR no longer
touches a production header for test access. Renamed the test to
tickInitWindow_ut.cpp to match what it tests (tickInitWindowIfNeededImpl
lives in threadLocalData.inline.h, not jvmThread.h) and fixed the stale
filename pointer in threadLocalData.inline.h.
Also adds a direct #include <cassert> to itimer.cpp, found while
reviewing: the assert(current != nullptr) added to ITimer::signalHandler
earlier in this branch compiled only via the threadLocalData.inline.h ->
jvmThread.h -> threadLocal.h chain this same branch introduced, so
trimming either include would have broken the build. ctimer_linux.cpp
and wallClock.cpp already include it directly.
There was a problem hiding this comment.
🔵 Needs a closer look
The changes touch low-level async-signal-handler behavior across multiple engines, so despite looking mechanically correct, they warrant human review and full runtime/regression testing before approval.
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 0 new
- Review effort level: Lite
rkennke
left a comment
There was a problem hiding this comment.
I did a manual review pass, and only found very minor problems. I also ran an AI assisted/Sphinx review pass which found more .. but see the comments there.
…er tests - Move into the SighandlerTidScope block in the four handlers flagged; rename the guard local to snake_case at all seven sites. - Add the missing assert(current != nullptr) to CTimerJvmti::signalHandler. - guards.h: note the siglongjmp caveat and SighandlerTidScope's non-nesting precondition; trim the ErrnoPreserver comment to its forward contract. - perfEvents: declare ErrnoPreserver below the si_code <= 0 fast bail (still ahead of PerfFdRearmGuard) so external signals don't touch errno. - threadLocalData.inline.h: plain instead of , assert the non-null precondition, drop the ticket ID, state the ErrnoPreserver contract as a caller requirement, and short-circuit on inInitWindow() so the TLS lookup stays off the hot path. - Record at the ITimer/PerfEvents sites why the init-window guard is absent. - Add guards_ut.cpp for ErrnoPreserver; give tickInitWindow_ut.cpp the standard gtest crash-handler rig.
…er tests - Move "int tid" into the SighandlerTidScope block in the four handlers flagged; rename the guard local to snake_case at all seven sites. - Add the missing assert(current != nullptr) to CTimerJvmti::signalHandler. - guards.h: note the siglongjmp caveat and SighandlerTidScope's non-nesting precondition; trim the ErrnoPreserver comment to its forward contract. - perfEvents: declare ErrnoPreserver below the si_code <= 0 fast bail (still ahead of PerfFdRearmGuard) so external signals don't touch errno. - threadLocalData.inline.h: plain "inline" instead of "static inline", assert the non-null precondition, drop the ticket ID, state the ErrnoPreserver contract as a caller requirement, and short-circuit on inInitWindow() so the TLS lookup stays off the hot path. - Record at the ITimer/PerfEvents sites why the init-window guard is absent. - Add guards_ut.cpp for ErrnoPreserver; give tickInitWindow_ut.cpp the standard gtest crash-handler rig.
eac7bd8 to
6268c57
Compare
The short-circuit on inInitWindow() dereferences current in the wrapper, ahead of the assert in tickInitWindowIfNeededImpl(), so production call paths lost the debug/ASan diagnostic that assert was added for.
rkennke
left a comment
There was a problem hiding this comment.
Looks good now, thank you! This is a great change!
What does this PR do?:
Extracts the boilerplate duplicated across the CPU/wall profiling signal
handlers (
CTimer::signalHandler,CTimerJvmti::signalHandler,ITimerJvmti::signalHandler,WallClockASGCT::signalHandler,WallClockJvmti::signalHandler) into two shared helpers, fixes severallatent errno-restore gaps found along the way, and extends the same
treatment to two handlers outside PROF-14748's original five
(
ITimer::signalHandler,PerfEvents::signalHandler):SighandlerTidScope(guards.h), a narrowly-scoped RAII guard aroundShims::instance().setSighandlerTid(tid)/setSighandlerTid(-1),replacing the manual set/reset pairs in all seven handlers.
tickInitWindowIfNeeded()(threadLocalData.inline.h), a sharedhelper for the 4-line init-window guard that was copied verbatim into the
original five handlers. It lives there because it bridges
JVMThread::current()and
ProfiledThread::inInitWindow()/tickInitWindow(), and that file alreadyowns
ProfiledThread's inline members; the new#include "jvmThread.h"addsno external headers, since
threadLocalData.halready pulled<jvmti.h>(which includes
jni.h) andthreadLocal.h. Split intotickInitWindowIfNeededImpl(bool has_jvm_thread, ProfiledThread*)plus athin wrapper that derives the bool, so the branch logic is unit testable
without a JVM (
JVMThread::current()asserts on an invalid pthread key). Deliberately not applied toITimer::signalHandlerorPerfEvents::signalHandler(see Additional Notes).CTimer::signalHandler's!_enabledearly return, which returned without restoringerrno(thethree Jvmti handlers already did this correctly on the equivalent path).
saved_errnosave/restore toWallClockASGCT::signalHandler,which previously didn't save/restore errno at all, unlike its sibling
WallClockJvmti::signalHandler.WallClockJvmti::signalHandler:saved_errnowas captured after theCriticalSectionentry check, so the!cs.entered()bail-out path lefterrnounrestored. The save now happensfirst, matching
WallClockASGCT.saved_errnosave/restore toITimer::signalHandlerandPerfEvents::signalHandler, neither of which had it before, and wraps theirrecordSamplespan inSighandlerTidScope(replacing the manualsetSighandlerTid/-1pair).assert(current != nullptr)ahead of thetidcomputation is added toITimer::signalHandleronly, mirroring theoriginal five handlers --
PerfEvents::signalHandleralready had it.ITimer::signalHandleralso gains a direct#include <cassert>: the newassert would otherwise have compiled only via the
jvmThread.h->threadLocal.hchain this PR introduced, so trimming an include wouldhave broken the build.
ITimerJvmti::signalHandlerwas already fullymigrated and needed no changes here.
PerfFdRearmGuard::~PerfFdRearmGuard()(
perfEvents_linux.cpp): itsioctl()/resetBuffer()calls could clobbererrnoon every exit path, including the drop paths, and nothing restoredit afterwards. Fixed by declaring
ErrnoPreserverinPerfEvents::signalHandlerahead ofPerfFdRearmGuard, so it destructslast — after
PerfFdRearmGuard— and restores whateverPerfFdRearmGuard'sside effects clobbered. The guard itself is deliberately not self-contained:
an earlier review pass asked for the nested
ErrnoPreserverinside~PerfFdRearmGuard()to be removed as redundant, and the handler-level one(declared just below the
si_code <= 0external-signal fast bail, so thatpath stays untouched) covers its only construction site.
Motivation:
PROF-14748 : these five handlers were identified during review of
the jvmtistacks addition as sharing identical boilerplate with no shared
abstraction.
Additional Notes:
ITimer::signalHandlerandPerfEvents::signalHandlerwere not named in PROF-14748'soriginal five. Both had the identical manual
set/reset-
setSighandlerTidpattern as their siblings and are now giventhe same
SighandlerTidScopetreatment, plus errno save/restore and acurrent != nullptrassert.tickInitWindowIfNeeded()was deliberately not wired into ITimer norPerfEvents handlers, unlike the original five. The reason is simply that
neither engine ever had this check: the init-window guard closes the race
between
Profiler::registerThread()andthread_native_entrypublishingthe JVM thread TLS, which is a property of the sampled thread and unrelated
to signal-origin validation (an earlier revision of this description claimed
otherwise — that rationale was wrong). Adding it here would be a behaviour
change to two engines in a PR that is otherwise a boilerplate extraction, so
it is left for separate review. A comment at each site now records that.
PerfEvents::signalHandler's existing control flow (the_enabledcheckgates only the
SighandlerTidScope/recordSampleblock, afternoteCPUSamplealready ran unconditionally) was left as-is; only errnohandling and the
SighandlerTidScope/assert changes were added.resolveThreadId(a candidate shared helper for thecurrent ? current->tid() : OS::threadId()duplication) was not extracted.The underlying duplication was eliminated directly instead: the original
five handlers now assert
current != nullptrbefore computingtid,making the ternary dead code, so it was removed rather than factored out.
(
ITimer::signalHandlerandPerfEvents::signalHandleralready computedtidfromcurrent->tid()directly, with no ternary to remove.) Thepattern still exists in
javaApi.cpp, a non-signal-handler context outsidethis ticket's scope.
critical-section semantics in any handler.
How to test the change?:
./gradlew :ddprof-lib:compileDebugcompiles cleanly on macOS and linux.ordering) and don't alter sampling logic or control flow beyond exit-path
cleanup.
tickInitWindowIfNeeded()is not wired intoITimer/PerfEvents(see Additional Notes), so no runtime sampling behavior changes for those
two engines.
ddprof-lib/src/test/cpp/guards_ut.cpp: three unit tests overErrnoPreserver-- restore on normal scope exit, restore on an earlyreturn, and the ordering invariant the handlers rest on (declared first, it
destructs last and wins over a later guard whose destructor clobbers
errno).SighandlerTidScopeis not covered: reading the sighandler tidback needs the
Shimssymbol a JVM provides, and this binary has none.ddprof-lib/src/test/cpp/tickInitWindow_ut.cpp: four unit tests overtickInitWindowIfNeededImpl()covering all fourhas_jvm_threadxinInitWindow()combinations, including the case thatwould break if
&&were mutated to||, plus the one-shot property (asecond call must not tick again). The one-line derivation in
tickInitWindowIfNeeded()itself (JVMThread::current() != nullptr) isdeliberately not unit tested:
JVMThread::current()reads a pthread keyestablished by JVM startup and this gtest binary has no JVM attached, so
covering it means mutating process-global state and leaning on
ThreadLocal<JVMThread*>::initialize()'s scan over every live pthread key-- more cost and fragility than a single argument expression warrants. No
integration test exercises the init-window path either, so that one line
rests on compilation and inspection.
correctness and signal-handler integration tests should be run to confirm
no regression, particularly around
CTimerandWallClockJvmtigiven theerrno-ordering change in the latter, and around
ITimer/PerfEventsgiven the new
SighandlerTidScope/assert/errno-restore paths.