diff --git a/programs/Makefile b/programs/Makefile index ac91210cc..f8b42893c 100644 --- a/programs/Makefile +++ b/programs/Makefile @@ -124,7 +124,7 @@ RUST_CLI_STATICLIB := $(RUST_CLI_TARGET_DIR)/release/libzstd_cli_rs.a RUST_CLI_STATICLIB_32 := $(RUST_CLI_TARGET_DIR)/$(RUST_TARGET_32)/release/libzstd_cli_rs.a RUST_CLI_CARGO_FLAGS := --manifest-path $(RUST_CLI_MANIFEST) --release \ --target-dir $(RUST_CLI_TARGET_DIR) \ - --no-default-features --features cli,compression,decompression + --no-default-features --features cli,compression,decompression,benchmark $(RUST_CLI_STATICLIB): $(RUST_CLI_SOURCES) $(CARGO) build $(RUST_CLI_CARGO_FLAGS) @@ -132,6 +132,16 @@ $(RUST_CLI_STATICLIB): $(RUST_CLI_SOURCES) $(RUST_CLI_STATICLIB_32): $(RUST_CLI_SOURCES) $(CARGO) build $(RUST_CLI_CARGO_FLAGS) --target $(RUST_TARGET_32) +RUST_NOBENCH_CLI_BUILD_CONFIG := cli-c1-d1-nobench-$(RUST_HUF_MODE) +RUST_NOBENCH_CLI_TARGET_DIR := $(RUST_DIR)/target/$(RUST_NOBENCH_CLI_BUILD_CONFIG) +RUST_NOBENCH_CLI_STATICLIB := $(RUST_NOBENCH_CLI_TARGET_DIR)/release/libzstd_cli_rs.a +RUST_NOBENCH_CLI_CARGO_FLAGS := --manifest-path $(RUST_CLI_MANIFEST) --release \ + --target-dir $(RUST_NOBENCH_CLI_TARGET_DIR) \ + --no-default-features --features cli,compression,decompression + +$(RUST_NOBENCH_CLI_STATICLIB): $(RUST_CLI_SOURCES) + $(CARGO) build $(RUST_NOBENCH_CLI_CARGO_FLAGS) + RUST_DECOMPRESS_BUILD_CONFIG := lib-c0-d1-b0-$(RUST_HUF_MODE)-$(RUST_DEBUG_MODE) RUST_DECOMPRESS_TARGET_DIR := $(RUST_DIR)/target/$(RUST_DECOMPRESS_BUILD_CONFIG) RUST_DECOMPRESS_STATICLIB := $(RUST_DECOMPRESS_TARGET_DIR)/release/libzstd_rs.a @@ -455,7 +465,7 @@ CLEAN += zstd-small zstd-frugal # requested HUF decoder mode from the C sources while the Rust archive sees it. ZSTD_SMALL_HUF_CFLAGS := $(filter -DHUF_FORCE_DECOMPRESS_X1% -DHUF_FORCE_DECOMPRESS_X2%,$(CFLAGS)) zstd-small: CFLAGS = -Os -Wl,-s $(ZSTD_SMALL_HUF_CFLAGS) -zstd-frugal zstd-small: $(ZSTDLIB_CORE_SRC) zstdcli.c util.c timefn.c fileio.c fileio_asyncio.c $(RUST_STATICLIB) $(RUST_CLI_STATICLIB) +zstd-frugal zstd-small: $(ZSTDLIB_CORE_SRC) zstdcli.c util.c timefn.c fileio.c fileio_asyncio.c $(RUST_STATICLIB) $(RUST_NOBENCH_CLI_STATICLIB) $(CC) $(FLAGS) -DZSTD_NOBENCH -DZSTD_NODICT -DZSTD_NOTRACE -UZSTD_LEGACY_SUPPORT -DZSTD_LEGACY_SUPPORT=0 $^ -o $@$(EXT) CLEAN += zstd-decompress diff --git a/programs/zstdcli.c b/programs/zstdcli.c index 1117a2714..810245b43 100644 --- a/programs/zstdcli.c +++ b/programs/zstdcli.c @@ -10,73 +10,17 @@ /* The CLI parser and control flow live in rust/src/zstd_cli.rs. Keep this * translation unit as the stable C entry point used by program launchers. */ -#include /* size_t */ #define ZSTD_STATIC_LINKING_ONLY /* ZSTD_compressionParameters */ #include "../lib/zstd.h" -#ifndef ZSTD_NOBENCH -# include "benchzstd.h" /* BMK_benchFilesAdvanced, BMK_syntheticTest */ -#endif int ZSTD_rust_cli_main(int argCount, const char* const argv[]); const char* ZSTD_rust_cli_expected_version(void); -int ZSTD_rust_cli_bench(const char* const* fileNames, unsigned nbFiles, - const char* dictFileName, - int startCLevel, int endCLevel, - const ZSTD_compressionParameters* compressionParams, - int displayLevel, unsigned nbSeconds, - size_t blockSize, int nbWorkers, int mode); const char* ZSTD_rust_cli_expected_version(void) { return ZSTD_VERSION_STRING; } -/* Benchmark bridge for the Rust CLI. Whether benchmarking exists is a C - * preprocessor property (ZSTD_NOBENCH), so the decision stays in this shim: - * the Rust frontend calls in unconditionally, and stripped program variants - * never reference benchmark symbols. - * @return the benchmark result code (>= 0), or -1 when unavailable. */ -int ZSTD_rust_cli_bench(const char* const* fileNames, unsigned nbFiles, - const char* dictFileName, - int startCLevel, int endCLevel, - const ZSTD_compressionParameters* compressionParams, - int displayLevel, unsigned nbSeconds, - size_t blockSize, int nbWorkers, int mode) -{ -#ifndef ZSTD_NOBENCH - BMK_advancedParams_t advancedParams = BMK_initAdvancedParams(); - int startLevel = startCLevel; - int endLevel = endCLevel; - advancedParams.nbSeconds = nbSeconds; - advancedParams.blockSize = blockSize; - advancedParams.nbWorkers = nbWorkers; - advancedParams.mode = (BMK_mode_t)mode; - if (advancedParams.mode == BMK_decodeOnly) { - startLevel = 0; - endLevel = 0; - } - if (startLevel > ZSTD_maxCLevel()) startLevel = ZSTD_maxCLevel(); - if (endLevel > ZSTD_maxCLevel()) endLevel = ZSTD_maxCLevel(); - if (endLevel < startLevel) endLevel = startLevel; - if (nbFiles == 0) { - /* No input file: benchmark a synthetic sample (lorem generator). */ - return BMK_syntheticTest(-1.0, startLevel, endLevel, - compressionParams, displayLevel, - &advancedParams); - } - return BMK_benchFilesAdvanced(fileNames, nbFiles, dictFileName, - startLevel, endLevel, - compressionParams, displayLevel, - &advancedParams); -#else - (void)fileNames; (void)nbFiles; (void)dictFileName; - (void)startCLevel; (void)endCLevel; (void)compressionParams; - (void)displayLevel; (void)nbSeconds; (void)blockSize; (void)nbWorkers; - (void)mode; - return -1; -#endif -} - int main(int argCount, const char* argv[]) { return ZSTD_rust_cli_main(argCount, argv); diff --git a/rust/cli/Cargo.toml b/rust/cli/Cargo.toml index d4c14a623..63035c22f 100644 --- a/rust/cli/Cargo.toml +++ b/rust/cli/Cargo.toml @@ -7,13 +7,18 @@ edition = "2021" crate-type = ["staticlib"] [features] -default = ["cli", "compression", "decompression"] +default = ["cli", "compression", "decompression", "benchmark"] # The command-line parser and dispatch layer, which requires the C fileio # backend at link time. Program archives enable it; C test binaries link a # helpers-only archive (timefn) built without it. cli = [] compression = [] decompression = [] +helpers = [] +# The integrated benchmark archive is optional so zstd-small and zstd-frugal +# can omit every benchmark implementation and its timing/data-generator +# dependencies. +benchmark = ["compression"] [dependencies] libc = "0.2" diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index f9b15c055..b4548f622 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -1,8 +1,10 @@ +#[cfg(any(feature = "benchmark", feature = "helpers"))] #[path = "../../src/benchfn.rs"] mod benchfn; -#[cfg(feature = "cli")] +#[cfg(all(feature = "cli", feature = "benchmark"))] #[path = "../../src/benchzstd.rs"] mod benchzstd; +#[cfg(any(feature = "benchmark", feature = "helpers"))] #[path = "../../src/datagen.rs"] mod datagen; #[cfg(feature = "cli")] @@ -11,6 +13,7 @@ mod dibio; #[cfg(feature = "cli")] #[path = "../../src/fileio_prefs.rs"] mod fileio_prefs; +#[cfg(any(feature = "benchmark", feature = "helpers"))] #[path = "../../src/lorem.rs"] mod lorem; #[path = "../../src/timefn.rs"] diff --git a/rust/src/zstd_cli.rs b/rust/src/zstd_cli.rs index 1c7a39931..8fa1fc8cc 100644 --- a/rust/src/zstd_cli.rs +++ b/rust/src/zstd_cli.rs @@ -10,11 +10,11 @@ //! writes, dictionary loading, streaming, and metadata preservation remain in //! `programs/fileio.c` for this first migration step. //! -//! Benchmark mode (`-b`) parses here and dispatches through the -//! `ZSTD_rust_cli_bench` bridge in `programs/zstdcli.c`: the run/timing loop -//! (benchfn, timefn) is Rust, while orchestration and result formatting -//! (`benchzstd.c`) remain C behind the preprocessor-gated bridge, so builds -//! with `ZSTD_NOBENCH` never reference benchmark symbols. +//! Benchmark mode (`-b`) parses and dispatches through the Rust CLI archive. +//! The bridge preserves the C CLI's argument normalization and uses the +//! existing Rust `benchzstd` APIs. Builds without the `benchmark` feature +//! return the same unavailable-build result as `ZSTD_NOBENCH` builds without +//! referencing benchmark symbols. //! //! Recursive expansion (`-r`), `--filelist`, the output-directory modes, and //! `--list` reuse the C `FileNamesTable` helpers from `programs/util.c` and @@ -104,7 +104,7 @@ struct FIO_ctx_t { #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] -struct ZSTD_compressionParameters { +pub(crate) struct ZSTD_compressionParameters { windowLog: u32, chainLog: u32, hashLog: u32, @@ -295,22 +295,6 @@ unsafe extern "C" { display_level: c_int, ) -> c_int; - /// Benchmark bridge implemented by the `programs/zstdcli.c` shim, which - /// owns the `ZSTD_NOBENCH` preprocessor decision. Returns the benchmark - /// result (>= 0), or -1 when benchmarking is compiled out. - fn ZSTD_rust_cli_bench( - file_names: *const *const c_char, - nb_files: c_uint, - dict_file_name: *const c_char, - start_level: c_int, - end_level: c_int, - compression_params: *const ZSTD_compressionParameters, - display_level: c_int, - nb_seconds: c_uint, - block_size: usize, - nb_workers: c_int, - mode: c_int, - ) -> c_int; /// Narrow bridge to `programs/dibio.c`. The file loader and dictionary /// algorithms remain on the C/Rust library side of this boundary. #[cfg(feature = "compression")] @@ -2374,9 +2358,122 @@ fn run_list(cli: &Cli) -> i32 { unsafe { FIO_listMultipleFiles(inputs.len() as c_uint, inputs.as_ptr(), cli.display_level) } } -/// Runs benchmark mode through the C bridge. Level clamping against -/// `ZSTD_maxCLevel()` happens on the C side, where the symbol is always -/// available when benchmarking is compiled in. No input file means a +#[cfg(feature = "benchmark")] +fn normalize_benchmark_levels( + start_c_level: c_int, + end_c_level: c_int, + mode: c_int, + max_c_level: c_int, +) -> (c_int, c_int) { + let mut start_level = start_c_level; + let mut end_level = end_c_level; + if mode == crate::benchzstd::BMK_decodeOnly { + start_level = 0; + end_level = 0; + } + if start_level > max_c_level { + start_level = max_c_level; + } + if end_level > max_c_level { + end_level = max_c_level; + } + if end_level < start_level { + end_level = start_level; + } + (start_level, end_level) +} + +/// Benchmark bridge formerly implemented by `programs/zstdcli.c`. +/// +/// Keep the exported name because it is a useful program-side ABI boundary, +/// but make the Rust CLI archive own both the normalization and dispatch. +#[cfg(feature = "benchmark")] +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_cli_bench( + file_names: *const *const c_char, + nb_files: c_uint, + dict_file_name: *const c_char, + start_c_level: c_int, + end_c_level: c_int, + compression_params: *const ZSTD_compressionParameters, + display_level: c_int, + nb_seconds: c_uint, + block_size: usize, + nb_workers: c_int, + mode: c_int, +) -> c_int { + let mut advanced_params = crate::benchzstd::BMK_initAdvancedParams(); + advanced_params.nbSeconds = nb_seconds; + advanced_params.blockSize = block_size; + advanced_params.nbWorkers = nb_workers; + advanced_params.mode = mode; + + let (start_level, end_level) = + normalize_benchmark_levels(start_c_level, end_c_level, mode, unsafe { + ZSTD_maxCLevel() + }); + let compression_params = compression_params.cast(); + if nb_files == 0 { + /* No input file: benchmark a synthetic sample (lorem generator). */ + unsafe { + crate::benchzstd::BMK_syntheticTest( + -1.0, + start_level, + end_level, + compression_params, + display_level, + &advanced_params, + ) + } + } else { + unsafe { + crate::benchzstd::BMK_benchFilesAdvanced( + file_names, + nb_files, + dict_file_name, + start_level, + end_level, + compression_params, + display_level, + &advanced_params, + ) + } + } +} + +/// `ZSTD_NOBENCH` compatibility bridge for compact CLI archives. +#[cfg(not(feature = "benchmark"))] +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_cli_bench( + file_names: *const *const c_char, + nb_files: c_uint, + dict_file_name: *const c_char, + start_c_level: c_int, + end_c_level: c_int, + compression_params: *const ZSTD_compressionParameters, + display_level: c_int, + nb_seconds: c_uint, + block_size: usize, + nb_workers: c_int, + mode: c_int, +) -> c_int { + let _ = ( + file_names, + nb_files, + dict_file_name, + start_c_level, + end_c_level, + compression_params, + display_level, + nb_seconds, + block_size, + nb_workers, + mode, + ); + -1 +} + +/// Runs benchmark mode through the Rust bridge. No input file means a /// synthetic-sample benchmark, matching the C CLI. fn run_bench(cli: &Cli) -> Result { let inputs: Vec<*const c_char> = cli.inputs.iter().map(|value| value.as_ptr()).collect(); @@ -3359,4 +3456,44 @@ mod tests { assert_eq!(cli.operation, Operation::Bench); assert!(cli.bench_decode); } + + #[cfg(feature = "benchmark")] + #[test] + fn benchmark_level_normalization_matches_c_bridge() { + assert_eq!( + normalize_benchmark_levels(100, 4, crate::benchzstd::BMK_both, 22), + (22, 22) + ); + assert_eq!( + normalize_benchmark_levels(7, 3, crate::benchzstd::BMK_both, 22), + (7, 7) + ); + assert_eq!( + normalize_benchmark_levels(7, 3, crate::benchzstd::BMK_decodeOnly, 22), + (0, 0) + ); + } + + #[cfg(not(feature = "benchmark"))] + #[test] + fn benchmark_bridge_returns_unavailable_without_feature() { + assert_eq!( + unsafe { + ZSTD_rust_cli_bench( + ptr::null(), + 0, + ptr::null(), + 3, + 3, + ptr::null(), + 0, + 3, + 0, + 1, + 0, + ) + }, + -1 + ); + } } diff --git a/tests/Makefile b/tests/Makefile index e34c24aaf..e78702ccd 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -117,7 +117,7 @@ RUST_CLI_HELPERS_STATICLIB := $(RUST_CLI_HELPERS_TARGET_DIR)/release/libzstd_cli RUST_CLI_HELPERS_STATICLIB_32 := $(RUST_CLI_HELPERS_TARGET_DIR)/$(RUST_TARGET_32)/release/libzstd_cli_rs.a RUST_CLI_HELPERS_CARGO_FLAGS := --manifest-path $(RUST_CLI_MANIFEST) --release \ --target-dir $(RUST_CLI_HELPERS_TARGET_DIR) \ - --no-default-features + --no-default-features --features helpers $(RUST_CLI_HELPERS_STATICLIB): $(RUST_CLI_HELPER_SOURCES) $(CARGO) build $(RUST_CLI_HELPERS_CARGO_FLAGS)