# 宿主性能基线:对 BakNRet 的关键路径做可复现的计时。 # 设计原则:所有指标都是"比值"或"固定夹具下的绝对耗时",可以在同一台机器上重复对比。 param( [string]$OutJson = '.scratch\ci-cd\20260928-001722\perf_baseline.json', [int]$Repeat = 5, [string]$Pwsh = 'pwsh' ) $ErrorActionPreference = 'Stop' $root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path $modulePath = Join-Path $root 'BakNRet\BakNRet.psd1' $listPath = Join-Path $root 'BackupList.txt' $cfgPath = Join-Path $root 'BackupConfig.psd1' function Get-Median([double[]]$Values) { $s = $Values | Sort-Object $n = $s.Count if ($n -eq 0) { return 0 } if ($n % 2 -eq 1) { return $s[[int](($n - 1) / 2)] } return ($s[$n / 2 - 1] + $s[$n / 2]) / 2 } function Measure-Once([scriptblock]$Body) { $sw = [System.Diagnostics.Stopwatch]::StartNew() & $Body | Out-Null $sw.Stop() return $sw.Elapsed.TotalMilliseconds } $results = [ordered]@{ measuredAt = (Get-Date).ToString('s') machine = $env:COMPUTERNAME os = [System.Environment]::OSVersion.VersionString psi = $PSVersionTable.PSVersion.ToString() cpu = (Get-CimInstance Win32_Processor | Select-Object -First 1 -ExpandProperty Name) logicalCpu = [int](Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors workdir = $root metrics = @{} notes = @() } # ---------------------------------------------------------------- 夹具(固定内容,避免与真实数据联动) $fixture = Join-Path $env:TEMP ('baknret-perf-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) New-Item -ItemType Directory -Path $fixture -Force | Out-Null $srcDir = Join-Path $fixture 'src' New-Item -ItemType Directory -Path $srcDir -Force | Out-Null $sw = [System.Diagnostics.Stopwatch]::StartNew() $rnd = [Random]::new(20260928) $payload = New-Object byte[] 32768 for ($f = 0; $f -lt 200; $f++) { $sub = Join-Path $srcDir ('d{0:D2}' -f ($f % 10)) if (-not (Test-Path $sub)) { New-Item -ItemType Directory -Path $sub -Force | Out-Null } $rnd.NextBytes($payload) [System.IO.File]::WriteAllBytes((Join-Path $sub ("f$f.bin")), $payload) } $sw.Stop() $results.fixture = [ordered]@{ files = 200; bytes = 200 * 32768; createMs = [math]::Round($sw.Elapsed.TotalMilliseconds, 1) } # ---------------------------------------------------------------- 1. 模块导入 $imports = 1..$Repeat | ForEach-Object { Measure-Once { & $Pwsh -NoProfile -Command "Import-Module '$modulePath' -Force" } } $results.metrics.moduleImportMs = [ordered]@{ min = [math]::Round(($imports | Measure-Object -Minimum).Minimum, 1) median = [math]::Round((Get-Median $imports), 1) max = [math]::Round(($imports | Measure-Object -Maximum).Maximum, 1) samples = $Repeat } # ---------------------------------------------------------------- 2. 清单解析 $parseScript = @" Import-Module '$modulePath' -Force 1..50 | ForEach-Object { Get-Content -LiteralPath '$listPath' | ForEach-Object { ConvertFrom-BackupListLine -Line `$_ } } "@ $parseFile = Join-Path $fixture 'parse.ps1' Set-Content -LiteralPath $parseFile -Value $parseScript -Encoding utf8 $parses = 1..$Repeat | ForEach-Object { Measure-Once { & $Pwsh -NoProfile -File $parseFile } } $results.metrics.backupListParse50xMs = [ordered]@{ min = [math]::Round(($parses | Measure-Object -Minimum).Minimum, 1) median = [math]::Round((Get-Median $parses), 1) max = [math]::Round(($parses | Measure-Object -Maximum).Maximum, 1) samples = $Repeat } # ---------------------------------------------------------------- 3. 只读干跑(真实清单 + 空间预估 + manifest) $dryFile = Join-Path $fixture 'dry.ps1' Set-Content -LiteralPath $dryFile -Encoding utf8 -Value @" Set-Location '$root' & '$root\Backup-Data.ps1' -DryRun -BackupDir '$fixture\Backups' -ConfigPath '$cfgPath' *> `$null exit `$LASTEXITCODE "@ $dries = 1..$Repeat | ForEach-Object { Measure-Once { & $Pwsh -NoProfile -File $dryFile } } $results.metrics.dryRunFullListMs = [ordered]@{ min = [math]::Round(($dries | Measure-Object -Minimum).Minimum, 1) median = [math]::Round((Get-Median $dries), 1) max = [math]::Round(($dries | Measure-Object -Maximum).Maximum, 1) samples = $Repeat } # ---------------------------------------------------------------- 4. 7z 压缩吞吐(固定夹具,无压缩) $sevenZip = (Get-Command 7z -ErrorAction SilentlyContinue).Source if (-not $sevenZip) { foreach ($p in 'C:\Programs\Scoop\shims\7z.exe', 'C:\Program Files\7-Zip\7z.exe') { if (Test-Path $p) { $sevenZip = $p; break } } } if ($sevenZip) { $archive = Join-Path $fixture 'bench.7z' $bench = @() foreach ($lvl in 0, 5, 9) { if (Test-Path $archive) { Remove-Item $archive -Force } $ms = Measure-Once { & $sevenZip a -t7z "-mx=$lvl" -bso0 -bsp0 $archive (Join-Path $srcDir '*') } $bench += [pscustomobject]@{ level = $lvl; ms = [math]::Round($ms, 1); archiveBytes = (Get-Item $archive).Length } } $results.metrics.sevenZipBench = @($bench) $results.fixture.compressionInputBytes = 200 * 32768 $results.metrics.sevenZipVersion = (& $sevenZip | Select-Object -First 2 | Select-Object -Last 1) } else { $results.notes += '找不到 7z.exe,跳过压缩基准' } # ---------------------------------------------------------------- 5. 源树扫描(Get-BakNRetFolderSummary 走真实目录) $summaryScript = @" Import-Module '$modulePath' -Force `$sw = [System.Diagnostics.Stopwatch]::StartNew() 1..3 | ForEach-Object { Get-BakNRetFolderSummary -FolderPath '$srcDir' | Out-Null } `$sw.Stop() Write-Output ([math]::Round(`$sw.Elapsed.TotalMilliseconds / 3, 1)) "@ $sumFile = Join-Path $fixture 'summary.ps1' Set-Content -LiteralPath $sumFile -Value $summaryScript -Encoding utf8 $sumMs = (& $Pwsh -NoProfile -File $sumFile | Select-Object -Last 1) $results.metrics.folderSummary200Files3xMs = [double]$sumMs Remove-Item -LiteralPath $fixture -Recurse -Force -ErrorAction SilentlyContinue $results | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $root $OutJson) -Encoding utf8 Write-Host ('基线已写入 {0}' -f $OutJson) $results.metrics | ConvertTo-Json -Depth 6