Benchmarks: pond-ts vs pondjs
pond-ts is a ground-up TypeScript rewrite of pondjs. 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/renamebecame 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
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 → aggregateon streaming data filter()as a first-class TimeSeries method (pondjs requires Pipeline)diff(),pctChange(),cumulative(),shift()columnar primitivesgroupBy()with optional transform callbackbfill(backward fill) strategy- TypeScript-first schema types that flow through every operation