From 69250c4f2145264bb29c3bd5d952c20a179308fa Mon Sep 17 00:00:00 2001
From: Jingtang Zhang <mrdrivingduck@gmail.com>
Date: Wed, 26 Aug 2026 12:23:43 +0800
Subject: [PATCH v2 1/2] vacuum: Separate heap page freezing state

Separate per-page freeze planning and execution state from pruning
state.

Keep the existing prune-and-freeze execution path and combined WAL
behavior unchanged.
---
 src/backend/access/heap/pruneheap.c | 182 ++++++++++++++++++----------
 src/tools/pgindent/typedefs.list    |   1 +
 2 files changed, 116 insertions(+), 67 deletions(-)

diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 29f4722b02d..90d59ca306f 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -31,6 +31,15 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
+/* Working data for freezing a single heap page */
+typedef struct
+{
+	bool		attempt_freeze;
+	HeapPageFreeze pagefrz;
+	int			nfrozen;
+	HeapTupleFreeze frozen[MaxHeapTuplesPerPage];
+} FreezeState;
+
 /* Working data for heap_page_prune_and_freeze() and subroutines */
 typedef struct
 {
@@ -43,8 +52,6 @@ typedef struct
 	GlobalVisState *vistest;
 	/* whether or not dead items can be set LP_UNUSED during pruning */
 	bool		mark_unused_now;
-	/* whether to attempt freezing tuples */
-	bool		attempt_freeze;
 	/* whether to attempt setting the VM */
 	bool		attempt_set_vm;
 	struct VacuumCutoffs *cutoffs;
@@ -68,12 +75,10 @@ typedef struct
 	int			nredirected;	/* numbers of entries in arrays below */
 	int			ndead;
 	int			nunused;
-	int			nfrozen;
 	/* arrays that accumulate indexes of items to be changed */
 	OffsetNumber redirected[MaxHeapTuplesPerPage * 2];
 	OffsetNumber nowdead[MaxHeapTuplesPerPage];
 	OffsetNumber nowunused[MaxHeapTuplesPerPage];
-	HeapTupleFreeze frozen[MaxHeapTuplesPerPage];
 
 	/*
 	 * set_all_visible and set_all_frozen indicate if the all-visible and
@@ -131,7 +136,7 @@ typedef struct
 	 * Working state for freezing
 	 *-------------------------------------------------------
 	 */
-	HeapPageFreeze pagefrz;
+	FreezeState freeze;
 
 	/*-------------------------------------------------------
 	 * Working state for visibility map processing
@@ -204,6 +209,12 @@ static void prune_freeze_setup(PruneFreezeParams *params,
 							   MultiXactId *new_relmin_mxid,
 							   PruneFreezeResult *presult,
 							   PruneState *prstate);
+static void heap_page_freeze_init(FreezeState *state, bool attempt_freeze,
+								  TransactionId new_relfrozen_xid,
+								  MultiXactId new_relmin_mxid);
+static void heap_page_freeze_update_relstats(FreezeState *state,
+											 TransactionId *new_relfrozen_xid,
+											 MultiXactId *new_relmin_mxid);
 static void heap_page_fix_vm_corruption(PruneState *prstate,
 										OffsetNumber offnum,
 										VMCorruptionType corruption_type);
@@ -211,8 +222,10 @@ static void prune_freeze_fast_path(PruneState *prstate,
 								   PruneFreezeResult *presult);
 static void prune_freeze_plan(PruneState *prstate,
 							  OffsetNumber *off_loc);
-static HTSV_Result heap_prune_satisfies_vacuum(PruneState *prstate,
-											   HeapTuple tup);
+static HTSV_Result heap_tuple_satisfies_vacuum(HeapTuple tup,
+											   Buffer buffer,
+											   GlobalVisState *vistest,
+											   VacuumCutoffs *cutoffs);
 static inline HTSV_Result htsv_get_valid_status(int status);
 static void heap_prune_chain(OffsetNumber maxoff,
 							 OffsetNumber rootoffnum, PruneState *prstate);
@@ -409,6 +422,51 @@ heap_page_prune_opt(Relation relation, Buffer buffer, Buffer *vmbuffer,
 	}
 }
 
+/* Initialize state used to plan and execute freezing on one heap page. */
+static void
+heap_page_freeze_init(FreezeState *state, bool attempt_freeze,
+					  TransactionId new_relfrozen_xid,
+					  MultiXactId new_relmin_mxid)
+{
+	state->attempt_freeze = attempt_freeze;
+	state->nfrozen = 0;
+	state->pagefrz.freeze_required = false;
+	state->pagefrz.FreezePageConflictXid = InvalidTransactionId;
+
+	if (attempt_freeze)
+	{
+		state->pagefrz.FreezePageRelfrozenXid = new_relfrozen_xid;
+		state->pagefrz.NoFreezePageRelfrozenXid = new_relfrozen_xid;
+		state->pagefrz.FreezePageRelminMxid = new_relmin_mxid;
+		state->pagefrz.NoFreezePageRelminMxid = new_relmin_mxid;
+	}
+	else
+	{
+		state->pagefrz.FreezePageRelminMxid = InvalidMultiXactId;
+		state->pagefrz.NoFreezePageRelminMxid = InvalidMultiXactId;
+		state->pagefrz.FreezePageRelfrozenXid = InvalidTransactionId;
+		state->pagefrz.NoFreezePageRelfrozenXid = InvalidTransactionId;
+	}
+}
+
+/* Commit this page's freeze result to the relation-wide horizon trackers. */
+static void
+heap_page_freeze_update_relstats(FreezeState *state,
+								 TransactionId *new_relfrozen_xid,
+								 MultiXactId *new_relmin_mxid)
+{
+	if (state->nfrozen > 0)
+	{
+		*new_relfrozen_xid = state->pagefrz.FreezePageRelfrozenXid;
+		*new_relmin_mxid = state->pagefrz.FreezePageRelminMxid;
+	}
+	else
+	{
+		*new_relfrozen_xid = state->pagefrz.NoFreezePageRelfrozenXid;
+		*new_relmin_mxid = state->pagefrz.NoFreezePageRelminMxid;
+	}
+}
+
 /*
  * Helper for heap_page_prune_and_freeze() to initialize the PruneState using
  * the provided parameters.
@@ -430,7 +488,6 @@ prune_freeze_setup(PruneFreezeParams *params,
 
 	/* cutoffs must be provided if we will attempt freezing */
 	Assert(!(params->options & HEAP_PAGE_PRUNE_FREEZE) || params->cutoffs);
-	prstate->attempt_freeze = (params->options & HEAP_PAGE_PRUNE_FREEZE) != 0;
 	prstate->attempt_set_vm = (params->options & HEAP_PAGE_PRUNE_SET_VM) != 0;
 	prstate->cutoffs = params->cutoffs;
 	prstate->relation = params->relation;
@@ -459,28 +516,20 @@ prune_freeze_setup(PruneFreezeParams *params,
 	prstate->new_prune_xid = InvalidTransactionId;
 	prstate->latest_xid_removed = InvalidTransactionId;
 	prstate->nredirected = prstate->ndead = prstate->nunused = 0;
-	prstate->nfrozen = 0;
 	prstate->nroot_items = 0;
 	prstate->nheaponly_items = 0;
 
-	/* initialize page freezing working state */
-	prstate->pagefrz.freeze_required = false;
-	prstate->pagefrz.FreezePageConflictXid = InvalidTransactionId;
-	if (prstate->attempt_freeze)
+	if (params->options & HEAP_PAGE_PRUNE_FREEZE)
 	{
 		Assert(new_relfrozen_xid && new_relmin_mxid);
-		prstate->pagefrz.FreezePageRelfrozenXid = *new_relfrozen_xid;
-		prstate->pagefrz.NoFreezePageRelfrozenXid = *new_relfrozen_xid;
-		prstate->pagefrz.FreezePageRelminMxid = *new_relmin_mxid;
-		prstate->pagefrz.NoFreezePageRelminMxid = *new_relmin_mxid;
+		heap_page_freeze_init(&prstate->freeze, true,
+							  *new_relfrozen_xid, *new_relmin_mxid);
 	}
 	else
 	{
 		Assert(!new_relfrozen_xid && !new_relmin_mxid);
-		prstate->pagefrz.FreezePageRelminMxid = InvalidMultiXactId;
-		prstate->pagefrz.NoFreezePageRelminMxid = InvalidMultiXactId;
-		prstate->pagefrz.FreezePageRelfrozenXid = InvalidTransactionId;
-		prstate->pagefrz.NoFreezePageRelfrozenXid = InvalidTransactionId;
+		heap_page_freeze_init(&prstate->freeze, false,
+							  InvalidTransactionId, InvalidMultiXactId);
 	}
 
 	prstate->ndeleted = 0;
@@ -535,7 +584,8 @@ prune_freeze_setup(PruneFreezeParams *params,
 	 * whether to freeze, but before updating the VM, to avoid setting the VM
 	 * bits incorrectly.
 	 */
-	prstate->set_all_frozen = prstate->attempt_freeze && prstate->attempt_set_vm;
+	prstate->set_all_frozen = prstate->freeze.attempt_freeze &&
+		prstate->attempt_set_vm;
 }
 
 /*
@@ -631,7 +681,10 @@ prune_freeze_plan(PruneState *prstate, OffsetNumber *off_loc)
 		tup.t_len = ItemIdGetLength(itemid);
 		ItemPointerSet(&tup.t_self, blockno, offnum);
 
-		prstate->htsv[offnum] = heap_prune_satisfies_vacuum(prstate, &tup);
+		prstate->htsv[offnum] = heap_tuple_satisfies_vacuum(&tup,
+															prstate->buffer,
+															prstate->vistest,
+															prstate->cutoffs);
 
 		if (!HeapTupleHeaderIsHeapOnly(htup))
 			prstate->root_items[prstate->nroot_items++] = offnum;
@@ -756,19 +809,20 @@ heap_page_will_freeze(bool did_tuple_hint_fpi,
 					  bool do_hint_prune,
 					  PruneState *prstate)
 {
+	FreezeState *fstate = &prstate->freeze;
 	bool		do_freeze = false;
 
 	/*
 	 * If the caller specified we should not attempt to freeze any tuples,
 	 * validate that everything is in the right state and return.
 	 */
-	if (!prstate->attempt_freeze)
+	if (!fstate->attempt_freeze)
 	{
-		Assert(!prstate->set_all_frozen && prstate->nfrozen == 0);
+		Assert(!prstate->set_all_frozen && fstate->nfrozen == 0);
 		return false;
 	}
 
-	if (prstate->pagefrz.freeze_required)
+	if (fstate->pagefrz.freeze_required)
 	{
 		/*
 		 * heap_prepare_freeze_tuple indicated that at least one XID/MXID from
@@ -790,7 +844,7 @@ heap_page_will_freeze(bool did_tuple_hint_fpi,
 		 * anymore.  The opportunistic freeze heuristic must be improved;
 		 * however, for now, try to approximate the old logic.
 		 */
-		if (prstate->set_all_frozen && prstate->nfrozen > 0)
+		if (prstate->set_all_frozen && fstate->nfrozen > 0)
 		{
 			Assert(prstate->set_all_visible);
 
@@ -823,20 +877,20 @@ heap_page_will_freeze(bool did_tuple_hint_fpi,
 		 * Validate the tuples we will be freezing before entering the
 		 * critical section.
 		 */
-		heap_pre_freeze_checks(prstate->buffer, prstate->frozen, prstate->nfrozen);
-		Assert(TransactionIdPrecedes(prstate->pagefrz.FreezePageConflictXid,
+		heap_pre_freeze_checks(prstate->buffer, fstate->frozen, fstate->nfrozen);
+		Assert(TransactionIdPrecedes(fstate->pagefrz.FreezePageConflictXid,
 									 prstate->cutoffs->OldestXmin));
 	}
-	else if (prstate->nfrozen > 0)
+	else if (fstate->nfrozen > 0)
 	{
 		/*
 		 * The page contained some tuples that were not already frozen, and we
 		 * chose not to freeze them now.  The page won't be all-frozen then.
 		 */
-		Assert(!prstate->pagefrz.freeze_required);
+		Assert(!fstate->pagefrz.freeze_required);
 
 		prstate->set_all_frozen = false;
-		prstate->nfrozen = 0;	/* avoid miscounts in instrumentation */
+		fstate->nfrozen = 0;	/* avoid miscounts in instrumentation */
 	}
 	else
 	{
@@ -1034,7 +1088,7 @@ prune_freeze_fast_path(PruneState *prstate, PruneFreezeResult *presult)
 
 	Assert((prstate->old_vmbits & VISIBILITYMAP_ALL_FROZEN) ||
 		   ((prstate->old_vmbits & VISIBILITYMAP_ALL_VISIBLE) &&
-			!prstate->attempt_freeze));
+			!prstate->freeze.attempt_freeze));
 
 	/* We'll fill in presult for the caller */
 	memset(presult, 0, sizeof(PruneFreezeResult));
@@ -1150,7 +1204,7 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
 	if ((params->options & HEAP_PAGE_PRUNE_ALLOW_FAST_PATH) != 0 &&
 		((prstate.old_vmbits & VISIBILITYMAP_ALL_FROZEN) ||
 		 ((prstate.old_vmbits & VISIBILITYMAP_ALL_VISIBLE) &&
-		  !prstate.attempt_freeze)))
+		  !prstate.freeze.attempt_freeze)))
 	{
 		prune_freeze_fast_path(&prstate, presult);
 		return;
@@ -1178,7 +1232,7 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
 		prstate.set_all_visible = prstate.set_all_frozen = false;
 
 	/*
-	 * If checksums are enabled, calling heap_prune_satisfies_vacuum() while
+	 * If checksums are enabled, calling heap_tuple_satisfies_vacuum() while
 	 * checking tuple visibility information in prune_freeze_plan() may have
 	 * caused an FPI to be emitted.
 	 */
@@ -1240,8 +1294,10 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
 	conflict_xid = InvalidTransactionId;
 	if (do_set_vm)
 		conflict_xid = prstate.newest_live_xid;
-	if (do_freeze && TransactionIdFollows(prstate.pagefrz.FreezePageConflictXid, conflict_xid))
-		conflict_xid = prstate.pagefrz.FreezePageConflictXid;
+	if (do_freeze &&
+		TransactionIdFollows(prstate.freeze.pagefrz.FreezePageConflictXid,
+							 conflict_xid))
+		conflict_xid = prstate.freeze.pagefrz.FreezePageConflictXid;
 	if (do_prune && TransactionIdFollows(prstate.latest_xid_removed, conflict_xid))
 		conflict_xid = prstate.latest_xid_removed;
 
@@ -1291,7 +1347,9 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
 		}
 
 		if (do_freeze)
-			heap_freeze_prepared_tuples(prstate.buffer, prstate.frozen, prstate.nfrozen);
+			heap_freeze_prepared_tuples(prstate.buffer,
+										prstate.freeze.frozen,
+										prstate.freeze.nfrozen);
 
 		/* Set the visibility map and page visibility hint */
 		if (do_set_vm)
@@ -1327,7 +1385,8 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
 									  conflict_xid,
 									  true, /* cleanup lock */
 									  params->reason,
-									  prstate.frozen, prstate.nfrozen,
+									  prstate.freeze.frozen,
+									  prstate.freeze.nfrozen,
 									  prstate.redirected, prstate.nredirected,
 									  prstate.nowdead, prstate.ndead,
 									  prstate.nowunused, prstate.nunused);
@@ -1377,7 +1436,7 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
 	/* Copy information back for caller */
 	presult->ndeleted = prstate.ndeleted;
 	presult->nnewlpdead = prstate.ndead;
-	presult->nfrozen = prstate.nfrozen;
+	presult->nfrozen = prstate.freeze.nfrozen;
 	presult->live_tuples = prstate.live_tuples;
 	presult->recently_dead_tuples = prstate.recently_dead_tuples;
 	presult->hastup = prstate.hastup;
@@ -1401,32 +1460,22 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
 			presult->newly_all_frozen = true;
 	}
 
