-
Notifications
You must be signed in to change notification settings - Fork 122
Streamline MGLMapSnapshotter; upgrade to Mapbox GL Native v1.4.0 #210
Conversation
|
||
__weak __typeof__(self) weakSelf = self; | ||
dispatch_async(workQueue, ^{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need to decorate the image on a worker thread? MGLAttributedSnapshot()
doesn’t even show up in time profiles of a basic snapshot at 1512 × 1288 points. Removing this worker thread dance would greatly simplify the code: we could make this method synchronous and have it call a completion block that dispatches to the result queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi there. Just looking at https://github.com/mapbox/mapbox-gl-native-ios/blob/bdb2e61624b30b57fcbb9b9c38540526aeab364f/platform/darwin/src/MGLMapSnapshotter.mm#L274
It looks like this decoration is happening on the same queue that the snapshot is occurring on? Is that correct? If that's the case I don't see much of a reason unless that queue is expected to be busy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed - the only thing I wonder is if the overlayHandler
rendering takes time...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, that comment is outdated. The block passed into MapSnapshotter::snapshot()
is executed on the same queue that -[MGLMapSnapshotter startWithQueue:completionHandler:]
is called from, though the actual resource fetching and rendering happens on a background thread as usual. -decorateImage:withOptions:attributions:pointForFn:latLngForFn:overlayHandler:completionHandler:queue:
does the decoration on a different background queue, including running overlayHandler
, and finally the results are dispatched to the queue passed into -startWithQueue:completionHandler:
. In all, there are three queues involved just at the SDK level, not counting what happens internally to mbgl.
The overlayHandler
could take arbitrary time depending on what the developer specifies. If we remove workQueue
from the picture, would it make sense to run the overlayHandler on the initial queue that calls -startWithQueue:completionHandler:
or on the queue passed into that method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MGLAttributedSnapshot() doesn’t even show up in time profiles of a basic snapshot at 1512 × 1288 points.
I don't have context on what this is, but we're about to greatly expand what can be done to snapshots with runtime styling. It seems like this is the wrong time to move everything to the main thread.
Removing this worker thread dance would greatly simplify the code: we could make this method synchronous and have it call a completion block that dispatches to the result queue.
Then snapshot generation could potentially block the main thread, right? Not sure this is a good idea. There are use cases where 6-7 of these snapshots are being generated at the same time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sorry if I responded to an outdated comment.
would it make sense to run the overlayHandler on the initial queue that calls -startWithQueue:completionHandler: or on the queue passed into that method?
I think this makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of mapbox/mapbox-gl-native#16268, the block that’s literally defined inline inside of -[MGLMapSnapshotter startWithQueue:completionHandler:]
does run on the queue on which -startWithQueue:completionHandler:
is called, typically the main queue. That change took place so that the developer can safely implement the runtime styling hooks called for in #200 without accidentally creating race conditions between the main thread and some background thread they have no awareness of. However, the bulk of the work continues to occur on background threads.
What I’m still unsure of is which queue the developer expects the overlay handler to be called on:
- The initial queue on which
-startWithQueue:completionHandler:
is called (think main queue) - The special-purpose worker queue that the developer has no control over (current behavior, could lead to race conditions or deadlocking)
- The queue that the developer passed into
-startWithQueue:completionHandler:
ostensibly for the purpose of receiving the finished snapshot
Despite the amount of code involved, MGLAttributedSnapshot()
and the methods it calls are so trivial that they can comfortably occur on any thread. But the overlayHandler
callback is less certain because the developer has free rein to perform expensive tasks in that callback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, I’ve kept the decorator queue but consolidated the dispatching into a single method, making the other methods synchronous on whatever queue they’re called on. This will simplify any potential change that would keep the snapshotter alive until completion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just my 2 cents:
I think it's fine to utilize the queue the developer passed in for the overlay handler work with a few caveats:
- They could pass in the main queue. This, in my very opinionated view, would be a developer error in this context and I would assert on it. However given this is an existing API, it's hard to assert on it without probably generating support tickets 🙃. Plus many developers (myself included) use the main queue as a sync point for completion handlers.
- Related to 1 - I'm unfamiliar with the queues mbgl is using but the interplay of multiple queues here has me looking at potential priority inversion issues. If the developer passes in a queue at the utility QOS, it'll slow the whole process down. Just something to keep in mind.
Tangentially related to this:
https://github.com/mapbox/mapbox-gl-native-ios/blob/master/platform/darwin/src/MGLMapSnapshotter.h#L241
That function should document what queue the completion handler is called on when developer's don't pass in a queue.
|
||
if (completion) { | ||
__typeof__(self) strongSelf = weakSelf; | ||
strongSelf.loading = NO; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is happening on the result queue, but loading
could’ve been set to YES
on a different queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks - this looks a lot cleaner; getting rid of the cruft certainly makes it more readable/understandable.
My minor niggle is the completion-block-not-called-on-cancel/dealloc but I massively prefer the cleaner code that results.
Quoting from this comment:
As far as I can tell, we don’t document anywhere that the completion handler does get called in this situation, so it would be a backwards-compatible change. The developer has full control over what happens when they call -cancel; they’re free to call that completion block themselves.
Perhaps the solution here is to clearly document this requirement, as a test for this situation (maybe even update the examples), and update mapbox/mapbox-gl-native#12336 as I fear this change will break these developers' code.
With code this difficult (including the C++/Obj-C dance), it's hard to fully understand the logic; my hope is that the integration tests catch the bulk of the issues.
cc @chloekraw @knov
@1ec5 mimicking @julianrex's concern in regards to the potential for the |
bdb2e61
to
360726e
Compare
The integration tests for this class cover a variety of concurrency scenarios. (I added a couple of my own.) This class has been a bugfarm in the past, so there’s some amount of risk involved with touching it in any way. But I think removing the expectation of always calling the completion handler reduces the class’s scope enough to be manageable. I’m not even sure we need to worry about the application termination case anymore, though I left it in there for consistency with MGLMapView.
This is a valid concern, given that we’ll be departing from the existing behavior of this class, not to mention MKMapSnapshotter. For starters, I’ve updated the changelog and MGLMapSnapshotter.h, including the example code in that header. This example (Objective-C, Swift) happens to continue to function because it recommends capturing the snapshotter in the completion block. However, if the snapshot were ever to get canceled for any reason, the completion block won’t get called, causing the snapshotter to leak. For this reason, we should recommend a more straightforward ownership model where the class that uses the snapshotter holds onto it, at least until we figure out a safe, maintainable way to keep the snapshotter alive. /cc @mapbox/maps-ios |
The “Show Snapshots” command in iosapp seems to expose a hang in the snapshotter. For example, if MBXSnapshotsViewController looks like this: then these two snapshotters haven’t finished: If I then push Back, the application attempts to cancel the snapshots, but instead the main thread is blocked until the snapshots run to completion:
|
Intermittently, some snapshots randomly take a long time to appear, suggesting a deadlock. Once again, if MBXSnapshotsViewController looks like this: then these two snapshotters haven’t finished: and the corresponding
All four
|
#211 keeps the snapshotter alive until after the completion handler executes. It probably needs more scrutiny than the snapshotter changes in this PR, since it may reintroduce some edge cases that this PR had eliminated as part of limiting the snapshotter’s responsibilities. |
I just noticed the alexshalamov_styleable_snapshotter branch, which has a more targeted update to MGLMapSnapshotter. However, it also hangs when canceling. Sometimes it hangs indefinitely. |
@alexshalamov was able to reproduce the hang on master, so we’ll track it in #214. |
Upgraded MGLMapSnapshotter to be compatible with the latest mbgl. Converted as many of MGLMapSnapshotter’s instance variables and properties as possible into local variables close to the point of use. The completion handler of -startWithCompletionHandler: and its variants is no longer called as part of -cancel, when the snapshotter is deallocated, or when the application is terminating. -loading once again works and is used to avoid concurrent requests, now that the completion handler is no longer memoized. Snapshotter options are copied and converted into a snapshotter on demand as late as possible. Made the decoration step synchronous while continuing to perform it on a worker thread. Only access self on user-facing queues. Ensure MGLMapSnapshotter.loading is reset after snapshotting. Pushed some drawing and completion code up from inner completion blocks to outer completion blocks to avoid excessively passing state around. Converted a few class methods into standalone C functions to emphasize the avoidance of captured state.
055a482
to
d357f8c
Compare
Upgraded MGLMapSnapshotter to be compatible with mbgl v1.4.0. For the iOS map SDK v5.8.0-beta.1 and macOS map SDK v0.15.0-beta.1 release notes, we’ll add:
MGLMapSnapshotter
no longer keeps itself from being deallocated before its completion handler is called. To ensure that the completion handler passed into-[MGLMapSnapshotter startWithCompletionHandler:]
is called, maintain a strong reference to the snapshotter from a longer-lived class, such as the class where you call-[MGLMapSnapshotter startWithCompletionHandler:]
. (Streamline MGLMapSnapshotter; upgrade to Mapbox GL Native v1.4.0 #210)-[MGLMapSnapshotter cancel]
method no longer calls the completion handler passed into-[MGLMapSnapshotter startWithCompletionHandler:]
. (Streamline MGLMapSnapshotter; upgrade to Mapbox GL Native v1.4.0 #210)MGLMapSnapshotter.loading
property always returnedNO
, even while loading a snapshot. (Streamline MGLMapSnapshotter; upgrade to Mapbox GL Native v1.4.0 #210)Converted as many of MGLMapSnapshotter’s instance variables and properties as possible into local variables close to the point of use.
-loading
once again works and is used to avoid concurrent requests, now that the completion handler is no longer memoized. Snapshotter options are converted into anmbgl::MapSnapshotter
on demand as late as possible.The completion handler of
-startWithCompletionHandler:
and its variants is no longer called as part of-cancel
, when the snapshotter is deallocated, or when the application is terminating, per the rationale in #200 (comment). To ensure that the snapshotter runs to completion, it is necessary to hold a strong reference to the snapshotter, ideally in the containing class.Pushed some drawing and completion code up from inner completion blocks to outer completion blocks to avoid excessively passing state around. Converted a few class methods into standalone C functions to emphasize the avoidance of captured state.
More to come:
Fixes #209. Undoes much of mapbox/mapbox-gl-native#12355. Depends on mapbox/mapbox-gl-native#16268. Working towards #200.
/cc @mapbox/maps-ios @alexshalamov