Skip to content

refactor(profiling): Signal handler boilerplate - #756

Open
yaronguro-datadog wants to merge 11 commits into
mainfrom
yg/signal-handler-boilerplate-refactor
Open

refactor(profiling): Signal handler boilerplate#756
yaronguro-datadog wants to merge 11 commits into
mainfrom
yg/signal-handler-boilerplate-refactor

Conversation

@yaronguro-datadog

@yaronguro-datadog yaronguro-datadog commented Aug 25, 2026

Copy link
Copy Markdown

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 several
latent 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):

  • Adds SighandlerTidScope (guards.h), a narrowly-scoped RAII guard around
    Shims::instance().setSighandlerTid(tid) / setSighandlerTid(-1),
    replacing the manual set/reset pairs in all seven handlers.
  • Adds tickInitWindowIfNeeded() (jvmThread.h), a shared helper for the
    4-line init-window guard that was copied verbatim into the original five
    handlers. Deliberately not applied to ITimer::signalHandler or
    PerfEvents::signalHandler (see Additional Notes).
  • Fixes a pre-existing errno-restore gap in CTimer::signalHandler's
    !_enabled early return, which returned without restoring errno (the
    three Jvmti handlers already did this correctly on the equivalent path).
  • Adds missing saved_errno save/restore to WallClockASGCT::signalHandler,
    which previously didn't save/restore errno at all, unlike its sibling
    WallClockJvmti::signalHandler.
  • Fixes a second, narrower errno-restore gap in WallClockJvmti::signalHandler:
    saved_errno was captured after the CriticalSection entry check, so the
    !cs.entered() bail-out path left errno unrestored. The save now happens
    first, matching WallClockASGCT.
  • Adds saved_errno save/restore to ITimer::signalHandler and
    PerfEvents::signalHandler, neither of which had it before, wraps their
    recordSample span in SighandlerTidScope (replacing the manual
    setSighandlerTid/-1 pair), and adds assert(current != nullptr)
    ahead of the tid computation, mirroring the original five handlers.
    ITimerJvmti::signalHandler was already fully migrated and needed no
    changes here.
  • Fixes a latent errno-clobbering bug in PerfFdRearmGuard::~PerfFdRearmGuard()
    (perfEvents_linux.cpp): it is the first local constructed in
    PerfEvents::signalHandler, so it destructs last — after any
    errno = saved_errno restore in the handler body — and its ioctl()/
    resetBuffer() calls were silently overwriting the restored value. Its
    destructor now saves/restores errno around its own side effects.

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::signalHandler and PerfEvents::signalHandler were not named in PROF-14748's
    original five. Both had the identical manual
    set/reset-setSighandlerTid pattern as their siblings and are now given
    the same SighandlerTidScope treatment, plus errno save/restore and a
    current != nullptr assert.
  • tickInitWindowIfNeeded() was deliberately not wired into ITimer nor PerfEvents
    handlers, unlike the original five. Neither engine had this check before,
    and unlike CTimer/WallClock — which gate it behind signal-origin
    validation (si_code/sival payload checks) — ITimer can't do that
    validation at all (setitimer(ITIMER_PROF) delivers SI_KERNEL, with no
    payload to check) and PerfEvents gates on a different, coarser
    si_code <= 0 "external signal" check. Adding the init-window drop
    without that same origin guard would be a behavior change with different
    risk characteristics for these two engines, so it's left out of this
    boilerplate-extraction PR and would need separate review.
  • PerfEvents::signalHandler's existing control flow (the _enabled check
    gates only the SighandlerTidScope/recordSample block, after
    noteCPUSample already ran unconditionally) was left as-is; only errno
    handling and the SighandlerTidScope/assert changes were added.
  • resolveThreadId (a candidate shared helper for the
    current ? current->tid() : OS::threadId() duplication) was not extracted.
    The underlying duplication was eliminated directly instead: the original
    five handlers now assert current != nullptr before computing tid,
    making the ternary dead code, so it was removed rather than factored out.
    (ITimer::signalHandler and PerfEvents::signalHandler already computed
    tid from current->tid() directly, with no ternary to remove.) The
    pattern still exists in javaApi.cpp, a non-signal-handler context outside
    this ticket's scope.
  • No change to signal-origin validation, foreign-signal forwarding, or
    critical-section semantics in any handler.

