DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Perf Hud — DSH Plugin for DeepSeek Harness
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins
P

dsh-perf-hud

Perf Hud

纯客户端性能 HUD:帧与最差帧、Longtask(四条腿归因)、每插件回调速率与同步耗时、DOM 增长与写入者、JS 堆、宿主 event-loop 与链路 RTT,带隔离实验与一键导出 JSON;守夜层默认常开,用帧级环形缓冲与 LoAF 自动留存最差几次卡顿并导出事故包。 Client-side performance HUD for DeepSeek Harness: FPS and worst frame, long tasks with four-leg attribution (fiber / subtree / DOM write / scheduler), per-plugi

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:yanzhaohui1999/dsh-perf-hud#e410b4d71e00e8ee2c82f83645f3183d07ee5510
READMECompatibilityVersions
07 settings

Description

纯客户端性能 HUD:帧与最差帧、Longtask(四条腿归因)、每插件回调速率与同步耗时、DOM 增长与写入者、JS 堆、宿主 event-loop 与链路 RTT,带隔离实验与一键导出 JSON;守夜层默认常开,用帧级环形缓冲与 LoAF 自动留存最差几次卡顿并导出事故包。 Client-side performance HUD for DeepSeek Harness: FPS and worst frame, long tasks with four-leg attribution (fiber / subtree / DOM write / scheduler), per-plugin callback rate and sync time, DOM growth and writers, JS heap, host event loop and link RTT, plus an isolation experiment and one-click JSON export. An always-on night-watch recorder (per-frame ring buffer + LoAF) keeps the worst stalls and exports an incident pack.

Compatibility and provenance

Perf Hud is published as dsh-perf-hud and currently resolves to version 0.4.0. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
github
Registry updated
10/11/2026

Versions

0.4.0stable
10/11/2026
0.3.0stable
10/11/2026

Related plugins

Loading related plugins…

Latest
0.4.0
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
web
License
MIT
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
10/12/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

Related plugins

More verified plugins in developer-tools.

Cc Safety Netcc-safety-netA coding agent CLI hook - block destructive commands and secret file accessRemotedsh-remoteRemote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive/proxy jump), pick a remote workspace, operate on it with 22 rw_* tools (incl. rw_edit/rw_stat/rw_mkdir/rw_remove/rw_move/rw_forward), conflict-aware mirror syFind Plugindsh-find-pluginFind DeepSeek Harness plugins inside the agent — live GitHub dsh-plugin topic search, ranked by stars.Web App@deepseek-ai/dsh-web-appThe dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)

README

dsh-perf-hud

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

🌏 中文 · English

⚠️ v0.3.0 已就绪,还没发到 npm —— 首发卡在 npm 凭据(见 REVIEW.md §3.1)。现在请从源码目录安装; 发布之后这一行会改成 dsh plugin --profile web add dsh-perf-hud。

📑 目录

  • ✨ 它回答什么问题
  • 🚀 安装
  • 📊 面板怎么读
  • 🛰️ 偶发卡顿:守夜层
  • 🔬 四条腿归因
  • 🧪 隔离实验(已移到控制台)
  • ⚠️ 风险与边界(必读)
  • 🧰 控制台 API 与存储键位
  • 🛠️ 开发与测试
  • 🤝 参与贡献

✨ 它回答什么问题

问题面板给的答案
现在顺不顺?FPS / 最差帧 / 帧内未归因;标题栏常显「流式 / 空闲 Ns」作为解读前提
卡的那一下是谁干的?Longtask 明细 + 四条腿归属(组件名 / 包名 / DOM 写入者)
哪个插件在偷偷跑?按插件的回调执行速率(/s)、峰值速率、同步耗时;DOM 写入次数与写入者
是插件的问题还是宿主/链路的问题?宿主 event-loop p50/p99/max + 链路 RTT/吞吐,附阈值判色与「链路已排除」结论
关掉某个插件到底有没有用?隔离实验(控制台):逐个关闭 60 秒,和紧邻基线对照,并给出噪声底

🛰️ 偶发卡顿:守夜层

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

  • 每帧一条帧级环形缓冲(12000 条 ≈ 60fps 下 3.3 分钟),另加一个 LoAF 观察器(Chrome 123+);
  • 一帧超过 100ms(或 LoAF 的 blockingDuration 超过 100ms)就自动抓一次:前 10 秒 + 后 3 秒原样切片,不等你反应;
  • 留存取两档并集:全局最差 10 次 + 每天最差 1 条(最多最近 7 天,清单里标「当天最差」)—— 只按全局最差排时,同一小时里的 10 次会把"昨天下午那次卡"整条挤掉(实测 捕获 40 · 丢弃 38)。抓到了但没留存下来的次数单独记(dropped),与"没抓到"分开;
  • 事故包把脚本 URL 抽成一张表(urls + scripts[].u):同一个 combo URL 会被写几十遍,抽表之后一份小 4 倍(实测 116.6KB → 29.6KB),信息一点没丢;
  • 落盘另有 2MB 字节上限,超了就从最弱的开始裁 —— 裁掉的条数写在常显那一行的「裁掉 N」里,内存按同一份裁(看到的永远等于刷新后还在的)。(上限原本是 512KB,但实测一个晚上就被裁了 6 次、实际只留得下 8 条):刷新页面、重启浏览器之后还在,watch().restored 告诉你这次读回了几条;写不进去会如实报 persistError,不假装存下来了;
  • 标签页隐藏造成的几百秒"间隙"不算卡顿(rAF 被挂起),事故包里标成 gap;
  • 这一层不包装任何原型:只用本来就在跑的 rAF 循环与一个 PerformanceObserver,所以默认开着也不改你的环境。
