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 6d64d2a87b4e612423227b25daebba367b9cbca8
Author: CritasWang <[email protected]>
AuthorDate: Mon Jul 13 15:26:14 2026 +0800

    Benchmark scale-up: --reuse-tablets (bounded memory) + --tablets-per-rpc 
(insert_tablets batching); 22.5M pts/s sustained over 250M points, server-side 
bottleneck attributed (1G heap flush stalls)
---
 README.md             |  18 +++++----
 README_ZH.md          |  18 +++++----
 examples/benchmark.rs | 103 +++++++++++++++++++++++++++++++++++++++++++-------
 src/data/tablet.rs    |   8 ++++
 4 files changed, 118 insertions(+), 29 deletions(-)

diff --git a/README.md b/README.md
index 2b5dfd5..fbb379f 100644
--- a/README.md
+++ b/README.md
@@ -264,17 +264,19 @@ 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`, `--cleanup`. 
Sensor types follow the Node.js default distribution (30% FLOAT, 20% DOUBLE, 
20% INT32, 10% INT64, 10% TEXT, 10% BOOLEAN). The report includes total points, 
wall time, points/sec, per-batch latency p50/p90/p95/p99/ma [...]
+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, release build, 8 clients:
+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:
 
-| Mode | Devices × Sensors × Batches × Rows | Points | Throughput | p50 / p99 
latency |
-| --- | --- | --- | --- | --- |
-| tree | 20 × 10 × 100 × 100 | 2M | ~1.98M pts/s | 2.46 ms / 8.38 ms |
-| table | 20 × 10 × 100 × 100 | 2M | ~1.97M pts/s | 2.13 ms / 9.97 ms |
-| tree | 100 × 10 × 10 × 1000 | 10M | ~9.73M pts/s | 4.36 ms / 72.18 ms |
+| Mode | Devices × Sensors × Batches × Rows | Clients | Points | Throughput | 
p50 / p99 latency |
+| --- | --- | --- | --- | --- | --- |
+| tree | 20 × 10 × 100 × 100 | 8 | 2M | ~1.98M pts/s | 2.46 ms / 8.38 ms |
+| table | 20 × 10 × 100 × 100 | 8 | 2M | ~1.97M pts/s | 2.13 ms / 9.97 ms |
+| tree | 100 × 10 × 20 × 1000 | 8 | 20M | ~12.4M pts/s | 4.45 ms / 27.03 ms |
+| tree | 100 × 100 × 4 × 1000 | 10 | 40M | ~15–20M pts/s | 31 ms / 156 ms |
+| tree | 100 × 100 × 25 × 1000, `--tablets-per-rpc 4` | 10 | 250M | 
**~21–22.5M pts/s** | 105 ms / 907 ms |
 
-Throughput scales with tablet size (rows × sensors per RPC); 1000-row tablets 
give ~5× the throughput of 100-row tablets at the same client count. Numbers 
are client+server on one machine — treat them as an upper bound on client 
overhead, not a server capacity measurement.
+Throughput scales with points per RPC: wider tablets (100 sensors = 100k 
points per 1000-row tablet) and multi-tablet `insert_tablets` batching lift the 
same hardware from ~12M to ~22M pts/s sustained (250M points, 
`--reuse-tablets`). Beyond ~400k points per RPC — or more clients than cores — 
throughput plateaus and tail latency grows; during peak runs the server JVM 
bursts to ~3 cores then stalls on memtable flushes while the Rust client sits 
at ~30% of one core, so the ceiling here is  [...]
 
 ## Project layout
 
diff --git a/README_ZH.md b/README_ZH.md
index 25e384b..dc5807f 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -264,17 +264,19 @@ 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`、`--cleanup`。传感器类型分布沿用 Node.js 默认比例(30% 
FLOAT、20% DOUBLE、20% INT32、10% INT64、10% TEXT、10% 
BOOLEAN)。报告包含总点数、耗时、points/sec、单批延迟 p50/p90/p95/p99/max、错误数,以及读回行数校验。
+参数:`--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,与客户端同机),release 构建,8 客户端:
+实测环境:Apple M2 Pro(10 核),IoTDB 2.0.6 standalone(Docker,与客户端同机;Docker VM 全部 10 核 
/ 8 GB,JVM 堆 1 GB),release 构建:
 
-| 模式 | 设备 × 传感器 × 批数 × 行数 | 总点数 | 吞吐量 | p50 / p99 延迟 |
-| --- | --- | --- | --- | --- |
-| tree | 20 × 10 × 100 × 100 | 200 万 | ~198 万 pts/s | 2.46 ms / 8.38 ms |
-| table | 20 × 10 × 100 × 100 | 200 万 | ~197 万 pts/s | 2.13 ms / 9.97 ms |
-| tree | 100 × 10 × 10 × 1000 | 1000 万 | ~973 万 pts/s | 4.36 ms / 72.18 ms |
+| 模式 | 设备 × 传感器 × 批数 × 行数 | 客户端 | 总点数 | 吞吐量 | p50 / p99 延迟 |
+| --- | --- | --- | --- | --- | --- |
+| tree | 20 × 10 × 100 × 100 | 8 | 200 万 | ~198 万 pts/s | 2.46 ms / 8.38 ms |
+| table | 20 × 10 × 100 × 100 | 8 | 200 万 | ~197 万 pts/s | 2.13 ms / 9.97 ms |
+| tree | 100 × 10 × 20 × 1000 | 8 | 2000 万 | ~1240 万 pts/s | 4.45 ms / 27.03 
ms |
+| tree | 100 × 100 × 4 × 1000 | 10 | 4000 万 | ~1500–2000 万 pts/s | 31 ms / 156 
ms |
+| tree | 100 × 100 × 25 × 1000,`--tablets-per-rpc 4` | 10 | 2.5 亿 | 
**~2100–2250 万 pts/s** | 105 ms / 907 ms |
 
-吞吐量随 tablet 大小(每 RPC 的行 × 传感器数)增长;同等客户端数下 1000 行 tablet 的吞吐约为 100 行的 5 
倍。数据为客户端与服务器同机测得——应视为客户端开销的上界,而非服务器容量。
+吞吐量随每 RPC 点数增长:更宽的 tablet(100 传感器 × 1000 行 = 每 tablet 10 万点)加上 
`insert_tablets` 多 tablet 批量,可将同一硬件从 ~1200 万提升到 ~2200 万 pts/s 持续吞吐(2.5 
亿点,`--reuse-tablets`)。每 RPC 超过约 40 万点、或客户端数超过核数后,吞吐进入平台期且尾延迟上升;峰值运行期间服务器 JVM 
写入突发时占 ~3 核、随后阻塞在 memtable 刷盘上,而 Rust 客户端仅占单核 ~30%——瓶颈在同机 Docker 内的服务器(1 GB 
堆),不在客户端。数据为客户端与服务器同机测得——应视为客户端开销的上界,而非服务器容量。
 
 ## 项目结构
 
diff --git a/examples/benchmark.rs b/examples/benchmark.rs
index 9c1fe3b..3f44afb 100644
--- a/examples/benchmark.rs
+++ b/examples/benchmark.rs
@@ -81,6 +81,16 @@ struct BenchConfig {
     /// Time interval between consecutive points per device (ms), Node.js
     /// `POINT_STEP`.
     point_step: i64,
+    /// When > 0, pre-generate only this many tablets per worker and re-send
+    /// them cyclically, rebasing timestamps per batch so every insert still
+    /// lands on a fresh, disjoint time range (values repeat; timestamps
+    /// don't). Bounds pre-generation memory for very large point counts.
+    /// 0 = pre-generate everything (default, original behavior).
+    reuse_tablets: usize,
+    /// Tree model only: batch this many tablets (different devices) into one
+    /// `insert_tablets` RPC. 1 = one `insert_tablet` per RPC (default,
+    /// original behavior). Latency percentiles are then per multi-tablet RPC.
+    tablets_per_rpc: usize,
     cleanup: bool,
 }
 
@@ -108,6 +118,11 @@ OPTIONS:
     --password <PASS>     Password (default: $IOTDB_PASSWORD or root)
     --base-ts <MS>        Base epoch-ms timestamp (default: 1720000000000)
     --point-step <MS>     Interval between points per device (default: 1000)
+    --reuse-tablets <N>   Pre-generate only N tablets per worker and re-send
+                          them with rebased timestamps (bounds memory for
+                          very large runs; 0 = pre-generate all, default)
+    --tablets-per-rpc <N> Tree model only: send N tablets per RPC via
+                          insert_tablets (default: 1 = insert_tablet)
     --cleanup             Drop the benchmark database after the run
     --help                Print this help
 
@@ -132,6 +147,8 @@ fn parse_args() -> BenchConfig {
         password: env_or("IOTDB_PASSWORD", "root"),
         base_ts: 1_720_000_000_000,
         point_step: 1000,
+        reuse_tablets: 0,
+        tablets_per_rpc: 1,
         cleanup: false,
     };
 
@@ -168,6 +185,8 @@ fn parse_args() -> BenchConfig {
             "--password" => config.password = value(&mut i, flag),
             "--base-ts" => config.base_ts = parse_num(&value(&mut i, flag), 
flag) as i64,
             "--point-step" => config.point_step = parse_num(&value(&mut i, 
flag), flag) as i64,
+            "--reuse-tablets" => config.reuse_tablets = parse_num(&value(&mut 
i, flag), flag),
+            "--tablets-per-rpc" => config.tablets_per_rpc = 
parse_num(&value(&mut i, flag), flag),
             "--cleanup" => config.cleanup = true,
             "--help" | "-h" => {
                 println!("{USAGE}");
@@ -187,12 +206,17 @@ fn parse_args() -> BenchConfig {
         ("--batches", config.batches),
         ("--batch-size", config.batch_size),
         ("--clients", config.clients),
+        ("--tablets-per-rpc", config.tablets_per_rpc),
     ] {
         if v == 0 {
             eprintln!("{name} must be positive");
             exit(2);
         }
     }
+    if config.tablets_per_rpc > 1 && config.mode == Mode::Table {
+        eprintln!("--tablets-per-rpc requires --mode tree (insert_tablets is 
tree-model only)");
+        exit(2);
+    }
     config
 }
 
@@ -609,14 +633,26 @@ fn main() -> Result<()> {
     // batch-major, i.e. round-robin over its devices.
     println!("[Setup] pre-generating test data...");
     let t0 = Instant::now();
-    let mut worker_tablets: Vec<Vec<Tablet>> = 
Vec::with_capacity(config.clients);
+    // Each worker's full schedule is `batches × its devices` inserts. With
+    // --reuse-tablets N only the first N tablets are materialized; the worker
+    // cycles over them and rebases timestamps per iteration (see worker loop).
+    let mut worker_tablets: Vec<(Vec<Tablet>, usize)> = 
Vec::with_capacity(config.clients);
     for w in 0..config.clients {
         let devices: Vec<usize> = (0..config.devices)
             .filter(|d| d % config.clients == w)
             .collect();
-        let mut tablets = Vec::with_capacity(devices.len() * config.batches);
-        for batch in 0..config.batches {
+        let schedule_len = devices.len() * config.batches;
+        let materialized = if config.reuse_tablets > 0 {
+            schedule_len.min(config.reuse_tablets)
+        } else {
+            schedule_len
+        };
+        let mut tablets = Vec::with_capacity(materialized);
+        'gen: for batch in 0..config.batches {
             for &device in &devices {
+                if tablets.len() == materialized {
+                    break 'gen;
+                }
                 tablets.push(build_tablet(
                     &config,
                     device,
@@ -626,12 +662,20 @@ fn main() -> Result<()> {
                 )?);
             }
         }
-        worker_tablets.push(tablets);
+        worker_tablets.push((tablets, schedule_len));
     }
     println!(
-        "[Setup] {} tablets generated in {:.2}s",
-        worker_tablets.iter().map(Vec::len).sum::<usize>(),
-        t0.elapsed().as_secs_f64()
+        "[Setup] {} tablets generated in {:.2}s{}",
+        worker_tablets.iter().map(|(t, _)| t.len()).sum::<usize>(),
+        t0.elapsed().as_secs_f64(),
+        if config.reuse_tablets > 0 {
+            format!(
+                " (reused cyclically over {} inserts)",
+                worker_tablets.iter().map(|(_, n)| n).sum::<usize>()
+            )
+        } else {
+            String::new()
+        }
     );
 
     // --- Timed insert phase ------------------------------------------------
@@ -665,21 +709,53 @@ fn main() -> Result<()> {
         });
 
         let handles: Vec<_> = worker_tablets
-            .iter()
-            .map(|tablets| {
+            .iter_mut()
+            .map(|(tablets, schedule_len)| {
+                let schedule_len = *schedule_len;
                 let pool = &pool;
                 let ops_done = &ops_done;
                 let points_done = &points_done;
                 scope.spawn(move || -> Result<WorkerStats> {
                     let mut session = pool.acquire()?;
                     let mut stats = WorkerStats {
-                        latencies_ms: Vec::with_capacity(tablets.len()),
+                        latencies_ms: Vec::with_capacity(schedule_len),
                         ..WorkerStats::default()
                     };
-                    for tablet in tablets {
-                        let points = (tablet.row_count() * config.sensors) as 
u64;
+                    let materialized = tablets.len();
+                    let mut i = 0;
+                    while i < schedule_len {
+                        // A chunk never wraps the materialized ring, so it
+                        // always maps to one contiguous slice.
+                        let idx = i % materialized;
+                        let chunk = config
+                            .tablets_per_rpc
+                            .min(schedule_len - i)
+                            .min(materialized - idx);
+                        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.
+                            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;
+                                for (r, ts) in 
tablet.timestamps_mut().iter_mut().enumerate() {
+                                    *ts = window + r as i64 * 
config.point_step;
+                                }
+                            }
+                        }
+                        let batch = &tablets[idx..idx + chunk];
+                        let points: u64 = batch
+                            .iter()
+                            .map(|t| (t.row_count() * config.sensors) as u64)
+                            .sum();
                         let start = Instant::now();
-                        let outcome = session.insert_tablet(tablet);
+                        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);
@@ -697,6 +773,7 @@ fn main() -> Result<()> {
                                 }
                             }
                         }
+                        i += chunk;
                     }
                     Ok(stats)
                 })
diff --git a/src/data/tablet.rs b/src/data/tablet.rs
index 0bc3127..177ece4 100644
--- a/src/data/tablet.rs
+++ b/src/data/tablet.rs
@@ -128,6 +128,14 @@ impl Tablet {
         &self.timestamps
     }
 
+    /// Mutable access to the timestamp column, e.g. to rebase a reused
+    /// tablet onto a fresh time range without regenerating its values.
+    /// Rows are re-sorted on serialization, so callers need not preserve
+    /// ordering.
+    pub fn timestamps_mut(&mut self) -> &mut [i64] {
+        &mut self.timestamps
+    }
+
     /// `Some` iff this is a table-model tablet.
     pub fn column_categories(&self) -> Option<&[ColumnCategory]> {
         self.column_categories.as_deref()

Reply via email to