Skip to main content

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.

CategorySpeedup at N=16kKey architectural difference
Rate~120xSingle columnar walk vs Pipeline materialization
Fill77–87xSingle columnar pass per strategy vs Pipeline/column
Aggregation57–82xO(N+B) single-pass bucketing vs O(N×B) Pipeline
Statistics18–80xDirect typed-array reduce vs ImmutableJS iteration
Alignment42xForward cursor vs repeated binary search
Construction13xColumnar intake + frozen objects vs ImmutableJS
Chained8xDerived constructors vs per-step Pipeline + collect
Transformsselect/rename instant; collapse 30x; map ~4xColumn-store reshapes vs Pipeline per event
Event access6xArray indexing vs ImmutableJS get()
Serialization4xLightweight 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

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