Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OutOfMemory exception when showing/hiding GoogleMaps composable #581

Open
dudeck opened this issue Jun 11, 2024 · 15 comments
Open

OutOfMemory exception when showing/hiding GoogleMaps composable #581

dudeck opened this issue Jun 11, 2024 · 15 comments
Assignees
Labels
triage me I really want to be triaged. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.

Comments

@dudeck
Copy link

dudeck commented Jun 11, 2024

Environment details

  1. Specify the API at the beginning of the title (for example, "Places: ...")
    Google Maps Compose Android

  2. OS type and version
    Mac OS Sonoma 14.5
    Android API 29 emulator

  3. Library version and other environment information
    gms-maps-compose = "com.google.maps.android:maps-compose:5.0.3"
    gms-maps-compose-utils = "com.google.maps.android:maps-compose-utils:5.0.3"

Steps to reproduce

  1. Add yours Google Maps API Key.
  2. Create Android Emulator like Pixel 3, API 29 arm64 with GooglePlay.
  3. Build app from attached code.
  4. Click multiple times on Show/Hide Map button.
  5. After at least 15 tries application crashes with OutOfMemory Exception
  6. See how Memory consumption is rising after each time map is shown.

Code example

// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.example.GMC

import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.StrokeStyle
import com.google.android.gms.maps.model.StyleSpan
import com.google.maps.android.compose.CameraPositionState
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MapProperties
import com.google.maps.android.compose.MapType
import com.google.maps.android.compose.MapUiSettings
import com.google.maps.android.compose.MarkerInfoWindowContent
import com.google.maps.android.compose.rememberCameraPositionState
import com.google.maps.android.compose.rememberMarkerState

private const val TAG = "ScrollingMapActivity"

val singapore = LatLng(1.3588227, 103.8742114)
val singapore2 = LatLng(1.40, 103.77)
val singapore3 = LatLng(1.45, 103.77)
val singapore4 = LatLng(1.50, 103.77)
val singapore5 = LatLng(1.3418, 103.8461)
val singapore6 = LatLng(1.3430, 103.8844)
val singapore7 = LatLng(1.3430, 103.9116)
val singapore8 = LatLng(1.3300, 103.8624)
val singapore9 = LatLng(1.3200, 103.8541)
val singapore10 = LatLng(1.3200, 103.8765)

val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f)

val styleSpan = StyleSpan(
    StrokeStyle.gradientBuilder(
        Color.Red.toArgb(),
        Color.Green.toArgb(),
    ).build(),
)

class MapInColumnActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // Observing and controlling the camera's state can be done with a CameraPositionState
            val cameraPositionState = rememberCameraPositionState {
                position = defaultCameraPosition
            }
            var columnScrollingEnabled by remember { mutableStateOf(true) }

            // Use a LaunchedEffect keyed on the camera moving state to enable column scrolling when the camera stops moving
            LaunchedEffect(cameraPositionState.isMoving) {
                if (!cameraPositionState.isMoving) {
                    columnScrollingEnabled = true
                    Log.d(TAG, "Map camera stopped moving - Enabling column scrolling...")
                }
            }

            MapInColumn(
                modifier = Modifier.fillMaxSize(),
                cameraPositionState,
                columnScrollingEnabled = columnScrollingEnabled,
                onMapTouched = {
                    columnScrollingEnabled = false
                    Log.d(
                        TAG,
                        "User touched map - Disabling column scrolling after user touched this Box..."
                    )
                },
                onMapLoaded = { }
            )
        }
    }
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun MapInColumn(
    modifier: Modifier = Modifier,
    cameraPositionState: CameraPositionState,
    columnScrollingEnabled: Boolean,
    onMapTouched: () -> Unit,
    onMapLoaded: () -> Unit,
) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colors.background
    ) {
        var isMapLoaded by remember { mutableStateOf(false) }
        var showMap by remember { mutableStateOf(true) }

        Column(
            Modifier
                .fillMaxSize()
                .verticalScroll(
                    rememberScrollState(),
                    columnScrollingEnabled
                ),
            horizontalAlignment = Alignment.Start
        ) {
            Spacer(modifier = Modifier.padding(10.dp))
            Button(onClick = { showMap = !showMap }) {
                Text(text = "Show/Hide map")
            }
            for (i in 1..20) {
                Text(
                    text = "Item $i",
                    modifier = Modifier
                        .padding(start = 10.dp)
                        .testTag("Item $i")
                )
            }
            Spacer(modifier = Modifier.padding(10.dp))

            Box(
                Modifier
                    .fillMaxWidth()
                    .height(200.dp)
            ) {
                if (showMap) GoogleMapViewInColumn(
                    modifier = Modifier
                        .fillMaxSize()
                        .testTag("Map")
                        .pointerInteropFilter(
                            onTouchEvent = {
                                when (it.action) {
                                    MotionEvent.ACTION_DOWN -> {
                                        onMapTouched()
                                        false
                                    }

                                    else -> {
                                        Log.d(
                                            TAG,
                                            "MotionEvent ${it.action} - this never triggers."
                                        )
                                        true
                                    }
                                }
                            }
                        ),
                    cameraPositionState = cameraPositionState,
                    onMapLoaded = {
                        isMapLoaded = true
                        onMapLoaded()
                    },
                )
                if (!isMapLoaded) {
                    androidx.compose.animation.AnimatedVisibility(
                        modifier = Modifier
                            .fillMaxSize(),
                        visible = !isMapLoaded,
                        enter = EnterTransition.None,
                        exit = fadeOut()
                    ) {
                        CircularProgressIndicator(
                            modifier = Modifier
                                .background(MaterialTheme.colors.background)
                                .wrapContentSize()
                        )
                    }
                }
            }
            Spacer(modifier = Modifier.padding(10.dp))
            for (i in 21..40) {
                Text(
                    text = "Item $i",
                    modifier = Modifier
                        .padding(start = 10.dp)
                        .testTag("Item $i")
                )
            }
            Spacer(modifier = Modifier.padding(10.dp))
        }
    }
}

@Composable
private fun GoogleMapViewInColumn(
    modifier: Modifier,
    cameraPositionState: CameraPositionState,
    onMapLoaded: () -> Unit,
) {
    val singaporeState = rememberMarkerState(position = singapore)

    var uiSettings by remember { mutableStateOf(MapUiSettings(compassEnabled = false)) }
    var mapProperties by remember {
        mutableStateOf(MapProperties(mapType = MapType.NORMAL))
    }

    GoogleMap(
        modifier = modifier,
        cameraPositionState = cameraPositionState,
        properties = mapProperties,
        uiSettings = uiSettings,
        onMapLoaded = onMapLoaded
    ) {
        // Drawing on the map is accomplished with a child-based API
        val markerClick: (Marker) -> Boolean = {
            Log.d(TAG, "${it.title} was clicked")
            cameraPositionState.projection?.let { projection ->
                Log.d(TAG, "The current projection is: $projection")
            }
            false
        }
        MarkerInfoWindowContent(
            state = singaporeState,
            title = "Singapore",
            onClick = markerClick,
            draggable = true,
        ) {
            Text(it.title ?: "Title", color = Color.Red)
        }
    }
}

Stack trace

java.lang.OutOfMemoryError: Failed to allocate a 118720 byte allocation with 106056 free bytes and 103KB until OOM, target footprint 50331648, growth limit 50331648
                                                                                                    	at m.fdu.d(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:22)
                                                                                                    	at m.fea.c(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:13)
                                                                                                    	at m.enc.k(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:8)
                                                                                                    	at m.etl.a(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:13)
                                                                                                    	at m.eum.d(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:116)
                                                                                                    	at m.erb.run(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:164)
                                                                                                    	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
                                                                                                    	at m.cab.run(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:12)
                                                                                                    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
                                                                                                    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
                                                                                                    	at m.car.run(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:40)

