Avoid allocating global offline area grid - #97
Merged
Merged
Conversation
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AREAS = generateAreas()allocation with an O(1) coordinate-to-area calculation.areaBox(latitudeIndex, longitudeIndex)helper.Motivation
Importing the
mapspackage currently creates a global slice containing 1,045,456Areavalues before the first tile lookup. On a 64-bit build, eachAreais 56 bytes, so the slice accounts for 55.83 MiB before considering other process memory.FindWaysAroundPositionthen linearly scans that slice to locate one 0.25-degree tile. If a tile cannot be loaded, the old implementation allocates another 55.83 MiB slice, constructs the complete grid again, and rescans the entire slice for the fallback box.That fallback is hit more often than tile crossings alone would suggest. The main loop re-enters
FindWaysAroundPositionwheneverlen(state.Data.Ways()) == 0, and that condition stays true for as long as no tile loads. On a device without tiles downloaded for the current area, the old path therefore ran on every GPS fix inside a loop with a 50 ms delay.This change calculates the same tile directly from latitude and longitude while preserving the existing first-match behavior at exact tile boundaries.
Memory impact
mapd-memory-raw.csv
68813e0)ad3c618)The PSS values are reported as mean +/- sample standard deviation.
Benchmark methodology:
pfeifer.dev/mapd/maps, forcedruntime.GC()anddebug.FreeOSMemory(), and then remained idle./proc/<pid>/smaps_rollup; Go heap values came fromruntime.MemStats.The measurements were collected at optimized commit
ad3c618. Final commit6b8c3eapreserves the same package-initialization behavior; its additional changes share the area-box calculation and size the offline generation grid exactly.The runtime lookup changes from a linear scan of up to 1,045,456 boxes to constant-time arithmetic, and the fallback grid allocation is removed entirely.
Correctness fixes
Three latent defects fall out of the rewrite:
break, so the last matching area won, while the filename it had just attempted came from the first match. On an exact 0.25-degree boundary, the fallback box could therefore disagree with the attempted tile path. The new code uses the same calculated area for both.make([]Area, int((361/0.25)*(181/0.25)))over-allocated 8,656 slots beyond the 1,036,800 real cells. Those trailing zero-valueAreas have box(0,0)-(0,0), and the last-match rescan selected one of them at exactly(0, 0).GenerateOfflineiterated the whole slice, so generating a region containing the origin could write a degenerateoffline/0/0/0.000000_0.000000_0.000000_0.000000tile. Sizing the slice tolatitudeAreas * longitudeAreasremoves those slots.Validation
A temporary comparison harness was used for focused and exhaustive validation without adding test-only code to this commit:
generateAreas()is bit-identical to the previous implementation across all 1,036,800 real cells; the 8,656 removed slots were all zero-valued.areaForPosition.math.IsNaNguard is required: NaN fails both range comparisons, and converting the result ofmath.Ceil(NaN)to an integer is implementation-dependent.go test ./...andgo vet ./...passed for the final source on Linux/amd64 with Go 1.25.1.Compatibility
generateAreas()remains available for offline map generation and now uses the sharedareaBoxhelper.AREA_BOX_DEGREESvalues, repeated addition and indexed multiplication can round differently. Generation and lookup now derive bounds through the same helper.AREASwas exported. Nothing in this repository referenced it, but removing it is a package-level API change for any external consumer that did.