perf(os): wake WaitAny only for objects a multi-wait watches #1353
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!1353
Loading…
Reference in a new issue
No description provided.
Delete branch "host-multiwait-targeted-wake"
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 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
Nice find on the npln wakeups. I went through the ordering and the main path looks right. A signal stores under
e->lkand then does a full-barrier read ofmw_linked, and Link increments undermw_lockbefore any WaitAny scans, so signal-vs-link can't drop a wakeup. Linking while a WaitAny is already asleep behaves the same as before (neither version pokes on link). MoveAll keeps the counts. CI is green.Two things around object lifetime, both inline:
Obj, so re-initializing the target zeroes it (thememsetinobj_lookup's same-address tombstone path) while holders still link that address. Several HLE getters re-runevent_initon every call, so this can happen in practice. After that, signals on a watched object skip the poke and WaitAny only notices on its 5 ms recheck. It's added latency, not a hang.holder_targetusesobj_lookup, which creates objects. Unlink/UnlinkAll now looks up the target at teardown, often after it was finalized. That brings the tombstone back as a live object, and if the address has since been reused for another kind it wipes that live object.Tests: both fixtures are single-threaded with stubbed lookups. They check the poke filter and the link/unlink counts but no races, and no re-init or finalize-before-unlink. A case that re-initializes a linked event and then signals it would cover (1).
Nit: the comment and the
checkpoints_lockedcheck sayobjmust never run undermw_lock, butwait_any→holder_readyalready callsobj()under it. That's fine as-is, the wording just overstates it.This field is after
owner, so the re-init path inobj_lookupzeroes it:fresh()sets kind to 0, thenfind_dead_lockedreuses the tombstone and doesmemset(&o->owner, ...). Holders link by guest address, so after a re-init the count says 0 while holders are still linked. For example,GetDisplayVsyncEvent,NvOsDrvQueryEvent, prepoGetCompletionEventandAcquireAudioDeviceNotificationForOutputall callhos_system_event_init→freshon every call. Call one of them again while a holder for that SystemEvent is linked, and later signals skippoke_multiwait, so WaitAny only notices on its 5 ms recheck. The next Unlink then takes it to -1, which pokes forever. That direction is harmless but it's wrong. Since the count is per address, the simplest fix is probably to carrymw_linkedacross the same-key tombstone reuse inobj_lookup(save before the memset, restore after). The spare/new-address path should keep resetting it.obj_lookupcreates objects, so it's risky in Unlink/UnlinkAll, which usually run at teardown. If the event was already finalized, this brings its tombstone back as a liveK_EVENT, which is never buried, and the count goes to -1. If the memory now holds a different kind of object (say a mutex),obj_lookupsets that live object's kind to 0 and memsets it into an event. Before this PR, Unlink never touched the target. On the unlink side I'd use a non-creating lookup, e.g.find_unlocked(h->event, kind), and skip when it returns NULL.holder_readyhas the same create-on-miss behaviour, but only while a WaitAny is actually scanning.Thanks, both lifetime issues were real. Fixed in
f0fe694by moving the count off the object.mw_lock, keyed by the guest address the holder links.fresh()and the tombstonememsetno longer touch the count. The fixture re-initializes a linked event, signals it, and checks that it still wakes.Link,UnlinkandUnlinkAllno longer look the target up at all; they use the holder's address.poke_multiwaitchecks the table undermw_lock, which it already took before broadcasting, and only when a WaitAny is waiting. A link before the check is seen; a link after it is followed by a scan that sees the stored state. The fixture finalizes an event before unlinking it and checks that nothing is resurrected and that no object lookup happens in link, unlink or poke.wait_any/holder_readyalready callobjundermw_lock. That's pre-existing, and this PR no longer adds any lookup under the lock or claims there are none.The fixture is still single-threaded. The cross-thread argument is that every read and write of the table happens under
mw_lock.This landed in the v2.0.12 branch (merged there for testing; it reaches
mainwith that release), so closing. Thanks!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.
Fixed in
f0fe694. The count now lives inmw_watched, keyed by guest address, sofresh()and the tombstone memset no longer touch it, andmw_watch_remove_lockeddrops the entry at zero instead of going to -1. The fixture covers re-init then signal.Fixed. Link/Unlink/UnlinkAll now go through
holder_watch(h), which only readsh->event, so nothing on the unlink path looks the target up or recreates it. The finalize-before-unlink case is in the fixture.Re-reviewed f0fe694..7b9f02f. Both lifetime issues are fixed. The watcher count is now a per-address table under
mw_lock, so re-initializing the target no longer zeroes it, and Link/Unlink/UnlinkAll useholder_watch(h)(justh->event), so nothing on the unlink path recreates a finalized object. The ordering argument still holds.poke_multiwaitreads the table undermw_lock, the same lock Link and the WaitAny scan take, and themw_waitingfast path is the same as before. The stickymw_watch_incompletefallback handles the allocation-failure undercount correctly. The fixture now covers re-init, finalize-before-unlink and OOM, and it's registered in run_all.py.Minor, not blocking: if a holder is re-initialized or finalized while it's still linked, its watch entry leaks, because
fresh()clearsh->mwand Unlink then skips the remove. That only means extra wakes, i.e. the old behaviour for that address, and the dangling link itself was already there before this PR.Looks good to me. The two Python/SPIR-V CI jobs were still pending when I checked, and everything else is green.