1mod cover;
26mod fastcover;
27mod frequency;
28mod legacy;
29mod reservoir;
30mod suffix_array;
31
32use crate::bit_io::BitWriter;
33use crate::blocks::sequence_section::{
34 MAX_LITERAL_LENGTH_CODE, MAX_MATCH_LENGTH_CODE, MAX_OFFSET_CODE,
35};
36use crate::decoding::dictionary::MAGIC_NUM as DICT_MAGIC_NUM;
37use crate::decoding::sequence_section_decoder::{LL_MAX_LOG, ML_MAX_LOG, OF_MAX_LOG};
38use crate::dictionary::reservoir::create_sample;
39use crate::fse::fse_encoder::{self, build_table_from_symbol_counts};
40use crate::huff0::HuffmanTable as HuffmanDecoderTable;
41use crate::huff0::huff0_encoder::{HuffmanEncoder, HuffmanTable as HuffmanEncoderTable};
42use core::cmp::Reverse;
43use cover::*;
44pub use fastcover::{
45 DEFAULT_D_CANDIDATES, DEFAULT_F_CANDIDATES, DEFAULT_K_CANDIDATES, FastCoverParams,
46 FastCoverTuned,
47};
48pub use legacy::DEFAULT_SELECTIVITY;
49use std::{
50 boxed::Box,
51 collections::{BinaryHeap, HashMap},
52 format,
53 fs::{self, File},
54 io::{self, Read},
55 path::{Path, PathBuf},
56 vec,
61 vec::Vec,
62};
63
64const MAX_TRAINING_PREALLOC_BYTES: usize = 8 * 1024 * 1024;
65const MAX_HUFFMAN_STATS_BYTES: usize = 64 * 1024;
66
67pub const MIN_TRAINED_DICT_SIZE: usize = DICT_MAGIC_NUM.len() + 4 + 12 + 8;
75
76#[derive(Debug, Clone)]
78pub struct FastCoverOptions {
79 pub optimize: bool,
80 pub split_point: f64,
81 pub accel: usize,
82 pub k: usize,
83 pub d: usize,
84 pub f: u32,
87 pub k_candidates: Vec<usize>,
88 pub d_candidates: Vec<usize>,
89 pub f_candidates: Vec<u32>,
90}
91
92impl Default for FastCoverOptions {
93 fn default() -> Self {
94 Self {
95 optimize: true,
96 split_point: 0.75,
97 accel: 1,
98 k: 256,
99 d: 8,
100 f: 20,
101 k_candidates: DEFAULT_K_CANDIDATES.to_vec(),
102 d_candidates: DEFAULT_D_CANDIDATES.to_vec(),
103 f_candidates: DEFAULT_F_CANDIDATES.to_vec(),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, Default)]
109pub struct FinalizeOptions {
110 pub dict_id: Option<u32>,
111}
112
113pub(super) struct DictParams {
118 pub segment_size: u32,
128}
129
130pub fn create_raw_dict_from_dir<P: AsRef<Path>, W: io::Write>(
150 path: P,
151 output: &mut W,
152 dict_size: usize,
153) -> Result<(), io::Error> {
154 let mut file_paths: Vec<PathBuf> = Vec::new();
156 let dir: fs::ReadDir = fs::read_dir(path)?;
157 fn recurse_read(dir: fs::ReadDir, file_paths: &mut Vec<PathBuf>) -> Result<(), io::Error> {
158 for entry in dir {
159 let entry = entry?;
160 if entry.file_type()?.is_dir() {
161 recurse_read(fs::read_dir(entry.path())?, file_paths)?;
162 } else {
163 file_paths.push(entry.path());
164 }
165 }
166 Ok(())
167 }
168 recurse_read(dir, &mut file_paths)?;
169
170 let mut total_file_len: u64 = 0;
172 let mut file_handles: Vec<fs::File> = Vec::new();
173 for path in file_paths {
174 let handle = File::open(path)?;
175 total_file_len += handle.metadata()?.len();
176 file_handles.push(handle);
177 }
178 let empty_reader: Box<dyn Read> = Box::new(io::empty());
179 let chained_files = file_handles
180 .iter()
181 .fold(empty_reader, |acc, reader| Box::new(acc.chain(reader)));
182
183 create_raw_dict_from_source(chained_files, total_file_len as usize, output, dict_size)?;
185 Ok(())
186}
187
188pub fn create_raw_dict_from_source<R: io::Read, W: io::Write>(
208 mut source: R,
209 source_size: usize,
210 output: &mut W,
211 dict_size: usize,
212) -> io::Result<()> {
213 if dict_size == 0 {
214 return Ok(());
215 }
216 let prealloc = source_size.min(MAX_TRAINING_PREALLOC_BYTES);
217 let mut all = Vec::with_capacity(prealloc);
218 source.read_to_end(&mut all)?;
219 create_raw_dict_from_slice(&all, output, dict_size)
220}
221
222pub fn create_raw_dict_from_slice<W: io::Write>(
244 all: &[u8],
245 output: &mut W,
246 dict_size: usize,
247) -> io::Result<()> {
248 if dict_size == 0 || all.is_empty() {
249 return Ok(());
250 }
251
252 if all.len() < K {
253 let keep = usize::min(all.len(), dict_size);
254 output.write_all(&all[all.len() - keep..])?;
255 return Ok(());
256 }
257
258 let source_size = all.len();
259 vprintln!("create_dict: creating {dict_size} byte dict from {source_size} byte source");
260
261 let params = DictParams { segment_size: 2048 };
262 let num_segments = usize::max(1, source_size / params.segment_size as usize);
263 let denom = usize::max(1, source_size / (2 * num_segments));
267 let sample_scale = usize::max(1, usize::min(denom, 256));
268 let mut sample_size = source_size / sample_scale;
269 sample_size = usize::max(sample_size, usize::min(source_size, 16));
270 vprintln!("create_dict: creating {sample_size} byte sample of collection");
271 let mut sample_reader = all;
272 let collection_sample = create_sample(&mut sample_reader, sample_size);
273
274 let mut pool: BinaryHeap<Reverse<Segment>> = BinaryHeap::new();
280 let (num_epochs, epoch_size_kmers) = compute_epoch_info(¶ms, dict_size, source_size / K);
281 let epoch_size = usize::max(K, epoch_size_kmers * K);
286 vprintln!("create_dict: computed epoch info, using {num_epochs} epochs of {epoch_size} bytes");
287 let mut epoch_counter = 0;
288 let mut ctx = Context {
289 frequencies: HashMap::with_capacity(epoch_size / K),
290 };
291 for epoch_idx in 0..num_epochs {
295 let start = epoch_idx * epoch_size;
296 if start >= all.len() {
297 break;
298 }
299 let end = if epoch_idx + 1 == num_epochs {
300 all.len()
301 } else {
302 usize::min(start + epoch_size, all.len())
303 };
304 let epoch = &all[start..end];
305 epoch_counter += 1;
306 let best_segment = pick_best_segment(¶ms, &mut ctx, epoch, &collection_sample);
307 vprintln!(
308 "\tcreate_dict: epoch {epoch_counter}/{num_epochs} has best segment score {}",
309 best_segment.score
310 );
311 pool.push(Reverse(best_segment));
312 ctx.frequencies.clear();
314 }
315 vprintln!(
316 "create_dict: {epoch_counter} epochs written, writing {} segments",
317 pool.len()
318 );
319 while let Some(segment) = pool.pop() {
322 output.write_all(&segment.0.raw)?;
323 }
324 Ok(())
325}
326
327fn strided_index(i: usize, len: usize) -> usize {
335 ((i as u64 * len as u64) / MAX_HUFFMAN_STATS_BYTES as u64) as usize
336}
337
338fn serialize_huffman_table(sample_data: &[u8], raw_content: &[u8]) -> io::Result<Vec<u8>> {
339 fn bounded_huffman_stats(data: &[u8]) -> Vec<u8> {
340 if data.len() <= MAX_HUFFMAN_STATS_BYTES {
341 return data.to_vec();
342 }
343
344 let mut stats = Vec::with_capacity(MAX_HUFFMAN_STATS_BYTES);
345 for i in 0..MAX_HUFFMAN_STATS_BYTES {
346 stats.push(data[strided_index(i, data.len())]);
347 }
348 stats
349 }
350
351 let source = if sample_data.len() >= 2 {
352 sample_data
353 } else {
354 raw_content
355 };
356 let mut stats = bounded_huffman_stats(source);
357 if stats.len() < 2 || stats.iter().all(|b| *b == stats[0]) {
358 stats = (0u8..128).collect();
364 }
365
366 let mut table = HuffmanEncoderTable::build_from_data(stats.as_slice());
367 if table
368 .writeable_table_description_size(&mut crate::fse::fse_encoder::FSETable::blank())
369 .is_none()
370 {
371 stats = (0u8..128).collect();
375 table = HuffmanEncoderTable::build_from_data(stats.as_slice());
376 }
377 let mut writer = BitWriter::new();
378 let mut encoder = HuffmanEncoder::new(&table, &mut writer);
379 encoder.encode(&[stats[0]], true);
380 let encoded = writer.dump();
381
382 let mut decoder = HuffmanDecoderTable::new();
383 let table_size = decoder
384 .build_decoder(encoded.as_slice())
385 .map_err(|e| io::Error::other(format!("failed to decode generated huffman table: {e}")))?;
386 Ok(encoded[..table_size as usize].to_vec())
387}
388
389fn serialize_fse_table(table: &fse_encoder::FSETable) -> Vec<u8> {
390 let mut writer = BitWriter::new();
391 table.write_table(&mut writer);
392 writer.dump()
393}
394
395fn bounded_fse_symbols(data: &[u8], max_symbol: u8) -> Vec<u8> {
396 let modulo = u16::from(max_symbol) + 1;
397 if data.is_empty() {
398 return Vec::from([0u8]);
399 }
400 if data.len() <= MAX_HUFFMAN_STATS_BYTES {
401 return data
402 .iter()
403 .map(|b| (u16::from(*b) % modulo) as u8)
404 .collect();
405 }
406
407 let mut out = Vec::with_capacity(MAX_HUFFMAN_STATS_BYTES);
408 for i in 0..MAX_HUFFMAN_STATS_BYTES {
409 let idx = strided_index(i, data.len());
410 out.push((u16::from(data[idx]) % modulo) as u8);
411 }
412 out
413}
414
415fn serialize_fse_table_from_corpus(
416 sample_data: &[u8],
417 raw_content: &[u8],
418 max_symbol: u8,
419 max_log: u8,
420) -> io::Result<Vec<u8>> {
421 fn counts_total_for_source(source: &[u8], max_symbol: u8, counts: &mut [usize]) -> usize {
422 counts.fill(0);
423 for symbol in bounded_fse_symbols(source, max_symbol) {
424 counts[usize::from(symbol)] += 1;
425 }
426 counts.iter().sum::<usize>()
427 }
428
429 let mut counts = vec![0usize; usize::from(max_symbol) + 1];
430 let using_sample = !sample_data.is_empty();
431 let mut total = counts_total_for_source(
432 if using_sample {
433 sample_data
434 } else {
435 raw_content
436 },
437 max_symbol,
438 &mut counts,
439 );
440 if total <= 1 && using_sample && !raw_content.is_empty() {
441 total = counts_total_for_source(raw_content, max_symbol, &mut counts);
442 }
443 if total <= 1 {
444 return Err(io::Error::new(
445 io::ErrorKind::InvalidInput,
446 "insufficient symbol statistics for FSE table",
447 ));
448 }
449 let table = build_table_from_symbol_counts(&counts, max_log, false);
450 Ok(serialize_fse_table(&table))
451}
452
453fn finalized_content_budget(
454 sample_data: &[u8],
455 raw_fallback: &[u8],
456 dict_size: usize,
457) -> io::Result<usize> {
458 let min_content_size = 8usize;
459 let huf_len = serialize_huffman_table(sample_data, raw_fallback)?.len();
460 let of_len =
461 serialize_fse_table_from_corpus(sample_data, raw_fallback, MAX_OFFSET_CODE, OF_MAX_LOG)?
462 .len();
463 let ml_len = serialize_fse_table_from_corpus(
464 sample_data,
465 raw_fallback,
466 MAX_MATCH_LENGTH_CODE,
467 ML_MAX_LOG,
468 )?
469 .len();
470 let ll_len = serialize_fse_table_from_corpus(
471 sample_data,
472 raw_fallback,
473 MAX_LITERAL_LENGTH_CODE,
474 LL_MAX_LOG,
475 )?
476 .len();
477
478 let header_len = DICT_MAGIC_NUM.len() + 4 + huf_len + of_len + ml_len + ll_len + 12;
479 let max_content_budget = dict_size.saturating_sub(header_len);
480 if max_content_budget < min_content_size {
481 return Err(io::Error::new(
482 io::ErrorKind::InvalidInput,
483 "dictionary size too small to fit header and offset history",
484 ));
485 }
486 Ok(max_content_budget)
487}
488
489fn derive_dict_id(raw_content: &[u8]) -> u32 {
490 let mut h = 0xcbf29ce484222325u64;
491 for &b in raw_content {
492 h ^= u64::from(b);
493 h = h.wrapping_mul(0x100000001b3);
494 }
495 let compliant = (h % ((1u64 << 31) - 32768)) + 32768;
496 compliant as u32
497}
498
499pub fn finalize_raw_dict(
502 raw_content: &[u8],
503 sample_data: &[u8],
504 dict_size: usize,
505 options: FinalizeOptions,
506) -> io::Result<Vec<u8>> {
507 if raw_content.is_empty() {
508 return Err(io::Error::new(
509 io::ErrorKind::InvalidInput,
510 "raw dictionary content must not be empty",
511 ));
512 }
513 let mut out = Vec::with_capacity(dict_size.max(256));
514 out.extend_from_slice(&DICT_MAGIC_NUM);
515 let dict_id = options
516 .dict_id
517 .unwrap_or_else(|| derive_dict_id(raw_content));
518 if dict_id == 0 {
519 return Err(io::Error::new(
520 io::ErrorKind::InvalidInput,
521 "dictionary id must be non-zero",
522 ));
523 }
524 out.extend_from_slice(&dict_id.to_le_bytes());
525 out.extend_from_slice(serialize_huffman_table(sample_data, raw_content)?.as_slice());
526 out.extend_from_slice(
527 serialize_fse_table_from_corpus(sample_data, raw_content, MAX_OFFSET_CODE, OF_MAX_LOG)?
528 .as_slice(),
529 );
530 out.extend_from_slice(
531 serialize_fse_table_from_corpus(
532 sample_data,
533 raw_content,
534 MAX_MATCH_LENGTH_CODE,
535 ML_MAX_LOG,
536 )?
537 .as_slice(),
538 );
539 out.extend_from_slice(
540 serialize_fse_table_from_corpus(
541 sample_data,
542 raw_content,
543 MAX_LITERAL_LENGTH_CODE,
544 LL_MAX_LOG,
545 )?
546 .as_slice(),
547 );
548
549 out.extend_from_slice(&1u32.to_le_bytes());
551 out.extend_from_slice(&4u32.to_le_bytes());
552 out.extend_from_slice(&8u32.to_le_bytes());
553
554 let min_content_size = 8usize;
555 let max_content_budget = dict_size.saturating_sub(out.len());
556 if max_content_budget < min_content_size {
557 return Err(io::Error::new(
558 io::ErrorKind::InvalidInput,
559 "dictionary size too small to fit header and offset history",
560 ));
561 }
562
563 let content = if raw_content.len() > max_content_budget {
564 &raw_content[raw_content.len() - max_content_budget..]
565 } else {
566 raw_content
567 };
568 if content.len() < min_content_size {
569 out.resize(out.len() + (min_content_size - content.len()), 0);
570 }
571 out.extend_from_slice(content);
572 Ok(out)
573}
574
575fn train_fastcover_internal(
578 sample: &[u8],
579 dict_size: usize,
580 options: &FastCoverOptions,
581) -> io::Result<(Vec<u8>, FastCoverTuned)> {
582 let trained = if options.optimize {
583 fastcover::optimize_fastcover_raw(
584 sample,
585 dict_size,
586 options.split_point,
587 options.accel,
588 options.d_candidates.as_slice(),
589 options.f_candidates.as_slice(),
590 options.k_candidates.as_slice(),
591 )
592 } else {
593 let params = fastcover::normalize_fastcover_params(FastCoverParams {
594 k: options.k,
595 d: options.d,
596 f: options.f,
597 accel: options.accel,
598 });
599 fastcover::train_fastcover_raw(sample, dict_size, params).map(|dict| {
600 (
601 dict,
602 FastCoverTuned {
603 k: params.k,
604 d: params.d,
605 f: params.f,
606 accel: params.accel,
607 score: 0,
608 },
609 )
610 })
611 };
612 trained.map_err(io::Error::from)
613}
614
615impl From<fastcover::TableTooLarge> for io::Error {
616 fn from(table: fastcover::TableTooLarge) -> Self {
617 io::Error::new(
618 io::ErrorKind::OutOfMemory,
619 format!(
620 "a FastCOVER table of {} entries does not fit in memory; use a smaller f",
621 table.entries
622 ),
623 )
624 }
625}
626
627pub fn train_fastcover_raw_from_slice(
629 sample: &[u8],
630 dict_size: usize,
631 options: &FastCoverOptions,
632) -> io::Result<(Vec<u8>, FastCoverTuned)> {
633 if sample.is_empty() {
634 return Err(io::Error::new(
635 io::ErrorKind::InvalidInput,
636 "source stream is empty",
637 ));
638 }
639 let (dict, tuned) = train_fastcover_internal(sample, dict_size, options)?;
640 if dict.is_empty() && dict_size > 0 {
641 return Err(io::Error::new(
642 io::ErrorKind::InvalidInput,
643 "training sample is too small for FastCOVER",
644 ));
645 }
646 Ok((dict, tuned))
647}
648
649pub fn create_fastcover_raw_dict_from_source<R: io::Read, W: io::Write>(
654 mut source: R,
655 output: &mut W,
656 dict_size: usize,
657 options: &FastCoverOptions,
658) -> io::Result<FastCoverTuned> {
659 let mut sample = Vec::new();
660 source.read_to_end(&mut sample)?;
661 let (dict, tuned) = train_fastcover_raw_from_slice(sample.as_slice(), dict_size, options)?;
662 output.write_all(dict.as_slice())?;
663 Ok(tuned)
664}
665
666pub fn create_fastcover_dict_from_source<R: io::Read, W: io::Write>(
671 mut source: R,
672 output: &mut W,
673 dict_size: usize,
674 fastcover: &FastCoverOptions,
675 finalize: FinalizeOptions,
676) -> io::Result<FastCoverTuned> {
677 let mut sample = Vec::new();
678 source.read_to_end(&mut sample)?;
679 create_fastcover_dict_from_slice(sample.as_slice(), output, dict_size, fastcover, finalize)
680}
681
682pub fn create_fastcover_dict_from_slice<W: io::Write>(
689 sample: &[u8],
690 output: &mut W,
691 dict_size: usize,
692 fastcover: &FastCoverOptions,
693 finalize: FinalizeOptions,
694) -> io::Result<FastCoverTuned> {
695 if sample.is_empty() {
696 return Err(io::Error::new(
697 io::ErrorKind::InvalidInput,
698 "source stream is empty",
699 ));
700 }
701 let content_budget = finalized_content_budget(sample, sample, dict_size)?;
702 let (raw_dict, tuned) = train_fastcover_raw_from_slice(sample, content_budget, fastcover)?;
703
704 let finalized = finalize_raw_dict(raw_dict.as_slice(), sample, dict_size, finalize)?;
705 output.write_all(finalized.as_slice())?;
706 Ok(tuned)
707}
708
709pub fn create_legacy_dict_from_slice<W: io::Write>(
743 samples: &[u8],
744 sample_sizes: &[usize],
745 output: &mut W,
746 dict_size: usize,
747 selectivity: u32,
748 finalize: FinalizeOptions,
749) -> io::Result<()> {
750 let described = sample_sizes
751 .iter()
752 .try_fold(0usize, |total, &size| total.checked_add(size));
753 if described != Some(samples.len()) {
754 return Err(io::Error::new(
755 io::ErrorKind::InvalidInput,
756 "the sample sizes do not add up to the corpus",
757 ));
758 }
759 let content =
760 legacy::train_legacy_raw(samples, sample_sizes, dict_size, selectivity).map_err(|err| {
761 let reason = match err {
762 legacy::TooSmall::Dictionary => format!(
763 "a legacy dictionary must be at least {} bytes",
764 legacy::DICT_SIZE_MIN
765 ),
766 legacy::TooSmall::Corpus => format!(
767 "the samples total {} bytes; the legacy trainer needs at least {}",
768 samples.len(),
769 legacy::MIN_SAMPLES_SIZE
770 ),
771 legacy::TooSmall::Content => {
772 "the samples repeat too little to yield dictionary content".into()
773 }
774 };
775 io::Error::new(io::ErrorKind::InvalidInput, reason)
776 })?;
777 let finalized = finalize_raw_dict(content.as_slice(), samples, dict_size, finalize)?;
782 output.write_all(finalized.as_slice())
783}
784
785#[cfg(feature = "bench-internals")]
788pub(crate) fn legacy_dict_content(
789 samples: &[u8],
790 sample_sizes: &[usize],
791 dict_size: usize,
792 selectivity: u32,
793) -> Option<Vec<u8>> {
794 legacy::train_legacy_raw(samples, sample_sizes, dict_size, selectivity).ok()
795}
796
797#[cfg(feature = "bench-internals")]
805pub(crate) fn dict_roundtrip_fixture() -> (
806 alloc::vec::Vec<u8>,
807 alloc::vec::Vec<u8>,
808 alloc::vec::Vec<u8>,
809) {
810 use crate::decoding::Dictionary;
811 use crate::encoding::{CompressionLevel, FrameCompressor};
812
813 let mut sample = alloc::vec::Vec::new();
814 for i in 0..512u32 {
815 sample.extend_from_slice(
816 alloc::format!(
817 "tenant=demo table=orders key={i} region=eu payload=aaaaabbbbbcccccdddddeeeee\n"
818 )
819 .as_bytes(),
820 );
821 }
822
823 let dict_size = 4096usize;
824 let content_budget = finalized_content_budget(sample.as_slice(), sample.as_slice(), dict_size)
825 .expect("content budget should be computable");
826 let raw = fastcover::train_fastcover_raw(
827 sample.as_slice(),
828 content_budget,
829 fastcover::FastCoverParams {
830 k: 256,
831 d: 8,
832 f: 20,
833 accel: 1,
834 },
835 )
836 .expect("a 2^20 table fits");
837 let finalized = finalize_raw_dict(
838 raw.as_slice(),
839 sample.as_slice(),
840 dict_size,
841 FinalizeOptions::default(),
842 )
843 .expect("finalization should succeed");
844 let parsed =
845 Dictionary::decode_dict(finalized.as_slice()).expect("finalized dictionary should parse");
846 assert!(!parsed.dict_content.is_empty());
847
848 let mut payload = alloc::vec::Vec::new();
849 for idx in 0..96u32 {
850 payload.extend_from_slice(
851 alloc::format!("tenant=demo op=put key={idx} value=aaaaabbbbbcccccdddddeeeee\n")
852 .as_bytes(),
853 );
854 }
855
856 let mut compressed = alloc::vec::Vec::new();
857 let mut compressor = FrameCompressor::new(CompressionLevel::Fastest);
858 compressor
859 .set_dictionary(parsed)
860 .expect("dictionary should attach");
861 compressor.set_source(payload.as_slice());
862 compressor.set_drain(&mut compressed);
863 compressor.compress();
864
865 (finalized, compressed, payload)
866}
867
868#[cfg(test)]
869mod tests;