Files
qdrant/lib/wal/examples/bench.rs
Arnaud Gourlay c196d2eb1a Benches: use SmallRng instead of ChaCha12-based generators (#9887)
* Benches: use SmallRng instead of ChaCha12-based generators

All benchmarks used StdRng or rand::rng() (ThreadRng), both backed by the
ChaCha12 block cipher in rand 0.10. Benchmarks do not need crypto-strength
randomness, and several draw random values inside the timed closure, so
cipher work was included in the measurement itself.

Switch every bench target to SmallRng (Xoshiro256++), and key the HNSW
graph cache and sparse index cache by RNG algorithm so stale caches built
from the old generator are not reused against newly generated vectors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Benches: replace free-function rand::random with local SmallRng

Addresses review: rand::random draws from the thread RNG (ChaCha12),
including inside the timed loop of the pq score benchmark.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:22:27 +02:00

173 lines
5.5 KiB
Rust

use std::path::Path;
use std::str::FromStr;
use docopt::Docopt;
use hdrhistogram::Histogram;
use rand::Rng;
use rand::rngs::SmallRng;
use regex::Regex;
use serde::Deserialize;
use wal::Segment;
static USAGE: &str = "
Usage:
bench append [--batch=<n>] [--segment-size=<ss>] [--entry-size=<es>] <segment-path>
Options:
--batch=<n> Sync entries to disk in batches of size n, or 0 for async [default: 1].
--segment-size=<ss> Segment size (bytes) [default: 100MiB]
--entry-size=<es> Entry size (bytes) [default: 1KiB]
-h --help Show a help message.
";
#[derive(Debug, Deserialize)]
struct Args {
cmd_append: bool,
arg_segment_path: String,
flag_batch: usize,
flag_entry_size: String,
flag_segment_size: String,
}
fn main() {
let args: Args = Docopt::new(USAGE)
.and_then(|d| d.deserialize())
.unwrap_or_else(|e| e.exit());
if args.cmd_append {
append(&args);
}
}
pub fn precise_time_ns() -> u64 {
chrono::offset::Utc::now()
.timestamp_nanos_opt()
.expect("timestamp after year 2262") as u64
}
fn format_duration(n: u64) -> String {
if n > 1_000_000_000 {
format!("{:.2}s", n as f64 / 1_000_000_000f64)
} else if n > 1_000_000 {
format!("{:.2}ms", n as f64 / 1_000_000f64)
} else if n > 1_000 {
format!("{:.2}μs", n as f64 / 1_000f64)
} else {
format!("{n}ns")
}
}
fn format_bytes(n: usize) -> String {
if n > 1_073_741_824 {
format!("{:.2}GiB", n as f64 / 1_073_741_824f64)
} else if n > 1_048_576 {
format!("{:.2}MiB", n as f64 / 1_048_576f64)
} else if n > 1_024 {
format!("{:.2}KiB", n as f64 / 1_024f64)
} else {
format!("{n}B")
}
}
fn parse_bytes(s: &str) -> usize {
let regex = Regex::new(r"(?i)^(\d+(?:\.\d+)?)\s?(k|m|g)?i?b?$").unwrap();
let caps = regex
.captures(s)
.unwrap_or_else(|| panic!("unable to parse byte amount: {s}"));
let n: usize = FromStr::from_str(caps.get(1).unwrap().as_str()).unwrap();
match caps.get(2).map(|m| m.as_str()) {
None => n,
Some("k") | Some("K") => n * 1_024,
Some("m") | Some("M") => n * 1_048_576,
Some("g") | Some("G") => n * 1_073_741_824,
_ => panic!("unable to parse byte amount: {s}"),
}
}
fn append(args: &Args) {
let path = Path::new(&args.arg_segment_path);
let entry_size = parse_bytes(&args.flag_entry_size);
let segment_size = parse_bytes(&args.flag_segment_size);
println!(
"entry size: {}, segment size: {}, batch size: {}",
format_bytes(entry_size),
format_bytes(segment_size),
args.flag_batch
);
let mut segment = Segment::create(path, segment_size).unwrap();
let mut buf = vec![0; entry_size];
let mut small_rng = rand::make_rng::<SmallRng>();
small_rng.fill_bytes(&mut buf);
let mut append_hist = Histogram::<u64>::new_with_bounds(1, 60 * 60 * 1000, 2).unwrap();
let mut sync_hist = Histogram::<u64>::new_with_bounds(1, 60 * 60 * 1000, 2).unwrap();
let mut entries = 0usize;
let mut time: u64 = precise_time_ns();
let start_time: u64 = time;
while segment.append(&buf).is_some() {
entries += 1;
if args.flag_batch != 0 && entries.is_multiple_of(args.flag_batch) {
let start_sync = precise_time_ns();
//future.await().unwrap();
sync_hist.record(precise_time_ns() - start_sync).unwrap();
}
let new_time = precise_time_ns();
append_hist.record(new_time - time).unwrap();
time = new_time;
small_rng.fill_bytes(&mut buf);
}
if args.flag_batch != 0 && !entries.is_multiple_of(args.flag_batch) {
//segment.flush().await().unwrap();
let new_time = precise_time_ns();
append_hist.record(new_time - time).unwrap();
}
let end_time = precise_time_ns();
if args.flag_batch == 0 {
//segment.flush().await().unwrap();
let flush_time = precise_time_ns() - end_time;
println!("final sync latency: {}", format_duration(flush_time));
}
let time = end_time - start_time;
let data = entries * entry_size;
let rate = (data as f64 / (time as f64 / 1_000_000_000f64)) as usize;
let overhead_amount = segment.len().saturating_sub(data);
let overhead_rate = (overhead_amount as f64 / (time as f64 / 1_000_000_000f64)) as usize;
println!(
"time: {}, data: {} ({}), rate {}/s ({}/s), entries appended: {}",
format_duration(time),
format_bytes(data),
format_bytes(overhead_amount),
format_bytes(rate),
format_bytes(overhead_rate),
entries
);
println!(
"append latency:\t\tp50: {:>7},\tp75: {:>7},\tp90: {:>7},\tp95: {:>7},\tp99: {:>7}",
format_duration(append_hist.value_at_percentile(0.5)),
format_duration(append_hist.value_at_percentile(0.75)),
format_duration(append_hist.value_at_percentile(0.90)),
format_duration(append_hist.value_at_percentile(0.95)),
format_duration(append_hist.value_at_percentile(0.99)),
);
println!(
"sync latency:\t\tp50: {:>7},\tp75: {:>7},\tp90: {:>7},\tp95: {:>7},\tp99: {:>7}",
format_duration(sync_hist.value_at_percentile(0.5)),
format_duration(sync_hist.value_at_percentile(0.75)),
format_duration(sync_hist.value_at_percentile(0.90)),
format_duration(sync_hist.value_at_percentile(0.95)),
format_duration(sync_hist.value_at_percentile(0.99)),
);
}