From efcdfa704ac39791d81b489f0879f2a2cd76c435 Mon Sep 17 00:00:00 2001 From: Dmitry Fomin Date: Fri, 18 Sep 2026 12:24:38 +0200 Subject: [PATCH v11 1/5] Add begin/end hooks for timed wait events The wait event machinery reports which wait a backend is in, but not how long it took. Measuring that in core would mean paying for a timestamp at every wait site; leaving it to an extension means giving the extension a place to stand. Add wait_event_begin_hook and wait_event_end_hook, called from a new pair of reporting functions, pgstat_report_wait_start_timed() and pgstat_report_wait_end_timed(), which an instrumented call site uses in place of the ordinary pair. The ordinary pgstat_report_wait_start() and pgstat_report_wait_end() are left exactly as they were, so an uninstrumented site, and any build with no extension loaded, behaves as it does today. The hooks run in the middle of the wait path, where a backend may hold an LWLock, sit inside a critical section, or belong to a process with no transaction at all. A hook implementation may therefore use only state it has preallocated for itself: it must not palloc(), acquire a lock, wait, or raise an error. wait_event_hook_depth guards against re-entry, so a hook that breaks that rule and waits is not called recursively. A consumer that chains onto a hook another extension has already installed calls the previous begin hook before its own begin work, and its own end work before the previous end hook, so that nested consumers unwind in the order they were installed. With no hook installed, the cost added at an instrumented site is a predictable test of a NULL function pointer. The enabled path lives in two cold out-of-line functions in wait_event.c; the inline pair is one unlikely()-hinted pointer test per side, the hint chosen because GCC's static predictor treats a pointer compared with NULL as non-NULL; recorded data, hook contract and the ordinary pair are unchanged. Discussion: https://postgr.es/m/CAPHG-0mAOn05ae6Kqx1wHXxzOk4E5W7ajjd=QBhgkR7a0uyQmw@mail.gmail.com --- src/backend/utils/activity/wait_event.c | 47 ++++++++++++++++++++ src/include/utils/wait_event.h | 58 +++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c index e36a740a888..2cf1f35f92a 100644 --- a/src/backend/utils/activity/wait_event.c +++ b/src/backend/utils/activity/wait_event.c @@ -40,6 +40,53 @@ static const char *pgstat_get_wait_io(WaitEventIO w); static uint32 local_my_wait_event_info; uint32 *my_wait_event_info = &local_my_wait_event_info; +wait_event_hook_type wait_event_begin_hook = NULL; +wait_event_hook_type wait_event_end_hook = NULL; +int wait_event_hook_depth = 0; + +/* + * Slow paths for pgstat_report_wait_start_timed() and + * pgstat_report_wait_end_timed() in wait_event.h, reached only once a hook + * has actually been installed in this process. Keeping them out of line + * and marked cold lets the inline callers stay down to a single hinted + * pointer test on the common no-consumer path. + * + * Each function re-reads its hook pointer rather than being handed the + * value the inline caller already loaded. The hook cannot actually change + * between the inline test and this call -- both run in the same process, + * and there is no window for another backend to reach in -- but re-reading + * it here keeps that assumption local to this file instead of leaking into + * the header. + */ +pg_noinline pg_attribute_cold void +pgstat_wait_event_hook_begin_slow(uint32 wait_event_info) +{ + wait_event_hook_type hook = wait_event_begin_hook; + + if (wait_event_hook_depth == 0) + { + wait_event_hook_depth = 1; + hook(wait_event_info); + wait_event_hook_depth = 0; + } +} + +pg_noinline pg_attribute_cold void +pgstat_wait_event_hook_end_slow(void) +{ + wait_event_hook_type hook = wait_event_end_hook; + + if (wait_event_hook_depth == 0) + { + uint32 wait_event_info; + + wait_event_info = *(volatile uint32 *) my_wait_event_info; + wait_event_hook_depth = 1; + hook(wait_event_info); + wait_event_hook_depth = 0; + } +} + #define WAIT_EVENT_CLASS_MASK 0xFF000000 #define WAIT_EVENT_ID_MASK 0x0000FFFF diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h index 86ee348220d..6111b83d9ee 100644 --- a/src/include/utils/wait_event.h +++ b/src/include/utils/wait_event.h @@ -17,11 +17,35 @@ extern const char *pgstat_get_wait_event(uint32 wait_event_info); extern const char *pgstat_get_wait_event_type(uint32 wait_event_info); static inline void pgstat_report_wait_start(uint32 wait_event_info); static inline void pgstat_report_wait_end(void); +static inline void pgstat_report_wait_start_timed(uint32 wait_event_info); +static inline void pgstat_report_wait_end_timed(void); extern void pgstat_set_wait_event_storage(uint32 *wait_event_info); extern void pgstat_reset_wait_event_storage(void); extern PGDLLIMPORT uint32 *my_wait_event_info; +/* + * Hooks for explicitly instrumented waits. Hook implementations may use + * only preallocated backend-local state; they must not wait, allocate memory, + * acquire locks, or report errors. The depth guard prevents re-entry. + * + * Hook users that chain callbacks must save the previous hook pointers, call + * the previous begin hook before their own begin work, and perform their own + * end work before calling the previous end hook. + */ +typedef void (*wait_event_hook_type) (uint32 wait_event_info); + +extern PGDLLIMPORT wait_event_hook_type wait_event_begin_hook; +extern PGDLLIMPORT wait_event_hook_type wait_event_end_hook; +extern PGDLLIMPORT int wait_event_hook_depth; + +/* + * Out-of-line, cold slow paths for the hook-enabled case of the timed + * wait-event pair below. See pgstat_report_wait_start_timed() for why + * these exist. + */ +extern pg_noinline pg_attribute_cold void pgstat_wait_event_hook_begin_slow(uint32 wait_event_info); +extern pg_noinline pg_attribute_cold void pgstat_wait_event_hook_end_slow(void); /* * Wait Events - Extension, InjectionPoint @@ -86,5 +110,39 @@ pgstat_report_wait_end(void) *(volatile uint32 *) my_wait_event_info = 0; } +/* + * Explicitly instrumented variant of the ordinary wait-event reporting pair. + * The ordinary functions above remain unchanged for uninstrumented sites. + * + * Every backend reaches these two functions at every instrumented wait, but + * only a process in which a consumer has installed a hook ever takes the + * enabled path. The pointer test is therefore hinted with unlikely() for the + * no-consumer path, which is laid out as the fall-through; without the hint, + * GCC's static branch predictor treats a pointer compared with NULL as + * non-NULL and lays out the enabled path there instead. The enabled path -- + * the depth guard, the volatile read in the end case, and the indirect call + * -- lives in a separate cold, noinline function rather than being inlined at + * each call site. A process with no consumer pays one predicted-not-taken + * branch per call; a process with a consumer pays one additional taken jump. + * The depth guard, the hook-chaining contract, and the exported hook + * variables are unchanged. + */ +static inline void +pgstat_report_wait_start_timed(uint32 wait_event_info) +{ + *(volatile uint32 *) my_wait_event_info = wait_event_info; + + if (unlikely(wait_event_begin_hook != NULL)) + pgstat_wait_event_hook_begin_slow(wait_event_info); +} + +static inline void +pgstat_report_wait_end_timed(void) +{ + if (unlikely(wait_event_end_hook != NULL)) + pgstat_wait_event_hook_end_slow(); + *(volatile uint32 *) my_wait_event_info = 0; +} + #endif /* WAIT_EVENT_H */ -- 2.49.0