Merge "Remove '.details-panel button' ruleset"
diff --git a/BUILD b/BUILD
index 9b50375..49778a3 100644
--- a/BUILD
+++ b/BUILD
@@ -3824,6 +3824,7 @@
     ],
     deps = [
         ":protos_perfetto_common_protos",
+        ":protos_perfetto_trace_android_protos",
         ":protos_perfetto_trace_gpu_protos",
         ":protos_perfetto_trace_profiling_protos",
         ":protos_perfetto_trace_track_event_protos",
@@ -3835,6 +3836,7 @@
     name = "protos_perfetto_trace_interned_data_zero",
     deps = [
         ":protos_perfetto_common_zero",
+        ":protos_perfetto_trace_android_zero",
         ":protos_perfetto_trace_gpu_zero",
         ":protos_perfetto_trace_interned_data_protos",
         ":protos_perfetto_trace_profiling_zero",
diff --git a/protos/perfetto/config/android/network_trace_config.proto b/protos/perfetto/config/android/network_trace_config.proto
index f58f280..ad5196b 100644
--- a/protos/perfetto/config/android/network_trace_config.proto
+++ b/protos/perfetto/config/android/network_trace_config.proto
@@ -27,4 +27,28 @@
   // The minimum polling rate is 100ms (values below this are ignored).
   // Introduced in Android 14 (U).
   optional uint32 poll_ms = 1;
+
+  // The aggregation_threshold is the number of packets at which an event will
+  // switch from per-packet details to aggregate details. For example, a value
+  // of 50 means that if a particular event (grouped by the unique combinations
+  // of metadata fields: {interface, direction, uid, etc}) has fewer than 50
+  // packets, the exact timestamp and length are recorded for each packet. If
+  // there were 50 or more packets in an event, it would only record the total
+  // duration, packets, and length. A value of zero or unspecified will always
+  /// record per-packet details. A value of 1 always records aggregate details.
+  optional uint32 aggregation_threshold = 2;
+
+  // Specifies the maximum number of packet contexts to intern at a time. This
+  // prevents the interning table from growing too large and controls whether
+  // interning is enabled or disabled (a value of zero disables interning and
+  // is the default). When a data sources interning table reaches this amount,
+  // packet contexts will be inlined into NetworkPacketEvents.
+  optional uint32 intern_limit = 3;
+
+  // The following fields specify whether certain fields should be dropped from
+  // the output. Dropping fields improves normalization results, reduces the
+  // size of the interning table, and slightly reduces event size.
+  optional bool drop_local_port = 4;
+  optional bool drop_remote_port = 5;
+  optional bool drop_tcp_flags = 6;
 }
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index 2504541..3673dd0 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -430,6 +430,30 @@
   // The minimum polling rate is 100ms (values below this are ignored).
   // Introduced in Android 14 (U).
   optional uint32 poll_ms = 1;
+
+  // The aggregation_threshold is the number of packets at which an event will
+  // switch from per-packet details to aggregate details. For example, a value
+  // of 50 means that if a particular event (grouped by the unique combinations
+  // of metadata fields: {interface, direction, uid, etc}) has fewer than 50
+  // packets, the exact timestamp and length are recorded for each packet. If
+  // there were 50 or more packets in an event, it would only record the total
+  // duration, packets, and length. A value of zero or unspecified will always
+  /// record per-packet details. A value of 1 always records aggregate details.
+  optional uint32 aggregation_threshold = 2;
+
+  // Specifies the maximum number of packet contexts to intern at a time. This
+  // prevents the interning table from growing too large and controls whether
+  // interning is enabled or disabled (a value of zero disables interning and
+  // is the default). When a data sources interning table reaches this amount,
+  // packet contexts will be inlined into NetworkPacketEvents.
+  optional uint32 intern_limit = 3;
+
+  // The following fields specify whether certain fields should be dropped from
+  // the output. Dropping fields improves normalization results, reduces the
+  // size of the interning table, and slightly reduces event size.
+  optional bool drop_local_port = 4;
+  optional bool drop_remote_port = 5;
+  optional bool drop_tcp_flags = 6;
 }
 
 // End of protos/perfetto/config/android/network_trace_config.proto
diff --git a/protos/perfetto/metrics/android/android_frame_timeline_metric.proto b/protos/perfetto/metrics/android/android_frame_timeline_metric.proto
index d0f4d50..0ae9ec6 100644
--- a/protos/perfetto/metrics/android/android_frame_timeline_metric.proto
+++ b/protos/perfetto/metrics/android/android_frame_timeline_metric.proto
@@ -39,12 +39,14 @@
     optional double frame_dur_ms_p90 = 15;
     optional double frame_dur_ms_p95 = 16;
     optional double frame_dur_ms_p99 = 17;
+    optional int64 dropped_frames = 18;
 
     reserved 1, 2;
   }
 
   optional int64 total_frames = 4;
   optional int64 missed_app_frames = 5;
+  optional int64 dropped_frames = 6;
 
   repeated ProcessBreakdown process = 2;
 
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 4db6c88..b4956f8 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -68,12 +68,14 @@
     optional double frame_dur_ms_p90 = 15;
     optional double frame_dur_ms_p95 = 16;
     optional double frame_dur_ms_p99 = 17;
+    optional int64 dropped_frames = 18;
 
     reserved 1, 2;
   }
 
   optional int64 total_frames = 4;
   optional int64 missed_app_frames = 5;
+  optional int64 dropped_frames = 6;
 
   repeated ProcessBreakdown process = 2;
 
diff --git a/protos/perfetto/trace/android/frame_timeline_event.proto b/protos/perfetto/trace/android/frame_timeline_event.proto
index bee0bc7..f1f0df8 100644
--- a/protos/perfetto/trace/android/frame_timeline_event.proto
+++ b/protos/perfetto/trace/android/frame_timeline_event.proto
@@ -42,6 +42,7 @@
     JANK_BUFFER_STUFFING = 128;
     JANK_UNKNOWN = 256;
     JANK_SF_STUFFING = 512;
+    JANK_DROPPED = 1024;
   };
 
   // Specifies how a frame was presented on screen w.r.t. timing.
diff --git a/protos/perfetto/trace/android/network_trace.proto b/protos/perfetto/trace/android/network_trace.proto
index d1c43a2..5fcd28b 100644
--- a/protos/perfetto/trace/android/network_trace.proto
+++ b/protos/perfetto/trace/android/network_trace.proto
@@ -33,7 +33,9 @@
   // The name of the interface if available (e.g. 'rmnet0').
   optional string interface = 2;
 
-  // The length of the packet in bytes (wire_size - L2_header_size).
+  // The length of the packet in bytes (wire_size - L2_header_size). Ignored
+  // when using NetworkPacketEvent as the ctx in either NetworkPacketBundle or
+  // NetworkPacketContext.
   optional uint32 length = 3;
 
   // The Linux user id associated with the packet's socket.
@@ -54,3 +56,36 @@
   // The remote udp/tcp port of the packet.
   optional uint32 remote_port = 9;
 }
+
+// NetworkPacketBundle bundles one or more packets sharing the same attributes.
+message NetworkPacketBundle {
+  oneof packet_context {
+    // The intern id for looking up the associated packet context.
+    uint64 iid = 1;
+
+    // The inlined context for events in this bundle.
+    NetworkPacketEvent ctx = 2;
+  }
+
+  // The timestamp of the i-th packet encoded as the nanoseconds since the
+  // enclosing TracePacket's timestamp.
+  repeated uint64 packet_timestamps = 3 [packed = true];
+
+  // The length of the i-th packet in bytes (wire_size - L2_header_size).
+  repeated uint32 packet_lengths = 4 [packed = true];
+
+  // Total number of packets in the bundle (when above aggregation_threshold).
+  optional uint32 total_packets = 5;
+
+  // Duration between first and last packet (when above aggregation_threshold).
+  optional uint64 total_duration = 6;
+
+  // Total packet length in bytes (when above aggregation_threshold).
+  optional uint64 total_length = 7;
+}
+
+// An internable packet context.
+message NetworkPacketContext {
+  optional uint64 iid = 1;
+  optional NetworkPacketEvent ctx = 2;
+}
diff --git a/protos/perfetto/trace/interned_data/BUILD.gn b/protos/perfetto/trace/interned_data/BUILD.gn
index 580bb5b..66436c5 100644
--- a/protos/perfetto/trace/interned_data/BUILD.gn
+++ b/protos/perfetto/trace/interned_data/BUILD.gn
@@ -17,6 +17,7 @@
 perfetto_proto_library("@TYPE@") {
   sources = [ "interned_data.proto" ]
   deps = [
+    "../android:@TYPE@",
     "../gpu:@TYPE@",
     "../profiling:@TYPE@",
     "../track_event:@TYPE@",
diff --git a/protos/perfetto/trace/interned_data/interned_data.proto b/protos/perfetto/trace/interned_data/interned_data.proto
index 59103a7..9b6db47 100644
--- a/protos/perfetto/trace/interned_data/interned_data.proto
+++ b/protos/perfetto/trace/interned_data/interned_data.proto
@@ -16,6 +16,7 @@
 
 syntax = "proto2";
 
+import "protos/perfetto/trace/android/network_trace.proto";
 import "protos/perfetto/trace/gpu/gpu_render_stage_event.proto";
 import "protos/perfetto/trace/track_event/chrome_histogram_sample.proto";
 import "protos/perfetto/trace/track_event/debug_annotation.proto";
@@ -53,7 +54,7 @@
 // emitted proactively in advance of referring to them in later packets.
 //
 // Next reserved id: 8 (up to 15).
-// Next id: 30.
+// Next id: 31.
 message InternedData {
   // TODO(eseckler): Replace iid fields inside interned messages with
   // map<iid, message> type fields in InternedData.
@@ -114,4 +115,7 @@
 
   // Interned string values in the DebugAnnotation proto.
   repeated InternedString debug_annotation_string_values = 29;
+
+  // Interned packet context for android.network_packets.
+  repeated NetworkPacketContext packet_context = 30;
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 29d22c9..7dc8589 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -430,6 +430,30 @@
   // The minimum polling rate is 100ms (values below this are ignored).
   // Introduced in Android 14 (U).
   optional uint32 poll_ms = 1;
+
+  // The aggregation_threshold is the number of packets at which an event will
+  // switch from per-packet details to aggregate details. For example, a value
+  // of 50 means that if a particular event (grouped by the unique combinations
+  // of metadata fields: {interface, direction, uid, etc}) has fewer than 50
+  // packets, the exact timestamp and length are recorded for each packet. If
+  // there were 50 or more packets in an event, it would only record the total
+  // duration, packets, and length. A value of zero or unspecified will always
+  /// record per-packet details. A value of 1 always records aggregate details.
+  optional uint32 aggregation_threshold = 2;
+
+  // Specifies the maximum number of packet contexts to intern at a time. This
+  // prevents the interning table from growing too large and controls whether
+  // interning is enabled or disabled (a value of zero disables interning and
+  // is the default). When a data sources interning table reaches this amount,
+  // packet contexts will be inlined into NetworkPacketEvents.
+  optional uint32 intern_limit = 3;
+
+  // The following fields specify whether certain fields should be dropped from
+  // the output. Dropping fields improves normalization results, reduces the
+  // size of the interning table, and slightly reduces event size.
+  optional bool drop_local_port = 4;
+  optional bool drop_remote_port = 5;
+  optional bool drop_tcp_flags = 6;
 }
 
 // End of protos/perfetto/config/android/network_trace_config.proto
@@ -3489,6 +3513,7 @@
     JANK_BUFFER_STUFFING = 128;
     JANK_UNKNOWN = 256;
     JANK_SF_STUFFING = 512;
+    JANK_DROPPED = 1024;
   };
 
   // Specifies how a frame was presented on screen w.r.t. timing.
@@ -3707,7 +3732,9 @@
   // The name of the interface if available (e.g. 'rmnet0').
   optional string interface = 2;
 
-  // The length of the packet in bytes (wire_size - L2_header_size).
+  // The length of the packet in bytes (wire_size - L2_header_size). Ignored
+  // when using NetworkPacketEvent as the ctx in either NetworkPacketBundle or
+  // NetworkPacketContext.
   optional uint32 length = 3;
 
   // The Linux user id associated with the packet's socket.
@@ -3729,6 +3756,39 @@
   optional uint32 remote_port = 9;
 }
 
