Skip to content

docs(plans): record why CI test time is bound by module import, not scheduling - #10909

Open
yiliang114 wants to merge 5 commits into
mainfrom
docs/test-suite-barrel-cost
Open

docs(plans): record why CI test time is bound by module import, not scheduling#10909
yiliang114 wants to merge 5 commits into
mainfrom
docs/test-suite-barrel-cost

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a plan document recording where release-run test time actually goes, what causes it, and a phased way out with its projected payoff and open risks. Documentation only — no code, config or workflow changes.

Why it's needed

Release runs spend more time importing modules than running assertions: in run 33713579913 the cli workspace reported 2223s of collect against 1372s of tests, and core reported 546s against 251s. The three shards finished within a minute of each other, so the sharding is already balanced and adding shards only divides a fixed per-file cost.

That cost is the core barrel. Every import from the package root evaluates a closure of roughly 612 modules regardless of how little it uses, and about 70% of cli source files import it that way. Rewriting a file's import to the defining module drops it from ~11.5s to ~2.1s, and the suites that already replace the barrel with a mock factory have always run at ~1.9s — two independent paths landing on the same floor.

This matters because test files grew 87% in ten weeks. Sharding buys a constant factor that gets consumed in about fifteen weeks at that rate, which is why the last month has produced a steady stream of timeout, retry and concurrency adjustments. Lowering the per-file cost, and adding a lint rule so it cannot come back, is the part that keeps working as the suite grows.

Reviewer Test Plan

How to verify

The claims are checkable without running anything locally. The workspace timings come from the release run's job logs, which can be read with gh api /repos/QwenLM/qwen-code/actions/jobs/<id>/logs --allow-escape-sequences and grepped for the per-workspace Duration (… collect …, tests …) lines. The import-cost figures were measured on a development machine with a single worker and coverage disabled, so the ratios hold but the absolute seconds should not be extrapolated to CI hardware — the document says so where those numbers appear.

What is worth a reviewer's judgement is the section on risks, particularly the two that would decide how a migration is written: whether subpath imports should point at core's sources or its build output, and how the existing barrel mocks are handled. Both are recorded as unresolved rather than settled.

Evidence (Before & After)

N/A — documentation only.

Tested on

OS Status
🍏 macOS N/A
🪟 Windows N/A
🐧 Linux N/A

Risk & Scope

  • Main risk or tradeoff: none to the product; this adds a file under docs/plans/ and changes no behaviour. The risk is that the plan is acted on before its two open questions are answered, which is why they are called out in the document rather than buried.
  • Not validated / out of scope: the projected timings are a component-cost model over a single release run, roughly ±20%. The bundle-size figures come from isolated esbuild runs on synthetic entry points and do not represent the real bundle. No migration work is included here.
  • Breaking changes / migration notes: none.

Linked Issues

Refs #10908

中文说明

这个 PR 做了什么

新增一份计划文档,记录 release run 的测试时间到底花在哪、根因是什么,以及分阶段的解决路径、预期收益和未决风险。纯文档,不涉及任何代码、配置或 workflow 改动。

为什么需要

Release run 里导入模块的时间比跑断言还长:run 33713579913 中 cli 的 collect 是 2223s、tests 只有 1372s,core 是 546s 对 251s。三个分片耗时相差不到一分钟,说明分片本身已经是均衡的,继续加分片只能摊薄一个固定的单文件成本。

这个成本来自 core 的 barrel。任何从包根导入的语句都会把约 612 个模块的闭包完整求值一遍,无论实际只用了多少,而约 70% 的 cli 源文件就是这么导入的。把某个文件的导入改成指向定义模块,耗时从 ~11.5s 降到 ~2.1s;而套件里本来就用 mock 工厂替换 barrel 的那些文件,一直是 ~1.9s —— 两条独立路径落在同一个下界上。

