dsh-recent-usage-metrics 0.2.1 → 0.2.2
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/CHANGELOG.md +6 -0
- package/README.en.md +51 -8
- package/README.md +59 -13
- package/client/client.js +127 -59
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.en.md
CHANGED
|
@@ -281,14 +281,32 @@ built, and undated rows keep showing `--` — the previous behaviour, with no er
|
|
|
281
281
|
|
|
282
282
|
## Install
|
|
283
283
|
|
|
284
|
+
Published to npm as `dsh-recent-usage-metrics`:
|
|
285
|
+
|
|
284
286
|
```powershell
|
|
285
|
-
# from the dsh GUI's Plugin Manager, or:
|
|
286
|
-
|
|
287
|
+
# from the dsh GUI's Plugin Manager, or directly:
|
|
288
|
+
npm install dsh-recent-usage-metrics
|
|
287
289
|
```
|
|
288
290
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
It can also be added to the profile's `package.json` dependencies and installed from there.
|
|
292
|
+
|
|
293
|
+
**Development mode**: point the profile dependency at this directory with `link:`, so editing
|
|
294
|
+
`client/client.js` and reloading the page picks up the change. Host-side code never needs a
|
|
295
|
+
restart because there is none.
|
|
296
|
+
|
|
297
|
+
## Release
|
|
298
|
+
|
|
299
|
+
Pushing a `v*` tag publishes automatically through GitHub Actions, authenticated with OIDC
|
|
300
|
+
trusted publishing — **no npm token is involved**.
|
|
301
|
+
|
|
302
|
+
```powershell
|
|
303
|
+
npm version patch # bump the version and create the tag
|
|
304
|
+
git push --follow-tags # push, and the release publishes itself
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The workflow first checks that the tag matches the `package.json` version and fails on a
|
|
308
|
+
mismatch, so a wrong version cannot slip out. Published tarballs carry a provenance statement,
|
|
309
|
+
visible on the package page.
|
|
292
310
|
|
|
293
311
|
## Layout
|
|
294
312
|
|
|
@@ -299,6 +317,8 @@ there is none.
|
|
|
299
317
|
| `cordis.patch.yml` | The one inserted Loader row |
|
|
300
318
|
| `locale/en.json`, `locale/zh.json` | Plugin Manager title and description |
|
|
301
319
|
| `icon.svg` | Plugin icon |
|
|
320
|
+
| `CHANGELOG.md` | What changed in each version |
|
|
321
|
+
| `.github/workflows/publish.yml` | Publishes to npm when a tag is pushed |
|
|
302
322
|
| `test/lifecycle.cjs` | Runs the real client against a DOM/hook shim: metrics, ten-row cap, insertion order, click/open, disposal |
|
|
303
323
|
| `test/repro-coldstart.cjs` | Regression: a cold page must paint Chinese and stay switchable both ways |
|
|
304
324
|
| `test/repro-hmr.cjs` | Regression: a reload must not collide with the live dictionary claim |
|
|
@@ -379,9 +399,32 @@ const target = uiConversation.binding(sessionId).target('stats')
|
|
|
379
399
|
```
|
|
380
400
|
|
|
381
401
|
That data comes from the Host-side `sessionStats` fold over the session log, and each step
|
|
382
|
-
carries `ttft`, `model`, `decode`, `tps`, `output` and `wall`.
|
|
383
|
-
|
|
384
|
-
|
|
402
|
+
carries `ttft`, `model`, `decode`, `tps`, `output` and `wall`.
|
|
403
|
+
|
|
404
|
+
**How the columns map:**
|
|
405
|
+
|
|
406
|
+
| Drawer column | Field | Note |
|
|
407
|
+
| --- | --- | --- |
|
|
408
|
+
| At | `startTime` | the `step/start` moment, same as the Statistics view |
|
|
409
|
+
| TTFT | `ttft` | `firstToken − startTime` |
|
|
410
|
+
| Speed | `tps` | `output ÷ decode` |
|
|
411
|
+
| Tokens | `output` | the provider's reported usage |
|
|
412
|
+
| **Time** | **`model`** | **model time, not the Statistics view's total** |
|
|
413
|
+
| Amortised | `output ÷ model` | this plugin's own figure |
|
|
414
|
+
|
|
415
|
+
**Why the total is `model` and not `wall`:**
|
|
416
|
+
|
|
417
|
+
The Statistics view's total column is `wall = endTime − startTime`, which **includes the step's
|
|
418
|
+
tool calls**. A step that thinks for 11.5 s and then runs a 31 s command shows `wall` 41.9 s but
|
|
419
|
+
`model` 11.5 s. This plugin measures model output only, so it reports `model` — a figure that
|
|
420
|
+
contains no tool time by construction, and is therefore final the moment the reply lands.
|
|
421
|
+
|
|
422
|
+
**Why the figures carry `~` while streaming:**
|
|
423
|
+
|
|
424
|
+
Until the model finishes, the Host has no `usage` from the provider, so `output` is `null`. The
|
|
425
|
+
plugin falls back to a character estimate (coefficients fitted per content type), which is why
|
|
426
|
+
those figures carry `~`. The moment `usage` arrives — which is essentially the same instant the
|
|
427
|
+
reply completes — the measured value takes over and the `~` disappears.
|
|
385
428
|
|
|
386
429
|
**Why the history is no longer stored locally:**
|
|
387
430
|
|
package/README.md
CHANGED
|
@@ -249,13 +249,30 @@ Chat 存储从实时 token 增量中累积它,而会话日志不存 live chunk
|
|
|
249
249
|
|
|
250
250
|
## 安装
|
|
251
251
|
|
|
252
|
+
已发布到 npm,包名 `dsh-recent-usage-metrics`:
|
|
253
|
+
|
|
254
|
+
```powershell
|
|
255
|
+
# 在 dsh GUI 的插件管理器里搜索并安装,或直接:
|
|
256
|
+
npm install dsh-recent-usage-metrics
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
也可以把它加进 profile 的 `package.json` 依赖,再由插件管理器安装。
|
|
260
|
+
|
|
261
|
+
**开发模式**:把 profile 里的依赖改成 `link:` 指向本目录,这样改完 `client/client.js`
|
|
262
|
+
刷新页面即可生效。宿主侧代码永远不需要重启,因为它本身就存在。
|
|
263
|
+
|
|
264
|
+
## 发布
|
|
265
|
+
|
|
266
|
+
推 `v*` 标签即由 GitHub Actions 自动发布,认证走 OIDC trusted publishing,
|
|
267
|
+
**不需要任何 npm token**。
|
|
268
|
+
|
|
252
269
|
```powershell
|
|
253
|
-
|
|
254
|
-
#
|
|
270
|
+
npm version patch # 改版本号并打 tag
|
|
271
|
+
git push --follow-tags # 推送后自动发布
|
|
255
272
|
```
|
|
256
273
|
|
|
257
|
-
|
|
258
|
-
|
|
274
|
+
工作流会先校验标签与 `package.json` 的版本一致,不一致直接失败,避免发错版本。
|
|
275
|
+
发布产物带 provenance 签名,npm 上可在包页面看到来源证明。
|
|
259
276
|
|
|
260
277
|
## 目录结构
|
|
261
278
|
|
|
@@ -266,6 +283,8 @@ Chat 存储从实时 token 增量中累积它,而会话日志不存 live chunk
|
|
|
266
283
|
| `cordis.patch.yml` | 唯一插入的那一行 Loader 条目 |
|
|
267
284
|
| `locale/en.json`、`locale/zh.json` | 插件管理器里显示的标题与说明 |
|
|
268
285
|
| `icon.svg` | 插件图标 |
|
|
286
|
+
| `CHANGELOG.md` | 各版本改动 |
|
|
287
|
+
| `.github/workflows/publish.yml` | 推标签时自动发布到 npm |
|
|
269
288
|
| `test/lifecycle.cjs` | 在 DOM/hook shim 上跑真实的客户端:指标、十行上限、插入顺序、点击开合、清理 |
|
|
270
289
|
| `test/repro-coldstart.cjs` | 回归:冷页面必须首屏中文,且双向可切换 |
|
|
271
290
|
| `test/repro-hmr.cjs` | 回归:重载不得与存活的字典占用冲突 |
|
|
@@ -278,6 +297,8 @@ node test/repro-hmr.cjs
|
|
|
278
297
|
$env:ELECTRON_RUN_AS_NODE='1'; & "C:\Cinstall\ProgramTools\DSH\DeepSeek Harness.exe" test\repro-log.cjs
|
|
279
298
|
```
|
|
280
299
|
|
|
300
|
+
四套共 227 项检查。
|
|
301
|
+
|
|
281
302
|
## 用真实会话日志复核
|
|
282
303
|
|
|
283
304
|
统计页的数字来自磁盘上的会话日志:
|
|
@@ -336,8 +357,31 @@ const target = uiConversation.binding(sessionId).target('stats')
|
|
|
336
357
|
```
|
|
337
358
|
|
|
338
359
|
那份数据由宿主侧的 `sessionStats` fold 从会话日志算出,每步都带着 `ttft`、`model`、
|
|
339
|
-
`decode`、`tps`、`output`、`wall
|
|
340
|
-
|
|
360
|
+
`decode`、`tps`、`output`、`wall`。
|
|
361
|
+
|
|
362
|
+
**字段对应关系:**
|
|
363
|
+
|
|
364
|
+
| 抽屉的列 | 取自 | 说明 |
|
|
365
|
+
| --- | --- | --- |
|
|
366
|
+
| 时间 | `startTime` | `step/start` 的时刻,与统计页一致 |
|
|
367
|
+
| 首字 | `ttft` | `firstToken − startTime` |
|
|
368
|
+
| 速度 | `tps` | `output ÷ decode` |
|
|
369
|
+
| 输出 Token | `output` | 上游返回的 usage |
|
|
370
|
+
| **总用时** | **`model`** | **模型用时,不是统计页的「总用时」列** |
|
|
371
|
+
| 折算速度 | `output ÷ model` | 本插件独有 |
|
|
372
|
+
|
|
373
|
+
**总用时为何取 `model` 而不是 `wall`:**
|
|
374
|
+
|
|
375
|
+
统计页的「总用时」列是 `wall = endTime − startTime`,它**把工具调用也算了进去**。
|
|
376
|
+
一个思考 11.5 秒、随后跑 31 秒工具的步骤,`wall` 是 41.9 秒而 `model` 是 11.5 秒。
|
|
377
|
+
本插件只统计模型输出性能,所以取 `model`——它天然不含工具时间,回复一落地就是最终值,
|
|
378
|
+
不再随工具执行增长。
|
|
379
|
+
|
|
380
|
+
**流式期间为什么带 `~`:**
|
|
381
|
+
|
|
382
|
+
模型正在输出时,宿主还没拿到上游的 `usage`,此时 `output` 为 `null`。
|
|
383
|
+
插件改用字符估算(按内容类型分别标定系数),所以那段数字带 `~`。
|
|
384
|
+
`usage` 一到(几乎就是回复完成的同一刻),立刻切成实测值,`~` 消失。
|
|
341
385
|
|
|
342
386
|
**为什么不再本地保存:**
|
|
343
387
|
|
|
@@ -348,16 +392,18 @@ const target = uiConversation.binding(sessionId).target('stats')
|
|
|
348
392
|
现在:
|
|
349
393
|
|
|
350
394
|
- 抽屉的行在每次渲染时从该会话自己的统计数据重建,没有本地副本可以过期
|
|
351
|
-
-
|
|
395
|
+
- 快照带会话标记,切换会话的瞬间旧数据即失效,不会短暂显示上一个会话的行
|
|
352
396
|
- 页面刷新后自动从日志重建,不需要额外的持久化
|
|
353
397
|
|
|
354
398
|
**视图目标需要先订阅才会存在**(`getSnapshot` 在订阅前返回 `undefined`)。插件在
|
|
355
399
|
`apply` 时按会话订阅一次;激活是永久且共享的,所以不会在统计页之外再跑一份 fold。
|
|
356
400
|
|
|
357
|
-
|
|
358
|
-
|
|
401
|
+
**拿不到统计视图时**,插件自动退回 Chat 快照 + 轨迹域自行计算(兜底路径)。
|
|
402
|
+
这条路径不落盘、也按会话隔离,但它读不到 `model`,所以三态会退化为两态。
|
|
403
|
+
它的字符估算系数与主路径相同。决定走哪条路径的唯一依据是那份数据源在不在,
|
|
404
|
+
没有任何开关参与。
|
|
359
405
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
406
|
+
**流式期间不额外催动 fold**:那套 fold 每来一个 chunk 就会重建整张表,而它本来就由
|
|
407
|
+
统计页或宿主驱动。状态栏的实时读数在回复完成前用字符估算与本地 tick,回复一落地
|
|
408
|
+
(`model` 有值)就切到折叠给出的实测值。这样既保住了「数字从 0 开始爬」的观感,
|
|
409
|
+
也不会因为插件而多跑一份 fold。
|
package/client/client.js
CHANGED
|
@@ -2155,12 +2155,12 @@ window.__ModuleLoader__.load({
|
|
|
2155
2155
|
* subscribes. Reading it inline during render froze the rows at whatever the first
|
|
2156
2156
|
* render saw — which is why the drawer showed timestamps minutes behind the log.
|
|
2157
2157
|
*/
|
|
2158
|
-
/*
|
|
2159
|
-
* The shipped Statistics view's data source for this session. `undefined` when that
|
|
2160
|
-
* view is absent — a composition without it — which is what routes the drawer to its
|
|
2161
|
-
* Chat-snapshot fallback. No switch decides this: the presence of the source does.
|
|
2162
|
-
*/
|
|
2163
|
-
const statsSource = typeof props.statsSource === 'function' ? props.statsSource() : undefined
|
|
2158
|
+
/*
|
|
2159
|
+
* The shipped Statistics view's data source for this session. `undefined` when that
|
|
2160
|
+
* view is absent — a composition without it — which is what routes the drawer to its
|
|
2161
|
+
* Chat-snapshot fallback. No switch decides this: the presence of the source does.
|
|
2162
|
+
*/
|
|
2163
|
+
const statsSource = typeof props.statsSource === 'function' ? props.statsSource() : undefined
|
|
2164
2164
|
const statsSubscribe = useCallback(
|
|
2165
2165
|
(listener) => {
|
|
2166
2166
|
if (statsSource === undefined || typeof statsSource.subscribe !== 'function') {
|
|
@@ -2183,10 +2183,45 @@ window.__ModuleLoader__.load({
|
|
|
2183
2183
|
[statsSource, sessionId],
|
|
2184
2184
|
)
|
|
2185
2185
|
const [statsSnapshot, setStatsSnapshot] = useState(statsRead)
|
|
2186
|
+
/*
|
|
2187
|
+
* Re-point the subscription at whatever target is current, then re-read.
|
|
2188
|
+
*
|
|
2189
|
+
* A conversation that is rebuilt underneath the drawer (a model switch, a reconnect, a
|
|
2190
|
+
* released binding) replaces its `BoundConversation`, and with it the view store the old
|
|
2191
|
+
* `target()` closure captured. Refreshing on every render rebinds the listener to the new
|
|
2192
|
+
* target, so the drawer cannot keep rendering a retired snapshot while the Statistics page
|
|
2193
|
+
* has already moved on.
|
|
2194
|
+
*/
|
|
2195
|
+
if (statsSource !== undefined && typeof statsSource.refresh === 'function') {
|
|
2196
|
+
statsSource.refresh()
|
|
2197
|
+
}
|
|
2186
2198
|
useEffect(() => {
|
|
2187
2199
|
setStatsSnapshot(statsRead())
|
|
2188
2200
|
return statsSubscribe(() => setStatsSnapshot(statsRead()))
|
|
2189
2201
|
}, [statsRead, statsSubscribe])
|
|
2202
|
+
/*
|
|
2203
|
+
* Follow a conversation that is rebuilt underneath the drawer.
|
|
2204
|
+
*
|
|
2205
|
+
* A model switch, a reconnect, or a released binding replaces the session's
|
|
2206
|
+
* `BoundConversation`, and with it the view store the old `target()` closure captured.
|
|
2207
|
+
* That stale closure keeps returning its last snapshot forever — which is exactly how the
|
|
2208
|
+
* drawer froze on one turn while the Statistics page moved on.
|
|
2209
|
+
*
|
|
2210
|
+
* On a slow tick this re-points the listener at the target that is current now and
|
|
2211
|
+
* re-reads the snapshot. Both are cheap (a map lookup and a field read) and neither
|
|
2212
|
+
* depends on the stale chain firing.
|
|
2213
|
+
*/
|
|
2214
|
+
useEffect(() => {
|
|
2215
|
+
if (statsSource === undefined) return undefined
|
|
2216
|
+
const sync = () => {
|
|
2217
|
+
if (typeof statsSource.refresh === 'function') statsSource.refresh()
|
|
2218
|
+
const next = statsRead()
|
|
2219
|
+
if (next !== undefined) setStatsSnapshot(next)
|
|
2220
|
+
}
|
|
2221
|
+
sync()
|
|
2222
|
+
const timer = setInterval(sync, 1000)
|
|
2223
|
+
return () => clearInterval(timer)
|
|
2224
|
+
}, [statsSource, statsRead])
|
|
2190
2225
|
/*
|
|
2191
2226
|
* A snapshot only counts while it belongs to the session being shown.
|
|
2192
2227
|
*
|
|
@@ -2940,82 +2975,115 @@ window.__ModuleLoader__.load({
|
|
|
2940
2975
|
*/
|
|
2941
2976
|
const statsSources = new Map()
|
|
2942
2977
|
/*
|
|
2943
|
-
* The Statistics target for one session
|
|
2978
|
+
* The Statistics target for one session.
|
|
2979
|
+
*
|
|
2980
|
+
* The target object is looked up FRESH on every call rather than cached alongside the
|
|
2981
|
+
* source. `uiConversation.binding(id)` keeps a `BoundConversation` per session, and that
|
|
2982
|
+
* object owns the assembler and the view store; when the conversation is rebuilt — a model
|
|
2983
|
+
* switch, a reconnect, a released binding — the old `target()` closure keeps pointing at
|
|
2984
|
+
* the retired store and returns its last snapshot forever. Caching it froze the drawer on
|
|
2985
|
+
* one turn while the Statistics page moved on.
|
|
2944
2986
|
*
|
|
2945
|
-
*
|
|
2946
|
-
*
|
|
2947
|
-
* the evicted target's subscription: without that, a long session that visits many
|
|
2948
|
-
* conversations would keep one live subscription per conversation visited, and each of
|
|
2949
|
-
* those keeps the conversation's fold active.
|
|
2987
|
+
* Only the subscriptions are cached, because those must be released on eviction and
|
|
2988
|
+
* re-subscribing on every render would multiply listeners.
|
|
2950
2989
|
*/
|
|
2951
2990
|
const STATS_SOURCE_LIMIT = 4
|
|
2952
2991
|
const statsSourceFor = (sessionId) => {
|
|
2953
2992
|
if (sessionId === null || sessionId === undefined) return undefined
|
|
2954
|
-
|
|
2993
|
+
const uiConversation = ctx.get('uiConversation')
|
|
2994
|
+
if (uiConversation === undefined || uiConversation === null) return undefined
|
|
2995
|
+
|
|
2996
|
+
/** The target that is current right now, or undefined when it cannot be resolved. */
|
|
2997
|
+
const targetNow = () => {
|
|
2998
|
+
try {
|
|
2999
|
+
return uiConversation.binding(sessionId).target(STATS_TARGET) ?? undefined
|
|
3000
|
+
} catch {
|
|
3001
|
+
// The session was released (or is inactive) — no statistics for it right now.
|
|
3002
|
+
return undefined
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
const cached = statsSources.get(sessionId)
|
|
3007
|
+
if (cached !== undefined) {
|
|
2955
3008
|
// Refresh recency so the active conversation is the last one evicted.
|
|
2956
|
-
const existing = statsSources.get(sessionId)
|
|
2957
3009
|
statsSources.delete(sessionId)
|
|
2958
|
-
statsSources.set(sessionId,
|
|
2959
|
-
return
|
|
3010
|
+
statsSources.set(sessionId, cached)
|
|
3011
|
+
return cached.source
|
|
2960
3012
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
const
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
const dispose = () => {
|
|
2980
|
-
if (subscriptions.get(listener)?.unsubscribe !== unsubscribe) return
|
|
2981
|
-
subscriptions.delete(listener)
|
|
2982
|
-
try {
|
|
2983
|
-
unsubscribe()
|
|
2984
|
-
} catch {}
|
|
2985
|
-
}
|
|
2986
|
-
subscriptions.set(listener, { unsubscribe, dispose })
|
|
2987
|
-
return dispose
|
|
2988
|
-
},
|
|
2989
|
-
release: () => {
|
|
2990
|
-
for (const [listener, entry] of [...subscriptions]) {
|
|
2991
|
-
subscriptions.delete(listener)
|
|
2992
|
-
try {
|
|
2993
|
-
entry.unsubscribe()
|
|
2994
|
-
} catch {}
|
|
2995
|
-
}
|
|
2996
|
-
},
|
|
3013
|
+
|
|
3014
|
+
/*
|
|
3015
|
+
* Each listener is bound to the target that was current when it subscribed; a listener
|
|
3016
|
+
* whose target has since been replaced is re-bound the next time it asks, which is what
|
|
3017
|
+
* keeps a drawn row tracking the live fold instead of a retired one.
|
|
3018
|
+
*/
|
|
3019
|
+
const subscriptions = new Map()
|
|
3020
|
+
|
|
3021
|
+
const bind = (listener) => {
|
|
3022
|
+
const target = targetNow()
|
|
3023
|
+
if (target === undefined) return
|
|
3024
|
+
const entry = subscriptions.get(listener)
|
|
3025
|
+
if (entry !== undefined && entry.target === target) return
|
|
3026
|
+
if (entry !== undefined) {
|
|
3027
|
+
try {
|
|
3028
|
+
entry.unsubscribe()
|
|
3029
|
+
} catch {}
|
|
3030
|
+
subscriptions.delete(listener)
|
|
2997
3031
|
}
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3032
|
+
try {
|
|
3033
|
+
subscriptions.set(listener, { target, unsubscribe: target.subscribe(listener) })
|
|
3034
|
+
} catch {}
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
const dispose = (listener) => {
|
|
3038
|
+
const entry = subscriptions.get(listener)
|
|
3039
|
+
if (entry === undefined) return
|
|
3040
|
+
subscriptions.delete(listener)
|
|
3041
|
+
try {
|
|
3042
|
+
entry.unsubscribe()
|
|
3043
|
+
} catch {}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
const source = {
|
|
3047
|
+
/** Always the newest snapshot the session's own target holds. */
|
|
3048
|
+
getSnapshot: () => {
|
|
3049
|
+
const target = targetNow()
|
|
3050
|
+
return target === undefined ? undefined : target.getSnapshot()
|
|
3051
|
+
},
|
|
3052
|
+
subscribe: (listener) => {
|
|
3053
|
+
bind(listener)
|
|
3054
|
+
return () => dispose(listener)
|
|
3055
|
+
},
|
|
3056
|
+
/**
|
|
3057
|
+
* Re-point every listener at the target that is current now.
|
|
3058
|
+
*
|
|
3059
|
+
* Called on each render: one map lookup per listener, and it is what lets the drawer
|
|
3060
|
+
* follow a conversation that was rebuilt underneath it.
|
|
3061
|
+
*/
|
|
3062
|
+
refresh: () => {
|
|
3063
|
+
for (const listener of [...subscriptions.keys()]) bind(listener)
|
|
3064
|
+
},
|
|
3065
|
+
release: () => {
|
|
3066
|
+
for (const listener of [...subscriptions.keys()]) dispose(listener)
|
|
3067
|
+
},
|
|
3001
3068
|
}
|
|
3002
|
-
|
|
3069
|
+
|
|
3070
|
+
statsSources.set(sessionId, { source })
|
|
3003
3071
|
while (statsSources.size > STATS_SOURCE_LIMIT) {
|
|
3004
3072
|
const oldest = statsSources.keys().next()
|
|
3005
3073
|
if (oldest.done === true) break
|
|
3006
3074
|
const evicted = statsSources.get(oldest.value)
|
|
3007
3075
|
statsSources.delete(oldest.value)
|
|
3008
3076
|
try {
|
|
3009
|
-
evicted?.release?.()
|
|
3077
|
+
evicted?.source?.release?.()
|
|
3010
3078
|
} catch {}
|
|
3011
3079
|
}
|
|
3012
3080
|
return source
|
|
3013
3081
|
}
|
|
3014
3082
|
ctx.effect(
|
|
3015
3083
|
() => () => {
|
|
3016
|
-
for (const
|
|
3084
|
+
for (const entry of statsSources.values()) {
|
|
3017
3085
|
try {
|
|
3018
|
-
source.release?.()
|
|
3086
|
+
entry.source.release?.()
|
|
3019
3087
|
} catch {}
|
|
3020
3088
|
}
|
|
3021
3089
|
statsSources.clear()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-recent-usage-metrics",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Geek level for the Performance & usage setting: a live reading of the newest request (first-token latency, speed, tokens, total, amortised) plus a drawer listing the last ten, all derived from the shipped Statistics view data",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|