+// NetworkPacketBundle bundles one or more packets sharing the same attributes.
+message NetworkPacketBundle {
+  oneof packet_context {
+    // The intern id for looking up the associated packet context.
+    uint64 iid = 1;
+
+    // The inlined context for events in this bundle.
+    NetworkPacketEvent ctx = 2;
+  }
+
+  // The timestamp of the i-th packet encoded as the nanoseconds since the
+  // enclosing TracePacket's timestamp.
+  repeated uint64 packet_timestamps = 3 [packed = true];
+
+  // The length of the i-th packet in bytes (wire_size - L2_header_size).
+  repeated uint32 packet_lengths = 4 [packed = true];
+
+  // Total number of packets in the bundle (when above aggregation_threshold).
+  optional uint32 total_packets = 5;
+
+  // Duration between first and last packet (when above aggregation_threshold).
+  optional uint64 total_duration = 6;
+
+  // Total packet length in bytes (when above aggregation_threshold).
+  optional uint64 total_length = 7;
+}
+
+// An internable packet context.
+message NetworkPacketContext {
+  optional uint64 iid = 1;
+  optional NetworkPacketEvent ctx = 2;
+}
+
 // End of protos/perfetto/trace/android/network_trace.proto
 
 // Begin of protos/perfetto/trace/android/packages_list.proto
@@ -9898,7 +9958,7 @@
 // emitted proactively in advance of referring to them in later packets.
 //
 // Next reserved id: 8 (up to 15).
-// Next id: 30.
+// Next id: 31.
 message InternedData {
   // TODO(eseckler): Replace iid fields inside interned messages with
   // map<iid, message> type fields in InternedData.
@@ -9959,6 +10019,9 @@
 
   // Interned string values in the DebugAnnotation proto.
   repeated InternedString debug_annotation_string_values = 29;
+
+  // Interned packet context for android.network_packets.
+  repeated NetworkPacketContext packet_context = 30;
 }
 
 // End of protos/perfetto/trace/interned_data/interned_data.proto
@@ -11803,7 +11866,7 @@
 // See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details.
 //
 // Next reserved id: 14 (up to 15).
-// Next id: 92.
+// Next id: 93.
 message TracePacket {
   // The timestamp of the TracePacket.
   // By default this timestamps refers to the trace clock (CLOCK_BOOTTIME on
@@ -11912,6 +11975,9 @@
     // Represents a single packet sent or received by the network.
     NetworkPacketEvent network_packet = 88;
 
+    // Represents one or more packets sent or received by the network.
+    NetworkPacketBundle network_packet_bundle = 92;
+
     // The "range of interest" for track events. See the message definition
     // comments for more details.
     TrackEventRangeOfInterest track_event_range_of_interest = 90;
diff --git a/protos/perfetto/trace/trace_packet.proto b/protos/perfetto/trace/trace_packet.proto
index c5c2de5..bb20291 100644
--- a/protos/perfetto/trace/trace_packet.proto
+++ b/protos/perfetto/trace/trace_packet.proto
@@ -94,7 +94,7 @@
 // See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details.
 //
 // Next reserved id: 14 (up to 15).
-// Next id: 92.
+// Next id: 93.
 message TracePacket {
   // The timestamp of the TracePacket.
   // By default this timestamps refers to the trace clock (CLOCK_BOOTTIME on
@@ -203,6 +203,9 @@
     // Represents a single packet sent or received by the network.
     NetworkPacketEvent network_packet = 88;
 
+    // Represents one or more packets sent or received by the network.
+    NetworkPacketBundle network_packet_bundle = 92;
+
     // The "range of interest" for track events. See the message definition
     // comments for more details.
     TrackEventRangeOfInterest track_event_range_of_interest = 90;
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index bf8b56e..ffc882c 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/protos.py b/python/perfetto/trace_processor/protos.py
index 4d0c40e..ea13297 100644
--- a/python/perfetto/trace_processor/protos.py
+++ b/python/perfetto/trace_processor/protos.py
@@ -43,6 +43,8 @@
 
     def create_message_factory(message_type):
       message_desc = self.descriptor_pool.FindMessageTypeByName(message_type)
+      if hasattr(message_factory, 'GetMessageClass'):
+        return message_factory.GetMessageClass(message_desc)
       return message_factory.MessageFactory().GetPrototype(message_desc)
 
     # Create proto messages to correctly communicate with the RPC API by sending
diff --git a/src/trace_processor/importers/proto/frame_timeline_event_parser.cc b/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
index 6a7faf3..0b3e84e 100644
--- a/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
+++ b/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
@@ -82,6 +82,8 @@
     jank_reasons.emplace_back("Unknown Jank");
   if (jank_type & FrameTimelineEvent::JANK_SF_STUFFING)
     jank_reasons.emplace_back("SurfaceFlinger Stuffing");
+  if (jank_type & FrameTimelineEvent::JANK_DROPPED)
+    jank_reasons.emplace_back("Dropped Frame");
 
   std::string jank_str(
       std::accumulate(jank_reasons.begin(), jank_reasons.end(), std::string(),
diff --git a/src/trace_processor/metrics/sql/android/android_frame_timeline_metric.sql b/src/trace_processor/metrics/sql/android/android_frame_timeline_metric.sql
index 44a55a2..40285e6 100644
--- a/src/trace_processor/metrics/sql/android/android_frame_timeline_metric.sql
+++ b/src/trace_processor/metrics/sql/android/android_frame_timeline_metric.sql
@@ -30,7 +30,9 @@
   SUM(jank_type GLOB '*App Deadline Missed*'
     OR jank_type GLOB '*SurfaceFlinger*'
     OR jank_type GLOB '*Prediction Error*'
-    OR jank_type GLOB '*Display HAL*') AS missed_frames,
+    OR jank_type GLOB '*Display HAL*'
+    OR jank_type GLOB '*Dropped Frame*') AS missed_frames,
+  SUM(jank_type GLOB '*Dropped Frame*') AS dropped_frames,
   CAST(PERCENTILE(dur, 50) AS INTEGER) AS frame_dur_p50,
   CAST(PERCENTILE(dur, 90) AS INTEGER) AS frame_dur_p90,
   CAST(PERCENTILE(dur, 95) AS INTEGER) AS frame_dur_p95,
@@ -53,6 +55,7 @@
   AndroidFrameTimelineMetric(
     'total_frames', SUM(total_frames),
     'missed_app_frames', SUM(missed_app_frames),
+    'dropped_frames', SUM(dropped_frames),
     'process', (
       SELECT
         RepeatedField(
@@ -67,6 +70,7 @@
             'frame_dur_p50', frame_dur_p50,
             'frame_dur_p90', frame_dur_p90,
             'frame_dur_p95', frame_dur_p95,
-            'frame_dur_p99', frame_dur_p99))
+            'frame_dur_p99', frame_dur_p99,
+            'dropped_frames', dropped_frames))
       FROM android_frame_timeline_metric_per_process))
 FROM android_frame_timeline_metric_per_process;
diff --git a/test/cts/heapprofd_java_test_cts.cc b/test/cts/heapprofd_java_test_cts.cc
index 40c4aa6..6435f76 100644
--- a/test/cts/heapprofd_java_test_cts.cc
+++ b/test/cts/heapprofd_java_test_cts.cc
@@ -146,10 +146,13 @@
   helper.StartTracing(trace_config);
   StartAppActivity(app_name, "JavaOomActivity", "target.app.running", &task_runner,
                    /*delay_ms=*/100);
+  task_runner.RunUntilCheckpoint("target.app.running", 10000 /*ms*/);
 