-	if (prstate.attempt_freeze)
-	{
-		if (presult->nfrozen > 0)
-		{
-			*new_relfrozen_xid = prstate.pagefrz.FreezePageRelfrozenXid;
-			*new_relmin_mxid = prstate.pagefrz.FreezePageRelminMxid;
-		}
-		else
-		{
-			*new_relfrozen_xid = prstate.pagefrz.NoFreezePageRelfrozenXid;
-			*new_relmin_mxid = prstate.pagefrz.NoFreezePageRelminMxid;
-		}
-	}
+	if (prstate.freeze.attempt_freeze)
+		heap_page_freeze_update_relstats(&prstate.freeze,
+										 new_relfrozen_xid, new_relmin_mxid);
 }
 
-
-/*
- * Perform visibility checks for heap pruning.
- */
+/* Perform visibility checks shared by pruning and freezing. */
 static HTSV_Result
-heap_prune_satisfies_vacuum(PruneState *prstate, HeapTuple tup)
+heap_tuple_satisfies_vacuum(HeapTuple tup,
+							Buffer buffer,
+							GlobalVisState *vistest,
+							VacuumCutoffs *cutoffs)
 {
 	HTSV_Result res;
 	TransactionId dead_after;
 
-	res = HeapTupleSatisfiesVacuumHorizon(tup, prstate->buffer, &dead_after);
+	res = HeapTupleSatisfiesVacuumHorizon(tup, buffer, &dead_after);
 
 	if (res != HEAPTUPLE_RECENTLY_DEAD)
 		return res;
@@ -1437,9 +1486,8 @@ heap_prune_satisfies_vacuum(PruneState *prstate, HeapTuple tup)
 	 * vacuuming the relation. OldestXmin is used for freezing determination
 	 * and we cannot freeze dead tuples' xmaxes.
 	 */
-	if (prstate->cutoffs &&
-		TransactionIdIsValid(prstate->cutoffs->OldestXmin) &&
-		NormalTransactionIdPrecedes(dead_after, prstate->cutoffs->OldestXmin))
+	if (cutoffs && TransactionIdIsValid(cutoffs->OldestXmin) &&
+		NormalTransactionIdPrecedes(dead_after, cutoffs->OldestXmin))
 		return HEAPTUPLE_DEAD;
 
 	/*
@@ -1450,7 +1498,7 @@ heap_prune_satisfies_vacuum(PruneState *prstate, HeapTuple tup)
 	 * if the GlobalVisState has been updated since the beginning of vacuuming
 	 * the relation.
 	 */
-	if (GlobalVisTestIsRemovableXid(prstate->vistest, dead_after, true))
+	if (GlobalVisTestIsRemovableXid(vistest, dead_after, true))
 		return HEAPTUPLE_DEAD;
 
 	return res;
@@ -1996,18 +2044,18 @@ heap_prune_record_unchanged_lp_normal(PruneState *prstate, OffsetNumber offnum)
 	}
 
 	/* Consider freezing any normal tuples which will not be removed */
-	if (prstate->attempt_freeze)
+	if (prstate->freeze.attempt_freeze)
 	{
 		bool		totally_frozen;
 
 		if ((heap_prepare_freeze_tuple(htup,
 									   prstate->cutoffs,
-									   &prstate->pagefrz,
-									   &prstate->frozen[prstate->nfrozen],
+									   &prstate->freeze.pagefrz,
+									   &prstate->freeze.frozen[prstate->freeze.nfrozen],
 									   &totally_frozen)))
 		{
 			/* Save prepared freeze plan for later */
-			prstate->frozen[prstate->nfrozen++].offset = offnum;
+			prstate->freeze.frozen[prstate->freeze.nfrozen++].offset = offnum;
 		}
 
 		/*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6c366d3a523..d419c9ea401 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -875,6 +875,7 @@ FixedParamState
 FlagMode
 Float
 FlushPosition
+FreezeState
 FmgrBuiltin
 FmgrHookEventType
 FmgrInfo
-- 
2.43.7

