From b1d28f72cfc6fe8925a334a0d8f7da0626d1cf9d Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Wed, 29 Jul 2026 14:11:27 +0000
Subject: [PATCH v1 2/5] pgstat: add new infrastructure for per-backend
 statistics

Add a new infrastructure for per-backend statistics that keep one shared entry
per live backend while keeping their existing shared data as global stats for
exited backends.

Define a common dshash entry header containing the ProcNumber key, BackendType,
and content LWLock. Extend PgStat_KindInfo with per-backend statistics related
informations.

At shared-memory initialization, create a dshash for every participating kind.

Create entries during pgstat_initialize(), so that auxiliary and shared memory
only processes are covered. Nonblocking flushes can then acquire the cached
entry's content lock without performing a dshash lookup, DSA address resolution,
allocation, or hash resize.

Add helpers to fetch per-backend entries, include live entries in global
snapshots, and transfer entries to the global statistics when backends exit.
Add a local cache keyed by statistics kind and ProcNumber for per-backend
fetches.

No statistics kind registers per backend metadata in this commit, so the new
shared memory creation and backend initialization loops are no ops. Subsequent
commits will add WAL, Lock, and IO statistics into the infrastructure individually.

Limit this infrastructure to built in fixed numbered statistics kinds as this is
the only use case we have had so far. We could extend to variable ones later on
if needed.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by:
Discussion:
---
 src/backend/utils/activity/pgstat.c       | 506 ++++++++++++++++++++++
 src/backend/utils/activity/pgstat_shmem.c |  46 ++
 src/include/utils/pgstat_internal.h       |  43 ++
 src/tools/pgindent/typedefs.list          |   4 +
 4 files changed, 599 insertions(+)
  89.6% src/backend/utils/activity/
   9.6% src/include/utils/

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 50cd07822b4..efc7d427f22 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -175,6 +175,41 @@ typedef struct PgStat_SnapshotEntry
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+/* hash table for per-backend stats snapshot entries */
+typedef struct PgStat_PerBackendSnapshotKey
+{
+	PgStat_Kind kind;
+	ProcNumber	procnum;
+} PgStat_PerBackendSnapshotKey;
+
+typedef struct PgStat_PerBackendSnapshotEntry
+{
+	PgStat_PerBackendSnapshotKey key;
+	char		status;			/* for simplehash use */
+	void	   *data;			/* the stats data itself */
+} PgStat_PerBackendSnapshotEntry;
+
+#define SH_PREFIX pgstat_per_backend_snapshot
+#define SH_ELEMENT_TYPE PgStat_PerBackendSnapshotEntry
+#define SH_KEY_TYPE PgStat_PerBackendSnapshotKey
+#define SH_KEY key
+#define SH_HASH_KEY(tb, key) \
+	fasthash32((const char *) &key, sizeof(PgStat_PerBackendSnapshotKey), 0)
+#define SH_EQUAL(tb, a, b) \
+	(memcmp(&a, &b, sizeof(PgStat_PerBackendSnapshotKey)) == 0)
+#define SH_SCOPE static inline
+#define SH_DEFINE
+#define SH_DECLARE
+#include "lib/simplehash.h"
+
+/* Per-kind, backend-local state for the per-backend dshashes. */
+typedef struct PgStat_PerBackendLocalState
+{
+	dshash_table *hash;
+	PgStatShared_PerBackendEntry *my_entry;
+} PgStat_PerBackendLocalState;
+
+static PgStat_PerBackendLocalState per_backend_states[PGSTAT_KIND_BUILTIN_SIZE];
 
 /* ----------
  * Local function forward declarations
@@ -195,6 +230,7 @@ static void pgstat_build_snapshot(void);
 static void pgstat_build_snapshot_fixed(PgStat_Kind kind);
 
 static inline bool pgstat_is_kind_valid(PgStat_Kind kind);
+static void pgstat_create_my_per_backend_entries(void);
 
 
 /* ----------
@@ -673,6 +709,13 @@ pgstat_initialize(void)
 
 	pgstat_attach_shmem();
 
+	/*
+	 * Create and cache per-backend statistics entries here. This also covers
+	 * processes that never call InitPostgres(), such as shared-memory-only
+	 * background workers.
+	 */
+	pgstat_create_my_per_backend_entries();
+
 	pgstat_init_snapshot_fixed();
 
 	/* Backend initialization callbacks */
