fix(builder): account for available memory and compiler children on macOS #455
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!455
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/mem-gate"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type: fix · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks and memory probes, no game run) · Needs: Windows and Linux, no GPU needed (the memory checks and the suite); macOS 14.4
Why
Anyone building LS on a Mac: the builder's memory gate starts compilers only while there is memory for them, and on
macOS it reads nothing. Free memory comes back unknown, and a compiler's children count as 0 MB, so the gate admits
compilers by the worker limit alone, whatever memory is left. This PR gives the gate native macOS readers, part of the
macOS port (#420).
free_mb()andtree_rss()(nx2/build.py) read/procand Windows' memory status. On a Mac/procis missing, sofree_mb()returnsNoneandtree_rss()raises, which the gate counts as 0 MB.How to reproduce
Seen on: macOS 26.7.1, Apple M4 Max (
tests/test_mem_gate.pyandtests/test_mem_gate_children.pyon main). macOSonly: Windows and Linux read their own memory status.
Settings and switches: defaults (the memory gate on, as it ships).
Steps: on a Mac with main,
python -I -B tests/run_all.py mem_gate rss_liveness.Expected: the gate reads the memory available now and counts a compiler's children.
Actual:
FAIL: the free memory reads (None)andFAIL: a driver's child counts (0 MB read, the child holds 300 MB).What changed
On macOS, the builder's
free_mb()andtree_rss()now call native readers in a newnx2/darwin_memory.py:available memory from the kernel's VM counters, and a compiler tree's resident bytes from libproc. Every Windows and
Linux line is unchanged.
build.py, thendarwin_memory.py'sfree_mb().nx2/build.py: on macOS,free_mb()andtree_rss()callnx2/darwin_memory.py(three lines each, and theirdocstrings).
rss_mb(), the reserve, the floor of one compiler and every Windows and Linux line are unchanged.nx2/darwin_memory.py(new), standard libraryctypesonly:sysctlbynamereads the free, file-backed (pageable) and purgeable page counts, scaled byhw.pagesize. Speculative pages are counted once. Ordinary anonymous, wired, compressed and swapped memory is notcounted as available. Each counter's returned length is honoured (8, 4, 4 and 8 bytes here);
proc_pidinfo(PROC_PIDTASKINFO)for each process andproc_listchildpidsforits children, growing the buffer when it is full, each PID counted once. A process that has gone or cannot be read
counts 0, as on Linux.
tests/test_mem_gate_darwin.py(new): 13 cases from canned input, on every OS, with no Apple library loaded:counter lengths and page scaling, the reads being current, the libproc structure layout, child counts, descendants,
duplicates and cycles, and
build.py's own dispatch and process lifetime (--sourceselects anotherbuild.py).docs/development.md: what the macOS estimate counts, and what it does not.test_mem_gate.py,test_mem_gate_children.py,test_rss_liveness.py),every switch (
LONGSHOT_JOBS,LONGSHOT_MEM_GATE,LONGSHOT_MEM_LIMIT_GB),nx2/make_game.py, and everythingoutside the builder.
e517f675(the first reader),f6dc5135(thesysctlreader thatreplaced it) and
5fc528d6(the dispatch moved behindtree_rss(), sorss_mb()'s lifetime rule, whichtest_rss_liveness.pychecks, applies on macOS too). Rebased, they are0eb1aa01,0e1505dfand41d17603. Reviewall three commits, to
41d17603: the whole branch is this PR's own (it depends on no other PR).Measurements
Verdict: fixed on macOS:
tests/test_mem_gate.pyandtests/test_mem_gate_children.pyfail on main and pass here;the new
tests/test_mem_gate_darwin.pyfails against main'sbuild.py(2 of 13) and passes here; two interleaved runseach before the rebase, one each after it with the same results. The suite gains those 3 passes and no failure (534
passed, 30 failed against main's 531 and 32). Also: three controlled 256 MiB allocations against
vm_stat, afour-process tree against
ps, and the readers' cost (below, from the first version).Commits: main
6c9edffe5da1f5b5383977aeab3f544c0de25d4c(2026-11-14); this PRmac/mem-gate@41d17603309e022d12fab4d2b04afe854baa17f8.Hardware: Apple M4 Max, 16 cores, 64 GiB, 16 KiB pages, macOS 26.7.1 (25G241); Python 3.14.10 arm64. No GPU.
Scene: none. Game-free checks, plus allocation and process-tree probes from the first version (below).
Method: the memory checks through
tests/run_all.pyonmainand on this PR; the new test againstmain'sbuild.py(
--source) and this PR's. Before the rebase (on5e072fc8) each ran twice, interleaved; after it, once each, with thesame results. The whole suite on this PR, compared by test name with a
mainrun on the same Mac.How to reproduce:
python -I -B tests/run_all.py mem_gate rss_liveness, thenpython -I -B tests/test_mem_gate_darwin.py --source <a checkout of main>/nx2/build.py.6c9edffe41d17603tests/test_mem_gate.py(runs 1 and 2 before the rebase; the run after it the same)the free memory reads (None)tests/test_mem_gate_children.pya driver's child counts (0 MB read, the child holds 300 MB)tests/test_mem_gate_darwin.pyFAILED (failures=2)/ the same, of 13OK, 13 / the sametests/test_mem_gate_leases.py,tests/test_rss_liveness.pyThe readers' own measurements were made on the first version of this PR, on the old base
514591a3, and arecarried here.
nx2/darwin_memory.pyis byte-identical to the version measured atf6dc5135(then2b8c796f), andfree_mb()'s dispatch has not changed since; the process-tree reader was measured one commit earlier and is unchanged(the first version's report, taken as reported):
less available, 251.922 MiB back at once after the unmap (the rest of the busy Mac moves the numbers too; every trial
is kept). In the trial compared step by step, the gate's readings stayed within 5 MiB of
vm_stat's(free + file-backed + purgeable) / 64MiB.The first reader,
host_statistics64(e517f675here), was dropped: after an unmap it kept reading 29,662 MiB whilevm_stathad moved to 29,919 MiB. Apple'shost.ccaches those statistics for one second for callers other than thesystem's.
proc_pidinfoandpsagreed per process (43.53, 123.59, 59.63and 156.38 MiB); the gate counted 383 MiB, the grandchild included, against
ps's 383.125.free_mb()5.1 µs (p95 5.4 µs); one process's RSS 1.8 µs; a four-processtree 141 µs, timed before the dispatch moved behind
tree_rss()(5fc528d6).pstakes 56.5 ms.Screenshots: none; nothing on screen.
Flags and settings
None. An unreadable free-memory value still returns
None, and an unreadable process still counts 0.Testing
Tested: the above, and the whole suite,
tests/run_all.py, 6 at once, on the same Mac:test_mem_gate_darwin, absent on main,passes here;
test_mem_gate: FAIL on main, pass here;test_mem_gate_children: FAIL on main, pass here.requests, some are checks main added or changed since
5e072fc8):test_appimage_package_retry,test_appimage_player_build,test_collect_wide,test_control,test_cpm_discovery,test_crash_dialog,test_crash_record,test_env_on,test_game_layout,test_lift_cleanup_error,test_linux_preflight,test_longshot_ctl,test_low_memory_notice,test_make_game_cache,test_mirror_trim,test_oot_state_tap,test_pcache_header_read,test_pipeline_cache_file,test_pipeline_cache_save,test_pipeline_play_cache,test_pipeline_tour,test_prof_symbols,test_program_cache_cleanup,test_program_cache_one_variant,test_readback_late,test_report_run,test_split_giants,test_vk_pgo,test_vram_budget_row,test_walk_key.recorder_sync_test.exe-2026-11-14-180744.ips).
aa95d35f, at82c2e103(41d17603before the rebases ontoe090cc48andaa95d35f):Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
apply) gave the same results as before;
python -I -B tests/run_all.pyon wave S1's merge11a5b4e7, which carriesthis head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
(
reports/2026-11-15-rebase-sweep-3.md).Not tested:
sys.platform == "darwin"; only thetwo docstrings changed); the new test runs there without any Apple library, but nobody has run it there.
nor build speed.
Must be tested:
82c2e103) and mainaa95d35f, runpython -I -B tests/run_all.py mem_gate rss_liveness(it includes the newtest_mem_gate_darwin.py), then thewhole suite, expecting the same results as main by test name and the new test passing (13 tests, no Apple library
loaded), where main has no such test (by hand: test request T-58EG)
sysctl vm.page_free_count vm.page_pageable_external_count vm.page_purgeable_count hw.pagesizeandpython -I -B tests/run_all.py mem_gate rss_liveness, expecting the fourvalues to read and the memory checks to pass as on macOS 26.7.1 (by hand: anyone with a Mac on macOS 14.4; not part
of this PR's test request)
Risks
What could break: low. On Windows and Linux nothing runs differently. On macOS:
MemAvailableand not a promise: file-backed pages mayneed writing or reading again, the three counters are not read at one instant, and other programs move them.
count in each process, as on Linux.
checks.
6c9edffe(2026-11-14) without a conflict. Main changednx2/build.pyin seven commitssince this branch's base (#371, #385, #370, #573, #524, #569, #574) and
docs/development.mdoften; this PR's hunksapplied as they were, and
nx2/darwin_memory.pyis byte for byte the measured one. No conflict with the author'sother open PRs as rebased on
6c9edffe(git merge-treeof the heads, 2026-11-14).Turning it off:
LONGSHOT_MEM_GATE=0turns the memory check off, as before; or revert the PR.Compatibility: the builder's tools: on macOS the builder now admits compilers by the memory estimate, as it does on
Windows and Linux. Nothing changes for saves, the options file, the caches or the vk ABI.
Issues
See #420.
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), head82c2e103, without a conflict. (Thee090cc48rebase's one,
docs/development.mdwith #314's reworded prompts paragraph, stays resolved: this PR's macOS memoryparagraph first; this PR's own commits, old → new:
0eb1aa01→8f8e03b7,0e1505df→5240e431,41d17603→82c2e103; the commit names elsewhere in this description are from before the rebase.)python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). On macOS: 534 passed, 17skipped, and 30 failures that also fail on main
6c9edffeon this Mac, named under Testing. After the rebase onto mainaa95d35f(head82c2e103): wave S1's merge11a5b4e7, 568/22/17 against main aa95d35f's 543/33/17, no new failure.third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.5fc528d6c241d176033041d17603303dcbb580053dcbb5800582c2e10341WIP: fix(builder): account for available memory and compiler children on macOSto fix(builder): account for available memory and compiler children on macOSFolded into #845 (feat(macos): run the host on macOS: build, runtime, control, crash records and the Vulkan loader)
Pull request closed