Merge "Add select widget inspired by material design."
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/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 4912220..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
@@ -3708,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.
@@ -3730,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
@@ -9899,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.
@@ -9960,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
@@ -11804,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
@@ -11913,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/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/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/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/details.scss b/ui/src/assets/details.scss
index 034a967..00919b8 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -198,27 +198,6 @@
         display: inline-flex;
         justify-content: flex-end;
       }
-      button {
-        width: fit-content;
-        height: 20px;
-        padding: 3px;
-        padding-top: 0;
-        margin: 2px;
-        font-size: 12px;
-        opacity: 0.5;
-        &.download {
-          opacity: 1;
-          padding-top: 3px;
-          height: 21px;
-        }
-        &.chosen {
-          opacity: 1;
-        }
-        .material-icons {
-          font-size: 15px;
-          margin-right: 3px;
-        }
-      }
       .title {
         justify-self: start;
         margin-left: 5px;
@@ -280,19 +259,6 @@
     table-layout: auto;
   }
 
-  button {
-    background-color: #262f3c;
-    color: #fff;
-    font-size: 0.875rem;
-    padding-left: 1rem;
-    padding-right: 1rem;
-    padding-top: 0.5rem;
-    padding-bottom: 0.5rem;
-    border-radius: 0.25rem;
-    margin-top: 12px;
-    margin-left: 10px;
-  }
-
   .slice-details-latency-panel {
     // This panel is set to relative to make this panel a positioned element
     // This is to allow the absolute text panels below to be positioned relative
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/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index 278ebda..e46be91 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -40,6 +40,7 @@
 import {Router} from './router';
 import {getCurrentTrace} from './sidebar';
 import {convertTraceToPprofAndDownload} from './trace_converter';
+import {Button} from './widgets/button';
 
 interface FlamegraphDetailsPanelAttrs {}
 
@@ -135,17 +136,14 @@
                     // Required to stop hot-key handling:
                     onkeydown: (e: Event) => e.stopPropagation(),
                   }),
-                  this.profileType === ProfileType.NATIVE_HEAP_PROFILE ||
-                          this.profileType === ProfileType.JAVA_HEAP_SAMPLES ?
-                      m('button.download',
-                        {
-                          onclick: () => {
-                            this.downloadPprof();
-                          },
+                  (this.profileType === ProfileType.NATIVE_HEAP_PROFILE ||
+                   this.profileType === ProfileType.JAVA_HEAP_SAMPLES) &&
+                      m(Button, {
+                        icon: 'file_download',
+                        onclick: () => {
+                          this.downloadPprof();
                         },
-                        m('i.material-icons', 'file_download'),
-                        'Download profile') :
-                      null,
+                      }),
                 ]),
             ]),
           m(`div[style=height:${height}px]`),
@@ -330,19 +328,16 @@
 
   private static buildButtonComponent(
       viewingOption: FlamegraphStateViewingOption, text: string) {
-    const buttonsClass =
-        (globals.state.currentFlamegraphState &&
-         globals.state.currentFlamegraphState.viewingOption === viewingOption) ?
-        '.chosen' :
-        '';
-    return m(
-        `button${buttonsClass}`,
-        {
-          onclick: () => {
-            globals.dispatch(
-                Actions.changeViewFlamegraphState({viewingOption}));
-          },
-        },
-        text);
+    const active =
+        (globals.state.currentFlamegraphState !== null &&
+         globals.state.currentFlamegraphState.viewingOption === viewingOption);
+    return m(Button, {
+      label: text,
+      active,
+      minimal: true,
+      onclick: () => {
+        globals.dispatch(Actions.changeViewFlamegraphState({viewingOption}));
+      },
+    });
   }
 }
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)));
   }
 }