--- sidebar_position: 15 --- # Benchmarks: pond-ts vs pondjs pond-ts is a ground-up TypeScript rewrite of [pondjs](https://github.com/esnet/pond). This page documents how the two libraries compare across all shared core operations. ## Summary **pond-ts is faster on every benchmark** — 54 out of 54 operations tested, across three data sizes (1k, 4k, 16k events), with zero regressions. The geometric mean speedup across the **measurable** operations is **~17x**, and the advantage grows with data size. A further **6 operations** (`select` / `rename` at each size) are **effectively instant** — pond-ts runs them below the timer's usable resolution (`<0.01 ms`), so they're reported as "instant" rather than a misleading `pondjs / ~0` ratio and are excluded from the geometric mean. | Category | Speedup at N=16k | Key architectural difference | | ------------- | ---------------------------------------------------- | ---------------------------------------------------- | | Rate | ~120x | Single columnar walk vs Pipeline materialization | | Fill | 77–87x | Single columnar pass per strategy vs Pipeline/column | | Aggregation | 57–82x | O(N+B) single-pass bucketing vs O(N×B) Pipeline | | Statistics | 18–80x | Direct typed-array reduce vs ImmutableJS iteration | | Alignment | 42x | Forward cursor vs repeated binary search | | Construction | 13x | Columnar intake + frozen objects vs ImmutableJS | | Chained | 8x | Derived constructors vs per-step Pipeline + collect | | Transforms | `select`/`rename` instant; `collapse` 30x; `map` ~4x | Column-store reshapes vs Pipeline per event | | Event access | 6x | Array indexing vs ImmutableJS `get()` | | Serialization | 4x | Lightweight columnar representation | The narrowest measurable gaps are `map()` (~2–4x, both libraries call a user-supplied function per event) and event access / serialization (~2–6x). > These are single-machine medians (Node, medians-of-20, pondjs 0.9.0); treat the > absolute milliseconds as illustrative and the ratios as the signal. The numbers > grew substantially over earlier releases as pond-ts moved to a columnar store > (the v0.2x "columnar wave") — transforms like `select` / `rename` became O(1) > metadata rebinds rather than per-event work. ## Why it's faster A few architectural decisions account for most of the improvement: ### 1. A columnar store, not per-event Pipelines pondjs routes every operation through a Pipeline abstraction — even simple transforms like `select()` or `rename()` construct a pipeline, push events through observable nodes, and collect output into keyed collections. This is flexible but adds constant per-event overhead that dominates at scale. pond-ts keeps data in a columnar store. Many transforms never touch per-event objects at all: `select` and `rename` are **metadata-only column rebinds** (the underlying typed-array buffers are shared by reference), which is why they clock below the timer's resolution. Aggregation, fill, rate, and the statistical reducers walk the typed arrays directly, once. ### 2. No ImmutableJS pondjs wraps event data in ImmutableJS maps. Every `get()` call, every `setData()`, and every event construction pays for ImmutableJS overhead. pond-ts uses plain frozen JavaScript objects (materialized lazily from the column store only when you actually read events). `Object.freeze()` provides the same immutability guarantee at near-zero cost; event access is a property lookup, not a map traversal. ### 3. Algorithmic improvements Several operations were re-implemented with better time complexity: - **Aggregation**: O(N+B) single-pass bucketing instead of O(N×B) window scanning per bucket. - **Rolling windows**: O(N) sliding window with incremental add/remove instead of O(N²) recomputation. - **Alignment**: Forward cursor that advances through the source array instead of binary search per output point. - **`includesKey()`**: O(log N) bisect instead of O(N) linear scan. ## Running the benchmarks ```bash npm run build node bench/vs-pondjs.cjs ``` The script measures median execution time over 20 iterations (3 warmup rounds) at N = 1000, 4000, and 16000 events, against pondjs 0.9.0. Operations whose pond-ts median falls below the timer's usable resolution (`<0.01 ms`) are reported as **instant** and excluded from the geometric mean — quoting a `pondjs / ~0` ratio there would be meaningless. ## Detailed results ### Construction ``` Operation N pondjs (ms) pond-ts (ms) Speedup new TimeSeries() 1000 0.53 0.19 2.8x new TimeSeries() 4000 2.59 0.22 11.9x new TimeSeries() 16000 12.33 0.94 13.1x ``` Construction speedup grows with N because pondjs wraps each event's data in an ImmutableJS map during construction; pond-ts writes straight into columnar buffers. ### Aggregation ``` Operation N pondjs (ms) pond-ts (ms) Speedup aggregate(10s, avg) 1000 1.26 0.10 12.1x aggregate(1m, sum) 1000 0.85 0.04 19.9x aggregate(10s, avg+max+min) 1000 1.56 0.07 22.2x aggregate(10s, avg) 4000 4.74 0.17 27.7x aggregate(1m, sum) 4000 3.65 0.05 69.5x aggregate(10s, avg+max+min) 4000 6.54 0.16 42.0x aggregate(10s, avg) 16000 18.20 0.31 58.8x aggregate(1m, sum) 16000 13.49 0.17 81.5x aggregate(10s, avg+max+min) 16000 28.00 0.50 56.5x ``` Among the largest speedups in the suite. pond-ts's O(N+B) bucket assignment walks the packed column once, so the number of buckets barely affects total time. pondjs's Pipeline-based `fixedWindowRollup` does more work per event. ### Rate ``` Operation N pondjs (ms) pond-ts (ms) Speedup rate(value) 1000 0.78 0.09 8.3x rate(value) 4000 4.33 0.04 108.4x rate(value) 16000 22.01 0.18 119.7x ``` pondjs routes `rate()` through the Pipeline. pond-ts walks the column once, computing deltas in place. ### Fill ``` Operation N pondjs (ms) pond-ts (ms) Speedup fill(hold/pad) 1000 0.55 0.03 17.7x fill(zero) 1000 0.51 0.03 16.9x fill(linear) 1000 0.53 0.06 9.1x fill(hold/pad) 4000 2.44 0.13 19.4x fill(zero) 4000 2.35 0.03 77.2x fill(linear) 4000 2.44 0.03 70.4x fill(hold/pad) 16000 11.03 0.13 86.9x fill(zero) 16000 10.58 0.13 79.5x fill(linear) 16000 11.17 0.15 76.7x ``` pondjs's `fill()` constructs a Pipeline per call (and, for `linear` over multiple columns, a separate Pipeline per column). pond-ts handles all strategies in a single columnar pass. ### Transforms ``` Operation N pondjs (ms) pond-ts (ms) Speedup select(value) 1000 0.60 <0.01 instant map(x*2) 1000 0.57 0.28 2.0x collapse(a+b+c, sum) 1000 0.96 0.14 7.1x rename(value→measurement) 1000 0.84 <0.01 instant select(value) 4000 2.92 <0.01 instant map(x*2) 4000 2.68 0.57 4.7x collapse(a+b+c, sum) 4000 3.84 0.38 10.2x rename(value→measurement) 4000 3.79 <0.01 instant select(value) 16000 14.50 <0.01 instant map(x*2) 16000 14.47 3.24 4.5x collapse(a+b+c, sum) 16000 19.34 0.65 29.9x rename(value→measurement) 16000 18.90 <0.01 instant ``` `select` and `rename` are **metadata-only column rebinds** in pond-ts — they return a new series whose columns reference the same underlying buffers, with no per-event work — so they run below the timer's resolution at every size (reported as "instant"; pondjs still pushes every event through a Pipeline). `map()` shows the narrowest measurable gap because both libraries must call a user-provided function per event; the difference is construction overhead. ### Alignment ``` Operation N pondjs (ms) pond-ts (ms) Speedup align(5s, linear) 1000 1.04 0.11 9.6x align(5s, linear) 4000 3.73 0.13 28.6x align(10s, linear) 16000 14.11 0.33 42.3x ``` pond-ts uses a forward cursor that advances through the source array in sync with the output sequence. pondjs uses binary search per output point, giving O(M log N) vs pond-ts's O(N + M). ### Event access ``` Operation N pondjs (ms) pond-ts (ms) Speedup at(i).get() full scan 1000 0.17 0.08 2.2x at(i).get() full scan 4000 0.45 0.13 3.5x at(i).get() full scan 16000 1.83 0.30 6.1x ``` Pure data access. The gap is ImmutableJS `get()` vs plain property lookup on a lazily-materialized event. ### Serialization ``` Operation N pondjs (ms) pond-ts (ms) Speedup toJSON() 1000 0.31 0.12 2.6x toJSON() 4000 1.07 0.29 3.6x toJSON() 16000 5.36 1.23 4.4x ``` ### Chained operations ``` Operation N pondjs (ms) pond-ts (ms) Speedup map → select 1000 1.23 0.15 8.0x map → select 4000 6.04 0.60 10.1x map → select 16000 29.07 3.45 8.4x ``` Each step in pondjs creates a new Pipeline and materializes a new collection. pond-ts chains derived constructors on the column store without re-validating the output. ### Statistics ``` Operation N pondjs (ms) pond-ts (ms) Speedup median(value) 1000 0.26 0.03 8.5x stdev(value) 1000 0.23 0.02 12.5x median(value) 4000 1.35 0.07 20.2x stdev(value) 4000 0.90 0.02 58.1x median(value) 16000 8.25 0.47 17.7x stdev(value) 16000 4.40 0.05 80.3x ``` `stdev` reduces straight over the packed numeric column (Welford, single pass); `median` pays for a sort but still walks a typed array rather than ImmutableJS. ## Capabilities only in pond-ts Beyond performance, pond-ts adds functionality that pondjs does not have: - **Live streaming**: `LiveSeries`, `LiveView`, `LiveAggregation`, `LiveRollingAggregation` - **Live composition**: chain `filter → diff → fill → aggregate` on streaming data - **`filter()`** as a first-class TimeSeries method (pondjs requires Pipeline) - **`diff()`**, **`pctChange()`**, **`cumulative()`**, **`shift()`** columnar primitives - **`groupBy()`** with optional transform callback - **`bfill`** (backward fill) strategy - **TypeScript-first schema types** that flow through every operation