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:
2026-07-18 18:46:13 +02:00
parent 1b85c430b1
commit 22bebab928
2 changed files with 368 additions and 30 deletions
+32 -29
View File
@@ -116,6 +116,18 @@ void ZSTDMT_rust_buffer_pool_release(ZSTDMT_RustBufferPool* pool,
ZSTDMT_RustBuffer ZSTDMT_rust_buffer_pool_resize(ZSTDMT_RustBufferPool* pool,
ZSTDMT_RustBuffer buffer);
typedef struct {
size_t error;
size_t lastBlockSize;
} ZSTDMT_chunkProcessResult;
typedef void (*ZSTDMT_chunkProgressFn)(void* opaque, size_t cSize, size_t consumed);
ZSTDMT_chunkProcessResult ZSTDMT_rust_compressJobChunks(
ZSTD_CCtx* cctx, const void* src, size_t srcSize,
void* dst, size_t dstCapacity, size_t chunkSize, unsigned lastJob,
void* progressContext, ZSTDMT_chunkProgressFn progressCallback);
typedef struct {
rawSeq* seq;
size_t pos;
@@ -660,6 +672,18 @@ typedef struct {
unsigned frameChecksumNeeded; /* used only by mtctx */
} ZSTDMT_jobDescription;
static void ZSTDMT_compressionJobProgress(void* opaque, size_t cSize, size_t consumed)
{
ZSTDMT_jobDescription* const job = (ZSTDMT_jobDescription*)opaque;
ZSTD_PTHREAD_MUTEX_LOCK(&job->job_mutex);
job->cSize += cSize;
job->consumed = consumed;
DEBUGLOG(5, "ZSTDMT_compressionJob: compress new block : cSize==%u bytes (total: %u)",
(U32)cSize, (U32)job->cSize);
ZSTD_pthread_cond_signal(&job->job_cond); /* warns some more data is ready to be flushed */
ZSTD_pthread_mutex_unlock(&job->job_mutex);
}
#define JOB_ERROR(e) \
do { \
ZSTD_PTHREAD_MUTEX_LOCK(&job->job_mutex); \
@@ -739,40 +763,19 @@ static void ZSTDMT_compressionJob(void* jobDescription)
/* compress the entire job by smaller chunks, for better granularity */
{ size_t const chunkSize = 4*ZSTD_BLOCKSIZE_MAX;
int const nbChunks = (int)((job->src.size + (chunkSize-1)) / chunkSize);
const BYTE* ip = (const BYTE*) job->src.start;
BYTE* const ostart = (BYTE*)dstBuff.start;
BYTE* op = ostart;
BYTE* oend = op + dstBuff.capacity;
int chunkNb;
if (sizeof(size_t) > sizeof(int)) assert(job->src.size < ((size_t)INT_MAX) * chunkSize); /* check overflow */
DEBUGLOG(5, "ZSTDMT_compressionJob: compress %u bytes in %i blocks", (U32)job->src.size, nbChunks);
assert(job->cSize == 0);
for (chunkNb = 1; chunkNb < nbChunks; chunkNb++) {
size_t const cSize = ZSTD_compressContinue_public(cctx, op, oend-op, ip, chunkSize);
if (ZSTD_isError(cSize)) JOB_ERROR(cSize);
ip += chunkSize;
op += cSize; assert(op < oend);
/* stats */
ZSTD_PTHREAD_MUTEX_LOCK(&job->job_mutex);
job->cSize += cSize;
job->consumed = chunkSize * chunkNb;
DEBUGLOG(5, "ZSTDMT_compressionJob: compress new block : cSize==%u bytes (total: %u)",
(U32)cSize, (U32)job->cSize);
ZSTD_pthread_cond_signal(&job->job_cond); /* warns some more data is ready to be flushed */
ZSTD_pthread_mutex_unlock(&job->job_mutex);
}
/* last block */
assert(chunkSize > 0);
assert((chunkSize & (chunkSize - 1)) == 0); /* chunkSize must be power of 2 for mask==(chunkSize-1) to work */
if ((nbChunks > 0) | job->lastJob /*must output a "last block" flag*/ ) {
size_t const lastBlockSize1 = job->src.size & (chunkSize-1);
size_t const lastBlockSize = ((lastBlockSize1==0) & (job->src.size>=chunkSize)) ? chunkSize : lastBlockSize1;
size_t const cSize = (job->lastJob) ?
ZSTD_compressEnd_public(cctx, op, oend-op, ip, lastBlockSize) :
ZSTD_compressContinue_public(cctx, op, oend-op, ip, lastBlockSize);
if (ZSTD_isError(cSize)) JOB_ERROR(cSize);
lastCBlockSize = cSize;
} }
{ ZSTDMT_chunkProcessResult const result = ZSTDMT_rust_compressJobChunks(
cctx, job->src.start, job->src.size,
dstBuff.start, dstBuff.capacity, chunkSize, job->lastJob,
job, ZSTDMT_compressionJobProgress);
if (ZSTD_isError(result.error)) JOB_ERROR(result.error);
lastCBlockSize = result.lastBlockSize;
}
}
if (!job->firstJob) {
/* Double check that we don't have an ext-dict, because then our
* repcode invalidation doesn't work.
+336 -1
View File
@@ -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,