blob: ec7ef50bc0ff676db5e9e5f9de5f718797dcada4 [file] [edit]
/*
* Copyright (C) 2024 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 com.android.app.viewcapture
import android.content.Context
import android.internal.perfetto.protos.InternedDataOuterClass.InternedData
import android.internal.perfetto.protos.ProfileCommon.InternedString
import android.internal.perfetto.protos.TracePacketOuterClass.TracePacket
import android.internal.perfetto.protos.Viewcapture.ViewCapture as ViewCaptureMessage
import android.internal.perfetto.protos.WinscopeExtensionsImplOuterClass.WinscopeExtensionsImpl
import android.os.Trace
import android.tracing.perfetto.DataSourceParams
import android.tracing.perfetto.InitArguments
import android.tracing.perfetto.Producer
import android.util.proto.ProtoOutputStream
import androidx.annotation.WorkerThread
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicInteger
/**
* ViewCapture that listens to Perfetto events (OnStart, OnStop, OnFlush) and continuously writes
* captured frames to the Perfetto service (traced).
*/
internal class PerfettoViewCapture
internal constructor(private val context: Context, executor: Executor) :
ViewCapture(RING_BUFFER_SIZE, DEFAULT_INIT_POOL_SIZE, executor) {
private val mDataSource = ViewCaptureDataSource({ onStart() }, {}, { onStop() })
private val mActiveSessions = AtomicInteger(0)
private val mViewIdProvider = ViewIdProvider(context.getResources())
private var mSerializationCurrentId: Int = 0
private var mSerializationCurrentView: ViewPropertyRef? = null
inner class NewInternedStrings {
val packageNames = mutableListOf<String>()
val windowNames = mutableListOf<String>()
val viewIds = mutableListOf<String>()
val classNames = mutableListOf<String>()
}
init {
enableOrDisableWindowListeners(false)
Producer.init(InitArguments.DEFAULTS)
val dataSourceParams =
DataSourceParams.Builder()
.setBufferExhaustedPolicy(
DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT
)
.setNoFlush(true)
.setWillNotifyOnStop(false)
.build()
mDataSource.register(dataSourceParams)
}
fun onStart() {
if (mActiveSessions.incrementAndGet() == 1) {
enableOrDisableWindowListeners(true)
}
}
fun onStop() {
if (mActiveSessions.decrementAndGet() == 0) {
enableOrDisableWindowListeners(false)
}
}
@WorkerThread
override fun onCapturedViewPropertiesBg(
elapsedRealtimeNanos: Long,
windowName: String,
startFlattenedTree: ViewPropertyRef
) {
Trace.beginSection("vc#onCapturedViewPropertiesBg")
mDataSource.trace { ctx ->
val newInternedStrings = NewInternedStrings()
val os = ctx.newTracePacket()
os.write(TracePacket.TIMESTAMP, elapsedRealtimeNanos)
serializeViews(
os,
windowName,
startFlattenedTree,
ctx.incrementalState,
newInternedStrings
)
serializeIncrementalState(os, ctx.incrementalState, newInternedStrings)
}
Trace.endSection()
}
private fun serializeViews(
os: ProtoOutputStream,
windowName: String,
startFlattenedTree: ViewPropertyRef,
incrementalState: ViewCaptureDataSource.IncrementalState,
newInternedStrings: NewInternedStrings
) {
mSerializationCurrentView = startFlattenedTree
mSerializationCurrentId = 0
val tokenExtensions = os.start(TracePacket.WINSCOPE_EXTENSIONS)
val tokenViewCapture = os.start(WinscopeExtensionsImpl.VIEWCAPTURE)
os.write(
ViewCaptureMessage.PACKAGE_NAME_IID,
internPackageName(context.packageName, incrementalState, newInternedStrings)
)
os.write(
ViewCaptureMessage.WINDOW_NAME_IID,
internWindowName(windowName, incrementalState, newInternedStrings)
)
serializeViewsRec(os, -1, incrementalState, newInternedStrings)
os.end(tokenViewCapture)
os.end(tokenExtensions)
}
private fun serializeViewsRec(
os: ProtoOutputStream,
parentId: Int,
incrementalState: ViewCaptureDataSource.IncrementalState,
newInternedStrings: NewInternedStrings
) {
if (mSerializationCurrentView == null) {
return
}
val id = mSerializationCurrentId
val childCount = mSerializationCurrentView!!.childCount
serializeView(
os,
mSerializationCurrentView!!,
mSerializationCurrentId,
parentId,
incrementalState,
newInternedStrings
)
++mSerializationCurrentId
mSerializationCurrentView = mSerializationCurrentView!!.next
for (i in 0..childCount - 1) {
serializeViewsRec(os, id, incrementalState, newInternedStrings)
}
}
private fun serializeView(
os: ProtoOutputStream,
view: ViewPropertyRef,
id: Int,
parentId: Int,
incrementalState: ViewCaptureDataSource.IncrementalState,
newInternedStrings: NewInternedStrings
) {
val token = os.start(ViewCaptureMessage.VIEWS)
os.write(ViewCaptureMessage.View.ID, id)
os.write(ViewCaptureMessage.View.PARENT_ID, parentId)
os.write(ViewCaptureMessage.View.HASHCODE, view.hashCode)
os.write(
ViewCaptureMessage.View.VIEW_ID_IID,
internViewId(mViewIdProvider.getName(view.id), incrementalState, newInternedStrings)
)
os.write(
ViewCaptureMessage.View.CLASS_NAME_IID,
internClassName(view.clazz.name, incrementalState, newInternedStrings)
)
os.write(ViewCaptureMessage.View.LEFT, view.left)
os.write(ViewCaptureMessage.View.TOP, view.top)
os.write(ViewCaptureMessage.View.WIDTH, view.right - view.left)
os.write(ViewCaptureMessage.View.HEIGHT, view.bottom - view.top)
os.write(ViewCaptureMessage.View.SCROLL_X, view.scrollX)
os.write(ViewCaptureMessage.View.SCROLL_Y, view.scrollY)
os.write(ViewCaptureMessage.View.TRANSLATION_X, view.translateX)
os.write(ViewCaptureMessage.View.TRANSLATION_Y, view.translateY)
os.write(ViewCaptureMessage.View.SCALE_X, view.scaleX)
os.write(ViewCaptureMessage.View.SCALE_Y, view.scaleY)
os.write(ViewCaptureMessage.View.ALPHA, view.alpha)
os.write(ViewCaptureMessage.View.WILL_NOT_DRAW, view.willNotDraw)
os.write(ViewCaptureMessage.View.CLIP_CHILDREN, view.clipChildren)
os.write(ViewCaptureMessage.View.VISIBILITY, view.visibility)
os.write(ViewCaptureMessage.View.ELEVATION, view.elevation)
os.end(token)
}
private fun internClassName(
string: String,
incrementalState: ViewCaptureDataSource.IncrementalState,
newInternedStrings: NewInternedStrings
): Int {
return internString(
string,
incrementalState.mInternMapClassName,
newInternedStrings.classNames
)
}
private fun internPackageName(
string: String,
incrementalState: ViewCaptureDataSource.IncrementalState,
newInternedStrings: NewInternedStrings
): Int {
return internString(
string,
incrementalState.mInternMapPackageName,
newInternedStrings.packageNames
)
}
private fun internViewId(
string: String,
incrementalState: ViewCaptureDataSource.IncrementalState,
newInternedStrings: NewInternedStrings
): Int {
return internString(string, incrementalState.mInternMapViewId, newInternedStrings.viewIds)
}
private fun internWindowName(
string: String,
incrementalState: ViewCaptureDataSource.IncrementalState,
newInternedStrings: NewInternedStrings
): Int {
return internString(
string,
incrementalState.mInternMapWindowName,
newInternedStrings.windowNames
)
}
private fun internString(
string: String,
internMap: MutableMap<String, Int>,
newInternedStrings: MutableList<String>
): Int {
if (internMap.containsKey(string)) {
return internMap[string]!!
}
// +1 to avoid intern ID = 0, because javastream optimizes out zero values
// and the perfetto trace processor would not de-intern that string.
val internId = internMap.size + 1
internMap.put(string, internId)
newInternedStrings.add(string)
return internId
}
private fun serializeIncrementalState(
os: ProtoOutputStream,
incrementalState: ViewCaptureDataSource.IncrementalState,
newInternedStrings: NewInternedStrings
) {
var flags = TracePacket.SEQ_NEEDS_INCREMENTAL_STATE
if (!incrementalState.mHasNotifiedClearedState) {
flags = flags or TracePacket.SEQ_INCREMENTAL_STATE_CLEARED
incrementalState.mHasNotifiedClearedState = true
}
os.write(TracePacket.SEQUENCE_FLAGS, flags)
val token = os.start(TracePacket.INTERNED_DATA)
serializeInternMap(
os,
InternedData.VIEWCAPTURE_CLASS_NAME,
incrementalState.mInternMapClassName,
newInternedStrings.classNames
)
serializeInternMap(
os,
InternedData.VIEWCAPTURE_PACKAGE_NAME,
incrementalState.mInternMapPackageName,
newInternedStrings.packageNames
)
serializeInternMap(
os,
InternedData.VIEWCAPTURE_VIEW_ID,
incrementalState.mInternMapViewId,
newInternedStrings.viewIds
)
serializeInternMap(
os,
InternedData.VIEWCAPTURE_WINDOW_NAME,
incrementalState.mInternMapWindowName,
newInternedStrings.windowNames
)
os.end(token)
}
private fun serializeInternMap(
os: ProtoOutputStream,
fieldId: Long,
map: Map<String, Int>,
newInternedStrings: List<String>
) {
if (newInternedStrings.isEmpty()) {
return
}
var currentInternId = map.size - newInternedStrings.size + 1
for (internedString in newInternedStrings) {
val token = os.start(fieldId)
os.write(InternedString.IID, currentInternId++)
os.write(InternedString.STR, internedString.toByteArray())
os.end(token)
}
}
companion object {
// Keep two frames in the base class' ring buffer.
// This is the minimum required by the current implementation to work.
private val RING_BUFFER_SIZE = 2
}
}