diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/benchmark/BenchmarkActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/benchmark/BenchmarkActivity.kt index 19eadd755..f12153510 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/benchmark/BenchmarkActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/benchmark/BenchmarkActivity.kt @@ -9,76 +9,76 @@ import android.os.Handler import android.view.View import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.putJsonObject -import org.maplibre.android.BuildConfig.GIT_REVISION +import org.maplibre.android.camera.CameraUpdate import org.maplibre.android.camera.CameraUpdateFactory import org.maplibre.android.geometry.LatLng import org.maplibre.android.log.Logger import org.maplibre.android.log.Logger.INFO -import org.maplibre.android.maps.* +import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.MapLibreMap.CancelableCallback -import org.maplibre.android.testapp.BuildConfig +import org.maplibre.android.maps.MapView import org.maplibre.android.testapp.R -import org.maplibre.android.testapp.utils.FpsStore -import org.maplibre.android.testapp.utils.BenchmarkResults +import org.maplibre.android.testapp.utils.BenchmarkInputData +import org.maplibre.android.testapp.utils.BenchmarkResult +import org.maplibre.android.testapp.utils.BenchmarkRun +import org.maplibre.android.testapp.utils.BenchmarkRunResult import org.maplibre.android.testapp.utils.FrameTimeStore +import org.maplibre.android.testapp.utils.jsonPayload import java.io.File +import java.util.ArrayList +import kotlin.collections.flatMap +import kotlin.collections.toTypedArray +import kotlin.coroutines.resume -data class BenchmarkInputData( - val styleNames: List, - val styleURLs: List, -) { - init { - if (styleNames.size != styleURLs.size) - throw Error("Different size: styleNames=$styleNames, styleURLs=$styleURLs") - } -} +suspend fun MapLibreMap.animateCameraSuspend( + cameraUpdate: CameraUpdate, + durationMs: Int +): Unit = suspendCancellableCoroutine { continuation -> + animateCamera(cameraUpdate, durationMs, object : CancelableCallback { + var resumed = false -@SuppressLint("NewApi") -fun jsonPayload(styleNames: List, fpsResults: BenchmarkResults, encodingTimeResults: BenchmarkResults, renderingTimeResults: BenchmarkResults): JsonObject { - return buildJsonObject { - putJsonObject("resultsPerStyle") { - for (style in styleNames) { - putJsonObject(style) { - fpsResults.resultsPerStyle[style].let { results -> - if (results !== null) { - put("avgFps", JsonPrimitive(results.map { it.average }.average())) - put("low1pFps", JsonPrimitive(results.map { it.low1p }.average())) - } - } + override fun onCancel() { + continuation.cancel() + } - encodingTimeResults.resultsPerStyle[style].let { results -> - if (results !== null) { - put("avgEncodingTime", JsonPrimitive(results.map { it.average }.average())) - put("low1pEncodingTime", JsonPrimitive(results.map { it.low1p }.average())) - } - } - - renderingTimeResults.resultsPerStyle[style].let { results -> - if (results !== null) { - put("avgRenderingTime", JsonPrimitive(results.map { it.average }.average())) - put("low1pRenderingTime", JsonPrimitive(results.map { it.low1p }.average())) - } - } - } + override fun onFinish() { + if (!resumed) { + resumed = true + continuation.resume(Unit) } } - put("deviceManufacturer", JsonPrimitive(Build.MANUFACTURER)) - put("model", JsonPrimitive(Build.MODEL)) - put("renderer", JsonPrimitive(BuildConfig.FLAVOR)) - put("debugBuild", JsonPrimitive(BuildConfig.DEBUG)) - put("gitRevision", JsonPrimitive(GIT_REVISION)) - } + }) } +suspend fun MapView.setStyleSuspend(styleUrl: String): Unit = + suspendCancellableCoroutine { continuation -> + var listener: MapView.OnDidFinishLoadingStyleListener? = null + + var resumed = false + listener = MapView.OnDidFinishLoadingStyleListener { + if (!resumed) { + resumed = true + listener?.let { removeOnDidFinishLoadingStyleListener(it) } + continuation.resume(Unit) + } + } + addOnDidFinishLoadingStyleListener(listener) + getMapAsync { map -> map.setStyle(styleUrl) } + + continuation.invokeOnCancellation { + removeOnDidFinishLoadingStyleListener(listener) + } + + } + /** * Benchmark using a [android.view.TextureView] */ @@ -88,14 +88,6 @@ class BenchmarkActivity : AppCompatActivity() { private lateinit var mapView: MapView private var handler: Handler? = null private var delayed: Runnable? = null - private var fpsStore = FpsStore() - private var encodingTimeStore = FrameTimeStore() - private var renderingTimeStore = FrameTimeStore() - private var fpsResults = BenchmarkResults() - private var encodingTimeResults = BenchmarkResults() - private var renderingTimeResults = BenchmarkResults() - private var runsLeft = 5 - private var measureFrameTime = true // the styles used for the benchmark // can be overridden adding with developer-config.xml @@ -160,7 +152,8 @@ class BenchmarkActivity : AppCompatActivity() { "Facebook Light", "Americana", "Protomaps Light", - "Versatiles Colorful" + "Versatiles Colorful", + "OpenFreeMap Bright" ), styleURLs = listOf( "https://maps.geo.us-east-2.amazonaws.com/maps/v0/maps/OpenDataStyle/style-descriptor?key=v1.public.eyJqdGkiOiI1NjY5ZTU4My0yNWQwLTQ5MjctODhkMS03OGUxOTY4Y2RhMzgifR_7GLT66TNRXhZJ4KyJ-GK1TPYD9DaWuc5o6YyVmlikVwMaLvEs_iqkCIydspe_vjmgUVsIQstkGoInXV_nd5CcmqRMMa-_wb66SxDdbeRDvmmkpy2Ow_LX9GJDgL2bbiCws0wupJPFDwWCWFLwpK9ICmzGvNcrPbX5uczOQL0N8V9iUvziA52a1WWkZucIf6MUViFRf3XoFkyAT15Ll0NDypAzY63Bnj8_zS8bOaCvJaQqcXM9lrbTusy8Ftq8cEbbK5aMFapXRjug7qcrzUiQ5sr0g23qdMvnKJQFfo7JuQn8vwAksxrQm6A0ByceEXSfyaBoVpFcTzEclxUomhY.NjAyMWJkZWUtMGMyOS00NmRkLThjZTMtODEyOTkzZTUyMTBi", @@ -168,6 +161,7 @@ class BenchmarkActivity : AppCompatActivity() { "https://americanamap.org/style.json", "https://api.protomaps.com/styles/v2/light.json?key=e761cc7daedf832a", "https://tiles.versatiles.org/assets/styles/colorful.json", + "https://tiles.openfreemap.org/styles/bright" ) ) } @@ -195,85 +189,64 @@ class BenchmarkActivity : AppCompatActivity() { private fun setupMapView() { mapView = findViewById(R.id.mapView) as MapView - if (measureFrameTime) { - mapView.addOnDidFinishRenderingFrameListener { _: Boolean, frameEncodingTime: Double, frameRenderingTime: Double -> - encodingTimeStore.add(frameEncodingTime * 1e3) - renderingTimeStore.add(frameRenderingTime * 1e3) - } - } mapView.getMapAsync { maplibreMap: MapLibreMap -> - maplibreMap.setStyle(inputData.styleURLs[0]) - maplibreMap.setSwapBehaviorFlush(measureFrameTime) - if (!measureFrameTime) { - maplibreMap.setOnFpsChangedListener { fps: Double -> - fpsStore.add(fps) - } - } + val benchmarkResult = BenchmarkResult(arrayListOf()) - // Start an animation on the map as well - flyTo(maplibreMap, 0, 0,14.0) + lifecycleScope.launch { + val benchmarkRuns = inputData.styleNames.zip(inputData.styleURLs).flatMap { (styleName, styleUrl) -> + listOf( + BenchmarkRun(styleName, styleUrl, true), + BenchmarkRun(styleName, styleUrl, false) + ) + }.toTypedArray() + val benchmarkIterations = 5 + for (i in 0 until benchmarkIterations) { + for (benchmarkRun in benchmarkRuns) { + val benchmarkRunResult = doBenchmarkRun(maplibreMap, benchmarkRun) + val benchmarkPair = Pair(benchmarkRun, benchmarkRunResult) + benchmarkResult.runs.add(benchmarkPair) + println(jsonPayload(BenchmarkResult(arrayListOf(benchmarkPair)))) + } + } + + println(jsonPayload(benchmarkResult)) + storeResults(benchmarkResult) + benchmarkDone() + } } } - private fun flyTo(maplibreMap: MapLibreMap, place: Int, style: Int, zoom: Double) { - maplibreMap.animateCamera( - CameraUpdateFactory.newLatLngZoom(PLACES[place], zoom), - 10000, - object : CancelableCallback { - override fun onCancel() { - delayed = Runnable { - delayed = null - flyTo(maplibreMap, place, style, zoom) - } - delayed?.let { - handler!!.postDelayed(it, 2000) - } - } + private suspend fun doBenchmarkRun(maplibreMap: MapLibreMap, benchmarkRun: BenchmarkRun): BenchmarkRunResult { + var numFrames = 0 - override fun onFinish() { - if (place == PLACES.size - 1) { // done with tour - if (measureFrameTime) { - encodingTimeResults.addResult( - inputData.styleNames[style], - encodingTimeStore - ) - encodingTimeStore.reset() + val encodingTimeStore = FrameTimeStore() + val renderingTimeStore = FrameTimeStore() - println("Encoding time results $encodingTimeResults") + maplibreMap.setSwapBehaviorFlush(benchmarkRun.syncRendering) - renderingTimeResults.addResult( - inputData.styleNames[style], - renderingTimeStore - ) - renderingTimeStore.reset() + val listener = MapView.OnDidFinishRenderingFrameListener { _: Boolean, frameEncodingTime: Double, frameRenderingTime: Double -> + encodingTimeStore.add(frameEncodingTime * 1e3) + renderingTimeStore.add(frameRenderingTime * 1e3) + numFrames++; + } + mapView.addOnDidFinishRenderingFrameListener(listener) + mapView.setStyleSuspend(benchmarkRun.styleURL) + numFrames = 0 - println("Rendering time results $renderingTimeResults") - } else { - fpsResults.addResult(inputData.styleNames[style], fpsStore) - fpsStore.reset() + val startTime = System.nanoTime() - println("FPS results $fpsResults") - } - println("Benchmark ${jsonPayload(inputData.styleNames, fpsResults, encodingTimeResults, renderingTimeResults)}") + for (place in PLACES) { + maplibreMap.animateCameraSuspend( + CameraUpdateFactory.newLatLngZoom(place, 14.0), + 10000 + ) + } + val endTime = System.nanoTime() + val fps = (numFrames * 1E9) / (endTime - startTime) - if (style < inputData.styleURLs.size - 1) { // continue with next style - maplibreMap.setStyle(inputData.styleURLs[style + 1]) - flyTo(maplibreMap, 0, style + 1, zoom) - } else if (runsLeft > 0) { // start over - --runsLeft - maplibreMap.setStyle(inputData.styleURLs[0]) - flyTo(maplibreMap, 0, 0, zoom) - } else { - benchmarkDone() - } - return - } + mapView.removeOnDidFinishRenderingFrameListener(listener) - // continue with next place - flyTo(maplibreMap, place + 1, style, zoom) - } - } - ) + return BenchmarkRunResult(fps, encodingTimeStore, renderingTimeStore) } override fun onStart() { @@ -314,16 +287,14 @@ class BenchmarkActivity : AppCompatActivity() { mapView.onLowMemory() } - private fun storeResults() { - val payload = jsonPayload(inputData.styleNames, fpsResults, encodingTimeResults, renderingTimeResults) - + private fun storeResults(benchmarkResult: BenchmarkResult) { + val payload = jsonPayload(benchmarkResult) val dataDir = this.filesDir val benchmarkResultsFile = File(dataDir, "benchmark_results.json") benchmarkResultsFile.writeText(Json.encodeToString(payload)) } private fun benchmarkDone() { - storeResults() setResult(Activity.RESULT_OK) finish() } diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/BenchmarkUtils.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/BenchmarkUtils.kt index 203457d6a..ed1d7da8c 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/BenchmarkUtils.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/utils/BenchmarkUtils.kt @@ -1,25 +1,63 @@ package org.maplibre.android.testapp.utils +import android.os.Build +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.addJsonObject +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.putJsonArray +import org.maplibre.android.BuildConfig.GIT_REVISION +import org.maplibre.android.testapp.BuildConfig import java.util.ArrayList -class FpsStore { - private val fpsValues = ArrayList(100000) - - fun add(fps: Double) { - fpsValues.add(fps) +data class BenchmarkInputData( + val styleNames: List, + val styleURLs: List, +) { + init { + if (styleNames.size != styleURLs.size) + throw Error("Different size: styleNames=$styleNames, styleURLs=$styleURLs") } +} - fun reset() { - fpsValues.clear() - } +data class BenchmarkRun( + val styleName: String, + val styleURL: String, + val syncRendering: Boolean, +) - fun low1p(): Double { - fpsValues.sort() - return fpsValues.slice(0..(fpsValues.size / 100)).average() - } +data class BenchmarkRunResult( + val fps: Double, + val encodingTimeStore: FrameTimeStore, + val renderingTimeStore: FrameTimeStore +) - fun average(): Double { - return fpsValues.average() +data class BenchmarkResult ( + var runs: ArrayList> +) + +//@SuppressLint("NewApi") +fun jsonPayload(benchmarkResult: BenchmarkResult): JsonObject { + return buildJsonObject { + putJsonArray("results") { + for (run in benchmarkResult.runs) { + addJsonObject { + put("styleName", JsonPrimitive(run.first.styleName)) + put("syncRendering", JsonPrimitive(run.first.syncRendering)) + put("fps", JsonPrimitive(run.second.fps)) + put("avgEncodingTime", JsonPrimitive(run.second.encodingTimeStore.average())) + put("avgRenderingTime", JsonPrimitive(run.second.renderingTimeStore.average())) + put("low1pEncodingTime", JsonPrimitive(run.second.encodingTimeStore.low1p())) + put("low1pRenderingTime", JsonPrimitive(run.second.renderingTimeStore.low1p())) + } + } + } + put("deviceManufacturer", JsonPrimitive(Build.MANUFACTURER)) + put("model", JsonPrimitive(Build.MODEL)) + put("renderer", JsonPrimitive(BuildConfig.FLAVOR)) + put("debugBuild", JsonPrimitive(BuildConfig.DEBUG)) + put("gitRevision", JsonPrimitive(GIT_REVISION)) } } @@ -36,40 +74,10 @@ class FrameTimeStore { fun low1p(): Double { timeValues.sort() - return timeValues.slice((99 * timeValues.size / 100)..timeValues.size - 1).average() + return timeValues.slice((99 * timeValues.size / 100)..> = mutableMapOf>().withDefault { emptyList() }) -{ - - fun addResult(styleName: String, fpsStore: FpsStore) { - val newResults = resultsPerStyle.getValue(styleName).plus( - BenchmarkResult( - fpsStore.average(), - fpsStore.low1p() - ) - ) - resultsPerStyle[styleName] = newResults - } - - fun addResult(styleName: String, frameTimeStore: FrameTimeStore) { - val newResults = resultsPerStyle.getValue(styleName).plus( - BenchmarkResult( - frameTimeStore.average(), - frameTimeStore.low1p() - ) - ) - resultsPerStyle[styleName] = newResults - } -} \ No newline at end of file