/* * 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() val windowNames = mutableListOf() val viewIds = mutableListOf() val classNames = mutableListOf() } 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, newInternedStrings: MutableList ): 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, newInternedStrings: List ) { 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 } }