From c501a399b78bfbfbb8b5fa7c2e37c0925b59932a Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Wed, 28 Oct 2026 06:49:11 +0000
Subject: [PATCH v5 5/8] Add buffer access strategies to table scan
 instrumentation

Extend table scan instrumentation with optional buffer access strategies and
have the heap table AM record them when requested. Store the observed strategies
in a bitmask so rescans preserve every selected strategy. An empty mask allows
another table AM to leave the information unavailable.

Propagate the strategies through the existing parallel worker instrumentation
for sequential, bitmap heap, and TID range scans.

Move BufferAccessStrategyType to buf.h so executor instrumentation can use it
without including the heavier bufmgr.h.

This provides the common infrastructure for EXPLAIN reporting added in a
following commit.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Sami Imseih <samimseih.pg@gmail.com>
Reviewed-by: Lukas Fittl <lukas@fittl.com>
Discussion: https://postgr.es/m/auCYYbDyrQEwIsQq@bdtpg
---
 src/backend/access/heap/heapam.c          | 20 ++++++++---
 src/backend/executor/nodeBitmapHeapscan.c | 10 ++++--
 src/backend/executor/nodeSeqscan.c        | 18 +++++++---
 src/backend/executor/nodeTidrangescan.c   | 18 +++++++---
 src/backend/storage/buffer/freelist.c     | 14 ++++++++
 src/include/access/tableam.h              |  4 ++-
 src/include/executor/instrument_node.h    | 42 +++++++++++++++++++++++
 src/include/storage/buf.h                 | 15 ++++++++
 src/include/storage/bufmgr.h              | 16 +--------
 9 files changed, 124 insertions(+), 33 deletions(-)
  14.5% src/backend/access/heap/
  42.6% src/backend/executor/
   4.9% src/backend/storage/buffer/
  17.5% src/include/executor/
  17.4% src/include/storage/

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 3bb33699349..5353d276d85 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -1317,12 +1317,20 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 														  sizeof(TBMIterateResult));
 	}
 
-	/* enable read stream instrumentation */
-	if ((flags & SO_SCAN_INSTRUMENT) && (scan->rs_read_stream != NULL))
+	/* enable scan instrumentation */
+	if (flags & (SO_SCAN_INSTRUMENT | SO_SCAN_INSTRUMENT_STRATEGY))
 	{
 		scan->rs_base.rs_instrument = palloc0_object(TableScanInstrumentation);
-		read_stream_enable_stats(scan->rs_read_stream,
-								 &scan->rs_base.rs_instrument->io);
+
+		/* strategy instrumentation */
+		if (flags & SO_SCAN_INSTRUMENT_STRATEGY)
+			RecordTableScanStrategy(scan->rs_base.rs_instrument,
+									GetAccessStrategyType(scan->rs_strategy));
+
+		/* read stream instrumentation */
+		if ((flags & SO_SCAN_INSTRUMENT) && scan->rs_read_stream != NULL)
+			read_stream_enable_stats(scan->rs_read_stream,
+									 &scan->rs_base.rs_instrument->io);
 	}
 
 	scan->rs_vmbuffer = InvalidBuffer;
@@ -1387,6 +1395,10 @@ heap_rescan(TableScanDesc sscan, ScanKey key, bool set_params,
 	 * reinitialize scan descriptor
 	 */
 	initscan(scan, key, true);
+
+	if (scan->rs_base.rs_flags & SO_SCAN_INSTRUMENT_STRATEGY)
+		RecordTableScanStrategy(scan->rs_base.rs_instrument,
+								GetAccessStrategyType(scan->rs_strategy));
 }
 
 void
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index a9e83f30687..3412ad21e52 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -149,6 +149,8 @@ BitmapTableScanSetup(BitmapHeapScanState *node)
 
 		if (node->ss.ps.state->es_instrument & INSTRUMENT_IO)
 			flags |= SO_SCAN_INSTRUMENT;
