| #!/usr/bin/env python3 |
| # Copyright (C) 2021 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| from perfetto.trace_processor.api import TraceProcessor |
| from perfetto.trace_processor.api import TraceProcessorException |
| |
| |
| def compute_breakdown(tp: TraceProcessor, |
| start_ts=None, |
| end_ts=None, |
| process_name=None, |
| main_thread: bool = False): |
| """For each userspace slice in the trace processor instance |tp|, computes |
| the self-time of that slice grouping by process name, thread name |
| and thread state. |
| |
| Args: |
| tp: the trace processor instance to query. |
| start_ts: optional bound to only consider slices after this ts |
| end_ts: optional bound to only consider slices until this ts |
| process_name: optional process name to filter for slices; specifying |
| this argument can make computing the breakdown a lot faster. |
| main_thread: whether to only consider slices on the main thread of each |
| process. |
| |
| Returns: |
| A Pandas dataframe containing the total self time taken by a slice stack |
| broken down by process name, thread name and thread state. |
| """ |
| bounds = tp.query('SELECT * FROM trace_bounds').as_pandas_dataframe() |
| start_ts = start_ts if start_ts else bounds['start_ts'][0] |
| end_ts = end_ts if end_ts else bounds['end_ts'][0] |
| |
| tp.query(""" |
| DROP VIEW IF EXISTS modded_names |
| """) |
| |
| tp.query(""" |
| SELECT IMPORT('android.slices'); |
| CREATE VIEW modded_names AS |
| SELECT |
| slice.id, |
| slice.depth, |
| slice.stack_id, |
| ANDROID_STANDARDIZE_SLICE_NAME(slice.name) AS modded_name |
| FROM slice |
| """) |
| |
| tp.query(""" |
| DROP VIEW IF EXISTS thread_slice_stack |
| """) |
| |
| tp.query(f""" |
| CREATE VIEW thread_slice_stack AS |
| SELECT |
| efs.ts, |
| efs.dur, |
| IFNULL(n.stack_id, -1) AS stack_id, |
| t.utid, |
| IIF(efs.source_id IS NULL, '[No slice]', IFNULL( |
| ( |
| SELECT GROUP_CONCAT(modded_name, ' > ') |
| FROM ( |
| SELECT p.modded_name |
| FROM ancestor_slice(efs.source_id) a |
| JOIN modded_names p ON a.id = p.id |
| ORDER BY p.depth |
| ) |
| ) || ' > ' || n.modded_name, |
| n.modded_name |
| )) AS stack_name |
| FROM experimental_flat_slice({start_ts}, {end_ts}) efs |
| LEFT JOIN modded_names n ON efs.source_id = n.id |
| JOIN thread_track t ON t.id = efs.track_id |
| """) |
| |
| tp.query(""" |
| DROP TABLE IF EXISTS thread_slice_stack_with_state |
| """) |
| |
| tp.query(""" |
| CREATE VIRTUAL TABLE thread_slice_stack_with_state |
| USING SPAN_JOIN( |
| thread_slice_stack PARTITIONED utid, |
| thread_state PARTITIONED utid |
| ) |
| """) |
| |
| if process_name: |
| where_process = f"AND process.name GLOB '{process_name}'" |
| else: |
| where_process = '' |
| |
| if main_thread: |
| where_main_thread = "AND thread.is_main_thread" |
| else: |
| where_main_thread = "" |
| |
| breakdown = tp.query(f""" |
| SELECT |
| process.name AS process_name, |
| thread.name AS thread_name, |
| CASE |
| WHEN slice.state = 'D' and slice.io_wait |
| THEN 'Uninterruptible sleep (IO)' |
| WHEN slice.state = 'DK' and slice.io_wait |
| THEN 'Uninterruptible sleep + Wake-kill (IO)' |
| WHEN slice.state = 'D' and not slice.io_wait |
| THEN 'Uninterruptible sleep (non-IO)' |
| WHEN slice.state = 'DK' and not slice.io_wait |
| THEN 'Uninterruptible sleep + Wake-kill (non-IO)' |
| WHEN slice.state = 'D' |
| THEN 'Uninterruptible sleep' |
| WHEN slice.state = 'DK' |
| THEN 'Uninterruptible sleep + Wake-kill' |
| WHEN slice.state = 'S' THEN 'Interruptible sleep' |
| WHEN slice.state = 'R' THEN 'Runnable' |
| WHEN slice.state = 'R+' THEN 'Runnable (Preempted)' |
| ELSE slice.state |
| END AS state, |
| slice.stack_name, |
| SUM(slice.dur)/1e6 AS dur_sum, |
| MIN(slice.dur/1e6) AS dur_min, |
| MAX(slice.dur/1e6) AS dur_max, |
| AVG(slice.dur/1e6) AS dur_mean, |
| PERCENTILE(slice.dur/1e6, 50) AS dur_median, |
| PERCENTILE(slice.dur/1e6, 25) AS dur_25_percentile, |
| PERCENTILE(slice.dur/1e6, 75) AS dur_75_percentile, |
| PERCENTILE(slice.dur/1e6, 95) AS dur_95_percentile, |
| PERCENTILE(slice.dur/1e6, 99) AS dur_99_percentile, |
| COUNT(1) as count |
| FROM process |
| JOIN thread USING (upid) |
| JOIN thread_slice_stack_with_state slice USING (utid) |
| WHERE dur != -1 |
| {where_main_thread} |
| {where_process} |
| GROUP BY thread.name, stack_id, state |
| ORDER BY dur_sum DESC |
| """).as_pandas_dataframe() |
| |
| return breakdown |
| |
| |
| def compute_breakdown_for_startup(tp: TraceProcessor, |
| package_name=None, |
| process_name=None, |
| main_thread: bool = False): |
| """Computes the slice breakdown (like |compute_breakdown|) but only |
| considering slices which happened during an app startup |
| |
| Args: |
| tp: the trace processor instance to query. |
| package_name: optional package name to filter for startups. Only a single |
| startup matching this package name should be present. If not specified, |
| only a single startup of any app should be in the trace. |
| process_name: optional process name to filter for slices; specifying |
| this argument can make computing the breakdown a lot faster. |
| main_thread: whether to only consider slices on the main thread of each |
| process. |
| |
| Returns: |
| The same as |compute_breakdown| but only containing slices which happened |
| during app startup. |
| """ |
| |
| # Verify there was only one startup in the trace matching the package |
| # name. |
| filter = "WHERE package = '{}'".format(package_name) if package_name else '' |
| launches = tp.query(f''' |
| SELECT IMPORT('android.startup.startups'); |
| |
| SELECT ts, ts_end, dur |
| FROM android_startups |
| {filter} |
| ''').as_pandas_dataframe() |
| if len(launches) == 0: |
| raise TraceProcessorException("Didn't find startup in trace") |
| if len(launches) > 1: |
| raise TraceProcessorException("Found multiple startups in trace") |
| |
| start = launches['ts'][0] |
| end = launches['ts_end'][0] |
| |
| return compute_breakdown(tp, start, end, process_name, main_thread) |