dsh-perf-hud

devtested: passnpmMIT

Client-side performance HUD and always-on recorder for DeepSeek Harness: FPS and worst frame, long tasks with four-leg attribution (React fiber / subtree / DOM writes / scheduler), per-plugin callback rate and sync time, DOM growth and writers, JS heap, host event loop and link RTT, plus a night-wat

下载量 / Downloads
1,531
依赖体积 / node_modules
9.1 MB
测试时间 / Tested
2026-11-06
DSH 版本 / Version
0.2.2-rc.1

Install

dsh plugin --profile web add dsh-perf-hud

Sandbox test results

StageResult
1 install✓
2 list✓
3 mount✓
4 boot—

Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).

README

dsh-perf-hud

纯客户端性能 HUD:右下角一块浮层,回答"现在卡不卡、卡在谁身上"。
帧与最差帧 · Longtask(带四条腿归因)· 每插件回调速率与同步耗时 · DOM 增长与写入者 · JS 堆 ·
宿主 event-loop 与链路 RTT · 证据包导出 · 守夜(偶发卡顿的事后回看)·
守夜层:偶发卡顿自动留存最差几次,连证据一次性导出。

<p>

<img alt="status" src="https://img.shields.io/badge/status-v0.8.0%20%C2%B7%20%E5%B7%B2%E5%8F%91%E5%B8%83-green" />

<img alt="license" src="https://img.shields.io/badge/license-MIT-blue" />

<img alt="dsh" src="https://img.shields.io/badge/DSH-0.2.1--rc.1-4d6bfe" />

</p>

<p align="center">🌏 <b>中文</b> · <a href="./README_EN.md">English</a></p>

✅ v0.8.0 已发布到 npm —— dsh plugin --profile web add dsh-perf-hud 即可(发布门槛与未还的债见
[REVIEW.md](REVIEW.md))。

