Skip to content

Speed up the Maps & Resources screen - #5765

Merged
alex-dev-neo merged 3 commits into
masterfrom
speed-up-maps-and-resources
Sep 15, 2026
Merged

alex-dev-neo merged 3 commits into
masterfrom
speed-up-maps-and-resources

Conversation

@alex-dev-neo

Copy link
Copy Markdown
Contributor

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

prepareData matched every region against every repository resource. 1738 regions x 8812 entries = 15.3M pairs, and twice over: once in the doInit scan and once in the deleted-map swap, which walked the whole repository per region although only 7 entries are flagged deleted. ResourceMatchesRegion also called WorldRegions::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. getArea walks the region polygon on each call, and requestMapDownloadInfo calls it from a comparator, so buildResourceGroupItem spent most of its time iterating QVector<Point<int>> - plus two NSNumber boxes per comparison.

The catalog refresh ran on the main thread. viewDidAppear -> updateRepository showed 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, forcing buildResourceGroupItem to redo everything.

Changes

  • prepareData builds a download-name index once and resolves each resource through it. A region's downloadsIdPrefix is 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.
  • OAWorldRegion caches its area (the polygon never changes) and the three comparators compare doubles instead of boxing NSNumber.
  • downloadOcbfIfUpdated: reports whether regions.ocbf actually changed, and the region tree is reloaded only when it did. loadWorldRegions and the synchronous repository update moved off the main thread; only updateContent and the UI stay on it.

Measurements

Main thread unavailable (CPU + blocked) during the first open:

before after
Maps & Resources 1878 ms 587 ms
Nautical maps - 489 ms (includes two back transitions)
Travel guides - 208 ms
Map markers, for reference 270 ms 270 ms

Startup to world region resource group built: 4.92 s -> 1.33 s, of which prepareData 2.82 s -> 0.03 s and buildResourceGroupItem 0.76 s -> 0.20 s.

Equivalence of the new matching was checked against the old predicate over the 8812 real ids from repository.cache.xml and 2601 prefixes, including empty, dot-only and multi-dot prefixes: 15998 matches both ways, identical.

Notes for review

  • The BOOL updated plumbing inside OAOcbfHelper lives in the code after the unconditional return, so it cannot run today. It is there so that re-enabling the downloader keeps reloading regions; the alternative is to pass NO everywhere and leave a silent trap behind.
  • The progress HUD now renders for ~0.7 s while the catalog refreshes in the background, where before it was shown and hidden without the run loop ever turning.
  • OADownloadedRegionsLayer is 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.

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.
@alex-dev-neo
alex-dev-neo requested a review from tigrim September 11, 2026 15:48
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];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@alex-dev-neo
alex-dev-neo merged commit 76d9baa into master Sep 15, 2026
@alex-dev-neo
alex-dev-neo deleted the speed-up-maps-and-resources branch September 15, 2026 14:07
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.

2 participants