CacheInvalidateHeapTuple(relation, tuple, NULL);
 }
 
+#define        FRM_NOOP                0x0001
+#define        FRM_INVALIDATE_XMAX     0x0002
+#define        FRM_RETURN_IS_XID       0x0004
+#define        FRM_RETURN_IS_MULTI     0x0008
+#define        FRM_MARK_COMMITTED      0x0010
 
 /*
- * heap_freeze_tuple
+ * FreezeMultiXactId
+ *     Determine what to do during freezing when a tuple is marked by a
+ *     MultiXactId.
+ *
+ * NB -- this might have the side-effect of creating a new MultiXactId!
+ *
+ * "flags" is an output value; it's used to tell caller what to do on return.
+ * Possible flags are:
+ * FRM_NOOP
+ *     don't do anything -- keep existing Xmax
+ * FRM_INVALIDATE_XMAX
+ *     mark Xmax as InvalidTransactionId and set XMAX_INVALID flag.
+ * FRM_RETURN_IS_XID
+ *     The Xid return value is a single update Xid to set as xmax.
+ * FRM_MARK_COMMITTED
+ *     Xmax can be marked as HEAP_XMAX_COMMITTED
+ * FRM_RETURN_IS_MULTI
+ *     The return value is a new MultiXactId to set as new Xmax.
+ *     (caller must obtain proper infomask bits using GetMultiXactIdHintBits)
+ */
+static TransactionId
+FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
+                 TransactionId cutoff_xid, MultiXactId cutoff_multi,
+                 uint16 *flags)
+{
+   TransactionId xid = InvalidTransactionId;
+   int         i;
+   MultiXactMember *members;
+   int         nmembers;
+   bool        need_replace;
+   int         nnewmembers;
+   MultiXactMember *newmembers;
+   bool        has_lockers;
+   TransactionId update_xid;
+   bool        update_committed;
+
+   *flags = 0;
+
+   /* We should only be called in Multis */
+   Assert(t_infomask & HEAP_XMAX_IS_MULTI);
+
+   if (!MultiXactIdIsValid(multi))
+   {
+       /* Ensure infomask bits are appropriately set/reset */
+       *flags |= FRM_INVALIDATE_XMAX;
+       return InvalidTransactionId;
+   }
+   else if (MultiXactIdPrecedes(multi, cutoff_multi))
+   {
+       /*
+        * This old multi cannot possibly have members still running.  If it
+        * was a locker only, it can be removed without any further
+        * consideration; but if it contained an update, we might need to
+        * preserve it.
+        */
+       Assert(!MultiXactIdIsRunning(multi));
+       if (HEAP_XMAX_IS_LOCKED_ONLY(t_infomask))
+       {
+           *flags |= FRM_INVALIDATE_XMAX;
+           xid = InvalidTransactionId; /* not strictly necessary */
+       }
+       else
+       {
+           /* replace multi by update xid */
+           xid = MultiXactIdGetUpdateXid(multi, t_infomask);
+
+           /* wasn't only a lock, xid needs to be valid */
+           Assert(TransactionIdIsValid(xid));
+
+           /*
+            * If the xid is older than the cutoff, it has to have aborted,
+            * otherwise the tuple would have gotten pruned away.
+            */
+           if (TransactionIdPrecedes(xid, cutoff_xid))
+           {
+               Assert(!TransactionIdDidCommit(xid));
+               *flags |= FRM_INVALIDATE_XMAX;
+               xid = InvalidTransactionId;     /* not strictly necessary */
+           }
+           else
+           {
+               *flags |= FRM_RETURN_IS_XID;
+           }
+       }
+
+       return xid;
+   }
+
+   /*
+    * This multixact might have or might not have members still running, but
+    * we know it's valid and is newer than the cutoff point for multis.
+    * However, some member(s) of it may be below the cutoff for Xids, so we
+    * need to walk the whole members array to figure out what to do, if
+    * anything.
+    */
+
+   nmembers = GetMultiXactIdMembers(multi, &members, false);
+   if (nmembers <= 0)
+   {
+       /* Nothing worth keeping */
+       *flags |= FRM_INVALIDATE_XMAX;
+       return InvalidTransactionId;
+   }
+
+   /* is there anything older than the cutoff? */
+   need_replace = false;
+   for (i = 0; i < nmembers; i++)
+   {
+       if (TransactionIdPrecedes(members[i].xid, cutoff_xid))
+       {
+           need_replace = true;
+           break;
+       }
+   }
+
+   /*
+    * In the simplest case, there is no member older than the cutoff; we can
+    * keep the existing MultiXactId as is.
+    */
+   if (!need_replace)
+   {
+       *flags |= FRM_NOOP;
+       pfree(members);
+       return InvalidTransactionId;
+   }
+
+   /*
+    * If the multi needs to be updated, figure out which members do we need
+    * to keep.
+    */
+   nnewmembers = 0;
+   newmembers = palloc(sizeof(MultiXactMember) * nmembers);
+   has_lockers = false;
+   update_xid = InvalidTransactionId;
+   update_committed = false;
+
+   for (i = 0; i < nmembers; i++)
+   {
+       /*
+        * Determine whether to keep this member or ignore it.
+        */
+       if (ISUPDATE_from_mxstatus(members[i].status))
+       {
+           TransactionId   xid = members[i].xid;
+
+           /*
+            * It's an update; should we keep it?  If the transaction is known
+            * aborted then it's okay to ignore it, otherwise not.  However,
+            * if the Xid is older than the cutoff_xid, we must remove it.
+            * Note that such an old updater cannot possibly be committed,
+            * because HeapTupleSatisfiesVacuum would have returned
+            * HEAPTUPLE_DEAD and we would not be trying to freeze the tuple.
+            *
+            * Note the TransactionIdDidAbort() test is just an optimization
+            * and not strictly necessary for correctness.
+            *
+            * As with all tuple visibility routines, it's critical to test
+            * TransactionIdIsInProgress before the transam.c routines,
+            * because of race conditions explained in detail in tqual.c.
+            */
+           if (TransactionIdIsCurrentTransactionId(xid) ||
+               TransactionIdIsInProgress(xid))
+           {
+               Assert(!TransactionIdIsValid(update_xid));
+               update_xid = xid;
+           }
+           else if (!TransactionIdDidAbort(xid))
+           {
+               /*
+                * Test whether to tell caller to set HEAP_XMAX_COMMITTED
+                * while we have the Xid still in cache.  Note this can only
+                * be done if the transaction is known not running.
+                */
+               if (TransactionIdDidCommit(xid))
+                   update_committed = true;
+               Assert(!TransactionIdIsValid(update_xid));
+               update_xid = xid;
+           }
+
+           /*
+            * If we determined that it's an Xid corresponding to an update
+            * that must be retained, additionally add it to the list of
+            * members of the new Multis, in case we end up using that.  (We
+            * might still decide to use only an update Xid and not a multi,
+            * but it's easier to maintain the list as we walk the old members
+            * list.)
+            *
+            * It is possible to end up with a very old updater Xid that
+            * crashed and thus did not mark itself as aborted in pg_clog.
+            * That would manifest as a pre-cutoff Xid.  Make sure to ignore
+            * it.
+            */
+           if (TransactionIdIsValid(update_xid))
+           {
+               if (!TransactionIdPrecedes(update_xid, cutoff_xid))
+               {
+                   newmembers[nnewmembers++] = members[i];
+               }
+               else
+               {
+                   /* cannot have committed: would be HEAPTUPLE_DEAD */
+                   Assert(!TransactionIdDidCommit(update_xid));
+                   update_xid = InvalidTransactionId;
+                   update_committed = false;
+               }
+           }
+       }
+       else
+       {
+           /* We only keep lockers if they are still running */
+           if (TransactionIdIsCurrentTransactionId(members[i].xid) ||
+               TransactionIdIsInProgress(members[i].xid))
+           {
+               /* running locker cannot possibly be older than the cutoff */
+               Assert(!TransactionIdPrecedes(members[i].xid, cutoff_xid));
+               newmembers[nnewmembers++] = members[i];
+               has_lockers = true;
+           }
+       }
+   }
+
+   pfree(members);
+
+   if (nnewmembers == 0)
+   {
+       /* nothing worth keeping!? Tell caller to remove the whole thing */
+       *flags |= FRM_INVALIDATE_XMAX;
+       xid = InvalidTransactionId;
+   }
+   else if (TransactionIdIsValid(update_xid) && !has_lockers)
+   {
+       /*
+        * If there's a single member and it's an update, pass it back alone
+        * without creating a new Multi.  (XXX we could do this when there's a
+        * single remaining locker, too, but that would complicate the API too
+        * much; moreover, the case with the single updater is more
+        * interesting, because those are longer-lived.)
+        */
+       Assert(nnewmembers == 1);
+       *flags |= FRM_RETURN_IS_XID;
+       if (update_committed)
+           *flags |= FRM_MARK_COMMITTED;
+       xid = update_xid;
+   }
+   else
+   {
+       /*
+        * Create a new multixact with the surviving members of the previous
+        * one, to set as new Xmax in the tuple.
+        */
+       xid = MultiXactIdCreateFromMembers(nnewmembers, newmembers);
+       *flags |= FRM_RETURN_IS_MULTI;
+   }
+
+   pfree(newmembers);
+
+   return xid;
+}
+
+/*
+ * heap_prepare_freeze_tuple
  *
  * Check to see whether any of the XID fields of a tuple (xmin, xmax, xvac)
- * are older than the specified cutoff XID.  If so, replace them with
- * FrozenTransactionId or InvalidTransactionId as appropriate, and return
- * TRUE.  Return FALSE if nothing was changed.
+ * are older than the specified cutoff XID and cutoff MultiXactId. If so,
+ * setup enough state (in the *frz output argument) to later execute and
+ * WAL-log what we would need to do, and return TRUE.  Return FALSE if nothing
+ * is to be changed.
+ *
+ * Caller is responsible for setting the offset field, if appropriate.
  *
  * It is assumed that the caller has checked the tuple with
  * HeapTupleSatisfiesVacuum() and determined that it is not HEAPTUPLE_DEAD
  * NB: cutoff_xid *must* be <= the current global xmin, to ensure that any
  * XID older than it could neither be running nor seen as running by any
  * open transaction.  This ensures that the replacement will not change
- * anyone's idea of the tuple state.  Also, since we assume the tuple is
- * not HEAPTUPLE_DEAD, the fact that an XID is not still running allows us
- * to assume that it is either committed good or aborted, as appropriate;
- * so we need no external state checks to decide what to do.  (This is good
- * because this function is applied during WAL recovery, when we don't have
- * access to any such state, and can't depend on the hint bits to be set.)
- * There is an exception we make which is to assume GetMultiXactIdMembers can
- * be called during recovery.
- *
+ * anyone's idea of the tuple state.
  * Similarly, cutoff_multi must be less than or equal to the smallest
  * MultiXactId used by any transaction currently open.
  *
  * If the tuple is in a shared buffer, caller must hold an exclusive lock on
  * that buffer.
  *
- * Note: it might seem we could make the changes without exclusive lock, since
- * TransactionId read/write is assumed atomic anyway.  However there is a race
- * condition: someone who just fetched an old XID that we overwrite here could
- * conceivably not finish checking the XID against pg_clog before we finish
- * the VACUUM and perhaps truncate off the part of pg_clog he needs.  Getting
- * exclusive lock ensures no other backend is in process of checking the
- * tuple status.  Also, getting exclusive lock makes it safe to adjust the
- * infomask bits.
- *
- * NB: Cannot rely on hint bits here, they might not be set after a crash or
- * on a standby.
+ * NB: It is not enough to set hint bits to indicate something is
+ * committed/invalid -- they might not be set on a standby, or after crash
+ * recovery.  We really need to remove old xids.
  */
 bool
