Merge "Output all_data_source_flushed_ns to metadata as an array" into main
diff --git a/Android.bp b/Android.bp
index bfa3740..6dff4d9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -11879,6 +11879,7 @@
         "src/trace_processor/perfetto_sql/stdlib/common/metadata.sql",
         "src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql",
         "src/trace_processor/perfetto_sql/stdlib/common/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/common/thread_states.sql",
         "src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql",
         "src/trace_processor/perfetto_sql/stdlib/experimental/android_broadcast.sql",
         "src/trace_processor/perfetto_sql/stdlib/experimental/flat_slices.sql",
diff --git a/BUILD b/BUILD
index 1ba49a5..edc1eca 100644
--- a/BUILD
+++ b/BUILD
@@ -2259,6 +2259,7 @@
         "src/trace_processor/perfetto_sql/stdlib/common/metadata.sql",
         "src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql",
         "src/trace_processor/perfetto_sql/stdlib/common/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/common/thread_states.sql",
         "src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql",
     ],
 )
diff --git a/protos/perfetto/metrics/android/android_boot.proto b/protos/perfetto/metrics/android/android_boot.proto
index 820b4e0..53de221 100644
--- a/protos/perfetto/metrics/android/android_boot.proto
+++ b/protos/perfetto/metrics/android/android_boot.proto
@@ -29,4 +29,10 @@
   optional ProcessStateDurations systemui_durations = 2;
   optional ProcessStateDurations launcher_durations = 3;
   optional ProcessStateDurations gms_durations = 4;
+  // Launcher related boot metrics
+  message LauncherBreakdown {
+    //  reports cold start time of NexusLauncher
+    optional int64 cold_start_dur = 1;
+  }
+  optional LauncherBreakdown launcher_breakdown = 5;
 }
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 3f270f6..d072a39 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -143,6 +143,12 @@
   optional ProcessStateDurations systemui_durations = 2;
   optional ProcessStateDurations launcher_durations = 3;
   optional ProcessStateDurations gms_durations = 4;
+  // Launcher related boot metrics
+  message LauncherBreakdown {
+    //  reports cold start time of NexusLauncher
+    optional int64 cold_start_dur = 1;
+  }
+  optional LauncherBreakdown launcher_breakdown = 5;
 }
 
 // End of protos/perfetto/metrics/android/android_boot.proto
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index fbf1987..8a6f511 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -45,7 +45,8 @@
   // Changes:
   // 7. Introduce GUESS_CPU_SIZE
   // 8. Add 'json' option to ComputeMetricArgs
-  TRACE_PROCESSOR_CURRENT_API_VERSION = 8;
+  // 9. Add get_thread_state_summary_for_interval.
+  TRACE_PROCESSOR_CURRENT_API_VERSION = 9;
 }
 
 // At lowest level, the wire-format of the RPC procol is a linear sequence of
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 7af1fde..d62f1fd 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/perfetto/trace_processor/trace_processor.descriptor b/python/perfetto/trace_processor/trace_processor.descriptor
index 33ca764..8b8383f 100644
--- a/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/src/trace_processor/metrics/sql/android/android_boot.sql b/src/trace_processor/metrics/sql/android/android_boot.sql
index 46caea8..4dc8936 100644
--- a/src/trace_processor/metrics/sql/android/android_boot.sql
+++ b/src/trace_processor/metrics/sql/android/android_boot.sql
@@ -47,5 +47,9 @@
         SELECT NULL_IF_EMPTY(ProcessStateDurations(
             'total_dur', total_dur,
             'uninterruptible_sleep_dur', uint_sleep_dur))
-        FROM get_durations('com.google.android.gms.persistent'))
+        FROM get_durations('com.google.android.gms.persistent')),
+    'launcher_breakdown', (
+        SELECT NULL_IF_EMPTY(AndroidBootMetric_LauncherBreakdown(
+            'cold_start_dur', dur))
+        FROM slice where name="LauncherColdStartup")
 );
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn
index 93ffc3d..bb4d758 100644
--- a/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn
@@ -22,6 +22,7 @@
     "metadata.sql",
     "percentiles.sql",
     "slices.sql",
+    "thread_states.sql",
     "timestamps.sql",
   ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/thread_states.sql b/src/trace_processor/perfetto_sql/stdlib/common/thread_states.sql
