改一行代码,等十二分钟才知道有没有编过;再等二十分钟,才知道有没有测过。一天里真正写代码的时间不到三小时,其余在等——等的时候你在看别的分支、回消息、想别的问题。
"够快"不是一个秒数,而是单位时间内你能完成多少次"改动—验证"的往返。 这篇谈这个环的四个位置、快慢的经济边界、以及什么时候优化这件事本身不值得。
1. 四个环节与各自的瓶颈
| 环节 | 典型等待 | 真正的瓶颈 | 加快它的手段 |
|---|---|---|---|
| 编辑 | 毫秒级 | 索引、大文件、语言服务 | 增量索引、排除生成目录 |
| 构建 | 秒到分钟 | 输入指纹算错 → 全量重编 | 声明输入输出、命中率 |
| 测试 | 秒到十分钟 | 粒度过粗、共享状态 | 分层、定向选择 |
| 部署 | 分钟级 | 环境排队、审批、镜像拉取 | 预留环境、流水线缓存 |
四段相加才是循环周期,团队却常只盯最长那段。把最长段砍半未必缩短周期:第二长的段若在等第一段释放资源,砍完只是把排队挪了位置。
2. "够快"的判据
| 判据 | 怎么量 | 为什么比秒数可靠 |
|---|---|---|
| 单位时间尝试次数 | 一天内"改—验"完整循环数 | 与产出直接相关,与机器型号无关 |
| 被打断次数 | 等待期间切换任务的次数 | 直接反映注意力损耗 |
| 排队占比 | 循环时长里等资源的比例 | 区分"慢"和"挤" |
同一台机器上,十二分钟在一天跑三次时可接受、跑二十次不可接受——先问"这环节一天过几次",再问"每次多久"。关键在于等待一旦超过切换成本,人就会切走,切回来要重新载入上下文,这段损耗从不出现在构建日志里。
// 一天的账,不用工具也能记
循环 = 改动 → 本地构建 → 定向测试 → 推送 → CI 结果
次数 = 8 // 低于 5 次,先看是不是环节太慢导致不愿改
切换 = 6 // 4 次发生在等 CI 时 → CI 是主瓶颈
3. 三档延迟带
| 档位 | 时长量级 | 人的行为 | 适合放什么 |
|---|---|---|---|
| 可保持心流 | 秒级(≤ 约 5s) | 盯着看完,接着改 | 语法检查、单文件编译、单测 |
| 需切换任务 | 十秒到两分钟 | 去开 issue、回条消息 | 增量构建、模块级测试 |
| 需重新进入 | 五分钟以上 | 切到别的分支或别的事 | 全量构建、集成/E2E、部署 |
档位之间不是线性关系:4 秒到 40 秒,成本涨十倍;40 秒到 400 秒,成本涨不到一倍——因为已经在第三档,行为变成"切走再回来"。所以优化要跨档才有意义:40 秒压到 25 秒收益接近零,压到 5 秒以内是整档跃迁。
第三档的方向不是"快一点",而是"别让人等":后台跑、失败才打断。
4. 增量构建的收益边界
增量构建、构建缓存、远程缓存的加速能力,全部由一个数决定:命中率。命中率高,构建近乎免费;命中率低,增量比全量还慢——还要花时间算指纹。
| 命中率来源 | 提高方式 | 上限由什么决定 |
|---|---|---|
| 输入声明正确 | 声明任务输入输出,不塞时间戳 | 代码是否模块化 |
| 缓存可复用 | 同输入产物跨机器共享 | 环境一致性与密钥管理 |
| 变更粒度小 | 一次改动只触及少数模块 | 依赖图是否真解耦 |
若项目"改一处、全图失效",增量收益天然有限——该动的是依赖结构。判据:一次典型改动让多少模块缓存失效,超过三成说明结构比工具更值得处理。
// 看命中与失效:按版本核对任务名与日志格式
./gradlew build --info 2>&1 | grep -E "Task .*(UP-TO-DATE|FROM-CACHE|executed)"
// 只看两项:executed 数量、from-cache 占比
5. 测试反馈:分层与定向
| 层 | 时长量级 | 覆盖 | 放错层的代价 |
|---|---|---|---|
| 单元 | 秒级 | 逻辑分支、边界 | 放进集成层:反馈慢、定位难 |
| 集成 | 分钟级 | 契约、SQL、序列化 | 放进单元层:mock 越写越多 |
| 端到端 | 十分钟级 | 关键路径、真实环境 | 放进每次提交:排队、假失败 |
分层判据不是"重要性",而是失败时能否立刻指到一行代码:能,放单元层;只能在边界断言,放集成层;只能从外部观察,放端到端层。
定向测试是收益最高的一招,代价是漏测:
| 策略 | 收益 | 风险 | 适用 |
|---|---|---|---|
| 按改动影响面 | 分钟变秒 | 依赖分析错就漏 | 本地迭代 |
| 按最近失败集 | 复跑快、噪音少 | 只覆盖已知问题 | 调试阶段 |
| 全量并行 | 无漏测 | 资源与排队 | 提交前一次 |
正确分工是本地定向、提交全量。反过来做(本地全量、CI 定向)会把等待堆在最贵的地方。
// 本地只跑受影响模块,提交前再全量
./gradlew :module-a:test --tests "*OrderService*" // 迭代用
./gradlew test // 提交前
6. 并行化的塌陷点
| 阶段 | 并行收益 | 何时塌陷 |
|---|---|---|
| 编译 | 接近线性 | CPU 用满后,内存与 IO 成瓶颈 |
| 测试 | 高(相互独立时) | 共享数据库/端口/固定数据 |
| 部署 | 低 | 环境数固定,排队无解 |
边际收益在资源总量处塌陷,不在任务数量处:四核上切十六份,只会让上下文切换与内存竞争吃掉收益。更隐蔽的是并行会暴露共享状态——原本串行时隐藏的固定数据、端口、时间依赖,一并行就变随机失败。
7. 可信度与假失败
| 状态 | 团队行为 | 长期结果 |
|---|---|---|
| 慢但可信 | 等,但结果当真 | 反馈有效,敢改 |
| 快但常有假失败 | 重跑、忽略红灯、本地绕过 | 反馈失效,回到人肉验证 |
假失败的破坏力不在浪费时间,而在训练团队忽略红灯:一旦"红了先重跑"成习惯,真实失败也会被重跑掉。所以顺序是先让红灯可信,再让它变快。
两个来源的治理顺序不能反,否则它们会互相冒充:
| 来源 | 表现 | 治理 | 优先级 |
|---|---|---|---|
| 测试自身不稳定 | 同一提交重跑结果不同 | 去共享状态、时间依赖、异步等待 | 先治 |
| 环境差异 | 本地过、CI 不过 | 对齐运行时与依赖、固定数据 | 后治 |
// 判据:同一提交重跑三次
结果不同 → 测试不稳定(隔离、去共享)
结果相同但 CI 失败 → 环境差异(对齐版本与数据)
治理节奏按"每周清一批",不要"专项一个月":专项会积压,清完又长。
8. 远程缓存的组织前提
远程缓存能把别人的构建结果直接拿来用,但前提是组织性的:
| 前提 | 不满足时 | 后果 |
|---|---|---|
| 环境可复现 | 同输入产物不同 | 缓存不可信,命中即事故 |
| 密钥与权限收敛 | 谁都能污染缓存 | 缓存成为攻击面 |
| 输入声明统一 | 各人本地配置不同 | 命中率长期很低 |
| 有清理机制 | 缓存无限膨胀 | 存储涨,命中变慢 |
最后一条常被忽略:缓存既是资产也是负债,没有清理机制,半年后命中率会自己掉下来。
// 远程缓存准入清单,四项全过再开
remote_cache:
reproducible: true // 同输入同产物,抽 5 个模块验证
access: team-scoped // 读写分离,写入需鉴权
input_declared: true // 无时间戳/环境变量混入输入
eviction: 30d // 过期清理,容量另设上限
9. 反馈环与 CI 成本
| 选择 | 省下什么 | 付出什么 |
|---|---|---|
| 自建大机器 | 等待时间 | 固定成本、闲置浪费 |
| 弹性云构建 | 无闲置 | 冷启动、排队不可控 |
| 远程缓存 | 重复计算 | 存储与一致性风险 |
| 少跑测试 | 时间 | 漏测,代价延后到线上 |
关键是把等待折成钱:一天八次循环、每次十分钟、十人团队,每周就是数小时团队工时。反直觉的结论是:构建已在一分钟以内时,继续投入回报极低——此时瓶颈通常在评审、审批或需求不清。
10. 度量
| 指标 | 口径 | 读到什么 | 反噬 |
|---|---|---|---|
| 提交到首次反馈 | 推送 → 第一条 CI 结果 | 入口是否通畅 | 靠减检查刷低 |
| 单日循环次数 | 完整"改—验"次数 | 反馈是否真可用 | 靠碎提交刷高 |
| 假失败率 | 重跑才通过的比例 | 信号可信度 | 靠放宽断言刷低 |
| 等待切换成本 | 等待期间切换任务次数 | 隐性损耗 | 靠不记录消失 |
四个数一起看:首次反馈短 + 循环次数高 + 假失败率低才是真的转起来了。只看第一项,容易得到"快但没人用"。
11. 排错
| 现象 | 可能原因 | 验证 |
|---|---|---|
| 本地快、CI 慢 | 环境差异或排队 | 对比各阶段耗时与队列长度 |
| 增量比全量还慢 | 输入声明有误、命中率极低 | 看 executed 任务占比 |
| 红灯没人看 | 假失败已训练出习惯 | 统计重跑通过比例 |
| 并行后失败变多 | 共享状态被暴露 | 单线程重跑同一提交 |
| 优化最长段但总时长没变 | 瓶颈在排队而非计算 | 分阶段测等待与计算 |
12. 边界
| 场景 | 为什么优化不划算 | 替代 |
|---|---|---|
| 代码库极小 | 全量本来就在秒级 | 先做别的 |
| 构建已在一分钟内 | 边际收益极低 | 转去评审与需求环节 |
| 团队一两人 | 共享缓存无规模效应 | 本地缓存足够 |
| 项目临近结束 | 投入回收不了 | 只做不改流程的小修 |
| 瓶颈是审批 | 加速构建不改周期 | 改流程,不改机器 |
还有一条:瓶颈不在工程侧时,构建提速不会体现在交付上。看等待时间里多少是"机器在算"、多少是"人在等别人",后者多就换目标。
13. 自测
- 为什么"够快"不能用秒数定义?用哪两个数替代?
- 三档延迟带之间为什么是非线性的?这对优化目标意味着什么?
- 把 40 秒优化到 25 秒为什么几乎没有收益?
- 增量构建的收益上限由什么决定?怎么快速判断该先动结构?
- 定向测试的代价是什么?本地与 CI 应如何分工?
- 为什么"先让红灯可信,再让它变快"?反过来会付出什么?