feat(mt): move compression chunk loop into Rust
ZSTDMT_compressionJob kept the chunking kernel in C while Rust already owned most of the MT pool and progress helpers. The loop mixed compression calls with mutex and condition-variable publication, making it difficult to test its exact final-chunk and partial-tail behavior independently. Move only the chunk-processing kernel into Rust. It calls the existing C compression entry points through an explicit ABI, reports the final block size or error in a repr(C) result, and invokes a C progress callback after each successful intermediate chunk. C retains worker setup, serial and raw-sequence state, synchronization, error publication, cleanup, and final completion reporting. Mock compressors cover empty, exact, partial, progress, and error boundaries without mirroring private MT job state in Rust. Test Plan: - `cargo test --manifest-path rust/Cargo.toml --lib zstdmt_compress -- --test-threads=1` -- passed (44 tests). - `cargo clippy --manifest-path rust/Cargo.toml --lib -- -D warnings` -- passed. - `cargo +nightly fmt --manifest-path rust/Cargo.toml -- --check` -- passed. - `ZSTREAM_TESTTIME=-T2s make -B -C tests -j2 test-zstream` -- passed, including multi-threaded dictionary/checksum coverage.
This commit is contained in:
+336
-1
@@ -19,7 +19,7 @@ use std::os::raw::{c_int, c_uint, c_void};
|
||||
use std::ptr;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::errors::{ZstdErrorCode, ERROR};
|
||||
use crate::errors::{ERR_isError, ZstdErrorCode, ERROR};
|
||||
use crate::zstd_compress::ZSTD_frameProgression;
|
||||
|
||||
const ZSTDMT_JOBLOG_MAX: c_uint = if mem::size_of::<usize>() == 4 { 29 } else { 30 };
|
||||
@@ -45,6 +45,146 @@ const PRIME8_BYTES: u64 = 0xCF1B_BCDC_B7A5_6463;
|
||||
const ROLL_HASH_CHAR_OFFSET: u64 = 10;
|
||||
const _: () = assert!(RSYNC_MIN_BLOCK_SIZE >= RSYNC_LENGTH);
|
||||
|
||||
type ZstdMtCompressFn =
|
||||
unsafe extern "C" fn(*mut c_void, *mut c_void, usize, *const c_void, usize) -> usize;
|
||||
pub type ZSTDMT_chunkProgressFn = unsafe extern "C" fn(*mut c_void, usize, usize);
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ZSTDMT_chunkProcessResult {
|
||||
pub error: usize,
|
||||
pub lastBlockSize: usize,
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
unsafe extern "C" {
|
||||
fn ZSTD_compressContinue_public(
|
||||
cctx: *mut c_void,
|
||||
dst: *mut c_void,
|
||||
dst_capacity: usize,
|
||||
src: *const c_void,
|
||||
src_size: usize,
|
||||
) -> usize;
|
||||
fn ZSTD_compressEnd_public(
|
||||
cctx: *mut c_void,
|
||||
dst: *mut c_void,
|
||||
dst_capacity: usize,
|
||||
src: *const c_void,
|
||||
src_size: usize,
|
||||
) -> usize;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
unsafe fn compress_job_chunks_with(
|
||||
cctx: *mut c_void,
|
||||
src: *const c_void,
|
||||
src_size: usize,
|
||||
dst: *mut c_void,
|
||||
dst_capacity: usize,
|
||||
chunk_size: usize,
|
||||
last_job: c_uint,
|
||||
progress_context: *mut c_void,
|
||||
progress_callback: Option<ZSTDMT_chunkProgressFn>,
|
||||
compress_continue: ZstdMtCompressFn,
|
||||
compress_end: ZstdMtCompressFn,
|
||||
) -> ZSTDMT_chunkProcessResult {
|
||||
debug_assert!(chunk_size > 0);
|
||||
debug_assert!(chunk_size.is_power_of_two());
|
||||
|
||||
let nb_chunks = src_size.div_ceil(chunk_size);
|
||||
let mut input = src.cast::<u8>();
|
||||
let mut produced = 0usize;
|
||||
|
||||
for chunk_number in 1..nb_chunks {
|
||||
let c_size = unsafe {
|
||||
compress_continue(
|
||||
cctx,
|
||||
dst.cast::<u8>().wrapping_add(produced).cast(),
|
||||
dst_capacity.wrapping_sub(produced),
|
||||
input.cast(),
|
||||
chunk_size,
|
||||
)
|
||||
};
|
||||
if ERR_isError(c_size) {
|
||||
return ZSTDMT_chunkProcessResult {
|
||||
error: c_size,
|
||||
lastBlockSize: 0,
|
||||
};
|
||||
}
|
||||
input = input.wrapping_add(chunk_size);
|
||||
produced = produced.wrapping_add(c_size);
|
||||
debug_assert!(produced < dst_capacity);
|
||||
|
||||
if let Some(callback) = progress_callback {
|
||||
unsafe { callback(progress_context, c_size, chunk_size * chunk_number) };
|
||||
}
|
||||
}
|
||||
|
||||
if nb_chunks > 0 || last_job != 0 {
|
||||
let last_block_size1 = src_size & (chunk_size - 1);
|
||||
let last_block_size = if last_block_size1 == 0 && src_size >= chunk_size {
|
||||
chunk_size
|
||||
} else {
|
||||
last_block_size1
|
||||
};
|
||||
let c_size = unsafe {
|
||||
let dst = dst.cast::<u8>().wrapping_add(produced).cast();
|
||||
let dst_capacity = dst_capacity.wrapping_sub(produced);
|
||||
let compress = if last_job != 0 {
|
||||
compress_end
|
||||
} else {
|
||||
compress_continue
|
||||
};
|
||||
compress(cctx, dst, dst_capacity, input.cast(), last_block_size)
|
||||
};
|
||||
if ERR_isError(c_size) {
|
||||
return ZSTDMT_chunkProcessResult {
|
||||
error: c_size,
|
||||
lastBlockSize: 0,
|
||||
};
|
||||
}
|
||||
return ZSTDMT_chunkProcessResult {
|
||||
error: 0,
|
||||
lastBlockSize: c_size,
|
||||
};
|
||||
}
|
||||
|
||||
ZSTDMT_chunkProcessResult {
|
||||
error: 0,
|
||||
lastBlockSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn ZSTDMT_rust_compressJobChunks(
|
||||
cctx: *mut c_void,
|
||||
src: *const c_void,
|
||||
src_size: usize,
|
||||
dst: *mut c_void,
|
||||
dst_capacity: usize,
|
||||
chunk_size: usize,
|
||||
last_job: c_uint,
|
||||
progress_context: *mut c_void,
|
||||
progress_callback: Option<ZSTDMT_chunkProgressFn>,
|
||||
) -> ZSTDMT_chunkProcessResult {
|
||||
unsafe {
|
||||
compress_job_chunks_with(
|
||||
cctx,
|
||||
src,
|
||||
src_size,
|
||||
dst,
|
||||
dst_capacity,
|
||||
chunk_size,
|
||||
last_job,
|
||||
progress_context,
|
||||
progress_callback,
|
||||
ZSTD_compressContinue_public,
|
||||
ZSTD_compressEnd_public,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn cycle_log(chain_log: c_uint, strategy: c_int) -> c_uint {
|
||||
chain_log.wrapping_sub((strategy >= ZSTD_BTLAZY2) as c_uint)
|
||||
@@ -1225,6 +1365,201 @@ mod tests {
|
||||
static JOB_TABLE_FAIL_INIT: AtomicBool = AtomicBool::new(false);
|
||||
static JOB_TABLE_TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
struct MockChunkCompressor {
|
||||
continue_inputs: Vec<usize>,
|
||||
end_inputs: Vec<usize>,
|
||||
continue_results: Vec<usize>,
|
||||
end_results: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MockChunkProgress {
|
||||
calls: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
unsafe extern "C" fn mock_compress_continue(
|
||||
cctx: *mut c_void,
|
||||
_dst: *mut c_void,
|
||||
_dst_capacity: usize,
|
||||
_src: *const c_void,
|
||||
src_size: usize,
|
||||
) -> usize {
|
||||
let compressor = unsafe { &mut *cctx.cast::<MockChunkCompressor>() };
|
||||
let call_number = compressor.continue_inputs.len();
|
||||
compressor.continue_inputs.push(src_size);
|
||||
compressor
|
||||
.continue_results
|
||||
.get(call_number)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
unsafe extern "C" fn mock_compress_end(
|
||||
cctx: *mut c_void,
|
||||
_dst: *mut c_void,
|
||||
_dst_capacity: usize,
|
||||
_src: *const c_void,
|
||||
src_size: usize,
|
||||
) -> usize {
|
||||
let compressor = unsafe { &mut *cctx.cast::<MockChunkCompressor>() };
|
||||
let call_number = compressor.end_inputs.len();
|
||||
compressor.end_inputs.push(src_size);
|
||||
compressor
|
||||
.end_results
|
||||
.get(call_number)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
unsafe extern "C" fn mock_chunk_progress(context: *mut c_void, c_size: usize, consumed: usize) {
|
||||
let progress = unsafe { &mut *context.cast::<MockChunkProgress>() };
|
||||
progress.calls.push((c_size, consumed));
|
||||
}
|
||||
|
||||
fn run_mock_chunk_loop(
|
||||
compressor: &mut MockChunkCompressor,
|
||||
progress: &mut MockChunkProgress,
|
||||
src_size: usize,
|
||||
chunk_size: usize,
|
||||
last_job: c_uint,
|
||||
) -> ZSTDMT_chunkProcessResult {
|
||||
let src = [0u8; 32];
|
||||
let mut dst = [0u8; 64];
|
||||
unsafe {
|
||||
compress_job_chunks_with(
|
||||
(compressor as *mut MockChunkCompressor).cast(),
|
||||
src.as_ptr().cast(),
|
||||
src_size,
|
||||
dst.as_mut_ptr().cast(),
|
||||
dst.len(),
|
||||
chunk_size,
|
||||
last_job,
|
||||
(progress as *mut MockChunkProgress).cast(),
|
||||
Some(mock_chunk_progress),
|
||||
mock_compress_continue,
|
||||
mock_compress_end,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_loop_handles_empty_jobs_without_compression() {
|
||||
let mut compressor = MockChunkCompressor {
|
||||
continue_inputs: Vec::new(),
|
||||
end_inputs: Vec::new(),
|
||||
continue_results: Vec::new(),
|
||||
end_results: vec![9],
|
||||
};
|
||||
let mut progress = MockChunkProgress::default();
|
||||
|
||||
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 0, 8, 0);
|
||||
assert_eq!(result, ZSTDMT_chunkProcessResult::default());
|
||||
assert!(compressor.continue_inputs.is_empty());
|
||||
assert!(compressor.end_inputs.is_empty());
|
||||
assert!(progress.calls.is_empty());
|
||||
|
||||
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 0, 8, 1);
|
||||
assert_eq!(result.error, 0);
|
||||
assert_eq!(result.lastBlockSize, 9);
|
||||
assert!(compressor.continue_inputs.is_empty());
|
||||
assert_eq!(compressor.end_inputs, [0]);
|
||||
assert!(progress.calls.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_loop_handles_one_exact_chunk_and_last_job_dispatch() {
|
||||
let mut compressor = MockChunkCompressor {
|
||||
continue_inputs: Vec::new(),
|
||||
end_inputs: Vec::new(),
|
||||
continue_results: vec![4],
|
||||
end_results: vec![6],
|
||||
};
|
||||
let mut progress = MockChunkProgress::default();
|
||||
|
||||
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 8, 8, 0);
|
||||
assert_eq!(result.lastBlockSize, 4);
|
||||
assert_eq!(compressor.continue_inputs, [8]);
|
||||
assert!(compressor.end_inputs.is_empty());
|
||||
assert!(progress.calls.is_empty());
|
||||
|
||||
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 8, 8, 1);
|
||||
assert_eq!(result.lastBlockSize, 6);
|
||||
assert_eq!(compressor.end_inputs, [8]);
|
||||
assert!(progress.calls.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_loop_handles_multiple_exact_chunks_and_orders_progress() {
|
||||
let mut compressor = MockChunkCompressor {
|
||||
continue_inputs: Vec::new(),
|
||||
end_inputs: Vec::new(),
|
||||
continue_results: vec![2, 3],
|
||||
end_results: Vec::new(),
|
||||
};
|
||||
let mut progress = MockChunkProgress::default();
|
||||
|
||||
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 16, 8, 0);
|
||||
assert_eq!(result.lastBlockSize, 3);
|
||||
assert_eq!(compressor.continue_inputs, [8, 8]);
|
||||
assert!(compressor.end_inputs.is_empty());
|
||||
assert_eq!(progress.calls, [(2, 8)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_loop_handles_partial_tail_and_final_dispatch() {
|
||||
let mut compressor = MockChunkCompressor {
|
||||
continue_inputs: Vec::new(),
|
||||
end_inputs: Vec::new(),
|
||||
continue_results: vec![2],
|
||||
end_results: vec![5],
|
||||
};
|
||||
let mut progress = MockChunkProgress::default();
|
||||
|
||||
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 11, 8, 1);
|
||||
assert_eq!(result.lastBlockSize, 5);
|
||||
assert_eq!(compressor.continue_inputs, [8]);
|
||||
assert_eq!(compressor.end_inputs, [3]);
|
||||
assert_eq!(progress.calls, [(2, 8)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_loop_propagates_intermediate_errors_after_prior_progress() {
|
||||
let error = ERROR(ZstdErrorCode::DstSizeTooSmall);
|
||||
let mut compressor = MockChunkCompressor {
|
||||
continue_inputs: Vec::new(),
|
||||
end_inputs: Vec::new(),
|
||||
continue_results: vec![2, error],
|
||||
end_results: vec![7],
|
||||
};
|
||||
let mut progress = MockChunkProgress::default();
|
||||
|
||||
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 24, 8, 1);
|
||||
assert_eq!(result.error, error);
|
||||
assert_eq!(result.lastBlockSize, 0);
|
||||
assert_eq!(compressor.continue_inputs, [8, 8]);
|
||||
assert!(compressor.end_inputs.is_empty());
|
||||
assert_eq!(progress.calls, [(2, 8)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_loop_propagates_final_errors_without_progress_callback() {
|
||||
let error = ERROR(ZstdErrorCode::Generic);
|
||||
let mut compressor = MockChunkCompressor {
|
||||
continue_inputs: Vec::new(),
|
||||
end_inputs: Vec::new(),
|
||||
continue_results: Vec::new(),
|
||||
end_results: vec![error],
|
||||
};
|
||||
let mut progress = MockChunkProgress::default();
|
||||
|
||||
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 3, 8, 1);
|
||||
assert_eq!(result.error, error);
|
||||
assert_eq!(result.lastBlockSize, 0);
|
||||
assert!(compressor.continue_inputs.is_empty());
|
||||
assert_eq!(compressor.end_inputs, [3]);
|
||||
assert!(progress.calls.is_empty());
|
||||
}
|
||||
|
||||
unsafe extern "C" fn probe_job_table_init(
|
||||
_job_table: *mut c_void,
|
||||
_nb_jobs: c_uint,
|
||||
|
||||
Reference in New Issue
Block a user