perf(store-screenshots): wait for the map to draw instead of a fixed 45 seconds (#7501)

This commit is contained in:
James Rich authored and GitHub committed 2026-09-30 18:39:02 +00:00
1 parent ee54232a98
commit 0c9d007da9
3 files changed
+42 -4

No files matched your search

@@ -706,7 +706,11 @@ fun MapView(
mapType = effectiveGoogleMapType,
isMyLocationEnabled = isLocationTrackingEnabled && locationPermission.isGranted,
),
onMapLoaded = { isMapLoaded = true },
onMapLoaded = {
isMapLoaded = true
// The store-screenshot capture waits for this tag instead of a fixed delay.
Logger.withTag("MapDrawn").d { "tiles drawn" }
},
onMapClick = { latLng ->
if (isMainMode && boxAuthoringDraft != null) {
val first = boxAuthoringFirstCorner
@@ -31,8 +31,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -51,6 +54,7 @@ import org.maplibre.compose.location.updateCamera
import org.maplibre.compose.map.CameraConstraints
import org.maplibre.compose.map.LocalMapState
import org.maplibre.compose.map.LocalViewport
import org.maplibre.compose.map.MapEvent
import org.maplibre.compose.map.MapState
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.rememberMapState
@@ -228,6 +232,13 @@ fun MeshMap(
// thrown by the time this is reached.
if (!LocalMapLibreRuntimeProbe.current()) return MapEngineUnavailable(modifier)
// The store-screenshot capture waits for this tag instead of a fixed delay.
LaunchedEffect(mapState) {
// Idle can arrive before the first render session; only an idle after a drawn frame means tiles are on screen.
mapState.events.dropWhile { it !is MapEvent.FrameRendered }.filterIsInstance<MapEvent.Idle>().first()
Logger.withTag("MapDrawn").d { "tiles drawn" }
}
val zoomRange = basemap.zoomRange()
MaplibreMap(
modifier = modifier,
@@ -132,7 +132,7 @@ class StoreScreenshots {
open(Shot.Nodes.path)
SystemClock.sleep(READ_WARM_UP_MS)
}
open(shot.path)
if (shot.waitsForMapDrawn) openAndAwaitMapDrawn(shot) else open(shot.path)
SystemClock.sleep(shot.minimumWaitMs)
val stable =
waitForStableInActiveWindow(
@@ -150,6 +150,22 @@ class StoreScreenshots {
save(bitmap, name)
}
/**
* Waits for the map to log that its tiles are drawn: Google Maps' onMapLoaded, MapLibre's first idle. The stability
* check after it still covers the camera settling on the mesh.
*/
private fun UiAutomatorTestScope.openAndAwaitMapDrawn(shot: Shot) {
// A time boundary, not a line count: logcat is a ring buffer and older matches rotate out.
val since = System.currentTimeMillis().let { "%d.%03d".format(it / MILLIS_PER_SECOND, it % MILLIS_PER_SECOND) }
open(shot.path)
val deadline = SystemClock.uptimeMillis() + MAP_DRAWN_TIMEOUT_MS
while (SystemClock.uptimeMillis() < deadline) {
if (MAP_DRAWN_MESSAGE in shell("logcat -d -T $since -s $MAP_DRAWN_TAG")) return
SystemClock.sleep(POLL_MS)
}
Log.w(TAG, "${shot.fileName} never logged $MAP_DRAWN_TAG; capturing after the timeout")
}
/** Launches through the debug build's shell-only alias, the one launch the app honours the switches on. */
private fun UiAutomatorTestScope.open(path: String, clearTask: Boolean = false) {
val flags = if (clearTask) "--activity-clear-task " else ""
@@ -189,7 +205,7 @@ class StoreScreenshots {
TenInch("tenInchScreenshots", 2560, 1440, 320),
}
/** The five listing shots, named as fastlane lays them out. The map loads tiles for a while before it settles. */
/** The five listing shots, named as fastlane lays them out. The map waits for its tiles before it settles. */
private enum class Shot(
val fileName: String,
val path: String,
@@ -197,11 +213,12 @@ class StoreScreenshots {
val stableTimeoutMs: Long = 30_000,
val stableIntervalMs: Long = 2_000,
val readFirst: Boolean = false,
val waitsForMapDrawn: Boolean = false,
) {
// The primary channel's contact key, raw: `am start` takes it literally and Uri.parse accepts the caret.
Messages("1_messages", "messages/0^all", readFirst = true),
Nodes("2_nodes", "nodes"),
Map("3_map", "map", minimumWaitMs = 45_000, stableTimeoutMs = 120_000, stableIntervalMs = 8_000),
Map("3_map", "map", stableTimeoutMs = 120_000, stableIntervalMs = 8_000, waitsForMapDrawn = true),
NodeDetail("4_node_detail", "nodes/$RIDGE_TOP_NUM"),
Channels("5_channels", "channels"),
}
@@ -230,6 +247,12 @@ class StoreScreenshots {
const val READ_WARM_UP_MS = 3_000L
/** Logged by both flavors' maps (MapView.kt, MeshMap.kt) once the tiles are drawn. */
const val MAP_DRAWN_TAG = "MapDrawn"
const val MAP_DRAWN_MESSAGE = "tiles drawn"
const val MAP_DRAWN_TIMEOUT_MS = 45_000L
const val MILLIS_PER_SECOND = 1_000L
const val CONNECT_ATTEMPTS = 3
const val CONNECT_TIMEOUT_MS = 60_000L
const val POLL_MS = 500L