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 androidx.benchmark.traceprocessor.Slice
20 import androidx.benchmark.traceprocessor.TraceProcessor
21 import androidx.benchmark.traceprocessor.processNameLikePkg
22 import androidx.benchmark.traceprocessor.toSlices
23 
24 internal object FrameTimingQuery {
25     private fun getFullQuery(packageName: String) =
26         """
27         ------ Select all frame-relevant slices from slice table
28         SELECT
29             slice.name as name,
30             slice.ts as ts,
31             slice.dur as dur
32         FROM slice
33             INNER JOIN thread_track on slice.track_id = thread_track.id
34             INNER JOIN thread USING(utid)
35             INNER JOIN process USING(upid)
36         WHERE (
37             ( slice.name LIKE 'Choreographer#doFrame%'
38                 AND slice.name NOT LIKE 'Choreographer#doFrame - resynced to%'
39                 AND process.pid LIKE thread.tid
40             ) OR
41             ( slice.name LIKE 'DrawFrame%' AND thread.name = 'RenderThread' )
42         ) AND ${processNameLikePkg(packageName)}
43         ------ Add in actual frame slices (prepended with 'actual ' to differentiate)
44         UNION
45         SELECT
46             'actual ' || actual_frame_timeline_slice.name as name,
47             actual_frame_timeline_slice.ts as ts,
48             actual_frame_timeline_slice.dur as dur
49         FROM actual_frame_timeline_slice
50             INNER JOIN process USING(upid)
51         WHERE
52             ${processNameLikePkg(packageName)}
53         ------ Add in expected time slices (prepended with 'expected ' to differentiate)
54         UNION
55         SELECT
56             'expected ' || expected_frame_timeline_slice.name as name,
57             expected_frame_timeline_slice.ts as ts,
58             expected_frame_timeline_slice.dur as dur
59         FROM expected_frame_timeline_slice
60             INNER JOIN process USING(upid)
61         WHERE
62             ${processNameLikePkg(packageName)}
63         ORDER BY ts ASC
64     """
65             .trimIndent()
66 
67     enum class SubMetric {
68         // Duration of UI thread
69         FrameDurationCpuNs,
70         // Total duration from UI through RT slice
71         FrameDurationUiNs,
72         // How much longer did frame take than expected
73         FrameOverrunNs,
74         // Total duration from expected frame start through true end of frame
75         FrameDurationFullNs;
76 
77         fun supportedOnApiLevel(apiLevel: Int): Boolean {
78             return apiLevel >= 31 || this != FrameOverrunNs && this != FrameDurationFullNs
79         }
80     }
81 
82     enum class FrameSliceType {
83         Expected,
84         Actual,
85         UiThread,
86         RenderThread
87     }
88 
89     /**
90      * Container for frame data.
91      *
92      * Nullable slices are always present on API 31+
93      */
94     internal class FrameData(
95         val uiSlice: Slice,
96         val rtSlice: Slice,
97         val expectedSlice: Slice?,
98         val actualSlice: Slice?
99     ) {
100         fun get(subMetric: SubMetric): Long {
101             return when (subMetric) {
102                 SubMetric.FrameDurationCpuNs -> rtSlice.endTs - uiSlice.ts
103                 SubMetric.FrameDurationUiNs -> uiSlice.dur
104                 SubMetric.FrameOverrunNs -> {
105                     // workaround b/279088460, where actual slice ends too early
106                     maxOf(actualSlice!!.endTs, rtSlice.endTs) - expectedSlice!!.endTs
107                 }
108                 SubMetric.FrameDurationFullNs -> {
109                     // workaround b/279088460, where actual slice ends too early
110                     maxOf(actualSlice!!.endTs, rtSlice.endTs) - expectedSlice!!.ts
111                 }
112             }
113         }
114 
115         companion object {
116             fun tryCreateBasic(uiSlice: Slice?, rtSlice: Slice?): FrameData? {
117                 return uiSlice?.let { rtSlice?.let { FrameData(uiSlice, rtSlice, null, null) } }
118             }
119 
120             fun tryCreate31(
121                 uiSlice: Slice?,
122                 rtSlice: Slice?,
123                 expectedSlice: Slice?,
124                 actualSlice: Slice?,
125             ): FrameData? {
126                 return if (
127                     uiSlice != null &&
128                         rtSlice != null &&
129                         expectedSlice != null &&
130                         actualSlice != null
131                 ) {
132                     FrameData(uiSlice, rtSlice, expectedSlice, actualSlice)
133                 } else {
134                     null
135                 }
136             }
137         }
138     }
139 
140     /** Binary search for a slice matching the specified frameId, or null if not found. */
141     private fun List<Slice>.binarySearchFrameId(frameId: Int): Slice? {
142         val targetIndex = binarySearch { potentialTarget -> potentialTarget.frameId!! - frameId }
143         return if (targetIndex >= 0) {
144             get(targetIndex)
145         } else {
146             null
147         }
148     }
149 
150     internal fun getFrameData(
151         session: TraceProcessor.Session,
152         captureApiLevel: Int,
153         packageName: String,
154     ): List<FrameData> {
155         val queryResultIterator = session.query(query = getFullQuery(packageName))
156         val slices =
157             queryResultIterator.toSlices().let { list ->
158                 list.map { it.copy(ts = it.ts - list.first().ts) }
159             }
160 
161         val groupedData =
162             slices
163                 .filter { it.dur > 0 } // drop non-terminated slices
164                 .filter { !it.name.contains("resynced") } // drop "#doFrame - resynced to" slices
165                 .groupBy {
166                     when {
167                         // note: we use "startsWith" as starting in S, all of these will end
168                         // with frame ID (or GPU completion frame ID)
169                         it.name.startsWith("Choreographer#doFrame") -> FrameSliceType.UiThread
170                         it.name.startsWith("DrawFrame") -> FrameSliceType.RenderThread
171                         it.name.startsWith("actual ") -> FrameSliceType.Actual
172                         it.name.startsWith("expected ") -> FrameSliceType.Expected
173                         else -> throw IllegalStateException("Unexpected slice $it")
174                     }
175                 }
176 
177         val uiSlices = groupedData.getOrElse(FrameSliceType.UiThread) { listOf() }
178         val rtSlices = groupedData.getOrElse(FrameSliceType.RenderThread) { listOf() }
179         val actualSlices = groupedData.getOrElse(FrameSliceType.Actual) { listOf() }
180         val expectedSlices = groupedData.getOrElse(FrameSliceType.Expected) { listOf() }
181 
182         if (uiSlices.isEmpty()) {
183             return emptyList()
184         }
185 
186         check(rtSlices.isNotEmpty()) {
187             "Observed no renderthread slices in trace - verify that your benchmark is redrawing" +
188                 " and is hardware accelerated (which is the default)."
189         }
190 
191         return if (captureApiLevel >= 31) {
192             check(actualSlices.isNotEmpty() && expectedSlices.isNotEmpty()) {
193                 "Observed no expect/actual slices in trace," +
194                     " please report bug and attach perfetto trace."
195             }
196             check(slices.none { it.frameId == null }) {
197                 "Observed frame in trace missing id, please report bug and attach perfetto trace."
198             }
199 
200             val actualSlicesPool = actualSlices.toMutableList()
201             rtSlices.mapNotNull { rtSlice ->
202                 val frameId = rtSlice.frameId!!
203 
204                 val uiSlice = uiSlices.binarySearchFrameId(frameId)
205 
206                 // Ideally, we'd rely on frameIds, but these can fall out of sync due to b/279088460
207                 //     expectedSlice = expectedSlices.binarySearchFrameId(frameId),
208                 //     actualSlice = actualSlices.binarySearchFrameId(frameId)
209                 // A pool of actual slices is used to prevent incorrect duplicate mapping. At the
210                 //     end of the trace, the synthetic expect/actual slices may be missing even if
211                 //     the complete end of frame is present, and we want to discard those. This
212                 //     doesn't happen at front of trace, since we find actuals from the end.
213                 if (uiSlice != null) {
214                     val actualSlice =
215                         actualSlicesPool.lastOrNull {
216                             // Use fixed offset since synthetic tracepoint for actual may start
217                             // after the
218                             // actual UI slice (have observed 2us in practice)
219                             it.ts < uiSlice.ts + 50_000 &&
220                                 // ensure there's some overlap - if actual doesn't contain ui, may
221                                 // just
222                                 // be "abandoned" slice at beginning of trace
223                                 it.contains(uiSlice.ts + (uiSlice.dur / 2))
224                         }
225                     actualSlicesPool.remove(actualSlice)
226                     val expectedSlice =
227                         actualSlice?.frameId?.run { expectedSlices.binarySearchFrameId(this) }
228 
229                     FrameData.tryCreate31(
230                         uiSlice = uiSlice,
231                         rtSlice = rtSlice,
232                         expectedSlice = expectedSlice,
233                         actualSlice = actualSlice
234                     )
235                 } else {
236                     null
237                 }
238             }
239         } else {
240             // note that we expect no frame ids on API < 31, and don't observe them
241             // on most devices, but it has been observed, so we just ignore them
242             rtSlices.mapNotNull { rtSlice ->
243                 FrameData.tryCreateBasic(
244                     uiSlice = uiSlices.firstOrNull { it.contains(rtSlice.ts) },
245                     rtSlice = rtSlice
246                 )
247             }
248         }
249     }
250 
251     fun List<FrameData>.getFrameSubMetrics(captureApiLevel: Int): Map<SubMetric, List<Long>> {
252         return SubMetric.values()
253             .filter { it.supportedOnApiLevel(captureApiLevel) }
254             .associateWith { subMetric -> map { frame -> frame.get(subMetric) } }
255     }
256 }
257