From 7e8926fc6256dc0966b0c65e4fcec0031fbd2988 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Tue, 29 Jul 2025 14:38:24 -0400 Subject: [PATCH v19 08/12] 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: in some cases, GlobalVisState may advance during a vacuum, allowing more pages to become considered all-visible. And, in the future, we could easily add a heuristic to update GlobalVisState more frequently during vacuums of large tables. In the rare case that the GlobalVisState 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 d03b754b2cc..d3c57eedfe3 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -712,11 +712,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; @@ -912,6 +913,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 @@ -1084,10 +1095,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); @@ -1615,19 +1625,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 ff6f0d1d0af..5e3c1d50378 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -465,7 +465,7 @@ 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, @@ -2741,7 +2741,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)) @@ -3496,14 +3496,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, @@ -3524,7 +3523,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: * @@ -3543,7 +3542,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, @@ -3617,7 +3616,7 @@ heap_page_would_be_all_visible(Relation rel, Buffer buf, /* Visibility checks may do IO or allocate memory */ Assert(CritSectionCount == 0); - switch (HeapTupleSatisfiesVacuum(&tuple, OldestXmin, buf)) + switch (HeapTupleSatisfiesVacuumGlobalVis(&tuple, vistest, buf)) { case HEAPTUPLE_LIVE: { @@ -3636,7 +3635,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 1471940b4a4..4fc6edf4261 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; @@ -444,7 +443,7 @@ extern void heap_vacuum_rel(Relation rel, #ifdef USE_ASSERT_CHECKING 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); @@ -457,6 +456,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