perf(build): a lift reused across work folders, so a new one skips translating the same game #372
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!372
Loading…
Reference in a new issue
No description provided.
Delete branch "HumanBean/Longshot:perf/lift-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
A new work folder translates the game again (55-76 s here, minutes on smaller PCs) even when another folder on the same PC translated the same game with the same translator a minute before: the lift key holds the work and game folders' paths. This keeps finished translations under a key without those paths, so a new folder (a new
dev_packs.pybase, a base's first pack, a second checkout) copies one instead.Builds on #371 (the object store): review that first; this PR's own commit is the top one.
What changed
nx2/lift_store.py(new): the store, off unlessLONGSHOT_LIFT_STORE=<dir>.savecopies<work>/sa/gen(without.lift_keyand build.py'slifted_objects.txt),<work>/sa/image,image/chunks.txtandimage/hot_pieces.txtinto a temporary folder, writes the list of files and sizes last and renames it in.restorechecks every file against that list, copies the C and image, runs a check of the copied C, and only then puts the two boundary files over the folder's; any failure leaves the folder to lift as before. Copies areFICLONEclones where the file system has them (btrfs, XFS), plain copies elsewhere, never hard links (the later steps write in these folders).prune: the newestLONGSHOT_LIFT_STORE_KEEP(3) by last use; none used in the last hour.nx2/make_game.py:lift_key(..., paths=None)maps the environment's values throughpaths;lift_share_keyis the lift key with the work and game folders' paths taken out (OOT_WORK,OOT_EXEFS,OOT_ROMFS,OOT_DUMP,A64_HOT_FUNCS). Every file is already hashed by its name inside its folder, and the lifted C holds no folder path (checked: no work or game path in an earlier folder'ssa/genorsa/image). Intranslate(), when the folder must lift: the share key is taken from the state the lift starts from (as the folder that saved it took it), a snapshot is restored if there is one (its C must not be cut short,lifted_cut_short, or it is forgotten), and the folder's own key is taken again afterwards from the restored boundaries and hot pieces, so the next build in that folder skips the lift as after a lift of its own. After a real lift whose inputs did not change while it ran, and whose C is whole, it is saved.A64_/OOT_/RX_settings other than the compile's, the Python), minus the two folders' paths. chunks.txt / hot_pieces.txt: the key holds their state before the lift, and the restore brings the state after it, so the new folder adopts the same boundaries and its objects match the saving folder's (with the object store, they come from there too).scripts/dev_packs.py run: its builds use~/.cache/longshot-lifts(%LOCALAPPDATA%\Longshot\lifts) unlessLONGSHOT_LIFT_STOREis set (0: none).docs/development.md(Building's switch table, Testing a change against a base).tests/test_lift_store.py(new): save and restore, a snapshot not whole or cut short never used, prune, and the share key equal across folders where the lift key differs.tests/test_lift_cache.py's check oftranslate()'s text still holds: the lift's steps keep their order and wording, one level deeper.Flags and settings
LONGSHOT_LIFT_STORE=<dir>make_game.py); on indev_packs.pyLONGSHOT_LIFT_STORE_KEEP=<n>Measurements
main:
0caf0618(2026-11-14)this PR: perf/lift-store @
7bd98427Hardware: Ryzen 9 7900X (24 threads), 30 GB, NVMe SSD (btrfs), CachyOS Linux
Scene: no game run:
dev_packs.py baseof one commit into a new folder after an earlier folder saved its liftMethod: the build's "where the time went" table. Not measured yet: the measurement builds were stopped when the PC ran low on memory. Baseline to be:
this PR @ 7bd98427, LONGSHOT_LIFT_STORE=0, same commit, so the binaries can be compared.How to reproduce: two
dev_packs.py baseruns of one commit into two new folders with oneLONGSHOT_LIFT_STORE; the second's lift line and itslongshot/longshot_vk.soagainst a run atLONGSHOT_LIFT_STORE=0.7bd98427, LONGSHOT_LIFT_STORE=0 (baseline)7bd98427, snapshot from an earlier folderTesting
Tested: nothing in the game yet (above). No checks were run on the author's PC (
tests/run_all.py, including the newtests/test_lift_store.py, is left to CI). The checks that readmake_game.py's text (tests/test_lift_cache.pycheck_key_after_lift,tests/test_kept_translation.py,tests/test_dev_packs.py) were read against the change.Not tested: any build with the store; Windows; two builds saving the same snapshot at once; a restore across drives.
Needs other testers:
dev_packs.py baseruns of one commit into new folders: the second printsthe game was translated with these tools in another work folderand its game starts (request to open)Screenshots
Nothing on screen.
Risk and rollback
A snapshot used where the lift would have written other C would build a wrong game: reuse is gated by everything the existing lift key covers, minus only the two folders' paths, and a snapshot is checked whole before use. Off unless
LONGSHOT_LIFT_STOREis set (dev_packs:LONGSHOT_LIFT_STORE=0). Reverting the commit is enough.Reviewer notes
OOT_ROMFS,OOT_DUMP) beyond what the lift key already ignores today.LONGSHOT_*variables the lift reads (LONGSHOT_LIFT_JOBS,LONGSHOT_LIFT_NO_FORK) are not in either key, as today: they change how the lift runs, not what it writes.Issues
See #371
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_lift_store.py; not run on the author's PC, CI runs it)🤖 Generated with Claude Code
https://claude.ai/code/session_017vw8e34avQ5cTCWow16MXn
Read head
7bd98427against main0caf0618: the complete two-commit diff, description, empty comments and reviews, lift inputs, snapshot readers and build consumers; ran eight game-free test files on Linux. Seven passed;test_lift_cleanup_error.pyhas five errors. No game, real lift, full build or timing run was made.What's good
nx2/oot_patches.py, the other imported patch modules, HLE names,hot_funcs.txt,chunks.txt,hot_pieces.txt, lift settings and Python. The three recent fresh-lift cases, atc0442078,76699aadand1ed75e61, changedoot_patches.py; this code would reject those old snapshots when the inputs remain stable during the operation. That conclusion is traced from the key and the retained build records, not a replay of those builds.Must change
Keep the shared key tied to the inputs throughout restore and save. In
nx2/make_game.py, lines 2333-2338, a hit is selected using the old inputs, but its copied C is stamped unconditionally with a newly computed key. If a translator file, patch table or hot list changes during the copy, old C is certified as belonging to the new inputs, and the following build skips the required lift. The miss branch has the converse window: an edit aftershareis computed but beforeinputsat line 2340 is accepted by the later comparison, so lines 2352-2356 save the new translation under the old shared key. Main's real-lift path already protects against changed inputs; the new store paths need that same protection starting before shared-key capture. Reject or retry when non-planning inputs change, rather than publishing a key for other inputs. Add tests that change a tracked input during restore and between shared-key capture and a miss, and check the next build cannot reuse mismatched C. Source-traced, not reproduced.Hash path-backed lift inputs before removing their folder names. In
nx2/make_game.py, lines 1711-1722, normalization is applied to every environment value, not just the five documented folder settings.A64_DEVIRT=<work>/devirt.txtconsequently becomes the same value in two folders even when their tables differ.lift_keynever reads either table: its work-folder inputs are onlyhot_funcs.txtand the two planning files. The consumer is real:third_party/a64-kit/lift/a64_lift.py, lines 908-917 and 982, reads that table, and lines 1693-1703 use it to choose emitted direct-call fast paths. An alternateA64_HOT_FUNCS=<work>/custom-hot.txthas the same problem when the normalhot_funcs.txtis absent. This newly permits cross-folder reuse for different requested translation inputs; it is separate from main's pre-existing same-path edit limitation. Hash the files actually selected by these settings, or keep their paths distinct until their contents are covered. Add two-folder tests with different tables and different custom hot lists. Source-traced, not reproduced.Update the fresh-lift regression fixture for the new dependency. The unconditional
_lift_store.root()atnx2/make_game.py, line 2332, breakstests/test_lift_cleanup_error.py, which executes the actual fresh-lift branch with an explicit namespace. That namespace has no_lift_store. Runningpython3 -I -B tests/test_lift_cleanup_error.pyon this head givesRan 9 testsandFAILED (errors=5), each failing at line 2332 withNameError: name '_lift_store' is not defined. The affected cases cover a first lift, successful cleanup, missing-file races, a failed lift and an input changed during the lift. Keep those checks, supply the new dependencies with temporary store paths, and cover the hit/miss cases rather than weakening the assertions. Run on this head.Measurements and smaller points
translate()cache-hit branch. A normal concurrent save uses separate process-named temporary directories and only publishes a complete directory; that is a source trace, not a concurrency test.nx2/lift_store.py, lines 151-164, can replacechunks.txt, fail onhot_pieces.txt, and remove the copied C while retaining the changed first planning file. The fallback then lifts from that remaining plan. Exercise this failure point explicitly; the current test fails before either planning file is replaced.AGENTS.mdsays Forgejo does not run source checks yet. Record actual local results instead. The firsttest_lift_cache.pyattempt here stopped on missing NumPy in the system Python; it passed with the existing dependency Python. That prerequisite failure is not a PR defect.Overlaps
8a7bf09d. Its object store is included in this diff; the lift-only change is the top commit. #30 removes the profile pathname from object keys; without that change, identical restored hot C can still recompile in each new folder. It merges pairwise with this head.nx2/build.pyafter successful compilation: itsrekey_depfiles(vk_jobs)meets the object-store publication. Preserve both, finalizing dependency keys before sharing the objects.0caf0618. No combined-stack build was run.Not checked
No Windows execution, cross-drive restore, simultaneous-process or interruption test, whole suite, game, real lift or build-output comparison. All exercised checks were Python-only, with no GCC or Clang fixture to compile. The passing files were
test_lift_store,test_object_store,test_lift_cache,test_kept_translation,test_lift_cut_short,test_dev_packsandtest_failed_compile_keeps_objects. The runtime and shader-preparation messages in the pack test come from stand-ins, not a game run.WIP: perf(build): a lift reused across work folders, so a new one skips translating the same gameto perf(build): a lift reused across work folders, so a new one skips translating the same gameView command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.