@@ -946,6 +989,7 @@ pgstat_clear_snapshot(void)
 
 		/* Reset variables */
 		pgStatLocal.snapshot.context = NULL;
+		pgStatLocal.snapshot.per_backend_stats = NULL;
 	}
 
 	/*
@@ -1161,6 +1205,463 @@ pgstat_prep_snapshot(void)
 							   NULL);
 }
 
+/*
+ * Look up a per-backend stats entry in the backend snapshot hash.
+ *
+ * The returned entry may be empty when no matching statistics were found on
+ * first access.
+ */
+static PgStat_PerBackendSnapshotEntry *
+pgstat_lookup_per_backend_entry(PgStat_Kind kind, ProcNumber procnum)
+{
+	PgStat_PerBackendSnapshotKey key;
+
+	if (pgStatLocal.snapshot.per_backend_stats == NULL)
+		return NULL;
+
+	key.kind = kind;
+	key.procnum = procnum;
+
+	return pgstat_per_backend_snapshot_lookup(pgStatLocal.snapshot.per_backend_stats,
+											  key);
+}
+
+/*
+ * Cache a per-backend stats entry in the backend snapshot hash.
+ *
+ * If data is NULL, cache an empty entry to record that no matching statistics
+ * were found on first access.
+ */
+static void *
+pgstat_cache_per_backend_entry(PgStat_Kind kind, ProcNumber procnum,
+							   const void *data)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	PgStat_PerBackendSnapshotKey key;
+	PgStat_PerBackendSnapshotEntry *entry;
+	bool		found;
+
+	Assert(pgstat_fetch_consistency > PGSTAT_FETCH_CONSISTENCY_NONE);
+	Assert(kind_info != NULL);
+	Assert(kind_info->per_backend_data_len > 0);
+
+	/* Ensure snapshot context exists */
+	if (!pgStatLocal.snapshot.context)
+		pgStatLocal.snapshot.context = AllocSetContextCreate(TopMemoryContext,
+															 "PgStat Snapshot",
+															 ALLOCSET_SMALL_SIZES);
+
+	/* Create per-backend hash on first use */
+	if (pgStatLocal.snapshot.per_backend_stats == NULL)
+		pgStatLocal.snapshot.per_backend_stats =
+			pgstat_per_backend_snapshot_create(pgStatLocal.snapshot.context, 64, NULL);
+
+	key.kind = kind;
+	key.procnum = procnum;
+
+	/* If already cached, return cached data */
+	entry = pgstat_per_backend_snapshot_lookup(pgStatLocal.snapshot.per_backend_stats, key);
+
+	if (entry)
+		return entry->data;
+
+	/* Insert new entry into the hash */
+	entry = pgstat_per_backend_snapshot_insert(pgStatLocal.snapshot.per_backend_stats,
+											   key, &found);
+	Assert(!found);
+
+	if (data != NULL)
+	{
+		entry->data = MemoryContextAlloc(pgStatLocal.snapshot.context,
+										 kind_info->per_backend_data_len);
+		memcpy(entry->data, data, kind_info->per_backend_data_len);
+	}
+	else
+		entry->data = NULL;
+
+	return entry->data;
+}
+
+static inline PgStat_PerBackendLocalState *
+pgstat_get_per_backend_local_state(PgStat_Kind kind)
+{
+	const PgStat_KindInfo *kind_info PG_USED_FOR_ASSERTS_ONLY = pgstat_get_kind_info(kind);
+
+	Assert(kind_info != NULL);
+	Assert(kind_info->fixed_amount);
+
+	if (!pgstat_is_kind_builtin(kind))
+		elog(ERROR, "invalid statistics kind: %u", kind);
+
+	return &per_backend_states[kind];
+}
+
+/*
+ * Create and cache this process's entry for one per-backend statistics kind.
+ */
+static PgStatShared_PerBackendEntry *
+pgstat_create_my_per_backend_entry(PgStat_Kind kind)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	PgStat_PerBackendLocalState *state = pgstat_get_per_backend_local_state(kind);
+	dshash_table *hash = pgstat_per_backend_attach(kind);
+	PgStatShared_PerBackendEntry *entry;
+	bool		found;
+
+	Assert(state->my_entry == NULL);
+
+	if (hash == NULL)
+		return NULL;
+
+	Assert(kind_info != NULL);
+	Assert(kind_info->per_backend_data_len > 0);
+
+	entry = dshash_find_or_insert(hash, &MyProcNumber, &found);
+
+	/*
+	 * A forced flush during early backend startup may already have created
+	 * the entry. Preserve any statistics it contains.
+	 */
+	if (!found)
+	{
+		entry->backend_type = MyBackendType;
+		LWLockInitialize(&entry->lock, LWTRANCHE_PGSTATS_DATA);
+		memset((char *) entry + kind_info->per_backend_data_off, 0,
+			   kind_info->per_backend_data_len);
+	}
+
+	state->my_entry = entry;
+	dshash_release_lock(hash, entry);
+
+	return entry;
+}
+
+/*
+ * Create entries for all the kinds that use per-backend dshashes.
+ *
+ * Allocations and dshash resizes are deliberately done during backend
+ * initialization so routine stats flushes only need to conditionally acquire
+ * the cached entry's content lock.
+ */
+static void
+pgstat_create_my_per_backend_entries(void)
+{
+	for (PgStat_Kind kind = PGSTAT_KIND_BUILTIN_MIN;
+		 kind <= PGSTAT_KIND_BUILTIN_MAX; kind++)
+	{
+		const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+		if (kind_info == NULL || kind_info->per_backend_data_len == 0)
+			continue;
+
+		(void) pgstat_create_my_per_backend_entry(kind);
+	}
+}
+
+/*
+ * Attach to the per-backend dshash for the given kind.
+ * Returns NULL if the hash is not available (e.g. during bootstrap or
+ * if this kind doesn't have per-backend tracking).
+ */
+dshash_table *
+pgstat_per_backend_attach(PgStat_Kind kind)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	PgStat_PerBackendLocalState *state = pgstat_get_per_backend_local_state(kind);
+	char	   *shared_struct;
+	dshash_table_handle *handle_ptr;
+	MemoryContext oldcontext;
+	dshash_parameters params;
+
+	if (state->hash != NULL)
+		return state->hash;
+
+	if (!kind_info || !kind_info->per_backend_data_len)
+		return NULL;
+
+	/* Get the shared struct for this kind */
+	shared_struct = (char *) pgStatLocal.shmem + kind_info->shared_ctl_off;
+	handle_ptr = (dshash_table_handle *) (shared_struct + kind_info->per_backend_hash_handle_off);
+
+	if (*handle_ptr == DSHASH_HANDLE_INVALID)
+		return NULL;
+
+	/*
+	 * Build dshash parameters from kind info. All per-backend hashes use
+	 * ProcNumber keys and dshash_memcmp/dshash_memhash.
+	 */
+	params.key_size = sizeof(ProcNumber);
+	params.entry_size = kind_info->per_backend_data_off + kind_info->per_backend_data_len;
+	params.compare_function = dshash_memcmp;
+	params.hash_function = dshash_memhash;
+	params.copy_function = dshash_memcpy;
+	params.tranche_id = LWTRANCHE_PGSTATS_HASH;
+
+	/* Attach in TopMemoryContext */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	state->hash = dshash_attach(pgStatLocal.dsa, &params, *handle_ptr, NULL);
+	MemoryContextSwitchTo(oldcontext);
+
+	return state->hash;
+}
+
+/*
+ * Lock this process's cached entry for a per-backend statistics kind.
+ *
+ * A missing entry is only created in the blocking path. The routine nowait
+ * path must not perform dshash lookups, allocations, or DSA address
+ * resolution.
+ */
+void *
+pgstat_lock_my_per_backend_entry(PgStat_Kind kind, bool nowait)
+{
+	const PgStat_KindInfo *kind_info PG_USED_FOR_ASSERTS_ONLY = pgstat_get_kind_info(kind);
+	PgStat_PerBackendLocalState *state = pgstat_get_per_backend_local_state(kind);
+	PgStatShared_PerBackendEntry *entry = state->my_entry;
+
+	Assert(kind_info != NULL);
+	Assert(kind_info->per_backend_data_len > 0);
+
+	if (entry == NULL)
+	{
+		if (nowait)
+			return NULL;
+
+		entry = pgstat_create_my_per_backend_entry(kind);
+		if (entry == NULL)
+			return NULL;
+	}
+
+	if (nowait)
+	{
+		if (!LWLockConditionalAcquire(&entry->lock, LW_EXCLUSIVE))
+			return NULL;
+	}
+	else
+		LWLockAcquire(&entry->lock, LW_EXCLUSIVE);
+
+	return entry;
+}
+
+/*
+ * Accumulate all live per-backend entries into the kind's data in
+ * pgStatLocal.snapshot, optionally caching each entry for SNAPSHOT mode.
+ */
+void
+pgstat_per_backend_snapshot(PgStat_Kind kind, dshash_table *hash, void *snap)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	dshash_seq_status hstat;
+	PgStatShared_PerBackendEntry *entry;
+
+	dshash_seq_init(&hstat, hash, false);
+
+	while ((entry = dshash_seq_next(&hstat)) != NULL)
+	{
+		LWLockAcquire(&entry->lock, LW_SHARED);
+
+		/* Kind-specific accumulation into global snapshot */
+		kind_info->per_backend_acc_cb(snap, entry);
+
+		/*
+		 * In SNAPSHOT mode, cache each per-backend entry so that
+		 * pgstat_fetch_per_backend() can return a consistent point-in-time
+		 * view without re-reading from shared memory.
+		 */
+		if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_SNAPSHOT)
+		{
+			pgstat_cache_per_backend_entry(kind, entry->key,
+										   (char *) entry + kind_info->per_backend_data_off);
+		}
+
+		LWLockRelease(&entry->lock);
+	}
+
+	dshash_seq_term(&hstat);
+}
+
+/*
+ * Fetch per-backend stats for the given kind and ProcNumber.
+ * Returns NULL if no entry exists. In NONE mode, returns a copy allocated in
+ * the current memory context. In CACHE and SNAPSHOT modes, returns a pointer
+ * owned by the statistics snapshot cache.
+ */
+void *
+pgstat_fetch_per_backend(PgStat_Kind kind, ProcNumber procnum)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	dshash_table *hash;
+	PgStatShared_PerBackendEntry *entry;
+	void	   *stats_data;
+	PgStat_PerBackendSnapshotEntry *snapshot_entry;
+
+	if (force_stats_snapshot_clear)
+		pgstat_clear_snapshot();
+
+	hash = pgstat_per_backend_attach(kind);
+
+	if (hash == NULL)
+		return NULL;
+
+	/* In NONE mode, read directly and don't cache */
+	if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_NONE)
+	{
+		entry = dshash_find(hash, &procnum, false);
+		if (entry == NULL)
+			return NULL;
+
+		LWLockAcquire(&entry->lock, LW_SHARED);
+		stats_data = palloc(kind_info->per_backend_data_len);
+		memcpy(stats_data, (char *) entry + kind_info->per_backend_data_off,
+			   kind_info->per_backend_data_len);
+		LWLockRelease(&entry->lock);
+		dshash_release_lock(hash, entry);
+
+		return stats_data;
+	}
+
+	Assert(pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_CACHE ||
+		   pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_SNAPSHOT);
+
+	/*
+	 * Building a full snapshot pre-caches all existing per-backend entries.
+	 * CACHE mode only needs the requested entry, so it must not build the
+	 * aggregate fixed-kind snapshot and scan every live backend.
+	 */
+	if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_SNAPSHOT)
+		pgstat_snapshot_fixed(kind);
+
+	snapshot_entry = pgstat_lookup_per_backend_entry(kind, procnum);
+
+	if (snapshot_entry != NULL)
+		return snapshot_entry->data;
+
+	/*
+	 * Once a full snapshot has been built, a cache miss means the entry did
+	 * not exist at the snapshot point. Do not admit an entry created later.
+	 */
+	if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_SNAPSHOT)
+		return NULL;
+
+	/* CACHE miss: copy the live entry directly into the snapshot context. */
+	entry = dshash_find(hash, &procnum, false);
+
+	if (entry == NULL)
+		return pgstat_cache_per_backend_entry(kind, procnum, NULL);
+
+	LWLockAcquire(&entry->lock, LW_SHARED);
+
+	stats_data = pgstat_cache_per_backend_entry(kind, procnum,
+												(char *) entry + kind_info->per_backend_data_off);
+
+	LWLockRelease(&entry->lock);
+	dshash_release_lock(hash, entry);
+
+	return stats_data;
+}
+
+/*
+ * Accumulate this process's per-backend stats into the global stats, then
+ * remove the entry from the dshash.
+ * Acquire the kind lock before the dshash partition lock. Snapshots and resets
+ * hold the kind lock while accessing both the global stats and live entries,
+ * so an entry cannot move between them during either operation.
+ *
+ * NB: The entry may belong to an earlier process that used the same ProcNumber.
+ * It must still be accumulated before removal.
+ */
+void
+pgstat_acc_my_per_backend(PgStat_Kind kind, LWLock *lock)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	PgStat_PerBackendLocalState *state = pgstat_get_per_backend_local_state(kind);
+	char	   *shared_struct;
+	dshash_table *hash;
+	void	   *dst;
+	PgStatShared_PerBackendEntry *entry;
+
+	hash = pgstat_per_backend_attach(kind);
+
+	if (hash == NULL)
+		return;
+
+	shared_struct = (char *) pgStatLocal.shmem + kind_info->shared_ctl_off;
+	dst = shared_struct + kind_info->shared_data_off;
+
+	LWLockAcquire(lock, LW_EXCLUSIVE);
+
+	entry = dshash_find(hash, &MyProcNumber, true);
+
+	if (entry == NULL)
+	{
+		state->my_entry = NULL;
+		LWLockRelease(lock);
+		return;
+	}
+
+	LWLockAcquire(&entry->lock, LW_EXCLUSIVE);
+
+	if (state->my_entry == entry)
+		state->my_entry = NULL;
+
+	/* Kind-specific accumulation */
+	kind_info->per_backend_acc_cb(dst, entry);
+
+	LWLockRelease(&entry->lock);
+
+	/* Remove the entry */
+	dshash_delete_entry(hash, entry);
+
+	LWLockRelease(lock);
+}
+
+/*
+ * Accumulate all per-backend entries into global stats and delete them.
+ * Called at clean server shutdown before writing the stats file. Acquire the
+ * kind's global lock before starting the dshash scan.
+ */
+void
+pgstat_acc_all_per_backend(PgStat_Kind kind, LWLock *lock)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	PgStat_PerBackendLocalState *state = pgstat_get_per_backend_local_state(kind);
+	char	   *shared_struct;
+	dshash_table *hash;
+	void	   *dst;
+	dshash_seq_status hstat;
+	PgStatShared_PerBackendEntry *entry;
+
+	hash = pgstat_per_backend_attach(kind);
+
+	if (hash == NULL)
+		return;
+
+	shared_struct = (char *) pgStatLocal.shmem + kind_info->shared_ctl_off;
+	dst = shared_struct + kind_info->shared_data_off;
+
+	LWLockAcquire(lock, LW_EXCLUSIVE);
+
+	dshash_seq_init(&hstat, hash, true);
+
+	while ((entry = dshash_seq_next(&hstat)) != NULL)
+	{
+		LWLockAcquire(&entry->lock, LW_EXCLUSIVE);
+
+		if (state->my_entry == entry)
+			state->my_entry = NULL;
+
+		/* Kind-specific accumulation */
+		kind_info->per_backend_acc_cb(dst, entry);
+
+		LWLockRelease(&entry->lock);
+		dshash_delete_current(&hstat);
+	}
+
+	dshash_seq_term(&hstat);
+
+	LWLockRelease(lock);
+}
+
 static void
 pgstat_build_snapshot(void)
 {
@@ -1540,6 +2041,11 @@ pgstat_register_kind(PgStat_Kind kind, const PgStat_KindInfo *kind_info)
 				 errdetail("Custom cumulative statistics must be registered while initializing modules in \"%s\".",
 						   "shared_preload_libraries")));
 
