From 5bfdf0a2fa7ef416f3c893c7197cd230afac4ef0 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Sun, 25 Oct 2026 18:45:50 +0100
Subject: [PATCH v1 8/9] Size recovery FPI history by the look-ahead distance

The history of recent FPIs had a fixed size of 256 slots (16 groups of
16 slots). But we only really care about FPIs that were not replayed
yet, which is limited by the look-ahead distance. That is determined by
maintenance_io_concurrency times XLOGPREFETCHER_DISTANCE_MULTIPLIER,
i.e. 64 by default, up to 4000.

When there are more FPIs in records that have not been replayed yet than
the history can hold, useful entries may get evicted, and later
references to those blocks are prefetched even though they are going to
be restored from the FPI. If the blocks are modified in the same order
as before, this may be painful, as we're evicting blocks in LSN order
(classical LRU adversary case).

There are never more than max_distance FPIs that still matter. We size
the history based on that, with four slots per block reference within
the look-ahead distance, rounded up to a power of two. This effectively
oversizes the whole cache 4x, to make group overflows very rare.

This is distinct from XLOGPREFETCHER_DISTANCE_MULTIPLIER, which is used
to calculate look-ahead distance from maintenance_io_concurrency. The
XLOGPREFETCHER_FPI_HISTORY_MULTIPLIER sizes the cache based on the
prefetch distance.

The history is allocated when the prefetcher is (re)configured, along
with the LsnReadQueue, which also takes care of resetting it after
XLogPrefetcherBeginRead(). As the number of groups is a power of two,
the group can be determined using a mask instead of a division.
---
 src/backend/access/transam/xlogprefetcher.c | 43 +++++++++++++++------
 1 file changed, 32 insertions(+), 11 deletions(-)

diff --git a/src/backend/access/transam/xlogprefetcher.c b/src/backend/access/transam/xlogprefetcher.c
index 1f77e20c08b..ba9e4e40a7e 100644
--- a/src/backend/access/transam/xlogprefetcher.c
+++ b/src/backend/access/transam/xlogprefetcher.c
@@ -36,6 +36,7 @@
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "port/atomics.h"
+#include "port/pg_bitutils.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/shmem.h"
@@ -66,11 +67,16 @@
  *
  * The history is a set-associative hash table, i.e. each block maps to a group
  * of slots, and it can only be stored in one of the slots of that group.
+ *
+ * Only FPIs in records that have not been replayed yet matter, and there are
+ * never more of those than the maximum look-ahead distance, because each of
+ * them occupies an entry in the LsnReadQueue until the record is replayed.
+ * The history has XLOGPREFETCHER_FPI_HISTORY_MULTIPLIER times as many slots
+ * (rounded up so that the number of groups is a power of two), so that a
+ * group overflows and forgets an FPI that still matters only very rarely.
  */
-#define XLOGPREFETCHER_FPI_GROUPS		16
 #define XLOGPREFETCHER_FPI_PER_GROUP	16
-#define XLOGPREFETCHER_FPI_HISTORY_SIZE	\
-	(XLOGPREFETCHER_FPI_GROUPS * XLOGPREFETCHER_FPI_PER_GROUP)
+#define XLOGPREFETCHER_FPI_HISTORY_MULTIPLIER 4
 
 /*
  * When maintenance_io_concurrency is not saturated, we're prepared to look
@@ -169,7 +175,8 @@ struct XLogPrefetcher
 	int			recent_idx;
 
 	/* Book-keeping to avoid prefetching blocks restored from FPIs. */
-	XLogPrefetcherFPI recent_fpi[XLOGPREFETCHER_FPI_HISTORY_SIZE];
+	XLogPrefetcherFPI *recent_fpi;
+	uint32		recent_fpi_groups;	/* always a power of two */
 
 	/* Book-keeping to disable prefetching temporarily. */
 	XLogRecPtr	no_readahead_until;
@@ -435,6 +442,8 @@ XLogPrefetcherFree(XLogPrefetcher *prefetcher)
 {
 	lrq_free(prefetcher->streaming_read);
 	hash_destroy(prefetcher->filter_table);
+	if (prefetcher->recent_fpi)
+		pfree(prefetcher->recent_fpi);
 	pfree(prefetcher);
 }
 
@@ -1044,7 +1053,7 @@ XLogPrefetcherFPIGroup(XLogPrefetcher *prefetcher, RelFileLocator rlocator,
 	hash = hash_combine(hash, murmurhash32(rlocator.dbOid));
 	hash = hash_combine(hash, murmurhash32(rlocator.spcOid));
 
-	group = hash % XLOGPREFETCHER_FPI_GROUPS;
+	group = hash & (prefetcher->recent_fpi_groups - 1);
 
 	return &prefetcher->recent_fpi[group * XLOGPREFETCHER_FPI_PER_GROUP];
 }
@@ -1188,13 +1197,25 @@ XLogPrefetcherReadRecord(XLogPrefetcher *prefetcher, char **errmsg)
 											   XLogPrefetcherNextBlock);
 
 		/*
-		 * Forget about FPIs seen so far.  XLogPrefetcherBeginRead() may have
-		 * moved the reader to a different position, and the entries would not
-		 * be considered replayed until we replay past them again (if ever),
-		 * so we might skip prefetching blocks that are not going to be
-		 * restored from an FPI.
+		 * Allocate a new history of FPIs, sized for the new look-ahead
+		 * distance.  This also forgets about FPIs seen so far, which is
+		 * necessary because XLogPrefetcherBeginRead() may have moved the
+		 * reader to a different position, and the entries would not be
+		 * considered replayed until we replay past them again (if ever), so
+		 * we might skip prefetching blocks that are not going to be restored
+		 * from an FPI.
 		 */
-		memset(prefetcher->recent_fpi, 0, sizeof(prefetcher->recent_fpi));
+		if (prefetcher->recent_fpi)
+			pfree(prefetcher->recent_fpi);
+
+		prefetcher->recent_fpi_groups =
+			pg_nextpower2_32(Max(1, max_distance *
+								 XLOGPREFETCHER_FPI_HISTORY_MULTIPLIER /
+								 XLOGPREFETCHER_FPI_PER_GROUP));
+		prefetcher->recent_fpi =
+			palloc0_array(XLogPrefetcherFPI,
+						  prefetcher->recent_fpi_groups *
+						  XLOGPREFETCHER_FPI_PER_GROUP);
 
 		prefetcher->reconfigure_count = XLogPrefetchReconfigureCount;
 	}
-- 
2.55.0

