]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Split heap_page_prune_and_freeze() into helpers
authorMelanie Plageman <melanieplageman@gmail.com>
Wed, 26 Nov 2025 15:57:50 +0000 (10:57 -0500)
committerMelanie Plageman <melanieplageman@gmail.com>
Wed, 26 Nov 2025 16:00:34 +0000 (11:00 -0500)
Refactor the setup and planning phases of pruning and freezing into
helpers. This streamlines heap_page_prune_and_freeze() and makes it more
clear when the examination of tuples ends and page modifications begin.

No code change beyond what was required to extract the code into helper
functions.

Author: Melanie Plageman <melanieplageman@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/mhf4vkmh3j57zx7vuxp4jagtdzwhu3573pgfpmnjwqa6i6yj5y%40sy4ymcdtdklo

src/backend/access/heap/pruneheap.c

index ac958d86374888437933e443b6b3aab393e9c2a2..5af84b4c875dd78ec747ea066f44ca19d022fe3b 100644 (file)
@@ -157,6 +157,14 @@ typedef struct
 } PruneState;
 
 /* Local functions */
+static void prune_freeze_setup(PruneFreezeParams *params,
+                                                          TransactionId new_relfrozen_xid,
+                                                          MultiXactId new_relmin_mxid,
+                                                          const PruneFreezeResult *presult,
+                                                          PruneState *prstate);
+static void prune_freeze_plan(Oid reloid, Buffer buffer,
+                                                         PruneState *prstate,
+                                                         OffsetNumber *off_loc);
 static HTSV_Result heap_prune_satisfies_vacuum(PruneState *prstate,
                                                                                           HeapTuple tup,
                                                                                           Buffer buffer);
