You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tsfile.apache.org by co...@apache.org on 2026/09/10 10:59:44 UTC
(tsfile) 10/17: docs(cpp): document and benchmark LabVIEW bulk I/O
This is an automated email from the ASF dual-hosted git repository.
ColinLeeo pushed a commit to branch feat/labview-bulk-io
in repository https://gitbox.apache.org/repos/asf/tsfile.git
commit 2824d3b7559e2b363df61b65b6aae4c65f15271b
Author: ColinLee <sh...@163.com>
AuthorDate: Thu Sep 10 17:17:53 2026 +0800
docs(cpp): document and benchmark LabVIEW bulk I/O
---
cpp/src/labview_wrapper/CMakeLists.txt | 10 +
...223\215\344\275\234\346\211\213\345\206\214.md" | 174 ++++++
cpp/src/labview_wrapper/README.md | 42 +-
cpp/src/labview_wrapper/benchmark_block.py | 178 +++---
cpp/src/labview_wrapper/benchmark_codec_matrix.c | 603 +++++++++++++++++++++
cpp/src/labview_wrapper/benchmark_read_block.py | 256 +++++++++
6 files changed, 1180 insertions(+), 83 deletions(-)
diff --git a/cpp/src/labview_wrapper/CMakeLists.txt b/cpp/src/labview_wrapper/CMakeLists.txt
index 2b79e6744..d984fae18 100644
--- a/cpp/src/labview_wrapper/CMakeLists.txt
+++ b/cpp/src/labview_wrapper/CMakeLists.txt
@@ -63,4 +63,14 @@ if (BUILD_LABVIEW_WRAPPER_TESTS)
C_STANDARD 99
C_STANDARD_REQUIRED ON
RUNTIME_OUTPUT_DIRECTORY ${LIBRARY_OUTPUT_PATH})
+
+ add_executable(tsfile_labview_codec_benchmark benchmark_codec_matrix.c)
+ target_include_directories(tsfile_labview_codec_benchmark
+ PRIVATE ${PROJECT_SRC_DIR})
+ target_link_libraries(tsfile_labview_codec_benchmark
+ PRIVATE tsfile_labview tsfile)
+ set_target_properties(tsfile_labview_codec_benchmark PROPERTIES
+ C_STANDARD 99
+ C_STANDARD_REQUIRED ON
+ RUNTIME_OUTPUT_DIRECTORY ${LIBRARY_OUTPUT_PATH})
endif ()
diff --git "a/cpp/src/labview_wrapper/LabVIEW\350\257\273\345\206\231TsFile\346\223\215\344\275\234\346\211\213\345\206\214.md" "b/cpp/src/labview_wrapper/LabVIEW\350\257\273\345\206\231TsFile\346\223\215\344\275\234\346\211\213\345\206\214.md"
new file mode 100644
index 000000000..5a5762f82
--- /dev/null
+++ "b/cpp/src/labview_wrapper/LabVIEW\350\257\273\345\206\231TsFile\346\223\215\344\275\234\346\211\213\345\206\214.md"
@@ -0,0 +1,174 @@
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+
+# LabVIEW 读写 TsFile 操作手册
+
+## 1. 适用范围
+
+本 Wrapper 面向 LabVIEW Call Library Function Node(CLFN),对外只使用
+固定宽度整数、C 字符串、Array Data Pointer 和 U64 不透明句柄。旧版逐行
+接口继续兼容;新增的块写入和块读取适合 INT32、FLOAT、DOUBLE 同类型通道。
+混合类型、TAG、字符串或布尔列仍使用 Tablet Builder 与逐行 ResultSet API。
+
+LabVIEW 进程与动态库必须同位数:32 位 LabVIEW 使用 x86 DLL,64 位
+LabVIEW 使用 x64 DLL。`LV_Handle` 即使在 32 位 LabVIEW 中也配置为 U64。
+
+## 2. CLFN 通用映射
+
+| C 类型 | LabVIEW CLFN 配置 |
+|---|---|
+| `LV_Status` / `int32_t` | Signed 32-bit Integer,by value |
+| `LV_Handle` / `uint64_t` | Unsigned 64-bit Integer;输出时 Pointer to Value |
+| `const char*` | String,C String Pointer |
+| `const int64_t*` / `int64_t*` | 1D I64 Array,Array Data Pointer |
+| `const int32_t*` / `int32_t*` | 1D I32 Array,Array Data Pointer |
+| `const float*` / `float*` | 1D SGL Array,Array Data Pointer |
+| `const double*` / `double*` | 1D DBL Array,Array Data Pointer |
+| `uint8_t*` | 1D U8 Array,Array Data Pointer;可选参数可传空指针 |
+| `int32_t* out_rows` | Signed 32-bit Integer,Pointer to Value |
+
+所有可能失败的调用都检查返回状态:`0` 表示成功,其他值来自 TsFile
+`ERRNO`。输入数组只在一次调用期间被借用,DLL 不保存 LabVIEW 数组地址。
+
+## 3. 连续块写入
+
+推荐初始化顺序:
+
+1. `lv_tsfile_set_global_compression` 和
+ `lv_tsfile_set_datatype_encoding`(如果需要覆盖默认 Codec)。
+2. `lv_tsfile_schema_builder_new` 创建 Schema Builder。
+3. 每列调用 `lv_tsfile_schema_builder_add_column`。
+4. `lv_tsfile_writer_open` 获取 Writer U64 Handle。
+5. 释放 Schema Builder。
+6. 每个采集块调用匹配类型的 `lv_tsfile_write_block_*`。
+7. 可在受控时机调用 `lv_tsfile_writer_flush`;它同步执行且 Writer 仍可继续写。
+8. 文件轮转或停止时调用一次 `lv_tsfile_writer_close`,随后把本地 Handle 置零。
+
+块写入参数:
+
+```c
+lv_tsfile_write_block_f64(writer, ts, data, nrows, ncols);
+```
+
+`ts` 有 `nrows` 个 I64 时间戳。`data` 有 `nrows * ncols` 个元素,采用
+row-major 布局:
+
+```text
+data[row * ncols + column]
+```
+
+例如 3 行、2 通道的扁平数组为
+`[r0c0, r0c1, r1c0, r1c1, r2c0, r2c1]`。Wrapper 内部把它拆分为列数据,
+缓存 Tablet 和暂存区;相同或更小的后续批次不会重新创建 Tablet,只有更大
+批次才扩容。建议从每批 1,000~10,000 行开始,用真实采样数据调优。
+
+## 4. 批量读取
+
+首先创建 batch-mode ResultSet:
+
+```c
+lv_tsfile_query_table_batch(reader, table, "x\ny\nz",
+ start_time, end_time, batch_rows, &result_set);
+```
+
+列字符串用实际 LF 字节分隔,不包含时间戳列。然后预分配:
+
+- `out_ts`:至少 `capacity_rows` 个 I64;
+- `out_data`:至少 `capacity_rows * ncols` 个对应数值;
+- `out_is_null`:可选,至少 `capacity_rows * ncols` 个 U8;
+- `out_rows`:一个 I32 输出值。
+
+读取 DOUBLE 示例:
+
+```c
+lv_tsfile_rs_read_block_f64(result_set, out_ts, out_data, out_is_null,
+ capacity_rows, ncols, &out_rows);
+```
+
+`out_data` 同样是 row-major。`out_is_null[index] == 1` 表示 null,此时
+`out_data[index]` 确定性写为零;不关心 null 时可传空指针。返回状态为 0 且
+`out_rows == 0` 表示 EOF,不是错误。
+
+`capacity_rows` 必须大于等于创建查询时的 `batch_rows`。容量不足会在获取
+下一 TsBlock 之前返回错误,因此修正容量后仍能读取原来的下一批。不要在
+batch ResultSet 上调用 `lv_tsfile_rs_next`/标量 Getter,也不要在 row-mode
+ResultSet 上调用块读取函数。批量路径仅支持所选值列全部为同一种 i32、f32
+或 f64;混合类型查询使用旧逐行接口。
+
+读取结束后依次调用 `lv_tsfile_rs_free` 和 `lv_tsfile_reader_close`。
+
+## 5. 为什么通道不多,close 仍可能阻塞
+
+通道数只影响工作量的一部分。`writer_close` 还可能执行:
+
+- 把尚未达到内存阈值的 Page/Chunk 编码并写出;
+- 汇总 Chunk/ChunkGroup 元数据;
+- 构建或序列化文件索引、统计信息、Bloom Filter 和 footer;
+- 触发运行库及文件系统写入,耗时会受磁盘、杀毒软件和系统缓存影响。
+
+因此,即使设备和通道较少,只要单文件累计时间长、页/Chunk 多,或大量数据
+仍留在内存,close 都可能出现数百毫秒尾延迟。显式 flush 可把数据页落盘与
+最终 close 分开观察,但 flush 本身也是同步调用,而且 close 仍需写最终索引。
+
+采集程序使用单一 Writer Owner:
+
+```text
+DAQ 采集循环 ──> 有界 Queue/FIFO ──> TsFile 写入循环
+ ├─ block write
+ ├─ 可控 flush
+ └─ 文件轮转与 close
+```
+
+DAQ 循环只负责采集和入队,不能直接 close。Queue 必须有明确容量、溢出策略
+和水位监控;Writer Handle 只在写入循环使用,避免并发 write/flush/close。
+
+## 6. 性能测量
+
+先执行小规模自检:
+
+```bash
+python3 cpp/src/labview_wrapper/benchmark_block.py --smoke
+python3 cpp/src/labview_wrapper/benchmark_read_block.py --smoke
+```
+
+正式对比:
+
+```bash
+python3 cpp/src/labview_wrapper/benchmark_block.py \
+ --rows 10000 --cols 9 --batches 30 --repeats 3
+python3 cpp/src/labview_wrapper/benchmark_read_block.py \
+ --rows 150000 --cols 9 --batch-rows 10000 --repeats 3
+```
+
+写基准分别报告宿主数组准备、block write、flush 和 close;读基准分别报告
+旧逐行耗时、批量 DLL 解码/复制调用耗时和宿主遍历耗时。基准会校验实际生效
+的 Codec,并验证逐行与批量结果一致。Codec 选择必须用真实设备数据比较:
+合成正弦数据、噪声数据与现场振动波形的压缩率和 CPU 开销可能完全不同。
+
+## 7. 文件与错误处理
+
+- 输出目录必须存在;Writer 使用新文件路径,轮转时生成唯一文件名。
+- Handle 为 0 表示无效。成功 close/free 后立即把 LabVIEW Shift Register
+ 中的 Handle 清零。
+- 输出 Handle 和 `out_rows` 在失败路径保持为零,不能继续使用旧值。
+- 同一 Writer、Reader 或 ResultSet Handle 的调用由调用方串行化。
+- 出错时先停止继续写入,记录状态码;仍持有有效 Writer 时在写入线程中执行
+ 清理,不能让 C++ 异常或资源所有权越过 CLFN 边界。
diff --git a/cpp/src/labview_wrapper/README.md b/cpp/src/labview_wrapper/README.md
index 54089e2fc..b15ebbc5d 100644
--- a/cpp/src/labview_wrapper/README.md
+++ b/cpp/src/labview_wrapper/README.md
@@ -31,6 +31,9 @@ Library Function Node (CLFN).
- Continuous acquisition: open one writer, call
`lv_tsfile_write_block_f64` once per acquisition block, and close the writer
when the file rolls over.
+- Homogeneous numeric reads: create a batch result set with
+ `lv_tsfile_query_table_batch`, then call the matching
+ `lv_tsfile_rs_read_block_i32/f32/f64` function until it returns zero rows.
- Existing mixed-type applications: use the schema, writer, and tablet builder
functions in `tsfile_labview.h`.
@@ -85,10 +88,39 @@ The streaming sequence is:
3. Call `lv_tsfile_write_block_f64` for each block. Configure `writer` as U64
by value, `ts` as 1D I64 Array Data Pointer, and `data` as 1D DBL Array Data
Pointer.
-4. Call `lv_tsfile_writer_close` exactly once.
+4. Optionally call `lv_tsfile_writer_flush` at a controlled point. It is
+ synchronous and leaves the writer open.
+5. Call `lv_tsfile_writer_close` exactly once.
-The block call creates and releases its temporary tablet. The caller retains
-ownership of both input arrays.
+The writer caches and reuses its internal tablet and typed deinterleave buffer;
+only a larger batch grows them. The caller retains ownership of both input
+arrays, and the wrapper never stores their addresses after the call returns.
+
+## Batch reads
+
+Pass only homogeneous `INT32`, `FLOAT`, or `DOUBLE` value columns to
+`lv_tsfile_query_table_batch`. Allocate `batch_rows` I64 timestamps and
+`batch_rows * ncols` typed values, then call the matching block-read function.
+Values use the same row-major indexing as writes. The optional byte-per-cell
+null array uses `0` for valid and `1` for null; null numeric values are written
+as zero. Status `0` with `out_rows == 0` means EOF.
+
+`capacity_rows` must be at least the `batch_rows` used to create the query.
+This is checked before the next TsBlock is consumed. Row-mode scalar getters
+and batch-mode block functions must not be mixed on one result-set handle.
+
+For exact LabVIEW CLFN mappings and acquisition-loop guidance, see
+[LabVIEW读写TsFile操作手册.md](LabVIEW读写TsFile操作手册.md).
+
+## Acquisition and file rotation
+
+`flush` and `close` are synchronous. File size is not the only factor in close
+latency: the writer may still need to encode buffered pages and serialize
+chunk metadata, indexes, Bloom filters, footer data, and filesystem writes.
+Keep acquisition timing isolated with a Producer/Consumer design: the DAQ loop
+enqueues blocks, while one writer loop owns write, flush, rotation, and close.
+Typical starting batch sizes are 1,000 to 10,000 rows; benchmark real device
+data before selecting codecs or rotation thresholds.
## Build and package
@@ -141,4 +173,8 @@ dist\TsFile-LabVIEW-x86\examples\run_block_test.cmd
python cpp/src/labview_wrapper/benchmark_block.py \
--dll /path/to/libtsfile_labview.so \
--rows 10000 --cols 9 --batches 3 --repeats 5
+
+python cpp/src/labview_wrapper/benchmark_read_block.py \
+ --dll /path/to/libtsfile_labview.so \
+ --rows 150000 --cols 9 --batch-rows 10000
```
diff --git a/cpp/src/labview_wrapper/benchmark_block.py b/cpp/src/labview_wrapper/benchmark_block.py
index 3d830749f..a06e9c61e 100644
--- a/cpp/src/labview_wrapper/benchmark_block.py
+++ b/cpp/src/labview_wrapper/benchmark_block.py
@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-"""Compare per-cell FFI calls with one-call LabVIEW block writes."""
+"""Benchmark legacy per-cell writes against reusable numeric block writes."""
from __future__ import annotations
@@ -24,6 +24,7 @@ import ctypes
import math
import os
import statistics
+import sys
import time
from pathlib import Path
@@ -33,24 +34,22 @@ LV_ENCODING_GORILLA = 8
LV_COMPRESSION_LZ4 = 7
+def default_library() -> Path:
+ name = {
+ "darwin": "libtsfile_labview.dylib",
+ "win32": "tsfile_labview.dll",
+ }.get(sys.platform, "libtsfile_labview.so")
+ return Path(__file__).resolve().parents[2] / "target" / "build" / "lib" / name
+
+
def bind(dll_path: Path) -> ctypes.CDLL:
- dll_directory_handles = []
+ directory_handles = []
if os.name == "nt":
- search_dirs = [dll_path.parent.resolve()]
- search_dirs.extend(
- Path(entry)
- for entry in os.environ.get("PATH", "").split(os.pathsep)
- if entry
- )
- for directory in search_dirs:
- if directory.is_dir():
- dll_directory_handles.append(
- os.add_dll_directory(str(directory.resolve()))
- )
+ directory_handles.append(os.add_dll_directory(str(dll_path.parent.resolve())))
try:
lib = ctypes.CDLL(str(dll_path.resolve()))
finally:
- for handle in dll_directory_handles:
+ for handle in directory_handles:
handle.close()
handle = ctypes.c_uint64
@@ -59,6 +58,9 @@ def bind(dll_path: Path) -> ctypes.CDLL:
lib.lv_tsfile_set_global_compression.restype = status
lib.lv_tsfile_set_datatype_encoding.argtypes = [ctypes.c_uint8, ctypes.c_uint8]
lib.lv_tsfile_set_datatype_encoding.restype = status
+ lib.get_global_compression.restype = ctypes.c_uint8
+ lib.get_datatype_encoding.argtypes = [ctypes.c_uint8]
+ lib.get_datatype_encoding.restype = ctypes.c_uint8
lib.lv_tsfile_schema_builder_new.argtypes = [ctypes.c_char_p]
lib.lv_tsfile_schema_builder_new.restype = handle
lib.lv_tsfile_schema_builder_add_column.argtypes = [
@@ -69,7 +71,6 @@ def bind(dll_path: Path) -> ctypes.CDLL:
]
lib.lv_tsfile_schema_builder_add_column.restype = status
lib.lv_tsfile_schema_builder_free.argtypes = [handle]
-
lib.lv_tsfile_writer_open.argtypes = [
ctypes.c_char_p,
handle,
@@ -87,16 +88,13 @@ def bind(dll_path: Path) -> ctypes.CDLL:
ctypes.c_int32,
]
lib.lv_tsfile_write_block_f64.restype = status
+ lib.lv_tsfile_writer_flush.argtypes = [handle]
+ lib.lv_tsfile_writer_flush.restype = status
lib.lv_tsfile_writer_close.argtypes = [handle]
lib.lv_tsfile_writer_close.restype = status
-
lib.lv_tsfile_tablet_new.argtypes = [ctypes.c_uint32]
lib.lv_tsfile_tablet_new.restype = handle
- lib.lv_tsfile_tablet_add_column.argtypes = [
- handle,
- ctypes.c_char_p,
- ctypes.c_uint8,
- ]
+ lib.lv_tsfile_tablet_add_column.argtypes = [handle, ctypes.c_char_p, ctypes.c_uint8]
lib.lv_tsfile_tablet_add_column.restype = status
lib.lv_tsfile_tablet_finalize_columns.argtypes = [handle]
lib.lv_tsfile_tablet_finalize_columns.restype = status
@@ -122,7 +120,8 @@ def check(status: int, operation: str) -> None:
raise RuntimeError(f"{operation} failed with status {status}")
-def open_writer(lib: ctypes.CDLL, path: Path, columns: list[bytes]) -> ctypes.c_uint64:
+def open_writer(lib: ctypes.CDLL, path: Path, columns: list[bytes]) -> int:
+ path.unlink(missing_ok=True)
builder = lib.lv_tsfile_schema_builder_new(b"vib")
if not builder:
raise RuntimeError("schema builder creation failed")
@@ -134,20 +133,20 @@ def open_writer(lib: ctypes.CDLL, path: Path, columns: list[bytes]) -> ctypes.c_
),
"schema_builder_add_column",
)
- path.unlink(missing_ok=True)
writer = ctypes.c_uint64()
check(
lib.lv_tsfile_writer_open(
- os.fsencode(path), builder, 128 * 1024 * 1024, ctypes.byref(writer)
+ os.fsencode(path), builder, 0, ctypes.byref(writer)
),
"writer_open",
)
- return writer
+ return writer.value
finally:
lib.lv_tsfile_schema_builder_free(builder)
def make_batches(rows: int, cols: int, count: int):
+ started = time.perf_counter()
timestamp_type = ctypes.c_int64 * rows
data_type = ctypes.c_double * (rows * cols)
batches = []
@@ -162,17 +161,10 @@ def make_batches(rows: int, cols: int, count: int):
)
)
batches.append((timestamps, data))
- return batches
+ return batches, time.perf_counter() - started
-def run_cell(
- lib: ctypes.CDLL,
- path: Path,
- columns: list[bytes],
- batches,
- rows: int,
- cols: int,
-) -> float:
+def run_cell(lib, path, columns, batches, rows, cols) -> float:
started = time.perf_counter()
writer = open_writer(lib, path, columns)
for timestamps, data in batches:
@@ -185,76 +177,99 @@ def run_cell(
lib.lv_tsfile_tablet_add_column(tablet, name, LV_TYPE_DOUBLE),
"tablet_add_column",
)
- check(
- lib.lv_tsfile_tablet_finalize_columns(tablet),
- "tablet_finalize_columns",
- )
+ check(lib.lv_tsfile_tablet_finalize_columns(tablet), "tablet_finalize")
for row in range(rows):
check(
lib.lv_tsfile_tablet_set_timestamp(tablet, row, timestamps[row]),
"tablet_set_timestamp",
)
- row_offset = row * cols
for col in range(cols):
check(
lib.lv_tsfile_tablet_set_f64(
- tablet, row, col, data[row_offset + col]
+ tablet, row, col, data[row * cols + col]
),
"tablet_set_f64",
)
check(lib.lv_tsfile_writer_write(writer, tablet), "writer_write")
finally:
lib.lv_tsfile_tablet_free(tablet)
+ check(lib.lv_tsfile_writer_flush(writer), "writer_flush")
check(lib.lv_tsfile_writer_close(writer), "writer_close")
return time.perf_counter() - started
-def run_block(
- lib: ctypes.CDLL,
- path: Path,
- columns: list[bytes],
- batches,
- rows: int,
- cols: int,
-) -> float:
- started = time.perf_counter()
+def run_block(lib, path, columns, batches, rows, cols) -> dict[str, float]:
+ opened = time.perf_counter()
writer = open_writer(lib, path, columns)
+ open_seconds = time.perf_counter() - opened
+
+ started = time.perf_counter()
for timestamps, data in batches:
check(
lib.lv_tsfile_write_block_f64(writer, timestamps, data, rows, cols),
"write_block_f64",
)
+ writer_seconds = time.perf_counter() - started
+
+ started = time.perf_counter()
+ check(lib.lv_tsfile_writer_flush(writer), "writer_flush")
+ flush_seconds = time.perf_counter() - started
+
+ started = time.perf_counter()
check(lib.lv_tsfile_writer_close(writer), "writer_close")
- return time.perf_counter() - started
+ close_seconds = time.perf_counter() - started
+ return {
+ "open_seconds": open_seconds,
+ "writer_seconds": writer_seconds,
+ "flush_seconds": flush_seconds,
+ "close_seconds": close_seconds,
+ }
+
+
+def median(samples: list[dict[str, float]], key: str) -> float:
+ return statistics.median(sample[key] for sample in samples)
def main() -> None:
parser = argparse.ArgumentParser()
- parser.add_argument("--dll", type=Path, required=True)
- parser.add_argument("--rows", type=int, default=2_000)
+ parser.add_argument("--dll", type=Path, default=default_library())
+ parser.add_argument("--rows", type=int, default=10_000)
parser.add_argument("--cols", type=int, default=9)
- parser.add_argument("--batches", type=int, default=3)
+ parser.add_argument("--batches", type=int, default=30)
parser.add_argument("--repeats", type=int, default=3)
parser.add_argument("--output-dir", type=Path, default=Path.cwd())
+ parser.add_argument("--smoke", action="store_true")
args = parser.parse_args()
- if args.rows <= 0 or args.cols <= 0 or args.batches <= 0 or args.repeats <= 0:
+ if args.smoke:
+ args.rows, args.cols, args.batches, args.repeats = 100, 3, 2, 2
+ if min(args.rows, args.cols, args.batches, args.repeats) <= 0:
parser.error("rows, cols, batches, and repeats must be positive")
args.output_dir.mkdir(parents=True, exist_ok=True)
lib = bind(args.dll)
- check(
- lib.lv_tsfile_set_global_compression(LV_COMPRESSION_LZ4),
- "set_global_compression(LZ4)",
- )
+ check(lib.lv_tsfile_set_global_compression(LV_COMPRESSION_LZ4), "set LZ4")
check(
lib.lv_tsfile_set_datatype_encoding(LV_TYPE_DOUBLE, LV_ENCODING_GORILLA),
- "set_datatype_encoding(DOUBLE, GORILLA)",
+ "set GORILLA",
)
- columns = [f"ch_{col}".encode() for col in range(args.cols)]
- batches = make_batches(args.rows, args.cols, args.batches)
+ effective_compression = int(lib.get_global_compression())
+ effective_encoding = int(lib.get_datatype_encoding(LV_TYPE_DOUBLE))
+ if (
+ effective_compression != LV_COMPRESSION_LZ4
+ or effective_encoding != LV_ENCODING_GORILLA
+ ):
+ raise RuntimeError(
+ "effective codec configuration differs from requested values"
+ )
+ columns = [f"ch_{col}".encode() for col in range(args.cols)]
+ batches, materialize_seconds = make_batches(args.rows, args.cols, args.batches)
cell_path = args.output_dir / "lv_cell_benchmark.tsfile"
block_path = args.output_dir / "lv_block_benchmark.tsfile"
+ warmup_path = args.output_dir / "lv_block_benchmark_warmup.tsfile"
+ run_block(lib, warmup_path, columns, batches[:1], args.rows, args.cols)
+ warmup_path.unlink(missing_ok=True)
+
cell_samples = []
block_samples = []
for repeat in range(args.repeats):
@@ -273,32 +288,35 @@ def main() -> None:
run_cell(lib, cell_path, columns, batches, args.rows, args.cols)
)
- cell_seconds = statistics.median(cell_samples)
- block_seconds = statistics.median(block_samples)
+ phases = {
+ "materialize_seconds": materialize_seconds,
+ "writer_seconds": median(block_samples, "writer_seconds"),
+ "flush_seconds": median(block_samples, "flush_seconds"),
+ "close_seconds": median(block_samples, "close_seconds"),
+ }
+ if args.smoke and any(value <= 0 for value in phases.values()):
+ raise RuntimeError(f"non-positive smoke phase: {phases}")
+ cell_seconds = statistics.median(cell_samples)
+ block_seconds = sum(
+ median(block_samples, key)
+ for key in ("open_seconds", "writer_seconds", "flush_seconds", "close_seconds")
+ )
points = args.rows * args.cols * args.batches
- value_bytes = points * ctypes.sizeof(ctypes.c_double)
- cell_calls = args.batches * (args.rows * (args.cols + 1) + args.cols + 4)
- block_calls = args.batches
print(
f"rows={args.rows} cols={args.cols} batches={args.batches} "
- f"repeats={args.repeats} points={points} value_bytes={value_bytes}"
- )
- print("cell samples : " + ", ".join(f"{sample:.6f}s" for sample in cell_samples))
- print("block samples: " + ", ".join(f"{sample:.6f}s" for sample in block_samples))
- print(
- f"cell : {cell_seconds:.6f}s, {points / cell_seconds:,.0f} points/s, "
- f"{value_bytes / cell_seconds / (1024 * 1024):,.2f} MiB/s values, "
- f"~{cell_calls:,} FFI calls in batch loop, "
- f"{cell_path.stat().st_size:,} file bytes"
+ f"repeats={args.repeats} points={points}"
)
print(
- f"block: {block_seconds:.6f}s, {points / block_seconds:,.0f} points/s, "
- f"{value_bytes / block_seconds / (1024 * 1024):,.2f} MiB/s values, "
- f"{block_calls:,} FFI calls in batch loop, "
- f"{block_path.stat().st_size:,} file bytes"
+ f"effective_value_encoding={effective_encoding} "
+ f"effective_compression={effective_compression}"
)
- print(f"speedup: {cell_seconds / block_seconds:.2f}x")
+ for key, value in phases.items():
+ print(f"{key}={value:.9f}")
+ print(f"legacy_cell_seconds={cell_seconds:.9f}")
+ print(f"block_total_seconds={block_seconds:.9f}")
+ print(f"block_speedup={cell_seconds / block_seconds:.2f}x")
+ print(f"output_bytes={block_path.stat().st_size}")
if __name__ == "__main__":
diff --git a/cpp/src/labview_wrapper/benchmark_codec_matrix.c b/cpp/src/labview_wrapper/benchmark_codec_matrix.c
new file mode 100644
index 000000000..818237526
--- /dev/null
+++ b/cpp/src/labview_wrapper/benchmark_codec_matrix.c
@@ -0,0 +1,603 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Reproducible Windows-oriented benchmark for separating TsFile value
+ * encoding, value compression, timestamp encoding/compression, and the
+ * LabVIEW wrapper's block materialization cost.
+ */
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#ifdef _WIN32
+/* The TsFile C API names its opaque file handle WriteFile. Rename the Win32
+ * function declaration while including windows.h to avoid a C identifier
+ * collision; this benchmark does not call the Win32 WriteFile function. */
+#define WriteFile Win32WriteFile
+#include <windows.h>
+#undef WriteFile
+#else
+#include <time.h>
+#endif
+
+#include "cwrapper/tsfile_cwrapper.h"
+#include "tsfile_labview.h"
+
+typedef enum BenchmarkMode {
+ MODE_WRAPPER,
+ MODE_CORE,
+} BenchmarkMode;
+
+typedef struct PhaseTimes {
+ double open_seconds;
+ double prepare_seconds;
+ double tablet_new_seconds;
+ double tablet_fill_seconds;
+ double writer_write_seconds;
+ double tablet_free_seconds;
+ double wrapper_block_seconds;
+ double buffer_free_seconds;
+ double flush_seconds;
+ double close_seconds;
+ double write_seconds;
+ double full_seconds;
+ double process_cpu_seconds;
+ uint64_t process_write_bytes;
+} PhaseTimes;
+
+static double monotonic_seconds(void) {
+#ifdef _WIN32
+ LARGE_INTEGER counter;
+ LARGE_INTEGER frequency;
+ QueryPerformanceFrequency(&frequency);
+ QueryPerformanceCounter(&counter);
+ return (double)counter.QuadPart / (double)frequency.QuadPart;
+#else
+ struct timespec value;
+ clock_gettime(CLOCK_MONOTONIC, &value);
+ return (double)value.tv_sec + (double)value.tv_nsec / 1000000000.0;
+#endif
+}
+
+static double process_cpu_seconds(void) {
+#ifdef _WIN32
+ FILETIME creation;
+ FILETIME exit;
+ FILETIME kernel;
+ FILETIME user;
+ ULARGE_INTEGER kernel_value;
+ ULARGE_INTEGER user_value;
+ if (!GetProcessTimes(GetCurrentProcess(), &creation, &exit, &kernel,
+ &user)) {
+ return 0.0;
+ }
+ kernel_value.LowPart = kernel.dwLowDateTime;
+ kernel_value.HighPart = kernel.dwHighDateTime;
+ user_value.LowPart = user.dwLowDateTime;
+ user_value.HighPart = user.dwHighDateTime;
+ return (double)(kernel_value.QuadPart + user_value.QuadPart) / 10000000.0;
+#else
+ return (double)clock() / (double)CLOCKS_PER_SEC;
+#endif
+}
+
+static uint64_t process_write_bytes(void) {
+#ifdef _WIN32
+ IO_COUNTERS counters;
+ if (GetProcessIoCounters(GetCurrentProcess(), &counters)) {
+ return (uint64_t)counters.WriteTransferCount;
+ }
+#endif
+ return 0;
+}
+
+static int check_status(int status, const char* operation) {
+ if (status != 0) {
+ fprintf(stderr, "%s failed with status %d\n", operation, status);
+ return 0;
+ }
+ return 1;
+}
+
+static int parse_mode(const char* text, BenchmarkMode* mode) {
+ if (strcmp(text, "wrapper") == 0) {
+ *mode = MODE_WRAPPER;
+ return 1;
+ }
+ if (strcmp(text, "core") == 0) {
+ *mode = MODE_CORE;
+ return 1;
+ }
+ return 0;
+}
+
+static int parse_encoding(const char* text, uint8_t* encoding) {
+ if (strcmp(text, "plain") == 0) {
+ *encoding = TS_ENCODING_PLAIN;
+ return 1;
+ }
+ if (strcmp(text, "ts2diff") == 0) {
+ *encoding = TS_ENCODING_TS_2DIFF;
+ return 1;
+ }
+ if (strcmp(text, "gorilla") == 0) {
+ *encoding = TS_ENCODING_GORILLA;
+ return 1;
+ }
+ return 0;
+}
+
+static int parse_compression(const char* text, uint8_t* compression) {
+ if (strcmp(text, "uncompressed") == 0) {
+ *compression = TS_COMPRESSION_UNCOMPRESSED;
+ return 1;
+ }
+ if (strcmp(text, "lz4") == 0) {
+ *compression = TS_COMPRESSION_LZ4;
+ return 1;
+ }
+ return 0;
+}
+
+static void free_column_names(char** names, int32_t count) {
+ int32_t i;
+ if (names == NULL) {
+ return;
+ }
+ for (i = 0; i < count; ++i) {
+ free(names[i]);
+ }
+ free(names);
+}
+
+static char** make_column_names(int32_t count) {
+ char** names = (char**)calloc((size_t)count, sizeof(char*));
+ int32_t i;
+ if (names == NULL) {
+ return NULL;
+ }
+ for (i = 0; i < count; ++i) {
+ names[i] = (char*)malloc(32);
+ if (names[i] == NULL) {
+ free_column_names(names, count);
+ return NULL;
+ }
+ if (count == 1) {
+ snprintf(names[i], 32, "channel");
+ } else {
+ snprintf(names[i], 32, "ch_%d", (int)i);
+ }
+ }
+ return names;
+}
+
+static int configure_codecs(uint8_t value_encoding, uint8_t value_compression,
+ uint8_t time_encoding, uint8_t time_compression) {
+ return check_status(
+ set_datatype_encoding(TS_DATATYPE_DOUBLE, value_encoding),
+ "set_datatype_encoding") &&
+ check_status(set_global_compression(value_compression),
+ "set_global_compression") &&
+ check_status(set_global_time_encoding(time_encoding),
+ "set_global_time_encoding") &&
+ check_status(set_global_time_compression(time_compression),
+ "set_global_time_compression");
+}
+
+static LV_Handle wrapper_open(const char* output_path, char** column_names,
+ int32_t ncols) {
+ LV_Handle builder = lv_tsfile_schema_builder_new("vib");
+ LV_Handle writer = 0;
+ int32_t col;
+ if (builder == 0) {
+ return 0;
+ }
+ for (col = 0; col < ncols; ++col) {
+ if (!check_status(
+ lv_tsfile_schema_builder_add_column(
+ builder, column_names[col], LV_TYPE_DOUBLE, LV_CAT_FIELD),
+ "schema_builder_add_column")) {
+ lv_tsfile_schema_builder_free(builder);
+ return 0;
+ }
+ }
+ if (!check_status(lv_tsfile_writer_open(output_path, builder, 0, &writer),
+ "writer_open")) {
+ lv_tsfile_schema_builder_free(builder);
+ return 0;
+ }
+ lv_tsfile_schema_builder_free(builder);
+ return writer;
+}
+
+static TsFileWriter core_open(const char* output_path, char** column_names,
+ int32_t ncols, WriteFile* out_file) {
+ ColumnSchema* columns = NULL;
+ TableSchema schema;
+ TsFileWriter writer = NULL;
+ ERRNO status = 0;
+ int32_t col;
+
+ *out_file = write_file_new(output_path, &status);
+ if (*out_file == NULL || status != 0) {
+ fprintf(stderr, "write_file_new failed with status %d\n", status);
+ return NULL;
+ }
+ columns = (ColumnSchema*)calloc((size_t)ncols, sizeof(ColumnSchema));
+ if (columns == NULL) {
+ free_write_file(out_file);
+ return NULL;
+ }
+ for (col = 0; col < ncols; ++col) {
+ columns[col].column_name = column_names[col];
+ columns[col].data_type = TS_DATATYPE_DOUBLE;
+ columns[col].column_category = FIELD;
+ }
+ schema.table_name = "vib";
+ schema.column_schemas = columns;
+ schema.column_num = ncols;
+ writer = tsfile_writer_new(*out_file, &schema, &status);
+ free(columns);
+ if (writer == NULL || status != 0) {
+ fprintf(stderr, "tsfile_writer_new failed with status %d\n", status);
+ free_write_file(out_file);
+ return NULL;
+ }
+ return writer;
+}
+
+int main(int argc, char** argv) {
+ BenchmarkMode mode;
+ const char* output_path;
+ uint64_t total_value_bytes;
+ uint64_t block_value_bytes;
+ int32_t ncols;
+ const char* raw_block_path;
+ uint8_t value_encoding;
+ uint8_t value_compression;
+ uint8_t time_encoding;
+ uint8_t time_compression;
+ uint64_t values_per_block;
+ int32_t rows_per_block;
+ uint64_t block_count;
+ uint64_t total_rows;
+ double* source_values = NULL;
+ FILE* source = NULL;
+ char** column_names = NULL;
+ TSDataType* column_types = NULL;
+ PhaseTimes phases;
+ LV_Handle wrapper_writer = 0;
+ WriteFile core_file = NULL;
+ TsFileWriter core_writer = NULL;
+ double full_started;
+ double write_started;
+ double cpu_started;
+ uint64_t io_started;
+ uint64_t block;
+ struct stat file_info;
+ int exit_code = 1;
+
+ memset(&phases, 0, sizeof(phases));
+ if (argc != 11) {
+ fprintf(stderr,
+ "usage: %s <wrapper|core> <output.tsfile> "
+ "<total-value-bytes> <block-value-bytes> <ncols> "
+ "<raw-block.bin> <value-encoding> <value-compression> "
+ "<time-encoding> <time-compression>\n",
+ argv[0]);
+ return 2;
+ }
+ if (!parse_mode(argv[1], &mode) ||
+ !parse_encoding(argv[7], &value_encoding) ||
+ !parse_compression(argv[8], &value_compression) ||
+ !parse_encoding(argv[9], &time_encoding) ||
+ !parse_compression(argv[10], &time_compression)) {
+ fprintf(stderr, "invalid mode, encoding, or compression name\n");
+ return 2;
+ }
+ output_path = argv[2];
+ total_value_bytes = strtoull(argv[3], NULL, 10);
+ block_value_bytes = strtoull(argv[4], NULL, 10);
+ ncols = (int32_t)strtol(argv[5], NULL, 10);
+ raw_block_path = argv[6];
+ if (total_value_bytes == 0 || block_value_bytes == 0 || ncols <= 0 ||
+ total_value_bytes % block_value_bytes != 0 ||
+ block_value_bytes % sizeof(double) != 0) {
+ fprintf(stderr, "invalid byte counts or column count\n");
+ return 2;
+ }
+ values_per_block = block_value_bytes / sizeof(double);
+ if (values_per_block % (uint64_t)ncols != 0 ||
+ values_per_block / (uint64_t)ncols > INT32_MAX) {
+ fprintf(stderr, "block size is not divisible into rows and columns\n");
+ return 2;
+ }
+ rows_per_block = (int32_t)(values_per_block / (uint64_t)ncols);
+ block_count = total_value_bytes / block_value_bytes;
+ total_rows = (uint64_t)rows_per_block * block_count;
+ if (total_rows > INT64_MAX) {
+ fprintf(stderr, "timestamp range is too large\n");
+ return 2;
+ }
+
+ source = fopen(raw_block_path, "rb");
+ source_values = (double*)malloc((size_t)block_value_bytes);
+ if (source == NULL || source_values == NULL ||
+ fread(source_values, 1, (size_t)block_value_bytes, source) !=
+ (size_t)block_value_bytes ||
+ fgetc(source) != EOF) {
+ fprintf(stderr, "could not read an exact raw source block\n");
+ if (source != NULL) {
+ fclose(source);
+ }
+ free(source_values);
+ return 2;
+ }
+ fclose(source);
+ source = NULL;
+
+ column_names = make_column_names(ncols);
+ column_types = (TSDataType*)malloc((size_t)ncols * sizeof(TSDataType));
+ if (column_names == NULL || column_types == NULL) {
+ fprintf(stderr, "column allocation failed\n");
+ goto cleanup;
+ }
+ for (int32_t col = 0; col < ncols; ++col) {
+ column_types[col] = TS_DATATYPE_DOUBLE;
+ }
+ if (!configure_codecs(value_encoding, value_compression, time_encoding,
+ time_compression)) {
+ goto cleanup;
+ }
+ if (get_datatype_encoding(TS_DATATYPE_DOUBLE) != value_encoding ||
+ get_global_compression() != value_compression ||
+ get_global_time_encoding() != time_encoding ||
+ get_global_time_compression() != time_compression) {
+ fprintf(stderr, "effective codec configuration differs from request\n");
+ goto cleanup;
+ }
+
+ remove(output_path);
+ full_started = monotonic_seconds();
+ {
+ const double started = monotonic_seconds();
+ if (mode == MODE_WRAPPER) {
+ wrapper_writer = wrapper_open(output_path, column_names, ncols);
+ if (wrapper_writer == 0) {
+ goto cleanup;
+ }
+ } else {
+ core_writer =
+ core_open(output_path, column_names, ncols, &core_file);
+ if (core_writer == NULL) {
+ goto cleanup;
+ }
+ }
+ phases.open_seconds = monotonic_seconds() - started;
+ }
+
+ write_started = monotonic_seconds();
+ cpu_started = process_cpu_seconds();
+ io_started = process_write_bytes();
+ for (block = 0; block < block_count; ++block) {
+ int64_t* timestamps = NULL;
+ double* values = NULL;
+ const int64_t base = (int64_t)(block * (uint64_t)rows_per_block);
+ double started = monotonic_seconds();
+ int32_t row;
+ int32_t col;
+
+ timestamps = (int64_t*)malloc((size_t)rows_per_block * sizeof(int64_t));
+ values = (double*)malloc((size_t)block_value_bytes);
+ if (timestamps == NULL || values == NULL) {
+ free(timestamps);
+ free(values);
+ fprintf(stderr, "block buffer allocation failed\n");
+ goto cleanup;
+ }
+ memcpy(values, source_values, (size_t)block_value_bytes);
+ for (row = 0; row < rows_per_block; ++row) {
+ timestamps[row] = base + row;
+ }
+ phases.prepare_seconds += monotonic_seconds() - started;
+
+ if (mode == MODE_WRAPPER) {
+ started = monotonic_seconds();
+ if (!check_status(
+ lv_tsfile_write_block_f64(wrapper_writer, timestamps,
+ values, rows_per_block, ncols),
+ "lv_tsfile_write_block_f64")) {
+ free(timestamps);
+ free(values);
+ goto cleanup;
+ }
+ phases.wrapper_block_seconds += monotonic_seconds() - started;
+ } else {
+ Tablet tablet;
+ started = monotonic_seconds();
+ tablet = tablet_new(column_names, column_types, (uint32_t)ncols,
+ (uint32_t)rows_per_block);
+ phases.tablet_new_seconds += monotonic_seconds() - started;
+ if (tablet == NULL) {
+ free(timestamps);
+ free(values);
+ fprintf(stderr, "tablet_new failed\n");
+ goto cleanup;
+ }
+
+ started = monotonic_seconds();
+ for (row = 0; row < rows_per_block; ++row) {
+ const size_t offset = (size_t)row * (size_t)ncols;
+ if (!check_status(tablet_add_timestamp(tablet, (uint32_t)row,
+ timestamps[row]),
+ "tablet_add_timestamp")) {
+ free_tablet(&tablet);
+ free(timestamps);
+ free(values);
+ goto cleanup;
+ }
+ for (col = 0; col < ncols; ++col) {
+ if (!check_status(tablet_add_value_by_index_double(
+ tablet, (uint32_t)row, (uint32_t)col,
+ values[offset + (size_t)col]),
+ "tablet_add_value_by_index_double")) {
+ free_tablet(&tablet);
+ free(timestamps);
+ free(values);
+ goto cleanup;
+ }
+ }
+ }
+ phases.tablet_fill_seconds += monotonic_seconds() - started;
+
+ started = monotonic_seconds();
+ if (!check_status(tsfile_writer_write(core_writer, tablet),
+ "tsfile_writer_write")) {
+ free_tablet(&tablet);
+ free(timestamps);
+ free(values);
+ goto cleanup;
+ }
+ phases.writer_write_seconds += monotonic_seconds() - started;
+
+ started = monotonic_seconds();
+ free_tablet(&tablet);
+ phases.tablet_free_seconds += monotonic_seconds() - started;
+ }
+
+ started = monotonic_seconds();
+ free(timestamps);
+ free(values);
+ phases.buffer_free_seconds += monotonic_seconds() - started;
+ }
+ phases.process_cpu_seconds = process_cpu_seconds() - cpu_started;
+ phases.process_write_bytes = process_write_bytes() - io_started;
+ phases.write_seconds = monotonic_seconds() - write_started;
+
+ {
+ const double started = monotonic_seconds();
+ if (mode == MODE_WRAPPER) {
+ if (!check_status(lv_tsfile_writer_flush(wrapper_writer),
+ "lv_tsfile_writer_flush")) {
+ goto cleanup;
+ }
+ } else if (!check_status(tsfile_writer_flush(core_writer),
+ "tsfile_writer_flush")) {
+ goto cleanup;
+ }
+ phases.flush_seconds = monotonic_seconds() - started;
+ }
+
+ {
+ const double started = monotonic_seconds();
+ if (mode == MODE_WRAPPER) {
+ if (!check_status(lv_tsfile_writer_close(wrapper_writer),
+ "lv_tsfile_writer_close")) {
+ wrapper_writer = 0;
+ goto cleanup;
+ }
+ wrapper_writer = 0;
+ } else {
+ if (!check_status(tsfile_writer_close(core_writer),
+ "tsfile_writer_close")) {
+ core_writer = NULL;
+ goto cleanup;
+ }
+ core_writer = NULL;
+ free_write_file(&core_file);
+ }
+ phases.close_seconds = monotonic_seconds() - started;
+ }
+ phases.full_seconds = monotonic_seconds() - full_started;
+
+ if (stat(output_path, &file_info) != 0) {
+ perror("stat");
+ goto cleanup;
+ }
+
+ printf("MODE=%s\n", mode == MODE_WRAPPER ? "wrapper" : "core");
+ printf("VALUE_ENCODING=%s\n", argv[7]);
+ printf("VALUE_COMPRESSION=%s\n", argv[8]);
+ printf("TIME_ENCODING=%s\n", argv[9]);
+ printf("TIME_COMPRESSION=%s\n", argv[10]);
+ printf("EFFECTIVE_VALUE_ENCODING=%u\n",
+ (unsigned)get_datatype_encoding(TS_DATATYPE_DOUBLE));
+ printf("EFFECTIVE_VALUE_COMPRESSION=%u\n",
+ (unsigned)get_global_compression());
+ printf("EFFECTIVE_TIME_ENCODING=%u\n",
+ (unsigned)get_global_time_encoding());
+ printf("EFFECTIVE_TIME_COMPRESSION=%u\n",
+ (unsigned)get_global_time_compression());
+ printf("TOTAL_VALUE_BYTES=%" PRIu64 "\n", total_value_bytes);
+ printf("TOTAL_TIMESTAMP_BYTES=%" PRIu64 "\n", total_rows * sizeof(int64_t));
+ printf("BLOCK_VALUE_BYTES=%" PRIu64 "\n", block_value_bytes);
+ printf("ROWS_PER_BLOCK=%d\n", (int)rows_per_block);
+ printf("COLS=%d\n", (int)ncols);
+ printf("BLOCKS=%" PRIu64 "\n", block_count);
+ printf("TOTAL_ROWS=%" PRIu64 "\n", total_rows);
+ printf("TOTAL_POINTS=%" PRIu64 "\n", total_rows * (uint64_t)ncols);
+ printf("OPEN_SECONDS=%.9f\n", phases.open_seconds);
+ printf("PREPARE_SECONDS=%.9f\n", phases.prepare_seconds);
+ printf("TABLET_NEW_SECONDS=%.9f\n", phases.tablet_new_seconds);
+ printf("TABLET_FILL_SECONDS=%.9f\n", phases.tablet_fill_seconds);
+ printf("WRITER_WRITE_SECONDS=%.9f\n", phases.writer_write_seconds);
+ printf("TABLET_FREE_SECONDS=%.9f\n", phases.tablet_free_seconds);
+ printf("WRAPPER_BLOCK_SECONDS=%.9f\n", phases.wrapper_block_seconds);
+ printf("BUFFER_FREE_SECONDS=%.9f\n", phases.buffer_free_seconds);
+ printf("WRITE_SECONDS=%.9f\n", phases.write_seconds);
+ printf("FLUSH_SECONDS=%.9f\n", phases.flush_seconds);
+ printf("CLOSE_SECONDS=%.9f\n", phases.close_seconds);
+ printf("FULL_SECONDS=%.9f\n", phases.full_seconds);
+ printf("PROCESS_CPU_SECONDS=%.9f\n", phases.process_cpu_seconds);
+ printf("CPU_TO_WALL_RATIO=%.6f\n",
+ phases.write_seconds > 0.0
+ ? phases.process_cpu_seconds / phases.write_seconds
+ : 0.0);
+ printf("PROCESS_WRITE_BYTES=%" PRIu64 "\n", phases.process_write_bytes);
+ printf("VALUE_BYTES_PER_SECOND=%.3f\n",
+ (double)total_value_bytes / phases.write_seconds);
+ printf("POINTS_PER_SECOND=%.3f\n",
+ (double)(total_rows * (uint64_t)ncols) / phases.write_seconds);
+ printf("OUTPUT_BYTES=%lld\n", (long long)file_info.st_size);
+ printf("OUTPUT_TO_VALUE_RATIO=%.9f\n",
+ (double)file_info.st_size / (double)total_value_bytes);
+ printf("OUTPUT_TO_LOGICAL_INPUT_RATIO=%.9f\n",
+ (double)file_info.st_size /
+ (double)(total_value_bytes + total_rows * sizeof(int64_t)));
+ exit_code = 0;
+
+cleanup:
+ if (wrapper_writer != 0) {
+ lv_tsfile_writer_close(wrapper_writer);
+ }
+ if (core_writer != NULL) {
+ tsfile_writer_close(core_writer);
+ }
+ if (core_file != NULL) {
+ free_write_file(&core_file);
+ }
+ free(column_types);
+ free_column_names(column_names, ncols > 0 ? ncols : 0);
+ free(source_values);
+ return exit_code;
+}
diff --git a/cpp/src/labview_wrapper/benchmark_read_block.py b/cpp/src/labview_wrapper/benchmark_read_block.py
new file mode 100644
index 000000000..8fb756e1f
--- /dev/null
+++ b/cpp/src/labview_wrapper/benchmark_read_block.py
@@ -0,0 +1,256 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Compare LabVIEW scalar-row reads with numeric TsBlock reads."""
+
+from __future__ import annotations
+
+import argparse
+import ctypes
+import math
+import os
+import statistics
+import time
+from pathlib import Path
+
+from benchmark_block import bind, check, default_library, open_writer
+
+
+def bind_reader(dll_path: Path) -> ctypes.CDLL:
+ lib = bind(dll_path)
+ handle = ctypes.c_uint64
+ status = ctypes.c_int32
+ lib.lv_tsfile_reader_open.argtypes = [ctypes.c_char_p, ctypes.POINTER(handle)]
+ lib.lv_tsfile_reader_open.restype = status
+ lib.lv_tsfile_reader_close.argtypes = [handle]
+ lib.lv_tsfile_reader_close.restype = status
+ lib.lv_tsfile_query_table.argtypes = [
+ handle,
+ ctypes.c_char_p,
+ ctypes.c_char_p,
+ ctypes.c_int64,
+ ctypes.c_int64,
+ ctypes.POINTER(handle),
+ ]
+ lib.lv_tsfile_query_table.restype = status
+ lib.lv_tsfile_query_table_batch.argtypes = [
+ handle,
+ ctypes.c_char_p,
+ ctypes.c_char_p,
+ ctypes.c_int64,
+ ctypes.c_int64,
+ ctypes.c_int32,
+ ctypes.POINTER(handle),
+ ]
+ lib.lv_tsfile_query_table_batch.restype = status
+ lib.lv_tsfile_rs_next.argtypes = [handle, ctypes.POINTER(status)]
+ lib.lv_tsfile_rs_next.restype = ctypes.c_int32
+ lib.lv_tsfile_rs_get_i64.argtypes = [handle, ctypes.c_uint32]
+ lib.lv_tsfile_rs_get_i64.restype = ctypes.c_int64
+ lib.lv_tsfile_rs_get_f64.argtypes = [handle, ctypes.c_uint32]
+ lib.lv_tsfile_rs_get_f64.restype = ctypes.c_double
+ lib.lv_tsfile_rs_read_block_f64.argtypes = [
+ handle,
+ ctypes.POINTER(ctypes.c_int64),
+ ctypes.POINTER(ctypes.c_double),
+ ctypes.POINTER(ctypes.c_uint8),
+ ctypes.c_int32,
+ ctypes.c_int32,
+ ctypes.POINTER(ctypes.c_int32),
+ ]
+ lib.lv_tsfile_rs_read_block_f64.restype = status
+ lib.lv_tsfile_rs_free.argtypes = [handle]
+ return lib
+
+
+def create_input(lib, path: Path, rows: int, cols: int, columns: list[bytes]) -> None:
+ timestamp_type = ctypes.c_int64 * rows
+ data_type = ctypes.c_double * (rows * cols)
+ timestamps = timestamp_type(*(1000 + row for row in range(rows)))
+ values = data_type(
+ *(
+ math.sin(row * 0.01 + col * 0.1)
+ for row in range(rows)
+ for col in range(cols)
+ )
+ )
+ writer = open_writer(lib, path, columns)
+ check(
+ lib.lv_tsfile_write_block_f64(writer, timestamps, values, rows, cols), "write"
+ )
+ check(lib.lv_tsfile_writer_flush(writer), "flush")
+ check(lib.lv_tsfile_writer_close(writer), "close")
+
+
+def open_reader(lib, path: Path) -> int:
+ reader = ctypes.c_uint64()
+ check(
+ lib.lv_tsfile_reader_open(os.fsencode(path), ctypes.byref(reader)),
+ "reader_open",
+ )
+ return reader.value
+
+
+def read_rows(lib, path: Path, table: bytes, columns: bytes, cols: int):
+ reader = open_reader(lib, path)
+ result_set = ctypes.c_uint64()
+ check(
+ lib.lv_tsfile_query_table(
+ reader, table, columns, 0, (1 << 63) - 1, ctypes.byref(result_set)
+ ),
+ "query_table",
+ )
+ started = time.perf_counter()
+ row_count = 0
+ checksum = 0.0
+ error = ctypes.c_int32()
+ while lib.lv_tsfile_rs_next(result_set, ctypes.byref(error)) == 1:
+ checksum += int(lib.lv_tsfile_rs_get_i64(result_set, 0)) * 1e-12
+ for col in range(cols):
+ checksum += float(lib.lv_tsfile_rs_get_f64(result_set, col + 1))
+ row_count += 1
+ elapsed = time.perf_counter() - started
+ check(error.value, "rs_next")
+ lib.lv_tsfile_rs_free(result_set)
+ check(lib.lv_tsfile_reader_close(reader), "reader_close")
+ return row_count, checksum, elapsed
+
+
+def read_blocks(
+ lib, path: Path, table: bytes, columns: bytes, cols: int, batch_rows: int
+):
+ reader = open_reader(lib, path)
+ result_set = ctypes.c_uint64()
+ check(
+ lib.lv_tsfile_query_table_batch(
+ reader,
+ table,
+ columns,
+ 0,
+ (1 << 63) - 1,
+ batch_rows,
+ ctypes.byref(result_set),
+ ),
+ "query_table_batch",
+ )
+ timestamps = (ctypes.c_int64 * batch_rows)()
+ values = (ctypes.c_double * (batch_rows * cols))()
+ copied = ctypes.c_int32()
+ row_count = 0
+ checksum = 0.0
+ decode_seconds = 0.0
+ host_copy_seconds = 0.0
+ while True:
+ started = time.perf_counter()
+ check(
+ lib.lv_tsfile_rs_read_block_f64(
+ result_set,
+ timestamps,
+ values,
+ None,
+ batch_rows,
+ cols,
+ ctypes.byref(copied),
+ ),
+ "read_block_f64",
+ )
+ decode_seconds += time.perf_counter() - started
+ if copied.value == 0:
+ break
+ started = time.perf_counter()
+ for row in range(copied.value):
+ checksum += int(timestamps[row]) * 1e-12
+ offset = row * cols
+ for col in range(cols):
+ checksum += float(values[offset + col])
+ host_copy_seconds += time.perf_counter() - started
+ row_count += copied.value
+ lib.lv_tsfile_rs_free(result_set)
+ check(lib.lv_tsfile_reader_close(reader), "reader_close")
+ return row_count, checksum, decode_seconds, host_copy_seconds
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--dll", type=Path, default=default_library())
+ parser.add_argument("--rows", type=int, default=150_000)
+ parser.add_argument("--cols", type=int, default=9)
+ parser.add_argument("--batch-rows", type=int, default=10_000)
+ parser.add_argument("--repeats", type=int, default=3)
+ parser.add_argument("--output-dir", type=Path, default=Path.cwd())
+ parser.add_argument("--smoke", action="store_true")
+ args = parser.parse_args()
+ if args.smoke:
+ args.rows, args.cols, args.batch_rows, args.repeats = 100, 3, 64, 2
+ if min(args.rows, args.cols, args.batch_rows, args.repeats) <= 0:
+ parser.error("rows, cols, batch-rows, and repeats must be positive")
+
+ args.output_dir.mkdir(parents=True, exist_ok=True)
+ path = args.output_dir / "lv_read_benchmark.tsfile"
+ table = b"vib"
+ column_names = [f"ch_{col}".encode() for col in range(args.cols)]
+ columns = b"\n".join(column_names)
+ lib = bind_reader(args.dll)
+ create_input(lib, path, args.rows, args.cols, column_names)
+
+ warm_rows = read_rows(lib, path, table, columns, args.cols)
+ warm_blocks = read_blocks(lib, path, table, columns, args.cols, args.batch_rows)
+ if warm_rows[0] != warm_blocks[0] or not math.isclose(
+ warm_rows[1], warm_blocks[1], rel_tol=1e-12, abs_tol=1e-9
+ ):
+ raise RuntimeError("warm-up row and block results differ")
+
+ legacy_samples = []
+ decode_samples = []
+ copy_samples = []
+ for _ in range(args.repeats):
+ row_result = read_rows(lib, path, table, columns, args.cols)
+ block_result = read_blocks(
+ lib, path, table, columns, args.cols, args.batch_rows
+ )
+ if row_result[0] != args.rows or block_result[0] != args.rows:
+ raise RuntimeError("unexpected result row count")
+ if not math.isclose(
+ row_result[1], block_result[1], rel_tol=1e-12, abs_tol=1e-9
+ ):
+ raise RuntimeError("row and block checksums differ")
+ legacy_samples.append(row_result[2])
+ decode_samples.append(block_result[2])
+ copy_samples.append(block_result[3])
+
+ result = {
+ "legacy_row_seconds": statistics.median(legacy_samples),
+ "batch_decode_seconds": statistics.median(decode_samples),
+ "host_copy_seconds": statistics.median(copy_samples),
+ }
+ if args.smoke and any(value <= 0 for value in result.values()):
+ raise RuntimeError(f"non-positive smoke phase: {result}")
+ batch_total = result["batch_decode_seconds"] + result["host_copy_seconds"]
+ print(
+ f"rows={args.rows} cols={args.cols} batch_rows={args.batch_rows} "
+ f"repeats={args.repeats}"
+ )
+ for key, value in result.items():
+ print(f"{key}={value:.9f}")
+ print(f"batch_total_seconds={batch_total:.9f}")
+ print(f"batch_speedup={result['legacy_row_seconds'] / batch_total:.2f}x")
+ print(f"total_rows={args.rows}")
+ print(f"total_values={args.rows * args.cols}")
+
+
+if __name__ == "__main__":
+ main()