+		if (node->ss.ps.state->es_instrument & INSTRUMENT_BUFFERS)
+			flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 		node->ss.ss_currentScanDesc =
 			table_beginscan_bm(node->ss.ss_currentRelation,
@@ -334,12 +336,14 @@ ExecEndBitmapHeapScan(BitmapHeapScanState *node)
 		si->exact_pages += node->stats.exact_pages;
 		si->lossy_pages += node->stats.lossy_pages;
 
-		/* collect I/O instrumentation for this process */
+		/* Collect scan instrumentation for this process */
 		if (node->ss.ss_currentScanDesc &&
 			node->ss.ss_currentScanDesc->rs_instrument)
 		{
-			AccumulateIOStats(&si->stats.io,
-							  &node->ss.ss_currentScanDesc->rs_instrument->io);
+			TableScanInstrumentation *stats =
+				node->ss.ss_currentScanDesc->rs_instrument;
+
+			CombineTableScanInstrumentation(&si->stats, stats);
 		}
 	}
 
diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c
index b8c528ca089..96f62451240 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -73,6 +73,8 @@ SeqNext(SeqScanState *node)
 
 		if (estate->es_instrument & INSTRUMENT_IO)
 			flags |= SO_SCAN_INSTRUMENT;
+		if (estate->es_instrument & INSTRUMENT_BUFFERS)
+			flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 		/*
 		 * We reach here if the scan is not parallel, or if we're serially
@@ -310,7 +312,7 @@ ExecEndSeqScan(SeqScanState *node)
 	scanDesc = node->ss.ss_currentScanDesc;
 
 	/*
-	 * Collect I/O stats for this process into shared instrumentation.
+	 * Collect scan statistics for this process into shared instrumentation.
 	 */
 	if (node->sinstrument != NULL && IsParallelWorker())
 	{
@@ -321,7 +323,7 @@ ExecEndSeqScan(SeqScanState *node)
 
 		if (scanDesc && scanDesc->rs_instrument)
 		{
-			AccumulateIOStats(&si->stats.io, &scanDesc->rs_instrument->io);
+			CombineTableScanInstrumentation(&si->stats, scanDesc->rs_instrument);
 		}
 	}
 
