From 0dbce8228aadacbebcd8475450ca53902ddcd18d Mon Sep 17 00:00:00 2001
From: Dmitry Fomin <fomin.list@gmail.com>
Date: Tue, 9 Jun 2026 16:35:24 +0000
Subject: [PATCH v1 3/7] wait_event_timing: expose overflow counters and add
 reset functions

Surface the per-backend truncation counters maintained by the recording
path, and add the ability to reset wait-event-timing statistics.

pg_stat_get_wait_event_timing_overflow(pid) and the
pg_stat_wait_event_timing_overflow view report, per backend,
lwlock_overflow_count (LWLock waits dropped because the per-backend
tranche hash was full), flat_overflow_count (events whose class index was
out of range), and reset_count.

Resets use a lock-free request/response so the hot path stays single
writer: each slot carries an atomic reset_generation, bumped by the
resetter; the owning backend compares it against a backend-local
last-seen value at its next wait_end and clears its own counters,
incrementing reset_count.  pg_stat_reset_wait_event_timing(pid) resets one
backend -- synchronously when it targets the caller's own session (any
user; pid defaults to NULL), or asynchronously for another backend
(requiring pg_signal_backend, matching pg_stat_reset_backend_stats).
pg_stat_reset_wait_event_timing_all() resets every backend and is
superuser-only.

Builds without --enable-wait-event-timing keep empty-result/feature-not-
supported stubs for the new functions.
---
 doc/src/sgml/config.sgml                      |  14 +-
 doc/src/sgml/monitoring.sgml                  | 178 +++++++++++++
 src/backend/catalog/system_views.sql          |  17 ++
 .../utils/activity/wait_event_timing.c        | 242 ++++++++++++++++++
 src/include/catalog/pg_proc.dat               |  23 ++
 src/include/utils/wait_event_timing.h         |  21 +-
 src/test/regress/expected/rules.out           |   7 +
 .../regress/expected/wait_event_timing.out    |  51 ++++
 .../regress/expected/wait_event_timing_1.out  |  40 +++
 src/test/regress/sql/wait_event_timing.sql    |  25 ++
 10 files changed, 611 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 43c9d395cf5..15131fd9b7f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9188,13 +9188,17 @@ GRANT SET ON PARAMETER wait_event_capture TO pg_monitor;
         Sets the maximum number of distinct LWLock tranches whose timing is
         recorded individually per backend.  PostgreSQL maintains a
         per-backend hash table mapping each tranche the backend encounters
-        to its histogram; once the table fills, further tranches are not
-        individually timed.  Sized at server start; this parameter has no
-        effect on builds compiled without
+        to its histogram; once the table fills, further tranches are
+        counted against <structfield>lwlock_overflow_count</structfield> in
+        <link linkend="monitoring-pg-stat-wait-event-timing-overflow-view">
+        <structname>pg_stat_wait_event_timing_overflow</structname></link>
+        and not individually timed.  Sized at server start; this parameter
+        has no effect on builds compiled without
         <option>--enable-wait-event-timing</option>.  The default is
         <literal>192</literal>; raise it if your installation loads many
-        extensions that register their own LWLock tranches.  This parameter
-        can only be set at server start.
+        extensions that register their own LWLock tranches and you observe
+        a non-zero <structfield>lwlock_overflow_count</structfield>.  This
+        parameter can only be set at server start.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 8c4a523f971..34676e43eae 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -570,6 +570,15 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
       </entry>
      </row>
 
+     <row>
+      <entry><structname>pg_stat_wait_event_timing_overflow</structname><indexterm><primary>pg_stat_wait_event_timing_overflow</primary></indexterm></entry>
+      <entry>Per-backend truncation and reset counters for the wait-event
+       timing subsystem.  See
+       <link linkend="monitoring-pg-stat-wait-event-timing-overflow-view">
+       <structname>pg_stat_wait_event_timing_overflow</structname></link> for details.
+      </entry>
+     </row>
+
      <!-- all "stat" for schema objects, by "importance" -->
 
      <row>
@@ -4098,6 +4107,124 @@ ORDER  BY b.bucket_idx;
   </table>
  </sect2>
 
