1use std::collections::BTreeMap;
18use std::hint::black_box;
19use std::io::{self, Write};
20use std::path::{Path, PathBuf};
21
22use subms::{SubMsFeatureManifest, SubMsP99Source, SubMsPerfHarness, classify_feature, summarize};
23use subms_lsm_tree::LsmTree;
24
25const SIZES: [usize; 3] = [8_192, 65_536, 524_288];
30const CANON_N: usize = SIZES[SIZES.len() - 1];
31const OPS: usize = 20_000;
33const BULK_REPS: usize = 256;
40const BULK_WARM_NANOS: u64 = 300_000_000;
46const BULK_WARM_MAX_REPS: usize = 5_000;
47const KEYED_WARM_NANOS: u64 = 300_000_000;
55const KEYED_WARM_MAX_REPS: usize = 200_000;
56
57const BLOCK_BYTES: usize = 4096;
60const ENTRIES_PER_RUN: usize = 4_096;
62const KEYS_PER_BLOCK: usize = 8;
65const KEYS_PER_SSTABLE: usize = 4_096;
67const FLUSH_BYTES: usize = 1_000_000;
70
71const VALUE: &[u8] = b"value-payload-bytes-24ch";
72
73fn key(i: usize) -> String {
76 format!("k{i:09}")
77}
78
79fn probe(i: usize, n: usize) -> usize {
81 (i.wrapping_mul(2_654_435_761)) % n
82}
83
84fn stat(h: &SubMsPerfHarness, median: bool) -> u64 {
85 summarize(h)
86 .stages
87 .iter()
88 .find(|s| s.name == "op")
89 .map_or(0, |s| if median { s.p50_ns } else { s.p99_ns })
90}
91
92fn keyed(mut op: impl FnMut(usize), median: bool) -> u64 {
94 let start = std::time::Instant::now();
95 for i in 0..KEYED_WARM_MAX_REPS {
96 op(i % OPS);
97 if start.elapsed().as_nanos() as u64 >= KEYED_WARM_NANOS {
98 break;
99 }
100 }
101 let mut h = SubMsPerfHarness::new("lsm-feature", "rust");
102 let st = h.stage("op", OPS);
103 for i in 0..OPS {
104 st.time(|| op(i));
105 }
106 stat(&h, median)
107}
108
109fn bulk(mut op: impl FnMut(), median: bool) -> u64 {
114 let start = std::time::Instant::now();
115 for _ in 0..BULK_WARM_MAX_REPS {
116 op();
117 if start.elapsed().as_nanos() as u64 >= BULK_WARM_NANOS {
118 break;
119 }
120 }
121 let mut h = SubMsPerfHarness::new("lsm-feature", "rust");
122 let st = h.stage("op", BULK_REPS);
123 for _ in 0..BULK_REPS {
124 st.time(&mut op);
125 }
126 stat(&h, median)
127}
128
129fn bulk_each<T>(mut setup: impl FnMut() -> T, mut op: impl FnMut(&mut T), median: bool) -> u64 {
135 let start = std::time::Instant::now();
136 for _ in 0..BULK_WARM_MAX_REPS {
137 let mut input = setup();
138 op(&mut input);
139 if start.elapsed().as_nanos() as u64 >= BULK_WARM_NANOS {
140 break;
141 }
142 }
143 let mut h = SubMsPerfHarness::new("lsm-feature", "rust");
144 let st = h.stage("op", BULK_REPS);
145 for _ in 0..BULK_REPS {
146 let mut input = setup();
147 st.time(|| op(&mut input));
148 }
149 stat(&h, median)
150}
151
152fn sweep(label: &str, mut at: impl FnMut(usize) -> u64) -> Vec<(usize, u64)> {
155 let rows: Vec<(usize, u64)> = SIZES.iter().map(|&n| (n, at(n))).collect();
156 eprintln!("sweep {label}: {rows:?}");
157 rows
158}
159
160fn main() -> io::Result<()> {
161 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
162 .join("..")
163 .join(".subms")
164 .join("features")
165 .join("rust.json");
166 let existing = std::fs::read_to_string(&path).unwrap_or_default();
167 let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
168 let (source, instance) = SubMsP99Source::from_env();
172 manifest.set_p99_source(source, instance.as_deref());
173
174 let tmp = TempDir::new("subms-lsm-features");
175
176 let base_p50 = {
179 let mut tree = LsmTree::open(tmp.path().join("base"), FLUSH_BYTES)?;
180 for i in 0..CANON_N {
181 tree.put(&key(i), VALUE)?;
182 }
183 tree.flush()?;
184 let probes: Vec<String> = (0..OPS).map(|i| key(probe(i, CANON_N))).collect();
185 keyed(
186 |i| {
187 black_box(tree.get(&probes[i]).expect("get"));
188 },
189 true,
190 )
191 };
192 eprintln!("base get p50: {base_p50}ns ({CANON_N} live keys)");
193
194 #[cfg(feature = "wal")]
195 feature_wal(&mut manifest, base_p50, tmp.path());
196
197 #[cfg(feature = "tiered-compaction")]
198 feature_tiered(&mut manifest, base_p50);
199
200 #[cfg(feature = "leveled-compaction")]
201 feature_leveled(&mut manifest, base_p50);
202
203 #[cfg(feature = "snapshot")]
204 feature_snapshot(&mut manifest, base_p50);
205
206 #[cfg(feature = "lz4")]
207 feature_lz4(&mut manifest, base_p50);
208
209 #[cfg(feature = "zstd")]
210 feature_zstd(&mut manifest, base_p50);
211
212 #[cfg(feature = "block-cache-integration")]
213 feature_block_cache(&mut manifest, base_p50);
214
215 drop(tmp);
216 std::fs::create_dir_all(path.parent().unwrap())?;
217 std::fs::write(&path, manifest.to_json())?;
218 io::stdout().write_all(manifest.to_json().as_bytes())?;
219 Ok(())
220}
221
222#[cfg(feature = "wal")]
226fn wal_of(dir: &Path, n: usize) -> PathBuf {
227 use subms_lsm_tree::WriteAheadLog;
228 let path = dir.join(format!("replay-{n}.wal"));
229 let _ = std::fs::remove_file(&path);
230 let mut wal = WriteAheadLog::open(&path).expect("open wal");
231 for i in 0..n {
232 wal.log_put(&key(i), VALUE).expect("log_put");
233 }
234 wal.sync().expect("sync");
235 path
236}
237
238#[cfg(feature = "wal")]
239fn feature_wal(manifest: &mut SubMsFeatureManifest, base_p50: u64, dir: &Path) {
240 use subms_lsm_tree::WriteAheadLog;
241
242 let sw = sweep("wal/replay", |n| {
247 let path = wal_of(dir, n);
248 let out = bulk(
249 || {
250 black_box(WriteAheadLog::replay(&path).expect("replay").len());
251 },
252 true,
253 );
254 let _ = std::fs::remove_file(&path);
255 out
256 });
257 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
258
259 let replay_path = wal_of(dir, CANON_N);
260 let mut p99 = BTreeMap::new();
261 p99.insert(
262 "replay".to_string(),
263 bulk(
264 || {
265 black_box(WriteAheadLog::replay(&replay_path).expect("replay").len());
266 },
267 false,
268 ),
269 );
270 let _ = std::fs::remove_file(&replay_path);
271
272 let append_path = dir.join("append.wal");
274 let _ = std::fs::remove_file(&append_path);
275 {
276 let mut wal = WriteAheadLog::open(&append_path).expect("open wal");
277 let keys: Vec<String> = (0..OPS).map(key).collect();
278 p99.insert(
279 "log_put".to_string(),
280 keyed(|i| wal.log_put(&keys[i], VALUE).expect("log_put"), false),
281 );
282 }
283 let _ = std::fs::remove_file(&append_path);
284 manifest.set_feature("wal", cat, &p99, &reason);
290}
291
292#[cfg(feature = "tiered-compaction")]
295fn feature_tiered(manifest: &mut SubMsFeatureManifest, base_p50: u64) {
296 use subms_lsm_tree::{TieredCompactionPlanner, TieredManifest, TieredRun};
297
298 fn runs(n: usize) -> Vec<TieredRun> {
301 (0..n.div_ceil(ENTRIES_PER_RUN))
302 .map(|r| {
303 let entries: Vec<(String, Option<Vec<u8>>)> = (0..ENTRIES_PER_RUN)
304 .map(|j| (key(r * ENTRIES_PER_RUN + j), Some(VALUE.to_vec())))
305 .collect();
306 TieredRun::new(r as u64, entries)
307 })
308 .collect()
309 }
310
311 let planner = TieredCompactionPlanner::new(2);
312 let sw = sweep("tiered-compaction/merge", |n| {
316 let template = runs(n);
317 bulk_each(
318 || TieredManifest {
319 levels: vec![template.clone()],
320 },
321 |m| planner.merge(m, 0, 9_999),
322 true,
323 )
324 });
325 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
326
327 let template = runs(CANON_N);
328 let mut p99 = BTreeMap::new();
329 p99.insert(
330 "merge".to_string(),
331 bulk_each(
332 || TieredManifest {
333 levels: vec![template.clone()],
334 },
335 |m| planner.merge(m, 0, 9_999),
336 false,
337 ),
338 );
339 let planned = TieredManifest {
340 levels: vec![template],
341 };
342 p99.insert(
343 "plan".to_string(),
344 keyed(|_| _ = black_box(planner.pick_level(&planned)), false),
345 );
346 manifest.set_feature("tiered-compaction", cat, &p99, &reason);
347}
348
349#[cfg(feature = "leveled-compaction")]
352fn feature_leveled(manifest: &mut SubMsFeatureManifest, base_p50: u64) {
353 use subms_lsm_tree::{LeveledCompactionPlanner, LeveledManifest, LeveledRun};
354
355 fn halves(n: usize) -> (Vec<LeveledRun>, Vec<LeveledRun>) {
360 let per_level = n / 2;
361 let build = |parity: usize| -> Vec<LeveledRun> {
362 (0..per_level.div_ceil(ENTRIES_PER_RUN))
363 .map(|r| {
364 let entries: Vec<(String, Option<Vec<u8>>)> = (0..ENTRIES_PER_RUN)
365 .map(|j| {
366 let idx = 2 * (r * ENTRIES_PER_RUN + j) + parity;
367 (key(idx), Some(VALUE.to_vec()))
368 })
369 .collect();
370 LeveledRun::new((r * 2 + parity) as u64, entries)
371 })
372 .collect()
373 };
374 (build(0), build(1))
375 }
376
377 let planner = LeveledCompactionPlanner::new(64_000, 10, 4);
378 let sw = sweep("leveled-compaction/compact", |n| {
381 let (l0, l1) = halves(n);
382 bulk_each(
383 || LeveledManifest {
384 levels: vec![l0.clone(), l1.clone()],
385 },
386 |m| planner.compact(m, 0, 9_999),
387 true,
388 )
389 });
390 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
391
392 let (l0, l1) = halves(CANON_N);
393 let mut p99 = BTreeMap::new();
394 p99.insert(
395 "compact".to_string(),
396 bulk_each(
397 || LeveledManifest {
398 levels: vec![l0.clone(), l1.clone()],
399 },
400 |m| planner.compact(m, 0, 9_999),
401 false,
402 ),
403 );
404 let planned = LeveledManifest {
405 levels: vec![l0, l1],
406 };
407 p99.insert(
408 "plan".to_string(),
409 keyed(|_| _ = black_box(planner.pick_level(&planned)), false),
410 );
411 manifest.set_feature("leveled-compaction", cat, &p99, &reason);
412}
413
414#[cfg(feature = "snapshot")]
417fn feature_snapshot(manifest: &mut SubMsFeatureManifest, base_p50: u64) {
418 use subms_lsm_tree::{SnapshotManager, SnapshotManifest};
419
420 fn manager(n: usize) -> SnapshotManager {
421 let ids: Vec<u64> = (0..(n / KEYS_PER_SSTABLE).max(1) as u64).collect();
422 SnapshotManager::with_initial(SnapshotManifest::new(ids))
423 }
424
425 let sw = sweep("snapshot/snapshot", |n| {
431 let mgr = manager(n);
432 keyed(|_| _ = black_box(mgr.snapshot()), true)
433 });
434 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
435
436 let mgr = manager(CANON_N);
437 let mut p99 = BTreeMap::new();
438 p99.insert(
439 "snapshot".to_string(),
440 keyed(|_| _ = black_box(mgr.snapshot()), false),
441 );
442 let held = mgr.snapshot();
445 let ids = held.sstable_ids();
446 let targets: Vec<u64> = (0..OPS)
447 .map(|i| probe(i, ids.len().max(1) * 2) as u64)
448 .collect();
449 p99.insert(
450 "get_on_snapshot".to_string(),
451 keyed(
452 |i| {
453 let t = targets[i];
454 black_box(ids.iter().rev().any(|&id| id == t));
455 },
456 false,
457 ),
458 );
459 manifest.set_feature("snapshot", cat, &p99, &reason);
460}
461
462#[cfg(any(feature = "lz4", feature = "zstd", feature = "block-cache-integration"))]
467fn representative_block() -> Vec<u8> {
468 let pattern = b"key-0000042\x00present\x00value-payload-bytes-for-block|";
469 let mut out = Vec::with_capacity(BLOCK_BYTES + pattern.len());
470 while out.len() < BLOCK_BYTES {
471 out.extend_from_slice(pattern);
472 }
473 out.truncate(BLOCK_BYTES);
474 out
475}
476
477#[cfg(feature = "lz4")]
478fn feature_lz4(manifest: &mut SubMsFeatureManifest, base_p50: u64) {
479 use subms_lsm_tree::Lz4BlockCompressor;
480
481 let c = Lz4BlockCompressor::new();
482 let block = representative_block();
489 let sw = sweep("lz4/compress", |_| {
490 keyed(|_| _ = black_box(c.compress(&block)), true)
491 });
492 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
493
494 let encoded = c.compress(&block);
495 let mut p99 = BTreeMap::new();
496 p99.insert(
497 "compress_block".to_string(),
498 keyed(|_| _ = black_box(c.compress(&block)), false),
499 );
500 p99.insert(
501 "decompress_block".to_string(),
502 keyed(
503 |_| _ = black_box(c.decompress(&encoded).expect("lz4 decode")),
504 false,
505 ),
506 );
507 manifest.set_feature("lz4", cat, &p99, &reason);
508}
509
510#[cfg(feature = "zstd")]
511fn feature_zstd(manifest: &mut SubMsFeatureManifest, base_p50: u64) {
512 use subms_lsm_tree::ZstdBlockCompressor;
513
514 let c = ZstdBlockCompressor::new();
515 let block = representative_block();
516 let sw = sweep("zstd/compress", |_| {
517 keyed(
518 |_| _ = black_box(c.compress(&block).expect("zstd encode")),
519 true,
520 )
521 });
522 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
523
524 let encoded = c.compress(&block).expect("zstd encode");
525 let mut p99 = BTreeMap::new();
526 p99.insert(
527 "compress_block".to_string(),
528 keyed(
529 |_| _ = black_box(c.compress(&block).expect("zstd encode")),
530 false,
531 ),
532 );
533 p99.insert(
534 "decompress_block".to_string(),
535 keyed(
536 |_| _ = black_box(c.decompress(&encoded).expect("zstd decode")),
537 false,
538 ),
539 );
540 manifest.set_feature("zstd", cat, &p99, &reason);
541}
542
543#[cfg(feature = "block-cache-integration")]
546fn feature_block_cache(manifest: &mut SubMsFeatureManifest, base_p50: u64) {
547 use std::sync::Arc;
548 use subms_lsm_tree::{Block, BlockCache, BlockKey, LruBlockCache};
549
550 fn filled(n: usize) -> (LruBlockCache, usize) {
556 let cap = (n / KEYS_PER_BLOCK).max(64);
557 let cache = LruBlockCache::new(cap);
558 let block: Block = Arc::from(representative_block().into_boxed_slice());
559 for i in 0..cap as u64 {
560 cache.put(BlockKey::new(i % 8, i * BLOCK_BYTES as u64), block.clone());
561 }
562 (cache, cap)
563 }
564
565 let sw = sweep("block-cache-integration/get_cached", |n| {
566 let (cache, cap) = filled(n);
567 let keys: Vec<BlockKey> = (0..OPS)
568 .map(|i| {
569 let k = probe(i, cap) as u64;
570 BlockKey::new(k % 8, k * BLOCK_BYTES as u64)
571 })
572 .collect();
573 keyed(|i| _ = black_box(cache.get(&keys[i])), true)
574 });
575 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
576
577 let (cache, cap) = filled(CANON_N);
578 let hits: Vec<BlockKey> = (0..OPS)
579 .map(|i| {
580 let k = probe(i, cap) as u64;
581 BlockKey::new(k % 8, k * BLOCK_BYTES as u64)
582 })
583 .collect();
584 let misses: Vec<BlockKey> = (0..OPS)
585 .map(|i| BlockKey::new(999, probe(i, cap) as u64))
586 .collect();
587 let mut p99 = BTreeMap::new();
588 p99.insert(
589 "get_cached".to_string(),
590 keyed(|i| _ = black_box(cache.get(&hits[i])), false),
591 );
592 p99.insert(
593 "get_miss".to_string(),
594 keyed(|i| _ = black_box(cache.get(&misses[i])), false),
595 );
596 manifest.set_feature("block-cache-integration", cat, &p99, &reason);
597}
598
599struct TempDir {
601 path: PathBuf,
602}
603
604impl TempDir {
605 fn new(label: &str) -> Self {
606 let path = std::env::temp_dir().join(format!("{}-{}", label, std::process::id()));
607 let _ = std::fs::remove_dir_all(&path);
608 std::fs::create_dir_all(&path).expect("create temp dir");
609 Self { path }
610 }
611 fn path(&self) -> &Path {
612 &self.path
613 }
614}
615
616impl Drop for TempDir {
617 fn drop(&mut self) {
618 let _ = std::fs::remove_dir_all(&self.path);
619 }
620}