-heap_freeze_tuple(HeapTupleHeader tuple, TransactionId cutoff_xid,
-                 MultiXactId cutoff_multi)
+heap_prepare_freeze_tuple(HeapTupleHeader tuple, TransactionId cutoff_xid,
+                         TransactionId cutoff_multi,
+                         xl_heap_freeze_tuple *frz)
+
 {
    bool        changed = false;
    bool        freeze_xmax = false;
    TransactionId xid;
 
+   frz->frzflags = 0;
+   frz->t_infomask2 = tuple->t_infomask2;
+   frz->t_infomask = tuple->t_infomask;
+   frz->xmax = HeapTupleHeaderGetRawXmax(tuple);
+
    /* Process xmin */
    xid = HeapTupleHeaderGetXmin(tuple);
    if (TransactionIdIsNormal(xid) &&
        TransactionIdPrecedes(xid, cutoff_xid))
    {
-       HeapTupleHeaderSetXmin(tuple, FrozenTransactionId);
+       frz->frzflags |= XLH_FREEZE_XMIN;
 
        /*
         * Might as well fix the hint bits too; usually XMIN_COMMITTED will
         * already be set here, but there's a small chance not.
         */
-       Assert(!(tuple->t_infomask & HEAP_XMIN_INVALID));
-       tuple->t_infomask |= HEAP_XMIN_COMMITTED;
+       frz->t_infomask |= HEAP_XMIN_COMMITTED;
        changed = true;
    }
 
 
    if (tuple->t_infomask & HEAP_XMAX_IS_MULTI)
    {
-       if (!MultiXactIdIsValid(xid))
+       TransactionId newxmax;
+       uint16      flags;
+
+       newxmax = FreezeMultiXactId(xid, tuple->t_infomask,
+                                   cutoff_xid, cutoff_multi, &flags);
+
+       if (flags & FRM_INVALIDATE_XMAX)
+           freeze_xmax = true;
+       else if (flags & FRM_RETURN_IS_XID)
        {
-           /* no xmax set, ignore */
-           ;
+           /*
+            * NB -- some of these transformations are only valid because
+            * we know the return Xid is a tuple updater (i.e. not merely a
+            * locker.) Also note that the only reason we don't explicitely
+            * worry about HEAP_KEYS_UPDATED is because it lives in t_infomask2
+            * rather than t_infomask.
+            */
+           frz->t_infomask &= ~HEAP_XMAX_BITS;
+           frz->xmax = newxmax;
+           if (flags & FRM_MARK_COMMITTED)
+               frz->t_infomask &= HEAP_XMAX_COMMITTED;
+           changed = true;
        }
-       else if (MultiXactIdPrecedes(xid, cutoff_multi))
+       else if (flags & FRM_RETURN_IS_MULTI)
        {
+           uint16  newbits;
+           uint16  newbits2;
+
            /*
-            * This old multi cannot possibly be running.  If it was a locker
-            * only, it can be removed without much further thought; but if it
-            * contained an update, we need to preserve it.
+            * We can't use GetMultiXactIdHintBits directly on the new multi
+            * here; that routine initializes the masks to all zeroes, which
+            * would lose other bits we need.  Doing it this way ensures all
+            * unrelated bits remain untouched.
             */
-           if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
-               freeze_xmax = true;
-           else
-           {
-               TransactionId update_xid;
+           frz->t_infomask &= ~HEAP_XMAX_BITS;
+           frz->t_infomask2 &= ~HEAP_KEYS_UPDATED;
+           GetMultiXactIdHintBits(newxmax, &newbits, &newbits2);
+           frz->t_infomask |= newbits;
+           frz->t_infomask2 |= newbits2;
 
-               update_xid = HeapTupleGetUpdateXid(tuple);
+           frz->xmax = newxmax;
 
-               /*
-                * The multixact has an update hidden within.  Get rid of it.
-                *
-                * If the update_xid is below the cutoff_xid, it necessarily
-                * must be an aborted transaction.  In a primary server, such
-                * an Xmax would have gotten marked invalid by
-                * HeapTupleSatisfiesVacuum, but in a replica that is not
-                * called before we are, so deal with it in the same way.
-                *
-                * If not below the cutoff_xid, then the tuple would have been
-                * pruned by vacuum, if the update committed long enough ago,
-                * and we wouldn't be freezing it; so it's either recently
-                * committed, or in-progress.  Deal with this by setting the
-                * Xmax to the update Xid directly and remove the IS_MULTI
-                * bit.  (We know there cannot be running lockers in this
-                * multi, because it's below the cutoff_multi value.)
-                */
-
-               if (TransactionIdPrecedes(update_xid, cutoff_xid))
-               {
-                   Assert(InRecovery || TransactionIdDidAbort(update_xid));
-                   freeze_xmax = true;
-               }
-               else
-               {
-                   Assert(InRecovery || !TransactionIdIsInProgress(update_xid));
-                   tuple->t_infomask &= ~HEAP_XMAX_BITS;
-                   HeapTupleHeaderSetXmax(tuple, update_xid);
-                   changed = true;
-               }
-           }
-       }
-       else if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
-       {
-           /* newer than the cutoff, so don't touch it */
-           ;
+           changed = true;
        }
        else
        {
-           TransactionId   update_xid;
-
-           /*
-            * This is a multixact which is not marked LOCK_ONLY, but which
-            * is newer than the cutoff_multi.  If the update_xid is below the
-            * cutoff_xid point, then we can just freeze the Xmax in the
-            * tuple, removing it altogether.  This seems simple, but there
-            * are several underlying assumptions:
-            *
-            * 1. A tuple marked by an multixact containing a very old
-            * committed update Xid would have been pruned away by vacuum; we
-            * wouldn't be freezing this tuple at all.
-            *
-            * 2. There cannot possibly be any live locking members remaining
-            * in the multixact.  This is because if they were alive, the
-            * update's Xid would had been considered, via the lockers'
-            * snapshot's Xmin, as part the cutoff_xid.
-            *
-            * 3. We don't create new MultiXacts via MultiXactIdExpand() that
-            * include a very old aborted update Xid: in that function we only
-            * include update Xids corresponding to transactions that are
-            * committed or in-progress.
-            */
-           update_xid = HeapTupleGetUpdateXid(tuple);
-           if (TransactionIdPrecedes(update_xid, cutoff_xid))
-               freeze_xmax = true;
+           Assert(flags & FRM_NOOP);
        }
    }
    else if (TransactionIdIsNormal(xid) &&
 
    if (freeze_xmax)
    {
-       HeapTupleHeaderSetXmax(tuple, InvalidTransactionId);
+       frz->xmax = InvalidTransactionId;
 
        /*
         * The tuple might be marked either XMAX_INVALID or XMAX_COMMITTED +
         * LOCKED.  Normalize to INVALID just to be sure no one gets confused.
         * Also get rid of the HEAP_KEYS_UPDATED bit.
         */
-       tuple->t_infomask &= ~HEAP_XMAX_BITS;
-       tuple->t_infomask |= HEAP_XMAX_INVALID;
-       HeapTupleHeaderClearHotUpdated(tuple);
-       tuple->t_infomask2 &= ~HEAP_KEYS_UPDATED;
+       frz->t_infomask &= ~HEAP_XMAX_BITS;
+       frz->t_infomask |= HEAP_XMAX_INVALID;
+       frz->t_infomask2 &= ~HEAP_HOT_UPDATED;
+       frz->t_infomask2 &= ~HEAP_KEYS_UPDATED;
        changed = true;
    }
 
             * xvac transaction succeeded.
             */
            if (tuple->t_infomask & HEAP_MOVED_OFF)
-               HeapTupleHeaderSetXvac(tuple, InvalidTransactionId);
+               frz->frzflags |= XLH_INVALID_XVAC;
            else
-               HeapTupleHeaderSetXvac(tuple, FrozenTransactionId);
+               frz->frzflags |= XLH_FREEZE_XVAC;
 
            /*
             * Might as well fix the hint bits too; usually XMIN_COMMITTED
             * will already be set here, but there's a small chance not.
             */
            Assert(!(tuple->t_infomask & HEAP_XMIN_INVALID));
-           tuple->t_infomask |= HEAP_XMIN_COMMITTED;
+           frz->t_infomask |= HEAP_XMIN_COMMITTED;
            changed = true;
        }
    }
    return changed;
 }
 