-  helper.WaitForTracingDisabled();
-  helper.ReadData();
-  helper.WaitForReadData();
+  if (SupportsOomHeapDump()) {
+    helper.WaitForTracingDisabled();
+    helper.ReadData();
+    helper.WaitForReadData();
+  }
 
   PERFETTO_CHECK(IsAppRunning(app_name));
   StopApp(app_name, "new.app.stopped", &task_runner);
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 d51b12b..56eb2c2 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 @@
-01cee2096f54717e22e8b662477bb3eb55490eca01f1e092e96f998d13696b21
\ No newline at end of file
+6559e421403a5417246d436f86d352aab868a66fd850240589d771bb6f8bf267
\ 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 d4896f7..0556f9d 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 @@
-2e4095665abc92e5b8f1c25a47689af0a0f7af349375e5831f405e18b23fffd8
\ No newline at end of file
+55d04a3a8c16c9afe3298def1cbf8939a3508023f6da2f0b6777427764fdff24
\ 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 341e2ce..43932c6 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 @@
-9df3bd84fdafacbc874214e876624606f4abe55ecbaaa82d3ffbf8d7f0619cce
\ No newline at end of file
+ff165563554a298ef54a9bf1acfcfff80ca09bf869412cd17c22eaa3d73ebd53
\ 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 259f7be..e342913 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 @@
-ab33f88f300df74195d963b0e0f827e9c637716a558cfb42952ea00b727c52b5
\ No newline at end of file
+5799b74804912156abf653b05c3e57598bf8c7719c257567eb9a58689f5fef29
\ 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 0937e8c..9ce11d7 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 @@
-28e6f5a2f066b5dc1c192b9b4386d08f99e532d5c8e6981cbb00f3a521c336a4
\ No newline at end of file
+44a5f57e15b33c714daad868c40ac379b3d0be06e64dba685e8ba36fde4426e3
\ 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 3f65312..956fa6a 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 @@
-61624a8c55db10d0a387100294b8a8b1191bc542f8f4da28a1d0447024689df0
\ No newline at end of file
+a6a3d22d4a437774e6aa555fa046bda49f2ff3d9ddca2eda0e2da96115bd98d7
\ 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 3e6d116..e0d079f 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 @@
-1ce5977375a3c459f0834c1e8114564e7fcb2c03f8e504e4c7558d62019338e5
\ No newline at end of file
+2650426962c62983041501bbd8fd2cad33d4c7734e859ae132885ed1e8f2d305
\ 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 1c4c4a1..e5613f6 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 @@
-f1e10b381882081846e9df1b024e8ad992fa470f0ce137b6fa90e0f0641d8c39
\ No newline at end of file
+94b7078e2c9452112262c365f841b4d504f89f79e745c871316e4235a31caaf9
\ 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 5863d55..b69e976 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 @@
-f3c0069c0ef9643552081835fd06c972b87448b7df5ac538a4d3e213a437bcb1
\ No newline at end of file
+5a0015c3a57443dae62ec68acea089c1dc186c040ea0cb31e2bfbb98213379f8
\ 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 e70add7..fb257d9 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 @@
-04b6ed7959eef3625b2d25d9daa68e7caea3f8ad4846234a45a6c264aebc8e47
\ No newline at end of file
+656dd0f321a2b0d81728ef6144d6d27b86d2a355d212eae82a07723f8bb51363
\ 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 d66c5dd..f5eea2d 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 @@
-e1eaeed835b30f5e575a17c922de4179bc320c5564160af276ced725903795ba
\ No newline at end of file
+1bd377e966ca2a42963f5d0ab22d30aaba00e11c7e56c534010bfd3e27d4066e
\ 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 ac9f60e..d3a37c5 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 @@
-08b1014ccc25e1bad77cccdc58545e212f804f51a75bf6cef233a33cbd835fac
\ No newline at end of file
+111744f4975530816a265893e4eb5213db59be514208e98424061b9aae622806
\ 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 01fb381..f52a7cf 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 @@
-7fa091fa3b7613ea388d12d59501ff6af5eb618bc113d8cc09dee86aee65eb20
\ No newline at end of file
+219f53dbacd26b4e2535b6bc581e2d3face7949546b5a53d52b75ce4d7e2d7e9
\ 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 c9480fb..9f21de4 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 @@
-b5bedbda51c728c7b058ee17c83be9536bcd85770ed2b0c3a3d305aea42b5078
\ No newline at end of file
+13537f7a1c55c131307413033086ff478db706a5f974d801bf3edadd02334d1d
\ 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 d733f51..96f9dbd 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 @@
-d1687347903fbe76615507eac353b0afdd7b1db2d3a8b58aa037b6a1016d3b7f
\ No newline at end of file
+956051d915faf570c30e8c596421d99da77276d130a077eb583425ab43e4dced
\ 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 4ead93a..0d59eb4 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 @@
-ad3a36f843c2b04d2acd7529036f32fdd4c91a5162ce0ce3502bdf27382bd463
\ No newline at end of file
+6c73b73c0e57e071143bf9030b38b9bd88409c3748640c1ebbef22f9c2e38c0c
\ 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 01fb381..f52a7cf 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 @@
-7fa091fa3b7613ea388d12d59501ff6af5eb618bc113d8cc09dee86aee65eb20
\ No newline at end of file
+219f53dbacd26b4e2535b6bc581e2d3face7949546b5a53d52b75ce4d7e2d7e9
\ 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 4ead93a..0d59eb4 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 @@
-ad3a36f843c2b04d2acd7529036f32fdd4c91a5162ce0ce3502bdf27382bd463
\ No newline at end of file
+6c73b73c0e57e071143bf9030b38b9bd88409c3748640c1ebbef22f9c2e38c0c
\ 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 3c37919..88aa4ba 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 @@
-cc6799a71bce86cd5c3b657182a99999337bda963c54a96b36b811051f5309dd
\ No newline at end of file
+1a4de1eb1aafa874a275174348fddd726364b1bfa05fd7179cab5fa4cffa36df
\ 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 45218a3..36df905 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 @@
-48f3843a740514558a711104b8fa156d613c7141fb1021941fd95e1806b0deff
\ No newline at end of file
+b2d299ac7e4695ef8ae62e84595efe5fef03c426b34122680af5ebe8ffe52589
\ 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 2723dae..fdbf575 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 @@
-006f0866f8dbf0181034b3d730627481787a04031a005dd6f0224ccfe30110d7
\ No newline at end of file
+4d52c4eaed29cacf5bf71bb6743b7b6556f6021c7a98d0b4d5beed11bf734b84
\ 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 01fb381..f52a7cf 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 @@
-7fa091fa3b7613ea388d12d59501ff6af5eb618bc113d8cc09dee86aee65eb20
\ No newline at end of file
+219f53dbacd26b4e2535b6bc581e2d3face7949546b5a53d52b75ce4d7e2d7e9
\ 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 4ead93a..0d59eb4 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 @@
-ad3a36f843c2b04d2acd7529036f32fdd4c91a5162ce0ce3502bdf27382bd463
\ No newline at end of file
+6c73b73c0e57e071143bf9030b38b9bd88409c3748640c1ebbef22f9c2e38c0c
\ 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 4ead93a..0d59eb4 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 @@
-ad3a36f843c2b04d2acd7529036f32fdd4c91a5162ce0ce3502bdf27382bd463
\ No newline at end of file
+6c73b73c0e57e071143bf9030b38b9bd88409c3748640c1ebbef22f9c2e38c0c
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/performance/frame_timeline_metric.out b/test/trace_processor/diff_tests/performance/frame_timeline_metric.out
index a121e82..1a06b3c 100644
--- a/test/trace_processor/diff_tests/performance/frame_timeline_metric.out
+++ b/test/trace_processor/diff_tests/performance/frame_timeline_metric.out
@@ -1,6 +1,7 @@
 android_frame_timeline_metric {
-  total_frames: 5
+  total_frames: 6
   missed_app_frames: 3
+  dropped_frames: 1
 
   process {
     process {
@@ -16,6 +17,7 @@
     frame_dur_p90: 33
     frame_dur_p95: 33
     frame_dur_p99: 33
+    dropped_frames: 0
   }
 
   process {
@@ -32,21 +34,23 @@
     frame_dur_p90: 16
     frame_dur_p95: 16
     frame_dur_p99: 16
+    dropped_frames: 0
   }
   
   process {
     process {
       name: "process3"
     }
-    total_frames: 1
-    missed_frames: 1
+    total_frames: 2
+    missed_frames: 2
     missed_app_frames: 0
     missed_sf_frames: 1
-    frame_dur_max: 4
-    frame_dur_avg: 4
-    frame_dur_p50: 4
+    frame_dur_max: 5
+    frame_dur_avg: 2
+    frame_dur_p50: 2
     frame_dur_p90: 4
     frame_dur_p95: 4
     frame_dur_p99: 4
+    dropped_frames: 1
     }
 }
diff --git a/test/trace_processor/diff_tests/performance/frame_timeline_metric.py b/test/trace_processor/diff_tests/performance/frame_timeline_metric.py
index b499eb4..17639a3 100755
--- a/test/trace_processor/diff_tests/performance/frame_timeline_metric.py
+++ b/test/trace_processor/diff_tests/performance/frame_timeline_metric.py
@@ -30,6 +30,7 @@
   JANK_BUFFER_STUFFING = 128
   JANK_UNKNOWN = 256
   JANK_SF_STUFFING = 512
+  JANK_DROPPED = 1024
 
 
 class PresentType:
@@ -123,5 +124,17 @@
     gpu_composition=0,
     jank_type=JankType.JANK_SF_CPU_DEADLINE_MISSED,
     prediction_type=PredictionType.PREDICTION_VALID)
-trace.add_frame_end_event(ts=85, cookie=15)
+trace.add_actual_surface_frame_start_event(
+    ts=90,
+    cookie=15,
+    token=8,
+    display_frame_token=9,
+    pid=1003,
+    layer_name="Layer1",
+    present_type=PresentType.PRESENT_DROPPED,
+    on_time_finish=0,
+    gpu_composition=0,
+    jank_type=JankType.JANK_DROPPED,
+    prediction_type=PredictionType.PREDICTION_VALID)
+trace.add_frame_end_event(ts=95, cookie=15)
 sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/tools/gen_amalgamated_sql.py b/tools/gen_amalgamated_sql.py
