From a28aef72286f446c53614621ebe7f8b65ee4b59b Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Tue, 29 Jul 2025 14:38:24 -0400 Subject: [PATCH v16 11/14] Use GlobalVisState in vacuum to determine page level visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During vacuum's first and third phases, we examine tuples' visibility to determine if we can set the page all-visible in the visibility map. Previously, this check compared tuple xmins against a single XID chosen at the start of vacuum (OldestXmin). We now use GlobalVisState, which also enables future work to set the VM during on-access pruning, since ordinary queries have access to GlobalVisState but not OldestXmin. This also benefits vacuum directly: GlobalVisState may advance during a vacuum, allowing more pages to become considered all-visible. In the rare case that it moves backward, VACUUM falls back to OldestXmin to ensure we don’t attempt to freeze a dead tuple that wasn’t yet prunable according to the GlobalVisState. Because comparing a transaction ID against GlobalVisState is more expensive than comparing against a single XID, we defer this check until after scanning all tuples on the page. If visibility_cutoff_xid was maintained, we perform the GlobalVisState check only once per page. This is safe because visibility_cutoff_xid records the newest xmin on the page; if it is globally visible, then the entire page is all-visible. This approach may result in examining more tuple xmins than before, since with OldestXmin we could sometimes rule out the page being all-visible earlier. However, profiling shows the additional cost is not significant. --- src/backend/access/heap/heapam_visibility.c | 28 ++++++++++++++++ src/backend/access/heap/pruneheap.c | 37 ++++++++++----------- src/backend/access/heap/vacuumlazy.c | 17 +++++----- src/include/access/heapam.h | 7 ++-- 4 files changed, 57 insertions(+), 32 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 78e04f1d17c..e5b16bd2b38 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -711,11 +711,12 @@ heap_page_prune_and_freeze(PruneFreezeParams *params, /* * 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 - * when updating the VM and/or freezing all the tuples on the page. + * on the page older than the visibility horizon represented in the + * GlobalVisState. 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 when updating the VM + * and/or freezing all the tuples on the page. */ prstate.visibility_cutoff_xid = InvalidTransactionId; @@ -911,6 +912,16 @@ heap_page_prune_and_freeze(PruneFreezeParams *params, 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 the hint @@ -1081,10 +1092,9 @@ heap_page_prune_and_freeze(PruneFreezeParams *params, bool debug_all_frozen; Assert(prstate.lpdead_items == 0); - Assert(prstate.cutoffs); if (!heap_page_is_all_visible(params->relation, buffer, - prstate.cutoffs->OldestXmin, + prstate.vistest, &debug_all_frozen, &debug_cutoff, off_loc)) Assert(false); @@ -1613,19 +1623,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 GlobalVisXidVisibleToAll() 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 bed77af23a2..3af8a359e42 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, @@ -2739,7 +2739,7 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, * done outside the critical section. */ 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)) @@ -3488,14 +3488,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, @@ -3514,7 +3513,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. * * Output parameters: * @@ -3533,7 +3532,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, @@ -3605,7 +3604,7 @@ 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: { @@ -3624,7 +3623,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 2de39ba0cd1..df0632aebc6 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -276,8 +276,7 @@ typedef struct PruneFreezeParams /* * cutoffs contains the freeze cutoffs, established by VACUUM at the * beginning of vacuuming the relation. Required if HEAP_PRUNE_FREEZE - * option is set. cutoffs->OldestXmin is also used to determine if dead - * tuples are HEAPTUPLE_RECENTLY_DEAD or HEAPTUPLE_DEAD. + * option is set. */ struct VacuumCutoffs *cutoffs; } PruneFreezeParams; @@ -443,7 +442,7 @@ extern void heap_vacuum_rel(Relation rel, const VacuumParams params, BufferAccessStrategy bstrategy); 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); @@ -455,6 +454,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