1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 @file:JvmName("Utils")
18
19 package android.tools.traces
20
21 import android.app.UiAutomation
22 import android.os.IBinder
23 import android.os.ParcelFileDescriptor
24 import android.os.Process
25 import android.os.SystemClock
26 import android.tools.MILLISECOND_AS_NANOSECONDS
27 import android.tools.io.TraceType
28 import android.tools.traces.io.ResultReader
29 import android.tools.traces.monitors.PerfettoTraceMonitor
30 import android.tools.traces.parsers.DeviceDumpParser
31 import android.tools.traces.surfaceflinger.LayerTraceEntry
32 import android.tools.traces.wm.WindowManagerState
33 import android.util.Log
34 import androidx.test.platform.app.InstrumentationRegistry
35 import com.google.protobuf.InvalidProtocolBufferException
36 import java.text.SimpleDateFormat
37 import java.util.Date
38 import java.util.Locale
39 import java.util.Optional
40 import java.util.TimeZone
41 import perfetto.protos.PerfettoConfig.TracingServiceState
42
43 fun formatRealTimestamp(timestampNs: Long): String {
44 val timestampMs = timestampNs / MILLISECOND_AS_NANOSECONDS
45 val remainderNs = timestampNs % MILLISECOND_AS_NANOSECONDS
46 val date = Date(timestampMs)
47
48 val timeFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.ENGLISH)
49 timeFormatter.timeZone = TimeZone.getTimeZone("UTC")
50
51 return "${timeFormatter.format(date)}${remainderNs.toString().padStart(6, '0')}"
52 }
53
executeShellCommandnull54 fun executeShellCommand(cmd: String): ByteArray {
55 Log.d(LOG_TAG, "Executing shell command $cmd")
56 val uiAutomation: UiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
57 val fileDescriptor = uiAutomation.executeShellCommand(cmd)
58 ParcelFileDescriptor.AutoCloseInputStream(fileDescriptor).use { inputStream ->
59 return inputStream.readBytes()
60 }
61 }
62
executeShellCommandnull63 fun executeShellCommand(cmd: String, stdin: ByteArray): ByteArray {
64 Log.d(LOG_TAG, "Executing shell command $cmd")
65 val uiAutomation: UiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
66 val fileDescriptors = uiAutomation.executeShellCommandRw(cmd)
67 val stdoutFileDescriptor = fileDescriptors[0]
68 val stdinFileDescriptor = fileDescriptors[1]
69
70 ParcelFileDescriptor.AutoCloseOutputStream(stdinFileDescriptor).use { it.write(stdin) }
71
72 ParcelFileDescriptor.AutoCloseInputStream(stdoutFileDescriptor).use {
73 return it.readBytes()
74 }
75 }
76
doBinderDumpnull77 private fun doBinderDump(name: String): ByteArray {
78 // create a fd for the binder transaction
79 val pipe = ParcelFileDescriptor.createPipe()
80 val source = pipe[0]
81 val sink = pipe[1]
82
83 // ServiceManager isn't accessible from tests, so use reflection
84 // this should return an IBinder
85 val service =
86 Class.forName("android.os.ServiceManager")
87 .getMethod("getServiceOrThrow", String::class.java)
88 .invoke(null, name) as IBinder?
89
90 // this is equal to ServiceManager::PROTO_ARG
91 val args = arrayOf("--proto")
92 service?.dump(sink.fileDescriptor, args)
93 sink.close()
94
95 // convert the FD into a ByteArray
96 ParcelFileDescriptor.AutoCloseInputStream(source).use { inputStream ->
97 return inputStream.readBytes()
98 }
99 }
100
getCurrentWindowManagerStatenull101 private fun getCurrentWindowManagerState() = doBinderDump("window")
102
103 /**
104 * Gets the current device state dump containing the [WindowManagerState] (optional) and the
105 * [LayerTraceEntry] (optional) in raw (byte) data.
106 *
107 * @param dumpTypes Flags determining which types of traces should be included in the dump
108 */
109 fun getCurrentState(
110 vararg dumpTypes: TraceType = arrayOf(TraceType.SF_DUMP, TraceType.WM_DUMP)
111 ): Pair<ByteArray, ByteArray> {
112 if (dumpTypes.isEmpty()) {
113 throw IllegalArgumentException("No dump specified")
114 }
115
116 val traceTypes = dumpTypes.filter { it.isTrace }
117 if (traceTypes.isNotEmpty()) {
118 throw IllegalArgumentException("Only dump types are supported. Invalid types: $traceTypes")
119 }
120
121 val requestedWmDump = dumpTypes.contains(TraceType.WM_DUMP)
122 val requestedSfDump = dumpTypes.contains(TraceType.SF_DUMP)
123
124 Log.d(LOG_TAG, "Requesting new device state dump")
125
126 val reader =
127 PerfettoTraceMonitor.newBuilder()
128 .also {
129 if (requestedWmDump && android.tracing.Flags.perfettoWmDump()) {
130 it.enableWindowManagerDump()
131 }
132
133 if (requestedSfDump) {
134 it.enableLayersDump()
135 }
136 }
137 .build()
138 .withTracing(resultReaderProvider = { ResultReader(it, SERVICE_TRACE_CONFIG) }) {}
139 val perfettoTrace = reader.readBytes(TraceType.PERFETTO) ?: ByteArray(0)
140
141 reader.artifact.deleteIfExists()
142
143 val wmDump =
144 if (android.tracing.Flags.perfettoWmDump()) {
145 if (requestedWmDump) perfettoTrace else ByteArray(0)
146 } else {
147 if (requestedWmDump) {
148 Log.d(LOG_TAG, "Requesting new legacy WM state dump")
149 getCurrentWindowManagerState()
150 } else {
151 ByteArray(0)
152 }
153 }
154
155 val sfDump = if (requestedSfDump) perfettoTrace else ByteArray(0)
156
157 return Pair(wmDump, sfDump)
158 }
159
160 /**
161 * Gets the current device state dump containing the [WindowManagerState] (optional) and the
162 * [LayerTraceEntry] (optional) parsed
163 *
164 * @param dumpTypes Flags determining which types of traces should be included in the dump
165 * @param clearCacheAfterParsing If the caching used while parsing the proto should be
166 *
167 * ```
168 * cleared or remain in memory
169 * ```
170 */
171 @JvmOverloads
getCurrentStateDumpNullablenull172 fun getCurrentStateDumpNullable(
173 vararg dumpTypes: TraceType = arrayOf(TraceType.SF_DUMP, TraceType.WM_DUMP),
174 clearCacheAfterParsing: Boolean = true,
175 ): NullableDeviceStateDump {
176 val currentStateDump = getCurrentState(*dumpTypes)
177 return DeviceDumpParser.fromNullableDump(
178 currentStateDump.first,
179 currentStateDump.second,
180 clearCacheAfterParsing = clearCacheAfterParsing,
181 )
182 }
183
184 @JvmOverloads
getCurrentStateDumpnull185 fun getCurrentStateDump(
186 vararg dumpTypes: TraceType = arrayOf(TraceType.SF_DUMP, TraceType.WM_DUMP),
187 clearCacheAfterParsing: Boolean = true,
188 ): DeviceStateDump {
189 val currentStateDump = getCurrentState(*dumpTypes)
190 return DeviceDumpParser.fromDump(
191 currentStateDump.first,
192 currentStateDump.second,
193 clearCacheAfterParsing = clearCacheAfterParsing,
194 )
195 }
196
197 @JvmOverloads
busyWaitForDataSourceRegistrationnull198 fun busyWaitForDataSourceRegistration(
199 dataSourceName: String,
200 busyWaitIntervalMs: Long = 100,
201 timeoutMs: Long = 10000,
202 ) {
203 busyWait(
204 busyWaitIntervalMs,
205 timeoutMs,
206 { isDataSourceAvailable(dataSourceName) },
207 { "Data source disn't become available" },
208 )
209 }
210
211 @JvmOverloads
busyWaitTracingSessionExistsnull212 fun busyWaitTracingSessionExists(
213 uniqueSessionName: String,
214 busyWaitIntervalMs: Long = 100,
215 timeoutMs: Long = 10000,
216 ) {
217 busyWait(
218 busyWaitIntervalMs,
219 timeoutMs,
220 { sessionExists(uniqueSessionName) },
221 { "Tracing session doesn't exist" },
222 )
223 }
224
225 @JvmOverloads
busyWaitTracingSessionDoesntExistnull226 fun busyWaitTracingSessionDoesntExist(
227 uniqueSessionName: String,
228 busyWaitIntervalMs: Long = 100,
229 timeoutMs: Long = 10000,
230 ) {
231 busyWait(
232 busyWaitIntervalMs,
233 timeoutMs,
234 { !sessionExists(uniqueSessionName) },
235 { "Tracing session still exists" },
236 )
237 }
238
isDataSourceAvailablenull239 private fun isDataSourceAvailable(dataSourceName: String): Boolean {
240 val proto = executeShellCommand("perfetto --query-raw")
241
242 try {
243 val state = TracingServiceState.parseFrom(proto)
244
245 var producerId = Optional.empty<Int>()
246
247 for (producer in state.producersList) {
248 if (producer.pid == Process.myPid()) {
249 producerId = Optional.of(producer.id)
250 break
251 }
252 }
253
254 if (!producerId.isPresent) {
255 return false
256 }
257
258 for (ds in state.dataSourcesList) {
259 if (ds.dsDescriptor.name.equals(dataSourceName) && ds.producerId == producerId.get()) {
260 return true
261 }
262 }
263 } catch (e: InvalidProtocolBufferException) {
264 throw RuntimeException(e)
265 }
266
267 return false
268 }
269
sessionExistsnull270 private fun sessionExists(uniqueSessionName: String): Boolean {
271 val proto = executeShellCommand("perfetto --query-raw")
272
273 try {
274 val state = TracingServiceState.parseFrom(proto)
275
276 for (session in state.tracingSessionsList) {
277 if (session.uniqueSessionName.equals(uniqueSessionName)) {
278 return true
279 }
280 }
281 } catch (e: InvalidProtocolBufferException) {
282 throw RuntimeException(e)
283 }
284
285 return false
286 }
287
busyWaitnull288 private fun busyWait(
289 busyWaitIntervalMs: Long,
290 timeoutMs: Long,
291 predicate: () -> Boolean,
292 errorMessage: () -> String,
293 ) {
294 var elapsedMs = 0L
295
296 while (!predicate()) {
297 SystemClock.sleep(busyWaitIntervalMs)
298 elapsedMs += busyWaitIntervalMs
299 if (elapsedMs >= timeoutMs) {
300 throw java.lang.RuntimeException(errorMessage())
301 }
302 }
303 }
304