Preflight had a texture cache that worked.
It was also doing almost nothing.
The cache hook was reached 6,654 times in one measured launch and served 6,651 of them. Three misses. Great. The manifest knew about 32,917 textures across the whole mod setup. Less great.
The missing textures weren't failing the cache. Most of them were never asking it anything.
Starsector already has an asynchronous image prefetcher. It walks the resources it expects to need, puts them on a queue, and starts one background thread to decode them. The loading thread later asks whether a texture is pending. If it is, it polls the result map and sleeps for 10ms at a time until that one decoder thread gets there.
On the development profile I was measuring, the loading thread spent about 27 seconds doing exactly that.
And Preflight's cache lookup happened after the wait.
The report said the loading thread never blocked
I did not find the 27 seconds immediately.
The first critical-path report said main, the loading thread under the direct benchmark protocol, did not appear in the blocked-thread ranking at all. I treated that absence as a finding. The machine was only about 28% busy across ten cores, GC pause time was basically absent, GPU upload was a small part of the sampled work, and the loading thread apparently wasn't blocked.
That led to a pretty strong conclusion: the load was one long serial chain, and the idle-core opportunity wasn't reachable from where an external fail-open accelerator sat.
The report printed eight blocked threads.
main was ninth.
Six of the rows above it were permanently idle Java daemons parked on empty queues. Ranked by raw blocked time, of course they looked enormous; doing nothing was their full-time job. The loading thread's 27 seconds of sleep landed one row below the cutoff and vanished from the report.
The stack made the difference obvious once I actually printed it:
main 27.4s ThreadSleep:27.2s
at java/lang/Thread.sleep
<- com/fs/graphics/L.class
<- com/fs/graphics/TextureLoader.Ô00000
<- com/fs/graphics/TextureLoader.o00000
There were 2,394 sleep events at that site.
The same recording had another instrumentation problem: Starsector's -XX:+UseFastUnorderedTimeStamps setting made JFR's event clock run at about 0.401× wall time. That invalidated durations I had previously read straight out of the recording. Event counts and shares were fine; the clock wasn't.
The day's first conclusion was wrong for two independent instrumentation reasons. For the cache, the decisive one was [:8].
The cache was after the expensive part
The placement had originally been conservative on purpose.
Preflight's compatibility texture rewrite found the branch where Starsector's own prefetcher missed and inserted the cache lookup there. If the game had already prefetched an image, Preflight left that path alone. If the game had not, Preflight could serve prepared pixels instead of doing the synchronous decode and conversion.
That meant the cache could only help after the game's prefetcher had already lost the race.
For any image the prefetcher owns, the loading thread checks the queue, sees the image is pending, and starts sleeping. The background decoder eventually publishes the image. Only after all of that would execution ever reach the branch where the cache lived.
The prepared pixels could be sitting there ready the entire time. It did not matter. The wait had already happened.
It also explained an earlier result. A large amount of texture CPU work had been removed, the cache counters looked healthy, and whole-launch time barely moved. The optimization was locally correct. It was simply on the wrong side of the cost it was supposed to avoid.
Stop putting the work on the queue
The repair did not need a cleverer cache.
If Preflight's manifest can serve a texture, do not enqueue that texture for Starsector's prefetcher in the first place.
Now the loading thread reaches the texture, finds no pending prefetch entry to wait for, and falls through to the cache path that already existed. Anything Preflight cannot serve stays on the game's original path.
The final run's telemetry changed:
prefetchSkipped 50879
prefetchKept 1
attempts 21656
hits 21653
fallbacks 3
The game had been enqueueing 50,880 images and later asking for 21,656 of them. One decoder thread was therefore also chewing through roughly 29,000 images nobody ever collected.
The cache itself was the same basic cache. It was finally being asked before the loading thread paid for the queue.
Then measure it like a user would
The follow-up August 1 campaign used the then-current 77-mod setup: five rounds across three conditions, with a 240-second cooldown before each launch.
The medians were:
| condition | median | | --- | ---: | | normal launch | 88.49s | | Preflight cache + prefetch bypass | 78.93s | | prepared-pixel path without the prefetch bypass | 87.89s |
The cache-plus-bypass path beat the normal launch in all five rounds. Median difference: 9.56 seconds, or 10.8%, with the retained campaign's permutation p = 0.048.
The prepared-pixel path without the bypass was 0.60 seconds from normal, with p = 1.000.
The prepared texture work could be present and correct and still buy essentially nothing if the loading thread had to wait before reaching it. Move the decision ahead of the wait and it mattered.