Initial commit — leak-hunt project complete
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>
This commit is contained in:
commit
57b5e43d0e
199 changed files with 1648333 additions and 0 deletions
105
bin/snapshot_leakers.ps1
Normal file
105
bin/snapshot_leakers.ps1
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#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
|
||||
Loading…
Add table
Add a link
Reference in a new issue