Merge "ui: Include one or more perf tracks in area selection."
diff --git a/Android.bp b/Android.bp
index 1b10e3d..b0a5498 100644
--- a/Android.bp
+++ b/Android.bp
@@ -92,6 +92,7 @@
         ":perfetto_src_tracing_common",
         ":perfetto_src_tracing_core_core",
         ":perfetto_src_tracing_ipc_common",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         "src/profiling/memory/main.cc",
     ],
@@ -347,6 +348,7 @@
         ":perfetto_src_tracing_common",
         ":perfetto_src_tracing_core_core",
         ":perfetto_src_tracing_ipc_common",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
     ],
     shared_libs: [
@@ -548,6 +550,7 @@
         ":perfetto_src_tracing_core_service",
         ":perfetto_src_tracing_ipc_common",
         ":perfetto_src_tracing_ipc_consumer_consumer",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         ":perfetto_src_tracing_ipc_service_service",
     ],
@@ -732,6 +735,7 @@
         ":perfetto_src_tracing_in_process_backend",
         ":perfetto_src_tracing_ipc_common",
         ":perfetto_src_tracing_ipc_consumer_consumer",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         ":perfetto_src_tracing_ipc_service_service",
         ":perfetto_src_tracing_platform_impl",
@@ -924,6 +928,7 @@
         ":perfetto_src_tracing_core_core",
         ":perfetto_src_tracing_ipc_common",
         ":perfetto_src_tracing_ipc_consumer_consumer",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         "src/perfetto_cmd/main.cc",
     ],
@@ -1099,6 +1104,7 @@
         ":perfetto_src_tracing_core_service",
         ":perfetto_src_tracing_ipc_common",
         ":perfetto_src_tracing_ipc_consumer_consumer",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         ":perfetto_src_tracing_ipc_service_service",
         ":perfetto_test_end_to_end_integrationtests",
@@ -1355,6 +1361,7 @@
         ":perfetto_src_tracing_core_service",
         ":perfetto_src_tracing_ipc_common",
         ":perfetto_src_tracing_ipc_consumer_consumer",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         ":perfetto_src_tracing_ipc_service_service",
         ":perfetto_test_test_helper",
@@ -1544,6 +1551,11 @@
     name: "perfetto_include_perfetto_ext_base_base",
 }
 
+// GN: //include/perfetto/ext/base/http:http
+filegroup {
+    name: "perfetto_include_perfetto_ext_base_http_http",
+}
+
 // GN: //include/perfetto/ext/ipc:ipc
 filegroup {
     name: "perfetto_include_perfetto_ext_ipc_ipc",
@@ -1791,6 +1803,7 @@
         ":perfetto_src_tracing_in_process_backend",
         ":perfetto_src_tracing_ipc_common",
         ":perfetto_src_tracing_ipc_consumer_consumer",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         ":perfetto_src_tracing_ipc_service_service",
         ":perfetto_src_tracing_platform_impl",
@@ -4136,6 +4149,7 @@
         "protos/perfetto/trace/ftrace/sde.proto",
         "protos/perfetto/trace/ftrace/signal.proto",
         "protos/perfetto/trace/ftrace/sync.proto",
+        "protos/perfetto/trace/ftrace/synthetic.proto",
         "protos/perfetto/trace/ftrace/systrace.proto",
         "protos/perfetto/trace/ftrace/task.proto",
         "protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4353,6 +4367,7 @@
         "protos/perfetto/trace/ftrace/sde.proto",
         "protos/perfetto/trace/ftrace/signal.proto",
         "protos/perfetto/trace/ftrace/sync.proto",
+        "protos/perfetto/trace/ftrace/synthetic.proto",
         "protos/perfetto/trace/ftrace/systrace.proto",
         "protos/perfetto/trace/ftrace/task.proto",
         "protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4404,6 +4419,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/sde.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/signal.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sync.gen.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/synthetic.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/systrace.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/task.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.gen.cc",
@@ -4455,6 +4471,7 @@
         "protos/perfetto/trace/ftrace/sde.proto",
         "protos/perfetto/trace/ftrace/signal.proto",
         "protos/perfetto/trace/ftrace/sync.proto",
+        "protos/perfetto/trace/ftrace/synthetic.proto",
         "protos/perfetto/trace/ftrace/systrace.proto",
         "protos/perfetto/trace/ftrace/task.proto",
         "protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4506,6 +4523,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/sde.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/signal.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sync.gen.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/synthetic.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/systrace.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/task.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.gen.h",
@@ -4561,6 +4579,7 @@
         "protos/perfetto/trace/ftrace/sde.proto",
         "protos/perfetto/trace/ftrace/signal.proto",
         "protos/perfetto/trace/ftrace/sync.proto",
+        "protos/perfetto/trace/ftrace/synthetic.proto",
         "protos/perfetto/trace/ftrace/systrace.proto",
         "protos/perfetto/trace/ftrace/task.proto",
         "protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4611,6 +4630,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/sde.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/signal.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sync.pb.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/synthetic.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/systrace.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/task.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.pb.cc",
@@ -4662,6 +4682,7 @@
         "protos/perfetto/trace/ftrace/sde.proto",
         "protos/perfetto/trace/ftrace/signal.proto",
         "protos/perfetto/trace/ftrace/sync.proto",
+        "protos/perfetto/trace/ftrace/synthetic.proto",
         "protos/perfetto/trace/ftrace/systrace.proto",
         "protos/perfetto/trace/ftrace/task.proto",
         "protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4712,6 +4733,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/sde.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/signal.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sync.pb.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/synthetic.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/systrace.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/task.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.pb.h",
@@ -4767,6 +4789,7 @@
         "protos/perfetto/trace/ftrace/sde.proto",
         "protos/perfetto/trace/ftrace/signal.proto",
         "protos/perfetto/trace/ftrace/sync.proto",
+        "protos/perfetto/trace/ftrace/synthetic.proto",
         "protos/perfetto/trace/ftrace/systrace.proto",
         "protos/perfetto/trace/ftrace/task.proto",
         "protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4818,6 +4841,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/sde.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/signal.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sync.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/synthetic.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/systrace.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/task.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.pbzero.cc",
@@ -4869,6 +4893,7 @@
         "protos/perfetto/trace/ftrace/sde.proto",
         "protos/perfetto/trace/ftrace/signal.proto",
         "protos/perfetto/trace/ftrace/sync.proto",
+        "protos/perfetto/trace/ftrace/synthetic.proto",
         "protos/perfetto/trace/ftrace/systrace.proto",
         "protos/perfetto/trace/ftrace/task.proto",
         "protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4920,6 +4945,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/sde.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/signal.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sync.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/synthetic.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/systrace.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/task.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.pbzero.h",
@@ -6875,6 +6901,24 @@
     ],
 }
 
+// GN: //src/base/http:http
+filegroup {
+    name: "perfetto_src_base_http_http",
+    srcs: [
+        "src/base/http/http_server.cc",
+        "src/base/http/sha1.cc",
+    ],
+}
+
+// GN: //src/base/http:unittests
+filegroup {
+    name: "perfetto_src_base_http_unittests",
+    srcs: [
+        "src/base/http/http_server_unittest.cc",
+        "src/base/http/sha1_unittest.cc",
+    ],
+}
+
 // GN: //src/base:test_support
 filegroup {
     name: "perfetto_src_base_test_support",
@@ -6892,6 +6936,7 @@
     srcs: [
         "src/base/base64_unittest.cc",
         "src/base/circular_queue_unittest.cc",
+        "src/base/flat_hash_map_unittest.cc",
         "src/base/flat_set_unittest.cc",
         "src/base/getopt_compat_unittest.cc",
         "src/base/logging_unittest.cc",
@@ -6901,6 +6946,7 @@
         "src/base/paged_memory_unittest.cc",
         "src/base/periodic_task_unittest.cc",
         "src/base/scoped_file_unittest.cc",
+        "src/base/small_vector_unittest.cc",
         "src/base/string_splitter_unittest.cc",
         "src/base/string_utils_unittest.cc",
         "src/base/string_view_unittest.cc",
@@ -8183,7 +8229,6 @@
         "src/trace_processor/importers/gzip/gzip_trace_parser.cc",
         "src/trace_processor/importers/json/json_trace_parser.cc",
         "src/trace_processor/importers/json/json_trace_tokenizer.cc",
-        "src/trace_processor/importers/json/json_tracker.cc",
         "src/trace_processor/importers/proto/android_probes_module.cc",
         "src/trace_processor/importers/proto/android_probes_parser.cc",
         "src/trace_processor/importers/proto/android_probes_tracker.cc",
@@ -8680,6 +8725,15 @@
     name: "perfetto_src_traced_probes_power_power",
     srcs: [
         "src/traced/probes/power/android_power_data_source.cc",
+        "src/traced/probes/power/linux_power_sysfs_data_source.cc",
+    ],
+}
+
+// GN: //src/traced/probes/power:unittests
+filegroup {
+    name: "perfetto_src_traced_probes_power_unittests",
+    srcs: [
+        "src/traced/probes/power/linux_power_sysfs_data_source_unittest.cc",
     ],
 }
 
@@ -8876,7 +8930,6 @@
 filegroup {
     name: "perfetto_src_tracing_ipc_common",
     srcs: [
-        "src/tracing/ipc/default_socket.cc",
         "src/tracing/ipc/memfd.cc",
         "src/tracing/ipc/posix_shared_memory.cc",
         "src/tracing/ipc/shared_memory_windows.cc",
@@ -8891,6 +8944,14 @@
     ],
 }
 
+// GN: //src/tracing/ipc:default_socket
+filegroup {
+    name: "perfetto_src_tracing_ipc_default_socket",
+    srcs: [
+        "src/tracing/ipc/default_socket.cc",
+    ],
+}
+
 // GN: //src/tracing/ipc/producer:producer
 filegroup {
     name: "perfetto_src_tracing_ipc_producer_producer",
@@ -9178,6 +9239,7 @@
     srcs: [
         ":perfetto_include_perfetto_base_base",
         ":perfetto_include_perfetto_ext_base_base",
+        ":perfetto_include_perfetto_ext_base_http_http",
         ":perfetto_include_perfetto_ext_ipc_ipc",
         ":perfetto_include_perfetto_ext_trace_processor_export_json",
         ":perfetto_include_perfetto_ext_trace_processor_importers_memory_tracker_memory_tracker",
@@ -9285,6 +9347,8 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_http_http",
+        ":perfetto_src_base_http_unittests",
         ":perfetto_src_base_test_support",
         ":perfetto_src_base_unittests",
         ":perfetto_src_base_unix_socket",
@@ -9394,6 +9458,7 @@
         ":perfetto_src_traced_probes_packages_list_packages_list_parser",
         ":perfetto_src_traced_probes_packages_list_unittests",
         ":perfetto_src_traced_probes_power_power",
+        ":perfetto_src_traced_probes_power_unittests",
         ":perfetto_src_traced_probes_probes_src",
         ":perfetto_src_traced_probes_ps_ps",
         ":perfetto_src_traced_probes_ps_unittests",
@@ -9412,6 +9477,7 @@
         ":perfetto_src_tracing_core_unittests",
         ":perfetto_src_tracing_ipc_common",
         ":perfetto_src_tracing_ipc_consumer_consumer",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         ":perfetto_src_tracing_ipc_service_service",
         ":perfetto_src_tracing_ipc_unittests",
@@ -9597,6 +9663,7 @@
     srcs: [
         ":perfetto_include_perfetto_base_base",
         ":perfetto_include_perfetto_ext_base_base",
+        ":perfetto_include_perfetto_ext_base_http_http",
         ":perfetto_include_perfetto_ext_trace_processor_export_json",
         ":perfetto_include_perfetto_ext_trace_processor_importers_memory_tracker_memory_tracker",
         ":perfetto_include_perfetto_ext_traced_sys_stats_counters",
@@ -9634,6 +9701,7 @@
         ":perfetto_protos_perfetto_trace_system_info_zero_gen",
         ":perfetto_protos_perfetto_trace_track_event_zero_gen",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_http_http",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_profiling_deobfuscator",
         ":perfetto_src_profiling_symbolizer_symbolize_database",
@@ -9984,6 +10052,7 @@
         ":perfetto_src_tracing_core_core",
         ":perfetto_src_tracing_core_service",
         ":perfetto_src_tracing_ipc_common",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         "src/profiling/perf/main.cc",
     ],
@@ -10148,6 +10217,7 @@
         ":perfetto_src_tracing_common",
         ":perfetto_src_tracing_core_core",
         ":perfetto_src_tracing_ipc_common",
+        ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         "src/perfetto_cmd/trigger_perfetto_main.cc",
     ],
diff --git a/BUILD b/BUILD
index c818210..85ba196 100644
--- a/BUILD
+++ b/BUILD
@@ -244,6 +244,7 @@
         ":src_tracing_core_service",
         ":src_tracing_ipc_common",
         ":src_tracing_ipc_consumer_consumer",
+        ":src_tracing_ipc_default_socket",
         ":src_tracing_ipc_producer_producer",
         ":src_tracing_ipc_service_service",
     ],
@@ -330,6 +331,15 @@
     ],
 )
 
+# GN target: //include/perfetto/ext/base/http:http
+perfetto_filegroup(
+    name = "include_perfetto_ext_base_http_http",
+    srcs = [
+        "include/perfetto/ext/base/http/http_server.h",
+        "include/perfetto/ext/base/http/sha1.h",
+    ],
+)
+
 # GN target: //include/perfetto/ext/base:base
 perfetto_filegroup(
     name = "include_perfetto_ext_base_base",
@@ -343,6 +353,7 @@
         "include/perfetto/ext/base/endian.h",
         "include/perfetto/ext/base/event_fd.h",
         "include/perfetto/ext/base/file_utils.h",
+        "include/perfetto/ext/base/flat_hash_map.h",
         "include/perfetto/ext/base/getopt.h",
         "include/perfetto/ext/base/getopt_compat.h",
         "include/perfetto/ext/base/hash.h",
@@ -355,6 +366,7 @@
         "include/perfetto/ext/base/pipe.h",
         "include/perfetto/ext/base/scoped_file.h",
         "include/perfetto/ext/base/small_set.h",
+        "include/perfetto/ext/base/small_vector.h",
         "include/perfetto/ext/base/string_splitter.h",
         "include/perfetto/ext/base/string_utils.h",
         "include/perfetto/ext/base/string_view.h",
@@ -639,6 +651,25 @@
     ],
 )
 
+# GN target: //src/base/http:http
+perfetto_cc_library(
+    name = "src_base_http_http",
+    srcs = [
+        "src/base/http/http_server.cc",
+        "src/base/http/sha1.cc",
+    ],
+    hdrs = [
+        ":include_perfetto_base_base",
+        ":include_perfetto_ext_base_base",
+        ":include_perfetto_ext_base_http_http",
+    ],
+    deps = [
+        ":src_base_base",
+        ":src_base_unix_socket",
+    ],
+    linkstatic = True,
+)
+
 # GN target: //src/base:base
 perfetto_cc_library(
     name = "src_base_base",
@@ -1376,8 +1407,6 @@
         "src/trace_processor/importers/json/json_trace_parser.h",
         "src/trace_processor/importers/json/json_trace_tokenizer.cc",
         "src/trace_processor/importers/json/json_trace_tokenizer.h",
-        "src/trace_processor/importers/json/json_tracker.cc",
-        "src/trace_processor/importers/json/json_tracker.h",
         "src/trace_processor/importers/proto/android_probes_module.cc",
         "src/trace_processor/importers/proto/android_probes_module.h",
         "src/trace_processor/importers/proto/android_probes_parser.cc",
@@ -1633,6 +1662,8 @@
     srcs = [
         "src/traced/probes/power/android_power_data_source.cc",
         "src/traced/probes/power/android_power_data_source.h",
+        "src/traced/probes/power/linux_power_sysfs_data_source.cc",
+        "src/traced/probes/power/linux_power_sysfs_data_source.h",
     ],
 )
 
@@ -1778,7 +1809,6 @@
 perfetto_filegroup(
     name = "src_tracing_ipc_common",
     srcs = [
-        "src/tracing/ipc/default_socket.cc",
         "src/tracing/ipc/memfd.cc",
         "src/tracing/ipc/memfd.h",
         "src/tracing/ipc/posix_shared_memory.cc",
@@ -1788,6 +1818,14 @@
     ],
 )
 
+# GN target: //src/tracing/ipc:default_socket
+perfetto_filegroup(
+    name = "src_tracing_ipc_default_socket",
+    srcs = [
+        "src/tracing/ipc/default_socket.cc",
+    ],
+)
+
 # GN target: //src/tracing:client_api_without_backends
 perfetto_filegroup(
     name = "src_tracing_client_api_without_backends",
@@ -2826,6 +2864,7 @@
         "protos/perfetto/trace/ftrace/sde.proto",
         "protos/perfetto/trace/ftrace/signal.proto",
         "protos/perfetto/trace/ftrace/sync.proto",
+        "protos/perfetto/trace/ftrace/synthetic.proto",
         "protos/perfetto/trace/ftrace/systrace.proto",
         "protos/perfetto/trace/ftrace/task.proto",
         "protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -3526,6 +3565,7 @@
         ":src_tracing_in_process_backend",
         ":src_tracing_ipc_common",
         ":src_tracing_ipc_consumer_consumer",
+        ":src_tracing_ipc_default_socket",
         ":src_tracing_ipc_producer_producer",
         ":src_tracing_ipc_service_service",
         ":src_tracing_platform_impl",
@@ -3618,6 +3658,7 @@
         ":src_tracing_core_core",
         ":src_tracing_ipc_common",
         ":src_tracing_ipc_consumer_consumer",
+        ":src_tracing_ipc_default_socket",
         ":src_tracing_ipc_producer_producer",
         "src/perfetto_cmd/main.cc",
     ],
@@ -3840,7 +3881,7 @@
                ":protos_perfetto_trace_track_event_zero",
                ":protozero",
                ":src_base_base",
-               ":src_base_unix_socket",
+               ":src_base_http_http",
                ":src_trace_processor_containers_containers",
                ":src_trace_processor_importers_gen_cc_chrome_track_event_descriptor",
                ":src_trace_processor_importers_gen_cc_config_descriptor",
@@ -4186,12 +4227,26 @@
     legacy_create_init = 0,
 )
 
+perfetto_py_library(
+    name = "batch_trace_processor",
+    srcs = glob([
+      "tools/batch_trace_processor/perfetto/batch_trace_processor/*.py"
+    ]),
+    deps = [
+        ":trace_processor_py",
+    ] + PERFETTO_CONFIG.deps.pandas_py,
+    imports = [
+        "tools/batch_trace_processor",
+    ],
+)
+
 perfetto_py_binary(
     name = "batch_trace_processor_shell",
     srcs = ["tools/batch_trace_processor/main.py"],
     main = "tools/batch_trace_processor/main.py",
     deps = [
         ":trace_processor_py",
+        ":batch_trace_processor",
     ] + PERFETTO_CONFIG.deps.pandas_py,
     python_version = "PY3",
     legacy_create_init = 0,
diff --git a/BUILD.extras b/BUILD.extras
index 28bec08..8c39316 100644
--- a/BUILD.extras
+++ b/BUILD.extras
@@ -142,12 +142,26 @@
     legacy_create_init = 0,
 )
 
+perfetto_py_library(
+    name = "batch_trace_processor",
+    srcs = glob([
+      "tools/batch_trace_processor/perfetto/batch_trace_processor/*.py"
+    ]),
+    deps = [
+        ":trace_processor_py",
+    ] + PERFETTO_CONFIG.deps.pandas_py,
+    imports = [
+        "tools/batch_trace_processor",
+    ],
+)
+
 perfetto_py_binary(
     name = "batch_trace_processor_shell",
     srcs = ["tools/batch_trace_processor/main.py"],
     main = "tools/batch_trace_processor/main.py",
     deps = [
         ":trace_processor_py",
+        ":batch_trace_processor",
     ] + PERFETTO_CONFIG.deps.pandas_py,
     python_version = "PY3",
     legacy_create_init = 0,
diff --git a/BUILD.gn b/BUILD.gn
index fe44ed2..b36d588 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -68,7 +68,10 @@
 }
 
 if (enable_perfetto_tools) {
-  all_targets += [ "tools" ]
+  all_targets += [
+    "tools",
+    "src/websocket_bridge",
+  ]
 }
 
 if (enable_perfetto_unittests) {
diff --git a/CHANGELOG b/CHANGELOG
index e316684..0f2befe 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,10 +4,15 @@
     * Changed compiler flags. Assume recent x64 CPUs (-msse4.2 -mavx -mpopcnt).
       This behavior affects only standalone builds and can be changed by setting
       enable_perfetto_x64_cpu_opt=false in the GN args.
+    * The java heap profiler now rescans all the processes every time for
+      continous_dump_config. The scan_pids_only_on_start can be used to restore
+      the old behavior.
+    * Added support for building on ARM Macs
   Trace Processor:
     * Changed LIKE comparisions to be case-senstive. This may break existing
       queries but was a necessary from a performance perspective.
     * Changed compiler flags, assume recent x64 CPUs (see above).
+    * Changed how displayTimeUnit is handled in JSON traces to match catapult.
   UI:
     *
   SDK:
diff --git a/docs/analysis/metrics.md b/docs/analysis/metrics.md
index 9e14b89..2602e88 100644
--- a/docs/analysis/metrics.md
+++ b/docs/analysis/metrics.md
@@ -63,30 +63,103 @@
 }
 ```
 
-### Case for upstreaming
+## Metric development guide
 
-NOTE: Googlers: for internal usage of metrics in Google3 (i.e. metrics which are
-confidential), please see [this internal page](https://goto.google.com/viecd).
+As metric writing requires a lot of iterations to get right, there are several
+tips which make the experience a lot smoother.
 
-Authors are strongly encouraged to add all metrics derived on Perfetto traces to
-the Perfetto repo unless there is a clear usecase (e.g. confidentiality) why
-these metrics should not be publicly available.
+### Hot reloading metrics
+To obtain the fastest possible iteration time when developing metrics,
+it's possible to hot reload any changes to SQL; this will skip over both
+recompilation (for builtin metrics) and trace load (for both builtin and
+custom metrics).
 
-In return for upstreaming metrics, authors will have first class support for
-running metrics locally and the confidence that their metrics will remain stable
-as trace processor is developed.
+To do this, trace processor is started in *interactive mode* while
+still specifying command line flags about which metrics should be run and
+the paths of any extensions. Then, in the REPL shell, the commands
+`.load-metrics-sql` (which causes any SQL on disk to be re-read) and
+`.run-metrics` (to run the metrics and print the result).
 
-As well as scaling upwards while developing from running on a single trace
-locally to running on a large set of traces, the reverse is also very useful.
-When an anomaly is observed in the metrics of a lab benchmark, a representative
-trace can be downloaded and the same metric can be run locally in trace
-processor.
+For example, suppose we want to iterate on the `android_startup` metric. We
+can run the following commands from a Perfetto checkout:
+```python
+> ./tools/trace_processor --interactive \
+  --run_metrics android_startup \
+  --metric-extension src/trace_processor/metric@/
+  --dev \
+  <trace>
+android_startup {
+  <contents of startup metric>
+}
 
-Since the same code is running locally and remotely, developers can be confident
-in reproducing the issue and use the trace processor and/or the Perfetto UI to
-identify the problem.
+# Now make any changes you want to the SQL files related to the startup
+# metric. Even adding new files in the src/trace_processor/metric works.
 
-## Metric Helper Functions
+# Then, we can reload the changes using `.load-metrics-sql`.
+> .load-metrics-sql
+
+# We can rerun the changed metric using `.run-metrics`
+> .run-metrics
+android_startup {
+  <contents of changed startup metric>
+}
+```
+
+NOTE: see below about why `--dev` was required for this command.
+
+This also works for custom metrics specified on the command line:
+```python
+> ./tools/trace_processor -i --run_metrics /tmp/my_custom_metric.sql <trace>
+my_custom_metric {
+  <contents of my_custom_metric>
+}
+
+# Change the SQL file as before.
+
+> .load-metrics-sql
+> .run-metrics
+my_custom_metric {
+  <contents of changed my_custom_metric>
+}
+```
+
+WARNING: it is currently not possible to reload protos in the same way. If
+protos are changed, a recompile (for built-in metrics) and reinvoking
+trace processor is necessary to pick up the changes.
+
+WARNING: Deleted files from `--metric-extension` folders are *not* removed
+and will remain available e.g. to RUN_METRIC invocations.
+
+### Modifying built-in metric SQL without recompiling
+It is possible to override the SQL of built-in metrics at runtime without
+needing to recompile trace processor. To do this, the flag `--metric-extension`
+needs to be specified with the disk path where the built-metrics live and the
+special string `/` for the virtual path.
+
+For example, from inside a Perfetto checkout:
+```python
+> ./tools/trace_processor \
+  --run_metrics android_cpu \
+  --metric-extension src/trace_processor/metrics@/
+  --dev
+  <trace>
+```
+This will run the CPU metric using the live SQL in the repo *not* the SQL
+defintion built into the binary.
+
+NOTE: protos are *not* overriden in the same way - if any proto messages are
+changed a recompile of trace processor is required for the changes to be
+available.
+
+NOTE: the `--dev` flag is required for the use of this feature. This
+flag ensures that this feature is not accidentally in production as it is only
+intended for local development.
+
+WARNING: protos are *not* overriden in the same way - if any proto messages are
+changed a recompile of trace processor is required for the changes to be
+available.
+
+## Metric helper functions
 
 There are several useful helpers functions which are available when writing a metric.
 
@@ -493,7 +566,30 @@
 }
 ```
 
-### Next steps
+## Next steps
 
 * The [common tasks](/docs/contributing/common-tasks.md) page gives a list of
   steps on how new metrics can be added to the trace processor.
+
+## Appendix: Case for upstreaming
+
+NOTE: Googlers: for internal usage of metrics in Google3 (i.e. metrics which are
+confidential), please see [this internal page](https://goto.google.com/viecd).
+
+Authors are strongly encouraged to add all metrics derived on Perfetto traces to
+the Perfetto repo unless there is a clear usecase (e.g. confidentiality) why
+these metrics should not be publicly available.
+
+In return for upstreaming metrics, authors will have first class support for
+running metrics locally and the confidence that their metrics will remain stable
+as trace processor is developed.
+
+As well as scaling upwards while developing from running on a single trace
+locally to running on a large set of traces, the reverse is also very useful.
+When an anomaly is observed in the metrics of a lab benchmark, a representative
+trace can be downloaded and the same metric can be run locally in trace
+processor.
+
+Since the same code is running locally and remotely, developers can be confident
+in reproducing the issue and use the trace processor and/or the Perfetto UI to
+identify the problem.
diff --git a/docs/contributing/common-tasks.md b/docs/contributing/common-tasks.md
index d0724c9..39a36c7 100644
--- a/docs/contributing/common-tasks.md
+++ b/docs/contributing/common-tasks.md
@@ -78,6 +78,7 @@
 | `dur`        | `int64`  | Mandatory for slice, NULL for counter | The duration of the slice                                    |
 | `slice_name` | `string` | Mandatory for slice, NULL for counter | The name of the slice                                        |
 | `value`      | `double` | Mandatory for counter, NULL for slice | The value of the counter                                     |
+| `group_name` | `string` | Optional                              | Name of the track group under which the track appears. All tracks with the same `group_name` are placed under the same group by that name. Tracks that lack this field or have NULL value in this field are displayed without any grouping. |
 
 #### Known issues:
 
diff --git a/docs/data-sources/battery-counters.md b/docs/data-sources/battery-counters.md
index 89f6668..5cc837c 100644
--- a/docs/data-sources/battery-counters.md
+++ b/docs/data-sources/battery-counters.md
@@ -85,7 +85,7 @@
 Config proto:
 [AndroidPowerConfig](/docs/reference/trace-config-proto.autogen#AndroidPowerConfig)
 
-Sample config:
+Sample config (Android):
 
 ```protobuf
 data_sources: {
@@ -101,6 +101,16 @@
 }
 ```
 
+Sample Config (Chrome OS or Linux):
+
+```protobuf
+data_sources: {
+    config {
+        name: "linux.sysfs_power"
+    }
+}
+```
+
 ## Power rails
 
 _This data source has been introduced in Android 10 (Q) and requires the
diff --git a/docs/data-sources/java-heap-profiler.md b/docs/data-sources/java-heap-profiler.md
index f03977e..1187b7a 100644
--- a/docs/data-sources/java-heap-profiler.md
+++ b/docs/data-sources/java-heap-profiler.md
@@ -21,6 +21,10 @@
 
 ![Flamegraph of a Java heap profiler](/docs/images/java-flamegraph.png)
 
+The native size of certain objects is represented as an extra child node in the
+flamegraph, prefixed with "[native]". The extra node counts as an extra object.
+This is available only on Android T+.
+
 ## SQL
 
 Information about the Java Heap is written to the following tables:
@@ -29,6 +33,9 @@
 * [`heap_graph_object`](/docs/analysis/sql-tables.autogen#heap_graph_object)
 * [`heap_graph_reference`](/docs/analysis/sql-tables.autogen#heap_graph_reference)
 
+`native_size` (available only on Android T+) is extracted from the related
+`libcore.util.NativeAllocationRegistry` and is not included in `self_size`.
+
 For instance, to get the bytes used by class name, run the following query.
 As-is this query will often return un-actionable information, as most of the
 bytes in the Java heap end up being primitive arrays or strings.
diff --git a/docs/visualization/deep-linking-to-perfetto-ui.md b/docs/visualization/deep-linking-to-perfetto-ui.md
index f6a9225..5f0148f 100644
--- a/docs/visualization/deep-linking-to-perfetto-ui.md
+++ b/docs/visualization/deep-linking-to-perfetto-ui.md
@@ -64,9 +64,9 @@
 
 ### Code samples
 
-See https://jsfiddle.net/primiano/1hd0a4wj/68/ (also mirrored on
-[this GitHub gist](https://gist.github.com/primiano/e164868b617844ef8fa4770eb3b323b9)
-)
+See [this example caller](https://gistcdn.rawgit.org/primiano/e164868b617844ef8fa4770eb3b323b9/1d9aa2bf52cf903709ea7dd4d583fd2d07d7a255/open_with_perfetto_ui.html),
+for which the code is in
+[this GitHub gist](https://gist.github.com/primiano/e164868b617844ef8fa4770eb3b323b9).
 
 Googlers: take a look at the
 [existing examples in the internal codesearch](http://go/perfetto-ui-deeplink-cs)
diff --git a/gn/standalone/toolchain/BUILD.gn b/gn/standalone/toolchain/BUILD.gn
index 8dba119..408f1f6 100644
--- a/gn/standalone/toolchain/BUILD.gn
+++ b/gn/standalone/toolchain/BUILD.gn
@@ -119,6 +119,8 @@
     _target_triplet = "x86_64-apple-darwin"
   } else if (target_os == "mac" && target_cpu == "x86") {
     _target_triplet = "i686-apple-darwin"
+  } else if (target_os == "mac" && target_cpu == "arm64") {
+    _target_triplet = "aarch64-apple-darwin"
   } else if (target_os == "linux" && target_cpu == "arm64") {
     _target_triplet = "aarch64-linux-gnu"
     _default_target_sysroot =
diff --git a/include/perfetto/ext/base/BUILD.gn b/include/perfetto/ext/base/BUILD.gn
index 34e09de..91dfa72 100644
--- a/include/perfetto/ext/base/BUILD.gn
+++ b/include/perfetto/ext/base/BUILD.gn
@@ -25,6 +25,7 @@
     "endian.h",
     "event_fd.h",
     "file_utils.h",
+    "flat_hash_map.h",
     "getopt.h",
     "getopt_compat.h",
     "hash.h",
@@ -37,6 +38,7 @@
     "pipe.h",
     "scoped_file.h",
     "small_set.h",
+    "small_vector.h",
     "string_splitter.h",
     "string_utils.h",
     "string_view.h",
diff --git a/include/perfetto/ext/base/crash_keys.h b/include/perfetto/ext/base/crash_keys.h
index 1d550f6..36ef358 100644
--- a/include/perfetto/ext/base/crash_keys.h
+++ b/include/perfetto/ext/base/crash_keys.h
@@ -79,7 +79,7 @@
     ScopedClear(const ScopedClear&) = delete;
     ScopedClear& operator=(const ScopedClear&) = delete;
     ScopedClear& operator=(ScopedClear&&) = delete;
-    ScopedClear(ScopedClear&& other) : key_(other.key_) {
+    ScopedClear(ScopedClear&& other) noexcept : key_(other.key_) {
       other.key_ = nullptr;
     }
 
@@ -100,22 +100,23 @@
   enum class Type : uint8_t { kUnset = 0, kInt, kStr };
 
   void Clear() {
-    int_value_ = 0;
-    type_ = Type::kUnset;
+    int_value_.store(0, std::memory_order_relaxed);
+    type_.store(Type::kUnset, std::memory_order_relaxed);
   }
 
   void Set(int64_t value) {
-    int_value_ = value;
-    type_ = Type::kInt;
+    int_value_.store(value, std::memory_order_relaxed);
+    type_.store(Type::kInt, std::memory_order_relaxed);
     if (PERFETTO_UNLIKELY(!registered_.load(std::memory_order_relaxed)))
       Register();
   }
 
   void Set(StringView sv) {
     size_t len = std::min(sv.size(), sizeof(str_value_) - 1);
-    memcpy(str_value_, sv.data(), len);
-    str_value_[len] = '\0';
-    type_ = Type::kStr;
+    for (size_t i = 0; i < len; ++i)
+      str_value_[i].store(sv.data()[i], std::memory_order_relaxed);
+    str_value_[len].store('\0', std::memory_order_relaxed);
+    type_.store(Type::kStr, std::memory_order_relaxed);
     if (PERFETTO_UNLIKELY(!registered_.load(std::memory_order_relaxed)))
       Register();
   }
@@ -130,18 +131,20 @@
     return ScopedClear(this);
   }
 
-  int64_t int_value() const { return int_value_; }
+  void Register();
+
+  int64_t int_value() const {
+    return int_value_.load(std::memory_order_relaxed);
+  }
   size_t ToString(char* dst, size_t len);
 
  private:
-  void Register();
-
   std::atomic<bool> registered_;
-  Type type_;
+  std::atomic<Type> type_;
   const char* const name_;
   union {
-    char str_value_[kCrashKeyMaxStrSize];
-    int64_t int_value_;
+    std::atomic<char> str_value_[kCrashKeyMaxStrSize];
+    std::atomic<int64_t> int_value_;
   };
 };
 
diff --git a/include/perfetto/ext/base/flat_hash_map.h b/include/perfetto/ext/base/flat_hash_map.h
new file mode 100644
index 0000000..7759dbb
--- /dev/null
+++ b/include/perfetto/ext/base/flat_hash_map.h
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2021 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 INCLUDE_PERFETTO_EXT_BASE_FLAT_HASH_MAP_H_
+#define INCLUDE_PERFETTO_EXT_BASE_FLAT_HASH_MAP_H_
+
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/utils.h"
+
+#include <algorithm>
+#include <functional>
+#include <limits>
+
+namespace perfetto {
+namespace base {
+
+// An open-addressing hashmap implementation.
+// Pointers are not stable, neither for keys nor values.
+// Has similar performances of a RobinHood hash (without the complications)
+// and 2x an unordered map.
+// Doc: http://go/perfetto-hashtables .
+//
+// When used to implement a string pool in TraceProcessor, the performance
+// characteristics obtained by replaying the set of strings seeen in a 4GB trace
+// (226M strings, 1M unique) are the following (see flat_hash_map_benchmark.cc):
+// This(Linear+AppendOnly)    879,383,676 ns    258.013M insertions/s
+// This(LinearProbe):         909,206,047 ns    249.546M insertions/s
+// This(QuadraticProbe):    1,083,844,388 ns    209.363M insertions/s
+// std::unordered_map:      6,203,351,870 ns    36.5811M insertions/s
+// tsl::robin_map:            931,403,397 ns    243.622M insertions/s
+// absl::flat_hash_map:       998,013,459 ns    227.379M insertions/s
+// FollyF14FastMap:         1,181,480,602 ns    192.074M insertions/s
+
+// The structs below define the probing algorithm used to probe slots upon a
+// collision. They are guaranteed to visit all slots as our table size is always
+// a power of two (see https://en.wikipedia.org/wiki/Quadratic_probing).
+
+// Linear probing can be faster if the hashing is well distributed and the load
+// is not high. For TraceProcessor's StringPool this is the fastest. It can
+// degenerate badly if the hashing doesn't spread (e.g., if using directly pids
+// as keys, with a no-op hashing function).
+struct LinearProbe {
+  static inline size_t Calc(size_t key_hash, size_t step, size_t capacity) {
+    return (key_hash + step) & (capacity - 1);  // Linear probe
+  }
+};
+
+// Generates the sequence: 0, 3, 10, 21, 36, 55, ...
+// Can be a bit (~5%) slower than LinearProbe because it's less cache hot, but
+// avoids degenerating badly if the hash function is bad and causes clusters.
+// A good default choice unless benchmarks prove otherwise.
+struct QuadraticProbe {
+  static inline size_t Calc(size_t key_hash, size_t step, size_t capacity) {
+    return (key_hash + 2 * step * step + step) & (capacity - 1);
+  }
+};
+
+// Tends to perform in the middle between linear and quadratic.
+// It's a bit more cache-effective than the QuadraticProbe but can create more
+// clustering if the hash function doesn't spread well.
+// Generates the sequence: 0, 1, 3, 6, 10, 15, 21, ...
+struct QuadraticHalfProbe {
+  static inline size_t Calc(size_t key_hash, size_t step, size_t capacity) {
+    return (key_hash + (step * step + step) / 2) & (capacity - 1);
+  }
+};
+
+template <typename Key,
+          typename Value,
+          typename Hasher = std::hash<Key>,
+          typename Probe = QuadraticProbe,
+          bool AppendOnly = false>
+class FlatHashMap {
+ public:
+  class Iterator {
+   public:
+    explicit Iterator(const FlatHashMap* map) : map_(map) { FindNextNonFree(); }
+    ~Iterator() = default;
+    Iterator(const Iterator&) = default;
+    Iterator& operator=(const Iterator&) = default;
+    Iterator(Iterator&&) noexcept = default;
+    Iterator& operator=(Iterator&&) noexcept = default;
+
+    Key& key() { return map_->keys_[idx_]; }
+    Value& value() { return map_->values_[idx_]; }
+    const Key& key() const { return map_->keys_[idx_]; }
+    const Value& value() const { return map_->values_[idx_]; }
+
+    explicit operator bool() const { return idx_ != kEnd; }
+    Iterator& operator++() {
+      PERFETTO_DCHECK(idx_ < map_->capacity_);
+      ++idx_;
+      FindNextNonFree();
+      return *this;
+    }
+
+   private:
+    static constexpr size_t kEnd = std::numeric_limits<size_t>::max();
+
+    void FindNextNonFree() {
+      const auto& tags = map_->tags_;
+      for (; idx_ < map_->capacity_; idx_++) {
+        if (tags[idx_] != kFreeSlot && (AppendOnly || tags[idx_] != kTombstone))
+          return;
+      }
+      idx_ = kEnd;
+    }
+
+    const FlatHashMap* map_ = nullptr;
+    size_t idx_ = 0;
+  };  // Iterator
+
+  static constexpr int kDefaultLoadLimitPct = 75;
+  explicit FlatHashMap(size_t initial_capacity = 0,
+                       int load_limit_pct = kDefaultLoadLimitPct)
+      : load_limit_percent_(load_limit_pct) {
+    if (initial_capacity > 0)
+      Reset(initial_capacity);
+  }
+
+  // We are calling Clear() so that the destructors for the inserted entries are
+  // called (unless they are trivial, in which case it will be a no-op).
+  ~FlatHashMap() { Clear(); }
+
+  FlatHashMap(FlatHashMap&& other) noexcept {
+    tags_ = std::move(other.tags_);
+    keys_ = std::move(other.keys_);
+    values_ = std::move(other.values_);
+    capacity_ = other.capacity_;
+    size_ = other.size_;
+    max_probe_length_ = other.max_probe_length_;
+    load_limit_ = other.load_limit_;
+    load_limit_percent_ = other.load_limit_percent_;
+
+    new (&other) FlatHashMap();
+  }
+
+  FlatHashMap& operator=(FlatHashMap&& other) noexcept {
+    this->~FlatHashMap();
+    new (this) FlatHashMap(std::move(other));
+    return *this;
+  }
+
+  FlatHashMap(const FlatHashMap&) = delete;
+  FlatHashMap& operator=(const FlatHashMap&) = delete;
+
+  std::pair<Value*, bool> Insert(Key key, Value value) {
+    const size_t key_hash = Hasher{}(key);
+    const uint8_t tag = HashToTag(key_hash);
+    static constexpr size_t kSlotNotFound = std::numeric_limits<size_t>::max();
+
+    // This for loop does in reality at most two attempts:
+    // The first iteration either:
+    //  - Early-returns, because the key exists already,
+    //  - Finds an insertion slot and proceeds because the load is < limit.
+    // The second iteration is only hit in the unlikely case of this insertion
+    // bringing the table beyond the target |load_limit_| (or the edge case
+    // of the HT being full, if |load_limit_pct_| = 100).
+    // We cannot simply pre-grow the table before insertion, because we must
+    // guarantee that calling Insert() with a key that already exists doesn't
+    // invalidate iterators.
+    size_t insertion_slot;
+    size_t probe_len;
+    for (;;) {
+      PERFETTO_DCHECK((capacity_ & (capacity_ - 1)) == 0);  // Must be a pow2.
+      insertion_slot = kSlotNotFound;
+      // Start the iteration at the desired slot (key_hash % capacity_)
+      // searching either for a free slot or a tombstone. In the worst case we
+      // might end up scanning the whole array of slots. The Probe functions are
+      // guaranteed to visit all the slots within |capacity_| steps. If we find
+      // a free slot, we can stop the search immediately (a free slot acts as an
+      // "end of chain for entries having the same hash". If we find a
+      // tombstones (a deleted slot) we remember its position, but have to keep
+      // searching until a free slot to make sure we don't insert a duplicate
+      // key.
+      for (probe_len = 0; probe_len < capacity_;) {
+        const size_t idx = Probe::Calc(key_hash, probe_len, capacity_);
+        PERFETTO_DCHECK(idx < capacity_);
+        const uint8_t tag_idx = tags_[idx];
+        ++probe_len;
+        if (tag_idx == kFreeSlot) {
+          // Rationale for "insertion_slot == kSlotNotFound": if we encountered
+          // a tombstone while iterating we should reuse that rather than
+          // taking another slot.
+          if (AppendOnly || insertion_slot == kSlotNotFound)
+            insertion_slot = idx;
+          break;
+        }
+        // We should never encounter tombstones in AppendOnly mode.
+        PERFETTO_DCHECK(!(tag_idx == kTombstone && AppendOnly));
+        if (!AppendOnly && tag_idx == kTombstone) {
+          insertion_slot = idx;
+          continue;
+        }
+        if (tag_idx == tag && keys_[idx] == key) {
+          // The key is already in the map.
+          return std::make_pair(&values_[idx], false);
+        }
+      }  // for (idx)
+
+      // If we got to this point the key does not exist (otherwise we would have
+      // hit the the return above) and we are going to insert a new entry.
+      // Before doing so, ensure we stay under the target load limit.
+      if (PERFETTO_UNLIKELY(size_ >= load_limit_)) {
+        MaybeGrowAndRehash(/*grow=*/true);
+        continue;
+      }
+      PERFETTO_DCHECK(insertion_slot != kSlotNotFound);
+      break;
+    }  // for (attempt)
+
+    PERFETTO_CHECK(insertion_slot < capacity_);
+
+    // We found a free slot (or a tombstone). Proceed with the insertion.
+    Value* value_idx = &values_[insertion_slot];
+    new (&keys_[insertion_slot]) Key(std::move(key));
+    new (value_idx) Value(std::move(value));
+    tags_[insertion_slot] = tag;
+    PERFETTO_DCHECK(probe_len > 0 && probe_len <= capacity_);
+    max_probe_length_ = std::max(max_probe_length_, probe_len);
+    size_++;
+
+    return std::make_pair(value_idx, true);
+  }
+
+  Value* Find(const Key& key) const {
+    const size_t idx = FindInternal(key);
+    if (idx == kNotFound)
+      return nullptr;
+    return &values_[idx];
+  }
+
+  bool Erase(const Key& key) {
+    if (AppendOnly)
+      PERFETTO_FATAL("Erase() not supported because AppendOnly=true");
+    size_t idx = FindInternal(key);
+    if (idx == kNotFound)
+      return false;
+    EraseInternal(idx);
+    return true;
+  }
+
+  void Clear() {
+    // Avoid trivial heap operations on zero-capacity std::move()-d objects.
+    if (PERFETTO_UNLIKELY(capacity_ == 0))
+      return;
+
+    for (size_t i = 0; i < capacity_; ++i) {
+      const uint8_t tag = tags_[i];
+      if (tag != kFreeSlot && tag != kTombstone)
+        EraseInternal(i);
+    }
+    // Clear all tombstones. We really need to do this for AppendOnly.
+    MaybeGrowAndRehash(/*grow=*/false);
+  }
+
+  Value& operator[](Key key) {
+    auto it_and_inserted = Insert(std::move(key), Value{});
+    return *it_and_inserted.first;
+  }
+
+  Iterator GetIterator() { return Iterator(this); }
+  const Iterator GetIterator() const { return Iterator(this); }
+
+  size_t size() const { return size_; }
+  size_t capacity() const { return capacity_; }
+
+  // "protected" here is only for the flat_hash_map_benchmark.cc. Everything
+  // below is by all means private.
+ protected:
+  enum ReservedTags : uint8_t { kFreeSlot = 0, kTombstone = 1 };
+  static constexpr size_t kNotFound = std::numeric_limits<size_t>::max();
+
+  size_t FindInternal(const Key& key) const {
+    const size_t key_hash = Hasher{}(key);
+    const uint8_t tag = HashToTag(key_hash);
+    PERFETTO_DCHECK((capacity_ & (capacity_ - 1)) == 0);  // Must be a pow2.
+    PERFETTO_DCHECK(max_probe_length_ <= capacity_);
+    for (size_t i = 0; i < max_probe_length_; ++i) {
+      const size_t idx = Probe::Calc(key_hash, i, capacity_);
+      const uint8_t tag_idx = tags_[idx];
+
+      if (tag_idx == kFreeSlot)
+        return kNotFound;
+      // HashToTag() never returns kTombstone, so the tag-check below cannot
+      // possibly match. Also we just want to skip tombstones.
+      if (tag_idx == tag && keys_[idx] == key) {
+        PERFETTO_DCHECK(tag_idx > kTombstone);
+        return idx;
+      }
+    }  // for (idx)
+    return kNotFound;
+  }
+
+  void EraseInternal(size_t idx) {
+    PERFETTO_DCHECK(tags_[idx] > kTombstone);
+    PERFETTO_DCHECK(size_ > 0);
+    tags_[idx] = kTombstone;
+    keys_[idx].~Key();
+    values_[idx].~Value();
+    size_--;
+  }
+
+  PERFETTO_NO_INLINE void MaybeGrowAndRehash(bool grow) {
+    PERFETTO_DCHECK(size_ <= capacity_);
+    const size_t old_capacity = capacity_;
+
+    // Grow quickly up to 1MB, then chill.
+    const size_t old_size_bytes = old_capacity * (sizeof(Key) + sizeof(Value));
+    const size_t grow_factor = old_size_bytes < (1024u * 1024u) ? 8 : 2;
+    const size_t new_capacity =
+        grow ? std::max(old_capacity * grow_factor, size_t(1024))
+             : old_capacity;
+
+    auto old_tags(std::move(tags_));
+    auto old_keys(std::move(keys_));
+    auto old_values(std::move(values_));
+    size_t old_size = size_;
+
+    // This must be a CHECK (i.e. not just a DCHECK) to prevent UAF attacks on
+    // 32-bit archs that try to double the size of the table until wrapping.
+    PERFETTO_CHECK(new_capacity >= old_capacity);
+    Reset(new_capacity);
+
+    size_t new_size = 0;  // Recompute the size.
+    for (size_t i = 0; i < old_capacity; ++i) {
+      const uint8_t old_tag = old_tags[i];
+      if (old_tag != kFreeSlot && old_tag != kTombstone) {
+        Insert(std::move(old_keys[i]), std::move(old_values[i]));
+        old_keys[i].~Key();  // Destroy the old objects.
+        old_values[i].~Value();
+        new_size++;
+      }
+    }
+    PERFETTO_DCHECK(new_size == old_size);
+    size_ = new_size;
+  }
+
+  // Doesn't call destructors. Use Clear() for that.
+  PERFETTO_NO_INLINE void Reset(size_t n) {
+    PERFETTO_DCHECK((n & (n - 1)) == 0);  // Must be a pow2.
+
+    capacity_ = n;
+    max_probe_length_ = 0;
+    size_ = 0;
+    load_limit_ = n * static_cast<size_t>(load_limit_percent_) / 100;
+    load_limit_ = std::min(load_limit_, n);
+
+    tags_.reset(new uint8_t[n]);
+    memset(&tags_[0], 0, n);                  // Clear all tags.
+    keys_ = AlignedAllocTyped<Key[]>(n);      // Deliberately not 0-initialized.
+    values_ = AlignedAllocTyped<Value[]>(n);  // Deliberately not 0-initialized.
+  }
+
+  static inline uint8_t HashToTag(size_t full_hash) {
+    uint8_t tag = full_hash >> (sizeof(full_hash) * 8 - 8);
+    // Ensure the hash is always >= 2. We use 0, 1 for kFreeSlot and kTombstone.
+    tag += (tag <= kTombstone) << 1;
+    PERFETTO_DCHECK(tag > kTombstone);
+    return tag;
+  }
+
+  size_t capacity_ = 0;
+  size_t size_ = 0;
+  size_t max_probe_length_ = 0;
+  size_t load_limit_ = 0;  // Updated every time |capacity_| changes.
+  int load_limit_percent_ =
+      kDefaultLoadLimitPct;  // Load factor limit in % of |capacity_|.
+
+  // These arrays have always the |capacity_| elements.
+  // Note: AlignedUniquePtr just allocates memory, doesn't invoke any ctor/dtor.
+  std::unique_ptr<uint8_t[]> tags_;
+  AlignedUniquePtr<Key[]> keys_;
+  AlignedUniquePtr<Value[]> values_;
+};
+
+}  // namespace base
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_EXT_BASE_FLAT_HASH_MAP_H_
diff --git a/include/perfetto/ext/base/http/BUILD.gn b/include/perfetto/ext/base/http/BUILD.gn
new file mode 100644
index 0000000..3e42607
--- /dev/null
+++ b/include/perfetto/ext/base/http/BUILD.gn
@@ -0,0 +1,21 @@
+# 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.
+
+source_set("http") {
+  sources = [
+    "http_server.h",
+    "sha1.h",
+  ]
+  public_deps = [ "..:base" ]
+}
diff --git a/include/perfetto/ext/base/http/http_server.h b/include/perfetto/ext/base/http/http_server.h
new file mode 100644
index 0000000..c251061
--- /dev/null
+++ b/include/perfetto/ext/base/http/http_server.h
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2021 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 INCLUDE_PERFETTO_EXT_BASE_HTTP_HTTP_SERVER_H_
+#define INCLUDE_PERFETTO_EXT_BASE_HTTP_HTTP_SERVER_H_
+
+#include <array>
+#include <initializer_list>
+#include <list>
+#include <memory>
+#include <string>
+
+#include "perfetto/base/task_runner.h"
+#include "perfetto/ext/base/optional.h"
+#include "perfetto/ext/base/paged_memory.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/ext/base/unix_socket.h"
+
+namespace perfetto {
+namespace base {
+
+class HttpServerConnection;
+
+struct HttpRequest {
+  explicit HttpRequest(HttpServerConnection* c) : conn(c) {}
+
+  Optional<StringView> GetHeader(StringView name) const;
+
+  HttpServerConnection* conn;
+
+  // These StringViews point to memory in the rxbuf owned by |conn|. They are
+  // valid only within the OnHttpRequest() call.
+  StringView method;
+  StringView uri;
+  StringView origin;
+  StringView body;
+  bool is_websocket_handshake = false;
+
+ private:
+  friend class HttpServer;
+  struct Header {
+    StringView name;
+    StringView value;
+  };
+
+  static constexpr uint32_t kMaxHeaders = 32;
+  std::array<Header, kMaxHeaders> headers{};
+  size_t num_headers = 0;
+};
+
+struct WebsocketMessage {
+  explicit WebsocketMessage(HttpServerConnection* c) : conn(c) {}
+
+  HttpServerConnection* conn;
+
+  // Note: message boundaries are not respected in case of fragmentation.
+  // This websocket implementation preserves only the byte stream, but not the
+  // atomicity of inbound messages (like SOCK_STREAM, unlike SOCK_DGRAM).
+  // Holds onto the connection's |rxbuf|. This is valid only within the scope
+  // of the OnWebsocketMessage() callback.
+  StringView data;
+
+  // If false the payload contains binary data. If true it's supposed to contain
+  // text. Note that there is no guarantee this will be the case. This merely
+  // reflect the opcode that the client sets on each message.
+  bool is_text = false;
+};
+
+class HttpServerConnection {
+ public:
+  static constexpr size_t kOmitContentLength = static_cast<size_t>(-1);
+
+  explicit HttpServerConnection(std::unique_ptr<UnixSocket>);
+  ~HttpServerConnection();
+
+  void SendResponseHeaders(const char* http_code,
+                           std::initializer_list<const char*> headers = {},
+                           size_t content_length = 0);
+
+  // Works also for websockets.
+  void SendResponseBody(const void* content, size_t content_length);
+  void Close();
+
+  // All the above in one shot.
+  void SendResponse(const char* http_code,
+                    std::initializer_list<const char*> headers = {},
+                    StringView content = {},
+                    bool force_close = false);
+  void SendResponseAndClose(const char* http_code,
+                            std::initializer_list<const char*> headers = {},
+                            StringView content = {}) {
+    SendResponse(http_code, headers, content, true);
+  }
+
+  // The metods below are only valid for websocket connections.
+
+  // Upgrade an existing connection to a websocket. This can be called only in
+  // the context of OnHttpRequest(req) if req.is_websocket_handshake == true.
+  // If the origin is not in the |allowed_origins_|, the request will fail with
+  // a 403 error (this is because there is no browser-side CORS support for
+  // websockets).
+  void UpgradeToWebsocket(const HttpRequest&);
+  void SendWebsocketMessage(const void* data, size_t len);
+  void SendWebsocketMessage(StringView sv) {
+    SendWebsocketMessage(sv.data(), sv.size());
+  }
+  void SendWebsocketFrame(uint8_t opcode,
+                          const void* payload,
+                          size_t payload_len);
+
+  bool is_websocket() const { return is_websocket_; }
+
+ private:
+  friend class HttpServer;
+
+  size_t rxbuf_avail() { return rxbuf.size() - rxbuf_used; }
+
+  std::unique_ptr<UnixSocket> sock;
+  PagedMemory rxbuf;
+  size_t rxbuf_used = 0;
+  bool is_websocket_ = false;
+  bool headers_sent_ = false;
+  size_t content_len_headers_ = 0;
+  size_t content_len_actual_ = 0;
+
+  // If the origin is in the server's |allowed_origins_| this contains the
+  // origin itself. This is used to handle CORS headers.
+  std::string origin_allowed_;
+
+  // By default treat connections as keep-alive unless the client says
+  // explicitly 'Connection: close'. This improves TraceProcessor's python API.
+  // This is consistent with that nginx does.
+  bool keepalive_ = true;
+};
+
+class HttpRequestHandler {
+ public:
+  virtual ~HttpRequestHandler();
+  virtual void OnHttpRequest(const HttpRequest&) = 0;
+  virtual void OnWebsocketMessage(const WebsocketMessage&);
+  virtual void OnHttpConnectionClosed(HttpServerConnection*);
+};
+
+class HttpServer : public UnixSocket::EventListener {
+ public:
+  HttpServer(TaskRunner*, HttpRequestHandler*);
+  ~HttpServer() override;
+  void Start(int port);
+  void AddAllowedOrigin(const std::string&);
+
+ private:
+  size_t ParseOneHttpRequest(HttpServerConnection*);
+  size_t ParseOneWebsocketFrame(HttpServerConnection*);
+  void HandleCorsPreflightRequest(const HttpRequest&);
+  bool IsOriginAllowed(StringView);
+
+  // UnixSocket::EventListener implementation.
+  void OnNewIncomingConnection(UnixSocket*,
+                               std::unique_ptr<UnixSocket>) override;
+  void OnConnect(UnixSocket* self, bool connected) override;
+  void OnDisconnect(UnixSocket* self) override;
+  void OnDataAvailable(UnixSocket* self) override;
+
+  TaskRunner* const task_runner_;
+  HttpRequestHandler* req_handler_;
+  std::unique_ptr<UnixSocket> sock4_;
+  std::unique_ptr<UnixSocket> sock6_;
+  std::list<HttpServerConnection> clients_;
+  std::list<std::string> allowed_origins_;
+  bool origin_error_logged_ = false;
+};
+
+}  // namespace base
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_EXT_BASE_HTTP_HTTP_SERVER_H_
diff --git a/include/perfetto/ext/base/http/sha1.h b/include/perfetto/ext/base/http/sha1.h
new file mode 100644
index 0000000..c583d69
--- /dev/null
+++ b/include/perfetto/ext/base/http/sha1.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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 INCLUDE_PERFETTO_EXT_BASE_HTTP_SHA1_H_
+#define INCLUDE_PERFETTO_EXT_BASE_HTTP_SHA1_H_
+
+#include <stddef.h>
+
+#include <array>
+#include <string>
+
+namespace perfetto {
+namespace base {
+
+constexpr size_t kSHA1Length = 20;
+using SHA1Digest = std::array<uint8_t, kSHA1Length>;
+
+SHA1Digest SHA1Hash(const std::string& str);
+SHA1Digest SHA1Hash(const void* data, size_t size);
+
+}  // namespace base
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_EXT_BASE_HTTP_SHA1_H_
diff --git a/include/perfetto/ext/base/small_vector.h b/include/perfetto/ext/base/small_vector.h
new file mode 100644
index 0000000..15b1773
--- /dev/null
+++ b/include/perfetto/ext/base/small_vector.h
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2021 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 INCLUDE_PERFETTO_EXT_BASE_SMALL_VECTOR_H_
+#define INCLUDE_PERFETTO_EXT_BASE_SMALL_VECTOR_H_
+
+#include <algorithm>
+#include <type_traits>
+#include <utility>
+
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/utils.h"
+
+namespace perfetto {
+namespace base {
+
+// Uses inline storage first, switches to dynamic storage when it overflows.
+template <typename T, size_t kSize>
+class SmallVector {
+ public:
+  static constexpr size_t kInlineSize = kSize;
+
+  explicit SmallVector() = default;
+
+  ~SmallVector() {
+    clear();
+    if (PERFETTO_UNLIKELY(is_using_heap()))
+      AlignedFree(begin_);
+    begin_ = end_ = end_of_storage_ = nullptr;
+  }
+
+  // Move operators.
+  SmallVector(SmallVector&& other) noexcept(
+      std::is_nothrow_move_constructible<T>::value) {
+    if (other.is_using_heap()) {
+      // Move the heap content, no need to move the individual objects as their
+      // location won't change.
+      begin_ = other.begin_;
+      end_ = other.end_;
+      end_of_storage_ = other.end_of_storage_;
+    } else {
+      const size_t other_size = other.size();
+      PERFETTO_DCHECK(other_size <= capacity());
+      for (size_t i = 0; i < other_size; i++) {
+        // Move the entries and destroy the ones in the moved-from object.
+        new (&begin_[i]) T(std::move(other.begin_[i]));
+        other.begin_[i].~T();
+      }
+      end_ = begin_ + other_size;
+    }
+    auto* const other_inline_storage = other.inline_storage_begin();
+    other.end_ = other.begin_ = other_inline_storage;
+    other.end_of_storage_ = other_inline_storage + kInlineSize;
+  }
+
+  SmallVector& operator=(SmallVector&& other) noexcept(
+      std::is_nothrow_move_constructible<T>::value) {
+    this->~SmallVector();
+    new (this) SmallVector<T, kSize>(std::move(other));
+    return *this;
+  }
+
+  // Copy operators.
+  SmallVector(const SmallVector& other) {
+    const size_t other_size = other.size();
+    if (other_size > capacity())
+      Grow(other_size);
+    // Copy-construct the elements.
+    for (size_t i = 0; i < other_size; ++i)
+      new (&begin_[i]) T(other.begin_[i]);
+    end_ = begin_ + other_size;
+  }
+
+  SmallVector& operator=(const SmallVector& other) {
+    if (PERFETTO_UNLIKELY(this == &other))
+      return *this;
+    this->~SmallVector();
+    new (this) SmallVector<T, kSize>(other);
+    return *this;
+  }
+
+  T* data() { return begin_; }
+  const T* data() const { return begin_; }
+
+  T* begin() { return begin_; }
+  const T* begin() const { return begin_; }
+
+  T* end() { return end_; }
+  const T* end() const { return end_; }
+
+  size_t size() const { return static_cast<size_t>(end_ - begin_); }
+
+  bool empty() const { return end_ == begin_; }
+
+  size_t capacity() const {
+    return static_cast<size_t>(end_of_storage_ - begin_);
+  }
+
+  T& back() {
+    PERFETTO_DCHECK(!empty());
+    return end_[-1];
+  }
+  const T& back() const {
+    PERFETTO_DCHECK(!empty());
+    return end_[-1];
+  }
+
+  T& operator[](size_t index) {
+    PERFETTO_DCHECK(index < size());
+    return begin_[index];
+  }
+
+  const T& operator[](size_t index) const {
+    PERFETTO_DCHECK(index < size());
+    return begin_[index];
+  }
+
+  template <typename... Args>
+  void emplace_back(Args&&... args) {
+    T* end = end_;
+    if (PERFETTO_UNLIKELY(end == end_of_storage_))
+      end = Grow();
+    new (end) T(std::forward<Args>(args)...);
+    end_ = end + 1;
+  }
+
+  void pop_back() {
+    PERFETTO_DCHECK(!empty());
+    back().~T();
+    --end_;
+  }
+
+  // Clear without reverting back to inline storage.
+  void clear() {
+    while (!empty())
+      pop_back();
+  }
+
+ private:
+  PERFETTO_NO_INLINE T* Grow(size_t desired_capacity = 0) {
+    size_t cur_size = size();
+    size_t new_capacity = desired_capacity;
+    if (desired_capacity <= cur_size)
+      new_capacity = std::max(capacity() * 2, size_t(128));
+    T* new_storage =
+        static_cast<T*>(AlignedAlloc(alignof(T), new_capacity * sizeof(T)));
+    for (size_t i = 0; i < cur_size; ++i) {
+      // Move the elements into the new heap buffer and destroy the old ones.
+      new (&new_storage[i]) T(std::move(begin_[i]));
+      begin_[i].~T();
+    }
+    if (is_using_heap())
+      AlignedFree(begin_);
+    begin_ = new_storage;
+    end_ = new_storage + cur_size;
+    end_of_storage_ = new_storage + new_capacity;
+    return end_;
+  }
+
+  T* inline_storage_begin() { return reinterpret_cast<T*>(&inline_storage_); }
+  bool is_using_heap() { return begin_ != inline_storage_begin(); }
+
+  T* begin_ = inline_storage_begin();
+  T* end_ = begin_;
+  T* end_of_storage_ = begin_ + kInlineSize;
+
+  typename std::aligned_storage<sizeof(T) * kInlineSize, alignof(T)>::type
+      inline_storage_;
+};
+
+}  // namespace base
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_EXT_BASE_SMALL_VECTOR_H_
diff --git a/include/perfetto/ext/base/string_view.h b/include/perfetto/ext/base/string_view.h
index 83c7ea6..81c1a75 100644
--- a/include/perfetto/ext/base/string_view.h
+++ b/include/perfetto/ext/base/string_view.h
@@ -106,7 +106,7 @@
     return StringView(data_ + pos, rcount);
   }
 
-  bool CaseInsensitiveEq(const StringView& other) {
+  bool CaseInsensitiveEq(const StringView& other) const {
     if (size() != other.size())
       return false;
     if (size() == 0)
diff --git a/include/perfetto/ext/base/unix_socket.h b/include/perfetto/ext/base/unix_socket.h
index 15f2eaa..a769423 100644
--- a/include/perfetto/ext/base/unix_socket.h
+++ b/include/perfetto/ext/base/unix_socket.h
@@ -140,6 +140,10 @@
                const int* send_fds = nullptr,
                size_t num_fds = 0);
 
+  ssize_t SendStr(const std::string& str) {
+    return Send(str.data(), str.size());
+  }
+
   // |fd_vec| and |max_files| are ignored on Windows.
   ssize_t Receive(void* msg,
                   size_t len,
@@ -171,6 +175,7 @@
 #endif
   SockFamily family_ = SockFamily::kUnix;
   SockType type_ = SockType::kStream;
+  uint32_t tx_timeout_ms_ = 0;
 };
 
 // A non-blocking UNIX domain socket. Allows also to transfer file descriptors.
diff --git a/include/perfetto/ext/base/utils.h b/include/perfetto/ext/base/utils.h
index b88655a..ae1fc33 100644
--- a/include/perfetto/ext/base/utils.h
+++ b/include/perfetto/ext/base/utils.h
@@ -161,6 +161,37 @@
       static_cast<TU*>(AlignedAlloc(alignof(TU), sizeof(TU) * n_membs)));
 }
 
+// A RAII wrapper to invoke a function when leaving a function/scope.
+template <typename Func>
+class OnScopeExitWrapper {
+ public:
+  explicit OnScopeExitWrapper(Func f) : f_(std::move(f)), active_(true) {}
+  OnScopeExitWrapper(OnScopeExitWrapper&& other) noexcept
+      : f_(std::move(other.f_)), active_(other.active_) {
+    other.active_ = false;
+  }
+  ~OnScopeExitWrapper() {
+    if (active_)
+      f_();
+  }
+
+ private:
+  Func f_;
+  bool active_;
+};
+
+template <typename Func>
+PERFETTO_WARN_UNUSED_RESULT OnScopeExitWrapper<Func> OnScopeExit(Func f) {
+  return OnScopeExitWrapper<Func>(std::move(f));
+}
+
+// Returns a xxd-style hex dump (hex + ascii chars) of the input data.
+std::string HexDump(const void* data, size_t len, size_t bytes_per_line = 16);
+inline std::string HexDump(const std::string& data,
+                           size_t bytes_per_line = 16) {
+  return HexDump(data.data(), data.size(), bytes_per_line);
+}
+
 }  // namespace base
 }  // namespace perfetto
 
diff --git a/infra/perfetto.dev/src/assets/style.scss b/infra/perfetto.dev/src/assets/style.scss
index 0d0bbbe..7f23862 100644
--- a/infra/perfetto.dev/src/assets/style.scss
+++ b/infra/perfetto.dev/src/assets/style.scss
@@ -86,7 +86,7 @@
     --sh-padding-y: 5px;
     max-height: var(--site-header-height);
     padding: var(--sh-padding-y) 30px;
-    box-shadow: rgba(0, 0, 0, 0.3) 0px 3px 3px 0px;
+    box-shadow: rgba(0, 0, 0, 0.3) 0 3px 3px 0;
     overflow: hidden;
     display: flex;
     z-index: 10;
diff --git a/protos/perfetto/common/perf_events.proto b/protos/perfetto/common/perf_events.proto
index 0d594c9..8c48396 100644
--- a/protos/perfetto/common/perf_events.proto
+++ b/protos/perfetto/common/perf_events.proto
@@ -18,6 +18,7 @@
 
 package perfetto.protos;
 
+// Next id: 12
 message PerfEvents {
   // What event to sample on, and how often. Commented from the perspective of
   // its use in |PerfEventConfig|.
@@ -50,6 +51,13 @@
       RawEvent raw_event = 5;
     }
 
+    // If set, samples will be timestamped with the given clock.
+    // If unset, the clock is chosen by the implementation.
+    // For software events, prefer PERF_CLOCK_BOOTTIME. However it cannot be
+    // used for hardware events (due to interrupt safety), for which the
+    // recommendation is to use one of the monotonic clocks.
+    optional PerfClock timestamp_clock = 11;
+
     // Optional arbitrary name for the event, to identify it in the parsed
     // trace. Does *not* affect the profiling itself. If unset, the trace
     // parser will choose a suitable name.
@@ -89,4 +97,15 @@
     optional uint64 config1 = 3;
     optional uint64 config2 = 4;
   }
+
+  // Subset of clocks that is supported by perf timestamping.
+  // CLOCK_TAI is excluded since it's not expected to be used in practice, but
+  // would require additions to the trace clock synchronisation logic.
+  enum PerfClock {
+    UNKNOWN_PERF_CLOCK = 0;
+    PERF_CLOCK_REALTIME = 1;
+    PERF_CLOCK_MONOTONIC = 2;
+    PERF_CLOCK_MONOTONIC_RAW = 3;
+    PERF_CLOCK_BOOTTIME = 4;
+  }
 }
diff --git a/protos/perfetto/config/ftrace/ftrace_config.proto b/protos/perfetto/config/ftrace/ftrace_config.proto
index 070b481..7a38d74 100644
--- a/protos/perfetto/config/ftrace/ftrace_config.proto
+++ b/protos/perfetto/config/ftrace/ftrace_config.proto
@@ -48,4 +48,20 @@
   // initialized synchronously on the data source start and hence avoiding
   // timing races in tests.
   optional bool initialize_ksyms_synchronously_for_testing = 14;
+
+  // When this boolean is true AND the ftrace_events contains "kmem/rss_stat",
+  // this option causes traced_probes to enable the "kmem/rss_stat_throttled"
+  // event instad if present, and fall back to "kmem/rss_stat" if not present.
+  // The historical context for this is the following:
+  // - Up to Android S (12), the rss_stat was internally throttled in its
+  //   kernel implementation.
+  // - A change introduced in the kernels after S has introduced a new
+  //   "rss_stat_throttled" making the original "rss_stat" event unthrottled
+  //   (hence very spammy).
+  // - Not all Android T/13 devices will receive a new kernel though, hence we
+  //   need to deal with both cases.
+  // For more context: go/rss-stat-throttled.
+  // TODO(kaleshsingh): implement the logic behind this. Right now this flag
+  // does nothing.
+  optional bool throttle_rss_stat = 15;
 }
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index 5c83bee..52f256a 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -401,6 +401,22 @@
   // initialized synchronously on the data source start and hence avoiding
   // timing races in tests.
   optional bool initialize_ksyms_synchronously_for_testing = 14;
+
+  // When this boolean is true AND the ftrace_events contains "kmem/rss_stat",
+  // this option causes traced_probes to enable the "kmem/rss_stat_throttled"
+  // event instad if present, and fall back to "kmem/rss_stat" if not present.
+  // The historical context for this is the following:
+  // - Up to Android S (12), the rss_stat was internally throttled in its
+  //   kernel implementation.
+  // - A change introduced in the kernels after S has introduced a new
+  //   "rss_stat_throttled" making the original "rss_stat" event unthrottled
+  //   (hence very spammy).
+  // - Not all Android T/13 devices will receive a new kernel though, hence we
+  //   need to deal with both cases.
+  // For more context: go/rss-stat-throttled.
+  // TODO(kaleshsingh): implement the logic behind this. Right now this flag
+  // does nothing.
+  optional bool throttle_rss_stat = 15;
 }
 
 // End of protos/perfetto/config/ftrace/ftrace_config.proto
@@ -769,6 +785,13 @@
     optional uint32 dump_phase_ms = 1;
     // ms to wait between following dumps.
     optional uint32 dump_interval_ms = 2;
+    // If true, scans all the processes to find `process_cmdline` and filter by
+    // `min_anonymous_memory_kb` only at data source start. Default on Android
+    // S-.
+    //
+    // If false, rescans all the processes to find on every dump. Default on
+    // Android T+.
+    optional bool scan_pids_only_on_start = 3;
   }
 
   // This input is normalized in the following way: if it contains slashes,
@@ -813,6 +836,7 @@
 
 // Begin of protos/perfetto/common/perf_events.proto
 
+// Next id: 12
 message PerfEvents {
   // What event to sample on, and how often. Commented from the perspective of
   // its use in |PerfEventConfig|.
@@ -845,6 +869,13 @@
       RawEvent raw_event = 5;
     }
 
+    // If set, samples will be timestamped with the given clock.
+    // If unset, the clock is chosen by the implementation.
+    // For software events, prefer PERF_CLOCK_BOOTTIME. However it cannot be
+    // used for hardware events (due to interrupt safety), for which the
+    // recommendation is to use one of the monotonic clocks.
+    optional PerfClock timestamp_clock = 11;
+
     // Optional arbitrary name for the event, to identify it in the parsed
     // trace. Does *not* affect the profiling itself. If unset, the trace
     // parser will choose a suitable name.
@@ -884,6 +915,17 @@
     optional uint64 config1 = 3;
     optional uint64 config2 = 4;
   }
+
+  // Subset of clocks that is supported by perf timestamping.
+  // CLOCK_TAI is excluded since it's not expected to be used in practice, but
+  // would require additions to the trace clock synchronisation logic.
+  enum PerfClock {
+    UNKNOWN_PERF_CLOCK = 0;
+    PERF_CLOCK_REALTIME = 1;
+    PERF_CLOCK_MONOTONIC = 2;
+    PERF_CLOCK_MONOTONIC_RAW = 3;
+    PERF_CLOCK_BOOTTIME = 4;
+  }
 }
 
 // End of protos/perfetto/common/perf_events.proto
diff --git a/protos/perfetto/config/profiling/java_hprof_config.proto b/protos/perfetto/config/profiling/java_hprof_config.proto
index d504678..9245ee6 100644
--- a/protos/perfetto/config/profiling/java_hprof_config.proto
+++ b/protos/perfetto/config/profiling/java_hprof_config.proto
@@ -27,6 +27,13 @@
     optional uint32 dump_phase_ms = 1;
     // ms to wait between following dumps.
     optional uint32 dump_interval_ms = 2;
+    // If true, scans all the processes to find `process_cmdline` and filter by
+    // `min_anonymous_memory_kb` only at data source start. Default on Android
+    // S-.
+    //
+    // If false, rescans all the processes to find on every dump. Default on
+    // Android T+.
+    optional bool scan_pids_only_on_start = 3;
   }
 
   // This input is normalized in the following way: if it contains slashes,
diff --git a/protos/perfetto/metrics/android/java_heap_stats.proto b/protos/perfetto/metrics/android/java_heap_stats.proto
index 3f269c0..2579888 100644
--- a/protos/perfetto/metrics/android/java_heap_stats.proto
+++ b/protos/perfetto/metrics/android/java_heap_stats.proto
@@ -26,14 +26,19 @@
     optional int64 obj_count = 3;
   }
 
-  // Next id: 7
+  // Next id: 10
   message Sample {
     optional int64 ts = 1;
     // Size of the Java heap in bytes
     optional int64 heap_size = 2;
+    // Native size of all the objects (not included in heap_size)
+    optional int64 heap_native_size = 8;
     optional int64 obj_count = 4;
     // Size of the reachable objects in bytes.
     optional int64 reachable_heap_size = 3;
+    // Native size of all the reachable objects (not included in
+    // reachable_heap_size)
+    optional int64 reachable_heap_native_size = 9;
     optional int64 reachable_obj_count = 5;
     // Sum of anonymous RSS + swap pages in bytes.
     optional int64 anon_rss_and_swap_size = 6;
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 6cf96ca..15a8d87 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -607,14 +607,19 @@
     optional int64 obj_count = 3;
   }
 
-  // Next id: 7
+  // Next id: 10
   message Sample {
     optional int64 ts = 1;
     // Size of the Java heap in bytes
     optional int64 heap_size = 2;
+    // Native size of all the objects (not included in heap_size)
+    optional int64 heap_native_size = 8;
     optional int64 obj_count = 4;
     // Size of the reachable objects in bytes.
     optional int64 reachable_heap_size = 3;
+    // Native size of all the reachable objects (not included in
+    // reachable_heap_size)
+    optional int64 reachable_heap_native_size = 9;
     optional int64 reachable_obj_count = 5;
     // Sum of anonymous RSS + swap pages in bytes.
     optional int64 anon_rss_and_swap_size = 6;
diff --git a/protos/perfetto/trace/ftrace/all_protos.gni b/protos/perfetto/trace/ftrace/all_protos.gni
index 8910853..ec0b55d 100644
--- a/protos/perfetto/trace/ftrace/all_protos.gni
+++ b/protos/perfetto/trace/ftrace/all_protos.gni
@@ -54,6 +54,7 @@
   "sde.proto",
   "signal.proto",
   "sync.proto",
+  "synthetic.proto",
   "systrace.proto",
   "task.proto",
   "thermal.proto",
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index 31e5b92..e236b0d 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -54,6 +54,7 @@
 import "protos/perfetto/trace/ftrace/sde.proto";
 import "protos/perfetto/trace/ftrace/signal.proto";
 import "protos/perfetto/trace/ftrace/sync.proto";
+import "protos/perfetto/trace/ftrace/synthetic.proto";
 import "protos/perfetto/trace/ftrace/systrace.proto";
 import "protos/perfetto/trace/ftrace/task.proto";
 import "protos/perfetto/trace/ftrace/thermal.proto";
@@ -447,5 +448,6 @@
     SdeSdePerfCrtcUpdateFtraceEvent sde_sde_perf_crtc_update = 356;
     SdeSdePerfSetQosLutsFtraceEvent sde_sde_perf_set_qos_luts = 357;
     SdeSdePerfUpdateBusFtraceEvent sde_sde_perf_update_bus = 358;
+    RssStatThrottledFtraceEvent rss_stat_throttled = 359;
   }
 }
diff --git a/protos/perfetto/trace/ftrace/synthetic.proto b/protos/perfetto/trace/ftrace/synthetic.proto
new file mode 100644
index 0000000..32ea403
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/synthetic.proto
@@ -0,0 +1,13 @@
+// Autogenerated by:
+// ../../tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message RssStatThrottledFtraceEvent {
+  optional uint32 curr = 1;
+  optional int32 member = 2;
+  optional uint32 mm_id = 3;
+  optional int64 size = 4;
+}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 1c58fbc..c4eb417 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -401,6 +401,22 @@
   // initialized synchronously on the data source start and hence avoiding
   // timing races in tests.
   optional bool initialize_ksyms_synchronously_for_testing = 14;
+
+  // When this boolean is true AND the ftrace_events contains "kmem/rss_stat",
+  // this option causes traced_probes to enable the "kmem/rss_stat_throttled"
+  // event instad if present, and fall back to "kmem/rss_stat" if not present.
+  // The historical context for this is the following:
+  // - Up to Android S (12), the rss_stat was internally throttled in its
+  //   kernel implementation.
+  // - A change introduced in the kernels after S has introduced a new
+  //   "rss_stat_throttled" making the original "rss_stat" event unthrottled
+  //   (hence very spammy).
+  // - Not all Android T/13 devices will receive a new kernel though, hence we
+  //   need to deal with both cases.
+  // For more context: go/rss-stat-throttled.
+  // TODO(kaleshsingh): implement the logic behind this. Right now this flag
+  // does nothing.
+  optional bool throttle_rss_stat = 15;
 }
 
 // End of protos/perfetto/config/ftrace/ftrace_config.proto
@@ -769,6 +785,13 @@
     optional uint32 dump_phase_ms = 1;
     // ms to wait between following dumps.
     optional uint32 dump_interval_ms = 2;
+    // If true, scans all the processes to find `process_cmdline` and filter by
+    // `min_anonymous_memory_kb` only at data source start. Default on Android
+    // S-.
+    //
+    // If false, rescans all the processes to find on every dump. Default on
+    // Android T+.
+    optional bool scan_pids_only_on_start = 3;
   }
 
   // This input is normalized in the following way: if it contains slashes,
@@ -813,6 +836,7 @@
 
 // Begin of protos/perfetto/common/perf_events.proto
 
+// Next id: 12
 message PerfEvents {
   // What event to sample on, and how often. Commented from the perspective of
   // its use in |PerfEventConfig|.
@@ -845,6 +869,13 @@
       RawEvent raw_event = 5;
     }
 
+    // If set, samples will be timestamped with the given clock.
+    // If unset, the clock is chosen by the implementation.
+    // For software events, prefer PERF_CLOCK_BOOTTIME. However it cannot be
+    // used for hardware events (due to interrupt safety), for which the
+    // recommendation is to use one of the monotonic clocks.
+    optional PerfClock timestamp_clock = 11;
+
     // Optional arbitrary name for the event, to identify it in the parsed
     // trace. Does *not* affect the profiling itself. If unset, the trace
     // parser will choose a suitable name.
@@ -884,6 +915,17 @@
     optional uint64 config1 = 3;
     optional uint64 config2 = 4;
   }
+
+  // Subset of clocks that is supported by perf timestamping.
+  // CLOCK_TAI is excluded since it's not expected to be used in practice, but
+  // would require additions to the trace clock synchronisation logic.
+  enum PerfClock {
+    UNKNOWN_PERF_CLOCK = 0;
+    PERF_CLOCK_REALTIME = 1;
+    PERF_CLOCK_MONOTONIC = 2;
+    PERF_CLOCK_MONOTONIC_RAW = 3;
+    PERF_CLOCK_BOOTTIME = 4;
+  }
 }
 
 // End of protos/perfetto/common/perf_events.proto
@@ -5113,6 +5155,17 @@
 
 // End of protos/perfetto/trace/ftrace/sync.proto
 
+// Begin of protos/perfetto/trace/ftrace/synthetic.proto
+
+message RssStatThrottledFtraceEvent {
+  optional uint32 curr = 1;
+  optional int32 member = 2;
+  optional uint32 mm_id = 3;
+  optional int64 size = 4;
+}
+
+// End of protos/perfetto/trace/ftrace/synthetic.proto
+
 // Begin of protos/perfetto/trace/ftrace/systrace.proto
 
 message ZeroFtraceEvent {
@@ -5584,6 +5637,7 @@
     SdeSdePerfCrtcUpdateFtraceEvent sde_sde_perf_crtc_update = 356;
     SdeSdePerfSetQosLutsFtraceEvent sde_sde_perf_set_qos_luts = 357;
     SdeSdePerfUpdateBusFtraceEvent sde_sde_perf_update_bus = 358;
+    RssStatThrottledFtraceEvent rss_stat_throttled = 359;
   }
 }
 
@@ -6903,6 +6957,9 @@
 // Next id: 2
 message ChromeRendererSchedulerState {
   optional ChromeRAILMode rail_mode = 1;
+
+  optional bool is_backgrounded = 2;
+  optional bool is_hidden = 3;
 }
 
 // End of protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto
@@ -8144,9 +8201,9 @@
 // individual sample with a counter value, and optionally a
 // callstack.
 //
-// Timestamps are within the root packet. This used to use the CLOCK_BOOTTIME
-// domain, but now the default is CLOCK_MONOTONIC_RAW which is compatible with
-// more event types.
+// Timestamps are within the root packet. The config can specify the clock, or
+// the implementation will default to CLOCK_MONOTONIC_RAW. Within the Android R
+// timeframe, the default was CLOCK_BOOTTIME.
 //
 // There are several distinct views of this message:
 // * indication of kernel buffer data loss (kernel_records_lost set)
@@ -8819,6 +8876,7 @@
     THREAD_DEVTOOLSADB = 38;
     THREAD_NETWORKCONFIGWATCHER = 39;
     THREAD_WASAPI_RENDER = 40;
+    THREAD_LOADER_LOCK_SAMPLER = 41;
 
     THREAD_MEMORY_INFRA = 50;
     THREAD_SAMPLING_PROFILER = 51;
diff --git a/protos/perfetto/trace/profiling/profile_packet.proto b/protos/perfetto/trace/profiling/profile_packet.proto
index 8b147fa..f599f17 100644
--- a/protos/perfetto/trace/profiling/profile_packet.proto
+++ b/protos/perfetto/trace/profiling/profile_packet.proto
@@ -279,9 +279,9 @@
 // individual sample with a counter value, and optionally a
 // callstack.
 //
-// Timestamps are within the root packet. This used to use the CLOCK_BOOTTIME
-// domain, but now the default is CLOCK_MONOTONIC_RAW which is compatible with
-// more event types.
+// Timestamps are within the root packet. The config can specify the clock, or
+// the implementation will default to CLOCK_MONOTONIC_RAW. Within the Android R
+// timeframe, the default was CLOCK_BOOTTIME.
 //
 // There are several distinct views of this message:
 // * indication of kernel buffer data loss (kernel_records_lost set)
diff --git a/protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto b/protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto
index ade7897..436cc79 100644
--- a/protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto
+++ b/protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto
@@ -34,4 +34,7 @@
 // Next id: 2
 message ChromeRendererSchedulerState {
   optional ChromeRAILMode rail_mode = 1;
+
+  optional bool is_backgrounded = 2;
+  optional bool is_hidden = 3;
 }
diff --git a/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto b/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto
index f142e49..bd82b5a 100644
--- a/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto
+++ b/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto
@@ -73,6 +73,7 @@
     THREAD_DEVTOOLSADB = 38;
     THREAD_NETWORKCONFIGWATCHER = 39;
     THREAD_WASAPI_RENDER = 40;
+    THREAD_LOADER_LOCK_SAMPLER = 41;
 
     THREAD_MEMORY_INFRA = 50;
     THREAD_SAMPLING_PROFILER = 51;
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index 7fb2c33..02a230f 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -41,7 +41,7 @@
   // every time a new feature that the UI depends on is being introduced (e.g.
   // new tables, new SQL operators, metrics that are required by the UI).
   // See also TraceProcessorVersion (below).
-  TRACE_PROCESSOR_CURRENT_API_VERSION = 2;
+  TRACE_PROCESSOR_CURRENT_API_VERSION = 3;
 }
 
 // At lowest level, the wire-format of the RPC procol is a linear sequence of
diff --git a/src/base/BUILD.gn b/src/base/BUILD.gn
index f6d86b0..4774846 100644
--- a/src/base/BUILD.gn
+++ b/src/base/BUILD.gn
@@ -159,9 +159,14 @@
     "../../gn:gtest_and_gmock",
   ]
 
+  if (enable_perfetto_ipc) {
+    deps += [ "http:unittests" ]
+  }
+
   sources = [
     "base64_unittest.cc",
     "circular_queue_unittest.cc",
+    "flat_hash_map_unittest.cc",
     "flat_set_unittest.cc",
     "getopt_compat_unittest.cc",
     "logging_unittest.cc",
@@ -170,6 +175,7 @@
     "paged_memory_unittest.cc",
     "periodic_task_unittest.cc",
     "scoped_file_unittest.cc",
+    "small_vector_unittest.cc",
     "string_splitter_unittest.cc",
     "string_utils_unittest.cc",
     "string_view_unittest.cc",
@@ -210,13 +216,33 @@
 }
 
 if (enable_perfetto_benchmarks) {
+  declare_args() {
+    perfetto_benchmark_3p_libs_prefix = ""
+  }
   source_set("benchmarks") {
+    # If you intend to reproduce the comparison with {Absl, Folly, Tessil}
+    # you need to manually install those libraries and then set the GN arg
+    # perfetto_benchmark_3p_libs_prefix = "/usr/local"
     testonly = true
     deps = [
       ":base",
       "../../gn:benchmark",
       "../../gn:default_deps",
     ]
-    sources = [ "flat_set_benchmark.cc" ]
+    if (perfetto_benchmark_3p_libs_prefix != "") {
+      configs -= [ "//gn/standalone:c++11" ]
+      configs += [ "//gn/standalone:c++17" ]
+      defines = [ "PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS" ]
+      cflags = [ "-isystem${perfetto_benchmark_3p_libs_prefix}/include" ]
+      libs = [
+        "${perfetto_benchmark_3p_libs_prefix}/lib/libfolly.a",
+        "${perfetto_benchmark_3p_libs_prefix}/lib/libabsl_raw_hash_set.a",
+        "${perfetto_benchmark_3p_libs_prefix}/lib/libabsl_hash.a",
+      ]
+    }
+    sources = [
+      "flat_hash_map_benchmark.cc",
+      "flat_set_benchmark.cc",
+    ]
   }
 }
diff --git a/src/base/base64.cc b/src/base/base64.cc
index 437ad67..bb2e3c4 100644
--- a/src/base/base64.cc
+++ b/src/base/base64.cc
@@ -146,7 +146,7 @@
 
   PERFETTO_CHECK(res <= static_cast<ssize_t>(dst.size()));
   dst.resize(static_cast<size_t>(res));
-  return make_optional(dst);
+  return base::make_optional(dst);
 }
 
 }  // namespace base
diff --git a/src/base/crash_keys.cc b/src/base/crash_keys.cc
index 5c975df..13e1c51 100644
--- a/src/base/crash_keys.cc
+++ b/src/base/crash_keys.cc
@@ -54,15 +54,19 @@
 size_t CrashKey::ToString(char* dst, size_t len) {
   if (len > 0)
     *dst = '\0';
-  switch (type_) {
+  switch (type_.load(std::memory_order_relaxed)) {
     case Type::kUnset:
       break;
     case Type::kInt:
-      return SprintfTrunc(dst, len, "%s: %" PRId64 "\n", name_, int_value_);
+      return SprintfTrunc(dst, len, "%s: %" PRId64 "\n", name_,
+                          int_value_.load(std::memory_order_relaxed));
     case Type::kStr:
+      char buf[sizeof(str_value_)];
+      for (size_t i = 0; i < sizeof(str_value_); i++)
+        buf[i] = str_value_[i].load(std::memory_order_relaxed);
+
       // Don't assume |str_value_| is properly null-terminated.
-      return SprintfTrunc(dst, len, "%s: %.*s\n", name_,
-                          int(sizeof(str_value_)), str_value_);
+      return SprintfTrunc(dst, len, "%s: %.*s\n", name_, int(sizeof(buf)), buf);
   }
   return 0;
 }
diff --git a/src/base/flat_hash_map_benchmark.cc b/src/base/flat_hash_map_benchmark.cc
new file mode 100644
index 0000000..33da669
--- /dev/null
+++ b/src/base/flat_hash_map_benchmark.cc
@@ -0,0 +1,378 @@
+// 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.
+
+#include <functional>
+#include <random>
+#include <unordered_map>
+
+#include <benchmark/benchmark.h>
+#include <stdio.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/hash.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/string_view.h"
+
+// This benchmark allows to compare our FlatHashMap implementation against
+// reference implementations from Absl (Google), Folly F14 (FB), and Tssil's
+// reference RobinHood hashmap.
+// Those libraries are not checked in into the repo. If you want to reproduce
+// the benchmark you need to:
+// - Manually install the three libraries using following the instructions in
+//   their readme (they all use cmake).
+// - When running cmake, remember to pass
+//   -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS='-DNDEBUG -O3 -msse4.2 -mavx'.
+//   That sets cflags for a more fair comparison.
+// - Set is_debug=false in the GN args.
+// - Set the GN var perfetto_benchmark_3p_libs_prefix="/usr/local" (or whatever
+//   other directory you set as DESTDIR when running make install).
+// The presence of the perfetto_benchmark_3p_libs_prefix GN variable will
+// automatically define PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS.
+
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+
+// Last tested: https://github.com/abseil/abseil-cpp @ f2dbd918d.
+#include <absl/container/flat_hash_map.h>
+
+// Last tested: https://github.com/facebook/folly @ 028a9abae3.
+#include <folly/container/F14Map.h>
+
+// Last tested: https://github.com/Tessil/robin-map @ a603419b9.
+#include <tsl/robin_map.h>
+#endif
+
+namespace {
+
+using namespace perfetto;
+using benchmark::Counter;
+using perfetto::base::AlreadyHashed;
+using perfetto::base::LinearProbe;
+using perfetto::base::QuadraticHalfProbe;
+using perfetto::base::QuadraticProbe;
+
+// Our FlatHashMap doesn't have a STL-like interface, mainly because we use
+// columnar-oriented storage, not array-of-tuples, so we can't easily map into
+// that interface. This wrapper makes our FlatHashMap compatible with STL (just
+// for what it takes to build this translation unit), at the cost of some small
+// performance penalty (around 1-2%).
+template <typename Key, typename Value, typename Hasher, typename Probe>
+class Ours : public base::FlatHashMap<Key, Value, Hasher, Probe> {
+ public:
+  struct Iterator {
+    using value_type = std::pair<const Key&, Value&>;
+    Iterator(const Key& k, Value& v) : pair_{k, v} {}
+    value_type* operator->() { return &pair_; }
+    value_type& operator*() { return pair_; }
+    bool operator==(const Iterator& other) const {
+      return &pair_.first == &other.pair_.first;
+    }
+    bool operator!=(const Iterator& other) const { return !operator==(other); }
+    value_type pair_;
+  };
+
+  void insert(std::pair<Key, Value>&& pair) {
+    this->Insert(std::move(pair.first), std::move(pair.second));
+  }
+
+  Iterator find(const Key& key) {
+    const size_t idx = this->FindInternal(key);
+    return Iterator(this->keys_[idx], this->values_[idx]);
+  }
+
+  Iterator end() {
+    return Iterator(this->keys_[this->kNotFound],
+                    this->values_[this->kNotFound]);
+  }
+};
+
+std::vector<uint64_t> LoadTraceStrings(benchmark::State& state) {
+  std::vector<uint64_t> str_hashes;
+  // This requires that the user has downloaded the file
+  // go/perfetto-benchmark-trace-strings into /tmp/trace_strings. The file is
+  // too big (2.3 GB after uncompression) and it's not worth adding it to the
+  // //test/data. Also it contains data from a team member's phone and cannot
+  // be public.
+  base::ScopedFstream f(fopen("/tmp/trace_strings", "re"));
+  if (!f) {
+    state.SkipWithError(
+        "Test strings missing. Googlers: download "
+        "go/perfetto-benchmark-trace-strings and save into /tmp/trace_strings");
+    return str_hashes;
+  }
+  char line[4096];
+  while (fgets(line, sizeof(line), *f)) {
+    base::Hash hasher;
+    hasher.Update(line, strlen(line));
+    str_hashes.emplace_back(hasher.digest());
+  }
+  return str_hashes;
+}
+
+bool IsBenchmarkFunctionalOnly() {
+  return getenv("BENCHMARK_FUNCTIONAL_TEST_ONLY") != nullptr;
+}
+
+size_t num_samples() {
+  return IsBenchmarkFunctionalOnly() ? size_t(100) : size_t(10 * 1000 * 1000);
+}
+
+// Uses directly the base::FlatHashMap with no STL wrapper. Configures the map
+// in append-only mode.
+void BM_HashMap_InsertTraceStrings_AppendOnly(benchmark::State& state) {
+  std::vector<uint64_t> hashes = LoadTraceStrings(state);
+  for (auto _ : state) {
+    base::FlatHashMap<uint64_t, uint64_t, AlreadyHashed<uint64_t>, LinearProbe,
+                      /*AppendOnly=*/true>
+        mapz;
+    for (uint64_t hash : hashes)
+      mapz.Insert(hash, 42);
+
+    benchmark::ClobberMemory();
+    state.counters["uniq"] = Counter(static_cast<double>(mapz.size()));
+  }
+  state.counters["tot"] = Counter(static_cast<double>(hashes.size()),
+                                  Counter::kIsIterationInvariant);
+  state.counters["rate"] = Counter(static_cast<double>(hashes.size()),
+                                   Counter::kIsIterationInvariantRate);
+}
+
+template <typename MapType>
+void BM_HashMap_InsertTraceStrings(benchmark::State& state) {
+  std::vector<uint64_t> hashes = LoadTraceStrings(state);
+  for (auto _ : state) {
+    MapType mapz;
+    for (uint64_t hash : hashes)
+      mapz.insert({hash, 42});
+
+    benchmark::ClobberMemory();
+    state.counters["uniq"] = Counter(static_cast<double>(mapz.size()));
+  }
+  state.counters["tot"] = Counter(static_cast<double>(hashes.size()),
+                                  Counter::kIsIterationInvariant);
+  state.counters["rate"] = Counter(static_cast<double>(hashes.size()),
+                                   Counter::kIsIterationInvariantRate);
+}
+
+template <typename MapType>
+void BM_HashMap_TraceTids(benchmark::State& state) {
+  std::vector<std::pair<char, int>> ops_and_tids;
+  {
+    base::ScopedFstream f(fopen("/tmp/tids", "re"));
+    if (!f) {
+      // This test requires a large (800MB) test file. It's not checked into the
+      // repository's //test/data because it would slow down all developers for
+      // a marginal benefit.
+      state.SkipWithError(
+          "Please run `curl -Lo /tmp/tids "
+          "https://storage.googleapis.com/perfetto/test_datalong_trace_tids.txt"
+          "` and try again.");
+      return;
+    }
+    char op;
+    int tid;
+    while (fscanf(*f, "%c %d\n", &op, &tid) == 2)
+      ops_and_tids.emplace_back(op, tid);
+  }
+
+  for (auto _ : state) {
+    MapType mapz;
+    for (const auto& ops_and_tid : ops_and_tids) {
+      if (ops_and_tid.first == '[') {
+        mapz[ops_and_tid.second]++;
+      } else {
+        mapz.insert({ops_and_tid.second, 0});
+      }
+    }
+
+    benchmark::ClobberMemory();
+    state.counters["uniq"] = Counter(static_cast<double>(mapz.size()));
+  }
+  state.counters["rate"] = Counter(static_cast<double>(ops_and_tids.size()),
+                                   Counter::kIsIterationInvariantRate);
+}
+
+template <typename MapType>
+void BM_HashMap_InsertRandInts(benchmark::State& state) {
+  std::minstd_rand0 rng(0);
+  std::vector<size_t> keys(static_cast<size_t>(num_samples()));
+  std::shuffle(keys.begin(), keys.end(), rng);
+  for (auto _ : state) {
+    MapType mapz;
+    for (const auto key : keys)
+      mapz.insert({key, key});
+    benchmark::DoNotOptimize(mapz);
+    benchmark::ClobberMemory();
+  }
+  state.counters["insertions"] = Counter(static_cast<double>(keys.size()),
+                                         Counter::kIsIterationInvariantRate);
+}
+
+// This test is performs insertions on integers that are designed to create
+// lot of clustering on the same small set of buckets.
+// This covers the unlucky case of using a map with a poor hashing function.
+template <typename MapType>
+void BM_HashMap_InsertCollidingInts(benchmark::State& state) {
+  std::vector<size_t> keys;
+  const size_t kNumSamples = num_samples();
+
+  // Generates numbers that are all distinct from each other, but that are
+  // designed to collide on the same buckets.
+  constexpr size_t kShift = 8;  // Collide on the same 2^8 = 256 buckets.
+  for (size_t i = 0; i < kNumSamples; i++) {
+    size_t bucket = i & ((1 << kShift) - 1);  // [0, 256].
+    size_t multiplier = i >> kShift;          // 0,0,0... 1,1,1..., 2,2,2...
+    size_t key = 8192 * multiplier + bucket;
+    keys.push_back(key);
+  }
+  for (auto _ : state) {
+    MapType mapz;
+    for (const size_t key : keys)
+      mapz.insert({key, key});
+    benchmark::DoNotOptimize(mapz);
+    benchmark::ClobberMemory();
+  }
+  state.counters["insertions"] = Counter(static_cast<double>(keys.size()),
+                                         Counter::kIsIterationInvariantRate);
+}
+
+// Unlike the previous benchmark, here integers don't just collide on the same
+// buckets, they have a large number of duplicates with the same values.
+// Most of those insertions are no-ops. This tests the ability of the hashmap
+// to deal with cases where the hash function is good but the insertions contain
+// lot of dupes (e.g. dealing with pids).
+template <typename MapType>
+void BM_HashMap_InsertDupeInts(benchmark::State& state) {
+  std::vector<size_t> keys;
+  const size_t kNumSamples = num_samples();
+
+  for (size_t i = 0; i < kNumSamples; i++)
+    keys.push_back(i % 16384);
+
+  for (auto _ : state) {
+    MapType mapz;
+    for (const size_t key : keys)
+      mapz.insert({key, key});
+    benchmark::DoNotOptimize(mapz);
+    benchmark::ClobberMemory();
+  }
+  state.counters["insertions"] = Counter(static_cast<double>(keys.size()),
+                                         Counter::kIsIterationInvariantRate);
+}
+
+template <typename MapType>
+void BM_HashMap_LookupRandInts(benchmark::State& state) {
+  std::minstd_rand0 rng(0);
+  std::vector<size_t> keys(static_cast<size_t>(num_samples()));
+  std::shuffle(keys.begin(), keys.end(), rng);
+
+  MapType mapz;
+  for (const size_t key : keys)
+    mapz.insert({key, key});
+
+  for (auto _ : state) {
+    int64_t total = 0;
+    for (const size_t key : keys) {
+      auto it = mapz.find(static_cast<uint64_t>(key));
+      PERFETTO_CHECK(it != mapz.end());
+      total += it->second;
+    }
+    benchmark::DoNotOptimize(total);
+    benchmark::ClobberMemory();
+    state.counters["sum"] = Counter(static_cast<double>(total));
+  }
+  state.counters["lookups"] = Counter(static_cast<double>(keys.size()),
+                                      Counter::kIsIterationInvariantRate);
+}
+
+}  // namespace
+
+using Ours_LinearProbing =
+    Ours<uint64_t, uint64_t, AlreadyHashed<uint64_t>, LinearProbe>;
+using Ours_QuadProbing =
+    Ours<uint64_t, uint64_t, AlreadyHashed<uint64_t>, QuadraticProbe>;
+using Ours_QuadCompProbing =
+    Ours<uint64_t, uint64_t, AlreadyHashed<uint64_t>, QuadraticHalfProbe>;
+using StdUnorderedMap =
+    std::unordered_map<uint64_t, uint64_t, AlreadyHashed<uint64_t>>;
+
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+using RobinMap = tsl::robin_map<uint64_t, uint64_t, AlreadyHashed<uint64_t>>;
+using AbslFlatHashMap =
+    absl::flat_hash_map<uint64_t, uint64_t, AlreadyHashed<uint64_t>>;
+using FollyF14FastMap =
+    folly::F14FastMap<uint64_t, uint64_t, AlreadyHashed<uint64_t>>;
+#endif
+
+BENCHMARK(BM_HashMap_InsertTraceStrings_AppendOnly);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, Ours_LinearProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, Ours_QuadProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, StdUnorderedMap);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, RobinMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, AbslFlatHashMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, FollyF14FastMap);
+#endif
+
+#define TID_ARGS int, uint64_t, std::hash<int>
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, Ours<TID_ARGS, LinearProbe>);
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, Ours<TID_ARGS, QuadraticProbe>);
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, Ours<TID_ARGS, QuadraticHalfProbe>);
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, std::unordered_map<TID_ARGS>);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, tsl::robin_map<TID_ARGS>);
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, absl::flat_hash_map<TID_ARGS>);
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, folly::F14FastMap<TID_ARGS>);
+#endif
+
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, Ours_LinearProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, Ours_QuadProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, StdUnorderedMap);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, RobinMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, AbslFlatHashMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, FollyF14FastMap);
+#endif
+
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, Ours_LinearProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, Ours_QuadProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, Ours_QuadCompProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, StdUnorderedMap);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, RobinMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, AbslFlatHashMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, FollyF14FastMap);
+#endif
+
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, Ours_LinearProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, Ours_QuadProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, Ours_QuadCompProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, StdUnorderedMap);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, RobinMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, AbslFlatHashMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, FollyF14FastMap);
+#endif
+
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, Ours_LinearProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, Ours_QuadProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, StdUnorderedMap);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, RobinMap);
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, AbslFlatHashMap);
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, FollyF14FastMap);
+#endif
diff --git a/src/base/flat_hash_map_unittest.cc b/src/base/flat_hash_map_unittest.cc
new file mode 100644
index 0000000..3dd02a4
--- /dev/null
+++ b/src/base/flat_hash_map_unittest.cc
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2021 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 "perfetto/ext/base/flat_hash_map.h"
+
+#include <array>
+#include <functional>
+#include <random>
+#include <set>
+#include <unordered_map>
+
+#include "perfetto/ext/base/hash.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+using ::testing::Types;
+
+struct CollidingHasher {
+  size_t operator()(int n) const { return static_cast<size_t>(n % 1000); }
+};
+
+template <typename T>
+class FlatHashMapTest : public testing::Test {
+ public:
+  using Probe = T;
+};
+
+using ProbeTypes = Types<LinearProbe, QuadraticHalfProbe, QuadraticProbe>;
+TYPED_TEST_SUITE(FlatHashMapTest, ProbeTypes, /* trailing ',' for GCC*/);
+
+struct Key {
+  static int instances;
+
+  explicit Key(int v) : val(v) {}
+  ~Key() { instances--; }
+  Key(Key&& other) noexcept {
+    val = other.val;
+    other.val = -1;
+  }
+  bool operator==(const Key& other) { return val == other.val; }
+  int val = 0;
+  int id = instances++;
+};
+
+struct Value {
+  static int instances;
+
+  explicit Value(int v = 0) : val(v) {}
+  ~Value() { instances--; }
+  Value(Value&& other) noexcept {
+    val = other.val;
+    other.val = -1;
+  }
+  Value(const Value&) = delete;
+  int val = 0;
+  int id = instances++;
+};
+
+struct Hasher {
+  size_t operator()(const Key& k) const { return static_cast<size_t>(k.val); }
+};
+
+int Key::instances = 0;
+int Value::instances = 0;
+
+TYPED_TEST(FlatHashMapTest, NonTrivialKeyValues) {
+  FlatHashMap<Key, Value, Hasher, typename TestFixture::Probe> fmap;
+
+  for (int iteration = 0; iteration < 3; iteration++) {
+    const int kNum = 10;
+    for (int i = 0; i < kNum; i++) {
+      ASSERT_TRUE(fmap.Insert(Key(i), Value(i * 2)).second);
+      Value* value = fmap.Find(Key(i));
+      ASSERT_NE(value, nullptr);
+      ASSERT_EQ(value->val, i * 2);
+      ASSERT_EQ(Key::instances, i + 1);
+      ASSERT_EQ(Value::instances, i + 1);
+    }
+
+    ASSERT_TRUE(fmap.Erase(Key(1)));
+    ASSERT_TRUE(fmap.Erase(Key(5)));
+    ASSERT_TRUE(fmap.Erase(Key(9)));
+
+    ASSERT_EQ(Key::instances, kNum - 3);
+    ASSERT_EQ(Value::instances, kNum - 3);
+
+    FlatHashMap<Key, Value, Hasher, typename TestFixture::Probe> fmap2(
+        std::move(fmap));
+    ASSERT_EQ(fmap.size(), 0u);
+    ASSERT_EQ(fmap2.size(), static_cast<size_t>(kNum - 3));
+
+    ASSERT_EQ(Key::instances, kNum - 3);
+    ASSERT_EQ(Value::instances, kNum - 3);
+
+    // Ensure the moved-from map is usable.
+    fmap.Insert(Key(1), Value(-1));
+    fmap.Insert(Key(5), Value(-5));
+    fmap.Insert(Key(9), Value(-9));
+    ASSERT_EQ(Key::instances, (kNum - 3) + 3);
+    ASSERT_EQ(Value::instances, (kNum - 3) + 3);
+
+    fmap2.Clear();
+    ASSERT_EQ(fmap2.size(), 0u);
+    ASSERT_EQ(fmap.size(), 3u);
+    ASSERT_EQ(Key::instances, 3);
+    ASSERT_EQ(Value::instances, 3);
+    ASSERT_EQ(fmap.Find(Key(1))->val, -1);
+    ASSERT_EQ(fmap.Find(Key(5))->val, -5);
+    ASSERT_EQ(fmap.Find(Key(9))->val, -9);
+
+    fmap = std::move(fmap2);
+    ASSERT_EQ(Key::instances, 0);
+    ASSERT_EQ(Value::instances, 0);
+    ASSERT_EQ(fmap.size(), 0u);
+  }
+
+  // Test that operator[] behaves rationally.
+  fmap = decltype(fmap)();  // Re-assign with a copy constructor.
+  fmap[Key{2}].val = 102;
+  fmap[Key{1}].val = 101;
+  ASSERT_EQ(fmap.Find(Key{2})->val, 102);
+  ASSERT_EQ(fmap.Find(Key{1})->val, 101);
+  fmap[Key{2}].val = 122;
+  ASSERT_EQ(fmap.Find(Key{2})->val, 122);
+  ASSERT_EQ(fmap[Key{1}].val, 101);
+  auto fmap2(std::move(fmap));
+  ASSERT_EQ(fmap[Key{1}].val, 0);
+  ASSERT_EQ(fmap.size(), 1u);
+}
+
+TYPED_TEST(FlatHashMapTest, AllTagsAreValid) {
+  FlatHashMap<size_t, size_t, base::AlreadyHashed<size_t>,
+              typename TestFixture::Probe>
+      fmap;
+  auto make_key = [](size_t tag) {
+    return tag << ((sizeof(size_t) - 1) * size_t(8));
+  };
+  for (size_t i = 0; i < 256; i++) {
+    size_t key = make_key(i);
+    fmap.Insert(key, i);
+    ASSERT_EQ(fmap.size(), i + 1);
+  }
+  for (size_t i = 0; i < 256; i++) {
+    size_t key = make_key(i);
+    ASSERT_NE(fmap.Find(key), nullptr);
+    ASSERT_EQ(*fmap.Find(key), i);
+  }
+  for (size_t i = 0; i < 256; i++) {
+    size_t key = make_key(i);
+    fmap.Erase(key);
+    ASSERT_EQ(fmap.size(), 255 - i);
+    ASSERT_EQ(fmap.Find(key), nullptr);
+  }
+}
+
+TYPED_TEST(FlatHashMapTest, FillWithTombstones) {
+  FlatHashMap<Key, Value, Hasher, typename TestFixture::Probe> fmap(
+      /*initial_capacity=*/0, /*load_limit_pct=*/100);
+
+  for (int rep = 0; rep < 3; rep++) {
+    for (int i = 0; i < 1024; i++)
+      ASSERT_TRUE(fmap.Insert(Key(i), Value(i)).second);
+
+    ASSERT_EQ(fmap.size(), 1024u);
+    ASSERT_EQ(Key::instances, 1024);
+    ASSERT_EQ(Value::instances, 1024);
+
+    // Erase all entries.
+    for (int i = 0; i < 1024; i++)
+      ASSERT_TRUE(fmap.Erase(Key(i)));
+
+    ASSERT_EQ(fmap.size(), 0u);
+    ASSERT_EQ(Key::instances, 0);
+    ASSERT_EQ(Value::instances, 0);
+  }
+}
+
+TYPED_TEST(FlatHashMapTest, Collisions) {
+  FlatHashMap<int, int, CollidingHasher, typename TestFixture::Probe> fmap(
+      /*initial_capacity=*/0, /*load_limit_pct=*/100);
+
+  for (int rep = 0; rep < 3; rep++) {
+    // Insert four values which collide on the same bucket.
+    ASSERT_TRUE(fmap.Insert(1001, 1001).second);
+    ASSERT_TRUE(fmap.Insert(2001, 2001).second);
+    ASSERT_TRUE(fmap.Insert(3001, 3001).second);
+    ASSERT_TRUE(fmap.Insert(4001, 4001).second);
+
+    // Erase the 2nd one, it will create a tombstone.
+    ASSERT_TRUE(fmap.Erase(2001));
+    ASSERT_EQ(fmap.size(), 3u);
+
+    // Insert an entry that exists already, but happens to be located after the
+    // tombstone. Should still fail.
+    ASSERT_FALSE(fmap.Insert(3001, 3001).second);
+    ASSERT_EQ(fmap.size(), 3u);
+
+    ASSERT_TRUE(fmap.Erase(3001));
+    ASSERT_FALSE(fmap.Erase(2001));
+    ASSERT_TRUE(fmap.Erase(4001));
+
+    // The only element left is 101.
+    ASSERT_EQ(fmap.size(), 1u);
+
+    ASSERT_TRUE(fmap.Erase(1001));
+    ASSERT_EQ(fmap.size(), 0u);
+  }
+}
+
+TYPED_TEST(FlatHashMapTest, ProbeVisitsAllSlots) {
+  const int kIterations = 1024;
+  FlatHashMap<int, int, CollidingHasher, typename TestFixture::Probe> fmap(
+      /*initial_capacity=*/kIterations, /*load_limit_pct=*/100);
+  for (int i = 0; i < kIterations; i++) {
+    ASSERT_TRUE(fmap.Insert(i, i).second);
+  }
+  // If the hashmap hits an expansion the tests doesn't make sense. This test
+  // makes sense only if we actually saturate all buckets.
+  EXPECT_EQ(fmap.capacity(), static_cast<size_t>(kIterations));
+}
+
+TYPED_TEST(FlatHashMapTest, Iterator) {
+  FlatHashMap<int, int, base::AlreadyHashed<int>, typename TestFixture::Probe>
+      fmap;
+
+  auto it = fmap.GetIterator();
+  ASSERT_FALSE(it);
+
+  // Insert 3 values and iterate.
+  ASSERT_TRUE(fmap.Insert(1, 1001).second);
+  ASSERT_TRUE(fmap.Insert(2, 2001).second);
+  ASSERT_TRUE(fmap.Insert(3, 3001).second);
+  it = fmap.GetIterator();
+  for (int i = 1; i <= 3; i++) {
+    ASSERT_TRUE(it);
+    ASSERT_EQ(it.key(), i);
+    ASSERT_EQ(it.value(), i * 1000 + 1);
+    ++it;
+  }
+  ASSERT_FALSE(it);
+
+  // Erase the middle one and iterate.
+  fmap.Erase(2);
+  it = fmap.GetIterator();
+  ASSERT_TRUE(it);
+  ASSERT_EQ(it.key(), 1);
+  ++it;
+  ASSERT_TRUE(it);
+  ASSERT_EQ(it.key(), 3);
+  ++it;
+  ASSERT_FALSE(it);
+
+  // Erase everything and iterate.
+  fmap.Clear();
+  it = fmap.GetIterator();
+  ASSERT_FALSE(it);
+}
+
+// Test that Insert() and operator[] don't invalidate pointers if the key exists
+// already, regardless of the load factor.
+TYPED_TEST(FlatHashMapTest, DontRehashIfKeyAlreadyExists) {
+  static constexpr size_t kInitialCapacity = 128;
+  static std::array<size_t, 3> kLimitPct{25, 50, 100};
+
+  for (size_t limit_pct : kLimitPct) {
+    FlatHashMap<size_t, size_t, AlreadyHashed<size_t>,
+                typename TestFixture::Probe>
+        fmap(kInitialCapacity, static_cast<int>(limit_pct));
+
+    const size_t limit = kInitialCapacity * limit_pct / 100u;
+    ASSERT_EQ(fmap.capacity(), kInitialCapacity);
+    std::vector<size_t*> key_ptrs;
+    for (size_t i = 0; i < limit; i++) {
+      auto it_and_ins = fmap.Insert(i, i);
+      ASSERT_TRUE(it_and_ins.second);
+      ASSERT_EQ(fmap.capacity(), kInitialCapacity);
+      key_ptrs.push_back(it_and_ins.first);
+    }
+
+    // Re-insert existing items. It should not cause rehashing.
+    for (size_t i = 0; i < limit; i++) {
+      auto it_and_ins = fmap.Insert(i, i);
+      ASSERT_FALSE(it_and_ins.second);
+      ASSERT_EQ(it_and_ins.first, key_ptrs[i]);
+
+      size_t* key_ptr = &fmap[i];
+      ASSERT_EQ(key_ptr, key_ptrs[i]);
+      ASSERT_EQ(fmap.capacity(), kInitialCapacity);
+    }
+  }
+}
+
+TYPED_TEST(FlatHashMapTest, VsUnorderedMap) {
+  std::unordered_map<int, int, CollidingHasher> umap;
+  FlatHashMap<int, int, CollidingHasher, typename TestFixture::Probe> fmap;
+  std::minstd_rand0 rng(0);
+
+  for (int rep = 0; rep < 2; rep++) {
+    std::set<int> keys_copy;
+    const int kRange = 1024;
+
+    // Insert some random elements.
+    for (int i = 0; i < kRange; i++) {
+      int key = static_cast<int>(rng());
+      keys_copy.insert(key);
+      int value = key * 2;
+      auto it_and_inserted_u = umap.insert({key, value});
+      auto it_and_inserted_f = fmap.Insert(key, value);
+      ASSERT_EQ(it_and_inserted_u.second, it_and_inserted_f.second);
+      ASSERT_EQ(*it_and_inserted_f.first, value);
+      ASSERT_EQ(umap.size(), fmap.size());
+      int* res = fmap.Find(key);
+      ASSERT_NE(res, nullptr);
+      ASSERT_EQ(*res, value);
+      ASSERT_EQ(fmap[key], value);  // Test that operator[] behaves like Find().
+    }
+    // Look them up.
+    for (int key : keys_copy) {
+      int* res = fmap.Find(key);
+      ASSERT_NE(res, nullptr);
+      ASSERT_EQ(*res, key * 2);
+      ASSERT_EQ(umap.size(), fmap.size());
+    }
+
+    // Some further deletions / insertions / reinsertions.
+    for (int key : keys_copy) {
+      auto op = rng() % 4;
+
+      if (op < 2) {
+        // With a 50% chance, erase the key.
+        bool erased_u = umap.erase(key) > 0;
+        bool erased_f = fmap.Erase(key);
+        ASSERT_EQ(erased_u, erased_f);
+      } else if (op == 3) {
+        // With a 25% chance, re-insert the same key (should fail).
+        umap.insert({key, 0});
+        ASSERT_FALSE(fmap.Insert(key, 0).second);
+      } else {
+        // With a 25% chance, insert a new key.
+        umap.insert({key + kRange, (key + kRange) * 2});
+        ASSERT_TRUE(fmap.Insert(key + kRange, (key + kRange) * 2).second);
+      }
+
+      ASSERT_EQ(umap.size(), fmap.size());
+    }
+
+    // Re-look up keys. Note some of them might be deleted by the loop above.
+    for (int k : keys_copy) {
+      for (int i = 0; i < 2; i++) {
+        const int key = k + kRange * i;
+        int* res = fmap.Find(key);
+        if (umap.count(key)) {
+          ASSERT_NE(res, nullptr);
+          ASSERT_EQ(*res, key * 2);
+        } else {
+          ASSERT_EQ(res, nullptr);
+        }
+      }
+    }
+
+    fmap.Clear();
+    umap.clear();
+    ASSERT_EQ(fmap.size(), 0u);
+
+    for (int key : keys_copy)
+      ASSERT_EQ(fmap.Find(key), nullptr);
+  }
+}
+
+}  // namespace
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/http/BUILD.gn b/src/base/http/BUILD.gn
new file mode 100644
index 0000000..e086134
--- /dev/null
+++ b/src/base/http/BUILD.gn
@@ -0,0 +1,47 @@
+# Copyright (C) 2021 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("../../../gn/perfetto.gni")
+import("../../../gn/perfetto_component.gni")
+import("../../../gn/test.gni")
+
+perfetto_component("http") {
+  deps = [
+    "..:base",
+    "..:unix_socket",
+    "../../../gn:default_deps",
+  ]
+  public_deps = [
+    "../../../include/perfetto/base",
+    "../../../include/perfetto/ext/base/http",
+  ]
+  sources = [
+    "http_server.cc",
+    "sha1.cc",
+  ]
+}
+
+perfetto_unittest_source_set("unittests") {
+  testonly = true
+  deps = [
+    ":http",
+    "..:test_support",
+    "../../../gn:default_deps",
+    "../../../gn:gtest_and_gmock",
+  ]
+  sources = [
+    "http_server_unittest.cc",
+    "sha1_unittest.cc",
+  ]
+}
diff --git a/src/base/http/http_server.cc b/src/base/http/http_server.cc
new file mode 100644
index 0000000..17a1652
--- /dev/null
+++ b/src/base/http/http_server.cc
@@ -0,0 +1,579 @@
+/*
+ * Copyright (C) 2021 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 "perfetto/ext/base/http/http_server.h"
+
+#include <cinttypes>
+
+#include <vector>
+
+#include "perfetto/ext/base/base64.h"
+#include "perfetto/ext/base/endian.h"
+#include "perfetto/ext/base/http/sha1.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+
+namespace perfetto {
+namespace base {
+
+namespace {
+constexpr size_t kMaxPayloadSize = 32 * 1024 * 1024;
+constexpr size_t kMaxRequestSize = kMaxPayloadSize + 4096;
+
+enum WebsocketOpcode : uint8_t {
+  kOpcodeContinuation = 0x0,
+  kOpcodeText = 0x1,
+  kOpcodeBinary = 0x2,
+  kOpcodeDataUnused = 0x3,
+  kOpcodeClose = 0x8,
+  kOpcodePing = 0x9,
+  kOpcodePong = 0xA,
+  kOpcodeControlUnused = 0xB,
+};
+
+// From https://datatracker.ietf.org/doc/html/rfc6455#section-1.3.
+constexpr char kWebsocketGuid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+}  // namespace
+
+HttpServer::HttpServer(TaskRunner* task_runner, HttpRequestHandler* req_handler)
+    : task_runner_(task_runner), req_handler_(req_handler) {}
+HttpServer::~HttpServer() = default;
+
+void HttpServer::Start(int port) {
+  std::string ipv4_addr = "127.0.0.1:" + std::to_string(port);
+  std::string ipv6_addr = "[::1]:" + std::to_string(port);
+
+  sock4_ = UnixSocket::Listen(ipv4_addr, this, task_runner_, SockFamily::kInet,
+                              SockType::kStream);
+  bool ipv4_listening = sock4_ && sock4_->is_listening();
+  if (!ipv4_listening) {
+    PERFETTO_PLOG("Failed to listen on IPv4 socket");
+    sock4_.reset();
+  }
+
+  sock6_ = UnixSocket::Listen(ipv6_addr, this, task_runner_, SockFamily::kInet6,
+                              SockType::kStream);
+  bool ipv6_listening = sock6_ && sock6_->is_listening();
+  if (!ipv6_listening) {
+    PERFETTO_PLOG("Failed to listen on IPv6 socket");
+    sock6_.reset();
+  }
+}
+
+void HttpServer::AddAllowedOrigin(const std::string& origin) {
+  allowed_origins_.emplace_back(origin);
+}
+
+void HttpServer::OnNewIncomingConnection(
+    UnixSocket*,  // The listening socket, irrelevant here.
+    std::unique_ptr<UnixSocket> sock) {
+  PERFETTO_LOG("[HTTP] New connection");
+  clients_.emplace_back(std::move(sock));
+}
+
+void HttpServer::OnConnect(UnixSocket*, bool) {}
+
+void HttpServer::OnDisconnect(UnixSocket* sock) {
+  PERFETTO_LOG("[HTTP] Client disconnected");
+  for (auto it = clients_.begin(); it != clients_.end(); ++it) {
+    if (it->sock.get() == sock) {
+      req_handler_->OnHttpConnectionClosed(&*it);
+      clients_.erase(it);
+      return;
+    }
+  }
+  PERFETTO_DFATAL("[HTTP] Untracked client in OnDisconnect()");
+}
+
+void HttpServer::OnDataAvailable(UnixSocket* sock) {
+  HttpServerConnection* conn = nullptr;
+  for (auto it = clients_.begin(); it != clients_.end() && !conn; ++it)
+    conn = (it->sock.get() == sock) ? &*it : nullptr;
+  PERFETTO_CHECK(conn);
+
+  char* rxbuf = reinterpret_cast<char*>(conn->rxbuf.Get());
+  for (;;) {
+    size_t avail = conn->rxbuf_avail();
+    PERFETTO_CHECK(avail <= kMaxRequestSize);
+    if (avail == 0) {
+      conn->SendResponseAndClose("413 Payload Too Large");
+      return;
+    }
+    size_t rsize = sock->Receive(&rxbuf[conn->rxbuf_used], avail);
+    conn->rxbuf_used += rsize;
+    if (rsize == 0 || conn->rxbuf_avail() == 0)
+      break;
+  }
+
+  // At this point |rxbuf| can contain a partial HTTP request, a full one or
+  // more (in case of HTTP Keepalive pipelining).
+  for (;;) {
+    size_t bytes_consumed;
+
+    if (conn->is_websocket()) {
+      bytes_consumed = ParseOneWebsocketFrame(conn);
+    } else {
+      bytes_consumed = ParseOneHttpRequest(conn);
+    }
+
+    if (bytes_consumed == 0)
+      break;
+    memmove(rxbuf, &rxbuf[bytes_consumed], conn->rxbuf_used - bytes_consumed);
+    conn->rxbuf_used -= bytes_consumed;
+  }
+}
+
+// Parses the HTTP request and invokes HandleRequest(). It returns the size of
+// the HTTP header + body that has been processed or 0 if there isn't enough
+// data for a full HTTP request in the buffer.
+size_t HttpServer::ParseOneHttpRequest(HttpServerConnection* conn) {
+  auto* rxbuf = reinterpret_cast<char*>(conn->rxbuf.Get());
+  StringView buf_view(rxbuf, conn->rxbuf_used);
+  bool has_parsed_first_line = false;
+  bool all_headers_received = false;
+  HttpRequest http_req(conn);
+  size_t body_size = 0;
+
+  // This loop parses the HTTP request headers and sets the |body_offset|.
+  while (!buf_view.empty()) {
+    size_t next = buf_view.find('\n');
+    if (next == StringView::npos)
+      break;
+    StringView line = buf_view.substr(0, next);
+    buf_view = buf_view.substr(next + 1);  // Eat the current line.
+    while (!line.empty() && (line.at(line.size() - 1) == '\r' ||
+                             line.at(line.size() - 1) == '\n')) {
+      line = line.substr(0, line.size() - 1);
+    }
+
+    if (!has_parsed_first_line) {
+      // Parse the "GET /xxx HTTP/1.1" line.
+      has_parsed_first_line = true;
+      size_t space = line.find(' ');
+      if (space == std::string::npos || space + 2 >= line.size()) {
+        conn->SendResponseAndClose("400 Bad Request");
+        return 0;
+      }
+      http_req.method = line.substr(0, space);
+      size_t uri_size = line.find(' ', space + 1) - (space + 1);
+      http_req.uri = line.substr(space + 1, uri_size);
+    } else if (line.empty()) {
+      all_headers_received = true;
+      // The CR-LF marker that separates headers from body.
+      break;
+    } else {
+      // Parse HTTP headers, e.g. "Content-Length: 1234".
+      size_t col = line.find(':');
+      if (col == StringView::npos) {
+        PERFETTO_DLOG("[HTTP] Malformed HTTP header: \"%s\"",
+                      line.ToStdString().c_str());
+        conn->SendResponseAndClose("400 Bad Request", {}, "Bad HTTP header");
+        return 0;
+      }
+      auto hdr_name = line.substr(0, col);
+      auto hdr_value = line.substr(col + 2);
+      if (http_req.num_headers < http_req.headers.size()) {
+        http_req.headers[http_req.num_headers++] = {hdr_name, hdr_value};
+      } else {
+        conn->SendResponseAndClose("400 Bad Request", {},
+                                   "Too many HTTP headers");
+      }
+
+      if (hdr_name.CaseInsensitiveEq("content-length")) {
+        body_size = static_cast<size_t>(atoi(hdr_value.ToStdString().c_str()));
+      } else if (hdr_name.CaseInsensitiveEq("origin")) {
+        http_req.origin = hdr_value;
+        if (IsOriginAllowed(hdr_value))
+          conn->origin_allowed_ = hdr_value.ToStdString();
+      } else if (hdr_name.CaseInsensitiveEq("connection")) {
+        conn->keepalive_ = hdr_value.CaseInsensitiveEq("keep-alive");
+        http_req.is_websocket_handshake =
+            hdr_value.CaseInsensitiveEq("upgrade");
+      }
+    }
+  }
+
+  // At this point |buf_view| has been stripped of the header and contains the
+  // request body. We don't know yet if we have all the bytes for it or not.
+  PERFETTO_CHECK(buf_view.size() <= conn->rxbuf_used);
+  const size_t headers_size = conn->rxbuf_used - buf_view.size();
+
+  if (body_size + headers_size >= kMaxRequestSize ||
+      body_size > kMaxPayloadSize) {
+    conn->SendResponseAndClose("413 Payload Too Large");
+    return 0;
+  }
+
+  // If we can't read the full request return and try again next time with more
+  // data.
+  if (!all_headers_received || buf_view.size() < body_size)
+    return 0;
+
+  http_req.body = buf_view.substr(0, body_size);
+
+  PERFETTO_LOG("[HTTP] %.*s %.*s [body=%zuB, origin=\"%.*s\"]",
+               static_cast<int>(http_req.method.size()), http_req.method.data(),
+               static_cast<int>(http_req.uri.size()), http_req.uri.data(),
+               http_req.body.size(), static_cast<int>(http_req.origin.size()),
+               http_req.origin.data());
+
+  if (http_req.method == "OPTIONS") {
+    HandleCorsPreflightRequest(http_req);
+  } else {
+    // Let the HttpHandler handle the request.
+    req_handler_->OnHttpRequest(http_req);
+  }
+
+  // The handler is expected to send a response. If not, bail with a HTTP 500.
+  if (!conn->headers_sent_)
+    conn->SendResponseAndClose("500 Internal Server Error");
+
+  // Allow chaining multiple responses in the same HTTP-Keepalive connection.
+  conn->headers_sent_ = false;
+
+  return headers_size + body_size;
+}
+
+void HttpServer::HandleCorsPreflightRequest(const HttpRequest& req) {
+  req.conn->SendResponseAndClose(
+      "204 No Content",
+      {
+          "Access-Control-Allow-Methods: POST, GET, OPTIONS",  //
+          "Access-Control-Allow-Headers: *",                   //
+          "Access-Control-Max-Age: 86400",                     //
+      });
+}
+
+bool HttpServer::IsOriginAllowed(StringView origin) {
+  for (const std::string& allowed_origin : allowed_origins_) {
+    if (origin.CaseInsensitiveEq(StringView(allowed_origin))) {
+      return true;
+    }
+  }
+  if (!origin_error_logged_ && !origin.empty()) {
+    origin_error_logged_ = true;
+    PERFETTO_ELOG(
+        "[HTTP] The origin \"%.*s\" is not allowed, Access-Control-Allow-Origin"
+        " won't be emitted. If this request comes from a browser it will fail.",
+        static_cast<int>(origin.size()), origin.data());
+  }
+  return false;
+}
+
+void HttpServerConnection::UpgradeToWebsocket(const HttpRequest& req) {
+  PERFETTO_CHECK(req.is_websocket_handshake);
+
+  // |origin_allowed_| is set to the req.origin only if it's in the allowlist.
+  if (origin_allowed_.empty())
+    return SendResponseAndClose("403 Forbidden", {}, "Origin not allowed");
+
+  auto ws_ver = req.GetHeader("sec-webSocket-version").value_or(StringView());
+  auto ws_key = req.GetHeader("sec-webSocket-key").value_or(StringView());
+
+  if (!ws_ver.CaseInsensitiveEq("13"))
+    return SendResponseAndClose("505 HTTP Version Not Supported", {});
+
+  if (ws_key.size() != 24) {
+    // The nonce must be a base64 encoded 16 bytes value (24 after base64).
+    return SendResponseAndClose("400 Bad Request", {});
+  }
+
+  // From https://datatracker.ietf.org/doc/html/rfc6455#section-1.3 :
+  // For this header field, the server has to take the value (as present
+  // in the header field, e.g., the base64-encoded [RFC4648] version minus
+  // any leading and trailing whitespace) and concatenate this with the
+  // Globally Unique Identifier (GUID, [RFC4122]) "258EAFA5-E914-47DA-
+  // 95CA-C5AB0DC85B11" in string form, which is unlikely to be used by
+  // network endpoints that do not understand the WebSocket Protocol.  A
+  // SHA-1 hash (160 bits) [FIPS.180-3], base64-encoded (see Section 4 of
+  // [RFC4648]), of this concatenation is then returned in the server's
+  // handshake.
+  StackString<128> signed_nonce("%.*s%s", static_cast<int>(ws_key.size()),
+                                ws_key.data(), kWebsocketGuid);
+  auto digest = SHA1Hash(signed_nonce.c_str(), signed_nonce.len());
+  std::string digest_b64 = Base64Encode(digest.data(), digest.size());
+
+  StackString<128> accept_hdr("Sec-WebSocket-Accept: %s", digest_b64.c_str());
+
+  std::initializer_list<const char*> headers = {
+      "Upgrade: websocket",   //
+      "Connection: Upgrade",  //
+      accept_hdr.c_str(),     //
+  };
+  PERFETTO_DLOG("[HTTP] Handshaking WebSocket for %.*s",
+                static_cast<int>(req.uri.size()), req.uri.data());
+  for (const char* hdr : headers)
+    PERFETTO_DLOG("> %s", hdr);
+
+  SendResponseHeaders("101 Switching Protocols", headers,
+                      HttpServerConnection::kOmitContentLength);
+
+  is_websocket_ = true;
+}
+
+size_t HttpServer::ParseOneWebsocketFrame(HttpServerConnection* conn) {
+  auto* rxbuf = reinterpret_cast<uint8_t*>(conn->rxbuf.Get());
+  const size_t frame_size = conn->rxbuf_used;
+  uint8_t* rd = rxbuf;
+  uint8_t* const end = rxbuf + frame_size;
+
+  auto avail = [&] {
+    PERFETTO_CHECK(rd <= end);
+    return static_cast<size_t>(end - rd);
+  };
+
+  // From https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 :
+  //   0                   1                   2                   3
+  //   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+  //  +-+-+-+-+-------+-+-------------+-------------------------------+
+  //  |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
+  //  |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
+  //  |N|V|V|V|       |S|             |   (if payload len==126/127)   |
+  //  | |1|2|3|       |K|             |                               |
+  //  +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+  //  |     Extended payload length continued, if payload len == 127  |
+  //  + - - - - - - - - - - - - - - - +-------------------------------+
+  //  |                               |Masking-key, if MASK set to 1  |
+  //  +-------------------------------+-------------------------------+
+  //  | Masking-key (continued)       |          Payload Data         |
+  //  +-------------------------------- - - - - - - - - - - - - - - - +
+  //  :                     Payload Data continued ...                :
+  //  + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+  //  |                     Payload Data continued ...                |
+  //  +---------------------------------------------------------------+
+
+  if (avail() < 2)
+    return 0;  // Can't even decode the frame header. Wait for more data.
+
+  uint8_t h0 = *(rd++);
+  uint8_t h1 = *(rd++);
+  const bool fin = !!(h0 & 0x80);  // This bit is set if this frame is the last
+                                   // data to complete this message.
+  const uint8_t opcode = h0 & 0x0F;
+
+  const bool has_mask = !!(h1 & 0x80);
+  uint64_t payload_len_u64 = (h1 & 0x7F);
+  uint8_t extended_payload_size = 0;
+  if (payload_len_u64 == 126) {
+    extended_payload_size = 2;
+  } else if (payload_len_u64 == 127) {
+    extended_payload_size = 8;
+  }
+
+  if (extended_payload_size > 0) {
+    if (avail() < extended_payload_size)
+      return 0;  // Not enough data to read the extended header.
+    payload_len_u64 = 0;
+    for (uint8_t i = 0; i < extended_payload_size; ++i) {
+      payload_len_u64 <<= 8;
+      payload_len_u64 |= *(rd++);
+    }
+  }
+
+  if (payload_len_u64 >= kMaxPayloadSize) {
+    PERFETTO_ELOG("[HTTP] Websocket payload too big (%" PRIu64 " > %zu)",
+                  payload_len_u64, kMaxPayloadSize);
+    conn->Close();
+    return 0;
+  }
+  const size_t payload_len = static_cast<size_t>(payload_len_u64);
+
+  if (!has_mask) {
+    // https://datatracker.ietf.org/doc/html/rfc6455#section-5.1
+    // The server MUST close the connection upon receiving a frame that is
+    // not masked.
+    PERFETTO_ELOG("[HTTP] Websocket inbound frames must be masked");
+    conn->Close();
+    return 0;
+  }
+
+  uint8_t mask[4];
+  if (avail() < sizeof(mask))
+    return 0;  // Not enough data to read the masking key.
+  memcpy(mask, rd, sizeof(mask));
+  rd += sizeof(mask);
+
+  PERFETTO_DLOG(
+      "[HTTP] Websocket fin=%d opcode=%u, payload_len=%zu (avail=%zu), "
+      "mask=%02x%02x%02x%02x",
+      fin, opcode, payload_len, avail(), mask[0], mask[1], mask[2], mask[3]);
+
+  if (avail() < payload_len)
+    return 0;  // Not enouh data to read the payload.
+  uint8_t* const payload_start = rd;
+
+  // Unmask the payload.
+  for (uint32_t i = 0; i < payload_len; ++i)
+    payload_start[i] ^= mask[i % sizeof(mask)];
+
+  if (opcode == kOpcodePing) {
+    PERFETTO_DLOG("[HTTP] Websocket PING");
+    conn->SendWebsocketFrame(kOpcodePong, payload_start, payload_len);
+  } else if (opcode == kOpcodeBinary || opcode == kOpcodeText ||
+             opcode == kOpcodeContinuation) {
+    // We do NOT handle fragmentation. We propagate all fragments as individual
+    // messages, breaking the message-oriented nature of websockets. We do this
+    // because in all our use cases we need only a byte stream without caring
+    // about message boundaries.
+    // If we wanted to support fragmentation, we'd have to stash
+    // kOpcodeContinuation messages in a buffer, until we FIN bit is set.
+    // When loading traces with trace processor, the messages can be up to
+    // 32MB big (SLICE_SIZE in trace_stream.ts). The double-buffering would
+    // slow down significantly trace loading with no benefits.
+    WebsocketMessage msg(conn);
+    msg.data =
+        StringView(reinterpret_cast<const char*>(payload_start), payload_len);
+    msg.is_text = opcode == kOpcodeText;
+    req_handler_->OnWebsocketMessage(msg);
+  } else if (opcode == kOpcodeClose) {
+    conn->Close();
+  } else {
+    PERFETTO_LOG("Unsupported WebSocket opcode: %d", opcode);
+  }
+  return static_cast<size_t>(rd - rxbuf) + payload_len;
+}
+
+void HttpServerConnection::SendResponseHeaders(
+    const char* http_code,
+    std::initializer_list<const char*> headers,
+    size_t content_length) {
+  PERFETTO_CHECK(!headers_sent_);
+  PERFETTO_CHECK(!is_websocket_);
+  headers_sent_ = true;
+  std::vector<char> resp_hdr;
+  resp_hdr.reserve(512);
+  bool has_connection_header = false;
+
+  auto append = [&resp_hdr](const char* str) {
+    resp_hdr.insert(resp_hdr.end(), str, str + strlen(str));
+  };
+
+  append("HTTP/1.1 ");
+  append(http_code);
+  append("\r\n");
+  for (const char* hdr : headers) {
+    if (strlen(hdr) == 0)
+      continue;
+    has_connection_header |= strncasecmp(hdr, "connection:", 11) == 0;
+    append(hdr);
+    append("\r\n");
+  }
+  content_len_actual_ = 0;
+  content_len_headers_ = content_length;
+  if (content_length != kOmitContentLength) {
+    append("Content-Length: ");
+    append(std::to_string(content_length).c_str());
+    append("\r\n");
+  }
+  if (!has_connection_header) {
+    // Various clients (e.g., python's http.client) assume that a HTTP
+    // connection is keep-alive if the server says nothing, even when they do
+    // NOT ask for it. Hence we must be explicit. If we are about to close the
+    // connection, we must say so.
+    append(keepalive_ ? "Connection: keep-alive\r\n" : "Connection: close\r\n");
+  }
+  if (!origin_allowed_.empty()) {
+    append("Access-Control-Allow-Origin: ");
+    append(origin_allowed_.c_str());
+    append("\r\n");
+    append("Vary: Origin\r\n");
+  }
+  append("\r\n");  // End-of-headers marker.
+  sock->Send(resp_hdr.data(),
+             resp_hdr.size());  // Send response headers.
+}
+
+void HttpServerConnection::SendResponseBody(const void* data, size_t len) {
+  PERFETTO_CHECK(!is_websocket_);
+  if (data == nullptr) {
+    PERFETTO_DCHECK(len == 0);
+    return;
+  }
+  content_len_actual_ += len;
+  PERFETTO_CHECK(content_len_actual_ <= content_len_headers_ ||
+                 content_len_headers_ == kOmitContentLength);
+  sock->Send(data, len);
+}
+
+void HttpServerConnection::Close() {
+  sock->Shutdown(/*notify=*/true);
+}
+
+void HttpServerConnection::SendResponse(
+    const char* http_code,
+    std::initializer_list<const char*> headers,
+    StringView content,
+    bool force_close) {
+  if (force_close)
+    keepalive_ = false;
+  SendResponseHeaders(http_code, headers, content.size());
+  SendResponseBody(content.data(), content.size());
+  if (!keepalive_)
+    Close();
+}
+
+void HttpServerConnection::SendWebsocketMessage(const void* data, size_t len) {
+  SendWebsocketFrame(kOpcodeBinary, data, len);
+}
+
+void HttpServerConnection::SendWebsocketFrame(uint8_t opcode,
+                                              const void* payload,
+                                              size_t payload_len) {
+  PERFETTO_CHECK(is_websocket_);
+
+  uint8_t hdr[10]{};
+  uint32_t hdr_len = 0;
+
+  hdr[0] = opcode | 0x80 /* FIN=1, no fragmentation */;
+  if (payload_len < 126) {
+    hdr_len = 2;
+    hdr[1] = static_cast<uint8_t>(payload_len);
+  } else if (payload_len < 0xffff) {
+    hdr_len = 4;
+    hdr[1] = 126;  // Special value: Header extends for 2 bytes.
+    uint16_t len_be = HostToBE16(static_cast<uint16_t>(payload_len));
+    memcpy(&hdr[2], &len_be, sizeof(len_be));
+  } else {
+    hdr_len = 10;
+    hdr[1] = 127;  // Special value: Header extends for 4 bytes.
+    uint64_t len_be = HostToBE64(payload_len);
+    memcpy(&hdr[2], &len_be, sizeof(len_be));
+  }
+
+  sock->Send(hdr, hdr_len);
+  if (payload && payload_len > 0)
+    sock->Send(payload, payload_len);
+}
+
+HttpServerConnection::HttpServerConnection(std::unique_ptr<UnixSocket> s)
+    : sock(std::move(s)), rxbuf(PagedMemory::Allocate(kMaxRequestSize)) {}
+
+HttpServerConnection::~HttpServerConnection() = default;
+
+Optional<StringView> HttpRequest::GetHeader(StringView name) const {
+  for (size_t i = 0; i < num_headers; i++) {
+    if (headers[i].name.CaseInsensitiveEq(name))
+      return headers[i].value;
+  }
+  return nullopt;
+}
+
+HttpRequestHandler::~HttpRequestHandler() = default;
+void HttpRequestHandler::OnWebsocketMessage(const WebsocketMessage&) {}
+void HttpRequestHandler::OnHttpConnectionClosed(HttpServerConnection*) {}
+
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/http/http_server_unittest.cc b/src/base/http/http_server_unittest.cc
new file mode 100644
index 0000000..9726ab6
--- /dev/null
+++ b/src/base/http/http_server_unittest.cc
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2021 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 "perfetto/ext/base/http/http_server.h"
+
+#include <initializer_list>
+#include <string>
+
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/unix_socket.h"
+#include "src/base/test/test_task_runner.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+using testing::_;
+using testing::Invoke;
+using testing::InvokeWithoutArgs;
+using testing::NiceMock;
+
+constexpr int kTestPort = 5127;  // Chosen with a fair dice roll.
+
+class MockHttpHandler : public HttpRequestHandler {
+ public:
+  MOCK_METHOD1(OnHttpRequest, void(const HttpRequest&));
+  MOCK_METHOD1(OnHttpConnectionClosed, void(HttpServerConnection*));
+  MOCK_METHOD1(OnWebsocketMessage, void(const WebsocketMessage&));
+};
+
+class HttpCli {
+ public:
+  explicit HttpCli(TestTaskRunner* ttr) : task_runner_(ttr) {
+    sock = UnixSocketRaw::CreateMayFail(SockFamily::kInet, SockType::kStream);
+    sock.SetBlocking(true);
+    sock.Connect("127.0.0.1:" + std::to_string(kTestPort));
+  }
+
+  void SendHttpReq(std::initializer_list<std::string> headers,
+                   const std::string& body = "") {
+    for (auto& header : headers)
+      sock.SendStr(header + "\r\n");
+    if (!body.empty())
+      sock.SendStr("Content-Length: " + std::to_string(body.size()) + "\r\n");
+    sock.SendStr("\r\n");
+    sock.SendStr(body);
+  }
+
+  std::string Recv(size_t min_bytes) {
+    static int n = 0;
+    auto checkpoint_name = "rx_" + std::to_string(n++);
+    auto checkpoint = task_runner_->CreateCheckpoint(checkpoint_name);
+    std::string rxbuf;
+    sock.SetBlocking(false);
+    task_runner_->AddFileDescriptorWatch(sock.fd(), [&] {
+      char buf[1024]{};
+      auto rsize = PERFETTO_EINTR(sock.Receive(buf, sizeof(buf)));
+      ASSERT_GE(rsize, 0);
+      rxbuf.append(buf, static_cast<size_t>(rsize));
+      if (rsize == 0 || (min_bytes && rxbuf.length() >= min_bytes))
+        checkpoint();
+    });
+    task_runner_->RunUntilCheckpoint(checkpoint_name);
+    task_runner_->RemoveFileDescriptorWatch(sock.fd());
+    return rxbuf;
+  }
+
+  std::string RecvAndWaitConnClose() { return Recv(0); }
+
+  TestTaskRunner* task_runner_;
+  UnixSocketRaw sock;
+};
+
+class HttpServerTest : public ::testing::Test {
+ public:
+  HttpServerTest() : srv_(&task_runner_, &handler_) { srv_.Start(kTestPort); }
+
+  TestTaskRunner task_runner_;
+  MockHttpHandler handler_;
+  HttpServer srv_;
+};
+
+TEST_F(HttpServerTest, GET) {
+  const int kIterations = 3;
+  EXPECT_CALL(handler_, OnHttpRequest(_))
+      .Times(kIterations)
+      .WillRepeatedly(Invoke([](const HttpRequest& req) {
+        EXPECT_EQ(req.uri.ToStdString(), "/foo/bar");
+        EXPECT_EQ(req.method.ToStdString(), "GET");
+        EXPECT_EQ(req.origin.ToStdString(), "https://example.com");
+        EXPECT_EQ("42",
+                  req.GetHeader("X-header").value_or("N/A").ToStdString());
+        EXPECT_EQ("foo",
+                  req.GetHeader("X-header2").value_or("N/A").ToStdString());
+        EXPECT_FALSE(req.is_websocket_handshake);
+        req.conn->SendResponseAndClose("200 OK", {}, "<html>");
+      }));
+  EXPECT_CALL(handler_, OnHttpConnectionClosed(_)).Times(kIterations);
+
+  for (int i = 0; i < 3; i++) {
+    HttpCli cli(&task_runner_);
+    cli.SendHttpReq(
+        {
+            "GET /foo/bar HTTP/1.1",        //
+            "Origin: https://example.com",  //
+            "X-header: 42",                 //
+            "X-header2: foo",               //
+        },
+        "");
+    EXPECT_EQ(cli.RecvAndWaitConnClose(),
+              "HTTP/1.1 200 OK\r\n"
+              "Content-Length: 6\r\n"
+              "Connection: close\r\n"
+              "\r\n<html>");
+  }
+}
+
+TEST_F(HttpServerTest, GET_404) {
+  HttpCli cli(&task_runner_);
+  EXPECT_CALL(handler_, OnHttpRequest(_))
+      .WillOnce(Invoke([&](const HttpRequest& req) {
+        EXPECT_EQ(req.uri.ToStdString(), "/404");
+        EXPECT_EQ(req.method.ToStdString(), "GET");
+        req.conn->SendResponseAndClose("404 Not Found");
+      }));
+  cli.SendHttpReq({"GET /404 HTTP/1.1"}, "");
+  EXPECT_CALL(handler_, OnHttpConnectionClosed(_));
+  EXPECT_EQ(cli.RecvAndWaitConnClose(),
+            "HTTP/1.1 404 Not Found\r\n"
+            "Content-Length: 0\r\n"
+            "Connection: close\r\n"
+            "\r\n");
+}
+
+TEST_F(HttpServerTest, POST) {
+  HttpCli cli(&task_runner_);
+
+  EXPECT_CALL(handler_, OnHttpRequest(_))
+      .WillOnce(Invoke([&](const HttpRequest& req) {
+        EXPECT_EQ(req.uri.ToStdString(), "/rpc");
+        EXPECT_EQ(req.method.ToStdString(), "POST");
+        EXPECT_EQ(req.origin.ToStdString(), "https://example.com");
+        EXPECT_EQ("foo", req.GetHeader("X-1").value_or("N/A").ToStdString());
+        EXPECT_EQ(req.body.ToStdString(), "the\r\npost\nbody\r\n\r\n");
+        req.conn->SendResponseAndClose("200 OK");
+      }));
+
+  cli.SendHttpReq(
+      {"POST /rpc HTTP/1.1", "Origin: https://example.com", "X-1: foo"},
+      "the\r\npost\nbody\r\n\r\n");
+  EXPECT_CALL(handler_, OnHttpConnectionClosed(_));
+  EXPECT_EQ(cli.RecvAndWaitConnClose(),
+            "HTTP/1.1 200 OK\r\n"
+            "Content-Length: 0\r\n"
+            "Connection: close\r\n"
+            "\r\n");
+}
+
+// An unhandled request should cause a HTTP 500.
+TEST_F(HttpServerTest, Unhadled_500) {
+  HttpCli cli(&task_runner_);
+  EXPECT_CALL(handler_, OnHttpRequest(_));
+  cli.SendHttpReq({"GET /unhandled HTTP/1.1"});
+  EXPECT_CALL(handler_, OnHttpConnectionClosed(_));
+  EXPECT_EQ(cli.RecvAndWaitConnClose(),
+            "HTTP/1.1 500 Internal Server Error\r\n"
+            "Content-Length: 0\r\n"
+            "Connection: close\r\n"
+            "\r\n");
+}
+
+// Send three requests within the same keepalive connection.
+TEST_F(HttpServerTest, POST_Keepalive) {
+  HttpCli cli(&task_runner_);
+  static const int kNumRequests = 3;
+  int req_num = 0;
+  EXPECT_CALL(handler_, OnHttpConnectionClosed(_)).Times(1);
+  EXPECT_CALL(handler_, OnHttpRequest(_))
+      .Times(3)
+      .WillRepeatedly(Invoke([&](const HttpRequest& req) {
+        EXPECT_EQ(req.uri.ToStdString(), "/" + std::to_string(req_num));
+        EXPECT_EQ(req.method.ToStdString(), "POST");
+        EXPECT_EQ(req.body.ToStdString(), "body" + std::to_string(req_num));
+        req.conn->SendResponseHeaders("200 OK");
+        if (++req_num == kNumRequests)
+          req.conn->Close();
+      }));
+
+  for (int i = 0; i < kNumRequests; i++) {
+    auto i_str = std::to_string(i);
+    cli.SendHttpReq({"POST /" + i_str + " HTTP/1.1", "Connection: keep-alive"},
+                    "body" + i_str);
+  }
+
+  std::string expected_response;
+  for (int i = 0; i < kNumRequests; i++) {
+    expected_response +=
+        "HTTP/1.1 200 OK\r\n"
+        "Content-Length: 0\r\n"
+        "Connection: keep-alive\r\n"
+        "\r\n";
+  }
+  EXPECT_EQ(cli.RecvAndWaitConnClose(), expected_response);
+}
+
+TEST_F(HttpServerTest, Websocket) {
+  srv_.AddAllowedOrigin("http://foo.com");
+  srv_.AddAllowedOrigin("http://websocket.com");
+  for (int rep = 0; rep < 3; rep++) {
+    HttpCli cli(&task_runner_);
+    EXPECT_CALL(handler_, OnHttpRequest(_))
+        .WillOnce(Invoke([&](const HttpRequest& req) {
+          EXPECT_EQ(req.uri.ToStdString(), "/websocket");
+          EXPECT_EQ(req.method.ToStdString(), "GET");
+          EXPECT_EQ(req.origin.ToStdString(), "http://websocket.com");
+          EXPECT_TRUE(req.is_websocket_handshake);
+          req.conn->UpgradeToWebsocket(req);
+        }));
+
+    cli.SendHttpReq({
+        "GET /websocket HTTP/1.1",                      //
+        "Origin: http://websocket.com",                 //
+        "Connection: upgrade",                          //
+        "Upgrade: websocket",                           //
+        "Sec-WebSocket-Version: 13",                    //
+        "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",  //
+    });
+    std::string expected_resp =
+        "HTTP/1.1 101 Switching Protocols\r\n"
+        "Upgrade: websocket\r\n"
+        "Connection: Upgrade\r\n"
+        "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
+        "Access-Control-Allow-Origin: http://websocket.com\r\n"
+        "Vary: Origin\r\n"
+        "\r\n";
+    EXPECT_EQ(cli.Recv(expected_resp.size()), expected_resp);
+
+    for (int i = 0; i < 3; i++) {
+      EXPECT_CALL(handler_, OnWebsocketMessage(_))
+          .WillOnce(Invoke([i](const WebsocketMessage& msg) {
+            EXPECT_EQ(msg.data.ToStdString(), "test message");
+            StackString<6> resp("PONG%d", i);
+            msg.conn->SendWebsocketMessage(resp.c_str(), resp.len());
+          }));
+
+      // A frame from a real tcpdump capture:
+      //   1... .... = Fin: True
+      //   .000 .... = Reserved: 0x0
+      //   .... 0001 = Opcode: Text (1)
+      //   1... .... = Mask: True
+      //   .000 1100 = Payload length: 12
+      //   Masking-Key: e17e8eb9
+      //   Masked payload: "test message"
+      cli.sock.SendStr(
+          "\x81\x8c\xe1\x7e\x8e\xb9\x95\x1b\xfd\xcd\xc1\x13\xeb\xca\x92\x1f\xe9"
+          "\xdc");
+      EXPECT_EQ(cli.Recv(2 + 5), "\x82\x05PONG" + std::to_string(i));
+    }
+
+    cli.sock.Shutdown();
+    auto checkpoint_name = "ws_close_" + std::to_string(rep);
+    auto ws_close = task_runner_.CreateCheckpoint(checkpoint_name);
+    EXPECT_CALL(handler_, OnHttpConnectionClosed(_))
+        .WillOnce(InvokeWithoutArgs(ws_close));
+    task_runner_.RunUntilCheckpoint(checkpoint_name);
+  }
+}
+
+TEST_F(HttpServerTest, Websocket_OriginNotAllowed) {
+  srv_.AddAllowedOrigin("http://websocket.com");
+  srv_.AddAllowedOrigin("http://notallowed.commando");
+  srv_.AddAllowedOrigin("http://iamnotallowed.com");
+  srv_.AddAllowedOrigin("iamnotallowed.com");
+  // The origin must match in full, including scheme. This won't match.
+  srv_.AddAllowedOrigin("notallowed.com");
+
+  HttpCli cli(&task_runner_);
+  EXPECT_CALL(handler_, OnHttpConnectionClosed(_));
+  EXPECT_CALL(handler_, OnHttpRequest(_))
+      .WillOnce(Invoke([&](const HttpRequest& req) {
+        EXPECT_EQ(req.origin.ToStdString(), "http://notallowed.com");
+        EXPECT_TRUE(req.is_websocket_handshake);
+        req.conn->UpgradeToWebsocket(req);
+      }));
+
+  cli.SendHttpReq({
+      "GET /websocket HTTP/1.1",                      //
+      "Origin: http://notallowed.com",                //
+      "Connection: upgrade",                          //
+      "Upgrade: websocket",                           //
+      "Sec-WebSocket-Version: 13",                    //
+      "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",  //
+  });
+  std::string expected_resp =
+      "HTTP/1.1 403 Forbidden\r\n"
+      "Content-Length: 18\r\n"
+      "Connection: close\r\n"
+      "\r\n"
+      "Origin not allowed";
+
+  EXPECT_EQ(cli.Recv(expected_resp.size()), expected_resp);
+  cli.sock.Shutdown();
+}
+
+}  // namespace
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/http/sha1.cc b/src/base/http/sha1.cc
new file mode 100644
index 0000000..da3f753
--- /dev/null
+++ b/src/base/http/sha1.cc
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2021 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 "perfetto/ext/base/http/sha1.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+// From chrome_elf/sha1/sha1.cc.
+
+namespace perfetto {
+namespace base {
+
+namespace {
+
+inline uint32_t BSwap32(uint32_t x) {
+#if defined(__GNUC__)
+  return __builtin_bswap32(x);
+#elif defined(_MSC_VER)
+  return _byteswap_ulong(val);
+#else
+  return (((x & 0xff000000u) >> 24) | ((x & 0x00ff0000u) >> 8) |
+          ((x & 0x0000ff00u) << 8) | ((x & 0x000000ffu) << 24));
+#endif
+}
+
+// Usage example:
+//
+// SecureHashAlgorithm sha;
+// while(there is data to hash)
+//   sha.Update(moredata, size of data);
+// sha.Final();
+// memcpy(somewhere, sha.Digest(), 20);
+//
+// to reuse the instance of sha, call sha.Init();
+class SecureHashAlgorithm {
+ public:
+  SecureHashAlgorithm() { Init(); }
+
+  void Init();
+  void Update(const void* data, size_t nbytes);
+  void Final();
+
+  // 20 bytes of message digest.
+  const unsigned char* Digest() const {
+    return reinterpret_cast<const unsigned char*>(H);
+  }
+
+ private:
+  void Pad();
+  void Process();
+
+  uint32_t A, B, C, D, E;
+
+  uint32_t H[5];
+
+  union {
+    uint32_t W[80];
+    uint8_t M[64];
+  };
+
+  uint32_t cursor;
+  uint64_t l;
+};
+
+//------------------------------------------------------------------------------
+// Private functions
+//------------------------------------------------------------------------------
+
+// Identifier names follow notation in FIPS PUB 180-3, where you'll
+// also find a description of the algorithm:
+// http://csrc.nist.gov/publications/fips/fips180-3/fips180-3_final.pdf
+
+inline uint32_t f(uint32_t t, uint32_t B, uint32_t C, uint32_t D) {
+  if (t < 20) {
+    return (B & C) | ((~B) & D);
+  } else if (t < 40) {
+    return B ^ C ^ D;
+  } else if (t < 60) {
+    return (B & C) | (B & D) | (C & D);
+  } else {
+    return B ^ C ^ D;
+  }
+}
+
+inline uint32_t S(uint32_t n, uint32_t X) {
+  return (X << n) | (X >> (32 - n));
+}
+
+inline uint32_t K(uint32_t t) {
+  if (t < 20) {
+    return 0x5a827999;
+  } else if (t < 40) {
+    return 0x6ed9eba1;
+  } else if (t < 60) {
+    return 0x8f1bbcdc;
+  } else {
+    return 0xca62c1d6;
+  }
+}
+
+void SecureHashAlgorithm::Init() {
+  A = 0;
+  B = 0;
+  C = 0;
+  D = 0;
+  E = 0;
+  cursor = 0;
+  l = 0;
+  H[0] = 0x67452301;
+  H[1] = 0xefcdab89;
+  H[2] = 0x98badcfe;
+  H[3] = 0x10325476;
+  H[4] = 0xc3d2e1f0;
+}
+
+void SecureHashAlgorithm::Update(const void* data, size_t nbytes) {
+  const uint8_t* d = reinterpret_cast<const uint8_t*>(data);
+  while (nbytes--) {
+    M[cursor++] = *d++;
+    if (cursor >= 64)
+      Process();
+    l += 8;
+  }
+}
+
+void SecureHashAlgorithm::Final() {
+  Pad();
+  Process();
+
+  for (size_t t = 0; t < 5; ++t)
+    H[t] = BSwap32(H[t]);
+}
+
+void SecureHashAlgorithm::Process() {
+  uint32_t t;
+
+  // Each a...e corresponds to a section in the FIPS 180-3 algorithm.
+
+  // a.
+  //
+  // W and M are in a union, so no need to memcpy.
+  // memcpy(W, M, sizeof(M));
+  for (t = 0; t < 16; ++t)
+    W[t] = BSwap32(W[t]);
+
+  // b.
+  for (t = 16; t < 80; ++t)
+    W[t] = S(1, W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16]);
+
+  // c.
+  A = H[0];
+  B = H[1];
+  C = H[2];
+  D = H[3];
+  E = H[4];
+
+  // d.
+  for (t = 0; t < 80; ++t) {
+    uint32_t TEMP = S(5, A) + f(t, B, C, D) + E + W[t] + K(t);
+    E = D;
+    D = C;
+    C = S(30, B);
+    B = A;
+    A = TEMP;
+  }
+
+  // e.
+  H[0] += A;
+  H[1] += B;
+  H[2] += C;
+  H[3] += D;
+  H[4] += E;
+
+  cursor = 0;
+}
+
+void SecureHashAlgorithm::Pad() {
+  M[cursor++] = 0x80;
+
+  if (cursor > 64 - 8) {
+    // pad out to next block
+    while (cursor < 64)
+      M[cursor++] = 0;
+
+    Process();
+  }
+
+  while (cursor < 64 - 8)
+    M[cursor++] = 0;
+
+  M[cursor++] = (l >> 56) & 0xff;
+  M[cursor++] = (l >> 48) & 0xff;
+  M[cursor++] = (l >> 40) & 0xff;
+  M[cursor++] = (l >> 32) & 0xff;
+  M[cursor++] = (l >> 24) & 0xff;
+  M[cursor++] = (l >> 16) & 0xff;
+  M[cursor++] = (l >> 8) & 0xff;
+  M[cursor++] = l & 0xff;
+}
+
+// Computes the SHA-1 hash of the |len| bytes in |data| and puts the hash
+// in |hash|. |hash| must be kSHA1Length bytes long.
+void SHA1HashBytes(const unsigned char* data, size_t len, unsigned char* hash) {
+  SecureHashAlgorithm sha;
+  sha.Update(data, len);
+  sha.Final();
+
+  ::memcpy(hash, sha.Digest(), kSHA1Length);
+}
+
+}  // namespace
+
+//------------------------------------------------------------------------------
+// Public functions
+//------------------------------------------------------------------------------
+SHA1Digest SHA1Hash(const void* data, size_t size) {
+  SHA1Digest digest;
+  SHA1HashBytes(static_cast<const unsigned char*>(data), size,
+                reinterpret_cast<unsigned char*>(&digest[0]));
+  return digest;
+}
+
+SHA1Digest SHA1Hash(const std::string& str) {
+  return SHA1Hash(str.data(), str.size());
+}
+
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/http/sha1_unittest.cc b/src/base/http/sha1_unittest.cc
new file mode 100644
index 0000000..269afc1
--- /dev/null
+++ b/src/base/http/sha1_unittest.cc
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 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 "perfetto/ext/base/http/sha1.h"
+
+#include <string>
+
+#include "perfetto/ext/base/string_view.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+using testing::ElementsAreArray;
+
+TEST(SHA1Test, Hash) {
+  EXPECT_THAT(SHA1Hash(""), ElementsAreArray<uint8_t>(
+                                {0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b,
+                                 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60,
+                                 0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09}));
+
+  EXPECT_THAT(SHA1Hash("abc"), ElementsAreArray<uint8_t>(
+                                   {0xa9, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81,
+                                    0x6a, 0xba, 0x3e, 0x25, 0x71, 0x78, 0x50,
+                                    0xc2, 0x6c, 0x9c, 0xd0, 0xd8, 0x9d}));
+
+  EXPECT_THAT(
+      SHA1Hash("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
+      ElementsAreArray<uint8_t>({0x84, 0x98, 0x3e, 0x44, 0x1c, 0x3b, 0xd2,
+                                 0x6e, 0xba, 0xae, 0x4a, 0xa1, 0xf9, 0x51,
+                                 0x29, 0xe5, 0xe5, 0x46, 0x70, 0xf1}));
+
+  EXPECT_THAT(
+      SHA1Hash(std::string(1000000, 'a')),
+      ElementsAreArray<uint8_t>({0x34, 0xaa, 0x97, 0x3c, 0xd4, 0xc4, 0xda,
+                                 0xa4, 0xf6, 0x1e, 0xeb, 0x2b, 0xdb, 0xad,
+                                 0x27, 0x31, 0x65, 0x34, 0x01, 0x6f}));
+}
+
+}  // namespace
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/small_vector_unittest.cc b/src/base/small_vector_unittest.cc
new file mode 100644
index 0000000..e99d84a
--- /dev/null
+++ b/src/base/small_vector_unittest.cc
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2021 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 "perfetto/ext/base/small_vector.h"
+
+#include <tuple>
+#include <utility>
+
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+int g_instances = 0;
+
+struct Obj {
+  explicit Obj(size_t v = 0) : value(v) {
+    EXPECT_FALSE(constructed);
+    constructed = true;
+    g_instances++;
+  }
+
+  ~Obj() {
+    EXPECT_TRUE(constructed);
+    g_instances--;
+  }
+
+  // Move operators.
+  Obj(Obj&& other) noexcept {
+    g_instances++;
+    constructed = true;
+    moved_into = true;
+    value = other.value;
+    other.moved_from = true;
+    other.value = 0xffffffff - value;
+  }
+
+  Obj& operator=(Obj&& other) noexcept {
+    this->~Obj();
+    new (this) Obj(std::move(other));
+    return *this;
+  }
+
+  // Copy operators.
+  Obj(const Obj& other) {
+    other.copied_from = true;
+    g_instances++;
+    constructed = true;
+    copied_into = true;
+    value = other.value;
+  }
+
+  Obj& operator=(const Obj& other) {
+    this->~Obj();
+    new (this) Obj(other);
+    return *this;
+  }
+
+  uintptr_t addr = reinterpret_cast<uintptr_t>(this);
+  bool constructed = false;
+  size_t value = 0;
+  bool moved_from = false;
+  mutable bool copied_from = false;
+  bool moved_into = false;
+  bool copied_into = false;
+};
+
+TEST(SmallVectorTest, StaySmall) {
+  SmallVector<Obj, 8> v;
+  EXPECT_EQ(g_instances, 0);
+  EXPECT_EQ(v.size(), 0u);
+  EXPECT_TRUE(v.empty());
+  EXPECT_EQ(v.begin(), v.end());
+
+  for (size_t i = 1; i <= 8; i++) {
+    v.emplace_back(i);
+    EXPECT_EQ(g_instances, static_cast<int>(i));
+    EXPECT_FALSE(v.empty());
+    EXPECT_EQ(v.end(), v.begin() + i);
+    EXPECT_EQ(v.back().value, i);
+    EXPECT_EQ(v[static_cast<size_t>(i - 1)].value, i);
+    EXPECT_EQ(v[static_cast<size_t>(i - 1)].value, i);
+  }
+
+  for (size_t i = 1; i <= 3; i++) {
+    v.pop_back();
+    EXPECT_EQ(g_instances, 8 - static_cast<int>(i));
+  }
+
+  v.clear();
+  EXPECT_EQ(g_instances, 0);
+}
+
+TEST(SmallVectorTest, GrowOnHeap) {
+  SmallVector<Obj, 4> v;
+  for (size_t i = 0; i < 10; i++) {
+    v.emplace_back(i);
+    EXPECT_EQ(g_instances, static_cast<int>(i + 1));
+    EXPECT_FALSE(v.empty());
+    EXPECT_EQ(v.end(), v.begin() + i + 1);
+    EXPECT_EQ(v[i].value, i);
+  }
+
+  // Do a second pass and check that the initial elements aren't corrupt.
+  for (size_t i = 0; i < 10; i++) {
+    EXPECT_EQ(v[i].value, i);
+    EXPECT_TRUE(v[i].constructed);
+  }
+
+  // The first 4 elements must have been moved into because of the heap growth.
+  for (size_t i = 0; i < 4; i++)
+    EXPECT_TRUE(v[i].moved_into);
+  EXPECT_FALSE(v.back().moved_into);
+}
+
+class SmallVectorTestP : public testing::TestWithParam<size_t> {};
+
+TEST_P(SmallVectorTestP, MoveOperators) {
+  size_t num_elements = GetParam();
+  static constexpr size_t kInlineCapacity = 4;
+  SmallVector<Obj, kInlineCapacity> v1;
+  for (size_t i = 0; i < num_elements; i++)
+    v1.emplace_back(i);
+
+  SmallVector<Obj, kInlineCapacity> v2(std::move(v1));
+  EXPECT_TRUE(v1.empty());
+  EXPECT_EQ(v2.size(), num_elements);
+
+  // Check that v2 (the moved into vector) is consistent.
+  for (size_t i = 0; i < num_elements; i++) {
+    EXPECT_EQ(v2[i].value, i);
+    EXPECT_TRUE(v2[i].constructed);
+    if (num_elements <= kInlineCapacity) {
+      EXPECT_TRUE(v2[i].moved_into);
+    }
+  }
+
+  // Check that v1 (the moved-from object) is still usable.
+  EXPECT_EQ(v1.size(), 0u);
+
+  for (size_t i = 0; i < num_elements; i++) {
+    v1.emplace_back(1000 + i);
+    EXPECT_EQ(v1.size(), i + 1);
+  }
+
+  EXPECT_NE(v1.data(), v2.data());
+
+  for (size_t i = 0; i < num_elements; i++) {
+    EXPECT_EQ(v1[i].value, 1000 + i);
+    EXPECT_EQ(v2[i].value, i);
+    EXPECT_TRUE(v1[i].constructed);
+    EXPECT_FALSE(v1[i].moved_from);
+  }
+
+  // Now swap again using the move-assignment.
+
+  v1 = std::move(v2);
+  EXPECT_EQ(v1.size(), num_elements);
+  EXPECT_TRUE(v2.empty());
+  for (size_t i = 0; i < num_elements; i++) {
+    EXPECT_EQ(v1[i].value, i);
+    EXPECT_TRUE(v1[i].constructed);
+  }
+
+  { auto destroy = std::move(v1); }
+
+  EXPECT_EQ(g_instances, 0);
+}
+
+TEST_P(SmallVectorTestP, CopyOperators) {
+  size_t num_elements = GetParam();
+  static constexpr size_t kInlineCapacity = 4;
+  SmallVector<Obj, kInlineCapacity> v1;
+  for (size_t i = 0; i < num_elements; i++)
+    v1.emplace_back(i);
+
+  SmallVector<Obj, kInlineCapacity> v2(v1);
+  EXPECT_EQ(v1.size(), num_elements);
+  EXPECT_EQ(v2.size(), num_elements);
+  EXPECT_EQ(g_instances, static_cast<int>(num_elements * 2));
+
+  for (size_t i = 0; i < num_elements; i++) {
+    EXPECT_EQ(v1[i].value, i);
+    EXPECT_TRUE(v1[i].copied_from);
+    EXPECT_EQ(v2[i].value, i);
+    EXPECT_TRUE(v2[i].copied_into);
+  }
+
+  // Now edit v2.
+  for (size_t i = 0; i < num_elements; i++)
+    v2[i].value = i + 100;
+  EXPECT_EQ(g_instances, static_cast<int>(num_elements * 2));
+
+  // Append some extra elements.
+  for (size_t i = 0; i < num_elements; i++)
+    v2.emplace_back(i + 200);
+  EXPECT_EQ(g_instances, static_cast<int>(num_elements * 3));
+
+  for (size_t i = 0; i < num_elements * 2; i++) {
+    if (i < num_elements) {
+      EXPECT_EQ(v1[i].value, i);
+      EXPECT_EQ(v2[i].value, 100 + i);
+    } else {
+      EXPECT_EQ(v2[i].value, 200 + i - num_elements);
+    }
+  }
+
+  v2.clear();
+  EXPECT_EQ(g_instances, static_cast<int>(num_elements));
+}
+
+INSTANTIATE_TEST_SUITE_P(SmallVectorTest,
+                         SmallVectorTestP,
+                         testing::Values(2, 4, 7, 512));
+
+}  // namespace
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/test/utils.cc b/src/base/test/utils.cc
index ff00f64..56e51a8 100644
--- a/src/base/test/utils.cc
+++ b/src/base/test/utils.cc
@@ -40,28 +40,5 @@
   return path;
 }
 
-std::string HexDump(const void* data_void, size_t len, size_t bytes_per_line) {
-  const char* data = reinterpret_cast<const char*>(data_void);
-  std::string res;
-  static const size_t kPadding = bytes_per_line * 3 + 12;
-  std::unique_ptr<char[]> line(new char[bytes_per_line * 4 + 128]);
-  for (size_t i = 0; i < len; i += bytes_per_line) {
-    char* wptr = line.get();
-    wptr += sprintf(wptr, "%08zX: ", i);
-    for (size_t j = i; j < i + bytes_per_line && j < len; j++)
-      wptr += sprintf(wptr, "%02X ", static_cast<unsigned>(data[j]) & 0xFF);
-    for (size_t j = static_cast<size_t>(wptr - line.get()); j < kPadding; ++j)
-      *(wptr++) = ' ';
-    for (size_t j = i; j < i + bytes_per_line && j < len; j++) {
-      char c = data[j];
-      *(wptr++) = (c >= 32 && c < 127) ? c : '.';
-    }
-    *(wptr++) = '\n';
-    *(wptr++) = '\0';
-    res.append(line.get());
-  }
-  return res;
-}
-
 }  // namespace base
 }  // namespace perfetto
diff --git a/src/base/test/utils.h b/src/base/test/utils.h
index 032194b..4dfc281 100644
--- a/src/base/test/utils.h
+++ b/src/base/test/utils.h
@@ -52,13 +52,6 @@
 
 std::string GetTestDataPath(const std::string& path);
 
-// Returns a xxd-style hex dump (hex + ascii chars) of the input data.
-std::string HexDump(const void* data, size_t len, size_t bytes_per_line = 16);
-inline std::string HexDump(const std::string& data,
-                           size_t bytes_per_line = 16) {
-  return HexDump(data.data(), data.size(), bytes_per_line);
-}
-
 }  // namespace base
 }  // namespace perfetto
 
diff --git a/src/base/unix_socket.cc b/src/base/unix_socket.cc
index 5986917..3088399 100644
--- a/src/base/unix_socket.cc
+++ b/src/base/unix_socket.cc
@@ -36,6 +36,7 @@
 #include <netdb.h>
 #include <netinet/in.h>
 #include <netinet/tcp.h>
+#include <poll.h>
 #include <sys/socket.h>
 #include <sys/un.h>
 #include <unistd.h>
@@ -51,6 +52,7 @@
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/task_runner.h"
+#include "perfetto/base/time.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/utils.h"
 
@@ -66,16 +68,6 @@
 
 namespace {
 
-// MSG_NOSIGNAL is not supported on Mac OS X, but in that case the socket is
-// created with SO_NOSIGPIPE (See InitializeSocket()).
-// On Windows this does't apply as signals don't exist.
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
-#elif PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
-constexpr int kNoSigPipe = 0;
-#else
-constexpr int kNoSigPipe = MSG_NOSIGNAL;
-#endif
-
 // Android takes an int instead of socklen_t for the control buffer size.
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
 using CBufLenType = size_t;
@@ -431,19 +423,60 @@
   // This does not make sense on non-blocking sockets.
   PERFETTO_DCHECK(fd_);
 
+  const bool is_blocking_with_timeout =
+      tx_timeout_ms_ > 0 && ((fcntl(*fd_, F_GETFL, 0) & O_NONBLOCK) == 0);
+  const int64_t start_ms = GetWallTimeMs().count();
+
+  // Waits until some space is available in the tx buffer.
+  // Returns true if some buffer space is available, false if times out.
+  auto poll_or_timeout = [&] {
+    PERFETTO_DCHECK(is_blocking_with_timeout);
+    const int64_t deadline = start_ms + tx_timeout_ms_;
+    const int64_t now_ms = GetWallTimeMs().count();
+    if (now_ms >= deadline)
+      return false;  // Timed out
+    const int timeout_ms = static_cast<int>(deadline - now_ms);
+    pollfd pfd{*fd_, POLLOUT, 0};
+    return PERFETTO_EINTR(poll(&pfd, 1, timeout_ms)) > 0;
+  };
+
+// We implement blocking sends that require a timeout as non-blocking + poll.
+// This is because SO_SNDTIMEO doesn't work as expected (b/193234818). On linux
+// we can just pass MSG_DONTWAIT to force the send to be non-blocking. On Mac,
+// instead we need to flip the O_NONBLOCK flag back and forth.
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
+  // MSG_NOSIGNAL is not supported on Mac OS X, but in that case the socket is
+  // created with SO_NOSIGPIPE (See InitializeSocket()).
+  int send_flags = 0;
+
+  if (is_blocking_with_timeout)
+    SetBlocking(false);
+
+  auto reset_nonblock_on_exit = OnScopeExit([&] {
+    if (is_blocking_with_timeout)
+      SetBlocking(true);
+  });
+#else
+  int send_flags = MSG_NOSIGNAL | (is_blocking_with_timeout ? MSG_DONTWAIT : 0);
+#endif
+
   ssize_t total_sent = 0;
   while (msg->msg_iov) {
-    ssize_t sent = PERFETTO_EINTR(sendmsg(*fd_, msg, kNoSigPipe));
-    if (sent <= 0) {
-      if (sent == -1 && IsAgain(errno))
-        return total_sent;
-      return sent;
+    ssize_t send_res = PERFETTO_EINTR(sendmsg(*fd_, msg, send_flags));
+    if (send_res == -1 && IsAgain(errno)) {
+      if (is_blocking_with_timeout && poll_or_timeout()) {
+        continue;  // Tx buffer unblocked, repeat the loop.
+      }
+      return total_sent;
+    } else if (send_res <= 0) {
+      return send_res;  // An error occurred.
+    } else {
+      total_sent += send_res;
+      ShiftMsgHdrPosix(static_cast<size_t>(send_res), msg);
+      // Only send the ancillary data with the first sendmsg call.
+      msg->msg_control = nullptr;
+      msg->msg_controllen = 0;
     }
-    total_sent += sent;
-    ShiftMsgHdrPosix(static_cast<size_t>(sent), msg);
-    // Only send the ancillary data with the first sendmsg call.
-    msg->msg_control = nullptr;
-    msg->msg_controllen = 0;
   }
   return total_sent;
 }
@@ -541,8 +574,14 @@
 
 bool UnixSocketRaw::SetTxTimeout(uint32_t timeout_ms) {
   PERFETTO_DCHECK(fd_);
+  // On Unix-based systems, SO_SNDTIMEO isn't used for Send() because it's
+  // unreliable (b/193234818). Instead we use non-blocking sendmsg() + poll().
+  // See SendMsgAllPosix(). We still make the setsockopt call because
+  // SO_SNDTIMEO also affects connect().
+  tx_timeout_ms_ = timeout_ms;
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
   DWORD timeout = timeout_ms;
+  ignore_result(tx_timeout_ms_);
 #else
   struct timeval timeout {};
   uint32_t timeout_sec = timeout_ms / 1000;
diff --git a/src/base/unix_socket_unittest.cc b/src/base/unix_socket_unittest.cc
index 86a6bb5..c838663 100644
--- a/src/base/unix_socket_unittest.cc
+++ b/src/base/unix_socket_unittest.cc
@@ -30,6 +30,7 @@
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/periodic_task.h"
 #include "perfetto/ext/base/pipe.h"
 #include "perfetto/ext/base/temp_file.h"
 #include "perfetto/ext/base/utils.h"
@@ -331,6 +332,53 @@
   tx_thread.join();
 }
 
+// Regression test for b/193234818. SO_SNDTIMEO is unreliable on most systems.
+// It doesn't guarantee that the whole send() call blocks for at most X, as the
+// kernel rearms the timeout if the send buffers frees up and allows a partial
+// send. This test reproduces the issue 100% on Mac. Unfortunately on Linux the
+// repro seem to happen only when a suspend happens in the middle.
+TEST_F(UnixSocketTest, BlockingSendTimeout) {
+  TestTaskRunner ttr;
+  UnixSocketRaw send_sock;
+  UnixSocketRaw recv_sock;
+  std::tie(send_sock, recv_sock) =
+      UnixSocketRaw::CreatePairPosix(kTestSocket.family(), SockType::kStream);
+
+  auto blocking_send_done = ttr.CreateCheckpoint("blocking_send_done");
+
+  std::thread tx_thread([&] {
+    // Fill the tx buffer in non-blocking mode.
+    send_sock.SetBlocking(false);
+    char buf[1024 * 16]{};
+    while (send_sock.Send(buf, sizeof(buf)) > 0) {
+    }
+
+    // Then do a blocking send. It should return a partial value within the tx
+    // timeout.
+    send_sock.SetBlocking(true);
+    send_sock.SetTxTimeout(10);
+    ASSERT_LT(send_sock.Send(buf, sizeof(buf)),
+              static_cast<ssize_t>(sizeof(buf)));
+    ttr.PostTask(blocking_send_done);
+  });
+
+  // This task needs to be slow enough so that doesn't unblock the send, but
+  // fast enough so that within a blocking cycle, the send re-attempts and
+  // re-arms the timeout.
+  PeriodicTask read_slowly_task(&ttr);
+  PeriodicTask::Args args;
+  args.period_ms = 1;  // Read 1 byte every ms (1 KiB/s).
+  args.task = [&] {
+    char rxbuf[1]{};
+    recv_sock.Receive(rxbuf, sizeof(rxbuf));
+  };
+  read_slowly_task.Start(args);
+
+  ttr.RunUntilCheckpoint("blocking_send_done");
+  read_slowly_task.Reset();
+  tx_thread.join();
+}
+
 // Regression test for b/76155349 . If the receiver end disconnects while the
 // sender is in the middle of a large send(), the socket should gracefully give
 // up (i.e. Shutdown()) but not crash.
diff --git a/src/base/utils.cc b/src/base/utils.cc
index b5d3d3c..363a0d6 100644
--- a/src/base/utils.cc
+++ b/src/base/utils.cc
@@ -266,5 +266,28 @@
 #endif
 }
 
+std::string HexDump(const void* data_void, size_t len, size_t bytes_per_line) {
+  const char* data = reinterpret_cast<const char*>(data_void);
+  std::string res;
+  static const size_t kPadding = bytes_per_line * 3 + 12;
+  std::unique_ptr<char[]> line(new char[bytes_per_line * 4 + 128]);
+  for (size_t i = 0; i < len; i += bytes_per_line) {
+    char* wptr = line.get();
+    wptr += sprintf(wptr, "%08zX: ", i);
+    for (size_t j = i; j < i + bytes_per_line && j < len; j++)
+      wptr += sprintf(wptr, "%02X ", static_cast<unsigned>(data[j]) & 0xFF);
+    for (size_t j = static_cast<size_t>(wptr - line.get()); j < kPadding; ++j)
+      *(wptr++) = ' ';
+    for (size_t j = i; j < i + bytes_per_line && j < len; j++) {
+      char c = data[j];
+      *(wptr++) = (c >= 32 && c < 127) ? c : '.';
+    }
+    *(wptr++) = '\n';
+    *(wptr++) = '\0';
+    res.append(line.get());
+  }
+  return res;
+}
+
 }  // namespace base
 }  // namespace perfetto
diff --git a/src/base/utils_unittest.cc b/src/base/utils_unittest.cc
index ef6e657..b75ec05 100644
--- a/src/base/utils_unittest.cc
+++ b/src/base/utils_unittest.cc
@@ -202,6 +202,19 @@
   EXPECT_EQ(0xffffff00u, AlignUp<16>(0xffffff00 - 1));
 }
 
+TEST(UtilsTest, HexDump) {
+  char input[] = {0x00, 0x00, 'a', 'b', 'c', 'd', 'e', 'f', 'g',
+                  'h',  'i',  'j', 'k', 'l', 'm', 'n', 'o', 'p'};
+
+  std::string output = HexDump(input, sizeof(input));
+
+  EXPECT_EQ(
+      output,
+      R"(00000000: 00 00 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E   ..abcdefghijklmn
+00000010: 6F 70                                             op
+)");
+}
+
 }  // namespace
 }  // namespace base
 }  // namespace perfetto
diff --git a/src/base/watchdog_posix.cc b/src/base/watchdog_posix.cc
index 6133b61..f352c57 100644
--- a/src/base/watchdog_posix.cc
+++ b/src/base/watchdog_posix.cc
@@ -50,6 +50,10 @@
 
 base::CrashKey g_crash_key_reason("wdog_reason");
 
+// TODO(primiano): for debugging b/191600928. Remove in Jan 2022.
+base::CrashKey g_crash_key_mono("wdog_mono");
+base::CrashKey g_crash_key_boot("wdog_boot");
+
 bool IsMultipleOf(uint32_t number, uint32_t divisor) {
   return number >= divisor && number % divisor == 0;
 }
@@ -211,6 +215,11 @@
 }
 
 void Watchdog::ThreadMain() {
+  // Register crash keys explicitly to avoid running out of slots at crash time.
+  g_crash_key_reason.Register();
+  g_crash_key_boot.Register();
+  g_crash_key_mono.Register();
+
   base::ScopedFile stat_fd(base::OpenFile("/proc/self/stat", O_RDONLY));
   if (!stat_fd) {
     PERFETTO_ELOG("Failed to open stat file to enforce resource limits.");
@@ -294,6 +303,8 @@
 void Watchdog::SerializeLogsAndKillThread(int tid,
                                           WatchdogCrashReason crash_reason) {
   g_crash_key_reason.Set(static_cast<int>(crash_reason));
+  g_crash_key_boot.Set(base::GetBootTimeS().count());
+  g_crash_key_mono.Set(base::GetWallTimeS().count());
 
   // We are about to die. Serialize the logs into the crash buffer so the
   // debuggerd crash handler picks them up and attaches to the bugreport.
diff --git a/src/ipc/host_impl.cc b/src/ipc/host_impl.cc
index da1ce4c..fa85c2b 100644
--- a/src/ipc/host_impl.cc
+++ b/src/ipc/host_impl.cc
@@ -20,7 +20,9 @@
 #include <cinttypes>
 #include <utility>
 
+#include "perfetto/base/build_config.h"
 #include "perfetto/base/task_runner.h"
+#include "perfetto/base/time.h"
 #include "perfetto/ext/base/crash_keys.h"
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/ipc/service.h"
@@ -28,6 +30,15 @@
 
 #include "protos/perfetto/ipc/wire_protocol.gen.h"
 
+#if (PERFETTO_BUILDFLAG(PERFETTO_STANDALONE_BUILD) || \
+     PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)) &&   \
+    (PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||         \
+     PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID))
+#define PERFETTO_LOG_TXBUF_FOR_B_191600928
+// TODO(primiano): temporary for investigating b/191600928. Remove in Jan 2022
+#include <sys/ioctl.h>
+#endif
+
 // TODO(primiano): put limits on #connections/uid and req. queue (b/69093705).
 
 namespace perfetto {
@@ -38,7 +49,11 @@
 constexpr base::SockFamily kHostSockFamily =
     kUseTCPSocket ? base::SockFamily::kInet : base::SockFamily::kUnix;
 
+// TODO(primiano): temporary for investigating b/191600928. Remove in Jan 2022.
 base::CrashKey g_crash_key_uid("ipc_uid");
+base::CrashKey g_crash_key_tx_b("ipc_tx_boot");
+base::CrashKey g_crash_key_tx_m("ipc_tx_mono");
+base::CrashKey g_crash_key_tx_qlen("ipc_tx_qlen");
 
 uid_t GetPosixPeerUid(base::UnixSocket* sock) {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
@@ -263,6 +278,17 @@
 
   std::string buf = BufferedFrameDeserializer::Serialize(frame);
 
+  auto crash_key_b = g_crash_key_tx_b.SetScoped(base::GetBootTimeS().count());
+  auto crash_key_w = g_crash_key_tx_m.SetScoped(base::GetWallTimeS().count());
+
+#if defined(PERFETTO_LOG_TXBUF_FOR_B_191600928)
+  int32_t tx_queue_len = 0;
+  ioctl(client->sock->fd(), TIOCOUTQ, &tx_queue_len);
+  auto crash_key_qlen = g_crash_key_tx_qlen.SetScoped(tx_queue_len);
+#else
+  base::ignore_result(g_crash_key_tx_qlen);
+#endif
+
   // When a new Client connects in OnNewClientConnection we set a timeout on
   // Send (see call to SetTxTimeout).
   //
diff --git a/src/perfetto_cmd/perfetto_cmd.cc b/src/perfetto_cmd/perfetto_cmd.cc
index ef1492d..92c8bfa 100644
--- a/src/perfetto_cmd/perfetto_cmd.cc
+++ b/src/perfetto_cmd/perfetto_cmd.cc
@@ -514,8 +514,9 @@
   trace_config_.reset(new TraceConfig());
 
   bool parsed = false;
-  const bool will_trace = !is_attach() && !query_service_ && !bugreport_;
-  if (!will_trace) {
+  const bool will_trace_or_trigger =
+      !is_attach() && !query_service_ && !bugreport_;
+  if (!will_trace_or_trigger) {
     if ((!trace_config_raw.empty() || has_config_options)) {
       PERFETTO_ELOG("Cannot specify a trace config with this option");
       return 1;
@@ -545,7 +546,7 @@
   if (parsed) {
     *trace_config_->mutable_statsd_metadata() = std::move(statsd_metadata);
     trace_config_raw.clear();
-  } else if (will_trace) {
+  } else if (will_trace_or_trigger) {
     PERFETTO_ELOG("The trace config is invalid, bailing out.");
     return 1;
   }
@@ -573,21 +574,23 @@
     return 1;
   }
 
-  if (trace_config_->activate_triggers().empty() &&
-      trace_config_->incident_report_config().destination_package().empty() &&
+  // Only save to incidentd if:
+  // 1) --upload is set
+  // 2) |skip_incidentd| is absent or false.
+  // 3) we are not simply activating triggers.
+  save_to_incidentd_ =
+      upload_flag_ &&
       !trace_config_->incident_report_config().skip_incidentd() &&
-      upload_flag_) {
+      trace_config_->activate_triggers().empty();
+
+  if (save_to_incidentd_ &&
+      trace_config_->incident_report_config().destination_package().empty()) {
     PERFETTO_ELOG(
         "Missing IncidentReportConfig.destination_package with --dropbox / "
         "--upload.");
     return 1;
   }
 
-  // Only save to incidentd if both --upload is set and |skip_incidentd| is
-  // absent or false.
-  save_to_incidentd_ =
-      upload_flag_ && !trace_config_->incident_report_config().skip_incidentd();
-
   // Respect the wishes of the config with respect to statsd logging or fall
   // back on the presence of the --upload flag if not set.
   switch (trace_config_->statsd_logging()) {
@@ -644,7 +647,7 @@
   }
 
   bool open_out_file = true;
-  if (!will_trace) {
+  if (!will_trace_or_trigger) {
     open_out_file = false;
     if (!trace_out_path_.empty() || upload_flag_) {
       PERFETTO_ELOG("Can't pass an --out file (or --upload) with this option");
diff --git a/src/profiling/memory/java_hprof_producer.cc b/src/profiling/memory/java_hprof_producer.cc
index aef9b5b..6c68995 100644
--- a/src/profiling/memory/java_hprof_producer.cc
+++ b/src/profiling/memory/java_hprof_producer.cc
@@ -40,8 +40,11 @@
   auto it = data_sources_.find(id);
   if (it == data_sources_.end())
     return;
-  const DataSource& ds = it->second;
-  SignalDataSource(ds);
+  DataSource& ds = it->second;
+  if (!ds.config().continuous_dump_config().scan_pids_only_on_start()) {
+    ds.CollectPids();
+  }
+  ds.SendSignal();
   auto weak_producer = weak_factory_.GetWeakPtr();
   task_runner_->PostDelayedTask(
       [weak_producer, id, dump_interval] {
@@ -52,10 +55,16 @@
       dump_interval);
 }
 
-// static
-void JavaHprofProducer::SignalDataSource(const DataSource& ds) {
-  const std::set<pid_t>& pids = ds.pids;
-  for (pid_t pid : pids) {
+JavaHprofProducer::DataSource::DataSource(
+    DataSourceConfig ds_config,
+    JavaHprofConfig config,
+    std::vector<std::string> normalized_cmdlines)
+    : ds_config_(ds_config),
+      config_(config),
+      normalized_cmdlines_(normalized_cmdlines) {}
+
+void JavaHprofProducer::DataSource::SendSignal() const {
+  for (pid_t pid : pids_) {
     auto opt_status = ReadStatus(pid);
     if (!opt_status) {
       PERFETTO_PLOG("Failed to read /proc/%d/status. Not signalling.", pid);
@@ -69,23 +78,32 @@
           pid);
       continue;
     }
-    if (!CanProfile(ds.ds_config, uids->effective,
-                    ds.config.target_installed_by())) {
+    if (!CanProfile(ds_config_, uids->effective,
+                    config_.target_installed_by())) {
       PERFETTO_ELOG("%d (UID %" PRIu64 ") not profileable.", pid,
                     uids->effective);
       continue;
     }
     PERFETTO_DLOG("Sending %d to %d", kJavaHeapprofdSignal, pid);
     union sigval signal_value;
-    signal_value.sival_int =
-        static_cast<int32_t>(ds.ds_config.tracing_session_id() %
-                             std::numeric_limits<int32_t>::max());
+    signal_value.sival_int = static_cast<int32_t>(
+        ds_config_.tracing_session_id() % std::numeric_limits<int32_t>::max());
     if (sigqueue(pid, kJavaHeapprofdSignal, signal_value) != 0) {
       PERFETTO_DPLOG("sigqueue");
     }
   }
 }
 
+void JavaHprofProducer::DataSource::CollectPids() {
+  pids_.clear();
+  for (uint64_t pid : config_.pid()) {
+    pids_.emplace(static_cast<pid_t>(pid));
+  }
+  FindPidsForCmdlines(normalized_cmdlines_, &pids_);
+  if (config_.min_anonymous_memory_kb() > 0)
+    RemoveUnderAnonThreshold(config_.min_anonymous_memory_kb(), &pids_);
+}
+
 void JavaHprofProducer::IncreaseConnectionBackoff() {
   connection_backoff_ms_ *= 2;
   if (connection_backoff_ms_ > kMaxConnectionBackoffMs)
@@ -104,23 +122,15 @@
   }
   JavaHprofConfig config;
   config.ParseFromString(ds_config.java_hprof_config_raw());
-  DataSource ds;
-  ds.id = id;
-  for (uint64_t pid : config.pid())
-    ds.pids.emplace(static_cast<pid_t>(pid));
   base::Optional<std::vector<std::string>> normalized_cmdlines =
       NormalizeCmdlines(config.process_cmdline());
   if (!normalized_cmdlines.has_value()) {
     PERFETTO_ELOG("Rejecting data source due to invalid cmdline in config.");
     return;
   }
-  FindPidsForCmdlines(normalized_cmdlines.value(), &ds.pids);
-  if (config.min_anonymous_memory_kb() > 0)
-    RemoveUnderAnonThreshold(config.min_anonymous_memory_kb(), &ds.pids);
-
-  ds.config = std::move(config);
-  ds.ds_config = std::move(ds_config);
-  data_sources_.emplace(id, std::move(ds));
+  DataSource ds(ds_config, std::move(config), std::move(*normalized_cmdlines));
+  ds.CollectPids();
+  data_sources_.emplace(id, ds);
 }
 
 void JavaHprofProducer::StartDataSource(DataSourceInstanceID id,
@@ -131,7 +141,7 @@
     return;
   }
   const DataSource& ds = it->second;
-  const auto continuous_dump_config = ds.config.continuous_dump_config();
+  const auto& continuous_dump_config = ds.config().continuous_dump_config();
   uint32_t dump_interval = continuous_dump_config.dump_interval_ms();
   if (dump_interval) {
     auto weak_producer = weak_factory_.GetWeakPtr();
@@ -143,7 +153,7 @@
         },
         continuous_dump_config.dump_phase_ms());
   }
-  SignalDataSource(ds);
+  ds.SendSignal();
 }
 
 void JavaHprofProducer::StopDataSource(DataSourceInstanceID id) {
diff --git a/src/profiling/memory/java_hprof_producer.h b/src/profiling/memory/java_hprof_producer.h
index 97ac07d..056c60a 100644
--- a/src/profiling/memory/java_hprof_producer.h
+++ b/src/profiling/memory/java_hprof_producer.h
@@ -69,11 +69,23 @@
     kConnected,
   };
 
-  struct DataSource {
-    DataSourceInstanceID id;
-    std::set<pid_t> pids;
-    JavaHprofConfig config;
-    DataSourceConfig ds_config;
+  class DataSource {
+   public:
+    DataSource(DataSourceConfig ds_config,
+               JavaHprofConfig config,
+               std::vector<std::string> normalized_cmdlines);
+    void CollectPids();
+    void SendSignal() const;
+
+    const JavaHprofConfig& config() const { return config_; }
+    const DataSourceConfig& ds_config() const { return ds_config_; }
+
+   private:
+    DataSourceConfig ds_config_;
+    JavaHprofConfig config_;
+    std::vector<std::string> normalized_cmdlines_;
+
+    std::set<pid_t> pids_;
   };
 
   void ConnectService();
@@ -82,7 +94,6 @@
   void IncreaseConnectionBackoff();
 
   void DoContinuousDump(DataSourceInstanceID id, uint32_t dump_interval);
-  static void SignalDataSource(const DataSource& ds);
 
   // State of connection to the tracing service.
   State state_ = kNotStarted;
diff --git a/src/profiling/perf/event_config.cc b/src/profiling/perf/event_config.cc
index 4195cc3..c45cb0b 100644
--- a/src/profiling/perf/event_config.cc
+++ b/src/profiling/perf/event_config.cc
@@ -183,6 +183,25 @@
   }
 }
 
+int32_t ToClockId(protos::gen::PerfEvents::PerfClock pb_enum) {
+  using protos::gen::PerfEvents;
+  switch (static_cast<int>(pb_enum)) {  // cast to pacify -Wswitch-enum
+    case PerfEvents::PERF_CLOCK_REALTIME:
+      return CLOCK_REALTIME;
+    case PerfEvents::PERF_CLOCK_MONOTONIC:
+      return CLOCK_MONOTONIC;
+    case PerfEvents::PERF_CLOCK_MONOTONIC_RAW:
+      return CLOCK_MONOTONIC_RAW;
+    case PerfEvents::PERF_CLOCK_BOOTTIME:
+      return CLOCK_BOOTTIME;
+    // Default to a monotonic clock since it should be compatible with all types
+    // of events. Whereas boottime cannot be used with hardware events due to
+    // potential access within non-maskable interrupts.
+    default:
+      return CLOCK_MONOTONIC_RAW;
+  }
+}
+
 }  // namespace
 
 // static
@@ -385,9 +404,7 @@
   // What the samples will contain.
   pe.sample_type = PERF_SAMPLE_TID | PERF_SAMPLE_TIME | PERF_SAMPLE_READ;
   // PERF_SAMPLE_TIME:
-  // We used to use CLOCK_BOOTTIME, but that is not nmi-safe, and therefore
-  // works only for software events.
-  pe.clockid = CLOCK_MONOTONIC_RAW;
+  pe.clockid = ToClockId(pb_config.timebase().timestamp_clock());
   pe.use_clockid = true;
 
   if (sample_callstacks) {
diff --git a/src/profiling/perf/event_config_unittest.cc b/src/profiling/perf/event_config_unittest.cc
index 30520f1..801d8ea 100644
--- a/src/profiling/perf/event_config_unittest.cc
+++ b/src/profiling/perf/event_config_unittest.cc
@@ -18,6 +18,7 @@
 
 #include <linux/perf_event.h>
 #include <stdint.h>
+#include <time.h>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/optional.h"
@@ -328,6 +329,40 @@
   }
 }
 
+TEST(EventConfigTest, TimestampClockId) {
+  {  // if unset, a default is used
+    protos::gen::PerfEventConfig cfg;
+    base::Optional<EventConfig> event_config =
+        EventConfig::Create(AsDataSourceConfig(cfg));
+
+    ASSERT_TRUE(event_config.has_value());
+    EXPECT_TRUE(event_config->perf_attr()->use_clockid);
+    EXPECT_EQ(event_config->perf_attr()->clockid, CLOCK_MONOTONIC_RAW);
+  }
+  {  // explicit boottime
+    protos::gen::PerfEventConfig cfg;
+    cfg.mutable_timebase()->set_timestamp_clock(
+        protos::gen::PerfEvents::PERF_CLOCK_BOOTTIME);
+    base::Optional<EventConfig> event_config =
+        EventConfig::Create(AsDataSourceConfig(cfg));
+
+    ASSERT_TRUE(event_config.has_value());
+    EXPECT_TRUE(event_config->perf_attr()->use_clockid);
+    EXPECT_EQ(event_config->perf_attr()->clockid, CLOCK_BOOTTIME);
+  }
+  {  // explicit monotonic
+    protos::gen::PerfEventConfig cfg;
+    cfg.mutable_timebase()->set_timestamp_clock(
+        protos::gen::PerfEvents::PERF_CLOCK_MONOTONIC);
+    base::Optional<EventConfig> event_config =
+        EventConfig::Create(AsDataSourceConfig(cfg));
+
+    ASSERT_TRUE(event_config.has_value());
+    EXPECT_TRUE(event_config->perf_attr()->use_clockid);
+    EXPECT_EQ(event_config->perf_attr()->clockid, CLOCK_MONOTONIC);
+  }
+}
+
 }  // namespace
 }  // namespace profiling
 }  // namespace perfetto
diff --git a/src/profiling/perf/perf_producer.cc b/src/profiling/perf/perf_producer.cc
index f9ad9e2..0497b9b 100644
--- a/src/profiling/perf/perf_producer.cc
+++ b/src/profiling/perf/perf_producer.cc
@@ -80,6 +80,23 @@
   return static_cast<size_t>(sysconf(_SC_NPROCESSORS_CONF));
 }
 
+int32_t ToBuiltinClock(int32_t clockid) {
+  switch (clockid) {
+    case CLOCK_REALTIME:
+      return protos::pbzero::BUILTIN_CLOCK_REALTIME;
+    case CLOCK_MONOTONIC:
+      return protos::pbzero::BUILTIN_CLOCK_MONOTONIC;
+    case CLOCK_MONOTONIC_RAW:
+      return protos::pbzero::BUILTIN_CLOCK_MONOTONIC_RAW;
+    case CLOCK_BOOTTIME:
+      return protos::pbzero::BUILTIN_CLOCK_BOOTTIME;
+    // Should never get invalid input here as otherwise the syscall itself
+    // would've failed earlier.
+    default:
+      return protos::pbzero::BUILTIN_CLOCK_UNKNOWN;
+  }
+}
+
 TraceWriter::TracePacketHandle StartTracePacket(TraceWriter* trace_writer) {
   auto packet = trace_writer->NewTracePacket();
   packet->set_sequence_flags(
@@ -97,16 +114,16 @@
   packet->set_sequence_flags(
       protos::pbzero::TracePacket::SEQ_INCREMENTAL_STATE_CLEARED);
 
-  // default packet timestamp clockid:
+  // default packet timestamp clock for the samples:
+  perf_event_attr* perf_attr = event_config.perf_attr();
   auto* defaults = packet->set_trace_packet_defaults();
-  defaults->set_timestamp_clock_id(protos::pbzero::BUILTIN_CLOCK_MONOTONIC_RAW);
-  PERFETTO_DCHECK(event_config.perf_attr()->clockid == CLOCK_MONOTONIC_RAW);
+  int32_t builtin_clock = ToBuiltinClock(perf_attr->clockid);
+  defaults->set_timestamp_clock_id(static_cast<uint32_t>(builtin_clock));
 
   auto* perf_defaults = defaults->set_perf_sample_defaults();
   auto* timebase_pb = perf_defaults->set_timebase();
 
   // frequency/period:
-  perf_event_attr* perf_attr = event_config.perf_attr();
   if (perf_attr->freq) {
     timebase_pb->set_frequency(perf_attr->sample_freq);
   } else {
@@ -141,6 +158,9 @@
   if (!timebase.name.empty()) {
     timebase_pb->set_name(timebase.name);
   }
+
+  // Not setting timebase.timestamp_clock since the field that matters during
+  // parsing is the root timestamp_clock_id set above.
 }
 
 uint32_t TimeToNextReadTickMs(DataSourceInstanceID ds_id, uint32_t period_ms) {
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 65f84e6..4ce7cec 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -211,8 +211,6 @@
     "importers/json/json_trace_parser.h",
     "importers/json/json_trace_tokenizer.cc",
     "importers/json/json_trace_tokenizer.h",
-    "importers/json/json_tracker.cc",
-    "importers/json/json_tracker.h",
     "importers/proto/android_probes_module.cc",
     "importers/proto/android_probes_module.h",
     "importers/proto/android_probes_parser.cc",
@@ -462,7 +460,6 @@
   if (enable_perfetto_trace_processor_json) {
     sources += [
       "importers/json/json_trace_tokenizer_unittest.cc",
-      "importers/json/json_tracker_unittest.cc",
       "importers/json/json_utils_unittest.cc",
     ]
     deps += [ "../../gn:jsoncpp" ]
diff --git a/src/trace_processor/containers/string_pool.cc b/src/trace_processor/containers/string_pool.cc
index fd65195..0189054 100644
--- a/src/trace_processor/containers/string_pool.cc
+++ b/src/trace_processor/containers/string_pool.cc
@@ -53,8 +53,8 @@
 
 StringPool::~StringPool() = default;
 
-StringPool::StringPool(StringPool&&) = default;
-StringPool& StringPool::operator=(StringPool&&) = default;
+StringPool::StringPool(StringPool&&) noexcept = default;
+StringPool& StringPool::operator=(StringPool&&) noexcept = default;
 
 StringPool::Id StringPool::InsertString(base::StringView str, uint64_t hash) {
   // Try and find enough space in the current block for the string and the
@@ -70,9 +70,8 @@
     // new block to store the string.
     if (str.size() + kMaxMetadataSize >= kMinLargeStringSizeBytes) {
       return InsertLargeString(str, hash);
-    } else {
-      blocks_.emplace_back(kBlockSizeBytes);
     }
+    blocks_.emplace_back(kBlockSizeBytes);
 
     // Try and reserve space again - this time we should definitely succeed.
     std::tie(success, offset) = blocks_.back().TryInsert(str);
@@ -82,7 +81,11 @@
   // Compute the id from the block index and offset and add a mapping from the
   // hash to the id.
   Id string_id = Id::BlockString(blocks_.size() - 1, offset);
-  string_index_.emplace(hash, string_id);
+
+  // Deliberately not adding |string_id| to |string_index_|. The caller
+  // (InternString()) must take care of this.
+  PERFETTO_DCHECK(string_index_.Find(hash));
+
   return string_id;
 }
 
@@ -91,7 +94,11 @@
   large_strings_.emplace_back(new std::string(str.begin(), str.size()));
   // Compute id from the index and add a mapping from the hash to the id.
   Id string_id = Id::LargeString(large_strings_.size() - 1);
-  string_index_.emplace(hash, string_id);
+
+  // Deliberately not adding |string_id| to |string_index_|. The caller
+  // (InternString()) must take care of this.
+  PERFETTO_DCHECK(string_index_.Find(hash));
+
   return string_id;
 }
 
diff --git a/src/trace_processor/containers/string_pool.h b/src/trace_processor/containers/string_pool.h
index 480c085..8d5e22f 100644
--- a/src/trace_processor/containers/string_pool.h
+++ b/src/trace_processor/containers/string_pool.h
@@ -21,9 +21,9 @@
 #include <stdint.h>
 
 #include <limits>
-#include <unordered_map>
 #include <vector>
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/hash.h"
 #include "perfetto/ext/base/optional.h"
 #include "perfetto/ext/base/paged_memory.h"
@@ -107,8 +107,8 @@
   ~StringPool();
 
   // Allow std::move().
-  StringPool(StringPool&&);
-  StringPool& operator=(StringPool&&);
+  StringPool(StringPool&&) noexcept;
+  StringPool& operator=(StringPool&&) noexcept;
 
   // Disable implicit copy.
   StringPool(const StringPool&) = delete;
@@ -119,12 +119,17 @@
       return Id::Null();
 
     auto hash = str.Hash();
-    auto id_it = string_index_.find(hash);
-    if (id_it != string_index_.end()) {
-      PERFETTO_DCHECK(Get(id_it->second) == str);
-      return id_it->second;
+
+    // Perform a hashtable insertion with a null ID just to check if the string
+    // is already inserted. If it's not, overwrite 0 with the actual Id.
+    auto it_and_inserted = string_index_.Insert(hash, Id());
+    Id* id = it_and_inserted.first;
+    if (!it_and_inserted.second) {
+      PERFETTO_DCHECK(Get(*id) == str);
+      return *id;
     }
-    return InsertString(str, hash);
+    *id = InsertString(str, hash);
+    return *id;
   }
 
   base::Optional<Id> GetId(base::StringView str) const {
@@ -132,10 +137,10 @@
       return Id::Null();
 
     auto hash = str.Hash();
-    auto id_it = string_index_.find(hash);
-    if (id_it != string_index_.end()) {
-      PERFETTO_DCHECK(Get(id_it->second) == str);
-      return id_it->second;
+    Id* id = string_index_.Find(hash);
+    if (id) {
+      PERFETTO_DCHECK(Get(*id) == str);
+      return *id;
     }
     return base::nullopt;
   }
@@ -287,10 +292,12 @@
   std::vector<std::unique_ptr<std::string>> large_strings_;
 
   // Maps hashes of strings to the Id in the string pool.
-  // TODO(lalitm): At some point we should benchmark just using a static
-  // hashtable of 1M elements, we can afford paying a fixed 8MB here
-  std::unordered_map<StringHash, Id, base::AlreadyHashed<StringHash>>
-      string_index_;
+  base::FlatHashMap<StringHash,
+                    Id,
+                    base::AlreadyHashed<StringHash>,
+                    base::LinearProbe,
+                    /*AppendOnly=*/true>
+      string_index_{/*initial_capacity=*/1024u * 1024u};
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/dynamic/thread_state_generator.cc b/src/trace_processor/dynamic/thread_state_generator.cc
index 4d33870..74115cd 100644
--- a/src/trace_processor/dynamic/thread_state_generator.cc
+++ b/src/trace_processor/dynamic/thread_state_generator.cc
@@ -91,7 +91,7 @@
   uint32_t sched_idx = 0;
   uint32_t waking_idx = 0;
   uint32_t blocked_idx = 0;
-  std::unordered_map<UniqueTid, ThreadSchedInfo> state_map;
+  TidInfoMap state_map(/*initial_capacity=*/1024);
   while (sched_idx < sched.row_count() || waking_idx < waking.row_count() ||
          blocked_idx < sched_blocked_reason.row_count()) {
     int64_t sched_ts = sched_idx < sched.row_count()
@@ -117,21 +117,21 @@
   }
 
   // At the end, go through and flush any remaining pending events.
-  for (const auto& utid_to_pending_info : state_map) {
-    UniqueTid utid = utid_to_pending_info.first;
-    const ThreadSchedInfo& pending_info = utid_to_pending_info.second;
+  for (auto it = state_map.GetIterator(); it; ++it) {
+    // for (const auto& utid_to_pending_info : state_map) {
+    UniqueTid utid = it.key();
+    const ThreadSchedInfo& pending_info = it.value();
     FlushPendingEventsForThread(utid, pending_info, table.get(), base::nullopt);
   }
 
   return table;
 }
 
-void ThreadStateGenerator::AddSchedEvent(
-    const Table& sched,
-    uint32_t sched_idx,
-    std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map,
-    int64_t trace_end_ts,
-    tables::ThreadStateTable* table) {
+void ThreadStateGenerator::AddSchedEvent(const Table& sched,
+                                         uint32_t sched_idx,
+                                         TidInfoMap& state_map,
+                                         int64_t trace_end_ts,
+                                         tables::ThreadStateTable* table) {
   int64_t ts = sched.GetTypedColumnByName<int64_t>("ts")[sched_idx];
   UniqueTid utid = sched.GetTypedColumnByName<uint32_t>("utid")[sched_idx];
   ThreadSchedInfo* info = &state_map[utid];
@@ -201,10 +201,9 @@
   info->scheduled_row = id_and_row.row;
 }
 
-void ThreadStateGenerator::AddWakingEvent(
-    const Table& waking,
-    uint32_t waking_idx,
-    std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map) {
+void ThreadStateGenerator::AddWakingEvent(const Table& waking,
+                                          uint32_t waking_idx,
+                                          TidInfoMap& state_map) {
   int64_t ts = waking.GetTypedColumnByName<int64_t>("ts")[waking_idx];
   UniqueTid utid = static_cast<UniqueTid>(
       waking.GetTypedColumnByName<int64_t>("ref")[waking_idx]);
@@ -299,10 +298,9 @@
   }
 }
 
-void ThreadStateGenerator::AddBlockedReasonEvent(
-    const Table& blocked_reason,
-    uint32_t blocked_idx,
-    std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map) {
+void ThreadStateGenerator::AddBlockedReasonEvent(const Table& blocked_reason,
+                                                 uint32_t blocked_idx,
+                                                 TidInfoMap& state_map) {
   const auto& utid_col = blocked_reason.GetTypedColumnByName<int64_t>("ref");
   const auto& arg_set_id_col =
       blocked_reason.GetTypedColumnByName<uint32_t>("arg_set_id");
diff --git a/src/trace_processor/dynamic/thread_state_generator.h b/src/trace_processor/dynamic/thread_state_generator.h
index 75895ba..439d4bb 100644
--- a/src/trace_processor/dynamic/thread_state_generator.h
+++ b/src/trace_processor/dynamic/thread_state_generator.h
@@ -19,6 +19,7 @@
 
 #include "src/trace_processor/sqlite/db_sqlite_table.h"
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
 namespace perfetto {
@@ -54,22 +55,25 @@
     base::Optional<int64_t> runnable_ts;
     base::Optional<StringId> blocked_function;
   };
+  using TidInfoMap = base::FlatHashMap<UniqueTid,
+                                       ThreadSchedInfo,
+                                       base::AlreadyHashed<UniqueTid>,
+                                       base::QuadraticProbe,
+                                       /*AppendOnly=*/true>;
 
   void AddSchedEvent(const Table& sched,
                      uint32_t sched_idx,
-                     std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map,
+                     TidInfoMap& state_map,
                      int64_t trace_end_ts,
                      tables::ThreadStateTable* table);
 
-  void AddWakingEvent(
-      const Table& wakeup,
-      uint32_t wakeup_idx,
-      std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map);
+  void AddWakingEvent(const Table& wakeup,
+                      uint32_t wakeup_idx,
+                      TidInfoMap& state_map);
 
-  void AddBlockedReasonEvent(
-      const Table& blocked_reason,
-      uint32_t blocked_idx,
-      std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map);
+  void AddBlockedReasonEvent(const Table& blocked_reason,
+                             uint32_t blocked_idx,
+                             TidInfoMap& state_map);
 
   void FlushPendingEventsForThread(UniqueTid utid,
                                    const ThreadSchedInfo&,
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index b983d7b..619a9e6 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -158,7 +158,7 @@
   if (size == 0)
     return kUnknownTraceType;
   std::string start(reinterpret_cast<const char*>(data),
-                    std::min<size_t>(size, 32));
+                    std::min<size_t>(size, kGuessTraceMaxLookahead));
   if (size >= 8) {
     uint64_t first_word;
     memcpy(&first_word, data, sizeof(first_word));
@@ -180,10 +180,16 @@
       base::StartsWith(start, "<html>"))
     return kSystraceTraceType;
 
-  // Ctrace is deflate'ed systrace.
-  if (base::Contains(start, "TRACE:"))
+  // Traces obtained from atrace -z (compress).
+  // They all have the string "TRACE:" followed by 78 9C which is a zlib header
+  // for "deflate, default compression, window size=32K" (see b/208691037)
+  if (base::Contains(start, "TRACE:\n\x78\x9c"))
     return kCtraceTraceType;
 
+  // Traces obtained from atrace without -z (no compression).
+  if (base::Contains(start, "TRACE:\n"))
+    return kSystraceTraceType;
+
   // Ninja's buils log (.ninja_log).
   if (base::StartsWith(start, "# ninja log"))
     return kNinjaLogTraceType;
diff --git a/src/trace_processor/forwarding_trace_parser.h b/src/trace_processor/forwarding_trace_parser.h
index a5164cf..566b3b8 100644
--- a/src/trace_processor/forwarding_trace_parser.h
+++ b/src/trace_processor/forwarding_trace_parser.h
@@ -24,6 +24,8 @@
 namespace perfetto {
 namespace trace_processor {
 
+constexpr size_t kGuessTraceMaxLookahead = 64;
+
 enum TraceType {
   kUnknownTraceType,
   kProtoTraceType,
diff --git a/src/trace_processor/importers/common/args_tracker.cc b/src/trace_processor/importers/common/args_tracker.cc
index c7a5b97..1b6e0e9 100644
--- a/src/trace_processor/importers/common/args_tracker.cc
+++ b/src/trace_processor/importers/common/args_tracker.cc
@@ -79,7 +79,7 @@
     }
 
     ArgSetId set_id =
-        context_->global_args_tracker->AddArgSet(args_, i, next_rid_idx);
+        context_->global_args_tracker->AddArgSet(&args_[0], i, next_rid_idx);
     column->Set(row, SqlValue::Long(set_id));
 
     i = next_rid_idx;
@@ -96,9 +96,9 @@
 
 ArgsTracker::BoundInserter::~BoundInserter() {}
 
-ArgsTracker::BoundInserter::BoundInserter(BoundInserter&&) = default;
+ArgsTracker::BoundInserter::BoundInserter(BoundInserter&&) noexcept = default;
 ArgsTracker::BoundInserter& ArgsTracker::BoundInserter::operator=(
-    BoundInserter&&) = default;
+    BoundInserter&&) noexcept = default;
 
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/common/args_tracker.h b/src/trace_processor/importers/common/args_tracker.h
index f891537..9452b95 100644
--- a/src/trace_processor/importers/common/args_tracker.h
+++ b/src/trace_processor/importers/common/args_tracker.h
@@ -17,6 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_ARGS_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_ARGS_TRACKER_H_
 
+#include "perfetto/ext/base/small_vector.h"
 #include "src/trace_processor/importers/common/global_args_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
@@ -38,8 +39,8 @@
    public:
     virtual ~BoundInserter();
 
-    BoundInserter(BoundInserter&&);
-    BoundInserter& operator=(BoundInserter&&);
+    BoundInserter(BoundInserter&&) noexcept;
+    BoundInserter& operator=(BoundInserter&&) noexcept;
 
     BoundInserter(const BoundInserter&) = delete;
     BoundInserter& operator=(const BoundInserter&) = delete;
@@ -160,7 +161,7 @@
               Variadic,
               UpdatePolicy);
 
-  std::vector<GlobalArgsTracker::Arg> args_;
+  base::SmallVector<GlobalArgsTracker::Arg, 16> args_;
   TraceProcessorContext* const context_;
 
   using ArrayKeyTuple =
diff --git a/src/trace_processor/importers/common/flow_tracker.cc b/src/trace_processor/importers/common/flow_tracker.cc
index 9e6e2d8..6e7dd2f 100644
--- a/src/trace_processor/importers/common/flow_tracker.cc
+++ b/src/trace_processor/importers/common/flow_tracker.cc
@@ -46,11 +46,11 @@
     context_->storage->IncrementStats(stats::flow_no_enclosing_slice);
     return;
   }
-  if (flow_to_slice_map_.count(flow_id) != 0) {
+  auto it_and_ins = flow_to_slice_map_.Insert(flow_id, open_slice_id.value());
+  if (!it_and_ins.second) {
     context_->storage->IncrementStats(stats::flow_duplicate_id);
     return;
   }
-  flow_to_slice_map_[flow_id] = open_slice_id.value();
 }
 
 void FlowTracker::Step(TrackId track_id, FlowId flow_id) {
@@ -60,13 +60,14 @@
     context_->storage->IncrementStats(stats::flow_no_enclosing_slice);
     return;
   }
-  if (flow_to_slice_map_.count(flow_id) == 0) {
+  auto* it = flow_to_slice_map_.Find(flow_id);
+  if (!it) {
     context_->storage->IncrementStats(stats::flow_step_without_start);
     return;
   }
-  SliceId slice_out_id = flow_to_slice_map_[flow_id];
+  SliceId slice_out_id = *it;
   InsertFlow(flow_id, slice_out_id, open_slice_id.value());
-  flow_to_slice_map_[flow_id] = open_slice_id.value();
+  *it = open_slice_id.value();
 }
 
 void FlowTracker::End(TrackId track_id,
@@ -83,29 +84,28 @@
     context_->storage->IncrementStats(stats::flow_no_enclosing_slice);
     return;
   }
-  if (flow_to_slice_map_.count(flow_id) == 0) {
+  auto* it = flow_to_slice_map_.Find(flow_id);
+  if (!it) {
     context_->storage->IncrementStats(stats::flow_end_without_start);
     return;
   }
-  SliceId slice_out_id = flow_to_slice_map_[flow_id];
-  if (close_flow) {
-    flow_to_slice_map_.erase(flow_to_slice_map_.find(flow_id));
-  }
+  SliceId slice_out_id = *it;
+  if (close_flow)
+    flow_to_slice_map_.Erase(flow_id);
   InsertFlow(flow_id, slice_out_id, open_slice_id.value());
 }
 
 bool FlowTracker::IsActive(FlowId flow_id) const {
-  return flow_to_slice_map_.find(flow_id) != flow_to_slice_map_.end();
+  return flow_to_slice_map_.Find(flow_id) != nullptr;
 }
 
 FlowId FlowTracker::GetFlowIdForV1Event(uint64_t source_id,
                                         StringId cat,
                                         StringId name) {
   V1FlowId v1_flow_id = {source_id, cat, name};
-  auto iter = v1_flow_id_to_flow_id_map_.find(v1_flow_id);
-  if (iter != v1_flow_id_to_flow_id_map_.end()) {
-    return iter->second;
-  }
+  auto* iter = v1_flow_id_to_flow_id_map_.Find(v1_flow_id);
+  if (iter)
+    return *iter;
   FlowId new_id = v1_id_counter_++;
   flow_id_to_v1_flow_id_map_[new_id] = v1_flow_id;
   v1_flow_id_to_flow_id_map_[v1_flow_id] = new_id;
@@ -114,16 +114,16 @@
 
 void FlowTracker::ClosePendingEventsOnTrack(TrackId track_id,
                                             SliceId slice_id) {
-  auto iter = pending_flow_ids_map_.find(track_id);
-  if (iter == pending_flow_ids_map_.end())
+  auto* iter = pending_flow_ids_map_.Find(track_id);
+  if (!iter)
     return;
 
-  for (FlowId flow_id : iter->second) {
+  for (FlowId flow_id : *iter) {
     SliceId slice_out_id = flow_to_slice_map_[flow_id];
     InsertFlow(flow_id, slice_out_id, slice_id);
   }
 
-  pending_flow_ids_map_.erase(iter);
+  pending_flow_ids_map_.Erase(track_id);
 }
 
 void FlowTracker::InsertFlow(FlowId flow_id,
@@ -132,13 +132,13 @@
   tables::FlowTable::Row row(slice_out_id, slice_in_id, kInvalidArgSetId);
   auto id = context_->storage->mutable_flow_table()->Insert(row).id;
 
-  auto it = flow_id_to_v1_flow_id_map_.find(flow_id);
-  if (it != flow_id_to_v1_flow_id_map_.end()) {
+  auto* it = flow_id_to_v1_flow_id_map_.Find(flow_id);
+  if (it) {
     // TODO(b/168007725): Add any args from v1 flow events and also export them.
     auto args_tracker = ArgsTracker(context_);
     auto inserter = context_->args_tracker->AddArgsTo(id);
-    inserter.AddArg(name_key_id_, Variadic::String(it->second.name));
-    inserter.AddArg(cat_key_id_, Variadic::String(it->second.cat));
+    inserter.AddArg(name_key_id_, Variadic::String(it->name));
+    inserter.AddArg(cat_key_id_, Variadic::String(it->cat));
     context_->args_tracker->Flush();
   }
 }
diff --git a/src/trace_processor/importers/common/flow_tracker.h b/src/trace_processor/importers/common/flow_tracker.h
index f05d854..5def9b6 100644
--- a/src/trace_processor/importers/common/flow_tracker.h
+++ b/src/trace_processor/importers/common/flow_tracker.h
@@ -19,6 +19,7 @@
 
 #include <stdint.h>
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
@@ -78,11 +79,11 @@
     }
   };
 
-  using FlowToSourceSliceMap = std::unordered_map<FlowId, SliceId>;
-  using PendingFlowsMap = std::unordered_map<TrackId, std::vector<FlowId>>;
+  using FlowToSourceSliceMap = base::FlatHashMap<FlowId, SliceId>;
+  using PendingFlowsMap = base::FlatHashMap<TrackId, std::vector<FlowId>>;
   using V1FlowIdToFlowIdMap =
-      std::unordered_map<V1FlowId, FlowId, V1FlowIdHasher>;
-  using FlowIdToV1FlowId = std::unordered_map<FlowId, V1FlowId>;
+      base::FlatHashMap<V1FlowId, FlowId, V1FlowIdHasher>;
+  using FlowIdToV1FlowId = base::FlatHashMap<FlowId, V1FlowId>;
 
   void InsertFlow(FlowId flow_id,
                   SliceId outgoing_slice_id,
diff --git a/src/trace_processor/importers/common/global_args_tracker.h b/src/trace_processor/importers/common/global_args_tracker.h
index ee1867e..0d1c365 100644
--- a/src/trace_processor/importers/common/global_args_tracker.h
+++ b/src/trace_processor/importers/common/global_args_tracker.h
@@ -17,7 +17,9 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_GLOBAL_ARGS_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_GLOBAL_ARGS_TRACKER_H_
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/hash.h"
+#include "perfetto/ext/base/small_vector.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
@@ -82,14 +84,11 @@
     }
   };
 
-  GlobalArgsTracker(TraceProcessorContext* context);
+  explicit GlobalArgsTracker(TraceProcessorContext* context);
 
   // Assumes that the interval [begin, end) of |args| is sorted by keys.
-  ArgSetId AddArgSet(const std::vector<Arg>& args,
-                     uint32_t begin,
-                     uint32_t end) {
-    std::vector<uint32_t> valid_indexes;
-    valid_indexes.reserve(end - begin);
+  ArgSetId AddArgSet(const Arg* args, uint32_t begin, uint32_t end) {
+    base::SmallVector<uint32_t, 64> valid_indexes;
 
     // TODO(eseckler): Also detect "invalid" key combinations in args sets (e.g.
     // "foo" and "foo.bar" in the same arg set)?
@@ -107,7 +106,7 @@
         }
       }
 
-      valid_indexes.push_back(i);
+      valid_indexes.emplace_back(i);
     }
 
     base::Hash hash;
@@ -118,13 +117,16 @@
     auto* arg_table = context_->storage->mutable_arg_table();
 
     ArgSetHash digest = hash.digest();
-    auto it = arg_row_for_hash_.find(digest);
-    if (it != arg_row_for_hash_.end())
-      return arg_table->arg_set_id()[it->second];
+    auto it_and_inserted =
+        arg_row_for_hash_.Insert(digest, arg_table->row_count());
+    if (!it_and_inserted.second) {
+      // Already inserted.
+      return arg_table->arg_set_id()[*it_and_inserted.first];
+    }
 
-    // The +1 ensures that nothing has an id == kInvalidArgSetId == 0.
-    ArgSetId id = static_cast<uint32_t>(arg_row_for_hash_.size()) + 1;
-    arg_row_for_hash_.emplace(digest, arg_table->row_count());
+    // Taking size() after the Insert() ensures that nothing has an id == 0
+    // (0 == kInvalidArgSetId).
+    ArgSetId id = static_cast<uint32_t>(arg_row_for_hash_.size());
     for (uint32_t i : valid_indexes) {
       const auto& arg = args[i];
 
@@ -163,10 +165,17 @@
     return id;
   }
 
+  // Exposed for making tests easier to write.
+  ArgSetId AddArgSet(const std::vector<Arg>& args,
+                     uint32_t begin,
+                     uint32_t end) {
+    return AddArgSet(args.data(), begin, end);
+  }
+
  private:
   using ArgSetHash = uint64_t;
 
-  std::unordered_map<ArgSetHash, uint32_t, base::AlreadyHashed<ArgSetHash>>
+  base::FlatHashMap<ArgSetHash, uint32_t, base::AlreadyHashed<ArgSetHash>>
       arg_row_for_hash_;
 
   TraceProcessorContext* context_;
diff --git a/src/trace_processor/importers/common/process_tracker.cc b/src/trace_processor/importers/common/process_tracker.cc
index 0981063..47b1d7a 100644
--- a/src/trace_processor/importers/common/process_tracker.cc
+++ b/src/trace_processor/importers/common/process_tracker.cc
@@ -89,7 +89,7 @@
   // of the process, we should also finish the process itself.
   PERFETTO_DCHECK(thread_table->is_main_thread()[utid].value());
   process_table->mutable_end_ts()->Set(*opt_upid, timestamp);
-  pids_.erase(tid);
+  pids_.Erase(tid);
 }
 
 base::Optional<UniqueTid> ProcessTracker::GetThreadOrNull(uint32_t tid) {
@@ -156,8 +156,8 @@
 
   // If the process has been replaced in |pids_|, this thread is dead.
   uint32_t current_pid = processes->pid()[current_upid];
-  auto pid_it = pids_.find(current_pid);
-  if (pid_it != pids_.end() && pid_it->second != current_upid)
+  auto pid_it = pids_.Find(current_pid);
+  if (pid_it && *pid_it != current_upid)
     return false;
 
   return true;
@@ -169,13 +169,13 @@
   auto* threads = context_->storage->mutable_thread_table();
   auto* processes = context_->storage->mutable_process_table();
 
-  auto vector_it = tids_.find(tid);
-  if (vector_it == tids_.end())
+  auto vector_it = tids_.Find(tid);
+  if (!vector_it)
     return base::nullopt;
 
   // Iterate backwards through the threads so ones later in the trace are more
   // likely to be picked.
-  const auto& vector = vector_it->second;
+  const auto& vector = *vector_it;
   for (auto it = vector.rbegin(); it != vector.rend(); it++) {
     UniqueTid current_utid = *it;
 
@@ -225,7 +225,7 @@
                                           uint32_t pid,
                                           StringId main_thread_name,
                                           ThreadNamePriority priority) {
-  pids_.erase(pid);
+  pids_.Erase(pid);
   // TODO(eseckler): Consider erasing all old entries in |tids_| that match the
   // |pid| (those would be for an older process with the same pid). Right now,
   // we keep them in |tids_| (if they weren't erased by EndThread()), but ignore
@@ -325,18 +325,20 @@
 
 UniquePid ProcessTracker::GetOrCreateProcess(uint32_t pid) {
   auto* process_table = context_->storage->mutable_process_table();
-  auto it = pids_.find(pid);
-  if (it != pids_.end()) {
+
+  // If the insertion succeeds, we'll fill the upid below.
+  auto it_and_ins = pids_.Insert(pid, UniquePid{0});
+  if (!it_and_ins.second) {
     // Ensure that the process has not ended.
-    PERFETTO_DCHECK(!process_table->end_ts()[it->second].has_value());
-    return it->second;
+    PERFETTO_DCHECK(!process_table->end_ts()[*it_and_ins.first].has_value());
+    return *it_and_ins.first;
   }
 
   tables::ProcessTable::Row row;
   row.pid = pid;
 
   UniquePid upid = process_table->Insert(row).row;
-  pids_.emplace(pid, upid);
+  *it_and_ins.first = upid;  // Update the newly inserted hashmap entry.
 
   // Create an entry for the main thread.
   // We cannot call StartNewThread() here, because threads for this process
@@ -459,8 +461,8 @@
 
 void ProcessTracker::SetPidZeroIgnoredForIdleProcess() {
   // Create a mapping from (t|p)id 0 -> u(t|p)id 0 for the idle process.
-  tids_.emplace(0, std::vector<UniqueTid>{0});
-  pids_.emplace(0, 0);
+  tids_.Insert(0, std::vector<UniqueTid>{0});
+  pids_.Insert(0, 0);
 
   auto swapper_id = context_->storage->InternString("swapper");
   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 86c4339..7cfe89d 100644
--- a/src/trace_processor/importers/common/process_tracker.h
+++ b/src/trace_processor/importers/common/process_tracker.h
@@ -19,6 +19,7 @@
 
 #include <tuple>
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
@@ -132,8 +133,8 @@
 
   // Returns the upid for a given pid.
   base::Optional<UniquePid> UpidForPidForTesting(uint32_t pid) {
-    auto it = pids_.find(pid);
-    return it == pids_.end() ? base::nullopt : base::make_optional(it->second);
+    auto it = pids_.Find(pid);
+    return it ? base::make_optional(*it) : base::nullopt;
   }
 
   // Returns the bounds of a range that includes all UniqueTids that have the
@@ -188,10 +189,10 @@
   // simultaneously. This is no longer the case so this should be removed
   // (though it seems like there are subtle things which break in Chrome if this
   // changes).
-  std::unordered_map<uint32_t /* tid */, std::vector<UniqueTid>> tids_;
+  base::FlatHashMap<uint32_t /* tid */, std::vector<UniqueTid>> tids_;
 
   // Mapping of the most recently seen pid to the associated upid.
-  std::unordered_map<uint32_t /* pid (aka tgid) */, UniquePid> pids_;
+  base::FlatHashMap<uint32_t /* pid (aka tgid) */, UniquePid> pids_;
 
   // Pending thread associations. The meaning of a pair<ThreadA, ThreadB> in
   // this vector is: we know that A and B belong to the same process, but we
diff --git a/src/trace_processor/importers/common/slice_tracker.cc b/src/trace_processor/importers/common/slice_tracker.cc
index dbb76a2..b160ec5 100644
--- a/src/trace_processor/importers/common/slice_tracker.cc
+++ b/src/trace_processor/importers/common/slice_tracker.cc
@@ -57,8 +57,8 @@
   // Double check that if we've seen this track in the past, it was also
   // marked as unnestable then.
 #if PERFETTO_DCHECK_IS_ON()
-  auto it = stacks_.find(row.track_id);
-  PERFETTO_DCHECK(it == stacks_.end() || it->second.is_legacy_unnestable);
+  auto* it = stacks_.Find(row.track_id);
+  PERFETTO_DCHECK(!it || it->is_legacy_unnestable);
 #endif
 
   // Ensure that StartSlice knows that this track is unnestable.
@@ -98,11 +98,11 @@
                                                StringId category,
                                                StringId name,
                                                SetArgsCallback args_callback) {
-  auto it = stacks_.find(track_id);
-  if (it == stacks_.end())
+  auto* it = stacks_.Find(track_id);
+  if (!it)
     return base::nullopt;
 
-  auto& stack = it->second.slice_stack;
+  auto& stack = it->slice_stack;
   if (stack.empty())
     return base::nullopt;
 
@@ -194,11 +194,11 @@
   }
   prev_timestamp_ = timestamp;
 
-  auto it = stacks_.find(track_id);
-  if (it == stacks_.end())
+  auto it = stacks_.Find(track_id);
+  if (!it)
     return base::nullopt;
 
-  TrackInfo& track_info = it->second;
+  TrackInfo& track_info = *it;
   SlicesStack& stack = track_info.slice_stack;
   MaybeCloseStack(timestamp, &stack, track_id);
   if (stack.empty())
@@ -275,7 +275,7 @@
   // TODO(eseckler): Reconsider whether we want to close pending slices by
   // setting their duration to |trace_end - event_start|. Might still want some
   // additional way of flagging these events as "incomplete" to the UI.
-  stacks_.clear();
+  stacks_.Clear();
 }
 
 void SliceTracker::SetOnSliceBeginCallback(OnSliceBeginCallback callback) {
@@ -284,10 +284,10 @@
 
 base::Optional<SliceId> SliceTracker::GetTopmostSliceOnTrack(
     TrackId track_id) const {
-  const auto iter = stacks_.find(track_id);
-  if (iter == stacks_.end())
+  const auto* iter = stacks_.Find(track_id);
+  if (!iter)
     return base::nullopt;
-  const auto& stack = iter->second.slice_stack;
+  const auto& stack = iter->slice_stack;
   if (stack.empty())
     return base::nullopt;
   uint32_t slice_idx = stack.back().row;
diff --git a/src/trace_processor/importers/common/slice_tracker.h b/src/trace_processor/importers/common/slice_tracker.h
index f574164..847bda6 100644
--- a/src/trace_processor/importers/common/slice_tracker.h
+++ b/src/trace_processor/importers/common/slice_tracker.h
@@ -19,6 +19,7 @@
 
 #include <stdint.h>
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
@@ -124,7 +125,7 @@
     uint32_t legacy_unnestable_begin_count = 0;
     int64_t legacy_unnestable_last_begin_ts = 0;
   };
-  using StackMap = std::unordered_map<TrackId, TrackInfo>;
+  using StackMap = base::FlatHashMap<TrackId, TrackInfo>;
 
   // virtual for testing.
   virtual base::Optional<SliceId> StartSlice(int64_t timestamp,
diff --git a/src/trace_processor/importers/ftrace/binder_tracker.cc b/src/trace_processor/importers/ftrace/binder_tracker.cc
index d866c0a..64eee90 100644
--- a/src/trace_processor/importers/ftrace/binder_tracker.cc
+++ b/src/trace_processor/importers/ftrace/binder_tracker.cc
@@ -15,14 +15,13 @@
  */
 
 #include "src/trace_processor/importers/ftrace/binder_tracker.h"
+#include "perfetto/base/compiler.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
-#include "perfetto/base/compiler.h"
-#include "perfetto/ext/base/string_utils.h"
-
 namespace perfetto {
 namespace trace_processor {
 
@@ -157,7 +156,8 @@
     return;
   }
 
-  if (transaction_await_rcv.count(transaction_id) > 0) {
+  TrackId* rcv_track_id = transaction_await_rcv.Find(transaction_id);
+  if (rcv_track_id) {
     // First begin the reply slice to get its slice id.
     auto reply_slice_id = context_->slice_tracker->Begin(
         ts, track_id, binder_category_id_, reply_id_);
@@ -171,9 +171,9 @@
                          Variadic::UnsignedInteger(reply_slice_id->value));
     };
     // Add the dest args to the current transaction slice and get the slice id.
-    auto transaction_slice_id = context_->slice_tracker->AddArgs(
-        transaction_await_rcv[transaction_id], binder_category_id_,
-        transaction_slice_id_, args_inserter);
+    auto transaction_slice_id =
+        context_->slice_tracker->AddArgs(*rcv_track_id, binder_category_id_,
+                                         transaction_slice_id_, args_inserter);
 
     // Add the dest slice id to the reply slice that has just begun.
     auto reply_dest_inserter =
@@ -184,15 +184,15 @@
         };
     context_->slice_tracker->AddArgs(track_id, binder_category_id_, reply_id_,
                                      reply_dest_inserter);
-    transaction_await_rcv.erase(transaction_id);
+    transaction_await_rcv.Erase(transaction_id);
     return;
   }
 
-  if (awaiting_async_rcv_.count(transaction_id) > 0) {
-    auto args = awaiting_async_rcv_[transaction_id];
+  SetArgsCallback* args = awaiting_async_rcv_.Find(transaction_id);
+  if (args) {
     context_->slice_tracker->Scoped(ts, track_id, binder_category_id_,
-                                    async_rcv_id_, 0, args);
-    awaiting_async_rcv_.erase(transaction_id);
+                                    async_rcv_id_, 0, *args);
+    awaiting_async_rcv_.Erase(transaction_id);
     return;
   }
 }
@@ -209,7 +209,7 @@
 void BinderTracker::Locked(int64_t ts, uint32_t pid) {
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
 
-  if (attempt_lock_.count(pid) == 0)
+  if (!attempt_lock_.Find(pid))
     return;
 
   TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
@@ -218,19 +218,19 @@
                                  lock_held_id_);
 
   lock_acquired_[pid] = ts;
-  attempt_lock_.erase(pid);
+  attempt_lock_.Erase(pid);
 }
 
 void BinderTracker::Unlock(int64_t ts, uint32_t pid) {
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
 
-  if (lock_acquired_.count(pid) == 0)
+  if (!lock_acquired_.Find(pid))
     return;
 
   TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
   context_->slice_tracker->End(ts, track_id, binder_category_id_,
                                lock_held_id_);
-  lock_acquired_.erase(pid);
+  lock_acquired_.Erase(pid);
 }
 
 void BinderTracker::TransactionAllocBuf(int64_t ts,
diff --git a/src/trace_processor/importers/ftrace/binder_tracker.h b/src/trace_processor/importers/ftrace/binder_tracker.h
index a96816d..a9482de 100644
--- a/src/trace_processor/importers/ftrace/binder_tracker.h
+++ b/src/trace_processor/importers/ftrace/binder_tracker.h
@@ -18,9 +18,9 @@
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_BINDER_TRACKER_H_
 
 #include <stdint.h>
-#include <unordered_map>
-#include <unordered_set>
 
+#include "perfetto/base/flat_set.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/destructible.h"
@@ -68,14 +68,14 @@
 
  private:
   TraceProcessorContext* const context_;
-  std::unordered_set<int32_t> awaiting_rcv_for_reply_;
+  base::FlatSet<int32_t> awaiting_rcv_for_reply_;
 
-  std::unordered_map<int32_t, TrackId> transaction_await_rcv;
-  std::unordered_map<int32_t, SetArgsCallback> awaiting_async_rcv_;
+  base::FlatHashMap<int32_t, TrackId> transaction_await_rcv;
+  base::FlatHashMap<int32_t, SetArgsCallback> awaiting_async_rcv_;
 
-  std::unordered_map<uint32_t, int64_t> attempt_lock_;
+  base::FlatHashMap<uint32_t, int64_t> attempt_lock_;
 
-  std::unordered_map<uint32_t, int64_t> lock_acquired_;
+  base::FlatHashMap<uint32_t, int64_t> lock_acquired_;
 
   const StringId binder_category_id_;
   const StringId lock_waiting_id_;
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index 808d7bd..a6219d1 100644
--- a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
@@ -24,7 +24,7 @@
 namespace trace_processor {
 namespace {
 
-std::array<MessageDescriptor, 359> descriptors{{
+std::array<MessageDescriptor, 360> descriptors{{
     {nullptr, 0, {}},
     {nullptr, 0, {}},
     {nullptr, 0, {}},
@@ -3843,6 +3843,17 @@
             {"ib_quota", ProtoSchemaType::kUint64},
         },
     },
+    {
+        "rss_stat_throttled",
+        4,
+        {
+            {},
+            {"curr", ProtoSchemaType::kUint32},
+            {"member", ProtoSchemaType::kInt32},
+            {"mm_id", ProtoSchemaType::kUint32},
+            {"size", ProtoSchemaType::kInt64},
+        },
+    },
 }};
 
 }  // namespace
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index c25859e..155d0f5 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -145,7 +145,8 @@
       sched_blocked_reason_id_(
           context->storage->InternString("sched_blocked_reason")),
       io_wait_id_(context->storage->InternString("io_wait")),
-      function_id_(context->storage->InternString("function")) {
+      function_id_(context->storage->InternString("function")),
+      waker_utid_id_(context->storage->InternString("waker_utid")) {
   // Build the lookup table for the strings inside ftrace events (e.g. the
   // name of ftrace event fields and the names of their args).
   for (size_t i = 0; i < GetDescriptorsSize(); i++) {
@@ -368,11 +369,11 @@
         break;
       }
       case FtraceEvent::kSchedWakeupFieldNumber: {
-        ParseSchedWakeup(ts, data);
+        ParseSchedWakeup(ts, pid, data);
         break;
       }
       case FtraceEvent::kSchedWakingFieldNumber: {
-        ParseSchedWaking(ts, data);
+        ParseSchedWaking(ts, pid, data);
         break;
       }
       case FtraceEvent::kSchedProcessFreeFieldNumber: {
@@ -399,8 +400,9 @@
         ParseZero(ts, pid, data);
         break;
       }
+      case FtraceEvent::kRssStatThrottledFieldNumber:
       case FtraceEvent::kRssStatFieldNumber: {
-        rss_stat_tracker_.ParseRssStat(ts, pid, data);
+        rss_stat_tracker_.ParseRssStat(ts, fld.id(), pid, data);
         break;
       }
       case FtraceEvent::kIonHeapGrowFieldNumber: {
@@ -734,24 +736,36 @@
       next_pid, ss.next_comm(), ss.next_prio());
 }
 
-void FtraceParser::ParseSchedWakeup(int64_t timestamp, ConstBytes blob) {
+void FtraceParser::ParseSchedWakeup(int64_t timestamp,
+                                    uint32_t pid,
+                                    ConstBytes blob) {
   protos::pbzero::SchedWakeupFtraceEvent::Decoder sw(blob.data, blob.size);
   uint32_t wakee_pid = static_cast<uint32_t>(sw.pid());
   StringId name_id = context_->storage->InternString(sw.comm());
-  auto utid = context_->process_tracker->UpdateThreadName(
+  auto wakee_utid = context_->process_tracker->UpdateThreadName(
       wakee_pid, name_id, ThreadNamePriority::kFtrace);
-  context_->event_tracker->PushInstant(timestamp, sched_wakeup_name_id_, utid,
-                                       RefType::kRefUtid);
+  InstantId id = context_->event_tracker->PushInstant(
+      timestamp, sched_wakeup_name_id_, wakee_utid, RefType::kRefUtid);
+
+  UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+  context_->args_tracker->AddArgsTo(id).AddArg(waker_utid_id_,
+                                               Variadic::UnsignedInteger(utid));
 }
 
-void FtraceParser::ParseSchedWaking(int64_t timestamp, ConstBytes blob) {
+void FtraceParser::ParseSchedWaking(int64_t timestamp,
+                                    uint32_t pid,
+                                    ConstBytes blob) {
   protos::pbzero::SchedWakingFtraceEvent::Decoder sw(blob.data, blob.size);
   uint32_t wakee_pid = static_cast<uint32_t>(sw.pid());
   StringId name_id = context_->storage->InternString(sw.comm());
-  auto utid = context_->process_tracker->UpdateThreadName(
+  auto wakee_utid = context_->process_tracker->UpdateThreadName(
       wakee_pid, name_id, ThreadNamePriority::kFtrace);
-  context_->event_tracker->PushInstant(timestamp, sched_waking_name_id_, utid,
-                                       RefType::kRefUtid);
+  InstantId id = context_->event_tracker->PushInstant(
+      timestamp, sched_waking_name_id_, wakee_utid, RefType::kRefUtid);
+
+  UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+  context_->args_tracker->AddArgsTo(id).AddArg(waker_utid_id_,
+                                               Variadic::UnsignedInteger(utid));
 }
 
 void FtraceParser::ParseSchedProcessFree(int64_t timestamp, ConstBytes blob) {
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index bf798a3..2f4cb0b 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -48,8 +48,8 @@
                              protozero::ConstBytes,
                              PacketSequenceStateGeneration*);
   void ParseSchedSwitch(uint32_t cpu, int64_t timestamp, protozero::ConstBytes);
-  void ParseSchedWakeup(int64_t timestamp, protozero::ConstBytes);
-  void ParseSchedWaking(int64_t timestamp, protozero::ConstBytes);
+  void ParseSchedWakeup(int64_t timestamp, uint32_t pid, protozero::ConstBytes);
+  void ParseSchedWaking(int64_t timestamp, uint32_t pid, protozero::ConstBytes);
   void ParseSchedProcessFree(int64_t timestamp, protozero::ConstBytes);
   void ParseCpuFreq(int64_t timestamp, protozero::ConstBytes);
   void ParseGpuFreq(int64_t timestamp, protozero::ConstBytes);
@@ -191,6 +191,7 @@
   const StringId sched_blocked_reason_id_;
   const StringId io_wait_id_;
   const StringId function_id_;
+  const StringId waker_utid_id_;
 
   struct FtraceMessageStrings {
     // The string id of name of the event field (e.g. sched_switch's id).
diff --git a/src/trace_processor/importers/ftrace/rss_stat_tracker.cc b/src/trace_processor/importers/ftrace/rss_stat_tracker.cc
index 214bd75..80ca10f 100644
--- a/src/trace_processor/importers/ftrace/rss_stat_tracker.cc
+++ b/src/trace_processor/importers/ftrace/rss_stat_tracker.cc
@@ -20,11 +20,15 @@
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
 #include "protos/perfetto/trace/ftrace/kmem.pbzero.h"
+#include "protos/perfetto/trace/ftrace/synthetic.pbzero.h"
 
 namespace perfetto {
 namespace trace_processor {
 
+using FtraceEvent = protos::pbzero::FtraceEvent;
+
 RssStatTracker::RssStatTracker(TraceProcessorContext* context)
     : context_(context) {
   rss_members_.emplace_back(context->storage->InternString("mem.rss.file"));
@@ -37,19 +41,41 @@
       context->storage->InternString("mem.unknown"));  // Keep this last.
 }
 
-void RssStatTracker::ParseRssStat(int64_t ts, uint32_t pid, ConstBytes blob) {
-  protos::pbzero::RssStatFtraceEvent::Decoder rss(blob.data, blob.size);
-  uint32_t member = static_cast<uint32_t>(rss.member());
-  int64_t size = rss.size();
+void RssStatTracker::ParseRssStat(int64_t ts,
+                                  int32_t field_id,
+                                  uint32_t pid,
+                                  ConstBytes blob) {
+  uint32_t member;
+  int64_t size;
   base::Optional<bool> curr;
   base::Optional<int64_t> mm_id;
-  if (rss.has_curr()) {
+
+  if (field_id == FtraceEvent::kRssStatFieldNumber) {
+    protos::pbzero::RssStatFtraceEvent::Decoder rss(blob.data, blob.size);
+
+    member = static_cast<uint32_t>(rss.member());
+    size = rss.size();
+    if (rss.has_curr()) {
+      curr = base::make_optional(static_cast<bool>(rss.curr()));
+    }
+    if (rss.has_mm_id()) {
+      mm_id = base::make_optional(rss.mm_id());
+    }
+
+    ParseRssStat(ts, pid, size, member, curr, mm_id);
+  } else if (field_id == FtraceEvent::kRssStatThrottledFieldNumber) {
+    protos::pbzero::RssStatThrottledFtraceEvent::Decoder rss(blob.data,
+                                                             blob.size);
+
+    member = static_cast<uint32_t>(rss.member());
+    size = rss.size();
     curr = base::make_optional(static_cast<bool>(rss.curr()));
-  }
-  if (rss.has_mm_id()) {
     mm_id = base::make_optional(rss.mm_id());
+
+    ParseRssStat(ts, pid, size, member, curr, mm_id);
+  } else {
+    PERFETTO_DFATAL("Unexpected field id");
   }
-  ParseRssStat(ts, pid, size, member, curr, mm_id);
 }
 
 void RssStatTracker::ParseRssStat(int64_t ts,
@@ -97,32 +123,33 @@
 
   // If curr is false, try and lookup the utid we previously saw for this
   // mm id.
-  auto it = mm_id_to_utid_.find(mm_id);
-  if (it == mm_id_to_utid_.end())
+  auto* it = mm_id_to_utid_.Find(mm_id);
+  if (!it)
     return base::nullopt;
 
   // If the utid in the map is the same as our current utid but curr is false,
   // that means we are in the middle of a process changing mm structs (i.e. in
   // the middle of a vfork + exec). Therefore, we should discard the association
   // of this vm struct with this thread.
-  UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
-  if (it->second == utid) {
-    mm_id_to_utid_.erase(it);
+  const UniqueTid mm_utid = *it;
+  const UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+  if (mm_utid == utid) {
+    mm_id_to_utid_.Erase(mm_id);
     return base::nullopt;
   }
 
   // Verify that the utid in the map is still alive. This can happen if an mm
   // struct we saw in the past is about to be reused after thread but we don't
   // know the new process that struct will be associated with.
-  if (!context_->process_tracker->IsThreadAlive(it->second)) {
-    mm_id_to_utid_.erase(it);
+  if (!context_->process_tracker->IsThreadAlive(mm_utid)) {
+    mm_id_to_utid_.Erase(mm_id);
     return base::nullopt;
   }
 
   // This case happens when a process is changing the VM of another process and
   // we know that the utid corresponding to the target process. Just return that
   // utid.
-  return it->second;
+  return mm_utid;
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/ftrace/rss_stat_tracker.h b/src/trace_processor/importers/ftrace/rss_stat_tracker.h
index fff203c..b3ba29a 100644
--- a/src/trace_processor/importers/ftrace/rss_stat_tracker.h
+++ b/src/trace_processor/importers/ftrace/rss_stat_tracker.h
@@ -17,8 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_RSS_STAT_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_RSS_STAT_TRACKER_H_
 
-#include <unordered_map>
-
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/protozero/field.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
@@ -33,7 +32,10 @@
 
   explicit RssStatTracker(TraceProcessorContext*);
 
-  void ParseRssStat(int64_t ts, uint32_t pid, ConstBytes blob);
+  void ParseRssStat(int64_t ts,
+                    int32_t field_id,
+                    uint32_t pid,
+                    ConstBytes blob);
   void ParseRssStat(int64_t ts,
                     uint32_t pid,
                     int64_t size,
@@ -46,7 +48,7 @@
                                             bool is_curr,
                                             uint32_t pid);
 
-  std::unordered_map<int64_t, UniqueTid> mm_id_to_utid_;
+  base::FlatHashMap<int64_t, UniqueTid> mm_id_to_utid_;
   std::vector<StringId> rss_members_;
   TraceProcessorContext* const context_;
 };
diff --git a/src/trace_processor/importers/ftrace/sched_event_tracker.cc b/src/trace_processor/importers/ftrace/sched_event_tracker.cc
index 2fc7fc3..00fc93f 100644
--- a/src/trace_processor/importers/ftrace/sched_event_tracker.cc
+++ b/src/trace_processor/importers/ftrace/sched_event_tracker.cc
@@ -36,7 +36,8 @@
 namespace trace_processor {
 
 SchedEventTracker::SchedEventTracker(TraceProcessorContext* context)
-    : context_(context) {
+    : waker_utid_id_(context->storage->InternString("waker_utid")),
+      context_(context) {
   // pre-parse sched_switch
   auto* switch_descriptor = GetMessageDescriptorForId(
       protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber);
@@ -318,7 +319,11 @@
   auto* instants = context_->storage->mutable_instant_table();
   auto ref_type_id = context_->storage->InternString(
       GetRefTypeStringMap()[static_cast<size_t>(RefType::kRefUtid)]);
-  instants->Insert({ts, sched_waking_id_, wakee_utid, ref_type_id});
+  tables::InstantTable::Id id =
+      instants->Insert({ts, sched_waking_id_, wakee_utid, ref_type_id}).id;
+
+  context_->args_tracker->AddArgsTo(id).AddArg(
+      waker_utid_id_, Variadic::UnsignedInteger(curr_utid));
 }
 
 void SchedEventTracker::FlushPendingEvents() {
diff --git a/src/trace_processor/importers/ftrace/sched_event_tracker.h b/src/trace_processor/importers/ftrace/sched_event_tracker.h
index eae4bac..eff442c 100644
--- a/src/trace_processor/importers/ftrace/sched_event_tracker.h
+++ b/src/trace_processor/importers/ftrace/sched_event_tracker.h
@@ -129,6 +129,8 @@
   std::array<StringId, kSchedWakingMaxFieldId + 1> sched_waking_field_ids_;
   StringId sched_waking_id_;
 
+  StringId waker_utid_id_;
+
   TraceProcessorContext* const context_;
 };
 
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
index d215b19..6628c41 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
@@ -17,7 +17,6 @@
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h"
 
 #include <cinttypes>
-#include <unordered_map>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/string_view.h"
diff --git a/src/trace_processor/importers/json/json_trace_parser.cc b/src/trace_processor/importers/json/json_trace_parser.cc
index cc9f76f..91cac7a 100644
--- a/src/trace_processor/importers/json/json_trace_parser.cc
+++ b/src/trace_processor/importers/json/json_trace_parser.cc
@@ -30,7 +30,6 @@
 #include "src/trace_processor/importers/common/process_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/json/json_tracker.h"
 #include "src/trace_processor/importers/json/json_utils.h"
 #include "src/trace_processor/tables/slice_tables.h"
 #include "src/trace_processor/types/trace_processor_context.h"
@@ -137,11 +136,9 @@
     row.track_id = track_id;
     row.category = cat_id;
     row.name = name_id;
-    row.thread_ts =
-        JsonTracker::GetOrCreate(context_)->CoerceToTs(value["tts"]);
+    row.thread_ts = json::CoerceToTs(value["tts"]);
     // tdur will only exist on 'X' events.
-    row.thread_dur =
-        JsonTracker::GetOrCreate(context_)->CoerceToTs(value["tdur"]);
+    row.thread_dur = json::CoerceToTs(value["tdur"]);
     // JSON traces don't report these counters as part of slices.
     row.thread_instruction_count = base::nullopt;
     row.thread_instruction_delta = base::nullopt;
@@ -161,8 +158,7 @@
       auto opt_slice_id = slice_tracker->End(timestamp, track_id, cat_id,
                                              name_id, args_inserter);
       // Now try to update thread_dur if we have a tts field.
-      auto opt_tts =
-          JsonTracker::GetOrCreate(context_)->CoerceToTs(value["tts"]);
+      auto opt_tts = json::CoerceToTs(value["tts"]);
       if (opt_slice_id.has_value() && opt_tts) {
         auto* thread_slice = storage->mutable_thread_slice_table();
         auto maybe_row = thread_slice->id().IndexOf(*opt_slice_id);
@@ -176,8 +172,7 @@
       break;
     }
     case 'X': {  // TRACE_EVENT (scoped event).
-      base::Optional<int64_t> opt_dur =
-          JsonTracker::GetOrCreate(context_)->CoerceToTs(value["dur"]);
+      base::Optional<int64_t> opt_dur = json::CoerceToTs(value["dur"]);
       if (!opt_dur.has_value())
         return;
       TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
diff --git a/src/trace_processor/importers/json/json_trace_parser.h b/src/trace_processor/importers/json/json_trace_parser.h
index 2084c4b..20d1e85 100644
--- a/src/trace_processor/importers/json/json_trace_parser.h
+++ b/src/trace_processor/importers/json/json_trace_parser.h
@@ -21,10 +21,8 @@
 
 #include <memory>
 #include <tuple>
-#include <unordered_map>
 
 #include "src/trace_processor/importers/common/trace_parser.h"
-#include "src/trace_processor/importers/json/json_tracker.h"
 #include "src/trace_processor/importers/systrace/systrace_line_parser.h"
 #include "src/trace_processor/timestamped_trace_piece.h"
 
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.cc b/src/trace_processor/importers/json/json_trace_tokenizer.cc
index bcfe02e..3677033 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.cc
@@ -22,7 +22,6 @@
 #include "perfetto/ext/base/string_utils.h"
 
 #include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/importers/json/json_tracker.h"
 #include "src/trace_processor/importers/json/json_utils.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/trace_sorter.h"
@@ -364,54 +363,6 @@
   return ReadSystemLineRes::kNeedsMoreData;
 }
 
-base::Optional<json::TimeUnit> MaybeParseDisplayTimeUnit(
-    base::StringView buffer) {
-  size_t idx = buffer.find("\"displayTimeUnit\"");
-  if (idx == base::StringView::npos)
-    return base::nullopt;
-
-  base::StringView time_unit = buffer.substr(idx);
-  std::string key;
-  const char* value_start = nullptr;
-  ReadKeyRes res =
-      ReadOneJsonKey(time_unit.data(), time_unit.end(), &key, &value_start);
-
-  // If we didn't read the key successfully, it could have been that
-  // displayTimeUnit is used somewhere else in the dict (maybe as a value?) so
-  // just ignore it.
-  if (res == ReadKeyRes::kEndOfDictionary || res == ReadKeyRes::kFatalError ||
-      res == ReadKeyRes::kNeedsMoreData) {
-    return base::nullopt;
-  }
-
-  // Double check that we did actually get the displayTimeUnit key.
-  if (key != "displayTimeUnit")
-    return base::nullopt;
-
-  // If the value isn't a string, we can do little except bail.
-  if (value_start[0] != '"')
-    return base::nullopt;
-
-  std::string value;
-  const char* unused;
-  ReadStringRes value_res =
-      ReadOneJsonString(value_start + 1, time_unit.end(), &value, &unused);
-
-  // If we didn't read the value successfully, we could be in another key
-  // in a nested dictionary. Just ignore the error.
-  if (value_res == ReadStringRes::kFatalError ||
-      value_res == ReadStringRes::kNeedsMoreData) {
-    return base::nullopt;
-  }
-
-  if (value == "ns") {
-    return json::TimeUnit::kNs;
-  } else if (value == "ms") {
-    return json::TimeUnit::kMs;
-  }
-  return base::nullopt;
-}
-
 JsonTraceTokenizer::JsonTraceTokenizer(TraceProcessorContext* ctx)
     : context_(ctx) {}
 JsonTraceTokenizer::~JsonTraceTokenizer() = default;
@@ -425,17 +376,6 @@
   const char* end = buf + buffer_.size();
 
   if (offset_ == 0) {
-    // It's possible the displayTimeUnit key is at the end of the json
-    // file so to be correct we ought to parse the whole file looking
-    // for this key before parsing any events however this would require
-    // two passes on the file so for now we only handle displayTimeUnit
-    // correctly if it is at the beginning of the file.
-    base::Optional<json::TimeUnit> timeunit =
-        MaybeParseDisplayTimeUnit(base::StringView(buf, blob.size()));
-    if (timeunit) {
-      JsonTracker::GetOrCreate(context_)->SetTimeUnit(*timeunit);
-    }
-
     // Strip leading whitespace.
     while (next != end && isspace(*next)) {
       next++;
@@ -481,7 +421,6 @@
                                                const char* end,
                                                const char** out) {
   PERFETTO_DCHECK(json::IsJsonSupported());
-  JsonTracker* json_tracker = JsonTracker::GetOrCreate(context_);
   auto* trace_sorter = context_->sorter.get();
 
   const char* next = start;
@@ -513,19 +452,10 @@
         return ParseInternal(next + 1, end, out);
       } else if (key == "displayTimeUnit") {
         std::string time_unit;
-        auto string_res = ReadOneJsonString(next + 1, end, &time_unit, &next);
-        if (string_res == ReadStringRes::kFatalError)
+        auto result = ReadOneJsonString(next + 1, end, &time_unit, &next);
+        if (result == ReadStringRes::kFatalError)
           return util::ErrStatus("Could not parse displayTimeUnit");
-        if (string_res == ReadStringRes::kNeedsMoreData)
-          return util::ErrStatus("displayTimeUnit too large");
-        if (time_unit != "ms" && time_unit != "ns")
-          return util::ErrStatus("displayTimeUnit unknown");
-        // If the displayTimeUnit came after the first chunk, the increment the
-        // too late stat.
-        if (offset_ != 0) {
-          context_->storage->IncrementStats(
-              stats::json_display_time_unit_too_late);
-        }
+        context_->storage->IncrementStats(stats::json_display_time_unit);
         return ParseInternal(next, end, out);
       } else if (key == "otherData") {
         base::StringView unparsed;
@@ -618,7 +548,7 @@
         base::Optional<std::string> opt_raw_ts;
         RETURN_IF_ERROR(ExtractValueForJsonKey(unparsed, "ts", &opt_raw_ts));
         base::Optional<int64_t> opt_ts =
-            opt_raw_ts ? json_tracker->CoerceToTs(*opt_raw_ts) : base::nullopt;
+            opt_raw_ts ? json::CoerceToTs(*opt_raw_ts) : base::nullopt;
         int64_t ts = 0;
         if (opt_ts.has_value()) {
           ts = opt_ts.value();
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.h b/src/trace_processor/importers/json/json_trace_tokenizer.h
index d25954d..8074b61 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.h
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.h
@@ -94,11 +94,6 @@
                                          std::string* line,
                                          const char** next);
 
-// Parses the "displayTimeUnit" key from the given trace buffer
-// and returns the associated time unit if one exists.
-base::Optional<json::TimeUnit> MaybeParseDisplayTimeUnit(
-    base::StringView buffer);
-
 // Reads a JSON trace in chunks and extracts top level json objects.
 class JsonTraceTokenizer : public ChunkedTraceReader {
  public:
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc b/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
index 6a11a44..051f52a 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
@@ -272,58 +272,6 @@
   ASSERT_EQ(*line, R"({"ts": 149029, "foo": "bar"})");
 }
 
-TEST(JsonTraceTokenizerTest, DisplayTimeUnit) {
-  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
-    {
-    }
-  )"),
-            base::nullopt);
-  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
-    {
-      "displayTimeUnit": 0
-    }
-  )"),
-            base::nullopt);
-  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
-    {
-      "str": "displayTimeUnit"
-    }
-  )"),
-            base::nullopt);
-
-  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
-    {
-      "traceEvents": [
-        {
-          "pid": 1, "tid": 1, "name": "test",
-          "ts": 1, "dur": 1000, "ph": "X", "cat": "fee"
-        }
-      ],
-      "displayTimeUnit": "ms"
-    }
-  )"),
-            json::TimeUnit::kMs);
-  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
-    {
-      "traceEvents": [
-        {
-          "pid": 1, "tid": 1, "name": "test",
-          "ts": 1, "dur": 1000, "ph": "X", "cat": "fee"
-        }
-      ],
-      "displayTimeUnit": "ns"
-    }
-  )"),
-            json::TimeUnit::kNs);
-
-  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
-    {
-      "displayTimeUnit":"ms"
-    }
-  )"),
-            json::TimeUnit::kMs);
-}
-
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/json/json_tracker.h b/src/trace_processor/importers/json/json_tracker.h
deleted file mode 100644
index 167881e..0000000
--- a/src/trace_processor/importers/json/json_tracker.h
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2020 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_JSON_JSON_TRACKER_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACKER_H_
-
-#include "src/trace_processor/importers/json/json_utils.h"
-#include "src/trace_processor/types/destructible.h"
-#include "src/trace_processor/types/trace_processor_context.h"
-
-namespace Json {
-class Value;
-}
-
-namespace perfetto {
-namespace trace_processor {
-
-class JsonTracker : public Destructible {
- public:
-  JsonTracker(const JsonTracker&) = delete;
-  JsonTracker& operator=(const JsonTracker&) = delete;
-  explicit JsonTracker(TraceProcessorContext*);
-  ~JsonTracker() override;
-
-  static JsonTracker* GetOrCreate(TraceProcessorContext* context) {
-    if (!context->json_tracker) {
-      context->json_tracker.reset(new JsonTracker(context));
-    }
-    return static_cast<JsonTracker*>(context->json_tracker.get());
-  }
-
-  void SetTimeUnit(json::TimeUnit time_unit) { time_unit_ = time_unit; }
-
-  base::Optional<int64_t> CoerceToTs(const Json::Value& value) {
-    return json::CoerceToTs(time_unit_, value);
-  }
-  base::Optional<int64_t> CoerceToTs(const std::string& value) {
-    return json::CoerceToTs(time_unit_, value);
-  }
-
- private:
-  json::TimeUnit time_unit_ = json::TimeUnit::kUs;
-};
-
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACKER_H_
diff --git a/src/trace_processor/importers/json/json_tracker_unittest.cc b/src/trace_processor/importers/json/json_tracker_unittest.cc
deleted file mode 100644
index 4ae6009..0000000
--- a/src/trace_processor/importers/json/json_tracker_unittest.cc
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2020 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/json/json_tracker.h"
-
-#include "test/gtest_and_gmock.h"
-
-#include <json/value.h>
-
-namespace perfetto {
-namespace trace_processor {
-namespace {
-
-TEST(JsonTrackerTest, Ns) {
-  JsonTracker tracker(nullptr);
-  tracker.SetTimeUnit(json::TimeUnit::kNs);
-  ASSERT_EQ(tracker.CoerceToTs(Json::Value(42)).value_or(-1), 42);
-}
-
-TEST(JsonTraceUtilsTest, Us) {
-  JsonTracker tracker(nullptr);
-  ASSERT_EQ(tracker.CoerceToTs(Json::Value(42)).value_or(-1), 42000);
-}
-
-}  // namespace
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/importers/json/json_utils.cc b/src/trace_processor/importers/json/json_utils.cc
index c44efa1..432ddbe 100644
--- a/src/trace_processor/importers/json/json_utils.cc
+++ b/src/trace_processor/importers/json/json_utils.cc
@@ -28,15 +28,6 @@
 namespace perfetto {
 namespace trace_processor {
 namespace json {
-namespace {
-
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
-int64_t TimeUnitToNs(TimeUnit unit) {
-  return static_cast<int64_t>(unit);
-}
-#endif
-
-}  // namespace
 
 bool IsJsonSupported() {
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
@@ -46,30 +37,28 @@
 #endif
 }
 
-base::Optional<int64_t> CoerceToTs(TimeUnit unit, const Json::Value& value) {
+base::Optional<int64_t> CoerceToTs(const Json::Value& value) {
   PERFETTO_DCHECK(IsJsonSupported());
 
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
   switch (static_cast<size_t>(value.type())) {
     case Json::realValue:
-      return static_cast<int64_t>(value.asDouble() *
-                                  static_cast<double>(TimeUnitToNs(unit)));
+      return static_cast<int64_t>(value.asDouble() * 1000.0);
     case Json::uintValue:
     case Json::intValue:
-      return value.asInt64() * TimeUnitToNs(unit);
+      return value.asInt64() * 1000;
     case Json::stringValue:
-      return CoerceToTs(unit, value.asString());
+      return CoerceToTs(value.asString());
     default:
       return base::nullopt;
   }
 #else
-  perfetto::base::ignore_result(unit);
   perfetto::base::ignore_result(value);
   return base::nullopt;
 #endif
 }
 
-base::Optional<int64_t> CoerceToTs(TimeUnit unit, const std::string& s) {
+base::Optional<int64_t> CoerceToTs(const std::string& s) {
   PERFETTO_DCHECK(IsJsonSupported());
 
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
@@ -82,11 +71,9 @@
       (!rhs.has_value() && rhs_start < s.size())) {
     return base::nullopt;
   }
-  int64_t factor = TimeUnitToNs(unit);
-  return lhs.value_or(0) * factor +
-         static_cast<int64_t>(rhs.value_or(0) * static_cast<double>(factor));
+  return lhs.value_or(0) * 1000 +
+         static_cast<int64_t>(rhs.value_or(0) * 1000.0);
 #else
-  perfetto::base::ignore_result(unit);
   perfetto::base::ignore_result(s);
   return base::nullopt;
 #endif
diff --git a/src/trace_processor/importers/json/json_utils.h b/src/trace_processor/importers/json/json_utils.h
index 74a2c3a..1cbcae8 100644
--- a/src/trace_processor/importers/json/json_utils.h
+++ b/src/trace_processor/importers/json/json_utils.h
@@ -40,9 +40,8 @@
 // build flags.
 bool IsJsonSupported();
 
-enum class TimeUnit { kNs = 1, kUs = 1000, kMs = 1000000 };
-base::Optional<int64_t> CoerceToTs(TimeUnit unit, const Json::Value& value);
-base::Optional<int64_t> CoerceToTs(TimeUnit unit, const std::string& value);
+base::Optional<int64_t> CoerceToTs(const Json::Value& value);
+base::Optional<int64_t> CoerceToTs(const std::string& value);
 base::Optional<int64_t> CoerceToInt64(const Json::Value& value);
 base::Optional<uint32_t> CoerceToUint32(const Json::Value& value);
 
diff --git a/src/trace_processor/importers/json/json_utils_unittest.cc b/src/trace_processor/importers/json/json_utils_unittest.cc
index a3846ea..7f01ad6 100644
--- a/src/trace_processor/importers/json/json_utils_unittest.cc
+++ b/src/trace_processor/importers/json/json_utils_unittest.cc
@@ -43,26 +43,17 @@
 }
 
 TEST(JsonTraceUtilsTest, CoerceToTs) {
-  ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value(42)).value_or(-1), 42000);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("42")).value_or(-1), 42000);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value(42.1)).value_or(-1), 42100);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("42.1")).value_or(-1), 42100);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value(".42")).value_or(-1), 420);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("42.")).value_or(-1), 42000);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("42.0")).value_or(-1), 42000);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("0.2")).value_or(-1), 200);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("0.2e-1")).value_or(-1), 20);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kNs, Json::Value(42)).value_or(-1), 42);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kMs, Json::Value(42)).value_or(-1), 42000000);
-  ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value(".")).value_or(-1), 0);
-  ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value("foo")).has_value());
-  ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value(".foo")).has_value());
-  ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value("0.foo")).has_value());
-  ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value("foo0.23")).has_value());
-  ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value("23.12foo")).has_value());
-  ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value("1234!")).has_value());
-  ASSERT_FALSE(CoerceToTs(TimeUnit::kUs, Json::Value("1234!")).has_value());
-  ASSERT_FALSE(CoerceToTs(TimeUnit::kMs, Json::Value("1234!")).has_value());
+  ASSERT_EQ(CoerceToTs(Json::Value(42)).value_or(-1), 42000);
+  ASSERT_EQ(CoerceToTs(Json::Value("42")).value_or(-1), 42000);
+  ASSERT_EQ(CoerceToTs(Json::Value(42.1)).value_or(-1), 42100);
+  ASSERT_EQ(CoerceToTs(Json::Value("42.1")).value_or(-1), 42100);
+  ASSERT_EQ(CoerceToTs(Json::Value(".42")).value_or(-1), 420);
+  ASSERT_EQ(CoerceToTs(Json::Value("42.")).value_or(-1), 42000);
+  ASSERT_EQ(CoerceToTs(Json::Value("42.0")).value_or(-1), 42000);
+  ASSERT_EQ(CoerceToTs(Json::Value("0.2")).value_or(-1), 200);
+  ASSERT_EQ(CoerceToTs(Json::Value("0.2e-1")).value_or(-1), 20);
+  ASSERT_EQ(CoerceToTs(Json::Value(".")).value_or(-1), 0);
+  ASSERT_FALSE(CoerceToTs(Json::Value("1234!")).has_value());
 }
 
 }  // namespace
diff --git a/src/trace_processor/importers/memory_tracker/graph_processor_unittest.cc b/src/trace_processor/importers/memory_tracker/graph_processor_unittest.cc
index 8d1cc31..4275b36 100644
--- a/src/trace_processor/importers/memory_tracker/graph_processor_unittest.cc
+++ b/src/trace_processor/importers/memory_tracker/graph_processor_unittest.cc
@@ -18,8 +18,6 @@
 
 #include <stddef.h>
 
-#include <unordered_map>
-
 #include "perfetto/base/build_config.h"
 #include "test/gtest_and_gmock.h"
 
diff --git a/src/trace_processor/importers/memory_tracker/raw_process_memory_node_unittest.cc b/src/trace_processor/importers/memory_tracker/raw_process_memory_node_unittest.cc
index 91a27ec..653dfe0 100644
--- a/src/trace_processor/importers/memory_tracker/raw_process_memory_node_unittest.cc
+++ b/src/trace_processor/importers/memory_tracker/raw_process_memory_node_unittest.cc
@@ -18,8 +18,6 @@
 
 #include <stddef.h>
 
-#include <unordered_map>
-
 #include "perfetto/base/build_config.h"
 #include "test/gtest_and_gmock.h"
 
diff --git a/src/trace_processor/importers/proto/async_track_set_tracker.h b/src/trace_processor/importers/proto/async_track_set_tracker.h
index b3dd836..6e7fa81 100644
--- a/src/trace_processor/importers/proto/async_track_set_tracker.h
+++ b/src/trace_processor/importers/proto/async_track_set_tracker.h
@@ -17,8 +17,6 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_ASYNC_TRACK_SET_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_ASYNC_TRACK_SET_TRACKER_H_
 
-#include <unordered_map>
-
 #include "src/trace_processor/storage/trace_storage.h"
 
 namespace perfetto {
diff --git a/src/trace_processor/importers/proto/chrome_string_lookup.cc b/src/trace_processor/importers/proto/chrome_string_lookup.cc
index 7858dde..7de67f0 100644
--- a/src/trace_processor/importers/proto/chrome_string_lookup.cc
+++ b/src/trace_processor/importers/proto/chrome_string_lookup.cc
@@ -167,6 +167,7 @@
     {ChromeThreadDescriptor::THREAD_NETWORKCONFIGWATCHER,
      "NetworkConfigWatcher"},
     {ChromeThreadDescriptor::THREAD_WASAPI_RENDER, "wasapi_render_thread"},
+    {ChromeThreadDescriptor::THREAD_LOADER_LOCK_SAMPLER, "LoaderLockSampler"},
 };
 
 }  // namespace
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.cc b/src/trace_processor/importers/proto/heap_graph_tracker.cc
index 062bd1f..f34377f 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.cc
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.cc
@@ -855,7 +855,7 @@
     size_t parent_id;  // id of parent node in the result tree.
     size_t i;          // Index of the next child of this node to handle.
     uint32_t depth;    // Depth in the resulting tree
-                       // (including artifical root).
+                       // (including artificial root).
     std::vector<tables::HeapGraphObjectTable::Id> children;
   };
 
@@ -912,9 +912,31 @@
           GetChildren(*storage, n);
       children.assign(children_set.cbegin(), children_set.cend());
       PERFETTO_CHECK(children.size() == children_set.size());
+
+      if (storage->heap_graph_object_table().native_size()[row]) {
+        StringPool::Id native_class_name_id = storage->InternString(
+            base::StringView(std::string("[native] ") +
+                             storage->GetString(class_name_id).ToStdString()));
+        std::map<StringId, size_t>::iterator native_it;
+        bool inserted_new_node;
+        std::tie(native_it, inserted_new_node) =
+            path->nodes[path_id].children.insert({native_class_name_id, 0});
+        if (inserted_new_node) {
+          native_it->second = path->nodes.size();
+          path->nodes.emplace_back(PathFromRoot::Node{});
+
+          path->nodes.back().class_name_id = native_class_name_id;
+          path->nodes.back().depth = depth + 1;
+          path->nodes.back().parent_id = path_id;
+        }
+        PathFromRoot::Node* new_output_tree_node = &path->nodes[native_it->second];
+
+        new_output_tree_node->size += storage->heap_graph_object_table().native_size()[row];
+        new_output_tree_node->count++;
+      }
     }
-    // Otherwise we have already handled this node and just need to get its
-    // i-th child.
+
+    // We have already handled this node and just need to get its i-th child.
     if (!children.empty()) {
       PERFETTO_CHECK(i < children.size());
       tables::HeapGraphObjectTable::Id child = children[i];
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 2af9a04..136c5de 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -1120,9 +1120,14 @@
     }
 
     TrackEventArgsParser args_writer(*inserter, *storage_, *sequence_state_);
+    int unknown_extensions = 0;
     log_errors(parser_->args_parser_.ParseMessage(
         blob_, ".perfetto.protos.TrackEvent", &parser_->reflect_fields_,
-        args_writer));
+        args_writer, &unknown_extensions));
+    if (unknown_extensions > 0) {
+      context_->storage->IncrementStats(stats::unknown_extension_fields,
+                                        unknown_extensions);
+    }
 
     {
       auto key = parser_->args_parser_.EnterDictionary("debug");
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.cc b/src/trace_processor/importers/systrace/systrace_line_parser.cc
index 15f6827..9120e65 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.cc
@@ -16,6 +16,7 @@
 
 #include "src/trace_processor/importers/systrace/systrace_line_parser.h"
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
@@ -31,7 +32,6 @@
 #include <cctype>
 #include <cinttypes>
 #include <string>
-#include <unordered_map>
 
 namespace perfetto {
 namespace trace_processor {
@@ -40,11 +40,13 @@
     : context_(ctx),
       rss_stat_tracker_(context_),
       sched_wakeup_name_id_(ctx->storage->InternString("sched_wakeup")),
+      sched_waking_name_id_(ctx->storage->InternString("sched_waking")),
       cpuidle_name_id_(ctx->storage->InternString("cpuidle")),
       workqueue_name_id_(ctx->storage->InternString("workqueue")),
       sched_blocked_reason_id_(
           ctx->storage->InternString("sched_blocked_reason")),
-      io_wait_id_(ctx->storage->InternString("io_wait")) {}
+      io_wait_id_(ctx->storage->InternString("io_wait")),
+      waker_utid_id_(ctx->storage->InternString("waker_utid")) {}
 
 util::Status SystraceLineParser::ParseLine(const SystraceLine& line) {
   auto utid = context_->process_tracker->UpdateThreadName(
@@ -58,14 +60,14 @@
     }
   }
 
-  std::unordered_map<std::string, std::string> args;
+  base::FlatHashMap<std::string, std::string> args;
   for (base::StringSplitter ss(line.args_str, ' '); ss.Next();) {
     std::string key;
     std::string value;
     if (!base::Contains(ss.cur_token(), "=")) {
       key = "name";
       value = ss.cur_token();
-      args.emplace(std::move(key), std::move(value));
+      args.Insert(std::move(key), std::move(value));
       continue;
     }
     for (base::StringSplitter inner(ss.cur_token(), '='); inner.Next();) {
@@ -75,7 +77,7 @@
         value = inner.cur_token();
       }
     }
-    args.emplace(std::move(key), std::move(value));
+    args.Insert(std::move(key), std::move(value));
   }
   if (line.event_name == "sched_switch") {
     auto prev_state_str = args["prev_state"];
@@ -101,7 +103,8 @@
              line.event_name == "0" || line.event_name == "print") {
     SystraceParser::GetOrCreate(context_)->ParsePrintEvent(
         line.ts, line.pid, line.args_str.c_str());
-  } else if (line.event_name == "sched_wakeup") {
+  } else if (line.event_name == "sched_wakeup" ||
+             line.event_name == "sched_waking") {
     auto comm = args["comm"];
     base::Optional<uint32_t> wakee_pid = base::StringToUInt32(args["pid"]);
     if (!wakee_pid.has_value()) {
@@ -111,8 +114,15 @@
     StringId name_id = context_->storage->InternString(base::StringView(comm));
     auto wakee_utid = context_->process_tracker->UpdateThreadName(
         wakee_pid.value(), name_id, ThreadNamePriority::kFtrace);
-    context_->event_tracker->PushInstant(line.ts, sched_wakeup_name_id_,
-                                         wakee_utid, RefType::kRefUtid);
+
+    StringId event_name_id = line.event_name == "sched_wakeup"
+                                 ? sched_wakeup_name_id_
+                                 : sched_waking_name_id_;
+    InstantId instant_id = context_->event_tracker->PushInstant(
+        line.ts, event_name_id, wakee_utid, RefType::kRefUtid);
+    context_->args_tracker->AddArgsTo(instant_id)
+        .AddArg(waker_utid_id_, Variadic::UnsignedInteger(utid));
+
   } else if (line.event_name == "cpu_idle") {
     base::Optional<uint32_t> event_cpu = base::StringToUInt32(args["cpu_id"]);
     base::Optional<double> new_state = base::StringToDouble(args["state"]);
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.h b/src/trace_processor/importers/systrace/systrace_line_parser.h
index 071ee31..24331fe 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.h
@@ -36,11 +36,14 @@
  private:
   TraceProcessorContext* const context_;
   RssStatTracker rss_stat_tracker_;
+
   const StringId sched_wakeup_name_id_ = kNullStringId;
+  const StringId sched_waking_name_id_ = kNullStringId;
   const StringId cpuidle_name_id_ = kNullStringId;
   const StringId workqueue_name_id_ = kNullStringId;
   const StringId sched_blocked_reason_id_ = kNullStringId;
   const StringId io_wait_id_ = kNullStringId;
+  const StringId waker_utid_id_ = kNullStringId;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/systrace/systrace_trace_parser.cc b/src/trace_processor/importers/systrace/systrace_trace_parser.cc
index 445dda1..0fe8781 100644
--- a/src/trace_processor/importers/systrace/systrace_trace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_trace_parser.cc
@@ -19,6 +19,7 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
+#include "src/trace_processor/forwarding_trace_parser.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/trace_sorter.h"
 
@@ -71,6 +72,18 @@
                       blob.data() + blob.size());
 
   if (state_ == ParseState::kBeforeParse) {
+    // Remove anything before the TRACE:\n marker, which is emitted when
+    // obtaining traces via  `adb shell "atrace -t 1 sched" > out.txt`.
+    std::array<uint8_t, 7> kAtraceMarker = {'T', 'R', 'A', 'C', 'E', ':', '\n'};
+    auto search_end = partial_buf_.begin() +
+                      static_cast<int>(std::min(partial_buf_.size(),
+                                                kGuessTraceMaxLookahead));
+    auto it = std::search(partial_buf_.begin(), search_end,
+                          kAtraceMarker.begin(), kAtraceMarker.end());
+    if (it != search_end)
+      partial_buf_.erase(partial_buf_.begin(), it + kAtraceMarker.size());
+
+    // Deal with HTML traces.
     state_ = partial_buf_[0] == '<' ? ParseState::kHtmlBeforeSystrace
                                     : ParseState::kSystrace;
   }
diff --git a/src/trace_processor/metrics/sql/android/android_multiuser_populator.sql b/src/trace_processor/metrics/sql/android/android_multiuser_populator.sql
index 98a4b16..c1726ff 100644
--- a/src/trace_processor/metrics/sql/android/android_multiuser_populator.sql
+++ b/src/trace_processor/metrics/sql/android/android_multiuser_populator.sql
@@ -14,6 +14,9 @@
 -- limitations under the License.
 --
 
+-- Create the base tables and views containing the launch spans.
+SELECT RUN_METRIC('android/startup/launches.sql');
+
 -- Collect the important timestamps for Multiuser events.
 DROP VIEW IF EXISTS multiuser_events;
 CREATE VIEW multiuser_events AS
@@ -31,9 +34,9 @@
     )
   ),
   (
-    SELECT slice.ts + slice.dur AS launcher_end_time_ns
-    FROM slice
-    WHERE (slice.name = "launching: com.google.android.apps.nexuslauncher")
+    SELECT ts_end AS launcher_end_time_ns
+    FROM launches
+    WHERE (package = 'com.android.launcher3' OR package = 'com.google.android.apps.nexuslauncher')
   ),
   (
     SELECT MIN(slice.ts) AS user_create_time_ns
diff --git a/src/trace_processor/metrics/sql/android/java_heap_stats.sql b/src/trace_processor/metrics/sql/android/java_heap_stats.sql
index dfafbbd..9f22a8a 100644
--- a/src/trace_processor/metrics/sql/android/java_heap_stats.sql
+++ b/src/trace_processor/metrics/sql/android/java_heap_stats.sql
@@ -26,9 +26,11 @@
     upid,
     graph_sample_ts,
     SUM(self_size) AS total_size,
+    SUM(native_size) AS total_native_size,
     COUNT(1) AS total_obj_count,
-    SUM(CASE reachable WHEN TRUE THEN self_size ELSE 0 END) AS reachable_size,
-    SUM(CASE reachable WHEN TRUE THEN 1 ELSE 0 END) AS reachable_obj_count
+    SUM(IIF(reachable, self_size, 0)) AS reachable_size,
+    SUM(IIF(reachable, native_size, 0)) AS reachable_native_size,
+    SUM(IIF(reachable, 1, 0)) AS reachable_obj_count
   FROM heap_graph_object
   GROUP BY 1, 2
 ),
@@ -92,8 +94,10 @@
     RepeatedField(JavaHeapStats_Sample(
       'ts', graph_sample_ts,
       'heap_size', total_size,
+      'heap_native_size', total_native_size,
       'obj_count', total_obj_count,
       'reachable_heap_size', reachable_size,
+      'reachable_heap_native_size', reachable_native_size,
       'reachable_obj_count', reachable_obj_count,
       'roots', roots,
       'anon_rss_and_swap_size', closest_anon_swap.val
diff --git a/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql b/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql
index cd1b809..595491d 100644
--- a/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql
+++ b/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql
@@ -46,14 +46,28 @@
 ON dropped_frames_with_upid.upid = process.upid;
 
 -- Create the derived event track for dropped frames.
+-- All tracks generated from chrome_dropped_frames_event are
+-- placed under a track group named 'Dropped Frames', whose summary
+-- track is the first track ('All Processes') in chrome_dropped_frames_event.
 DROP VIEW IF EXISTS chrome_dropped_frames_event;
 CREATE VIEW chrome_dropped_frames_event AS
 SELECT
   'slice' AS track_type,
-  'Dropped Frames' AS track_name,
+  'All Processes' AS track_name,
   ts,
   0 AS dur,
-  'Dropped Frame' AS slice_name
+  'Dropped Frame' AS slice_name,
+  'Dropped Frames' AS group_name
+FROM dropped_frames_with_process_info
+GROUP BY ts
+UNION ALL
+SELECT
+  'slice' AS track_type,
+  process_name || ' ' || process_id AS track_name,
+  ts,
+  0 AS dur,
+  'Dropped Frame' AS slice_name,
+  'Dropped Frames' AS group_name
 FROM dropped_frames_with_process_info
 GROUP BY ts;
 
diff --git a/src/trace_processor/python/perfetto/trace_processor/__init__.py b/src/trace_processor/python/perfetto/trace_processor/__init__.py
index fbbffcd..7106a6c 100644
--- a/src/trace_processor/python/perfetto/trace_processor/__init__.py
+++ b/src/trace_processor/python/perfetto/trace_processor/__init__.py
@@ -13,5 +13,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from .api import TraceProcessor
-from .http import TraceProcessorHttp
\ No newline at end of file
+from .api import TraceProcessor, TraceProcessorException
+from .http import TraceProcessorHttp
diff --git a/src/trace_processor/python/perfetto/trace_processor/api.py b/src/trace_processor/python/perfetto/trace_processor/api.py
index 1bf499f..56d9fc9 100644
--- a/src/trace_processor/python/perfetto/trace_processor/api.py
+++ b/src/trace_processor/python/perfetto/trace_processor/api.py
@@ -135,7 +135,7 @@
 
       except ModuleNotFoundError:
         raise TraceProcessorException(
-            'The sufficient libraries are not installed')
+            'Python dependencies missing. Please pip3 install pandas numpy')
 
     def __len__(self):
       return self.__count
@@ -238,8 +238,13 @@
 
     return response.metatrace
 
-  # TODO(@aninditaghosh): Investigate context managers for
-  # cleaner usage
+  def __enter__(self):
+    return self
+
+  def __exit__(self, _, __, ___):
+    self.close()
+    return False
+
   def close(self):
     if hasattr(self, 'subprocess'):
       self.subprocess.kill()
diff --git a/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor b/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor
index 0eb8e34..167645f 100644
--- a/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor
+++ b/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor
index 1212bcd..c7b0668 100644
--- a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1 b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1
index ecdef65..23712a1 100644
--- a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1
+++ b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1
@@ -2,5 +2,5 @@
 // SHA1(tools/gen_binary_descriptors)
 // 9fc6d77de57ec76a80b76aa282f4c7cf5ce55eec
 // SHA1(protos/perfetto/trace_processor/trace_processor.proto)
-// 8ed3e45d389366979465140f6dcdbbb1356f323a
+// e303e1fc877a9fe4f8dd8413c03266ee68dfd3aa
   
\ No newline at end of file
diff --git a/src/trace_processor/rpc/BUILD.gn b/src/trace_processor/rpc/BUILD.gn
index 09b9061..7246ec1 100644
--- a/src/trace_processor/rpc/BUILD.gn
+++ b/src/trace_processor/rpc/BUILD.gn
@@ -66,7 +66,7 @@
       "../../../include/perfetto/trace_processor",
       "../../../protos/perfetto/trace_processor:zero",
       "../../base",
-      "../../base:unix_socket",
+      "../../base/http",
       "../../protozero",
     ]
   }
diff --git a/src/trace_processor/rpc/httpd.cc b/src/trace_processor/rpc/httpd.cc
index 654c566..72c0f12 100644
--- a/src/trace_processor/rpc/httpd.cc
+++ b/src/trace_processor/rpc/httpd.cc
@@ -20,14 +20,11 @@
 
 #include "src/trace_processor/rpc/httpd.h"
 
-#include <map>
-#include <string>
-
-#include "perfetto/ext/base/paged_memory.h"
+#include "perfetto/ext/base/http/http_server.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
-#include "perfetto/ext/base/unix_socket.h"
 #include "perfetto/ext/base/unix_task_runner.h"
+#include "perfetto/ext/base/utils.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
 #include "perfetto/trace_processor/trace_processor.h"
 #include "src/trace_processor/rpc/rpc.h"
@@ -39,8 +36,7 @@
 
 namespace {
 
-constexpr char kBindPort[] = "9001";
-constexpr size_t kOmitContentLength = static_cast<size_t>(-1);
+constexpr int kBindPort = 9001;
 
 // Sets the Access-Control-Allow-Origin: $origin on the following origins.
 // This affects only browser clients that use CORS. Other HTTP clients (e.g. the
@@ -51,350 +47,146 @@
     "http://127.0.0.1:10000",
 };
 
-// 32 MiB payload + 128K for HTTP headers.
-constexpr size_t kMaxRequestSize = (32 * 1024 + 128) * 1024;
-
-// Owns the socket and data for one HTTP client connection.
-struct Client {
-  Client(std::unique_ptr<base::UnixSocket> s)
-      : sock(std::move(s)),
-        rxbuf(base::PagedMemory::Allocate(kMaxRequestSize)) {}
-  size_t rxbuf_avail() { return rxbuf.size() - rxbuf_used; }
-
-  std::unique_ptr<base::UnixSocket> sock;
-  base::PagedMemory rxbuf;
-  size_t rxbuf_used = 0;
-};
-
-struct HttpRequest {
-  base::StringView method;
-  base::StringView uri;
-  base::StringView origin;
-  base::StringView body;
-  int id = 0;
-};
-
-class HttpServer : public base::UnixSocket::EventListener {
+class Httpd : public base::HttpRequestHandler {
  public:
-  explicit HttpServer(std::unique_ptr<TraceProcessor>);
-  ~HttpServer() override;
-  void Run(const char*, const char*);
-
-  // This is non-null only while serving an HTTP request.
-  Client* active_client() { return active_client_; }
+  explicit Httpd(std::unique_ptr<TraceProcessor>);
+  ~Httpd() override;
+  void Run(int port);
 
  private:
-  size_t ParseOneHttpRequest(Client* client);
-  void HandleRequest(Client*, const HttpRequest&);
-  void ServeHelpPage(Client*);
+  // HttpRequestHandler implementation.
+  void OnHttpRequest(const base::HttpRequest&) override;
+  void OnWebsocketMessage(const base::WebsocketMessage&) override;
 
-  void OnNewIncomingConnection(base::UnixSocket*,
-                               std::unique_ptr<base::UnixSocket>) override;
-  void OnConnect(base::UnixSocket* self, bool connected) override;
-  void OnDisconnect(base::UnixSocket* self) override;
-  void OnDataAvailable(base::UnixSocket* self) override;
+  void ServeHelpPage(const base::HttpRequest&);
 
   Rpc trace_processor_rpc_;
   base::UnixTaskRunner task_runner_;
-  std::unique_ptr<base::UnixSocket> sock4_;
-  std::unique_ptr<base::UnixSocket> sock6_;
-  std::list<Client> clients_;
-  Client* active_client_ = nullptr;
-  bool origin_error_logged_ = false;
+  base::HttpServer http_srv_;
 };
 
-HttpServer* g_httpd_instance;
+base::HttpServerConnection* g_cur_conn;
 
-void Append(std::vector<char>& buf, const char* str) {
-  buf.insert(buf.end(), str, str + strlen(str));
+base::StringView Vec2Sv(const std::vector<uint8_t>& v) {
+  return base::StringView(reinterpret_cast<const char*>(v.data()), v.size());
 }
 
-void Append(std::vector<char>& buf, const std::string& str) {
-  buf.insert(buf.end(), str.begin(), str.end());
-}
-
-void HttpReply(base::UnixSocket* sock,
-               const char* http_code,
-               std::initializer_list<const char*> headers = {},
-               const uint8_t* content = nullptr,
-               size_t content_length = 0) {
-  std::vector<char> response;
-  response.reserve(4096);
-  Append(response, "HTTP/1.1 ");
-  Append(response, http_code);
-  Append(response, "\r\n");
-  for (const char* hdr : headers) {
-    if (strlen(hdr) == 0)
-      continue;
-    Append(response, hdr);
-    Append(response, "\r\n");
+// Used both by websockets and /rpc chunked HTTP endpoints.
+void SendRpcChunk(const void* data, uint32_t len) {
+  if (data == nullptr) {
+    // Unrecoverable RPC error case.
+    if (!g_cur_conn->is_websocket())
+      g_cur_conn->SendResponseBody("0\r\n\r\n", 5);
+    g_cur_conn->Close();
+    return;
   }
-  if (content_length != kOmitContentLength) {
-    Append(response, "Content-Length: ");
-    Append(response, std::to_string(content_length));
-    Append(response, "\r\n");
+  if (g_cur_conn->is_websocket()) {
+    g_cur_conn->SendWebsocketMessage(data, len);
+  } else {
+    base::StackString<32> chunk_hdr("%x\r\n", len);
+    g_cur_conn->SendResponseBody(chunk_hdr.c_str(), chunk_hdr.len());
+    g_cur_conn->SendResponseBody(data, len);
+    g_cur_conn->SendResponseBody("\r\n", 2);
   }
-  Append(response, "\r\n");                      // End-of-headers marker.
-  sock->Send(response.data(), response.size());  // Send response headers.
-  if (content_length > 0 && content_length != kOmitContentLength)
-    sock->Send(content, content_length);  // Send response payload.
 }
 
-void ShutdownBadRequest(base::UnixSocket* sock, const char* reason) {
-  HttpReply(sock, "500 Bad Request", {},
-            reinterpret_cast<const uint8_t*>(reason), strlen(reason));
-  sock->Shutdown(/*notify=*/true);
-}
+Httpd::Httpd(std::unique_ptr<TraceProcessor> preloaded_instance)
+    : trace_processor_rpc_(std::move(preloaded_instance)),
+      http_srv_(&task_runner_, this) {}
+Httpd::~Httpd() = default;
 
-HttpServer::HttpServer(std::unique_ptr<TraceProcessor> preloaded_instance)
-    : trace_processor_rpc_(std::move(preloaded_instance)) {}
-HttpServer::~HttpServer() = default;
-
-void HttpServer::Run(const char* kBindAddr4, const char* kBindAddr6) {
-  PERFETTO_ILOG("[HTTP] Starting RPC server on %s and %s", kBindAddr4,
-                kBindAddr6);
+void Httpd::Run(int port) {
+  PERFETTO_ILOG("[HTTP] Starting RPC server on localhost:%d", port);
   PERFETTO_LOG(
       "[HTTP] This server can be used by reloading https://ui.perfetto.dev and "
       "clicking on YES on the \"Trace Processor native acceleration\" dialog "
       "or through the Python API (see "
       "https://perfetto.dev/docs/analysis/trace-processor#python-api).");
 
-  sock4_ = base::UnixSocket::Listen(kBindAddr4, this, &task_runner_,
-                                    base::SockFamily::kInet,
-                                    base::SockType::kStream);
-  bool ipv4_listening = sock4_ && sock4_->is_listening();
-  if (!ipv4_listening) {
-    PERFETTO_ILOG("Failed to listen on IPv4 socket");
-  }
-
-  sock6_ = base::UnixSocket::Listen(kBindAddr6, this, &task_runner_,
-                                    base::SockFamily::kInet6,
-                                    base::SockType::kStream);
-  bool ipv6_listening = sock6_ && sock6_->is_listening();
-  if (!ipv6_listening) {
-    PERFETTO_ILOG("Failed to listen on IPv6 socket");
-  }
-
-  PERFETTO_CHECK(ipv4_listening || ipv6_listening);
-
+  for (size_t i = 0; i < base::ArraySize(kAllowedCORSOrigins); ++i)
+    http_srv_.AddAllowedOrigin(kAllowedCORSOrigins[i]);
+  http_srv_.Start(port);
   task_runner_.Run();
 }
 
-void HttpServer::OnNewIncomingConnection(
-    base::UnixSocket*,
-    std::unique_ptr<base::UnixSocket> sock) {
-  PERFETTO_LOG("[HTTP] New connection");
-  clients_.emplace_back(std::move(sock));
-}
-
-void HttpServer::OnConnect(base::UnixSocket*, bool) {}
-
-void HttpServer::OnDisconnect(base::UnixSocket* sock) {
-  PERFETTO_LOG("[HTTP] Client disconnected");
-  for (auto it = clients_.begin(); it != clients_.end(); ++it) {
-    if (it->sock.get() == sock) {
-      clients_.erase(it);
-      return;
-    }
-  }
-  PERFETTO_DFATAL("[HTTP] untracked client in OnDisconnect()");
-}
-
-void HttpServer::OnDataAvailable(base::UnixSocket* sock) {
-  Client* client = nullptr;
-  for (auto it = clients_.begin(); it != clients_.end() && !client; ++it)
-    client = (it->sock.get() == sock) ? &*it : nullptr;
-  PERFETTO_CHECK(client);
-
-  char* rxbuf = reinterpret_cast<char*>(client->rxbuf.Get());
-  for (;;) {
-    size_t avail = client->rxbuf_avail();
-    PERFETTO_CHECK(avail <= kMaxRequestSize);
-    if (avail == 0)
-      return ShutdownBadRequest(sock, "Request body too big");
-    size_t rsize = sock->Receive(&rxbuf[client->rxbuf_used], avail);
-    client->rxbuf_used += rsize;
-    if (rsize == 0 || client->rxbuf_avail() == 0)
-      break;
-  }
-
-  // At this point |rxbuf| can contain a partial HTTP request, a full one or
-  // more (in case of HTTP Keepalive pipelining).
-  for (;;) {
-    active_client_ = client;
-    size_t bytes_consumed = ParseOneHttpRequest(client);
-    active_client_ = nullptr;
-    if (bytes_consumed == 0)
-      break;
-    memmove(rxbuf, &rxbuf[bytes_consumed], client->rxbuf_used - bytes_consumed);
-    client->rxbuf_used -= bytes_consumed;
-  }
-}
-
-// Parses the HTTP request and invokes HandleRequest(). It returns the size of
-// the HTTP header + body that has been processed or 0 if there isn't enough
-// data for a full HTTP request in the buffer.
-size_t HttpServer::ParseOneHttpRequest(Client* client) {
-  auto* rxbuf = reinterpret_cast<char*>(client->rxbuf.Get());
-  base::StringView buf_view(rxbuf, client->rxbuf_used);
-  size_t pos = 0;
-  size_t body_offset = 0;
-  size_t body_size = 0;
-  bool has_parsed_first_line = false;
-  HttpRequest http_req;
-
-  // This loop parses the HTTP request headers and sets the |body_offset|.
-  for (;;) {
-    size_t next = buf_view.find("\r\n", pos);
-    size_t col;
-    if (next == std::string::npos)
-      break;
-
-    if (!has_parsed_first_line) {
-      // Parse the "GET /xxx HTTP/1.1" line.
-      has_parsed_first_line = true;
-      size_t space = buf_view.find(' ');
-      if (space == std::string::npos || space + 2 >= client->rxbuf_used) {
-        ShutdownBadRequest(client->sock.get(), "Malformed HTTP request");
-        return 0;
-      }
-      http_req.method = buf_view.substr(0, space);
-      size_t uri_size = buf_view.find(' ', space + 1) - space - 1;
-      http_req.uri = buf_view.substr(space + 1, uri_size);
-    } else if (next == pos) {
-      // The CR-LF marker that separates headers from body.
-      body_offset = next + 2;
-      break;
-    } else if ((col = buf_view.find(':', pos)) < next) {
-      // Parse HTTP headers. They look like: "Content-Length: 1234".
-      auto hdr_name = buf_view.substr(pos, col - pos);
-      auto hdr_value = buf_view.substr(col + 2, next - col - 2);
-      if (hdr_name.CaseInsensitiveEq("content-length")) {
-        body_size = static_cast<size_t>(atoi(hdr_value.ToStdString().c_str()));
-      } else if (hdr_name.CaseInsensitiveEq("origin")) {
-        http_req.origin = hdr_value;
-      } else if (hdr_name.CaseInsensitiveEq("x-seq-id")) {
-        http_req.id = atoi(hdr_value.ToStdString().c_str());
-      }
-    }
-    pos = next + 2;
-  }
-
-  // If we have a full header but not yet the full body, return and try again
-  // next time we receive some more data.
-  size_t http_req_size = body_offset + body_size;
-  if (!body_offset || client->rxbuf_used < http_req_size)
-    return 0;
-
-  http_req.body = base::StringView(&rxbuf[body_offset], body_size);
-  HandleRequest(client, http_req);
-  return http_req_size;
-}
-
-void HttpServer::HandleRequest(Client* client, const HttpRequest& req) {
+void Httpd::OnHttpRequest(const base::HttpRequest& req) {
+  base::HttpServerConnection& conn = *req.conn;
   if (req.uri == "/") {
     // If a user tries to open http://127.0.0.1:9001/ show a minimal help page.
-    return ServeHelpPage(client);
+    return ServeHelpPage(req);
   }
 
   static int last_req_id = 0;
-  if (req.id) {
-    if (last_req_id && req.id != last_req_id + 1 && req.id != 1)
+  auto seq_hdr = req.GetHeader("x-seq-id").value_or(base::StringView());
+  int seq_id = base::StringToInt32(seq_hdr.ToStdString()).value_or(0);
+
+  if (seq_id) {
+    if (last_req_id && seq_id != last_req_id + 1 && seq_id != 1)
       PERFETTO_ELOG("HTTP Request out of order");
-    last_req_id = req.id;
-  }
-
-  PERFETTO_LOG("[HTTP] %04d %s %s (body: %zu bytes).", req.id,
-               req.method.ToStdString().c_str(), req.uri.ToStdString().c_str(),
-               req.body.size());
-
-  std::string allow_origin_hdr;
-  for (const char* allowed_origin : kAllowedCORSOrigins) {
-    if (req.origin != base::StringView(allowed_origin))
-      continue;
-    allow_origin_hdr =
-        "Access-Control-Allow-Origin: " + req.origin.ToStdString();
-    break;
-  }
-  if (allow_origin_hdr.empty() && !origin_error_logged_) {
-    origin_error_logged_ = true;
-    PERFETTO_ELOG(
-        "The HTTP origin \"%s\" is not trusted, no Access-Control-Allow-Origin "
-        "will be emitted. If this request comes from a browser it will fail. "
-        "For the list of allowed origins see kAllowedCORSOrigins.",
-        req.origin.ToStdString().c_str());
+    last_req_id = seq_id;
   }
 
   // This is the default. Overridden by the /query handler for chunked replies.
   char transfer_encoding_hdr[255] = "Transfer-Encoding: identity";
   std::initializer_list<const char*> headers = {
-      "Connection: Keep-Alive",                //
       "Cache-Control: no-cache",               //
-      "Keep-Alive: timeout=5, max=1000",       //
       "Content-Type: application/x-protobuf",  //
-      "Vary: Origin",                          //
       transfer_encoding_hdr,                   //
-      allow_origin_hdr.c_str(),
   };
 
-  if (req.method == "OPTIONS") {
-    // CORS headers.
-    return HttpReply(client->sock.get(), "204 No Content",
-                     {
-                         "Access-Control-Allow-Methods: POST, GET, OPTIONS",
-                         "Access-Control-Allow-Headers: *",
-                         "Access-Control-Max-Age: 86400",
-                         "Vary: Origin",
-                         allow_origin_hdr.c_str(),
-                     });
+  if (req.uri == "/status") {
+    auto status = trace_processor_rpc_.GetStatus();
+    return conn.SendResponse("200 OK", headers, Vec2Sv(status));
   }
 
+  if (req.uri == "/websocket" && req.is_websocket_handshake) {
+    // Will trigger OnWebsocketMessage() when is received.
+    // It returns a 403 if the origin is not in kAllowedCORSOrigins.
+    return conn.UpgradeToWebsocket(req);
+  }
+
+  // --- Everything below this line is a legacy endpoint not used by the UI.
+  // There are two generations of pre-websocket legacy-ness:
+  // 1. The /rpc based endpoint. This is based on a chunked transfer, doing one
+  //    POST request for each RPC invocation. All RPC methods are multiplexed
+  //    into this one. This is still used by the python API.
+  // 2. The REST API, with one enpoint per RPC method (/parse, /query, ...).
+  //    This is unused and will be removed at some point.
+
   if (req.uri == "/rpc") {
     // Start the chunked reply.
     base::StringCopy(transfer_encoding_hdr, "Transfer-Encoding: chunked",
                      sizeof(transfer_encoding_hdr));
-    base::UnixSocket* cli_sock = client->sock.get();
-    HttpReply(cli_sock, "200 OK", headers, nullptr, kOmitContentLength);
-
-    static auto resp_fn = [](const void* data, uint32_t len) {
-      char chunk_hdr[32];
-      auto hdr_len = static_cast<size_t>(sprintf(chunk_hdr, "%x\r\n", len));
-      auto* http_client = g_httpd_instance->active_client();
-      PERFETTO_CHECK(http_client);
-      if (data == nullptr) {
-        // Unrecoverable RPC error case.
-        http_client->sock->Send("0\r\n\r\n", 5);
-        http_client->sock->Shutdown(/*notify=*/true);
-        return;
-      }
-      http_client->sock->Send(chunk_hdr, hdr_len);
-      http_client->sock->Send(data, len);
-      http_client->sock->Send("\r\n", 2);
-    };
-
-    trace_processor_rpc_.SetRpcResponseFunction(resp_fn);
+    conn.SendResponseHeaders("200 OK", headers,
+                             base::HttpServerConnection::kOmitContentLength);
+    PERFETTO_CHECK(g_cur_conn == nullptr);
+    g_cur_conn = req.conn;
+    trace_processor_rpc_.SetRpcResponseFunction(SendRpcChunk);
+    // OnRpcRequest() will call SendRpcChunk() one or more times.
     trace_processor_rpc_.OnRpcRequest(req.body.data(), req.body.size());
     trace_processor_rpc_.SetRpcResponseFunction(nullptr);
+    g_cur_conn = nullptr;
 
     // Terminate chunked stream.
-    cli_sock->Send("0\r\n\r\n", 5);
+    conn.SendResponseBody("0\r\n\r\n", 5);
+    conn.Close();
     return;
   }
 
   if (req.uri == "/parse") {
     trace_processor_rpc_.Parse(
         reinterpret_cast<const uint8_t*>(req.body.data()), req.body.size());
-    return HttpReply(client->sock.get(), "200 OK", headers);
+    return conn.SendResponse("200 OK", headers);
   }
 
   if (req.uri == "/notify_eof") {
     trace_processor_rpc_.NotifyEndOfFile();
-    return HttpReply(client->sock.get(), "200 OK", headers);
+    return conn.SendResponse("200 OK", headers);
   }
 
   if (req.uri == "/restore_initial_tables") {
     trace_processor_rpc_.RestoreInitialTables();
-    return HttpReply(client->sock.get(), "200 OK", headers);
+    return conn.SendResponse("200 OK", headers);
   }
 
   // New endpoint, returns data in batches using chunked transfer encoding.
@@ -407,8 +199,8 @@
     // Start the chunked reply.
     base::StringCopy(transfer_encoding_hdr, "Transfer-Encoding: chunked",
                      sizeof(transfer_encoding_hdr));
-    base::UnixSocket* cli_sock = client->sock.get();
-    HttpReply(cli_sock, "200 OK", headers, nullptr, kOmitContentLength);
+    conn.SendResponseHeaders("200 OK", headers,
+                             base::HttpServerConnection::kOmitContentLength);
 
     // |on_result_chunk| will be called nested within the same callstack of the
     // rpc.Query() call. No further calls will be made once Query() returns.
@@ -416,12 +208,12 @@
       PERFETTO_DLOG("Sending response chunk, len=%zu eof=%d", len, !has_more);
       char chunk_hdr[32];
       auto hdr_len = static_cast<size_t>(sprintf(chunk_hdr, "%zx\r\n", len));
-      cli_sock->Send(chunk_hdr, hdr_len);
-      cli_sock->Send(buf, len);
-      cli_sock->Send("\r\n", 2);
+      conn.SendResponseBody(chunk_hdr, hdr_len);
+      conn.SendResponseBody(buf, len);
+      conn.SendResponseBody("\r\n", 2);
       if (!has_more) {
         hdr_len = static_cast<size_t>(sprintf(chunk_hdr, "0\r\n\r\n"));
-        cli_sock->Send(chunk_hdr, hdr_len);
+        conn.SendResponseBody(chunk_hdr, hdr_len);
       }
     };
     trace_processor_rpc_.Query(
@@ -436,50 +228,49 @@
   if (req.uri == "/raw_query") {
     std::vector<uint8_t> response = trace_processor_rpc_.RawQuery(
         reinterpret_cast<const uint8_t*>(req.body.data()), req.body.size());
-    return HttpReply(client->sock.get(), "200 OK", headers, response.data(),
-                     response.size());
-  }
-
-  if (req.uri == "/status") {
-    auto status = trace_processor_rpc_.GetStatus();
-    return HttpReply(client->sock.get(), "200 OK", headers, status.data(),
-                     status.size());
+    return conn.SendResponse("200 OK", headers, Vec2Sv(response));
   }
 
   if (req.uri == "/compute_metric") {
     std::vector<uint8_t> res = trace_processor_rpc_.ComputeMetric(
         reinterpret_cast<const uint8_t*>(req.body.data()), req.body.size());
-    return HttpReply(client->sock.get(), "200 OK", headers, res.data(),
-                     res.size());
+    return conn.SendResponse("200 OK", headers, Vec2Sv(res));
   }
 
   if (req.uri == "/enable_metatrace") {
     trace_processor_rpc_.EnableMetatrace();
-    return HttpReply(client->sock.get(), "200 OK", headers);
+    return conn.SendResponse("200 OK", headers);
   }
 
   if (req.uri == "/disable_and_read_metatrace") {
     std::vector<uint8_t> res = trace_processor_rpc_.DisableAndReadMetatrace();
-    return HttpReply(client->sock.get(), "200 OK", headers, res.data(),
-                     res.size());
+    return conn.SendResponse("200 OK", headers, Vec2Sv(res));
   }
 
-  return HttpReply(client->sock.get(), "404 Not Found", headers);
+  return conn.SendResponseAndClose("404 Not Found", headers);
+}
+
+void Httpd::OnWebsocketMessage(const base::WebsocketMessage& msg) {
+  PERFETTO_CHECK(g_cur_conn == nullptr);
+  g_cur_conn = msg.conn;
+  trace_processor_rpc_.SetRpcResponseFunction(SendRpcChunk);
+  // OnRpcRequest() will call SendRpcChunk() one or more times.
+  trace_processor_rpc_.OnRpcRequest(msg.data.data(), msg.data.size());
+  trace_processor_rpc_.SetRpcResponseFunction(nullptr);
+  g_cur_conn = nullptr;
 }
 
 }  // namespace
 
 void RunHttpRPCServer(std::unique_ptr<TraceProcessor> preloaded_instance,
                       std::string port_number) {
-  HttpServer srv(std::move(preloaded_instance));
-  g_httpd_instance = &srv;
-  std::string port = port_number.empty() ? kBindPort : port_number;
-  std::string ipv4_addr = "127.0.0.1:" + port;
-  std::string ipv6_addr = "[::1]:" + port;
-  srv.Run(ipv4_addr.c_str(), ipv6_addr.c_str());
+  Httpd srv(std::move(preloaded_instance));
+  base::Optional<int> port_opt = base::StringToInt32(port_number);
+  int port = port_opt.has_value() ? *port_opt : kBindPort;
+  srv.Run(port);
 }
 
-void HttpServer::ServeHelpPage(Client* client) {
+void Httpd::ServeHelpPage(const base::HttpRequest& req) {
   static const char kPage[] = R"(Perfetto Trace Processor RPC Server
 
 
@@ -503,12 +294,8 @@
 https://perfetto.dev/docs/contributing/getting-started#community
 )";
 
-  char content_length[255];
-  sprintf(content_length, "Content-Length: %zu", sizeof(kPage) - 1);
-  std::initializer_list<const char*> headers{"Content-Type: text/plain",
-                                             content_length};
-  HttpReply(client->sock.get(), "200 OK", headers,
-            reinterpret_cast<const uint8_t*>(kPage), sizeof(kPage) - 1);
+  std::initializer_list<const char*> headers{"Content-Type: text/plain"};
+  req.conn->SendResponse("200 OK", headers, kPage);
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/sqlite/span_join_operator_table.h b/src/trace_processor/sqlite/span_join_operator_table.h
index 6fba96d..a6256f8 100644
--- a/src/trace_processor/sqlite/span_join_operator_table.h
+++ b/src/trace_processor/sqlite/span_join_operator_table.h
@@ -25,9 +25,9 @@
 #include <map>
 #include <memory>
 #include <string>
-#include <unordered_map>
 #include <vector>
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/status.h"
 #include "src/trace_processor/sqlite/scoped_db.h"
@@ -428,7 +428,7 @@
   TableDefinition t1_defn_;
   TableDefinition t2_defn_;
   PartitioningType partitioning_;
-  std::unordered_map<size_t, ColumnLocator> global_index_to_column_locator_;
+  base::FlatHashMap<size_t, ColumnLocator> global_index_to_column_locator_;
 
   sqlite3* const db_;
 };
diff --git a/src/trace_processor/sqlite/sqlite_raw_table.h b/src/trace_processor/sqlite/sqlite_raw_table.h
index ce1b9ac..a3ec5c2 100644
--- a/src/trace_processor/sqlite/sqlite_raw_table.h
+++ b/src/trace_processor/sqlite/sqlite_raw_table.h
@@ -18,6 +18,7 @@
 #define SRC_TRACE_PROCESSOR_SQLITE_SQLITE_RAW_TABLE_H_
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/string_writer.h"
 #include "src/trace_processor/sqlite/db_sqlite_table.h"
 #include "src/trace_processor/storage/trace_storage.h"
@@ -31,13 +32,13 @@
  public:
   using ScopedCString = std::unique_ptr<char, void (*)(void*)>;
 
-  SystraceSerializer(TraceProcessorContext* context);
+  explicit SystraceSerializer(TraceProcessorContext* context);
 
   ScopedCString SerializeToString(uint32_t raw_row);
 
  private:
   using StringIdMap =
-      std::unordered_map<StringId, std::vector<base::Optional<uint32_t>>>;
+      base::FlatHashMap<StringId, std::vector<base::Optional<uint32_t>>>;
 
   void SerializePrefix(uint32_t raw_row, base::StringWriter* writer);
 
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index ea71ef0..7e537ca 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -124,10 +124,11 @@
   F(process_tracker_errors,             kSingle,  kError,    kAnalysis, ""),   \
   F(json_tokenizer_failure,             kSingle,  kError,    kTrace,    ""),   \
   F(json_parser_failure,                kSingle,  kError,    kTrace,    ""),   \
-  F(json_display_time_unit_too_late,    kSingle,  kError,    kTrace,           \
-      "The displayTimeUnit key came too late in the JSON trace so was "        \
-      "ignored. Trace processor only supports displayTimeUnit appearing "      \
-      "at the start of JSON traces"),                                          \
+  F(json_display_time_unit,             kSingle,  kInfo,     kTrace,           \
+      "The displayTimeUnit key was set in the JSON trace. In some prior "      \
+      "versions of trace processor this key could effect how the trace "       \
+      "processor parsed timestamps and durations. In this version the key is " \
+      "ignored which more closely matches the bavahiour of catapult."),        \
   F(heap_graph_invalid_string_id,       kIndexed, kError,    kTrace,    ""),   \
   F(heap_graph_non_finalized_graph,     kSingle,  kError,    kTrace,    ""),   \
   F(heap_graph_malformed_packet,        kIndexed, kError,    kTrace,    ""),   \
@@ -187,9 +188,13 @@
       "processor."),                                                           \
   F(perf_guardrail_stop_ts,             kIndexed, kDataLoss, kTrace,    ""),   \
   F(sorter_push_event_out_of_order,     kSingle, kError,     kTrace,           \
-       "Trace events are out of order event after sorting. This can happen "   \
-       "due to many factors including clock sync drift, producers emitting "   \
-       "events out of order or a bug in trace processor's logic of sorting.")
+      "Trace events are out of order event after sorting. This can happen "    \
+      "due to many factors including clock sync drift, producers emitting "    \
+      "events out of order or a bug in trace processor's logic of sorting."),  \
+  F(unknown_extension_fields,           kSingle,  kError,    kTrace,           \
+      "TraceEvent had unknown extension fields, which might result in "        \
+      "missing some arguments. You may need a newer version of trace "         \
+      "processor to parse them.")
 // clang-format on
 
 enum Type {
diff --git a/src/trace_processor/trace_database_integrationtest.cc b/src/trace_processor/trace_database_integrationtest.cc
index c6c8095..6a59ca6 100644
--- a/src/trace_processor/trace_database_integrationtest.cc
+++ b/src/trace_processor/trace_database_integrationtest.cc
@@ -58,9 +58,24 @@
   ASSERT_EQ(it.Get(0).long_value, 1);
 }
 
+TEST(TraceProcessorCustomConfigTest, EmptyStringSkipsAllMetrics) {
+  auto config = Config();
+  config.skip_builtin_metric_paths = {""};
+  auto processor = TraceProcessor::CreateInstance(config);
+  processor->NotifyEndOfFile();
+
+  // Check that other metrics have been loaded.
+  auto it = processor->ExecuteQuery(
+      "select count(*) from trace_metrics "
+      "where name = 'trace_metadata';");
+  ASSERT_TRUE(it.Next());
+  ASSERT_EQ(it.Get(0).type, SqlValue::kLong);
+  ASSERT_EQ(it.Get(0).long_value, 0);
+}
+
 TEST(TraceProcessorCustomConfigTest, HandlesMalformedMountPath) {
   auto config = Config();
-  config.skip_builtin_metric_paths = {"", "androi"};
+  config.skip_builtin_metric_paths = {"androi"};
   auto processor = TraceProcessor::CreateInstance(config);
   processor->NotifyEndOfFile();
 
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index ca3725d..2976e2c 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -687,10 +687,17 @@
   tp->ExtendMetricsProto(kAllChromeMetricsDescriptor.data(),
                          kAllChromeMetricsDescriptor.size(), skip_prefixes);
 
-  for (const auto& file_to_sql : metrics::sql_metrics::kFileToSql) {
-    if (base::StartsWithAny(file_to_sql.path, sanitized_extension_paths))
-      continue;
-    tp->RegisterMetric(file_to_sql.path, file_to_sql.sql);
+  // TODO(lalitm): remove this special casing and change
+  // SanitizeMetricMountPaths if/when we move all protos for builtin metrics to
+  // match extension protos.
+  bool skip_all_sql = std::find(extension_paths.begin(), extension_paths.end(),
+                                "") != extension_paths.end();
+  if (!skip_all_sql) {
+    for (const auto& file_to_sql : metrics::sql_metrics::kFileToSql) {
+      if (base::StartsWithAny(file_to_sql.path, sanitized_extension_paths))
+        continue;
+      tp->RegisterMetric(file_to_sql.path, file_to_sql.sql);
+    }
   }
 
   RegisterFunction<metrics::NullIfEmpty>(db, "NULL_IF_EMPTY", 1);
@@ -1054,6 +1061,23 @@
   if (IsRootMetricField(no_ext_name)) {
     metric.proto_field_name = no_ext_name;
     metric.output_table_name = no_ext_name + "_output";
+
+    auto field_it_and_inserted =
+        proto_field_to_sql_metric_path_.emplace(*metric.proto_field_name, path);
+    if (!field_it_and_inserted.second) {
+      // We already had a metric with this field name in the map. However, if
+      // this was the case, we should have found the metric in
+      // |path_to_sql_metric_file_| above if we are simply overriding the
+      // metric. Return an error since this means we have two different SQL
+      // files which are trying to output the same metric.
+      const auto& prev_path = field_it_and_inserted.first->second;
+      PERFETTO_DCHECK(prev_path != path);
+      return base::ErrStatus(
+          "RegisterMetric Error: Metric paths %s (which is already registered) "
+          "and %s are both trying to output the proto field %s",
+          prev_path.c_str(), path.c_str(), metric.proto_field_name->c_str());
+    }
+
     InsertIntoTraceMetricsTable(*db_, no_ext_name);
   }
 
diff --git a/src/trace_processor/trace_processor_impl.h b/src/trace_processor/trace_processor_impl.h
index 90a4393..33ec127 100644
--- a/src/trace_processor/trace_processor_impl.h
+++ b/src/trace_processor/trace_processor_impl.h
@@ -125,6 +125,7 @@
 
   DescriptorPool pool_;
   std::vector<metrics::SqlMetricFile> sql_metrics_;
+  std::unordered_map<std::string, std::string> proto_field_to_sql_metric_path_;
 
   // This is atomic because it is set by the CTRL-C signal handler and we need
   // to prevent single-flow compiler optimizations in ExecuteQuery().
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index 3fe3486..1eede6f 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -342,7 +342,6 @@
   base::ReadFile(register_metric, &sql);
 
   std::string path = "shell/" + BaseName(register_metric);
-
   return g_tp->RegisterMetric(path, sql);
 }
 
@@ -387,9 +386,19 @@
   kNone,
 };
 
-util::Status RunMetrics(const std::vector<std::string>& metric_names,
+struct MetricNameAndPath {
+  std::string name;
+  base::Optional<std::string> no_ext_path;
+};
+
+util::Status RunMetrics(const std::vector<MetricNameAndPath>& metrics,
                         OutputFormat format,
                         const google::protobuf::DescriptorPool& pool) {
+  std::vector<std::string> metric_names(metrics.size());
+  for (size_t i = 0; i < metrics.size(); ++i) {
+    metric_names[i] = metrics[i].name;
+  }
+
   if (format == OutputFormat::kTextProto) {
     std::string out;
     util::Status status =
@@ -416,10 +425,10 @@
       google::protobuf::DynamicMessageFactory factory(&pool);
       auto* descriptor =
           pool.FindMessageTypeByName("perfetto.protos.TraceMetrics");
-      std::unique_ptr<google::protobuf::Message> metrics(
+      std::unique_ptr<google::protobuf::Message> metric_msg(
           factory.GetPrototype(descriptor)->New());
-      metrics->ParseFromArray(metric_result.data(),
-                              static_cast<int>(metric_result.size()));
+      metric_msg->ParseFromArray(metric_result.data(),
+                                 static_cast<int>(metric_result.size()));
 
       // We need to instantiate field options from dynamic message factory
       // because otherwise it cannot parse our custom extensions.
@@ -427,7 +436,7 @@
           factory.GetPrototype(
               pool.FindMessageTypeByName("google.protobuf.FieldOptions"));
       auto out = proto_to_json::MessageToJsonWithAnnotations(
-          *metrics, field_options_prototype, 0);
+          *metric_msg, field_options_prototype, 0);
       fwrite(out.c_str(), sizeof(char), out.size(), stdout);
       break;
     }
@@ -708,6 +717,7 @@
   bool wide = false;
   bool force_full_sort = false;
   std::string metatrace_path;
+  bool dev = false;
 };
 
 void PrintUsage(char** argv) {
@@ -759,7 +769,12 @@
                                       Loads metric proto and sql files from
                                       DISK_PATH/protos and DISK_PATH/sql
                                       respectively, and mounts them onto
-                                      VIRTUAL_PATH.)",
+                                      VIRTUAL_PATH.
+ --dev                                Enables features which are reserved for
+                                      local development use only and
+                                      *should not* be enabled on production
+                                      builds. The features behind this flag can
+                                      break at any time without any warning.)",
                 argv[0]);
 }
 
@@ -772,6 +787,7 @@
     OPT_FORCE_FULL_SORT,
     OPT_HTTP_PORT,
     OPT_METRIC_EXTENSION,
+    OPT_DEV,
   };
 
   static const option long_options[] = {
@@ -791,6 +807,7 @@
       {"full-sort", no_argument, nullptr, OPT_FORCE_FULL_SORT},
       {"http-port", required_argument, nullptr, OPT_HTTP_PORT},
       {"metric-extension", required_argument, nullptr, OPT_METRIC_EXTENSION},
+      {"dev", no_argument, nullptr, OPT_DEV},
       {nullptr, 0, nullptr, 0}};
 
   bool explicit_interactive = false;
@@ -882,6 +899,11 @@
       continue;
     }
 
+    if (option == OPT_DEV) {
+      command_line_options.dev = true;
+      continue;
+    }
+
     PrintUsage(argv);
     exit(option == 'h' ? 0 : 1);
   }
@@ -995,7 +1017,8 @@
   return util::OkStatus();
 }
 
-base::Status ParseSingleMetricExtensionPath(const std::string& raw_extension,
+base::Status ParseSingleMetricExtensionPath(bool dev,
+                                            const std::string& raw_extension,
                                             MetricExtension& parsed_extension) {
   // We cannot easily use ':' as a path separator because windows paths can have
   // ':' in them (e.g. C:\foo\bar).
@@ -1008,6 +1031,15 @@
   parsed_extension.SetDiskPath(std::move(parts[0]));
   parsed_extension.SetVirtualPath(std::move(parts[1]));
 
+  if (parsed_extension.virtual_path() == "/") {
+    if (!dev) {
+      return base::ErrStatus(
+          "Local development features must be enabled (using the "
+          "--dev flag) to override built-in metrics");
+    }
+    parsed_extension.SetVirtualPath("");
+  }
+
   if (parsed_extension.virtual_path() == "shell/") {
     return base::Status(
         "Cannot have 'shell/' as metric extension virtual path.");
@@ -1037,11 +1069,12 @@
 }
 
 base::Status ParseMetricExtensionPaths(
+    bool dev,
     const std::vector<std::string>& raw_metric_extensions,
     std::vector<MetricExtension>& metric_extensions) {
   for (const auto& raw_extension : raw_metric_extensions) {
     metric_extensions.push_back({});
-    RETURN_IF_ERROR(ParseSingleMetricExtensionPath(raw_extension,
+    RETURN_IF_ERROR(ParseSingleMetricExtensionPath(dev, raw_extension,
                                                    metric_extensions.back()));
   }
   return CheckForDuplicateMetricExtension(metric_extensions);
@@ -1123,13 +1156,9 @@
   return base::OkStatus();
 }
 
-util::Status RunMetrics(const CommandLineOptions& options,
-                        std::vector<MetricExtension>& metric_extensions) {
-  // Descriptor pool used for printing output as textproto. Building on top of
-  // generated pool so default protos in google.protobuf.descriptor.proto are
-  // available.
-  google::protobuf::DescriptorPool pool(
-      google::protobuf::DescriptorPool::generated_pool());
+util::Status PopulateDescriptorPool(
+    google::protobuf::DescriptorPool& pool,
+    const std::vector<MetricExtension>& metric_extensions) {
   // TODO(b/182165266): There is code duplication here with trace_processor_impl
   // SetupMetrics. This will be removed when we switch the output formatter to
   // use internal DescriptorPool.
@@ -1143,65 +1172,100 @@
   ExtendPoolWithBinaryDescriptor(pool, kAllChromeMetricsDescriptor.data(),
                                  kAllChromeMetricsDescriptor.size(),
                                  skip_prefixes);
+  return base::OkStatus();
+}
 
-  std::vector<std::string> metrics;
-  for (base::StringSplitter ss(options.metric_names, ','); ss.Next();) {
-    metrics.emplace_back(ss.cur_token());
+util::Status LoadMetrics(const std::string& raw_metric_names,
+                         google::protobuf::DescriptorPool& pool,
+                         std::vector<MetricNameAndPath>& name_and_path) {
+  std::vector<std::string> split;
+  for (base::StringSplitter ss(raw_metric_names, ','); ss.Next();) {
+    split.emplace_back(ss.cur_token());
   }
 
   // For all metrics which are files, register them and extend the metrics
   // proto.
-  for (size_t i = 0; i < metrics.size(); ++i) {
-    const std::string& metric_or_path = metrics[i];
-
+  for (const std::string& metric_or_path : split) {
     // If there is no extension, we assume it is a builtin metric.
     auto ext_idx = metric_or_path.rfind('.');
-    if (ext_idx == std::string::npos)
+    if (ext_idx == std::string::npos) {
+      name_and_path.emplace_back(
+          MetricNameAndPath{metric_or_path, base::nullopt});
       continue;
+    }
 
-    std::string no_ext_name = metric_or_path.substr(0, ext_idx);
+    std::string no_ext_path = metric_or_path.substr(0, ext_idx);
 
     // The proto must be extended before registering the metric.
-    util::Status status = ExtendMetricsProto(no_ext_name + ".proto", &pool);
+    util::Status status = ExtendMetricsProto(no_ext_path + ".proto", &pool);
     if (!status.ok()) {
       return util::ErrStatus("Unable to extend metrics proto %s: %s",
                              metric_or_path.c_str(), status.c_message());
     }
 
-    status = RegisterMetric(no_ext_name + ".sql");
+    status = RegisterMetric(no_ext_path + ".sql");
     if (!status.ok()) {
       return util::ErrStatus("Unable to register metric %s: %s",
                              metric_or_path.c_str(), status.c_message());
     }
+    name_and_path.emplace_back(
+        MetricNameAndPath{BaseName(no_ext_path), no_ext_path});
+  }
+  return base::OkStatus();
+}
 
-    metrics[i] = BaseName(no_ext_name);
+OutputFormat ParseOutputFormat(const CommandLineOptions& options) {
+  if (!options.query_file_path.empty())
+    return OutputFormat::kNone;
+  if (options.metric_output == "binary")
+    return OutputFormat::kBinaryProto;
+  if (options.metric_output == "json")
+    return OutputFormat::kJson;
+  return OutputFormat::kTextProto;
+}
+
+base::Status LoadMetricsAndExtensionsSql(
+    const std::vector<MetricNameAndPath>& metrics,
+    const std::vector<MetricExtension>& extensions) {
+  for (const MetricExtension& extension : extensions) {
+    const std::string& disk_path = extension.disk_path();
+    const std::string& virtual_path = extension.virtual_path();
+
+    RETURN_IF_ERROR(LoadMetricExtensionSql(disk_path + "sql/", virtual_path));
   }
 
-  OutputFormat format;
-  if (!options.query_file_path.empty()) {
-    format = OutputFormat::kNone;
-  } else if (options.metric_output == "binary") {
-    format = OutputFormat::kBinaryProto;
-  } else if (options.metric_output == "json") {
-    format = OutputFormat::kJson;
-  } else {
-    format = OutputFormat::kTextProto;
+  for (const MetricNameAndPath& metric : metrics) {
+    // Ignore builtin metrics.
+    if (!metric.no_ext_path.has_value())
+      continue;
+    RETURN_IF_ERROR(RegisterMetric(metric.no_ext_path.value() + ".sql"));
   }
-
-  return RunMetrics(std::move(metrics), format, pool);
+  return base::OkStatus();
 }
 
 void PrintShellUsage() {
   PERFETTO_ELOG(
       "Available commands:\n"
-      ".quit, .q    Exit the shell.\n"
-      ".help        This text.\n"
-      ".dump FILE   Export the trace as a sqlite database.\n"
-      ".read FILE   Executes the queries in the FILE.\n"
-      ".reset       Destroys all tables/view created by the user.\n");
+      ".quit, .q         Exit the shell.\n"
+      ".help             This text.\n"
+      ".dump FILE        Export the trace as a sqlite database.\n"
+      ".read FILE        Executes the queries in the FILE.\n"
+      ".reset            Destroys all tables/view created by the user.\n"
+      ".load-metrics-sql Reloads SQL from extension and custom metric paths\n"
+      "                  specified in command line args.\n"
+      ".run-metrics      Runs metrics specified in command line args\n"
+      "                  and prints the result.\n");
 }
 
-util::Status StartInteractiveShell(uint32_t column_width) {
+struct InteractiveOptions {
+  uint32_t column_width;
+  OutputFormat metric_format;
+  std::vector<MetricExtension> extensions;
+  std::vector<MetricNameAndPath> metrics;
+  const google::protobuf::DescriptorPool* pool;
+};
+
+util::Status StartInteractiveShell(const InteractiveOptions& options) {
   SetupLineEditor();
 
   for (;;) {
@@ -1230,6 +1294,23 @@
         if (!status.ok()) {
           PERFETTO_ELOG("%s", status.c_message());
         }
+      } else if (strcmp(command, "load-metrics-sql") == 0) {
+        base::Status status =
+            LoadMetricsAndExtensionsSql(options.metrics, options.extensions);
+        if (!status.ok()) {
+          PERFETTO_ELOG("%s", status.c_message());
+        }
+      } else if (strcmp(command, "run-metrics") == 0) {
+        if (options.metrics.empty()) {
+          PERFETTO_ELOG("No metrics specified on command line");
+          continue;
+        }
+
+        base::Status status =
+            RunMetrics(options.metrics, options.metric_format, *options.pool);
+        if (!status.ok()) {
+          PERFETTO_ELOG("%s", status.c_message());
+        }
       } else {
         PrintShellUsage();
       }
@@ -1238,7 +1319,7 @@
 
     base::TimeNanos t_start = base::GetWallTimeNs();
     auto it = g_tp->ExecuteQuery(line.get());
-    PrintQueryResultInteractively(&it, t_start, column_width);
+    PrintQueryResultInteractively(&it, t_start, options.column_width);
   }
   return util::OkStatus();
 }
@@ -1252,8 +1333,8 @@
                             : SortingMode::kDefaultHeuristics;
 
   std::vector<MetricExtension> metric_extensions;
-  RETURN_IF_ERROR(ParseMetricExtensionPaths(options.raw_metric_extensions,
-                                            metric_extensions));
+  RETURN_IF_ERROR(ParseMetricExtensionPaths(
+      options.dev, options.raw_metric_extensions, metric_extensions));
 
   for (const auto& extension : metric_extensions) {
     config.skip_builtin_metric_paths.push_back(extension.virtual_path());
@@ -1282,8 +1363,8 @@
     t_load = base::GetWallTimeNs() - t_load_start;
 
     double t_load_s = static_cast<double>(t_load.count()) / 1E9;
-    PERFETTO_ILOG("Trace loaded: %.2f MB (%.1f MB/s)", size_mb,
-                  size_mb / t_load_s);
+    PERFETTO_ILOG("Trace loaded: %.2f MB in %.2fs (%.1f MB/s)", size_mb,
+                  t_load_s, size_mb / t_load_s);
 
     RETURN_IF_ERROR(PrintStats());
   }
@@ -1304,8 +1385,23 @@
     RETURN_IF_ERROR(RunQueries(options.pre_metrics_path, false));
   }
 
+  // Descriptor pool used for printing output as textproto. Building on top of
+  // generated pool so default protos in google.protobuf.descriptor.proto are
+  // available.
+  // For some insane reason, the descriptor pool is not movable so we need to
+  // create it here so we can create references and pass it everywhere.
+  google::protobuf::DescriptorPool pool(
+      google::protobuf::DescriptorPool::generated_pool());
+  RETURN_IF_ERROR(PopulateDescriptorPool(pool, metric_extensions));
+
+  std::vector<MetricNameAndPath> metrics;
   if (!options.metric_names.empty()) {
-    RETURN_IF_ERROR(RunMetrics(options, metric_extensions));
+    RETURN_IF_ERROR(LoadMetrics(options.metric_names, pool, metrics));
+  }
+
+  OutputFormat metric_format = ParseOutputFormat(options);
+  if (!metrics.empty()) {
+    RETURN_IF_ERROR(RunMetrics(metrics, metric_format, pool));
   }
 
   if (!options.query_file_path.empty()) {
@@ -1318,7 +1414,9 @@
   }
 
   if (options.launch_shell) {
-    RETURN_IF_ERROR(StartInteractiveShell(options.wide ? 40 : 20));
+    RETURN_IF_ERROR(StartInteractiveShell(
+        InteractiveOptions{options.wide ? 40u : 20u, metric_format,
+                           metric_extensions, metrics, &pool}));
   } else if (!options.perf_file_path.empty()) {
     RETURN_IF_ERROR(PrintPerfFile(options.perf_file_path, t_load, t_query));
   }
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index 1cf723c..4bc0e2b 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -48,7 +48,6 @@
 class TraceSorter;
 class TraceStorage;
 class TrackTracker;
-class JsonTracker;
 class DescriptorPool;
 
 class TraceProcessorContext {
@@ -92,7 +91,6 @@
   std::unique_ptr<Destructible> binder_tracker;          // BinderTracker
   std::unique_ptr<Destructible> systrace_parser;         // SystraceParser
   std::unique_ptr<Destructible> heap_graph_tracker;      // HeapGraphTracker
-  std::unique_ptr<Destructible> json_tracker;            // JsonTracker
   std::unique_ptr<Destructible> system_info_tracker;     // SystemInfoTracker
 
   // These fields are trace readers which will be called by |forwarding_parser|
diff --git a/src/trace_processor/util/interned_message_view.h b/src/trace_processor/util/interned_message_view.h
index 4f5b993..3422b8b 100644
--- a/src/trace_processor/util/interned_message_view.h
+++ b/src/trace_processor/util/interned_message_view.h
@@ -17,10 +17,9 @@
 #ifndef SRC_TRACE_PROCESSOR_UTIL_INTERNED_MESSAGE_VIEW_H_
 #define SRC_TRACE_PROCESSOR_UTIL_INTERNED_MESSAGE_VIEW_H_
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 
-#include <unordered_map>
-
 namespace perfetto {
 namespace trace_processor {
 
@@ -49,7 +48,7 @@
     this->message_ = view.message_.copy();
     this->decoder_ = nullptr;
     this->decoder_type_ = nullptr;
-    this->submessages_.clear();
+    this->submessages_.Clear();
     return *this;
   }
 
@@ -87,9 +86,9 @@
   // TODO(eseckler): Support repeated fields.
   template <typename MessageType, uint32_t FieldId>
   InternedMessageView* GetOrCreateSubmessageView() {
-    auto it = submessages_.find(FieldId);
-    if (it != submessages_.end())
-      return it->second.get();
+    auto it_and_ins = submessages_.Insert(FieldId, nullptr);
+    if (!it_and_ins.second)
+      return it_and_ins.first->get();
     auto* decoder = GetOrCreateDecoder<MessageType>();
     // Calls the at() template method on the decoder.
     auto field = decoder->template at<FieldId>().as_bytes();
@@ -98,8 +97,7 @@
     TraceBlobView submessage = message_.slice(field.data, field.size);
     InternedMessageView* submessage_view =
         new InternedMessageView(std::move(submessage));
-    submessages_.emplace_hint(
-        it, FieldId, std::unique_ptr<InternedMessageView>(submessage_view));
+    it_and_ins.first->reset(submessage_view);
     return submessage_view;
   }
 
@@ -107,8 +105,8 @@
 
  private:
   using SubMessageViewMap =
-      std::unordered_map<uint32_t /*field_id*/,
-                         std::unique_ptr<InternedMessageView>>;
+      base::FlatHashMap<uint32_t /*field_id*/,
+                        std::unique_ptr<InternedMessageView>>;
 
   TraceBlobView message_;
 
diff --git a/src/trace_processor/util/proto_to_args_parser.cc b/src/trace_processor/util/proto_to_args_parser.cc
index a17b5dc..8e3e8d1 100644
--- a/src/trace_processor/util/proto_to_args_parser.cc
+++ b/src/trace_processor/util/proto_to_args_parser.cc
@@ -79,9 +79,11 @@
     const protozero::ConstBytes& cb,
     const std::string& type,
     const std::vector<uint16_t>* allowed_fields,
-    Delegate& delegate) {
+    Delegate& delegate,
+    int* unknown_extensions) {
   ScopedNestedKeyContext key_context(key_prefix_);
-  return ParseMessageInternal(key_context, cb, type, allowed_fields, delegate);
+  return ParseMessageInternal(key_context, cb, type, allowed_fields, delegate,
+                              unknown_extensions);
 }
 
 base::Status ProtoToArgsParser::ParseMessageInternal(
@@ -89,7 +91,8 @@
     const protozero::ConstBytes& cb,
     const std::string& type,
     const std::vector<uint16_t>* allowed_fields,
-    Delegate& delegate) {
+    Delegate& delegate,
+    int* unknown_extensions) {
   if (auto override_result =
           MaybeApplyOverrideForType(type, key_context, cb, delegate)) {
     return override_result.value();
@@ -112,6 +115,9 @@
     empty_message = false;
     auto field = descriptor.FindFieldByTag(f.id());
     if (!field) {
+      if (unknown_extensions != nullptr) {
+        (*unknown_extensions)++;
+      }
       // Unknown field, possibly an unknown extension.
       continue;
     }
@@ -127,8 +133,8 @@
       // reflected.
       continue;
     }
-    RETURN_IF_ERROR(
-        ParseField(*field, repeated_field_index[f.id()], f, delegate));
+    RETURN_IF_ERROR(ParseField(*field, repeated_field_index[f.id()], f,
+                               delegate, unknown_extensions));
     if (field->is_repeated()) {
       repeated_field_index[f.id()]++;
     }
@@ -145,7 +151,8 @@
     const FieldDescriptor& field_descriptor,
     int repeated_field_number,
     protozero::Field field,
-    Delegate& delegate) {
+    Delegate& delegate,
+    int* unknown_extensions) {
   std::string prefix_part = field_descriptor.name();
   if (field_descriptor.is_repeated()) {
     std::string number = std::to_string(repeated_field_number);
@@ -176,7 +183,7 @@
       protos::pbzero::FieldDescriptorProto::TYPE_MESSAGE) {
     return ParseMessageInternal(key_context, field.as_bytes(),
                                 field_descriptor.resolved_type_name(), nullptr,
-                                delegate);
+                                delegate, unknown_extensions);
   }
 
   return ParseSimpleField(field_descriptor, field, delegate);
diff --git a/src/trace_processor/util/proto_to_args_parser.h b/src/trace_processor/util/proto_to_args_parser.h
index 2ec4f1c..2329edb 100644
--- a/src/trace_processor/util/proto_to_args_parser.h
+++ b/src/trace_processor/util/proto_to_args_parser.h
@@ -136,7 +136,8 @@
   base::Status ParseMessage(const protozero::ConstBytes& cb,
                             const std::string& type,
                             const std::vector<uint16_t>* allowed_fields,
-                            Delegate& delegate);
+                            Delegate& delegate,
+                            int* unknown_extensions = nullptr);
 
   // This class is responsible for resetting the current key prefix to the old
   // value when deleted or reset.
@@ -237,7 +238,8 @@
   base::Status ParseField(const FieldDescriptor& field_descriptor,
                           int repeated_field_number,
                           protozero::Field field,
-                          Delegate& delegate);
+                          Delegate& delegate,
+                          int* unknown_extensions);
 
   base::Optional<base::Status> MaybeApplyOverrideForField(
       const protozero::Field&,
@@ -255,7 +257,8 @@
                                     const protozero::ConstBytes& cb,
                                     const std::string& type,
                                     const std::vector<uint16_t>* fields,
-                                    Delegate& delegate);
+                                    Delegate& delegate,
+                                    int* unknown_extensions);
 
   base::Status ParseSimpleField(const FieldDescriptor& desciptor,
                                 const protozero::Field& field,
diff --git a/src/tracebox/BUILD.gn b/src/tracebox/BUILD.gn
index 647ad5e..9000732 100644
--- a/src/tracebox/BUILD.gn
+++ b/src/tracebox/BUILD.gn
@@ -24,6 +24,7 @@
     "../perfetto_cmd:trigger_perfetto_cmd",
     "../traced/probes",
     "../traced/service",
+    "../websocket_bridge:lib",
   ]
   sources = [ "tracebox.cc" ]
 }
diff --git a/src/tracebox/tracebox.cc b/src/tracebox/tracebox.cc
index ed75b31..a421b97 100644
--- a/src/tracebox/tracebox.cc
+++ b/src/tracebox/tracebox.cc
@@ -20,6 +20,7 @@
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/traced/traced.h"
 #include "src/perfetto_cmd/perfetto_cmd.h"
+#include "src/websocket_bridge/websocket_bridge.h"
 
 #include <stdio.h>
 
@@ -39,6 +40,7 @@
     {"traced_probes", ProbesMain},
     {"perfetto", PerfettoCmdMain},
     {"trigger_perfetto", TriggerPerfettoMain},
+    {"websocket_bridge", WebsocketBridgeMain},
 };
 
 void PrintUsage() {
diff --git a/src/traced/probes/BUILD.gn b/src/traced/probes/BUILD.gn
index db547fc..4c4b65a 100644
--- a/src/traced/probes/BUILD.gn
+++ b/src/traced/probes/BUILD.gn
@@ -101,6 +101,7 @@
     "filesystem:unittests",
     "initial_display_state:unittests",
     "packages_list:unittests",
+    "power:unittests",
     "ps:unittests",
     "sys_stats:unittests",
     "system_info:unittests",
diff --git a/src/traced/probes/ftrace/cpu_reader.cc b/src/traced/probes/ftrace/cpu_reader.cc
index 10e9788..6e09c01 100644
--- a/src/traced/probes/ftrace/cpu_reader.cc
+++ b/src/traced/probes/ftrace/cpu_reader.cc
@@ -28,6 +28,7 @@
 #include "perfetto/ext/base/crash_keys.h"
 #include "perfetto/ext/base/metatrace.h"
 #include "perfetto/ext/base/optional.h"
+#include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/tracing/core/trace_writer.h"
@@ -141,6 +142,16 @@
   return fcntl(fd, F_SETFL, flags) == 0;
 }
 
+void LogInvalidPage(const void* start, size_t size) {
+  PERFETTO_ELOG("Invalid ftrace page");
+  std::string hexdump = base::HexDump(start, size);
+  // Only a single line per log message, because log message size might be
+  // limited.
+  for (base::StringSplitter ss(std::move(hexdump), '\n'); ss.Next();) {
+    PERFETTO_ELOG("%s", ss.cur_token());
+  }
+}
+
 }  // namespace
 
 using protos::pbzero::GenericFtraceEvent;
@@ -411,6 +422,7 @@
     if (!page_header.has_value() || page_header->size == 0 ||
         parse_pos >= curr_page_end ||
         parse_pos + page_header->size > curr_page_end) {
+      LogInvalidPage(curr_page, base::kPageSize);
       PERFETTO_DFATAL("invalid page header");
       return false;
     }
@@ -436,6 +448,7 @@
 
     if (evt_size != page_header->size) {
       pages_parsed_ok = false;
+      LogInvalidPage(curr_page, base::kPageSize);
       PERFETTO_DFATAL("could not parse ftrace page");
     }
   }
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index 9d6ac9d..dbb9364 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -6481,6 +6481,25 @@
        kUnsetFtraceId,
        40,
        kUnsetSize},
+      {"rss_stat_throttled",
+       "synthetic",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "curr", 1, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "member", 2, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "mm_id", 3, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "size", 4, ProtoSchemaType::kInt64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       359,
+       kUnsetSize},
       {"0",
        "systrace",
        {
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index a19564f..4ebd30f 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -93,6 +93,12 @@
   *out = std::move(v);
 }
 
+bool SupportsRssStatThrottled(const FtraceProcfs& ftrace_procfs) {
+  const auto trigger_info = ftrace_procfs.ReadEventTrigger("kmem", "rss_stat");
+
+  return trigger_info.find("rss_stat_throttled") != std::string::npos;
+}
+
 // This is just to reduce binary size and stack frame size of the insertions.
 // It effectively undoes STL's set::insert inlining.
 void PERFETTO_NO_INLINE InsertEvent(const char* group,
@@ -407,13 +413,19 @@
       }
 
       if (category == "memory") {
-        InsertEvent("kmem", "rss_stat", &events);
+        // Use rss_stat_throttled if supported
+        if (SupportsRssStatThrottled(*ftrace_)) {
+          InsertEvent("synthetic", "rss_stat_throttled", &events);
+        } else {
+          InsertEvent("kmem", "rss_stat", &events);
+        }
         InsertEvent("kmem", "ion_heap_grow", &events);
         InsertEvent("kmem", "ion_heap_shrink", &events);
         // ion_stat supersedes ion_heap_grow / shrink for kernel 4.19+
         InsertEvent("ion", "ion_stat", &events);
         InsertEvent("mm_event", "mm_event_record", &events);
         InsertEvent("dmabuf_heap", "dma_heap_stat", &events);
+        InsertEvent("gpu_mem", "gpu_mem_total", &events);
         continue;
       }
 
@@ -424,6 +436,20 @@
       }
     }
   }
+
+  // If throttle_rss_stat: true, use the rss_stat_throttled event if supported
+  if (request.throttle_rss_stat() && SupportsRssStatThrottled(*ftrace_)) {
+    auto it = std::find_if(
+        events.begin(), events.end(), [](const GroupAndName& event) {
+          return event.group() == "kmem" && event.name() == "rss_stat";
+        });
+
+    if (it != events.end()) {
+      events.erase(it);
+      InsertEvent("synthetic", "rss_stat_throttled", &events);
+    }
+  }
+
   return events;
 }
 
diff --git a/src/traced/probes/ftrace/ftrace_procfs.cc b/src/traced/probes/ftrace/ftrace_procfs.cc
index c6d21d5..e393648 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs.cc
@@ -130,6 +130,12 @@
   return ReadFileIntoString(path);
 }
 
+std::string FtraceProcfs::ReadEventTrigger(const std::string& group,
+                                           const std::string& name) const {
+  std::string path = root_ + "events/" + group + "/" + name + "/trigger";
+  return ReadFileIntoString(path);
+}
+
 std::string FtraceProcfs::ReadPrintkFormats() const {
   std::string path = root_ + "printk_formats";
   return ReadFileIntoString(path);
diff --git a/src/traced/probes/ftrace/ftrace_procfs.h b/src/traced/probes/ftrace/ftrace_procfs.h
index 81d3063..6e0791b 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.h
+++ b/src/traced/probes/ftrace/ftrace_procfs.h
@@ -58,6 +58,10 @@
 
   virtual std::string ReadPageHeaderFormat() const;
 
+  // Read the triggers for event with the given |group| and |name|.
+  std::string ReadEventTrigger(const std::string& group,
+                               const std::string& name) const;
+
   // Read the printk formats file.
   std::string ReadPrintkFormats() const;
 
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/synthetic/rss_stat_throttled/format b/src/traced/probes/ftrace/test/data/synthetic/events/synthetic/rss_stat_throttled/format
new file mode 100644
index 0000000..1b8185f
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/synthetic/rss_stat_throttled/format
@@ -0,0 +1,14 @@
+name: rss_stat_throttled
+ID: 1471
+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:unsigned int mm_id;	offset:8;	size:4;	signed:0;
+	field:unsigned int curr;	offset:16;	size:4;	signed:0;
+	field:int member;	offset:24;	size:4;	signed:1;
+	field:long size;	offset:32;	size:8;	signed:1;
+
+print fmt: "mm_id=%u, curr=%u, member=%d, size=%ld", REC->mm_id, REC->curr, REC->member, REC->size
diff --git a/src/traced/probes/power/BUILD.gn b/src/traced/probes/power/BUILD.gn
index fc28f67..0131a92 100644
--- a/src/traced/probes/power/BUILD.gn
+++ b/src/traced/probes/power/BUILD.gn
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import("../../../../gn/test.gni")
+
 source_set("power") {
   public_deps = [ "../../../tracing/core" ]
   deps = [
@@ -28,5 +30,19 @@
   sources = [
     "android_power_data_source.cc",
     "android_power_data_source.h",
+    "linux_power_sysfs_data_source.cc",
+    "linux_power_sysfs_data_source.h",
   ]
 }
+
+perfetto_unittest_source_set("unittests") {
+  testonly = true
+  deps = [
+    ":power",
+    "../../../../gn:default_deps",
+    "../../../../gn:gtest_and_gmock",
+    "../../../../src/tracing/test:test_support",
+    "../../../base:test_support",
+  ]
+  sources = [ "linux_power_sysfs_data_source_unittest.cc" ]
+}
diff --git a/src/traced/probes/power/linux_power_sysfs_data_source.cc b/src/traced/probes/power/linux_power_sysfs_data_source.cc
new file mode 100644
index 0000000..ff1e8b4
--- /dev/null
+++ b/src/traced/probes/power/linux_power_sysfs_data_source.cc
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2021 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/traced/probes/power/linux_power_sysfs_data_source.h"
+
+#include <dirent.h>
+#include <sys/types.h>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/task_runner.h"
+#include "perfetto/base/time.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/optional.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/tracing/core/trace_packet.h"
+#include "perfetto/ext/tracing/core/trace_writer.h"
+#include "perfetto/tracing/core/data_source_config.h"
+
+#include "protos/perfetto/trace/power/battery_counters.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto {
+
+namespace {
+constexpr uint32_t kDefaultPollIntervalMs = 1000;
+
+base::Optional<int64_t> ReadFileAsInt64(std::string path) {
+  std::string buf;
+  if (!base::ReadFile(path, &buf))
+    return base::nullopt;
+  return base::StringToInt64(base::StripSuffix(buf, "\n"));
+}
+}  // namespace
+
+LinuxPowerSysfsDataSource::BatteryInfo::BatteryInfo(
+    const char* power_supply_dir_path) {
+  base::ScopedDir power_supply_dir(opendir(power_supply_dir_path));
+  if (!power_supply_dir)
+    return;
+
+  for (auto* ent = readdir(power_supply_dir.get()); ent;
+       ent = readdir(power_supply_dir.get())) {
+    if (ent->d_name[0] == '.')
+      continue;
+    std::string dir_name =
+        std::string(power_supply_dir_path) + "/" + ent->d_name;
+    std::string buf;
+    if (!base::ReadFile(dir_name + "/type", &buf) ||
+        base::StripSuffix(buf, "\n") != "Battery")
+      continue;
+    buf.clear();
+    if (!base::ReadFile(dir_name + "/present", &buf) ||
+        base::StripSuffix(buf, "\n") != "1")
+      continue;
+    sysfs_battery_dirs_.push_back(dir_name);
+  }
+}
+LinuxPowerSysfsDataSource::BatteryInfo::~BatteryInfo() = default;
+
+size_t LinuxPowerSysfsDataSource::BatteryInfo::num_batteries() const {
+  return sysfs_battery_dirs_.size();
+}
+
+base::Optional<int64_t>
+LinuxPowerSysfsDataSource::BatteryInfo::GetChargeCounterUah(
+    size_t battery_idx) {
+  PERFETTO_CHECK(battery_idx < sysfs_battery_dirs_.size());
+  return ReadFileAsInt64(sysfs_battery_dirs_[battery_idx] + "/charge_now");
+}
+
+base::Optional<int64_t>
+LinuxPowerSysfsDataSource::BatteryInfo::GetCapacityPercent(size_t battery_idx) {
+  PERFETTO_CHECK(battery_idx < sysfs_battery_dirs_.size());
+  return ReadFileAsInt64(sysfs_battery_dirs_[battery_idx] + "/capacity");
+}
+
+base::Optional<int64_t> LinuxPowerSysfsDataSource::BatteryInfo::GetCurrentNowUa(
+    size_t battery_idx) {
+  PERFETTO_CHECK(battery_idx < sysfs_battery_dirs_.size());
+  return ReadFileAsInt64(sysfs_battery_dirs_[battery_idx] + "/current_now");
+}
+
+base::Optional<int64_t>
+LinuxPowerSysfsDataSource::BatteryInfo::GetAverageCurrentUa(
+    size_t battery_idx) {
+  PERFETTO_CHECK(battery_idx < sysfs_battery_dirs_.size());
+  return ReadFileAsInt64(sysfs_battery_dirs_[battery_idx] + "/current_avg");
+}
+
+// static
+const ProbesDataSource::Descriptor LinuxPowerSysfsDataSource::descriptor = {
+    /*name*/ "linux.sysfs_power",
+    /*flags*/ Descriptor::kFlagsNone,
+};
+
+LinuxPowerSysfsDataSource::LinuxPowerSysfsDataSource(
+    DataSourceConfig cfg,
+    base::TaskRunner* task_runner,
+    TracingSessionID session_id,
+    std::unique_ptr<TraceWriter> writer)
+    : ProbesDataSource(session_id, &descriptor),
+      poll_interval_ms_(kDefaultPollIntervalMs),
+      task_runner_(task_runner),
+      writer_(std::move(writer)),
+      weak_factory_(this) {
+  base::ignore_result(cfg);  // The data source doesn't need any config yet.
+}
+
+LinuxPowerSysfsDataSource::~LinuxPowerSysfsDataSource() = default;
+
+void LinuxPowerSysfsDataSource::Start() {
+  battery_info_.reset(new BatteryInfo());
+  Tick();
+}
+
+void LinuxPowerSysfsDataSource::Tick() {
+  // Post next task.
+  auto now_ms = base::GetWallTimeMs().count();
+  auto weak_this = weak_factory_.GetWeakPtr();
+  task_runner_->PostDelayedTask(
+      [weak_this] {
+        if (weak_this)
+          weak_this->Tick();
+      },
+      poll_interval_ms_ - static_cast<uint32_t>(now_ms % poll_interval_ms_));
+
+  WriteBatteryCounters();
+}
+
+void LinuxPowerSysfsDataSource::WriteBatteryCounters() {
+  auto packet = writer_->NewTracePacket();
+  packet->set_timestamp(static_cast<uint64_t>(base::GetBootTimeNs().count()));
+
+  // Query battery counters from sysfs. Report the battery counters for each
+  // battery.
+  for (size_t battery_idx = 0; battery_idx < battery_info_->num_batteries();
+       battery_idx++) {
+    auto* counters_proto = packet->set_battery();
+    auto value = battery_info_->GetChargeCounterUah(battery_idx);
+    if (value)
+      counters_proto->set_charge_counter_uah(*value);
+    value = battery_info_->GetCapacityPercent(battery_idx);
+    if (value)
+      counters_proto->set_capacity_percent(static_cast<float>(*value));
+    value = battery_info_->GetCurrentNowUa(battery_idx);
+    if (value)
+      counters_proto->set_current_ua(*value);
+    value = battery_info_->GetAverageCurrentUa(battery_idx);
+    if (value)
+      counters_proto->set_current_ua(*value);
+  }
+}
+
+void LinuxPowerSysfsDataSource::Flush(FlushRequestID,
+                                      std::function<void()> callback) {
+  writer_->Flush(callback);
+}
+
+}  // namespace perfetto
diff --git a/src/traced/probes/power/linux_power_sysfs_data_source.h b/src/traced/probes/power/linux_power_sysfs_data_source.h
new file mode 100644
index 0000000..9838a15
--- /dev/null
+++ b/src/traced/probes/power/linux_power_sysfs_data_source.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 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_TRACED_PROBES_POWER_LINUX_POWER_SYSFS_DATA_SOURCE_H_
+#define SRC_TRACED_PROBES_POWER_LINUX_POWER_SYSFS_DATA_SOURCE_H_
+
+#include "perfetto/ext/base/optional.h"
+#include "perfetto/ext/base/weak_ptr.h"
+#include "perfetto/tracing/core/data_source_config.h"
+#include "src/traced/probes/probes_data_source.h"
+
+namespace perfetto {
+class BatteryInfo;
+class TraceWriter;
+
+namespace base {
+class TaskRunner;
+}
+
+class LinuxPowerSysfsDataSource : public ProbesDataSource {
+ public:
+  class BatteryInfo {
+   public:
+    explicit BatteryInfo(
+        const char* power_supply_dir_path = "/sys/class/power_supply");
+    ~BatteryInfo();
+
+    // The current coloumb counter value in µAh.
+    base::Optional<int64_t> GetChargeCounterUah(size_t battery_idx);
+
+    // The battery capacity in percent.
+    base::Optional<int64_t> GetCapacityPercent(size_t battery_idx);
+
+    // The current reading of the battery in µA.
+    base::Optional<int64_t> GetCurrentNowUa(size_t battery_idx);
+
+    // The smoothed current reading of the battery in µA.
+    base::Optional<int64_t> GetAverageCurrentUa(size_t battery_idx);
+
+    size_t num_batteries() const;
+
+   private:
+    // The subdirectories that contain info of a battery power supply, not
+    // USBPD, AC or other types of power supplies.
+    std::vector<std::string> sysfs_battery_dirs_;
+  };
+  static const ProbesDataSource::Descriptor descriptor;
+
+  LinuxPowerSysfsDataSource(DataSourceConfig,
+                            base::TaskRunner*,
+                            TracingSessionID,
+                            std::unique_ptr<TraceWriter> writer);
+
+  ~LinuxPowerSysfsDataSource() override;
+
+  base::WeakPtr<LinuxPowerSysfsDataSource> GetWeakPtr() const;
+
+  // ProbesDataSource implementation.
+  void Start() override;
+  void Flush(FlushRequestID, std::function<void()> callback) override;
+  // Use the default ClearIncrementalState() implementation: this data source
+  // doesn't have any incremental state.
+
+ private:
+  void Tick();
+  void WriteBatteryCounters();
+
+  uint32_t poll_interval_ms_ = 0;
+
+  base::TaskRunner* const task_runner_;
+  std::unique_ptr<TraceWriter> writer_;
+  std::unique_ptr<BatteryInfo> battery_info_;
+  base::WeakPtrFactory<LinuxPowerSysfsDataSource> weak_factory_;  // Keep last.
+};
+
+}  // namespace perfetto
+
+#endif  // SRC_TRACED_PROBES_POWER_LINUX_POWER_SYSFS_DATA_SOURCE_H_
diff --git a/src/traced/probes/power/linux_power_sysfs_data_source_unittest.cc b/src/traced/probes/power/linux_power_sysfs_data_source_unittest.cc
new file mode 100644
index 0000000..590c926
--- /dev/null
+++ b/src/traced/probes/power/linux_power_sysfs_data_source_unittest.cc
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 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/traced/probes/power/linux_power_sysfs_data_source.h"
+#include "src/base/test/tmp_dir_tree.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace {
+
+TEST(LinuxPowerSysfsDataSourceTest, BatteryCounters) {
+  base::TmpDirTree tmpdir;
+  std::unique_ptr<LinuxPowerSysfsDataSource::BatteryInfo> battery_info_;
+
+  tmpdir.AddDir("BAT0");
+  tmpdir.AddFile("BAT0/type", "Battery\n");
+  tmpdir.AddFile("BAT0/present", "1\n");
+  tmpdir.AddFile("BAT0/capacity", "95\n");         // 95 percent.
+  tmpdir.AddFile("BAT0/charge_now", "3074000\n");  // 3074000 µAh.
+  tmpdir.AddFile("BAT0/current_now", "245000\n");  // 245000 µA.
+  tmpdir.AddFile("BAT0/current_avg", "240000\n");  // 240000 µA.
+
+  battery_info_.reset(
+      new LinuxPowerSysfsDataSource::BatteryInfo(tmpdir.path().c_str()));
+
+  EXPECT_EQ(battery_info_->num_batteries(), 1u);
+  EXPECT_EQ(*battery_info_->GetCapacityPercent(0), 95);
+  EXPECT_EQ(*battery_info_->GetCurrentNowUa(0), 245000);
+  EXPECT_EQ(*battery_info_->GetAverageCurrentUa(0), 240000);
+  EXPECT_EQ(*battery_info_->GetChargeCounterUah(0), 3074000);
+}
+
+TEST(LinuxPowerSysfsDataSourceTest, HidDeviceCounters) {
+  base::TmpDirTree tmpdir;
+  std::unique_ptr<LinuxPowerSysfsDataSource::BatteryInfo> battery_info_;
+
+  // Some HID devices (e.g. stylus) can also report battery info.
+  tmpdir.AddDir("hid-0001-battery");
+  tmpdir.AddFile("hid-0001-battery/type", "Battery\n");
+  tmpdir.AddFile("hid-0001-battery/present", "1\n");
+  tmpdir.AddFile("hid-0001-battery/capacity", "88\n");  // 88 percent.
+  // The HID device only reports the battery capacity in percent.
+
+  battery_info_.reset(
+      new LinuxPowerSysfsDataSource::BatteryInfo(tmpdir.path().c_str()));
+
+  EXPECT_EQ(battery_info_->num_batteries(), 1u);
+  EXPECT_EQ(*battery_info_->GetCapacityPercent(0), 88);
+  EXPECT_EQ(battery_info_->GetCurrentNowUa(0), base::nullopt);
+  EXPECT_EQ(battery_info_->GetAverageCurrentUa(0), base::nullopt);
+  EXPECT_EQ(battery_info_->GetChargeCounterUah(0), base::nullopt);
+}
+
+}  // namespace
+}  // namespace perfetto
diff --git a/src/traced/probes/probes_producer.cc b/src/traced/probes/probes_producer.cc
index e33a2a0..b2c4297 100644
--- a/src/traced/probes/probes_producer.cc
+++ b/src/traced/probes/probes_producer.cc
@@ -42,6 +42,7 @@
 #include "src/traced/probes/metatrace/metatrace_data_source.h"
 #include "src/traced/probes/packages_list/packages_list_data_source.h"
 #include "src/traced/probes/power/android_power_data_source.h"
+#include "src/traced/probes/power/linux_power_sysfs_data_source.h"
 #include "src/traced/probes/probes_data_source.h"
 #include "src/traced/probes/ps/process_stats_data_source.h"
 #include "src/traced/probes/sys_stats/sys_stats_data_source.h"
@@ -71,6 +72,7 @@
     &InodeFileDataSource::descriptor,            //
     &SysStatsDataSource::descriptor,             //
     &AndroidPowerDataSource::descriptor,         //
+    &LinuxPowerSysfsDataSource::descriptor,      //
     &AndroidLogDataSource::descriptor,           //
     &PackagesListDataSource::descriptor,         //
     &MetatraceDataSource::descriptor,            //
@@ -176,6 +178,8 @@
     data_source = CreateSysStatsDataSource(session_id, config);
   } else if (config.name() == AndroidPowerDataSource::descriptor.name) {
     data_source = CreateAndroidPowerDataSource(session_id, config);
+  } else if (config.name() == LinuxPowerSysfsDataSource::descriptor.name) {
+    data_source = CreateLinuxPowerSysfsDataSource(session_id, config);
   } else if (config.name() == AndroidLogDataSource::descriptor.name) {
     data_source = CreateAndroidLogDataSource(session_id, config);
   } else if (config.name() == PackagesListDataSource::descriptor.name) {
@@ -289,6 +293,16 @@
                                  endpoint_->CreateTraceWriter(buffer_id)));
 }
 
+std::unique_ptr<ProbesDataSource>
+ProbesProducer::CreateLinuxPowerSysfsDataSource(
+    TracingSessionID session_id,
+    const DataSourceConfig& config) {
+  auto buffer_id = static_cast<BufferID>(config.target_buffer());
+  return std::unique_ptr<ProbesDataSource>(
+      new LinuxPowerSysfsDataSource(config, task_runner_, session_id,
+                                    endpoint_->CreateTraceWriter(buffer_id)));
+}
+
 std::unique_ptr<ProbesDataSource> ProbesProducer::CreateAndroidLogDataSource(
     TracingSessionID session_id,
     const DataSourceConfig& config) {
diff --git a/src/traced/probes/probes_producer.h b/src/traced/probes/probes_producer.h
index dda1759..6650501 100644
--- a/src/traced/probes/probes_producer.h
+++ b/src/traced/probes/probes_producer.h
@@ -87,6 +87,9 @@
   std::unique_ptr<ProbesDataSource> CreateAndroidLogDataSource(
       TracingSessionID session_id,
       const DataSourceConfig& config);
+  std::unique_ptr<ProbesDataSource> CreateLinuxPowerSysfsDataSource(
+      TracingSessionID session_id,
+      const DataSourceConfig& config);
   std::unique_ptr<ProbesDataSource> CreatePackagesListDataSource(
       TracingSessionID session_id,
       const DataSourceConfig& config);
diff --git a/src/tracing/core/trace_buffer.cc b/src/tracing/core/trace_buffer.cc
index 4b72d42..acdcaa9 100644
--- a/src/tracing/core/trace_buffer.cc
+++ b/src/tracing/core/trace_buffer.cc
@@ -27,25 +27,6 @@
 #define TRACE_BUFFER_VERBOSE_LOGGING() 0  // Set to 1 when debugging unittests.
 #if TRACE_BUFFER_VERBOSE_LOGGING()
 #define TRACE_BUFFER_DLOG PERFETTO_DLOG
-namespace {
-constexpr char kHexDigits[] = "0123456789abcdef";
-std::string HexDump(const uint8_t* src, size_t size) {
-  std::string buf;
-  buf.reserve(4096 * 4);
-  char line[64];
-  char* c = line;
-  for (size_t i = 0; i < size; i++) {
-    *c++ = kHexDigits[(src[i] >> 4) & 0x0f];
-    *c++ = kHexDigits[(src[i] >> 0) & 0x0f];
-    if (i % 16 == 15) {
-      buf.append("\n");
-      buf.append(line);
-      c = line;
-    }
-  }
-  return buf;
-}
-}  // namespace
 #else
 #define TRACE_BUFFER_DLOG(...) void()
 #endif
@@ -224,7 +205,8 @@
     TRACE_BUFFER_DLOG("  copying @ [%lu - %lu] %zu", wptr - begin(),
                       uintptr_t(wptr - begin()) + record_size, record_size);
     WriteChunkRecord(wptr, record, src, size);
-    TRACE_BUFFER_DLOG("Chunk raw: %s", HexDump(wptr, record_size).c_str());
+    TRACE_BUFFER_DLOG("Chunk raw: %s",
+                      base::HexDump(wptr, record_size).c_str());
     stats_.set_chunks_rewritten(stats_.chunks_rewritten() + 1);
     return;
   }
@@ -281,7 +263,7 @@
   TRACE_BUFFER_DLOG("  copying @ [%lu - %lu] %zu", wptr_ - begin(),
                     uintptr_t(wptr_ - begin()) + record_size, record_size);
   WriteChunkRecord(wptr_, record, src, size);
-  TRACE_BUFFER_DLOG("Chunk raw: %s", HexDump(wptr_, record_size).c_str());
+  TRACE_BUFFER_DLOG("Chunk raw: %s", base::HexDump(wptr_, record_size).c_str());
   wptr_ += record_size;
   if (wptr_ >= end()) {
     PERFETTO_DCHECK(padding_size == 0);
@@ -456,7 +438,7 @@
   }
   TRACE_BUFFER_DLOG(
       "Chunk raw (after patch): %s",
-      HexDump(chunk_begin, chunk_meta.chunk_record->size).c_str());
+      base::HexDump(chunk_begin, chunk_meta.chunk_record->size).c_str());
 
   stats_.set_patches_succeeded(stats_.patches_succeeded() + patches_size);
   if (!other_patches_pending) {
diff --git a/src/tracing/core/tracing_service_impl.cc b/src/tracing/core/tracing_service_impl.cc
index 1121ed9..b898a1a 100644
--- a/src/tracing/core/tracing_service_impl.cc
+++ b/src/tracing/core/tracing_service_impl.cc
@@ -53,6 +53,7 @@
 #include "perfetto/base/status.h"
 #include "perfetto/base/task_runner.h"
 #include "perfetto/ext/base/android_utils.h"
+#include "perfetto/ext/base/crash_keys.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/metatrace.h"
 #include "perfetto/ext/base/string_utils.h"
@@ -126,6 +127,11 @@
 constexpr uint32_t kGuardrailsMaxTracingBufferSizeKb = 128 * 1024;
 constexpr uint32_t kGuardrailsMaxTracingDurationMillis = 24 * kMillisPerHour;
 
+// TODO(primiano): this is to investigate b/191600928. Remove in Jan 2022.
+base::CrashKey g_crash_key_prod_name("producer_name");
+base::CrashKey g_crash_key_ds_count("ds_count");
+base::CrashKey g_crash_key_ds_clear_count("ds_clear_count");
+
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) || PERFETTO_BUILDFLAG(PERFETTO_OS_NACL)
 struct iovec {
   void* iov_base;  // Address
@@ -1172,7 +1178,7 @@
     task_runner_->PostDelayedTask(
         [weak_this, tsid] {
           if (weak_this)
-            weak_this->ReadBuffers(tsid, nullptr);
+            weak_this->ReadBuffersIntoFile(tsid);
         },
         tracing_session->delay_to_next_write_period_ms());
   }
@@ -1592,7 +1598,7 @@
 
   if (tracing_session->write_into_file) {
     tracing_session->write_period_ms = 0;
-    ReadBuffers(tracing_session->id, nullptr);
+    ReadBuffersIntoFile(tracing_session->id);
   }
 
   if (tracing_session->on_disable_callback_for_bugreport) {
@@ -1930,13 +1936,18 @@
 
   // Queue the IPCs to producers with active data sources that opted in.
   std::map<ProducerID, std::vector<DataSourceInstanceID>> clear_map;
+  int ds_clear_count = 0;
   for (const auto& kv : tracing_session->data_source_instances) {
     ProducerID producer_id = kv.first;
     const DataSourceInstance& data_source = kv.second;
-    if (data_source.handles_incremental_state_clear)
+    if (data_source.handles_incremental_state_clear) {
       clear_map[producer_id].push_back(data_source.instance_id);
+      ++ds_clear_count;
+    }
   }
 
+  g_crash_key_ds_clear_count.Set(ds_clear_count);
+
   for (const auto& kv : clear_map) {
     ProducerID producer_id = kv.first;
     const std::vector<DataSourceInstanceID>& data_sources = kv.second;
@@ -1947,44 +1958,25 @@
     }
     producer->ClearIncrementalState(data_sources);
   }
+
+  // ClearIncrementalState internally posts a task for each data source. Clear
+  // the crash key in a task queued at the end of the tasks atove.
+  task_runner_->PostTask([] { g_crash_key_ds_clear_count.Clear(); });
 }
 
-// Note: when this is called to write into a file passed when starting tracing
-// |consumer| will be == nullptr (as opposite to the case of a consumer asking
-// to send the trace data back over IPC).
-bool TracingServiceImpl::ReadBuffers(TracingSessionID tsid,
-                                     ConsumerEndpointImpl* consumer) {
+bool TracingServiceImpl::ReadBuffersIntoConsumer(
+    TracingSessionID tsid,
+    ConsumerEndpointImpl* consumer) {
+  PERFETTO_DCHECK(consumer);
   PERFETTO_DCHECK_THREAD(thread_checker_);
   TracingSession* tracing_session = GetTracingSession(tsid);
   if (!tracing_session) {
-    // This will be hit systematically from the PostDelayedTask when directly
-    // writing into the file (in which case consumer == nullptr). Suppress the
-    // log in this case as it's just spam.
-    if (consumer) {
-      PERFETTO_DLOG("Cannot ReadBuffers(): no tracing session is active");
-    }
-    return false;
-  }
-
-  // When a tracing session is waiting for a trigger it is considered empty. If
-  // a tracing session finishes and moves into DISABLED without ever receiving a
-  // trigger the trace should never return any data. This includes the synthetic
-  // packets like TraceConfig and Clock snapshots. So we bail out early and let
-  // the consumer know there is no data.
-  if (!tracing_session->config.trigger_config().triggers().empty() &&
-      tracing_session->received_triggers.empty() &&
-      !tracing_session->seized_for_bugreport) {
     PERFETTO_DLOG(
-        "ReadBuffers(): tracing session has not received a trigger yet.");
+        "Cannot ReadBuffersIntoConsumer(): no tracing session is active");
     return false;
   }
 
-  // This can happen if the file is closed by a previous task because it reaches
-  // |max_file_size_bytes|.
-  if (!tracing_session->write_into_file && !consumer)
-    return false;
-
-  if (tracing_session->write_into_file && consumer) {
+  if (tracing_session->write_into_file) {
     // If the consumer enabled tracing and asked to save the contents into the
     // passed file makes little sense to also try to read the buffers over IPC,
     // as that would just steal data from the periodic draining task.
@@ -1992,14 +1984,12 @@
     return false;
   }
 
-  std::vector<TracePacket> packets;
-  packets.reserve(1024);  // Just an educated guess to avoid trivial expansions.
-
   // If a bugreport request happened and the trace was stolen for that, give
   // an empty trace with a clear signal to the consumer. This deals only with
   // the case of readback-from-IPC. A similar code-path deals with the
   // write_into_file case in MaybeSaveTraceForBugreport().
-  if (tracing_session->seized_for_bugreport && consumer) {
+  if (tracing_session->seized_for_bugreport) {
+    std::vector<TracePacket> packets;
     if (!tracing_session->config.builtin_data_sources()
              .disable_service_events()) {
       EmitSeizedForBugreportLifecycleEvent(&packets);
@@ -2009,6 +1999,66 @@
     return true;
   }
 
+  if (IsWaitingForTrigger(tracing_session))
+    return false;
+
+  return ReadBuffers(tsid, tracing_session, consumer);
+}
+
+bool TracingServiceImpl::ReadBuffersIntoFile(TracingSessionID tsid) {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+  TracingSession* tracing_session = GetTracingSession(tsid);
+  if (!tracing_session) {
+    // This will be hit systematically from the PostDelayedTask. Avoid logging,
+    // it would be just spam.
+    return false;
+  }
+
+  // This can happen if the file is closed by a previous task because it reaches
+  // |max_file_size_bytes|.
+  if (!tracing_session->write_into_file)
+    return false;
+
+  if (!tracing_session->seized_for_bugreport &&
+      IsWaitingForTrigger(tracing_session))
+    return false;
+
+  return ReadBuffers(tsid, tracing_session, nullptr);
+}
+
+bool TracingServiceImpl::IsWaitingForTrigger(TracingSession* tracing_session) {
+  // When a tracing session is waiting for a trigger, it is considered empty. If
+  // a tracing session finishes and moves into DISABLED without ever receiving a
+  // trigger, the trace should never return any data. This includes the
+  // synthetic packets like TraceConfig and Clock snapshots. So we bail out
+  // early and let the consumer know there is no data.
+  if (!tracing_session->config.trigger_config().triggers().empty() &&
+      tracing_session->received_triggers.empty()) {
+    PERFETTO_DLOG(
+        "ReadBuffers(): tracing session has not received a trigger yet.");
+    return true;
+  }
+  return false;
+}
+
+// Note: when this is called to write into a file passed when starting tracing
+// |consumer| will be == nullptr (as opposite to the case of a consumer asking
+// to send the trace data back over IPC).
+bool TracingServiceImpl::ReadBuffers(TracingSessionID tsid,
+                                     TracingSession* tracing_session,
+                                     ConsumerEndpointImpl* consumer) {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+  PERFETTO_DCHECK(tracing_session);
+
+  // Speculative fix for the memory watchdog crash in b/195145848. This function
+  // uses the heap extensively and might need a M_PURGE. window.gc() is back.
+  // TODO(primiano): if this fixes the crash we might want to coalesce the purge
+  // and throttle it.
+  auto on_ret = base::OnScopeExit([] { base::MaybeReleaseAllocatorMemToOS(); });
+
+  std::vector<TracePacket> packets;
+  packets.reserve(1024);  // Just an educated guess to avoid trivial expansions.
+
   if (!tracing_session->initial_clock_snapshot.empty()) {
     EmitClockSnapshot(tracing_session,
                       std::move(tracing_session->initial_clock_snapshot),
@@ -2040,12 +2090,10 @@
     EmitLifecycleEvents(tracing_session, &packets);
 
   size_t packets_bytes = 0;  // SUM(slice.size() for each slice in |packets|).
-  size_t total_slices = 0;   // SUM(#slices in |packets|).
 
   // Add up size for packets added by the Maybe* calls above.
   for (const TracePacket& packet : packets) {
     packets_bytes += packet.size();
-    total_slices += packet.slices().size();
   }
 
   // This is a rough threshold to determine how much to read from the buffer in
@@ -2116,7 +2164,6 @@
 
       // Append the packet (inclusive of the trusted uid) to |packets|.
       packets_bytes += packet.size();
-      total_slices += packet.slices().size();
       did_hit_threshold = packets_bytes >= kApproxBytesPerTask &&
                           !tracing_session->write_into_file;
       packets.emplace_back(std::move(packet));
@@ -2125,7 +2172,6 @@
 
   const bool has_more = did_hit_threshold;
 
-  size_t prev_packets_size = packets.size();
   if (!tracing_session->config.builtin_data_sources()
            .disable_service_events()) {
     // We don't bother snapshotting clocks here because we wouldn't be able to
@@ -2148,12 +2194,6 @@
     tracing_session->should_emit_stats = false;
   }
 
-  // Add sizes of packets emitted by the EmitLifecycleEvents + EmitStats.
-  for (size_t i = prev_packets_size; i < packets.size(); ++i) {
-    packets_bytes += packets[i].size();
-    total_slices += packets[i].slices().size();
-  }
-
   // +-------------------------------------------------------------------------+
   // | NO MORE CHANGES TO |packets| AFTER THIS POINT.                          |
   // +-------------------------------------------------------------------------+
@@ -2205,13 +2245,17 @@
                                   ? tracing_session->max_file_size_bytes
                                   : std::numeric_limits<size_t>::max();
 
+    size_t total_slices = 0;
+    for (const TracePacket& packet : packets) {
+      total_slices += packet.slices().size();
+    }
     // When writing into a file, the file should look like a root trace.proto
     // message. Each packet should be prepended with a proto preamble stating
     // its field id (within trace.proto) and size. Hence the addition below.
     const size_t max_iovecs = total_slices + packets.size();
 
     size_t num_iovecs = 0;
-    bool stop_writing_into_file = tracing_session->write_period_ms == 0;
+    bool stop_writing_into_file = false;
     std::unique_ptr<struct iovec[]> iovecs(new struct iovec[max_iovecs]);
     size_t num_iovecs_at_last_packet = 0;
     uint64_t bytes_about_to_be_written = 0;
@@ -2261,7 +2305,7 @@
 
     PERFETTO_DLOG("Draining into file, written: %" PRIu64 " KB, stop: %d",
                   (total_wr_size + 1023) / 1024, stop_writing_into_file);
-    if (stop_writing_into_file) {
+    if (stop_writing_into_file || tracing_session->write_period_ms == 0) {
       // Ensure all data was written to the file before we close it.
       base::FlushFile(fd);
       tracing_session->write_into_file.reset();
@@ -2275,7 +2319,7 @@
     task_runner_->PostDelayedTask(
         [weak_this, tsid] {
           if (weak_this)
-            weak_this->ReadBuffers(tsid, nullptr);
+            weak_this->ReadBuffersIntoFile(tsid);
         },
         tracing_session->delay_to_next_write_period_ms());
     return true;
@@ -2287,7 +2331,7 @@
     task_runner_->PostTask([weak_this, weak_consumer, tsid] {
       if (!weak_this || !weak_consumer)
         return;
-      weak_this->ReadBuffers(tsid, weak_consumer.get());
+      weak_this->ReadBuffersIntoConsumer(tsid, weak_consumer.get());
     });
   }
 
@@ -2379,6 +2423,7 @@
 
   auto reg_ds = data_sources_.emplace(desc.name(),
                                       RegisteredDataSource{producer_id, desc});
+  g_crash_key_ds_count.Set(static_cast<int64_t>(data_sources_.size()));
 
   // If there are existing tracing sessions, we need to check if the new
   // data source is enabled by any of them.
@@ -2498,6 +2543,7 @@
     if (it->second.producer_id == producer_id &&
         it->second.descriptor.name() == name) {
       data_sources_.erase(it);
+      g_crash_key_ds_count.Set(static_cast<int64_t>(data_sources_.size()));
       return;
     }
   }
@@ -3437,7 +3483,7 @@
     consumer_->OnTraceData({}, /* has_more = */ false);
     return;
   }
-  if (!service_->ReadBuffers(tracing_session_id_, this)) {
+  if (!service_->ReadBuffersIntoConsumer(tracing_session_id_, this)) {
     consumer_->OnTraceData({}, /* has_more = */ false);
   }
 }
@@ -3931,6 +3977,8 @@
   auto weak_this = weak_ptr_factory_.GetWeakPtr();
   task_runner_->PostTask([weak_this, data_sources] {
     if (weak_this) {
+      base::StringView producer_name(weak_this->name_);
+      auto scoped_crash_key = g_crash_key_prod_name.SetScoped(producer_name);
       weak_this->producer_->ClearIncrementalState(data_sources.data(),
                                                   data_sources.size());
     }
diff --git a/src/tracing/core/tracing_service_impl.h b/src/tracing/core/tracing_service_impl.h
index 97371f3..8100968 100644
--- a/src/tracing/core/tracing_service_impl.h
+++ b/src/tracing/core/tracing_service_impl.h
@@ -285,7 +285,30 @@
              uint32_t timeout_ms,
              ConsumerEndpoint::FlushCallback);
   void FlushAndDisableTracing(TracingSessionID);
-  bool ReadBuffers(TracingSessionID, ConsumerEndpointImpl*);
+
+  // Starts reading the internal tracing buffers from the tracing session `tsid`
+  // and sends them to `*consumer` (which must be != nullptr).
+  //
+  // Only reads a limited amount of data in one call. If there's more data,
+  // immediately schedules itself on a PostTask.
+  //
+  // Returns false in case of error.
+  bool ReadBuffersIntoConsumer(TracingSessionID tsid,
+                               ConsumerEndpointImpl* consumer);
+
+  // Reads all the tracing buffers from the tracing session `tsid` and writes
+  // them into the associated file.
+  //
+  // Reads all the data in the buffers (or until the file is full) before
+  // returning.
+  //
+  // If the tracing session write_period_ms is 0, the file is full or there has
+  // been an error, flushes the file and closes it. Otherwise, schedules itself
+  // to be executed after write_period_ms.
+  //
+  // Returns false in case of error.
+  bool ReadBuffersIntoFile(TracingSessionID);
+
   void FreeBuffers(TracingSessionID);
 
   // Service implementation.
@@ -654,6 +677,10 @@
   void ScrapeSharedMemoryBuffers(TracingSession*, ProducerEndpointImpl*);
   void PeriodicClearIncrementalStateTask(TracingSessionID, bool post_next_only);
   TraceBuffer* GetBufferByID(BufferID);
+  bool ReadBuffers(TracingSessionID, TracingSession*, ConsumerEndpointImpl*);
+  // Returns true if `*tracing_session` is waiting for a trigger that hasn't
+  // happened.
+  static bool IsWaitingForTrigger(TracingSession*);
   void OnStartTriggersTimeout(TracingSessionID tsid);
   void MaybeLogUploadEvent(const TraceConfig&,
                            PerfettoStatsdAtom atom,
diff --git a/src/tracing/ipc/BUILD.gn b/src/tracing/ipc/BUILD.gn
index f86aeb5..dfd0eda 100644
--- a/src/tracing/ipc/BUILD.gn
+++ b/src/tracing/ipc/BUILD.gn
@@ -27,7 +27,6 @@
     "../../../include/perfetto/ext/tracing/ipc",
   ]
   sources = [
-    "default_socket.cc",
     "memfd.cc",
     "memfd.h",
     "posix_shared_memory.cc",
@@ -36,6 +35,7 @@
     "shared_memory_windows.h",
   ]
   deps = [
+    ":default_socket",
     "../../../gn:default_deps",
     "../../../include/perfetto/ext/ipc",
     "../../base",
@@ -43,6 +43,17 @@
   ]
 }
 
+source_set("default_socket") {
+  sources = [ "default_socket.cc" ]
+  public_deps = [ "../../../include/perfetto/ext/tracing/ipc" ]
+  deps = [
+    "../../../gn:default_deps",
+    "../../../include/perfetto/ext/ipc",
+    "../../../include/perfetto/ext/tracing/core",
+    "../../base",
+  ]
+}
+
 perfetto_unittest_source_set("unittests") {
   testonly = true
   deps = [
diff --git a/src/websocket_bridge/BUILD.gn b/src/websocket_bridge/BUILD.gn
new file mode 100644
index 0000000..38f3e66
--- /dev/null
+++ b/src/websocket_bridge/BUILD.gn
@@ -0,0 +1,35 @@
+# Copyright (C) 2021 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.
+
+source_set("lib") {
+  deps = [
+    "../../gn:default_deps",
+    "../base",
+    "../base:unix_socket",
+    "../base/http",
+    "../tracing/ipc:default_socket",
+  ]
+  sources = [
+    "websocket_bridge.cc",
+    "websocket_bridge.h",
+  ]
+}
+
+executable("websocket_bridge") {
+  deps = [
+    ":lib",
+    "../../gn:default_deps",
+  ]
+  sources = [ "websocket_bridge_main.cc" ]
+}
diff --git a/src/websocket_bridge/websocket_bridge.cc b/src/websocket_bridge/websocket_bridge.cc
new file mode 100644
index 0000000..eb26506
--- /dev/null
+++ b/src/websocket_bridge/websocket_bridge.cc
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2021 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/websocket_bridge/websocket_bridge.h"
+
+#include <stdint.h>
+
+#include <map>
+#include <memory>
+#include <vector>
+
+#include "perfetto/ext/base/http/http_server.h"
+#include "perfetto/ext/base/unix_socket.h"
+#include "perfetto/ext/base/unix_task_runner.h"
+#include "perfetto/ext/tracing/ipc/default_socket.h"
+
+namespace perfetto {
+namespace {
+
+constexpr int kWebsocketPort = 8037;
+
+struct Endpoint {
+  const char* uri;
+  const char* endpoint;
+  base::SockFamily family;
+};
+
+class WSBridge : public base::HttpRequestHandler,
+                 public base::UnixSocket::EventListener {
+ public:
+  void Main(int argc, char** argv);
+
+  // base::HttpRequestHandler implementation.
+  void OnHttpRequest(const base::HttpRequest&) override;
+  void OnWebsocketMessage(const base::WebsocketMessage&) override;
+  void OnHttpConnectionClosed(base::HttpServerConnection*) override;
+
+  // base::UnixSocket::EventListener implementation.
+  void OnNewIncomingConnection(base::UnixSocket*,
+                               std::unique_ptr<base::UnixSocket>) override;
+  void OnConnect(base::UnixSocket*, bool) override;
+  void OnDisconnect(base::UnixSocket*) override;
+  void OnDataAvailable(base::UnixSocket* self) override;
+
+ private:
+  base::HttpServerConnection* GetWebsocket(base::UnixSocket*);
+
+  base::UnixTaskRunner task_runner_;
+  std::vector<Endpoint> endpoints_;
+  std::map<base::HttpServerConnection*, std::unique_ptr<base::UnixSocket>>
+      conns_;
+};
+
+void WSBridge::Main(int, char**) {
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  // On Windows traced used a TCP socket.
+  const auto kTracedFamily = base::SockFamily::kInet;
+#else
+  const auto kTracedFamily = base::SockFamily::kUnix;
+#endif
+  endpoints_.push_back({"/traced", GetConsumerSocket(), kTracedFamily});
+  endpoints_.push_back({"/adb", "127.0.0.1:5037", base::SockFamily::kInet});
+
+  base::HttpServer srv(&task_runner_, this);
+  srv.AddAllowedOrigin("http://localhost:10000");
+  srv.AddAllowedOrigin("http://127.0.0.1:10000");
+  srv.AddAllowedOrigin("https://ui.perfetto.dev");
+
+  srv.Start(kWebsocketPort);
+  PERFETTO_LOG("[WSBridge] Listening on 127.0.0.1:%d", kWebsocketPort);
+  task_runner_.Run();
+}
+
+void WSBridge::OnHttpRequest(const base::HttpRequest& req) {
+  for (const auto& ep : endpoints_) {
+    if (req.uri != ep.uri || !req.is_websocket_handshake)
+      continue;
+
+    // Connect to the endpoint in blocking mode.
+    auto sock_raw =
+        base::UnixSocketRaw::CreateMayFail(ep.family, base::SockType::kStream);
+    if (!sock_raw) {
+      PERFETTO_PLOG("socket() failed");
+      req.conn->SendResponseAndClose("500 Server Error");
+      return;
+    }
+    PERFETTO_LOG("[WSBridge] New connection from \"%.*s\"",
+                 static_cast<int>(req.origin.size()), req.origin.data());
+    sock_raw.SetTxTimeout(3000);
+    sock_raw.SetBlocking(true);
+
+    if (!sock_raw.Connect(ep.endpoint)) {
+      PERFETTO_ELOG("[WSBridge] Connection to %s failed", ep.endpoint);
+      req.conn->SendResponseAndClose("503 Service Unavailable");
+      return;
+    }
+    sock_raw.SetBlocking(false);
+
+    PERFETTO_DLOG("[WSBridge] Connected to %s", ep.endpoint);
+    conns_[req.conn] = base::UnixSocket::AdoptConnected(
+        sock_raw.ReleaseFd(), this, &task_runner_, ep.family,
+        base::SockType::kStream);
+
+    req.conn->UpgradeToWebsocket(req);
+    return;
+  }  // for (endpoint)
+  req.conn->SendResponseAndClose("404 Not Found");
+}
+
+// Called when an inbound websocket message is received from the browser.
+void WSBridge::OnWebsocketMessage(const base::WebsocketMessage& msg) {
+  auto it = conns_.find(msg.conn);
+  PERFETTO_CHECK(it != conns_.end());
+  // Pass through the websocket message onto the endpoint TCP socket.
+  base::UnixSocket& sock = *it->second;
+  sock.Send(msg.data.data(), msg.data.size());
+}
+
+// Called when a TCP message is received from the endpoint.
+void WSBridge::OnDataAvailable(base::UnixSocket* sock) {
+  base::HttpServerConnection* websocket = GetWebsocket(sock);
+  PERFETTO_CHECK(websocket);
+
+  char buf[8192];
+  auto rsize = sock->Receive(buf, sizeof(buf));
+  if (rsize > 0) {
+    websocket->SendWebsocketMessage(buf, static_cast<size_t>(rsize));
+  } else {
+    // Connection closed or errored.
+    sock->Shutdown(/*notify=*/true);  // Will trigger OnDisconnect().
+    websocket->Close();
+  }
+}
+
+// Called when the browser terminates the websocket connection.
+void WSBridge::OnHttpConnectionClosed(base::HttpServerConnection* websocket) {
+  PERFETTO_DLOG("[WSBridge] Websocket connection closed");
+  auto it = conns_.find(websocket);
+  if (it == conns_.end())
+    return;  // Can happen if ADB closed first.
+  base::UnixSocket& sock = *it->second;
+  sock.Shutdown(/*notify=*/true);
+  conns_.erase(websocket);
+}
+
+void WSBridge::OnDisconnect(base::UnixSocket* sock) {
+  base::HttpServerConnection* websocket = GetWebsocket(sock);
+  if (!websocket)
+    return;
+  websocket->Close();
+  sock->Shutdown(/*notify=*/false);
+  conns_.erase(websocket);
+  PERFETTO_DLOG("[WSBridge] Socket connection closed");
+}
+
+base::HttpServerConnection* WSBridge::GetWebsocket(base::UnixSocket* sock) {
+  for (const auto& it : conns_) {
+    if (it.second.get() == sock) {
+      return it.first;
+    }
+  }
+  return nullptr;
+}
+
+void WSBridge::OnConnect(base::UnixSocket*, bool) {}
+void WSBridge::OnNewIncomingConnection(base::UnixSocket*,
+                                       std::unique_ptr<base::UnixSocket>) {}
+
+}  // namespace
+
+int PERFETTO_EXPORT_ENTRYPOINT WebsocketBridgeMain(int argc, char** argv) {
+  perfetto::WSBridge ws_bridge;
+  ws_bridge.Main(argc, argv);
+  return 0;
+}
+
+}  // namespace perfetto
diff --git a/src/trace_processor/importers/json/json_tracker.cc b/src/websocket_bridge/websocket_bridge.h
similarity index 66%
rename from src/trace_processor/importers/json/json_tracker.cc
rename to src/websocket_bridge/websocket_bridge.h
index 971042a..d93bd61 100644
--- a/src/trace_processor/importers/json/json_tracker.cc
+++ b/src/websocket_bridge/websocket_bridge.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2021 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.
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-#include "src/trace_processor/importers/json/json_tracker.h"
+#ifndef SRC_WEBSOCKET_BRIDGE_WEBSOCKET_BRIDGE_H_
+#define SRC_WEBSOCKET_BRIDGE_WEBSOCKET_BRIDGE_H_
+
+#include "perfetto/base/compiler.h"
 
 namespace perfetto {
-namespace trace_processor {
+int WebsocketBridgeMain(int argc, char** argv);
+}
 
-JsonTracker::JsonTracker(TraceProcessorContext*) {}
-JsonTracker::~JsonTracker() = default;
-
-}  // namespace trace_processor
-}  // namespace perfetto
+#endif  // SRC_WEBSOCKET_BRIDGE_WEBSOCKET_BRIDGE_H_
diff --git a/src/trace_processor/importers/json/json_tracker.cc b/src/websocket_bridge/websocket_bridge_main.cc
similarity index 62%
copy from src/trace_processor/importers/json/json_tracker.cc
copy to src/websocket_bridge/websocket_bridge_main.cc
index 971042a..480522b 100644
--- a/src/trace_processor/importers/json/json_tracker.cc
+++ b/src/websocket_bridge/websocket_bridge_main.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2021 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.
@@ -14,13 +14,10 @@
  * limitations under the License.
  */
 
-#include "src/trace_processor/importers/json/json_tracker.h"
+#include "src/websocket_bridge/websocket_bridge.h"
 
-namespace perfetto {
-namespace trace_processor {
-
-JsonTracker::JsonTracker(TraceProcessorContext*) {}
-JsonTracker::~JsonTracker() = default;
-
-}  // namespace trace_processor
-}  // namespace perfetto
+// This is split in a dedicated translation unit so that tracebox can refer to
+// WebsocketBridgeMain() without pulling in a main() symbol.
+int main(int argc, char** argv) {
+  return perfetto::WebsocketBridgeMain(argc, argv);
+}
diff --git a/test/data/atrace_compressed.ctrace.sha256 b/test/data/atrace_compressed.ctrace.sha256
new file mode 100644
index 0000000..d39c5ea
--- /dev/null
+++ b/test/data/atrace_compressed.ctrace.sha256
@@ -0,0 +1 @@
+db92be3d78ab0619af5068240b88bad78a655f3e403c5f7c7876956eb5b260b0
\ No newline at end of file
diff --git a/test/data/atrace_uncompressed_b_208691037.sha256 b/test/data/atrace_uncompressed_b_208691037.sha256
new file mode 100644
index 0000000..7348172
--- /dev/null
+++ b/test/data/atrace_uncompressed_b_208691037.sha256
@@ -0,0 +1 @@
+9872b827df72895a98af2977c117ad3bc31f8c62be79d211bdf254170db4e1de
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
index 5ddebaa..86c5f5e 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 @@
-f7ff6334f50962bde39b2000a2fc331ac48341782916c62969d026c81570814e
\ No newline at end of file
+0c1ab8b9a3d60c59e43fc4be2f7d6f41a5a0bc81b7a5e5693d8d933952965783
\ 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 7d5af5a..c44a1c0 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 @@
-1eaf9d88a907eea4517052cba5bc06fda521d84086cf761b2710c3ae2dda3d13
\ No newline at end of file
+babc1a1b16f85e2746e157705987fddd90b680ac24ed69600c4709acca997d33
\ 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 5c7c86d..0813ae8 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 @@
-f63513514da003a0f24d9df61a5516f1961b3fa88ca1cdf74ad50cd916571222
\ No newline at end of file
+11a73e2b898d70785c9480500e0bf35c9426c49340bee1df3eab672a72bd91a2
\ 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 eb52a0a..6211277 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 @@
-6241ed1b7972f4cb709c391d383513b6e2999b46386b3dedb4e2565baa9677e1
\ No newline at end of file
+5c76ac76334433ef5ca847f47f0020d57d0bb78a6bc3741c7e4e1e9ad4e32c81
\ 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 6c7bbde..4efc85f 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 @@
-498e62690012328f236403968b31aab16a63db000c29b6f38bcec36ea0726a06
\ No newline at end of file
+99911b5f7e6db8081ecbca7d7e2c47bf57b8a54434c639a17830dd4d84b67009
\ 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 f565bae..2104823 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 @@
-80e9b51139060f6c3a0fe7a6d35d91d2fdd9b600cf6c8e667236b8aa8498cbfe
\ No newline at end of file
+459f3bcd870dc4d697510e380983e1daf39355732766e754f6649f066c60007e
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256 b/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
index b57e638..815741c 100644
--- a/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
@@ -1 +1 @@
-8ac1dbfa90bdf508de0ffda2d37c0a55200dc579fbb8515dc38dd01711735fff
\ No newline at end of file
+f5f87e3e2bbb7026e797dcd3fd8c25f4776ed03aaa50e399115fbe92d0008e75
\ 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 6c7bbde..4efc85f 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 @@
-498e62690012328f236403968b31aab16a63db000c29b6f38bcec36ea0726a06
\ No newline at end of file
+99911b5f7e6db8081ecbca7d7e2c47bf57b8a54434c639a17830dd4d84b67009
\ 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 9e91e29..89e28ad 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 @@
-cf77763da2a3cc43b56e08debd14a09b1e3fe13ad9a5dc25b599d81bc0d5e236
\ No newline at end of file
+c64e2df83124c5e42b7552d8e673fa390ee31d1b6c5a8acd07eb022afd97b84d
\ 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 f01dadb..a461053 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 @@
-46abbeaf901c8c60d22d3bf3b77bf820a382ec6a2023d7efd85a740c549c5093
\ No newline at end of file
+ffb8037bb4c40d392cd0d99220fc1330ef53ca0c912dbf104dcb5c4900dc9e05
\ No newline at end of file
diff --git a/test/end_to_end_integrationtest.cc b/test/end_to_end_integrationtest.cc
index 1ec13d3..3d2b65e 100644
--- a/test/end_to_end_integrationtest.cc
+++ b/test/end_to_end_integrationtest.cc
@@ -296,10 +296,30 @@
 #define TEST_PRODUCER_SOCK_NAME ::perfetto::GetProducerSocket()
 #endif
 
+// Defining this macro out-of-line works around C/C++'s macro rules (see
+// https://stackoverflow.com/questions/26284393/nested-operator-in-c-preprocessor).
+#define DisableTest(x) DISABLED_##x
+
 #if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
 #define TreeHuggerOnly(x) x
 #else
-#define TreeHuggerOnly(x) DISABLED_##x
+#define TreeHuggerOnly(x) DisableTest(x)
+#endif
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+#define AndroidOnly(x) x
+#else
+#define AndroidOnly(x) DisableTest(x)
+#endif
+
+// Disable cmdline tests on sanitizets because they use fork() and that messes
+// up leak / races detections, which has been fixed only recently (see
+// https://github.com/google/sanitizers/issues/836 ).
+#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || \
+    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER)
+#define NoSanitizers(X) DisableTest(X)
+#else
+#define NoSanitizers(X) X
 #endif
 
 // TODO(b/73453011): reenable on more platforms (including standalone Android).
@@ -1190,16 +1210,6 @@
                     Property(&protos::gen::TestEvent::str, SizeIs(kMsgSize)))));
 }
 
-// Disable cmdline tests on sanitizets because they use fork() and that messes
-// up leak / races detections, which has been fixed only recently (see
-// https://github.com/google/sanitizers/issues/836 ).
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || \
-    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER)
-#define NoSanitizers(X) DISABLED_##X
-#else
-#define NoSanitizers(X) X
-#endif
-
 TEST_F(PerfettoCmdlineTest, NoSanitizers(InvalidCases)) {
   std::string cfg("duration_ms: 100");
 
@@ -1518,11 +1528,8 @@
 
 // Dropbox on the commandline client only works on android builds. So disable
 // this test on all other builds.
-#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
-TEST_F(PerfettoCmdlineTest, NoSanitizers(NoDataNoFileWithoutTrigger)) {
-#else
-TEST_F(PerfettoCmdlineTest, DISABLED_NoDataNoFileWithoutTrigger) {
-#endif
+TEST_F(PerfettoCmdlineTest,
+       NoSanitizers(TreeHuggerOnly(NoDataNoFileWithoutTrigger))) {
   // See |message_count| and |message_size| in the TraceConfig above.
   constexpr size_t kMessageCount = 11;
   constexpr size_t kMessageSize = 32;
@@ -1746,4 +1753,86 @@
   EXPECT_EQ(0, query_raw.Run(&stderr_)) << stderr_;
 }
 
+TEST_F(PerfettoCmdlineTest,
+       NoSanitizers(AndroidOnly(CmdTriggerWithUploadFlag))) {
+  // See |message_count| and |message_size| in the TraceConfig above.
+  constexpr size_t kMessageCount = 2;
+  constexpr size_t kMessageSize = 2;
+  protos::gen::TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(1024);
+  auto* ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("android.perfetto.FakeProducer");
+  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
+  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  auto* trigger_cfg = trace_config.mutable_trigger_config();
+  trigger_cfg->set_trigger_mode(
+      protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
+  trigger_cfg->set_trigger_timeout_ms(15000);
+  auto* trigger = trigger_cfg->add_triggers();
+  trigger->set_name("trigger_name");
+  // |stop_delay_ms| must be long enough that we can write the packets in
+  // before the trace finishes. This has to be long enough for the slowest
+  // emulator. But as short as possible to prevent the test running a long
+  // time.
+  trigger->set_stop_delay_ms(500);
+
+  // We have to construct all the processes we want to fork before we start the
+  // service with |StartServiceIfRequired()|. this is because it is unsafe
+  // (could deadlock) to fork after we've spawned some threads which might
+  // printf (and thus hold locks).
+  const std::string path = RandomTraceFileName();
+  auto perfetto_proc = ExecPerfetto(
+      {
+          "-o",
+          path,
+          "-c",
+          "-",
+      },
+      trace_config.SerializeAsString());
+
+  std::string triggers = R"(
+    activate_triggers: "trigger_name"
+  )";
+  auto perfetto_proc_2 = ExecPerfetto(
+      {
+          "--upload",
+          "-c",
+          "-",
+          "--txt",
+      },
+      triggers);
+
+  // Start the service and connect a simple fake producer.
+  StartServiceIfRequiredNoNewExecsAfterThis();
+  auto* fake_producer = ConnectFakeProducer();
+  EXPECT_TRUE(fake_producer);
+
+  std::thread background_trace([&perfetto_proc]() {
+    std::string stderr_str;
+    EXPECT_EQ(0, perfetto_proc.Run(&stderr_str)) << stderr_str;
+  });
+
+  WaitForProducerEnabled();
+  // Wait for the producer to start, and then write out 11 packets, before the
+  // trace actually starts (the trigger is seen).
+  auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
+  fake_producer->ProduceEventBatch(WrapTask(on_data_written));
+  task_runner_.RunUntilCheckpoint("data_written_1");
+
+  EXPECT_EQ(0, perfetto_proc_2.Run(&stderr_)) << "stderr: " << stderr_;
+
+  background_trace.join();
+
+  std::string trace_str;
+  base::ReadFile(path, &trace_str);
+  protos::gen::Trace trace;
+  ASSERT_TRUE(trace.ParseFromString(trace_str));
+  EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
+  for (const auto& packet : trace.packet()) {
+    if (packet.has_trigger()) {
+      EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
+    }
+  }
+}
+
 }  // namespace perfetto
diff --git a/test/trace_processor/parsing/android_multiuser_switch.textproto b/test/trace_processor/parsing/android_multiuser_switch.textproto
index 4798139..d2a836c 100644
--- a/test/trace_processor/parsing/android_multiuser_switch.textproto
+++ b/test/trace_processor/parsing/android_multiuser_switch.textproto
@@ -1,11 +1,29 @@
 packet {
+  process_tree {
+    processes {
+      pid: 2609
+      ppid: 129
+      cmdline: "system_server"
+      uid: 1000
+    }
+  }
+  process_tree {
+    processes {
+      pid: 5993
+      ppid: 129
+      cmdline: "com.android.systemui"
+      uid: 10216
+    }
+  }
+}
+packet {
   ftrace_events {
     cpu: 3
     event {
       timestamp: 3000000000 # 3e9
       pid: 4064
       print {
-        buf: "S|1204|UserDetailView.Adapter#onClick|0\n"
+        buf: "S|5993|UserDetailView.Adapter#onClick|0\n"
       }
     }
   }
@@ -17,7 +35,31 @@
       timestamp: 3100000000
       pid: 4064
       print {
-        buf: "F|1204|UserDetailView.Adapter#onClick|0\n"
+        buf: "F|5993|UserDetailView.Adapter#onClick|0\n"
+      }
+    }
+  }
+}
+packet {
+  ftrace_events {
+    cpu: 2
+    event {
+      timestamp: 5186970000
+      pid: 4032
+      print {
+        buf: "S|2609|MetricsLogger:launchObserverNotifyIntentStarted|0\n"
+      }
+    }
+  }
+}
+packet {
+  ftrace_events {
+    cpu: 2
+    event {
+      timestamp: 5187000000
+      pid: 4032
+      print {
+        buf: "F|2609|MetricsLogger:launchObserverNotifyIntentStarted|0\n"
       }
     }
   }
@@ -29,7 +71,7 @@
       timestamp: 5200000000
       pid: 4065
       print {
-        buf: "S|1204|launching: com.google.android.apps.nexuslauncher|0\n"
+        buf: "S|2609|launching: com.google.android.apps.nexuslauncher|0\n"
       }
     }
   }
@@ -41,8 +83,32 @@
       timestamp: 7900000000 # 7.9e9
       pid: 4065
       print {
-        buf: "F|1204|launching: com.google.android.apps.nexuslauncher|0\n"
+        buf: "F|2609|launching: com.google.android.apps.nexuslauncher|0\n"
       }
     }
   }
-}
\ No newline at end of file
+}
+packet {
+  ftrace_events {
+    cpu: 2
+    event {
+      timestamp: 7900016000
+      pid: 4075
+      print {
+        buf: "S|2609|MetricsLogger:launchObserverNotifyActivityLaunchFinished|0\n"
+      }
+    }
+  }
+}
+packet {
+  ftrace_events {
+    cpu: 2
+    event {
+      timestamp: 7900516000
+      pid: 4075
+      print {
+        buf: "F|2609|MetricsLogger:launchObserverNotifyActivityLaunchFinished|0\n"
+      }
+    }
+  }
+}
diff --git a/test/trace_processor/parsing/atrace_compressed_sched_count.out b/test/trace_processor/parsing/atrace_compressed_sched_count.out
new file mode 100644
index 0000000..c03bd11
--- /dev/null
+++ b/test/trace_processor/parsing/atrace_compressed_sched_count.out
@@ -0,0 +1,2 @@
+"COUNT(1)"
+1120
diff --git a/test/trace_processor/parsing/atrace_uncompressed_sched_count.out b/test/trace_processor/parsing/atrace_uncompressed_sched_count.out
new file mode 100644
index 0000000..41c340b
--- /dev/null
+++ b/test/trace_processor/parsing/atrace_uncompressed_sched_count.out
@@ -0,0 +1,2 @@
+"COUNT(1)"
+9
diff --git a/test/trace_processor/parsing/display_time_unit_slices.out b/test/trace_processor/parsing/display_time_unit_slices.out
index 4343a9e..cee485b 100644
--- a/test/trace_processor/parsing/display_time_unit_slices.out
+++ b/test/trace_processor/parsing/display_time_unit_slices.out
@@ -1,2 +1,2 @@
 "ts","dur","name"
-1597071955492308000,211463000,"add_graph"
+-7794778920422990592,211463000000,"add_graph"
diff --git a/test/trace_processor/parsing/index b/test/trace_processor/parsing/index
index b586e28..ce6f074 100644
--- a/test/trace_processor/parsing/index
+++ b/test/trace_processor/parsing/index
@@ -163,3 +163,10 @@
 
 # Multiuser
 android_multiuser_switch.textproto android_multiuser android_multiuser_switch.out
+
+# Output of atrace -z.
+../../data/atrace_compressed.ctrace sched_smoke.sql atrace_compressed_sched_count.out
+
+# Output of adb shell "atrace -t 1 sched" > out.txt". It has extra garbage
+# coming from stderr before the TRACE: marker. See b/208691037.
+../../data/atrace_uncompressed_b_208691037 sched_smoke.sql atrace_uncompressed_sched_count.out
diff --git a/test/trace_processor/profiling/heap_stats_closest_proc.out b/test/trace_processor/profiling/heap_stats_closest_proc.out
index ddcdc95..1ee09bf 100644
--- a/test/trace_processor/profiling/heap_stats_closest_proc.out
+++ b/test/trace_processor/profiling/heap_stats_closest_proc.out
@@ -8,7 +8,9 @@
     samples {
       ts: 200000000
       heap_size: 64
+      heap_native_size: 0
       reachable_heap_size: 64
+      reachable_heap_native_size: 0
       obj_count: 1
       reachable_obj_count: 1
       anon_rss_and_swap_size: 2048
@@ -30,7 +32,9 @@
     samples {
       ts: 1500000000
       heap_size: 64
+      heap_native_size: 0
       reachable_heap_size: 64
+      reachable_heap_native_size: 0
       obj_count: 1
       reachable_obj_count: 1
       anon_rss_and_swap_size: 3072
@@ -41,4 +45,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/test/trace_processor/profiling/java_heap_stats.out b/test/trace_processor/profiling/java_heap_stats.out
index a4ff25c..4cecef7 100644
--- a/test/trace_processor/profiling/java_heap_stats.out
+++ b/test/trace_processor/profiling/java_heap_stats.out
@@ -8,7 +8,9 @@
     samples {
       ts: 10
       heap_size: 992
+      heap_native_size: 0
       reachable_heap_size: 352
+      reachable_heap_native_size: 0
       obj_count: 6
       reachable_obj_count: 3
       anon_rss_and_swap_size: 4096000
diff --git a/tools/batch_trace_processor/main.py b/tools/batch_trace_processor/main.py
index 36a4d53..843e5f1 100644
--- a/tools/batch_trace_processor/main.py
+++ b/tools/batch_trace_processor/main.py
@@ -23,46 +23,9 @@
 import pandas as pd
 import plotille
 
-from concurrent.futures import ThreadPoolExecutor
-from dataclasses import dataclass
-from perfetto.trace_processor import TraceProcessor
-from perfetto.trace_processor.api import TraceProcessorException
+from perfetto.trace_processor import TraceProcessorException
 
-
-@dataclass
-class TpArg:
-  shell_path: str
-  verbose: bool
-  file: str
-
-
-def create_tp_args(args, files):
-  return [TpArg(args.shell_path, args.verbose, f) for f in files]
-
-
-def create_tp(arg):
-  return TraceProcessor(
-      file_path=arg.file, bin_path=arg.shell_path, verbose=arg.verbose)
-
-
-def close_tp(tp):
-  tp.close()
-
-
-def query_single_result(tp, query):
-  df = tp.query(query).as_pandas_dataframe()
-  if len(df.index) != 1:
-    raise TraceProcessorException("Query should only return a single row")
-
-  if len(df.columns) != 1:
-    raise TraceProcessorException("Query should only return a single column")
-
-  return df.iloc[0, 0]
-
-
-def query_file_and_return_last(tp, queries_str):
-  queries = [q.strip() for q in queries_str.split(";\n")]
-  return [tp.query(q).as_pandas_dataframe() for q in queries if q][-1]
+from perfetto.batch_trace_processor.api import BatchTraceProcessor
 
 
 def prefix_path_column(path, df):
@@ -72,16 +35,14 @@
 
 class TpBatchShell(cmd.Cmd):
 
-  def __init__(self, executor, files, tps):
+  def __init__(self, files, batch_tp):
     super().__init__()
-    self.executor = executor
     self.files = files
-    self.tps = tps
+    self.batch_tp = batch_tp
 
   def do_histogram(self, arg):
     try:
-      data = list(
-          self.executor.map(lambda tp: query_single_result(tp, arg), self.tps))
+      data = self.batch_tp.query_single_result(arg)
       print(plotille.histogram(data))
       self.print_percentiles(data)
     except TraceProcessorException as ex:
@@ -89,8 +50,7 @@
 
   def do_vhistogram(self, arg):
     try:
-      data = list(
-          self.executor.map(lambda tp: query_single_result(tp, arg), self.tps))
+      data = self.batch_tp.query_single_result(arg)
       print(plotille.hist(data))
       self.print_percentiles(data)
     except TraceProcessorException as ex:
@@ -98,8 +58,7 @@
 
   def do_count(self, arg):
     try:
-      data = list(
-          self.executor.map(lambda tp: query_single_result(tp, arg), self.tps))
+      data = self.batch_tp.query_single_result(arg)
       counts = dict()
       for i in data:
         counts[i] = counts.get(i, 0) + 1
@@ -145,32 +104,28 @@
   if not files:
     logging.info("At least one file must be specified in files or file list")
 
-  executor = ThreadPoolExecutor()
-
   logging.info('Loading traces...')
-  tps = [tp for tp in executor.map(create_tp, create_tp_args(args, files))]
+  with BatchTraceProcessor(
+      files, bin_path=args.shell_path, verbose=args.verbose) as batch_tp:
+    if args.query_file:
+      logging.info('Running query file...')
 
-  if args.query_file:
-    logging.info('Running query file...')
+      with open(args.query_file, 'r') as f:
+        queries_str = f.read()
 
-    with open(args.query_file, 'r') as f:
-      query = f.read()
+      queries = [q.strip() for q in queries_str.split(";\n")]
+      out = [batch_tp.query(q) for q in queries if q][-1]
+      res = pd.concat(
+          [prefix_path_column(path, df) for (path, df) in zip(files, out)])
+      print(res.to_csv(index=False))
 
-    out = list(
-        executor.map(lambda tp: query_file_and_return_last(tp, query), tps))
-    res = pd.concat(
-        [prefix_path_column(path, df) for (path, df) in zip(files, out)])
-    print(res.to_csv(index=False))
+    if args.interactive or not args.query_file:
+      try:
+        TpBatchShell(files, batch_tp).cmdloop()
+      except KeyboardInterrupt:
+        pass
 
-  if args.interactive or not args.query_file:
-    try:
-      TpBatchShell(executor, files, tps).cmdloop()
-    except KeyboardInterrupt:
-      pass
-
-  logging.info("Closing; please wait...")
-  executor.map(close_tp, tps)
-  executor.shutdown()
+    logging.info("Closing; please wait...")
 
 
 if __name__ == '__main__':
diff --git a/tools/batch_trace_processor/perfetto/batch_trace_processor/__init__.py b/tools/batch_trace_processor/perfetto/batch_trace_processor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/batch_trace_processor/perfetto/batch_trace_processor/__init__.py
diff --git a/tools/batch_trace_processor/perfetto/batch_trace_processor/api.py b/tools/batch_trace_processor/perfetto/batch_trace_processor/api.py
new file mode 100644
index 0000000..f492641
--- /dev/null
+++ b/tools/batch_trace_processor/perfetto/batch_trace_processor/api.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+# Copyright (C) 2021 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.
+
+from concurrent.futures import ThreadPoolExecutor
+from dataclasses import dataclass
+from perfetto.trace_processor import TraceProcessor, TraceProcessorException
+
+
+@dataclass
+class TpArg:
+  bin_path: str
+  verbose: bool
+  file: str
+
+
+class BatchTraceProcessor:
+  """BatchTraceProcessor is the blessed way of running ad-hoc queries on
+  Python across many Perfetto traces.
+
+  Usage:
+    with BatchTraceProcessor(file_paths=files) as btp:
+      dfs = btp.query('select * from slice')
+      for df in dfs:
+        print(df)
+  """
+
+  def __init__(self, file_paths, bin_path=None, verbose=False):
+    """Creates a batch trace processor instance: the blessed way of running
+    ad-hoc queries on Python across many traces.
+
+    Args:
+      file_paths: List of trace file paths to load into this batch trace
+        processor instance.
+      bin_path: Optional path to a trace processor shell binary to use to
+        load the traces.
+      verbose: Optional flag indiciating whether verbose trace processor
+        output should be printed to stderr.
+    """
+    self.executor = ThreadPoolExecutor()
+    self.paths = file_paths
+
+    def create_tp(arg):
+      return TraceProcessor(
+          file_path=arg.file, bin_path=arg.bin_path, verbose=arg.verbose)
+
+    tp_args = [TpArg(bin_path, verbose, file) for file in file_paths]
+    self.tps = list(self.executor.map(create_tp, tp_args))
+
+  def query(self, sql):
+    """Executes the provided SQL statement in parallel across all the traces.
+
+    Args:
+      sql: The SQL statement to execute.
+
+    Returns:
+      A list of Pandas dataframes with the result of executing the query (one
+      per trace).
+
+    Raises:
+      TraceProcessorException: An error occurred running the query.
+    """
+    return self.__execute_on_tps(lambda tp: tp.query(sql).as_pandas_dataframe())
+
+  def query_single_result(self, sql):
+    """Executes the provided SQL statement (which should return a single row)
+    in parallel across all the traces.
+
+    Args:
+      sql: The SQL statement to execute. This statement should return exactly
+        one row on any trace.
+
+    Returns:
+      A list of values with the result of executing the query (one per ftrace).
+
+    Raises:
+      TraceProcessorException: An error occurred running the query or more than
+        one result was returned.
+    """
+
+    def query_single_result_inner(tp):
+      df = tp.query(sql).as_pandas_dataframe()
+      if len(df.index) != 1:
+        raise TraceProcessorException("Query should only return a single row")
+
+      if len(df.columns) != 1:
+        raise TraceProcessorException(
+            "Query should only return a single column")
+
+      return df.iloc[0, 0]
+
+    return self.__execute_on_tps(query_single_result_inner)
+
+  def close(self):
+    """Closes this batch trace processor instance: this closes all spawned
+    trace processor instances, releasing all the memory and resources those
+    instances take.
+
+    No further calls to other methods in this class should be made after
+    calling this method.
+    """
+    self.executor.map(lambda tp: tp.close(), self.tps)
+    self.executor.shutdown()
+
+  def __execute_on_tps(self, fn):
+    return list(self.executor.map(fn, self.tps))
+
+  def __enter__(self):
+    return self
+
+  def __exit__(self, _, __, ___):
+    self.close()
+    return False
diff --git a/tools/build_all_configs.py b/tools/build_all_configs.py
index 95fe044..0c0ec5d 100755
--- a/tools/build_all_configs.py
+++ b/tools/build_all_configs.py
@@ -100,6 +100,11 @@
   else:
     assert False, 'Unsupported system %r' % system
 
+  machine = platform.machine()
+  if machine == 'arm64':
+    for name, config in configs.items():
+      configs[name] = config + ('host_cpu="arm64"',)
+
   if args.ccache:
     for config_name, gn_args in iteritems(configs):
       configs[config_name] = gn_args + ('cc_wrapper="ccache"',)
diff --git a/tools/ftrace_proto_gen/event_list b/tools/ftrace_proto_gen/event_list
index a4193d4..a3378a2 100644
--- a/tools/ftrace_proto_gen/event_list
+++ b/tools/ftrace_proto_gen/event_list
@@ -353,3 +353,4 @@
 sde/sde_perf_crtc_update
 sde/sde_perf_set_qos_luts
 sde/sde_perf_update_bus
+synthetic/rss_stat_throttled
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 492d03d..e714cb8 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -296,7 +296,7 @@
 # This variable is updated by tools/roll-catapult-trace-viewer.
 CATAPULT_SHA256 = 'b30108e05268ce6c65bb4126b65f6bfac165d17f5c1fd285046e7e6fd76c209f'
 
-TYPEFACES_SHA256 = 'b3f0f14eeecd4555ae94f897ec246b2c6e046ce0ea417407553f5767e7812575'
+TYPEFACES_SHA256 = 'f5f78f8f4395db65cdf5fdc1bf51da65f2161e6d61a305091d6ea54d3094a1f0'
 
 UI_DEPS = [
     Dependency(
diff --git a/tools/run_python_api_tests.py b/tools/run_python_api_tests.py
index 789a1f0..78a78a1 100755
--- a/tools/run_python_api_tests.py
+++ b/tools/run_python_api_tests.py
@@ -22,6 +22,13 @@
 
 
 def main():
+  try:
+    import numpy
+    import pandas
+  except ModuleNotFoundError:
+    print('Cannot proceed. Please `pip3 install pandas numpy`', file=sys.stderr)
+    return 1
+
   # Append test and src paths so that all imports are loaded in correctly
   sys.path.append(os.path.join(ROOT_DIR, 'test', 'trace_processor', 'python'))
   sys.path.append(
diff --git a/tools/test_data b/tools/test_data
index 4aa2139..cf8ae14 100755
--- a/tools/test_data
+++ b/tools/test_data
@@ -136,7 +136,7 @@
     assert (fs.actual_digest is not None)
     dst_name = '%s/%s-%s' % (args.bucket, os.path.basename(
         fs.path), fs.actual_digest)
-    cmd = ['gsutil', '-q', 'cp', '-a', 'public-read', fs.path, dst_name]
+    cmd = ['gsutil', '-q', 'cp', '-n', '-a', 'public-read', fs.path, dst_name]
     logging.debug(' '.join(cmd))
     subprocess.check_call(cmd)
     with open(fs.path + SUFFIX + '.swp', 'w') as f:
diff --git a/ui/README.md b/ui/README.md
index 3fed469..a89b0f6 100644
--- a/ui/README.md
+++ b/ui/README.md
@@ -29,7 +29,7 @@
 Finally run:
 
 ```bash
-$ ./ui/run-dev-server out/debug
+$ ./ui/run-dev-server
 ```
 
 and navigate to `localhost:10000`.
diff --git a/ui/config/rollup.config.js b/ui/config/rollup.config.js
index 42f5afe..7a28ba4 100644
--- a/ui/config/rollup.config.js
+++ b/ui/config/rollup.config.js
@@ -65,6 +65,16 @@
       // Translate source maps to point back to the .ts sources.
       sourcemaps(),
     ],
+    onwarn: function(warning, warn) {
+      // Ignore circular dependency warnings coming from third party code.
+      if (warning.code === 'CIRCULAR_DEPENDENCY' &&
+          warning.importer.includes('node_modules')) {
+        return;
+      }
+
+      // Call the default warning handler for all remaining warnings.
+      warn(warning);
+    }
   };
 }
 
diff --git a/ui/release/channels.json b/ui/release/channels.json
index 5ee2472..359228c 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,11 +2,11 @@
   "channels": [
     {
       "name": "stable",
-      "rev": "6f7273f220d1812e315be627605be2561c617ed7"
+      "rev": "b4bd17f12d544c7b09cc93c6e7ed22e80ed73e48"
     },
     {
       "name": "canary",
-      "rev": "78d87a701cdcab64ca0937baed175d27ef38ef9c"
+      "rev": "9e3a964c585f3108b1ba2a1b58d4bb6ab96dc1ba"
     },
     {
       "name": "autopush",
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 3e36ee2..42a3460 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -70,7 +70,7 @@
 
 * {
     box-sizing: border-box;
-    -webkit-tap-highlight-color: none;
+    -webkit-tap-highlight-color: transparent;
     touch-action: none;
 }
 
@@ -91,6 +91,10 @@
     overscroll-behavior: none;
 }
 
+pre, code {
+  font-family: var(--monospace-font);
+}
+
 // This is to minimize Mac vs Linux Chrome Headless rendering differences
 // when running UI intergrationtests via puppeteer.
 body.testing {
@@ -155,17 +159,13 @@
 
 .full-page-loading-screen {
     position: absolute;
-    background: #3e4a5a;
     width: 100%;
     height: 100%;
     display: flex;
     justify-content: center;
     align-items: center;
     flex-direction: row;
-    background-image: url('assets/logo-3d.png');
-    background-attachment: fixed;
-    background-repeat: no-repeat;
-    background-position: center;
+    background: #3e4a5a url('assets/logo-3d.png') no-repeat fixed center;
 }
 
 .page {
@@ -484,7 +484,7 @@
 
 .debug-panel-border {
   position: absolute;
-  top: 0px;
+  top: 0;
   height: 100%;
   width: 100%;
   border: 1px solid rgba(69, 187, 73, 0.5);
@@ -498,7 +498,7 @@
   left: 0;
   width: 600px;
   color: var(--stroke-color);
-  font-family: monospace;
+  font-family: var(--monospace-font);
   padding: 10px 24px;
   z-index: 100;
   background-color: rgba(27, 28, 29, 0.90);
@@ -522,13 +522,13 @@
     border-bottom: 1px solid var(--stroke-color);
   }
   div {
-    margin: 2px 0px;
+    margin: 2px 0;
   }
   table, td, th {
     border: 1px solid var(--stroke-color);
     text-align: center;
     padding: 4px;
-    margin: 4px 0px;
+    margin: 4px 0;
   }
   table {
     border-collapse: collapse;
@@ -541,6 +541,7 @@
   --expanded-background: hsl(215, 22%, 19%);
   --expanded-transparent: hsl(215, 22%, 19%, 0);
   display: grid;
+  align-items: center;
   grid-template-columns: auto 1fr;
   grid-template-rows: 1fr;
   transition: background-color .4s, color .4s;
@@ -581,6 +582,7 @@
     align-items: center;
     line-height: 1;
     width: var(--track-shell-width);
+    min-height: 40px;
     transition: background-color .4s;
     h1 {
       grid-area: title;
@@ -605,6 +607,9 @@
       background-color: #ebeef9;
     }
   }
+  .track-content {
+    @include track_shell_title();
+  }
 }
 
 .time-selection-panel {
@@ -750,7 +755,7 @@
   margin-left: 10px;
   border-radius: 50%;
   border: 2px solid #408ee0;
-  border-color: #408ee0 transparent #408ee0 transparent;
+  border-color: #408ee0 transparent;
   animation: spinner 1.25s linear infinite;
   @keyframes spinner {
     0% { transform: rotate(0deg); }
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index 1545e4d..b7bfad8 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -35,10 +35,10 @@
       .tab {
         font-family: 'Roboto Condensed', sans-serif;
         color: #3c4b5d;
-        padding: 3px 10px 0px 10px;
+        padding: 3px 10px 0 10px;
         margin-top: 3px;
         font-size: 13px;
-        border-radius: 5px 5px 0px 0px;
+        border-radius: 5px 5px 0 0;
         background-color: hsla(0, 0%, 75%, 1);
         border-top: solid 1px hsla(0, 0%, 75%, 1);
         border-left: solid 1px hsla(0, 0%, 75%, 1);
@@ -120,7 +120,7 @@
   .details-panel-heading {
     padding: 10px 0 5px 0;
     position: sticky;
-    top: 0px;
+    top: 0;
     display: flex;
     background: white;
     &.aggregation {
@@ -176,7 +176,7 @@
       justify-content: space-between;
       align-content: center;
       height: 30px;
-      padding: 0px;
+      padding: 0;
       font-size: 12px;
       * {
         align-self: center;
@@ -193,7 +193,7 @@
         width: fit-content;
         height: 20px;
         padding: 3px;
-        padding-top: 0px;
+        padding-top: 0;
         margin: 2px;
         font-size: 12px;
         opacity: 0.5;
@@ -240,7 +240,7 @@
     // which is done by using fixed table layout.
     table-layout: fixed;
     word-wrap: break-word;
-    padding: 0px 10px;
+    padding: 0 10px;
     tr:hover {
       background-color: hsl(214, 22%, 90%);
     }
@@ -385,7 +385,7 @@
 
   header {
     position: sticky;
-    top: 0px;
+    top: 0;
     z-index: 1;
     background-color: white;
     color: #3c4b5d;
diff --git a/ui/src/assets/home_page.scss b/ui/src/assets/home_page.scss
index 8eed245..f76137c 100644
--- a/ui/src/assets/home_page.scss
+++ b/ui/src/assets/home_page.scss
@@ -86,7 +86,8 @@
         z-index: 2;
         text-transform: uppercase;
         font-size: 16px;
-        font-weight: 200;
+        font-family: 'Raleway';
+        font-weight: 400;
         letter-spacing: 0.3px;
       }
 
@@ -108,7 +109,7 @@
         opacity: 0;
         color: #da4534;
         font-weight: 400;
-        @include transition;
+        @include transition();
         &.show { opacity: 1; }
       }
     }  // .channel-select
diff --git a/ui/src/assets/modal.scss b/ui/src/assets/modal.scss
index 0f54b00..fe1bc87 100644
--- a/ui/src/assets/modal.scss
+++ b/ui/src/assets/modal.scss
@@ -94,15 +94,11 @@
   -moz-osx-font-smoothing: grayscale;
   -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
-  -webkit-transform: translateZ(0);
   transform: translateZ(0);
-  transition: -webkit-transform .25s ease-out;
   transition: transform .25s ease-out;
-  transition: transform .25s ease-out,-webkit-transform .25s ease-out;
 }
 
 .modal-btn:focus, .modal-btn:hover {
-  -webkit-transform: scale(1.05);
   transform: scale(1.05);
 }
 
@@ -195,7 +191,7 @@
   border: 1px solid #999;
   background: #eee;
   font-size: 10px;
-  font-family: monospace;
+  font-family: var(--monospace-font);
   -webkit-user-select: text;
   margin-top: 10px;
   margin-bottom: 10px;
diff --git a/ui/src/assets/record.scss b/ui/src/assets/record.scss
index 110920d..df6935d 100644
--- a/ui/src/assets/record.scss
+++ b/ui/src/assets/record.scss
@@ -90,12 +90,12 @@
 
         &:hover {
           background-color: hsl(88, 50%, 84%);
-          box-shadow: 0 0 4px 0px #999;
+          box-shadow: 0 0 4px 0 #999;
         }
 
         &.selected {
           background-color: hsl(88, 50%, 67%);
-          box-shadow: 0 0 4px 0px #999;
+          box-shadow: 0 0 4px 0 #999;
         }
 
         &.disabled {
@@ -116,7 +116,7 @@
       }
 
       label, select, button {
-        font-weight: 100;
+        font-weight: 300;
         margin: 3px;
         color: #333;
         font-size: 17px;
@@ -140,7 +140,7 @@
         padding: 4px;
         height: 30px;
         &:hover, &:active {
-          box-shadow: 0 0 4px 0px #ccc;
+          box-shadow: 0 0 4px 0 #ccc;
           background-color: #fafafa;
         }
         i {
@@ -290,7 +290,7 @@
 
     height: auto;
     width: 100%;
-    padding: 0px;
+    padding: 0;
     display: flex;
     align-items: center;
   }
@@ -317,7 +317,7 @@
     padding: 7px;
 
     &:hover:enabled {
-      box-shadow: 0 0 3px 0px #aaa;
+      box-shadow: 0 0 3px 0 #aaa;
     }
 
     &:not(:enabled) {
@@ -358,7 +358,7 @@
     margin-bottom: 20px;
     display: flex;
     align-items: center;
-    padding: 0px;
+    padding: 0;
 
     input {
       border-radius: 20px;
@@ -484,7 +484,7 @@
           height: 26px;
           border-radius: 100px;
           background: #f5f5f5;
-          box-shadow: 0px 3px 3px rgba(0,0,0,0.15);
+          box-shadow: 0 3px 3px rgba(0,0,0,0.15);
           content: '';
           transition: all 0.3s ease;
         }
@@ -582,7 +582,7 @@
           height: 20px;
           border-radius: 100px;
           background: #f5f5f5;
-          box-shadow: 0px 3px 3px rgba(0,0,0,0.15);
+          box-shadow: 0 3px 3px rgba(0,0,0,0.15);
           content: '';
           transition: all 0.3s ease;
         }
@@ -636,12 +636,12 @@
 
       &:hover {
         background-color: hsl(88, 50%, 84%);
-        box-shadow: 0 0 4px 0px #999;
+        box-shadow: 0 0 4px 0 #999;
       }
 
       &.selected {
         background-color: hsl(88, 50%, 67%);
-        box-shadow: 0 0 4px 0px #999;
+        box-shadow: 0 0 4px 0 #999;
       }
 
       img {
@@ -716,7 +716,7 @@
       width: 100%;
       appearance: none;
       -webkit-appearance: none;
-      scroll-snap-type: mandatory;
+      scroll-snap-type: x mandatory;
       background-color : transparent;
       outline: none;
       margin-left: -10px;
@@ -947,7 +947,7 @@
     }
 
     &.no-top-bar {
-      white-space: 'pre';
+      white-space: pre;
       &::before {
         height: 0;
       }
@@ -1054,12 +1054,12 @@
 
         &:hover {
           background-color: hsl(88, 50%, 84%);
-          box-shadow: 0 0 4px 0px #999;
+          box-shadow: 0 0 4px 0 #999;
         }
 
         &.selected {
           background-color: hsl(88, 50%, 67%);
-          box-shadow: 0 0 4px 0px #999;
+          box-shadow: 0 0 4px 0 #999;
         }
       }
     }
@@ -1076,7 +1076,7 @@
       background-color: hsl(88, 50%, 67%);
 
       &:hover {
-        box-shadow: 0 0 4px 0px #999;
+        box-shadow: 0 0 4px 0 #999;
       }
     }
 
@@ -1097,10 +1097,10 @@
   }
 }  // record-section
 
-.inline-chip { 
+.inline-chip {
   @include transition();
   &:hover, &:active {
-    box-shadow: 0 0 2px 0px #ccc;
+    box-shadow: 0 0 2px 0 #ccc;
     background-color: #fafafa;
   }
 
diff --git a/ui/src/assets/sidebar.scss b/ui/src/assets/sidebar.scss
index 7bd6dc9..db8428f 100644
--- a/ui/src/assets/sidebar.scss
+++ b/ui/src/assets/sidebar.scss
@@ -29,26 +29,28 @@
     }
     input[type=file] { display:none; }
     >header {
-        font-family: 'Raleway', sans-serif;
+        font-family: 'Roboto Condensed', sans-serif;
+        font-weight: 700;
+        font-size: 24px;
         height: var(--topbar-height);
         line-height: var(--topbar-height);
         vertical-align: middle;
         padding: 0 20px;
         color: #fff;
-        font-weight: 400;
-        font-size: 24px;
-        letter-spacing: 0.5px;
         overflow: visible;
         .brand {
           height: 40px;
           margin-top: 4px;
         }
+        &::before {
+          z-index: 10;
+        }
         &.canary::before, &.autopush::before {
           display: block;
           position: absolute;
           font-size: 10px;
           line-height: 10px;
-          font-family: 'Roboto', sans-serif;
+          font-family: 'Raleway', sans-serif;
           left: 155px;
           top: 7px;
         }
@@ -183,7 +185,7 @@
                         margin-left: 10px;
                         border-radius: 50%;
                         border: 2px solid #b4b7ba;
-                        border-color: #b4b7ba transparent #b4b7ba transparent;
+                        border-color: #b4b7ba transparent;
                         animation: pending-spinner 1.25s linear infinite;
                       }
                       @keyframes pending-spinner {
@@ -225,6 +227,7 @@
       height: - var(--sidebar-padding-bottom);
       grid-template-columns: repeat(4, min-content);
       grid-gap: 10px;
+      user-select: text;
 
       > button {
         color: hsl(217, 39%, 94%);
@@ -270,7 +273,7 @@
         right: 8px;
         bottom: 3px;
         font-size: 12px;
-        font-family: 'Roboto Condensed', 'Roboto', sans-serif;
+        font-family: 'Roboto Condensed', sans-serif;
         a {
           color: rgba(255, 255, 255, 0.5);
           text-decoration: none;
diff --git a/ui/src/assets/topbar.scss b/ui/src/assets/topbar.scss
index 4ab3907..27dd63c 100644
--- a/ui/src/assets/topbar.scss
+++ b/ui/src/assets/topbar.scss
@@ -22,7 +22,7 @@
     z-index: 3;
     overflow: visible;
     background-color: hsl(215, 1%, 95%);
-    box-shadow: 0px -3px 14px 2px #bbb;
+    box-shadow: 0 -3px 14px 2px #bbb;
     min-height: var(--topbar-height);
     display: flex;
     justify-content: center;
diff --git a/ui/src/assets/typefaces.scss b/ui/src/assets/typefaces.scss
index 46ad9b4..a8661b9 100644
--- a/ui/src/assets/typefaces.scss
+++ b/ui/src/assets/typefaces.scss
@@ -26,6 +26,42 @@
 
 /* latin */
 @font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 100;
+  src: url(assets/Roboto-100.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* latin */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 300;
+  src: url(assets/Roboto-300.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* latin */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 400;
+  src: url(assets/Roboto-400.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* latin */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 500;
+  src: url(assets/Roboto-500.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* latin */
+@font-face {
   font-family: 'Roboto Condensed';
   font-style: normal;
   font-weight: 300;
@@ -52,7 +88,6 @@
 }
 
 .material-icons {
-  font-display: block;
   font-family: 'Material Icons';
   font-weight: normal;
   font-style: normal;
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 4df3cb7..8ab5034 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -16,6 +16,7 @@
 
 import {assertExists, assertTrue} from '../base/logging';
 import {randomColor} from '../common/colorizer';
+import {RecordConfig} from '../controller/record_config_types';
 import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames/common';
 import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices/common';
 import {COUNTER_TRACK_KIND} from '../tracks/counter/common';
@@ -32,6 +33,7 @@
 } from '../tracks/process_scheduling/common';
 import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
 
+import {createEmptyState} from './empty_state';
 import {DEFAULT_VIEWING_OPTION, PERF_SAMPLES_KEY} from './flamegraph_util';
 import {
   AggregationAttrs,
@@ -43,14 +45,12 @@
   AdbRecordingTarget,
   Area,
   CallsiteInfo,
-  createEmptyState,
   EngineMode,
   FlamegraphStateViewingOption,
   LoadedConfig,
   LogsPagination,
   NewEngineMode,
   OmniboxState,
-  RecordConfig,
   RecordingTarget,
   SCROLLING_TRACK_GROUP,
   State,
@@ -83,6 +83,7 @@
   engineId: string;
   kind: string;
   name: string;
+  labels?: string[];
   trackKindPriority: TrackKindPriority;
   trackGroup?: string;
   config: {};
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index 85eaba8..53e8f65 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -23,8 +23,8 @@
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
 
 import {StateActions} from './actions';
+import {createEmptyState} from './empty_state';
 import {
-  createEmptyState,
   SCROLLING_TRACK_GROUP,
   State,
   TraceUrlSource,
diff --git a/ui/src/common/canvas_utils.ts b/ui/src/common/canvas_utils.ts
index 0835bad..aef36a0 100644
--- a/ui/src/common/canvas_utils.ts
+++ b/ui/src/common/canvas_utils.ts
@@ -72,10 +72,8 @@
     x: number,
     y: number,
     width: number,
-    height: number,
-    color: string) {
+    height: number) {
   ctx.beginPath();
-  ctx.fillStyle = color;
   const triangleSize = height / 4;
   ctx.moveTo(x, y);
   ctx.lineTo(x + width, y);
@@ -89,4 +87,4 @@
   ctx.lineTo(x + width, y + 4 * triangleSize);
   ctx.lineTo(x, y + height);
   ctx.fill();
-}
\ No newline at end of file
+}
diff --git a/ui/src/common/colorizer.ts b/ui/src/common/colorizer.ts
index 5ff5e78..0c30df8 100644
--- a/ui/src/common/colorizer.ts
+++ b/ui/src/common/colorizer.ts
@@ -43,7 +43,7 @@
   {c: 'yellow', h: 54, s: 100, l: 62},
 ];
 
-const GREY_COLOR: Color = {
+export const GRAY_COLOR: Color = {
   c: 'grey',
   h: 0,
   s: 0,
@@ -129,7 +129,7 @@
 
 export function colorForThread(thread?: {pid?: number, tid: number}): Color {
   if (thread === undefined) {
-    return Object.assign({}, GREY_COLOR);
+    return Object.assign({}, GRAY_COLOR);
   }
   const tid = thread.pid ? thread.pid : thread.tid;
   return colorForTid(tid);
@@ -172,3 +172,14 @@
   newLightness = Math.min(newLightness, 88);
   return [hue, saturation, newLightness];
 }
+
+export function colorToStr(color: Color) {
+  if (color.a !== undefined) {
+    return `hsla(${color.h}, ${color.s}%, ${color.l}%, ${color.a})`;
+  }
+  return `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
+}
+
+export function colorCompare(x: Color, y: Color) {
+  return (x.h - y.h) || (x.s - y.s) || (x.l - y.l);
+}
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
new file mode 100644
index 0000000..a3b1fdc
--- /dev/null
+++ b/ui/src/common/empty_state.ts
@@ -0,0 +1,106 @@
+// Copyright (C) 2021 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 {createEmptyRecordConfig} from '../controller/record_config_types';
+import {
+  autosaveConfigStore,
+  recordTargetStore
+} from '../frontend/record_config';
+
+import {featureFlags} from './feature_flags';
+import {defaultTraceTime, State, STATE_VERSION} from './state';
+
+const AUTOLOAD_STARTED_CONFIG_FLAG = featureFlags.register({
+  id: 'autoloadStartedConfig',
+  name: 'Auto-load last used recording config',
+  description: 'Starting a recording automatically saves its configuration. ' +
+      'This flag controls whether this config is automatically loaded.',
+  defaultValue: true,
+});
+
+export function createEmptyState(): State {
+  return {
+    version: STATE_VERSION,
+    nextId: 0,
+    nextNoteId: 1,  // 0 is reserved for ephemeral area marking.
+    nextAreaId: 0,
+    newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE',
+    engines: {},
+    traceTime: {...defaultTraceTime},
+    tracks: {},
+    uiTrackIdByTraceTrackId: new Map<number, string>(),
+    aggregatePreferences: {},
+    trackGroups: {},
+    visibleTracks: [],
+    pinnedTracks: [],
+    scrollingTracks: [],
+    areas: {},
+    queries: {},
+    metrics: {},
+    permalink: {},
+    notes: {},
+    pivotTableConfig: {},
+    pivotTable: {},
+
+    recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get() ?
+        autosaveConfigStore.get() :
+        createEmptyRecordConfig(),
+    displayConfigAsPbtxt: false,
+    lastLoadedConfig: {type: 'NONE'},
+
+    frontendLocalState: {
+      omniboxState: {
+        lastUpdate: 0,
+        omnibox: '',
+        mode: 'SEARCH',
+      },
+
+      visibleState: {
+        ...defaultTraceTime,
+        lastUpdate: 0,
+        resolution: 0,
+      },
+    },
+
+    logsPagination: {
+      offset: 0,
+      count: 0,
+    },
+
+    status: {msg: '', timestamp: 0},
+    currentSelection: null,
+    currentFlamegraphState: null,
+    traceConversionInProgress: false,
+
+    perfDebug: false,
+    sidebarVisible: true,
+    hoveredUtid: -1,
+    hoveredPid: -1,
+    hoveredLogsTimestamp: -1,
+    hoveredNoteTimestamp: -1,
+    highlightedSliceId: -1,
+    focusedFlowIdLeft: -1,
+    focusedFlowIdRight: -1,
+    searchIndex: -1,
+
+    recordingInProgress: false,
+    recordingCancelled: false,
+    extensionInstalled: false,
+    recordingTarget: recordTargetStore.getValidTarget(),
+    availableAdbDevices: [],
+
+    fetchChromeCategories: false,
+    chromeCategories: undefined,
+  };
+}
\ No newline at end of file
diff --git a/ui/src/common/pivot_table_query_generator.ts b/ui/src/common/pivot_table_query_generator.ts
index 1e55eb6..dd6f71d 100644
--- a/ui/src/common/pivot_table_query_generator.ts
+++ b/ui/src/common/pivot_table_query_generator.ts
@@ -121,7 +121,6 @@
   private generateJoinQuery(
       pivots: PivotAttrs[], aggregations: AggregationAttrs[],
       whereFilters: string[], joinTables: string[]): string {
-    let joinQuery = 'SELECT\n';
 
     const pivotCols = [];
     for (const pivot of pivots) {
@@ -147,15 +146,14 @@
           `${getSqlAggregationAlias(aggregation)}`);
     }
 
-    joinQuery += pivotCols.concat(aggCols).join(',\n  ');
-    joinQuery += '\n';
-    joinQuery += 'FROM\n';
-    joinQuery += joinTables.join(',\n  ');
-    joinQuery += '\n';
-    joinQuery += 'WHERE\n';
-    joinQuery += whereFilters.join(' AND\n  ');
-    joinQuery += '\n';
-    return joinQuery;
+    return `
+      SELECT
+        ${pivotCols.concat(aggCols).join(',\n  ')}
+      FROM
+        ${joinTables.join(',\n  ')}
+      WHERE
+        ${whereFilters.join(' AND\n  ')}
+    `;
   }
 
   // Partitions the aggregations from the subquery generateJoinQuery over
@@ -170,7 +168,6 @@
           pivots, aggregations, whereFilters, joinTables);
     }
 
-    let aggQuery = 'SELECT\n';
     const pivotCols = getSqlAliasedPivotColumns(pivots);
     let partitionByPivotCols = pivotCols;
     if (pivots.length > 0 && pivots[0].isStackPivot) {
@@ -211,13 +208,14 @@
           `${getSqlAggregationAlias(aggregation)}`);
     }
 
-    aggQuery += pivotCols.concat(aggCols).join(',\n  ');
-    aggQuery += '\n';
-    aggQuery += 'FROM (\n';
-    aggQuery +=
-        this.generateJoinQuery(pivots, aggregations, whereFilters, joinTables);
-    aggQuery += ')\n';
-    return aggQuery;
+    return `
+      SELECT
+        ${pivotCols.concat(aggCols).join(',\n  ')}
+      FROM (
+        ${
+        this.generateJoinQuery(pivots, aggregations, whereFilters, joinTables)}
+      )
+    `;
   }
 
   // Takes a list of pivots and aggregations and generates a query that
@@ -233,7 +231,6 @@
       return '';
     }
 
-    let query = '\nSELECT\n';
 
     const pivotCols = getSqlAliasedPivotColumns(pivots);
     const aggCols = getSqlAliasedAggregationsColumns(
@@ -241,19 +238,20 @@
         /* has_pivots_selected = */ pivots.length > 0,
         isStackQuery);
 
-    query += pivotCols.concat(aggCols).join(',\n  ');
-    query += '\n';
-    query += 'FROM (\n';
-    query += this.generateAggregationQuery(
-        pivots, aggregations, whereFilters, joinTables, isStackQuery);
-    query += ')\n';
-    query += 'GROUP BY ';
-
-    // Grouping by each pivot, additional pivots, and aggregations.
     const aggregationsGroupBy =
         aggregations.map(aggregation => getSqlAggregationAlias(aggregation));
-    query += pivotCols.concat(aggregationsGroupBy).join(',  ');
-    query += '\n';
+
+    let query = `
+      SELECT
+        ${pivotCols.concat(aggCols).join(',\n  ')}
+      FROM (
+        ${
+        this.generateAggregationQuery(
+            pivots, aggregations, whereFilters, joinTables, isStackQuery)}
+      )
+      GROUP BY
+        ${pivotCols.concat(aggregationsGroupBy).join(',  ')}
+    `;
 
     const pivotsOrderBy = [];
 
@@ -274,9 +272,10 @@
         aggregations.map(aggregation => orderString(aggregation));
 
     if (orderBy && pivotsOrderBy.length + aggregationsOrderBy.length > 0) {
-      query += 'ORDER BY ';
-      query += pivotsOrderBy.concat(aggregationsOrderBy).join(',  ');
-      query += '\n';
+      query += `
+        ORDER BY
+          ${pivotsOrderBy.concat(aggregationsOrderBy).join(',  ')}
+      `;
     }
     return query;
   }
diff --git a/ui/src/common/pivot_table_query_generator_unittest.ts b/ui/src/common/pivot_table_query_generator_unittest.ts
index 93681c7..42b95af 100644
--- a/ui/src/common/pivot_table_query_generator_unittest.ts
+++ b/ui/src/common/pivot_table_query_generator_unittest.ts
@@ -22,6 +22,16 @@
 
 const TABLES = ['slice'];
 
+// Normalize query for comparison by replacing repeated whitespace characters
+// with a single space.
+function normalize(s: string): string {
+  return s.replace(/\s+/g, ' ');
+}
+
+function expectQueryEqual(actual: string, expected: string) {
+  expect(normalize(actual)).toEqual(normalize(expected));
+}
+
 test('Generate query with pivots and aggregations', () => {
   const pivotTableQueryGenerator = new PivotTableQueryGenerator();
   const selectedPivots: PivotAttrs[] = [
@@ -31,34 +41,38 @@
   const selectedAggregations: AggregationAttrs[] = [
     {aggregation: 'SUM', tableName: 'slice', columnName: 'dur', order: 'DESC'}
   ];
-  const expectedQuery = '\nSELECT\n' +
-      '"slice type",\n' +
-      '  "slice id",\n' +
-      '  "slice dur (SUM)",\n' +
-      '  "slice dur (SUM) (total)"\n' +
-      'FROM (\n' +
-      'SELECT\n' +
-      '"slice type",\n' +
-      '  "slice id",\n' +
-      '  SUM("slice dur (SUM)") OVER () AS "slice dur (SUM) (total)",\n' +
-      '  SUM("slice dur (SUM)") OVER (PARTITION BY "slice type",  "slice id")' +
-      ' AS "slice dur (SUM)"\n' +
-      'FROM (\n' +
-      'SELECT\n' +
-      'slice.type AS "slice type",\n' +
-      '  slice.id AS "slice id",\n' +
-      '  slice.dur AS "slice dur (SUM)"\n' +
-      'FROM\n' +
-      'slice\n' +
-      'WHERE\n' +
-      'slice.dur != -1\n' +
-      ')\n' +
-      ')\n' +
-      'GROUP BY "slice type",  "slice id",  "slice dur (SUM)"\n' +
-      'ORDER BY "slice dur (SUM)" DESC\n';
-  expect(pivotTableQueryGenerator.generateQuery(
-             selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES))
-      .toEqual(expectedQuery);
+  const expectedQuery = `
+    SELECT
+      "slice type",
+      "slice id",
+      "slice dur (SUM)",
+      "slice dur (SUM) (total)"
+    FROM (
+      SELECT
+        "slice type",
+        "slice id",
+        SUM("slice dur (SUM)") OVER () AS "slice dur (SUM) (total)",
+        SUM("slice dur (SUM)")
+          OVER (PARTITION BY "slice type",  "slice id")
+          AS "slice dur (SUM)"
+      FROM (
+        SELECT
+          slice.type AS "slice type",
+          slice.id AS "slice id",
+          slice.dur AS "slice dur (SUM)"
+        FROM
+          slice
+        WHERE
+          slice.dur != -1
+      )
+    )
+    GROUP BY "slice type",  "slice id",  "slice dur (SUM)"
+    ORDER BY "slice dur (SUM)" DESC
+  `;
+  expectQueryEqual(
+      pivotTableQueryGenerator.generateQuery(
+          selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES),
+      expectedQuery);
 });
 
 test('Generate query with pivots', () => {
@@ -68,22 +82,25 @@
     {tableName: 'slice', columnName: 'id', isStackPivot: false}
   ];
   const selectedAggregations: AggregationAttrs[] = [];
-  const expectedQuery = '\nSELECT\n' +
-      '"slice type",\n' +
-      '  "slice id"\n' +
-      'FROM (\n' +
-      'SELECT\n' +
-      'slice.type AS "slice type",\n' +
-      '  slice.id AS "slice id"\n' +
-      'FROM\n' +
-      'slice\n' +
-      'WHERE\n' +
-      'slice.dur != -1\n' +
-      ')\n' +
-      'GROUP BY "slice type",  "slice id"\n';
-  expect(pivotTableQueryGenerator.generateQuery(
-             selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES))
-      .toEqual(expectedQuery);
+  const expectedQuery = `
+    SELECT
+      "slice type",
+      "slice id"
+    FROM (
+      SELECT
+        slice.type AS "slice type",
+        slice.id AS "slice id"
+      FROM
+        slice
+      WHERE
+        slice.dur != -1
+    )
+    GROUP BY "slice type",  "slice id"
+  `;
+  expectQueryEqual(
+      pivotTableQueryGenerator.generateQuery(
+          selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES),
+      expectedQuery);
 });
 
 test('Generate query with aggregations', () => {
@@ -93,28 +110,31 @@
     {aggregation: 'SUM', tableName: 'slice', columnName: 'dur', order: 'DESC'},
     {aggregation: 'MAX', tableName: 'slice', columnName: 'dur', order: 'ASC'}
   ];
-  const expectedQuery = '\nSELECT\n' +
-      '"slice dur (SUM)",\n' +
-      '  "slice dur (MAX)"\n' +
-      'FROM (\n' +
-      'SELECT\n' +
-      'SUM("slice dur (SUM)") AS "slice dur (SUM)",\n' +
-      '  MAX("slice dur (MAX)") AS "slice dur (MAX)"\n' +
-      'FROM (\n' +
-      'SELECT\n' +
-      'slice.dur AS "slice dur (SUM)",\n' +
-      '  slice.dur AS "slice dur (MAX)"\n' +
-      'FROM\n' +
-      'slice\n' +
-      'WHERE\n' +
-      'slice.dur != -1\n' +
-      ')\n' +
-      ')\n' +
-      'GROUP BY "slice dur (SUM)",  "slice dur (MAX)"\n' +
-      'ORDER BY "slice dur (SUM)" DESC,  "slice dur (MAX)" ASC\n';
-  expect(pivotTableQueryGenerator.generateQuery(
-             selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES))
-      .toEqual(expectedQuery);
+  const expectedQuery = `
+    SELECT
+      "slice dur (SUM)",
+      "slice dur (MAX)"
+    FROM (
+      SELECT
+        SUM("slice dur (SUM)") AS "slice dur (SUM)",
+        MAX("slice dur (MAX)") AS "slice dur (MAX)"
+      FROM (
+        SELECT
+          slice.dur AS "slice dur (SUM)",
+          slice.dur AS "slice dur (MAX)"
+        FROM
+          slice
+        WHERE
+          slice.dur != -1
+      )
+    )
+    GROUP BY "slice dur (SUM)",  "slice dur (MAX)"
+    ORDER BY "slice dur (SUM)" DESC,  "slice dur (MAX)" ASC
+  `;
+  expectQueryEqual(
+      pivotTableQueryGenerator.generateQuery(
+          selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES),
+      expectedQuery);
 });
 
 test('Generate a query with stack pivot', () => {
@@ -126,45 +146,52 @@
   const selectedAggregations: AggregationAttrs[] = [
     {aggregation: 'COUNT', tableName: 'slice', columnName: 'id', order: 'DESC'},
   ];
-  const expectedQuery = '\nSELECT\n' +
-      '"slice name (stack)",\n' +
-      '  "slice depth (hidden)",\n' +
-      '  "slice stack_id (hidden)",\n' +
-      '  "slice parent_stack_id (hidden)",\n' +
-      '  "slice category",\n' +
-      '  "slice id (COUNT)",\n' +
-      '  "slice id (COUNT) (total)"\n' +
-      'FROM (\n' +
-      'SELECT\n' +
-      '"slice name (stack)",\n' +
-      '  "slice depth (hidden)",\n' +
-      '  "slice stack_id (hidden)",\n' +
-      '  "slice parent_stack_id (hidden)",\n' +
-      '  "slice category",\n' +
-      '  COUNT("slice id (COUNT)") OVER () AS "slice id (COUNT) (total)",\n' +
-      '  COUNT("slice id (COUNT)") OVER (PARTITION BY' +
-      ' "slice stack_id (hidden)",  "slice category") AS "slice id (COUNT)"\n' +
-      'FROM (\n' +
-      'SELECT\n' +
-      'slice.name AS "slice name (stack)",\n' +
-      '  slice.depth AS "slice depth (hidden)",\n' +
-      '  slice.stack_id AS "slice stack_id (hidden)",\n' +
-      '  slice.parent_stack_id AS "slice parent_stack_id (hidden)",\n' +
-      '  slice.category AS "slice category",\n' +
-      '  slice.id AS "slice id (COUNT)"\n' +
-      'FROM\n' +
-      'slice\n' +
-      'WHERE\n' +
-      'slice.dur != -1\n' +
-      ')\n' +
-      ')\n' +
-      'GROUP BY "slice name (stack)",  "slice depth (hidden)",  ' +
-      '"slice stack_id (hidden)",  "slice parent_stack_id (hidden)",  ' +
-      '"slice category",  "slice id (COUNT)"\n' +
-      'ORDER BY "slice id (COUNT)" DESC\n';
-  expect(pivotTableQueryGenerator.generateQuery(
-             selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES))
-      .toEqual(expectedQuery);
+  const expectedQuery = `
+    SELECT
+      "slice name (stack)",
+      "slice depth (hidden)",
+      "slice stack_id (hidden)",
+      "slice parent_stack_id (hidden)",
+      "slice category",
+      "slice id (COUNT)",
+      "slice id (COUNT) (total)"
+    FROM (
+      SELECT
+        "slice name (stack)",
+        "slice depth (hidden)",
+        "slice stack_id (hidden)",
+        "slice parent_stack_id (hidden)",
+        "slice category",
+        COUNT("slice id (COUNT)") OVER () AS "slice id (COUNT) (total)",
+        COUNT("slice id (COUNT)")
+          OVER (PARTITION BY "slice stack_id (hidden)",  "slice category")
+          AS "slice id (COUNT)"
+      FROM (
+        SELECT
+          slice.name AS "slice name (stack)",
+          slice.depth AS "slice depth (hidden)",
+          slice.stack_id AS "slice stack_id (hidden)",
+          slice.parent_stack_id AS "slice parent_stack_id (hidden)",
+          slice.category AS "slice category",
+          slice.id AS "slice id (COUNT)"
+        FROM
+          slice
+        WHERE
+          slice.dur != -1
+      )
+    )
+    GROUP BY "slice name (stack)",
+             "slice depth (hidden)",
+             "slice stack_id (hidden)",
+             "slice parent_stack_id (hidden)",
+             "slice category",
+             "slice id (COUNT)"
+    ORDER BY "slice id (COUNT)" DESC
+  `;
+  expectQueryEqual(
+      pivotTableQueryGenerator.generateQuery(
+          selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES),
+      expectedQuery);
 });
 
 test('Generate a descendant stack query', () => {
@@ -173,34 +200,39 @@
     {tableName: 'slice', columnName: SLICE_STACK_COLUMN, isStackPivot: true},
   ];
   const selectedAggregations: AggregationAttrs[] = [];
-  const expectedQuery = '\nSELECT\n' +
-      '"slice name (stack)",\n' +
-      '  "slice depth (hidden)",\n' +
-      '  "slice stack_id (hidden)",\n' +
-      '  "slice parent_stack_id (hidden)"\n' +
-      'FROM (\n' +
-      'SELECT\n' +
-      'slice.name AS "slice name (stack)",\n' +
-      '  slice.depth AS "slice depth (hidden)",\n' +
-      '  slice.stack_id AS "slice stack_id (hidden)",\n' +
-      '  slice.parent_stack_id AS "slice parent_stack_id (hidden)"\n' +
-      'FROM\n' +
-      'descendant_slice_by_stack(stack_id) AS slice\n' +
-      'WHERE\n' +
-      'slice.dur != -1\n' +
-      ')\n' +
-      'GROUP BY "slice name (stack)",  "slice depth (hidden)",  ' +
-      '"slice stack_id (hidden)",  "slice parent_stack_id (hidden)"\n' +
-      'ORDER BY "slice depth (hidden)" ASC\n';
+  const expectedQuery = `
+    SELECT
+      "slice name (stack)",
+      "slice depth (hidden)",
+      "slice stack_id (hidden)",
+      "slice parent_stack_id (hidden)"
+    FROM (
+      SELECT
+        slice.name AS "slice name (stack)",
+        slice.depth AS "slice depth (hidden)",
+        slice.stack_id AS "slice stack_id (hidden)",
+        slice.parent_stack_id AS "slice parent_stack_id (hidden)"
+      FROM
+        descendant_slice_by_stack(stack_id) AS slice
+      WHERE
+        slice.dur != -1
+    )
+    GROUP BY "slice name (stack)",
+             "slice depth (hidden)",
+             "slice stack_id (hidden)",
+             "slice parent_stack_id (hidden)"
+    ORDER BY "slice depth (hidden)" ASC
+  `;
 
   const table = ['descendant_slice_by_stack(stack_id) AS slice'];
-  expect(pivotTableQueryGenerator.generateStackQuery(
-             selectedPivots,
-             selectedAggregations,
-             WHERE_FILTERS,
-             table,
-             /* stack_id = */ 'stack_id'))
-      .toEqual(expectedQuery);
+  expectQueryEqual(
+      pivotTableQueryGenerator.generateStackQuery(
+          selectedPivots,
+          selectedAggregations,
+          WHERE_FILTERS,
+          table,
+          /* stack_id = */ 'stack_id'),
+      expectedQuery);
 });
 
 test('Generate a descendant stack query with another pivot', () => {
@@ -210,58 +242,65 @@
     {tableName: 'slice', columnName: 'category', isStackPivot: false}
   ];
   const selectedAggregations: AggregationAttrs[] = [];
-  const expectedQuery = '\nSELECT\n' +
-      '"slice name (stack)",\n' +
-      '  "slice depth (hidden)",\n' +
-      '  "slice stack_id (hidden)",\n' +
-      '  "slice parent_stack_id (hidden)",\n' +
-      '  "slice category"\n' +
-      'FROM (\n' +
-      'SELECT\n' +
-      'slice.name AS "slice name (stack)",\n' +
-      '  slice.depth AS "slice depth (hidden)",\n' +
-      '  slice.stack_id AS "slice stack_id (hidden)",\n' +
-      '  slice.parent_stack_id AS "slice parent_stack_id (hidden)",\n' +
-      '  slice.category AS "slice category"\n' +
-      'FROM\n' +
-      'slice\n' +
-      'WHERE\n' +
-      'slice.dur != -1 AND\n' +
-      '  slice.stack_id = stack_id\n' +
-      ')\n' +
-      'GROUP BY "slice name (stack)",  "slice depth (hidden)",  ' +
-      '"slice stack_id (hidden)",  "slice parent_stack_id (hidden)",  ' +
-      '"slice category"\n' +
-      ' UNION ALL \n' +
-      'SELECT\n' +
-      '"slice name (stack)",\n' +
-      '  "slice depth (hidden)",\n' +
-      '  "slice stack_id (hidden)",\n' +
-      '  "slice parent_stack_id (hidden)",\n' +
-      '  "slice category"\n' +
-      'FROM (\n' +
-      'SELECT\n' +
-      'slice.name AS "slice name (stack)",\n' +
-      '  slice.depth AS "slice depth (hidden)",\n' +
-      '  slice.stack_id AS "slice stack_id (hidden)",\n' +
-      '  slice.parent_stack_id AS "slice parent_stack_id (hidden)",\n' +
-      '  slice.category AS "slice category"\n' +
-      'FROM\n' +
-      'descendant_slice_by_stack(stack_id) AS slice\n' +
-      'WHERE\n' +
-      'slice.dur != -1\n' +
-      ')\n' +
-      'GROUP BY "slice name (stack)",  "slice depth (hidden)",  ' +
-      '"slice stack_id (hidden)",  "slice parent_stack_id (hidden)",  ' +
-      '"slice category"\n' +
-      'ORDER BY "slice depth (hidden)" ASC\n';
+  const expectedQuery = `
+    SELECT
+      "slice name (stack)",
+      "slice depth (hidden)",
+      "slice stack_id (hidden)",
+      "slice parent_stack_id (hidden)",
+      "slice category"
+    FROM (
+      SELECT
+        slice.name AS "slice name (stack)",
+        slice.depth AS "slice depth (hidden)",
+        slice.stack_id AS "slice stack_id (hidden)",
+        slice.parent_stack_id AS "slice parent_stack_id (hidden)",
+        slice.category AS "slice category"
+      FROM
+        slice
+      WHERE
+        slice.dur != -1 AND
+        slice.stack_id = stack_id
+    )
+    GROUP BY "slice name (stack)",
+             "slice depth (hidden)",
+             "slice stack_id (hidden)",
+             "slice parent_stack_id (hidden)",
+             "slice category"
+    UNION ALL
+    SELECT
+      "slice name (stack)",
+      "slice depth (hidden)",
+      "slice stack_id (hidden)",
+      "slice parent_stack_id (hidden)",
+      "slice category"
+    FROM (
+      SELECT
+        slice.name AS "slice name (stack)",
+        slice.depth AS "slice depth (hidden)",
+        slice.stack_id AS "slice stack_id (hidden)",
+        slice.parent_stack_id AS "slice parent_stack_id (hidden)",
+        slice.category AS "slice category"
+      FROM
+        descendant_slice_by_stack(stack_id) AS slice
+      WHERE
+        slice.dur != -1
+    )
+    GROUP BY "slice name (stack)",
+             "slice depth (hidden)",
+             "slice stack_id (hidden)",
+             "slice parent_stack_id (hidden)",
+             "slice category"
+    ORDER BY "slice depth (hidden)" ASC
+  `;
 
   const table = ['descendant_slice_by_stack(stack_id) AS slice'];
-  expect(pivotTableQueryGenerator.generateStackQuery(
-             selectedPivots,
-             selectedAggregations,
-             WHERE_FILTERS,
-             table,
-             /* stack_id = */ 'stack_id'))
-      .toEqual(expectedQuery);
+  expectQueryEqual(
+      pivotTableQueryGenerator.generateStackQuery(
+          selectedPivots,
+          selectedAggregations,
+          WHERE_FILTERS,
+          table,
+          /* stack_id = */ 'stack_id'),
+      expectedQuery);
 });
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 4d0c3f7..865982f 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -12,13 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {createEmptyRecordConfig} from '../controller/validate_config';
-import {
-  autosaveConfigStore,
-  recordTargetStore
-} from '../frontend/record_config';
+import {RecordConfig} from '../controller/record_config_types';
 
-import {featureFlags} from './feature_flags';
 import {
   AggregationAttrs,
   PivotAttrs,
@@ -149,6 +144,7 @@
   engineId: string;
   kind: string;
   name: string;
+  labels?: string[];
   trackKindPriority: TrackKindPriority;
   trackGroup?: string;
   config: {};
@@ -280,6 +276,7 @@
     (NoteSelection|SliceSelection|CounterSelection|HeapProfileSelection|
      CpuProfileSampleSelection|ChromeSliceSelection|ThreadStateSelection|
      AreaSelection|PerfSamplesSelection)&{trackId?: string};
+export type SelectionKind = Selection['kind'];  // 'THREAD_STATE' | 'SLICE' ...
 
 export interface LogsPagination {
   offset: number;
@@ -481,87 +478,6 @@
   return config.chromeHighOverheadCategoriesSelected.length > 0;
 }
 
-export interface RecordConfig {
-  // Global settings
-  mode: RecordMode;
-  durationMs: number;
-  bufferSizeMb: number;
-  maxFileSizeMb: number;      // Only for mode == 'LONG_TRACE'.
-  fileWritePeriodMs: number;  // Only for mode == 'LONG_TRACE'.
-
-  cpuSched: boolean;
-  cpuFreq: boolean;
-  cpuCoarse: boolean;
-  cpuCoarsePollMs: number;
-  cpuSyscall: boolean;
-
-  gpuFreq: boolean;
-  gpuMemTotal: boolean;
-
-  ftrace: boolean;
-  atrace: boolean;
-  ftraceEvents: string[];
-  ftraceExtraEvents: string;
-  atraceCats: string[];
-  atraceApps: string;
-  ftraceBufferSizeKb: number;
-  ftraceDrainPeriodMs: number;
-  androidLogs: boolean;
-  androidLogBuffers: string[];
-  androidFrameTimeline: boolean;
-
-  batteryDrain: boolean;
-  batteryDrainPollMs: number;
-
-  boardSensors: boolean;
-
-  memHiFreq: boolean;
-  memLmk: boolean;
-  meminfo: boolean;
-  meminfoPeriodMs: number;
-  meminfoCounters: string[];
-  vmstat: boolean;
-  vmstatPeriodMs: number;
-  vmstatCounters: string[];
-
-  heapProfiling: boolean;
-  hpSamplingIntervalBytes: number;
-  hpProcesses: string;
-  hpContinuousDumpsPhase: number;
-  hpContinuousDumpsInterval: number;
-  hpSharedMemoryBuffer: number;
-  hpBlockClient: boolean;
-  hpAllHeaps: boolean;
-
-  javaHeapDump: boolean;
-  jpProcesses: string;
-  jpContinuousDumpsPhase: number;
-  jpContinuousDumpsInterval: number;
-
-  procStats: boolean;
-  procStatsPeriodMs: number;
-
-  chromeCategoriesSelected: string[];
-  chromeHighOverheadCategoriesSelected: string[];
-
-  chromeLogs: boolean;
-  taskScheduling: boolean;
-  ipcFlows: boolean;
-  jsExecution: boolean;
-  webContentRendering: boolean;
-  uiRendering: boolean;
-  inputEvents: boolean;
-  navigationAndLoading: boolean;
-
-  symbolizeKsyms: boolean;
-}
-
-export interface NamedRecordConfig {
-  title: string;
-  config: RecordConfig;
-  key: string;
-}
-
 export function getDefaultRecordingTargets(): RecordingTarget[] {
   return [
     {os: 'Q', name: 'Android Q+'},
@@ -817,90 +733,6 @@
   ];
 }
 
-const AUTOLOAD_STARTED_CONFIG_FLAG = featureFlags.register({
-  id: 'autoloadStartedConfig',
-  name: 'Auto-load last used recording config',
-  description: 'Starting a recording automatically saves its configuration. ' +
-      'This flag controls whether this config is automatically loaded.',
-  defaultValue: false,
-});
-
-export function createEmptyState(): State {
-  return {
-    version: STATE_VERSION,
-    nextId: 0,
-    nextNoteId: 1,  // 0 is reserved for ephemeral area marking.
-    nextAreaId: 0,
-    newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE',
-    engines: {},
-    traceTime: {...defaultTraceTime},
-    tracks: {},
-    uiTrackIdByTraceTrackId: new Map<number, string>(),
-    aggregatePreferences: {},
-    trackGroups: {},
-    visibleTracks: [],
-    pinnedTracks: [],
-    scrollingTracks: [],
-    areas: {},
-    queries: {},
-    metrics: {},
-    permalink: {},
-    notes: {},
-    pivotTableConfig: {},
-    pivotTable: {},
-
-    recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get() ?
-        autosaveConfigStore.get() :
-        createEmptyRecordConfig(),
-    displayConfigAsPbtxt: false,
-    lastLoadedConfig: {type: 'NONE'},
-
-    frontendLocalState: {
-      omniboxState: {
-        lastUpdate: 0,
-        omnibox: '',
-        mode: 'SEARCH',
-      },
-
-      visibleState: {
-        ...defaultTraceTime,
-        lastUpdate: 0,
-        resolution: 0,
-      },
-    },
-
-    logsPagination: {
-      offset: 0,
-      count: 0,
-    },
-
-    status: {msg: '', timestamp: 0},
-    currentSelection: null,
-    currentFlamegraphState: null,
-    traceConversionInProgress: false,
-
-    perfDebug: false,
-    sidebarVisible: true,
-    hoveredUtid: -1,
-    hoveredPid: -1,
-    hoveredLogsTimestamp: -1,
-    hoveredNoteTimestamp: -1,
-    highlightedSliceId: -1,
-    focusedFlowIdLeft: -1,
-    focusedFlowIdRight: -1,
-    searchIndex: -1,
-
-    recordingInProgress: false,
-    recordingCancelled: false,
-    extensionInstalled: false,
-    recordingTarget: recordTargetStore.getValidTarget(),
-    availableAdbDevices: [],
-
-    fetchChromeCategories: false,
-    chromeCategories: undefined,
-  };
-}
-
 export function getContainingTrackId(state: State, trackId: string): null|
     string {
   const track = state.tracks[trackId];
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
index 2f94a6e..046f864 100644
--- a/ui/src/common/state_unittest.ts
+++ b/ui/src/common/state_unittest.ts
@@ -12,12 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  createEmptyState,
-  getContainingTrackId,
-  State,
-  TrackKindPriority
-} from './state';
+import {createEmptyState} from './empty_state';
+import {getContainingTrackId, State, TrackKindPriority} from './state';
 
 test('createEmptyState', () => {
   const state: State = createEmptyState();
diff --git a/ui/src/common/upload_utils.ts b/ui/src/common/upload_utils.ts
index 03a9e39..d817084 100644
--- a/ui/src/common/upload_utils.ts
+++ b/ui/src/common/upload_utils.ts
@@ -12,9 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {RecordConfig} from '../controller/record_config_types';
+
 export const BUCKET_NAME = 'perfetto-ui-data';
 import * as uuidv4 from 'uuid/v4';
-import {State, RecordConfig} from './state';
+import {State} from './state';
 
 export async function saveTrace(trace: File|ArrayBuffer): Promise<string> {
   // TODO(hjd): This should probably also be a hash but that requires
diff --git a/ui/src/controller/globals.ts b/ui/src/controller/globals.ts
index f479f37..84564a8 100644
--- a/ui/src/controller/globals.ts
+++ b/ui/src/controller/globals.ts
@@ -16,8 +16,10 @@
 
 import {assertExists} from '../base/logging';
 import {DeferredAction} from '../common/actions';
-import {createEmptyState, State} from '../common/state';
+import {createEmptyState} from '../common/empty_state';
+import {State} from '../common/state';
 import {globals as frontendGlobals} from '../frontend/globals';
+
 import {ControllerAny} from './controller';
 
 export interface App {
diff --git a/ui/src/controller/permalink_controller.ts b/ui/src/controller/permalink_controller.ts
index 99b962c..cfb87dd 100644
--- a/ui/src/controller/permalink_controller.ts
+++ b/ui/src/controller/permalink_controller.ts
@@ -17,8 +17,9 @@
 import {assertExists} from '../base/logging';
 import {Actions} from '../common/actions';
 import {ConversionJobStatus} from '../common/conversion_jobs';
-import {createEmptyState, State} from '../common/state';
-import {RecordConfig, STATE_VERSION} from '../common/state';
+import {createEmptyState} from '../common/empty_state';
+import {State} from '../common/state';
+import {STATE_VERSION} from '../common/state';
 import {
   BUCKET_NAME,
   saveState,
@@ -30,7 +31,8 @@
 
 import {Controller} from './controller';
 import {globals} from './globals';
-import {JsonObject, runParser, validateRecordConfig} from './validate_config';
+import {RecordConfig, recordConfigValidator} from './record_config_types';
+import {runValidator} from './validators';
 
 export class PermalinkController extends Controller<'main'> {
   private lastRequestId?: string;
@@ -76,10 +78,9 @@
           if (PermalinkController.isRecordConfig(stateOrConfig)) {
             // This permalink state only contains a RecordConfig. Show the
             // recording page with the config, but keep other state as-is.
-            const validConfig = runParser(
-                                    validateRecordConfig,
-                                    stateOrConfig as unknown as JsonObject)
-                                    .result;
+            const validConfig =
+                runValidator(recordConfigValidator, stateOrConfig as unknown)
+                    .result;
             globals.dispatch(Actions.setRecordConfig({config: validConfig}));
             Router.navigate('#!/record');
             return;
diff --git a/ui/src/controller/record_config_types.ts b/ui/src/controller/record_config_types.ts
new file mode 100644
index 0000000..95df440
--- /dev/null
+++ b/ui/src/controller/record_config_types.ts
@@ -0,0 +1,111 @@
+// Copyright (C) 2021 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 {
+  oneOf,
+  num,
+  bool,
+  arrayOf,
+  str,
+  requiredStr,
+  record,
+  runValidator,
+  ValidatedType
+} from './validators';
+
+const recordModes = ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'] as const;
+export const recordConfigValidator = record({
+  mode: oneOf(recordModes, 'STOP_WHEN_FULL'),
+  durationMs: num(10000.0),
+  maxFileSizeMb: num(100),
+  fileWritePeriodMs: num(2500),
+  bufferSizeMb: num(64.0),
+
+  cpuSched: bool(),
+  cpuFreq: bool(),
+  cpuSyscall: bool(),
+
+  gpuFreq: bool(),
+  gpuMemTotal: bool(),
+
+  ftrace: bool(),
+  atrace: bool(),
+  ftraceEvents: arrayOf(str()),
+  ftraceExtraEvents: str(),
+  atraceCats: arrayOf(str()),
+  atraceApps: str(),
+  ftraceBufferSizeKb: num(2 * 1024),
+  ftraceDrainPeriodMs: num(250),
+  androidLogs: bool(),
+  androidLogBuffers: arrayOf(str()),
+  androidFrameTimeline: bool(),
+
+  cpuCoarse: bool(),
+  cpuCoarsePollMs: num(1000),
+
+  batteryDrain: bool(),
+  batteryDrainPollMs: num(1000),
+
+  boardSensors: bool(),
+
+  memHiFreq: bool(),
+  meminfo: bool(),
+  meminfoPeriodMs: num(1000),
+  meminfoCounters: arrayOf(str()),
+
+  vmstat: bool(),
+  vmstatPeriodMs: num(1000),
+  vmstatCounters: arrayOf(str()),
+
+  heapProfiling: bool(),
+  hpSamplingIntervalBytes: num(4096),
+  hpProcesses: str(),
+  hpContinuousDumpsPhase: num(),
+  hpContinuousDumpsInterval: num(),
+  hpSharedMemoryBuffer: num(8 * 1048576),
+  hpBlockClient: bool(true),
+  hpAllHeaps: bool(),
+
+  javaHeapDump: bool(),
+  jpProcesses: str(),
+  jpContinuousDumpsPhase: num(),
+  jpContinuousDumpsInterval: num(),
+
+  memLmk: bool(),
+  procStats: bool(),
+  procStatsPeriodMs: num(1000),
+
+  chromeCategoriesSelected: arrayOf(str()),
+  chromeHighOverheadCategoriesSelected: arrayOf(str()),
+
+  chromeLogs: bool(),
+  taskScheduling: bool(),
+  ipcFlows: bool(),
+  jsExecution: bool(),
+  webContentRendering: bool(),
+  uiRendering: bool(),
+  inputEvents: bool(),
+  navigationAndLoading: bool(),
+
+  symbolizeKsyms: bool(),
+});
+export const namedRecordConfigValidator = record(
+    {title: requiredStr, key: requiredStr, config: recordConfigValidator});
+export type NamedRecordConfig =
+    ValidatedType<typeof namedRecordConfigValidator>;
+export type RecordConfig = ValidatedType<typeof recordConfigValidator>;
+
+export function createEmptyRecordConfig(): RecordConfig {
+  return runValidator(recordConfigValidator, {}).result;
+}
diff --git a/ui/src/controller/record_controller.ts b/ui/src/controller/record_controller.ts
index 4518e87..0868940 100644
--- a/ui/src/controller/record_controller.ts
+++ b/ui/src/controller/record_controller.ts
@@ -43,7 +43,7 @@
   isAndroidP,
   isChromeTarget,
   isCrOSTarget,
-  RecordConfig,
+  isLinuxTarget,
   RecordingTarget
 } from '../common/state';
 import {publishBufferUsage, publishTrackData} from '../frontend/publish';
@@ -63,6 +63,7 @@
 } from './consumer_port_types';
 import {Controller} from './controller';
 import {App, globals} from './globals';
+import {RecordConfig} from './record_config_types';
 import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
 
 type RPCImplMethod = (Method|rpc.ServiceMethod<Message<{}>, Message<{}>>);
@@ -164,15 +165,19 @@
   if (uiCfg.batteryDrain) {
     const ds = new TraceConfig.DataSource();
     ds.config = new DataSourceConfig();
-    ds.config.name = 'android.power';
-    ds.config.androidPowerConfig = new AndroidPowerConfig();
-    ds.config.androidPowerConfig.batteryPollMs = uiCfg.batteryDrainPollMs;
-    ds.config.androidPowerConfig.batteryCounters = [
-      AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CAPACITY_PERCENT,
-      AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
-      AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
-    ];
-    ds.config.androidPowerConfig.collectPowerRails = true;
+    if (isCrOSTarget(target) || isLinuxTarget(target)) {
+      ds.config.name = 'linux.sysfs_power';
+    } else {
+      ds.config.name = 'android.power';
+      ds.config.androidPowerConfig = new AndroidPowerConfig();
+      ds.config.androidPowerConfig.batteryPollMs = uiCfg.batteryDrainPollMs;
+      ds.config.androidPowerConfig.batteryCounters = [
+        AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CAPACITY_PERCENT,
+        AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
+        AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
+      ];
+      ds.config.androidPowerConfig.collectPowerRails = true;
+    }
     if (!isChromeTarget(target) || isCrOSTarget(target)) {
       protoCfg.dataSources.push(ds);
     }
diff --git a/ui/src/controller/record_controller_jsdomtest.ts b/ui/src/controller/record_controller_jsdomtest.ts
index 09916a2..d263cf0 100644
--- a/ui/src/controller/record_controller_jsdomtest.ts
+++ b/ui/src/controller/record_controller_jsdomtest.ts
@@ -15,8 +15,8 @@
 import {assertExists} from '../base/logging';
 import {TraceConfig} from '../common/protos';
 
+import {createEmptyRecordConfig} from './record_config_types';
 import {genConfigProto, toPbtxt} from './record_controller';
-import {createEmptyRecordConfig} from './validate_config';
 
 test('encodeConfig', () => {
   const config = createEmptyRecordConfig();
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 3726947..82f6e22 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -416,10 +416,22 @@
     }
     // Find the sched slice with the utid of the waker running when the
     // sched wakeup occurred. This is the waker.
-    const queryWaker = `select utid, cpu from sched where utid =
-    (select utid from raw where name = '${event}' and ts = ${wakeupTs})
+    let queryWaker = `select utid, cpu from sched where utid =
+    (select EXTRACT_ARG(arg_set_id, 'waker_utid') from instants where name =
+     '${event}' and ts = ${wakeupTs})
     and ts < ${wakeupTs} and ts + dur >= ${wakeupTs};`;
-    const wakerResult = await this.args.engine.query(queryWaker);
+    let wakerResult = await this.args.engine.query(queryWaker);
+    if (wakerResult.numRows() === 0) {
+      // An old version of trace processor (that does not populate the
+      // 'waker_utid' arg) might be in use. Try getting the same info from the
+      // raw table).
+      // TODO(b/206390308): Remove this workaround when
+      // TRACE_PROCESSOR_CURRENT_API_VERSION is incremented.
+      queryWaker = `select utid, cpu from sched where utid =
+      (select utid from raw where name = '${event}' and ts = ${wakeupTs})
+      and ts < ${wakeupTs} and ts + dur >= ${wakeupTs};`;
+      wakerResult =  await this.args.engine.query(queryWaker);
+    }
     if (wakerResult.numRows() === 0) {
       return undefined;
     }
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 74576b0..1686210 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -616,7 +616,8 @@
         id INTEGER PRIMARY KEY,
         name STRING,
         __metric_name STRING,
-        upid INTEGER
+        upid INTEGER,
+        group_name STRING
       );
     `);
 
@@ -672,6 +673,7 @@
         let hasDur = false;
         let hasUpid = false;
         let hasValue = false;
+        let hasGroupName = false;
         const it = result.iter({name: STR});
         for (; it.valid(); it.next()) {
           const name = it.name;
@@ -679,17 +681,22 @@
           hasDur = hasDur || name === 'dur';
           hasUpid = hasUpid || name === 'upid';
           hasValue = hasValue || name === 'value';
+          hasGroupName = hasGroupName || name === 'group_name';
         }
 
         const upidColumnSelect = hasUpid ? 'upid' : '0 AS upid';
         const upidColumnWhere = hasUpid ? 'upid' : '0';
+        const groupNameColumn =
+            hasGroupName ? 'group_name' : 'NULL AS group_name';
         if (hasSliceName && hasDur) {
           await engine.query(`
-            INSERT INTO annotation_slice_track(name, __metric_name, upid)
+            INSERT INTO annotation_slice_track(
+              name, __metric_name, upid, group_name)
             SELECT DISTINCT
               track_name,
               '${metric}' as metric_name,
-              ${upidColumnSelect}
+              ${upidColumnSelect},
+              ${groupNameColumn}
             FROM ${metric}_event
             WHERE track_type = 'slice'
           `);
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index dbb7bb6..f1765fc 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -21,7 +21,7 @@
   DeferredAction,
 } from '../common/actions';
 import {Engine} from '../common/engine';
-import {PERF_SAMPLE_FLAG} from '../common/feature_flags';
+import {featureFlags, PERF_SAMPLE_FLAG} from '../common/feature_flags';
 import {
   NUM,
   NUM_NULL,
@@ -50,6 +50,13 @@
 import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
 
+const TRACKS_V2_FLAG = featureFlags.register({
+  id: 'tracksV2',
+  name: 'Tracks V2',
+  description: 'Show tracks built on top of the Track V2 API.',
+  defaultValue: false,
+});
+
 const MEM_DMA_COUNTER_NAME = 'mem.dma_heap';
 const MEM_DMA = 'mem.dma_buffer';
 const MEM_ION = 'mem.ion';
@@ -420,25 +427,56 @@
 
   async addAnnotationTracks(): Promise<void> {
     const sliceResult = await this.engine.query(`
-    SELECT id, name, upid FROM annotation_slice_track`);
+    SELECT id, name, upid, group_name FROM annotation_slice_track`);
 
     const sliceIt = sliceResult.iter({
       id: NUM,
       name: STR,
       upid: NUM,
+      group_name: STR_NULL,
     });
 
+    interface GroupIds {
+      id: string;
+      summaryTrackId: string;
+    }
+
+    const groupNameToIds = new Map<string, GroupIds>();
+
     for (; sliceIt.valid(); sliceIt.next()) {
       const id = sliceIt.id;
       const name = sliceIt.name;
       const upid = sliceIt.upid;
+      const groupName = sliceIt.group_name;
+
+      let trackId = undefined;
+      let trackGroupId =
+          upid === 0 ? SCROLLING_TRACK_GROUP : this.upidToUuid.get(upid);
+
+      if (groupName) {
+        // If this is the first track encountered for a certain group,
+        // create an id for the group and use this track as the group's
+        // summary track.
+        const groupIds = groupNameToIds.get(groupName);
+        if (groupIds) {
+          trackGroupId = groupIds.id;
+        } else {
+          trackGroupId = uuidv4();
+          trackId = uuidv4();
+          groupNameToIds.set(groupName, {
+            id: trackGroupId,
+            summaryTrackId: trackId,
+          });
+        }
+      }
+
       this.tracksToAdd.push({
+        id: trackId,
         engineId: this.engineId,
         kind: SLICE_TRACK_KIND,
         name,
         trackKindPriority: TrackDecider.inferTrackKindPriority(name),
-        trackGroup: upid === 0 ? SCROLLING_TRACK_GROUP :
-                                 this.upidToUuid.get(upid),
+        trackGroup: trackGroupId,
         config: {
           maxDepth: 0,
           namespace: 'annotation',
@@ -447,6 +485,17 @@
       });
     }
 
+    for (const [groupName, groupIds] of groupNameToIds) {
+      const addGroup = Actions.addTrackGroup({
+        engineId: this.engineId,
+        summaryTrackId: groupIds.summaryTrackId,
+        name: groupName,
+        id: groupIds.id,
+        collapsed: true,
+      });
+      this.addTrackGroupActions.push(addGroup);
+    }
+
     const counterResult = await this.engine.query(`
     SELECT
       id,
@@ -869,6 +918,17 @@
         trackKindPriority,
         config: {trackId, maxDepth, tid, isThreadSlice: onlyThreadSlice === 1}
       });
+
+      if (TRACKS_V2_FLAG.get()) {
+        this.tracksToAdd.push({
+          engineId: this.engineId,
+          kind: 'GenericSliceTrack',
+          name,
+          trackGroup: uuid,
+          trackKindPriority,
+          config: {sqlTrackId: trackId},
+        });
+      }
     }
   }
 
@@ -1003,7 +1063,8 @@
       process.pid as pid,
       thread.tid as tid,
       process.name as processName,
-      thread.name as threadName
+      thread.name as threadName,
+      process.arg_set_id as argSetId
     from (
       select upid, 0 as utid from process_track
       union
@@ -1065,6 +1126,7 @@
       processName: STR_NULL,
       hasSched: NUM_NULL,
       hasHeapProfiles: NUM_NULL,
+      argSetId: NUM_NULL
     });
     for (; it.valid(); it.next()) {
       const utid = it.utid;
@@ -1076,6 +1138,21 @@
       const hasSched = !!it.hasSched;
       const hasHeapProfiles = !!it.hasHeapProfiles;
 
+      const labels = [];
+      if (it.argSetId !== null) {
+        const result = await this.engine.query(`
+          select string_value as label
+          from args
+          where arg_set_id = ${it.argSetId}
+        `);
+        const argIt = result.iter({label: STR_NULL});
+        for (; argIt.valid(); argIt.next()) {
+          if (argIt.label !== null) {
+            labels.push(argIt.label);
+          }
+        }
+      }
+
       // Group by upid if present else by utid.
       let pUuid =
           upid === null ? this.utidToUuid.get(utid) : this.upidToUuid.get(upid);
@@ -1095,6 +1172,7 @@
           trackKindPriority: TrackDecider.inferTrackKindPriority(threadName),
           name: `${upid === null ? tid : pid} summary`,
           config: {pidForColor, upid, utid, tid},
+          labels,
         });
 
         const name = TrackDecider.getTrackName(
@@ -1123,6 +1201,7 @@
     await this.addGpuFreqTracks();
     await this.addGlobalCounterTracks();
     await this.addCpuPerfCounterTracks();
+    await this.addAnnotationTracks();
     await this.groupGlobalIonTracks();
 
     // Create the per-process track groups. Note that this won't necessarily
@@ -1144,7 +1223,6 @@
     await this.addThreadSliceTracks();
     await this.addThreadCpuSampleTracks();
     await this.addLogsTrack();
-    await this.addAnnotationTracks();
 
     this.addTrackGroupActions.push(
         Actions.addTracks({tracks: this.tracksToAdd}));
diff --git a/ui/src/controller/validate_config.ts b/ui/src/controller/validate_config.ts
deleted file mode 100644
index 0d9e81e..0000000
--- a/ui/src/controller/validate_config.ts
+++ /dev/null
@@ -1,279 +0,0 @@
-// Copyright (C) 2020 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 {NamedRecordConfig, RecordConfig, RecordMode} from '../common/state';
-
-type Json = JsonObject|Json[]|null|number|boolean|string;
-
-export interface JsonObject {
-  [key: string]: Json;
-}
-
-class ObjectValidator {
-  raw: JsonObject;
-  invalidKeys: string[];
-  prefix: string;
-
-  constructor(raw: JsonObject, prefix?: string, invalidKeys?: string[]) {
-    this.raw = raw;
-    this.prefix = prefix || '';
-    this.invalidKeys = invalidKeys || [];
-  }
-
-  private reportInvalidKey(key: string) {
-    this.invalidKeys.push(this.prefix + key);
-  }
-
-  number(key: string, def = 0): number {
-    if (!(key in this.raw)) {
-      return def;
-    }
-
-    const val = this.raw[key];
-    delete this.raw[key];
-    if (typeof val === 'number') {
-      return val;
-    } else {
-      this.reportInvalidKey(key);
-      return def;
-    }
-  }
-
-  string(key: string, def = ''): string {
-    if (!(key in this.raw)) {
-      return def;
-    }
-
-    const val = this.raw[key];
-    delete this.raw[key];
-    if (typeof val === 'string') {
-      return val;
-    } else {
-      this.reportInvalidKey(key);
-      return def;
-    }
-  }
-
-  requiredString(key: string): string {
-    if (!(key in this.raw)) {
-      throw new Error(`key ${this.prefix + key} not found`);
-    }
-
-    const val = this.raw[key];
-    delete this.raw[key];
-    if (typeof val === 'string') {
-      return val;
-    } else {
-      throw new Error(`key ${this.prefix + key} not found`);
-    }
-  }
-
-  stringArray(key: string, def: string[] = []): string[] {
-    if (!(key in this.raw)) {
-      return def;
-    }
-
-    const val = this.raw[key];
-    delete this.raw[key];
-    if (Array.isArray(val)) {
-      for (let i = 0; i < val.length; i++) {
-        if (typeof val[i] !== 'string') {
-          this.reportInvalidKey(key);
-          return def;
-        }
-      }
-      return val as string[];
-    } else {
-      this.reportInvalidKey(key);
-      return def;
-    }
-  }
-
-  boolean(key: string, def = false): boolean {
-    if (!(key in this.raw)) {
-      return def;
-    }
-
-    const val = this.raw[key];
-    delete this.raw[key];
-    if (typeof val === 'boolean') {
-      return val;
-    } else {
-      this.reportInvalidKey(key);
-      return def;
-    }
-  }
-
-  recordMode(key: string, def: RecordMode): RecordMode {
-    if (!(key in this.raw)) {
-      return def;
-    }
-
-    const mode = this.raw[key];
-    delete this.raw[key];
-    if (typeof mode !== 'string') {
-      this.reportInvalidKey(key);
-      return def;
-    }
-
-    if (mode === 'STOP_WHEN_FULL') {
-      return mode;
-    } else if (mode === 'RING_BUFFER') {
-      return mode;
-    } else if (mode === 'LONG_TRACE') {
-      return mode;
-    } else {
-      this.reportInvalidKey(key);
-      return def;
-    }
-  }
-
-  private childObject(key: string): JsonObject {
-    if (!(key in this.raw)) {
-      return {};
-    }
-
-    const result = this.raw[key];
-    delete this.raw[key];
-    if (typeof result === 'object' && !Array.isArray(result) &&
-        result !== null) {
-      return result;
-    } else {
-      this.reportInvalidKey(key);
-      return {};
-    }
-  }
-
-  object(key: string): ObjectValidator {
-    return new ObjectValidator(
-        this.childObject(key), key + '.', this.invalidKeys);
-  }
-}
-
-export interface ValidationResult<T> {
-  result: T;
-  invalidKeys: string[];
-  extraKeys: string[];
-}
-
-// Run the parser that takes raw JSON and outputs (potentially reconstructed
-// with default values) typed object, together with additional information,
-// such as fields with invalid values and extraneous keys.
-//
-// Parsers modify input JSON objects destructively.
-export function runParser<T>(
-    parser: (validator: ObjectValidator) => T,
-    input: JsonObject): ValidationResult<T> {
-  const validator = new ObjectValidator(input);
-  const valid = parser(validator);
-
-  return {
-    result: valid,
-    // Validator removes all the parsed keys, therefore the only ones that
-    // remain are extraneous.
-    extraKeys: Object.keys(input),
-    invalidKeys: validator.invalidKeys
-  };
-}
-
-export function validateNamedRecordConfig(v: ObjectValidator):
-    NamedRecordConfig {
-  return {
-    title: v.requiredString('title'),
-    config: validateRecordConfig(v.object('config')),
-    key: v.requiredString('key')
-  };
-}
-
-export function validateRecordConfig(v: ObjectValidator): RecordConfig {
-  return {
-    mode: v.recordMode('mode', 'STOP_WHEN_FULL'),
-    durationMs: v.number('durationMs', 10000.0),
-    maxFileSizeMb: v.number('maxFileSizeMb', 100),
-    fileWritePeriodMs: v.number('fileWritePeriodMs', 2500),
-    bufferSizeMb: v.number('bufferSizeMb', 64.0),
-
-    cpuSched: v.boolean('cpuSched'),
-    cpuFreq: v.boolean('cpuFreq'),
-    cpuSyscall: v.boolean('cpuSyscall'),
-
-    gpuFreq: v.boolean('gpuFreq'),
-    gpuMemTotal: v.boolean('gpuMemTotal'),
-
-    ftrace: v.boolean('ftrace'),
-    atrace: v.boolean('atrace'),
-    ftraceEvents: v.stringArray('ftraceEvents'),
-    ftraceExtraEvents: v.string('ftraceExtraEvents'),
-    atraceCats: v.stringArray('atraceCats'),
-    atraceApps: v.string('atraceApps'),
-    ftraceBufferSizeKb: v.number('ftraceBufferSizeKb', 2 * 1024),
-    ftraceDrainPeriodMs: v.number('ftraceDrainPerionMs', 250),
-    androidLogs: v.boolean('androidLogs'),
-    androidLogBuffers: v.stringArray('androidLogBuffers'),
-    androidFrameTimeline: v.boolean('androidFrameTimeline'),
-
-    cpuCoarse: v.boolean('cpuCoarse'),
-    cpuCoarsePollMs: v.number('cpuCoarsePollMs', 1000),
-
-    batteryDrain: v.boolean('batteryDrain'),
-    batteryDrainPollMs: v.number('batteryDrainPollMs', 1000),
-
-    boardSensors: v.boolean('boardSensors'),
-
-    memHiFreq: v.boolean('memHiFreq'),
-    meminfo: v.boolean('meminfo'),
-    meminfoPeriodMs: v.number('meminfoPeriodMs', 1000),
-    meminfoCounters: v.stringArray('meminfoCounters'),
-
-    vmstat: v.boolean('vmstat'),
-    vmstatPeriodMs: v.number('vmstatPeriodMs', 1000),
-    vmstatCounters: v.stringArray('vmstatCounters'),
-
-    heapProfiling: v.boolean('heapProfiling'),
-    hpSamplingIntervalBytes: v.number('hpSamplingIntervalBytes', 4096),
-    hpProcesses: v.string('hpProcesses'),
-    hpContinuousDumpsPhase: v.number('hpContinuousDumpsPhase'),
-    hpContinuousDumpsInterval: v.number('hpContinuousDumpsInterval'),
-    hpSharedMemoryBuffer: v.number('hpSharedMemoryBuffer', 8 * 1048576),
-    hpBlockClient: v.boolean('hpBlockClient', true),
-    hpAllHeaps: v.boolean('hpAllHeaps'),
-
-    javaHeapDump: v.boolean('javaHeapDump'),
-    jpProcesses: v.string('jpProcesses'),
-    jpContinuousDumpsPhase: v.number('jpContinuousDumpsPhase'),
-    jpContinuousDumpsInterval: v.number('jpContinuousDumpsInterval'),
-
-    memLmk: v.boolean('memLmk'),
-    procStats: v.boolean('procStats'),
-    procStatsPeriodMs: v.number('procStatsPeriodMs', 1000),
-
-    chromeCategoriesSelected: v.stringArray('chromeCategoriesSelected'),
-
-    chromeLogs: v.boolean('chromeLogs'),
-    taskScheduling: v.boolean('taskScheduling'),
-    ipcFlows: v.boolean('ipcFlows'),
-    jsExecution: v.boolean('jsExecution'),
-    webContentRendering: v.boolean('webContentRendering'),
-    uiRendering: v.boolean('uiRendering'),
-    inputEvents: v.boolean('inputEvents'),
-    navigationAndLoading: v.boolean('navigationAndLoading'),
-    chromeHighOverheadCategoriesSelected:
-        v.stringArray('chromeHighOverheadCategoriesSelected'),
-    symbolizeKsyms: v.boolean('symbolizeKsyms'),
-  };
-}
-
-export function createEmptyRecordConfig(): RecordConfig {
-  return runParser(validateRecordConfig, {}).result;
-}
diff --git a/ui/src/controller/validate_config_jsdomtest.ts b/ui/src/controller/validate_config_jsdomtest.ts
deleted file mode 100644
index f8a6546..0000000
--- a/ui/src/controller/validate_config_jsdomtest.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2020 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 {RecordConfig} from '../common/state';
-
-import {
-  createEmptyRecordConfig,
-  runParser,
-  validateNamedRecordConfig,
-  validateRecordConfig,
-  ValidationResult
-} from './validate_config';
-
-test('validateRecordConfig does not keep invalid keys', () => {
-  const key = 'Invalid key';
-  const config: ValidationResult<RecordConfig> =
-      runParser(validateRecordConfig, {[key]: 'Some random value'});
-
-  expect((config.result as object).hasOwnProperty(key)).toEqual(false);
-
-  // Information about an extra key is available in validation result.
-  expect(config.extraKeys.includes(key)).toEqual(true);
-});
-
-test('validateRecordConfig keeps provided values', () => {
-  const value = 31337;
-  const config: ValidationResult<RecordConfig> =
-      runParser(validateRecordConfig, {'durationMs': value});
-
-  expect(config.result.durationMs).toEqual(value);
-
-  // Check that the valid keys do not show as extra keys in validation result.
-  expect(config.extraKeys.includes('durationMs')).toEqual(false);
-});
-
-test(
-    'validateRecordConfig tracks invalid keys while using default values',
-    () => {
-      const config: ValidationResult<RecordConfig> = runParser(
-          validateRecordConfig,
-          {'durationMs': 'a string, this should not be a string'});
-      const defaultConfig = createEmptyRecordConfig();
-
-      expect(config.result.durationMs).toEqual(defaultConfig.durationMs);
-      expect(config.invalidKeys.includes('durationMs')).toEqual(true);
-    });
-
-test(
-    'validateNamedRecordConfig throws exception on required field missing',
-    () => {
-      const unparsedConfig = {
-        title: 'Invalid config'
-        // Key is missing
-      };
-
-      let thrown = false;
-      try {
-        runParser(validateNamedRecordConfig, unparsedConfig);
-      } catch {
-        thrown = true;
-      }
-
-      expect(thrown).toBeTruthy();
-    });
diff --git a/ui/src/controller/validators.ts b/ui/src/controller/validators.ts
new file mode 100644
index 0000000..c35c69d
--- /dev/null
+++ b/ui/src/controller/validators.ts
@@ -0,0 +1,267 @@
+// Copyright (C) 2021 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.
+
+// Execution context of object validator
+interface ValidatorContext {
+  // Path to the current value starting from the root. Object field names are
+  // stored as is, array indices are wrapped to square brackets. Represented
+  // as an array to avoid unnecessary string concatenation: parts are going to
+  // be concatenated into a single string when reporting errors, which should
+  // not happen on a happy path.
+  // Example: ["config", "androidLogBuffers", "1"] when parsing object
+  // accessible through expression `root.config.androidLogBuffers[1]`
+  path: string[];
+
+  // Paths from the root to extraneous keys in a validated object.
+  extraKeys: string[];
+
+  // Paths from the root to keys containing values of wrong type in validated
+  // object.
+  invalidKeys: string[];
+}
+
+// Validator accepting arbitrary data structure and returning a typed value.
+// Can throw an error if a part of the value does not have a reasonable
+// default.
+export interface Validator<T> {
+  validate(input: unknown, context: ValidatorContext): T;
+}
+
+// Helper function to flatten array of path chunks into a single string
+// Example: ["config", "androidLogBuffers", "1"] is mapped to
+// "config.androidLogBuffers[1]".
+function renderPath(path: string[]): string {
+  let result = '';
+  for (let i = 0; i < path.length; i++) {
+    if (i > 0 && !path[i].startsWith('[')) {
+      result += '.';
+    }
+    result += path[i];
+  }
+  return result;
+}
+
+export class ValidationError extends Error {}
+
+// Abstract class for validating simple values, such as strings and booleans.
+// Allows to avoid repetition of most of the code related to validation of
+// these.
+abstract class PrimitiveValidator<T> implements Validator<T> {
+  defaultValue: T;
+  required: boolean;
+
+  constructor(defaultValue: T, required: boolean) {
+    this.defaultValue = defaultValue;
+    this.required = required;
+  }
+
+  // Abstract method that checks whether passed input has correct type.
+  abstract predicate(input: unknown): input is T;
+
+  validate(input: unknown, context: ValidatorContext): T {
+    if (this.predicate(input)) {
+      return input;
+    }
+    if (this.required) {
+      throw new ValidationError(renderPath(context.path));
+    }
+    if (input !== undefined) {
+      // The value is defined, but does not conform to the expected type;
+      // proceed with returning the default value but report the key.
+      context.invalidKeys.push(renderPath(context.path));
+    }
+    return this.defaultValue;
+  }
+}
+
+
+class StringValidator extends PrimitiveValidator<string> {
+  predicate(input: unknown): input is string {
+    return typeof input === 'string';
+  }
+}
+
+class NumberValidator extends PrimitiveValidator<number> {
+  predicate(input: unknown): input is number {
+    return typeof input === 'number';
+  }
+}
+
+class BooleanValidator extends PrimitiveValidator<boolean> {
+  predicate(input: unknown): input is boolean {
+    return typeof input === 'boolean';
+  }
+}
+
+// Type-level function returning resulting type of a validator.
+export type ValidatedType<T> = T extends Validator<infer S>? S : never;
+
+// Type-level function traversing a record of validator and returning record
+// with the same keys and valid types.
+export type RecordValidatedType<T> = {
+  [k in keyof T]: ValidatedType<T[k]>
+};
+
+// Combinator for validators: takes a record of validators, and returns a
+// validator for a record where record's fields passed to validator with the
+// same name.
+//
+// Generic parameter T is instantiated to type of record of validators, and
+// should be provided implicitly by type inference due to verbosity of its
+// instantiations.
+class RecordValidator<T extends Record<string, Validator<unknown>>> implements
+    Validator<RecordValidatedType<T>> {
+  validators: T;
+
+  constructor(validators: T) {
+    this.validators = validators;
+  }
+
+  validate(input: unknown, context: ValidatorContext): RecordValidatedType<T> {
+    // If value is missing or of incorrect type, empty record is still processed
+    // in the loop below to initialize default fields of the nested object.
+    let o: object = {};
+    if (typeof input === 'object' && input !== null) {
+      o = input;
+    } else if (input !== undefined) {
+      context.invalidKeys.push(renderPath(context.path));
+    }
+
+    const result: Partial<RecordValidatedType<T>> = {};
+    // Separate declaration is required to avoid assigning `string` type to `k`.
+    for (const k in this.validators) {
+      if (this.validators.hasOwnProperty(k)) {
+        context.path.push(k);
+        const validator = this.validators[k];
+
+        // Accessing value of `k` of `o` is safe because `undefined` values are
+        // considered to indicate a missing value and handled appropriately by
+        // every provided validator.
+        const valid =
+            validator.validate((o as Record<string, unknown>)[k], context);
+
+        result[k] = valid as ValidatedType<T[string]>;
+        context.path.pop();
+      }
+    }
+
+    // Check if passed object has any extra keys to be reported as such.
+    for (const key of Object.keys(o)) {
+      if (!this.validators.hasOwnProperty(key)) {
+        context.path.push(key);
+        context.extraKeys.push(renderPath(context.path));
+        context.path.pop();
+      }
+    }
+    return result as RecordValidatedType<T>;
+  }
+}
+
+// Validator checking whether a value is one of preset values. Used in order to
+// provide easy validation for union of literal types.
+class OneOfValidator<T> implements Validator<T> {
+  validValues: readonly T[];
+  defaultValue: T;
+
+  constructor(validValues: readonly T[], defaultValue: T) {
+    this.defaultValue = defaultValue;
+    this.validValues = validValues;
+  }
+
+  validate(input: unknown, context: ValidatorContext): T {
+    if (this.validValues.includes(input as T)) {
+      return input as T;
+    } else if (input !== undefined) {
+      context.invalidKeys.push(renderPath(context.path));
+    }
+    return this.defaultValue;
+  }
+}
+
+// Validator for an array of elements, applying the same element validator for
+// each element of an array. Uses empty array as a default value.
+class ArrayValidator<T> implements Validator<T[]> {
+  elementValidator: Validator<T>;
+
+  constructor(elementValidator: Validator<T>) {
+    this.elementValidator = elementValidator;
+  }
+
+  validate(input: unknown, context: ValidatorContext): T[] {
+    const result: T[] = [];
+    if (Array.isArray(input)) {
+      for (let i = 0; i < input.length; i++) {
+        context.path.push(`[${i}]`);
+        result.push(this.elementValidator.validate(input[i], context));
+        context.path.pop();
+      }
+    } else if (input !== undefined) {
+      context.invalidKeys.push(renderPath(context.path));
+    }
+    return result;
+  }
+}
+
+// Wrapper container for validation result contaiting diagnostic information in
+// addition to the resulting typed value.
+export interface ValidationResult<T> {
+  result: T;
+  invalidKeys: string[];
+  extraKeys: string[];
+}
+
+// Wrapper for running a validator initializing the context.
+export function runValidator<T>(
+    validator: Validator<T>, input: unknown): ValidationResult<T> {
+  const context: ValidatorContext = {
+    path: [],
+    invalidKeys: [],
+    extraKeys: [],
+  };
+  const result = validator.validate(input, context);
+  return {
+    result,
+    invalidKeys: context.invalidKeys,
+    extraKeys: context.extraKeys,
+  };
+}
+
+// Shorthands for the validator classes above enabling concise notation.
+export function str(defaultValue = ''): StringValidator {
+  return new StringValidator(defaultValue, false);
+}
+
+export const requiredStr = new StringValidator('', true);
+
+export function num(defaultValue = 0): NumberValidator {
+  return new NumberValidator(defaultValue, false);
+}
+
+export function bool(defaultValue = false): BooleanValidator {
+  return new BooleanValidator(defaultValue, false);
+}
+
+export function record<T extends Record<string, Validator<unknown>>>(
+    validators: T): RecordValidator<T> {
+  return new RecordValidator<T>(validators);
+}
+
+export function oneOf<T>(
+    values: readonly T[], defaultValue: T): OneOfValidator<T> {
+  return new OneOfValidator<T>(values, defaultValue);
+}
+
+export function arrayOf<T>(elementValidator: Validator<T>): ArrayValidator<T> {
+  return new ArrayValidator<T>(elementValidator);
+}
diff --git a/ui/src/controller/validators_unittest.ts b/ui/src/controller/validators_unittest.ts
new file mode 100644
index 0000000..4d54c5c
--- /dev/null
+++ b/ui/src/controller/validators_unittest.ts
@@ -0,0 +1,109 @@
+// Copyright (C) 2021 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 {
+  arrayOf,
+  num,
+  oneOf,
+  record,
+  requiredStr,
+  runValidator,
+  ValidatedType,
+  ValidationError
+} from './validators';
+
+const colors = ['RED', 'GREEN', 'BLUE'] as const;
+
+type Color = typeof colors[number];
+
+const point = record({
+  id: requiredStr,
+  color: oneOf<Color>(colors, 'RED'),
+  x: num(),
+  y: num(1),
+  properties: record({mass: num(10)})
+});
+
+type Point = ValidatedType<typeof point>;
+
+const nested =
+    record({deeply: record({nested: record({array: arrayOf(point)})})});
+
+test('validator ensures presence of required fields', () => {
+  expect(() => {
+    runValidator(point, {});
+  }).toThrow(ValidationError);
+});
+
+test('validator ensures correct type of required fields', () => {
+  expect(() => {
+    runValidator(point, {id: 0});
+  }).toThrow(ValidationError);
+});
+
+test('validator fills default values', () => {
+  const p: Point = runValidator(point, {id: 'test'}).result;
+
+  expect(p.color).toEqual('RED');
+  expect(p.x).toEqual(0);
+  expect(p.y).toEqual(1);
+  expect(p.properties.mass).toEqual(10);
+});
+
+test('validator uses provided values', () => {
+  const p: Point =
+      runValidator(
+          point,
+          {id: 'test', x: 100, y: 200, color: 'GREEN', properties: {mass: 20}})
+          .result;
+
+  expect(p.color).toEqual('GREEN');
+  expect(p.x).toEqual(100);
+  expect(p.y).toEqual(200);
+  expect(p.properties.mass).toEqual(20);
+});
+
+test('validator keeps information about extra and invalid keys', () => {
+  const result = runValidator(point, {
+    id: 'test',
+    x: 'should not be a string',
+    extra: 'should not be here',
+    properties: {mass: 'should be a number', weight: 'should not be here'}
+  });
+
+  expect(result.extraKeys).toContain('extra');
+  expect(result.extraKeys).toContain('properties.weight');
+  expect(result.invalidKeys).toContain('x');
+  expect(result.invalidKeys).toContain('properties.mass');
+});
+
+test('validator correctly keeps track of path when reporting keys', () => {
+  const result = runValidator(nested, {
+    extra1: 0,
+    deeply: {
+      extra2: 1,
+      nested: {
+        array: [
+          {id: 'point1', x: 'should not be a string'},
+          {id: 'point2', extra3: 'should not be here'}
+        ]
+      }
+    }
+  });
+
+  expect(result.extraKeys).toContain('extra1');
+  expect(result.extraKeys).toContain('deeply.extra2');
+  expect(result.extraKeys).toContain('deeply.nested.array[1].extra3');
+  expect(result.invalidKeys).toContain('deeply.nested.array[0].x');
+});
diff --git a/ui/src/frontend/analyze_page.ts b/ui/src/frontend/analyze_page.ts
index 2585ebc..c464651 100644
--- a/ui/src/frontend/analyze_page.ts
+++ b/ui/src/frontend/analyze_page.ts
@@ -26,6 +26,7 @@
 const INPUT_MAX_LINES = 10;
 const INPUT_LINE_HEIGHT_EM = 1.2;
 const TAB_SPACES = 2;
+const TAB_SPACES_STRING = ' '.repeat(TAB_SPACES);
 const QUERY_ID = 'analyze-page-query';
 
 class QueryInput implements m.ClassComponent {
@@ -35,10 +36,14 @@
   static onKeyDown(e: Event) {
     const event = e as KeyboardEvent;
     const target = e.target as HTMLTextAreaElement;
+    const {selectionStart, selectionEnd} = target;
 
     if (event.code === 'Enter' && (event.metaKey || event.ctrlKey)) {
       event.preventDefault();
-      const query = target.value;
+      let query = target.value;
+      if (selectionEnd > selectionStart) {
+        query = query.substring(selectionStart, selectionEnd);
+      }
       if (!query) return;
       globals.dispatch(
           Actions.executeQuery({engineId: '0', queryId: QUERY_ID, query}));
@@ -47,14 +52,62 @@
     if (event.code === 'Tab') {
       // Handle tabs to insert spaces.
       event.preventDefault();
-      const whitespace = ' '.repeat(TAB_SPACES);
-      const {selectionStart, selectionEnd} = target;
-      target.value = target.value.substring(0, selectionStart) + whitespace +
-          target.value.substring(selectionEnd);
-      target.selectionEnd = selectionStart + TAB_SPACES;
+      const lastLineBreak = target.value.lastIndexOf('\n', selectionEnd);
+
+      if (selectionStart === selectionEnd || lastLineBreak < selectionStart) {
+        // Selection does not contain line breaks, therefore is on a single
+        // line. In this case, replace the selection with spaces. Replacement is
+        // done via document.execCommand as opposed to direct manipulation of
+        // element's value attribute because modifying latter programmatically
+        // drops the edit history which breaks undo/redo functionality.
+        document.execCommand('insertText', false, TAB_SPACES_STRING);
+      } else {
+        this.handleMultilineTab(target, event);
+      }
     }
   }
 
+  // Handle Tab press when the current selection is multiline: find all the
+  // lines intersecting with the selection, and either indent or dedent (if
+  // Shift key is held) them.
+  private static handleMultilineTab(
+      target: HTMLTextAreaElement, event: KeyboardEvent) {
+    const {selectionStart, selectionEnd} = target;
+    const firstLineBreak = target.value.lastIndexOf('\n', selectionStart - 1);
+
+    // If no line break is found (selection begins at the first line),
+    // replacementStart would have the correct value of 0.
+    const replacementStart = firstLineBreak + 1;
+    const replacement = target.value.substring(replacementStart, selectionEnd)
+                            .split('\n')
+                            .map((line) => {
+                              if (event.shiftKey) {
+                                // When Shift is held, remove whitespace at the
+                                // beginning
+                                return this.dedent(line);
+                              } else {
+                                return TAB_SPACES_STRING + line;
+                              }
+                            })
+                            .join('\n');
+    // Select the range to be replaced.
+    target.setSelectionRange(replacementStart, selectionEnd);
+    document.execCommand('insertText', false, replacement);
+    // Restore the selection to match the previous selection, allowing to chain
+    // indent operations by just pressing Tab several times.
+    target.setSelectionRange(
+        replacementStart, replacementStart + replacement.length);
+  }
+
+  // Chop off up to TAB_SPACES leading spaces from a string.
+  private static dedent(line: string): string {
+    let i = 0;
+    while (i < line.length && i < TAB_SPACES && line[i] === ' ') {
+      i++;
+    }
+    return line.substring(i);
+  }
+
   onInput(textareaValue: string) {
     const textareaLines = textareaValue.split('\n').length;
     const clampedNumLines =
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
new file mode 100644
index 0000000..dc818d3
--- /dev/null
+++ b/ui/src/frontend/base_slice_track.ts
@@ -0,0 +1,696 @@
+// Copyright (C) 2021 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 {assertExists} from '../base/logging';
+import {Actions} from '../common/actions';
+import {cropText, drawIncompleteSlice} from '../common/canvas_utils';
+import {colorCompare, colorToStr, GRAY_COLOR} from '../common/colorizer';
+import {NUM, QueryResult} from '../common/query_result';
+import {SelectionKind} from '../common/state';
+import {fromNs, toNs} from '../common/time';
+
+import {checkerboardExcept} from './checkerboard';
+import {globals} from './globals';
+import {Slice} from './slice';
+import {DEFAULT_SLICE_LAYOUT, SliceLayout} from './slice_layout';
+import {NewTrackArgs, SliceRect, Track} from './track';
+
+// The common class that underpins all tracks drawing slices.
+
+export const SLICE_FLAGS_INCOMPLETE = 1;
+export const SLICE_FLAGS_INSTANT = 2;
+
+// Slices smaller than this don't get any text:
+const SLICE_MIN_WIDTH_FOR_TEXT_PX = 5;
+// Slices smaller than this aren't rendered at all.
+const SLICE_MIN_WIDTH_PX = 0.1;
+const CHEVRON_WIDTH_PX = 10;
+const DEFAULT_SLICE_COLOR = GRAY_COLOR;
+
+// TODO(hjd): Implement caching.
+
+// The minimal set of columns that any table/view must expose to render tracks.
+// Note: this class assumes that, at the SQL level, slices are:
+// - Not temporally overlapping (unless they are nested at inner depth).
+// - Strictly stacked (i.e. a slice at depth N+1 cannot be larger than any
+//   slices at depth 0..N.
+// If you need temporally overlapping slices, look at AsyncSliceTrack, which
+// merges several tracks into one visual track.
+export const BASE_SLICE_ROW = {
+  id: NUM,     // The slice ID, for selection / lookups.
+  tsq: NUM,    // Quantized |ts|. This class owns the quantization logic.
+  ts: NUM,     // Start time in nanoseconds.
+  dur: NUM,    // Duration in nanoseconds. -1 = incomplete, 0 = instant.
+  depth: NUM,  // Vertical depth.
+};
+
+export type BaseSliceRow = typeof BASE_SLICE_ROW;
+
+// The meta-type which describes the types used to extend the BaseSliceTrack.
+// Derived classes can extend this interface to override these types if needed.
+export interface BaseSliceTrackTypes {
+  slice: Slice;
+  row: BaseSliceRow;
+  config: {};
+}
+
+export abstract class BaseSliceTrack<T extends BaseSliceTrackTypes =
+                                                   BaseSliceTrackTypes> extends
+    Track<T['config']> {
+  // This is the slice cache.
+  private slices = new Array<T['slice']>();
+  protected sliceLayout: SliceLayout = {...DEFAULT_SLICE_LAYOUT};
+
+  // These are the over-skirted cached bounds.
+  private slicesStartNs = -1;
+  private slicesEndNs = -1;
+  private slicesBucketNs = -1;
+
+  private readonly tableName: string;
+  private maxDurNs = 0;
+  private sqlState: 'UNINITIALIZED'|'INITIALIZING'|'QUERY_PENDING'|
+      'QUERY_DONE' = 'UNINITIALIZED';
+  private extraSqlColumns: string[];
+
+  private charWidth = -1;
+  private hoverPos?: {x: number, y: number};
+  protected hoveredSlice?: T['slice'];
+  private hoverTooltip: string[] = [];
+  private maxDataDepth = 0;
+
+  // Computed layout.
+  private computedTrackHeight = 0;
+  private computedSliceHeight = 0;
+  private computedRowSpacing = 0;
+
+  // TODO(hjd): Remove when updating selection.
+  // We shouldn't know here about CHROME_SLICE. Maybe should be set by
+  // whatever deals with that. Dunno the namespace of selection is weird. For
+  // most cases in non-ambiguous (because most things are a 'slice'). But some
+  // others (e.g. THREAD_SLICE) have their own ID namespace so we need this.
+  protected selectionKinds: SelectionKind[] = ['SLICE', 'CHROME_SLICE'];
+
+  // Extension points.
+  // Each extension point should take a dedicated argument type (e.g.,
+  // OnSliceOverArgs {slice?: T['slice']}) so it makes future extensions
+  // non-API-breaking (e.g. if we want to add the X position).
+  abstract initSqlTable(_tableName: string): Promise<void>;
+  getRowSpec(): T['row'] {
+    return BASE_SLICE_ROW;
+  }
+  onSliceOver(_args: OnSliceOverArgs<T['slice']>): void {}
+  onSliceOut(_args: OnSliceOutArgs<T['slice']>): void {}
+  onSliceClick(_args: OnSliceClickArgs<T['slice']>): void {}
+  prepareSlices(slices: Array<T['slice']>): void {
+    this.highlightHovererdAndSameTitle(slices);
+  }
+
+  // TODO(hjd): Remove.
+  drawSchedLatencyArrow(
+      _: CanvasRenderingContext2D, _selectedSlice?: T['slice']): void {}
+
+  constructor(args: NewTrackArgs) {
+    super(args);
+    this.frontendOnly = true;  // Disable auto checkerboarding.
+    this.tableName = `track_${this.trackId}`.replace(/[^a-zA-Z0-9_]+/g, '_');
+
+    // Work out the extra columns.
+    // This is the union of the embedder-defined columns and the base columns
+    // we know about (ts, dur, ...).
+    const allCols = Object.keys(this.getRowSpec());
+    const baseCols = Object.keys(BASE_SLICE_ROW);
+    this.extraSqlColumns = allCols.filter(key => !baseCols.includes(key));
+  }
+
+  setSliceLayout(sliceLayout: SliceLayout) {
+    if (sliceLayout.minDepth > sliceLayout.maxDepth) {
+      const {maxDepth, minDepth} = sliceLayout;
+      throw new Error(`minDepth ${minDepth} must be <= maxDepth ${maxDepth}`);
+    }
+    this.sliceLayout = sliceLayout;
+  }
+
+  onFullRedraw(): void {
+    // TODO(hjd): Call this only when cache changes. See discussion:
+    // What we want to do here is give the Impl a chance to colour the slice,
+    // e.g. depending on the currently selected thread or process.
+    // Here's an interesting thought. We have two options here:
+    //   A) We could pass only the vizSlices, but then we'd have to call this
+    //      @ 60FPS (because vizSlices changes as we pan).
+    //   B) We could call this only on full redraws (when the state changes),
+    //      but then the track needs to process *all* cached slices, not just
+    //      the visible ones. It's okay now (it's a 2x factor) but might get
+    //      worse if we cache several layers of slices at various resolutions.
+    // But there's an escape, I think. I think the right thing to do is:
+    // - For now call it on the full slices, but only on full redraws.
+    // - When we get caching, call it every time we switch "cached quantization
+    //  level", which is a way in the middle between 60FPS and full redraws..
+    // Overall the API contract of this prepareSlices() call is:
+    //  - I am going to draw these slices in the near future.
+    //  - I am not going to draw any slice that I haven't passed here first.
+    //  - This is guaranteed to be called at least on every state change.
+    //  - This is NOT guaranteed to be called on every frame. For instance you
+    //    cannot use this to do some colour-based animation.
+
+    // Give a chance to the embedder to change colors and other stuff.
+    this.prepareSlices(this.slices);
+  }
+
+  renderCanvas(ctx: CanvasRenderingContext2D): void {
+    // TODO(hjd): fonts and colors should come from the CSS and not hardcoded
+    // here.
+    const {timeScale} = globals.frontendLocalState;
+    const vizTime = globals.frontendLocalState.visibleWindowTime;
+
+    // If the visible time range is outside the cached area, requests
+    // asynchronously new data from the SQL engine.
+    this.maybeRequestData();
+
+    // In any case, draw whatever we have (which might be stale/incomplete).
+
+    // If the cached trace slices don't fully cover the visible time range,
+    // show a gray rectangle with a "Loading..." label.
+    checkerboardExcept(
+        ctx,
+        this.getHeight(),
+        timeScale.timeToPx(vizTime.start),
+        timeScale.timeToPx(vizTime.end),
+        timeScale.timeToPx(fromNs(this.slicesStartNs)),
+        timeScale.timeToPx(fromNs(this.slicesEndNs)));
+
+    let charWidth = this.charWidth;
+    if (charWidth < 0) {
+      // TODO(hjd): Centralize font measurement/invalidation.
+      ctx.font = '12px Roboto Condensed';
+      charWidth = this.charWidth = ctx.measureText('dbpqaouk').width / 8;
+    }
+
+    // Filter only the visible slices. |this.slices| will have more slices than
+    // needed because maybeRequestData() over-fetches to handle small pan/zooms.
+    // We don't want to waste time drawing slices that are off screen.
+    const vizSlices = this.getVisibleSlices(vizTime.start, vizTime.end);
+
+    let selection = globals.state.currentSelection;
+
+    if (!selection || !this.selectionKinds.includes(selection.kind)) {
+      selection = null;
+    }
+
+    // Believe it or not, doing 4xO(N) passes is ~2x faster than trying to draw
+    // everything in one go. The key is that state changes operations on the
+    // canvas (e.g., color, fonts) dominate any number crunching we do in JS.
+
+    this.updateSliceAndTrackHeight();
+    const sliceHeight = this.computedSliceHeight;
+    const padding = this.sliceLayout.padding;
+    const rowSpacing = this.computedRowSpacing;
+
+    // First pass: compute geometry of slices.
+    let selSlice: T['slice']|undefined;
+
+    // pxEnd is the last visible pixel in the visible viewport. Drawing
+    // anything < 0 or > pxEnd doesn't produce any visible effect as it goes
+    // beyond the visible portion of the canvas.
+    const pxEnd = Math.floor(timeScale.timeToPx(vizTime.end));
+
+    for (const slice of vizSlices) {
+      // Compute the basic geometry for any visible slice, even if only
+      // partially visible. This might end up with a negative x if the
+      // slice starts before the visible time or with a width that overflows
+      // pxEnd.
+      slice.x = timeScale.timeToPx(slice.startS);
+      slice.w = timeScale.deltaTimeToPx(slice.durationS);
+      if (slice.flags & SLICE_FLAGS_INSTANT) {
+        // In the case of an instant slice, set the slice geometry on the
+        // bounding box that will contain the chevron.
+        slice.x -= CHEVRON_WIDTH_PX / 2;
+        slice.w = CHEVRON_WIDTH_PX;
+      } else {
+        // If the slice is an actual slice, intersect the slice geometry with
+        // the visible viewport (this affects only the first and last slice).
+        // This is so that text is always centered even if we are zoomed in.
+        // Visually if we have
+        //                   [    visible viewport   ]
+        //  [         slice         ]
+        // The resulting geometry will be:
+        //                   [slice]
+        // So that the slice title stays within the visible region.
+        const sliceVizLimit = Math.min(slice.x + slice.w, pxEnd);
+        slice.x = Math.max(slice.x, 0);
+        slice.w = sliceVizLimit - slice.x;
+      }
+
+      if (selection && (selection as {id: number}).id === slice.id) {
+        selSlice = slice;
+      }
+    }
+
+    // Second pass: fill slices by color.
+    // The .slice() turned out to be an unintended pun.
+    const vizSlicesByColor = vizSlices.slice();
+    vizSlicesByColor.sort((a, b) => colorCompare(a.color, b.color));
+    let lastColor = undefined;
+    for (const slice of vizSlicesByColor) {
+      if (slice.color !== lastColor) {
+        lastColor = slice.color;
+        ctx.fillStyle = colorToStr(slice.color);
+      }
+      const y = padding + slice.depth * (sliceHeight + rowSpacing);
+      if (slice.flags & SLICE_FLAGS_INSTANT) {
+        this.drawChevron(ctx, slice.x, y, sliceHeight);
+      } else if (slice.flags & SLICE_FLAGS_INCOMPLETE) {
+        const w = Math.max(slice.w - 2, 2);
+        drawIncompleteSlice(ctx, slice.x, y, w, sliceHeight);
+      } else if (slice.w > SLICE_MIN_WIDTH_PX) {
+        ctx.fillRect(slice.x, y, slice.w, sliceHeight);
+      }
+    }
+
+    // Third pass, draw the titles (e.g., process name for sched slices).
+    ctx.fillStyle = '#fff';
+    ctx.textAlign = 'center';
+    ctx.font = '12px Roboto Condensed';
+    ctx.textBaseline = 'middle';
+    for (const slice of vizSlices) {
+      if ((slice.flags & SLICE_FLAGS_INSTANT) || !slice.title ||
+          slice.w < SLICE_MIN_WIDTH_FOR_TEXT_PX) {
+        continue;
+      }
+
+      const title = cropText(slice.title, charWidth, slice.w);
+      const rectXCenter = slice.x + slice.w / 2;
+      const y = padding + slice.depth * (sliceHeight + rowSpacing);
+      const yDiv = slice.subTitle ? 3 : 2;
+      const yMidPoint = Math.floor(y + sliceHeight / yDiv) - 0.5;
+      ctx.fillText(title, rectXCenter, yMidPoint);
+    }
+
+    // Fourth pass, draw the subtitles (e.g., thread name for sched slices).
+    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
+    ctx.font = '10px Roboto Condensed';
+    for (const slice of vizSlices) {
+      if (slice.w < SLICE_MIN_WIDTH_FOR_TEXT_PX || !slice.subTitle ||
+          (slice.flags & SLICE_FLAGS_INSTANT)) {
+        continue;
+      }
+      const rectXCenter = slice.x + slice.w / 2;
+      const subTitle = cropText(slice.subTitle, charWidth, slice.w);
+      const y = padding + slice.depth * (sliceHeight + rowSpacing);
+      const yMidPoint = Math.ceil(y + sliceHeight * 2 / 3) + 1.5;
+      ctx.fillText(subTitle, rectXCenter, yMidPoint);
+    }
+
+    // Draw a thicker border around the selected slice (or chevron).
+    if (selSlice !== undefined) {
+      const color = selSlice.color;
+      const y = padding + selSlice.depth * (sliceHeight + rowSpacing);
+      ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`;
+      ctx.beginPath();
+      const THICKNESS = 3;
+      ctx.lineWidth = THICKNESS;
+      ctx.strokeRect(
+          selSlice.x, y - THICKNESS / 2, selSlice.w, sliceHeight + THICKNESS);
+      ctx.closePath();
+    }
+
+    // TODO(hjd): Remove this.
+    // The only thing this does is drawing the sched latency arrow. We should
+    // have some abstraction for that arrow (ideally the same we'd use for
+    // flows).
+    this.drawSchedLatencyArrow(ctx, selSlice);
+
+    // If a slice is hovered, draw the tooltip.
+    const tooltip = this.hoverTooltip;
+    if (this.hoveredSlice !== undefined && tooltip.length > 0 &&
+        this.hoverPos !== undefined) {
+      if (tooltip.length === 1) {
+        this.drawTrackHoverTooltip(ctx, this.hoverPos, tooltip[0]);
+      } else {
+        this.drawTrackHoverTooltip(ctx, this.hoverPos, tooltip[0], tooltip[1]);
+      }
+    }  // if (howSlice)
+  }
+
+  // This method figures out if the visible window is outside the bounds of
+  // the cached data and if so issues new queries (i.e. sorta subsumes the
+  // onBoundsChange).
+  async maybeRequestData() {
+    // Important: this method is async and is invoked on every frame. Care
+    // must be taken to avoid piling up queries on every frame, hence the FSM.
+    if (this.sqlState === 'UNINITIALIZED') {
+      this.sqlState = 'INITIALIZING';
+
+      // TODO(hjd): we need an onDestroy. Right now if you contract and expand a
+      // track group this will crash, because the 2nd time we create the track
+      // we end up re-issuing the CREATE VIEW table_name.
+      // Right now this DROP VIEW is a hack, because it: (1) assumes that
+      // tableName is a VIEW and not a TABLE; (2) assume the impl track didn't
+      // create any other TABLE/VIEW (which happens to be true right now but
+      // might now be in future).
+      await this.engine.query(`DROP VIEW IF EXISTS ${this.tableName}`);
+      await this.initSqlTable(this.tableName);
+
+      const queryRes = await this.engine.query(`select
+          ifnull(max(dur), 0) as maxDur, count(1) as rowCount
+          from ${this.tableName}`);
+      const row = queryRes.firstRow({maxDur: NUM, rowCount: NUM});
+      this.maxDurNs = row.maxDur;
+      this.sqlState = 'QUERY_DONE';
+    } else if (
+        this.sqlState === 'INITIALIZING' || this.sqlState === 'QUERY_PENDING') {
+      return;
+    }
+
+    const resolutionNs = toNs(globals.getCurResolution());
+    const vizTime = globals.frontendLocalState.visibleWindowTime;
+
+    const startNs = toNs(vizTime.start);
+    const endNs = toNs(vizTime.end);
+
+    // TODO(hjd): figure out / centralize the resolution steps.
+    // Will handle this at the same time as cacheing.
+    const bucketNs = resolutionNs;
+
+    if (startNs >= this.slicesStartNs && endNs <= this.slicesEndNs &&
+        bucketNs === this.slicesBucketNs) {
+      return;  // We have the data already, no need to re-query
+    }
+
+    this.sqlState = 'QUERY_PENDING';
+    const queryTsq = `(ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs}`;
+
+    const extraCols = this.extraSqlColumns.join(',');
+    let depthCol = 'depth';
+    let maybeGroupByDepth = 'depth, ';
+    const layout = this.sliceLayout;
+    const isFlat = (layout.maxDepth - layout.minDepth) <= 1;
+    // maxDepth === minDepth only makes sense if track is empty which on the
+    // one hand isn't very useful (and so maybe should be an error) on the
+    // other hand I can see it happening if someone does:
+    // minDepth = min(slices.depth); maxDepth = max(slices.depth);
+    // and slices is empty, so we treat that as flat.
+    if (isFlat) {
+      depthCol = `${this.sliceLayout.minDepth} as depth`;
+      maybeGroupByDepth = '';
+    }
+
+    // TODO(hjd): Re-reason and improve this query:
+    // - Materialize the unfinished slices one off.
+    // - Avoid the union if we know we don't have any -1 slices.
+    // - Maybe we don't need the union at all and can deal in TS?
+    const queryRes = await this.engine.query(`
+    with q1 as (
+      select
+        ${queryTsq} as tsq,
+        ts,
+        max(dur) as dur,
+        id,
+        ${depthCol}
+        ${extraCols ? ',' + extraCols : ''}
+      from ${this.tableName}
+      where
+        ts >= ${startNs - this.maxDurNs /* - durNs */} and
+        ts <= ${endNs /* + durNs */}
+      group by ${maybeGroupByDepth} tsq
+      order by tsq),
+    q2 as (
+      select
+        ${queryTsq} as tsq,
+        ts,
+        -1 as dur,
+        id,
+        ${depthCol}
+        ${extraCols ? ',' + extraCols : ''}
+      from ${this.tableName}
+      where dur = -1
+      group by ${maybeGroupByDepth} tsq
+      )
+      select min(dur) as _unused, * from
+      (select * from q1 union all select * from q2)
+      group by ${maybeGroupByDepth} tsq
+      order by tsq
+    `);
+    this.convertQueryResultToSlices(queryRes, startNs, endNs, bucketNs);
+    this.sqlState = 'QUERY_DONE';
+    globals.rafScheduler.scheduleRedraw();
+  }
+
+  // Here convert each row to a Slice. We do what we can do generically
+  // in the base class, and delegate the rest to the impl via that rowToSlice()
+  // abstract call.
+  convertQueryResultToSlices(
+      queryRes: QueryResult, startNs: number, endNs: number, bucketNs: number) {
+    const slices = new Array<T['slice']>(queryRes.numRows());
+    const it = queryRes.iter(this.getRowSpec());
+
+    let maxDataDepth = this.maxDataDepth;
+    this.slicesStartNs = startNs;
+    this.slicesEndNs = endNs;
+    this.slicesBucketNs = bucketNs;
+    for (let i = 0; it.valid(); it.next(), ++i) {
+      maxDataDepth = Math.max(maxDataDepth, it.depth);
+
+      // Construct the base slice. The Impl will construct and return the full
+      // derived T["slice"] (e.g. CpuSlice) in the rowToSlice() method.
+      slices[i] = this.rowToSlice(it);
+    }
+    this.maxDataDepth = maxDataDepth;
+    this.slices = slices;
+  }
+
+  rowToSlice(row: T['row']): T['slice'] {
+    const startNsQ = row.tsq;
+    const startNs = row.ts;
+    let flags = 0;
+    let durNs: number;
+    if (row.dur === -1) {
+      durNs = toNs(globals.state.traceTime.endSec) - startNs;
+      flags |= SLICE_FLAGS_INCOMPLETE;
+    } else {
+      flags |= (row.dur === 0) ? SLICE_FLAGS_INSTANT : 0;
+      durNs = row.dur;
+    }
+    const endNs = startNs + durNs;
+    const bucketNs = this.slicesBucketNs;
+    let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
+    endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
+
+    return {
+      id: row.id,
+      startS: fromNs(startNsQ),
+      durationS: fromNs(endNsQ - startNsQ),
+      flags,
+      depth: row.depth,
+      title: '',
+      subTitle: '',
+
+      // The derived class doesn't need to initialize these. They are
+      // rewritten on every renderCanvas() call. We just need to initialize
+      // them to something.
+      baseColor: DEFAULT_SLICE_COLOR,
+      color: DEFAULT_SLICE_COLOR,
+      x: -1,
+      w: -1,
+    };
+  }
+
+  private findSlice({x, y}: {x: number, y: number}): undefined|Slice {
+    const trackHeight = this.computedTrackHeight;
+    const sliceHeight = this.computedSliceHeight;
+    const padding = this.sliceLayout.padding;
+    const rowSpacing = this.computedRowSpacing;
+
+    // Need at least a draw pass to resolve the slice layout.
+    if (sliceHeight === 0) {
+      return undefined;
+    }
+
+    if (y >= padding && y <= trackHeight - padding) {
+      const depth = Math.floor((y - padding) / (sliceHeight + rowSpacing));
+      for (const slice of this.slices) {
+        if (slice.depth === depth && slice.x <= x && x <= slice.x + slice.w) {
+          return slice;
+        }
+      }
+    }
+
+    return undefined;
+  }
+
+  onMouseMove(position: {x: number, y: number}): void {
+    this.hoverPos = position;
+    this.updateHoveredSlice(this.findSlice(position));
+  }
+
+  onMouseOut(): void {
+    this.updateHoveredSlice(undefined);
+  }
+
+  private updateHoveredSlice(slice?: T['slice']): void {
+    const lastHoveredSlice = this.hoveredSlice;
+    this.hoveredSlice = slice;
+
+    // Only notify the Impl if the hovered slice changes:
+    if (slice === lastHoveredSlice) return;
+
+    if (this.hoveredSlice === undefined) {
+      globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));
+      this.onSliceOut({slice: assertExists(lastHoveredSlice)});
+      this.hoverTooltip = [];
+      this.hoverPos = undefined;
+    } else {
+      const args: OnSliceOverArgs<T['slice']> = {slice: this.hoveredSlice};
+      globals.dispatch(
+          Actions.setHighlightedSliceId({sliceId: this.hoveredSlice.id}));
+      this.onSliceOver(args);
+      this.hoverTooltip = args.tooltip || [];
+    }
+  }
+
+  onMouseClick(position: {x: number, y: number}): boolean {
+    const slice = this.findSlice(position);
+    if (slice === undefined) {
+      return false;
+    }
+    const args: OnSliceClickArgs<T['slice']> = {slice};
+    this.onSliceClick(args);
+    return true;
+  }
+
+  getVisibleSlices(startS: number, endS: number): Array<T['slice']> {
+    let startIdx = -1;
+    let endIdx = -1;
+    let i = 0;
+
+    // TODO(hjd): binary search.
+    for (const slice of this.slices) {
+      if (startIdx < 0 && slice.startS + slice.durationS >= startS) {
+        startIdx = i;
+      }
+      if (slice.startS <= endS) {
+        endIdx = i + 1;
+      } else if (slice.startS > endS) {
+        endIdx = i;
+        break;
+      }
+      i++;
+    }
+    return this.slices.slice(startIdx, endIdx);
+  }
+
+  private updateSliceAndTrackHeight() {
+    const lay = this.sliceLayout;
+
+    const rows =
+        Math.min(Math.max(this.maxDataDepth + 1, lay.minDepth), lay.maxDepth);
+
+    // Compute the track height.
+    let trackHeight;
+    if (lay.heightMode === 'FIXED') {
+      trackHeight = lay.fixedHeight;
+    } else {
+      trackHeight = 2 * lay.padding + rows * (lay.sliceHeight + lay.rowSpacing);
+    }
+
+    // Compute the slice height.
+    let sliceHeight: number;
+    let rowSpacing: number = lay.rowSpacing;
+    if (lay.heightMode === 'FIXED') {
+      const rowHeight = (trackHeight - 2 * lay.padding) / rows;
+      sliceHeight = Math.floor(Math.max(rowHeight - lay.rowSpacing, 0.5));
+      rowSpacing = Math.max(lay.rowSpacing, rowHeight - sliceHeight);
+      rowSpacing = Math.floor(rowSpacing * 2) / 2;
+    } else {
+      sliceHeight = lay.sliceHeight;
+    }
+    this.computedSliceHeight = sliceHeight;
+    this.computedTrackHeight = trackHeight;
+    this.computedRowSpacing = rowSpacing;
+  }
+
+  private drawChevron(
+      ctx: CanvasRenderingContext2D, x: number, y: number, h: number) {
+    // Draw an upward facing chevrons, in order: A, B, C, D, and back to A.
+    // . (x, y)
+    //      A
+    //     ###
+    //    ##C##
+    //   ##   ##
+    //  D       B
+    //            . (x + CHEVRON_WIDTH_PX, y + h)
+    const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
+    const midX = x + HALF_CHEVRON_WIDTH_PX;
+    ctx.beginPath();
+    ctx.moveTo(midX, y);                              // A.
+    ctx.lineTo(x + CHEVRON_WIDTH_PX, y + h);          // B.
+    ctx.lineTo(midX, y + h - HALF_CHEVRON_WIDTH_PX);  // C.
+    ctx.lineTo(x, y + h);                             // D.
+    ctx.lineTo(midX, y);                              // Back to A.
+    ctx.closePath();
+    ctx.fill();
+  }
+
+  // This is a good default implemenation for highlighting slices. By default
+  // prepareSlices() calls this. However, if the XxxSliceTrack impl overrides
+  // prepareSlices() this gives them a chance to call the highlighting witout
+  // having to reimplement it.
+  protected highlightHovererdAndSameTitle(slices: Slice[]) {
+    for (const slice of slices) {
+      const isHovering = globals.state.highlightedSliceId === slice.id ||
+          (this.hoveredSlice && this.hoveredSlice.title === slice.title);
+      if (isHovering) {
+        slice.color = {
+          c: slice.baseColor.c,
+          h: slice.baseColor.h,
+          s: slice.baseColor.s,
+          l: 30
+        };
+      } else {
+        slice.color = slice.baseColor;
+      }
+    }
+  }
+
+  getHeight(): number {
+    this.updateSliceAndTrackHeight();
+    return this.computedTrackHeight;
+  }
+
+  getSliceRect(_tStart: number, _tEnd: number, _depth: number): SliceRect
+      |undefined {
+    // TODO(hjd): Implement this as part of updating flow events.
+    return undefined;
+  }
+}
+
+// This is the argument passed to onSliceOver(args).
+// This is really a workaround for the fact that TypeScript doesn't allow
+// inner types within a class (whether the class is templated or not).
+export interface OnSliceOverArgs<S extends Slice> {
+  // Input args (BaseSliceTrack -> Impl):
+  slice: S;  // The slice being hovered.
+
+  // Output args (Impl -> BaseSliceTrack):
+  tooltip?: string[];  // One entry per row, up to a max of 2.
+}
+
+export interface OnSliceOutArgs<S extends Slice> {
+  // Input args (BaseSliceTrack -> Impl):
+  slice: S;  // The slice which is not hovered anymore.
+}
+
+export interface OnSliceClickArgs<S extends Slice> {
+  // Input args (BaseSliceTrack -> Impl):
+  slice: S;  // The slice which is clicked.
+}
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index ea3d5a5..35a4d9f 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -264,13 +264,13 @@
     switch (profileType) {
       case ProfileType.PERF_SAMPLE:
         return [this.buildButtonComponent(PERF_SAMPLES_KEY, 'samples')];
-      case ProfileType.NATIVE_HEAP_PROFILE:
+      case ProfileType.JAVA_HEAP_GRAPH:
         return [
           this.buildButtonComponent(
               SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'),
           this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'objects')
         ];
-      case ProfileType.JAVA_HEAP_GRAPH:
+      case ProfileType.NATIVE_HEAP_PROFILE:
         return [
           this.buildButtonComponent(
               SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'),
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 3a989cd..4cde97b 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -263,8 +263,11 @@
       begin: {x: number, y: number, dir: LineDirection},
       end: {x: number, y: number, dir: LineDirection}, hue: number,
       intensity: number, width: number) {
+    const hasArrowHead = Math.abs(begin.x - end.x) > 3 * TRIANGLE_SIZE;
     const END_OFFSET =
-        (end.dir === 'RIGHT' || end.dir === 'LEFT' ? TRIANGLE_SIZE : 0);
+        (((end.dir === 'RIGHT' || end.dir === 'LEFT') && hasArrowHead) ?
+             TRIANGLE_SIZE :
+             0);
     const color = `hsl(${hue}, 50%, ${intensity}%)`;
     // draw curved line from begin to end (bezier curve)
     ctx.strokeStyle = color;
@@ -300,17 +303,23 @@
       ctx.arc(end.x, end.y, CIRCLE_RADIUS, 0, 2 * Math.PI);
       ctx.closePath();
       ctx.fill();
-    } else {
-      const dx = this.getDeltaX(end.dir, TRIANGLE_SIZE);
-      const dy = this.getDeltaY(end.dir, TRIANGLE_SIZE);
-      // draw small triangle
-      ctx.fillStyle = color;
-      ctx.beginPath();
-      ctx.moveTo(end.x, end.y);
-      ctx.lineTo(end.x - dx - dy, end.y + dx - dy);
-      ctx.lineTo(end.x - dx + dy, end.y - dx - dy);
-      ctx.closePath();
-      ctx.fill();
+    } else if (hasArrowHead) {
+      this.drawArrowHead(end, ctx, color);
     }
   }
+
+  private drawArrowHead(
+      end: {x: number; y: number; dir: LineDirection},
+      ctx: CanvasRenderingContext2D, color: string) {
+    const dx = this.getDeltaX(end.dir, TRIANGLE_SIZE);
+    const dy = this.getDeltaY(end.dir, TRIANGLE_SIZE);
+    // draw small triangle
+    ctx.fillStyle = color;
+    ctx.beginPath();
+    ctx.moveTo(end.x, end.y);
+    ctx.lineTo(end.x - dx - dy, end.y + dx - dy);
+    ctx.lineTo(end.x - dx + dy, end.y - dx - dy);
+    ctx.closePath();
+    ctx.fill();
+  }
 }
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 3c2b847..f730b1d 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -20,10 +20,11 @@
   ConversionJobName,
   ConversionJobStatus
 } from '../common/conversion_jobs';
+import {createEmptyState} from '../common/empty_state';
 import {Engine} from '../common/engine';
 import {MetricResult} from '../common/metric_data';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
-import {CallsiteInfo, createEmptyState, State} from '../common/state';
+import {CallsiteInfo, State} from '../common/state';
 import {fromNs, toNs} from '../common/time';
 
 import {Analytics, initAnalytics} from './analytics';
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index b264c3c..9265090 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -18,8 +18,9 @@
 import {defer} from '../base/deferred';
 import {assertExists, reportError, setErrorHandler} from '../base/logging';
 import {Actions, DeferredAction, StateActions} from '../common/actions';
+import {createEmptyState} from '../common/empty_state';
 import {initializeImmerJs} from '../common/immer_init';
-import {createEmptyState, State} from '../common/state';
+import {State} from '../common/state';
 import {initWasm} from '../common/wasm_engine_proxy';
 import {ControllerWorkerInitMessage} from '../common/worker_messages';
 import {
diff --git a/ui/src/frontend/named_slice_track.ts b/ui/src/frontend/named_slice_track.ts
new file mode 100644
index 0000000..90534ad
--- /dev/null
+++ b/ui/src/frontend/named_slice_track.ts
@@ -0,0 +1,85 @@
+// Copyright (C) 2021 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 {Actions} from '../common/actions';
+import {
+  Color,
+  hslForSlice,
+} from '../common/colorizer';
+import {STR_NULL} from '../common/query_result';
+
+import {
+  BASE_SLICE_ROW,
+  BaseSliceTrack,
+  BaseSliceTrackTypes,
+  OnSliceClickArgs,
+  OnSliceOverArgs,
+} from './base_slice_track';
+import {globals} from './globals';
+import {NewTrackArgs} from './track';
+
+export const NAMED_SLICE_ROW = {
+  // Base columns (tsq, ts, dur, id, depth).
+  ...BASE_SLICE_ROW,
+
+  // Impl-specific columns.
+  name: STR_NULL,
+};
+export type NamedSliceRow = typeof NAMED_SLICE_ROW;
+
+export interface NamedSliceTrackTypes extends BaseSliceTrackTypes {
+  row: NamedSliceRow;
+}
+
+export abstract class NamedSliceTrack<
+    T extends NamedSliceTrackTypes = NamedSliceTrackTypes> extends
+    BaseSliceTrack<T> {
+  constructor(args: NewTrackArgs) {
+    super(args);
+  }
+
+  // This is used by the base class to call iter().
+  getRowSpec(): T['row'] {
+    return NAMED_SLICE_ROW;
+  }
+
+  // Converts a SQL result row to an "Impl" Slice.
+  rowToSlice(row: T['row']): T['slice'] {
+    const baseSlice = super.rowToSlice(row);
+    // Ignore PIDs or numeric arguments when hashing.
+    const name = row.name || '';
+    const nameForHashing = name.replace(/\s?\d+/g, '');
+    const hsl = hslForSlice(nameForHashing, /*isSelected=*/ false);
+    // We cache the color so we hash only once per query.
+    const baseColor: Color = {c: '', h: hsl[0], s: hsl[1], l: hsl[2]};
+    return {...baseSlice, title: name, baseColor};
+  }
+
+  onSliceOver(args: OnSliceOverArgs<T['slice']>) {
+    const name = args.slice.title;
+    args.tooltip = [name];
+  }
+
+  onSliceClick(args: OnSliceClickArgs<T['slice']>) {
+    globals.makeSelection(Actions.selectChromeSlice({
+      id: args.slice.id,
+      trackId: this.trackId,
+
+      // |table| here can be either 'slice' or 'annotation'. The
+      // AnnotationSliceTrack overrides the onSliceClick and sets this to
+      // 'annotation'
+      table: 'slice',
+    }));
+  }
+}
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index 5a6b88f..9e75844 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -213,21 +213,17 @@
               {pivotTableId, row, column, rowIndices, expandedRowColumns}));
         continue;
       }
-      let indentationLevel = 0;
-      let expandIconSpace = 0;
-      if (column.aggregation !== undefined) {
-        indentationLevel = rowIndices.length - 1;
-      } else {
-        indentationLevel = row.depth;
-        if (row.depth > 0 && column.isStackColumn) {
-          expandIconSpace = 3;
-        }
-      }
-      // For each indentation level add 2 spaces, if we have an expansion button
-      // add 3 spaces to cover the icon size.
+
       let value = row.row[column.name]!.toString();
-      value = value.padStart(
-          (indentationLevel * 2) + expandIconSpace + value.length, ' ');
+      if (column.aggregation === undefined) {
+        // For each indentation level add 2 spaces, if we have an expansion
+        // button add 3 spaces to cover the icon size.
+        let padding = 2 * row.depth;
+        if (row.depth > 0 && column.isStackColumn) {
+          padding += 3;
+        }
+        value = value.padStart(padding + value.length, ' ');
+      }
       cells.push(m('td.allow-white-space', value));
     }
     return m('tr', cells);
diff --git a/ui/src/frontend/post_message_handler.ts b/ui/src/frontend/post_message_handler.ts
index 68d010f..5309b6b 100644
--- a/ui/src/frontend/post_message_handler.ts
+++ b/ui/src/frontend/post_message_handler.ts
@@ -91,12 +91,11 @@
     throw new Error('Incoming message trace buffer is empty');
   }
 
-  /* Removing this event listener because it is fired multiple times
-   * with the same payload, causing issues such as b/182502595. This can be
-   * reproduced by taking https://jsfiddle.net/primiano/1hd0a4wj/68/ and
-   * replacing 'ui.perfetto.dev' -> 'localhost:10000'. If you open multiple
-   * traces or the same trace multiple times, every tab will receive a message
-   * for every window.open() call.
+  /* Removing this event listener to avoid callers posting the trace multiple
+   * times. If the callers add an event listener which upon receiving 'PONG'
+   * posts the trace to ui.perfetto.dev, the callers can receive multiple 'PONG'
+   * messages and accidentally post the trace multiple times. This was part of
+   * the cause of b/182502595.
    */
   window.removeEventListener('message', postMessageHandler);
 
diff --git a/ui/src/frontend/record_config.ts b/ui/src/frontend/record_config.ts
index 77da241..847a270 100644
--- a/ui/src/frontend/record_config.ts
+++ b/ui/src/frontend/record_config.ts
@@ -12,20 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  getDefaultRecordingTargets,
-  NamedRecordConfig,
-  RecordConfig,
-  RecordingTarget
-} from '../common/state';
+import {getDefaultRecordingTargets, RecordingTarget} from '../common/state';
 import {
   createEmptyRecordConfig,
-  JsonObject,
-  runParser,
-  validateNamedRecordConfig,
-  validateRecordConfig,
-  ValidationResult
-} from '../controller/validate_config';
+  NamedRecordConfig,
+  namedRecordConfigValidator,
+  RecordConfig,
+  recordConfigValidator
+} from '../controller/record_config_types';
+import {runValidator, ValidationResult} from '../controller/validators';
 
 const LOCAL_STORAGE_RECORD_CONFIGS_KEY = 'recordConfigs';
 const LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY = 'autosaveConfig';
@@ -129,8 +124,8 @@
 
         for (let i = 0; i < parsedConfigsLocalStorage.length; ++i) {
           try {
-            validConfigLocalStorage.push(runParser(
-                validateNamedRecordConfig, parsedConfigsLocalStorage[i]));
+            validConfigLocalStorage.push(runValidator(
+                namedRecordConfigValidator, parsedConfigsLocalStorage[i]));
           } catch {
             // Parsing failed with unrecoverable error (e.g. title or key are
             // missing), ignore the result.
@@ -177,8 +172,7 @@
     }
     const parsed = JSON.parse(savedItem);
     if (parsed !== null && typeof parsed === 'object') {
-      this.config =
-          runParser(validateRecordConfig, parsed as JsonObject).result;
+      this.config = runValidator(recordConfigValidator, parsed).result;
       this.hasSavedConfig = true;
     }
   }
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index dd1150c..e1b853e 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -32,12 +32,14 @@
   isLinuxTarget,
   LoadedConfig,
   MAX_TIME,
-  RecordConfig,
   RecordingTarget,
   RecordMode
 } from '../common/state';
 import {AdbOverWebUsb} from '../controller/adb';
-import {createEmptyRecordConfig} from '../controller/validate_config';
+import {
+  createEmptyRecordConfig,
+  RecordConfig
+} from '../controller/record_config_types';
 
 import {globals} from './globals';
 import {createPage, PageAttrs} from './pages';
@@ -66,7 +68,7 @@
   id: 'persistConfigsUI',
   name: 'Config persistence UI',
   description: 'Show experimental config persistence UI on the record page.',
-  defaultValue: false,
+  defaultValue: true,
 });
 
 const POLL_INTERVAL_MS = [250, 500, 1000, 2500, 5000, 30000, 60000];
@@ -754,7 +756,7 @@
         m(Toggle, {
           title: 'Resolve kernel symbols',
           cssClass: '.thin',
-          descr: `Enables lookup via /proc/kallsyms for workqueue, 
+          descr: `Enables lookup via /proc/kallsyms for workqueue,
               sched_blocked_reason and other events (userdebug/eng builds only).`,
           setEnabled: (cfg, val) => cfg.symbolizeKsyms = val,
           isEnabled: (cfg) => cfg.symbolizeKsyms
@@ -1434,7 +1436,7 @@
                 }
               },
               m(`li${routePage === 'config' ? '.active' : ''}`,
-                m('i.material-icons', 'tune'),
+                m('i.material-icons', 'save'),
                 m('.title', 'Saved configs'),
                 m('.sub', 'Manage local configs'))) :
             null),
diff --git a/ui/src/frontend/record_widgets.ts b/ui/src/frontend/record_widgets.ts
index 082bbd4..11a36eb 100644
--- a/ui/src/frontend/record_widgets.ts
+++ b/ui/src/frontend/record_widgets.ts
@@ -15,12 +15,12 @@
 import {Draft, produce} from 'immer';
 import * as m from 'mithril';
 
+import {assertExists} from '../base/logging';
 import {Actions} from '../common/actions';
-import {RecordConfig} from '../common/state';
+import {RecordConfig} from '../controller/record_config_types';
 
 import {copyToClipboard} from './clipboard';
 import {globals} from './globals';
-import {assertExists} from '../base/logging';
 
 declare type Setter<T> = (draft: Draft<RecordConfig>, val: T) => void;
 declare type Getter<T> = (cfg: RecordConfig) => T;
diff --git a/ui/src/frontend/slice.ts b/ui/src/frontend/slice.ts
new file mode 100644
index 0000000..ddbbb98
--- /dev/null
+++ b/ui/src/frontend/slice.ts
@@ -0,0 +1,40 @@
+// Copyright (C) 2021 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 {Color} from '../common/colorizer';
+
+export interface Slice {
+  // These properties are updated only once per query result when the Slice
+  // object is created and don't change afterwards.
+  readonly id: number;
+  readonly startS: number;
+  readonly durationS: number;
+  readonly depth: number;
+  readonly flags: number;
+
+  // These can be changed by the Impl.
+  title: string;
+  subTitle: string;
+  baseColor: Color;
+  color: Color;
+
+  // These properties change @ 60FPS and shouldn't be touched by the Impl.
+  // to the Impl. These are really ephemeral and change on every frame. But
+  // the Impl doesn't see every frame. Somebody might be tempted to reason on
+  // those but then fail.
+  // TODO(hjd): Would be nice to find some clever typing hack to avoid exposing
+  // these.
+  x: number;
+  w: number;
+}
diff --git a/ui/src/frontend/slice_layout.ts b/ui/src/frontend/slice_layout.ts
new file mode 100644
index 0000000..1f28617
--- /dev/null
+++ b/ui/src/frontend/slice_layout.ts
@@ -0,0 +1,76 @@
+// Copyright (C) 2021 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.
+
+export interface SliceLayoutBase {
+  padding: number;     // top/bottom pixel padding between slices and track.
+  rowSpacing: number;  // Spacing between rows.
+  minDepth: number;    // Minimum depth a slice can be (normally zero)
+  // Maximum depth a slice can be plus 1 (a half open range with minDepth).
+  // We have a optimization for when maxDepth - minDepth == 1 so it is useful
+  // to set this correctly:
+  maxDepth: number;
+}
+
+export const SLICE_LAYOUT_BASE_DEFAULTS: SliceLayoutBase = Object.freeze({
+  padding: 3,
+  rowSpacing: 0,
+  minDepth: 0,
+  // A realistic bound to avoid tracks with unlimited height. If somebody wants
+  // extremely deep tracks they need to change this explicitly.
+  maxDepth: 128,
+});
+
+export interface SliceLayoutFixed extends SliceLayoutBase {
+  heightMode: 'FIXED';
+  fixedHeight: number;  // Outer height of the track.
+}
+
+export const SLICE_LAYOUT_FIXED_DEFAULTS: SliceLayoutFixed = Object.freeze({
+  ...SLICE_LAYOUT_BASE_DEFAULTS,
+  heightMode: 'FIXED',
+  fixedHeight: 30,
+});
+
+export interface SliceLayoutFitContent extends SliceLayoutBase {
+  heightMode: 'FIT_CONTENT';
+  sliceHeight: number;  // Only when heightMode = 'FIT_CONTENT'.
+}
+
+export const SLICE_LAYOUT_FIT_CONTENT_DEFAULTS: SliceLayoutFitContent =
+    Object.freeze({
+      ...SLICE_LAYOUT_BASE_DEFAULTS,
+      heightMode: 'FIT_CONTENT',
+      sliceHeight: 18,
+    });
+
+export interface SliceLayoutFlat extends SliceLayoutBase {
+  heightMode: 'FIXED';
+  fixedHeight: number;  // Outer height of the track.
+  minDepth: 0;
+  maxDepth: 1;
+}
+
+export const SLICE_LAYOUT_FLAT_DEFAULTS: SliceLayoutFlat = Object.freeze({
+  ...SLICE_LAYOUT_BASE_DEFAULTS,
+  minDepth: 0,
+  maxDepth: 1,
+  heightMode: 'FIXED',
+  fixedHeight: 30,
+});
+
+export type SliceLayout =
+    SliceLayoutFixed|SliceLayoutFitContent|SliceLayoutFlat;
+
+export const DEFAULT_SLICE_LAYOUT: SliceLayout =
+    SLICE_LAYOUT_FIT_CONTENT_DEFAULTS;
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index fed8355..f319614 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -59,9 +59,14 @@
  */
 export abstract class Track<Config = {}, Data extends TrackData = TrackData> {
   // The UI-generated track ID (not to be confused with the SQL track.id).
-  private trackId: string;
+  protected readonly trackId: string;
   protected readonly engine: Engine;
 
+  // When true this is a new controller-less track type.
+  // TODO(hjd): eventually all tracks will be controller-less and this
+  // should be removed then.
+  protected frontendOnly = false;
+
   // Caches the last state.track[this.trackId]. This is to deal with track
   // deletion, see comments in trackState() below.
   private lastTrackState: TrackState;
@@ -94,6 +99,9 @@
   }
 
   data(): Data|undefined {
+    if (this.frontendOnly) {
+      return undefined;
+    }
     return globals.trackDataStore.get(this.trackId) as Data;
   }
 
@@ -115,11 +123,13 @@
     return false;
   }
 
-  onMouseOut() {}
+  onMouseOut(): void {}
+
+  onFullRedraw(): void {}
 
   render(ctx: CanvasRenderingContext2D) {
     globals.frontendLocalState.addVisibleTrack(this.trackState.id);
-    if (this.data() === undefined) {
+    if (this.data() === undefined && !this.frontendOnly) {
       const {visibleWindowTime, timeScale} = globals.frontendLocalState;
       const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
       const endPx = Math.ceil(timeScale.timeToPx(visibleWindowTime.end));
@@ -130,37 +140,64 @@
   }
 
   drawTrackHoverTooltip(
-      ctx: CanvasRenderingContext2D, xPos: number, text: string,
+      ctx: CanvasRenderingContext2D, pos: {x: number, y: number}, text: string,
       text2?: string) {
     ctx.font = '10px Roboto Condensed';
-    const textWidth = ctx.measureText(text).width;
-    let width = textWidth;
-    let textYPos = this.getHeight() / 2;
-
-    if (text2 !== undefined) {
-      const text2Width = ctx.measureText(text2).width;
-      width = Math.max(textWidth, text2Width);
-      textYPos = this.getHeight() / 2 - 6;
-    }
-
-    // Move tooltip over if it would go off the right edge of the viewport.
-    const rectWidth = width + 16;
-    const endPx = globals.frontendLocalState.timeScale.endPx;
-    if (xPos + rectWidth > endPx) {
-      xPos -= (xPos + rectWidth - endPx);
-    }
-
-    ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
-    const rectMargin = this.getHeight() / 12;
-    ctx.fillRect(
-        xPos, rectMargin, rectWidth, this.getHeight() - rectMargin * 2);
-    ctx.fillStyle = 'hsl(200, 50%, 40%)';
-    ctx.textAlign = 'left';
     ctx.textBaseline = 'middle';
-    ctx.fillText(text, xPos + 8, textYPos);
+    ctx.textAlign = 'left';
 
+    // TODO(hjd): Avoid measuring text all the time (just use monospace?)
+    const textMetrics = ctx.measureText(text);
+    const text2Metrics = ctx.measureText(text2 || '');
+
+    // Padding on each side of the box containing the tooltip:
+    const paddingPx = 4;
+
+    // Figure out the width of the tool tip box:
+    let width = Math.max(textMetrics.width, text2Metrics.width);
+    width += paddingPx * 2;
+
+    // and the height:
+    let height = 0;
+    height += textMetrics.fontBoundingBoxAscent;
+    height += textMetrics.fontBoundingBoxDescent;
     if (text2 !== undefined) {
-      ctx.fillText(text2, xPos + 8, this.getHeight() / 2 + 6);
+      height += text2Metrics.fontBoundingBoxAscent;
+      height += text2Metrics.fontBoundingBoxDescent;
+    }
+    height += paddingPx * 2;
+
+    let x = pos.x;
+    let y = pos.y;
+
+    // Move box to the top right of the mouse:
+    x += 10;
+    y -= 10;
+
+    // Ensure the box is on screen:
+    const endPx = globals.frontendLocalState.timeScale.endPx;
+    if (x + width > endPx) {
+      x -= x + width - endPx;
+    }
+    if (y < 0) {
+      y = 0;
+    }
+    if (y + height > this.getHeight()) {
+      y -= y + height - this.getHeight();
+    }
+
+    // Draw everything:
+    ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
+    ctx.fillRect(x, y, width, height);
+
+    ctx.fillStyle = 'hsl(200, 50%, 40%)';
+    ctx.fillText(
+        text, x + paddingPx, y + paddingPx + textMetrics.fontBoundingBoxAscent);
+    if (text2 !== undefined) {
+      const yOffsetPx = textMetrics.fontBoundingBoxAscent +
+          textMetrics.fontBoundingBoxDescent +
+          text2Metrics.fontBoundingBoxAscent;
+      ctx.fillText(text, x + paddingPx, y + paddingPx + yOffsetPx);
     }
   }
 
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index e842d94..cc05cc3 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -106,6 +106,12 @@
       }
     }
 
+    let child = '';
+    if (this.summaryTrackState.labels &&
+        this.summaryTrackState.labels.length > 0) {
+      child = this.summaryTrackState.labels.join(', ');
+    }
+
     return m(
         `.track-group-panel[collapsed=${collapsed}]`,
         {id: 'track_' + this.trackGroupId},
@@ -140,7 +146,10 @@
                 checkBox) :
               ''),
 
-        this.summaryTrack ? m(TrackContent, {track: this.summaryTrack}) : null);
+        this.summaryTrack ? m(TrackContent,
+                              {track: this.summaryTrack},
+                              this.trackGroupState.collapsed ? '' : child) :
+                            null);
   }
 
   oncreate(vnode: m.CVnodeDOM<Attrs>) {
@@ -158,6 +167,9 @@
       this.backgroundColor =
           getComputedStyle(dom).getPropertyValue('--expanded-background');
     }
+    if (this.summaryTrack !== undefined) {
+      this.summaryTrack.onFullRedraw();
+    }
   }
 
   highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 084342b..a916e93 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -182,47 +182,53 @@
   private mouseDownY?: number;
   private selectionOccurred = false;
 
-  view({attrs}: m.CVnode<TrackContentAttrs>) {
-    return m('.track-content', {
-      onmousemove: (e: PerfettoMouseEvent) => {
-        attrs.track.onMouseMove({x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY});
-        globals.rafScheduler.scheduleRedraw();
-      },
-      onmouseout: () => {
-        attrs.track.onMouseOut();
-        globals.rafScheduler.scheduleRedraw();
-      },
-      onmousedown: (e: PerfettoMouseEvent) => {
-        this.mouseDownX = e.layerX;
-        this.mouseDownY = e.layerY;
-      },
-      onmouseup: (e: PerfettoMouseEvent) => {
-        if (this.mouseDownX === undefined || this.mouseDownY === undefined) {
-          return;
-        }
-        if (Math.abs(e.layerX - this.mouseDownX) > 1 ||
-            Math.abs(e.layerY - this.mouseDownY) > 1) {
-          this.selectionOccurred = true;
-        }
-        this.mouseDownX = undefined;
-        this.mouseDownY = undefined;
-      },
-      onclick: (e: PerfettoMouseEvent) => {
-        // This click event occurs after any selection mouse up/drag events
-        // so we have to look if the mouse moved during this click to know
-        // if a selection occurred.
-        if (this.selectionOccurred) {
-          this.selectionOccurred = false;
-          return;
-        }
-        // Returns true if something was selected, so stop propagation.
-        if (attrs.track.onMouseClick(
-                {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY})) {
-          e.stopPropagation();
-        }
-        globals.rafScheduler.scheduleRedraw();
-      }
-    });
+  view(node: m.CVnode<TrackContentAttrs>) {
+    const attrs = node.attrs;
+    return m(
+        '.track-content',
+        {
+          onmousemove: (e: PerfettoMouseEvent) => {
+            attrs.track.onMouseMove(
+                {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY});
+            globals.rafScheduler.scheduleRedraw();
+          },
+          onmouseout: () => {
+            attrs.track.onMouseOut();
+            globals.rafScheduler.scheduleRedraw();
+          },
+          onmousedown: (e: PerfettoMouseEvent) => {
+            this.mouseDownX = e.layerX;
+            this.mouseDownY = e.layerY;
+          },
+          onmouseup: (e: PerfettoMouseEvent) => {
+            if (this.mouseDownX === undefined ||
+                this.mouseDownY === undefined) {
+              return;
+            }
+            if (Math.abs(e.layerX - this.mouseDownX) > 1 ||
+                Math.abs(e.layerY - this.mouseDownY) > 1) {
+              this.selectionOccurred = true;
+            }
+            this.mouseDownX = undefined;
+            this.mouseDownY = undefined;
+          },
+          onclick: (e: PerfettoMouseEvent) => {
+            // This click event occurs after any selection mouse up/drag events
+            // so we have to look if the mouse moved during this click to know
+            // if a selection occurred.
+            if (this.selectionOccurred) {
+              this.selectionOccurred = false;
+              return;
+            }
+            // Returns true if something was selected, so stop propagation.
+            if (attrs.track.onMouseClick(
+                    {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY})) {
+              e.stopPropagation();
+            }
+            globals.rafScheduler.scheduleRedraw();
+          }
+        },
+        node.children);
   }
 }
 
@@ -308,6 +314,18 @@
     return m(TrackComponent, {trackState: this.trackState, track: this.track});
   }
 
+  oncreate() {
+    if (this.track !== undefined) {
+      this.track.onFullRedraw();
+    }
+  }
+
+  onupdate() {
+    if (this.track !== undefined) {
+      this.track.onFullRedraw();
+    }
+  }
+
   highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
     const localState = globals.frontendLocalState;
     const selection = globals.state.currentSelection;
diff --git a/ui/src/tracks/chrome_slices/frontend.ts b/ui/src/tracks/chrome_slices/frontend.ts
index abc7129..dc286ff 100644
--- a/ui/src/tracks/chrome_slices/frontend.ts
+++ b/ui/src/tracks/chrome_slices/frontend.ts
@@ -153,8 +153,7 @@
       }
 
       if (isIncomplete && rect.width > SLICE_HEIGHT / 4) {
-        drawIncompleteSlice(
-            ctx, rect.left, rect.top, rect.width, SLICE_HEIGHT, color);
+        drawIncompleteSlice(ctx, rect.left, rect.top, rect.width, SLICE_HEIGHT);
       } else if (isThreadSlice) {
         // We draw two rectangles, representing the ratio between wall time and
         // time spent on cpu.
diff --git a/ui/src/tracks/counter/frontend.ts b/ui/src/tracks/counter/frontend.ts
index 2836c43..45908a6 100644
--- a/ui/src/tracks/counter/frontend.ts
+++ b/ui/src/tracks/counter/frontend.ts
@@ -84,7 +84,7 @@
     return new CounterTrack(args);
   }
 
-  private mouseXpos = 0;
+  private mousePos = {x: 0, y: 0};
   private hoveredValue: number|undefined = undefined;
   private hoveredTs: number|undefined = undefined;
   private hoveredTsEnd: number|undefined = undefined;
@@ -261,7 +261,7 @@
       ctx.stroke();
 
       // Draw the tooltip.
-      this.drawTrackHoverTooltip(ctx, this.mouseXpos, text);
+      this.drawTrackHoverTooltip(ctx, this.mousePos, text);
     }
 
     // Write the Y scale on the top left corner.
@@ -296,12 +296,12 @@
         timeScale.timeToPx(data.end));
   }
 
-  onMouseMove({x}: {x: number, y: number}) {
+  onMouseMove(pos: {x: number, y: number}) {
     const data = this.data();
     if (data === undefined) return;
-    this.mouseXpos = x;
+    this.mousePos = pos;
     const {timeScale} = globals.frontendLocalState;
-    const time = timeScale.pxToTime(x);
+    const time = timeScale.pxToTime(pos.x);
 
     const values = this.config.scale === 'DELTA_FROM_PREVIOUS' ?
         data.totalDeltas :
diff --git a/ui/src/tracks/cpu_freq/frontend.ts b/ui/src/tracks/cpu_freq/frontend.ts
index 6feed5f..c8b2d74 100644
--- a/ui/src/tracks/cpu_freq/frontend.ts
+++ b/ui/src/tracks/cpu_freq/frontend.ts
@@ -36,7 +36,7 @@
     return new CpuFreqTrack(args);
   }
 
-  private mouseXpos = 0;
+  private mousePos = {x: 0, y: 0};
   private hoveredValue: number|undefined = undefined;
   private hoveredTs: number|undefined = undefined;
   private hoveredTsEnd: number|undefined = undefined;
@@ -192,7 +192,7 @@
       }
 
       // Draw the tooltip.
-      this.drawTrackHoverTooltip(ctx, this.mouseXpos, text);
+      this.drawTrackHoverTooltip(ctx, this.mousePos, text);
     }
 
     // Write the Y scale on the top left corner.
@@ -214,12 +214,12 @@
         timeScale.timeToPx(data.end));
   }
 
-  onMouseMove({x}: {x: number, y: number}) {
+  onMouseMove(pos: {x: number, y: number}) {
     const data = this.data();
     if (data === undefined) return;
-    this.mouseXpos = x;
+    this.mousePos = pos;
     const {timeScale} = globals.frontendLocalState;
-    const time = timeScale.pxToTime(x);
+    const time = timeScale.pxToTime(pos.x);
 
     const [left, right] = searchSegment(data.timestamps, time);
     this.hoveredTs = left === -1 ? undefined : data.timestamps[left];
diff --git a/ui/src/tracks/cpu_slices/frontend.ts b/ui/src/tracks/cpu_slices/frontend.ts
index e81c55f..16f140c 100644
--- a/ui/src/tracks/cpu_slices/frontend.ts
+++ b/ui/src/tracks/cpu_slices/frontend.ts
@@ -39,7 +39,7 @@
     return new CpuSliceTrack(args);
   }
 
-  private mouseXpos?: number;
+  private mousePos?: {x: number, y: number};
   private utidHoveredInThisTrack = -1;
 
   constructor(args: NewTrackArgs) {
@@ -211,28 +211,28 @@
     }
 
     const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
-    if (hoveredThread !== undefined && this.mouseXpos !== undefined) {
+    if (hoveredThread !== undefined && this.mousePos !== undefined) {
       const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
       if (hoveredThread.pid) {
         const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
-        this.drawTrackHoverTooltip(ctx, this.mouseXpos, pidText, tidText);
+        this.drawTrackHoverTooltip(ctx, this.mousePos, pidText, tidText);
       } else {
-        this.drawTrackHoverTooltip(ctx, this.mouseXpos, tidText);
+        this.drawTrackHoverTooltip(ctx, this.mousePos, tidText);
       }
     }
   }
 
-  onMouseMove({x, y}: {x: number, y: number}) {
+  onMouseMove(pos: {x: number, y: number}) {
     const data = this.data();
-    this.mouseXpos = x;
+    this.mousePos = pos;
     if (data === undefined) return;
     const {timeScale} = globals.frontendLocalState;
-    if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
+    if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
       this.utidHoveredInThisTrack = -1;
       globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
       return;
     }
-    const t = timeScale.pxToTime(x);
+    const t = timeScale.pxToTime(pos.x);
     let hoveredUtid = -1;
 
     for (let i = 0; i < data.starts.length; i++) {
@@ -254,7 +254,7 @@
   onMouseOut() {
     this.utidHoveredInThisTrack = -1;
     globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-    this.mouseXpos = 0;
+    this.mousePos = undefined;
   }
 
   onMouseClick({x}: {x: number}) {
diff --git a/ui/src/tracks/generic_slice_track/index.ts b/ui/src/tracks/generic_slice_track/index.ts
new file mode 100644
index 0000000..aaff6ff
--- /dev/null
+++ b/ui/src/tracks/generic_slice_track/index.ts
@@ -0,0 +1,48 @@
+// Copyright (C) 2021 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 {
+  NamedSliceTrack,
+  NamedSliceTrackTypes
+} from '../../frontend/named_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {trackRegistry} from '../../frontend/track_registry';
+
+export interface GenericSliceTrackConfig {
+  sqlTrackId: number;
+}
+
+export interface GenericSliceTrackTypes extends NamedSliceTrackTypes {
+  config: GenericSliceTrackConfig;
+}
+
+export class GenericSliceTrack extends NamedSliceTrack<GenericSliceTrackTypes> {
+  static readonly kind = 'GenericSliceTrack';
+  static create(args: NewTrackArgs) {
+    return new GenericSliceTrack(args);
+  }
+
+  constructor(args: NewTrackArgs) {
+    super(args);
+  }
+
+  async initSqlTable(tableName: string): Promise<void> {
+    const sql = `create view ${tableName} as
+    select ts, dur, id, depth, ifnull(name, '') as name
+    from slice where track_id = ${this.config.sqlTrackId}`;
+    await this.engine.query(sql);
+  }
+}
+
+trackRegistry.register(GenericSliceTrack);
diff --git a/ui/src/tracks/process_scheduling/frontend.ts b/ui/src/tracks/process_scheduling/frontend.ts
index dc5f9c6..b83b1289 100644
--- a/ui/src/tracks/process_scheduling/frontend.ts
+++ b/ui/src/tracks/process_scheduling/frontend.ts
@@ -37,7 +37,7 @@
     return new ProcessSchedulingTrack(args);
   }
 
-  private mouseXpos?: number;
+  private mousePos?: {x: number, y: number};
   private utidHoveredInThisTrack = -1;
 
   constructor(args: NewTrackArgs) {
@@ -113,31 +113,31 @@
     }
 
     const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
-    if (hoveredThread !== undefined && this.mouseXpos !== undefined) {
+    if (hoveredThread !== undefined && this.mousePos !== undefined) {
       const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
       if (hoveredThread.pid) {
         const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
-        this.drawTrackHoverTooltip(ctx, this.mouseXpos, pidText, tidText);
+        this.drawTrackHoverTooltip(ctx, this.mousePos, pidText, tidText);
       } else {
-        this.drawTrackHoverTooltip(ctx, this.mouseXpos, tidText);
+        this.drawTrackHoverTooltip(ctx, this.mousePos, tidText);
       }
     }
   }
 
-  onMouseMove({x, y}: {x: number, y: number}) {
+  onMouseMove(pos: {x: number, y: number}) {
     const data = this.data();
-    this.mouseXpos = x;
+    this.mousePos = pos;
     if (data === undefined) return;
-    if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
+    if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
       this.utidHoveredInThisTrack = -1;
       globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
       return;
     }
 
     const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
-    const cpu = Math.floor((y - MARGIN_TOP) / (cpuTrackHeight + 1));
+    const cpu = Math.floor((pos.y - MARGIN_TOP) / (cpuTrackHeight + 1));
     const {timeScale} = globals.frontendLocalState;
-    const t = timeScale.pxToTime(x);
+    const t = timeScale.pxToTime(pos.x);
 
     const [i, j] = searchRange(data.starts, t, searchEq(data.cpus, cpu));
     if (i === j || i >= data.starts.length || t > data.ends[i]) {
@@ -156,7 +156,7 @@
   onMouseOut() {
     this.utidHoveredInThisTrack = -1;
     globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-    this.mouseXpos = 0;
+    this.mousePos = undefined;
   }
 }