Skip to content

Commit 2aeb6c3

Browse files
committed
perf(profile): 优化加载性能并修复补全响应
- 修复 starship 初始化脚本缓存,使用 `--print-full-init` 缓存完整脚本,消除每次 prompt 渲染 spawn 外部进程的问题 - 将 Tab 补全模式从 `MenuComplete` 切换为 `Complete`,避免一次性枚举所有候选的开销 - 优化工具检测逻辑,用批量 `Get-Command` 替代逐个 `Test-EXEProgram` 调用 - 优化代理探测,缩短超时并添加缓存 - 精简 PSModulePath,移除不必要的额外路径 - 添加分阶段计时诊断,便于监控性能回归
1 parent be88e36 commit 2aeb6c3

6 files changed

Lines changed: 328 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-02-11
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
## Context
2+
3+
当前 Profile 在 Windows Full 模式下加载耗时约 2s,相比此前约 1s 有明显回归。同时 Tab 补全响应需数秒,严重影响日常使用体验。
4+
5+
通过代码走读和链路分析,**加载瓶颈**分布在以下环节:
6+
7+
1. **工具检测**~100-200ms):`Test-EXEProgram` 对 starship/zoxide/sccache/fnm 逐个调用 `Get-Command -CommandType Application`,每次扫描整个 PATH
8+
2. **代理探测**~100-300ms):`Set-Proxy auto` 通过 TCP 连接检测代理可用性,超时 100ms + 连通后二次检测 200ms
9+
3. **编码初始化**~40ms):`Set-ProfileUtf8Encoding` 中两次不限类型的 `Get-Command` 调用
10+
11+
**Tab 补全慢**的根因:
12+
13+
4. **starship 缓存形同虚设**`Invoke-WithFileCache` 的 Generator `{ & starship init powershell }` 输出的是一行引导代码(`Invoke-Expression (& starship init powershell --print-full-init ...)`),而非完整初始化脚本。这导致缓存命中时 dot-source 仍会 spawn starship 进程。每次 prompt 渲染(包括 Tab 补全后刷新)都会启动一个 starship 外部进程。
14+
5. **MenuComplete 模式**:要求一次性枚举所有候选项(~70 个 psutils 函数 + PATH 中所有可执行文件 + 别名 + 文件),在 PATH 很长时扫描开销大。
15+
6. **PSModulePath 包含额外目录**:命令发现阶段需要扫描更多路径寻找自动加载模块。
16+
17+
约束条件:
18+
- 不能破坏 Full/Minimal/UltraMinimal 三种模式的语义和行为
19+
- `psutils` 模块功能必须完整保留
20+
- 所有现有函数、别名、环境变量在 Full 模式下必须仍然可用
21+
- profile 错误不得阻止 shell 启动
22+
23+
## Goals / Non-Goals
24+
25+
**Goals:**
26+
27+
- 将 Windows Full 模式加载时间降至 ~1s 以内(减少约 50%)
28+
- 显著改善 Tab 补全响应速度(从数秒降至即时响应)
29+
- 提供分阶段计时诊断能力,便于后续监控性能回归
30+
- 保持所有现有功能和模式语义不变
31+
32+
**Non-Goals:**
33+
34+
- 不重写 psutils 模块架构(如编译为 .dll 二进制模块)
35+
- 不拆分 psutils 的 Import-Module 加载(会影响 Tab 补全和函数可用性)
36+
- 不修改 Minimal/UltraMinimal 模式的行为(这两个模式已经足够快)
37+
- 不引入异步/并行 PowerShell Job 机制(复杂度过高,收益不确定)
38+
- 不优化 starship/zoxide 上游初始化脚本本身的执行时间
39+
40+
## Decisions
41+
42+
### Decision 1: 修复 starship 缓存(收益最大)
43+
44+
**选择**:将 `Invoke-WithFileCache` 的 Generator 从 `{ & starship init powershell }` 改为 `{ & starship init powershell --print-full-init }`
45+
46+
**问题分析**`starship init powershell` 不加 `--print-full-init` 时输出的是一行引导代码:
47+
```powershell
48+
Invoke-Expression (& starship init powershell --print-full-init | Out-String)
49+
```
50+
缓存这段代码后 dot-source 时仍会调用 starship 二进制。加 `--print-full-init` 后输出约 200 行完整的 PowerShell 初始化脚本(包含 prompt 函数定义等),缓存后 dot-source 不再需要外部进程。
51+
52+
**替代方案**
53+
- 不缓存直接每次调用 → 每次启动都要 spawn starship 进程,更慢
54+
- 在 prompt 函数中做异步渲染 → 改动太大,属于 starship 上游职责
55+
56+
**理由**:这是一行参数的修复,收益极大——消除了每次 prompt 渲染 spawn 外部进程的问题,直接解决 Tab 补全慢的核心根因。zoxide 缓存已确认是正确的(完整脚本约 130 行),无需修改。
57+
58+
### Decision 2: Tab 补全模式从 MenuComplete 切换回 Complete
59+
60+
**选择**:将 `Set-PSReadLineKeyHandler -Key Tab -Function MenuComplete` 改为 `Set-PSReadLineKeyHandler -Key Tab -Function Complete`
61+
62+
**替代方案**
63+
- 保持 MenuComplete + 优化候选源 → 根本问题无法解决,PowerShell 仍需一次性枚举全部候选
64+
- 使用 PSReadLine Prediction 替代 → 互补关系,不冲突但不解决 Tab 问题
65+
66+
**理由**`Complete` 模式只补全到最长公共前缀,多次 Tab 循环候选,不需要一次性扫描所有候选源。这是 PowerShell 的默认行为,用户已习惯。
67+
68+
### Decision 3: 批量工具检测替代逐个调用
69+
70+
**选择**:用单次 `Get-Command -Name @('starship','zoxide','sccache','fnm') -CommandType Application -ErrorAction SilentlyContinue` 批量查询替代 4 次独立 `Test-EXEProgram` 调用。
71+
72+
**替代方案**
73+
-`[System.IO.File]::Exists()` 检查已知路径 → 不跨平台,需要维护路径表
74+
-`where.exe`/`which` 命令 → 启动外部进程更慢
75+
76+
**理由**:单次 `Get-Command` 批量查询比 4 次独立调用快,因为 PowerShell 内部会复用 PATH 扫描结果。
77+
78+
### Decision 4: 代理探测优化
79+
80+
**选择**
81+
1.`Set-Proxy auto` 的 TCP 超时从 100ms 缩短为 50ms
82+
2. 取消 `Set-Proxy on` 中的二次端口检测(200ms timeout),改为仅设置环境变量
83+
3.`Initialize-Environment` 中为 `Set-Proxy auto` 包装 `Invoke-WithCache` 缓存层,缓存有效期 5 分钟
84+
85+
**理由**:局域网代理 50ms 足以完成 TCP 握手;端口检测主要用于提示用户,不需要阻塞启动路径;缓存状态可以大幅减少重复探测。
86+
87+
### Decision 5: Get-Command 搜索范围收窄
88+
89+
**选择**:在 `Set-ProfileUtf8Encoding` 中:
90+
- PSReadLine 是 pwsh 7 内置模块,直接调用 `Set-PSReadLineKeyHandler` 而不做 `Get-Command` 检查
91+
- `Register-FzfHistorySmartKeyBinding``-CommandType Function` 限定搜索范围
92+
93+
**理由**:PSReadLine 在 PowerShell 7+ 中始终可用(是 profile 的运行时基线),无需检查。加 `-CommandType Function` 避免扫描 Application 类型。
94+
95+
### Decision 6: 精简 PSModulePath
96+
97+
**选择**:在 `profile/core/loadModule.ps1` 中不再将项目父目录追加到 `PSModulePath`,仅保留 PSModulePath 去重逻辑。
98+
99+
**替代方案**
100+
- 保持添加但在命令发现策略中优化 → PowerShell 不提供此级别控制
101+
102+
**理由**:额外的 PSModulePath 条目会导致 PowerShell 命令发现阶段扫描更多目录。项目父目录可能包含大量子目录,拖慢命令查找和 Tab 补全。psutils 模块通过显式 `Import-Module` 加载,不依赖 PSModulePath 自动发现。
103+
104+
### Decision 7: 分阶段计时诊断
105+
106+
**选择**:在 `profile.ps1` 中使用 `[System.Diagnostics.Stopwatch]` 对关键阶段计时,通过 `Verbose` 流输出,并在环境变量 `POWERSHELL_PROFILE_TIMING=1` 时输出到主机。
107+
108+
**理由**:Stopwatch 精度高、开销极小(微秒级),不影响正常加载性能。Verbose 流不干扰正常输出,环境变量开关便于按需启用。
109+
110+
## Risks / Trade-offs
111+
112+
- **[Risk] starship `--print-full-init` 输出在版本升级后可能变化** → Mitigation: 缓存有效期 7 天,升级 starship 后最多 7 天自动刷新;用户可手动删除 `.cache/starship-init-powershell.ps1` 强制重建。
113+
114+
- **[Risk] Complete 模式相比 MenuComplete 体验不同** → Mitigation: Complete 是 PowerShell 默认行为;如果用户不适应可以随时在 encoding.ps1 改回。
115+
116+
- **[Risk] 缓存代理状态可能导致代理切换后延迟生效** → Mitigation: 缓存有效期仅 5 分钟,用户可通过 `Set-Proxy on/off` 手动刷新。
117+
118+
- **[Risk] 移除 PSModulePath 中的额外路径可能影响某些自动加载场景** → Mitigation: psutils 通过显式 Import-Module 加载,不依赖自动发现。如果有其他模块依赖此路径,会在测试阶段发现。
119+
120+
- **[Risk] 批量 Get-Command 的结果顺序** → Mitigation: 用 `.Name` 属性做 HashSet 查找,不依赖顺序。
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## Why
2+
3+
Windows 环境下 Profile 加载耗时从约 1s 增长到约 2s,且 Tab 补全响应变慢(需数秒才能返回候选)。经分析,加载瓶颈集中在代理 TCP 探测阻塞、`Test-EXEProgram` 多次扫描 PATH、`Get-Command` 搜索范围过宽等问题;Tab 补全慢的根因是 starship 初始化脚本缓存形同虚设(缓存的是引导代码而非完整初始化脚本,导致每次 prompt 渲染都 spawn 外部进程)以及 `MenuComplete` 模式要求一次性枚举所有候选。需要在不破坏现有功能和模式语义的前提下,将 Full 模式加载时间降回 1s 以内,并显著改善 Tab 补全响应速度。
4+
5+
## What Changes
6+
7+
- 修复 starship 初始化脚本的 `Invoke-WithFileCache` 缓存,使用 `--print-full-init` 缓存完整初始化脚本而非引导代码,消除每次 prompt 渲染 spawn starship 进程的问题
8+
- 将 Tab 补全模式从 `MenuComplete` 切换回 `Complete`,避免一次性枚举所有候选的开销
9+
- 优化 `Initialize-Environment` 中的工具检测逻辑,用批量 `Get-Command` 替代逐个 `Test-EXEProgram` 调用
10+
- 优化 `Set-Proxy -Command auto` 的 TCP 探测,缩短超时并考虑缓存上次代理状态
11+
- 优化 `Set-ProfileUtf8Encoding` 中的 `Get-Command` 调用,缩窄搜索范围
12+
- 精简 PSModulePath,不添加不必要的额外模块搜索路径
13+
-`Initialize-Environment` 添加分阶段计时诊断,便于后续持续监控性能回归
14+
15+
## Capabilities
16+
17+
### New Capabilities
18+
19+
- `profile-perf-diagnostics`: Profile 加载分阶段计时诊断能力,提供每个阶段的耗时报告
20+
21+
### Modified Capabilities
22+
23+
- `unified-profile`: 修复 starship 缓存、切换 Tab 补全模式、优化工具初始化流程和代理检测逻辑、精简 PSModulePath
24+
25+
## Impact
26+
27+
- **profile/features/environment.ps1**: `Initialize-Environment` 函数修复 starship 缓存参数、重构工具检测和代理探测逻辑
28+
- **profile/core/encoding.ps1**: `Set-ProfileUtf8Encoding` 函数切换 Tab 补全模式并优化 `Get-Command` 调用
29+
- **profile/core/loadModule.ps1**: 精简 PSModulePath 操作
30+
- **psutils/modules/proxy.psm1**: `Set-Proxy auto` 优化 TCP 超时
31+
- **profile/profile.ps1**: 添加分阶段计时支持
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
## ADDED Requirements
2+
3+
### Requirement: 分阶段计时诊断
4+
5+
Profile 加载过程 SHALL 对关键阶段进行 `[System.Diagnostics.Stopwatch]` 精确计时,并将结果存入结构化变量 `$script:ProfileTimings`
6+
7+
#### Scenario: 默认 Verbose 输出
8+
9+
- **WHEN** Profile 加载完成且未设置 `POWERSHELL_PROFILE_TIMING` 环境变量
10+
- **THEN** 各阶段耗时 SHALL 仅通过 `Write-Verbose` 输出,不影响正常终端显示
11+
12+
#### Scenario: 环境变量启用详细计时
13+
14+
- **WHEN** `POWERSHELL_PROFILE_TIMING=1` 且 Profile 加载完成
15+
- **THEN** SHALL 在终端输出各阶段耗时报告,格式包含阶段名称和毫秒数
16+
17+
#### Scenario: 计时覆盖关键阶段
18+
19+
- **WHEN** Profile 以 Full 模式加载
20+
- **THEN** SHALL 至少对以下阶段独立计时:模块加载、代理检测、工具初始化(含各工具明细)、别名注册、总耗时
21+
22+
#### Scenario: 计时开销可忽略
23+
24+
- **WHEN** Profile 加载
25+
- **THEN** 计时机制本身的开销 SHALL 不超过 5ms
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
## MODIFIED Requirements
2+
3+
### Requirement: 统一工具初始化
4+
5+
`Initialize-Environment` 函数 SHALL 维护统一的工具初始化表,并依据当前模式控制初始化范围。每个工具的初始化逻辑 SHALL 内部判断平台适用性。工具检测 SHALL 使用批量 `Get-Command` 查询替代逐个调用 `Test-EXEProgram`,以减少 PATH 扫描次数。
6+
7+
#### Scenario: Full 模式初始化完整工具链
8+
9+
- **WHEN** 当前模式为 Full 且工具已安装
10+
- **THEN** SHALL 执行 starship、zoxide、fnm、sccache 等初始化逻辑,并按平台规则完成配置
11+
12+
#### Scenario: 批量工具检测
13+
14+
- **WHEN** 进入工具初始化阶段
15+
- **THEN** SHALL 使用单次 `Get-Command -Name @(...) -CommandType Application` 批量检测所有工具的可用性,而非对每个工具独立调用 `Test-EXEProgram`
16+
17+
#### Scenario: 通用工具初始化
18+
19+
- **WHEN** starship 或 zoxide 已安装
20+
- **THEN** SHALL 在所有平台上初始化该工具,使用 `Invoke-WithFileCache` 缓存初始化脚本
21+
22+
#### Scenario: starship 缓存完整初始化脚本
23+
24+
- **WHEN** 使用 `Invoke-WithFileCache` 缓存 starship 初始化脚本
25+
- **THEN** Generator SHALL 使用 `& starship init powershell --print-full-init` 以缓存完整的初始化脚本(约 200 行),而非缓存引导代码
26+
27+
#### Scenario: 平台特定工具初始化
28+
29+
- **WHEN** 在 Windows 平台且 sccache 已安装
30+
- **THEN** SHALL 设置 `$env:RUSTC_WRAPPER = 'sccache'`
31+
- **WHEN** 在 Unix 平台且 fnm 已安装
32+
- **THEN** SHALL 执行 `fnm env --use-on-cd | Out-String | Invoke-Expression`
33+
34+
#### Scenario: Minimal 模式跳过交互增强
35+
36+
- **WHEN** 当前模式为 Minimal
37+
- **THEN** SHALL 跳过工具初始化和别名注册,但保留模块函数可用性
38+
39+
#### Scenario: UltraMinimal 模式仅保留最小能力
40+
41+
- **WHEN** 当前模式为 UltraMinimal
42+
- **THEN** SHALL 跳过模块加载、工具初始化、代理检测、PATH 同步、别名与包装函数注册,仅保留 UTF8 设置、`POWERSHELL_SCRIPTS_ROOT` 与基础变量兼容
43+
44+
## ADDED Requirements
45+
46+
### Requirement: 代理探测性能优化
47+
48+
`Set-Proxy -Command auto` 的 TCP 探测 SHALL 使用缩短的超时时间,并缓存代理可用性状态以避免每次 profile 加载都做网络探测。
49+
50+
#### Scenario: TCP 超时缩短
51+
52+
- **WHEN** 执行 `Set-Proxy -Command auto`
53+
- **THEN** TCP 连接超时 SHALL 不超过 50ms
54+
55+
#### Scenario: 代理状态缓存
56+
57+
- **WHEN** Profile 加载且代理状态缓存有效(5 分钟内)
58+
- **THEN** SHALL 直接使用缓存的代理状态,不执行 TCP 探测
59+
60+
#### Scenario: 缓存过期重新探测
61+
62+
- **WHEN** Profile 加载且代理状态缓存已过期或不存在
63+
- **THEN** SHALL 执行 TCP 探测并更新缓存
64+
65+
#### Scenario: 手动操作绕过缓存
66+
67+
- **WHEN** 用户显式调用 `Set-Proxy on``Set-Proxy off`
68+
- **THEN** SHALL 直接执行操作并更新缓存状态
69+
70+
### Requirement: 编码初始化优化
71+
72+
`Set-ProfileUtf8Encoding` SHALL 直接调用 PSReadLine API 而不做 `Get-Command` 可用性检查(PowerShell 7+ 基线保证 PSReadLine 内置),仅对非内置模块(如 PSFzf)的函数用限定类型的 `Get-Command` 检查。
73+
74+
#### Scenario: PSReadLine 直接调用
75+
76+
- **WHEN** 执行 `Set-ProfileUtf8Encoding`
77+
- **THEN** SHALL 直接调用 `Set-PSReadLineKeyHandler` 而不先用 `Get-Command` 检查其是否存在
78+
79+
#### Scenario: PSFzf 限定类型检查
80+
81+
- **WHEN** 检查 `Register-FzfHistorySmartKeyBinding` 可用性
82+
- **THEN** SHALL 使用 `Get-Command -CommandType Function` 限定搜索范围
83+
84+
### Requirement: Tab 补全模式
85+
86+
`Set-ProfileUtf8Encoding` SHALL 将 Tab 键绑定为 `Complete` 模式(补全最长公共前缀,多次 Tab 循环候选),不使用 `MenuComplete` 模式(一次性枚举所有候选)。
87+
88+
#### Scenario: Tab 键使用 Complete 模式
89+
90+
- **WHEN** Profile 加载完成后用户按下 Tab 键
91+
- **THEN** SHALL 使用 `Complete` 函数进行补全,仅补全到最长公共前缀
92+
93+
### Requirement: PSModulePath 精简
94+
95+
`profile/core/loadModule.ps1` SHALL 仅对 `PSModulePath` 执行去重操作,不向其中追加项目父目录等额外路径,以减少命令发现阶段的目录扫描开销。
96+
97+
#### Scenario: 不添加额外模块路径
98+
99+
- **WHEN** Profile 加载模块
100+
- **THEN** SHALL 不将项目父目录追加到 `PSModulePath`
101+
102+
#### Scenario: 保留去重逻辑
103+
104+
- **WHEN** `PSModulePath` 中存在重复路径
105+
- **THEN** SHALL 去除重复条目
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
## 1. Tab 补全与 Prompt 性能修复
2+
3+
- [ ] 1.1 在 `profile/features/environment.ps1` 中将 starship 的 `Invoke-WithFileCache` Generator 从 `{ & starship init powershell }` 改为 `{ & starship init powershell --print-full-init }`,使缓存包含完整初始化脚本
4+
- [ ] 1.2 删除现有的 `profile/.cache/starship-init-powershell.ps1` 缓存文件,强制下次加载时重建
5+
- [ ] 1.3 在 `profile/core/encoding.ps1` 中将 `Set-PSReadLineKeyHandler -Key Tab -Function MenuComplete` 改为 `Set-PSReadLineKeyHandler -Key Tab -Function Complete`
6+
7+
## 2. 编码初始化优化
8+
9+
- [ ] 2.1 在 `profile/core/encoding.ps1` 中移除 `Get-Command -Name Set-PSReadLineKeyHandler` 检查,直接调用 `Set-PSReadLineKeyHandler`(PowerShell 7+ 内置 PSReadLine)
10+
- [ ] 2.2 将 `Get-Command -Name Register-FzfHistorySmartKeyBinding` 改为 `Get-Command -Name Register-FzfHistorySmartKeyBinding -CommandType Function`
11+
12+
## 3. 工具检测批量化
13+
14+
- [ ] 3.1 在 `profile/features/environment.ps1``Initialize-Environment` 中,将工具初始化循环前的逐个 `Test-EXEProgram` 替换为单次 `Get-Command -Name @('starship','zoxide','sccache','fnm') -CommandType Application` 批量查询
15+
- [ ] 3.2 将批量查询结果存入 HashSet,后续用 `$availableTools.Contains($name)` 替代 `Test-EXEProgram` 调用
16+
- [ ] 3.3 保留工具未安装时的提示逻辑不变
17+
18+
## 4. 代理探测优化
19+
20+
- [ ] 4.1 在 `psutils/modules/proxy.psm1``Set-Proxy auto` 中将 TCP 超时从 100ms 缩短为 50ms
21+
- [ ] 4.2 在 `Set-Proxy on` 中移除二次端口检测(200ms timeout 的 TCP 连接),改为直接设置环境变量
22+
- [ ] 4.3 在 `Initialize-Environment` 中为 `Set-Proxy auto` 包装 `Invoke-WithCache` 缓存层,缓存有效期 5 分钟
23+
24+
## 5. PSModulePath 精简
25+
26+
- [ ] 5.1 在 `profile/core/loadModule.ps1` 中移除将项目父目录 `$moduleParent` 追加到 `PSModulePath` 的逻辑
27+
- [ ] 5.2 保留 PSModulePath 去重逻辑
28+
29+
## 6. 分阶段计时诊断
30+
31+
- [ ] 6.1 在 `profile/profile.ps1` 中用 `[System.Diagnostics.Stopwatch]` 替换 `Get-Date` 计时
32+
- [ ] 6.2 在关键阶段(模块加载、代理检测、工具初始化、别名注册)插入计时点
33+
- [ ] 6.3 实现 `$script:ProfileTimings` 变量存储各阶段耗时
34+
- [ ] 6.4 实现 `POWERSHELL_PROFILE_TIMING=1` 环境变量控制的详细计时输出
35+
- [ ] 6.5 默认模式下通过 `Write-Verbose` 输出计时信息
36+
37+
## 7. 验证与测试
38+
39+
- [ ] 7.1 在 Windows 上验证 Full 模式加载时间降至 ~1s 以内
40+
- [ ] 7.2 验证 Tab 补全响应速度恢复正常(即时响应)
41+
- [ ] 7.3 验证 starship prompt 正常显示(缓存重建后不再每次 spawn 进程)
42+
- [ ] 7.4 验证 Minimal 和 UltraMinimal 模式行为不变
43+
- [ ] 7.5 验证所有别名、函数、环境变量在 Full 模式下仍可用
44+
- [ ] 7.6 运行 `pnpm test:profile` 确保 profile 测试通过
45+
- [ ] 7.7 运行 `pnpm qa` 确保整体质量

0 commit comments

Comments
 (0)