1 /*
<lambda>null2  * Copyright 2021 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 package androidx.benchmark.macro.perfetto
18 
19 import android.util.Log
20 import androidx.benchmark.macro.StartupMode
21 import androidx.benchmark.traceprocessor.Slice
22 import androidx.benchmark.traceprocessor.TraceProcessor
23 import androidx.benchmark.traceprocessor.processNameLikePkg
24 import androidx.benchmark.traceprocessor.toSlices
25 
26 internal object StartupTimingQuery {
27     private fun getFullQuery(targetPackageName: String) =
28         """
29         ------ Select all startup-relevant slices from slice table
30         SELECT
31             slice.name as name,
32             slice.ts as ts,
33             slice.dur as dur
34         FROM slice
35             INNER JOIN thread_track on slice.track_id = thread_track.id
36             INNER JOIN thread USING(utid)
37             INNER JOIN process USING(upid)
38         WHERE (
39                 ${processNameLikePkg(targetPackageName)} AND (
40                     (slice.name LIKE "activityResume" AND process.pid LIKE thread.tid) OR
41                     (slice.name LIKE "Choreographer#doFrame%" AND process.pid LIKE thread.tid) OR
42                     (slice.name LIKE "reportFullyDrawn() for %" AND process.pid LIKE thread.tid) OR
43                     (slice.name LIKE "DrawFrame%" AND thread.name LIKE "RenderThread")
44             ) OR
45             (
46                 -- Signals beginning of launch event, only present in API 29+
47                 process.name LIKE "system_server" AND
48                 slice.name LIKE "MetricsLogger:launchObserverNotifyIntentStarted"
49             )
50         )
51         ------ Add in async slices
52         UNION
53         SELECT
54             slice.name as name,
55             slice.ts as ts,
56             slice.dur as dur
57         FROM slice
58             INNER JOIN process_track on slice.track_id = process_track.id
59             INNER JOIN process USING(upid)
60         WHERE (
61             -- API 23+:   "launching: <target>"
62             -- API 19-22: "launching"
63             slice.name LIKE "launching%" AND process.name LIKE "system_server"
64         )
65         ORDER BY ts ASC
66     """
67             .trimIndent()
68 
69     enum class StartupSliceType {
70         NotifyStarted,
71         Launching,
72         ReportFullyDrawn,
73         FrameUiThread,
74         FrameRenderThread,
75         ActivityResume
76     }
77 
78     data class SubMetrics(
79         val timeToInitialDisplayNs: Long,
80         val timeToFullDisplayNs: Long?,
81         val timelineRangeNs: LongRange
82     ) {
83         constructor(
84             startTs: Long,
85             initialDisplayTs: Long,
86             fullDisplayTs: Long?
87         ) : this(
88             timeToInitialDisplayNs = initialDisplayTs - startTs,
89             timeToFullDisplayNs = fullDisplayTs?.let { it - startTs },
90             timelineRangeNs = startTs..(fullDisplayTs ?: initialDisplayTs),
91         )
92     }
93 
94     private fun findEndRenderTimeForUiFrame(
95         uiSlices: List<Slice>,
96         rtSlices: List<Slice>,
97         predicateErrorLabel: String,
98         predicate: (Slice) -> Boolean
99     ): Long {
100         // find first UI slice that corresponds with the predicate
101         val uiSlice = uiSlices.firstOrNull(predicate)
102 
103         check(uiSlice != null) { "No Choreographer#doFrame $predicateErrorLabel" }
104 
105         // find corresponding rt slice
106         val rtSlice = rtSlices.firstOrNull { rtSlice -> rtSlice.ts > uiSlice.ts }
107 
108         check(rtSlice != null) {
109             "No RT frame slice associated with UI thread frame slice $predicateErrorLabel"
110         }
111 
112         return rtSlice.endTs
113     }
114 
115     fun getFrameSubMetrics(
116         session: TraceProcessor.Session,
117         captureApiLevel: Int,
118         targetPackageName: String,
119         startupMode: StartupMode
120     ): SubMetrics? {
121         val queryResultIterator =
122             session.query(query = getFullQuery(targetPackageName = targetPackageName))
123         val slices = queryResultIterator.toSlices()
124 
125         val groupedData =
126             slices
127                 .filter { it.dur > 0 } // drop non-terminated slices
128                 .groupBy {
129                     when {
130                         // note: we use "startsWith" as many of these have more details
131                         // appended to the slice name in more recent platform versions
132                         it.name.startsWith("Choreographer#doFrame") ->
133                             StartupSliceType.FrameUiThread
134                         it.name.startsWith("DrawFrame") -> StartupSliceType.FrameRenderThread
135                         it.name.startsWith("launching") -> StartupSliceType.Launching
136                         it.name.startsWith("reportFullyDrawn") -> StartupSliceType.ReportFullyDrawn
137                         it.name == "MetricsLogger:launchObserverNotifyIntentStarted" ->
138                             StartupSliceType.NotifyStarted
139                         it.name == "activityResume" -> StartupSliceType.ActivityResume
140                         else -> throw IllegalStateException("Unexpected slice $it")
141                     }
142                 }
143 
144         val uiSlices = groupedData.getOrElse(StartupSliceType.FrameUiThread) { listOf() }
145         val rtSlices = groupedData.getOrElse(StartupSliceType.FrameRenderThread) { listOf() }
146 
147         if (uiSlices.isEmpty() || rtSlices.isEmpty()) {
148             Log.w("Benchmark", "No UI / RT slices seen, not reporting startup.")
149             return null
150         }
151 
152         val startTs: Long
153         val initialDisplayTs: Long
154         if (captureApiLevel >= 29 || startupMode != StartupMode.HOT) {
155             // find first matching "launching" slice
156             val launchingSlice =
157                 groupedData[StartupSliceType.Launching]?.firstOrNull {
158                     // verify full name only on API 23+, since before package name not specified
159                     (captureApiLevel < 23 || it.name == "launching: $targetPackageName")
160                 }
161                     ?: run {
162                         Log.w("Benchmark", "No launching slice seen, not reporting startup.")
163                         return null
164                     }
165 
166             startTs =
167                 if (captureApiLevel >= 29) {
168                     // Starting on API 29, expect to see 'notify started' system_server slice
169                     val notifyStartedSlice =
170                         groupedData[StartupSliceType.NotifyStarted]?.lastOrNull {
171                             it.ts < launchingSlice.ts
172                         }
173                             ?: run {
174                                 Log.w(
175                                     "Benchmark",
176                                     "No launchObserverNotifyIntentStarted slice seen before launching: " +
177                                         "slice, not reporting startup."
178                                 )
179                                 return null
180                             }
181                     notifyStartedSlice.ts
182                 } else {
183                     launchingSlice.ts
184                 }
185 
186             // We use the end of rt slice here instead of the end of the 'launching' slice. This is
187             // both because on some platforms the launching slice may not wait for renderthread, but
188             // also because this allows us to make the guarantee that timeToInitialDisplay ==
189             // timeToFirstDisplay when they are the same frame.
190             initialDisplayTs =
191                 findEndRenderTimeForUiFrame(
192                     uiSlices = uiSlices,
193                     rtSlices = rtSlices,
194                     predicateErrorLabel = "after launching slice"
195                 ) { uiSlice ->
196                     uiSlice.ts > launchingSlice.ts
197                 }
198         } else {
199             // Prior to API 29, hot starts weren't traced with the launching slice, so we do a best
200             // guess - the time taken to Activity#onResume, and then produce the next frame.
201             startTs =
202                 groupedData[StartupSliceType.ActivityResume]?.first()?.ts
203                     ?: run {
204                         Log.w("Benchmark", "No activityResume slice, not reporting startup.")
205                         return null
206                     }
207             initialDisplayTs =
208                 findEndRenderTimeForUiFrame(
209                     uiSlices = uiSlices,
210                     rtSlices = rtSlices,
211                     predicateErrorLabel = "after activityResume"
212                 ) { uiSlice ->
213                     uiSlice.ts > startTs
214                 }
215         }
216 
217         val reportFullyDrawnSlice = groupedData[StartupSliceType.ReportFullyDrawn]?.firstOrNull()
218 
219         val reportFullyDrawnEndTs: Long? =
220             reportFullyDrawnSlice?.let {
221                 // find first uiSlice with end after reportFullyDrawn (reportFullyDrawn may happen
222                 // during or before a given frame)
223                 findEndRenderTimeForUiFrame(
224                     uiSlices = uiSlices,
225                     rtSlices = rtSlices,
226                     predicateErrorLabel = "ends after reportFullyDrawn"
227                 ) { uiSlice ->
228                     uiSlice.endTs > reportFullyDrawnSlice.ts
229                 }
230             }
231 
232         return SubMetrics(
233             startTs = startTs,
234             initialDisplayTs = initialDisplayTs,
235             fullDisplayTs = reportFullyDrawnEndTs,
236         )
237     }
238 }
239