+/*
+ * heap_execute_freeze_tuple
+ *     Execute the prepared freezing of a tuple.
+ *
+ * Caller is responsible for ensuring that no other backend can access the
+ * storage underlying this tuple, either by holding an exclusive lock on the
+ * buffer containing it (which is what lazy VACUUM does), or by having it by
+ * in private storage (which is what CLUSTER and friends do).
+ *
+ * Note: it might seem we could make the changes without exclusive lock, since
+ * TransactionId read/write is assumed atomic anyway.  However there is a race
+ * condition: someone who just fetched an old XID that we overwrite here could
+ * conceivably not finish checking the XID against pg_clog before we finish
+ * the VACUUM and perhaps truncate off the part of pg_clog he needs.  Getting
+ * exclusive lock ensures no other backend is in process of checking the
+ * tuple status.  Also, getting exclusive lock makes it safe to adjust the
+ * infomask bits.
+ *
+ * NB: All code in here must be safe to execute during crash recovery!
+ */
+void
+heap_execute_freeze_tuple(HeapTupleHeader tuple, xl_heap_freeze_tuple *frz)
+{
+   if (frz->frzflags & XLH_FREEZE_XMIN)
+       HeapTupleHeaderSetXmin(tuple, FrozenTransactionId);
+
+   HeapTupleHeaderSetXmax(tuple, frz->xmax);
+
+   if (frz->frzflags & XLH_FREEZE_XVAC)
+       HeapTupleHeaderSetXvac(tuple, FrozenTransactionId);
+
+   if (frz->frzflags & XLH_INVALID_XVAC)
+       HeapTupleHeaderSetXvac(tuple, InvalidTransactionId);
+
+   tuple->t_infomask = frz->t_infomask;
+   tuple->t_infomask2 = frz->t_infomask2;
+}
+
+/*
+ * heap_freeze_tuple
+ *     Freeze tuple in place, without WAL logging.
+ *
+ * Useful for callers like CLUSTER that perform their own WAL logging.
+ */
+bool
+heap_freeze_tuple(HeapTupleHeader tuple, TransactionId cutoff_xid,
+                 TransactionId cutoff_multi)
+{
+   xl_heap_freeze_tuple frz;
+   bool        do_freeze;
+
+   do_freeze = heap_prepare_freeze_tuple(tuple, cutoff_xid, cutoff_multi,
+                                         &frz);
+
+   /*
+    * Note that because this is not a WAL-logged operation, we don't need to
+    * fill in the offset in the freeze record.
+    */
+
+   if (do_freeze)
+       heap_execute_freeze_tuple(tuple, &frz);
+   return do_freeze;
+}
+
 /*
  * For a given MultiXactId, return the hint bits that should be set in the
  * tuple's infomask.
        }
        else if (MultiXactIdPrecedes(multi, cutoff_multi))
            return true;
-       else if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
-       {
-           /* only-locker multis don't need internal examination */
-           ;
-       }
        else
        {
-           if (TransactionIdPrecedes(HeapTupleGetUpdateXid(tuple),
-                                     cutoff_xid))
-               return true;
+           MultiXactMember *members;
+           int         nmembers;
+           int         i;
+
+           /* need to check whether any member of the mxact is too old */
+
+           nmembers = GetMultiXactIdMembers(multi, &members, false);
+
+           for (i = 0; i < nmembers; i++)
+           {
+               if (TransactionIdPrecedes(members[i].xid, cutoff_xid))
+               {
+                   pfree(members);
+                   return true;
+               }
+           }
+           if (nmembers > 0)
+               pfree(members);
        }
    }
    else
 }
 
 /*
- * Perform XLogInsert for a heap-freeze operation. Caller must already
- * have modified the buffer and marked it dirty.
+ * Perform XLogInsert for a heap-freeze operation. Caller must have already
+ * modified the buffer and marked it dirty.
  */
 XLogRecPtr