@@ -312,203 +320,25 @@ heap_page_prune_opt(Relation relation, Buffer buffer)
 }
 
 /*
- * Decide whether to proceed with freezing according to the freeze plans
- * prepared for the given heap buffer. If freezing is chosen, this function
- * performs several pre-freeze checks.
- *
- * The values of do_prune, do_hint_prune, and did_tuple_hint_fpi must be
- * determined before calling this function.
- *
- * prstate is both an input and output parameter.
- *
- * Returns true if we should apply the freeze plans and freeze tuples on the
- * page, and false otherwise.
- */
-static bool
-heap_page_will_freeze(Relation relation, Buffer buffer,
-                                         bool did_tuple_hint_fpi,
-                                         bool do_prune,
-                                         bool do_hint_prune,
-                                         PruneState *prstate)
-{
-       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)
-       {
-               Assert(!prstate->all_frozen && prstate->nfrozen == 0);
-               Assert(prstate->lpdead_items == 0 || !prstate->all_visible);
-               return false;
-       }
-
-       if (prstate->pagefrz.freeze_required)
-       {
-               /*
-                * heap_prepare_freeze_tuple indicated that at least one XID/MXID from
-                * before FreezeLimit/MultiXactCutoff is present.  Must freeze to
-                * advance relfrozenxid/relminmxid.
-                */
-               do_freeze = true;
-       }
-       else
-       {
-               /*
-                * Opportunistically freeze the page if we are generating an FPI
-                * anyway and if doing so means that we can set the page all-frozen
-                * afterwards (might not happen until VACUUM's final heap pass).
-                *
-                * XXX: Previously, we knew if pruning emitted an FPI by checking
-                * pgWalUsage.wal_fpi before and after pruning.  Once the freeze and
-                * prune records were combined, this heuristic couldn't be used
-                * anymore.  The opportunistic freeze heuristic must be improved;
-                * however, for now, try to approximate the old logic.
-                */
-               if (prstate->all_frozen && prstate->nfrozen > 0)
-               {
-                       Assert(prstate->all_visible);
-
-                       /*
-                        * Freezing would make the page all-frozen.  Have already emitted
-                        * an FPI or will do so anyway?
-                        */
-                       if (RelationNeedsWAL(relation))
-                       {
-                               if (did_tuple_hint_fpi)
-                                       do_freeze = true;
-                               else if (do_prune)
-                               {
-                                       if (XLogCheckBufferNeedsBackup(buffer))
-                                               do_freeze = true;
-                               }
-                               else if (do_hint_prune)
-                               {
-                                       if (XLogHintBitIsNeeded() && XLogCheckBufferNeedsBackup(buffer))
-                                               do_freeze = true;
-                               }
-                       }
-               }
-       }
-
-       if (do_freeze)
-       {
-               /*
-                * Validate the tuples we will be freezing before entering the
-                * critical section.
-                */
-               heap_pre_freeze_checks(buffer, prstate->frozen, prstate->nfrozen);
-
-               /*
-                * Calculate what the snapshot conflict horizon should be for a record
-                * freezing tuples. We can use the visibility_cutoff_xid as our cutoff
-                * for conflicts when the whole page is eligible to become all-frozen
-                * in the VM once we're done with it. Otherwise, we generate a
-                * conservative cutoff by stepping back from OldestXmin.
-                */
-               if (prstate->all_frozen)
-                       prstate->frz_conflict_horizon = prstate->visibility_cutoff_xid;
-               else
-               {
-                       /* Avoids false conflicts when hot_standby_feedback in use */
-                       prstate->frz_conflict_horizon = prstate->cutoffs->OldestXmin;
-                       TransactionIdRetreat(prstate->frz_conflict_horizon);
-               }
-       }
-       else if (prstate->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);
-
-               prstate->all_frozen = false;
-               prstate->nfrozen = 0;   /* avoid miscounts in instrumentation */
-       }
-       else
-       {
-               /*
-                * We have no freeze plans to execute.  The page might already be
-                * all-frozen (perhaps only following pruning), though.  Such pages
-                * can be marked all-frozen in the VM by our caller, even though none
-                * of its tuples were newly frozen here.
-                */
-       }
-
-       return do_freeze;
-}
-
-
-/*
- * Prune and repair fragmentation and potentially freeze tuples on the
- * specified page.
- *
- * Caller must have pin and buffer cleanup lock on the page.  Note that we
- * don't update the FSM information for page on caller's behalf.  Caller might
- * also need to account for a reduction in the length of the line pointer
- * array following array truncation by us.
- *
- * params contains the input parameters used to control freezing and pruning
- * behavior. See the definition of PruneFreezeParams for more on what each
- * parameter does.
- *
- * If the HEAP_PAGE_PRUNE_FREEZE option is set in params, we will freeze
- * tuples if it's required in order to advance relfrozenxid / relminmxid, or
- * if it's considered advantageous for overall system performance to do so
- * now.  The 'params.cutoffs', 'presult', 'new_relfrozen_xid' and
- * 'new_relmin_mxid' arguments are required when freezing.  When
- * HEAP_PAGE_PRUNE_FREEZE option is passed, we also set presult->all_visible
- * and presult->all_frozen after determining whether or not to
- * opportunistically freeze, to indicate if the VM bits can be set.  They are
- * always set to false when the HEAP_PAGE_PRUNE_FREEZE option is not passed,
- * because at the moment only callers that also freeze need that information.
- *
- * presult contains output parameters needed by callers, such as the number of
- * tuples removed and the offsets of dead items on the page after pruning.
- * heap_page_prune_and_freeze() is responsible for initializing it.  Required
- * by all callers.
- *
- * off_loc is the offset location required by the caller to use in error
- * callback.
- *
- * new_relfrozen_xid and new_relmin_mxid must provided by the caller if the
- * HEAP_PAGE_PRUNE_FREEZE option is set in params.  On entry, they contain the
- * oldest XID and multi-XID seen on the relation so far.  They will be updated
- * with oldest values present on the page after pruning.  After processing the
- * whole relation, VACUUM can use these values as the new
- * relfrozenxid/relminmxid for the relation.
+ * Helper for heap_page_prune_and_freeze() to initialize the PruneState using
+ * the provided parameters.
  */
