From 2b36fd5974ee68291ddfa12fae5bc3337bc199a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=BE=D0=BD=D0=B8=D0=B4=20=D0=AE=D1=80=D1=8C?= =?UTF-8?q?=D0=B5=D0=B2=20=28Leonid=20Yuriev=29?= Date: Sat, 26 Apr 2025 00:15:41 +0300 Subject: [PATCH] =?UTF-8?q?mdbx:=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20GC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mdbx.h | 6 +- src/api-env.c | 2 +- src/api-txn.c | 20 +- src/audit.c | 28 +- src/cursor.c | 3 +- src/dpl.h | 4 +- src/dxb.c | 11 +- src/env.c | 4 +- src/essentials.h | 2 + src/gc-get.c | 242 +++--- src/gc-put.c | 1849 ++++++++++++++++++++++++++++---------------- src/gc.h | 66 +- src/internals.h | 36 +- src/mvcc-readers.c | 4 +- src/pnl.h | 9 +- src/proto.h | 6 +- src/rkl.h | 1 + src/txl.c | 4 +- src/txl.h | 10 +- src/txn-basal.c | 33 +- src/txn-nested.c | 26 +- src/txn.c | 19 +- src/utils.c | 11 + src/utils.h | 2 + 24 files changed, 1493 insertions(+), 905 deletions(-) diff --git a/mdbx.h b/mdbx.h index bbfe68c5..fdfcd04e 100644 --- a/mdbx.h +++ b/mdbx.h @@ -2775,10 +2775,10 @@ typedef struct MDBX_stat MDBX_stat; * Legacy mdbx_env_stat() correspond to calling \ref mdbx_env_stat_ex() with the * null `txn` argument. * - * \param [in] env An environment handle returned by \ref mdbx_env_create() - * \param [in] txn A transaction handle returned by \ref mdbx_txn_begin() + * \param [in] env An environment handle returned by \ref mdbx_env_create(). + * \param [in] txn A transaction handle returned by \ref mdbx_txn_begin(). * \param [out] stat The address of an \ref MDBX_stat structure where - * the statistics will be copied + * the statistics will be copied. * \param [in] bytes The size of \ref MDBX_stat. * * \returns A non-zero error value on failure and 0 on success. */ diff --git a/src/api-env.c b/src/api-env.c index f56e10be..358dc56f 100644 --- a/src/api-env.c +++ b/src/api-env.c @@ -955,7 +955,7 @@ __cold int mdbx_env_set_geometry(MDBX_env *env, intptr_t size_lower, intptr_t si env->basal_txn->wr.troika = meta_tap(env); eASSERT(env, !env->txn && !env->basal_txn->nested); env->basal_txn->txnid = env->basal_txn->wr.troika.txnid[env->basal_txn->wr.troika.recent]; - txn_snapshot_oldest(env->basal_txn); + txn_gc_detent(env->basal_txn); } /* get untouched params from current TXN or DB */ diff --git a/src/api-txn.c b/src/api-txn.c index 04feabea..b798188c 100644 --- a/src/api-txn.c +++ b/src/api-txn.c @@ -513,23 +513,25 @@ int mdbx_txn_info(const MDBX_txn *txn, MDBX_txn_info *info, bool scan_rlt) { info->txn_reader_lag = INT64_MAX; lck_t *const lck = env->lck_mmap.lck; if (scan_rlt && lck) { - txnid_t oldest_snapshot = txn->txnid; + txnid_t oldest_reading = txn->txnid; const size_t snap_nreaders = atomic_load32(&lck->rdt_length, mo_AcquireRelease); if (snap_nreaders) { - oldest_snapshot = txn_snapshot_oldest(txn); - if (oldest_snapshot == txn->txnid - 1) { - /* check if there is at least one reader */ - bool exists = false; + txn_gc_detent(txn); + oldest_reading = txn->env->gc.detent; + if (oldest_reading == txn->wr.troika.txnid[txn->wr.troika.recent]) { + /* Если самый старый используемый снимок является предыдущим, т. е. непосредственно предшествующим текущей + * транзакции, то просматриваем таблицу читателей чтобы выяснить действительно ли снимок используется + * читателями. */ + oldest_reading = txn->txnid; for (size_t i = 0; i < snap_nreaders; ++i) { - if (atomic_load32(&lck->rdt[i].pid, mo_Relaxed) && txn->txnid > safe64_read(&lck->rdt[i].txnid)) { - exists = true; + if (atomic_load32(&lck->rdt[i].pid, mo_Relaxed) && txn->env->gc.detent == safe64_read(&lck->rdt[i].txnid)) { + oldest_reading = txn->env->gc.detent; break; } } - oldest_snapshot += !exists; } } - info->txn_reader_lag = txn->txnid - oldest_snapshot; + info->txn_reader_lag = txn->txnid - oldest_reading; } } diff --git a/src/audit.c b/src/audit.c index b7304a58..2fe8f6f4 100644 --- a/src/audit.c +++ b/src/audit.c @@ -24,12 +24,11 @@ static size_t audit_db_used(const tree_t *db) { return db ? (size_t)db->branch_pages + (size_t)db->leaf_pages + (size_t)db->large_pages : 0; } -__cold static int audit_ex_locked(MDBX_txn *txn, size_t retired_stored, bool dont_filter_gc) { +__cold static int audit_ex_locked(MDBX_txn *txn, const size_t retired_stored, const bool dont_filter_gc) { const MDBX_env *const env = txn->env; - size_t pending = 0; - if ((txn->flags & MDBX_TXN_RDONLY) == 0) - pending = txn->wr.loose_count + MDBX_PNL_GETSIZE(txn->wr.repnl) + - (MDBX_PNL_GETSIZE(txn->wr.retired_pages) - retired_stored); + tASSERT(txn, (txn->flags & MDBX_TXN_RDONLY) == 0); + const size_t pending = txn->wr.loose_count + MDBX_PNL_GETSIZE(txn->wr.repnl) + + (MDBX_PNL_GETSIZE(txn->wr.retired_pages) - retired_stored); cursor_couple_t cx; int rc = cursor_init(&cx.outer, txn, FREE_DBI); @@ -40,17 +39,16 @@ __cold static int audit_ex_locked(MDBX_txn *txn, size_t retired_stored, bool don MDBX_val key, data; rc = outer_first(&cx.outer, &key, &data); while (rc == MDBX_SUCCESS) { - if (!dont_filter_gc) { - if (unlikely(key.iov_len != sizeof(txnid_t))) { - ERROR("%s/%d: %s %u", "MDBX_CORRUPTED", MDBX_CORRUPTED, "invalid GC-key size", (unsigned)key.iov_len); - return MDBX_CORRUPTED; - } - txnid_t id = unaligned_peek_u64(4, key.iov_base); - if (txn->wr.gc.retxl ? txl_contain(txn->wr.gc.retxl, id) : (id <= txn->wr.gc.last_reclaimed)) - goto skip; + if (unlikely(key.iov_len != sizeof(txnid_t))) { + ERROR("%s/%d: %s %u", "MDBX_CORRUPTED", MDBX_CORRUPTED, "invalid GC-key size", (unsigned)key.iov_len); + return MDBX_CORRUPTED; } - gc += *(pgno_t *)data.iov_base; - skip: + const txnid_t id = unaligned_peek_u64(4, key.iov_base); + const size_t len = *(pgno_t *)data.iov_base; + const bool acc = dont_filter_gc || !gc_is_reclaimed(txn, id); + TRACE("%s id %" PRIaTXN " len %zu", acc ? "acc" : "skip", id, len); + if (acc) + gc += len; rc = outer_next(&cx.outer, &key, &data, MDBX_NEXT); } tASSERT(txn, rc == MDBX_NOTFOUND); diff --git a/src/cursor.c b/src/cursor.c index 9cc24f59..3fc0a2d3 100644 --- a/src/cursor.c +++ b/src/cursor.c @@ -1780,8 +1780,7 @@ __hot csr_t cursor_seek(MDBX_cursor *mc, MDBX_val *key, MDBX_val *data, MDBX_cur } int cmp = mc->clc->k.cmp(&aligned_key, &nodekey); if (unlikely(cmp == 0)) { - /* Probably happens rarely, but first node on the page - * was the one we wanted. */ + /* Probably happens rarely, but first node on the page was the one we wanted. */ mc->ki[mc->top] = 0; ret.exact = true; goto got_node; diff --git a/src/dpl.h b/src/dpl.h index cc9799a0..87ceeee8 100644 --- a/src/dpl.h +++ b/src/dpl.h @@ -53,7 +53,7 @@ static inline dpl_t *dpl_sort(const MDBX_txn *txn) { return likely(dl->sorted == dl->length) ? dl : dpl_sort_slowpath(txn); } -MDBX_INTERNAL __noinline size_t dpl_search(const MDBX_txn *txn, pgno_t pgno); +MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL __noinline size_t dpl_search(const MDBX_txn *txn, pgno_t pgno); MDBX_MAYBE_UNUSED MDBX_INTERNAL const page_t *debug_dpl_find(const MDBX_txn *txn, const pgno_t pgno); @@ -68,7 +68,7 @@ MDBX_NOTHROW_PURE_FUNCTION static inline pgno_t dpl_endpgno(const dpl_t *dl, siz return dpl_npages(dl, i) + dl->items[i].pgno; } -static inline bool dpl_intersect(const MDBX_txn *txn, pgno_t pgno, size_t npages) { +MDBX_NOTHROW_PURE_FUNCTION static inline bool dpl_intersect(const MDBX_txn *txn, pgno_t pgno, size_t npages) { tASSERT(txn, (txn->flags & MDBX_TXN_RDONLY) == 0); tASSERT(txn, (txn->flags & MDBX_WRITEMAP) == 0 || MDBX_AVOID_MSYNC); diff --git a/src/dxb.c b/src/dxb.c index 37ddcaa7..cae134a3 100644 --- a/src/dxb.c +++ b/src/dxb.c @@ -1061,16 +1061,17 @@ int dxb_sync_locked(MDBX_env *env, unsigned flags, meta_t *const pending, troika #endif /* MADV_DONTNEED || POSIX_MADV_DONTNEED */ /* LY: check conditions to shrink datafile */ - const pgno_t backlog_gap = 3 + pending->trees.gc.height * 3; + const pgno_t stockpile_gap = 3 + pending->trees.gc.height * 3; pgno_t shrink_step = 0; if (pending->geometry.shrink_pv && pending->geometry.now - pending->geometry.first_unallocated > - (shrink_step = pv2pages(pending->geometry.shrink_pv)) + backlog_gap) { - if (pending->geometry.now > largest_pgno && pending->geometry.now - largest_pgno > shrink_step + backlog_gap) { + (shrink_step = pv2pages(pending->geometry.shrink_pv)) + stockpile_gap) { + if (pending->geometry.now > largest_pgno && + pending->geometry.now - largest_pgno > shrink_step + stockpile_gap) { const pgno_t aligner = pending->geometry.grow_pv ? /* grow_step */ pv2pages(pending->geometry.grow_pv) : shrink_step; - const pgno_t with_backlog_gap = largest_pgno + backlog_gap; + const pgno_t with_stockpile_gap = largest_pgno + stockpile_gap; const pgno_t aligned = - pgno_align2os_pgno(env, (size_t)with_backlog_gap + aligner - with_backlog_gap % aligner); + pgno_align2os_pgno(env, (size_t)with_stockpile_gap + aligner - with_stockpile_gap % aligner); const pgno_t bottom = (aligned > pending->geometry.lower) ? aligned : pending->geometry.lower; if (pending->geometry.now > bottom) { if (TROIKA_HAVE_STEADY(troika)) diff --git a/src/env.c b/src/env.c index e2540144..19024135 100644 --- a/src/env.c +++ b/src/env.c @@ -164,7 +164,7 @@ retry:; } eASSERT(env, head.txnid == recent_committed_txnid(env)); env->basal_txn->txnid = head.txnid; - txn_snapshot_oldest(env->basal_txn); + txn_gc_detent(env->basal_txn); flags |= txn_shrink_allowed; } @@ -524,7 +524,7 @@ __cold int env_close(MDBX_env *env, bool resurrect_after_fork) { env->defer_free = nullptr; #endif /* MDBX_ENABLE_DBI_LOCKFREE */ - if (!(env->flags & MDBX_RDONLY)) + if ((env->flags & MDBX_RDONLY) == 0) osal_ioring_destroy(&env->ioring); env->lck = nullptr; diff --git a/src/essentials.h b/src/essentials.h index f46aec2e..7a76b27c 100644 --- a/src/essentials.h +++ b/src/essentials.h @@ -30,8 +30,10 @@ typedef struct iov_ctx iov_ctx_t; #if UINTPTR_MAX > 0xffffFFFFul || ULONG_MAX > 0xffffFFFFul || defined(_WIN64) #define MDBX_WORDBITS 64 +#define MDBX_WORDBITS_LN2 6 #else #define MDBX_WORDBITS 32 +#define MDBX_WORDBITS_LN2 5 #endif /* MDBX_WORDBITS */ #include "options.h" diff --git a/src/gc-get.c b/src/gc-get.c index b3e98bab..6fe4bf3b 100644 --- a/src/gc-get.c +++ b/src/gc-get.c @@ -570,14 +570,11 @@ static pgno_t *scan4seq_resolver(pgno_t *range, const size_t len, const size_t s /*----------------------------------------------------------------------------*/ -#define ALLOC_COALESCE 4 /* внутреннее состояние */ -#define ALLOC_SHOULD_SCAN 8 /* внутреннее состояние */ -#define ALLOC_LIFO 16 /* внутреннее состояние */ - -static inline bool is_gc_usable(MDBX_txn *txn, const MDBX_cursor *mc, const uint8_t flags) { +static inline bool is_reclaimable(MDBX_txn *txn, const MDBX_cursor *mc, const uint8_t flags) { /* If txn is updating the GC, then the retired-list cannot play catch-up with * itself by growing while trying to save it. */ - if (mc->tree == &txn->dbs[FREE_DBI] && !(flags & ALLOC_RESERVE) && !(mc->flags & z_gcu_preparation)) + STATIC_ASSERT(ALLOC_RESERVE == z_gcu_preparation); + if (mc->tree == &txn->dbs[FREE_DBI] && !((flags | mc->flags) & z_gcu_preparation)) return false; /* avoid search inside empty tree and while tree is updating, @@ -590,8 +587,6 @@ static inline bool is_gc_usable(MDBX_txn *txn, const MDBX_cursor *mc, const uint return true; } -static inline bool is_already_reclaimed(const MDBX_txn *txn, txnid_t id) { return txl_contain(txn->wr.gc.retxl, id); } - __hot static pgno_t repnl_get_single(MDBX_txn *txn) { const size_t len = MDBX_PNL_GETSIZE(txn->wr.repnl); assert(len > 0); @@ -721,6 +716,10 @@ __hot static pgno_t repnl_get_sequence(MDBX_txn *txn, const size_t num, uint8_t return 0; } +bool gc_repnl_has_span(MDBX_txn *txn, const size_t num) { + return (num > 1) ? repnl_get_sequence(txn, num, ALLOC_RESERVE) != 0 : !MDBX_PNL_IS_EMPTY(txn->wr.repnl); +} + static inline pgr_t page_alloc_finalize(MDBX_env *const env, MDBX_txn *const txn, const MDBX_cursor *const mc, const pgno_t pgno, const size_t num) { #if MDBX_ENABLE_PROFGC @@ -842,6 +841,13 @@ pgr_t gc_alloc_ex(const MDBX_cursor *const mc, const size_t num, uint8_t flags) prof->spe_counter += 1; #endif /* MDBX_ENABLE_PROFGC */ + /* Если взведен флажок ALLOC_RESERVE, то требуется только обеспечение соответствующего резерва в txn->wr.repnl + * и/или txn->wr.gc.reclaimed, но без выделения и возврата страницы. При этом возможны три варианта вызова: + * 1. num == 0 — требуется слот для возврата в GC остатков ранее переработанных/извлеченных страниц, + * при этом нет смысла перерабатывать длинные записи, так как тогда дефицит свободных id/слотов не уменьшится; + * 2. num == 1 — требуется увеличение резерва перед обновлением GC; + * 3. num > 1 — требуется последовательность страниц для сохранения retired-страниц + * при выключенном MDBX_ENABLE_BIGFOOT. */ eASSERT(env, num > 0 || (flags & ALLOC_RESERVE)); eASSERT(env, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); @@ -866,13 +872,12 @@ pgr_t gc_alloc_ex(const MDBX_cursor *const mc, const size_t num, uint8_t flags) goto done; } } else { - eASSERT(env, num == 0 || MDBX_PNL_GETSIZE(txn->wr.repnl) == 0); - eASSERT(env, !(flags & ALLOC_RESERVE) || num == 0); + eASSERT(env, num == 0 || MDBX_PNL_GETSIZE(txn->wr.repnl) == 0 || (flags & ALLOC_RESERVE)); } //--------------------------------------------------------------------------- - if (unlikely(!is_gc_usable(txn, mc, flags))) { + if (unlikely(!is_reclaimable(txn, mc, flags))) { eASSERT(env, (txn->flags & txn_gc_drained) || num > 1); goto no_gc; } @@ -880,21 +885,18 @@ pgr_t gc_alloc_ex(const MDBX_cursor *const mc, const size_t num, uint8_t flags) eASSERT(env, (flags & (ALLOC_COALESCE | ALLOC_LIFO | ALLOC_SHOULD_SCAN)) == 0); flags += (env->flags & MDBX_LIFORECLAIM) ? ALLOC_LIFO : 0; - if (/* Не коагулируем записи при подготовке резерва для обновления GC. - * Иначе попытка увеличить резерв может приводить к необходимости ещё - * большего резерва из-за увеличения списка переработанных страниц. */ - (flags & ALLOC_RESERVE) == 0) { - if (txn->dbs[FREE_DBI].branch_pages && MDBX_PNL_GETSIZE(txn->wr.repnl) < env->maxgc_large1page / 2) - flags += ALLOC_COALESCE; - } + /* Не коагулируем записи в случае запроса слота для возврата страниц в GC. Иначе попытка увеличить резерв + * может приводить к необходимости ещё большего резерва из-за увеличения списка переработанных страниц. */ + if (num > 0 && txn->dbs[FREE_DBI].branch_pages && MDBX_PNL_GETSIZE(txn->wr.repnl) < env->maxgc_large1page / 2) + flags += ALLOC_COALESCE; - MDBX_cursor *const gc = ptr_disp(env->basal_txn, sizeof(MDBX_txn)); + MDBX_cursor *const gc = txn_gc_cursor(txn); eASSERT(env, mc != gc && gc->next == gc); gc->txn = txn; gc->dbi_state = txn->dbi_state; gc->top_and_flags = z_fresh_mark; - txn->wr.prefault_write_activated = env->options.prefault_write; + txn->wr.prefault_write_activated = !env->incore && env->options.prefault_write; if (txn->wr.prefault_write_activated) { /* Проверка посредством minicore() существенно снижает затраты, но в * простейших случаях (тривиальный бенчмарк) интегральная производительность @@ -911,45 +913,38 @@ pgr_t gc_alloc_ex(const MDBX_cursor *const mc, const size_t num, uint8_t flags) txn->wr.prefault_write_activated = false; } -retry_gc_refresh_oldest:; - txnid_t oldest = txn_snapshot_oldest(txn); -retry_gc_have_oldest: - if (unlikely(oldest >= txn->txnid)) { - ERROR("unexpected/invalid oldest-readed txnid %" PRIaTXN " for current-txnid %" PRIaTXN, oldest, txn->txnid); +retry_gc_refresh_detent: + txn_gc_detent(txn); +retry_gc_have_detent: + if (unlikely(txn->env->gc.detent >= txn->txnid)) { + FATAL("unexpected/invalid gc-detent %" PRIaTXN " for current-txnid %" PRIaTXN, txn->env->gc.detent, txn->txnid); ret.err = MDBX_PROBLEM; goto fail; } - const txnid_t detent = oldest + 1; txnid_t id = 0; MDBX_cursor_op op = MDBX_FIRST; if (flags & ALLOC_LIFO) { - if (!txn->wr.gc.retxl) { - txn->wr.gc.retxl = txl_alloc(); - if (unlikely(!txn->wr.gc.retxl)) { - ret.err = MDBX_ENOMEM; - goto fail; - } - } /* Begin lookup backward from oldest reader */ - id = detent - 1; + id = txn->env->gc.detent; op = MDBX_SET_RANGE; - } else if (txn->wr.gc.last_reclaimed) { + } else { /* Continue lookup forward from last-reclaimed */ - id = txn->wr.gc.last_reclaimed + 1; - if (id >= detent) - goto depleted_gc; - op = MDBX_SET_RANGE; + id = rkl_highest(&txn->wr.gc.reclaimed); + if (id) { + id += 1; + op = MDBX_SET_RANGE; + if (id >= txn->env->gc.detent) + goto depleted_gc; + } } -next_gc:; - MDBX_val key; - key.iov_base = &id; - key.iov_len = sizeof(id); - +next_gc: #if MDBX_ENABLE_PROFGC - prof->rsteps += 1; + prof->rsteps += 1 #endif /* MDBX_ENABLE_PROFGC */ + ; + MDBX_val key = {.iov_base = &id, .iov_len = sizeof(id)}; /* Seek first/next GC record */ ret.err = cursor_ops(gc, &key, nullptr, op); @@ -967,15 +962,18 @@ next_gc:; ret.err = MDBX_CORRUPTED; goto fail; } + id = unaligned_peek_u64(4, key.iov_base); if (flags & ALLOC_LIFO) { op = MDBX_PREV; - if (id >= detent || is_already_reclaimed(txn, id)) + if (id >= txn->env->gc.detent || gc_is_reclaimed(txn, id)) goto next_gc; } else { - op = MDBX_NEXT; - if (unlikely(id >= detent)) + if (unlikely(id >= txn->env->gc.detent)) goto depleted_gc; + op = MDBX_NEXT; + if (gc_is_reclaimed(txn, id)) + goto next_gc; } txn->flags &= ~txn_gc_drained; @@ -996,12 +994,23 @@ next_gc:; const size_t gc_len = MDBX_PNL_GETSIZE(gc_pnl); TRACE("gc-read: id #%" PRIaTXN " len %zu, re-list will %zu ", id, gc_len, gc_len + MDBX_PNL_GETSIZE(txn->wr.repnl)); - if (unlikely(gc_len + MDBX_PNL_GETSIZE(txn->wr.repnl) >= env->maxgc_large1page)) { - /* Don't try to coalesce too much. */ + if (unlikely(!num)) { + /* TODO: Проверка критериев пункта 2 сформулированного в gc_provide_slots(). + * Сейчас тут сильно упрощенная и не совсем верная проверка, так как пока недоступна информация о кол-ве имеющихся + * слотов и их дефиците для возврата wr.repl. */ + if (gc_len > env->maxgc_large1page / 4 * 3 + /* если запись достаточно длинная, то переработка слота не особо увеличит место для возврата wr.repl, и т.п. */ + && MDBX_PNL_GETSIZE(txn->wr.repnl) + gc_len > env->maxgc_large1page /* не помещается в хвост */) { + DEBUG("avoid reclaiming %" PRIaTXN " slot, since it is too long (%zu)", id, gc_len); + ret.err = MDBX_NOTFOUND; + goto reserve_done; + } + } + + if (unlikely(gc_len + MDBX_PNL_GETSIZE(txn->wr.repnl) /* Don't try to coalesce too much. */ >= + env->maxgc_large1page)) { if (flags & ALLOC_SHOULD_SCAN) { - eASSERT(env, flags & ALLOC_COALESCE); - eASSERT(env, !(flags & ALLOC_RESERVE)); - eASSERT(env, num > 0); + eASSERT(env, (flags & ALLOC_COALESCE) /* && !(flags & ALLOC_RESERVE) */ && num > 0); #if MDBX_ENABLE_PROFGC env->lck->pgops.gc_prof.coalescences += 1; #endif /* MDBX_ENABLE_PROFGC */ @@ -1010,25 +1019,25 @@ next_gc:; eASSERT(env, MDBX_PNL_LAST(txn->wr.repnl) < txn->geo.first_unallocated && MDBX_PNL_FIRST(txn->wr.repnl) < txn->geo.first_unallocated); if (likely(num == 1)) { - pgno = repnl_get_single(txn); + pgno = (flags & ALLOC_RESERVE) ? P_INVALID : repnl_get_single(txn); goto done; } pgno = repnl_get_sequence(txn, num, flags); if (likely(pgno)) goto done; } - flags -= ALLOC_COALESCE | ALLOC_SHOULD_SCAN; } + flags &= ~(ALLOC_COALESCE | ALLOC_SHOULD_SCAN); if (unlikely(/* list is too long already */ MDBX_PNL_GETSIZE(txn->wr.repnl) >= env->options.rp_augment_limit) && ((/* not a slot-request from gc-update */ num && /* have enough unallocated space */ txn->geo.upper >= txn->geo.first_unallocated + num && - monotime_since_cached(monotime_begin, &now_cache) + txn->wr.gc.time_acc >= env->options.gc_time_limit) || + monotime_since_cached(monotime_begin, &now_cache) + txn->wr.gc.spent >= env->options.gc_time_limit) || gc_len + MDBX_PNL_GETSIZE(txn->wr.repnl) >= PAGELIST_LIMIT)) { /* Stop reclaiming to avoid large/overflow the page list. This is a rare - * case while search for a continuously multi-page region in a - * large database, see https://libmdbx.dqdkfa.ru/dead-github/issues/123 */ + * case while search for a continuously multi-page region in a large database, + * see https://libmdbx.dqdkfa.ru/dead-github/issues/123 */ NOTICE("stop reclaiming %s: %zu (current) + %zu " - "(chunk) -> %zu, rp_augment_limit %u", + "(chunk) >= %zu, rp_augment_limit %u", likely(gc_len + MDBX_PNL_GETSIZE(txn->wr.repnl) < PAGELIST_LIMIT) ? "since rp_augment_limit was reached" : "to avoid PNL overflow", MDBX_PNL_GETSIZE(txn->wr.repnl), gc_len, gc_len + MDBX_PNL_GETSIZE(txn->wr.repnl), @@ -1038,12 +1047,17 @@ next_gc:; } /* Remember ID of readed GC record */ - txn->wr.gc.last_reclaimed = id; - if (flags & ALLOC_LIFO) { - ret.err = txl_append(&txn->wr.gc.retxl, id); - if (unlikely(ret.err != MDBX_SUCCESS)) - goto fail; - } + ret.err = rkl_push(&txn->wr.gc.reclaimed, id, + false /* Вместо false, тут можно передавать/использовать (flags & ALLOC_LIFO) == 0, тогда + * дыры/пропуски в идентификаторах GC будут образовывать непрерывные интервалы в wr.gc.reclaimed, + * что обеспечит больше свободных идентификаторов/слотов для возврата страниц. Однако, это + * также приведёт к пустым попыткам удаления отсутствующих записей в gc_clear_reclaimed(), + * а далее к перекладыванию этих сплошных интервалов поэлементно в ready4reuse. + * Поэтому смысла в этом решительно нет. Следует либо формировать сплошные интервалы при + * работе gc_clear_reclaimed(), особенно в FIFO-режиме, либо искать их только в gc_provide_ids() */); + TRACE("%" PRIaTXN " len %zu pushed to txn-rkl, err %d", id, gc_len, ret.err); + if (unlikely(ret.err != MDBX_SUCCESS)) + goto fail; /* Append PNL from GC record to wr.repnl */ ret.err = pnl_need(&txn->wr.repnl, gc_len); @@ -1087,22 +1101,25 @@ next_gc:; } eASSERT(env, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); - /* Done for a kick-reclaim mode, actually no page needed */ - if (unlikely(num == 0)) { - eASSERT(env, ret.err == MDBX_SUCCESS); - TRACE("%s: last id #%" PRIaTXN ", re-len %zu", "early-exit for slot", id, MDBX_PNL_GETSIZE(txn->wr.repnl)); - goto early_exit; - } - - /* TODO: delete reclaimed records */ + /* TODO: удаление загруженных из GC записей */ eASSERT(env, op == MDBX_PREV || op == MDBX_NEXT); if (flags & ALLOC_COALESCE) { - TRACE("%s: last id #%" PRIaTXN ", re-len %zu", "coalesce-continue", id, MDBX_PNL_GETSIZE(txn->wr.repnl)); - goto next_gc; + if (MDBX_PNL_GETSIZE(txn->wr.repnl) < env->maxgc_large1page / 2) { + TRACE("%s: last id #%" PRIaTXN ", re-len %zu", "coalesce-continue", id, MDBX_PNL_GETSIZE(txn->wr.repnl)); + goto next_gc; + } + flags -= ALLOC_COALESCE; } scan: + if ((flags & ALLOC_RESERVE) && num < 2) { + /* Если был нужен только slot/id для gc_reclaim_slot() или gc_reserve4stockpile() */ + TRACE("%s: last id #%" PRIaTXN ", re-len %zu", "reserve-done", id, MDBX_PNL_GETSIZE(txn->wr.repnl)); + ret.err = MDBX_SUCCESS; + goto reserve_done; + } + eASSERT(env, flags & ALLOC_SHOULD_SCAN); eASSERT(env, num > 0); if (MDBX_PNL_GETSIZE(txn->wr.repnl) >= num) { @@ -1118,17 +1135,16 @@ scan: goto done; } flags -= ALLOC_SHOULD_SCAN; - if (ret.err == MDBX_SUCCESS) { + if ((txn->flags & txn_gc_drained) == 0) { TRACE("%s: last id #%" PRIaTXN ", re-len %zu", "continue-search", id, MDBX_PNL_GETSIZE(txn->wr.repnl)); goto next_gc; } depleted_gc: TRACE("%s: last id #%" PRIaTXN ", re-len %zu", "gc-depleted", id, MDBX_PNL_GETSIZE(txn->wr.repnl)); - ret.err = MDBX_NOTFOUND; + txn->flags |= txn_gc_drained; if (flags & ALLOC_SHOULD_SCAN) goto scan; - txn->flags |= txn_gc_drained; //------------------------------------------------------------------------- @@ -1145,9 +1161,9 @@ depleted_gc: /* Does reclaiming stopped at the last steady point? */ const meta_ptr_t recent = meta_recent(env, &txn->wr.troika); const meta_ptr_t prefer_steady = meta_prefer_steady(env, &txn->wr.troika); - if (recent.ptr_c != prefer_steady.ptr_c && prefer_steady.is_steady && detent == prefer_steady.txnid + 1) { - DEBUG("gc-kick-steady: recent %" PRIaTXN "-%s, steady %" PRIaTXN "-%s, detent %" PRIaTXN, recent.txnid, - durable_caption(recent.ptr_c), prefer_steady.txnid, durable_caption(prefer_steady.ptr_c), detent); + if (recent.ptr_c != prefer_steady.ptr_c && prefer_steady.is_steady && txn->env->gc.detent == prefer_steady.txnid) { + DEBUG("gc-kick-steady: recent %" PRIaTXN "-%s, steady %" PRIaTXN "-%s", recent.txnid, durable_caption(recent.ptr_c), + prefer_steady.txnid, durable_caption(prefer_steady.ptr_c)); const pgno_t autosync_threshold = atomic_load32(&env->lck->autosync_threshold, mo_Relaxed); const uint64_t autosync_period = atomic_load64(&env->lck->autosync_period, mo_Relaxed); uint64_t eoos_timestamp; @@ -1166,12 +1182,12 @@ depleted_gc: #if MDBX_ENABLE_PROFGC env->lck->pgops.gc_prof.wipes += 1; #endif /* MDBX_ENABLE_PROFGC */ - ret.err = meta_wipe_steady(env, detent); + ret.err = meta_wipe_steady(env, txn->env->gc.detent); DEBUG("gc-wipe-steady, rc %d", ret.err); if (unlikely(ret.err != MDBX_SUCCESS)) goto fail; eASSERT(env, prefer_steady.ptr_c != meta_prefer_steady(env, &txn->wr.troika).ptr_c); - goto retry_gc_refresh_oldest; + goto retry_gc_refresh_detent; } if ((autosync_threshold && atomic_load64(&env->lck->unsynced_pages, mo_Relaxed) >= autosync_threshold) || (autosync_period && (eoos_timestamp = atomic_load64(&env->lck->eoos_timestamp, mo_Relaxed)) && @@ -1189,15 +1205,12 @@ depleted_gc: if (unlikely(ret.err != MDBX_SUCCESS)) goto fail; eASSERT(env, prefer_steady.ptr_c != meta_prefer_steady(env, &txn->wr.troika).ptr_c); - goto retry_gc_refresh_oldest; + goto retry_gc_refresh_detent; } } - if (unlikely(true == atomic_load32(&env->lck->rdt_refresh_flag, mo_AcquireRelease))) { - oldest = txn_snapshot_oldest(txn); - if (oldest >= detent) - goto retry_gc_have_oldest; - } + if (unlikely(true == atomic_load32(&env->lck->rdt_refresh_flag, mo_AcquireRelease)) && txn_gc_detent(txn)) + goto retry_gc_have_detent; /* Avoid kick lagging reader(s) if is enough unallocated space * at the end of database file. */ @@ -1206,11 +1219,8 @@ depleted_gc: goto done; } - if (oldest < txn->txnid - xMDBX_TXNID_STEP) { - oldest = mvcc_kick_laggards(env, oldest); - if (oldest >= detent) - goto retry_gc_have_oldest; - } + if (txn->txnid - txn->env->gc.detent > xMDBX_TXNID_STEP && mvcc_kick_laggards(env, txn->env->gc.detent)) + goto retry_gc_refresh_detent; //--------------------------------------------------------------------------- @@ -1277,30 +1287,40 @@ done: eASSERT(env, ret.err != MDBX_SUCCESS); eASSERT(env, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); int level; - const char *what; - if (flags & ALLOC_RESERVE) { - level = (flags & ALLOC_UNIMPORTANT) ? MDBX_LOG_DEBUG : MDBX_LOG_NOTICE; - what = num ? "reserve-pages" : "fetch-slot"; - } else { + if (flags & ALLOC_UNIMPORTANT) + level = MDBX_LOG_DEBUG; + else if (flags & ALLOC_RESERVE) + level = MDBX_LOG_NOTICE; + else { txn->flags |= MDBX_TXN_ERROR; level = MDBX_LOG_ERROR; - what = "pages"; } - if (LOG_ENABLED(level)) - debug_log(level, __func__, __LINE__, - "unable alloc %zu %s, alloc-flags 0x%x, err %d, txn-flags " - "0x%x, re-list-len %zu, loose-count %zu, gc: height %u, " - "branch %zu, leaf %zu, large %zu, entries %zu\n", - num, what, flags, ret.err, txn->flags, MDBX_PNL_GETSIZE(txn->wr.repnl), txn->wr.loose_count, - txn->dbs[FREE_DBI].height, (size_t)txn->dbs[FREE_DBI].branch_pages, - (size_t)txn->dbs[FREE_DBI].leaf_pages, (size_t)txn->dbs[FREE_DBI].large_pages, - (size_t)txn->dbs[FREE_DBI].items); + if (LOG_ENABLED(level)) { + if (num) + debug_log(level, __func__, __LINE__, + "unable %s %zu, alloc-flags 0x%x, err %d, txn-flags " + "0x%x, re-list-len %zu, loose-count %zu, gc: height %u, " + "branch %zu, leaf %zu, large %zu, entries %zu\n", + (flags & ALLOC_RESERVE) ? "reserve" : "alloc", num, flags, ret.err, txn->flags, + MDBX_PNL_GETSIZE(txn->wr.repnl), txn->wr.loose_count, txn->dbs[FREE_DBI].height, + (size_t)txn->dbs[FREE_DBI].branch_pages, (size_t)txn->dbs[FREE_DBI].leaf_pages, + (size_t)txn->dbs[FREE_DBI].large_pages, (size_t)txn->dbs[FREE_DBI].items); + else + debug_log(level, __func__, __LINE__, + "unable fetch-slot, alloc-flags 0x%x, err %d, txn-flags " + "0x%x, re-list-len %zu, loose-count %zu, gc: height %u, " + "branch %zu, leaf %zu, large %zu, entries %zu\n", + flags, ret.err, txn->flags, MDBX_PNL_GETSIZE(txn->wr.repnl), txn->wr.loose_count, + txn->dbs[FREE_DBI].height, (size_t)txn->dbs[FREE_DBI].branch_pages, + (size_t)txn->dbs[FREE_DBI].leaf_pages, (size_t)txn->dbs[FREE_DBI].large_pages, + (size_t)txn->dbs[FREE_DBI].items); + } ret.page = nullptr; } if (num > 1) - txn->wr.gc.time_acc += monotime_since_cached(monotime_begin, &now_cache); + txn->wr.gc.spent += monotime_since_cached(monotime_begin, &now_cache); } else { - early_exit: + reserve_done: DEBUG("return nullptr for %zu pages for ALLOC_%s, rc %d", num, num ? "RESERVE" : "SLOT", ret.err); ret.page = nullptr; } diff --git a/src/gc-put.c b/src/gc-put.c index 2ebe1063..bf6e159a 100644 --- a/src/gc-put.c +++ b/src/gc-put.c @@ -1,28 +1,164 @@ /// \copyright SPDX-License-Identifier: Apache-2.0 -/// \author Леонид Юрьев aka Leonid Yuriev \date 2015-2025 +/// \author Леонид Юрьев aka Leonid Yuriev \date 2025 #include "internals.h" +int gc_put_init(MDBX_txn *txn, gcu_t *ctx) { + memset(ctx, 0, offsetof(gcu_t, ready4reuse)); + /* Размер куска помещающийся на одну отдельную "overflow" страницу, но с небольшим запасом сводобного места. */ + ctx->goodchunk = txn->env->maxgc_large1page - (txn->env->maxgc_large1page >> 4); + rkl_init(&ctx->ready4reuse); + rkl_init(&ctx->sequel); +#if MDBX_ENABLE_BIGFOOT + ctx->bigfoot = txn->txnid; +#endif /* MDBX_ENABLE_BIGFOOT */ + return cursor_init(&ctx->cursor, txn, FREE_DBI); +} + +void gc_put_destroy(gcu_t *ctx) { + rkl_destroy(&ctx->ready4reuse); + rkl_destroy(&ctx->sequel); +} + +static int gc_peekid(const MDBX_val *key, txnid_t *id) { + if (likely(key->iov_len == sizeof(txnid_t))) { + *id = unaligned_peek_u64(4, key->iov_base); + return MDBX_SUCCESS; + } + ERROR("%s/%d: %s", "MDBX_CORRUPTED", MDBX_CORRUPTED, "invalid GC key-length"); + return MDBX_CORRUPTED; +} + +#if MDBX_DEBUG_GCU +#pragma push_macro("LOG_ENABLED") +#undef LOG_ENABLED +#define LOG_ENABLED(LVL) \ + unlikely(MDBX_DEBUG_GCU > 2 || (ctx->loop > 1 && (MDBX_DEBUG_GCU > 1 || LVL < MDBX_LOG_EXTRA)) || \ + LVL <= globals.loglevel) +#endif /* MDBX_DEBUG_GCU */ + MDBX_NOTHROW_PURE_FUNCTION static bool is_lifo(const MDBX_txn *txn) { return (txn->env->flags & MDBX_LIFORECLAIM) != 0; } -MDBX_MAYBE_UNUSED static inline const char *dbg_prefix(const gcu_t *ctx) { +MDBX_NOTHROW_PURE_FUNCTION MDBX_MAYBE_UNUSED static inline const char *dbg_prefix(const gcu_t *ctx) { return is_lifo(ctx->cursor.txn) ? " lifo" : " fifo"; } -static inline size_t backlog_size(MDBX_txn *txn) { return MDBX_PNL_GETSIZE(txn->wr.repnl) + txn->wr.loose_count; } +MDBX_MAYBE_UNUSED static void dbg_id(gcu_t *ctx, txnid_t id) { +#if MDBX_DEBUG_GCU + if (ctx->dbg.prev) { + if (ctx->dbg.prev != id - 1) { + if (ctx->dbg.n) + DEBUG_EXTRA_PRINT("-%" PRIaTXN, ctx->dbg.prev); + if (id) + DEBUG_EXTRA_PRINT(" %" PRIaTXN, id); + ctx->dbg.n = 0; + } else + ctx->dbg.n += 1; + } else { + DEBUG_EXTRA_PRINT(" %" PRIaTXN, id); + ctx->dbg.n = 0; + } + ctx->dbg.prev = id; +#else + (void)ctx; + (void)id; +#endif /* MDBX_DEBUG_GCU */ +} -static int clean_stored_retired(MDBX_txn *txn, gcu_t *ctx) { +MDBX_MAYBE_UNUSED static void dbg_dump_ids(gcu_t *ctx) { +#if MDBX_DEBUG_GCU + if (LOG_ENABLED(MDBX_LOG_EXTRA)) { + DEBUG_EXTRA("%s", "GC:"); + if (ctx->cursor.tree->items) { + cursor_couple_t couple; + MDBX_val key; + int err = cursor_init(&couple.outer, ctx->cursor.txn, FREE_DBI); + if (err != MDBX_SUCCESS) + ERROR("%s(), %d", "cursor_init", err); + else + err = outer_first(&couple.outer, &key, nullptr); + + txnid_t id; + while (err == MDBX_SUCCESS) { + err = gc_peekid(&key, &id); + if (unlikely(err == MDBX_SUCCESS)) { + dbg_id(ctx, id); + if (id >= couple.outer.txn->env->gc.detent) + break; + err = outer_next(&couple.outer, &key, nullptr, MDBX_NEXT); + } + } + dbg_id(ctx, 0); + DEBUG_EXTRA_PRINT("%s\n", (id >= couple.outer.txn->env->gc.detent) ? "..." : ""); + } else + DEBUG_EXTRA_PRINT("%s\n", " empty"); + + DEBUG_EXTRA("%s", "ready4reuse:"); + if (rkl_empty(&ctx->ready4reuse)) + DEBUG_EXTRA_PRINT("%s\n", " empty"); + else { + rkl_iter_t i = rkl_iterator(&ctx->ready4reuse, false); + txnid_t id = rkl_turn(&i, false); + while (id) { + dbg_id(ctx, id); + id = rkl_turn(&i, false); + } + dbg_id(ctx, 0); + DEBUG_EXTRA_PRINT("%s\n", ""); + } + + DEBUG_EXTRA("%s", "comeback:"); + if (rkl_empty(&ctx->cursor.txn->wr.gc.comeback)) + DEBUG_EXTRA_PRINT("%s\n", " empty"); + else { + rkl_iter_t i = rkl_iterator(&ctx->cursor.txn->wr.gc.comeback, false); + txnid_t id = rkl_turn(&i, false); + while (id) { + dbg_id(ctx, id); + id = rkl_turn(&i, false); + } + dbg_id(ctx, 0); + DEBUG_EXTRA_PRINT("%s\n", ""); + } + } +#else + (void)ctx; +#endif /* MDBX_DEBUG_GCU */ +} + +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION static inline size_t gc_reclaimed_maxspan_chunk(MDBX_txn *txn, + gcu_t *ctx) { + (void)ctx; + /* Функция вычисляет размер куска списка возвращаемых/неиспользованных в GC страниц, который можно разместить + * в последовательности смежных страниц, которая возможно есть в самом этом списке. + * + * С одной стороны, такое размещение позволяет обойтись меньшим кол-вом слотов при возврате страниц, + * уменьшить кол-во итераций при резервировании, а также не дробить саму последовательность. + * + * Однако, при последующей переработке такая тактика допустима только при не-отложенной очистке GC. Иначе + * последовательность смежных страниц останется в GC до завершения переработавшей эту запись транзакции. А затем, в + * ходе обновления GC при фиксации транзакции, будет раздроблена или снова возвращена в GC. Таким образом, без + * не-отложенной очистки, такая тактика способствует миграции последовательностей страниц внутрь структуры самой GC + * делая его более громоздкой. + * + * Поэтому пока используем только при нехватке слотов. */ + const size_t maxspan = pnl_maxspan(txn->wr.repnl); + tASSERT(txn, maxspan > 0); + size_t start_lp = txn->env->maxgc_large1page /* начало набольшой страницы, с заголовком */; + size_t tail_lp = + ((maxspan - 1) << txn->env->ps2ln) / sizeof(txnid_t) /* продолжение большой страницы, без заголовка */; + size_t pages4span = maxspan /* кол-во страниц для размещения*/; + size_t chunk = start_lp + tail_lp - pages4span; + TRACE("maxspan %zu, chunk %zu: %zu (start_lp) + %zu (tail_lp) - %zu (pages4span))", maxspan, chunk, start_lp, tail_lp, + pages4span); + return chunk; +} + +static int gc_clean_stored_retired(MDBX_txn *txn, gcu_t *ctx) { int err = MDBX_SUCCESS; if (ctx->retired_stored) { - MDBX_cursor *const gc = ptr_disp(txn, sizeof(MDBX_txn)); - tASSERT(txn, txn == txn->env->basal_txn && gc->next == gc); - gc->txn = txn; - gc->dbi_state = txn->dbi_state; - gc->top_and_flags = z_fresh_mark; - gc->next = txn->cursors[FREE_DBI]; - txn->cursors[FREE_DBI] = gc; do { MDBX_val key, val; #if MDBX_ENABLE_BIGFOOT @@ -31,11 +167,12 @@ static int clean_stored_retired(MDBX_txn *txn, gcu_t *ctx) { key.iov_base = &txn->txnid; #endif /* MDBX_ENABLE_BIGFOOT */ key.iov_len = sizeof(txnid_t); - const csr_t csr = cursor_seek(gc, &key, &val, MDBX_SET); + const csr_t csr = cursor_seek(&ctx->cursor, &key, &val, MDBX_SET); if (csr.err == MDBX_SUCCESS && csr.exact) { ctx->retired_stored = 0; - err = cursor_del(gc, 0); - TRACE("== clear-4linear, backlog %zu, err %d", backlog_size(txn), err); + err = cursor_del(&ctx->cursor, 0); + TRACE("== clear-4linear @%" PRIaTXN ", stockpile %zu, err %d", *(txnid_t *)key.iov_base, gc_stockpile(txn), + err); } else err = (csr.err == MDBX_NOTFOUND) ? MDBX_SUCCESS : csr.err; } @@ -44,13 +181,11 @@ static int clean_stored_retired(MDBX_txn *txn, gcu_t *ctx) { #else while (0); #endif /* MDBX_ENABLE_BIGFOOT */ - txn->cursors[FREE_DBI] = gc->next; - gc->next = gc; } return err; } -static int touch_gc(gcu_t *ctx) { +static int gc_touch(gcu_t *ctx) { tASSERT(ctx->cursor.txn, is_pointed(&ctx->cursor) || ctx->cursor.txn->dbs[FREE_DBI].leaf_pages == 0); MDBX_val key, val; key.iov_base = val.iov_base = nullptr; @@ -62,94 +197,101 @@ static int touch_gc(gcu_t *ctx) { return err; } -/* Prepare a backlog of pages to modify GC itself, while reclaiming is - * prohibited. It should be enough to prevent search in gc_alloc_ex() - * during a deleting, when GC tree is unbalanced. */ -static int prepare_backlog(MDBX_txn *txn, gcu_t *ctx) { - const size_t for_cow = txn->dbs[FREE_DBI].height; - const size_t for_rebalance = for_cow + 1 + (txn->dbs[FREE_DBI].height + 1ul >= txn->dbs[FREE_DBI].branch_pages); - size_t for_split = ctx->retired_stored == 0; - tASSERT(txn, is_pointed(&ctx->cursor) || txn->dbs[FREE_DBI].leaf_pages == 0); +static inline int gc_reclaim_slot(MDBX_txn *txn, gcu_t *ctx) { + (void)txn; + return gc_alloc_ex(&ctx->cursor, 0, ALLOC_RESERVE | ALLOC_UNIMPORTANT).err; +} - const intptr_t retired_left = MDBX_PNL_SIZEOF(txn->wr.retired_pages) - ctx->retired_stored; - size_t for_repnl = 0; - if (MDBX_ENABLE_BIGFOOT && retired_left > 0) { - for_repnl = (retired_left + txn->env->maxgc_large1page - 1) / txn->env->maxgc_large1page; - const size_t per_branch_page = txn->env->maxgc_per_branch; - for (size_t entries = for_repnl; entries > 1; for_split += entries) - entries = (entries + per_branch_page - 1) / per_branch_page; - } else if (!MDBX_ENABLE_BIGFOOT && retired_left != 0) { - for_repnl = largechunk_npages(txn->env, MDBX_PNL_SIZEOF(txn->wr.retired_pages)); - } +static inline int gc_reserve4retired(MDBX_txn *txn, gcu_t *ctx, size_t sequence_length) { + (void)txn; + return gc_alloc_ex(&ctx->cursor, sequence_length, ALLOC_RESERVE | ALLOC_UNIMPORTANT).err; +} - const size_t for_tree_before_touch = for_cow + for_rebalance + for_split; - const size_t for_tree_after_touch = for_rebalance + for_split; - const size_t for_all_before_touch = for_repnl + for_tree_before_touch; - const size_t for_all_after_touch = for_repnl + for_tree_after_touch; +static inline int gc_reserve4stockpile(MDBX_txn *txn, gcu_t *ctx) { + (void)txn; + return gc_alloc_ex(&ctx->cursor, 1, ALLOC_RESERVE | ALLOC_UNIMPORTANT).err; +} - if (likely(for_repnl < 2 && backlog_size(txn) > for_all_before_touch) && - (ctx->cursor.top < 0 || is_modifable(txn, ctx->cursor.pg[ctx->cursor.top]))) - return MDBX_SUCCESS; +static int gc_prepare_stockpile(MDBX_txn *txn, gcu_t *ctx, const size_t for_retired) { + for (;;) { + tASSERT(txn, is_pointed(&ctx->cursor) || txn->dbs[FREE_DBI].leaf_pages == 0); - TRACE(">> retired-stored %zu, left %zi, backlog %zu, need %zu (4list %zu, " - "4split %zu, " - "4cow %zu, 4tree %zu)", - ctx->retired_stored, retired_left, backlog_size(txn), for_all_before_touch, for_repnl, for_split, for_cow, - for_tree_before_touch); + const size_t for_cow = txn->dbs[FREE_DBI].height; + const size_t for_rebalance = for_cow + 1 + (txn->dbs[FREE_DBI].height + 1ul >= txn->dbs[FREE_DBI].branch_pages); + const size_t for_tree_before_touch = for_cow + for_rebalance; + const size_t for_tree_after_touch = for_rebalance; + const size_t for_all_before_touch = for_retired + for_tree_before_touch; + const size_t for_all_after_touch = for_retired + for_tree_after_touch; - int err = touch_gc(ctx); - TRACE("== after-touch, backlog %zu, err %d", backlog_size(txn), err); + if (likely(for_retired < 2 && gc_stockpile(txn) > for_all_before_touch)) + return MDBX_SUCCESS; - if (!MDBX_ENABLE_BIGFOOT && unlikely(for_repnl > 1) && - MDBX_PNL_GETSIZE(txn->wr.retired_pages) != ctx->retired_stored && err == MDBX_SUCCESS) { - if (unlikely(ctx->retired_stored)) { - err = clean_stored_retired(txn, ctx); - if (unlikely(err != MDBX_SUCCESS)) - return err; - if (!ctx->retired_stored) - return /* restart by tail-recursion */ prepare_backlog(txn, ctx); + TRACE(">> retired-stored %zu, retired-left %zi, stockpile %zu, now-need %zu (4list %zu, " + "4cow %zu, 4tree %zu)", + ctx->retired_stored, MDBX_PNL_GETSIZE(txn->wr.retired_pages) - ctx->retired_stored, gc_stockpile(txn), + for_all_before_touch, for_retired, for_cow, for_tree_before_touch); + + int err = gc_touch(ctx); + TRACE("== after-touch, stockpile %zu, err %d", gc_stockpile(txn), err); + + if (!MDBX_ENABLE_BIGFOOT && unlikely(for_retired > 1) && + MDBX_PNL_GETSIZE(txn->wr.retired_pages) != ctx->retired_stored && err == MDBX_SUCCESS) { + if (unlikely(ctx->retired_stored)) { + err = gc_clean_stored_retired(txn, ctx); + if (unlikely(err != MDBX_SUCCESS)) + return err; + if (!ctx->retired_stored) + continue; + } + err = gc_reserve4retired(txn, ctx, for_retired); + TRACE("== after-4linear, stockpile %zu, err %d", gc_stockpile(txn), err); + cASSERT(&ctx->cursor, gc_stockpile(txn) >= for_retired || err != MDBX_SUCCESS); } - err = gc_alloc_ex(&ctx->cursor, for_repnl, ALLOC_RESERVE).err; - TRACE("== after-4linear, backlog %zu, err %d", backlog_size(txn), err); - cASSERT(&ctx->cursor, backlog_size(txn) >= for_repnl || err != MDBX_SUCCESS); + + while (gc_stockpile(txn) < for_all_after_touch && err == MDBX_SUCCESS) + err = gc_reserve4stockpile(txn, ctx); + + TRACE("<< stockpile %zu, err %d, gc: height %u, branch %zu, leaf %zu, large " + "%zu, entries %zu", + gc_stockpile(txn), err, txn->dbs[FREE_DBI].height, (size_t)txn->dbs[FREE_DBI].branch_pages, + (size_t)txn->dbs[FREE_DBI].leaf_pages, (size_t)txn->dbs[FREE_DBI].large_pages, + (size_t)txn->dbs[FREE_DBI].items); + return (err != MDBX_NOTFOUND) ? err : MDBX_SUCCESS; + } +} + +static int gc_prepare_stockpile4update(MDBX_txn *txn, gcu_t *ctx) { return gc_prepare_stockpile(txn, ctx, 0); } + +static int gc_prepare_stockpile4retired(MDBX_txn *txn, gcu_t *ctx) { + const size_t retired_whole = MDBX_PNL_GETSIZE(txn->wr.retired_pages); + const intptr_t retired_left = retired_whole - ctx->retired_stored; + size_t for_retired = 0; + if (retired_left > 0) { + if (unlikely(!ctx->retired_stored)) { + /* Make sure last page of GC is touched and on retired-list */ + int err = outer_last(&ctx->cursor, nullptr, nullptr); + if (unlikely(err != MDBX_SUCCESS) && err != MDBX_NOTFOUND) + return err; + for_retired += 1; + } + if (MDBX_ENABLE_BIGFOOT) { + const size_t per_branch_page = txn->env->maxgc_per_branch; + for_retired += (retired_left + ctx->goodchunk - 1) / ctx->goodchunk; + for (size_t entries = for_retired; entries > 1; for_retired += entries) + entries = (entries + per_branch_page - 1) / per_branch_page; + } else + for_retired += largechunk_npages(txn->env, retired_whole); } - while (backlog_size(txn) < for_all_after_touch && err == MDBX_SUCCESS) - err = gc_alloc_ex(&ctx->cursor, 0, ALLOC_RESERVE | ALLOC_UNIMPORTANT).err; - - TRACE("<< backlog %zu, err %d, gc: height %u, branch %zu, leaf %zu, large " - "%zu, entries %zu", - backlog_size(txn), err, txn->dbs[FREE_DBI].height, (size_t)txn->dbs[FREE_DBI].branch_pages, - (size_t)txn->dbs[FREE_DBI].leaf_pages, (size_t)txn->dbs[FREE_DBI].large_pages, - (size_t)txn->dbs[FREE_DBI].items); - tASSERT(txn, err != MDBX_NOTFOUND || (txn->flags & txn_gc_drained) != 0); - return (err != MDBX_NOTFOUND) ? err : MDBX_SUCCESS; + return gc_prepare_stockpile(txn, ctx, for_retired); } -static inline void zeroize_reserved(const MDBX_env *env, MDBX_val pnl) { -#if MDBX_DEBUG && (defined(ENABLE_MEMCHECK) || defined(__SANITIZE_ADDRESS__)) - /* Для предотвращения предупреждения Valgrind из mdbx_dump_val() - * вызванное через макрос DVAL_DEBUG() на выходе - * из cursor_seek(MDBX_SET_KEY), которая вызывается ниже внутри gc_update() в - * цикле очистки и цикле заполнения зарезервированных элементов. */ - memset(pnl.iov_base, 0xBB, pnl.iov_len); -#endif /* MDBX_DEBUG && (ENABLE_MEMCHECK || __SANITIZE_ADDRESS__) */ - - /* PNL is initially empty, zero out at least the length */ - memset(pnl.iov_base, 0, sizeof(pgno_t)); - if ((env->flags & (MDBX_WRITEMAP | MDBX_NOMEMINIT)) == 0) - /* zero out to avoid leaking values from uninitialized malloc'ed memory - * to the file in non-writemap mode if length of the saving page-list - * was changed during space reservation. */ - memset(pnl.iov_base, 0, pnl.iov_len); -} - -static int gcu_loose(MDBX_txn *txn, gcu_t *ctx) { +static int gc_merge_loose(MDBX_txn *txn, gcu_t *ctx) { tASSERT(txn, txn->wr.loose_count > 0); /* Return loose page numbers to wr.repnl, though usually none are left at this point. * The pages themselves remain in dirtylist. */ - if (unlikely(!txn->wr.gc.retxl && txn->wr.gc.last_reclaimed < 1)) { - /* Put loose page numbers in wr.retired_pages, since unable to return ones to wr.repnl. */ + if (unlikely(!(txn->dbi_state[FREE_DBI] & DBI_DIRTY)) && txn->wr.loose_count < 3 + (unsigned)txn->dbs->height * 2) { + /* Put loose page numbers in wr.retired_pages, since unreasonable to return ones to wr.repnl. */ TRACE("%s: merge %zu loose-pages into %s-pages", dbg_prefix(ctx), txn->wr.loose_count, "retired"); int err = pnl_need(&txn->wr.retired_pages, txn->wr.loose_count); if (unlikely(err != MDBX_SUCCESS)) @@ -219,73 +361,60 @@ static int gcu_loose(MDBX_txn *txn, gcu_t *ctx) { return MDBX_SUCCESS; } -static int gcu_retired(MDBX_txn *txn, gcu_t *ctx) { +static int gc_store_retired(MDBX_txn *txn, gcu_t *ctx) { int err; - if (unlikely(!ctx->retired_stored)) { - /* Make sure last page of GC is touched and on retired-list */ - err = outer_last(&ctx->cursor, nullptr, nullptr); - if (likely(err == MDBX_SUCCESS)) - err = touch_gc(ctx); - if (unlikely(err != MDBX_SUCCESS) && err != MDBX_NOTFOUND) - return err; - } - MDBX_val key, data; + #if MDBX_ENABLE_BIGFOOT - size_t retired_pages_before; + size_t retired_before; + bool should_retry; do { if (ctx->bigfoot > txn->txnid) { - err = clean_stored_retired(txn, ctx); + err = gc_clean_stored_retired(txn, ctx); if (unlikely(err != MDBX_SUCCESS)) return err; tASSERT(txn, ctx->bigfoot <= txn->txnid); } - retired_pages_before = MDBX_PNL_GETSIZE(txn->wr.retired_pages); - err = prepare_backlog(txn, ctx); + err = gc_prepare_stockpile4retired(txn, ctx); if (unlikely(err != MDBX_SUCCESS)) return err; - if (retired_pages_before != MDBX_PNL_GETSIZE(txn->wr.retired_pages)) { - TRACE("%s: retired-list changed (%zu -> %zu), retry", dbg_prefix(ctx), retired_pages_before, - MDBX_PNL_GETSIZE(txn->wr.retired_pages)); - break; - } pnl_sort(txn->wr.retired_pages, txn->geo.first_unallocated); + retired_before = MDBX_PNL_GETSIZE(txn->wr.retired_pages); + should_retry = false; ctx->retired_stored = 0; ctx->bigfoot = txn->txnid; do { if (ctx->retired_stored) { - err = prepare_backlog(txn, ctx); + err = gc_prepare_stockpile4retired(txn, ctx); if (unlikely(err != MDBX_SUCCESS)) return err; - if (ctx->retired_stored >= MDBX_PNL_GETSIZE(txn->wr.retired_pages)) { - TRACE("%s: retired-list changed (%zu -> %zu), retry", dbg_prefix(ctx), retired_pages_before, - MDBX_PNL_GETSIZE(txn->wr.retired_pages)); - break; - } } key.iov_len = sizeof(txnid_t); key.iov_base = &ctx->bigfoot; - const size_t left = MDBX_PNL_GETSIZE(txn->wr.retired_pages) - ctx->retired_stored; - const size_t chunk = - (left > txn->env->maxgc_large1page && ctx->bigfoot < MAX_TXNID) ? txn->env->maxgc_large1page : left; - data.iov_len = (chunk + 1) * sizeof(pgno_t); + const size_t left_before = retired_before - ctx->retired_stored; + const size_t chunk_hi = ((left_before | 3) > ctx->goodchunk && ctx->bigfoot < (MAX_TXNID - UINT32_MAX)) + ? ctx->goodchunk + : (left_before | 3); + data.iov_len = (chunk_hi + 1) * sizeof(pgno_t); err = cursor_put(&ctx->cursor, &key, &data, MDBX_RESERVE); if (unlikely(err != MDBX_SUCCESS)) return err; #if MDBX_DEBUG && (defined(ENABLE_MEMCHECK) || defined(__SANITIZE_ADDRESS__)) - /* Для предотвращения предупреждения Valgrind из mdbx_dump_val() - * вызванное через макрос DVAL_DEBUG() на выходе - * из cursor_seek(MDBX_SET_KEY), которая вызывается как выше в цикле - * очистки, так и ниже в цикле заполнения зарезервированных элементов. - */ + /* Для предотвращения предупреждения Valgrind из mdbx_dump_val() вызванное через макрос DVAL_DEBUG() на выходе из + * cursor_seek(MDBX_SET_KEY), которая вызывается как выше в цикле очистки, так и ниже в цикле заполнения + * зарезервированных элементов. */ memset(data.iov_base, 0xBB, data.iov_len); #endif /* MDBX_DEBUG && (ENABLE_MEMCHECK || __SANITIZE_ADDRESS__) */ - if (retired_pages_before == MDBX_PNL_GETSIZE(txn->wr.retired_pages)) { - const size_t at = (is_lifo(txn) == MDBX_PNL_ASCENDING) ? left - chunk : ctx->retired_stored; + const size_t retired_after = MDBX_PNL_GETSIZE(txn->wr.retired_pages); + const size_t left_after = retired_after - ctx->retired_stored; + const size_t chunk = (left_after < chunk_hi) ? left_after : chunk_hi; + should_retry = retired_before != retired_after && chunk < retired_after; + if (likely(!should_retry)) { + const size_t at = (is_lifo(txn) == MDBX_PNL_ASCENDING) ? left_before - chunk : ctx->retired_stored; pgno_t *const begin = txn->wr.retired_pages + at; /* MDBX_PNL_ASCENDING == false && LIFO == false: * - the larger pgno is at the beginning of retired list @@ -298,17 +427,17 @@ static int gcu_retired(MDBX_txn *txn, gcu_t *ctx) { memcpy(data.iov_base, begin, data.iov_len); *begin = save; TRACE("%s: put-retired/bigfoot @ %" PRIaTXN " (slice #%u) #%zu [%zu..%zu] of %zu", dbg_prefix(ctx), - ctx->bigfoot, (unsigned)(ctx->bigfoot - txn->txnid), chunk, at, at + chunk, retired_pages_before); + ctx->bigfoot, (unsigned)(ctx->bigfoot - txn->txnid), chunk, at, at + chunk, retired_before); } ctx->retired_stored += chunk; } while (ctx->retired_stored < MDBX_PNL_GETSIZE(txn->wr.retired_pages) && (++ctx->bigfoot, true)); - } while (retired_pages_before != MDBX_PNL_GETSIZE(txn->wr.retired_pages)); + } while (unlikely(should_retry)); #else /* Write to last page of GC */ key.iov_len = sizeof(txnid_t); key.iov_base = &txn->txnid; do { - prepare_backlog(txn, ctx); + gc_prepare_stockpile4retired(txn, ctx); data.iov_len = MDBX_PNL_SIZEOF(txn->wr.retired_pages); err = cursor_put(&ctx->cursor, &key, &data, MDBX_RESERVE); if (unlikely(err != MDBX_SUCCESS)) @@ -332,7 +461,7 @@ static int gcu_retired(MDBX_txn *txn, gcu_t *ctx) { TRACE("%s: put-retired #%zu @ %" PRIaTXN, dbg_prefix(ctx), ctx->retired_stored, txn->txnid); #endif /* MDBX_ENABLE_BIGFOOT */ - if (LOG_ENABLED(MDBX_LOG_EXTRA)) { + if (MDBX_DEBUG_GCU < 2 && LOG_ENABLED(MDBX_LOG_EXTRA)) { size_t i = ctx->retired_stored; DEBUG_EXTRA("txn %" PRIaTXN " root %" PRIaPGNO " num %zu, retired-PNL", txn->txnid, txn->dbs[FREE_DBI].root, i); for (; i; i--) @@ -342,304 +471,914 @@ static int gcu_retired(MDBX_txn *txn, gcu_t *ctx) { return MDBX_SUCCESS; } -typedef struct gcu_rid_result { - int err; - txnid_t rid; -} rid_t; - -static rid_t get_rid_for_reclaimed(MDBX_txn *txn, gcu_t *ctx, const size_t left) { - rid_t r; - if (is_lifo(txn)) { - if (txn->wr.gc.retxl == nullptr) { - txn->wr.gc.retxl = txl_alloc(); - if (unlikely(!txn->wr.gc.retxl)) { - r.err = MDBX_ENOMEM; - goto return_error; - } +static int gc_remove_rkl(MDBX_txn *txn, gcu_t *ctx, rkl_t *rkl) { + while (!rkl_empty(rkl)) { + txnid_t id = rkl_edge(rkl, is_lifo(txn)); + if (ctx->gc_first == id) + ctx->gc_first = 0; + tASSERT(txn, id <= txn->env->lck->cached_oldest.weak); + MDBX_val key = {.iov_base = &id, .iov_len = sizeof(id)}; + int err = cursor_seek(&ctx->cursor, &key, nullptr, MDBX_SET).err; + tASSERT(txn, id == rkl_edge(rkl, is_lifo(txn))); + if (err == MDBX_NOTFOUND) { + err = rkl_push(&ctx->ready4reuse, rkl_pop(rkl, is_lifo(txn)), false); + WARNING("unexpected %s for gc-id %" PRIaTXN ", ignore and continue, push-err %d", "MDBX_NOTFOUND", id, err); + if (unlikely(MDBX_IS_ERROR(err))) + return err; + continue; } - if (MDBX_PNL_GETSIZE(txn->wr.gc.retxl) < txl_max && - left > (MDBX_PNL_GETSIZE(txn->wr.gc.retxl) - ctx->reused_slot) * txn->env->maxgc_large1page && !ctx->dense) { - /* Hужен свободный для для сохранения списка страниц. */ - bool need_cleanup = false; - txnid_t snap_oldest = 0; - retry_rid: - do { - r.err = gc_alloc_ex(&ctx->cursor, 0, ALLOC_RESERVE).err; - snap_oldest = txn->env->lck->cached_oldest.weak; - if (likely(r.err == MDBX_SUCCESS)) { - TRACE("%s: took @%" PRIaTXN " from GC", dbg_prefix(ctx), MDBX_PNL_LAST(txn->wr.gc.retxl)); - need_cleanup = true; - } - } while (r.err == MDBX_SUCCESS && MDBX_PNL_GETSIZE(txn->wr.gc.retxl) < txl_max && - left > (MDBX_PNL_GETSIZE(txn->wr.gc.retxl) - ctx->reused_slot) * txn->env->maxgc_large1page); + if (unlikely(err != MDBX_SUCCESS)) + return err; - if (likely(r.err == MDBX_SUCCESS)) { - TRACE("%s: got enough from GC.", dbg_prefix(ctx)); - goto return_continue; - } else if (unlikely(r.err != MDBX_NOTFOUND)) - /* LY: some troubles... */ - goto return_error; - - if (MDBX_PNL_GETSIZE(txn->wr.gc.retxl)) { - if (need_cleanup) { - txl_sort(txn->wr.gc.retxl); - ctx->cleaned_slot = 0; - } - ctx->rid = MDBX_PNL_LAST(txn->wr.gc.retxl); - } else { - tASSERT(txn, txn->wr.gc.last_reclaimed == 0); - if (unlikely(txn_snapshot_oldest(txn) != snap_oldest)) - /* should retry gc_alloc_ex() - * if the oldest reader changes since the last attempt */ - goto retry_rid; - /* no reclaimable GC entries, - * therefore no entries with ID < mdbx_find_oldest(txn) */ - txn->wr.gc.last_reclaimed = ctx->rid = snap_oldest; - TRACE("%s: none recycled yet, set rid to @%" PRIaTXN, dbg_prefix(ctx), ctx->rid); - } - - /* В GC нет годных к переработке записей, - * будем использовать свободные id в обратном порядке. */ - while (MDBX_PNL_GETSIZE(txn->wr.gc.retxl) < txl_max && - left > (MDBX_PNL_GETSIZE(txn->wr.gc.retxl) - ctx->reused_slot) * txn->env->maxgc_large1page) { - if (unlikely(ctx->rid <= MIN_TXNID)) { - ctx->dense = true; - if (unlikely(MDBX_PNL_GETSIZE(txn->wr.gc.retxl) <= ctx->reused_slot)) { - VERBOSE("** restart: reserve depleted (reused_gc_slot %zu >= " - "gc.reclaimed %zu)", - ctx->reused_slot, MDBX_PNL_GETSIZE(txn->wr.gc.retxl)); - goto return_restart; - } - break; - } - - tASSERT(txn, ctx->rid >= MIN_TXNID && ctx->rid <= MAX_TXNID); - ctx->rid -= 1; - MDBX_val key = {&ctx->rid, sizeof(ctx->rid)}, data; - r.err = cursor_seek(&ctx->cursor, &key, &data, MDBX_SET_KEY).err; - if (unlikely(r.err == MDBX_SUCCESS)) { - DEBUG("%s: GC's id %" PRIaTXN " is present, going to first", dbg_prefix(ctx), ctx->rid); - r.err = outer_first(&ctx->cursor, &key, nullptr); - if (unlikely(r.err != MDBX_SUCCESS || key.iov_len != sizeof(txnid_t))) { - ERROR("%s/%d: %s %u", "MDBX_CORRUPTED", MDBX_CORRUPTED, "invalid GC-key size", (unsigned)key.iov_len); - r.err = MDBX_CORRUPTED; - goto return_error; - } - const txnid_t gc_first = unaligned_peek_u64(4, key.iov_base); - if (unlikely(gc_first <= INITIAL_TXNID)) { - NOTICE("%s: no free GC's id(s) less than %" PRIaTXN " (going dense-mode)", dbg_prefix(ctx), ctx->rid); - ctx->dense = true; - goto return_restart; - } - ctx->rid = gc_first - 1; - } - - tASSERT(txn, !ctx->dense); - r.err = txl_append(&txn->wr.gc.retxl, ctx->rid); - if (unlikely(r.err != MDBX_SUCCESS)) - goto return_error; - - if (ctx->reused_slot) - /* rare case, but it is better to clear and re-create GC entries - * with less fragmentation. */ - need_cleanup = true; - else - ctx->cleaned_slot += 1 /* mark cleanup is not needed for added slot. */; - - TRACE("%s: append @%" PRIaTXN " to lifo-reclaimed, cleaned-gc-slot = %zu", dbg_prefix(ctx), ctx->rid, - ctx->cleaned_slot); - } - - if (need_cleanup) { - if (ctx->cleaned_slot) { - TRACE("%s: restart to clear and re-create GC entries", dbg_prefix(ctx)); - goto return_restart; - } - goto return_continue; - } + err = gc_prepare_stockpile4update(txn, ctx); + if (unlikely(err != MDBX_SUCCESS)) + return err; + if (unlikely(id != rkl_edge(rkl, is_lifo(txn)))) { + TRACE("id %" PRIaTXN " not at edge, continue", id); + continue; } - - const size_t i = MDBX_PNL_GETSIZE(txn->wr.gc.retxl) - ctx->reused_slot; - tASSERT(txn, i > 0 && i <= MDBX_PNL_GETSIZE(txn->wr.gc.retxl)); - r.rid = txn->wr.gc.retxl[i]; - TRACE("%s: take @%" PRIaTXN " from lifo-reclaimed[%zu]", dbg_prefix(ctx), r.rid, i); - } else { - tASSERT(txn, txn->wr.gc.retxl == nullptr); - if (unlikely(ctx->rid == 0)) { - ctx->rid = txn_snapshot_oldest(txn); - MDBX_val key; - r.err = outer_first(&ctx->cursor, &key, nullptr); - if (likely(r.err == MDBX_SUCCESS)) { - if (unlikely(key.iov_len != sizeof(txnid_t))) { - ERROR("%s/%d: %s %u", "MDBX_CORRUPTED", MDBX_CORRUPTED, "invalid GC-key size", (unsigned)key.iov_len); - r.err = MDBX_CORRUPTED; - goto return_error; - } - const txnid_t gc_first = unaligned_peek_u64(4, key.iov_base); - if (ctx->rid >= gc_first && gc_first) - ctx->rid = gc_first - 1; - if (unlikely(ctx->rid <= MIN_TXNID)) { - ERROR("%s", "** no GC tail-space to store (going dense-mode)"); - ctx->dense = true; - goto return_restart; - } - } else if (r.err != MDBX_NOTFOUND) { - r.rid = 0; - return r; - } - txn->wr.gc.last_reclaimed = ctx->rid; - ctx->cleaned_id = ctx->rid + 1; - } - r.rid = ctx->rid--; - TRACE("%s: take @%" PRIaTXN " from GC", dbg_prefix(ctx), r.rid); + err = cursor_del(&ctx->cursor, 0); + if (unlikely(err != MDBX_SUCCESS)) + return err; + ENSURE(txn->env, id == rkl_pop(rkl, is_lifo(txn))); + tASSERT(txn, id <= txn->env->lck->cached_oldest.weak); + err = rkl_push(&ctx->ready4reuse, id, false); + if (unlikely(err != MDBX_SUCCESS)) + return err; + TRACE("id %" PRIaTXN " cleared and moved to ready4reuse", id); } - ++ctx->reused_slot; - r.err = MDBX_SUCCESS; - return r; - -return_continue: - r.err = MDBX_SUCCESS; - r.rid = 0; - return r; - -return_restart: - r.err = MDBX_RESULT_TRUE; - r.rid = 0; - return r; - -return_error: - tASSERT(txn, r.err != MDBX_SUCCESS); - r.rid = 0; - return r; + return MDBX_SUCCESS; } -/* Cleanups retxl GC (aka freeDB) records, saves the retired-list (aka - * freelist) of current transaction to GC, puts back into GC leftover of the - * retxl pages with chunking. This recursive changes the retxl-list, - * loose-list and retired-list. Keep trying until it stabilizes. +static inline int gc_clear_reclaimed(MDBX_txn *txn, gcu_t *ctx) { + return gc_remove_rkl(txn, ctx, &txn->wr.gc.reclaimed); +} + +static inline int gc_clear_returned(MDBX_txn *txn, gcu_t *ctx) { + ctx->return_reserved_lo = 0; + ctx->return_reserved_hi = 0; + return gc_remove_rkl(txn, ctx, &txn->wr.gc.comeback); +} + +static int gc_push_sequel(MDBX_txn *txn, gcu_t *ctx, txnid_t id) { + tASSERT(txn, id > 0 && id < txn->env->gc.detent); + tASSERT(txn, !rkl_contain(&txn->wr.gc.comeback, id) && !rkl_contain(&ctx->ready4reuse, id)); + TRACE("id %" PRIaTXN ", return-left %zi", id, ctx->return_left); + int err = rkl_push(&ctx->sequel, id, false); + if (unlikely(err != MDBX_SUCCESS)) { + if (err == MDBX_RESULT_TRUE) { + ERROR("%s/%d: %s", "MDBX_PROBLEM", MDBX_PROBLEM, "unexpected duplicate(s) during rkl-push"); + err = MDBX_PROBLEM; + } + return err; + } + ctx->return_left -= ctx->goodchunk; + return (ctx->return_left <= 0) ? MDBX_RESULT_TRUE : MDBX_RESULT_FALSE; +} + +/* Строит гистограмму длин последовательностей соседствующих/примыкающих страниц */ +static void gc_dense_hist(MDBX_txn *txn, gcu_t *ctx) { + memset(&ctx->dense_histogram, 0, sizeof(ctx->dense_histogram)); + size_t seqlen = 0, seqmax = 1; + for (size_t i = 2; i <= MDBX_PNL_GETSIZE(txn->wr.repnl); ++i) { + seqlen += 1; + if (seqlen == ARRAY_LENGTH(ctx->dense_histogram.array) || + !MDBX_PNL_CONTIGUOUS(txn->wr.repnl[i - 1], txn->wr.repnl[i], 1)) { + ctx->dense_histogram.array[seqlen - 1] += 1; + seqmax = (seqmax >= seqlen) ? seqmax : seqlen; + seqlen = 0; + } + } + ctx->dense_histogram.array[seqlen] += 1; + ctx->dense_histogram.end = (unsigned)((seqmax > seqlen) ? seqmax : seqlen + 1); +} + +/* Оптимальным решением является использование всех доступных слотов/идентификаторов, при максимальном использовании + * последовательностей длиной ближе к целевой средней длине, необходимой для размещения всех возвращаемых страниц. * - * NOTE: This code is a consequence of many iterations of adding crutches (aka - * "checks and balances") to partially bypass the fundamental design problems - * inherited from LMDB. So do not try to understand it completely in order to - * avoid your madness. */ + * Если последовательностей нужной или большей длины хватает, то достаточно просто выполнить соответствующую нарезку. + * Иначе поиск решения можно рассматривать как необходимую замену в наборе коротких (в том числе нулевых) + * последовательностей/кусков более длинными. Сложность в том, что нужно учитывать возможность разбиения/деления длинных + * последовательностей на несколько более коротких. + * + * Поэтому алгоритмически поиск решения выглядит как попытка сначала нарезать N кусков по L, а в случае неуспеха + * попробовать комбинации X=1..N-1 кусков по L+1 и Y=N-X кусков длины L и меньше (при достижении объёма/суммы), + * затем комбинаций X=1..N-1 кусков по L+2, Y=0..N-X кусков длины L-1 и Z=N-(X+Y) кусков длины L и меньше: + * - a=0..(V/(L+1)) кусков L+1, плюс хвост N-a длиной до L включительно; + * - b=0..(V/(L+2)) кусков L+2, a=0..(V/(L+1)) кусков L+1, плюс хвост N-b-a длиной до L включительно; + * - c=0..(V/(L+3)) кусков L+3, b=0..(V/(L+2)) кусков L+2, a=0..(V/(L+1)) кусков L+1, + * плюс хвост N-c-b-a длиной до L включительно; + * - и т.д. + * + * 1. начинаем с максимальной длины из гистограммы и спускаемся до L + * для каждого уровня начинаем с 0 кусков и одного из условий: + * - исчерпание/отсутствие кусков нужной длины, + * - либо до достижения объёма (что должно привести к возврату решения); + * 2. сначала спускаемся рекурсивно вглубь, затем идем двоичным поиском 0 --> hi на каждом уровне; + * 3. поиск/выделение кусков и восстановление/откат: + * - каждое выделение может быть дробным, т.е. образовывать осколки меньшего размера, которые могут использовать + * последующие шаги; + * - на каждом уровне нужна своя актуальная гистограмма; + * - два варианта: локальная копия гистограммы, либо "дельта" для отката + * - с "дельтой" очень много возни, в том числе условных переходов, + * поэтому проще делать локальные копии и целиком их откатывать. + * + * Максимальная потребность памяти на стеке sizeof(pgno_t)*L*L, где L = максимальная длина последовательности + * учитываемой в гистограмме. Для L=31 получается порядка 4 Кб на стеке, что представляется допустимым, а отслеживание + * более длинных последовательностей не представляется рациональным. + * + * Сложность можно оценить как O(H*N*log(N)), либо как O(H*V*log(N)), где: + * - H = высота гистограммы, + * - N = количество имеющихся слотов/идентификаторов, + * - V = объем/количество не помещающихся номеров страниц. */ + +typedef struct sr_state { + unsigned left_slots; + pgno_t left_volume; + gc_dense_histogram_t hist; +} sr_state_t; + +/* Пытается отъесть n кусков длиной len, двигаясь по гистограмме от больших элементов к меньшим. */ +static bool consume_stack(sr_state_t *const st, const size_t len, size_t n) { + assert(len > 1 && n > 0); + while (st->hist.end >= len) { + if (st->hist.array[st->hist.end - 1] < 1) + st->hist.end -= 1; + else { + if (st->hist.end > len) + st->hist.array[st->hist.end - len - 1] += 1; + st->hist.array[st->hist.end - 1] -= 1; + if (--n == 0) + return true; + } + } + return false; +} + +typedef struct sr_context { + /* расход страниц / ёмкость кусков */ + pgno_t first_page, other_pages; + /* длина последовательностей смежных страниц, при нарезке на куски соответствующей длины, имеющихся + * слотов/идентификаторов хватит для размещения возвращаемых страниц. Нарезать куски большего размера, есть смысл + * только если недостаточно последовательностей такой длины (с учетом более длинных, в том числе кратно длиннее). */ + pgno_t factor; + /* результирующее решение */ + gc_dense_histogram_t *solution; +} sr_context_t; + +/* Пытается покрыть остаток объёма и слотов, кусками длиной не более factor, + * двигаясь по гистограмме от больших элементов к меньшим. */ +static bool consume_remaining(const sr_context_t *const ct, sr_state_t *const st, size_t len) { + pgno_t *const solution = ct->solution->array; + size_t per_chunk = ct->first_page + ct->other_pages * (len - 1); + solution[len - 1] = 0; + if (unlikely(0 >= (int)st->left_volume)) + goto done; + + while (st->hist.end > 0 && st->left_slots > 0) { + if (st->hist.array[st->hist.end - 1]) { + solution[len - 1] += 1; + if (st->hist.end > len) + st->hist.array[st->hist.end - len - 1] += 1; + st->hist.array[st->hist.end - 1] -= 1; + st->left_slots -= 1; + st->left_volume -= per_chunk; + if (0 >= (int)st->left_volume) { + done: + while (--len) + solution[len - 1] = 0; + return true; + } + } else { + st->hist.end -= 1; + if (len > st->hist.end) { + assert(len == st->hist.end + 1); + len = st->hist.end; + per_chunk -= ct->other_pages; + solution[len - 1] = 0; + } + } + } + return false; +} + +/* Поиск оптимального решения путем жадного бинарного деления и рекурсивного спуска по уже посчитанной гистограмме. */ +static bool solve_recursive(const sr_context_t *const ct, sr_state_t *const st, size_t len) { + assert(st->left_slots >= 1); + size_t per_chunk = ct->first_page + ct->other_pages * (len - 1); + if (len > ct->factor && st->left_slots > 1 && st->left_volume > per_chunk) { + unsigned lo = 0, hi = st->left_slots - 1, n = lo; + do { + sr_state_t local = *st; + if (n) { + if (!consume_stack(&local, len, n)) { + hi = n - 1; + n = (hi + lo) / 2; + continue; + } + assert(local.left_slots > n); + local.left_slots -= n; + assert(local.left_volume >= n * per_chunk); + local.left_volume -= n * per_chunk; + } + if (!solve_recursive(ct, &local, len - 1)) { + lo = n + 1; + } else if (n > lo && n < hi) { + hi = n; + } else { + ct->solution->array[len - 1] = n; + *st = local; + return true; + } + n = (hi + lo + 1) / 2; + } while (hi >= lo); + return false; + } + + return consume_remaining(ct, st, len); +} + +static int gc_dense_solve(MDBX_txn *txn, gcu_t *ctx, gc_dense_histogram_t *const solution) { + sr_state_t st = { + .left_slots = rkl_len(&ctx->ready4reuse), .left_volume = ctx->return_left, .hist = ctx->dense_histogram}; + assert(st.left_slots > 0 && st.left_volume > 0 && MDBX_PNL_GETSIZE(txn->wr.repnl) > 0); + if (unlikely(!st.left_slots || !st.left_volume)) { + ERROR("%s/%d: %s", "MDBX_PROBLEM", MDBX_PROBLEM, "recursive-solving preconditions violated"); + return MDBX_PROBLEM; + } + + const sr_context_t ct = {.factor = largechunk_npages( + txn->env, ((st.left_volume + st.left_slots - 1) / st.left_slots + 1) * sizeof(pgno_t)), + .first_page = /* на первой странице */ txn->env->maxgc_large1page + + /* сама страница также будет израсходована */ 1, + .other_pages = /* на второй и последующих страницах */ txn->env->ps / sizeof(pgno_t) + + /* каждая страница также будет израсходована */ 1, + .solution = solution}; + + memset(solution, 0, sizeof(*solution)); + if (solve_recursive(&ct, &st, st.hist.end)) { + const pgno_t *end = ARRAY_END(solution->array); + while (end > solution->array && end[-1] == 0) + --end; + solution->end = (unsigned)(end - solution->array); + + /* проверяем решение */ + size_t items = 0, volume = 0; + for (size_t i = 0, chunk = ct.first_page; i < solution->end; ++i) { + items += solution->array[i]; + volume += solution->array[i] * chunk; + chunk += ct.other_pages; + } + + if (unlikely(volume < (size_t)ctx->return_left || items > rkl_len(&ctx->ready4reuse))) { + assert(!"recursive-solving failure"); + ERROR("%s/%d: %s", "MDBX_PROBLEM", MDBX_PROBLEM, "recursive-solving failure"); + return MDBX_PROBLEM; + } + return MDBX_RESULT_TRUE; + } + + /* решение НЕ найдено */ + return MDBX_RESULT_FALSE; +} + +// int gc_solve_test(MDBX_txn *txn, gcu_t *ctx) { +// gc_dense_histogram_t r; +// gc_dense_histogram_t *const solution = &r; + +// sr_state_t st = {.left_slots = 5, +// .left_volume = 8463, +// .hist = {.end = 31, .array = {6493, 705, 120, 14, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, +// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}}}; +// assert(st.left_slots > 0 && st.left_volume > 0 && MDBX_PNL_GETSIZE(txn->wr.repnl) > 0); +// if (unlikely(!st.left_slots || !st.left_volume)) { +// ERROR("%s/%d: %s", "MDBX_PROBLEM", MDBX_PROBLEM, "recursive-solving preconditions violated"); +// return MDBX_PROBLEM; +// } + +// const sr_context_t ct = {.factor = largechunk_npages( +// txn->env, ((st.left_volume + st.left_slots - 1) / st.left_slots + 1) * +// sizeof(pgno_t)), +// .first_page = /* на первой странице */ txn->env->maxgc_large1page + +// /* сама страница также будет израсходована */ 1, +// .other_pages = /* на второй и последующих страницах */ txn->env->ps / sizeof(pgno_t) + +// /* каждая страница также будет израсходована */ 1, +// .solution = solution}; + +// memset(solution, 0, sizeof(*solution)); +// if (solve_recursive(&ct, &st, st.hist.end)) { +// const pgno_t *end = ARRAY_END(solution->array); +// while (end > solution->array && end[-1] == 0) +// --end; +// solution->end = (unsigned)(end - solution->array); + +// /* проверяем решение */ +// size_t items = 0, volume = 0; +// for (size_t i = 0, chunk = ct.first_page; i < solution->end; ++i) { +// items += solution->array[i]; +// volume += solution->array[i] * chunk; +// chunk += ct.other_pages; +// } + +// if (unlikely(volume < (size_t)ctx->return_left || items > rkl_len(&ctx->ready4reuse))) { +// assert(!"recursive-solving failure"); +// ERROR("%s/%d: %s", "MDBX_PROBLEM", MDBX_PROBLEM, "recursive-solving failure"); +// return MDBX_PROBLEM; +// } +// return MDBX_RESULT_TRUE; +// } + +// /* решение НЕ найдено */ +// return MDBX_RESULT_FALSE; +// } + +/* Ищем свободные/неиспользуемые id в GC, чтобы затем использовать эти идентификаторы для возврата неиспользованных + * остатков номеров страниц ранее изъятых из GC. + * + * Нехватка идентификаторов достаточно редкая ситуация, так как возвращается страниц обычно не более чем было извлечено. + * Однако, больше идентификаторов может потребоваться в следующих ситуациях: + * + * - ранее с БД работала старая версия libmdbx без поддержки BigFoot и поэтому были переработано очень длинные записи, + * возврат остатков которых потребует нарезки на несколько кусков. + * + * - ранее было зафиксировано несколько транзакций, которые помещали в GC retired-списки близкие к максимальному + * размеру помещающемуся на одну листовую страницу, после чего текущая транзакция переработала все эти записи, + * но по совокупности операций эти страницы оказались лишними и теперь при возврате потребуют больше слотов + * из-за обеспечения резерва свободного места при нарезке на множество кусков. + * + * Таким образом, потребность в поиске возникает редко и в большинстве случаев необходимо найти 1-2 свободных + * слота/идентификатора. Если же требуется много слотов, то нет смысла экономить на поиске. */ +static int gc_search_holes(MDBX_txn *txn, gcu_t *ctx) { + tASSERT(txn, ctx->return_left > 0 && txn->env->gc.detent); + tASSERT(txn, rkl_empty(&txn->wr.gc.reclaimed)); + if (!ctx->gc_first) { + ctx->gc_first = txn->env->gc.detent; + if (txn->dbs[FREE_DBI].items) { + MDBX_val key; + int err = outer_first(&ctx->cursor, &key, nullptr); + if (unlikely(err != MDBX_SUCCESS)) + return err; + err = gc_peekid(&key, &ctx->gc_first); + if (unlikely(err != MDBX_SUCCESS)) + return err; + } + } + + /* В LIFO режиме требуется поиск в направлении от новых к старым записям с углублением в потенциально + * рыхлую/неоднородную структуру, с последующим её заполнением возвращаемыми страницами. */ + + /* В FIFO режиме, поиск внутри GC может быть полезным при нелинейной переработке (которая пока не реализована), + * когда будет переработан один из следующих MVCC-снимков без переработки предыдущего. Необходимая для этого + * независимость (отсутствие пересечения) снимков по набору retired-страниц может сложиться при последовательности + * пишущих транзакций изменяющих данные в структурно одних и тех же страницах b-tree. + * + * Однако, в текущем понимании, это крайне редкая ситуация, при которой также весьма вероятно наличие свободного + * интервала в начале GC, а поэтому вероятность выигрыша от дополнительного поиска вперед стремится к нулю. Кроме + * этого, из-за мизерной вероятности ситуаций в которых такой поиск будет работать, его крайне сложно тестировать + * -- требуется разработка отдельного теста, который может быть достаточно хрупким, так как любая доработка + * основного кода может требовать изменений/подстройки сценария теста. + * + * Поэтому пока, до появления явной необходимости и/или пользы, решено отказаться от поиска свободных слотов вглубь GC + * в направлении от старых записей к новым, в том числе в режиме FIFO. */ + + dbg_dump_ids(ctx); + const intptr_t tail_space = + ((ctx->gc_first > UINT16_MAX) ? UINT16_MAX : (unsigned)ctx->gc_first - 1) * ctx->goodchunk; + const txnid_t reasonable_deep = + txn->env->maxgc_per_branch + + 2 * (txn->env->gc.detent - txnid_min(rkl_lowest(&ctx->ready4reuse), rkl_lowest(&txn->wr.gc.comeback))); + const txnid_t scan_threshold = (txn->env->gc.detent > reasonable_deep) ? txn->env->gc.detent - reasonable_deep : 0; + + txnid_t scan_hi = txn->env->gc.detent, scan_lo = INVALID_TXNID; + if (!is_lifo(txn) && ctx->gc_first < txn->env->gc.detent && + txn->env->gc.detent - ctx->gc_first < ctx->cursor.tree->items) { + scan_hi = ctx->gc_first; + scan_lo = 0; + } + + rkl_iter_t iter_ready4reuse, iter_comeback; + rkl_find(&ctx->ready4reuse, scan_hi, &iter_ready4reuse); + rkl_find(&txn->wr.gc.comeback, scan_hi, &iter_comeback); + rkl_hole_t hole_ready4reuse = rkl_hole(&iter_ready4reuse, true); + rkl_hole_t hole_comeback = rkl_hole(&iter_comeback, true); + txnid_t begin, end; + /* Ищем свободные id в GC в направлении от конца (новых записей) к началу (старым записям). */ + do { + TRACE("hole-ready4reuse %" PRIaTXN "..%" PRIaTXN ", hole-comeback %" PRIaTXN "..%" PRIaTXN ", scan-range %" PRIaTXN + "..%" PRIaTXN, + hole_ready4reuse.begin, hole_ready4reuse.end, hole_comeback.begin, hole_comeback.end, scan_lo, scan_hi); + MDBX_val key; + int err; + end = txnid_min(scan_hi, txnid_min(hole_ready4reuse.end, hole_comeback.end)); + if (hole_comeback.begin >= end) { + hole_comeback = rkl_hole(&iter_comeback, true); + TRACE("turn-comeback %" PRIaTXN "..%" PRIaTXN, hole_comeback.begin, hole_comeback.end); + } else if (hole_ready4reuse.begin >= end) { + hole_ready4reuse = rkl_hole(&iter_ready4reuse, true); + TRACE("turn-ready4reuse %" PRIaTXN "..%" PRIaTXN, hole_ready4reuse.begin, hole_ready4reuse.end); + } else if (scan_lo >= end) { + TRACE("turn-scan from %" PRIaTXN "..%" PRIaTXN, scan_lo, scan_hi); + scan_hi = scan_lo - 1; + if (scan_lo - end > 4) { + scan_lo = end - 1; + key.iov_base = &scan_lo; + key.iov_len = sizeof(scan_lo); + const csr_t csr = cursor_seek(&ctx->cursor, &key, nullptr, MDBX_SET_RANGE); + if (csr.err != MDBX_NOTFOUND) { + if (unlikely(csr.err != MDBX_SUCCESS)) + return csr.err; + } + scan_hi = end - csr.exact; + } + goto scan; + } else { + begin = txnid_max(scan_lo, txnid_max(hole_ready4reuse.begin, hole_comeback.begin)); + tASSERT(txn, begin <= scan_hi && begin > 0); + while (--end >= begin) { + err = gc_push_sequel(txn, ctx, end); + tASSERT(txn, (ctx->return_left > 0) == (err != MDBX_RESULT_TRUE)); + if (err != MDBX_SUCCESS) { + return err; + } + } + if (MIN_TXNID >= begin) + break; + if (begin == hole_comeback.begin) { + hole_comeback = rkl_hole(&iter_comeback, true); + TRACE("pull-comeback %" PRIaTXN "..%" PRIaTXN, hole_comeback.begin, hole_comeback.end); + } + if (begin == hole_ready4reuse.begin) { + hole_ready4reuse = rkl_hole(&iter_ready4reuse, true); + TRACE("pull-ready4reuse %" PRIaTXN "..%" PRIaTXN, hole_ready4reuse.begin, hole_ready4reuse.end); + } + if (begin == scan_lo) { + TRACE("pull-scan from %" PRIaTXN "..%" PRIaTXN, scan_lo, scan_hi); + do { + scan_hi = scan_lo - 1; + scan: + if (scan_hi < scan_threshold && tail_space >= ctx->return_left) { + /* Искать глубже нет смысла, ибо в начале GC есть достаточно свободных идентификаторов. */ + TRACE("stop-scan %s", "threshold"); + scan_lo = 0; + scan_hi = ctx->gc_first; + break; + } + err = outer_prev(&ctx->cursor, &key, nullptr, MDBX_PREV); + if (err == MDBX_NOTFOUND) { + /* больше нет записей ближе к началу GC, все значения id свободны */ + TRACE("stop-scan %s", "eof"); + scan_lo = 0; + break; + } + if (unlikely(err != MDBX_SUCCESS)) + return err; + err = gc_peekid(&key, &scan_lo); + if (unlikely(err != MDBX_SUCCESS)) + return err; + TRACE("scan: peek %" PRIaTXN, scan_lo); + scan_lo += 1; + } while (scan_lo >= scan_hi); + TRACE("scan-range %" PRIaTXN "..%" PRIaTXN, scan_lo, scan_hi); + } + } + } while (end > MIN_TXNID); + return MDBX_SUCCESS; +} + +static inline int gc_reserve4return(MDBX_txn *txn, gcu_t *ctx, const size_t chunk_lo, const size_t chunk_hi) { + txnid_t reservation_id = rkl_pop(&ctx->ready4reuse, true); + TRACE("%s: slots-ready4reuse-left %zu, reservation-id %" PRIaTXN, dbg_prefix(ctx), rkl_len(&ctx->ready4reuse), + reservation_id); + tASSERT(txn, reservation_id >= MIN_TXNID && reservation_id < txn->txnid); + tASSERT(txn, reservation_id <= txn->env->lck->cached_oldest.weak); + if (unlikely(reservation_id < MIN_TXNID || + reservation_id > atomic_load64(&txn->env->lck->cached_oldest, mo_Relaxed))) { + ERROR("** internal error (reservation gc-id %" PRIaTXN ")", reservation_id); + return MDBX_PROBLEM; + } + + int err = rkl_push(&txn->wr.gc.comeback, reservation_id, false); + if (unlikely(err != MDBX_SUCCESS)) + return err; + + MDBX_val key = {.iov_base = &reservation_id, .iov_len = sizeof(reservation_id)}; + MDBX_val data = {.iov_base = nullptr, .iov_len = (chunk_hi + 1) * sizeof(pgno_t)}; + TRACE("%s: reserved +%zu...+%zu [%zu...%zu), err %d", dbg_prefix(ctx), chunk_lo, chunk_hi, + ctx->return_reserved_lo + 1, ctx->return_reserved_hi + chunk_hi + 1, err); + gc_prepare_stockpile4update(txn, ctx); + err = cursor_put(&ctx->cursor, &key, &data, MDBX_RESERVE | MDBX_NOOVERWRITE); + tASSERT(txn, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); + if (unlikely(err != MDBX_SUCCESS)) + return err; + + memset(data.iov_base, 0, data.iov_len); + ctx->return_reserved_lo += chunk_lo; + ctx->return_reserved_hi += chunk_hi; + return MDBX_SUCCESS; +} + +static size_t adjust_chunk(const gcu_t *const ctx, const size_t chunk) { + const MDBX_env *const env = ctx->cursor.txn->env; + size_t hi = chunk, lo = chunk - largechunk_npages(env, (chunk + 1) * sizeof(pgno_t)), adjusted = lo; + while (lo < hi) { + adjusted = (hi + lo) / 2; + size_t probe = chunk - largechunk_npages(env, (adjusted + 1) * sizeof(pgno_t)); + if (probe > adjusted) + lo = adjusted + 1; + else if (probe < adjusted) + hi = adjusted - 1; + else + break; + } + return adjusted; +} + +static int gc_handle_dense(MDBX_txn *txn, gcu_t *ctx, size_t left_min, size_t left_max) { + /* Крайне маловероятная ситуация, в текущем понимании не возможная при нормальной/ожидаемой работе всех + * актуальных версий движка. Тем не менее, сюда мы можем попасть при использовании БД с содержимым GC + * оставшимся после старых версий и/или при выключенном BigFoot. Тогда в GC могут быть записи огромного + * размера, при возврате которых мы получаем так много кусков, что в GC не хватает свободных/неиспользуемых + * идентификаторов от прошлых транзакций. + * + * Дальше три возможности: + * 1. Искать в GC доступные для переработки записи короче maxgc_large1page. Это малоэффективный путь, + * так как если мы уже попали в текущую ситуацию, то маловероятно что в GC есть такие записи и что запаса + * места хватит. Поэтому оставляем этот путь в качестве предпоследнего варианта. + * 2. Попытаться запихнуть остаток одним куском, который может быть многократно больше maxgc_large1page, + * т.е. потребуется несколько последовательных свободных страниц, из-за чего может произойти загрузка всей + * GC и т.д. Это плохой путь, который можно использовать только в качестве последнего шанса. + * 3. Искать смежные страницы среди возвращаемых и сохранять куски помещающиеся в такие последовательности. + * + * Поэтому комбинируем все три варианта 3+1+2: + * - Вычисляем среднюю целевую длину куска в large/overflow страницах, при нарезке на которые имеющихся + * слотов/идентификаторов хватит для размещения возвращаемых страниц. + * - В идеале ищем в wr.repnl последовательности смежных страниц длиной от ⌊целевой длины куска⌋ + * до ⌈целевой длины куска⌉ и выполняем резервирование кусками помещающимися в эти последовательности. + * Теоретически, вероятность (а следовательно и количество) последовательностей экспоненциально уменьшается + * с увеличением длины. На практике, в основном, это будут пары и тройки страниц, но также и длинные + * последовательности, которые образуются в исходных больших транзакциях (порождающий большие + * retired-списки), особенно при выделении новых страниц. При этом использование длинных + * последовательностей чревато повторением проблем при переработке созданных сейчас записей. + * - Поэтому оптимальное решение выглядит как поиск набора последовательностей, мощность которого равна + * количеству доступных слотов/идентификаторов, а длины последовательностей минимальны, но достаточны для + * размещения всех возвращаемых страниц. */ + + int err = MDBX_RESULT_FALSE; + if (!rkl_empty(&ctx->ready4reuse)) { + gc_dense_hist(txn, ctx); + gc_dense_histogram_t solution; + ctx->return_left = left_max; + err = gc_dense_solve(txn, ctx, &solution); + if (err == MDBX_RESULT_FALSE /* решение НЕ найдено */ && left_max != left_min) { + ctx->return_left = left_min; + err = gc_dense_solve(txn, ctx, &solution); + } + if (err == MDBX_RESULT_TRUE /* решение найдено */) { + for (size_t i = solution.end; i > 0; --i) + for (pgno_t n = 0; n < solution.array[i - 1]; ++n) { + size_t chunk_hi = txn->env->maxgc_large1page + txn->env->ps / sizeof(pgno_t) * (i - 1); + size_t chunk_lo = chunk_hi - txn->env->maxgc_large1page + ctx->goodchunk; + if (chunk_hi > left_max) + chunk_hi = left_max; + if (chunk_lo > left_min) + chunk_lo = left_min; + TRACE("%s: dense-chunk (seq-len %zu, %d of %d) %zu...%zu, gc-per-ovpage %u", dbg_prefix(ctx), i, n + 1, + solution.array[i - 1], chunk_lo, chunk_hi, txn->env->maxgc_large1page); + err = gc_reserve4return(txn, ctx, chunk_lo, chunk_hi); + if (unlikely(err != MDBX_SUCCESS)) + return err; + const size_t amount = MDBX_PNL_GETSIZE(txn->wr.repnl); + if (ctx->return_reserved_hi >= amount) + return rkl_empty(&txn->wr.gc.reclaimed) ? MDBX_SUCCESS + : MDBX_RESULT_TRUE /* нужно чистить переработанные записи */; + left_min = amount - ctx->return_reserved_hi; + left_max = amount - ctx->return_reserved_lo; + } + } + } else if (rkl_len(&txn->wr.gc.comeback)) + return /* повтор цикла */ MDBX_RESULT_TRUE; + + if (err == MDBX_RESULT_FALSE /* решение НЕ найдено, либо нет идентификаторов */) { + if (ctx->return_left > txn->env->maxgc_large1page) { + err = gc_reclaim_slot(txn, ctx); + if (err == MDBX_NOTFOUND) + err = gc_reserve4retired(txn, ctx, + 1 + (ctx->return_left - txn->env->maxgc_large1page) / (txn->env->ps / sizeof(pgno_t))); + if (err != MDBX_NOTFOUND) { + if (err != MDBX_SUCCESS) + return err; + } + } + if (ctx->loop == 1 && !rkl_empty(&txn->wr.gc.comeback)) + return MDBX_RESULT_TRUE; + + const size_t per_page = txn->env->ps / sizeof(pgno_t); + do { + if (!rkl_empty(&txn->wr.gc.reclaimed) || rkl_empty(&ctx->ready4reuse)) + return MDBX_RESULT_TRUE; + const size_t amount = MDBX_PNL_GETSIZE(txn->wr.repnl); + const size_t reserved = (ctx->return_reserved_hi - ctx->return_reserved_lo > per_page) ? ctx->return_reserved_hi + : ctx->return_reserved_lo; + const size_t slots = rkl_len(&ctx->ready4reuse); + const size_t left = amount - reserved; + const size_t base = (left + slots - 1) / slots; + const size_t adjusted = adjust_chunk(ctx, base); + const size_t predicted = (adjusted > txn->env->maxgc_large1page && + !gc_repnl_has_span(txn, largechunk_npages(txn->env, (adjusted + 1) * sizeof(pgno_t)))) + ? base + : adjusted; + const size_t chunk_hi = + (predicted > txn->env->maxgc_large1page) + ? txn->env->maxgc_large1page + ceil_powerof2(base - txn->env->maxgc_large1page, per_page) + : txn->env->maxgc_large1page; + const size_t chunk_lo = + (adjusted > txn->env->maxgc_large1page) + ? txn->env->maxgc_large1page + floor_powerof2(adjusted - txn->env->maxgc_large1page, per_page) + : adjusted; + err = gc_reserve4return(txn, ctx, chunk_lo, chunk_hi); + } while (err == MDBX_SUCCESS && ctx->return_reserved_hi < MDBX_PNL_GETSIZE(txn->wr.repnl)); + } + + if (unlikely(err != MDBX_SUCCESS)) + ERROR("unable provide IDs and/or to fit returned PNL (%zd+%zd pages, %zd+%zd slots), err %d", ctx->retired_stored, + MDBX_PNL_GETSIZE(txn->wr.repnl), rkl_len(&txn->wr.gc.comeback), rkl_len(&ctx->ready4reuse), err); + return err; +} + +/* Выполняет один шаг резервирования записей для возврата в GC страниц (их номеров), оставшихся после + * переработки GC и последующего использования в транзакции. */ +static int gc_rerere(MDBX_txn *txn, gcu_t *ctx) { + /* При резервировании часть оставшихся страниц может быть использована до полного исчерпания остатка, + * что также может привести к переработке дополнительных записей GC. Таким образом, на каждой итерации + * ситуация может существенно меняться, в том числе может потребоваться очистка резерва и повтор всего цикла. + * + * Кроме этого, теоретически в GC могут быть очень большие записи (созданные старыми версиями движка и/или + * при выключенной опции MDBX_ENABLE_BIGFOOT), которые при возврате будут нарезаться на более мелкие куски. + * В этом случае возвращаемых записей будет больше чем было переработано, поэтому потребуются дополнительные + * идентификаторы/слоты отсутствующие в GC. */ + + // gc_solve_test(txn, ctx); + + tASSERT(txn, rkl_empty(&txn->wr.gc.reclaimed)); + const size_t amount = MDBX_PNL_GETSIZE(txn->wr.repnl); + if (ctx->return_reserved_hi >= amount) { + if (unlikely(ctx->dense)) { + ctx->dense = false; + NOTICE("%s: out of dense-mode (amount %zu, reserved %zu..%zu)", dbg_prefix(ctx), amount, ctx->return_reserved_lo, + ctx->return_reserved_hi); + } + if (unlikely(amount ? (amount + txn->env->maxgc_large1page < ctx->return_reserved_lo) + : (ctx->return_reserved_hi > 3))) { + /* после резервирования было израсходованно слишком много страниц и получилось слишком много резерва */ + TRACE("%s: reclaimed-list %zu < reversed %zu..%zu, retry", dbg_prefix(ctx), amount, ctx->return_reserved_lo, + ctx->return_reserved_hi); + return MDBX_RESULT_TRUE; + } + /* резерва достаточно, ничего делать не надо */ + return MDBX_SUCCESS; + } + + const size_t left_min = amount - ctx->return_reserved_hi; + const size_t left_max = amount - ctx->return_reserved_lo; + if (likely(left_min < txn->env->maxgc_large1page && !rkl_empty(&ctx->ready4reuse))) { + /* Есть хотя-бы один слот и весь остаток списка номеров страниц помещается в один кусок. + * Это самая частая ситуация, просто продолжаем. */ + } else { + if (likely(rkl_len(&ctx->ready4reuse) * ctx->goodchunk >= left_max)) { + /* Слотов хватает, основная задача делить на куски так, чтобы изменение (уменьшение) кол-ва возвращаемых страниц в + * процессе резервирования записей в GC не потребовало менять резервирование, т.е. удалять и повторять всё снова. + */ + } else { + /* Слотов нет, либо не хватает для нарезки возвращаемых страниц кусками по goodchunk */ + ctx->return_left = left_max; + int err = gc_search_holes(txn, ctx); + tASSERT(txn, (ctx->return_left <= 0) == (err == MDBX_RESULT_TRUE)); + if (unlikely(MDBX_IS_ERROR(err))) + return err; + + if (!rkl_empty(&ctx->sequel)) { + err = rkl_merge(&ctx->ready4reuse, &ctx->sequel, false); + if (unlikely(err != MDBX_SUCCESS)) { + if (err == MDBX_RESULT_TRUE) { + ERROR("%s/%d: %s", "MDBX_PROBLEM", MDBX_PROBLEM, "unexpected duplicate(s) during rkl-merge"); + err = MDBX_PROBLEM; + } + return err; + } + rkl_clear(&ctx->sequel); + } + + if (unlikely(ctx->return_left > 0)) { + /* Делаем переоценку баланса для кусков предельного размера (по maxgc_large1page, вместо goodchunk). */ + const intptr_t dense_unfit = left_min - rkl_len(&ctx->ready4reuse) * txn->env->maxgc_large1page; + if (dense_unfit > 0) { + /* Имеющихся идентификаторов НЕ хватит, + * даже если если их использовать для кусков размером maxgc_large1page вместо goodchunk. */ + if (!ctx->dense) { + NOTICE("%s: enter to dense-mode (amount %zu, reserved %zu..%zu, slots/ids %zu, left %zu..%zu, unfit %zu)", + dbg_prefix(ctx), amount, ctx->return_reserved_lo, ctx->return_reserved_hi, + rkl_len(&ctx->ready4reuse), left_min, left_max, dense_unfit); + ctx->dense = true; + } + return gc_handle_dense(txn, ctx, left_min, left_max); + } + } + tASSERT(txn, rkl_empty(&txn->wr.gc.reclaimed)); + } + } + + /* Максимальный размер куска, который помещается на листовой странице, без выноса на отдельную "overflow" страницу. */ + const size_t chunk_inpage = (txn->env->leaf_nodemax - NODESIZE - sizeof(txnid_t)) / sizeof(pgno_t) - 1; + + /* Размер куска помещающийся на одну отдельную "overflow" страницу, но с небольшим запасом сводобного места. */ + const size_t chunk_good = ctx->goodchunk; + + /* Учитываем резервирование по минимальному размеру кусков (chunk_lo), но резервируем слоты с некоторым запасом + * (chunk_hi). При этом предполагая что каждый слот может быть заполнен от chunk_lo до chunk_hi, что обеспечивает + * хорошую амортизацию изменения размера списка возвращаемых страниц, как из-за расходов на создаваемые записи, так и + * из-за переработки GC. */ + const size_t chunk_lo = (left_min < chunk_inpage) ? left_min : chunk_good; + /* Куски размером больше chunk_inpage и до maxgc_large1page включительно требуют одной "overflow" страницы. + * Соответственно требуют одинаковых затрат на обслуживание, а диапазон между chunk_good и maxgc_large1page + * амортизирует изменения кол-ва списка возвращаемых страниц. + * + * Выравниваем размер коротких кусков на 4 (т.е. до 3, с учетом нулевого элемента с длиной), + * а длинных кусков до maxgc_large1page */ + const size_t chunk_hi = (((left_max + 1) | 3) > chunk_inpage) ? txn->env->maxgc_large1page : ((left_max + 1) | 3); + + TRACE("%s: chunk %zu...%zu, gc-per-ovpage %u", dbg_prefix(ctx), chunk_lo, chunk_hi, txn->env->maxgc_large1page); + tASSERT(txn, chunk_lo > 0 && chunk_lo <= chunk_hi && chunk_hi > 1); + return gc_reserve4return(txn, ctx, chunk_lo, chunk_hi); +} + +/* Заполняет зарезервированные записи номерами возвращаемых в GC страниц. */ +static int gc_fill_returned(MDBX_txn *txn, gcu_t *ctx) { + tASSERT(txn, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); + tASSERT(txn, dpl_check(txn)); + + /* Уже есть набор зарезервированных записей GC, id которых собраны в txn->wr.gc.comeback. При этом текущее + * кол-вол возвращаемых страниц (оставшихся после расходов на резервирование) точно помещается в + * эти записи и скорее всего с некоторым запасом. Иначе, если резерва недостаточно или избыток + * резерва неприемлемо велик, то нет другого способа как удалить все созданные записи и повторить + * всё ещё раз, и дальше этот путь здесь не рассматривается. + * + * В большинстве случаев, при резервировании записей переработка GC происходить не будет. Поэтому + * размер резервированных записей кроме последней будет равен gc_largechunk_preferred_size(), + * а последней округлённому/выровненному остатку страниц. Однако, в общем случае, может существенно + * колебаться как размер записей, так и "баланс" отклонения от среднего. + * + * Если считать что резерва достаточно и имеющийся избыток допустим, то задача заполнения сводится + * к распределению излишков резерва по записям с учётом их размера, а далее просто к записи данных. + * При этом желательно обойтись без каких-то сложных операций типа деления и т.п. */ + const size_t amount = MDBX_PNL_GETSIZE(txn->wr.repnl); + tASSERT(txn, amount > 0 && amount <= ctx->return_reserved_hi && !rkl_empty(&txn->wr.gc.comeback)); + const size_t slots = rkl_len(&txn->wr.gc.comeback); + if (likely(slots == 1)) { + /* самый простой и частый случай */ + txnid_t id = rkl_lowest(&txn->wr.gc.comeback); + MDBX_val key = {.iov_base = &id, .iov_len = sizeof(id)}; + MDBX_val data = {.iov_base = nullptr, .iov_len = 0}; + int err = cursor_seek(&ctx->cursor, &key, &data, MDBX_SET_KEY).err; + if (likely(err == MDBX_SUCCESS)) { + tASSERT(txn, data.iov_len >= MDBX_PNL_SIZEOF(txn->wr.repnl)); + if (unlikely(data.iov_len - MDBX_PNL_SIZEOF(txn->wr.repnl) >= txn->env->ps)) { + NOTICE("too long comeback-reserve @%" PRIaTXN ", have %zu bytes, need %zu bytes", id, data.iov_len, + MDBX_PNL_SIZEOF(txn->wr.repnl)); + return MDBX_RESULT_TRUE; + } + memcpy(data.iov_base, txn->wr.repnl, MDBX_PNL_SIZEOF(txn->wr.repnl)); + pgno_t *const from = MDBX_PNL_BEGIN(txn->wr.repnl), *const to = MDBX_PNL_END(txn->wr.repnl); + TRACE("%s: fill %zu [ %zu:%" PRIaPGNO "...%zu:%" PRIaPGNO "] @%" PRIaTXN " (%s)", dbg_prefix(ctx), + MDBX_PNL_GETSIZE(txn->wr.repnl), from - txn->wr.repnl, from[0], to - txn->wr.repnl, to[-1], id, "at-once"); + } + return err; + } + + rkl_iter_t iter = rkl_iterator(&txn->wr.gc.comeback, is_lifo(txn)); + size_t surplus = ctx->return_reserved_hi - amount, stored = 0; + const size_t scale = 32 - ceil_log2n(ctx->return_reserved_hi), half4rounding = (1 << scale) / 2 - 1; + tASSERT(txn, scale > 3 && scale < 32); + const size_t factor = (surplus << scale) / ctx->return_reserved_hi; + TRACE("%s: amount %zu, slots %zu, surplus %zu (%zu..%zu), factor %.5f (sharp %.7f)", dbg_prefix(ctx), amount, slots, + surplus, ctx->return_reserved_lo, ctx->return_reserved_hi, factor / (double)(1 << scale), + surplus / (double)ctx->return_reserved_lo); + do { + const size_t left = amount - stored; + tASSERT(txn, left > 0 && left <= amount); + txnid_t id = rkl_turn(&iter, is_lifo(txn)); + if (unlikely(!id)) { + ERROR("reserve depleted (used %zu slots, left %zu loop %u)", rkl_len(&txn->wr.gc.comeback), left, ctx->loop); + return MDBX_PROBLEM; + } + MDBX_val key = {.iov_base = &id, .iov_len = sizeof(id)}; + MDBX_val data = {.iov_base = nullptr, .iov_len = 0}; + const int err = cursor_seek(&ctx->cursor, &key, &data, MDBX_SET_KEY).err; + if (unlikely(err != MDBX_SUCCESS)) + return err; + + tASSERT(txn, data.iov_len >= sizeof(pgno_t) * 2); + const size_t chunk_hi = data.iov_len / sizeof(pgno_t) - 1; + tASSERT(txn, chunk_hi >= 2); + size_t chunk = left; + if (chunk > chunk_hi) { + chunk = chunk_hi; + const size_t left_slots = rkl_left(&iter, is_lifo(txn)); + if (surplus && left_slots) { + /* Единственный путь выполнения (набор условий) когда нужно распределять избыток резерва. */ + size_t hole = (chunk_hi * factor + half4rounding) >> scale; + tASSERT(txn, hole < chunk_hi && hole <= surplus); + chunk = chunk_hi - hole; + tASSERT(txn, chunk > 0 && chunk <= chunk_hi); + const intptr_t estimate_balance = + (((left + surplus - chunk_hi) * factor + half4rounding) >> scale) - (surplus - hole); + if (MDBX_HAVE_CMOV || estimate_balance) { + chunk -= estimate_balance < 0 && chunk > 1; + chunk += estimate_balance > 0 && hole > 0 && surplus > hole; + } + } + tASSERT(txn, chunk <= chunk_hi && surplus >= chunk_hi - chunk && chunk <= left); + surplus -= chunk_hi - chunk; + } + + tASSERT(txn, chunk > 0 && chunk <= chunk_hi && chunk <= left); + if (unlikely(data.iov_len - (chunk + 1) * sizeof(pgno_t) >= txn->env->ps)) { + NOTICE("too long comeback-reserve @%" PRIaTXN ", have %zu bytes, need %zu bytes", id, data.iov_len, + (chunk + 1) * sizeof(pgno_t)); + return MDBX_RESULT_TRUE; + } + + pgno_t *const dst = data.iov_base; + pgno_t *const src = MDBX_PNL_BEGIN(txn->wr.repnl) + left - chunk; + *dst = (pgno_t)chunk; + memcpy(dst + 1, src, chunk * sizeof(pgno_t)); + stored += chunk; + + pgno_t *const from = src, *const to = src + chunk; + TRACE("%s: fill +%zu (surplus %zu) [ %zu:%" PRIaPGNO "...%zu:%" PRIaPGNO "] @%" PRIaTXN " (%s)", dbg_prefix(ctx), + chunk, chunk_hi - chunk, from - txn->wr.repnl, from[0], to - txn->wr.repnl, to[-1], id, "series"); + TRACE("%s: left %zu, surplus %zu, slots %zu", dbg_prefix(ctx), amount - stored, surplus, + rkl_left(&iter, is_lifo(txn))); + } while (stored < amount); + return MDBX_SUCCESS; +} + int gc_update(MDBX_txn *txn, gcu_t *ctx) { TRACE("\n>>> @%" PRIaTXN, txn->txnid); MDBX_env *const env = txn->env; ctx->cursor.next = txn->cursors[FREE_DBI]; txn->cursors[FREE_DBI] = &ctx->cursor; - int rc; + int err; - /* txn->wr.repnl[] can grow and shrink during this call. - * txn->wr.gc.last_reclaimed and txn->wr.retired_pages[] can only grow. + if (unlikely(!txn->env->gc.detent)) + txn_gc_detent(txn); + + if (AUDIT_ENABLED()) { + err = audit_ex(txn, 0, false); + if (unlikely(err != MDBX_SUCCESS)) + goto bailout; + } + + /* The txn->wr.repnl[] can grow and shrink during this call. + * The txn->wr.gc.reclaimed[] can grow, then migrate into ctx->ready4reuse and later to txn->wr.gc.comeback[]. * But page numbers cannot disappear from txn->wr.retired_pages[]. */ -retry_clean_adj: - ctx->reserve_adj = 0; retry: ctx->loop += !(ctx->prev_first_unallocated > txn->geo.first_unallocated); - TRACE(">> restart, loop %u", ctx->loop); + TRACE(">> %sstart, loop %u, gc: txn-rkl %zu, detent %" PRIaTXN, (ctx->loop > 1) ? "re" : "", ctx->loop, + rkl_len(&txn->wr.gc.reclaimed), txn->env->gc.detent); tASSERT(txn, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); tASSERT(txn, dpl_check(txn)); if (unlikely(/* paranoia */ ctx->loop > ((MDBX_DEBUG > 0) ? 12 : 42))) { ERROR("txn #%" PRIaTXN " too more loops %u, bailout", txn->txnid, ctx->loop); - rc = MDBX_PROBLEM; + err = MDBX_PROBLEM; goto bailout; } - if (unlikely(ctx->dense || ctx->prev_first_unallocated > txn->geo.first_unallocated)) { - rc = clean_stored_retired(txn, ctx); - if (unlikely(rc != MDBX_SUCCESS)) + if (unlikely(ctx->prev_first_unallocated > txn->geo.first_unallocated)) { + err = gc_clean_stored_retired(txn, ctx); + if (unlikely(err != MDBX_SUCCESS)) goto bailout; } ctx->prev_first_unallocated = txn->geo.first_unallocated; - rc = MDBX_SUCCESS; - ctx->reserved = 0; - ctx->cleaned_slot = 0; - ctx->reused_slot = 0; - ctx->amount = 0; - ctx->fill_idx = ~0u; - ctx->cleaned_id = 0; - ctx->rid = txn->wr.gc.last_reclaimed; + err = gc_clear_returned(txn, ctx); + if (unlikely(err != MDBX_SUCCESS)) + goto bailout; + while (true) { - /* Come back here after each Put() in case retired-list changed */ + /* come back here after each put() in case retired-list changed */ TRACE("%s", " >> continue"); tASSERT(txn, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); - MDBX_val key, data; - if (is_lifo(txn)) { - if (ctx->cleaned_slot < (txn->wr.gc.retxl ? MDBX_PNL_GETSIZE(txn->wr.gc.retxl) : 0)) { - ctx->reserved = 0; - ctx->cleaned_slot = 0; - ctx->reused_slot = 0; - ctx->fill_idx = ~0u; - /* LY: cleanup reclaimed records. */ - do { - ctx->cleaned_id = txn->wr.gc.retxl[++ctx->cleaned_slot]; - tASSERT(txn, ctx->cleaned_slot > 0 && ctx->cleaned_id <= env->lck->cached_oldest.weak); - key.iov_base = &ctx->cleaned_id; - key.iov_len = sizeof(ctx->cleaned_id); - rc = cursor_seek(&ctx->cursor, &key, nullptr, MDBX_SET).err; - if (rc == MDBX_NOTFOUND) - continue; - if (unlikely(rc != MDBX_SUCCESS)) - goto bailout; - rc = prepare_backlog(txn, ctx); - if (unlikely(rc != MDBX_SUCCESS)) - goto bailout; - tASSERT(txn, ctx->cleaned_id <= env->lck->cached_oldest.weak); - TRACE("%s: cleanup-reclaimed-id [%zu]%" PRIaTXN, dbg_prefix(ctx), ctx->cleaned_slot, ctx->cleaned_id); - tASSERT(txn, *txn->cursors == &ctx->cursor); - rc = cursor_del(&ctx->cursor, 0); - if (unlikely(rc != MDBX_SUCCESS)) - goto bailout; - } while (ctx->cleaned_slot < MDBX_PNL_GETSIZE(txn->wr.gc.retxl)); - txl_sort(txn->wr.gc.retxl); - } - } else { - /* Удаляем оставшиеся вынутые из GC записи. */ - while (txn->wr.gc.last_reclaimed && ctx->cleaned_id <= txn->wr.gc.last_reclaimed) { - rc = outer_first(&ctx->cursor, &key, nullptr); - if (rc == MDBX_NOTFOUND) { - ctx->cleaned_id = txn->wr.gc.last_reclaimed + 1; - ctx->rid = txn->wr.gc.last_reclaimed; - ctx->reserved = 0; - ctx->reused_slot = 0; - break; - } - if (unlikely(rc != MDBX_SUCCESS)) - goto bailout; - if (!MDBX_DISABLE_VALIDATION && unlikely(key.iov_len != sizeof(txnid_t))) { - ERROR("%s/%d: %s %u", "MDBX_CORRUPTED", MDBX_CORRUPTED, "invalid GC-key size", (unsigned)key.iov_len); - rc = MDBX_CORRUPTED; - goto bailout; - } - if (ctx->rid != ctx->cleaned_id) { - ctx->rid = ctx->cleaned_id; - ctx->reserved = 0; - ctx->reused_slot = 0; - } - ctx->cleaned_id = unaligned_peek_u64(4, key.iov_base); - if (ctx->cleaned_id > txn->wr.gc.last_reclaimed) - break; - rc = prepare_backlog(txn, ctx); - if (unlikely(rc != MDBX_SUCCESS)) - goto bailout; - tASSERT(txn, ctx->cleaned_id <= txn->wr.gc.last_reclaimed); - tASSERT(txn, ctx->cleaned_id <= env->lck->cached_oldest.weak); - TRACE("%s: cleanup-reclaimed-id %" PRIaTXN, dbg_prefix(ctx), ctx->cleaned_id); - tASSERT(txn, *txn->cursors == &ctx->cursor); - rc = cursor_del(&ctx->cursor, 0); - if (unlikely(rc != MDBX_SUCCESS)) - goto bailout; - } - } + err = gc_clear_reclaimed(txn, ctx); + if (unlikely(err != MDBX_SUCCESS)) + goto bailout; tASSERT(txn, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); tASSERT(txn, dpl_check(txn)); if (AUDIT_ENABLED()) { - rc = audit_ex(txn, ctx->retired_stored, false); - if (unlikely(rc != MDBX_SUCCESS)) + err = audit_ex(txn, ctx->retired_stored, false); + if (unlikely(err != MDBX_SUCCESS)) goto bailout; } @@ -647,317 +1386,83 @@ retry: if (txn_refund(txn)) { tASSERT(txn, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); if (AUDIT_ENABLED()) { - rc = audit_ex(txn, ctx->retired_stored, false); - if (unlikely(rc != MDBX_SUCCESS)) + err = audit_ex(txn, ctx->retired_stored, false); + if (unlikely(err != MDBX_SUCCESS)) goto bailout; } } if (txn->wr.loose_pages) { - /* put loose pages into the reclaimed- or retired-list */ - rc = gcu_loose(txn, ctx); - if (unlikely(rc != MDBX_SUCCESS)) { - if (rc == MDBX_RESULT_TRUE) + /* merge loose pages into the reclaimed- either retired-list */ + err = gc_merge_loose(txn, ctx); + if (unlikely(err != MDBX_SUCCESS)) { + if (err == MDBX_RESULT_TRUE) continue; goto bailout; } tASSERT(txn, txn->wr.loose_pages == 0); } - if (unlikely(ctx->reserved > MDBX_PNL_GETSIZE(txn->wr.repnl)) && - (ctx->loop < 5 || ctx->reserved - MDBX_PNL_GETSIZE(txn->wr.repnl) > env->maxgc_large1page / 2)) { - TRACE("%s: reclaimed-list changed %zu -> %zu, retry", dbg_prefix(ctx), ctx->amount, - MDBX_PNL_GETSIZE(txn->wr.repnl)); - ctx->reserve_adj += ctx->reserved - MDBX_PNL_GETSIZE(txn->wr.repnl); - goto retry; - } - ctx->amount = MDBX_PNL_GETSIZE(txn->wr.repnl); - if (ctx->retired_stored < MDBX_PNL_GETSIZE(txn->wr.retired_pages)) { /* store retired-list into GC */ - rc = gcu_retired(txn, ctx); - if (unlikely(rc != MDBX_SUCCESS)) + err = gc_store_retired(txn, ctx); + if (unlikely(err != MDBX_SUCCESS)) goto bailout; continue; } tASSERT(txn, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); tASSERT(txn, txn->wr.loose_count == 0); - - TRACE("%s", " >> reserving"); if (AUDIT_ENABLED()) { - rc = audit_ex(txn, ctx->retired_stored, false); - if (unlikely(rc != MDBX_SUCCESS)) + err = audit_ex(txn, ctx->retired_stored, false); + if (unlikely(err != MDBX_SUCCESS)) goto bailout; } - const size_t left = ctx->amount - ctx->reserved - ctx->reserve_adj; - TRACE("%s: amount %zu, reserved %zd, reserve_adj %zu, left %zd, " - "lifo-reclaimed-slots %zu, " - "reused-gc-slots %zu", - dbg_prefix(ctx), ctx->amount, ctx->reserved, ctx->reserve_adj, left, - txn->wr.gc.retxl ? MDBX_PNL_GETSIZE(txn->wr.gc.retxl) : 0, ctx->reused_slot); - if (0 >= (intptr_t)left) - break; - const rid_t rid_result = get_rid_for_reclaimed(txn, ctx, left); - if (unlikely(!rid_result.rid)) { - rc = rid_result.err; - if (likely(rc == MDBX_SUCCESS)) - continue; - if (likely(rc == MDBX_RESULT_TRUE)) - goto retry; - goto bailout; - } - tASSERT(txn, rid_result.err == MDBX_SUCCESS); - const txnid_t reservation_gc_id = rid_result.rid; - - size_t chunk = left; - if (unlikely(left > env->maxgc_large1page)) { - const size_t avail_gc_slots = txn->wr.gc.retxl ? MDBX_PNL_GETSIZE(txn->wr.gc.retxl) - ctx->reused_slot + 1 - : (ctx->rid < INT16_MAX) ? (size_t)ctx->rid - : INT16_MAX; - if (likely(avail_gc_slots > 1)) { -#if MDBX_ENABLE_BIGFOOT - chunk = env->maxgc_large1page; - if (avail_gc_slots < INT16_MAX && unlikely(left > env->maxgc_large1page * avail_gc_slots)) - /* TODO: Можно смотреть последовательности какой длины есть в repnl - * и пробовать нарезать куски соответствующего размера. - * Смысл в том, чтобы не дробить последовательности страниц, - * а использовать целиком. */ - chunk = env->maxgc_large1page + left / (env->maxgc_large1page * avail_gc_slots) * env->maxgc_large1page; -#else - if (chunk < env->maxgc_large1page * 2) - chunk /= 2; - else { - const size_t prefer_max_scatter = 257; - const size_t threshold = - env->maxgc_large1page * ((avail_gc_slots < prefer_max_scatter) ? avail_gc_slots : prefer_max_scatter); - if (left < threshold) - chunk = env->maxgc_large1page; - else { - const size_t tail = left - threshold + env->maxgc_large1page + 1; - size_t span = 1; - size_t avail = ((pgno2bytes(env, span) - PAGEHDRSZ) / sizeof(pgno_t)) /* - 1 + span */; - if (tail > avail) { - for (size_t i = ctx->amount - span; i > 0; --i) { - if (MDBX_PNL_ASCENDING ? (txn->wr.repnl[i] + span) - : (txn->wr.repnl[i] - span) == txn->wr.repnl[i + span]) { - span += 1; - avail = ((pgno2bytes(env, span) - PAGEHDRSZ) / sizeof(pgno_t)) - 1 + span; - if (avail >= tail) - break; - } - } - } - - chunk = (avail >= tail) ? tail - span - : (avail_gc_slots > 3 && ctx->reused_slot < prefer_max_scatter - 3) ? avail - span - : tail; - } - } -#endif /* MDBX_ENABLE_BIGFOOT */ - } - } - tASSERT(txn, chunk > 0); - - TRACE("%s: gc_rid %" PRIaTXN ", reused_gc_slot %zu, reservation-id " - "%" PRIaTXN, - dbg_prefix(ctx), ctx->rid, ctx->reused_slot, reservation_gc_id); - - TRACE("%s: chunk %zu, gc-per-ovpage %u", dbg_prefix(ctx), chunk, env->maxgc_large1page); - - tASSERT(txn, reservation_gc_id <= env->lck->cached_oldest.weak); - if (unlikely(reservation_gc_id < MIN_TXNID || - reservation_gc_id > atomic_load64(&env->lck->cached_oldest, mo_Relaxed))) { - ERROR("** internal error (reservation_gc_id %" PRIaTXN ")", reservation_gc_id); - rc = MDBX_PROBLEM; - goto bailout; + if (unlikely(MDBX_PNL_GETSIZE(txn->wr.repnl) + env->maxgc_large1page <= ctx->return_reserved_lo) && !ctx->dense) { + /* после резервирования было израсходованно слишком много страниц и получилось слишком много резерва */ + TRACE("%s: reclaimed-list %zu < reversed %zu, retry", dbg_prefix(ctx), MDBX_PNL_GETSIZE(txn->wr.repnl), + ctx->return_reserved_lo); + goto retry; } - tASSERT(txn, reservation_gc_id >= MIN_TXNID && reservation_gc_id <= MAX_TXNID); - key.iov_len = sizeof(reservation_gc_id); - key.iov_base = (void *)&reservation_gc_id; - data.iov_len = (chunk + 1) * sizeof(pgno_t); - TRACE("%s: reserve %zu [%zu...%zu) @%" PRIaTXN, dbg_prefix(ctx), chunk, ctx->reserved + 1, - ctx->reserved + chunk + 1, reservation_gc_id); - prepare_backlog(txn, ctx); - rc = cursor_put(&ctx->cursor, &key, &data, MDBX_RESERVE | MDBX_NOOVERWRITE); - tASSERT(txn, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); - if (unlikely(rc != MDBX_SUCCESS)) - goto bailout; - - zeroize_reserved(env, data); - ctx->reserved += chunk; - TRACE("%s: reserved %zu (+%zu), continue", dbg_prefix(ctx), ctx->reserved, chunk); - - continue; - } - - tASSERT(txn, ctx->cleaned_slot == (txn->wr.gc.retxl ? MDBX_PNL_GETSIZE(txn->wr.gc.retxl) : 0)); - - TRACE("%s", " >> filling"); - /* Fill in the reserved records */ - size_t excess_slots = 0; - ctx->fill_idx = txn->wr.gc.retxl ? MDBX_PNL_GETSIZE(txn->wr.gc.retxl) - ctx->reused_slot : ctx->reused_slot; - rc = MDBX_SUCCESS; - tASSERT(txn, pnl_check_allocated(txn->wr.repnl, txn->geo.first_unallocated - MDBX_ENABLE_REFUND)); - tASSERT(txn, dpl_check(txn)); - if (ctx->amount) { - MDBX_val key, data; - key.iov_len = data.iov_len = 0; - key.iov_base = data.iov_base = nullptr; - - size_t left = ctx->amount, excess = 0; - if (txn->wr.gc.retxl == nullptr) { - tASSERT(txn, is_lifo(txn) == 0); - rc = outer_first(&ctx->cursor, &key, &data); - if (unlikely(rc != MDBX_SUCCESS)) { - if (rc != MDBX_NOTFOUND) - goto bailout; - } - } else { - tASSERT(txn, is_lifo(txn) != 0); - } - - while (true) { - txnid_t fill_gc_id; - TRACE("%s: left %zu of %zu", dbg_prefix(ctx), left, MDBX_PNL_GETSIZE(txn->wr.repnl)); - if (txn->wr.gc.retxl == nullptr) { - tASSERT(txn, is_lifo(txn) == 0); - fill_gc_id = key.iov_base ? unaligned_peek_u64(4, key.iov_base) : MIN_TXNID; - if (ctx->fill_idx == 0 || fill_gc_id > txn->wr.gc.last_reclaimed) { - if (!left) - break; - VERBOSE("** restart: reserve depleted (fill_idx %zu, fill_id %" PRIaTXN " > last_reclaimed %" PRIaTXN - ", left %zu", - ctx->fill_idx, fill_gc_id, txn->wr.gc.last_reclaimed, left); - ctx->reserve_adj = (ctx->reserve_adj > left) ? ctx->reserve_adj - left : 0; + if (ctx->return_reserved_hi < MDBX_PNL_GETSIZE(txn->wr.repnl)) { + /* верхней границы резерва НЕ хватает, продолжаем резервирование */ + TRACE(">> %s, %zu...%zu, %s %zu", "reserving", ctx->return_reserved_lo, ctx->return_reserved_hi, "return-left", + MDBX_PNL_GETSIZE(txn->wr.repnl) - ctx->return_reserved_hi); + err = gc_rerere(txn, ctx); + if (unlikely(err != MDBX_SUCCESS)) { + if (err == MDBX_RESULT_TRUE) goto retry; - } - ctx->fill_idx -= 1; - } else { - tASSERT(txn, is_lifo(txn) != 0); - if (ctx->fill_idx >= MDBX_PNL_GETSIZE(txn->wr.gc.retxl)) { - if (!left) - break; - VERBOSE("** restart: reserve depleted (fill_idx %zu >= " - "gc.reclaimed %zu, left %zu", - ctx->fill_idx, MDBX_PNL_GETSIZE(txn->wr.gc.retxl), left); - ctx->reserve_adj = (ctx->reserve_adj > left) ? ctx->reserve_adj - left : 0; - goto retry; - } - ctx->fill_idx += 1; - fill_gc_id = txn->wr.gc.retxl[ctx->fill_idx]; - TRACE("%s: seek-reservation @%" PRIaTXN " at gc.reclaimed[%zu]", dbg_prefix(ctx), fill_gc_id, ctx->fill_idx); - key.iov_base = &fill_gc_id; - key.iov_len = sizeof(fill_gc_id); - rc = cursor_seek(&ctx->cursor, &key, &data, MDBX_SET_KEY).err; - if (unlikely(rc != MDBX_SUCCESS)) - goto bailout; - } - tASSERT(txn, ctx->cleaned_slot == (txn->wr.gc.retxl ? MDBX_PNL_GETSIZE(txn->wr.gc.retxl) : 0)); - tASSERT(txn, fill_gc_id > 0 && fill_gc_id <= env->lck->cached_oldest.weak); - key.iov_base = &fill_gc_id; - key.iov_len = sizeof(fill_gc_id); - - tASSERT(txn, data.iov_len >= sizeof(pgno_t) * 2); - size_t chunk = data.iov_len / sizeof(pgno_t) - 1; - if (unlikely(chunk > left)) { - const size_t delta = chunk - left; - excess += delta; - TRACE("%s: chunk %zu > left %zu, @%" PRIaTXN, dbg_prefix(ctx), chunk, left, fill_gc_id); - if (!left) { - excess_slots += 1; - goto next; - } - if ((ctx->loop < 5 && delta > (ctx->loop / 2)) || delta > env->maxgc_large1page) - data.iov_len = (left + 1) * sizeof(pgno_t); - chunk = left; - } - rc = cursor_put(&ctx->cursor, &key, &data, MDBX_CURRENT | MDBX_RESERVE); - if (unlikely(rc != MDBX_SUCCESS)) goto bailout; - zeroize_reserved(env, data); - - if (unlikely(txn->wr.loose_count || ctx->amount != MDBX_PNL_GETSIZE(txn->wr.repnl))) { - NOTICE("** restart: reclaimed-list changed (%zu -> %zu, loose +%zu)", ctx->amount, - MDBX_PNL_GETSIZE(txn->wr.repnl), txn->wr.loose_count); - if (ctx->loop < 5 || (ctx->loop > 10 && (ctx->loop & 1))) - goto retry_clean_adj; - goto retry; - } - - if (unlikely(txn->wr.gc.retxl ? ctx->cleaned_slot < MDBX_PNL_GETSIZE(txn->wr.gc.retxl) - : ctx->cleaned_id < txn->wr.gc.last_reclaimed)) { - NOTICE("%s", "** restart: reclaimed-slots changed"); - goto retry; - } - if (unlikely(ctx->retired_stored != MDBX_PNL_GETSIZE(txn->wr.retired_pages))) { - tASSERT(txn, ctx->retired_stored < MDBX_PNL_GETSIZE(txn->wr.retired_pages)); - NOTICE("** restart: retired-list growth (%zu -> %zu)", ctx->retired_stored, - MDBX_PNL_GETSIZE(txn->wr.retired_pages)); - goto retry; - } - - pgno_t *dst = data.iov_base; - *dst++ = (pgno_t)chunk; - pgno_t *src = MDBX_PNL_BEGIN(txn->wr.repnl) + left - chunk; - memcpy(dst, src, chunk * sizeof(pgno_t)); - pgno_t *from = src, *to = src + chunk; - TRACE("%s: fill %zu [ %zu:%" PRIaPGNO "...%zu:%" PRIaPGNO "] @%" PRIaTXN, dbg_prefix(ctx), chunk, - from - txn->wr.repnl, from[0], to - txn->wr.repnl, to[-1], fill_gc_id); - - left -= chunk; - if (AUDIT_ENABLED()) { - rc = audit_ex(txn, ctx->retired_stored + ctx->amount - left, true); - if (unlikely(rc != MDBX_SUCCESS)) - goto bailout; - } - - next: - - if (txn->wr.gc.retxl == nullptr) { - tASSERT(txn, is_lifo(txn) == 0); - rc = outer_next(&ctx->cursor, &key, &data, MDBX_NEXT); - if (unlikely(rc != MDBX_SUCCESS)) { - if (rc == MDBX_NOTFOUND && !left) { - rc = MDBX_SUCCESS; - break; - } - goto bailout; - } - } else { - tASSERT(txn, is_lifo(txn) != 0); } + continue; } - if (excess) { - size_t n = excess, adj = excess; - while (n >= env->maxgc_large1page) - adj -= n /= env->maxgc_large1page; - ctx->reserve_adj += adj; - TRACE("%s: extra %zu reserved space, adj +%zu (%zu)", dbg_prefix(ctx), excess, adj, ctx->reserve_adj); + if (MDBX_PNL_GETSIZE(txn->wr.repnl) > 0) { + TRACE(">> %s, %s %zu -> %zu...%zu", "filling", "return-reserved", MDBX_PNL_GETSIZE(txn->wr.repnl), + ctx->return_reserved_lo, ctx->return_reserved_hi); + err = gc_fill_returned(txn, ctx); + if (unlikely(err != MDBX_SUCCESS)) { + if (err == MDBX_RESULT_TRUE) + goto retry; + goto bailout; + } } + break; } - tASSERT(txn, rc == MDBX_SUCCESS); - if (unlikely(txn->wr.loose_count != 0 || ctx->amount != MDBX_PNL_GETSIZE(txn->wr.repnl))) { - NOTICE("** restart: got %zu loose pages (reclaimed-list %zu -> %zu)", txn->wr.loose_count, ctx->amount, - MDBX_PNL_GETSIZE(txn->wr.repnl)); + tASSERT(txn, err == MDBX_SUCCESS); + if (AUDIT_ENABLED()) { + err = audit_ex(txn, ctx->retired_stored + MDBX_PNL_GETSIZE(txn->wr.repnl), true); + if (unlikely(err != MDBX_SUCCESS)) + goto bailout; + } + if (unlikely(txn->wr.loose_count > 0)) { + DEBUG("** restart: got %zu loose pages", txn->wr.loose_count); goto retry; } - if (unlikely(excess_slots)) { - const bool will_retry = ctx->loop < 5 || excess_slots > 1; - NOTICE("** %s: reserve excess (excess-slots %zu, filled-slot %zu, adj %zu, " - "loop %u)", - will_retry ? "restart" : "ignore", excess_slots, ctx->fill_idx, ctx->reserve_adj, ctx->loop); - if (will_retry) - goto retry; - } - - tASSERT(txn, txn->wr.gc.retxl == nullptr || ctx->cleaned_slot == MDBX_PNL_GETSIZE(txn->wr.gc.retxl)); - bailout: txn->cursors[FREE_DBI] = ctx->cursor.next; @@ -965,6 +1470,10 @@ bailout: #if MDBX_ENABLE_PROFGC env->lck->pgops.gc_prof.wloops += (uint32_t)ctx->loop; #endif /* MDBX_ENABLE_PROFGC */ - TRACE("<<< %u loops, rc = %d", ctx->loop, rc); - return rc; + TRACE("<<< %u loops, rc = %d\n", ctx->loop, err); + return err; } + +#if MDBX_DEBUG_GCU +#pragma pop_macro("LOG_ENABLED") +#endif /* MDBX_DEBUG_GCU */ diff --git a/src/gc.h b/src/gc.h index 171037f4..f854e6a1 100644 --- a/src/gc.h +++ b/src/gc.h @@ -5,14 +5,37 @@ #include "essentials.h" +/* Гистограмма решения нарезки фрагментов для ситуации нехватки идентификаторов/слотов. */ +typedef struct gc_dense_histogram { + /* Размер массива одновременно задаёт максимальный размер последовательностей, + * с которыми решается задача распределения. + * + * Использование длинных последовательностей контрпродуктивно, так как такие последовательности будут + * создавать/воспроизводить/повторять аналогичные затруднения при последующей переработке. Однако, + * в редких ситуациях это может быть единственным выходом. */ + unsigned end; + pgno_t array[31]; +} gc_dense_histogram_t; + typedef struct gc_update_context { unsigned loop; - pgno_t prev_first_unallocated; + unsigned goodchunk; bool dense; - size_t reserve_adj; + pgno_t prev_first_unallocated; size_t retired_stored; - size_t amount, reserved, cleaned_slot, reused_slot, fill_idx; - txnid_t cleaned_id, rid; + size_t return_reserved_lo, return_reserved_hi; + txnid_t gc_first; + intptr_t return_left; +#ifndef MDBX_DEBUG_GCU +#define MDBX_DEBUG_GCU 0 +#endif +#if MDBX_DEBUG_GCU + struct { + txnid_t prev; + unsigned n; + } dbg; +#endif /* MDBX_DEBUG_GCU */ + rkl_t ready4reuse, sequel; #if MDBX_ENABLE_BIGFOOT txnid_t bigfoot; #endif /* MDBX_ENABLE_BIGFOOT */ @@ -20,21 +43,34 @@ typedef struct gc_update_context { MDBX_cursor cursor; cursor_couple_t couple; }; + gc_dense_histogram_t dense_histogram; } gcu_t; -static inline int gc_update_init(MDBX_txn *txn, gcu_t *ctx) { - memset(ctx, 0, offsetof(gcu_t, cursor)); - ctx->dense = txn->txnid <= MIN_TXNID; -#if MDBX_ENABLE_BIGFOOT - ctx->bigfoot = txn->txnid; -#endif /* MDBX_ENABLE_BIGFOOT */ - return cursor_init(&ctx->cursor, txn, FREE_DBI); -} +MDBX_INTERNAL int gc_put_init(MDBX_txn *txn, gcu_t *ctx); +MDBX_INTERNAL void gc_put_destroy(gcu_t *ctx); + +#define ALLOC_DEFAULT 0 /* штатное/обычное выделение страниц */ +#define ALLOC_UNIMPORTANT 1 /* запрос неважен, невозможность выделения не приведет к ошибке транзакции */ +#define ALLOC_RESERVE 2 /* подготовка резерва для обновления GC, без аллокации */ +#define ALLOC_COALESCE 4 /* внутреннее состояние/флажок */ +#define ALLOC_SHOULD_SCAN 8 /* внутреннее состояние/флажок */ +#define ALLOC_LIFO 16 /* внутреннее состояние/флажок */ -#define ALLOC_DEFAULT 0 -#define ALLOC_RESERVE 1 -#define ALLOC_UNIMPORTANT 2 MDBX_INTERNAL pgr_t gc_alloc_ex(const MDBX_cursor *const mc, const size_t num, uint8_t flags); MDBX_INTERNAL pgr_t gc_alloc_single(const MDBX_cursor *const mc); MDBX_INTERNAL int gc_update(MDBX_txn *txn, gcu_t *ctx); + +MDBX_NOTHROW_PURE_FUNCTION static inline size_t gc_stockpile(const MDBX_txn *txn) { + return MDBX_PNL_GETSIZE(txn->wr.repnl) + txn->wr.loose_count; +} + +MDBX_INTERNAL bool gc_repnl_has_span(MDBX_txn *txn, const size_t num); + +static inline bool gc_is_reclaimed(const MDBX_txn *txn, const txnid_t id) { + return rkl_contain(&txn->wr.gc.reclaimed, id) || rkl_contain(&txn->wr.gc.comeback, id); +} + +static inline txnid_t txnid_min(txnid_t a, txnid_t b) { return (a < b) ? a : b; } + +static inline txnid_t txnid_max(txnid_t a, txnid_t b) { return (a > b) ? a : b; } diff --git a/src/internals.h b/src/internals.h index 2c53a136..5400cac4 100644 --- a/src/internals.h +++ b/src/internals.h @@ -214,10 +214,9 @@ struct MDBX_txn { troika_t troika; pnl_t __restrict repnl; /* Reclaimed GC pages */ struct { - /* The list of reclaimed txn-ids from GC */ - txl_t __restrict retxl; - txnid_t last_reclaimed; /* ID of last used record */ - uint64_t time_acc; + rkl_t reclaimed; /* The list of reclaimed txn-ids from GC */ + uint64_t spent; /* Time spent reading and searching GC */ + rkl_t comeback; /* The list of ids of records returned into GC during commit, etc */ } gc; bool prefault_write_activated; #if MDBX_ENABLE_REFUND @@ -287,13 +286,14 @@ struct MDBX_cursor { }; /* флаги проверки, в том числе биты для проверки типа листовых страниц. */ uint8_t checking; + uint8_t pad; /* Указывает на txn->dbi_state[] для DBI этого курсора. * Модификатор __restrict тут полезен и безопасен в текущем понимании, * так как пересечение возможно только с dbi_state транзакции, * и происходит по-чтению до последующего изменения/записи. */ uint8_t *__restrict dbi_state; - /* Связь списка отслеживания курсоров в транзакции */ + /* Связь списка отслеживания курсоров в транзакции. */ MDBX_txn *txn; /* Указывает на tree->dbs[] для DBI этого курсора. */ tree_t *tree; @@ -362,15 +362,14 @@ struct MDBX_env { uint16_t subpage_reserve_prereq; uint16_t subpage_reserve_limit; atomic_pgno_t mlocked_pgno; - uint8_t ps2ln; /* log2 of DB page size */ - int8_t stuck_meta; /* recovery-only: target meta page or less that zero */ - uint16_t merge_threshold, merge_threshold_gc; /* pages emptier than this are - candidates for merging */ - unsigned max_readers; /* size of the reader table */ - MDBX_dbi max_dbi; /* size of the DB table */ - uint32_t pid; /* process ID of this env */ - osal_thread_key_t me_txkey; /* thread-key for readers */ - struct { /* path to the DB files */ + uint8_t ps2ln; /* log2 of DB page size */ + int8_t stuck_meta; /* recovery-only: target meta page or less that zero */ + uint16_t merge_threshold; /* pages emptier than this are candidates for merging */ + unsigned max_readers; /* size of the reader table */ + MDBX_dbi max_dbi; /* size of the DB table */ + uint32_t pid; /* process ID of this env */ + osal_thread_key_t me_txkey; /* thread-key for readers */ + struct { /* path to the DB files */ pathchar_t *lck, *dxb, *specified; void *buffer; } pathname; @@ -467,6 +466,9 @@ struct MDBX_env { /* --------------------------------------------------- mostly volatile part */ MDBX_txn *txn; /* current write transaction */ + struct { + txnid_t detent; + } gc; osal_fastmutex_t dbi_lock; unsigned n_dbi; /* number of DBs opened */ @@ -549,11 +551,7 @@ MDBX_MAYBE_UNUSED static void static_checks(void) { STATIC_ASSERT(sizeof(clc_t) == 3 * sizeof(void *)); STATIC_ASSERT(sizeof(kvx_t) == 8 * sizeof(void *)); -#if MDBX_WORDBITS == 64 -#define KVX_SIZE_LN2 6 -#else -#define KVX_SIZE_LN2 5 -#endif +#define KVX_SIZE_LN2 MDBX_WORDBITS_LN2 STATIC_ASSERT(sizeof(kvx_t) == (1u << KVX_SIZE_LN2)); } #endif /* Disabled for MSVC 19.0 (VisualStudio 2015) */ diff --git a/src/mvcc-readers.c b/src/mvcc-readers.c index 0a3af363..699c6be8 100644 --- a/src/mvcc-readers.c +++ b/src/mvcc-readers.c @@ -300,7 +300,7 @@ __cold MDBX_INTERNAL int mvcc_cleanup_dead(MDBX_env *env, int rdt_locked, int *d return rc; } -__cold txnid_t mvcc_kick_laggards(MDBX_env *env, const txnid_t straggler) { +__cold bool mvcc_kick_laggards(MDBX_env *env, const txnid_t straggler) { DEBUG("DB size maxed out by reading #%" PRIaTXN, straggler); osal_memory_fence(mo_AcquireRelease, false); MDBX_hsr_func *const callback = env->hsr_callback; @@ -410,5 +410,5 @@ __cold txnid_t mvcc_kick_laggards(MDBX_env *env, const txnid_t straggler) { NOTICE("hsr-kick: done turn %" PRIaTXN " -> %" PRIaTXN " +%" PRIaTXN, straggler, oldest, turn); callback(env, env->txn, 0, 0, straggler, (turn < UINT_MAX) ? (unsigned)turn : UINT_MAX, 0, -retry); } - return oldest; + return oldest > straggler; } diff --git a/src/pnl.h b/src/pnl.h index a329b9b2..416ed9bb 100644 --- a/src/pnl.h +++ b/src/pnl.h @@ -56,7 +56,7 @@ typedef const pgno_t *const_pnl_t; #define MDBX_PNL_SIZEOF(pl) ((MDBX_PNL_GETSIZE(pl) + 1) * sizeof(pgno_t)) #define MDBX_PNL_IS_EMPTY(pl) (MDBX_PNL_GETSIZE(pl) == 0) -MDBX_MAYBE_UNUSED static inline size_t pnl_size2bytes(size_t size) { +MDBX_NOTHROW_PURE_FUNCTION MDBX_MAYBE_UNUSED static inline size_t pnl_size2bytes(size_t size) { assert(size > 0 && size <= PAGELIST_LIMIT); #if MDBX_PNL_PREALLOC_FOR_RADIXSORT @@ -71,7 +71,7 @@ MDBX_MAYBE_UNUSED static inline size_t pnl_size2bytes(size_t size) { return bytes; } -MDBX_MAYBE_UNUSED static inline pgno_t pnl_bytes2size(const size_t bytes) { +MDBX_NOTHROW_PURE_FUNCTION MDBX_MAYBE_UNUSED static inline pgno_t pnl_bytes2size(const size_t bytes) { size_t size = bytes / sizeof(pgno_t); assert(size > 3 && size <= PAGELIST_LIMIT + /* alignment gap */ 65536); size -= 3; @@ -114,7 +114,7 @@ MDBX_INTERNAL int __must_check_result pnl_append_span(__restrict pnl_t *ppnl, pg MDBX_INTERNAL int __must_check_result pnl_insert_span(__restrict pnl_t *ppnl, pgno_t pgno, size_t n); -MDBX_INTERNAL size_t pnl_search_nochk(const pnl_t pnl, pgno_t pgno); +MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL size_t pnl_search_nochk(const pnl_t pnl, pgno_t pgno); MDBX_INTERNAL void pnl_sort_nochk(pnl_t pnl); @@ -130,7 +130,8 @@ MDBX_MAYBE_UNUSED static inline void pnl_sort(pnl_t pnl, size_t limit4check) { (void)limit4check; } -MDBX_MAYBE_UNUSED static inline size_t pnl_search(const pnl_t pnl, pgno_t pgno, size_t limit) { +MDBX_NOTHROW_PURE_FUNCTION MDBX_MAYBE_UNUSED static inline size_t pnl_search(const pnl_t pnl, pgno_t pgno, + size_t limit) { assert(pnl_check_allocated(pnl, limit)); if (MDBX_HAVE_CMOV) { /* cmov-ускоренный бинарный поиск может читать (но не использовать) один diff --git a/src/proto.h b/src/proto.h index e1886c5d..3d76ed74 100644 --- a/src/proto.h +++ b/src/proto.h @@ -15,9 +15,8 @@ MDBX_INTERNAL bsr_t mvcc_bind_slot(MDBX_env *env); MDBX_MAYBE_UNUSED MDBX_INTERNAL pgno_t mvcc_largest_this(MDBX_env *env, pgno_t largest); MDBX_INTERNAL txnid_t mvcc_shapshot_oldest(MDBX_env *const env, const txnid_t steady); MDBX_INTERNAL pgno_t mvcc_snapshot_largest(const MDBX_env *env, pgno_t last_used_page); -MDBX_INTERNAL txnid_t mvcc_kick_laggards(MDBX_env *env, const txnid_t straggler); MDBX_INTERNAL int mvcc_cleanup_dead(MDBX_env *env, int rlocked, int *dead); -MDBX_INTERNAL txnid_t mvcc_kick_laggards(MDBX_env *env, const txnid_t laggard); +MDBX_INTERNAL bool mvcc_kick_laggards(MDBX_env *env, const txnid_t laggard); /* dxb.c */ MDBX_INTERNAL int dxb_setup(MDBX_env *env, const int lck_rc, const mdbx_mode_t mode_bits); @@ -62,10 +61,11 @@ struct commit_timestamp { }; MDBX_INTERNAL bool txn_refund(MDBX_txn *txn); -MDBX_INTERNAL txnid_t txn_snapshot_oldest(const MDBX_txn *const txn); +MDBX_INTERNAL bool txn_gc_detent(const MDBX_txn *const txn); MDBX_INTERNAL int txn_check_badbits_parked(const MDBX_txn *txn, int bad_bits); MDBX_INTERNAL void txn_done_cursors(MDBX_txn *txn); MDBX_INTERNAL int txn_shadow_cursors(const MDBX_txn *parent, const size_t dbi); +MDBX_INTERNAL MDBX_cursor *txn_gc_cursor(MDBX_txn *txn); MDBX_INTERNAL MDBX_txn *txn_alloc(const MDBX_txn_flags_t flags, MDBX_env *env); MDBX_INTERNAL int txn_abort(MDBX_txn *txn); diff --git a/src/rkl.h b/src/rkl.h index b7e8b7cd..777a094e 100644 --- a/src/rkl.h +++ b/src/rkl.h @@ -33,6 +33,7 @@ typedef struct MDBX_rkl { MDBX_MAYBE_UNUSED MDBX_INTERNAL void rkl_init(rkl_t *rkl); MDBX_MAYBE_UNUSED MDBX_INTERNAL void rkl_clear(rkl_t *rkl); +static inline void rkl_clear_and_shrink(rkl_t *rkl) { rkl_clear(rkl); /* TODO */ } MDBX_MAYBE_UNUSED MDBX_INTERNAL void rkl_destroy(rkl_t *rkl); MDBX_MAYBE_UNUSED MDBX_INTERNAL void rkl_destructive_move(rkl_t *dst, rkl_t *src); MDBX_MAYBE_UNUSED MDBX_INTERNAL __must_check_result int rkl_copy(const rkl_t *src, rkl_t *dst); diff --git a/src/txl.c b/src/txl.c index d369f3bd..21a895dc 100644 --- a/src/txl.c +++ b/src/txl.c @@ -63,14 +63,14 @@ static int txl_reserve(txl_t __restrict *__restrict ptxl, const size_t wanna) { return MDBX_ENOMEM; } -static __always_inline int __must_check_result txl_need(txl_t __restrict *__restrict ptxl, size_t num) { +static inline int __must_check_result txl_need(txl_t __restrict *__restrict ptxl, size_t num) { assert(MDBX_PNL_GETSIZE(*ptxl) <= txl_max && MDBX_PNL_ALLOCLEN(*ptxl) >= MDBX_PNL_GETSIZE(*ptxl)); assert(num <= PAGELIST_LIMIT); const size_t wanna = (size_t)MDBX_PNL_GETSIZE(*ptxl) + num; return likely(MDBX_PNL_ALLOCLEN(*ptxl) >= wanna) ? MDBX_SUCCESS : txl_reserve(ptxl, wanna); } -static __always_inline void txl_xappend(txl_t __restrict txl, txnid_t id) { +static inline void txl_xappend(txl_t __restrict txl, txnid_t id) { assert(MDBX_PNL_GETSIZE(txl) < MDBX_PNL_ALLOCLEN(txl)); txl[0] += 1; MDBX_PNL_LAST(txl) = id; diff --git a/src/txl.h b/src/txl.h index 79cb5524..76d6a3cb 100644 --- a/src/txl.h +++ b/src/txl.h @@ -15,12 +15,12 @@ enum txl_rules { txl_max = (1u << 26) - 2 - MDBX_ASSUME_MALLOC_OVERHEAD / sizeof(txnid_t) }; -MDBX_INTERNAL txl_t txl_alloc(void); +MDBX_MAYBE_UNUSED MDBX_INTERNAL txl_t txl_alloc(void); -MDBX_INTERNAL void txl_free(txl_t txl); +MDBX_MAYBE_UNUSED MDBX_INTERNAL void txl_free(txl_t txl); -MDBX_INTERNAL int __must_check_result txl_append(txl_t __restrict *ptxl, txnid_t id); +MDBX_MAYBE_UNUSED MDBX_INTERNAL int __must_check_result txl_append(txl_t __restrict *ptxl, txnid_t id); -MDBX_INTERNAL void txl_sort(txl_t txl); +MDBX_MAYBE_UNUSED MDBX_INTERNAL void txl_sort(txl_t txl); -MDBX_INTERNAL bool txl_contain(const txl_t txl, txnid_t id); +MDBX_MAYBE_UNUSED MDBX_INTERNAL bool txl_contain(const txl_t txl, txnid_t id); diff --git a/src/txn-basal.c b/src/txn-basal.c index 26506872..d8e9ff0e 100644 --- a/src/txn-basal.c +++ b/src/txn-basal.c @@ -62,6 +62,8 @@ __cold MDBX_txn *txn_basal_create(const size_t max_dbi) { if (unlikely(!txn)) return txn; + rkl_init(&txn->wr.gc.reclaimed); + rkl_init(&txn->wr.gc.comeback); txn->dbs = ptr_disp(txn, base); txn->cursors = ptr_disp(txn->dbs, max_dbi * sizeof(txn->dbs[0])); txn->dbi_seqs = ptr_disp(txn->cursors, max_dbi * sizeof(txn->cursors[0])); @@ -82,7 +84,8 @@ __cold MDBX_txn *txn_basal_create(const size_t max_dbi) { __cold void txn_basal_destroy(MDBX_txn *txn) { dpl_free(txn); - txl_free(txn->wr.gc.retxl); + rkl_destroy(&txn->wr.gc.reclaimed); + rkl_destroy(&txn->wr.gc.comeback); pnl_free(txn->wr.retired_pages); pnl_free(txn->wr.spilled.list); pnl_free(txn->wr.repnl); @@ -121,10 +124,9 @@ int txn_basal_start(MDBX_txn *txn, unsigned flags) { MDBX_PNL_SETSIZE(txn->wr.retired_pages, 0); txn->wr.spilled.list = nullptr; txn->wr.spilled.least_removed = 0; - txn->wr.gc.time_acc = 0; - txn->wr.gc.last_reclaimed = 0; - if (txn->wr.gc.retxl) - MDBX_PNL_SETSIZE(txn->wr.gc.retxl, 0); + txn->wr.gc.spent = 0; + tASSERT(txn, rkl_empty(&txn->wr.gc.reclaimed)); + txn->env->gc.detent = 0; env->txn = txn; return MDBX_SUCCESS; @@ -140,6 +142,8 @@ int txn_basal_end(MDBX_txn *txn, unsigned mode) { env->txn = nullptr; pnl_free(txn->wr.spilled.list); txn->wr.spilled.list = nullptr; + rkl_clear_and_shrink(&txn->wr.gc.reclaimed); + rkl_clear_and_shrink(&txn->wr.gc.comeback); eASSERT(env, txn->parent == nullptr); pnl_shrink(&txn->wr.retired_pages); @@ -258,9 +262,19 @@ int txn_basal_commit(MDBX_txn *txn, struct commit_timestamp *ts) { } gcu_t gcu_ctx; - int rc = gc_update_init(txn, &gcu_ctx); + int rc = gc_put_init(txn, &gcu_ctx); if (likely(rc == MDBX_SUCCESS)) rc = gc_update(txn, &gcu_ctx); + +#if MDBX_ENABLE_BIGFOOT + const txnid_t commit_txnid = gcu_ctx.bigfoot; + if (commit_txnid > txn->txnid) + TRACE("use @%" PRIaTXN " (+%zu) for commit bigfoot-txn", commit_txnid, (size_t)(commit_txnid - txn->txnid)); +#else + const txnid_t commit_txnid = txn->txnid; +#endif + gc_put_destroy(&gcu_ctx); + if (ts) ts->gc_cpu = osal_cputime(nullptr) - ts->gc_cpu; if (unlikely(rc != MDBX_SUCCESS)) @@ -334,13 +348,6 @@ int txn_basal_commit(MDBX_txn *txn, struct commit_timestamp *ts) { meta.canary = txn->canary; memcpy(&meta.dxbid, &head.ptr_c->dxbid, sizeof(meta.dxbid)); - txnid_t commit_txnid = txn->txnid; -#if MDBX_ENABLE_BIGFOOT - if (gcu_ctx.bigfoot > txn->txnid) { - commit_txnid = gcu_ctx.bigfoot; - TRACE("use @%" PRIaTXN " (+%zu) for commit bigfoot-txn", commit_txnid, (size_t)(commit_txnid - txn->txnid)); - } -#endif meta.unsafe_sign = DATASIGN_NONE; meta_set_txnid(env, &meta, commit_txnid); diff --git a/src/txn-nested.c b/src/txn-nested.c index 09cd87aa..ffc50231 100644 --- a/src/txn-nested.c +++ b/src/txn-nested.c @@ -349,6 +349,7 @@ int txn_nested_create(MDBX_txn *parent, const MDBX_txn_flags_t flags) { return LOG_IFERR(MDBX_ENOMEM); tASSERT(parent, dpl_check(parent)); + rkl_init(&txn->wr.gc.reclaimed); #if MDBX_ENABLE_DBI_SPARSE txn->dbi_sparse = parent->dbi_sparse; #endif /* MDBX_ENABLE_DBI_SPARSE */ @@ -403,12 +404,11 @@ int txn_nested_create(MDBX_txn *parent, const MDBX_txn_flags_t flags) { = parent->geo.first_unallocated) - MDBX_ENABLE_REFUND)); - txn->wr.gc.time_acc = parent->wr.gc.time_acc; - txn->wr.gc.last_reclaimed = parent->wr.gc.last_reclaimed; - if (parent->wr.gc.retxl) { - txn->wr.gc.retxl = parent->wr.gc.retxl; - parent->wr.gc.retxl = (void *)(intptr_t)MDBX_PNL_GETSIZE(parent->wr.gc.retxl); - } + txn->wr.gc.spent = parent->wr.gc.spent; + rkl_init(&txn->wr.gc.comeback); + err = rkl_copy(&parent->wr.gc.reclaimed, &txn->wr.gc.reclaimed); + if (unlikely(err != MDBX_SUCCESS)) + return err; txn->wr.retired_pages = parent->wr.retired_pages; parent->wr.retired_pages = (void *)(intptr_t)MDBX_PNL_GETSIZE(parent->wr.retired_pages); @@ -438,6 +438,7 @@ int txn_nested_create(MDBX_txn *parent, const MDBX_txn_flags_t flags) { (txn->parent ? txn->parent->wr.dirtyroom : txn->env->options.dp_limit)); parent->env->txn = txn; tASSERT(parent, parent->cursors[FREE_DBI] == nullptr); + // TODO: shadow GC' cursor return txn_shadow_cursors(parent, MAIN_DBI); } @@ -447,11 +448,7 @@ void txn_nested_abort(MDBX_txn *nested) { nested->signature = 0; nested->owner = 0; - if (nested->wr.gc.retxl) { - tASSERT(parent, MDBX_PNL_GETSIZE(nested->wr.gc.retxl) >= (uintptr_t)parent->wr.gc.retxl); - MDBX_PNL_SETSIZE(nested->wr.gc.retxl, (uintptr_t)parent->wr.gc.retxl); - parent->wr.gc.retxl = nested->wr.gc.retxl; - } + rkl_destroy(&nested->wr.gc.reclaimed); if (nested->wr.retired_pages) { tASSERT(parent, MDBX_PNL_GETSIZE(nested->wr.retired_pages) >= (uintptr_t)parent->wr.retired_pages); @@ -530,17 +527,14 @@ int txn_nested_join(MDBX_txn *txn, struct commit_timestamp *ts) { //------------------------------------------------------------------------- - parent->wr.gc.retxl = txn->wr.gc.retxl; - txn->wr.gc.retxl = nullptr; - parent->wr.retired_pages = txn->wr.retired_pages; txn->wr.retired_pages = nullptr; pnl_free(parent->wr.repnl); parent->wr.repnl = txn->wr.repnl; txn->wr.repnl = nullptr; - parent->wr.gc.time_acc = txn->wr.gc.time_acc; - parent->wr.gc.last_reclaimed = txn->wr.gc.last_reclaimed; + parent->wr.gc.spent = txn->wr.gc.spent; + rkl_destructive_move(&txn->wr.gc.reclaimed, &parent->wr.gc.reclaimed); parent->geo = txn->geo; parent->canary = txn->canary; diff --git a/src/txn.c b/src/txn.c index 5d553595..aa603aa2 100644 --- a/src/txn.c +++ b/src/txn.c @@ -3,8 +3,18 @@ #include "internals.h" -__hot txnid_t txn_snapshot_oldest(const MDBX_txn *const txn) { - return mvcc_shapshot_oldest(txn->env, txn->wr.troika.txnid[txn->wr.troika.prefer_steady]); +MDBX_cursor *txn_gc_cursor(MDBX_txn *txn) { + tASSERT(txn, (txn->flags & (MDBX_TXN_BLOCKED | MDBX_TXN_RDONLY)) == 0); + return ptr_disp(txn->env->basal_txn, sizeof(MDBX_txn)); +} + +__hot bool txn_gc_detent(const MDBX_txn *const txn) { + const txnid_t detent = mvcc_shapshot_oldest(txn->env, txn->wr.troika.txnid[txn->wr.troika.prefer_steady]); + if (likely(detent == txn->env->gc.detent)) + return false; + + txn->env->gc.detent = detent; + return true; } void txn_done_cursors(MDBX_txn *txn) { @@ -417,12 +427,9 @@ MDBX_txn *txn_alloc(const MDBX_txn_flags_t flags, MDBX_env *env) { txn = osal_malloc(size); if (unlikely(!txn)) return txn; -#if MDBX_DEBUG - memset(txn, 0xCD, size); - VALGRIND_MAKE_MEM_UNDEFINED(txn, size); -#endif /* MDBX_DEBUG */ MDBX_ANALYSIS_ASSUME(size > base); memset(txn, 0, (MDBX_GOOFY_MSVC_STATIC_ANALYZER && base > size) ? size : base); + txn->dbs = ptr_disp(txn, base); txn->cursors = ptr_disp(txn->dbs, env->max_dbi * sizeof(txn->dbs[0])); #if MDBX_DEBUG diff --git a/src/utils.c b/src/utils.c index ead1e4d3..30f5f309 100644 --- a/src/utils.c +++ b/src/utils.c @@ -3,6 +3,17 @@ #include "internals.h" +MDBX_NOTHROW_CONST_FUNCTION MDBX_MAYBE_UNUSED MDBX_INTERNAL unsigned ceil_log2n(size_t value_uintptr) { + assert(value_uintptr > 0 && value_uintptr < INT32_MAX); + value_uintptr -= 1; + value_uintptr |= value_uintptr >> 1; + value_uintptr |= value_uintptr >> 2; + value_uintptr |= value_uintptr >> 4; + value_uintptr |= value_uintptr >> 8; + value_uintptr |= value_uintptr >> 16; + return log2n_powerof2(value_uintptr + 1); +} + MDBX_MAYBE_UNUSED MDBX_NOTHROW_CONST_FUNCTION MDBX_INTERNAL unsigned log2n_powerof2(size_t value_uintptr) { assert(value_uintptr > 0 && value_uintptr < INT32_MAX && is_powerof2(value_uintptr)); assert((value_uintptr & -(intptr_t)value_uintptr) == value_uintptr); diff --git a/src/utils.h b/src/utils.h index 9043a760..a77b3a30 100644 --- a/src/utils.h +++ b/src/utils.h @@ -58,6 +58,8 @@ MDBX_NOTHROW_CONST_FUNCTION MDBX_MAYBE_UNUSED static inline size_t ceil_powerof2 MDBX_NOTHROW_CONST_FUNCTION MDBX_MAYBE_UNUSED MDBX_INTERNAL unsigned log2n_powerof2(size_t value_uintptr); +MDBX_NOTHROW_CONST_FUNCTION MDBX_MAYBE_UNUSED MDBX_INTERNAL unsigned ceil_log2n(size_t value_uintptr); + MDBX_NOTHROW_CONST_FUNCTION MDBX_INTERNAL uint64_t rrxmrrxmsx_0(uint64_t v); struct monotime_cache {