* perf: reclaim launcher memory under system pressure * docs: record Caravan save-load OOM with spare swap * fix: wait for completed reclaim before increasing backoff
5.8 KiB
Low-memory reclaim and userspace compression
The Steam session monitor uses the existing libblsession.so page-out request to reclaim
private anonymous launcher memory. The default remains a request 20 seconds after game focus,
with five minutes between requests. With client compression enabled, low system headroom can
also trigger a request while a game is loading, before its first focused window.
Every five seconds the monitor reads MemAvailable, swap headroom and memory PSI. Low
headroom means less than one fifth of total RAM (bounded to 512–1536 MiB), or at least 1%
some avg10 memory stall with less than one third of RAM available. Pressure requests target
only the launcher processes already recognized by the monitor, never the game. They require
at least 128 MiB launcher RSS and enough free swap to cover its entire RSS while leaving
one fifth of swap (at least 256 MiB) free. The regular automatic client request uses the
same swap guard. An explicit session RAM cap retains its existing behavior.
Pressure requests are at least 30 seconds apart. Reclaiming less than 16 MiB doubles the
retry delay, up to five minutes. After game exit, speculative launcher readback is deferred
until the session is under any explicit RAM cap, pressure has subsided, and available RAM
can cover twice the launcher's swap while leaving the headroom reserve. Normal demand paging
continues while readback is deferred; Steam does not have to wait for the monitor to recover.
Missing memory counters disable automatic pressure reclaim and speculative readback. Missing
PSI alone still permits the MemAvailable fallback. No root settings, swap sizes, compression
algorithms, Android process priorities or game-specific settings are changed.
A bounded live test of the pressure helper on the FIT during Caravan SandWitch loading requested page-out at 20:52:37 UTC, 18 seconds before the installed timed reclaimer. Aggregate launcher RSS fell from 608 to 283 MiB across that request; this is summed process RSS, not a measurement of net physical RAM saved. The game reached its main menu at 60 FPS. The test ran the candidate monitor functions alongside the old APK's monitor, not a newly built APK; the old monitor's later request is visible separately at 20:52:55 in the session log. This does not establish gameplay performance or resolve Stellar Blade's larger memory failure.
Continuing an existing Caravan save reproduced a whole-session low-memory kill with the
pressure helper running. Android recorded ApplicationExitInfo reason LOW_MEMORY, and
lmkd killed the foreground app with oom_score_adj=0 because the device was not responding.
The last half-second sample before the kill still had 2,172,612 KiB swap free. Over the
captured interval, system unevictable memory grew from 1,955,932 to 3,236,276 KiB and shmem
from 1,690,276 to 2,965,900 KiB, while the game's private swap reached 1,916,136 KiB. The
pressure helper found the launcher already largely reclaimed: its first request at Continue
reduced aggregate launcher RSS only from 263 to 255 MiB and backed off. Thus launcher reclaim
does not resolve this load failure, and this run did not exhaust swap. The unevictable/shmem
growth needs allocation attribution before treating it as a driver or GPU-memory diagnosis.
CRAM and the bounded pager experiment
Meta's CRAM proposal uses memory compression hardware to allow byte/cacheline reads without software decompression faults. A userspace software pager cannot reproduce that hardware property. This is separate from Valve's device memory cgroup prioritization work.
tools/experiments/anonymous-memory-pager.c is an opt-in feasibility probe, not a preload or
runtime component. Build it for the guest architecture with a Linux compiler:
cc -O2 -Wall -Wextra -Werror -pthread tools/experiments/anonymous-memory-pager.c -ldl -o /tmp/anonymous-memory-pager
/tmp/anonymous-memory-pager 64
It requires liblz4.so.1 and UFFD_USER_MODE_ONLY. The requested arena is bounded to
1–256 MiB. Half its pages contain a repeating byte and half contain pseudorandom bytes;
these are synthetic data, not a game-memory compression estimate. The probe registers only
its own arena, packs compressed or incompressible pages into a separate store, discards the
original physical pages, then restores them on demand with UFFDIO_COPY. It checks the full
checksum and subsequent writes. A separate child checks kernel access to an evicted buffer.
The compressed store is retained until the probe ends; this is one eviction/restore cycle,
without hot/cold selection, concurrent eviction, lifecycle hooks or game integration.
On the Pocket FIT Elite's Android 6.6.82 kernel, tested from DroidDeck's unprivileged guest:
| Measurement | Result |
|---|---|
unrestricted userfaultfd |
EPERM |
| userspace-only creation and API negotiation | available |
| arena / compressed payload | 64 MiB / 33,767,424 bytes |
| process RSS before / after eviction | 68,864 / 36,340 KiB |
| restored pages / checksum / writes | 16,384 / intact / intact |
| resident scan / compress and evict / faulted scan | 46.851 / 17.289 / 446.524 ms |
| kernel pipe write from evicted buffer | EFAULT |
These timings include the full checksum scan and are one synthetic run, not a comparison
against zram or a game-performance result. The normal unrestricted pager interface was denied;
/dev/userfaultfd was also inaccessible. The kernel-access result is a hard integration
constraint: a transparent heap-wide pager cannot assume every access comes from userspace.
Wine/FEX signal handling, syscall buffers, shared and GPU memory, mapping changes, concurrent
writes and allocation overhead would all need a correctness design before enabling such a
pager in a real game. The experiment is deliberately excluded from runtime builds.