How to test the change?:

  • ./gradlew :ddprof-lib:compileDebug compiles cleanly on macOS and linux.
  • The changes are mechanical (RAII scope-exit timing, errno save/restore
    ordering) and don't alter sampling logic or control flow beyond exit-path
    cleanup. tickInitWindowIfNeeded() is not wired into ITimer/PerfEvents
    (see Additional Notes), so no runtime sampling behavior changes for those
    two engines. No new automated test was added; existing CPU/wall sampler
    correctness and signal-handler integration tests should be run to confirm
    no regression, particularly around CTimer and WallClockJvmti given the
    errno-ordering change in the latter, and around ITimer/PerfEvents
    given the new SighandlerTidScope/assert/errno-restore paths.

…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.
@dd-octo-sts

dd-octo-sts Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Scan-Build Report

User:runner@runnervmgx7h7
Working Directory:/home/runner/work/java-profiler/java-profiler/ddprof-lib/src/test/make
Command Line:make -j4 all
Clang Version:Ubuntu clang version 18.1.3 (1ubuntu1)
Date:Wed Sep 2 18:18:57 2026

Bug Summary

Bug TypeQuantityDisplay?
All Bugs1
Logic error
Dereference of null pointer1

Reports

Bug Group Bug Type ▾ File Function/Method Line Path Length
Logic errorDereference of null pointerfaultInjection.cppcrashNow242

@dd-octo-sts

dd-octo-sts Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #33666237762 | Commit: cbc9af8 | Duration: 15m 1s (longest job)

All 32 test jobs passed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - - -
8-ibm - - -
8-j9 - -
8-librca - -
8-orcl - - -
11 - - -
11-j9 - -
11-librca - -
17 - -
17-graal - -
17-j9 - -
17-librca - -
21 - -
21-graal - -
21-librca - -
25 - -
25-graal - -
25-librca - -

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

Summary: Total: 32 | Passed: 32 | Failed: 0


Updated: 2026-09-02 18:34:26 UTC

@datadog-datadog-us1-prod

This comment has been minimized.

@jbachorik

Copy link
Copy Markdown
Collaborator

One small typo to fix, otherwise looks good!

Comment thread ddprof-lib/src/main/cpp/jvmThread.h Outdated
Co-authored-by: Jaroslav Bachorik <jaroslav.bachorik@datadoghq.com>

Copilot AI left a comment

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.

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 manual setSighandlerTid(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 errno save/restore coverage in several signal handlers and prevents PerfFdRearmGuard destruction from clobbering restored errno.

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.

Comment thread ddprof-lib/src/main/cpp/perfEvents_linux.cpp Outdated
@dd-octo-sts

dd-octo-sts Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

All 40 integration tests passed

📊 Dashboard · 👷 Pipeline · 📦 09fdb194

@zhengyu123

Copy link
Copy Markdown
Contributor

Good cleanup.

For preserving errno, probably can use a RAII class, e.g.

class ErrnoPreserver {
private:
  int _errno;
public:
   ErrnoPreserver(const ErrnoPreserver&) = delete;
   ErrnoPreserver& operator=(const ErrnoPreserver&) = delete;

  ErrnoPreserver() : _errno(errno) { }
  ~ErrnoPreserve() { errno = _errno};
};
 

zhengyu123 and others added 3 commits September 1, 2026 13:39
Addresses PR feedback suggesting an RAII wrapper for errno preservation.
Replaces the `int saved_errno = errno; ... errno = saved_errno;` pattern
repeated before every early return in ITimer, ITimerJvmti, WallClockASGCT,
WallClockJvmti, CTimer, CTimerJvmti, PerfEvents::signalHandler,
PerfFdRearmGuard, and OS::forwardForeignSignal.

Declaring ErrnoPreserver as the first local also fixes a latent gap: since
locals destruct in reverse order, it now restores errno after every other
guard's destructor (e.g. InflightGuard's clock_gettime call) has run,
instead of before.

Removes the now-redundant SIGNAL_HANDLER_GUARD_OR_DROP_WITH_ERRNO macro
variant.
…ataDog/java-profiler into yg/signal-handler-boilerplate-refactor

@zhengyu123 zhengyu123 left a comment

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.

LGTM

Comment thread ddprof-lib/src/main/cpp/wallClock.cpp Outdated