-log_heap_freeze(Relation reln, Buffer buffer,
-               TransactionId cutoff_xid, MultiXactId cutoff_multi,
-               OffsetNumber *offsets, int offcnt)
+log_heap_freeze(Relation reln, Buffer buffer, TransactionId cutoff_xid,
+               xl_heap_freeze_tuple *tuples, int ntuples)
 {
-   xl_heap_freeze xlrec;
+   xl_heap_freeze_page xlrec;
    XLogRecPtr  recptr;
    XLogRecData rdata[2];
 
    /* Caller should not call me on a non-WAL-logged relation */
    Assert(RelationNeedsWAL(reln));
    /* nor when there are no tuples to freeze */
-   Assert(offcnt > 0);
+   Assert(ntuples > 0);
 
    xlrec.node = reln->rd_node;
    xlrec.block = BufferGetBlockNumber(buffer);
    xlrec.cutoff_xid = cutoff_xid;
-   xlrec.cutoff_multi = cutoff_multi;
+   xlrec.ntuples = ntuples;
 
    rdata[0].data = (char *) &xlrec;
-   rdata[0].len = SizeOfHeapFreeze;
+   rdata[0].len = SizeOfHeapFreezePage;
    rdata[0].buffer = InvalidBuffer;
    rdata[0].next = &(rdata[1]);
 
    /*
-    * The tuple-offsets array is not actually in the buffer, but pretend that
-    * it is.  When XLogInsert stores the whole buffer, the offsets array need
+    * The freeze plan array is not actually in the buffer, but pretend that
+    * it is.  When XLogInsert stores the whole buffer, the freeze plan need
     * not be stored too.
     */
-   rdata[1].data = (char *) offsets;
-   rdata[1].len = offcnt * sizeof(OffsetNumber);
+   rdata[1].data = (char *) tuples;
+   rdata[1].len = ntuples * sizeof(xl_heap_freeze_tuple);
    rdata[1].buffer = buffer;
    rdata[1].buffer_std = true;
    rdata[1].next = NULL;
 
-   recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_FREEZE, rdata);
+   recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_FREEZE_PAGE, rdata);
 
    return recptr;
 }
    XLogRecordPageWithFreeSpace(xlrec->node, xlrec->block, freespace);
 }
 
