From d301117f52d6a6e78fbdafbbb2c0c4dd62b5b861 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Tue, 29 Jul 2025 14:38:24 -0400 Subject: [PATCH v27 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 | 50 +++++++++++++++++++++ src/backend/access/heap/pruneheap.c | 43 ++++++++---------- src/backend/access/heap/vacuumlazy.c | 10 ++--- src/include/access/heapam.h | 13 +++--- 4 files changed, 82 insertions(+), 34 deletions(-) diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c index 05f6946fe60..6bcd8b6d017 100644 --- a/src/backend/access/heap/heapam_visibility.c +++ b/src/backend/access/heap/heapam_visibility.c @@ -1189,6 +1189,56 @@ HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin, return res; } +/* + * Wrapper around GlobalVisTestIsRemovableXid() for use when examining live + * tuples. Returns true if the given XID is no longer considered running by + * any snapshot. + * + * This function alone is insufficient to determine tuple visibility; callers + * must also consider the tuple’s commit status. Its purpose is purely + * semantic: when applied to live tuples, GlobalVisTestIsRemovableXid() is + * checking whether the inserting transaction is still considered running, + * not whether the tuple is removable. Live tuples are, by definition, not + * removable, but the snapshot criteria for “transaction still running” are + * identical to those used for deletion XIDs. + * + * See the comment above GlobalVisTestIsRemovable[Full]Xid() for details on the + * required preconditions for calling this function. + */ +bool +GlobalVisTestXidNotRunning(GlobalVisState *state, TransactionId xid) +{ + return GlobalVisTestIsRemovableXid(state, xid); +} + +/* + * 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 (GlobalVisTestXidNotRunning(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 8568587af4a..08ffe511d03 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -461,11 +461,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; } @@ -994,14 +995,13 @@ heap_page_will_set_vm(PruneState *prstate, */ 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, @@ -1088,6 +1088,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) && + !GlobalVisTestXidNotRunning(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 @@ -1269,10 +1279,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)); @@ -1801,20 +1810,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 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 = 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 87820f3ff49..3b8c9dbdb4b 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -2730,7 +2730,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)) @@ -3491,7 +3491,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: * @@ -3507,7 +3507,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, @@ -3581,7 +3581,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: { @@ -3600,7 +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 (!GlobalVisTestXidNotRunning(vistest, xmin)) { all_visible = false; *all_frozen = false; diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 9100d42ccbb..e2ee035ae0b 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -272,10 +272,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; @@ -439,7 +438,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, @@ -453,6 +452,10 @@ extern TM_Result HeapTupleSatisfiesUpdate(HeapTuple htup, CommandId curcid, Buffer buffer); extern HTSV_Result HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin, Buffer buffer); + +extern bool GlobalVisTestXidNotRunning(GlobalVisState *state, TransactionId xid); +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