From 5b170ea2c43b4ab828e67afc0bc875b1d593ecd6 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Mon, 29 Jun 2026 18:24:01 -0400
Subject: [PATCH v1] Update FSM after updating VM on-access

b46e1e54d078de allowed setting the VM while on-access pruning, but it
neglected to update the freespace map. Once the page was all-visible,
vacuum could skip it, leading to stale freespace map values and,
effectively, bloat. Fix it by updating the FSM if we updated the VM.
---
 src/backend/access/heap/pruneheap.c | 26 +++++++++++++++++++++++---
 1 file changed, 23 insertions(+), 3 deletions(-)

diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index fdddd23035b..bcffec3055e 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -27,6 +27,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "storage/bufmgr.h"
+#include "storage/freespace.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
@@ -321,6 +322,9 @@ heap_page_prune_opt(Relation relation, Buffer buffer, Buffer *vmbuffer,
 
 	if (PageIsFull(page) || PageGetHeapFreeSpace(page) < minfree)
 	{
+		bool		record_free_space = false;
+		Size		freespace = 0;
+
 		/* OK, try to get exclusive buffer lock */
 		if (!ConditionalLockBufferForCleanup(buffer))
 			return;
@@ -376,16 +380,32 @@ heap_page_prune_opt(Relation relation, Buffer buffer, Buffer *vmbuffer,
 			if (presult.ndeleted > presult.nnewlpdead)
 				pgstat_update_heap_dead_tuples(relation,
 											   presult.ndeleted - presult.nnewlpdead);
+
+			/*
+			 * If this prune newly set the page all-visible, VACUUM may later
+			 * skip the page and thus not update its free space map (FSM)
+			 * entry. Keep the FSM from going stale by recording it now. We do
+			 * not want to update the freespace map otherwise (to reserve
+			 * freespace on this page for future updates).
+			 */
+			if (presult.newly_all_visible)
+			{
+				record_free_space = true;
+				freespace = PageGetHeapFreeSpace(page);
+			}
 		}
 
 		/* And release buffer lock */
 		LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
 
 		/*
-		 * We avoid reuse of any free space created on the page by unrelated
-		 * UPDATEs/INSERTs by opting to not update the FSM at this point.  The
-		 * free space should be reused by UPDATEs to *this* page.
+		 * RecordPageWithFreeSpace() only dirties the FSM when the recorded
+		 * free-space category actually changes. Note that vacuum will still
+		 * do FreeSpaceMapVacuum() for ranges of pages that are skipped, so we
+		 * don't have to worry about that here.
 		 */
+		if (record_free_space)
+			RecordPageWithFreeSpace(relation, BufferGetBlockNumber(buffer), freespace);
 	}
 }
 
-- 
2.43.0