-static void
-heap_xlog_freeze(XLogRecPtr lsn, XLogRecord *record)
-{
-   xl_heap_freeze *xlrec = (xl_heap_freeze *) XLogRecGetData(record);
-   TransactionId cutoff_xid = xlrec->cutoff_xid;
-   MultiXactId cutoff_multi = xlrec->cutoff_multi;
-   Buffer      buffer;
-   Page        page;
-
-   /*
-    * In Hot Standby mode, ensure that there's no queries running which still
-    * consider the frozen xids as running.
-    */
-   if (InHotStandby)
-       ResolveRecoveryConflictWithSnapshot(cutoff_xid, xlrec->node);
-
-   /* If we have a full-page image, restore it and we're done */
-   if (record->xl_info & XLR_BKP_BLOCK(0))
-   {
-       (void) RestoreBackupBlock(lsn, record, 0, false, false);
-       return;
-   }
-
-   buffer = XLogReadBuffer(xlrec->node, xlrec->block, false);
-   if (!BufferIsValid(buffer))
-       return;
-   page = (Page) BufferGetPage(buffer);
-
-   if (lsn <= PageGetLSN(page))
-   {
-       UnlockReleaseBuffer(buffer);
-       return;
-   }
-
-   if (record->xl_len > SizeOfHeapFreeze)
-   {
-       OffsetNumber *offsets;
-       OffsetNumber *offsets_end;
-
-       offsets = (OffsetNumber *) ((char *) xlrec + SizeOfHeapFreeze);
-       offsets_end = (OffsetNumber *) ((char *) xlrec + record->xl_len);
-
-       while (offsets < offsets_end)
-       {
-           /* offsets[] entries are one-based */
-           ItemId      lp = PageGetItemId(page, *offsets);
-           HeapTupleHeader tuple = (HeapTupleHeader) PageGetItem(page, lp);
-
-           (void) heap_freeze_tuple(tuple, cutoff_xid, cutoff_multi);
-           offsets++;
-       }
-   }
-
-   PageSetLSN(page, lsn);
-   MarkBufferDirty(buffer);
-   UnlockReleaseBuffer(buffer);
-}
-
 /*
  * Replay XLOG_HEAP2_VISIBLE record.
  *
    }
 }
 
+/*
+ * Replay XLOG_HEAP2_FREEZE_PAGE records
+ */
+static void
+heap_xlog_freeze_page(XLogRecPtr lsn, XLogRecord *record)
+{
+   xl_heap_freeze_page *xlrec = (xl_heap_freeze_page *) XLogRecGetData(record);
+   TransactionId cutoff_xid = xlrec->cutoff_xid;
+   Buffer      buffer;
+   Page        page;
+   int         ntup;
+
+   /*
+    * In Hot Standby mode, ensure that there's no queries running which still
+    * consider the frozen xids as running.
+    */
+   if (InHotStandby)
+       ResolveRecoveryConflictWithSnapshot(cutoff_xid, xlrec->node);
+
+   /* If we have a full-page image, restore it and we're done */
+   if (record->xl_info & XLR_BKP_BLOCK(0))
+   {
+       (void) RestoreBackupBlock(lsn, record, 0, false, false);
+       return;
+   }
+
+   buffer = XLogReadBuffer(xlrec->node, xlrec->block, false);
+   if (!BufferIsValid(buffer))
+       return;
+
+   page = (Page) BufferGetPage(buffer);
+
+   if (lsn <= PageGetLSN(page))
+   {
+       UnlockReleaseBuffer(buffer);
+       return;
+   }
+
+   /* now execute freeze plan for each frozen tuple */
+   for (ntup = 0; ntup < xlrec->ntuples; ntup++)
+   {
+       xl_heap_freeze_tuple *xlrec_tp;
+       ItemId      lp;
+       HeapTupleHeader tuple;
+
+       xlrec_tp = &xlrec->tuples[ntup];
+       lp = PageGetItemId(page, xlrec_tp->offset);     /* offsets are one-based */
+       tuple = (HeapTupleHeader) PageGetItem(page, lp);
+
+       heap_execute_freeze_tuple(tuple, xlrec_tp);
+   }
+
+   PageSetLSN(page, lsn);
+   MarkBufferDirty(buffer);
+   UnlockReleaseBuffer(buffer);
+}
+
 static void
 heap_xlog_newpage(XLogRecPtr lsn, XLogRecord *record)
 {
 
    switch (info & XLOG_HEAP_OPMASK)
    {
-       case XLOG_HEAP2_FREEZE:
-           heap_xlog_freeze(lsn, record);
-           break;
        case XLOG_HEAP2_CLEAN:
            heap_xlog_clean(lsn, record);
            break;
+       case XLOG_HEAP2_FREEZE_PAGE:
+           heap_xlog_freeze_page(lsn, record);
+           break;
        case XLOG_HEAP2_CLEANUP_INFO:
            heap_xlog_cleanup_info(lsn, record);
            break;