use segment_buffer::{DurabilityPolicy, FlushPolicy, SegmentBuffer, SegmentConfig};
use serde::{Deserialize, Serialize};
use std::time::Instant;
#[derive(Serialize, Deserialize, Clone)]
struct Event {
id: u64,
timestamp_ms: u64,
kind: u8,
payload: String,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum PayloadKind {
Uniform,
Text,
Json,
Random,
}
impl PayloadKind {
fn parse(s: &str) -> Result<Self, String> {
match s.to_ascii_lowercase().as_str() {
"uniform" | "u" => Ok(Self::Uniform),
"text" | "t" => Ok(Self::Text),
"json" | "j" => Ok(Self::Json),
"random" | "r" => Ok(Self::Random),
other => Err(format!(
"unknown payload_kind '{other}' (expected: uniform|text|json|random)"
)),
}
}
}
impl std::fmt::Display for PayloadKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Uniform => write!(f, "uniform"),
Self::Text => write!(f, "text"),
Self::Json => write!(f, "json"),
Self::Random => write!(f, "random"),
}
}
}
struct SplitMix64(u64);
impl SplitMix64 {
fn new(seed: u64) -> Self {
Self(seed.wrapping_add(0x9E3779B9_7F4A7C15))
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E3779B9_7F4A7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF5847_6D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049B_B133111EB);
z ^ (z >> 31)
}
fn pick<'a, T>(&mut self, slice: &'a [T]) -> &'a T {
let i = (self.next_u64() % slice.len() as u64) as usize;
&slice[i]
}
}
const WORDS: &[&str] = &[
"event",
"user",
"action",
"status",
"request",
"response",
"error",
"warn",
"info",
"debug",
"trace",
"worker",
"handler",
"service",
"module",
"session",
"token",
"cache",
"queue",
"batch",
"timeout",
"retry",
"connect",
"close",
"auth",
"login",
"logout",
"create",
"update",
"delete",
"read",
"write",
"flush",
"sync",
"commit",
"abort",
"start",
"stop",
"pause",
"resume",
"init",
"shutdown",
"health",
"metric",
"counter",
"gauge",
"span",
"ctx",
"node",
"shard",
"partition",
"offset",
"latency",
"throughput",
"memory",
"cpu",
"disk",
"network",
"db",
"redis",
"kafka",
"grpc",
"http",
];
const LEVELS: &[&str] = &["INFO", "WARN", "ERROR", "DEBUG"];
fn make_payload(id: u64, kind: PayloadKind, target_len: usize) -> String {
if target_len == 0 {
return String::new();
}
match kind {
PayloadKind::Uniform => "x".repeat(target_len),
PayloadKind::Text => {
let mut rng = SplitMix64::new(id);
let mut s = String::with_capacity(target_len + 64);
while s.len() < target_len {
let ts = 1_700_000_000 + id;
let level = rng.pick(LEVELS);
let w1 = rng.pick(WORDS);
let w2 = rng.pick(WORDS);
let worker = rng.next_u64() % 64;
let n = rng.next_u64();
s.push_str(&format!("{ts} {level} worker={worker} {w1}={w2} n={n} "));
}
s.truncate(target_len);
s
}
PayloadKind::Json => {
let mut rng = SplitMix64::new(id);
let mut s = String::from('[');
while s.len() < target_len {
let word = rng.pick(WORDS);
let level = rng.pick(LEVELS);
let num = rng.next_u64();
let f = (rng.next_u64() as f64) / (u64::MAX as f64) * 1000.0;
if !s.ends_with('[') {
s.push(',');
}
s.push_str(&format!(
r#"{{"id":{id},"lvl":"{level}","k":"{word}","v":{num},"f":{f:.3}}}"#
));
}
s.push(']');
s.truncate(target_len);
s
}
PayloadKind::Random => {
let mut rng = SplitMix64::new(id);
let mut s = String::with_capacity(target_len + 16);
while s.len() < target_len {
let n = rng.next_u64();
s.push_str(&format!("{n:016x}"));
}
s.truncate(target_len);
s
}
}
}
const DURABILITY: DurabilityPolicy = DurabilityPolicy::Throughput;
fn mib(bytes: u64) -> f64 {
bytes as f64 / (1024.0 * 1024.0)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let count: u64 = std::env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(1_000_000);
let batch: usize = std::env::args()
.nth(2)
.and_then(|s| s.parse().ok())
.unwrap_or(5_000);
let compression: i32 = std::env::args()
.nth(3)
.and_then(|s| s.parse().ok())
.unwrap_or(3);
let payload_mult: usize = std::env::args()
.nth(4)
.and_then(|s| s.parse().ok())
.unwrap_or(1);
let payload_kind = match std::env::args().nth(5).as_deref() {
Some(s) => PayloadKind::parse(s)?,
None => PayloadKind::Uniform,
};
let batch = batch.max(1);
let payload_mult = payload_mult.max(1);
let payload_len = 64 * payload_mult;
let bytes_per_item: u64 = 8 + 8 + 1 + payload_len as u64;
let tmp = tempfile::tempdir()?;
let dir = tmp.path().to_path_buf();
println!("=== segment-buffer scaling test ===");
println!(
"count: {count} | batch: {batch} | compression: zstd-{compression} | durability: {DURABILITY:?}"
);
println!(
"payload: {payload_kind} {payload_len} B/item ({payload_mult}x base-64) | uncompressed: {bytes_per_item} B/item"
);
println!("dir: {}", dir.display());
println!();
let config = SegmentConfig::builder()
.flush_policy(FlushPolicy::Manual) .max_size_bytes(u64::MAX) .compression_level(compression)
.durability(DURABILITY)
.build();
println!("--- phase 1: load (append_all + flush per batch) ---");
let buf = SegmentBuffer::<Event>::open(&dir, config.clone())?;
let wall_start = Instant::now();
let mut load_elapsed = std::time::Duration::ZERO;
let mut id = 0u64;
let heartbeat = (count / 10).max(1);
let mut next_heartbeat = heartbeat;
while id < count {
let take = std::cmp::min(batch as u64, count - id) as usize;
let items: Vec<Event> = (0..take)
.map(|i| {
let eid = id + i as u64;
Event {
id: eid,
timestamp_ms: eid,
kind: (eid % 4) as u8,
payload: make_payload(eid, payload_kind, payload_len),
}
})
.collect();
let t = Instant::now();
let last = buf.append_all(items)?;
buf.flush()?;
load_elapsed += t.elapsed();
id = last + 1;
if id >= next_heartbeat {
eprintln!(
" ... {id}/{count} items flushed ({:.1}s wall, {:.2}s buffer)",
wall_start.elapsed().as_secs_f64(),
load_elapsed.as_secs_f64()
);
next_heartbeat += heartbeat;
}
}
let peak_disk = buf.stats().approx_disk_bytes;
assert_eq!(
buf.latest_sequence(),
count.saturating_sub(1),
"load phase: latest_sequence should be count-1"
);
drop(buf);
let load_secs = load_elapsed.as_secs_f64();
let wall_secs = wall_start.elapsed().as_secs_f64();
let load_ips = count as f64 / load_secs;
println!("items/sec: {load_ips:.0}");
println!(
"MiB/s: {:.1} (uncompressed, est. {bytes_per_item} B/item)",
load_ips * bytes_per_item as f64 / (1024.0 * 1024.0)
);
println!("buffer: {load_secs:.2}s");
println!("wall: {wall_secs:.2}s (includes payload generation)");
println!("peak disk: {:.1} MiB (compressed)", mib(peak_disk));
if peak_disk > 0 {
println!(
"comp ratio: {:.1}x ({:.2} B/item compressed)",
(bytes_per_item * count) as f64 / peak_disk as f64,
peak_disk as f64 / count as f64
);
}
println!();
println!("--- phase 2: recover (drop + reopen) ---");
let t1 = Instant::now();
let (buf, report) = SegmentBuffer::<Event>::open_with_report(&dir, config.clone())?;
let recover_elapsed = t1.elapsed();
let recover_secs = recover_elapsed.as_secs_f64();
let segs = report.segment_count;
println!("segments: {segs}");
println!("disk: {:.1} MiB", mib(report.disk_bytes));
println!("elapsed: {recover_secs:.3}s");
if recover_secs > 0.0 {
println!("seg/s: {:.0}", segs as f64 / recover_secs);
}
println!();
println!("--- phase 3: drain (read_from + delete_acked) ---");
let mut cursor = buf.stats().head_sequence;
let mut seen = 0u64;
let mut expected_id = cursor;
let t2 = Instant::now();
let mut next_heartbeat = heartbeat;
loop {
let batch_items = buf.read_from(cursor, batch)?;
if batch_items.is_empty() {
break;
}
for item in &batch_items {
assert_eq!(
item.id, expected_id,
"drain verify: id {} expected, got {} (gap or out-of-order)",
expected_id, item.id
);
expected_id += 1;
}
let last_seq = cursor + batch_items.len() as u64 - 1;
buf.delete_acked(last_seq)?;
seen += batch_items.len() as u64;
cursor = last_seq + 1;
if seen >= next_heartbeat {
eprintln!(" ... {seen}/{count} items drained");
next_heartbeat += heartbeat;
}
}
let drain_elapsed = t2.elapsed();
let final_disk = buf.stats().approx_disk_bytes;
let drain_secs = drain_elapsed.as_secs_f64();
let drain_ips = seen as f64 / drain_secs;
println!("items/sec: {drain_ips:.0}");
println!(
"MiB/s: {:.1} (uncompressed, est. {bytes_per_item} B/item)",
drain_ips * bytes_per_item as f64 / (1024.0 * 1024.0)
);
println!("elapsed: {drain_secs:.2}s");
println!("final disk: {:.1} MiB (should be ~0)", mib(final_disk));
println!();
println!("--- verify ---");
println!("items seen: {seen}");
println!("expected: {count}");
assert_eq!(
seen, count,
"drain verify: saw {seen} items, expected {count}"
);
assert_eq!(
cursor, count,
"drain verify: cursor {cursor}, expected {count}"
);
assert_eq!(final_disk, 0, "drain verify: disk not fully drained");
println!("OK: gap-free, in-order, exactly {count} items, disk drained");
Ok(())
}