@@ -400,6 +402,8 @@ ExecSeqScanInitializeDSM(SeqScanState *node,
 
 	if (estate->es_instrument & INSTRUMENT_IO)
 		flags |= SO_SCAN_INSTRUMENT;
+	if (estate->es_instrument & INSTRUMENT_BUFFERS)
+		flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 	pscan = shm_toc_allocate(pcxt->toc, node->pscan_len);
 	table_parallelscan_initialize(node->ss.ss_currentRelation,
@@ -445,6 +449,8 @@ ExecSeqScanInitializeWorker(SeqScanState *node,
 
 	if (node->ss.ps.state->es_instrument & INSTRUMENT_IO)
 		flags |= SO_SCAN_INSTRUMENT;
+	if (node->ss.ps.state->es_instrument & INSTRUMENT_BUFFERS)
+		flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 	pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false);
 	node->ss.ss_currentScanDesc =
@@ -461,7 +467,8 @@ ExecSeqScanInstrumentEstimate(SeqScanState *node, ParallelContext *pcxt)
 	EState	   *estate = node->ss.ps.state;
 	Size		size;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0 ||
+		pcxt->nworkers == 0)
 		return;
 
 	size = add_size(offsetof(SharedSeqScanInstrumentation, sinstrument),
@@ -481,7 +488,8 @@ ExecSeqScanInstrumentInitDSM(SeqScanState *node, ParallelContext *pcxt)
 	SharedSeqScanInstrumentation *sinstrument;
 	Size		size;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0 ||
+		pcxt->nworkers == 0)
 		return;
 
 	size = add_size(offsetof(SharedSeqScanInstrumentation, sinstrument),
@@ -505,7 +513,7 @@ ExecSeqScanInstrumentInitWorker(SeqScanState *node,
 {
 	EState	   *estate = node->ss.ps.state;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0)
 		return;
 
 	node->sinstrument = shm_toc_lookup(pwcxt->toc,
diff --git a/src/backend/executor/nodeTidrangescan.c b/src/backend/executor/nodeTidrangescan.c
index b387ed6c308..597e0288f3b 100644
--- a/src/backend/executor/nodeTidrangescan.c
+++ b/src/backend/executor/nodeTidrangescan.c
@@ -251,6 +251,8 @@ TidRangeNext(TidRangeScanState *node)
 
 			if (estate->es_instrument & INSTRUMENT_IO)
 				flags |= SO_SCAN_INSTRUMENT;
+			if (estate->es_instrument & INSTRUMENT_BUFFERS)
+				flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 			scandesc = table_beginscan_tidrange(node->ss.ss_currentRelation,
 												estate->es_snapshot,
@@ -351,7 +353,7 @@ ExecEndTidRangeScan(TidRangeScanState *node)
 {
 	TableScanDesc scan = node->ss.ss_currentScanDesc;
 
-	/* Collect IO stats for this process into shared instrumentation */
+	/* Collect scan statistics for this process into shared instrumentation */
 	if (node->trss_sinstrument != NULL && IsParallelWorker())
 	{
 		TidRangeScanInstrumentation *si;
@@ -361,7 +363,7 @@ ExecEndTidRangeScan(TidRangeScanState *node)
 
 		if (scan && scan->rs_instrument)
 		{
-			AccumulateIOStats(&si->stats.io, &scan->rs_instrument->io);
+			CombineTableScanInstrumentation(&si->stats, scan->rs_instrument);
 		}
 	}
 
@@ -484,6 +486,8 @@ ExecTidRangeScanInitializeDSM(TidRangeScanState *node, ParallelContext *pcxt)
 
 	if (estate->es_instrument & INSTRUMENT_IO)
 		flags |= SO_SCAN_INSTRUMENT;
+	if (estate->es_instrument & INSTRUMENT_BUFFERS)
+		flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 	pscan = shm_toc_allocate(pcxt->toc, node->trss_pscanlen);
 	table_parallelscan_initialize(node->ss.ss_currentRelation,
@@ -529,6 +533,8 @@ ExecTidRangeScanInitializeWorker(TidRangeScanState *node,
 
 	if (node->ss.ps.state->es_instrument & INSTRUMENT_IO)
 		flags |= SO_SCAN_INSTRUMENT;
+	if (node->ss.ps.state->es_instrument & INSTRUMENT_BUFFERS)
+		flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 	pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false);
 	node->ss.ss_currentScanDesc =
@@ -547,7 +553,8 @@ ExecTidRangeScanInstrumentEstimate(TidRangeScanState *node,
 	EState	   *estate = node->ss.ps.state;
 	Size		size;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0 ||
+		pcxt->nworkers == 0)
 		return;
 
 	size = add_size(offsetof(SharedTidRangeScanInstrumentation, sinstrument),
@@ -568,7 +575,8 @@ ExecTidRangeScanInstrumentInitDSM(TidRangeScanState *node,
 	SharedTidRangeScanInstrumentation *sinstrument;
 	Size		size;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0 ||
+		pcxt->nworkers == 0)
 		return;
 
 	size = add_size(offsetof(SharedTidRangeScanInstrumentation, sinstrument),
@@ -592,7 +600,7 @@ ExecTidRangeScanInstrumentInitWorker(TidRangeScanState *node,
 {
 	EState	   *estate = node->ss.ps.state;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0)
 		return;
 
 	node->trss_sinstrument = shm_toc_lookup(pwcxt->toc,
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 3e29592f634..1b9542363f8 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -542,6 +542,20 @@ GetAccessStrategyWithSize(BufferAccessStrategyType btype, int ring_size_kb)
 	return strategy;
 }
 
+/*
+ * GetAccessStrategyType -- return the type of a buffer access strategy.
+ *
+ * A NULL strategy represents normal buffer replacement.
+ */
+BufferAccessStrategyType
+GetAccessStrategyType(BufferAccessStrategy strategy)
+{
+	if (strategy == NULL)
+		return BAS_NORMAL;
+
+	return strategy->btype;
+}
+
 /*
  * GetAccessStrategyBufferCount -- an accessor for the number of buffers in
  *		the ring
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 9d2fc7f9acb..39193f57597 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -71,8 +71,10 @@ typedef enum ScanOptions
 	/* set if the query doesn't modify the relation */
 	SO_HINT_REL_READ_ONLY = 1 << 10,
 
-	/* collect scan instrumentation */
+	/* collect I/O instrumentation */
 	SO_SCAN_INSTRUMENT = 1 << 11,
+	/* collect buffer access strategy instrumentation */
+	SO_SCAN_INSTRUMENT_STRATEGY = 1 << 12,
 }			ScanOptions;
 
 /*
diff --git a/src/include/executor/instrument_node.h b/src/include/executor/instrument_node.h
index c48e8ad756e..ef97ca14d7c 100644
--- a/src/include/executor/instrument_node.h
+++ b/src/include/executor/instrument_node.h
@@ -18,6 +18,8 @@
 #ifndef INSTRUMENT_NODE_H
 #define INSTRUMENT_NODE_H
 
+#include "storage/buf.h"
+
 /*
  * Offset added to plan_node_id to create a second TOC key for per-worker scan
  * instrumentation. Instrumentation and parallel-awareness are independent, so
@@ -78,8 +80,35 @@ typedef struct IOStats
 typedef struct TableScanInstrumentation
 {
 	IOStats		io;
+
+	/* Buffer access strategies reported by the table AM. */
+	uint32		strategy_mask;
 } TableScanInstrumentation;
 
+/*
+ * Return the instrumentation bit for a buffer access strategy.
+ */
+static inline uint32
+BufferAccessStrategyBit(BufferAccessStrategyType strategy)
+{
+	Assert(strategy >= BAS_NORMAL && strategy <= BAS_VACUUM);
+
+	return 1U << strategy;
+}
+
+/*
+ * Record a buffer access strategy selected by a table AM.
+ *
+ * Preserve every strategy observed across rescans.
+ */
+static inline void
+RecordTableScanStrategy(TableScanInstrumentation *instrument,
+						BufferAccessStrategyType strategy)
+{
+	if (instrument)
+		instrument->strategy_mask |= BufferAccessStrategyBit(strategy);
+}
+
 /* merge IO statistics from 'src' into 'dst' */
 static inline void
 AccumulateIOStats(IOStats *dst, IOStats *src)
@@ -96,6 +125,19 @@ AccumulateIOStats(IOStats *dst, IOStats *src)
 	dst->io_in_progress += src->io_in_progress;
 }
 
+/*
+ * Combine table scan instrumentation from 'src' into 'dst'.
+ *
+ * I/O statistics are accumulated and observed strategies are preserved.
+ */
+static inline void
+CombineTableScanInstrumentation(TableScanInstrumentation *dst,
+								TableScanInstrumentation *src)
+{
+	AccumulateIOStats(&dst->io, &src->io);
+	dst->strategy_mask |= src->strategy_mask;
+}
+
 
 /* ---------------------
  *	Instrumentation information for index scans (used by all AM interfaces)
diff --git a/src/include/storage/buf.h b/src/include/storage/buf.h
index b21445522b1..06583a253fd 100644
--- a/src/include/storage/buf.h
+++ b/src/include/storage/buf.h
@@ -36,6 +36,21 @@ typedef int Buffer;
  */
 #define BufferIsLocal(buffer)	((buffer) < 0)
 
+/*
+ * Possible arguments for GetAccessStrategy().
+ *
+ * If adding a new BufferAccessStrategyType, also add a new IOContext so
+ * IO statistics using this strategy are tracked.
+ */
+typedef enum BufferAccessStrategyType
+{
+	BAS_NORMAL,					/* Normal random access */
+	BAS_BULKREAD,				/* Large read-only scan (hint bit updates are
+								 * ok) */
+	BAS_BULKWRITE,				/* Large multi-block write (e.g. COPY IN) */
+	BAS_VACUUM,					/* VACUUM */
+} BufferAccessStrategyType;
+
 /*
  * Buffer access strategy objects.
  *
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..be45f000982 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -25,21 +25,6 @@
 
 typedef void *Block;
 
-/*
- * Possible arguments for GetAccessStrategy().
- *
- * If adding a new BufferAccessStrategyType, also add a new IOContext so
- * IO statistics using this strategy are tracked.
- */
-typedef enum BufferAccessStrategyType
-{
-	BAS_NORMAL,					/* Normal random access */
-	BAS_BULKREAD,				/* Large read-only scan (hint bit updates are
-								 * ok) */
-	BAS_BULKWRITE,				/* Large multi-block write (e.g. COPY IN) */
-	BAS_VACUUM,					/* VACUUM */
-} BufferAccessStrategyType;
-
 /* Possible modes for ReadBufferExtended() */
 typedef enum
 {
@@ -379,6 +364,7 @@ extern void AtProcExit_LocalBuffers(void);
 extern BufferAccessStrategy GetAccessStrategy(BufferAccessStrategyType btype);
 extern BufferAccessStrategy GetAccessStrategyWithSize(BufferAccessStrategyType btype,
 													  int ring_size_kb);
+extern BufferAccessStrategyType GetAccessStrategyType(BufferAccessStrategy strategy);
 extern int	GetAccessStrategyBufferCount(BufferAccessStrategy strategy);
 extern int	GetAccessStrategyPinLimit(BufferAccessStrategy strategy);
 
-- 
2.34.1

