feat(cli): move trace callbacks into Rust

The CLI trace translation unit still contained the complete CSV writer and
strong callback implementations even though the Rust CLI archive already had
the equivalent trace module. Reduce the C file to its public-header shim and
make the Rust implementation authoritative, preserving the exact CSV header,
version assertion, timing fields, and no-trace build behavior. Focused tests
cover header creation and the version-mismatch assertion.

Test Plan:
- CLI tests: 161 default and 127 reduced-feature -- passed
- Focused trace tests: 3/3 in both configurations -- passed
- `make -B -C programs -j2` for all CLI variants -- passed
- Trace compression/decompression smoke test -- passed
- C shim compile with and without `ZSTD_NOTRACE` -- passed
- Clippy, nightly rustfmt, and `git diff --check` -- passed
This commit is contained in:
2026-07-18 18:00:42 +02:00
parent 4aafcde301
commit 829f781855
2 changed files with 42 additions and 172 deletions
+3 -161
View File
@@ -8,165 +8,7 @@
* You may select, at your option, one of the above-listed licenses.
*/
/* The trace implementation lives in rust/src/zstdcli_trace.rs. Keep this
* translation unit so C build systems can continue to enumerate the public
* trace header without defining a second set of ABI symbols. */
#include "zstdcli_trace.h"
#include <stdio.h>
#include <stdlib.h>
#include "timefn.h"
#include "util.h"
#define ZSTD_STATIC_LINKING_ONLY
#include "../lib/zstd.h"
/* We depend on the trace header to avoid duplicating the ZSTD_trace struct.
* But, we check the version so it is compatible with dynamic linking.
*/
#include "../lib/common/zstd_trace.h"
/* We only use macros from threading.h so it is compatible with dynamic linking */
#include "../lib/common/threading.h"
#if ZSTD_TRACE
static FILE* g_traceFile = NULL;
static int g_mutexInit = 0;
static ZSTD_pthread_mutex_t g_mutex;
static UTIL_time_t g_enableTime = UTIL_TIME_INITIALIZER;
void TRACE_enable(char const* filename)
{
int const writeHeader = !UTIL_isRegularFile(filename);
if (g_traceFile)
fclose(g_traceFile);
g_traceFile = fopen(filename, "a");
if (g_traceFile && writeHeader) {
/* Fields:
* algorithm
* version
* method
* streaming
* level
* workers
* dictionary size
* uncompressed size
* compressed size
* duration nanos
* compression ratio
* speed MB/s
*/
fprintf(g_traceFile, "Algorithm, Version, Method, Mode, Level, Workers, Dictionary Size, Uncompressed Size, Compressed Size, Duration Nanos, Compression Ratio, Speed MB/s\n");
}
g_enableTime = UTIL_getTime();
if (!g_mutexInit) {
if (!ZSTD_pthread_mutex_init(&g_mutex, NULL)) {
g_mutexInit = 1;
} else {
TRACE_finish();
}
}
}
void TRACE_finish(void)
{
if (g_traceFile) {
fclose(g_traceFile);
}
g_traceFile = NULL;
if (g_mutexInit) {
ZSTD_pthread_mutex_destroy(&g_mutex);
g_mutexInit = 0;
}
}
static void TRACE_log(char const* method, PTime duration, ZSTD_Trace const* trace)
{
int level = 0;
int workers = 0;
double const ratio = (double)trace->uncompressedSize / (double)trace->compressedSize;
double const speed = ((double)trace->uncompressedSize * 1000) / (double)duration;
if (trace->params) {
ZSTD_CCtxParams_getParameter(trace->params, ZSTD_c_compressionLevel, &level);
ZSTD_CCtxParams_getParameter(trace->params, ZSTD_c_nbWorkers, &workers);
}
assert(g_traceFile != NULL);
ZSTD_pthread_mutex_lock(&g_mutex);
/* Fields:
* algorithm
* version
* method
* streaming
* level
* workers
* dictionary size
* uncompressed size
* compressed size
* duration nanos
* compression ratio
* speed MB/s
*/
fprintf(g_traceFile,
"zstd, %u, %s, %s, %d, %d, %llu, %llu, %llu, %llu, %.2f, %.2f\n",
trace->version,
method,
trace->streaming ? "streaming" : "single-pass",
level,
workers,
(unsigned long long)trace->dictionarySize,
(unsigned long long)trace->uncompressedSize,
(unsigned long long)trace->compressedSize,
(unsigned long long)duration,
ratio,
speed);
ZSTD_pthread_mutex_unlock(&g_mutex);
}
/**
* These symbols override the weak symbols provided by the library.
*/
ZSTD_TraceCtx ZSTD_trace_compress_begin(ZSTD_CCtx const* cctx)
{
(void)cctx;
if (g_traceFile == NULL)
return 0;
return (ZSTD_TraceCtx)UTIL_clockSpanNano(g_enableTime);
}
void ZSTD_trace_compress_end(ZSTD_TraceCtx ctx, ZSTD_Trace const* trace)
{
PTime const beginNanos = (PTime)ctx;
PTime const endNanos = UTIL_clockSpanNano(g_enableTime);
PTime const durationNanos = endNanos > beginNanos ? endNanos - beginNanos : 0;
assert(g_traceFile != NULL);
assert(trace->version == ZSTD_VERSION_NUMBER); /* CLI version must match. */
TRACE_log("compress", durationNanos, trace);
}
ZSTD_TraceCtx ZSTD_trace_decompress_begin(ZSTD_DCtx const* dctx)
{
(void)dctx;
if (g_traceFile == NULL)
return 0;
return (ZSTD_TraceCtx)UTIL_clockSpanNano(g_enableTime);
}
void ZSTD_trace_decompress_end(ZSTD_TraceCtx ctx, ZSTD_Trace const* trace)
{
PTime const beginNanos = (PTime)ctx;
PTime const endNanos = UTIL_clockSpanNano(g_enableTime);
PTime const durationNanos = endNanos > beginNanos ? endNanos - beginNanos : 0;
assert(g_traceFile != NULL);
assert(trace->version == ZSTD_VERSION_NUMBER); /* CLI version must match. */
TRACE_log("decompress", durationNanos, trace);
}
#else /* ZSTD_TRACE */
void TRACE_enable(char const* filename)
{
(void)filename;
}
void TRACE_finish(void) {}
#endif /* ZSTD_TRACE */
+39 -11
View File
@@ -4,11 +4,12 @@
//! Command-line trace callbacks.
//!
//! `programs/zstdcli_trace.c` is a small strong-symbol override for the
//! library's weak tracing callbacks. Keep the wire-visible structures and
//! CSV format here so the CLI archive does not need a C implementation merely
//! to enable tracing. The decompression-only CLI still gets the no-op
//! callbacks below; that variant does not link the compression parameter API.
//! `programs/zstdcli_trace.c` is a declaration-only ABI shim. These functions
//! provide the strong-symbol override for the library's weak tracing
//! callbacks. Keep the wire-visible structures and CSV format here so the CLI
//! archive does not need a C implementation merely to enable tracing. The
//! decompression-only CLI still gets the no-op callbacks below; that variant
//! does not link the compression parameter API.
use std::ffi::{c_char, c_int, c_uint, c_void, CStr};
use std::fs::{File, OpenOptions};
@@ -17,6 +18,10 @@ use std::path::Path;
use std::sync::{Mutex, OnceLock};
use std::time::Instant;
/* Mirrors ZSTD_VERSION_NUMBER in lib/zstd.h for the CLI/library ABI check. */
const ZSTD_VERSION_NUMBER: c_uint = 10_507;
const TRACE_HEADER: &str = "Algorithm, Version, Method, Mode, Level, Workers, Dictionary Size, Uncompressed Size, Compressed Size, Duration Nanos, Compression Ratio, Speed MB/s";
#[cfg(feature = "compression")]
const ZSTD_C_COMPRESSION_LEVEL: c_int = 100;
#[cfg(feature = "compression")]
@@ -81,10 +86,7 @@ pub unsafe extern "C" fn TRACE_enable(filename: *const c_char) {
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(mut file) = file {
if !was_regular_file {
let _ = writeln!(
file,
"Algorithm, Version, Method, Mode, Level, Workers, Dictionary Size, Uncompressed Size, Compressed Size, Duration Nanos, Compression Ratio, Speed MB/s"
);
let _ = writeln!(file, "{TRACE_HEADER}");
}
state.file = Some(file);
} else {
@@ -163,6 +165,13 @@ fn trace_log(method: &str, begin: u64, trace: &ZSTD_Trace, state: &mut TraceStat
);
}
fn assert_trace_version(trace: &ZSTD_Trace) {
debug_assert_eq!(
trace.version, ZSTD_VERSION_NUMBER,
"CLI version must match trace version"
);
}
#[no_mangle]
pub extern "C" fn ZSTD_trace_compress_begin(_cctx: *const c_void) -> u64 {
trace_begin()
@@ -176,6 +185,7 @@ pub unsafe extern "C" fn ZSTD_trace_compress_end(ctx: u64, trace: *const ZSTD_Tr
let mut state = trace_state()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
assert_trace_version(unsafe { &*trace });
trace_log("compress", ctx, unsafe { &*trace }, &mut state);
}
@@ -192,6 +202,7 @@ pub unsafe extern "C" fn ZSTD_trace_decompress_end(ctx: u64, trace: *const ZSTD_
let mut state = trace_state()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
assert_trace_version(unsafe { &*trace });
trace_log("decompress", ctx, unsafe { &*trace }, &mut state);
}
@@ -222,8 +233,25 @@ mod tests {
assert_ne!(ZSTD_trace_compress_begin(std::ptr::null()), 0);
TRACE_finish();
let contents = fs::read_to_string(&path).unwrap();
assert_eq!(contents.lines().count(), 1);
assert!(contents.starts_with("Algorithm, Version,"));
assert_eq!(contents, format!("{TRACE_HEADER}\n"));
let _ = fs::remove_file(path);
}
#[test]
#[should_panic(expected = "CLI version must match trace version")]
fn trace_version_mismatch_panics_like_c_assertion() {
let trace = ZSTD_Trace {
version: ZSTD_VERSION_NUMBER - 1,
streaming: 0,
dictionaryID: 0,
dictionaryIsCold: 0,
dictionarySize: 0,
uncompressedSize: 0,
compressedSize: 1,
params: std::ptr::null(),
cctx: std::ptr::null(),
dctx: std::ptr::null(),
};
assert_trace_version(&trace);
}
}