Files
libmdbx/mdbx_defrag.c
Леонид Юрьев (Leonid Yuriev) 81eaf88944 mdbx++: provide copy content for move-assignment of mdbx::buffer for non-equal allocators case.
2026-07-13 mdbx: Update ChangeLog.
2026-07-12 mdbx-tools: fix/remove leftover usage of float point in `mdbx_stat`.
2026-07-12 mdbx: don't fail transaction on `MDBX_UNABLE_EXTEND_MAPSIZE`.
2026-07-12 mdbx: replace `reasonable_db_maxsize()` with `globals.assume_ram_pages` and `globals.reasonable_db_maxsize`.
2026-07-12 mdbx: avoid false-positive warning from Coverity.
2026-07-12 mdbx-make: more MDBX_CHECKING for `test-*` targets.
2026-07-12 mdbx-doc: more minor typos fixing.
2026-07-12 mdbx++: provide copy content for move-assignment of `mdbx::buffer` for non-equal allocators case.
2026-07-11 mdbx: introduce `globals.mmap_limit` and adjusting mmap-limit for ASAN/Valgrind.
2026-07-11 mdbx-tools: refine `mdbx_load`.
2026-07-11 mdbx-tools: fix 2Gb ram consumption by `mdbx_load` due leak of debug changes.
2026-07-11 mdbx-make: refine `test-*` and `smoke-*` targets.
2026-07-10 mdbx: more `LOG_IFERR(err)` into the opening path.
2026-07-10 mdbx-test: refine `copy-db` testcases, add assertions for `copy_pathname.empty()`.
2026-07-10 mdbx: fix dummy `EINVAL` inside `mdbx_chk`.
2026-07-10 mdbx: introduce `osal_fseek_shut()` with workaround for `EINVAL` from `lseek()` with huge offsets.
2026-07-10 mdbx: fix ODR-warnings from modern GCC while both LTO and UBSAN enabled.
2026-07-10 mdbx-make: fix `CTEST_OPT` for `test-memcheck` target.
2026-07-10 mdbx-test: move `valgrind_suppress.txt` and corresponding cmake-definitions.
2026-07-13 02:41:02 +03:00

447 lines
16 KiB
C

