Skip to content

Commit e6f821d

Browse files
committed
fix(profile): 用 Find-ExecutableCommand 替换 Get-Command 以恢复启动性能
引入统一的轻量级命令探测 API(Find-ExecutableCommand),在同步启动阶段替换性能低下的 Get-Command -CommandType Application 调用。该 API 默认不缓存未命中结果,仅在 Profile 等性能敏感路径显式开启,避免了 Windows 上缺失包管理器(choco、brew、apt)导致的数十秒冷启动延迟。 同时将 commandDiscovery 模块加入核心同步加载集合,确保 Profile 可直接调用,并更新相关测试、文档和性能诊断脚本以反映新的命令探测路径。
1 parent e0b1600 commit e6f821d

16 files changed

Lines changed: 1376 additions & 33 deletions

PesterConfiguration.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ $isVerbose = -not [string]::IsNullOrWhiteSpace($env:PWSH_TEST_VERBOSE)
4444
$qaDefaultPaths = @(
4545
"./tests/DeferredLoading.Tests.ps1"
4646
"./tests/losslessToAdaptiveAudio.Tests.ps1"
47+
"./tests/ProfileInstallHints.Tests.ps1"
4748
"./tests/ProfileMode.Tests.ps1"
49+
"./psutils/tests/commandDiscovery.Tests.ps1"
4850
"./tests/Switch-Mirrors.Tests.ps1"
4951
"./psutils/tests/error.Tests.ps1"
5052
"./psutils/tests/filesystem.Tests.ps1"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
date: 2026-03-14
3+
topic: profile-command-discovery
4+
---
5+
6+
# Profile 命令探测性能回归
7+
8+
## What We're Building
9+
10+
`psutils` 增加一个统一的高性能命令探测函数,用来判断某个命令在当前 shell 环境中是否可执行,并在需要时支持一次探测多个命令。这个函数的核心目标不是替代通用 `Get-Command`,而是为性能敏感路径提供更窄、更可控的“可执行文件发现”能力。
11+
12+
第一阶段只在 Profile 启动路径中接入这项能力,优先解决最近一次提交引入的启动性能回归。当前已确认回归点来自 `profile/features/environment.ps1` 在同步启动阶段额外探测 `choco``brew``apt` 等命令,导致 Windows 上缺失命令的发现成本暴涨到几十秒。阶段一的结果应当让 Profile 启动时间回到回归前的大致量级,而不是仅仅“比现在快一些”。
13+
14+
## Why This Approach
15+
16+
讨论过三种方向:新增专用模块并拆成单个/批量两个函数、继续把能力塞进 `test.psm1`、以及新增专用模块但只公开一个统一函数。最终选择“新增专用模块 + 统一函数”的方向,因为调用入口最直接,用户只需要记住一个命令即可,通过参数同时覆盖单个与批量探测场景。
17+
18+
相比之下,把能力继续堆进 `test.psm1` 会让模块职责更混乱;拆成两个公开函数虽然更显式,但日常使用时需要额外记忆单个版和批量版的差异,不符合这次“查找命令”这一能力本身的心智模型。当前需求更适合一个统一入口、清晰参数和稳定返回结构。
19+
20+
## Key Decisions
21+
22+
- 新能力放在 `psutils/modules/` 的专用模块中,并作为公开 API 导出。
23+
- 公开 API 采用统一函数形式,默认返回对象结构:`Name``Found``Path`
24+
- 同名命令默认只返回首个命中路径;只有显式参数开启时才返回全部命中项。
25+
- Windows 按真实可执行语义处理,基于 `PATH + PATHEXT` 识别 `.exe``.cmd``.bat` 等可执行命令。
26+
- 需要支持单个和批量探测,但通过同一个函数入口完成,而不是公开两个并列命令。
27+
- 底层允许显式开启“当前会话内缓存负结果”,但默认关闭;只有 Profile 这类性能敏感路径才应主动开启,避免影响通用调用语义。
28+
- 第一阶段只替换 Profile 启动路径中的相关探测逻辑,先恢复启动性能;`Test-EXEProgram` 暂不切换到底层新实现。
29+
- 第一阶段的验收重点是恢复 Profile 启动性能,同时保留最近一次提交引入的聚合安装提示目标,不在这一轮扩大到全仓库调用迁移。
30+
31+
## Resolved Questions
32+
33+
- `Get-Command -CommandType Application` 不适合作为 Profile 启动期的缺失命令探测手段;在当前 Windows 环境中,对不存在的 `choco``brew``apt` 探测会各自耗时约 20 秒。
34+
- 负结果缓存不能作为共享默认行为,否则会影响“同一会话中刚安装命令后立即检测”的常规调用场景。
35+
- 新能力应当成为 `psutils` 的公共基础能力,而不是只藏在 Profile 内部。
36+
- 这次更关注调用体验,因此选择“一个统一函数 + 参数控制行为”,而不是多个公开函数。
37+
- 第一阶段不改造整个仓库,也不立即替换 `Test-EXEProgram`;先把 Profile 回归问题压回去,再决定后续扩散范围。
38+
39+
## Open Questions
40+
41+
- 暂无。当前边界已经足够进入 planning 或直接实施。
42+
43+
## Next Steps
44+
45+
-> `/ce:plan` 用于整理模块设计、Profile 接入和验证步骤
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
---
2+
title: fix: restore profile command discovery performance
3+
type: fix
4+
status: completed
5+
date: 2026-03-14
6+
origin: docs/brainstorms/2026-03-14-profile-command-discovery-brainstorm.md
7+
---
8+
9+
# fix: restore profile command discovery performance
10+
11+
## Overview
12+
13+
修复最近一次 Profile 安装提示聚合改动带来的启动性能回归:在保留聚合提示目标的前提下,用一个更轻量、更可控的可执行命令探测 API 替换同步启动路径中的 `Get-Command -CommandType Application` 探测。该 API 作为 `psutils` 的公共能力公开,但第一阶段只在 Profile 启动路径接入(see brainstorm: `docs/brainstorms/2026-03-14-profile-command-discovery-brainstorm.md`)。
14+
15+
本计划直接承接 brainstorm 已确认的边界:公开 API 采用单一函数入口;默认返回对象 `Name``Found``Path`;默认只返回首个命中路径;支持单个与批量探测;负结果缓存只能由调用方显式开启,不能成为共享默认语义。
16+
17+
## Problem Statement / Motivation
18+
19+
当前回归由 `profile/features/environment.ps1` 在同步启动阶段扩大命令探测范围引起。最近一次聚合提示改动把 `scoop``winget``choco``brew``apt` 一并放进 `Get-Command -Name $trackedCommandNames -CommandType Application` 的批量探测中(`profile/features/environment.ps1:393`, `profile/features/environment.ps1:396`, `profile/features/environment.ps1:400`)。
20+
21+
本地复现已经确认:
22+
23+
- 在受影响 Windows 环境中,缺失的 `choco``brew``apt` 每个都可能让 `Get-Command` 卡住约 20 秒。
24+
- 同一批量探测在全新 `pwsh -NoProfile` 进程里可达到 60 秒以上,直接把 Profile 启动从约 1 秒级拉高到 50 秒级。
25+
- `where.exe` 对相同缺失命令返回很快,说明瓶颈不是简单的 PATH 遍历,而是 PowerShell 的命令发现回退链路。
26+
27+
同时,Profile 还有一个不能绕开的结构约束:启动同步路径只能依赖核心 psutils 子模块。`profile/README.md:246` 已明确说明,`Initialize-Environment` 执行期间使用的 psutils 函数必须位于核心模块;`profile/core/loadModule.ps1:9` 也记录了 `test.psm1` 已从同步路径移出,当前核心同步模块仅按需加载少量子模块(`profile/core/loadModule.ps1:11`, `profile/core/loadModule.ps1:20`)。因此,这次不能只写一个公共 API 然后在 Profile 中直接调用;计划必须同时解决“高性能探测”和“同步加载可达性”。
28+
29+
## Proposed Solution
30+
31+
### 1. 新增统一的轻量命令探测 API
32+
33+
`psutils/modules/` 下新增一个专用模块,提供统一的公开函数,例如 `Find-ExecutableCommand`。该函数接受一个或多个命令名,返回统一对象结构:
34+
35+
- `Name`
36+
- `Found`
37+
- `Path`
38+
39+
当输入多个命令时,返回一组同结构对象;当输入单个命令时,返回单个对象即可。默认仅返回首个命中路径;通过显式参数再返回全部命中项,例如增加 `AllPaths` 或等价字段。
40+
41+
该 API 的职责刻意收窄为“按当前 shell 可执行语义查找外部命令”,不处理函数、别名、模块自动导入,也不试图复刻 `Get-Command` 的完整发现语义。
42+
43+
### 2. 采用轻量实现而非 `Get-Command`
44+
45+
底层实现应直接面向可执行文件探测:
46+
47+
- Windows:按 `PATH + PATHEXT` 顺序查找,覆盖 `.exe``.cmd``.bat` 等真实可执行命令。
48+
- Linux/macOS:按 PATH 目录顺序查找命令文件,避免为探测本身额外启动 shell 进程。
49+
- 默认保持与当前 `Test-EXEProgram` 相近的保守缓存语义:可缓存命中结果,但不默认缓存未命中结果。
50+
- 提供显式参数让调用方开启“当前会话内缓存负结果”,供 Profile 这种性能敏感路径选择性使用。
51+
52+
实现时需要确保对重复、无效、畸形或不存在的 PATH 条目安全跳过,不因为路径拼接异常而抛错或显著放大耗时。
53+
54+
### 3. 让新 API 进入 Profile 同步路径,但不破坏延迟加载设计
55+
56+
由于 `Initialize-Environment` 位于同步启动阶段,这个新模块不能只存在于 `psutils.psd1` 的延迟全量导入路径中。计划必须显式处理以下内容:
57+
58+
- 将新模块加入 `psutils/psutils.psd1` 的模块清单与导出列表。
59+
- 将新模块纳入 `profile/core/loadModule.ps1` 的核心同步加载集合,保证 Profile 可直接调用。
60+
- 保持该模块足够轻量,不依赖当前未进同步路径的 psutils 子模块,避免因为新增核心模块反向吞掉本次性能收益。
61+
- 更新 `profile/README.md` 中关于核心同步模块的说明,避免文档继续宣称“启动阶段只能靠现有四个核心模块”。
62+
63+
这一步是本计划的关键实现约束,否则会重演同步路径误触发全量模块导入的问题(`tests/DeferredLoading.Tests.ps1`)。
64+
65+
### 4. 用新 API 替换 Profile 当前的命令探测
66+
67+
`profile/features/environment.ps1` 中,将当前“工具 + 包管理器”批量探测从 `Get-Command` 改为新 API:
68+
69+
- 继续保留当前聚合安装提示的高层逻辑,包括缺失工具收集、包管理器优先级选择、统一输出提示。
70+
-`availableCommands` / `availableTools` 的构建来源改为新 API 的返回对象。
71+
- 只在 Profile 这类明确受益的调用点显式开启负结果缓存,不把该行为扩散到共享默认语义。
72+
- 保持 `Get-ProfilePreferredPackageManager``Get-ProfileMissingToolInstallHint` 等聚合提示辅助函数的职责边界,避免这次修复把安装提示逻辑重新拆散。
73+
74+
### 5. 同步更新诊断与测试
75+
76+
现有性能诊断脚本和文档仍把 Phase 4.06 标记为 `Get-Command` 批量检测(`profile/Debug-ProfilePerformance.ps1`, `profile/README.md:94`, `profile/README.md:282`)。本次计划应一并更新:
77+
78+
- `profile/Debug-ProfilePerformance.ps1` 的探测步骤实现与标签,让诊断输出反映新的命令探测路径。
79+
- `profile/README.md` 中关于 `test.psm1`、同步路径命令探测方式和性能验证流程的说明。
80+
- `psutils` 的单元测试与 Profile 侧测试,覆盖新的 API 契约和本次回归场景。
81+
82+
## SpecFlow Analysis
83+
84+
从用户流和系统流角度,这次修复至少需要覆盖以下场景:
85+
86+
- **Flow 1: Windows 启动且多个包管理器缺失**
87+
- 用户启动 `pwsh`
88+
- Profile 进入同步探测阶段
89+
- 缺失的 `choco``brew``apt` 不应再把启动拖到几十秒
90+
- 若仍有缺失工具需要提示,聚合提示照常输出
91+
92+
- **Flow 2: Windows 启动且存在可用包管理器**
93+
- 用户缺失 `starship` / `zoxide` 等工具
94+
- 系统能快速识别 `scoop``winget`
95+
- 最终仍输出一条聚合安装命令
96+
97+
- **Flow 3: 公共 API 被普通调用者使用**
98+
- 调用者输入单个命令名
99+
- 默认获得对象结果,但不会因为默认负结果缓存而影响“同会话刚安装命令”的再探测语义
100+
101+
- **Flow 4: Profile 显式启用更激进的缓存**
102+
- 只有 Profile 或同类性能敏感调用者显式开启时,未命中结果才会在当前会话内缓存
103+
- 该策略不应悄悄污染其他 `psutils` 调用点
104+
105+
由此得到的补充要求:
106+
107+
- 计划中必须明确“默认语义”和“Profile 显式策略”的区别,避免实现时偷懒直接把负结果缓存做成全局默认。
108+
- 计划中必须覆盖“同步加载可达性”,否则 API 即便本身很快,也会因模块加载路径不当导致新的回归。
109+
110+
## Technical Considerations
111+
112+
- 性能优先级高于抽象完整性:这次目标是把启动时间从几十秒压回到回归前量级,API 设计应围绕这个目标收敛。
113+
- 命令发现语义应刻意比 `Get-Command` 更窄:只处理外部可执行命令,避免 PowerShell 的回退搜索、模块自动导入和 `get-*` 推断路径。
114+
- 新模块进入核心同步加载集合后,其依赖必须可审计且足够小,否则会用固定导入成本替代掉当前的动态回归成本。
115+
- `Test-EXEProgram` 在第一阶段不切换到底层新实现,避免一次改动同时重写过多历史调用语义(`psutils/modules/test.psm1:48`, `psutils/modules/test.psm1:69`, `psutils/modules/test.psm1:110`)。
116+
- 新公共 API 需要有清晰命名和帮助说明,让后续调用者知道它适合“可执行文件探测”,而不是拿来替换所有 `Get-Command` 用法。
117+
- 如果需要会话级缓存,缓存 key 需要考虑大小写不敏感平台、PATH 顺序、PATHEXT 及显式参数差异,避免错误复用。
118+
119+
## System-Wide Impact
120+
121+
- **Interaction graph**`profile/core/loadModule.ps1` 将同步导入一个新的轻量 psutils 子模块;`Initialize-Environment` 改为调用新 API 获取工具/包管理器可用性,再继续现有聚合提示流程。
122+
- **Error propagation**:命令探测失败时必须安全降级,不得阻塞 Profile 启动;最差结果也应退化为“无法生成安装命令,但 shell 继续可用”。
123+
- **State lifecycle risks**:新增的会话级缓存只应存在于新模块内部,并且默认不缓存负结果;不应影响现有 `Clear-EXEProgramCache` 语义。
124+
- **API surface parity**:第一阶段新增公共 API,但不立即替换 `Test-EXEProgram`;公共 API 与历史布尔型 API 并存,职责边界要写清楚。
125+
- **Integration test scenarios**
126+
- Windows 缺失 `choco``brew``apt` 时,冷启动不再出现数十秒阻塞。
127+
- Windows 存在 `scoop``winget` 时,缺失工具仍能输出一条正确的聚合安装命令。
128+
- PATH 中存在重复、无效或畸形条目时,不抛错且结果稳定。
129+
- 同名命令多个命中时,默认只返回首个路径;显式参数时返回全部命中。
130+
- 默认未命中不缓存;显式开启后才在当前会话复用负结果。
131+
132+
## Acceptance Criteria
133+
134+
- [x] `psutils` 新增统一的公开可执行命令探测 API,支持单个与批量输入,并默认返回包含 `Name``Found``Path` 的对象。
135+
- [x] 新 API 默认只返回首个命中路径;显式参数开启时可返回全部命中项。
136+
- [x] Windows 上的新 API 按 `PATH + PATHEXT` 的真实可执行语义工作,能识别 `.exe``.cmd``.bat` 等命令。
137+
- [x] 新 API 默认不缓存未命中结果;只有调用方显式开启时,才在当前会话内缓存负结果。
138+
- [x] 新模块被纳入 Profile 同步核心加载路径,且不会触发 psutils 全量自动导入回归。
139+
- [x] `profile/features/environment.ps1` 不再用 `Get-Command -CommandType Application` 批量探测工具与包管理器;聚合安装提示行为保持不变。
140+
- [x] `profile/Debug-ProfilePerformance.ps1` 和相关文档能反映新的命令探测路径,而不是继续显示旧的 `Get-Command` 阶段名称。
141+
- [x] 新增或更新测试,覆盖 API 契约、缓存边界、Windows 可执行语义、Profile 集成路径与延迟加载防护。
142+
- [x] 在受影响 Windows 环境上的手工验证中,Profile 启动时间恢复到回归前的大致量级,不再停留在 50 秒级别。
143+
- [x] 根目录 `pnpm qa` 通过。
144+
145+
## Success Metrics
146+
147+
- 受影响环境中,Profile 启动从“几十秒”回到“约 1 秒量级”,至少恢复到回归前同一数量级。
148+
- Phase 4 的命令探测不再成为主导耗时,性能诊断结果能把热点重新压回 `starship`、模块加载等原有主要项。
149+
- 安装提示聚合功能保留,用户仍能在一条高信号提示中看到当前缺失工具和可执行安装命令。
150+
- 新 API 成为后续命令存在性检测的可复用基础能力,但不会在第一阶段强行扩散到全仓库。
151+
152+
## Dependencies & Risks
153+
154+
- 风险:新模块若依赖过多或实现过重,会把动态回归变成固定同步加载成本。
155+
缓解:要求模块自包含、轻依赖,并以 `Debug-ProfilePerformance.ps1` 验证净收益。
156+
157+
- 风险:手写 PATH 扫描逻辑可能与 PowerShell 真正执行语义出现边缘差异。
158+
缓解:范围只收敛到“外部可执行命令”,并用 Windows `PATHEXT`、多后缀与多命中测试覆盖关键行为。
159+
160+
- 风险:如果只修改 `environment.ps1` 而忘记同步核心模块清单,Profile 会因调用新 API 而误触发延迟加载回归。
161+
缓解:将 `profile/core/loadModule.ps1``profile/README.md`、必要的防护栏测试一起纳入计划。
162+
163+
- 风险:默认缓存策略若设计不清晰,后续调用者容易误以为新 API 会自动复用未命中结果。
164+
缓解:在帮助文本、测试和计划中都明确“负结果缓存必须显式开启”。
165+
166+
- 风险:仓库当前没有 `docs/solutions/` 目录可供检索,本次计划缺少可复用的 institutional learnings。
167+
缓解:基于现有代码、文档和现场性能复现结果制定方案,并把关键约束写进计划避免丢失。
168+
169+
## Sources & References
170+
171+
- **Origin brainstorm:** `docs/brainstorms/2026-03-14-profile-command-discovery-brainstorm.md`
172+
- 延续的关键决策:公共 API 采用统一函数入口;默认返回 `Name` / `Found` / `Path`;默认不缓存未命中;只有 Profile 显式开启负结果缓存;第一阶段只修复 Profile。
173+
- **Regression entry point:** `profile/features/environment.ps1:393`, `profile/features/environment.ps1:396`, `profile/features/environment.ps1:400`, `profile/features/environment.ps1:512`
174+
- **Profile sync-load constraints:** `profile/core/loadModule.ps1:9`, `profile/core/loadModule.ps1:11`, `profile/core/loadModule.ps1:20`
175+
- **Profile architecture & performance guidance:** `profile/README.md:94`, `profile/README.md:246`, `profile/README.md:282`, `profile/README.md:289`
176+
- **Existing executable detection contract:** `psutils/modules/test.psm1:48`, `psutils/modules/test.psm1:69`, `psutils/modules/test.psm1:110`, `psutils/modules/test.psm1:544`
177+
- **Existing tests & guardrails:** `psutils/tests/test.Tests.ps1`, `tests/DeferredLoading.Tests.ps1`, `tests/ProfileInstallHints.Tests.ps1`, `tests/ProfileMode.Tests.ps1`
178+
- **Institutional learnings search:** 未发现 `docs/solutions/` 目录,本次未检索到可复用的历史方案
179+
- **External research:** 基于强本地上下文和明确回归点,当前计划未额外引入外部资料

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"test:profile": "pwsh -NoProfile -Command \"$env:PWSH_TEST_MODE='serial'; $env:PWSH_TEST_PATH='./tests/ProfileMode.Tests.ps1'; $env:PWSH_TEST_VERBOSE='1'; Invoke-Pester -Configuration ( ./PesterConfiguration.ps1 )\"",
3131
"test:slow": "pwsh -NoProfile -Command \"$c = ./PesterConfiguration.ps1; $c.Filter.Tag = 'Slow'; $c.Filter.ExcludeTag = @($c.Filter.ExcludeTag.Value | Where-Object { $_ -ne 'Slow' }); Invoke-Pester -Configuration $c\"",
3232
"test:detailed": "pwsh -Command \"Invoke-Pester -Output Detailed\"",
33+
"benchmark": "pwsh -NoProfile -File ./scripts/pwsh/devops/Invoke-Benchmark.ps1",
3334
"scripts:install": "pwsh -File ./install.ps1",
3435
"scoop:update": "scoop update -a",
3536
"choco:update": "choco upgrade all -y",

0 commit comments

Comments
 (0)