Replies: 5 comments
|
根因定位我独立复核过,三条都成立。但修法这一层有一个会踩坑的地方,#6081 的提议如果被当成性能修复采纳,会把"慢"换成"桌面端不启动"。下面是我这边读码得到的边界与两条独立杠杆。 独立复核
★ 为什么"加回 inject"不能当根修桌面端根本没有 所以:
★ 正确的根修:让初始扫描等激活稳定,而不是绑回某个兄弟服务仓库里已有现成信号和现成先例:
把初始扫描从"构造即跑"改成"loader 激活稳定后再跑",启动期 compose 从 8 次降到 1–2 次(稳态那 1 次仍属必要,它是幂等收敛)。两点必须一并处理:
★ 第二个独立杠杆:
|
|
@argszero 感谢这段复核 —— 桌面宿主那条我独立验证确认了,而且它纠正了我此前的一处表述。 1) 桌面端没有 webServer:确认,并补一个易误读点 2) 接受对 #6081 那条交叉引用的边界修正 3) 你的修法 (1):两条实测支撑,以及对你两个前提的回应
4) "不要用时序 hack 绕过":完全同意,并想补一个可 review 的边界 5) 杠杆 (2):同意正交,而且是乘数不是替代 6) 你对构造期那次 compose 的提醒同样成立 7) 你没条件复测计时,我这边可以补齐条件 |
|
核实结论:属实,机制逐行成立。
边界:「8 次」与 2.7s→17s 是实测,我未独立复测;源码只能证实「每次 flush 全量重组合、无去抖无合并」,不能反推精确次数。修法方向同意:初始扫描等 loader 激活稳定(锚 loader fiber)+ compose 派生产物按 rev 缓存(正交杠杆)。 |
|
@PerryLink 感谢逐行核对 —— 你引的每一处行号我都对着 你那句"源码只能证实『每次 flush 全量重组合、无去抖无合并』,不能反推精确次数"——完全同意,而且可以再收紧一层:这个次数本身不是常量,它是组合的函数。
所以"从源码推次数"不只是不可靠,而是连一个确定的数都不存在 —— 它由激活波次决定。这反过来支持同一结论:修法必须是结构性的(把初始扫描推迟到激活稳定,或把 flush 去抖合并),而不是去调一个常数。(次数与耗时都由包住全局 目前这条线只剩一个开放的设计问题:推迟之后"未就绪窗口"的语义。 |
|
补一份直接计数的独立佐证——不是从 flush 耗时反推,而是用 ESM 加载钩子把 环境:Windows 11,Node v24.15.0, → 本机是 10 次(比你那台多两次),符合你「次数是组合的函数、不是常量」的判断:这台机器启用组合更大。10 次合计 ≈5.2s,占该配置启动 8.0s 的约 65%。 两点补充:
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
TL;DR
dsh web启动变慢(本机 2.7s → 17s)的根因已定位到一个提交:旧写法是一道隐形的延迟闸门:注册表必须等
webServer就绪才构造,而 Web server 在插件树里较晚就绪,于是整棵树只在激活波次结束后 compose 一次。改成只依赖
loader后,注册表随loader立即构造,此后每一波插件激活都触发一次同步全量重组合。
同机、同仪器实测:
client-modules/lib/index.js最后一行是单文件隔离实验:客户端图仍是 12.6 MB、其他包一个没动,只换这一个文件,启动即 17.3s → 4.28s。
客户端图确实也变大了(0.1.5 官方客户端包 39 → 45,新增的
ui-sidebar-documentpreview单包 6.7 MB),但它只解释约 2.4×;剩余的量级来自 compose 次数 1 → 8。
1. 测量方法(可复现、不改动真实环境)
@deepseek-ai/dsh@0.1.5-rc.1(npmlatest),web profile 9 个 bundle(官方 base/web-app + 7 个第三方)。
profiles用 junction 指向真实 profile,sessions/storages为空目录,全程不在真实 home 上跑第二个实例(避免与正在运行的实例争会话/存储)。
@deepseek-ai/dsh@0.1.2-rc.1本体到独立目录,以它自己作为INSTALL_ANCHOR+ 全新 DSH_HOME(启动器、模块回退闭包、client-modules、peer 全是 0.1.2-rc.1,无混血),再装上 09-08 那套插件(
@liustack/modlens@3.25.4、@liustack/modsearch@5.10.1、dsh-better-sidebar@0.18.0、dshmarket@1.41.0、@wingsky-1/dsh-notifier@0.2.1、dsh-opencode-session-header@0.1.0)——2.69s 复现了此前的 2.9s 记录。--import预加载包住全局queueMicrotask,统计每次 flush 的耗时(≥200ms 记为一次重组合)。旧系统 1 次 / 新系统 8 次,分界清晰。
并报中位数,不跨时段比绝对值。
2. 关键数据
2.1 逐版本边界(已发布 npm tarball + 对应源码 ref 双向核对)
static inject['webServer', 'loader']['webServer', 'loader']19444907(08-28)['loader']['loader']→ 首次进入发布的版本是 0.1.5-alpha.1;0.1.3-alpha.2 及以前都没有。
2.2 该提交的精确改动
同一提交把双语 README 从"绑定 Web server"改写为"载体无关":
"an available Web carrier serves each bundle over
/plugins, and a shell-owned carrier can readthe same graph and bundle paths directly";"A Web carrier renders those rows into its index response;
a shell-owned carrier can render the same rows without a Web server."
2.3 CPU profile(0.1.5-rc.1,17s 采样窗口)
@deepseek-ai/dsh-client-modules自耗时合计 ≈9.3s,占启动忙时约 65%:buildCombo3.15s /newlineCount2.45s /utf8Write2.04s /identitySectionMap1.71s;官方包自身与第三方插件的
apply自耗时合计不到 100ms。2.4 逐项排除
补充:
better-sidebar单独开关的交错 A/B 为 13.9s vs 17.3s(Δ=3.4s),机制是它既让每遍多 879KB,又多买了一整次重组合(8 次 vs 7 次)。
3. 机制:为什么一个
inject会放大 8 倍compose()是同步的,现状下单次约 1.6s,阻塞事件循环。loader就绪时即构造;此后每次internal/plugin(fiber 构造/销毁)都会dirty.add(name)+queueMicrotask→flush()→ 只要该批有任何变化就this.compose()(全图全量重算)。这解释了为什么字节只涨 3.7 倍、compose 耗时却涨约 32 倍。
webServer这个"必需依赖"事实上充当了"延迟到激活波次结束"的闸门;本次为 Electron/壳载体把它变可选时,没有补替代的延迟机制。
4. 建议修法(保留载体无关设计的前提下)
loader.await()(激活静默)后再做首次 compose;或把微任务 flush 去抖为"波次结束只 compose 一次";或让 compose 惰性化到首次请求
/plugins。这样 README 里 "the shell boots it before any plugin runs" 的诉求也能满足 —— 构造期只注册载体与路由,
组合推迟到激活静默。
newlineCount改indexOf扫描;identity map 不嵌
sourcesContent(或.map完全按需);Buffer.concat一次;去掉"批量 combo + 逐条 combo"双份产物。
optimizeDeps(node_modules/.vite+ lockfile/配置失效判定)或webpack
cache: { type: 'filesystem' },让 12.6MB 的拼装每个版本只做一次。避免单个插件(如
ui-sidebar-documentpreview,6.7 MB 内联 PDF.js)成为启动路径上的固定成本。方向上与 #6081 提出的"给 Web bundle 的
modules行补回webServer激活边"一致 ——那个方案解决的是服务访问竞态(
webServer without inject),同样也是本次变更的连带影响;两者的共同点是:
modules行的激活时机需要被显式治理,而不是隐含在包内静态 inject 里。5. 复现
仪器与脚本(Windows / pwsh + Node):
如需脚本原文我可以在回复里贴出(
boot-probe.mjs/probe-microtask.mjs/ab-boot.mjs/split-boot.mjs/analyze-cpuprofile.mjs,均为无依赖的独立脚本)。6. 方法与局限(如实说明)
但 2.69s 与此前记录的 2.9s 吻合,且 compose 次数 1 次与代码路径一致。
better-sidebar的 A/B 来自真实配置开关;其余对照均用--patch覆盖层完成,不改动用户配置。exports['./client']指向的产物"统计;图内实际条目以__DSH_BOOT__为准(本次未直接读取该变量)。
相关讨论
English summary
Root cause of the slow
dsh webstartup (2.7s → 17s) is one commit:19444907"feat: electron 打包" (2026-08-28), which changedClientModuleRegistry'sstatic injectfrom['webServer', 'loader']to['loader']inpackages/client/modules/src/index.ts(so a shell-owned carrier can run without a Web server — a legitimate goal).
That removal also deleted an accidental gate: the registry used to be constructed only after
webServerexisted, i.e. after the Loader's activation waves had finished, so the whole graph wascomposed once. Now it is constructed as soon as
loaderexists, and every subsequent pluginactivation triggers a synchronous full recomposition.
Measured on the same machine with the same instruments:
client-modules/lib/index.jsrevertedThe 3.7× larger client graph (0.1.5 added 6 client packages;
ui-sidebar-documentpreviewalone is6.7 MB of inlined PDF.js) explains only ~2.4×; the rest is the 1 → 8 recomposition count.
Because
compose()is synchronous and takes ~1.6s, it blocks the event loop, pushing later activationsinto further waves — a positive feedback loop.
Suggested fix: keep the carrier-agnostic design but restore deferral/coalescing (first compose after
loader.await(), or debounce the microtask flush to one compose per activation wave, or compose lazilyon the first
/pluginsrequest). Consistent with #6196 and #6369. Boundary note: #6081's activation edge is Web-composition-only and is not the root fix — see above.All reactions