Setting the file. One moment.
Iot Wasm Headroom Bench · Clickhouse Js Node Rowbinary · ClickHouse/agent-skills · Skills Docs
ContentsBack to the top of the page tests/iot.wasm-headroom.bench.ts
tests/ iot.wasm-headroom.bench.ts
TypeScript · 132 lines · 5 KB
* A WASM parser can read bytes, but it CANNOT allocate JS objects / strings /
11 * BigInts / Dates — those must be materialized on the JS side whatever decodes
12 * the bytes. So the maximum a WASM parser could ever shave off our current
13 * row-object decode is bounded by:
14 *
15 * (full row-object decode time) − (unavoidable JS-side materialization)
16 *
17 * We bracket that headroom with three decoders over the SAME IoT buffer (the
18 * best case for RowBinary — every column fixed-width numeric):
19 *
20 * 1. rows — the current fast reader: builds {…} objects + Date per row.
21 * 2. columnar — same reads, written into preallocated typed arrays, NO
22 * per-row objects. The "different output contract" a WASM
23 * parser would target.
24 * 3. parseOnly — same reads, accumulated into a scalar checksum, allocates
25 * NOTHING. The pure byte-arithmetic floor: a WASM parser
26 * cannot beat this slice by much (V8 already compiles DataView
27 * reads to native loads), and still has to pay it.
28 *
29 * Read the gaps: rows→parseOnly is the materialization WASM can't remove;
30 * rows→columnar is the win available in plain JS by changing the output shape.
31 */
32 const N = 50_000 ;
33 const SELECT =
34 `SELECT toUInt32(number % 1000) AS sensor_id, ` +
35 `toDateTime64(1700000000 + number, 3) AS ts, ` +
36 `20 + (number % 1500) / 100 AS temperature, ` +
37 `30 + (number % 7000) / 100 AS humidity, ` +
38 `980 + (number % 6000) / 100 AS pressure, ` +
39 `toFloat32(3 + (number % 200) / 100) AS battery, ` +
40 `toUInt8(number % 4) AS status ` +
41 `FROM numbers(${ N })` ;
42 const BUF = await query ( `${ SELECT } FORMAT RowBinary` );
43 const ROW_BYTES = 41 ;
44
45 // 1. Current output contract: an array of row objects.
46 function decodeRows () : IotRow [] {
47 const s = new Cursor ( BUF );
48 const out : IotRow [] = [];
49 while (s.pos < s.buf. length ) out. push ( readIotRowFast (s));
50 return out;
51 }
52
53 type Columns = {
54 sensor_id : Uint32Array ;
55 ts : Float64Array ; // epoch ms
56 temperature : Float64Array ;
57 humidity : Float64Array ;
58 pressure : Float64Array ;
59 battery : Float32Array ;
60 status : Uint8Array ;
61 };
62
63 // 2. Columnar contract: straight into typed arrays, no per-row objects.
64 function decodeColumnar () : Columns {
65 const view = new DataView ( BUF .buffer, BUF .byteOffset, BUF .byteLength);
66 const n = ( BUF . length / ROW_BYTES ) | 0 ;
67 const c : Columns = {
68 sensor_id: new Uint32Array (n),
69 ts: new Float64Array (n),
70 temperature: new Float64Array (n),
71 humidity: new Float64Array (n),
72 pressure: new Float64Array (n),
73 battery: new Float32Array (n),
74 status: new Uint8Array (n),
75 };
76 let o = 0 ;
77 for ( let i = 0 ; i < n; i ++ ) {
78 c.sensor_id[i] = view. getUint32 (o, true );
79 c.ts[i] = Number (view. getBigInt64 (o + 4 , true ));
80 c.temperature[i] = view. getFloat64 (o + 12 , true );
81 c.humidity[i] = view. getFloat64 (o + 20 , true );
82 c.pressure[i] = view. getFloat64 (o + 28 , true );
83 c.battery[i] = view. getFloat32 (o + 36 , true );
84 c.status[i] = BUF [o + 40 ] ! ;
85 o += ROW_BYTES ;
86 }
87 return c;
88 }
89
90 // 3. Pure parse floor: read everything, allocate nothing, fold into a checksum.
91 let sink = 0 ;
92 function parseOnly () : number {
93 const view = new DataView ( BUF .buffer, BUF .byteOffset, BUF .byteLength);
94 const n = ( BUF . length / ROW_BYTES ) | 0 ;
95 let acc = 0 ;
96 let o = 0 ;
97 for ( let i = 0 ; i < n; i ++ ) {
98 acc += view. getUint32 (o, true );
99 acc += Number (view. getBigInt64 (o + 4 , true ));
100 acc += view. getFloat64 (o + 12 , true );
101 acc += view. getFloat64 (o + 20 , true );
102 acc += view. getFloat64 (o + 28 , true );
103 acc += view. getFloat32 (o + 36 , true );
104 acc += BUF [o + 40 ] ! ;
105 o += ROW_BYTES ;
106 }
107 return (sink = acc); // observable, so V8 can't elide the reads
108 }
109
110 // sanity: all three agree on row count / a sampled value
111 {
112 const rows = decodeRows ();
113 const cols = decodeColumnar ();
114 if (rows. length !== N || cols.sensor_id. length !== N )
115 throw new Error ( "headroom: row count" );
116 if (rows[ 123 ] ! .temperature !== cols.temperature[ 123 ])
117 throw new Error ( "headroom: value mismatch" );
118 parseOnly ();
119 if ( ! Number. isFinite (sink)) throw new Error ( "headroom: checksum" );
120 }
121
122 describe ( "WASM headroom on IoT RowBinary (best case for RowBinary)" , () => {
123 bench ( "rows — current fast reader (objects + Date)" , () => {
124 decodeRows ();
125 });
126 bench ( "columnar — into typed arrays (no per-row objects)" , () => {
127 decodeColumnar ();
128 });
129 bench ( "parseOnly — reads only, zero allocation (the WASM floor)" , () => {
130 parseOnly ();
131 });
132 });