Merge "tp: tag the no_resolvers target as avoid_dep" into main
diff --git a/Android.bp b/Android.bp
index 79d28ce..05b6bfb 100644
--- a/Android.bp
+++ b/Android.bp
@@ -5151,6 +5151,7 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
@@ -5239,6 +5240,7 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
@@ -5310,6 +5312,7 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
@@ -11959,6 +11962,7 @@
         "src/trace_processor/metrics/sql/android/android_batt.sql",
         "src/trace_processor/metrics/sql/android/android_binder.sql",
         "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql",
+        "src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_boot.sql",
         "src/trace_processor/metrics/sql/android/android_boot_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_camera.sql",
@@ -12818,6 +12822,7 @@
         "src/trace_redaction/process_thread_timeline.cc",
         "src/trace_redaction/proto_util.cc",
         "src/trace_redaction/prune_package_list.cc",
+        "src/trace_redaction/redact_sched_switch.cc",
         "src/trace_redaction/scrub_ftrace_events.cc",
         "src/trace_redaction/scrub_process_trees.cc",
         "src/trace_redaction/scrub_task_rename.cc",
@@ -12836,6 +12841,7 @@
         "src/trace_redaction/process_thread_timeline_unittest.cc",
         "src/trace_redaction/proto_util_unittest.cc",
         "src/trace_redaction/prune_package_list_unittest.cc",
+        "src/trace_redaction/redact_sched_switch_unittest.cc",
         "src/trace_redaction/scrub_ftrace_events_unittest.cc",
         "src/trace_redaction/scrub_task_rename_unittest.cc",
         "src/trace_redaction/scrub_trace_packet_unittest.cc",
diff --git a/BUILD b/BUILD
index b8f0a29..6f1ab75 100644
--- a/BUILD
+++ b/BUILD
@@ -1950,6 +1950,7 @@
         "src/trace_processor/metrics/sql/android/android_batt.sql",
         "src/trace_processor/metrics/sql/android/android_binder.sql",
         "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql",
+        "src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_boot.sql",
         "src/trace_processor/metrics/sql/android/android_boot_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_camera.sql",
@@ -4372,6 +4373,7 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
diff --git a/gn/standalone/wasm.gni b/gn/standalone/wasm.gni
index 3b09531..eb6d4d4 100644
--- a/gn/standalone/wasm.gni
+++ b/gn/standalone/wasm.gni
@@ -48,6 +48,8 @@
       "-s",
       "INITIAL_MEMORY=33554432",
       "-s",
+      "MAXIMUM_MEMORY=4GB",
+      "-s",
       "ALLOW_MEMORY_GROWTH=1",
       "-s",
       "ALLOW_TABLE_GROWTH=1",
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index 33f4195..5b8fab6 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -23,6 +23,7 @@
     "ad_services_metric.proto",
     "android_blocking_call.proto",
     "android_blocking_calls_cuj_metric.proto",
+    "android_blocking_calls_unagg.proto",
     "android_boot.proto",
     "android_boot_unagg.proto",
     "android_frame_timeline_metric.proto",
diff --git a/protos/perfetto/metrics/android/android_blocking_calls_unagg.proto b/protos/perfetto/metrics/android/android_blocking_calls_unagg.proto
new file mode 100644
index 0000000..96b73ba
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_blocking_calls_unagg.proto
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 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
+ *
+ *      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.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+import "protos/perfetto/metrics/android/android_blocking_call.proto";
+import "protos/perfetto/metrics/android/process_metadata.proto";
+
+// All blocking calls for a trace. Shows count and total duration for each.
+message AndroidBlockingCallsUnagg {
+
+    repeated ProcessWithBlockingCalls process_with_blocking_calls = 1;
+
+    message ProcessWithBlockingCalls {
+        // Details about the process (uid, version, etc)
+        optional AndroidProcessMetadata process = 1;
+
+        // List of blocking calls on the process main thread.
+        repeated AndroidBlockingCall blocking_calls = 2;
+    }
+}
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index f06f085..83395ec 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -29,6 +29,7 @@
 import "protos/perfetto/metrics/android/batt_metric.proto";
 import "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto";
 import "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto";
+import "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto";
 import "protos/perfetto/metrics/android/codec_metrics.proto";
 import "protos/perfetto/metrics/android/cpu_metric.proto";
 import "protos/perfetto/metrics/android/camera_metric.proto";
@@ -116,7 +117,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 64
+// Next id: 66
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -294,6 +295,9 @@
   // Specific for Android Auto
   optional AndroidMultiuserMetric android_auto_multiuser = 64;
 
+  // All blocking calls (e.g. binder calls) for a trace.
+  optional AndroidBlockingCallsUnagg android_blocking_calls_unagg = 65;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index fbd42fd..c2e2263 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -145,6 +145,24 @@
 
 // End of protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto
 
+// Begin of protos/perfetto/metrics/android/android_blocking_calls_unagg.proto
+
+// All blocking calls for a trace. Shows count and total duration for each.
+message AndroidBlockingCallsUnagg {
+
+    repeated ProcessWithBlockingCalls process_with_blocking_calls = 1;
+
+    message ProcessWithBlockingCalls {
+        // Details about the process (uid, version, etc)
+        optional AndroidProcessMetadata process = 1;
+
+        // List of blocking calls on the process main thread.
+        repeated AndroidBlockingCall blocking_calls = 2;
+    }
+}
+
+// End of protos/perfetto/metrics/android/android_blocking_calls_unagg.proto
+
 // Begin of protos/perfetto/metrics/android/android_boot.proto
 
 // This metric computes how much time processes spend in UNINTERRUPTIBLE_SLEEP
@@ -2520,7 +2538,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 64
+// Next id: 66
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -2698,6 +2716,9 @@
   // Specific for Android Auto
   optional AndroidMultiuserMetric android_auto_multiuser = 64;
 
+  // All blocking calls (e.g. binder calls) for a trace.
+  optional AndroidBlockingCallsUnagg android_blocking_calls_unagg = 65;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 6b4b352..a5fe326 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index 6e1e1bb..bc832bf 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -68,6 +68,7 @@
 #include "protos/perfetto/trace/ftrace/mm_event.pbzero.h"
 #include "protos/perfetto/trace/ftrace/net.pbzero.h"
 #include "protos/perfetto/trace/ftrace/oom.pbzero.h"
+#include "protos/perfetto/trace/ftrace/panel.pbzero.h"
 #include "protos/perfetto/trace/ftrace/power.pbzero.h"
 #include "protos/perfetto/trace/ftrace/raw_syscalls.pbzero.h"
 #include "protos/perfetto/trace/ftrace/rpm.pbzero.h"
@@ -1143,6 +1144,10 @@
         ParseRpmStatus(ts, fld_bytes);
         break;
       }
+      case FtraceEvent::kPanelWriteGenericFieldNumber: {
+        ParsePanelWriteGeneric(ts, pid, fld_bytes);
+        break;
+      }
       default:
         break;
     }
@@ -3329,6 +3334,22 @@
   devices_with_active_rpm_slice_.insert(device_name);
 }
 
+void FtraceParser::ParsePanelWriteGeneric(int64_t timestamp,
+                                          uint32_t pid,
+                                          ConstBytes blob) {
+  protos::pbzero::PanelWriteGenericFtraceEvent::Decoder evt(blob.data,
+                                                            blob.size);
+  if (!evt.type()) {
+    context_->storage->IncrementStats(stats::systrace_parse_failure);
+    return;
+  }
+
+  uint32_t tgid = static_cast<uint32_t>(evt.pid());
+  SystraceParser::GetOrCreate(context_)->ParseKernelTracingMarkWrite(
+      timestamp, pid, static_cast<char>(evt.type()), false /*trace_begin*/,
+      evt.name(), tgid, evt.value());
+}
+
 StringId FtraceParser::InternedKernelSymbolOrFallback(
     uint64_t key,
     PacketSequenceStateGeneration* seq_state) {
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index a3f8fc6..402ad73 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -296,6 +296,9 @@
                                    protozero::ConstBytes);
   StringId GetRpmStatusStringId(int32_t rpm_status_val);
   void ParseRpmStatus(int64_t ts, protozero::ConstBytes);
+  void ParsePanelWriteGeneric(int64_t timestamp,
+                              uint32_t pid,
+                              protozero::ConstBytes);
 
   TraceProcessorContext* context_;
   RssStatTracker rss_stat_tracker_;
diff --git a/src/trace_processor/importers/systrace/systrace_parser.cc b/src/trace_processor/importers/systrace/systrace_parser.cc
index b06bb71..e00322b 100644
--- a/src/trace_processor/importers/systrace/systrace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_parser.cc
@@ -98,11 +98,12 @@
   point.int_value = value;
   point.tgid = tgid;
 
