bench_latency/
bench_latency.rs1use std::time::{Duration, Instant};
8
9use bytes::Bytes;
10use partitionline::{Consumer, ConsumerConfig, ProduceRecord, Producer, ProducerConfig};
11
12fn env_or(key: &str, default: &str) -> String {
13 std::env::var(key).unwrap_or_else(|_| default.into())
14}
15
16fn env_parse<T: std::str::FromStr>(key: &str, default: T) -> T {
17 std::env::var(key)
18 .ok()
19 .and_then(|s| s.parse().ok())
20 .unwrap_or(default)
21}
22
23fn percentile_us(sorted: &[u64], p: u32) -> partitionline::Result<u64> {
24 if sorted.is_empty() {
25 return Err(partitionline::Error::protocol("no latency samples"));
26 }
27 let n = sorted.len();
28 let rank = n
29 .saturating_mul(p as usize)
30 .div_ceil(100)
31 .saturating_sub(1)
32 .min(n.saturating_sub(1));
33 sorted
34 .get(rank)
35 .copied()
36 .ok_or_else(|| partitionline::Error::protocol("percentile index"))
37}
38
39fn print_latency(kind: &str, mut samples: Vec<u64>, extra: &str) -> partitionline::Result<()> {
40 if samples.is_empty() {
41 return Err(partitionline::Error::protocol(format!(
42 "{kind}: no latency samples"
43 )));
44 }
45 samples.sort_unstable();
46 let n = samples.len();
47 let min_us = samples.first().copied().unwrap_or(0);
48 let max_us = samples.last().copied().unwrap_or(0);
49 let sum: u128 = samples.iter().map(|v| u128::from(*v)).sum();
50 let mean_us = u64::try_from(sum / u128::from(n as u64))
51 .map_err(|_| partitionline::Error::protocol("mean overflow"))?;
52 let p50_us = percentile_us(&samples, 50)?;
53 let p99_us = percentile_us(&samples, 99)?;
54 println!(
55 "{{\"kind\":\"{kind}\",\"samples\":{n},\"p50_us\":{p50_us},\"p99_us\":{p99_us},\"min_us\":{min_us},\"max_us\":{max_us},\"mean_us\":{mean_us}{extra}}}"
56 );
57 Ok(())
58}
59
60async fn produce_ack(
61 producer: &Producer,
62 topic: &str,
63 value: &Bytes,
64 n: u64,
65) -> partitionline::Result<Vec<u64>> {
66 let mut samples = Vec::with_capacity(usize::try_from(n).unwrap_or(0));
67 for _ in 0..n {
68 let start = Instant::now();
69 let _md = producer
70 .send(ProduceRecord::to(topic).value(value.clone()))
71 .await?;
72 samples.push(u64::try_from(start.elapsed().as_micros()).unwrap_or(u64::MAX));
73 }
74 Ok(samples)
75}
76
77async fn fetch_rpc(consumer: &mut Consumer, count: u64) -> partitionline::Result<(Vec<u64>, u64)> {
78 let mut samples = Vec::new();
79 let mut got = 0u64;
80 let mut empty = 0u32;
81 while got < count {
82 let start = Instant::now();
83 let recs = consumer.fetch().await?;
84 let elapsed = u64::try_from(start.elapsed().as_micros()).unwrap_or(u64::MAX);
85 if recs.is_empty() {
86 empty += 1;
87 if empty > 600 {
88 return Err(partitionline::Error::Timeout);
89 }
90 continue;
91 }
92 empty = 0;
93 samples.push(elapsed);
94 got += recs.len() as u64;
95 }
96 Ok((samples, got))
97}
98
99#[tokio::main]
100async fn main() -> partitionline::Result<()> {
101 let bootstrap = env_or("KAFKA_BOOTSTRAP", "127.0.0.1:9092");
102 let topic = env_or("KAFKA_TOPIC", "pllat");
103 let payload = env_parse("PAYLOAD_BYTES", 100usize);
104 let warmup = env_parse("WARMUP", 1_000u64);
105 let count = env_parse("COUNT", 10_000u64);
106 let linger_ms = env_parse("LINGER_MS", 0u64);
107 let acks = env_parse("ACKS", 1i16);
108 let max_wait_ms = env_parse("MAX_WAIT_MS", 100i32);
109 let max_bytes = env_parse("MAX_BYTES", 4_096i32);
110 let min_bytes = env_parse("MIN_BYTES", 1i32);
111 let mode = env_or("MODE", "both");
112
113 let mut pcfg = ProducerConfig::bootstrap([bootstrap.clone()]);
114 pcfg.linger = Duration::from_millis(linger_ms);
115 pcfg.acks = acks;
116 pcfg.batch_records = 1;
117 pcfg.batch_bytes = payload.saturating_add(256).max(1);
118 pcfg.connections = 1;
119 pcfg.max_in_flight = 1;
120 let producer = Producer::new(pcfg).await?;
121 let value = Bytes::from(vec![b'x'; payload]);
122
123 if warmup > 0 {
124 let _ = produce_ack(&producer, &topic, &value, warmup).await?;
125 }
126
127 if mode == "produce" || mode == "both" {
128 let samples = produce_ack(&producer, &topic, &value, count).await?;
129 print_latency(
130 "produce_ack",
131 samples,
132 &format!(
133 ",\"payload_bytes\":{payload},\"acks\":{acks},\"linger_ms\":{linger_ms},\"client\":\"partitionline\""
134 ),
135 )?;
136 }
137 producer.close().await?;
138
139 if mode == "fetch" || mode == "both" {
140 if mode == "fetch" {
141 let producer = Producer::new({
142 let mut cfg = ProducerConfig::bootstrap([bootstrap.clone()]);
143 cfg.linger = Duration::ZERO;
144 cfg.acks = acks;
145 cfg.batch_records = 1;
146 cfg.connections = 1;
147 cfg.max_in_flight = 1;
148 cfg
149 })
150 .await?;
151 let _ = produce_ack(&producer, &topic, &value, count).await?;
152 producer.close().await?;
153 }
154 let mut ccfg = ConsumerConfig::bootstrap([bootstrap]);
155 ccfg.max_wait_ms = max_wait_ms;
156 ccfg.max_bytes = max_bytes;
157 ccfg.min_bytes = min_bytes;
158 let mut consumer = Consumer::new(ccfg).await?;
159 consumer.assign(&topic, 0, 0).await?;
160 let (samples, got) = fetch_rpc(&mut consumer, count).await?;
161 print_latency(
162 "fetch_rpc",
163 samples,
164 &format!(
165 ",\"consumed\":{got},\"max_wait_ms\":{max_wait_ms},\"max_bytes\":{max_bytes},\"min_bytes\":{min_bytes},\"client\":\"partitionline\""
166 ),
167 )?;
168 }
169 Ok(())
170}