1#!/usr/bin/env python3 2# Copyright (C) 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 16from perfetto.trace_processor.api import TraceProcessor 17from perfetto.trace_processor.api import TraceProcessorException 18 19 20def compute_breakdown(tp: TraceProcessor, 21 start_ts=None, 22 end_ts=None, 23 process_name=None): 24 """For each userspace slice in the trace processor instance |tp|, computes 25 the self-time of that slice grouping by process name, thread name 26 and thread state. 27 28 Args: 29 tp: the trace processor instance to query. 30 start_ts: optional bound to only consider slices after this ts 31 end_ts: optional bound to only consider slices until this ts 32 process_name: optional process name to filter for slices; specifying 33 this argument can make computing the breakdown a lot faster. 34 35 Returns: 36 A Pandas dataframe containing the total self time taken by a slice stack 37 broken down by process name, thread name and thread state. 38 """ 39 bounds = tp.query('SELECT * FROM trace_bounds').as_pandas_dataframe() 40 start_ts = start_ts if start_ts else bounds['start_ts'][0] 41 end_ts = end_ts if end_ts else bounds['end_ts'][0] 42 43 tp.query(""" 44 DROP VIEW IF EXISTS modded_names 45 """) 46 47 tp.query(""" 48 CREATE VIEW modded_names AS 49 SELECT 50 slice.id, 51 slice.depth, 52 slice.stack_id, 53 CASE 54 WHEN slice.name LIKE 'Choreographer#doFrame%' 55 THEN 'Choreographer#doFrame' 56 WHEN slice.name LIKE 'DrawFrames%' 57 THEN 'DrawFrames' 58 WHEN slice.name LIKE '/data/app%.apk' 59 THEN 'APK load' 60 WHEN slice.name LIKE 'OpenDexFilesFromOat%' 61 THEN 'OpenDexFilesFromOat' 62 WHEN slice.name LIKE 'Open oat file%' 63 THEN 'Open oat file' 64 ELSE slice.name 65 END AS modded_name 66 FROM slice 67 """) 68 69 tp.query(""" 70 DROP VIEW IF EXISTS thread_slice_stack 71 """) 72 73 tp.query(""" 74 CREATE VIEW thread_slice_stack AS 75 SELECT 76 efs.ts, 77 efs.dur, 78 IFNULL(n.stack_id, -1) AS stack_id, 79 t.utid, 80 IIF(efs.source_id IS NULL, '[No slice]', IFNULL( 81 ( 82 SELECT GROUP_CONCAT(modded_name, ' > ') 83 FROM ( 84 SELECT p.modded_name 85 FROM ancestor_slice(efs.source_id) a 86 JOIN modded_names p ON a.id = p.id 87 ORDER BY p.depth 88 ) 89 ) || ' > ' || n.modded_name, 90 n.modded_name 91 )) AS stack_name 92 FROM experimental_flat_slice({}, {}) efs 93 LEFT JOIN modded_names n ON efs.source_id = n.id 94 JOIN thread_track t ON t.id = efs.track_id 95 """.format(start_ts, end_ts)) 96 97 tp.query(""" 98 DROP TABLE IF EXISTS thread_slice_stack_with_state 99 """) 100 101 tp.query(""" 102 CREATE VIRTUAL TABLE thread_slice_stack_with_state 103 USING SPAN_JOIN( 104 thread_slice_stack PARTITIONED utid, 105 thread_state PARTITIONED utid 106 ) 107 """) 108 109 if process_name: 110 where_process = "AND process.name = '{}'".format(process_name) 111 else: 112 where_process = '' 113 114 breakdown = tp.query(""" 115 SELECT 116 process.name AS process_name, 117 thread.name AS thread_name, 118 CASE 119 WHEN slice.state = 'D' and slice.io_wait 120 THEN 'Uninterruptible sleep (IO)' 121 WHEN slice.state = 'DK' and slice.io_wait 122 THEN 'Uninterruptible sleep + Wake-kill (IO)' 123 WHEN slice.state = 'D' and not slice.io_wait 124 THEN 'Uninterruptible sleep (non-IO)' 125 WHEN slice.state = 'DK' and not slice.io_wait 126 THEN 'Uninterruptible sleep + Wake-kill (non-IO)' 127 WHEN slice.state = 'D' 128 THEN 'Uninterruptible sleep' 129 WHEN slice.state = 'DK' 130 THEN 'Uninterruptible sleep + Wake-kill' 131 WHEN slice.state = 'S' THEN 'Interruptible sleep' 132 WHEN slice.state = 'R' THEN 'Runnable' 133 WHEN slice.state = 'R+' THEN 'Runnable (Preempted)' 134 ELSE slice.state 135 END AS state, 136 slice.stack_name, 137 SUM(slice.dur)/1e6 AS dur_sum, 138 MIN(slice.dur/1e6) AS dur_min, 139 MAX(slice.dur/1e6) AS dur_max, 140 AVG(slice.dur/1e6) AS dur_mean, 141 PERCENTILE(slice.dur/1e6, 50) AS dur_median, 142 PERCENTILE(slice.dur/1e6, 25) AS dur_25_percentile, 143 PERCENTILE(slice.dur/1e6, 75) AS dur_75_percentile, 144 PERCENTILE(slice.dur/1e6, 95) AS dur_95_percentile, 145 PERCENTILE(slice.dur/1e6, 99) AS dur_99_percentile, 146 COUNT(1) as count 147 FROM process 148 JOIN thread USING (upid) 149 JOIN thread_slice_stack_with_state slice USING (utid) 150 WHERE dur != -1 {} 151 GROUP BY thread.name, stack_id, state 152 ORDER BY dur_sum DESC 153 """.format(where_process)).as_pandas_dataframe() 154 155 return breakdown 156 157 158def compute_breakdown_for_startup(tp: TraceProcessor, 159 package_name=None, 160 process_name=None): 161 """Computes the slice breakdown (like |compute_breakdown|) but only 162 considering slices which happened during an app startup 163 164 Args: 165 tp: the trace processor instance to query. 166 package_name: optional package name to filter for startups. Only a single 167 startup matching this package name should be present. If not specified, 168 only a single startup of any app should be in the trace. 169 process_name: optional process name to filter for slices; specifying 170 this argument can make computing the breakdown a lot faster. 171 172 Returns: 173 The same as |compute_breakdown| but only containing slices which happened 174 during app startup. 175 """ 176 tp.metric(['android_startup']) 177 178 # Verify there was only one startup in the trace matching the package 179 # name. 180 filter = "WHERE package = '{}'".format(package_name) if package_name else '' 181 launches = tp.query(''' 182 SELECT ts, ts_end, dur 183 FROM launches 184 {} 185 '''.format(filter)).as_pandas_dataframe() 186 if len(launches) == 0: 187 raise TraceProcessorException("Didn't find startup in trace") 188 if len(launches) > 1: 189 raise TraceProcessorException("Found multiple startups in trace") 190 191 start = launches['ts'][0] 192 end = launches['ts_end'][0] 193 194 return compute_breakdown(tp, start, end, process_name) 195