-  // Some versions of this trace point fill trace_type with one of (B/E/C),
+  // Some versions of this trace point fill trace_type with one of (B/E/C/I),
   // others use the trace_begin boolean and only support begin/end events:
   if (trace_type == 0) {
     point.phase = trace_begin ? 'B' : 'E';
-  } else if (trace_type == 'B' || trace_type == 'E' || trace_type == 'C') {
+  } else if (trace_type == 'B' || trace_type == 'E' || trace_type == 'C' ||
+             trace_type == 'I') {
     point.phase = trace_type;
   } else {
     context_->storage->IncrementStats(stats::systrace_parse_failure);
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index f87b560..bd77d0a 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -25,6 +25,7 @@
     "android_batt.sql",
     "android_binder.sql",
     "android_blocking_calls_cuj_metric.sql",
+    "android_blocking_calls_unagg.sql",
     "android_boot.sql",
     "android_boot_unagg.sql",
     "android_camera.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql b/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql
index eaa2104..5838125 100644
--- a/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql
+++ b/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql
@@ -21,6 +21,7 @@
 
 INCLUDE PERFETTO MODULE android.slices;
 INCLUDE PERFETTO MODULE android.binder;
+INCLUDE PERFETTO MODULE android.critical_blocking_calls;
 
 -- Jank "J<*>" and latency "L<*>" cujs are put together in android_cujs table.
 -- They are computed separately as latency ones are slightly different, don't
@@ -75,99 +76,22 @@
 SELECT ROW_NUMBER() OVER (ORDER BY ts) AS cuj_id, *
 FROM all_cujs;
 
-DROP TABLE IF EXISTS relevant_binder_calls_with_names;
-CREATE TABLE relevant_binder_calls_with_names AS
-SELECT DISTINCT
-    tx.aidl_name AS name,
-    tx.client_ts AS ts,
-    s.track_id,
-    tx.client_dur AS dur,
-    s.id,
-    tx.client_process as process_name,
-    tx.client_utid as utid,
-    tx.client_upid as upid
-FROM android_binder_txns AS tx
-         JOIN slice AS s ON s.id = tx.binder_txn_id
-WHERE is_main_thread AND aidl_name IS NOT NULL AND is_sync = 1;
-
-DROP TABLE IF EXISTS android_blocking_calls_cuj_calls;
-CREATE TABLE android_blocking_calls_cuj_calls AS
-WITH all_main_thread_relevant_slices AS (
-    SELECT DISTINCT
-        android_standardize_slice_name(s.name) AS name,
-        s.ts,
-        s.track_id,
-        s.dur,
-        s.id,
-        process.name AS process_name,
-        thread.utid,
-        process.upid
-    FROM slice s
-        JOIN thread_track ON s.track_id = thread_track.id
-        JOIN thread USING (utid)
-        JOIN process USING (upid)
-    WHERE
-        thread.is_main_thread AND (
-               s.name = 'measure'
-            OR s.name = 'layout'
-            OR s.name = 'configChanged'
-            OR s.name = 'animation'
-            OR s.name = 'input'
-            OR s.name = 'traversal'
-            OR s.name = 'Contending for pthread mutex'
-            OR s.name = 'postAndWait'
-            OR s.name GLOB 'monitor contention with*'
-            OR s.name GLOB 'SuspendThreadByThreadId*'
-            OR s.name GLOB 'LoadApkAssetsFd*'
-            OR s.name GLOB '*binder transaction*'
-            OR s.name GLOB 'inflate*'
-            OR s.name GLOB 'Lock contention on*'
-            OR s.name GLOB 'android.os.Handler: kotlinx.coroutines*'
-            OR s.name GLOB 'relayoutWindow*'
-            OR s.name GLOB 'ImageDecoder#decode*'
-            OR s.name GLOB 'NotificationStackScrollLayout#onMeasure'
-            OR s.name GLOB 'ExpNotRow#*'
-            OR s.name GLOB 'GC: Wait For*'
-            OR (
-                -- Some top level handler slices
-                    s.depth = 0
-                AND s.name NOT GLOB '*Choreographer*'
-                AND s.name NOT GLOB '*Input*'
-                AND s.name NOT GLOB '*input*'
-                AND s.name NOT GLOB 'android.os.Handler: #*'
-                AND (
-                   -- Handler pattern heuristics
-                      s.name GLOB '*Handler: *$*'
-                   OR s.name GLOB '*.*.*: *$*'
-                   OR s.name GLOB '*.*$*: #*'
-                )
-            )
-        )
-    UNION ALL
-    SELECT
-        name,
-        ts,
-        track_id,
-        dur,
-        id,
-        process_name,
-        utid,
-        upid
-    FROM relevant_binder_calls_with_names
-),
--- Now we have:
---  (1) a list of slices from the main thread of each process
+-- We have:
+--  (1) a list of slices from the main thread of each process from the
+--  all_main_thread_relevant_slices table.
 --  (2) a list of android cuj with beginning, end, and process
 -- It's needed to:
 --  (1) assign a cuj to each slice. If there are multiple cujs going on during a
 --      slice, there needs to be 2 entries for that slice, one for each cuj id.
 --  (2) each slice needs to be trimmed to be fully inside the cuj associated
 --      (as we don't care about what's outside cujs)
+DROP TABLE IF EXISTS android_blocking_calls_cuj_calls;
+CREATE TABLE android_blocking_calls_cuj_calls AS
+WITH
 main_thread_slices_scoped_to_cujs AS (
 SELECT
     s.id,
     s.id AS slice_id,
-    s.track_id,
     s.name,
     max(s.ts, cuj.ts) AS ts,
     min(s.ts + s.dur, cuj.ts_end) as ts_end,
@@ -177,7 +101,7 @@
     s.process_name,
     s.upid,
     s.utid
-FROM all_main_thread_relevant_slices s
+FROM _android_critical_blocking_calls s
     JOIN  android_cujs cuj
     -- only when there is an overlap
     ON s.ts + s.dur > cuj.ts AND s.ts < cuj.ts_end
diff --git a/src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql b/src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql
new file mode 100644
index 0000000..2008c0b
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql
@@ -0,0 +1,96 @@
+--
+-- 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.
+
+SELECT RUN_METRIC('android/process_metadata.sql');
+
+INCLUDE PERFETTO MODULE android.slices;
+INCLUDE PERFETTO MODULE android.binder;
+INCLUDE PERFETTO MODULE android.critical_blocking_calls;
+
+DROP TABLE IF EXISTS process_info;
+CREATE TABLE process_info AS
+SELECT
+  process.upid AS upid,
+  process.name AS process_name,
+  process_metadata.metadata AS process_metadata
+FROM process
+JOIN process_metadata USING (upid);
+
+DROP TABLE IF EXISTS android_blocking_calls_unagg_calls;
+CREATE TABLE android_blocking_calls_unagg_calls AS
+SELECT
+  name,
+  COUNT(*) AS occurrences,
+  MAX(dur) AS max_dur_ns,
+  MIN(dur) AS min_dur_ns,
+  SUM(dur) AS total_dur_ns,
+  upid,
+  process_name
+FROM
+  _android_critical_blocking_calls
+GROUP BY name, upid, process_name;
+
+DROP TABLE IF EXISTS filtered_processes_with_non_zero_blocking_calls;
+CREATE TABLE filtered_processes_with_non_zero_blocking_calls AS
+SELECT pi.upid,
+  pi.process_name,
+  pi.process_metadata
+FROM process_info pi WHERE pi.upid IN
+  (SELECT DISTINCT upid FROM _android_critical_blocking_calls);
+
+
+DROP TABLE IF EXISTS filtered_processes_with_non_zero_blocking_calls;
+CREATE TABLE filtered_processes_with_non_zero_blocking_calls AS
+SELECT pi.upid,
+  pi.process_name,
+  pi.process_metadata
+FROM process_info pi WHERE pi.upid IN
+  (SELECT DISTINCT upid FROM _android_critical_blocking_calls);
+
+DROP VIEW IF EXISTS android_blocking_calls_unagg_output;
+CREATE PERFETTO VIEW android_blocking_calls_unagg_output AS
+SELECT AndroidBlockingCallsUnagg(
+  'process_with_blocking_calls', (
+     SELECT RepeatedField(
+       AndroidBlockingCallsUnagg_ProcessWithBlockingCalls(
+         'process', e.process_metadata,
+         'blocking_calls', (
+            SELECT RepeatedField(
+              AndroidBlockingCall(
+                'name', d.name,
+                'cnt', d.occurrences,
+                'total_dur_ms', CAST(total_dur_ns / 1e6 AS INT),
+                'max_dur_ms', CAST(max_dur_ns / 1e6 AS INT),
+                'min_dur_ms', CAST(min_dur_ns / 1e6 AS INT),
+                'total_dur_ns', d.total_dur_ns,
+                'max_dur_ns', d.max_dur_ns,
+                'min_dur_ns', d.min_dur_ns
+              )
+            ) FROM (
+            SELECT b.name,
+              b.occurrences,
+              b.total_dur_ns,
+              b.max_dur_ns,
+              b.min_dur_ns
+            FROM android_blocking_calls_unagg_calls b INNER JOIN filtered_processes_with_non_zero_blocking_calls c
+            ON b.upid = c.upid WHERE b.upid = e.upid
+            ORDER BY total_dur_ns DESC
+            ) d
+         )
+       )
+     )
+     FROM filtered_processes_with_non_zero_blocking_calls e
+  )
+);
diff --git a/src/trace_processor/metrics/sql/android/android_startup.sql b/src/trace_processor/metrics/sql/android/android_startup.sql
index 18f9fbf..4564f64 100644
--- a/src/trace_processor/metrics/sql/android/android_startup.sql
+++ b/src/trace_processor/metrics/sql/android/android_startup.sql
@@ -504,16 +504,6 @@
   ) AS startup
 FROM android_startups launches;
 
-DROP VIEW IF EXISTS android_startup_event;
-CREATE PERFETTO VIEW android_startup_event AS
-SELECT
-  'slice' AS track_type,
-  'Android App Startups' AS track_name,
-  l.ts AS ts,
-  l.dur AS dur,
-  l.package AS slice_name
-FROM android_startups l;
-
 DROP VIEW IF EXISTS android_startup_output;
 CREATE PERFETTO VIEW android_startup_output AS
 SELECT
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
index 383ed17..59e571f 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
@@ -38,6 +38,7 @@
 #include "protos/perfetto/trace/ftrace/g2d.pbzero.h"
 #include "protos/perfetto/trace/ftrace/irq.pbzero.h"
 #include "protos/perfetto/trace/ftrace/mdss.pbzero.h"
+#include "protos/perfetto/trace/ftrace/panel.pbzero.h"
 #include "protos/perfetto/trace/ftrace/power.pbzero.h"
 #include "protos/perfetto/trace/ftrace/samsung.pbzero.h"
 #include "protos/perfetto/trace/ftrace/sched.pbzero.h"
@@ -459,6 +460,19 @@
     writer_->AppendString("|");
     WriteValueForField(TMW::kValueFieldNumber, DVW());
     return;
+  } else if (event_name_ == "panel_write_generic") {
+    using TMW = protos::pbzero::PanelWriteGenericFtraceEvent;
+    WriteValueForField(TMW::kTypeFieldNumber, [this](const Variadic& value) {
+      PERFETTO_DCHECK(value.type == Variadic::Type::kUint);
+      writer_->AppendChar(static_cast<char>(value.uint_value));
+    });
+    writer_->AppendString("|");
+    WriteValueForField(TMW::kPidFieldNumber, DVW());
+    writer_->AppendString("|");
+    WriteValueForField(TMW::kNameFieldNumber, DVW());
+    writer_->AppendString("|");
+    WriteValueForField(TMW::kValueFieldNumber, DVW());
+    return;
   } else if (event_name_ == "g2d_tracing_mark_write") {
     using TMW = protos::pbzero::G2dTracingMarkWriteFtraceEvent;
     WriteValueForField(TMW::kTypeFieldNumber, [this](const Variadic& value) {
diff --git a/src/trace_redaction/BUILD.gn b/src/trace_redaction/BUILD.gn
index 3630898..ed04178 100644
--- a/src/trace_redaction/BUILD.gn
+++ b/src/trace_redaction/BUILD.gn
@@ -42,6 +42,8 @@
     "proto_util.h",
     "prune_package_list.cc",
     "prune_package_list.h",
+    "redact_sched_switch.cc",
+    "redact_sched_switch.h",
     "scrub_ftrace_events.cc",
     "scrub_ftrace_events.h",
     "scrub_process_trees.cc",
@@ -72,6 +74,7 @@
 source_set("integrationtests") {
   testonly = true
   sources = [
+    "redact_sched_switch_integrationtest.cc",
     "scrub_ftrace_events_integrationtest.cc",
     "scrub_process_trees_integrationtest.cc",
     "scrub_task_rename_integrationtest.cc",
@@ -98,6 +101,7 @@
     "process_thread_timeline_unittest.cc",
     "proto_util_unittest.cc",
     "prune_package_list_unittest.cc",
+    "redact_sched_switch_unittest.cc",
     "scrub_ftrace_events_unittest.cc",
     "scrub_task_rename_unittest.cc",
     "scrub_trace_packet_unittest.cc",
diff --git a/src/trace_redaction/main.cc b/src/trace_redaction/main.cc
index e8cea13..0b9aafe 100644
--- a/src/trace_redaction/main.cc
+++ b/src/trace_redaction/main.cc
@@ -21,6 +21,7 @@
 #include "src/trace_redaction/optimize_timeline.h"
 #include "src/trace_redaction/populate_allow_lists.h"
 #include "src/trace_redaction/prune_package_list.h"
+#include "src/trace_redaction/redact_sched_switch.h"
 #include "src/trace_redaction/scrub_ftrace_events.h"
 #include "src/trace_redaction/scrub_process_trees.h"
 #include "src/trace_redaction/scrub_task_rename.h"
@@ -50,6 +51,7 @@
   redactor.transformers()->emplace_back(new ScrubFtraceEvents());
   redactor.transformers()->emplace_back(new ScrubProcessTrees());
   redactor.transformers()->emplace_back(new ScrubTaskRename());
+  redactor.transformers()->emplace_back(new RedactSchedSwitch());
 
   Context context;
   context.package_name = package_name;
diff --git a/src/trace_redaction/redact_sched_switch.cc b/src/trace_redaction/redact_sched_switch.cc
new file mode 100644
index 0000000..b70ef7a
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch.cc
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 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
+ *
+ *      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.
+ */
+
+#include "src/trace_redaction/redact_sched_switch.h"
+
+#include <string>
+
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+#include "src/trace_redaction/proto_util.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+
+// Redact sched switch trace events in an ftrace event bundle:
+//
+//  event {
+//    timestamp: 6702093744772646
+//    pid: 0
+//    sched_switch {
+//      prev_comm: "swapper/0"
+//      prev_pid: 0
+//      prev_prio: 120
+//      prev_state: 0
+//      next_comm: "writer"
+//      next_pid: 23020
+//      next_prio: 96
+//    }
+//  }
+//
+// In the above message, it should be noted that "event.pid" will always be
+// equal to "event.sched_switch.prev_pid".
+//
+// "ftrace_event_bundle_message" is the ftrace event bundle (contains a
+// collection of ftrace event messages) because data in a sched_switch message
+// is needed in order to know if the event should be added to the bundle.
+void RedactSwitchEvent(
+    const Context& context,
+    protos::pbzero::FtraceEvent::Decoder event,
+    protos::pbzero::FtraceEventBundle* ftrace_event_bundle_message) {
+  PERFETTO_DCHECK(context.timeline);
+  PERFETTO_DCHECK(context.package_uid.has_value());
+  PERFETTO_DCHECK(event.has_sched_switch());
+  PERFETTO_DCHECK(ftrace_event_bundle_message);
+
+  // If there is no timestamp in the event, it is not possible to query the
+  // timeline. This is too risky to keep.
+  if (!event.has_timestamp()) {
+    return;
+  }
+
+  protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_switch(
+      event.sched_switch());
+
+  // There must be a prev pid and a next pid. Otherwise, the event is invalid.
+  // Dropping the event is the safest option.
+  if (!sched_switch.has_prev_pid() || !sched_switch.has_next_pid()) {
+    return;
+  }
+
+  auto uid = context.package_uid.value();
+
+  auto prev_slice =
+      context.timeline->Search(event.timestamp(), sched_switch.prev_pid());
+  auto next_slice =
+      context.timeline->Search(event.timestamp(), sched_switch.next_pid());
+
+  // Build a new event, clearing the comm values when needed.
+  auto* event_message = ftrace_event_bundle_message->add_event();
+
+  // Reset to scan event fields.
+  event.Reset();
+
+  for (auto event_field = event.ReadField(); event_field.valid();
+       event_field = event.ReadField()) {
+    // This primitive only needs to affect sched switch events.
+    if (event_field.id() !=
+        protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber) {
+      proto_util::AppendField(event_field, event_message);
+      continue;
+    }
+
+    // Reset to scan sched_switch fields.
+    sched_switch.Reset();
+
+    auto switch_message = event_message->set_sched_switch();
+
+    for (auto switch_field = sched_switch.ReadField(); switch_field.valid();
+         switch_field = sched_switch.ReadField()) {
+      switch (switch_field.id()) {
+        case protos::pbzero::SchedSwitchFtraceEvent::kPrevCommFieldNumber:
+          if (prev_slice.uid == uid) {
+            proto_util::AppendField(switch_field, switch_message);
+          }
+          break;
+
+        case protos::pbzero::SchedSwitchFtraceEvent::kNextCommFieldNumber: {
+          if (next_slice.uid == uid) {
+            proto_util::AppendField(switch_field, switch_message);
+          }
+          break;
+        }
+
+        default:
+          proto_util::AppendField(switch_field, switch_message);
+          break;
+      }
+    }
+  }
+}
+
+}  // namespace
+
+base::Status RedactSchedSwitch::Transform(const Context& context,
+                                          std::string* packet) const {
+  if (packet == nullptr || packet->empty()) {
+    return base::ErrStatus("RedactSchedSwitch: null or empty packet.");
+  }
+
+  if (!context.package_uid.has_value()) {
+    return base::ErrStatus("RedactSchedSwitch: missing packet uid.");
+  }
+
+  if (!context.timeline) {
+    return base::ErrStatus("RedactSchedSwitch: missing timeline.");
+  }
+
+  protozero::ProtoDecoder packet_decoder(*packet);
+
+  auto trace_event_bundle = packet_decoder.FindField(
+      protos::pbzero::TracePacket::kFtraceEventsFieldNumber);
+
+  if (!trace_event_bundle.valid()) {
+    return base::OkStatus();
+  }
+
+  protozero::HeapBuffered<protos::pbzero::TracePacket> packet_message;
+
+  for (auto packet_field = packet_decoder.ReadField(); packet_field.valid();
+       packet_field = packet_decoder.ReadField()) {
+    if (packet_field.id() !=
+        protos::pbzero::TracePacket::kFtraceEventsFieldNumber) {
+      proto_util::AppendField(packet_field, packet_message.get());
+      continue;
+    }
+
+    protozero::ProtoDecoder bundle(packet_field.as_bytes());
+
+    auto* bundle_message = packet_message->set_ftrace_events();
+
+    for (auto field = bundle.ReadField(); field.valid();
+         field = bundle.ReadField()) {
+      if (field.id() != protos::pbzero::FtraceEventBundle::kEventFieldNumber) {
+        proto_util::AppendField(field, bundle_message);
+        continue;
+      }
+
+      protos::pbzero::FtraceEvent::Decoder ftrace_event(field.as_bytes());
+
+      if (ftrace_event.has_sched_switch()) {
+        RedactSwitchEvent(context, std::move(ftrace_event), bundle_message);
+      } else {
+        proto_util::AppendField(field, bundle_message);
+      }
+    }
+  }
+
+  *packet = packet_message.SerializeAsString();
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_switch.h b/src/trace_redaction/redact_sched_switch.h
new file mode 100644
index 0000000..e33527d
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 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
+ *
+ *      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.
+ */
+
+#ifndef SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
+#define SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
+
+#include <string>
+
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+//  Assumptions:
+//    1. This is a hot path (a lot of ftrace packets)
+//    2. Allocations are slower than CPU cycles.
+//
+// Redact sched switch is called "redact" and not "prune" or "scrub" because it
+// is not removing sched switch events, but rather removing information from
+// within the event.
+class RedactSchedSwitch final : public TransformPrimitive {
+ public:
+  base::Status Transform(const Context& context,
+                         std::string* packet) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
diff --git a/src/trace_redaction/redact_sched_switch_integrationtest.cc b/src/trace_redaction/redact_sched_switch_integrationtest.cc
new file mode 100644
index 0000000..7cb516b
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch_integrationtest.cc
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 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
+ *
+ *      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.
+ */
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/base/test/status_matchers.h"
+#include "src/base/test/tmp_dir_tree.h"
+#include "src/base/test/utils.h"
+#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/find_package_uid.h"
+#include "src/trace_redaction/optimize_timeline.h"
+#include "src/trace_redaction/redact_sched_switch.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redactor.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+namespace {
+
+constexpr std::string_view kTracePath =
+    "test/data/trace-redaction-general.pftrace";
+constexpr std::string_view kPackageName =
+    "com.Unity.com.unity.multiplayer.samples.coop";
+
+class RedactSchedSwitchIntegrationTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    redactor_.collectors()->emplace_back(new FindPackageUid());
+    redactor_.collectors()->emplace_back(new BuildTimeline());
+    redactor_.builders()->emplace_back(new OptimizeTimeline());
+    redactor_.transformers()->emplace_back(new RedactSchedSwitch());
+
+    context_.package_name = kPackageName;
+
+    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
+
+    dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
+    tmp_dir_.TrackFile("dst.pftrace");
+  }
+
+  base::Status Redact() {
+    return redactor_.Redact(src_trace_, dest_trace_, &context_);
+  }
+
+  base::StatusOr<std::string> LoadOriginal() const {
+    return ReadRawTrace(src_trace_);
+  }
+
+  base::StatusOr<std::string> LoadRedacted() const {
+    return ReadRawTrace(dest_trace_);
+  }
+
+ private:
+  base::StatusOr<std::string> ReadRawTrace(const std::string& path) const {
+    std::string redacted_buffer;
+
+    if (base::ReadFile(path, &redacted_buffer)) {
+      return redacted_buffer;
+    }
+
+    return base::ErrStatus("Failed to read %s", path.c_str());
+  }
+
+  Context context_;
+  TraceRedactor redactor_;
+
+  base::TmpDirTree tmp_dir_;
+
+  std::string src_trace_;
+  std::string dest_trace_;
+};
+
+// >>> SELECT uid
+// >>>   FROM package_list
+// >>>   WHERE package_name='com.Unity.com.unity.multiplayer.samples.coop'
+//
+//     +-------+
+//     |  uid  |
+//     +-------+
+//     | 10252 |
+//     +-------+
+//
+// >>> SELECT uid, upid, name
+// >>>   FROM process
+// >>>   WHERE uid=10252
+//
+//     +-------+------+----------------------------------------------+
+//     |  uid  | upid | name                                         |
+//     +-------+------+----------------------------------------------+
+//     | 10252 | 843  | com.Unity.com.unity.multiplayer.samples.coop |
+//     +-------+------+----------------------------------------------+
+//
+// >>> SELECT tid, name
+// >>>   FROM thread
+// >>>   WHERE upid=843 AND name IS NOT NULL
+//
+//     +------+-----------------+
+//     | tid  | name            |
+//     +------+-----------------+
+//     | 7120 | Binder:7105_2   |
+//     | 7127 | UnityMain       |
+//     | 7142 | Job.worker 0    |
+//     | 7143 | Job.worker 1    |
+//     | 7144 | Job.worker 2    |
+//     | 7145 | Job.worker 3    |
+//     | 7146 | Job.worker 4    |
+//     | 7147 | Job.worker 5    |
+//     | 7148 | Job.worker 6    |
+//     | 7150 | Background Job. |
+//     | 7151 | Background Job. |
+//     | 7167 | UnityGfxDeviceW |
+//     | 7172 | AudioTrack      |
+//     | 7174 | FMOD stream thr |
+//     | 7180 | Binder:7105_3   |
+//     | 7184 | UnityChoreograp |
+//     | 7945 | Filter0         |
+//     | 7946 | Filter1         |
+//     | 7947 | Thread-7        |
+//     | 7948 | FMOD mixer thre |
+//     | 7950 | UnityGfxDeviceW |
+//     | 7969 | UnityGfxDeviceW |
+//     +------+-----------------+
+
+TEST_F(RedactSchedSwitchIntegrationTest, ClearsNonTargetSwitchComms) {
+  auto result = Redact();
+  ASSERT_OK(result) << result.c_message();
+
+  auto original = LoadOriginal();
+  ASSERT_OK(original) << original.status().c_message();
+
+  auto redacted = LoadRedacted();
+  ASSERT_OK(redacted) << redacted.status().c_message();
+
+  base::FlatHashMap<int32_t, std::string> expected_names;
+  expected_names.Insert(7120, "Binder:7105_2");
+  expected_names.Insert(7127, "UnityMain");
+  expected_names.Insert(7142, "Job.worker 0");
+  expected_names.Insert(7143, "Job.worker 1");
+  expected_names.Insert(7144, "Job.worker 2");
+  expected_names.Insert(7145, "Job.worker 3");
+  expected_names.Insert(7146, "Job.worker 4");
+  expected_names.Insert(7147, "Job.worker 5");
+  expected_names.Insert(7148, "Job.worker 6");
+  expected_names.Insert(7150, "Background Job.");
+  expected_names.Insert(7151, "Background Job.");
+  expected_names.Insert(7167, "UnityGfxDeviceW");
+  expected_names.Insert(7172, "AudioTrack");
+  expected_names.Insert(7174, "FMOD stream thr");
+  expected_names.Insert(7180, "Binder:7105_3");
+  expected_names.Insert(7184, "UnityChoreograp");
+  expected_names.Insert(7945, "Filter0");
+  expected_names.Insert(7946, "Filter1");
+  expected_names.Insert(7947, "Thread-7");
+  expected_names.Insert(7948, "FMOD mixer thre");
+  expected_names.Insert(7950, "UnityGfxDeviceW");
+  expected_names.Insert(7969, "UnityGfxDeviceW");
+
+  auto redacted_trace_data = LoadRedacted();
+  ASSERT_OK(redacted_trace_data) << redacted.status().c_message();
+
+  protos::pbzero::Trace::Decoder decoder(redacted_trace_data.value());
+
+  for (auto packet = decoder.packet(); packet; ++packet) {
+    protos::pbzero::TracePacket::Decoder packet_decoder(*packet);
+
+    if (!packet_decoder.has_ftrace_events()) {
+      continue;
+    }
+
+    protos::pbzero::FtraceEventBundle::Decoder ftrace_events_decoder(
+        packet_decoder.ftrace_events());
+
+    for (auto event = ftrace_events_decoder.event(); event; ++event) {
+      protos::pbzero::FtraceEvent::Decoder event_decoder(*event);
+
+      if (!event_decoder.has_sched_switch()) {
+        continue;
+      }
+
+      protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_decoder(
+          event_decoder.sched_switch());
+
+      ASSERT_TRUE(sched_decoder.has_next_pid());
+      ASSERT_TRUE(sched_decoder.has_prev_pid());
+
+      auto next_pid = sched_decoder.next_pid();
+      auto prev_pid = sched_decoder.prev_pid();
+
+      // If the pid is expected, make sure it has the right now. If it is not
+      // expected, it should be missing.
+      const auto* next_comm = expected_names.Find(next_pid);
+      const auto* prev_comm = expected_names.Find(prev_pid);
+
+      if (next_comm) {
+        EXPECT_TRUE(sched_decoder.has_next_comm());
+        EXPECT_EQ(sched_decoder.next_comm().ToStdString(), *next_comm);
+      } else {
+        EXPECT_FALSE(sched_decoder.has_next_comm());
+      }
+
+      if (prev_comm) {
+        EXPECT_TRUE(sched_decoder.has_prev_comm());
+        EXPECT_EQ(sched_decoder.prev_comm().ToStdString(), *prev_comm);
+      } else {
+        EXPECT_FALSE(sched_decoder.has_prev_comm());
+      }
+    }
+  }
+}
+
+}  // namespace
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_switch_unittest.cc b/src/trace_redaction/redact_sched_switch_unittest.cc
new file mode 100644
index 0000000..5f74078
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch_unittest.cc
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 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
+ *
+ *      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.
+ */
+
+#include "src/trace_redaction/redact_sched_switch.h"
+#include "protos/perfetto/trace/ftrace/power.gen.h"
+#include "protos/perfetto/trace/ftrace/sched.gen.h"
+#include "protos/perfetto/trace/trace.gen.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.gen.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+constexpr uint64_t kUidA = 1;
+constexpr uint64_t kUidB = 2;
+constexpr uint64_t kUidC = 3;
+
+constexpr int32_t kNoParent = 10;
+constexpr int32_t kPidA = 11;
+constexpr int32_t kPidB = 12;
+constexpr int32_t kPidC = 13;
+
+constexpr std::string_view kCommA = "comm-a";
+constexpr std::string_view kCommB = "comm-b";
+constexpr std::string_view kCommC = "comm-c";
+
+constexpr uint64_t kTimeA = 100;
+constexpr uint64_t kTimeB = 200;
+constexpr uint64_t kTimeC = 300;
+
+}  // namespace
+
+// Tests which nested messages and fields are removed.
+class RedactSchedSwitchTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    context_.timeline = std::make_unique<ProcessThreadTimeline>();
+
+    // Three concurrent processes. No parent. All in different packages.
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(0, kPidA, kNoParent, kUidA));
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(0, kPidB, kNoParent, kUidB));
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(0, kPidC, kNoParent, kUidC));
+
+    context_.timeline->Sort();
+  }
+
+  void BeginBundle() { ftrace_bundle_ = trace_packet_.mutable_ftrace_events(); }
+
+  void AddSwitch(uint64_t ts,
+                 int32_t prev_pid,
+                 std::string_view prev_comm,
+                 int32_t next_pid,
+                 std::string_view next_comm) {
+    ASSERT_NE(ftrace_bundle_, nullptr);
+
+    auto* event = ftrace_bundle_->add_event();
+    event->set_timestamp(ts);
+
+    auto* sched_switch = event->mutable_sched_switch();
+    sched_switch->set_prev_pid(prev_pid);
+    sched_switch->set_prev_comm(std::string(prev_comm));
+    sched_switch->set_next_pid(next_pid);
+    sched_switch->set_next_comm(std::string(next_comm));
+  }
+
+  base::StatusOr<protos::gen::TracePacket> Transform() {
+    auto packet = trace_packet_.SerializeAsString();
+    auto result = transform_.Transform(context_, &packet);
+
+    if (!result.ok()) {
+      return result;
+    }
+
+    protos::gen::TracePacket redacted_packet;
+    redacted_packet.ParseFromString(packet);
+
+    return redacted_packet;
+  }
+
+  Context context_;
+
+  const RedactSchedSwitch& transform() const { return transform_; }
+
+ private:
+  protos::gen::TracePacket trace_packet_;
+  protos::gen::FtraceEventBundle* ftrace_bundle_;
+
+  RedactSchedSwitch transform_;
+};
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForNullPacket) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kUidA;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  ASSERT_FALSE(transform().Transform(context, nullptr).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForEmptyPacket) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kUidA;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  std::string packet_str = "";
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForNoTimeline) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kUidA;
+
+  protos::gen::TracePacket packet;
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForNoPackage) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  protos::gen::TracePacket packet;
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, BundleWithNonEventChild) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context.package_uid = 0;
+
+  // packet {
+  //   ftrace_events {
+  //     cpu: 0
+  //     event {
+  //       timestamp: 6702093744772646
+  //       pid: 0
+  //       sched_switch {
+  //         prev_comm: "swapper/0"
+  //         prev_pid: 0
+  //         prev_prio: 120
+  //         prev_state: 0
+  //         next_comm: "writer"
+  //         next_pid: 23020
+  //         next_prio: 96
+  //       }
+  //     }
+  //   }
+  // }
+
+  protos::gen::TracePacket packet;
+  auto* events = packet.mutable_ftrace_events();
+  events->set_cpu(0);
+
+  auto* event = events->add_event();
+  event->set_timestamp(kPidA);
+  event->set_pid(kPidA);
+
+  auto* sched_switch = event->mutable_sched_switch();
+  sched_switch->set_prev_comm("swapper/0");
+  sched_switch->set_prev_pid(kPidA);
+  sched_switch->set_prev_prio(120);
+  sched_switch->set_prev_state(0);
+
+  sched_switch->set_next_comm("writer");
+  sched_switch->set_next_pid(kPidB);
+  sched_switch->set_next_prio(96);
+
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_TRUE(transform().Transform(context, &packet_str).ok());
+
+  protos::gen::TracePacket redacted;
+  redacted.ParseFromString(packet_str);
+
+  // Make sure values alongside the "event" value (e.g. "cpu") are retained.
+  ASSERT_TRUE(redacted.has_ftrace_events());
+  ASSERT_TRUE(redacted.ftrace_events().has_cpu());
+}
+
+// There are more than sched_switch events in the ftrace_events message.
+// Beyond supporting simple fields along side the event (e.g. cpu), not all
+// events will contain sched_switch events. Make sure that all every message is
+// retained while redacting the sched_switch.
+TEST_F(RedactSchedSwitchTest, KeepsNonSwitchEvents) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = 2;
+
+  // Keep the previous PID and remove the next PID.
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context.timeline->Append(ProcessThreadTimeline::Event::Open(0, 0, 1, 2));
+  context.timeline->Sort();
+
+  // packet {
+  //   ftrace_events {
+  //     cpu: 0
+  //     event {
+  //       timestamp: 6702093744766292
+  //       pid: 0
+  //       cpu_idle {
+  //         state: 4294967295
+  //         cpu_id: 0
+  //       }
+  //     }
+  //     event {
+  //       timestamp: 6702093744772646
+  //       pid: 0
+  //       sched_switch {
+  //         prev_comm: "swapper/0"
+  //         prev_pid: 0
+  //         prev_prio: 120
+  //         prev_state: 0
+  //         next_comm: "writer"
+  //         next_pid: 23020
+  //         next_prio: 96
+  //       }
+  //     }
+  //     event {
+  //       timestamp: 6702093744803376
+  //       pid: 23020
+  //       sched_waking {
+  //         comm: "FastMixer"
+  //         pid: 1619
+  //         prio: 96
+  //         success: 1
+  //         target_cpu: 1
+  //       }
+  //     }
+  //   }
+  // }
+
+  protos::gen::TracePacket source_packet;
+  source_packet.mutable_ftrace_events()->set_cpu(0);
+
+  // cpu_idle
+  do {
+    auto* event = source_packet.mutable_ftrace_events()->add_event();
+    event->set_timestamp(6702093744766292);
+    event->set_pid(0);
+
+    auto* cpu_idle = event->mutable_cpu_idle();
+    cpu_idle->set_state(4294967295);
+    cpu_idle->set_cpu_id(0);
+  } while (false);
+
+  // sched_switch
+  do {
+    auto* event = source_packet.mutable_ftrace_events()->add_event();
+    event->set_timestamp(6702093744772646);
+    event->set_pid(0);
+
+    auto* sched_switch = event->mutable_sched_switch();
+    sched_switch->set_prev_comm("swapper/0");
+    sched_switch->set_prev_pid(0);
+    sched_switch->set_prev_prio(120);
+    sched_switch->set_prev_state(0);
+    sched_switch->set_next_comm("writer");
+    sched_switch->set_next_pid(23020);
+    sched_switch->set_next_prio(96);
+  } while (false);
+
+  // sched_waking
+  do {
+    auto* event = source_packet.mutable_ftrace_events()->add_event();
+    event->set_timestamp(6702093744803376);
+    event->set_pid(23020);
+
+    auto* sched_waking = event->mutable_sched_waking();
+    sched_waking->set_comm("FastMixer");
+    sched_waking->set_pid(1619);
+    sched_waking->set_prio(96);
+    sched_waking->set_success(1);
+    sched_waking->set_target_cpu(1);
+  } while (false);
+
+  auto packet_str = source_packet.SerializeAsString();
+
+  ASSERT_TRUE(transform().Transform(context, &packet_str).ok());
+
+  protos::gen::TracePacket packet;
+  source_packet.ParseFromString(packet_str);
+
+  // Make sure values alongside the "event" value (e.g. "cpu") are retained.
+  ASSERT_TRUE(source_packet.has_ftrace_events());
+
+  auto& ftrace_packets = source_packet.ftrace_events();
+
+  ASSERT_TRUE(ftrace_packets.has_cpu());
+  ASSERT_EQ(ftrace_packets.cpu(), 0u);
+
+  // Assumes order is retained.
+  ASSERT_EQ(ftrace_packets.event_size(), 3);
+  ASSERT_TRUE(ftrace_packets.event().at(0).has_cpu_idle());
+  ASSERT_TRUE(ftrace_packets.event().at(1).has_sched_switch());
+  ASSERT_TRUE(ftrace_packets.event().at(2).has_sched_waking());
+
+  // The sched switch event's next comm should be cleared.
+  const auto& sched_switch = ftrace_packets.event().at(1).sched_switch();
+
+  ASSERT_TRUE(sched_switch.has_prev_comm());
+  ASSERT_EQ(sched_switch.prev_comm(), "swapper/0");
+
+  ASSERT_FALSE(sched_switch.has_next_comm());
+}
+
+class CommTestParams {
+ public:
+  CommTestParams(size_t event_index,
+                 int32_t prev_pid,
+                 std::optional<std::string_view> prev_comm,
+                 int32_t next_pid,
+                 std::optional<std::string_view> next_comm)
+      : event_index_(event_index),
+        prev_pid_(prev_pid),
+        prev_comm_(prev_comm),
+        next_pid_(next_pid),
+        next_comm_(next_comm) {}
+
+  size_t event_index() const { return event_index_; }
+
+  int32_t prev_pid() const { return prev_pid_; }
+
+  std::optional<std::string> prev_comm() const { return prev_comm_; }
+
+  int32_t next_pid() const { return next_pid_; }
+
+  std::optional<std::string> next_comm() const { return next_comm_; }
+
+ private:
+  size_t event_index_;
+
+  int32_t prev_pid_;
+  std::optional<std::string> prev_comm_;
+
+  int32_t next_pid_;
+  std::optional<std::string> next_comm_;
+};
+
+class RedactSchedSwitchTestRemoveComm
+    : public RedactSchedSwitchTest,
+      public testing::WithParamInterface<CommTestParams> {};
+
+TEST_P(RedactSchedSwitchTestRemoveComm, AllEvents) {
+  auto params = GetParam();
+
+  context_.package_uid = kUidA;
+
+  BeginBundle();
+
+  // Cycle through all the processes: Pid A -> Pid B -> Pid C -> Pid A
+  AddSwitch(kTimeA, kPidA, kCommA, kPidB, kCommB);
+  AddSwitch(kTimeB, kPidB, kCommB, kPidC, kCommC);
+  AddSwitch(kTimeC, kPidC, kCommC, kPidA, kCommA);
+
+  auto packet = Transform();
+
+  ASSERT_TRUE(packet->has_ftrace_events());
+
+  auto& ftrace_events = packet->ftrace_events().event();
+
+  ASSERT_EQ(ftrace_events.size(), 3u);
+
+  auto event_index = params.event_index();
+
+  ASSERT_TRUE(ftrace_events[event_index].has_sched_switch());
+
+  auto& sched_switch = ftrace_events[event_index].sched_switch();
+
+  ASSERT_EQ(sched_switch.prev_pid(), params.prev_pid());
+  ASSERT_EQ(sched_switch.next_pid(), params.next_pid());
+
+  ASSERT_EQ(sched_switch.has_prev_comm(), params.prev_comm().has_value());
+  ASSERT_EQ(sched_switch.has_next_comm(), params.next_comm().has_value());
+
+  if (sched_switch.has_prev_comm()) {
+    ASSERT_EQ(sched_switch.prev_comm(), params.prev_comm());
+  }
+
+  if (sched_switch.has_next_comm()) {
+    ASSERT_EQ(sched_switch.next_comm(), params.next_comm());
+  }
+}
+
+// Cycle through all the processes: Pid A -> Pid B -> Pid C -> Pid A
+//
+// Only kPidA is attached to kUidA, so it should be the only one with a comm
+// value.
+INSTANTIATE_TEST_SUITE_P(
+    EveryPid,
+    RedactSchedSwitchTestRemoveComm,
+    testing::Values(CommTestParams(0, kPidA, kCommA, kPidB, std::nullopt),
+                    CommTestParams(1, kPidB, std::nullopt, kPidC, std::nullopt),
+                    CommTestParams(2, kPidC, std::nullopt, kPidA, kCommA)));
+
+}  // namespace perfetto::trace_redaction
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 221817f..e21997c 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 @@
-0d8e9f71b51633ce3927ff1ec94e1ce527c836df8f9fd748924ca46711185331
\ No newline at end of file
+8e2f7043d233187a8e2229e3bfd64c3a21522e52036bf4135641c4b22ff2a40d
\ 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 b7941b6..22796d2 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 @@
-bf670ed387e44d0b4209f4da443f15e82be24da5b9573290017775cb5d68446d
\ No newline at end of file
+804eb26f54e147bc536d14354cc1694f748e598b7baaaa50cfd41e137781cbac
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_unagg.out b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_unagg.out
new file mode 100644
index 0000000..4f288a7
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_unagg.out
@@ -0,0 +1,362 @@
+android_blocking_calls_unagg {
+  process_with_blocking_calls {
+    process {
+      name: "com.android.systemui"
+      uid: 10001
+      pid: 1000
+    }
+    blocking_calls {
+      name: "binder transaction"
+      cnt: 7
+      total_dur_ms: 20
+      max_dur_ms: 10
+      min_dur_ms: 1
+      total_dur_ns: 20000000
+      max_dur_ns: 10000000
+      min_dur_ns: 1000000
+    }
+    blocking_calls {
+      name: "monitor contention with <...>"
+      cnt: 1
+      total_dur_ms: 12
+      max_dur_ms: 12
+      min_dur_ms: 12
+      total_dur_ns: 12000000
+      max_dur_ns: 12000000
+      min_dur_ns: 12000000
+    }
+    blocking_calls {
+      name: "AIDL::java::IWindowManager::hasNavigationBar::server"
+      cnt: 1
+      total_dur_ms: 10
+      max_dur_ms: 10
+      min_dur_ms: 10
+      total_dur_ns: 10000000
+      max_dur_ns: 10000000
+      min_dur_ns: 10000000
+    }
+  }
+  process_with_blocking_calls {
+   process {
+      name: "com.google.android.apps.nexuslauncher"
+      uid: 10002
+      pid: 2000
+    }
+    blocking_calls {
+      name: "binder transaction"
+      cnt: 6
+      total_dur_ms: 10
+      max_dur_ms: 3
+      min_dur_ms: 1
+      total_dur_ns: 10000000
+      max_dur_ns: 3000000
+      min_dur_ns: 1000000
+   }
+ }
+ process_with_blocking_calls {
+   process {
+     name: "com.google.android.third.process"
+     uid: 10003
+     pid: 3000
+   }
+
+   blocking_calls {
+     name: "CoroutineContinuation"
+     cnt: 2
+     total_dur_ms: 20
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 20000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Contending for pthread mutex"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "ExpNotRow#onMeasure(BigTextStyle)"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "ExpNotRow#onMeasure(MessagingStyle)"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Garbage Collector"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "ImageDecoder#decodeBitmap"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "ImageDecoder#decodeDrawable"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "LoadApkAssetsFd <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Lock contention on a monitor lock <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Lock contention on thread list lock <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Lock contention on thread suspend count lock <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "NotificationStackScrollLayout#onMeasure"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "SuspendThreadByThreadId <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "animation"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "binder transaction"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "configChanged"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "inflate"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "input"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "layout"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "measure"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "monitor contention with <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "postAndWait"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "relayoutWindow <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "traversal"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+}
+process_with_blocking_calls {
+   process {
+     name: "com.google.android.top.level.slices"
+     uid: 10004
+     pid: 4000
+   }
+
+   blocking_calls {
+     name: "Handler: android.os.AsyncTask"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Handler: android.view.View"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Handler: com.android.keyguard.KeyguardUpdateMonitor"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Handler: com.android.systemui.broadcast.ActionReceiver"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Handler: com.android.systemui.qs.external.TileServiceManager"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   }
+}
diff --git a/test/trace_processor/diff_tests/metrics/android/tests.py b/test/trace_processor/diff_tests/metrics/android/tests.py
index 5038aec..06cbe59 100644
--- a/test/trace_processor/diff_tests/metrics/android/tests.py
+++ b/test/trace_processor/diff_tests/metrics/android/tests.py
@@ -121,6 +121,12 @@
         query=Metric('android_blocking_calls_cuj_metric'),
         out=Path('android_blocking_calls_cuj_metric.out'))
 
+  def test_android_blocking_calls_unagg(self):
+    return DiffTestBlueprint(
+        trace=Path('android_blocking_calls_cuj_metric.py'),
+        query=Metric('android_blocking_calls_unagg'),
+        out=Path('android_blocking_calls_unagg.out'))
+
   def test_android_blocking_calls_on_jank_cujs(self):
     return DiffTestBlueprint(
         trace=Path('../graphics/android_jank_cuj.py'),
@@ -296,4 +302,4 @@
            duration_ms: 3878
          }
        }
-       """))
\ No newline at end of file
+       """))
diff --git a/tools/check_sql_metrics.py b/tools/check_sql_metrics.py
index 3819c27..f025f47 100755
--- a/tools/check_sql_metrics.py
+++ b/tools/check_sql_metrics.py
@@ -41,6 +41,11 @@
         'android_cujs', 'relevant_binder_calls_with_names',
         'android_blocking_calls_cuj_calls'
     ],
