diff options
Diffstat (limited to 'python/perfetto/experimental/slice_breakdown/breakdown.py')
-rw-r--r-- | python/perfetto/experimental/slice_breakdown/breakdown.py | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/python/perfetto/experimental/slice_breakdown/breakdown.py b/python/perfetto/experimental/slice_breakdown/breakdown.py new file mode 100644 index 000000000..9eb99defe --- /dev/null +++ b/python/perfetto/experimental/slice_breakdown/breakdown.py @@ -0,0 +1,194 @@ +#!/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): + """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. + + 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(""" + CREATE VIEW modded_names AS + SELECT + slice.id, + slice.depth, + slice.stack_id, + CASE + WHEN slice.name LIKE 'Choreographer#doFrame%' + THEN 'Choreographer#doFrame' + WHEN slice.name LIKE 'DrawFrames%' + THEN 'DrawFrames' + WHEN slice.name LIKE '/data/app%.apk' + THEN 'APK load' + WHEN slice.name LIKE 'OpenDexFilesFromOat%' + THEN 'OpenDexFilesFromOat' + WHEN slice.name LIKE 'Open oat file%' + THEN 'Open oat file' + ELSE slice.name + END AS modded_name + FROM slice + """) + + tp.query(""" + DROP VIEW IF EXISTS thread_slice_stack + """) + + tp.query(""" + 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({}, {}) efs + LEFT JOIN modded_names n ON efs.source_id = n.id + JOIN thread_track t ON t.id = efs.track_id + """.format(start_ts, end_ts)) + + 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 = "AND process.name = '{}'".format(process_name) + else: + where_process = '' + + breakdown = tp.query(""" + 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 {} + GROUP BY thread.name, stack_id, state + ORDER BY dur_sum DESC + """.format(where_process)).as_pandas_dataframe() + + return breakdown + + +def compute_breakdown_for_startup(tp: TraceProcessor, + package_name=None, + process_name=None): + """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. + + Returns: + The same as |compute_breakdown| but only containing slices which happened + during app startup. + """ + tp.metric(['android_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(''' + SELECT ts, ts_end, dur + FROM launches + {} + '''.format(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) |