+ <sect2 id="monitoring-pg-stat-wait-event-timing-overflow-view">
+  <title><structname>pg_stat_wait_event_timing_overflow</structname></title>
+
+  <indexterm>
+   <primary>pg_stat_wait_event_timing_overflow</primary>
+  </indexterm>
+
+  <para>
+   The <structname>pg_stat_wait_event_timing_overflow</structname> view
+   exposes per-backend truncation counters for the wait-event timing
+   subsystem.  Each backend owns a bounded LWLock timing hash
+   (<xref linkend="guc-wait-event-timing-max-tranches"/> tranches) and a bounded flat event array; events that cannot
+   be mapped to a slot are counted here.  A non-zero value means the
+   corresponding row(s) in
+   <link linkend="monitoring-pg-stat-wait-event-timing-view">
+   <structname>pg_stat_wait_event_timing</structname></link>
+   are incomplete for that backend.  Requires the server to be
+   compiled with <option>--enable-wait-event-timing</option>.
+  </para>
+
+  <table id="pg-stat-wait-event-timing-overflow-view" xreflabel="pg_stat_wait_event_timing_overflow">
+   <title><structname>pg_stat_wait_event_timing_overflow</structname> View</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pid</structfield> <type>integer</type>
+      </para>
+      <para>
+       Process ID of the backend
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>backend_type</structfield> <type>text</type>
+      </para>
+      <para>
+       Type of the backend (e.g. <literal>client backend</literal>,
+       <literal>checkpointer</literal>, <literal>walwriter</literal>)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>procnumber</structfield> <type>integer</type>
+      </para>
+      <para>
+       Internal slot number (0-based process number) of the backend.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lwlock_overflow_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of LWLock wait events dropped because the per-backend
+       LWLock timing hash was already full (more distinct tranches
+       observed in this session than
+       <xref linkend="guc-wait-event-timing-max-tranches"/> allows).
+       Zero means no LWLock truncation.  A one-time
+       <literal>WARNING</literal> is also emitted to the server log on
+       first overflow.  If you see this counter rising, raise
+       <varname>wait_event_timing_max_tranches</varname> at server
+       start (the per-backend memory cost is proportional and
+       described under that GUC).
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>flat_overflow_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of non-LWLock wait events dropped because the event
+       could not be mapped to a known class / index.  This almost
+       always indicates a code path emitting a wait event of a class
+       the timing infrastructure was not compiled for; it should be
+       zero in supported builds.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>reset_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of resets this backend has <emphasis>observed and acted
+       on</emphasis>; not a request counter.  Own-backend resets via
+       <function>pg_stat_reset_wait_event_timing(NULL)</function> (or
+       passing the caller's own PID) are synchronous and bump this
+       column once per call.  Cross-backend reset requests
+       <emphasis>coalesce</emphasis>: if several
+       <function>pg_stat_reset_wait_event_timing(<replaceable>pid</replaceable>)</function>
+       calls land between two of the target's wait events, the target
+       observes them as a single reset and increments
+       <structfield>reset_count</structfield> only once.  Callers
+       polling for asynchronous-reset acknowledgment should watch for
+       any <literal>N &rarr; N+1</literal> transition.
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect2>
+
  <sect2 id="monitoring-pg-stat-database-view">
   <title><structname>pg_stat_database</structname></title>
 
@@ -6154,6 +6281,57 @@ ORDER  BY b.bucket_idx;
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_stat_reset_wait_event_timing</primary>
+        </indexterm>
+        <function>pg_stat_reset_wait_event_timing</function> ( <parameter>pid</parameter> <type>integer</type> <literal>DEFAULT</literal> <literal>NULL</literal> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Resets wait event timing counters for a single backend, identified
+        by its process ID (see <structfield>pid</structfield> in
+        <link linkend="monitoring-pg-stat-activity-view">
+        <structname>pg_stat_activity</structname></link>).
+        Passing <literal>NULL</literal> (or the caller's own
+        <function>pg_backend_pid()</function>) resets the current session;
+        any user may do this.  Passing any other PID resets that backend
+        and requires membership in the
+        <link linkend="predefined-roles"><literal>pg_signal_backend</literal></link>
+        role &mdash; the same role required by
+        <function>pg_stat_reset_backend_stats</function>,
+        <function>pg_terminate_backend</function>, and
+        <function>pg_cancel_backend</function>.  Unknown or
+        already-exited PIDs are silent no-ops, matching the behavior of
+        <function>pg_stat_reset_backend_stats</function>.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_stat_reset_wait_event_timing_all</primary>
+        </indexterm>
+        <function>pg_stat_reset_wait_event_timing_all</function> ()
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Resets wait event timing counters for every backend in the
+        cluster.  Requires superuser.  This is intentionally stricter
+        than the per-backend variant
+        <function>pg_stat_reset_wait_event_timing</function>(<parameter>pid</parameter>),
+        which only requires <literal>pg_signal_backend</literal>: the
+        cluster-wide form has unbounded blast radius (it affects every
+        backend in a single call) and would erase forensic patterns
+        that span multiple backends, so it is gated to the cluster
+        owner.  Returns before the resets have been observed by their
+        target backends; callers that need strict read-after-reset
+        semantics should poll each target's
+        <structfield>reset_count</structfield> column.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index b41661eb5d2..96aba833895 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1630,3 +1630,20 @@ CREATE VIEW pg_stat_wait_event_timing AS
     FROM pg_stat_get_wait_event_timing(NULL) t;
 REVOKE ALL ON pg_stat_wait_event_timing FROM PUBLIC;
 GRANT SELECT ON pg_stat_wait_event_timing TO pg_read_all_stats;
+
+CREATE VIEW pg_stat_wait_event_timing_overflow AS
+    SELECT
+        t.pid,
+        t.backend_type,
+        t.procnumber,
+        t.lwlock_overflow_count,
+        t.flat_overflow_count,
+        t.reset_count
+    FROM pg_stat_get_wait_event_timing_overflow(NULL) t;
+REVOKE ALL ON pg_stat_wait_event_timing_overflow FROM PUBLIC;
+GRANT SELECT ON pg_stat_wait_event_timing_overflow TO pg_read_all_stats;
+
+-- Cluster-scope operations: revoked from PUBLIC (administrators can
+-- delegate with GRANT EXECUTE); not granted to pg_read_all_stats because
+-- they mutate state rather than read it.
+REVOKE EXECUTE ON FUNCTION pg_stat_reset_wait_event_timing_all() FROM PUBLIC;
diff --git a/src/backend/utils/activity/wait_event_timing.c b/src/backend/utils/activity/wait_event_timing.c
index 44c43066ca7..2ec7d246399 100644
--- a/src/backend/utils/activity/wait_event_timing.c
+++ b/src/backend/utils/activity/wait_event_timing.c
@@ -60,6 +60,9 @@ const struct config_enum_entry wait_event_capture_options[] = {
 #include "funcapi.h"
 
 Datum		pg_stat_get_wait_event_timing(PG_FUNCTION_ARGS);
+Datum		pg_stat_get_wait_event_timing_overflow(PG_FUNCTION_ARGS);
+Datum		pg_stat_reset_wait_event_timing(PG_FUNCTION_ARGS);
+Datum		pg_stat_reset_wait_event_timing_all(PG_FUNCTION_ARGS);
 
 Datum
 pg_stat_get_wait_event_timing(PG_FUNCTION_ARGS)
@@ -68,6 +71,33 @@ pg_stat_get_wait_event_timing(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+pg_stat_get_wait_event_timing_overflow(PG_FUNCTION_ARGS)
+{
+	InitMaterializedSRF(fcinfo, 0);
+	PG_RETURN_VOID();
+}
+
+Datum
+pg_stat_reset_wait_event_timing(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("wait event capture is not supported by this build"),
+			 errhint("Compile PostgreSQL with --enable-wait-event-timing.")));
+	PG_RETURN_VOID();
+}
+
+Datum
+pg_stat_reset_wait_event_timing_all(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("wait event capture is not supported by this build"),
+			 errhint("Compile PostgreSQL with --enable-wait-event-timing.")));
+	PG_RETURN_VOID();
+}
+
 /*
  * GUC check hook for the stub build.  Any value other than 'off' is
  * meaningless without --enable-wait-event-timing, so reject it -- or
@@ -124,6 +154,7 @@ pgstat_reset_wait_event_timing_storage(void)
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "port/pg_bitutils.h"
+#include "storage/latch.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "storage/procnumber.h"
@@ -143,6 +174,14 @@ pgstat_reset_wait_event_timing_storage(void)
 /* Pointer to this backend's timing state in shared memory. */
 WaitEventTimingState *my_wait_event_timing = NULL;
 
