From 0f825212eca0a1466cabccd9f1b02f903a18411a Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Sun, 25 Oct 2026 12:03:34 +0100
Subject: [PATCH v1 2/9] Separate the FPIs from recently prefetched blocks

Don't build the FPI skipping on the detection of recently prefetched
blocks. We're not prefetching blocks from records with FPIs, so adding
them to the recent_block cache is a bit misleading. It's also not clear
why these two caches should use the same capacity, i.e. maybe 4 entries
is enough for the recent_block cache, but we probably want a different
(much larger) cache for the FPIs.

Add a separate (larger) cache for FPIs, and check it after the existing
recent_block cache.

Extend the pg_stat_recovery_prefetch view to also track the number of
prefetches skipped because of a recent FPI. Document the new field.

XXX: The skip_fpi name is not very self-explanatory, and it's easy to
confuse with skip_fpw. We should probably pick a better name.
---
 doc/src/sgml/monitoring.sgml                | 11 +++
 src/backend/access/transam/xlogprefetcher.c | 77 +++++++++++++++------
 src/backend/catalog/system_views.sql        |  1 +
 src/include/catalog/pg_proc.dat             |  6 +-
 src/test/regress/expected/rules.out         |  3 +-
 5 files changed, 72 insertions(+), 26 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 31ff4a9d99f..af8f9f5e336 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -2353,6 +2353,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>skip_fpi</structfield> <type>bigint</type>
+       </para>
+       <para>
+        Number of blocks not prefetched because they would be restored from a full page image included in an earlier WAL record
+       </para>
+      </entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry">
        <para role="column_definition">
diff --git a/src/backend/access/transam/xlogprefetcher.c b/src/backend/access/transam/xlogprefetcher.c
index 41c1caf45e1..ed06c3e429f 100644
--- a/src/backend/access/transam/xlogprefetcher.c
+++ b/src/backend/access/transam/xlogprefetcher.c
@@ -54,10 +54,19 @@
 
 /*
  * To detect repeated access to the same block and skip useless extra system
- * calls, we remember a small window of recently prefetched / FPW blocks.
+ * calls, we remember a small window of recently prefetched blocks.
  */
 #define XLOGPREFETCHER_SEQ_WINDOW_SIZE 4
 
+/*
+ * A small history of most recently seen FPIs, so that we can skip useless
+ * prefetches for these blocks (most likely still in cache).
+ *
+ * XXX At which point should we switch to a hash table, instead of a linear
+ * search in a small array?
+ */
+#define XLOGPREFETCHER_FPI_HISTORY_SIZE 64
+
 /*
  * When maintenance_io_concurrency is not saturated, we're prepared to look
  * ahead up to N times that number of block references.
@@ -143,6 +152,11 @@ struct XLogPrefetcher
 	BlockNumber recent_block[XLOGPREFETCHER_SEQ_WINDOW_SIZE];
 	int			recent_idx;
 
+	/* Book-keeping to avoid prefetching right after FPI */
+	RelFileLocator recent_fpi_rlocator[XLOGPREFETCHER_FPI_HISTORY_SIZE];
+	BlockNumber recent_fpi_block[XLOGPREFETCHER_FPI_HISTORY_SIZE];
+	int			recent_fpi_idx;
+
 	/* Book-keeping to disable prefetching temporarily. */
 	XLogRecPtr	no_readahead_until;
 
@@ -179,6 +193,7 @@ typedef struct XLogPrefetchStats
 	pg_atomic_uint64 skip_init; /* Zero-inited blocks skipped. */
 	pg_atomic_uint64 skip_new;	/* New/missing blocks filtered. */
 	pg_atomic_uint64 skip_fpw;	/* FPWs skipped. */
+	pg_atomic_uint64 skip_fpi;	/* Accesses after FPI skipped. */
 	pg_atomic_uint64 skip_rep;	/* Repeat accesses skipped. */
 
 	/* Dynamic values */
@@ -319,6 +334,7 @@ XLogPrefetchShmemInit(void *arg)
 	pg_atomic_init_u64(&SharedStats->skip_init, 0);
 	pg_atomic_init_u64(&SharedStats->skip_new, 0);
 	pg_atomic_init_u64(&SharedStats->skip_fpw, 0);
+	pg_atomic_init_u64(&SharedStats->skip_fpi, 0);
 	pg_atomic_init_u64(&SharedStats->skip_rep, 0);
 }
 
@@ -334,6 +350,7 @@ XLogPrefetchResetStats(void)
 	pg_atomic_write_u64(&SharedStats->skip_init, 0);
 	pg_atomic_write_u64(&SharedStats->skip_new, 0);
 	pg_atomic_write_u64(&SharedStats->skip_fpw, 0);
+	pg_atomic_write_u64(&SharedStats->skip_fpi, 0);
 	pg_atomic_write_u64(&SharedStats->skip_rep, 0);
 }
 
@@ -681,23 +698,17 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn)
 				XLogPrefetchIncrement(&SharedStats->skip_fpw);
 
 				/*
-				 * A full page image is copied to shared buffer during replay.
-				 * Every next WAL records related to particular block doesn't
-				 * require prefetching of shared buffer.
+				 * We should not really see a FPI for the same block very
+				 * often, not more than once per checkpoint cycle. So don't
+				 * bother with scanning the history here. If we end up with a
+				 * duplicate FPI, it means the system is almost idle. We also
+				 * don't bother with evicting blocks from the history, for the
+				 * same reason.
 				 */