__perfHud.watch()             // 在不在录、覆盖多少秒、LoAF 装没装上、抓到几次
__perfHud.incidents()         // 最差几次的摘要(索引 0 最差)
__perfHud.report(0)           // 完整事故包:帧序列 + LoAF 条目 + 窗口内 longtask + 环境读数
__perfHud.exportIncident(0)   // 下载成 JSON —— 把这份丢给分析的人(或 agent)就行
__perfHud.capture('切了视图')  // 手动标记"刚刚卡了一下"(自动回退 3 秒的反应时间)
__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 同一条路)。只覆盖"定得到包名"的脚本,所以合计 ≤ 脚本耗时;名字后面会跟一段 「其余来源」,那是定不到插件包名的部分,而且按原因给名字:无 URL(内联脚本)(内联 / 动态构造的处理器)、插件批次(未按 sourcemap 拆)(URL 在 plugins/ 下但还没拆)、或者直接就是文件名(比如 index-5SrrfWpU.js —— 那是应用自己的外壳 bundle,React 根容器上的事件处理器就来自它,它本来不属于任何插件)。先看「其余来源」那段有多少毫秒,再读排行。
  • 这一层只回答"哪一类调用",不回答"函数里面慢在哪":函数级要 Chrome trace(JS Self-Profiling 被 Document-Policy 挡着,页面内没有采样器,实测 new Profiler() 直接抛 NotAllowedError)。

只有"平均每帧脚本"那一栏和饼可比(饼是单帧;除数其实是帧数,不是脚本条数);纯执行 21623.8ms 这种是 200 条各百来毫秒攒出来的累计,不是某一次执行花了 21 秒 —— 跨度越久累计越大。值超过一行时按块式换行(左对齐),不挤标签。

面板会自己藏掉没意义的行(字太多本身就会让人读不动):「回调归因」那一块总在首屏,没开「深录」时降级成一行 —— 标签 回调归因(耗时·下界) + 值 要深录(Top3 表与「其余 (N)」行都不渲染;绝不写 0ms/s,那会被读成"没有开销");插桩之后 0ms/s 才是真的(确实没有回调跑过那次调度点)。DOM 写入 同样只在插桩后显示(不插桩时计数恒为 0)。帧内未归因 与组摘要里的未归因直接写"要深录"—— 不插桩时那个数会退化成平均帧间隔,给数就是误导;一个包名都没定到时,「按包」那条既不会空、也不会摆一行"0 条定到名" —— 喂给堆叠条的就是 whoRows(tally)(定到名的部分 + 按原因命名的「其余来源」),所以那种情况看到的是 插件批次(未按 sourcemap 拆) / 无 URL(内联脚本) / 外壳文件名这些带名字与毫秒的段;没定到名的原因是合并批要靠 sourcemap 还原,而那条路只在「深录」(精确归因)打开时才拉。

「取证明细」组的最前面是一条 60 秒帧条(从右对齐的窄条改成铺满一行的块,约 365px 宽、一格约 6px):一格一秒,柱高 = 那一秒里最差那一帧的间隔,颜色与导出件 ASCII 条带用同一组阈值(≥100ms 红 / ≥50ms 黄);每格 hover 给墙钟时间 + 那一秒最差帧多少 + 点得动还是点不动 —— 只有那一秒真的有长动画帧(LoAF 环里存在)的格子才可点,点了就把翻帧切到那条帧;点不动的格子明确不给手型,并在 title 里说明。整格都是 rAF 挂起的那一秒画成最矮的灰柱 —— 它是间隙,不是卡顿。展开着不动也能看出"这分钟哪几秒抖了"。

紧跟它的是「事故回放」—— 与上面那种实时帧条刻意分开:它画的是选中的那条事故当时那一段窗口(标签写明是哪一条、跨度多大,例:事故回放 · i84 · 13秒),数据全部来自事故包(incident.frames / incident.loaf / summary),所以刷新生效,而且不增加任何采集成本。横轴按时间放:整段 rAF 被挂起的那几秒留白,一眼看出"这段时间根本没在测",不是没卡(按序号放会把那个间隔压成一根柱子,那就等于说谎);档位与实时帧条用同一个函数、同一组 theme token,两处读数不许打架。下面三行分别是范围(窗口跨度 / 帧数 / p50 / p99 / 最差帧 / 缺口数)、最差帧的时间去向(复用帧内四段分解:脚本 / 任务 / 渲染 / 样式+布局+绘制,相加 = 这一帧时长)、口径脚注(这是捕获时那一段,不代表现在)。⚠️ 它不排帧内执行顺序 —— LoAF 不给每帧内脚本的起止时间;要看逐帧明细仍然走「导出事故包」+ tools/inspect-incident.mjs。它在「取证明细」组里,组折叠时不计算;历史事故跨会话读回后依然能回放(数据本来就在包里)。