Note:
I checked in Memory Profiler that memory increases from around 350 MB to 450MB and then crashes.
It is easy to reproduce, need some older/low end device (tested on emulators).
It blocks us of releasing feature to the client.
I used your sample code from GoogleMaps repo:https://github.com/googlemaps/android-maps-compose
just modifying by adding Button to change visibility state of Google maps in column.

Could you fix it, please? Or at least give us some temporary quick fix solution?
Thank you in advance.

@dudeck dudeck added triage me I really want to be triaged. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. labels Jun 11, 2024
@dkhawk dkhawk self-assigned this Jun 11, 2024
@dkhawk
Copy link
Contributor

dkhawk commented Jun 11, 2024

Can be triggered with this minimal example

class MinimumMapActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var showMap by remember { mutableStateOf(true) }

            val cameraPositionState: CameraPositionState = rememberCameraPositionState() {
                position = defaultCameraPosition
            }

            val uiSettings by remember { mutableStateOf(MapUiSettings(compassEnabled = false)) }

            val mapProperties by remember {
                mutableStateOf(MapProperties(mapType = MapType.NORMAL))
            }

            LaunchedEffect(Unit) {
                while (true) {
                    delay(100.milliseconds.toJavaDuration())
                    showMap = !showMap
                }
            }

            if (showMap) {
                GoogleMap(
                    modifier = Modifier.fillMaxSize(),
                    cameraPositionState = cameraPositionState,
                    properties = mapProperties,
                    uiSettings = uiSettings,
                )
            }
        }
    }
}

Stack trace:

Process: com.example.memorybug, PID: 10996
java.lang.OutOfMemoryError: Failed to allocate a 475987 byte allocation with 245072 free bytes and 239KB until OOM, target footprint 50331648, growth limit 50331648
at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
at java.nio.DirectByteBuffer$MemoryRef.(DirectByteBuffer.java:70)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:258)
at m.fab.Y(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:8)
at m.fab.B(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:1)
at m.fdf.a(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:12)
at m.fdj.a(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:29)
at m.ezb.x(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:12)
at m.fbq.a(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:25)
at m.ezm.e(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:170)
at m.ezw.run(:com.google.android.gms.policy_maps_core_dynamite@[email protected]:2346)

@kikoso
Copy link
Collaborator

kikoso commented Jun 18, 2024

I am unfortunately not able to reproduce this on the emulator, @dkhawk . Are you using a real device?

@dkhawk
Copy link
Contributor

dkhawk commented Jun 18, 2024

This was on a pixel 6 (IIRC).

@el-qq
Copy link
Contributor

el-qq commented Jul 5, 2024

maybe this will give you some ideas

Data

  • device: Pixel 5a
  • test from the comment above

Test

  1. set delay = 100ms --> OOM happens in a minute (image 1)
  2. set delay = 300ms --> memory increases a bit, but without exception (3 minutes of stable open-close, image 2)

изображение
изображение

@el-qq
Copy link
Contributor

el-qq commented Jul 8, 2024

maybe the problem is somewhere in these lines (deleted all but this one in fun GoogleMap)

    val context = LocalContext.current
    val mapView = remember { MapView(context, googleMapOptionsFactory()) }
    MapLifecycle(mapView)

@dudeck
Copy link
Author

dudeck commented Jul 9, 2024

maybe this will give you some ideas

Data

  • device: Pixel 5a
  • test from the comment above

Test

  1. set delay = 100ms --> OOM happens in a minute (image 1)
  2. set delay = 300ms --> memory increases a bit, but without exception (3 minutes of stable open-close, image 2)

изображение изображение

In my case I was just tapping button Show/Hide 1-3s each time
image

@philip-segerfast
Copy link
Contributor

Did you try again after the last release?

@el-qq
Copy link
Contributor

el-qq commented Jul 15, 2024

yes
is repeated in the example in this comment

@bubenheimer
Copy link
Contributor

Using androidx.compose.runtime.ReusableContentHost() or friends would likely get rid of the OOM. This will leverage the ReusableComposition support from the recent release. SubcomposeLayout can be used for this as well, but is more complex.