📑 目录

  • [✨ 它回答什么问题](#-它回答什么问题)
  • [🧭 和 DSH 自带开发者工具的分工](#-和-dsh-自带开发者工具的分工)
  • [🚀 安装](#-安装)
  • [📊 面板怎么读](#-面板怎么读)
  • [🛰️ 偶发卡顿:守夜层](#️-偶发卡顿守夜层)
  • [🔬 四条腿归因](#-四条腿归因)
  • [🧪 隔离实验(已移到控制台)](#-隔离实验已移到控制台)
  • [⚠️ 风险与边界(必读)](#-风险与边界必读)
  • [🧰 控制台 API 与存储键位](#-控制台-api-与存储键位)
  • [🛠️ 开发与测试](#️-开发与测试)
  • [🤝 参与贡献](#-参与贡献)
  • [📚 文档地图](docs/README.md) —— 每份文档干什么、什么时候读

✨ 它回答什么问题

问题 · 面板给的答案

现在顺不顺? · FPS / 最差帧 / 帧内未归因;标题栏常显「流式 / 空闲 Ns」作为解读前提

卡的那一下是谁干的? · Longtask 明细 + 四条腿归属(组件名 / 包名 / DOM 写入者)

哪个插件在偷偷跑? · 按插件的回调执行速率(/s)、峰值速率、同步耗时;DOM 写入次数与写入者

是插件的问题还是宿主/链路的问题? · 宿主 event-loop p50/p99/max + 链路 RTT/吞吐,附阈值判色与「链路已排除」结论

关掉某个插件到底有没有用? · 隔离实验(控制台):逐个关闭 60 秒,和紧邻基线对照,并给出噪声底

🧭 和 DSH 自带开发者工具的分工

DSH 自带的实验性开发者工具(插件管理器官方分组,NodeJS 诊断 / 会话数据诊断)是前台显微镜:断点、Host console、网络抓包、原始 session 日志、Cordis 树。本插件是行车记录仪:默认常开,记帧、LoAF、事件计时,事后给事故包。

想回答 · 用

现在为什么慢 / 断点 / 网络 / 会话日志 / Cordis 树 · 开发者工具

刚刚那一下有多慢、是谁、能不能复盘 · 本插件

两边不互相依赖。开发者工具的 Performance 只采 Host CPU,Client 侧只有 Console 求值与只读 Sources,不给页面渲染 trace / 帧数据;所以本插件事故包里的 devtools-trace 缺失仍然如实保留,不会因为装了它就变成"已补上"。它也抓全量 Host fetch(含密钥),与本插件"导出前减法脱敏"是两套口径。

快捷键:Cmd/Ctrl+Shift+U 展开/收起(macOS / Windows;Linux Web 的平台白名单不允许这个组合,未绑定;收起/展开不抢输入框焦点)。

🛰️ 偶发卡顿:守夜层

面板要你盯着看;偶发卡顿恰恰发生在你没看的时候。所以插件默认开着一层不插桩的记录器:

  • 每帧一条帧级环形缓冲(12000 条 ≈ 60fps 下 3.3 分钟),另加一个 LoAF 观察器(Chrome 123+);
  • 一帧超过 100ms(或 LoAF 的 blockingDuration 超过 100ms)就自动抓一次:前 10 秒 + 后 3 秒原样切片,不等你反应;
  • 留存取三档并集:全局最差 10 次 + 每天最差 1 条(最多最近 7 天,清单里标「当天最差」)+ 最近 3 次(不看严重度,标「最近一次」)—— 只按全局最差排时,同一小时里的 10 次会把"昨天下午那次卡"整条挤掉(实测 捕获 40 · 丢弃 38)。抓到了但没留存下来的次数单独记(dropped),与"没抓到"分开;
  • 事故包把脚本 URL 抽成一张表(urls + scripts[].u):同一个 combo URL 会被写几十遍,抽表之后一份小 4 倍(实测 116.6KB → 29.6KB),信息一点没丢;
  • 落盘另有 3MB 字节上限,超了就从最弱的开始裁(最近档最后动);配额真的不够时先丢掉可重建的 sourcemap 分段表,再谈裁证据 —— 裁掉的条数写在常显那一行的「裁掉 N」里,内存按同一份裁(看到的永远等于刷新后还在的)。(上限原本是 512KB,但实测一个晚上就被裁了 6 次、实际只留得下 8 条):刷新页面、重启浏览器之后还在,watch().restored 告诉你这次读回了几条;写不进去会如实报 persistError,不假装存下来了;
  • "发生了几次"和"留了几份现场"是两个数,分开算(产品计划 §3.A):留存清单受容量限制,它的条数不能回答"最近卡了几次" —— 被挤掉的卡顿不是没发生。所以另有一层轻量计数:每分钟一个桶、只留最近 24 小时,再按天汇总 30 天(跨刷新)。三个数各有各的名字,面板悬停里都写着:
  • 检测到 = triggered —— 起过几次捕获。一次卡顿通常是一串触发,而这一串只算一次;被并进同一次 pending(或落进已覆盖窗口)的那些触发不算新的一次;
  • 同串触发(coalesced) —— 上面那一串里被并掉的触发个数。它是"这一串有多密",不是次数:把两者相加会把一次停顿记成十几次(在真页面上实测踩过,一次 3 秒的后摇里每帧都触发一次);
  • 已保存现场 = 留存清单的条数 —— 受容量限制(最差 10 次 + 每天 1 条 + 最近 3 次),不等于检测到;
  • 丢失 = (triggered − captured) + dropped + trimmed + failed —— 起过捕获却没能切成现场 / 抓到了但没留存下来 / 字节装不下被裁 / 写盘失败,单列,不混进"检测到"。

折叠药丸上那一格 检测 N 次 说的是最近 24 小时(跨刷新),不是留存份数;悬停里三个数各自的名字与口径都写着(__perfHud.tally() 同一个口径)。

  • 标签页隐藏造成的几百秒"间隙"不算卡顿(rAF 被挂起),事故包里标成 gap;
  • 这一层不包装任何原型:只用本来就在跑的 rAF 循环与一个 PerformanceObserver,所以默认开着也不改你的环境。
__perfHud.watch()             // 在不在录、覆盖多少秒、LoAF 装没装上、抓到几次
__perfHud.incidents()         // 最差几次的摘要(索引 0 最差)
__perfHud.report(0)           // 完整事故包:帧序列 + LoAF 条目 + 窗口内 longtask + 环境读数
__perfHud.exportIncident(0)   // 下载成 JSON —— 把这份丢给分析的人(或 agent)就行
__perfHud.capture('切了视图')  // 手动标记"刚刚卡了一下"(自动回退 3 秒的反应时间;返回值说清留在哪一条)
await __perfHud.stress(8000, 80) // 自测:返回 longtask / fiber / DOM 写入三条腿各测到什么(也进面板工具箱)
__perfHud.clearIncidents()    // 只清事故;clear() 刻意不动它

事故包里的脚本会定到包名:合并批(58 个包挤在一个 URL 里)靠"字符位置 → 行号 → sourcemap 分段表"还原,单包 URL 直接从 URL 读;拿不到时写清为什么(精确归因没开 / 文本还在拉 / 越界),并按包给一份汇总 —— 这一层才是"是谁"。

导出文件名是 perf-hud-20261008-220358-i84.json —— 事故发生时刻的本地时间 + 事故 id(不是导出时刻;日期在前可排序,完整 ISO 在文件体里)。

看的是哪一帧、怎么翻:圆环上面有一行 帧 1/12 · 530ms · ≥100ms ◀ ▶ ⟲ —— 判据与排序统一用 frameSeverity = max(blocking, 帧时长):清单只收最近 200 条长动画帧里值得回看的(帧时长 ≥ 100ms,与守夜触发线同一条;刚过 50ms 的边角帧不进榜),排序也按 max(blocking, 帧时长)、最差的排第一(blocking 只是补充 —— 只看它会把 92ms 的帧排到 300ms 的帧前面);一条都不到线时退回显示全部(那时标签上不出现 ≥100ms)。默认是自动:一直跟着最差那条走(所以没有更差的出现时,它不会变);按 ◀ ▶ 换一条看,就钉住那一条,⟲ 回到自动。选中的那条被挤出 200 条之后会自动退回自动,不会指着一个已经不在环里的帧。

长动画帧的时间去哪了:「取证明细」里有一张恒定四段的圆环(84px),段与段之间留 1.4px 缝:

  • 脚本(被 LoAF 归到脚本的同步 JS)= 红;任务里没归到脚本的那部分(短于上报阈值的脚本 + 事件派发 / 微任务 / GC)= 中性灰;渲染(rAF 回调)= 橙;样式+布局+绘制 = 蓝。

环只说这一件事("帧的时间分给了谁")。脚本内部"是谁"、以及"哪个包"另画两条 100% 堆叠横条(紧接着的「按来源」「按包」两行:上面是每项一行的小表(名字左、毫秒右),下面一条是占比;段越靠左越重(不透明,名次靠压暗而不是变透明),段间 1px 缝,小段保底 3px;鼠标停上去给 名字 毫秒 · 占比;顶段 ≥ 80% 时条右边会内联一个占比,例如 86%)。两个维度交叉的问题("哪个包的哪类回调")交给再下面那张可折叠的交叉表,默认收起,展开是 Top3 × Top3 + 其余行列。悬停联动整体停用、辅助代码已删干净(环 ↔ 按来源/按包堆叠条 ↔ 交叉表):2026-10-08 实测「鼠标指到交叉表的行标签会让整个面板被外壳停用」,而且它不经过我们自带的那层错误边界(连错误原文都拿不到);2026-10-11 在当前代码上又复现不出来 —— 既不能证明根因、也不能证明已修好,所以保持停用(见「风险与边界」第 9 条);环、两条堆叠条与交叉表本身照常工作。小表的每一行可点(点了复制「名字 + 毫秒」到剪贴板,成功/失败都照实显示),但堆叠条的色段本身不可点 —— 它只是同一份数据的比例视图,没有别的去处。

这是页面内能拆到的极限:绘制与合成在页面内分不开(要分开只能录 Chrome trace),所以最后一段是"剩下的都在这儿"。

脚本那一侧再拆三层、两个分母:「本帧」那组第一行(以及窗口组在非全纯执行时的对应行)把脚本耗时拆成 纯执行 / 强制样式布局 / 暂停 —— 三者是分区,相加 = 脚本耗时,不是另加的时间(forcedStyleAndLayoutDuration 是读布局 / 写样式逼出来的同步布局,pauseDuration 是同步 XHR、对话框把它卡住的时间)。

再往下是同一份脚本耗时的两种切法,由两个小标题分成两组:

  • 「本帧」= 上面那张饼的同一帧(数字能和饼对着看);「窗口」= 最近 200 条 LoAF 的累计,范围就写在小标题上:窗口:200 条长动画帧 · 跨 43分12秒 · 平均每帧脚本 108.1ms。范围写在标题上,行标签就不用每行重复"本帧 / ·窗口"。
  • 按来源(LoAF 的 invoker):这条脚本是谁调起来的 —— setTimeout、DOMWebSocket.onmessage(会话事件流的消息处理器,实测最常见)、Response.json.then(Promise 回调)、script(脚本加载)…… 覆盖全部脚本,所以合计 = 脚本耗时。
  • 按包:单包 URL 直接读出来,合并批按 sourcemap 分段还原(与事故包 incident.packages 同一条路)。只覆盖"定得到包名"的脚本,所以合计 ≤

…(truncated)

Rendered from the README published on npm. Formatted only, not edited.

Versions

Published on npm::7,最后更新 2026-11-04

Links

GitHub · Homepage · npm

Same category