首屏「最差事件」块(守夜留下的最差那条事故,按严重度排 —— 「最差」不是「最近」):标题行是 id · cause · 墙钟时间,右边给严重度 —— 文字 + token 颜色两样都给(≥200ms 严重 / ≥100ms 警告,阈值与面板判色同一条线,不只靠颜色)。这一行的值有三种状态:未开录 → 未开录;没有事故 → 还没抓到(≥100ms)(阈值就写在这一行里,不再另起一行重复);有事故 → 严重 / 警告 + 毫秒。下面依次是 自动·最差 / 自动·最近 / 已选中(标题行右边有个「最近 / 最差」切换,一键切"自动跟着谁"并清掉手选)、JS 证据有无(那一帧有没有 longtask/LoAF 与它重叠)、主要归属(先取按包汇总的第一名,没有就看 topScripts[0].pkg,一个都没定到就写「未归因」)、窗口行(帧数 / LoAF 条数 / 按包覆盖率 已定名/总数,带分母);再底下那行只说"读回 / 裁掉 / 落盘失败"这类状态,不重复"还没抓到"。三个按钮都是真动作:看这一帧(选中它 + 展开「取证明细」+ 把翻帧钉到事故开始时刻最近、且落在事故窗口内的那条长动画帧 —— 跨会话读回的事故定位不到时会写一句如实说明)、导出事故包(这条事故的完整证据包)、复制摘要(自描述的交接文本:结论 + 口径 + 「可疑脚本」的证据行(最多 4 条)+ 「窗口内 ≥100ms 的交互」(标明是端到端时长、不是帧内拆分)+ 缺什么 —— 贴给别人能接着查;整份 JSON 走「导出事故包」)。

面板上能看到: 常显只留三块 ——「概览(现在卡不卡 + 最差那条是谁)」「最差事件(守夜留下的事故,见上一段)」「回调归因 Top3(谁的锅,没插桩时降级成一行「要深录」)」;原来的「帧内未归因」搬进了取证明细组(数字挂在那一组的摘要上:最差秒 186ms · 未归因 50ms),因为它是「解释」不是「结论」;DOM 写入则从这一组挪进了「DOM 与堆」组 —— 写入者属于 DOM,不属于"这一帧的时间去哪了"。 常显区一行 守夜 抓到 N 次 · 最差 X ms(附带最近那一条的时刻与备注);工具箱里有最差几次的清单(按严重度排、可点选:选中的那条前面有 ● 且加粗,点了之后首屏「最差事件」块与「取证明细」的翻帧都跟着走 —— 三处共享同一个选中上下文,存的是事故 id 不是下标)+ 「标记刚才」(手动补记,自动回退 3 秒)与「导出最近一次」两个按钮,清单下面按需出现两个布局按钮:全部折叠(只在有组开着时出现;收起所有组,不动翻帧与事故选中)与恢复默认布局(只在布局/上下文与默认不同时出现 —— 有组开着、翻帧被钉住、手选了某条事故、自动口径不是"最差"、或挂着「已复制 / 定位不到」这类一次性说明;组全收起 + 翻帧回自动 + 事故选中回自动),两者都无意义时整行不渲染;面板折叠成小药丸时也会显示抓到几次 —— 这个数字就是"行车记录仪在工作"的那盏灯。

事故包带会话基线(排除本窗口之后的 p50/p90/最差,所以「比会话最差还差」这种比较才成立),导出件另外带一份跨会话基线(每个会话一条 p50/p90/p99/最差/帧数/时刻,最多留最近 20 条)—— 它只进导出件、面板不显示,而且标明是粗参照,不是回归判定(会话之间的负载、视口、是否插桩都不同,帧数量级能差一个数量级)。还会写明最差那一帧有没有 JS 证据:若既没有 longtask/LoAF 与它重叠、前后又都是 60fps 的健康帧,就标成 suspectedThrottle —— 那种情况更像浏览器把 rAF 节流/挂起了一次(窗口被遮挡、省电),而不是页面真的忙了那么久(真实数据里出现过一帧 983.7ms,就长这样),所以能直接回答「这一段是真的比平时差,还是这台机器一直这样」;它是纯 JSON,人也能直接读;想看得舒服点:node tools/inspect-incident.mjs <file.json> 会渲染出帧条带(哪一格是哪个量级一眼可见)、窗口内的 longtask、LoAF 与按耗时排序的脚本。它自带 context.missing:这一次没拿到什么(没有 DevTools trace、没有宿主 CPU profile、平台不支持 LoAF……)以及怎么补 —— 缺的东西写清楚,比不写有用。它不做归因推断,只把当时的东西原样装好。

🚀 安装

发布到 npm 后:

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

从源码/本地目录(开发用,link: 安装,改客户端半刷新页面即可生效):

git clone <repo> && cd dsh-perf-hud
dsh plugin --profile web add "$PWD"

装完刷新页面即可看到右下角浮层。改动生效方式:

改了什么怎么生效
client.js(客户端半)刷新页面(dev 环境下客户端 HMR 也会自动热载)
index.js(宿主半)必须重启宿主服务
package.json 的 dsh.*重装或重启

宿主半是可选的:它没加载时面板照常工作,只是「宿主 event-loop」「宿主事件到达」两处显示不可用,并写明原因 —— 不假装成功。

