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