This is an automated email from the ASF dual-hosted git repository. CritasWang pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/iotdb-client-rust.git
commit d6d3ed3e989af4c3004764f03bc02a49e615d283 Author: CritasWang <[email protected]> AuthorDate: Mon Jul 13 19:32:40 2026 +0800 Benchmark: mirror iot-benchmark measurement semantics (prep inside timed span, failures excluded from latency, Result/Latency Matrix output with P10-P999 + SLOWEST_THREAD) --- README.md | 8 ++- README_ZH.md | 8 ++- examples/benchmark.rs | 159 +++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 141 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index fbb379f..a403b7d 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,9 @@ cargo test # now includes the live-server tests ## Benchmark -`examples/benchmark.rs` is a write-performance benchmark modeled on the Node.js client's `benchmark/` suite (which follows [thulab/iot-benchmark](https://github.com/thulab/iot-benchmark)); metric definitions match, so results are comparable across the SDKs. Tablets are pre-generated outside the timed section; N worker threads each own a pooled session and insert `insert_tablet` batches round-robin over their devices. Timestamps are sequential per device from a fixed base, so runs are det [...] +`examples/benchmark.rs` is a write-performance benchmark modeled on the Node.js client's `benchmark/` suite and on [thulab/iot-benchmark](https://github.com/thulab/iot-benchmark). Tablets are pre-generated outside the timed section; N worker threads each own a pooled session and insert `insert_tablet` batches round-robin over their devices. Timestamps are sequential per device from a fixed base, so runs are deterministic. + +> **Statistics semantics now mirror iot-benchmark:** the per-operation timed span includes batch preparation (not just the insert RPC), failed operations are excluded from latency samples (counted as `failOperation`/`failPoint`), and the output includes iot-benchmark-style Result Matrix and Latency (ms) Matrix sections (AVG…P999/MAX/SLOWEST_THREAD; percentiles are exact, whereas iot-benchmark uses a t-digest approximation). Numbers produced by earlier versions of this benchmark (RPC-only [...] ```sh # tree model, defaults: 100 devices × 10 sensors × 20 batches × 1000 rows = 20M points, 8 clients @@ -264,9 +266,9 @@ cargo run --release --example benchmark -- --mode table \ --devices 20 --sensors 10 --batches 100 --batch-size 100 --clients 8 --cleanup ``` -Knobs: `--mode tree|table`, `--devices`, `--sensors`, `--batches` (per device), `--batch-size` (rows per tablet), `--clients` (worker threads = pool size), `--host/--port/--user/--password` (also via `IOTDB_HOST/PORT/USER/PASSWORD`), `--base-ts`, `--point-step`, `--reuse-tablets` (pre-generate only N tablets per worker and re-send them with rebased timestamps — bounds memory for very large runs; the per-batch timestamp rewrite happens inside the timed loop, like a real streaming producer [...] +Knobs: `--mode tree|table`, `--devices`, `--sensors`, `--batches` (per device), `--batch-size` (rows per tablet), `--clients` (worker threads = pool size), `--host/--port/--user/--password` (also via `IOTDB_HOST/PORT/USER/PASSWORD`), `--base-ts`, `--point-step`, `--reuse-tablets` (pre-generate only N tablets per worker and re-send them with rebased timestamps — bounds memory for very large runs; the per-batch timestamp rewrite happens inside the timed loop, like a real streaming producer [...] -Measured on an Apple M2 Pro (10 cores), IoTDB 2.0.6 standalone in Docker on the same machine (Docker VM: all 10 CPUs / 8 GB; JVM heap 1 GB), release build: +Measured on an Apple M2 Pro (10 cores), IoTDB 2.0.6 standalone in Docker on the same machine (Docker VM: all 10 CPUs / 8 GB; JVM heap 1 GB), release build, **with the old RPC-only timing** (see note above — expect somewhat lower throughput/higher latency with the current semantics): | Mode | Devices × Sensors × Batches × Rows | Clients | Points | Throughput | p50 / p99 latency | | --- | --- | --- | --- | --- | --- | diff --git a/README_ZH.md b/README_ZH.md index dc5807f..6f62b4f 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -253,7 +253,9 @@ cargo test # 此时包含在线测试 ## 性能基准测试 -`examples/benchmark.rs` 是写入性能基准,参考 Node.js 客户端的 `benchmark/` 套件(后者遵循 [thulab/iot-benchmark](https://github.com/thulab/iot-benchmark));指标定义一致,因此各语言 SDK 的结果可直接对比。Tablet 在计时区间之外预先生成;N 个工作线程各持有一个池化会话,按批次轮询各自的设备执行 `insert_tablet`。时间戳从固定基准按设备顺序递增,运行结果可复现。 +`examples/benchmark.rs` 是写入性能基准,参考 Node.js 客户端的 `benchmark/` 套件与 [thulab/iot-benchmark](https://github.com/thulab/iot-benchmark)。Tablet 在计时区间之外预先生成;N 个工作线程各持有一个池化会话,按批次轮询各自的设备执行 `insert_tablet`。时间戳从固定基准按设备顺序递增,运行结果可复现。 + +> **统计口径现已对齐 iot-benchmark:**单次操作的计时区间包含批次准备(而非仅 insert RPC);失败操作不计入延迟样本(单独统计 `failOperation`/`failPoint`);输出包含 iot-benchmark 风格的 Result Matrix 与 Latency (ms) Matrix(AVG…P999/MAX/SLOWEST_THREAD;百分位为精确值,iot-benchmark 使用 t-digest 近似)。旧版基准(仅计 RPC 时间,含下表数据)的数字与新输出**不可直接对比**。 ```sh # 树模型,默认规模:100 设备 × 10 传感器 × 20 批 × 1000 行 = 2000 万点,8 客户端 @@ -264,9 +266,9 @@ cargo run --release --example benchmark -- --mode table \ --devices 20 --sensors 10 --batches 100 --batch-size 100 --clients 8 --cleanup ``` -参数:`--mode tree|table`、`--devices`、`--sensors`、`--batches`(每设备批数)、`--batch-size`(每 tablet 行数)、`--clients`(工作线程数 = 池大小)、`--host/--port/--user/--password`(亦支持 `IOTDB_HOST/PORT/USER/PASSWORD` 环境变量)、`--base-ts`、`--point-step`、`--reuse-tablets`(每工作线程仅预生成 N 个 tablet 并循环重发,每批重写时间戳基准——为超大规模运行限定内存占用;时间戳重写在计时循环内进行,等同真实流式生产者的按批打时间戳)、`--tablets-per-rpc`(树模型:每次 RPC 通过 `insert_tablets` 批量发送 N 个 tablet)、`--cleanup`。传感器类型分布沿用 Node.js 默认比例(30% FLOAT、20% DOUBLE、20% INT32、10% INT64、10% TEXT、10% BOOLEAN)。报告 [...] +参数:`--mode tree|table`、`--devices`、`--sensors`、`--batches`(每设备批数)、`--batch-size`(每 tablet 行数)、`--clients`(工作线程数 = 池大小)、`--host/--port/--user/--password`(亦支持 `IOTDB_HOST/PORT/USER/PASSWORD` 环境变量)、`--base-ts`、`--point-step`、`--reuse-tablets`(每工作线程仅预生成 N 个 tablet 并循环重发,每批重写时间戳基准——为超大规模运行限定内存占用;时间戳重写在计时循环内进行,等同真实流式生产者的按批打时间戳)、`--tablets-per-rpc`(树模型:每次 RPC 通过 `insert_tablets` 批量发送 N 个 tablet)、`--cleanup`。传感器类型分布沿用 Node.js 默认比例(30% FLOAT、20% DOUBLE、20% INT32、10% INT64、10% TEXT、10% BOOLEAN)。报告 [...] -实测环境:Apple M2 Pro(10 核),IoTDB 2.0.6 standalone(Docker,与客户端同机;Docker VM 全部 10 核 / 8 GB,JVM 堆 1 GB),release 构建: +实测环境:Apple M2 Pro(10 核),IoTDB 2.0.6 standalone(Docker,与客户端同机;Docker VM 全部 10 核 / 8 GB,JVM 堆 1 GB),release 构建,**采用旧的仅计 RPC 口径**(见上方说明——新口径下吞吐会略低、延迟会略高): | 模式 | 设备 × 传感器 × 批数 × 行数 | 客户端 | 总点数 | 吞吐量 | p50 / p99 延迟 | | --- | --- | --- | --- | --- | --- | diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 3f44afb..b949cae 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -16,14 +16,28 @@ // under the License. //! Write-performance benchmark for the tree and table models, modeled on the -//! Node.js client's `benchmark/` suite (which in turn follows -//! [thulab/iot-benchmark](https://github.com/thulab/iot-benchmark)). Metric -//! definitions match the Node.js `MetricsCollector` so results are directly -//! comparable: per-`insert_tablet` latency percentiles (p = ceil(p% × n) − 1 -//! over the sorted samples), points/sec over the wall time of the timed -//! insert phase only. Schema setup and data generation happen **outside** -//! the timed section ("Pre-generated Test Data"); timestamps are sequential -//! per device from a fixed base, so runs are deterministic. +//! Node.js client's `benchmark/` suite and on +//! [thulab/iot-benchmark](https://github.com/thulab/iot-benchmark). The +//! **measurement semantics mirror iot-benchmark** (`DBWrapper` / +//! `Measurement`): +//! +//! - the per-operation latency span starts **before** batch preparation for +//! that batch (Java times `genTablet` + submit + `Future.get`, +//! `SessionStrategy.insertOneBatchByTablet`), not just the insert RPC; +//! - failed operations record **no latency sample** — they only count toward +//! `failOperation` / `failPoint` (`DBWrapper.measureOneBatch`); +//! - throughput = okPoint / elapsed, where elapsed is the wall time of the +//! whole data phase (schema creation excluded), matching +//! `BaseMode.measure`; +//! - output includes an iot-benchmark-style "Result Matrix" and +//! "Latency (ms) Matrix" (AVG MIN P10 P25 MEDIAN P75 P90 P95 P99 P999 MAX +//! SLOWEST_THREAD) so runs diff side-by-side with Java logs. Percentiles +//! here are exact over the sorted samples (p = ceil(p% × n) − 1); +//! iot-benchmark uses a t-digest approximation. SLOWEST_THREAD is the max +//! over threads of that thread's latency sum (`Measurement.mergeMeasurement`). +//! +//! Schema setup happens **outside** the timed section; timestamps are +//! sequential per device from a fixed base, so runs are deterministic. //! //! Sensor data types follow the Node.js default distribution: //! 30% FLOAT, 20% DOUBLE, 20% INT32, 10% INT64, 10% TEXT, 10% BOOLEAN. @@ -462,17 +476,23 @@ fn verify_row_count(pool: &Pool, config: &BenchConfig) -> Result<()> { } // --------------------------------------------------------------------------- -// Metrics (definitions match Node.js benchmark-core.js MetricsCollector) +// Metrics (semantics mirror iot-benchmark's Measurement / DBWrapper) // --------------------------------------------------------------------------- #[derive(Default)] struct WorkerStats { - /// One entry per insert_tablet call (ms), success or failure. + /// One entry per **successful** operation (ms). The timed span covers + /// batch preparation + the insert RPC (iot-benchmark's DBWrapper times + /// genTablet + submit + Future.get; failures record no latency). latencies_ms: Vec<f64>, + /// All operations attempted (ok + failed). ops: u64, - failures: u64, - /// Points from successful operations only. + /// Failed operations (iot-benchmark failOperation). + fail_operations: u64, + /// Points from successful operations only (iot-benchmark okPoint). points: u64, + /// Points belonging to failed operations (iot-benchmark failPoint). + fail_points: u64, error_samples: Vec<String>, } @@ -492,14 +512,23 @@ fn print_summary(config: &BenchConfig, wall: Duration, workers: &[WorkerStats]) .collect(); latencies.sort_by(|a, b| a.partial_cmp(b).expect("latency is never NaN")); let ops: u64 = workers.iter().map(|w| w.ops).sum(); - let failures: u64 = workers.iter().map(|w| w.failures).sum(); - let points: u64 = workers.iter().map(|w| w.points).sum(); + let fail_operations: u64 = workers.iter().map(|w| w.fail_operations).sum(); + let ok_operations = ops - fail_operations; + let ok_points: u64 = workers.iter().map(|w| w.points).sum(); + let fail_points: u64 = workers.iter().map(|w| w.fail_points).sum(); let secs = wall.as_secs_f64(); + // AVG = sum(ok latencies) / okOperations (Measurement.calculateMetrics). let avg = if latencies.is_empty() { 0.0 } else { latencies.iter().sum::<f64>() / latencies.len() as f64 }; + // SLOWEST_THREAD = max over threads of that thread's latency sum + // (Measurement.mergeMeasurement → MAX_THREAD_LATENCY_SUM). + let slowest_thread: f64 = workers + .iter() + .map(|w| w.latencies_ms.iter().sum::<f64>()) + .fold(0.0, f64::max); let sep = "=".repeat(80); println!("\n{sep}\nBENCHMARK RESULTS\n{sep}"); @@ -511,22 +540,25 @@ fn print_summary(config: &BenchConfig, wall: Duration, workers: &[WorkerStats]) ); println!("\n[Operations]"); println!(" Total Operations: {ops}"); - println!(" Successful: {}", ops - failures); - println!(" Failed: {failures}"); + println!(" Successful: {ok_operations}"); + println!(" Failed: {fail_operations}"); println!( " Success Rate: {:.2}%", if ops == 0 { 0.0 } else { - (ops - failures) as f64 / ops as f64 * 100.0 + ok_operations as f64 / ops as f64 * 100.0 } ); println!("\n[Data Points]"); - println!(" Total Points Written: {points}"); + println!(" Total Points Written: {ok_points}"); + if fail_points > 0 { + println!(" Failed Points: {fail_points}"); + } println!("\n[Throughput]"); println!(" Operations/sec: {:.2}", ops as f64 / secs); - println!(" Points/sec: {:.0}", points as f64 / secs); - println!("\n[Latency (ms)]"); + println!(" Points/sec: {:.0}", ok_points as f64 / secs); + println!("\n[Latency (ms)] (successful operations only; span includes batch preparation)"); println!( " Min: {:.2}ms", latencies.first().copied().unwrap_or(0.0) @@ -575,6 +607,67 @@ fn print_summary(config: &BenchConfig, wall: Duration, workers: &[WorkerStats]) config.clients, config.total_points(), ); + + // --- iot-benchmark-style matrices (columns/order match Measurement) ---- + // %-25s per Result-Matrix cell, %-12s per Latency-Matrix metric cell. + println!("\nTest elapsed time (not include schema creation): {secs:.2} second"); + println!( + "----------------------------------------------------------Result Matrix----------------------------------------------------------" + ); + println!( + "{:<25}{:<25}{:<25}{:<25}{:<25}{:<25}", + "Operation", "okOperation", "okPoint", "failOperation", "failPoint", "throughput(point/s)" + ); + println!( + "{:<25}{:<25}{:<25}{:<25}{:<25}{:<25}", + "INGESTION", + ok_operations, + ok_points, + fail_operations, + fail_points, + format!( + "{:.2}", + if secs > 0.0 { + ok_points as f64 / secs + } else { + 0.0 + } + ) + ); + println!( + "---------------------------------------------------------------------------------------------------------------------------------" + ); + println!( + "--------------------------------------------------------------------------Latency (ms) Matrix--------------------------------------------------------------------------" + ); + print!("{:<25}", "Operation"); + let metrics: [(&str, f64); 12] = [ + ("AVG", avg), + ("MIN", latencies.first().copied().unwrap_or(0.0)), + ("P10", percentile(&latencies, 10.0)), + ("P25", percentile(&latencies, 25.0)), + ("MEDIAN", percentile(&latencies, 50.0)), + ("P75", percentile(&latencies, 75.0)), + ("P90", percentile(&latencies, 90.0)), + ("P95", percentile(&latencies, 95.0)), + ("P99", percentile(&latencies, 99.0)), + ("P999", percentile(&latencies, 99.9)), + ("MAX", latencies.last().copied().unwrap_or(0.0)), + ("SLOWEST_THREAD", slowest_thread), + ]; + for (name, _) in &metrics { + print!("{name:<12}"); + } + println!(); + print!("{:<25}", "INGESTION"); + for (_, value) in &metrics { + print!("{:<12}", format!("{value:.2}")); + } + println!(); + println!( + "-----------------------------------------------------------------------------------------------------------------------------------------------------------------------" + ); + println!("Note: exact percentiles (iot-benchmark uses t-digest approximation)"); } // --------------------------------------------------------------------------- @@ -724,6 +817,17 @@ fn main() -> Result<()> { let materialized = tablets.len(); let mut i = 0; while i < schedule_len { + // Per-op timing mirrors iot-benchmark: the timer + // starts BEFORE batch preparation, as Java's + // DBWrapper.insertOneBatch times genTablet + submit + // + Future.get. Mapping per mode: + // * reuse mode — timestamp rebase (the per-batch + // work a streaming producer would do) + the RPC + // are inside the timed span; + // * non-reuse — tablets are pre-built (Java's + // pre-built workload), so the span covers + // borrowing the tablet slice + the RPC. + let start = Instant::now(); // A chunk never wraps the materialized ring, so it // always maps to one contiguous slice. let idx = i % materialized; @@ -734,9 +838,7 @@ fn main() -> Result<()> { if config.reuse_tablets > 0 { // Rebase each reused tablet onto its iteration's // disjoint time window so every insert writes - // fresh timestamps. This runs inside the timed - // loop on purpose: it's the same per-batch - // timestamping a real streaming producer would do. + // fresh timestamps. for (k, tablet) in tablets[idx..idx + chunk].iter_mut().enumerate() { let window = config.base_ts + ((i + k) * config.batch_size) as i64 * config.point_step; @@ -750,24 +852,25 @@ fn main() -> Result<()> { .iter() .map(|t| (t.row_count() * config.sensors) as u64) .sum(); - let start = Instant::now(); let outcome = if chunk == 1 { session.insert_tablet(&batch[0]) } else { session.insert_tablets(batch, false) }; - stats - .latencies_ms - .push(start.elapsed().as_secs_f64() * 1000.0); + let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0; stats.ops += 1; ops_done.fetch_add(1, Ordering::Relaxed); match outcome { Ok(()) => { + // Only successful operations record a latency + // sample (DBWrapper.measureOneBatch). + stats.latencies_ms.push(elapsed_ms); stats.points += points; points_done.fetch_add(points, Ordering::Relaxed); } Err(e) => { - stats.failures += 1; + stats.fail_operations += 1; + stats.fail_points += points; if stats.error_samples.len() < 5 { stats.error_samples.push(e.to_string()); }
