Skip to content

Commit c2965c1

Browse files
committed
perf(profile): 实现模块延迟加载以优化启动性能
- 重构 loadModule.ps1 为三层加载策略:同步加载6个核心子模块,PSModulePath 兜底,OnIdle 事件延迟全量加载 - 将 wrapper.ps1 的加载移至 OnIdle 阶段,减少同步加载时间 - 优化环境变量检测函数,使用 .NET API 替代 Get-Item Env: - 改进 fnm 初始化,使用临时文件 dot-source 替代字符串解析 - 验证 core-loaders 阶段从 ~680ms 降至 ~77ms,总启动时间显著减少
1 parent eb205f9 commit c2965c1

5 files changed

Lines changed: 91 additions & 46 deletions

File tree

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
## 1. 环境变量检测 API 替换
22

3-
- [ ] 1.1 将 `profile/core/mode.ps1``Test-EnvSwitchEnabled` 函数的 `Get-Item -Path "Env:$Name"` 替换为 `[System.Environment]::GetEnvironmentVariable($Name)`,保持返回值语义不变
4-
- [ ] 1.2 将 `profile/core/mode.ps1``Test-EnvValuePresent` 函数的 `Get-Item -Path "Env:$Name"` 替换为 `[System.Environment]::GetEnvironmentVariable($Name)`,保持返回值语义不变
5-
- [ ] 1.3 验证 `Get-ProfileModeDecision` 在各种环境变量组合下的行为与替换前一致(Full / Minimal / UltraMinimal / Codex 自动降级 / 默认)
3+
- [x] 1.1 将 `profile/core/mode.ps1``Test-EnvSwitchEnabled` 函数的 `Get-Item -Path "Env:$Name"` 替换为 `[System.Environment]::GetEnvironmentVariable($Name)`,保持返回值语义不变
4+
- [x] 1.2 将 `profile/core/mode.ps1``Test-EnvValuePresent` 函数的 `Get-Item -Path "Env:$Name"` 替换为 `[System.Environment]::GetEnvironmentVariable($Name)`,保持返回值语义不变
5+
- [x] 1.3 验证 `Get-ProfileModeDecision` 在各种环境变量组合下的行为与替换前一致(Full / Minimal / UltraMinimal / Codex 自动降级 / 默认)
66

77
## 2. psutils 分层延迟加载
88

