From c75f7d1281fadf5c49e37577ef42ff96b92b3f59 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Tue, 29 Jul 2025 14:38:24 -0400 Subject: [PATCH v23 09/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: 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. Reviewed-by: Andres Freund Reviewed-by: Chao Li --- src/backend/access/heap/heapam_visibility.c | 28 ++++++++++++++ src/backend/access/heap/pruneheap.c | 43 +++++++++------------ src/backend/access/heap/vacuumlazy.c | 10 ++--- src/include/access/heapam.h | 11 +++--- 4 files changed, 58 insertions(+), 34 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 1a3c7cf1ef5..d836bbeaf52 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -450,11 +450,12 @@ prune_freeze_setup(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; } @@ -980,14 +981,13 @@ heap_page_will_set_vis(Relation relation, */ static 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, @@ -1078,6 +1078,16 @@ heap_page_prune_and_freeze(PruneFreezeParams *params, prune_freeze_plan(RelationGetRelid(params->relation), buffer, &prstate, off_loc); + /* + * 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; + /* * If checksums are enabled, calling heap_prune_satisfies_vacuum() while * checking tuple visibility information in prune_freeze_plan() may have @@ -1255,10 +1265,9 @@ heap_page_prune_and_freeze(PruneFreezeParams *params, bool debug_all_frozen; Assert(prstate.lpdead_items == 0); - Assert(prstate.cutoffs); Assert(heap_page_is_all_visible(params->relation, buffer, - prstate.cutoffs->OldestXmin, + prstate.vistest, &debug_all_frozen, &debug_cutoff, off_loc)); @@ -1787,20 +1796,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' and 'all_frozen' 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 = false; - 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 5d88a1592e3..c0e1350cb11 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -2735,7 +2735,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)) @@ -3495,7 +3495,7 @@ dead_items_cleanup(LVRelState *vacrel) * 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: * @@ -3511,7 +3511,7 @@ dead_items_cleanup(LVRelState *vacrel) */ bool heap_page_would_be_all_visible(Relation rel, Buffer buf, - TransactionId OldestXmin, + GlobalVisState *vistest, OffsetNumber *deadoffsets, int ndeadoffsets, bool *all_frozen, @@ -3585,7 +3585,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: { @@ -3604,7 +3604,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 14c1d92604d..4702ec00dea 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -278,10 +278,9 @@ typedef struct PruneFreezeParams /* * Contains the cutoffs used for freezing. They are required if the - * HEAP_PAGE_PRUNE_FREEZE option is set. cutoffs->OldestXmin is also used - * to determine if dead tuples are HEAPTUPLE_RECENTLY_DEAD or - * HEAPTUPLE_DEAD. Currently only vacuum passes in cutoffs. Vacuum - * calculates them once, at the beginning of vacuuming the relation. + * HEAP_PAGE_PRUNE_FREEZE option is set. Currently only vacuum passes in + * cutoffs. Vacuum calculates them once, at the beginning of vacuuming the + * relation. */ struct VacuumCutoffs *cutoffs; } PruneFreezeParams; @@ -446,7 +445,7 @@ extern void log_heap_prune_and_freeze(Relation relation, Buffer buffer, extern void heap_vacuum_rel(Relation rel, const VacuumParams params, BufferAccessStrategy bstrategy); extern bool heap_page_would_be_all_visible(Relation rel, Buffer buf, - TransactionId OldestXmin, + GlobalVisState *vistest, OffsetNumber *deadoffsets, int ndeadoffsets, bool *all_frozen, @@ -460,6 +459,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