📊 面板怎么读

面板分三档,默认是最便宜的那一档 —— 它测的是别人的性能,所以自己必须先便宜、可关、可还原:

档位什么时候做什么代价
守夜(默认)一直开着帧级环形缓冲 + LoAF 观察器 + 交互计时;自动留存最差 10 次 + 每天最差 1 条零插桩:只挂本来就在跑的 rAF 和两个 PerformanceObserver,不动任何全局
现场想看一眼,展开面板概览 / 最差事件 / 回调归因 Top3 / 守夜那行;其余五组折叠与守夜同一档采集 —— "展开看看"不会改变被测对象
深录面板右上角「深录」才装上 setTimeout / setInterval / rAF 与 DOM 写入的包装,并顺带拉那份 12MB 的合并批 sourcemap(精确归因)有代价,必须显式开;关掉立刻还原成原生引用

面板默认分层:常显只留"现在顺不顺 + 谁的锅",其余五组(取证明细 / 包 × 来源 / DOM 与堆 / 宿主与链路 / 工具箱 —— 「包 × 来源」嵌在「取证明细」里)默认折叠,折叠组的标题栏自带一句话摘要。折叠组真的是惰性的:group() 支持传一个函数当子节点,收起时连函数体都不进,「取证明细」里那些派生统计(脚本按包 / 交叉表 / 帧条)在收起状态下不再每个 tick 重算。

读数口径(面板 hover 里也有同一份)
FPS / 最差帧自己的 rAF 循环,1 秒滚动窗口。合并判色:FPS ≤50 或最差帧 ≥120ms → 黄;≤30 或 ≥200ms → 红(任一触发即降色)
LongtaskPerformanceObserver({entryTypes:['longtask']}),近 60 秒;命中数 = 四条腿任一给出名字的条数 / 总条数
帧内未归因帧间隔里没被插桩回调解释掉的部分(样式重算、绘制、等待 vsync)。CSS 动画只在这里现形 —— 它不经 JS 调度器
帧 N/M ◀ ▶ ⟲面板在看哪一条长动画帧:最近 200 条里 帧时长 ≥ 100ms(判据与排序统一用 frameSeverity = max(blocking, 帧时长),与守夜触发线同一条;不到线的边角帧不进榜,一条都不到线时退回全部)的那些,排序也按 max(blocking, 帧时长)、最差在前 —— blocking 只是补充,只看它会把 92ms 的帧排到 300ms 的帧前面。默认自动跟着最差那条(没有更差的就不换);选过之后钉住,⟲ 回到自动;选中的被挤出 200 条会自动退回
最差事件(首屏)守夜留下的最差那条事故,按严重度排,不是按时间(「最差」不是「最近」)。标题行 id · cause · 墙钟时间;值有三种状态:未开录 → 未开录、没有事故 → 还没抓到(≥100ms)(阈值写在这一行里)、有事故 → 严重度文字 + token 颜色两样都给(≥200ms 严重 / ≥100ms 警告,与面板判色同一条线)+ 毫秒 + 自动·最差 / 自动·最近 / 已选中(标题行右边有「最近 / 最差」切换,一键切"自动跟着谁"并清掉手选);再来是 JS 证据有无、主要归属(先取按包汇总的第一名,没有就看 topScripts[0].pkg,一个都没定到写「未归因」)、窗口行(帧数 / LoAF 条数 / 按包覆盖率 已定名/总数,带分母)。它底下那行只说"读回 / 裁掉 / 落盘失败",不重复"还没抓到"。三个按钮都是真动作:看这一帧 = 选中它 + 展开「取证明细」+ 把翻帧钉到事故开始时刻最近、且落在事故窗口内的那条长动画帧(跨会话读回的事故定位不到时写一句如实说明)、导出事故包、复制摘要(自描述的交接文本:结论 + 口径 + 「可疑脚本」证据行(最多 4 条,与面板同一份、用捕获时存下来的定位,所以刷新后照样在)+「窗口内 ≥100ms 的交互」(端到端,不拆帧内)+ 缺什么)
工具箱的事故清单清单按严重度排、可点选:选中的那条前面有 ● 且加粗,点了之后首屏「最差事件」块与「取证明细」的翻帧都跟着走(同一个选中上下文,存的是事故 id 不是下标;选中的被挤出留存清单会自动退回自动)。清单下面两个布局按钮都按需出现:全部折叠(只在有组开着时出现;收起所有组,不动翻帧与事故选中)、恢复默认布局(只在布局/上下文与默认不同时出现 —— 有组开着 / 翻帧被钉住 / 手选了事故 / 自动口径不是"最差" / 挂着一次性说明;组全收起 + 翻帧回自动 + 事故选中回自动,并清掉「已复制 / 复制失败」与「定位不到帧」这两句一次性说明),都无意义时整行不渲染
帧里花在哪(恒定四段的环)最近那条长动画帧按 LoAF 的三个时间点拆成四段:脚本(红)/ 任务里没归到脚本的那部分(中性灰)/ 渲染(rAF 回调,橙)/ 样式+布局+绘制(蓝)。段间 1.4px 缝,小到扣不起缝的段不扣。绘制与合成分不开 —— 要分开只能录 Chrome trace,所以最后一段是"剩下的都在这儿",

