# ctrl/mem.sh ## Purpose How much memory this machine will actually give you before something dies. This is rig's memory tool, and the standalone rigmini.sh is generated from this file. There are two numbers and they are rarely the same. `status` reports what the machine ADVERTISES and what is quietly capping it. `push` finds what it will SURVIVE, by allocating until it stops. `all` does both and weighs the result against what this profile's cluster needs. The gap between them is the whole reason this exists. Under WSL the cap lives in .wslconfig; in a container or a managed workspace it is a cgroup limit, and there /proc/meminfo reports the HOST's memory while the kernel kills you at a fraction of it. A script that only read MemTotal would confidently report 32 GB on a box that OOMs at 2. Runs on native Linux and under WSL. On WSL the memory you see is a VM allocation that can be raised, and the commonest failure is raising it without restarting, so status compares what .wslconfig says with what actually booted. It reports and instructs. It never raises a limit, frees anything or installs a package. The one write it can make is `backup`, which copies .wslconfig beside itself, so that `restore` has something to put back after a hand edit. Usage: ``` mem.sh status what it has, what caps it mem.sh push [--to GB] [--to-oom] climb until it stops mem.sh all [--budget GB] both, then the verdict mem.sh backup | restore .wslconfig, WSL only ``` ## require_linux Windows outside WSL (Git Bash, MSYS, Cygwin) looks close enough to work and then fails in a pile of confusing ways: no /proc, no docker socket, none of the tooling. It is detectable, so name it instead. ## CG_MAX_FILE / CG_CUR_FILE Where a cgroup records this cgroup's own limit and usage. Set once by find_cgroup, because every later reading needs both, and hunting for the files on each call would be the slow part of the poll loop. ## find_cgroup Inside a container the cgroup namespace makes the top of the tree BE the container's own cgroup, so the unqualified path is already the right one. On a host it is the root cgroup, which is never limited; hence the second attempt via /proc/self/cgroup, which names the slice this shell is in. ## cgroup_cap_mb Returns the cap in MB, or "" when there is none worth reporting. cgroup v2 spells unlimited "max"; v1 spells it as a number near 2^63, which is why this compares against MemTotal rather than testing for a magic value. A "limit" above the machine's own memory is not a limit, however it is written. ## headroom_mb How much room is left RIGHT NOW, from whichever accounting actually governs. In a capped container /proc/meminfo describes the host and is worse than useless for this: it would report tens of gigabytes free on a box that is one allocation from being killed. ## wslconfig_path /mnt/c/Users can hold several real accounts (a renamed login leaves the old directory behind), so picking the first alphabetically is a coin toss. Ask Windows, then fall back to whichever profile actually owns a config. ## status: overcommit overcommit_memory=0 is the default heuristic: a large allocation is granted on a guess, and the reckoning arrives later as an OOM kill rather than as a failed malloc. It is why `push` touches every page it asks for. ## status: WSL WSL keeps its cap on the Windows side, in a file this shell can read but not usefully apply: the change costs a full VM restart. Report it, and report the commonest mistake, which is editing it and not restarting. ## backup Backups are timestamped and never overwritten: a backup that can destroy itself on a second run is not a backup. ## restore Newest is the right default (undo the last edit), but if you backed up *after* editing, the state you want is older. The rest are shown so a no-op restore is obviously a no-op rather than a mystery. ## allocator The child allocates and stops itself; the parent only watches. That split is the point: under --to-oom the allocating process is expected to be killed, and something has to survive to say how far it got. ### OOM score The child raises its own OOM score to the maximum so the kernel picks THIS process first. Raising needs no privilege (only lowering does). Without it, the kernel is free to choose your shell, your ssh session or dockerd; on a box you are still using, that is not an acceptable coin toss. ### Writing straight into the array element Each chunk is written STRAIGHT INTO the array element (`printf -v "arr[$i]"`). The obvious spelling, building one chunk and `arr+=("$chunk")`, costs three copies per step, not one: the template stays resident, expanding "$chunk" makes a temporary word, and the append makes the element. A 128 MB step then needs 384 MB transiently, and on a small box it is killed on the first append while reporting a third of the true ceiling. printf -v into a subscript also means every page is written, so it is resident rather than merely promised: the only kind of allocation that measures anything under heuristic overcommit. ### First swap Worth calling out separately from the ceiling: this is where the box stops being fast and starts being unusable, which for a scheduler is a different and earlier problem than being killed. ## push: step size A step is worth about a sixty-fourth of the ceiling: enough resolution to find the edge, few enough lines to read, and small enough that the transient cost of one allocation never dominates a small box. A fixed size cannot do all three: 128 MB is fine on 16 GB and absurd on 512 MB. ## push: floor Stop with a cushion rather than riding it to the kill. How big a cushion depends on what it is protecting. Under a cgroup cap, running out kills only this container's own processes, so it need cover no more than the shell that prints the result, and a 512 MB cushion on a 1 GB box would halve the answer. On a host there is everything else to protect, and the OOM killer does not promise to pick the process that caused the problem. ## push: Ctrl-C INT kills the child and lets the summary print anyway, so an impatient Ctrl-C still tells you how far it got and, more importantly, still gives the memory back. ## push: claimed vs. measured The gap between the claim and the measurement is the finding, but only when the BOX chose where to stop. An empty $stop means the child was ended rather than deciding to end; anything else (--to, the floor) is a stop we asked for, and flagging those as short of the ceiling would put a warning on every deliberately small run.