Repository navigation
fix(android): don't drop events that come within 10 ms of the previous one - #4426
aderiushev wants to merge 2 commits into
Conversation
mfazekas
left a comment
There was a problem hiding this comment.
Thanks, this is a good find. I could reproduce the stale onCameraChanged on main (25 of 33 idles in the slow run, 10 of 13 in the fast one), and with this PR it's 0 in both, at about the same event volume. Removing the drop also fixes lost onImageMissing events (only 1–3 of 20 arrived on main) and one map's events dropping another map's. The drop dates back to 2017 and was only a precaution for the old bridge, so I'm happy to see it go.
A few changes before merging:
-
Presses shouldn't be coalesced. Every event that inherits
canCoalesce() == truefromevents/AbstractEvent.ktcan now be merged by Fabric. With JS busy for a few seconds, 4 taps on aShapeSourcearrived as a singleonPress(main delivers all 4); vector and raster source presses work the same way. MapView presses survived in my tests, but only because other touch events happened to be queued between them. Please makecanCoalesce()default tofalseand opt in onlyCameraChangeEvent,CameraChangedEventand the intermediate point-annotation drag event. -
React Native 0.79 is still supported (
peerDependencies: >=0.79, Expo SDK 53). On 0.79,EventQueue::enqueueUniqueEventreplaces the newest queued event of the same name for the view without checking whether that one was coalescable; that check came in 0.80 (react/react-native#50988). SincetopMapChangecarries both coalescable camera changes and non-coalescable idle / style-loaded / frame events, a camera change could overwrite a queuedonMapIdleoronDidFinishLoadingStylethere. Please give camera changes their own event name on Android (e.g.topCameraChanged, as on iOS), sotopMapChangeonly carries events that are never merged. -
The comment on
dispatchModernis longer than it needs to be and the reason given for surface id -1 isn't quite right: it's -1 because the event is created withEvent(viewId), and it applies to every component event, not just the map view. One sentence is enough, e.g.// The default dispatchModern() uses dispatch() for surfaceId -1, which Fabric never coalesces.
Optional cleanup: IEvent.timestamp and events/AbstractEvent.equals look unused once the drop is gone.
- canCoalesce() defaults to false; CameraChangeEvent, CameraChangedEvent and the point annotation drag moves (not drag start/end) opt in - camera changes get their own event name on Android (topCameraChanged, as on iOS), so topMapChange only carries events that are never merged (RN 0.79 merges unique events by name without checking that the queued one is unique) - one-line dispatchModern comment; drop the unused IEvent.timestamp and equals
|
Thanks for the review and for measuring it. Pushed b4386d1:
I also removed Checked in the example app (Android, New Architecture) with the reproducer from the description, plus a
The iOS Detox failure in the last run ( |
Description
Fixes #4414
On Android,
AbstractEventEmitter.handleEventdropped an event when one with the same"<key>-<type>"had been sent at most 10 ms earlier, and it never sent the dropped one later. During an animated camera move the map makes two camera updates per frame, so the second one was always dropped, and when the last frame's update was dropped,onCameraChanged(onMapViewand onCameraGestureObserver) stopped on a stale camera.The drop was there to protect the bridge, but
canCoalesce()never reached React Native:components/AbstractEventdispatches through the legacydispatch(), which Fabric sends as an event that cannot be coalesced.This PR:
dispatchModernincomponents/AbstractEvent, so Fabric getscanCoalesce()and merges queued camera changes of a view. The surface id stays -1, so Fabric finds the view by its tag, as it does today.Events that can't be coalesced (
MapChangeEvent: idle, style loaded, region, frame; image missing; Android callbacks; map steady) are never merged, and now they are not dropped either. Fabric merges a new event only into the newest queued event of the same view, and only when that one is coalescable and has the same name (EventQueue.cpp), so camera events can't swallow the othertopMapChangeevents. The counts below showonMapIdleandonDidFinishRenderingFrameFullyarriving as before. iOS is not changed.Other events keep
canCoalesce() == true: inherited fromevents/AbstractEvent(press, feature press, point annotation select/drag, user tracking mode), or set byLocationEvent. Fabric can now merge two of the same kind that are queued for one view in the same frame, keeping the newer one; before, the second one within 10 ms was dropped. If presses should never merge, they can overridecanCoalesce()likeAndroidCallbackEventdoes — I left that out to keep this change small.Things I tried first, for the review:
Event(surfaceId, viewTag)+getEventData()):UIManagerHelper.getSurfaceId(view)is -1 for the map view, because its context is the activity (getMapViewContext). The events then took the legacy path again (no merging), and every event logged adoes not have SurfaceIdsoft exception. A surface id stored per manager would send events of a map on another surface to the wrong surface.topCameraChanged, as on iOS): not needed. With this change, camera events merge although they sharetopMapChangewith idle and frame events (same call counts as onmain).Checklist
CONTRIBUTING.mdyarn generatein the root folder (no changes)/exampleapp./example)Also:
./gradlew :rnmapbox_maps:testDebugUnitTestpasses (14 tests).Screenshot OR Video
The reproducer below in the example app (
mainat b42e5ed, React Native 0.84.1, New Architecture, Mapbox 11.33.0), Android 13 emulator (arm64). Each build: a fresh start, then Slow x30 (30 moves, one every 3 s), then Fast x10 (10 bursts of 15 back-to-back moves 200 ms apart, each move interrupting the last, then 3 s to go idle).mainMapView.onCameraChanged≠ the camera inonMapIdleCameraGestureObserver.onCameraChanged≠ the camera inonMapIdleonMapIdle/onDidFinishRenderingFrameFully/MapView.onCameraChangedcallsMapView/CameraGestureObserveronCameraChangedat the idle after a burstonMapIdle/onDidFinishRenderingFrameFully/MapView.onCameraChangedcallsAn example of a stale idle on
main:map 12.0000 | onCameraChanged 12.0060 | observer 12.0060. The frame counts depend on how fast the emulator renders (software GL on a busy host); they show the frame events still arrive.The map's start camera from #4414 did not reproduce in the example app on
main(the firstonCameraChangedalready had the start camera); in our app on 10.3.5 it did. The per-frame drop reproduces onmainevery time.Component to reproduce the issue you're fixing
BugReportExample.js — Slow x30 / Fast x10, counts stale
onCameraChangedat eachonMapIdle🤖 Generated with Claude Code