-void
-heap_page_prune_and_freeze(PruneFreezeParams *params,
-                                                  PruneFreezeResult *presult,
-                                                  OffsetNumber *off_loc,
-                                                  TransactionId *new_relfrozen_xid,
-                                                  MultiXactId *new_relmin_mxid)
+static void
+prune_freeze_setup(PruneFreezeParams *params,
+                                  TransactionId new_relfrozen_xid,
+                                  MultiXactId new_relmin_mxid,
+                                  const PruneFreezeResult *presult,
+                                  PruneState *prstate)
 {
-       Buffer          buffer = params->buffer;
-       Page            page = BufferGetPage(buffer);
-       BlockNumber blockno = BufferGetBlockNumber(buffer);
-       OffsetNumber offnum,
-                               maxoff;
-       PruneState      prstate;
-       HeapTupleData tup;
-       bool            do_freeze;
-       bool            do_prune;
-       bool            do_hint_prune;
-       bool            did_tuple_hint_fpi;
-       int64           fpi_before = pgWalUsage.wal_fpi;
-
        /* Copy parameters to prstate */
-       prstate.vistest = params->vistest;
-       prstate.mark_unused_now =
+       prstate->vistest = params->vistest;
+       prstate->mark_unused_now =
                (params->options & HEAP_PAGE_PRUNE_MARK_UNUSED_NOW) != 0;
 
        /* 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.cutoffs = params->cutoffs;
+       prstate->attempt_freeze = (params->options & HEAP_PAGE_PRUNE_FREEZE) != 0;
+       prstate->cutoffs = params->cutoffs;
 
        /*
         * Our strategy is to scan the page and make lists of items to change,
@@ -521,41 +351,42 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
         * prunable, we will save the lowest relevant XID in new_prune_xid. Also
         * initialize the rest of our working state.
         */
-       prstate.new_prune_xid = InvalidTransactionId;
-       prstate.latest_xid_removed = InvalidTransactionId;
-       prstate.nredirected = prstate.ndead = prstate.nunused = prstate.nfrozen = 0;
-       prstate.nroot_items = 0;
-       prstate.nheaponly_items = 0;
+       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;
-       if (prstate.attempt_freeze)
+       prstate->pagefrz.freeze_required = false;
+       if (prstate->attempt_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;
+               prstate->pagefrz.FreezePageRelfrozenXid = new_relfrozen_xid;
+               prstate->pagefrz.NoFreezePageRelfrozenXid = new_relfrozen_xid;
+               prstate->pagefrz.FreezePageRelminMxid = new_relmin_mxid;
+               prstate->pagefrz.NoFreezePageRelminMxid = new_relmin_mxid;
        }
        else
        {
-               Assert(new_relfrozen_xid == NULL && new_relmin_mxid == NULL);
-               prstate.pagefrz.FreezePageRelminMxid = InvalidMultiXactId;
-               prstate.pagefrz.NoFreezePageRelminMxid = InvalidMultiXactId;
-               prstate.pagefrz.FreezePageRelfrozenXid = InvalidTransactionId;
-               prstate.pagefrz.NoFreezePageRelfrozenXid = InvalidTransactionId;
+               Assert(new_relfrozen_xid == InvalidTransactionId &&
+                          new_relmin_mxid == InvalidMultiXactId);
+               prstate->pagefrz.FreezePageRelminMxid = InvalidMultiXactId;
+               prstate->pagefrz.NoFreezePageRelminMxid = InvalidMultiXactId;
+               prstate->pagefrz.FreezePageRelfrozenXid = InvalidTransactionId;
+               prstate->pagefrz.NoFreezePageRelfrozenXid = InvalidTransactionId;
        }
 
-       prstate.ndeleted = 0;
-       prstate.live_tuples = 0;
-       prstate.recently_dead_tuples = 0;
-       prstate.hastup = false;
-       prstate.lpdead_items = 0;
-       prstate.deadoffsets = presult->deadoffsets;
-       prstate.frz_conflict_horizon = InvalidTransactionId;
+       prstate->ndeleted = 0;
+       prstate->live_tuples = 0;
+       prstate->recently_dead_tuples = 0;
+       prstate->hastup = false;
+       prstate->lpdead_items = 0;
+       prstate->deadoffsets = (OffsetNumber *) presult->deadoffsets;
+       prstate->frz_conflict_horizon = InvalidTransactionId;
 
        /*
-        * Caller may update the VM after we're done.  We can keep track of
+        * Vacuum may update the VM after we're done.  We can keep track of
         * whether the page will be all-visible and all-frozen after pruning and
         * freezing to help the caller to do that.
         *
@@ -578,10 +409,10 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
         * all_frozen before we return them to the caller, so that the caller
         * doesn't set the VM bits incorrectly.
         */
-       if (prstate.attempt_freeze)
+       if (prstate->attempt_freeze)
        {
-               prstate.all_visible = true;
-               prstate.all_frozen = true;
+               prstate->all_visible = true;
+               prstate->all_frozen = true;
        }
        else
        {
@@ -589,8 +420,8 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
                 * Initializing to false allows skipping the work to update them in
                 * heap_prune_record_unchanged_lp_normal().
                 */
-               prstate.all_visible = false;
-               prstate.all_frozen = false;
+               prstate->all_visible = false;
+               prstate->all_frozen = false;
        }
 
        /*
@@ -601,10 +432,29 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
         * running transaction on the standby does not see tuples on the page as
         * all-visible, so the conflict horizon remains InvalidTransactionId.
         */
-       prstate.visibility_cutoff_xid = InvalidTransactionId;
+       prstate->visibility_cutoff_xid = InvalidTransactionId;
+}
 
