- Duplicates tab: groups are confirmed by each file's last 8 KiB and keyed by head+tail; unchecked files are never shown. False-positive duplicates drop from 3.4% to 0.1%, and indexing is unchanged. - Encrypted search cache cap raised to 192 MiB at 150 B per file: a keystroke at 1M files takes 59 ms, down from 99 ms. - Case-insensitive filename matching uses a memchr2 scan (1.18x). - Benches run again on the compact index, seed with the indexer's 2 GiB WAL cap (peak scratch 6.2 GB -> 2.45 GB), and every recorded figure is re-measured. - A fresh index no longer logs a rebuild warning. - CI: the sandbox memory test's allocation survives release builds, and --toggle retries an interrupted read.
142 lines
5.2 KiB
Rust
142 lines
5.2 KiB
Rust
//! The search connection has to *open* at the ceiling it was sized for.
|
|
//!
|
|
//! `db::schema`'s unit tests cover the arithmetic; this covers the wiring,
|
|
//! which is where it can silently do nothing: the ceiling is resolved inside
|
|
//! `open_search_reader` from the per-root counts a completed run stores and a
|
|
//! process-global, and a break anywhere along that path leaves a connection
|
|
//! quietly running on the read-only profile's 4 MiB with every test still
|
|
//! green. That a completed run stores counts equal to its rows is asserted in
|
|
//! `tests/full_index.rs`.
|
|
//!
|
|
//! Its own integration binary, with one `#[test]`, because it sets the
|
|
//! process-global key and cache override; see `tests/encrypted.rs`.
|
|
|
|
use quicksearch_core::db;
|
|
use quicksearch_core::db::repo::RootCounts;
|
|
use quicksearch_core::db::schema::{
|
|
recommended_search_cache_mib, SEARCH_CACHE_MAX_MIB, SEARCH_CACHE_PLAIN_MIB,
|
|
};
|
|
use quicksearch_core::testutil::{measurement_key, seed_index, Scratch, SeedSpec};
|
|
|
|
/// Enough files that the recommendation clears the floor and is derived rather
|
|
/// than clamped: at 150 B/file, 16 MiB is reached around 112k. `seed_index`
|
|
/// stores that count, as a run over the index would. Seeded without content:
|
|
/// the FTS write is the slow part and this measures nothing about it.
|
|
const FILES: usize = 150_000;
|
|
|
|
fn spec() -> SeedSpec {
|
|
SeedSpec {
|
|
files: FILES,
|
|
// No document bodies at all; `content_every` past `files` never fires.
|
|
content_every: FILES + 1,
|
|
commit_every: 25_000,
|
|
dup_every: 2,
|
|
dir_depth: 6,
|
|
..SeedSpec::default()
|
|
}
|
|
}
|
|
|
|
/// Store `files` as the seed root's count, as a completed run does.
|
|
fn set_files(db_path: &str, files: usize) {
|
|
let conn = db::open_existing(db_path, true).unwrap();
|
|
let counts = RootCounts {
|
|
files: files as i64,
|
|
fts: 0,
|
|
};
|
|
db::repo::set_root_counts(&conn, "/seed", counts).unwrap();
|
|
}
|
|
|
|
/// Forget every root's counts, as for an index no run has completed.
|
|
fn clear_counts(db_path: &str) {
|
|
let conn = db::open_existing(db_path, true).unwrap();
|
|
db::repo::prune_root_stats(&conn, &[]).unwrap();
|
|
}
|
|
|
|
/// The ceiling a fresh search connection on `db_path` opens at.
|
|
fn opened_mib(db_path: &str) -> i64 {
|
|
cache_mib(&db::open::open_search_reader(db_path).unwrap())
|
|
}
|
|
|
|
/// `PRAGMA cache_size` reads back as the negative KiB it was set to.
|
|
fn cache_mib(conn: &rusqlite::Connection) -> i64 {
|
|
let kib: i64 = conn
|
|
.query_row("PRAGMA cache_size", [], |r| r.get(0))
|
|
.unwrap();
|
|
assert!(
|
|
kib < 0,
|
|
"cache_size came back as {} — a positive value is a *page* count, \
|
|
which would mean the KiB form never reached SQLite",
|
|
kib
|
|
);
|
|
-kib / 1024
|
|
}
|
|
|
|
#[test]
|
|
fn the_search_connection_opens_at_the_ceiling_it_was_sized_for() {
|
|
let (_dir, path) = Scratch::db("searchcache");
|
|
let db_path = path.to_string_lossy().into_owned();
|
|
|
|
// --- keyed, automatic -------------------------------------------------
|
|
db::set_process_key(Some(measurement_key()));
|
|
db::set_search_cache_override(None);
|
|
seed_index(&path, &spec());
|
|
let floor = recommended_search_cache_mib(0, true);
|
|
let expected = recommended_search_cache_mib(FILES as i64, true);
|
|
assert!(
|
|
expected > floor,
|
|
"the corpus must be big enough to clear the floor, or this proves nothing"
|
|
);
|
|
// `seed_index` stored the counts a completed run would.
|
|
assert_eq!(
|
|
opened_mib(&db_path),
|
|
expected,
|
|
"the derived ceiling did not reach the connection"
|
|
);
|
|
|
|
// Without counts there is nothing to derive from, so the floor is correct
|
|
// and is what must come out.
|
|
clear_counts(&db_path);
|
|
assert_eq!(
|
|
opened_mib(&db_path),
|
|
floor,
|
|
"an index with no completed run must fall back, not guess"
|
|
);
|
|
|
|
// A small index stays on the floor.
|
|
set_files(&db_path, 1_000);
|
|
assert_eq!(opened_mib(&db_path), floor, "a small index needs no more");
|
|
|
|
// The next run's count is the next connection's ceiling, however little
|
|
// the index grew.
|
|
let grown = recommended_search_cache_mib((2 * FILES) as i64, true);
|
|
assert!(grown > expected && grown < SEARCH_CACHE_MAX_MIB);
|
|
set_files(&db_path, 2 * FILES);
|
|
assert_eq!(
|
|
opened_mib(&db_path),
|
|
grown,
|
|
"a grown index must grow its ceiling"
|
|
);
|
|
|
|
// --- an explicit override wins, including past the automatic cap ------
|
|
let big = SEARCH_CACHE_MAX_MIB * 2;
|
|
db::set_search_cache_override(Some(big));
|
|
assert_eq!(
|
|
opened_mib(&db_path),
|
|
big,
|
|
"an explicit ceiling must be applied verbatim, above the cap"
|
|
);
|
|
db::set_search_cache_override(None);
|
|
|
|
// --- plain, automatic -------------------------------------------------
|
|
// Same corpus, no key: an unencrypted index has no cliff and is held at a
|
|
// flat ceiling by choice, for memory, however large the index is.
|
|
let (_plain_dir, plain) = Scratch::db("searchcache-plain");
|
|
let plain_path = plain.to_string_lossy().into_owned();
|
|
db::set_process_key(None);
|
|
seed_index(&plain, &spec());
|
|
assert_eq!(
|
|
opened_mib(&plain_path),
|
|
SEARCH_CACHE_PLAIN_MIB,
|
|
"an unencrypted index must not grow its cache with the corpus"
|
|
);
|
|
}
|