「其余 (N)」 = 除 Top3 外还有几个归属方(名字各不相同,按累计耗时排序),行值给它们的合计速率。

导出 JSON 带 context 自描述头:场景 / 空闲秒数 / 精确归因与每个 sourcemap 分段的状态 / 宿主路由与 profile、Node、插件版本 / 实验候选与方法 / 全部判色阈值 / 跨会话基线。window.__perfHud.snapshot() 与面板「导出 JSON」同源。

交互延迟(event / element timing,只记 ≥100ms 的事件)也只进导出件。那一段给端到端时长 ms、推导出来的 waitMs(输入延迟)与 handlerMs(处理器本身),isInteraction 标出哪些是真交互(interactionId > 0);帧内拆分(输入延迟 / 处理 / 呈现)页面内拿不到,要拆得去 DevTools 的 Interactions 轨道看。element timing 只观察带 elementtiming 属性的元素 —— DSH 的 DOM 上没有这个属性,所以这一路恒为 0,导出件里照实写 0 并解释原因,不假装采到。

🔬 四条腿归因

长任务归属同时给出不一致的两条,不挑好看的报:

腿机制擅长 / 局限
fiber从被改动节点往祖先走最近的具名函数组件擅长增量更新;整棵子树一次性挂载时只能给到容器
子树从被改动节点往子树走,优先 alternate === null(React 标记"本次新挂载")补"整棵挂载"的空档;有界 BFS
DOM写入采样 appendChild / insertBefore / setAttribute(1/10),在写入点抓栈并取组件名提交阶段栈里没有插件帧时的主力;估算值带 ≈
包名调度点插桩(setTimeout/setInterval/rAF)与长任务时间重叠定时器/动画类;事件处理器、Promise、微任务不在内

深录会按合并 bundle 自带的 indexed sourcemap 把单包名还原出来(懒加载,同一个 rev 只拉一次那份 12MB 的 map)—— 面板上不再有独立的「精确」按钮,深录开就是精确归因开;只想在控制台单独开它用 __perfHud.setPrecise(true)(与深录互相同步)。

🧪 隔离实验(已移到控制台)

面板上不再提供入口:它测的是"每个插件的固定开销",而那个方向已经明确不做(见 AGENTS.md §0.1); 它还要求写 profile 的持久化选择,不该放在一个"随手点"的位置。能力没删,控制台调 __perfHud.experiment()。

逐个关闭候选插件 60 秒 → 测量 → 恢复并逐项核验,每个候选只跟紧邻基线对照(A/B/A),基线漂移作为噪声底,Δ 小于噪声一律标「噪声内」;窗口含流式则标「不可比」且不给结论。

候选从你当前的 bundle 列表里推导,不写死任何插件名:取「已启用 + 可移除 + 非官方内核 + 不是本 HUD 自己」的社区包,按名字排序最多取 4 个(每个候选 90 秒)。列表为空时实验直接说"没有可对照的第三方插件",不拿不存在的名字空跑。

所有窗口注入同一份合成负载(每 250ms 燃烧 8ms,走未插桩的 setInterval,只烧 CPU 不动 DOM,避免污染归因表)—— 空闲下的比较本身没有意义。看「超出噪声」列才是证据。

⚠️ 它测的是固定开销(跑飞的时间器、常驻动画、轮询、observer),不是"该插件在某种用法下才贵",别当因果证明。

⚠️ 风险与边界(必读)

  1. 默认开启,但默认不插桩。安装后浮层默认就显示,这一档(守夜/现场)只挂本来就在跑的 requestAnimationFrame 与两个 PerformanceObserver,不动任何全局;面板右上角那个「深录」开关打开之后,才会插桩 setTimeout / setInterval / requestAnimationFrame 以及 Node.prototype.appendChild / insertBefore、Element.prototype.setAttribute(1/10 采样),并顺带拉那份 12MB 的合并批 sourcemap(精确归因)。关掉深录,插桩立刻卸掉并还原成原生引用。
  2. 一切可还原。面板「关闭」或 window.__perfHud.disable() 会把原型方法还原成原生实现,并且只在"当前仍是我们那个 wrapper"时才还原(不会踩掉别人的包装)。
  3. 隔离实验会临时写 profile 的持久化 bundle 选择(pluginManager.setBundleEnabled)。它有硬安全线:必须 confirm、拒绝改本插件与 session-persistence-jsonl、每步记宿主回执、结束后逐项核验恢复;中断也会走恢复流程。面板上已不提供入口,只在控制台调 __perfHud.experiment()(__perfHud.marks() 同理)—— 不想动配置就别调它。
  4. 宿主半只做两件事:事件循环采样与一个 HTTP 路由(/api/dsh-perf-hud/host,受宿主自身鉴权保护,无 cookie 返回 401)。它不注册服务、不改宿主原型、不写插件自己的持久化状态。
  5. CSS 动画不经 JS:鲸鱼摆动那类 steps() 关键帧只体现在 FPS / 最差帧 /「帧内未归因」,归因表里看不到它。
  6. 命中率会被 DOM 腿抬高:流式时几乎必然命中,必须配合「归属 Top」分布读。记录器自己也在四条腿的射程里 —— 命中本 HUD(PerfHud / PerfHudStress / 包名)的长任务会被标成「记录器自身」,并从「归属 Top」里单列出来,只有别的候选都说不通时才让它当元凶(静默剔除会让人读成"这段时间没人干活",比标注更糟)。
  7. 面板状态只进 localStorage;导出件在你本机生成,不发往任何地方。
  8. 浏览器在另一台机器上时,「链路 RTT」高是真实的:如果经隧道访问 127.0.0.1:3080,隧道本身的 RTT p50 可以是数十到数百毫秒,而本机 curl 同一路由只要几毫秒。面板为此有「疑似链路」判据和「链路已排除(宿主与隧道均正常)」结论 —— 别把这个数字当 bug。
  9. 三处悬停联动仍然停用,而且辅助代码已整个删掉(环 ↔ 按来源/按包堆叠条 ↔ 交叉表):2026-10-08 的症状是,真实鼠标移到交叉表的行标签上会让外壳把整个面板条目停用(面板整块消失,而 errors 为空、window.onerror 不响、__perfHud.enabled 仍是 true)—— 也就是它不经过我们自带的那层错误边界,连错误原文都拿不到;2026-10-11 在当前代码上复现不出来:真实鼠标依次扫过交叉表内 163 个元素(外加用 JS 派发 153 次 mouseover),全程存活、errors: 0、环数不变、控制台无输出 —— 所以既不能证明根因、也不能证明已修好。因此保持停用,并把那套辅助代码(hot / setHot / hotProps / hotStyle 与各处的 hot === …)整个删掉,不再留半活的死代码。点击类交互不受影响(清单选中、复制、帧条点击、折叠都已实测)。