-       maxoff = PageGetMaxOffsetNumber(page);
-       tup.t_tableOid = RelationGetRelid(params->relation);
+/*
+ * Helper for heap_page_prune_and_freeze(). Iterates over every tuple on the
+ * page, examines its visibility information, and determines the appropriate
+ * action for each tuple. All tuples are processed and classified during this
+ * phase, but no modifications are made to the page until the later execution
+ * stage.
+ *
+ * *off_loc is used for error callback and cleared before returning.
+ */
+static void
+prune_freeze_plan(Oid reloid, Buffer buffer, PruneState *prstate,
+                                 OffsetNumber *off_loc)
+{
+       Page            page = BufferGetPage(buffer);
+       BlockNumber blockno = BufferGetBlockNumber(buffer);
+       OffsetNumber maxoff = PageGetMaxOffsetNumber(page);
+       OffsetNumber offnum;
+       HeapTupleData tup;
+
+       tup.t_tableOid = reloid;
 
        /*
         * Determine HTSV for all tuples, and queue them up for processing as HOT
@@ -639,13 +489,13 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
                 */
                *off_loc = offnum;
 
-               prstate.processed[offnum] = false;
-               prstate.htsv[offnum] = -1;
+               prstate->processed[offnum] = false;
+               prstate->htsv[offnum] = -1;
 
                /* Nothing to do if slot doesn't contain a tuple */
                if (!ItemIdIsUsed(itemid))
                {
-                       heap_prune_record_unchanged_lp_unused(page, &prstate, offnum);
+                       heap_prune_record_unchanged_lp_unused(page, prstate, offnum);
                        continue;
                }
 
@@ -655,17 +505,17 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
                         * If the caller set mark_unused_now true, we can set dead line
                         * pointers LP_UNUSED now.
                         */
-                       if (unlikely(prstate.mark_unused_now))
-                               heap_prune_record_unused(&prstate, offnum, false);
+                       if (unlikely(prstate->mark_unused_now))
+                               heap_prune_record_unused(prstate, offnum, false);
                        else
-                               heap_prune_record_unchanged_lp_dead(page, &prstate, offnum);
+                               heap_prune_record_unchanged_lp_dead(page, prstate, offnum);
                        continue;
                }
 
                if (ItemIdIsRedirected(itemid))
                {
                        /* This is the start of a HOT chain */
-                       prstate.root_items[prstate.nroot_items++] = offnum;
+                       prstate->root_items[prstate->nroot_items++] = offnum;
                        continue;
                }
 