-				for (int i = 0; i < XLOGPREFETCHER_SEQ_WINDOW_SIZE; ++i)
-				{
-					if (block->blkno == prefetcher->recent_block[i] &&
-						RelFileLocatorEquals(block->rlocator, prefetcher->recent_rlocator[i]))
-					{
-						return LRQ_NEXT_NO_IO;
-					}
-				}
-
-				prefetcher->recent_rlocator[prefetcher->recent_idx] = block->rlocator;
-				prefetcher->recent_block[prefetcher->recent_idx] = block->blkno;
-				prefetcher->recent_idx =
-					(prefetcher->recent_idx + 1) % XLOGPREFETCHER_SEQ_WINDOW_SIZE;
+				prefetcher->recent_fpi_rlocator[prefetcher->recent_fpi_idx] = block->rlocator;
+				prefetcher->recent_fpi_block[prefetcher->recent_fpi_idx] = block->blkno;
+				prefetcher->recent_fpi_idx =
+					(prefetcher->recent_fpi_idx + 1) % XLOGPREFETCHER_FPI_HISTORY_SIZE;
 
 				return LRQ_NEXT_NO_IO;
 			}
@@ -731,6 +742,27 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn)
 					return LRQ_NEXT_NO_IO;
 				}
 			}
+
+			/*
+			 * Likewise, no point in prefetching a block right after restoring
+			 * it from an FPI. We keep a bit longer history of FPIs.
+			 *
+			 * XXX Maybe we could skip this with full-page writes disabled?
+			 *
+			 * XXX Maybe we could skip this backwards, with the assumption
+			 * that if there is a FPI, it's likely very close?
+			 */
+			for (int i = 0; i < XLOGPREFETCHER_FPI_HISTORY_SIZE; ++i)
+			{
+				if (block->blkno == prefetcher->recent_fpi_block[i] &&
+					RelFileLocatorEquals(block->rlocator, prefetcher->recent_fpi_rlocator[i]))
+				{
+					XLogPrefetchIncrement(&SharedStats->skip_fpi);
+					return LRQ_NEXT_NO_IO;
+				}
+			}
+
+			/* We're going to prefetch the block, so add it to history. */
 			prefetcher->recent_rlocator[prefetcher->recent_idx] = block->rlocator;
 			prefetcher->recent_block[prefetcher->recent_idx] = block->blkno;
 			prefetcher->recent_idx =
@@ -849,7 +881,7 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn)
 Datum
 pg_stat_get_recovery_prefetch(PG_FUNCTION_ARGS)
 {
-#define PG_STAT_GET_RECOVERY_PREFETCH_COLS 10
+#define PG_STAT_GET_RECOVERY_PREFETCH_COLS 11
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	Datum		values[PG_STAT_GET_RECOVERY_PREFETCH_COLS];
 	bool		nulls[PG_STAT_GET_RECOVERY_PREFETCH_COLS];
@@ -865,10 +897,11 @@ pg_stat_get_recovery_prefetch(PG_FUNCTION_ARGS)
 	values[3] = Int64GetDatum(pg_atomic_read_u64(&SharedStats->skip_init));
 	values[4] = Int64GetDatum(pg_atomic_read_u64(&SharedStats->skip_new));
 	values[5] = Int64GetDatum(pg_atomic_read_u64(&SharedStats->skip_fpw));
-	values[6] = Int64GetDatum(pg_atomic_read_u64(&SharedStats->skip_rep));
-	values[7] = Int32GetDatum(SharedStats->wal_distance);
-	values[8] = Int32GetDatum(SharedStats->block_distance);
-	values[9] = Int32GetDatum(SharedStats->io_depth);
+	values[6] = Int64GetDatum(pg_atomic_read_u64(&SharedStats->skip_fpi));
+	values[7] = Int64GetDatum(pg_atomic_read_u64(&SharedStats->skip_rep));
+	values[8] = Int32GetDatum(SharedStats->wal_distance);
+	values[9] = Int32GetDatum(SharedStats->block_distance);
+	values[10] = Int32GetDatum(SharedStats->io_depth);
 	tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
 
 	return (Datum) 0;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index ddf801307eb..83fb070a617 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1054,6 +1054,7 @@ CREATE VIEW pg_stat_recovery_prefetch AS
             s.skip_init,
             s.skip_new,
             s.skip_fpw,
+            s.skip_fpi,
             s.skip_rep,
             s.wal_distance,
             s.block_distance,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1f21a8eb6d4..ff1a233d5c6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6106,9 +6106,9 @@
 { oid => '6248', descr => 'statistics: information about WAL prefetching',
   proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{timestamptz,int8,int8,int8,int8,int8,int8,int4,int4,int4}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{stats_reset,prefetch,hit,skip_init,skip_new,skip_fpw,skip_rep,wal_distance,block_distance,io_depth}',
+  proallargtypes => '{timestamptz,int8,int8,int8,int8,int8,int8,int8,int4,int4,int4}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{stats_reset,prefetch,hit,skip_init,skip_new,skip_fpw,skip_fpi,skip_rep,wal_distance,block_distance,io_depth}',
   prosrc => 'pg_stat_get_recovery_prefetch' },
 
 { oid => '2306', descr => 'statistics: information about SLRU caches',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 38c9b04f72c..99ecf72970e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2238,11 +2238,12 @@ pg_stat_recovery_prefetch| SELECT stats_reset,
     skip_init,
     skip_new,
     skip_fpw,
+    skip_fpi,
     skip_rep,
     wal_distance,
     block_distance,
     io_depth
-   FROM pg_stat_get_recovery_prefetch() s(stats_reset, prefetch, hit, skip_init, skip_new, skip_fpw, skip_rep, wal_distance, block_distance, io_depth);
+   FROM pg_stat_get_recovery_prefetch() s(stats_reset, prefetch, hit, skip_init, skip_new, skip_fpw, skip_fpi, skip_rep, wal_distance, block_distance, io_depth);
 pg_stat_replication| SELECT s.pid,
     s.usesysid,
     u.rolname AS usename,
-- 
2.55.0

