Merge "Trace Redaction - Remove comm value from new task events" into main
diff --git a/Android.bp b/Android.bp
index c136ed5..1cf094b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -11435,6 +11435,7 @@
         "src/trace_processor/importers/common/flow_tracker.cc",
         "src/trace_processor/importers/common/global_args_tracker.cc",
         "src/trace_processor/importers/common/jit_cache.cc",
+        "src/trace_processor/importers/common/machine_tracker.cc",
         "src/trace_processor/importers/common/mapping_tracker.cc",
         "src/trace_processor/importers/common/metadata_tracker.cc",
         "src/trace_processor/importers/common/process_tracker.cc",
@@ -11766,6 +11767,7 @@
         "src/trace_processor/importers/proto/memory_tracker_snapshot_module.cc",
         "src/trace_processor/importers/proto/memory_tracker_snapshot_parser.cc",
         "src/trace_processor/importers/proto/metadata_minimal_module.cc",
+        "src/trace_processor/importers/proto/multi_machine_trace_manager.cc",
         "src/trace_processor/importers/proto/network_trace_module.cc",
         "src/trace_processor/importers/proto/packet_analyzer.cc",
         "src/trace_processor/importers/proto/packet_sequence_state_generation.cc",
diff --git a/BUILD b/BUILD
index 2e137b7..2b53c16 100644
--- a/BUILD
+++ b/BUILD
@@ -1477,6 +1477,8 @@
         "src/trace_processor/importers/common/global_args_tracker.h",
         "src/trace_processor/importers/common/jit_cache.cc",
         "src/trace_processor/importers/common/jit_cache.h",
+        "src/trace_processor/importers/common/machine_tracker.cc",
+        "src/trace_processor/importers/common/machine_tracker.h",
         "src/trace_processor/importers/common/mapping_tracker.cc",
         "src/trace_processor/importers/common/mapping_tracker.h",
         "src/trace_processor/importers/common/metadata_tracker.cc",
@@ -1850,6 +1852,8 @@
         "src/trace_processor/importers/proto/memory_tracker_snapshot_parser.h",
         "src/trace_processor/importers/proto/metadata_minimal_module.cc",
         "src/trace_processor/importers/proto/metadata_minimal_module.h",
+        "src/trace_processor/importers/proto/multi_machine_trace_manager.cc",
+        "src/trace_processor/importers/proto/multi_machine_trace_manager.h",
         "src/trace_processor/importers/proto/network_trace_module.cc",
         "src/trace_processor/importers/proto/network_trace_module.h",
         "src/trace_processor/importers/proto/packet_analyzer.cc",
diff --git a/docs/instrumentation/tracing-sdk.md b/docs/instrumentation/tracing-sdk.md
index ea9393d..a9b0831 100644
--- a/docs/instrumentation/tracing-sdk.md
+++ b/docs/instrumentation/tracing-sdk.md
@@ -30,7 +30,7 @@
 To start using the Client API, first check out the latest SDK release:
 
 ```bash
-git clone https://android.googlesource.com/platform/external/perfetto -b v43.1
+git clone https://android.googlesource.com/platform/external/perfetto -b v44.0
 ```
 
 The SDK consists of two files, `sdk/perfetto.h` and `sdk/perfetto.cc`. These are
diff --git a/examples/sdk/README.md b/examples/sdk/README.md
index 8c53d85..6e5754e 100644
--- a/examples/sdk/README.md
+++ b/examples/sdk/README.md
@@ -15,7 +15,7 @@
 First, check out the latest Perfetto release:
 
 ```bash
-git clone https://android.googlesource.com/platform/external/perfetto -b v43.1
+git clone https://android.googlesource.com/platform/external/perfetto -b v44.0
 ```
 
 Then, build using CMake:
diff --git a/protos/perfetto/trace/ftrace/fastrpc.proto b/protos/perfetto/trace/ftrace/fastrpc.proto
index 2008bae..01ad8f9 100644
--- a/protos/perfetto/trace/ftrace/fastrpc.proto
+++ b/protos/perfetto/trace/ftrace/fastrpc.proto
@@ -10,3 +10,29 @@
   optional int64 len = 2;
   optional uint64 total_allocated = 3;
 }
+message FastrpcDmaFreeFtraceEvent {
+  optional int32 cid = 1;
+  optional uint64 phys = 2;
+  optional uint64 size = 3;
+}
+message FastrpcDmaAllocFtraceEvent {
+  optional int32 cid = 1;
+  optional uint64 phys = 2;
+  optional uint64 size = 3;
+  optional uint64 attr = 4;
+  optional int32 mflags = 5;
+}
+message FastrpcDmaUnmapFtraceEvent {
+  optional int32 cid = 1;
+  optional uint64 phys = 2;
+  optional uint64 size = 3;
+}
+message FastrpcDmaMapFtraceEvent {
+  optional int32 cid = 1;
+  optional int32 fd = 2;
+  optional uint64 phys = 3;
+  optional uint64 size = 4;
+  optional uint64 len = 5;
+  optional uint32 attr = 6;
+  optional int32 mflags = 7;
+}
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index 5d5bf06..302de2a 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -614,5 +614,9 @@
     F2fsBackgroundGcFtraceEvent f2fs_background_gc = 495;
     F2fsGcBeginFtraceEvent f2fs_gc_begin = 496;
     F2fsGcEndFtraceEvent f2fs_gc_end = 497;
+    FastrpcDmaFreeFtraceEvent fastrpc_dma_free = 498;
+    FastrpcDmaAllocFtraceEvent fastrpc_dma_alloc = 499;
+    FastrpcDmaUnmapFtraceEvent fastrpc_dma_unmap = 500;
+    FastrpcDmaMapFtraceEvent fastrpc_dma_map = 501;
   }
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 4988bfd..a821823 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -8339,6 +8339,32 @@
   optional int64 len = 2;
   optional uint64 total_allocated = 3;
 }
+message FastrpcDmaFreeFtraceEvent {
+  optional int32 cid = 1;
+  optional uint64 phys = 2;
+  optional uint64 size = 3;
+}
+message FastrpcDmaAllocFtraceEvent {
+  optional int32 cid = 1;
+  optional uint64 phys = 2;
+  optional uint64 size = 3;
+  optional uint64 attr = 4;
+  optional int32 mflags = 5;
+}
+message FastrpcDmaUnmapFtraceEvent {
+  optional int32 cid = 1;
+  optional uint64 phys = 2;
+  optional uint64 size = 3;
+}
+message FastrpcDmaMapFtraceEvent {
+  optional int32 cid = 1;
+  optional int32 fd = 2;
+  optional uint64 phys = 3;
+  optional uint64 size = 4;
+  optional uint64 len = 5;
+  optional uint32 attr = 6;
+  optional int32 mflags = 7;
+}
 
 // End of protos/perfetto/trace/ftrace/fastrpc.proto
 
@@ -10685,6 +10711,10 @@
     F2fsBackgroundGcFtraceEvent f2fs_background_gc = 495;
     F2fsGcBeginFtraceEvent f2fs_gc_begin = 496;
     F2fsGcEndFtraceEvent f2fs_gc_end = 497;
+    FastrpcDmaFreeFtraceEvent fastrpc_dma_free = 498;
+    FastrpcDmaAllocFtraceEvent fastrpc_dma_alloc = 499;
+    FastrpcDmaUnmapFtraceEvent fastrpc_dma_unmap = 500;
+    FastrpcDmaMapFtraceEvent fastrpc_dma_map = 501;
   }
 }
 
diff --git a/python/perfetto/prebuilts/manifests/trace_processor_shell.py b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
index d71c679..cd0e407 100755
--- a/python/perfetto/prebuilts/manifests/trace_processor_shell.py
+++ b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v43.2
+# This file has been generated by: tools/roll-prebuilts v44.0
 TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8583624,
+        8879352,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-amd64/trace_processor_shell',
     'sha256':
-        'a1c16a74725cefb62406b39538b5d22f56a94e390a0394816d2945793f91f8cf',
+        '8ac591150919d5e3701a3fdb7ce44f2ae9b48a4b27afc2da31b97dba3238c4c8',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7980232,
+        8261544,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-arm64/trace_processor_shell',
     'sha256':
-        '3651654cd462df8a2ec8cb3f7375cee01ccc11861a675b9da0d00aa697efe7b2',
+        '55ef799a383fb460e0167fad68b8f169d6d46bc10285df9db26cecec52dd24f1',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8770200,
+        9035064,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-amd64/trace_processor_shell',
     'sha256':
-        '0796a01af496a6b62623fea89b2d34063ade9d156783e1f88949d8b7ab1f76d0',
+        'd4826f1c2acf0a4caaa167bc089d12a1e6460fcd2847ec8c026933ed73d30540',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        6371036,
+        6581588,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm/trace_processor_shell',
     'sha256':
-        'f4bbef5008de376913c3a95410802d94d8d5715439c3f797be0e4ca8c9bccb1a',
+        '1918aa71521e3daaeedadd9a0760f597c94c3baa803836e442f54c584dc402ac',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8425776,
+        8698104,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm64/trace_processor_shell',
     'sha256':
-        'f1ff5585a06ad8b9fc1d13dbf8d02f39d2804019ea7e70b740872e0f6826695f',
+        '9b4e77e541bf397bcdb4f51b30b0cea23aea761865c5bebdac424b15beecdf18',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,55 +75,55 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        6382140,
+        6581420,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm/trace_processor_shell',
     'sha256':
-        '989b209a108c7d44e2531bb15a5d57f667c717cf774bb8a4a810f99fda0b958d'
+        'cdf7d4f0ad38f977f20d52c2d881f5aeac3ad1a9b07032dee32b08ee631ad041'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8340616,
+        8592256,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm64/trace_processor_shell',
     'sha256':
-        'ade7e72990cb97fd74766cd0df50a24cbd547d2f54c26b49d66236c809922645'
+        '8df9e4e01509184fe5d65f79af343384f8e11f90eabebe8075cf779d6d82304d'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9170488,
+        9457000,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x86/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x86/trace_processor_shell',
     'sha256':
-        '6bd1f74616fd8f620fbf3228f83301844adae08a772b8ac2a64703724a79b516'
+        'd0404998a661864dfabb440a1227ded43c6e793741675576c863460ce869bead'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8591040,
+        8848144,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x64/trace_processor_shell',
     'sha256':
-        '1ccbcb8b2928615cf512cf97eaba395de6f1fc5d70313e884ea3975867f365ea'
+        '8e5a539457f8a50b898a2f2178acd14d7f4076a6a10bbc030a7c2a8cb229b57f'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'trace_processor_shell.exe',
     'file_size':
-        8676352,
+        8951296,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/windows-amd64/trace_processor_shell.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/windows-amd64/trace_processor_shell.exe',
     'sha256':
-        '9125418fedd96eb0e6f1ddeaf46069a05bdcb910592b059669fc982b4fff3f1b',
+        'd01a7d0c3bd460a041fdd25cba88163d945ef939f48de3c76cdc554e19321419',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/python/perfetto/prebuilts/manifests/tracebox.py b/python/perfetto/prebuilts/manifests/tracebox.py
index 159aa28..22cb347 100755
--- a/python/perfetto/prebuilts/manifests/tracebox.py
+++ b/python/perfetto/prebuilts/manifests/tracebox.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v43.2
+# This file has been generated by: tools/roll-prebuilts v44.0
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1564728,
+        1548256,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-amd64/tracebox',
     'sha256':
-        '239736808cbfba5085892e15c145381ea37ddba5df7c8fad97b68d9c04a4d860',
+        '392cb1ae5f11c6a87d15e69fb6576e5c62ae3b1d87a43d68d7fe8bd3cea4fd7e',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1459160,
+        1459096,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-arm64/tracebox',
     'sha256':
-        '4af1449dc90e5505bd5f3d638f11b8bf7e5dc82c0290f0085dc0b335ababd143',
+        '0edde5e3d35ef044848eeca5f63da7fa9f96e4bb3cac5e87ba4e2772a09e8f8f',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2314424,
+        2304304,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-amd64/tracebox',
     'sha256':
-        'a97a5efdaf475f13f4f5947c03289029253f89d0f44caa64765b00b269551297',
+        'cd2b3c0fdc7d0a649bbe4103901263927b2f736836ce56fa06467efa5c825472',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1418968,
+        1408648,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm/tracebox',
     'sha256':
-        '818390305d15730fadcbd87dc3c8d87a439e040a02b5098f51af15dfff3f0ca0',
+        '51794d99493c04ced26a40a242ddb6a53b1213ee96b0b1af9ba874715656ff06',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2221176,
+        2212000,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm64/tracebox',
     'sha256':
-        '5a3cf8c755e08b7a558083a70ad28293baa389e544ebd09806b6a883a5f17952',
+        '7faf03feecf045ed25bfad7cb845621c533ca50b586e722e1bb61aa7fd54cd74',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,42 +75,42 @@
     'file_name':
         'tracebox',
     'file_size':
-        1304280,
+        1299200,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm/tracebox',
     'sha256':
-        '87bb07c7ac4c58d306975cabad3ed5d4b6fe11a8d617dad30fe7dd25bfdc6736'
+        'fa78644befc527481dac76b217762cbddb9233cc050c2a31444c78391ed7715c'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        2076144,
+        2067768,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm64/tracebox',
     'sha256':
-        '501b2bb0cba0ecb770e2b568698f89f6b42d083fcca111c872f7a0e95c0cacc5'
+        'f3167ce57aec78e200675640a683a938bcb206219346427e42f86fdc70923386'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        2253568,
+        2241784,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x86/tracebox',
     'sha256':
-        '28be4f88a9b8f950ebc45a20d4844002f9b3f81ef0230d0a5d9b1627cf89c9a5'
+        '9c9a4b1c498c985c3fbbd9122a9df66bb5275c715c54475d7fbabfec5722d50a'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2101752,
+        2092696,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x64/tracebox',
     'sha256':
-        '6c08b743b9cb6073a75e2b3dd34098e09e3c8bcace89096dc5b9d0f071b2831a'
+        '96fdc7b584247a2f4fc18d4a4a8e8891ab88693c8bc7a301e8dffb3ec8b96a1d'
 }]
diff --git a/python/perfetto/prebuilts/manifests/traceconv.py b/python/perfetto/prebuilts/manifests/traceconv.py
index 85bdfe6..0978c4a 100755
--- a/python/perfetto/prebuilts/manifests/traceconv.py
+++ b/python/perfetto/prebuilts/manifests/traceconv.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v43.2
+# This file has been generated by: tools/roll-prebuilts v44.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        7790424,
+        8069808,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-amd64/traceconv',
     'sha256':
-        'c1d9c50c89545b41af88525dc6f3ce508156ed3787ccecae0ff7c8e736c39318',
+        '7d9c0421235c083932408a5a716372dfddc3a87828b2b3b7e30f8d3aa1c5bf43',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7264824,
+        7529704,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-arm64/traceconv',
     'sha256':
-        'df5349ae462dbd7c1ca9a1b8a0f09c044a47026d6ad8dc24e6945701d7c61a84',
+        'bffadacd2a6e44a9f5c7b0beb48f3f5d568433fd9d425cdee5342e7f3c112cbb',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7885952,
+        8152216,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-amd64/traceconv',
     'sha256':
-        'b2c19364c1fb68e9f5cde610e5d71dd59b9fdf2bada8f7e1eefc319f828f7cb1',
+        'b1815e29aabb51deff0c68e3e690c96aedfea0796a0292d5f177815d33584995',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        5919372,
+        6132076,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm/traceconv',
     'sha256':
-        'b669be326b4b6a024e557e0927f1014fd1ea5d5427e194dc0653f21acac273ee',
+        '2b391081ce9ce45d843584816bc11ba7383b634c88ffa75c7dc927a9632e6d28',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7588200,
+        7862696,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm64/traceconv',
     'sha256':
-        'aff1c4751e721733ce85f58048c17971399fe605a81ac300d306c200d6957818',
+        'd10a598fb6c14926ceb3afb0fc9841a4924c2fedadf9ef981609781ecb8b338b',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,55 +75,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        5931120,
+        6131288,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm/traceconv',
     'sha256':
-        '826212f658fef744fbaeea66331b6fe7ca0152f69cf63ff2ea218a376d5d41d9'
+        '3cf391f42bb51e47159b2236b1171cd1bd4461f3e4576b00100f590cf7ff8b2b'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7546224,
+        7798968,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm64/traceconv',
     'sha256':
-        '29dd7e93e9182c4413a9f9c1c6a6f643f64e1fe0b9657ab1ea3cec8b0bb360c9'
+        '1b17a740ba86a5e218b69dd981d739173515777ee761f3723446f6e400e9367e'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8176528,
+        8464080,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x86/traceconv',
     'sha256':
-        '63c2ebe7ed51f9667bcf69d7b9679f6077db5fd8ee9e1be7b786037e2a649fcb'
+        '2dc045a79276e62f71cf40c1e8ee433125785ce32a223ce4c9e5871cacc3940e'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        7767560,
+        8025896,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x64/traceconv',
     'sha256':
-        'bb9350230c2fac5adf9e6fe21937865b6eaafaefc555ae26e68cae9419ad5ee8'
+        '03db509df8e3816b4c4d78d187d42794b37c3d2c830d85feae0f17a5b581ee53'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        7645696,
+        7920128,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/windows-amd64/traceconv.exe',
     'sha256':
-        '0c84b712941e4f63f74e66731745f94aec3cd30d94469e52cdf1143262f063a4',
+        '97b66259d385a5bd482ebb5a21535b67e3836fb0cf7c971bb36d5f5ea00774bd',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/src/tools/ftrace_proto_gen/event_list b/src/tools/ftrace_proto_gen/event_list
index db13230..3d5ead4 100644
--- a/src/tools/ftrace_proto_gen/event_list
+++ b/src/tools/ftrace_proto_gen/event_list
@@ -492,3 +492,7 @@
 f2fs/f2fs_background_gc
 f2fs/f2fs_gc_begin
 f2fs/f2fs_gc_end
+fastrpc/fastrpc_dma_free
+fastrpc/fastrpc_dma_alloc
+fastrpc/fastrpc_dma_unmap
+fastrpc/fastrpc_dma_map
diff --git a/src/trace_processor/importers/common/BUILD.gn b/src/trace_processor/importers/common/BUILD.gn
index c3ba78a..43dfb23 100644
--- a/src/trace_processor/importers/common/BUILD.gn
+++ b/src/trace_processor/importers/common/BUILD.gn
@@ -39,6 +39,8 @@
     "global_args_tracker.h",
     "jit_cache.cc",
     "jit_cache.h",
+    "machine_tracker.cc",
+    "machine_tracker.h",
     "mapping_tracker.cc",
     "mapping_tracker.h",
     "metadata_tracker.cc",
diff --git a/src/trace_processor/importers/common/event_tracker.cc b/src/trace_processor/importers/common/event_tracker.cc
index 0ed799b..43cf710 100644
--- a/src/trace_processor/importers/common/event_tracker.cc
+++ b/src/trace_processor/importers/common/event_tracker.cc
@@ -65,7 +65,9 @@
   max_timestamp_ = timestamp;
 
   auto* counter_values = context_->storage->mutable_counter_table();
-  return counter_values->Insert({timestamp, track_id, value}).id;
+  return counter_values
+      ->Insert({timestamp, track_id, value, {}, context_->machine_id()})
+      .id;
 }
 
 std::optional<CounterId> EventTracker::PushCounter(
diff --git a/src/trace_processor/importers/common/machine_tracker.cc b/src/trace_processor/importers/common/machine_tracker.cc
new file mode 100644
index 0000000..26e8d4a
--- /dev/null
+++ b/src/trace_processor/importers/common/machine_tracker.cc
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/common/machine_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
+
+namespace perfetto::trace_processor {
+
+MachineTracker::MachineTracker(TraceProcessorContext* context,
+                               uint32_t raw_machine_id)
+    : context_(context) {
+  auto id =
+      context_->storage->mutable_machine_table()->Insert({raw_machine_id}).id;
+
+  if (raw_machine_id)
+    machine_id_ = id;
+}
+MachineTracker::~MachineTracker() = default;
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/machine_tracker.h b/src/trace_processor/importers/common/machine_tracker.h
new file mode 100644
index 0000000..1909311
--- /dev/null
+++ b/src/trace_processor/importers/common/machine_tracker.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_MACHINE_TRACKER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_MACHINE_TRACKER_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor {
+
+// Tracks information in the machine table.
+class MachineTracker {
+ public:
+  MachineTracker(TraceProcessorContext* contex, uint32_t raw_machine_id);
+  ~MachineTracker();
+
+  std::optional<MachineId> machine_id() const { return machine_id_; }
+
+ private:
+  std::optional<MachineId> machine_id_;
+  TraceProcessorContext* const context_;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_MACHINE_TRACKER_H_
diff --git a/src/trace_processor/importers/common/process_tracker.cc b/src/trace_processor/importers/common/process_tracker.cc
index 473c48e..910cc25 100644
--- a/src/trace_processor/importers/common/process_tracker.cc
+++ b/src/trace_processor/importers/common/process_tracker.cc
@@ -32,15 +32,25 @@
   // tid0/pid0 to utid0/upid0. If other types of traces refer to tid0/pid0,
   // then they will get their own non-zero utid/upid, so that those threads are
   // still surfaced in embedder UIs.
-  tables::ThreadTable::Row thread_row;
-  thread_row.tid = 0u;
-  thread_row.upid = 0u;
-  thread_row.is_main_thread = true;
-  context_->storage->mutable_thread_table()->Insert(thread_row);
-
+  //
+  // Note on multi-machine tracing: utid/upid of the swapper process of
+  // secondary machine will not be 0. The ProcessTracker needs to insert to the
+  // thread and process tables to reserve utid and upid.
   tables::ProcessTable::Row process_row;
   process_row.pid = 0u;
-  context_->storage->mutable_process_table()->Insert(process_row);
+  process_row.machine_id = context_->machine_id();
+  auto upid =
+      context_->storage->mutable_process_table()->Insert(process_row).row;
+
+  tables::ThreadTable::Row thread_row;
+  thread_row.tid = 0u;
+  thread_row.upid = upid;  // The swapper upid may be != 0 for remote machines.
+  thread_row.is_main_thread = true;
+  thread_row.machine_id = context_->machine_id();
+  auto utid = context_->storage->mutable_thread_table()->Insert(thread_row).row;
+
+  swapper_upid_ = upid;
+  swapper_utid_ = utid;
 
   // An element to match the reserved tid = 0.
   thread_name_priorities_.push_back(ThreadNamePriority::kOther);
@@ -53,12 +63,20 @@
   tables::ThreadTable::Row row;
   row.tid = tid;
   row.start_ts = timestamp;
+  row.machine_id = context_->machine_id();
 
   auto* thread_table = context_->storage->mutable_thread_table();
   UniqueTid new_utid = thread_table->Insert(row).row;
   tids_[tid].emplace_back(new_utid);
-  PERFETTO_DCHECK(thread_name_priorities_.size() == new_utid);
-  thread_name_priorities_.push_back(ThreadNamePriority::kOther);
+
+  if (PERFETTO_UNLIKELY(thread_name_priorities_.size() <= new_utid)) {
+    // This condition can happen in a multi-machine tracing session:
+    // Machine 1 gets utid 0, 1
+    // Machine 2 gets utid 2, 3
+    // Machine 1 gets utid 4: where thread_name_priorities_.size() == 2.
+    thread_name_priorities_.resize(new_utid + 1);
+  }
+  thread_name_priorities_[new_utid] = ThreadNamePriority::kOther;
   return new_utid;
 }
 
@@ -107,7 +125,8 @@
 
   // Ensure that the tid matches the tid we were looking for.
   PERFETTO_DCHECK(threads->tid()[utid] == tid);
-
+  // Ensure that the thread's machine ID matches the context's machine ID.
+  PERFETTO_DCHECK(threads->machine_id()[utid] == context_->machine_id());
   // If the thread is being tracked by the process tracker, it should not be
   // known to have ended.
   PERFETTO_DCHECK(!threads->end_ts()[utid].has_value());
@@ -135,6 +154,13 @@
     return;
 
   auto* thread_table = context_->storage->mutable_thread_table();
+  if (PERFETTO_UNLIKELY(thread_name_priorities_.size() <= utid)) {
+    // This condition can happen in a multi-machine tracing session:
+    // Machine 1 gets utid 0, 1
+    // Machine 2 gets utid 2, 3
+    // Machine 1 gets utid 4: where thread_name_priorities_.size() == 2.
+    thread_name_priorities_.resize(utid + 1);
+  }
   if (priority >= thread_name_priorities_[utid]) {
     thread_table->mutable_name()->Set(utid, thread_name_id);
     thread_name_priorities_[utid] = priority;
@@ -214,6 +240,8 @@
   // If no matching thread was found, create a new one.
   UniqueTid utid = opt_utid ? *opt_utid : StartNewThread(std::nullopt, tid);
   PERFETTO_DCHECK(thread_table->tid()[utid] == tid);
+  // Ensure that the thread's machine ID matches the context's machine ID.
+  PERFETTO_DCHECK(thread_table->machine_id()[utid] == context_->machine_id());
 
   // Find matching process or create new one.
   if (!thread_table->upid()[utid].has_value()) {
@@ -383,6 +411,7 @@
 
   tables::ProcessTable::Row row;
   row.pid = pid;
+  row.machine_id = context_->machine_id();
 
   UniquePid upid = process_table->Insert(row).row;
   *it_and_ins.first = upid;  // Update the newly inserted hashmap entry.
@@ -507,11 +536,13 @@
 }
 
 void ProcessTracker::SetPidZeroIsUpidZeroIdleProcess() {
-  // Create a mapping from (t|p)id 0 -> u(t|p)id 0 for the idle process.
-  tids_.Insert(0, std::vector<UniqueTid>{0});
-  pids_.Insert(0, UniquePid{0});
-
   auto swapper_id = context_->storage->InternString("swapper");
+
+  // Create a mapping from (t|p)id 0 -> u(t|p)id for the idle process.
+  tids_.Insert(0, std::vector<UniqueTid>{swapper_utid_});
+  pids_.Insert(0, swapper_upid_);
+
+  // Use null StringId for the swapper process/thread.
   UpdateThreadName(0, swapper_id, ThreadNamePriority::kTraceProcessorConstant);
 }
 
diff --git a/src/trace_processor/importers/common/process_tracker.h b/src/trace_processor/importers/common/process_tracker.h
index df648c2..3808f99 100644
--- a/src/trace_processor/importers/common/process_tracker.h
+++ b/src/trace_processor/importers/common/process_tracker.h
@@ -189,6 +189,10 @@
                               uint32_t tid,
                               std::vector<uint32_t> nstid);
 
+  // The UniqueTid of the swapper thread, is 0 for the default machine and is
+  // > 0 for remote machines.
+  UniqueTid swapper_utid() const { return swapper_utid_; }
+
  private:
   // Returns the utid of a thread having |tid| and |pid| as the parent process.
   // pid == std::nullopt matches all processes.
@@ -253,6 +257,9 @@
   // Keeps track pid-namespaced processes, keyed by root-level pids.
   std::unordered_map<uint32_t /* pid (aka tgid) */, NamespacedProcess>
       namespaced_processes_;
+
+  UniquePid swapper_upid_ = 0;
+  UniqueTid swapper_utid_ = 0;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/sched_event_tracker.h b/src/trace_processor/importers/common/sched_event_tracker.h
index 3eba6ae..12aa4c0 100644
--- a/src/trace_processor/importers/common/sched_event_tracker.h
+++ b/src/trace_processor/importers/common/sched_event_tracker.h
@@ -45,8 +45,9 @@
     // just switched to. Set the duration to -1, to indicate that the event is
     // not finished. Duration will be updated later after event finish.
     auto* sched = context_->storage->mutable_sched_slice_table();
-    auto row_and_id = sched->Insert(
-        {ts, /* duration */ -1, cpu, next_utid, kNullStringId, next_prio});
+    auto row_and_id =
+        sched->Insert({ts, /* duration */ -1, cpu, next_utid, kNullStringId,
+                       next_prio, context_->machine_id()});
     SchedId sched_id = row_and_id.id;
     return *sched->id().IndexOf(sched_id);
   }
diff --git a/src/trace_processor/importers/common/thread_state_tracker.cc b/src/trace_processor/importers/common/thread_state_tracker.cc
index 9c3ee3c..37d26d1 100644
--- a/src/trace_processor/importers/common/thread_state_tracker.cc
+++ b/src/trace_processor/importers/common/thread_state_tracker.cc
@@ -15,14 +15,17 @@
  */
 
 #include "src/trace_processor/importers/common/thread_state_tracker.h"
+#include <cstdint>
 #include <optional>
+#include "src/trace_processor/importers/common/process_tracker.h"
 
 namespace perfetto {
 namespace trace_processor {
-ThreadStateTracker::ThreadStateTracker(TraceStorage* storage)
-    : storage_(storage),
-      running_string_id_(storage->InternString("Running")),
-      runnable_string_id_(storage->InternString("R")) {}
+ThreadStateTracker::ThreadStateTracker(TraceProcessorContext* context)
+    : storage_(context->storage.get()),
+      context_(context),
+      running_string_id_(storage_->InternString("Running")),
+      runnable_string_id_(storage_->InternString("R")) {}
 ThreadStateTracker::~ThreadStateTracker() = default;
 
 void ThreadStateTracker::PushSchedSwitchEvent(int64_t event_ts,
@@ -124,9 +127,9 @@
                                       std::optional<uint16_t> cpu,
                                       std::optional<UniqueTid> waker_utid,
                                       std::optional<uint16_t> common_flags) {
-  // Ignore utid 0 because it corresponds to the swapper thread which doesn't
-  // make sense to insert.
-  if (utid == 0)
+  // Ignore the swapper utid because it corresponds to the swapper thread which
+  // doesn't make sense to insert.
+  if (utid == context_->process_tracker->swapper_utid())
     return;
 
   // Insert row with unfinished state
@@ -137,6 +140,7 @@
   row.dur = -1;
   row.utid = utid;
   row.state = state;
+  row.machine_id = context_->machine_id();
   if (common_flags.has_value()) {
     row.irq_context = CommonFlagsToIrqContext(*common_flags);
   }
diff --git a/src/trace_processor/importers/common/thread_state_tracker.h b/src/trace_processor/importers/common/thread_state_tracker.h
index 2b7206b..9a78ada 100644
--- a/src/trace_processor/importers/common/thread_state_tracker.h
+++ b/src/trace_processor/importers/common/thread_state_tracker.h
@@ -28,14 +28,13 @@
 // waking events and blocking reasons.
 class ThreadStateTracker : public Destructible {
  public:
-  explicit ThreadStateTracker(TraceStorage*);
+  explicit ThreadStateTracker(TraceProcessorContext*);
   ThreadStateTracker(const ThreadStateTracker&) = delete;
   ThreadStateTracker& operator=(const ThreadStateTracker&) = delete;
   ~ThreadStateTracker() override;
   static ThreadStateTracker* GetOrCreate(TraceProcessorContext* context) {
     if (!context->thread_state_tracker) {
-      context->thread_state_tracker.reset(
-          new ThreadStateTracker(context->storage.get()));
+      context->thread_state_tracker.reset(new ThreadStateTracker(context));
     }
     return static_cast<ThreadStateTracker*>(
         context->thread_state_tracker.get());
@@ -90,6 +89,7 @@
   }
 
   TraceStorage* const storage_;
+  TraceProcessorContext* const context_;
 
   // Strings
   StringId running_string_id_;
diff --git a/src/trace_processor/importers/common/thread_state_tracker_unittest.cc b/src/trace_processor/importers/common/thread_state_tracker_unittest.cc
index ea6794a..153b02b 100644
--- a/src/trace_processor/importers/common/thread_state_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/thread_state_tracker_unittest.cc
@@ -20,6 +20,7 @@
 
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/global_args_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "test/gtest_and_gmock.h"
 
@@ -41,10 +42,11 @@
  public:
   ThreadStateTrackerUnittest() {
     context_.storage.reset(new TraceStorage());
+    context_.process_tracker.reset(new ProcessTracker(&context_));
     context_.global_args_tracker.reset(
         new GlobalArgsTracker(context_.storage.get()));
     context_.args_tracker.reset(new ArgsTracker(&context_));
-    tracker_.reset(new ThreadStateTracker(context_.storage.get()));
+    tracker_.reset(new ThreadStateTracker(&context_));
   }
 
   StringId StringIdOf(const char* s) {
diff --git a/src/trace_processor/importers/common/track_tracker.cc b/src/trace_processor/importers/common/track_tracker.cc
index 2163b80..ca5ffbd 100644
--- a/src/trace_processor/importers/common/track_tracker.cc
+++ b/src/trace_processor/importers/common/track_tracker.cc
@@ -70,6 +70,7 @@
 
   tables::ThreadTrackTable::Row row;
   row.utid = utid;
+  row.machine_id = context_->machine_id();
   auto id = context_->storage->mutable_thread_track_table()->Insert(row).id;
   thread_tracks_[utid] = id;
   return id;
@@ -82,6 +83,7 @@
 
   tables::ProcessTrackTable::Row row;
   row.upid = upid;
+  row.machine_id = context_->machine_id();
   auto id = context_->storage->mutable_process_track_table()->Insert(row).id;
   process_tracks_[upid] = id;
   return id;
@@ -102,6 +104,7 @@
 
   tables::CpuTrackTable::Row row(name);
   row.cpu = cpu;
+  row.machine_id = context_->machine_id();
   auto id = context_->storage->mutable_cpu_track_table()->Insert(row).id;
   cpu_tracks_[std::make_pair(name, cpu)] = id;
 
@@ -115,7 +118,9 @@
   if (it != gpu_tracks_.end())
     return it->second;
 
-  auto id = context_->storage->mutable_gpu_track_table()->Insert(row).id;
+  auto row_copy = row;
+  row_copy.machine_id = context_->machine_id();
+  auto id = context_->storage->mutable_gpu_track_table()->Insert(row_copy).id;
   gpu_tracks_[tuple] = id;
   return id;
 }
@@ -163,6 +168,7 @@
   // the ID's scope is global.
   tables::ProcessTrackTable::Row track(name);
   track.upid = upid;
+  track.machine_id = context_->machine_id();
   TrackId id =
       context_->storage->mutable_process_track_table()->Insert(track).id;
   chrome_tracks_[tuple] = id;
@@ -179,6 +185,7 @@
 
 TrackId TrackTracker::CreateGlobalAsyncTrack(StringId name, StringId source) {
   tables::TrackTable::Row row(name);
+  row.machine_id = context_->machine_id();
   auto id = context_->storage->mutable_track_table()->Insert(row).id;
   if (!source.is_null()) {
     context_->args_tracker->AddArgsTo(id).AddArg(source_key_,
@@ -192,6 +199,7 @@
                                               StringId source) {
   tables::ProcessTrackTable::Row row(name);
   row.upid = upid;
+  row.machine_id = context_->machine_id();
   auto id = context_->storage->mutable_process_track_table()->Insert(row).id;
   if (!source.is_null()) {
     context_->args_tracker->AddArgsTo(id).AddArg(source_key_,
@@ -207,6 +215,7 @@
 
   tables::ProcessTrackTable::Row row;
   row.upid = upid;
+  row.machine_id = context_->machine_id();
   auto id = context_->storage->mutable_process_track_table()->Insert(row).id;
   chrome_process_instant_tracks_[upid] = id;
 
@@ -218,8 +227,10 @@
 
 TrackId TrackTracker::GetOrCreateLegacyChromeGlobalInstantTrack() {
   if (!chrome_global_instant_track_id_) {
+    tables::TrackTable::Row row;
+    row.machine_id = context_->machine_id();
     chrome_global_instant_track_id_ =
-        context_->storage->mutable_track_table()->Insert({}).id;
+        context_->storage->mutable_track_table()->Insert(row).id;
 
     context_->args_tracker->AddArgsTo(*chrome_global_instant_track_id_)
         .AddArg(source_key_, Variadic::String(chrome_source_));
@@ -233,6 +244,7 @@
   }
   tables::TrackTable::Row row;
   row.name = context_->storage->InternString("Trace Triggers");
+  row.machine_id = context_->machine_id();
   trigger_track_id_ = context_->storage->mutable_track_table()->Insert(row).id;
   return *trigger_track_id_;
 }
@@ -251,6 +263,7 @@
   row.parent_id = InternTrackForGroup(group);
   row.unit = unit;
   row.description = description;
+  row.machine_id = context_->machine_id();
   TrackId track =
       context_->storage->mutable_counter_track_table()->Insert(row).id;
   global_counter_tracks_by_name_[name] = track;
@@ -269,6 +282,7 @@
 
   tables::CpuCounterTrackTable::Row row(name);
   row.cpu = cpu;
+  row.machine_id = context_->machine_id();
 
   TrackId track =
       context_->storage->mutable_cpu_counter_track_table()->Insert(row).id;
@@ -284,6 +298,7 @@
 
   tables::ThreadCounterTrackTable::Row row(name);
   row.utid = utid;
+  row.machine_id = context_->machine_id();
 
   TrackId track =
       context_->storage->mutable_thread_counter_track_table()->Insert(row).id;
@@ -304,6 +319,7 @@
   row.upid = upid;
   row.unit = unit;
   row.description = description;
+  row.machine_id = context_->machine_id();
 
   TrackId track =
       context_->storage->mutable_process_counter_track_table()->Insert(row).id;
@@ -319,6 +335,7 @@
 
   tables::IrqCounterTrackTable::Row row(name);
   row.irq = irq;
+  row.machine_id = context_->machine_id();
 
   TrackId track =
       context_->storage->mutable_irq_counter_track_table()->Insert(row).id;
@@ -335,6 +352,7 @@
 
   tables::SoftirqCounterTrackTable::Row row(name);
   row.softirq = softirq;
+  row.machine_id = context_->machine_id();
 
   TrackId track =
       context_->storage->mutable_softirq_counter_track_table()->Insert(row).id;
@@ -364,6 +382,7 @@
   row.consumer_id = consumer_id;
   row.consumer_type = consumer_type;
   row.ordinal = ordinal;
+  row.machine_id = context_->machine_id();
   TrackId track =
       context_->storage->mutable_energy_counter_track_table()->Insert(row).id;
   energy_counter_tracks_[std::make_pair(name, consumer_id)] = track;
@@ -381,6 +400,7 @@
   tables::EnergyPerUidCounterTrackTable::Row row(name);
   row.consumer_id = consumer_id;
   row.uid = uid;
+  row.machine_id = context_->machine_id();
   TrackId track =
       context_->storage->mutable_energy_per_uid_counter_track_table()
           ->Insert(row)
@@ -410,6 +430,7 @@
   row.gpu_id = gpu_id;
   row.description = description;
   row.unit = unit;
+  row.machine_id = context_->machine_id();
 
   return context_->storage->mutable_gpu_counter_track_table()->Insert(row).id;
 }
@@ -422,6 +443,7 @@
   row.perf_session_id = perf_session_id;
   row.cpu = cpu;
   row.is_timebase = is_timebase;
+  row.machine_id = context_->machine_id();
   return context_->storage->mutable_perf_counter_track_table()->Insert(row).id;
 }
 
@@ -433,7 +455,9 @@
   }
 
   StringId id = context_->storage->InternString(GetNameForGroup(group));
-  TrackId track_id = context_->storage->mutable_track_table()->Insert({id}).id;
+  tables::TrackTable::Row row{id};
+  row.machine_id = context_->machine_id();
+  TrackId track_id = context_->storage->mutable_track_table()->Insert(row).id;
   group_track_ids_[group_idx] = track_id;
   return track_id;
 }
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index b648907..4390a71 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -1264,9 +1264,10 @@
   protos::pbzero::GenericFtraceEvent::Decoder evt(blob.data, blob.size);
   StringId event_id = context_->storage->InternString(evt.event_name());
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(tid);
-  RawId id = context_->storage->mutable_ftrace_event_table()
-                 ->Insert({ts, event_id, cpu, utid})
-                 .id;
+  RawId id =
+      context_->storage->mutable_ftrace_event_table()
+          ->Insert({ts, event_id, cpu, utid, {}, {}, context_->machine_id()})
+          .id;
   auto inserter = context_->args_tracker->AddArgsTo(id);
 
   for (auto it = evt.field(); it; ++it) {
@@ -1305,10 +1306,15 @@
   FtraceMessageDescriptor* m = GetMessageDescriptorForId(ftrace_id);
   const auto& message_strings = ftrace_message_strings_[ftrace_id];
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(tid);
-  RawId id =
-      context_->storage->mutable_ftrace_event_table()
-          ->Insert({timestamp, message_strings.message_name_id, cpu, utid})
-          .id;
+  RawId id = context_->storage->mutable_ftrace_event_table()
+                 ->Insert({timestamp,
+                           message_strings.message_name_id,
+                           cpu,
+                           utid,
+                           {},
+                           {},
+                           context_->machine_id()})
+                 .id;
   auto inserter = context_->args_tracker->AddArgsTo(id);
 
   for (auto fld = decoder.ReadField(); fld.valid(); fld = decoder.ReadField()) {
diff --git a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
index 1a4f5d7..2c0a962 100644
--- a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
@@ -241,6 +241,7 @@
     row.cpu = cpu;
     row.utid = curr_utid;
     row.common_flags = common_flags;
+    row.machine_id = context_->machine_id();
 
     // Add an entry to the raw table.
     RawId id = context_->storage->mutable_ftrace_event_table()->Insert(row).id;
@@ -281,7 +282,13 @@
     // Push the raw event - this is done as the raw ftrace event codepath does
     // not insert sched_switch.
     RawId id = context_->storage->mutable_ftrace_event_table()
-                   ->Insert({ts, sched_switch_id_, cpu, prev_utid})
+                   ->Insert({ts,
+                             sched_switch_id_,
+                             cpu,
+                             prev_utid,
+                             {},
+                             {},
+                             context_->machine_id()})
                    .id;
 
     // Note: this ordering is important. The events should be pushed in the same
diff --git a/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc b/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
index 9d678d1..cf38ac2 100644
--- a/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
@@ -21,6 +21,7 @@
 #include "perfetto/protozero/proto_decoder.h"
 #include "perfetto/protozero/proto_utils.h"
 #include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
@@ -280,7 +281,8 @@
   }
 
   context_->sorter->PushFtraceEvent(cpu, *timestamp, std::move(event),
-                                    state->current_generation());
+                                    state->current_generation(),
+                                    context_->machine_id());
 }
 
 PERFETTO_ALWAYS_INLINE
@@ -340,7 +342,8 @@
       DlogWithLimit(timestamp.status());
       return;
     }
-    context_->sorter->PushInlineFtraceEvent(cpu, *timestamp, event);
+    context_->sorter->PushInlineFtraceEvent(cpu, *timestamp, event,
+                                            context_->machine_id());
   }
 
   // Check that all packed buffers were decoded correctly, and fully.
@@ -396,7 +399,8 @@
       DlogWithLimit(timestamp.status());
       return;
     }
-    context_->sorter->PushInlineFtraceEvent(cpu, *timestamp, event);
+    context_->sorter->PushInlineFtraceEvent(cpu, *timestamp, event,
+                                            context_->machine_id());
   }
 
   // Check that all packed buffers were decoded correctly, and fully.
@@ -461,7 +465,8 @@
   }
 
   context_->sorter->PushFtraceEvent(cpu, *timestamp, std::move(event),
-                                    state->current_generation());
+                                    state->current_generation(),
+                                    context_->machine_id());
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc b/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
index 07e60a4..86db07b 100644
--- a/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
+++ b/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
@@ -40,6 +40,7 @@
   tables::GpuWorkPeriodTrackTable::Row track(gpu_work_period_id_);
   track.uid = static_cast<int32_t>(evt.uid());
   track.gpu_id = evt.gpu_id();
+  track.machine_id = context_->machine_id();
   TrackId track_id = context_->track_tracker->InternGpuWorkPeriodTrack(track);
 
   const int64_t duration =
diff --git a/src/trace_processor/importers/proto/BUILD.gn b/src/trace_processor/importers/proto/BUILD.gn
index 03b0399..0f2f53a 100644
--- a/src/trace_processor/importers/proto/BUILD.gn
+++ b/src/trace_processor/importers/proto/BUILD.gn
@@ -34,6 +34,8 @@
     "memory_tracker_snapshot_parser.h",
     "metadata_minimal_module.cc",
     "metadata_minimal_module.h",
+    "multi_machine_trace_manager.cc",
+    "multi_machine_trace_manager.h",
     "network_trace_module.cc",
     "network_trace_module.h",
     "packet_analyzer.cc",
diff --git a/src/trace_processor/importers/proto/additional_modules.cc b/src/trace_processor/importers/proto/additional_modules.cc
index 6bd58ee..83b13e5 100644
--- a/src/trace_processor/importers/proto/additional_modules.cc
+++ b/src/trace_processor/importers/proto/additional_modules.cc
@@ -22,6 +22,7 @@
 #include "src/trace_processor/importers/proto/graphics_event_module.h"
 #include "src/trace_processor/importers/proto/heap_graph_module.h"
 #include "src/trace_processor/importers/proto/metadata_module.h"
+#include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
 #include "src/trace_processor/importers/proto/network_trace_module.h"
 #include "src/trace_processor/importers/proto/statsd_module.h"
 #include "src/trace_processor/importers/proto/system_probes_module.h"
@@ -51,6 +52,11 @@
   context->modules.emplace_back(new FtraceModuleImpl(context));
   context->ftrace_module =
       static_cast<FtraceModule*>(context->modules.back().get());
+
+  if (context->multi_machine_trace_manager) {
+    context->multi_machine_trace_manager->EnableAdditionalModules(
+        RegisterAdditionalModules);
+  }
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/android_camera_event_module.cc b/src/trace_processor/importers/proto/android_camera_event_module.cc
index 1fdddd1..5c7722a 100644
--- a/src/trace_processor/importers/proto/android_camera_event_module.cc
+++ b/src/trace_processor/importers/proto/android_camera_event_module.cc
@@ -20,6 +20,7 @@
 #include "protos/perfetto/trace/android/camera_event.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
@@ -54,7 +55,7 @@
           decoder.android_camera_frame_event());
   context_->sorter->PushTracePacket(
       android_camera_frame_event.request_processing_started_ns(),
-      state->current_generation(), std::move(*packet));
+      state->current_generation(), std::move(*packet), context_->machine_id());
   return ModuleResult::Handled();
 }
 
diff --git a/src/trace_processor/importers/proto/android_probes_module.cc b/src/trace_processor/importers/proto/android_probes_module.cc
index d26b6ef..02dcfe7 100644
--- a/src/trace_processor/importers/proto/android_probes_module.cc
+++ b/src/trace_processor/importers/proto/android_probes_module.cc
@@ -19,6 +19,7 @@
 #include "perfetto/base/build_config.h"
 #include "perfetto/ext/base/string_writer.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/proto/android_probes_parser.h"
 #include "src/trace_processor/importers/proto/android_probes_tracker.h"
@@ -193,7 +194,8 @@
     std::vector<uint8_t> vec = data_packet.SerializeAsArray();
     TraceBlob blob = TraceBlob::CopyFrom(vec.data(), vec.size());
     context_->sorter->PushTracePacket(actual_ts, state->current_generation(),
-                                      TraceBlobView(std::move(blob)));
+                                      TraceBlobView(std::move(blob)),
+                                      context_->machine_id());
   }
 
   return ModuleResult::Handled();
diff --git a/src/trace_processor/importers/proto/multi_machine_trace_manager.cc b/src/trace_processor/importers/proto/multi_machine_trace_manager.cc
new file mode 100644
index 0000000..e91dcee
--- /dev/null
+++ b/src/trace_processor/importers/proto/multi_machine_trace_manager.cc
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
+#include <memory>
+
+#include "src/trace_processor/importers/common/args_translation_table.h"
+#include "src/trace_processor/importers/common/async_track_set_tracker.h"
+#include "src/trace_processor/importers/common/clock_converter.h"
+#include "src/trace_processor/importers/common/clock_tracker.h"
+#include "src/trace_processor/importers/common/event_tracker.h"
+#include "src/trace_processor/importers/common/flow_tracker.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/sched_event_tracker.h"
+#include "src/trace_processor/importers/common/slice_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/importers/proto/default_modules.h"
+#include "src/trace_processor/importers/proto/perf_sample_tracker.h"
+#include "src/trace_processor/importers/proto/proto_importer_module.h"
+#include "src/trace_processor/importers/proto/proto_trace_parser.h"
+#include "src/trace_processor/importers/proto/proto_trace_reader.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+MultiMachineTraceManager::MultiMachineTraceManager(
+    TraceProcessorContext* default_context)
+    : default_context_(default_context) {
+  PERFETTO_DCHECK(default_context && !default_context_->machine_id());
+}
+MultiMachineTraceManager::~MultiMachineTraceManager() = default;
+
+std::unique_ptr<TraceProcessorContext> MultiMachineTraceManager::CreateContext(
+    RawMachineId raw_machine_id) {
+  TraceProcessorContext::InitArgs args{
+      default_context_->config, default_context_->storage, raw_machine_id};
+  auto ctx = std::make_unique<TraceProcessorContext>(args);
+
+  // Register default and additional modules (if enabled).
+  RegisterDefaultModules(ctx.get());
+  // Register addtional modules through the registered function pointer.
+  if (additional_modules_factory_)
+    additional_modules_factory_(ctx.get());
+
+  return ctx;
+}
+
+void MultiMachineTraceManager::EnableAdditionalModules(
+    ProtoImporterModuleFactory factory) {
+  additional_modules_factory_ = factory;
+}
+
+ProtoTraceReader* MultiMachineTraceManager::GetOrCreateReader(
+    RawMachineId raw_machine_id) {
+  auto* remote_ctx = remote_machine_contexts_.Find(raw_machine_id);
+  if (remote_ctx)
+    return remote_ctx->reader.get();
+
+  auto context = CreateContext(raw_machine_id);
+  // Share the sorter, but enable for the parser.
+  context->sorter = default_context_->sorter;
+  context->sorter->AddMachine(
+      context->machine_id(), std::make_unique<ProtoTraceParser>(context.get()));
+  context->process_tracker->SetPidZeroIsUpidZeroIdleProcess();
+
+  auto new_reader = std::make_unique<ProtoTraceReader>(context.get());
+  remote_machine_contexts_[raw_machine_id] =
+      RemoteMachineContext{std::move(context), std::move(new_reader)};
+  return remote_machine_contexts_[raw_machine_id].reader.get();
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/multi_machine_trace_manager.h b/src/trace_processor/importers/proto/multi_machine_trace_manager.h
new file mode 100644
index 0000000..0184d85
--- /dev/null
+++ b/src/trace_processor/importers/proto/multi_machine_trace_manager.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_MULTI_MACHINE_TRACE_MANAGER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_MULTI_MACHINE_TRACE_MANAGER_H_
+
+#include <memory>
+#include <vector>
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class TraceProcessorContext;
+class ProtoTraceReader;
+
+// This class provides the get-or-create function for ProtoTraceReader to
+// support multi-machine tracing. When the default ProtoTraceReader instance
+// decodes a trace packet with a non-default machine ID:
+//
+// packet {
+//    ftrace_events {
+//    }
+//    machine_id: 1001
+// }
+//
+// An object graph rooted from a new ProtoTraceReader is created for the
+// machine:
+//
+// ProtoTraceReader -> TraceProcessorContext (with a non-null machine_id).
+//                     +--> TraceProcessorStorage (shared with the default
+//                     instance)
+//                     |--> TraceSorter (shared with the default instance)
+//                     |--> TrackTracker (created for machine 1001)
+//                     |--> ProcessTracker (created for machine 1001)
+//                     |--> ... other data members rooted from
+//                     TraceProcessorContext
+//
+// and the new ProtoTraceReader is used to parse all trace packet with the same
+// machine ID. The context is used to insert the machine ID into the sqlite
+// tables. for query in the trace processor or from the UI frontend.
+class MultiMachineTraceManager {
+ public:
+  // RawMachineId is the value of 'machine_id' in trace packets.
+  using RawMachineId = uint32_t;
+
+  explicit MultiMachineTraceManager(TraceProcessorContext* default_context);
+  ~MultiMachineTraceManager();
+
+  // Get or create an instance of ProtoTraceReader for parsing the trace packets
+  // with the RawMachineId from the trace packet.
+  ProtoTraceReader* GetOrCreateReader(RawMachineId);
+
+  using ProtoImporterModuleFactory = void (*)(TraceProcessorContext*);
+  void EnableAdditionalModules(ProtoImporterModuleFactory);
+
+ private:
+  struct RemoteMachineContext {
+    std::unique_ptr<TraceProcessorContext> context;
+    std::unique_ptr<ProtoTraceReader> reader;
+  };
+
+  std::unique_ptr<TraceProcessorContext> CreateContext(RawMachineId);
+
+  // The default TraceProcessorContext instance.
+  TraceProcessorContext* default_context_;
+  // Owns contexts for remote machines.
+  base::FlatHashMap<RawMachineId, RemoteMachineContext>
+      remote_machine_contexts_;
+
+  ProtoImporterModuleFactory additional_modules_factory_ = nullptr;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_MULTI_MACHINE_TRACE_MANAGER_H_
diff --git a/src/trace_processor/importers/proto/profile_module.cc b/src/trace_processor/importers/proto/profile_module.cc
index f8e2711..c194107 100644
--- a/src/trace_processor/importers/proto/profile_module.cc
+++ b/src/trace_processor/importers/proto/profile_module.cc
@@ -139,8 +139,9 @@
     sequence_state->IncrementAndGetTrackEventTimeNs(*timestamp_it * 1000);
   }
 
-  context_->sorter->PushTracePacket(
-      packet_ts, sequence_state->current_generation(), std::move(*packet));
+  context_->sorter->PushTracePacket(packet_ts,
+                                    sequence_state->current_generation(),
+                                    std::move(*packet), context_->machine_id());
   return ModuleResult::Handled();
 }
 
diff --git a/src/trace_processor/importers/proto/proto_trace_parser.cc b/src/trace_processor/importers/proto/proto_trace_parser.cc
index b23a513..4043ec6 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser.cc
@@ -30,6 +30,7 @@
 
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
@@ -255,7 +256,8 @@
   ArgsTracker args(context_);
   if (bundle.has_metadata()) {
     RawId id = storage->mutable_raw_table()
-                   ->Insert({ts, raw_chrome_metadata_event_id_, 0, 0})
+                   ->Insert({ts, raw_chrome_metadata_event_id_, 0, 0, 0, 0,
+                             context_->machine_id()})
                    .id;
     auto inserter = args.AddArgsTo(id);
 
@@ -301,10 +303,10 @@
   }
 
   if (bundle.has_legacy_ftrace_output()) {
-    RawId id =
-        storage->mutable_raw_table()
-            ->Insert({ts, raw_chrome_legacy_system_trace_event_id_, 0, 0})
-            .id;
+    RawId id = storage->mutable_raw_table()
+                   ->Insert({ts, raw_chrome_legacy_system_trace_event_id_, 0, 0,
+                             0, 0, context_->machine_id()})
+                   .id;
 
     std::string data;
     for (auto it = bundle.legacy_ftrace_output(); it; ++it) {
@@ -322,10 +324,10 @@
           protos::pbzero::ChromeLegacyJsonTrace::USER_TRACE) {
         continue;
       }
-      RawId id =
-          storage->mutable_raw_table()
-              ->Insert({ts, raw_chrome_legacy_user_trace_event_id_, 0, 0})
-              .id;
+      RawId id = storage->mutable_raw_table()
+                     ->Insert({ts, raw_chrome_legacy_user_trace_event_id_, 0, 0,
+                               0, 0, context_->machine_id()})
+                     .id;
       Variadic value =
           Variadic::String(storage->InternString(legacy_trace.data()));
       args.AddArgsTo(id).AddArg(data_name_id_, value);
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc
index 12e044b..972acf3 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -25,9 +25,11 @@
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/protozero/proto_decoder.h"
 #include "perfetto/protozero/proto_utils.h"
+#include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/status.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/ftrace/ftrace_module.h"
@@ -86,6 +88,21 @@
   // Any compressed packets should have been handled by the tokenizer.
   PERFETTO_CHECK(!decoder.has_compressed_packets());
 
+  // When the trace packet is emitted from a remote machine: parse the packet
+  // using a different ProtoTraceReader instance. The packet will be parsed
+  // in the context of the remote machine.
+  if (PERFETTO_UNLIKELY(decoder.has_machine_id())) {
+    if (!context_->machine_id()) {
+      // Default context: switch to another reader instance to parse the packet.
+      PERFETTO_DCHECK(context_->multi_machine_trace_manager);
+      auto* reader = context_->multi_machine_trace_manager->GetOrCreateReader(
+          decoder.machine_id());
+      return reader->ParsePacket(std::move(packet));
+    }
+  }
+  // Assert that the packet is parsed using the right instance of reader.
+  PERFETTO_DCHECK(decoder.has_machine_id() == !!context_->machine_id());
+
   const uint32_t seq_id = decoder.trusted_packet_sequence_id();
   auto* state = GetIncrementalStateForPacketSequence(seq_id);
 
@@ -239,7 +256,7 @@
   // Use parent data and length because we want to parse this again
   // later to get the exact type of the packet.
   context_->sorter->PushTracePacket(timestamp, state->current_generation(),
-                                    std::move(packet));
+                                    std::move(packet), context_->machine_id());
 
   return util::OkStatus();
 }
@@ -407,6 +424,7 @@
         clock_timestamp.timestamp * clock_timestamp.clock.unit_multiplier_ns;
     row.clock_name = GetBuiltinClockNameOrNull(clock_timestamp.clock.id);
     row.snapshot_id = *snapshot_id;
+    row.machine_id = context_->machine_id();
 
     context_->storage->mutable_clock_snapshot_table()->Insert(row);
   }
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.h b/src/trace_processor/importers/proto/proto_trace_reader.h
index 8afc40c..1243b97 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.h
+++ b/src/trace_processor/importers/proto/proto_trace_reader.h
@@ -22,6 +22,7 @@
 #include <memory>
 
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
 #include "src/trace_processor/importers/proto/proto_incremental_state.h"
 #include "src/trace_processor/importers/proto/proto_trace_tokenizer.h"
 #include "src/trace_processor/storage/trace_storage.h"
diff --git a/src/trace_processor/importers/proto/statsd_module.cc b/src/trace_processor/importers/proto/statsd_module.cc
index 49a7300..2f9d3cd 100644
--- a/src/trace_processor/importers/proto/statsd_module.cc
+++ b/src/trace_processor/importers/proto/statsd_module.cc
@@ -20,6 +20,7 @@
 #include "protos/perfetto/trace/statsd/statsd_atom.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state.h"
@@ -245,9 +246,9 @@
     std::vector<uint8_t> vec = forged.SerializeAsArray();
     TraceBlob blob = TraceBlob::CopyFrom(vec.data(), vec.size());
 
-    context_->sorter->PushTracePacket(atom_timestamp,
-                                      state->current_generation(),
-                                      TraceBlobView(std::move(blob)));
+    context_->sorter->PushTracePacket(
+        atom_timestamp, state->current_generation(),
+        TraceBlobView(std::move(blob)), context_->machine_id());
   }
 
   return ModuleResult::Handled();
diff --git a/src/trace_processor/importers/proto/system_probes_parser.cc b/src/trace_processor/importers/proto/system_probes_parser.cc
index 8812674..5b878ed 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.cc
+++ b/src/trace_processor/importers/proto/system_probes_parser.cc
@@ -761,6 +761,7 @@
       cluster_id++;
     }
     cpu_row.cluster_id = cluster_id;
+    cpu_row.machine_id = context_->machine_id();
 
     last_cpu_freqs = freqs;
     tables::CpuTable::Id cpu_row_id =
@@ -771,6 +772,7 @@
       tables::CpuFreqTable::Row cpu_freq_row;
       cpu_freq_row.cpu_id = cpu_row_id;
       cpu_freq_row.freq = freq;
+      cpu_freq_row.machine_id = context_->machine_id();
       context_->storage->mutable_cpu_freq_table()->Insert(cpu_freq_row);
     }
   }
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index cdf2c45..cb80844 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -28,6 +28,7 @@
 #include "src/trace_processor/importers/common/args_translation_table.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/json/json_utils.h"
@@ -1122,7 +1123,8 @@
       return util::ErrStatus("raw legacy event without thread association");
 
     RawId id = storage_->mutable_raw_table()
-                   ->Insert({ts_, parser_->raw_legacy_event_id_, 0, *utid_})
+                   ->Insert({ts_, parser_->raw_legacy_event_id_, 0, *utid_, 0,
+                             0, context_->machine_id()})
                    .id;
 
     auto inserter = context_->args_tracker->AddArgsTo(id);
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index 6b655f4..7f20742 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -19,6 +19,7 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
@@ -370,7 +371,8 @@
     return;
   }
 
-  context_->sorter->PushTrackEventPacket(timestamp, std::move(data));
+  context_->sorter->PushTrackEventPacket(timestamp, std::move(data),
+                                         context_->machine_id());
 }
 
 template <typename T>
diff --git a/src/trace_processor/importers/proto/track_event_tracker.cc b/src/trace_processor/importers/proto/track_event_tracker.cc
index ad78346..56e081d 100644
--- a/src/trace_processor/importers/proto/track_event_tracker.cc
+++ b/src/trace_processor/importers/proto/track_event_tracker.cc
@@ -20,6 +20,7 @@
 #include "src/trace_processor/importers/common/args_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/tables/track_tables_py.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -163,6 +164,7 @@
 TrackId TrackEventTracker::InsertThreadTrack(UniqueTid utid) {
   tables::ThreadTrackTable::Row row;
   row.utid = utid;
+  row.machine_id = context_->machine_id();
   auto* thread_tracks = context_->storage->mutable_thread_track_table();
   return thread_tracks->Insert(row).id;
 }
@@ -283,6 +285,7 @@
       if (track.is_counter()) {
         tables::ThreadCounterTrackTable::Row row;
         row.utid = track.utid();
+        row.machine_id = context_->machine_id();
 
         auto* thread_counter_tracks =
             context_->storage->mutable_thread_counter_track_table();
@@ -295,6 +298,7 @@
       if (track.is_counter()) {
         tables::ProcessCounterTrackTable::Row row;
         row.upid = track.upid();
+        row.machine_id = context_->machine_id();
 
         auto* process_counter_tracks =
             context_->storage->mutable_process_counter_track_table();
@@ -303,14 +307,20 @@
 
       tables::ProcessTrackTable::Row row;
       row.upid = track.upid();
+      row.machine_id = context_->machine_id();
 
       auto* process_tracks = context_->storage->mutable_process_track_table();
       return process_tracks->Insert(row).id;
     }
     case ResolvedDescriptorTrack::Scope::kGlobal: {
-      if (track.is_counter())
-        return context_->storage->mutable_counter_track_table()->Insert({}).id;
-      return context_->storage->mutable_track_table()->Insert({}).id;
+      if (track.is_counter()) {
+        tables::CounterTrackTable::Row row;
+        row.machine_id = context_->machine_id();
+        return context_->storage->mutable_counter_track_table()->Insert(row).id;
+      }
+      tables::TrackTable::Row row;
+      row.machine_id = context_->machine_id();
+      return context_->storage->mutable_track_table()->Insert(row).id;
     }
   }
   PERFETTO_FATAL("For GCC");
diff --git a/src/trace_processor/sorter/trace_sorter.cc b/src/trace_processor/sorter/trace_sorter.cc
index 0a09224..b937e2e 100644
--- a/src/trace_processor/sorter/trace_sorter.cc
+++ b/src/trace_processor/sorter/trace_sorter.cc
@@ -31,9 +31,8 @@
 TraceSorter::TraceSorter(TraceProcessorContext* context,
                          std::unique_ptr<TraceParser> parser,
                          SortingMode sorting_mode)
-    : context_(context),
-      parser_(std::move(parser)),
-      sorting_mode_(sorting_mode) {
+    : context_(context), sorting_mode_(sorting_mode) {
+  AddMachine(context_->machine_id(), std::move(parser));
   const char* env = getenv("TRACE_PROCESSOR_SORT_ONLY");
   bypass_next_stage_for_testing_ = env && !strcmp(env, "1");
   if (bypass_next_stage_for_testing_)
@@ -44,9 +43,11 @@
   // If trace processor encountered a fatal error, it's possible for some events
   // to have been pushed without evicting them by pushing to the next stage. Do
   // that now.
-  for (auto& queue : queues_) {
-    for (const auto& event : queue.events_) {
-      ExtractAndDiscardTokenizedObject(event);
+  for (auto& sorter_data : sorter_data_by_machine_) {
+    for (auto& queue : sorter_data.queues) {
+      for (const auto& event : queue.events_) {
+        ExtractAndDiscardTokenizedObject(event);
+      }
     }
   }
 }
@@ -98,6 +99,7 @@
     BumpAllocator::AllocId limit_alloc_id) {
   constexpr int64_t kTsMax = std::numeric_limits<int64_t>::max();
   for (;;) {
+    size_t min_machine_idx = 0;
     size_t min_queue_idx = 0;  // The index of the queue with the min(ts).
 
     // The top-2 min(ts) among all queues.
@@ -107,25 +109,30 @@
     // This loop identifies the queue which starts with the earliest event and
     // also remembers the earliest event of the 2nd queue (in min_queue_ts[1]).
     bool all_queues_empty = true;
-    for (size_t i = 0; i < queues_.size(); i++) {
-      auto& queue = queues_[i];
-      if (queue.events_.empty())
-        continue;
-      all_queues_empty = false;
+    for (size_t m = 0; m < sorter_data_by_machine_.size(); m++) {
+      TraceSorterData& sorter_data = sorter_data_by_machine_[m];
+      for (size_t i = 0; i < sorter_data.queues.size(); i++) {
+        auto& queue = sorter_data.queues[i];
+        if (queue.events_.empty())
+          continue;
+        all_queues_empty = false;
 
-      PERFETTO_DCHECK(queue.max_ts_ <= append_max_ts_);
-      if (queue.min_ts_ < min_queue_ts[0]) {
-        min_queue_ts[1] = min_queue_ts[0];
-        min_queue_ts[0] = queue.min_ts_;
-        min_queue_idx = i;
-      } else if (queue.min_ts_ < min_queue_ts[1]) {
-        min_queue_ts[1] = queue.min_ts_;
+        PERFETTO_DCHECK(queue.max_ts_ <= append_max_ts_);
+        if (queue.min_ts_ < min_queue_ts[0]) {
+          min_queue_ts[1] = min_queue_ts[0];
+          min_queue_ts[0] = queue.min_ts_;
+          min_queue_idx = i;
+          min_machine_idx = m;
+        } else if (queue.min_ts_ < min_queue_ts[1]) {
+          min_queue_ts[1] = queue.min_ts_;
+        }
       }
     }
     if (all_queues_empty)
       break;
 
-    Queue& queue = queues_[min_queue_idx];
+    auto& sorter_data = sorter_data_by_machine_[min_machine_idx];
+    auto& queue = sorter_data.queues[min_queue_idx];
     auto& events = queue.events_;
     if (queue.needs_sorting())
       queue.Sort();
@@ -148,7 +155,7 @@
       }
 
       ++num_extracted;
-      MaybeExtractEvent(min_queue_idx, event);
+      MaybeExtractEvent(min_machine_idx, min_queue_idx, event);
     }  // for (event: events)
 
     // The earliest event cannot be extracted without going past the limit.
@@ -174,32 +181,33 @@
   }  // for(;;)
 }
 
-void TraceSorter::ParseTracePacket(const TimestampedEvent& event) {
+void TraceSorter::ParseTracePacket(TraceParser* parser,
+                                   const TimestampedEvent& event) {
   TraceTokenBuffer::Id id = GetTokenBufferId(event);
   switch (static_cast<TimestampedEvent::Type>(event.event_type)) {
     case TimestampedEvent::Type::kTraceBlobView:
-      parser_->ParseTraceBlobView(event.ts,
-                                  token_buffer_.Extract<TraceBlobView>(id));
+      parser->ParseTraceBlobView(event.ts,
+                                 token_buffer_.Extract<TraceBlobView>(id));
       return;
     case TimestampedEvent::Type::kTracePacket:
-      parser_->ParseTracePacket(event.ts,
-                                token_buffer_.Extract<TracePacketData>(id));
+      parser->ParseTracePacket(event.ts,
+                               token_buffer_.Extract<TracePacketData>(id));
       return;
     case TimestampedEvent::Type::kTrackEvent:
-      parser_->ParseTrackEvent(event.ts,
-                               token_buffer_.Extract<TrackEventData>(id));
+      parser->ParseTrackEvent(event.ts,
+                              token_buffer_.Extract<TrackEventData>(id));
       return;
     case TimestampedEvent::Type::kFuchsiaRecord:
-      parser_->ParseFuchsiaRecord(event.ts,
-                                  token_buffer_.Extract<FuchsiaRecord>(id));
+      parser->ParseFuchsiaRecord(event.ts,
+                                 token_buffer_.Extract<FuchsiaRecord>(id));
       return;
     case TimestampedEvent::Type::kJsonValue:
-      parser_->ParseJsonPacket(
+      parser->ParseJsonPacket(
           event.ts, std::move(token_buffer_.Extract<JsonEvent>(id).value));
       return;
     case TimestampedEvent::Type::kSystraceLine:
-      parser_->ParseSystraceLine(event.ts,
-                                 token_buffer_.Extract<SystraceLine>(id));
+      parser->ParseSystraceLine(event.ts,
+                                token_buffer_.Extract<SystraceLine>(id));
       return;
     case TimestampedEvent::Type::kInlineSchedSwitch:
     case TimestampedEvent::Type::kInlineSchedWaking:
@@ -210,7 +218,8 @@
   PERFETTO_FATAL("For GCC");
 }
 
-void TraceSorter::ParseEtwPacket(uint32_t /*cpu*/,
+void TraceSorter::ParseEtwPacket(TraceParser* /*parser*/,
+                                 uint32_t /*cpu*/,
                                  const TimestampedEvent& event) {
   switch (static_cast<TimestampedEvent::Type>(event.event_type)) {
     case TimestampedEvent::Type::kEtwEvent:
@@ -229,21 +238,22 @@
   PERFETTO_FATAL("For GCC");
 }
 
-void TraceSorter::ParseFtracePacket(uint32_t cpu,
+void TraceSorter::ParseFtracePacket(TraceParser* parser,
+                                    uint32_t cpu,
                                     const TimestampedEvent& event) {
   TraceTokenBuffer::Id id = GetTokenBufferId(event);
   switch (static_cast<TimestampedEvent::Type>(event.event_type)) {
     case TimestampedEvent::Type::kInlineSchedSwitch:
-      parser_->ParseInlineSchedSwitch(
+      parser->ParseInlineSchedSwitch(
           cpu, event.ts, token_buffer_.Extract<InlineSchedSwitch>(id));
       return;
     case TimestampedEvent::Type::kInlineSchedWaking:
-      parser_->ParseInlineSchedWaking(
+      parser->ParseInlineSchedWaking(
           cpu, event.ts, token_buffer_.Extract<InlineSchedWaking>(id));
       return;
     case TimestampedEvent::Type::kFtraceEvent:
-      parser_->ParseFtraceEvent(cpu, event.ts,
-                                token_buffer_.Extract<TracePacketData>(id));
+      parser->ParseFtraceEvent(cpu, event.ts,
+                               token_buffer_.Extract<TracePacketData>(id));
       return;
     case TimestampedEvent::Type::kEtwEvent:
     case TimestampedEvent::Type::kTrackEvent:
@@ -295,8 +305,10 @@
   PERFETTO_FATAL("For GCC");
 }
 
-void TraceSorter::MaybeExtractEvent(size_t queue_idx,
+void TraceSorter::MaybeExtractEvent(size_t min_machine_idx,
+                                    size_t queue_idx,
                                     const TimestampedEvent& event) {
+  auto* parser = sorter_data_by_machine_[min_machine_idx].parser.get();
   int64_t timestamp = event.ts;
   if (timestamp < latest_pushed_event_ts_)
     context_->storage->IncrementStats(stats::sorter_push_event_out_of_order);
@@ -311,16 +323,16 @@
   }
 
   if (queue_idx == 0) {
-    ParseTracePacket(event);
+    ParseTracePacket(parser, event);
   } else {
     // Ftrace queues start at offset 1. So queues_[1] = cpu[0] and so on.
     uint32_t cpu = static_cast<uint32_t>(queue_idx - 1);
     auto event_type = static_cast<TimestampedEvent::Type>(event.event_type);
 
     if (event_type == TimestampedEvent::Type::kEtwEvent) {
-      ParseEtwPacket(static_cast<uint32_t>(cpu), event);
+      ParseEtwPacket(parser, static_cast<uint32_t>(cpu), event);
     } else {
-      ParseFtracePacket(cpu, event);
+      ParseFtracePacket(parser, cpu, event);
     }
   }
 }
diff --git a/src/trace_processor/sorter/trace_sorter.h b/src/trace_processor/sorter/trace_sorter.h
index fc4bac8..56fd0a1 100644
--- a/src/trace_processor/sorter/trace_sorter.h
+++ b/src/trace_processor/sorter/trace_sorter.h
@@ -19,11 +19,13 @@
 
 #include <algorithm>
 #include <memory>
+#include <optional>
 #include <utility>
 #include <vector>
 
 #include "perfetto/ext/base/circular_queue.h"
 #include "perfetto/ext/base/utils.h"
+#include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/parser_types.h"
@@ -95,21 +97,37 @@
               SortingMode);
   ~TraceSorter();
 
-  inline void PushTraceBlobView(int64_t timestamp, TraceBlobView tbv) {
+  inline void AddMachine(std::optional<MachineId> machine_id,
+                         std::unique_ptr<TraceParser> parser) {
+    sorter_data_by_machine_.emplace_back(machine_id, std::move(parser));
+  }
+
+  inline void PushTraceBlobView(
+      int64_t timestamp,
+      TraceBlobView tbv,
+      std::optional<MachineId> machine_id = std::nullopt) {
     TraceTokenBuffer::Id id = token_buffer_.Append(std::move(tbv));
-    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kTraceBlobView, id);
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kTraceBlobView, id,
+                         machine_id);
   }
 
-  inline void PushTracePacket(int64_t timestamp, TracePacketData data) {
+  inline void PushTracePacket(
+      int64_t timestamp,
+      TracePacketData data,
+      std::optional<MachineId> machine_id = std::nullopt) {
     TraceTokenBuffer::Id id = token_buffer_.Append(std::move(data));
-    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kTracePacket, id);
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kTracePacket, id,
+                         machine_id);
   }
 
-  inline void PushTracePacket(int64_t timestamp,
-                              RefPtr<PacketSequenceStateGeneration> state,
-                              TraceBlobView tbv) {
+  inline void PushTracePacket(
+      int64_t timestamp,
+      RefPtr<PacketSequenceStateGeneration> state,
+      TraceBlobView tbv,
+      std::optional<MachineId> machine_id = std::nullopt) {
     PushTracePacket(timestamp,
-                    TracePacketData{std::move(tbv), std::move(state)});
+                    TracePacketData{std::move(tbv), std::move(state)},
+                    machine_id);
   }
 
   inline void PushJsonValue(int64_t timestamp, std::string json_value) {
@@ -130,37 +148,45 @@
                          TimestampedEvent::Type::kSystraceLine, id);
   }
 
-  inline void PushTrackEventPacket(int64_t timestamp,
-                                   TrackEventData track_event) {
+  inline void PushTrackEventPacket(
+      int64_t timestamp,
+      TrackEventData track_event,
+      std::optional<MachineId> machine_id = std::nullopt) {
     TraceTokenBuffer::Id id = token_buffer_.Append(std::move(track_event));
-    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kTrackEvent, id);
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kTrackEvent, id,
+                         machine_id);
   }
 
   inline void PushEtwEvent(uint32_t cpu,
                            int64_t timestamp,
                            TraceBlobView tbv,
-                           RefPtr<PacketSequenceStateGeneration> state) {
+                           RefPtr<PacketSequenceStateGeneration> state,
+                           std::optional<MachineId> machine_id = std::nullopt) {
     TraceTokenBuffer::Id id =
         token_buffer_.Append(TracePacketData{std::move(tbv), std::move(state)});
-    auto* queue = GetQueue(cpu + 1);
+    auto* queue = GetQueue(cpu + 1, machine_id);
     queue->Append(timestamp, TimestampedEvent::Type::kEtwEvent, id);
     UpdateAppendMaxTs(queue);
   }
 
-  inline void PushFtraceEvent(uint32_t cpu,
-                              int64_t timestamp,
-                              TraceBlobView tbv,
-                              RefPtr<PacketSequenceStateGeneration> state) {
+  inline void PushFtraceEvent(
+      uint32_t cpu,
+      int64_t timestamp,
+      TraceBlobView tbv,
+      RefPtr<PacketSequenceStateGeneration> state,
+      std::optional<MachineId> machine_id = std::nullopt) {
     TraceTokenBuffer::Id id =
         token_buffer_.Append(TracePacketData{std::move(tbv), std::move(state)});
-    auto* queue = GetQueue(cpu + 1);
+    auto* queue = GetQueue(cpu + 1, machine_id);
     queue->Append(timestamp, TimestampedEvent::Type::kFtraceEvent, id);
     UpdateAppendMaxTs(queue);
   }
 
-  inline void PushInlineFtraceEvent(uint32_t cpu,
-                                    int64_t timestamp,
-                                    InlineSchedSwitch inline_sched_switch) {
+  inline void PushInlineFtraceEvent(
+      uint32_t cpu,
+      int64_t timestamp,
+      InlineSchedSwitch inline_sched_switch,
+      std::optional<MachineId> machine_id = std::nullopt) {
     // TODO(rsavitski): if a trace has a mix of normal & "compact" events
     // (being pushed through this function), the ftrace batches will no longer
     // be fully sorted by timestamp. In such situations, we will have to sort
@@ -170,17 +196,19 @@
     // // instead.
     TraceTokenBuffer::Id id =
         token_buffer_.Append(std::move(inline_sched_switch));
-    auto* queue = GetQueue(cpu + 1);
+    auto* queue = GetQueue(cpu + 1, machine_id);
     queue->Append(timestamp, TimestampedEvent::Type::kInlineSchedSwitch, id);
     UpdateAppendMaxTs(queue);
   }
 
-  inline void PushInlineFtraceEvent(uint32_t cpu,
-                                    int64_t timestamp,
-                                    InlineSchedWaking inline_sched_waking) {
+  inline void PushInlineFtraceEvent(
+      uint32_t cpu,
+      int64_t timestamp,
+      InlineSchedWaking inline_sched_waking,
+      std::optional<MachineId> machine_id = std::nullopt) {
     TraceTokenBuffer::Id id =
         token_buffer_.Append(std::move(inline_sched_waking));
-    auto* queue = GetQueue(cpu + 1);
+    auto* queue = GetQueue(cpu + 1, machine_id);
     queue->Append(timestamp, TimestampedEvent::Type::kInlineSchedWaking, id);
     UpdateAppendMaxTs(queue);
   }
@@ -188,10 +216,12 @@
   void ExtractEventsForced() {
     BumpAllocator::AllocId end_id = token_buffer_.PastTheEndAllocId();
     SortAndExtractEventsUntilAllocId(end_id);
-    for (const auto& queue : queues_) {
-      PERFETTO_DCHECK(queue.events_.empty());
+    for (auto& sorter_data : sorter_data_by_machine_) {
+      for (const auto& queue : sorter_data.queues) {
+        PERFETTO_DCHECK(queue.events_.empty());
+      }
+      sorter_data.queues.clear();
     }
-    queues_.clear();
 
     alloc_id_for_extraction_ = end_id;
     flushes_since_extraction_ = 0;
@@ -319,16 +349,34 @@
 
   void SortAndExtractEventsUntilAllocId(BumpAllocator::AllocId alloc_id);
 
-  inline Queue* GetQueue(size_t index) {
-    if (PERFETTO_UNLIKELY(index >= queues_.size()))
-      queues_.resize(index + 1);
-    return &queues_[index];
+  inline Queue* GetQueue(size_t index,
+                         std::optional<MachineId> machine_id = std::nullopt) {
+    // sorter_data_by_machine_[0] corresponds to the default machine.
+    PERFETTO_DCHECK(sorter_data_by_machine_[0].machine_id == std::nullopt);
+    auto* queues = &sorter_data_by_machine_[0].queues;
+
+    // Find the TraceSorterData instance when |machine_id| is not nullopt.
+    if (PERFETTO_UNLIKELY(!!machine_id)) {
+      auto it = std::find_if(sorter_data_by_machine_.begin() + 1,
+                             sorter_data_by_machine_.end(),
+                             [machine_id](const TraceSorterData& item) {
+                               return item.machine_id == machine_id;
+                             });
+      PERFETTO_DCHECK(it != sorter_data_by_machine_.end());
+      queues = &it->queues;
+    }
+
+    if (PERFETTO_UNLIKELY(index >= queues->size()))
+      queues->resize(index + 1);
+    return &queues->at(index);
   }
 
-  inline void AppendNonFtraceEvent(int64_t ts,
-                                   TimestampedEvent::Type event_type,
-                                   TraceTokenBuffer::Id id) {
-    Queue* queue = GetQueue(0);
+  inline void AppendNonFtraceEvent(
+      int64_t ts,
+      TimestampedEvent::Type event_type,
+      TraceTokenBuffer::Id id,
+      std::optional<MachineId> machine_id = std::nullopt) {
+    Queue* queue = GetQueue(0, machine_id);
     queue->Append(ts, event_type, id);
     UpdateAppendMaxTs(queue);
   }
@@ -337,19 +385,33 @@
     append_max_ts_ = std::max(append_max_ts_, queue->max_ts_);
   }
 
-  void ParseTracePacket(const TimestampedEvent&);
-  void ParseFtracePacket(uint32_t cpu, const TimestampedEvent&);
-  void ParseEtwPacket(uint32_t cpu, const TimestampedEvent&);
+  void ParseTracePacket(TraceParser*, const TimestampedEvent&);
+  void ParseFtracePacket(TraceParser*, uint32_t cpu, const TimestampedEvent&);
+  void ParseEtwPacket(TraceParser*, uint32_t cpu, const TimestampedEvent&);
 
-  void MaybeExtractEvent(size_t queue_idx, const TimestampedEvent&);
+  void MaybeExtractEvent(size_t machine_idx,
+                         size_t queue_idx,
+                         const TimestampedEvent&);
   void ExtractAndDiscardTokenizedObject(const TimestampedEvent& event);
 
   TraceTokenBuffer::Id GetTokenBufferId(const TimestampedEvent& event) {
     return TraceTokenBuffer::Id{event.alloc_id()};
   }
 
+  struct TraceSorterData {
+    TraceSorterData(std::optional<MachineId> _machine_id,
+                    std::unique_ptr<TraceParser> _parser)
+        : machine_id(_machine_id), parser(std::move(_parser)) {}
+    std::optional<MachineId> machine_id;
+    std::unique_ptr<TraceParser> parser;
+    // queues_[0] is the general (non-ftrace) queue.
+    // queues_[1] is the ftrace queue for CPU(0).
+    // queues_[x] is the ftrace queue for CPU(x - 1).
+    std::vector<Queue> queues;
+  };
+  std::vector<TraceSorterData> sorter_data_by_machine_;
+
   TraceProcessorContext* context_ = nullptr;
-  std::unique_ptr<TraceParser> parser_;
 
   // Whether we should ignore incremental extraction and just wait for
   // forced extractionn at the end of the trace.
@@ -368,11 +430,6 @@
   // extraction.
   uint32_t flushes_since_extraction_ = 0;
 
-  // queues_[0] is the general (non-ftrace) queue.
-  // queues_[1] is the ftrace queue for CPU(0).
-  // queues_[x] is the ftrace queue for CPU(x - 1).
-  std::vector<Queue> queues_;
-
   // max(e.ts for e appended to the sorter)
   int64_t append_max_ts_ = 0;
 
diff --git a/src/trace_processor/sorter/trace_sorter_unittest.cc b/src/trace_processor/sorter/trace_sorter_unittest.cc
index 791e21a..8f7e73d 100644
--- a/src/trace_processor/sorter/trace_sorter_unittest.cc
+++ b/src/trace_processor/sorter/trace_sorter_unittest.cc
@@ -38,20 +38,26 @@
 using ::testing::MockFunction;
 using ::testing::NiceMock;
 
+constexpr std::optional<MachineId> kNullMachineId = std::nullopt;
+
 class MockTraceParser : public ProtoTraceParser {
  public:
-  MockTraceParser(TraceProcessorContext* context) : ProtoTraceParser(context) {}
+  explicit MockTraceParser(TraceProcessorContext* context)
+      : ProtoTraceParser(context), machine_id_(context->machine_id()) {}
 
-  MOCK_METHOD(
-      void,
-      MOCK_ParseFtracePacket,
-      (uint32_t cpu, int64_t timestamp, const uint8_t* data, size_t length));
+  MOCK_METHOD(void,
+              MOCK_ParseFtracePacket,
+              (uint32_t cpu,
+               int64_t timestamp,
+               const uint8_t* data,
+               size_t length,
+               std::optional<MachineId>));
 
   void ParseFtraceEvent(uint32_t cpu,
                         int64_t timestamp,
                         TracePacketData data) override {
     MOCK_ParseFtracePacket(cpu, timestamp, data.packet.data(),
-                           data.packet.length());
+                           data.packet.length(), machine_id_);
   }
 
   MOCK_METHOD(void,
@@ -64,6 +70,8 @@
     TraceBlobView& tbv = data.packet;
     MOCK_ParseTracePacket(ts, tbv.data(), tbv.length());
   }
+
+  std::optional<MachineId> machine_id_;
 };
 
 class MockTraceStorage : public TraceStorage {
@@ -100,7 +108,8 @@
 TEST_F(TraceSorterTest, TestFtrace) {
   PacketSequenceState state(&context_);
   TraceBlobView view = test_buffer_.slice_off(0, 1);
-  EXPECT_CALL(*parser_, MOCK_ParseFtracePacket(0, 1000, view.data(), 1));
+  EXPECT_CALL(*parser_,
+              MOCK_ParseFtracePacket(0, 1000, view.data(), 1, kNullMachineId));
   context_.sorter->PushFtraceEvent(0 /*cpu*/, 1000 /*timestamp*/,
                                    std::move(view), state.current_generation());
   context_.sorter->ExtractEventsForced();
@@ -124,10 +133,12 @@
 
   InSequence s;
 
-  EXPECT_CALL(*parser_, MOCK_ParseFtracePacket(0, 1000, view_1.data(), 1));
+  EXPECT_CALL(*parser_, MOCK_ParseFtracePacket(0, 1000, view_1.data(), 1,
+                                               kNullMachineId));
   EXPECT_CALL(*parser_, MOCK_ParseTracePacket(1001, view_2.data(), 2));
   EXPECT_CALL(*parser_, MOCK_ParseTracePacket(1100, view_3.data(), 3));
-  EXPECT_CALL(*parser_, MOCK_ParseFtracePacket(2, 1200, view_4.data(), 4));
+  EXPECT_CALL(*parser_, MOCK_ParseFtracePacket(2, 1200, view_4.data(), 4,
+                                               kNullMachineId));
 
   context_.sorter->PushFtraceEvent(2 /*cpu*/, 1200 /*timestamp*/,
                                    std::move(view_4),
@@ -280,9 +291,10 @@
   std::minstd_rand0 rnd_engine(0);
   std::map<int64_t /*ts*/, std::vector<uint32_t /*cpu*/>> expectations;
 
-  EXPECT_CALL(*parser_, MOCK_ParseFtracePacket(_, _, _, _))
+  EXPECT_CALL(*parser_, MOCK_ParseFtracePacket(_, _, _, _, _))
       .WillRepeatedly(Invoke([&expectations](uint32_t cpu, int64_t timestamp,
-                                             const uint8_t*, size_t) {
+                                             const uint8_t*, size_t,
+                                             std::optional<MachineId>) {
         EXPECT_EQ(expectations.begin()->first, timestamp);
         auto& cpus = expectations.begin()->second;
         bool cpu_found = false;
@@ -318,6 +330,115 @@
   EXPECT_TRUE(expectations.empty());
 }
 
+// An generalized version of MultiQueueSorting with multiple machines.
+TEST_F(TraceSorterTest, MultiMachineSorting) {
+  PacketSequenceState state(&context_);
+  std::minstd_rand0 rnd_engine(0);
+
+  struct ExpectedMachineAndCpu {
+    std::optional<MachineId> machine_id;
+    uint32_t cpu;
+
+    bool operator==(const ExpectedMachineAndCpu& other) const {
+      return std::tie(machine_id, cpu) == std::tie(other.machine_id, other.cpu);
+    }
+    bool operator!=(const ExpectedMachineAndCpu& other) const {
+      return !operator==(other);
+    }
+  };
+  std::map<int64_t /*ts*/, std::vector<ExpectedMachineAndCpu>> expectations;
+
+  // The total number of machines (including the default one).
+  constexpr size_t num_machines = 5;
+  std::vector<MockTraceParser*> extra_parsers;
+  std::vector<std::unique_ptr<TraceProcessorContext>> extra_contexts;
+  // Set up extra machines and add to the sorter.
+  // MachineIdValue are 1..(num_machines-1).
+  for (auto i = 1u; i < num_machines; i++) {
+    TraceProcessorContext::InitArgs args{context_.config, context_.storage, i};
+    auto ctx = std::make_unique<TraceProcessorContext>(args);
+    auto parser = std::make_unique<MockTraceParser>(ctx.get());
+    extra_parsers.push_back(parser.get());
+
+    extra_contexts.push_back(std::move(ctx));
+    context_.sorter->AddMachine(extra_contexts.back()->machine_id(),
+                                std::move(parser));
+  }
+
+  // Set up the expectation for the default machine.
+  EXPECT_CALL(*parser_, MOCK_ParseFtracePacket(_, _, _, _, _))
+      .WillRepeatedly(Invoke([&expectations](uint32_t cpu, int64_t timestamp,
+                                             const uint8_t*, size_t,
+                                             std::optional<MachineId>) {
+        EXPECT_EQ(expectations.begin()->first, timestamp);
+        auto& machines_and_cpus = expectations.begin()->second;
+        bool found = false;
+        for (auto it = machines_and_cpus.begin(); it < machines_and_cpus.end();
+             it++) {
+          // The default machine is called machine ID == std::nullopt.
+          if (*it != ExpectedMachineAndCpu{kNullMachineId, cpu})
+            continue;
+          found = true;
+          machines_and_cpus.erase(it);
+          break;
+        }
+        if (machines_and_cpus.empty())
+          expectations.erase(expectations.begin());
+        EXPECT_TRUE(found);
+      }));
+  // Set up expectations for remote machines.
+  for (auto* parser : extra_parsers) {
+    EXPECT_CALL(*parser, MOCK_ParseFtracePacket(_, _, _, _, _))
+        .WillRepeatedly(Invoke(
+            [&expectations](uint32_t cpu, int64_t timestamp, const uint8_t*,
+                            size_t, std::optional<MachineId> machine_id) {
+              EXPECT_TRUE(machine_id.has_value());
+              EXPECT_EQ(expectations.begin()->first, timestamp);
+              auto& machines_and_cpus = expectations.begin()->second;
+              bool found = false;
+              for (auto it = machines_and_cpus.begin();
+                   it < machines_and_cpus.end(); it++) {
+                // Remote machines are called with non-null machine_id.
+                if (*it != ExpectedMachineAndCpu{machine_id, cpu})
+                  continue;
+                found = true;
+                machines_and_cpus.erase(it);
+                break;
+              }
+              if (machines_and_cpus.empty())
+                expectations.erase(expectations.begin());
+              EXPECT_TRUE(found);
+            }));
+  }
+
+  // Allocate a 1000 byte trace blob (per-machine) and push one byte chunks to
+  // be sorted with random timestamps.
+  constexpr size_t alloc_size = 1000;
+  TraceBlobView tbv(TraceBlob::Allocate(alloc_size * num_machines));
+  for (size_t m = 0; m < num_machines; m++) {
+    // TraceProcessorContext::machine_id is nullopt for the default machine or a
+    // monotonic counter starting from 1. 0 is a reserved value that isn't used.
+    std::optional<MachineId> machine;
+    if (m)
+      machine = extra_contexts[m - 1]->machine_id();
+
+    for (uint16_t i = 0; i < alloc_size; i++) {
+      int64_t ts = abs(static_cast<int64_t>(rnd_engine()));
+      uint8_t num_cpus = rnd_engine() % 3;
+      for (uint8_t j = 0; j < num_cpus; j++) {
+        uint32_t cpu = static_cast<uint32_t>(rnd_engine() % 32);
+        expectations[ts].push_back(ExpectedMachineAndCpu{machine, cpu});
+        context_.sorter->PushFtraceEvent(cpu, ts,
+                                         tbv.slice_off(m * alloc_size + i, 1),
+                                         state.current_generation(), machine);
+      }
+    }
+  }
+
+  context_.sorter->ExtractEventsForced();
+  EXPECT_TRUE(expectations.empty());
+}
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/storage/trace_storage.h b/src/trace_processor/storage/trace_storage.h
index c2158d7..21b6090 100644
--- a/src/trace_processor/storage/trace_storage.h
+++ b/src/trace_processor/storage/trace_storage.h
@@ -541,6 +541,9 @@
     return &ftrace_event_table_;
   }
 
+  const tables::MachineTable& machine_table() const { return machine_table_; }
+  tables::MachineTable* mutable_machine_table() { return &machine_table_; }
+
   const tables::CpuTable& cpu_table() const { return cpu_table_; }
   tables::CpuTable* mutable_cpu_table() { return &cpu_table_; }
 
@@ -1046,6 +1049,8 @@
   tables::RawTable raw_table_{&string_pool_};
   tables::FtraceEventTable ftrace_event_table_{&string_pool_, &raw_table_};
 
+  tables::MachineTable machine_table_{&string_pool_};
+
   tables::CpuTable cpu_table_{&string_pool_};
 
   tables::CpuFreqTable cpu_freq_table_{&string_pool_};
diff --git a/src/trace_processor/tables/counter_tables.py b/src/trace_processor/tables/counter_tables.py
index e882452..3a9e062 100644
--- a/src/trace_processor/tables/counter_tables.py
+++ b/src/trace_processor/tables/counter_tables.py
@@ -23,6 +23,7 @@
 from python.generators.trace_processor_table.public import CppTableId
 from python.generators.trace_processor_table.public import CppUint32
 
+from src.trace_processor.tables.metadata_tables import MACHINE_TABLE
 from src.trace_processor.tables.track_tables import COUNTER_TRACK_TABLE
 
 COUNTER_TABLE = Table(
@@ -34,15 +35,25 @@
         C('track_id', CppTableId(COUNTER_TRACK_TABLE)),
         C('value', CppDouble()),
         C('arg_set_id', CppOptional(CppUint32())),
+        C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     tabledoc=TableDoc(
         doc='''''',
         group='Events',
         columns={
-            'ts': '''''',
-            'track_id': '''''',
-            'value': '''''',
-            'arg_set_id': '''''',
+            'ts':
+                '''''',
+            'track_id':
+                '''''',
+            'value':
+                '''''',
+            'arg_set_id':
+                '''''',
+            'machine_id':
+                '''
+                  Machine identifier, non-null for counters from a remote
+                  machine.
+                ''',
         }))
 
 # Keep this list sorted.
diff --git a/src/trace_processor/tables/metadata_tables.py b/src/trace_processor/tables/metadata_tables.py
index d620b6c..0d91d8d 100644
--- a/src/trace_processor/tables/metadata_tables.py
+++ b/src/trace_processor/tables/metadata_tables.py
@@ -28,6 +28,26 @@
 from python.generators.trace_processor_table.public import CppSelfTableId
 from python.generators.trace_processor_table.public import WrappingSqlView
 
+MACHINE_TABLE = Table(
+    python_module=__file__,
+    class_name='MachineTable',
+    sql_name='machine',
+    columns=[
+        C('raw_id', CppUint32()),
+    ],
+    tabledoc=TableDoc(
+        doc='''
+          Contains raw machine_id of trace packets emitted from remote machines.
+        ''',
+        group='Metadata',
+        columns={
+            'raw_id':
+                '''
+                  Raw machine identifier in the trace packet, non-zero for
+                  remote machines.
+                '''
+        }))
+
 PROCESS_TABLE = Table(
     python_module=__file__,
     class_name='ProcessTable',
@@ -43,6 +63,7 @@
         C('android_appid', CppOptional(CppUint32())),
         C('cmdline', CppOptional(CppString())),
         C('arg_set_id', CppUint32()),
+        C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     wrapping_sql_view=WrappingSqlView(view_name='process',),
     tabledoc=TableDoc(
@@ -98,6 +119,11 @@
             'arg_set_id':
                 ColumnDoc(
                     'Extra args for this process.', joinable='args.arg_set_id'),
+            'machine_id':
+                '''
+                  Machine identifier, non-null for processes on a remote
+                  machine.
+                ''',
         }))
 
 THREAD_TABLE = Table(
@@ -112,6 +138,7 @@
         C('end_ts', CppOptional(CppInt64())),
         C('upid', CppOptional(CppTableId(PROCESS_TABLE))),
         C('is_main_thread', CppOptional(CppUint32())),
+        C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     wrapping_sql_view=WrappingSqlView(view_name='thread',),
     tabledoc=TableDoc(
@@ -157,7 +184,11 @@
                 '''
                   Boolean indicating if this thread is the main thread
                   in the process.
+                ''',
+            'machine_id':
                 '''
+                  Machine identifier, non-null for threads on a remote machine.
+                ''',
         }))
 
 RAW_TABLE = Table(
@@ -170,7 +201,8 @@
         C('cpu', CppUint32()),
         C('utid', CppTableId(THREAD_TABLE)),
         C('arg_set_id', CppUint32()),
-        C('common_flags', CppUint32())
+        C('common_flags', CppUint32()),
+        C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     tabledoc=TableDoc(
         doc='''
@@ -196,7 +228,15 @@
             'utid':
                 'The thread this event was emitted on.',
             'common_flags':
-                'Ftrace event flags for this event. Currently only emitted for sched_waking events.'
+                '''
+                  Ftrace event flags for this event. Currently only emitted for
+                  sched_waking events.
+                ''',
+            'machine_id':
+                '''
+                  Machine identifier, non-null for raw events on a remote
+                  machine.
+                ''',
         }))
 
 FTRACE_EVENT_TABLE = Table(
@@ -324,6 +364,7 @@
     columns=[
         C('cluster_id', CppUint32()),
         C('processor', CppString()),
+        C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     tabledoc=TableDoc(
         doc='''
@@ -335,7 +376,11 @@
                 '''the cluster id is shared by CPUs in
 the same cluster''',
             'processor':
-                '''a string describing this core'''
+                '''a string describing this core''',
+            'machine_id':
+                '''
+                  Machine identifier, non-null for CPUs on a remote machine.
+                ''',
         }))
 
 CPU_FREQ_TABLE = Table(
@@ -345,11 +390,18 @@
     columns=[
         C('cpu_id', CppTableId(CPU_TABLE)),
         C('freq', CppUint32()),
+        C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     tabledoc=TableDoc(
-        doc='''''', group='Misc', columns={
+        doc='''''',
+        group='Misc',
+        columns={
             'cpu_id': '''''',
-            'freq': ''''''
+            'freq': '''''',
+            'machine_id':
+                '''
+                  Machine identifier, non-null for CPUs on a remote machine.
+                ''',
         }))
 
 CLOCK_SNAPSHOT_TABLE = Table(
@@ -362,6 +414,7 @@
         C('clock_name', CppOptional(CppString())),
         C('clock_value', CppInt64()),
         C('snapshot_id', CppUint32()),
+        C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     tabledoc=TableDoc(
         doc='''
@@ -382,7 +435,12 @@
             'clock_value':
                 '''timestamp of the snapshot in clock time.''',
             'snapshot_id':
-                '''the index of this snapshot (only useful for debugging)'''
+                '''the index of this snapshot (only useful for debugging)''',
+            'machine_id':
+                '''
+                  Machine identifier, non-null for clock snapshots on a remote
+                  machine.
+                ''',
         }))
 
 # Keep this list sorted.
@@ -398,4 +456,5 @@
     RAW_TABLE,
     THREAD_TABLE,
     FTRACE_EVENT_TABLE,
+    MACHINE_TABLE,
 ]
diff --git a/src/trace_processor/tables/sched_tables.py b/src/trace_processor/tables/sched_tables.py
index 0af8014..4ef4de8 100644
--- a/src/trace_processor/tables/sched_tables.py
+++ b/src/trace_processor/tables/sched_tables.py
@@ -27,6 +27,8 @@
 from python.generators.trace_processor_table.public import TableDoc
 from python.generators.trace_processor_table.public import WrappingSqlView
 
+from src.trace_processor.tables.metadata_tables import MACHINE_TABLE
+
 SCHED_SLICE_TABLE = Table(
     python_module=__file__,
     class_name='SchedSliceTable',
@@ -38,6 +40,7 @@
         C('utid', CppUint32()),
         C('end_state', CppString()),
         C('priority', CppInt32()),
+        C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     tabledoc=TableDoc(
         doc='''
@@ -70,7 +73,12 @@
                   cleanup).
                 ''',
             'priority':
-                '''The kernel priority that the thread ran at.'''
+                '''The kernel priority that the thread ran at.''',
+            'machine_id':
+                '''
+                  Machine identifier, non-null for scheduling slices on a remote
+                  machine.
+                ''',
         }))
 
 SPURIOUS_SCHED_WAKEUP_TABLE = Table(
@@ -122,6 +130,7 @@
         C('waker_utid', CppOptional(CppUint32())),
         C('waker_id', CppOptional(CppSelfTableId())),
         C('irq_context', CppOptional(CppUint32())),
+        C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     tabledoc=TableDoc(
         doc='''
@@ -160,7 +169,11 @@
             'waker_id':
                 '''
                   The unique thread state id which caused a wakeup of this thread.
+                ''',
+            'machine_id':
                 '''
+                  Machine identifier, non-null for threads on a remote machine.
+                ''',
         }))
 
 # Keep this list sorted.
diff --git a/src/trace_processor/tables/table_destructors.cc b/src/trace_processor/tables/table_destructors.cc
index 826316c..eade18c 100644
--- a/src/trace_processor/tables/table_destructors.cc
+++ b/src/trace_processor/tables/table_destructors.cc
@@ -60,6 +60,7 @@
 ProcessTable::~ProcessTable() = default;
 FiledescriptorTable::~FiledescriptorTable() = default;
 ClockSnapshotTable::~ClockSnapshotTable() = default;
+MachineTable::~MachineTable() = default;
 
 // profiler_tables_py.h
 StackProfileMappingTable::~StackProfileMappingTable() = default;
diff --git a/src/trace_processor/tables/track_tables.py b/src/trace_processor/tables/track_tables.py
index cfdc59d..4d7bc93 100644
--- a/src/trace_processor/tables/track_tables.py
+++ b/src/trace_processor/tables/track_tables.py
@@ -22,8 +22,11 @@
 from python.generators.trace_processor_table.public import TableDoc
 from python.generators.trace_processor_table.public import ColumnDoc
 from python.generators.trace_processor_table.public import CppSelfTableId
+from python.generators.trace_processor_table.public import CppTableId
 from python.generators.trace_processor_table.public import CppUint32
 
+from src.trace_processor.tables.metadata_tables import MACHINE_TABLE
+
 TRACK_TABLE = Table(
     python_module=__file__,
     class_name="TrackTable",
@@ -32,6 +35,7 @@
         C("name", CppString()),
         C("parent_id", CppOptional(CppSelfTableId())),
         C("source_arg_set_id", CppOptional(CppUint32())),
+        C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     tabledoc=TableDoc(
         doc='''
@@ -60,6 +64,10 @@
                       track orginated from atrace, Chrome tracepoints etc.
                     ''',
                     joinable='args.arg_set_id'),
+            'machine_id':
+                '''
+                  Machine identifier, non-null for tracks on a remote machine.
+                ''',
         }))
 
 PROCESS_TRACK_TABLE = Table(
diff --git a/src/trace_processor/trace_processor_context.cc b/src/trace_processor/trace_processor_context.cc
index 2ee7b3d..c861f63 100644
--- a/src/trace_processor/trace_processor_context.cc
+++ b/src/trace_processor/trace_processor_context.cc
@@ -15,6 +15,7 @@
  */
 
 #include "src/trace_processor/types/trace_processor_context.h"
+#include <optional>
 
 #include "src/trace_processor/forwarding_trace_parser.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
@@ -27,6 +28,7 @@
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
 #include "src/trace_processor/importers/common/global_args_tracker.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
@@ -36,9 +38,12 @@
 #include "src/trace_processor/importers/common/stack_profile_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/ftrace/ftrace_module.h"
+#include "src/trace_processor/importers/proto/chrome_track_event.descriptor.h"
+#include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
 #include "src/trace_processor/importers/proto/perf_sample_tracker.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/proto_trace_parser.h"
+#include "src/trace_processor/importers/proto/track_event.descriptor.h"
 #include "src/trace_processor/importers/proto/track_event_module.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
 #include "src/trace_processor/types/destructible.h"
@@ -46,8 +51,59 @@
 namespace perfetto {
 namespace trace_processor {
 
+TraceProcessorContext::TraceProcessorContext(const InitArgs& args)
+    : config(args.config), storage(args.storage) {
+  // Init the trackers.
+  machine_tracker.reset(new MachineTracker(this, args.raw_machine_id));
+  if (!machine_id()) {
+    multi_machine_trace_manager.reset(new MultiMachineTraceManager(this));
+  }
+  track_tracker.reset(new TrackTracker(this));
+  async_track_set_tracker.reset(new AsyncTrackSetTracker(this));
+  args_tracker.reset(new ArgsTracker(this));
+  args_translation_table.reset(new ArgsTranslationTable(storage.get()));
+  slice_tracker.reset(new SliceTracker(this));
+  slice_translation_table.reset(new SliceTranslationTable(storage.get()));
+  flow_tracker.reset(new FlowTracker(this));
+  event_tracker.reset(new EventTracker(this));
+  sched_event_tracker.reset(new SchedEventTracker(this));
+  process_tracker.reset(new ProcessTracker(this));
+  clock_tracker.reset(new ClockTracker(this));
+  clock_converter.reset(new ClockConverter(this));
+  mapping_tracker.reset(new MappingTracker(this));
+  perf_sample_tracker.reset(new PerfSampleTracker(this));
+  stack_profile_tracker.reset(new StackProfileTracker(this));
+  metadata_tracker.reset(new MetadataTracker(storage.get()));
+  global_args_tracker.reset(new GlobalArgsTracker(storage.get()));
+  {
+    descriptor_pool_.reset(new DescriptorPool());
+    auto status = descriptor_pool_->AddFromFileDescriptorSet(
+        kTrackEventDescriptor.data(), kTrackEventDescriptor.size());
+
+    PERFETTO_DCHECK(status.ok());
+
+    status = descriptor_pool_->AddFromFileDescriptorSet(
+        kChromeTrackEventDescriptor.data(), kChromeTrackEventDescriptor.size());
+
+    PERFETTO_DCHECK(status.ok());
+  }
+
+  slice_tracker->SetOnSliceBeginCallback(
+      [this](TrackId track_id, SliceId slice_id) {
+        flow_tracker->ClosePendingEventsOnTrack(track_id, slice_id);
+      });
+}
+
 TraceProcessorContext::TraceProcessorContext() = default;
 TraceProcessorContext::~TraceProcessorContext() = default;
 
+std::optional<MachineId> TraceProcessorContext::machine_id() const {
+  if (!machine_tracker) {
+    // Doesn't require that |machine_tracker| is initialzed, e.g. in unit tests.
+    return std::nullopt;
+  }
+  return machine_tracker->machine_id();
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/trace_processor_storage_impl.cc b/src/trace_processor/trace_processor_storage_impl.cc
index b0c7a15..b6cc299 100644
--- a/src/trace_processor/trace_processor_storage_impl.cc
+++ b/src/trace_processor/trace_processor_storage_impl.cc
@@ -26,6 +26,7 @@
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
+#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
@@ -47,48 +48,8 @@
 namespace perfetto {
 namespace trace_processor {
 
-TraceProcessorStorageImpl::TraceProcessorStorageImpl(const Config& cfg) {
-  context_.config = cfg;
-
-  context_.storage.reset(new TraceStorage(context_.config));
-  context_.track_tracker.reset(new TrackTracker(&context_));
-  context_.async_track_set_tracker.reset(new AsyncTrackSetTracker(&context_));
-  context_.args_tracker.reset(new ArgsTracker(&context_));
-  context_.args_translation_table.reset(
-      new ArgsTranslationTable(context_.storage.get()));
-  context_.slice_tracker.reset(new SliceTracker(&context_));
-  context_.slice_translation_table.reset(
-      new SliceTranslationTable(context_.storage.get()));
-  context_.flow_tracker.reset(new FlowTracker(&context_));
-  context_.event_tracker.reset(new EventTracker(&context_));
-  context_.sched_event_tracker.reset(new SchedEventTracker(&context_));
-  context_.process_tracker.reset(new ProcessTracker(&context_));
-  context_.clock_tracker.reset(new ClockTracker(&context_));
-  context_.clock_converter.reset(new ClockConverter(&context_));
-  context_.mapping_tracker.reset(new MappingTracker(&context_));
-  context_.perf_sample_tracker.reset(new PerfSampleTracker(&context_));
-  context_.stack_profile_tracker.reset(new StackProfileTracker(&context_));
-  context_.metadata_tracker.reset(new MetadataTracker(context_.storage.get()));
-  context_.global_args_tracker.reset(
-      new GlobalArgsTracker(context_.storage.get()));
-  {
-    context_.descriptor_pool_.reset(new DescriptorPool());
-    auto status = context_.descriptor_pool_->AddFromFileDescriptorSet(
-        kTrackEventDescriptor.data(), kTrackEventDescriptor.size());
-
-    PERFETTO_DCHECK(status.ok());
-
-    status = context_.descriptor_pool_->AddFromFileDescriptorSet(
-        kChromeTrackEventDescriptor.data(), kChromeTrackEventDescriptor.size());
-
-    PERFETTO_DCHECK(status.ok());
-  }
-
-  context_.slice_tracker->SetOnSliceBeginCallback(
-      [this](TrackId track_id, SliceId slice_id) {
-        context_.flow_tracker->ClosePendingEventsOnTrack(track_id, slice_id);
-      });
-
+TraceProcessorStorageImpl::TraceProcessorStorageImpl(const Config& cfg)
+    : context_({cfg, std::make_shared<TraceStorage>(cfg)}) {
   RegisterDefaultModules(&context_);
 }
 
@@ -163,6 +124,8 @@
   context.system_info_tracker = std::move(context_.system_info_tracker);
 
   context_ = std::move(context);
+
+  // TODO(chinglinyu): also need to destroy secondary contextes.
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/types/BUILD.gn b/src/trace_processor/types/BUILD.gn
index 0605ea1..6b66c53 100644
--- a/src/trace_processor/types/BUILD.gn
+++ b/src/trace_processor/types/BUILD.gn
@@ -31,6 +31,7 @@
     "../../../include/perfetto/ext/base",
     "../../../include/perfetto/trace_processor",
     "../containers",
+    "../tables:tables_python",
   ]
 }
 
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index 164df00..af2837f 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -21,6 +21,7 @@
 #include <vector>
 
 #include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/types/destructible.h"
 
 namespace perfetto {
@@ -55,8 +56,10 @@
 class StackProfileTracker;
 class HeapGraphTracker;
 class PerfSampleTracker;
+class MachineTracker;
 class MappingTracker;
 class MetadataTracker;
+class MultiMachineTraceManager;
 class PacketAnalyzer;
 class ProtoImporterModule;
 class TrackEventModule;
@@ -71,8 +74,17 @@
 class TrackTracker;
 class DescriptorPool;
 
+using MachineId = tables::MachineTable::Id;
+
 class TraceProcessorContext {
  public:
+  struct InitArgs {
+    Config config;
+    std::shared_ptr<TraceStorage> storage;
+    uint32_t raw_machine_id = 0;
+  };
+  explicit TraceProcessorContext(const InitArgs&);
+  // The default constructor is used in testing.
   TraceProcessorContext();
   ~TraceProcessorContext();
 
@@ -81,10 +93,14 @@
 
   Config config;
 
-  std::unique_ptr<TraceStorage> storage;
+  // |storage| is shared among multiple contexts in multi-machine tracing.
+  std::shared_ptr<TraceStorage> storage;
 
   std::unique_ptr<ChunkedTraceReader> chunk_reader;
-  std::unique_ptr<TraceSorter> sorter;
+
+  // The sorter is used to sort trace data by timestamp and is shared among
+  // multiple machines.
+  std::shared_ptr<TraceSorter> sorter;
 
   // Keep the global tracker before the args tracker as we access the global
   // tracker in the destructor of the args tracker. Also keep it before other
@@ -104,6 +120,7 @@
   std::unique_ptr<ClockTracker> clock_tracker;
   std::unique_ptr<ClockConverter> clock_converter;
   std::unique_ptr<MappingTracker> mapping_tracker;
+  std::unique_ptr<MachineTracker> machine_tracker;
   std::unique_ptr<PerfSampleTracker> perf_sample_tracker;
   std::unique_ptr<StackProfileTracker> stack_profile_tracker;
   std::unique_ptr<MetadataTracker> metadata_tracker;
@@ -171,6 +188,11 @@
   bool uuid_found_in_trace = false;
 
   TraceType trace_type = kUnknownTraceType;
+
+  std::optional<MachineId> machine_id() const;
+
+  // Manages the contexts for reading trace data emitted from remote machines.
+  std::unique_ptr<MultiMachineTraceManager> multi_machine_trace_manager;
 };
 
 }  // namespace trace_processor
diff --git a/src/traced/probes/ftrace/atrace_wrapper.cc b/src/traced/probes/ftrace/atrace_wrapper.cc
index 7254325..1a64544 100644
--- a/src/traced/probes/ftrace/atrace_wrapper.cc
+++ b/src/traced/probes/ftrace/atrace_wrapper.cc
@@ -38,9 +38,6 @@
 
 namespace {
 
-RunAtraceFunction g_run_atrace_for_testing = nullptr;
-std::optional<bool> g_is_old_atrace_for_testing{};
-
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
 // Args should include "atrace" for argv[0].
 bool ExecvAtrace(const std::vector<std::string>& args,
@@ -174,25 +171,23 @@
 
 }  // namespace
 
-bool RunAtrace(const std::vector<std::string>& args,
-               std::string* atrace_errors) {
-  if (g_run_atrace_for_testing)
-    return g_run_atrace_for_testing(args, atrace_errors);
+AtraceWrapper::~AtraceWrapper() = default;
+
+AtraceWrapperImpl::~AtraceWrapperImpl() = default;
+
+bool AtraceWrapperImpl::RunAtrace(const std::vector<std::string>& args,
+                                  std::string* atrace_errors) {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
   return ExecvAtrace(args, atrace_errors);
 #else
+  base::ignore_result(args);
+  base::ignore_result(atrace_errors);
   PERFETTO_LOG("Atrace only supported on Android.");
   return false;
 #endif
 }
 
-void SetRunAtraceForTesting(RunAtraceFunction f) {
-  g_run_atrace_for_testing = f;
-}
-
-bool IsOldAtrace() {
-  if (g_is_old_atrace_for_testing.has_value())
-    return *g_is_old_atrace_for_testing;
+bool AtraceWrapperImpl::IsOldAtrace() {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) && \
     !PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
   // Sideloaded case. We could be sideloaded on a modern device or an older one.
@@ -207,12 +202,4 @@
 #endif
 }
 
-void SetIsOldAtraceForTesting(bool value) {
-  g_is_old_atrace_for_testing = value;
-}
-
-void ClearIsOldAtraceForTesting() {
-  g_is_old_atrace_for_testing.reset();
-}
-
 }  // namespace perfetto
diff --git a/src/traced/probes/ftrace/atrace_wrapper.h b/src/traced/probes/ftrace/atrace_wrapper.h
index 29847aa..5c6a148 100644
--- a/src/traced/probes/ftrace/atrace_wrapper.h
+++ b/src/traced/probes/ftrace/atrace_wrapper.h
@@ -18,27 +18,30 @@
 #define SRC_TRACED_PROBES_FTRACE_ATRACE_WRAPPER_H_
 
 #include <string>
-#include <type_traits>
 #include <vector>
 
 namespace perfetto {
 
-using RunAtraceFunction =
-    std::add_pointer<bool(const std::vector<std::string>& /*args*/,
-                          std::string* /*atrace_errors*/)>::type;
+class AtraceWrapper {
+ public:
+  virtual ~AtraceWrapper();
+  // When we are sideloaded on an old version of Android (pre P), we cannot use
+  // atrace --only_userspace because that option doesn't exist. In that case we:
+  // - Just use atrace --async_start/stop, which will cause atrace to also
+  //   poke at ftrace.
+  // - Suppress the checks for "somebody else enabled ftrace unexpectedly".
+  virtual bool IsOldAtrace() = 0;
+  virtual bool RunAtrace(const std::vector<std::string>& args,
+                         std::string* atrace_errors) = 0;
+};
 
-// When we are sideloaded on an old version of Android (pre P), we cannot use
-// atrace --only_userspace because that option doesn't exist. In that case we:
-// - Just use atrace --async_start/stop, which will cause atrace to also
-//   poke at ftrace.
-// - Suppress the checks for "somebody else enabled ftrace unexpectedly".
-bool IsOldAtrace();
-void SetIsOldAtraceForTesting(bool);
-void ClearIsOldAtraceForTesting();
-
-bool RunAtrace(const std::vector<std::string>& args,
-               std::string* atrace_errors);
-void SetRunAtraceForTesting(RunAtraceFunction);
+class AtraceWrapperImpl : public AtraceWrapper {
+ public:
+  ~AtraceWrapperImpl() override;
+  bool IsOldAtrace() override;
+  bool RunAtrace(const std::vector<std::string>& args,
+                 std::string* atrace_errors) override;
+};
 
 }  // namespace perfetto
 
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index e472d06..e11184b 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -4758,6 +4758,88 @@
        kUnsetFtraceId,
        347,
        kUnsetSize},
+      {"fastrpc_dma_free",
+       "fastrpc",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "cid", 1, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "phys", 2, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "size", 3, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       498,
+       kUnsetSize},
+      {"fastrpc_dma_alloc",
+       "fastrpc",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "cid", 1, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "phys", 2, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "size", 3, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "attr", 4, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "mflags", 5, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       499,
+       kUnsetSize},
+      {"fastrpc_dma_unmap",
+       "fastrpc",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "cid", 1, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "phys", 2, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "size", 3, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       500,
+       kUnsetSize},
+      {"fastrpc_dma_map",
+       "fastrpc",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "cid", 1, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "fd", 2, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "phys", 3, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "size", 4, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "len", 5, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "attr", 6, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "mflags", 7, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       501,
+       kUnsetSize},
       {"fence_init",
        "fence",
        {
@@ -7515,22 +7597,16 @@
        "panel",
        {
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "pid", 1, ProtoSchemaType::kInt32,
+            "type", 1, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "trace_name", 2, ProtoSchemaType::kString,
+            "pid", 2, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "trace_begin", 3, ProtoSchemaType::kUint32,
+            "name", 3, ProtoSchemaType::kString,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "name", 4, ProtoSchemaType::kString,
-            TranslationStrategy::kInvalidTranslationStrategy},
-           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "type", 5, ProtoSchemaType::kUint32,
-            TranslationStrategy::kInvalidTranslationStrategy},
-           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "value", 6, ProtoSchemaType::kInt32,
+            "value", 4, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
        },
        kUnsetFtraceId,
@@ -7540,55 +7616,55 @@
        "perf_trace_counters",
        {
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "old_pid", 1, ProtoSchemaType::kInt32,
+            "prev_comm", 1, ProtoSchemaType::kString,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "new_pid", 2, ProtoSchemaType::kInt32,
+            "prev_pid", 2, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "cctr", 3, ProtoSchemaType::kUint32,
+            "cyc", 3, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr0", 4, ProtoSchemaType::kUint32,
+            "inst", 4, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr1", 5, ProtoSchemaType::kUint32,
+            "stallbm", 5, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr2", 6, ProtoSchemaType::kUint32,
+            "l3dm", 6, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr3", 7, ProtoSchemaType::kUint32,
+            "old_pid", 7, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "lctr0", 8, ProtoSchemaType::kUint32,
+            "new_pid", 8, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "lctr1", 9, ProtoSchemaType::kUint32,
+            "cctr", 9, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr4", 10, ProtoSchemaType::kUint32,
+            "ctr0", 10, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr5", 11, ProtoSchemaType::kUint32,
+            "ctr1", 11, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "prev_comm", 12, ProtoSchemaType::kString,
+            "ctr2", 12, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "prev_pid", 13, ProtoSchemaType::kInt32,
+            "ctr3", 13, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "cyc", 14, ProtoSchemaType::kUint32,
+            "lctr0", 14, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "inst", 15, ProtoSchemaType::kUint32,
+            "lctr1", 15, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "stallbm", 16, ProtoSchemaType::kUint32,
+            "ctr4", 16, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "l3dm", 17, ProtoSchemaType::kUint32,
+            "ctr5", 17, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
        },
        kUnsetFtraceId,
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index 3e2b9ab..e4d8a19 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -579,11 +579,13 @@
 
 FtraceConfigMuxer::FtraceConfigMuxer(
     FtraceProcfs* ftrace,
+    AtraceWrapper* atrace_wrapper,
     ProtoTranslationTable* table,
     SyscallTable syscalls,
     std::map<std::string, std::vector<GroupAndName>> vendor_events,
     bool secondary_instance)
     : ftrace_(ftrace),
+      atrace_wrapper_(atrace_wrapper),
       table_(table),
       syscalls_(std::move(syscalls)),
       current_state_(),
@@ -654,7 +656,7 @@
           "atrace_apps options as they affect global state");
       return false;
     }
-    if (IsOldAtrace() && !ds_configs_.empty()) {
+    if (atrace_wrapper_->IsOldAtrace() && !ds_configs_.empty()) {
       PERFETTO_ELOG(
           "Concurrent atrace sessions are not supported before Android P, "
           "bailing out.");
@@ -1040,7 +1042,6 @@
   }
 }
 
-// static
 bool FtraceConfigMuxer::StartAtrace(const std::vector<std::string>& apps,
                                     const std::vector<std::string>& categories,
                                     std::string* atrace_errors) {
@@ -1049,7 +1050,7 @@
   std::vector<std::string> args;
   args.push_back("atrace");  // argv0 for exec()
   args.push_back("--async_start");
-  if (!IsOldAtrace())
+  if (!atrace_wrapper_->IsOldAtrace())
     args.push_back("--only_userspace");
 
   for (const auto& category : categories)
@@ -1066,7 +1067,7 @@
     args.push_back(arg);
   }
 
-  bool result = RunAtrace(args, atrace_errors);
+  bool result = atrace_wrapper_->RunAtrace(args, atrace_errors);
   PERFETTO_DLOG("...done (%s)", result ? "success" : "fail");
   return result;
 }
@@ -1077,9 +1078,9 @@
   PERFETTO_DLOG("Stop atrace...");
 
   std::vector<std::string> args{"atrace", "--async_stop"};
-  if (!IsOldAtrace())
+  if (!atrace_wrapper_->IsOldAtrace())
     args.push_back("--only_userspace");
-  if (RunAtrace(args, /*atrace_errors=*/nullptr)) {
+  if (atrace_wrapper_->RunAtrace(args, /*atrace_errors=*/nullptr)) {
     current_state_.atrace_categories.clear();
     current_state_.atrace_apps.clear();
     current_state_.atrace_on = false;
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.h b/src/traced/probes/ftrace/ftrace_config_muxer.h
index e11efb0..4c3f10b 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.h
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.h
@@ -22,6 +22,7 @@
 #include <set>
 
 #include "src/kernel_utils/syscall_table.h"
+#include "src/traced/probes/ftrace/atrace_wrapper.h"
 #include "src/traced/probes/ftrace/compact_sched.h"
 #include "src/traced/probes/ftrace/ftrace_config_utils.h"
 #include "src/traced/probes/ftrace/ftrace_print_filter.h"
@@ -106,6 +107,7 @@
   // should outlive this instance.
   FtraceConfigMuxer(
       FtraceProcfs* ftrace,
+      AtraceWrapper* atrace_wrapper,
       ProtoTranslationTable* table,
       SyscallTable syscalls,
       std::map<std::string, std::vector<GroupAndName>> vendor_events,
@@ -174,10 +176,6 @@
       const SyscallTable& syscalls);
 
  private:
-  static bool StartAtrace(const std::vector<std::string>& apps,
-                          const std::vector<std::string>& categories,
-                          std::string* atrace_errors);
-
   struct FtraceState {
     EventFilter ftrace_events;
     std::set<size_t> syscall_filter;  // syscall ids or kAllSyscallsId
@@ -198,6 +196,9 @@
   void SetupBufferSize(const FtraceConfig& request);
   bool UpdateBufferPercent();
   void UpdateAtrace(const FtraceConfig& request, std::string* atrace_errors);
+  bool StartAtrace(const std::vector<std::string>& apps,
+                   const std::vector<std::string>& categories,
+                   std::string* atrace_errors);
   void DisableAtrace();
 
   // This processes the config to get the exact events.
@@ -226,6 +227,7 @@
   bool SetSyscallEventFilter(const EventFilter& extra_syscalls);
 
   FtraceProcfs* ftrace_;
+  AtraceWrapper* atrace_wrapper_;
   ProtoTranslationTable* table_;
   SyscallTable syscalls_;
 
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
index 3ef5635..513d851 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
@@ -93,19 +93,10 @@
               (const, override));
 };
 
-struct MockRunAtrace {
-  MockRunAtrace() {
-    static MockRunAtrace* instance;
-    instance = this;
-    SetRunAtraceForTesting(
-        [](const std::vector<std::string>& args, std::string* atrace_errors) {
-          return instance->RunAtrace(args, atrace_errors);
-        });
-  }
-
-  ~MockRunAtrace() { SetRunAtraceForTesting(nullptr); }
-
+class MockAtraceWrapper : public AtraceWrapper {
+ public:
   MOCK_METHOD(bool, RunAtrace, (const std::vector<std::string>&, std::string*));
+  MOCK_METHOD(bool, IsOldAtrace, ());
 };
 
 class MockProtoTranslationTable : public ProtoTranslationTable {
@@ -131,22 +122,66 @@
               (const, override));
 };
 
+TEST(ComputeCpuBufferSizeInPagesTest, DifferentCases) {
+  constexpr auto test = ComputeCpuBufferSizeInPages;
+  auto KbToPages = [](uint64_t kb) {
+    return kb * 1024 / base::GetSysPageSize();
+  };
+  auto kMaxBufSizePages = KbToPages(64 * 1024);
+  int64_t kNoRamInfo = 0;
+  bool kExactSize = false;
+  bool kLowerBoundSize = true;
+  int64_t kLowRamPages =
+      static_cast<int64_t>(KbToPages(3 * (1ULL << 20) + 512 * (1ULL << 10)));
+  int64_t kHighRamPages =
+      static_cast<int64_t>(KbToPages(7 * (1ULL << 20) + 512 * (1ULL << 10)));
+
+  // No buffer size given: good default.
+  EXPECT_EQ(test(0, kExactSize, kNoRamInfo), KbToPages(2048));
+  // Default depends on device ram size.
+  EXPECT_EQ(test(0, kExactSize, kLowRamPages), KbToPages(2048));
+  EXPECT_EQ(test(0, kExactSize, kHighRamPages), KbToPages(8192));
+
+  // buffer_size_lower_bound lets us choose a higher default than given.
+  // default > requested:
+  EXPECT_EQ(test(4096, kExactSize, kHighRamPages), KbToPages(4096));
+  EXPECT_EQ(test(4096, kLowerBoundSize, kHighRamPages), KbToPages(8192));
+  // requested > default:
+  EXPECT_EQ(test(4096, kExactSize, kLowRamPages), KbToPages(4096));
+  EXPECT_EQ(test(4096, kLowerBoundSize, kLowRamPages), KbToPages(4096));
+  // requested > default:
+  EXPECT_EQ(test(16384, kExactSize, kHighRamPages), KbToPages(16384));
+  EXPECT_EQ(test(16384, kLowerBoundSize, kHighRamPages), KbToPages(16384));
+
+  // Buffer size given way too big: good default.
+  EXPECT_EQ(test(10 * (1ULL << 20), kExactSize, kNoRamInfo), kMaxBufSizePages);
+  EXPECT_EQ(test(512 * 1024, kExactSize, kNoRamInfo), kMaxBufSizePages);
+
+  // Your size ends up with less than 1 page per cpu -> 1 page.
+  EXPECT_EQ(test(3, kExactSize, kNoRamInfo), 1u);
+  // You picked a good size -> your size rounded to nearest page.
+  EXPECT_EQ(test(42, kExactSize, kNoRamInfo), KbToPages(42));
+
+  // Sysconf returning an error is ok.
+  EXPECT_EQ(test(0, kExactSize, -1), KbToPages(2048));
+  EXPECT_EQ(test(4096, kExactSize, -1), KbToPages(4096));
+}
+
+// Base fixture that provides some dependencies but doesn't construct a
+// FtraceConfigMuxer.
 class FtraceConfigMuxerTest : public ::testing::Test {
  protected:
-  void SetUp() override {
-    // Don't probe for older SDK levels, that would relax the atrace-related
-    // checks on older versions of Android (But some tests here test those).
-    // We want the unittests to behave consistently (as if we were on a post P
-    // device) regardless of the Android versions they run on.
-    SetIsOldAtraceForTesting(false);
+  FtraceConfigMuxerTest() {
+    ON_CALL(atrace_wrapper_, RunAtrace).WillByDefault(Return(true));
+    ON_CALL(atrace_wrapper_, IsOldAtrace).WillByDefault(Return(false));
   }
-  void TearDown() override { ClearIsOldAtraceForTesting(); }
+
   std::unique_ptr<MockProtoTranslationTable> GetMockTable() {
     std::vector<Field> common_fields;
     std::vector<Event> events;
     return std::unique_ptr<MockProtoTranslationTable>(
         new MockProtoTranslationTable(
-            &table_procfs_, events, std::move(common_fields),
+            &ftrace_, events, std::move(common_fields),
             ProtoTranslationTable::DefaultPageHeaderSpecForTesting(),
             InvalidCompactSchedEventFormatForTesting()));
   }
@@ -225,956 +260,25 @@
     }
 
     return std::unique_ptr<ProtoTranslationTable>(new ProtoTranslationTable(
-        &table_procfs_, events, std::move(common_fields),
+        &ftrace_, events, std::move(common_fields),
         ProtoTranslationTable::DefaultPageHeaderSpecForTesting(),
         compact_format, PrintkMap()));
   }
 
-  NiceMock<MockFtraceProcfs> table_procfs_;
-  std::unique_ptr<ProtoTranslationTable> table_ = CreateFakeTable();
+  NiceMock<MockFtraceProcfs> ftrace_;
+  NiceMock<MockAtraceWrapper> atrace_wrapper_;
 };
 
-TEST_F(FtraceConfigMuxerTest, ComputeCpuBufferSizeInPages) {
-  constexpr auto test = ComputeCpuBufferSizeInPages;
-  auto KbToPages = [](uint64_t kb) {
-    return kb * 1024 / base::GetSysPageSize();
-  };
-  auto kMaxBufSizePages = KbToPages(64 * 1024);
-  int64_t kNoRamInfo = 0;
-  bool kExactSize = false;
-  bool kLowerBoundSize = true;
-  int64_t kLowRamPages =
-      static_cast<int64_t>(KbToPages(3 * (1ULL << 20) + 512 * (1ULL << 10)));
-  int64_t kHighRamPages =
-      static_cast<int64_t>(KbToPages(7 * (1ULL << 20) + 512 * (1ULL << 10)));
-
-  // No buffer size given: good default.
-  EXPECT_EQ(test(0, kExactSize, kNoRamInfo), KbToPages(2048));
-  // Default depends on device ram size.
-  EXPECT_EQ(test(0, kExactSize, kLowRamPages), KbToPages(2048));
-  EXPECT_EQ(test(0, kExactSize, kHighRamPages), KbToPages(8192));
-
-  // buffer_size_lower_bound lets us choose a higher default than given.
-  // default > requested:
-  EXPECT_EQ(test(4096, kExactSize, kHighRamPages), KbToPages(4096));
-  EXPECT_EQ(test(4096, kLowerBoundSize, kHighRamPages), KbToPages(8192));
-  // requested > default:
-  EXPECT_EQ(test(4096, kExactSize, kLowRamPages), KbToPages(4096));
-  EXPECT_EQ(test(4096, kLowerBoundSize, kLowRamPages), KbToPages(4096));
-  // requested > default:
-  EXPECT_EQ(test(16384, kExactSize, kHighRamPages), KbToPages(16384));
-  EXPECT_EQ(test(16384, kLowerBoundSize, kHighRamPages), KbToPages(16384));
-
-  // Buffer size given way too big: good default.
-  EXPECT_EQ(test(10 * (1ULL << 20), kExactSize, kNoRamInfo), kMaxBufSizePages);
-  EXPECT_EQ(test(512 * 1024, kExactSize, kNoRamInfo), kMaxBufSizePages);
-
-  // Your size ends up with less than 1 page per cpu -> 1 page.
-  EXPECT_EQ(test(3, kExactSize, kNoRamInfo), 1u);
-  // You picked a good size -> your size rounded to nearest page.
-  EXPECT_EQ(test(42, kExactSize, kNoRamInfo), KbToPages(42));
-
-  // Sysconf returning an error is ok.
-  EXPECT_EQ(test(0, kExactSize, -1), KbToPages(2048));
-  EXPECT_EQ(test(4096, kExactSize, -1), KbToPages(4096));
-}
-
-TEST_F(FtraceConfigMuxerTest, GenericSyscallFiltering) {
+TEST_F(FtraceConfigMuxerTest, SecondaryInstanceDoNotSupportAtrace) {
   auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"raw_syscalls/sys_enter"});
-  *config.add_syscall_events() = "sys_open";
-  *config.add_syscall_events() = "sys_read";
-
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-  EXPECT_CALL(ftrace, WriteToFile(_, _)).WillRepeatedly(Return(true));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/raw_syscalls/sys_enter/filter",
-                                  "id == 0 || id == 1"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/raw_syscalls/sys_exit/filter",
-                                  "id == 0 || id == 1"));
-
-  FtraceConfigId id = 37;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const std::set<size_t>& filter = model.GetSyscallFilterForTesting();
-  ASSERT_THAT(filter, UnorderedElementsAre(0, 1));
-}
-
-TEST_F(FtraceConfigMuxerTest, UnknownSyscallFilter) {
-  auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
-
-  FtraceConfig config = CreateFtraceConfig({"raw_syscalls/sys_enter"});
-  config.add_syscall_events("sys_open");
-  config.add_syscall_events("sys_not_a_call");
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-
-  // Unknown syscall is ignored.
-  ASSERT_TRUE(model.SetupConfig(/*id = */ 73, config));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
-}
-
-TEST_F(FtraceConfigMuxerTest, SyscallFilterMuxing) {
-  auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
-
-  FtraceConfig empty_config = CreateFtraceConfig({});
-
-  FtraceConfig syscall_config = empty_config;
-  syscall_config.add_ftrace_events("raw_syscalls/sys_enter");
-
-  FtraceConfig syscall_open_config = syscall_config;
-  syscall_open_config.add_syscall_events("sys_open");
-
-  FtraceConfig syscall_read_config = syscall_config;
-  syscall_read_config.add_syscall_events("sys_read");
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-
-  // Expect no filter for non-syscall config.
-  ASSERT_TRUE(model.SetupConfig(/* id= */ 179239, empty_config));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
-
-  // Expect no filter for syscall config with no specified events.
-  FtraceConfigId syscall_id = 73;
-  ASSERT_TRUE(model.SetupConfig(syscall_id, syscall_config));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
-
-  // Still expect no filter to satisfy this and the above.
-  FtraceConfigId syscall_open_id = 101;
-  ASSERT_TRUE(model.SetupConfig(syscall_open_id, syscall_open_config));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
-
-  // After removing the generic syscall trace, only the one with filter is left.
-  ASSERT_TRUE(model.RemoveConfig(syscall_id));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
-
-  // With sys_read and sys_open traced separately, filter includes both.
-  FtraceConfigId syscall_read_id = 57;
-  ASSERT_TRUE(model.SetupConfig(syscall_read_id, syscall_read_config));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0, 1));
-
-  // After removing configs with filters, filter is reset to empty.
-  ASSERT_TRUE(model.RemoveConfig(syscall_open_id));
-  ASSERT_TRUE(model.RemoveConfig(syscall_read_id));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
-}
-
-TEST_F(FtraceConfigMuxerTest, AddGenericEvent) {
-  auto mock_table = GetMockTable();
-  MockFtraceProcfs ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"power/cpu_frequency"});
-
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
-
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/power/cpu_frequency/enable", "1"));
-  EXPECT_CALL(*mock_table, GetEvent(GroupAndName("power", "cpu_frequency")))
-      .Times(AnyNumber());
-
-  static constexpr int kExpectedEventId = 77;
-  Event event_to_return;
-  event_to_return.name = "cpu_frequency";
-  event_to_return.group = "power";
-  event_to_return.ftrace_event_id = kExpectedEventId;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("power", "cpu_frequency")))
-      .WillByDefault(Return(&event_to_return));
-  EXPECT_CALL(*mock_table,
-              GetOrCreateEvent(GroupAndName("power", "cpu_frequency")));
-
-  FtraceConfigId id = 7;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              ElementsAreArray({kExpectedEventId}));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  ASSERT_THAT(central_filter->GetEnabledEvents(),
-              ElementsAreArray({kExpectedEventId}));
-}
-
-TEST_F(FtraceConfigMuxerTest, AddSameNameEvents) {
-  auto mock_table = GetMockTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"group_one/foo", "group_two/foo"});
-
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
-
-  static constexpr int kEventId1 = 1;
-  Event event1;
-  event1.name = "foo";
-  event1.group = "group_one";
-  event1.ftrace_event_id = kEventId1;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_one", "foo")))
-      .WillByDefault(Return(&event1));
-  EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_one", "foo")));
-
-  static constexpr int kEventId2 = 2;
-  Event event2;
-  event2.name = "foo";
-  event2.group = "group_two";
-  event2.ftrace_event_id = kEventId2;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")))
-      .WillByDefault(Return(&event2));
-  EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")));
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-
-  FtraceConfigId id = 5;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              ElementsAreArray({kEventId1, kEventId2}));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  ASSERT_THAT(central_filter->GetEnabledEvents(),
-              ElementsAreArray({kEventId1, kEventId2}));
-}
-
-TEST_F(FtraceConfigMuxerTest, AddAllEvents) {
-  auto mock_table = GetMockTable();
-  MockFtraceProcfs ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"sched/*"});
-
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_new_event/enable", "1"));
-
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
-  std::set<std::string> n = {"sched_switch", "sched_new_event"};
-  ON_CALL(ftrace, GetEventNamesForGroup("events/sched"))
-      .WillByDefault(Return(n));
-  EXPECT_CALL(ftrace, GetEventNamesForGroup("events/sched")).Times(1);
-
-  // Non-generic event.
-  static constexpr int kSchedSwitchEventId = 1;
-  Event sched_switch = {"sched_switch", "sched", {}, 0, 0, 0};
-  sched_switch.ftrace_event_id = kSchedSwitchEventId;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("sched", "sched_switch")))
-      .WillByDefault(Return(&sched_switch));
-  EXPECT_CALL(*mock_table,
-              GetOrCreateEvent(GroupAndName("sched", "sched_switch")))
-      .Times(AnyNumber());
-
-  // Generic event.
-  static constexpr int kGenericEventId = 2;
-  Event event_to_return;
-  event_to_return.name = "sched_new_event";
-  event_to_return.group = "sched";
-  event_to_return.ftrace_event_id = kGenericEventId;
-  ON_CALL(*mock_table,
-          GetOrCreateEvent(GroupAndName("sched", "sched_new_event")))
-      .WillByDefault(Return(&event_to_return));
-  EXPECT_CALL(*mock_table,
-              GetOrCreateEvent(GroupAndName("sched", "sched_new_event")));
-
-  FtraceConfigId id = 13;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-  ASSERT_TRUE(id);
-
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              ElementsAreArray({kSchedSwitchEventId, kGenericEventId}));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  ASSERT_THAT(central_filter->GetEnabledEvents(),
-              ElementsAreArray({kSchedSwitchEventId, kGenericEventId}));
-}
-
-TEST_F(FtraceConfigMuxerTest, TwoWildcardGroups) {
-  auto mock_table = GetMockTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"group_one/*", "group_two/*"});
-
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
-
-  std::set<std::string> event_names = {"foo"};
-  ON_CALL(ftrace, GetEventNamesForGroup("events/group_one"))
-      .WillByDefault(Return(event_names));
-  EXPECT_CALL(ftrace, GetEventNamesForGroup("events/group_one"))
-      .Times(AnyNumber());
-
-  ON_CALL(ftrace, GetEventNamesForGroup("events/group_two"))
-      .WillByDefault(Return(event_names));
-  EXPECT_CALL(ftrace, GetEventNamesForGroup("events/group_two"))
-      .Times(AnyNumber());
-
-  static constexpr int kEventId1 = 1;
-  Event event1;
-  event1.name = "foo";
-  event1.group = "group_one";
-  event1.ftrace_event_id = kEventId1;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_one", "foo")))
-      .WillByDefault(Return(&event1));
-  EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_one", "foo")));
-
-  static constexpr int kEventId2 = 2;
-  Event event2;
-  event2.name = "foo";
-  event2.group = "group_two";
-  event2.ftrace_event_id = kEventId2;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")))
-      .WillByDefault(Return(&event2));
-  EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")));
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-
-  FtraceConfigId id = 23;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              ElementsAreArray({kEventId1, kEventId2}));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  ASSERT_THAT(central_filter->GetEnabledEvents(),
-              ElementsAreArray({kEventId1, kEventId2}));
-}
-
-TEST_F(FtraceConfigMuxerTest, TurnFtraceOnOff) {
-  MockFtraceProcfs ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"sched_switch", "foo"});
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
-
-  FtraceConfigId id = 97;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              ElementsAreArray({kFakeSchedSwitchEventId}));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  ASSERT_THAT(central_filter->GetEnabledEvents(),
-              ElementsAreArray({kFakeSchedSwitchEventId}));
-
-  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace));
-  EXPECT_CALL(ftrace, NumberOfCpus()).Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_percent", _))
-      .WillRepeatedly(Return(true));
-
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", PageSizeKb()));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-
-  ASSERT_TRUE(model.RemoveConfig(id));
-}
-
-TEST_F(FtraceConfigMuxerTest, FtraceIsAlreadyOn) {
-  MockFtraceProcfs ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  // If someone is using ftrace already don't stomp on what they are doing.
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("function"));
-  ASSERT_FALSE(model.SetupConfig(/* id= */ 123, config));
-}
-
-TEST_F(FtraceConfigMuxerTest, Atrace) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
+  FtraceConfigMuxer model(&ftrace_, &atrace_wrapper_, fake_table.get(),
+                          GetSyscallTable(), {},
+                          /* secondary_instance= */ true);
 
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
   *config.add_atrace_categories() = "sched";
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "sched"}),
-                                _))
-      .WillOnce(Return(true));
-
-  FtraceConfigId id = 57;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-
-  // "ftrace" group events are always enabled, and therefore the "print" event
-  // will show up in the per data source event filter (as we want to record it),
-  // but not the central filter (as we're not enabling/disabling it).
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              Contains(kFakeSchedSwitchEventId));
-  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              Contains(kFakePrintEventId));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  EXPECT_THAT(central_filter->GetEnabledEvents(),
-              Contains(kFakeSchedSwitchEventId));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceTwoApps) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config = CreateFtraceConfig({});
-  *config.add_atrace_apps() = "com.google.android.gms.persistent";
-  *config.add_atrace_apps() = "com.google.android.gms";
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray(
-              {"atrace", "--async_start", "--only_userspace", "-a",
-               "com.google.android.gms,com.google.android.gms.persistent"}),
-          _))
-      .WillOnce(Return(true));
-
-  FtraceConfigId id = 97;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              Contains(kFakePrintEventId));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceMultipleConfigs) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config_a = CreateFtraceConfig({});
-  *config_a.add_atrace_apps() = "app_a";
-  *config_a.add_atrace_categories() = "cat_a";
-
-  FtraceConfig config_b = CreateFtraceConfig({});
-  *config_b.add_atrace_apps() = "app_b";
-  *config_b.add_atrace_categories() = "cat_b";
-
-  FtraceConfig config_c = CreateFtraceConfig({});
-  *config_c.add_atrace_apps() = "app_c";
-  *config_c.add_atrace_categories() = "cat_c";
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "cat_a",
-                                                  "-a", "app_a"}),
-                                _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_a = 3;
-  ASSERT_TRUE(model.SetupConfig(id_a, config_a));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_a", "cat_b", "-a", "app_a,app_b"}),
-                _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_b = 13;
-  ASSERT_TRUE(model.SetupConfig(id_b, config_b));
-
-  EXPECT_CALL(atrace,
-              RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                          "--only_userspace", "cat_a", "cat_b",
-                                          "cat_c", "-a", "app_a,app_b,app_c"}),
-                        _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_c = 23;
-  ASSERT_TRUE(model.SetupConfig(id_c, config_c));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_a", "cat_c", "-a", "app_a,app_c"}),
-                _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_b));
-
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "cat_c",
-                                                  "-a", "app_c"}),
-                                _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_a));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_c));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceFailedConfig) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config_a = CreateFtraceConfig({});
-  *config_a.add_atrace_apps() = "app_1";
-  *config_a.add_atrace_apps() = "app_2";
-  *config_a.add_atrace_categories() = "cat_1";
-  *config_a.add_atrace_categories() = "cat_2";
-
-  FtraceConfig config_b = CreateFtraceConfig({});
-  *config_b.add_atrace_apps() = "app_fail";
-  *config_b.add_atrace_categories() = "cat_fail";
-
-  FtraceConfig config_c = CreateFtraceConfig({});
-  *config_c.add_atrace_apps() = "app_1";
-  *config_c.add_atrace_apps() = "app_3";
-  *config_c.add_atrace_categories() = "cat_1";
-  *config_c.add_atrace_categories() = "cat_3";
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_1", "cat_2", "-a", "app_1,app_2"}),
-                _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_a = 7;
-  ASSERT_TRUE(model.SetupConfig(id_a, config_a));
-
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "cat_1",
-                                                  "cat_2", "cat_fail", "-a",
-                                                  "app_1,app_2,app_fail"}),
-                                _))
-      .WillOnce(Return(false));
-  FtraceConfigId id_b = 17;
-  ASSERT_TRUE(model.SetupConfig(id_b, config_b));
-
-  EXPECT_CALL(atrace,
-              RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                          "--only_userspace", "cat_1", "cat_2",
-                                          "cat_3", "-a", "app_1,app_2,app_3"}),
-                        _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_c = 47;
-  ASSERT_TRUE(model.SetupConfig(id_c, config_c));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_1", "cat_2", "-a", "app_1,app_2"}),
-                _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_c));
-
-  // Removing the config we failed to enable doesn't change the atrace state
-  // so we don't expect a call here.
-  ASSERT_TRUE(model.RemoveConfig(id_b));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_a));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceDuplicateConfigs) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config_a = CreateFtraceConfig({});
-  *config_a.add_atrace_apps() = "app_1";
-  *config_a.add_atrace_categories() = "cat_1";
-
-  FtraceConfig config_b = CreateFtraceConfig({});
-  *config_b.add_atrace_apps() = "app_1";
-  *config_b.add_atrace_categories() = "cat_1";
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "cat_1",
-                                                  "-a", "app_1"}),
-                                _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_a = 19;
-  ASSERT_TRUE(model.SetupConfig(id_a, config_a));
-
-  FtraceConfigId id_b = 29;
-  ASSERT_TRUE(model.SetupConfig(id_b, config_b));
-
-  ASSERT_TRUE(model.RemoveConfig(id_a));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_b));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceAndFtraceConfigs) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config_a = CreateFtraceConfig({"sched/sched_cpu_hotplug"});
-
-  FtraceConfig config_b = CreateFtraceConfig({"sched/sched_switch"});
-  *config_b.add_atrace_categories() = "b";
-
-  FtraceConfig config_c = CreateFtraceConfig({"sched/sched_switch"});
-
-  FtraceConfig config_d = CreateFtraceConfig({"sched/sched_cpu_hotplug"});
-  *config_d.add_atrace_categories() = "d";
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  FtraceConfigId id_a = 179;
-  ASSERT_TRUE(model.SetupConfig(id_a, config_a));
-
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "b"}),
-                                _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_b = 239;
-  ASSERT_TRUE(model.SetupConfig(id_b, config_b));
-
-  FtraceConfigId id_c = 101;
-  ASSERT_TRUE(model.SetupConfig(id_c, config_c));
-
-  EXPECT_CALL(atrace,
-              RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                          "--only_userspace", "b", "d"}),
-                        _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_d = 47;
-  ASSERT_TRUE(model.SetupConfig(id_d, config_d));
-
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "b"}),
-                                _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_d));
-
-  ASSERT_TRUE(model.RemoveConfig(id_c));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_b));
-
-  ASSERT_TRUE(model.RemoveConfig(id_a));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceErrorsPropagated) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config = CreateFtraceConfig({});
-  *config.add_atrace_categories() = "cat_1";
-  *config.add_atrace_categories() = "cat_2";
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "cat_1",
-                                                  "cat_2"}),
-                                _))
-      .WillOnce(Invoke([](const std::vector<std::string>&, std::string* err) {
-        EXPECT_NE(err, nullptr);
-        if (err)
-          err->append("foo\nbar\n");
-        return true;
-      }));
-
-  FtraceSetupErrors errors{};
-  FtraceConfigId id_a = 23;
-  ASSERT_TRUE(model.SetupConfig(id_a, config, &errors));
-  EXPECT_EQ(errors.atrace_errors, "foo\nbar\n");
-}
-
-TEST_F(FtraceConfigMuxerTest, SetupClockForTesting) {
-  MockFtraceProcfs ftrace;
-  FtraceConfig config;
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-  namespace pb0 = protos::pbzero;
-
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  model.SetupClockForTesting(config);
-  // unspecified = boot.
-  EXPECT_EQ(model.ftrace_clock(),
-            static_cast<int>(pb0::FTRACE_CLOCK_UNSPECIFIED));
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "global"));
-  model.SetupClockForTesting(config);
-  EXPECT_EQ(model.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_GLOBAL));
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return(""));
-  model.SetupClockForTesting(config);
-  EXPECT_EQ(model.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_UNKNOWN));
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("local [global]"));
-  model.SetupClockForTesting(config);
-  EXPECT_EQ(model.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_GLOBAL));
-}
-
-TEST_F(FtraceConfigMuxerTest, GetFtraceEvents) {
-  MockFtraceProcfs ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
-  std::set<GroupAndName> events =
-      model.GetFtraceEventsForTesting(config, table_.get());
-
-  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
-  EXPECT_THAT(events, Not(Contains(GroupAndName("ftrace", "print"))));
-}
-
-TEST_F(FtraceConfigMuxerTest, GetFtraceEventsAtrace) {
-  MockFtraceProcfs ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  FtraceConfig config = CreateFtraceConfig({});
-  *config.add_atrace_categories() = "sched";
-  std::set<GroupAndName> events =
-      model.GetFtraceEventsForTesting(config, table_.get());
-
-  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
-  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_cpu_hotplug")));
-  EXPECT_THAT(events, Contains(GroupAndName("ftrace", "print")));
-}
-
-TEST_F(FtraceConfigMuxerTest, GetFtraceEventsAtraceCategories) {
-  MockFtraceProcfs ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  FtraceConfig config = CreateFtraceConfig({});
-  *config.add_atrace_categories() = "sched";
-  *config.add_atrace_categories() = "memreclaim";
-  std::set<GroupAndName> events =
-      model.GetFtraceEventsForTesting(config, table_.get());
-
-  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
-  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_cpu_hotplug")));
-  EXPECT_THAT(events, Contains(GroupAndName("cgroup", "cgroup_mkdir")));
-  EXPECT_THAT(events, Contains(GroupAndName("vmscan",
-                                            "mm_vmscan_direct_reclaim_begin")));
-  EXPECT_THAT(events,
-              Contains(GroupAndName("lowmemorykiller", "lowmemory_kill")));
-  EXPECT_THAT(events, Contains(GroupAndName("ftrace", "print")));
-}
-
-// Tests the enabling fallback logic that tries to use the "set_event" interface
-// if writing the individual xxx/enable file fails.
-TEST_F(FtraceConfigMuxerTest, FallbackOnSetEvent) {
-  MockFtraceProcfs ftrace;
-  FtraceConfig config =
-      CreateFtraceConfig({"sched/sched_switch", "cgroup/cgroup_mkdir"});
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_percent", _))
-      .WillRepeatedly(Return(true));
-
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/cgroup/cgroup_mkdir/enable", "1"))
-      .WillOnce(Return(false));
-  EXPECT_CALL(ftrace, AppendToFile("/root/set_event", "cgroup:cgroup_mkdir"))
-      .WillOnce(Return(true));
-  FtraceConfigId id = 97;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              Contains(kFakeSchedSwitchEventId));
-  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              Contains(kCgroupMkdirEventId));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  EXPECT_THAT(central_filter->GetEnabledEvents(),
-              Contains(kFakeSchedSwitchEventId));
-  EXPECT_THAT(central_filter->GetEnabledEvents(),
-              Contains(kCgroupMkdirEventId));
-
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "0"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/cgroup/cgroup_mkdir/enable", "0"))
-      .WillOnce(Return(false));
-  EXPECT_CALL(ftrace, AppendToFile("/root/set_event", "!cgroup:cgroup_mkdir"))
-      .WillOnce(Return(true));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", PageSizeKb()));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-  ASSERT_TRUE(model.RemoveConfig(id));
+  ASSERT_FALSE(model.SetupConfig(/* id= */ 73, config));
 }
 
 TEST_F(FtraceConfigMuxerTest, CompactSchedConfig) {
@@ -1185,13 +289,14 @@
   auto valid_compact_format = CompactSchedEventFormat{
       /*format_valid=*/true, format_with_id, CompactSchedWakingFormat{}};
 
-  NiceMock<MockFtraceProcfs> ftrace;
-  table_ = CreateFakeTable(valid_compact_format);
-  FtraceConfigMuxer muxer(&ftrace, table_.get(), GetSyscallTable(), {});
+  std::unique_ptr<ProtoTranslationTable> table =
+      CreateFakeTable(valid_compact_format);
+  FtraceConfigMuxer muxer(&ftrace_, &atrace_wrapper_, table.get(),
+                          GetSyscallTable(), {});
 
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
       .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
       .WillByDefault(Return("0"));
 
   {
@@ -1246,38 +351,659 @@
   }
 }
 
-TEST_F(FtraceConfigMuxerTest, CompactSchedConfigWithInvalidFormat) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
+// Fixture that constructs a FtraceConfigMuxer with a fake
+// ProtoTranslationTable.
+class FtraceConfigMuxerFakeTableTest : public FtraceConfigMuxerTest {
+ protected:
+  std::unique_ptr<ProtoTranslationTable> table_ = CreateFakeTable();
+  FtraceConfigMuxer model_ = FtraceConfigMuxer(&ftrace_,
+                                               &atrace_wrapper_,
+                                               table_.get(),
+                                               GetSyscallTable(),
+                                               {});
+};
 
+TEST_F(FtraceConfigMuxerFakeTableTest, GenericSyscallFiltering) {
+  FtraceConfig config = CreateFtraceConfig({"raw_syscalls/sys_enter"});
+  *config.add_syscall_events() = "sys_open";
+  *config.add_syscall_events() = "sys_read";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile(_, _)).WillRepeatedly(Return(true));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/raw_syscalls/sys_enter/filter",
+                                   "id == 0 || id == 1"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/raw_syscalls/sys_exit/filter",
+                                   "id == 0 || id == 1"));
+
+  FtraceConfigId id = 37;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const std::set<size_t>& filter = model_.GetSyscallFilterForTesting();
+  ASSERT_THAT(filter, UnorderedElementsAre(0, 1));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, UnknownSyscallFilter) {
+  FtraceConfig config = CreateFtraceConfig({"raw_syscalls/sys_enter"});
+  config.add_syscall_events("sys_open");
+  config.add_syscall_events("sys_not_a_call");
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+
+  // Unknown syscall is ignored.
+  ASSERT_TRUE(model_.SetupConfig(/*id = */ 73, config));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, SyscallFilterMuxing) {
+  FtraceConfig empty_config = CreateFtraceConfig({});
+
+  FtraceConfig syscall_config = empty_config;
+  syscall_config.add_ftrace_events("raw_syscalls/sys_enter");
+
+  FtraceConfig syscall_open_config = syscall_config;
+  syscall_open_config.add_syscall_events("sys_open");
+
+  FtraceConfig syscall_read_config = syscall_config;
+  syscall_read_config.add_syscall_events("sys_read");
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+
+  // Expect no filter for non-syscall config.
+  ASSERT_TRUE(model_.SetupConfig(/* id= */ 179239, empty_config));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre());
+
+  // Expect no filter for syscall config with no specified events.
+  FtraceConfigId syscall_id = 73;
+  ASSERT_TRUE(model_.SetupConfig(syscall_id, syscall_config));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre());
+
+  // Still expect no filter to satisfy this and the above.
+  FtraceConfigId syscall_open_id = 101;
+  ASSERT_TRUE(model_.SetupConfig(syscall_open_id, syscall_open_config));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre());
+
+  // After removing the generic syscall trace, only the one with filter is left.
+  ASSERT_TRUE(model_.RemoveConfig(syscall_id));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
+
+  // With sys_read and sys_open traced separately, filter includes both.
+  FtraceConfigId syscall_read_id = 57;
+  ASSERT_TRUE(model_.SetupConfig(syscall_read_id, syscall_read_config));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre(0, 1));
+
+  // After removing configs with filters, filter is reset to empty.
+  ASSERT_TRUE(model_.RemoveConfig(syscall_open_id));
+  ASSERT_TRUE(model_.RemoveConfig(syscall_read_id));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre());
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, TurnFtraceOnOff) {
+  FtraceConfig config = CreateFtraceConfig({"sched_switch", "foo"});
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
+
+  FtraceConfigId id = 97;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAreArray({kFakeSchedSwitchEventId}));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAreArray({kFakeSchedSwitchEventId}));
+
+  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace_));
+  EXPECT_CALL(ftrace_, NumberOfCpus()).Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_percent", _))
+      .WillRepeatedly(Return(true));
+
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_switch/enable", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", PageSizeKb()));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+
+  ASSERT_TRUE(model_.RemoveConfig(id));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, FtraceIsAlreadyOn) {
+  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
+
+  // If someone is using ftrace already don't stomp on what they are doing.
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("function"));
+  ASSERT_FALSE(model_.SetupConfig(/* id= */ 123, config));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, Atrace) {
+  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
+  *config.add_atrace_categories() = "sched";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "sched"}),
+                        _))
+      .WillOnce(Return(true));
+
+  FtraceConfigId id = 57;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  // "ftrace" group events are always enabled, and therefore the "print" event
+  // will show up in the per data source event filter (as we want to record it),
+  // but not the central filter (as we're not enabling/disabling it).
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              Contains(kFakeSchedSwitchEventId));
+  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              Contains(kFakePrintEventId));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  EXPECT_THAT(central_filter->GetEnabledEvents(),
+              Contains(kFakeSchedSwitchEventId));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceTwoApps) {
+  FtraceConfig config = CreateFtraceConfig({});
+  *config.add_atrace_apps() = "com.google.android.gms.persistent";
+  *config.add_atrace_apps() = "com.google.android.gms";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray(
+              {"atrace", "--async_start", "--only_userspace", "-a",
+               "com.google.android.gms,com.google.android.gms.persistent"}),
+          _))
+      .WillOnce(Return(true));
+
+  FtraceConfigId id = 97;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              Contains(kFakePrintEventId));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceMultipleConfigs) {
+  FtraceConfig config_a = CreateFtraceConfig({});
+  *config_a.add_atrace_apps() = "app_a";
+  *config_a.add_atrace_categories() = "cat_a";
+
+  FtraceConfig config_b = CreateFtraceConfig({});
+  *config_b.add_atrace_apps() = "app_b";
+  *config_b.add_atrace_categories() = "cat_b";
+
+  FtraceConfig config_c = CreateFtraceConfig({});
+  *config_c.add_atrace_apps() = "app_c";
+  *config_c.add_atrace_categories() = "cat_c";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_a", "-a", "app_a"}),
+                _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_a = 3;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config_a));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_a", "cat_b", "-a", "app_a,app_b"}),
+                _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_b = 13;
+  ASSERT_TRUE(model_.SetupConfig(id_b, config_b));
+
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "cat_a", "cat_b",
+                                          "cat_c", "-a", "app_a,app_b,app_c"}),
+                        _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_c = 23;
+  ASSERT_TRUE(model_.SetupConfig(id_c, config_c));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_a", "cat_c", "-a", "app_a,app_c"}),
+                _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_b));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_c", "-a", "app_c"}),
+                _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_a));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_c));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceFailedConfig) {
+  FtraceConfig config_a = CreateFtraceConfig({});
+  *config_a.add_atrace_apps() = "app_1";
+  *config_a.add_atrace_apps() = "app_2";
+  *config_a.add_atrace_categories() = "cat_1";
+  *config_a.add_atrace_categories() = "cat_2";
+
+  FtraceConfig config_b = CreateFtraceConfig({});
+  *config_b.add_atrace_apps() = "app_fail";
+  *config_b.add_atrace_categories() = "cat_fail";
+
+  FtraceConfig config_c = CreateFtraceConfig({});
+  *config_c.add_atrace_apps() = "app_1";
+  *config_c.add_atrace_apps() = "app_3";
+  *config_c.add_atrace_categories() = "cat_1";
+  *config_c.add_atrace_categories() = "cat_3";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_1", "cat_2", "-a", "app_1,app_2"}),
+                _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_a = 7;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config_a));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_1", "cat_2", "cat_fail", "-a",
+                                  "app_1,app_2,app_fail"}),
+                _))
+      .WillOnce(Return(false));
+  FtraceConfigId id_b = 17;
+  ASSERT_TRUE(model_.SetupConfig(id_b, config_b));
+
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "cat_1", "cat_2",
+                                          "cat_3", "-a", "app_1,app_2,app_3"}),
+                        _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_c = 47;
+  ASSERT_TRUE(model_.SetupConfig(id_c, config_c));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_1", "cat_2", "-a", "app_1,app_2"}),
+                _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_c));
+
+  // Removing the config we failed to enable doesn't change the atrace state
+  // so we don't expect a call here.
+  ASSERT_TRUE(model_.RemoveConfig(id_b));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_a));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceDuplicateConfigs) {
+  FtraceConfig config_a = CreateFtraceConfig({});
+  *config_a.add_atrace_apps() = "app_1";
+  *config_a.add_atrace_categories() = "cat_1";
+
+  FtraceConfig config_b = CreateFtraceConfig({});
+  *config_b.add_atrace_apps() = "app_1";
+  *config_b.add_atrace_categories() = "cat_1";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_1", "-a", "app_1"}),
+                _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_a = 19;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config_a));
+
+  FtraceConfigId id_b = 29;
+  ASSERT_TRUE(model_.SetupConfig(id_b, config_b));
+
+  ASSERT_TRUE(model_.RemoveConfig(id_a));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_b));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceAndFtraceConfigs) {
+  FtraceConfig config_a = CreateFtraceConfig({"sched/sched_cpu_hotplug"});
+
+  FtraceConfig config_b = CreateFtraceConfig({"sched/sched_switch"});
+  *config_b.add_atrace_categories() = "b";
+
+  FtraceConfig config_c = CreateFtraceConfig({"sched/sched_switch"});
+
+  FtraceConfig config_d = CreateFtraceConfig({"sched/sched_cpu_hotplug"});
+  *config_d.add_atrace_categories() = "d";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  FtraceConfigId id_a = 179;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config_a));
+
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "b"}),
+                        _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_b = 239;
+  ASSERT_TRUE(model_.SetupConfig(id_b, config_b));
+
+  FtraceConfigId id_c = 101;
+  ASSERT_TRUE(model_.SetupConfig(id_c, config_c));
+
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "b", "d"}),
+                        _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_d = 47;
+  ASSERT_TRUE(model_.SetupConfig(id_d, config_d));
+
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "b"}),
+                        _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_d));
+
+  ASSERT_TRUE(model_.RemoveConfig(id_c));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_b));
+
+  ASSERT_TRUE(model_.RemoveConfig(id_a));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceErrorsPropagated) {
+  FtraceConfig config = CreateFtraceConfig({});
+  *config.add_atrace_categories() = "cat_1";
+  *config.add_atrace_categories() = "cat_2";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_1", "cat_2"}),
+                _))
+      .WillOnce(Invoke([](const std::vector<std::string>&, std::string* err) {
+        EXPECT_NE(err, nullptr);
+        if (err)
+          err->append("foo\nbar\n");
+        return true;
+      }));
+
+  FtraceSetupErrors errors{};
+  FtraceConfigId id_a = 23;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config, &errors));
+  EXPECT_EQ(errors.atrace_errors, "foo\nbar\n");
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, SetupClockForTesting) {
+  FtraceConfig config;
+
+  namespace pb0 = protos::pbzero;
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  model_.SetupClockForTesting(config);
+  // unspecified = boot.
+  EXPECT_EQ(model_.ftrace_clock(),
+            static_cast<int>(pb0::FTRACE_CLOCK_UNSPECIFIED));
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "global"));
+  model_.SetupClockForTesting(config);
+  EXPECT_EQ(model_.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_GLOBAL));
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return(""));
+  model_.SetupClockForTesting(config);
+  EXPECT_EQ(model_.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_UNKNOWN));
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("local [global]"));
+  model_.SetupClockForTesting(config);
+  EXPECT_EQ(model_.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_GLOBAL));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, GetFtraceEvents) {
+  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
+  std::set<GroupAndName> events =
+      model_.GetFtraceEventsForTesting(config, table_.get());
+
+  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
+  EXPECT_THAT(events, Not(Contains(GroupAndName("ftrace", "print"))));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, GetFtraceEventsAtrace) {
+  FtraceConfig config = CreateFtraceConfig({});
+  *config.add_atrace_categories() = "sched";
+  std::set<GroupAndName> events =
+      model_.GetFtraceEventsForTesting(config, table_.get());
+
+  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
+  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_cpu_hotplug")));
+  EXPECT_THAT(events, Contains(GroupAndName("ftrace", "print")));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, GetFtraceEventsAtraceCategories) {
+  FtraceConfig config = CreateFtraceConfig({});
+  *config.add_atrace_categories() = "sched";
+  *config.add_atrace_categories() = "memreclaim";
+  std::set<GroupAndName> events =
+      model_.GetFtraceEventsForTesting(config, table_.get());
+
+  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
+  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_cpu_hotplug")));
+  EXPECT_THAT(events, Contains(GroupAndName("cgroup", "cgroup_mkdir")));
+  EXPECT_THAT(events, Contains(GroupAndName("vmscan",
+                                            "mm_vmscan_direct_reclaim_begin")));
+  EXPECT_THAT(events,
+              Contains(GroupAndName("lowmemorykiller", "lowmemory_kill")));
+  EXPECT_THAT(events, Contains(GroupAndName("ftrace", "print")));
+}
+
+// Tests the enabling fallback logic that tries to use the "set_event" interface
+// if writing the individual xxx/enable file fails.
+TEST_F(FtraceConfigMuxerFakeTableTest, FallbackOnSetEvent) {
+  FtraceConfig config =
+      CreateFtraceConfig({"sched/sched_switch", "cgroup/cgroup_mkdir"});
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_percent", _))
+      .WillRepeatedly(Return(true));
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/cgroup/cgroup_mkdir/enable", "1"))
+      .WillOnce(Return(false));
+  EXPECT_CALL(ftrace_, AppendToFile("/root/set_event", "cgroup:cgroup_mkdir"))
+      .WillOnce(Return(true));
+  FtraceConfigId id = 97;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              Contains(kFakeSchedSwitchEventId));
+  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              Contains(kCgroupMkdirEventId));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  EXPECT_THAT(central_filter->GetEnabledEvents(),
+              Contains(kFakeSchedSwitchEventId));
+  EXPECT_THAT(central_filter->GetEnabledEvents(),
+              Contains(kCgroupMkdirEventId));
+
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_switch/enable", "0"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/cgroup/cgroup_mkdir/enable", "0"))
+      .WillOnce(Return(false));
+  EXPECT_CALL(ftrace_, AppendToFile("/root/set_event", "!cgroup:cgroup_mkdir"))
+      .WillOnce(Return(true));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", PageSizeKb()));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.RemoveConfig(id));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, CompactSchedConfigWithInvalidFormat) {
   // Request compact encoding.
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
   config.mutable_compact_sched()->set_enabled(true);
 
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
       .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
       .WillByDefault(Return("0"));
 
   FtraceConfigId id = 67;
-  ASSERT_TRUE(model.SetupConfig(id, config));
+  ASSERT_TRUE(model_.SetupConfig(id, config));
 
   // The translation table says that the scheduling events' format didn't match
   // compile-time assumptions, so we won't enable compact events even if
   // requested.
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
   ASSERT_TRUE(ds_config);
   EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
               Contains(kFakeSchedSwitchEventId));
   EXPECT_FALSE(ds_config->compact_sched.enabled);
 }
 
-TEST_F(FtraceConfigMuxerTest, SkipGenericEventsOption) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
+TEST_F(FtraceConfigMuxerFakeTableTest, SkipGenericEventsOption) {
   static constexpr int kFtraceGenericEventId = 42;
-  ON_CALL(table_procfs_, ReadEventFormat("sched", "generic"))
+  ON_CALL(ftrace_, ReadEventFormat("sched", "generic"))
       .WillByDefault(Return(R"(name: generic
 ID: 42
 format:
@@ -1297,15 +1023,15 @@
       CreateFtraceConfig({"sched/sched_switch", "sched/generic"});
   config_with_disable.set_disable_generic_events(true);
 
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
       .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
       .WillByDefault(Return("0"));
 
   {
     FtraceConfigId id = 123;
-    ASSERT_TRUE(model.SetupConfig(id, config_default));
-    const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
+    ASSERT_TRUE(model_.SetupConfig(id, config_default));
+    const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
     ASSERT_TRUE(ds_config);
     // Both events enabled for the data source by default.
     EXPECT_THAT(
@@ -1314,8 +1040,8 @@
   }
   {
     FtraceConfigId id = 321;
-    ASSERT_TRUE(model.SetupConfig(id, config_with_disable));
-    const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
+    ASSERT_TRUE(model_.SetupConfig(id, config_with_disable));
+    const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
     ASSERT_TRUE(ds_config);
     // Only the statically known event is enabled.
     EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
@@ -1323,11 +1049,7 @@
   }
 }
 
-TEST_F(FtraceConfigMuxerTest, Funcgraph) {
-  auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
-
+TEST_F(FtraceConfigMuxerFakeTableTest, Funcgraph) {
   FtraceConfig config;
   config.set_enable_function_graph(true);
   *config.add_function_filters() = "sched*";
@@ -1336,76 +1058,276 @@
   *config.add_function_graph_roots() = "sched*";
   *config.add_function_graph_roots() = "*mm_fault";
 
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
       .WillByDefault(Return("nop"));
 
-  EXPECT_CALL(ftrace, WriteToFile(_, _)).WillRepeatedly(Return(true));
+  EXPECT_CALL(ftrace_, WriteToFile(_, _)).WillRepeatedly(Return(true));
 
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
 
   // Set up config, assert that the tracefs writes happened:
-  EXPECT_CALL(ftrace, ClearFile("/root/set_ftrace_filter"));
-  EXPECT_CALL(ftrace, ClearFile("/root/set_graph_function"));
-  EXPECT_CALL(ftrace, AppendToFile("/root/set_ftrace_filter",
-                                   "sched*\nhandle_mm_fault"))
+  EXPECT_CALL(ftrace_, ClearFile("/root/set_ftrace_filter"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/set_graph_function"));
+  EXPECT_CALL(ftrace_, AppendToFile("/root/set_ftrace_filter",
+                                    "sched*\nhandle_mm_fault"))
       .WillOnce(Return(true));
-  EXPECT_CALL(ftrace,
+  EXPECT_CALL(ftrace_,
               AppendToFile("/root/set_graph_function", "sched*\n*mm_fault"))
       .WillOnce(Return(true));
-  EXPECT_CALL(ftrace, WriteToFile("/root/current_tracer", "function_graph"))
+  EXPECT_CALL(ftrace_, WriteToFile("/root/current_tracer", "function_graph"))
       .WillOnce(Return(true));
   FtraceConfigId id = 43;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace));
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace_));
   // Toggle config on and off, tracer won't be reset yet:
-  ASSERT_TRUE(model.ActivateConfig(id));
-  ASSERT_TRUE(model.RemoveConfig(id));
-  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+  ASSERT_TRUE(model_.RemoveConfig(id));
+  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace_));
 
   // Emulate ftrace_controller's call to ResetCurrentTracer (see impl comments
   // for why RemoveConfig is insufficient).
-  EXPECT_CALL(ftrace, ClearFile("/root/set_ftrace_filter"));
-  EXPECT_CALL(ftrace, ClearFile("/root/set_graph_function"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/current_tracer", "nop"))
+  EXPECT_CALL(ftrace_, ClearFile("/root/set_ftrace_filter"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/set_graph_function"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/current_tracer", "nop"))
       .WillOnce(Return(true));
-  ASSERT_TRUE(model.ResetCurrentTracer());
-  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace));
+  ASSERT_TRUE(model_.ResetCurrentTracer());
+  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace_));
 }
 
-TEST_F(FtraceConfigMuxerTest, SecondaryInstanceDoNotSupportAtrace) {
-  auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {},
-                          /* secondary_instance= */ true);
-
-  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
-  *config.add_atrace_categories() = "sched";
-
-  ASSERT_FALSE(model.SetupConfig(/* id= */ 73, config));
-}
-
-TEST_F(FtraceConfigMuxerTest, PreserveFtraceBufferNotSetBufferSizeKb) {
-  auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {},
-                          /* secondary_instance= */ false);
-
+TEST_F(FtraceConfigMuxerFakeTableTest, PreserveFtraceBufferNotSetBufferSizeKb) {
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
 
   config.set_preserve_ftrace_buffer(true);
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
       .WillOnce(Return('1'));
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
       .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
       .Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _)).Times(0);
-  EXPECT_CALL(ftrace,
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _)).Times(0);
+  EXPECT_CALL(ftrace_,
               WriteToFile("/root/events/sched/sched_switch/enable", "1"));
 
   FtraceConfigId id = 44;
-  ASSERT_TRUE(model.SetupConfig(id, config));
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+}
+
+// Fixture that constructs a FtraceConfigMuxer with a mock
+// ProtoTranslationTable.
+class FtraceConfigMuxerMockTableTest : public FtraceConfigMuxerTest {
+ protected:
+  std::unique_ptr<MockProtoTranslationTable> mock_table_ = GetMockTable();
+  FtraceConfigMuxer model_ = FtraceConfigMuxer(&ftrace_,
+                                               &atrace_wrapper_,
+                                               mock_table_.get(),
+                                               GetSyscallTable(),
+                                               {});
+};
+
+TEST_F(FtraceConfigMuxerMockTableTest, AddGenericEvent) {
+  FtraceConfig config = CreateFtraceConfig({"power/cpu_frequency"});
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/power/cpu_frequency/enable", "1"));
+  EXPECT_CALL(*mock_table_, GetEvent(GroupAndName("power", "cpu_frequency")))
+      .Times(AnyNumber());
+
+  static constexpr int kExpectedEventId = 77;
+  Event event_to_return;
+  event_to_return.name = "cpu_frequency";
+  event_to_return.group = "power";
+  event_to_return.ftrace_event_id = kExpectedEventId;
+  ON_CALL(*mock_table_,
+          GetOrCreateEvent(GroupAndName("power", "cpu_frequency")))
+      .WillByDefault(Return(&event_to_return));
+  EXPECT_CALL(*mock_table_,
+              GetOrCreateEvent(GroupAndName("power", "cpu_frequency")));
+
+  FtraceConfigId id = 7;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAreArray({kExpectedEventId}));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAreArray({kExpectedEventId}));
+}
+
+TEST_F(FtraceConfigMuxerMockTableTest, AddAllEvents) {
+  FtraceConfig config = CreateFtraceConfig({"sched/*"});
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_new_event/enable", "1"));
+
+  std::set<std::string> n = {"sched_switch", "sched_new_event"};
+  ON_CALL(ftrace_, GetEventNamesForGroup("events/sched"))
+      .WillByDefault(Return(n));
+  EXPECT_CALL(ftrace_, GetEventNamesForGroup("events/sched")).Times(1);
+
+  // Non-generic event.
+  static constexpr int kSchedSwitchEventId = 1;
+  Event sched_switch = {"sched_switch", "sched", {}, 0, 0, 0};
+  sched_switch.ftrace_event_id = kSchedSwitchEventId;
+  ON_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("sched", "sched_switch")))
+      .WillByDefault(Return(&sched_switch));
+  EXPECT_CALL(*mock_table_,
+              GetOrCreateEvent(GroupAndName("sched", "sched_switch")))
+      .Times(AnyNumber());
+
+  // Generic event.
+  static constexpr int kGenericEventId = 2;
+  Event event_to_return;
+  event_to_return.name = "sched_new_event";
+  event_to_return.group = "sched";
+  event_to_return.ftrace_event_id = kGenericEventId;
+  ON_CALL(*mock_table_,
+          GetOrCreateEvent(GroupAndName("sched", "sched_new_event")))
+      .WillByDefault(Return(&event_to_return));
+  EXPECT_CALL(*mock_table_,
+              GetOrCreateEvent(GroupAndName("sched", "sched_new_event")));
+
+  FtraceConfigId id = 13;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+  ASSERT_TRUE(id);
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAreArray({kSchedSwitchEventId, kGenericEventId}));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAreArray({kSchedSwitchEventId, kGenericEventId}));
+}
+
+TEST_F(FtraceConfigMuxerMockTableTest, TwoWildcardGroups) {
+  FtraceConfig config = CreateFtraceConfig({"group_one/*", "group_two/*"});
+
+  std::set<std::string> event_names = {"foo"};
+  ON_CALL(ftrace_, GetEventNamesForGroup("events/group_one"))
+      .WillByDefault(Return(event_names));
+  EXPECT_CALL(ftrace_, GetEventNamesForGroup("events/group_one"))
+      .Times(AnyNumber());
+
+  ON_CALL(ftrace_, GetEventNamesForGroup("events/group_two"))
+      .WillByDefault(Return(event_names));
+  EXPECT_CALL(ftrace_, GetEventNamesForGroup("events/group_two"))
+      .Times(AnyNumber());
+
+  static constexpr int kEventId1 = 1;
+  Event event1;
+  event1.name = "foo";
+  event1.group = "group_one";
+  event1.ftrace_event_id = kEventId1;
+  ON_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_one", "foo")))
+      .WillByDefault(Return(&event1));
+  EXPECT_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_one", "foo")));
+
+  static constexpr int kEventId2 = 2;
+  Event event2;
+  event2.name = "foo";
+  event2.group = "group_two";
+  event2.ftrace_event_id = kEventId2;
+  ON_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_two", "foo")))
+      .WillByDefault(Return(&event2));
+  EXPECT_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_two", "foo")));
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
+  FtraceConfigId id = 23;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAreArray({kEventId1, kEventId2}));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAreArray({kEventId1, kEventId2}));
+}
+
+TEST_F(FtraceConfigMuxerMockTableTest, AddSameNameEvents) {
+  FtraceConfig config = CreateFtraceConfig({"group_one/foo", "group_two/foo"});
+
+  static constexpr int kEventId1 = 1;
+  Event event1;
+  event1.name = "foo";
+  event1.group = "group_one";
+  event1.ftrace_event_id = kEventId1;
+  ON_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_one", "foo")))
+      .WillByDefault(Return(&event1));
+  EXPECT_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_one", "foo")));
+
+  static constexpr int kEventId2 = 2;
+  Event event2;
+  event2.name = "foo";
+  event2.group = "group_two";
+  event2.ftrace_event_id = kEventId2;
+  ON_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_two", "foo")))
+      .WillByDefault(Return(&event2));
+  EXPECT_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_two", "foo")));
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
+  FtraceConfigId id = 5;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAreArray({kEventId1, kEventId2}));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAreArray({kEventId1, kEventId2}));
 }
 
 }  // namespace
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index ad924fa..750629d 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -179,25 +179,31 @@
   if (!table)
     return nullptr;
 
+  auto atrace_wrapper = std::make_unique<AtraceWrapperImpl>();
+
   std::map<std::string, std::vector<GroupAndName>> vendor_evts =
       GetAtraceVendorEvents(ftrace_procfs.get());
 
   SyscallTable syscalls = SyscallTable::FromCurrentArch();
 
   auto muxer = std::make_unique<FtraceConfigMuxer>(
-      ftrace_procfs.get(), table.get(), std::move(syscalls), vendor_evts);
-  return std::unique_ptr<FtraceController>(
-      new FtraceController(std::move(ftrace_procfs), std::move(table),
-                           std::move(muxer), runner, observer));
+      ftrace_procfs.get(), atrace_wrapper.get(), table.get(),
+      std::move(syscalls), vendor_evts);
+  return std::unique_ptr<FtraceController>(new FtraceController(
+      std::move(ftrace_procfs), std::move(table), std::move(atrace_wrapper),
+      std::move(muxer), runner, observer));
 }
 
-FtraceController::FtraceController(std::unique_ptr<FtraceProcfs> ftrace_procfs,
-                                   std::unique_ptr<ProtoTranslationTable> table,
-                                   std::unique_ptr<FtraceConfigMuxer> muxer,
-                                   base::TaskRunner* task_runner,
-                                   Observer* observer)
+FtraceController::FtraceController(
+    std::unique_ptr<FtraceProcfs> ftrace_procfs,
+    std::unique_ptr<ProtoTranslationTable> table,
+    std::unique_ptr<AtraceWrapper> atrace_wrapper,
+    std::unique_ptr<FtraceConfigMuxer> muxer,
+    base::TaskRunner* task_runner,
+    Observer* observer)
     : task_runner_(task_runner),
       observer_(observer),
+      atrace_wrapper_(std::move(atrace_wrapper)),
       primary_(std::move(ftrace_procfs), std::move(table), std::move(muxer)),
       weak_factory_(this) {}
 
@@ -816,7 +822,8 @@
   auto syscalls = SyscallTable::FromCurrentArch();
 
   auto muxer = std::make_unique<FtraceConfigMuxer>(
-      ftrace_procfs.get(), table.get(), std::move(syscalls), vendor_evts,
+      ftrace_procfs.get(), atrace_wrapper_.get(), table.get(),
+      std::move(syscalls), vendor_evts,
       /* secondary_instance= */ true);
   return std::make_unique<FtraceInstanceState>(
       std::move(ftrace_procfs), std::move(table), std::move(muxer));
diff --git a/src/traced/probes/ftrace/ftrace_controller.h b/src/traced/probes/ftrace/ftrace_controller.h
index 1e7c575..a858dfe 100644
--- a/src/traced/probes/ftrace/ftrace_controller.h
+++ b/src/traced/probes/ftrace/ftrace_controller.h
@@ -29,6 +29,7 @@
 #include "perfetto/ext/base/weak_ptr.h"
 #include "perfetto/ext/tracing/core/basic_types.h"
 #include "src/kallsyms/lazy_kernel_symbolizer.h"
+#include "src/traced/probes/ftrace/atrace_wrapper.h"
 #include "src/traced/probes/ftrace/cpu_reader.h"
 #include "src/traced/probes/ftrace/ftrace_config_utils.h"
 
@@ -99,6 +100,7 @@
 
   FtraceController(std::unique_ptr<FtraceProcfs>,
                    std::unique_ptr<ProtoTranslationTable>,
+                   std::unique_ptr<AtraceWrapper>,
                    std::unique_ptr<FtraceConfigMuxer>,
                    base::TaskRunner*,
                    Observer*);
@@ -123,6 +125,8 @@
 
   virtual uint64_t NowMs() const;
 
+  AtraceWrapper* atrace_wrapper() const { return atrace_wrapper_.get(); }
+
  private:
   friend class TestFtraceController;
   enum class PollSupport { kUntested, kSupported, kUnsupported };
@@ -168,6 +172,7 @@
   bool retain_ksyms_on_stop_ = false;
   PollSupport buffer_watermark_support_ = PollSupport::kUntested;
   std::set<FtraceDataSource*> data_sources_;
+  std::unique_ptr<AtraceWrapper> atrace_wrapper_;
   // Default tracefs instance (normally /sys/kernel/tracing) is valid for as
   // long as the controller is valid.
   // Secondary instances (i.e. /sys/kernel/tracing/instances/...) are created
diff --git a/src/traced/probes/ftrace/ftrace_controller_unittest.cc b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
index 28ca2d4..1a3538e 100644
--- a/src/traced/probes/ftrace/ftrace_controller_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
@@ -102,9 +102,10 @@
 }
 
 std::unique_ptr<FtraceConfigMuxer> FakeMuxer(FtraceProcfs* ftrace,
+                                             AtraceWrapper* atrace_wrapper,
                                              ProtoTranslationTable* table) {
   return std::unique_ptr<FtraceConfigMuxer>(new FtraceConfigMuxer(
-      ftrace, table, SyscallTable(Architecture::kUnknown), {}));
+      ftrace, atrace_wrapper, table, SyscallTable(Architecture::kUnknown), {}));
 }
 
 class MockFtraceProcfs : public FtraceProcfs {
@@ -201,6 +202,12 @@
   std::string current_tracer_ = "nop";
 };
 
+class MockAtraceWrapper : public AtraceWrapper {
+ public:
+  MOCK_METHOD(bool, RunAtrace, (const std::vector<std::string>&, std::string*));
+  MOCK_METHOD(bool, IsOldAtrace, ());
+};
+
 }  // namespace
 
 class TestFtraceController : public FtraceController,
@@ -208,11 +215,13 @@
  public:
   TestFtraceController(std::unique_ptr<MockFtraceProcfs> ftrace_procfs,
                        std::unique_ptr<Table> table,
+                       std::unique_ptr<AtraceWrapper> atrace_wrapper,
                        std::unique_ptr<FtraceConfigMuxer> muxer,
                        std::unique_ptr<MockTaskRunner> runner,
                        MockFtraceProcfs* raw_procfs)
       : FtraceController(std::move(ftrace_procfs),
                          std::move(table),
+                         std::move(atrace_wrapper),
                          std::move(muxer),
                          runner.get(),
                          /*observer=*/this),
@@ -256,7 +265,7 @@
     PERFETTO_CHECK(ftrace_procfs);
 
     auto table = FakeTable(ftrace_procfs.get());
-    auto muxer = FakeMuxer(ftrace_procfs.get(), table.get());
+    auto muxer = FakeMuxer(ftrace_procfs.get(), atrace_wrapper(), table.get());
     return std::unique_ptr<FtraceController::FtraceInstanceState>(
         new FtraceController::FtraceInstanceState(
             std::move(ftrace_procfs), std::move(table), std::move(muxer)));
@@ -289,14 +298,17 @@
         new MockFtraceProcfs("/root/", cpu_count));
   }
 
+  std::unique_ptr<AtraceWrapper> atrace_wrapper;
+
   auto table = FakeTable(ftrace_procfs.get());
 
-  auto muxer = FakeMuxer(ftrace_procfs.get(), table.get());
+  auto muxer =
+      FakeMuxer(ftrace_procfs.get(), atrace_wrapper.get(), table.get());
 
   MockFtraceProcfs* raw_procfs = ftrace_procfs.get();
   return std::unique_ptr<TestFtraceController>(new TestFtraceController(
-      std::move(ftrace_procfs), std::move(table), std::move(muxer),
-      std::move(runner), raw_procfs));
+      std::move(ftrace_procfs), std::move(table), std::move(atrace_wrapper),
+      std::move(muxer), std::move(runner), raw_procfs));
 }
 
 }  // namespace
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_alloc/format b/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_alloc/format
new file mode 100644
index 0000000..77172a3
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_alloc/format
@@ -0,0 +1,15 @@
+name: fastrpc_dma_alloc
+ID: 1353
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:int cid;	offset:8;	size:4;	signed:1;
+	field:u64 phys;	offset:16;	size:8;	signed:0;
+	field:size_t size;	offset:24;	size:8;	signed:0;
+	field:unsigned long attr;	offset:32;	size:8;	signed:0;
+	field:int mflags;	offset:40;	size:4;	signed:1;
+
+print fmt: "cid %d, phys 0x%llx, size %zu, attr 0x%lx, flags 0x%x", REC->cid, REC->phys, REC->size, REC->attr, REC->mflags
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_free/format b/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_free/format
new file mode 100644
index 0000000..44d6e76
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_free/format
@@ -0,0 +1,13 @@
+name: fastrpc_dma_free
+ID: 1354
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:int cid;	offset:8;	size:4;	signed:1;
+	field:u64 phys;	offset:16;	size:8;	signed:0;
+	field:size_t size;	offset:24;	size:8;	signed:0;
+
+print fmt: "cid %d, phys 0x%llx, size %zu", REC->cid, REC->phys, REC->size
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_map/format b/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_map/format
new file mode 100644
index 0000000..6576a1f
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_map/format
@@ -0,0 +1,17 @@
+name: fastrpc_dma_map
+ID: 1351
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:int cid;	offset:8;	size:4;	signed:1;
+	field:int fd;	offset:12;	size:4;	signed:1;
+	field:u64 phys;	offset:16;	size:8;	signed:0;
+	field:size_t size;	offset:24;	size:8;	signed:0;
+	field:size_t len;	offset:32;	size:8;	signed:0;
+	field:unsigned int attr;	offset:40;	size:4;	signed:0;
+	field:int mflags;	offset:44;	size:4;	signed:1;
+
+print fmt: "cid %d, fd %d, phys 0x%llx, size %zu (len %zu), attr 0x%x, flags 0x%x", REC->cid, REC->fd, REC->phys, REC->size, REC->len, REC->attr, REC->mflags
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_unmap/format b/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_unmap/format
new file mode 100644
index 0000000..028495c
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/fastrpc/fastrpc_dma_unmap/format
@@ -0,0 +1,13 @@
+name: fastrpc_dma_unmap
+ID: 1352
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:int cid;	offset:8;	size:4;	signed:1;
+	field:u64 phys;	offset:16;	size:8;	signed:0;
+	field:size_t size;	offset:24;	size:8;	signed:0;
+
+print fmt: "cid %d, phys 0x%llx, size %zu", REC->cid, REC->phys, REC->size
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index ba0d010..b71f0b8 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -4930,15 +4930,23 @@
 
   // Check that clock snapshots are monotonic and don't contain timestamps from
   // trace events with explicit timestamps.
-  std::unordered_map<uint64_t, uint64_t> last_clock_ts;
+  struct LastTs {
+    uint64_t ts = 0;
+    uint64_t seq_id = 0;
+  };
+  std::unordered_map<uint64_t, LastTs> last_clock_ts;
   for (const auto& packet : trace.packet()) {
     if (packet.has_clock_snapshot()) {
       for (auto& clock : packet.clock_snapshot().clocks()) {
         if (!clock.is_incremental()) {
           uint64_t ts = clock.timestamp();
           uint64_t id = clock.clock_id();
-          EXPECT_LE(last_clock_ts[id], ts);
-          last_clock_ts[id] = ts;
+          EXPECT_LE(last_clock_ts[id].ts, ts)
+              << "This sequence:" << packet.trusted_packet_sequence_id()
+              << " prev sequence:" << last_clock_ts[id].seq_id
+              << " clock_id:" << id;
+          last_clock_ts[id].ts = ts;
+          last_clock_ts[id].seq_id = packet.trusted_packet_sequence_id();
         }
       }
 
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 e82ef5c..a359061 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 @@
-c40361fe7a34f3506e0b84d585ea15eb07d111bf0fa49d3976f40738e3448c7a
\ No newline at end of file
+4b4bd13cbae5710efda25c6e2495c7ec5614d348701b9fec1fd554d5b1c61064
\ 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 260ea4f..02b1315 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 @@
-0954283d6fc7beb554ffbaee5afe354aed896a7eeefe2475e14c1ad64327a6f7
\ No newline at end of file
+687a6fc2a478b048bdcbbbfe6231cf3b33463068c8100659a5be0220f41d07fb
\ 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 880b363..ce2d330 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 @@
-8d0cb7b3d4794c4f036fb851dd8a4726e7e6c90f15657b86c06a1fabe2c7984c
\ No newline at end of file
+ce747a5d7a1547aaa238bef8aa7b241d3e16cdc38dd34ef165e57aecb6659a92
\ 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 e327bd1..905ad76 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 @@
-9e84ad0881a56d3731066604e973b6297dee7b9a602e6b35013560fd0bc8fd39
\ No newline at end of file
+c1fa13737c59bf1eeaa997037ca99117625b4d7956496702734317555ff5a2bc
\ 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 1d3de51..de5d8b4 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 @@
-eb92c26c12d039aac315e3514bc24ebfcf392914309b28bc83a172127fe60250
\ No newline at end of file
+b1cbf99aeee1bab2eabe0d1495697970535ef350f3d6e6d177ed21d469f3b11e
\ 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 11dfc47..0843823 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 @@
-1d80ba6fc24a1db8252e24e0d4250787bf7da4f6f3f7efe4e71b63a2b42c6e23
\ No newline at end of file
+562482bc25d7a2d34b30d6003d100207e7f1eb0a48ea4549445a340e34a2e3b4
\ 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 97c82b1..24ab368 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 @@
-46d1d64e852be22cb86a7eb6868aea3007ce04c663e9667ae55b71d5a0fad6b7
\ No newline at end of file
+f9b1725cd859b6cccf34d171e8b2e9d87b44a4fc241e9f577ccfde2ae3cc5216
\ 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 f60a4ff..fa3e082 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 @@
-cd2fafc00d2f2ab761a7c17a0bdc9e606e3996fe20c17ff7e0b61a0e11c55199
\ No newline at end of file
+82b386830bbfc6287b893331ecffcb1058634868a1bd43cce240b09297f060c7
\ 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 c417604..7b76fd1 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 @@
-049301241729848f47474868766daf513e7f739efddbfc3dc3f96200f9b63bc6
\ No newline at end of file
+431b27cc6333c21aac4bf681ab9d7f06e096f340f7913f1c1e3a943d4c4f0111
\ 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 97c82b1..24ab368 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 @@
-46d1d64e852be22cb86a7eb6868aea3007ce04c663e9667ae55b71d5a0fad6b7
\ No newline at end of file
+f9b1725cd859b6cccf34d171e8b2e9d87b44a4fc241e9f577ccfde2ae3cc5216
\ 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 c417604..7b76fd1 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 @@
-049301241729848f47474868766daf513e7f739efddbfc3dc3f96200f9b63bc6
\ No newline at end of file
+431b27cc6333c21aac4bf681ab9d7f06e096f340f7913f1c1e3a943d4c4f0111
\ 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 c417604..7b76fd1 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 @@
-049301241729848f47474868766daf513e7f739efddbfc3dc3f96200f9b63bc6
\ No newline at end of file
+431b27cc6333c21aac4bf681ab9d7f06e096f340f7913f1c1e3a943d4c4f0111
\ 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 c69fd14..a902763 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 @@
-4b0c253d17c45c7e787e7b9cc484a9c0af38bcf77a92645266f15879af92d8e2
\ No newline at end of file
+eeb0625eb5286e0ca40b5d308608714ae7da0ca67d6bf07fac314a6639b5ddb5
\ 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 97c82b1..24ab368 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 @@
-46d1d64e852be22cb86a7eb6868aea3007ce04c663e9667ae55b71d5a0fad6b7
\ No newline at end of file
+f9b1725cd859b6cccf34d171e8b2e9d87b44a4fc241e9f577ccfde2ae3cc5216
\ 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 c417604..7b76fd1 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 @@
-049301241729848f47474868766daf513e7f739efddbfc3dc3f96200f9b63bc6
\ No newline at end of file
+431b27cc6333c21aac4bf681ab9d7f06e096f340f7913f1c1e3a943d4c4f0111
\ 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 c417604..7b76fd1 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 @@
-049301241729848f47474868766daf513e7f739efddbfc3dc3f96200f9b63bc6
\ No newline at end of file
+431b27cc6333c21aac4bf681ab9d7f06e096f340f7913f1c1e3a943d4c4f0111
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/syntax/table_tests.py b/test/trace_processor/diff_tests/syntax/table_tests.py
index 0125c72..2eba2b0 100644
--- a/test/trace_processor/diff_tests/syntax/table_tests.py
+++ b/test/trace_processor/diff_tests/syntax/table_tests.py
@@ -78,6 +78,7 @@
         3,"perfetto_table_info","track_id","uint32",0,0
         4,"perfetto_table_info","value","double",0,0
         5,"perfetto_table_info","arg_set_id","uint32",1,0
+        6,"perfetto_table_info","machine_id","uint32",1,0
         """))
 
   def test_perfetto_table_info_runtime_table(self):
diff --git a/test/trace_processor/diff_tests/tables/tests_sched.py b/test/trace_processor/diff_tests/tables/tests_sched.py
index c2ad002..306b8e8 100644
--- a/test/trace_processor/diff_tests/tables/tests_sched.py
+++ b/test/trace_processor/diff_tests/tables/tests_sched.py
@@ -124,7 +124,8 @@
     return DiffTestBlueprint(
         trace=DataPath('sched_wakeup_trace.atr'),
         query="""
-        SELECT * FROM raw WHERE common_flags != 0 ORDER BY ts LIMIT 10
+        SELECT id, type, ts, name, cpu, utid, arg_set_id, common_flags
+        FROM raw WHERE common_flags != 0 ORDER BY ts LIMIT 10
         """,
         out=Csv("""
         "id","type","ts","name","cpu","utid","arg_set_id","common_flags"
diff --git a/tools/cpu_profile b/tools/cpu_profile
index 9d84d12..17f0996 100755
--- a/tools/cpu_profile
+++ b/tools/cpu_profile
@@ -37,18 +37,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v43.2
+# This file has been generated by: tools/roll-prebuilts v44.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        7790424,
+        8069808,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-amd64/traceconv',
     'sha256':
-        'c1d9c50c89545b41af88525dc6f3ce508156ed3787ccecae0ff7c8e736c39318',
+        '7d9c0421235c083932408a5a716372dfddc3a87828b2b3b7e30f8d3aa1c5bf43',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -58,11 +58,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7264824,
+        7529704,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-arm64/traceconv',
     'sha256':
-        'df5349ae462dbd7c1ca9a1b8a0f09c044a47026d6ad8dc24e6945701d7c61a84',
+        'bffadacd2a6e44a9f5c7b0beb48f3f5d568433fd9d425cdee5342e7f3c112cbb',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -72,11 +72,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7885952,
+        8152216,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-amd64/traceconv',
     'sha256':
-        'b2c19364c1fb68e9f5cde610e5d71dd59b9fdf2bada8f7e1eefc319f828f7cb1',
+        'b1815e29aabb51deff0c68e3e690c96aedfea0796a0292d5f177815d33584995',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -86,11 +86,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        5919372,
+        6132076,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm/traceconv',
     'sha256':
-        'b669be326b4b6a024e557e0927f1014fd1ea5d5427e194dc0653f21acac273ee',
+        '2b391081ce9ce45d843584816bc11ba7383b634c88ffa75c7dc927a9632e6d28',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -100,11 +100,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7588200,
+        7862696,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm64/traceconv',
     'sha256':
-        'aff1c4751e721733ce85f58048c17971399fe605a81ac300d306c200d6957818',
+        'd10a598fb6c14926ceb3afb0fc9841a4924c2fedadf9ef981609781ecb8b338b',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -114,55 +114,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        5931120,
+        6131288,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm/traceconv',
     'sha256':
-        '826212f658fef744fbaeea66331b6fe7ca0152f69cf63ff2ea218a376d5d41d9'
+        '3cf391f42bb51e47159b2236b1171cd1bd4461f3e4576b00100f590cf7ff8b2b'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7546224,
+        7798968,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm64/traceconv',
     'sha256':
-        '29dd7e93e9182c4413a9f9c1c6a6f643f64e1fe0b9657ab1ea3cec8b0bb360c9'
+        '1b17a740ba86a5e218b69dd981d739173515777ee761f3723446f6e400e9367e'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8176528,
+        8464080,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x86/traceconv',
     'sha256':
-        '63c2ebe7ed51f9667bcf69d7b9679f6077db5fd8ee9e1be7b786037e2a649fcb'
+        '2dc045a79276e62f71cf40c1e8ee433125785ce32a223ce4c9e5871cacc3940e'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        7767560,
+        8025896,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x64/traceconv',
     'sha256':
-        'bb9350230c2fac5adf9e6fe21937865b6eaafaefc555ae26e68cae9419ad5ee8'
+        '03db509df8e3816b4c4d78d187d42794b37c3d2c830d85feae0f17a5b581ee53'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        7645696,
+        7920128,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/windows-amd64/traceconv.exe',
     'sha256':
-        '0c84b712941e4f63f74e66731745f94aec3cd30d94469e52cdf1143262f063a4',
+        '97b66259d385a5bd482ebb5a21535b67e3836fb0cf7c971bb36d5f5ea00774bd',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/heap_profile b/tools/heap_profile
index 39249bb..700cfeb 100755
--- a/tools/heap_profile
+++ b/tools/heap_profile
@@ -34,18 +34,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v43.2
+# This file has been generated by: tools/roll-prebuilts v44.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        7790424,
+        8069808,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-amd64/traceconv',
     'sha256':
-        'c1d9c50c89545b41af88525dc6f3ce508156ed3787ccecae0ff7c8e736c39318',
+        '7d9c0421235c083932408a5a716372dfddc3a87828b2b3b7e30f8d3aa1c5bf43',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -55,11 +55,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7264824,
+        7529704,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-arm64/traceconv',
     'sha256':
-        'df5349ae462dbd7c1ca9a1b8a0f09c044a47026d6ad8dc24e6945701d7c61a84',
+        'bffadacd2a6e44a9f5c7b0beb48f3f5d568433fd9d425cdee5342e7f3c112cbb',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -69,11 +69,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7885952,
+        8152216,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-amd64/traceconv',
     'sha256':
-        'b2c19364c1fb68e9f5cde610e5d71dd59b9fdf2bada8f7e1eefc319f828f7cb1',
+        'b1815e29aabb51deff0c68e3e690c96aedfea0796a0292d5f177815d33584995',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -83,11 +83,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        5919372,
+        6132076,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm/traceconv',
     'sha256':
-        'b669be326b4b6a024e557e0927f1014fd1ea5d5427e194dc0653f21acac273ee',
+        '2b391081ce9ce45d843584816bc11ba7383b634c88ffa75c7dc927a9632e6d28',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -97,11 +97,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7588200,
+        7862696,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm64/traceconv',
     'sha256':
-        'aff1c4751e721733ce85f58048c17971399fe605a81ac300d306c200d6957818',
+        'd10a598fb6c14926ceb3afb0fc9841a4924c2fedadf9ef981609781ecb8b338b',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -111,55 +111,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        5931120,
+        6131288,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm/traceconv',
     'sha256':
-        '826212f658fef744fbaeea66331b6fe7ca0152f69cf63ff2ea218a376d5d41d9'
+        '3cf391f42bb51e47159b2236b1171cd1bd4461f3e4576b00100f590cf7ff8b2b'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7546224,
+        7798968,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm64/traceconv',
     'sha256':
-        '29dd7e93e9182c4413a9f9c1c6a6f643f64e1fe0b9657ab1ea3cec8b0bb360c9'
+        '1b17a740ba86a5e218b69dd981d739173515777ee761f3723446f6e400e9367e'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8176528,
+        8464080,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x86/traceconv',
     'sha256':
-        '63c2ebe7ed51f9667bcf69d7b9679f6077db5fd8ee9e1be7b786037e2a649fcb'
+        '2dc045a79276e62f71cf40c1e8ee433125785ce32a223ce4c9e5871cacc3940e'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        7767560,
+        8025896,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x64/traceconv',
     'sha256':
-        'bb9350230c2fac5adf9e6fe21937865b6eaafaefc555ae26e68cae9419ad5ee8'
+        '03db509df8e3816b4c4d78d187d42794b37c3d2c830d85feae0f17a5b581ee53'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        7645696,
+        7920128,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/windows-amd64/traceconv.exe',
     'sha256':
-        '0c84b712941e4f63f74e66731745f94aec3cd30d94469e52cdf1143262f063a4',
+        '97b66259d385a5bd482ebb5a21535b67e3836fb0cf7c971bb36d5f5ea00774bd',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/record_android_trace b/tools/record_android_trace
index f77222b..ceb0808 100755
--- a/tools/record_android_trace
+++ b/tools/record_android_trace
@@ -33,18 +33,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v43.2
+# This file has been generated by: tools/roll-prebuilts v44.0
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1564728,
+        1548256,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-amd64/tracebox',
     'sha256':
-        '239736808cbfba5085892e15c145381ea37ddba5df7c8fad97b68d9c04a4d860',
+        '392cb1ae5f11c6a87d15e69fb6576e5c62ae3b1d87a43d68d7fe8bd3cea4fd7e',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -54,11 +54,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1459160,
+        1459096,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-arm64/tracebox',
     'sha256':
-        '4af1449dc90e5505bd5f3d638f11b8bf7e5dc82c0290f0085dc0b335ababd143',
+        '0edde5e3d35ef044848eeca5f63da7fa9f96e4bb3cac5e87ba4e2772a09e8f8f',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -68,11 +68,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2314424,
+        2304304,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-amd64/tracebox',
     'sha256':
-        'a97a5efdaf475f13f4f5947c03289029253f89d0f44caa64765b00b269551297',
+        'cd2b3c0fdc7d0a649bbe4103901263927b2f736836ce56fa06467efa5c825472',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -82,11 +82,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1418968,
+        1408648,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm/tracebox',
     'sha256':
-        '818390305d15730fadcbd87dc3c8d87a439e040a02b5098f51af15dfff3f0ca0',
+        '51794d99493c04ced26a40a242ddb6a53b1213ee96b0b1af9ba874715656ff06',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -96,11 +96,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2221176,
+        2212000,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm64/tracebox',
     'sha256':
-        '5a3cf8c755e08b7a558083a70ad28293baa389e544ebd09806b6a883a5f17952',
+        '7faf03feecf045ed25bfad7cb845621c533ca50b586e722e1bb61aa7fd54cd74',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -110,44 +110,44 @@
     'file_name':
         'tracebox',
     'file_size':
-        1304280,
+        1299200,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm/tracebox',
     'sha256':
-        '87bb07c7ac4c58d306975cabad3ed5d4b6fe11a8d617dad30fe7dd25bfdc6736'
+        'fa78644befc527481dac76b217762cbddb9233cc050c2a31444c78391ed7715c'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        2076144,
+        2067768,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm64/tracebox',
     'sha256':
-        '501b2bb0cba0ecb770e2b568698f89f6b42d083fcca111c872f7a0e95c0cacc5'
+        'f3167ce57aec78e200675640a683a938bcb206219346427e42f86fdc70923386'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        2253568,
+        2241784,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x86/tracebox',
     'sha256':
-        '28be4f88a9b8f950ebc45a20d4844002f9b3f81ef0230d0a5d9b1627cf89c9a5'
+        '9c9a4b1c498c985c3fbbd9122a9df66bb5275c715c54475d7fbabfec5722d50a'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2101752,
+        2092696,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x64/tracebox',
     'sha256':
-        '6c08b743b9cb6073a75e2b3dd34098e09e3c8bcace89096dc5b9d0f071b2831a'
+        '96fdc7b584247a2f4fc18d4a4a8e8891ab88693c8bc7a301e8dffb3ec8b96a1d'
 }]
 
 # ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/trace_processor b/tools/trace_processor
index ba2a38b..0917f54 100755
--- a/tools/trace_processor
+++ b/tools/trace_processor
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/trace_processor_shell.py
-# This file has been generated by: tools/roll-prebuilts v43.2
+# This file has been generated by: tools/roll-prebuilts v44.0
 TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8583624,
+        8879352,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-amd64/trace_processor_shell',
     'sha256':
-        'a1c16a74725cefb62406b39538b5d22f56a94e390a0394816d2945793f91f8cf',
+        '8ac591150919d5e3701a3fdb7ce44f2ae9b48a4b27afc2da31b97dba3238c4c8',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7980232,
+        8261544,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-arm64/trace_processor_shell',
     'sha256':
-        '3651654cd462df8a2ec8cb3f7375cee01ccc11861a675b9da0d00aa697efe7b2',
+        '55ef799a383fb460e0167fad68b8f169d6d46bc10285df9db26cecec52dd24f1',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8770200,
+        9035064,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-amd64/trace_processor_shell',
     'sha256':
-        '0796a01af496a6b62623fea89b2d34063ade9d156783e1f88949d8b7ab1f76d0',
+        'd4826f1c2acf0a4caaa167bc089d12a1e6460fcd2847ec8c026933ed73d30540',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        6371036,
+        6581588,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm/trace_processor_shell',
     'sha256':
-        'f4bbef5008de376913c3a95410802d94d8d5715439c3f797be0e4ca8c9bccb1a',
+        '1918aa71521e3daaeedadd9a0760f597c94c3baa803836e442f54c584dc402ac',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8425776,
+        8698104,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm64/trace_processor_shell',
     'sha256':
-        'f1ff5585a06ad8b9fc1d13dbf8d02f39d2804019ea7e70b740872e0f6826695f',
+        '9b4e77e541bf397bcdb4f51b30b0cea23aea761865c5bebdac424b15beecdf18',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,55 +107,55 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        6382140,
+        6581420,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm/trace_processor_shell',
     'sha256':
-        '989b209a108c7d44e2531bb15a5d57f667c717cf774bb8a4a810f99fda0b958d'
+        'cdf7d4f0ad38f977f20d52c2d881f5aeac3ad1a9b07032dee32b08ee631ad041'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8340616,
+        8592256,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm64/trace_processor_shell',
     'sha256':
-        'ade7e72990cb97fd74766cd0df50a24cbd547d2f54c26b49d66236c809922645'
+        '8df9e4e01509184fe5d65f79af343384f8e11f90eabebe8075cf779d6d82304d'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9170488,
+        9457000,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x86/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x86/trace_processor_shell',
     'sha256':
-        '6bd1f74616fd8f620fbf3228f83301844adae08a772b8ac2a64703724a79b516'
+        'd0404998a661864dfabb440a1227ded43c6e793741675576c863460ce869bead'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8591040,
+        8848144,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x64/trace_processor_shell',
     'sha256':
-        '1ccbcb8b2928615cf512cf97eaba395de6f1fc5d70313e884ea3975867f365ea'
+        '8e5a539457f8a50b898a2f2178acd14d7f4076a6a10bbc030a7c2a8cb229b57f'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'trace_processor_shell.exe',
     'file_size':
-        8676352,
+        8951296,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/windows-amd64/trace_processor_shell.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/windows-amd64/trace_processor_shell.exe',
     'sha256':
-        '9125418fedd96eb0e6f1ddeaf46069a05bdcb910592b059669fc982b4fff3f1b',
+        'd01a7d0c3bd460a041fdd25cba88163d945ef939f48de3c76cdc554e19321419',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/tracebox b/tools/tracebox
index 496fe43..a9a89e6 100755
--- a/tools/tracebox
+++ b/tools/tracebox
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v43.2
+# This file has been generated by: tools/roll-prebuilts v44.0
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1564728,
+        1548256,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-amd64/tracebox',
     'sha256':
-        '239736808cbfba5085892e15c145381ea37ddba5df7c8fad97b68d9c04a4d860',
+        '392cb1ae5f11c6a87d15e69fb6576e5c62ae3b1d87a43d68d7fe8bd3cea4fd7e',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1459160,
+        1459096,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-arm64/tracebox',
     'sha256':
-        '4af1449dc90e5505bd5f3d638f11b8bf7e5dc82c0290f0085dc0b335ababd143',
+        '0edde5e3d35ef044848eeca5f63da7fa9f96e4bb3cac5e87ba4e2772a09e8f8f',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2314424,
+        2304304,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-amd64/tracebox',
     'sha256':
-        'a97a5efdaf475f13f4f5947c03289029253f89d0f44caa64765b00b269551297',
+        'cd2b3c0fdc7d0a649bbe4103901263927b2f736836ce56fa06467efa5c825472',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1418968,
+        1408648,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm/tracebox',
     'sha256':
-        '818390305d15730fadcbd87dc3c8d87a439e040a02b5098f51af15dfff3f0ca0',
+        '51794d99493c04ced26a40a242ddb6a53b1213ee96b0b1af9ba874715656ff06',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2221176,
+        2212000,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm64/tracebox',
     'sha256':
-        '5a3cf8c755e08b7a558083a70ad28293baa389e544ebd09806b6a883a5f17952',
+        '7faf03feecf045ed25bfad7cb845621c533ca50b586e722e1bb61aa7fd54cd74',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,44 +107,44 @@
     'file_name':
         'tracebox',
     'file_size':
-        1304280,
+        1299200,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm/tracebox',
     'sha256':
-        '87bb07c7ac4c58d306975cabad3ed5d4b6fe11a8d617dad30fe7dd25bfdc6736'
+        'fa78644befc527481dac76b217762cbddb9233cc050c2a31444c78391ed7715c'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        2076144,
+        2067768,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm64/tracebox',
     'sha256':
-        '501b2bb0cba0ecb770e2b568698f89f6b42d083fcca111c872f7a0e95c0cacc5'
+        'f3167ce57aec78e200675640a683a938bcb206219346427e42f86fdc70923386'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        2253568,
+        2241784,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x86/tracebox',
     'sha256':
-        '28be4f88a9b8f950ebc45a20d4844002f9b3f81ef0230d0a5d9b1627cf89c9a5'
+        '9c9a4b1c498c985c3fbbd9122a9df66bb5275c715c54475d7fbabfec5722d50a'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2101752,
+        2092696,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x64/tracebox',
     'sha256':
-        '6c08b743b9cb6073a75e2b3dd34098e09e3c8bcace89096dc5b9d0f071b2831a'
+        '96fdc7b584247a2f4fc18d4a4a8e8891ab88693c8bc7a301e8dffb3ec8b96a1d'
 }]
 
 # ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/traceconv b/tools/traceconv
index dba29fa..8ba7114 100755
--- a/tools/traceconv
+++ b/tools/traceconv
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v43.2
+# This file has been generated by: tools/roll-prebuilts v44.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        7790424,
+        8069808,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-amd64/traceconv',
     'sha256':
-        'c1d9c50c89545b41af88525dc6f3ce508156ed3787ccecae0ff7c8e736c39318',
+        '7d9c0421235c083932408a5a716372dfddc3a87828b2b3b7e30f8d3aa1c5bf43',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7264824,
+        7529704,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/mac-arm64/traceconv',
     'sha256':
-        'df5349ae462dbd7c1ca9a1b8a0f09c044a47026d6ad8dc24e6945701d7c61a84',
+        'bffadacd2a6e44a9f5c7b0beb48f3f5d568433fd9d425cdee5342e7f3c112cbb',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7885952,
+        8152216,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-amd64/traceconv',
     'sha256':
-        'b2c19364c1fb68e9f5cde610e5d71dd59b9fdf2bada8f7e1eefc319f828f7cb1',
+        'b1815e29aabb51deff0c68e3e690c96aedfea0796a0292d5f177815d33584995',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        5919372,
+        6132076,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm/traceconv',
     'sha256':
-        'b669be326b4b6a024e557e0927f1014fd1ea5d5427e194dc0653f21acac273ee',
+        '2b391081ce9ce45d843584816bc11ba7383b634c88ffa75c7dc927a9632e6d28',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7588200,
+        7862696,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/linux-arm64/traceconv',
     'sha256':
-        'aff1c4751e721733ce85f58048c17971399fe605a81ac300d306c200d6957818',
+        'd10a598fb6c14926ceb3afb0fc9841a4924c2fedadf9ef981609781ecb8b338b',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,55 +107,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        5931120,
+        6131288,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm/traceconv',
     'sha256':
-        '826212f658fef744fbaeea66331b6fe7ca0152f69cf63ff2ea218a376d5d41d9'
+        '3cf391f42bb51e47159b2236b1171cd1bd4461f3e4576b00100f590cf7ff8b2b'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7546224,
+        7798968,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-arm64/traceconv',
     'sha256':
-        '29dd7e93e9182c4413a9f9c1c6a6f643f64e1fe0b9657ab1ea3cec8b0bb360c9'
+        '1b17a740ba86a5e218b69dd981d739173515777ee761f3723446f6e400e9367e'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8176528,
+        8464080,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x86/traceconv',
     'sha256':
-        '63c2ebe7ed51f9667bcf69d7b9679f6077db5fd8ee9e1be7b786037e2a649fcb'
+        '2dc045a79276e62f71cf40c1e8ee433125785ce32a223ce4c9e5871cacc3940e'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        7767560,
+        8025896,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/android-x64/traceconv',
     'sha256':
-        'bb9350230c2fac5adf9e6fe21937865b6eaafaefc555ae26e68cae9419ad5ee8'
+        '03db509df8e3816b4c4d78d187d42794b37c3d2c830d85feae0f17a5b581ee53'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        7645696,
+        7920128,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v43.2/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v44.0/windows-amd64/traceconv.exe',
     'sha256':
-        '0c84b712941e4f63f74e66731745f94aec3cd30d94469e52cdf1143262f063a4',
+        '97b66259d385a5bd482ebb5a21535b67e3836fb0cf7c971bb36d5f5ea00774bd',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/ui/src/assets/track_panel.scss b/ui/src/assets/track_panel.scss
index 5025959..747014d 100644
--- a/ui/src/assets/track_panel.scss
+++ b/ui/src/assets/track_panel.scss
@@ -44,6 +44,7 @@
   display: grid;
   grid-template-columns: auto 1fr;
   grid-template-rows: 1fr 0;
+  container-type: size;
 
   &::after {
     display: block;
@@ -56,15 +57,28 @@
 
   .track-shell {
     @include transition();
-    padding-left: 10px;
-    display: grid;
     cursor: grab;
-    grid-template-areas: "title buttons";
-    grid-template-columns: 1fr auto;
-    align-items: center;
     width: var(--track-shell-width);
     border-right: 1px solid #c7d0db;
-    overflow: hidden;
+
+    .track-menubar {
+      position: sticky;
+      top: 0;
+      display: grid;
+      padding-block: 6px;
+      padding-left: 10px;
+      padding-right: 2px;
+      grid-template-areas: "title buttons";
+      grid-template-columns: 1fr auto;
+    }
+
+    .pf-visible-on-hover {
+      visibility: hidden;
+    }
+
+    &:hover .pf-visible-on-hover {
+      visibility: visible;
+    }
 
     &.drag {
       background-color: #eee;
@@ -99,33 +113,9 @@
       display: flex;
       height: 100%;
       align-items: center;
-    }
-    .track-button {
-      @include transition();
-      color: rgb(60, 86, 136);
-      cursor: pointer;
-      width: 22px;
       font-size: 18px;
-      visibility: hidden;
     }
 
-    .track-button.show {
-      visibility: visible;
-    }
-    .track-button.full-height {
-      display: flex;
-      height: 100%;
-      align-items: center;
-      justify-content: center;
-
-      &:hover {
-        background-color: #ebeef9;
-      }
-    }
-
-    &:hover .track-button {
-      visibility: visible;
-    }
     &.flash {
       background-color: #ffe263;
     }
@@ -137,6 +127,12 @@
   grid-template-columns: auto 1fr;
   grid-template-rows: 1fr;
   height: 40px;
+
+  .shell {
+    border-right: 1px solid transparent;
+    padding-right: 2px;
+  }
+
   &::after {
     display: block;
     content: "";
@@ -168,10 +164,10 @@
     }
   }
   .shell {
-    padding: 4px 4px;
+    padding-left: 10px;
     display: grid;
-    grid-template-areas: "fold-button title buttons check";
-    grid-template-columns: 28px 1fr auto 20px;
+    grid-template-areas: "fold-button title buttons";
+    grid-template-columns: 28px 1fr auto;
     align-items: center;
     line-height: 1;
     width: var(--track-shell-width);
@@ -209,9 +205,15 @@
     .fold-button {
       grid-area: fold-button;
     }
-    .track-button {
-      font-size: 20px;
+
+    .track-buttons {
+      grid-area: buttons;
+      display: flex;
+      height: 100%;
+      align-items: center;
+      font-size: 18px;
     }
+
     &:hover {
       cursor: pointer;
       .fold-button {
@@ -233,3 +235,27 @@
     }
   }
 }
+
+.pf-panel-group {
+  .track-shell {
+    .track-menubar {
+      top: 40px;
+    }
+  }
+}
+
+// If the track is short, center the track titlebar vertically
+@container (height < 26px) {
+  .track {
+    .track-shell {
+      display: flex;
+      flex-direction: column;
+      align-items: stretch;
+      justify-content: center;
+
+      .track-menubar {
+        padding-block: 0px;
+      }
+    }
+  }
+}
diff --git a/ui/src/assets/viewer_page.scss b/ui/src/assets/viewer_page.scss
index 2ccc9eb..c090dc9 100644
--- a/ui/src/assets/viewer_page.scss
+++ b/ui/src/assets/viewer_page.scss
@@ -81,6 +81,9 @@
 
   .notes-panel {
     height: 20px;
+    .pf-toolbar {
+      font-size: 24px;
+    }
   }
 
   .time-selection-panel {
diff --git a/ui/src/assets/widgets/button.scss b/ui/src/assets/widgets/button.scss
index 1c7ef99..a5e8f9a 100644
--- a/ui/src/assets/widgets/button.scss
+++ b/ui/src/assets/widgets/button.scss
@@ -36,7 +36,8 @@
     margin-left: 6px; // Make some room between the icon and label
   }
 
-  & > .material-icons {
+  & > .material-icons,
+  & > .material-icons-filled {
     font-size: inherit;
     line-height: inherit;
   }
@@ -66,7 +67,7 @@
   // Remove default background in minimal mode, showing only the text
   &.pf-minimal {
     background: $pf-minimal-background;
-    color: $pf-minimal-foreground;
+    color: inherit;
 
     &:hover {
       background: $pf-minimal-background-hover;
diff --git a/ui/src/base/monitor.ts b/ui/src/base/monitor.ts
index d4e0f87..e26eb84 100644
--- a/ui/src/base/monitor.ts
+++ b/ui/src/base/monitor.ts
@@ -26,11 +26,24 @@
     this.cached = reducers.map(() => undefined);
   }
 
-  ifStateChanged(callback: Callback): void {
-    const state = this.reducers.map((f) => f());
-    if (state.some((x, i) => x !== this.cached[i])) {
-      callback();
+  /**
+   * Invokes all reducers and compares values against with the previous values.
+   *
+   * If any of the values have changed, |callback| is called (if present) and
+   * returns true, otherwise no callback is called and returns false.
+   *
+   * @param callback Optional callback to call when diffs are detected.
+   * @returns True if diffs were detected, false otherwise.
+   */
+  ifStateChanged(callback?: Callback): boolean {
+    const oldState = this.cached;
+    const newState = this.reducers.map((f) => f());
+    this.cached = newState;
+    if (newState.some((x, i) => x !== oldState[i])) {
+      callback?.();
+      return true;
     }
-    this.cached = state;
+
+    return false;
   }
 }
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index f99ae5c..4910cbd 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -54,7 +54,6 @@
   NewEngineMode,
   OmniboxMode,
   OmniboxState,
-  Pagination,
   PendingDeeplinkState,
   PivotTableResult,
   ProfileType,
@@ -851,10 +850,6 @@
     };
   },
 
-  updateLogsPagination(state: StateDraft, args: Pagination): void {
-    state.logsPagination = args;
-  },
-
   startRecording(state: StateDraft, _: {}): void {
     state.recordingInProgress = true;
     state.lastRecordingError = undefined;
@@ -1202,31 +1197,6 @@
         aggregations,
       );
   },
-
-  setMinimumLogLevel(state: StateDraft, args: {minimumLevel: number}) {
-    state.logFilteringCriteria.minimumLevel = args.minimumLevel;
-  },
-
-  addLogTag(state: StateDraft, args: {tag: string}) {
-    if (!state.logFilteringCriteria.tags.includes(args.tag)) {
-      state.logFilteringCriteria.tags.push(args.tag);
-    }
-  },
-
-  removeLogTag(state: StateDraft, args: {tag: string}) {
-    state.logFilteringCriteria.tags = state.logFilteringCriteria.tags.filter(
-      (t) => t !== args.tag,
-    );
-  },
-
-  updateLogFilterText(state: StateDraft, args: {textEntry: string}) {
-    state.logFilteringCriteria.textEntry = args.textEntry;
-  },
-
-  toggleCollapseByTextEntry(state: StateDraft, _: {}) {
-    state.logFilteringCriteria.hideNonMatching =
-      !state.logFilteringCriteria.hideNonMatching;
-  },
 };
 
 // When we are on the frontend side, we don't really want to execute the
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index 4d987d6..5bf1f5a 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -124,11 +124,6 @@
       mode: 'SEARCH',
     },
 
-    logsPagination: {
-      offset: 0,
-      count: 0,
-    },
-
     status: {msg: '', timestamp: 0},
     selection: {
       kind: 'empty',
@@ -163,14 +158,6 @@
     chromeCategories: undefined,
     nonSerializableState: createEmptyNonSerializableState(),
 
-    logFilteringCriteria: {
-      // The first two log priorities are ignored.
-      minimumLevel: 2,
-      tags: [],
-      textEntry: '',
-      hideNonMatching: true,
-    },
-
     // Somewhere to store plugins' persistent state.
     plugins: {},
   };
diff --git a/ui/src/common/logs.ts b/ui/src/common/logs.ts
deleted file mode 100644
index d0366c0..0000000
--- a/ui/src/common/logs.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2019 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 {time} from '../base/time';
-
-export const LogExistsKey = 'log-exists';
-export const LogBoundsKey = 'log-bounds';
-export const LogEntriesKey = 'log-entries';
-
-export interface LogExists {
-  exists: boolean;
-}
-
-export interface LogBounds {
-  firstLogTs: time;
-  lastLogTs: time;
-  firstVisibleLogTs: time;
-  lastVisibleLogTs: time;
-  totalVisibleLogs: number;
-}
-
-export interface LogEntries {
-  offset: number;
-  timestamps: time[];
-  priorities: number[];
-  tags: string[];
-  messages: string[];
-  isHighlighted: boolean[];
-  processName: string[];
-}
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index dfb93d7..ba60270 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -16,7 +16,7 @@
 
 import {Disposable, Trash} from '../base/disposable';
 import {Registry} from '../base/registry';
-import {time} from '../base/time';
+import {Span, duration, time} from '../base/time';
 import {globals} from '../frontend/globals';
 import {
   Command,
@@ -25,7 +25,6 @@
   MetricVisualisation,
   Migrate,
   Plugin,
-  PluginClass,
   PluginContext,
   PluginContextTrace,
   PluginDescriptor,
@@ -46,6 +45,7 @@
 import {assertExists} from '../base/logging';
 import {raf} from '../core/raf_scheduler';
 import {defaultPlugins} from '../core/default_plugins';
+import {HighPrecisionTimeSpan} from './high_precision_time';
 
 // Every plugin gets its own PluginContext. This is how we keep track
 // what each plugin is doing and how we can blame issues on particular
@@ -311,6 +311,15 @@
     panToTimestamp(ts: time): void {
       globals.panToTimestamp(ts);
     },
+
+    setViewportTime(start: time, end: time): void {
+      const interval = HighPrecisionTimeSpan.fromTime(start, end);
+      globals.timeline.updateVisibleTime(interval);
+    },
+
+    get viewport(): Span<time, duration> {
+      return globals.timeline.visibleTimeSpan;
+    },
   };
 
   dispose(): void {
@@ -321,6 +330,12 @@
   mountStore<T>(migrate: Migrate<T>): Store<T> {
     return globals.store.createSubStore(['plugins', this.pluginId], migrate);
   }
+
+  readonly trace = {
+    get span(): Span<time, duration> {
+      return globals.stateTraceTimeTP();
+    },
+  };
 }
 
 function isPinned(trackId: string): boolean {
@@ -341,20 +356,13 @@
   previousOnTraceLoadTimeMillis?: number;
 }
 
-function isPluginClass(v: unknown): v is PluginClass {
-  return typeof v === 'function' && !!v.prototype.onActivate;
-}
-
 function makePlugin(info: PluginDescriptor): Plugin {
   const {plugin} = info;
 
+  // Class refs are functions, concrete plugins are not
   if (typeof plugin === 'function') {
-    if (isPluginClass(plugin)) {
-      const PluginClass = plugin;
-      return new PluginClass();
-    } else {
-      return plugin();
-    }
+    const PluginClass = plugin;
+    return new PluginClass();
   } else {
     return plugin;
   }
@@ -433,7 +441,7 @@
 
     const context = new PluginContextImpl(id);
 
-    plugin.onActivate(context);
+    plugin.onActivate?.(context);
 
     const pluginDetails: PluginDetails = {
       plugin,
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 396df65..3b27e16 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -149,7 +149,8 @@
 // 50. Remove ftrace filter state.
 // 51. Changed structure of FlamegraphState.expandedCallsiteByViewingOption.
 // 52. Update track group state - don't make the summary track the first track.
-export const STATE_VERSION = 52;
+// 53. Remove android log state.
+export const STATE_VERSION = 53;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -450,13 +451,6 @@
   pivotTable: PivotTableState;
 }
 
-export interface LogFilteringCriteria {
-  minimumLevel: number;
-  tags: string[];
-  textEntry: string;
-  hideNonMatching: boolean;
-}
-
 export interface PendingDeeplinkState {
   ts?: string;
   dur?: string;
@@ -505,7 +499,6 @@
   status: Status;
   selection: Selection;
   currentFlamegraphState: FlamegraphState | null;
-  logsPagination: Pagination;
   traceConversionInProgress: boolean;
 
   /**
@@ -556,9 +549,6 @@
   // be serialized at the moment, such as ES6 Set and Map.
   nonSerializableState: NonSerializableState;
 
-  // Android logs filtering state.
-  logFilteringCriteria: LogFilteringCriteria;
-
   // Omnibox info.
   omniboxState: OmniboxState;
 
diff --git a/ui/src/controller/logs_controller.ts b/ui/src/controller/logs_controller.ts
deleted file mode 100644
index 74077be..0000000
--- a/ui/src/controller/logs_controller.ts
+++ /dev/null
@@ -1,328 +0,0 @@
-// Copyright (C) 2019 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 {duration, Span, time, Time, TimeSpan} from '../base/time';
-import {
-  LogBounds,
-  LogBoundsKey,
-  LogEntries,
-  LogEntriesKey,
-  LogExistsKey,
-} from '../common/logs';
-import {LogFilteringCriteria} from '../common/state';
-import {globals} from '../frontend/globals';
-import {publishTrackData} from '../frontend/publish';
-import {Engine} from '../trace_processor/engine';
-import {LONG, LONG_NULL, NUM, STR} from '../trace_processor/query_result';
-import {escapeGlob, escapeQuery} from '../trace_processor/query_utils';
-
-import {Controller} from './controller';
-
-async function updateLogBounds(
-  engine: Engine,
-  span: Span<time, duration>,
-): Promise<LogBounds> {
-  const vizStartNs = span.start;
-  const vizEndNs = span.end;
-
-  const vizFilter = `ts between ${vizStartNs} and ${vizEndNs}`;
-
-  const result = await engine.query(`select
-      min(ts) as minTs,
-      max(ts) as maxTs,
-      min(case when ${vizFilter} then ts end) as minVizTs,
-      max(case when ${vizFilter} then ts end) as maxVizTs,
-      count(case when ${vizFilter} then ts end) as countTs
-    from filtered_logs`);
-
-  const data = result.firstRow({
-    minTs: LONG_NULL,
-    maxTs: LONG_NULL,
-    minVizTs: LONG_NULL,
-    maxVizTs: LONG_NULL,
-    countTs: NUM,
-  });
-
-  const firstLogTs = Time.fromRaw(data.minTs ?? 0n);
-  const lastLogTs = Time.fromRaw(data.maxTs ?? Time.MAX);
-
-  const bounds: LogBounds = {
-    firstLogTs,
-    lastLogTs,
-    firstVisibleLogTs: Time.fromRaw(data.minVizTs ?? firstLogTs),
-    lastVisibleLogTs: Time.fromRaw(data.maxVizTs ?? lastLogTs),
-    totalVisibleLogs: data.countTs,
-  };
-
-  return bounds;
-}
-
-async function updateLogEntries(
-  engine: Engine,
-  span: Span<time, duration>,
-  pagination: Pagination,
-): Promise<LogEntries> {
-  const vizStartNs = span.start;
-  const vizEndNs = span.end;
-  const vizSqlBounds = `ts >= ${vizStartNs} and ts <= ${vizEndNs}`;
-
-  const rowsResult = await engine.query(`
-        select
-          ts,
-          prio,
-          ifnull(tag, '[NULL]') as tag,
-          ifnull(msg, '[NULL]') as msg,
-          is_msg_highlighted as isMsgHighlighted,
-          is_process_highlighted as isProcessHighlighted,
-          ifnull(process_name, '') as processName
-        from filtered_logs
-        where ${vizSqlBounds}
-        order by ts
-        limit ${pagination.start}, ${pagination.count}
-    `);
-
-  const timestamps: time[] = [];
-  const priorities = [];
-  const tags = [];
-  const messages = [];
-  const isHighlighted = [];
-  const processName = [];
-
-  const it = rowsResult.iter({
-    ts: LONG,
-    prio: NUM,
-    tag: STR,
-    msg: STR,
-    isMsgHighlighted: NUM,
-    isProcessHighlighted: NUM,
-    processName: STR,
-  });
-  for (; it.valid(); it.next()) {
-    timestamps.push(Time.fromRaw(it.ts));
-    priorities.push(it.prio);
-    tags.push(it.tag);
-    messages.push(it.msg);
-    isHighlighted.push(
-      it.isMsgHighlighted === 1 || it.isProcessHighlighted === 1,
-    );
-    processName.push(it.processName);
-  }
-
-  return {
-    offset: pagination.start,
-    timestamps,
-    priorities,
-    tags,
-    messages,
-    isHighlighted,
-    processName,
-  };
-}
-
-class Pagination {
-  private _offset: number;
-  private _count: number;
-
-  constructor(offset: number, count: number) {
-    this._offset = offset;
-    this._count = count;
-  }
-
-  get start() {
-    return this._offset;
-  }
-
-  get count() {
-    return this._count;
-  }
-
-  get end() {
-    return this._offset + this._count;
-  }
-
-  contains(other: Pagination): boolean {
-    return this.start <= other.start && other.end <= this.end;
-  }
-
-  grow(n: number): Pagination {
-    const newStart = Math.max(0, this.start - n / 2);
-    const newCount = this.count + n;
-    return new Pagination(newStart, newCount);
-  }
-}
-
-export interface LogsControllerArgs {
-  engine: Engine;
-}
-
-/**
- * LogsController looks at three parts of the state:
- * 1. The visible trace window
- * 2. The requested offset and count the log lines to display
- * 3. The log filtering criteria.
- * And keeps two bits of published information up to date:
- * 1. The total number of log messages in visible range
- * 2. The logs lines that should be displayed
- * Based on the log filtering criteria, it also builds the filtered_logs view
- * and keeps it up to date.
- */
-export class LogsController extends Controller<'main'> {
-  private engine: Engine;
-  private span: Span<time, duration>;
-  private pagination: Pagination;
-  private hasLogs = false;
-  private logFilteringCriteria?: LogFilteringCriteria;
-  private requestingData = false;
-  private queuedRunRequest = false;
-
-  constructor(args: LogsControllerArgs) {
-    super('main');
-    this.engine = args.engine;
-    this.span = new TimeSpan(Time.ZERO, Time.fromSeconds(10));
-    this.pagination = new Pagination(0, 0);
-    this.hasAnyLogs().then((exists) => {
-      this.hasLogs = exists;
-      publishTrackData({
-        id: LogExistsKey,
-        data: {
-          exists,
-        },
-      });
-    });
-  }
-
-  async hasAnyLogs() {
-    const result = await this.engine.query(`
-      select count(*) as cnt from android_logs
-    `);
-    return result.firstRow({cnt: NUM}).cnt > 0;
-  }
-
-  run() {
-    if (!this.hasLogs) return;
-    if (this.requestingData) {
-      this.queuedRunRequest = true;
-      return;
-    }
-    this.requestingData = true;
-    this.updateLogTracks().finally(() => {
-      this.requestingData = false;
-      if (this.queuedRunRequest) {
-        this.queuedRunRequest = false;
-        this.run();
-      }
-    });
-  }
-
-  private async updateLogTracks() {
-    const newSpan = globals.stateVisibleTime();
-    const oldSpan = this.span;
-
-    const pagination = globals.state.logsPagination;
-    // This can occur when loading old traces.
-    // TODO(hjd): Fix the problem of accessing state from a previous version of
-    // the UI in a general way.
-    if (pagination === undefined) {
-      return;
-    }
-
-    const {offset, count} = pagination;
-    const requestedPagination = new Pagination(offset, count);
-    const oldPagination = this.pagination;
-
-    const newFilteringCriteria =
-      this.logFilteringCriteria !== globals.state.logFilteringCriteria;
-    const needBoundsUpdate = !oldSpan.equals(newSpan) || newFilteringCriteria;
-    const needEntriesUpdate =
-      !oldPagination.contains(requestedPagination) || needBoundsUpdate;
-
-    if (newFilteringCriteria) {
-      this.logFilteringCriteria = globals.state.logFilteringCriteria;
-      await this.engine.query('drop view if exists filtered_logs');
-
-      const globMatch = LogsController.composeGlobMatch(
-        this.logFilteringCriteria.hideNonMatching,
-        this.logFilteringCriteria.textEntry,
-      );
-      let selectedRows = `select prio, ts, tag, msg,
-          process.name as process_name, ${globMatch}
-          from android_logs
-          left join thread using(utid)
-          left join process using(upid)
-          where prio >= ${this.logFilteringCriteria.minimumLevel}`;
-      if (this.logFilteringCriteria.tags.length) {
-        selectedRows += ` and tag in (${LogsController.serializeTags(
-          this.logFilteringCriteria.tags,
-        )})`;
-      }
-
-      // We extract only the rows which will be visible.
-      await this.engine.query(`create view filtered_logs as select *
-        from (${selectedRows})
-        where is_msg_chosen is 1 or is_process_chosen is 1`);
-    }
-
-    if (needBoundsUpdate) {
-      this.span = newSpan;
-      const logBounds = await updateLogBounds(this.engine, newSpan);
-      publishTrackData({
-        id: LogBoundsKey,
-        data: logBounds,
-      });
-    }
-
-    if (needEntriesUpdate) {
-      this.pagination = requestedPagination.grow(100);
-      const logEntries = await updateLogEntries(
-        this.engine,
-        newSpan,
-        this.pagination,
-      );
-      publishTrackData({
-        id: LogEntriesKey,
-        data: logEntries,
-      });
-    }
-  }
-
-  private static serializeTags(tags: string[]) {
-    return tags.map((tag) => escapeQuery(tag)).join();
-  }
-
-  private static composeGlobMatch(isCollaped: boolean, textEntry: string) {
-    if (isCollaped) {
-      // If the entries are collapsed, we won't highlight any lines.
-      return `msg glob ${escapeGlob(textEntry)} as is_msg_chosen,
-        (process.name is not null and process.name glob ${escapeGlob(
-          textEntry,
-        )}) as is_process_chosen,
-        0 as is_msg_highlighted,
-        0 as is_process_highlighted`;
-    } else if (!textEntry) {
-      // If there is no text entry, we will show all lines, but won't highlight.
-      // any.
-      return `1 as is_msg_chosen,
-        1 as is_process_chosen,
-        0 as is_msg_highlighted,
-        0 as is_process_highlighted`;
-    } else {
-      return `1 as is_msg_chosen,
-        1 as is_process_chosen,
-        msg glob ${escapeGlob(textEntry)} as is_msg_highlighted,
-        (process.name is not null and process.name glob ${escapeGlob(
-          textEntry,
-        )}) as is_process_highlighted`;
-    }
-  }
-}
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index de8fde3..5194215 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -82,7 +82,6 @@
   FlowEventsControllerArgs,
 } from './flow_events_controller';
 import {LoadingManager} from './loading_manager';
-import {LogsController} from './logs_controller';
 import {
   PIVOT_TABLE_REDUX_FLAG,
   PivotTableController,
@@ -343,13 +342,6 @@
         );
 
         childControllers.push(
-          Child('logs', LogsController, {
-            engine,
-            app: globals,
-          }),
-        );
-
-        childControllers.push(
           Child('traceError', TraceErrorController, {engine}),
         );
 
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 3f942e9..d8f42fb 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -483,6 +483,7 @@
       new RegExp('^Trace Triggers$'),
       new RegExp('^Android App Startups$'),
       new RegExp('^Device State.*$'),
+      new RegExp('^Android logs$'),
     ];
 
     let groupUuid = undefined;
@@ -544,22 +545,6 @@
     }
   }
 
-  async addLogsTrack(engine: EngineProxy): Promise<void> {
-    const result = await engine.query(
-      `select count(1) as cnt from android_logs`,
-    );
-    const count = result.firstRow({cnt: NUM}).cnt;
-
-    if (count > 0) {
-      this.tracksToAdd.push({
-        uri: 'perfetto.AndroidLog',
-        name: 'Android logs',
-        trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-        trackGroup: SCROLLING_TRACK_GROUP,
-      });
-    }
-  }
-
   async addAnnotationTracks(engine: EngineProxy): Promise<void> {
     const sliceResult = await engine.query(`
       select id, name, upid, group_name
@@ -1652,7 +1637,6 @@
     await this.addThreadCpuSampleTracks(
       this.engine.getProxy('TrackDecider::addThreadCpuSampleTracks'),
     );
-    await this.addLogsTrack(this.engine.getProxy('TrackDecider::addLogsTrack'));
 
     // TODO(hjd): Move into plugin API.
     {
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index 98730e0..e87f38b 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -450,14 +450,14 @@
     return m(
       PopupMenu2,
       {
-        trigger: m(Button, {icon: 'show_chart', minimal: true}),
+        trigger: m(Button, {icon: 'show_chart', minimal: true, compact: true}),
       },
       this.getCounterContextMenuItems(),
     );
   }
 
   getTrackShellButtons(): m.Children {
-    return [this.getCounterContextMenu()];
+    return this.getCounterContextMenu();
   }
 
   async onCreate(): Promise<void> {
diff --git a/ui/src/frontend/close_track_button.ts b/ui/src/frontend/close_track_button.ts
index 0b3a46c..e7b667b 100644
--- a/ui/src/frontend/close_track_button.ts
+++ b/ui/src/frontend/close_track_button.ts
@@ -18,7 +18,7 @@
 import {Actions} from '../common/actions';
 
 import {globals} from './globals';
-import {TrackButton} from './track_panel';
+import {Button} from '../widgets/button';
 
 export interface CloseTrackButtonAttrs {
   trackKey: string;
@@ -28,13 +28,14 @@
   implements m.ClassComponent<CloseTrackButtonAttrs>
 {
   view({attrs}: m.CVnode<CloseTrackButtonAttrs>) {
-    return m(TrackButton, {
-      action: () => {
+    return m(Button, {
+      onclick: () => {
         globals.dispatch(Actions.removeTracks({trackKeys: [attrs.trackKey]}));
       },
-      i: Icons.Close,
-      tooltip: 'Close',
-      showButton: true,
+      icon: Icons.Close,
+      title: 'Close',
+      minimal: true,
+      compact: true,
     });
   }
 }
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 5cecd7e..8fc392d 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -673,6 +673,7 @@
   set isInternalUser(value: boolean) {
     localStorage.setItem('isInternalUser', value ? '1' : '0');
     this._isInternalUser = value;
+    raf.scheduleFullRedraw();
   }
 
   get testing() {
diff --git a/ui/src/frontend/logs_filters.ts b/ui/src/frontend/logs_filters.ts
deleted file mode 100644
index 93239e7..0000000
--- a/ui/src/frontend/logs_filters.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-// Copyright (C) 2022 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {Actions} from '../common/actions';
-import {Button} from '../widgets/button';
-import {Select} from '../widgets/select';
-import {TextInput} from '../widgets/text_input';
-
-import {globals} from './globals';
-
-export const LOG_PRIORITIES = [
-  '-',
-  '-',
-  'Verbose',
-  'Debug',
-  'Info',
-  'Warn',
-  'Error',
-  'Fatal',
-];
-const IGNORED_STATES = 2;
-
-interface LogPriorityWidgetAttrs {
-  options: string[];
-  selectedIndex: number;
-  onSelect: (id: number) => void;
-}
-
-interface LogTagChipAttrs {
-  name: string;
-  removeTag: (name: string) => void;
-}
-
-interface LogTagsWidgetAttrs {
-  tags: string[];
-}
-
-interface FilterByTextWidgetAttrs {
-  hideNonMatching: boolean;
-}
-
-class LogPriorityWidget implements m.ClassComponent<LogPriorityWidgetAttrs> {
-  view(vnode: m.Vnode<LogPriorityWidgetAttrs>) {
-    const attrs = vnode.attrs;
-    const optionComponents = [];
-    for (let i = IGNORED_STATES; i < attrs.options.length; i++) {
-      const selected = i === attrs.selectedIndex;
-      optionComponents.push(
-        m('option', {value: i, selected}, attrs.options[i]),
-      );
-    }
-    return m(
-      Select,
-      {
-        onchange: (e: Event) => {
-          const selectionValue = (e.target as HTMLSelectElement).value;
-          attrs.onSelect(Number(selectionValue));
-        },
-      },
-      optionComponents,
-    );
-  }
-}
-
-class LogTagChip implements m.ClassComponent<LogTagChipAttrs> {
-  view({attrs}: m.CVnode<LogTagChipAttrs>) {
-    return m(Button, {
-      label: attrs.name,
-      rightIcon: 'close',
-      onclick: () => attrs.removeTag(attrs.name),
-    });
-  }
-}
-
-class LogTagsWidget implements m.ClassComponent<LogTagsWidgetAttrs> {
-  removeTag(tag: string) {
-    globals.dispatch(Actions.removeLogTag({tag}));
-  }
-
-  view(vnode: m.Vnode<LogTagsWidgetAttrs>) {
-    const tags = vnode.attrs.tags;
-    return [
-      tags.map((tag) =>
-        m(LogTagChip, {
-          name: tag,
-          removeTag: this.removeTag.bind(this),
-        }),
-      ),
-      m(TextInput, {
-        placeholder: 'Filter by tag...',
-        onkeydown: (e: KeyboardEvent) => {
-          // This is to avoid zooming on 'w'(and other unexpected effects
-          // of key presses in this input field).
-          e.stopPropagation();
-          const htmlElement = e.target as HTMLInputElement;
-
-          // When the user clicks 'Backspace' we delete the previous tag.
-          if (
-            e.key === 'Backspace' &&
-            tags.length > 0 &&
-            htmlElement.value === ''
-          ) {
-            globals.dispatch(
-              Actions.removeLogTag({tag: tags[tags.length - 1]}),
-            );
-            return;
-          }
-
-          if (e.key !== 'Enter') {
-            return;
-          }
-          if (htmlElement.value === '') {
-            return;
-          }
-          globals.dispatch(Actions.addLogTag({tag: htmlElement.value.trim()}));
-          htmlElement.value = '';
-        },
-      }),
-    ];
-  }
-}
-
-class LogTextWidget implements m.ClassComponent {
-  view() {
-    return m(TextInput, {
-      placeholder: 'Search logs...',
-      onkeyup: (e: KeyboardEvent) => {
-        // We want to use the value of the input field after it has been
-        // updated with the latest key (onkeyup).
-        const htmlElement = e.target as HTMLInputElement;
-        globals.dispatch(
-          Actions.updateLogFilterText({textEntry: htmlElement.value}),
-        );
-      },
-    });
-  }
-}
-
-class FilterByTextWidget implements m.ClassComponent<FilterByTextWidgetAttrs> {
-  view({attrs}: m.Vnode<FilterByTextWidgetAttrs>) {
-    const icon = attrs.hideNonMatching ? 'unfold_less' : 'unfold_more';
-    const tooltip = attrs.hideNonMatching
-      ? 'Expand all and view highlighted'
-      : 'Collapse all';
-    return m(Button, {
-      icon,
-      title: tooltip,
-      disabled: globals.state.logFilteringCriteria.textEntry === '',
-      minimal: true,
-      onclick: () => globals.dispatch(Actions.toggleCollapseByTextEntry({})),
-    });
-  }
-}
-
-export class LogsFilters implements m.ClassComponent {
-  view(_: m.CVnode<{}>) {
-    return [
-      m('.log-label', 'Log Level'),
-      m(LogPriorityWidget, {
-        options: LOG_PRIORITIES,
-        selectedIndex: globals.state.logFilteringCriteria.minimumLevel,
-        onSelect: (minimumLevel) => {
-          globals.dispatch(Actions.setMinimumLogLevel({minimumLevel}));
-        },
-      }),
-      m(LogTagsWidget, {tags: globals.state.logFilteringCriteria.tags}),
-      m(LogTextWidget),
-      m(FilterByTextWidget, {
-        hideNonMatching: globals.state.logFilteringCriteria.hideNonMatching,
-      }),
-    ];
-  }
-}
diff --git a/ui/src/frontend/logs_panel.ts b/ui/src/frontend/logs_panel.ts
deleted file mode 100644
index 18165a6..0000000
--- a/ui/src/frontend/logs_panel.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {time, Time} from '../base/time';
-import {Actions} from '../common/actions';
-import {HighPrecisionTimeSpan} from '../common/high_precision_time';
-import {
-  LogBounds,
-  LogBoundsKey,
-  LogEntries,
-  LogEntriesKey,
-} from '../common/logs';
-import {raf} from '../core/raf_scheduler';
-import {DetailsShell} from '../widgets/details_shell';
-import {VirtualScrollContainer} from '../widgets/virtual_scroll_container';
-
-import {SELECTED_LOG_ROWS_COLOR} from './css_constants';
-import {globals} from './globals';
-import {LOG_PRIORITIES, LogsFilters} from './logs_filters';
-import {Timestamp} from './widgets/timestamp';
-
-const ROW_H = 20;
-
-export class LogPanel implements m.ClassComponent {
-  private bounds?: LogBounds;
-  private entries?: LogEntries;
-
-  private visibleRowOffset = 0;
-  private visibleRowCount = 0;
-
-  recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) {
-    const prevOffset = this.visibleRowOffset;
-    const prevCount = this.visibleRowCount;
-    this.visibleRowOffset = Math.floor(scrollContainer.scrollTop / ROW_H);
-    this.visibleRowCount = Math.ceil(scrollContainer.clientHeight / ROW_H);
-
-    if (
-      this.visibleRowOffset !== prevOffset ||
-      this.visibleRowCount !== prevCount
-    ) {
-      globals.dispatch(
-        Actions.updateLogsPagination({
-          offset: this.visibleRowOffset,
-          count: this.visibleRowCount,
-        }),
-      );
-    }
-  }
-
-  oncreate(_: m.CVnodeDOM) {
-    // TODO(stevegolton): Type assersions are a source of bugs.
-    // Let's try to find another way of doing this.
-    this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
-    this.entries = globals.trackDataStore.get(LogEntriesKey) as LogEntries;
-  }
-
-  onbeforeupdate(_: m.CVnode) {
-    // TODO(stevegolton): Type assersions are a source of bugs.
-    // Let's try to find another way of doing this.
-    this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
-    this.entries = globals.trackDataStore.get(LogEntriesKey) as LogEntries;
-  }
-
-  onScroll = (scrollContainer: HTMLElement) => {
-    this.recomputeVisibleRowsAndUpdate(scrollContainer);
-    raf.scheduleFullRedraw();
-  };
-
-  onRowOver(ts: time) {
-    globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
-  }
-
-  onRowOut() {
-    globals.dispatch(Actions.setHoverCursorTimestamp({ts: Time.INVALID}));
-  }
-
-  private totalRows(): {
-    isStale: boolean;
-    total: number;
-    offset: number;
-    count: number;
-  } {
-    if (!this.bounds) {
-      return {isStale: false, total: 0, offset: 0, count: 0};
-    }
-    const {totalVisibleLogs, firstVisibleLogTs, lastVisibleLogTs} = this.bounds;
-    const vis = globals.timeline.visibleWindowTime;
-
-    const visibleLogSpan = new HighPrecisionTimeSpan(
-      firstVisibleLogTs,
-      lastVisibleLogTs,
-    );
-    const isStale = !vis.contains(visibleLogSpan);
-    const offset = Math.min(this.visibleRowOffset, totalVisibleLogs);
-    const visCount = Math.min(totalVisibleLogs - offset, this.visibleRowCount);
-    return {isStale, total: totalVisibleLogs, count: visCount, offset};
-  }
-
-  view(_: m.CVnode<{}>) {
-    const {isStale, total, offset, count} = this.totalRows();
-
-    const hasProcessNames =
-      this.entries &&
-      this.entries.processName.filter((name) => name).length > 0;
-
-    const rows: m.Children = [];
-    rows.push(
-      m(
-        `.row`,
-        m('.cell.row-header', 'Timestamp'),
-        m('.cell.row-header', 'Level'),
-        m('.cell.row-header', 'Tag'),
-        hasProcessNames
-          ? m('.cell.with-process.row-header', 'Process name')
-          : undefined,
-        hasProcessNames
-          ? m('.cell.with-process.row-header', 'Message')
-          : m('.cell.no-process.row-header', 'Message'),
-        m('br'),
-      ),
-    );
-    if (this.entries) {
-      const offset = this.entries.offset;
-      const timestamps = this.entries.timestamps;
-      const priorities = this.entries.priorities;
-      const tags = this.entries.tags;
-      const messages = this.entries.messages;
-      const processNames = this.entries.processName;
-      for (let i = 0; i < this.entries.timestamps.length; i++) {
-        const priorityLetter = LOG_PRIORITIES[priorities[i]][0];
-        const ts = timestamps[i];
-        const prioClass = priorityLetter || '';
-        const style: {top: string; backgroundColor?: string} = {
-          // 1.5 is for the width of the header
-          top: `${(offset + i + 1.5) * ROW_H}px`,
-        };
-        if (this.entries.isHighlighted[i]) {
-          style.backgroundColor = SELECTED_LOG_ROWS_COLOR;
-        }
-
-        rows.push(
-          m(
-            `.row.${prioClass}`,
-            {
-              class: isStale ? 'stale' : '',
-              style,
-              onmouseover: this.onRowOver.bind(this, ts),
-              onmouseout: this.onRowOut.bind(this),
-            },
-            m('.cell', m(Timestamp, {ts})),
-            m('.cell', priorityLetter || '?'),
-            m('.cell', tags[i]),
-            hasProcessNames
-              ? m('.cell.with-process', processNames[i])
-              : undefined,
-            hasProcessNames
-              ? m('.cell.with-process', messages[i])
-              : m('.cell.no-process', messages[i]),
-            m('br'),
-          ),
-        );
-      }
-    }
-
-    // TODO(stevegolton): Add a 'loading' state to DetailsShell, which shows a
-    // scrolling scrolly bar at the bottom of the banner & map isStale to it
-    return m(
-      DetailsShell,
-      {
-        title: 'Android Logs',
-        description: `[${offset}, ${offset + count}] / ${total}`,
-        buttons: m(LogsFilters),
-      },
-      m(
-        VirtualScrollContainer,
-        {onScroll: this.onScroll},
-        m(
-          '.log-panel',
-          m('.rows', {style: {height: `${total * ROW_H}px`}}, rows),
-        ),
-      ),
-    );
-  }
-}
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 69362e45..85cbe07 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -21,7 +21,7 @@
 import {randomColor} from '../core/colorizer';
 import {AreaNote, Note, getLegacySelection} from '../common/state';
 import {raf} from '../core/raf_scheduler';
-import {Button} from '../widgets/button';
+import {Button, ButtonBar} from '../widgets/button';
 
 import {BottomTab, NewBottomTabArgs} from './bottom_tab';
 import {TRACK_SHELL_WIDTH} from './css_constants';
@@ -89,46 +89,39 @@
           globals.dispatch(Actions.setHoveredNoteTimestamp({ts: Time.INVALID}));
         },
       },
-      isTraceLoaded()
-        ? [
-            m(
-              'button',
-              {
-                onclick: (e: Event) => {
-                  e.preventDefault();
-                  if (allCollapsed) {
-                    globals.commandManager.runCommand(
-                      'dev.perfetto.CoreCommands#ExpandAllGroups',
-                    );
-                  } else {
-                    globals.commandManager.runCommand(
-                      'dev.perfetto.CoreCommands#CollapseAllGroups',
-                    );
-                  }
-                },
-              },
-              m(
-                'i.material-icons',
-                {title: allCollapsed ? 'Expand all' : 'Collapse all'},
-                allCollapsed ? 'unfold_more' : 'unfold_less',
-              ),
-            ),
-            m(
-              'button',
-              {
-                onclick: (e: Event) => {
-                  e.preventDefault();
-                  globals.dispatch(Actions.clearAllPinnedTracks({}));
-                },
-              },
-              m(
-                'i.material-icons',
-                {title: 'Clear all pinned tracks'},
-                'clear_all',
-              ),
-            ),
-          ]
-        : '',
+      isTraceLoaded() &&
+        m(
+          ButtonBar,
+          {className: 'pf-toolbar'},
+          m(Button, {
+            onclick: (e: Event) => {
+              e.preventDefault();
+              if (allCollapsed) {
+                globals.commandManager.runCommand(
+                  'dev.perfetto.CoreCommands#ExpandAllGroups',
+                );
+              } else {
+                globals.commandManager.runCommand(
+                  'dev.perfetto.CoreCommands#CollapseAllGroups',
+                );
+              }
+            },
+            title: allCollapsed ? 'Expand all' : 'Collapse all',
+            icon: allCollapsed ? 'unfold_more' : 'unfold_less',
+            minimal: true,
+            compact: true,
+          }),
+          m(Button, {
+            onclick: (e: Event) => {
+              e.preventDefault();
+              globals.dispatch(Actions.clearAllPinnedTracks({}));
+            },
+            title: 'Clear all pinned tracks',
+            icon: 'clear_all',
+            minimal: true,
+            compact: true,
+          }),
+        ),
     );
   }
 
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 517ae4b..9214430 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -270,7 +270,7 @@
   renderTree(node: PanelOrGroup, path: string): m.Vnode {
     if (node.kind === 'group') {
       return m(
-        'div',
+        'div.pf-panel-group',
         {key: path},
         this.renderPanel(
           node.header,
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index 46773b2..f8c36cb 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -16,12 +16,6 @@
 import {Actions} from '../common/actions';
 import {AggregateData} from '../common/aggregation_data';
 import {ConversionJobStatusUpdate} from '../common/conversion_jobs';
-import {
-  LogBoundsKey,
-  LogEntriesKey,
-  LogExists,
-  LogExistsKey,
-} from '../common/logs';
 import {MetricResult} from '../common/metric_data';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
 import {raf} from '../core/raf_scheduler';
@@ -64,13 +58,7 @@
 
 export function publishTrackData(args: {id: string; data: {}}) {
   globals.setTrackData(args.id, args.data);
-  if ([LogExistsKey, LogBoundsKey, LogEntriesKey].includes(args.id)) {
-    const trackDataStore = globals.trackDataStore;
-    const data = trackDataStore.get(LogExistsKey) as LogExists | undefined;
-    if (data && data.exists) raf.scheduleFullRedraw();
-  } else {
-    raf.scheduleRedraw();
-  }
+  raf.scheduleRedraw();
 }
 
 export function publishMetricResult(metricResult: MetricResult) {
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 05ca28f..be1d20e 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -128,158 +128,160 @@
   appendOpenedTraceTitle?: boolean;
 }
 
-const SECTIONS: Section[] = [
-  {
-    title: 'Navigation',
-    summary: 'Open or record a new trace',
-    expanded: true,
-    items: [
-      {t: 'Open trace file', a: popupFileSelectionDialog, i: 'folder_open'},
-      {
-        t: 'Open with legacy UI',
-        a: popupFileSelectionDialogOldUI,
-        i: 'filter_none',
-      },
-      {t: 'Record new trace', a: navigateRecord, i: 'fiber_smart_record'},
-      {
-        t: 'Widgets',
-        a: navigateWidgets,
-        i: 'widgets',
-        isVisible: () => WIDGETS_PAGE_IN_NAV_FLAG.get(),
-      },
-      {
-        t: 'Plugins',
-        a: navigatePlugins,
-        i: 'extension',
-        isVisible: () => PLUGINS_PAGE_IN_NAV_FLAG.get(),
-      },
-    ],
-  },
+function getSections(): Section[] {
+  return [
+    {
+      title: 'Navigation',
+      summary: 'Open or record a new trace',
+      expanded: true,
+      items: [
+        {t: 'Open trace file', a: popupFileSelectionDialog, i: 'folder_open'},
+        {
+          t: 'Open with legacy UI',
+          a: popupFileSelectionDialogOldUI,
+          i: 'filter_none',
+        },
+        {t: 'Record new trace', a: navigateRecord, i: 'fiber_smart_record'},
+        {
+          t: 'Widgets',
+          a: navigateWidgets,
+          i: 'widgets',
+          isVisible: () => WIDGETS_PAGE_IN_NAV_FLAG.get(),
+        },
+        {
+          t: 'Plugins',
+          a: navigatePlugins,
+          i: 'extension',
+          isVisible: () => PLUGINS_PAGE_IN_NAV_FLAG.get(),
+        },
+      ],
+    },
 
-  {
-    title: 'Current Trace',
-    summary: 'Actions on the current trace',
-    expanded: true,
-    hideIfNoTraceLoaded: true,
-    appendOpenedTraceTitle: true,
-    items: [
-      {t: 'Show timeline', a: navigateViewer, i: 'line_style'},
-      {
-        t: 'Share',
-        a: handleShareTrace,
-        i: 'share',
-        internalUserOnly: true,
-        isPending: () =>
-          globals.getConversionJobStatus('create_permalink') ===
-          ConversionJobStatus.InProgress,
-      },
-      {
-        t: 'Download',
-        a: downloadTrace,
-        i: 'file_download',
-        checkDownloadDisabled: true,
-      },
-      {t: 'Query (SQL)', a: navigateQuery, i: 'database'},
-      {
-        t: 'Insights',
-        a: navigateInsights,
-        i: 'insights',
-        isVisible: () => INSIGHTS_PAGE_IN_NAV_FLAG.get(),
-      },
-      {
-        t: 'Viz',
-        a: navigateViz,
-        i: 'area_chart',
-        isVisible: () => VIZ_PAGE_IN_NAV_FLAG.get(),
-      },
-      {t: 'Metrics', a: navigateMetrics, i: 'speed'},
-      {t: 'Info and stats', a: navigateInfo, i: 'info'},
-    ],
-  },
+    {
+      title: 'Current Trace',
+      summary: 'Actions on the current trace',
+      expanded: true,
+      hideIfNoTraceLoaded: true,
+      appendOpenedTraceTitle: true,
+      items: [
+        {t: 'Show timeline', a: navigateViewer, i: 'line_style'},
+        {
+          t: 'Share',
+          a: handleShareTrace,
+          i: 'share',
+          internalUserOnly: true,
+          isPending: () =>
+            globals.getConversionJobStatus('create_permalink') ===
+            ConversionJobStatus.InProgress,
+        },
+        {
+          t: 'Download',
+          a: downloadTrace,
+          i: 'file_download',
+          checkDownloadDisabled: true,
+        },
+        {t: 'Query (SQL)', a: navigateQuery, i: 'database'},
+        {
+          t: 'Insights',
+          a: navigateInsights,
+          i: 'insights',
+          isVisible: () => INSIGHTS_PAGE_IN_NAV_FLAG.get(),
+        },
+        {
+          t: 'Viz',
+          a: navigateViz,
+          i: 'area_chart',
+          isVisible: () => VIZ_PAGE_IN_NAV_FLAG.get(),
+        },
+        {t: 'Metrics', a: navigateMetrics, i: 'speed'},
+        {t: 'Info and stats', a: navigateInfo, i: 'info'},
+      ],
+    },
 
-  {
-    title: 'Convert trace',
-    summary: 'Convert to other formats',
-    expanded: true,
-    hideIfNoTraceLoaded: true,
-    items: [
-      {
-        t: 'Switch to legacy UI',
-        a: openCurrentTraceWithOldUI,
-        i: 'filter_none',
-        isPending: () =>
-          globals.getConversionJobStatus('open_in_legacy') ===
-          ConversionJobStatus.InProgress,
-      },
-      {
-        t: 'Convert to .json',
-        a: convertTraceToJson,
-        i: 'file_download',
-        isPending: () =>
-          globals.getConversionJobStatus('convert_json') ===
-          ConversionJobStatus.InProgress,
-        checkDownloadDisabled: true,
-      },
+    {
+      title: 'Convert trace',
+      summary: 'Convert to other formats',
+      expanded: true,
+      hideIfNoTraceLoaded: true,
+      items: [
+        {
+          t: 'Switch to legacy UI',
+          a: openCurrentTraceWithOldUI,
+          i: 'filter_none',
+          isPending: () =>
+            globals.getConversionJobStatus('open_in_legacy') ===
+            ConversionJobStatus.InProgress,
+        },
+        {
+          t: 'Convert to .json',
+          a: convertTraceToJson,
+          i: 'file_download',
+          isPending: () =>
+            globals.getConversionJobStatus('convert_json') ===
+            ConversionJobStatus.InProgress,
+          checkDownloadDisabled: true,
+        },
 
-      {
-        t: 'Convert to .systrace',
-        a: convertTraceToSystrace,
-        i: 'file_download',
-        isVisible: () => globals.hasFtrace,
-        isPending: () =>
-          globals.getConversionJobStatus('convert_systrace') ===
-          ConversionJobStatus.InProgress,
-        checkDownloadDisabled: true,
-      },
-    ],
-  },
+        {
+          t: 'Convert to .systrace',
+          a: convertTraceToSystrace,
+          i: 'file_download',
+          isVisible: () => globals.hasFtrace,
+          isPending: () =>
+            globals.getConversionJobStatus('convert_systrace') ===
+            ConversionJobStatus.InProgress,
+          checkDownloadDisabled: true,
+        },
+      ],
+    },
 
-  {
-    title: 'Example Traces',
-    expanded: true,
-    summary: 'Open an example trace',
-    items: [
-      {
-        t: 'Open Android example',
-        a: openTraceUrl(EXAMPLE_ANDROID_TRACE_URL),
-        i: 'description',
-      },
-      {
-        t: 'Open Chrome example',
-        a: openTraceUrl(EXAMPLE_CHROME_TRACE_URL),
-        i: 'description',
-      },
-    ],
-  },
+    {
+      title: 'Example Traces',
+      expanded: true,
+      summary: 'Open an example trace',
+      items: [
+        {
+          t: 'Open Android example',
+          a: openTraceUrl(EXAMPLE_ANDROID_TRACE_URL),
+          i: 'description',
+        },
+        {
+          t: 'Open Chrome example',
+          a: openTraceUrl(EXAMPLE_CHROME_TRACE_URL),
+          i: 'description',
+        },
+      ],
+    },
 
-  {
-    title: 'Support',
-    expanded: true,
-    summary: 'Documentation & Bugs',
-    items: [
-      {t: 'Keyboard shortcuts', a: openHelp, i: 'help'},
-      {t: 'Documentation', a: 'https://perfetto.dev/docs', i: 'find_in_page'},
-      {t: 'Flags', a: navigateFlags, i: 'emoji_flags'},
-      {
-        t: 'Report a bug',
-        a: () => window.open(getBugReportUrl()),
-        i: 'bug_report',
-      },
-      {
-        t: 'Record metatrace',
-        a: recordMetatrace,
-        i: 'fiber_smart_record',
-        checkMetatracingDisabled: true,
-      },
-      {
-        t: 'Finalise metatrace',
-        a: finaliseMetatrace,
-        i: 'file_download',
-        checkMetatracingEnabled: true,
-      },
-    ],
-  },
-];
+    {
+      title: 'Support',
+      expanded: true,
+      summary: 'Documentation & Bugs',
+      items: [
+        {t: 'Keyboard shortcuts', a: openHelp, i: 'help'},
+        {t: 'Documentation', a: 'https://perfetto.dev/docs', i: 'find_in_page'},
+        {t: 'Flags', a: navigateFlags, i: 'emoji_flags'},
+        {
+          t: 'Report a bug',
+          a: getBugReportUrl(),
+          i: 'bug_report',
+        },
+        {
+          t: 'Record metatrace',
+          a: recordMetatrace,
+          i: 'fiber_smart_record',
+          checkMetatracingDisabled: true,
+        },
+        {
+          t: 'Finalise metatrace',
+          a: finaliseMetatrace,
+          i: 'file_download',
+          checkMetatracingEnabled: true,
+        },
+      ],
+    },
+  ];
+}
 
 function openHelp(e: Event) {
   e.preventDefault();
@@ -812,7 +814,7 @@
   view() {
     if (globals.hideSidebar) return null;
     const vdomSections = [];
-    for (const section of SECTIONS) {
+    for (const section of getSections()) {
       if (section.hideIfNoTraceLoaded && !isTraceLoaded()) continue;
       const vdomItems = [];
       for (const item of section.items) {
diff --git a/ui/src/frontend/simple_counter_track.ts b/ui/src/frontend/simple_counter_track.ts
index 78aa012..084c14f 100644
--- a/ui/src/frontend/simple_counter_track.ts
+++ b/ui/src/frontend/simple_counter_track.ts
@@ -52,7 +52,7 @@
   }
 
   getTrackShellButtons(): m.Children {
-    return [this.getCounterContextMenu()];
+    return this.getCounterContextMenu();
   }
 
   getSqlSource(): string {
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 0f0f401..1e35329 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -35,6 +35,7 @@
   TrackContent,
 } from './track_panel';
 import {canvasClip} from '../common/canvas_utils';
+import {Button} from '../widgets/button';
 
 interface Attrs {
   trackGroupId: string;
@@ -133,24 +134,26 @@
           m('h1.track-title', {title: name}, name, renderChips(tags)),
           collapsed && child !== null ? m('h2.track-subtitle', child) : null,
         ),
-        error && m(CrashButton, {error}),
-        selection && selection.kind === 'AREA'
-          ? m(
-              'i.material-icons.track-button',
-              {
-                onclick: (e: MouseEvent) => {
-                  globals.dispatch(
-                    Actions.toggleTrackSelection({
-                      id: trackGroupId,
-                      isTrackGroup: true,
-                    }),
-                  );
-                  e.stopPropagation();
-                },
+        m(
+          '.track-buttons',
+          error && m(CrashButton, {error}),
+          selection &&
+            selection.kind === 'AREA' &&
+            m(Button, {
+              onclick: (e: MouseEvent) => {
+                globals.dispatch(
+                  Actions.toggleTrackSelection({
+                    id: trackGroupId,
+                    isTrackGroup: true,
+                  }),
+                );
+                e.stopPropagation();
               },
-              checkBox,
-            )
-          : '',
+              icon: checkBox,
+              minimal: true,
+              compact: true,
+            }),
+        ),
       ),
       trackFSM
         ? m(
@@ -158,6 +161,7 @@
             {
               track: trackFSM.track,
               hasError: Boolean(trackFSM.getError()),
+              height: this.attrs.trackFSM?.track.getHeight(),
             },
             !collapsed && child !== null ? m('span', child) : null,
           )
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 4240ef4..59c9857 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -32,12 +32,13 @@
 import {verticalScrollToTrack} from './scroll_helper';
 import {drawVerticalLineAtTime} from './vertical_line_helper';
 import {classNames} from '../base/classnames';
-import {Button} from '../widgets/button';
+import {Button, ButtonBar} from '../widgets/button';
 import {Popup} from '../widgets/popup';
 import {canvasClip} from '../common/canvas_utils';
 import {TimeScale} from './time_scale';
 import {getLegacySelection} from '../common/state';
 import {CloseTrackButton} from './close_track_button';
+import {exists} from '../base/utils';
 
 function getTitleSize(title: string): string | undefined {
   const length = title.length;
@@ -146,6 +147,7 @@
     }
 
     const currentSelection = getLegacySelection(globals.state);
+    const pinned = isPinned(attrs.trackKey);
 
     return m(
       `.track-shell[draggable=true]`,
@@ -162,51 +164,57 @@
         ondrop: (e: DragEvent) => this.ondrop(e, attrs.trackKey),
       },
       m(
-        'h1',
-        {
-          title: attrs.title,
-          style: {
-            'font-size': getTitleSize(attrs.title),
+        '.track-menubar',
+        m(
+          'h1',
+          {
+            title: attrs.title,
+            style: {
+              'font-size': getTitleSize(attrs.title),
+            },
           },
-        },
-        attrs.title,
-        renderChips(attrs.tags),
-      ),
-      m(
-        '.track-buttons',
-        attrs.buttons,
-        m(TrackButton, {
-          action: () => {
-            globals.dispatch(
-              Actions.toggleTrackPinned({trackKey: attrs.trackKey}),
-            );
-          },
-          i: Icons.Pin,
-          filledIcon: isPinned(attrs.trackKey),
-          tooltip: isPinned(attrs.trackKey) ? 'Unpin' : 'Pin to top',
-          showButton: isPinned(attrs.trackKey),
-          fullHeight: true,
-        }),
-        currentSelection !== null && currentSelection.kind === 'AREA'
-          ? m(TrackButton, {
-              action: (e: MouseEvent) => {
-                globals.dispatch(
-                  Actions.toggleTrackSelection({
-                    id: attrs.trackKey,
-                    isTrackGroup: false,
-                  }),
-                );
-                e.stopPropagation();
-              },
-              i: isSelected(attrs.trackKey)
-                ? Icons.Checkbox
-                : Icons.BlankCheckbox,
-              tooltip: isSelected(attrs.trackKey)
-                ? 'Remove track'
-                : 'Add track to selection',
-              showButton: true,
-            })
-          : '',
+          attrs.title,
+          renderChips(attrs.tags),
+        ),
+        m(
+          ButtonBar,
+          {className: 'track-buttons'},
+          attrs.buttons,
+          m(Button, {
+            className: classNames(!pinned && 'pf-visible-on-hover'),
+            onclick: () => {
+              globals.dispatch(
+                Actions.toggleTrackPinned({trackKey: attrs.trackKey}),
+              );
+            },
+            icon: Icons.Pin,
+            iconFilled: pinned,
+            title: pinned ? 'Unpin' : 'Pin to top',
+            minimal: true,
+            compact: true,
+          }),
+          currentSelection !== null && currentSelection.kind === 'AREA'
+            ? m(Button, {
+                onclick: (e: MouseEvent) => {
+                  globals.dispatch(
+                    Actions.toggleTrackSelection({
+                      id: attrs.trackKey,
+                      isTrackGroup: false,
+                    }),
+                  );
+                  e.stopPropagation();
+                },
+                minimal: true,
+                compact: true,
+                icon: isSelected(attrs.trackKey)
+                  ? Icons.Checkbox
+                  : Icons.BlankCheckbox,
+                title: isSelected(attrs.trackKey)
+                  ? 'Remove track'
+                  : 'Add track to selection',
+              })
+            : '',
+        ),
       ),
     );
   }
@@ -264,6 +272,7 @@
 export interface TrackContentAttrs {
   track: Track;
   hasError?: boolean;
+  height?: number;
 }
 export class TrackContent implements m.ClassComponent<TrackContentAttrs> {
   private mouseDownX?: number;
@@ -275,6 +284,9 @@
     return m(
       '.track-content',
       {
+        style: exists(attrs.height) && {
+          height: `${attrs.height}px`,
+        },
         className: classNames(attrs.hasError && 'pf-track-content-error'),
         onmousemove: (e: MouseEvent) => {
           attrs.track.onMouseMove?.(currentTargetOffset(e));
@@ -372,6 +384,7 @@
           m(TrackContent, {
             track: attrs.track,
             hasError: Boolean(attrs.error),
+            height: attrs.heightPx,
           }),
       ],
     );
@@ -395,34 +408,6 @@
   }
 }
 
-export interface TrackButtonAttrs {
-  action: (e: MouseEvent) => void;
-  i: string;
-  tooltip: string;
-  showButton: boolean;
-  fullHeight?: boolean;
-  filledIcon?: boolean;
-}
-export class TrackButton implements m.ClassComponent<TrackButtonAttrs> {
-  view({attrs}: m.CVnode<TrackButtonAttrs>) {
-    return m(
-      'i.track-button',
-      {
-        class: [
-          attrs.showButton ? 'show' : '',
-          attrs.fullHeight ? 'full-height' : '',
-          attrs.filledIcon ? 'material-icons-filled' : 'material-icons',
-        ]
-          .filter(Boolean)
-          .join(' '),
-        onclick: attrs.action,
-        title: attrs.tooltip,
-      },
-      attrs.i,
-    );
-  }
-}
-
 interface TrackPanelAttrs {
   trackKey: string;
   title: string;
diff --git a/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts b/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts
index e7ddcb1..ffda5a3 100644
--- a/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts
@@ -12,12 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  MetricVisualisation,
-  Plugin,
-  PluginContext,
-  PluginDescriptor,
-} from '../../public';
+import {MetricVisualisation, Plugin, PluginDescriptor} from '../../public';
 
 const SPEC = `
 {
@@ -37,10 +32,6 @@
 `;
 
 class AndroidBinderVizPlugin implements Plugin {
-  onActivate(_: PluginContext): void {
-    //
-  }
-
   metricVisualisations(): MetricVisualisation[] {
     return [
       {
diff --git a/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts b/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
index 5ebbdcb..d60fdb7 100644
--- a/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
@@ -16,7 +16,6 @@
   NUM,
   NUM_NULL,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   STR,
@@ -24,8 +23,6 @@
 import {addDebugSliceTrack} from '../../public';
 
 class AndroidClientServer implements Plugin {
-  onActivate(_: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     ctx.registerCommand({
       id: 'dev.perfetto.AndroidClientServer#ThreadRuntimeIPC',
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index 0d60ccf..5fb1a1a 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -14,12 +14,7 @@
 
 import {runQuery} from '../../common/queries';
 import {addDebugSliceTrack} from '../../public';
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 
 const JANK_CUJ_QUERY_PRECONDITIONS = `
   SELECT RUN_METRIC('android/android_jank_cuj.sql');
@@ -131,8 +126,6 @@
 
 const LATENCY_COLUMNS = ['name', 'dur_ms', 'ts', 'dur', 'track_id', 'slice_id'];
 class AndroidCujs implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     ctx.registerCommand({
       id: 'dev.perfetto.AndroidCujs#PinJankCUJs',
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index c022dba..414a662 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -12,12 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {EngineProxy} from '../../trace_processor/engine';
 import {
   SimpleSliceTrack,
@@ -1086,8 +1081,6 @@
 `;
 
 class AndroidLongBatteryTracing implements Plugin {
-  onActivate(_: PluginContext): void {}
-
   addSliceTrack(
     ctx: PluginContextTrace,
     name: string,
diff --git a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
index 419c356..0eb7e0f 100644
--- a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
@@ -12,18 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {EngineProxy} from '../../trace_processor/engine';
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {addDebugSliceTrack} from '../../public';
+import {EngineProxy} from '../../trace_processor/engine';
 
 class AndroidNetwork implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   // Adds a debug track using the provided query and given columns. The columns
   // must be start with ts, dur, and a name column. The name column and all
   // following columns are shown as arguments in slice details.
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
index a221db6..a53ff20 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
@@ -15,15 +15,12 @@
 import {
   addDebugSliceTrack,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
 import {EngineProxy} from '../../trace_processor/engine';
 
 class AndroidPerf implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async addAppProcessStartsDebugTrack(
     engine: EngineProxy,
     reason: string,
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
index e7d6639..2ec9223 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -12,12 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {addDebugSliceTrack} from '../../public';
 import {runQuery} from '../../common/queries';
 
@@ -31,8 +26,6 @@
 `;
 
 class AndroidPerfTraceCounters implements Plugin {
-  onActivate(_: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const resp = await runQuery(PERF_TRACE_COUNTERS_PRECONDITION, ctx.engine);
     if (resp.totalRowCount === 0) return;
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
index ebb10b8..4092621 100644
--- a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
@@ -12,21 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  LONG,
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {LONG, Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {
   SimpleSliceTrack,
   SimpleSliceTrackConfig,
 } from '../../frontend/simple_slice_track';
 
 class AndroidStartup implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const e = ctx.engine;
     await e.query(`include perfetto module android.startup.startups;`);
diff --git a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts b/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
index 6e5d474..8dc6a6f 100644
--- a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
+++ b/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
@@ -88,7 +88,7 @@
 order by total_self_size desc
 limit 100;`;
 
-const coreCommands: Plugin = {
+class CoreCommandsPlugin implements Plugin {
   onActivate(ctx: PluginContext) {
     ctx.registerCommand({
       id: 'dev.perfetto.CoreCommands#ToggleLeftSidebar',
@@ -102,7 +102,7 @@
       },
       defaultHotkey: '!Mod+B',
     });
-  },
+  }
 
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     ctx.registerCommand({
@@ -249,8 +249,8 @@
         ctx.tabs.showTab('current_selection');
       },
     });
-  },
-};
+  }
+}
 
 function promptForTimestamp(message: string): time | undefined {
   const tsStr = window.prompt(message);
@@ -266,5 +266,5 @@
 
 export const plugin: PluginDescriptor = {
   pluginId: 'dev.perfetto.CoreCommands',
-  plugin: coreCommands,
+  plugin: CoreCommandsPlugin,
 };
diff --git a/ui/src/plugins/dev.perfetto.ExampleState/index.ts b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
index 0f3c584..41e2562 100644
--- a/ui/src/plugins/dev.perfetto.ExampleState/index.ts
+++ b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
@@ -15,7 +15,6 @@
 import {
   createStore,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   Store,
@@ -43,10 +42,6 @@
     }
   }
 
-  onActivate(_: PluginContext): void {
-    //
-  }
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     this.store = ctx.mountStore((init: unknown) => this.migrate(init));
 
diff --git a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
index 3ccc12c..9bd57d3 100644
--- a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
@@ -12,16 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 
 class LargeScreensPerf implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     ctx.registerCommand({
       id: 'dev.perfetto.LargeScreensPerf#PinUnfoldLatencyTracks',
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
index e9bf9b0..d7a37d8 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
@@ -15,7 +15,6 @@
 import {
   NUM,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   STR_NULL,
@@ -26,8 +25,6 @@
 // This plugin renders visualizations of runtime power state transitions for
 // Linux kernel devices (devices managed by Linux drivers).
 class LinuxKernelDevices implements Plugin {
-  onActivate(_: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
       with
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 97cee3c..e51d5d5 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -15,7 +15,7 @@
 import m from 'mithril';
 
 import {Hotkey} from '../base/hotkeys';
-import {duration, time} from '../base/time';
+import {Span, duration, time} from '../base/time';
 import {Migrate, Store} from '../base/store';
 import {ColorScheme} from '../core/colorizer';
 import {LegacySelection} from '../common/state';
@@ -376,6 +376,12 @@
 
     // Bring a timestamp into view.
     panToTimestamp(ts: time): void;
+
+    // Move the viewport
+    setViewportTime(start: time, end: time): void;
+
+    // A span representing the current viewport location
+    readonly viewport: Span<time, duration>;
   };
 
   // Control over the bottom details pane.
@@ -421,11 +427,16 @@
 
   // Create a store mounted over the top of this plugin's persistent state.
   mountStore<T>(migrate: Migrate<T>): Store<T>;
+
+  trace: {
+    // A span representing the start and end time of the trace
+    readonly span: Span<time, duration>;
+  };
 }
 
 export interface Plugin {
   // Lifecycle methods.
-  onActivate(ctx: PluginContext): void;
+  onActivate?(ctx: PluginContext): void;
   onTraceLoad?(ctx: PluginContextTrace): Promise<void>;
   onTraceUnload?(ctx: PluginContextTrace): Promise<void>;
   onDeactivate?(ctx: PluginContext): void;
@@ -504,9 +515,8 @@
   [key: string]: string | number | boolean | undefined;
 };
 
-// Plugins can be passed as class refs, factory functions, or concrete plugin
-// implementations.
-export type PluginFactory = PluginClass | Plugin | (() => Plugin);
+// Plugins can be class refs or concrete plugin implementations.
+export type PluginFactory = PluginClass | Plugin;
 
 export interface PluginDescriptor {
   // A unique string for your plugin. To ensure the name is unique you
diff --git a/ui/src/tracks/android_log/index.ts b/ui/src/tracks/android_log/index.ts
index b83202b..c35143e 100644
--- a/ui/src/tracks/android_log/index.ts
+++ b/ui/src/tracks/android_log/index.ts
@@ -14,159 +14,49 @@
 
 import m from 'mithril';
 
-import {duration, Time, time} from '../../base/time';
-import {LIMIT, TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
-import {LogPanel} from '../../frontend/logs_panel';
-import {PanelSize} from '../../frontend/panel';
+import {LogFilteringCriteria, LogPanel} from './logs_panel';
 import {
-  EngineProxy,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
-  Track,
 } from '../../public';
-import {LONG, NUM} from '../../trace_processor/query_result';
+import {NUM} from '../../trace_processor/query_result';
+import {AndroidLogTrack} from './logs_track';
 
 export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack';
 
-export interface Data extends TrackData {
-  // Total number of log events within [start, end], before any quantization.
-  numEvents: number;
+const VERSION = 1;
 
-  // Below: data quantized by resolution and aggregated by event priority.
-  timestamps: BigInt64Array;
+const DEFAULT_STATE: AndroidLogPluginState = {
+  version: VERSION,
+  filter: {
+    // The first two log priorities are ignored.
+    minimumLevel: 2,
+    tags: [],
+    textEntry: '',
+    hideNonMatching: true,
+  },
+};
 
-  // Each Uint8 value has the i-th bit is set if there is at least one log
-  // event at the i-th priority level at the corresponding time in |timestamps|.
-  priorities: Uint8Array;
-}
-
-export interface Config {}
-
-interface LevelCfg {
-  color: string;
-  prios: number[];
-}
-
-const LEVELS: LevelCfg[] = [
-  {color: 'hsl(122, 39%, 49%)', prios: [0, 1, 2, 3]}, // Up to DEBUG: Green.
-  {color: 'hsl(0, 0%, 70%)', prios: [4]}, // 4 (INFO) -> Gray.
-  {color: 'hsl(45, 100%, 51%)', prios: [5]}, // 5 (WARN) -> Amber.
-  {color: 'hsl(4, 90%, 58%)', prios: [6]}, // 6 (ERROR) -> Red.
-  {color: 'hsl(291, 64%, 42%)', prios: [7]}, // 7 (FATAL) -> Purple
-];
-
-const MARGIN_TOP = 2;
-const RECT_HEIGHT = 35;
-const EVT_PX = 2; // Width of an event tick in pixels.
-
-class AndroidLogTrack implements Track {
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-
-  constructor(private engine: EngineProxy) {}
-
-  async onUpdate(): Promise<void> {
-    await this.fetcher.requestDataForCurrentTime();
-  }
-
-  async onDestroy(): Promise<void> {
-    this.fetcher.dispose();
-  }
-
-  getHeight(): number {
-    return 40;
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    const queryRes = await this.engine.query(`
-      select
-        cast(ts / ${resolution} as integer) * ${resolution} as tsQuant,
-        prio,
-        count(prio) as numEvents
-      from android_logs
-      where ts >= ${start} and ts <= ${end}
-      group by tsQuant, prio
-      order by tsQuant, prio limit ${LIMIT};`);
-
-    const rowCount = queryRes.numRows();
-    const result = {
-      start,
-      end,
-      resolution,
-      length: rowCount,
-      numEvents: 0,
-      timestamps: new BigInt64Array(rowCount),
-      priorities: new Uint8Array(rowCount),
-    };
-
-    const it = queryRes.iter({tsQuant: LONG, prio: NUM, numEvents: NUM});
-    for (let row = 0; it.valid(); it.next(), row++) {
-      result.timestamps[row] = it.tsQuant;
-      const prio = Math.min(it.prio, 7);
-      result.priorities[row] |= 1 << prio;
-      result.numEvents += it.numEvents;
-    }
-    return result;
-  }
-
-  render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
-    const {visibleTimeScale} = globals.timeline;
-
-    const data = this.fetcher.data;
-
-    if (data === undefined) return; // Can't possibly draw anything.
-
-    const dataStartPx = visibleTimeScale.timeToPx(data.start);
-    const dataEndPx = visibleTimeScale.timeToPx(data.end);
-
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      dataStartPx,
-      dataEndPx,
-    );
-
-    const quantWidth = Math.max(
-      EVT_PX,
-      visibleTimeScale.durationToPx(data.resolution),
-    );
-    const blockH = RECT_HEIGHT / LEVELS.length;
-    for (let i = 0; i < data.timestamps.length; i++) {
-      for (let lev = 0; lev < LEVELS.length; lev++) {
-        let hasEventsForCurColor = false;
-        for (const prio of LEVELS[lev].prios) {
-          if (data.priorities[i] & (1 << prio)) hasEventsForCurColor = true;
-        }
-        if (!hasEventsForCurColor) continue;
-        ctx.fillStyle = LEVELS[lev].color;
-        const timestamp = Time.fromRaw(data.timestamps[i]);
-        const px = Math.floor(visibleTimeScale.timeToPx(timestamp));
-        ctx.fillRect(px, MARGIN_TOP + blockH * lev, quantWidth, blockH);
-      } // for(lev)
-    } // for (timestamps)
-  }
+interface AndroidLogPluginState {
+  version: number;
+  filter: LogFilteringCriteria;
 }
 
 class AndroidLog implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    const store = ctx.mountStore<AndroidLogPluginState>((init) => {
+      return init && (init as {version: unknown}).version === VERSION
+        ? (init as AndroidLogPluginState)
+        : DEFAULT_STATE;
+    });
+
     const result = await ctx.engine.query(
       `select count(1) as cnt from android_logs`,
     );
     const logCount = result.firstRow({cnt: NUM}).cnt;
     if (logCount > 0) {
-      ctx.registerTrack({
+      ctx.registerStaticTrack({
         uri: 'perfetto.AndroidLog',
         displayName: 'Android logs',
         kind: ANDROID_LOGS_TRACK_KIND,
@@ -177,11 +67,17 @@
     const androidLogsTabUri = 'perfetto.AndroidLog#tab';
 
     // Eternal tabs should always be available even if there is nothing to show
+    const filterStore = store.createSubStore(
+      ['filter'],
+      (x) => x as LogFilteringCriteria,
+    );
+
     ctx.registerTab({
       isEphemeral: false,
       uri: androidLogsTabUri,
       content: {
-        render: () => m(LogPanel),
+        render: () =>
+          m(LogPanel, {filterStore: filterStore, engine: ctx.engine}),
         getTitle: () => 'Android Logs',
       },
     });
diff --git a/ui/src/tracks/android_log/logs_panel.ts b/ui/src/tracks/android_log/logs_panel.ts
new file mode 100644
index 0000000..1a8d233
--- /dev/null
+++ b/ui/src/tracks/android_log/logs_panel.ts
@@ -0,0 +1,548 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+import {duration, Span, time, Time, TimeSpan} from '../../base/time';
+import {Actions} from '../../common/actions';
+import {raf} from '../../core/raf_scheduler';
+import {DetailsShell} from '../../widgets/details_shell';
+import {VirtualScrollContainer} from '../../widgets/virtual_scroll_container';
+
+import {SELECTED_LOG_ROWS_COLOR} from '../../frontend/css_constants';
+import {globals} from '../../frontend/globals';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {createStore, EngineProxy, LONG, NUM, Store, STR} from '../../public';
+import {Monitor} from '../../base/monitor';
+import {AsyncLimiter} from '../../base/async_limiter';
+import {escapeGlob, escapeQuery} from '../../trace_processor/query_utils';
+import {Select} from '../../widgets/select';
+import {Button} from '../../widgets/button';
+import {TextInput} from '../../widgets/text_input';
+
+const ROW_H = 20;
+
+export interface LogFilteringCriteria {
+  minimumLevel: number;
+  tags: string[];
+  textEntry: string;
+  hideNonMatching: boolean;
+}
+
+export interface LogPanelAttrs {
+  filterStore: Store<LogFilteringCriteria>;
+  engine: EngineProxy;
+}
+
+interface Pagination {
+  offset: number;
+  count: number;
+}
+
+interface LogEntries {
+  offset: number;
+  timestamps: time[];
+  priorities: number[];
+  tags: string[];
+  messages: string[];
+  isHighlighted: boolean[];
+  processName: string[];
+  totalEvents: number; // Count of the total number of events within this window
+}
+
+export class LogPanel implements m.ClassComponent<LogPanelAttrs> {
+  private readonly SKIRT_SIZE = 50;
+  private entries?: LogEntries;
+  private isStale = true;
+  private viewportBounds = {top: 0, bottom: 0};
+
+  private readonly paginationStore = createStore<Pagination>({
+    offset: 0,
+    count: 0,
+  });
+  private readonly rowsMonitor: Monitor;
+  private readonly filterMonitor: Monitor;
+  private readonly queryLimiter = new AsyncLimiter();
+
+  constructor({attrs}: m.CVnode<LogPanelAttrs>) {
+    this.rowsMonitor = new Monitor([
+      () => attrs.filterStore.state,
+      () => globals.state.frontendLocalState.visibleState.start,
+      () => globals.state.frontendLocalState.visibleState.end,
+      () => this.paginationStore.state,
+    ]);
+
+    this.filterMonitor = new Monitor([() => attrs.filterStore.state]);
+  }
+
+  view({attrs}: m.CVnode<LogPanelAttrs>) {
+    if (this.rowsMonitor.ifStateChanged()) {
+      this.queryLimiter.schedule(async () => {
+        this.isStale = true;
+        raf.scheduleFullRedraw();
+
+        const visibleState = globals.state.frontendLocalState.visibleState;
+        const visibleSpan = new TimeSpan(visibleState.start, visibleState.end);
+
+        if (this.filterMonitor.ifStateChanged()) {
+          await updateLogView(attrs.engine, attrs.filterStore.state);
+        }
+
+        this.entries = await updateLogEntries(
+          attrs.engine,
+          visibleSpan,
+          this.paginationStore.state,
+        );
+
+        raf.scheduleFullRedraw();
+        this.isStale = false;
+      });
+    }
+
+    const hasProcessNames =
+      this.entries &&
+      this.entries.processName.filter((name) => name).length > 0;
+
+    const rows: m.Children = [];
+    rows.push(
+      m(
+        `.row`,
+        m('.cell.row-header', 'Timestamp'),
+        m('.cell.row-header', 'Level'),
+        m('.cell.row-header', 'Tag'),
+        hasProcessNames
+          ? m('.cell.with-process.row-header', 'Process name')
+          : undefined,
+        hasProcessNames
+          ? m('.cell.with-process.row-header', 'Message')
+          : m('.cell.no-process.row-header', 'Message'),
+        m('br'),
+      ),
+    );
+    if (this.entries) {
+      const offset = this.entries.offset;
+      const timestamps = this.entries.timestamps;
+      const priorities = this.entries.priorities;
+      const tags = this.entries.tags;
+      const messages = this.entries.messages;
+      const processNames = this.entries.processName;
+      const totalEvents = this.entries.totalEvents;
+
+      for (let i = 0; i < this.entries.timestamps.length; i++) {
+        const priorityLetter = LOG_PRIORITIES[priorities[i]][0];
+        const ts = timestamps[i];
+        const prioClass = priorityLetter || '';
+        const style: {top: string; backgroundColor?: string} = {
+          // 1.5 is for the width of the header
+          top: `${(offset + i + 1.5) * ROW_H}px`,
+        };
+        if (this.entries.isHighlighted[i]) {
+          style.backgroundColor = SELECTED_LOG_ROWS_COLOR;
+        }
+
+        rows.push(
+          m(
+            `.row.${prioClass}`,
+            {
+              class: this.isStale ? 'stale' : '',
+              style,
+              onmouseover: () => {
+                globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
+              },
+              onmouseout: () => {
+                globals.dispatch(
+                  Actions.setHoverCursorTimestamp({ts: Time.INVALID}),
+                );
+              },
+            },
+            m('.cell', m(Timestamp, {ts})),
+            m('.cell', priorityLetter || '?'),
+            m('.cell', tags[i]),
+            hasProcessNames
+              ? m('.cell.with-process', processNames[i])
+              : undefined,
+            hasProcessNames
+              ? m('.cell.with-process', messages[i])
+              : m('.cell.no-process', messages[i]),
+            m('br'),
+          ),
+        );
+      }
+
+      return m(
+        DetailsShell,
+        {
+          title: 'Android Logs',
+          description: `[${this.viewportBounds.top}, ${this.viewportBounds.bottom}] / ${totalEvents}`,
+          buttons: m(LogsFilters, {store: attrs.filterStore}),
+        },
+        m(
+          VirtualScrollContainer,
+          {
+            onScroll: (scrollContainer: HTMLElement) => {
+              this.recomputeVisibleRowsAndUpdate(scrollContainer);
+              raf.scheduleFullRedraw();
+            },
+          },
+          m(
+            '.log-panel',
+            m('.rows', {style: {height: `${totalEvents * ROW_H}px`}}, rows),
+          ),
+        ),
+      );
+    }
+
+    return null;
+  }
+
+  recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) {
+    const viewportTop = Math.floor(scrollContainer.scrollTop / ROW_H);
+    const viewportHeight = Math.ceil(scrollContainer.clientHeight / ROW_H);
+    const viewportBottom = viewportTop + viewportHeight;
+
+    this.viewportBounds = {
+      top: viewportTop,
+      bottom: viewportBottom,
+    };
+
+    const curPage = this.paginationStore.state;
+
+    if (
+      viewportTop < curPage.offset ||
+      viewportBottom >= curPage.offset + curPage.count
+    ) {
+      this.paginationStore.edit((draft) => {
+        const offset = Math.max(0, viewportTop - this.SKIRT_SIZE);
+        // Make it even so alternating coloured rows line up
+        const offsetEven = Math.floor(offset / 2) * 2;
+        draft.offset = offsetEven;
+        draft.count = viewportHeight + this.SKIRT_SIZE * 2;
+      });
+    }
+  }
+}
+
+export const LOG_PRIORITIES = [
+  '-',
+  '-',
+  'Verbose',
+  'Debug',
+  'Info',
+  'Warn',
+  'Error',
+  'Fatal',
+];
+const IGNORED_STATES = 2;
+
+interface LogPriorityWidgetAttrs {
+  options: string[];
+  selectedIndex: number;
+  onSelect: (id: number) => void;
+}
+
+class LogPriorityWidget implements m.ClassComponent<LogPriorityWidgetAttrs> {
+  view(vnode: m.Vnode<LogPriorityWidgetAttrs>) {
+    const attrs = vnode.attrs;
+    const optionComponents = [];
+    for (let i = IGNORED_STATES; i < attrs.options.length; i++) {
+      const selected = i === attrs.selectedIndex;
+      optionComponents.push(
+        m('option', {value: i, selected}, attrs.options[i]),
+      );
+    }
+    return m(
+      Select,
+      {
+        onchange: (e: Event) => {
+          const selectionValue = (e.target as HTMLSelectElement).value;
+          attrs.onSelect(Number(selectionValue));
+        },
+      },
+      optionComponents,
+    );
+  }
+}
+
+interface LogTagChipAttrs {
+  name: string;
+  removeTag: (name: string) => void;
+}
+
+class LogTagChip implements m.ClassComponent<LogTagChipAttrs> {
+  view({attrs}: m.CVnode<LogTagChipAttrs>) {
+    return m(Button, {
+      label: attrs.name,
+      rightIcon: 'close',
+      onclick: () => attrs.removeTag(attrs.name),
+    });
+  }
+}
+
+interface LogTagsWidgetAttrs {
+  tags: string[];
+  onRemoveTag: (tag: string) => void;
+  onAddTag: (tag: string) => void;
+}
+
+class LogTagsWidget implements m.ClassComponent<LogTagsWidgetAttrs> {
+  view(vnode: m.Vnode<LogTagsWidgetAttrs>) {
+    const tags = vnode.attrs.tags;
+    return [
+      tags.map((tag) =>
+        m(LogTagChip, {
+          name: tag,
+          removeTag: (tag) => vnode.attrs.onRemoveTag(tag),
+        }),
+      ),
+      m(TextInput, {
+        placeholder: 'Filter by tag...',
+        onkeydown: (e: KeyboardEvent) => {
+          // This is to avoid zooming on 'w'(and other unexpected effects
+          // of key presses in this input field).
+          e.stopPropagation();
+          const htmlElement = e.target as HTMLInputElement;
+
+          // When the user clicks 'Backspace' we delete the previous tag.
+          if (
+            e.key === 'Backspace' &&
+            tags.length > 0 &&
+            htmlElement.value === ''
+          ) {
+            vnode.attrs.onRemoveTag(tags[tags.length - 1]);
+            return;
+          }
+
+          if (e.key !== 'Enter') {
+            return;
+          }
+          if (htmlElement.value === '') {
+            return;
+          }
+          vnode.attrs.onAddTag(htmlElement.value.trim());
+          htmlElement.value = '';
+        },
+      }),
+    ];
+  }
+}
+
+interface LogTextWidgetAttrs {
+  onChange: (value: string) => void;
+}
+
+class LogTextWidget implements m.ClassComponent<LogTextWidgetAttrs> {
+  view({attrs}: m.CVnode<LogTextWidgetAttrs>) {
+    return m(TextInput, {
+      placeholder: 'Search logs...',
+      onkeyup: (e: KeyboardEvent) => {
+        // We want to use the value of the input field after it has been
+        // updated with the latest key (onkeyup).
+        const htmlElement = e.target as HTMLInputElement;
+        attrs.onChange(htmlElement.value);
+      },
+    });
+  }
+}
+
+interface FilterByTextWidgetAttrs {
+  hideNonMatching: boolean;
+  disabled: boolean;
+  onClick: () => void;
+}
+
+class FilterByTextWidget implements m.ClassComponent<FilterByTextWidgetAttrs> {
+  view({attrs}: m.Vnode<FilterByTextWidgetAttrs>) {
+    const icon = attrs.hideNonMatching ? 'unfold_less' : 'unfold_more';
+    const tooltip = attrs.hideNonMatching
+      ? 'Expand all and view highlighted'
+      : 'Collapse all';
+    return m(Button, {
+      icon,
+      title: tooltip,
+      disabled: attrs.disabled,
+      minimal: true,
+      onclick: attrs.onClick,
+    });
+  }
+}
+
+interface LogsFiltersAttrs {
+  store: Store<LogFilteringCriteria>;
+}
+
+export class LogsFilters implements m.ClassComponent<LogsFiltersAttrs> {
+  view({attrs}: m.CVnode<LogsFiltersAttrs>) {
+    return [
+      m('.log-label', 'Log Level'),
+      m(LogPriorityWidget, {
+        options: LOG_PRIORITIES,
+        selectedIndex: attrs.store.state.minimumLevel,
+        onSelect: (minimumLevel) => {
+          attrs.store.edit((draft) => {
+            draft.minimumLevel = minimumLevel;
+          });
+        },
+      }),
+      m(LogTagsWidget, {
+        tags: attrs.store.state.tags,
+        onAddTag: (tag) => {
+          attrs.store.edit((draft) => {
+            draft.tags.push(tag);
+          });
+        },
+        onRemoveTag: (tag) => {
+          attrs.store.edit((draft) => {
+            draft.tags = draft.tags.filter((t) => t !== tag);
+          });
+        },
+      }),
+      m(LogTextWidget, {
+        onChange: (text) => {
+          attrs.store.edit((draft) => {
+            draft.textEntry = text;
+          });
+        },
+      }),
+      m(FilterByTextWidget, {
+        hideNonMatching: attrs.store.state.hideNonMatching,
+        onClick: () => {
+          attrs.store.edit((draft) => {
+            draft.hideNonMatching = !draft.hideNonMatching;
+          });
+        },
+        disabled: attrs.store.state.textEntry === '',
+      }),
+    ];
+  }
+}
+
+async function updateLogEntries(
+  engine: EngineProxy,
+  span: Span<time, duration>,
+  pagination: Pagination,
+): Promise<LogEntries> {
+  const rowsResult = await engine.query(`
+        select
+          ts,
+          prio,
+          ifnull(tag, '[NULL]') as tag,
+          ifnull(msg, '[NULL]') as msg,
+          is_msg_highlighted as isMsgHighlighted,
+          is_process_highlighted as isProcessHighlighted,
+          ifnull(process_name, '') as processName
+        from filtered_logs
+        where ts >= ${span.start} and ts <= ${span.end}
+        order by ts
+        limit ${pagination.offset}, ${pagination.count}
+    `);
+
+  const timestamps: time[] = [];
+  const priorities = [];
+  const tags = [];
+  const messages = [];
+  const isHighlighted = [];
+  const processName = [];
+
+  const it = rowsResult.iter({
+    ts: LONG,
+    prio: NUM,
+    tag: STR,
+    msg: STR,
+    isMsgHighlighted: NUM,
+    isProcessHighlighted: NUM,
+    processName: STR,
+  });
+  for (; it.valid(); it.next()) {
+    timestamps.push(Time.fromRaw(it.ts));
+    priorities.push(it.prio);
+    tags.push(it.tag);
+    messages.push(it.msg);
+    isHighlighted.push(
+      it.isMsgHighlighted === 1 || it.isProcessHighlighted === 1,
+    );
+    processName.push(it.processName);
+  }
+
+  const queryRes = await engine.query(`
+    select
+      count(*) as totalEvents
+    from filtered_logs
+    where ts >= ${span.start} and ts <= ${span.end}
+  `);
+  const {totalEvents} = queryRes.firstRow({totalEvents: NUM});
+
+  return {
+    offset: pagination.offset,
+    timestamps,
+    priorities,
+    tags,
+    messages,
+    isHighlighted,
+    processName,
+    totalEvents,
+  };
+}
+
+async function updateLogView(
+  engine: EngineProxy,
+  filter: LogFilteringCriteria,
+) {
+  await engine.query('drop view if exists filtered_logs');
+
+  const globMatch = composeGlobMatch(filter.hideNonMatching, filter.textEntry);
+  let selectedRows = `select prio, ts, tag, msg,
+      process.name as process_name, ${globMatch}
+      from android_logs
+      left join thread using(utid)
+      left join process using(upid)
+      where prio >= ${filter.minimumLevel}`;
+  if (filter.tags.length) {
+    selectedRows += ` and tag in (${serializeTags(filter.tags)})`;
+  }
+
+  // We extract only the rows which will be visible.
+  await engine.query(`create view filtered_logs as select *
+    from (${selectedRows})
+    where is_msg_chosen is 1 or is_process_chosen is 1`);
+}
+
+function serializeTags(tags: string[]) {
+  return tags.map((tag) => escapeQuery(tag)).join();
+}
+
+function composeGlobMatch(isCollaped: boolean, textEntry: string) {
+  if (isCollaped) {
+    // If the entries are collapsed, we won't highlight any lines.
+    return `msg glob ${escapeGlob(textEntry)} as is_msg_chosen,
+      (process.name is not null and process.name glob ${escapeGlob(
+        textEntry,
+      )}) as is_process_chosen,
+      0 as is_msg_highlighted,
+      0 as is_process_highlighted`;
+  } else if (!textEntry) {
+    // If there is no text entry, we will show all lines, but won't highlight.
+    // any.
+    return `1 as is_msg_chosen,
+      1 as is_process_chosen,
+      0 as is_msg_highlighted,
+      0 as is_process_highlighted`;
+  } else {
+    return `1 as is_msg_chosen,
+      1 as is_process_chosen,
+      msg glob ${escapeGlob(textEntry)} as is_msg_highlighted,
+      (process.name is not null and process.name glob ${escapeGlob(
+        textEntry,
+      )}) as is_process_highlighted`;
+  }
+}
diff --git a/ui/src/tracks/android_log/logs_track.ts b/ui/src/tracks/android_log/logs_track.ts
new file mode 100644
index 0000000..1c5b3c6
--- /dev/null
+++ b/ui/src/tracks/android_log/logs_track.ts
@@ -0,0 +1,143 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Time, duration, time} from '../../base/time';
+import {LIMIT, TrackData} from '../../common/track_data';
+import {LONG, NUM, TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {globals} from '../../frontend/globals';
+import {PanelSize} from '../../frontend/panel';
+import {EngineProxy, Track} from '../../public';
+
+export interface Data extends TrackData {
+  // Total number of log events within [start, end], before any quantization.
+  numEvents: number;
+
+  // Below: data quantized by resolution and aggregated by event priority.
+  timestamps: BigInt64Array;
+
+  // Each Uint8 value has the i-th bit is set if there is at least one log
+  // event at the i-th priority level at the corresponding time in |timestamps|.
+  priorities: Uint8Array;
+}
+
+const LEVELS: LevelCfg[] = [
+  {color: 'hsl(122, 39%, 49%)', prios: [0, 1, 2, 3]}, // Up to DEBUG: Green.
+  {color: 'hsl(0, 0%, 70%)', prios: [4]}, // 4 (INFO) -> Gray.
+  {color: 'hsl(45, 100%, 51%)', prios: [5]}, // 5 (WARN) -> Amber.
+  {color: 'hsl(4, 90%, 58%)', prios: [6]}, // 6 (ERROR) -> Red.
+  {color: 'hsl(291, 64%, 42%)', prios: [7]}, // 7 (FATAL) -> Purple
+];
+
+const MARGIN_TOP = 2;
+const RECT_HEIGHT = 35;
+const EVT_PX = 2; // Width of an event tick in pixels.
+
+interface LevelCfg {
+  color: string;
+  prios: number[];
+}
+
+export class AndroidLogTrack implements Track {
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+
+  constructor(private engine: EngineProxy) {}
+
+  async onUpdate(): Promise<void> {
+    await this.fetcher.requestDataForCurrentTime();
+  }
+
+  async onDestroy(): Promise<void> {
+    this.fetcher.dispose();
+  }
+
+  getHeight(): number {
+    return 40;
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    const queryRes = await this.engine.query(`
+      select
+        cast(ts / ${resolution} as integer) * ${resolution} as tsQuant,
+        prio,
+        count(prio) as numEvents
+      from android_logs
+      where ts >= ${start} and ts <= ${end}
+      group by tsQuant, prio
+      order by tsQuant, prio limit ${LIMIT};`);
+
+    const rowCount = queryRes.numRows();
+    const result = {
+      start,
+      end,
+      resolution,
+      length: rowCount,
+      numEvents: 0,
+      timestamps: new BigInt64Array(rowCount),
+      priorities: new Uint8Array(rowCount),
+    };
+
+    const it = queryRes.iter({tsQuant: LONG, prio: NUM, numEvents: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      result.timestamps[row] = it.tsQuant;
+      const prio = Math.min(it.prio, 7);
+      result.priorities[row] |= 1 << prio;
+      result.numEvents += it.numEvents;
+    }
+    return result;
+  }
+
+  render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
+    const {visibleTimeScale} = globals.timeline;
+
+    const data = this.fetcher.data;
+
+    if (data === undefined) return; // Can't possibly draw anything.
+
+    const dataStartPx = visibleTimeScale.timeToPx(data.start);
+    const dataEndPx = visibleTimeScale.timeToPx(data.end);
+
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      dataStartPx,
+      dataEndPx,
+    );
+
+    const quantWidth = Math.max(
+      EVT_PX,
+      visibleTimeScale.durationToPx(data.resolution),
+    );
+    const blockH = RECT_HEIGHT / LEVELS.length;
+    for (let i = 0; i < data.timestamps.length; i++) {
+      for (let lev = 0; lev < LEVELS.length; lev++) {
+        let hasEventsForCurColor = false;
+        for (const prio of LEVELS[lev].prios) {
+          if (data.priorities[i] & (1 << prio)) hasEventsForCurColor = true;
+        }
+        if (!hasEventsForCurColor) continue;
+        ctx.fillStyle = LEVELS[lev].color;
+        const timestamp = Time.fromRaw(data.timestamps[i]);
+        const px = Math.floor(visibleTimeScale.timeToPx(timestamp));
+        ctx.fillRect(px, MARGIN_TOP + blockH * lev, quantWidth, blockH);
+      } // for(lev)
+    } // for (timestamps)
+  }
+}
diff --git a/ui/src/tracks/annotation/index.ts b/ui/src/tracks/annotation/index.ts
index 6fc3973..d36e9a2 100644
--- a/ui/src/tracks/annotation/index.ts
+++ b/ui/src/tracks/annotation/index.ts
@@ -12,19 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
 import {ChromeSliceTrack, SLICE_TRACK_KIND} from '../chrome_slices/';
 import {COUNTER_TRACK_KIND, TraceProcessorCounterTrack} from '../counter';
 
 class AnnotationPlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await this.addAnnotationTracks(ctx);
     await this.addAnnotationCounterTracks(ctx);
diff --git a/ui/src/tracks/async_slices/index.ts b/ui/src/tracks/async_slices/index.ts
index c4f55c9..2e8b3d5 100644
--- a/ui/src/tracks/async_slices/index.ts
+++ b/ui/src/tracks/async_slices/index.ts
@@ -12,12 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {getTrackName} from '../../public/utils';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
 
@@ -26,8 +21,6 @@
 export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
 
 class AsyncSlicePlugin implements Plugin {
-  onActivate(_ctx: PluginContext) {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await this.addGlobalAsyncTracks(ctx);
     await this.addProcessAsyncSliceTracks(ctx);
diff --git a/ui/src/tracks/chrome_scroll_jank/index.ts b/ui/src/tracks/chrome_scroll_jank/index.ts
index 41917bb..dc59118 100644
--- a/ui/src/tracks/chrome_scroll_jank/index.ts
+++ b/ui/src/tracks/chrome_scroll_jank/index.ts
@@ -23,7 +23,6 @@
   BottomTabToSCSAdapter,
   NUM,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
@@ -148,8 +147,6 @@
 }
 
 class ChromeScrollJankPlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await this.addChromeScrollJankTrack(ctx);
     await this.addTopLevelScrollTrack(ctx);
diff --git a/ui/src/tracks/chrome_slices/index.ts b/ui/src/tracks/chrome_slices/index.ts
index d7410b5..cae2557 100644
--- a/ui/src/tracks/chrome_slices/index.ts
+++ b/ui/src/tracks/chrome_slices/index.ts
@@ -29,7 +29,6 @@
   BottomTabToSCSAdapter,
   EngineProxy,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
@@ -222,8 +221,6 @@
 }
 
 class ChromeSlicesPlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const {engine} = ctx;
     const result = await engine.query(`
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index 56ed65a..2ac64d6 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -25,7 +25,6 @@
   LONG_NULL,
   NUM,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   PrimaryTrackSortKey,
@@ -175,8 +174,6 @@
 }
 
 class CounterPlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await this.addCounterTracks(ctx);
     await this.addGpuFrequencyTracks(ctx);
diff --git a/ui/src/tracks/cpu_freq/index.ts b/ui/src/tracks/cpu_freq/index.ts
index a2327fa..52708e9 100644
--- a/ui/src/tracks/cpu_freq/index.ts
+++ b/ui/src/tracks/cpu_freq/index.ts
@@ -29,7 +29,6 @@
 import {
   EngineProxy,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   Track,
@@ -522,8 +521,6 @@
 }
 
 class CpuFreq implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const {engine} = ctx;
 
diff --git a/ui/src/tracks/cpu_profile/index.ts b/ui/src/tracks/cpu_profile/index.ts
index 344f0e3..4d3e87f 100644
--- a/ui/src/tracks/cpu_profile/index.ts
+++ b/ui/src/tracks/cpu_profile/index.ts
@@ -26,7 +26,6 @@
 import {
   EngineProxy,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   Track,
@@ -263,8 +262,6 @@
 }
 
 class CpuProfile implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
       with thread_cpu_sample as (
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index c561cbd..412ee6a 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -38,7 +38,6 @@
 import {
   EngineProxy,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   Track,
@@ -543,10 +542,6 @@
 }
 
 class CpuSlices implements Plugin {
-  onActivate(_ctx: PluginContext): void {
-    // No-op
-  }
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const cpus = await ctx.engine.getCpus();
     const cpuToSize = await this.guessCpuSizes(ctx.engine);
diff --git a/ui/src/tracks/custom_sql_table_slices/index.ts b/ui/src/tracks/custom_sql_table_slices/index.ts
index 2222f9b..72f2075 100644
--- a/ui/src/tracks/custom_sql_table_slices/index.ts
+++ b/ui/src/tracks/custom_sql_table_slices/index.ts
@@ -26,7 +26,7 @@
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+import {Plugin, PluginDescriptor} from '../../public';
 
 export interface CustomSqlImportConfig {
   modules: string[];
@@ -140,9 +140,7 @@
   }
 }
 
-class CustomSqlTrackPlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-}
+class CustomSqlTrackPlugin implements Plugin {}
 
 export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.CustomSqlTrack',
diff --git a/ui/src/tracks/debug/index.ts b/ui/src/tracks/debug/index.ts
index 9ab23c0..fad9cae 100644
--- a/ui/src/tracks/debug/index.ts
+++ b/ui/src/tracks/debug/index.ts
@@ -20,7 +20,6 @@
 import {
   BottomTabToSCSAdapter,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
@@ -31,8 +30,6 @@
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 
 class DebugTrackPlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     ctx.registerTrack({
       uri: DEBUG_SLICE_TRACK_URI,
diff --git a/ui/src/tracks/frames/index.ts b/ui/src/tracks/frames/index.ts
index ccf9ae6..47e88ff 100644
--- a/ui/src/tracks/frames/index.ts
+++ b/ui/src/tracks/frames/index.ts
@@ -12,12 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {getTrackName} from '../../public/utils';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
 
@@ -28,8 +23,6 @@
 export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
 
 class FramesPlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     this.addExpectedFrames(ctx);
     this.addActualFrames(ctx);
diff --git a/ui/src/tracks/ftrace/index.ts b/ui/src/tracks/ftrace/index.ts
index b3fd003..7542232 100644
--- a/ui/src/tracks/ftrace/index.ts
+++ b/ui/src/tracks/ftrace/index.ts
@@ -18,7 +18,6 @@
 import {
   EngineProxy,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
@@ -39,8 +38,6 @@
 class FtraceRawPlugin implements Plugin {
   private trash = new Trash();
 
-  onActivate(_: PluginContext) {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const store = ctx.mountStore<FtracePluginState>((init: unknown) => {
       if (
diff --git a/ui/src/tracks/heap_profile/index.ts b/ui/src/tracks/heap_profile/index.ts
index b905ef7..512af1a 100644
--- a/ui/src/tracks/heap_profile/index.ts
+++ b/ui/src/tracks/heap_profile/index.ts
@@ -27,7 +27,6 @@
 import {NewTrackArgs} from '../../frontend/track';
 import {
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   Slice,
@@ -115,7 +114,6 @@
 }
 
 class HeapProfilePlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
       select distinct(upid) from heap_profile_allocation
diff --git a/ui/src/tracks/perf_samples_profile/index.ts b/ui/src/tracks/perf_samples_profile/index.ts
index 6720142..752684a 100644
--- a/ui/src/tracks/perf_samples_profile/index.ts
+++ b/ui/src/tracks/perf_samples_profile/index.ts
@@ -26,7 +26,6 @@
 import {
   EngineProxy,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   Track,
@@ -245,8 +244,6 @@
 }
 
 class PerfSamplesProfilePlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
       select distinct upid, pid
diff --git a/ui/src/tracks/process_summary/index.ts b/ui/src/tracks/process_summary/index.ts
index 70e22e6..47a93b0 100644
--- a/ui/src/tracks/process_summary/index.ts
+++ b/ui/src/tracks/process_summary/index.ts
@@ -14,12 +14,7 @@
 
 import {v4 as uuidv4} from 'uuid';
 
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {
   LONG_NULL,
   NUM,
@@ -45,8 +40,6 @@
   private upidToUuid = new Map<number, string>();
   private utidToUuid = new Map<number, string>();
 
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await this.addProcessTrackGroups(ctx);
     await this.addKernelThreadSummary(ctx);
diff --git a/ui/src/tracks/sched/active_cpu_count.ts b/ui/src/tracks/sched/active_cpu_count.ts
index a5fd9d7..721a884 100644
--- a/ui/src/tracks/sched/active_cpu_count.ts
+++ b/ui/src/tracks/sched/active_cpu_count.ts
@@ -69,11 +69,9 @@
   }
 
   getTrackShellButtons(): m.Children {
-    return [
-      m(CloseTrackButton, {
-        trackKey: this.trackKey,
-      }),
-    ];
+    return m(CloseTrackButton, {
+      trackKey: this.trackKey,
+    });
   }
 
   protected getDefaultCounterOptions(): CounterOptions {
diff --git a/ui/src/tracks/sched/runnable_thread_count.ts b/ui/src/tracks/sched/runnable_thread_count.ts
index 4522588..f5ab7ca 100644
--- a/ui/src/tracks/sched/runnable_thread_count.ts
+++ b/ui/src/tracks/sched/runnable_thread_count.ts
@@ -49,11 +49,9 @@
   }
 
   getTrackShellButtons(): m.Children {
-    return [
-      m(CloseTrackButton, {
-        trackKey: this.trackKey,
-      }),
-    ];
+    return m(CloseTrackButton, {
+      trackKey: this.trackKey,
+    });
   }
 
   protected getDefaultCounterOptions(): CounterOptions {
diff --git a/ui/src/tracks/screenshots/index.ts b/ui/src/tracks/screenshots/index.ts
index aa55903..029f818 100644
--- a/ui/src/tracks/screenshots/index.ts
+++ b/ui/src/tracks/screenshots/index.ts
@@ -20,7 +20,6 @@
   BottomTabToSCSAdapter,
   NUM,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   PrimaryTrackSortKey,
@@ -84,8 +83,6 @@
 }
 
 class ScreenshotsPlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await ctx.engine.query(`INCLUDE PERFETTO MODULE android.screenshots`);
 
diff --git a/ui/src/tracks/thread_state/index.ts b/ui/src/tracks/thread_state/index.ts
index cc8bcc2..b38eb1d 100644
--- a/ui/src/tracks/thread_state/index.ts
+++ b/ui/src/tracks/thread_state/index.ts
@@ -18,7 +18,6 @@
 import {
   BottomTabToSCSAdapter,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
@@ -30,8 +29,6 @@
 export const THREAD_STATE_TRACK_KIND = 'ThreadStateTrack';
 
 class ThreadState implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const {engine} = ctx;
     const result = await engine.query(`
diff --git a/ui/src/tracks/visualised_args/index.ts b/ui/src/tracks/visualised_args/index.ts
index 434874f..d2ede0c 100644
--- a/ui/src/tracks/visualised_args/index.ts
+++ b/ui/src/tracks/visualised_args/index.ts
@@ -12,18 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// import {NewTrackArgs, Track} from '../../frontend/track';
-// import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
 import m from 'mithril';
 import {v4 as uuidv4} from 'uuid';
 
 import {Actions} from '../../common/actions';
 import {globals} from '../../frontend/globals';
-import {TrackButton} from '../../frontend/track_panel';
 import {
   EngineProxy,
   Plugin,
-  PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   TrackContext,
@@ -33,6 +29,8 @@
   VISUALISED_ARGS_SLICE_TRACK_URI,
   VisualisedArgsState,
 } from '../../frontend/visualized_args_tracks';
+import {Button} from '../../widgets/button';
+import {Icons} from '../../base/semantic_icons';
 
 export class VisualisedArgsTrack extends ChromeSliceTrack {
   private helperViewName: string;
@@ -90,8 +88,8 @@
   }
 
   getTrackShellButtons(): m.Children {
-    return m(TrackButton, {
-      action: () => {
+    return m(Button, {
+      onclick: () => {
         // This behavior differs to the original behavior a little.
         // Originally, hitting the close button on a single track removed ALL
         // tracks with this argName, whereas this one only closes the single
@@ -100,16 +98,15 @@
         // tracks instead of this "initial state" approach to add these tracks.
         globals.dispatch(Actions.removeTracks({trackKeys: [this.trackKey]}));
       },
-      i: 'close',
-      tooltip: 'Close',
-      showButton: true,
+      icon: Icons.Close,
+      title: 'Close',
+      minimal: true,
+      compact: true,
     });
   }
 }
 
 class VisualisedArgsPlugin implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     ctx.registerTrack({
       uri: VISUALISED_ARGS_SLICE_TRACK_URI,
diff --git a/ui/src/widgets/button.ts b/ui/src/widgets/button.ts
index 619f23c..276b2f4 100644
--- a/ui/src/widgets/button.ts
+++ b/ui/src/widgets/button.ts
@@ -16,7 +16,7 @@
 
 import {classNames} from '../base/classnames';
 
-import {HTMLButtonAttrs} from './common';
+import {HTMLAttrs, HTMLButtonAttrs} from './common';
 import {Icon} from './icon';
 import {Popup} from './popup';
 import {Spinner} from './spinner';
@@ -44,6 +44,9 @@
   // Show loading spinner instead of icon.
   // Defaults to false.
   loading?: boolean;
+  // Whether to use a filled icon
+  // Defaults to false;
+  iconFilled?: boolean;
 }
 
 interface IconButtonAttrs extends CommonAttrs {
@@ -70,6 +73,7 @@
       rightIcon,
       className,
       dismissPopup,
+      iconFilled,
       ...htmlAttrs
     } = attrs;
 
@@ -81,7 +85,6 @@
       minimal && 'pf-minimal',
       icon && !label && 'pf-icon-only',
       dismissPopup && Popup.DISMISS_POPUP_GROUP_CLASS,
-      // loading && 'pf-loading',
       className,
     );
 
@@ -92,17 +95,23 @@
         className: classes,
       },
       this.renderIcon(attrs),
-      rightIcon && m(Icon, {className: 'pf-right-icon', icon: rightIcon}),
+      rightIcon &&
+        m(Icon, {
+          className: 'pf-right-icon',
+          icon: rightIcon,
+          filled: iconFilled,
+        }),
       label || '\u200B', // Zero width space keeps button in-flow
     );
   }
 
   private renderIcon(attrs: ButtonAttrs): m.Children {
+    const {icon, iconFilled} = attrs;
     const className = 'pf-left-icon';
     if (attrs.loading) {
       return m(Spinner, {className});
-    } else if (attrs.icon) {
-      return m(Icon, {className, icon: attrs.icon});
+    } else if (icon) {
+      return m(Icon, {className, icon, filled: iconFilled});
     } else {
       return undefined;
     }
@@ -112,8 +121,8 @@
 /**
  * Space buttons out with a little gap between each one.
  */
-export class ButtonBar implements m.ClassComponent {
-  view({children}: m.CVnode): m.Children {
-    return m('.pf-button-bar', children);
+export class ButtonBar implements m.ClassComponent<HTMLAttrs> {
+  view({attrs, children}: m.CVnode<HTMLAttrs>): m.Children {
+    return m('.pf-button-bar', attrs, children);
   }
 }