🧰 控制台 API 与存储键位

window.__perfHud:

成员作用
snapshot() / export()导出的对象 / 触发下载
watch()守夜层状态(在不在录、覆盖秒数、LoAF 状态、抓到几次)
incidents() / incident(id)最差几次的摘要 / 完整事故对象(索引 0 最差)
report(id) / exportIncident(id)事故包对象 / 触发下载
capture(note)手动标记"刚刚卡了一下"(回退 3 秒反应时间)
clearIncidents()只清事故(clear() 不动事故)
table() / ring / longTasks归因表 / 环形缓冲 / 长任务明细
verification()验收自证(四条腿计数、独立包名、空闲基线、导出段名)
enable() / disable()总开关(等价面板「开启/关闭」,会还原原型方法)
precise / setPrecise(on)精确归因开关与状态
mark(note)场景标记
experiment() / abortExperiment() / baselines()隔离实验启停与基线读数
stress(nodes?, burnMs?)渲染型卡顿自测(显式燃烧,面板会标注"人为燃烧")
hostEvents(on) / host() / link()宿主事件到达延迟开关 / 宿主回执 / 链路样本

localStorage 键位(清状态时按需删):dsh-perf-hud:ring:v1(环形缓冲)、:on、:collapsed、:precise、:groups、:marks、:map:<rev>:<len>(按 rev 缓存的 sourcemap 分段表)。

🛠️ 开发与测试

node tests/attribution.mjs   # 11 条:脚本 URL 形状 → 包名 / bundle key
node tests/logic.mjs         # 408 条:从 client.js 抽真函数进沙箱 + 静态检查 + 长度预算
node tests/host.mjs          # 39 条:用假 ctx 驱动真实 index.js
# 合计 458 条断言,离线,无需 DSH 在跑

i18n 词典现在是中英各 239 键(两本字典键集合必须一致,且每个键都得有人引用 —— 撤下界面时留下的死键会被断言报出来)。

线上验证(需要先换 cookie,步骤见 AGENTS.md §2.2):

DSH_COOKIE=… node tests/live-sourcemap.mjs   # 真实 indexed sourcemap 的分段与包名
DSH_COOKIE=… node tests/live-host.mjs        # 真实路由;退出码 3 = 宿主半还未加载(需要重启)

改到面板渲染或 apply() / start() 路径时,再跑一次真页面验证(自己起 headless Chrome,token 从 ~/Library/Logs/ai.dsh.web.log 取):

node tools/check-panel.mjs        # 面板渲染 0 错误 + 目标元素在场(--expr= 换断言)
node tools/check-storage.mjs      # 写 localStorage → 重新加载 → 读回
node tools/check-interaction.mjs  # CDP 发真实输入,验 event timing 真的交付

