From 408726452c655446c9934163edac71af2a64ad93 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 v4 2/6] pgstat: add new infrastructure for per-backend
 statistics

Add infrastructure for per-backend statistics that keeps one shared array entry
per possible process, while retaining the existing shared data as global statistics
for exited processes.

Define a common array entry header containing a BackendType and content
LWLock. B_INVALID marks an unused entry. Extend PgStat_KindInfo with metadata
describing the entry and its statistics payload.

At shared-memory initialization, allocate a ProcNumber-indexed array for every
participating kind. Activate the current process entries during pgstat_initialize(),
so auxiliary and shared-memory-only processes are covered.

Add helpers to fetch per-backend entries, include active entries in global
snapshots, and transfer entries to the global statistics when processes
exit. Reuse the backend-local statistics snapshot hash for per-backend
fetches, keyed by statistics kind and ProcNumber.

No statistics kind registers per-backend metadata in this commit, so the
new shared-memory allocation and process initialization loops are no ops.
Subsequent commits add WAL, lock, and I/O statistics to the infrastructure
individually.

Limit this infrastructure to built-in fixed-numbered statistics kinds, as
this is the only use case so far. It could be extended later if needed.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/anCjoFkGah/uB7lw%40bdtpg
---
 src/backend/utils/activity/Makefile           |   1 +
 src/backend/utils/activity/meson.build        |   1 +
 src/backend/utils/activity/pgstat.c           |  72 +++-
 .../utils/activity/pgstat_per_backend.c       | 340 ++++++++++++++++++
 src/backend/utils/activity/pgstat_shmem.c     |  45 +++
 src/include/utils/pgstat_internal.h           |  71 ++++
 src/tools/pgindent/typedefs.list              |   1 +
 7 files changed, 529 insertions(+), 2 deletions(-)
  81.8% src/backend/utils/activity/
  17.9% src/include/utils/

diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index 2e32d1485d6..8924b10ed2e 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -29,6 +29,7 @@ OBJS = \
 	pgstat_io.o \
 	pgstat_kind.o \
 	pgstat_lock.o \
+	pgstat_per_backend.o \
 	pgstat_relation.o \
 	pgstat_replslot.o \
 	pgstat_shmem.o \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index e6dcb2e26fc..6434c377e53 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -14,6 +14,7 @@ backend_sources += files(
   'pgstat_io.c',
   'pgstat_kind.c',
   'pgstat_lock.c',
+  'pgstat_per_backend.c',
   'pgstat_relation.c',
   'pgstat_replslot.c',
   'pgstat_shmem.c',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index d31c0b48a1d..3dd03a71bba 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -69,6 +69,7 @@
  * To keep things manageable, stats handling is split across several
  * files. Infrastructure pieces are in:
  * - pgstat.c - this file, to tie it all together
+ * - pgstat_per_backend.c - generic per-backend statistics infrastructure
  * - pgstat_shmem.c - nearly everything dealing with shared memory, including
  *   the maintenance of hashtable entries
  * - pgstat_xact.c - transactional integration, including the transactional
@@ -189,7 +190,6 @@ static void pgstat_reset_after_failure(void);
 
 static bool pgstat_flush_pending_entries(bool nowait);
 
-static void pgstat_prep_snapshot(void);
 static void pgstat_build_snapshot(void);
 static void pgstat_build_snapshot_fixed(PgStat_Kind kind);
 
@@ -688,6 +688,13 @@ pgstat_initialize(void)
 
 	pgstat_attach_shmem();
 
+	/*
+	 * Activate 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 */
@@ -1155,7 +1162,12 @@ pgstat_init_snapshot_fixed(void)
 	}
 }
 
-static void
+/*
+ * Prepare the backend-local statistics snapshot.
+ *
+ * Clear invalidated snapshot state first. NONE mode does not need a cache.
+ */
+void
 pgstat_prep_snapshot(void)
 {
 	if (force_stats_snapshot_clear)
@@ -1176,6 +1188,57 @@ pgstat_prep_snapshot(void)
 							   NULL);
 }
 
