stdlib: Expose app startup breakdown module
This is a similar approach to the existing binder_breakdowns added
in aosp/3208231.
Test: tools/diff_test_trace_processor.py out/android/trace_processor_shell
Change-Id: Ia6eaff5797b6c2e7315a499bdfc16331770bcca7
diff --git a/Android.bp b/Android.bp
index d19109a..9415383 100644
--- a/Android.bp
+++ b/Android.bp
@@ -13565,6 +13565,7 @@
"src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql",
"src/trace_processor/perfetto_sql/stdlib/android/services.sql",
"src/trace_processor/perfetto_sql/stdlib/android/slices.sql",
+ "src/trace_processor/perfetto_sql/stdlib/android/startup/startup_breakdowns.sql",
"src/trace_processor/perfetto_sql/stdlib/android/startup/startup_events.sql",
"src/trace_processor/perfetto_sql/stdlib/android/startup/startups.sql",
"src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql",
diff --git a/BUILD b/BUILD
index c856483..f69117f 100644
--- a/BUILD
+++ b/BUILD
@@ -2784,6 +2784,7 @@
perfetto_filegroup(
name = "src_trace_processor_perfetto_sql_stdlib_android_startup_startup",
srcs = [
+ "src/trace_processor/perfetto_sql/stdlib/android/startup/startup_breakdowns.sql",
"src/trace_processor/perfetto_sql/stdlib/android/startup/startup_events.sql",
"src/trace_processor/perfetto_sql/stdlib/android/startup/startups.sql",
"src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn
index c495f08..bf7a41a 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn
@@ -16,6 +16,7 @@
perfetto_sql_source_set("startup") {
sources = [
+ "startup_breakdowns.sql",
"startup_events.sql",
"startups.sql",
"startups_maxsdk28.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/startup/startup_breakdowns.sql b/src/trace_processor/perfetto_sql/stdlib/android/startup/startup_breakdowns.sql
new file mode 100644
index 0000000..c6b972a
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/startup/startup_breakdowns.sql
@@ -0,0 +1,153 @@
+--
+-- Copyright 2024 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
+--
+-- https://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.
+
+INCLUDE PERFETTO MODULE android.startup.startups;
+INCLUDE PERFETTO MODULE intervals.overlap;
+INCLUDE PERFETTO MODULE slices.hierarchy;
+INCLUDE PERFETTO MODULE slices.with_context;
+
+-- Maps slice names with common prefixes to a static string key.
+-- Returns NULL if there's no mapping.
+CREATE PERFETTO FUNCTION _normalize_android_string(name STRING)
+RETURNS STRING
+AS
+SELECT
+ CASE
+ WHEN $name = 'mm_vmscan_direct_reclaim' THEN 'kernel_memory_reclaim'
+ WHEN $name GLOB 'GC: Wait For*' THEN 'userspace_memory_reclaim'
+ WHEN ($name GLOB 'monitor contention*' OR $name GLOB 'Lock contention on a monitor lock*')
+ THEN 'monitor_contention'
+ WHEN $name GLOB 'Lock contention*' THEN 'art_lock_contention'
+ WHEN ($name = 'binder transaction' OR $name = 'binder reply') THEN 'binder'
+ WHEN $name = 'Contending for pthread mutex' THEN 'mutex_contention'
+ WHEN $name GLOB 'dlopen*' THEN 'dlopen'
+ WHEN $name GLOB 'VerifyClass*' THEN 'verify_class'
+ WHEN $name = 'inflate' THEN 'inflate'
+ WHEN $name GLOB 'Choreographer#doFrame*' THEN 'choreographer_do_frame'
+ WHEN $name GLOB 'OpenDexFilesFromOat*' THEN 'open_dex_files_from_oat'
+ WHEN $name = 'ResourcesManager#getResources' THEN 'resources_manager_get_resources'
+ WHEN $name = 'bindApplication' THEN 'bind_application'
+ WHEN $name = 'activityStart' THEN 'activity_start'
+ WHEN $name = 'activityResume' THEN 'activity_resume'
+ WHEN $name = 'activityRestart' THEN 'activity_restart'
+ WHEN $name = 'clientTransactionExecuted' THEN 'client_transaction_executed'
+ ELSE NULL
+ END name;
+
+-- Derives a startup reason from a slice name and some thread_state columns.
+CREATE PERFETTO FUNCTION _startup_breakdown_reason(
+ name STRING,
+ state STRING,
+ io_wait INT,
+ irq_context INT)
+RETURNS STRING
+AS
+SELECT
+ CASE
+ WHEN $io_wait = 1 THEN 'io'
+ WHEN $name IS NOT NULL THEN $name
+ WHEN $irq_contex = 1 THEN 'irq'
+ ELSE $state
+ END name;
+
+-- List of startups with unique ids for each possible upid. The existing
+-- startup_ids are not necessarily unique (because of multiuser).
+CREATE PERFETTO TABLE _startup_root_slices
+AS
+SELECT
+ (SELECT MAX(id) FROM slice) + row_number() OVER () AS id,
+ android_startups.dur AS dur,
+ android_startups.ts AS ts,
+ android_startups.startup_id,
+ android_startups.startup_type,
+ process.name AS process_name,
+ thread.utid AS utid
+FROM android_startup_processes startup
+JOIN android_startups
+ USING (startup_id)
+JOIN thread
+ ON thread.upid = process.upid AND thread.is_main_thread
+JOIN process
+ ON process.upid = startup.upid
+WHERE android_startups.dur > 0
+ORDER BY ts;
+
+-- All slices normalized with _normalize_android_string.
+CREATE PERFETTO TABLE _startup_normalized_slices
+AS
+SELECT p.id, p.parent_id, p.depth, p.name, thread_slice.ts, thread_slice.dur, thread_slice.utid
+FROM
+ _slice_remove_nulls_and_reparent
+ !((SELECT id, parent_id, depth, _normalize_android_string(name) AS name FROM slice WHERE dur > 0), name)
+ p
+JOIN thread_slice
+ USING (id);
+
+-- Subset of _startup_normalized_slices that occurred during any app startups on the main thread.
+-- Their timestamps and durations are chopped to fit within the respective app startup duration.
+CREATE PERFETTO TABLE _startup_slices_breakdown
+AS
+SELECT *
+FROM _intervals_merge_root_and_children_by_intersection !(_startup_root_slices, _startup_normalized_slices, utid);
+
+-- Flattened slice version of _startup_slices_breakdown. This selects the leaf slice at every region
+-- of the slice stack.
+CREATE PERFETTO TABLE _startup_flat_slices_breakdown
+AS
+SELECT i.ts, i.dur, i.root_id, s.id AS slice_id, s.name FROM _intervals_flatten !(_startup_slices_breakdown) i
+JOIN _startup_normalized_slices s USING (id);
+
+-- Subset of thread_states that occurred during any app startups on the main thread.
+CREATE PERFETTO TABLE _startup_thread_states_breakdown
+AS
+SELECT i.ts, i.dur, i.root_id, t.id AS thread_state_id, t.state, t.io_wait, t.irq_context
+ FROM _intervals_merge_root_and_children_by_intersection!(_startup_root_slices,
+ (SELECT *, NULL AS parent_id FROM thread_state),
+ utid) i
+JOIN thread_state t USING(id);
+
+-- Intersection of _startup_flat_slices_breakdown and _startup_thread_states_breakdown.
+-- A left intersection is used since some parts of the slice stack may not have any slices
+-- but will have thread states.
+CREATE VIRTUAL TABLE _startup_thread_states_and_slices_breakdown_sp
+USING
+ SPAN_LEFT_JOIN(
+ _startup_thread_states_breakdown PARTITIONED root_id,
+ _startup_flat_slices_breakdown PARTITIONED root_id);
+
+-- Blended thread state and slice breakdown blocking app startups.
+--
+-- Each row blames a unique period during an app startup with a reason
+-- derived from the slices and thread states on the main thread.
+--
+-- Some helpful events to enables are binder transactions, ART, am and view.
+CREATE PERFETTO TABLE android_startup_opinionated_breakdown(
+ -- Startup id. Alias of `slice.id`
+ startup_id INT,
+ -- Id of relevant slice blocking startup. Alias of `slice.id`.
+ slice_id INT,
+ -- Id of thread_state blocking startup. Alias of `thread_state.id`.
+ thread_state_id INT,
+ -- Timestamp of an exclusive interval during the app startup with a single latency reason.
+ ts INT,
+ -- Duration of an exclusive interval during the app startup with a single latency reason.
+ dur INT,
+ -- Cause of delay during an exclusive interval of the app startup.
+ reason STRING
+)
+AS
+SELECT b.ts, b.dur, startup.startup_id, b.slice_id, b.thread_state_id, _startup_breakdown_reason(name, state, io_wait, irq_context) AS reason
+FROM _startup_thread_states_and_slices_breakdown_sp b
+JOIN _startup_root_slices startup ON startup.id = b.root_id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/intervals/overlap.sql b/src/trace_processor/perfetto_sql/stdlib/intervals/overlap.sql
index cd84164..356ec31 100644
--- a/src/trace_processor/perfetto_sql/stdlib/intervals/overlap.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/intervals/overlap.sql
@@ -13,6 +13,8 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
+INCLUDE PERFETTO MODULE intervals.intersect;
+
-- Compute the distribution of the overlap of the given intervals over time.
--
-- Each interval is a (ts, dur) pair and the overlap represented as a (ts, value)
@@ -94,6 +96,8 @@
-- Merges a |roots_table| and |children_table| into one table. See _intervals_flatten
-- that accepts the output of this macro to flatten intervals.
+
+-- See: _intervals_merge_root_and_children_by_intersection.
CREATE PERFETTO MACRO _intervals_merge_root_and_children(
-- Table or subquery containing all the root intervals: (id, ts, dur).
-- Note that parent_id is not necessary in this table as it will be NULL anyways.
@@ -141,6 +145,46 @@
JOIN _roots USING(root_id)
);
+-- Merges a |roots_table| and |children_table| into one table. See _intervals_flatten
+-- that accepts the output of this macro to flatten intervals.
+
+-- This is very similar to _intervals_merge_root_and_children but there is no explicit
+-- root_id shared between the root and the children. Instead an _interval_intersect is
+-- used to derive the root and child relationships.
+CREATE PERFETTO MACRO _intervals_merge_root_and_children_by_intersection(
+ -- Table or subquery containing all the root intervals: (id, ts, dur).
+ -- Note that parent_id is not necessary in this table as it will be NULL anyways.
+ roots_table TableOrSubquery,
+ -- Table or subquery containing all the child intervals:
+ -- (root_id, id, parent_id, ts, dur)
+ children_table TableOrSubquery,
+ -- intersection key used in deriving the root child relationships.
+ key ColumnName)
+RETURNS TableOrSubQuery
+AS (
+ WITH
+ _roots AS (
+ SELECT * FROM $roots_table WHERE dur > 0 ORDER BY ts
+ ),
+ _children AS (
+ SELECT * FROM $children_table WHERE dur > 0 ORDER BY ts
+ )
+ SELECT
+ ii.ts,
+ ii.dur,
+ _children.id,
+ IIF(_children.parent_id IS NULL, id_1, _children.parent_id) AS parent_id,
+ _roots.id AS root_id,
+ _roots.ts AS root_ts,
+ _roots.dur AS root_dur,
+ ii.$key
+ FROM _interval_intersect!((_children, _roots), ($key)) ii
+ JOIN _children
+ ON _children.id = id_0
+ JOIN _roots
+ ON _roots.id = id_1
+);
+
-- Partition and flatten a hierarchy of intervals into non-overlapping intervals where
-- each resulting interval is the leaf in the hierarchy at any given time. The result also
-- denotes the 'self-time' of each interval.
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql b/src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql
index dff6475..137a123 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql
@@ -91,4 +91,41 @@
UNION ALL
SELECT
id, type, ts, dur, track_id, category, name, depth, parent_id, arg_set_id, thread_ts, thread_dur
-FROM descendant_slice($slice_id);
\ No newline at end of file
+FROM descendant_slice($slice_id);
+
+-- Delete rows from |slice_table| where the |column_name| value is NULL.
+--
+-- The |parent_id| of the remaining rows are adjusted to point to the closest
+-- ancestor remaining. This keeps the trees as connected as possible,
+-- allowing further graph analysis.
+CREATE PERFETTO MACRO _slice_remove_nulls_and_reparent(
+ -- Table or subquery containing a subset of the slice table. Required columns are
+ -- (id INT64, parent_id INT64, depth UINT32, <column_name>).
+ slice_table TableOrSubQuery,
+ -- Column name for which a NULL value indicates the row will be deleted.
+ column_name ColumnName)
+ -- The returned table has the schema (id INT64, parent_id INT64, depth UINT32, <column_name>).
+RETURNS TableOrSubQuery
+AS (
+ WITH _slice AS (
+ SELECT * FROM $slice_table WHERE $column_name IS NOT NULL
+ )
+ SELECT
+ id,
+ parent_id,
+ depth,
+ $column_name
+ FROM _slice
+ WHERE depth = 0
+ UNION ALL
+ SELECT
+ child.id,
+ anc.id AS parent_id,
+ MAX(IIF(parent.$column_name IS NULL, 0, anc.depth)) AS depth,
+ child.$column_name
+ FROM _slice child
+ JOIN ancestor_slice(child.id) anc
+ LEFT JOIN _slice parent
+ ON parent.id = anc.id
+ GROUP BY child.id
+);
diff --git a/test/trace_processor/diff_tests/stdlib/android/startups_tests.py b/test/trace_processor/diff_tests/stdlib/android/startups_tests.py
index c4f5a2e..acce2d7 100644
--- a/test/trace_processor/diff_tests/stdlib/android/startups_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/android/startups_tests.py
@@ -168,3 +168,34 @@
"startup_id","time_to_initial_display","time_to_full_display","ttid_frame_id","ttfd_frame_id","upid"
0,143980066,620815843,5873276,5873353,229
"""))
+
+ def test_android_startup_breakdown(self):
+ return DiffTestBlueprint(
+ trace=DataPath('api31_startup_cold.perfetto-trace'),
+ query="""
+ INCLUDE PERFETTO MODULE android.startup.startup_breakdowns;
+ SELECT
+ SUM(dur) AS dur,
+ reason
+ FROM android_startup_opinionated_breakdown
+ GROUP BY reason ORDER BY dur DESC;
+ """,
+ out=Csv("""
+ "dur","reason"
+ 28663023,"choreographer_do_frame"
+ 22564487,"binder"
+ 16351925,"Running"
+ 13212137,"activity_start"
+ 10264635,"io"
+ 6779947,"inflate"
+ 6240207,"bind_application"
+ 5214375,"R+"
+ 3072397,"resources_manager_get_resources"
+ 2722869,"D"
+ 2574273,"open_dex_files_from_oat"
+ 2392761,"S"
+ 2353124,"activity_resume"
+ 1325727,"R"
+ 43698,"art_lock_contention"
+ 5573,"verify_class"
+ """))
diff --git a/test/trace_processor/diff_tests/stdlib/intervals/tests.py b/test/trace_processor/diff_tests/stdlib/intervals/tests.py
index cf48cdc..2712599 100644
--- a/test/trace_processor/diff_tests/stdlib/intervals/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/intervals/tests.py
@@ -118,3 +118,72 @@
8,1,0,0
9,1,1,1
"""))
+
+ def test_intervals_flatten_by_intersection(self):
+ return DiffTestBlueprint(
+ trace=TextProto(""),
+ query="""
+ INCLUDE PERFETTO MODULE intervals.overlap;
+
+ CREATE PERFETTO TABLE foo AS
+ WITH roots_data (id, ts, dur, utid) AS (
+ VALUES
+ (0, 0, 9, 0),
+ (0, 0, 9, 1),
+ (1, 9, 1, 2)
+ ), children_data (id, parent_id, ts, dur, utid) AS (
+ VALUES
+ (2, 0, 1, 3, 0),
+ (3, 0, 5, 1, 0),
+ (4, 0, 6, 1, 0),
+ (5, 0, 7, 0, 0),
+ (6, 0, 7, 1, 0),
+ (7, 2, 2, 1, 0)
+ )
+ SELECT *
+ FROM _intervals_merge_root_and_children_by_intersection!(roots_data, children_data, utid);
+
+ SELECT ts, dur, id, root_id FROM _intervals_flatten!(foo) ORDER BY ts;
+ """,
+ out=Csv("""
+ "ts","dur","id","root_id"
+ 0,1,0,0
+ 1,1,2,0
+ 2,1,7,0
+ 3,1,2,0
+ 4,1,0,0
+ 5,1,3,0
+ 6,1,4,0
+ 7,1,6,0
+ 8,1,0,0
+ """))
+
+ def test_intervals_flatten_by_intersection_no_matching_key(self):
+ return DiffTestBlueprint(
+ trace=TextProto(""),
+ query="""
+ INCLUDE PERFETTO MODULE intervals.overlap;
+
+ CREATE PERFETTO TABLE foo AS
+ WITH roots_data (id, ts, dur, utid) AS (
+ VALUES
+ (0, 0, 9, 1),
+ (0, 0, 9, 2),
+ (1, 9, 1, 3)
+ ), children_data (id, parent_id, ts, dur, utid) AS (
+ VALUES
+ (2, 0, 1, 3, 0),
+ (3, 0, 5, 1, 0),
+ (4, 0, 6, 1, 0),
+ (5, 0, 7, 0, 0),
+ (6, 0, 7, 1, 0),
+ (7, 2, 2, 1, 0)
+ )
+ SELECT *
+ FROM _intervals_merge_root_and_children_by_intersection!(roots_data, children_data, utid);
+
+ SELECT ts, dur, id, root_id FROM _intervals_flatten!(foo) ORDER BY ts;
+ """,
+ out=Csv("""
+ "ts","dur","id","root_id"
+ """))
diff --git a/test/trace_processor/diff_tests/stdlib/slices/tests.py b/test/trace_processor/diff_tests/stdlib/slices/tests.py
index fa326d0..625659e 100644
--- a/test/trace_processor/diff_tests/stdlib/slices/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/slices/tests.py
@@ -104,6 +104,26 @@
"NestedThreadSlice",6,1,1
"""))
+ def test_slice_remove_nulls_and_reparent(self):
+ return DiffTestBlueprint(
+ trace=Path('trace.py'),
+ query="""
+ INCLUDE PERFETTO MODULE slices.hierarchy;
+
+ SELECT id, parent_id, name, depth
+ FROM _slice_remove_nulls_and_reparent!(
+ (SELECT id, parent_id, depth, IIF(name = 'ProcessSlice', NULL, name) AS name
+ FROM slice),
+ name
+ ) LIMIT 10;
+ """,
+ out=Csv("""
+ "id","parent_id","name","depth"
+ 0,"[NULL]","AsyncSlice",0
+ 2,"[NULL]","ThreadSlice",0
+ 3,2,"NestedThreadSlice",0
+ """))
+
# Common functions
def test_slice_flattened(self):
@@ -156,4 +176,4 @@
7,33333
8,46926
9,17865
- """))
\ No newline at end of file
+ """))