feat(mt): move job-ring decisions into Rust

Move multithreaded compression's input-retention and pending-output policy
behind scalar job projections. Rust scans the ring and selects source or
prefix ranges while C keeps job descriptors, mutexes, pointer ownership, and
worker-private layout behind the projection callback.

Test Plan:
- cargo test --manifest-path rust/Cargo.toml --all-targets -- --test-threads=1
- cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings
- make -B -C lib -j2 lib
- make -B -C programs -j2 zstd
- make -B -C tests -j2 test-fuzzer
- make -B -C tests -j2 test-zstd
This commit is contained in:
2026-07-18 22:08:46 +02:00
parent f3bc5e98f1
commit 380d8075c6
2 changed files with 307 additions and 57 deletions
+258
View File
@@ -65,6 +65,36 @@ pub struct ZSTDMT_flushPublicationResult {
pub dstFlushed: usize,
}
/// Scalar snapshot of one C-owned job descriptor.
///
/// The C adapter fills this projection while holding the descriptor mutex.
/// Rust owns only the ring-scan decisions below; the descriptor layout,
/// synchronization objects, and all pointer ownership stay in C.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ZSTDMT_jobProjection {
pub consumed: usize,
pub cSize: usize,
pub srcStart: *const c_void,
pub srcSize: usize,
pub prefixStart: *const c_void,
pub prefixSize: usize,
pub dstFlushed: usize,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ZSTDMT_inputRange {
pub start: *const c_void,
pub size: usize,
}
pub type ZSTDMT_jobProjectionFn = unsafe extern "C" fn(
opaque: *mut c_void,
job_id: c_uint,
projection: *mut ZSTDMT_jobProjection,
);
const FLUSH_PUBLICATION_OK: c_int = 0;
const FLUSH_PUBLICATION_INVALID_BOUNDS: c_int = 1;
@@ -276,6 +306,140 @@ pub unsafe extern "C" fn ZSTDMT_rust_publishJobOutput(
}
}
#[inline]
fn get_input_data_in_use_with<F>(
first_job_id: c_uint,
last_job_id: c_uint,
job_id_mask: c_uint,
round_buffer_capacity: usize,
target_section_size: usize,
mut project_job: F,
) -> ZSTDMT_inputRange
where
F: FnMut(c_uint) -> ZSTDMT_jobProjection,
{
debug_assert!(target_section_size > 0);
if target_section_size == 0 {
return ZSTDMT_inputRange::default();
}
/* No job can refer to the first round buffer before this point. */
let jobs_before_round_buffer_reuse = round_buffer_capacity / target_section_size;
/* C's usual arithmetic conversions promote `lastJobID` to size_t here. */
if (last_job_id as usize) < jobs_before_round_buffer_reuse {
return ZSTDMT_inputRange::default();
}
for job_id in first_job_id..last_job_id {
let projection = project_job(job_id & job_id_mask);
if projection.consumed >= projection.srcSize {
continue;
}
if projection.prefixSize > 0 {
debug_assert!((projection.prefixStart as usize) <= (projection.srcStart as usize));
return ZSTDMT_inputRange {
start: projection.prefixStart,
size: projection.prefixSize,
};
}
return ZSTDMT_inputRange {
start: projection.srcStart,
size: projection.srcSize,
};
}
ZSTDMT_inputRange::default()
}
/// Find the earliest still-in-use input range in the C-owned job ring.
///
/// C supplies each snapshot under its private job mutex. Rust performs the
/// ring ordering and prefix/source selection without receiving the job table
/// or any of its private synchronization layout.
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_getInputDataInUse(
firstJobID: c_uint,
lastJobID: c_uint,
jobIDMask: c_uint,
roundBufferCapacity: usize,
targetSectionSize: usize,
opaque: *mut c_void,
projectJob: Option<ZSTDMT_jobProjectionFn>,
) -> ZSTDMT_inputRange {
let Some(project_job) = projectJob else {
return ZSTDMT_inputRange::default();
};
get_input_data_in_use_with(
firstJobID,
lastJobID,
jobIDMask,
roundBufferCapacity,
targetSectionSize,
|job_id| {
let mut projection = ZSTDMT_jobProjection::default();
unsafe { project_job(opaque, job_id, &mut projection) };
projection
},
)
}
#[inline]
fn to_flush_now_with<F>(
done_job_id: c_uint,
next_job_id: c_uint,
job_id_mask: c_uint,
mut project_job: F,
) -> usize
where
F: FnMut(c_uint) -> ZSTDMT_jobProjection,
{
if done_job_id == next_job_id {
return 0;
}
let projection = project_job(done_job_id & job_id_mask);
let produced = if ERR_isError(projection.cSize) {
0
} else {
projection.cSize
};
let flushed = if ERR_isError(projection.cSize) {
0
} else {
projection.dstFlushed
};
debug_assert!(flushed <= produced);
debug_assert!(projection.consumed <= projection.srcSize);
let to_flush = produced.wrapping_sub(flushed);
if to_flush == 0 {
/* A live job with no output yet must still have input remaining. */
debug_assert!(projection.consumed < projection.srcSize);
}
to_flush
}
/// Return the output already produced by the oldest active C job but not yet
/// published to the caller's output buffer.
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_toFlushNow(
doneJobID: c_uint,
nextJobID: c_uint,
jobIDMask: c_uint,
opaque: *mut c_void,
projectJob: Option<ZSTDMT_jobProjectionFn>,
) -> usize {
let Some(project_job) = projectJob else {
return 0;
};
to_flush_now_with(doneJobID, nextJobID, jobIDMask, |job_id| {
let mut projection = ZSTDMT_jobProjection::default();
unsafe { project_job(opaque, job_id, &mut projection) };
projection
})
}
#[inline]
fn cycle_log(chain_log: c_uint, strategy: c_int) -> c_uint {
chain_log.wrapping_sub((strategy >= ZSTD_BTLAZY2) as c_uint)
@@ -1917,6 +2081,100 @@ mod tests {
);
}
#[test]
fn input_data_in_use_skips_first_round_and_scans_oldest_ring_slot() {
let source_a = [0u8; 8];
let backing = [1u8; 16];
let source_b = &backing[8..];
let prefix_b = &backing[..3];
let projections = [
ZSTDMT_jobProjection {
consumed: source_a.len(),
srcStart: source_a.as_ptr().cast(),
srcSize: source_a.len(),
..ZSTDMT_jobProjection::default()
},
ZSTDMT_jobProjection {
consumed: 2,
srcStart: source_b.as_ptr().cast(),
srcSize: source_b.len(),
prefixStart: prefix_b.as_ptr().cast(),
prefixSize: prefix_b.len(),
..ZSTDMT_jobProjection::default()
},
];
let mut calls = 0;
let result = get_input_data_in_use_with(4, 6, 1, 16, 8, |slot| {
calls += 1;
projections[slot as usize]
});
assert_eq!(calls, 2);
assert_eq!(result.start, prefix_b.as_ptr().cast());
assert_eq!(result.size, prefix_b.len());
let mut first_round_calls = 0;
let result = get_input_data_in_use_with(0, 3, 1, 32, 8, |slot| {
first_round_calls += 1;
projections[slot as usize]
});
assert_eq!(first_round_calls, 0);
assert_eq!(result, ZSTDMT_inputRange::default());
}
#[test]
fn input_data_in_use_falls_back_to_source_without_prefix() {
let source = [3u8; 11];
let projection = ZSTDMT_jobProjection {
consumed: 4,
srcStart: source.as_ptr().cast(),
srcSize: source.len(),
..ZSTDMT_jobProjection::default()
};
let result = get_input_data_in_use_with(7, 8, 7, 8, 8, |_| projection);
assert_eq!(result.start, source.as_ptr().cast());
assert_eq!(result.size, source.len());
}
#[test]
fn to_flush_now_projects_oldest_slot_and_normalizes_errors() {
let projection = ZSTDMT_jobProjection {
consumed: 5,
cSize: 23,
srcSize: 17,
dstFlushed: 9,
..ZSTDMT_jobProjection::default()
};
let mut seen_slot = None;
let to_flush = to_flush_now_with(5, 9, 3, |slot| {
seen_slot = Some(slot);
projection
});
assert_eq!(seen_slot, Some(1));
assert_eq!(to_flush, 14);
let error_projection = ZSTDMT_jobProjection {
consumed: 0,
cSize: ERROR(ZstdErrorCode::Generic),
srcSize: 17,
dstFlushed: 23,
..ZSTDMT_jobProjection::default()
};
assert_eq!(to_flush_now_with(1, 2, 1, |_| error_projection), 0);
let mut called = false;
assert_eq!(
to_flush_now_with(2, 2, 1, |_| {
called = true;
projection
}),
0
);
assert!(!called);
}
fn call_synchronization_point(
input: &[u8],
input_pos: usize,