/* This file is part of the libmdbx amalgamated source code (v0.14.2-293-g43618122 at 2026-07-13T02:40:29+03:00).
*
* libmdbx (aka MDBX) is an extremely fast, compact, powerful, embeddedable, transactional key-value storage engine with
* open-source code. MDBX has a specific set of properties and capabilities, focused on creating unique lightweight
* solutions. Please visit https://libmdbx.dqdkfa.ru for more information, changelog, documentation, C++ API description
* and links to the original git repo with the source code. Questions, feedback and suggestions are welcome to the
* Telegram' group https://t.me/libmdbx, MAX' chat https://max.ru/join/dKckvyuARxp1vRK-wnPur8zYCEkbR3OUOmpPWkWxp78.
*
* The libmdbx code will forever remain open and with high-quality free support, as far as the life circumstances of the
* project participants allow. Donations are welcome to ETH `0xD104d8f8B2dC312aaD74899F83EBf3EEBDC1EA3A`,
* BTC `bc1qzvl9uegf2ea6cwlytnanrscyv8snwsvrc0xfsu`, SOL `FTCTgbHajoLVZGr8aEFWMzx3NDMyS5wXJgfeMTmJznRi`.
* Всё будет хорошо!
*
* For ease of use and to eliminate potential limitations in both distribution and obstacles in technology development,
* libmdbx is distributed as an amalgamated source code starting at the end of 2025. The source code of the tests, as
* well as the internal documentation, will be available only to the team directly involved in the development.
*
* Copyright 2015-2026 Леонид Юрьев aka Leonid Yuriev <leo@yuriev.ru>
* SPDX-License-Identifier: Apache-2.0
*
* For notes about the license change, credits and acknowledgments, please refer to the COPYRIGHT file. */
/* clang-format off */
#define xMDBX_TOOLS /* Avoid using internal ASSERT(), etc */
#include "mdbx-internals.h"
#if IS_WINDOWS
/* Bit of madness for Windows console */
#define mdbx_strerror mdbx_strerror_ANSI2OEM
#define mdbx_strerror_r mdbx_strerror_r_ANSI2OEM
#include "mdbx-wingetopt.h"
static volatile BOOL user_break;
static BOOL WINAPI ConsoleBreakHandlerRoutine(DWORD dwCtrlType) {
(void)dwCtrlType;
user_break = user_break ? 2 : 1;
return true;
}
#else /* WINDOWS */
static volatile sig_atomic_t user_break;
static void signal_handler(int sig) {
(void)sig;
user_break = user_break ? 2 : 1;
}
#endif /* !WINDOWS */
static char *prog;
static void usage(void) {
fprintf(stderr,
"usage: %s [-V] [-v[v[v...]]] [-q] [-1..9] [-t seconds] [-f percent] [-r percent] [-s megabytes] [-c] [-u|U] "
"db_pathname\n"
" -V\t\tprint version and exit\n"
" -v\t\tmore verbose, could be repeated for extra details from debug-enabled builds.\n"
" -q\t\tbe quiet.\n"
" -1\t\tperform a single quick defragmentation cycle without two-stage moves.\n"
" -2 .. -9\tlimits the number of defragmentation cycles.\n"
" -t seconds\tlimits the duration of defragmentation in seconds.\n"
" -f percent\tdefragment until free up a given percentage of unused space (default 100).\n"
" -r percent\tacceptable undefragmented residue in percent of used space (0 by default).\n"
" -s megabytes\tpreferred defragmentation step/transaction size.\n"
" -c\t\tforce cooperative mode (don't try exclusive).\n"
" -u\t\twarmup database before defragmenting.\n"
" -U\t\twarmup and try lock database pages in memory before defragmenting.\n"
" db_pathname\tpath to the database.\n",
prog);
exit(EXIT_FAILURE);
}
static MDBX_log_level_t verbosity = MDBX_LOG_WARN;
static bool quiet = false;
static bool is_console = true;
static unsigned cycles_limit;
static unsigned progress_dots;
static void logger(MDBX_log_level_t level, const char *function, int line, const char *fmt, va_list args) {
static const char *const prefixes[] = {
"!!!fatal: ", // 0 fatal
" ! ", // 1 error
" ~ ", // 2 warning
" ", // 3 notice
" // ", // 4 verbose
" //// ", // 5 verbose
" ////// ", // 6 verbose
" //////// ", // 7 verbose
};
if (!quiet && level < MDBX_LOG_DEBUG) {
if (progress_dots) {
putchar(' ');
putchar('\n');
progress_dots = 0;
if (level <= MDBX_LOG_NOTICE)
fflush(nullptr);
}
FILE *out = (level < MDBX_LOG_NOTICE) ? stderr : stdout;
if (function && line)
fprintf(out, "%s", prefixes[level]);
vfprintf(out, fmt, args);
if (level <= MDBX_LOG_NOTICE)
fflush(nullptr);
}
}
static void defrag_report_progress(const MDBX_defrag_result_t *progress, unsigned dots) {
if (!quiet) {
if (progress->cycles == 0)
printf("\r - loading the b-tree structure: %u.%u%%", progress->rough_estimation_cycle_progress_permille / 10,
progress->rough_estimation_cycle_progress_permille % 10);
else {
printf("\r - cycle %u: %u.%u%%", progress->cycles, progress->rough_estimation_cycle_progress_permille / 10,
progress->rough_estimation_cycle_progress_permille % 10);
if (progress->pages_moved) {
printf(", moved %zi", progress->pages_moved);
if (progress->pages_moved < progress->pages_scheduled)
printf(" of %zi", progress->pages_scheduled);
} else if (progress->pages_scheduled)
printf(", scheduled %zi", progress->pages_scheduled);
if (progress->pages_retained)
printf(", retained %zi", progress->pages_retained);
if (progress->pages_shrinked)
printf(", shrinked %zi", progress->pages_shrinked);
printf(", left %zi", progress->pages_left);
}
for (unsigned i = 0; i < 3 || (i < dots / 8 && i < 64); ++i)
putchar('.');
if (is_console && dots) {
static char twirl[] = "\\|/-\\|/-";
putchar(twirl[(progress->spent_time_dot16 >> 13) % (ARRAY_LENGTH(twirl) - 1)]);
putchar('\b');
}
fflush(nullptr);
}
}
static const char *stop_reason(const MDBX_defrag_result_t *progress) {
if (progress->stopping_reasons & MDBX_defrag_error)
return "error";
if (progress->stopping_reasons & MDBX_defrag_aborted)
return "aborted by user";
if (progress->stopping_reasons & MDBX_defrag_discontinued)
return "discontinued by user";
if (progress->stopping_reasons & MDBX_defrag_enough_threshold)
return "enough threshold";
if (progress->stopping_reasons & MDBX_defrag_time_limit)
return "time limit reached";
if (progress->stopping_reasons & MDBX_defrag_laggard_reader)
return "laggard reader";
if (progress->stopping_reasons & MDBX_defrag_large_chunk)
return "large chunk";
if (progress->stopping_reasons & MDBX_defrag_step_size)
return "step size";
return "done";
}
static int defrag_notify(void *ctx, const MDBX_defrag_result_t *progress) {
(void)ctx;
if (!quiet) {
static MDBX_defrag_result_t last_progress = {.cycles = UINT32_MAX};
static uint32_t last_report_spenttime;
bool tick = progress->spent_time_dot16 - last_report_spenttime > 65536 / 8;
if (!ctx || last_progress.cycles != progress->cycles) {
if (progress_dots) {
if (last_progress.cycles == progress->cycles)
last_progress.stopping_reasons = progress->stopping_reasons;
defrag_report_progress(&last_progress, progress_dots);
progress_dots = 0;
printf(" %s%s\n", stop_reason(&last_progress), is_console ? "\033[K" : "");
fflush(nullptr);
}
tick = true;
}
if (ctx && tick) {
last_report_spenttime = progress->spent_time_dot16;
defrag_report_progress(progress, progress_dots++);
}
last_progress = *progress;
}
if (user_break > 1)
return -1;
if (cycles_limit && (progress->cycles > cycles_limit ||
(progress->cycles == cycles_limit && (progress->stopping_reasons & MDBX_defrag_large_chunk))))
return 1;
return user_break;
}
int main(int argc, char *argv[]) {
int rc;
MDBX_env *env = nullptr;
const char *const progname = argv[0];
bool warmup = false;
size_t time_limit_seconds = 0;
unsigned acceptable_residue_percent = 0;
unsigned wanna_defrag_percent = 100;
size_t step_size_MiB = 0;
MDBX_env_flags_t env_flags = MDBX_ENV_DEFAULTS | MDBX_EXCLUSIVE;
MDBX_warmup_flags_t warmup_flags = MDBX_warmup_default;
prog = argv[0];
if (argc < 2)
usage();
for (int i; (i = getopt(argc, argv,
"V"
"v"
"q"
"123456789"
"s:"
"t:"
"f:"
"r:"
"c"
"u"
"U")) != EOF;) {
switch (i) {
case 'V':
printf("mdbx_defrag version %d.%d.%d.%d\n"
" - source: %s %s, commit %s, tree %s\n"
" - anchor: %s\n"
" - build: %s for %s by %s\n"
" - flags: %s\n"
" - options: %s\n",
mdbx_version.major, mdbx_version.minor, mdbx_version.patch, mdbx_version.tweak, mdbx_version.git.describe,
mdbx_version.git.datetime, mdbx_version.git.commit, mdbx_version.git.tree, mdbx_sourcery_anchor,
mdbx_build.datetime, mdbx_build.target, mdbx_build.compiler, mdbx_build.flags, mdbx_build.options);
return EXIT_SUCCESS;
case 'v':
if (++verbosity > 9)
usage();
break;
case 'q':
quiet = true;
break;
case 'c':
env_flags = (env_flags & ~MDBX_EXCLUSIVE) | MDBX_ACCEDE;
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
cycles_limit = i - '0';
break;
case 't':
if (sscanf(optarg, "%zu", &time_limit_seconds) != 1) {
if (!quiet)
fprintf(stderr, "%s: %s option: expecting %s, but got '%s'\n", prog, "-t", "unsigned integer value", optarg);
return EXIT_FAILURE;
}
break;
case 'f':
if (sscanf(optarg, "%u", &wanna_defrag_percent) != 1 || wanna_defrag_percent < 1 || wanna_defrag_percent > 100) {
if (!quiet)
fprintf(stderr, "%s: %s option: expecting %s, but got '%s'\n", prog, "-f",
"unsigned integer value in range 1..100", optarg);
return EXIT_FAILURE;
}
break;
case 'r':
if (sscanf(optarg, "%u", &acceptable_residue_percent) != 1 || acceptable_residue_percent < 1 ||
acceptable_residue_percent > 100) {
if (!quiet)
fprintf(stderr, "%s: %s option: expecting %s, but got '%s'\n", prog, "-r",
"unsigned integer value in range 1..100", optarg);
return EXIT_FAILURE;
}
break;
case 's':
if (sscanf(optarg, "%zu", &step_size_MiB) != 1) {
if (!quiet)
fprintf(stderr, "%s: %s option: expecting %s, but got '%s'\n", prog, "-s",
"unsigned integer value in megabytes", optarg);
return EXIT_FAILURE;
}
break;
case 'u':
warmup = true;
break;
case 'U':
warmup_flags = MDBX_warmup_force | MDBX_warmup_touchlimit | MDBX_warmup_lock;
warmup = true;
break;
default:
usage();
}
}
if (optind != argc - 1)
usage();
#if IS_WINDOWS
SetConsoleCtrlHandler(ConsoleBreakHandlerRoutine, true);
is_console = _isatty(_fileno(stdout)) != 0;
#else
is_console = isatty(fileno(stdout)) == 1;
#ifdef SIGPIPE
signal(SIGPIPE, signal_handler);
#endif
#ifdef SIGHUP
signal(SIGHUP, signal_handler);
#endif
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
#endif /* !WINDOWS */
const char *const db_pathname = argv[optind];
if (!quiet) {
fprintf(stdout, "mdbx_defrag %s (%s, T-%s)\nRunning for %s...\n", mdbx_version.git.describe,
mdbx_version.git.datetime, mdbx_version.git.tree, db_pathname);
if (verbosity > MDBX_LOG_VERBOSE && MDBX_DEBUG < 1)
printf("Verbosity level %u exposures only to"
" a debug/extra-logging-enabled builds (MDBX_DEBUG > 0)\n",
verbosity);
mdbx_setup_debug(verbosity, MDBX_DBG_DONTCHANGE, logger);
fflush(nullptr);
}
const char *act = "opening environment";
rc = mdbx_env_create(&env);
if (rc == MDBX_SUCCESS)
rc = mdbx_env_open(env, db_pathname, env_flags, 0);
if ((env_flags & MDBX_EXCLUSIVE) && (rc == MDBX_BUSY ||
#if IS_WINDOWS
rc == ERROR_LOCK_VIOLATION || rc == ERROR_SHARING_VIOLATION
#else
rc == EBUSY || rc == EAGAIN
#endif
)) {
env_flags = (env_flags & ~MDBX_EXCLUSIVE) | MDBX_ACCEDE;
rc = mdbx_env_open(env, db_pathname, env_flags, 0);
}
if (rc == MDBX_SUCCESS && warmup) {
act = "warming up";
rc = mdbx_env_warmup(env, nullptr, warmup_flags, 3600 * 65536);
rc = MDBX_IS_ERROR(rc) ? rc : MDBX_SUCCESS;
}
MDBX_txn *txn = nullptr;
if (rc == MDBX_SUCCESS) {
act = "preparing";
rc = mdbx_txn_begin(env, nullptr, MDBX_TXN_READWRITE, &txn);
}
MDBX_envinfo info_env;
memset(&info_env, 0, sizeof(info_env)); /* zap `uninitialized` warning */
if (rc == MDBX_SUCCESS)
rc = mdbx_env_info_ex(env, txn, &info_env, sizeof(info_env));
MDBX_gc_info_t info_gc;
memset(&info_gc, 0, sizeof(info_gc)); /* zap `uninitialized` warning */
if (rc == MDBX_SUCCESS)
rc = mdbx_gc_info(txn, &info_gc, sizeof(info_gc), nullptr, nullptr);
char ratio_buffer[42];
if (rc == MDBX_SUCCESS) {
const size_t pages_per_MiB = MEGABYTE / info_env.mi_dxb_pagesize;
const size_t payload_pages = info_gc.pages_allocated - info_gc.pages_gc;
const size_t preferred_batch = (step_size_MiB > INT_MAX / pages_per_MiB) ? 0 : step_size_MiB * pages_per_MiB;
const size_t duration_limit_dot16 =
(time_limit_seconds > SIZE_MAX >> 16) ? /* unlimited */ 0 : time_limit_seconds << /* seconds to dot16 */ 16;
const size_t defrag_enough = (wanna_defrag_percent < 100u) ? (info_gc.pages_gc + 99u) / 100u : 0;
const intptr_t acceptable_residue =
acceptable_residue_percent ? (intptr_t)((payload_pages + 99u) / 100u * acceptable_residue_percent) : -1;
if (!quiet) {
printf(" - space usage in pages: %zu allocated, payload %zu, unused/GC %zu, pagesize %u\n",
info_gc.pages_allocated, payload_pages, info_gc.pages_gc, info_env.mi_dxb_pagesize);
printf(" - defragmentation: target %u%% (shrink %zu pages), cycles ", wanna_defrag_percent,
defrag_enough ? defrag_enough : info_gc.pages_gc);
if (cycles_limit)
printf("limit %u", cycles_limit);
else
printf("unlimited");
printf(", step ");
if (preferred_batch)
printf("%zu pages (%zu MiB)", preferred_batch, step_size_MiB);
else
printf("unlimited");
if (acceptable_residue_percent)
printf(", acceptable residue %u%% (leftover %zu pages)", acceptable_residue_percent, acceptable_residue);
printf(", duration ");
if (time_limit_seconds)
printf("limit %s seconds\n",
mdbx_ratio2digits(duration_limit_dot16, 65536, 1, ratio_buffer, sizeof(ratio_buffer)));
else
puts("unlimited");
fflush(nullptr);
}
act = "defragmenting";
MDBX_defrag_result_t result;
rc = mdbx_env_defrag(env,
/* defrag_atleast */ 0,
/* time_atleast_dot16 */ 0, defrag_enough, duration_limit_dot16, acceptable_residue,
preferred_batch, defrag_notify, env, &result);
defrag_notify(nullptr, &result);
if (!MDBX_IS_ERROR(rc)) {
if (!quiet) {
printf("Defragmentation%s: shrinked %zi pages, %u passes, moved %zu pages",
(rc == MDBX_SUCCESS) ? " done" : " incomplete", result.pages_shrinked, result.cycles,
result.pages_moved);
if (result.stopping_reasons)
printf(", stopping reasons bits 0x%x", result.stopping_reasons);
printf(", took %s seconds, %s\n",
mdbx_ratio2digits(result.spent_time_dot16, 65536, 3, ratio_buffer, sizeof(ratio_buffer)),
stop_reason(&result));
}
}
rc = MDBX_SUCCESS;
}
if (txn) {
if (rc != MDBX_SUCCESS)
mdbx_txn_abort(txn);
else {
act = "final commit";
rc = mdbx_txn_commit(txn);
}
txn = nullptr;
}
if (!quiet && rc != MDBX_SUCCESS) {
fflush(nullptr);
fprintf(stderr, "%s: %s failed, error %d (%s)\n", progname, act, rc, mdbx_strerror(rc));
}
mdbx_env_close(env);
fflush(nullptr);
return (rc != MDBX_SUCCESS) ? EXIT_FAILURE : EXIT_SUCCESS;
}