use std::collections::BTreeMap;
use std::hint::black_box;
use std::io::{self, Write};
use std::path::PathBuf;
use std::time::Instant;
use subms::{
SubMsFeatureManifest, SubMsP99Source, SubMsPerfHarness, SubMsTimer, classify_feature, summarize,
};
use subms_merge_iterator::MergeIterator;
#[cfg(feature = "reverse")]
use subms_merge_iterator::ReverseMergeIterator;
#[cfg(feature = "seek-to")]
use subms_merge_iterator::SeekableMergeIterator;
#[cfg(feature = "dedup")]
use subms_merge_iterator::{DedupEntry, DedupMergeIterator};
#[cfg(feature = "priority")]
use subms_merge_iterator::{PriorityEntry, PriorityMergeIterator, PrioritySource};
#[cfg(feature = "tombstones")]
use subms_merge_iterator::{TombstoneEntry, TombstoneMergeIterator};
const SIZES: [usize; 3] = [32_768, 262_144, 2_097_152];
const CANON: usize = SIZES[SIZES.len() - 1];
const STREAMS: usize = 16;
const BATCH: usize = 64;
const SAMPLES: usize = 512;
const MAX_PASSES: usize = 16;
const WARM_NANOS: u64 = 300_000_000;
const WARM_MAX_REPS: usize = 64;
#[cfg(any(feature = "seek-to", feature = "reverse"))]
const SEEK_SKIP: u64 = 64;
#[cfg(any(feature = "seek-to", feature = "reverse"))]
const SEEK_ROUNDS: usize = 256;
#[cfg(any(feature = "seek-to", feature = "reverse"))]
const SEEK_BATCH: usize = 2;
#[cfg(feature = "seek-to")]
const SEEK_NEXT_ROUNDS: usize = 128;
#[cfg(any(feature = "seek-to", feature = "reverse"))]
const SEEK_PASSES: usize = 4;
fn plain_streams(n: usize) -> Vec<std::vec::IntoIter<u64>> {
let per = n / STREAMS;
(0..STREAMS)
.map(|s| {
(0..per)
.map(move |i| (s + i * STREAMS) as u64)
.collect::<Vec<u64>>()
.into_iter()
})
.collect()
}
fn stat(h: &SubMsPerfHarness, median: bool) -> u64 {
summarize(h)
.stages
.iter()
.find(|s| s.name == "op")
.map_or(0, |s| if median { s.p50_ns } else { s.p99_ns })
}
fn harness() -> SubMsPerfHarness {
SubMsPerfHarness::new("merge-iterator-feature", "rust")
}
fn warmed(mut measure: impl FnMut(&mut SubMsPerfHarness), median: bool) -> u64 {
let start = Instant::now();
for _ in 0..WARM_MAX_REPS {
let mut scratch = harness();
measure(&mut scratch);
black_box(stat(&scratch, median));
if start.elapsed().as_nanos() as u64 >= WARM_NANOS {
break;
}
}
let mut h = harness();
measure(&mut h);
stat(&h, median)
}
fn per_element<It: Iterator>(
mut make: impl FnMut() -> It,
expected_out: usize,
median: bool,
) -> u64 {
let stride = (expected_out / (BATCH * SAMPLES)).max(1);
warmed(
|h| {
let st = h.stage("op", SAMPLES + 1);
let mut recorded = 0usize;
for _ in 0..MAX_PASSES {
if recorded >= SAMPLES {
break;
}
let mut it = make();
let mut batch = 0usize;
loop {
let timed = batch % stride == 0;
let mut taken = 0usize;
if timed {
let t0 = SubMsTimer::tick();
while taken < BATCH && it.next().is_some() {
taken += 1;
}
let ns = t0.elapsed_ns();
if taken == BATCH {
st.record(ns / BATCH as u64);
recorded += 1;
}
} else {
while taken < BATCH && it.next().is_some() {
taken += 1;
}
}
if taken < BATCH {
break;
}
batch += 1;
}
}
},
median,
)
}
fn sweep(label: &str, mut at: impl FnMut(usize) -> u64) -> Vec<(usize, u64)> {
let rows: Vec<(usize, u64)> = SIZES.iter().map(|&n| (n, at(n))).collect();
eprintln!("sweep {label}: {rows:?}");
rows
}
#[cfg(feature = "seek-to")]
fn seek_only(n: usize, median: bool) -> u64 {
warmed(
|h| {
let st = h.stage("op", SEEK_PASSES * SEEK_ROUNDS / SEEK_BATCH + 1);
for _ in 0..SEEK_PASSES {
let mut it = SeekableMergeIterator::new(plain_streams(n));
let mut r = 0usize;
while r < SEEK_ROUNDS {
let t0 = SubMsTimer::tick();
for _ in 0..SEEK_BATCH {
r += 1;
it.seek(&(r as u64 * SEEK_SKIP));
}
st.record(t0.elapsed_ns() / SEEK_BATCH as u64);
}
}
},
median,
)
}
#[cfg(feature = "seek-to")]
fn seek_then_next(n: usize, median: bool) -> u64 {
let stride = (SEEK_SKIP as usize + BATCH) as u64;
warmed(
|h| {
let st = h.stage("op", SEEK_PASSES * SEEK_NEXT_ROUNDS + 1);
for _ in 0..SEEK_PASSES {
let mut it = SeekableMergeIterator::new(plain_streams(n));
for r in 0..SEEK_NEXT_ROUNDS {
it.seek(&(r as u64 * stride));
let t0 = SubMsTimer::tick();
let mut taken = 0usize;
while taken < BATCH && it.next().is_some() {
taken += 1;
}
let ns = t0.elapsed_ns();
if taken == BATCH {
st.record(ns / BATCH as u64);
}
}
}
},
median,
)
}
fn main() -> io::Result<()> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join(".subms")
.join("features")
.join("rust.json");
let existing = std::fs::read_to_string(&path).unwrap_or_default();
let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
let (source, instance) = SubMsP99Source::from_env();
manifest.set_p99_source(source, instance.as_deref());
let base_sw = sweep("base/next", |n| {
per_element(|| MergeIterator::new(plain_streams(n)), n, true)
});
let base_p50 = base_sw[base_sw.len() - 1].1;
eprintln!("base next p50: {base_p50}ns/element");
#[cfg(feature = "seek-to")]
{
let sw = sweep("seek-to/seek", |n| seek_only(n, true));
let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
let mut p99 = BTreeMap::new();
p99.insert("seek".to_string(), seek_only(CANON, false));
p99.insert("next_after_seek".to_string(), seek_then_next(CANON, false));
manifest.set_feature("seek-to", cat, &p99, &reason);
}
#[cfg(feature = "reverse")]
{
let sw = sweep("reverse/next", |n| {
per_element(|| ReverseMergeIterator::new(descending_streams(n)), n, true)
});
let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
let mut p99 = BTreeMap::new();
p99.insert(
"reverse_next".to_string(),
per_element(
|| ReverseMergeIterator::new(descending_streams(CANON)),
CANON,
false,
),
);
p99.insert(
"seek_for_prev".to_string(),
seek_for_prev_only(CANON, false),
);
manifest.set_feature("reverse", cat, &p99, &reason);
}
#[cfg(feature = "tombstones")]
{
let sw = sweep("tombstones/next", |n| {
per_element(
|| TombstoneMergeIterator::new(tombstone_streams(n)),
n / 8 * 7,
true,
)
});
let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
let mut p99 = BTreeMap::new();
p99.insert(
"tombstones_next".to_string(),
per_element(
|| TombstoneMergeIterator::new(tombstone_streams(CANON)),
CANON / 8 * 7,
false,
),
);
manifest.set_feature("tombstones", cat, &p99, &reason);
}
#[cfg(feature = "dedup")]
{
let sw = sweep("dedup/next", |n| {
per_element(|| DedupMergeIterator::new(dedup_streams(n)), n / 2, true)
});
let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
let mut p99 = BTreeMap::new();
p99.insert(
"dedup_next".to_string(),
per_element(
|| DedupMergeIterator::new(dedup_streams(CANON)),
CANON / 2,
false,
),
);
manifest.set_feature("dedup", cat, &p99, &reason);
}
#[cfg(feature = "priority")]
{
let sw = sweep("priority/next", |n| {
per_element(
|| PriorityMergeIterator::new(priority_sources(n)),
n / 2,
true,
)
});
let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
let mut p99 = BTreeMap::new();
p99.insert(
"priority_next".to_string(),
per_element(
|| PriorityMergeIterator::new(priority_sources(CANON)),
CANON / 2,
false,
),
);
manifest.set_feature("priority", cat, &p99, &reason);
}
std::fs::create_dir_all(path.parent().unwrap())?;
std::fs::write(&path, manifest.to_json())?;
io::stdout().write_all(manifest.to_json().as_bytes())?;
Ok(())
}
#[cfg(feature = "reverse")]
fn descending_streams(n: usize) -> Vec<std::vec::IntoIter<u64>> {
let per = n / STREAMS;
(0..STREAMS)
.map(|s| {
(0..per)
.map(move |i| (s + (per - 1 - i) * STREAMS) as u64)
.collect::<Vec<u64>>()
.into_iter()
})
.collect()
}
#[cfg(feature = "reverse")]
fn seek_for_prev_only(n: usize, median: bool) -> u64 {
warmed(
|h| {
let st = h.stage("op", SEEK_PASSES * SEEK_ROUNDS / SEEK_BATCH + 1);
for _ in 0..SEEK_PASSES {
let mut it = ReverseMergeIterator::new(descending_streams(n));
let top = (n - 1) as u64;
let mut r = 0usize;
while r < SEEK_ROUNDS {
let t0 = SubMsTimer::tick();
for _ in 0..SEEK_BATCH {
r += 1;
it.seek_for_prev(&top.saturating_sub(r as u64 * SEEK_SKIP));
}
st.record(t0.elapsed_ns() / SEEK_BATCH as u64);
}
}
},
median,
)
}
#[cfg(feature = "tombstones")]
fn tombstone_streams(n: usize) -> Vec<std::vec::IntoIter<TombstoneEntry<u64, u64>>> {
let per = n / STREAMS;
(0..STREAMS)
.map(|s| {
(0..per)
.map(move |i| {
let key = (s + i * STREAMS) as u64;
if key % 8 == 0 {
TombstoneEntry::tombstone(key)
} else {
TombstoneEntry::live(key, key)
}
})
.collect::<Vec<_>>()
.into_iter()
})
.collect()
}
#[cfg(feature = "dedup")]
fn dedup_streams(n: usize) -> Vec<std::vec::IntoIter<DedupEntry<u64, u64>>> {
let per = n / STREAMS;
(0..STREAMS)
.map(|s| {
(0..per)
.map(move |i| {
let key = ((s + i * STREAMS) as u64) / 2;
DedupEntry::new(key, key)
})
.collect::<Vec<_>>()
.into_iter()
})
.collect()
}
#[cfg(feature = "priority")]
fn priority_sources(n: usize) -> Vec<PrioritySource<std::vec::IntoIter<PriorityEntry<u64, u64>>>> {
let per = n / STREAMS;
(0..STREAMS)
.map(|s| {
let stream = (0..per)
.map(move |i| {
let key = ((s + i * STREAMS) as u64) / 2;
PriorityEntry::new(key, key)
})
.collect::<Vec<_>>()
.into_iter();
PrioritySource::new((STREAMS - s) as i32, stream)
})
.collect()
}