+/*
+ * Look up an entry in the backend-local statistics snapshot.
+ *
+ * The snapshot must have been prepared by pgstat_prep_snapshot(). Set *found
+ * for both entries containing data and cached negative lookups.
+ */
+void *
+pgstat_lookup_snapshot_entry(const PgStat_HashKey *key, bool *found)
+{
+	PgStat_SnapshotEntry *entry;
+
+	Assert(pgstat_fetch_consistency > PGSTAT_FETCH_CONSISTENCY_NONE);
+	Assert(pgStatLocal.snapshot.stats != NULL);
+
+	entry = pgstat_snapshot_lookup(pgStatLocal.snapshot.stats, *key);
+	*found = entry != NULL;
+
+	return entry != NULL ? entry->data : NULL;
+}
+
+/*
+ * Copy an entry into the backend-local statistics snapshot.
+ *
+ * A NULL data pointer records a negative lookup. Return an existing entry
+ * without replacing it if the key was already cached.
+ */
+void *
+pgstat_cache_snapshot_entry(const PgStat_HashKey *key, const void *data,
+							Size data_len)
+{
+	PgStat_SnapshotEntry *entry;
+	bool		found;
+
+	Assert(pgstat_fetch_consistency > PGSTAT_FETCH_CONSISTENCY_NONE);
+	Assert(pgStatLocal.snapshot.stats != NULL);
+
+	entry = pgstat_snapshot_insert(pgStatLocal.snapshot.stats, *key, &found);
+	if (found)
+		return entry->data;
+
+	if (data != NULL)
+	{
+		entry->data = MemoryContextAlloc(pgStatLocal.snapshot.context, data_len);
+		memcpy(entry->data, data, data_len);
+	}
+	else
+		entry->data = NULL;
+
+	return entry->data;
+}
+
 static void
 pgstat_build_snapshot(void)
 {
@@ -1560,6 +1623,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_entry_size != 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_per_backend.c b/src/backend/utils/activity/pgstat_per_backend.c
new file mode 100644
index 00000000000..629559e369f
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_per_backend.c
@@ -0,0 +1,340 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_per_backend.c
+ *	  Generic infrastructure for per-backend statistics.
+ *
+ * This file manages the per-kind arrays used for per-backend statistics,
+ * including entry creation, fetching, snapshots, transfer to global
+ * statistics, and invalidation.
+ *
+ * Copyright (c) 2001-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/activity/pgstat_per_backend.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "storage/proc.h"
+#include "utils/pgstat_internal.h"
+
+/*
+ * Return one per-backend entry, whether or not it is currently active.
+ *
+ * Callers that inspect or modify the entry must hold its content lock.
+ */
+void *
+pgstat_get_per_backend_entry(PgStat_Kind kind, ProcNumber procnum)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	char	   *array;
+
+	if (kind_info == NULL || kind_info->per_backend_entry_size == 0)
+		return NULL;
+
+	Assert(pgstat_is_kind_builtin(kind));
+
+	if (procnum < 0 || procnum >= NumPerBackendStatSlots)
+		return NULL;
+
+	array = pgStatLocal.shmem->per_backend_arrays[kind];
+	Assert(array != NULL);
+
+	return array + procnum * kind_info->per_backend_entry_size;
+}
+
+/*
+ * Initialize this process's entry for one per-backend statistics kind.
+ *
+ * A blocking flush during early startup may already have initialized the
+ * entry. In that case, preserve the statistics it contains.
+ */
+static PgStatShared_PerBackendEntry *
+pgstat_create_my_per_backend_entry(PgStat_Kind kind)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	PgStatShared_PerBackendEntry *entry;
+
+	entry = pgstat_get_per_backend_entry(kind, MyProcNumber);
+
+	LWLockAcquire(&entry->lock, LW_EXCLUSIVE);
+
+	if (entry->backend_type == B_INVALID)
+	{
+		entry->backend_type = MyBackendType;
+		memset((char *) entry + kind_info->per_backend_data_off, 0,
+			   kind_info->per_backend_data_len);
+	}
+
+	LWLockRelease(&entry->lock);
+
+	return entry;
+}
+
+/*
+ * Initialize entries for all kinds that use per-backend arrays.
+ *
+ * This runs after any stale entries for the reused ProcNumber have been
+ * transferred to global statistics.
+ */
+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_entry_size == 0)
+			continue;
+
+		(void) pgstat_create_my_per_backend_entry(kind);
+	}
+}
+
+/*
+ * Lock this process's entry for a per-backend statistics kind.
+ *
+ * The blocking path initializes an inactive entry, which allows forced
+ * flushes during early process startup. The routine nonblocking path does not
+ * initialize entries.
+ *
+ * NB: On success, the entry is returned with its content lock held. A
+ * nonblocking call returns NULL if the entry cannot be locked or is inactive.
+ */
+void *
+pgstat_lock_my_per_backend_entry(PgStat_Kind kind, bool nowait)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	PgStatShared_PerBackendEntry *entry;
+
+	entry = pgstat_get_per_backend_entry(kind, MyProcNumber);
+
+	if (nowait)
+	{
+		if (!LWLockConditionalAcquire(&entry->lock, LW_EXCLUSIVE))
+			return NULL;
+
+		if (entry->backend_type == B_INVALID)
+		{
+			LWLockRelease(&entry->lock);
+			return NULL;
+		}
+	}
+	else
+	{
+		LWLockAcquire(&entry->lock, LW_EXCLUSIVE);
+
+		if (entry->backend_type == B_INVALID)
+		{
+			entry->backend_type = MyBackendType;
+			memset((char *) entry + kind_info->per_backend_data_off, 0,
+				   kind_info->per_backend_data_len);
+		}
+	}
+
+	return entry;
+}
+
+/*
+ * Accumulate all active per-backend entries into a fixed-kind snapshot.
+ *
+ * SNAPSHOT mode also copies every active entry into the backend-local
+ * snapshot hash, fixing both the values and the set of visible processes.
+ * The caller must hold the kind lock to prevent entries from moving into the
+ * global statistics while the snapshot is assembled.
+ */
+void
+pgstat_per_backend_snapshot(PgStat_Kind kind, void *snap)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	PgStat_HashKey key = {0};
+
+	key.kind = kind;
+	key.dboid = InvalidOid;
+
+	for (ProcNumber procnum = 0; procnum < NumPerBackendStatSlots; procnum++)
+	{
+		PgStatShared_PerBackendEntry *entry;
+
+		entry = pgstat_get_per_backend_entry(kind, procnum);
+
+		LWLockAcquire(&entry->lock, LW_SHARED);
+
+		if (entry->backend_type != B_INVALID)
+		{
+			kind_info->per_backend_acc_cb(snap, entry);
+
+			if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_SNAPSHOT)
+			{
+				key.objid = procnum;
+				pgstat_cache_snapshot_entry(&key,
+											(char *) entry +
+											kind_info->per_backend_data_off,
+											kind_info->per_backend_data_len);
+			}
+		}
+
+		LWLockRelease(&entry->lock);
+	}
+}
+
+/*
+ * Fetch one process's per-backend statistics.
+ *
+ * NONE mode returns a copy allocated in the current memory context. CACHE and
+ * SNAPSHOT modes return data owned by the backend-local statistics snapshot.
+ * Return NULL if the ProcNumber is invalid or no process was active at the
+ * consistency mode's observation point.
+ */
+void *
+pgstat_fetch_per_backend(PgStat_Kind kind, ProcNumber procnum)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	PgStatShared_PerBackendEntry *entry;
+	PgStat_HashKey key = {0};
+	void	   *stats_data;
+	bool		found;
+
+	pgstat_prep_snapshot();
+
+	entry = pgstat_get_per_backend_entry(kind, procnum);
+
+	if (entry == NULL)
+		return NULL;
+
+	/* Read directly without caching in NONE mode */
+	if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_NONE)
+	{
+		LWLockAcquire(&entry->lock, LW_SHARED);
+
+		if (entry->backend_type == B_INVALID)
+		{
+			LWLockRelease(&entry->lock);
+			return NULL;
+		}
+
+		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);
+
+		return stats_data;
+	}
+
+	Assert(pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_CACHE ||
+		   pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_SNAPSHOT);
+
+	/*
+	 * A full fixed-kind snapshot pre-caches all active entries. CACHE mode
+	 * reads only the requested array entry on a cache miss.
+	 */
+	if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_SNAPSHOT)
+		pgstat_snapshot_fixed(kind);
+	else
+		pgStatLocal.snapshot.mode = PGSTAT_FETCH_CONSISTENCY_CACHE;
+
+	key.kind = kind;
+	key.dboid = InvalidOid;
+	key.objid = procnum;
+
+	stats_data = pgstat_lookup_snapshot_entry(&key, &found);
+
+	if (found)
+		return stats_data;
+
+	/*
+	 * A miss after building a full snapshot means that the process was not
+	 * active at the snapshot point.
+	 */
+	if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_SNAPSHOT)
+		return NULL;
+
+	LWLockAcquire(&entry->lock, LW_SHARED);
+
+	if (entry->backend_type == B_INVALID)
+		stats_data = pgstat_cache_snapshot_entry(&key, NULL, 0);
+	else
+		stats_data = pgstat_cache_snapshot_entry(&key,
+												 (char *) entry +
+												 kind_info->per_backend_data_off,
+												 kind_info->per_backend_data_len);
+
+	LWLockRelease(&entry->lock);
+
+	return stats_data;
+}
+
+/*
+ * Transfer this process's entry into global statistics and invalidate it.
+ *
+ * The kind lock serializes this transfer with global snapshots and resets.
+ * The entry lock serializes it with routine flushes and per-backend reads.
+ */
+void
+pgstat_acc_my_per_backend(PgStat_Kind kind, LWLock *lock)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	PgStatShared_PerBackendEntry *entry;
+	char	   *shared_struct;
+	void	   *dst;
+
+	entry = pgstat_get_per_backend_entry(kind, MyProcNumber);
+
+	if (entry == NULL)
+		return;
+
+	shared_struct = (char *) pgStatLocal.shmem + kind_info->shared_ctl_off;
+	dst = shared_struct + kind_info->shared_data_off;
+
+	LWLockAcquire(lock, LW_EXCLUSIVE);
+	LWLockAcquire(&entry->lock, LW_EXCLUSIVE);
+
+	if (entry->backend_type != B_INVALID)
+	{
+		kind_info->per_backend_acc_cb(dst, entry);
+		entry->backend_type = B_INVALID;
+	}
+
+	LWLockRelease(&entry->lock);
+	LWLockRelease(lock);
+}
+
+/*
+ * Transfer every active entry into global statistics and invalidate it.
+ *
+ * This is called during clean server shutdown before the statistics file is
+ * written.
+ */
+void
+pgstat_acc_all_per_backend(PgStat_Kind kind, LWLock *lock)
+{
+	const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+	char	   *shared_struct;
+	void	   *dst;
+
+	shared_struct = (char *) pgStatLocal.shmem + kind_info->shared_ctl_off;
+	dst = shared_struct + kind_info->shared_data_off;
+
+	LWLockAcquire(lock, LW_EXCLUSIVE);
+
+	for (ProcNumber procnum = 0; procnum < NumPerBackendStatSlots; procnum++)
+	{
+		PgStatShared_PerBackendEntry *entry;
+
+		entry = pgstat_get_per_backend_entry(kind, procnum);
+
+		LWLockAcquire(&entry->lock, LW_EXCLUSIVE);
+
+		if (entry->backend_type != B_INVALID)
+		{
+			kind_info->per_backend_acc_cb(dst, entry);
+			entry->backend_type = B_INVALID;
+		}
+
+		LWLockRelease(&entry->lock);
+	}
+
+	LWLockRelease(lock);
+}
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 8500927a68b..c2116fe087f 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -164,6 +164,21 @@ StatsShmemSize(void)
 		sz = add_size(sz, MAXALIGN(kind_info->shared_size));
 	}
 