void WallClockASGCT::signalHandler(int signo, siginfo_t *siginfo, void *ucontext,
u64 last_sample, ProfiledThread* current) {
ErrnoPreserver errno_preserver;

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.

🗿 🤖 🔴

[Sphinx Review — MEDIUM] For the wall-clock engines the ErrnoPreserver is declared in the inner WallClockASGCT/WallClockJvmti::signalHandler helper rather than at the top of sharedSignalHandler, which is the actual installed sa_sigaction. Everything between signal entry and this line - Counters::increment, SIGNAL_HANDLER_GUARD_OR_DROP() -> SignalHandlerScope -> ProfiledThread::acquireCurrent() -> SignalBlocker -> pthread_sigmask, and BaseWallClock::eventsEnabled() - runs before errno is captured, so a clobbered errno is snapshotted and restored, leaving the interrupted user code with the wrong value. Symmetrically, sharedSignalHandler's SignalHandlerScope/InflightGuard destructors run after the inner restore. This is the same gap guards.h documents ('declare the ErrnoPreserver as the first local in the guarded function') and that the other five handlers do avoid, since there ErrnoPreserver is the first statement of the sa_sigaction itself.

Suggestion: Move the ErrnoPreserver declaration to be the first local of WallClockASGCT::sharedSignalHandler (wallClock.cpp:206) and WallClockJvmti::sharedSignalHandler (wallClock.cpp:412), and drop it from the inner signalHandler helpers. That makes it the first local of the real signal-entry frame, matching the CTimer/ITimer/PerfEvents handlers and the contract stated in guards.h.

Comment thread ddprof-lib/src/main/cpp/jvmThread.h Outdated
// (where JVMThread::current() is always null) is allowed through once its
// one-shot init window has ticked down. Returns true if the caller should
// tick-and-return, in which case the tick has already happened; the caller
// remains responsible for restoring errno at its own return, since not all

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.

🗿 🤖 🔴

[Sphinx Review — MEDIUM] The new doc comment states that callers of tickInitWindowIfNeeded() must restore errno at their own return 'since not all call sites save errno the same way'. That is untrue of every call site created in this diff: all five (ctimer_linux.cpp:231/281, itimer.cpp:113, wallClock.cpp:242/449) now save errno identically via ErrnoPreserver and none performs a manual restore. The comment documents the very pattern this PR removed.

Suggestion: Replace the errno sentence with a statement of the actual convention, e.g. that all call sites are signal handlers holding an ErrnoPreserver and therefore need no manual errno handling on this return path.

// tick-and-return, in which case the tick has already happened; the caller
// remains responsible for restoring errno at its own return, since not all
// call sites save errno the same way.
static inline bool tickInitWindowIfNeeded(ProfiledThread* current) {

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.

🗿 🤖 🔴

[Sphinx Review — MEDIUM] No test in this PR would detect changing && to || in the init-window condition of tickInitWindowIfNeeded(), which would make the function return true if either condition (JVMThread::current() == nullptr OR current->inInitWindow()) holds, instead of requiring both.

Suggestion: Add a test case that calls tickInitWindowIfNeeded() with different combinations of conditions and verifies the return value and whether tickInitWindow() is called in each case (null/in-window, null/not-in-window, not-null/in-window, not-null/not-in-window).

public:
PerfFdRearmGuard(int fd, int tid) : _fd(fd), _tid(tid) {}
~PerfFdRearmGuard() {
// These calls must not leak an errno change to the caller.

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.

🗿 🤖 🔴

[Sphinx Review — LOW] The ErrnoPreserver inside ~PerfFdRearmGuard is redundant. The PR description justifies it by claiming PerfFdRearmGuard 'is the first local constructed in PerfEvents::signalHandler, so it destructs last - after any errno = saved_errno restore in the handler body'. The same diff makes ErrnoPreserver the first local (line 782), so PerfFdRearmGuard now destructs strictly before the errno restore and cannot clobber it. PerfFdRearmGuard has a single instantiation, so there is no other caller the extra guard protects.

Suggestion: Drop the nested ErrnoPreserver (and its comment) from ~PerfFdRearmGuard, relying on the handler-level ErrnoPreserver; or, if the belt-and-braces guard is intentional, correct the PR description since its stated rationale no longer holds.

errno = saved_errno;
return;
}
int tid = 0;

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.

🗿 🤖 🔴

[Sphinx Review — LOW] int tid = 0; is declared before the init-window early return and then unconditionally reassigned at line 236 without ever being read. The refactor removed the reason for the split declaration; CTimer::signalHandler just below (line 285) already uses int tid = current->tid();.

Suggestion: Delete line 229 and change line 236 to int tid = current->tid();, matching CTimer::signalHandler.

Comment thread ddprof-lib/src/main/cpp/itimer.cpp Outdated
#include "threadLocalData.inline.h"
#include "threadState.inline.h"
#include "guards.h"
#include <errno.h>

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.

🗿 🤖 🔴

[Sphinx Review — LOW] #include <errno.h> was added to itimer.cpp, but the diff simultaneously removed every direct use of errno from that file (only ErrnoPreserver remains, whose definition in guards.h already includes ). The include is unnecessary.

Suggestion: Remove the #include <errno.h> line; guards.h already provides for ErrnoPreserver.

Comment thread ddprof-lib/src/main/cpp/os_linux.cpp Outdated
@@ -474,16 +475,14 @@ void OS::forwardForeignSignal(int signo, siginfo_t* siginfo, void* ucontext) {
// chained handler) may set errno. Callers that save errno AFTER
// forwardForeignSignal (e.g. CTimer::signalHandler) would see a clobbered

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.

🗿 🤖 🔴

[Sphinx Review — LOW] The rationale comment above the new ErrnoPreserver names CTimer::signalHandler as a caller that 'saves errno AFTER forwardForeignSignal'. It does not - ctimer_linux.cpp:252 declares ErrnoPreserver as the handler's first statement, before the forwardForeignSignal call, as does CTimerJvmti at line 209. The real justification for the guard is that the interrupted code must not observe an errno clobbered by rt_sigprocmask or the chained handler.

Suggestion: Restate the rationale in terms of not leaking errno changes from the chained handler / rt_sigprocmask to the interrupted code, and drop the now-incorrect CTimer::signalHandler example.

#include <jvmti.h>

#include "threadLocal.h"
#include "threadLocalData.h"

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.

🗿 🤖 🔴

[Sphinx Review — LOW] Adding #include "threadLocalData.h" to jvmThread.h drags a heavy header (which transitively pulls context.h, otel_context.h, os.h, unwindStats.h, , , jvmti.h) into all 17 TUs that include jvmThread.h, and transitively into everything including hotspot/vmStructs.h, solely to support a 5-line helper used by 5 call sites that all already include threadLocalData.h themselves.

Suggestion: Consider putting tickInitWindowIfNeeded in a lighter, purpose-specific header (or in threadLocalData.inline.h, which already has ProfiledThread available), keeping jvmThread.h's forward-declaration-only footprint.

@rkennke rkennke added the sphinx:spotcheck Sphinx: spot-check recommended label Sep 2, 2026
jbachorik and others added 3 commits September 2, 2026 17:27
- Move ErrnoPreserver to the top of WallClock{ASGCT,Jvmti}::sharedSignalHandler
  (the real signal-entry points), not the inner signalHandler helpers.
- Fix stale tickInitWindowIfNeeded() doc comment describing errno handling
  the refactor removed; state the actual ErrnoPreserver convention instead.
- Add tickInitWindowIfNeeded() unit tests covering all 4 combinations of its
  guard condition, catching a `&&` -> `||` mutation.
- Drop the redundant nested ErrnoPreserver in ~PerfFdRearmGuard now that the
  handler-level guard is declared first and destructs after it.
- Remove dead pre-init of `tid` in CTimerJvmti::signalHandler.
- Remove unused `#include <errno.h>` in itimer.cpp.
- Correct forwardForeignSignal()'s errno rationale comment, which
  misdescribed CTimer::signalHandler's save ordering.
…ataDog/java-profiler into yg/signal-handler-boilerplate-refactor

Copilot AI left a comment

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.

🟡 Changes recommended

The new unit test deletes a pthread key that JVMThread’s ThreadLocal intentionally treats as process-immortal, which can invalidate JVMThread::current() for later tests, and there is a PR-description/implementation mismatch around PerfFdRearmGuard errno preservation.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines +42 to +45
void TearDown() override {
pthread_key_delete(_key);
ProfiledThread::release();
}
Comment on lines 766 to 770
~PerfFdRearmGuard() {
// Errno changes made here are caught by the handler-level
// ErrnoPreserver, which is declared before this guard and therefore
// destructs after it.
PerfEvents::resetBuffer(_tid);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants