blob: 1e51428164426d0ff70208448d7ce7c70cdf4810 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.tools.common.traces.surfaceflinger
import android.tools.common.Timestamp
import kotlin.js.JsExport
import kotlin.js.JsName
/** Builder for LayerTraceEntries */
@JsExport
class LayerTraceEntryBuilder {
private var elapsedTimestamp: Long = 0L
private var realTimestamp: Long? = null
private var orphanLayerCallback: ((Layer) -> Boolean)? = null
private val orphans = mutableListOf<Layer>()
private var layers: MutableMap<Int, Layer> = mutableMapOf()
private var ignoreVirtualDisplay = false
private var ignoreLayersStackMatchNoDisplay = false
private var timestamp: Timestamp? = null
private var displays: Array<Display> = emptyArray()
private var vSyncId: Long = 0L
private var hwcBlob: String = ""
private var where: String = ""
private var duplicateLayerCallback: ((Layer) -> Boolean) = {
error("Duplicate layer id found: ${it.id}")
}
@JsName("setVSyncId")
fun setVSyncId(vSyncId: String): LayerTraceEntryBuilder =
// Necessary for compatibility with JS number type
apply {
this.vSyncId = vSyncId.toLong()
}
@JsName("setHwcBlob")
fun setHwcBlob(hwcBlob: String): LayerTraceEntryBuilder = apply { this.hwcBlob = hwcBlob }
@JsName("setWhere")
fun setWhere(where: String): LayerTraceEntryBuilder = apply { this.where = where }
@JsName("setDisplays")
fun setDisplays(displays: Array<Display>): LayerTraceEntryBuilder = apply {
this.displays = displays
}
@JsName("setElapsedTimestamp")
fun setElapsedTimestamp(timestamp: String): LayerTraceEntryBuilder =
// Necessary for compatibility with JS number type
apply {
this.elapsedTimestamp = timestamp.toLong()
}
@JsName("setRealToElapsedTimeOffsetNs")
fun setRealToElapsedTimeOffsetNs(realToElapsedTimeOffsetNs: String?): LayerTraceEntryBuilder =
apply {
this.realTimestamp =
if (realToElapsedTimeOffsetNs != null && realToElapsedTimeOffsetNs.toLong() != 0L) {
realToElapsedTimeOffsetNs.toLong() + elapsedTimestamp
} else {
null
}
}
@JsName("setLayers")
fun setLayers(layers: Array<Layer>): LayerTraceEntryBuilder = apply {
val result = mutableMapOf<Int, Layer>()
layers.forEach { layer ->
val id = layer.id
if (result.containsKey(id)) {
duplicateLayerCallback(layer)
}
result[id] = layer
}
this.layers = result
}
@JsName("setOrphanLayerCallback")
fun setOrphanLayerCallback(value: ((Layer) -> Boolean)?): LayerTraceEntryBuilder = apply {
this.orphanLayerCallback = value
}
@JsName("setDuplicateLayerCallback")
fun setDuplicateLayerCallback(value: ((Layer) -> Boolean)): LayerTraceEntryBuilder = apply {
this.duplicateLayerCallback = value
}
private fun notifyOrphansLayers() {
val callback = this.orphanLayerCallback ?: return
// Fail if we find orphan layers.
orphans.forEach { orphan ->
// Workaround for b/141326137, ignore the existence of an orphan layer
if (callback.invoke(orphan)) {
return@forEach
}
throw RuntimeException(
("Failed to parse layers trace. Found orphan layer with id = ${orphan.id}" +
" with parentId = ${orphan.parentId}")
)
}
}
/**
* Update the parent layers or each trace
*
* @return root layer
*/
private fun updateParents() {
for (layer in layers.values) {
val parentId = layer.parentId
val parentLayer = layers[parentId]
if (parentLayer == null) {
orphans.add(layer)
continue
}
parentLayer.addChild(layer)
layer.parent = parentLayer
}
}
/**
* Update the parent layers or each trace
*
* @return root layer
*/
private fun updateRelZParents() {
for (layer in layers.values) {
val parentId = layer.zOrderRelativeOfId
val parentLayer = layers[parentId]
if (parentLayer == null) {
layer.zOrderRelativeParentOf = parentId
continue
}
layer.zOrderRelativeOf = parentLayer
}
}
private fun computeRootLayers(): List<Layer> {
updateParents()
updateRelZParents()
// Find all root layers (any sibling of the root layer is considered a root layer in the
// trace)
val rootLayers = mutableListOf<Layer>()
// Getting the first orphan works because when dumping the layers, the root layer comes
// first, and given that orphans are added in the same order as the layers are provided
// in the first orphan layer should be the root layer.
if (orphans.isNotEmpty()) {
val firstRoot = orphans.first()
orphans.remove(firstRoot)
rootLayers.add(firstRoot)
val remainingRoots = orphans.filter { it.parentId == firstRoot.parentId }
rootLayers.addAll(remainingRoots)
// Remove RootLayers from orphans
orphans.removeAll(rootLayers)
}
return rootLayers
}
private fun filterOutLayersInVirtualDisplays(roots: List<Layer>): List<Layer> {
val physicalDisplays = displays.filterNot { it.isVirtual }.map { it.layerStackId }
return roots.filter { physicalDisplays.contains(it.stackId) }
}
private fun filterOutVirtualDisplays(displays: List<Display>): List<Display> {
return displays.filterNot { it.isVirtual }
}
private fun filterOutOffDisplays(displays: List<Display>): List<Display> {
return displays.filterNot { it.isOff }
}
private fun filterOutLayersStackMatchNoDisplay(roots: List<Layer>): List<Layer> {
val displayStacks = displays.map { it.layerStackId }
return roots.filter { displayStacks.contains(it.stackId) }
}
/**
* Defines if virtual displays and the layers belonging to virtual displays (e.g., Screen
* Recording) should be ignored while parsing the entry
*
* @param ignore If the layers from virtual displays should be ignored or not
*/
@JsName("ignoreVirtualDisplay")
fun ignoreVirtualDisplay(ignore: Boolean): LayerTraceEntryBuilder = apply {
this.ignoreVirtualDisplay = ignore
}
/**
* Ignore layers whose stack ID doesn't match any display. This is the case, for example, when
* the device screen is off, or for layers that have not yet been removed after a display change
* (e.g., virtual screen recording display removed)
*
* @param ignore If the layers not matching any stack id should be removed or not
*/
@JsName("ignoreLayersStackMatchNoDisplay")
fun ignoreLayersStackMatchNoDisplay(ignore: Boolean): LayerTraceEntryBuilder = apply {
this.ignoreLayersStackMatchNoDisplay = ignore
}
/** Constructs the layer hierarchy from a flattened list of layers. */
@JsName("build")
fun build(): LayerTraceEntry {
val allRoots = computeRootLayers()
var filteredRoots = allRoots
var filteredDisplays = displays.toList()
if (ignoreLayersStackMatchNoDisplay) {
filteredRoots = filterOutLayersStackMatchNoDisplay(filteredRoots)
}
if (ignoreVirtualDisplay) {
filteredRoots = filterOutLayersInVirtualDisplays(filteredRoots)
filteredDisplays = filterOutVirtualDisplays(filteredDisplays)
}
filteredDisplays = filterOutOffDisplays(filteredDisplays)
// Fail if we find orphan layers.
notifyOrphansLayers()
return LayerTraceEntry(
elapsedTimestamp,
realTimestamp,
hwcBlob,
where,
filteredDisplays.toTypedArray(),
vSyncId,
filteredRoots.toTypedArray(),
)
}
}