value_reads.rs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. use std::{
  2. hint::black_box,
  3. io::{Cursor, Read},
  4. };
  5. #[cfg(feature = "bench-allocations")]
  6. use std::fmt::Write as _;
  7. #[cfg(not(feature = "bench-allocations"))]
  8. use std::time::Duration;
  9. #[cfg(not(feature = "bench-allocations"))]
  10. use criterion::{BenchmarkId, SamplingMode, Throughput};
  11. use criterion::{Criterion, criterion_group, criterion_main};
  12. use kvdb_overlay::{Database, DatabaseOverlay, Value};
  13. #[cfg(feature = "fjall-backend")]
  14. const BACKEND: &str = "fjall";
  15. #[cfg(feature = "sled-backend")]
  16. const BACKEND: &str = "sled";
  17. const TREE: &str = "value_reads";
  18. const KEY: &[u8] = b"value";
  19. const SIZES: [(&str, usize); 7] = [
  20. ("64B", 64),
  21. ("4KiB", 4096),
  22. ("1MiB", 1024 * 1024),
  23. ("32MiB", 32 * 1024 * 1024),
  24. ("128MiB", 128 * 1024 * 1024),
  25. ("256MiB", 256 * 1024 * 1024),
  26. ("512MiB", 512 * 1024 * 1024),
  27. ];
  28. // Both decoders use the same generic Read implementation, with no decode heap allocation.
  29. fn decode(mut reader: impl Read, words: usize) -> u64 {
  30. let mut checksum = 0u64;
  31. let mut word = [0u8; 8];
  32. for _ in 0..words {
  33. reader.read_exact(&mut word).unwrap();
  34. checksum = checksum.wrapping_add(u64::from_le_bytes(word));
  35. }
  36. checksum
  37. }
  38. type ReadValue = fn(&DatabaseOverlay, usize) -> u64;
  39. const READERS: [(&str, ReadValue); 4] = [
  40. ("vec_cursor", |overlay, words| {
  41. let value = overlay.get(TREE, KEY).unwrap().unwrap().into_vec();
  42. decode(Cursor::new(value), words)
  43. }),
  44. ("vec_slice", |overlay, words| {
  45. let value = overlay.get(TREE, KEY).unwrap().unwrap().into_vec();
  46. decode(value.as_slice(), words)
  47. }),
  48. ("handle_slice", |overlay, words| {
  49. let value = overlay.get(TREE, KEY).unwrap().unwrap();
  50. decode(value.as_ref(), words)
  51. }),
  52. ("handle_cursor", |overlay, words| {
  53. let value = overlay.get(TREE, KEY).unwrap().unwrap();
  54. decode(Cursor::new(value), words)
  55. }),
  56. ];
  57. fn value_reads(_criterion: &mut Criterion) {
  58. let selection = std::env::var("VALUE_READ_SIZE").unwrap_or_else(|_| "all".into());
  59. assert!(
  60. selection == "all"
  61. || selection == "large"
  62. || SIZES.iter().any(|(name, _)| *name == selection),
  63. "VALUE_READ_SIZE must be all, large, 64B, 4KiB, 1MiB, 32MiB, 128MiB, 256MiB, or 512MiB"
  64. );
  65. #[cfg(feature = "bench-allocations")]
  66. let mut report = format!(
  67. "# Value Read Allocations ({BACKEND})\n\n\
  68. Generated by `VALUE_READ_SIZE={selection} cargo bench --bench value_reads --no-default-features \
  69. --features {BACKEND}-backend,bench-allocations`.\n\n\
  70. Warm repeated get + decode + drop; foreground thread only. \
  71. Totals are cumulative allocations, not live/peak memory or RSS. \
  72. Backend worker allocations are excluded. No Criterion timings in this run.\n\n\
  73. | backend | source | size_bytes | decode | approach | operations | allocations_total | bytes_total | allocations_per_op | bytes_per_op |\n\
  74. | --- | --- | ---: | --- | --- | ---: | ---: | ---: | ---: | ---: |\n"
  75. );
  76. for source in ["backend_fallback", "overlay_cache"] {
  77. for (size_name, size) in SIZES {
  78. let large = size >= 32 * 1024 * 1024;
  79. if selection != "all" && selection != size_name && !(selection == "large" && large) {
  80. continue;
  81. }
  82. // Bound fixed validation/allocation work for large payloads.
  83. let repeats = (100 * 1024 * 1024 / size).clamp(3, 100);
  84. // Build deterministic, non-uniform words and independent expected checksums.
  85. let mut payload = Vec::with_capacity(size);
  86. let mut expected_full = 0u64;
  87. let mut expected_header = 0u64;
  88. for index in 0..size / 8 {
  89. let word = (index as u64 + 1).wrapping_mul(0x9e3779b97f4a7c15);
  90. if index == 0 {
  91. expected_header = word;
  92. }
  93. expected_full = expected_full.wrapping_add(word);
  94. payload.extend_from_slice(&word.to_le_bytes());
  95. }
  96. // Keep the directory alive until all database/overlay handles are dropped.
  97. let directory = tempfile::tempdir().unwrap();
  98. let database = Database::open_default(directory.path()).unwrap();
  99. let tree = database
  100. .open_tree(
  101. TREE,
  102. #[cfg(feature = "fjall-backend")]
  103. || {
  104. let options = kvdb_overlay::fjall::KeyspaceCreateOptions::default();
  105. if large {
  106. // Keep the warm-handle experiment on the same storage path.
  107. options.max_memtable_size(1024 * 1024 * 1024)
  108. } else {
  109. options
  110. }
  111. },
  112. )
  113. .unwrap();
  114. tree.insert(KEY, &payload).unwrap();
  115. database.flush_default_mode().unwrap();
  116. let mut overlay = DatabaseOverlay::new(&database, vec![]).unwrap();
  117. overlay.open_tree_default(TREE, false).unwrap();
  118. if source == "overlay_cache" {
  119. overlay.insert(TREE, KEY, &payload).unwrap();
  120. }
  121. assert_eq!(
  122. matches!(overlay.get(TREE, KEY).unwrap().unwrap(), Value::Cached(_)),
  123. source == "overlay_cache"
  124. );
  125. drop(payload);
  126. #[cfg(feature = "fjall-backend")]
  127. let assert_resident = || {
  128. assert_eq!(tree.tree().sealed_memtable_count(), 0);
  129. assert_eq!(tree.tree().table_count(), 0);
  130. };
  131. #[cfg(not(feature = "bench-allocations"))]
  132. let mut group =
  133. _criterion.benchmark_group(format!("value_reads/{BACKEND}/{source}/{size_name}"));
  134. #[cfg(not(feature = "bench-allocations"))]
  135. {
  136. group.sample_size(25);
  137. group.measurement_time(Duration::from_secs(1));
  138. group.warm_up_time(Duration::from_millis(300));
  139. group.throughput(Throughput::Elements(1));
  140. if large {
  141. group.sampling_mode(SamplingMode::Flat);
  142. group.measurement_time(Duration::from_secs(3));
  143. }
  144. }
  145. for (decode_name, words, expected) in [
  146. ("header_8B", 1, expected_header),
  147. ("full_payload", size / 8, expected_full),
  148. ] {
  149. for (approach, read_value) in READERS {
  150. // Validate every approach and warm this exact path before measuring.
  151. for _ in 0..repeats {
  152. assert_eq!(read_value(black_box(&overlay), black_box(words)), expected);
  153. }
  154. #[cfg(feature = "fjall-backend")]
  155. assert_resident();
  156. #[cfg(feature = "bench-allocations")]
  157. {
  158. let allocations = allocation_counter::measure(|| {
  159. for _ in 0..repeats {
  160. black_box(read_value(black_box(&overlay), black_box(words)));
  161. }
  162. });
  163. writeln!(
  164. report,
  165. "| {BACKEND} | {source} | {size} | {decode_name} | {approach} | {repeats} | {} | {} | {:.2} | {:.2} |",
  166. allocations.count_total,
  167. allocations.bytes_total,
  168. allocations.count_total as f64 / repeats as f64,
  169. allocations.bytes_total as f64 / repeats as f64,
  170. ).unwrap();
  171. }
  172. #[cfg(not(feature = "bench-allocations"))]
  173. group.bench_function(BenchmarkId::new(decode_name, approach), |bencher| {
  174. bencher.iter(|| {
  175. // The owned Vec/handle is dropped inside read_value, before return.
  176. black_box(read_value(black_box(&overlay), black_box(words)))
  177. });
  178. });
  179. #[cfg(feature = "fjall-backend")]
  180. assert_resident();
  181. }
  182. }
  183. #[cfg(not(feature = "bench-allocations"))]
  184. group.finish();
  185. }
  186. }
  187. #[cfg(feature = "bench-allocations")]
  188. {
  189. let directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("target/criterion");
  190. std::fs::create_dir_all(&directory).unwrap();
  191. let suffix = if selection == "all" {
  192. String::new()
  193. } else {
  194. format!("-{selection}")
  195. };
  196. let path = directory.join(format!("value-allocations-{BACKEND}{suffix}.md"));
  197. std::fs::write(&path, &report).unwrap();
  198. print!("{report}");
  199. eprintln!("Allocation report: {}", path.display());
  200. }
  201. }
  202. criterion_group!(benches, value_reads);
  203. criterion_main!(benches);