+	/* Add the per-backend arrays for built-in fixed-numbered kinds */
+	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 || kind_info->per_backend_entry_size == 0)
+			continue;
+
+		Assert(kind_info->fixed_amount);
+		Assert(kind_info->per_backend_acc_cb != NULL);
+
+		sz = add_size(sz, MAXALIGN(mul_size(NumPerBackendStatSlots,
+											kind_info->per_backend_entry_size)));
+	}
+
 	return sz;
 }
 
@@ -296,6 +311,36 @@ StatsShmemInit(void *arg)
 			kind_info->init_shmem_cb(ptr);
 		}
 	}
+
+	/*
+	 * Allocate and initialize arrays for built-in fixed-numbered kinds that
+	 * keep per-backend statistics.
+	 */
+	memset(ctl->per_backend_arrays, 0, sizeof(ctl->per_backend_arrays));
+
+	for (PgStat_Kind kind = PGSTAT_KIND_BUILTIN_MIN; kind <= PGSTAT_KIND_BUILTIN_MAX; kind++)
+	{
+		const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+		Size		array_size;
+
+		if (!kind_info || kind_info->per_backend_entry_size == 0)
+			continue;
+
+		array_size = mul_size(NumPerBackendStatSlots, kind_info->per_backend_entry_size);
+
+		ctl->per_backend_arrays[kind] = p;
+
+		for (ProcNumber procnum = 0; procnum < NumPerBackendStatSlots; procnum++)
+		{
+			PgStatShared_PerBackendEntry *entry;
+
+			entry = (PgStatShared_PerBackendEntry *) (p + procnum * kind_info->per_backend_entry_size);
+			LWLockInitialize(&entry->lock, LWTRANCHE_PGSTATS_DATA);
+			entry->backend_type = B_INVALID;
+		}
+
+		p += MAXALIGN(array_size);
+	}
 }
 
 void
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 21403516e28..da5a67cf827 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -19,6 +19,7 @@
 #include "lib/ilist.h"
 #include "pgstat.h"
 #include "storage/lwlock.h"
