Skip to content

Keep async completions that land while a transition is signalled - #615

Merged
simonhamp merged 1 commit into
mainfrom
fix/keep-completions-during-transition-signal
Oct 26, 2026
Merged

simonhamp merged 1 commit into
mainfrom
fix/keep-completions-during-transition-signal

Conversation

@simonhamp

Copy link
Copy Markdown
Member

#555 made flushDeferredTransition() take queued async completions before nativephp_element_reset(), so a task the new screen dispatched from mount() wasn't dropped. But it took them before signalling the transition, and the reset comes after. On iOS, NativeUI.Transition.Set does a DispatchQueue.main.sync, and during a tab switch the main thread is busy with the outgoing screen. A task that finishes during that wait gets posted, and then the reset drops it. The task has finished, so its watchdog is already disarmed, and the screen waits forever.

This moves the drain to after the signal, right before the reset. The window goes from however long the main thread is busy to the gap between two C calls.

dev/null still hits this: its tabs draw skeleton rows, dispatch a no-op task from mount(), and fill in when it comes back. In the edge-nav log from an iPhone 16 Pro (Release, OPcache), 972 tab switches:

  • First publish (which includes the transition signal) under 4 ms: 3% stayed on skeletons.
  • 7 ms or more: 17 to 20% did.
  • Notifications draws its skeleton in 1 to 2 ms and stalled on 42 of 204 switches. Remotes takes about 10 ms, so its task is usually back before the signal, and it stalled once in 83.

Android's Transition.Set doesn't hop threads, so the window there was already small.

The new test queues a completion from inside the NativeUI.Transition.Set call. It fails before the change and passes after. The full suite passes (2329 tests).

Not yet tried on a phone with the fix in.

🤖 Generated with Claude Code

flushDeferredTransition() took the queued async completions, then
signalled the transition, then reset. On iOS the signal
(NativeUI.Transition.Set) waits on the main thread, which is busy with
the outgoing screen at that moment. A task the new screen started in
mount() could finish during that wait, and the reset dropped its
completion. The task had finished, so its watchdog was disarmed too, and
the screen waited forever.

The completions are now taken after the signal, right before the reset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@simonhamp
simonhamp marked this pull request as ready for review October 26, 2026 12:20
@simonhamp
simonhamp merged commit f4d8101 into main Oct 26, 2026
6 checks passed
@simonhamp
simonhamp deleted the fix/keep-completions-during-transition-signal branch October 26, 2026 16:19
gwleuverink added a commit that referenced this pull request Nov 2, 2026
Covers mobile PRs #525, #527, #528, #560, #605, #608, #609, #611, #613, #614, #615, #616, #617, #619, #621, #622, #623, #624, #625, #626, #627, #628, #629, #632, #633, #634, #635 and #636, and the commits on main without a PR: 4082538 (bounded()'s docblock), 49528ba (the Android logging fix from a fork), b23aba9, 2a773cc and b9316b6 (security policy, issue template and README links). Main b107ed9..767a7c8, 33 commits, 4.6.1 among them.

Ported to core on branch sync/mobile-main-2026-11-01, which this merge needs: #608 (scrollVirtualListTo()), #625 with 4082538 (snap points on a gesture area), #621 (:context-menu on a pressable, matched through core's ConvertsToElement as :menu is), #617's farewell frame through a hook, and #614's ValidatesInput and FieldErrors, which move to core. The old names of those two resolve to core's through the alias shim.

Kept on this branch: #616 in full. EVENT_SYSTEM_BACK sits on NativeComponent next to EVENT_HOT_RELOAD and EVENT_SHUTDOWN, the router half is in NativeRouter, and InteractsWithAppShell's back button handler checks the constant, as main's runLoop() does. #617's element types: NativeComponent answers core's typesClosedInFarewellFrame() hook with bottom_sheet and modal. BroadcastsGlobally extends core's marker and #609's DispatchableFromNative.

Removed again in the merge: HasVirtualListWindow, GestureArea and Pressable (main changed them, core has them), and ValidatesInput and FieldErrors (main added them, core has them). Main's import of the old dispatcher name came out of DispatchEventFromAppController, where it clashed with core's.

Adapted for the extraction: BroadcastsGloballyNamesTest expects DispatchableFromNative among the old marker's interfaces, as #609 makes it.

#609 is merged as main has it and is not ported.

Needs NativePHP/core#18.

Tests, all on PHP 8.4.27, with dev dependencies updated for #628 (Laravel 12.70.2, Pest 3.8.7): mobile on this merge against the core sync branch (445049b), 2,467 passed, 4 risky, 4 skipped. PHPStan clean. Mobile main (767a7c8), 2,412 passed, 4 risky, 4 skipped. Desktop main (fd40580) against the core sync branch, test:php 2,240 passed, as against core main. Desktop #57 (977a2af), 2,245 passed against the earlier core sync head (59c73fc), which differs only by the constant moved to mobile; not rerun.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant