Speed up the Maps & Resources screen - #5765
Conversation
prepareData matched every region against every repository resource - 1738 x 8812 pairs, twice over - and now resolves each resource through a download-name index: 2.82 s -> 0.03 s. Region areas are cached instead of being recomputed inside sort comparators. Opening the screen no longer reloads the region tree unless regions.ocbf changed, and the catalog refresh runs off the main thread: main thread unavailable 1878 ms -> 587 ms, startup to resource groups 4.9 s -> 1.4 s.
| dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ | ||
| // Reloading the region tree drops the group items built on startup, so do it only when regions.ocbf changed | ||
| if (ocbfUpdated) | ||
| [_app loadWorldRegions]; |
There was a problem hiding this comment.
Could publishing the new worldRegion from this background queue race with readers on the main thread? loadWorldRegions directly replaces the shared _worldRegion, while self.region and the region-resource cache are updated only later. This path is currently disabled by the early return in OAOcbfHelper, but when downloading is re-enabled, would it be safer to build the tree off-thread and publish it together with the related UI/cache updates on the main thread?
There was a problem hiding this comment.
Good catch, fixed in 1b33761.
You are right about what would break. The screen compares regions by identity - self.region == _app.worldRegion gates the section layout in seven places in prepareContent, plus viewDidAppear, shouldHideBanner and getItemByIndexPath: - so swapping the tree from a background queue would flip all of those mid-render and desynchronise the sections from the cells already on screen. _resourcesByRegions is also keyed by OAWorldRegion *__weak, so the old keys zero out once the previous tree is released.
One note on scope: this hazard predates the PR rather than being introduced by it. The original block called [_app loadWorldRegions] and self.region = _app.worldRegion inside the OCBF completion, which only happens to run on the main thread today because downloadOcbfIfUpdated: returns early; once downloading is re-enabled that completion arrives on an NSURLSession queue and both lines run off-thread. The same off-thread call exists in OAFirstUsageWizardController.updateRepository. So the PR narrowed the window (it already published self.region on the main thread) but did not close it - worth closing properly, as you say.
loadWorldRegions is now split into readWorldRegions, which reads the tree from disk and publishes nothing, and applyWorldRegions:, which publishes it. updateRepository reads off-thread and applies on the main thread in the same block as self.region, updateContent (which rebuilds the region-keyed cache) and buildResourceGroupItem, so nothing observes a half-updated state. loadWorldRegions stays as the composition of the two for the startup path, where there is no UI yet. The wizard got the same treatment.
One behaviour change to flag: applyWorldRegions: ignores nil, so a failed loadFrom: now keeps the previous tree instead of leaving _worldRegion nil as before. Happy to drop that guard if you would rather keep the refactor pure - both new call sites already check the result before publishing.
loadWorldRegions replaced the shared _worldRegion wherever it was called from, so reloading it off-thread let the main thread see a new tree while self.region and the region-keyed resource cache still pointed at the old one - and the screen compares those by identity to decide its section layout. It is now split into readWorldRegions, which stays off the main thread, and applyWorldRegions:, called on the main thread together with self.region, updateContent and buildResourceGroupItem.
Opening Maps & Resources froze the UI, and the screens reached from it (Nautical maps, Travel guides) were slower still. Measured on an iPhone 17 simulator, Debug build, by sampling the main thread over the whole open and checking coverage against the app's own log timestamps.
What was slow
prepareDatamatched every region against every repository resource. 1738 regions x 8812 entries = 15.3M pairs, and twice over: once in thedoInitscan and once in the deleted-map swap, which walked the whole repository per region although only 7 entries are flagged deleted.ResourceMatchesRegionalso calledWorldRegions::TravelRegionId.toNSString()on every one of those pairs, allocating an NSString inside the hottest loop. Cost: 2.82 s on every catalog rebuild.Region areas were recomputed inside sort comparators.
getAreawalks the region polygon on each call, andrequestMapDownloadInfocalls it from a comparator, sobuildResourceGroupItemspent most of its time iteratingQVector<Point<int>>- plus twoNSNumberboxes per comparison.The catalog refresh ran on the main thread.
viewDidAppear->updateRepositoryshowed a HUD and then, inline on the main thread, reloaded the entire region tree, ran the repository update synchronously and rebuilt every resource group.downloadOcbfIfUpdated:calls its completion synchronously (the download itself is disabled by an early return), so nothing yielded to the run loop - which is also why the progress HUD never actually appeared. Reloading the tree threw away the group items built at startup, forcingbuildResourceGroupItemto redo everything.Changes
prepareDatabuilds a download-name index once and resolves each resource through it. A region'sdownloadsIdPrefixis its download name plus a dot and core builds resource ids as<download name>.<suffix>, so the prefix match is an exact lookup by the id up to its first dot. Regions matched by extension or by a partial prefix (world, others, custom) keep the scan path. The deleted-map swap iterates the deleted entries only, and the travel-region check is hoisted out of the per-resource predicate.OAWorldRegioncaches its area (the polygon never changes) and the three comparators compare doubles instead of boxingNSNumber.downloadOcbfIfUpdated:reports whetherregions.ocbfactually changed, and the region tree is reloaded only when it did.loadWorldRegionsand the synchronous repository update moved off the main thread; onlyupdateContentand the UI stay on it.Measurements
Main thread unavailable (CPU + blocked) during the first open:
Startup to
world region resource group built: 4.92 s -> 1.33 s, of whichprepareData2.82 s -> 0.03 s andbuildResourceGroupItem0.76 s -> 0.20 s.Equivalence of the new matching was checked against the old predicate over the 8812 real ids from
repository.cache.xmland 2601 prefixes, including empty, dot-only and multi-dot prefixes: 15998 matches both ways, identical.Notes for review
BOOL updatedplumbing insideOAOcbfHelperlives in the code after the unconditionalreturn, so it cannot run today. It is there so that re-enabling the downloader keeps reloading regions; the alternative is to passNOeverywhere and leave a silent trap behind.OADownloadedRegionsLayeris outside this screen but carried the same boxing pattern and benefits from the cached area.Testing
Maps & Resources, Nautical maps, Travel guides and the installed-maps screen all render the same content as before; the catalog refresh button still works and leaves the list intact.