blob: cd1404fdf03aa5fdfa0f7dd804d0e1aea48083f7 [file] [log] [blame]
#!/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)