FPS added to Android benchmark (#2944)

This commit is contained in:
Alex Cristici
2024-10-21 20:06:49 +03:00
committed by GitHub
parent 8b5310285d
commit 600abc68c9
2 changed files with 154 additions and 175 deletions

View File

@@ -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<String>,
val styleURLs: List<String>,
) {
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<String>, 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<View>(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()
}

View File

@@ -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<Double>(100000)
fun add(fps: Double) {
fpsValues.add(fps)
data class BenchmarkInputData(
val styleNames: List<String>,
val styleURLs: List<String>,
) {
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<Pair<BenchmarkRun, BenchmarkRunResult>>
)
//@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)..<timeValues.size).average()
}
fun average(): Double {
return timeValues.average()
}
}
/**
* Result of single benchmark run
*/
data class BenchmarkResult(val average: Double, val low1p: Double)
data class BenchmarkResults(
var resultsPerStyle: MutableMap<String, List<BenchmarkResult>> = mutableMapOf<String, List<BenchmarkResult>>().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
}
}