pi-show-token-rate 0.1.0 → 0.1.1
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/README.md +6 -3
- package/package.json +1 -1
- package/src/rate.ts +55 -33
package/README.md
CHANGED
|
@@ -109,9 +109,12 @@ pnpm typecheck # tsc --noEmit
|
|
|
109
109
|
|
|
110
110
|
Tests cover the character estimator, the tracker (windowing, monotonicity,
|
|
111
111
|
pruning), the summary math, the exact status-line strings, and the controller's
|
|
112
|
-
throttling and toggle behavior. The
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
throttling and toggle behavior. The windowing test is a differential one: it
|
|
113
|
+
replays thousands of irregularly-spaced observations and checks `liveRate()`
|
|
114
|
+
against the trailing-window definition at every step, so the tracker's
|
|
115
|
+
incremental pruning cannot drift from it. The suite drives an injected clock,
|
|
116
|
+
so no test depends on wall-clock timing, and it uses a stub theme instead of
|
|
117
|
+
the real ANSI palette.
|
|
115
118
|
|
|
116
119
|
## License
|
|
117
120
|
|
package/package.json
CHANGED
package/src/rate.ts
CHANGED
|
@@ -33,6 +33,13 @@ const CHARS_PER_TOKEN = 4;
|
|
|
33
33
|
*/
|
|
34
34
|
export const LIVE_WINDOW_MS = 3_000;
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* How many consumed checkpoints may pile up in front of the cursor before
|
|
38
|
+
* they are reclaimed in one splice. Bounds the dead prefix to a few hundred
|
|
39
|
+
* bytes while amortizing the compaction over that many deltas.
|
|
40
|
+
*/
|
|
41
|
+
const PRUNE_SLACK = 64;
|
|
42
|
+
|
|
36
43
|
/**
|
|
37
44
|
* Where a token count came from:
|
|
38
45
|
*
|
|
@@ -128,11 +135,22 @@ function argumentsChars(args: unknown): number {
|
|
|
128
135
|
*
|
|
129
136
|
* Two checkpoint sets are kept: `#first` pins the start of the generation
|
|
130
137
|
* window so `finish()` can still average the whole response after pruning,
|
|
131
|
-
* and `#
|
|
132
|
-
*
|
|
138
|
+
* and `#recent` holds the trailing window that `liveRate()` reads.
|
|
139
|
+
*
|
|
140
|
+
* `#recent` is never rebuilt. Checkpoints arrive in non-decreasing `at` order
|
|
141
|
+
* and only ever fall off the *front* of the window, so `#head` walks the
|
|
142
|
+
* consumed prefix forward once per checkpoint over the whole response
|
|
143
|
+
* (amortized O(1)) instead of re-copying the window on every single delta.
|
|
144
|
+
* `liveRate()` reads exactly two points — the newest and the baseline — so
|
|
145
|
+
* the points in between are dead weight that only has to survive until they
|
|
146
|
+
* are consumed.
|
|
133
147
|
*/
|
|
134
148
|
export class RateTracker {
|
|
135
|
-
#
|
|
149
|
+
#recent: Checkpoint[] = [];
|
|
150
|
+
/** Index of the oldest checkpoint that can still become a baseline. */
|
|
151
|
+
#head = 0;
|
|
152
|
+
/** Newest checkpoint at/before the window cutoff, once one exists. */
|
|
153
|
+
#baseline: Checkpoint | undefined;
|
|
136
154
|
#first: WindowStart | undefined;
|
|
137
155
|
#armed = false;
|
|
138
156
|
|
|
@@ -143,7 +161,7 @@ export class RateTracker {
|
|
|
143
161
|
|
|
144
162
|
/** True once at least one token has been observed for this response. */
|
|
145
163
|
get streaming(): boolean {
|
|
146
|
-
return this.#
|
|
164
|
+
return this.#recent.length > this.#head;
|
|
147
165
|
}
|
|
148
166
|
|
|
149
167
|
/**
|
|
@@ -161,8 +179,7 @@ export class RateTracker {
|
|
|
161
179
|
|
|
162
180
|
/** Mark a response as in flight. Drops any state from the previous one. */
|
|
163
181
|
arm(): void {
|
|
164
|
-
this.#
|
|
165
|
-
this.#first = undefined;
|
|
182
|
+
this.#reset();
|
|
166
183
|
this.#armed = true;
|
|
167
184
|
}
|
|
168
185
|
|
|
@@ -180,7 +197,7 @@ export class RateTracker {
|
|
|
180
197
|
observe(at: number, tokens: number, source: TokenSource): void {
|
|
181
198
|
if (!this.#armed) this.arm();
|
|
182
199
|
|
|
183
|
-
const last = this.#
|
|
200
|
+
const last = this.#recent.at(-1);
|
|
184
201
|
if (last) {
|
|
185
202
|
if (at <= last.at || tokens < last.tokens) return;
|
|
186
203
|
// Stay on the scale the window opened with. `finish()` is where a
|
|
@@ -190,7 +207,7 @@ export class RateTracker {
|
|
|
190
207
|
this.#first = { at, tokens, source };
|
|
191
208
|
}
|
|
192
209
|
|
|
193
|
-
this.#
|
|
210
|
+
this.#recent.push({ at, tokens });
|
|
194
211
|
this.#prune(at);
|
|
195
212
|
}
|
|
196
213
|
|
|
@@ -199,15 +216,19 @@ export class RateTracker {
|
|
|
199
216
|
* `null` when nothing was observed yet, when the window holds a single
|
|
200
217
|
* point, or when no new token arrived inside it.
|
|
201
218
|
*
|
|
202
|
-
* The window length is not a parameter: `#prune` already
|
|
219
|
+
* The window length is not a parameter: `#prune` already retires anything
|
|
203
220
|
* older than `LIVE_WINDOW_MS`, so a wider window could only ever read from
|
|
204
221
|
* checkpoints that no longer exist.
|
|
205
222
|
*/
|
|
206
223
|
liveRate(): number | null {
|
|
207
|
-
const current = this.#
|
|
224
|
+
const current = this.#recent.at(-1);
|
|
208
225
|
if (!current) return null;
|
|
209
226
|
|
|
210
|
-
|
|
227
|
+
// Before anything has aged out of the window there is no baseline yet;
|
|
228
|
+
// the oldest checkpoint standing then plays the same role.
|
|
229
|
+
const baseline = this.#baseline ?? this.#recent[this.#head];
|
|
230
|
+
if (!baseline) return null;
|
|
231
|
+
|
|
211
232
|
const elapsedMs = current.at - baseline.at;
|
|
212
233
|
const tokens = current.tokens - baseline.tokens;
|
|
213
234
|
if (elapsedMs <= 0 || tokens <= 0) return null;
|
|
@@ -223,9 +244,7 @@ export class RateTracker {
|
|
|
223
244
|
*/
|
|
224
245
|
finish(at: number, tokens: number, source: TokenSource): RateSample | null {
|
|
225
246
|
const first = this.#first;
|
|
226
|
-
this.#
|
|
227
|
-
this.#first = undefined;
|
|
228
|
-
this.#armed = false;
|
|
247
|
+
this.#reset();
|
|
229
248
|
|
|
230
249
|
if (!first) return null;
|
|
231
250
|
const elapsedMs = at - first.at;
|
|
@@ -247,32 +266,35 @@ export class RateTracker {
|
|
|
247
266
|
|
|
248
267
|
/** Drop the in-flight response without producing a sample. */
|
|
249
268
|
abort(): void {
|
|
250
|
-
this.#
|
|
251
|
-
this.#first = undefined;
|
|
252
|
-
this.#armed = false;
|
|
269
|
+
this.#reset();
|
|
253
270
|
}
|
|
254
271
|
|
|
255
|
-
/**
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
return baseline;
|
|
272
|
+
/** Forget every checkpoint and the generation window that opened on it. */
|
|
273
|
+
#reset(): void {
|
|
274
|
+
this.#recent = [];
|
|
275
|
+
this.#head = 0;
|
|
276
|
+
this.#baseline = undefined;
|
|
277
|
+
this.#first = undefined;
|
|
278
|
+
this.#armed = false;
|
|
264
279
|
}
|
|
265
280
|
|
|
266
|
-
/**
|
|
281
|
+
/**
|
|
282
|
+
* Retire the checkpoints that just fell out of the trailing window, keeping
|
|
283
|
+
* the newest of them as the next baseline. The cursor only moves forward, so
|
|
284
|
+
* each checkpoint is visited once for the whole response; the consumed
|
|
285
|
+
* prefix is dropped in one splice once it has grown past {@link PRUNE_SLACK},
|
|
286
|
+
* which keeps `#recent` bounded without paying for it on every delta.
|
|
287
|
+
*/
|
|
267
288
|
#prune(now: number): void {
|
|
268
289
|
const cutoff = now - LIVE_WINDOW_MS;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
290
|
+
while (this.#recent[this.#head]?.at <= cutoff) {
|
|
291
|
+
this.#baseline = this.#recent[this.#head];
|
|
292
|
+
this.#head += 1;
|
|
293
|
+
}
|
|
294
|
+
if (this.#head > PRUNE_SLACK) {
|
|
295
|
+
this.#recent.splice(0, this.#head);
|
|
296
|
+
this.#head = 0;
|
|
274
297
|
}
|
|
275
|
-
this.#checkpoints = baseline ? [baseline, ...kept] : kept;
|
|
276
298
|
}
|
|
277
299
|
}
|
|
278
300
|
|