⚠️ 这一组不是离线门禁:要真浏览器 + 本机跑着 dsh web,所以 npm test 与 CI 里都没有它(CI 只对 tools/*.mjs 做语法检查)。离线三套件只抽纯函数、不跑 apply() 与渲染,而"三套件全绿、浏览器里 面板整块消失"这种事本项目真出过 —— 所以改那两条路径时别只跑 npm test。

开发规则、提交规范、代码约定、源码级归因机制与已证伪的死路见 AGENTS.md(§6 机制 / §8 坑); 贡献流程见 CONTRIBUTING.md。

🤝 参与贡献

欢迎报 bug 与提 PR。提交前请跑完上面三套离线断言,并阅读 CONTRIBUTING.md 与 AGENTS.md。

📄 License

MIT

不能读成"样式布局占比"
按来源 / 按包(两条 100% 堆叠条)同一份脚本耗时的两种切法,各一条满宽的条:上面是"名字 + 毫秒",下面是占比(Top3 + 其余,段宽 = 该维度内的占比)。来源覆盖全部脚本(合计 = 脚本耗时);按包只覆盖定得到包名的那些,其余按原因给名字(无 URL(内联脚本) / 插件批次(未按 sourcemap 拆) / 外壳文件名)。两个合计本来就不相等,别互相印证。小表的每一行可点 —— 点了复制「名字 + 毫秒」到剪贴板,成功/失败都照实显示(已复制 / 复制失败: 原文);堆叠条的色段本身不可点(以前是手型却没有任何行为,那是假按钮,已去掉)。悬停联动仍然停用、辅助代码已删(见「风险与边界」第 9 条)
包 × 来源 · 窗口(默认折叠的交叉表)两个维度交叉开:行 = 包(或"其余来源"),列 = 调用来源,格子 = 毫秒(悬停给占比)。只展开 Top3 × Top3,其余行列用减法补足,四块之和 = 脚本总耗时。组标题写明范围是窗口,hint 也写明那是最近 200 条长动画帧的累计(与上面那两行「·窗口」是同一份数据),不是某一帧。回答的是"哪个包的哪类回调在烧时间"
可疑脚本(在「取证明细」里)把「事故 → 帧 → 来源 → 包 → 文件:行」落到个体脚本上:上面那几块回答"时间花在哪个包 / 哪类调用"(排行),这一块回答"打开哪个文件、哪个函数"(定位)。最多列 4 行,按耗时降序,而且只列能定位的行(函数名 / 包名 / 文件至少有一个 —— ⚠️ 光有行号不算:合并批里的行号不知道属于哪个包,指不出任何可打开的东西;一个定位字段都没有的行不占名额,不再摆"函数名未记录 @ 包未定名 + 毫秒");每行 <函数名> @ <包名> + 毫秒,悬停给完整证据行,点这一行复制「函数 @ 包 · 文件(:行) · 耗时 · 来源」到剪贴板(成功/失败都照实显示)。⚠️ 合并批不拼 文件:行:那种脚本的行号属于合并后的大文件,而文件只回答"这段代码属于哪个包的那个文件" —— 两者不是一个坐标系,拼起来看着精确其实指不到任何一行,所以显示成 文件 · 合并脚本行 25580(行号单独一段、写明它只在合并脚本里成立)。范围写在标题上:事故优先(可疑脚本 · 事故 i84 · 窗口内,事故的脚本是窗口内切片、最多 5 条、跨会话读回之后仍在 —— 这几行的包 / 文件 / 行号读的是捕获那一刻存进事故包的定位(summary.topScripts[]),不是现算,所以刷新、关掉深录都不会把它算成 null);没有事故的脚本时才退回 可疑脚本 · 本帧。底下一行给分母(11条脚本 · 定到包 1/11 —— 分母按全部脚本算,不随上面列出的行数变,所以"没定到名"这件事不会被藏起来)+ 口径 + 缺什么;一条能定位的都没有时整块收敛成一行带分母的说明:可疑脚本:0 条定到名(窗口内 11条脚本)· 要深录 + 一行"缺什么"
可疑脚本的三档口径① 直接测量:函数名与毫秒来自 LoAF 自己(sourceFunctionName / duration);② 归因推断:包名 / 文件路径 / 行号是由 URL 与 sourcemap 分段表推出来的(悬停看这一行属于哪一档:直接读出来 / 分段推断 / 不是插件脚本 / 解析失败 / 还没到位);③ 尚无法确认:原始源码(TS/TSX)行号、调用栈、函数级耗时 —— 都拿不到。⚠️ 行号是"脚本文本自己的行号"(单包 URL = 那个文件;合并批 = 合并后的大文件),不是原始源码行 —— 要原始源码行得解 section 内层 mappings(VLQ),本项目没做。所以这一层能缩小到"哪个包的哪个文件哪一段",不能直接指出根因。深录才拿得到文件与行号:默认(不插桩)时合并批的包名定不出来(精确归因关着),那一块通常就是"要深录";函数名经常是"未记录" —— 合并/压缩后的脚本里 LoAF 常常不给 sourceFunctionName,面板照实写,不编。事故那一侧读的是"捕获时存下来的定位":面板展示与「复制摘要」的证据行由同一个函数生成(同一份口径),所以不会出现"面板上有、复制出来没有";只有老事故包(加 file / line / lineVia / invoker 之前抓的)缺字段时才回退现算 —— 现算依赖当前会话的精确归因与内存里的脚本文本 / 分段缓存,刷新后两者都是空的,所以新包必须读存下来的那份,否则历史事故的定位会丢
本帧 / 窗口(两个小标题)「本帧」那一组 = 上面那张饼的同一帧;「窗口」那一行标题自带分母(条数 / 跨度 / 平均每帧脚本),它下面三行是最近 200 条 LoAF 的累计。同一个口径、两个分母,不能互相印证
脚本细拆脚本耗时拆成 纯执行 / 强制样式布局 / 暂停;三者相加 = 脚本耗时(分区,不是另加);为 0 的两项不显示
按包(本帧 / 窗口各一行)脚本耗时按包汇总(与事故包的 incident.packages 同一条还原路:单包 URL 直接读,合并批按 sourcemap 分段还原)。「本帧」= 上面那张饼的同一帧,「窗口」= 最近 200 条 LoAF 的聚合。没开深录(精确归因)时,合并批只能给到那个 58 包的批 URL —— 这是刻意的,不硬猜。只覆盖定得到包名的脚本,合计 ≤ 脚本耗时。本帧与窗口用的是两条不同的 hint(本帧那条只说这一帧,不再误写"窗口")
窗口那一行标题窗口:N 条长动画帧 · 跨 T · 平均每帧脚本 Xms。窗口那三行是累计(200 条长帧的脚本耗时加总),不是任何一帧的时长;跨度越久累计越大,别读成"越来越慢"。只有平均每帧那一栏和上面的饼可比(除数是帧数,不是脚本条数)
按来源(本帧 / 窗口各一行)脚本耗时按 LoAF 的 invoker 汇总:这条脚本是谁调起来的(setTimeout / rAF、事件处理器如 DOMWebSocket.onmessage、Promise 回调、脚本加载)。覆盖全部脚本,所以合计 = 脚本耗时,与「按包」的合计不相等是正常的。SCRIPT[…] 收敛成 script,带冒号的取最后一段(TimerHandler:setTimeout → setTimeout),缺失时退到 invokerType。它不回答"函数里面慢在哪" —— 那要 Chrome trace,页面内没有采样器
60 秒帧条(在「取证明细」最前面)铺满一行的块(约 365px 宽,一格约 6px),一格一秒,柱高 = 那一秒里最差那一帧的间隔;颜色与导出件 ASCII 帧条带逐条对应(≥100ms 红 / ≥50ms 黄 / 其余正常)。每格 hover 给墙钟时间 + 那一秒最差帧多少 + 点得动还是点不动:只有那一秒真的有长动画帧(LoAF 环里存在)的格子才可点,点了把翻帧切到那条帧;点不动的格子明确不给手型,title 里也说明。整格都是 rAF 挂起的那一秒画成最矮的灰柱 —— 那是间隙,不是卡顿
事故回放(在「取证明细」里,紧跟实时帧条)把选中的那条事故当时那一段窗口画成一条帧条,标签写明是哪一条、跨度多大(例:事故回放 · i84 · 13秒)。数据全部来自事故包(incident.frames / incident.loaf / summary),所以刷新生效、零新增采集成本;它与上面那条实时的 60 秒帧条是刻意分开的两件事。横轴按时间放:整段 rAF 被挂起的那几秒留白(那是"没在测",不是没卡),档位与实时帧条用同一个函数 sparkLevel、同一组 theme token,两处读数不许打架。下面三行:范围(窗口跨度 / 帧数 / p50 / p99 / 最差帧 / 缺口数)、最差帧的时间去向(复用帧内四段分解:脚本 / 任务 / 渲染 / 样式+布局+绘制,相加 = 这一帧时长)、口径脚注(这是捕获时那一段,不代表现在)。⚠️ 不排帧内执行顺序 —— LoAF 不给每帧内脚本的起止时间;逐帧明细仍走「导出事故包」+ tools/inspect-incident.mjs。它在「取证明细」组里,组折叠时不计算;历史事故跨会话读回后依然能回放(数据本来就在包里)
记录器自身每帧给这个 HUD 自己的 rAF 回调计时,给最近 600 帧的 p99 / 峰 / 样本数。测量工具必须自证没有明显改变被测对象 —— 它是"这一层记录器贵不贵",不是页面的开销
回调归因(耗时·下界)这一块总在首屏:没开「深录」时只有一行 —— 标签 回调归因(耗时·下界)、值 要深录(Top3 与「其余 (N)」都不渲染,也不写 0ms/s);插桩之后给 x/s · 峰 y/s(EMA α=0.4 平滑的执行速率 / 单秒最大执行次数),此时 0ms/s 才是真的(确实没有回调跑过),hover 给该秒同步耗时 ms/s 与累计 N× / Xms。覆盖范围是下界:只含 setTimeout / setInterval / rAF 三次调度的回调,事件处理器 / Promise / 微任务 / React 自身调度不计入
DOM 写入(在「DOM 与堆」组)精确总次数 + 每个写入者的 ≈ 估算(每 10 次采样一次栈,命中按 10 折算);没开深录不显示(计数恒为 0,会被读成"没有开销")。本 HUD 自身的写入不记账。它从「取证明细」挪到了「DOM 与堆」:写入者属于 DOM,不属于"这一帧的时间去哪了"
宿主 event-loopperf_hooks.monitorEventLoopDelay({resolution: 1}),每秒 p50 / p99 / max 三个数都给 —— 只给 p99 会把偶发的秒级停顿藏掉(max 才是"最坏卡了多久");宿主半每秒采一次、ring 保留 300 秒。老版本宿主半没有 max 字段时,如实只给 p50/p99
宿主事件到达宿主用公开 ctx.on('session/event') 记「事件提交(event.time)→ 监听器被调用」的延迟,不是监听器的 CPU 耗时
链路 RTT每 10 秒一次 1KB 同源探测(带 cache-buster),吞吐每 60 秒一次 64KB。p99 > 300ms 且 FPS 正常且 n ≥ 2 才判「疑似链路」