PROJECT_REPORT.md:论文式项目报告(摘要 / 目录 / 7 章 / 参考文献 / 致谢 / 附录 A),
4 张 Mermaid 图同样经真实浏览器渲染验证。所有数字都与证据文件逐个核对过。
.scratch/ci-cd/:本次流水线的全部证据与可重跑脚本
* 阶段报告:依赖安全扫描 / 许可证合规 / 阶段 0 静态分析 / 阶段 3 测试与性能
* 证据:分析器原始输出(修复前 84 条、修复后 80 条)、Pester 详细输出、
性能基线 JSON、黑盒用例结果(阶段 1 的 11 例与阶段 2 回归的 12 例)
* 可重跑:blackbox-tests.ps1 / blackbox-regression.ps1 / perf-baseline.ps1
两条边界必须写在明处,不能含糊:
* **VM 内验证只取到修复前的快照**(Pester 183 项全绿)。随后会话审批策略改为 never,
gsudo 提权被自动拒绝(退出码 999),而 Hyper-V 与 PowerShell Direct 都需要管理员,
于是修复后的三套件在 VM 内**没有跑成**。报告里明确标注了范围,没有把"宿主跑绿了"
说成"VM 也跑绿了"。
* **性能基线是首次建立**,无历史可比,故无退化可判;指标只在同机同宿主下对比,
跨机比数字没有意义。
另外记一笔:静态分析的口径是"仓库自己的门禁"。剩余 80 条全是风格类(0 Error),且逐条
有依据 —— 行长 160 是配置里写明的有意偏离,PSPlaceCloseBrace 等集中在测试夹具字符串内
(改了会改变断言语义)。严格模式的"零容忍"在这里与仓库自身约定相抵,选择尊重仓库约定
并在报告里登记,而不是制造一个横跨 12 个文件的纯排版大 diff。
140 lines
6.1 KiB
PowerShell
140 lines
6.1 KiB
PowerShell
# 宿主性能基线:对 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
|