use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use serde::{Deserialize, Serialize};
use ytsaurus_client::{Client, RetryPolicy};
use ytsaurus_yson::{YsonFormat, to_vec};
const SIZES: [usize; 3] = [1_000, 10_000, 100_000];
#[derive(Serialize, Deserialize, Clone)]
struct Row {
n: i64,
name: String,
payload: String,
ratio: f64,
flag: bool,
}
fn row(n: usize) -> Row {
Row {
n: n as i64,
name: format!("row-{n:08}"),
payload: "0123456789abcdef0123456789abcdef".to_owned(),
ratio: n as f64 / 7.0,
flag: n.is_multiple_of(2),
}
}
fn encoded_by_hand(rows: usize) -> Vec<u8> {
let mut out = Vec::new();
for n in 0..rows {
out.extend_from_slice(&to_vec(&row(n), YsonFormat::Binary).expect("encodes"));
out.push(b';');
}
out
}
struct NullCluster {
address: String,
stop: Arc<AtomicBool>,
}
impl NullCluster {
fn new(reply_body: Vec<u8>) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("binds");
let address = format!("http://{}", listener.local_addr().expect("has an address"));
let stop = Arc::new(AtomicBool::new(false));
let stopping = Arc::clone(&stop);
std::thread::spawn(move || {
for connection in listener.incoming() {
if stopping.load(Ordering::Relaxed) {
return;
}
let Ok(stream) = connection else { return };
let body = reply_body.clone();
std::thread::spawn(move || serve(stream, &body));
}
});
Self { address, stop }
}
}
impl Drop for NullCluster {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
let _ = TcpStream::connect(self.address.trim_start_matches("http://"));
}
}
fn serve(mut stream: TcpStream, body: &[u8]) {
let mut reader = BufReader::new(stream.try_clone().expect("clones"));
loop {
let mut head = String::new();
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => return,
Ok(_) if line == "\r\n" => break,
Ok(_) => head.push_str(&line),
Err(_) => return,
}
}
if head.is_empty() {
return;
}
let lowercase = head.to_lowercase();
if let Some(length) = lowercase
.lines()
.find(|line| line.starts_with("content-length:"))
.and_then(|line| line.split(':').nth(1))
.and_then(|value| value.trim().parse::<usize>().ok())
{
let mut sink = vec![0_u8; length];
if reader.read_exact(&mut sink).is_err() {
return;
}
} else if lowercase.contains("transfer-encoding: chunked") {
if !drain_chunked(&mut reader) {
return;
}
}
let mut reply =
format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", body.len()).into_bytes();
reply.extend_from_slice(body);
if stream.write_all(&reply).is_err() {
return;
}
stream.flush().ok();
}
}
fn drain_chunked(reader: &mut BufReader<TcpStream>) -> bool {
loop {
let mut header = String::new();
if reader.read_line(&mut header).is_err() {
return false;
}
let size = header.trim().split(';').next().unwrap_or("");
let Ok(size) = usize::from_str_radix(size, 16) else {
return false;
};
if size == 0 {
let mut trailer = String::new();
reader.read_line(&mut trailer).ok();
return true;
}
let mut chunk = vec![0_u8; size + 2]; if reader.read_exact(&mut chunk).is_err() {
return false;
}
}
}
fn client_for(cluster: &NullCluster) -> Client {
Client::new(&cluster.address).with_retries(RetryPolicy::none())
}
fn writing(c: &mut Criterion) {
let mut group = c.benchmark_group("write");
for rows in SIZES {
let bytes = encoded_by_hand(rows).len() as u64;
group.throughput(Throughput::Bytes(bytes));
group.bench_with_input(
BenchmarkId::new("write_table_rows", rows),
&rows,
|b, &n| {
let cluster = NullCluster::new(br#"{}"#.to_vec());
let client = client_for(&cluster);
b.iter(|| {
client
.write_table_rows("//tmp/bench", (0..n).map(row))
.expect("writes");
});
},
);
group.bench_with_input(
BenchmarkId::new("encode_then_write_table", rows),
&rows,
|b, &n| {
let cluster = NullCluster::new(br#"{}"#.to_vec());
let client = client_for(&cluster);
b.iter(|| {
let encoded = encoded_by_hand(n);
client.write_table("//tmp/bench", &encoded).expect("writes");
});
},
);
}
group.finish();
}
fn reading(c: &mut Criterion) {
let mut group = c.benchmark_group("read");
for rows in SIZES {
let table = encoded_by_hand(rows);
group.throughput(Throughput::Bytes(table.len() as u64));
group.bench_with_input(BenchmarkId::new("read_table_rows", rows), &rows, |b, _| {
let cluster = NullCluster::new(table.clone());
let client = client_for(&cluster);
b.iter(|| {
let rows: Vec<Row> = client.read_table_rows("//tmp/bench").expect("reads");
assert!(!rows.is_empty());
});
});
group.bench_with_input(BenchmarkId::new("read_table", rows), &rows, |b, _| {
let cluster = NullCluster::new(table.clone());
let client = client_for(&cluster);
b.iter(|| {
let bytes = client.read_table("//tmp/bench").expect("reads");
assert!(!bytes.is_empty());
});
});
}
group.finish();
}
criterion_group!(benches, writing, reading);
criterion_main!(benches);