Repository navigation
perf(os): wake WaitAny only for objects a multi-wait watches - #1353
Merged
NotRexed merged 6 commits intoNov 10, 2026
Merged
Conversation
Every event signal, semaphore release, queue change and timer start poked one global condition variable that every WaitAny sleeps on. So each change anywhere in the game woke every multi-wait waiter to re-check its holders. OoT's three nn.npln threads wait on their own events, which are never set offline, and woke ~8,500 times a second each: 57% of the game's context switches and ~0.4 of a logical CPU in a WPR capture. Each event, timer, semaphore and queue now counts the holders that link it into a multi-wait (LinkMultiWaitHolder / UnlinkMultiWaitHolder), and poke_multiwait(o) wakes WaitAny only when that count is non-zero. The signal stores and then reads the count with a full barrier; a link increments it with one before the WaitAny reads the state, so one side always sees the other. Thread and native-handle holders were never woken by a poke. HOS_MW_WAKE=all restores the old wake-everything behaviour. tests/test_multiwait_wake.py compiles the real poke_multiwait with a counting harness and checks every poke site and the link/unlink count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review: UnlinkAll cleared the multi-wait's links without decrementing the watched objects' counts, so after Link -> UnlinkAll those objects kept phantom watchers and their changes woke every WaitAny again. MoveAll moves holders between multi-waits and leaves the counts as they are. (A holder finalized while still linked keeps its count: more wake-ups, as before this change, not fewer; the 5 ms re-check stays a latency backstop.) tests/test_multiwait_wake.py: the real Link/Unlink/UnlinkAll/MoveAll and holder_target compiled together, two multi-waits, two holders over one event, one over another and a thread holder; each object's count is checked after every step. Fails on the previous commit at UnlinkAll. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review: holder_target runs under mw_lock (Link, Unlink, UnlinkAll), and obj() is a virtual-core scheduling point (hos_vcore_checkpoint): with HOS_VCORE on and a kick pending it could give its core away while holding mw_lock, and a higher-priority thread then blocking on mw_lock waits until the 250 ms escape. obj_lookup is the same lookup without the checkpoint (the HLE entries already checkpoint before taking the lock). tests/test_multiwait_wake.py: the link fixture counts obj() calls made while mw_lock is held and fails on any (the previous commit made 8). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Thanks, both lifetime issues were real. Fixed in f0fe694 by moving the count off the object.
The fixture is still single-threaded. The cross-thread argument is that every read and write of the table happens under |
Review of NotRexed#1353: the count lived on the target Obj, so re-initializing the target (fresh() -> the tombstone path's memset, which several SystemEvent getters do on every call) zeroed it while holders still linked it, and Unlink/UnlinkAll looked the target up with obj_lookup, which creates objects: after a finalize it brought the tombstone back, or turned a reused address's live object into an event. The watched addresses and their holder counts now live in a small table under mw_lock. Link/Unlink/UnlinkAll update it by the holder's guest address and never look the target up; poke_multiwait checks it under mw_lock (taken there before too) only when a WaitAny is waiting, so a link before the check is seen and a link after it is followed by a scan that sees the stored state. tests/test_multiwait_wake.py compiles the real table, poke_multiwait, holder_watch and Link/Unlink/UnlinkAll/MoveAll: counts after every step, a re-initialized watched event still wakes, finalize-before-unlink resurrects nothing, and no object lookup happens in link, unlink or poke. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f0fe694 named its table `watched`, and nn_os_sync.c already has a `watched()` (the mutex watch log): the host did not compile. The table, its type and helpers are now MwWatched / mw_watched / mw_watched_at_locked / mw_watch_add_locked / mw_watch_remove_locked. Checked by a syntax-only compile of the whole file, not only the test's extracted functions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If growing the watched-address table fails, that holder's address goes uncounted, so its changes would wake WaitAny only on the 5 ms re-check, and unlinking it could drop the count of another holder of the same address. A sticky flag now falls back to the old wake-everything policy for the rest of the process once any registration fails. The test injects the allocation failure and checks both cases. (Review by Codex.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
|
This landed in the v2.0.12 branch (merged there for testing; it reaches |
Owner
|
Reopened: closing this was a mistake on my side. It was tested in the v2.0.12 branch, and it will be merged into main here, so it shows as merged. Sorry for the noise. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
In offline OoT, unrelated event, timer, semaphore and queue changes repeatedly wake online-service threads in WaitAny, consuming CPU while their events remain idle.
Why merge
HOS_MW_WAKE=allforces the old policy.Change
host/nn_os_sync.c: track watched addresses undermw_lockand maintain holder counts.tests/test_multiwait_wake.py: check lifetimes, notification filtering and allocation failure.tests/run_all.py: register the check.Tested
python -I -B tests/test_multiwait_wake.py --cxx clang++compiles the real table, poke and holder operations with counting stubs and injected allocation failure. Fails on the old code, passes now.HOS_MW_WAKE=allcontrol: CPU ms/frame −6.4%; median fps 55.5 → 57.3, every pair higher.VUID-VkImageCreateInfo-imageCreateMaxMipLevels-02251remains.Not covered