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