D651 - The thread that was not calling

Status: measured Date: 2026-09-09

Three rounds of hand instrumentation to learn two facts

D650 established that PPSA25872 raises signal 30 on a thread other than the caller, and sketched a delivery mechanism: a pending-signal slot checked on the way into the thunk dispatch. Before building it, the question was whether it would ever fire - the target has to call something for a dispatch-entry check to see the signal.

It would not. Measured:

DIAG cross-thread raise at call 310985 of the run   (of 310986)

The raise is the second-to-last call in the entire run. The sketched mechanism is ruled out by measurement, and finding that out cost a println rather than the mechanism plus its hot-path load on all 310,986 dispatches.

Two more rounds established that the caller is an AssetGarbageCollectorHelper and the target is main, and that the call after the raise is sceKernelWaitSema - a collector suspending a mutator and then waiting for it to acknowledge. Textbook stop-the-world.

Every one of those three rounds re-derived something the process already knew. The thread registry holds names, handles and finished flags. The recorded-call ring holds a label, a sequence and a host thread. Neither could be read against the other, because nothing recorded which host thread a guest handle was running on.

The join, and what it costs

ThreadRecord gains a host field, filled in become_thread - on the thread itself, because a thread is created by one thread and runs on another, so the host identity is only knowable once it is the one asking. orbistoun_thunk::host_thread becomes public to supply it.

CallTrace gains threads: Vec<ThreadNote>, joined at collection time, and the run report prints it when the guest went quiet - the case that had a fault site and a tail for every other kind of ending and nothing at all for this one.

Grouped by name, and that is the whole of its usefulness. The first version printed one line per thread, and Unity's thirteen identically-named helpers filled the block and pushed main off the end of it - the thread the signal was aimed at, invisible in a report about the signal. Grouped, and sorted so the busiest is last, the same run says it in three lines.

What the three lines then said, which nobody had asked

threads  15 guest thread(s), 13 with no call in the recorded window
         AssetGarbageCollectorHelper    x13              no call in the last 48 of the run
         main                           0x5e2d00000200   last called sceKernelSyncOnAddressWait at 310583
         (unnamed)                      0x5e2d00001dc0   last called sceKernelWaitSema at 310985

main is parked inside sceKernelSyncOnAddressWait - a wait orbistoun implements and owns.

That reverses this record's own opening. Delivery to a running native thread is out of reach; that was never in question. But the target here is not running native code - it is asleep in orbistoun's own futex wait, at a point orbistoun chose, holding a stack orbistoun can see. Marking a pending signal on its handle, waking it out of that wait, running the handler there, and returning it to the wait is a bounded mechanism at a place this project already controls.

Nobody asked for that fact. It fell out of printing what the report should have been printing all along, which is the argument for the field rather than for the three printlns that preceded it.

The limit, stated before it is built

Delivery would reach a thread parked in an orbistoun wait and no other. A mutator spinning in pure guest code still never takes the signal, and this title only works because Unity's mutator happens to be blocked on a futex. That is a real restriction and it goes in the next record, not this one - this one is about being able to see the difference.