+/*
+ * Backend-local copy of the last reset generation this backend acted on.
+ * Compared against the shared reset_generation at every wait_end; when they
+ * differ, the owning backend performs the reset of its own counters on
+ * behalf of whoever called pg_stat_reset_wait_event_timing(target).
+ */
+static uint32 my_last_reset_generation = 0;
+
 /*
  * Backend-local cached pointer to the start of the shared slot array, set
  * at shmem init (postmaster) and, in EXEC_BACKEND mode, at attach.  Slots
@@ -406,6 +445,7 @@ WaitEventTimingShmemInit(void *arg)
 		LWLockTimingHashEntry *entries;
 		int			j;
 
+		pg_atomic_init_u32(&slot->reset_generation, 0);
 		slot->lwlock_hash.num_used = 0;
 		slot->lwlock_hash.hash_size = wait_event_timing_hash_size;
 		slot->lwlock_hash.max_entries = wait_event_timing_max_entries;
@@ -448,9 +488,17 @@ pgstat_set_wait_event_timing_storage(int procNumber)
 	lwlock_timing_hash_clear(slot);
 	slot->lwlock_overflow_count = 0;
 	slot->flat_overflow_count = 0;
+	slot->reset_count = 0;
 	slot->current_event = 0;
 	INSTR_TIME_SET_ZERO(slot->wait_start);
 
+	/*
+	 * Adopt the current shared reset generation as our baseline; the
+	 * reset_generation counter persists across slot reuse, so a new backend
+	 * must not treat the prior occupant's resets as its own.
+	 */
+	my_last_reset_generation = pg_atomic_read_u32(&slot->reset_generation);
+
 	/* Publish only after the slot is fully initialised. */
 	my_wait_event_timing = slot;
 }
