1use std::collections::BTreeMap;
29use std::hint::black_box;
30use std::io::{self, Write};
31use std::path::PathBuf;
32use std::time::Instant;
33
34use subms::{
35 SubMsFeatureManifest, SubMsP99Source, SubMsPerfHarness, SubMsTimer, classify_feature, summarize,
36};
37
38use subms_merge_iterator::MergeIterator;
39#[cfg(feature = "reverse")]
40use subms_merge_iterator::ReverseMergeIterator;
41#[cfg(feature = "seek-to")]
42use subms_merge_iterator::SeekableMergeIterator;
43#[cfg(feature = "dedup")]
44use subms_merge_iterator::{DedupEntry, DedupMergeIterator};
45#[cfg(feature = "priority")]
46use subms_merge_iterator::{PriorityEntry, PriorityMergeIterator, PrioritySource};
47#[cfg(feature = "tombstones")]
48use subms_merge_iterator::{TombstoneEntry, TombstoneMergeIterator};
49
50const SIZES: [usize; 3] = [32_768, 262_144, 2_097_152];
55const CANON: usize = SIZES[SIZES.len() - 1];
56const STREAMS: usize = 16;
57
58const BATCH: usize = 64;
61const SAMPLES: usize = 512;
64const MAX_PASSES: usize = 16;
69
70const WARM_NANOS: u64 = 300_000_000;
71const WARM_MAX_REPS: usize = 64;
72
73#[cfg(any(feature = "seek-to", feature = "reverse"))]
79const SEEK_SKIP: u64 = 64;
80#[cfg(any(feature = "seek-to", feature = "reverse"))]
82const SEEK_ROUNDS: usize = 256;
83#[cfg(any(feature = "seek-to", feature = "reverse"))]
87const SEEK_BATCH: usize = 2;
88#[cfg(feature = "seek-to")]
89const SEEK_NEXT_ROUNDS: usize = 128;
90#[cfg(any(feature = "seek-to", feature = "reverse"))]
97const SEEK_PASSES: usize = 4;
98
99fn plain_streams(n: usize) -> Vec<std::vec::IntoIter<u64>> {
102 let per = n / STREAMS;
103 (0..STREAMS)
104 .map(|s| {
105 (0..per)
106 .map(move |i| (s + i * STREAMS) as u64)
107 .collect::<Vec<u64>>()
108 .into_iter()
109 })
110 .collect()
111}
112
113fn stat(h: &SubMsPerfHarness, median: bool) -> u64 {
114 summarize(h)
115 .stages
116 .iter()
117 .find(|s| s.name == "op")
118 .map_or(0, |s| if median { s.p50_ns } else { s.p99_ns })
119}
120
121fn harness() -> SubMsPerfHarness {
122 SubMsPerfHarness::new("merge-iterator-feature", "rust")
123}
124
125fn warmed(mut measure: impl FnMut(&mut SubMsPerfHarness), median: bool) -> u64 {
135 let start = Instant::now();
136 for _ in 0..WARM_MAX_REPS {
137 let mut scratch = harness();
138 measure(&mut scratch);
139 black_box(stat(&scratch, median));
140 if start.elapsed().as_nanos() as u64 >= WARM_NANOS {
141 break;
142 }
143 }
144 let mut h = harness();
145 measure(&mut h);
146 stat(&h, median)
147}
148
149fn per_element<It: Iterator>(
155 mut make: impl FnMut() -> It,
156 expected_out: usize,
157 median: bool,
158) -> u64 {
159 let stride = (expected_out / (BATCH * SAMPLES)).max(1);
160 warmed(
161 |h| {
162 let st = h.stage("op", SAMPLES + 1);
163 let mut recorded = 0usize;
164 for _ in 0..MAX_PASSES {
165 if recorded >= SAMPLES {
166 break;
167 }
168 let mut it = make();
169 let mut batch = 0usize;
170 loop {
171 let timed = batch % stride == 0;
172 let mut taken = 0usize;
173 if timed {
174 let t0 = SubMsTimer::tick();
175 while taken < BATCH && it.next().is_some() {
176 taken += 1;
177 }
178 let ns = t0.elapsed_ns();
179 if taken == BATCH {
180 st.record(ns / BATCH as u64);
181 recorded += 1;
182 }
183 } else {
184 while taken < BATCH && it.next().is_some() {
185 taken += 1;
186 }
187 }
188 if taken < BATCH {
189 break;
190 }
191 batch += 1;
192 }
193 }
194 },
195 median,
196 )
197}
198
199fn sweep(label: &str, mut at: impl FnMut(usize) -> u64) -> Vec<(usize, u64)> {
202 let rows: Vec<(usize, u64)> = SIZES.iter().map(|&n| (n, at(n))).collect();
203 eprintln!("sweep {label}: {rows:?}");
204 rows
205}
206
207#[cfg(feature = "seek-to")]
208fn seek_only(n: usize, median: bool) -> u64 {
209 warmed(
210 |h| {
211 let st = h.stage("op", SEEK_PASSES * SEEK_ROUNDS / SEEK_BATCH + 1);
212 for _ in 0..SEEK_PASSES {
213 let mut it = SeekableMergeIterator::new(plain_streams(n));
214 let mut r = 0usize;
215 while r < SEEK_ROUNDS {
216 let t0 = SubMsTimer::tick();
217 for _ in 0..SEEK_BATCH {
218 r += 1;
219 it.seek(&(r as u64 * SEEK_SKIP));
220 }
221 st.record(t0.elapsed_ns() / SEEK_BATCH as u64);
222 }
223 }
224 },
225 median,
226 )
227}
228
229#[cfg(feature = "seek-to")]
233fn seek_then_next(n: usize, median: bool) -> u64 {
234 let stride = (SEEK_SKIP as usize + BATCH) as u64;
235 warmed(
236 |h| {
237 let st = h.stage("op", SEEK_PASSES * SEEK_NEXT_ROUNDS + 1);
238 for _ in 0..SEEK_PASSES {
239 let mut it = SeekableMergeIterator::new(plain_streams(n));
240 for r in 0..SEEK_NEXT_ROUNDS {
241 it.seek(&(r as u64 * stride));
242 let t0 = SubMsTimer::tick();
243 let mut taken = 0usize;
244 while taken < BATCH && it.next().is_some() {
245 taken += 1;
246 }
247 let ns = t0.elapsed_ns();
248 if taken == BATCH {
249 st.record(ns / BATCH as u64);
250 }
251 }
252 }
253 },
254 median,
255 )
256}
257
258fn main() -> io::Result<()> {
259 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
260 .join("..")
261 .join(".subms")
262 .join("features")
263 .join("rust.json");
264 let existing = std::fs::read_to_string(&path).unwrap_or_default();
265 let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
266 let (source, instance) = SubMsP99Source::from_env();
270 manifest.set_p99_source(source, instance.as_deref());
271
272 let base_sw = sweep("base/next", |n| {
277 per_element(|| MergeIterator::new(plain_streams(n)), n, true)
278 });
279 let base_p50 = base_sw[base_sw.len() - 1].1;
280 eprintln!("base next p50: {base_p50}ns/element");
281
282 #[cfg(feature = "seek-to")]
284 {
285 let sw = sweep("seek-to/seek", |n| seek_only(n, true));
286 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
287
288 let mut p99 = BTreeMap::new();
289 p99.insert("seek".to_string(), seek_only(CANON, false));
290 p99.insert("next_after_seek".to_string(), seek_then_next(CANON, false));
291 manifest.set_feature("seek-to", cat, &p99, &reason);
292 }
293
294 #[cfg(feature = "reverse")]
296 {
297 let sw = sweep("reverse/next", |n| {
298 per_element(|| ReverseMergeIterator::new(descending_streams(n)), n, true)
299 });
300 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
301
302 let mut p99 = BTreeMap::new();
303 p99.insert(
304 "reverse_next".to_string(),
305 per_element(
306 || ReverseMergeIterator::new(descending_streams(CANON)),
307 CANON,
308 false,
309 ),
310 );
311 p99.insert(
312 "seek_for_prev".to_string(),
313 seek_for_prev_only(CANON, false),
314 );
315 manifest.set_feature("reverse", cat, &p99, &reason);
316 }
317
318 #[cfg(feature = "tombstones")]
320 {
321 let sw = sweep("tombstones/next", |n| {
324 per_element(
325 || TombstoneMergeIterator::new(tombstone_streams(n)),
326 n / 8 * 7,
327 true,
328 )
329 });
330 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
331
332 let mut p99 = BTreeMap::new();
333 p99.insert(
334 "tombstones_next".to_string(),
335 per_element(
336 || TombstoneMergeIterator::new(tombstone_streams(CANON)),
337 CANON / 8 * 7,
338 false,
339 ),
340 );
341 manifest.set_feature("tombstones", cat, &p99, &reason);
342 }
343
344 #[cfg(feature = "dedup")]
346 {
347 let sw = sweep("dedup/next", |n| {
350 per_element(|| DedupMergeIterator::new(dedup_streams(n)), n / 2, true)
351 });
352 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
353
354 let mut p99 = BTreeMap::new();
355 p99.insert(
356 "dedup_next".to_string(),
357 per_element(
358 || DedupMergeIterator::new(dedup_streams(CANON)),
359 CANON / 2,
360 false,
361 ),
362 );
363 manifest.set_feature("dedup", cat, &p99, &reason);
364 }
365
366 #[cfg(feature = "priority")]
368 {
369 let sw = sweep("priority/next", |n| {
372 per_element(
373 || PriorityMergeIterator::new(priority_sources(n)),
374 n / 2,
375 true,
376 )
377 });
378 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
379
380 let mut p99 = BTreeMap::new();
381 p99.insert(
382 "priority_next".to_string(),
383 per_element(
384 || PriorityMergeIterator::new(priority_sources(CANON)),
385 CANON / 2,
386 false,
387 ),
388 );
389 manifest.set_feature("priority", cat, &p99, &reason);
390 }
391
392 std::fs::create_dir_all(path.parent().unwrap())?;
393 std::fs::write(&path, manifest.to_json())?;
394 io::stdout().write_all(manifest.to_json().as_bytes())?;
395 Ok(())
396}
397
398#[cfg(feature = "reverse")]
401fn descending_streams(n: usize) -> Vec<std::vec::IntoIter<u64>> {
402 let per = n / STREAMS;
403 (0..STREAMS)
404 .map(|s| {
405 (0..per)
406 .map(move |i| (s + (per - 1 - i) * STREAMS) as u64)
407 .collect::<Vec<u64>>()
408 .into_iter()
409 })
410 .collect()
411}
412
413#[cfg(feature = "reverse")]
416fn seek_for_prev_only(n: usize, median: bool) -> u64 {
417 warmed(
418 |h| {
419 let st = h.stage("op", SEEK_PASSES * SEEK_ROUNDS / SEEK_BATCH + 1);
420 for _ in 0..SEEK_PASSES {
421 let mut it = ReverseMergeIterator::new(descending_streams(n));
422 let top = (n - 1) as u64;
423 let mut r = 0usize;
424 while r < SEEK_ROUNDS {
425 let t0 = SubMsTimer::tick();
426 for _ in 0..SEEK_BATCH {
427 r += 1;
428 it.seek_for_prev(&top.saturating_sub(r as u64 * SEEK_SKIP));
429 }
430 st.record(t0.elapsed_ns() / SEEK_BATCH as u64);
431 }
432 }
433 },
434 median,
435 )
436}
437
438#[cfg(feature = "tombstones")]
439fn tombstone_streams(n: usize) -> Vec<std::vec::IntoIter<TombstoneEntry<u64, u64>>> {
440 let per = n / STREAMS;
441 (0..STREAMS)
442 .map(|s| {
443 (0..per)
444 .map(move |i| {
445 let key = (s + i * STREAMS) as u64;
446 if key % 8 == 0 {
447 TombstoneEntry::tombstone(key)
448 } else {
449 TombstoneEntry::live(key, key)
450 }
451 })
452 .collect::<Vec<_>>()
453 .into_iter()
454 })
455 .collect()
456}
457
458#[cfg(feature = "dedup")]
459fn dedup_streams(n: usize) -> Vec<std::vec::IntoIter<DedupEntry<u64, u64>>> {
460 let per = n / STREAMS;
461 (0..STREAMS)
462 .map(|s| {
463 (0..per)
464 .map(move |i| {
465 let key = ((s + i * STREAMS) as u64) / 2;
466 DedupEntry::new(key, key)
467 })
468 .collect::<Vec<_>>()
469 .into_iter()
470 })
471 .collect()
472}
473
474#[cfg(feature = "priority")]
475fn priority_sources(n: usize) -> Vec<PrioritySource<std::vec::IntoIter<PriorityEntry<u64, u64>>>> {
476 let per = n / STREAMS;
477 (0..STREAMS)
478 .map(|s| {
479 let stream = (0..per)
480 .map(move |i| {
481 let key = ((s + i * STREAMS) as u64) / 2;
482 PriorityEntry::new(key, key)
483 })
484 .collect::<Vec<_>>()
485 .into_iter();
486 PrioritySource::new((STREAMS - s) as i32, stream)
487 })
488 .collect()
489}