@@ -679,21 +529,15 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
                tup.t_len = ItemIdGetLength(itemid);
                ItemPointerSet(&tup.t_self, blockno, offnum);
 
-               prstate.htsv[offnum] = heap_prune_satisfies_vacuum(&prstate, &tup,
-                                                                                                                  buffer);
+               prstate->htsv[offnum] = heap_prune_satisfies_vacuum(prstate, &tup,
+                                                                                                                       buffer);
 
                if (!HeapTupleHeaderIsHeapOnly(htup))
-                       prstate.root_items[prstate.nroot_items++] = offnum;
+                       prstate->root_items[prstate->nroot_items++] = offnum;
                else
-                       prstate.heaponly_items[prstate.nheaponly_items++] = offnum;
+                       prstate->heaponly_items[prstate->nheaponly_items++] = offnum;
        }
 
-       /*
-        * If checksums are enabled, heap_prune_satisfies_vacuum() may have caused
-        * an FPI to be emitted.
-        */
-       did_tuple_hint_fpi = fpi_before != pgWalUsage.wal_fpi;
-
        /*
         * Process HOT chains.
         *
@@ -705,30 +549,30 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
         * the page instead of using the root_items array, also did it in
         * ascending offset number order.)
         */
-       for (int i = prstate.nroot_items - 1; i >= 0; i--)
+       for (int i = prstate->nroot_items - 1; i >= 0; i--)
        {
-               offnum = prstate.root_items[i];
+               offnum = prstate->root_items[i];
 
                /* Ignore items already processed as part of an earlier chain */
-               if (prstate.processed[offnum])
+               if (prstate->processed[offnum])
                        continue;
 
                /* see preceding loop */
                *off_loc = offnum;
 
                /* Process this item or chain of items */
-               heap_prune_chain(page, blockno, maxoff, offnum, &prstate);
+               heap_prune_chain(page, blockno, maxoff, offnum, prstate);
        }
 
        /*
         * Process any heap-only tuples that were not already processed as part of
         * a HOT chain.
         */
-       for (int i = prstate.nheaponly_items - 1; i >= 0; i--)
+       for (int i = prstate->nheaponly_items - 1; i >= 0; i--)
        {
-               offnum = prstate.heaponly_items[i];
+               offnum = prstate->heaponly_items[i];
 
-               if (prstate.processed[offnum])
+               if (prstate->processed[offnum])
                        continue;
 
                /* see preceding loop */
@@ -747,7 +591,7 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
                 * return true for an XMIN_INVALID tuple, so this code will work even
                 * when there were sequential updates within the aborted transaction.)
                 */
-               if (prstate.htsv[offnum] == HEAPTUPLE_DEAD)
+               if (prstate->htsv[offnum] == HEAPTUPLE_DEAD)
                {
                        ItemId          itemid = PageGetItemId(page, offnum);
                        HeapTupleHeader htup = (HeapTupleHeader) PageGetItem(page, itemid);
@@ -755,8 +599,8 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
                        if (likely(!HeapTupleHeaderIsHotUpdated(htup)))
                        {
                                HeapTupleHeaderAdvanceConflictHorizon(htup,
-                                                                                                         &prstate.latest_xid_removed);
-                               heap_prune_record_unused(&prstate, offnum, true);
+                                                                                                         &prstate->latest_xid_removed);
+                               heap_prune_record_unused(prstate, offnum, true);
                        }
                        else
                        {
@@ -773,7 +617,7 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
                        }
                }
                else
-                       heap_prune_record_unchanged_lp_normal(page, &prstate, offnum);
+                       heap_prune_record_unchanged_lp_normal(page, prstate, offnum);
        }
 
        /* We should now have processed every tuple exactly once  */
