This document describes the performance optimizations implemented in the IoTDB Node.js client.
| Configuration | Workers | Clients/Worker | Throughput | Avg Latency |
|---|---|---|---|---|
| Best Config | 8 | 10 | 5.42M pts/s | 329ms |
| Single Process | 1 | 20 | 4.28M pts/s | 107ms |
| Over-parallel | 10 | 10 | 3.81M pts/s | 551ms |
Best Configuration:
WORKER_COUNT=8 CLIENT_NUMBER=10 DEVICE_NUMBER=1000 \ SENSOR_NUMBER=50 BATCH_SIZE_PER_WRITE=500 POOL_MAX_SIZE=10 \ node benchmark/benchmark-table-cluster.js
| Client | Max Throughput | Architecture |
|---|---|---|
| Node.js (multi-process) | ~5.5M pts/s | 8 workers × event loop |
| Java iot-benchmark | ~60M pts/s | Multi-threaded |
Gap Analysis (~11x):
Tablet Size: 25K points (500 rows × 50 sensors) is optimal
Worker Count: 8 workers is optimal for most servers
Memory Management: Use streaming batch processing
Problem: Frequent buffer allocations and deallocations cause significant GC (Garbage Collection) pressure, especially when serializing large datasets.
Solution: Implemented BufferPool with size-based pooling strategy:
import { globalBufferPool } from '@iotdb/client'; // Buffer pool automatically manages buffers in 7 size classes: // 1KB, 4KB, 16KB, 64KB, 256KB, 1MB, 4MB // Get statistics const stats = globalBufferPool.getStats(); console.log(`Hit rate: ${stats.hitRate}`); console.log(`Pooled buffers: ${stats.pooledBuffers}`);
Impact:
When to use:
enableFastSerialization: trueProblem: Original serialization used multiple buffer concatenations and intermediate allocations, causing performance bottlenecks.
Solution: Implemented type-specific fast serializers in FastSerializer.ts:
// Old approach (multiple allocations): const buffer1 = serializeColumn1(); const buffer2 = serializeColumn2(); const result = Buffer.concat([buffer1, buffer2]); // Extra allocation! // New approach (single pre-allocated buffer): const totalSize = calculateSize(); const result = Buffer.allocUnsafe(totalSize); // Write directly to result buffer
Features:
Impact:
Problem: Converting timestamps one-by-one to BigInt and writing to buffer was inefficient.
Solution: Batch timestamp conversion with optimized buffer writes:
// Optimized timestamp serialization function serializeTimestamps(timestamps: number[]): Buffer { const size = timestamps.length * 8; const buffer = size >= 1024 ? globalBufferPool.acquire(size) : Buffer.allocUnsafe(size); for (let i = 0; i < timestamps.length; i++) { buffer.writeBigInt64BE(BigInt(Math.floor(timestamps[i])), i * 8); } return buffer.subarray(0, size); }
Impact:
Problem: Row-by-row processing with object allocation creates overhead for large result sets.
Solution: Added toColumnar() API inspired by pg's array mode:
const dataSet = await session.executeQueryStatement('SELECT temp, humidity FROM root.test'); // OLD WAY: Object per row (high allocation overhead) while (await dataSet.hasNext()) { const row = dataSet.next(); // Creates RowRecord object console.log(row.getValue('temp')); } // NEW WAY: Columnar format (zero allocation overhead) const columnar = await dataSet.toColumnar(); // columnar = { // timestamps: [ts1, ts2, ts3, ...], // values: [[temp1, temp2, temp3, ...], [humidity1, humidity2, humidity3, ...]], // columnNames: ['temp', 'humidity'], // columnTypes: ['FLOAT', 'FLOAT'] // } // Process entire columns at once const avgTemp = columnar.values[0].reduce((a, b) => a + b) / columnar.values[0].length;
Impact:
When to use:
import { Session } from '@iotdb/client'; // Enable (default) const session = new Session({ host: 'localhost', port: 6667, enableFastSerialization: true, // Uses optimized serializers }); // Disable (fall back to legacy) const legacySession = new Session({ host: 'localhost', port: 6667, enableFastSerialization: false, // Uses original serializers });
You might want to disable fast serialization if:
| Scenario | Legacy | Optimized | Improvement |
|---|---|---|---|
| Small batch (10 rows, 10 columns) | 2.5ms | 1.8ms | 1.4x |
| Medium batch (100 rows, 10 columns) | 15ms | 6ms | 2.5x |
| Large batch (1000 rows, 10 columns) | 180ms | 65ms | 2.8x |
| Mixed data types | 25ms | 10ms | 2.5x |
| Result Set Size | Iterator (objects) | toColumnar | Improvement |
|---|---|---|---|
| 1,000 rows | 45ms | 18ms | 2.5x |
| 10,000 rows | 520ms | 180ms | 2.9x |
| 100,000 rows | 5800ms | 1900ms | 3.1x |
Benchmarks performed on Node.js v20, Intel i7, 16GB RAM
// ❌ BAD: One-by-one inserts for (let i = 0; i < 1000; i++) { await session.insertTablet({ deviceId: 'root.test.device1', measurements: ['temp'], dataTypes: [TSDataType.FLOAT], timestamps: [Date.now() + i], values: [[25.5]], }); } // ✅ GOOD: Batch insert const batchSize = 100; await session.insertTablet({ deviceId: 'root.test.device1', measurements: ['temp'], dataTypes: [TSDataType.FLOAT], timestamps: Array.from({ length: batchSize }, (_, i) => Date.now() + i), values: Array.from({ length: batchSize }, () => [25.5]), });
// ✅ GOOD: Columnar processing for analytics const columnar = await dataSet.toColumnar(); const temps = columnar.values[0]; // Vectorized operations const avg = temps.reduce((a, b) => a + b, 0) / temps.length; const max = Math.max(...temps); const min = Math.min(...temps); await dataSet.close();
// For small result sets - use toColumnar() const smallDataSet = await session.executeQueryStatement('SELECT * FROM root.test LIMIT 100'); const columnar = await smallDataSet.toColumnar(); await smallDataSet.close(); // For large result sets - use iterator const largeDataSet = await session.executeQueryStatement('SELECT * FROM root.test'); while (await largeDataSet.hasNext()) { const row = largeDataSet.next(); await processRow(row); // Process with backpressure } await largeDataSet.close();
import { globalBufferPool } from '@iotdb/client'; // After warmup period setInterval(() => { const stats = globalBufferPool.getStats(); console.log(`Buffer Pool - Hit rate: ${stats.hitRate}, Pooled: ${stats.pooledBuffers}`); // If hit rate < 50%, consider adjusting batch sizes if (parseFloat(stats.hitRate) < 50) { console.warn('Low buffer pool hit rate - consider larger batch sizes'); } }, 60000); // Check every minute
// Clear buffer pool periodically in long-running processes import { globalBufferPool } from '@iotdb/client'; // Clear pool every hour to prevent potential memory bloat setInterval(() => { globalBufferPool.clear(); }, 3600000);
// Enable performance logging process.env.LOG_LEVEL = 'debug'; // Check serialization timings in logs: // [PERF] Values serialization: 5ms, buffer size: 4096 bytes // [PERF] Timestamp serialization (fast=true): 1ms
// Disable fast serialization for debugging const session = new Session({ host: 'localhost', port: 6667, enableFastSerialization: false, // Use legacy serializers });
Performance improvements are welcome! When contributing:
Apache License 2.0