new file mode 100644
index 0000000..2e9dce1
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/common/thread_states.sql
@@ -0,0 +1,111 @@
+--
+-- Copyright 2022 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 common.timestamps;
+INCLUDE PERFETTO MODULE common.cpus;
+
+-- TODO(altimin): this doesn't handle some corner cases which thread_state.ts
+-- handles (as complex strings manipulations in SQL are pretty painful),
+-- but they are pretty niche.
+-- Translates the thread state name from a single-letter shorthard to
+-- a human-readable name.
+CREATE PERFETTO FUNCTION internal_translate_thread_state_name(name STRING)
+RETURNS STRING AS
+SELECT CASE $name
+WHEN 'Running' THEN 'Running'
+WHEN 'R' THEN 'Runnable'
+WHEN 'R+' THEN 'Runnable (Preempted)'
+WHEN 'S' THEN 'Sleeping'
+WHEN 'D' THEN 'Uninterruptible Sleep'
+WHEN 'T' THEN 'Stopped'
+WHEN 't' THEN 'Traced'
+WHEN 'X' THEN 'Exit (Dead)'
+WHEN 'Z' THEN 'Exit (Zombie)'
+WHEN 'x' THEN 'Task Dead'
+WHEN 'I' THEN 'Idle'
+WHEN 'K' THEN 'Wakekill'
+WHEN 'W' THEN 'Waking'
+WHEN 'P' THEN 'Parked'
+WHEN 'N' THEN 'No Load'
+ELSE $name
+END;
+
+-- Returns a human-readable name for a thread state.
+-- @arg id INT  Thread state id.
+-- @ret STRING  Human-readable name for the thread state.
+CREATE PERFETTO FUNCTION human_readable_thread_state_name(id INT)
+RETURNS STRING AS
+WITH data AS (
+  SELECT
+    internal_translate_thread_state_name(state) AS state,
+    (CASE io_wait
+      WHEN 1 THEN ' (IO)'
+      WHEN 0 THEN ' (non-IO)'
+      ELSE ''
+    END) AS io_wait
+  FROM thread_state
+  WHERE id = $id
+)
+SELECT
+  printf('%s%s', state, io_wait)
+FROM data;
+
+-- Returns an aggregation of thread states (by state and cpu) for a given
+-- interval of time for a given thread.
+-- @arg ts INT       The start of the interval.
+-- @arg dur INT      The duration of the interval.
+-- @arg utid INT     The utid of the thread.
+-- @column state     Human-readable thread state name.
+-- @column raw_state Raw thread state name, alias of `thread_state.state`.
+-- @column cpu_type  The type of CPU if available (e.g. "big" / "mid" / "little").
+-- @column cpu       The CPU index.
+-- @column blocked_function The name of the kernel function execution is blocked in.
+-- @column dur       The total duration.
+CREATE PERFETTO FUNCTION thread_state_summary_for_interval(
+  ts INT, dur INT, utid INT)
+RETURNS TABLE(
+  state STRING, raw_state STRING, cpu_type STRING, cpu INT, blocked_function STRING, dur INT)
+AS
+WITH
+states_starting_inside AS (
+  SELECT id
+  FROM thread_state
+  WHERE $ts <= ts
+    AND ts <= $ts + $dur
+    AND utid = $utid
+),
+first_state_starting_before AS (
+  SELECT id
+  FROM thread_state
+  WHERE ts < $ts AND utid = $utid
+  ORDER BY ts DESC
+  LIMIT 1
+),
+relevant_states AS (
+  SELECT * FROM states_starting_inside
+  UNION ALL
+  SELECT * FROM first_state_starting_before
+)
+SELECT
+  human_readable_thread_state_name(id) as state,
+  state as raw_state,
+  guess_cpu_size(cpu) as cpu_type,
+  cpu,
+  blocked_function,
+  sum(spans_overlapping_dur($ts, $dur, ts, dur)) as dur
+FROM thread_state
+JOIN relevant_states USING (id)
+GROUP BY state, raw_state, cpu_type, cpu, blocked_function
+ORDER BY dur desc;
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
index e5e8fa3..e908009 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
@@ -1 +1 @@
-041ebd18b8b3f62f76b1613f548c8cf8ea5660f818b4ecfab787c4b08fd55b50
\ No newline at end of file
+3a07a7ddf3093eb5c6672262585ffa510a61a6697e1bd0abc2cdea5a4f7d43e8
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
index 6cee1b2..34a1640 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
@@ -1 +1 @@
-ec0a00856b147b2e13d0fe18666a307eb085ac437d67f78787131d4ea4190581
\ No newline at end of file
+988f934497578397dd5296c2b13b102b0f20d0c0be7a3a456edbb0af12d86af3
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
index 6e1e7b7..b54ff87 100644
--- a/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
@@ -1 +1 @@
-875e17941e1dd5c362eef4bc679af41db5a9b2e37bbde5e5b2d1e90fd54b9e28
\ No newline at end of file
+5da7d4256544d4d354adb709731c6f0f190fa02545c8d0ae5ca3bf5cd92c4737
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
index 9203ad1..6920dd4 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
@@ -1 +1 @@
-1d185424a99b85372cecac21728fbf782fe33abf6a0664791ad08a3902bcdc3e
\ No newline at end of file
+3cef07cfaa35974d947d078764da91201dad7d8dd524fcfe72640de7adea6806
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
index 64e5c99..022b7e0 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
@@ -1 +1 @@
-1ae776d7033331f560685bfd61aa83a8a3f9639a400150bfe7be19642be3855a
\ No newline at end of file
+3095f3a5dd9b778eea5b3dff0b2e16b0eb1f7465be30ae57ac6403d79059c0d4
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
index 60017fd..d06f2da 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
@@ -1 +1 @@
-d4047dbc06457be945fc612e629fca6a8ffaf15332fe76c653b1dd7a9a58d06b
\ No newline at end of file
+22ac238188e556571d92ef85001b632f880e00833de3902a670c70e887f2a2b1
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
index 9b5f546..27e856c 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
@@ -1 +1 @@
-846bdc1f6e20082da46d483049322300d91cb38ef72f922c934d46cc5df3507d
\ No newline at end of file
+20b4490ecc9f130cd3a4a4fb32b8474cf85a5217e4d2513bcd5a72aa42a868ae
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
index 6710525..f43095d 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
@@ -1 +1 @@
-5f6f302958f0b26df40b90740bd8786480d7e8aa89232baa4fe2d881c070ba3c
\ No newline at end of file
+efc88907b3637cb94687b2dea5f84c0545c3e2d893e7c704d2891b9e9d0c6804
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
index 655b0f9..73fda6b 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
@@ -1 +1 @@
-fff7ed44c6100f75f93e21bdb9dbd093a8e67274d5eee255bf96e7d26a5614ca
\ No newline at end of file
+8795bbc1fe7e8394bdfebab94fb52d523db94645d8a0e9e0936ff7aebd26005c
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
index 8febff4..b7d6ed3 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
@@ -1 +1 @@
-31029a524c31322ce5c72f246de163b28047b839275a30b07061153a963a386b
\ No newline at end of file
+9d49ae65dbefffb6ecff8c5fa88c40fda7010597c30f5f5352bb60bd4c7fa2f8
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
index 38f1e1d..2b82951 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
@@ -1 +1 @@
-cf12d662d4137c081875afbb1c508827c9430e2837013bd917acd6eaadfac37c
\ No newline at end of file
+919fb0fb7dee21d9863631dfefe1dc361b10bcfbc538d6304bdf6bf08956b9ea
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
index 895dc82..dcf9e23 100644
--- a/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
@@ -1 +1 @@
-177e27d4d86bee1a17fce48d651b160f1541434aeb0f9e8fc1bac2b8fb07ac6d
\ No newline at end of file
+2bb21af6fe2d7485395456fc3645a966ac8127848b08b43da1a6acbb94fffedf
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
index 5294534..3bf47c4 100644
--- a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
@@ -1 +1 @@
-c099f4ab43ee73de87c83ca2bb8cd2c087abdb12512ca3855e5cb6e5203e378b
\ No newline at end of file
+c67a816aa2f557db82b0ba7c5f75b9bb055b1a5e4bebee7621e86c3ba39f4598
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256 b/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
index 7a976a8..5dad293 100644
--- a/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
@@ -1 +1 @@
-4486b3cba3f423541c1efd3f8dba5ee157e8215263c3250e8cefac6382905691
\ No newline at end of file
+90fbaef03da9a8b0af6a3d20ae58fd9c77bfd9dcb878eebe4b7b0fd6863faa1a
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256 b/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
index b0f92e9..f19511f 100644
--- a/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
@@ -1 +1 @@
-3c80ba72b9bd0454af4aac3352c4e8f855f48feeba53d2a5ac7566333b4cf763
\ No newline at end of file
+3f15c9a2d649e73575a524ee64832edcd994d2f46b0452a506c9b7a88b6439c4
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
index 33f95ef..d0c2640 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
@@ -1 +1 @@
-7506ad1268c6d92743d19f52d37a1e8b7cf00fc7907bf9e3e06966dbbb1b40c1
\ No newline at end of file
+361878a37a45869f351268cebb3da33476b8dbe41a15f498ee0c4a6c7794ce36
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
index 5294534..3bf47c4 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
@@ -1 +1 @@
-c099f4ab43ee73de87c83ca2bb8cd2c087abdb12512ca3855e5cb6e5203e378b
\ No newline at end of file
+c67a816aa2f557db82b0ba7c5f75b9bb055b1a5e4bebee7621e86c3ba39f4598
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
index 33f95ef..d0c2640 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
@@ -1 +1 @@
-7506ad1268c6d92743d19f52d37a1e8b7cf00fc7907bf9e3e06966dbbb1b40c1
\ No newline at end of file
+361878a37a45869f351268cebb3da33476b8dbe41a15f498ee0c4a6c7794ce36
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
index 33f95ef..d0c2640 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
@@ -1 +1 @@
-7506ad1268c6d92743d19f52d37a1e8b7cf00fc7907bf9e3e06966dbbb1b40c1
\ No newline at end of file
+361878a37a45869f351268cebb3da33476b8dbe41a15f498ee0c4a6c7794ce36
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_to_page_with_no_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_to_page_with_no_trace.png.sha256
index 22444d8..09f7455 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_to_page_with_no_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_to_page_with_no_trace.png.sha256
@@ -1 +1 @@
-52be49df180c482cad1a48979ce0bb2d20a7cdd27ad10cb972b5bad61b9865ca
\ No newline at end of file
+90d36af3ef3497134692d91b177f598a7b8998db7f74fb24d69440eaff3c24fa
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
index 99e5f07..b204fca 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
@@ -1 +1 @@
-4b96c2f13ce655e31532c21eac0652244803043ae6c56b5dcaec0e3158552256
\ No newline at end of file
+44562cc042fd22093ceb0f7e857f1fb7906324fe03106ee5bec356764bd534b0
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
index 5294534..3bf47c4 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
@@ -1 +1 @@
-c099f4ab43ee73de87c83ca2bb8cd2c087abdb12512ca3855e5cb6e5203e378b
\ No newline at end of file
+c67a816aa2f557db82b0ba7c5f75b9bb055b1a5e4bebee7621e86c3ba39f4598
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
index 33f95ef..d0c2640 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
@@ -1 +1 @@
-7506ad1268c6d92743d19f52d37a1e8b7cf00fc7907bf9e3e06966dbbb1b40c1
\ No newline at end of file
+361878a37a45869f351268cebb3da33476b8dbe41a15f498ee0c4a6c7794ce36
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
index 33f95ef..d0c2640 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
@@ -1 +1 @@
-7506ad1268c6d92743d19f52d37a1e8b7cf00fc7907bf9e3e06966dbbb1b40c1
\ No newline at end of file
+361878a37a45869f351268cebb3da33476b8dbe41a15f498ee0c4a6c7794ce36
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 0918d10..1c05fad 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -87,6 +87,7 @@
 from diff_tests.parser.translated_args.tests import TranslatedArgs
 from diff_tests.parser.ufs.tests import Ufs
 from diff_tests.stdlib.android.tests import AndroidStdlib