+    ('/android'
+    '/android_blocking_calls_unagg.sql'): [
+        'filtered_processes_with_non_zero_blocking_calls',
+        'process_info', 'android_blocking_calls_unagg_calls'
+    ],
     '/android/jank/cujs.sql': ['android_jank_cuj'],
     '/chrome/gesture_flow_event.sql': [
         '{{prefix}}_latency_info_flow_step_filtered'
diff --git a/ui/src/base/async_limiter.ts b/ui/src/base/async_limiter.ts
new file mode 100644
index 0000000..3130d6d
--- /dev/null
+++ b/ui/src/base/async_limiter.ts
@@ -0,0 +1,73 @@
+// Copyright (C) 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
+//
+//      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 {Deferred, defer} from './deferred';
+
+type Callback = () => Promise<void>;
+
+interface Task {
+  deferred: Deferred<void>;
+  work: Callback;
+}
+
+/**
+ * A tiny task queue management utility that ensures async tasks are not
+ * executed concurrently.
+ *
+ * If a task is run while a previous one is still running, it is enqueued and
+ * run after the first task completes.
+ *
+ * If multiple tasks are enqueued, only the latest task is run.
+ */
+export class AsyncLimiter {
+  private readonly taskQueue: Task[] = [];
+  private isRunning: boolean = false;
+
+  /**
+   * Schedule a task to be run.
+   *
+   * @param work An async function to schedule.
+   * @returns A promise that resolves when either the task has finished
+   * executing, or after the task has silently been discarded because a newer
+   * task was scheduled.
+   */
+  schedule(work: Callback): Promise<void> {
+    const deferred = defer<void>();
+    this.taskQueue.push({work, deferred});
+
+    if (!this.isRunning) {
+      this.isRunning = true;
+      this.runTaskQueue().finally(() => (this.isRunning = false));
+    }
+
+    return deferred;
+  }
+
+  private async runTaskQueue(): Promise<void> {
+    let task: Task | undefined;
+
+    while ((task = this.taskQueue.shift())) {
+      if (this.taskQueue.length > 0) {
+        task.deferred.resolve();
+      } else {
+        try {
+          await task.work();
+          task.deferred.resolve();
+        } catch (e) {
+          task.deferred.reject(e);
+        }
+      }
+    }
+  }
+}
diff --git a/ui/src/base/async_limiter_unittest.ts b/ui/src/base/async_limiter_unittest.ts
new file mode 100644
index 0000000..6b080e1
--- /dev/null
+++ b/ui/src/base/async_limiter_unittest.ts
@@ -0,0 +1,80 @@
+// Copyright (C) 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
+//
+//      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 {AsyncLimiter} from './async_limiter';
+
+test('no concurrent callbacks', async () => {
+  const limiter = new AsyncLimiter();
+
+  const mock1 = jest.fn();
+  limiter.schedule(async () => mock1());
+  expect(mock1).toHaveBeenCalled();
+
+  const mock2 = jest.fn();
+  limiter.schedule(async () => mock2());
+  expect(mock2).not.toHaveBeenCalled();
+});
+
+test('queueing', async () => {
+  const limiter = new AsyncLimiter();
+
+  const mock1 = jest.fn();
+  limiter.schedule(async () => mock1());
+
+  const mock2 = jest.fn();
+  await limiter.schedule(async () => mock2());
+
+  expect(mock1).toHaveBeenCalled();
+  expect(mock2).toHaveBeenCalled();
+});
+
+test('multiple queuing', async () => {
+  const limiter = new AsyncLimiter();
+
+  const mock1 = jest.fn();
+  limiter.schedule(async () => mock1());
+
+  const mock2 = jest.fn();
+  limiter.schedule(async () => mock2());
+
+  const mock3 = jest.fn();
+  await limiter.schedule(async () => mock3());
+
+  expect(mock1).toHaveBeenCalled();
+  expect(mock2).not.toHaveBeenCalled();
+  expect(mock3).toHaveBeenCalled();
+});
+
+test('error in callback bubbles up to caller', async () => {
+  const limiter = new AsyncLimiter();
+  const failingCallback = async () => {
+    throw Error();
+  };
+
+  expect(async () => await limiter.schedule(failingCallback)).rejects.toThrow();
+});
+
+test('chain continues even when one callback fails', async () => {
+  const limiter = new AsyncLimiter();
+
+  const failingCallback = async () => {
+    throw Error();
+  };
+  limiter.schedule(failingCallback).catch(() => {});
+
+  const mock = jest.fn();
+  await limiter.schedule(async () => mock());
+
+  expect(mock).toHaveBeenCalled();
+});
diff --git a/ui/src/base/monitor.ts b/ui/src/base/monitor.ts
new file mode 100644
index 0000000..d4e0f87
--- /dev/null
+++ b/ui/src/base/monitor.ts
@@ -0,0 +1,36 @@
+// Copyright (C) 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
+//
+//      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.
+
+type Reducer = () => unknown;
+type Callback = () => void;
+
+/**
+ * A little helper that monitors a list of immutable objects and calls a
+ * callback only when at least one them changes.
+ */
+export class Monitor {
+  private cached: unknown[];
+
+  constructor(private reducers: Reducer[]) {
+    this.cached = reducers.map(() => undefined);
+  }
+
+  ifStateChanged(callback: Callback): void {
+    const state = this.reducers.map((f) => f());
+    if (state.some((x, i) => x !== this.cached[i])) {
+      callback();
+    }
+    this.cached = state;
+  }
+}
diff --git a/ui/src/base/monitor_unittest.ts b/ui/src/base/monitor_unittest.ts
new file mode 100644
index 0000000..51b22da
--- /dev/null
+++ b/ui/src/base/monitor_unittest.ts
@@ -0,0 +1,38 @@
+// Copyright (C) 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
+//
+//      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 {Monitor} from './monitor';
+
+test('callback is called when state changes', () => {
+  const reducer = jest.fn().mockReturnValue('foo');
+  const monitor = new Monitor([reducer]);
+  const mockCallback = jest.fn();
+
+  monitor.ifStateChanged(mockCallback);
+  expect(mockCallback).toHaveBeenCalledTimes(1);
+
+  mockCallback.mockReset();
+  monitor.ifStateChanged(mockCallback);
+  monitor.ifStateChanged(mockCallback);
+  expect(mockCallback).not.toHaveBeenCalled();
+
+  mockCallback.mockReset();
+  reducer.mockReturnValue('bar');
+  monitor.ifStateChanged(mockCallback);
+  expect(mockCallback).toHaveBeenCalledTimes(1);
+
+  mockCallback.mockReset();
+  monitor.ifStateChanged(mockCallback);
+  expect(mockCallback).not.toHaveBeenCalled();
+});
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 9e39eed..161fba7 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -104,7 +104,6 @@
 type States = 'init' | 'loading_trace' | 'ready';
 
 const METRICS = [
-  'android_startup',
   'android_ion',
   'android_lmk',
   'android_dma_heap',
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index 8a03d47..583e6bc 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -28,6 +28,7 @@
   'dev.perfetto.AndroidNetwork',
   'dev.perfetto.AndroidPerf',
   'dev.perfetto.AndroidPerfTraceCounters',
+  'dev.perfetto.AndroidStartup',
   'dev.perfetto.BookmarkletApi',
   'dev.perfetto.CoreCommands',
   'dev.perfetto.LargeScreensPerf',
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 24769c2..4bed3fc 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -322,8 +322,8 @@
 
         if (engine !== undefined && trackUtid != 0) {
           await runQuery(
-              `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
-              engine,
+            `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
+            engine,
           );
           await addDebugSliceTrack(
             engine,
@@ -365,8 +365,8 @@
 
         if (engine !== undefined && trackUtid != 0) {
           await runQuery(
-              `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-              engine,
+            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+            engine,
           );
           await addDebugSliceTrack(
             engine,
@@ -399,8 +399,7 @@
 
         if (engine !== undefined && trackUtid != 0) {
           addQueryResultsTab({
-            query:
-                `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
+            query: `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
                    SELECT *
                       FROM
                         _thread_executing_span_critical_path_graph(
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index 6b0b2ef..26dcb57 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -271,62 +271,63 @@
       `${state.state} for ${renderDuration(state.dur)}`;
     return [
       m(
-          Tree,
-          this.relatedStates.waker && m(TreeNode, {
+        Tree,
+        this.relatedStates.waker &&
+          m(TreeNode, {
             left: 'Waker',
             right: renderRef(
-                this.relatedStates.waker,
-                getFullThreadName(this.relatedStates.waker.thread),
-                ),
+              this.relatedStates.waker,
+              getFullThreadName(this.relatedStates.waker.thread),
+            ),
           }),
-          this.relatedStates.prev && m(TreeNode, {
+        this.relatedStates.prev &&
+          m(TreeNode, {
             left: 'Previous state',
             right: renderRef(
-                this.relatedStates.prev,
-                nameForNextOrPrev(this.relatedStates.prev),
-                ),
+              this.relatedStates.prev,
+              nameForNextOrPrev(this.relatedStates.prev),
+            ),
           }),
-          this.relatedStates.next && m(TreeNode, {
+        this.relatedStates.next &&
+          m(TreeNode, {
             left: 'Next state',
             right: renderRef(
-                this.relatedStates.next,
-                nameForNextOrPrev(this.relatedStates.next),
-                ),
+              this.relatedStates.next,
+              nameForNextOrPrev(this.relatedStates.next),
+            ),
           }),
-          this.relatedStates.wakee && this.relatedStates.wakee.length > 0 &&
-              m(
-                  TreeNode,
-                  {
-                    left: 'Woken threads',
-                  },
-                  this.relatedStates.wakee.map(
-                      (state) => m(TreeNode, {
-                        left: m(Timestamp, {
-                          ts: state.ts,
-                          display: [
-                            'Start+',
-                            m(DurationWidget,
-                              {dur: Time.sub(state.ts, startTs)}),
-                          ],
-                        }),
-                        right:
-                            renderRef(state, getFullThreadName(state.thread)),
-                      }),
-                      ),
-                  ),
+        this.relatedStates.wakee &&
+          this.relatedStates.wakee.length > 0 &&
+          m(
+            TreeNode,
+            {
+              left: 'Woken threads',
+            },
+            this.relatedStates.wakee.map((state) =>
+              m(TreeNode, {
+                left: m(Timestamp, {
+                  ts: state.ts,
+                  display: [
+                    'Start+',
+                    m(DurationWidget, {dur: Time.sub(state.ts, startTs)}),
+                  ],
+                }),
+                right: renderRef(state, getFullThreadName(state.thread)),
+              }),
+            ),
           ),
+      ),
       m(Button, {
         label: 'Critical path lite',
         onclick: () =>
-            runQuery(
-                `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
-                this.engine,
-                )
-                .then(
-                    () => addDebugSliceTrack(
-                        this.engine,
-                        {
-                          sqlSource: `
+          runQuery(
+            `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
+            this.engine,
+          ).then(() =>
+            addDebugSliceTrack(
+              this.engine,
+              {
+                sqlSource: `
                     SELECT
                       cr.id,
                       cr.utid,
@@ -344,26 +345,25 @@
                     JOIN thread USING(utid)
                     JOIN process USING(upid)
                   `,
-                          columns: sliceLiteColumnNames,
-                        },
-                        `${this.state?.thread?.name}`,
-                        sliceLiteColumns,
-                        sliceLiteColumnNames,
-                        ),
-                    ),
+                columns: sliceLiteColumnNames,
+              },
+              `${this.state?.thread?.name}`,
+              sliceLiteColumns,
+              sliceLiteColumnNames,
+            ),
+          ),
       }),
       m(Button, {
         label: 'Critical path',
         onclick: () =>
-            runQuery(
-                `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-                this.engine,
-                )
-                .then(
-                    () => addDebugSliceTrack(
-                        this.engine,
-                        {
-                          sqlSource: `
+          runQuery(
+            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+            this.engine,
+          ).then(() =>
+            addDebugSliceTrack(
+              this.engine,
+              {
+                sqlSource: `
                     SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
                       FROM
                         _thread_executing_span_critical_path_stack(
@@ -372,13 +372,13 @@
                           trace_bounds.end_ts - trace_bounds.start_ts) cr,
                         trace_bounds WHERE name IS NOT NULL
                   `,
-                          columns: sliceColumnNames,
-                        },
-                        `${this.state?.thread?.name}`,
-                        sliceColumns,
-                        sliceColumnNames,
-                        ),
-                    ),
+                columns: sliceColumnNames,
+              },
+              `${this.state?.thread?.name}`,
+              sliceColumns,
+              sliceColumnNames,
+            ),
+          ),
       }),
     ];
   }
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/OWNERS b/ui/src/plugins/dev.perfetto.AndroidStartup/OWNERS
new file mode 100644
index 0000000..52dc56a
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidStartup/OWNERS
@@ -0,0 +1,2 @@
+lalitm@google.com
+ilkos@google.com
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
new file mode 100644
index 0000000..ebb10b8
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
@@ -0,0 +1,63 @@
+// Copyright (C) 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
+//
+//      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 {
+  LONG,
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {
+  SimpleSliceTrack,
+  SimpleSliceTrackConfig,
+} from '../../frontend/simple_slice_track';
+
+class AndroidStartup implements Plugin {
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    const e = ctx.engine;
+    await e.query(`include perfetto module android.startup.startups;`);
+
+    const cnt = await e.query('select count() cnt from android_startups');
+    if (cnt.firstRow({cnt: LONG}).cnt === 0n) {
+      return;
+    }
+
+    const config: SimpleSliceTrackConfig = {
+      data: {
+        sqlSource: `
+          SELECT l.ts AS ts, l.dur AS dur, l.package AS name
+          FROM android_startups l
+        `,
+        columns: ['ts', 'dur', 'name'],
+      },
+      columns: {ts: 'ts', dur: 'dur', name: 'name'},
+      argColumns: [],
+    };
+    ctx.registerStaticTrack({
+      uri: `dev.perfetto.AndroidStartup#startups`,
+      displayName: 'Android App Startups',
+      trackFactory: (trackCtx) => {
+        return new SimpleSliceTrack(ctx.engine, trackCtx, config);
+      },
+    });
+  }
+}
+
+export const plugin: PluginDescriptor = {
+  pluginId: 'dev.perfetto.AndroidStartup',
+  plugin: AndroidStartup,
+};
diff --git a/ui/src/tracks/ftrace/ftrace_explorer.ts b/ui/src/tracks/ftrace/ftrace_explorer.ts
index 7015d5c..99a2ce2 100644
--- a/ui/src/tracks/ftrace/ftrace_explorer.ts
+++ b/ui/src/tracks/ftrace/ftrace_explorer.ts
@@ -39,14 +39,15 @@
   STR_NULL,
 } from '../../public';
 import {raf} from '../../core/raf_scheduler';
