Five bugs identified and patched in retail Asheron's Call client: - v3b: palette refcount over-increment (3-byte NOP at two sites) - v5: RenderSurface PurgeResource no-op stub (vtable slot 2 thunk) - v11: two dangling-pointer crash guards (NULL-check + reorder) - v14: CEnvCell::Destroy ClipPlaneList leak (18-byte JMP to cleanup thunk) - v22: unpacker stale-pointer SEH guard (whole-function __try/__except) All five ship in leakfix.dll (117 KB, SHA d282f23c…) which is loaded by acclient.exe at process start via PE import table patching by tools/install_leakfix.py. Controlled 15-client fleet soak: unpatched control died at 26h with palette exhaustion; all 14 patched clients survived past that point and reached ≥5-day uptime. Residual ~15 MB/h growth traced to d3d9.dll's internal slab allocator (260KB surface backing buffers retained after Release). See REPORT.md §10 for the full investigation; conclusion is that it's unfixable from outside d3d9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
4 KiB
PowerShell
105 lines
4 KiB
PowerShell
#requires -Version 5.1
|
|
<#
|
|
snapshot_leakers.ps1
|
|
Takes a `procdump -r 1 -ma` clone-snapshot of the top-N leakiest
|
|
acclient processes that haven't been dumped recently. Designed to
|
|
be called every 30-60 min from the wakeup loop.
|
|
|
|
Decision rules:
|
|
* Skip a PID if there is already a procdump -e watcher attached
|
|
(it would block our snapshot attach; the watcher will catch OOM).
|
|
* Skip a PID if we already wrote a dump for it in the last
|
|
`$staleAfterMin` minutes.
|
|
* Rank by VirtualMemorySize64 descending. Take top $TopN.
|
|
* Output dir: artifacts/soak/ filename: dump_<pid>_<NNN>.dmp.
|
|
|
|
Tracks state in artifacts/soak/snapshot_state.json.
|
|
#>
|
|
|
|
param(
|
|
[int] $TopN = 2,
|
|
[int] $StaleAfterMin = 60,
|
|
[int] $MinVirtualMB = 1200,
|
|
[switch] $DryRun
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$root = Split-Path -Parent $PSScriptRoot
|
|
$soakDir = Join-Path $root 'artifacts\soak'
|
|
$procdump = 'C:\Users\acbot\Tools\Procdump\procdump.exe'
|
|
$state = Join-Path $soakDir 'snapshot_state.json'
|
|
|
|
if (-not (Test-Path $soakDir)) { New-Item -ItemType Directory -Path $soakDir -Force | Out-Null }
|
|
|
|
# Load state
|
|
if (Test-Path $state) {
|
|
$st = Get-Content $state -Raw | ConvertFrom-Json
|
|
} else {
|
|
$st = @{ snapshots = @() }
|
|
}
|
|
|
|
# What PIDs already have a procdump -e watcher attached?
|
|
$attachedTo = @()
|
|
$watcherCsv = Join-Path $soakDir 'watchers.csv'
|
|
if (Test-Path $watcherCsv) {
|
|
Import-Csv $watcherCsv | ForEach-Object {
|
|
$wp = [int]$_.watcher_pid
|
|
if (Get-Process -Id $wp -ErrorAction SilentlyContinue) {
|
|
$attachedTo += [int]$_.target_pid
|
|
}
|
|
}
|
|
}
|
|
|
|
# Rank live acclient processes by virtual mem
|
|
$candidates = Get-Process -Name acclient -ErrorAction SilentlyContinue |
|
|
Where-Object { [math]::Round($_.VirtualMemorySize64/1MB,0) -ge $MinVirtualMB -and $_.Id -notin $attachedTo } |
|
|
Sort-Object VirtualMemorySize64 -Descending |
|
|
Select-Object -First $TopN
|
|
|
|
$now = [DateTime]::UtcNow
|
|
foreach ($p in $candidates) {
|
|
# Skip if dumped recently
|
|
$existing = @($st.snapshots) | Where-Object { $_.pid -eq $p.Id } | Sort-Object ts_utc -Descending | Select-Object -First 1
|
|
if ($existing) {
|
|
$age = ($now - [DateTime]::Parse($existing.ts_utc)).TotalMinutes
|
|
if ($age -lt $StaleAfterMin) {
|
|
Write-Output ("skip pid={0} dumped {1:N1} min ago" -f $p.Id, $age)
|
|
continue
|
|
}
|
|
}
|
|
# Compute filename — increment N
|
|
$n = 1
|
|
while (Test-Path (Join-Path $soakDir ("dump_{0}_{1:000}.dmp" -f $p.Id, $n))) { $n++ }
|
|
$out = Join-Path $soakDir ("dump_{0}_{1:000}.dmp" -f $p.Id, $n)
|
|
$vMB = [math]::Round($p.VirtualMemorySize64/1MB,1)
|
|
$pMB = [math]::Round($p.PrivateMemorySize64/1MB,1)
|
|
if ($DryRun) {
|
|
Write-Output ("would dump pid={0} virtual={1}MB private={2}MB -> {3}" -f $p.Id, $vMB, $pMB, $out)
|
|
continue
|
|
}
|
|
Write-Output ("dump pid={0} virtual={1}MB private={2}MB" -f $p.Id, $vMB, $pMB)
|
|
$sw = [Diagnostics.Stopwatch]::StartNew()
|
|
& $procdump -r 1 -ma -accepteula $p.Id $out *> $null
|
|
$sw.Stop()
|
|
if (Test-Path $out) {
|
|
$sz = [math]::Round((Get-Item $out).Length/1MB,1)
|
|
Write-Output (" wrote {0} MB in {1:N1}s" -f $sz, $sw.Elapsed.TotalSeconds)
|
|
$entry = [PSCustomObject]@{
|
|
ts_utc = $now.ToString('o')
|
|
pid = $p.Id
|
|
start = $p.StartTime.ToUniversalTime().ToString('o')
|
|
uptime_h = [math]::Round(($now - $p.StartTime.ToUniversalTime()).TotalHours, 2)
|
|
ws_mb = [math]::Round($p.WorkingSet64/1MB,1)
|
|
private_mb = $pMB
|
|
virtual_mb = $vMB
|
|
path = $out
|
|
size_mb = $sz
|
|
elapsed_s = [math]::Round($sw.Elapsed.TotalSeconds, 1)
|
|
ust_likely = ($p.StartTime.ToUniversalTime() -gt [DateTime]::Parse('2026-05-13T06:07:43Z'))
|
|
}
|
|
$st.snapshots = @($st.snapshots) + $entry
|
|
} else {
|
|
Write-Output " DUMP FAILED — file not present"
|
|
}
|
|
}
|
|
$st | ConvertTo-Json -Depth 5 | Out-File -FilePath $state -Encoding utf8
|