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 +32 -0
- package/extensions/tps-meter.ts +159 -0
- package/package.json +21 -0
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
|
+
}
|