@@ -523,6 +571,7 @@ void
 pgstat_report_wait_end_timing(int capture_level)
 {
 	uint32		event;
+	uint32		cur_reset_gen;
 
 	(void) capture_level;
 
@@ -531,6 +580,27 @@ pgstat_report_wait_end_timing(int capture_level)
 
 	event = my_wait_event_timing->current_event;
 
+	/*
+	 * Service a pending cross-backend reset request.  A single relaxed atomic
+	 * load; when the shared generation has advanced past the value we last
+	 * acted on, clear our own counters on behalf of the requester and record
+	 * the reset.  wait_start is left untouched so the in-flight measurement
+	 * still lands (in the freshly-zeroed counters), and current_event is
+	 * zeroed so external readers do not see stale state.
+	 */
+	cur_reset_gen = pg_atomic_read_u32(&my_wait_event_timing->reset_generation);
+	if (cur_reset_gen != my_last_reset_generation)
+	{
+		memset(my_wait_event_timing->events, 0,
+			   sizeof(my_wait_event_timing->events));
+		lwlock_timing_hash_clear(my_wait_event_timing);
+		my_wait_event_timing->reset_count++;
+		my_wait_event_timing->lwlock_overflow_count = 0;
+		my_wait_event_timing->flat_overflow_count = 0;
+		my_wait_event_timing->current_event = 0;
+		my_last_reset_generation = cur_reset_gen;
+	}
+
 	if (event != 0 && !INSTR_TIME_IS_ZERO(my_wait_event_timing->wait_start))
 	{
 		instr_time	now;
@@ -773,4 +843,176 @@ pg_stat_get_wait_event_timing(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/*
+ * SQL function: pg_stat_get_wait_event_timing_overflow(pid int4, OUT ...)
+ *
+ * Exposes the per-backend truncation counters that the recording path
+ * maintains: lwlock_overflow_count (LWLock waits dropped because the
+ * per-backend tranche hash was full), flat_overflow_count (events whose
+ * class index was out of range), and reset_count (resets the backend has
+ * observed and acted on).  pid has the same optional semantics as
+ * pg_stat_get_wait_event_timing().
+ */
+Datum
+pg_stat_get_wait_event_timing_overflow(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	int			start_idx;
+	int			end_idx;
+	int			backend_idx;
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	if (WaitEventTimingArray == NULL)
+		PG_RETURN_VOID();
+
+	if (!wait_event_timing_pid_range(fcinfo, &start_idx, &end_idx))
+		PG_RETURN_VOID();
+
+	for (backend_idx = start_idx; backend_idx < end_idx; backend_idx++)
+	{
+		WaitEventTimingState *state = wet_slot(backend_idx);
+		PgBackendStatus *beentry;
+		Datum		values[6];
+		bool		nulls[6];
+
+		beentry = pgstat_get_beentry_by_proc_number(backend_idx);
+		if (beentry == NULL)
+			continue;
+		if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid))
+			continue;
+
+		memset(nulls, 0, sizeof(nulls));
+
+		values[0] = Int32GetDatum(beentry->st_procpid);
+		values[1] = CStringGetTextDatum(GetBackendTypeDesc(beentry->st_backendType));
+		values[2] = Int32GetDatum(backend_idx);
+		values[3] = Int64GetDatum(state->lwlock_overflow_count);
+		values[4] = Int64GetDatum(state->flat_overflow_count);
+		values[5] = Int64GetDatum(state->reset_count);
+
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Request a cross-backend self-reset on the given slot: bump the slot's
+ * reset_generation and wake the target so it promptly observes the change
+ * and clears its own counters at its next wait_end.  Lock-free: only the
+ * owning backend ever writes its statistics.
+ */
+static void
+wait_event_timing_request_reset(int slot_idx)
+{
+	Assert(slot_idx >= 0 && slot_idx < NUM_WAIT_EVENT_TIMING_SLOTS);
+
+	if (WaitEventTimingArray == NULL)
+		return;
+
+	pg_atomic_fetch_add_u32(&wet_slot(slot_idx)->reset_generation, 1);
+
+	/*
+	 * The slot index is also the PGPROC array index.  Waking the target
+	 * shortens the time before it completes its current wait and notices the
+	 * request; setting a latch on a slot with no live owner is harmless.
+	 */
+	if (ProcGlobal != NULL && ProcGlobal->allProcs != NULL)
+		SetLatch(&ProcGlobal->allProcs[slot_idx].procLatch);
+}
+
+/*
+ * SQL function: pg_stat_reset_wait_event_timing(pid int4)
+ *
+ *   NULL or own pid : reset the caller's own counters synchronously.
+ *   another pid     : request a cross-backend reset (pg_signal_backend).
+ *   unknown pid     : silent no-op.
+ *
+ * Cross-backend resets are asynchronous: the target clears its counters at
+ * its next wait_end.  Callers needing read-after-reset semantics should
+ * target their own backend, or poll reset_count in
+ * pg_stat_wait_event_timing_overflow until it increments.
+ */
+Datum
+pg_stat_reset_wait_event_timing(PG_FUNCTION_ARGS)
+{
+	int			target_pid;
+	PGPROC	   *proc;
+	int			procNumber;
+
+	if (PG_ARGISNULL(0) || PG_GETARG_INT32(0) == MyProcPid)
+	{
+		/*
+		 * Own backend: synchronous, no lock needed (single writer).
+		 * wait_start is already zero (every wait_end zeroes it and we cannot
+		 * be mid-wait while running this function), so there is no in-flight
+		 * measurement to preserve.
+		 */
+		if (my_wait_event_timing != NULL)
+		{
+			memset(my_wait_event_timing->events, 0,
+				   sizeof(my_wait_event_timing->events));
+			lwlock_timing_hash_clear(my_wait_event_timing);
+			my_wait_event_timing->reset_count++;
+			my_wait_event_timing->lwlock_overflow_count = 0;
+			my_wait_event_timing->flat_overflow_count = 0;
+			my_wait_event_timing->current_event = 0;
+		}
+		PG_RETURN_VOID();
+	}
+
+	/*
+	 * Cross-backend reset requires pg_signal_backend, matching
+	 * pg_stat_reset_backend_stats(pid): anyone who can terminate the target
+	 * backend can already destroy more forensic state than a counter wipe.
+	 */
+	if (!has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied to reset another backend's wait event timing"),
+				 errdetail("Only roles with privileges of the \"pg_signal_backend\" role may reset another backend's wait event timing.")));
+
+	target_pid = PG_GETARG_INT32(0);
+
+	proc = BackendPidGetProc(target_pid);
+	if (proc == NULL)
+		proc = AuxiliaryPidGetProc(target_pid);
+	if (proc == NULL)
+		PG_RETURN_VOID();		/* unknown/dead pid: silent no-op */
+
+	procNumber = GetNumberFromPGProc(proc);
+	if (procNumber < 0 || procNumber >= NUM_WAIT_EVENT_TIMING_SLOTS)
+		PG_RETURN_VOID();
+
+	wait_event_timing_request_reset(procNumber);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * SQL function: pg_stat_reset_wait_event_timing_all()
+ *
+ * Request a reset on every backend.  Execution is revoked from PUBLIC by
+ * default (the blast radius is the whole cluster, a different decision
+ * from the per-backend variant); administrators can delegate with GRANT.
+ */
+Datum
+pg_stat_reset_wait_event_timing_all(PG_FUNCTION_ARGS)
+{
+	int			i;
+
+	/*
+	 * Execution is revoked from PUBLIC in system_views.sql; administrators
+	 * can delegate with GRANT EXECUTE.
+	 */
+	if (WaitEventTimingArray == NULL)
+		PG_RETURN_VOID();
+
+	for (i = 0; i < NUM_WAIT_EVENT_TIMING_SLOTS; i++)
+		wait_event_timing_request_reset(i);
+
+	PG_RETURN_VOID();
+}
+
 #endif							/* USE_WAIT_EVENT_TIMING */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1b384af298b..9c5dad6d925 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12728,4 +12728,27 @@
   proargnames => '{pid,pid,backend_type,procnumber,wait_event_type,wait_event,calls,total_time_ms,avg_time_us,max_time_us,histogram}',
   prosrc => 'pg_stat_get_wait_event_timing' },
 
+{ oid => '9958',
+  descr => 'statistics: reset wait event timing counters for the given backend (NULL = own)',
+  proname => 'pg_stat_reset_wait_event_timing', proisstrict => 'f',
+  provolatile => 'v', prorettype => 'void', proargtypes => 'int4',
+  proargnames => '{pid}', proargdefaults => '{NULL}',
+  prosrc => 'pg_stat_reset_wait_event_timing' },
+
+{ oid => '9959',
+  descr => 'statistics: per-backend wait event timing overflow counters (rows lost to LWLock hash / flat array overflow)',
+  proname => 'pg_stat_get_wait_event_timing_overflow', prorows => '1000',
+  proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r',
+  prorettype => 'record', proargtypes => 'int4',
+  proallargtypes => '{int4,int4,text,int4,int8,int8,int8}',
+  proargmodes => '{i,o,o,o,o,o,o}',
+  proargnames => '{pid,pid,backend_type,procnumber,lwlock_overflow_count,flat_overflow_count,reset_count}',
+  prosrc => 'pg_stat_get_wait_event_timing_overflow' },
+
+{ oid => '9960',
+  descr => 'statistics: reset wait event timing counters for all backends (superuser only)',
+  proname => 'pg_stat_reset_wait_event_timing_all',
+  provolatile => 'v', prorettype => 'void', proargtypes => '',
+  prosrc => 'pg_stat_reset_wait_event_timing_all' },
+
 ]
diff --git a/src/include/utils/wait_event_timing.h b/src/include/utils/wait_event_timing.h
index e3f94fe87b1..22dfc1410fa 100644
--- a/src/include/utils/wait_event_timing.h
+++ b/src/include/utils/wait_event_timing.h
@@ -25,6 +25,7 @@
 #ifndef WAIT_EVENT_TIMING_H
 #define WAIT_EVENT_TIMING_H
 
+#include "port/atomics.h"
 #include "portability/instr_time.h"
 #include "storage/shmem.h"
 #include "utils/wait_event_types.h"
@@ -137,12 +138,28 @@ extern PGDLLIMPORT int wait_event_timing_max_tranches;
  */
 typedef struct WaitEventTimingState
 {
+	/*
+	 * Generation counter for cross-backend reset requests.  Bumped atomically
+	 * by pg_stat_reset_wait_event_timing(target); the owning backend notices
+	 * the change at its next wait_end and clears its own counters.  This
+	 * keeps the hot path lock-free: only the owning backend ever writes its
+	 * statistics, so there is no writer/resetter race.
+	 */
+	pg_atomic_uint32 reset_generation;
+
 	/* Current wait start timestamp (set by pgstat_report_wait_start). */
 	instr_time	wait_start;
 
 	/* Current wait_event_info (cached for use in wait_end). */
 	uint32		current_event;
 
+	/*
+	 * Number of resets this backend has observed and acted on.  Own-backend
+	 * resets are synchronous (one bump per call); cross-backend resets
+	 * coalesce (multiple requests between two wait_ends count as one).
+	 */
+	int64		reset_count;
+
 	/* Per-event statistics: flat array for bounded classes. */
 	WaitEventTimingEntry events[WAIT_EVENT_TIMING_NUM_EVENTS];
 
@@ -151,8 +168,8 @@ typedef struct WaitEventTimingState
 
 	/*
 	 * Count of LWLock events dropped because the LWLock-timing hash reached
-	 * its cap (wait_event_timing_max_tranches).  Written here; a later commit
-	 * in the series exposes it via SQL.
+	 * its cap (wait_event_timing_max_tranches).  Exposed via
+	 * pg_stat_wait_event_timing_overflow.
 	 */
 	int64		lwlock_overflow_count;
 
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 7375d411c81..71c95cb0746 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2434,6 +2434,13 @@ pg_stat_wait_event_timing| SELECT pid,
     max_time_us,
     histogram
    FROM pg_stat_get_wait_event_timing(NULL::integer) t(pid, backend_type, procnumber, wait_event_type, wait_event, calls, total_time_ms, avg_time_us, max_time_us, histogram);
+pg_stat_wait_event_timing_overflow| SELECT pid,
+    backend_type,
+    procnumber,
+    lwlock_overflow_count,
+    flat_overflow_count,
+    reset_count
+   FROM pg_stat_get_wait_event_timing_overflow(NULL::integer) t(pid, backend_type, procnumber, lwlock_overflow_count, flat_overflow_count, reset_count);
 pg_stat_wal| SELECT wal_records,
     wal_fpi,
     wal_bytes,
diff --git a/src/test/regress/expected/wait_event_timing.out b/src/test/regress/expected/wait_event_timing.out
index 925b315c4e8..8938ffe5fb0 100644
--- a/src/test/regress/expected/wait_event_timing.out
+++ b/src/test/regress/expected/wait_event_timing.out
@@ -81,4 +81,55 @@ FROM pg_stat_get_wait_event_timing(-1);
                   0
 (1 row)
 
+-- Overflow/reset counters for this backend.  A simple test backend uses
+-- few LWLock tranches and no out-of-range classes, so both overflow
+-- counters are zero, and a fresh backend has not been reset.
+SELECT lwlock_overflow_count, flat_overflow_count, reset_count
+FROM pg_stat_wait_event_timing_overflow
+WHERE pid = pg_backend_pid();
+ lwlock_overflow_count | flat_overflow_count | reset_count 
+-----------------------+---------------------+-------------
+                     0 |                   0 |           0
+(1 row)
+
+-- Resetting our own backend is synchronous: the PgSleep row is cleared and
+-- reset_count advances.  (We filter to PgSleep because inter-command waits
+-- such as ClientRead may be recorded again before the next statement runs.)
+SELECT pg_stat_reset_wait_event_timing(NULL);
+ pg_stat_reset_wait_event_timing 
+---------------------------------
+ 
+(1 row)
+
+SELECT count(*) AS pgsleep_rows_after_reset
+FROM pg_stat_wait_event_timing
+WHERE pid = pg_backend_pid() AND wait_event = 'PgSleep';
+ pgsleep_rows_after_reset 
+--------------------------
+                        0
+(1 row)
+
+SELECT reset_count
+FROM pg_stat_wait_event_timing_overflow
+WHERE pid = pg_backend_pid();
+ reset_count 
+-------------
+           1
+(1 row)
+
+-- The pid argument defaults to NULL, so a no-argument call resets the
+-- caller's own backend.
+SELECT pg_stat_reset_wait_event_timing();
+ pg_stat_reset_wait_event_timing 
+---------------------------------
+ 
+(1 row)
+
+-- Resetting an unknown pid is a silent no-op, not an error.
+SELECT pg_stat_reset_wait_event_timing(2147483647);
+ pg_stat_reset_wait_event_timing 
+---------------------------------
+ 
+(1 row)
+
 RESET wait_event_capture;
diff --git a/src/test/regress/expected/wait_event_timing_1.out b/src/test/regress/expected/wait_event_timing_1.out
index 038a8565657..3aad898d9bf 100644
--- a/src/test/regress/expected/wait_event_timing_1.out
+++ b/src/test/regress/expected/wait_event_timing_1.out
@@ -82,4 +82,44 @@ FROM pg_stat_get_wait_event_timing(-1);
                   0
 (1 row)
 
+-- Overflow/reset counters for this backend.  A simple test backend uses
+-- few LWLock tranches and no out-of-range classes, so both overflow
+-- counters are zero, and a fresh backend has not been reset.
+SELECT lwlock_overflow_count, flat_overflow_count, reset_count
+FROM pg_stat_wait_event_timing_overflow
+WHERE pid = pg_backend_pid();
+ lwlock_overflow_count | flat_overflow_count | reset_count 
+-----------------------+---------------------+-------------
+(0 rows)
+
+-- Resetting our own backend is synchronous: the PgSleep row is cleared and
+-- reset_count advances.  (We filter to PgSleep because inter-command waits
+-- such as ClientRead may be recorded again before the next statement runs.)
+SELECT pg_stat_reset_wait_event_timing(NULL);
+ERROR:  wait event capture is not supported by this build
+HINT:  Compile PostgreSQL with --enable-wait-event-timing.
+SELECT count(*) AS pgsleep_rows_after_reset
+FROM pg_stat_wait_event_timing
+WHERE pid = pg_backend_pid() AND wait_event = 'PgSleep';
+ pgsleep_rows_after_reset 
+--------------------------
+                        0
+(1 row)
+
+SELECT reset_count
+FROM pg_stat_wait_event_timing_overflow
+WHERE pid = pg_backend_pid();
+ reset_count 
+-------------
+(0 rows)
+
+-- The pid argument defaults to NULL, so a no-argument call resets the
+-- caller's own backend.
+SELECT pg_stat_reset_wait_event_timing();
+ERROR:  wait event capture is not supported by this build
+HINT:  Compile PostgreSQL with --enable-wait-event-timing.
+-- Resetting an unknown pid is a silent no-op, not an error.
+SELECT pg_stat_reset_wait_event_timing(2147483647);
+ERROR:  wait event capture is not supported by this build
+HINT:  Compile PostgreSQL with --enable-wait-event-timing.
 RESET wait_event_capture;
diff --git a/src/test/regress/sql/wait_event_timing.sql b/src/test/regress/sql/wait_event_timing.sql
index da15dbbe495..a30fcd155fd 100644
--- a/src/test/regress/sql/wait_event_timing.sql
+++ b/src/test/regress/sql/wait_event_timing.sql
@@ -51,4 +51,29 @@ WHERE pid = pg_backend_pid() AND wait_event = 'PgSleep';
 SELECT count(*) AS rows_for_bogus_pid
 FROM pg_stat_get_wait_event_timing(-1);
 
+-- Overflow/reset counters for this backend.  A simple test backend uses
+-- few LWLock tranches and no out-of-range classes, so both overflow
+-- counters are zero, and a fresh backend has not been reset.
+SELECT lwlock_overflow_count, flat_overflow_count, reset_count
+FROM pg_stat_wait_event_timing_overflow
+WHERE pid = pg_backend_pid();
+
+-- Resetting our own backend is synchronous: the PgSleep row is cleared and
+-- reset_count advances.  (We filter to PgSleep because inter-command waits
+-- such as ClientRead may be recorded again before the next statement runs.)
+SELECT pg_stat_reset_wait_event_timing(NULL);
+SELECT count(*) AS pgsleep_rows_after_reset
+FROM pg_stat_wait_event_timing
+WHERE pid = pg_backend_pid() AND wait_event = 'PgSleep';
+SELECT reset_count
+FROM pg_stat_wait_event_timing_overflow
+WHERE pid = pg_backend_pid();
+
+-- The pid argument defaults to NULL, so a no-argument call resets the
+-- caller's own backend.
+SELECT pg_stat_reset_wait_event_timing();
+
+-- Resetting an unknown pid is a silent no-op, not an error.
+SELECT pg_stat_reset_wait_event_timing(2147483647);
+
 RESET wait_event_capture;
-- 
2.43.0

