use apache_datasketches::tuple::generic::{TupleSketch, TupleSketchBuilder, TupleSummary};
use std::time::{Duration, Instant};
const LG_K: u8 = 12;
const HOT_KEY_SPACE: u64 = 1 << 10;
const STR_KEY_SPACE: u64 = 1 << 16;
fn string_keys() -> Vec<String> {
(0..STR_KEY_SPACE).map(|i| format!("key_{i:010}")).collect()
}
#[derive(Clone)]
struct Count(u64);
impl TupleSummary for Count {
type Update = ();
fn create(_: &()) -> Self {
Count(1)
}
fn union_combine(&mut self, other: &Self) {
self.0 += other.0;
}
fn intersection_combine(&mut self, other: &Self) {
self.0 += other.0;
}
}
const LADDER: [u64; 3] = [1_000_000, 10_000_000, 100_000_000];
const DEFAULT_ITEMS: u64 = 10_000_000;
const DEFAULT_REPS: usize = 3;
fn parse_args() -> (Vec<u64>, usize) {
let mut items = None;
let mut reps = DEFAULT_REPS;
let mut ladder = false;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--ladder" => ladder = true,
"--reps" => {
reps = args
.next()
.and_then(|v| v.parse().ok())
.filter(|&n| n > 0)
.expect("--reps needs a positive integer")
}
other => {
let n = other
.parse()
.expect("item count must be a positive integer");
assert!(n > 0, "item count must be a positive integer");
items = Some(n);
}
}
}
assert!(
!(ladder && items.is_some()),
"pass an item count or --ladder, not both"
);
let counts = if ladder {
LADDER.to_vec()
} else {
vec![items.unwrap_or(DEFAULT_ITEMS)]
};
(counts, reps)
}
fn report(label: &str, items: u64, passes: &[Pass]) {
for (i, pass) in passes.iter().enumerate() {
assert_eq!(
pass.estimate, passes[0].estimate,
"rep {i} estimated {} but rep 0 estimated {}: the reps are not running \
the same workload",
pass.estimate, passes[0].estimate
);
}
let mut ns_per_op: Vec<f64> = passes
.iter()
.map(|p| p.elapsed.as_secs_f64() * 1e9 / items as f64)
.collect();
ns_per_op.sort_by(f64::total_cmp);
let median = ns_per_op[(ns_per_op.len() - 1) / 2];
let (min, max) = (ns_per_op[0], ns_per_op[ns_per_op.len() - 1]);
let rate = 1000.0 / median;
let (reps, estimate) = (passes.len(), passes[0].estimate);
println!(
"{label:9} {items:>12} items {median:>7.2} ns/op min {min:>7.2} max {max:>7.2} \
{rate:>8.1} M/s reps={reps} estimate={estimate:.0}"
);
}
struct Pass {
elapsed: Duration,
estimate: f64,
}
fn build() -> TupleSketch<Count> {
TupleSketchBuilder::new()
.lg_k(LG_K)
.build()
.expect("builder rejected fixed valid parameters")
}
fn bench_distinct(items: u64, reps: usize) {
let mut passes = Vec::with_capacity(reps);
for _ in 0..reps {
let mut sketch = build();
let start = Instant::now();
for key in 0..items {
sketch.update_u64(key, &());
}
let elapsed = start.elapsed();
passes.push(Pass {
elapsed,
estimate: sketch.get_estimate(),
});
}
report("distinct", items, &passes);
}
fn bench_hot(items: u64, reps: usize) {
let mut passes = Vec::with_capacity(reps);
for _ in 0..reps {
let mut sketch = build();
let start = Instant::now();
for i in 0..items {
sketch.update_u64(i % HOT_KEY_SPACE, &());
}
let elapsed = start.elapsed();
passes.push(Pass {
elapsed,
estimate: sketch.get_estimate(),
});
}
report("hot", items, &passes);
}
fn bench_str(items: u64, reps: usize) {
let keys = string_keys();
let mut passes = Vec::with_capacity(reps);
for _ in 0..reps {
let mut sketch = build();
let start = Instant::now();
for i in 0..items {
sketch.update_str(&keys[(i % STR_KEY_SPACE) as usize], &());
}
let elapsed = start.elapsed();
passes.push(Pass {
elapsed,
estimate: sketch.get_estimate(),
});
}
report("str", items, &passes);
}
fn main() {
let (counts, reps) = parse_args();
for items in counts {
println!("lg_k={LG_K} items={items} reps={reps}");
bench_distinct(items, reps);
bench_hot(items, reps);
bench_str(items, reps);
}
}