#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::string_slice,
clippy::panic_in_result_fn,
clippy::as_conversions,
clippy::arithmetic_side_effects,
clippy::pedantic,
clippy::nursery
)]
use segment_buffer::{FlushPolicy, SegmentBuffer, SegmentConfig, SegmentSizeStats};
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::path::Path;
const TARGET_MIN_BYTES: u64 = 4 * 1024;
const TARGET_MAX_BYTES: u64 = 256 * 1024;
const WORKLOAD: usize = 5_000;
#[derive(Serialize, Deserialize, Clone, Debug)]
struct Event {
timestamp: u64,
level: u8,
source: String,
message: String,
}
fn make_event(i: usize) -> Event {
let services = ["api", "db", "cache", "queue"];
Event {
timestamp: 1_700_000_000 + i as u64,
level: (i % 5) as u8,
source: format!("host-{}/{}", i % 32, services[i % 4]),
message: format!("processed request #{i:05} with payload data"),
}
}
fn measure(dir: &Path, batch_size: usize) -> Result<SegmentSizeStats, Box<dyn Error>> {
let config = SegmentConfig::builder()
.flush_policy(FlushPolicy::Batch(batch_size))
.compression_level(3)
.build();
let buf = SegmentBuffer::<Event>::open(dir, config)?;
for i in 0..WORKLOAD {
buf.append(make_event(i))?;
}
buf.flush()?;
Ok(buf.segment_size_stats()?)
}
fn in_target_window(stats: &SegmentSizeStats) -> bool {
stats.count > 0 && stats.p50_bytes >= TARGET_MIN_BYTES && stats.max_bytes <= TARGET_MAX_BYTES
}
fn assess(stats: &SegmentSizeStats) -> &'static str {
if stats.count == 0 {
"empty"
} else if stats.p50_bytes < TARGET_MIN_BYTES {
"too small"
} else if stats.max_bytes > TARGET_MAX_BYTES {
"too large"
} else {
"well-tuned"
}
}
fn print_row(label: &str, batch: usize, stats: &SegmentSizeStats) {
println!(
" {label:<10} Batch({batch:<5}) {:>4} segs \
p50 {:>6} B p90 {:>6} B max {:>6} B -> {}",
stats.count,
stats.p50_bytes,
stats.p90_bytes,
stats.max_bytes,
assess(stats),
);
}
fn main() -> Result<(), Box<dyn Error>> {
println!(
"Workload: {WORKLOAD} events | target segment size: \
{TARGET_MIN_BYTES}-{TARGET_MAX_BYTES} B\n"
);
let dir = tempfile::tempdir()?;
let stats = measure(dir.path(), 8)?;
println!("Phase 1 - initial guess (tiny batch):");
print_row("initial", 8, &stats);
println!("\nPhase 2 - sweep candidate batch sizes:");
let candidates = [32, 64, 128, 256, 512, 1024];
let mut winner: Option<(usize, SegmentSizeStats)> = None;
for &batch in &candidates {
let dir = tempfile::tempdir()?;
let stats = measure(dir.path(), batch)?;
print_row("candidate", batch, &stats);
if in_target_window(&stats) && winner.is_none() {
winner = Some((batch, stats));
}
}
println!("\nPhase 3 - recommendation:");
match winner {
Some((batch, stats)) => {
println!(" FlushPolicy::Batch({batch})");
println!(
" {} segments, p50 {} B, max {} B - inside target window.",
stats.count, stats.p50_bytes, stats.max_bytes,
);
}
None => {
println!(
" No candidate landed p50 inside [{TARGET_MIN_BYTES}, \
{TARGET_MAX_BYTES}] B."
);
println!(
" Widen the target window or try batch sizes outside the \
swept range."
);
}
}
Ok(())
}