@dudeck
Copy link
Author

dudeck commented Jul 17, 2024

Using androidx.compose.runtime.ReusableContentHost() or friends would likely get rid of the OOM. This will leverage the ReusableComposition support from the recent release. SubcomposeLayout can be used for this as well, but is more complex.

I can confirm that wrapping my Composable into ReusableContentHost(active = true) fixed OOM. Thank you for this workaround!

@bubenheimer
Copy link
Contributor

@dudeck To clarify, I had this in mind: ReusableContentHost(active = showMap) { GoogleMapViewInColumn(...) } instead of if (showMap) { GoogleMapViewInColumn(...) }

I would not expect the OOM to go away in the above code if you merely wrap it while keeping active = true at all times, but maybe it changes the GC behavior or something.

@dudeck
Copy link
Author

dudeck commented Jul 24, 2024

@bubenheimer yes, you are right. I mean when I use like you said it works only if my composable with map is NOT INSIDE a LazyColumn (as LazyListScope item). Otherwise I'm receiving:

FATAL EXCEPTION: main      Process: com.example, PID: 13495                                                                                                    java.lang.IllegalArgumentException: measure is called on a deactivated node
at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:604)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.measure-BRTryo0(LayoutNodeLayoutDelegate.kt:596)
at androidx.compose.foundation.layout.BoxMeasurePolicy.measure-3p2s80s(Box.kt:122)
at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:126)
at androidx.compose.foundation.layout.SizeNode.measure-3p2s80s(Size.kt:838)
at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
at androidx.compose.foundation.layout.PaddingNode.measure-3p2s80s(Padding.kt:397)
at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
at androidx.compose.foundation.layout.FillNode.measure-3p2s80s(Size.kt:699)
at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke(LayoutNodeLayoutDelegate.kt:252)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke(LayoutNodeLayoutDelegate.kt:251)
at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2303)
at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:500)
at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:256)
at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133)
at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:113)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1617)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:36)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:620)
at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release(LayoutNode.kt:1145)
at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release$default(LayoutNode.kt:1136)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-sdFAvZA(MeasureAndLayoutDelegate.kt:356)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:514)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded$default(MeasureAndLayoutDelegate.kt:491)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout(MeasureAndLayoutDelegate.kt:377)
at androidx.compose.ui.platform.AndroidComposeView.measureAndLayout(AndroidComposeView.android.kt:971)
at androidx.compose.ui.node.Owner.measureAndLayout$default(Owner.kt:228)
at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:1224)
at android.view.View.draw(View.java:21424)
at android.view.View.updateDisplayListIfDirty(View.java:20298)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
at android.view.View.updateDisplayListIfDirty(View.java:20258)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
at android.view.View.updateDisplayListIfDirty(View.java:20258)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
at android.view.View.updateDisplayListIfDirty(View.java:20258)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)

I need to place my component inside other list. This exception does not occurs only when I'm wrapping it with a box with set by modifier hardcoded height and when showMap == false it will deactivate google map composable node and leave empty place in a column with that hardcoded height. Unfortunately, it is not my case.

@bubenheimer
Copy link
Contributor

LazyColumnn has ReusableComposition built in these days, so I guess an additional ReusableContentHost seems odd.

@dudeck
Copy link
Author

dudeck commented Jul 25, 2024

Thanks, so is it possible to use ReusableComposition to have GoogleMaps composable inside LazyColumn that would get rid off OOM issue or maybe could you suggest other solution ?
In addition, does anyone plan from @googlemaps team to fix this issue in next near releases?

@bubenheimer
Copy link
Contributor

@dudeck not sure without experimenting or digging into impl. I assume LazyColumn would deactivate the reusable composition when an item falls off the radar. That should deactivate an item's Google map, and it would reactivate when the item gets recycled. If you structure your code around this pattern then I'd imagine it would let you avoid OOM. I've never looked at it deeply, though. YMMV

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
triage me I really want to be triaged. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Projects
None yet
Development

No branches or pull requests

6 participants