这件事之所以要紧,是因为测试文件数十周涨了 87%。按这个速度,分片买来的常数因子约十五周就被吃完,这也解释了过去一个月为什么持续在调超时、重试和并发。降低单文件成本、再加一条 lint 规则防止它回来,才是随套件增长仍然有效的部分。

审查者验证计划

如何验证

文中结论不需要在本地跑任何东西就能核对。各 workspace 的耗时来自 release run 的 job 日志,用 gh api /repos/QwenLM/qwen-code/actions/jobs/<id>/logs --allow-escape-sequences 取出后,grep 每个 workspace 的 Duration (… collect …, tests …) 行即可。导入成本的数字是在一台开发机上以单 worker、关闭 coverage 测得的,因此倍数关系成立、绝对秒数不应外推到 CI 机器 —— 文档在引用这些数字的地方都做了说明。

真正需要审查者判断的是风险一节,尤其是决定迁移写法的那两条:subpath 导入应该指向 core 的源码还是构建产物,以及现有的 barrel mock 如何处理。这两条都按「未决」记录,而不是当作已有定论。

证据(前后对比)

N/A —— 纯文档。

风险与范围

  • 主要风险或权衡:对产品没有风险,只是在 docs/plans/ 下新增一个文件,不改变任何行为。真正的风险是在两个未决问题被回答之前就照着计划动手,所以它们被写在文档显眼处而不是藏在正文里。
  • 未验证 / 超出范围:预估耗时是基于单次 release run 的组件成本模型,误差约 ±20%。bundle 体积数据来自对合成入口的独立 esbuild 运行,不代表真实构建产物。本 PR 不包含任何迁移改动。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Refs #10908

…cheduling

Release runs spend more time importing modules than running assertions: in
run 33713579913 the cli workspace reported 2223s of collect against 1372s of
tests, and the three shards finished within a minute of each other, so the
sharding itself is balanced and adding shards only divides a fixed unit cost.

The unit cost is the core barrel. Every `import { X } from
'@qwen-code/qwen-code-core'` evaluates roughly 612 modules regardless of how
little it uses, and about 70% of cli sources import it that way. Two
independent paths converge on the same floor: rewriting an import to the
defining module drops a file from ~11.5s to ~2.1s, and the suites that
already replace the barrel with a mock factory have always run at ~1.9s.

This plan records the measurements, the phased remediation and its projected
payoff, and the open risks — chiefly that mixing resolution forms would put
two copies of a module in the bundle, that the barrel currently resolves to
core sources rather than dist for reasons not yet established, and that the
existing barrel mocks would fail silently if their targets are not migrated
alongside the code under test.
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

  • Template: complete ✓ — all sections filled in, docs-appropriate N/A evidence.
  • Problem: observed, not theoretical. I pulled the shard 1/3 job log from release run 33713579913 myself: cli reports collect 2223.78s vs tests 1372.60s, core 545.75s vs 250.59s — import time genuinely exceeds assertion time, exactly as recorded. Backed by open issue CI test time is bound by module import cost, not scheduling #10908 (P2, performance).
  • Direction: aligned — docs/plans/ is the designated home for implementation plans, and this records the analysis behind an open P2 performance issue. The doc is also careful about what it does not know (§6.2, §6.3), which is the right posture for a plan others will act on. CHANGELOG: no direct reference, but the area (CI test cost) is what the last month of merged PRs has been circling.
  • Size: N/A — docs only, no core paths, zero production lines.
  • Approach: minimal — one new 270-line plan document, zero code/config/workflow changes. Scope matches the title exactly; nothing to split out.
  • Risk: no elevated risk signals (no high-risk paths touched).

Moving on to code review. 🔍

中文说明

