Select to view content in your preferred language

MapView is Stuck on Screen Rotation

367
6
05-03-2024 12:35 PM
dev4567
New Contributor III

Context

Prerequisites

1. Have a slow-slow connection: e.g.: on emulator go to Developers options, Network Download Rate Limit, set 128kb.

2. Enable screen rotation.

Actions

  1. Zoom in to initiate tiles loading;
  2. Rotate device when tiles are partially loaded;
  3. Repeat several times.

Result

  • Map composable is not drawn at all for a period of time (no background, nothing);
  • Heap Dump shows memory leak: several ArcGISMap instances are present (one inside the MapView, and others leak to own StateFlow internally.

It seems like basemap's baseLayers remote loading is not cancelled when MapView is destroyed due to lifecycle. cancelLoad on any item (ArcGISMap, Basemap, Layer) doesn't give any effect.

0 Kudos
6 Replies
dev4567
New Contributor III

Code snippet I was playing with to try to understand the issue:

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // authentication with an API key or named user is
        // required to access basemaps and other location services
        ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY)

        enableEdgeToEdge()

        setContent {
            SampleAppTheme {
                val context = LocalContext.current
                // create a map with a navigation night basemap style
                val map = remember { ArcGISMap(Basemap()) }
                var insets by remember { mutableStateOf(PaddingValues(0.dp)) }

                MapView(
                    modifier = Modifier.fillMaxSize(),
                    arcGISMap = map,
                    isAttributionBarVisible = false,
                    insets = insets
                )
                Column(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(WindowInsets.systemBars.asPaddingValues()),
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.spacedBy(16.dp)
                ) {
                    var count by remember {
                        mutableIntStateOf(0)
                    }
                    Button(
                        onClick = { count++ },
                        content = { Text(text = "Click me") }
                    )
                    Text(text = count.toString())
                }

                LaunchedEffect(Unit) {
                    delay(500L)
                    insets = PaddingValues(32.dp)
                }

                LaunchedEffect(Unit) {
                    delay(100L)
                    map.basemap.value?.baseLayers?.add(ArcGISTiledLayer("https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer"))
                    delay(500L)
                    map.basemap.value?.referenceLayers?.add(ArcGISVectorTiledLayer("https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/layers/arcgis/imagery/labels"))
                }

                DisposableEffect(Unit) {
                    onDispose {
                        map.basemap
                        println("!")
                    }
                }
            }
        }
    }
}
0 Kudos
dev4567
New Contributor III

Also, what I've found in the other project is that ArcGISMap seems to be leaking inside NetworkAuthenticationInterceptor: 

dev4567_0-1714801211729.png

 

0 Kudos
dev4567
New Contributor III

in previous release 200.3 display-map sample has another issue on rotation with bad connection: I could see two MapView in the heap, which makes me think the problem could be connected with 200.4 leak 🤔  could it be rendering thread / Looper and the lifecycle in the GeoView?

0 Kudos
dev4567
New Contributor III

Also, it feels like reading tiles is done via DefaultDispatcher. As a result -> with bad connection default dispatcher is over-queued easily as it's not intended for long-running operations. If so, http tile reading operations should be switched to IO and asap.

dev4567_1-1714834965800.png

dev4567_0-1714834911882.png

dev4567_2-1714835625103.png

 

0 Kudos
dev4567
New Contributor III

The hacky solution is:

 

private fun ArcGISEnvironment.fixCoroutineScope() = this::class.apply {
    val setCoroutineScopeFun = this.java.methods.find { it.name.startsWith("setScope") } ?: return@apply
    val arcgisCoroutineScope =
        CoroutineScope(
            SupervisorJob() + Dispatchers.IO +
                    CoroutineExceptionHandler { _, throwable ->
                        Log.w("ArcGISEnvironment", throwable)
                    },
        )
    setCoroutineScopeFun.invoke(this.objectInstance, arcgisCoroutineScope)
}

 

Would be cool to see setScope fun being open -> to control the dispatcher, but also e.g. if we want to react on exceptions on our (client) terms.

0 Kudos
HudsonMiears
Esri Contributor

Hello,

Thanks for reporting this and the detailed information you've provided. We have investigated your issues.

Map composable is not drawn at all for a period of time (no background, nothing);

Based on our observations this is expected due to the limited bandwidth of the network set on the device.

Heap Dump shows memory leak: several ArcGISMap instances are present (one inside the MapView, and others leak to own StateFlow internally.

We have implemented a stress test that rotates the device periodically over a long period of time. Based on this test, we believe that the additional allocations of the ArcGISMap that you are seeing in the heap dump are due to your code that uses `remember` to create an ArcGISMap:

val map = remember { ArcGISMap(Basemap()) }

 `remember` only caches the object across recompositions, but not if the composition is destroyed and recreated, as it is during a configuration change. This is the reason why you see multiple ArcGISMap instances, as they are allocated with each screen rotation. To ensure only one allocation is used, we recommend using a ViewModel to store the map.

Regardless of whether you use `remember` to store the map or a ViewModel, we have not observed any memory leaks in the memory profiler when running our stress test.

Also, it feels like reading tiles is done via DefaultDispatcher. As a result -> with bad connection default dispatcher is over-queued easily as it's not intended for long-running operations. If so, http tile reading operations should be switched to IO and asap.

It is correct that network requests are initiated and processed on the DefaultDispatcher, however the requests are actually executed on a separate threadpool owned by the OkHttpClient. While requests are executing, the coroutines on the DefaultDispatcher are suspended but they are not blocking any threads. In other words, the behavior you are observing is working as expected.

Would be cool to see setScope fun being open -> to control the dispatcher, but also e.g. if we want to react on exceptions on our (client) terms.

That's a good point. We are going to consider this as an enhancement in a future release. In particular, setting a CoroutineExceptionHandler might be useful.

0 Kudos