522 lines
27 KiB
PowerShell
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 个项目
|
|
- 保留原有列表结构
|
|
|
|

|
|
|
|
'@
|
|
$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 }
|