+from diff_tests.stdlib.common.tests import StdlibCommon
 from diff_tests.stdlib.chrome.tests import ChromeStdlib
 from diff_tests.stdlib.chrome.tests_scroll_jank import ChromeScrollJankStdlib
 from diff_tests.stdlib.dynamic_tables.tests import DynamicTables
@@ -160,7 +161,8 @@
       *TranslatedArgs(index_path, 'parser/translated_args',
                       'TranslatedArgs').fetch(),
       *Ufs(index_path, 'parser/ufs', 'Ufs').fetch(),
-      # TODO(altimin, lalitm): "parsing" should be split into more specific directories.
+      # TODO(altimin, lalitm): "parsing" should be split into more specific
+      # directories.
       *Parsing(index_path, 'parser/parsing', 'Parsing').fetch(),
       *ParsingDebugAnnotation(index_path, 'parser/parsing',
                               'ParsingDebugAnnotation').fetch(),
@@ -208,6 +210,7 @@
       *DynamicTables(index_path, 'stdlib/dynamic_tables',
                      'DynamicTables').fetch(),
       *Pkvm(index_path, 'stdlib/pkvm', 'Pkvm').fetch(),
+      *StdlibCommon(index_path, 'stdlib/common', 'StdlibCommon').fetch(),
       *Slices(index_path, 'stdlib/slices', 'Slices').fetch(),
       *SpanJoinLeftJoin(index_path, 'stdlib/span_join',
                         'SpanJoinLeftJoin').fetch(),
