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.
Files changed (3) hide show
  1. package/README.md +6 -3
  2. package/package.json +1 -1
  3. 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 suite drives an injected clock, so no test
113
- depends on wall-clock timing, and it uses a stub theme instead of the real ANSI
114
- palette.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-show-token-rate",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shows the live output token rate (tok/s) of the current model in the pi TUI status bar.",
5
5
  "keywords": [
6
6
  "pi-package",
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 `#checkpoints` holds only the trailing window that `liveRate()`
132
- * reads.
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
- #checkpoints: Checkpoint[] = [];
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.#checkpoints.length > 0;
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.#checkpoints = [];
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.#checkpoints.at(-1);
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.#checkpoints.push({ at, tokens });
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 discards anything
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.#checkpoints.at(-1);
224
+ const current = this.#recent.at(-1);
208
225
  if (!current) return null;
209
226
 
210
- const baseline = this.#baselineAt(current.at);
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.#checkpoints = [];
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.#checkpoints = [];
251
- this.#first = undefined;
252
- this.#armed = false;
269
+ this.#reset();
253
270
  }
254
271
 
255
- /** Newest checkpoint at/before the window start; falls back to the oldest. */
256
- #baselineAt(now: number): Checkpoint {
257
- const cutoff = now - LIVE_WINDOW_MS;
258
- let baseline = this.#checkpoints[0];
259
- for (const checkpoint of this.#checkpoints) {
260
- if (checkpoint.at <= cutoff) baseline = checkpoint;
261
- else break;
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
- /** Keep the window's tail plus the single baseline in front of it. */
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
- let baseline: Checkpoint | undefined;
270
- const kept: Checkpoint[] = [];
271
- for (const checkpoint of this.#checkpoints) {
272
- if (checkpoint.at <= cutoff) baseline = checkpoint;
273
- else kept.push(checkpoint);
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