+	if (kind_info->per_backend_data_len != 0)
+		ereport(ERROR,
+				(errmsg("failed to register custom cumulative statistics \"%s\" with ID %u", kind_info->name, kind),
+				 errdetail("Per-backend statistics are not supported for custom cumulative statistics.")));
+
 	/*
 	 * Check some data for fixed-numbered stats.
 	 */
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 8511c25fd29..3b003f00245 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -168,6 +168,47 @@ StatsShmemRequest(void *arg)
 		);
 }
 
+/*
+ * Create a dshash for each built-in kind that stores per-backend statistics.
+ * Derive the entry size and handle location from the kind metadata, just as
+ * attachment does.
+ */
+static void
+pgstat_create_per_backend_hashes(dsa_area *dsa, PgStat_ShmemControl *ctl)
+{
+	dshash_parameters params = {
+		sizeof(ProcNumber),
+		0,
+		dshash_memcmp,
+		dshash_memhash,
+		dshash_memcpy,
+		LWTRANCHE_PGSTATS_HASH
+	};
+
+	for (PgStat_Kind kind = PGSTAT_KIND_BUILTIN_MIN;
+		 kind <= PGSTAT_KIND_BUILTIN_MAX; kind++)
+	{
+		const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+		char	   *shared_struct;
+		dshash_table_handle *handle_ptr;
+		dshash_table *dsh;
+
+		if (kind_info == NULL || kind_info->per_backend_data_len == 0)
+			continue;
+
+		shared_struct = (char *) ctl + kind_info->shared_ctl_off;
+		handle_ptr = (dshash_table_handle *)
+			(shared_struct + kind_info->per_backend_hash_handle_off);
+
+		params.entry_size = kind_info->per_backend_data_off +
+			kind_info->per_backend_data_len;
+
+		dsh = dshash_create(dsa, &params, NULL);
+		*handle_ptr = dshash_get_hash_table_handle(dsh);
+		dshash_detach(dsh);
+	}
+}
+
 /*
  * Initialize cumulative statistics system during startup
  */