+#include "storage/proc.h"
 #include "utils/dsa.h"
 
 
@@ -54,6 +55,8 @@
  * NB: We assume that this struct contains no padding.  Also, 8 bytes
  * allocated for the object ID are good enough to ensure the uniqueness
  * of the hash key, hence the addition of new fields is not recommended.
+ * Backend-local snapshots also use this key for per-backend fixed statistics,
+ * with InvalidOid as the database ID and ProcNumber as the object ID.
  */
 typedef struct PgStat_HashKey
 {
@@ -390,6 +393,28 @@ typedef struct PgStat_KindInfo
 	 */
 	void		(*snapshot_cb) (void);
 
+	/*
+	 * Per-backend array metadata for built-in fixed-numbered statistics
+	 * kinds.
+	 *
+	 * A nonzero per_backend_entry_size enables array allocation and process
+	 * entry initialization. Zero means that the kind has no per-backend
+	 * entries. The remaining fields describe the kind-specific payload used
+	 * by the generic fetch, snapshot, and accumulation helpers.
+	 *
+	 * Each entry starts with PgStatShared_PerBackendEntry, followed by the
+	 * kind-specific statistics payload.
+	 */
+	uint32		per_backend_entry_size; /* size of one array entry */
+	uint32		per_backend_data_off;	/* offset of stats data in entry */
+	uint32		per_backend_data_len;	/* size of stats data in entry */
+
+	/*
+	 * 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;
@@ -489,6 +514,24 @@ typedef struct PgStatShared_SLRU
 	PgStat_SLRUStats stats[SLRU_NUM_ELEMENTS];
 } PgStatShared_SLRU;
 
+/*
+ * Number of slots in each per-backend statistics array. Prepared
+ * transactions do not run code and therefore do not report statistics.
+ */
+#define NumPerBackendStatSlots (MaxBackends + NUM_AUXILIARY_PROCS)
+
+/*
+ * Common header for entries in per-backend statistics arrays.
+ *
+ * The lock protects the backend type and kind-specific payload that follows
+ * this header. B_INVALID marks an unused entry.
+ */
+typedef struct PgStatShared_PerBackendEntry
+{
+	LWLock		lock;
+	BackendType backend_type;
+} PgStatShared_PerBackendEntry;
+
 typedef struct PgStatShared_Wal
 {
 	/* lock protects ->stats */
@@ -599,6 +642,12 @@ typedef struct PgStat_ShmemControl
 	PgStatShared_SLRU slru;
 	PgStatShared_Wal wal;
 
+	/*
+	 * Per-backend arrays for built-in fixed-numbered statistics kinds.
+	 * Entries are indexed by ProcNumber.
+	 */
+	void	   *per_backend_arrays[PGSTAT_KIND_BUILTIN_SIZE];
+
 	/*
 	 * Custom stats data with fixed-numbered objects, indexed by (PgStat_Kind
 	 * - PGSTAT_KIND_CUSTOM_MIN).
@@ -644,6 +693,11 @@ typedef struct PgStat_Snapshot
 
 	/* to free snapshot in bulk */
 	MemoryContext context;
+
+	/*
+	 * Cached variable-numbered and per-backend entries, including negative
+	 * lookups. Entries are owned by the snapshot memory context.
+	 */
 	struct pgstat_snapshot_hash *stats;
 } PgStat_Snapshot;
 
@@ -711,6 +765,23 @@ extern PgStat_EntryRef *pgstat_fetch_pending_entry(PgStat_Kind kind,
 extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid,
 								bool *may_free);
 extern void pgstat_snapshot_fixed(PgStat_Kind kind);
+extern void pgstat_prep_snapshot(void);
+extern void *pgstat_lookup_snapshot_entry(const PgStat_HashKey *key,
+										  bool *found);
+extern void *pgstat_cache_snapshot_entry(const PgStat_HashKey *key,
+										 const void *data, Size data_len);
+
+/*
+ * These functions implement generic per-backend statistics.
+ */
+extern void *pgstat_get_per_backend_entry(PgStat_Kind kind,
+										  ProcNumber procnum);
+extern void pgstat_create_my_per_backend_entries(void);
+extern void *pgstat_lock_my_per_backend_entry(PgStat_Kind kind, bool nowait);
+extern void pgstat_per_backend_snapshot(PgStat_Kind kind, 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);
 
 
 /*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a7a8dbdd23f..1306c96c3e7 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2302,6 +2302,7 @@ PgStatShared_HashEntry
 PgStatShared_Index
 PgStatShared_IO
 PgStatShared_Lock
+PgStatShared_PerBackendEntry
 PgStatShared_Relation
 PgStatShared_ReplSlot
 PgStatShared_SLRU
-- 
2.34.1

