use std::fmt::Write as _;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use criterion::{Criterion, criterion_group, criterion_main};
use processkit::{Command, Stdin};
use tokio::runtime::Runtime;
const SHORT_LINE_COUNT: usize = 300_000;
const LONG_LINE_COUNT: usize = 500;
const LONG_LINE_WIDTH: usize = 2_000;
fn passthrough() -> Command {
if cfg!(windows) {
Command::new("cmd").args(["/c", "findstr", "^^"])
} else {
Command::new("cat")
}
}
fn short_lines_payload(count: usize) -> String {
let mut text = String::with_capacity(count * 12);
for i in 0..count {
let _ = writeln!(text, "line-{i:06}");
}
text
}
fn long_lines_payload(count: usize, width: usize) -> String {
let filler = "x".repeat(width);
let mut text = String::with_capacity(count * (width + 1));
for _ in 0..count {
text.push_str(&filler);
text.push('\n');
}
text
}
fn legacy_payload(count: usize) -> Vec<u8> {
const PHRASE: &str = "café \u{20AC} déjà vu\n";
let mut text = String::with_capacity(PHRASE.len() * count);
for _ in 0..count {
text.push_str(PHRASE);
}
encoding_rs::WINDOWS_1252.encode(&text).0.into_owned()
}
fn sanity_check(rt: &Runtime, command: &Command, expected_lines: usize) {
let result = rt
.block_on(command.clone().output_string())
.expect("sanity run must complete");
assert!(result.is_success(), "sanity run must exit 0: {result:?}");
assert_eq!(
result.stdout().lines().count(),
expected_lines,
"workload must round-trip every line intact"
);
}
fn bench_short_lines(c: &mut Criterion) {
let rt = Runtime::new().expect("build a tokio runtime for the async benches");
let base = passthrough().stdin(Stdin::from_string(short_lines_payload(SHORT_LINE_COUNT)));
sanity_check(&rt, &base, SHORT_LINE_COUNT);
c.bench_function("short_lines", |b| {
b.to_async(&rt).iter(|| {
let command = base.clone();
async move {
let result = command.output_string().await.expect("run passthrough");
assert!(result.is_success());
let _ = std::hint::black_box(result);
}
});
});
}
fn bench_long_lines(c: &mut Criterion) {
let rt = Runtime::new().expect("build a tokio runtime for the async benches");
let base = passthrough().stdin(Stdin::from_string(long_lines_payload(
LONG_LINE_COUNT,
LONG_LINE_WIDTH,
)));
sanity_check(&rt, &base, LONG_LINE_COUNT);
c.bench_function("long_lines", |b| {
b.to_async(&rt).iter(|| {
let command = base.clone();
async move {
let result = command.output_string().await.expect("run passthrough");
assert!(result.is_success());
let _ = std::hint::black_box(result);
}
});
});
}
fn bench_non_utf8_decode(c: &mut Criterion) {
let rt = Runtime::new().expect("build a tokio runtime for the async benches");
let base = passthrough()
.stdin(Stdin::from_bytes(legacy_payload(SHORT_LINE_COUNT)))
.stdout_encoding(encoding_rs::WINDOWS_1252);
sanity_check(&rt, &base, SHORT_LINE_COUNT);
c.bench_function("non_utf8_decode", |b| {
b.to_async(&rt).iter(|| {
let command = base.clone();
async move {
let result = command.output_string().await.expect("run passthrough");
assert!(result.is_success());
let _ = std::hint::black_box(result);
}
});
});
}
fn bench_tee_handler(c: &mut Criterion) {
let rt = Runtime::new().expect("build a tokio runtime for the async benches");
let handler_calls = Arc::new(AtomicUsize::new(0));
let counted = handler_calls.clone();
let base = passthrough()
.stdin(Stdin::from_string(short_lines_payload(SHORT_LINE_COUNT)))
.on_stdout_line(move |_line: &str| {
counted.fetch_add(1, Ordering::Relaxed);
})
.stdout_tee(tokio::io::sink());
sanity_check(&rt, &base, SHORT_LINE_COUNT);
c.bench_function("tee_handler", |b| {
b.to_async(&rt).iter(|| {
let command = base.clone();
async move {
let result = command.output_string().await.expect("run passthrough");
assert!(result.is_success());
let _ = std::hint::black_box(result);
}
});
});
}
fn configure() -> Criterion {
Criterion::default()
.sample_size(20)
.measurement_time(Duration::from_secs(8))
}
criterion_group! {
name = pump_benches;
config = configure();
targets = bench_short_lines, bench_long_lines, bench_non_utf8_decode, bench_tee_handler
}
criterion_main!(pump_benches);