Port the recursive post-block-split partition search behind a narrow Rust ABI leaf while keeping CCtx ownership, entropy-state projection, and block emission in C. Preserve left-first midpoint insertion, minimum-sequence gating, the split cap, and error-as-no-split behavior. Test Plan: - cargo test --manifest-path rust/Cargo.toml --no-default-features --features compression - cargo clippy --manifest-path rust/Cargo.toml --no-default-features --features compression --all-targets - cargo +nightly fmt --manifest-path rust/Cargo.toml - make -B -C lib -j2 lib - tests/fuzzer -s4560 -t47 -i48 -v
446 lines
14 KiB
Rust
446 lines
14 KiB
Rust
//! Post-block-split partition search.
|
|
//!
|
|
//! The C compressor owns `ZSTD_CCtx`, block-state ownership, entropy-table
|
|
//! selection, and block emission. This module owns only the recursive search
|
|
//! over shallow `SeqStore_t` views. The estimator is supplied with projected
|
|
//! entropy state and parameter scalars rather than the private C context.
|
|
|
|
use crate::errors::ERR_isError;
|
|
use crate::zstd_compress_stats::{
|
|
SeqStore_t, ZSTD_entropyCTablesMetadata_t, ZSTD_entropyCTables_t,
|
|
ZSTD_rust_buildBlockEntropyStats, ZSTD_rust_deriveSeqStoreChunk, ZSTD_rust_estimateBlockSize,
|
|
};
|
|
use std::ffi::c_void;
|
|
use std::os::raw::c_int;
|
|
|
|
const MIN_SEQUENCES_BLOCK_SPLITTING: usize = 300;
|
|
const MAX_NB_BLOCK_SPLITS: usize = 196;
|
|
const SET_COMPRESSED: c_int = 2;
|
|
|
|
/// Recursively searches one sequence range for useful midpoint partitions.
|
|
///
|
|
/// The callback receives each derived view in the same order as the C
|
|
/// implementation: full range, left half, right half. It returns either an
|
|
/// estimated size or a zstd error code. Errors stop the current search path
|
|
/// and therefore cannot create a split.
|
|
#[allow(clippy::too_many_arguments)]
|
|
unsafe fn derive_block_splits_helper<F>(
|
|
partitions: *mut u32,
|
|
split_index: &mut usize,
|
|
start_index: usize,
|
|
end_index: usize,
|
|
original_seq_store: *const SeqStore_t,
|
|
full_seq_store_chunk: *mut SeqStore_t,
|
|
first_half_seq_store: *mut SeqStore_t,
|
|
second_half_seq_store: *mut SeqStore_t,
|
|
estimate: &mut F,
|
|
) where
|
|
F: FnMut(*mut SeqStore_t) -> usize,
|
|
{
|
|
debug_assert!(end_index >= start_index);
|
|
let sequence_count = end_index - start_index;
|
|
let midpoint = start_index.wrapping_add(end_index) / 2;
|
|
|
|
if sequence_count < MIN_SEQUENCES_BLOCK_SPLITTING || *split_index >= MAX_NB_BLOCK_SPLITS {
|
|
return;
|
|
}
|
|
|
|
unsafe {
|
|
ZSTD_rust_deriveSeqStoreChunk(
|
|
full_seq_store_chunk,
|
|
original_seq_store,
|
|
start_index,
|
|
end_index,
|
|
);
|
|
ZSTD_rust_deriveSeqStoreChunk(
|
|
first_half_seq_store,
|
|
original_seq_store,
|
|
start_index,
|
|
midpoint,
|
|
);
|
|
ZSTD_rust_deriveSeqStoreChunk(
|
|
second_half_seq_store,
|
|
original_seq_store,
|
|
midpoint,
|
|
end_index,
|
|
);
|
|
}
|
|
|
|
let estimated_original_size = estimate(full_seq_store_chunk);
|
|
let estimated_first_half_size = estimate(first_half_seq_store);
|
|
let estimated_second_half_size = estimate(second_half_seq_store);
|
|
if ERR_isError(estimated_original_size)
|
|
|| ERR_isError(estimated_first_half_size)
|
|
|| ERR_isError(estimated_second_half_size)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if estimated_first_half_size.wrapping_add(estimated_second_half_size) < estimated_original_size
|
|
{
|
|
unsafe {
|
|
derive_block_splits_helper(
|
|
partitions,
|
|
split_index,
|
|
start_index,
|
|
midpoint,
|
|
original_seq_store,
|
|
full_seq_store_chunk,
|
|
first_half_seq_store,
|
|
second_half_seq_store,
|
|
estimate,
|
|
);
|
|
}
|
|
|
|
/* Keep the configured split limit a true upper bound even if a left
|
|
* subtree reaches it before its parent gets to midpoint insertion. */
|
|
if *split_index >= MAX_NB_BLOCK_SPLITS {
|
|
return;
|
|
}
|
|
unsafe {
|
|
*partitions.add(*split_index) = midpoint as u32;
|
|
}
|
|
*split_index += 1;
|
|
|
|
unsafe {
|
|
derive_block_splits_helper(
|
|
partitions,
|
|
split_index,
|
|
midpoint,
|
|
end_index,
|
|
original_seq_store,
|
|
full_seq_store_chunk,
|
|
first_half_seq_store,
|
|
second_half_seq_store,
|
|
estimate,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Runs the partition search with a caller-provided estimator.
|
|
unsafe fn derive_block_splits_with_estimator<F>(
|
|
partitions: *mut u32,
|
|
nb_sequences: u32,
|
|
original_seq_store: *const SeqStore_t,
|
|
full_seq_store_chunk: *mut SeqStore_t,
|
|
first_half_seq_store: *mut SeqStore_t,
|
|
second_half_seq_store: *mut SeqStore_t,
|
|
mut estimate: F,
|
|
) -> usize
|
|
where
|
|
F: FnMut(*mut SeqStore_t) -> usize,
|
|
{
|
|
if nb_sequences <= 4 {
|
|
return 0;
|
|
}
|
|
|
|
let mut split_index = 0usize;
|
|
unsafe {
|
|
derive_block_splits_helper(
|
|
partitions,
|
|
&mut split_index,
|
|
0,
|
|
nb_sequences as usize,
|
|
original_seq_store,
|
|
full_seq_store_chunk,
|
|
first_half_seq_store,
|
|
second_half_seq_store,
|
|
&mut estimate,
|
|
);
|
|
*partitions.add(split_index) = nb_sequences;
|
|
}
|
|
split_index
|
|
}
|
|
|
|
/// Rust leaf for C's `ZSTD_deriveBlockSplits()`.
|
|
///
|
|
/// The entropy and parameter arguments are the state projected from the C
|
|
/// context by the narrow shim. Each estimate rebuilds `next_entropy` and the
|
|
/// shared metadata exactly as the original C helper did before comparing the
|
|
/// three candidate sizes.
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTD_rust_deriveBlockSplits(
|
|
partitions: *mut u32,
|
|
nb_sequences: u32,
|
|
original_seq_store: *const SeqStore_t,
|
|
full_seq_store_chunk: *mut SeqStore_t,
|
|
first_half_seq_store: *mut SeqStore_t,
|
|
second_half_seq_store: *mut SeqStore_t,
|
|
prev_entropy: *const ZSTD_entropyCTables_t,
|
|
next_entropy: *mut ZSTD_entropyCTables_t,
|
|
strategy: c_int,
|
|
disable_literal_compression: c_int,
|
|
entropy_metadata: *mut ZSTD_entropyCTablesMetadata_t,
|
|
workspace: *mut c_void,
|
|
workspace_size: usize,
|
|
) -> usize {
|
|
let mut estimate = |seq_store: *mut SeqStore_t| -> usize {
|
|
let entropy_stats = unsafe {
|
|
ZSTD_rust_buildBlockEntropyStats(
|
|
seq_store,
|
|
prev_entropy,
|
|
next_entropy,
|
|
strategy,
|
|
disable_literal_compression,
|
|
entropy_metadata,
|
|
workspace,
|
|
workspace_size,
|
|
)
|
|
};
|
|
if ERR_isError(entropy_stats) {
|
|
return entropy_stats;
|
|
}
|
|
|
|
let seq_store = unsafe { &*seq_store };
|
|
let lit_size = unsafe { seq_store.lit.offset_from(seq_store.litStart) } as usize;
|
|
let nb_seq = unsafe { seq_store.sequences.offset_from(seq_store.sequencesStart) } as usize;
|
|
let write_lit_entropy =
|
|
unsafe { c_int::from((*entropy_metadata).hufMetadata.hType == SET_COMPRESSED) };
|
|
unsafe {
|
|
ZSTD_rust_estimateBlockSize(
|
|
seq_store.litStart,
|
|
lit_size,
|
|
seq_store.ofCode,
|
|
seq_store.llCode,
|
|
seq_store.mlCode,
|
|
nb_seq,
|
|
next_entropy,
|
|
entropy_metadata,
|
|
workspace,
|
|
workspace_size,
|
|
write_lit_entropy,
|
|
1,
|
|
)
|
|
}
|
|
};
|
|
|
|
unsafe {
|
|
derive_block_splits_with_estimator(
|
|
partitions,
|
|
nb_sequences,
|
|
original_seq_store,
|
|
full_seq_store_chunk,
|
|
first_half_seq_store,
|
|
second_half_seq_store,
|
|
&mut estimate,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::errors::{ZstdErrorCode, ERROR};
|
|
use crate::zstd_compress_sequences::SeqDef;
|
|
|
|
fn empty_seq_store() -> SeqStore_t {
|
|
SeqStore_t {
|
|
sequencesStart: std::ptr::null_mut(),
|
|
sequences: std::ptr::null_mut(),
|
|
litStart: std::ptr::null_mut(),
|
|
lit: std::ptr::null_mut(),
|
|
llCode: std::ptr::null_mut(),
|
|
mlCode: std::ptr::null_mut(),
|
|
ofCode: std::ptr::null_mut(),
|
|
maxNbSeq: 0,
|
|
maxNbLit: 0,
|
|
longLengthType: 0,
|
|
longLengthPos: 0,
|
|
}
|
|
}
|
|
|
|
fn make_seq_store(
|
|
sequences: &mut [SeqDef],
|
|
codes: &mut [u8],
|
|
literals: &mut [u8],
|
|
long_length_type: c_int,
|
|
long_length_pos: u32,
|
|
) -> SeqStore_t {
|
|
for sequence in sequences.iter_mut() {
|
|
*sequence = SeqDef {
|
|
offBase: 4,
|
|
litLength: 0,
|
|
mlBase: 0,
|
|
};
|
|
}
|
|
let sequences_start = sequences.as_mut_ptr();
|
|
let literals_start = literals.as_mut_ptr();
|
|
let codes_start = codes.as_mut_ptr();
|
|
SeqStore_t {
|
|
sequencesStart: sequences_start,
|
|
sequences: unsafe { sequences_start.add(sequences.len()) },
|
|
litStart: literals_start,
|
|
lit: literals_start,
|
|
llCode: codes_start,
|
|
mlCode: codes_start,
|
|
ofCode: codes_start,
|
|
maxNbSeq: sequences.len(),
|
|
maxNbLit: literals.len(),
|
|
longLengthType: long_length_type,
|
|
longLengthPos: long_length_pos,
|
|
}
|
|
}
|
|
|
|
unsafe fn sequence_range(
|
|
seq_store: *mut SeqStore_t,
|
|
original_sequences: *mut SeqDef,
|
|
) -> (usize, usize) {
|
|
let seq_store = unsafe { &*seq_store };
|
|
let start = unsafe { seq_store.sequencesStart.offset_from(original_sequences) } as usize;
|
|
let end = unsafe { seq_store.sequences.offset_from(original_sequences) } as usize;
|
|
(start, end)
|
|
}
|
|
|
|
unsafe fn run_search<F>(
|
|
sequences: &mut [SeqDef],
|
|
long_length_type: c_int,
|
|
long_length_pos: u32,
|
|
partitions: &mut [u32],
|
|
estimate: F,
|
|
) -> usize
|
|
where
|
|
F: FnMut(*mut SeqStore_t) -> usize,
|
|
{
|
|
let mut codes = vec![0u8; sequences.len()];
|
|
let mut literals = vec![0u8; sequences.len() + 1];
|
|
let original = make_seq_store(
|
|
sequences,
|
|
&mut codes,
|
|
&mut literals,
|
|
long_length_type,
|
|
long_length_pos,
|
|
);
|
|
let mut full = empty_seq_store();
|
|
let mut first = empty_seq_store();
|
|
let mut second = empty_seq_store();
|
|
derive_block_splits_with_estimator(
|
|
partitions.as_mut_ptr(),
|
|
sequences.len() as u32,
|
|
&original,
|
|
&mut full,
|
|
&mut first,
|
|
&mut second,
|
|
estimate,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn recursive_search_is_left_first_with_midpoint_insertion() {
|
|
let mut sequences = vec![SeqDef::default(); 1200];
|
|
let original_sequences = sequences.as_mut_ptr();
|
|
let mut partitions = vec![u32::MAX; 8];
|
|
let mut calls = Vec::new();
|
|
let mut call_index = 0usize;
|
|
let splits = unsafe {
|
|
run_search(&mut sequences, 0, 0, &mut partitions, |chunk| {
|
|
let range = sequence_range(chunk, original_sequences);
|
|
calls.push(range);
|
|
let estimate = if call_index.is_multiple_of(3) {
|
|
1000
|
|
} else {
|
|
0
|
|
};
|
|
call_index += 1;
|
|
estimate
|
|
})
|
|
};
|
|
|
|
assert_eq!(splits, 7);
|
|
assert_eq!(&partitions[..splits], &[150, 300, 450, 600, 750, 900, 1050]);
|
|
assert_eq!(
|
|
&calls[..6],
|
|
&[
|
|
(0, 1200),
|
|
(0, 600),
|
|
(600, 1200),
|
|
(0, 600),
|
|
(0, 300),
|
|
(300, 600)
|
|
]
|
|
);
|
|
assert_eq!(partitions[splits], 1200);
|
|
}
|
|
|
|
#[test]
|
|
fn minimum_sequence_count_skips_estimation() {
|
|
let mut sequences = vec![SeqDef::default(); 299];
|
|
let mut partitions = [u32::MAX; 2];
|
|
let mut calls = 0usize;
|
|
let splits = unsafe {
|
|
run_search(&mut sequences, 0, 0, &mut partitions, |_| {
|
|
calls += 1;
|
|
0
|
|
})
|
|
};
|
|
|
|
assert_eq!(splits, 0);
|
|
assert_eq!(calls, 0);
|
|
assert_eq!(partitions[0], 299);
|
|
}
|
|
|
|
#[test]
|
|
fn split_count_is_capped_at_configured_limit() {
|
|
let mut sequences = vec![SeqDef::default(); 60_000];
|
|
let mut partitions = vec![u32::MAX; MAX_NB_BLOCK_SPLITS + 1];
|
|
let mut call_index = 0usize;
|
|
let splits = unsafe {
|
|
run_search(&mut sequences, 0, 0, &mut partitions, |_| {
|
|
let estimate = if call_index.is_multiple_of(3) {
|
|
1000
|
|
} else {
|
|
0
|
|
};
|
|
call_index += 1;
|
|
estimate
|
|
})
|
|
};
|
|
|
|
assert_eq!(splits, MAX_NB_BLOCK_SPLITS);
|
|
assert!(partitions[..splits]
|
|
.windows(2)
|
|
.all(|window| window[0] < window[1]));
|
|
assert_eq!(partitions[splits], 60_000);
|
|
}
|
|
|
|
#[test]
|
|
fn estimator_errors_leave_the_current_range_unsplit() {
|
|
let mut sequences = vec![SeqDef::default(); 600];
|
|
let mut partitions = [u32::MAX; 2];
|
|
let mut calls = 0usize;
|
|
let splits = unsafe {
|
|
run_search(&mut sequences, 0, 0, &mut partitions, |_| {
|
|
calls += 1;
|
|
ERROR(ZstdErrorCode::Generic)
|
|
})
|
|
};
|
|
|
|
assert_eq!(splits, 0);
|
|
assert_eq!(calls, 3);
|
|
assert_eq!(partitions[0], 600);
|
|
}
|
|
|
|
#[test]
|
|
fn derived_views_keep_long_length_metadata_in_their_ranges() {
|
|
let mut sequences = vec![SeqDef::default(); 600];
|
|
let original_sequences = sequences.as_mut_ptr();
|
|
let mut partitions = [u32::MAX; 2];
|
|
let mut metadata = Vec::new();
|
|
let splits = unsafe {
|
|
run_search(&mut sequences, 1, 450, &mut partitions, |chunk| {
|
|
let range = sequence_range(chunk, original_sequences);
|
|
let view = &*chunk;
|
|
metadata.push((range, view.longLengthType, view.longLengthPos));
|
|
0
|
|
})
|
|
};
|
|
|
|
assert_eq!(splits, 0);
|
|
assert_eq!(
|
|
metadata,
|
|
vec![((0, 600), 1, 450), ((0, 300), 0, 450), ((300, 600), 1, 150)]
|
|
);
|
|
}
|
|
}
|