index 1ac0ab0..a726002 100755
--- a/tools/gen_amalgamated_sql.py
+++ b/tools/gen_amalgamated_sql.py
@@ -64,8 +64,10 @@
 };
 '''
 
-def filename_to_variable(filename):
-  return "k" + "".join([x.capitalize() for x in filename.split("_")])
+
+def filename_to_variable(filename: str):
+  return "k" + "".join(
+      [x.capitalize() for x in filename.replace(os.path.sep, '_').split("_")])
 
 
 def main():
@@ -114,8 +116,7 @@
 
     # Create the C++ variable for each SQL file.
     for path, sql in sql_outputs.items():
-      name = os.path.basename(path)
-      variable = filename_to_variable(os.path.splitext(name)[0])
+      variable = filename_to_variable(os.path.splitext(path)[0])
       output.write('\nconst char {}[] = '.format(variable))
       # MSVC doesn't like string literals that are individually longer than 16k.
       # However it's still fine "if" "we" "concatenate" "many" "of" "them".
@@ -135,8 +136,7 @@
     # Create mapping of filename to variable name for each variable.
     output.write("\nconst FileToSql kFileToSql[] = {")
     for path in sql_outputs.keys():
-      name = os.path.basename(path)
-      variable = filename_to_variable(os.path.splitext(name)[0])
+      variable = filename_to_variable(os.path.splitext(path)[0])
 
       # This is for Windows which has \ as a path separator.
       path = path.replace("\\", "/")
diff --git a/ui/src/assets/analyze_page.scss b/ui/src/assets/analyze_page.scss
index 4a700a3..fbe9bab 100644
--- a/ui/src/assets/analyze_page.scss
+++ b/ui/src/assets/analyze_page.scss
@@ -15,6 +15,8 @@
 .analyze-page {
   overflow-y: auto;
   overflow-x: hidden;
+  display: grid;
+  grid-template-rows: auto auto 1fr;
   .query-input {
     width: 100%;
     background-color: #111;
@@ -29,5 +31,6 @@
     padding: 0.5em;
     overflow: auto;
     resize: vertical;
+    outline: none;
   }
 }
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 9e26d09..82a9569 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -12,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+@import "widgets/theme";
 @import "typefaces";
 @import "fonts";
 
 :root {
-  --sidebar-width: 256px;
-  --topbar-height: 48px;
+  --sidebar-width: 230px;
+  --topbar-height: 44px;
   --monospace-font: "Roboto Mono", monospace;
   --track-shell-width: 250px;
   --track-border-color: #00000025;
@@ -51,7 +52,7 @@
   overflow-wrap: break-word;
   font-family: "Roboto Condensed", sans-serif;
   font-weight: 300;
-  letter-spacing: -0.25px;
+  letter-spacing: -0.5px;
 }
 
 * {
@@ -175,12 +176,13 @@
   grid-area: alerts;
   background-color: #f2f2f2;
   > div {
-    font-family: "Raleway", sans-serif;
+    font-family: "Roboto", sans-serif;
     font-weight: 400;
     letter-spacing: 0.25px;
     padding: 1rem;
     display: flex;
     justify-content: space-between;
+    align-items: center;
     button {
       width: 24px;
       height: 24px;
@@ -377,7 +379,7 @@
 
   .track-shell {
     @include transition();
-    padding-left: 10px;
+    padding-left: 5px;
     display: grid;
     cursor: grab;
     grid-template-areas: "title buttons";
@@ -408,7 +410,7 @@
 
     .chip {
       background-color: #bed6ff;
-      border-radius: 3px;
+      border-radius: $pf-border-radius;
       font-size: smaller;
       padding: 0 0.1rem;
       margin-left: 1ch;
@@ -441,6 +443,7 @@
       display: flex;
       height: 100%;
       align-items: center;
+      justify-content: center;
 
       &:hover {
         background-color: #ebeef9;
@@ -469,6 +472,7 @@
 }
 
 .details-panel-container {
+  box-shadow: #0000003b 0px 0px 3px 1px;
   position: relative;
   overflow-x: hidden;
   overflow-y: auto;
@@ -533,7 +537,7 @@
 }
 
 .overview-timeline {
-  height: 100px;
+  height: 70px;
 }
 
 .time-axis-panel {
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index c1d112e..00919b8 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -41,14 +41,14 @@
         padding: 3px 10px 0 10px;
         margin-top: 3px;
         font-size: 13px;
-        border-radius: 5px 5px 0 0;
-        background-color: hsla(0, 0%, 75%, 1);
-        border-top: solid 1px hsla(0, 0%, 75%, 1);
-        border-left: solid 1px hsla(0, 0%, 75%, 1);
+        border-radius: 3px 3px 0 0;
+        background-color: #0000000f;
         border-right: solid 1px hsla(0, 0%, 75%, 1);
         overflow: hidden;
         white-space: nowrap;
         text-overflow: ellipsis;
+        z-index: 5;
+        box-shadow: #0000003b 0px 0px 3px 1px;
 
         &[active] {
           background-color: white;
@@ -62,6 +62,10 @@
           cursor: pointer;
           background-color: hsla(0, 0%, 85%, 1);
         }
+
+        &:nth-child(1) {
+          margin-left: 3px;
+        }
       }
     }
 
diff --git a/ui/src/assets/flags_page.scss b/ui/src/assets/flags_page.scss
index a23c60f..d65b678 100644
--- a/ui/src/assets/flags_page.scss
+++ b/ui/src/assets/flags_page.scss
@@ -12,31 +12,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+@import "widgets/theme";
+
 .flags-page {
   overflow-y: scroll;
 }
 
 .flags-content {
-  max-width: 90ch;
+  max-width: 100ch;
   width: 60%;
   margin: 0 auto;
   padding: 3rem;
   display: grid;
-  grid-row-gap: 1rem;
 
   h1 {
     font-size: larger;
+    margin: 1rem 1rem;
   }
 
   button {
     background: none;
     border: 1px solid rgb(218, 220, 224);
-    border-radius: 3px;
+    border-radius: $pf-border-radius;
     color: rgb(25, 103, 210);
     font-size: 0.8125rem;
     padding: 8px 12px;
     cursor: pointer;
     font-weight: 500;
+    margin: 3px 0.5rem;
   }
 }
 
@@ -45,9 +48,9 @@
   grid-template:
     "title control" auto
     "description control" auto / 1fr auto;
-
-  row-gap: 0.5rem;
-  align-content: center;
+  row-gap: 0.3rem;
+  padding: 1rem 1rem;
+  align-items: center;
 
   select {
     grid-area: control;
@@ -70,3 +73,8 @@
     font-size: smaller;
   }
 }
+
+.flag-widget:nth-child(2n+1) {
+  background-color: #0000000a;
+}
+
diff --git a/ui/src/assets/hiring_banner.scss b/ui/src/assets/hiring_banner.scss
index a6807df..3e3b46d 100644
--- a/ui/src/assets/hiring_banner.scss
+++ b/ui/src/assets/hiring_banner.scss
@@ -1,5 +1,5 @@
 .hiring-banner {
-  font-family: "Raleway", sans-serif;
+  font-family: "Roboto", sans-serif;
   font-size: 12px;
   background: #db4634;
   box-shadow: 0 0 3px rgba(0, 0, 0, 30%);
diff --git a/ui/src/assets/home_page.scss b/ui/src/assets/home_page.scss
index 4d3f5a8..f421b83 100644
--- a/ui/src/assets/home_page.scss
+++ b/ui/src/assets/home_page.scss
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+@import "widgets/theme";
+
 .home-page {
   text-align: center;
   align-items: center;
@@ -31,15 +33,15 @@
       font-size: 60px;
       margin: 25px;
       text-align: center;
-      font-family: "Raleway", sans-serif;
-      font-weight: 100;
+      font-family: "Roboto", sans-serif;
+      font-weight: 400;
       color: #333;
     }
 
     .channel-select {
       font-family: "Roboto", sans-serif;
       font-size: 1.2rem;
-      font-weight: 200;
+      font-weight: 400;
       margin-top: 3em;
       --chan-width: 100px;
       --chan-num: 2;
@@ -69,7 +71,7 @@
         padding: 0;
         position: relative;
         background-color: hsl(218, 8%, 30%);
-        border-radius: 3px;
+        border-radius: $pf-border-radius;
         box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.4);
         border: 0;
         width: calc(var(--chan-width) * var(--chan-num));
@@ -88,7 +90,7 @@
         z-index: 2;
         text-transform: uppercase;
         font-size: 16px;
-        font-family: "Raleway";
+        font-family: "Roboto";
         font-weight: 400;
         letter-spacing: 0.3px;
       }
@@ -106,7 +108,7 @@
         left: 0;
         z-index: 1;
         border-radius: inherit;
-        @include transition();
+        @include transition(0.2s);
       }
 
       .home-page-reload {
@@ -114,7 +116,7 @@
         opacity: 0;
         color: #da4534;
         font-weight: 400;
-        @include transition();
+        @include transition(0.2s);
         &.show {
           opacity: 1;
         }
@@ -126,7 +128,7 @@
     grid-area: footer;
     text-decoration: none;
     font-family: "Roboto", sans-serif;
-    font-weight: 200;
+    font-weight: 400;
     color: #333;
     font-size: 15px;
   }
diff --git a/ui/src/assets/metrics_page.scss b/ui/src/assets/metrics_page.scss
index 3ed881c..1487e34 100644
--- a/ui/src/assets/metrics_page.scss
+++ b/ui/src/assets/metrics_page.scss
@@ -12,9 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+@import "widgets/theme";
+
 .metrics-page {
   padding: 30px;
-  font-family: "Raleway", sans-serif;
+  font-family: "Roboto", sans-serif;
   overflow-y: auto;
 
   .metric-run-button {
@@ -23,12 +25,12 @@
     border-radius: 4px;
     padding: 5px 10px;
     font-weight: bold;
-    font-family: "Raleway";
+    font-family: "Roboto";
   }
 
   select {
     margin: 10px;
-    font-family: "Raleway";
+    font-family: "Roboto";
     font-size: 1em;
     border: 1px solid black;
     background-color: #eee;
@@ -39,7 +41,7 @@
     padding: 20px;
     font-family: "Roboto Mono";
     line-height: 1.5em;
-    border-radius: 5px;
+    border-radius: $pf-border-radius;
     overflow-x: auto;
     &.metric-error {
       color: #ef6c00;
diff --git a/ui/src/assets/modal.scss b/ui/src/assets/modal.scss
index 76d3cea..ee575f0 100644
--- a/ui/src/assets/modal.scss
+++ b/ui/src/assets/modal.scss
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+@import "widgets/theme";
+
 // The opacity changes are only transitional. Once the `modalFadeOut` animation
 // reaches the end, the Mithril component that renders .modal-backdrop
 // (and .modal-dialog) is fully destroyed and removed from the DOM.
@@ -57,7 +59,6 @@
 .modal-dialog {
   position: absolute;
   z-index: 100;
-  border: 1px solid #333;
   background-color: #fff;
   margin: auto;
   min-width: 25vw;
@@ -65,7 +66,7 @@
   padding: 30px;
   max-width: 90vw;
   max-height: 90vh;
-  border-radius: 4px;
+  border-radius: $pf-border-radius;
   overflow-y: auto;
   top: 50%;
   left: 50%;
@@ -86,7 +87,7 @@
     h2 {
       margin-top: 0;
       margin-bottom: 0;
-      font-family: "Raleway", sans-serif;
+      font-family: "Roboto", sans-serif;
       font-weight: 600;
       font-size: 1.25rem;
       line-height: 1.25;
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index cb3c14e..5e72a03 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -32,3 +32,4 @@
 @import "widgets/empty_state";
 @import "widgets/anchor";
 @import "widgets/popup";
+@import "widgets/multiselect";
diff --git a/ui/src/assets/record.scss b/ui/src/assets/record.scss
index 529e57e..43cf635 100644
--- a/ui/src/assets/record.scss
+++ b/ui/src/assets/record.scss
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+@import "widgets/theme";
+
 :root {
   --record-text-color: #333;
 }
@@ -30,7 +32,7 @@
   max-width: 900px;
   min-height: 500px;
   margin: auto;
-  border-radius: 5px;
+  border-radius: $pf-border-radius;
   box-shadow: 0 1px 2px 0 #aaa, 0 1px 3px 1px #eee;
   background-color: #fff;
   display: grid;
@@ -53,7 +55,7 @@
     height: 100%;
     text-align: center;
     padding: 180px 30px;
-    font-family: "Raleway", sans-serif;
+    font-family: "Roboto", sans-serif;
     font-size: 25px;
   }
 }
@@ -72,8 +74,6 @@
     flex-direction: row;
 
     .logo-wrapping {
-      border-radius: 50%;
-      background-color: #f0f0f0;
       width: 150px;
       height: 150px;
       display: inline-block;
@@ -81,7 +81,7 @@
       align-self: center;
 
       i.material-icons {
-        color: #0000ff;
+        color: #16161d;
         font-size: 150px;
       }
     }
@@ -127,7 +127,7 @@
 
       input[type="text"] {
         flex-grow: 1;
-        border-radius: 4px;
+        border-radius: $pf-border-radius;
         border: 1px solid #dcdcdc;
         padding: 3px;
         margin: 0 10px;
@@ -169,7 +169,7 @@
 
   .record-modal-button,
   .record-modal-button-high {
-    border-radius: 0.25rem;
+    border-radius: $pf-border-radius;
     border-style: none;
     border-width: 0;
   }
@@ -234,7 +234,7 @@
         margin: 10px;
         text-align: center;
         background-color: #eee;
-        font-family: "Raleway", sans-serif;
+        font-family: "Roboto", sans-serif;
         font-size: 17px;
         @media (max-width: 1280px) {
           font-size: 1.6vw;
@@ -366,7 +366,7 @@
     padding: 0 1em;
     font-size: 15px;
     letter-spacing: 0.5px;
-    font-family: "Raleway", sans-serif;
+    font-family: "Roboto", sans-serif;
     font-weight: 600;
     color: #666;
     display: grid;
@@ -378,12 +378,10 @@
 
     i {
       margin: auto;
-      border-radius: 100%;
       font-size: 32px;
       width: 38px;
       height: 38px;
       padding: 3px;
-      background: #eee;
       grid-area: icon;
     }
 
@@ -476,7 +474,7 @@
     margin-right: 10px;
     text-align: center;
     justify-items: center;
-    font-family: "Raleway", sans-serif;
+    font-family: "Roboto", sans-serif;
     padding: 7px;
 
     &:hover:enabled {
@@ -542,7 +540,7 @@
       }
       &::placeholder {
         color: #b4b7ba;
-        font-family: "Raleway", sans-serif;
+        font-family: "Roboto", sans-serif;
         font-weight: 400;
       }
     }
@@ -564,7 +562,7 @@
 
   > header {
     text-align: center;
-    font-family: "Raleway", sans-serif;
+    font-family: "Roboto", sans-serif;
     font-size: 20px;
     padding: 15px 10px;
     color: #333;
@@ -801,7 +799,7 @@
       margin: 5px;
       text-align: center;
       background-color: #eee;
-      font-family: "Raleway", sans-serif;
+      font-family: "Roboto", sans-serif;
       font-size: 20px;
       @media (max-width: 1280px) {
         font-size: 1.6vw;
@@ -1175,7 +1173,7 @@
         text-align: center;
         margin: 3px;
         background-color: #eee;
-        font-family: "Raleway", sans-serif;
+        font-family: "Roboto", sans-serif;
         flex-grow: 1;
         font-size: 17px;
         @media (max-width: 1280px) {
@@ -1202,7 +1200,7 @@
       border-radius: 10px;
       text-align: center;
       justify-items: center;
-      font-family: "Raleway", sans-serif;
+      font-family: "Roboto", sans-serif;
       padding: 7px;
       background-color: hsl(88, 50%, 67%);
 
diff --git a/ui/src/assets/sidebar.scss b/ui/src/assets/sidebar.scss
index b0a7092..6fedf61 100644
--- a/ui/src/assets/sidebar.scss
+++ b/ui/src/assets/sidebar.scss
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+@import "widgets/theme";
+
 .sidebar {
   --sidebar-padding-bottom: 40px;
   --sidebar-timing: 0.15s;
@@ -38,11 +40,11 @@
     height: var(--topbar-height);
     line-height: var(--topbar-height);
     vertical-align: middle;
-    padding: 0 20px;
+    padding: 0 12px;
     color: #fff;
     overflow: visible;
     .brand {
-      height: 40px;
+      height: 36px;
       margin-top: 4px;
     }
     &::before {
@@ -54,7 +56,7 @@
       position: absolute;
       font-size: 10px;
       line-height: 10px;
-      font-family: "Raleway", sans-serif;
+      font-family: "Roboto", sans-serif;
       left: 155px;
       top: 7px;
     }
@@ -121,17 +123,15 @@
           cursor: pointer;
           > h1,
           > h2 {
-            font-family: "Raleway", sans-serif;
             letter-spacing: 0.25px;
             overflow: hidden;
             text-overflow: ellipsis;
             white-space: nowrap;
-            margin: 0 24px;
+            margin: 0 12px;
           }
           > h1 {
             color: #fff;
             font-size: 15px;
-            font-weight: 500;
           }
           > h2 {
             @include transition();
@@ -189,10 +189,8 @@
           a {
             line-height: 24px;
             font-size: 14px;
-            font-weight: 400;
-            font-family: "Raleway", sans-serif;
             letter-spacing: 0.5px;
-            padding: 5px 24px;
+            padding: 4px 12px;
             text-decoration: none;
             display: block;
             &.pending {
@@ -225,6 +223,7 @@
           }
           .material-icons {
             margin-right: 10px;
+            font-size: 20px
           }
           &:hover {
             background-color: #373f4b;
@@ -265,13 +264,19 @@
     }
 
     > .dbg-info-square {
+      font-family: "Roboto Condensed", sans-serif;
       width: 24px;
-      height: 22px;
-      line-height: 22px;
+      height: 24px;
+      line-height: 24px;
+      display: flex;
+      justify-content: center;
+      flex-direction: column;
+      align-items: center;
+
       margin: 1px 0;
       background: #12161b;
       color: #4e71b3;
-      border-radius: 5px;
+      border-radius: $pf-border-radius;
       font-size: 12px;
       text-align: center;
       &.green {
diff --git a/ui/src/assets/topbar.scss b/ui/src/assets/topbar.scss
index 561c9364..7742b5a 100644
--- a/ui/src/assets/topbar.scss
+++ b/ui/src/assets/topbar.scss
@@ -11,6 +11,9 @@
 // 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 "widgets/theme";
+
 @mixin omnibox-width() {
   width: 90%;
   max-width: 600px;
@@ -22,7 +25,7 @@
   z-index: 3;
   overflow: visible;
   background-color: hsl(215, 1%, 95%);
-  box-shadow: 0 -3px 14px 2px #bbb;
+  box-shadow: 0px 1px 2px 1px #00000026;
   min-height: var(--topbar-height);
   display: flex;
   justify-content: center;
@@ -33,7 +36,7 @@
     display: grid;
     grid-template-areas: "icon input stepthrough";
     grid-template-columns: 34px auto max-content;
-    border-radius: 20px;
+    border-radius: $pf-border-radius;
     background-color: #fcfcfc;
     border: 0;
     line-height: 34px;
@@ -83,14 +86,14 @@
     }
     &.message-mode {
       background-color: hsl(0, 0%, 89%);
-      border-radius: 4px;
+      border-radius: $pf-border-radius;
       input::placeholder {
         font-weight: 400;
         font-family: var(--monospace-font);
         color: hsl(213, 40%, 50%);
       }
       &:before {
-        content: "bubble_chart";
+        content: "info";
       }
     }
     .stepthrough {
diff --git a/ui/src/assets/trace_info_page.scss b/ui/src/assets/trace_info_page.scss
index 69baaaf..3846b7a 100644
--- a/ui/src/assets/trace_info_page.scss
+++ b/ui/src/assets/trace_info_page.scss
@@ -36,7 +36,7 @@
     }
 
     h2 {
-      font-family: "Raleway", sans-serif;
+      font-family: "Roboto", sans-serif;
       font-weight: 400;
       letter-spacing: 0.25px;
       font-size: 2rem;
diff --git a/ui/src/assets/typefaces.scss b/ui/src/assets/typefaces.scss
index d0c4b82..02ef5e8 100644
--- a/ui/src/assets/typefaces.scss
+++ b/ui/src/assets/typefaces.scss
@@ -1,27 +1,5 @@
 /* latin */
 @font-face {
-  font-family: "Raleway";
-  font-style: normal;
-  font-weight: 100;
-  src: url(assets/Raleway-Thin.woff2) format("woff2");
-  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
-    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
-    U+FEFF, U+FFFD;
-}
-
-/* latin */
-@font-face {
-  font-family: "Raleway";
-  font-style: normal;
-  font-weight: 400;
-  src: url(assets/Raleway-Regular.woff2) format("woff2");
-  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
-    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
-    U+FEFF, U+FFFD;
-}
-
-/* latin */
-@font-face {
   font-family: "Roboto";
   font-style: normal;
   font-weight: 100;
diff --git a/ui/src/assets/widgets/empty_state.scss b/ui/src/assets/widgets/empty_state.scss
index 46ab9bc..081bca9 100644
--- a/ui/src/assets/widgets/empty_state.scss
+++ b/ui/src/assets/widgets/empty_state.scss
@@ -23,7 +23,7 @@
 
   & > i {
     margin: auto;
-    font-size: 5em; // Size of the icon is relative to the font size
+    font-size: 5em; // Size of the icon is relative to the font size.
     color: $pf-minimal-foreground;
     margin-bottom: 10px;
   }
@@ -31,6 +31,14 @@
   .pf-empty-state-header {
     margin-bottom: 10px;
     color: $pf-minimal-foreground;
+    text-align: center;
+
+    // Limit width to the size of the container and use no wrap & elipsis to
+    // stop the size getting out of control.
+    max-width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
   }
 
   .pf-empty-state-detail {
diff --git a/ui/src/assets/widgets/multiselect.scss b/ui/src/assets/widgets/multiselect.scss
new file mode 100644
index 0000000..c751d3a
--- /dev/null
+++ b/ui/src/assets/widgets/multiselect.scss
@@ -0,0 +1,56 @@
+// 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.
+
+// Generic list with y-overflow, move me to a common file if we want to reuse.
+.pf-list {
+  overflow-y: auto;
+}
+
+.pf-multiselect-popup {
+  font-family: $pf-font;
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+  width: 280px;
+  max-height: 300px;
+  & > .pf-search-bar {
+    margin-bottom: 8px;
+    display: flex;
+    & > .pf-search-box {
+      flex-grow: 1;
+    }
+  }
+  .pf-multiselect-item {
+    display: block; // Put each item on a new line
+    margin-top: 5px;
+  }
+  .pf-multiselect-header {
+    align-items: baseline;
+    display: flex;
+    position: sticky;
+    top: 0;
+    font-size: 1em;
+    background-color: white;
+    z-index: 1;
+    font-size: 0.75em;
+    border-bottom: solid 1px $pf-minimal-foreground;
+    & > span {
+      margin-right: auto;
+    }
+  }
+  .pf-multiselect-container {
+    position: relative;
+    margin-bottom: 16px;
+  }
+}
diff --git a/ui/src/assets/widgets/theme.scss b/ui/src/assets/widgets/theme.scss
index 3902dc4..2a49642 100644
--- a/ui/src/assets/widgets/theme.scss
+++ b/ui/src/assets/widgets/theme.scss
@@ -15,7 +15,7 @@
 // Standard theme settings for widgets
 
 $pf-font: "Roboto Condensed", sans-serif;
-$pf-border-radius: 3px;
+$pf-border-radius: 2px;
 $pf-anim-timing: 250ms cubic-bezier(0.4, 0, 0.2, 1);
 
 // Here we describe two colour schemes: primary and minimal
diff --git a/ui/src/common/protos.ts b/ui/src/common/protos.ts
index 1fa770d..9443a00 100644
--- a/ui/src/common/protos.ts
+++ b/ui/src/common/protos.ts
@@ -25,6 +25,8 @@
 import ChromeConfig = protos.perfetto.protos.ChromeConfig;
 import TrackEventConfig = protos.perfetto.protos.TrackEventConfig;
 import ConsumerPort = protos.perfetto.protos.ConsumerPort;
+import NetworkPacketTraceConfig =
+    protos.perfetto.protos.NetworkPacketTraceConfig;
 import NativeContinuousDumpConfig =
     protos.perfetto.protos.HeapprofdConfig.ContinuousDumpConfig;
 import JavaContinuousDumpConfig =
@@ -112,6 +114,7 @@
   JavaHprofConfig,
   MeminfoCounters,
   NativeContinuousDumpConfig,
+  NetworkPacketTraceConfig,
   ProcessStatsConfig,
   PerfettoMetatrace,
   ReadBuffersRequest,
diff --git a/ui/src/common/recordingV2/recording_config_utils.ts b/ui/src/common/recordingV2/recording_config_utils.ts
index 6a329b3..2840b3c 100644
--- a/ui/src/common/recordingV2/recording_config_utils.ts
+++ b/ui/src/common/recordingV2/recording_config_utils.ts
@@ -28,6 +28,7 @@
   JavaHprofConfig,
   MeminfoCounters,
   NativeContinuousDumpConfig,
+  NetworkPacketTraceConfig,
   ProcessStatsConfig,
   SysStatsConfig,
   TraceConfig,
@@ -368,6 +369,25 @@
     }
   }
 
+  if (uiCfg.androidNetworkTracing) {
+    if (targetInfo.targetType !== 'CHROME') {
+      const net = new TraceConfig.DataSource();
+      net.config = new DataSourceConfig();
+      net.config.name = 'android.network_packets';
+      net.config.networkPacketTraceConfig = new NetworkPacketTraceConfig();
+      net.config.networkPacketTraceConfig.pollMs =
+          uiCfg.androidNetworkTracingPollMs;
+      protoCfg.dataSources.push(net);
+
+      // Record package info so that Perfetto can display the package name for
+      // network packet events based on the event uid.
+      const pkg = new TraceConfig.DataSource();
+      pkg.config = new DataSourceConfig();
+      pkg.config.name = 'android.packages_list';
+      protoCfg.dataSources.push(pkg);
+    }
+  }
+
   if (uiCfg.chromeLogs) {
     chromeCategories.add('log');
   }
diff --git a/ui/src/controller/record_config_types.ts b/ui/src/controller/record_config_types.ts
index fc306b2..a23acfd 100644
--- a/ui/src/controller/record_config_types.ts
+++ b/ui/src/controller/record_config_types.ts
@@ -53,6 +53,8 @@
   androidLogBuffers: arrayOf(str()),
   androidFrameTimeline: bool(),
   androidGameInterventionList: bool(),
+  androidNetworkTracing: bool(),
+  androidNetworkTracingPollMs: num(250),
 
   cpuCoarse: bool(),
   cpuCoarsePollMs: num(1000),
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index b577210..543c87c 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -93,7 +93,7 @@
 const KERNEL_WAKELOCK_GROUP = 'Kernel wakelocks';
 const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
 const NETWORK_TRACK_GROUP = 'Networking';
-const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency: (.*)$');
+const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
 const ENTITY_RESIDENCY_GROUP = 'Entity residency';
 
 // Sets the default 'scale' for counter tracks. If the regex matches
@@ -632,17 +632,11 @@
     }
   }
 
-  async groupTracksByRegex(
-      regex: RegExp, groupName: string,
-      renameToCapturingGroup?: number): Promise<void> {
+  async groupTracksByRegex(regex: RegExp, groupName: string): Promise<void> {
     let groupUuid = undefined;
 
     for (const track of this.tracksToAdd) {
-      const matches = regex.exec(track.name);
-      if (matches !== null) {
-        if (renameToCapturingGroup) {
-          track.name = matches[renameToCapturingGroup];
-        }
+      if (regex.test(track.name)) {
         if (groupUuid === undefined) {
           groupUuid = uuidv4();
         }
@@ -1695,7 +1689,7 @@
     await this.groupTracksByRegex(KERNEL_WAKELOCK_REGEX, KERNEL_WAKELOCK_GROUP);
     await this.groupTracksByRegex(NETWORK_TRACK_REGEX, NETWORK_TRACK_GROUP);
     await this.groupTracksByRegex(
-        ENTITY_RESIDENCY_REGEX, ENTITY_RESIDENCY_GROUP, 1);
+        ENTITY_RESIDENCY_REGEX, ENTITY_RESIDENCY_GROUP);
 
     // Pre-group all kernel "threads" (actually processes) if this is a linux
     // system trace. Below, addProcessTrackGroups will skip them due to an
diff --git a/ui/src/frontend/icons.ts b/ui/src/frontend/icons.ts
index 0725a41..d30ba54 100644
--- a/ui/src/frontend/icons.ts
+++ b/ui/src/frontend/icons.ts
@@ -20,3 +20,10 @@
 export const EXPAND_UP = 'expand_less';
 
 export const PIN = 'push_pin';
+
+export const LIBRARY_ADD_CHECK = 'library_add_check';
+
+export const SELECT_ALL = 'select_all';
+export const DESELECT = 'deselect';
+
+export const STAR = 'star';
diff --git a/ui/src/frontend/metrics_page.ts b/ui/src/frontend/metrics_page.ts
index 1eabd99..606236f 100644
--- a/ui/src/frontend/metrics_page.ts
+++ b/ui/src/frontend/metrics_page.ts
@@ -17,6 +17,7 @@
 import {Actions} from '../common/actions';
 import {globals} from './globals';
 import {createPage} from './pages';
+import {Button} from './widgets/button';
 
 function getCurrSelectedMetric() {
   const {availableMetrics, selectedIndex} = globals.state.metrics;
@@ -64,9 +65,10 @@
         },
         availableMetrics.map(
             (metric) => m('option', {value: metric, key: metric}, metric))),
-      m('button.metric-run-button',
-        {onclick: () => globals.dispatch(Actions.requestSelectedMetric({}))},
-        'Run'),
+      m(Button, {
+        onclick: () => globals.dispatch(Actions.requestSelectedMetric({})),
+        label: 'Run',
+      }),
     ]);
   }
 }
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 295c893..52cec48 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -232,7 +232,7 @@
     const prevBaseline = ctx.textBaseline;
     ctx.textBaseline = 'alphabetic';
     // Adjust height for icon font.
-    ctx.font = '24px Material Icons';
+    ctx.font = '24px Material Symbols Sharp';
     ctx.fillStyle = color;
     ctx.strokeStyle = color;
     // The ligatures have padding included that means the icon is not drawn
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index adc06d9..2e351f3 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -34,7 +34,7 @@
 import {TimeScale} from './time_scale';
 
 export class OverviewTimelinePanel extends Panel {
-  private static HANDLE_SIZE_PX = 7;
+  private static HANDLE_SIZE_PX = 5;
 
   private width = 0;
   private gesture?: DragGestureHandler;
@@ -79,7 +79,7 @@
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
     if (this.width === undefined) return;
     if (this.timeScale === undefined) return;
-    const headerHeight = 25;
+    const headerHeight = 20;
     const tracksHeight = size.height - headerHeight;
     const timeSpan = new TimeSpan(0, this.totTime.duration);
 
@@ -147,18 +147,18 @@
     ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight);
 
     const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX;
-    const hbarDivisionFactor = 3.5;
+    const hbarHeight = tracksHeight * 0.4;
     // Draw handlebar
     ctx.fillRect(
         vizStartPx - Math.floor(hbarWidth / 2) - 1,
         headerHeight,
         hbarWidth,
-        tracksHeight / hbarDivisionFactor);
+        hbarHeight);
     ctx.fillRect(
         vizEndPx - Math.floor(hbarWidth / 2),
         headerHeight,
         hbarWidth,
-        tracksHeight / hbarDivisionFactor);
+        hbarHeight);
   }
 
   private onMouseMove(e: MouseEvent) {
diff --git a/ui/src/frontend/query_history.ts b/ui/src/frontend/query_history.ts
index e2e503c..a50627a 100644
--- a/ui/src/frontend/query_history.ts
+++ b/ui/src/frontend/query_history.ts
@@ -17,6 +17,8 @@
 import {Actions} from '../common/actions';
 
 import {globals} from './globals';
+import {STAR} from './icons';
+
 import {
   arrayOf,
   bool,
@@ -60,15 +62,17 @@
     return m(
         '.history-item',
         m('.history-item-buttons',
-          m('button',
-            {
-              onclick: () => {
-                queryHistoryStorage.setStarred(
-                    vnode.attrs.index, !vnode.attrs.entry.starred);
-                globals.rafScheduler.scheduleFullRedraw();
+          m(
+              'button',
+              {
+                onclick: () => {
+                  queryHistoryStorage.setStarred(
+                      vnode.attrs.index, !vnode.attrs.entry.starred);
+                  globals.rafScheduler.scheduleFullRedraw();
+                },
               },
-            },
-            m(Icon, {icon: 'star', filled: vnode.attrs.entry.starred})),
+              m(Icon, {icon: STAR, filled: vnode.attrs.entry.starred}),
+              ),
           m('button',
             {
               onclick: () => {
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index d142ee6..d6d574a 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -711,7 +711,7 @@
             m('.sub', 'Buffer mode, size and duration'))),
         m('a[href="#!/record/instructions"]',
           m(`li${routePage === 'instructions' ? '.active' : ''}`,
-            m('i.material-icons.rec', 'fiber_manual_record'),
+            m('i.material-icons-filled.rec', 'fiber_manual_record'),
             m('.title', 'Recording command'),
             m('.sub', 'Manually record trace'))),
         PERSIST_CONFIG_FLAG.get() ?
diff --git a/ui/src/frontend/record_page_v2.ts b/ui/src/frontend/record_page_v2.ts
index 105411d..70ab452 100644
--- a/ui/src/frontend/record_page_v2.ts
+++ b/ui/src/frontend/record_page_v2.ts
@@ -466,7 +466,7 @@
             m('.sub', 'Buffer mode, size and duration'))),
         m('a[href="#!/record/instructions"]',
           m(`li${routePage === 'instructions' ? '.active' : ''}`,
-            m('i.material-icons.rec', 'fiber_manual_record'),
+            m('i.material-icons-filled.rec', 'fiber_manual_record'),
             m('.title', 'Recording command'),
             m('.sub', 'Manually record trace'))),
         PERSIST_CONFIG_FLAG.get() ?
diff --git a/ui/src/frontend/recording/android_settings.ts b/ui/src/frontend/recording/android_settings.ts
index 2536a69..7f9a6e5 100644
--- a/ui/src/frontend/recording/android_settings.ts
+++ b/ui/src/frontend/recording/android_settings.ts
@@ -21,6 +21,8 @@
   DropdownAttrs,
   Probe,
   ProbeAttrs,
+  Slider,
+  SliderAttrs,
   Textarea,
   TextareaAttrs,
   Toggle,
@@ -172,6 +174,23 @@
                     Requires Android 13 (T) or above.`,
           setEnabled: (cfg, val) => cfg.androidGameInterventionList = val,
           isEnabled: (cfg) => cfg.androidGameInterventionList,
-        } as ProbeAttrs));
+        } as ProbeAttrs),
+        m(Probe,
+          {
+            title: 'Network Tracing',
+            img: '',
+            descr: `Records detailed information on network packets.
+                      Requires Android 14 (U) or above.`,
+            setEnabled: (cfg, val) => cfg.androidNetworkTracing = val,
+            isEnabled: (cfg) => cfg.androidNetworkTracing,
+          } as ProbeAttrs,
+          m(Slider, {
+            title: 'Poll interval',
+            cssClass: '.thin',
+            values: [100, 250, 500, 1000, 2500],
+            unit: 'ms',
+            set: (cfg, val) => cfg.androidNetworkTracingPollMs = val,
+            get: (cfg) => cfg.androidNetworkTracingPollMs,
+          } as SliderAttrs)));
   }
 }
diff --git a/ui/src/frontend/recording/reset_target_modal.ts b/ui/src/frontend/recording/reset_target_modal.ts
index a50cce8..5e34c7b 100644
--- a/ui/src/frontend/recording/reset_target_modal.ts
+++ b/ui/src/frontend/recording/reset_target_modal.ts
@@ -69,7 +69,6 @@
       m('.logo-wrapping', m('i.material-icons', 'usb')),
       m('.record-modal-description',
         m('h3', 'Android device over WebUSB'),
-        m('h4', 'JustWorks from the browser with one click'),
         m('text',
           'Android developers: this option cannot co-operate ' +
               'with the adb host on your machine. Only one entity between ' +
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index b76eeed..0aa518a 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -245,7 +245,7 @@
         '.track',
         {
           style: {
-            height: `${Math.max(24, attrs.track.getHeight())}px`,
+            height: `${Math.max(18, attrs.track.getHeight())}px`,
           },
           id: 'track_' + attrs.trackState.id,
         },
diff --git a/ui/src/frontend/widgets/checkbox.ts b/ui/src/frontend/widgets/checkbox.ts
index e9fa7d9..34a8eb8 100644
--- a/ui/src/frontend/widgets/checkbox.ts
+++ b/ui/src/frontend/widgets/checkbox.ts
@@ -25,16 +25,25 @@
   // events will be fired.
   // Defaults to false.
   disabled?: boolean;
+  // Extra classes
+  classes?: string|string[];
   // Remaining attributes forwarded to the underlying HTML <label>.
   [htmlAttrs: string]: any;
 }
 
 export class Checkbox implements m.ClassComponent<CheckboxAttrs> {
   view({attrs}: m.CVnode<CheckboxAttrs>) {
-    const {label, checked, disabled = false, ...htmlAttrs} = attrs;
+    const {
+      label,
+      checked,
+      disabled = false,
+      classes: extraClasses,
+      ...htmlAttrs
+    } = attrs;
 
     const classes = classNames(
         disabled && 'pf-disabled',
+        extraClasses,
     );
 
     // The default checkbox is removed and an entirely new one created inside
diff --git a/ui/src/frontend/widgets/multiselect.ts b/ui/src/frontend/widgets/multiselect.ts
new file mode 100644
index 0000000..84c788b
--- /dev/null
+++ b/ui/src/frontend/widgets/multiselect.ts
@@ -0,0 +1,233 @@
+// 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 * as m from 'mithril';
+import {globals} from '../globals';
+import {DESELECT, SELECT_ALL} from '../icons';
+import {Button} from './button';
+import {Checkbox} from './checkbox';
+import {EmptyState} from './empty_state';
+import {Popup, PopupPosition} from './popup';
+import {TextInput} from './text_input';
+
+export interface Option {
+  // The ID is used to indentify this option, and is used in callbacks.
+  id: string;
+  // This is the name displayed and used for searching.
+  name: string;
+  // Whether the option is selected or not.
+  checked: boolean;
+}
+
+export interface MultiSelectDiff {
+  id: string;
+  checked: boolean;
+}
+
+export interface MultiSelectAttrs {
+  icon?: string;
+  label: string;
+  options: Option[];
+  onChange?: (diffs: MultiSelectDiff[]) => void;
+  repeatCheckedItemsAtTop?: boolean;
+  showNumSelected?: boolean;
+  popupPosition?: PopupPosition;
+}
+
+// A component which shows a list of items with checkboxes, allowing the user to
+// select from the list which ones they want to be selected.
+// Also provides search functionality.
+// This component is entirely controlled and callbacks must be supplied for when
+// the selected items changes, and when the search term changes.
+// There is an optional boolean flag to enable repeating the selected items at
+// the top of the list for easy access - defaults to false.
+export class MultiSelect implements m.ClassComponent<MultiSelectAttrs> {
+  private searchText: string = '';
+  view({attrs}: m.CVnode<MultiSelectAttrs>) {
+    const {
+      icon,
+      popupPosition = PopupPosition.Auto,
+    } = attrs;
+
+    return m(
+        Popup,
+        {
+          trigger: m(Button, {label: this.labelText(attrs), icon}),
+          position: popupPosition,
+        },
+        this.renderPopup(attrs),
+    );
+  }
+
+  private labelText(attrs: MultiSelectAttrs): string {
+    const {
+      options,
+      showNumSelected,
+      label,
+    } = attrs;
+
+    if (showNumSelected) {
+      const numSelected = options.filter(({checked}) => checked).length;
+      return `${label} (${numSelected} selected)`;
+    } else {
+      return label;
+    }
+  }
+
+  private renderPopup(attrs: MultiSelectAttrs) {
+    const {
+      options,
+    } = attrs;
+
+    const filteredItems = options.filter(({name}) => {
+      return name.toLowerCase().includes(this.searchText.toLowerCase());
+    });
+
+    return m(
+        '.pf-multiselect-popup',
+        this.renderSearchBox(),
+        this.renderListOfItems(attrs, filteredItems),
+    );
+  }
+
+  private renderListOfItems(attrs: MultiSelectAttrs, options: Option[]) {
+    const {
+      repeatCheckedItemsAtTop,
+      onChange = () => {},
+    } = attrs;
+    const allChecked = options.every(({checked}) => checked);
+    const anyChecked = options.some(({checked}) => checked);
+
+    if (options.length === 0) {
+      return m(EmptyState, {
+        header: `No results for '${this.searchText}'`,
+      });
+    } else {
+      return [m(
+          '.pf-list',
+          repeatCheckedItemsAtTop && anyChecked &&
+              m(
+                  '.pf-multiselect-container',
+                  m(
+                      '.pf-multiselect-header',
+                      m('span',
+                        this.searchText === '' ? 'Selected' :
+                                                 `Selected (Filtered)`),
+                      m(Button, {
+                        label: 'Clear All',
+                        icon: DESELECT,
+                        minimal: true,
+                        onclick: () => {
+                          const diffs =
+                              options.filter(({checked}) => checked)
+                                  .map(({id}) => ({id, checked: false}));
+                          onChange(diffs);
+                          globals.rafScheduler.scheduleFullRedraw();
+                        },
+                        disabled: !anyChecked,
+                      }),
+                      ),
+                  this.renderOptions(
+                      attrs, options.filter(({checked}) => checked)),
+                  ),
+          m(
+              '.pf-multiselect-container',
+              m(
+                  '.pf-multiselect-header',
+                  m('span',
+                    this.searchText === '' ? 'Options' : `Options (Filtered)`),
+                  m(Button, {
+                    label: 'Select All',
+                    icon: SELECT_ALL,
+                    minimal: true,
+                    onclick: () => {
+                      const diffs = options.filter(({checked}) => !checked)
+                                        .map(({id}) => ({id, checked: true}));
+                      onChange(diffs);
+                      globals.rafScheduler.scheduleFullRedraw();
+                    },
+                    disabled: allChecked,
+                  }),
+                  m(Button, {
+                    label: 'Select None',
+                    icon: DESELECT,
+                    minimal: true,
+                    onclick: () => {
+                      const diffs = options.filter(({checked}) => checked)
+                                        .map(({id}) => ({id, checked: false}));
+                      onChange(diffs);
+                      globals.rafScheduler.scheduleFullRedraw();
+                    },
+                    disabled: !anyChecked,
+                  }),
+                  ),
+              this.renderOptions(attrs, options),
+              ),
+          )];
+    }
+  }
+
+  private renderSearchBox() {
+    return m(
+        '.pf-search-bar',
+        m(TextInput, {
+          oninput: (event: Event) => {
+            const eventTarget = event.target as HTMLTextAreaElement;
+            this.searchText = eventTarget.value;
+            globals.rafScheduler.scheduleFullRedraw();
+          },
+          value: this.searchText,
+          placeholder: 'Search...',
+          extraClasses: 'pf-search-box',
+        }),
+        this.renderClearButton(),
+    );
+  }
+
+  private renderClearButton() {
+    if (this.searchText != '') {
+      return m(Button, {
+        onclick: () => {
+          this.searchText = '';
+          globals.rafScheduler.scheduleFullRedraw();
+        },
+        label: '',
+        icon: 'close',
+        minimal: true,
+      });
+    } else {
+      return null;
+    }
+  }
+
+  private renderOptions(attrs: MultiSelectAttrs, options: Option[]) {
+    const {
+      onChange = () => {},
+    } = attrs;
+
+    return options.map((item) => {
+      const {checked, name, id} = item;
+      return m(Checkbox, {
+        label: name,
+        key: id,  // Prevents transitions jumping between items when searching
+        checked,
+        classes: 'pf-multiselect-item',
+        onchange: () => {
+          onChange([{id, checked: !checked}]);
+          globals.rafScheduler.scheduleFullRedraw();
+        },
+      });
+    });
+  }
+}
diff --git a/ui/src/frontend/widgets/text_input.ts b/ui/src/frontend/widgets/text_input.ts
index cd5f7e8..326dcd2 100644
--- a/ui/src/frontend/widgets/text_input.ts
+++ b/ui/src/frontend/widgets/text_input.ts
@@ -13,9 +13,11 @@
 // limitations under the License.
 
 import * as m from 'mithril';
+import {classNames} from '../classnames';
 
 export interface TextInputAttrs {
   [htmlAttrs: string]: any;
+  extraClasses?: string|string[];
 }
 
 // For now, this component is just a simple wrapper around a plain old input
@@ -25,6 +27,8 @@
 // become more apparent.
 export class TextInput implements m.ClassComponent<TextInputAttrs> {
   view({attrs}: m.CVnode<TextInputAttrs>) {
-    return m('input.pf-text-input', attrs);
+    const {extraClasses = '', ...htmlAttrs} = attrs;
+    const classes = classNames(extraClasses);
+    return m('input.pf-text-input', {class: classes, ...htmlAttrs});
   }
 }
diff --git a/ui/src/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts
index 0a946cc..054ad6d 100644
--- a/ui/src/frontend/widgets_page.ts
+++ b/ui/src/frontend/widgets_page.ts
@@ -17,16 +17,35 @@
 import {Anchor} from './anchor';
 import {classNames} from './classnames';
 import {globals} from './globals';
+import {LIBRARY_ADD_CHECK} from './icons';
 import {createPage} from './pages';
 import {TableShowcase} from './tables/table_showcase';
 import {Button} from './widgets/button';
 import {Checkbox} from './widgets/checkbox';
 import {EmptyState} from './widgets/empty_state';
 import {Icon} from './widgets/icon';
+import {MultiSelect, MultiSelectDiff} from './widgets/multiselect';
 import {Popup, PopupPosition} from './widgets/popup';
 import {Portal} from './widgets/portal';
 import {TextInput} from './widgets/text_input';
 
+const options: {[key: string]: boolean} = {
+  foobar: false,
+  foo: false,
+  bar: false,
+  baz: false,
+  qux: false,
+  quux: false,
+  corge: false,
+  grault: false,
+  garply: false,
+  waldo: false,
+  fred: false,
+  plugh: false,
+  xyzzy: false,
+  thud: false,
+};
+
 function PortalButton() {
   let portalOpen = false;
 
@@ -338,6 +357,34 @@
         m(WidgetShowcase, {
           renderWidget: (opts) => m(Icon, {icon: 'star', ...opts}),
           initialOpts: {filled: false},
-        }));
+        }),
+        m('h2', 'MultiSelect'),
+        m(WidgetShowcase, {
+          renderWidget: ({icon, ...rest}) => m(MultiSelect, {
+            options: Object.entries(options).map(([key, value]) => {
+              return {
+                id: key,
+                name: key,
+                checked: value,
+              };
+            }),
+            popupPosition: PopupPosition.Top,
+            label: 'Multi Select',
+            icon: icon ? LIBRARY_ADD_CHECK : undefined,
+            onChange: (diffs: MultiSelectDiff[]) => {
+              diffs.forEach(({id, checked}) => {
+                options[id] = checked;
+              });
+              globals.rafScheduler.scheduleFullRedraw();
+            },
+            ...rest,
+          }),
+          initialOpts: {
+            icon: true,
+            showNumSelected: true,
+            repeatCheckedItemsAtTop: true,
+          },
+        }),
+    );
   },
 });
diff --git a/ui/src/tracks/chrome_slices/index.ts b/ui/src/tracks/chrome_slices/index.ts
index 00c475a..23f9a5e 100644
--- a/ui/src/tracks/chrome_slices/index.ts
+++ b/ui/src/tracks/chrome_slices/index.ts
@@ -29,7 +29,7 @@
 
 export const SLICE_TRACK_KIND = 'ChromeSliceTrack';
 const SLICE_HEIGHT = 18;
-const TRACK_PADDING = 4;
+const TRACK_PADDING = 2;
 const CHEVRON_WIDTH_PX = 10;
 const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
 const INNER_CHEVRON_OFFSET = -3;
diff --git a/ui/src/tracks/thread_state/index.ts b/ui/src/tracks/thread_state/index.ts
index 475d8b1..3713999 100644
--- a/ui/src/tracks/thread_state/index.ts
+++ b/ui/src/tracks/thread_state/index.ts
@@ -167,8 +167,8 @@
   }
 }
 
-const MARGIN_TOP = 4;
-const RECT_HEIGHT = 14;
+const MARGIN_TOP = 3;
+const RECT_HEIGHT = 12;
 const EXCESS_WIDTH = 10;
 
 class ThreadStateTrack extends Track<Config, Data> {