感谢贡献!

  • 模板:完整 ✓ —— 各节齐全,纯文档场景的证据按规范填了 N/A
  • 问题:已观测到,不是理论推测。我自行拉取了 release run 33713579913 分片 1/3 的 job 日志:cli collect 2223.78s vs tests 1372.60s,core 545.75s vs 250.59s —— 导入时间确实比跑断言还长,与文档记录一致。且有 open issue CI test time is bound by module import cost, not scheduling #10908(P2,性能)背书。
  • 方向:对齐 —— docs/plans/ 是实现计划的指定目录,本文档记录的正是这个 open P2 性能问题的分析。文档对未知项(§6.2、§6.3)也如实标注,这是计划文档该有的姿态。CHANGELOG:无直接引用,但该领域(CI 测试成本)正是近一个月合并 PR 持续围绕的主题。
  • 规模:不适用 —— 纯文档,不触及核心路径,无生产代码改动。
  • 方案:最小化 —— 只新增一个 270 行计划文档,零代码/配置/workflow 改动,范围与标题完全一致,无可拆分内容。
  • 风险:无升级风险信号(未触及高风险路径)。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at c3a421a65bbf77cdebf02b68d83e0abdc9fefc76 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

Independent baseline first: for "record why CI test time is bound by module import, not scheduling", I'd write a dated docs/plans/ doc carrying measured data, a controlled experiment isolating the cause, a phased plan with projected payoff, and explicit open questions. That is exactly what this is — plus a data-source confidence table and a timeout-history appendix, and both earn their place.

Spot checks against the code at the reviewed commit:

  • packages/core/index.ts is indeed export * from './src/index.js'
  • core's package.json carries the wildcard ./dist/* / ./src/* exports and named-subpath precedent; @qwen-code/qwen-code-core/memoryScopes is used in production (serve/run-qwen-serve.ts) ✓
  • The §1 shard-timing table matches the real release-run logs exactly — I re-pulled job 100569301411: cli 1671.15s wall / collect 2223.78s / tests 1372.60s; core 388.15s / 545.75s / 250.59s; web-shell 147.95s / 108.04s / 94.08s ✓

One non-blocking note:

No critical blockers, no convention violations. The local-machine timings are carried as ratios with the caveat that absolute seconds don't extrapolate to CI hardware — the doc says so itself where those numbers appear, which is the honest way to carry them.

Testing evidence

Docs-only change; nothing user-visible to drive in a terminal — real-scenario testing: N/A. The evidence carried here is the PR's own CI on the reviewed commit, read via the API:

Check Conclusion
Qwen Code CI ✅ success
Security Checks ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Dependency CVE audit ✅ success
Test (windows-latest / macos-latest, Node 22.x) ⏭️ skipped
web-shell E2E Smoke / Integration Tests (CLI, No Sandbox) ⏭️ skipped

Both pull_request-event runs are green; no failures anywhere. The windows/macos unit lanes and the e2e/integration lanes are skipped by CI's classify_prdocs_only profile — the expected shape for a docs-only change. On top of CI I re-read the release-run job log this doc is built on (run 33713579913, shard 1/3) and confirmed the §1 figures line for line; the per-file before/after timings (§4) come from the author's dev machine and are presented as ratios, which I did not independently re-run — not verified: absolute §4/§5 seconds, by design per the doc's own caveats.

中文说明

代码审查

先说独立基线:要记录"CI 测试时间受限于模块导入而非调度",我会写一篇带日期的 docs/plans/ 文档,包含实测数据、隔离根因的对照实验、带预期收益的分阶段计划和明确的未决项。这个 PR 正是这么做的 —— 还附了数据来源可信度表和超时参数沿革附录,两者都有存在价值。