@@ -210,6 +251,11 @@ StatsShmemInit(void *arg)
 	/* lift limit set above */
 	dsa_set_size_limit(dsa, -1);
 
+	/*
+	 * Create per-backend hashes while the local DSA reference is available.
+	 */
+	pgstat_create_per_backend_hashes(dsa, ctl);
+
 	/*
 	 * Postmaster will never access these again, thus free the local
 	 * dsa/dshash references.
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b0a17691966..340244252c9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -380,6 +380,26 @@ typedef struct PgStat_KindInfo
 	 */
 	void		(*snapshot_cb) (void);
 
+	/*
+	 * Per-backend dshash support for built-in fixed-numbered statistics kinds
+	 * that also maintain per-backend entries in a dedicated dshash. If
+	 * per_backend_data_len is non-zero, the generic infrastructure handles
+	 * attach, fetch, accumulate, and snapshot pre-caching automatically.
+	 *
+	 * Each entry starts with PgStatShared_PerBackendEntry, followed by the
+	 * kind-specific statistics payload.
+	 */
+	uint32		per_backend_data_off;	/* offset of stats data in entry */
+	uint32		per_backend_data_len;	/* size of stats data in entry */
+	/* offset of dshash_table_handle in shared struct */
+	uint32		per_backend_hash_handle_off;
+
+	/*
+	 * Callback to accumulate one per-backend entry into a destination of the
+	 * kind's statistics type. Called with the entry's content lock held.
+	 */
+	void		(*per_backend_acc_cb) (void *dst, void *entry);
+
 	/* name of the kind of stats */
 	const char *const name;
 } PgStat_KindInfo;
