perf(build): one object store for every work folder, so a new one compiles only what no other built #371
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
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/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!371
Loading…
Reference in a new issue
No description provided.
Delete branch "HumanBean/Longshot:perf/object-store"
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?
Why
Each new work folder (a second checkout, a new
dev_packs.pybase, a fresh--work) compiles every one of the ~480 files again, although another folder on the same PC already built the same objects: 8 min of compile on a 12-thread share of a Ryzen 9 7900X, 23 min for a whole base on an 8-core lab PC. This shares one object store across work folders, so a new folder compiles only what none built before.What changed
nx2/object_store.py(new): the store, off unlessLONGSHOT_OBJ_STORE=<dir>. One sub-folder per compiler and system headers (the first 16 characters of the work folder'ssystem_headers.txt, whichmake_game.py drop_stale_objectswrites; a folder without it uses no store).fetchhard-links the keys a build needs from the store into<work>/objcache,sharelinks the folder's objects the store lacks back; across volumes or without hard links, a copy under a temporary name renamed in (no torn object, no overwrite on Windows).prune: the store's own clean-up.forget: objects out of the store.nx2/build.py main: before the compile, shares the folder's own objects, works out every job's key (intoPLANNED, so nothing is hashed twice) and fetches those keys; the kept-translation check fetches the kept keys first. After a compile with no failures, shares the new objects.compile_one,cache_put,plan_jobsand the object keys are unchanged: an existing folder compiles nothing again.nx2/make_game.py:forget_failed_build(a link that failed) also takes that link's objects (object_stamps, fromsa/build/obj/*.sha1) out of the store, or the next try would take the same objects back. After each build,object_store.prune(): an object only the store holds (one link; every object where hard links don't exist) goes once unused forLONGSHOT_OBJ_STORE_DAYS(14), and oldest first while those exceedLONGSHOT_OBJ_STORE_GB(8); nothing used in the last hour.prune_object_cacheand thermtrees of<work>/objcacheonly ever remove the folder's own links, so they never take an object another folder uses.scripts/dev_packs.py run: its builds use~/.cache/longshot-objects(%LOCALAPPDATA%\Longshot\objects) unlessLONGSHOT_OBJ_STOREis set (0: none).docs/development.md(Building's switch table, Testing a change against a base).tests/test_object_store.py(new): fetch/share, the folder's prune leaves the store whole,forget, the store's prune by links, age and size.Flags and settings
LONGSHOT_OBJ_STORE=<dir>make_game.py); on indev_packs.pyLauncherHome.Work,make_game.py's default--work), so there is nothing to share. On for developmentLONGSHOT_OBJ_STORE_GB=<n>LONGSHOT_OBJ_STORE_DAYS=<n>Measurements
main:
0caf0618(2026-11-14); measured oncf605c85with this change (34b7e787), then rebased: the three commits since touch neithernx2/build.py,nx2/make_game.pynorscripts/dev_packs.py(the Windows lift's worker count, the button prompts, a docs file), so the compile numbers hold.this PR: perf/object-store @
8a7bf09d(34b7e787 before the rebase)Hardware: Ryzen 9 7900X (24 threads), 30 GB, NVMe SSD (btrfs), CachyOS Linux; the builds pinned to 12 threads (
oot-status --timing --cpu-only), the main session's builds running on the other 12Scene: no game run:
python -I scripts/dev_packs.py base <new folder> --romfs ... --exefs ...of the same commit,LONGSHOT_NO_PREPARE=1, NGX SDK and FSR as in the regression env, into a new folder each timeMethod: the build's own "where the time went" table and its
@@file ... new|cachedlines. Baseline:this PR @ 34b7e787, LONGSHOT_OBJ_STORE=0, which is main's behaviour (the store's code does nothing then), on the same commit so the binaries can be compared byte for byte (the version fingerprint coversnx2/). Not main itself because main's binaries carry another fingerprint.How to reproduce: two
dev_packs.py baseruns of one commit into two new folders, the first with an emptyLONGSHOT_OBJ_STORE, the second with the same store; compare the second's compile with a run atLONGSHOT_OBJ_STORE=0.longshot,longshot_vk.sobyte-identical to the baselineThe run with the store (an empty store, then a second folder over it) was stopped by Claude Code because the PC ran low on memory while other builds ran; it is to be run again. Until then this stays WIP.
Testing
Tested: a cold
dev_packs.py basewithLONGSHOT_OBJ_STORE=0on Linux (above): builds and links as before. No checks were run on the author's PC (tests/run_all.py, including the newtests/test_object_store.py, is left to CI). The checks that cutcompile_one,cache_put,plan_jobs,prune_object_cacheandforget_failed_buildout of the source were read: those functions are unchanged or keep their names and signatures, so no check needed updating.Not tested: the build with the store on (stopped, above); Windows (the copy fallback across drives, NTFS hard links, a file held open during the store's prune); two builds sharing a store at the same time; a failed link taking its objects out of the store.
Needs other testers:
dev_packs.py baseruns of one commit into two new folders (LONGSHOT_OBJ_STOREon the bases' drive, then on another drive): the second compiles only what the first didn't, and its game starts (request to open)Screenshots
Nothing on screen.
Risk and rollback
A wrong object in the store would reach every folder that uses it, where today it stays in one: the keys are the per-folder cache's (path-free since issue 565), partitioned by the same compiler and headers stamp that guards a folder's cache, and a failed link drops its objects from the store. Off unless
LONGSHOT_OBJ_STOREis set (dev_packs:LONGSHOT_OBJ_STORE=0). Reverting the commit is enough; a store folder left behind can be deleted.Reviewer notes
-fprofile-instr-use=<work>/longshot_pieces.prof(make_game.pysetsRX_PGOto the work folder's copy), so in a checkout with a play profile the 8 hot pieces still compile once per work folder. Fixing that changes those keys (one recompile of them in every folder), so it is left for a follow-up.Issues
None.
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above). (no tests run on the author's PC; CI runs them)third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.mainand passing here. (added, tests/test_object_store.py; not run on the author's PC, CI runs it)🤖 Generated with Claude Code
https://claude.ai/code/session_017vw8e34avQ5cTCWow16MXn
Read head
8a7bf09dagainst main0caf0618: the complete six-file diff, description, empty comment and review lists, object writers and consumers, recovery paths and related build changes; ran 10 game-free check invocations across 9 files, all passed, including the header-dependency fixture with Clang and GCC. No game, full build or timing run.What's good
compile_oneactually consumes the populated local cache. The WIP description clearly admits that no store-enabled build comparison completed.Must change
Include the selected external headers in shared-object identity. In
nx2/build.py, lines 1290-1293, the new fetch trusts keys which omit some real inputs. For a concrete case, build two fresh work folders with the same compiler and source but different VMA packages selected byRX_CPM.flag_text, lines 666-678, drops their-Ipaths, and the dependency roots at lines 1218-1222 contain neither package.system_headers.txtdoes not distinguish them either. Yetgpu/vk/vk_vma.cpp, lines 4-8, compiles the allocator implementation fromvk_mem_alloc.h. The second folder therefore takes the first package's allocator object rather than compiling its selected one. Main would compile in that fresh folder. Cover the selected external headers and their contents before enabling cross-folder reuse; the same audit is needed for Vulkan, SPIR-V and the C/C++ system headers. A two-folder test that changes an implementation-bearing header, then checks both the miss and the resulting object, would settle this. Traced through the source, not reproduced with a native build.Make local-cache publication atomic before exporting interrupted builds. In
nx2/build.py, lines 1288-1289,sharepublishes every existing local.o, explicitly including a stopped build's entries. However,cache_put, lines 323-333, falls back toshutil.copyfile(obj, dst)directly at the final cache name when hard links are unavailable. Killing that copy, or running out of space during it, leaves an incompletekey.o. The next build now exports it, and a different work folder accepts it by existence incompile_one, lines 962-972. The store's temporary-file copy cannot repair an already partial source. Make the producer's fallback atomic and ensure incomplete entries left by older producers are not imported. Add an interrupted-copy test which restarts publication and tries a fetch from a second folder, not just a successfulshare/fetchtest. Traced through the source; no process-kill or disk-full reproduction run.Keep failed-link invalidation from being undone by another folder. In
nx2/object_store.py, lines 123-127,forgetonly unlinks the store pathname. A second folder that already fetched the rejected object keeps its local link, and its nextsharerecreates that same entry. The first folder's retry can then fetch the object it just rejected instead of recompiling. On Windows, an open copy source can also make the unlink fail; that exception is discarded, leaving the same entry immediately reusable. Preserve a rejected-entry record or otherwise make the retry bypass that object even if another publisher retains it. Test two folders through fetch, failed-link eviction, re-sharing and retry, plus a refused unlink. The current test checks only that one removal leaves the first folder's local copy intact. Traced through the source, not exercised with concurrent builds or Windows file handles.Measurements and smaller points
34b7e787with the store off: 483 files compiled, 498 s compiling and 590 s total. The store-enabled run was stopped, so there is no comparison, output-equivalence result, median or spread. An on/off comparison at one named commit can isolate this feature, but needs at least two alternating runs per condition on the same PC, controlled store state and retained individual results. Record the warm-up that fills the store separately and keep other builds out of the comparison. The current WIP qualification is appropriate.build.pydirectly, so they do not run the cleanup added tomake_game.py. Explain those limits when recommending a shared store, especially the copy fallback across drives.tests/test_object_store.pyuses authored bytes and prewritten stamps, not the real compiler or header-note producer. It has no simultaneous publishers, interrupted copy, forced copy fallback, denied Windows deletion or failed-link retry through another folder. These passing checks are useful but do not settle the findings above. The description also leaves checks to CI, while both source-check shards are skipped: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/4408.Overlaps
nx2/build.pyimmediately after compilation: itsrekey_depfiles(vk_jobs)and this change'sshareoccupy the same position. Re-key before sharing; that change's restricted dependency roots do not by themselves fix the external-header omission above.OBJ_CACHEdeclaration and changes the local cache's publication method. Choose one storage layout rather than combining both defaults.0caf0618.Not checked
No Windows execution, actual concurrent publishers, interrupted-copy recovery, changed-header native reproduction, full suite, clean/incremental game build, game start or independent speed measurement. The 10 passing invocations were
test_object_store,test_object_cache_prune,test_build_hash_cache,test_kept_translation,test_compiler_failure,test_compile_retry_jobs,test_failed_compile_keeps_objects,test_dev_packs, andtest_system_header_cachewith each compiler. The latter preprocesses authored headers; it is not a Windows runtime test. The requested failure tests and clean/incremental Linux and Windows builds, on both same-drive and cross-drive stores, would decide the remaining safety questions.WIP: perf(build): one object store for every work folder, so a new one compiles only what no other builtto perf(build): one object store for every work folder, so a new one compiles only what no other built