diff --git a/test/trace_processor/diff_tests/stdlib/common/tests.py b/test/trace_processor/diff_tests/stdlib/common/tests.py
new file mode 100644
index 0000000..a902b5b
--- /dev/null
+++ b/test/trace_processor/diff_tests/stdlib/common/tests.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 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 a
+#
+#      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 python.generators.diff_tests.testing import Path, DataPath, Metric
+from python.generators.diff_tests.testing import Csv, Json, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class StdlibCommon(TestSuite):
+
+  def test_thread_state_summary(self):
+    return DiffTestBlueprint(
+        trace=Path('../../common/synth_1.py'),
+        query="""
+        INCLUDE PERFETTO MODULE common.thread_states;
+
+        SELECT
+          state,
+          cpu,
+          dur
+        FROM thread_state_summary_for_interval(
+          25,
+          75,
+          (
+            SELECT utid
+            FROM thread
+            WHERE name = 'init'
+          )
+        )
+        """,
+        out=Csv("""
+        "state","cpu","dur"
+        "Running",1,50
+        "Runnable","[NULL]",25
+        """))
\ No newline at end of file
diff --git a/ui/src/base/mithril_utils.ts b/ui/src/base/mithril_utils.ts
index 854221f..1f07578 100644
--- a/ui/src/base/mithril_utils.ts
+++ b/ui/src/base/mithril_utils.ts
@@ -14,10 +14,8 @@
 
 import m from 'mithril';
 
