FPS added to Android benchmark (#2944)
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user