9-
- [ ] 2.1 改造 `profile/core/loadModule.ps1`:移除 `Import-Module psutils.psd1`,改为按依赖顺序 dot-source 6 个核心子模块(`os``cache``test``env``proxy``wrapper`
10-
- [ ] 2.2 在 `loadModule.ps1` 中将 psutils 模块父目录追加到 `$env:PSModulePath`(去重检查),作为自动加载兜底
11-
- [ ] 2.3 在 `loadModule.ps1` 中注册 `Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -MaxTriggerCount 1 -Action { Import-Module psutils.psd1 -Force -Global }`,实现空闲时全量加载
12-
- [ ] 2.4 为 OnIdle 事件的 Action 添加 `try/catch` 错误处理,失败时通过 `Write-Warning` 静默记录
13-
- [ ] 2.5 保留 `loadModule.ps1` 中的 PSModulePath 去重逻辑(现有的 `HashSet` 去重代码)
9+
- [x] 2.1 改造 `profile/core/loadModule.ps1`:移除 `Import-Module psutils.psd1`,改为按依赖顺序 Import-Module 6 个核心子模块(`os``cache``test``env``proxy``wrapper`
10+
- [x] 2.2 在 `loadModule.ps1` 中将 psutils 模块父目录追加到 `$env:PSModulePath`(去重检查),作为自动加载兜底
11+
- [x] 2.3 在 `loadModule.ps1` 中注册 `Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -MaxTriggerCount 1 -Action { Import-Module psutils.psd1 -Force -Global }`,实现空闲时全量加载
12+
- [x] 2.4 为 OnIdle 事件的 Action 添加 `try/catch` 错误处理,失败时通过 `Write-Warning` 静默记录
13+
- [x] 2.5 保留 `loadModule.ps1` 中的 PSModulePath 去重逻辑(现有的 `HashSet` 去重代码)
1414

1515
## 3. wrapper.ps1 延迟加载
1616

17-
- [ ] 3.1 将 `profile/core/loaders.ps1``wrapper.ps1` 的 dot-source 从同步阶段移到 OnIdle 事件中(与 psutils 全量加载合并在同一个 OnIdle Action 中)
18-
- [ ] 3.2 确认 `Set-AliasProfile` 所依赖的 `Set-CustomAlias` / `Get-CustomAlias` 来自 `wrapper.psm1`(核心子模块同步阶段已加载),不依赖 `wrapper.ps1`
17+
- [x] 3.1 将 `profile/core/loaders.ps1``wrapper.ps1` 的 dot-source 从同步阶段移到 OnIdle 事件中(与 psutils 全量加载合并在同一个 OnIdle Action 中)
18+
- [x] 3.2 确认 `Set-AliasProfile` 所依赖的 `Set-CustomAlias` / `Get-CustomAlias` 来自 `wrapper.psm1`(核心子模块同步阶段已加载),不依赖 `wrapper.ps1`
1919

20-
## 4. fnm 初始化缓存化
20+
## 4. fnm 初始化优化
2121

22-
- [ ] 4.1 将 `profile/features/environment.ps1` 中 fnm 初始化从 `fnm env --use-on-cd | Out-String | Invoke-Expression` 改为 `Invoke-WithFileCache -Key "fnm-init-powershell" -MaxAge ([TimeSpan]::FromDays(7)) -Generator { fnm env --use-on-cd } -BaseDir (Join-Path $profileRoot '.cache')` + dot-source 缓存文件
23-
- [ ] 4.2 验证 fnm 缓存文件内容可被正确 dot-source(环境变量设置和 `use-on-cd` hook 正常工作)
22+
- [x] 4.1 将 `profile/features/environment.ps1` 中 fnm 初始化从 `fnm env --use-on-cd | Out-String | Invoke-Expression` 改为临时文件 dot-source 方式(fnm env 输出包含会话特定 multishell 临时路径,不适合长期缓存,改用临时文件 dot-source 替代字符串 Invoke-Expression)
23+
- [x] 4.2 验证 fnm env 输出为 PowerShell 语法(在 pwsh 进程中自动输出 `$env:` 语法而非 bash `export`),可正确 dot-source
2424

2525
## 5. 验证与回归测试
2626

27-
- [ ] 5.1 使用 `POWERSHELL_PROFILE_TIMING=1` 运行 profile,验证 `core-loaders` 阶段从 ~680ms 降至 ~230ms
28-
- [ ] 5.2 验证 Full 模式下总加载时间降至 ~1.1s 以下
29-
- [ ] 5.3 验证 prompt 显示后执行 `Get-Tree`(非核心函数)可正常工作(PSModulePath 自动加载兜底)
30-
- [ ] 5.4 验证 OnIdle 触发后所有 70+ 个 psutils 函数可 Tab 补全
31-
- [ ] 5.5 验证 UltraMinimal 模式行为不变(跳过所有模块加载)
32-
- [ ] 5.6 验证 Minimal 模式行为不变(加载模块但跳过工具和别名)
33-
- [ ] 5.7 运行 `pnpm test:fast` 确保现有 Pester 测试全部通过
27+
- [x] 5.1 使用 `POWERSHELL_PROFILE_TIMING=1` 运行 profile,验证 `core-loaders` 阶段从 ~680ms 降至 ~77ms(超额完成,节省 ~600ms)
28+
- [x] 5.2 验证 `mode-decision` 阶段从 ~88ms 降至 ~47ms(节省 ~41ms)。总加载时间受 `initialize-environment` 外部操作(Sync-PathFromBash、fnm、starship、proxy)制约,冷启动约 1750ms,热缓存约 970ms
29+
- [x] 5.3 验证 prompt 显示后执行 `Get-Tree`(非核心函数)可正常工作(PSModulePath 自动加载兜底),source 显示为 `psutils`
30+
- [ ] 5.4 验证 OnIdle 触发后所有 70+ 个 psutils 函数可 Tab 补全(需在交互式 shell 中手动验证)
31+
- [x] 5.5 验证 UltraMinimal 模式行为不变(跳过所有模块加载)
32+
- [x] 5.6 验证 Minimal 模式行为不变(加载模块但跳过工具和别名)
33+
- [x] 5.7 运行 `pnpm test:fast` 确保现有 Pester 测试全部通过(324 passed, 0 failed)

profile/core/loadModule.ps1

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,49 @@
11
$moduleParent = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
22
$psutilsRoot = Join-Path $moduleParent 'psutils'
33
$moduleManifest = Join-Path $psutilsRoot 'psutils.psd1'
4+
$modulesDir = Join-Path $psutilsRoot 'modules'
45

5-
try {
6-
Import-Module $moduleManifest -ErrorAction Stop
7-
}
8-
catch {
9-
Write-Error "[profile/core/loadModule.ps1] Import-Module 失败: $moduleManifest :: $($_.Exception.Message)"
10-
throw
6+
# ── 第 1 层:同步加载核心子模块(仅 profile 启动路径必需的 6 个) ──
7+
# 使用 Import-Module 逐个加载(.psm1 不支持 dot-source,必须走模块系统)
8+
# 加载顺序按依赖关系:os → cache(依赖 os) → test → env → proxy → wrapper
9+
$coreModules = @('os', 'cache', 'test', 'env', 'proxy', 'wrapper')
10+
foreach ($mod in $coreModules) {
11+
$modPath = Join-Path $modulesDir "$mod.psm1"
12+
try {
13+
Import-Module $modPath -Global -ErrorAction Stop
14+
}
15+
catch {
16+
Write-Error "[profile/core/loadModule.ps1] Import-Module 失败: $modPath :: $($_.Exception.Message)"
17+
throw
18+
}
1119
}
1220

13-
# PSModulePath 去重(不追加额外路径,仅清理重复条目)
21+
# ── 第 2 层:PSModulePath 兜底(确保 PowerShell 自动发现 psutils.psd1) ──
22+
# 需要将 psutils 的父目录加入 PSModulePath(PowerShell 按 ModuleName/ModuleName.psd1 结构查找)
23+
$psutilsParent = Split-Path -Parent $psutilsRoot
1424
$sep = [System.IO.Path]::PathSeparator
15-
$paths = ($env:PSModulePath -split [string]$sep) | Where-Object { $_ }
16-
1725
$pathComparer = if ($IsWindows -or $env:OS -eq 'Windows_NT') {
1826
[System.StringComparer]::OrdinalIgnoreCase
1927
}
2028
else {
2129
[System.StringComparer]::Ordinal
2230
}
2331

32+
# 将 psutils 父目录追加到 PSModulePath(仅在尚未存在时)
33+
$currentPaths = ($env:PSModulePath -split [string]$sep) | Where-Object { $_ }
34+
$alreadyInPath = $false
35+
foreach ($p in $currentPaths) {
36+
if ($pathComparer.Equals($p, $psutilsParent)) {
37+
$alreadyInPath = $true
38+
break
39+
}
40+
}
41+
if (-not $alreadyInPath) {
42+
$env:PSModulePath = $env:PSModulePath + $sep + $psutilsParent
43+
}
44+
45+
# PSModulePath 去重(清理重复条目)
46+
$paths = ($env:PSModulePath -split [string]$sep) | Where-Object { $_ }
2447
$seenPaths = [System.Collections.Generic.HashSet[string]]::new($pathComparer)
2548
$uniquePaths = [System.Collections.Generic.List[string]]::new()
2649

@@ -32,3 +55,26 @@ foreach ($path in $paths) {
3255
}
3356

3457
$env:PSModulePath = ($uniquePaths.ToArray()) -join $sep
58+
59+
# ── 第 3 层:OnIdle 事件延迟全量加载(空闲时静默加载完整 psutils 模块) ──
60+
# 将 wrapper.ps1 的加载也合并到此处
61+
$script:__PsutilsManifestPath = $moduleManifest
62+
$script:__WrapperScriptPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'wrapper.ps1'
63+
Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -MaxTriggerCount 1 -Action {
64+
try {
65+
# 全量加载 psutils 模块(覆盖单独加载的子模块,补全其余子模块)
66+
Import-Module $script:__PsutilsManifestPath -Force -Global -ErrorAction Stop
67+
}
68+
catch {
69+
Write-Warning "[profile/loadModule.ps1] OnIdle psutils 全量加载失败: $($_.Exception.Message)"
70+
}
71+
try {
72+
# 延迟加载 wrapper.ps1(yaz, Add-CondaEnv 等函数)
73+
if (Test-Path $script:__WrapperScriptPath) {
74+
. $script:__WrapperScriptPath
75+
}
76+
}
77+
catch {
78+
Write-Warning "[profile/loadModule.ps1] OnIdle wrapper.ps1 加载失败: $($_.Exception.Message)"
79+
}
80+
} | Out-Null

profile/core/loaders.ps1

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,7 @@ $script:InvokeProfileCoreLoaders = {
1515
throw
1616
}
1717

18-
# 加载自定义函数包装 (yaz, Add-CondaEnv 等)
19-
$wrapperScript = Join-Path $profileRoot 'wrapper.ps1'
20-
try {
21-
. $wrapperScript
22-
}
23-
catch {
24-
Write-Error "[profile/profile.ps1] dot-source 失败: $wrapperScript :: $($_.Exception.Message)"
25-
throw
26-
}
18+
# wrapper.ps1 (yaz, Add-CondaEnv 等) 已移至 OnIdle 延迟加载(见 loadModule.ps1)
2719

2820
# 自定义别名配置(配置目录)
2921
$userAliasScript = Join-Path $profileRoot 'config/aliases/user_aliases.ps1'

profile/core/mode.ps1

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ function Test-EnvSwitchEnabled {
55
[string]$Name
66
)
77

8-
$item = Get-Item -Path "Env:$Name" -ErrorAction SilentlyContinue
9-
if (-not $item) { return $false }
10-
11-
$rawValue = [string]$item.Value
8+
$rawValue = [System.Environment]::GetEnvironmentVariable($Name)
9+
if ($null -eq $rawValue) { return $false }
1210
if ([string]::IsNullOrWhiteSpace($rawValue)) { return $false }
1311

1412
switch ($rawValue.Trim().ToLowerInvariant()) {
@@ -28,9 +26,9 @@ function Test-EnvValuePresent {
2826
[string]$Name
2927
)
3028

31-
$item = Get-Item -Path "Env:$Name" -ErrorAction SilentlyContinue
32-
if (-not $item) { return $false }
33-
return -not [string]::IsNullOrWhiteSpace([string]$item.Value)
29+
$rawValue = [System.Environment]::GetEnvironmentVariable($Name)
30+
if ($null -eq $rawValue) { return $false }
31+
return -not [string]::IsNullOrWhiteSpace($rawValue)
3432
}
3533

3634
function Get-ProfileModeDecision {

profile/features/environment.ps1

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,16 @@ function Initialize-Environment {
221221
# 仅 Unix:Node.js 版本管理器
222222
if ($IsWindows -or $SkipTools) { return }
223223
Write-Verbose "初始化 fnm Node.js 版本管理器"
224-
fnm env --use-on-cd | Out-String | Invoke-Expression
224+
# fnm env 输出包含会话特定的 multishell 临时路径,不适合长期缓存
225+
# 使用临时文件 dot-source 替代 Out-String | Invoke-Expression 以减少解析开销
226+
$fnmInitFile = Join-Path ([System.IO.Path]::GetTempPath()) "fnm-init-$PID.ps1"
227+
try {
228+
fnm env --use-on-cd | Set-Content -Path $fnmInitFile -Encoding utf8NoBOM
229+
. $fnmInitFile
230+
}
231+
finally {
232+
Remove-Item -Path $fnmInitFile -Force -ErrorAction SilentlyContinue
233+
}
225234
}
226235
}
227236

0 commit comments

Comments
 (0)