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