Repository navigation
perf(libSceSysmodule): cache the unwind region of loaded images on Windows - #6064
Conversation
…ndows sceSysmoduleGetModuleInfoForUnwind called VirtualQuery and GetMappedFileNameA on every call. VirtualQuery on a large image section costs time proportional to the section (2 us for 16 KiB, 51 us for 8 MiB, 167 us for 32 MiB), and a title that unwinds exceptions while loading calls it for every frame of every throw. The region and file name of an address inside an image of the Windows loader's module list are kept per region and used while the loader still reports the same image (base, time stamp and size). Other addresses are queried every time. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Windows run of head
Cost per call. I wrote a small harness that loads each side's
Titles: none of the 47 local catalog titles I ran calls this function. I counted calls with a diagnostic build of
The longest frame always falls in the first 10 s, and both sides sit at 17–18 ms after that. Since this title never calls the function, the differences are run-to-run noise. One behaviour change: the reported region is not always the same as on The same happens for two addresses 8 MiB apart in a Verdict: on Windows the build, the tests and the suite pass. The cache makes a call about 150 times cheaper for large image regions and about 90 times cheaper for small ones on this machine. No local title exercises the function, so this run cannot confirm the title-level gain, and PPSA28319 shows no regression. The reported region differs from AI-assisted (Claude Code). |
|
Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.
Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.
|
|
Besiege (PPSA20313), |
|
I reproduced the cached-region mismatch reported by screamm and prepared a correction on top of this PR's unchanged head
The cache hit now returns the queried address rounded down to the system page boundary, with size The new Windows fixture uses committed, loader-owned executable image storage and checks eight mixed-order offsets across three equal-protection pages, repeating each lookup three times. It compares the region bounds and mapped name against Windows queries, checks the structure size and zero unwind fields, and poisons the output before each call. Existing image, private-memory, protection-change and freed-memory checks remain. Native Windows 11 Pro build 26300, Ryzen 7 5800X, WinLibs GCC 15.2.0 r7, Release, SPIRV-Tools ON. This PC has an RTX 5090 / driver 617.42; these are CPU tests and do not validate GPU behavior. The test-only baseline Exact candidate
The regression reported all 24 page lookups passing. The regression executable and recorded shared runtime dependencies were byte-identical between baseline and candidate except for This is focused Windows correctness coverage, not full-suite or Linux validation. I have not measured Besiege, startup latency or FPS. The existing documented limitation around page-protection changes inside cached images remains. AI-assisted implementation and validation. |
…ched unwind region VirtualQuery describes the pages from the queried address to the end of the run of equal pages, so the reported seg0 begins at the queried page. The cached entry returned the first lookup's page for every later address in the run. It now returns the queried page and the bytes up to the end of the cached run, which is what VirtualQuery returns. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thanks for the run, the difference is real: |
…ion after a later one The backward lookup added with the region fix compared the address with a page it could not reach and never ran. The test now queries an ntdll address three pages in, then the first page, then the pages between, and compares each with VirtualQuery. The lookups after the first fail on the cache before the region fix. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Correction to my reply above: the backward lookup I described was not exercised, its guard could never be true. |
|
Rerun on Windows of head
Title: Middle Ages Ping Pong PPSA28319 (Unity IL2CPP; in my first run it made 0 calls to this function, which I did not re-measure here). 130 s per run, 2 alternating runs per side,
These are equal within run-to-run noise, as expected for a title that does not call the function. Verdict: the region mismatch I reported is fixed, and the new backward lookup in Written with Claude Code assistance (validation by a subagent, reviewed in this session). |
|
Checked the diff, the build and |
Besiege's long freeze while it loads is about 75% shorter on Windows: the longest frame goes from about 13 s to about 3 s, and it presents about 1.16 times as many frames in the first 40 seconds. Steady-state frame time is unchanged.
What
sceSysmoduleGetModuleInfoForUnwindcalledVirtualQueryandGetMappedFileNameAon every call. On WindowsVirtualQuerytakes time proportional to the size of the region it describes: measured in Besiege it costs 2 us for a 16 KiB region, 51 us for 8 MiB and 167 us for 32 MiB, and Besiege's loaded images have regions of 8 to 32 MiB. A title that unwinds exceptions while loading calls the function for every frame of every throw: 80,000 calls in the first 40 seconds, 6.7 s ofVirtualQueryon one thread, while the rest of the game waits.GetMappedFileNameAis 4.6 us per call.The region (begin, size) and the file name of an address inside an image of the Windows loader's module list are now kept per region. A cached entry is used only while the loader still reports the same image for the address (same base, time stamp and size); otherwise it is dropped and the address is queried again. Addresses outside loaded images, such as guest memory, are queried every time, so a protection change there is still seen. The reported fields are unchanged: for an address in a cached run the entry returns the queried page and the bytes to the end of the run, which is what
VirtualQueryreturns. Linux is not affected.docs/dev/debt/libSceSysmodule.mdrecords that a page protection change inside a cached image after its first lookup is not noticed.Seen in PPSA20313 (Besiege, Unity IL2CPP). A thread sample of the game while loading showed one thread spending 32% of its samples in
NtQueryVirtualMemoryunder this function.Tested
Windows 11, WinLibs GCC 15.2.0 r7, Release, RTX 4070 SUPER, NVIDIA 610.88, 1920x1080. Besiege, the first 40 s after launch (
APS5_TRACE_SECONDS=0:40), warm shader cache, three alternating runs of main at 7003eda and of this branch at cd95f1a, no otherAPS5_TRACE_*switch, the game's output sent tonul:The same three alternating runs with the game started in a console window instead (output to the console),
mainat44df21632and this branch merged onto it: longest frame 8.4 / 8.8 / 8.6 s onmain, 2.5 / 2.5 / 2.6 s with this PR; frames presented in 40 s 1240 / 1136 / 1196 -> 1355 / 1371 / 1364.Summary lines of the middle run of each:
main:
{"commit": "7003eda5d6ef", "dirty": false, "env": {"APS5_TRACE_SECONDS": "0:40"}, "gpu": "NVIDIA GeForce RTX 4070 SUPER", "driver": "NVIDIA 610.88", "os": "Windows 10.0.26100", "resolution": "1920x1080", "window": {"spec": "seconds 0:40", "first_present": 0, "ms": 40000.469}, "presents": 668, "frame_ms": {"p50": 8.721, "p90": 80.313, "p99": 100.605, "max": 13696.79}, "gpu_busy_ms_per_present": 9.05, "worker_busy_pct": {"queue worker 0x0": 57.8}, "top_phases_ms_per_frame": {"Driver.Packet": 34.528, "Driver.Packet.draw": 31.739, "Driver.Draw": 31.595, "Driver.Draw.draw_and_resource_release": 20.246, "Graphics.Draw": 20.018, "Graphics.Draw.draw_and_resource_release": 17.981, "Driver.Draw.shader_compile_and_link": 7.683, "Shader.DrawStage": 6.444}, "trace_dropped": true, "trace_dropped_threads": {"queue worker 0x0": {"events_full_after_s": 22.997, "events_lost": 799947, "totals_full": false}}, "trace": "aps5-trace-7003eda5d6ef-36496.json.gz"}this PR:
{"commit": "cd95f1a42675", "dirty": false, "env": {"APS5_TRACE_SECONDS": "0:40"}, "gpu": "NVIDIA GeForce RTX 4070 SUPER", "driver": "NVIDIA 610.88", "os": "Windows 10.0.26100", "resolution": "1920x1080", "window": {"spec": "seconds 0:40", "first_present": 0, "ms": 40036.534}, "presents": 760, "frame_ms": {"p50": 72.211, "p90": 86.39, "p99": 100.219, "max": 3306.136}, "gpu_busy_ms_per_present": 12.236, "worker_busy_pct": {"queue worker 0x0": 83.801}, "top_phases_ms_per_frame": {"Driver.Packet": 44.067, "Driver.Packet.draw": 40.706, "Driver.Draw": 40.536, "Driver.Draw.draw_and_resource_release": 26.502, "Graphics.Draw": 26.214, "Graphics.Draw.draw_and_resource_release": 23.711, "Driver.Draw.shader_compile_and_link": 9.495, "Shader.DrawStage": 7.966}, "trace_dropped": true, "trace_dropped_threads": {"queue worker 0x0": {"events_full_after_s": 12.671, "events_lost": 1209258, "totals_full": false}}, "trace": "aps5-trace-cd95f1a42675-38032.json.gz"}guest_sysmodulenow also checks on Windows that the reported region and name equal whatVirtualQueryandGetMappedFileNameAreturn, for code, data and read-only data of the test executable, a host DLL, the stack and private memory, three times each so the later calls come from the cache; a protection change in private memory and a freed region are seen. The new assertions pass on main, because the change only affects speed. Mutation check: removing the check that the address is inside the cached region fails the test. Further addresses inside an already cached run, queried forward, backward (an earlier page after a later one) and repeated, are compared withVirtualQueryas well; those checks fail on the first commit of this branch and pass on its head.guest_sysmoduleandguest_unwind_module_infopass;tools/check_conventions.pypasses.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)🤖 Generated with Claude Code