From 95d94ee991ea163b4b7861a193b3a1a3497de73e Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Sat, 27 Sep 2025 11:54:38 -0400 Subject: [PATCH v16 07/14] Eliminate XLOG_HEAP2_VISIBLE from vacuum phase III Instead of emitting a separate XLOG_HEAP2_VISIBLE record for each page that becomes all-visible in vacuum's third phase, record the visibility map update in the already emitted XLOG_HEAP2_PRUNE_VACUUM_CLEANUP record. Visibility checks are now performed before marking dead items unused. This is safe because the heap page is held under exclusive lock for the entire operation. This reduces the number of WAL records generated by VACUUM phase III by up to 50%. Author: Melanie Plageman Reviewed-by: Robert Haas Reviewed-by: Kirill Reshke Reviewed-by: Andres Freund Discussion: https://postgr.es/m/flat/CAAKRu_ZMw6Npd_qm2KM%2BFwQ3cMOMx1Dh3VMhp8-V7SOLxdK9-g%40mail.gmail.com --- src/backend/access/heap/vacuumlazy.c | 174 +++++++++++++++++++-------- 1 file changed, 124 insertions(+), 50 deletions(-) diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 39526bf608f..cf1c2efc999 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -463,6 +463,13 @@ static void dead_items_add(LVRelState *vacrel, BlockNumber blkno, OffsetNumber * int num_offsets); 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, + OffsetNumber *deadoffsets, + int ndeadoffsets, + bool *all_frozen, + TransactionId *visibility_cutoff_xid, + OffsetNumber *logging_offnum); static void update_relstats_all_indexes(LVRelState *vacrel); static void vacuum_error_callback(void *arg); static void update_vacuum_error_info(LVRelState *vacrel, @@ -2685,8 +2692,10 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, OffsetNumber unused[MaxHeapTuplesPerPage]; int nunused = 0; TransactionId visibility_cutoff_xid; + TransactionId conflict_xid = InvalidTransactionId; bool all_frozen; LVSavedErrInfo saved_err_info; + uint8 vmflags = 0; Assert(vacrel->do_index_vacuuming); @@ -2697,6 +2706,31 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, VACUUM_ERRCB_PHASE_VACUUM_HEAP, blkno, InvalidOffsetNumber); + /* + * Before marking dead items unused, check whether the page will become + * all-visible once that change is applied. This lets us reap the tuples + * and mark the page all-visible within the same critical section, + * enabling both changes to be emitted in a single WAL record. Since the + * visibility checks may perform I/O and allocate memory, they must be + * done outside the critical section. + */ + if (heap_page_would_be_all_visible(vacrel->rel, buffer, + vacrel->cutoffs.OldestXmin, + deadoffsets, num_offsets, + &all_frozen, &visibility_cutoff_xid, + &vacrel->offnum)) + { + vmflags |= VISIBILITYMAP_ALL_VISIBLE; + if (all_frozen) + { + vmflags |= VISIBILITYMAP_ALL_FROZEN; + Assert(!TransactionIdIsValid(visibility_cutoff_xid)); + } + + /* Take the lock on the vmbuffer before entering a critical section */ + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + } + START_CRIT_SECTION(); for (int i = 0; i < num_offsets; i++) @@ -2716,6 +2750,21 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, /* Attempt to truncate line pointer array now */ PageTruncateLinePointerArray(page); + /* + * The page is guaranteed to have had dead line pointers, so + * PD_ALL_VISIBLE cannot be already set. Therefore, whenever we set the VM + * bit, we must also set PD_ALL_VISIBLE. The heap page lock is held while + * updating the VM to ensure consistency. + */ + if ((vmflags & VISIBILITYMAP_VALID_BITS) != 0) + { + PageSetAllVisible(page); + visibilitymap_set_vmbits(blkno, + vmbuffer, vmflags, + RelationGetRelationName(vacrel->rel)); + conflict_xid = visibility_cutoff_xid; + } + /* * Mark buffer dirty before we write WAL. */ @@ -2725,11 +2774,10 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, if (RelationNeedsWAL(vacrel->rel)) { log_heap_prune_and_freeze(vacrel->rel, buffer, - InvalidBuffer, /* vmbuffer */ - 0, /* vmflags */ - InvalidTransactionId, /* conflict_xid */ + vmbuffer, vmflags, + conflict_xid, false, /* no cleanup lock required */ - false, /* set_pd_all_vis */ + (vmflags & VISIBILITYMAP_VALID_BITS) != 0, PRUNE_VACUUM_CLEANUP, NULL, 0, /* frozen */ NULL, 0, /* redirected */ @@ -2737,41 +2785,12 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, unused, nunused); } - /* - * End critical section, so we safely can do visibility tests (which - * possibly need to perform IO and allocate memory!). If we crash now the - * page (including the corresponding vm bit) might not be marked all - * visible, but that's fine. A later vacuum will fix that. - */ END_CRIT_SECTION(); - /* - * Now that we have removed the LP_DEAD items from the page, once again - * check if the page has become all-visible. The page is already marked - * dirty, exclusively locked, and, if needed, a full page image has been - * emitted. - */ - Assert(!PageIsAllVisible(page)); - if (heap_page_is_all_visible(vacrel->rel, buffer, vacrel->cutoffs.OldestXmin, - &all_frozen, - &visibility_cutoff_xid, - &vacrel->offnum)) + if ((vmflags & VISIBILITYMAP_ALL_VISIBLE) != 0) { - uint8 flags = VISIBILITYMAP_ALL_VISIBLE; - - if (all_frozen) - { - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); - flags |= VISIBILITYMAP_ALL_FROZEN; - } - - PageSetAllVisible(page); - visibilitymap_set(vacrel->rel, blkno, buffer, - InvalidXLogRecPtr, - vmbuffer, visibility_cutoff_xid, - flags); - /* Count the newly set VM page for logging */ + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); vacrel->vm_new_visible_pages++; if (all_frozen) vacrel->vm_new_visible_frozen_pages++; @@ -3440,18 +3459,8 @@ dead_items_cleanup(LVRelState *vacrel) } /* - * Check if every tuple in the given page is visible to all current and future - * transactions. Also return the visibility_cutoff_xid which is the highest - * xmin amongst the visible tuples. Set *all_frozen to true if every tuple - * on this page is frozen. - * - * *logging_offnum will have the OffsetNumber of the current tuple being - * processed for vacuum's error callback system. - * - * This is similar logic to that in heap_prune_record_unchanged_lp_normal() If - * you change anything here, make sure that everything stays in sync. Note - * that an assertion calls us to verify that everybody still agrees. Be sure - * to avoid introducing new side-effects here. + * Wrapper for heap_page_would_be_all_visible() which can be used for + * callers that expect no LP_DEAD on the page. */ bool heap_page_is_all_visible(Relation rel, Buffer buf, @@ -3460,15 +3469,74 @@ heap_page_is_all_visible(Relation rel, Buffer buf, TransactionId *visibility_cutoff_xid, OffsetNumber *logging_offnum) { + + return heap_page_would_be_all_visible(rel, buf, + OldestXmin, + NULL, 0, + all_frozen, + visibility_cutoff_xid, + logging_offnum); +} + +/* + * Check whether the heap page in buf is all-visible except for the dead + * tuples referenced in the deadoffsets array. + * + * The visibility checks may perform IO and allocate memory so they must not + * be done in a critical section. This function is used by vacuum to determine + * if the page will be all-visible once it reaps known dead tuples. That way + * it can do both in the same critical section and emit a single WAL record. + * + * Returns true if the page is all-visible other than the provided + * deadoffsets and false otherwise. + * + * OldestXmin is used to determine visibility. + * + * Output parameters: + * + * - *all_frozen: true if every tuple on the page is frozen + * - *visibility_cutoff_xid: newest xmin; valid only if page is all-visible + * - *logging_offnum: OffsetNumber of current tuple being processed; + * used by vacuum's error callback system. + * + * Callers looking to verify that the page is already all-visible can call + * heap_page_is_all_visible(). + * + * This logic is closely related to heap_prune_record_unchanged_lp_normal(). + * If you modify this function, ensure consistency with that code. An + * assertion cross-checks that both remain in agreement. Do not introduce new + * side-effects. + */ +static bool +heap_page_would_be_all_visible(Relation rel, Buffer buf, + TransactionId OldestXmin, + OffsetNumber *deadoffsets, + int ndeadoffsets, + bool *all_frozen, + TransactionId *visibility_cutoff_xid, + OffsetNumber *logging_offnum) +{ Page page = BufferGetPage(buf); BlockNumber blockno = BufferGetBlockNumber(buf); OffsetNumber offnum, maxoff; bool all_visible = true; + int matched_dead_count = 0; *visibility_cutoff_xid = InvalidTransactionId; *all_frozen = true; + Assert(ndeadoffsets == 0 || deadoffsets); + +#ifdef USE_ASSERT_CHECKING + /* Confirm input deadoffsets[] is strictly sorted */ + if (ndeadoffsets > 1) + { + for (int i = 1; i < ndeadoffsets; i++) + Assert(deadoffsets[i - 1] < deadoffsets[i]); + } +#endif + maxoff = PageGetMaxOffsetNumber(page); for (offnum = FirstOffsetNumber; offnum <= maxoff && all_visible; @@ -3496,9 +3564,15 @@ heap_page_is_all_visible(Relation rel, Buffer buf, */ if (ItemIdIsDead(itemid)) { - all_visible = false; - *all_frozen = false; - break; + if (!deadoffsets || + matched_dead_count >= ndeadoffsets || + deadoffsets[matched_dead_count] != offnum) + { + *all_frozen = all_visible = false; + break; + } + matched_dead_count++; + continue; } Assert(ItemIdIsNormal(itemid)); -- 2.43.0