按被审提交抽查代码:

  • packages/core/index.ts 确实是 export * from './src/index.js'
  • core 的 package.json 确有 ./dist/* / ./src/* 通配 exports 及具名 subpath 先例;@qwen-code/qwen-code-core/memoryScopes 在生产代码(serve/run-qwen-serve.ts)中使用 ✓
  • §1 分片耗时表与真实 release run 日志完全一致 —— 我重新拉取了 job 100569301411:cli 墙钟 1671.15s / collect 2223.78s / tests 1372.60s;core 388.15s / 545.75s / 250.59s;web-shell 147.95s / 108.04s / 94.08s ✓

一条非阻塞提示:

无关键阻塞项,无规范违规。本机实测耗时以比值呈现、并注明绝对秒数不可外推到 CI 硬件 —— 文档在引用这些数字的地方也是这么写的,这是诚实的处理方式。

测试证据

纯文档改动,无用户可见内容可在终端驱动 —— 真实场景测试:N/A。此处的证据是 PR 自身 CI 在被审提交上的结果(经 API 读取,见上表):两个 pull_request 触发的 run 均绿,无任何失败。windows/macos 单测通道与 e2e/集成通道由 CI 的 classify_prdocs_only 画像跳过 —— 纯文档改动的预期形态。除 CI 外,我还重新读取了本文档所依据的 release run 日志(run 33713579913 分片 1/3),§1 数字逐行核对一致;§4 的逐文件前后耗时来自作者开发机、以比值呈现,我未独立重跑 —— 未验证:§4/§5 的绝对秒数,按文档自身说明本就不可外推。

Qwen Code · qwen3.8-max

Reviewed at c3a421a65bbf77cdebf02b68d83e0abdc9fefc76 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — evidence independently verified against the real run logs; one non-blocking nit (§5's jsdom phase was already landed by #10890).

This is what a good plan document looks like. The diagnosis is built on measurements, not vibes: the collect-vs-tests split reproduces from the actual release-run logs, the root cause is isolated by two independent paths (deep imports vs barrel-mocked suites) converging on the same ~2s floor, and the doc is unusually honest about what it doesn't know — §6.2's barrel-resolution question is flagged as must-answer-before-acting rather than buried. The migration risks it leads with (§6.1 module identity in a bundled CLI, §6.3 vi.mock silently missing deep imports) are exactly the ones a reviewer would worry about.

Scope stayed minimal: one file, no code, no drive-by edits. In six months the confidence table and the "ratios yes, absolute seconds no" caveats are what will keep this document trustworthy instead of misleading.

The one reservation, named in the review: #10890 landed the jsdom opt-in this morning, so phase ③ in the §5 table is no longer open work. That doesn't touch the core thesis — barrel evaluation still dominates, and #10890 did nothing to it — and a one-line note or follow-up settles it.

Approving, pinned to the reviewed commit. ✅

中文说明

置信度:4/5 —— 证据已与真实 run 日志独立核对一致;仅一条非阻塞提示(§5 的 jsdom 阶段已由 #10890 落地)。

这是一篇高质量的计划文档。诊断建立在实测而非感觉上:collect/tests 比例可从真实 release run 日志复现;根因由两条独立路径(深路径导入与 barrel mock 套件)收敛到同一 ~2s 下界来锁定;文档对未知项也异常诚实 —— §6.2 的 barrel 解析问题被标为"动手前必须查清"而不是藏起来。它优先列出的迁移风险(§6.1 单文件 bundle 下的模块同一性、§6.3 vi.mock 对深路径导入静默失效)也正是审查者会担心的点。

范围保持最小:单文件、无代码、无顺手改动。六个月后,可信度表和"比值可用、绝对秒数不可外推"的标注会让这份文档保持可信而非误导。

唯一保留意见已在审查中点名:#10890 今天上午落地了 jsdom 按需,§5 表中的阶段 ③ 不再是待办。这不动摇核心结论 —— barrel 求值仍是大头,#10890 未触及 —— 一行说明或一次跟进即可解决。

批准,固定在被审提交上。✅

Qwen Code · qwen3.8-max

Reviewed at c3a421a65bbf77cdebf02b68d83e0abdc9fefc76 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

The open question about which files the package root resolves to is answered:
cli's own tsconfig carries a paths block mapping both the package root and a
wildcard subpath onto core's TypeScript sources, and esbuild honours it, so
neither the build nor the test run ever consults the package's exports map.
Both chains land on core sources and never on the build output.

That fixes the spelling a migration has to use — the package specifier plus a
path under core's source tree — and explains the duplicate module the earlier
experiment produced: a specifier naming the build output misses the wildcard
rule, falls back to node resolution, and lands on a second copy of a module
the package root had already pulled in from source.

It also surfaces a prerequisite that has to come first: the test runner
mirrors those path mappings by hand and currently lists only four named
subpaths, so deep imports do not resolve there at all until a wildcard entry
is added.
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

📄 Docs-only change — the automatic review ran at --effort medium (verified findings, no reverse audit; medium posts no inline comments). Outcome:

Review complete: pr-10909 — Comment, not posted (0 Critical, 12 Suggestion)

Reviewed head: b482add0554012985f8d6b2547ded44bf83886e0. Full report in the workflow run. For a full high-effort review with inline comments, comment @qwen-code /review.

中文说明

📄 纯文档变更 —— 自动评审以 --effort medium 运行(发现已验证、无反向审计;medium 不发布行内评论),结果见上方引用行。评审的 head:b482add0554012985f8d6b2547ded44bf83886e0。完整报告见 workflow 运行;如需带行内评论的完整高强度(high-effort)评审,请评论 @qwen-code /review

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Cross-linking the implementation: #10917 is open and it is the step this document names.

§6.1 says the wildcard alias is the prerequisite — "vitest alias:目前只有 4 个具名 subpath …… 没有通配。深路径导入今天在测试里会解析失败,必须先补一条通配 alias,这是 phase ① 的第一步" — and that is exactly what #10917 does, plus two migrated files as an end-to-end proof. It resolves @qwen-code/qwen-code-core/* to packages/core/src/*, which matches the §6.1 table: both chains land on core's TypeScript sources and neither consults exports. So the "写法已确定" row is now demonstrated under vitest rather than only derived from the tsconfig rule.

An independent datapoint for §1, from a PR run rather than a release run. This document's collect > tests evidence comes from release run 33713579913. The same signature showed up today on #10910's unit lane (job 100630658328, ecs-qwen-hk3-16), which was cancelled at the 120-minute cap:

Duration 5013.58s (transform 549.37s, setup 1235.22s, collect 17994.55s, tests 6333.43s, ...)

collect is 2.8x tests there, and packages/cli alone took 83 minutes of the 120 before packages/core got 12 minutes and the job died. Two failures in that run were both import-time, not assertion-time: voice-keyterms-race.test.ts timed out at 20s inside a beforeAll whose only statement is await import('./voice-keyterms.js'), and SessionPreview.test.tsx failed on an incomplete render.

Worth recording alongside it, because it bounds how much of this is contention rather than per-file cost — the DFSAMPLE lines from that job (the hosttests field #10869 added) read:

load[282.76 288.85 285.65] hosttests[133] memavail[169267280 kB] space[42%]
load[300.46 292.18 287.27] hosttests[185] memavail[161363944 kB] space[41%]

128-core host, load ~290, 114–187 concurrent vitest processes, with ~160 GB memory free and disk at 41%. So it is CPU oversubscription, not resource exhaustion — and collect is the phase that gets starved, which is the same conclusion this document reaches from the other direction. The ecs-qwen pool was at 78/94 runners busy at that moment, hk3 at 30/31.

One number to reconcile: §6.3 says 135 cli test files carry vi.mock('@qwen-code/qwen-code-core', factory). On #10917's head it is 138. Same order, and the risk is unchanged, but the codemod's scope figure should be re-counted at the commit it runs against rather than taken from here.

Refs #10908, #10917.

中文说明

互相链接一下实现侧:#10917 已经开着,而它做的正是本文档指定的那一步。

§6.1 写明通配 alias 是前置 —— 「vitest alias:目前只有 4 个具名 subpath …… 没有通配。深路径导入今天在测试里会解析失败,必须先补一条通配 alias,这是 phase ① 的第一步」—— #10917 做的就是这件事,另外带了两个迁移文件做端到端验证。它把 @qwen-code/qwen-code-core/* 解析到 packages/core/src/*,与 §6.1 的表格一致:两条链都落在 core 的 TypeScript 源码上,都不经过 exports。所以「写法已确定」这一行现在是在 vitest 下被验证过的,而不只是从 tsconfig 规则推导出来的。

给 §1 补一个独立数据点,来自 PR 运行而非 release 运行。 本文档的 collect > tests 证据取自 release run 33713579913。今天在 #10910 的单测通道上出现了同样的特征(job 100630658328ecs-qwen-hk3-16),该 job 在 120 分钟上限被取消:

Duration 5013.58s (transform 549.37s, setup 1235.22s, collect 17994.55s, tests 6333.43s, ...)

那里 collecttests2.8 倍,而 packages/cli 单独就吃掉了 120 分钟里的 83 分钟,随后 packages/core 只拿到 12 分钟,job 即被杀。那次运行中的两个失败都发生在导入期而非断言期:voice-keyterms-race.test.ts 在一个唯一语句是 await import('./voice-keyterms.js')beforeAll 里 20 秒超时,SessionPreview.test.tsx 则因渲染未完成而失败。

值得并列记录的是,这能界定其中有多少是争抢、多少是单文件成本 —— 该 job 的 DFSAMPLE 行(hosttests 字段是 #10869 加的)是:

load[282.76 288.85 285.65] hosttests[133] memavail[169267280 kB] space[42%]
load[300.46 292.18 287.27] hosttests[185] memavail[161363944 kB] space[41%]

128 核主机、load 约 290、并发 114–187 个 vitest 进程,同时还有约 160 GB 内存空闲、磁盘 41%。所以这是 CPU 超订而非资源耗尽 —— 而被饿死的正是 collect 阶段,这与本文档从另一个方向得到的结论一致。当时 ecs-qwen 池是 94 个 runner 中 78 个在忙,hk3 是 31 中 30。

有一个数字需要对齐: §6.3 写的是 135 个 cli 测试文件带 vi.mock('@qwen-code/qwen-code-core', factory)。在 #10917 的 head 上是 138。量级相同、风险不变,但 codemod 的范围数字应当在它实际运行的那个 commit 上重新统计,而不是沿用这里的值。

Refs #10908#10917

…st batch taught

The impact section was measured on a checkout three weeks behind main and
undercounted the surface: 453 cli modules import the package root for a value,
not 380, and 138 tests mock the package, not 113. The gap over three weeks
matches the growth rate the document already cites.

It also treated every barrel mock as an equal obstacle. They are not: a mock
that replaces the package outright never evaluates it, so the code under test
breaks the moment it imports a module directly, while a mock that spreads the
real package and overrides a few names only matters when an overridden name is
one the file imports. Separating the two is what makes 142 modules movable
today without touching a single test, and it changes the batching advice from
"by directory" to "by mock topology" — files in one directory have nothing in
common where this is concerned.

Two things the first batch cost us are written down so the next one does not
repeat them: a symbol map built from re-export chains can name the module that
re-exports a symbol rather than the one declaring it, which needs its own
verification pass; and Prettier invoked against paths outside its working
directory skips them while still reporting success.

The worry about code splitting is resolved rather than left open — the helper
that anchors sibling asset lookups already normalizes both possible chunk
locations, so import granularity cannot move a module anywhere that matters.
CI triggers on pull requests against main and release branches only, so a PR
based on another feature branch picks up the TUI gates and the bot jobs and
nothing else — no unit suite, no lint. Splitting this migration into reviewable
layers walked straight into that: two batches totalling well over two hundred
files sat green on seven or eight passing checks without a single test having
run, which is precisely the validation the whole approach leans on.

The section records what to do instead — point the top of the stack at main so
the cumulative tree gets a real run — and the detail that changing a base does
not restart anything, since GitHub reports that as an edit rather than a push.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants