diff --git a/cmake/options.cmake b/cmake/options.cmake
index 4c50c17b1d1f..86ff61486206 100644
--- a/cmake/options.cmake
+++ b/cmake/options.cmake
@@ -137,6 +137,17 @@ option(BUILD_WITH_RAND_ERR "If build with random error injection" OFF)
option(BUILD_TSZ_ENABLED "If build with TSZ compression" ON)
option(BUILD_USE_PUBLIC_DEPS "Use public (internet) URLs for all external dependencies instead of internal mirrors" OFF)
+# Runtime-pluggable accelerated L2 compression backends.
+# When ON (Linux only), taosd can dlopen a user-built drop-in zlib/zstd/lz4
+# (e.g. Intel QAT/IAA, ISA-L) at startup and route the L2 dispatch table
+# through it. If the env var is unset or the dlopen/dlsym fails, the
+# statically linked stock implementations are used unchanged.
+# Activated by: TAOS_COMPRESS_ACCEL=
(or per-codec TAOS_COMPRESS_ACCEL_{ZLIB,ZSTD,LZ4}).
+# See docs/zh/26-tdinternal/11-compress.md for the symbol contract and ABI requirements.
+if(TD_LINUX)
+ option(BUILD_WITH_ACCEL_COMPRESS "Build runtime-pluggable accelerated L2 compression backends (Linux only)" OFF)
+endif()
+
# When BUILD_RELEASE is ON, force CMAKE_BUILD_TYPE to Release so that
# CMake built-in Release flags and ExternalProject configuration align.
if(BUILD_RELEASE)
diff --git a/docs/zh/26-tdinternal/11-compress.md b/docs/zh/26-tdinternal/11-compress.md
index d7b0901313c5..c96f53361780 100644
--- a/docs/zh/26-tdinternal/11-compress.md
+++ b/docs/zh/26-tdinternal/11-compress.md
@@ -35,6 +35,72 @@ TDengine TSDB 在存储架构上采用了列式存储技术,这意味着在存
TDengine TSDB 支持多种压缩算法,包括 LZ4、ZLIB、ZSTD、XZ 等,用户可以根据具体的应用场景和需求,在压缩率和写入速度之间进行灵活权衡,选择最适合的压缩方案。
+#### 使用硬件加速的二级压缩库(可选)
+
+二级压缩默认链接到打包在 TDengine 中的静态 zlib/zstd/lz4。如果部署环境提供了硬件加速的 ABI 兼容替代品(例如 Intel QAT/IAA 加速版 zlib、ISA-L 的 libz、ARM 上经过 SVE 优化的 zstd 等),可以让 taosd 在启动时把这些替代品挂入二级压缩 dispatch table,从而在不修改 SQL 的前提下获得加速;如果替代品不可用,自动回退到打包的静态实现。
+
+该能力仅在 Linux 平台、并且编译时显式开启 `BUILD_WITH_ACCEL_COMPRESS` 时生效:
+
+```bash
+# 编译
+mkdir build && cd build
+cmake -DBUILD_WITH_ACCEL_COMPRESS=ON ..
+make -j$(nproc)
+```
+
+启动时通过环境变量告诉 taosd 从哪里加载替代库:
+
+| 环境变量 | 取值 | 说明 |
+|---|---|---|
+| `TAOS_COMPRESS_ACCEL` | 目录路径 / 不设置 | 设为目录则按惯例从 `/libz.so`、`/libzstd.so`、`/liblz4.so` 加载;不设置(或设为空)时使用内置实现 |
+| `TAOS_COMPRESS_ACCEL_ZLIB` | `.so` 完整路径 | 单独覆盖 zlib 路径,优先于 `TAOS_COMPRESS_ACCEL` |
+| `TAOS_COMPRESS_ACCEL_ZSTD` | `.so` 完整路径 | 同上,覆盖 zstd |
+| `TAOS_COMPRESS_ACCEL_LZ4` | `.so` 完整路径 | 同上,覆盖 lz4 |
+
+**符号约定**:替代库必须导出与上游一致的公共符号,TDengine 启动时会 `dlsym` 它们:
+
+- libz:`compress2`、`uncompress`
+- libzstd:`ZSTD_compress`、`ZSTD_decompress`
+- liblz4:`LZ4_compress_default`、`LZ4_decompress_safe`
+
+ABI 必须与上游相同(参数顺序、返回值语义)。绝大多数硬件加速版本都是 drop-in 替换,无需关心。
+
+**失败回退**:任一步失败(环境变量未设、文件不存在、`dlopen` 失败、缺符号),都不会中断 taosd 启动;该 codec 继续使用静态打包的实现,并在日志中输出 `UTL WARN accel : ...`。
+
+**确认加载成功**:taosd 启动日志会显示一行类似:
+
+```text
+UTL INFO accel zlib: loaded from /opt/qat-zlib/libz.so, L2_ZLIB dispatch patched
+UTL INFO accel zstd: loaded from /opt/qat-zlib/libzstd.so, L2_ZSTD dispatch patched
+```
+
+如果完全没设环境变量,则会看到:
+
+```text
+UTL INFO accel compression: TAOS_COMPRESS_ACCEL{,_ZLIB,_ZSTD,_LZ4} unset; using stock L2 implementations
+```
+
+**评估加速效果**:与 `BUILD_TOOLS=ON` 一起编译会得到 `compressBench`,可以直接对二级压缩 dispatch table 做微基准(绕开 SQL、网络、WAL,结果只反映压缩本身):
+
+```bash
+# Stock 基线(不设 TAOS_COMPRESS_ACCEL)
+./build/bin/compressBench --codec all --size 1 --iters 30 --warmup 5 \
+ --shape mixed --label stock --csv result.csv
+
+# 切换到加速库再跑一次,对比同 codec 的 throughput
+export TAOS_COMPRESS_ACCEL=/opt/qat-zlib
+./build/bin/compressBench --codec all --size 1 --iters 30 --warmup 5 \
+ --shape mixed --label accel --csv result.csv
+```
+
+输出每个 codec 的 mean / p50 / p95 / stdev 以及 MB/s 吞吐和压缩率,并把 `backend=stock|accel` 标在行尾以便对照。`--shape` 提供 `random / repeating / sequential / mixed` 四种数据形态,可分别评估高熵、低熵、单调时间戳和混合负载下的表现;`--size` 支持 0.004(4 KiB)、0.0625(64 KiB)、1(1 MiB)等,覆盖真实列块的常见尺寸。建议至少跑 30 轮 measure + 5 轮 warmup,并在不同空闲时段重复两次以排除瞬时干扰。
+
+**注意事项**:
+
+- 替代库会被 `dlopen` 一次并常驻整个 taosd 生命周期,因此磁盘上别在运行期间替换或删除该文件。
+- 如果替代库本身又依赖其他动态库(例如 QAT 用户态驱动),需要确保它们也能被 `dlopen` 找到——常规手段(`/etc/ld.so.conf.d/`、`LD_LIBRARY_PATH`)都适用。
+- TSZ(浮点有损压缩)和 XZ 不在替换范围内:前者是 TDengine 内部实现,后者使用的是 fast-lzma2 而非主流 xz,没有通用的 drop-in 加速版。
+
### 有损压缩
TDengine TSDB 引擎为浮点数类型数据提供了无损压缩和有损压缩两种模式。浮点数的精度通常由其小数点后的位数决定。在某些情况下,设备采集的浮点数精度较高,但实际应用中关注的精度却较低,此时采用有损压缩可以有效地节约存储空间。TDengine TSDB 的有损压缩算法基于预测模型,其核心思想是利用前序数据点的趋势来预测后续数据点的走势。这种算法能够显著提高压缩率,相比之下,其压缩效果远超无损压缩。有损压缩算法的名称为 TSZ。
diff --git a/source/util/CMakeLists.txt b/source/util/CMakeLists.txt
index 85c181eb3cab..e3353f4478a9 100644
--- a/source/util/CMakeLists.txt
+++ b/source/util/CMakeLists.txt
@@ -23,6 +23,11 @@ DEP_ext_lz4(util)
if(TD_LINUX)
DEP_ext_xxhash(util)
DEP_ext_lzma2(util)
+ if(BUILD_WITH_ACCEL_COMPRESS)
+ # tcompression_accel.c needs dlopen/dlsym/dlerror at runtime
+ target_compile_definitions(util PRIVATE TD_BUILD_WITH_ACCEL_COMPRESS)
+ target_link_libraries(util PUBLIC dl)
+ endif()
endif()
if (NOT ${TD_WINDOWS})
DEP_ext_ssl(util)
diff --git a/source/util/src/tcompression.c b/source/util/src/tcompression.c
index 79870d79ce03..ecceb7eff594 100644
--- a/source/util/src/tcompression.c
+++ b/source/util/src/tcompression.c
@@ -334,6 +334,10 @@ static const int32_t TEST_NUMBER = 1;
bool lossyFloat = false;
bool lossyDouble = false;
+// Defined in tcompression_accel.c. Compiled to a no-op stub when
+// BUILD_WITH_ACCEL_COMPRESS=OFF, so we can call it unconditionally.
+extern void tcompressionAccelInit(void);
+
// init call
void tsCompressInit(char *lossyColumns, float fPrecision, double dPrecision, uint32_t maxIntervals, uint32_t intervals,
int32_t ifAdtFse, const char *compressor) {
@@ -344,7 +348,11 @@ void tsCompressInit(char *lossyColumns, float fPrecision, double dPrecision, uin
tdszInit(fPrecision, dPrecision, maxIntervals, intervals, ifAdtFse, compressor);
if (lossyFloat) uTrace("lossy compression float is opened. ");
if (lossyDouble) uTrace("lossy compression double is opened. ");
- return;
+
+ // Patch compressL2Dict[] with accelerated backends if TAOS_COMPRESS_ACCEL
+ // points at a drop-in shared object. Failure is non-fatal (logged) and the
+ // stock implementations remain in the dispatch table.
+ tcompressionAccelInit();
}
// exit call
void tsCompressExit() { tdszExit(); }
diff --git a/source/util/src/tcompression_accel.c b/source/util/src/tcompression_accel.c
new file mode 100644
index 000000000000..3356dc722891
--- /dev/null
+++ b/source/util/src/tcompression_accel.c
@@ -0,0 +1,328 @@
+/*
+ * Runtime-pluggable accelerated L2 compression backends.
+ *
+ * Stock zlib / zstd / lz4 are statically linked into util at build time and
+ * remain the default. When BUILD_WITH_ACCEL_COMPRESS=ON and the operator
+ * points TAOS_COMPRESS_ACCEL{,_ZLIB,_ZSTD,_LZ4} at a drop-in ABI-compatible
+ * shared object (e.g. an Intel QAT/IAA-accelerated libz, ISA-L's libz, an
+ * arm64-optimized libzstd), startup dlopens it, resolves the public symbols,
+ * and rebinds the corresponding entries in compressL2Dict[]. Any failure
+ * (env unset, file missing, missing symbols) leaves the stock implementation
+ * in place and is logged.
+ *
+ * The whole translation unit compiles to an empty object when the build
+ * option is off, so callers don't need any #ifdef around tcompressionAccelInit().
+ *
+ * See docs/zh/26-tdinternal/11-compress.md for the operator-facing contract.
+ */
+
+#ifdef TD_BUILD_WITH_ACCEL_COMPRESS
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include
+#include
+#include
+#include
+#include
+
+#include "tcompression.h"
+#include "tdef.h"
+#include "tlog.h"
+
+// Stock dispatch table lives in tcompression.c
+extern TCmprL2FnSet compressL2Dict[];
+
+// ---- Resolved function pointers per backend ---------------------------------
+
+typedef struct {
+ void *handle;
+ int (*compress2_fn)(unsigned char *dest, unsigned long *destLen,
+ const unsigned char *source, unsigned long sourceLen, int level);
+ int (*uncompress_fn)(unsigned char *dest, unsigned long *destLen,
+ const unsigned char *source, unsigned long sourceLen);
+} SZlibAccel;
+
+typedef struct {
+ void *handle;
+ size_t (*compress_fn)(void *dst, size_t dstCapacity,
+ const void *src, size_t srcSize, int compressionLevel);
+ size_t (*decompress_fn)(void *dst, size_t dstCapacity,
+ const void *src, size_t compressedSize);
+} SZstdAccel;
+
+typedef struct {
+ void *handle;
+ int (*compress_default_fn)(const char *src, char *dst, int srcSize, int dstCapacity);
+ int (*decompress_safe_fn)(const char *src, char *dst, int compressedSize, int dstCapacity);
+} SLz4Accel;
+
+static SZlibAccel g_zlibAccel;
+static SZstdAccel g_zstdAccel;
+static SLz4Accel g_lz4Accel;
+
+// ---- Wrappers (signatures match l2*Impl_* in tcompression.c) ---------------
+//
+// Defensive bounds checks mirror the L2 dispatch contract (callers always pass
+// outputSize > inputSize + 1 and compressedSize > 1). Without them, an
+// out-of-spec caller or an accel library returning a wrong size that we then
+// re-feed here would underflow `outputSize - 1` / `compressedSize - 1` into
+// ULONG_MAX / SIZE_MAX and turn the subsequent codec call or fallback memcpy
+// into a buffer overrun. The stock implementations in tcompression.c rely on
+// the same invariants but skip the checks; this dispatch lives behind dlopen
+// and is the more exposed surface, so we harden it here (gemini-code-assist
+// security review on PR #35367).
+
+static int32_t accelCompress_zlib(const char *const input, const int32_t inputSize, char *const output,
+ int32_t outputSize, const char type, int8_t lvl) {
+ if (inputSize < 0 || outputSize <= 1) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ unsigned long dstLen = (unsigned long)(outputSize - 1);
+ int rc = g_zlibAccel.compress2_fn((unsigned char *)(output + 1), &dstLen,
+ (const unsigned char *)input, (unsigned long)inputSize, lvl);
+ if (rc == 0 /* Z_OK */) {
+ output[0] = 1;
+ return (int32_t)dstLen + 1;
+ }
+ // Fallback path stores the input verbatim, prefixed with a 0 marker byte;
+ // refuse if the output buffer can't fit it rather than overflow it.
+ if (outputSize - 1 < inputSize) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ output[0] = 0;
+ memcpy(output + 1, input, inputSize);
+ return inputSize + 1;
+}
+
+static int32_t accelDecompress_zlib(const char *const input, const int32_t compressedSize, char *const output,
+ int32_t outputSize, const char type) {
+ if (compressedSize <= 0 || outputSize < 0) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ if (input[0] == 1) {
+ unsigned long len = (unsigned long)outputSize;
+ int rc = g_zlibAccel.uncompress_fn((unsigned char *)output, &len,
+ (const unsigned char *)input + 1,
+ (unsigned long)(compressedSize - 1));
+ if (rc == 0) return (int32_t)len;
+ return TSDB_CODE_THIRDPARTY_ERROR;
+ } else if (input[0] == 0) {
+ if (outputSize < compressedSize - 1) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ memcpy(output, input + 1, compressedSize - 1);
+ return compressedSize - 1;
+ }
+ return TSDB_CODE_THIRDPARTY_ERROR;
+}
+
+static int32_t accelCompress_zstd(const char *const input, const int32_t inputSize, char *const output,
+ int32_t outputSize, const char type, int8_t lvl) {
+ if (inputSize < 0 || outputSize <= 1) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ size_t len = g_zstdAccel.compress_fn(output + 1, (size_t)(outputSize - 1), input, (size_t)inputSize, lvl);
+ // ZSTD_isError() returns a value > srcSize for error codes, so the same
+ // sentinel as the stock path catches both error and ratio-not-worth-it.
+ if (len > (size_t)inputSize) {
+ if (outputSize - 1 < inputSize) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ output[0] = 0;
+ memcpy(output + 1, input, inputSize);
+ return inputSize + 1;
+ }
+ output[0] = 1;
+ return (int32_t)len + 1;
+}
+
+static int32_t accelDecompress_zstd(const char *const input, const int32_t compressedSize, char *const output,
+ int32_t outputSize, const char type) {
+ if (compressedSize <= 0 || outputSize < 0) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ if (input[0] == 1) {
+ return (int32_t)g_zstdAccel.decompress_fn(output, (size_t)outputSize, input + 1, (size_t)(compressedSize - 1));
+ } else if (input[0] == 0) {
+ if (outputSize < compressedSize - 1) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ memcpy(output, input + 1, compressedSize - 1);
+ return compressedSize - 1;
+ }
+ return TSDB_CODE_THIRDPARTY_ERROR;
+}
+
+static int32_t accelCompress_lz4(const char *const input, const int32_t inputSize, char *const output,
+ int32_t outputSize, const char type, int8_t lvl) {
+ if (inputSize < 0 || outputSize <= 1) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ const int32_t n = g_lz4Accel.compress_default_fn(input, output + 1, inputSize, outputSize - 1);
+ if (n <= 0 || n > inputSize) {
+ if (outputSize - 1 < inputSize) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ output[0] = 0;
+ memcpy(output + 1, input, inputSize);
+ return inputSize + 1;
+ }
+ output[0] = 1;
+ return n + 1;
+}
+
+static int32_t accelDecompress_lz4(const char *const input, const int32_t compressedSize, char *const output,
+ int32_t outputSize, const char type) {
+ if (compressedSize <= 0 || outputSize < 0) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ if (input[0] == 1) {
+ const int32_t n = g_lz4Accel.decompress_safe_fn(input + 1, output, compressedSize - 1, outputSize);
+ if (n < 0) {
+ uError("accel lz4: LZ4_decompress_safe returned %d", n);
+ return TSDB_CODE_THIRDPARTY_ERROR;
+ }
+ return n;
+ } else if (input[0] == 0) {
+ if (outputSize < compressedSize - 1) {
+ return TSDB_CODE_INVALID_PARA;
+ }
+ memcpy(output, input + 1, compressedSize - 1);
+ return compressedSize - 1;
+ }
+ return TSDB_CODE_THIRDPARTY_ERROR;
+}
+
+// ---- dlopen + dlsym + dispatch-table patch ---------------------------------
+
+static void *openLibOrLog(const char *codec, const char *path) {
+ void *h = dlopen(path, RTLD_NOW | RTLD_LOCAL);
+ if (h == NULL) {
+ uWarn("accel %s: dlopen(\"%s\") failed: %s", codec, path, dlerror());
+ }
+ return h;
+}
+
+static void *symOrLog(void *h, const char *codec, const char *sym) {
+ dlerror(); // clear stale error
+ void *p = dlsym(h, sym);
+ const char *err = dlerror();
+ if (err != NULL) {
+ uWarn("accel %s: dlsym(\"%s\") failed: %s", codec, sym, err);
+ return NULL;
+ }
+ return p;
+}
+
+static void tryLoadZlibAccel(const char *path) {
+ void *h = openLibOrLog("zlib", path);
+ if (h == NULL) return;
+
+ void *fn_c = symOrLog(h, "zlib", "compress2");
+ void *fn_u = symOrLog(h, "zlib", "uncompress");
+ if (fn_c == NULL || fn_u == NULL) {
+ dlclose(h);
+ return;
+ }
+ g_zlibAccel.handle = h;
+ g_zlibAccel.compress2_fn = (int (*)(unsigned char *, unsigned long *, const unsigned char *, unsigned long, int))fn_c;
+ g_zlibAccel.uncompress_fn = (int (*)(unsigned char *, unsigned long *, const unsigned char *, unsigned long))fn_u;
+ compressL2Dict[L2_ZLIB].comprFn = accelCompress_zlib;
+ compressL2Dict[L2_ZLIB].decomprFn = accelDecompress_zlib;
+ uInfo("accel zlib: loaded from %s, L2_ZLIB dispatch patched", path);
+}
+
+static void tryLoadZstdAccel(const char *path) {
+ void *h = openLibOrLog("zstd", path);
+ if (h == NULL) return;
+
+ void *fn_c = symOrLog(h, "zstd", "ZSTD_compress");
+ void *fn_u = symOrLog(h, "zstd", "ZSTD_decompress");
+ if (fn_c == NULL || fn_u == NULL) {
+ dlclose(h);
+ return;
+ }
+ g_zstdAccel.handle = h;
+ g_zstdAccel.compress_fn = (size_t (*)(void *, size_t, const void *, size_t, int))fn_c;
+ g_zstdAccel.decompress_fn = (size_t (*)(void *, size_t, const void *, size_t))fn_u;
+ compressL2Dict[L2_ZSTD].comprFn = accelCompress_zstd;
+ compressL2Dict[L2_ZSTD].decomprFn = accelDecompress_zstd;
+ uInfo("accel zstd: loaded from %s, L2_ZSTD dispatch patched", path);
+}
+
+static void tryLoadLz4Accel(const char *path) {
+ void *h = openLibOrLog("lz4", path);
+ if (h == NULL) return;
+
+ void *fn_c = symOrLog(h, "lz4", "LZ4_compress_default");
+ void *fn_u = symOrLog(h, "lz4", "LZ4_decompress_safe");
+ if (fn_c == NULL || fn_u == NULL) {
+ dlclose(h);
+ return;
+ }
+ g_lz4Accel.handle = h;
+ g_lz4Accel.compress_default_fn = (int (*)(const char *, char *, int, int))fn_c;
+ g_lz4Accel.decompress_safe_fn = (int (*)(const char *, char *, int, int))fn_u;
+ compressL2Dict[L2_LZ4].comprFn = accelCompress_lz4;
+ compressL2Dict[L2_LZ4].decomprFn = accelDecompress_lz4;
+ uInfo("accel lz4: loaded from %s, L2_LZ4 dispatch patched", path);
+}
+
+// Resolve "/" without overflowing the destination buffer.
+// Returns 0 on success, -1 on overflow (with the buffer left empty).
+static int joinDirAndName(char *dst, size_t dstSize, const char *dir, const char *base) {
+ int n = snprintf(dst, dstSize, "%s/%s", dir, base);
+ if (n < 0 || (size_t)n >= dstSize) {
+ dst[0] = '\0';
+ return -1;
+ }
+ return 0;
+}
+
+// An empty value counts as unset, so operators can disable acceleration with
+// FOO= in unit files / container images where deleting a variable is awkward.
+static const char *getenvNonEmpty(const char *name) {
+ const char *v = getenv(name);
+ return (v != NULL && v[0] != '\0') ? v : NULL;
+}
+
+void tcompressionAccelInit(void) {
+ const char *dir = getenvNonEmpty("TAOS_COMPRESS_ACCEL");
+ const char *zlibLib = getenvNonEmpty("TAOS_COMPRESS_ACCEL_ZLIB");
+ const char *zstdLib = getenvNonEmpty("TAOS_COMPRESS_ACCEL_ZSTD");
+ const char *lz4Lib = getenvNonEmpty("TAOS_COMPRESS_ACCEL_LZ4");
+
+ if (dir == NULL && zlibLib == NULL && zstdLib == NULL && lz4Lib == NULL) {
+ uInfo("accel compression: TAOS_COMPRESS_ACCEL{,_ZLIB,_ZSTD,_LZ4} unset; using stock L2 implementations");
+ return;
+ }
+
+ char buf[1024];
+
+ if (zlibLib != NULL) {
+ tryLoadZlibAccel(zlibLib);
+ } else if (dir != NULL && joinDirAndName(buf, sizeof(buf), dir, "libz.so") == 0) {
+ tryLoadZlibAccel(buf);
+ }
+
+ if (zstdLib != NULL) {
+ tryLoadZstdAccel(zstdLib);
+ } else if (dir != NULL && joinDirAndName(buf, sizeof(buf), dir, "libzstd.so") == 0) {
+ tryLoadZstdAccel(buf);
+ }
+
+ if (lz4Lib != NULL) {
+ tryLoadLz4Accel(lz4Lib);
+ } else if (dir != NULL && joinDirAndName(buf, sizeof(buf), dir, "liblz4.so") == 0) {
+ tryLoadLz4Accel(buf);
+ }
+}
+
+#else // !TD_BUILD_WITH_ACCEL_COMPRESS — no-op stub so callers stay simple
+
+void tcompressionAccelInit(void) {}
+
+#endif // TD_BUILD_WITH_ACCEL_COMPRESS
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 5041f50044b6..4f44cfbdea3b 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -7,3 +7,7 @@ add_subdirectory(shell)
IF(TD_LINUX)
add_subdirectory(rocks-reader)
ENDIF()
+
+IF(BUILD_TOOLS)
+ add_subdirectory(compressBench)
+ENDIF()
diff --git a/tools/compressBench/CMakeLists.txt b/tools/compressBench/CMakeLists.txt
new file mode 100644
index 000000000000..38833cef06cf
--- /dev/null
+++ b/tools/compressBench/CMakeLists.txt
@@ -0,0 +1,13 @@
+IF(BUILD_TOOLS)
+ add_executable(compressBench compressBench.c)
+
+ target_link_libraries(compressBench PRIVATE util m)
+
+ target_include_directories(
+ compressBench
+ PRIVATE
+ ${TD_SOURCE_DIR}/include/util
+ ${TD_SOURCE_DIR}/include/common
+ ${TD_SOURCE_DIR}/include/os
+ )
+ENDIF()
diff --git a/tools/compressBench/compressBench.c b/tools/compressBench/compressBench.c
new file mode 100644
index 000000000000..67305b1e0b99
--- /dev/null
+++ b/tools/compressBench/compressBench.c
@@ -0,0 +1,557 @@
+/*
+ * Copyright (c) 2019 TAOS Data, Inc.
+ *
+ * L2 compression layer micro-benchmark.
+ *
+ * Measures per-block compress and decompress latency + throughput for each
+ * codec in the TDengine L2 dispatch table. Supports multiple iterations,
+ * warmup rounds, and outputs p50/p95/stdev so stock vs. accelerated backends
+ * can be compared side-by-side.
+ *
+ * Build: linked via tools/compressBench/CMakeLists.txt (BUILD_TOOLS=ON).
+ * Run: compressBench --help
+ */
+
+// Standalone bench: it does not participate in the TDengine memory/file/clock
+// accounting layer, so it calls libc malloc/free/fopen/clock_gettime directly.
+// The escape hatch must be defined BEFORE any include that pulls in os/.
+#define ALLOW_FORBID_FUNC
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* Pull in the L2 types and tsCompressInit/tsCompressExit */
+#include "tcompression.h"
+
+/* -------------------------------------------------------------------------
+ * External symbols from tcompression.c
+ * ---------------------------------------------------------------------- */
+extern TCmprL2FnSet compressL2Dict[];
+
+/* Stock compress pointers – used for backend=stock|accel detection.
+ * On Windows/Darwin tcompression.c does NOT define the zlib/zstd/xz variants;
+ * compressL2Dict[] there aliases all of them to l2CompressImpl_lz4 instead.
+ * Only extern what actually exists on the target platform or the link breaks
+ * with "Undefined symbols _l2CompressImpl_{zlib,zstd,xz}" on macOS/Windows. */
+extern int32_t l2CompressImpl_lz4(const char *const, const int32_t, char *const, int32_t, const char, int8_t);
+#if !defined(WINDOWS) && !defined(_TD_DARWIN_64)
+extern int32_t l2CompressImpl_zlib(const char *const, const int32_t, char *const, int32_t, const char, int8_t);
+extern int32_t l2CompressImpl_zstd(const char *const, const int32_t, char *const, int32_t, const char, int8_t);
+extern int32_t l2CompressImpl_xz(const char *const, const int32_t, char *const, int32_t, const char, int8_t);
+#endif
+
+/* tsGetCompressL2Level lives in tcompression.c but has no header prototype */
+extern int8_t tsGetCompressL2Level(uint8_t alg, uint8_t lvl);
+
+/* -------------------------------------------------------------------------
+ * Small helpers
+ * ---------------------------------------------------------------------- */
+
+static double ns_to_ms(double ns) { return ns * 1e-6; }
+static double ns_to_mbs(double ns, size_t bytes) {
+ /* MB/s = bytes / (ns * 1e-9) / 1e6 */
+ return (ns > 0.0) ? ((double)bytes / ns * 1e3) : 0.0;
+}
+
+/* xorshift64 – fast PRNG with good entropy, no deps */
+static uint64_t xstate = 42ULL;
+static uint64_t xorshift64(void) {
+ xstate ^= xstate << 13;
+ xstate ^= xstate >> 7;
+ xstate ^= xstate << 17;
+ return xstate;
+}
+
+static int cmp_double(const void *a, const void *b) {
+ double x = *(const double *)a;
+ double y = *(const double *)b;
+ return (x > y) - (x < y);
+}
+
+typedef struct {
+ double mean;
+ double p50;
+ double p95;
+ double stdev;
+} Stats;
+
+static Stats compute_stats(double *samples, int n) {
+ Stats s = {0};
+ if (n <= 0) return s;
+ qsort(samples, n, sizeof(double), cmp_double);
+ double sum = 0.0;
+ for (int i = 0; i < n; i++) sum += samples[i];
+ s.mean = sum / n;
+ s.p50 = samples[(int)(n * 0.50)];
+ s.p95 = samples[(int)(n * 0.95 < n - 1 ? n * 0.95 : n - 1)];
+ double var = 0.0;
+ for (int i = 0; i < n; i++) {
+ double d = samples[i] - s.mean;
+ var += d * d;
+ }
+ s.stdev = (n > 1) ? sqrt(var / (n - 1)) : 0.0;
+ return s;
+}
+
+/* -------------------------------------------------------------------------
+ * Data generators
+ * ---------------------------------------------------------------------- */
+
+typedef enum { SHAPE_RANDOM = 0, SHAPE_REPEATING, SHAPE_SEQUENTIAL, SHAPE_MIXED } Shape;
+
+static void gen_random(uint8_t *buf, size_t sz) {
+ size_t i = 0;
+ for (; i + 8 <= sz; i += 8) {
+ uint64_t v = xorshift64();
+ memcpy(buf + i, &v, 8);
+ }
+ if (i < sz) {
+ uint64_t v = xorshift64();
+ memcpy(buf + i, &v, sz - i);
+ }
+}
+
+static void gen_repeating(uint8_t *buf, size_t sz) {
+ /* 64-byte pattern repeated */
+ uint8_t pat[64];
+ for (int i = 0; i < 64; i++) pat[i] = (uint8_t)(i * 3 + 7);
+ for (size_t i = 0; i < sz; i++) buf[i] = pat[i % 64];
+}
+
+static void gen_sequential(uint8_t *buf, size_t sz) {
+ /* Monotonic int64 timestamps: base + i*1000000 (1ms apart) */
+ int64_t ts = 1700000000000000000LL; /* 2023-ish nanoseconds */
+ size_t i = 0;
+ for (; i + 8 <= sz; i += 8, ts += 1000000LL) {
+ memcpy(buf + i, &ts, 8);
+ }
+ if (i < sz) {
+ memcpy(buf + i, &ts, sz - i);
+ }
+}
+
+static void gen_mixed(uint8_t *buf, size_t sz) {
+ size_t third = sz / 3;
+ size_t rem = sz - 2 * third;
+ gen_random(buf, third);
+ gen_repeating(buf + third, third);
+ gen_sequential(buf + 2 * third, rem);
+}
+
+static void fill_input(uint8_t *buf, size_t sz, Shape shape, uint64_t seed) {
+ xstate = seed ? seed : 42ULL;
+ switch (shape) {
+ case SHAPE_RANDOM: gen_random(buf, sz); break;
+ case SHAPE_REPEATING: gen_repeating(buf, sz); break;
+ case SHAPE_SEQUENTIAL:gen_sequential(buf, sz); break;
+ default: gen_mixed(buf, sz); break;
+ }
+}
+
+/* -------------------------------------------------------------------------
+ * Timing
+ * ---------------------------------------------------------------------- */
+
+static inline double now_ns(void) {
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (double)ts.tv_sec * 1e9 + (double)ts.tv_nsec;
+}
+
+/* -------------------------------------------------------------------------
+ * CSV helpers
+ * ---------------------------------------------------------------------- */
+
+static void csv_ensure_header(FILE *f) {
+ /* Write header when file is newly created / empty */
+ if (ftell(f) == 0) {
+ fprintf(f,
+ "timestamp,label,codec,size_bytes,lvl,shape,iters,"
+ "c_mean_ms,c_p50_ms,c_p95_ms,c_stdev_ms,c_throughput_MBs,"
+ "d_mean_ms,d_p50_ms,d_p95_ms,d_stdev_ms,d_throughput_MBs,"
+ "ratio,backend\n");
+ }
+}
+
+/* -------------------------------------------------------------------------
+ * Backend detection
+ * ---------------------------------------------------------------------- */
+
+static const char *detect_backend(int codec_idx) {
+ __data_compress_l2_fn_t fn = compressL2Dict[codec_idx].comprFn;
+ switch (codec_idx) {
+ case L2_LZ4: return (fn == l2CompressImpl_lz4) ? "stock" : "accel";
+#if !defined(WINDOWS) && !defined(_TD_DARWIN_64)
+ case L2_ZLIB: return (fn == l2CompressImpl_zlib) ? "stock" : "accel";
+ case L2_ZSTD: return (fn == l2CompressImpl_zstd) ? "stock" : "accel";
+ case L2_XZ: return (fn == l2CompressImpl_xz) ? "stock" : "accel";
+#else
+ // Windows / Darwin alias these three to lz4 in tcompression.c, so the
+ // stock fn pointer is the lz4 one. accel substitution isn't compiled
+ // on those platforms anyway (BUILD_WITH_ACCEL_COMPRESS is Linux-only).
+ case L2_ZLIB:
+ case L2_ZSTD:
+ case L2_XZ: return (fn == l2CompressImpl_lz4) ? "stock" : "accel";
+#endif
+ default: return "auto";
+ }
+}
+
+/* -------------------------------------------------------------------------
+ * Per-codec bench
+ * ---------------------------------------------------------------------- */
+
+typedef struct {
+ int codec_idx;
+ const char *codec_name;
+ size_t input_size;
+ int warmup;
+ int iters;
+ int8_t level;
+ const char *shape_name;
+ const char *label;
+ const char *csv_path;
+} BenchParams;
+
+static int run_bench(const BenchParams *p, const uint8_t *input, uint8_t *compressed, uint8_t *decompressed) {
+ size_t out_buf_sz = p->input_size * 2 + 1024;
+ int total = p->warmup + p->iters;
+
+ double *c_ns = (double *)malloc(sizeof(double) * (size_t)total);
+ double *d_ns = (double *)malloc(sizeof(double) * (size_t)total);
+ if (!c_ns || !d_ns) {
+ fprintf(stderr, "OOM allocating sample arrays\n");
+ free(c_ns);
+ free(d_ns);
+ return 1;
+ }
+
+ tsCompressInit("", 0.0f, 0.0, 0, 0, 0, "");
+
+ int exit_code = 0;
+ for (int it = 0; it < total && exit_code == 0; it++) {
+ /* Compress */
+ double t0 = now_ns();
+ int32_t compressed_sz = compressL2Dict[p->codec_idx].comprFn(
+ (const char *)input, (int32_t)p->input_size,
+ (char *)compressed, (int32_t)out_buf_sz,
+ /* type */ 0, p->level);
+ double t1 = now_ns();
+
+ if (compressed_sz <= 0) {
+ fprintf(stderr, "codec %s compress returned %d at iter %d\n",
+ p->codec_name, compressed_sz, it);
+ exit_code = 2;
+ break;
+ }
+
+ /* Decompress */
+ double t2 = now_ns();
+ int32_t decompressed_sz = compressL2Dict[p->codec_idx].decomprFn(
+ (const char *)compressed, compressed_sz,
+ (char *)decompressed, (int32_t)p->input_size,
+ /* type */ 0);
+ double t3 = now_ns();
+
+ if (decompressed_sz < 0) {
+ fprintf(stderr, "codec %s decompress returned %d at iter %d\n",
+ p->codec_name, decompressed_sz, it);
+ exit_code = 3;
+ break;
+ }
+
+ /* Integrity check */
+ if ((size_t)decompressed_sz != p->input_size ||
+ memcmp(input, decompressed, p->input_size) != 0) {
+ fprintf(stderr,
+ "INTEGRITY FAIL: codec=%s iter=%d decompressed_sz=%d expected=%zu\n",
+ p->codec_name, it, decompressed_sz, p->input_size);
+ exit_code = 4;
+ break;
+ }
+
+ c_ns[it] = t1 - t0;
+ d_ns[it] = t3 - t2;
+ }
+
+ tsCompressExit();
+
+ if (exit_code != 0) {
+ free(c_ns);
+ free(d_ns);
+ return exit_code;
+ }
+
+ /* Skip warmup samples */
+ double *c_meas = c_ns + p->warmup;
+ double *d_meas = d_ns + p->warmup;
+ int n = p->iters;
+
+ /* Convert to ms for display */
+ double *c_ms_arr = (double *)malloc(sizeof(double) * (size_t)n);
+ double *d_ms_arr = (double *)malloc(sizeof(double) * (size_t)n);
+ if (!c_ms_arr || !d_ms_arr) {
+ free(c_ns); free(d_ns); free(c_ms_arr); free(d_ms_arr);
+ return 1;
+ }
+ for (int i = 0; i < n; i++) {
+ c_ms_arr[i] = ns_to_ms(c_meas[i]);
+ d_ms_arr[i] = ns_to_ms(d_meas[i]);
+ }
+
+ Stats cs = compute_stats(c_ms_arr, n);
+ Stats ds = compute_stats(d_ms_arr, n);
+
+ /* Use mean ns for throughput */
+ double c_mean_ns = 0.0, d_mean_ns = 0.0;
+ for (int i = 0; i < n; i++) { c_mean_ns += c_meas[i]; d_mean_ns += d_meas[i]; }
+ c_mean_ns /= n; d_mean_ns /= n;
+
+ double c_mbps = ns_to_mbs(c_mean_ns, p->input_size);
+ double d_mbps = ns_to_mbs(d_mean_ns, p->input_size);
+
+ /* Compute compression ratio from last iteration */
+ /* Re-run once outside timing to get stable compressed_sz */
+ tsCompressInit("", 0.0f, 0.0, 0, 0, 0, "");
+ int32_t last_csz = compressL2Dict[p->codec_idx].comprFn(
+ (const char *)input, (int32_t)p->input_size,
+ (char *)compressed, (int32_t)out_buf_sz,
+ 0, p->level);
+ tsCompressExit();
+ double ratio = (last_csz > 0) ? ((double)p->input_size / (double)last_csz) : 0.0;
+
+ const char *backend = detect_backend(p->codec_idx);
+
+ double size_mib = (double)p->input_size / (1024.0 * 1024.0);
+ char size_str[32];
+ if (size_mib >= 1.0)
+ snprintf(size_str, sizeof(size_str), "%.0fMiB", size_mib);
+ else
+ snprintf(size_str, sizeof(size_str), "%.0fKiB", size_mib * 1024.0);
+
+ /* stdout summary */
+ printf("BENCH %-6s size=%-7s lvl=%-6s shape=%-10s iters=%d\n"
+ " compress mean=%6.2fms p50=%6.2f p95=%6.2f stdev=%5.2f throughput=%7.1fMB/s\n"
+ " decompress mean=%6.2fms p50=%6.2f p95=%6.2f stdev=%5.2f throughput=%7.1fMB/s\n"
+ " ratio=%.2fx backend=%s\n\n",
+ p->codec_name, size_str, "medium", p->shape_name, n,
+ cs.mean, cs.p50, cs.p95, cs.stdev, c_mbps,
+ ds.mean, ds.p50, ds.p95, ds.stdev, d_mbps,
+ ratio, backend);
+
+ /* CSV append */
+ if (p->csv_path) {
+ FILE *cf = fopen(p->csv_path, "a");
+ if (cf) {
+ csv_ensure_header(cf);
+ time_t now = time(NULL);
+ fprintf(cf,
+ "%lld,%s,%s,%zu,%d,%s,%d,"
+ "%.4f,%.4f,%.4f,%.4f,%.2f,"
+ "%.4f,%.4f,%.4f,%.4f,%.2f,"
+ "%.4f,%s\n",
+ (long long)now, p->label, p->codec_name, p->input_size,
+ (int)p->level, p->shape_name, n,
+ cs.mean, cs.p50, cs.p95, cs.stdev, c_mbps,
+ ds.mean, ds.p50, ds.p95, ds.stdev, d_mbps,
+ ratio, backend);
+ fclose(cf);
+ } else {
+ fprintf(stderr, "warning: cannot open csv file: %s\n", p->csv_path);
+ }
+ }
+
+ free(c_ns); free(d_ns); free(c_ms_arr); free(d_ms_arr);
+ return 0;
+}
+
+/* -------------------------------------------------------------------------
+ * CLI parsing
+ * ---------------------------------------------------------------------- */
+
+static void usage(const char *prog) {
+ fprintf(stderr,
+ "Usage: %s [options]\n"
+ "\n"
+ " --codec {lz4|zlib|zstd|xz|all} default: all\n"
+ " --size default: 1 (try 0.004 for 4KiB)\n"
+ " --iters default: 30\n"
+ " --warmup default: 5\n"
+ " --lvl {low|medium|high} default: medium\n"
+ " --shape {random|repeating|sequential|mixed} default: mixed\n"
+ " --seed default: 42\n"
+ " --csv append CSV row(s)\n"
+ " --label opaque label (default: bench)\n"
+ "\n", prog);
+}
+
+int main(int argc, char **argv) {
+ /* Defaults */
+ int codec_all = 1;
+ int codec_idx = L2_LZ4; /* used when codec_all==0 */
+ const char *codec_name = "lz4";
+ double size_mib = 1.0;
+ int iters = 30;
+ int warmup = 5;
+ uint8_t lvl_enum = L2_LVL_MEDIUM;
+ Shape shape = SHAPE_MIXED;
+ const char *shape_name = "mixed";
+ uint64_t seed = 42ULL;
+ const char *csv_path = NULL;
+ const char *label = "bench";
+
+ /* Hand-rolled option parser */
+ for (int i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
+ usage(argv[0]);
+ return 0;
+ }
+#define NEED_ARG(flag) \
+ do { \
+ if (i + 1 >= argc) { \
+ fprintf(stderr, "error: %s requires an argument\n", flag); \
+ return 1; \
+ } \
+ i++; \
+ } while (0)
+
+ else if (strcmp(argv[i], "--codec") == 0) {
+ NEED_ARG("--codec");
+ const char *v = argv[i];
+ if (strcmp(v, "all") == 0) {
+ codec_all = 1;
+ } else if (strcmp(v, "lz4") == 0) {
+ codec_all = 0; codec_idx = L2_LZ4; codec_name = "lz4";
+ } else if (strcmp(v, "zlib") == 0) {
+ codec_all = 0; codec_idx = L2_ZLIB; codec_name = "zlib";
+ } else if (strcmp(v, "zstd") == 0) {
+ codec_all = 0; codec_idx = L2_ZSTD; codec_name = "zstd";
+ } else if (strcmp(v, "xz") == 0) {
+ codec_all = 0; codec_idx = L2_XZ; codec_name = "xz";
+ } else {
+ fprintf(stderr, "error: unknown codec '%s'\n", v);
+ return 1;
+ }
+ } else if (strcmp(argv[i], "--size") == 0) {
+ NEED_ARG("--size");
+ size_mib = atof(argv[i]);
+ if (size_mib <= 0.0) { fprintf(stderr, "error: --size must be > 0\n"); return 1; }
+ } else if (strcmp(argv[i], "--iters") == 0) {
+ NEED_ARG("--iters");
+ iters = atoi(argv[i]);
+ if (iters < 1) { fprintf(stderr, "error: --iters must be >= 1\n"); return 1; }
+ } else if (strcmp(argv[i], "--warmup") == 0) {
+ NEED_ARG("--warmup");
+ warmup = atoi(argv[i]);
+ if (warmup < 0) { fprintf(stderr, "error: --warmup must be >= 0\n"); return 1; }
+ } else if (strcmp(argv[i], "--lvl") == 0) {
+ NEED_ARG("--lvl");
+ const char *v = argv[i];
+ if (strcmp(v, "low") == 0) lvl_enum = L2_LVL_LOW;
+ else if (strcmp(v, "medium") == 0) lvl_enum = L2_LVL_MEDIUM;
+ else if (strcmp(v, "high") == 0) lvl_enum = L2_LVL_HIGH;
+ else { fprintf(stderr, "error: unknown level '%s'\n", v); return 1; }
+ } else if (strcmp(argv[i], "--shape") == 0) {
+ NEED_ARG("--shape");
+ const char *v = argv[i];
+ if (strcmp(v, "random") == 0) { shape = SHAPE_RANDOM; shape_name = "random"; }
+ else if (strcmp(v, "repeating") == 0) { shape = SHAPE_REPEATING; shape_name = "repeating"; }
+ else if (strcmp(v, "sequential") == 0) { shape = SHAPE_SEQUENTIAL; shape_name = "sequential"; }
+ else if (strcmp(v, "mixed") == 0) { shape = SHAPE_MIXED; shape_name = "mixed"; }
+ else { fprintf(stderr, "error: unknown shape '%s'\n", v); return 1; }
+ } else if (strcmp(argv[i], "--seed") == 0) {
+ NEED_ARG("--seed");
+ seed = (uint64_t)strtoull(argv[i], NULL, 10);
+ } else if (strcmp(argv[i], "--csv") == 0) {
+ NEED_ARG("--csv");
+ csv_path = argv[i];
+ } else if (strcmp(argv[i], "--label") == 0) {
+ NEED_ARG("--label");
+ label = argv[i];
+ } else {
+ fprintf(stderr, "error: unknown option '%s'\n", argv[i]);
+ usage(argv[0]);
+ return 1;
+ }
+#undef NEED_ARG
+ }
+
+ /* Allocate buffers */
+ size_t input_size = (size_t)(size_mib * 1024.0 * 1024.0);
+ if (input_size < 16) input_size = 16;
+ size_t out_buf_sz = input_size * 2 + 1024;
+
+ uint8_t *input = (uint8_t *)malloc(input_size);
+ uint8_t *compressed = (uint8_t *)malloc(out_buf_sz);
+ uint8_t *decompressed= (uint8_t *)malloc(input_size);
+ if (!input || !compressed || !decompressed) {
+ fprintf(stderr, "OOM: cannot allocate %.2f MiB benchmark buffers\n", size_mib);
+ free(input); free(compressed); free(decompressed);
+ return 1;
+ }
+
+ fill_input(input, input_size, shape, seed);
+
+ /* Print header */
+ double size_mib_act = (double)input_size / (1024.0 * 1024.0);
+ printf("compressBench: size=%.4f MiB iters=%d warmup=%d seed=%llu label=%s\n\n",
+ size_mib_act, iters, warmup, (unsigned long long)seed, label);
+
+ /* Codec list for --codec=all */
+ typedef struct { int idx; const char *name; } CodecEntry;
+ static const CodecEntry ALL_CODECS[] = {
+ {L2_LZ4, "lz4"},
+ {L2_ZLIB, "zlib"},
+ {L2_ZSTD, "zstd"},
+ {L2_XZ, "xz"},
+ };
+ static const int N_CODECS = (int)(sizeof(ALL_CODECS) / sizeof(ALL_CODECS[0]));
+
+ int ret = 0;
+ if (codec_all) {
+ for (int c = 0; c < N_CODECS && ret == 0; c++) {
+ int8_t level = tsGetCompressL2Level((uint8_t)ALL_CODECS[c].idx, lvl_enum);
+ BenchParams bp = {
+ .codec_idx = ALL_CODECS[c].idx,
+ .codec_name = ALL_CODECS[c].name,
+ .input_size = input_size,
+ .warmup = warmup,
+ .iters = iters,
+ .level = level,
+ .shape_name = shape_name,
+ .label = label,
+ .csv_path = csv_path,
+ };
+ ret = run_bench(&bp, input, compressed, decompressed);
+ if (ret != 0) {
+ fprintf(stderr, "FAIL: codec=%s rc=%d\n", ALL_CODECS[c].name, ret);
+ }
+ }
+ } else {
+ int8_t level = tsGetCompressL2Level((uint8_t)codec_idx, lvl_enum);
+ BenchParams bp = {
+ .codec_idx = codec_idx,
+ .codec_name = codec_name,
+ .input_size = input_size,
+ .warmup = warmup,
+ .iters = iters,
+ .level = level,
+ .shape_name = shape_name,
+ .label = label,
+ .csv_path = csv_path,
+ };
+ ret = run_bench(&bp, input, compressed, decompressed);
+ if (ret != 0) {
+ fprintf(stderr, "FAIL: codec=%s rc=%d\n", codec_name, ret);
+ }
+ }
+
+ free(input);
+ free(compressed);
+ free(decompressed);
+ return ret;
+}