perf(lift): a Windows lift runs one worker per 0.75 GB free, and makes the runtime tables beside the edits #357
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!357
Loading…
Reference in a new issue
No description provided.
Delete branch "madebyjordan/Longshot:perf/lift-workers"
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
On Windows, the lift (the build's Translate step) used one or two processes on a typical 16 GB PC, so a first build sat in Translate for about 4 minutes. It now uses the PC's cores where memory allows, and the lifted C it writes is byte for byte the same.
What changed
lift:
file_jobs()innx2/chunks.pyallows one process per 0.75 GB of available memory where the processes are spawned (Windows). Where they are forked it stays at one per 2 GB. A spawned worker holds only the emit's tables and its run's function bodies: at most 395 MB, measured over a whole lift with 4 workers on a Ryzen 5 5600 with 16 GB. The 2 GB rule is a forked worker's, which touches the lift's shared pages. With it, a 16 GB Windows PC with 4-8 GB free got one or two processes; that build's lift took 254 s.lift:
start_side()/end_side()innx2/chunks.py. Where there is no fork, the runtime tables and standalone files (~11 s of Python) are made in a thread while the C is edited. This process only waits on the edit workers meanwhile; before, they were made after the edits. A side run that raises is done again in the lift's own thread, as a failed forked process already is.docs:
docs/development.md'sLONGSHOT_LIFT_JOBSrow, andfile_jobs' docstring, say 0.75 GB on Windows.tests:
tests/test_lift_workers.pychecks:LONGSHOT_LIFT_JOBSfirst;Against main's
chunks.py, the spawned count and both thread cases fail.Linux and macOS are unchanged: they fork, so
file_jobskeeps 2 GB a process and the side work keeps its forked process.Flags and settings
LONGSHOT_LIFT_JOBS=<n>(existing)Measurements
main:
cf605c85(2026-11-14)this PR: perf/lift-workers @
d0564f17Hardware: Ryzen 5 5600 (6 cores, 12 threads), 16 GB DDR4 at 2133 MT/s, NVMe, Windows 11
Scene: none (the build). nightly-34's exefs, a standalone lift as
make_game.pyruns it:nx2/lift.py --standalone --write, with the builder's tools (Python 3.13, llvm-mingw clang 23).Method: main and this PR alternated, main / PR / main / PR, each from an empty work folder, with each checkout's
a64emit.dllbuilt beforehand. Wall time is the lift process's. The 90 files the lift writes (sa/gen) were compared by SHA-256 between the two.How to reproduce:
OOT_WORK=<empty folder> OOT_EXEFS=<exefs> RX_RELEASE=1 A64_HOT_CHUNK=1 python -I nx2/lift.py --standalone --write --exefs <exefs> --work <folder>on each checkout, alternated.cf605c85d0564f17An earlier run on the same PC at
dd02c6cc(2026-11-13) measured main at 232.3 / 223.3 / 171.3 s (1, 1 and 2 emit processes) and this change at 150.7 / 142.8 / 125.6 s (3, 3 and 5), with the outputs byte-identical. How many processes it gets depends on the memory free at the start.Testing
Tested:
tests/test_lift_workers.py.python -I -B tests/run_all.py --jobs 4on main (483 passed, 6 failed, 70 skipped) and on this PR merged with the other four build PRs of this batch (486 passed, 6 failed (the same 6 as main), 70 skipped).Not tested:
Needs other testers:
Screenshots
Nothing on screen.
Risk and rollback
LONGSHOT_LIFT_JOBS=1(or any smaller number) sets the count.Reviewer notes
The side thread shares the process with the edits' waiting loop.
tables()reads the image and writes the runtime tables, but no file the edits touch: the same split the forked process already had on Linux.Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.tests/that reads changed source updated in this PR, not weakened.WIP: perf(lift): a Windows lift runs one worker per 0.75 GB free, and makes the runtime tables beside the editsto perf(lift): a Windows lift runs one worker per 0.75 GB free, and makes the runtime tables beside the editsRead head
d0564f17against maincf605c85: the complete diff and description, the empty comments and reviews, and the emit, edit and runtime-table callers; ran the new worker test and seven existing lift checks, including the spawned emitter with GCC and Clang. All pass here; the new test against main gives the expected two failures and one error. No game or full build was run.What's good
nx2/lift.py, lines 200-214, starts the side work, editscode_*.c, then waits for its result before reporting completion. The side work writes different files.end_sidejoins the thread before reading its result and retries a failed callback only after that join.Must change
No functional defect found in this patch. It merges cleanly with main
cf605c85.Measurements and smaller points
Keep the gain tied to this Windows lift and show the spread. The named commits, same PC and empty-work-folder method, and main / PR / main / PR order satisfy the basic comparison. The displayed medians are correct: 246.7 s to 150.6 s is 39.0% less lift time. Main ranges from 219.1 to 274.3 s, versus 147.0 to 154.2 s here. Those ranges do not overlap, but their spreads are 22.4% and 4.8% of their medians, and the worker counts change between repetitions. The table supports the saving for these runs, not a fixed gain on every Windows PC. Add the per-run available-memory and stage-time lines, and say how warm-up was handled. It also does not separate the thread's contribution from the increased worker count.
Make the output comparison and test record unambiguous. Measurements says all 90 generated files were compared, while the table says none of 85 differ. Reconcile those counts and explicitly include the runtime and standalone files now generated by the thread. Testing reports a suite run on a five-PR combination, not this head alone, and says the six inherited failures are named above, but does not name them. Give the combined commit and the six test names and diagnostics, keeping that result separate from this head's focused checks. The source-check jobs currently say “Blocked by required conditions”, not passed: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/4277.
Pin the overlap and wait behavior in the thread test.
tests/test_lift_workers.py, lines 68-77, checks the callback's thread and result, but its immediate callback does not establish thatstart_sidereturns while work is pending or thatend_sidewaits. A callback held by athreading.Eventwould make those two guarantees explicit. This is a coverage improvement, not an observed failure.Not checked
No Windows or macOS execution, low-memory Windows build, whole suite, or reproduction of the author's timing and peak-memory measurements here. The passing Linux spawn fixture compares generated output, but is not Windows memory-pressure evidence. The outstanding 8 GB test should retain available memory, peak usage across emit and edit stages, and whether fallback ran. No old tested-configurations table rows are changed by this PR.