-import {VisibleState} from '../../common/state';
+import {AsyncLimiter} from '../../base/async_limiter';
+import {Monitor} from '../../base/monitor';
 
 const ROW_H = 20;
 const PAGE_SIZE = 250;
 
 interface FtraceExplorerAttrs {
   counters: FtraceStat[];
-  store: Store<FtraceFilter>;
+  filterStore: Store<FtraceFilter>;
   engine: EngineProxy;
 }
 
@@ -72,32 +73,37 @@
 }
 
 export class FtraceExplorer implements m.ClassComponent<FtraceExplorerAttrs> {
-  private paginationStore = createStore<Pagination>({
+  private readonly paginationStore = createStore<Pagination>({
     page: 0,
     pageCount: 0,
   });
-  private oldPagination?: Pagination;
-  private oldFilterState?: FtraceFilter;
-  private oldVisibleState?: VisibleState;
+  private readonly monitor: Monitor;
+  private readonly queryLimiter = new AsyncLimiter();
 
   // A cache of the data we have most recently loaded from our store
   private data?: FtracePanelData;
 
+  constructor({attrs}: m.CVnode<FtraceExplorerAttrs>) {
+    this.monitor = new Monitor([
+      () => globals.state.frontendLocalState.visibleState.start,
+      () => globals.state.frontendLocalState.visibleState.end,
+      () => attrs.filterStore.state,
+      () => this.paginationStore.state,
+    ]);
+  }
+
   view({attrs}: m.CVnode<FtraceExplorerAttrs>) {
-    if (this.shouldUpdate(attrs.store)) {
-      this.oldVisibleState = globals.state.frontendLocalState.visibleState;
-      this.oldFilterState = attrs.store.state;
-      this.oldPagination = this.paginationStore.state;
-      lookupFtraceEvents(
-        attrs.engine,
-        this.paginationStore.state.page * PAGE_SIZE,
-        this.paginationStore.state.pageCount * PAGE_SIZE,
-        attrs.store.state,
-      ).then((data) => {
-        this.data = data;
+    this.monitor.ifStateChanged(() =>
+      this.queryLimiter.schedule(async () => {
+        this.data = await lookupFtraceEvents(
+          attrs.engine,
+          this.paginationStore.state.page * PAGE_SIZE,
+          this.paginationStore.state.pageCount * PAGE_SIZE,
+          attrs.filterStore.state,
+        );
         raf.scheduleFullRedraw();
-      });
-    }
+      }),
+    );
 
     return m(
       DetailsShell,
@@ -124,7 +130,7 @@
     const visibleRowCount = Math.ceil(scrollContainer.clientHeight / ROW_H);
 
     // Work out which "page" we're on
-    const page = Math.floor(visibleRowOffset / PAGE_SIZE) - 1;
+    const page = Math.max(0, Math.floor(visibleRowOffset / PAGE_SIZE) - 1);
     const pageCount = Math.ceil(visibleRowCount / PAGE_SIZE) + 2;
 
     if (page !== prevPage || pageCount !== prevPageCount) {
@@ -136,24 +142,6 @@
     }
   }
 
-  private shouldUpdate(filterStore: Store<FtraceFilter>): boolean {
-    if (filterStore.state !== this.oldFilterState) {
-      return true;
-    }
-
-    if (
-      globals.state.frontendLocalState.visibleState !== this.oldVisibleState
-    ) {
-      return true;
-    }
-
-    if (this.paginationStore.state !== this.oldPagination) {
-      return true;
-    }
-
-    return false;
-  }
-
   onRowOver(ts: time) {
     globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
   }
@@ -172,7 +160,7 @@
   }
 
   private renderFilterPanel(attrs: FtraceExplorerAttrs) {
-    const excludeList = attrs.store.state.excludeList;
+    const excludeList = attrs.filterStore.state.excludeList;
     const options: MultiSelectOption[] = attrs.counters.map(({name, count}) => {
       return {
         id: name,
@@ -196,7 +184,7 @@
             newList.add(id);
           }
         });
-        attrs.store.edit((draft) => {
+        attrs.filterStore.edit((draft) => {
           draft.excludeList = Array.from(newList);
         });
       },
diff --git a/ui/src/tracks/ftrace/index.ts b/ui/src/tracks/ftrace/index.ts
index c5860b9..a58c27a 100644
--- a/ui/src/tracks/ftrace/index.ts
+++ b/ui/src/tracks/ftrace/index.ts
@@ -85,7 +85,11 @@
       isEphemeral: false,
       content: {
         render: () =>
-          m(FtraceExplorer, {counters, store: filterStore, engine: ctx.engine}),
+          m(FtraceExplorer, {
+            counters,
+            filterStore,
+            engine: ctx.engine,
+          }),
         getTitle: () => 'Ftrace Events',
       },
     });