Repository navigation
perf: 发布体积 -33%,长会话报告 -28%,图片编码按变体去重 - #95
Conversation
The tarball carried lib/index.js.map and lib/client.js.map: 1.63 MiB raw, 32.9% of the unpacked payload. Nothing reads them: lib/client.js is a CJS bundle the webview executes, and scripts/verify-engine-load.mjs audits import/require statements rather than comments, so the `//# sourceMappingURL` trailer is inert. `files` now excludes them, and they are untracked as well. They are the one tsdown output that does not rebuild byte-identically on this checkout -- lib/index.js, lib/client.js and lib/index.d.ts all reproduce exactly -- so tracking them only gives the CI `git diff --exit-code -- lib/` guard a platform-dependent failure. Local builds still emit them, so debugging keeps its maps. npm pack --dry-run: tarball 2,948,915 -> 2,479,266 B (-15.9%), unpacked 5,188,371 -> 3,480,662 B (-32.9%), 18 -> 16 files.
Two shapes re-scanned the whole selection per model: * the per-model totals filtered `selected` once per distinct model, so a 5,000-fact report made 96 full passes to build subtotals that a single bucketing pass produces; * the scenario matrix resolved the candidate's price row with a Map lookup for every (fact, catalog model) pair — 480,000 lookups for the same 96 rows. The selection is now bucketed by model once, and `priceRequest`'s arithmetic moved into `cost-facts.requestCost`, which takes an already-resolved row so the scenario loop can hoist the lookup out of its inner loop. The arithmetic still lives in exactly one place. `requestRates` gained an optional peak-hour memo: `peakHour` constructs a Date per call and the answer is a pure function of `at` and this one table, so a report shares one memo across all of its pricing questions. Measured on node 22 with 96 catalog models (throwaway harness, not committed): 500 facts 4.65 -> 3.84 ms 2,000 facts 18.19 -> 12.35 ms 5,000 facts 42.17 -> 30.36 ms Output is unchanged: the bucket order matches the previous `Set` order and the sort was already a total order over unique ids.
`readImageRequest()` returns one cached codec object per (attachment, encoder
target) and `requestImages()` reuses that object for every occurrence, but a
history that carries an image also replays it in every later message. Each of
the three wire codecs base64'd the buffer per occurrence, so the same encoded
bytes were re-derived once per replay — `Buffer.from(...).toString('base64')`
measured 0.738 ms for a 1 MiB payload and 18.66 ms for 24 MiB, against ~21 ns
for the memoised lookup that now replaces it, and an image can legitimately
appear several times in one message.
The three codecs share one WeakMap keyed on the codec object, so the copy's
lifetime follows the harness's own version cache instead of this plugin's, and
the data URL is memoised alongside the raw base64 because building it also
copies the whole payload.
Six declarations carried `export` with no reachable consumer: AccountRotationContext, CommandCodeAccount, CommandCodeUsage, CommandCodePlan, MODELS_TIMEOUT_MS, PROTOCOL_CACHE_TTL_MS None is re-exported by src/index.ts, and the package's `exports` map offers no deep-import path, so a module was the only place they could be named from — and in every case the only reference is in that same file. The emitted bundle already agreed: none of the six is an export in lib/index.d.ts, five survive there only as non-exported declarations that the public `CommandCodeUsageReport` and `CommandCodeAdapterDeps` shapes need, and the two constants do not appear at all. `git diff` over the rebuilt lib/index.d.ts adds and removes no `export` line, so the published type surface is unchanged. The JSDoc now says which public shape each internal type is reached through, which is what a reader needs instead of an `export` that promises more than it delivers. Three siblings were left exported on purpose: `SurfaceImagePolicy`, `headersTimeoutAdvice` and `AnalyticsEvent` are all used by production code *and* named by tests, so their export is a real seam rather than dead weight.
`tests/analytics.test.ts` ended its browser-controller test with `assert.equal(emptyMetrics().requests, 0)`. That checks a literal initializer against itself and says nothing about the controller the test is named for. It was also the only consumer of that factory outside `src/analytics.ts` — which is the only reason the factory was exported. `emptyMetrics` itself stays: `aggregate` builds every accumulator with it. It loses the `export`, so the module surface now matches its real audience. The rebuilt lib/index.js gains exactly one line for this change — the doc comment; the name was never in the bundle's export list. That closes the in-scope inventory for "code only tests use": nothing in src/analytics.ts, src/cost-facts.ts or src/adapter.ts has a consumer that is exclusively a test.
…indows
The memo added with the single-pass report was called `peakHours`, which is also
the name of `CommandCodePriceTable.peakHours` — the `[start, end]` window list.
In `requestRates` the two sit two lines apart:
let peak = peakHours?.get(at)
peak = peakHour(at, table.peakHours)
so the collision is read before it is understood. `peakHourCache` says which one
is the memo. No behaviour change: same values, same lookups.
PR #95 把两份 sourcemap 移出发布包并取消跟踪,这部分独立成立:没有任何读取方, 本地构建照旧产出。但它给出的第二条理由——「map 是 tsdown 产物里唯一无法逐字节 重建的」——把 CRLF 污染误归为平台固有。实测:在 /tmp 干净目录用 git archive main 展开后重建,两份 map 与提交里的逐字节相同(SHA-256 8f94557d… / 5ca1b0aa…), 作者本机 28,584 / 23,464 字节的差值约等于内嵌源码行数的 2 倍,根因是仓库从未有 .gitattributes。 补上 * text=auto eol=lf(图片与压缩包按 binary 排除),红绿对照成立:无该文件时 core.autocrlf=true 检出后 src/adapter.ts 有 3,482 行 CRLF、lib/index.js 有 11,817 行,重建后 git diff --exit-code -- lib/ 报出 lib/client.js.map 差异;加上后两者均 为 0,守卫退出码 0。加入时已跟踪文件 100% 为 LF,git add --renormalize . 未改写 任何文件。 合并后验证:typecheck 退出码 0、npm test 1056/1056、build 两段、git diff --exit-code -- lib/ 退出码 0、test:engine 2/2、test:pack 16 文件、npm pack --dry-run 2.5 MB / 解包 3.5 MB(与文档所述一致)。 文档:CHANGELOG 新建 [Unreleased] 段;《决策记录》第八章第 15 条记录更正后的判据; 《对接说明》第四章第 4 条并标注 rc.2 轮次的「18 个文件」为历史值;《提交历史》总览; 仓库地图补 .gitattributes。
|
已合并( 更正:map 不可逐字节重建不是平台固有,是 CRLF 污染PR 描述里写「它们是 tsdown 产物里唯一在本机无法逐字节重建的」,并据此把不可重建归为跨平台差异。我在 macOS 上做了对照:
这个倍数关系指向 CRLF: 已补的修复新增
也就是说,PR 描述里那个「Windows 上 map 重建不一致」的症状在本机完整复现了,加上属性文件后消失。加入时已跟踪文件 100% 是 LF, 复核记录合并前我在本地 worktree 独立跑了全套,不是照抄描述:
另外两项独立验证:
关于「顺带发现」三条我都核实了,结论与你一致,本轮未越界处理:
|
为什么:用户要求「提交并发布新版本」。版本号取 patch——三批改动均为非破坏性的 性能与界面调整,无新功能、公开导出面零变化(lib/index.d.ts 的 export 列表逐字 未改),沿 0.13.4「设置页信息架构调整随 patch 发布」先例。 - 语义化版本 0.14.2 → 0.14.3(package.json 与 package-lock.json 两处同步) - CHANGELOG:[Unreleased] 定版为 [0.14.3] - 2026-10-19,含 PR #95 的三条性能 改动、.gitattributes、以及本轮两处界面调整 - lib/ 重建:lib/client.js 内嵌版本常量 0.14.3,分析入口已移入「账户用量」标题行 - 发布前检查全通过:锁文件隔离校验(npm ci --dry-run)、typecheck、测试 1056/1056、 构建两段、test:engine 2/2(0.2.0-rc.2 与 0.2.1-rc.2)、test:install 2/2 (pnpm 10.34.5)、test:pack(16 文件,版本 0.14.3)、npm pack --dry-run (16 文件,2.5 MB / 解包 3.5 MB) - 环境问题一处:npm 因 ~/.npm 的 root 属主文件报 EPERM(与 10-04、10-08、10-14、 10-18 同因),全程用 npm_config_cache=$PWD/.npm-cache 绕过,未做权限根治
本次改动集中在性能:发布包体积、长会话报告聚合、以及图片请求路径上的重复编码。全部改动都保持用户侧兼容性不变(详见「兼容性」一节),并包含重建后的
lib/产物。性能提升总览
一、发布体积:
sourcemap不再随包发布tsdown.config.ts的sourcemap: true会产出lib/index.js.map与lib/client.js.map,而files直接收整个lib,于是两份 map 进了 tarball:1.63 MiB 原始字节,占解包体积的 32.9%。它们没有任何读取方:
lib/client.js是 WebView 直接执行的 CJS 包,//# sourceMappingURL只是尾部注释;scripts/verify-engine-load.mjs的审计用正则匹配import/require语句,不匹配注释,少一份 map 照样通过;scripts/verify-package-exports.mjs只校验声明入口在包内存在,不断言.map。改动:
files增加"!lib/**/*.map",两份 map 同时从 git 取消跟踪。本地npm run build照旧产出 map,调试能力不受影响。取消跟踪还有第二个理由:它们是 tsdown 产物里唯一在本机无法逐字节重建的。同一棵源码树上,
lib/index.js、lib/client.js、lib/index.d.ts都能逐字节重现,两份 map 却不能(本机 919,315/783,625,而提交里是 890,731/760,161)。把它们留在版本控制里,等于让 CI 的git diff --exit-code -- lib/(注释里写明是「stale bundle」守卫)依赖平台相关的字节。取消跟踪并移出 tarball 后,该守卫在本机稳定退出 0。npm pack --dry-run:18 → 16 个文件。二、报告聚合:从两趟扫描降到一趟
buildAnalytics里有两处对「事实 × 模型」的重复扫描:[...new Set(selected.map(f => f.model))].map(id => aggregate(selected.filter(f => f.model === id), ...))。5,000 条事实、96 个模型时要整表过滤 96 次,只为得到一次分桶就能拿到的结果。改动:
analytics.ts搬到cost-facts.requestCost(tokens, price, ...),接收已解析好的价格行,于是场景循环把查价提到内层循环之外,算式仍只存在一处;requestRates增加可选的高峰时段 memo:peakHour()每次调用都要new Date()并读三个字段,而答案只是at与该表的纯函数,报告因此可以复用同一份 memo(memo 由调用方持有,不可能活得比它的表更久)。测量(node 22,96 个目录模型,一次性脚本,未入库):
输出逐位不变:分桶顺序与原
Set顺序一致,排序本来就是按唯一 id 的全序。三、图片:同一变体只编码一次
readImageRequest()对每个(附件,编码目标)返回同一个缓存对象,requestImages()在一次生成内也复用这个对象——但一条请求里同一张图会出现在多条历史消息中。三个 wire 编解码器原先各自按「出现次数」执行Buffer.from(version.data).toString('base64'):改动:三个编解码器共用一个以该缓存对象为键的
WeakMap,保存原始 base64 与 data URL(拼接同样要整份拷贝)。副本生命周期跟随宿主自己的版本缓存,而不是插件。命中约 21 ns。兼容性
lib/index.d.ts的export { ... }一行未改;去导出前后的git diff里没有一行含export(见下节的清理说明)。dsh.bundle清单形状、exports["./client"]、dsh.client.inject、dsh.client.platform均未动。engines.dsh、dsh.compatibility.dshReleases、peer 范围均未动。/alpha/generate、Provider API/provider/v1/chat/completions、Messages 的请求形状与内容完全一致(测试覆盖)。name=llm-commandcode、inject=["llm"]、Config/apply齐全,导出清单与改动前一致。验证
npm run typecheck:退出 0。npm test:1040/1056。失败的 16 条全部是本机既有的web-search/settings 环境性失败;我把失败测试名去掉耗时后缀后与main基线做了集合比对,逐条相同(基线、性能改动后、清理后三次运行一致)。npm run build:两段Build complete。git diff --exit-code -- lib/:退出 0(产物与源码一致,lib/已随每个提交重建)。lib/产物,逐提交 checkout 也能过 CI 守卫。npm run test:engine与npm run test:pack都以spawnSync('npm.cmd')且未开shell启动,在 Windows/Node 24 上直接EINVAL(scripts/verify-engine-load.mjs:124、scripts/verify-package-exports.mjs:28),与本次改动无关。作为替代,我把打包后的插件放进真实引擎(@deepseek-ai/dsh0.2.1-rc.2)的 peer 树中导入验证,如上节所述。附带的代码清理(3 个提交,不属于性能)
在同一批路径内顺手做了非性能清理,均可单独审视:
refactor(adapter):6 个export没有任何可达的消费者(AccountRotationContext、CommandCodeAccount、CommandCodeUsage、CommandCodePlan、MODELS_TIMEOUT_MS、PROTOCOL_CACHE_TTL_MS)——src/index.ts未转出,exports表也不给深导入路径,引用只在本文件内。产物其实早已「不同意」它们:没有一个是lib/index.d.ts的导出,其中 5 个只作为公共形状所需的非导出声明存在,两个常量完全不出现。刻意保留SurfaceImagePolicy、headersTimeoutAdvice、AnalyticsEvent:它们既被生产代码使用也被测试具名引用,是真实接缝。test(analytics):删掉一条挂在控制器测试末尾的assert.equal(emptyMetrics().requests, 0)——它拿零初始化字面量断言自己,与被测对象无关,且是该工厂在模块外唯一的消费者。工厂保留(aggregate每次都用),收掉export。refactor(analytics):把我这次新增的高峰时段 memo 从peakHours改名为peakHourCache。原名字与CommandCodePriceTable.peakHours(窗口列表)同名,而两者在requestRates里只隔两行。顺带发现(未改动,供参考)
src/client/panel.ts的resetPanelAutoRefresh()其 JSDoc 自述「Exported for tests only」,生产侧零调用,仅tests/panel.test.ts调用 6 次;生产通过startPanelAutoRefresh返回的 disposer 释放。按要求本次未越出改动范围,如需处理可以另开一轮。src/analytics.ts与src/cost-projection.ts有一段逐字重复的守卫代码(record、tokensOf,以及只差名字的数值守卫number/finite),合理归属是src/cost-facts.ts。src/login.ts的buildCommandAuthUrl注释写着「exported for tests」,但它其实被生产调用并公开导出,注释已过时。如何复核