Start faster: an index for the program cache, and the precompile's programs read in parallel #859
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!859
Loading…
Reference in a new issue
No description provided.
Delete branch "boot-program-index"
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?
What
Two changes to how
hookshot_programs.binis opened at every start. Nothing the game does changes: the same programs are loaded and the same pipelines are built before the game starts, in the same order.hookshot_programs.idx. The cache's records follow one another with nothing pointing to them, so finding them meant reading each record's 20-byte head. That is tens of thousands of small reads spread over a file of 4 to 6 GB, one after another, at every start. The index is those heads in file order, 20 bytes each, under a header with the cache's stamp. It is under 1 MB and read in one go.program_cache_load_someread every compute program and every program in the pipeline notes (a few thousand records, a few hundred MB) one at a time, holdingshader_mu. Now up to 16 threads read and parse the records, each with its own handle on the file.vkCreateShaderModuleis thread-safe. The results go into the program table afterwards, undershader_mu, in the same order as before.A third commit adds to the precompile's log line how much of its time went to opening the caches and reading the notes and programs.
Why
On a warm start (every cache already built), the first frame came 12.8 s after launch here. 8 s of that went to finding the records in the program cache, before a single pipeline was built. The game thread waits at its first submit for all of it.
On a file system that compresses (btrfs
compress=zstdhere), each of those 20-byte reads also unpacks a 128 KB block. NTFS without compression has the same problem on a cold disk: one random read per record.Measurements
RTX 3090 (driver 615.78), Ryzen 9 7900X, Linux, NVMe (btrfs on LUKS, zstd). Program cache with 39,933 programs (6.1 GB), 700 noted graphics pipelines and 3,842 compute pipelines. Same game build, same caches, only
hookshot_vk.soswapped. Times are seconds from process start, median of 4 starts each, run alternately A/B, with nothing else running. "Cold" means the folder's files were dropped from the page cache (POSIX_FADV_DONTNEED) before each start. "Warm" starts back to back. With the PC's memory mostly in use, little of a 6 GB file stays cached either way, which is why the two columns are close.Each start's first frame, warm: before 12.99 / 12.91 / 12.70 / 12.09 s, after 4.34 / 4.12 / 4.01 / 4.03 s.
The disk reads by the time the game starts went from about 1.8 GB to 0.8 GB (
/proc/<pid>/ioread_bytes).The old scan grows with the cache, since every program the game translates in play adds a record. On a 4 GB cache of 22,000 programs, a script doing the same reads took 4 to 6 s here.
Tests:
tests/test_program_cache_index.pychecks the source: the index entry is written after its record, the start-up checks, the head check on load, and the parallel reads joining before the program table is touched.tests/test_program_cache_reader.pynow follows the reader into its new function. Index checks, each a real start:program cache index: 40044 records from it, 3 more read from the cache, then rewritten whole;Notes
vk_shader.cppis part ofHOOKSHOT_SHADER_STAMP, so this change starts the program cache over once, like any translator change: the first start after the update prepares the shaders again (about 70 s here). The cache's format is unchanged, and a build without the index just ignores the.idxfile.RX_VK_NO_PROGRAM_CACHE=1still turns the whole cache off, index included.precompile_runand the draw-time builds, this changes what happens before the first pipeline is built. With #702, the start gets shorter at both ends: before the builds begin (here) and the slow stragglers at the end (there). The only overlap is the log commit, which edits the precompile's final log line that #702 also rewrites. It's a one-line merge, and it can be dropped.precompile_run, which #702 rewrites, so it is left for after.