@pond-ts/react 0.15.0 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +170 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,179 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
7
7
|
file covers both packages. Pre-1.0: minor bumps may include new features and
|
|
8
8
|
type-level changes; patch bumps are strictly additive.
|
|
9
9
|
|
|
10
|
-
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.15.
|
|
10
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.15.2...HEAD
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.15.2] — 2026-05-06
|
|
15
|
+
|
|
16
|
+
Performance fix for live rolling at firehose rates. The gRPC
|
|
17
|
+
experiment's step 6
|
|
18
|
+
([pond-grpc-experiment#26](https://github.com/pjm17971/pond-grpc-experiment/pull/26))
|
|
19
|
+
attempted to use the non-partitioned `live.rolling({...}, opts)`
|
|
20
|
+
overload for global counters and saw throughput collapse from 88k/s
|
|
21
|
+
to 21k/s — a 4× regression even worse than the V7→V6 gap that
|
|
22
|
+
motivated v0.15.0. The cliff is the same `Array.shift()` pattern
|
|
23
|
+
already flagged as queued tactical work in PLAN; the gRPC encounter
|
|
24
|
+
made it urgent.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Eviction is now O(1) per ingest in all live rolling classes.**
|
|
29
|
+
Replaced `entries.shift()` (worst-case O(N) on the deque length)
|
|
30
|
+
with a head-index pointer + periodic batched compaction:
|
|
31
|
+
- `LiveFusedRolling.#compactFront` — non-partitioned multi-window
|
|
32
|
+
- `LivePartitionedFusedRolling.#compactPartitionFront` —
|
|
33
|
+
per-partition fused
|
|
34
|
+
- `LiveRollingAggregation.#removeFirst` — single-window
|
|
35
|
+
non-partitioned
|
|
36
|
+
- `LivePartitionedSyncRolling.#evictPartition` — per-partition
|
|
37
|
+
single-window synced
|
|
38
|
+
|
|
39
|
+
The pattern: track a `frontIdx` field; "evicting" advances the
|
|
40
|
+
pointer instead of shifting. When the dead prefix grows past
|
|
41
|
+
half the array length, batch-splice it off and reset the
|
|
42
|
+
pointer. Per-event cost stays O(1) amortized at every live-
|
|
43
|
+
window size — each surviving entry is copied at most once
|
|
44
|
+
between two compactions, and compactions fire at most every
|
|
45
|
+
(live-size) events.
|
|
46
|
+
|
|
47
|
+
An earlier draft also compacted on a fixed 1024-entry threshold;
|
|
48
|
+
Codex's adversarial review on PR #119 caught that this would
|
|
49
|
+
reintroduce O(live_size / 1024) per-eviction cost on large
|
|
50
|
+
windows (100k+ live entries) — the threshold would fire
|
|
51
|
+
repeatedly and copy the entire live slice each time. The
|
|
52
|
+
proportional guard alone has the right amortization invariant.
|
|
53
|
+
|
|
54
|
+
### Performance
|
|
55
|
+
|
|
56
|
+
`packages/core/scripts/perf-fused-rolling.mjs` — new regression
|
|
57
|
+
scenario that reproduces the cliff (50k-event deque with continuous
|
|
58
|
+
eviction):
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Worst-case shift pattern (50s window, 50k fill + 50k evict):
|
|
62
|
+
median (ms) min (ms) max (ms)
|
|
63
|
+
pre-fix 1123.12 1118.47 1149.95
|
|
64
|
+
v0.15.2 53.00 52.34 53.56
|
|
65
|
+
speedup 21.2×
|
|
66
|
+
|
|
67
|
+
Steady-state deque, no eviction (5m window, 200k events):
|
|
68
|
+
median (ms) min (ms) max (ms)
|
|
69
|
+
pre-fix 91.28 89.84 97.04
|
|
70
|
+
v0.15.2 99.28 96.80 103.94
|
|
71
|
+
delta +9% (within noise)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The fix targets the eviction-loop case specifically. Workloads with
|
|
75
|
+
no eviction (or rare eviction relative to ingest) see no change —
|
|
76
|
+
V8's internal hidden-offset optimization handles those well. The
|
|
77
|
+
cliff appears once eviction fires per-ingest at large deque size,
|
|
78
|
+
which is exactly the firehose-rolling shape.
|
|
79
|
+
|
|
80
|
+
### Why the cliff was hidden
|
|
81
|
+
|
|
82
|
+
V8's `Array.shift()` is amortized O(1) for shift-heavy workloads up
|
|
83
|
+
to ~10k-element arrays — it maintains a hidden offset and only
|
|
84
|
+
periodically compacts. Beyond that size or with mixed access
|
|
85
|
+
patterns, the optimization breaks down and shift falls back to true
|
|
86
|
+
O(N) memcpy. The bench scales from 1k to 50k deque sizes and the
|
|
87
|
+
cliff appears around 30k-40k. Pond's tests pin behavior at small
|
|
88
|
+
window sizes; the cliff was invisible to the test suite, only
|
|
89
|
+
showed up under the gRPC experiment's firehose load.
|
|
90
|
+
|
|
91
|
+
### What this unlocks
|
|
92
|
+
|
|
93
|
+
The agent's manual-counter workaround in `aggregator/src/aggregate.ts`
|
|
94
|
+
can now drop. The natural shape — a non-partitioned
|
|
95
|
+
`live.rolling({...}, { trigger })` over the firehose — is now
|
|
96
|
+
viable at the rates the experiment cares about. PLAN's
|
|
97
|
+
"`samples` reducer would exhibit a similar shape at firehose"
|
|
98
|
+
caveat also resolves: same fix in the same call sites covers
|
|
99
|
+
samples too.
|
|
100
|
+
|
|
101
|
+
### Note for downstream consumers
|
|
102
|
+
|
|
103
|
+
This is a **strict-additive perf fix.** All output behavior is
|
|
104
|
+
preserved — same eviction order, same emission timing, same
|
|
105
|
+
snapshot values. The deque's internal representation changed
|
|
106
|
+
(`#entries[0]` may now be a logically-evicted entry until periodic
|
|
107
|
+
compaction); any downstream code reading `#entries` directly would
|
|
108
|
+
break, but those fields are private. Public APIs and types are
|
|
109
|
+
unchanged.
|
|
110
|
+
|
|
111
|
+
[0.15.2]: https://github.com/pjm17971/pond-ts/compare/v0.15.1...v0.15.2
|
|
112
|
+
|
|
113
|
+
## [0.15.1] — 2026-05-05
|
|
114
|
+
|
|
115
|
+
Type-narrowing follow-up to v0.15.0. The fused partitioned-rolling
|
|
116
|
+
typing chain exposed a pre-existing pond limitation where
|
|
117
|
+
`partitionBy('host')` widened the partition-column type instead of
|
|
118
|
+
narrowing it to the literal `'host'`. The gRPC experiment's V8
|
|
119
|
+
migration ([pond-grpc-experiment#22](https://github.com/pjm17971/pond-grpc-experiment/pull/22))
|
|
120
|
+
worked around it as `partitionBy<'host'>('host')` — clobbering the
|
|
121
|
+
value-type parameter `K` to fill the column-name slot. v0.15.1
|
|
122
|
+
captures the column literal directly so the workaround can drop.
|
|
123
|
+
|
|
124
|
+
### Fixed
|
|
125
|
+
|
|
126
|
+
- **`partitionBy` narrows the partition column literal.** The
|
|
127
|
+
`by` argument's literal type now flows into a new `ByCol`
|
|
128
|
+
generic on `LivePartitionedSeries<S, K, ByCol>` and
|
|
129
|
+
`LivePartitionedView<SBase, R, K, ByCol>`. Threaded through every
|
|
130
|
+
per-partition method (`fill`, `diff`, `rate`, `pctChange`,
|
|
131
|
+
`cumulative`, `apply`, the rolling overloads). The fused
|
|
132
|
+
partitioned-rolling overload's
|
|
133
|
+
`FusedPartitionedRollingSchema<S, ByCol, FM>` now resolves
|
|
134
|
+
correctly without the `<'host'>` workaround:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// Before v0.15.1: needed the explicit type arg to narrow
|
|
138
|
+
// host through the fused-rolling schema chain.
|
|
139
|
+
live.partitionBy<'host'>('host').rolling({ ... }, { trigger });
|
|
140
|
+
|
|
141
|
+
// v0.15.1+: the literal 'host' is captured automatically.
|
|
142
|
+
live.partitionBy('host').rolling({ ... }, { trigger });
|
|
143
|
+
// Output schema includes `host` narrowed to its column kind;
|
|
144
|
+
// event.get('host') resolves correctly.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Existing V8 callers using the `partitionBy<'host'>('host')`
|
|
148
|
+
workaround continue to narrow correctly. Type-parameter order
|
|
149
|
+
on `partitionBy` is `<ByCol, K>` (column name first, value type
|
|
150
|
+
second) so the explicit `<'host'>` binds the literal to `ByCol`
|
|
151
|
+
— exactly what the workaround intended pre-v0.15.1. The
|
|
152
|
+
workaround can now drop because automatic inference does the
|
|
153
|
+
same job, but it doesn't have to.
|
|
154
|
+
|
|
155
|
+
### Type system
|
|
156
|
+
|
|
157
|
+
- `LivePartitionedSeries<S, K, ByCol>` — third generic added with
|
|
158
|
+
default `keyof EventDataForSchema<S> & string`. Backwards-
|
|
159
|
+
compatible: existing references to `LivePartitionedSeries<S, K>`
|
|
160
|
+
and `LivePartitionedSeries<S>` resolve to the upper-bound default.
|
|
161
|
+
- `LivePartitionedView<SBase, R, K, ByCol>` — same shape; `ByCol`
|
|
162
|
+
threaded through every chain hop so partition-column literals
|
|
163
|
+
survive `partitionBy('host').fill(...).rolling({...}, opts)`.
|
|
164
|
+
|
|
165
|
+
### Test surface
|
|
166
|
+
|
|
167
|
+
`test-d/fused-rolling.test-d.ts` extended to pin the narrowing at
|
|
168
|
+
both the root and chained levels:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const fC = live.partitionBy('host').rolling({ ... }, { trigger });
|
|
172
|
+
sampleEvent.get('host'); // narrows to string | undefined
|
|
173
|
+
|
|
174
|
+
const chained = live.partitionBy('host').fill({ cpu: 'hold' })
|
|
175
|
+
.rolling({ '1m': { cpu_avg: ... } }, { trigger });
|
|
176
|
+
chainedSample.get('host'); // narrows correctly through the chain
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
All 1115 + 55 runtime tests still pass; type-d clean.
|
|
180
|
+
|
|
181
|
+
[0.15.1]: https://github.com/pjm17971/pond-ts/compare/v0.15.0...v0.15.1
|
|
182
|
+
|
|
14
183
|
## [0.15.0] — 2026-05-05
|
|
15
184
|
|
|
16
185
|
The "fused multi-window rolling" release. Shipping the primitive
|