diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ea34172..1b7e9835 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,8 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/preface.h" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/proto.h" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/refund.c" + AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/rkl.c" + AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/rkl.h" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/sort.h" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/spill.c" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/spill.h" @@ -832,6 +834,8 @@ else() "${MDBX_SOURCE_DIR}/preface.h" "${MDBX_SOURCE_DIR}/proto.h" "${MDBX_SOURCE_DIR}/refund.c" + "${MDBX_SOURCE_DIR}/rkl.c" + "${MDBX_SOURCE_DIR}/rkl.h" "${MDBX_SOURCE_DIR}/sort.h" "${MDBX_SOURCE_DIR}/spill.c" "${MDBX_SOURCE_DIR}/spill.h" diff --git a/ChangeLog.md b/ChangeLog.md index adc2db6a..9e0d507b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,7 +4,7 @@ ChangeLog English version [by liar Google](https://libmdbx-dqdkfa-ru.translate.goog/md__change_log.html?_x_tr_sl=ru&_x_tr_tl=en) and [by Yandex](https://translated.turbopages.org/proxy_u/ru-en.en/https/libmdbx.dqdkfa.ru/md__change_log.html). -## v0.14.1 в активной разработке без конкретизации даты выпуска +## v0.14.1 выпуск запланирован в начале мая Первый выпуск в новом кусте/линейке версий с добавлением функционала, расширением API и внутренними переработками. @@ -19,9 +19,62 @@ and [by Yandex](https://translated.turbopages.org/proxy_u/ru-en.en/https/libmdbx - [maxc0d3r](https://gitflic.ru/user/maxc0d3r) for bug reporting and testing. - [Алексею Костюку (aka Keller)](https://t.me/keller18306) за сообщения о проблеме копирования на NFS. - Новое: + - Переработан код обновления GC и возврата страниц при фиксации транзакций. + + Возникающая при этом задача алгоритмически сложна, так как список + возвращаемых страниц находится в рекурсивной зависимости от самой + процедуры возврата и связанных с этим операций, а прямые решения во + многих случаях приводят к многократному росту накладных расходов. + Поэтому исторически эта часть кода была запутанным наслоением «сдержек и + противовесов», что создавало препятствие для развития. В ходе этой + доработки, унаследованный из LMDB код связанный с обновлением GC, был + полностью заменен вместе со всеми базирующимися на нём заплатками. + + Новая реализация использует контейнеры идентификаторы (aka RKL), + комбинирующие внутри списки элементов и непрерывные интервалы, что + позволяет предельно сократить накладные расходы и упросить реализацию + остальных алгоритмов. Основывается новая реализация на простом + прагматичном подходе «резервирования со взвешенным запасом». Для + подавляющего подмножества сценариев этого достаточно для однопроходного + обновления GC, с общей сложностью от `O(1)` для мелких транзакций, до + `O(log(N))` для огромных. При этом реализованный еще в 0.12.1 подход «Big + Foot» (дробление больших списков retired-страниц) полностью избавляет GC + от потребности в последовательностях смежных/соседствующих страниц и + одновременно позволяет работать новому коду обновления GC только по + самому простому и быстрому пути. + + Тем не менее, при намеренном отключении «Big Foot», либо при работы с БД + от старых версий движка без «Big Foot», возможны сложные ситуации, когда + в GC могут огромные списки страниц, которые желательно дробить при + возвращении неиспользованных переработанных остатков. В таких сценариях + для возврата в GC требуется создавать больше записей чем было исходно + переработано, что может приводить к нехватке имеющихся/переработанных + идентификаторов. Тогда в игру вступает следующая часть нового кода, + поиск в GC «дыр» (неиспользуемых промежутков/интервалов в пространстве + ключей GC). Далее, если свободных идентификаторов (неиспользуемого + пространства ключей GC) будет недостаточно, что весьма вероятно в + некоторых сценариях, будет решаться задача родственная «укладке + рюкзака». В конечном итоге, неиспользованные переработанные страницы + будут возвращены в GC, с максимально равномерным + распределением/дроблением и использованием имеющихся последовательностей + смежных/соседствующих страниц, что гарантирует близость к теоретическому + минимуму суммарной стоимости текущих действий и последующих операций. + + На данный момент нет известных практических сценариев ведущих к + отказу/неуспеху новой реализации обновления GC. Но гипотетически такие + случаи возможны, как из-за ошибок/недочетов в реализации, так и из-за + использования катастрофически неудачных режимов работы и значений опций + (например `MDBX_opt_rp_augment_limit`). В текущем понимании, в том числе + основываясь на объем тестирования, вероятность проявления + ошибок/недочетов оценивается как крайне низкая, а устраняться замеченные + проблемы будут по мере обнаружения. Однако, полностью автоматическое + решение самых кошмарных и запутанных ситуаций с GC следует ожидать + только при реализации дефрагментации — просто потому что нет иного + рационального способа решения, за вычетом копирования БД с + дефрагментацией. + - Добавлена опция сборки `MDBX_NOSUCCESS_PURE_COMMIT` предназначенная для отладки кода пользователя. По-умолчанию опция выключена и при фиксации пустых транзакции возвращается `MDBX_SUCCESS`. При включении опции, фиксация пишущих транзакций без каких-либо изменений считается нештатным поведением, с возвратом из `mdbx_txn_commit()` кода `MDBX_RESULT_TRUE` вместо `MDBX_SUCCESS`. diff --git a/GNUmakefile b/GNUmakefile index e14c17bf..2744b06e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -722,6 +722,7 @@ $(DIST_DIR)/@tmp-internals.inc: $(DIST_DIR)/@tmp-essentials.inc src/version.c $( -e '/#include "essentials.h"/d' \ -e '/#include "atomics-ops.h"/r src/atomics-ops.h' \ -e '/#include "proto.h"/r src/proto.h' \ + -e '/#include "rkl.h"/r src/rkl.h' \ -e '/#include "txl.h"/r src/txl.h' \ -e '/#include "unaligned.h"/r src/unaligned.h' \ -e '/#include "cogs.h"/r src/cogs.h' \ 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/alloy.c b/src/alloy.c index 4b5cf057..9c3cab5e 100644 --- a/src/alloy.c +++ b/src/alloy.c @@ -41,6 +41,7 @@ #include "page-ops.c" #include "pnl.c" #include "refund.c" +#include "rkl.c" #include "spill.c" #include "table.c" #include "tls.c" 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-opts.c b/src/api-opts.c index 4f13d875..eb23215a 100644 --- a/src/api-opts.c +++ b/src/api-opts.c @@ -147,6 +147,9 @@ void env_options_adjust_dp_limit(MDBX_env *env) { if (env->options.dp_limit < CURSOR_STACK_SIZE * 4) env->options.dp_limit = CURSOR_STACK_SIZE * 4; } +#ifdef MDBX_DEBUG_DPL_LIMIT + env->options.dp_limit = MDBX_DEBUG_DPL_LIMIT; +#endif /* MDBX_DEBUG_DPL_LIMIT */ if (env->options.dp_initial > env->options.dp_limit && env->options.dp_initial > default_dp_initial(env)) env->options.dp_initial = env->options.dp_limit; env->options.need_dp_limit_adjust = false; diff --git a/src/api-txn.c b/src/api-txn.c index 847d8f9e..19371211 100644 --- a/src/api-txn.c +++ b/src/api-txn.c @@ -514,23 +514,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/chk.c b/src/chk.c index e444ff85..a56fc26f 100644 --- a/src/chk.c +++ b/src/chk.c @@ -1330,9 +1330,9 @@ __cold static int chk_handle_gc(MDBX_chk_scope_t *const scope, MDBX_chk_table_t (number + 1) * sizeof(pgno_t), data->iov_len); number = data->iov_len / sizeof(pgno_t) - 1; } else if (data->iov_len - (number + 1) * sizeof(pgno_t) >= - /* LY: allow gap up to one page. it is ok + /* LY: allow gap up to two page. it is ok * and better than shink-and-retry inside gc_update() */ - usr->env->ps) + usr->env->ps * 2) chk_object_issue(scope, "entry", txnid, "extra idl space", "%" PRIuSIZE " < %" PRIuSIZE " (minor, not a trouble)", (number + 1) * sizeof(pgno_t), data->iov_len); diff --git a/src/cogs.h b/src/cogs.h index 498e92b7..a54097f5 100644 --- a/src/cogs.h +++ b/src/cogs.h @@ -250,9 +250,15 @@ MDBX_NOTHROW_PURE_FUNCTION static inline const page_t *data_page(const void *dat MDBX_NOTHROW_PURE_FUNCTION static inline meta_t *page_meta(page_t *mp) { return (meta_t *)page_data(mp); } -MDBX_NOTHROW_PURE_FUNCTION static inline size_t page_numkeys(const page_t *mp) { return mp->lower >> 1; } +MDBX_NOTHROW_PURE_FUNCTION static inline size_t page_numkeys(const page_t *mp) { + assert(mp->lower <= mp->upper); + return mp->lower >> 1; +} -MDBX_NOTHROW_PURE_FUNCTION static inline size_t page_room(const page_t *mp) { return mp->upper - mp->lower; } +MDBX_NOTHROW_PURE_FUNCTION static inline size_t page_room(const page_t *mp) { + assert(mp->lower <= mp->upper); + return mp->upper - mp->lower; +} MDBX_NOTHROW_PURE_FUNCTION static inline size_t page_space(const MDBX_env *env) { STATIC_ASSERT(PAGEHDRSZ % 2 == 0); diff --git a/src/cursor.c b/src/cursor.c index 92c59025..d11224a6 100644 --- a/src/cursor.c +++ b/src/cursor.c @@ -1781,8 +1781,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 e65323ff..97a17590 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..cfff49db 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(const MDBX_txn *txn, const size_t num) { + return (num > 1) ? repnl_get_sequence((MDBX_txn *)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..8cfaa77e 100644 --- a/src/gc-put.c +++ b/src/gc-put.c @@ -1,28 +1,168 @@ /// \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 size_t gc_chunk_pages(const MDBX_txn *txn, const size_t chunk) { + return largechunk_npages(txn->env, gc_chunk_bytes(chunk)); +} + +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 +171,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 +185,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 +201,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 +365,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 = gc_chunk_bytes(chunk_hi); 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 +431,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 +465,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 +475,949 @@ 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; + while (len > ct->factor) + solution[--len] = 0; + solution[len - 1] = 0; + if (unlikely(0 >= (int)st->left_volume)) + goto done; + + size_t per_chunk = ct->first_page + ct->other_pages * (len - 1); + 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; + local.left_volume = (local.left_volume > n * per_chunk) ? local.left_volume - n * per_chunk : 0; + } + 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 = gc_chunk_pages(txn, (st.left_volume + st.left_slots - 1) / st.left_slots), + .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 = gc_chunk_pages(txn, (st.left_volume + st.left_slots - 1) / st.left_slots), +// .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 = gc_chunk_bytes(chunk_hi)}; + 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; + if (unlikely(!rkl_empty(&txn->wr.gc.reclaimed))) { + NOTICE("%s: restart since %zu slot(s) reclaimed (reserved %zu...%zu of %zu)", dbg_prefix(ctx), + rkl_len(&txn->wr.gc.reclaimed), ctx->return_reserved_lo, ctx->return_reserved_hi, + MDBX_PNL_GETSIZE(txn->wr.repnl)); + return MDBX_RESULT_TRUE; + } + + return MDBX_SUCCESS; +} + +static size_t dense_chunk_outlay(const MDBX_txn *txn, const size_t chunk) { + size_t need_span = gc_chunk_pages(txn, chunk); + return gc_repnl_has_span(txn, need_span) ? need_span : 0; +} + +static size_t dense_adjust_chunk(const MDBX_txn *txn, const size_t chunk) { + size_t adjusted = chunk; + if (chunk > txn->env->maxgc_large1page) { + size_t hi = chunk + 1, lo = chunk - gc_chunk_pages(txn, chunk) - 1; + while (lo < hi) { + adjusted = (hi + lo) / 2; + size_t probe = chunk - dense_chunk_outlay(txn, adjusted); + if (probe > adjusted) + lo = adjusted + 1; + else if (probe < adjusted) + hi = adjusted - 1; + else + break; + } + } + return adjusted; +} + +static size_t dense_adjust_amount(const MDBX_txn *const txn, size_t amount) { + const size_t gap = 2 + txn->dbs[FREE_DBI].height; + const size_t snubber = txn->env->ps / sizeof(pgno_t) / 2; + return ((amount + gap < txn->env->maxgc_large1page) ? txn->env->maxgc_large1page : amount + snubber); +} + +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; + if (ctx->loop == 1 || ctx->loop % 3 == 0) + left_max = dense_adjust_amount(txn, left_max); + ctx->return_left = left_max; + err = gc_dense_solve(txn, ctx, &solution); + if (err == MDBX_RESULT_FALSE /* решение НЕ найдено */ && left_max != left_min) { + if (ctx->loop == 1 || ctx->loop % 3 == 0) + left_min = dense_adjust_amount(txn, left_min); + if (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 span = i; + size_t chunk_hi = txn->env->maxgc_large1page + txn->env->ps / sizeof(pgno_t) * (span - 1); + if (chunk_hi > left_max) { + chunk_hi = left_max; + span = gc_chunk_pages(txn, chunk_hi); + } + size_t chunk_lo = chunk_hi - txn->env->maxgc_large1page + ctx->goodchunk; + 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); + size_t amount = MDBX_PNL_GETSIZE(txn->wr.repnl); + err = gc_reserve4return(txn, ctx, chunk_lo, chunk_hi); + if (unlikely(err != MDBX_SUCCESS)) + return err; + + const size_t now = MDBX_PNL_GETSIZE(txn->wr.repnl); + if (span < amount - now - txn->dbs[FREE_DBI].height || span > amount - now + txn->dbs[FREE_DBI].height) + TRACE("dense-%s-reservation: miss %zu (expected) != %zi (got)", "solve", span, amount - now); + amount = now; + if (ctx->return_reserved_hi >= amount) + return MDBX_SUCCESS; + left_max = dense_adjust_amount(txn, amount) - ctx->return_reserved_lo; + } + } + } else if (rkl_len(&txn->wr.gc.comeback)) { + NOTICE("%s: restart since %zu slot(s) comemack non-dense (reserved %zu...%zu of %zu)", dbg_prefix(ctx), + rkl_len(&txn->wr.gc.comeback), ctx->return_reserved_lo, ctx->return_reserved_hi, + MDBX_PNL_GETSIZE(txn->wr.repnl)); + 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, gc_chunk_pages(txn, dense_adjust_chunk(txn, ctx->return_left))); + if (err != MDBX_NOTFOUND && err != MDBX_SUCCESS) + return err; + } + + const size_t per_page = txn->env->ps / sizeof(pgno_t); + size_t amount = MDBX_PNL_GETSIZE(txn->wr.repnl); + do { + if (rkl_empty(&ctx->ready4reuse)) { + NOTICE("%s: restart since no slot(s) available (reserved %zu...%zu of %zu)", dbg_prefix(ctx), + ctx->return_reserved_lo, ctx->return_reserved_hi, amount); + return MDBX_RESULT_TRUE; + } + const size_t left = dense_adjust_amount(txn, amount) - ctx->return_reserved_hi; + const size_t slots = rkl_len(&ctx->ready4reuse); + const size_t base = (left + slots - 1) / slots; + const size_t adjusted = dense_adjust_chunk(txn, base); + TRACE("dense-reservation: reserved %zu...%zu of %zu, left %zu slot(s) and %zu pnl, step: %zu base," + " %zu adjusted", + ctx->return_reserved_lo, ctx->return_reserved_hi, amount, slots, left, base, adjusted); + const size_t chunk_hi = + (adjusted > txn->env->maxgc_large1page) + ? txn->env->maxgc_large1page + ceil_powerof2(adjusted - 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); + if (unlikely(err != MDBX_SUCCESS)) + return err; + const size_t now = MDBX_PNL_GETSIZE(txn->wr.repnl); + if (base - adjusted + txn->dbs[FREE_DBI].height < amount - now || + base - adjusted > amount - now + txn->dbs[FREE_DBI].height) + TRACE("dense-%s-reservation: miss %zu (expected) != %zi (got)", "unsolve", base - adjusted, amount - now); + amount = now; + } while (ctx->return_reserved_hi < amount); + } + + 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)) { + 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"); + tASSERT(txn, data.iov_len >= gc_chunk_bytes(MDBX_PNL_GETSIZE(txn->wr.repnl))); + if (unlikely(data.iov_len - gc_chunk_bytes(MDBX_PNL_GETSIZE(txn->wr.repnl)) >= txn->env->ps * 2)) { + NOTICE("too long %s-comeback-reserve @%" PRIaTXN ", have %zu bytes, need %zu bytes", "single", id, data.iov_len, + gc_chunk_bytes(MDBX_PNL_GETSIZE(txn->wr.repnl))); + return MDBX_RESULT_TRUE; + } + /* coverity[var_deref_model] */ + memcpy(data.iov_base, txn->wr.repnl, gc_chunk_bytes(MDBX_PNL_GETSIZE(txn->wr.repnl))); + } + 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; + } + + pgno_t *const dst = data.iov_base; + pgno_t *const src = MDBX_PNL_BEGIN(txn->wr.repnl) + left - 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 + chunk), surplus, + rkl_left(&iter, is_lifo(txn))); + tASSERT(txn, chunk > 0 && chunk <= chunk_hi && chunk <= left); + if (unlikely(data.iov_len - gc_chunk_bytes(chunk) >= txn->env->ps)) { + NOTICE("too long %s-comeback-reserve @%" PRIaTXN ", have %zu bytes, need %zu bytes", "multi", id, data.iov_len, + gc_chunk_bytes(chunk)); + return MDBX_RESULT_TRUE; + } + + /* coverity[var_deref_op] */ + *dst = (pgno_t)chunk; + memcpy(dst + 1, src, chunk * sizeof(pgno_t)); + stored += chunk; + } 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 +1425,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 +1509,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..b81dc82f 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,38 @@ 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_NOTHROW_PURE_FUNCTION static inline size_t gc_chunk_bytes(const size_t chunk) { + return (chunk + 1) * sizeof(pgno_t); +} + +MDBX_INTERNAL bool gc_repnl_has_span(const 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 98f89ee1..5400cac4 100644 --- a/src/internals.h +++ b/src/internals.h @@ -46,6 +46,7 @@ typedef struct bind_reader_slot_result { #include "atomics-ops.h" #include "proto.h" +#include "rkl.h" #include "txl.h" #include "unaligned.h" #if defined(_WIN32) || defined(_WIN64) @@ -213,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 @@ -286,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; @@ -361,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; @@ -466,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 */ @@ -537,7 +540,9 @@ MDBX_MAYBE_UNUSED static void static_checks(void) { STATIC_ASSERT(offsetof(lck_t, cached_oldest) % MDBX_CACHELINE_SIZE == 0); STATIC_ASSERT(offsetof(lck_t, rdt_length) % MDBX_CACHELINE_SIZE == 0); #endif /* MDBX_LOCKING */ +#if FLEXIBLE_ARRAY_MEMBERS STATIC_ASSERT(offsetof(lck_t, rdt) % MDBX_CACHELINE_SIZE == 0); +#endif /* FLEXIBLE_ARRAY_MEMBERS */ #if FLEXIBLE_ARRAY_MEMBERS STATIC_ASSERT(NODESIZE == offsetof(node_t, payload)); @@ -546,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/node.c b/src/node.c index f7676eaf..6528fb3e 100644 --- a/src/node.c +++ b/src/node.c @@ -53,12 +53,6 @@ int __must_check_result node_add_branch(MDBX_cursor *mc, size_t indx, const MDBX cASSERT(mc, mp->txnid >= mc->txn->front_txnid); STATIC_ASSERT(NODESIZE % 2 == 0); - /* Move higher pointers up one slot. */ - const size_t nkeys = page_numkeys(mp); - cASSERT(mc, nkeys >= indx); - for (size_t i = nkeys; i > indx; --i) - mp->entries[i] = mp->entries[i - 1]; - /* Adjust free space offsets. */ const size_t branch_bytes = branch_size(mc->txn->env, key); const intptr_t lower = mp->lower + sizeof(indx_t); @@ -67,6 +61,13 @@ int __must_check_result node_add_branch(MDBX_cursor *mc, size_t indx, const MDBX mc->txn->flags |= MDBX_TXN_ERROR; return MDBX_PAGE_FULL; } + + /* Move higher pointers up one slot. */ + const size_t nkeys = page_numkeys(mp); + cASSERT(mc, nkeys >= indx); + for (size_t i = nkeys; i > indx; --i) + mp->entries[i] = mp->entries[i - 1]; + mp->lower = (indx_t)lower; mp->entries[indx] = mp->upper = (indx_t)upper; diff --git a/src/pnl.c b/src/pnl.c index 88f14c7f..d573c987 100644 --- a/src/pnl.c +++ b/src/pnl.c @@ -23,6 +23,13 @@ void pnl_free(pnl_t pnl) { osal_free(pnl - 1); } +pnl_t pnl_clone(const pnl_t src) { + pnl_t pl = pnl_alloc(MDBX_PNL_ALLOCLEN(src)); + if (likely(pl)) + memcpy(pl, src, MDBX_PNL_SIZEOF(src)); + return pl; +} + void pnl_shrink(pnl_t __restrict *__restrict ppnl) { assert(pnl_bytes2size(pnl_size2bytes(MDBX_PNL_INITIAL)) >= MDBX_PNL_INITIAL && pnl_bytes2size(pnl_size2bytes(MDBX_PNL_INITIAL)) < MDBX_PNL_INITIAL * 3 / 2); @@ -234,3 +241,18 @@ __hot __noinline size_t pnl_search_nochk(const pnl_t pnl, pgno_t pgno) { assert(!MDBX_PNL_ORDERED(it[0], pgno)); return it - begin + 1; } + +size_t pnl_maxspan(const pnl_t pnl) { + size_t len = MDBX_PNL_GETSIZE(pnl); + if (len > 1) { + size_t span = 1, left = len - span; + const pgno_t *scan = MDBX_PNL_BEGIN(pnl); + do { + const bool contiguous = MDBX_PNL_CONTIGUOUS(*scan, scan[span], span); + span += contiguous; + scan += 1 - contiguous; + } while (--left); + len = span; + } + return len; +} diff --git a/src/pnl.h b/src/pnl.h index c0b7d542..416ed9bb 100644 --- a/src/pnl.h +++ b/src/pnl.h @@ -45,16 +45,18 @@ typedef const pgno_t *const_pnl_t; #define MDBX_PNL_EDGE(pl) ((pl) + 1) #define MDBX_PNL_LEAST(pl) MDBX_PNL_FIRST(pl) #define MDBX_PNL_MOST(pl) MDBX_PNL_LAST(pl) +#define MDBX_PNL_CONTIGUOUS(prev, next, span) ((next) - (prev)) == (span)) #else #define MDBX_PNL_EDGE(pl) ((pl) + MDBX_PNL_GETSIZE(pl)) #define MDBX_PNL_LEAST(pl) MDBX_PNL_LAST(pl) #define MDBX_PNL_MOST(pl) MDBX_PNL_FIRST(pl) +#define MDBX_PNL_CONTIGUOUS(prev, next, span) (((prev) - (next)) == (span)) #endif #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 @@ -69,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; @@ -83,6 +85,8 @@ MDBX_INTERNAL pnl_t pnl_alloc(size_t size); MDBX_INTERNAL void pnl_free(pnl_t pnl); +MDBX_MAYBE_UNUSED MDBX_INTERNAL pnl_t pnl_clone(const pnl_t src); + MDBX_INTERNAL int pnl_reserve(pnl_t __restrict *__restrict ppnl, const size_t wanna); MDBX_MAYBE_UNUSED static inline int __must_check_result pnl_need(pnl_t __restrict *__restrict ppnl, size_t num) { @@ -110,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); @@ -126,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-ускоренный бинарный поиск может читать (но не использовать) один @@ -144,3 +149,5 @@ MDBX_MAYBE_UNUSED static inline size_t pnl_search(const pnl_t pnl, pgno_t pgno, } MDBX_INTERNAL size_t pnl_merge(pnl_t dst, const pnl_t src); + +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL size_t pnl_maxspan(const pnl_t pnl); 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.c b/src/rkl.c new file mode 100644 index 00000000..1eebbf45 --- /dev/null +++ b/src/rkl.c @@ -0,0 +1,639 @@ +/// \copyright SPDX-License-Identifier: Apache-2.0 +/// \author Леонид Юрьев aka Leonid Yuriev \date 2025 + +#include "internals.h" + +static inline size_t rkl_size2bytes(const size_t size) { + assert(size > 0 && size <= txl_max * 2); + size_t bytes = ceil_powerof2(MDBX_ASSUME_MALLOC_OVERHEAD + sizeof(txnid_t) * size, txl_granulate * sizeof(txnid_t)) - + MDBX_ASSUME_MALLOC_OVERHEAD; + return bytes; +} + +static inline size_t rkl_bytes2size(const size_t bytes) { + size_t size = bytes / sizeof(txnid_t); + assert(size > 0 && size <= txl_max * 2); + return size; +} + +void rkl_init(rkl_t *rkl) { + rkl->list_limit = ARRAY_LENGTH(rkl->inplace); + rkl->list = rkl->inplace; + rkl_clear(rkl); +} + +void rkl_clear(rkl_t *rkl) { + rkl->solid_begin = UINT64_MAX; + rkl->solid_end = 0; + rkl->list_length = 0; +} + +void rkl_destroy(rkl_t *rkl) { + void *ptr = rkl->list; + rkl->list = nullptr; + if (ptr != rkl->inplace) + osal_free(ptr); +} + +static inline bool solid_empty(const rkl_t *rkl) { return !(rkl->solid_begin < rkl->solid_end); } + +#define RKL_ORDERED(first, last) ((first) < (last)) + +SEARCH_IMPL(rkl_bsearch, txnid_t, txnid_t, RKL_ORDERED) + +void rkl_destructive_move(rkl_t *src, rkl_t *dst) { + assert(rkl_check(src)); + dst->solid_begin = src->solid_begin; + dst->solid_end = src->solid_end; + dst->list_length = src->list_length; + if (dst->list != dst->inplace) + osal_free(dst->list); + if (src->list != src->inplace) { + dst->list = src->list; + dst->list_limit = src->list_limit; + } else { + dst->list = dst->inplace; + dst->list_limit = ARRAY_LENGTH(src->inplace); + memcpy(dst->inplace, src->list, sizeof(dst->inplace)); + } + rkl_init(src); +} + +static int rkl_resize(rkl_t *rkl, size_t wanna_size) { + assert(wanna_size > rkl->list_length); + assert(rkl_check(rkl)); + STATIC_ASSERT(txl_max < INT_MAX / sizeof(txnid_t)); + if (unlikely(wanna_size > txl_max)) { + ERROR("rkl too long (%zu >= %zu)", wanna_size, (size_t)txl_max); + return MDBX_TXN_FULL; + } + if (unlikely(wanna_size < rkl->list_length)) { + ERROR("unable shrink rkl to %zu since length is %u", wanna_size, rkl->list_length); + return MDBX_PROBLEM; + } + + if (unlikely(wanna_size <= ARRAY_LENGTH(rkl->inplace))) { + if (rkl->list != rkl->inplace) { + assert(rkl->list_limit > ARRAY_LENGTH(rkl->inplace) && rkl->list_length <= ARRAY_LENGTH(rkl->inplace)); + memcpy(rkl->inplace, rkl->list, sizeof(rkl->inplace)); + rkl->list_limit = ARRAY_LENGTH(rkl->inplace); + osal_free(rkl->list); + rkl->list = rkl->inplace; + } else { + assert(rkl->list_limit == ARRAY_LENGTH(rkl->inplace)); + } + return MDBX_SUCCESS; + } + + if (wanna_size != rkl->list_limit) { + size_t bytes = rkl_size2bytes(wanna_size); + void *ptr = (rkl->list == rkl->inplace) ? osal_malloc(bytes) : osal_realloc(rkl->list, bytes); + if (unlikely(!ptr)) + return MDBX_ENOMEM; +#ifdef osal_malloc_usable_size + bytes = osal_malloc_usable_size(ptr); +#endif /* osal_malloc_usable_size */ + rkl->list_limit = rkl_bytes2size(bytes); + if (rkl->list == rkl->inplace) + memcpy(ptr, rkl->inplace, sizeof(rkl->inplace)); + rkl->list = ptr; + } + return MDBX_SUCCESS; +} + +int rkl_copy(const rkl_t *src, rkl_t *dst) { + assert(rkl_check(src)); + rkl_init(dst); + if (!rkl_empty(src)) { + if (dst->list_limit < src->list_length) { + int err = rkl_resize(dst, src->list_limit); + if (unlikely(err != MDBX_SUCCESS)) + return err; + } + memcpy(dst->list, src->list, sizeof(txnid_t) * src->list_length); + dst->list_length = src->list_length; + dst->solid_begin = src->solid_begin; + dst->solid_end = src->solid_end; + } + return MDBX_SUCCESS; +} + +size_t rkl_len(const rkl_t *rkl) { return rkl_empty(rkl) ? 0 : rkl->solid_end - rkl->solid_begin + rkl->list_length; } + +__hot bool rkl_contain(const rkl_t *rkl, txnid_t id) { + assert(rkl_check(rkl)); + if (id >= rkl->solid_begin && id < rkl->solid_end) + return true; + if (rkl->list_length) { + const txnid_t *it = rkl_bsearch(rkl->list, rkl->list_length, id); + const txnid_t *const end = rkl->list + rkl->list_length; + assert(it >= rkl->list && it <= end); + if (it != rkl->list) + assert(RKL_ORDERED(it[-1], id)); + if (it != end) { + assert(!RKL_ORDERED(it[0], id)); + return *it == id; + } + } + return false; +} + +__hot bool rkl_find(const rkl_t *rkl, txnid_t id, rkl_iter_t *iter) { + assert(rkl_check(rkl)); + *iter = rkl_iterator(rkl, false); + if (id >= rkl->solid_begin) { + if (id < rkl->solid_end) { + iter->pos = iter->solid_offset + (unsigned)(id - rkl->solid_begin); + return true; + } + iter->pos = (unsigned)(rkl->solid_end - rkl->solid_begin); + } + if (rkl->list_length) { + const txnid_t *it = rkl_bsearch(rkl->list, rkl->list_length, id); + const txnid_t *const end = rkl->list + rkl->list_length; + assert(it >= rkl->list && it <= end); + if (it != rkl->list) + assert(RKL_ORDERED(it[-1], id)); + iter->pos += (unsigned)(it - rkl->list); + if (it != end) { + assert(!RKL_ORDERED(it[0], id)); + return *it == id; + } + } + return false; +} + +static inline txnid_t list_remove_first(rkl_t *rkl) { + assert(rkl->list_length > 0); + const txnid_t first = rkl->list[0]; + if (--rkl->list_length) { + /* TODO: Можно подумать о том, чтобы для избавления от memove() добавить headroom или вместо длины и + * указателя на список использовать три поля: list_begin, list_end и list_buffer. */ + size_t i = 0; + do + rkl->list[i] = rkl->list[i + 1]; + while (++i <= rkl->list_length); + } + return first; +} + +static inline txnid_t after_cut(rkl_t *rkl, const txnid_t out) { + if (rkl->list_length == 0 && rkl->solid_begin == rkl->solid_end) { + rkl->solid_end = 0; + rkl->solid_begin = UINT64_MAX; + } + return out; +} + +static int extend_solid(rkl_t *rkl, txnid_t solid_begin, txnid_t solid_end, const txnid_t id) { + if (rkl->list_length) { + const txnid_t *i = rkl_bsearch(rkl->list, rkl->list_length, id); + const txnid_t *const end = rkl->list + rkl->list_length; + /* если начало или конец списка примыкает к непрерывному интервалу, + * то переносим эти элементы из списка в непрерывный интервал */ + txnid_t *f = (txnid_t *)i; + while (f > rkl->list && f[-1] >= solid_begin - 1) { + f -= 1; + solid_begin -= 1; + if (unlikely(*f != solid_begin)) + return MDBX_RESULT_TRUE; + } + txnid_t *t = (txnid_t *)i; + while (t < end && *t <= solid_end) { + if (unlikely(*t != solid_end)) + return MDBX_RESULT_TRUE; + solid_end += 1; + t += 1; + } + if (f < t) { + rkl->list_length -= t - f; + while (t < end) + *f++ = *t++; + } + } + + rkl->solid_begin = solid_begin; + rkl->solid_end = solid_end; + assert(rkl_check(rkl)); + return MDBX_SUCCESS; +} + +int rkl_push(rkl_t *rkl, const txnid_t id, const bool known_continuous) { + assert(id >= MIN_TXNID && id < INVALID_TXNID); + assert(rkl_check(rkl)); + + if (rkl->solid_begin >= rkl->solid_end) { + /* непрерывный интервал пуст */ + return extend_solid(rkl, id, id + 1, id); + } else if (id < rkl->solid_begin) { + if (known_continuous || id + 1 == rkl->solid_begin) + /* id примыкает к solid_begin */ + return extend_solid(rkl, id, rkl->solid_end, id); + } else if (id >= rkl->solid_end) { + if (known_continuous || id == rkl->solid_end) + /* id примыкает к solid_end */ + return extend_solid(rkl, rkl->solid_begin, id + 1, id); + } else { + /* id входит в интервал между solid_begin и solid_end, т.е. подан дубликат */ + return MDBX_RESULT_TRUE; + } + + if (rkl->list_length == 1 && rkl->solid_end == rkl->solid_begin + 1 && + (rkl->list[0] == id + 1 || rkl->list[0] == id - 1)) { + /* В списке один элемент и добавляемый id примыкает к нему, при этом в непрерывном интервале тоже один элемент. + * Лучше поменять элементы списка и непрерывного интервала. */ + const txnid_t couple = (rkl->list[0] == id - 1) ? id - 1 : id; + rkl->list[0] = rkl->solid_begin; + rkl->solid_begin = couple; + rkl->solid_end = couple + 2; + assert(rkl_check(rkl)); + return MDBX_SUCCESS; + } + + if (unlikely(rkl->list_length == rkl->list_limit)) { + /* удваиваем размер буфера если закончилось место */ + size_t x2 = (rkl->list_limit + 1) << 1; + x2 = (x2 > 62) ? x2 : 62; + x2 = (x2 < txl_max) ? x2 : txl_max; + x2 = (x2 > rkl->list_length) ? x2 : rkl->list_length + 42; + int err = rkl_resize(rkl, x2); + if (unlikely(err != MDBX_SUCCESS)) + return err; + assert(rkl->list_limit > rkl->list_length); + } + + size_t i = rkl->list_length; + /* ищем место для вставки двигаясь от конца к началу списка, сразу переставляя/раздвигая элементы */ + while (i > 0) { + if (RKL_ORDERED(id, rkl->list[i - 1])) { + rkl->list[i] = rkl->list[i - 1]; + i -= 1; + continue; + } + if (unlikely(id == rkl->list[i - 1])) { + while (++i < rkl->list_length) + rkl->list[i - 1] = rkl->list[i]; + return MDBX_RESULT_TRUE; + } + break; + } + + rkl->list[i] = id; + rkl->list_length++; + assert(rkl_check(rkl)); + + /* После добавления id в списке могла образоваться длинная последовательность, + * которую (возможно) стоит обменять с непрерывным интервалом. */ + if (rkl->list_length > (MDBX_DEBUG ? 2 : 16) && + ((i > 0 && rkl->list[i - 1] == id - 1) || (i + 1 < rkl->list_length && rkl->list[i + 1] == id + 1))) { + txnid_t new_solid_begin = id; + size_t from = i; + while (from > 0 && rkl->list[from - 1] == new_solid_begin - 1) { + from -= 1; + new_solid_begin -= 1; + } + txnid_t new_solid_end = id + 1; + size_t to = i + 1; + while (to < rkl->list_length && rkl->list[to] == new_solid_end) { + to += 1; + new_solid_end += 1; + } + + const size_t new_solid_len = to - from; + if (new_solid_len > 3) { + const size_t old_solid_len = rkl->solid_end - rkl->solid_begin; + if (new_solid_len > old_solid_len) { + /* Новая непрерывная последовательность длиннее текущей. + * Считаем обмен выгодным, если он дешевле пути развития событий с добавлением следующего элемента в список. */ + const size_t old_solid_pos = rkl_bsearch(rkl->list, rkl->list_length, rkl->solid_begin) - rkl->list; + const size_t swap_cost = + /* количество элементов списка после изымаемой из списка последовательности, + * которые нужно переместить */ + rkl->list_length - to + + /* количество элементов списка после позиции добавляемой в список последовательности, + * которые нужно переместить */ + ((from > old_solid_pos) ? from - old_solid_pos : 0) + /* количество элементов списка добавляемой последовательности, которые нужно добавить */ + + old_solid_len; + /* количество элементов списка, которые нужно переместить для вставки еще-одного/следующего элемента */ + const size_t new_insert_cost = rkl->list_length - i; + /* coverity[logical_vs_bitwise] */ + if (unlikely(swap_cost < new_insert_cost) || MDBX_DEBUG) { + /* Изымаемая последовательность длиннее добавляемой, поэтому: + * - список станет короче; + * - перемещать хвост нужно всегда к началу; + * - если начальные элементы потребуется раздвигать, + * то места хватит и остающиеся элементы в конце не будут перезаписаны. */ + size_t moved = 0; + if (from > old_solid_pos) { + /* добавляемая последовательность ближе к началу, нужно раздвинуть элементы в голове для вставки. */ + moved = from - old_solid_pos; + do { + from -= 1; + rkl->list[from + old_solid_len] = rkl->list[from]; + } while (from > old_solid_pos); + } else if (from + new_solid_len < old_solid_pos) { + /* добавляемая последовательность дальше от начала, + * перемещаем часть элементов из хвоста после изымаемой последовательности */ + do + rkl->list[from++] = rkl->list[to++]; + while (from < old_solid_pos - new_solid_len); + } + + /* вставляем последовательноть */ + i = 0; + do + rkl->list[from++] = rkl->solid_begin + i++; + while (i != old_solid_len); + + /* сдвигаем оставшийся хвост */ + while (to < rkl->list_length) + rkl->list[moved + from++] = rkl->list[to++]; + + rkl->list_length = rkl->list_length - new_solid_len + old_solid_len; + rkl->solid_begin = new_solid_begin; + rkl->solid_end = new_solid_end; + assert(rkl_check(rkl)); + } + } + } + } + return MDBX_SUCCESS; +} + +txnid_t rkl_pop(rkl_t *rkl, const bool highest_not_lowest) { + assert(rkl_check(rkl)); + + if (rkl->list_length) { + assert(rkl->solid_begin <= rkl->solid_end); + if (highest_not_lowest && (solid_empty(rkl) || rkl->solid_end < rkl->list[rkl->list_length - 1])) + return after_cut(rkl, rkl->list[rkl->list_length -= 1]); + if (!highest_not_lowest && (solid_empty(rkl) || rkl->solid_begin > rkl->list[0])) + return after_cut(rkl, list_remove_first(rkl)); + } + + if (!solid_empty(rkl)) + return after_cut(rkl, highest_not_lowest ? --rkl->solid_end : rkl->solid_begin++); + + assert(rkl_empty(rkl)); + return 0; +} + +txnid_t rkl_lowest(const rkl_t *rkl) { + if (rkl->list_length) + return (solid_empty(rkl) || rkl->list[0] < rkl->solid_begin) ? rkl->list[0] : rkl->solid_begin; + return !solid_empty(rkl) ? rkl->solid_begin : INVALID_TXNID; +} + +txnid_t rkl_highest(const rkl_t *rkl) { + if (rkl->list_length) + return (solid_empty(rkl) || rkl->list[rkl->list_length - 1] >= rkl->solid_end) ? rkl->list[rkl->list_length - 1] + : rkl->solid_end - 1; + return !solid_empty(rkl) ? rkl->solid_end - 1 : 0; +} + +int rkl_merge(rkl_t *dst, const rkl_t *src, bool ignore_duplicates) { + if (src->list_length) { + size_t i = src->list_length; + do { + int err = rkl_push(dst, src->list[i - 1], false); + if (unlikely(err != MDBX_SUCCESS) && (!ignore_duplicates || err != MDBX_RESULT_TRUE)) + return err; + } while (--i); + } + + txnid_t id = src->solid_begin; + while (id < src->solid_end) { + int err = rkl_push(dst, id, false); + if (unlikely(err != MDBX_SUCCESS) && (!ignore_duplicates || err != MDBX_RESULT_TRUE)) + return err; + ++id; + } + return MDBX_SUCCESS; +} + +rkl_iter_t rkl_iterator(const rkl_t *rkl, const bool reverse) { + rkl_iter_t iter = {.rkl = rkl, .pos = reverse ? rkl_len(rkl) : 0, .solid_offset = 0}; + if (!solid_empty(rkl) && rkl->list_length) { + const txnid_t *it = rkl_bsearch(rkl->list, rkl->list_length, rkl->solid_begin); + const txnid_t *const end = rkl->list + rkl->list_length; + assert(it >= rkl->list && it <= end && (it == end || *it > rkl->solid_begin)); + iter.solid_offset = it - rkl->list; + } + return iter; +} + +txnid_t rkl_turn(rkl_iter_t *iter, const bool reverse) { + assert((unsigned)reverse == (unsigned)!!reverse); + size_t pos = iter->pos - reverse; + if (unlikely(pos >= rkl_len(iter->rkl))) + return 0; + + iter->pos = pos + !reverse; + assert(iter->pos <= rkl_len(iter->rkl)); + + const size_t solid_len = iter->rkl->solid_end - iter->rkl->solid_begin; + if (iter->rkl->list_length) { + if (pos < iter->solid_offset) + return iter->rkl->list[pos]; + else if (pos < iter->solid_offset + solid_len) + return iter->rkl->solid_begin + pos - iter->solid_offset; + else + return iter->rkl->list[pos - solid_len]; + } + + assert(pos < solid_len); + return iter->rkl->solid_begin + pos; +} + +size_t rkl_left(rkl_iter_t *iter, const bool reverse) { + assert(iter->pos <= rkl_len(iter->rkl)); + return reverse ? iter->pos : rkl_len(iter->rkl) - iter->pos; +} + +#if 1 +#define DEBUG_HOLE(hole) \ + do { \ + } while (0) +#else +#define DEBUG_HOLE(hole) \ + do { \ + printf(" return-%sward: %d, ", reverse ? "back" : "for", __LINE__); \ + if (hole.begin == hole.end) \ + printf("empty-hole\n"); \ + else if (hole.end - hole.begin == 1) \ + printf("hole %" PRIaTXN "\n", hole.begin); \ + else \ + printf("hole %" PRIaTXN "-%" PRIaTXN "\n", hole.begin, hole.end - 1); \ + fflush(nullptr); \ + } while (0) +#endif + +rkl_hole_t rkl_hole(rkl_iter_t *iter, const bool reverse) { + assert((unsigned)reverse == (unsigned)!!reverse); + rkl_hole_t hole; + const size_t len = rkl_len(iter->rkl); + size_t pos = iter->pos; + if (unlikely(pos >= len)) { + if (len == 0) { + hole.begin = 1; + hole.end = MAX_TXNID; + iter->pos = 0; + DEBUG_HOLE(hole); + return hole; + } else if (pos == len && reverse) { + /* шаг назад из позиции на конце rkl */ + } else if (reverse) { + hole.begin = 1; + hole.end = 1 /* rkl_lowest(iter->rkl); */; + iter->pos = 0; + DEBUG_HOLE(hole); + return hole; + } else { + hole.begin = MAX_TXNID /* rkl_highest(iter->rkl) + 1 */; + hole.end = MAX_TXNID; + iter->pos = len; + DEBUG_HOLE(hole); + return hole; + } + } + + const size_t solid_len = iter->rkl->solid_end - iter->rkl->solid_begin; + if (iter->rkl->list_length) { + /* список элементов не пуст */ + txnid_t here, there; + for (size_t next;; pos = next) { + next = reverse ? pos - 1 : pos + 1; + if (pos < iter->solid_offset) { + /* текущая позиция перед непрерывным интервалом */ + here = iter->rkl->list[pos]; + if (next == iter->solid_offset) { + /* в следующей позиции начинается непрерывный интерал (при поиске вперед) */ + assert(!reverse); + hole.begin = here + 1; + hole.end = iter->rkl->solid_begin; + next += solid_len; + assert(hole.begin < hole.end /* зазор обязан быть, иначе это ошибка не-слияния */); + /* зазор между элементом списка перед сплошным интервалом и началом интервала */ + iter->pos = next - 1; + DEBUG_HOLE(hole); + return hole; + } + if (next >= len) + /* уперлись в конец или начало rkl */ + break; + /* следующая позиция также перед непрерывным интервалом */ + there = iter->rkl->list[next]; + } else if (pos >= iter->solid_offset + solid_len) { + /* текущая позиция после непрерывного интервала */ + here = (pos < len) ? iter->rkl->list[pos - solid_len] : MAX_TXNID; + if (next >= len) + /* уперлись в конец или начало rkl */ + break; + if (next == iter->solid_offset + solid_len - 1) { + /* в следующей позиции конец непрерывного интервала (при поиске назад) */ + assert(reverse); + hole.begin = iter->rkl->solid_end; + hole.end = here; + pos = iter->solid_offset; + assert(hole.begin < hole.end /* зазор обязан быть, иначе это ошибка не-слияния */); + /* зазор между элементом списка после сплошного интервала и концом интервала */ + iter->pos = pos; + DEBUG_HOLE(hole); + return hole; + } + /* следующая позиция также после непрерывного интервала */ + there = iter->rkl->list[next - solid_len]; + } else if (reverse) { + /* текущая позиция внутри непрерывного интервала и поиск назад */ + next = iter->solid_offset - 1; + here = iter->rkl->solid_begin; + if (next >= len) + /* нет элементов списка перед непрерывным интервалом */ + break; + /* предыдущая позиция перед непрерывным интервалом */ + there = iter->rkl->list[next]; + } else { + /* текущая позиция внутри непрерывного интервала и поиск вперед */ + next = iter->solid_offset + solid_len; + here = iter->rkl->solid_end - 1; + if (next >= len) + /* нет элементов списка после непрерывного интервала */ + break; + /* следующая позиция после непрерывного интервала */ + there = iter->rkl->list[next - solid_len]; + } + + hole.begin = (reverse ? there : here) + 1; + hole.end = reverse ? here : there; + if (hole.begin < hole.end) { + /* есть зазор между текущей и следующей позицией */ + iter->pos = next; + DEBUG_HOLE(hole); + return hole; + } + } + + if (reverse) { + /* уперлись в начало rkl, возвращаем зазор перед началом rkl */ + hole.begin = 1; + hole.end = here; + iter->pos = 0; + DEBUG_HOLE(hole); + } else { + /* уперлись в конец rkl, возвращаем зазор после конца rkl */ + hole.begin = here + 1; + hole.end = MAX_TXNID; + iter->pos = len; + DEBUG_HOLE(hole); + } + return hole; + } + + /* список элементов пуст, но есть непрерывный интервал */ + iter->pos = reverse ? 0 : len; + if (reverse && pos < len) { + /* возвращаем зазор перед непрерывным интервалом */ + hole.begin = 1; + hole.end = iter->rkl->solid_begin; + DEBUG_HOLE(hole); + } else { + /* возвращаем зазор после непрерывного интервала */ + hole.begin = iter->rkl->solid_end; + hole.end = MAX_TXNID; + DEBUG_HOLE(hole); + } + return hole; +} + +bool rkl_check(const rkl_t *rkl) { + if (!rkl) + return false; + if (rkl->list == rkl->inplace && unlikely(rkl->list_limit != ARRAY_LENGTH(rkl->inplace))) + return false; + if (unlikely(rkl->list_limit < ARRAY_LENGTH(rkl->inplace))) + return false; + + if (rkl_empty(rkl)) + return rkl->list_length == 0 && solid_empty(rkl); + + if (rkl->list_length) { + for (size_t i = 1; i < rkl->list_length; ++i) + if (unlikely(!RKL_ORDERED(rkl->list[i - 1], rkl->list[i]))) + return false; + if (!solid_empty(rkl) && rkl->solid_begin - 1 <= rkl->list[rkl->list_length - 1] && + rkl->solid_end >= rkl->list[0]) { + /* непрерывный интервал "плавает" внутри списка, т.е. находится между какими-то соседними значениями */ + const txnid_t *it = rkl_bsearch(rkl->list, rkl->list_length, rkl->solid_begin); + const txnid_t *const end = rkl->list + rkl->list_length; + if (it < rkl->list || it > end) + return false; + if (it > rkl->list && it[-1] >= rkl->solid_begin) + return false; + if (it < end && it[0] <= rkl->solid_end) + return false; + } + } + + return true; +} diff --git a/src/rkl.h b/src/rkl.h new file mode 100644 index 00000000..777a094e --- /dev/null +++ b/src/rkl.h @@ -0,0 +1,76 @@ +/// \copyright SPDX-License-Identifier: Apache-2.0 +/// \author Леонид Юрьев aka Leonid Yuriev \date 2025 + +#pragma once + +#include "essentials.h" + +/* Сортированный набор txnid, использующий внутри комбинацию непрерывного интервала и списка. + * Обеспечивает хранение id записей при переработке, очистку и обновлении GC, включая возврат остатков переработанных + * страниц. + * + * При переработке GC записи преимущественно выбираются последовательно, но это не гарантируется. В LIFO-режиме + * переработка и добавление записей в rkl происходит преимущественно в обратном порядке, но из-за завершения читающих + * транзакций могут быть «скачки» в прямом направлении. В FIFO-режиме записи GC перерабатываются в прямом порядке и при + * этом линейно, но не обязательно строго последовательно, при этом гарантируется что между добавляемыми в rkl + * идентификаторами в GC нет записей, т.е. между первой (минимальный id) и последней (максимальный id) в GC нет записей + * и весь интервал может быть использован для возврата остатков страниц в GC. + * + * Таким образом, комбинация линейного интервала и списка (отсортированного в порядке возрастания элементов) является + * рациональным решением, близким к теоретически оптимальному пределу. + * + * Реализация rkl достаточно проста/прозрачная, если не считать неочевидную «магию» обмена непрерывного интервала и + * образующихся в списке последовательностей. Однако, именно этот автоматически выполняемый без лишних операций обмен + * оправдывает все накладные расходы. */ +typedef struct MDBX_rkl { + txnid_t solid_begin, solid_end; /* начало и конец непрерывной последовательности solid_begin ... solid_end-1. */ + unsigned list_length; /* текущая длина списка. */ + unsigned list_limit; /* размер буфера выделенного под список, равен ARRAY_LENGTH(inplace) когда list == inplace. */ + txnid_t *list; /* список отдельных элементов в порядке возрастания (наименьший в начале). */ + txnid_t inplace[4 + 8]; /* статический массив для коротких списков, чтобы избавиться от выделения/освобождения памяти + * в большинстве случаев. */ +} rkl_t; + +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); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION static inline bool rkl_empty(const rkl_t *rkl) { + return rkl->solid_begin > rkl->solid_end; +} +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL bool rkl_check(const rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL size_t rkl_len(const rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL txnid_t rkl_lowest(const rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL txnid_t rkl_highest(const rkl_t *rkl); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION static inline txnid_t rkl_edge(const rkl_t *rkl, + const bool highest_not_lowest) { + return highest_not_lowest ? rkl_highest(rkl) : rkl_lowest(rkl); +} +MDBX_MAYBE_UNUSED MDBX_INTERNAL __must_check_result int rkl_push(rkl_t *rkl, const txnid_t id, + const bool known_continuous); +MDBX_MAYBE_UNUSED MDBX_INTERNAL txnid_t rkl_pop(rkl_t *rkl, const bool highest_not_lowest); +MDBX_MAYBE_UNUSED MDBX_INTERNAL __must_check_result int rkl_merge(rkl_t *dst, const rkl_t *src, bool ignore_duplicates); + +/* Итератор для rkl. + * Обеспечивает изоляцию внутреннего устройства rkl от остального кода, чем существенно его упрощает. + * Фактически именно использованием rkl с итераторами ликвидируется "ребус" исторически образовавшийся в gc-update. */ +typedef struct MDBX_rkl_iter { + const rkl_t *rkl; + unsigned pos; + unsigned solid_offset; +} rkl_iter_t; + +MDBX_MAYBE_UNUSED MDBX_INTERNAL __must_check_result rkl_iter_t rkl_iterator(const rkl_t *rkl, const bool reverse); +MDBX_MAYBE_UNUSED MDBX_INTERNAL __must_check_result txnid_t rkl_turn(rkl_iter_t *iter, const bool reverse); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION MDBX_INTERNAL size_t rkl_left(rkl_iter_t *iter, const bool reverse); +MDBX_MAYBE_UNUSED MDBX_INTERNAL bool rkl_find(const rkl_t *rkl, const txnid_t id, rkl_iter_t *iter); +MDBX_MAYBE_UNUSED MDBX_NOTHROW_PURE_FUNCTION __must_check_result MDBX_INTERNAL bool rkl_contain(const rkl_t *rkl, + txnid_t id); + +typedef struct MDBX_rkl_hole { + txnid_t begin; + txnid_t end; +} rkl_hole_t; +MDBX_MAYBE_UNUSED MDBX_INTERNAL __must_check_result rkl_hole_t rkl_hole(rkl_iter_t *iter, const bool reverse); diff --git a/src/tree-ops.c b/src/tree-ops.c index cc9acf48..f34f064e 100644 --- a/src/tree-ops.c +++ b/src/tree-ops.c @@ -38,11 +38,10 @@ static MDBX_cursor *cursor_clone(const MDBX_cursor *csrc, cursor_couple_t *coupl /*----------------------------------------------------------------------------*/ void recalculate_merge_thresholds(MDBX_env *env) { - const size_t bytes = page_space(env); - env->merge_threshold = (uint16_t)(bytes - (bytes * env->options.merge_threshold_16dot16_percent >> 16)); - env->merge_threshold_gc = - (uint16_t)(bytes - ((env->options.merge_threshold_16dot16_percent > 19005) ? bytes / 3 /* 33 % */ - : bytes / 4 /* 25 % */)); + const size_t whole_page_space = page_space(env); + env->merge_threshold = + (uint16_t)(whole_page_space - (whole_page_space * env->options.merge_threshold_16dot16_percent >> 16)); + eASSERT(env, env->merge_threshold >= whole_page_space / 2 && env->merge_threshold <= whole_page_space / 64 * 63); } int tree_drop(MDBX_cursor *mc, const bool may_have_tables) { @@ -446,8 +445,8 @@ static int page_merge(MDBX_cursor *csrc, MDBX_cursor *cdst) { cASSERT(cdst, cdst->top > 0); cASSERT(cdst, cdst->top + 1 < cdst->tree->height || is_leaf(cdst->pg[cdst->tree->height - 1])); cASSERT(csrc, csrc->top + 1 < csrc->tree->height || is_leaf(csrc->pg[csrc->tree->height - 1])); - cASSERT(cdst, - csrc->txn->env->options.prefer_waf_insteadof_balance || page_room(pdst) >= page_used(cdst->txn->env, psrc)); + cASSERT(cdst, cursor_dbi(csrc) == FREE_DBI || csrc->txn->env->options.prefer_waf_insteadof_balance || + page_room(pdst) >= page_used(cdst->txn->env, psrc)); const int pagetype = page_type(psrc); /* Move all nodes from src to dst */ @@ -680,8 +679,18 @@ int tree_rebalance(MDBX_cursor *mc) { const size_t minkeys = (pagetype & P_BRANCH) + (size_t)1; /* Pages emptier than this are candidates for merging. */ - size_t room_threshold = - likely(mc->tree != &mc->txn->dbs[FREE_DBI]) ? mc->txn->env->merge_threshold : mc->txn->env->merge_threshold_gc; + size_t room_threshold = mc->txn->env->merge_threshold; + bool minimize_waf = mc->txn->env->options.prefer_waf_insteadof_balance; + if (unlikely(mc->tree == &mc->txn->dbs[FREE_DBI])) { + /* В случае GC всегда минимизируем WAF, а рыхлые страницы объединяем только при наличии запаса в gc_stockpile(). + * Это позволяет уменьшить WAF и избавиться от лишних действий/циклов как при переработке GC, + * так и при возврате неиспользованных страниц. Сбалансированность b-tree при этом почти не деградирует, + * ибо добавление/удаление/обновление запиcей происходит почти всегда только по краям. */ + minimize_waf = true; + room_threshold = page_space(mc->txn->env); + if (gc_stockpile(mc->txn) > mc->tree->height + mc->tree->height) + room_threshold >>= 1; + } const size_t numkeys = page_numkeys(tp); const size_t room = page_room(tp); @@ -802,10 +811,26 @@ int tree_rebalance(MDBX_cursor *mc) { const size_t right_room = right ? page_room(right) : 0; const size_t left_nkeys = left ? page_numkeys(left) : 0; const size_t right_nkeys = right ? page_numkeys(right) : 0; + + /* Нужно выбрать между правой и левой страницами для слияния текущей или перемещения узла в текущую. + * Таким образом, нужно выбрать один из четырёх вариантов согласно критериям. + * + * Если включен minimize_waf, то стараемся не вовлекать чистые страницы, + * пренебрегая идеальностью баланса ради уменьшения WAF. + * + * При этом отдельные варианты могут быть не доступны, либо "не сработать" из-за того что: + * - в какой-то branch-странице не хватит места из-за распространения/обновления первых ключей, + * которые хранятся в родительских страницах; + * - при включенном minimize_waf распространение/обновление первых ключей + * потребуется разделение какой-либо странице, что увеличит WAF и поэтому обесценивает дальнейшее + * следование minimize_waf. */ + bool involve = !(left && right); retry: cASSERT(mc, mc->top > 0); - if (left_room > room_threshold && left_room >= right_room && (is_modifable(mc->txn, left) || involve)) { + const bool consider_left = left && (involve || is_modifable(mc->txn, left)); + const bool consider_right = right && (involve || is_modifable(mc->txn, right)); + if (consider_left && left_room > room_threshold && left_room >= right_room) { /* try merge with left */ cASSERT(mc, left_nkeys >= minkeys); mn->pg[mn->top] = left; @@ -825,7 +850,7 @@ retry: return rc; } } - if (right_room > room_threshold && (is_modifable(mc->txn, right) || involve)) { + if (consider_right && right_room > room_threshold) { /* try merge with right */ cASSERT(mc, right_nkeys >= minkeys); mn->pg[mn->top] = right; @@ -843,8 +868,7 @@ retry: } } - if (left_nkeys > minkeys && (right_nkeys <= left_nkeys || right_room >= left_room) && - (is_modifable(mc->txn, left) || involve)) { + if (consider_left && left_nkeys > minkeys && (right_nkeys <= left_nkeys || right_room >= left_room)) { /* try move from left */ mn->pg[mn->top] = left; mn->ki[mn->top - 1] = (indx_t)(ki_pre_top - 1); @@ -860,7 +884,7 @@ retry: return rc; } } - if (right_nkeys > minkeys && (is_modifable(mc->txn, right) || involve)) { + if (consider_right && right_nkeys > minkeys) { /* try move from right */ mn->pg[mn->top] = right; mn->ki[mn->top - 1] = (indx_t)(ki_pre_top + 1); @@ -884,17 +908,20 @@ retry: return MDBX_SUCCESS; } - if (mc->txn->env->options.prefer_waf_insteadof_balance && likely(room_threshold > 0)) { + if (minimize_waf && room_threshold > 0) { + /* Если включен minimize_waf, то переходим к попыткам слияния с сильно + * заполненными страницами до вовлечения чистых страниц (не измененных в этой транзакции) */ room_threshold = 0; goto retry; } - if (likely(!involve) && - (likely(mc->tree != &mc->txn->dbs[FREE_DBI]) || mc->txn->wr.loose_pages || MDBX_PNL_GETSIZE(mc->txn->wr.repnl) || - (mc->flags & z_gcu_preparation) || (mc->txn->flags & txn_gc_drained) || room_threshold)) { + if (!involve) { + /* Теперь допускаем вовлечение чистых страниц (не измененных в этой транзакции), + * что улучшает баланс в дереве, но увеличивает WAF. */ involve = true; goto retry; } - if (likely(room_threshold > 0)) { + if (room_threshold > 0) { + /* Если не нашли подходящей соседней, то допускаем слияние с сильно заполненными страницами */ room_threshold = 0; goto retry; } @@ -1228,6 +1255,7 @@ int page_split(MDBX_cursor *mc, const MDBX_val *const newkey, MDBX_val *const ne /* root split? */ prev_top += mc->top - top; + cASSERT(mn, prev_top <= mn->top && prev_top <= mc->top); /* Right page might now have changed parent. * Check if left page also changed parent. */ 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 41afc209..25856fda 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); @@ -122,10 +125,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; @@ -141,6 +143,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); @@ -259,9 +263,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)) @@ -335,13 +349,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 719e11ce..5c5bbde5 100644 --- a/src/txn-nested.c +++ b/src/txn-nested.c @@ -357,6 +357,7 @@ int txn_nested_create(MDBX_txn *parent, const MDBX_txn_flags_t flags) { txn->env->txn = txn; txn->owner = parent->owner; txn->wr.troika = parent->wr.troika; + rkl_init(&txn->wr.gc.reclaimed); #if MDBX_ENABLE_DBI_SPARSE txn->dbi_sparse = parent->dbi_sparse; @@ -411,12 +412,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); @@ -433,6 +433,7 @@ int txn_nested_create(MDBX_txn *parent, const MDBX_txn_flags_t flags) { tASSERT(txn, txn->wr.dirtyroom + txn->wr.dirtylist->length == (txn->parent ? txn->parent->wr.dirtyroom : txn->env->options.dp_limit)); tASSERT(parent, parent->cursors[FREE_DBI] == nullptr); + // TODO: shadow GC' cursor return txn_shadow_cursors(parent, MAIN_DBI); } @@ -442,11 +443,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); @@ -522,17 +519,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 321e9a52..fd161ac4 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) { @@ -420,12 +430,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 { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dda4a909..5d79c054 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -298,6 +298,7 @@ else() add_extra_test(upsert_alldups SOURCE extra/upsert_alldups.c) add_extra_test(dupfix_addodd SOURCE extra/dupfix_addodd.c) endif() + add_extra_test(details_rkl SOURCE extra/details_rkl.c) if(MDBX_BUILD_CXX) if(NOT WIN32 OR NOT MDBX_CXX_STANDARD LESS 17) add_extra_test(cursor_closing TIMEOUT 10800) diff --git a/test/extra/details_rkl.c b/test/extra/details_rkl.c new file mode 100644 index 00000000..29a0120e --- /dev/null +++ b/test/extra/details_rkl.c @@ -0,0 +1,488 @@ +/// \copyright SPDX-License-Identifier: Apache-2.0 +/// \author Леонид Юрьев aka Leonid Yuriev \date 2025 + +#define debug_log debug_log_sub + +#include "../../src/rkl.c" +#include "../../src/txl.c" + +MDBX_MAYBE_UNUSED __cold void debug_log_sub(int level, const char *function, int line, const char *fmt, ...) { + (void)level; + (void)function; + (void)line; + (void)fmt; +} + +/*-----------------------------------------------------------------------------*/ + +static size_t tst_failed, tst_ok, tst_iterations, tst_cases, tst_cases_hole; +#ifndef NDEBUG +static size_t tst_target; +#endif + +static bool check_bool(bool v, bool expect, const char *fn, unsigned line) { + if (unlikely(v != expect)) { + ++tst_failed; + fflush(nullptr); + fprintf(stderr, "iteration %zi: got %s, expected %s, at %s:%u\n", tst_iterations, v ? "true" : "false", + expect ? "true" : "false", fn, line); + fflush(nullptr); + return false; + } + ++tst_ok; + return true; +} + +static bool check_eq(uint64_t v, uint64_t expect, const char *fn, unsigned line) { + if (unlikely(v != expect)) { + ++tst_failed; + fflush(nullptr); + fprintf(stderr, "iteration %zi: %" PRIu64 " (got) != %" PRIu64 " (expected), at %s:%u\n", tst_iterations, v, expect, + fn, line); + fflush(nullptr); + return false; + } + ++tst_ok; + return true; +} + +#define CHECK_BOOL(T, EXPECT) check_bool((T), (EXPECT), __func__, __LINE__) +#define CHECK_TRUE(T) CHECK_BOOL(T, true) +#define CHECK_FALSE(T) CHECK_BOOL(T, false) +#define CHECK_EQ(T, EXPECT) check_eq((T), (EXPECT), __func__, __LINE__) + +void trivia(void) { + rkl_t x, y; + + rkl_init(&x); + rkl_init(&y); + CHECK_TRUE(rkl_check(&x)); + CHECK_TRUE(rkl_empty(&x)); + CHECK_EQ(rkl_len(&x), 0); + + rkl_iter_t f = rkl_iterator(&x, false); + rkl_iter_t r = rkl_iterator(&x, true); + CHECK_EQ(rkl_left(&f, false), 0); + CHECK_EQ(rkl_left(&f, true), 0); + CHECK_EQ(rkl_left(&r, false), 0); + CHECK_EQ(rkl_left(&r, true), 0); + CHECK_EQ(rkl_turn(&f, false), 0); + CHECK_EQ(rkl_turn(&f, true), 0); + CHECK_EQ(rkl_turn(&r, false), 0); + CHECK_EQ(rkl_turn(&r, true), 0); + CHECK_TRUE(rkl_check(&x)); + + rkl_hole_t hole; + hole = rkl_hole(&f, true); + CHECK_EQ(hole.begin, 1); + CHECK_EQ(hole.end, MAX_TXNID); + hole = rkl_hole(&f, false); + CHECK_EQ(hole.begin, 1); + CHECK_EQ(hole.end, MAX_TXNID); + hole = rkl_hole(&r, true); + CHECK_EQ(hole.begin, 1); + CHECK_EQ(hole.end, MAX_TXNID); + hole = rkl_hole(&r, false); + CHECK_EQ(hole.begin, 1); + CHECK_EQ(hole.end, MAX_TXNID); + + CHECK_EQ(rkl_push(&x, 42, false), MDBX_SUCCESS); + CHECK_TRUE(rkl_check(&x)); + CHECK_FALSE(rkl_empty(&x)); + CHECK_EQ(rkl_len(&x), 1); + CHECK_EQ(rkl_push(&x, 42, true), MDBX_RESULT_TRUE); + CHECK_TRUE(rkl_check(&x)); + + f = rkl_iterator(&x, false); + r = rkl_iterator(&x, true); + CHECK_EQ(rkl_left(&f, false), 1); + CHECK_EQ(rkl_left(&f, true), 0); + CHECK_EQ(rkl_left(&r, false), 0); + CHECK_EQ(rkl_left(&r, true), 1); + + CHECK_EQ(rkl_turn(&f, true), 0); + CHECK_EQ(rkl_turn(&f, false), 42); + CHECK_EQ(rkl_turn(&f, false), 0); + CHECK_EQ(rkl_turn(&f, true), 42); + CHECK_EQ(rkl_turn(&f, true), 0); + + CHECK_EQ(rkl_turn(&r, false), 0); + CHECK_EQ(rkl_turn(&r, true), 42); + CHECK_EQ(rkl_turn(&r, true), 0); + CHECK_EQ(rkl_turn(&r, false), 42); + CHECK_EQ(rkl_turn(&r, false), 0); + + f = rkl_iterator(&x, false); + hole = rkl_hole(&f, false); + CHECK_EQ(hole.begin, 43); + CHECK_EQ(hole.end, MAX_TXNID); + hole = rkl_hole(&f, false); + CHECK_EQ(hole.begin, MAX_TXNID); + CHECK_EQ(hole.end, MAX_TXNID); + hole = rkl_hole(&f, true); + CHECK_EQ(hole.begin, 43); + CHECK_EQ(hole.end, MAX_TXNID); + hole = rkl_hole(&f, true); + CHECK_EQ(hole.begin, 1); + CHECK_EQ(hole.end, 42); + hole = rkl_hole(&f, true); + CHECK_EQ(hole.begin, 1); + CHECK_EQ(hole.end, 42); + + r = rkl_iterator(&x, true); + hole = rkl_hole(&r, false); + CHECK_EQ(hole.begin, MAX_TXNID); + CHECK_EQ(hole.end, MAX_TXNID); + hole = rkl_hole(&r, true); + CHECK_EQ(hole.begin, 43); + CHECK_EQ(hole.end, MAX_TXNID); + hole = rkl_hole(&r, true); + CHECK_EQ(hole.begin, 1); + CHECK_EQ(hole.end, 42); + hole = rkl_hole(&r, false); + CHECK_EQ(hole.begin, 43); + CHECK_EQ(hole.end, MAX_TXNID); + hole = rkl_hole(&r, false); + CHECK_EQ(hole.begin, MAX_TXNID); + CHECK_EQ(hole.end, MAX_TXNID); + + rkl_resize(&x, 222); + CHECK_FALSE(rkl_empty(&x)); + CHECK_TRUE(rkl_check(&x)); + + rkl_destructive_move(&x, &y); + CHECK_TRUE(rkl_check(&x)); + CHECK_TRUE(rkl_check(&y)); + rkl_destroy(&x); + rkl_destroy(&y); +} + +/*-----------------------------------------------------------------------------*/ + +uint64_t prng_state; + +static uint64_t prng(void) { + prng_state = prng_state * UINT64_C(6364136223846793005) + 1; + return prng_state; +} + +static bool flipcoin(void) { return (bool)prng() & 1; } + +static bool stochastic_pass(const unsigned start, const unsigned width, const unsigned n) { + rkl_t k, c; + txl_t l = txl_alloc(); + if (!CHECK_TRUE(l)) + return false; + + rkl_init(&k); + rkl_init(&c); + const size_t errors = tst_failed; + + rkl_iter_t f = rkl_iterator(&k, false); + rkl_iter_t r = rkl_iterator(&k, true); + + txnid_t lowest = UINT_MAX; + txnid_t highest = 0; + while (MDBX_PNL_GETSIZE(l) < n) { + txnid_t id = (txnid_t)(prng() % width + start); + if (id < MIN_TXNID || id >= INVALID_TXNID) + continue; + if (txl_contain(l, id)) { + if (CHECK_TRUE(rkl_contain(&k, id)) && CHECK_EQ(rkl_push(&k, id, false), MDBX_RESULT_TRUE)) + continue; + break; + } + if (!CHECK_FALSE(rkl_contain(&k, id))) + break; + + if (tst_iterations % (1u << 24) == 0 && tst_iterations) { + printf("done %.3fM iteration, %zu cases\n", tst_iterations / 1000000.0, tst_cases); + fflush(nullptr); + } + tst_iterations += 1; + +#ifndef NDEBUG + if (tst_iterations == tst_target) { + printf("reach %zu iteration\n", tst_iterations); + fflush(nullptr); + } +#endif + + if (!CHECK_EQ(rkl_push(&k, id, false), MDBX_SUCCESS)) + break; + if (!CHECK_TRUE(rkl_check(&k))) + break; + if (!CHECK_EQ(txl_append(&l, id), MDBX_SUCCESS)) + break; + if (!CHECK_TRUE(rkl_contain(&k, id))) + break; + + lowest = (lowest < id) ? lowest : id; + highest = (highest > id) ? highest : id; + if (!CHECK_EQ(rkl_lowest(&k), lowest)) + break; + if (!CHECK_EQ(rkl_highest(&k), highest)) + break; + } + + txl_sort(l); + CHECK_EQ(rkl_len(&k), n); + CHECK_EQ(MDBX_PNL_GETSIZE(l), n); + + f = rkl_iterator(&k, false); + r = rkl_iterator(&k, true); + CHECK_EQ(rkl_left(&f, false), n); + CHECK_EQ(rkl_left(&f, true), 0); + CHECK_EQ(rkl_left(&r, false), 0); + CHECK_EQ(rkl_left(&r, true), n); + + for (size_t i = 0; i < n; ++i) { + CHECK_EQ(rkl_turn(&f, false), l[n - i]); + CHECK_EQ(rkl_left(&f, false), n - i - 1); + CHECK_EQ(rkl_left(&f, true), i + 1); + + CHECK_EQ(rkl_turn(&r, true), l[i + 1]); + r.pos += 1; + CHECK_EQ(rkl_turn(&r, true), l[i + 1]); + CHECK_EQ(rkl_left(&r, true), n - i - 1); + CHECK_EQ(rkl_left(&r, false), i + 1); + } + + if (CHECK_EQ(rkl_copy(&k, &c), MDBX_SUCCESS)) { + for (size_t i = 1; i <= n; ++i) { + if (!CHECK_FALSE(rkl_empty(&k))) + break; + if (!CHECK_FALSE(rkl_empty(&c))) + break; + CHECK_EQ(rkl_pop(&k, true), l[i]); + CHECK_EQ(rkl_pop(&c, false), l[1 + n - i]); + } + } + + CHECK_TRUE(rkl_empty(&k)); + CHECK_TRUE(rkl_empty(&c)); + + rkl_destroy(&k); + rkl_destroy(&c); + txl_free(l); + + ++tst_cases; + return errors == tst_failed; +} + +static bool stochastic(const size_t limit_cases, const size_t limit_loops) { + for (unsigned loop = 0; tst_cases < limit_cases || loop < limit_loops; ++loop) + for (unsigned width = 2; width < 10; ++width) + for (unsigned n = 1; n < width; ++n) + for (unsigned prev = 1, start = 0, t; start < 4242; t = start + prev, prev = start, start = t) + if (!stochastic_pass(start, 1u << width, 1u << n) || tst_failed > 42) { + puts("bailout\n"); + return false; + } + return true; +} + +/*-----------------------------------------------------------------------------*/ + +static bool bit(size_t set, size_t n) { + assert(n < CHAR_BIT * sizeof(set)); + return (set >> n) & 1; +} + +static size_t hamming_weight(size_t v) { + const size_t m1 = (size_t)UINT64_C(0x5555555555555555); + const size_t m2 = (size_t)UINT64_C(0x3333333333333333); + const size_t m4 = (size_t)UINT64_C(0x0f0f0f0f0f0f0f0f); + const size_t h01 = (size_t)UINT64_C(0x0101010101010101); + v -= (v >> 1) & m1; + v = (v & m2) + ((v >> 2) & m2); + v = (v + (v >> 4)) & m4; + return (v * h01) >> (sizeof(v) * 8 - 8); +} + +static bool check_hole(const size_t set, const rkl_hole_t hole, size_t *acc) { + const size_t errors = tst_failed; + ++tst_iterations; + + if (hole.begin > 1) + CHECK_EQ(bit(set, hole.begin - 1), 1); + if (hole.end < CHAR_BIT * sizeof(set)) + CHECK_EQ(bit(set, hole.end), 1); + + for (size_t n = hole.begin; n < hole.end && n < CHAR_BIT * sizeof(set); n++) { + CHECK_EQ(bit(set, n), 0); + *acc += 1; + } + + return errors == tst_failed; +} + +static void debug_set(const size_t set, const char *str, int iter_offset) { +#if 1 + (void)set; + (void)str; + (void)iter_offset; +#else + printf("\ncase %s+%d: count %zu, holes", str, iter_offset, hamming_weight(~set) - 1); + for (size_t k, i = 1; i < CHAR_BIT * sizeof(set); ++i) { + if (!bit(set, i)) { + printf(" %zu", i); + for (k = i; k < CHAR_BIT * sizeof(set) - 1 && !bit(set, k + 1); ++k) + ; + if (k > i) { + printf("-%zu", k); + i = k; + } + } + } + printf("\n"); + fflush(nullptr); +#endif +} + +static bool check_holes_bothsides(const size_t set, rkl_iter_t const *i) { + const size_t number_of_holes = hamming_weight(~set) - 1; + size_t acc = 0; + + rkl_iter_t f = *i; + for (;;) { + rkl_hole_t hole = rkl_hole(&f, false); + if (hole.begin == hole.end) + break; + if (!check_hole(set, hole, &acc)) + return false; + if (hole.end >= CHAR_BIT * sizeof(set)) + break; + } + + rkl_iter_t b = *i; + for (;;) { + rkl_hole_t hole = rkl_hole(&b, true); + if (hole.begin == hole.end) + break; + if (!check_hole(set, hole, &acc)) + return false; + if (hole.begin == 1) + break; + } + + if (!CHECK_EQ(acc, number_of_holes)) + return false; + + return true; +} + +static bool check_holes_fourways(const size_t set, const rkl_t *rkl) { + rkl_iter_t i = rkl_iterator(rkl, false); + int o = 0; + do { + debug_set(set, "initial-forward", o++); + if (!check_holes_bothsides(set, &i)) + return false; + } while (rkl_turn(&i, false)); + + do { + debug_set(set, "recoil-reverse", --o); + if (!check_holes_bothsides(set, &i)) + return false; + } while (rkl_turn(&i, true)); + + i = rkl_iterator(rkl, true); + o = 0; + do { + debug_set(set, "initial-reverse", --o); + if (!check_holes_bothsides(set, &i)) + return false; + } while (rkl_turn(&i, false)); + + do { + debug_set(set, "recoil-forward", o++); + if (!check_holes_bothsides(set, &i)) + return false; + } while (rkl_turn(&i, true)); + + return true; +} + +static bool stochastic_pass_hole(size_t set, size_t trims) { + const size_t one = 1; + set &= ~one; + if (!set) + return true; + + ++tst_cases_hole; + + rkl_t rkl; + rkl_init(&rkl); + for (size_t n = 1; n < CHAR_BIT * sizeof(set); ++n) + if (bit(set, n)) + CHECK_EQ(rkl_push(&rkl, n, false), MDBX_SUCCESS); + + if (!check_holes_fourways(set, &rkl)) + return false; + + while (rkl_len(&rkl) > 1 && trims-- > 0) { + if (flipcoin()) { + const size_t l = (size_t)rkl_pop(&rkl, false); + if (l == 0) + break; + assert(bit(set, l)); + set -= one << l; + if (!check_holes_fourways(set, &rkl)) + return false; + } else { + + const size_t h = (size_t)rkl_pop(&rkl, true); + if (h == 0) + break; + assert(bit(set, h)); + set -= one << h; + if (!check_holes_fourways(set, &rkl)) + return false; + } + } + + return true; +} + +static size_t prng_word(void) { + size_t word = (size_t)(prng() >> 32); + if (sizeof(word) > 4) + word = (uint64_t)word << 32 | (size_t)(prng() >> 32); + return word; +} + +static bool stochastic_hole(size_t probes) { + for (size_t n = 0; n < probes; ++n) { + size_t set = prng_word(); + if (!stochastic_pass_hole(set, prng() % 11)) + return false; + if (!stochastic_pass_hole(set & prng_word(), prng() % 11)) + return false; + if (!stochastic_pass_hole(set | prng_word(), prng() % 11)) + return false; + } + return true; +} + +/*-----------------------------------------------------------------------------*/ + +int main(int argc, const char *argv[]) { + (void)argc; + (void)argv; + +#ifndef NDEBUG + // tst_target = 281870; +#endif + prng_state = (uint64_t)time(nullptr); + printf("prng-seed %" PRIu64 "\n", prng_state); + fflush(nullptr); + + trivia(); + stochastic(42 * 42 * 42, 42); + stochastic_hole(24 * 24 * 24); + printf("done: %zu+%zu cases, %zu iterations, %zu checks ok, %zu checks failed\n", tst_cases, tst_cases_hole, + tst_iterations, tst_ok, tst_failed); + fflush(nullptr); + return tst_failed ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/test/main.c++ b/test/main.c++ index bc2bee70..1f879975 100644 --- a/test/main.c++ +++ b/test/main.c++ @@ -460,9 +460,9 @@ int main(int argc, char *const argv[]) { params.datalen_max = params.datalen_min; continue; } - if (config::parse_option(argc, argv, narg, "batch.read", params.batch_read, config::no_scale, 1)) + if (config::parse_option(argc, argv, narg, "batch.read", params.batch_read, config::decimal, 1)) continue; - if (config::parse_option(argc, argv, narg, "batch.write", params.batch_write, config::no_scale, 1)) + if (config::parse_option(argc, argv, narg, "batch.write", params.batch_write, config::decimal, 1)) continue; if (config::parse_option(argc, argv, narg, "delay", params.delaystart, config::duration)) continue;