Move the implementation of programs/timefn.c into rust/src/timefn.rs. The file provides the monotonic nanosecond clock (UTIL_getTime, span helpers, UTIL_waitForNextTick, UTIL_support_MT_measurements) used by the CLI and by several C test tools. timefn.c remains as a declaration-only shim so the original source lists and header configuration keep working, and it pins the ABI with static asserts: UTIL_time_t is returned by value and must stay a plain 64-bit counter, which the Rust #[repr(C)] mirror also asserts. Platform selection mirrors the C preprocessor structure: Windows uses QueryPerformanceCounter, Apple targets use mach_absolute_time, and other POSIX systems use libc clock_gettime(CLOCK_MONOTONIC). Only the unix path is exercised by this environment; the Windows and Apple paths are written from the C source and compile-checked logically but are untested here. The C90 clock() fallback is unreachable on Rust-supported targets, so multi-threaded measurement support is always reported. The symbols live in the program-only zstd-cli-rs package, keeping them out of library builds. Linking that archive into C test binaries surfaced a structural problem: rustc's local ThinLTO promotes internal symbols across codegen units, so extracting the timefn object could drag in the zstd_cli parser object, whose FIO_* externs test binaries cannot satisfy. The parser is therefore gated behind a new additive `cli` cargo feature (default on). Program archives build with cli,compression,decompression as before, while tests/Makefile links a helpers-only archive (rust/target/cli-helpers) built with --no-default-features, which contains no fileio references at all. tests/Makefile gains build rules for the helpers archive and adds it as a prerequisite of every binary that compiles the timefn shim: fullbench(32), fullbench-lib, fullbench-dll, fuzzer(32), zstreamtest(32/asan/tsan/ubsan), paramgrill, decodecorpus, and poolTests. Prerequisite order places the archive after all C objects in `$^` link lines; the known-broken -dll recipes filter to %.c, so they name the archive explicitly. Original C test sources are untouched; only link inputs changed. zstd-cli-rs now depends on libc (already used by the core crate) for clock_gettime and the Mach timebase bindings. Test Plan: - cd rust && cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test --all-targets && cargo build --release - cd rust/cli && cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test --all-targets; repeat tests with --no-default-features plus features compression / decompression / (none) - make -C programs zstd; roundtrip echo hello | zstd | zstd -d - make -C tests fullbench fuzzer zstreamtest paramgrill decodecorpus poolTests; ./tests/fullbench -i0; ./tests/fuzzer -i1 --no-big-tests; ./tests/poolTests; make -C tests test-rust-lib-smoke - verified with nm that the helpers archive member defining UTIL_getTime has no FIO_*/ZSTD_* undefined references Refs: rust/README.md
199 lines
6.5 KiB
Rust
199 lines
6.5 KiB
Rust
#![allow(non_camel_case_types)]
|
|
#![allow(non_snake_case)]
|
|
|
|
//! Precise monotonic time measurement for the command-line programs.
|
|
//!
|
|
//! Port of `programs/timefn.c`. `UTIL_time_t` is a plain nanosecond counter
|
|
//! whose absolute value is meaningless; only spans between two measurements
|
|
//! are valid. The struct crosses the C ABI by value, so it stays `repr(C)`
|
|
//! with the exact `timefn.h` layout.
|
|
//!
|
|
//! Platform selection mirrors the C preprocessor structure: Windows uses the
|
|
//! performance counter, Apple systems use the Mach absolute clock, and other
|
|
//! POSIX systems use `clock_gettime(CLOCK_MONOTONIC)`. The C90 `clock()`
|
|
//! fallback is never needed on targets Rust supports, so multi-threaded
|
|
//! measurements are always supported.
|
|
|
|
use std::os::raw::c_int;
|
|
|
|
/// Precise Time (`PTime` in timefn.h): an unsigned 64-bit nanosecond count.
|
|
pub type PTime = u64;
|
|
|
|
/// Nanosecond time counter with the `timefn.h` `UTIL_time_t` layout.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct UTIL_time_t {
|
|
pub t: PTime,
|
|
}
|
|
|
|
const _: () = assert!(std::mem::size_of::<PTime>() == 8);
|
|
const _: () = assert!(std::mem::size_of::<UTIL_time_t>() == std::mem::size_of::<PTime>());
|
|
|
|
#[cfg(windows)]
|
|
mod platform {
|
|
use super::PTime;
|
|
use std::sync::OnceLock;
|
|
|
|
#[link(name = "kernel32")]
|
|
unsafe extern "system" {
|
|
/// Takes a `LARGE_INTEGER*`; the union is ABI-identical to `i64*`.
|
|
fn QueryPerformanceCounter(count: *mut i64) -> i32;
|
|
fn QueryPerformanceFrequency(frequency: *mut i64) -> i32;
|
|
}
|
|
|
|
pub fn monotonic_ns() -> PTime {
|
|
static TICKS_PER_SECOND: OnceLock<i64> = OnceLock::new();
|
|
let ticks_per_second = *TICKS_PER_SECOND.get_or_init(|| {
|
|
let mut frequency = 0i64;
|
|
if unsafe { QueryPerformanceFrequency(&mut frequency) } == 0 {
|
|
eprintln!(
|
|
"timefn::QueryPerformanceFrequency: {}",
|
|
std::io::Error::last_os_error()
|
|
);
|
|
std::process::abort();
|
|
}
|
|
frequency
|
|
});
|
|
let mut counter = 0i64;
|
|
unsafe { QueryPerformanceCounter(&mut counter) };
|
|
(counter as PTime).wrapping_mul(1_000_000_000) / ticks_per_second as PTime
|
|
}
|
|
}
|
|
|
|
#[cfg(all(unix, target_vendor = "apple"))]
|
|
mod platform {
|
|
use super::PTime;
|
|
use std::sync::OnceLock;
|
|
|
|
pub fn monotonic_ns() -> PTime {
|
|
static RATE: OnceLock<(PTime, PTime)> = OnceLock::new();
|
|
let (numer, denom) = *RATE.get_or_init(|| {
|
|
let mut rate = libc::mach_timebase_info { numer: 0, denom: 0 };
|
|
unsafe { libc::mach_timebase_info(&mut rate) };
|
|
(PTime::from(rate.numer), PTime::from(rate.denom))
|
|
});
|
|
unsafe { libc::mach_absolute_time() }.wrapping_mul(numer) / denom
|
|
}
|
|
}
|
|
|
|
#[cfg(all(unix, not(target_vendor = "apple")))]
|
|
mod platform {
|
|
use super::PTime;
|
|
|
|
pub fn monotonic_ns() -> PTime {
|
|
// Zero-initialized like the C source, which works around timespec_get
|
|
// msan limitations on some targets.
|
|
let mut time: libc::timespec = unsafe { std::mem::zeroed() };
|
|
if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut time) } != 0 {
|
|
eprintln!(
|
|
"timefn::clock_gettime(CLOCK_MONOTONIC): {}",
|
|
std::io::Error::last_os_error()
|
|
);
|
|
std::process::abort();
|
|
}
|
|
(time.tv_sec as PTime)
|
|
.wrapping_mul(1_000_000_000)
|
|
.wrapping_add(time.tv_nsec as PTime)
|
|
}
|
|
}
|
|
|
|
/// Returns the current value of the platform's monotonic nanosecond clock.
|
|
#[no_mangle]
|
|
pub extern "C" fn UTIL_getTime() -> UTIL_time_t {
|
|
UTIL_time_t {
|
|
t: platform::monotonic_ns(),
|
|
}
|
|
}
|
|
|
|
/// Nanoseconds elapsed between two measurements, with C unsigned wrap-around.
|
|
#[no_mangle]
|
|
pub extern "C" fn UTIL_getSpanTimeNano(clockStart: UTIL_time_t, clockEnd: UTIL_time_t) -> PTime {
|
|
clockEnd.t.wrapping_sub(clockStart.t)
|
|
}
|
|
|
|
/// Microseconds elapsed between two measurements, truncated like C division.
|
|
#[no_mangle]
|
|
pub extern "C" fn UTIL_getSpanTimeMicro(begin: UTIL_time_t, end: UTIL_time_t) -> PTime {
|
|
UTIL_getSpanTimeNano(begin, end) / 1000
|
|
}
|
|
|
|
/// Microseconds elapsed since `clockStart`.
|
|
#[no_mangle]
|
|
pub extern "C" fn UTIL_clockSpanMicro(clockStart: UTIL_time_t) -> PTime {
|
|
UTIL_getSpanTimeMicro(clockStart, UTIL_getTime())
|
|
}
|
|
|
|
/// Nanoseconds elapsed since `clockStart`.
|
|
#[no_mangle]
|
|
pub extern "C" fn UTIL_clockSpanNano(clockStart: UTIL_time_t) -> PTime {
|
|
UTIL_getSpanTimeNano(clockStart, UTIL_getTime())
|
|
}
|
|
|
|
/// Busy-waits until the clock produces a new tick, improving measurement
|
|
/// accuracy on platforms with a low timer resolution.
|
|
#[no_mangle]
|
|
pub extern "C" fn UTIL_waitForNextTick() {
|
|
let clockStart = UTIL_getTime();
|
|
loop {
|
|
let clockEnd = UTIL_getTime();
|
|
if UTIL_getSpanTimeNano(clockStart, clockEnd) != 0 {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// All clock sources used by the Rust port are valid under multi-threaded
|
|
/// workloads; only the C90 `clock()` fallback of the C source was not.
|
|
#[no_mangle]
|
|
pub extern "C" fn UTIL_support_MT_measurements() -> c_int {
|
|
1
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn nanosecond_spans_subtract_with_unsigned_wrap_around() {
|
|
let start = UTIL_time_t { t: 100 };
|
|
let end = UTIL_time_t { t: 350 };
|
|
|
|
assert_eq!(UTIL_getSpanTimeNano(start, end), 250);
|
|
assert_eq!(UTIL_getSpanTimeNano(end, start), u64::MAX - 249);
|
|
assert_eq!(UTIL_getSpanTimeNano(start, start), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn microsecond_spans_truncate_sub_tick_remainders() {
|
|
let start = UTIL_time_t { t: 0 };
|
|
|
|
assert_eq!(UTIL_getSpanTimeMicro(start, UTIL_time_t { t: 999 }), 0);
|
|
assert_eq!(UTIL_getSpanTimeMicro(start, UTIL_time_t { t: 1_000 }), 1);
|
|
assert_eq!(UTIL_getSpanTimeMicro(start, UTIL_time_t { t: 1_999 }), 1);
|
|
assert_eq!(UTIL_getSpanTimeMicro(start, UTIL_time_t { t: 2_000 }), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn clock_is_monotonic_across_measurements() {
|
|
let first = UTIL_getTime();
|
|
let second = UTIL_getTime();
|
|
|
|
assert!(second.t >= first.t);
|
|
assert!(UTIL_clockSpanNano(first) >= UTIL_getSpanTimeNano(first, second));
|
|
}
|
|
|
|
#[test]
|
|
fn waiting_for_the_next_tick_advances_the_clock() {
|
|
let before = UTIL_getTime();
|
|
UTIL_waitForNextTick();
|
|
let after = UTIL_getTime();
|
|
|
|
assert!(UTIL_getSpanTimeNano(before, after) > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn multi_threaded_measurements_are_supported() {
|
|
assert_eq!(UTIL_support_MT_measurements(), 1);
|
|
}
|
|
}
|