Files
pebrel/scripts/markdown-memory-stress.ps1
T

522 lines
27 KiB
PowerShell

<#
.SYNOPSIS
Exercise a dedicated Windows Markdown window and record process/GPU memory.
.DESCRIPTION
Generates original formulas, tables, code, lists and PNGs. HTTP images are served
only from generated assets on loopback. Samples private working set, total
working set and private commit separately; none is added to GPU counters.
No working-set trimming is used.
The default edit coordinates reproduce the fixture at 144 DPI and a 1938x1103
window. They scale with window DPI. Inspect first-cycle screenshots when changing
fonts, UI layout or the fixture; a click count alone does not prove editing.
Only this script is required. Reports, fixtures and screenshots belong in tmp/.
Run editing in a dedicated desktop session. Losing foreground focus stops the
edit run; screenshots capture only the owned window, including when occluded.
.EXAMPLE
./scripts/markdown-memory-stress.ps1 -Executable D:/build/pebrel.exe -BuildProfile release
.EXAMPLE
./scripts/markdown-memory-stress.ps1 -GenerateOnly -OutputDirectory D:/temp/markdown-fixture
#>
[CmdletBinding()]
param(
[string]$Executable,
[string]$OutputDirectory,
[ValidateSet('release','dev','unknown')][string]$BuildProfile = 'unknown',
[ValidateRange(1,1024)][int]$Sections = 128,
[ValidateRange(1,10)][int]$Rounds = 3,
[ValidateRange(1,200)][int]$Cycles = 40,
[ValidateRange(1,10000)][int]$ScrollSteps = 550,
[ValidateRange(80,2000)][int]$StepMilliseconds = 180,
[ValidateRange(3,60)][int]$IdleSeconds = 20,
[double]$WorkingSetLimitMiB = 100,
[double]$TerminalLimitMiB = 70,
[string[]]$EditTargets = @('formula:1130:358','table:645:508','code:750:632','list:720:834','inline-formula:756:885'),
[switch]$GenerateOnly,
[switch]$TerminalOnly,
[switch]$SkipEditing,
[ValidateSet('run','sample','serve')][string]$Mode = 'run',
[int]$OwnerId = 0
)
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
$utf8 = [Text.UTF8Encoding]::new($false)
if (-not $OutputDirectory) {
$OutputDirectory = Join-Path (Split-Path $PSScriptRoot) ('tmp/markdown-memory-' + (Get-Date -Format 'yyyyMMdd-HHmmss'))
}
$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
function Write-Utf8([string]$Path, [string]$Text) { [IO.File]::WriteAllText($Path, $Text, $utf8) }
function Append-Json([string]$File, $Value) {
[IO.File]::AppendAllText((Join-Path $OutputDirectory $File), ($Value | ConvertTo-Json -Compress -Depth 6) + "`n", $utf8)
}
function Median($Values) {
$ordered = @($Values | Sort-Object)
if ($ordered.Count -eq 0) { return $null }
$middle = [int][Math]::Floor($ordered.Count/2)
if ($ordered.Count%2) { return $ordered[$middle] }
return ($ordered[$middle-1]+$ordered[$middle])/2
}
if ($Mode -eq 'serve') {
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
$listener.Start()
Write-Utf8 (Join-Path $OutputDirectory 'http-port.txt') ([string]$listener.LocalEndpoint.Port)
try {
while (-not (Test-Path (Join-Path $OutputDirectory 'stop-server'))) {
if (-not $listener.Pending()) { Start-Sleep -Milliseconds 25; continue }
$client = $listener.AcceptTcpClient()
try {
$stream = $client.GetStream()
$stream.ReadTimeout = 2000
$reader = [IO.StreamReader]::new($stream, [Text.Encoding]::ASCII, $false, 1024, $true)
$request = $reader.ReadLine()
while ($reader.ReadLine()) {}
$reader.Dispose()
$status = '404 Not Found'
$body = [byte[]]@()
if ($request -match '^GET /(image-[0-9]{2}\.png)(?:\?[^ ]*)? HTTP/1\.[01]$') {
$path = Join-Path (Join-Path $OutputDirectory 'assets') $Matches[1]
if (Test-Path -LiteralPath $path) { $body = [IO.File]::ReadAllBytes($path); $status = '200 OK' }
}
$headers = [Text.Encoding]::ASCII.GetBytes("HTTP/1.1 $status`r`nContent-Type: image/png`r`nContent-Length: $($body.Length)`r`nConnection: close`r`n`r`n")
$stream.Write($headers, 0, $headers.Length)
$stream.Write($body, 0, $body.Length)
Append-Json 'http-requests.jsonl' @{time=(Get-Date).ToString('o');request=$request;status=$status}
} catch { Append-Json 'http-errors.jsonl' @{error=$_.Exception.Message} }
finally { $client.Dispose() }
}
} finally { $listener.Stop() }
exit
}
if ($Mode -eq 'sample') {
$timer = [Diagnostics.Stopwatch]::StartNew()
$gpuAvailable = $true
$ready = $false
while (-not (Test-Path (Join-Path $OutputDirectory "stop-sampling-$OwnerId"))) {
$target = Get-Process -Id $OwnerId -ErrorAction SilentlyContinue
if ($null -eq $target) { break }
$target.Refresh()
$phaseFile = Join-Path $OutputDirectory 'phase.txt'
$phase = if (Test-Path $phaseFile) { (Get-Content $phaseFile -Raw).Trim() } else { 'startup' }
$dedicated = $null; $shared = $null
$gpuTimer = [Diagnostics.Stopwatch]::StartNew()
if ($gpuAvailable) {
try {
$gpu = @(Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUProcessMemory -Filter "Name LIKE 'pid_${OwnerId}_%'" -ErrorAction Stop)
if ($gpu.Count) {
$dedicated = [long](($gpu | Measure-Object DedicatedUsage -Sum).Sum)
$shared = [long](($gpu | Measure-Object SharedUsage -Sum).Sum)
}
} catch { $gpuAvailable = $false; Append-Json 'limitations.jsonl' @{gpu=$_.Exception.Message} }
}
$gpuTimer.Stop()
$privateWorkingSet = $null
try {
$resident = @(Get-CimInstance Win32_PerfFormattedData_PerfProc_Process -Filter "IDProcess=$OwnerId" -ErrorAction Stop)
if ($resident.Count -ne 1 -or $null -eq $resident[0].WorkingSetPrivate) {
throw 'The private working-set counter is missing or ambiguous.'
}
$privateWorkingSet = [long]$resident[0].WorkingSetPrivate
} catch { Append-Json 'limitations.jsonl' @{privateWorkingSet=$_.Exception.Message} }
# Counter discovery can be slow. Read the process and phase again
# afterwards so a delayed query cannot label a new phase as an old one.
$target.Refresh()
$phase = if (Test-Path $phaseFile) { (Get-Content $phaseFile -Raw).Trim() } else { 'startup' }
$panic = Test-Path (Join-Path $OutputDirectory 'pebrel-panic.log')
[pscustomobject]@{
time=(Get-Date).ToString('o');pid=$OwnerId;seconds=$timer.Elapsed.TotalSeconds;phase=$phase
privateBytes=$target.PrivateMemorySize64;workingSetBytes=$target.WorkingSet64
privateWorkingSetBytes=$privateWorkingSet
peakWorkingSetBytes=$target.PeakWorkingSet64;cpuSeconds=$target.TotalProcessorTime.TotalSeconds
handles=$target.HandleCount;threads=$target.Threads.Count
gpuDedicatedBytes=$dedicated;gpuSharedBytes=$shared;valid=(-not $panic)
gpuQueryMilliseconds=$gpuTimer.Elapsed.TotalMilliseconds
} | Export-Csv -NoTypeInformation -Encoding UTF8 -Append (Join-Path $OutputDirectory 'samples.csv')
if (-not $ready) {
Write-Utf8 (Join-Path $OutputDirectory 'sampler-ready') 'ready'
$ready = $true
}
if ($panic) { break }
Start-Sleep -Seconds 2
}
exit
}
if (Test-Path $OutputDirectory) { throw 'Use a new output directory; existing evidence is never overwritten.' }
$null = New-Item -ItemType Directory -Path (Join-Path $OutputDirectory 'assets') -Force
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-Type @'
using System;
using System.Runtime.InteropServices;
public static class MarkdownStressWindow {
[StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left,Top,Right,Bottom; }
[StructLayout(LayoutKind.Sequential)] public struct Point { public int X,Y; }
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h,out Rect r);
[DllImport("user32.dll")] public static extern bool ClientToScreen(IntPtr h,ref Point p);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern short GetAsyncKeyState(int key);
[DllImport("user32.dll")] public static extern uint MapVirtualKey(uint code,uint type);
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr h,IntPtr dc,uint flags);
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
[DllImport("user32.dll")] public static extern uint GetDpiForWindow(IntPtr h);
[DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr h,int x,int y,int w,int z,bool repaint);
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr h,int m,IntPtr w,IntPtr l);
}
'@
[void][MarkdownStressWindow]::SetProcessDPIAware()
$hostExecutable = (Get-Process -Id $PID).Path
function Start-Worker([string]$WorkerMode, [int]$Id = 0) {
Start-Process $hostExecutable -PassThru -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File',('"' + $PSCommandPath + '"'),'-Mode',$WorkerMode,'-OutputDirectory',('"' + $OutputDirectory + '"'),'-OwnerId',$Id) -WindowStyle Hidden
}
function Phase([string]$Name) {
Write-Utf8 (Join-Path $OutputDirectory 'phase.txt') $Name
Append-Json 'actions.jsonl' @{time=(Get-Date).ToString('o');phase=$Name}
Write-Output $Name
}
function Assert-Running {
$script:process.Refresh()
if ($script:process.HasExited -or (Test-Path (Join-Path $OutputDirectory 'pebrel-panic.log'))) {
throw 'The owned test process exited or panicked; automation has stopped.'
}
}
function Pause-Checked([int]$Milliseconds) {
$remaining = $Milliseconds
while ($remaining -gt 0) {
Assert-Running
$step = [Math]::Min(200, $remaining)
Start-Sleep -Milliseconds $step
$remaining -= $step
}
}
function Key([int]$Code) {
Assert-Running
$scan = [long][MarkdownStressWindow]::MapVirtualKey($Code,0)
$down = 1 -bor ($scan -shl 16)
[void][MarkdownStressWindow]::PostMessage($script:handle,0x100,[IntPtr]$Code,[IntPtr]$down)
[void][MarkdownStressWindow]::PostMessage($script:handle,0x101,[IntPtr]$Code,[IntPtr]($down -bor 0xC0000000L))
}
function Assert-EditingFocus {
if ([MarkdownStressWindow]::GetForegroundWindow() -ne $script:handle) {
throw 'Editing lost foreground focus. Use a dedicated desktop session, or -SkipEditing for scrolling only.'
}
foreach ($key in @(16,17,18,91,92)) {
if ([MarkdownStressWindow]::GetAsyncKeyState($key) -lt 0) { throw 'Physical modifier input interrupted automation.' }
}
}
function Click([int]$X,[int]$Y,[switch]$Middle) {
Assert-Running
$position = [IntPtr](([int]($Y*$script:scale) -shl 16) -bor [int]($X*$script:scale))
[void][MarkdownStressWindow]::PostMessage($script:handle,0x200,[IntPtr]0,$position)
$down = if ($Middle) { 0x207 } else { 0x201 }
$up = if ($Middle) { 0x208 } else { 0x202 }
$button = if ($Middle) { 16 } else { 1 }
[void][MarkdownStressWindow]::PostMessage($script:handle,$down,[IntPtr]$button,$position)
[void][MarkdownStressWindow]::PostMessage($script:handle,$up,[IntPtr]0,$position)
}
function Capture([string]$Name) {
Assert-Running
Pause-Checked 150
$rect = New-Object MarkdownStressWindow+Rect
[void][MarkdownStressWindow]::GetWindowRect($script:handle,[ref]$rect)
$bitmap = New-Object Drawing.Bitmap ($rect.Right-$rect.Left),($rect.Bottom-$rect.Top)
$graphics = [Drawing.Graphics]::FromImage($bitmap)
try {
$dc = $graphics.GetHdc()
try { $captured = [MarkdownStressWindow]::PrintWindow($script:handle,$dc,2) }
finally { $graphics.ReleaseHdc($dc) }
if (-not $captured) { throw 'The owned window could not be captured.' }
$bitmap.Save((Join-Path $OutputDirectory ($Name + '.png')),[Drawing.Imaging.ImageFormat]::Png)
} finally { $graphics.Dispose(); $bitmap.Dispose() }
}
function Check-Accessibility {
Assert-Running
$element = [Windows.Automation.AutomationElement]::FromHandle($script:handle)
$nodes = $element.FindAll([Windows.Automation.TreeScope]::Descendants,[Windows.Automation.Condition]::TrueCondition)
Append-Json 'accessibility.jsonl' @{time=(Get-Date).ToString('o');nodes=$nodes.Count;pid=$script:process.Id}
Pause-Checked 500
}
function Assert-Surface([bool]$DocumentExpected) {
Assert-Running
$element = [Windows.Automation.AutomationElement]::FromHandle($script:handle)
$nodes = $element.FindAll([Windows.Automation.TreeScope]::Descendants,[Windows.Automation.Condition]::TrueCondition)
$save = @($nodes | Where-Object { $_.Current.Name -eq '保存' -and $_.Current.ControlType -eq [Windows.Automation.ControlType]::Button })
$dialogs = @($nodes | Where-Object { $_.Current.ControlType -eq [Windows.Automation.ControlType]::Window })
if ($dialogs.Count -or (($save.Count -gt 0) -ne $DocumentExpected)) {
throw 'The requested document/terminal surface was not reached, or a modal dialog is open.'
}
Append-Json 'surface-checks.jsonl' @{time=(Get-Date).ToString('o');document=$DocumentExpected;nodes=$nodes.Count}
}
function Scroll([int]$Delta) {
Key 27
$point = New-Object MarkdownStressWindow+Point
$point.X = [int](1000*$script:scale); $point.Y = [int](700*$script:scale)
[void][MarkdownStressWindow]::PostMessage($script:handle,0x200,[IntPtr]0,[IntPtr](($point.Y -shl 16) -bor $point.X))
[void][MarkdownStressWindow]::ClientToScreen($script:handle,[ref]$point)
$position = [IntPtr](($point.Y -shl 16) -bor ($point.X -band 65535))
for ($step = 0; $step -lt $ScrollSteps; $step++) {
Assert-Running
[void][MarkdownStressWindow]::PostMessage($script:handle,0x20A,[IntPtr]($Delta -shl 16),$position)
Pause-Checked $StepMilliseconds
}
}
$server = $null; $sampler = $null; $script:process = $null
$oldConfig = $env:PEBREL_CONFIG_DIR; $oldDocument = $env:NEBULA_GPUI_OPEN_DOC
$success = $false
try {
for ($i = 0; $i -lt 32; $i++) {
$bitmap = New-Object Drawing.Bitmap 1024,640
$graphics = [Drawing.Graphics]::FromImage($bitmap)
for ($x = 0; $x -lt 1024; $x += 8) {
$brush = New-Object Drawing.SolidBrush ([Drawing.Color]::FromArgb(([int][Math]::Floor($x/8)+$i*13)%256,([int][Math]::Floor($x/4)+$i*19)%256,([int][Math]::Floor($x/16)+$i*29)%256))
$graphics.FillRectangle($brush,$x,0,8,640)
$brush.Dispose()
}
$bitmap.Save((Join-Path $OutputDirectory ('assets/image-{0:d2}.png' -f $i)),[Drawing.Imaging.ImageFormat]::Png)
$graphics.Dispose(); $bitmap.Dispose()
}
$server = Start-Worker 'serve'
for ($retry = 0; $retry -lt 100 -and -not (Test-Path (Join-Path $OutputDirectory 'http-port.txt')); $retry++) { Start-Sleep -Milliseconds 100 }
$port = [int](Get-Content (Join-Path $OutputDirectory 'http-port.txt'))
$intro = @'
# 混合文档内存检查
反复点击公式、表格、代码和列表,观察编辑控件的创建与回收。
$$
E = mc^2
$$
| 内容 | 操作 |
| :--- | :--- |
| 单元格 | 直接输入 |
```rust
fn check() {
let value = 42;
}
```
- [ ] 任务项目
- 普通列表项目
行内公式 $a^2 + b^2 = c^2$,以及 [图片链接](http://127.0.0.1:PORT/image-00.png)。
'@
$template = @'
## 内容段落 NUMBER
这是第 NUMBER 组独立内容,包含 **强调**、*说明*、`行内代码` 和 [图片链接](assets/IMAGE)。
行内表达式 $x_{NUMBER} + y = NEXT$。
$$
\frac{1}{NUMBER} \sum_{k=1}^{NEXT} k^2
$$
$$
\int_0^1 x^{POWER} \, dx = \frac{1}{DENOMINATOR} \quad n=NUMBER
$$
| 项目 | 内容 | 次数 |
| :--- | :--- | ---: |
| 编辑 | 单元格保持边框 | NUMBER |
| 滚动 | 只挂载可见内容 | NEXT |
```rust
fn item_INDEX() -> usize {
let values = [1, 2, 3, NUMBER];
values.iter().sum()
}
```
- [ ] 检查第 NUMBER 个项目
- 保留原有列表结构
![生成的测试图片 NUMBER](URI)
'@
$source = [Text.StringBuilder]::new($intro.Replace('PORT',[string]$port))
for ($i = 0; $i -lt $Sections; $i++) {
$name = 'image-{0:d2}.png' -f ($i%32)
$uri = if ($i%2) { "http://127.0.0.1:$port/${name}?image=$i" } else { "assets/$name" }
[void]$source.Append($template.Replace('NUMBER',[string]($i+1)).Replace('NEXT',[string]($i+2)).Replace('POWER',[string]($i%9+1)).Replace('DENOMINATOR',[string]($i%9+2)).Replace('INDEX',[string]$i).Replace('IMAGE',$name).Replace('URI',$uri))
}
$document = Join-Path $OutputDirectory 'mixed.md'
Write-Utf8 $document $source.ToString()
$manifest = [ordered]@{
runnerSha256=(Get-FileHash $PSCommandPath -Algorithm SHA256).Hash
sections=$Sections;displayFormulas=(2*$Sections+1);inlineFormulas=($Sections+1)
tables=($Sections+1);codeBlocks=($Sections+1);images=$Sections;imagePixels=@(1024,640)
rounds=$Rounds;editCycles=$Cycles;editTargets=$EditTargets;skipEditing=[bool]$SkipEditing
scrollSteps=$ScrollSteps;stepMilliseconds=$StepMilliseconds;profile=$BuildProfile
fixtureSha256=(Get-FileHash $document -Algorithm SHA256).Hash
workingSetLimitMiB=$WorkingSetLimitMiB;terminalLimitMiB=$TerminalLimitMiB;terminalOnly=[bool]$TerminalOnly
measurement='Separate private working set, total working set, private commit and GPU counters; HTTP loopback, no WAN latency claim.'
}
if ($GenerateOnly) { $success = $true; Write-Utf8 (Join-Path $OutputDirectory 'manifest.json') ($manifest | ConvertTo-Json -Depth 6); return }
$Executable = (Resolve-Path -LiteralPath $Executable).Path
$manifest.executable = $Executable
$manifest.executableSha256 = (Get-FileHash $Executable -Algorithm SHA256).Hash
$manifest.os = (Get-CimInstance Win32_OperatingSystem | Select-Object Caption,Version,TotalVisibleMemorySize)
Write-Utf8 (Join-Path $OutputDirectory 'manifest.json') ($manifest | ConvertTo-Json -Depth 6)
Write-Utf8 (Join-Path $OutputDirectory 'pebrel_settings.txt') (@('language=zh-CN','theme=SilverLight','opacity=1','blur=off','restore_session=false','resume_ai=false','windowing_behavior=UseNew','panel_resize=on','auto_check_updates=off',"startup_directory=$OutputDirectory") -join "`n")
$env:PEBREL_CONFIG_DIR = $OutputDirectory
$env:NEBULA_GPUI_OPEN_DOC = if ($TerminalOnly) { $null } else { $document }
$script:process = Start-Process $Executable -WorkingDirectory $OutputDirectory -PassThru -ArgumentList '-e','cmd.exe','/d','/k','prompt MemoryCheck' -RedirectStandardOutput (Join-Path $OutputDirectory 'stdout.log') -RedirectStandardError (Join-Path $OutputDirectory 'stderr.log')
Write-Utf8 (Join-Path $OutputDirectory 'pid.txt') ([string]$script:process.Id)
for ($retry = 0; $retry -lt 100; $retry++) {
Assert-Running
if ($script:process.MainWindowHandle -ne 0) { break }
Start-Sleep -Milliseconds 100
}
$script:handle = $script:process.MainWindowHandle
if ($script:handle -eq 0) { throw 'The test window did not appear.' }
$script:scale = [MarkdownStressWindow]::GetDpiForWindow($script:handle)/144.0
[void][MarkdownStressWindow]::MoveWindow($script:handle,0,0,[int](1938*$script:scale),[int](1103*$script:scale),$true)
$sampler = Start-Worker 'sample' $script:process.Id
# Let one counter query finish before starting measured stages. Initial GPU
# discovery can otherwise consume the entire first idle interval.
for ($retry = 0; $retry -lt 600 -and -not (Test-Path (Join-Path $OutputDirectory 'sampler-ready')); $retry++) {
if ($sampler.HasExited) { throw 'The memory sampler exited before its first sample.' }
Pause-Checked 100
}
if (-not (Test-Path (Join-Path $OutputDirectory 'sampler-ready'))) { throw 'The memory sampler did not become ready within 60 seconds.' }
if (-not $SkipEditing -and -not $TerminalOnly) {
if ([MarkdownStressWindow]::GetForegroundWindow() -ne $script:handle -and -not [MarkdownStressWindow]::SetForegroundWindow($script:handle)) {
throw 'Windows did not grant foreground focus to the edit-test window.'
}
Pause-Checked 100
Assert-EditingFocus
}
if ($TerminalOnly) {
Phase 'terminal-never-opened-markdown'
Pause-Checked ($IdleSeconds*1000)
Capture 'terminal-baseline'
$success = $true
return
}
Phase 'markdown-initial'
Pause-Checked ($IdleSeconds*1000)
Check-Accessibility
Assert-Surface $true
Capture 'initial'
for ($round = 1; $round -le $Rounds; $round++) {
if (-not $SkipEditing) {
Phase "round-$round-edit"
for ($cycle = 0; $cycle -lt $Cycles; $cycle++) {
foreach ($target in $EditTargets) {
Assert-EditingFocus
$parts = $target.Split(':')
Click ([int]$parts[1]) ([int]$parts[2])
Pause-Checked 100
[void][MarkdownStressWindow]::PostMessage($script:handle,0x102,[IntPtr]81,[IntPtr]1)
Pause-Checked 100
if ($cycle -eq 0) { Capture "round-$round-$($parts[0])-typed" }
Assert-EditingFocus
Key 8
Pause-Checked 100
Key 27
Pause-Checked 80
}
}
}
Phase "round-$round-down"
Scroll -600
Phase "round-$round-bottom"
Pause-Checked ($IdleSeconds*1000)
Capture "round-$round-bottom"
Check-Accessibility
Phase "round-$round-up"
Scroll 600
Phase "round-$round-top"
Pause-Checked ($IdleSeconds*1000)
Capture "round-$round-top"
}
Phase 'markdown-tab-inactive'
Click 140 170
Pause-Checked 500
Assert-Surface $false
Pause-Checked ($IdleSeconds*1000)
Capture 'cli-with-markdown-background'
Phase 'markdown-resumed'
Click 150 220
Pause-Checked 500
Assert-Surface $true
Pause-Checked ($IdleSeconds*1000)
Check-Accessibility
Capture 'resumed'
Phase 'verify-edit-restoration'
Click 1850 110
Pause-Checked 1000
if ((Get-FileHash $document -Algorithm SHA256).Hash -ne $manifest.fixtureSha256) {
throw 'Edit/delete did not restore the generated document. The saved draft is retained for diagnosis.'
}
Click 150 220 -Middle
Pause-Checked 500
Assert-Surface $false
Phase 'markdown-closed'
Pause-Checked ($IdleSeconds*1000)
Capture 'closed'
$success = $true
} catch {
$phasePath = Join-Path $OutputDirectory 'phase.txt'
$failedPhase = if ([IO.File]::Exists($phasePath)) { [IO.File]::ReadAllText($phasePath,$utf8).Trim() } else { 'startup' }
Append-Json 'failure.jsonl' @{time=(Get-Date).ToString('o');message=$_.Exception.Message;phase=$failedPhase}
Write-Warning $_.Exception.Message
} finally {
Write-Utf8 (Join-Path $OutputDirectory 'stop-server') 'stop'
if ($script:process) { Write-Utf8 (Join-Path $OutputDirectory "stop-sampling-$($script:process.Id)") 'stop' }
$samplingComplete = if ($sampler) { $sampler.WaitForExit(5000) -and $sampler.ExitCode -eq 0 } else { $false }
if ($server) { [void]$server.WaitForExit(3000) }
$env:PEBREL_CONFIG_DIR = $oldConfig; $env:NEBULA_GPUI_OPEN_DOC = $oldDocument
$rows = @()
$sampleFile = Join-Path $OutputDirectory 'samples.csv'
if (Test-Path $sampleFile) { $rows = @(Import-Csv $sampleFile | Where-Object valid -eq 'True') }
$phases = @($rows | Group-Object phase | ForEach-Object {
$working = @($_.Group | ForEach-Object { [double]$_.workingSetBytes/1MB } | Sort-Object)
$private = @($_.Group | ForEach-Object { [double]$_.privateBytes/1MB } | Sort-Object)
$privateWorking = @($_.Group | Where-Object privateWorkingSetBytes -ne '' | ForEach-Object { [double]$_.privateWorkingSetBytes/1MB } | Sort-Object)
$privateWorkingComplete = $privateWorking.Count -eq $_.Group.Count
$dedicated = @($_.Group | Where-Object gpuDedicatedBytes -ne '' | ForEach-Object { [double]$_.gpuDedicatedBytes/1MB })
$shared = @($_.Group | Where-Object gpuSharedBytes -ne '' | ForEach-Object { [double]$_.gpuSharedBytes/1MB })
$limit = if ($TerminalOnly -or $_.Name -in @('markdown-tab-inactive','markdown-closed')) { $TerminalLimitMiB } else { $WorkingSetLimitMiB }
[pscustomobject]@{
phase=$_.Name;samples=$working.Count
workingMedianMiB=(Median $working);workingMaxMiB=$working[-1]
privateMedianMiB=(Median $private);privateMaxMiB=$private[-1]
privateWorkingMedianMiB=(Median $privateWorking)
privateWorkingMaxMiB=if ($privateWorking.Count) { $privateWorking[-1] } else { $null }
privateWorkingSetComplete=$privateWorkingComplete
privateWorkingSetWithinTarget=($privateWorkingComplete -and $privateWorking[-1] -le $limit)
gpuDedicatedMedianMiB=(Median $dedicated);gpuSharedMedianMiB=(Median $shared)
processLimitMiB=$limit;bothProcessMetricsWithinTarget=($working[-1] -le $limit -and $private[-1] -le $limit)
}
})
$budget = if ($TerminalOnly) { $TerminalLimitMiB } else { $WorkingSetLimitMiB }
$overBudget = @($rows | Where-Object { [double]$_.workingSetBytes/1MB -gt $budget })
$processTargetsPassed = $phases.Count -gt 0 -and @($phases | Where-Object bothProcessMetricsWithinTarget -eq $false).Count -eq 0
$privateWorkingComplete = $phases.Count -gt 0 -and @($phases | Where-Object privateWorkingSetComplete -eq $false).Count -eq 0
$privateWorkingTargetsPassed = $phases.Count -gt 0 -and @($phases | Where-Object privateWorkingSetWithinTarget -eq $false).Count -eq 0
$requiredPhases = if ($TerminalOnly) { @('terminal-never-opened-markdown') } else {
@('markdown-initial','markdown-tab-inactive','markdown-resumed','markdown-closed') + @(for ($round = 1; $round -le $Rounds; $round++) { "round-$round-bottom"; "round-$round-top" })
}
$missingPhases = @($requiredPhases | Where-Object { $_ -notin $phases.phase })
$phaseCoverageComplete = $missingPhases.Count -eq 0
Write-Utf8 (Join-Path $OutputDirectory 'summary.json') (@{
completed=$success;samplingComplete=$samplingComplete;phases=$phases
phaseCoverageComplete=$phaseCoverageComplete;missingPhases=$missingPhases
workingSetLimitMiB=$budget;workingSetBudgetPassed=($success -and $samplingComplete -and $phaseCoverageComplete -and $rows.Count -gt 0 -and $overBudget.Count -eq 0)
privateWorkingSetSamplingComplete=$privateWorkingComplete
privateWorkingSetBudgetPassed=($success -and $samplingComplete -and $phaseCoverageComplete -and $privateWorkingTargetsPassed)
processMemoryBudgetPassed=($success -and $samplingComplete -and $phaseCoverageComplete -and $processTargetsPassed -and $privateWorkingTargetsPassed)
requiresVisualCoverageReview=$true;limitsAreTargetsNotGuarantees=$true
} | ConvertTo-Json -Depth 6)
Write-Output "Evidence: $OutputDirectory"
}
if (-not $success) { exit 1 }