From 0929158ba964c53d84a873c57144c008f4b0ef25 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 v2 5/8] Add buffer access strategy to table scan
 instrumentation

Extend table scan instrumentation with an optional buffer access strategy and
have the heap table AM record it when requested. Track whether the value is
available so another table AM can leave it unset without it being mistaken
for BAS_NORMAL.

Propagate the strategy 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>
Discussion: https://postgr.es/m/auCYYbDyrQEwIsQq@bdtpg
---
 src/backend/access/heap/heapam.c          | 24 ++++++++++++++++++----
 src/backend/executor/nodeBitmapHeapscan.c | 10 ++++++---
 src/backend/executor/nodeSeqscan.c        | 22 ++++++++++++--------
 src/backend/executor/nodeTidrangescan.c   | 20 +++++++++++-------
 src/backend/storage/buffer/freelist.c     | 14 +++++++++++++
 src/include/access/tableam.h              |  4 +++-
 src/include/executor/instrument_node.h    | 25 +++++++++++++++++++++++
 src/include/storage/buf.h                 | 15 ++++++++++++++
 src/include/storage/bufmgr.h              | 16 +--------------
 9 files changed, 111 insertions(+), 39 deletions(-)
  16.7% src/backend/access/heap/
  45.5% src/backend/executor/
   5.2% src/backend/storage/buffer/
   3.1% src/include/access/
  10.8% src/include/executor/
  18.4% src/include/storage/

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 6a09a1a1517..fd9702abcc5 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -1317,12 +1317,22 @@ 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)
+		{
+			scan->rs_base.rs_instrument->strategy = GetAccessStrategyType(scan->rs_strategy);
+			scan->rs_base.rs_instrument->strategy_valid = true;
+		}
+
+		/* 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 +1397,12 @@ 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)
+	{
+		scan->rs_base.rs_instrument->strategy = GetAccessStrategyType(scan->rs_strategy);
+		scan->rs_base.rs_instrument->strategy_valid = true;
+	}
 }
 
 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..fbf44729ff5 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
@@ -309,9 +311,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())
 	{
 		SeqScanInstrumentation *si;
@@ -320,9 +320,7 @@ ExecEndSeqScan(SeqScanState *node)
 		si = &node->sinstrument->sinstrument[ParallelWorkerNumber];
 
 		if (scanDesc && scanDesc->rs_instrument)
-		{
-			AccumulateIOStats(&si->stats.io, &scanDesc->rs_instrument->io);
-		}
+			CombineTableScanInstrumentation(&si->stats, scanDesc->rs_instrument);
 	}
 
 	/*
@@ -400,6 +398,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 +445,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 +463,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 +484,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 +509,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..83fc7142e7b 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;
@@ -360,9 +362,7 @@ ExecEndTidRangeScan(TidRangeScanState *node)
 		si = &node->trss_sinstrument->sinstrument[ParallelWorkerNumber];
 
 		if (scan && scan->rs_instrument)
-		{
-			AccumulateIOStats(&si->stats.io, &scan->rs_instrument->io);
-		}
+			CombineTableScanInstrumentation(&si->stats, scan->rs_instrument);
 	}
 
 	if (scan != NULL)
@@ -484,6 +484,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 +531,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 +551,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 +573,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 +598,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 f4b5f2cf44e..57dba7a893e 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 ea3f2a6be99..9ad8dde300f 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 read stream 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 4759a628afe..845c18e7aa1 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,6 +80,10 @@ typedef struct IOStats
 typedef struct TableScanInstrumentation
 {
 	IOStats		io;
+
+	/* Buffer access strategy reported by the table AM, if available. */
+	BufferAccessStrategyType strategy;
+	bool		strategy_valid;
 } TableScanInstrumentation;
 
 /* merge IO statistics from 'src' into 'dst' */
@@ -96,6 +102,25 @@ 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, while the strategy from 'src' replaces the
+ * strategy in 'dst' when available.
+ */
+static inline void
+CombineTableScanInstrumentation(TableScanInstrumentation *dst,
+								TableScanInstrumentation *src)
+{
+	AccumulateIOStats(&dst->io, &src->io);
+
+	if (src->strategy_valid)
+	{
+		dst->strategy = src->strategy;
+		dst->strategy_valid = true;
+	}
+}
+
 
 /* ---------------------
  *	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