-import {exists} from './utils';
-
 // Check if a mithril component vnode has children
 export function hasChildren({children}: m.Vnode<any>): boolean {
   return Array.isArray(children) && children.length > 0 &&
-      children.some(exists);
+      children.some((value) => value);
 }
diff --git a/ui/src/base/time.ts b/ui/src/base/time.ts
index 528fdbf..c8e5837 100644
--- a/ui/src/base/time.ts
+++ b/ui/src/base/time.ts
@@ -319,6 +319,10 @@
     this.end = end;
   }
 
+  static fromTimeAndDuration(start: time, duration: duration): TimeSpan {
+    return new TimeSpan(start, Time.add(start, duration));
+  }
+
   get duration(): duration {
     return this.end - this.start;
   }
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index a298b36..881cb37 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -15,11 +15,12 @@
 import m from 'mithril';
 
 import {Icons} from '../base/semantic_icons';
-import {duration, Time} from '../base/time';
+import {duration, Time, TimeSpan} from '../base/time';
 import {exists} from '../base/utils';
 import {EngineProxy} from '../common/engine';
 import {runQuery} from '../common/queries';
 import {LONG, LONG_NULL, NUM, STR_NULL} from '../common/query_result';
+import {raf} from '../core/raf_scheduler';
 import {addDebugSliceTrack} from '../tracks/debug/slice_track';
 import {Button} from '../widgets/button';
 import {DetailsShell} from '../widgets/details_shell';
@@ -39,6 +40,10 @@
 import {renderArguments} from './slice_args';
 import {renderDetails} from './slice_details';
 import {getSlice, SliceDetails, SliceRef} from './sql/slice';
