Skip to content

Commit eabb503

Browse files
committed
docs: 添加性能回归问题排查文档
新增性能问题排查文档,记录 Profile 模块中命令探测导致的启动性能回归问题。文档详细描述了问题的症状、根因分析、修复方案及预防措施,帮助团队理解如何避免在同步启动路径中使用语义过重的 API。
1 parent 4253888 commit eabb503

1 file changed

Lines changed: 140 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
---
2+
module: Profile
3+
date: 2026-03-14
4+
problem_type: performance_issue
5+
component: tooling
6+
symptoms:
7+
- "PowerShell Profile 加载耗时从约 1000ms 回归到 52368ms"
8+
- "全新 pwsh -NoProfile 进程中命令探测阶段单独耗时超过 60 秒"
9+
- "Windows 上缺失 choco、brew、apt 时,每个命令探测都可能卡住约 20 秒"
10+
root_cause: wrong_api
11+
resolution_type: code_fix
12+
severity: high
13+
tags: [profile, powershell, command-discovery, startup-performance, get-command]
14+
---
15+
16+
# Troubleshooting: Profile 命令探测导致启动性能回归
17+
18+
## Problem
19+
20+
Profile 安装提示聚合改动后,PowerShell 启动时间从约 1 秒级回归到 50 秒级。问题只在真实冷启动路径中稳定复现,表现为 `Profile 加载耗时: 52368 毫秒`,严重影响日常 shell 使用。
21+
22+
根因不在 `starship``zoxide` 或代理检测本身,而是在同步启动阶段对缺失包管理器命令使用了错误的探测 API,触发了 PowerShell 命令发现的高成本回退链路。
23+
24+
## Environment
25+
26+
- Module: Profile
27+
- Affected Component: PowerShell 启动期命令探测 / 安装提示聚合逻辑
28+
- Platform: Windows 10.0.26200
29+
- PowerShell: 7.5.4
30+
- Key files:
31+
- `profile/features/environment.ps1`
32+
- `profile/Debug-ProfilePerformance.ps1`
33+
- `profile/core/loadModule.ps1`
34+
- `psutils/modules/commandDiscovery.psm1`
35+
- Date: 2026-03-14
36+
37+
## Symptoms
38+
39+
- 用户实际看到 `Profile 加载耗时: 52368 毫秒`,原本基线约为 1000ms 左右。
40+
- 全新 `pwsh -NoProfile` 子进程中,`Get-Command -Name @('starship','zoxide','sccache','fnm','scoop','winget','choco','brew','apt') -CommandType Application` 可达到 64770ms。
41+
- 单独测量缺失命令时,`choco``brew``apt` 各自都可能消耗约 18-22 秒。
42+
- `where.exe` 对同样的缺失命令几乎瞬时返回,说明问题不是简单的 PATH 文件系统扫描,而是 PowerShell 的命令发现回退逻辑。
43+
44+
## What Didn't Work
45+
46+
**Attempted Solution 1:** 继续依赖 `Get-Command -CommandType Application`,只是把工具和包管理器改成一次批量探测。
47+
- **Why it failed:** 缺失命令会触发 PowerShell 命令发现回退,包含额外的 `get-*` 推断与更重的搜索路径,批量调用并不会规避这个成本,反而把多个高延迟探测叠加到了同步启动路径里。
48+
49+
**Attempted Solution 2:** 先假设问题只是 PATH 太长或某些目录过慢,使用 `where.exe` 做对照验证。
50+
- **Why it failed:** `where.exe` 结果很快,说明根因不是“纯 PATH 遍历过多”,而是 `Get-Command` 语义过重;它帮助定位了问题,但本身不是修复方案。
51+
52+
**Attempted Solution 3:** 直接相信旧版 `Debug-ProfilePerformance.ps1` 结果。
53+
- **Why it failed:** 旧诊断脚本当时只测工具探测,没有把新增的包管理器集合纳入 Phase 4.06,因此无法准确复现真实回归路径,必须先让诊断脚本跟真实启动路径保持一致。
54+
55+
## Solution
56+
57+
修复分成三步:
58+
59+
1.`psutils` 新增统一的轻量命令探测 API:`Find-ExecutableCommand`
60+
2. 将该模块纳入 Profile 同步核心加载路径,避免因为调用新 API 又触发 psutils 全量自动导入
61+
3. 在 Profile 启动期只探测“当前平台真正需要”的工具和包管理器,避免 Windows 冷启动再去碰 `brew``apt` 之类无关命令
62+
63+
**Code changes**:
64+
65+
```powershell
66+
# Before (broken):
67+
$toolNames = @('starship', 'zoxide', 'sccache', 'fnm')
68+
$packageManagerNames = @('scoop', 'winget', 'choco', 'brew', 'apt')
69+
$trackedCommandNames = @($toolNames + $packageManagerNames)
70+
$foundCommands = Get-Command -Name $trackedCommandNames -CommandType Application -ErrorAction SilentlyContinue
71+
72+
# After (fixed):
73+
$toolNames = @('starship', 'zoxide', 'sccache', 'fnm')
74+
$trackedToolNames = if ($runtimePlatform -eq 'windows') {
75+
@('starship', 'zoxide', 'sccache')
76+
} else {
77+
$toolNames
78+
}
79+
$packageManagerNames = switch ($runtimePlatform) {
80+
'windows' { @('scoop', 'winget', 'choco') }
81+
'macos' { @('brew') }
82+
default { @('brew', 'apt') }
83+
}
84+
$trackedCommandNames = @($trackedToolNames + $packageManagerNames)
85+
$commandDiscoveryResults = Find-ExecutableCommand -Name $trackedCommandNames -CacheMisses
86+
```
87+
88+
```powershell
89+
# New shared API:
90+
Find-ExecutableCommand -Name @('starship', 'zoxide', 'scoop') -CacheMisses
91+
```
92+
93+
**Verification commands**:
94+
95+
```powershell
96+
# 1. 详细性能诊断
97+
pwsh -NoProfile -NoLogo -File ./profile/Debug-ProfilePerformance.ps1
98+
99+
# 2. 冷启动 benchmark
100+
pnpm benchmark -- command-discovery -Iterations 2
101+
102+
# 3. 质量验证
103+
pnpm qa
104+
```
105+
106+
**Observed result after fix**:
107+
108+
- `Debug-ProfilePerformance.ps1` 总耗时约 `1083ms`
109+
- 其中 `4.06-command-discovery``220ms`
110+
- benchmark 中:
111+
- `Find-ExecutableCommand` 平均 `321.37ms`
112+
- `Get-Command -CommandType Application` 平均 `17406.726ms`
113+
- 冷启动命令探测速率约提升 `54.164x`
114+
115+
## Why This Works
116+
117+
根因是 **在性能敏感的同步启动路径里,用了语义过重的通用命令发现 API**
118+
119+
`Get-Command -CommandType Application` 看起来像“查可执行文件是否存在”,但在命令不存在时,PowerShell 仍会走一套昂贵的命令发现流程,包括额外的回退搜索和 `get-*` 推断。这在交互式一般命令发现里可以接受,但放进 Profile 启动同步路径就会把多个未命中成本直接叠加。
120+
121+
`Find-ExecutableCommand` 的修复关键在于两点:
122+
123+
1. **缩窄语义**:它只做“按当前 shell 可执行语义查外部命令”,不处理函数、别名、模块自动导入,也不复刻 `Get-Command` 的完整搜索行为。
124+
2. **缩窄范围**:Windows 启动时只探测 Windows 真正需要的工具和包管理器,不再无意义地扫描 `brew` / `apt`
125+
126+
这样既保住了聚合安装提示的功能,又把命令探测重新收回到可控成本范围内。
127+
128+
## Prevention
129+
130+
- **不要在 Profile 同步启动路径里直接用 `Get-Command -CommandType Application` 做缺失命令探测。** 如果只是判断外部命令是否存在,优先使用 `Find-ExecutableCommand`
131+
- **Profile 启动期的命令探测必须做平台裁剪。** 不要在 Windows 上探测 Linux/macOS 包管理器,反之亦然。
132+
- **新增同步路径能力时,同时更新诊断脚本。** 否则 `Debug-ProfilePerformance.ps1` 会和真实启动路径脱节,导致回归被漏检。
133+
- **任何启动期修改都要跑两类验证**
134+
- `pwsh -NoProfile -NoLogo -File ./profile/Debug-ProfilePerformance.ps1`
135+
- `pnpm benchmark -- command-discovery`
136+
- **如果启动期要调用新的 psutils 函数,必须先确认它位于核心同步模块集合中。** 否则会通过 `PSModulePath` 误触发 psutils 全量自动导入,直接抵消性能优化。
137+
138+
## Related Issues
139+
140+
No related issues documented yet.

0 commit comments

Comments
 (0)