2026-09-25 14:58:51 -04:00
|
|
|
//! Peak and settled memory of a full indexing run.
|
2026-08-02 20:21:19 -04:00
|
|
|
//!
|
2026-08-23 00:33:53 -04:00
|
|
|
//! [`indexprobe`](indexprobe.rs) answers "how fast"; this answers "how
|
|
|
|
|
//! much RAM", driving the same [`IndexingService`] the GUI drives with no
|
|
|
|
|
//! window or GL context in the total. For the idle footprint of a running
|
|
|
|
|
//! GUI, use [`rssprobe`](rssprobe.rs).
|
2026-08-02 20:21:19 -04:00
|
|
|
//!
|
|
|
|
|
//! ```text
|
|
|
|
|
//! cargo build -p quicksearch-core --example memprobe --release
|
|
|
|
|
//! ./target/release/examples/memprobe cold /media/shared /var/tmp/qs-mem/index.db
|
|
|
|
|
//! ./target/release/examples/memprobe warm /media/shared /var/tmp/qs-mem/index.db
|
2026-08-04 17:07:46 -04:00
|
|
|
//! ./target/release/examples/memprobe cold ~ /var/tmp/qs-mem/index.db 250 probe.toml
|
2026-08-02 20:21:19 -04:00
|
|
|
//! ```
|
|
|
|
|
//!
|
2026-09-25 14:58:51 -04:00
|
|
|
//! Roots are comma-separated: per-root state multiplies, and a one-root run
|
|
|
|
|
//! cannot show the buffers that exist once per pipeline.
|
2026-08-23 20:01:16 -04:00
|
|
|
//!
|
|
|
|
|
//! ```text
|
|
|
|
|
//! ./target/release/examples/memprobe cold /media/shared,/home/me,/usr /var/tmp/qs-mem/index.db
|
|
|
|
|
//! ```
|
|
|
|
|
//!
|
2026-09-25 14:58:51 -04:00
|
|
|
//! Trailing arguments: the sampling interval in ms (default 100) and a
|
|
|
|
|
//! config file. `cold` deletes the database first; `warm` re-runs against
|
|
|
|
|
//! the finished one. Reading the report: quote `VmHWM`, which cannot miss a
|
|
|
|
|
//! spike; the sampled peak only says when. `growth per file` is a ratio, not
|
2026-08-23 00:33:53 -04:00
|
|
|
//! a per-file cost. Nothing reported is evictable page cache. `settled RSS`
|
2026-10-11 20:32:00 -04:00
|
|
|
//! is what a long-lived process keeps: what the allocators still hold after
|
|
|
|
|
//! the run's closing `release_free_heap`.
|
2026-08-23 20:01:16 -04:00
|
|
|
//!
|
|
|
|
|
//! Built with `--features probe` the indexer also prints a `census` line
|
|
|
|
|
//! naming what its run-scoped structures hold, which is the half `smaps`
|
|
|
|
|
//! cannot answer: a mapping is "heap", never "the stale-candidate list".
|
2026-08-02 20:21:19 -04:00
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
// Settled RSS is a property of the allocator, so this must be the one the
|
|
|
|
|
// shipped binaries install.
|
2026-08-25 01:04:36 -04:00
|
|
|
#[global_allocator]
|
|
|
|
|
static GLOBAL: quicksearch_core::platform::Allocator = quicksearch_core::platform::Allocator;
|
|
|
|
|
|
2026-08-02 20:21:19 -04:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
|
|
|
|
use quicksearch_core::config::Config;
|
|
|
|
|
use quicksearch_core::indexing::{IndexingService, IndexingStatus, RootPhase};
|
2026-09-25 14:58:51 -04:00
|
|
|
use quicksearch_core::testutil::mib;
|
2026-08-02 20:21:19 -04:00
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
mod common;
|
|
|
|
|
|
2026-08-02 20:21:19 -04:00
|
|
|
const DEFAULT_SAMPLE_MS: u64 = 100;
|
|
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
/// Rarer than sampling: each check opens a connection to the live database.
|
2026-08-02 20:21:19 -04:00
|
|
|
const MARKER_INTERVAL_MS: u64 = 500;
|
|
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
/// Past this is a hang; a peak for a run that never finished would be worse
|
|
|
|
|
/// than failing.
|
2026-08-02 20:21:19 -04:00
|
|
|
const TIMEOUT: Duration = Duration::from_secs(3 * 3600);
|
|
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
/// Generous: the wait is for `VACUUM` on a multi-gigabyte index, not for
|
|
|
|
|
/// anything that scales with the sampling interval.
|
2026-08-05 19:17:11 -04:00
|
|
|
const SETTLE_TIMEOUT: Duration = Duration::from_secs(600);
|
|
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
/// The writer releases its connection and returns pages *after* setting
|
|
|
|
|
/// `Idle`; sampling the instant it flips would miss the thing measured.
|
2026-08-05 19:17:11 -04:00
|
|
|
const SETTLE_QUIET: Duration = Duration::from_secs(2);
|
|
|
|
|
|
2026-08-02 20:21:19 -04:00
|
|
|
struct Sample {
|
|
|
|
|
at: Duration,
|
|
|
|
|
rss: u64,
|
|
|
|
|
walked: usize,
|
|
|
|
|
extracted: usize,
|
|
|
|
|
phase: &'static str,
|
2026-08-23 00:33:53 -04:00
|
|
|
/// What extraction was working on: a single-file peak is a different
|
|
|
|
|
/// problem from one growing with the tree.
|
2026-08-02 20:21:19 -04:00
|
|
|
file: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
let mut args = std::env::args().skip(1);
|
|
|
|
|
let mode = args.next().unwrap_or_default();
|
2026-08-23 20:01:16 -04:00
|
|
|
let (Some(roots), Some(db)) = (args.next(), args.next()) else {
|
|
|
|
|
eprintln!("usage: memprobe <cold|warm> <root[,root...]> <db> [sample_ms] [config.toml]");
|
2026-08-02 20:21:19 -04:00
|
|
|
std::process::exit(2);
|
|
|
|
|
};
|
|
|
|
|
if mode != "cold" && mode != "warm" {
|
2026-08-23 20:01:16 -04:00
|
|
|
eprintln!("usage: memprobe <cold|warm> <root[,root...]> <db> [sample_ms] [config.toml]");
|
|
|
|
|
std::process::exit(2);
|
|
|
|
|
}
|
|
|
|
|
let roots: Vec<String> = roots
|
|
|
|
|
.split(',')
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|r| !r.is_empty())
|
|
|
|
|
.map(str::to_string)
|
|
|
|
|
.collect();
|
|
|
|
|
if roots.is_empty() {
|
|
|
|
|
eprintln!("memprobe: no roots given");
|
2026-08-02 20:21:19 -04:00
|
|
|
std::process::exit(2);
|
|
|
|
|
}
|
|
|
|
|
let interval = Duration::from_millis(
|
|
|
|
|
args.next()
|
|
|
|
|
.map(|s| s.parse().expect("sample_ms must be a number"))
|
|
|
|
|
.unwrap_or(DEFAULT_SAMPLE_MS)
|
|
|
|
|
.max(1),
|
|
|
|
|
);
|
2026-08-04 17:07:46 -04:00
|
|
|
let config_path = args.next().map(PathBuf::from);
|
2026-08-02 20:21:19 -04:00
|
|
|
let db = PathBuf::from(db);
|
|
|
|
|
|
|
|
|
|
if let Some(parent) = db.parent() {
|
|
|
|
|
std::fs::create_dir_all(parent).expect("create database directory");
|
|
|
|
|
}
|
|
|
|
|
if mode == "cold" {
|
|
|
|
|
for suffix in ["", "-wal", "-shm"] {
|
|
|
|
|
let _ = std::fs::remove_file(format!("{}{}", db.display(), suffix));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 20:01:16 -04:00
|
|
|
run(&mode, &roots, &db, interval, config_path.as_deref());
|
2026-08-02 20:21:19 -04:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 20:01:16 -04:00
|
|
|
fn run(mode: &str, roots: &[String], db: &Path, interval: Duration, config_path: Option<&Path>) {
|
2026-08-23 00:33:53 -04:00
|
|
|
// A config file only supplies the knobs that change *what* indexing
|
|
|
|
|
// does; the defaults keep runs comparable.
|
2026-08-04 17:07:46 -04:00
|
|
|
let config = match config_path {
|
|
|
|
|
Some(p) => Config::load_from(p).expect("load probe config"),
|
|
|
|
|
None => Config::default(),
|
|
|
|
|
};
|
2026-08-02 20:21:19 -04:00
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
// A stale marker from the previous run would end this one immediately.
|
2026-08-02 20:21:19 -04:00
|
|
|
if db.exists() {
|
|
|
|
|
let conn = rusqlite::Connection::open(db).expect("open db");
|
|
|
|
|
conn.execute("DELETE FROM schema_info WHERE key = 'last_full_index'", [])
|
|
|
|
|
.expect("clear marker");
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
let baseline = common::rss().expect("read /proc/self/status");
|
2026-08-02 20:21:19 -04:00
|
|
|
eprintln!(
|
2026-08-23 20:01:16 -04:00
|
|
|
"memprobe {}: {} root(s)={} db={}\n baseline RSS {} (process before indexing starts)",
|
2026-08-02 20:21:19 -04:00
|
|
|
mode,
|
2026-08-23 20:01:16 -04:00
|
|
|
roots.len(),
|
|
|
|
|
roots.join(" "),
|
2026-08-02 20:21:19 -04:00
|
|
|
db.display(),
|
|
|
|
|
mib(baseline)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let service = IndexingService::new();
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
service
|
2026-08-23 20:01:16 -04:00
|
|
|
.start_indexing(roots.to_vec(), db.to_string_lossy().into_owned(), config)
|
2026-08-02 20:21:19 -04:00
|
|
|
.expect("start indexing");
|
|
|
|
|
|
|
|
|
|
let mut samples: Vec<Sample> = Vec::new();
|
|
|
|
|
let deadline = start + TIMEOUT;
|
|
|
|
|
let marker_every = (MARKER_INTERVAL_MS / interval.as_millis().max(1) as u64).max(1) as u32;
|
|
|
|
|
let mut ticks: u32 = 0;
|
|
|
|
|
let mut done = false;
|
|
|
|
|
let mut high = 0u64;
|
2026-09-25 14:58:51 -04:00
|
|
|
let mut at_peak = Vec::new();
|
2026-08-02 20:21:19 -04:00
|
|
|
|
|
|
|
|
while Instant::now() < deadline {
|
|
|
|
|
std::thread::sleep(interval);
|
|
|
|
|
ticks += 1;
|
|
|
|
|
|
|
|
|
|
let status = service.get_status();
|
|
|
|
|
if let IndexingStatus::Error(e) = &status {
|
|
|
|
|
panic!("indexing failed: {}", e);
|
|
|
|
|
}
|
|
|
|
|
let (walked, extracted, phase, file) = progress(&status);
|
2026-09-25 14:58:51 -04:00
|
|
|
let now = common::rss().unwrap_or(0);
|
|
|
|
|
// Only on a new high: `smaps` costs far more than `status`.
|
2026-08-02 20:21:19 -04:00
|
|
|
if now > high {
|
|
|
|
|
high = now;
|
|
|
|
|
at_peak = breakdown();
|
|
|
|
|
}
|
|
|
|
|
samples.push(Sample {
|
|
|
|
|
at: start.elapsed(),
|
|
|
|
|
rss: now,
|
|
|
|
|
walked,
|
|
|
|
|
extracted,
|
|
|
|
|
phase,
|
|
|
|
|
file,
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
if ticks.is_multiple_of(marker_every) && db.exists() {
|
2026-08-02 20:21:19 -04:00
|
|
|
if let Ok(conn) = rusqlite::Connection::open(db) {
|
2026-10-11 20:32:00 -04:00
|
|
|
if matches!(
|
|
|
|
|
quicksearch_core::db::repo::get_last_full_index(&conn),
|
|
|
|
|
Ok(Some(_))
|
|
|
|
|
) {
|
2026-08-02 20:21:19 -04:00
|
|
|
done = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let elapsed = start.elapsed();
|
|
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
let hwm = common::vm_hwm();
|
2026-08-02 20:21:19 -04:00
|
|
|
assert!(done, "indexing did not finish within {:?}", TIMEOUT);
|
2026-08-05 19:17:11 -04:00
|
|
|
|
|
|
|
|
let settled = settle(&service);
|
2026-08-02 20:21:19 -04:00
|
|
|
service.stop_indexing().expect("stop");
|
|
|
|
|
|
2026-08-04 03:27:05 -04:00
|
|
|
report(
|
2026-08-05 19:17:11 -04:00
|
|
|
mode, elapsed, baseline, hwm, settled, &samples, db, interval, &at_peak,
|
2026-08-04 03:27:05 -04:00
|
|
|
);
|
2026-08-02 20:21:19 -04:00
|
|
|
}
|
|
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
/// RSS once the service is idle: what indexing keeps, where the peak says
|
|
|
|
|
/// what it needs. The final release lands after `Idle` is published, hence
|
|
|
|
|
/// the wait and then [`SETTLE_QUIET`].
|
2026-08-05 19:17:11 -04:00
|
|
|
fn settle(service: &IndexingService) -> u64 {
|
|
|
|
|
let deadline = Instant::now() + SETTLE_TIMEOUT;
|
|
|
|
|
while Instant::now() < deadline {
|
|
|
|
|
if matches!(service.get_status(), IndexingStatus::Idle) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(Duration::from_millis(100));
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(SETTLE_QUIET);
|
2026-09-25 14:58:51 -04:00
|
|
|
common::rss().unwrap_or(0)
|
2026-08-05 19:17:11 -04:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
/// Flatten per-root progress into one line: the process has one address
|
|
|
|
|
/// space, so a per-root split would not explain a shared peak.
|
2026-08-02 20:21:19 -04:00
|
|
|
fn progress(status: &IndexingStatus) -> (usize, usize, &'static str, String) {
|
|
|
|
|
let IndexingStatus::Running { roots, .. } = status else {
|
|
|
|
|
return (0, 0, "-", String::new());
|
|
|
|
|
};
|
|
|
|
|
let walked = roots.iter().map(|r| r.walked).sum();
|
|
|
|
|
let extracted = roots.iter().map(|r| r.extracted).sum();
|
|
|
|
|
// The whole run's phase is the least-advanced root's: while any root is
|
|
|
|
|
// still walking, walk-sized allocations are still live.
|
|
|
|
|
let phase = if roots.iter().any(|r| r.phase == RootPhase::Walking) {
|
|
|
|
|
"walk"
|
|
|
|
|
} else if roots.iter().any(|r| r.phase == RootPhase::Extracting) {
|
|
|
|
|
"extract"
|
|
|
|
|
} else {
|
|
|
|
|
"done"
|
|
|
|
|
};
|
|
|
|
|
let file = roots
|
|
|
|
|
.iter()
|
|
|
|
|
.find_map(|r| r.current_file.clone())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
(walked, extracted, phase, file)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
2026-08-02 20:21:19 -04:00
|
|
|
fn report(
|
|
|
|
|
mode: &str,
|
|
|
|
|
elapsed: Duration,
|
|
|
|
|
baseline: u64,
|
|
|
|
|
hwm: Option<u64>,
|
2026-08-05 19:17:11 -04:00
|
|
|
settled: u64,
|
2026-08-02 20:21:19 -04:00
|
|
|
samples: &[Sample],
|
|
|
|
|
db: &Path,
|
|
|
|
|
interval: Duration,
|
2026-09-25 14:58:51 -04:00
|
|
|
at_peak: &[(String, u64)],
|
2026-08-02 20:21:19 -04:00
|
|
|
) {
|
|
|
|
|
// One line per 5% of the run, so the shape is visible at any duration.
|
|
|
|
|
let step = (samples.len() / 20).max(1);
|
2026-08-04 03:27:05 -04:00
|
|
|
eprintln!(
|
2026-08-05 18:05:04 -04:00
|
|
|
"\n {:>8} {:>10} {:>9} {:>10} phase",
|
|
|
|
|
"t", "RSS", "walked", "extracted"
|
2026-08-04 03:27:05 -04:00
|
|
|
);
|
2026-08-02 20:21:19 -04:00
|
|
|
for s in samples.iter().step_by(step) {
|
|
|
|
|
eprintln!(
|
|
|
|
|
" {:>7.1}s {:>10} {:>9} {:>10} {}",
|
|
|
|
|
s.at.as_secs_f64(),
|
|
|
|
|
mib(s.rss),
|
|
|
|
|
s.walked,
|
|
|
|
|
s.extracted,
|
|
|
|
|
s.phase
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let peak = samples.iter().max_by_key(|s| s.rss);
|
2026-08-23 00:33:53 -04:00
|
|
|
// The maximum, not the last: progress reads zero again at Idle.
|
2026-08-02 20:21:19 -04:00
|
|
|
let files = samples.iter().map(|s| s.walked).max().unwrap_or(0);
|
|
|
|
|
let db_bytes = db_size(db);
|
|
|
|
|
|
2026-08-04 03:27:05 -04:00
|
|
|
eprintln!(
|
|
|
|
|
"\n{} run: {:.1}s, {} files walked",
|
|
|
|
|
mode,
|
|
|
|
|
elapsed.as_secs_f64(),
|
|
|
|
|
files
|
|
|
|
|
);
|
2026-08-02 20:21:19 -04:00
|
|
|
match hwm {
|
|
|
|
|
Some(h) => eprintln!(" peak RSS (VmHWM) {}", mib(h)),
|
|
|
|
|
None => eprintln!(" peak RSS (VmHWM) unavailable"),
|
|
|
|
|
}
|
|
|
|
|
if let Some(p) = peak {
|
|
|
|
|
eprintln!(
|
|
|
|
|
" sampled peak {} at t={:.1}s during {} ({} walked, {} extracted){}",
|
|
|
|
|
mib(p.rss),
|
|
|
|
|
p.at.as_secs_f64(),
|
|
|
|
|
p.phase,
|
|
|
|
|
p.walked,
|
|
|
|
|
p.extracted,
|
|
|
|
|
if p.file.is_empty() {
|
|
|
|
|
String::new()
|
|
|
|
|
} else {
|
|
|
|
|
format!("\n on {}", p.file)
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
eprintln!(" baseline RSS {}", mib(baseline));
|
2026-08-05 19:17:11 -04:00
|
|
|
eprintln!(
|
|
|
|
|
" settled RSS {} (once idle: what the run kept, not what it needed)",
|
|
|
|
|
mib(settled)
|
|
|
|
|
);
|
|
|
|
|
if let Some(h) = hwm {
|
|
|
|
|
eprintln!(
|
|
|
|
|
" returned {} of the {} the run took above baseline",
|
|
|
|
|
mib(h.saturating_sub(settled)),
|
|
|
|
|
mib(h.saturating_sub(baseline))
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-08-02 20:21:19 -04:00
|
|
|
if let Some(h) = hwm {
|
|
|
|
|
if files > 0 {
|
|
|
|
|
eprintln!(
|
|
|
|
|
" growth per file {:.0} bytes ((peak - baseline) / files walked)",
|
|
|
|
|
h.saturating_sub(baseline) as f64 / files as f64
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 20:32:00 -04:00
|
|
|
eprintln!(" database on disk {}", mib(db_bytes));
|
2026-08-02 20:21:19 -04:00
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
if !at_peak.is_empty() {
|
2026-08-02 20:21:19 -04:00
|
|
|
eprintln!("\n resident bytes at the peak, by mapping:");
|
2026-09-25 14:58:51 -04:00
|
|
|
for (name, bytes) in at_peak.iter().take(8) {
|
2026-08-02 20:21:19 -04:00
|
|
|
eprintln!(" {:>10} {}", mib(*bytes), name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
// A peak figure hides a transient spike; ranking sample-to-sample rises
|
|
|
|
|
// names the files that cause it.
|
2026-08-02 20:21:19 -04:00
|
|
|
let mut jumps: Vec<(u64, &Sample)> = samples
|
|
|
|
|
.windows(2)
|
|
|
|
|
.map(|w| (w[1].rss.saturating_sub(w[0].rss), &w[1]))
|
|
|
|
|
.filter(|(delta, _)| *delta > 4 * 1024 * 1024)
|
|
|
|
|
.collect();
|
|
|
|
|
jumps.sort_by_key(|(delta, _)| std::cmp::Reverse(*delta));
|
|
|
|
|
if !jumps.is_empty() {
|
2026-08-04 03:27:05 -04:00
|
|
|
eprintln!(
|
|
|
|
|
"\n largest RSS rises between samples ({:?} apart):",
|
|
|
|
|
interval
|
|
|
|
|
);
|
2026-08-02 20:21:19 -04:00
|
|
|
for (delta, s) in jumps.iter().take(8) {
|
|
|
|
|
eprintln!(
|
|
|
|
|
" +{:>9} to {:>10} at t={:>6.1}s {} {}",
|
|
|
|
|
mib(*delta),
|
|
|
|
|
mib(s.rss),
|
|
|
|
|
s.at.as_secs_f64(),
|
|
|
|
|
s.phase,
|
|
|
|
|
s.file
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
/// Resident bytes per mapping from `/proc/self/smaps`, summed by name: heap,
|
|
|
|
|
/// droppable file-backed cache and thread stacks each want different fixes,
|
|
|
|
|
/// and only `smaps` shows which.
|
|
|
|
|
fn breakdown() -> Vec<(String, u64)> {
|
2026-08-02 20:21:19 -04:00
|
|
|
let Ok(smaps) = std::fs::read_to_string("/proc/self/smaps") else {
|
2026-09-25 14:58:51 -04:00
|
|
|
return Vec::new();
|
2026-08-02 20:21:19 -04:00
|
|
|
};
|
2026-09-25 14:58:51 -04:00
|
|
|
common::resident_by_name(common::smaps_regions(&smaps).map(|r| (r.name, r.rss)))
|
2026-08-02 20:21:19 -04:00
|
|
|
}
|
|
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
/// The index plus its WAL; leaving the WAL out understates a run in progress.
|
2026-08-02 20:21:19 -04:00
|
|
|
fn db_size(db: &Path) -> u64 {
|
|
|
|
|
["", "-wal"]
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|s| std::fs::metadata(format!("{}{}", db.display(), s)).ok())
|
|
|
|
|
.map(|m| m.len())
|
|
|
|
|
.sum()
|
|
|
|
|
}
|