googlemaps / googlemaps/ios-maps-3d-sdk
Photorealistic tile cache grows without bound and cannot be cleared, app Jetsam-killed at 3064 MB
- Dominant language
- Swift
- Stars
- 5
- Forks
- 2
- PR merge metrics
- No merged PRs in 30d
Description
`Map` never releases the photorealistic 3D tiles it streams, and the SDK exposes no
API to bound or clear that cache. With a continuously moving camera the app's
`phys_footprint` climbs from ~400 MB to the 3072 MB high watermark in about 100
seconds, and iOS kills the process.
#### Environment details
1. **API:** Maps 3D SDK for iOS
2. **OS:** iOS 26.5.2, iPhone (14 Plus), physical device, reproduced in debug and profile builds
3. **Library version:** GoogleMaps3D 0.2.1 (latest), via Swift Package Manager
4. **Host:** Flutter app; the `Map` is in a SwiftUI view hosted in a `UIKitView`
#### Steps to reproduce
1. Mount a `Map` with `mode: .hybrid`.
2. Assign a new `Camera` every frame at 30 Hz: `altitudeMode: .relativeToGround`,
`range: 10`, `tilt: 60`, centre moving a few metres per frame.
3. Keep the camera low (~150 m) over a dense city and let it travel. Midtown
Manhattan reproduces it every time.
4. Watch `task_info(TASK_VM_INFO).phys_footprint`.
The footprint climbs steadily, never plateaus, and the process is killed at 3072 MB.
#### Code example
Self contained; it moves on its own and prints the footprint.
```swift
struct CameraStressView: View {
@State private var camera = Camera(
center: LatLngAltitude(latitude: 40.7484, longitude: -73.9857, altitude: 150),
heading: 0, tilt: 60, roll: 0, range: 10, altitudeMode: .relativeToGround)
@State private var lat = 40.7484
@State private var heading = 0.0
private let timer = Timer.publish(every: 1.0 / 30.0, on: .main, in: .common)
.autoconnect()
var body: some View {
Map(camera: $camera, mode: .hybrid) { }
.ignoresSafeArea()
.onReceive(timer) { _ in
lat += 0.0000063 // ~22 m/s
heading = (heading + 0.05).truncatingRemainder(dividingBy: 360)
camera = Camera(
center: LatLngAltitude(latitude: lat, longitude: -73.9857, altitude: 150),
heading: heading, tilt: 60, roll: 0, range: 10,
altitudeMode: .relativeToGround)
if let mb = footprintMB() { print("footprint: \(mb) MB") }
}
}
private func footprintMB() -> Int? {
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(
MemoryLayout.size / MemoryLayout.size)
let kr = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
guard kr == KERN_SUCCESS else { return nil }
return Int(info.phys_footprint) / 1_048_576
}
}
```
#### Stack trace
```
Process 5378 stopped
* thread #71, name = 'SQLiteDiskCache_Service',
stop reason = EXC_RESOURCE (RESOURCE_TYPE_MEMORY:
high watermark memory limit exceeded) (limit=3072 MB)
frame #0: libsystem_platform.dylib`_platform_memset + 108
```
Another run stopped on the main thread in `libsystem_malloc.dylib` instead. In
release builds on TestFlight there is no trace at all, the app just disappears,
which is the Jetsam signature.
#### What we measured
* **Growth tracks unique ground seen.** With the camera stationary the footprint is
flat for minutes. Resume movement and it climbs again. So this is the tile cache
accumulating, not a per frame allocation.
* **Recreating the `Map` does not free it.** We changed the SwiftUI identity
(`.id(generation)`) to rebuild the whole surface, on a timer and on
`didReceiveMemoryWarning`. One session rebuilt it **22 times** while the footprint
went past 3 GB. It never dropped. The cache looks process scoped rather than owned
by the view, consistent with `Map.apiKey` being static.
* **Memory warnings arrive and shedding does not help.** We get
`didReceiveMemoryWarningNotification` in bursts, drop every overlay we own and
clear our image caches. The footprint keeps climbing.
* **Limiting what is visible only slows it down.** We confined the camera to a 600 m
radius, capped altitude at 160 m, clamped tilt to 60 degrees so the horizon never
enters frame, and narrowed `fieldOfView` to 28 degrees. Total visible ground is
then a few km², a finite set. Still reaches 3 GB.
* **Plain panning accumulates too.** Browsing alone leaves the app at ~800 MB in
Manhattan, so long sessions walk toward the same limit with no moving camera.
#### The request
Expose a way to bound or clear the tile cache. The capability is already in the
engine; these symbols are present in `GoogleMaps3D.framework/GoogleMaps3D`:
```
SetTargetMemoryCacheSizeMb(target_mb = %f)
GetTargetMemoryCacheSizeMb()
GetCurrentMemoryCacheSizeMb()
ClearMapTileCache
setMemoryCacheSizeMb:
```
None is reachable. The public Swift interface has nothing matching `cache`, `memory`,
`tile`, `budget`, `evict` or `purge`, and there are no exported C++ symbols for the
above, so `dlsym` is not an option either.
Any one of these would resolve it:
1. **A cache budget**, e.g. a `Map` modifier `.tileCacheBudget(megabytes:)`, or a
public equivalent of `SetTargetMemoryCacheSizeMb`. This is what we would use.
2. **An explicit purge**, a public equivalent of `ClearMapTileCache`.
3. **Eviction of tiles outside the current view**, or honouring
`didReceiveMemoryWarning` internally.
For comparison, CesiumJS consuming the same Google Photorealistic 3D Tiles honours
`cacheBytes`: we measured 511 MB held against a 512 MB budget across 194k tiles, with
the camera driven the same way.
#### Impact
Any use of the SDK with a continuously moving camera is unshippable on iOS today,
and long browsing sessions trend the same way. Every mitigation available to us is
at its limit and the crash still happens.
Happy to provide a reproducer project, footprint traces or an Instruments capture.
cc @bertankofon @ekimcem
Contributor guide
Research direction
Start with the CameraStressView reproducer and the exported symbols in GoogleMaps3D.framework/GoogleMaps3D, then compare the public Swift interface for cache controls. Confirm that movement grows phys_footprint and that a public budget or purge keeps memory bounded without requiring Map recreation; done means the SDK exposes and honors a usable cache limit or clear operation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- flutter, ios, swift
- Domain
- api, mobile-dev, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 32/100