opencode-tps-meter 0.1.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 +594 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/constants.d.ts +43 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +832 -0
- package/dist/index.mjs +786 -0
- package/dist/tokenCounter.d.ts +85 -0
- package/dist/tokenCounter.d.ts.map +1 -0
- package/dist/tracker.d.ts +17 -0
- package/dist/tracker.d.ts.map +1 -0
- package/dist/types.d.ts +322 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ui.d.ts +18 -0
- package/dist/ui.d.ts.map +1 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
19
|
+
var __toCommonJS = (from) => {
|
|
20
|
+
var entry = __moduleCache.get(from), desc;
|
|
21
|
+
if (entry)
|
|
22
|
+
return entry;
|
|
23
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
25
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
26
|
+
get: () => from[key],
|
|
27
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
28
|
+
}));
|
|
29
|
+
__moduleCache.set(from, entry);
|
|
30
|
+
return entry;
|
|
31
|
+
};
|
|
32
|
+
var __export = (target, all) => {
|
|
33
|
+
for (var name in all)
|
|
34
|
+
__defProp(target, name, {
|
|
35
|
+
get: all[name],
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
set: (newValue) => all[name] = () => newValue
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/index.ts
|
|
43
|
+
var exports_src = {};
|
|
44
|
+
__export(exports_src, {
|
|
45
|
+
default: () => TpsMeterPlugin
|
|
46
|
+
});
|
|
47
|
+
// OpenCode compatibility: unwrap plugin function from getter
|
|
48
|
+
module.exports = exports_src.default();
|
|
49
|
+
|
|
50
|
+
// src/constants.ts
|
|
51
|
+
var MIN_TPS_ELAPSED_MS = 250;
|
|
52
|
+
var DEFAULT_ROLLING_WINDOW_MS = 1000;
|
|
53
|
+
var MAX_BUFFER_SIZE = 100;
|
|
54
|
+
var MIN_WINDOW_DURATION_SECONDS = 0.1;
|
|
55
|
+
var DEFAULT_UPDATE_INTERVAL_MS = 50;
|
|
56
|
+
var MIN_TOAST_INTERVAL_MS = 150;
|
|
57
|
+
var DEFAULT_TOAST_DURATION_MS = 20000;
|
|
58
|
+
var FINAL_STATS_DURATION_MS = 2000;
|
|
59
|
+
var MAX_MESSAGE_AGE_MS = 5 * 60 * 1000;
|
|
60
|
+
var CLEANUP_INTERVAL_MS = 30000;
|
|
61
|
+
var CHARS_DIV_4 = 4;
|
|
62
|
+
var CHARS_DIV_3 = 3;
|
|
63
|
+
var WORDS_DIV_0_75 = 0.75;
|
|
64
|
+
var DEFAULT_SLOW_TPS_THRESHOLD = 10;
|
|
65
|
+
var DEFAULT_FAST_TPS_THRESHOLD = 50;
|
|
66
|
+
var INVALID_FINISH_REASONS = new Set(["tool-calls", "unknown"]);
|
|
67
|
+
var COUNTABLE_PART_TYPES = new Set(["text", "reasoning"]);
|
|
68
|
+
|
|
69
|
+
// src/tracker.ts
|
|
70
|
+
function createTracker(options = {}) {
|
|
71
|
+
let startTime = Date.now();
|
|
72
|
+
let totalTokens = 0;
|
|
73
|
+
let buffer = [];
|
|
74
|
+
const sessionId = options.sessionId;
|
|
75
|
+
const windowMs = typeof options.rollingWindowMs === "number" && options.rollingWindowMs > 0 ? options.rollingWindowMs : DEFAULT_ROLLING_WINDOW_MS;
|
|
76
|
+
function pruneBuffer(now) {
|
|
77
|
+
const cutoff = now - windowMs;
|
|
78
|
+
let validStartIndex = 0;
|
|
79
|
+
while (validStartIndex < buffer.length && buffer[validStartIndex].timestamp < cutoff) {
|
|
80
|
+
validStartIndex++;
|
|
81
|
+
}
|
|
82
|
+
if (validStartIndex > 0) {
|
|
83
|
+
buffer = buffer.slice(validStartIndex);
|
|
84
|
+
}
|
|
85
|
+
if (buffer.length > MAX_BUFFER_SIZE) {
|
|
86
|
+
buffer = buffer.slice(-MAX_BUFFER_SIZE);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
recordTokens(count, timestamp) {
|
|
91
|
+
const ts = timestamp ?? Date.now();
|
|
92
|
+
totalTokens += count;
|
|
93
|
+
buffer.push({ timestamp: ts, count });
|
|
94
|
+
pruneBuffer(ts);
|
|
95
|
+
},
|
|
96
|
+
getInstantTPS() {
|
|
97
|
+
if (buffer.length === 0) {
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const cutoff = now - windowMs;
|
|
102
|
+
let tokensInWindow = 0;
|
|
103
|
+
let oldestTimestamp = now;
|
|
104
|
+
let newestTimestamp = 0;
|
|
105
|
+
for (const entry of buffer) {
|
|
106
|
+
if (entry.timestamp >= cutoff) {
|
|
107
|
+
tokensInWindow += entry.count;
|
|
108
|
+
if (entry.timestamp < oldestTimestamp) {
|
|
109
|
+
oldestTimestamp = entry.timestamp;
|
|
110
|
+
}
|
|
111
|
+
if (entry.timestamp > newestTimestamp) {
|
|
112
|
+
newestTimestamp = entry.timestamp;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (tokensInWindow === 0) {
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
const windowDurationMs = newestTimestamp - oldestTimestamp;
|
|
120
|
+
const windowDurationSeconds = windowDurationMs / 1000;
|
|
121
|
+
const effectiveDuration = Math.max(windowDurationSeconds, MIN_WINDOW_DURATION_SECONDS);
|
|
122
|
+
return tokensInWindow / effectiveDuration;
|
|
123
|
+
},
|
|
124
|
+
getAverageTPS() {
|
|
125
|
+
const elapsedSeconds = (Date.now() - startTime) / 1000;
|
|
126
|
+
if (elapsedSeconds === 0) {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
return totalTokens / elapsedSeconds;
|
|
130
|
+
},
|
|
131
|
+
getTotalTokens() {
|
|
132
|
+
return totalTokens;
|
|
133
|
+
},
|
|
134
|
+
getElapsedMs() {
|
|
135
|
+
return Date.now() - startTime;
|
|
136
|
+
},
|
|
137
|
+
getSessionId() {
|
|
138
|
+
return sessionId;
|
|
139
|
+
},
|
|
140
|
+
reset() {
|
|
141
|
+
startTime = Date.now();
|
|
142
|
+
totalTokens = 0;
|
|
143
|
+
buffer = [];
|
|
144
|
+
},
|
|
145
|
+
getBufferSize() {
|
|
146
|
+
return buffer.length;
|
|
147
|
+
},
|
|
148
|
+
getMaxBufferSize() {
|
|
149
|
+
return MAX_BUFFER_SIZE;
|
|
150
|
+
},
|
|
151
|
+
getWindowMs() {
|
|
152
|
+
return windowMs;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/ui.ts
|
|
158
|
+
function createUIManager(client, config) {
|
|
159
|
+
const uiConfig = {
|
|
160
|
+
updateIntervalMs: config?.updateIntervalMs ?? 50,
|
|
161
|
+
format: config?.format ?? "compact",
|
|
162
|
+
showAverage: config?.showAverage ?? true,
|
|
163
|
+
showInstant: config?.showInstant ?? true,
|
|
164
|
+
showTotalTokens: config?.showTotalTokens ?? true,
|
|
165
|
+
showElapsed: config?.showElapsed ?? false,
|
|
166
|
+
enableColorCoding: config?.enableColorCoding ?? false,
|
|
167
|
+
slowTpsThreshold: config?.slowTpsThreshold ?? 10,
|
|
168
|
+
fastTpsThreshold: config?.fastTpsThreshold ?? 50
|
|
169
|
+
};
|
|
170
|
+
let flushTimer = null;
|
|
171
|
+
let lastFlushAt = 0;
|
|
172
|
+
let lastToastAt = 0;
|
|
173
|
+
let lastToastMessage = "";
|
|
174
|
+
let pendingState = null;
|
|
175
|
+
let lastDisplayedState = null;
|
|
176
|
+
function formatElapsedTime(ms) {
|
|
177
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
178
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
179
|
+
const seconds = totalSeconds % 60;
|
|
180
|
+
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
181
|
+
}
|
|
182
|
+
function formatNumberWithCommas(num) {
|
|
183
|
+
return num.toLocaleString("en-US");
|
|
184
|
+
}
|
|
185
|
+
function formatDisplay(state) {
|
|
186
|
+
const parts = [];
|
|
187
|
+
if (uiConfig.showInstant || uiConfig.showAverage) {
|
|
188
|
+
const tpsParts = [];
|
|
189
|
+
if (uiConfig.showInstant) {
|
|
190
|
+
tpsParts.push(`TPS: ${state.instantTps.toFixed(1)}`);
|
|
191
|
+
}
|
|
192
|
+
if (uiConfig.showAverage) {
|
|
193
|
+
tpsParts.push(`(avg ${state.avgTps.toFixed(1)})`);
|
|
194
|
+
}
|
|
195
|
+
parts.push(tpsParts.join(" "));
|
|
196
|
+
}
|
|
197
|
+
if (uiConfig.showTotalTokens) {
|
|
198
|
+
parts.push(`tokens: ${formatNumberWithCommas(state.totalTokens)}`);
|
|
199
|
+
}
|
|
200
|
+
if (uiConfig.showElapsed) {
|
|
201
|
+
parts.push(formatElapsedTime(state.elapsedMs));
|
|
202
|
+
}
|
|
203
|
+
return parts.join(" | ");
|
|
204
|
+
}
|
|
205
|
+
function getColorVariant(instantTps, isFinal) {
|
|
206
|
+
if (isFinal) {
|
|
207
|
+
return "success";
|
|
208
|
+
}
|
|
209
|
+
if (!uiConfig.enableColorCoding) {
|
|
210
|
+
return "info";
|
|
211
|
+
}
|
|
212
|
+
if (instantTps < uiConfig.slowTpsThreshold) {
|
|
213
|
+
return "error";
|
|
214
|
+
}
|
|
215
|
+
if (instantTps > uiConfig.fastTpsThreshold) {
|
|
216
|
+
return "success";
|
|
217
|
+
}
|
|
218
|
+
return "warning";
|
|
219
|
+
}
|
|
220
|
+
function display(message, isFinal = false, instantTps = 0) {
|
|
221
|
+
const minToastIntervalMs = Math.max(MIN_TOAST_INTERVAL_MS, uiConfig.updateIntervalMs * 3);
|
|
222
|
+
const toastDuration = Math.max(DEFAULT_TOAST_DURATION_MS, minToastIntervalMs * 20);
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
if (!isFinal) {
|
|
225
|
+
if (message === lastToastMessage || now - lastToastAt < minToastIntervalMs) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const variant = getColorVariant(instantTps, isFinal);
|
|
230
|
+
let displayed = false;
|
|
231
|
+
if (client.tui?.showToast) {
|
|
232
|
+
try {
|
|
233
|
+
const result = client.tui.showToast({
|
|
234
|
+
body: {
|
|
235
|
+
title: "TPS Meter",
|
|
236
|
+
message,
|
|
237
|
+
variant,
|
|
238
|
+
duration: isFinal ? 2000 : toastDuration
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
if (result && typeof result.catch === "function") {
|
|
242
|
+
result.catch(() => {});
|
|
243
|
+
}
|
|
244
|
+
displayed = true;
|
|
245
|
+
} catch {}
|
|
246
|
+
}
|
|
247
|
+
if (!displayed && client.tui?.publish) {
|
|
248
|
+
try {
|
|
249
|
+
const result = client.tui.publish({
|
|
250
|
+
body: {
|
|
251
|
+
type: "tui.toast.show",
|
|
252
|
+
properties: {
|
|
253
|
+
title: "TPS Meter",
|
|
254
|
+
message,
|
|
255
|
+
variant,
|
|
256
|
+
duration: isFinal ? FINAL_STATS_DURATION_MS : toastDuration
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
if (result && typeof result.catch === "function") {
|
|
261
|
+
result.catch(() => {});
|
|
262
|
+
}
|
|
263
|
+
displayed = true;
|
|
264
|
+
} catch {}
|
|
265
|
+
}
|
|
266
|
+
if (!displayed && client.toast?.success && client.toast?.info) {
|
|
267
|
+
try {
|
|
268
|
+
if (isFinal) {
|
|
269
|
+
client.toast.success(message, { duration: FINAL_STATS_DURATION_MS });
|
|
270
|
+
} else {
|
|
271
|
+
client.toast.info(message, { duration: toastDuration });
|
|
272
|
+
}
|
|
273
|
+
displayed = true;
|
|
274
|
+
} catch {}
|
|
275
|
+
}
|
|
276
|
+
if (displayed) {
|
|
277
|
+
lastToastAt = now;
|
|
278
|
+
lastToastMessage = message;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function flushPendingUpdate() {
|
|
282
|
+
if (pendingState) {
|
|
283
|
+
const formatted = formatDisplay(pendingState);
|
|
284
|
+
display(formatted, false, pendingState.instantTps);
|
|
285
|
+
lastDisplayedState = { ...pendingState };
|
|
286
|
+
pendingState = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function scheduleFlush() {
|
|
290
|
+
if (flushTimer) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
const delay = lastFlushAt === 0 ? 0 : Math.max(0, uiConfig.updateIntervalMs - (now - lastFlushAt));
|
|
295
|
+
flushTimer = setTimeout(() => {
|
|
296
|
+
flushTimer = null;
|
|
297
|
+
lastFlushAt = Date.now();
|
|
298
|
+
flushPendingUpdate();
|
|
299
|
+
}, delay);
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
updateDisplay(instantTps, avgTps, totalTokens, elapsedMs) {
|
|
303
|
+
pendingState = {
|
|
304
|
+
instantTps,
|
|
305
|
+
avgTps,
|
|
306
|
+
totalTokens,
|
|
307
|
+
elapsedMs
|
|
308
|
+
};
|
|
309
|
+
scheduleFlush();
|
|
310
|
+
},
|
|
311
|
+
showFinalStats(totalTokens, avgTps, elapsedMs) {
|
|
312
|
+
flushPendingUpdate();
|
|
313
|
+
lastToastMessage = "";
|
|
314
|
+
const state = {
|
|
315
|
+
instantTps: 0,
|
|
316
|
+
avgTps,
|
|
317
|
+
totalTokens,
|
|
318
|
+
elapsedMs
|
|
319
|
+
};
|
|
320
|
+
const formatted = formatDisplay(state);
|
|
321
|
+
display(formatted, true);
|
|
322
|
+
},
|
|
323
|
+
clear() {
|
|
324
|
+
if (flushTimer) {
|
|
325
|
+
clearTimeout(flushTimer);
|
|
326
|
+
flushTimer = null;
|
|
327
|
+
}
|
|
328
|
+
flushPendingUpdate();
|
|
329
|
+
pendingState = null;
|
|
330
|
+
lastDisplayedState = null;
|
|
331
|
+
},
|
|
332
|
+
setUpdateInterval(ms) {
|
|
333
|
+
uiConfig.updateIntervalMs = ms;
|
|
334
|
+
lastFlushAt = 0;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/tokenCounter.ts
|
|
340
|
+
function countByChars(text, divisor) {
|
|
341
|
+
if (!text || text.length === 0) {
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
return Math.ceil(text.length / divisor);
|
|
345
|
+
}
|
|
346
|
+
function countByWords(text, divisor) {
|
|
347
|
+
if (!text || text.length === 0) {
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
const trimmed = text.trim();
|
|
351
|
+
if (trimmed.length === 0) {
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
const wordCount = trimmed.split(/\s+/).length;
|
|
355
|
+
return Math.ceil(wordCount / divisor);
|
|
356
|
+
}
|
|
357
|
+
function createCounter(algorithm) {
|
|
358
|
+
const strategies = {
|
|
359
|
+
heuristic: (text) => countByChars(text, CHARS_DIV_4),
|
|
360
|
+
word: (text) => countByWords(text, WORDS_DIV_0_75),
|
|
361
|
+
code: (text) => countByChars(text, CHARS_DIV_3)
|
|
362
|
+
};
|
|
363
|
+
return {
|
|
364
|
+
count(text) {
|
|
365
|
+
return strategies[algorithm](text);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function createTokenizer(algorithm = "heuristic") {
|
|
370
|
+
return createCounter(algorithm);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/config.ts
|
|
374
|
+
var fs = __toESM(require("fs"));
|
|
375
|
+
var path = __toESM(require("path"));
|
|
376
|
+
var os = __toESM(require("os"));
|
|
377
|
+
var defaultConfig = {
|
|
378
|
+
enabled: true,
|
|
379
|
+
updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS,
|
|
380
|
+
rollingWindowMs: DEFAULT_ROLLING_WINDOW_MS,
|
|
381
|
+
showAverage: true,
|
|
382
|
+
showInstant: true,
|
|
383
|
+
showTotalTokens: true,
|
|
384
|
+
showElapsed: false,
|
|
385
|
+
format: "compact",
|
|
386
|
+
minVisibleTPS: 0,
|
|
387
|
+
fallbackTokenHeuristic: "chars_div_4",
|
|
388
|
+
enableColorCoding: false,
|
|
389
|
+
slowTpsThreshold: DEFAULT_SLOW_TPS_THRESHOLD,
|
|
390
|
+
fastTpsThreshold: DEFAULT_FAST_TPS_THRESHOLD
|
|
391
|
+
};
|
|
392
|
+
function isBoolean(value) {
|
|
393
|
+
return typeof value === "boolean";
|
|
394
|
+
}
|
|
395
|
+
function isNumber(value) {
|
|
396
|
+
return typeof value === "number" && !isNaN(value) && isFinite(value) && value >= 0;
|
|
397
|
+
}
|
|
398
|
+
function isString(value) {
|
|
399
|
+
return typeof value === "string";
|
|
400
|
+
}
|
|
401
|
+
function clamp(value, min, max) {
|
|
402
|
+
return Math.max(min, Math.min(max, value));
|
|
403
|
+
}
|
|
404
|
+
function mergeConfig(partial, defaults) {
|
|
405
|
+
const updateIntervalMs = isNumber(partial.updateIntervalMs) ? clamp(partial.updateIntervalMs, 10, 5000) : defaults.updateIntervalMs;
|
|
406
|
+
const rollingWindowMs = isNumber(partial.rollingWindowMs) ? clamp(partial.rollingWindowMs, 100, 30000) : defaults.rollingWindowMs;
|
|
407
|
+
const minVisibleTPS = isNumber(partial.minVisibleTPS) ? clamp(partial.minVisibleTPS, 0, 1e4) : defaults.minVisibleTPS;
|
|
408
|
+
let slowTpsThreshold = isNumber(partial.slowTpsThreshold) ? clamp(partial.slowTpsThreshold, 0, 1e4) : defaults.slowTpsThreshold;
|
|
409
|
+
let fastTpsThreshold = isNumber(partial.fastTpsThreshold) ? clamp(partial.fastTpsThreshold, 0, 1e4) : defaults.fastTpsThreshold;
|
|
410
|
+
if (slowTpsThreshold >= fastTpsThreshold) {
|
|
411
|
+
slowTpsThreshold = defaults.slowTpsThreshold;
|
|
412
|
+
fastTpsThreshold = defaults.fastTpsThreshold;
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
enabled: isBoolean(partial.enabled) ? partial.enabled : defaults.enabled,
|
|
416
|
+
updateIntervalMs,
|
|
417
|
+
rollingWindowMs,
|
|
418
|
+
showAverage: isBoolean(partial.showAverage) ? partial.showAverage : defaults.showAverage,
|
|
419
|
+
showInstant: isBoolean(partial.showInstant) ? partial.showInstant : defaults.showInstant,
|
|
420
|
+
showTotalTokens: isBoolean(partial.showTotalTokens) ? partial.showTotalTokens : defaults.showTotalTokens,
|
|
421
|
+
showElapsed: isBoolean(partial.showElapsed) ? partial.showElapsed : defaults.showElapsed,
|
|
422
|
+
format: isString(partial.format) && ["compact", "verbose", "minimal"].includes(partial.format) ? partial.format : defaults.format,
|
|
423
|
+
minVisibleTPS,
|
|
424
|
+
fallbackTokenHeuristic: isString(partial.fallbackTokenHeuristic) && ["chars_div_4", "chars_div_3", "words_div_0_75"].includes(partial.fallbackTokenHeuristic) ? partial.fallbackTokenHeuristic : defaults.fallbackTokenHeuristic,
|
|
425
|
+
enableColorCoding: isBoolean(partial.enableColorCoding) ? partial.enableColorCoding : defaults.enableColorCoding,
|
|
426
|
+
slowTpsThreshold,
|
|
427
|
+
fastTpsThreshold
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function loadConfigFile(filePath) {
|
|
431
|
+
try {
|
|
432
|
+
if (!fs.existsSync(filePath)) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
436
|
+
const parsed = JSON.parse(content);
|
|
437
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
return parsed;
|
|
441
|
+
} catch (error) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function loadEnvConfig() {
|
|
446
|
+
const envConfig = {};
|
|
447
|
+
if (process.env.TPS_METER_ENABLED !== undefined) {
|
|
448
|
+
envConfig.enabled = process.env.TPS_METER_ENABLED === "true";
|
|
449
|
+
}
|
|
450
|
+
if (process.env.TPS_METER_UPDATE_INTERVAL_MS !== undefined) {
|
|
451
|
+
const val = parseInt(process.env.TPS_METER_UPDATE_INTERVAL_MS, 10);
|
|
452
|
+
if (!isNaN(val))
|
|
453
|
+
envConfig.updateIntervalMs = val;
|
|
454
|
+
}
|
|
455
|
+
if (process.env.TPS_METER_ROLLING_WINDOW_MS !== undefined) {
|
|
456
|
+
const val = parseInt(process.env.TPS_METER_ROLLING_WINDOW_MS, 10);
|
|
457
|
+
if (!isNaN(val))
|
|
458
|
+
envConfig.rollingWindowMs = val;
|
|
459
|
+
}
|
|
460
|
+
if (process.env.TPS_METER_SHOW_AVERAGE !== undefined) {
|
|
461
|
+
envConfig.showAverage = process.env.TPS_METER_SHOW_AVERAGE === "true";
|
|
462
|
+
}
|
|
463
|
+
if (process.env.TPS_METER_SHOW_INSTANT !== undefined) {
|
|
464
|
+
envConfig.showInstant = process.env.TPS_METER_SHOW_INSTANT === "true";
|
|
465
|
+
}
|
|
466
|
+
if (process.env.TPS_METER_SHOW_TOTAL_TOKENS !== undefined) {
|
|
467
|
+
envConfig.showTotalTokens = process.env.TPS_METER_SHOW_TOTAL_TOKENS === "true";
|
|
468
|
+
}
|
|
469
|
+
if (process.env.TPS_METER_SHOW_ELAPSED !== undefined) {
|
|
470
|
+
envConfig.showElapsed = process.env.TPS_METER_SHOW_ELAPSED === "true";
|
|
471
|
+
}
|
|
472
|
+
if (process.env.TPS_METER_FORMAT !== undefined) {
|
|
473
|
+
const val = process.env.TPS_METER_FORMAT;
|
|
474
|
+
if (["compact", "verbose", "minimal"].includes(val)) {
|
|
475
|
+
envConfig.format = val;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (process.env.TPS_METER_MIN_VISIBLE_TPS !== undefined) {
|
|
479
|
+
const val = parseFloat(process.env.TPS_METER_MIN_VISIBLE_TPS);
|
|
480
|
+
if (!isNaN(val))
|
|
481
|
+
envConfig.minVisibleTPS = val;
|
|
482
|
+
}
|
|
483
|
+
if (process.env.TPS_METER_FALLBACK_HEURISTIC !== undefined) {
|
|
484
|
+
const val = process.env.TPS_METER_FALLBACK_HEURISTIC;
|
|
485
|
+
if (["chars_div_4", "chars_div_3", "words_div_0_75"].includes(val)) {
|
|
486
|
+
envConfig.fallbackTokenHeuristic = val;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (process.env.TPS_METER_ENABLE_COLOR_CODING !== undefined) {
|
|
490
|
+
envConfig.enableColorCoding = process.env.TPS_METER_ENABLE_COLOR_CODING === "true";
|
|
491
|
+
}
|
|
492
|
+
if (process.env.TPS_METER_SLOW_TPS_THRESHOLD !== undefined) {
|
|
493
|
+
const val = parseFloat(process.env.TPS_METER_SLOW_TPS_THRESHOLD);
|
|
494
|
+
if (!isNaN(val))
|
|
495
|
+
envConfig.slowTpsThreshold = val;
|
|
496
|
+
}
|
|
497
|
+
if (process.env.TPS_METER_FAST_TPS_THRESHOLD !== undefined) {
|
|
498
|
+
const val = parseFloat(process.env.TPS_METER_FAST_TPS_THRESHOLD);
|
|
499
|
+
if (!isNaN(val))
|
|
500
|
+
envConfig.fastTpsThreshold = val;
|
|
501
|
+
}
|
|
502
|
+
return envConfig;
|
|
503
|
+
}
|
|
504
|
+
function loadConfigSync() {
|
|
505
|
+
let mergedConfig = {};
|
|
506
|
+
const projectConfigPath = path.join(process.cwd(), ".opencode", "tps-meter.json");
|
|
507
|
+
const projectConfig = loadConfigFile(projectConfigPath);
|
|
508
|
+
if (projectConfig) {
|
|
509
|
+
mergedConfig = { ...mergedConfig, ...projectConfig };
|
|
510
|
+
}
|
|
511
|
+
const homeDir = os.homedir();
|
|
512
|
+
const globalConfigPath = path.join(homeDir, ".config", "opencode", "tps-meter.json");
|
|
513
|
+
const globalConfig = loadConfigFile(globalConfigPath);
|
|
514
|
+
if (globalConfig) {
|
|
515
|
+
mergedConfig = { ...mergedConfig, ...globalConfig };
|
|
516
|
+
}
|
|
517
|
+
const envConfig = loadEnvConfig();
|
|
518
|
+
mergedConfig = { ...mergedConfig, ...envConfig };
|
|
519
|
+
return mergeConfig(mergedConfig, defaultConfig);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/index.ts
|
|
523
|
+
function stringifyValue(value) {
|
|
524
|
+
if (typeof value === "string") {
|
|
525
|
+
return value;
|
|
526
|
+
}
|
|
527
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
528
|
+
return String(value);
|
|
529
|
+
}
|
|
530
|
+
if (value === null || value === undefined) {
|
|
531
|
+
return "";
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
return JSON.stringify(value);
|
|
535
|
+
} catch {
|
|
536
|
+
return "";
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function extractToolStateText(state) {
|
|
540
|
+
if (!state) {
|
|
541
|
+
return "";
|
|
542
|
+
}
|
|
543
|
+
const parts = [];
|
|
544
|
+
if (typeof state.raw === "string") {
|
|
545
|
+
parts.push(state.raw);
|
|
546
|
+
}
|
|
547
|
+
if (typeof state.output === "string") {
|
|
548
|
+
parts.push(state.output);
|
|
549
|
+
}
|
|
550
|
+
if (typeof state.error === "string") {
|
|
551
|
+
parts.push(state.error);
|
|
552
|
+
}
|
|
553
|
+
if (state.input && typeof state.input === "object") {
|
|
554
|
+
parts.push(stringifyValue(state.input));
|
|
555
|
+
}
|
|
556
|
+
if (typeof state.title === "string") {
|
|
557
|
+
parts.push(state.title);
|
|
558
|
+
}
|
|
559
|
+
return parts.filter((value) => value.length > 0).join(`
|
|
560
|
+
`);
|
|
561
|
+
}
|
|
562
|
+
function extractPartText(part) {
|
|
563
|
+
if (!part || typeof part !== "object") {
|
|
564
|
+
return "";
|
|
565
|
+
}
|
|
566
|
+
if (!part.type || typeof part.type !== "string") {
|
|
567
|
+
return stringifyValue(part);
|
|
568
|
+
}
|
|
569
|
+
switch (part.type) {
|
|
570
|
+
case "text":
|
|
571
|
+
case "reasoning":
|
|
572
|
+
return part.text ?? "";
|
|
573
|
+
case "subtask":
|
|
574
|
+
return [part.prompt, part.description, part.command].filter((value) => typeof value === "string" && value.length > 0).join(`
|
|
575
|
+
`);
|
|
576
|
+
case "tool":
|
|
577
|
+
return extractToolStateText(part.state);
|
|
578
|
+
case "file":
|
|
579
|
+
return [
|
|
580
|
+
part.source?.text?.value,
|
|
581
|
+
part.filename,
|
|
582
|
+
part.url,
|
|
583
|
+
part.source?.path,
|
|
584
|
+
part.source?.name,
|
|
585
|
+
part.source?.uri
|
|
586
|
+
].filter((value) => typeof value === "string" && value.length > 0).join(`
|
|
587
|
+
`);
|
|
588
|
+
case "snapshot":
|
|
589
|
+
case "step-start":
|
|
590
|
+
return part.snapshot ?? "";
|
|
591
|
+
case "step-finish":
|
|
592
|
+
return [part.reason, part.snapshot].filter((value) => typeof value === "string" && value.length > 0).join(`
|
|
593
|
+
`);
|
|
594
|
+
case "patch":
|
|
595
|
+
return Array.isArray(part.files) ? part.files.join(`
|
|
596
|
+
`) : "";
|
|
597
|
+
case "agent":
|
|
598
|
+
return [part.name, part.source?.text?.value].filter((value) => typeof value === "string" && value.length > 0).join(`
|
|
599
|
+
`);
|
|
600
|
+
case "retry":
|
|
601
|
+
return stringifyValue(part.error);
|
|
602
|
+
case "compaction":
|
|
603
|
+
return part.auto ? "compaction:auto" : "compaction";
|
|
604
|
+
default:
|
|
605
|
+
return stringifyValue(part);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function TpsMeterPlugin(context) {
|
|
609
|
+
const safeContext = context || {};
|
|
610
|
+
const logger = safeContext.logger || {
|
|
611
|
+
debug: () => {},
|
|
612
|
+
info: () => {},
|
|
613
|
+
warn: () => {},
|
|
614
|
+
error: () => {}
|
|
615
|
+
};
|
|
616
|
+
let config;
|
|
617
|
+
try {
|
|
618
|
+
config = loadConfigSync();
|
|
619
|
+
} catch (error) {
|
|
620
|
+
logger.warn("[TpsMeter] Failed to load config, using defaults:", error instanceof Error ? error.message : String(error));
|
|
621
|
+
config = defaultConfig;
|
|
622
|
+
}
|
|
623
|
+
if (!config.enabled) {
|
|
624
|
+
logger.debug("[TpsMeter] Plugin disabled by configuration");
|
|
625
|
+
return {};
|
|
626
|
+
}
|
|
627
|
+
const trackers = new Map;
|
|
628
|
+
const partTextCache = new Map;
|
|
629
|
+
const messageTokenCache = new Map;
|
|
630
|
+
const messageRoleCache = new Map;
|
|
631
|
+
const messageFirstTokenCache = new Map;
|
|
632
|
+
const activeMessageCache = new Map;
|
|
633
|
+
const resolvedConfig = config;
|
|
634
|
+
const ui = createUIManager(safeContext.client || {}, resolvedConfig);
|
|
635
|
+
const tokenizer = createTokenizer(resolvedConfig.fallbackTokenHeuristic === "words_div_0_75" ? "word" : resolvedConfig.fallbackTokenHeuristic === "chars_div_3" ? "code" : "heuristic");
|
|
636
|
+
logger.info("[TpsMeter] Plugin initialized and ready");
|
|
637
|
+
function getOrCreateTracker(sessionId) {
|
|
638
|
+
let tracker = trackers.get(sessionId);
|
|
639
|
+
if (!tracker) {
|
|
640
|
+
tracker = createTracker({
|
|
641
|
+
sessionId,
|
|
642
|
+
rollingWindowMs: resolvedConfig.rollingWindowMs
|
|
643
|
+
});
|
|
644
|
+
trackers.set(sessionId, tracker);
|
|
645
|
+
logger.debug(`[TpsMeter] Created tracker for session: ${sessionId}`);
|
|
646
|
+
}
|
|
647
|
+
return tracker;
|
|
648
|
+
}
|
|
649
|
+
function cleanup() {
|
|
650
|
+
logger.debug("[TpsMeter] Cleaning up all trackers and UI");
|
|
651
|
+
trackers.clear();
|
|
652
|
+
messageRoleCache.clear();
|
|
653
|
+
messageFirstTokenCache.clear();
|
|
654
|
+
activeMessageCache.clear();
|
|
655
|
+
ui.clear();
|
|
656
|
+
}
|
|
657
|
+
function handleMessagePartUpdated(event) {
|
|
658
|
+
const part = event.properties.part;
|
|
659
|
+
let delta = event.properties.delta;
|
|
660
|
+
if (!part) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (!COUNTABLE_PART_TYPES.has(part.type)) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const sessionId = part.sessionID || "default";
|
|
667
|
+
const partText = extractPartText(part);
|
|
668
|
+
if (!delta && partText.length > 0) {
|
|
669
|
+
const sessionCache = partTextCache.get(sessionId) || new Map;
|
|
670
|
+
partTextCache.set(sessionId, sessionCache);
|
|
671
|
+
const cacheKey = `${part.messageID}:${part.id}:${part.type}`;
|
|
672
|
+
const previousText = sessionCache.get(cacheKey) || "";
|
|
673
|
+
if (partText.startsWith(previousText)) {
|
|
674
|
+
delta = partText.slice(previousText.length);
|
|
675
|
+
} else {
|
|
676
|
+
delta = partText;
|
|
677
|
+
}
|
|
678
|
+
sessionCache.set(cacheKey, partText);
|
|
679
|
+
}
|
|
680
|
+
if (!delta || delta.length === 0) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const roleCache = messageRoleCache.get(sessionId);
|
|
684
|
+
const role = roleCache?.get(part.messageID);
|
|
685
|
+
if (role !== "assistant") {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const tracker = getOrCreateTracker(sessionId);
|
|
689
|
+
const messageId = part.messageID;
|
|
690
|
+
const activeMessageId = activeMessageCache.get(sessionId);
|
|
691
|
+
if (activeMessageId !== messageId) {
|
|
692
|
+
tracker.reset();
|
|
693
|
+
activeMessageCache.set(sessionId, messageId);
|
|
694
|
+
}
|
|
695
|
+
const now = Date.now();
|
|
696
|
+
cleanupStaleMessages(now);
|
|
697
|
+
const firstTokenCache = messageFirstTokenCache.get(sessionId) || new Map;
|
|
698
|
+
messageFirstTokenCache.set(sessionId, firstTokenCache);
|
|
699
|
+
let firstTokenAt = firstTokenCache.get(messageId);
|
|
700
|
+
if (firstTokenAt === undefined) {
|
|
701
|
+
firstTokenAt = now;
|
|
702
|
+
firstTokenCache.set(messageId, firstTokenAt);
|
|
703
|
+
}
|
|
704
|
+
const tokenCount = tokenizer.count(delta);
|
|
705
|
+
tracker.recordTokens(tokenCount, now);
|
|
706
|
+
const messageCache = messageTokenCache.get(sessionId) || new Map;
|
|
707
|
+
messageTokenCache.set(sessionId, messageCache);
|
|
708
|
+
const previousTokens = messageCache.get(messageId) ?? 0;
|
|
709
|
+
messageCache.set(messageId, previousTokens + tokenCount);
|
|
710
|
+
const instantTps = tracker.getInstantTPS();
|
|
711
|
+
const avgTps = tracker.getAverageTPS();
|
|
712
|
+
const totalTokens = tracker.getTotalTokens();
|
|
713
|
+
const elapsedMs = tracker.getElapsedMs();
|
|
714
|
+
const elapsedSinceFirstToken = Math.max(0, now - firstTokenAt);
|
|
715
|
+
if (elapsedSinceFirstToken >= MIN_TPS_ELAPSED_MS && instantTps >= resolvedConfig.minVisibleTPS) {
|
|
716
|
+
ui.updateDisplay(instantTps, avgTps, totalTokens, elapsedMs);
|
|
717
|
+
}
|
|
718
|
+
logger.debug(`[TpsMeter] Session ${sessionId}: +${tokenCount} tokens, TPS: ${instantTps.toFixed(1)} (avg: ${avgTps.toFixed(1)})`);
|
|
719
|
+
}
|
|
720
|
+
function handleMessageUpdated(event) {
|
|
721
|
+
const info = event.properties.info;
|
|
722
|
+
if (!info) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const roleCache = messageRoleCache.get(info.sessionID) || new Map;
|
|
726
|
+
messageRoleCache.set(info.sessionID, roleCache);
|
|
727
|
+
roleCache.set(info.id, info.role);
|
|
728
|
+
if (info.role === "assistant") {
|
|
729
|
+
const sessionId = info.sessionID;
|
|
730
|
+
const tracker = trackers.get(sessionId);
|
|
731
|
+
const sessionCache = partTextCache.get(sessionId);
|
|
732
|
+
const tokenCache = messageTokenCache.get(sessionId) || new Map;
|
|
733
|
+
messageTokenCache.set(sessionId, tokenCache);
|
|
734
|
+
const firstTokenCache = messageFirstTokenCache.get(sessionId) || new Map;
|
|
735
|
+
messageFirstTokenCache.set(sessionId, firstTokenCache);
|
|
736
|
+
const outputTokens = info.tokens?.output ?? 0;
|
|
737
|
+
const reasoningTokens = info.tokens?.reasoning ?? 0;
|
|
738
|
+
const reportedTokens = outputTokens + reasoningTokens;
|
|
739
|
+
const messageId = info.id;
|
|
740
|
+
const previous = tokenCache.get(messageId) ?? 0;
|
|
741
|
+
const nextTokens = Math.max(previous, reportedTokens);
|
|
742
|
+
tokenCache.set(messageId, nextTokens);
|
|
743
|
+
if (info.time?.completed && tracker) {
|
|
744
|
+
const completedAt = info.time?.completed ?? Date.now();
|
|
745
|
+
const createdAt = info.time?.created ?? completedAt;
|
|
746
|
+
const firstTokenAt = firstTokenCache.get(messageId) ?? createdAt;
|
|
747
|
+
const elapsedMs = Math.max(0, completedAt - firstTokenAt);
|
|
748
|
+
const cachedTokens = tokenCache.get(messageId) ?? 0;
|
|
749
|
+
const totalTokens = reportedTokens > 0 ? reportedTokens : cachedTokens;
|
|
750
|
+
const avgTps = elapsedMs > 0 ? totalTokens / (elapsedMs / 1000) : 0;
|
|
751
|
+
const hasValidFinish = info.finish !== undefined && info.finish !== null && !INVALID_FINISH_REASONS.has(info.finish);
|
|
752
|
+
const shouldShowFinalStats = hasValidFinish && totalTokens > 0 && elapsedMs >= MIN_TPS_ELAPSED_MS;
|
|
753
|
+
if (shouldShowFinalStats) {
|
|
754
|
+
ui.showFinalStats(totalTokens, avgTps, elapsedMs);
|
|
755
|
+
logger.info(`[TpsMeter] Session ${sessionId} complete: ${totalTokens} tokens in ${(elapsedMs / 1000).toFixed(1)}s (avg ${avgTps.toFixed(1)} TPS)`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (sessionCache) {
|
|
759
|
+
for (const key of sessionCache.keys()) {
|
|
760
|
+
if (key.startsWith(`${info.id}:`)) {
|
|
761
|
+
sessionCache.delete(key);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (info.time?.completed) {
|
|
766
|
+
tokenCache.delete(messageId);
|
|
767
|
+
roleCache.delete(messageId);
|
|
768
|
+
firstTokenCache.delete(messageId);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (info.error) {
|
|
772
|
+
logger.warn(`[TpsMeter] Message error for session: ${info.sessionID}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function handleSessionIdle(event) {
|
|
776
|
+
const sessionId = event.properties.sessionID || "default";
|
|
777
|
+
logger.debug(`[TpsMeter] Session idle: ${sessionId}`);
|
|
778
|
+
trackers.delete(sessionId);
|
|
779
|
+
partTextCache.delete(sessionId);
|
|
780
|
+
messageTokenCache.delete(sessionId);
|
|
781
|
+
messageRoleCache.delete(sessionId);
|
|
782
|
+
messageFirstTokenCache.delete(sessionId);
|
|
783
|
+
activeMessageCache.delete(sessionId);
|
|
784
|
+
if (trackers.size === 0) {
|
|
785
|
+
cleanup();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
let lastCleanupTime = 0;
|
|
789
|
+
function cleanupStaleMessages(now) {
|
|
790
|
+
if (now - lastCleanupTime < CLEANUP_INTERVAL_MS) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
lastCleanupTime = now;
|
|
794
|
+
let cleanedCount = 0;
|
|
795
|
+
for (const [sessionId, firstTokenCache] of messageFirstTokenCache) {
|
|
796
|
+
const tokenCache = messageTokenCache.get(sessionId);
|
|
797
|
+
const roleCache = messageRoleCache.get(sessionId);
|
|
798
|
+
for (const [messageId, firstTokenAt] of firstTokenCache) {
|
|
799
|
+
if (now - firstTokenAt > MAX_MESSAGE_AGE_MS) {
|
|
800
|
+
firstTokenCache.delete(messageId);
|
|
801
|
+
tokenCache?.delete(messageId);
|
|
802
|
+
roleCache?.delete(messageId);
|
|
803
|
+
cleanedCount++;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (cleanedCount > 0) {
|
|
808
|
+
logger.debug(`[TpsMeter] Cleaned up ${cleanedCount} stale message entries`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
event: async ({ event }) => {
|
|
813
|
+
try {
|
|
814
|
+
switch (event.type) {
|
|
815
|
+
case "message.part.updated":
|
|
816
|
+
handleMessagePartUpdated(event);
|
|
817
|
+
break;
|
|
818
|
+
case "message.updated":
|
|
819
|
+
handleMessageUpdated(event);
|
|
820
|
+
break;
|
|
821
|
+
case "session.idle":
|
|
822
|
+
handleSessionIdle(event);
|
|
823
|
+
break;
|
|
824
|
+
default:
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
} catch (error) {
|
|
828
|
+
logger.error("[TpsMeter] Error handling event:", error instanceof Error ? error.message : String(error));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
}
|