Files
Shuery 45bbd66815 docs(report): 新增 PROJECT_REPORT.md 与 CI/CD 流水线记录
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。
2026-10-02 01:17:40 +08:00

140 lines
6.1 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 宿主性能基线:对 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