pi-tps-meter 1.0.0

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 ADDED
@@ -0,0 +1,32 @@
1
+ # pi-tps-meter
2
+
3
+ Tokens per second meter for [pi CLI](https://pi.dev).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install npm:pi-tps-meter
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Footer shows live stats during and after streaming:
14
+
15
+ ```
16
+ ⚡ 42 tps (during streaming, updates every 500ms)
17
+ TPS: 42 avg | μ 39 | p95 61 (after message completes)
18
+ ```
19
+
20
+ - **avg** — rolling average over last 60 seconds
21
+ - **μ** — all-time mean
22
+ - **p95** — 95th percentile of all measurements
23
+
24
+ No config needed. Works out of the box.
25
+
26
+ ## Author
27
+
28
+ Venkata Sai Chirasani
29
+
30
+ ## License
31
+
32
+ MIT
@@ -0,0 +1,159 @@
1
+ /**
2
+ * TPS Meter — Tokens Per Second
3
+ *
4
+ * Displays in footer status bar next to caveman level:
5
+ * TPS: 42.1 avg | μ 38.7 | p95 61.2
6
+ *
7
+ * - Rolling avg: last 60s window
8
+ * - μ (mean): all-time average
9
+ * - p95: 95th percentile of all recorded TPS values
10
+ *
11
+ * Uses character count / 4 as token estimate.
12
+ * Fires on message_update (text_delta) and finalizes on message_end.
13
+ */
14
+
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+
17
+ // --- Config ---
18
+ const WINDOW_MS = 60_000; // 1 minute rolling window
19
+ const CHARS_PER_TOKEN = 4;
20
+ const UPDATE_INTERVAL_MS = 500; // throttle status bar updates
21
+
22
+ // --- State ---
23
+ interface StreamSample {
24
+ tokens: number;
25
+ endMs: number; // when this sample was recorded
26
+ }
27
+
28
+ let streamStartMs = 0;
29
+ let streamTokens = 0;
30
+ let lastUpdateMs = 0;
31
+
32
+ // Rolling window (last 60s)
33
+ const window: StreamSample[] = [];
34
+
35
+ // All-time for mean and p95
36
+ const allTime: number[] = []; // TPS values per assistant message
37
+
38
+ // --- Helpers ---
39
+
40
+ function now(): number {
41
+ return Date.now();
42
+ }
43
+
44
+ function estimateTokens(chars: number): number {
45
+ return Math.max(1, Math.round(chars / CHARS_PER_TOKEN));
46
+ }
47
+
48
+ function pruneWindow(): void {
49
+ const cutoff = now() - WINDOW_MS;
50
+ while (window.length > 0 && window[0].endMs < cutoff) {
51
+ window.shift();
52
+ }
53
+ }
54
+
55
+ function rollingTps(): number {
56
+ pruneWindow();
57
+ if (window.length === 0) return 0;
58
+ const totalTokens = window.reduce((s, w) => s + w.tokens, 0);
59
+ const spanMs = window[window.length - 1].endMs - window[0].endMs;
60
+ if (spanMs < 100) return 0; // too short to measure
61
+ return (totalTokens / spanMs) * 1000;
62
+ }
63
+
64
+ function meanTps(): number {
65
+ if (allTime.length === 0) return 0;
66
+ return allTime.reduce((a, b) => a + b, 0) / allTime.length;
67
+ }
68
+
69
+ function p95Tps(): number {
70
+ if (allTime.length === 0) return 0;
71
+ const sorted = [...allTime].sort((a, b) => a - b);
72
+ const idx = Math.ceil(sorted.length * 0.95) - 1;
73
+ return sorted[Math.max(0, idx)];
74
+ }
75
+
76
+ function formatTps(v: number): string {
77
+ return v < 10 ? v.toFixed(1) : v.toFixed(0);
78
+ }
79
+
80
+ function statusText(): string {
81
+ const avg = rollingTps();
82
+ const mu = meanTps();
83
+ const p95 = p95Tps();
84
+ if (avg === 0 && mu === 0) return "";
85
+ return `TPS: ${formatTps(avg)} avg | μ ${formatTps(mu)} | p95 ${formatTps(p95)}`;
86
+ }
87
+
88
+ // --- Extension ---
89
+
90
+ export default function tpsMeter(pi: ExtensionAPI): void {
91
+
92
+ // Reset on new assistant message
93
+ pi.on("message_start", async (event) => {
94
+ if (event.message.role !== "assistant") return;
95
+ streamStartMs = now();
96
+ streamTokens = 0;
97
+ lastUpdateMs = 0;
98
+ });
99
+
100
+ // Count tokens from stream deltas, update status bar periodically
101
+ pi.on("message_update", async (event, ctx) => {
102
+ if (event.message.role !== "assistant") return;
103
+ if (!event.assistantMessageEvent) return;
104
+
105
+ const evt = event.assistantMessageEvent;
106
+
107
+ // Count text and thinking deltas
108
+ if (evt.type === "text_delta" || evt.type === "thinking_delta") {
109
+ const delta = evt.delta as string;
110
+ streamTokens += estimateTokens(delta.length);
111
+ }
112
+
113
+ // Throttle status bar updates
114
+ const t = now();
115
+ if (t - lastUpdateMs < UPDATE_INTERVAL_MS) return;
116
+ lastUpdateMs = t;
117
+
118
+ // Live TPS during streaming
119
+ const elapsed = (t - streamStartMs) / 1000;
120
+ if (elapsed < 0.3) return; // avoid flicker at start
121
+
122
+ const liveTps = streamTokens / elapsed;
123
+ ctx.ui.setStatus("tps", ctx.ui.theme.fg("accent", `⚡ ${formatTps(liveTps)} tps`));
124
+ });
125
+
126
+ // Finalize: record TPS for this message, update rolling + all-time stats
127
+ pi.on("message_end", async (event, ctx) => {
128
+ if (event.message.role !== "assistant") return;
129
+
130
+ const elapsed = (now() - streamStartMs) / 1000;
131
+ if (elapsed < 0.1 || streamTokens === 0) return;
132
+
133
+ const tps = streamTokens / elapsed;
134
+
135
+ // Record sample
136
+ const sample: StreamSample = { tokens: streamTokens, endMs: now() };
137
+ window.push(sample);
138
+ allTime.push(tps);
139
+
140
+ // Cap all-time history (keep last 500 measurements)
141
+ if (allTime.length > 500) allTime.splice(0, allTime.length - 500);
142
+
143
+ // Update status bar with aggregate stats
144
+ const txt = statusText();
145
+ if (txt) {
146
+ ctx.ui.setStatus("tps", ctx.ui.theme.fg("accent", txt));
147
+ }
148
+ });
149
+
150
+ // Clear on session start
151
+ pi.on("session_start", async (_event, ctx) => {
152
+ streamStartMs = 0;
153
+ streamTokens = 0;
154
+ lastUpdateMs = 0;
155
+ window.length = 0;
156
+ allTime.length = 0;
157
+ ctx.ui.setStatus("tps", undefined);
158
+ });
159
+ }
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "pi-tps-meter",
3
+ "version": "1.0.0",
4
+ "description": "Tokens per second meter for pi CLI — live TPS, rolling avg, mean, p95",
5
+ "author": "Venkata Sai Chirasani",
6
+ "license": "MIT",
7
+ "keywords": ["pi-package", "pi-extension", "tps", "tokens-per-second", "performance"],
8
+ "pi": {
9
+ "extensions": ["./extensions"]
10
+ },
11
+ "peerDependencies": {
12
+ "@earendil-works/pi-coding-agent": "*",
13
+ "@earendil-works/pi-ai": "*",
14
+ "@earendil-works/pi-agent-core": "*"
15
+ },
16
+ "files": ["extensions"],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/vskrch/pi-tps-meter.git"
20
+ }
21
+ }