@@ -784,12 +628,223 @@ heap_page_prune_and_freeze(PruneFreezeParams *params,
        {
                *off_loc = offnum;
 
-               Assert(prstate.processed[offnum]);
+               Assert(prstate->processed[offnum]);
        }
 #endif
 
        /* Clear the offset information once we have processed the given page. */
        *off_loc = InvalidOffsetNumber;
+}
+
+/*
+ * Decide whether to proceed with freezing according to the freeze plans
+ * prepared for the given heap buffer. If freezing is chosen, this function
+ * performs several pre-freeze checks.
+ *
+ * The values of do_prune, do_hint_prune, and did_tuple_hint_fpi must be
+ * determined before calling this function.
+ *
+ * prstate is both an input and output parameter.
+ *
+ * Returns true if we should apply the freeze plans and freeze tuples on the
+ * page, and false otherwise.
+ */
+static bool
+heap_page_will_freeze(Relation relation, Buffer buffer,
+                                         bool did_tuple_hint_fpi,
+                                         bool do_prune,
+                                         bool do_hint_prune,
+                                         PruneState *prstate)
+{
+       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)
+       {
+               Assert(!prstate->all_frozen && prstate->nfrozen == 0);
+               Assert(prstate->lpdead_items == 0 || !prstate->all_visible);
+               return false;
+       }
+
+       if (prstate->pagefrz.freeze_required)
+       {
+               /*
+                * heap_prepare_freeze_tuple indicated that at least one XID/MXID from
+                * before FreezeLimit/MultiXactCutoff is present.  Must freeze to
+                * advance relfrozenxid/relminmxid.
+                */
+               do_freeze = true;
+       }
+       else
+       {
+               /*
+                * Opportunistically freeze the page if we are generating an FPI
+                * anyway and if doing so means that we can set the page all-frozen
+                * afterwards (might not happen until VACUUM's final heap pass).
+                *
+                * XXX: Previously, we knew if pruning emitted an FPI by checking
+                * pgWalUsage.wal_fpi before and after pruning.  Once the freeze and
+                * prune records were combined, this heuristic couldn't be used
+                * anymore.  The opportunistic freeze heuristic must be improved;
+                * however, for now, try to approximate the old logic.
+                */
+               if (prstate->all_frozen && prstate->nfrozen > 0)
+               {
+                       Assert(prstate->all_visible);
+
+                       /*
+                        * Freezing would make the page all-frozen.  Have already emitted
+                        * an FPI or will do so anyway?
+                        */
+                       if (RelationNeedsWAL(relation))
+                       {
+                               if (did_tuple_hint_fpi)
+                                       do_freeze = true;
+                               else if (do_prune)
+                               {
+                                       if (XLogCheckBufferNeedsBackup(buffer))
+                                               do_freeze = true;
+                               }
+                               else if (do_hint_prune)
+                               {
+                                       if (XLogHintBitIsNeeded() && XLogCheckBufferNeedsBackup(buffer))
+                                               do_freeze = true;
+                               }
+                       }
+               }
+       }
+
+       if (do_freeze)
+       {
+               /*
+                * Validate the tuples we will be freezing before entering the
+                * critical section.
+                */
+               heap_pre_freeze_checks(buffer, prstate->frozen, prstate->nfrozen);
+
+               /*
+                * Calculate what the snapshot conflict horizon should be for a record
+                * freezing tuples. We can use the visibility_cutoff_xid as our cutoff
+                * for conflicts when the whole page is eligible to become all-frozen
+                * in the VM once we're done with it. Otherwise, we generate a
+                * conservative cutoff by stepping back from OldestXmin.
+                */
+               if (prstate->all_frozen)
+                       prstate->frz_conflict_horizon = prstate->visibility_cutoff_xid;
+               else
+               {
+                       /* Avoids false conflicts when hot_standby_feedback in use */
+                       prstate->frz_conflict_horizon = prstate->cutoffs->OldestXmin;
+                       TransactionIdRetreat(prstate->frz_conflict_horizon);
+               }
+       }
+       else if (prstate->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);
+
+               prstate->all_frozen = false;
+               prstate->nfrozen = 0;   /* avoid miscounts in instrumentation */
+       }
+       else
+       {
+               /*
+                * We have no freeze plans to execute.  The page might already be
+                * all-frozen (perhaps only following pruning), though.  Such pages
+                * can be marked all-frozen in the VM by our caller, even though none
+                * of its tuples were newly frozen here.
+                */
+       }
+
+       return do_freeze;
+}
+
+
+/*
+ * Prune and repair fragmentation and potentially freeze tuples on the
+ * specified page.
+ *
+ * Caller must have pin and buffer cleanup lock on the page.  Note that we
+ * don't update the FSM information for page on caller's behalf.  Caller might
+ * also need to account for a reduction in the length of the line pointer
+ * array following array truncation by us.
+ *
+ * params contains the input parameters used to control freezing and pruning
+ * behavior. See the definition of PruneFreezeParams for more on what each
+ * parameter does.
+ *
+ * If the HEAP_PAGE_PRUNE_FREEZE option is set in params, we will freeze
+ * tuples if it's required in order to advance relfrozenxid / relminmxid, or
+ * if it's considered advantageous for overall system performance to do so
+ * now.  The 'params.cutoffs', 'presult', 'new_relfrozen_xid' and
+ * 'new_relmin_mxid' arguments are required when freezing.  When
+ * HEAP_PAGE_PRUNE_FREEZE option is passed, we also set presult->all_visible
+ * and presult->all_frozen after determining whether or not to
+ * opportunistically freeze, to indicate if the VM bits can be set.  They are
+ * always set to false when the HEAP_PAGE_PRUNE_FREEZE option is not passed,
+ * because at the moment only callers that also freeze need that information.
+ *
+ * presult contains output parameters needed by callers, such as the number of
+ * tuples removed and the offsets of dead items on the page after pruning.
+ * heap_page_prune_and_freeze() is responsible for initializing it.  Required
+ * by all callers.
+ *
+ * off_loc is the offset location required by the caller to use in error
+ * callback.
+ *
+ * new_relfrozen_xid and new_relmin_mxid must be provided by the caller if the
+ * HEAP_PAGE_PRUNE_FREEZE option is set in params.  On entry, they contain the
+ * oldest XID and multi-XID seen on the relation so far.  They will be updated
+ * with the oldest values present on the page after pruning.  After processing
+ * the whole relation, VACUUM can use these values as the new
+ * relfrozenxid/relminmxid for the relation.
+ */
+void
+heap_page_prune_and_freeze(PruneFreezeParams *params,
+                                                  PruneFreezeResult *presult,
+                                                  OffsetNumber *off_loc,
+                                                  TransactionId *new_relfrozen_xid,
+                                                  MultiXactId *new_relmin_mxid)
+{
+       Buffer          buffer = params->buffer;
+       Page            page = BufferGetPage(buffer);
+       PruneState      prstate;
+       bool            do_freeze;
+       bool            do_prune;
+       bool            do_hint_prune;
+       bool            did_tuple_hint_fpi;
+       int64           fpi_before = pgWalUsage.wal_fpi;
+
+       /* Initialize prstate */
+       prune_freeze_setup(params,
+                                          new_relfrozen_xid ?
+                                          *new_relfrozen_xid : InvalidTransactionId,
+                                          new_relmin_mxid ?
+                                          *new_relmin_mxid : InvalidMultiXactId,
+                                          presult,
+                                          &prstate);
+
+       /*
+        * Examine all line pointers and tuple visibility information to determine
+        * which line pointers should change state and which tuples may be frozen.
+        * Prepare queue of state changes to later be executed in a critical
+        * section.
+        */
+       prune_freeze_plan(RelationGetRelid(params->relation),
+                                         buffer, &prstate, off_loc);
+
+       /*
+        * If checksums are enabled, calling heap_prune_satisfies_vacuum() while
+        * checking tuple visibility information in prune_freeze_plan() may have
+        * caused an FPI to be emitted.
+        */
+       did_tuple_hint_fpi = fpi_before != pgWalUsage.wal_fpi;
 
        do_prune = prstate.nredirected > 0 ||
                prstate.ndead > 0 ||