@@ -479,6 +499,17 @@ typedef struct PgStatShared_SLRU
 	PgStat_SLRUStats stats[SLRU_NUM_ELEMENTS];
 } PgStatShared_SLRU;
 
+/*
+ * Common header for entries in per-backend statistics dshashes. The
+ * ProcNumber key must be the first field for dshash.
+ */
+typedef struct PgStatShared_PerBackendEntry
+{
+	ProcNumber	key;
+	BackendType backend_type;
+	LWLock		lock;
+} PgStatShared_PerBackendEntry;
+
 typedef struct PgStatShared_Wal
 {
 	/* lock protects ->stats */
@@ -617,6 +648,9 @@ typedef struct PgStat_Snapshot
 
 	PgStat_WalStats wal;
 
+	/* Per-backend snapshot hash */
+	struct pgstat_per_backend_snapshot_hash *per_backend_stats;
+
 	/*
 	 * Data in snapshot for custom fixed-numbered statistics, indexed by
 	 * (PgStat_Kind - PGSTAT_KIND_CUSTOM_MIN).  Each entry is allocated in
@@ -691,6 +725,15 @@ extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid,
 								bool *may_free);
 extern void pgstat_snapshot_fixed(PgStat_Kind kind);
 
+/* Generic per-backend helpers */
+extern dshash_table *pgstat_per_backend_attach(PgStat_Kind kind);
+extern void *pgstat_lock_my_per_backend_entry(PgStat_Kind kind, bool nowait);
+extern void pgstat_per_backend_snapshot(PgStat_Kind kind, dshash_table *hash,
+										void *snap);
+extern void *pgstat_fetch_per_backend(PgStat_Kind kind, ProcNumber procnum);
+extern void pgstat_acc_my_per_backend(PgStat_Kind kind, LWLock *lock);
+extern void pgstat_acc_all_per_backend(PgStat_Kind kind, LWLock *lock);
+
 
 /*
  * Functions in pgstat_archiver.c
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 85d989f395d..0b7dbc42e80 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2327,6 +2327,7 @@ PgStatShared_Function
 PgStatShared_HashEntry
 PgStatShared_IO
 PgStatShared_Lock
+PgStatShared_PerBackendEntry
 PgStatShared_Relation
 PgStatShared_ReplSlot
 PgStatShared_SLRU
@@ -2351,6 +2352,9 @@ PgStat_KindInfo
 PgStat_LocalState
 PgStat_Lock
 PgStat_LockEntry
+PgStat_PerBackendLocalState
+PgStat_PerBackendSnapshotEntry
+PgStat_PerBackendSnapshotKey
 PgStat_PendingDroppedStatsItem
 PgStat_PendingIO
 PgStat_PendingLock
-- 
2.34.1

