From 1e836a4bca61d6ab748a9d1c43dfcef6e0b06f81 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Tue, 29 Jul 2025 14:38:24 -0400 Subject: [PATCH v11 14/20] Use GlobalVisState to determine page level visibility During pruning and during vacuum's third phase, we try to determine if the whole page can be set all-visible in the visibility map. Instead of using OldestXmin to determine if all the tuples on a page are visible to everyone, use the GlobalVisState. This allows us to start setting the VM during on-access pruning in a future commit. It is possible for the GlobalVisState to change during the course of a vacuum. In all but extraordinary cases, it moves forward, meaning more pages could potentially be set in the VM. Because comparing a transaction ID to the GlobalVisState requires more operations than comparing it to another single transaction ID, we now wait until after examining all the tuples on the page and if we have maintained the visibility_cutoff_xid, we compare that to the GlobalVisState just once per page. This works because if the page is all-visible and has live, committed tuples on it, the visibility_cutoff_xid will contain the newest xmin on the page. If everyone can see it, the page is truly all-visible. Doing this may mean we examine more tuples' xmins than before, as we may have set all_visible to false sooner when encountering a live tuple newer than OldestXmin. However, these extra comparisons were found not to be significant in a profile. --- src/backend/access/heap/heapam_visibility.c | 28 ++++++++++++ src/backend/access/heap/pruneheap.c | 48 +++++++++------------ src/backend/access/heap/vacuumlazy.c | 19 ++++---- src/include/access/heapam.h | 4 +- 4 files changed, 60 insertions(+), 39 deletions(-) diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c index 4ebc8abdbeb..edd529dc3c0 100644 --- a/src/backend/access/heap/heapam_visibility.c +++ b/src/backend/access/heap/heapam_visibility.c @@ -1189,6 +1189,34 @@ HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin, return res; } +/* + * Nearly the same as HeapTupleSatisfiesVacuum, but uses a GlobalVisState to + * determine whether or not a tuple is HEAPTUPLE_DEAD Or + * HEAPTUPLE_RECENTLY_DEAD. It serves the same purpose but can be used by + * callers that have not calculated a single OldestXmin value. + */ +HTSV_Result +HeapTupleSatisfiesVacuumGlobalVis(HeapTuple htup, GlobalVisState *vistest, + Buffer buffer) +{ + TransactionId dead_after = InvalidTransactionId; + HTSV_Result res; + + res = HeapTupleSatisfiesVacuumHorizon(htup, buffer, &dead_after); + + if (res == HEAPTUPLE_RECENTLY_DEAD) + { + Assert(TransactionIdIsValid(dead_after)); + + if (GlobalVisXidVisibleToAll(vistest, dead_after)) + res = HEAPTUPLE_DEAD; + } + else + Assert(!TransactionIdIsValid(dead_after)); + + return res; +} + /* * Work horse for HeapTupleSatisfiesVacuum and similar routines. * diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 0211effeec7..c6935e45cec 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -141,10 +141,9 @@ typedef struct * all_visible and all_frozen indicate if the all-visible and all-frozen * bits in the visibility map can be set for this page after pruning. * - * visibility_cutoff_xid is the newest xmin of live tuples on the page. - * The caller can use it as the conflict horizon, when setting the VM - * bits. It is only valid if we froze some tuples, and all_frozen is - * true. + * visibility_cutoff_xid is the newest xmin of live tuples on the page. It + * can be used as the conflict horizon, when setting the VM or when + * freezing all the live tuples on the page. * * NOTE: all_visible and all_frozen don't include LP_DEAD items until * directly before updating the VM. We ignore LP_DEAD items when deciding @@ -559,14 +558,12 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, /* * The visibility cutoff xid is the newest xmin of live, committed tuples - * older than OldestXmin on the page. This field is only kept up-to-date - * if the page is all-visible. As soon as a tuple is encountered that is - * not visible to all, this field is unmaintained. As long as it is - * maintained, it can be used to calculate the snapshot conflict horizon. - * This is most likely to happen when updating the VM and/or freezing all - * live tuples on the page. It is updated before returning to the caller - * because vacuum does assert-build only validation on the page using this - * field. + * on the page older than the visibility horizon represented in the + * GlobalVisState. + * + * If we encounter an uncommitted tuple, this field is unmaintained. If + * the page is being set all-visible or when freezing all live tuples on + * the page, it is used to calculate the snapshot conflict horizon. */ prstate.visibility_cutoff_xid = InvalidTransactionId; @@ -762,6 +759,16 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, prstate.ndead > 0 || prstate.nunused > 0; + /* + * After processing all the live tuples on the page, if the newest xmin + * amongst them is not visible to everyone, the page cannot be + * all-visible. + */ + if (prstate.all_visible && + TransactionIdIsNormal(prstate.visibility_cutoff_xid) && + !GlobalVisXidVisibleToAll(prstate.vistest, prstate.visibility_cutoff_xid)) + prstate.all_visible = prstate.all_frozen = false; + /* * Even if we don't prune anything, if we found a new value for the * pd_prune_xid field or the page was marked full, we will update those @@ -1108,12 +1115,10 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, TransactionId debug_cutoff; bool debug_all_frozen; - Assert(cutoffs); - Assert(prstate.lpdead_items == 0); if (!heap_page_is_all_visible(relation, buffer, - cutoffs->OldestXmin, + prstate.vistest, &debug_all_frozen, &debug_cutoff, off_loc)) Assert(false); @@ -1638,19 +1643,6 @@ heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumb */ xmin = HeapTupleHeaderGetXmin(htup); - /* - * For now always use prstate->cutoffs for this test, because - * we only update 'all_visible' when freezing is requested. We - * could use GlobalVisTestIsRemovableXid instead, if a - * non-freezing caller wanted to set the VM bit. - */ - Assert(prstate->cutoffs); - if (!TransactionIdPrecedes(xmin, prstate->cutoffs->OldestXmin)) - { - prstate->all_visible = prstate->all_frozen = false; - break; - } - /* Track newest xmin on page. */ if (TransactionIdFollows(xmin, prstate->visibility_cutoff_xid) && TransactionIdIsNormal(xmin)) diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 2dcca071a45..4ad05ba4db6 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -464,7 +464,7 @@ static void dead_items_add(LVRelState *vacrel, BlockNumber blkno, OffsetNumber * static void dead_items_reset(LVRelState *vacrel); static void dead_items_cleanup(LVRelState *vacrel); static bool heap_page_would_be_all_visible(Relation rel, Buffer buf, - TransactionId OldestXmin, + GlobalVisState *vistest, OffsetNumber *deadoffsets, int ndeadoffsets, bool *all_frozen, @@ -2717,7 +2717,7 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, InvalidOffsetNumber); if (heap_page_would_be_all_visible(vacrel->rel, buffer, - vacrel->cutoffs.OldestXmin, + vacrel->vistest, deadoffsets, num_offsets, &all_frozen, &visibility_cutoff_xid, &vacrel->offnum)) @@ -3462,13 +3462,13 @@ dead_items_cleanup(LVRelState *vacrel) */ bool heap_page_is_all_visible(Relation rel, Buffer buf, - TransactionId OldestXmin, + GlobalVisState *vistest, bool *all_frozen, TransactionId *visibility_cutoff_xid, OffsetNumber *logging_offnum) { - return heap_page_would_be_all_visible(rel, buf, OldestXmin, + return heap_page_would_be_all_visible(rel, buf, vistest, NULL, 0, all_frozen, visibility_cutoff_xid, @@ -3487,7 +3487,7 @@ heap_page_is_all_visible(Relation rel, Buffer buf, * Returns true if the page is all-visible other than the provided * deadoffsets and false otherwise. * - * OldestXmin is used to determine visibility. + * vistest is used to determine visibility. * * *all_frozen is an output parameter indicating to the caller if every tuple * on the page is frozen. @@ -3508,7 +3508,7 @@ heap_page_is_all_visible(Relation rel, Buffer buf, */ static bool heap_page_would_be_all_visible(Relation rel, Buffer buf, - TransactionId OldestXmin, + GlobalVisState *vistest, OffsetNumber *deadoffsets, int ndeadoffsets, bool *all_frozen, @@ -3580,8 +3580,8 @@ heap_page_would_be_all_visible(Relation rel, Buffer buf, tuple.t_len = ItemIdGetLength(itemid); tuple.t_tableOid = RelationGetRelid(rel); - switch (HeapTupleSatisfiesVacuum(&tuple, OldestXmin, - buf)) + switch (HeapTupleSatisfiesVacuumGlobalVis(&tuple, vistest, + buf)) { case HEAPTUPLE_LIVE: { @@ -3600,8 +3600,7 @@ heap_page_would_be_all_visible(Relation rel, Buffer buf, * that everyone sees it as committed? */ xmin = HeapTupleHeaderGetXmin(tuple.t_data); - if (!TransactionIdPrecedes(xmin, - OldestXmin)) + if (!GlobalVisXidVisibleToAll(vistest, xmin)) { all_visible = false; *all_frozen = false; diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 0b9bb1c9b13..4278f351bdf 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -342,7 +342,7 @@ extern void heap_inplace_unlock(Relation relation, HeapTuple oldtup, Buffer buffer); extern bool heap_page_is_all_visible(Relation rel, Buffer buf, - TransactionId OldestXmin, + GlobalVisState *vistest, bool *all_frozen, TransactionId *visibility_cutoff_xid, OffsetNumber *logging_offnum); @@ -415,6 +415,8 @@ extern TM_Result HeapTupleSatisfiesUpdate(HeapTuple htup, CommandId curcid, Buffer buffer); extern HTSV_Result HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin, Buffer buffer); +extern HTSV_Result HeapTupleSatisfiesVacuumGlobalVis(HeapTuple htup, + GlobalVisState *vistest, Buffer buffer); extern HTSV_Result HeapTupleSatisfiesVacuumHorizon(HeapTuple htup, Buffer buffer, TransactionId *dead_after); extern void HeapTupleSetHintBits(HeapTupleHeader tuple, Buffer buffer, -- 2.43.0