perf(tests): cache the checks' compiles by what they compile #893
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/tooling
area/upscaling
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
risk
high
risk
low
risk
medium
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!893
Loading…
Reference in a new issue
No description provided.
Delete branch "dyst/Longshot:perf/check-compile-cache"
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: perf · Risk: low · Tested on: Windows 11, llvm-mingw (v2.0.14 tools) · Needs: Linux, a run of
tests/run_all.pytwiceWhy
Many game-free checks compile a small program from the source every time they run, in every worktree, even when nothing they read has changed. Those compiles were a third of a full run's check time on Windows, and they come back on every PR's run. The build already keeps objects by what they compile (
nx2/object_store.py); the checks now do the same.What changed
tests/run_all.pystarts each check through a small boot script that sends the check's compiler calls (and run_all's own C++ checks) through a compile cache. A compile is keyed by what it compiles, not by its paths, so a second run, or a run in another worktree, copies the program instead of compiling it.LONGSHOT_CHECK_CACHE=0turns it off.tests/check_cache.py(new):key_of/plankey a compile by the compiler's real path, size and time, its flags (the paths of its inputs, output and include folders left out), each source as-E -Ppreprocesses it (with the source's own path, asassert's__FILE__spells it, left out, so a fixture cut into a new temporary folder each run still hits) and each other input's bytes.compile_cachedcopies a hit's program and replays what the compiler printed; it keeps only compiles that succeed, writing each entry whole (os.replace). A compile it can't key that way goes to the compiler untouched:-x,-E,-M*,-save-temps, profiling, coverage,@file, stdin,-L,-B, a sysroot, a linker named by its path, or a second output (an import library, map, PDB,-gsplit-dwarf). On Windows,-o fixturewritesfixture.exe:writtenkeeps and gives back that file.prunedrops entries unused for 14 days and the oldest over 4 GB.installpatchessubprocess.Popenin the check's process for a list command whose first word is a C or C++ compiler.tests/check_cache_boot.py(new): runs a check aspython -I -B <check>would (runpy, the samesys.path), aftercheck_cache.install().tests/run_all.py:python_checkstarts the check through the boot script andcpp_checkscompiles throughcached(), both only whilecache_root()is on (and the two files are there:test_run_all_output.pycopiesrun_all.pyalone).mainprunes the cache and ends with acompile cache: H hits, M misses, P passed throughline before the summary.tests/test_check_cache.py(new): a hit gives the same program; a program named without an extension hits; a changed header or flag misses; a source in a new folder hits; a failed compile isn't kept; warnings are replayed on a hit; what it can't key passes through;=0turns it off; a check started through the boot script has its compiles cached.run_all.py's docstring say what the cache does and how to move or turn it off.Measurements
Verdict: a full warm run takes 36% less check time (2970 s → 1886 s), 425 of its 434 compiles from the cache, with the same failures as without it. Wall time fell less (308 s → 228-246 s) because one check,
test_seed_reader, runs its sanitized program for about 245 s and is now the run's long pole.Hardware: Windows 11 Pro, the PC that runs the bench (other sessions' checks ran at the same time, so the times are noisy)
Scene: none (game-free checks)
Method:
python -I -B tests/run_all.py --jobs 12on this branch, withLONGSHOT_CHECK_CACHEset to an empty folder: a cold run, a run to fill the cache, then a warm run. Check time is the sum of each check's own time from run_all's lines; wall time is run_all's summary. One run each, so the times are a guide, not a median.How to reproduce:
LONGSHOT_CHECK_CACHE=<an empty folder>, thenpython -I -B tests/run_all.pytwice; the second run'scompile cache:line.e36ebb6ee36ebb6eBaseline: a cold cache rather than main, so both columns run the same checks; a cold run compiles every program, as main does, plus the preprocessing for the key.
Checks that gained the most (cold → warm):
test_system_header_cache53.7 → 13.4 s,test_pipeline_cache_file40.5 → 9.1 s,test_upscaler_round_trip36.7 → 11.6 s,test_split_giants43.1 → 18.0 s,test_vk_occlusion_queries22.9 → 4.6 s.Flags and settings
One new environment variable for the test runner; nothing in the game or the build changes.
LONGSHOT_CHECK_CACHEtests/run_all.pykeeps the checks' compiles%LOCALAPPDATA%\Longshot\check-cache, else$XDG_CACHE_HOME/longshot-checksor~/.cache/longshot-checks;=<dir>moves it,=0turns it off0to rule the cache out when a check behaves oddlyTesting
Tested:
tests/run_all.pyon this branch (cold, fill, warm), each 513 passed, 19 failed, 81 skipped. The same 19 fail withLONGSHOT_CHECK_CACHE=0: the 16 that fail onmain@1310ecc6(the baseline), andtest_auto_handheld,test_crt_optionsandtest_vk_occlusion_queries, which fail on this branch's main with the cache off too.main@b8d2b04e, the checks that read this diff:tests/run_all.py check_cache run_all byte_compile utf8_paths(7 checks,test_check_cache,test_run_all_output,test_run_all_junit,test_run_all_declarations,test_run_all_schedule,test_utf8_paths, the byte-compile), all pass.Not tested: Linux (gcc and clang): the keying is the same code, but nobody has run it there; CI's Linux run with a cold cache each time.
Must be tested:
python -I -B tests/run_all.pytwice, expecting the second run'scompile cache:line to show most compiles as hits, and the same failures asLONGSHOT_CHECK_CACHE=0 python -I -B tests/run_all.py(Test Farm: @Beanstalk linux, run the game-free checks twice and compare with the cache off)Risks
What could break: a check whose result depends on something its compile reads that the key leaves out would pass on an old program. The key covers the preprocessed source, flags, compiler and input files, and refuses library folders,
-B, sysroots and linker paths. What it leaves out on purpose is the source file's own path: a hit's program names the path of the run that made it, in anassertmessage only. A check that compares its compiler's timing or counts its compiler starts would see fewer.Turning it off:
LONGSHOT_CHECK_CACHE=0, or revert the commit.Compatibility: Nothing changes for a player. Developers get a cache folder of up to 4 GB (a full run's compiles came to 48 MB on Windows), pruned by each run.
Checklist
Everyone
type(scope): description) of the status line's type.main.python -I -B tests/run_all.pypasses (any failure that also fails onmainis named).third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.🤖 Generated with Claude Code
WIP: perf(tests): cache the checks' compiles by what they compileto perf(tests): cache the checks' compiles by what they compileView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.