1use std::ops::RangeInclusive;
30
31use crate::shard::ShardBits;
32
33pub trait Compactable {
36 fn range(&self) -> RangeInclusive<u64>;
38
39 fn size(&self) -> u64;
41
42 fn is_bottom(&self) -> bool;
44
45 fn is_fresh(&self) -> bool;
47
48 fn entry_count(&self) -> u64;
50
51 fn tombstone_count(&self) -> u64;
53}
54
55#[derive(Clone, Copy, Debug)]
57pub struct CompactConfig {
58 pub max_space_amplification_percent: u16,
62
63 pub min_bottom_merge_bytes: u64,
67
68 pub max_files_above_bottom: usize,
71
72 pub size_ratio_percent: u16,
77
78 pub rewrite_per_fresh_byte: f32,
83
84 pub max_merge_jobs: usize,
88}
89
90impl Default for CompactConfig {
91 fn default() -> Self {
92 Self {
93 max_space_amplification_percent: 50,
94 min_bottom_merge_bytes: 1024 * 1024,
95 max_files_above_bottom: 6,
96 rewrite_per_fresh_byte: 3.0,
97 size_ratio_percent: 100,
98 max_merge_jobs: 8,
99 }
100 }
101}
102
103impl CompactConfig {
104 pub fn full() -> Self {
106 Self {
107 max_space_amplification_percent: 0,
108 min_bottom_merge_bytes: 0,
109 max_files_above_bottom: usize::MAX,
111 rewrite_per_fresh_byte: f32::INFINITY,
112 size_ratio_percent: u16::MAX,
113 max_merge_jobs: usize::MAX,
114 }
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct MergeJob {
121 pub members: Vec<usize>,
123 pub bottom: bool,
125}
126
127struct Candidate {
129 job: MergeJob,
130 priority: f32,
133}
134
135fn components<T: Compactable>(compactables: &[T], shard_bits: ShardBits) -> Vec<Vec<usize>> {
143 let mut spans = compactables
144 .iter()
145 .enumerate()
146 .map(|(index, c)| {
147 let range = c.range();
148 let shard_start = shard_bits.shard_of(*range.start());
149 let shard_end = shard_bits.shard_of(*range.end());
150 (shard_start, shard_end, index)
151 })
152 .collect::<Vec<_>>();
153 spans.sort_unstable();
154 let mut result: Vec<(u32, Vec<usize>)> = Vec::new();
155 for (shard_start, shard_end, index) in spans {
156 match result.last_mut() {
157 Some((component_end, members)) if shard_start <= *component_end => {
158 *component_end = (*component_end).max(shard_end);
161 members.push(index);
162 }
163 _ => result.push((shard_end, vec![index])),
164 }
165 }
166 result
167 .into_iter()
168 .map(|(_, mut members)| {
169 members.sort_unstable();
170 members
171 })
172 .collect()
173}
174
175fn intermediate_candidate<T: Compactable>(
187 compactables: &[T],
188 above: Vec<usize>,
189 above_is_all_shard_files: bool,
190 config: &CompactConfig,
191) -> Candidate {
192 let priority = above.len() as f32 / config.max_files_above_bottom.max(1) as f32;
193 let mut taken = 0u64;
194 let mut first = above.len();
195 let min_to_take = (above.len() + 1)
197 .saturating_sub(config.max_files_above_bottom.max(1))
198 .max(2);
199 while first > 0 {
200 let size = compactables[above[first - 1]].size();
201 let taken_files = above.len() - first;
202 if taken_files >= min_to_take {
205 let limit = taken.saturating_mul(100 + u64::from(config.size_ratio_percent));
207 if size.saturating_mul(100) > limit {
208 break;
209 }
210 }
211 taken += size;
212 first -= 1;
213 }
214 let mut above = above;
215 above.drain(..first);
216 Candidate {
217 priority,
218 job: MergeJob {
219 members: above,
220 bottom: above_is_all_shard_files && first == 0,
223 },
224 }
225}
226
227fn plan_family<T: Compactable>(
229 compactables: &[T],
230 shard_bits: ShardBits,
231 config: &CompactConfig,
232) -> Vec<Candidate> {
233 let fresh_bytes = compactables
234 .iter()
235 .filter(|c| c.is_fresh())
236 .map(|c| c.size())
237 .sum::<u64>();
238
239 let mut bottom_candidates = Vec::new();
240 let mut intermediate_candidates = Vec::new();
241 for members in components(compactables, shard_bits) {
242 let (bottom, above): (Vec<usize>, Vec<usize>) =
243 members.iter().partition(|&&i| compactables[i].is_bottom());
244 if above.is_empty() {
245 continue;
246 }
247 debug_assert!(
251 bottom.iter().all(|&b| above.iter().all(|&a| {
252 let (b_range, a_range) = (compactables[b].range(), compactables[a].range());
253 b < a || b_range.end() < a_range.start() || a_range.end() < b_range.start()
254 })),
255 "bottom files must be older than the files above them that they overlap"
256 );
257 let bottom_bytes = bottom.iter().map(|&i| compactables[i].size()).sum::<u64>();
260 let above_bytes = above.iter().map(|&i| compactables[i].size()).sum::<u64>();
261 let bottom_entries = bottom
267 .iter()
268 .map(|&i| compactables[i].entry_count())
269 .sum::<u64>();
270 let above_tombstones = above
271 .iter()
272 .map(|&i| compactables[i].tombstone_count())
273 .sum::<u64>();
274 let deleted_bytes = bottom_bytes
275 .checked_div(bottom_entries)
276 .unwrap_or(0)
277 .saturating_mul(above_tombstones);
278 let reclaimable_bytes = above_bytes.saturating_add(deleted_bytes);
281 let limit = f64::from(config.max_space_amplification_percent) / 100.0;
282 let amplification = if bottom_bytes == 0 {
283 f64::INFINITY
284 } else {
285 reclaimable_bytes as f64 / bottom_bytes as f64
286 };
287 if amplification > limit && reclaimable_bytes >= config.min_bottom_merge_bytes {
288 let candidate = Candidate {
289 job: MergeJob {
290 members,
291 bottom: true,
292 },
293 priority: if limit > 0.0 {
294 (amplification / limit) as f32
295 } else {
296 f32::INFINITY
297 },
298 };
299 let overdue = limit > 0.0
302 && (amplification > 2.0 * limit || above.len() > 2 * config.max_files_above_bottom);
303 bottom_candidates.push((
304 candidate,
305 bottom_bytes + above_bytes,
306 above,
307 overdue,
308 bottom.is_empty(),
309 ));
310 } else if above.len() > config.max_files_above_bottom.max(1) {
311 intermediate_candidates.push(intermediate_candidate(
312 compactables,
313 above,
314 bottom.is_empty(),
315 config,
316 ));
317 }
318 }
319
320 bottom_candidates.sort_by(|a, b| b.0.priority.total_cmp(&a.0.priority));
323 let mut rewrite_target = if config.rewrite_per_fresh_byte.is_finite() {
324 (f64::from(config.rewrite_per_fresh_byte) * fresh_bytes as f64) as u64
326 } else {
327 u64::MAX
329 };
330 let mut result = Vec::new();
331 for (candidate, cost, above, overdue, above_is_all_shard_files) in bottom_candidates {
332 if result.is_empty() || rewrite_target > 0 || overdue {
335 rewrite_target = rewrite_target.saturating_sub(cost);
336 result.push(candidate);
337 } else if above.len() > config.max_files_above_bottom.max(1) {
338 intermediate_candidates.push(intermediate_candidate(
341 compactables,
342 above,
343 above_is_all_shard_files,
344 config,
345 ));
346 }
347 }
348 intermediate_candidates.sort_by(|a, b| b.priority.total_cmp(&a.priority));
349 result.extend(intermediate_candidates);
350 result
351}
352
353pub fn plan_compaction<T: Compactable>(
360 families: &[(&[T], ShardBits)],
361 config: &CompactConfig,
362) -> Vec<Vec<MergeJob>> {
363 let mut candidates = families
364 .iter()
365 .enumerate()
366 .flat_map(|(family, (compactables, shard_bits))| {
367 plan_family(compactables, *shard_bits, config)
368 .into_iter()
369 .enumerate()
370 .map(move |(order, candidate)| (family, order, candidate))
371 })
372 .collect::<Vec<_>>();
373 candidates.sort_by(|a, b| {
377 b.2.priority
378 .total_cmp(&a.2.priority)
379 .then(b.2.job.bottom.cmp(&a.2.job.bottom))
380 .then(a.1.cmp(&b.1))
381 .then(a.0.cmp(&b.0))
382 });
383 let mut result = vec![Vec::new(); families.len()];
384 for (family, _, candidate) in candidates.into_iter().take(config.max_merge_jobs) {
385 result[family].push(candidate.job);
386 }
387 for jobs in &mut result {
388 jobs.sort_by_key(|job| job.members[0]);
389 }
390 result
391}
392
393#[cfg(test)]
394mod tests {
395 use std::collections::{BTreeMap, BTreeSet};
396
397 use rand::{RngExt, SeedableRng, seq::SliceRandom};
398
399 use super::*;
400
401 #[derive(Clone, Debug)]
402 struct File {
403 range: RangeInclusive<u64>,
404 size: u64,
405 bottom: bool,
406 fresh: bool,
407 }
408
409 impl Compactable for File {
410 fn range(&self) -> RangeInclusive<u64> {
411 self.range.clone()
412 }
413
414 fn size(&self) -> u64 {
415 self.size
416 }
417
418 fn is_bottom(&self) -> bool {
419 self.bottom
420 }
421
422 fn is_fresh(&self) -> bool {
423 self.fresh
424 }
425
426 fn entry_count(&self) -> u64 {
427 0
428 }
429
430 fn tombstone_count(&self) -> u64 {
431 0
432 }
433 }
434
435 fn file(shard: u32, shard_bits: u8, size: u64, bottom: bool, fresh: bool) -> File {
436 File {
437 range: ShardBits::new(shard_bits).range(shard),
438 size,
439 bottom,
440 fresh,
441 }
442 }
443
444 fn test_config() -> CompactConfig {
447 CompactConfig {
448 min_bottom_merge_bytes: 0,
449 max_files_above_bottom: 4,
450 ..Default::default()
451 }
452 }
453
454 fn plan(files: &[File], shard_bits: u8, config: &CompactConfig) -> Vec<(Vec<usize>, bool)> {
455 plan_compaction(&[(files, ShardBits::new(shard_bits))], config)
456 .remove(0)
457 .into_iter()
458 .map(|job| (job.members, job.bottom))
459 .collect()
460 }
461
462 #[test]
463 fn test_shard_without_bottom_run_gets_one() {
464 let files = [file(0, 0, 100, false, true), file(0, 0, 100, false, true)];
465 assert_eq!(plan(&files, 0, &test_config()), vec![(vec![0, 1], true)]);
466 }
467
468 #[test]
469 fn test_bottom_merge_by_space_amplification() {
470 let config = test_config();
471 let files = [file(0, 0, 1000, true, false), file(0, 0, 400, false, true)];
473 assert_eq!(plan(&files, 0, &config), vec![]);
474 let files = [file(0, 0, 1000, true, false), file(0, 0, 600, false, true)];
476 assert_eq!(plan(&files, 0, &config), vec![(vec![0, 1], true)]);
477 }
478
479 #[test]
480 fn test_intermediate_merge_by_file_count() {
481 let mut files = vec![file(0, 0, 1000, true, false)];
482 files.extend((0..5).map(|_| file(0, 0, 10, false, true)));
483 assert_eq!(
484 plan(&files, 0, &test_config()),
485 vec![(vec![1, 2, 3, 4, 5], false)]
486 );
487 files.pop();
488 assert_eq!(plan(&files, 0, &test_config()), vec![]);
489 }
490
491 #[test]
492 fn test_intermediate_merge_takes_the_newest_files_of_similar_size() {
493 let mut files = vec![
496 file(0, 0, 10_000, true, false),
497 file(0, 0, 1000, false, true),
498 ];
499 files.extend((0..5).map(|_| file(0, 0, 10, false, true)));
500 assert_eq!(
501 plan(&files, 0, &test_config()),
502 vec![(vec![2, 3, 4, 5, 6], false)]
503 );
504 let mut files = vec![file(0, 0, 100_000, true, false)];
507 files.extend([10_000, 1000, 100, 10, 1].map(|size| file(0, 0, size, false, true)));
508 assert_eq!(plan(&files, 0, &test_config()), vec![(vec![4, 5], false)]);
509 let mut files = vec![file(0, 0, 10_000, true, false)];
512 files.extend([500, 40, 20, 10, 10].map(|size| file(0, 0, size, false, true)));
513 assert_eq!(
514 plan(&files, 0, &test_config()),
515 vec![(vec![2, 3, 4, 5], false)]
516 );
517 }
518
519 #[test]
520 fn test_budget_spreads_bottom_merges() {
521 let mut files = (0..4)
524 .map(|shard| file(shard, 2, 1000, true, false))
525 .collect::<Vec<_>>();
526 files.extend((0..4).map(|shard| file(shard, 2, 600 + u64::from(shard), false, true)));
527 let config = CompactConfig {
528 rewrite_per_fresh_byte: 0.1,
529 ..test_config()
530 };
531 assert_eq!(plan(&files, 2, &config), vec![(vec![3, 7], true)]);
532 assert_eq!(
534 plan(&files, 2, &test_config()),
535 vec![
536 (vec![0, 4], true),
537 (vec![1, 5], true),
538 (vec![2, 6], true),
539 (vec![3, 7], true)
540 ]
541 );
542 }
543
544 #[test]
545 fn test_full_config_merges_every_shard_without_fresh_bytes() {
546 let mut files = (0..4)
549 .map(|shard| file(shard, 2, 1000, true, false))
550 .collect::<Vec<_>>();
551 files.extend((0..4).map(|shard| file(shard, 2, 10, false, false)));
552 assert_eq!(plan(&files, 2, &CompactConfig::full()).len(), 4);
553 let config = CompactConfig {
556 rewrite_per_fresh_byte: 3.0,
557 ..CompactConfig::full()
558 };
559 assert_eq!(plan(&files, 2, &config).len(), 1);
560 }
561
562 #[test]
563 fn test_quota_stops_when_exactly_reached() {
564 let mut files = (0..4)
568 .map(|shard| file(shard, 2, 360, true, false))
569 .collect::<Vec<_>>();
570 files.extend((0..4).map(|shard| file(shard, 2, 140, false, false)));
571 files.extend((0..4).map(|shard| file(shard, 2, 100, false, true)));
572 assert_eq!(plan(&files, 2, &test_config()).len(), 2);
573 }
574
575 #[test]
576 fn test_shards_far_behind_are_merged_over_the_quota() {
577 let mut files = (0..4)
581 .map(|shard| file(shard, 2, 1000, true, false))
582 .collect::<Vec<_>>();
583 files.extend(
584 [(0, 600), (1, 700), (2, 1100)].map(|(shard, size)| file(shard, 2, size, false, false)),
585 );
586 files.extend((0..9).map(|_| file(3, 2, 600 / 9 + 1, false, false)));
587 let config = CompactConfig {
588 rewrite_per_fresh_byte: 0.0,
589 ..test_config()
590 };
591 let bottom_merged = plan(&files, 2, &config)
592 .into_iter()
593 .filter(|(_, bottom)| *bottom)
594 .map(|(members, _)| members[0])
595 .collect::<Vec<_>>();
596 assert_eq!(bottom_merged, vec![2, 3]);
599 }
600
601 #[test]
602 fn test_byte_floor_skips_bottom_merges_of_tiny_shards() {
603 let mut files = vec![file(0, 0, 50, true, false)];
606 files.push(file(0, 0, 50, false, true));
607 let config = CompactConfig {
608 min_bottom_merge_bytes: 1000,
609 ..test_config()
610 };
611 assert_eq!(plan(&files, 0, &config), vec![]);
612 files.extend((0..4).map(|_| file(0, 0, 50, false, true)));
613 assert_eq!(plan(&files, 0, &config), vec![(vec![1, 2, 3, 4, 5], false)]);
614 }
615
616 #[test]
617 fn test_intermediate_merges_are_not_starved() {
618 let mut many_files = vec![file(0, 0, 1000, true, false)];
621 many_files.extend((0..8).map(|_| file(0, 0, 1, false, true)));
622 let amplified = [file(0, 0, 1000, true, false), file(0, 0, 600, false, true)];
623 let config = CompactConfig {
624 max_merge_jobs: 1,
625 ..test_config()
626 };
627 let jobs = plan_compaction(
628 &[
629 (&many_files[..], ShardBits::new(0)),
630 (&lified[..], ShardBits::new(0)),
631 ],
632 &config,
633 );
634 assert_eq!(jobs[0].len(), 1);
635 assert!(!jobs[0][0].bottom);
636 assert!(jobs[1].is_empty());
637 }
638
639 #[test]
640 fn test_coarse_files_merge_the_shards_they_span() {
641 let files = [
643 file(0, 1, 1000, true, false),
644 file(1, 1, 1000, true, false),
645 file(0, 2, 600, false, true),
646 file(1, 2, 10, false, true),
647 file(3, 2, 10, false, true),
648 ];
649 assert_eq!(plan(&files, 2, &test_config()), vec![(vec![0, 2, 3], true)]);
650 }
651
652 #[test]
653 fn test_merging_all_files_of_a_shard_without_bottom_run_makes_a_bottom_run() {
654 let files = (0..5)
657 .map(|_| file(0, 0, 10, false, true))
658 .collect::<Vec<_>>();
659 let config = CompactConfig {
660 min_bottom_merge_bytes: 1000,
661 ..test_config()
662 };
663 assert_eq!(plan(&files, 0, &config), vec![(vec![0, 1, 2, 3, 4], true)]);
664 let mut files = vec![file(0, 0, 10, true, false)];
666 files.extend((0..5).map(|_| file(0, 0, 10, false, true)));
667 assert_eq!(plan(&files, 0, &config), vec![(vec![1, 2, 3, 4, 5], false)]);
668 }
669
670 #[test]
671 fn test_merge_job_limit_across_families() {
672 let a = [file(0, 0, 100, false, true), file(0, 0, 100, false, true)];
673 let b = [file(0, 0, 100, false, true), file(0, 0, 100, false, true)];
674 let config = CompactConfig {
675 max_merge_jobs: 1,
676 ..test_config()
677 };
678 let jobs = plan_compaction(
679 &[(&a[..], ShardBits::new(0)), (&b[..], ShardBits::new(0))],
680 &config,
681 );
682 assert_eq!(jobs.iter().map(Vec::len).sum::<usize>(), 1);
683 }
684
685 struct Container {
687 keys: Vec<(u64, bool)>,
689 bottom: bool,
690 fresh: bool,
691 }
692
693 impl Compactable for Container {
694 fn range(&self) -> RangeInclusive<u64> {
695 self.keys[0].0..=self.keys.last().unwrap().0
696 }
697
698 fn size(&self) -> u64 {
699 self.keys.len() as u64
700 }
701
702 fn is_bottom(&self) -> bool {
703 self.bottom
704 }
705
706 fn is_fresh(&self) -> bool {
707 self.fresh
708 }
709
710 fn entry_count(&self) -> u64 {
711 self.keys.len() as u64
712 }
713
714 fn tombstone_count(&self) -> u64 {
715 self.keys.iter().filter(|(_, tombstone)| *tombstone).count() as u64
716 }
717 }
718
719 fn split_by_shard(
721 entries: Vec<(u64, bool)>,
722 shard_bits: ShardBits,
723 bottom: bool,
724 fresh: bool,
725 ) -> Vec<Container> {
726 let mut by_shard = BTreeMap::<u32, Vec<(u64, bool)>>::new();
727 for entry in entries {
728 by_shard
729 .entry(shard_bits.shard_of(entry.0))
730 .or_default()
731 .push(entry);
732 }
733 by_shard
734 .into_values()
735 .map(|keys| Container {
736 keys,
737 bottom,
738 fresh,
739 })
740 .collect()
741 }
742
743 fn run_jobs(
745 containers: &mut Vec<Container>,
746 jobs: Vec<MergeJob>,
747 shard_bits: ShardBits,
748 ) -> u64 {
749 let mut written = 0;
750 let mut outputs = Vec::new();
751 let mut merged = BTreeSet::new();
752 for job in jobs {
753 let oldest = job.members[0];
754 let mut entries = job
756 .members
757 .iter()
758 .rev()
759 .flat_map(|&i| containers[i].keys.iter().copied())
760 .collect::<Vec<_>>();
761 entries.sort_by_key(|(key, _)| *key);
762 entries.dedup_by_key(|(key, _)| *key);
763 entries.retain(|&(key, tombstone)| {
765 !tombstone
766 || containers[..oldest]
767 .iter()
768 .any(|c| c.keys.binary_search_by_key(&key, |(k, _)| *k).is_ok())
769 });
770 written += entries.len() as u64;
771 outputs.extend(split_by_shard(entries, shard_bits, job.bottom, false));
772 merged.extend(job.members);
773 }
774 let mut i = 0;
775 containers.retain(|_| {
776 i += 1;
777 !merged.contains(&(i - 1))
778 });
779 containers.extend(outputs);
780 written
781 }
782
783 const KEY_COUNT: u64 = 20_000;
784 const KEY_SCALE: u64 = u64::MAX / KEY_COUNT;
786
787 struct SimulationResult {
788 compactions_with_bottom_merges: usize,
789 written: u64,
791 rewritten: u64,
792 max_space_amplification: f64,
795 max_files_per_shard: usize,
797 }
798
799 fn compact(
803 containers: &mut Vec<Container>,
804 config: &CompactConfig,
805 shard_bits: ShardBits,
806 ) -> (u64, bool) {
807 let jobs = plan_compaction(&[(&containers[..], shard_bits)], config).remove(0);
808 let bottom = jobs.iter().any(|job| job.bottom);
809 (run_jobs(containers, jobs, shard_bits), bottom)
810 }
811
812 fn simulate(
815 config: &CompactConfig,
816 shard_bits: ShardBits,
817 iterations: usize,
818 small_commits: usize,
819 ) -> SimulationResult {
820 let mut rnd = rand::rngs::SmallRng::from_seed([0; 32]);
821 let mut live = (0..KEY_COUNT).map(|k| k * KEY_SCALE).collect::<Vec<_>>();
822 live.shuffle(&mut rnd);
823 let mut containers = split_by_shard(
824 {
825 let mut all = live.iter().map(|&k| (k, false)).collect::<Vec<_>>();
826 all.sort_unstable();
827 all
828 },
829 shard_bits,
830 false,
831 true,
832 );
833 let mut written = KEY_COUNT;
834 let mut rewritten = 0;
835 let mut result = SimulationResult {
836 compactions_with_bottom_merges: 0,
837 written: 0,
838 rewritten: 0,
839 max_space_amplification: 0.0,
840 max_files_per_shard: 0,
841 };
842 let hot_count = KEY_COUNT as usize / 20;
843 for iteration in 0..iterations {
844 let (entries, bottom) = compact(&mut containers, config, shard_bits);
845 rewritten += entries;
846 if bottom {
847 result.compactions_with_bottom_merges += 1;
848 }
849 if iteration >= iterations / 4 {
850 let stored = containers.iter().map(|c| c.keys.len()).sum::<usize>();
851 result.max_space_amplification = result
852 .max_space_amplification
853 .max(stored as f64 / live.len() as f64);
854 for shard in 0..shard_bits.count() {
855 let range = shard_bits.range(shard);
856 let files = containers
857 .iter()
858 .filter(|c| {
859 let r = c.range();
860 r.start() <= range.end() && range.start() <= r.end()
861 })
862 .count();
863 result.max_files_per_shard = result.max_files_per_shard.max(files);
864 }
865 }
866
867 let mut commit = live[..hot_count]
869 .iter()
870 .map(|&k| (k, false))
871 .collect::<Vec<_>>();
872 for _ in 0..hot_count / 10 {
873 let i = rnd.random_range(0..hot_count);
874 let j = rnd.random_range(hot_count..live.len());
875 live.swap(i, j);
876 }
877 for _ in 0..rnd.random_range(0..KEY_COUNT as usize / 200) {
880 let i = rnd.random_range(hot_count..live.len());
881 commit.push((live[i], true));
882 let new_key = rnd.random::<u64>();
883 live[i] = new_key;
884 commit.push((new_key, false));
885 }
886 commit.sort_unstable();
887 written += commit.len() as u64;
888 containers.extend(split_by_shard(commit, shard_bits, false, true));
889
890 for _ in 0..small_commits {
891 rewritten += compact(&mut containers, config, shard_bits).0;
892 let mut commit = (0..20)
893 .map(|_| (live[rnd.random_range(0..hot_count)], false))
894 .collect::<Vec<_>>();
895 commit.sort_unstable();
896 commit.dedup();
897 written += commit.len() as u64;
898 containers.extend(split_by_shard(commit, shard_bits, false, true));
899 }
900 }
901 result.written = written;
902 result.rewritten = rewritten;
903 result
904 }
905
906 fn simulation_config(
907 max_space_amplification_percent: u16,
908 rewrite_per_fresh_byte: f32,
909 ) -> CompactConfig {
910 CompactConfig {
911 max_space_amplification_percent,
912 rewrite_per_fresh_byte,
913 max_merge_jobs: usize::MAX,
914 ..test_config()
915 }
916 }
917
918 #[cfg(not(miri))]
920 #[test]
921 fn simulate_compactions() {
922 let config = simulation_config(50, 2.0);
923 let result = simulate(&config, ShardBits::new(3), 200, 0);
924 let write_amplification = result.rewritten as f64 / result.written as f64;
925 println!(
926 "space amp {:.2}, write amp {write_amplification:.2}, files per shard {}, compactions \
927 with bottom merges {}",
928 result.max_space_amplification,
929 result.max_files_per_shard,
930 result.compactions_with_bottom_merges
931 );
932 assert!(result.max_space_amplification < 1.6);
935 assert!(result.max_files_per_shard <= config.max_files_above_bottom + 1);
936 assert!(write_amplification < 2.0);
937 }
938
939 #[cfg(not(miri))]
941 #[test]
942 fn simulate_small_commits() {
943 let result = |size_ratio_percent| {
946 let config = CompactConfig {
947 size_ratio_percent,
948 ..simulation_config(50, 2.0)
949 };
950 let result = simulate(&config, ShardBits::new(3), 60, 10);
951 let write_amplification = result.rewritten as f64 / result.written as f64;
952 println!(
953 "size ratio {size_ratio_percent}%: write amp {write_amplification:.2}, files per \
954 shard {}",
955 result.max_files_per_shard
956 );
957 (write_amplification, result.max_files_per_shard)
958 };
959 let (merge_all, merge_all_files) = result(u16::MAX);
960 let (similar_size, similar_size_files) = result(100);
961 assert!(
962 similar_size < merge_all * 0.8,
963 "{similar_size:.2} vs {merge_all:.2}"
964 );
965 assert!(similar_size_files <= merge_all_files + 1);
966 }
967
968 #[test]
971 #[ignore]
972 fn sweep_space_amplification() {
973 println!("space amp threshold % | rewrite factor | space amp | write amp | files/shard");
974 for threshold in [25, 50, 100, 200] {
975 for factor in [1.0, 2.0, 4.0, f32::INFINITY] {
976 let result = simulate(
977 &simulation_config(threshold, factor),
978 ShardBits::new(3),
979 300,
980 0,
981 );
982 println!(
983 "{threshold:>21} | {factor:>14} | {:>9.2} | {:>9.2} | {:>11}",
984 result.max_space_amplification,
985 result.rewritten as f64 / result.written as f64,
986 result.max_files_per_shard
987 );
988 }
989 }
990 }
991}