+import {
+  BreakdownByThreadState,
+  breakDownIntervalByThreadState,
+} from './sql/thread_state';
 import {asSliceSqlId} from './sql_types';
 
 interface ContextMenuItem {
@@ -226,6 +231,7 @@
   static readonly kind = 'dev.perfetto.ChromeSliceDetailsTab';
 
   private sliceDetails?: SliceDetails;
+  private breakdownByThreadState?: BreakdownByThreadState;
 
   static create(args: NewBottomTabArgs): ChromeSliceDetailsTab {
     return new ChromeSliceDetailsTab(args);
@@ -233,11 +239,23 @@
 
   constructor(args: NewBottomTabArgs) {
     super(args);
+    this.load();
+  }
 
+  async load() {
     // Start loading the slice details
     const {id, table} = this.config;
-    getSliceDetails(this.engine, id, table)
-        .then((sliceDetails) => this.sliceDetails = sliceDetails);
+    const details = await getSliceDetails(this.engine, id, table);
+
+    if (details !== undefined && details.thread !== undefined) {
+      this.breakdownByThreadState = await breakDownIntervalByThreadState(
+          this.engine,
+          TimeSpan.fromTimeAndDuration(details.ts, details.dur),
+          details.thread.utid);
+    }
+
+    this.sliceDetails = details;
+    raf.scheduleFullRedraw();
   }
 
   getTitle(): string {
@@ -245,24 +263,23 @@
   }
 
   viewTab() {
-    if (exists(this.sliceDetails)) {
-      const slice = this.sliceDetails;
-      return m(
-          DetailsShell,
-          {
-            title: 'Slice',
-            description: slice.name,
-            buttons: this.renderContextButton(slice),
-          },
-          m(
-              GridLayout,
-              renderDetails(slice),
-              this.renderRhs(this.engine, slice),
-              ),
-      );
-    } else {
+    if (!exists(this.sliceDetails)) {
       return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
     }
+    const slice = this.sliceDetails;
+    return m(
+        DetailsShell,
+        {
+          title: 'Slice',
+          description: slice.name,
+          buttons: this.renderContextButton(slice),
+        },
+        m(
+            GridLayout,
+            renderDetails(slice, this.breakdownByThreadState),
+            this.renderRhs(this.engine, slice),
+            ),
+    );
   }
 
   isLoading() {
diff --git a/ui/src/frontend/slice_details.ts b/ui/src/frontend/slice_details.ts
index 1f782a7..6056bbc 100644
--- a/ui/src/frontend/slice_details.ts
+++ b/ui/src/frontend/slice_details.ts
@@ -28,6 +28,10 @@
 import {addTab} from './bottom_tab';
 import {globals} from './globals';
 import {SliceDetails} from './sql/slice';
+import {
+  BreakdownByThreadState,
+  BreakdownByThreadStateTreeNode,
+} from './sql/thread_state';
 import {SqlTableTab} from './sql_table/tab';
 import {SqlTables} from './sql_table/well_known_tables';
 import {getProcessName, getThreadName} from './thread_and_process_info';
@@ -44,7 +48,8 @@
 
 // Renders a widget storing all of the generic details for a slice from the
 // slice table.
-export function renderDetails(slice: SliceDetails) {
+export function renderDetails(
+    slice: SliceDetails, durationBreakdown?: BreakdownByThreadState) {
   return m(
       Section,
       {title: 'Details'},
@@ -84,10 +89,18 @@
           }),
           exists(slice.absTime) &&
               m(TreeNode, {left: 'Absolute Time', right: slice.absTime}),
-          m(TreeNode, {
-            left: 'Duration',
-            right: computeDuration(slice.ts, slice.dur),
-          }),
+          m(
+              TreeNode,
+              {
+                left: 'Duration',
+                right: computeDuration(slice.ts, slice.dur),
+              },
+              exists(durationBreakdown) && slice.dur > 0 &&
+                  m(BreakdownByThreadStateTreeNode, {
+                    data: durationBreakdown,
+                    dur: slice.dur,
+                  }),
+              ),
           renderThreadDuration(slice),
           slice.thread && m(TreeNode, {
             left: 'Thread',
diff --git a/ui/src/frontend/sql/thread_state.ts b/ui/src/frontend/sql/thread_state.ts
new file mode 100644
index 0000000..2de948b
--- /dev/null
+++ b/ui/src/frontend/sql/thread_state.ts
@@ -0,0 +1,139 @@
+// Copyright (C) 2023 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.
+
+import m from 'mithril';
+
+import {Duration, duration, TimeSpan} from '../../base/time';
+import {LONG, NUM_NULL, STR, STR_NULL} from '../../common/query_result';
+import {EngineProxy} from '../../public';
+import {TreeNode} from '../../widgets/tree';
+import {Utid} from '../sql_types';
+
+// An individual node of the thread state breakdown tree.
+class Node {
+  parent?: Node;
+  children: Map<string, Node>;
+  dur: duration;
+  startsCollapsed: boolean = true;
+
+  constructor(parent?: Node) {
+    this.parent = parent;
+    this.children = new Map();
+    this.dur = 0n;
+  }
+
+  getOrCreateChild(name: string) {
+    let child = this.children.get(name);
+    if (!child) {
+      child = new Node(this);
+      this.children.set(name, child);
+    }
+    return child;
+  }
+
+  addDuration(dur: duration) {
+    let node: Node|undefined = this;
+    while (node !== undefined) {
+      node.dur += dur;
+      node = node.parent;
+    }
+  }
+}
+
+// Thread state breakdown data (tree).
+// Can be passed to ThreadStateBreakdownTreeNode to be rendered as a part of a
+// tree.
+export interface BreakdownByThreadState {
+  root: Node;
+}
+
+// Compute a breakdown of thread states for a given thread for a given time
+// interval.
+export async function breakDownIntervalByThreadState(
+    engine: EngineProxy, range: TimeSpan, utid: Utid):
+    Promise<BreakdownByThreadState> {
+  // TODO(altimin): this probably should share some code with pivot tables when
+  // we actually get some pivot tables we like.
+  const query = await engine.query(`
+    INCLUDE PERFETTO MODULE common.thread_states;
+
+    SELECT
+      state,
+      raw_state as rawState,
+      cpu_type as cpuType,
+      cpu,
+      blocked_function as blockedFunction,
+      dur
+    FROM thread_state_summary_for_interval(${range.start}, ${range.duration}, ${
+      utid});
+  `);
+  const it = query.iter({
+    state: STR,
+    rawState: STR,
+    cpuType: STR_NULL,
+    cpu: NUM_NULL,
+    blockedFunction: STR_NULL,
+    dur: LONG,
+  });
+  const root = new Node();
+  for (; it.valid(); it.next()) {
+    let currentNode = root;
+    currentNode = currentNode.getOrCreateChild(it.state);
+    // If the CPU time is not null, add it to the breakdown.
+    if (it.cpuType !== null) {
+      currentNode = currentNode.getOrCreateChild(it.cpuType);
+    }
+    if (it.cpu !== null) {
+      currentNode = currentNode.getOrCreateChild(`CPU ${it.cpu}`);
+    }
+    if (it.blockedFunction !== null) {
+      currentNode = currentNode.getOrCreateChild(`${it.blockedFunction}`);
+    }
+    currentNode.addDuration(it.dur);
+  }
+  return {
+    root,
+  };
+}
+
+function renderChildren(node: Node, totalDur: duration): m.Child[] {
+  const res = Array.from(node.children.entries())
+                  .map(([name, child]) => renderNode(child, name, totalDur));
+  return res;
+}
+
+function renderNode(node: Node, name: string, totalDur: duration): m.Child {
+  const durPercent = 100. * Number(node.dur) / Number(totalDur);
+  return m(
+      TreeNode,
+      {
+        left: name,
+        right: `${Duration.humanise(node.dur)} (${durPercent.toFixed(2)}%)`,
+        startsCollapsed: node.startsCollapsed,
+      },
+      renderChildren(node, totalDur));
+}
+
+interface BreakdownByThreadStateTreeNodeAttrs {
+  dur: duration;
+  data: BreakdownByThreadState;
+}
+
+// A tree node that displays a nested breakdown a time interval by thread state.
+export class BreakdownByThreadStateTreeNode implements
+    m.ClassComponent<BreakdownByThreadStateTreeNodeAttrs> {
+  view({attrs}: m.Vnode<BreakdownByThreadStateTreeNodeAttrs>): m.Child[] {
+    return renderChildren(attrs.data.root, attrs.dur);
+  }
+}
diff --git a/ui/src/tracks/debug/counter_track.ts b/ui/src/tracks/debug/counter_track.ts
index adcc92d..56e3d42 100644
--- a/ui/src/tracks/debug/counter_track.ts
+++ b/ui/src/tracks/debug/counter_track.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
 
 import {Actions, DEBUG_COUNTER_TRACK_KIND} from '../../common/actions';
 import {EngineProxy} from '../../common/engine';
@@ -103,14 +104,19 @@
       from data
       order by ts;`);
 
-  globals.dispatch(Actions.addTrack({
-    uri: DEBUG_COUNTER_TRACK_URI,
-    name: trackName.trim() || `Debug Track ${debugTrackId}`,
-    trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
-    trackGroup: SCROLLING_TRACK_GROUP,
-    initialState: {
-      sqlTableName,
-      columns,
-    },
-  }));
+  const trackInstanceId = uuidv4();
+  globals.dispatchMultiple([
+    Actions.addTrack({
+      id: trackInstanceId,
+      uri: DEBUG_COUNTER_TRACK_URI,
+      name: trackName.trim() || `Debug Track ${debugTrackId}`,
+      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
+      trackGroup: SCROLLING_TRACK_GROUP,
+      initialState: {
+        sqlTableName,
+        columns,
+      },
+    }),
+    Actions.toggleTrackPinned({trackId: trackInstanceId}),
+  ]);
 }
diff --git a/ui/src/tracks/debug/slice_track.ts b/ui/src/tracks/debug/slice_track.ts
index e05fdaf..6a1accb 100644
--- a/ui/src/tracks/debug/slice_track.ts
+++ b/ui/src/tracks/debug/slice_track.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
 
 import {Disposable} from '../../base/disposable';
 import {Actions, DEBUG_SLICE_TRACK_KIND} from '../../common/actions';
@@ -145,14 +146,19 @@
       from prepared_data
       order by ts;`);
 
-  globals.dispatch(Actions.addTrack({
-    uri: DEBUG_SLICE_TRACK_URI,
-    name: trackName.trim() || `Debug Track ${debugTrackId}`,
-    trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
-    trackGroup: SCROLLING_TRACK_GROUP,
-    initialState: {
-      sqlTableName,
-      columns: sliceColumns,
-    },
-  }));
+  const trackInstanceId = uuidv4();
+  globals.dispatchMultiple([
+    Actions.addTrack({
+      id: trackInstanceId,
+      uri: DEBUG_SLICE_TRACK_URI,
+      name: trackName.trim() || `Debug Track ${debugTrackId}`,
+      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
+      trackGroup: SCROLLING_TRACK_GROUP,
+      initialState: {
+        sqlTableName,
+        columns: sliceColumns,
+      },
+    }),
+    Actions.toggleTrackPinned({trackId: trackInstanceId}),
+  ]);
 }
diff --git a/ui/src/widgets/tree.ts b/ui/src/widgets/tree.ts
index e046a35..034bf7c 100644
--- a/ui/src/widgets/tree.ts
+++ b/ui/src/widgets/tree.ts
@@ -59,6 +59,8 @@
   // Whether this node is collapsed or not.
   // If omitted, collapsed state 'uncontrolled' - i.e. controlled internally.
   collapsed?: boolean;
+  // Whether the node should start collapsed or not, default: false.
+  startsCollapsed?: boolean;
   loading?: boolean;
   showCaret?: boolean;
   // Optional icon to show to the left of the text.
@@ -69,7 +71,12 @@
 }
 
 export class TreeNode implements m.ClassComponent<TreeNodeAttrs> {
-  private collapsed = false;
+  private collapsed;
+
+  constructor({attrs}: m.CVnode<TreeNodeAttrs>) {
+    this.collapsed = attrs.startsCollapsed ?? false;
+  }
+
   view(vnode: m.CVnode<TreeNodeAttrs>): m.Children {
     const {children, attrs, attrs: {left, onCollapseChanged = () => {}}} =
         vnode;