DO NOT MERGE - Merge ab/7272582

Bug: 190855093
Change-Id: Iddd4bf7d03ac1e0d370393b44a5a4c8108bf5da4
diff --git a/.gn b/.gn
index 692a951..d7ecabe 100644
--- a/.gn
+++ b/.gn
@@ -13,3 +13,4 @@
 # limitations under the License.
 
 buildconfig = "//gn/standalone/BUILDCONFIG.gn"
+script_executable = "python3"
diff --git a/Android.bp b/Android.bp
index 5dd84e5..9338c5a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -83,6 +83,7 @@
     ":perfetto_src_profiling_common_profiler_guardrails",
     ":perfetto_src_profiling_common_unwind_support",
     ":perfetto_src_profiling_memory_daemon",
+    ":perfetto_src_profiling_memory_heapprofd_main",
     ":perfetto_src_profiling_memory_ring_buffer",
     ":perfetto_src_profiling_memory_scoped_spinlock",
     ":perfetto_src_profiling_memory_wire_protocol",
@@ -516,6 +517,9 @@
     ":perfetto_src_ipc_common",
     ":perfetto_src_ipc_host",
     ":perfetto_src_kallsyms_kallsyms",
+    ":perfetto_src_protozero_filtering_bytecode_common",
+    ":perfetto_src_protozero_filtering_bytecode_parser",
+    ":perfetto_src_protozero_filtering_message_filter",
     ":perfetto_src_protozero_protozero",
     ":perfetto_src_traced_probes_android_log_android_log",
     ":perfetto_src_traced_probes_common_common",
@@ -719,6 +723,9 @@
     ":perfetto_src_ipc_client",
     ":perfetto_src_ipc_common",
     ":perfetto_src_ipc_host",
+    ":perfetto_src_protozero_filtering_bytecode_common",
+    ":perfetto_src_protozero_filtering_bytecode_parser",
+    ":perfetto_src_protozero_filtering_message_filter",
     ":perfetto_src_protozero_protozero",
     ":perfetto_src_tracing_client_api_without_backends",
     ":perfetto_src_tracing_common",
@@ -1068,6 +1075,9 @@
     ":perfetto_src_ipc_host",
     ":perfetto_src_ipc_perfetto_ipc",
     ":perfetto_src_kallsyms_kallsyms",
+    ":perfetto_src_protozero_filtering_bytecode_common",
+    ":perfetto_src_protozero_filtering_bytecode_parser",
+    ":perfetto_src_protozero_filtering_message_filter",
     ":perfetto_src_protozero_protozero",
     ":perfetto_src_traced_probes_android_log_android_log",
     ":perfetto_src_traced_probes_common_common",
@@ -1321,6 +1331,9 @@
     ":perfetto_src_ipc_host",
     ":perfetto_src_ipc_perfetto_ipc",
     ":perfetto_src_kallsyms_kallsyms",
+    ":perfetto_src_protozero_filtering_bytecode_common",
+    ":perfetto_src_protozero_filtering_bytecode_parser",
+    ":perfetto_src_protozero_filtering_message_filter",
     ":perfetto_src_protozero_protozero",
     ":perfetto_src_traced_probes_android_log_android_log",
     ":perfetto_src_traced_probes_common_common",
@@ -1727,13 +1740,16 @@
     ":perfetto_src_profiling_memory_ring_buffer",
     ":perfetto_src_profiling_memory_scoped_spinlock",
     ":perfetto_src_profiling_memory_wire_protocol",
+    ":perfetto_src_protozero_filtering_bytecode_common",
+    ":perfetto_src_protozero_filtering_bytecode_parser",
+    ":perfetto_src_protozero_filtering_message_filter",
     ":perfetto_src_protozero_protozero",
     ":perfetto_src_trace_processor_analysis_analysis",
     ":perfetto_src_trace_processor_containers_containers",
-    ":perfetto_src_trace_processor_db_lib",
+    ":perfetto_src_trace_processor_db_db",
     ":perfetto_src_trace_processor_export_json",
     ":perfetto_src_trace_processor_ftrace_descriptors",
-    ":perfetto_src_trace_processor_importers_common",
+    ":perfetto_src_trace_processor_importers_common_common",
     ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
     ":perfetto_src_trace_processor_lib",
     ":perfetto_src_trace_processor_metatrace",
@@ -1745,7 +1761,11 @@
     ":perfetto_src_trace_processor_tables_tables",
     ":perfetto_src_trace_processor_types_types",
     ":perfetto_src_trace_processor_util_descriptors",
+    ":perfetto_src_trace_processor_util_gzip",
+    ":perfetto_src_trace_processor_util_interned_message_view",
+    ":perfetto_src_trace_processor_util_proto_to_args_parser",
     ":perfetto_src_trace_processor_util_protozero_to_text",
+    ":perfetto_src_trace_processor_util_trace_blob_view",
     ":perfetto_src_trace_processor_util_util",
     ":perfetto_src_traced_probes_android_log_android_log",
     ":perfetto_src_traced_probes_common_common",
@@ -3637,7 +3657,6 @@
     "protos/perfetto/metrics/android/fastrpc_metric.proto",
     "protos/perfetto/metrics/android/g2d_metric.proto",
     "protos/perfetto/metrics/android/gpu_metric.proto",
-    "protos/perfetto/metrics/android/heap_profile_callsites.proto",
     "protos/perfetto/metrics/android/hwcomposer.proto",
     "protos/perfetto/metrics/android/hwui_metric.proto",
     "protos/perfetto/metrics/android/ion_metric.proto",
@@ -3687,7 +3706,6 @@
     "protos/perfetto/metrics/android/fastrpc_metric.proto",
     "protos/perfetto/metrics/android/g2d_metric.proto",
     "protos/perfetto/metrics/android/gpu_metric.proto",
-    "protos/perfetto/metrics/android/heap_profile_callsites.proto",
     "protos/perfetto/metrics/android/hwcomposer.proto",
     "protos/perfetto/metrics/android/hwui_metric.proto",
     "protos/perfetto/metrics/android/ion_metric.proto",
@@ -4014,6 +4032,165 @@
   ],
 }
 
+// GN: //protos/perfetto/trace:descriptor
+genrule {
+  name: "perfetto_protos_perfetto_trace_descriptor",
+  srcs: [
+    "protos/perfetto/common/android_energy_consumer_descriptor.proto",
+    "protos/perfetto/common/android_log_constants.proto",
+    "protos/perfetto/common/builtin_clock.proto",
+    "protos/perfetto/common/commit_data_request.proto",
+    "protos/perfetto/common/data_source_descriptor.proto",
+    "protos/perfetto/common/descriptor.proto",
+    "protos/perfetto/common/gpu_counter_descriptor.proto",
+    "protos/perfetto/common/interceptor_descriptor.proto",
+    "protos/perfetto/common/observable_events.proto",
+    "protos/perfetto/common/perf_events.proto",
+    "protos/perfetto/common/sys_stats_counters.proto",
+    "protos/perfetto/common/trace_stats.proto",
+    "protos/perfetto/common/tracing_service_capabilities.proto",
+    "protos/perfetto/common/tracing_service_state.proto",
+    "protos/perfetto/common/track_event_descriptor.proto",
+    "protos/perfetto/config/android/android_log_config.proto",
+    "protos/perfetto/config/android/android_polled_state_config.proto",
+    "protos/perfetto/config/android/packages_list_config.proto",
+    "protos/perfetto/config/chrome/chrome_config.proto",
+    "protos/perfetto/config/data_source_config.proto",
+    "protos/perfetto/config/ftrace/ftrace_config.proto",
+    "protos/perfetto/config/gpu/gpu_counter_config.proto",
+    "protos/perfetto/config/gpu/vulkan_memory_config.proto",
+    "protos/perfetto/config/inode_file/inode_file_config.proto",
+    "protos/perfetto/config/interceptor_config.proto",
+    "protos/perfetto/config/interceptors/console_config.proto",
+    "protos/perfetto/config/power/android_power_config.proto",
+    "protos/perfetto/config/process_stats/process_stats_config.proto",
+    "protos/perfetto/config/profiling/heapprofd_config.proto",
+    "protos/perfetto/config/profiling/java_hprof_config.proto",
+    "protos/perfetto/config/profiling/perf_event_config.proto",
+    "protos/perfetto/config/stress_test_config.proto",
+    "protos/perfetto/config/sys_stats/sys_stats_config.proto",
+    "protos/perfetto/config/test_config.proto",
+    "protos/perfetto/config/trace_config.proto",
+    "protos/perfetto/config/track_event/track_event_config.proto",
+    "protos/perfetto/trace/android/android_log.proto",
+    "protos/perfetto/trace/android/frame_timeline_event.proto",
+    "protos/perfetto/trace/android/gpu_mem_event.proto",
+    "protos/perfetto/trace/android/graphics_frame_event.proto",
+    "protos/perfetto/trace/android/initial_display_state.proto",
+    "protos/perfetto/trace/android/packages_list.proto",
+    "protos/perfetto/trace/chrome/chrome_benchmark_metadata.proto",
+    "protos/perfetto/trace/chrome/chrome_metadata.proto",
+    "protos/perfetto/trace/chrome/chrome_trace_event.proto",
+    "protos/perfetto/trace/clock_snapshot.proto",
+    "protos/perfetto/trace/extension_descriptor.proto",
+    "protos/perfetto/trace/filesystem/inode_file_map.proto",
+    "protos/perfetto/trace/ftrace/binder.proto",
+    "protos/perfetto/trace/ftrace/block.proto",
+    "protos/perfetto/trace/ftrace/cgroup.proto",
+    "protos/perfetto/trace/ftrace/clk.proto",
+    "protos/perfetto/trace/ftrace/compaction.proto",
+    "protos/perfetto/trace/ftrace/cpuhp.proto",
+    "protos/perfetto/trace/ftrace/dmabuf_heap.proto",
+    "protos/perfetto/trace/ftrace/dpu.proto",
+    "protos/perfetto/trace/ftrace/ext4.proto",
+    "protos/perfetto/trace/ftrace/f2fs.proto",
+    "protos/perfetto/trace/ftrace/fastrpc.proto",
+    "protos/perfetto/trace/ftrace/fence.proto",
+    "protos/perfetto/trace/ftrace/filemap.proto",
+    "protos/perfetto/trace/ftrace/ftrace.proto",
+    "protos/perfetto/trace/ftrace/ftrace_event.proto",
+    "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
+    "protos/perfetto/trace/ftrace/ftrace_stats.proto",
+    "protos/perfetto/trace/ftrace/g2d.proto",
+    "protos/perfetto/trace/ftrace/generic.proto",
+    "protos/perfetto/trace/ftrace/gpu_mem.proto",
+    "protos/perfetto/trace/ftrace/i2c.proto",
+    "protos/perfetto/trace/ftrace/ion.proto",
+    "protos/perfetto/trace/ftrace/ipi.proto",
+    "protos/perfetto/trace/ftrace/irq.proto",
+    "protos/perfetto/trace/ftrace/kmem.proto",
+    "protos/perfetto/trace/ftrace/lowmemorykiller.proto",
+    "protos/perfetto/trace/ftrace/mali.proto",
+    "protos/perfetto/trace/ftrace/mdss.proto",
+    "protos/perfetto/trace/ftrace/mm_event.proto",
+    "protos/perfetto/trace/ftrace/oom.proto",
+    "protos/perfetto/trace/ftrace/power.proto",
+    "protos/perfetto/trace/ftrace/raw_syscalls.proto",
+    "protos/perfetto/trace/ftrace/regulator.proto",
+    "protos/perfetto/trace/ftrace/sched.proto",
+    "protos/perfetto/trace/ftrace/scm.proto",
+    "protos/perfetto/trace/ftrace/sde.proto",
+    "protos/perfetto/trace/ftrace/signal.proto",
+    "protos/perfetto/trace/ftrace/sync.proto",
+    "protos/perfetto/trace/ftrace/systrace.proto",
+    "protos/perfetto/trace/ftrace/task.proto",
+    "protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
+    "protos/perfetto/trace/ftrace/thermal.proto",
+    "protos/perfetto/trace/ftrace/vmscan.proto",
+    "protos/perfetto/trace/ftrace/workqueue.proto",
+    "protos/perfetto/trace/gpu/gpu_counter_event.proto",
+    "protos/perfetto/trace/gpu/gpu_log.proto",
+    "protos/perfetto/trace/gpu/gpu_render_stage_event.proto",
+    "protos/perfetto/trace/gpu/vulkan_api_event.proto",
+    "protos/perfetto/trace/gpu/vulkan_memory_event.proto",
+    "protos/perfetto/trace/interned_data/interned_data.proto",
+    "protos/perfetto/trace/memory_graph.proto",
+    "protos/perfetto/trace/perfetto/perfetto_metatrace.proto",
+    "protos/perfetto/trace/perfetto/tracing_service_event.proto",
+    "protos/perfetto/trace/power/android_energy_estimation_breakdown.proto",
+    "protos/perfetto/trace/power/battery_counters.proto",
+    "protos/perfetto/trace/power/power_rails.proto",
+    "protos/perfetto/trace/profiling/deobfuscation.proto",
+    "protos/perfetto/trace/profiling/heap_graph.proto",
+    "protos/perfetto/trace/profiling/profile_common.proto",
+    "protos/perfetto/trace/profiling/profile_packet.proto",
+    "protos/perfetto/trace/profiling/smaps.proto",
+    "protos/perfetto/trace/ps/process_stats.proto",
+    "protos/perfetto/trace/ps/process_tree.proto",
+    "protos/perfetto/trace/sys_stats/sys_stats.proto",
+    "protos/perfetto/trace/system_info.proto",
+    "protos/perfetto/trace/system_info/cpu_info.proto",
+    "protos/perfetto/trace/test_event.proto",
+    "protos/perfetto/trace/test_extensions.proto",
+    "protos/perfetto/trace/trace.proto",
+    "protos/perfetto/trace/trace_packet.proto",
+    "protos/perfetto/trace/trace_packet_defaults.proto",
+    "protos/perfetto/trace/track_event/chrome_application_state_info.proto",
+    "protos/perfetto/trace/track_event/chrome_compositor_scheduler_state.proto",
+    "protos/perfetto/trace/track_event/chrome_content_settings_event_info.proto",
+    "protos/perfetto/trace/track_event/chrome_frame_reporter.proto",
+    "protos/perfetto/trace/track_event/chrome_histogram_sample.proto",
+    "protos/perfetto/trace/track_event/chrome_keyed_service.proto",
+    "protos/perfetto/trace/track_event/chrome_latency_info.proto",
+    "protos/perfetto/trace/track_event/chrome_legacy_ipc.proto",
+    "protos/perfetto/trace/track_event/chrome_message_pump.proto",
+    "protos/perfetto/trace/track_event/chrome_mojo_event_info.proto",
+    "protos/perfetto/trace/track_event/chrome_process_descriptor.proto",
+    "protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto",
+    "protos/perfetto/trace/track_event/chrome_thread_descriptor.proto",
+    "protos/perfetto/trace/track_event/chrome_user_event.proto",
+    "protos/perfetto/trace/track_event/chrome_window_handle_event_info.proto",
+    "protos/perfetto/trace/track_event/counter_descriptor.proto",
+    "protos/perfetto/trace/track_event/debug_annotation.proto",
+    "protos/perfetto/trace/track_event/log_message.proto",
+    "protos/perfetto/trace/track_event/process_descriptor.proto",
+    "protos/perfetto/trace/track_event/source_location.proto",
+    "protos/perfetto/trace/track_event/task_execution.proto",
+    "protos/perfetto/trace/track_event/thread_descriptor.proto",
+    "protos/perfetto/trace/track_event/track_descriptor.proto",
+    "protos/perfetto/trace/track_event/track_event.proto",
+    "protos/perfetto/trace/trigger.proto",
+    "protos/perfetto/trace/ui_state.proto",
+  ],
+  tools: [
+    "aprotoc",
+  ],
+  cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --descriptor_set_out=$(out) $(in)",
+  out: [
+    "perfetto_protos_perfetto_trace_descriptor.bin",
+  ],
+}
+
 // GN: //protos/perfetto/trace/filesystem:cpp
 genrule {
   name: "perfetto_protos_perfetto_trace_filesystem_cpp_gen",
@@ -5139,6 +5316,7 @@
     "protos/perfetto/trace/extension_descriptor.proto",
     "protos/perfetto/trace/memory_graph.proto",
     "protos/perfetto/trace/test_event.proto",
+    "protos/perfetto/trace/test_extensions.proto",
     "protos/perfetto/trace/trace.proto",
     "protos/perfetto/trace/trace_packet.proto",
     "protos/perfetto/trace/trace_packet_defaults.proto",
@@ -5153,6 +5331,7 @@
     "external/perfetto/protos/perfetto/trace/extension_descriptor.gen.cc",
     "external/perfetto/protos/perfetto/trace/memory_graph.gen.cc",
     "external/perfetto/protos/perfetto/trace/test_event.gen.cc",
+    "external/perfetto/protos/perfetto/trace/test_extensions.gen.cc",
     "external/perfetto/protos/perfetto/trace/trace.gen.cc",
     "external/perfetto/protos/perfetto/trace/trace_packet.gen.cc",
     "external/perfetto/protos/perfetto/trace/trace_packet_defaults.gen.cc",
@@ -5167,6 +5346,7 @@
     "protos/perfetto/trace/extension_descriptor.proto",
     "protos/perfetto/trace/memory_graph.proto",
     "protos/perfetto/trace/test_event.proto",
+    "protos/perfetto/trace/test_extensions.proto",
     "protos/perfetto/trace/trace.proto",
     "protos/perfetto/trace/trace_packet.proto",
     "protos/perfetto/trace/trace_packet_defaults.proto",
@@ -5181,6 +5361,7 @@
     "external/perfetto/protos/perfetto/trace/extension_descriptor.gen.h",
     "external/perfetto/protos/perfetto/trace/memory_graph.gen.h",
     "external/perfetto/protos/perfetto/trace/test_event.gen.h",
+    "external/perfetto/protos/perfetto/trace/test_extensions.gen.h",
     "external/perfetto/protos/perfetto/trace/trace.gen.h",
     "external/perfetto/protos/perfetto/trace/trace_packet.gen.h",
     "external/perfetto/protos/perfetto/trace/trace_packet_defaults.gen.h",
@@ -5199,6 +5380,7 @@
     "protos/perfetto/trace/extension_descriptor.proto",
     "protos/perfetto/trace/memory_graph.proto",
     "protos/perfetto/trace/test_event.proto",
+    "protos/perfetto/trace/test_extensions.proto",
     "protos/perfetto/trace/trace.proto",
     "protos/perfetto/trace/trace_packet.proto",
     "protos/perfetto/trace/trace_packet_defaults.proto",
@@ -5212,6 +5394,7 @@
     "external/perfetto/protos/perfetto/trace/extension_descriptor.pb.cc",
     "external/perfetto/protos/perfetto/trace/memory_graph.pb.cc",
     "external/perfetto/protos/perfetto/trace/test_event.pb.cc",
+    "external/perfetto/protos/perfetto/trace/test_extensions.pb.cc",
     "external/perfetto/protos/perfetto/trace/trace.pb.cc",
     "external/perfetto/protos/perfetto/trace/trace_packet.pb.cc",
     "external/perfetto/protos/perfetto/trace/trace_packet_defaults.pb.cc",
@@ -5226,6 +5409,7 @@
     "protos/perfetto/trace/extension_descriptor.proto",
     "protos/perfetto/trace/memory_graph.proto",
     "protos/perfetto/trace/test_event.proto",
+    "protos/perfetto/trace/test_extensions.proto",
     "protos/perfetto/trace/trace.proto",
     "protos/perfetto/trace/trace_packet.proto",
     "protos/perfetto/trace/trace_packet_defaults.proto",
@@ -5239,6 +5423,7 @@
     "external/perfetto/protos/perfetto/trace/extension_descriptor.pb.h",
     "external/perfetto/protos/perfetto/trace/memory_graph.pb.h",
     "external/perfetto/protos/perfetto/trace/test_event.pb.h",
+    "external/perfetto/protos/perfetto/trace/test_extensions.pb.h",
     "external/perfetto/protos/perfetto/trace/trace.pb.h",
     "external/perfetto/protos/perfetto/trace/trace_packet.pb.h",
     "external/perfetto/protos/perfetto/trace/trace_packet_defaults.pb.h",
@@ -5257,6 +5442,7 @@
     "protos/perfetto/trace/extension_descriptor.proto",
     "protos/perfetto/trace/memory_graph.proto",
     "protos/perfetto/trace/test_event.proto",
+    "protos/perfetto/trace/test_extensions.proto",
     "protos/perfetto/trace/trace.proto",
     "protos/perfetto/trace/trace_packet.proto",
     "protos/perfetto/trace/trace_packet_defaults.proto",
@@ -5271,6 +5457,7 @@
     "external/perfetto/protos/perfetto/trace/extension_descriptor.pbzero.cc",
     "external/perfetto/protos/perfetto/trace/memory_graph.pbzero.cc",
     "external/perfetto/protos/perfetto/trace/test_event.pbzero.cc",
+    "external/perfetto/protos/perfetto/trace/test_extensions.pbzero.cc",
     "external/perfetto/protos/perfetto/trace/trace.pbzero.cc",
     "external/perfetto/protos/perfetto/trace/trace_packet.pbzero.cc",
     "external/perfetto/protos/perfetto/trace/trace_packet_defaults.pbzero.cc",
@@ -5285,6 +5472,7 @@
     "protos/perfetto/trace/extension_descriptor.proto",
     "protos/perfetto/trace/memory_graph.proto",
     "protos/perfetto/trace/test_event.proto",
+    "protos/perfetto/trace/test_extensions.proto",
     "protos/perfetto/trace/trace.proto",
     "protos/perfetto/trace/trace_packet.proto",
     "protos/perfetto/trace/trace_packet_defaults.proto",
@@ -5299,6 +5487,7 @@
     "external/perfetto/protos/perfetto/trace/extension_descriptor.pbzero.h",
     "external/perfetto/protos/perfetto/trace/memory_graph.pbzero.h",
     "external/perfetto/protos/perfetto/trace/test_event.pbzero.h",
+    "external/perfetto/protos/perfetto/trace/test_extensions.pbzero.h",
     "external/perfetto/protos/perfetto/trace/trace.pbzero.h",
     "external/perfetto/protos/perfetto/trace/trace_packet.pbzero.h",
     "external/perfetto/protos/perfetto/trace/trace_packet_defaults.pbzero.h",
@@ -6646,6 +6835,7 @@
     "src/base/logging.cc",
     "src/base/metatrace.cc",
     "src/base/paged_memory.cc",
+    "src/base/periodic_task.cc",
     "src/base/pipe.cc",
     "src/base/status.cc",
     "src/base/string_splitter.cc",
@@ -6685,10 +6875,12 @@
     "src/base/circular_queue_unittest.cc",
     "src/base/flat_set_unittest.cc",
     "src/base/getopt_compat_unittest.cc",
+    "src/base/logging_unittest.cc",
     "src/base/metatrace_unittest.cc",
     "src/base/no_destructor_unittest.cc",
     "src/base/optional_unittest.cc",
     "src/base/paged_memory_unittest.cc",
+    "src/base/periodic_task_unittest.cc",
     "src/base/scoped_file_unittest.cc",
     "src/base/string_splitter_unittest.cc",
     "src/base/string_utils_unittest.cc",
@@ -7097,6 +7289,14 @@
   ],
 }
 
+// GN: //src/profiling/memory:heapprofd_main
+filegroup {
+  name: "perfetto_src_profiling_memory_heapprofd_main",
+  srcs: [
+    "src/profiling/memory/heapprofd.cc",
+  ],
+}
+
 // GN: //src/profiling/memory:malloc_interceptor_bionic_hooks
 filegroup {
   name: "perfetto_src_profiling_memory_malloc_interceptor_bionic_hooks",
@@ -7255,6 +7455,63 @@
   ],
 }
 
+// GN: //src/protozero/filtering:bytecode_common
+filegroup {
+  name: "perfetto_src_protozero_filtering_bytecode_common",
+}
+
+// GN: //src/protozero/filtering:bytecode_generator
+filegroup {
+  name: "perfetto_src_protozero_filtering_bytecode_generator",
+  srcs: [
+    "src/protozero/filtering/filter_bytecode_generator.cc",
+  ],
+}
+
+// GN: //src/protozero/filtering:bytecode_parser
+filegroup {
+  name: "perfetto_src_protozero_filtering_bytecode_parser",
+  srcs: [
+    "src/protozero/filtering/filter_bytecode_parser.cc",
+  ],
+}
+
+// GN: //src/protozero/filtering:filter_util
+filegroup {
+  name: "perfetto_src_protozero_filtering_filter_util",
+  srcs: [
+    "src/protozero/filtering/filter_util.cc",
+  ],
+}
+
+// GN: //src/protozero/filtering:message_filter
+filegroup {
+  name: "perfetto_src_protozero_filtering_message_filter",
+  srcs: [
+    "src/protozero/filtering/message_filter.cc",
+  ],
+}
+
+// GN: //src/protozero/filtering:unittests
+filegroup {
+  name: "perfetto_src_protozero_filtering_unittests",
+  srcs: [
+    "src/protozero/filtering/filter_bytecode_generator_unittest.cc",
+    "src/protozero/filtering/filter_bytecode_parser_unittest.cc",
+    "src/protozero/filtering/filter_util_unittest.cc",
+    "src/protozero/filtering/message_filter_unittest.cc",
+    "src/protozero/filtering/message_tokenizer_unittest.cc",
+  ],
+}
+
+// GN: //src/protozero:proto_ring_buffer
+filegroup {
+  name: "perfetto_src_protozero_proto_ring_buffer",
+  srcs: [
+    "src/protozero/proto_ring_buffer.cc",
+  ],
+}
+
 // GN: //src/protozero/protoc_plugin:cppgen_plugin
 cc_binary_host {
   name: "perfetto_src_protozero_protoc_plugin_cppgen_plugin",
@@ -7478,6 +7735,7 @@
     "src/protozero/message_handle_unittest.cc",
     "src/protozero/message_unittest.cc",
     "src/protozero/proto_decoder_unittest.cc",
+    "src/protozero/proto_ring_buffer_unittest.cc",
     "src/protozero/proto_utils_unittest.cc",
     "src/protozero/scattered_stream_writer_unittest.cc",
     "src/protozero/test/cppgen_conformance_unittest.cc",
@@ -7518,9 +7776,9 @@
   ],
 }
 
-// GN: //src/trace_processor/db:lib
+// GN: //src/trace_processor/db:db
 filegroup {
-  name: "perfetto_src_trace_processor_db_lib",
+  name: "perfetto_src_trace_processor_db_db",
   srcs: [
     "src/trace_processor/db/column.cc",
     "src/trace_processor/db/table.cc",
@@ -7567,9 +7825,9 @@
   ],
 }
 
-// GN: //src/trace_processor/importers:common
+// GN: //src/trace_processor/importers/common:common
 filegroup {
-  name: "perfetto_src_trace_processor_importers_common",
+  name: "perfetto_src_trace_processor_importers_common_common",
   srcs: [
     "src/trace_processor/importers/common/args_tracker.cc",
     "src/trace_processor/importers/common/clock_tracker.cc",
@@ -7583,6 +7841,18 @@
   ],
 }
 
+// GN: //src/trace_processor/importers/common:unittests
+filegroup {
+  name: "perfetto_src_trace_processor_importers_common_unittests",
+  srcs: [
+    "src/trace_processor/importers/common/clock_tracker_unittest.cc",
+    "src/trace_processor/importers/common/event_tracker_unittest.cc",
+    "src/trace_processor/importers/common/flow_tracker_unittest.cc",
+    "src/trace_processor/importers/common/process_tracker_unittest.cc",
+    "src/trace_processor/importers/common/slice_tracker_unittest.cc",
+  ],
+}
+
 // GN: //src/trace_processor/importers:gen_cc_chrome_track_event_descriptor
 genrule {
   name: "perfetto_src_trace_processor_importers_gen_cc_chrome_track_event_descriptor",
@@ -7640,18 +7910,6 @@
   ],
 }
 
-// GN: //src/trace_processor/importers:unittests
-filegroup {
-  name: "perfetto_src_trace_processor_importers_unittests",
-  srcs: [
-    "src/trace_processor/importers/common/clock_tracker_unittest.cc",
-    "src/trace_processor/importers/common/event_tracker_unittest.cc",
-    "src/trace_processor/importers/common/flow_tracker_unittest.cc",
-    "src/trace_processor/importers/common/process_tracker_unittest.cc",
-    "src/trace_processor/importers/common/slice_tracker_unittest.cc",
-  ],
-}
-
 // GN: //src/trace_processor:lib
 filegroup {
   name: "perfetto_src_trace_processor_lib",
@@ -7660,8 +7918,10 @@
     "src/trace_processor/dynamic/connected_flow_generator.cc",
     "src/trace_processor/dynamic/descendant_slice_generator.cc",
     "src/trace_processor/dynamic/describe_slice_generator.cc",
+    "src/trace_processor/dynamic/experimental_annotated_stack_generator.cc",
     "src/trace_processor/dynamic/experimental_counter_dur_generator.cc",
     "src/trace_processor/dynamic/experimental_flamegraph_generator.cc",
+    "src/trace_processor/dynamic/experimental_flat_slice_generator.cc",
     "src/trace_processor/dynamic/experimental_sched_upid_generator.cc",
     "src/trace_processor/dynamic/experimental_slice_layout_generator.cc",
     "src/trace_processor/dynamic/thread_state_generator.cc",
@@ -7739,6 +7999,7 @@
     "src/trace_processor/metrics/android/android_sysui_cuj.sql",
     "src/trace_processor/metrics/android/android_task_names.sql",
     "src/trace_processor/metrics/android/android_thread_time_in_state.sql",
+    "src/trace_processor/metrics/android/composer_execution.sql",
     "src/trace_processor/metrics/android/composition_layers.sql",
     "src/trace_processor/metrics/android/cpu_info.sql",
     "src/trace_processor/metrics/android/display_metrics.sql",
@@ -7746,7 +8007,6 @@
     "src/trace_processor/metrics/android/g2d.sql",
     "src/trace_processor/metrics/android/g2d_duration.sql",
     "src/trace_processor/metrics/android/global_counter_span_view.sql",
-    "src/trace_processor/metrics/android/heap_profile_callsites.sql",
     "src/trace_processor/metrics/android/hsc_startups.sql",
     "src/trace_processor/metrics/android/java_heap_histogram.sql",
     "src/trace_processor/metrics/android/java_heap_stats.sql",
@@ -7759,6 +8019,7 @@
     "src/trace_processor/metrics/android/process_oom_score.sql",
     "src/trace_processor/metrics/android/process_unagg_mem_view.sql",
     "src/trace_processor/metrics/android/span_view_stats.sql",
+    "src/trace_processor/metrics/android/thread_counter_span_view.sql",
     "src/trace_processor/metrics/android/unsymbolized_frames.sql",
     "src/trace_processor/metrics/chrome/actual_power_by_category.sql",
     "src/trace_processor/metrics/chrome/actual_power_by_rail_mode.sql",
@@ -7812,11 +8073,18 @@
   ],
 }
 
+// GN: //src/trace_processor/rpc:httpd
+filegroup {
+  name: "perfetto_src_trace_processor_rpc_httpd",
+  srcs: [
+    "src/trace_processor/rpc/httpd.cc",
+  ],
+}
+
 // GN: //src/trace_processor/rpc:rpc
 filegroup {
   name: "perfetto_src_trace_processor_rpc_rpc",
   srcs: [
-    "src/trace_processor/rpc/proto_ring_buffer.cc",
     "src/trace_processor/rpc/query_result_serializer.cc",
     "src/trace_processor/rpc/rpc.cc",
   ],
@@ -7826,7 +8094,6 @@
 filegroup {
   name: "perfetto_src_trace_processor_rpc_unittests",
   srcs: [
-    "src/trace_processor/rpc/proto_ring_buffer_unittest.cc",
     "src/trace_processor/rpc/query_result_serializer_unittest.cc",
   ],
 }
@@ -7905,10 +8172,8 @@
     "src/trace_processor/forwarding_trace_parser.cc",
     "src/trace_processor/importers/default_modules.cc",
     "src/trace_processor/importers/ftrace/ftrace_module.cc",
-    "src/trace_processor/importers/gzip/gzip_utils.cc",
     "src/trace_processor/importers/json/json_utils.cc",
     "src/trace_processor/importers/ninja/ninja_log_parser.cc",
-    "src/trace_processor/importers/proto/args_table_utils.cc",
     "src/trace_processor/importers/proto/async_track_set_tracker.cc",
     "src/trace_processor/importers/proto/chrome_string_lookup.cc",
     "src/trace_processor/importers/proto/chrome_system_probes_module.cc",
@@ -7988,6 +8253,7 @@
   name: "perfetto_src_trace_processor_unittests",
   srcs: [
     "src/trace_processor/dynamic/experimental_counter_dur_generator_unittest.cc",
+    "src/trace_processor/dynamic/experimental_flat_slice_generator_unittest.cc",
     "src/trace_processor/dynamic/experimental_slice_layout_generator_unittest.cc",
     "src/trace_processor/dynamic/thread_state_generator_unittest.cc",
     "src/trace_processor/forwarding_trace_parser_unittest.cc",
@@ -7996,7 +8262,6 @@
     "src/trace_processor/importers/memory_tracker/graph_processor_unittest.cc",
     "src/trace_processor/importers/memory_tracker/graph_unittest.cc",
     "src/trace_processor/importers/memory_tracker/raw_process_memory_node_unittest.cc",
-    "src/trace_processor/importers/proto/args_table_utils_unittest.cc",
     "src/trace_processor/importers/proto/async_track_set_tracker_unittest.cc",
     "src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc",
     "src/trace_processor/importers/proto/heap_profile_tracker_unittest.cc",
@@ -8016,6 +8281,28 @@
   ],
 }
 
+// GN: //src/trace_processor/util:gzip
+filegroup {
+  name: "perfetto_src_trace_processor_util_gzip",
+  srcs: [
+    "src/trace_processor/util/gzip_utils.cc",
+  ],
+}
+
+// GN: //src/trace_processor/util:interned_message_view
+filegroup {
+  name: "perfetto_src_trace_processor_util_interned_message_view",
+}
+
+// GN: //src/trace_processor/util:proto_to_args_parser
+filegroup {
+  name: "perfetto_src_trace_processor_util_proto_to_args_parser",
+  srcs: [
+    "src/trace_processor/util/debug_annotation_parser.cc",
+    "src/trace_processor/util/proto_to_args_parser.cc",
+  ],
+}
+
 // GN: //src/trace_processor/util:protozero_to_text
 filegroup {
   name: "perfetto_src_trace_processor_util_protozero_to_text",
@@ -8024,10 +8311,17 @@
   ],
 }
 
+// GN: //src/trace_processor/util:trace_blob_view
+filegroup {
+  name: "perfetto_src_trace_processor_util_trace_blob_view",
+}
+
 // GN: //src/trace_processor/util:unittests
 filegroup {
   name: "perfetto_src_trace_processor_util_unittests",
   srcs: [
+    "src/trace_processor/util/debug_annotation_parser_unittest.cc",
+    "src/trace_processor/util/proto_to_args_parser_unittest.cc",
     "src/trace_processor/util/protozero_to_text_unittests.cc",
   ],
 }
@@ -8454,6 +8748,7 @@
     "src/tracing/internal/tracing_muxer_fake.cc",
     "src/tracing/internal/tracing_muxer_impl.cc",
     "src/tracing/internal/track_event_internal.cc",
+    "src/tracing/internal/track_event_interned_fields.cc",
     "src/tracing/platform.cc",
     "src/tracing/traced_value.cc",
     "src/tracing/tracing.cc",
@@ -8547,6 +8842,7 @@
     "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",
   ],
 }
 
@@ -8589,6 +8885,7 @@
   name: "perfetto_src_tracing_platform_impl",
   srcs: [
     "src/tracing/platform_posix.cc",
+    "src/tracing/platform_windows.cc",
   ],
 }
 
@@ -8644,6 +8941,7 @@
 filegroup {
   name: "perfetto_src_tracing_unittests",
   srcs: [
+    "src/tracing/traced_proto_unittest.cc",
     "src/tracing/traced_value_unittest.cc",
   ],
 }
@@ -8697,6 +8995,21 @@
   ],
 }
 
+// GN: //tools/trace_to_text:gen_cc_trace_descriptor
+genrule {
+  name: "perfetto_tools_trace_to_text_gen_cc_trace_descriptor",
+  srcs: [
+    ":perfetto_protos_perfetto_trace_descriptor",
+  ],
+  cmd: "$(location tools/gen_cc_proto_descriptor.py) --gen_dir=$(genDir) --cpp_out=$(out) $(in)",
+  out: [
+    "tools/trace_to_text/trace.descriptor.h",
+  ],
+  tool_files: [
+    "tools/gen_cc_proto_descriptor.py",
+  ],
+}
+
 // GN: //tools/trace_to_text:pprofbuilder
 filegroup {
   name: "perfetto_tools_trace_to_text_pprofbuilder",
@@ -8975,6 +9288,13 @@
     ":perfetto_src_profiling_symbolizer_symbolizer",
     ":perfetto_src_profiling_symbolizer_unittests",
     ":perfetto_src_profiling_unittests",
+    ":perfetto_src_protozero_filtering_bytecode_common",
+    ":perfetto_src_protozero_filtering_bytecode_generator",
+    ":perfetto_src_protozero_filtering_bytecode_parser",
+    ":perfetto_src_protozero_filtering_filter_util",
+    ":perfetto_src_protozero_filtering_message_filter",
+    ":perfetto_src_protozero_filtering_unittests",
+    ":perfetto_src_protozero_proto_ring_buffer",
     ":perfetto_src_protozero_protozero",
     ":perfetto_src_protozero_testing_messages_cpp_gen",
     ":perfetto_src_protozero_testing_messages_lite_gen",
@@ -8983,13 +9303,13 @@
     ":perfetto_src_trace_processor_analysis_analysis",
     ":perfetto_src_trace_processor_containers_containers",
     ":perfetto_src_trace_processor_containers_unittests",
-    ":perfetto_src_trace_processor_db_lib",
+    ":perfetto_src_trace_processor_db_db",
     ":perfetto_src_trace_processor_db_unittests",
     ":perfetto_src_trace_processor_export_json",
     ":perfetto_src_trace_processor_ftrace_descriptors",
-    ":perfetto_src_trace_processor_importers_common",
+    ":perfetto_src_trace_processor_importers_common_common",
+    ":perfetto_src_trace_processor_importers_common_unittests",
     ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
-    ":perfetto_src_trace_processor_importers_unittests",
     ":perfetto_src_trace_processor_lib",
     ":perfetto_src_trace_processor_metatrace",
     ":perfetto_src_trace_processor_metrics_lib",
@@ -9007,7 +9327,11 @@
     ":perfetto_src_trace_processor_types_unittests",
     ":perfetto_src_trace_processor_unittests",
     ":perfetto_src_trace_processor_util_descriptors",
+    ":perfetto_src_trace_processor_util_gzip",
+    ":perfetto_src_trace_processor_util_interned_message_view",
+    ":perfetto_src_trace_processor_util_proto_to_args_parser",
     ":perfetto_src_trace_processor_util_protozero_to_text",
+    ":perfetto_src_trace_processor_util_trace_blob_view",
     ":perfetto_src_trace_processor_util_unittests",
     ":perfetto_src_trace_processor_util_util",
     ":perfetto_src_traced_probes_android_log_android_log",
@@ -9066,6 +9390,7 @@
     "libbase",
     "liblog",
     "libprocinfo",
+    "libprotobuf-cpp-full",
     "libprotobuf-cpp-lite",
     "libsqlite",
     "libunwindstack",
@@ -9272,20 +9597,24 @@
     ":perfetto_protos_perfetto_trace_system_info_zero_gen",
     ":perfetto_protos_perfetto_trace_track_event_zero_gen",
     ":perfetto_src_base_base",
+    ":perfetto_src_base_unix_socket",
     ":perfetto_src_profiling_deobfuscator",
     ":perfetto_src_profiling_symbolizer_symbolize_database",
     ":perfetto_src_profiling_symbolizer_symbolizer",
+    ":perfetto_src_protozero_proto_ring_buffer",
     ":perfetto_src_protozero_protozero",
     ":perfetto_src_trace_processor_analysis_analysis",
     ":perfetto_src_trace_processor_containers_containers",
-    ":perfetto_src_trace_processor_db_lib",
+    ":perfetto_src_trace_processor_db_db",
     ":perfetto_src_trace_processor_export_json",
     ":perfetto_src_trace_processor_ftrace_descriptors",
-    ":perfetto_src_trace_processor_importers_common",
+    ":perfetto_src_trace_processor_importers_common_common",
     ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
     ":perfetto_src_trace_processor_lib",
     ":perfetto_src_trace_processor_metatrace",
     ":perfetto_src_trace_processor_metrics_lib",
+    ":perfetto_src_trace_processor_rpc_httpd",
+    ":perfetto_src_trace_processor_rpc_rpc",
     ":perfetto_src_trace_processor_sqlite_sqlite",
     ":perfetto_src_trace_processor_storage_full",
     ":perfetto_src_trace_processor_storage_minimal",
@@ -9293,7 +9622,11 @@
     ":perfetto_src_trace_processor_tables_tables",
     ":perfetto_src_trace_processor_types_types",
     ":perfetto_src_trace_processor_util_descriptors",
+    ":perfetto_src_trace_processor_util_gzip",
+    ":perfetto_src_trace_processor_util_interned_message_view",
+    ":perfetto_src_trace_processor_util_proto_to_args_parser",
     ":perfetto_src_trace_processor_util_protozero_to_text",
+    ":perfetto_src_trace_processor_util_trace_blob_view",
     ":perfetto_src_trace_processor_util_util",
     "src/trace_processor/trace_processor_shell.cc",
     "src/trace_processor/util/proto_to_json.cc",
@@ -9423,13 +9756,14 @@
     ":perfetto_src_profiling_deobfuscator",
     ":perfetto_src_profiling_symbolizer_symbolize_database",
     ":perfetto_src_profiling_symbolizer_symbolizer",
+    ":perfetto_src_protozero_proto_ring_buffer",
     ":perfetto_src_protozero_protozero",
     ":perfetto_src_trace_processor_analysis_analysis",
     ":perfetto_src_trace_processor_containers_containers",
-    ":perfetto_src_trace_processor_db_lib",
+    ":perfetto_src_trace_processor_db_db",
     ":perfetto_src_trace_processor_export_json",
     ":perfetto_src_trace_processor_ftrace_descriptors",
-    ":perfetto_src_trace_processor_importers_common",
+    ":perfetto_src_trace_processor_importers_common_common",
     ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
     ":perfetto_src_trace_processor_lib",
     ":perfetto_src_trace_processor_metatrace",
@@ -9441,7 +9775,11 @@
     ":perfetto_src_trace_processor_tables_tables",
     ":perfetto_src_trace_processor_types_types",
     ":perfetto_src_trace_processor_util_descriptors",
+    ":perfetto_src_trace_processor_util_gzip",
+    ":perfetto_src_trace_processor_util_interned_message_view",
+    ":perfetto_src_trace_processor_util_proto_to_args_parser",
     ":perfetto_src_trace_processor_util_protozero_to_text",
+    ":perfetto_src_trace_processor_util_trace_blob_view",
     ":perfetto_src_trace_processor_util_util",
     ":perfetto_tools_trace_to_text_common",
     ":perfetto_tools_trace_to_text_full",
@@ -9491,6 +9829,7 @@
     "perfetto_src_trace_processor_metrics_gen_cc_all_chrome_metrics_descriptor",
     "perfetto_src_trace_processor_metrics_gen_cc_metrics_descriptor",
     "perfetto_src_trace_processor_metrics_gen_merged_sql_metrics",
+    "perfetto_tools_trace_to_text_gen_cc_trace_descriptor",
   ],
   defaults: [
     "perfetto_defaults",
@@ -9600,6 +9939,9 @@
     ":perfetto_src_profiling_perf_regs_parsing",
     ":perfetto_src_profiling_perf_traced_perf_main",
     ":perfetto_src_profiling_perf_unwinding",
+    ":perfetto_src_protozero_filtering_bytecode_common",
+    ":perfetto_src_protozero_filtering_bytecode_parser",
+    ":perfetto_src_protozero_filtering_message_filter",
     ":perfetto_src_protozero_protozero",
     ":perfetto_src_traced_probes_ftrace_ftrace_procfs",
     ":perfetto_src_traced_probes_packages_list_packages_list_parser",
diff --git a/BUILD b/BUILD
index 3c6b25a..3604922 100644
--- a/BUILD
+++ b/BUILD
@@ -178,6 +178,9 @@
         ":src_android_stats_android_stats",
         ":src_android_stats_perfetto_atoms",
         ":src_kallsyms_kallsyms",
+        ":src_protozero_filtering_bytecode_common",
+        ":src_protozero_filtering_bytecode_parser",
+        ":src_protozero_filtering_message_filter",
         ":src_traced_probes_android_log_android_log",
         ":src_traced_probes_common_common",
         ":src_traced_probes_data_source",
@@ -306,6 +309,7 @@
         "include/perfetto/ext/base/no_destructor.h",
         "include/perfetto/ext/base/optional.h",
         "include/perfetto/ext/base/paged_memory.h",
+        "include/perfetto/ext/base/periodic_task.h",
         "include/perfetto/ext/base/pipe.h",
         "include/perfetto/ext/base/scoped_file.h",
         "include/perfetto/ext/base/small_set.h",
@@ -434,6 +438,7 @@
         "include/perfetto/protozero/copyable_ptr.h",
         "include/perfetto/protozero/cpp_message_obj.h",
         "include/perfetto/protozero/field.h",
+        "include/perfetto/protozero/field_writer.h",
         "include/perfetto/protozero/message.h",
         "include/perfetto/protozero/message_arena.h",
         "include/perfetto/protozero/message_handle.h",
@@ -527,12 +532,14 @@
         "include/perfetto/tracing/internal/tracing_tls.h",
         "include/perfetto/tracing/internal/track_event_data_source.h",
         "include/perfetto/tracing/internal/track_event_internal.h",
+        "include/perfetto/tracing/internal/track_event_interned_fields.h",
         "include/perfetto/tracing/internal/track_event_macros.h",
         "include/perfetto/tracing/internal/write_track_event_args.h",
         "include/perfetto/tracing/locked_handle.h",
         "include/perfetto/tracing/platform.h",
         "include/perfetto/tracing/string_helpers.h",
         "include/perfetto/tracing/trace_writer_base.h",
+        "include/perfetto/tracing/traced_proto.h",
         "include/perfetto/tracing/traced_value.h",
         "include/perfetto/tracing/traced_value_forward.h",
         "include/perfetto/tracing/tracing.h",
@@ -597,6 +604,7 @@
         "src/base/logging.cc",
         "src/base/metatrace.cc",
         "src/base/paged_memory.cc",
+        "src/base/periodic_task.cc",
         "src/base/pipe.cc",
         "src/base/status.cc",
         "src/base/string_splitter.cc",
@@ -768,6 +776,42 @@
     ],
 )
 
+# GN target: //src/protozero/filtering:bytecode_common
+filegroup(
+    name = "src_protozero_filtering_bytecode_common",
+    srcs = [
+        "src/protozero/filtering/filter_bytecode_common.h",
+    ],
+)
+
+# GN target: //src/protozero/filtering:bytecode_parser
+filegroup(
+    name = "src_protozero_filtering_bytecode_parser",
+    srcs = [
+        "src/protozero/filtering/filter_bytecode_parser.cc",
+        "src/protozero/filtering/filter_bytecode_parser.h",
+    ],
+)
+
+# GN target: //src/protozero/filtering:message_filter
+filegroup(
+    name = "src_protozero_filtering_message_filter",
+    srcs = [
+        "src/protozero/filtering/message_filter.cc",
+        "src/protozero/filtering/message_filter.h",
+        "src/protozero/filtering/message_tokenizer.h",
+    ],
+)
+
+# GN target: //src/protozero:proto_ring_buffer
+filegroup(
+    name = "src_protozero_proto_ring_buffer",
+    srcs = [
+        "src/protozero/proto_ring_buffer.cc",
+        "src/protozero/proto_ring_buffer.h",
+    ],
+)
+
 # GN target: //src/trace_processor/analysis:analysis
 filegroup(
     name = "src_trace_processor_analysis_analysis",
@@ -778,26 +822,34 @@
 )
 
 # GN target: //src/trace_processor/containers:containers
-filegroup(
+perfetto_cc_library(
     name = "src_trace_processor_containers_containers",
     srcs = [
         "src/trace_processor/containers/bit_vector.cc",
-        "src/trace_processor/containers/bit_vector.h",
         "src/trace_processor/containers/bit_vector_iterators.cc",
+        "src/trace_processor/containers/nullable_vector.cc",
+        "src/trace_processor/containers/row_map.cc",
+        "src/trace_processor/containers/string_pool.cc",
+    ],
+    hdrs = [
+        ":include_perfetto_base_base",
+        ":include_perfetto_protozero_protozero",
+        "src/trace_processor/containers/bit_vector.h",
         "src/trace_processor/containers/bit_vector_iterators.h",
         "src/trace_processor/containers/null_term_string_view.h",
-        "src/trace_processor/containers/nullable_vector.cc",
         "src/trace_processor/containers/nullable_vector.h",
-        "src/trace_processor/containers/row_map.cc",
         "src/trace_processor/containers/row_map.h",
-        "src/trace_processor/containers/string_pool.cc",
         "src/trace_processor/containers/string_pool.h",
     ],
+    deps = [
+        ":src_base_base",
+    ],
+    linkstatic = True,
 )
 
-# GN target: //src/trace_processor/db:lib
+# GN target: //src/trace_processor/db:db
 filegroup(
-    name = "src_trace_processor_db_lib",
+    name = "src_trace_processor_db_db",
     srcs = [
         "src/trace_processor/db/column.cc",
         "src/trace_processor/db/column.h",
@@ -809,24 +861,13 @@
     ],
 )
 
-# GN target: //src/trace_processor/importers/memory_tracker:graph_processor
+# GN target: //src/trace_processor/importers/common:common
 filegroup(
-    name = "src_trace_processor_importers_memory_tracker_graph_processor",
-    srcs = [
-        "src/trace_processor/importers/memory_tracker/graph.cc",
-        "src/trace_processor/importers/memory_tracker/graph_processor.cc",
-        "src/trace_processor/importers/memory_tracker/memory_allocator_node_id.cc",
-        "src/trace_processor/importers/memory_tracker/raw_memory_graph_node.cc",
-        "src/trace_processor/importers/memory_tracker/raw_process_memory_node.cc",
-    ],
-)
-
-# GN target: //src/trace_processor/importers:common
-filegroup(
-    name = "src_trace_processor_importers_common",
+    name = "src_trace_processor_importers_common_common",
     srcs = [
         "src/trace_processor/importers/common/args_tracker.cc",
         "src/trace_processor/importers/common/args_tracker.h",
+        "src/trace_processor/importers/common/chunked_trace_reader.h",
         "src/trace_processor/importers/common/clock_tracker.cc",
         "src/trace_processor/importers/common/clock_tracker.h",
         "src/trace_processor/importers/common/event_tracker.cc",
@@ -841,11 +882,24 @@
         "src/trace_processor/importers/common/slice_tracker.h",
         "src/trace_processor/importers/common/system_info_tracker.cc",
         "src/trace_processor/importers/common/system_info_tracker.h",
+        "src/trace_processor/importers/common/trace_parser.h",
         "src/trace_processor/importers/common/track_tracker.cc",
         "src/trace_processor/importers/common/track_tracker.h",
     ],
 )
 
+# GN target: //src/trace_processor/importers/memory_tracker:graph_processor
+filegroup(
+    name = "src_trace_processor_importers_memory_tracker_graph_processor",
+    srcs = [
+        "src/trace_processor/importers/memory_tracker/graph.cc",
+        "src/trace_processor/importers/memory_tracker/graph_processor.cc",
+        "src/trace_processor/importers/memory_tracker/memory_allocator_node_id.cc",
+        "src/trace_processor/importers/memory_tracker/raw_memory_graph_node.cc",
+        "src/trace_processor/importers/memory_tracker/raw_process_memory_node.cc",
+    ],
+)
+
 perfetto_cc_proto_descriptor(
     name = "src_trace_processor_importers_gen_cc_chrome_track_event_descriptor",
     deps = [
@@ -924,6 +978,7 @@
         "src/trace_processor/metrics/android/android_sysui_cuj.sql",
         "src/trace_processor/metrics/android/android_task_names.sql",
         "src/trace_processor/metrics/android/android_thread_time_in_state.sql",
+        "src/trace_processor/metrics/android/composer_execution.sql",
         "src/trace_processor/metrics/android/composition_layers.sql",
         "src/trace_processor/metrics/android/cpu_info.sql",
         "src/trace_processor/metrics/android/display_metrics.sql",
@@ -931,7 +986,6 @@
         "src/trace_processor/metrics/android/g2d.sql",
         "src/trace_processor/metrics/android/g2d_duration.sql",
         "src/trace_processor/metrics/android/global_counter_span_view.sql",
-        "src/trace_processor/metrics/android/heap_profile_callsites.sql",
         "src/trace_processor/metrics/android/hsc_startups.sql",
         "src/trace_processor/metrics/android/java_heap_histogram.sql",
         "src/trace_processor/metrics/android/java_heap_stats.sql",
@@ -944,6 +998,7 @@
         "src/trace_processor/metrics/android/process_oom_score.sql",
         "src/trace_processor/metrics/android/process_unagg_mem_view.sql",
         "src/trace_processor/metrics/android/span_view_stats.sql",
+        "src/trace_processor/metrics/android/thread_counter_span_view.sql",
         "src/trace_processor/metrics/android/unsymbolized_frames.sql",
         "src/trace_processor/metrics/chrome/actual_power_by_category.sql",
         "src/trace_processor/metrics/chrome/actual_power_by_rail_mode.sql",
@@ -1003,8 +1058,6 @@
 filegroup(
     name = "src_trace_processor_rpc_rpc",
     srcs = [
-        "src/trace_processor/rpc/proto_ring_buffer.cc",
-        "src/trace_processor/rpc/proto_ring_buffer.h",
         "src/trace_processor/rpc/query_result_serializer.cc",
         "src/trace_processor/rpc/query_result_serializer.h",
         "src/trace_processor/rpc/rpc.cc",
@@ -1096,6 +1149,34 @@
     ],
 )
 
+# GN target: //src/trace_processor/util:gzip
+filegroup(
+    name = "src_trace_processor_util_gzip",
+    srcs = [
+        "src/trace_processor/util/gzip_utils.cc",
+        "src/trace_processor/util/gzip_utils.h",
+    ],
+)
+
+# GN target: //src/trace_processor/util:interned_message_view
+filegroup(
+    name = "src_trace_processor_util_interned_message_view",
+    srcs = [
+        "src/trace_processor/util/interned_message_view.h",
+    ],
+)
+
+# GN target: //src/trace_processor/util:proto_to_args_parser
+filegroup(
+    name = "src_trace_processor_util_proto_to_args_parser",
+    srcs = [
+        "src/trace_processor/util/debug_annotation_parser.cc",
+        "src/trace_processor/util/debug_annotation_parser.h",
+        "src/trace_processor/util/proto_to_args_parser.cc",
+        "src/trace_processor/util/proto_to_args_parser.h",
+    ],
+)
+
 # GN target: //src/trace_processor/util:protozero_to_text
 filegroup(
     name = "src_trace_processor_util_protozero_to_text",
@@ -1105,6 +1186,14 @@
     ],
 )
 
+# GN target: //src/trace_processor/util:trace_blob_view
+filegroup(
+    name = "src_trace_processor_util_trace_blob_view",
+    srcs = [
+        "src/trace_processor/util/trace_blob_view.h",
+    ],
+)
+
 # GN target: //src/trace_processor/util:util
 filegroup(
     name = "src_trace_processor_util_util",
@@ -1143,10 +1232,14 @@
         "src/trace_processor/dynamic/descendant_slice_generator.h",
         "src/trace_processor/dynamic/describe_slice_generator.cc",
         "src/trace_processor/dynamic/describe_slice_generator.h",
+        "src/trace_processor/dynamic/experimental_annotated_stack_generator.cc",
+        "src/trace_processor/dynamic/experimental_annotated_stack_generator.h",
         "src/trace_processor/dynamic/experimental_counter_dur_generator.cc",
         "src/trace_processor/dynamic/experimental_counter_dur_generator.h",
         "src/trace_processor/dynamic/experimental_flamegraph_generator.cc",
         "src/trace_processor/dynamic/experimental_flamegraph_generator.h",
+        "src/trace_processor/dynamic/experimental_flat_slice_generator.cc",
+        "src/trace_processor/dynamic/experimental_flat_slice_generator.h",
         "src/trace_processor/dynamic/experimental_sched_upid_generator.cc",
         "src/trace_processor/dynamic/experimental_sched_upid_generator.h",
         "src/trace_processor/dynamic/experimental_slice_layout_generator.cc",
@@ -1248,7 +1341,6 @@
 filegroup(
     name = "src_trace_processor_storage_minimal",
     srcs = [
-        "src/trace_processor/chunked_trace_reader.h",
         "src/trace_processor/forwarding_trace_parser.cc",
         "src/trace_processor/forwarding_trace_parser.h",
         "src/trace_processor/importers/default_modules.cc",
@@ -1257,14 +1349,10 @@
         "src/trace_processor/importers/ftrace/ftrace_module.h",
         "src/trace_processor/importers/fuchsia/fuchsia_record.h",
         "src/trace_processor/importers/fuchsia/fuchsia_trace_utils.h",
-        "src/trace_processor/importers/gzip/gzip_utils.cc",
-        "src/trace_processor/importers/gzip/gzip_utils.h",
         "src/trace_processor/importers/json/json_utils.cc",
         "src/trace_processor/importers/json/json_utils.h",
         "src/trace_processor/importers/ninja/ninja_log_parser.cc",
         "src/trace_processor/importers/ninja/ninja_log_parser.h",
-        "src/trace_processor/importers/proto/args_table_utils.cc",
-        "src/trace_processor/importers/proto/args_table_utils.h",
         "src/trace_processor/importers/proto/async_track_set_tracker.cc",
         "src/trace_processor/importers/proto/async_track_set_tracker.h",
         "src/trace_processor/importers/proto/chrome_string_lookup.cc",
@@ -1315,8 +1403,6 @@
         "src/trace_processor/importers/syscalls/syscall_tracker.h",
         "src/trace_processor/importers/systrace/systrace_line.h",
         "src/trace_processor/timestamped_trace_piece.h",
-        "src/trace_processor/trace_blob_view.h",
-        "src/trace_processor/trace_parser.h",
         "src/trace_processor/trace_processor_context.cc",
         "src/trace_processor/trace_processor_storage.cc",
         "src/trace_processor/trace_processor_storage_impl.cc",
@@ -1612,6 +1698,8 @@
         "src/tracing/ipc/memfd.h",
         "src/tracing/ipc/posix_shared_memory.cc",
         "src/tracing/ipc/posix_shared_memory.h",
+        "src/tracing/ipc/shared_memory_windows.cc",
+        "src/tracing/ipc/shared_memory_windows.h",
     ],
 )
 
@@ -1632,6 +1720,7 @@
         "src/tracing/internal/tracing_muxer_impl.cc",
         "src/tracing/internal/tracing_muxer_impl.h",
         "src/tracing/internal/track_event_internal.cc",
+        "src/tracing/internal/track_event_interned_fields.cc",
         "src/tracing/platform.cc",
         "src/tracing/traced_value.cc",
         "src/tracing/tracing.cc",
@@ -1665,6 +1754,7 @@
     name = "src_tracing_platform_impl",
     srcs = [
         "src/tracing/platform_posix.cc",
+        "src/tracing/platform_windows.cc",
     ],
 )
 
@@ -1707,6 +1797,16 @@
     ],
 )
 
+perfetto_cc_proto_descriptor(
+    name = "tools_trace_to_text_gen_cc_trace_descriptor",
+    deps = [
+        ":protos_perfetto_trace_descriptor",
+    ],
+    outs = [
+        "tools/trace_to_text/trace.descriptor.h",
+    ],
+)
+
 # GN target: //tools/trace_to_text:pprofbuilder
 filegroup(
     name = "tools_trace_to_text_pprofbuilder",
@@ -1814,6 +1914,7 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_config_android_zero",
     deps = [
+        ":protos_perfetto_common_zero",
         ":protos_perfetto_config_android_protos",
     ],
 )
@@ -1989,6 +2090,7 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_config_interceptors_zero",
     deps = [
+        ":protos_perfetto_common_zero",
         ":protos_perfetto_config_interceptors_protos",
     ],
 )
@@ -2147,6 +2249,7 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_config_profiling_zero",
     deps = [
+        ":protos_perfetto_common_zero",
         ":protos_perfetto_config_profiling_protos",
     ],
 )
@@ -2213,6 +2316,7 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_config_sys_stats_zero",
     deps = [
+        ":protos_perfetto_common_zero",
         ":protos_perfetto_config_sys_stats_protos",
     ],
 )
@@ -2256,7 +2360,18 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_config_zero",
     deps = [
+        ":protos_perfetto_common_zero",
+        ":protos_perfetto_config_android_zero",
+        ":protos_perfetto_config_ftrace_zero",
+        ":protos_perfetto_config_gpu_zero",
+        ":protos_perfetto_config_inode_file_zero",
+        ":protos_perfetto_config_interceptors_zero",
+        ":protos_perfetto_config_power_zero",
+        ":protos_perfetto_config_process_stats_zero",
+        ":protos_perfetto_config_profiling_zero",
         ":protos_perfetto_config_protos",
+        ":protos_perfetto_config_sys_stats_zero",
+        ":protos_perfetto_config_track_event_zero",
     ],
 )
 
@@ -2365,7 +2480,6 @@
         "protos/perfetto/metrics/android/fastrpc_metric.proto",
         "protos/perfetto/metrics/android/g2d_metric.proto",
         "protos/perfetto/metrics/android/gpu_metric.proto",
-        "protos/perfetto/metrics/android/heap_profile_callsites.proto",
         "protos/perfetto/metrics/android/hwcomposer.proto",
         "protos/perfetto/metrics/android/hwui_metric.proto",
         "protos/perfetto/metrics/android/ion_metric.proto",
@@ -2497,6 +2611,7 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_trace_android_zero",
     deps = [
+        ":protos_perfetto_common_zero",
         ":protos_perfetto_trace_android_protos",
     ],
 )
@@ -2530,6 +2645,17 @@
     ],
 )
 
+# GN target: //protos/perfetto/trace:descriptor
+perfetto_proto_descriptor(
+    name = "protos_perfetto_trace_descriptor",
+    deps = [
+        ":protos_perfetto_trace_protos",
+    ],
+    outs = [
+        "protos_perfetto_trace_descriptor.bin",
+    ],
+)
+
 # GN target: //protos/perfetto/trace/filesystem:lite
 perfetto_cc_proto_library(
     name = "protos_perfetto_trace_filesystem_lite",
@@ -2657,6 +2783,7 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_trace_gpu_zero",
     deps = [
+        ":protos_perfetto_common_zero",
         ":protos_perfetto_trace_gpu_protos",
     ],
 )
@@ -2690,7 +2817,11 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_trace_interned_data_zero",
     deps = [
+        ":protos_perfetto_common_zero",
+        ":protos_perfetto_trace_gpu_zero",
         ":protos_perfetto_trace_interned_data_protos",
+        ":protos_perfetto_trace_profiling_zero",
+        ":protos_perfetto_trace_track_event_zero",
     ],
 )
 
@@ -2750,6 +2881,18 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_trace_minimal_zero",
     deps = [
+        ":protos_perfetto_common_zero",
+        ":protos_perfetto_config_android_zero",
+        ":protos_perfetto_config_ftrace_zero",
+        ":protos_perfetto_config_gpu_zero",
+        ":protos_perfetto_config_inode_file_zero",
+        ":protos_perfetto_config_interceptors_zero",
+        ":protos_perfetto_config_power_zero",
+        ":protos_perfetto_config_process_stats_zero",
+        ":protos_perfetto_config_profiling_zero",
+        ":protos_perfetto_config_sys_stats_zero",
+        ":protos_perfetto_config_track_event_zero",
+        ":protos_perfetto_config_zero",
         ":protos_perfetto_trace_minimal_protos",
     ],
 )
@@ -2769,6 +2912,7 @@
         "protos/perfetto/trace/extension_descriptor.proto",
         "protos/perfetto/trace/memory_graph.proto",
         "protos/perfetto/trace/test_event.proto",
+        "protos/perfetto/trace/test_extensions.proto",
         "protos/perfetto/trace/trace.proto",
         "protos/perfetto/trace/trace_packet.proto",
         "protos/perfetto/trace/trace_packet_defaults.proto",
@@ -2809,7 +2953,33 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_trace_non_minimal_zero",
     deps = [
+        ":protos_perfetto_common_zero",
+        ":protos_perfetto_config_android_zero",
+        ":protos_perfetto_config_ftrace_zero",
+        ":protos_perfetto_config_gpu_zero",
+        ":protos_perfetto_config_inode_file_zero",
+        ":protos_perfetto_config_interceptors_zero",
+        ":protos_perfetto_config_power_zero",
+        ":protos_perfetto_config_process_stats_zero",
+        ":protos_perfetto_config_profiling_zero",
+        ":protos_perfetto_config_sys_stats_zero",
+        ":protos_perfetto_config_track_event_zero",
+        ":protos_perfetto_config_zero",
+        ":protos_perfetto_trace_android_zero",
+        ":protos_perfetto_trace_chrome_zero",
+        ":protos_perfetto_trace_filesystem_zero",
+        ":protos_perfetto_trace_ftrace_zero",
+        ":protos_perfetto_trace_gpu_zero",
+        ":protos_perfetto_trace_interned_data_zero",
+        ":protos_perfetto_trace_minimal_zero",
         ":protos_perfetto_trace_non_minimal_protos",
+        ":protos_perfetto_trace_perfetto_zero",
+        ":protos_perfetto_trace_power_zero",
+        ":protos_perfetto_trace_profiling_zero",
+        ":protos_perfetto_trace_ps_zero",
+        ":protos_perfetto_trace_sys_stats_zero",
+        ":protos_perfetto_trace_system_info_zero",
+        ":protos_perfetto_trace_track_event_zero",
     ],
 )
 
@@ -2869,6 +3039,7 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_trace_power_zero",
     deps = [
+        ":protos_perfetto_common_zero",
         ":protos_perfetto_trace_power_protos",
     ],
 )
@@ -2910,6 +3081,7 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_trace_processor_zero",
     deps = [
+        ":protos_perfetto_common_zero",
         ":protos_perfetto_trace_processor_protos",
     ],
 )
@@ -2944,10 +3116,51 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_trace_profiling_zero",
     deps = [
+        ":protos_perfetto_common_zero",
         ":protos_perfetto_trace_profiling_protos",
     ],
 )
 
+# GN target: //protos/perfetto/trace:descriptor
+perfetto_proto_library(
+    name = "protos_perfetto_trace_protos",
+    srcs = [
+        "protos/perfetto/trace/trace.proto",
+    ],
+    visibility = [
+        PERFETTO_CONFIG.proto_library_visibility,
+    ],
+    deps = [
+        ":protos_perfetto_common_protos",
+        ":protos_perfetto_config_android_protos",
+        ":protos_perfetto_config_ftrace_protos",
+        ":protos_perfetto_config_gpu_protos",
+        ":protos_perfetto_config_inode_file_protos",
+        ":protos_perfetto_config_interceptors_protos",
+        ":protos_perfetto_config_power_protos",
+        ":protos_perfetto_config_process_stats_protos",
+        ":protos_perfetto_config_profiling_protos",
+        ":protos_perfetto_config_protos",
+        ":protos_perfetto_config_sys_stats_protos",
+        ":protos_perfetto_config_track_event_protos",
+        ":protos_perfetto_trace_android_protos",
+        ":protos_perfetto_trace_chrome_protos",
+        ":protos_perfetto_trace_filesystem_protos",
+        ":protos_perfetto_trace_ftrace_protos",
+        ":protos_perfetto_trace_gpu_protos",
+        ":protos_perfetto_trace_interned_data_protos",
+        ":protos_perfetto_trace_minimal_protos",
+        ":protos_perfetto_trace_non_minimal_protos",
+        ":protos_perfetto_trace_perfetto_protos",
+        ":protos_perfetto_trace_power_protos",
+        ":protos_perfetto_trace_profiling_protos",
+        ":protos_perfetto_trace_ps_protos",
+        ":protos_perfetto_trace_sys_stats_protos",
+        ":protos_perfetto_trace_system_info_protos",
+        ":protos_perfetto_trace_track_event_protos",
+    ],
+)
+
 # GN target: //protos/perfetto/trace/ps:lite
 perfetto_cc_proto_library(
     name = "protos_perfetto_trace_ps_lite",
@@ -3002,6 +3215,7 @@
 perfetto_cc_protozero_library(
     name = "protos_perfetto_trace_sys_stats_zero",
     deps = [
+        ":protos_perfetto_common_zero",
         ":protos_perfetto_trace_sys_stats_protos",
     ],
 )
@@ -3189,6 +3403,9 @@
     srcs = [
         ":src_android_stats_android_stats",
         ":src_android_stats_perfetto_atoms",
+        ":src_protozero_filtering_bytecode_common",
+        ":src_protozero_filtering_bytecode_parser",
+        ":src_protozero_filtering_message_filter",
         ":src_tracing_client_api_without_backends",
         ":src_tracing_common",
         ":src_tracing_core_core",
@@ -3350,11 +3567,10 @@
     name = "trace_processor",
     srcs = [
         ":src_trace_processor_analysis_analysis",
-        ":src_trace_processor_containers_containers",
-        ":src_trace_processor_db_lib",
+        ":src_trace_processor_db_db",
         ":src_trace_processor_export_json",
         ":src_trace_processor_ftrace_descriptors",
-        ":src_trace_processor_importers_common",
+        ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
         ":src_trace_processor_lib",
         ":src_trace_processor_metatrace",
@@ -3366,7 +3582,11 @@
         ":src_trace_processor_tables_tables",
         ":src_trace_processor_types_types",
         ":src_trace_processor_util_descriptors",
+        ":src_trace_processor_util_gzip",
+        ":src_trace_processor_util_interned_message_view",
+        ":src_trace_processor_util_proto_to_args_parser",
         ":src_trace_processor_util_protozero_to_text",
+        ":src_trace_processor_util_trace_blob_view",
         ":src_trace_processor_util_util",
     ],
     hdrs = [
@@ -3375,7 +3595,6 @@
         ":include_perfetto_ext_trace_processor_export_json",
         ":include_perfetto_ext_trace_processor_importers_memory_tracker_memory_tracker",
         ":include_perfetto_ext_traced_sys_stats_counters",
-        ":include_perfetto_protozero_protozero",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -3415,6 +3634,7 @@
                ":protos_perfetto_trace_track_event_zero",
                ":protozero",
                ":src_base_base",
+               ":src_trace_processor_containers_containers",
                ":src_trace_processor_importers_gen_cc_chrome_track_event_descriptor",
                ":src_trace_processor_importers_gen_cc_config_descriptor",
                ":src_trace_processor_importers_gen_cc_track_event_descriptor",
@@ -3445,12 +3665,12 @@
         ":src_profiling_deobfuscator",
         ":src_profiling_symbolizer_symbolize_database",
         ":src_profiling_symbolizer_symbolizer",
+        ":src_protozero_proto_ring_buffer",
         ":src_trace_processor_analysis_analysis",
-        ":src_trace_processor_containers_containers",
-        ":src_trace_processor_db_lib",
+        ":src_trace_processor_db_db",
         ":src_trace_processor_export_json",
         ":src_trace_processor_ftrace_descriptors",
-        ":src_trace_processor_importers_common",
+        ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
         ":src_trace_processor_lib",
         ":src_trace_processor_metatrace",
@@ -3464,7 +3684,11 @@
         ":src_trace_processor_tables_tables",
         ":src_trace_processor_types_types",
         ":src_trace_processor_util_descriptors",
+        ":src_trace_processor_util_gzip",
+        ":src_trace_processor_util_interned_message_view",
+        ":src_trace_processor_util_proto_to_args_parser",
         ":src_trace_processor_util_protozero_to_text",
+        ":src_trace_processor_util_trace_blob_view",
         ":src_trace_processor_util_util",
         "src/trace_processor/trace_processor_shell.cc",
         "src/trace_processor/util/proto_to_json.cc",
@@ -3506,6 +3730,7 @@
                ":protozero",
                ":src_base_base",
                ":src_base_unix_socket",
+               ":src_trace_processor_containers_containers",
                ":src_trace_processor_importers_gen_cc_chrome_track_event_descriptor",
                ":src_trace_processor_importers_gen_cc_config_descriptor",
                ":src_trace_processor_importers_gen_cc_track_event_descriptor",
@@ -3603,6 +3828,7 @@
         ":protos_perfetto_trace_track_event_zero",
         ":protos_third_party_pprof_zero",
         ":protozero",
+        ":src_trace_processor_containers_containers",
     ] + PERFETTO_CONFIG.deps.zlib,
     linkstatic = True,
 )
@@ -3624,12 +3850,12 @@
         ":src_profiling_deobfuscator",
         ":src_profiling_symbolizer_symbolize_database",
         ":src_profiling_symbolizer_symbolizer",
+        ":src_protozero_proto_ring_buffer",
         ":src_trace_processor_analysis_analysis",
-        ":src_trace_processor_containers_containers",
-        ":src_trace_processor_db_lib",
+        ":src_trace_processor_db_db",
         ":src_trace_processor_export_json",
         ":src_trace_processor_ftrace_descriptors",
-        ":src_trace_processor_importers_common",
+        ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
         ":src_trace_processor_lib",
         ":src_trace_processor_metatrace",
@@ -3641,7 +3867,11 @@
         ":src_trace_processor_tables_tables",
         ":src_trace_processor_types_types",
         ":src_trace_processor_util_descriptors",
+        ":src_trace_processor_util_gzip",
+        ":src_trace_processor_util_interned_message_view",
+        ":src_trace_processor_util_proto_to_args_parser",
         ":src_trace_processor_util_protozero_to_text",
+        ":src_trace_processor_util_trace_blob_view",
         ":src_trace_processor_util_util",
         ":tools_trace_to_text_common",
         ":tools_trace_to_text_full",
@@ -3684,11 +3914,13 @@
                ":protos_third_party_pprof_zero",
                ":protozero",
                ":src_base_base",
+               ":src_trace_processor_containers_containers",
                ":src_trace_processor_importers_gen_cc_chrome_track_event_descriptor",
                ":src_trace_processor_importers_gen_cc_config_descriptor",
                ":src_trace_processor_importers_gen_cc_track_event_descriptor",
                ":src_trace_processor_metrics_gen_cc_all_chrome_metrics_descriptor",
                ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
+               ":tools_trace_to_text_gen_cc_trace_descriptor",
            ] + PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.protobuf_full +
            PERFETTO_CONFIG.deps.sqlite +
diff --git a/BUILD.gn b/BUILD.gn
index 6f6cc7d..fe44ed2 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -144,6 +144,9 @@
   if (is_linux || is_android) {
     all_targets += [ "src/tracing/consumer_api_deprecated:consumer_api_test" ]
   }
+  if (is_linux || is_android || is_mac) {
+    all_targets += [ "src/tracebox" ]
+  }
 }
 
 # The CTS code is built (but not ran) also in standalone builds. This is to
diff --git a/CHANGELOG b/CHANGELOG
index fee282f..ccb4b8d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,10 +4,78 @@
   Trace Processor:
     *
   UI:
-    *
+    * Fixed ADB connection issues ("unable to reset device") on Windows and Mac.
   SDK:
+    * Added support for writing track events using custom clock timestamps.
+
+
+v16.1 - 2021-06-08:
+  Tracing service and probes:
+    * Cherry-pick of r.android.com/1716718 which missed the v16 branch cut and
+      fixed MSVC 2019 builds.
+
+
+v16.0 - 2021-06-01:
+  Tracing service and probes:
+    * Added support for building most targets (including traced, SDK and
+      trace_processor_shell) from Windows using either clang-cl or MSVC 2019.
+    * Added tracebox, a monolithic binary to capture traces with one command
+      on Linux and older versions of Android (tested on Android Oreo).
+    * Added support for service-side field-level filtering of traces. The
+      consumer can pass a filter bytecode and ensure that non-allowed fields
+      are never emitted in output.
+    * Added reporting of service version and producer SDK version into the trace
+      and `perfetto --query`.
+    * Fixed compatibility with Android versions older than Pie (for sideloading)
+      which cause failures when trying to enable atrace categories.
+  Trace Processor:
+    * Added new RPC interface based on a bidirectional binary pipe. This allows
+      to simplify integration with out-of-process users. The existing --httpd
+      interface now exposes a single /rpc endpoint. Older endpoints are still
+      available for legacy clients.
+    * Added support for counters and instant events in JSON traces.
+    * Fixed support of displayTimeUnit in JSON traces.
+  UI:
+    * Added warning dialog when trying to use a trace_processor_shell --httpd
+      which is too old.
+    * Added warning dialog when trying to use a trace_processor_shell --httpd
+      RPC instance from more than one tab.
+    * Added links to convert the trace to JSON or systrace directly from the UI.
+    * Changed track sorting logic. Tracks are now sorted in groups (e.g.,
+      scheduling tracks, summary tracks, frame timeline tracks).
+    * Fixed crashes happening flakily when pushing traces via window.open().
+
+
+v15.0 - 2021-05-05:
+  Tracing service and probes:
+    * Added support for {host,target}=aarch64 standalone builds.
+    * Added --background cmdline switch to traced and traced_probes services.
+    * Changed trigger_perfetto to ignore unknown command line arguments to
+      preserve forward compatibility.
+    * Added -a / --app cmdline argument to tools/record_android_trace.
+  Trace Processor:
+    * Added sanitisation of keys in nested debug annotation dictionaries.
+    * Changed Android startup metric: count CPU time of JIT thread pool, report
+      timestamp of activities during startup.
+    * Changed android_surfaceflinger metric, added missed frame counters.
+    * Changed version of SQLite to 3.35.4.
+    * Fixed importing of JSON traces with decimal (sub-us) timestamp.
+    * Fixed prepending "debug." prefix to debug annotations with non-interned
+      names.
+  UI:
+    * Added support to visualize the lifetime of individual dmabuf allocations
+      as async slices (datasource: already existing ftrace dmabuf event).
+    * Fixed visualization of unfinished slices to extend to the end of the
+      viewport.
+  SDK:
+    * Added support for passing arbitrary number of debug annotations to
+      TRACE_EVENT and combining them with lambdas.
+    * Added support for writing typed TrackEvent arguments using TRACE_EVENT
+      inline without lambdas.
     * Changed ConvertTimestampToTraceTimeNs to be a member of
       TraceTimestampTraits<T> struct instead of a standalone function.
+    * Changed TracedValue to use nested DebugAnnotation proto instead of
+      DebugAnnotation::NestedValue.
 
 
 v14.0 - 2021-04-01:
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 97acb3e..7cbc8e4 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -24,9 +24,16 @@
         x,
         files_to_check='.*',
         files_to_skip=[
-            'Android[.]bp', '.*[.]json$', '.*[.]sql$', '.*[.]out$',
-            'test/trace_processor/.*/index$', '(.*/)?BUILD$', 'WORKSPACE',
-            '.*/Makefile$', '/perfetto_build_flags.h$'
+            'Android[.]bp',
+            '.*[.]json$',
+            '.*[.]sql$',
+            '.*[.]out$',
+            'test/trace_processor/.*/index$',
+            '(.*/)?BUILD$',
+            'WORKSPACE',
+            '.*/Makefile$',
+            '/perfetto_build_flags.h$',
+            "infra/luci/.*",
         ])
 
   results = []
diff --git a/bazel/rules.bzl b/bazel/rules.bzl
index 1767aa4..4503867 100644
--- a/bazel/rules.bzl
+++ b/bazel/rules.bzl
@@ -93,9 +93,20 @@
     ):
         return
 
+    # A perfetto_cc_protozero_library has two types of dependencies:
+    # 1. Exactly one dependency on a proto_library target. This defines the
+    #    .proto sources for the target
+    # 2. Zero or more deps on other perfetto_cc_protozero_library targets. This
+    #    to deal with the case of foo.proto including common.proto from another
+    #    target.
+    _proto_deps = [d for d in deps if d.endswith("_protos")]
+    _cc_deps = [d for d in deps if d not in _proto_deps]
+    if len(_proto_deps) != 1:
+        fail("Too many proto deps for target %s" % name)
+
     proto_gen(
         name = name + "_src",
-        deps = deps,
+        deps = _proto_deps,
         suffix = "pbzero",
         plugin = PERFETTO_CONFIG.root + ":protozero_plugin",
         wrapper_namespace = "pbzero",
@@ -113,7 +124,7 @@
         name = name,
         srcs = [":" + name + "_src"],
         hdrs = [":" + name + "_h"],
-        deps = [PERFETTO_CONFIG.root + ":protozero"],
+        deps = [PERFETTO_CONFIG.root + ":protozero"] + _cc_deps,
         **kwargs
     )
 
diff --git a/buildtools/BUILD.gn b/buildtools/BUILD.gn
index 3e2df10..9d5fad4 100644
--- a/buildtools/BUILD.gn
+++ b/buildtools/BUILD.gn
@@ -724,7 +724,8 @@
       "//gn/standalone/sanitizers:sanitizer_options_link_helper",
     ]
     defines = [ "_LIBCPP_BUILDING_LIBRARY" ]
-    if ((is_linux || is_android) && (is_asan || is_tsan || is_msan)) {
+    if ((is_linux || is_android) && using_sanitizer &&
+        (is_asan || is_tsan || is_msan)) {
       # In {a,t,m}san configurations, operator new and operator delete will be
       # provided by the sanitizer runtime library.  Since libc++ defines these
       # symbols with weak linkage, and the *san runtime uses strong linkage, it
@@ -788,7 +789,9 @@
   defines = [ "HAVE_POSIX_REGEX" ]
   public_configs = [ ":benchmark_config" ]
   all_dependent_configs = [ ":benchmark_config" ]
-  cflags = [ "-Wno-deprecated-declarations" ]
+  if (!is_win) {
+    cflags = [ "-Wno-deprecated-declarations" ]
+  }
   configs -= [ "//gn/standalone:extra_warnings" ]
   deps = [ "//gn:default_deps" ]
 }
@@ -1040,8 +1043,10 @@
     "//gn/standalone:visibility_hidden",
   ]
   cflags = [ "-DFAKE_LOG_DEVICE=1" ]
-  configs += [ "//gn/standalone:c++17" ]
-  public_configs = [ ":libunwindstack_config" ]
+  public_configs = [
+    ":libunwindstack_config",
+    "//gn/standalone:c++17",
+  ]
 }
 
 config("bionic_kernel_uapi_headers") {
@@ -1054,9 +1059,11 @@
 
 config("jsoncpp_config") {
   visibility = _buildtools_visibility
-  cflags = [
-    "-DJSON_USE_EXCEPTION=0",
-
+  cflags = [ "-DJSON_USE_EXCEPTION=0" ]
+  if (!is_win) {
+    cflags += [ "-Wno-deprecated-declarations" ]
+  }
+  cflags += [
     # Using -isystem instead of include_dirs (-I), so we don't need to suppress
     # warnings coming from third-party headers. Doing so would mask warnings in
     # our own code.
diff --git a/debian/changelog b/debian/changelog
index fb48257..2b4ba12 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+perfetto (15.0-1) unstable; urgency=medium
+  * Performance improvements.
+  * Bugfixes.
+  * More flexible SDK.
+  * New data-sources.
+
+ -- Florian Mayer <fmayer@google.com>  Fri, 14 May 2021 10:50:00 +0000
+
 perfetto (11.0-1) unstable; urgency=medium
   * Add the Perfetto perf sampling and profiling service.
 
diff --git a/debian/compat b/debian/compat
index f599e28..b1bd38b 100644
--- a/debian/compat
+++ b/debian/compat
@@ -1 +1 @@
-10
+13
diff --git a/debian/control b/debian/control
index be52af4..00f1346 100644
--- a/debian/control
+++ b/debian/control
@@ -2,15 +2,25 @@
 Section: kernel
 Priority: optional
 Maintainer: Sami Kyostila <skyostil@google.com>
-Build-Depends: debhelper (>= 10)
-Standards-Version: 3.9.8
+Build-Depends: debhelper (>= 13),
+  generate-ninja,
+  git,
+  libprotoc-dev,
+  ninja-build,
+  pandoc,
+  protobuf-compiler,
+  python3,
+  zlib1g-dev,
+  zlib1g
+Standards-Version: 4.5.1
 Homepage: https://perfetto.dev
 Vcs-Git: https://android.googlesource.com/platform/external/perfetto/
 Vcs-Browser: https://android.googlesource.com/platform/external/perfetto/
+Rules-Requires-Root: no
 
 Package: perfetto
 Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}
+Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, zlib1g
 Description: Performance instrumentation and logging framework
  Perfetto is a performance instrumentation and logging framework for POSIX
  systems.
diff --git a/debian/copyright b/debian/copyright
index e23f57d..7b7058c 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -4,12 +4,12 @@
 
 Files: *
 Copyright: Copyright (C) 2017 The Android Open Source Project
-License: Apache 2
+License: Apache-2
  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
+      /usr/share/common-licenses/Apache-2.0
  .
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/debian/gitlab-ci.yml b/debian/gitlab-ci.yml
new file mode 100644
index 0000000..892f3cd
--- /dev/null
+++ b/debian/gitlab-ci.yml
@@ -0,0 +1,3 @@
+include:
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml
diff --git a/debian/perfetto.install b/debian/perfetto.install
index 1ae73aa..ded0fdc 100644
--- a/debian/perfetto.install
+++ b/debian/perfetto.install
@@ -1,8 +1,6 @@
 out/release/libperfetto.so usr/lib
 out/release/traced usr/sbin
-out/release/traced_perf usr/sbin
 out/release/traced_probes usr/sbin
 out/release/perfetto usr/bin
 debian/traced.service lib/systemd/system
-debian/traced-perf.service lib/systemd/system
 debian/traced-probes.service lib/systemd/system
diff --git a/debian/perfetto.manpages b/debian/perfetto.manpages
new file mode 100644
index 0000000..07cb459
--- /dev/null
+++ b/debian/perfetto.manpages
@@ -0,0 +1 @@
+debian/perfetto.1
diff --git a/debian/postinst b/debian/postinst
index c32e828..a583930 100755
--- a/debian/postinst
+++ b/debian/postinst
@@ -1,5 +1,9 @@
 #!/bin/sh
 set -e
-adduser --quiet --system --no-create-home --group traced
+adduser --home /nonexistent --quiet --system --no-create-home --group traced
 addgroup --quiet --system traced-consumer
 usermod -a -G traced-consumer traced
+mkdir -m 755 /run/perfetto
+chown traced:traced /run/perfetto
+
+#DEBHELPER#
diff --git a/debian/postrm b/debian/postrm
index 20c9767..8f8abc0 100755
--- a/debian/postrm
+++ b/debian/postrm
@@ -1,3 +1,5 @@
 #!/bin/sh
 set -e
-rm -f /tmp/perfetto-consumer /tmp/perfetto-producer
+rm -rf /run/perfetto
+
+#DEBHELPER#
diff --git a/debian/rules b/debian/rules
index 594466f..f8d21e0 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,13 +1,41 @@
 #!/usr/bin/make -f
+
+export DEB_BUILD_MAINT_OPTIONS = hardening=+all
+DPKG_EXPORT_BUILDFLAGS = 1
+include /usr/share/dpkg/buildflags.mk
+
 %:
 	dh $@
 
+override_dh_auto_configure: MAYBE_HOST_CPU=$(shell \
+	if [ "${DEB_BUILD_GNU_CPU}" = "i686" ]; then \
+	  echo "host_cpu=\\\"x86\\\"";\
+	elif [ "${DEB_BUILD_GNU_CPU}" = "x86_64" ]; then \
+	  echo "host_cpu=\\\"x64\\\"";\
+	elif [ "${DEB_BUILD_GNU_CPU}" = "aarch64" ]; then \
+	  echo "host_cpu=\\\"arm64\\\"";\
+	elif [ "${DEB_BUILD_GNU_CPU}" = "arm64" ]; then \
+	  echo "host_cpu=\\\"arm64\\\"";\
+	elif [ "${DEB_BUILD_GNU_CPU:0:3}" == "arm" ]; then \
+	  echo "host_cpu=\\\"arm\\\"";\
+	fi\
+)
 override_dh_auto_configure:
-	tools/install-build-deps
-	tools/gn gen out/release --args="is_debug=false"
+	env
+	uname -a
+	gn gen out/release --args="is_debug=false use_custom_libcxx=false\
+	  is_hermetic_clang=false is_system_compiler=true is_clang=false\
+	  skip_buildtools_check=true enable_perfetto_integration_tests=false\
+	  enable_perfetto_unittests=false perfetto_use_system_protobuf=true\
+	  perfetto_use_system_zlib=true perfetto_enable_git_rev_version_header=false\
+	  extra_cflags=\"${CFLAGS}\" extra_cxxflags=\"${CXXFLAGS}\"\
+	  extra_ldflags=\"${LDFLAGS}\" cc=\"${CC}\" cxx=\"${CXX}\"\
+	  ${MAYBE_HOST_CPU}"
 
 override_dh_auto_build:
-	tools/ninja -C out/release
+	ninja -C out/release perfetto traced traced_probes
+	pandoc docs/reference/perfetto-cli.md -s -t man --shift-heading-level-by=-1 >\
+	 debian/perfetto.1
 
 override_dh_auto_clean:
-	tools/gn clean out/release
+	rm -rf out/release
diff --git a/debian/traced-perf.service b/debian/traced-perf.service
index b04463b..73bffb9 100644
--- a/debian/traced-perf.service
+++ b/debian/traced-perf.service
@@ -1,5 +1,6 @@
 [Unit]
 Description=Perfetto sampling and profiling data source
+Documentation=https://perfetto.dev/docs/
 
 [Service]
 ExecStart=/usr/sbin/traced_perf
diff --git a/debian/traced-probes.service b/debian/traced-probes.service
index 81a23f7..1d853b0 100644
--- a/debian/traced-probes.service
+++ b/debian/traced-probes.service
@@ -1,5 +1,6 @@
 [Unit]
 Description=Perfetto data sources for system tracing (ftrace and /proc pollers)
+Documentation=https://perfetto.dev/docs/
 
 [Service]
 ExecStart=/usr/sbin/traced_probes
diff --git a/debian/traced.service b/debian/traced.service
index 6eb7d8a..4ca8d7a 100644
--- a/debian/traced.service
+++ b/debian/traced.service
@@ -1,5 +1,6 @@
 [Unit]
 Description=Perfetto tracing service daemon
+Documentation=https://perfetto.dev/docs/
 
 [Service]
 ExecStart=/usr/sbin/traced \
diff --git a/debian/triggers b/debian/triggers
new file mode 100644
index 0000000..dd86603
--- /dev/null
+++ b/debian/triggers
@@ -0,0 +1 @@
+activate-noawait ldconfig
diff --git a/docs/analysis/trace-processor.md b/docs/analysis/trace-processor.md
index f12afb6..d9059c7 100644
--- a/docs/analysis/trace-processor.md
+++ b/docs/analysis/trace-processor.md
@@ -244,6 +244,41 @@
 GROUP BY thread_name
 ```
 
+## Helper functions
+Helper functions are functions built into C++ which reduce the amount of
+boilerplate which needs to be written in SQL.
+
+### Extract args
+`EXTRACT_ARG` is a helper function which retreives a property of an
+event (e.g. slice or counter) from the `args` table.
+
+It takes an `arg_set_id` and `key` as input and returns the value looked
+up in the `args` table.
+
+For example, to retrieve the `prev_comm` field for `sched_switch` events in
+the `raw` table.
+```sql
+SELECT EXTRACT_ARG(arg_set_id, 'prev_comm')
+FROM raw
+WHERE name = 'sched_switch'
+```
+
+Behind the scenes, the above query would desugar to the following:
+```sql
+SELECT
+  (
+    SELECT string_value
+    FROM args
+    WHERE key = 'prev_comm' AND args.arg_set_id = raw.arg_set_id
+  )
+FROM raw
+WHERE name = 'sched_switch'
+```
+
+NOTE: while convinient, `EXTRACT_ARG` can inefficient compared to a `JOIN`
+when working with very large tables; a function call is required for every
+row which will be slower than the batch filters/sorts used by `JOIN`.
+
 ## Operator tables
 SQL queries are usually sufficient to retrieve data from trace processor.
 Sometimes though, certain constructs can be difficult to express pure SQL.
diff --git a/docs/concepts/config.md b/docs/concepts/config.md
index 4226653..99f4cbc 100644
--- a/docs/concepts/config.md
+++ b/docs/concepts/config.md
@@ -42,6 +42,9 @@
 TIP: Some more complete examples of trace configs can be found in the repo in
 [`/test/configs/`](/test/configs/).
 
+NOTE: If you are tracing on Android using adb and experiencing problems, see
+      [the Android section](#android) below.
+
 ## TraceConfig
 
 The TraceConfig is a protobuf message
@@ -472,6 +475,24 @@
 data_sources { ... }
 ```
 
+## Android
+
+On Android, there are some caveats around using `adb shell`
+
+* Ctrl+C, which normally causes a graceful termination of the trace, is not
+  propagated by ADB when using `adb shell perfetto` but only when using an
+  interactive PTY-based session via `adb shell`.
+* On non-rooted devices before Android 12, the config can only be passed as
+  `cat config | adb shell perfetto -c -` (-: stdin) because of over-restrictive
+  SELinux rules. Since Android 12 `/data/misc/perfetto-configs` can be used for
+  storing configs.
+* On devices before Android 10, adb cannot directly pull
+  `/data/misc/perfetto-traces`. Use
+  `adb shell cat /data/misc/perfetto-traces/trace > trace` to work around.
+* When capturing longer traces, e.g. in the context of benchmarks or CI, use
+  `PID=$(perfetto --background)` and then `kill $PID` to stop.
+
+
 ## Other resources
 
 * [TraceConfig Reference](/docs/reference/trace-config-proto.autogen)
diff --git a/docs/contributing/build-instructions.md b/docs/contributing/build-instructions.md
index 0c12902..9eba8f2 100644
--- a/docs/contributing/build-instructions.md
+++ b/docs/contributing/build-instructions.md
@@ -7,64 +7,99 @@
 
 Perfetto can be built both from the Android tree (AOSP) and standalone.
 Standalone builds are meant only for local testing and are not shipped.
-Due to the reduced dependencies they are faster to iterate on and the
-suggested way to work on Perfetto.
+Due to the reduced dependencies, the standalone workflow is faster to iterate on
+and the suggested way to work on Perfetto, unless you are working on code that
+has non-NDK depedencies into Android internals. Profilers and internal HAL/AIDL
+dependencies will not be built in the standalone build.
 
-## Get the code
+If you are chromium contributor, AOSP is still the place you should send CLs to.
+The code inside chromium's
+[third_party/perfetto](https://source.chromium.org/chromium/chromium/src/+/main:third_party/perfetto/?q=f:third_party%2Fperfetto&ss=chromium)
+is a direct mirror of the AOSP repo. The
+[AOSP->Chromium autoroller](https://autoroll.skia.org/r/perfetto-chromium-autoroll)
+takes care of keeping chromium's DEPS up to date.
 
-**Standalone checkout**:
+## Standalone builds
+
+#### Get the code
 
 ```bash
 git clone https://android.googlesource.com/platform/external/perfetto/
 ```
 
-**Android tree**:
-
-Perfetto lives in [`external/perfetto` in the AOSP tree](https://cs.android.com/android/platform/superproject/+/master:external/perfetto/).
-
-## Prerequisites
-
-**Standalone checkout**:
-
-All dependent libraries are self-hosted and pulled through:
+#### Pull dependent libraries and toolchains
 
 ```bash
 tools/install-build-deps [--android] [--ui]
 ```
 
-**Android tree**:
+`--android` will pull the Android NDK, emulator and other deps required
+to build for `target_os = "android"`.
 
-See https://source.android.com/setup
+`--ui` will pull NodeJS and all the NPM modules required to build the
+Web UI. See the [UI Development](#ui-development) section below for more.
 
-## Building
+#### Generate the build files via GN
 
-**Standalone checkout**:
-
-If you are a chromium developer and have depot_tools installed you can avoid
-the `tools/` prefix below and just use gn/ninja from depot_tools.
-
-`$ tools/gn args out/android` to generate build files and enter in the editor:
-
-```python
-target_os = "android"                 # Only when building for Android
-target_cpu = "arm" / "arm64" / "x64"
-is_debug = true / false
-cc_wrapper = "ccache"                 # Optionally speed repeated builds with ccache
-```
-
-(See the [Build Configurations](#build-configurations) section below for more)
+Perfetto uses [GN](https://gn.googlesource.com/gn/+/HEAD/docs/quick_start.md)
+as primary build system. See the [Build files](#build-files) section below for
+more.
 
 ```bash
-tools/ninja -C out/android
+tools/gn args out/android` 
 ```
 
-**Android tree**
+This will open an editor to customize the GN args. Enter:
 
-`mmma external/perfetto`
-or
-`m perfetto traced traced_probes`
+```python
+# Set only when building for Android, omit when building for linux, mac or win.
+target_os = "android"
+target_cpu = "arm" / "arm64" / "x64"
+
+is_debug = true / false
+cc_wrapper = "ccache"             # [Optional] speed up rebuilds with ccache.
+```
+
+See the [Build Configurations](#build-configurations) and
+[Building on Windows](#building-on-windows) sections below for more.
+
+TIP: If you are a chromium developer and have depot_tools installed you can
+avoid the `tools/` prefix below and just use gn/ninja from depot_tools.
+
+#### Build native C/C++ targets
+
+```bash
+# This will build all the targets.
+tools/ninja -C out/android
+
+# Alternatively, list targets explicitly.
+tools/ninja -C out/android \
+  traced \                 # Tracing service.
+  traced_probes \          # Ftrace interop and /proc poller.
+  perfetto \               # Cmdline client.
+  trace_processor_shell \  # Trace parsing.
+  trace_to_text            # Trace conversion.
+...
+```
+
+## Android tree builds
+
+Follow these instructions if you are an AOSP contributor.
+
+The source code lives in [`external/perfetto` in the AOSP tree](https://cs.android.com/android/platform/superproject/+/master:external/perfetto/).
+
+Follow the instructions on https://source.android.com/setup/build/building .
+
+Then:
+
+```bash
+mmma external/perfetto
+# or
+m traced traced_probes perfetto
+```
 
 This will generate artifacts `out/target/product/XXX/system/`.
+
 Executables and shared libraries are stripped by default by the Android build
 system. The unstripped artifacts are kept into `out/target/product/XXX/symbols`.
 
@@ -100,19 +135,6 @@
 source file is changed it, the script will automatically re-build it and show a
 prompt in the web page.
 
-## IDE setup
-
-Use a following command in the checkout directory in order to generate the
-compilation database file:
-
-```bash
-tools/gn gen out/default --export-compile-commands
-```
-
-After generating, it can be used in CLion (File -> Open -> Open As Project),
-Visual Studio Code with C/C++ extension and any other tool and editor that
-supports the compilation database format.
-
 ## Build files
 
 The source of truth of our build file is in the BUILD.gn files, which are based
@@ -120,6 +142,8 @@
 The Android build file ([Android.bp](/Android.bp)) is autogenerated from the GN
 files through `tools/gen_android_bp`, which needs to be invoked whenever a
 change touches GN files or introduces new ones.
+Likewise, the Bazel build file ([BUILD](/BUILD)) is autogenerated through the
+`tools/gen_bazel` script.
 
 A presubmit check checks that the Android.bp is consistent with GN files when
 submitting a CL through `git cl upload`.
@@ -142,15 +166,79 @@
 
 **Mac**
 
-- XCode 9 / clang (currently maintained best-effort).
+- XCode 9 / clang (maintained best-effort).
 
 **Windows**
 
-Windows builds are not currently supported when using the standalone checkout
-and GN. Windows is supported only for a subset of the targets (mainly
-`trace_processor` and the in-process version of the
-[Tracing SDK](/docs/instrumentation/tracing-sdk.md)) in two ways:
-(1) when building through Bazel; (2) when building as part of Chromium.
+- Windows 10 with either MSVC 2019 or clang-cl (maintained best-effort).
+
+### Building on Windows
+
+Building on Windows is possible using both the MSVC 2019 compiler (you don't
+need the full IDE, just the build tools) or the LLVM clang-cl compiler.
+
+The Windows support in standalone builds has been introduced in v16 by
+[r.android.com/1711913](https://r.android.com/1711913).
+
+clang-cl support is more stable because that build configuration is actively
+covered by the Chromium project (Perfetto rolls into chromium and underpins
+chrome://tracing). The MSVC build is maintained best-effort.
+
+The following targets are supported on Windows:
+
+- `trace_processor_shell`: the trace importer and SQL query engine.
+- `trace_to_text`: the trace conversion tool.
+- `traced` and `perfetto`: the tracing service and cmdline client. They use an
+  alternative implementation of the [inter-process tracing protocol](/docs/design-docs/api-and-abi.md#tracing-protocol-abi)
+  based on a TCP socket and named shared memory. This configuration is only for
+  testing / benchmarks and is not shipped in production.
+  Googlers: see [go/perfetto-win](http://go/perfetto-win) for details.
+- `perfetto_unittests` / `perfetto_integrationtests`: although they support only
+  the subset of code that is supported on Windows (e.g. no ftrace).
+
+It is NOT possible to build the Perfetto UI from Windows.
+
+#### Prerequisites
+
+You need all of these both for MSVC and clang-cl:
+
+- [Build Tools for Visual Studio 2019](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019)
+- [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk/)
+- [Python 3](https://www.python.org/downloads/windows/)
+
+The [`win_find_msvc.py`](/gn/standalone/toolchain/win_find_msvc.py) script will
+locate the higest version numbers available from
+`C:\Program Files (x86)\Windows Kits\10` and
+`C:\Program Files (x86)\Microsoft Visual Studio\2019`.
+
+#### Pull dependent libraries and toolchains
+
+```bash
+# This will download also the LLVM clang-cl prebuilt used by chromium.
+python3 tools/install-build-deps
+```
+
+#### Generate build files
+
+```bash
+python3 tools/gn gen out/win
+```
+
+In the editor type:
+
+```bash
+is_debug = true | false
+
+is_clang = true  # Will use the hermetic clang-cl toolchain.
+# or
+is_clang = false  # Will use MSVC 2019.
+```
+
+#### Build
+
+```bash
+python3 tools/ninja -C out/win perfetto traced trace_processor_shell
+```
 
 ## Build configurations
 
@@ -304,3 +392,73 @@
 ```
 
 [gn-quickstart]: https://gn.googlesource.com/gn/+/master/docs/quick_start.md
+
+## IDE setup
+
+Use a following command in the checkout directory in order to generate the
+compilation database file:
+
+```bash
+tools/gn gen out/default --export-compile-commands
+```
+
+After generating, it can be used in CLion (File -> Open -> Open As Project),
+Visual Studio Code with C/C++ extension and any other tool and editor that
+supports the compilation database format.
+
+#### Useful extensions
+
+If you are using VS Code we suggest the following extensions:
+
+- [Clang-Format](https://marketplace.visualstudio.com/items?itemName=xaver.clang-format)
+- [C/C++](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools)
+- [clangd](https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.vscode-clangd)
+- [Native Debug](https://marketplace.visualstudio.com/items?itemName=webfreak.debug)
+- [GNFormat](https://marketplace.visualstudio.com/items?itemName=persidskiy.vscode-gnformat)
+- [ESlint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
+- [markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint)
+
+#### Useful settings
+
+In `.vscode/settings.json`:
+
+```json
+{
+  "C_Cpp.clang_format_path": "${workspaceRoot}/buildtools/mac/clang-format",
+  "C_Cpp.clang_format_sortIncludes": true,
+  "files.exclude": {
+    "out/*/obj": true,
+    "out/*/gen": true,
+  },
+  "clangd.arguments": [
+    "--compile-commands-dir=${workspaceFolder}/out/mac_debug",
+    "--completion-style=detailed",
+    "--header-insertion=never"
+  ],
+}
+```
+
+Replace `/mac/` with `/linux64/` on Linux.
+
+### Debugging with VSCode
+
+Edit `.vscode/launch.json`:
+
+```json
+{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "request": "launch",
+      "type": "cppdbg",
+      "name": "Perfetto unittests",
+      "program": "${workspaceRoot}/out/mac_debug/perfetto_unittests",
+      "args": ["--gtest_filter=TracingServiceImplTest.StopTracingTriggerRingBuffer"],
+      "cwd": "${workspaceFolder}/out/mac_debug",
+      "MIMode": "lldb",
+    },
+  ]
+}
+```
+
+Then open the command palette `Meta`+`Shift`+`P` -> `Debug: Start debugging`.
diff --git a/docs/contributing/embedding.md b/docs/contributing/embedding.md
index ad3ac88..dbf6c56 100644
--- a/docs/contributing/embedding.md
+++ b/docs/contributing/embedding.md
@@ -42,7 +42,7 @@
 
 Metrics can also be registered at run time using the `RegisterMetric` and `ExtendMetricsProto` functions. These can subsequently be executed with `ComputeMetric`.
 
-WARNING: embedders should ensure that the path of any registered metric is consistent with the the name used to execute the metric and output view in the SQL.
+WARNING: embedders should ensure that the path of any registered metric is consistent with the name used to execute the metric and output view in the SQL.
 
 ### Annotations
 
diff --git a/docs/contributing/sdk-releasing.md b/docs/contributing/sdk-releasing.md
index 8469809..9679382 100644
--- a/docs/contributing/sdk-releasing.md
+++ b/docs/contributing/sdk-releasing.md
@@ -15,19 +15,19 @@
 Next, decide the version number for the new release (vX.Y).
 The major version number (X) is incremented on every release (monthly).
 The minor version number is incremented only for minor changes / fixes on top of the monthly
-release (cherry-picks on the releases/vN.x branch). 
+release (cherry-picks on the releases/vN.x branch).
 
 Continue with the appropriate section below.
 
 ## a) Creating a new major version
 
-Create a release branch for the new major version ("v15.x" here):
+Create a release branch for the new major version ("v16.x" here):
 
 ```bash
 git fetch origin
-git push origin origin/master:refs/heads/releases/v15.x
+git push origin origin/master:refs/heads/releases/v16.x
 git fetch origin
-git checkout -b releases/v15.x -t origin/releases/v15.x
+git checkout -b releases/v16.x -t origin/releases/v16.x
 ```
 
 Continue with [building the release](#building-and-tagging-the-release).
@@ -38,7 +38,7 @@
 revision for the new release, resolving any conflicts you may encounter.
 
 ```bash
-git checkout -b releases/v15.x -t origin/releases/v15.x
+git checkout -b releases/v16.x -t origin/releases/v16.x
 ```
 
 If you only want to introduce one or two patches in the new release, consider
@@ -54,6 +54,24 @@
 git merge <sha1>
 ```
 
+Update the CHANGELOG with a dedicated entry for the new minor version.
+This is important because the
+[write_version_header.py](/tools/write_version_header.py) script, which is
+invoked by the build system, looks at the CHANGELOG to work out the latest
+v${maj}.${min} version.
+
+For an example see [r.android.com/1730332](https://r.android.com/1730332)
+
+```txt
+v16.1 - 2021-06-08:
+  Tracing service and probes:
+    * Cherry-pick of r.android.com/1716718 which missed the v16 branch ... .
+
+
+v16.0 - 2021-06-01:
+  ...
+```
+
 ## Building and tagging the release
 
 1. Generate and commit the amalgamated source files.
@@ -96,7 +114,7 @@
 git pull
 
 git status
-# Should print: Your branch is up to date with 'origin/releases/v15.x'.
+# Should print: Your branch is up to date with 'origin/releases/v16.x'.
 # Do NOT proceed if your branch has diverged from origin/releases/vX.X
 
 git tag -a -m "Perfetto vX.Y" vX.Y
@@ -108,4 +126,6 @@
    - [docs/instrumentation/tracing-sdk.md](/docs/instrumentation/tracing-sdk.md)
    - [examples/sdk/README.md](/examples/sdk/README.md)
 
+6. Send an email with the CHANGELOG to perfetto-dev@ (internal) and perfetto-dev@googlegroups.com.
+
 Phew, you're done!
diff --git a/docs/data-sources/cpu-scheduling.md b/docs/data-sources/cpu-scheduling.md
index 8b771e8..29a2144 100644
--- a/docs/data-sources/cpu-scheduling.md
+++ b/docs/data-sources/cpu-scheduling.md
@@ -147,3 +147,45 @@
 
 ![](/docs/images/latency.png "Scheduling wake-up events in the UI")
 
+### Decoding `end_state`
+
+The [sched_slice](/docs/analysis/sql-tables.autogen#sched_slice) table contains
+information on scheduling activity of the system:
+
+```
+> select * from sched_slice limit 1
+id  type        ts          dur    cpu utid end_state priority
+0   sched_slice 70730062200 125364 0   1    S         130     
+```
+
+Each row of the table shows when a given thread (`utid`) began running
+(`ts`), on which core it ran (`cpu`), for how long it ran (`dur`), 
+and why it stopped running: `end_state`.
+
+`end_state` is encoded as one or more ascii characters. The UI uses
+the following translations to convert `end_state` into human readable
+text:
+
+| end_state  | Translation            |
+|------------|------------------------|
+| R          | Runnable               |
+| S          | Sleeping               |
+| D          | Uninterruptible Sleep |
+| T          | Stopped                |
+| t          | Traced                 |
+| X          | Exit (Dead)            |
+| Z          | Exit (Zombie)          |
+| x          | Task Dead              |
+| I          | Task Dead              |
+| K          | Wake Kill              |
+| W          | Waking                 |
+| P          | Parked                 |
+| N          | No Load                |
+| +          | (Preempted)            |
+
+Not all combinations of characters are meaningful.
+
+If we do not know when the scheduling ended (for example because the
+trace ended while the thread was still running) `end_state` will be
+`NULL` and `dur` will be -1.
+
diff --git a/docs/data-sources/frametimeline.md b/docs/data-sources/frametimeline.md
new file mode 100644
index 0000000..6b711bf
--- /dev/null
+++ b/docs/data-sources/frametimeline.md
@@ -0,0 +1,270 @@
+# Android Jank detection with FrameTimeline
+
+NOTE: **FrameTimeline requires Android 12(S) or higher**
+
+A frame is said to be janky if the time the frame was presented on screen does
+not match the predicted present time given by the scheduler.
+
+A jank can cause:
+* Unstable frame rate
+* Increased latency
+
+FrameTimeline is a module within SurfaceFlinger that detects janks and reports
+the source of the jank.
+[SurfaceViews](https://developer.android.com/reference/android/view/SurfaceView)
+are currently **not supported**, but will be, in future.
+
+## UI
+
+Two new tracks are added for every application that had at least one frame on
+screen.
+
+![](/docs/images/frametimeline/timeline_tracks.png)
+
+* Expected Timeline
+Each slice represents the time given to the app for rendering the
+frame. To avoid janks in the system, the app is expected to finish within this
+time frame.
+
+* Actual Timeline
+These slices represent the actual time an app took to complete the frame
+(including GPU work) and send it to SurfaceFlinger for composition. **Note: The
+actual frame start of apps is not yet known to FrameTimeline. Expected start
+time is used instead**. The end time of the slices here represent `max(gpu time,
+post time)`. **Post time** is the time the app's frame was posted to
+SurfaceFlinger.
+
+![](/docs/images/frametimeline/app-timelines.png)
+
+Similarly, SurfaceFlinger also gets these two new tracks representing the
+expected time it's supposed to finish within, and the actual time it took to
+finish compositing frames and presenting on-screen. Here, SurfaceFlinger's work
+represents everything underneath it in the display stack. This includes the
+Composer and the DisplayHAL. So, the slices represent SurfaceFlinger main
+thread's start to on-screen update.
+
+The names of the slices represent the token received from
+[choreographer](https://developer.android.com/reference/android/view/Choreographer).
+You can compare a slice in the actual timeline track to its corresponding slice
+in the expected timeline track to see how the app performed compared to the
+expectations. In addition, for debugging purposes, the token is added to the
+app's **doFrame** and **RenderThread** slices. For SurfaceFlinger, the same
+token is shown in **onMessageReceived**.
+
+![](/docs/images/frametimeline/app-vsyncid.png)
+
+![](/docs/images/frametimeline/sf-vsyncid.png)
+
+### Selecting an actual timeline slice
+
+![](/docs/images/frametimeline/selection.png)
+
+The selection details provide more information on what happened with the frame.
+These include:
+
+* **Present Type**
+
+Was the frame early, on time or late.
+* **On time finish**
+
+Did the application finish its work for the frame on time?
+* **Jank Type**
+
+Was there a jank observed with this frame? If yes, this shows what type of jank
+was observed. If not, the type would be **None**.
+* **Prediction type**
+
+Did the prediction expire by the time this frame was received by FrameTimeline?
+If yes, this will say **Expired Prediction**. If not, **Valid Prediction**.
+* **GPU Composition**
+
+Boolean that tells if the frame was composited by the GPU or not.
+* **Layer Name**
+
+Name of the Layer/Surface to which the frame was presented. Some processes
+update frames to multiple surfaces. Here, multiple slices with the same token
+will be shown in the Actual Timeline. Layer Name can be a good way to
+disambiguate between these slices.
+* **Is Buffer?**
+
+Boolean that tells if the frame corresponds to a buffer or an animation.
+
+### Flow events
+
+Selecting an actual timeline slice in the app also draws a line back to the
+corresponding SurfaceFlinger timeline slice.
+
+![](/docs/images/frametimeline/select-app-slice.png)
+
+Since SurfaceFlinger can composite frames from multiple layers into a single
+frame-on-screen (called a **DisplayFrame**), selecting a DisplayFrame draws
+arrows to all the frames that were composited together. This can span over
+multiple processes.
+
+![](/docs/images/frametimeline/select-sf-slice-1.png)
+![](/docs/images/frametimeline/select-sf-slice-2.png)
+
+### Color codes
+
+| Color | Image | Description    |
+| :---      | :---: | :---           |
+| Green | ![](/docs/images/frametimeline/green.png) | A good frame. No janks observed |
+| Light Green | ![](/docs/images/frametimeline/light-green.png) | High latency state. The framerate is smooth but frames are presented late, resulting in an increased input latency.|
+| Red | ![](/docs/images/frametimeline/red.png) | Janky frame. The process the slice belongs to, is the reason for the jank. |
+| Yellow | ![](/docs/images/frametimeline/yellow.png) | Used only by the apps. The frame is janky but app wasn't the reason, SurfaceFlinger caused the jank. |
+| Blue | ![](/docs/images/frametimeline/blue.png) | Dropped frame. Not related to jank. The frame was dropped by SurfaceFlinger, preferring an updated frame over this. |
+
+## Janks explained
+
+The jank types are defined in
+[JankInfo.h](http://cs/android/frameworks/native/libs/gui/include/gui/JankInfo.h?l=22).
+Since each app is written differently, there is no common way to go into the
+internals of the apps and specify what the reason for the jank was. Our goal is
+not to do this but rather, provide a quick way to tell if app was janky or if
+SurfaceFlinger was janky.
+
+### None
+
+All good. No jank with the frame. The ideal state that should be aimed for.
+
+### App janks
+
+* **AppDeadlineMissed**
+
+The app ran longer than expected causing a jank. The total time taken by the app
+frame is calculated by using the choreographer wake-up as the start time and
+max(gpu, post time) as the end time. Post time is the time the frame was sent to
+SurfaceFlinger. Since the GPU usually runs in parallel, it could be that the gpu
+finished later than the post time.
+
+* **BufferStuffing**
+
+This is more of a state than a jank. This happens if the app keeps sending new
+frames to SurfaceFlinger before the previous frame was even presented. The
+internal Buffer Queue is stuffed with buffers that are yet to be presented,
+hence the name, Buffer Stuffing. These extra buffers in the queue are presented
+only one after the other thus resulting in extra latency.
+This can also result in a stage where there are no more buffers for the app to
+use and it goes into a dequeue blocking wait.
+The actual duration of work performed by the app might still be within the
+deadline, but due to the stuffed nature, all the frames will be presented at
+least one vsync late no matter how quickly the app finishes its work.
+Frames will still be smooth in this state but there is an increased input
+latency associated with the late present.
+
+### SurfaceFlinger Janks
+
+There are two ways SurfaceFlinger can composite frames.
+* Device Composition - uses a dedicated hardware
+* GPU/Client composition - uses GPU to composite
+
+An important thing to note is that performing device composition happens as a
+blocking call on the main thread. However, GPU composition happens in parallel.
+SurfaceFlinger performs the necessary draw calls and then hands over the gpu
+fence to the display device. The display device then waits for the fence to be
+signaled, and then presents the frame.
+
+* **SurfaceFlingerCpuDeadlineMissed**
+
+SurfaceFlinger is expected to finish within the given deadline. If the main
+thread ran for longer than that, the jank is then
+SurfaceFlingerCpuDeadlineMissed. SurfaceFlinger’s CPU time is the time spent on
+the main thread. This includes the entire composition time if device composition
+was used. If GPU composition was used, this includes the time to write the draw
+calls and handing over the frame to the GPU.
+
+* **SurfaceFlingerGpuDeadlineMissed**
+
+The time taken by SurfaceFlinger’s main thread on the CPU + the GPU composition
+time together were longer than expected. Here, the CPU time would have still
+been within the deadline but since the work on the GPU wasn’t ready on time, the
+frame got pushed to the next vsync.
+
+* **DisplayHAL**
+
+DisplayHAL jank refers to the case where SurfaceFlinger finished its work and
+sent the frame down to the HAL on time, but the frame wasn’t presented on the
+vsync. It was presented on the next vsync. It could be that SurfaceFlinger did
+not give enough time for the HAL’s work or it could be that there was a genuine
+delay in the HAL’s work.
+
+* **PredictionError**
+
+SurfaceFlinger’s scheduler plans ahead the time to present the frames. However,
+this prediction sometimes drifts away from the actual hardware vsync time. For
+example, a frame might have predicted present time as 20ms. Due to a drift in
+estimation, the actual present time of the frame could be 23ms. This is called a
+Prediction Error in SurfaceFlinger’s scheduler. The scheduler corrects itself
+periodically, so this drift isn’t permanent. However, the frames that had a
+drift in prediction will still be classified as jank for tracking purposes.
+
+Isolated prediction errors are not usually perceived by the user as the
+scheduler is quick to adapt and fix the drift.
+
+### Unknown jank
+
+As the name suggests, the reason for the jank is unknown in this case. An
+example here would be that SurfaceFlinger or the App took longer than expected
+and missed the deadline but the frame was still presented early. The probability
+of such a jank happening is very low but not impossible.
+
+## SQL
+
+At the SQL level, frametimeline data is available in two tables
+* [`expected_frame_timeline_slice`](/docs/analysis/sql-tables.autogen#expected_frame_timeline_slice)
+* [`actual_frame_timeline_slice`](/docs/analysis/sql-tables.autogen#actual_frame_timeline_slice)
+
+```
+select ts, dur, surface_frame_token as app_token, display_frame_token as sf_token, process.name
+from expected_frame_timeline_slice left join process using(upid)
+```
+
+ts | dur | app_token | sf_token | name
+---|-----|-----------|----------|-----
+60230453475 | 20500000 | 3135 | 3142 | com.google.android.apps.nexuslauncher
+60241677540 | 20500000 | 3137 | 3144 | com.google.android.apps.nexuslauncher
+60252895412 | 20500000 | 3139 | 3146 | com.google.android.apps.nexuslauncher
+60284614241 | 10500000 | 0 | 3144 | /system/bin/surfaceflinger
+60295858299 | 10500000 | 0 | 3146 | /system/bin/surfaceflinger
+60297798913 | 20500000 | 3147 | 3150 | com.android.systemui
+60307075728 | 10500000 | 0 | 3148 | /system/bin/surfaceflinger
+60318297746 | 10500000 | 0 | 3150 | /system/bin/surfaceflinger
+60320236468 | 20500000 | 3151 | 3154 | com.android.systemui
+60329511401 | 10500000 | 0 | 3152 | /system/bin/surfaceflinger
+60340732956 | 10500000 | 0 | 3154 | /system/bin/surfaceflinger
+60342673064 | 20500000 | 3155 | 3158 | com.android.systemui
+
+
+```
+select ts, dur, surface_frame_token as app_token, display_frame_token, jank_type, on_time_finish, present_type, layer_name, process.name
+from actual_frame_timeline_slice left join process using(upid)
+```
+
+ts | dur | app_token | sf_token | jank_type | on_time_finish | present_type | layer_name | name
+---|-----|-----------|----------|-----------|----------------|--------------|------------|-----
+60230453475 | 26526379 | 3135 | 3142 | Buffer Stuffing | 1 | Late Present | TX - com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0 | com.google.android.apps.nexuslauncher
+60241677540 | 28235805 | 3137 | 3144 | Buffer Stuffing | 1 | Late Present | TX - com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0 | com.google.android.apps.nexuslauncher
+60252895412 | 2546525 | 3139 | 3142 | None | 1 | On-time Present | TX - NavigationBar0#0 | com.android.systemui
+60252895412 | 27945382 | 3139 | 3146 | Buffer Stuffing | 1 | Late Present | TX - com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0 | com.google.android.apps.nexuslauncher
+60284808190 | 10318230 | 0 | 3144 | None | 1 | On-time Present | [NULL] | /system/bin/surfaceflinger
+60296067722 | 10265574 | 0 | 3146 | None | 1 | On-time Present | [NULL] | /system/bin/surfaceflinger
+60297798913 | 5239227 | 3147 | 3150 | None | 1 | On-time Present | TX - NavigationBar0#0 | com.android.systemui
+60307246161 | 10301772 | 0 | 3148 | None | 1 | On-time Present | [NULL] | /system/bin/surfaceflinger
+60318497204 | 10281199 | 0 | 3150 | None | 1 | On-time Present | [NULL] | /system/bin/surfaceflinger
+60320236468 | 2747559 | 3151 | 3154 | None | 1 | On-time Present | TX - NavigationBar0#0 | com.android.systemui
+
+## TraceConfig
+
+Trace Protos:
+[FrameTimelineEvent](/docs/reference/trace-packet-proto.autogen#FrameTimelineEvent)
+
+Datasource:
+
+```protobuf
+data_sources {
+    config {
+        name: "android.surfaceflinger.frametimeline"
+    }
+}
+```
+
diff --git a/docs/data-sources/native-heap-profiler.md b/docs/data-sources/native-heap-profiler.md
index 5b605ee..108a32d 100644
--- a/docs/data-sources/native-heap-profiler.md
+++ b/docs/data-sources/native-heap-profiler.md
@@ -116,6 +116,8 @@
 gets attributed the complete n bytes. For more accuracy, allocations larger than
 the sampling interval bypass the sampling logic and are recorded with their true
 size.
+See the [heapprofd Sampling](/docs/design-docs/heapprofd-sampling) document for
+details.
 
 ## Startup profiling
 
@@ -237,8 +239,6 @@
 
 ## Symbolization
 
-NOTE: Symbolization is currently only available on Linux and MacOS.
-
 ### Set up llvm-symbolizer
 
 You only need to do this once.
@@ -389,10 +389,11 @@
 
 ## (non-Android) Linux support
 
-NOTE: This is experimental and only for ad-hoc investigations.
+NOTE: Do not use this for production purposes.
 
 You can use a standalone library to profile memory allocations on Linux.
-First [build Perfetto](/docs/contributing/build-instructions.md)
+First [build Perfetto](/docs/contributing/build-instructions.md). You only need
+to do this once.
 
 ```
 tools/build_all_configs.py
@@ -408,34 +409,10 @@
 Start the profile (e.g. targeting trace_processor_shell)
 
 ```
+tools/heap_profile -n trace_processor_shell --print-config  | \
 out/linux_clang_release/perfetto \
   -c - --txt \
-  -o ~/heapprofd-trace \
-<<EOF
-
-buffers {
-  size_kb: 32768
-}
-
-data_sources {
-  config {
-    name: "android.heapprofd"
-    heapprofd_config {
-      shmem_size_bytes: 8388608
-      sampling_interval_bytes: 4096
-      block_client: true
-      process_cmdline: "trace_processor_shell"
-      dump_at_max: true
-    }
-  }
-}
-
-duration_ms: 604800000
-write_into_file: true
-flush_timeout_ms: 30000
-flush_period_ms: 604800000
-
-EOF
+  -o ~/heapprofd-trace
 ```
 
 Finally, run your target (e.g. trace_processor_shell) with LD_PRELOAD
@@ -459,6 +436,8 @@
 * `Failed to send control socket byte.` is displayed in logcat at the end of
   every profile. This is benign.
 * The object count may be incorrect in `dump_at_max` profiles.
+* Choosing a low shared memory buffer size and `block_client` mode might
+  lock up the target process.
 
 ### {#known-issues-android10} Android 10
 * Function names in libraries with load bias might be incorrect. Use
@@ -485,6 +464,8 @@
 * `Failed to send control socket byte.` is displayed in logcat at the end of
   every profile. This is benign.
 * The object count may be incorrect in `dump_at_max` profiles.
+* Choosing a low shared memory buffer size and `block_client` mode might
+  lock up the target process.
 
 ## Heapprofd vs malloc_info() vs RSS
 
@@ -523,7 +504,7 @@
 | fragmentation       |                   |              |  x  |
 
 If you observe high RSS or malloc\_info metrics but heapprofd does not match,
-you might be hitting some patological fragmentation problem in the allocator.
+you might be hitting some pathological fragmentation problem in the allocator.
 
 ## Convert to pprof
 
diff --git a/docs/design-docs/api-and-abi.md b/docs/design-docs/api-and-abi.md
index 2c8a6b6..29f47a5 100644
--- a/docs/design-docs/api-and-abi.md
+++ b/docs/design-docs/api-and-abi.md
@@ -272,7 +272,7 @@
 [`InitializeConnectionRequest`][producer_port.proto] request to the
 service, which is the very first IPC sent after connection.
 By default, the service creates the SMB and passes back its file descriptor to
-the producer with the the [`InitializeConnectionResponse`][producer_port.proto]
+the producer with the [`InitializeConnectionResponse`][producer_port.proto]
 IPC reply. Recent versions of the service (Android R / 11) allow the FD to be
 created by the producer and passed down to the service in the request. When the
 service supports this, it acks the request setting
diff --git a/docs/reference/checkpoint-atoms.md b/docs/design-docs/checkpoint-atoms.md
similarity index 100%
rename from docs/reference/checkpoint-atoms.md
rename to docs/design-docs/checkpoint-atoms.md
diff --git a/docs/design-docs/heapprofd-design.md b/docs/design-docs/heapprofd-design.md
index fe5a8ef..005b0d3 100644
--- a/docs/design-docs/heapprofd-design.md
+++ b/docs/design-docs/heapprofd-design.md
@@ -182,6 +182,9 @@
 
 The sampling rate is configurable as part of the initial handshake. A sampling rate == 1 will degenerate into the fully-accurate high-overhead mode.
 
+See [Sampling for Memory Profiles](/docs/design-docs/heapprofd-sampling) for
+more details.
+
 Prior art: [crbug.com/812262](http://crbug.com/812262), [crbug.com/803276](http://crbug.com/803276).
 
 ## Implementation Plan
diff --git a/docs/design-docs/heapprofd-sampling.md b/docs/design-docs/heapprofd-sampling.md
new file mode 100644
index 0000000..3ebfb3d
--- /dev/null
+++ b/docs/design-docs/heapprofd-sampling.md
@@ -0,0 +1,179 @@
+# heapprofd: Sampling for Memory Profiles
+
+_tomlewin, fmayer **·** 2021-04-14_  
+
+## Background
+
+A heap profiler associates memory allocations with the callstacks on which they
+happen ([example visualization]). It is prohibitively expensive to handle every
+allocation done by a program, so the [Android Heap Profiler] employs a sampling
+approach that handles a statistically meaningful subset of allocations. This
+document explores the statistical properties thereof.
+
+## Conceptual motivation
+Conceptually the sampling is implemented such that each byte has some
+probability p of being sampled. In theory we can think of each byte undergoing a
+Bernoulli trial. The reason we use a random sampling approach, as opposed to
+taking every nth byte, is that there may be regular allocation patterns in the
+code that may be missed by a correspondingly regular sampling.
+
+To scale the sampled bytes to the correct scale, we multiply by 1 / p, i.e. if
+we sample a byte with probability 10%, then each byte sampled represents 10
+bytes allocated.
+
+
+## Implementation
+
+In practice, the [algorithm] works as follows:
+
+1. We look at an allocation
+
+2. If the size of the allocation is large enough that there’s a greater than 99%
+   chance of it being sampled at least once, we return the size of the
+   allocation directly. This is a performance optimization.
+
+3. If the size of the allocation is smaller, then we compute the number of times
+   we would draw a sample if we sampled each byte with the given sampling rate:
+
+  * In practice we do this by keeping track of the arrival time of the next
+    sample. When an allocation happens, we subtract its size from the arrival
+    time of the next sample, and check whether we have brought it below zero. We
+    then repeatedly draw from the exponential distribution (which is the
+    interarrival time of Poisson) until the arrival time is brought back
+    above 0. The amount of times we had to draw from the exponential
+    distribution is the number of samples the allocation should count as.
+
+  * We multiply the number of samples we drew within the allocation by the
+    sampling rate to get an estimate of the size of the allocation
+
+We instead draw directly from the Poisson/binomial distribution to see how many
+samples we get for a given allocation size, but this is not as computationally
+efficient. This is because we don’t sample most of the allocations, due to their
+small size and our low sampling rate. This means it’s more efficient to use the
+exponential draw approach, as for a non-sample, we only need to decrement a
+counter. Sampling from the Poisson for every allocation (rather than drawing
+from exponential for every sample) is more expensive.
+
+## Theoretical performance
+
+If we sample at some rate 1 / p, then to set p reasonably we need to know what
+our probability of sampling an allocation of any size is, as well as our
+expected error when we sample it. If we set p = 1 then we sample everything and
+have no error. Reducing the sampling rate costs us coverage and accuracy.
+
+### Sampling probabilities
+
+We will sample an allocation with probability Exponential(sampling rate) <
+allocation size. This is equivalent to the probability that we do not fail to
+sample all bytes within the allocation if we sample bytes at our sampling rate.
+
+Because the exponential distribution is memoryless, we can add together
+allocations that are the same even if they occur apart for the purposes of
+probability. This means that if we have an allocation of 1MB and then another of
+1MB, the probability of us taking at least one sample is the same as the
+probability of us taking at least one sample of a contiguous 2MB allocation.
+
+We can see from the chart below that if we 16X our sampling rate from 32KiB to
+512KiB we still have a 95% chance of sampling anything above 1.5MiB. If we 64X
+it to 2048KiB we still have an 80% chance to sample anything above 3.5MiB.
+
+![](/docs/images/heapprofd-sampling/one-sample.png)
+
+### Expected errors
+We can also consider the expected error we’ll make at given allocation sizes and
+sampling rates. Like before it doesn’t matter where the allocation happens — if
+we have two allocations of the same type occurring at different times they have
+the same statistical properties as a single allocation with size equal to their
+sum. This is because the exponential distribution we use is memoryless.
+
+
+For expected error we report something like the [mean absolute percentage
+error]. This just means we see how wrong we might be in percent relative to the
+true allocation size, and then scale these results according to their
+probability of occurrence. This is an estimator that has some issues (i.e. it’s
+biased such that underestimates are more penalised) but it’s easy to reason
+about and it’s useful as a gauge of how wrong on average we might be with our
+estimates. I would recommend just reading this as analogous to a standard
+deviation.
+
+
+Charts below show both the expected error and the conditional expected error:
+the expected error assuming we have at least one sample within the allocation.
+There’s periodicity in both in line with the sampling rate used. The thing to
+take away is that, while the estimates are unbiased such that their expected
+value is equal to their real value, substantial errors are still possible.
+
+![](/docs/images/heapprofd-sampling/expected-error.png)
+
+Assuming that we take at least one sample of an allocation, what error might we
+expect? We can answer that using the following chart, which shows expected error
+conditional on at least one sample being taken. This is the expected error we
+can expect for the things that end up in our heap profile. It’s important to
+emphasise that this expected error methodology is not exact, and also that the
+underlying sampling remains unbiased — the expected value is the true value.
+This should be considered more as a measure akin to standard deviation.
+
+![](/docs/images/heapprofd-sampling/conditional-expected-error.png)
+
+## Performance Considerations
+### STL Distributions
+
+Benchmarking of the STL distributions on a Pixel 4 reinforces our approach of
+estimating the geometric distribution using an exponential distribution, as its
+performance is ~8x better ([full results]).
+
+
+Draw sample from exponential distribution with p = 1 / 32000:
+
+ARM64: 21.3ns (sd 0.066ns, negligibly small, smaller than a CPU cycle)
+
+ARM32: 33.2ns (sd 0.011ns, negligibly small, smaller than a CPU cycle)
+
+
+Draw sample from geometric distribution with p = 1 / 32000:
+
+ARM64: 169ns (sd 0.023ns, negligibly small, smaller than a CPU cycle) (7.93x)
+
+ARM32: 222ns (sd 0.118ns, negligibly small, smaller than a CPU cycle) (6.69x)
+
+## Appendix
+
+### Improvements made
+
+We previously (before Android 12) returned the size of the allocation accurately
+and immediately if the allocation size was greater than our sampling rate. This
+had several impacts.
+
+
+The most obvious impact is that with the old approach we would expect to sample
+an allocation equal in size to our sampling rate ~63% of the time, rather than
+100%. This means there is a discontinuity in coverage between an allocation a
+byte smaller than our sampling rate, and one a byte larger. This is still
+unbiased from a statistical perspective, but it’s important to note.
+
+
+Another unusual impact is that the sampling rate depends not only on the size of
+the memory being used, but also how it’s allocated. If our sampling rate were
+10KB, and we have an allocation that’s 10KB, we sample it with certainty. If
+instead that 10KB is split among two 5KB allocations, we sample it with
+probability 63%. Again this doesn’t bias our results, but it means that our
+measurements of memory where there are many small allocations might be noisier
+than ones where there are a few large allocations, even if total memory used is
+the same. If we didn’t return allocations greater than the sampling rate every
+time, then the memorylessness property of the exponential distribution would
+mean our method is insensitive to how the memory is split amongst allocations,
+which seems a desirable property.
+
+
+We altered the cutoff at which we simply returned the allocation size.
+Previously, as discussed, the cutoff was at the sampling rate, which led to a
+discontinuity. Now the cutoff is determined by the size at which we have a >99%
+chance of sampling an allocation at least once, given the sampling rate we’re
+using. This resolves the above issues.
+
+[algorithm]:
+  https://cs.android.com/android/platform/superproject/+/master:external/perfetto/src/profiling/memory/sampler.h
+[example visualization]: /docs/images/syssrv-apk-assets-two.png
+[Android Heap Profiler]: /docs/design-docs/heapprofd-design
+[mean absolute percentage error]: https://en.wikipedia.org/wiki/Mean_absolute_percentage_error
+[full results]: https://gist.github.com/fmayer/3aafcaf58f8db09714ba09aa4ac2b1ac
\ No newline at end of file
diff --git a/docs/images/frametimeline/app-timelines.png b/docs/images/frametimeline/app-timelines.png
new file mode 100644
index 0000000..73e5ee2
--- /dev/null
+++ b/docs/images/frametimeline/app-timelines.png
Binary files differ
diff --git a/docs/images/frametimeline/app-vsyncid.png b/docs/images/frametimeline/app-vsyncid.png
new file mode 100644
index 0000000..dbefd02
--- /dev/null
+++ b/docs/images/frametimeline/app-vsyncid.png
Binary files differ
diff --git a/docs/images/frametimeline/blue.png b/docs/images/frametimeline/blue.png
new file mode 100644
index 0000000..4bbad6f
--- /dev/null
+++ b/docs/images/frametimeline/blue.png
Binary files differ
diff --git a/docs/images/frametimeline/green.png b/docs/images/frametimeline/green.png
new file mode 100644
index 0000000..3735c6d
--- /dev/null
+++ b/docs/images/frametimeline/green.png
Binary files differ
diff --git a/docs/images/frametimeline/light-green.png b/docs/images/frametimeline/light-green.png
new file mode 100644
index 0000000..c04ea6e
--- /dev/null
+++ b/docs/images/frametimeline/light-green.png
Binary files differ
diff --git a/docs/images/frametimeline/red.png b/docs/images/frametimeline/red.png
new file mode 100644
index 0000000..29df82e
--- /dev/null
+++ b/docs/images/frametimeline/red.png
Binary files differ
diff --git a/docs/images/frametimeline/select-app-slice.png b/docs/images/frametimeline/select-app-slice.png
new file mode 100644
index 0000000..990616c
--- /dev/null
+++ b/docs/images/frametimeline/select-app-slice.png
Binary files differ
diff --git a/docs/images/frametimeline/select-sf-slice-1.png b/docs/images/frametimeline/select-sf-slice-1.png
new file mode 100644
index 0000000..b034a34
--- /dev/null
+++ b/docs/images/frametimeline/select-sf-slice-1.png
Binary files differ
diff --git a/docs/images/frametimeline/select-sf-slice-2.png b/docs/images/frametimeline/select-sf-slice-2.png
new file mode 100644
index 0000000..1685e9d
--- /dev/null
+++ b/docs/images/frametimeline/select-sf-slice-2.png
Binary files differ
diff --git a/docs/images/frametimeline/selection.png b/docs/images/frametimeline/selection.png
new file mode 100644
index 0000000..abb3759
--- /dev/null
+++ b/docs/images/frametimeline/selection.png
Binary files differ
diff --git a/docs/images/frametimeline/sf-vsyncid.png b/docs/images/frametimeline/sf-vsyncid.png
new file mode 100644
index 0000000..752324e
--- /dev/null
+++ b/docs/images/frametimeline/sf-vsyncid.png
Binary files differ
diff --git a/docs/images/frametimeline/timeline_tracks.png b/docs/images/frametimeline/timeline_tracks.png
new file mode 100644
index 0000000..7f4f173
--- /dev/null
+++ b/docs/images/frametimeline/timeline_tracks.png
Binary files differ
diff --git a/docs/images/frametimeline/yellow.png b/docs/images/frametimeline/yellow.png
new file mode 100644
index 0000000..56930fb
--- /dev/null
+++ b/docs/images/frametimeline/yellow.png
Binary files differ
diff --git a/docs/images/heapprofd-sampling/conditional-expected-error.png b/docs/images/heapprofd-sampling/conditional-expected-error.png
new file mode 100644
index 0000000..cf92006
--- /dev/null
+++ b/docs/images/heapprofd-sampling/conditional-expected-error.png
Binary files differ
diff --git a/docs/images/heapprofd-sampling/expected-error.png b/docs/images/heapprofd-sampling/expected-error.png
new file mode 100644
index 0000000..ea6a7ac
--- /dev/null
+++ b/docs/images/heapprofd-sampling/expected-error.png
Binary files differ
diff --git a/docs/images/heapprofd-sampling/one-sample.png b/docs/images/heapprofd-sampling/one-sample.png
new file mode 100644
index 0000000..d179ff1
--- /dev/null
+++ b/docs/images/heapprofd-sampling/one-sample.png
Binary files differ
diff --git a/docs/images/perfetto-ui-channel-toggle.png b/docs/images/perfetto-ui-channel-toggle.png
new file mode 100644
index 0000000..e530c0d
--- /dev/null
+++ b/docs/images/perfetto-ui-channel-toggle.png
Binary files differ
diff --git a/docs/images/perfetto-ui-channel.png b/docs/images/perfetto-ui-channel.png
new file mode 100644
index 0000000..aed8ac4
--- /dev/null
+++ b/docs/images/perfetto-ui-channel.png
Binary files differ
diff --git a/docs/images/perfetto-ui-version.png b/docs/images/perfetto-ui-version.png
new file mode 100644
index 0000000..00b523e
--- /dev/null
+++ b/docs/images/perfetto-ui-version.png
Binary files differ
diff --git a/docs/instrumentation/heapprofd-api.md b/docs/instrumentation/heapprofd-api.md
index 65afa95..88f6dff 100644
--- a/docs/instrumentation/heapprofd-api.md
+++ b/docs/instrumentation/heapprofd-api.md
@@ -1,22 +1,12 @@
 # heapprofd Custom Allocator API - Early Access
 
-WARNING: The heapprofd Custom Allocator API is currently in **pre-alpha**
-         stage. The API is subject to change, and the implementation might
-         still be unstable. Please file
-         [bugs](https://github.com/google/perfetto/issues/new) for any
-         issues you encounter.
+WARNING: The heapprofd Custom Allocator API is currently in **beta** stage.
+         Please file [bugs](https://github.com/google/perfetto/issues/new)
+         for any issues you encounter.
 
 NOTE: The heapprofd Custom Allocator API requires a device running Android
       10 or newer.
 
-## Give us a head's up
-
-Thanks for trying out the Custom Allocator API! As this is pre-alpha, we
-would ask you to [file a tracking GitHub issue](
-https://github.com/google/perfetto/issues/new) so we can communicate with you
-in case we have updated information. You do not need to wait for any reply on
-the bug before proceeding.
-
 ## Get SDK
 
 Before instrumenting your app, you need to get the heapprofd library and
diff --git a/docs/instrumentation/track-events.md b/docs/instrumentation/track-events.md
index e4b7279..6b31791 100644
--- a/docs/instrumentation/track-events.md
+++ b/docs/instrumentation/track-events.md
@@ -328,11 +328,8 @@
 
 ```C++
 TRACE_EVENT("cat", "name"[, track][, timestamp]
-                         [, "debug_name1", debug_value1]
-                         [, "debug_name2", debug_value2]
-                         ...
-                         [, "debug_nameN", debug_valueN]
-                         [, lambda]);
+    (, "debug_name", debug_value |, TrackEvent::kFieldName, value)*
+    [, lambda]);
 ```
 
 Some examples of valid combinations:
@@ -355,7 +352,7 @@
    ```
 
    |time_in_nanoseconds| should be an uint64_t by default. To support custom
-   timestamp types, 
+   timestamp types,
    |perfetto::TraceTimestampTraits<MyTimestamp>::ConvertTimestampToTraceTimeNs|
    should be defined. See |ConvertTimestampToTraceTimeNs| for more details.
 
@@ -364,22 +361,38 @@
    ```C++
      TRACE_EVENT("category", "Name", "arg", value);
      TRACE_EVENT("category", "Name", "arg", value, "arg2", value2);
-     TRACE_EVENT("category", "Name", "arg", value, "arg2", value2, 
-                                     "arg3", value3);
+     TRACE_EVENT("category", "Name", "arg", value, "arg2", value2,
+                 "arg3", value3);
    ```
 
    See |TracedValue| for recording custom types as debug annotations.
 
-4. Arbitrary number of debug annotations and a lambda:
+4. Arbitrary number of TrackEvent fields (including extensions):
+
+  ```C++
+    TRACE_EVENT("category", "Name",
+                perfetto::protos::pbzero::TrackEvent::kFieldName, value);
+  ```
+
+5. Arbitrary combination of debug annotations and TrackEvent fields:
+
+  ```C++
+    TRACE_EVENT("category", "Name",
+                perfetto::protos::pbzero::TrackEvent::kFieldName, value1,
+                "arg", value2);
+  ```
+
+6. Arbitrary combination of debug annotations / TrackEvent fields and a lambda:
 
    ```C++
-     TRACE_EVENT("category", "Name", "arg", value,
-         [&](perfetto::EventContext ctx) {
-       ctx.event()->set_custom_value(...);
-     });
+     TRACE_EVENT("category", "Name", "arg", value1,
+                 pbzero::TrackEvent::kFieldName, value2,
+                 [&](perfetto::EventContext ctx) {
+                     ctx.event()->set_custom_value(...);
+                 });
    ```
 
-5. An overridden track:
+7. An overridden track:
 
    ```C++
      TRACE_EVENT("category", "Name", perfetto::Track(1234));
@@ -387,38 +400,41 @@
 
    See |Track| for other types of tracks which may be used.
 
-6. A track and a lambda:
+8. A track and a lambda:
 
    ```C++
      TRACE_EVENT("category", "Name", perfetto::Track(1234),
-         [&](perfetto::EventContext ctx) {
-       ctx.event()->set_custom_value(...);
-     });
+                 [&](perfetto::EventContext ctx) {
+                     ctx.event()->set_custom_value(...);
+                 });
    ```
 
-7. A track and a timestamp:
+9. A track and a timestamp:
 
    ```C++
      TRACE_EVENT("category", "Name", perfetto::Track(1234),
-         time_in_nanoseconds);
+                 time_in_nanoseconds);
    ```
 
-8. A track, a timestamp and a lambda:
+10. A track, a timestamp and a lambda:
 
    ```C++
      TRACE_EVENT("category", "Name", perfetto::Track(1234),
-         time_in_nanoseconds, [&](perfetto::EventContext ctx) {
-       ctx.event()->set_custom_value(...);
-     });
+                 time_in_nanoseconds, [&](perfetto::EventContext ctx) {
+                     ctx.event()->set_custom_value(...);
+                 });
    ```
 
-9. A track and an arbitrary number of debug annotions:
+11. A track and any combination of debug annotions and TrackEvent fields:
 
    ```C++
      TRACE_EVENT("category", "Name", perfetto::Track(1234),
                  "arg", value);
      TRACE_EVENT("category", "Name", perfetto::Track(1234),
                  "arg", value, "arg2", value2);
+     TRACE_EVENT("category", "Name", perfetto::Track(1234),
+                 "arg", value, "arg2", value2,
+                 pbzero::TrackEvent::kFieldName, value3);
    ```
 
 ### Tracks
diff --git a/docs/quickstart/android-tracing.md b/docs/quickstart/android-tracing.md
index 7c7fef3..3d3b37e 100644
--- a/docs/quickstart/android-tracing.md
+++ b/docs/quickstart/android-tracing.md
@@ -17,6 +17,12 @@
 adb shell setprop persist.traced.enable 1
 ```
 
+If you are running a version of Android older than P, you can still capture a
+trace with Perfetto using the `record_android_trace` script. See instructions
+below in the
+[Recording a trace through the cmdline](#recording-a-trace-through-the-cmdline)
+section.
+
 ## Recording a trace
 
 Command line tools (usage examples below in this page):
@@ -65,6 +71,8 @@
 the command line. It is the equivalent of running `adb shell perfetto` but it
 helps with getting the paths right, auto-pulling the trace once done and opening
 it on the browser.
+Furthermore, on older versions of Android it takes care of sideloading the
+`tracebox` binary to make up for the lack of tracing system services.
 
 If you are already familiar with `systrace` or `atrace`, both cmdline tools
 support a systrace-equivalent syntax:
@@ -103,6 +111,9 @@
   `cat config | adb shell perfetto -c -` (-: stdin) because of over-restrictive
   SELinux rules. Since Android 12 `/data/misc/perfetto-configs` can be used for
   storing configs.
+* On devices before Android 10, adb cannot directly pull
+  `/data/misc/perfetto-traces`. Use
+  `adb shell cat /data/misc/perfetto-traces/trace > trace` to work around.
 * When capturing longer traces, e.g. in the context of benchmarks or CI, use
   `PID=$(perfetto --background)` and then `kill $PID` to stop.
 
@@ -190,5 +201,10 @@
 Pull the file using `adb pull /data/misc/perfetto-traces/trace ~/trace.perfetto-trace`
 and open it in the [Perfetto UI](https://ui.perfetto.dev).
 
+NOTE: On devices before Android 10, adb cannot directly pull
+      `/data/misc/perfetto-traces`. Use
+       `adb shell cat /data/misc/perfetto-traces/trace > trace.perfetto-trace`
+       to work around.
+
 The full reference for the `perfetto` cmdline interface can be found
 [here](/docs/reference/perfetto-cli.md).
diff --git a/docs/quickstart/linux-tracing.md b/docs/quickstart/linux-tracing.md
index 93fa823..104b02e 100644
--- a/docs/quickstart/linux-tracing.md
+++ b/docs/quickstart/linux-tracing.md
@@ -17,24 +17,42 @@
 ```bash
 tools/install-build-deps
 ```
-_If the script fails with SSL errors, try invoking it as `python3 tools/install-build-deps`, or upgrading your openssl libraries._
+_If the script fails with SSL errors, try upgrading your openssl package._
 
-3. Generate all most common GN build configurations:
+3. Generate the build configuration
 ```bash
-tools/build_all_configs.py
+tools/gn gn gen --args='is_debug=false' out/linux
+# Or use `tools/build_all_configs.py` to generate more build configs.
 ```
 
 4. Build the Linux tracing binaries (On Linux it uses a hermetic clang toolchain, downloaded as part of step 2):
 ```bash
-tools/ninja -C out/linux_clang_release traced traced_probes perfetto
+tools/ninja -C out/linux tracebox traced traced_probes perfetto 
 ```
-_This step is optional when using the convenience `tools/tmux` script below._
 
 ## Capturing a trace
 
 Due to Perfetto's [service-based architecture](/docs/concepts/service-model.md),
 in order to capture a trace, the `traced` (session daemon) and `traced_probes`
 (probes and ftrace-interop daemon) need to be running.
+As per Perfetto v16, the `tracebox` binary bundles together all the binaries you
+need in a single executable (a bit like `toybox` or `busybox`).
+
+#### Capturing a trace with ftrace and /proc pollers, no SDK
+
+If you are interested in overall system tracing and are not interested in
+testing the SDK, you can use `tracebox` in autostart mode as follows:
+
+```bash
+out/linux/tracebox -o trace_file.perfetto-trace --txt -c test/configs/scheduling.cfg
+```
+
+#### Testing the SDK integration in out-of-process tracing mode (system mode)
+
+If you are using the Perfetto [tracing SDK](/docs/instrumentation/tracing-sdk)
+and want to capture a fused trace that contains both system traces events and
+your custom app trace events, you need to start the `traced` and `traced_probes`
+services ahead of time and then use the `perfetto` cmdline client.
 
 For a quick start, the [tools/tmux](/tools/tmux) script takes care of building,
 setting up and running everything.
@@ -44,8 +62,8 @@
 [ftrace]: https://www.kernel.org/doc/Documentation/trace/ftrace.txt
 
 1. Run the convenience script with an example tracing config (10s duration):
-```
-OUT=out/linux_clang_release CONFIG=test/configs/scheduling.cfg tools/tmux -n
+```bash
+tools/tmux -c test/configs/scheduling.cfg -C out/linux -n
 ```
 This will open a tmux window with three panes, one per the binary involved in
 tracing: `traced`, `traced_probes` and the `perfetto` client cmdline.
@@ -62,9 +80,9 @@
 
 We can now explore the captured trace visually by using a dedicated web-based UI.
 
-NOTE: The UI runs fully in-browser using JavaScript + Web Assembly. The trace
+NOTE: The UI runs in-browser using JavaScript + Web Assembly. The trace
       file is **not** uploaded anywhere by default, unless you explicitly click
-      on the 'Share' link.
+      on the 'Share' link. The 'Share' link is available only to Googlers.
 
 1. Navigate to [ui.perfetto.dev](https://ui.perfetto.dev) in a browser.
 
@@ -75,5 +93,5 @@
    process tracks (rows) into their constituent thread tracks.
    Press "?" for further navigation controls.
 
-Alternatively, you can explore the trace contents issuing SQL queries through 
+Alternatively, you can explore the trace contents issuing SQL queries through
 the [trace processor](/docs/analysis/trace-processor).
diff --git a/docs/quickstart/traceconv.md b/docs/quickstart/traceconv.md
index 226e5b2..83cf975 100644
--- a/docs/quickstart/traceconv.md
+++ b/docs/quickstart/traceconv.md
@@ -38,6 +38,6 @@
 
 If you just want to open a Perfetto trace with the legacy (Catapult) trace
 viewer, you can just navigate to [ui.perfetto.dev](https://ui.perfetto.dev),
-and use the the _"Open with legacy UI"_ link. This runs `traceconv` within
+and use the _"Open with legacy UI"_ link. This runs `traceconv` within
 the browser using WebAssembly and passes the converted trace seamlessly to
 chrome://tracing.
diff --git a/docs/reference/heap_profile-cli.md b/docs/reference/heap_profile-cli.md
index 457b699..7a68ea2 100644
--- a/docs/reference/heap_profile-cli.md
+++ b/docs/reference/heap_profile-cli.md
@@ -1,4 +1,10 @@
-# heap_profile
+# HEAP_PROFILE(1)
+
+## NAME
+
+heap_profile - record heap profile on Android device
+
+## DESCRIPTION
 
 `tools/heap_profile` allows to collect native memory profiles on Android.
 See [Recording traces](/docs/data-sources/native-heap-profiler.md) for more
@@ -16,27 +22,78 @@
                     [--print-config] [-o DIRECTORY]
 ```
 
-## Options
-|Option|Description|
-|---|---|
-| -n, --name | Comma-separated list of process names to profile. |
-| -p, --pid | Comma-separated list of PIDs to profile. |
-| -i, --interval | Sampling interval. Default 4096 (4KiB) |
-| -o, --output | Output directory. |
-| -d, --duration | Duration of profile (ms). Default 7 days. |
-| --block-client | When buffer is full, block the client to wait for buffer space. Use with caution as this can significantly slow down the client. This is the default |
-| --no-block-client | When buffer is full, stop the profile early. |
-| --block-client-timeout | If --block-client is given, do not block any allocation for longer than this timeout (us). |
-| -h, --help | Show this help message and exit |
-| --no-start | Do not start heapprofd. |
-| -c, --continuous-dump | Dump interval in ms. 0 to disable continuous dump. |
-| --disable-selinux | Disable SELinux enforcement for duration of profile. |
-| --no-versions | Do not get version information about APKs. |
-| --no-running | Do not target already running processes. Requires Android 11. |
-| --no-startup | Do not target processes that start during the profile. Requires Android 11. |
-| --shmem-size | Size of buffer between client and heapprofd. Default 8MiB. Needs to be a power of two multiple of 4096, at least 8192. |
-| --dump-at-max | Dump the maximum memory usage rather than at the time of the dump. |
-| --disable-fork-teardown | Do not tear down client in forks. This can be useful for programs that use vfork. Android 11+ only. |
-| --simpleperf | Get simpleperf profile of heapprofd. This is only for heapprofd development. |
-| --trace-to-text-binary | Path to local trace to text. For debugging. |
-| --print-config | Print config instead of running. For debugging. |
+## OPTIONS
+`-n`, `--name` _NAMES_
+:    Comma-separated list of process names to profile.
+
+`-p`, `--pid` _PIDS_
+:    Comma-separated list of PIDs to profile.
+
+`-i`, `--interval`
+:    Sampling interval. Default 4096 (4KiB)
+
+`-o`, `--output` _DIRECTORY_
+:    Output directory.
+
+`--all-heaps`
+:    Collect allocations from all heaps registered by target.
+
+`--block-client`
+:    When buffer is full, block the client to wait for buffer space. Use with caution as this can significantly slow down the client. This is the default
+
+`--block-client-timeout`
+:    If --block-client is given, do not block any allocation for longer than this timeout (us).
+
+`-c`, `--continuous-dump`
+:    Dump interval in ms. 0 to disable continuous dump.
+
+`-d`, `--duration`
+:    Duration of profile (ms). 0 to run until interrupted. Default: until interrupted by user.
+
+`--disable-fork-teardown`
+:    Do not tear down client in forks. This can be useful for programs that use vfork. Android 11+ only.
+
+`--disable-selinux`
+:    Disable SELinux enforcement for duration of profile.
+
+`--dump-at-max`
+:    Dump the maximum memory usage rather than at the time of the dump.
+
+`-h`, `--help`
+:    show this help message and exit
+
+`--heaps` _HEAPS_
+:    Comma-separated list of heaps to collect, e.g: malloc,art. Requires Android 12.
+
+`--idle-allocations`
+:    Keep track of how many bytes were unused since the last dump, per callstack
+
+`--no-android-tree-symbolization`
+:    Do not symbolize using currently lunched target in the Android tree.
+
+`--no-block-client`
+:    When buffer is full, stop the profile early.
+
+`--no-running`
+:    Do not target already running processes. Requires Android 11.
+
+`--no-start`
+:    Do not start heapprofd.
+
+`--no-startup`
+:    Do not target processes that start during the profile. Requires Android 11.
+
+`--no-versions`
+:    Do not get version information about APKs.
+
+`--print-config`
+:    Print config instead of running. For debugging.
+
+`--shmem-size`
+:    Size of buffer between client and heapprofd. Default 8MiB. Needs to be a power of two multiple of 4096, at least 8192.
+
+`--simpleperf`
+:    Get simpleperf profile of heapprofd. This is only for heapprofd development.
+
+`--trace-to-text-binary`
+:    Path to local trace to text. For debugging.
diff --git a/docs/reference/perfetto-cli.md b/docs/reference/perfetto-cli.md
index f0771d6..8de0f31 100644
--- a/docs/reference/perfetto-cli.md
+++ b/docs/reference/perfetto-cli.md
@@ -1,4 +1,10 @@
-# Perfetto CLI
+# PERFETTO(1)
+
+## NAME
+
+perfetto - capture traces
+
+## DESCRIPTION
 
 This section describes how to use the `perfetto` commandline binary to capture
 traces. Examples are given in terms of an Android device connected over ADB.
@@ -6,70 +12,117 @@
 `perfetto` has two modes for configuring the tracing session (i.e. what and how
 to collect):
 
-* __lightweight mode__: all config options are supplied as commandline flags,
+__lightweight mode__
+: all config options are supplied as commandline flags,
   but the available data sources are restricted to ftrace and atrace. This mode
   is similar to
   [`systrace`](https://developer.android.com/topic/performance/tracing/command-line).
-* __normal mode__: the configuration is specified in a protocol buffer. This
-  allows for full customisation of collected traces.
+
+__normal mode__
+: the configuration is specified in a protocol buffer. This allows for full
+  customisation of collected traces.
 
 
-## General options
+## GENERAL OPTIONS
 
 The following table lists the available options when using `perfetto` in either
 mode.
 
-|Option|Description|
-|---|---|
-| `--background \| -d` |Perfetto immediately exits the command-line interface and continues recording your trace in background.|
-|`--out OUT_FILE \| -o OUT_FILE`|Specifies the desired path to the output trace file, or `-` for stdout. `perfetto` writes the output to the file described in the flags above. The output format compiles with the format defined in [AOSP `trace.proto`](/protos/perfetto/trace/trace.proto).|
-|`--dropbox TAG`|Uploads your trace via the [DropBoxManager API](https://developer.android.com/reference/android/os/DropBoxManager.html) using the tag you specify.|
-|`--no-guardrails`|Disables protections against excessive resource usage when enabling the `--dropbox` flag during testing.|
-|`--reset-guardrails`|Resets the persistent state of the guardrails and exits (for testing).|
-|`--query`|Queries the service state and prints it as human-readable text.|
-|`--query-raw`|Similar to `--query`, but prints raw proto-encoded bytes of `tracing_service_state.proto`.|
-|`--help \| -h`|Prints out help text for the `perfetto` tool.|
+`-d`, `--background`
+:    Perfetto immediately exits the command-line interface and continues
+     recording your trace in background.
+
+`-o`, `--out` _OUT_FILE_
+:    Specifies the desired path to the output trace file, or `-` for stdout.
+     `perfetto` writes the output to the file described in the flags above.
+     The output format compiles with the format defined in
+     [AOSP `trace.proto`](/protos/perfetto/trace/trace.proto).
+
+`--dropbox` _TAG_
+:    Uploads your trace via the
+     [DropBoxManager API](https://developer.android.com/reference/android/os/DropBoxManager.html)
+     using the tag you specify. Android only.
+
+`--no-guardrails`
+:     Disables protections against excessive resource usage when enabling the
+      `--dropbox` flag during testing.
 
 
-## Lightweight mode
+`--reset-guardrails`
+:     Resets the persistent state of the guardrails and exits (for testing).
+
+`--query`
+:     Queries the service state and prints it as human-readable text.
+
+`--query-raw`
+:     Similar to `--query`, but prints raw proto-encoded bytes of
+      `tracing_service_state.proto`.
+
+`-h`,  `--help`
+:     Prints out help text for the `perfetto` tool.
+
+
+## LIGHTWEIGHT MODE
 
 The general syntax for using `perfetto` in *lightweight mode* is as follows:
 
-<pre class="none">
- adb shell perfetto [ --time <var>TIMESPEC</var> ] [ --buffer <var>SIZE</var> ] [ --size <var>SIZE</var> ]
-           [ <var>ATRACE_CAT</var> | <var>FTRACE_GROUP/FTRACE_NAME</var>]...
-</pre>
+```
+ adb shell perfetto [ --time TIMESPEC ] [ --buffer SIZE ] [ --size SIZE ]
+    [ ATRACE_CAT | FTRACE_GROUP/FTRACE_NAME]...
+```
 
 
 The following table lists the available options when using `perfetto` in
 *lightweight mode*.
 
-|Option|Description|
-|--- |--- |
-|`--time TIME[s\|m\|h] \| -t TIME[s\|m\|h]`|Specifies the trace duration in seconds, minutes, or hours. For example, `--time 1m` specifies a trace duration of 1 minute. The default duration is 10 seconds.|
-|`--buffer SIZE[mb\|gb] \| -b SIZE[mb\|gb`]|Specifies the ring buffer size in megabytes (mb) or gigabytes (gb). The default parameter is `--buffer 32mb`.|
-|`--size SIZE[mb\|gb] \| -s SIZE[mb\|gb]`|Specifies the max file size in megabytes (mb) or gigabytes (gb). By default `perfetto` uses only in-memory ring-buffer.|
+`-t`, `--time` _TIME[s|m|h]_
+:    Specifies the trace duration in seconds, minutes, or hours.
+     For example, `--time 1m` specifies a trace duration of 1 minute.
+     The default duration is 10 seconds.
+
+`-b`, `--buffer` _SIZE[mb|gb]_
+:    Specifies the ring buffer size in megabytes (mb) or gigabytes (gb).
+     The default parameter is `--buffer 32mb`.
+
+`-s`, `--size` _SIZE[mb|gb]_
+:    Specifies the max file size in megabytes (mb) or gigabytes (gb).
+     By default `perfetto` uses only in-memory ring-buffer.
 
 
 This is followed by a list of event specifiers:
 
-|Event|Description|
-|--- |--- |
-|`ATRACE_CAT`|Specifies the atrace categories you want to record a trace for. For example, the following command traces Window Manager using atrace: `adb shell perfetto --out FILE wm`. To record other categories, see this [list of atrace categories](https://android.googlesource.com/platform/frameworks/native/+/refs/tags/android-q-preview-5/cmds/atrace/atrace.cpp#100).|
-|`FTRACE_GROUP/FTRACE_NAME`|Specifies the ftrace events you want to record a trace for. For example, the following command traces sched/sched_switch events: `adb shell perfetto --out FILE sched/sched_switch`|
+`ATRACE_CAT`
+:    Specifies the atrace categories you want to record a trace for.
+     For example, the following command traces Window Manager using atrace:
+     `adb shell perfetto --out FILE wm`. To record other categories, see this
+     [list of atrace categories](https://android.googlesource.com/platform/frameworks/native/+/refs/tags/android-q-preview-5/cmds/atrace/atrace.cpp#100).
+
+`FTRACE_GROUP/FTRACE_NAME`
+:    Specifies the ftrace events you want to record a trace for.
+     For example, the following command traces sched/sched_switch events:
+     `adb shell perfetto --out FILE sched/sched_switch`
 
 
-## Normal mode
+## NORMAL MODE
 
 The general syntax for using `perfetto` in *normal mode* is as follows:
 
-<pre class="none">
- adb shell perfetto [ --txt ] --config <var>CONFIG_FILE</var>
-</pre>
+```
+ adb shell perfetto [ --txt ] --config CONFIG_FILE
+```
 
-The following table lists the available options when using `perfetto` in *normal* mode.
+The following table lists the available options when using `perfetto` in
+*normal* mode.
 
-|Option|Description|
-|--- |--- |
-|`--config CONFIG_FILE \| -c CONFIG_FILE`|Specifies the path to a configuration file. In normal mode, some configurations may be encoded in a configuration protocol buffer. This file must comply with the protocol buffer schema defined in [AOSP `trace_config.proto`](/protos/perfetto/config/data_source_config.proto). You select and configure the data sources using the DataSourceConfig member of the TraceConfig, as defined in [AOSP `data_source_config.proto`](/protos/perfetto/config/data_source_config.proto).|
-|`--txt`|Instructs `perfetto` to parse the config file as pbtxt. This flag is experimental, and it's not recommended that you enable it for production.|
+`-c`, `--config` _CONFIG_FILE_
+:    Specifies the path to a configuration file. In normal mode, some
+     configurations may be encoded in a configuration protocol buffer.
+     This file must comply with the protocol buffer schema defined in AOSP
+     [`trace_config.proto`](/protos/perfetto/config/data_source_config.proto).
+     You select and configure the data sources using the DataSourceConfig member
+     of the TraceConfig, as defined in AOSP
+     [`data_source_config.proto`](/protos/perfetto/config/data_source_config.proto).
+
+`--txt`
+:    Instructs `perfetto` to parse the config file as pbtxt. This flag is
+     experimental, and it's not recommended that you enable it for production.
diff --git a/docs/toc.md b/docs/toc.md
index ac1500b..b9f8b9c 100644
--- a/docs/toc.md
+++ b/docs/toc.md
@@ -24,6 +24,7 @@
   * [Android system](#)
     * [Atrace instrumentation](data-sources/atrace.md)
     * [Android log (logcat)](data-sources/android-log.md)
+    * [Android Janks](data-sources/frametimeline.md)
 
 * [App Instrumentation](#)
   * [Tracing SDK](instrumentation/tracing-sdk.md)
@@ -38,6 +39,9 @@
 
 * [Trace visualization](#)
   * [Perfetto UI](visualization/perfetto-ui.md)
+  * [Visualising large traces](visualization/large-traces.md)
+  * [Deep linking to Perfetto UI](visualization/deep-linking-to-perfetto-ui.md)
+  * [Perfetto UI release process](visualization/perfetto-ui-release-process.md)
 
 * [Core concepts](#)
   * [Trace configuration](concepts/config.md)
@@ -65,7 +69,9 @@
     * [API and ABI surface](design-docs/api-and-abi.md)
     * [Heapprofd design](design-docs/heapprofd-design.md)
     * [Heapprofd wire protocol](design-docs/heapprofd-wire-protocol.md)
+    * [Heapprofd sampling](design-docs/heapprofd-sampling.md)
     * [Life of a tracing session](design-docs/life-of-a-tracing-session.md)
     * [Perfetto CI](design-docs/continuous-integration.md)
     * [ProtoZero](design-docs/protozero.md)
     * [Security model](design-docs/security-model.md)
+    * [Statsd Checkpoint Atoms](design-docs/checkpoint-atoms.md)
diff --git a/docs/visualization/deep-linking-to-perfetto-ui.md b/docs/visualization/deep-linking-to-perfetto-ui.md
new file mode 100644
index 0000000..f6a9225
--- /dev/null
+++ b/docs/visualization/deep-linking-to-perfetto-ui.md
@@ -0,0 +1,142 @@
+# Deep linking to the Perfetto UI
+
+This document describes how to open traces hosted on external servers with the
+Perfetto UI. This can help integrating the Perfetto UI with custom dashboards
+and implement _'Open with Perfetto UI'_-like features.
+
+## Using window.open and postMessage
+
+The supported way of doing this is to _inject_ the trace as an ArrayBuffer
+via `window.open('https://ui.perfetto.dev')` and `postMessage()`.
+In order to do this you need some minimal JavaScript code running on some
+hosting infrastructure you control which can access the trace file. In most
+cases this is some dashboard which you want to deep-link to the Perfetto UI.
+
+#### Open ui.perfetto.dev via window.open
+
+The source dashboard, the one that knows how to locate a trace and deal with
+ACL checking / oauth authentication and the like, creates a new tab by doing
+
+```js
+var handle = window.open('https://ui.perfetto.dev');
+```
+
+The window handle allows bidirectional communication using `postMessage()`
+between the source dashboard and the Perfetto UI.
+
+#### Wait for the UI to be ready via PING/PONG
+
+Wait for the UI to be ready. The `window.open()` message channel is not
+buffered. If you send a message before the opened page has registered an
+`onmessage` listener the messagge will be dropped on the floor.
+In order to avoid this race, you can use a very basic PING/PONG protocol: keep
+sending a 'PING' message until the opened window replies with a 'PONG'.
+When this happens, that is the signal that the Perfetto UI is ready to open
+traces.
+
+#### Post a message the following JavaScript object
+
+```js
+  {
+    'perfetto': {
+      buffer: ArrayBuffer;
+      title: string;
+      fileName?: string;  // Optional
+      url?: string;       // Optional
+    }
+  }
+```
+
+`buffer` is the ArrayBuffer with the actual trace file content. This is
+typically something that you obtain by doing a `fetch()` on your backend
+storage.
+
+`title` is the human friendly trace title that will be shown in the
+sidebar. This can help people to disambiguate traces from several tabs.
+
+`fileName` will be used if the user clicks on "Download". A generic name will
+be used if omitted.
+
+`url` is used if the user clicks on the "Share" link in the sidebar. This should
+print to a URL owned by you that would cause your dashboard to re-open the
+current trace, by re-kicking-off the window.open() process herein described.
+If omitted traces won't be shareable.
+
+### Code samples
+
+See https://jsfiddle.net/primiano/1hd0a4wj/68/ (also mirrored on
+[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)
+
+### Common pitfalls
+
+Many browsers sometimes block window.open() requests prompting the user to allow
+popups for the site. This usually happens if:
+
+- The window.open() is NOT initiated by a user gesture.
+- Too much time is passed from the user gesture to the window.open()
+
+If the trace file is big enough, the fetch() might take long time and pass the
+user gesture threshold. This can be detected by observing that the window.open()
+returned `null`. When this happens the best option is to show another clickable
+element and bind the fetched trace ArrayBuffer to the new onclick handler, like
+the code in the example above does.
+
+Some browser can have a variable time threshold for the user gesture timeout
+which depends on the website engagement score (how much the user has visited
+the page that does the window.open() before). It's quite common when testing
+this code to see a popup blocker the first time the new feature is used and
+then not see it again.
+
+### Where does the posted trace go?
+
+The Perfetto UI is client-only and doesn't require any server-side interaction.
+Traces pushed via postMessage() are kept only in the browser memory/cache and
+are not sent to any server.
+
+## Why can't I just pass a URL?
+
+_"Why you don't let me just pass a URL to the Perfetto UI (e.g. ui.perfetto.dev?url=...) and you deal with all this?"_
+
+The answer to this is manifold and boils down to security.
+
+#### Cross origin requests blocking
+
+If ui.perfetto.dev had to do a `fetch('https://yourwebsite.com/trace')` that
+would be a cross-origin request. Browsers disallow by default cross-origin
+fetch requests.
+In order for this to work, the web server that hosts yourwebsite.com would have
+to expose a custom HTTP response header
+ (`Access-Control-Allow-Origin: https://ui.perfetto.dev`) to allow the fetch.
+In most cases customizing the HTTP response headers is outside of dashboard's
+owners control.
+
+You can learn more about CORS at
+https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
+
+#### Content Security Policy
+
+Perfetto UI uses a strict Content Security Policy which disallows foreign
+fetches and subresources, as a security mitigation about common attacks.
+Even assuming that CORS headers are properly set and your trace files are
+publicly accessible, fetching the trace from the Perfetto UI would require
+allow-listing your origin in our CSP policy. This is not scalable.
+
+You can learn more about CSP at
+https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
+
+#### Dealing with OAuth2 or other authentication mechanisms
+
+Even ignoring CORS, the Perfetto UI would have to deal with OAuth2 or other
+authentication mechanisms to fetch the trace file. Even if all the dashboards
+out there used OAuth2, that would still mean that Perfetto UI would have to know
+about all the possible OAuth2 scopes, one for each dashboard. This is not
+scalable.
+
+## Source links
+
+The source code that deals with the postMessage() in the Perfetto UI is
+[`post_message_handler.ts`](/ui/src/frontend/post_message_handler.ts)
diff --git a/docs/visualization/large-traces.md b/docs/visualization/large-traces.md
new file mode 100644
index 0000000..f4e40d5
--- /dev/null
+++ b/docs/visualization/large-traces.md
@@ -0,0 +1,27 @@
+# Visualising large traces
+
+Browsers often limit the amount of memory a site can use.
+This can cause problems when visualising large traces.
+
+## How to visualise large traces using the Perfetto UI
+
+Perfetto UI has support for a mode where the processing of the trace
+is offloaded to a 'server' instance of `trace_processor` running natively on your local machine.
+This server process can take full advantage of the RAM of your machine as well as running at full native (rather than WASM) performance.
+
+```
+curl -LO https://get.perfetto.dev/trace_processor
+chmod +x ./trace_processor
+trace_processor --httpd /path/to/trace.pftrace
+# Navigate to http://ui.perfetto.dev, it will prompt to use the HTTP+RPC interface
+```
+
+## How big is too big?
+
+The exact memory limit can vary by browser, architecture, and OS however 2gb is typical.
+This limit is a limit on the total memory used at runtime, not on the binary size of the trace.
+The `trace_processor` (and hence the UI) representation of a trace at runtime is normally larger than the binary size of that trace.
+This is because the representation is optimized for query performance rather than size.
+The exact inflation factor varies depending on the trace format but can be 2-4x for uncompressed proto traces.
+
+
diff --git a/docs/visualization/perfetto-ui-release-process.md b/docs/visualization/perfetto-ui-release-process.md
new file mode 100644
index 0000000..0f06439
--- /dev/null
+++ b/docs/visualization/perfetto-ui-release-process.md
@@ -0,0 +1,104 @@
+# Perfetto UI Release Process
+
+The UI has three release channels which are configured by the
+[channels.json](/ui/release/channels.json) file. The channels are:
+
+- `stable`, the version served by default on ui.perfetto.dev.
+  Updated every four weeks.
+- `canary`, a less stable but fresher release. Updated every 1-2 weeks.
+- `autopush`, the current HEAD version of the UI. Unstable.
+
+The release process is based around a four week cycle.
+
+- Week 1: Update `canary` to `HEAD`.
+- Week 2: Update `canary` to `HEAD`.
+  Canary stabilization week 1/2 starts here.
+  Only critical bug fixes can be cherry-picked onto `canary`.
+- Week 3: Canary stabilization week 2/2.
+- Week 4: Update `stable` to current `canary`, update `canary` to `HEAD`.
+
+After the fourth week the cycle repeats from week one.
+This is so that:
+
+- Canary soaks for two weeks before being promoted to stable.
+- Newer features can be tried out in Canary within a week, or two at most (if
+  in the stabilization weeks).
+- Stable users aren't disrupted more than once per month.
+
+## Changing release channel
+
+NOTE: The channel setting is persistent across page reloads.
+
+The channel the UI is currently using is displayed in the top left corner.
+If the tag after the logo shows `autopush` or `canary` that is the current channel
+and if no tag is displayed the current channel is `stable`.
+
+![perfetto-ui-channel.png](/docs/images/perfetto-ui-channel.png)
+
+To change the channel the UI is using between `stable` and `canery` you can use the toggle on the [entrance page](https://ui.perfetto.dev).
+
+![perfetto-ui-channel-toggle.png](/docs/images/perfetto-ui-channel-toggle.png)
+
+To change to the `autopush` channel open devtools and enter `localStorage.setItem('perfettoUiChannel', 'autopush');` then reload.
+
+## Which version am I using?
+
+You can see the version of the UI you are currently using in the bottom left hand corner of the UI.
+
+![perfetto-ui-version.png](/docs/images/perfetto-ui-version.png)
+
+Clicking on the version number takes you to Github where you can see which commits are part of this version. The version number format is `v<maj>.<min>.<Commits since that version>` where `<maj>.<min>` are extracted from the top entry in the
+[CHANGELOG](/CHANGELOG).
+
+## Cherry-picking a change
+
+If a change needs to be backported onto canary or stable branches, do the
+following:
+
+```bash
+git fetch origin
+git co -b ui-canary -t origin/ui-canary
+git cherry-pick -x $SHA1_OF_ORIGINAL_CL
+git cl upload
+
+# Repeat for origin/ui-stable branch if needed.
+```
+
+Once the cherry-picks are landed, send out a CL to update the
+[channels.json](/ui/release/channels.json) in the `master` branch. See
+[r.android.com/1726101](https://r.android.com/1726101) for an example.
+
+```json
+{
+  "channels": [
+    {
+      "name": "stable",
+      "rev": "6dd6756ffbdff4f845c4db28e1fd5aed9ba77b56"
+      //     ^ This should point to the HEAD of origin/ui-stable.
+    },
+    {
+      "name": "canary",
+      "rev": "3e21f613f20779c04b0bcc937f2605b9b05556ad"
+      //     ^ This should point to the HEAD of origin/ui-canary.
+    },
+    {
+      "name": "autopush",
+      "rev": "HEAD"
+      //     ^ Don't touch this one.
+    }
+  ]
+}
+```
+
+The state of `channels.json` in the other branches is irrelevant, the release
+infrastructure only looks at the `master` branch to determine the pinning of
+each channel.
+
+After the `channels.json` CL lands, the build infrastructure will pick it up
+and update ui.perfetto.dev within ~30 mins.
+
+Googlers: You can check build progress and logs on
+[go/perfetto-ui-build-status](http://go/perfetto-ui-build-status). See also
+[go/perfetto-ui-autopush](http://go/perfetto-ui-autopush) and
+[go/perfetto-ui-channels](http://go/perfetto-ui-channels) for the design docs of
+the serving infrastructure.
diff --git a/gn/BUILD.gn b/gn/BUILD.gn
index 6793fce..15fa31b 100644
--- a/gn/BUILD.gn
+++ b/gn/BUILD.gn
@@ -79,6 +79,9 @@
     "PERFETTO_TP_JSON=$enable_perfetto_trace_processor_json",
     "PERFETTO_LOCAL_SYMBOLIZER=$perfetto_local_symbolizer",
     "PERFETTO_ZLIB=$enable_perfetto_zlib",
+    "PERFETTO_TRACED_PERF=$enable_perfetto_traced_perf",
+    "PERFETTO_HEAPPROFD=$enable_perfetto_heapprofd",
+    "PERFETTO_STDERR_CRASH_DUMP=$enable_perfetto_stderr_crash_dump",
   ]
 
   rel_out_path = rebase_path(gen_header_path, "$root_build_dir")
@@ -195,7 +198,9 @@
 protobuf_full_deps_allowlist = [
   "../src/ipc/protoc_plugin:*",
   "../src/protozero/protoc_plugin:*",
+  "../src/protozero/filtering:filter_util",
   "../src/trace_processor:trace_processor_shell",
+  "../src/protozero/filtering:filter_util",
   "../tools/*",
 ]
 
@@ -363,10 +368,16 @@
   }
 }
 
+config("system_zlib_config") {
+  libs = [ "z" ]
+}
+
 # Zlib is used both by trace_processor and by perfetto_cmd.
 if (enable_perfetto_zlib) {
   group("zlib") {
-    if (perfetto_root_path == "//") {
+    if (perfetto_use_system_zlib) {
+      public_configs = [ "//gn:system_zlib_config" ]
+    } else if (perfetto_root_path == "//") {
       public_configs = [ "//buildtools:zlib_config" ]
       public_deps = [ "//buildtools:zlib" ]
     } else {
diff --git a/gn/gen_perfetto_version_header.gni b/gn/gen_perfetto_version_header.gni
index 29e02e9..17c879f 100644
--- a/gn/gen_perfetto_version_header.gni
+++ b/gn/gen_perfetto_version_header.gni
@@ -28,7 +28,8 @@
     inputs = [ changelog ]
     outputs = []
     args = []
-    if (perfetto_build_standalone && !is_perfetto_build_generator) {
+    if (perfetto_build_standalone && !is_perfetto_build_generator &&
+        perfetto_enable_git_rev_version_header) {
       inputs += [ "${perfetto_root_path}.git/HEAD" ]
     }
 
@@ -48,5 +49,8 @@
       ]
       outputs += [ invoker.ts_out ]
     }
+    if (!perfetto_enable_git_rev_version_header) {
+      args += [ "--no_git" ]
+    }
   }
 }
diff --git a/gn/perfetto.gni b/gn/perfetto.gni
index 13831a045..ebed8f3 100644
--- a/gn/perfetto.gni
+++ b/gn/perfetto.gni
@@ -144,9 +144,12 @@
   # system backend in the client library.
   # This includes building things that rely on POSIX sockets, this places
   # limitations on the supported operating systems.
-  enable_perfetto_ipc = !is_win && !is_fuchsia && !is_nacl &&
-                        (perfetto_build_standalone ||
-                         perfetto_build_with_android || build_with_chromium)
+  # For now the IPC layer is conservatively not enabled on Chromium+Windows
+  # builds.
+  enable_perfetto_ipc =
+      !is_fuchsia && !is_nacl &&
+      (perfetto_build_standalone || perfetto_build_with_android ||
+       (build_with_chromium && !is_win))
 
   # Makes the heap profiling daemon target reachable. It works only on Android,
   # but is built on Linux as well for test/compiler coverage.
@@ -186,7 +189,7 @@
                               build_with_chromium || perfetto_build_with_android
 
   enable_perfetto_integration_tests =
-      (perfetto_build_standalone && !is_win) || perfetto_build_with_android
+      perfetto_build_standalone || perfetto_build_with_android
 
   enable_perfetto_benchmarks = perfetto_build_standalone && !is_win
 
@@ -222,6 +225,8 @@
 }
 
 declare_args() {
+  perfetto_enable_git_rev_version_header = enable_perfetto_version_gen
+
   # The traced_probes daemon is very Linux-specific, as it depends on ftrace and
   # various /proc interfaces. There is no point making its code platform-neutral
   # as it won't do anything useful on Windows.
@@ -257,7 +262,8 @@
   # Enables httpd RPC support in the trace processor.
   # Further per-OS conditionals are applied in gn/BUILD.gn.
   enable_perfetto_trace_processor_httpd =
-      enable_perfetto_trace_processor && perfetto_build_standalone
+      enable_perfetto_trace_processor &&
+      (perfetto_build_standalone || perfetto_build_with_android)
 
   # Enables Zlib support. This is used both by the "perfetto" cmdline client
   # (for compressing traces) and by trace processor (for compressed traces).
@@ -281,6 +287,8 @@
   # Used by CrOS system builds. Uses the system version of protobuf
   # from /usr/include instead of the hermetic one.
   perfetto_use_system_protobuf = false
+
+  perfetto_use_system_zlib = false
 }
 
 if (is_win) {
@@ -310,9 +318,6 @@
 # |is_perfetto_build_generator| must be true.
 assert(!perfetto_build_with_android || is_perfetto_build_generator)
 
-# The IPC layer based on UNIX sockets can't be built on Win.
-assert(!enable_perfetto_ipc || !is_win)
-
 # We should never end up in a state where is_perfetto_embedder=true but
 # perfetto_build_with_embedder=false.
 assert(!is_perfetto_embedder || perfetto_build_with_embedder)
diff --git a/gn/perfetto_benchmarks.gni b/gn/perfetto_benchmarks.gni
index 2bde376..e154064 100644
--- a/gn/perfetto_benchmarks.gni
+++ b/gn/perfetto_benchmarks.gni
@@ -18,6 +18,7 @@
   "gn:default_deps",
   "src/base:benchmarks",
   "src/protozero:benchmarks",
+  "src/protozero/filtering:benchmarks",
   "src/trace_processor/sqlite:benchmarks",
   "src/trace_processor/containers:benchmarks",
   "src/trace_processor/tables:benchmarks",
diff --git a/gn/perfetto_fuzzers.gni b/gn/perfetto_fuzzers.gni
index 0a5dbfa..5874f8e 100644
--- a/gn/perfetto_fuzzers.gni
+++ b/gn/perfetto_fuzzers.gni
@@ -18,6 +18,8 @@
   "gn:default_deps",
   "src/ipc:buffered_frame_deserializer_fuzzer",
   "src/protozero:protozero_decoder_fuzzer",
+  "src/protozero/filtering:protozero_bytecode_parser_fuzzer",
+  "src/protozero/filtering:protozero_message_filter_fuzzer",
   "src/tracing/core:packet_stream_validator_fuzzer",
   "src/trace_processor:trace_processor_fuzzer",
   "src/traced/probes/ftrace:cpu_reader_fuzzer",
diff --git a/gn/proto_library.gni b/gn/proto_library.gni
index 03d6455..5b87753 100644
--- a/gn/proto_library.gni
+++ b/gn/proto_library.gni
@@ -173,6 +173,17 @@
     } else {
       deps += [ "$perfetto_root_path/src/ipc:common" ]
     }
+    if (is_win) {
+      # TODO(primiano): investigate this. In Windows standalone builds, some
+      # executable targets end up in a state where no code pulls a dep on the
+      # ipc:client (in turn that seems a subtle consequence of not having
+      # traced_probes on Windows). This dep here is effectively needed because
+      # the client-side code in the generated .ipc.cc effectively depends on the
+      # client-side IPC library. Perhaps we just should do this unconditionally
+      # on all platforms?
+      deps += [ "$perfetto_root_path/src/ipc:client" ]
+    }
+
     if (defined(invoker.deps)) {
       deps += invoker.deps
     }
diff --git a/gn/standalone/BUILD.gn b/gn/standalone/BUILD.gn
index af70070..ffe5725 100644
--- a/gn/standalone/BUILD.gn
+++ b/gn/standalone/BUILD.gn
@@ -161,6 +161,8 @@
     cflags += [
       "/bigobj",  # Some of our files are bigger than the regular limits.
       "/Gy",  # Enable function-level linking.
+      "/FS",  # Preserve previous PDB behavior.
+      "/utf-8",  # Assume UTF-8 by default to avoid code page dependencies.
     ]
     defines += [
       "_CRT_NONSTDC_NO_WARNINGS",
@@ -207,10 +209,10 @@
     ldflags += [ "-flto=full" ]
   }
 
-  # We support only x64 builds on Windows.
-  assert(!is_win || current_cpu == "x64")
-
-  if (current_cpu == "arm") {
+  if (is_win) {
+    # We support only x86/x64 builds on Windows.
+    assert(current_cpu == "x64" || current_cpu == "x86")
+  } else if (current_cpu == "arm") {
     cflags += [
       "-march=armv7-a",
       "-mfpu=neon",
diff --git a/gn/standalone/BUILDCONFIG.gn b/gn/standalone/BUILDCONFIG.gn
index e080700..6f32686 100644
--- a/gn/standalone/BUILDCONFIG.gn
+++ b/gn/standalone/BUILDCONFIG.gn
@@ -55,9 +55,13 @@
   current_cpu = target_cpu
 }
 
-is_cross_compiling =
-    target_cpu != host_cpu || target_os != host_os || target_triplet != ""
-
+declare_args() {
+  # the ossfuzz sanitizer overrides this to true. In that config the
+  # host/target cpu and arch are identical, but we want to build only the
+  # targets with the sanitizer/fuzzer flags
+  is_cross_compiling =
+      target_cpu != host_cpu || target_os != host_os || target_triplet != ""
+}
 default_configs = [
   "//gn/standalone:debug_symbols",
   "//gn/standalone:default",
diff --git a/gn/standalone/libc++/BUILD.gn b/gn/standalone/libc++/BUILD.gn
index 809a448..f6b6131 100644
--- a/gn/standalone/libc++/BUILD.gn
+++ b/gn/standalone/libc++/BUILD.gn
@@ -20,6 +20,13 @@
       "_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS",
       "_LIBCXXABI_DISABLE_VISIBILITY_ANNOTATIONS",
     ]
+    if (is_debug) {
+      # libc++ has two levels of debug mode. Setting _LIBCPP_DEBUG to zero
+      # enables most assertions. Setting it to one additionally enables iterator
+      # debugging, but that seems to require some extra link-time dependencies.
+      # See https://libcxx.llvm.org/docs/DesignDocs/DebugMode.html
+      defines += [ "_LIBCPP_DEBUG=0" ]
+    }
     cflags_cc = [
       "-nostdinc++",
       "-isystem" + rebase_path("$libcxx_prefix/include", root_build_dir),
@@ -28,9 +35,7 @@
 
     # Avoid linking both libc++ and libstdc++.
     ldflags = [ "-nostdlib++" ]
-    libs = [
-      "dl",  # libdl: dynamic linking.
-    ]
+    libs = [ "dl" ]  # libdl: dynamic linking.
   }
 }
 
diff --git a/gn/standalone/sanitizers/BUILD.gn b/gn/standalone/sanitizers/BUILD.gn
index d3374de..574dcfb 100644
--- a/gn/standalone/sanitizers/BUILD.gn
+++ b/gn/standalone/sanitizers/BUILD.gn
@@ -33,61 +33,60 @@
 }
 
 config("sanitizers_cflags") {
-  cflags = []
-  defines = []
   if (using_sanitizer) {
-    cflags += [ "-fno-omit-frame-pointer" ]
-  }
+    cflags = [ "-fno-omit-frame-pointer" ]
+    defines = []
 
-  if (is_asan) {
-    cflags += [ "-fsanitize=address" ]
-    defines += [ "ADDRESS_SANITIZER" ]
-  }
-  if (is_lsan) {
-    cflags += [ "-fsanitize=leak" ]
-    defines += [ "LEAK_SANITIZER" ]
-  }
-  if (is_tsan) {
-    cflags += [ "-fsanitize=thread" ]
-    defines += [
-      "THREAD_SANITIZER",
-      "DYNAMIC_ANNOTATIONS_EXTERNAL_IMPL=1",
-    ]
-  }
-  if (is_msan) {
-    cflags += [
-      "-fsanitize=memory",
-      "-fsanitize-memory-track-origins=2",
-    ]
-    defines += [ "MEMORY_SANITIZER" ]
-  }
-  if (is_ubsan) {
-    cflags += [
-      "-fsanitize=bounds",
-      "-fsanitize=float-divide-by-zero",
-      "-fsanitize=integer-divide-by-zero",
-      "-fsanitize=null",
-      "-fsanitize=object-size",
-      "-fsanitize=return",
-      "-fsanitize=returns-nonnull-attribute",
-      "-fsanitize=shift-exponent",
-      "-fsanitize=signed-integer-overflow",
-      "-fsanitize=unreachable",
-      "-fsanitize=vla-bound",
-    ]
-    defines += [ "UNDEFINED_SANITIZER" ]
-  }
-  if (is_fuzzer) {
-    # FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION is also defined by oss-fuzz,
-    # so using the same name.
-    defines += [ "FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION" ]
-    cflags += [ "-fsanitize=fuzzer-no-link" ]
     if (is_asan) {
-      cflags += [
-        "-mllvm",
-        "-asan-use-private-alias",
+      cflags += [ "-fsanitize=address" ]
+      defines += [ "ADDRESS_SANITIZER" ]
+    }
+    if (is_lsan) {
+      cflags += [ "-fsanitize=leak" ]
+      defines += [ "LEAK_SANITIZER" ]
+    }
+    if (is_tsan) {
+      cflags += [ "-fsanitize=thread" ]
+      defines += [
+        "THREAD_SANITIZER",
+        "DYNAMIC_ANNOTATIONS_EXTERNAL_IMPL=1",
       ]
     }
+    if (is_msan) {
+      cflags += [
+        "-fsanitize=memory",
+        "-fsanitize-memory-track-origins=2",
+      ]
+      defines += [ "MEMORY_SANITIZER" ]
+    }
+    if (is_ubsan) {
+      cflags += [
+        "-fsanitize=bounds",
+        "-fsanitize=float-divide-by-zero",
+        "-fsanitize=integer-divide-by-zero",
+        "-fsanitize=null",
+        "-fsanitize=object-size",
+        "-fsanitize=return",
+        "-fsanitize=returns-nonnull-attribute",
+        "-fsanitize=shift-exponent",
+        "-fsanitize=signed-integer-overflow",
+        "-fsanitize=unreachable",
+        "-fsanitize=vla-bound",
+      ]
+      defines += [ "UNDEFINED_SANITIZER" ]
+    }
+    if (is_fuzzer) {
+      # FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION is also defined by oss-fuzz,
+      # so using the same name.
+      defines += [ "FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION" ]
+      cflags += [ "-fsanitize=fuzzer-no-link" ]
+      if (is_asan) {
+        cflags += [
+          "-mllvm",
+          "-asan-use-private-alias",
+        ]
+      }
+    }
   }
 }
 
@@ -98,25 +97,27 @@
 }
 
 config("sanitizers_ldflags") {
-  visibility = [ ":deps" ]
-  ldflags = []
-  if (is_asan) {
-    ldflags += [ "-fsanitize=address" ]
+  if (using_sanitizer) {
+    visibility = [ ":deps" ]
+    ldflags = []
+    if (is_asan) {
+      ldflags += [ "-fsanitize=address" ]
+    }
+    if (is_lsan) {
+      # This is not a copy/paste mistake. The LSan runtime library has
+      # moved into asan. So in order to make LSan work one has to build
+      # .cc files with -fsanitize=leak but link with -fsanitize=address.
+      ldflags += [ "-fsanitize=address" ]
+    }
+    if (is_tsan) {
+      ldflags += [ "-fsanitize=thread" ]
+    }
+    if (is_msan) {
+      ldflags += [ "-fsanitize=memory" ]
+    }
+    if (is_ubsan) {
+      ldflags += [ "-fsanitize=undefined" ]
+    }
+    configs = [ ":sanitizer_options_link_helper" ]
   }
-  if (is_lsan) {
-    # This is not a copy/paste mistake. The LSan runtime library has
-    # moved into asan. So in order to make LSan work one has to build
-    # .cc files with -fsanitize=leak but link with -fsanitize=address.
-    ldflags += [ "-fsanitize=address" ]
-  }
-  if (is_tsan) {
-    ldflags += [ "-fsanitize=thread" ]
-  }
-  if (is_msan) {
-    ldflags += [ "-fsanitize=memory" ]
-  }
-  if (is_ubsan) {
-    ldflags += [ "-fsanitize=undefined" ]
-  }
-  configs = [ ":sanitizer_options_link_helper" ]
 }
diff --git a/gn/standalone/sanitizers/vars.gni b/gn/standalone/sanitizers/vars.gni
index 9dada78..228908f 100644
--- a/gn/standalone/sanitizers/vars.gni
+++ b/gn/standalone/sanitizers/vars.gni
@@ -47,8 +47,9 @@
 }
 
 declare_args() {
-  using_sanitizer =
-      is_asan || is_lsan || is_tsan || is_msan || is_ubsan || use_libfuzzer
+  # Don't build host artifacts with sanitizers/fuzzers, only target toolchain.
+  using_sanitizer = (is_asan || is_lsan || is_tsan || is_msan || is_ubsan ||
+                     use_libfuzzer) && current_toolchain == default_toolchain
 }
 
 assert(!using_sanitizer || is_clang || is_system_compiler,
diff --git a/gn/standalone/toolchain/BUILD.gn b/gn/standalone/toolchain/BUILD.gn
index f72c606..ca8f247 100644
--- a/gn/standalone/toolchain/BUILD.gn
+++ b/gn/standalone/toolchain/BUILD.gn
@@ -54,9 +54,13 @@
   gcc_toolchain = ""
   ar = "ar"
   linker = ""
+  strip = ""
 
   if (is_linux_host) {
     linker = "gold"
+    strip = "strip"
+  } else if (is_mac_host) {
+    strip = "strip -x"
   }
 
   if (is_clang) {
@@ -129,6 +133,7 @@
 }
 
 declare_args() {
+  target_strip = ""
   if (is_linux || is_android) {
     target_linker = "gold"
   } else {
@@ -141,6 +146,7 @@
     target_cc = cc
     target_cxx = cxx
     target_linker = linker
+    target_strip = strip
   } else {
     target_ar = "ar"
     if (is_android) {
@@ -148,6 +154,7 @@
       target_cc = "$android_llvm_dir/bin/clang"
       target_cxx = "$android_llvm_dir/bin/clang++"
       target_linker = "$android_llvm_dir/bin/ld.lld"
+      target_strip = "$android_toolchain_root/bin/$android_abi_target-strip"
     } else {
       assert(_target_triplet != "",
              "target_triplet must be non-empty when cross-compiling")
@@ -180,6 +187,7 @@
     external_cflags = ""
     external_cxxflags = ""
     external_ldflags = ""
+    strip = ""
     if (defined(invoker.linker) && invoker.linker != "") {
       _invoker_linker = invoker.linker
       ld_arg = "-fuse-ld=$_invoker_linker"
@@ -203,6 +211,9 @@
     if (defined(invoker.external_ldflags)) {
       external_ldflags = invoker.external_ldflags
     }
+    if (defined(invoker.strip)) {
+      strip = invoker.strip
+    }
 
     tool("cc") {
       depfile = "{{output}}.d"
@@ -249,24 +260,34 @@
 
     tool("solink") {
       soname = "{{target_output_name}}{{output_extension}}"
-
+      unstripped_so = "{{root_out_dir}}/$soname"
       rpath = "-Wl,-soname,$soname"
       if (is_mac) {
         rpath = "-Wl,-install_name,@rpath/$soname"
       }
-
-      command = "$cc_wrapper $cxx $ld_arg -shared {{ldflags}} ${external_ldflags} {{inputs}} {{solibs}} {{libs}} $rpath -o {{output}}"
-      outputs = [ "{{root_out_dir}}/$soname" ]
+      command = "$cc_wrapper $cxx $ld_arg -shared {{ldflags}} ${external_ldflags} {{inputs}} {{solibs}} {{libs}} $rpath -o $unstripped_so"
+      outputs = [ unstripped_so ]
       output_prefix = "lib"
       default_output_extension = ".so"
-      description = "link {{output}}"
+      description = "link $unstripped_so"
+      if (strip != "") {
+        stripped_so = "{{root_out_dir}}/stripped/$soname"
+        outputs += [ stripped_so ]
+        command += " && $strip -o $stripped_so $unstripped_so"
+      }
     }
 
     tool("link") {
-      command = "$cc_wrapper $cxx $ld_arg {{ldflags}} ${external_ldflags} {{inputs}} {{solibs}} {{libs}} -o {{output}}"
-      outputs =
-          [ "{{root_out_dir}}/{{target_output_name}}{{output_extension}}" ]
-      description = "link {{output}}"
+      unstripped_exe =
+          "{{root_out_dir}}/{{target_output_name}}{{output_extension}}"
+      command = "$cc_wrapper $cxx $ld_arg {{ldflags}} ${external_ldflags} {{inputs}} {{solibs}} {{libs}} -o $unstripped_exe"
+      outputs = [ unstripped_exe ]
+      description = "link $unstripped_exe"
+      if (strip != "") {
+        stripped_exe = "{{root_out_dir}}/stripped/{{target_output_name}}{{output_extension}}"
+        outputs += [ stripped_exe ]
+        command += " && $strip -o $stripped_exe $unstripped_exe"
+      }
     }
 
     tool("stamp") {
@@ -293,6 +314,7 @@
   cc = target_cc
   cxx = target_cxx
   linker = target_linker
+  strip = target_strip
   sysroot = target_sysroot
   gcc_toolchain = target_gcc_toolchain
   external_cflags = string_join(" ",
@@ -319,6 +341,7 @@
   cc = cc
   cxx = cxx
   linker = linker
+  strip = strip
   sysroot = sysroot
   gcc_toolchain = gcc_toolchain
   external_cflags = string_join(" ",
@@ -345,6 +368,7 @@
   ar = "$emsdk_dir/emscripten/emar --em-config $em_config"
   cc = "$emsdk_dir/emscripten/emcc --em-config $em_config"
   cxx = "$emsdk_dir/emscripten/em++ --em-config $em_config"
+  strip = ""
 }
 
 # This is used both for MSVC anc clang-cl. clang-cl cmdline interface pretends
@@ -352,10 +376,7 @@
 toolchain("msvc") {
   lib_switch = ""
   lib_dir_switch = "/LIBPATH:"
-
-  sys_lib_flags = "/LIBPATH:\"${win_sdk_lib_dir}\\ucrt\\x64\" "
-  sys_lib_flags += "/LIBPATH:\"${win_sdk_lib_dir}\\um\\x64\" "
-  sys_lib_flags += "/LIBPATH:\"${win_msvc_lib_dir}\" "
+  sys_lib_flags = string_join(" ", win_msvc_sys_lib_flags)
 
   # Note: /showIncludes below is required for ninja, to build a complete
   # dependency graph for headers. Removing it breaks incremental builds.
diff --git a/gn/standalone/toolchain/msvc.gni b/gn/standalone/toolchain/msvc.gni
index 34c0a50..7ec7313 100644
--- a/gn/standalone/toolchain/msvc.gni
+++ b/gn/standalone/toolchain/msvc.gni
@@ -36,22 +36,28 @@
 
   # These variables are required both for clang-cl.exe and MSVC (cl.exe).
   win_sdk_lib_dir = _win_sdk_base + "\\Lib\\" + _win_sdk_ver
-  win_msvc_lib_dir = _win_msvc_base + "\\lib\\x64"
+  win_msvc_lib_dir = _win_msvc_base + "\\lib\\${target_cpu}"
 
   # These variables are only required when building with MSVC.
   # Clang is clever enough to figure out the right include path by querying the
   # registry and detect the Windows SDK path (it still needs the /LIBPATH
   # though, hence the _lib_dir above).
-  win_msvc_bin_dir = _win_msvc_base + "\\bin\\Hostx64\\x64"
+  win_msvc_bin_dir = _win_msvc_base + "\\bin\\Host${host_cpu}\\${target_cpu}"
   win_msvc_inc_dirs = [
     _win_msvc_base + "\\include",
     _win_sdk_base + "\\Include\\" + _win_sdk_ver + "\\ucrt",
     _win_sdk_base + "\\Include\\" + _win_sdk_ver + "\\um",
     _win_sdk_base + "\\Include\\" + _win_sdk_ver + "\\shared",
   ]
+  win_msvc_sys_lib_flags = [
+    "/LIBPATH:\"${win_sdk_lib_dir}\\ucrt\\${target_cpu}\"",
+    "/LIBPATH:\"${win_sdk_lib_dir}\\um\\${target_cpu}\"",
+    "/LIBPATH:\"${win_msvc_lib_dir}\"",
+  ]
 } else {
   win_sdk_lib_dir = ""
   win_msvc_lib_dir = ""
   win_msvc_bin_dir = ""
   win_msvc_inc_dirs = []
+  win_msvc_sys_lib_flags = []
 }
diff --git a/gn/standalone/toolchain/win_find_msvc.py b/gn/standalone/toolchain/win_find_msvc.py
index 41b3476..0badd1b 100644
--- a/gn/standalone/toolchain/win_find_msvc.py
+++ b/gn/standalone/toolchain/win_find_msvc.py
@@ -63,7 +63,7 @@
     filt = lambda x: os.path.exists(os.path.join(x, 'ucrt', 'x64', 'ucrt.lib'))
     out[1] = find_max_subdir(lib_base, filt)
 
-  for version in ['BuildTools', 'Community']:
+  for version in ['BuildTools', 'Community', 'Professional']:
     msvc_base = ('C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\'
                  '{}\\VC\\Tools\\MSVC').format(version)
     if os.path.exists(msvc_base):
diff --git a/gn/standalone/wasm_typescript_declaration.d.ts b/gn/standalone/wasm_typescript_declaration.d.ts
index 67b0c9b..3f8547d 100644
--- a/gn/standalone/wasm_typescript_declaration.d.ts
+++ b/gn/standalone/wasm_typescript_declaration.d.ts
@@ -32,7 +32,7 @@
 
   export interface FileSystemNode {
     contents: Uint8Array;
-    usedBytes: number;
+    usedBytes?: number;
   }
 
   export interface FileSystem {
diff --git a/gn/write_buildflag_header.py b/gn/write_buildflag_header.py
index 8045cce..617ab7d 100644
--- a/gn/write_buildflag_header.py
+++ b/gn/write_buildflag_header.py
@@ -67,7 +67,7 @@
   guard = '%s_' % args.out.upper()
   guard = guard.replace('/', '_').replace('\\', '_').replace('.', '_')
   lines = []
-  lines.append('// Generated by %s' % __file__)
+  lines.append('// Generated by %s' % os.path.basename(__file__))
   lines.append('')
   lines.append('// fix_include_guards: off')
   lines.append('#ifndef %s' % guard)
diff --git a/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h b/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h
index 7a34606..a114665 100644
--- a/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h
+++ b/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-// Generated by ../../gn/write_buildflag_header.py
+// Generated by write_buildflag_header.py
 
 // fix_include_guards: off
 #ifndef GEN_BUILD_CONFIG_PERFETTO_BUILD_FLAGS_H_
@@ -34,10 +34,13 @@
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_VERSION_GEN() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_PERCENTILE() (0)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_LINENOISE() (0)
-#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_HTTPD() (0)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_HTTPD() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_JSON() (0)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_LOCAL_SYMBOLIZER() (PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_LINUX() || PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_MAC() ||PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_WIN())
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ZLIB() (1)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TRACED_PERF() (1)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_HEAPPROFD() (1)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_STDERR_CRASH_DUMP() (0)
 
 // clang-format on
 #endif  // GEN_BUILD_CONFIG_PERFETTO_BUILD_FLAGS_H_
diff --git a/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h b/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
index ada8006..393c2b2 100644
--- a/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
+++ b/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-// Generated by ../../gn/write_buildflag_header.py
+// Generated by write_buildflag_header.py
 
 // fix_include_guards: off
 #ifndef GEN_BUILD_CONFIG_PERFETTO_BUILD_FLAGS_H_
@@ -38,6 +38,9 @@
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_JSON() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_LOCAL_SYMBOLIZER() (PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_LINUX() || PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_MAC() ||PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_WIN())
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ZLIB() (1)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TRACED_PERF() (0)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_HEAPPROFD() (0)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_STDERR_CRASH_DUMP() (0)
 
 // clang-format on
 #endif  // GEN_BUILD_CONFIG_PERFETTO_BUILD_FLAGS_H_
diff --git a/include/perfetto/base/logging.h b/include/perfetto/base/logging.h
index 3cef830..8641a1c 100644
--- a/include/perfetto/base/logging.h
+++ b/include/perfetto/base/logging.h
@@ -80,6 +80,19 @@
 
 enum LogLev { kLogDebug = 0, kLogInfo, kLogImportant, kLogError };
 
+struct LogMessageCallbackArgs {
+  LogLev level;
+  int line;
+  const char* filename;
+  const char* message;
+};
+
+using LogMessageCallback = void (*)(LogMessageCallbackArgs);
+
+// This is not thread safe and must be called before using tracing from other
+// threads.
+PERFETTO_EXPORT void SetLogMessageCallback(LogMessageCallback callback);
+
 PERFETTO_EXPORT void LogMessage(LogLev,
                                 const char* fname,
                                 int line,
diff --git a/include/perfetto/base/template_util.h b/include/perfetto/base/template_util.h
index 58f90e7..4a5359c 100644
--- a/include/perfetto/base/template_util.h
+++ b/include/perfetto/base/template_util.h
@@ -17,6 +17,7 @@
 #ifndef INCLUDE_PERFETTO_BASE_TEMPLATE_UTIL_H_
 #define INCLUDE_PERFETTO_BASE_TEMPLATE_UTIL_H_
 
+#include <cstddef>
 #include <type_traits>
 
 namespace perfetto {
@@ -31,6 +32,13 @@
 template <>
 struct priority_tag<0> {};
 
+// enable_if_t is an implementation of std::enable_if_t from C++14.
+//
+// Specification:
+// https://en.cppreference.com/w/cpp/types/enable_if
+template <bool B, class T = void>
+using enable_if_t = typename std::enable_if<B, T>::type;
+
 // decay_t is an implementation of std::decay_t from C++14.
 //
 // Specification:
diff --git a/include/perfetto/ext/base/BUILD.gn b/include/perfetto/ext/base/BUILD.gn
index 8cfb32e..ec8cca8 100644
--- a/include/perfetto/ext/base/BUILD.gn
+++ b/include/perfetto/ext/base/BUILD.gn
@@ -30,6 +30,7 @@
     "no_destructor.h",
     "optional.h",
     "paged_memory.h",
+    "periodic_task.h",
     "pipe.h",
     "scoped_file.h",
     "small_set.h",
diff --git a/include/perfetto/ext/base/getopt.h b/include/perfetto/ext/base/getopt.h
index 77c1490..bf993fc 100644
--- a/include/perfetto/ext/base/getopt.h
+++ b/include/perfetto/ext/base/getopt.h
@@ -34,8 +34,10 @@
 // replacement to the various main.cc, which can't know about the nested
 // namespace.
 using ::perfetto::base::getopt_compat::optarg;
+using ::perfetto::base::getopt_compat::opterr;
 using ::perfetto::base::getopt_compat::optind;
 using ::perfetto::base::getopt_compat::option;
+using ::perfetto::base::getopt_compat::optopt;
 constexpr auto getopt = ::perfetto::base::getopt_compat::getopt;
 constexpr auto getopt_long = ::perfetto::base::getopt_compat::getopt_long;
 constexpr auto no_argument = ::perfetto::base::getopt_compat::no_argument;
diff --git a/include/perfetto/ext/base/getopt_compat.h b/include/perfetto/ext/base/getopt_compat.h
index d1f5436..d854ff0 100644
--- a/include/perfetto/ext/base/getopt_compat.h
+++ b/include/perfetto/ext/base/getopt_compat.h
@@ -53,6 +53,8 @@
 
 extern char* optarg;
 extern int optind;
+extern int optopt;
+extern int opterr;
 
 int getopt_long(int argc,
                 char** argv,
diff --git a/include/perfetto/ext/base/periodic_task.h b/include/perfetto/ext/base/periodic_task.h
new file mode 100644
index 0000000..b948322
--- /dev/null
+++ b/include/perfetto/ext/base/periodic_task.h
@@ -0,0 +1,86 @@
+/*
+ * 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_PERIODIC_TASK_H_
+#define INCLUDE_PERFETTO_EXT_BASE_PERIODIC_TASK_H_
+
+#include <functional>
+
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/thread_checker.h"
+#include "perfetto/ext/base/weak_ptr.h"
+
+namespace perfetto {
+namespace base {
+
+class TaskRunner;
+
+// A periodic task utility class. It wraps the logic necessary to do periodic
+// tasks using a TaskRunner, taking care of subtleties like ensuring that
+// outstanding tasks are cancelled after reset/dtor.
+// Tasks are aligned on wall time, this is to ensure that when using multiple
+// periodic tasks, they happen at the same time, minimizing wakeups.
+// On Linux/Android it also supports suspend-aware mode (via timerfd). On other
+// operating systems it falls back to PostDelayedTask, which is not
+// suspend-aware.
+// TODO(primiano): this should probably become a periodic timer scheduler, so we
+// can use one FD for everything rather than one FD per task. For now we take
+// the hit of a FD-per-task to keep this low-risk.
+class PeriodicTask {
+ public:
+  explicit PeriodicTask(base::TaskRunner*);
+  ~PeriodicTask();  // Calls Reset().
+
+  struct Args {
+    uint32_t period_ms = 0;
+    std::function<void()> task = nullptr;
+    bool start_first_task_immediately = false;
+    bool use_suspend_aware_timer = false;
+  };
+
+  void Start(Args);
+
+  // Safe to be called multiple times, even without calling Start():
+  void Reset();
+
+  // No copy or move. WeakPtr-wrapped pointers to |this| are posted on the
+  // task runner, this class is not easily movable.
+  PeriodicTask(const PeriodicTask&) = delete;
+  PeriodicTask& operator=(const PeriodicTask&) = delete;
+  PeriodicTask(PeriodicTask&&) = delete;
+  PeriodicTask& operator=(PeriodicTask&&) = delete;
+
+  base::PlatformHandle timer_fd_for_testing() { return *timer_fd_; }
+
+ private:
+  static void RunTaskAndPostNext(base::WeakPtr<PeriodicTask>,
+                                 uint32_t generation);
+  void PostNextTask();
+  void ResetTimerFd();
+
+  base::TaskRunner* const task_runner_;
+  Args args_;
+  uint32_t generation_ = 0;
+  base::ScopedPlatformHandle timer_fd_;
+
+  PERFETTO_THREAD_CHECKER(thread_checker_)
+  base::WeakPtrFactory<PeriodicTask> weak_ptr_factory_;  // Keep last.
+};
+
+}  // namespace base
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_EXT_BASE_PERIODIC_TASK_H_
diff --git a/include/perfetto/ext/base/scoped_file.h b/include/perfetto/ext/base/scoped_file.h
index 978843e..5a99de6 100644
--- a/include/perfetto/ext/base/scoped_file.h
+++ b/include/perfetto/ext/base/scoped_file.h
@@ -53,8 +53,10 @@
           class Checker = internal::DefaultValidityChecker<T, InvalidValue>>
 class PERFETTO_EXPORT ScopedResource {
  public:
-  explicit ScopedResource(T t = InvalidValue) : t_(t) {}
+  using ValidityChecker = Checker;
   static constexpr T kInvalid = InvalidValue;
+
+  explicit ScopedResource(T t = InvalidValue) : t_(t) {}
   ScopedResource(ScopedResource&& other) noexcept {
     t_ = other.t_;
     other.t_ = InvalidValue;
diff --git a/include/perfetto/ext/base/string_utils.h b/include/perfetto/ext/base/string_utils.h
index 7310b01..bd4f1f6 100644
--- a/include/perfetto/ext/base/string_utils.h
+++ b/include/perfetto/ext/base/string_utils.h
@@ -98,6 +98,7 @@
 bool StartsWith(const std::string& str, const std::string& prefix);
 bool EndsWith(const std::string& str, const std::string& suffix);
 bool Contains(const std::string& haystack, const std::string& needle);
+bool Contains(const std::string& haystack, char needle);
 size_t Find(const StringView& needle, const StringView& haystack);
 bool CaseInsensitiveEqual(const std::string& first, const std::string& second);
 std::string Join(const std::vector<std::string>& parts,
@@ -122,7 +123,7 @@
                        const std::string& to_replace,
                        const std::string& replacement);
 std::string TrimLeading(const std::string& str);
-std::string Base64Encode(const void* ptr, size_t size);
+std::string Base64Encode(const void* raw, size_t size);
 
 }  // namespace base
 }  // namespace perfetto
diff --git a/include/perfetto/ext/base/string_view.h b/include/perfetto/ext/base/string_view.h
index dce2fdc..4f16f90 100644
--- a/include/perfetto/ext/base/string_view.h
+++ b/include/perfetto/ext/base/string_view.h
@@ -119,7 +119,7 @@
   }
 
   std::string ToStdString() const {
-    return data_ == nullptr ? "" : std::string(data_, size_);
+    return size_ == 0 ? "" : std::string(data_, size_);
   }
 
   uint64_t Hash() const {
diff --git a/include/perfetto/ext/base/subprocess.h b/include/perfetto/ext/base/subprocess.h
index 77b2dfd..80adbdc 100644
--- a/include/perfetto/ext/base/subprocess.h
+++ b/include/perfetto/ext/base/subprocess.h
@@ -30,6 +30,7 @@
 #include "perfetto/base/platform_handle.h"
 #include "perfetto/base/proc_utils.h"
 #include "perfetto/ext/base/event_fd.h"
+#include "perfetto/ext/base/optional.h"
 #include "perfetto/ext/base/pipe.h"
 #include "perfetto/ext/base/scoped_file.h"
 
@@ -133,6 +134,13 @@
     // just before the exec() call, but after having closed all fds % stdin/o/e.
     // This is for synchronization barriers in tests.
     std::function<void()> posix_entrypoint_for_testing;
+
+    // When set, will will move the process to the given process group. If set
+    // and zero, it will create a new process group. Effectively this calls
+    // setpgid(0 /*self_pid*/, posix_proc_group_id).
+    // This can be used to avoid that subprocesses receive CTRL-C from the
+    // terminal, while still living in the same session.
+    base::Optional<pid_t> posix_proc_group_id{};
 #endif
 
     // If non-empty, replaces the environment passed to exec().
diff --git a/include/perfetto/ext/base/thread_task_runner.h b/include/perfetto/ext/base/thread_task_runner.h
index d4e672e..5f5947e 100644
--- a/include/perfetto/ext/base/thread_task_runner.h
+++ b/include/perfetto/ext/base/thread_task_runner.h
@@ -32,7 +32,7 @@
 // * the UnixTaskRunner will be constructed and destructed on the task thread.
 // * the task thread will live for the lifetime of the UnixTaskRunner.
 //
-class ThreadTaskRunner : public TaskRunner {
+class PERFETTO_EXPORT ThreadTaskRunner : public TaskRunner {
  public:
   static ThreadTaskRunner CreateAndStart(const std::string& name = "") {
     return ThreadTaskRunner(name);
diff --git a/include/perfetto/ext/base/utils.h b/include/perfetto/ext/base/utils.h
index a680b5c..729225f 100644
--- a/include/perfetto/ext/base/utils.h
+++ b/include/perfetto/ext/base/utils.h
@@ -47,7 +47,9 @@
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
 using uid_t = unsigned int;
+#if !PERFETTO_BUILDFLAG(PERFETTO_COMPILER_GCC)
 using pid_t = unsigned int;
+#endif
 #if defined(_WIN64)
 using ssize_t = int64_t;
 #else
@@ -117,6 +119,18 @@
 // geteuid() on POSIX OSes, returns 0 on Windows (See comment in utils.cc).
 uid_t GetCurrentUserId();
 
+// Forks the process.
+// Parent: prints the PID of the child and exit(0).
+// Child: redirects stdio onto /dev/null and chdirs into .
+void Daemonize();
+
+// Returns the path of the current executable, e.g. /foo/bar/exe.
+std::string GetCurExecutablePath();
+
+// Returns the directory where the current executable lives in, e.g. /foo/bar.
+// This is independent of cwd().
+std::string GetCurExecutableDir();
+
 }  // namespace base
 }  // namespace perfetto
 
diff --git a/include/perfetto/ext/trace_processor/importers/memory_tracker/graph.h b/include/perfetto/ext/trace_processor/importers/memory_tracker/graph.h
index fccf57c..d8a9d87 100644
--- a/include/perfetto/ext/trace_processor/importers/memory_tracker/graph.h
+++ b/include/perfetto/ext/trace_processor/importers/memory_tracker/graph.h
@@ -297,8 +297,8 @@
 
  private:
   // Creates a node in the arena which is associated with the given
-  // |node_graph| and for the given |parent|.
-  Node* CreateNode(GlobalNodeGraph::Process* node_graph,
+  // |process_graph| and for the given |parent|.
+  Node* CreateNode(GlobalNodeGraph::Process* process_graph,
                    GlobalNodeGraph::Node* parent);
 
   std::forward_list<Node> all_nodes_;
diff --git a/include/perfetto/ext/tracing/core/slice.h b/include/perfetto/ext/tracing/core/slice.h
index 4ce66fd..063043b 100644
--- a/include/perfetto/ext/tracing/core/slice.h
+++ b/include/perfetto/ext/tracing/core/slice.h
@@ -44,6 +44,14 @@
     return slice;
   }
 
+  static Slice TakeOwnership(std::unique_ptr<uint8_t[]> buf, size_t size) {
+    Slice slice;
+    slice.own_data_ = std::move(buf);
+    slice.start = &slice.own_data_[0];
+    slice.size = size;
+    return slice;
+  }
+
   uint8_t* own_data() {
     PERFETTO_DCHECK(own_data_);
     return own_data_.get();
diff --git a/include/perfetto/ext/tracing/core/tracing_service.h b/include/perfetto/ext/tracing/core/tracing_service.h
index 20382a1..c26d949 100644
--- a/include/perfetto/ext/tracing/core/tracing_service.h
+++ b/include/perfetto/ext/tracing/core/tracing_service.h
@@ -326,7 +326,8 @@
       ProducerSMBScrapingMode smb_scraping_mode =
           ProducerSMBScrapingMode::kDefault,
       size_t shared_memory_page_size_hint_bytes = 0,
-      std::unique_ptr<SharedMemory> shm = nullptr) = 0;
+      std::unique_ptr<SharedMemory> shm = nullptr,
+      const std::string& sdk_version = {}) = 0;
 
   // Connects a Consumer instance and obtains a ConsumerEndpoint, which is
   // essentially a 1:1 channel between one Consumer and the Service.
diff --git a/include/perfetto/profiling/pprof_builder.h b/include/perfetto/profiling/pprof_builder.h
index 20b9d2e..df18c5a 100644
--- a/include/perfetto/profiling/pprof_builder.h
+++ b/include/perfetto/profiling/pprof_builder.h
@@ -17,7 +17,6 @@
 #ifndef INCLUDE_PERFETTO_PROFILING_PPROF_BUILDER_H_
 #define INCLUDE_PERFETTO_PROFILING_PPROF_BUILDER_H_
 
-#include <iostream>
 #include <string>
 #include <vector>
 
@@ -48,9 +47,19 @@
 
 enum class ConversionMode { kHeapProfile, kPerfProfile };
 
+enum class ConversionFlags : uint64_t {
+  kNone = 0,
+  // Suffix frame names with additional information. Current annotations are
+  // specific to apps running within the Android runtime, and include
+  // information such as whether the given frame was interpreted / executed
+  // under JIT / etc.
+  kAnnotateFrames = 1
+};
+
 bool TraceToPprof(trace_processor::TraceProcessor* tp,
                   std::vector<SerializedProfile>* output,
                   ConversionMode mode = ConversionMode::kHeapProfile,
+                  uint64_t flags = 0,
                   uint64_t pid = 0,
                   const std::vector<uint64_t>& timestamps = {});
 
diff --git a/include/perfetto/protozero/BUILD.gn b/include/perfetto/protozero/BUILD.gn
index 93fdce4..c99f7fd 100644
--- a/include/perfetto/protozero/BUILD.gn
+++ b/include/perfetto/protozero/BUILD.gn
@@ -19,6 +19,7 @@
     "copyable_ptr.h",
     "cpp_message_obj.h",
     "field.h",
+    "field_writer.h",
     "message.h",
     "message_arena.h",
     "message_handle.h",
diff --git a/include/perfetto/protozero/field_writer.h b/include/perfetto/protozero/field_writer.h
new file mode 100644
index 0000000..e8d76d7
--- /dev/null
+++ b/include/perfetto/protozero/field_writer.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.
+ */
+
+#include "perfetto/protozero/message.h"
+#include "perfetto/protozero/proto_utils.h"
+
+#ifndef INCLUDE_PERFETTO_PROTOZERO_FIELD_WRITER_H_
+#define INCLUDE_PERFETTO_PROTOZERO_FIELD_WRITER_H_
+
+namespace protozero {
+namespace internal {
+
+template <proto_utils::ProtoSchemaType proto_schema_type>
+struct FieldWriter {
+  static_assert(proto_schema_type != proto_utils::ProtoSchemaType::kMessage,
+                "FieldWriter can't be used with nested messages");
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kDouble> {
+  inline static void Append(Message& message, uint32_t field_id, double value) {
+    message.AppendFixed(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kFloat> {
+  inline static void Append(Message& message, uint32_t field_id, float value) {
+    message.AppendFixed(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kBool> {
+  inline static void Append(Message& message, uint32_t field_id, bool value) {
+    message.AppendTinyVarInt(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kInt32> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            int32_t value) {
+    message.AppendVarInt(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kInt64> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            int64_t value) {
+    message.AppendVarInt(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kUint32> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            uint32_t value) {
+    message.AppendVarInt(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kUint64> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            uint64_t value) {
+    message.AppendVarInt(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kSint32> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            int32_t value) {
+    message.AppendSignedVarInt(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kSint64> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            int64_t value) {
+    message.AppendSignedVarInt(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kFixed32> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            uint32_t value) {
+    message.AppendFixed(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kFixed64> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            uint64_t value) {
+    message.AppendFixed(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kSfixed32> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            int32_t value) {
+    message.AppendFixed(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kSfixed64> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            int64_t value) {
+    message.AppendFixed(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kEnum> {
+  template <typename EnumType>
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            EnumType value) {
+    message.AppendVarInt(field_id, value);
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kString> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            const char* data,
+                            size_t size) {
+    message.AppendBytes(field_id, data, size);
+  }
+
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            const std::string& value) {
+    message.AppendBytes(field_id, value.data(), value.size());
+  }
+};
+
+template <>
+struct FieldWriter<proto_utils::ProtoSchemaType::kBytes> {
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            const uint8_t* data,
+                            size_t size) {
+    message.AppendBytes(field_id, data, size);
+  }
+
+  inline static void Append(Message& message,
+                            uint32_t field_id,
+                            const std::string& value) {
+    message.AppendBytes(field_id, value.data(), value.size());
+  }
+};
+
+}  // namespace internal
+}  // namespace protozero
+
+#endif  // INCLUDE_PERFETTO_PROTOZERO_FIELD_WRITER_H_
diff --git a/include/perfetto/protozero/proto_utils.h b/include/perfetto/protozero/proto_utils.h
index df7386d..6b195cb 100644
--- a/include/perfetto/protozero/proto_utils.h
+++ b/include/perfetto/protozero/proto_utils.h
@@ -201,12 +201,20 @@
 // used to backfill fixed-size reservations for the length field using a
 // non-canonical varint encoding (e.g. \x81\x80\x80\x00 instead of \x01).
 // See https://github.com/google/protobuf/issues/1530.
-// In particular, this is used for nested messages. The size of a nested message
-// is not known until all its field have been written. |kMessageLengthFieldSize|
-// bytes are reserved to encode the size field and backfilled at the end.
-inline void WriteRedundantVarInt(uint32_t value, uint8_t* buf) {
-  for (size_t i = 0; i < kMessageLengthFieldSize; ++i) {
-    const uint8_t msb = (i < kMessageLengthFieldSize - 1) ? 0x80 : 0;
+// This is used mainly in two cases:
+// 1) At trace writing time, when starting a nested messages. The size of a
+//    nested message is not known until all its field have been written.
+//    |kMessageLengthFieldSize| bytes are reserved to encode the size field and
+//    backfilled at the end.
+// 2) When rewriting a message at trace filtering time, in protozero/filtering.
+//    At that point we know only the upper bound of the length (a filtered
+//    message is <= the original one) and we backfill after the message has been
+//    filtered.
+inline void WriteRedundantVarInt(uint32_t value,
+                                 uint8_t* buf,
+                                 size_t size = kMessageLengthFieldSize) {
+  for (size_t i = 0; i < size; ++i) {
+    const uint8_t msb = (i < size - 1) ? 0x80 : 0;
     buf[i] = static_cast<uint8_t>(value) | msb;
     value >>= 7;
   }
@@ -244,6 +252,53 @@
   return start;
 }
 
+enum class RepetitionType {
+  kNotRepeated,
+  kRepeatedPacked,
+  kRepeatedNotPacked,
+};
+
+// Provide a common base struct for all templated FieldMetadata types to allow
+// simple checks if a given type is a FieldMetadata or not.
+struct FieldMetadataBase {
+  constexpr FieldMetadataBase() = default;
+};
+
+template <uint32_t field_id,
+          RepetitionType repetition_type,
+          ProtoSchemaType proto_schema_type,
+          typename CppFieldType,
+          typename MessageType>
+struct FieldMetadata : public FieldMetadataBase {
+  constexpr FieldMetadata() = default;
+
+  static constexpr int kFieldId = field_id;
+  // Whether this field is repeated, packed (repeated [packed-true]) or not
+  // (optional).
+  static constexpr RepetitionType kRepetitionType = repetition_type;
+  // Proto type of this field (e.g. int64, fixed32 or nested message).
+  static constexpr ProtoSchemaType kProtoFieldType = proto_schema_type;
+  // C++ type of this field (for nested messages - C++ protozero class).
+  using cpp_field_type = CppFieldType;
+  // Protozero message which this field belongs to.
+  using message_type = MessageType;
+};
+
+namespace internal {
+
+// Ideally we would create variables of FieldMetadata<...> type directly,
+// but before C++17's support for constexpr inline variables arrive, we have to
+// actually use pointers to inline functions instead to avoid having to define
+// symbols in *.pbzero.cc files.
+//
+// Note: protozero bindings will generate Message::kFieldName variable and which
+// can then be passed to TRACE_EVENT macro for inline writing of typed messages.
+// The fact that the former can be passed to the latter is a part of the stable
+// API, while the particular type is not and users should not rely on it.
+template <typename T>
+using FieldMetadataHelper = T (*)(void);
+
+}  // namespace internal
 }  // namespace proto_utils
 }  // namespace protozero
 
diff --git a/include/perfetto/tracing/BUILD.gn b/include/perfetto/tracing/BUILD.gn
index 7c37cf6..b7a91d0 100644
--- a/include/perfetto/tracing/BUILD.gn
+++ b/include/perfetto/tracing/BUILD.gn
@@ -46,12 +46,14 @@
     "internal/tracing_tls.h",
     "internal/track_event_data_source.h",
     "internal/track_event_internal.h",
+    "internal/track_event_interned_fields.h",
     "internal/track_event_macros.h",
     "internal/write_track_event_args.h",
     "locked_handle.h",
     "platform.h",
     "string_helpers.h",
     "trace_writer_base.h",
+    "traced_proto.h",
     "traced_value.h",
     "traced_value_forward.h",
     "tracing.h",
diff --git a/include/perfetto/tracing/console_interceptor.h b/include/perfetto/tracing/console_interceptor.h
index e62b90d..98c8879 100644
--- a/include/perfetto/tracing/console_interceptor.h
+++ b/include/perfetto/tracing/console_interceptor.h
@@ -49,7 +49,7 @@
 namespace perfetto {
 namespace protos {
 namespace pbzero {
-class DebugAnnotation_NestedValue_Decoder;
+class DebugAnnotation_Decoder;
 class TracePacket_Decoder;
 class TrackEvent_Decoder;
 }  // namespace pbzero
@@ -57,7 +57,8 @@
 
 struct ConsoleColor;
 
-class ConsoleInterceptor : public Interceptor<ConsoleInterceptor> {
+class PERFETTO_EXPORT ConsoleInterceptor
+    : public Interceptor<ConsoleInterceptor> {
  public:
   ~ConsoleInterceptor() override;
 
@@ -102,14 +103,16 @@
   static void SetColor(InterceptorContext& context, const ConsoleColor&);
   static void SetColor(InterceptorContext& context, const char*);
 
-  static void PrintAnnotations(InterceptorContext&,
-                               const protos::pbzero::TrackEvent_Decoder&,
-                               const ConsoleColor& slice_color,
-                               const ConsoleColor& highlight_color);
-  static void PrintNestedValue(
+  static void PrintDebugAnnotations(InterceptorContext&,
+                                    const protos::pbzero::TrackEvent_Decoder&,
+                                    const ConsoleColor& slice_color,
+                                    const ConsoleColor& highlight_color);
+  static void PrintDebugAnnotationName(
       InterceptorContext&,
-      const perfetto::protos::pbzero::DebugAnnotation_NestedValue_Decoder&
-          value);
+      const perfetto::protos::pbzero::DebugAnnotation_Decoder& annotation);
+  static void PrintDebugAnnotationValue(
+      InterceptorContext&,
+      const perfetto::protos::pbzero::DebugAnnotation_Decoder& annotation);
 
   int fd_ = STDOUT_FILENO;
   bool use_colors_ = true;
diff --git a/include/perfetto/tracing/data_source.h b/include/perfetto/tracing/data_source.h
index cd35ef0..c4c6681 100644
--- a/include/perfetto/tracing/data_source.h
+++ b/include/perfetto/tracing/data_source.h
@@ -319,6 +319,11 @@
     if (PERFETTO_UNLIKELY(!tls_state_))
       tls_state_ = GetOrCreateDataSourceTLS(&static_state_);
 
+    // Avoid re-entering the trace point recursively.
+    if (PERFETTO_UNLIKELY(tls_state_->root_tls->is_in_trace_point))
+      return;
+    internal::ScopedReentrancyAnnotator scoped_annotator(*tls_state_->root_tls);
+
     // TracingTLS::generation is a global monotonic counter that is incremented
     // every time a tracing session is stopped. We use that as a signal to force
     // a slow-path garbage collection of all the trace writers for the current
@@ -373,12 +378,6 @@
       // handshaking to make this extremely unrealistic.
 
       auto& tls_inst = tls_state_->per_instance[i];
-
-      // Avoid re-entering the trace point recursively.
-      if (PERFETTO_UNLIKELY(tls_inst.is_in_trace_point))
-        continue;
-      ScopedReentrancyAnnotator scoped_annotator(tls_inst);
-
       if (PERFETTO_UNLIKELY(!tls_inst.trace_writer)) {
         // Here we need an acquire barrier, which matches the release-store made
         // by TracingMuxerImpl::SetupDataSource(), to ensure that the backend_id
@@ -456,18 +455,6 @@
     }
   };
 
-  struct ScopedReentrancyAnnotator {
-    ScopedReentrancyAnnotator(
-        internal::DataSourceInstanceThreadLocalState& tls_inst)
-        : tls_inst_(tls_inst) {
-      tls_inst_.is_in_trace_point = true;
-    }
-    ~ScopedReentrancyAnnotator() { tls_inst_.is_in_trace_point = false; }
-
-   private:
-    internal::DataSourceInstanceThreadLocalState& tls_inst_;
-  };
-
   // Create the user provided incremental state in the given thread-local
   // storage. Note: The second parameter here is used to specialize the case
   // where there is no incremental state type.
diff --git a/include/perfetto/tracing/event_context.h b/include/perfetto/tracing/event_context.h
index 9e9523f..e0c1a16 100644
--- a/include/perfetto/tracing/event_context.h
+++ b/include/perfetto/tracing/event_context.h
@@ -19,6 +19,7 @@
 
 #include "perfetto/protozero/message_handle.h"
 #include "perfetto/tracing/internal/track_event_internal.h"
+#include "perfetto/tracing/traced_proto.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
 namespace perfetto {
@@ -64,6 +65,16 @@
     return static_cast<EventType*>(event_);
   }
 
+  // Convert a raw pointer to protozero message to TracedProto which captures
+  // the reference to this EventContext.
+  template <typename MessageType>
+  TracedProto<MessageType> Wrap(MessageType* message) {
+    static_assert(std::is_base_of<protozero::Message, MessageType>::value,
+                  "TracedProto can be used only with protozero messages");
+
+    return TracedProto<MessageType>(message, this);
+  }
+
  private:
   template <typename, size_t, typename, typename>
   friend class TrackEventInternedDataIndex;
diff --git a/include/perfetto/tracing/internal/data_source_internal.h b/include/perfetto/tracing/internal/data_source_internal.h
index 6f19db0..cb052c6 100644
--- a/include/perfetto/tracing/internal/data_source_internal.h
+++ b/include/perfetto/tracing/internal/data_source_internal.h
@@ -156,7 +156,6 @@
     data_source_instance_id = 0;
     incremental_state_generation = 0;
     is_intercepted = false;
-    is_in_trace_point = false;
   }
 
   std::unique_ptr<TraceWriterBase> trace_writer;
@@ -167,7 +166,6 @@
   BufferId buffer_id;
   uint64_t data_source_instance_id;
   bool is_intercepted;
-  bool is_in_trace_point;
 };
 
 // Per-DataSource-type thread-local state.
diff --git a/include/perfetto/tracing/internal/tracing_tls.h b/include/perfetto/tracing/internal/tracing_tls.h
index 67f0637..b2340a4 100644
--- a/include/perfetto/tracing/internal/tracing_tls.h
+++ b/include/perfetto/tracing/internal/tracing_tls.h
@@ -74,6 +74,13 @@
   // thread-local TraceWriter(s) is issued.
   uint32_t generation = 0;
 
+  // This flag is true while this thread is inside a trace point for any data
+  // source or in other delicate parts of the tracing machinery during which we
+  // should not try to trace. Used to prevent unexpected re-entrancy.
+  // This flag is also load-bearing when handling re-entrancy during thread-exit
+  // handlers. See comment in TracingTLS::~TracingTLS().
+  bool is_in_trace_point = false;
+
   // By default all data source instances have independent thread-local state
   // (see above).
   std::array<DataSourceThreadLocalState, kMaxDataSources> data_sources_tls{};
@@ -84,6 +91,17 @@
   DataSourceThreadLocalState track_event_tls{};
 };
 
+struct ScopedReentrancyAnnotator {
+  ScopedReentrancyAnnotator(TracingTLS& root_tls) : root_tls_(root_tls) {
+    PERFETTO_DCHECK(!root_tls_.is_in_trace_point);
+    root_tls_.is_in_trace_point = true;
+  }
+  ~ScopedReentrancyAnnotator() { root_tls_.is_in_trace_point = false; }
+
+ private:
+  TracingTLS& root_tls_;
+};
+
 }  // namespace internal
 }  // namespace perfetto
 
diff --git a/include/perfetto/tracing/internal/track_event_data_source.h b/include/perfetto/tracing/internal/track_event_data_source.h
index 12a5a85..9229153 100644
--- a/include/perfetto/tracing/internal/track_event_data_source.h
+++ b/include/perfetto/tracing/internal/track_event_data_source.h
@@ -35,11 +35,6 @@
 
 namespace perfetto {
 
-struct TraceTimestamp {
-  protos::pbzero::BuiltinClock clock_id;
-  uint64_t nanoseconds;
-};
-
 // This template provides a way to convert an abstract timestamp into the trace
 // clock timebase in nanoseconds. By specialising this template and defining
 // static ConvertTimestampToTraceTimeNs function in it the user can register
@@ -60,6 +55,15 @@
   }
 };
 
+// A pass-through implementation for the trace timestamp structure.
+template <>
+struct TraceTimestampTraits<TraceTimestamp> {
+  static inline TraceTimestamp ConvertTimestampToTraceTimeNs(
+      const TraceTimestamp& timestamp) {
+    return timestamp;
+  }
+};
+
 namespace internal {
 namespace {
 
@@ -440,19 +444,16 @@
             return;
           }
 
-          // TODO(skyostil): Support additional clock ids.
           TraceTimestamp trace_timestamp = ::perfetto::TraceTimestampTraits<
               TimestampType>::ConvertTimestampToTraceTimeNs(timestamp);
-          PERFETTO_DCHECK(trace_timestamp.clock_id ==
-                          TrackEventInternal::GetClockId());
 
           // Make sure incremental state is valid.
           TraceWriterBase* trace_writer = ctx.tls_inst_->trace_writer.get();
           TrackEventIncrementalState* incr_state = ctx.GetIncrementalState();
           if (incr_state->was_cleared) {
             incr_state->was_cleared = false;
-            TrackEventInternal::ResetIncrementalState(
-                trace_writer, trace_timestamp.nanoseconds);
+            TrackEventInternal::ResetIncrementalState(trace_writer,
+                                                      trace_timestamp);
           }
 
           // Write the track descriptor before any event on the track.
@@ -465,7 +466,7 @@
           {
             auto event_ctx = TrackEventInternal::WriteEvent(
                 trace_writer, incr_state, static_category, event_name, type,
-                trace_timestamp.nanoseconds);
+                trace_timestamp);
             // Write dynamic categories (except for events that don't require
             // categories). For counter events, the counter name (and optional
             // category) is stored as part of the track descriptor instead being
diff --git a/include/perfetto/tracing/internal/track_event_internal.h b/include/perfetto/tracing/internal/track_event_internal.h
index 8c4f20f..afd874c 100644
--- a/include/perfetto/tracing/internal/track_event_internal.h
+++ b/include/perfetto/tracing/internal/track_event_internal.h
@@ -32,6 +32,13 @@
 #include <unordered_map>
 
 namespace perfetto {
+
+// Represents a point in time for the clock specified by |clock_id|.
+struct TraceTimestamp {
+  protos::pbzero::BuiltinClock clock_id;
+  uint64_t nanoseconds;
+};
+
 class EventContext;
 class TrackEventSessionObserver;
 struct Category;
@@ -139,9 +146,9 @@
       const Category* category,
       const char* name,
       perfetto::protos::pbzero::TrackEvent::Type,
-      uint64_t timestamp = GetTimeNs());
+      TraceTimestamp timestamp = {GetClockId(), GetTimeNs()});
 
-  static void ResetIncrementalState(TraceWriterBase*, uint64_t timestamp);
+  static void ResetIncrementalState(TraceWriterBase*, TraceTimestamp);
 
   template <typename T>
   static void AddDebugAnnotation(perfetto::EventContext* event_ctx,
@@ -171,7 +178,7 @@
   static void WriteTrackDescriptor(const TrackType& track,
                                    TraceWriterBase* trace_writer) {
     TrackRegistry::Get()->SerializeTrack(
-        track, NewTracePacket(trace_writer, GetTimeNs()));
+        track, NewTracePacket(trace_writer, {GetClockId(), GetTimeNs()}));
   }
 
   // Get the current time in nanoseconds in the trace clock timebase.
@@ -187,18 +194,22 @@
 #endif
   }
 
+  static int GetSessionCount();
+
   // Represents the default track for the calling thread.
   static const Track kDefaultTrack;
 
  private:
   static protozero::MessageHandle<protos::pbzero::TracePacket> NewTracePacket(
       TraceWriterBase*,
-      uint64_t timestamp,
+      TraceTimestamp,
       uint32_t seq_flags =
           protos::pbzero::TracePacket::SEQ_NEEDS_INCREMENTAL_STATE);
   static protos::pbzero::DebugAnnotation* AddDebugAnnotation(
       perfetto::EventContext*,
       const char* name);
+
+  static std::atomic<int> session_count_;
 };
 
 }  // namespace internal
diff --git a/include/perfetto/tracing/internal/track_event_interned_fields.h b/include/perfetto/tracing/internal/track_event_interned_fields.h
new file mode 100644
index 0000000..81bfd98
--- /dev/null
+++ b/include/perfetto/tracing/internal/track_event_interned_fields.h
@@ -0,0 +1,74 @@
+/*
+ * 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/base/export.h"
+#include "perfetto/tracing/track_event_interned_data_index.h"
+
+#ifndef INCLUDE_PERFETTO_TRACING_INTERNAL_TRACK_EVENT_INTERNED_FIELDS_H_
+#define INCLUDE_PERFETTO_TRACING_INTERNAL_TRACK_EVENT_INTERNED_FIELDS_H_
+
+namespace perfetto {
+namespace internal {
+
+// These helpers are exposed here to allow Chromium-without-client library
+// to share the interning buffers with Perfetto internals (e.g.
+// perfetto::TracedValue implementation).
+
+struct PERFETTO_EXPORT InternedEventCategory
+    : public TrackEventInternedDataIndex<
+          InternedEventCategory,
+          perfetto::protos::pbzero::InternedData::kEventCategoriesFieldNumber,
+          const char*,
+          SmallInternedDataTraits> {
+  ~InternedEventCategory() override;
+
+  static void Add(protos::pbzero::InternedData* interned_data,
+                  size_t iid,
+                  const char* value,
+                  size_t length);
+};
+
+struct PERFETTO_EXPORT InternedEventName
+    : public TrackEventInternedDataIndex<
+          InternedEventName,
+          perfetto::protos::pbzero::InternedData::kEventNamesFieldNumber,
+          const char*,
+          SmallInternedDataTraits> {
+  ~InternedEventName() override;
+
+  static void Add(protos::pbzero::InternedData* interned_data,
+                  size_t iid,
+                  const char* value);
+};
+
+struct PERFETTO_EXPORT InternedDebugAnnotationName
+    : public TrackEventInternedDataIndex<
+          InternedDebugAnnotationName,
+          perfetto::protos::pbzero::InternedData::
+              kDebugAnnotationNamesFieldNumber,
+          const char*,
+          SmallInternedDataTraits> {
+  ~InternedDebugAnnotationName() override;
+
+  static void Add(protos::pbzero::InternedData* interned_data,
+                  size_t iid,
+                  const char* value);
+};
+
+}  // namespace internal
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_TRACING_INTERNAL_TRACK_EVENT_INTERNED_FIELDS_H_
diff --git a/include/perfetto/tracing/internal/write_track_event_args.h b/include/perfetto/tracing/internal/write_track_event_args.h
index 9fff885..2f84878 100644
--- a/include/perfetto/tracing/internal/write_track_event_args.h
+++ b/include/perfetto/tracing/internal/write_track_event_args.h
@@ -19,6 +19,7 @@
 
 #include "perfetto/base/compiler.h"
 #include "perfetto/tracing/event_context.h"
+#include "perfetto/tracing/traced_proto.h"
 
 namespace perfetto {
 namespace internal {
@@ -90,6 +91,31 @@
   WriteTrackEventArgs(std::move(event_ctx), std::forward<Args>(args)...);
 }
 
+// Write one typed message and recursively write the rest of the arguments.
+template <typename FieldMetadataType,
+          typename ArgValue,
+          typename... Args,
+          typename Check = base::enable_if_t<
+              std::is_base_of<protozero::proto_utils::FieldMetadataBase,
+                              FieldMetadataType>::value>>
+PERFETTO_ALWAYS_INLINE void WriteTrackEventArgs(
+    EventContext event_ctx,
+    protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadataType>
+        field_name,
+    ArgValue&& arg_value,
+    Args&&... args) {
+  static_assert(
+      std::is_base_of<protos::pbzero::TrackEvent,
+                      typename FieldMetadataType::message_type>::value,
+      "Only fields of TrackEvent (and TrackEvent's extensions) can "
+      "be passed to TRACE_EVENT");
+  WriteIntoTracedProto(
+      event_ctx.Wrap(
+          event_ctx.event<typename FieldMetadataType::message_type>()),
+      field_name, std::forward<ArgValue>(arg_value));
+  WriteTrackEventArgs(std::move(event_ctx), std::forward<Args>(args)...);
+}
+
 }  // namespace internal
 }  // namespace perfetto
 
diff --git a/include/perfetto/tracing/traced_proto.h b/include/perfetto/tracing/traced_proto.h
new file mode 100644
index 0000000..a887d96
--- /dev/null
+++ b/include/perfetto/tracing/traced_proto.h
@@ -0,0 +1,250 @@
+/*
+ * 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_TRACING_TRACED_PROTO_H_
+#define INCLUDE_PERFETTO_TRACING_TRACED_PROTO_H_
+
+#include "perfetto/base/template_util.h"
+#include "perfetto/protozero/field_writer.h"
+#include "perfetto/protozero/proto_utils.h"
+#include "perfetto/tracing/traced_value.h"
+
+namespace perfetto {
+class EventContext;
+
+// A Wrapper around a protozero message to allow C++ classes to specify how it
+// should be serialised into the trace:
+//
+// class Foo {
+//  public:
+//   void WriteIntoTrace(perfetto::TracedProto<pbzero::Foo> message) {
+//     message->set_int_field(int_field_);
+//   }
+// };
+//
+// This class also exposes EventContext, e.g. to enable data interning.
+//
+// NOTE: the functionality below is not ready yet.
+// TODO(altimin): Make the interop below possible.
+// TracedProto also provides a seamless integration with writing untyped
+// values via TracedValue / TracedDictionary / TracedArray:
+//
+// - TracedValue can be converted to a TracedProto, either by calling
+//   TracedValue::WriteProto<T>() or implicitly.
+// - If a proto message has a repeating DebugAnnotation debug_annotations
+//   field, it can be filled using the TracedDictionary obtained from
+//   TracedProto::AddDebugAnnotations.
+template <typename MessageType>
+class TracedProto {
+ public:
+  TracedProto(const TracedProto&) = delete;
+  TracedProto& operator=(const TracedProto&) = delete;
+  TracedProto& operator=(TracedProto&&) = delete;
+  TracedProto(TracedProto&&) = default;
+  ~TracedProto() = default;
+
+  MessageType* operator->() const { return message_; }
+
+  MessageType* message() { return message_; }
+
+  // Write additional untyped values into the same context, which is useful
+  // when a given C++ class has a typed representation, but also either has
+  // members which can only be written into an untyped context (e.g. they are
+  // autogenerated) or it's desirable to have a way to quickly extend the
+  // trace representation of this class (e.g. for debugging).
+  //
+  // The usage of the returned TracedDictionary should not be interleaved with
+  // writing into |message| as this results in an inefficient proto layout. To
+  // enforce this, AddDebugAnnotations should be called on TracedProto&&, i.e.
+  // std::move(message).AddDebugAnnotations().
+  //
+  // This requires a 'repeated DebugAnnotations debug_annotations' field in
+  // MessageType.
+  template <typename Check = void>
+  TracedDictionary AddDebugAnnotations() && {
+    static_assert(
+        std::is_base_of<
+            protozero::proto_utils::FieldMetadataBase,
+            typename MessageType::FieldMetadata_DebugAnnotations>::value,
+        "This message does not have a |debug_annotations| field. Please add a"
+        "'repeated perfetto.protos.DebugAnnotation debug_annnotations = N;' "
+        "field to your message.");
+    return TracedDictionary(message_, MessageType::kDebugAnnotations, nullptr);
+  }
+
+  // Write a nested message into a field according to the provided metadata.
+  template <typename FieldMetadata>
+  TracedProto<typename FieldMetadata::cpp_field_type> WriteNestedMessage() {
+    static_assert(std::is_base_of<MessageType,
+                                  typename FieldMetadata::message_type>::value,
+                  "Field should belong to the current message");
+    return TracedProto<typename FieldMetadata::cpp_field_type>(
+        message_->template BeginNestedMessage<
+            typename FieldMetadata::cpp_field_type>(FieldMetadata::kFieldId),
+        context_);
+  }
+
+  template <typename FieldMetadata>
+  TracedProto<typename FieldMetadata::cpp_field_type> WriteNestedMessage(
+      protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>) {
+    return WriteNestedMessage<FieldMetadata>();
+  }
+
+ private:
+  friend class EventContext;
+  // Allow TracedProto<Foo> to create TracedProto<Bar>.
+  template <typename T>
+  friend class TracedProto;
+
+  // Wraps a raw protozero message using the same context as the current object.
+  template <typename ChildMessageType>
+  TracedProto<ChildMessageType> Wrap(ChildMessageType* message) {
+    return TracedProto(message, context_);
+  }
+
+  // Context might be null here when writing typed message which is
+  // nested into untyped legacy trace event macro argument.
+  // TODO(altimin): Turn this into EventContext& when this case is eliminated
+  // and expose it in public API.
+  EventContext* context() const { return context_; }
+
+  TracedProto(MessageType* message, EventContext* context)
+      : message_(message), context_(context) {}
+
+  MessageType* const message_;
+  EventContext* context_;
+};
+
+namespace internal {
+
+template <typename FieldMetadata,
+          bool is_message,
+          protozero::proto_utils::RepetitionType repetition_type>
+struct TypedProtoWriterImpl;
+
+// Simple non-repeated field.
+template <typename FieldMetadata>
+struct TypedProtoWriterImpl<
+    FieldMetadata,
+    /*is_message=*/false,
+    protozero::proto_utils::RepetitionType::kNotRepeated> {
+  template <typename Proto, typename ValueType>
+  static void Write(TracedProto<Proto> context, ValueType&& value) {
+    protozero::internal::FieldWriter<FieldMetadata::kProtoFieldType>::Append(
+        *context.message(), FieldMetadata::kFieldId, value);
+  }
+};
+
+// Simple repeated non-packed field.
+template <typename FieldMetadata>
+struct TypedProtoWriterImpl<
+    FieldMetadata,
+    /*is_message=*/false,
+    protozero::proto_utils::RepetitionType::kRepeatedNotPacked> {
+  template <typename Proto, typename ValueType>
+  static void Write(TracedProto<Proto> context, ValueType&& value) {
+    for (auto&& item : value) {
+      protozero::internal::FieldWriter<FieldMetadata::kProtoFieldType>::Append(
+          *context.message(), FieldMetadata::kFieldId, item);
+    }
+  }
+};
+
+// Nested repeated non-packed field.
+template <typename FieldMetadata>
+struct TypedProtoWriterImpl<
+    FieldMetadata,
+    /*is_message=*/true,
+    protozero::proto_utils::RepetitionType::kNotRepeated> {
+  template <typename Proto, typename ValueType>
+  static void Write(TracedProto<Proto> context, ValueType&& value) {
+    // TODO(altimin): support TraceFormatTraits here.
+    value.WriteIntoTrace(context.template WriteNestedMessage<FieldMetadata>());
+  }
+};
+
+// Nested repeated non-packed field.
+template <typename FieldMetadata>
+struct TypedProtoWriterImpl<
+    FieldMetadata,
+    /*is_message=*/true,
+    protozero::proto_utils::RepetitionType::kRepeatedNotPacked> {
+  template <typename Proto, typename ValueType>
+  static void Write(TracedProto<Proto> context, ValueType&& value) {
+    // TODO(altimin): support TraceFormatTraits here.
+    for (auto&& item : value) {
+      item.WriteIntoTrace(context.template WriteNestedMessage<FieldMetadata>());
+    }
+  }
+};
+
+// TypedProtoWriter takes the protozero message (TracedProto<MessageType>),
+// field description (FieldMetadata) and value and writes the given value
+// into the given field of the given protozero message.
+//
+// This is primarily used for inline writing of typed messages:
+// TRACE_EVENT(..., pbzero::Message:kField, value);
+//
+// Ideally we would use a function here and not a struct, but passing template
+// arguments directly to the function (e.g. foo<void>()) isn't supported until
+// C++20, so we have to use a helper struct here.
+template <typename FieldMetadata>
+struct TypedProtoWriter {
+ private:
+  using ProtoSchemaType = protozero::proto_utils::ProtoSchemaType;
+  using RepetitionType = protozero::proto_utils::RepetitionType;
+
+  static_assert(FieldMetadata::kRepetitionType !=
+                    RepetitionType::kRepeatedPacked,
+                "writing packed fields isn't supported yet");
+
+  template <bool is_message, RepetitionType repetition_type>
+  struct Writer;
+
+ public:
+  template <typename Proto, typename ValueType>
+  static void Write(TracedProto<Proto> context, ValueType&& value) {
+    TypedProtoWriterImpl<
+        FieldMetadata,
+        FieldMetadata::kProtoFieldType == ProtoSchemaType::kMessage,
+        FieldMetadata::kRepetitionType>::Write(std::move(context),
+                                               std::forward<ValueType>(value));
+  }
+};
+
+}  // namespace internal
+
+template <typename MessageType, typename FieldMetadataType, typename ValueType>
+void WriteIntoTracedProto(
+    TracedProto<MessageType> message,
+    protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadataType>,
+    ValueType&& value) {
+  static_assert(
+      std::is_base_of<protozero::proto_utils::FieldMetadataBase,
+                      FieldMetadataType>::value,
+      "Field name should be a protozero::internal::FieldMetadata<...>");
+  static_assert(
+      std::is_base_of<MessageType,
+                      typename FieldMetadataType::message_type>::value,
+      "Field's parent type should match the context.");
+
+  internal::TypedProtoWriter<FieldMetadataType>::Write(
+      std::move(message), std::forward<ValueType>(value));
+}
+
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_TRACING_TRACED_PROTO_H_
diff --git a/include/perfetto/tracing/traced_value.h b/include/perfetto/tracing/traced_value.h
index b7b62d6..53f1cb6 100644
--- a/include/perfetto/tracing/traced_value.h
+++ b/include/perfetto/tracing/traced_value.h
@@ -20,16 +20,24 @@
 #include "perfetto/base/compiler.h"
 #include "perfetto/base/export.h"
 #include "perfetto/base/template_util.h"
+#include "perfetto/protozero/message.h"
+#include "perfetto/protozero/proto_utils.h"
 #include "perfetto/tracing/internal/checked_scope.h"
 #include "perfetto/tracing/string_helpers.h"
 #include "perfetto/tracing/traced_value_forward.h"
-#include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 
+#include <memory>
 #include <type_traits>
 #include <utility>
 
 namespace perfetto {
 
+namespace protos {
+namespace pbzero {
+class DebugAnnotation;
+}
+}  // namespace protos
+
 class DebugAnnotation;
 
 // These classes provide a JSON-inspired way to write structed data into traces.
@@ -49,9 +57,9 @@
 //
 // To define how a custom class should be written into the trace, users should
 // define one of the two following functions:
-// - Foo::WriteIntoTracedValue(TracedValue) const
+// - Foo::WriteIntoTrace(TracedValue) const
 //   (preferred for code which depends on perfetto directly)
-// - perfetto::TraceFormatTraits<T>::WriteIntoTracedValue(
+// - perfetto::TraceFormatTraits<T>::WriteIntoTrace(
 //       TracedValue, const T&);
 //   (should be used if T is defined in a library which doesn't know anything
 //   about tracing).
@@ -81,17 +89,17 @@
 // });
 //
 // template <typename T>
-// TraceFormatTraits<std::optional<T>>::WriteIntoTracedValue(
+// TraceFormatTraits<std::optional<T>>::WriteIntoTrace(
 //    TracedValue context, const std::optional<T>& value) {
 //  if (!value) {
 //    std::move(context).WritePointer(nullptr);
 //    return;
 //  }
-//  perfetto::WriteIntoTracedValue(std::move(context), *value);
+//  perfetto::WriteIntoTrace(std::move(context), *value);
 // }
 //
 // template <typename T>
-// TraceFormatTraits<std::vector<T>>::WriteIntoTracedValue(
+// TraceFormatTraits<std::vector<T>>::WriteIntoTrace(
 //    TracedValue context, const std::array<T>& value) {
 //  auto array = std::move(context).WriteArray();
 //  for (const auto& item: value) {
@@ -100,7 +108,7 @@
 // }
 //
 // class Foo {
-//   void WriteIntoTracedValue(TracedValue context) const {
+//   void WriteIntoTrace(TracedValue context) const {
 //     auto dict = std::move(context).WriteDictionary();
 //     dict->Set("key", 42);
 //     dict->Set("foo", "bar");
@@ -160,24 +168,16 @@
 
   static TracedValue CreateFromProto(protos::pbzero::DebugAnnotation*);
 
-  inline explicit TracedValue(protos::pbzero::DebugAnnotation* root_context,
+  inline explicit TracedValue(protos::pbzero::DebugAnnotation* context,
                               internal::CheckedScope* parent_scope)
-      : root_context_(root_context), checked_scope_(parent_scope) {}
-  inline explicit TracedValue(
-      protos::pbzero::DebugAnnotation::NestedValue* nested_context,
-      internal::CheckedScope* parent_scope)
-      : nested_context_(nested_context), checked_scope_(parent_scope) {}
+      : context_(context), checked_scope_(parent_scope) {}
 
   // Temporary support for perfetto::DebugAnnotation C++ class before it's going
   // to be replaced by TracedValue.
   // TODO(altimin): Convert v8 to use TracedValue directly and delete it.
   friend class DebugAnnotation;
 
-  // Only one of them can be null.
-  // TODO(altimin): replace DebugAnnotation with something that doesn't require
-  // this duplication.
-  protos::pbzero::DebugAnnotation* root_context_ = nullptr;
-  protos::pbzero::DebugAnnotation::NestedValue* nested_context_ = nullptr;
+  protos::pbzero::DebugAnnotation* const context_ = nullptr;
 
   internal::CheckedScope checked_scope_;
 };
@@ -203,12 +203,11 @@
  private:
   friend class TracedValue;
 
-  inline explicit TracedArray(
-      protos::pbzero::DebugAnnotation::NestedValue* value,
-      internal::CheckedScope* parent_scope)
-      : value_(value), checked_scope_(parent_scope) {}
+  inline explicit TracedArray(protos::pbzero::DebugAnnotation* context,
+                              internal::CheckedScope* parent_scope)
+      : context_(context), checked_scope_(parent_scope) {}
 
-  protos::pbzero::DebugAnnotation::NestedValue* value_;
+  protos::pbzero::DebugAnnotation* context_;
 
   internal::CheckedScope checked_scope_;
 };
@@ -249,13 +248,39 @@
 
  private:
   friend class TracedValue;
+  template <typename T>
+  friend class TracedProto;
 
+  // Create a |TracedDictionary| which will populate the given field of the
+  // given |message|.
+  template <typename MessageType, typename FieldMetadata>
   inline explicit TracedDictionary(
-      protos::pbzero::DebugAnnotation::NestedValue* value,
+      MessageType* message,
+      protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
       internal::CheckedScope* parent_scope)
-      : value_(value), checked_scope_(parent_scope) {}
+      : message_(message),
+        field_id_(FieldMetadata::kFieldId),
+        checked_scope_(parent_scope) {
+    static_assert(std::is_base_of<protozero::Message, MessageType>::value,
+                  "Message should be a subclass of protozero::Message");
+    static_assert(std::is_base_of<protozero::proto_utils::FieldMetadataBase,
+                                  FieldMetadata>::value,
+                  "FieldMetadata should be a subclass of FieldMetadataBase");
+    static_assert(
+        std::is_same<typename FieldMetadata::message_type, MessageType>::value,
+        "Field does not belong to this message");
+    static_assert(
+        std::is_same<typename FieldMetadata::cpp_field_type,
+                     ::perfetto::protos::pbzero::DebugAnnotation>::value,
+        "Field should be of DebugAnnotation type");
+    static_assert(
+        FieldMetadata::kRepetitionType ==
+            protozero::proto_utils::RepetitionType::kRepeatedNotPacked,
+        "Field should be non-packed repeated");
+  }
 
-  protos::pbzero::DebugAnnotation::NestedValue* value_;
+  protozero::Message* const message_;
+  const uint32_t field_id_;
 
   internal::CheckedScope checked_scope_;
 };
@@ -275,6 +300,13 @@
   value.WriteIntoTracedValue(std::move(context));
 }
 
+// If T has WriteIntoTrace member function, call it.
+template <typename T>
+decltype(std::declval<T>().WriteIntoTrace(std::declval<TracedValue>()), void())
+WriteImpl(base::priority_tag<4>, TracedValue context, T&& value) {
+  value.WriteIntoTrace(std::move(context));
+}
+
 // If perfetto::TraceFormatTraits<T>::WriteIntoTracedValue(TracedValue, const
 // T&) is available, use it.
 template <typename T>
@@ -287,6 +319,18 @@
       std::move(context), std::forward<T>(value));
 }
 
+// If perfetto::TraceFormatTraits<T>::WriteIntoTrace(TracedValue, const T&)
+// is available, use it.
+template <typename T>
+decltype(TraceFormatTraits<base::remove_cvref_t<T>>::WriteIntoTrace(
+             std::declval<TracedValue>(),
+             std::declval<T>()),
+         void())
+WriteImpl(base::priority_tag<3>, TracedValue context, T&& value) {
+  TraceFormatTraits<base::remove_cvref_t<T>>::WriteIntoTrace(
+      std::move(context), std::forward<T>(value));
+}
+
 // If T has operator(), which takes TracedValue, use it.
 // Very useful for lambda resolutions.
 template <typename T>
@@ -439,7 +483,7 @@
     typename std::enable_if<std::is_integral<T>::value &&
                             !std::is_same<T, bool>::value &&
                             std::is_signed<T>::value>::type> {
-  inline static void WriteIntoTracedValue(TracedValue context, T value) {
+  inline static void WriteIntoTrace(TracedValue context, T value) {
     std::move(context).WriteInt64(value);
   }
 };
@@ -452,7 +496,7 @@
     typename std::enable_if<std::is_integral<T>::value &&
                             !std::is_same<T, bool>::value &&
                             std::is_unsigned<T>::value>::type> {
-  inline static void WriteIntoTracedValue(TracedValue context, T value) {
+  inline static void WriteIntoTrace(TracedValue context, T value) {
     std::move(context).WriteUInt64(value);
   }
 };
@@ -460,7 +504,7 @@
 // Specialisation for bools.
 template <>
 struct TraceFormatTraits<bool> {
-  inline static void WriteIntoTracedValue(TracedValue context, bool value) {
+  inline static void WriteIntoTrace(TracedValue context, bool value) {
     std::move(context).WriteBoolean(value);
   }
 };
@@ -470,7 +514,7 @@
 struct TraceFormatTraits<
     T,
     typename std::enable_if<std::is_floating_point<T>::value>::type> {
-  inline static void WriteIntoTracedValue(TracedValue context, T value) {
+  inline static void WriteIntoTrace(TracedValue context, T value) {
     std::move(context).WriteDouble(static_cast<double>(value));
   }
 };
@@ -483,7 +527,7 @@
         std::is_enum<T>::value &&
         std::is_signed<
             typename internal::safe_underlying_type<T>::type>::value>::type> {
-  inline static void WriteIntoTracedValue(TracedValue context, T value) {
+  inline static void WriteIntoTrace(TracedValue context, T value) {
     std::move(context).WriteInt64(static_cast<int64_t>(value));
   }
 };
@@ -496,7 +540,7 @@
         std::is_enum<T>::value &&
         std::is_unsigned<
             typename internal::safe_underlying_type<T>::type>::value>::type> {
-  inline static void WriteIntoTracedValue(TracedValue context, T value) {
+  inline static void WriteIntoTrace(TracedValue context, T value) {
     std::move(context).WriteUInt64(static_cast<uint64_t>(value));
   }
 };
@@ -504,24 +548,21 @@
 // Specialisations for C-style strings.
 template <>
 struct TraceFormatTraits<const char*> {
-  inline static void WriteIntoTracedValue(TracedValue context,
-                                          const char* value) {
+  inline static void WriteIntoTrace(TracedValue context, const char* value) {
     std::move(context).WriteString(value);
   }
 };
 
 template <>
 struct TraceFormatTraits<char[]> {
-  inline static void WriteIntoTracedValue(TracedValue context,
-                                          const char value[]) {
+  inline static void WriteIntoTrace(TracedValue context, const char value[]) {
     std::move(context).WriteString(value);
   }
 };
 
 template <size_t N>
 struct TraceFormatTraits<char[N]> {
-  inline static void WriteIntoTracedValue(TracedValue context,
-                                          const char value[N]) {
+  inline static void WriteIntoTrace(TracedValue context, const char value[N]) {
     std::move(context).WriteString(value);
   }
 };
@@ -529,8 +570,8 @@
 // Specialisation for C++ strings.
 template <>
 struct TraceFormatTraits<std::string> {
-  inline static void WriteIntoTracedValue(TracedValue context,
-                                          const std::string& value) {
+  inline static void WriteIntoTrace(TracedValue context,
+                                    const std::string& value) {
     std::move(context).WriteString(value);
   }
 };
@@ -538,15 +579,14 @@
 // Specialisation for (const) void*, which writes the pointer value.
 template <>
 struct TraceFormatTraits<void*> {
-  inline static void WriteIntoTracedValue(TracedValue context, void* value) {
+  inline static void WriteIntoTrace(TracedValue context, void* value) {
     std::move(context).WritePointer(value);
   }
 };
 
 template <>
 struct TraceFormatTraits<const void*> {
-  inline static void WriteIntoTracedValue(TracedValue context,
-                                          const void* value) {
+  inline static void WriteIntoTrace(TracedValue context, const void* value) {
     std::move(context).WritePointer(value);
   }
 };
@@ -555,8 +595,8 @@
 // object it points to.
 template <typename T>
 struct TraceFormatTraits<std::unique_ptr<T>, check_traced_value_support_t<T>> {
-  inline static void WriteIntoTracedValue(TracedValue context,
-                                          const std::unique_ptr<T>& value) {
+  inline static void WriteIntoTrace(TracedValue context,
+                                    const std::unique_ptr<T>& value) {
     ::perfetto::WriteIntoTracedValue(std::move(context), value.get());
   }
 };
@@ -565,7 +605,7 @@
 // points to.
 template <typename T>
 struct TraceFormatTraits<T*, check_traced_value_support_t<T>> {
-  inline static void WriteIntoTracedValue(TracedValue context, T* value) {
+  inline static void WriteIntoTrace(TracedValue context, T* value) {
     if (!value) {
       std::move(context).WritePointer(nullptr);
       return;
@@ -577,7 +617,7 @@
 // Specialisation for nullptr.
 template <>
 struct TraceFormatTraits<std::nullptr_t> {
-  inline static void WriteIntoTracedValue(TracedValue context, std::nullptr_t) {
+  inline static void WriteIntoTrace(TracedValue context, std::nullptr_t) {
     std::move(context).WritePointer(nullptr);
   }
 };
diff --git a/include/perfetto/tracing/tracing.h b/include/perfetto/tracing/tracing.h
index 0b7fe6e..8a688f2 100644
--- a/include/perfetto/tracing/tracing.h
+++ b/include/perfetto/tracing/tracing.h
@@ -63,6 +63,10 @@
   }
 };
 
+using LogLev = ::perfetto::base::LogLev;
+using LogMessageCallbackArgs = ::perfetto::base::LogMessageCallbackArgs;
+using LogMessageCallback = ::perfetto::base::LogMessageCallback;
+
 struct TracingInitArgs {
   uint32_t backends = 0;                     // One or more BackendTypes.
   TracingBackend* custom_backend = nullptr;  // [Optional].
@@ -107,6 +111,10 @@
   // lifetime of the process.
   TracingPolicy* tracing_policy = nullptr;
 
+  // [Optional] If set, log messages generated by perfetto are passed to this
+  // callback instead of being logged directly.
+  LogMessageCallback log_message_callback = nullptr;
+
  protected:
   friend class Tracing;
   friend class internal::TracingMuxerImpl;
diff --git a/include/perfetto/tracing/track.h b/include/perfetto/tracing/track.h
index 7ac5271..0ff225d 100644
--- a/include/perfetto/tracing/track.h
+++ b/include/perfetto/tracing/track.h
@@ -100,6 +100,17 @@
   // accidental clashes with track identifiers emitted by other producers.
   static Track Global(uint64_t id) { return Track(id, Track()); }
 
+  // Construct a track using |ptr| as identifier.
+  static Track FromPointer(const void* ptr, Track parent = MakeProcessTrack()) {
+    // Using pointers as global TrackIds isn't supported as pointers are
+    // per-proccess and the same pointer value can be used in different
+    // processes.
+    PERFETTO_DCHECK(parent.uuid != Track().uuid);
+
+    return Track(static_cast<uint64_t>(reinterpret_cast<uintptr_t>(ptr)),
+                 parent);
+  }
+
  protected:
   constexpr Track(uint64_t uuid_, uint64_t parent_uuid_)
       : uuid(uuid_), parent_uuid(parent_uuid_) {}
diff --git a/include/perfetto/tracing/track_event_legacy.h b/include/perfetto/tracing/track_event_legacy.h
index 6555f8f..cd86510 100644
--- a/include/perfetto/tracing/track_event_legacy.h
+++ b/include/perfetto/tracing/track_event_legacy.h
@@ -177,20 +177,14 @@
 //   #define TRACE_TIME_TICKS_NOW() ...
 //   #define TRACE_TIME_NOW() ...
 
-// User-provided function to convert an abstract thread id into either a track
-// uuid or a pid/tid override. Return true if the conversion succeeded.
+// User-provided function to convert an abstract thread id into a thread track.
 template <typename T>
-bool ConvertThreadId(const T&,
-                     uint64_t* track_uuid_out,
-                     int32_t* pid_override_out,
-                     int32_t* tid_override_out);
+ThreadTrack ConvertThreadId(const T&);
 
 // Built-in implementation for events referring to the current thread.
 template <>
-bool PERFETTO_EXPORT ConvertThreadId(const PerfettoLegacyCurrentThreadId&,
-                                     uint64_t*,
-                                     int32_t*,
-                                     int32_t*);
+ThreadTrack PERFETTO_EXPORT
+ConvertThreadId(const PerfettoLegacyCurrentThreadId&);
 
 }  // namespace legacy
 
@@ -256,7 +250,7 @@
     uint32_t id_flags_ = legacy::kTraceEventFlagHasId;
   };
 
-  LegacyTraceId(const void* raw_id)
+  explicit LegacyTraceId(const void* raw_id)
       : raw_id_(static_cast<uint64_t>(reinterpret_cast<uintptr_t>(raw_id))) {
     id_flags_ = legacy::kTraceEventFlagHasLocalId;
   }
@@ -332,7 +326,6 @@
                                uint32_t flags,
                                Args&&... args) PERFETTO_NO_INLINE {
     AddDebugAnnotations(&ctx, std::forward<Args>(args)...);
-    SetTrackIfNeeded(&ctx, phase, flags);
     if (NeedLegacyFlags(phase, flags)) {
       auto legacy_event = ctx.event()->set_legacy_event();
       SetLegacyFlags(legacy_event, phase, flags);
@@ -352,38 +345,28 @@
     // 1. If we have an id, we need to write {unscoped,local,global}_id and/or
     //    bind_id.
     // 2. If we have a thread id, we need to write track_uuid() or
-    //    {pid,tid}_override. This happens in embedder code since the thread id
-    //    is embedder-specified.
+    //    {pid,tid}_override if the id represents another process.  The
+    //    conversion from |thread_id| happens in embedder code since the type is
+    //    embedder-specified.
     // 3. If we have a timestamp, we need to write a different timestamp in the
     //    trace packet itself and make sure TrackEvent won't write one
     //    internally. This is already done at the call site.
     //
     flags |= id.id_flags();
     AddDebugAnnotations(&ctx, std::forward<Args>(args)...);
-    int32_t pid_override = 0;
-    int32_t tid_override = 0;
-    uint64_t track_uuid = 0;
-    if (legacy::ConvertThreadId(thread_id, &track_uuid, &pid_override,
-                                &tid_override) &&
-        track_uuid) {
-      if (track_uuid != ThreadTrack::Current().uuid)
-        ctx.event()->set_track_uuid(track_uuid);
-    } else if (pid_override || tid_override) {
-      // Explicitly clear the track so the overrides below take effect.
-      ctx.event()->set_track_uuid(0);
-    } else {
-      // No pid/tid/track overrides => obey the flags instead.
-      SetTrackIfNeeded(&ctx, phase, flags);
-    }
-    if (NeedLegacyFlags(phase, flags) || pid_override || tid_override) {
+    if (NeedLegacyFlags(phase, flags)) {
       auto legacy_event = ctx.event()->set_legacy_event();
       SetLegacyFlags(legacy_event, phase, flags);
       if (id.id_flags())
         id.Write(legacy_event, flags);
-      if (pid_override)
+      if (flags & TRACE_EVENT_FLAG_HAS_PROCESS_ID) {
+        // The thread identifier actually represents a process id. Let's set an
+        // override for it.
+        int32_t pid_override =
+            static_cast<int32_t>(legacy::ConvertThreadId(thread_id).tid);
         legacy_event->set_pid_override(pid_override);
-      if (tid_override)
-        legacy_event->set_tid_override(tid_override);
+        legacy_event->set_tid_override(-1);
+      }
     }
   }
 
@@ -410,40 +393,19 @@
   }
 
  private:
-  static void SetTrackIfNeeded(EventContext* ctx, char phase, uint32_t flags) {
-    // Scopes are only relevant for instant events.
-    if (phase != TRACE_EVENT_PHASE_INSTANT)
-      return;
-    // Note: This avoids the need to set LegacyEvent::instant_event_scope.
-    auto scope = flags & TRACE_EVENT_FLAG_SCOPE_MASK;
-    switch (scope) {
-      case TRACE_EVENT_SCOPE_GLOBAL:
-        ctx->event()->set_track_uuid(0);
-        break;
-      case TRACE_EVENT_SCOPE_PROCESS:
-        ctx->event()->set_track_uuid(ProcessTrack::Current().uuid);
-        break;
-      default:
-      case TRACE_EVENT_SCOPE_THREAD:
-        // Thread scope is already the default.
-        break;
-    }
-  }
-
   static bool NeedLegacyFlags(char phase, uint32_t flags) {
     if (PhaseToType(phase) == protos::pbzero::TrackEvent::TYPE_UNSPECIFIED)
       return true;
     // TODO(skyostil): Implement/deprecate:
     // - TRACE_EVENT_FLAG_EXPLICIT_TIMESTAMP
     // - TRACE_EVENT_FLAG_HAS_CONTEXT_ID
-    // - TRACE_EVENT_FLAG_HAS_PROCESS_ID
     // - TRACE_EVENT_FLAG_TYPED_PROTO_ARGS
     // - TRACE_EVENT_FLAG_JAVA_STRING_LITERALS
     return flags &
            (TRACE_EVENT_FLAG_HAS_ID | TRACE_EVENT_FLAG_HAS_LOCAL_ID |
             TRACE_EVENT_FLAG_HAS_GLOBAL_ID | TRACE_EVENT_FLAG_ASYNC_TTS |
             TRACE_EVENT_FLAG_BIND_TO_ENCLOSING | TRACE_EVENT_FLAG_FLOW_IN |
-            TRACE_EVENT_FLAG_FLOW_OUT);
+            TRACE_EVENT_FLAG_FLOW_OUT | TRACE_EVENT_FLAG_HAS_PROCESS_ID);
   }
 
   static void SetLegacyFlags(
@@ -478,15 +440,66 @@
 
 // Implementations for the INTERNAL_* adapter macros used by the trace points
 // below.
-#define INTERNAL_TRACE_EVENT_ADD(phase, category, name, flags, ...)          \
-  PERFETTO_INTERNAL_TRACK_EVENT(                                             \
-      category,                                                              \
-      ::perfetto::internal::GetStaticString(::perfetto::StaticString{name}), \
-      ::perfetto::internal::TrackEventLegacy::PhaseToType(phase),            \
-      [&](perfetto::EventContext ctx) PERFETTO_NO_THREAD_SAFETY_ANALYSIS {   \
-        using ::perfetto::internal::TrackEventLegacy;                        \
-        TrackEventLegacy::WriteLegacyEvent(std::move(ctx), phase, flags,     \
-                                           ##__VA_ARGS__);                   \
+#define PERFETTO_INTERNAL_LEGACY_EVENT_ON_TRACK(phase, category, name, track, \
+                                                ...)                          \
+  PERFETTO_INTERNAL_TRACK_EVENT(                                              \
+      category,                                                               \
+      ::perfetto::internal::GetStaticString(::perfetto::StaticString{name}),  \
+      ::perfetto::internal::TrackEventLegacy::PhaseToType(phase), track,      \
+      ##__VA_ARGS__);
+
+// The main entrypoint for writing unscoped legacy events.  This macro
+// determines the right track to write the event on based on |flags| and
+// |thread_id|.
+#define PERFETTO_INTERNAL_LEGACY_EVENT(phase, category, name, flags,         \
+                                       thread_id, ...)                       \
+  [&]() {                                                                    \
+    constexpr auto& kDefaultTrack =                                          \
+        ::perfetto::internal::TrackEventInternal::kDefaultTrack;             \
+    /* First check the scope for instant events. */                          \
+    if ((phase) == TRACE_EVENT_PHASE_INSTANT) {                              \
+      /* Note: Avoids the need to set LegacyEvent::instant_event_scope. */   \
+      auto scope = (flags)&TRACE_EVENT_FLAG_SCOPE_MASK;                      \
+      switch (scope) {                                                       \
+        case TRACE_EVENT_SCOPE_GLOBAL:                                       \
+          PERFETTO_INTERNAL_LEGACY_EVENT_ON_TRACK(                           \
+              phase, category, name, ::perfetto::Track::Global(0),           \
+              ##__VA_ARGS__);                                                \
+          return;                                                            \
+        case TRACE_EVENT_SCOPE_PROCESS:                                      \
+          PERFETTO_INTERNAL_LEGACY_EVENT_ON_TRACK(                           \
+              phase, category, name, ::perfetto::ProcessTrack::Current(),    \
+              ##__VA_ARGS__);                                                \
+          return;                                                            \
+        default:                                                             \
+        case TRACE_EVENT_SCOPE_THREAD:                                       \
+          /* Fallthrough. */                                                 \
+          break;                                                             \
+      }                                                                      \
+    }                                                                        \
+    /* If an event targets the current thread or another process, write      \
+     * it on the current thread's track. The process override case is        \
+     * handled through |pid_override| in WriteLegacyEvent. */                \
+    if (std::is_same<                                                        \
+            decltype(thread_id),                                             \
+            ::perfetto::legacy::PerfettoLegacyCurrentThreadId>::value ||     \
+        ((flags)&TRACE_EVENT_FLAG_HAS_PROCESS_ID)) {                         \
+      PERFETTO_INTERNAL_LEGACY_EVENT_ON_TRACK(phase, category, name,         \
+                                              kDefaultTrack, ##__VA_ARGS__); \
+    } else {                                                                 \
+      PERFETTO_INTERNAL_LEGACY_EVENT_ON_TRACK(                               \
+          phase, category, name,                                             \
+          ::perfetto::legacy::ConvertThreadId(thread_id), ##__VA_ARGS__);    \
+    }                                                                        \
+  }()
+
+#define INTERNAL_TRACE_EVENT_ADD(phase, category, name, flags, ...)        \
+  PERFETTO_INTERNAL_LEGACY_EVENT(                                          \
+      phase, category, name, flags, ::perfetto::legacy::kCurrentThreadId,  \
+      [&](perfetto::EventContext ctx) PERFETTO_NO_THREAD_SAFETY_ANALYSIS { \
+        using ::perfetto::internal::TrackEventLegacy;                      \
+        TrackEventLegacy::WriteLegacyEvent(std::move(ctx), phase, flags,   \
+                                           ##__VA_ARGS__);                 \
       })
 
 // PERFETTO_INTERNAL_SCOPED_TRACK_EVENT does not require GetStaticString, as it
@@ -514,24 +527,21 @@
             ##__VA_ARGS__);                                                  \
       })
 
-#define INTERNAL_TRACE_EVENT_ADD_WITH_TIMESTAMP(phase, category, name,       \
-                                                timestamp, flags, ...)       \
-  PERFETTO_INTERNAL_TRACK_EVENT(                                             \
-      category,                                                              \
-      ::perfetto::internal::GetStaticString(::perfetto::StaticString{name}), \
-      ::perfetto::internal::TrackEventLegacy::PhaseToType(phase), timestamp, \
-      [&](perfetto::EventContext ctx) PERFETTO_NO_THREAD_SAFETY_ANALYSIS {   \
-        using ::perfetto::internal::TrackEventLegacy;                        \
-        TrackEventLegacy::WriteLegacyEvent(std::move(ctx), phase, flags,     \
-                                           ##__VA_ARGS__);                   \
+#define INTERNAL_TRACE_EVENT_ADD_WITH_TIMESTAMP(phase, category, name,     \
+                                                timestamp, flags, ...)     \
+  PERFETTO_INTERNAL_LEGACY_EVENT(                                          \
+      phase, category, name, flags, ::perfetto::legacy::kCurrentThreadId,  \
+      timestamp,                                                           \
+      [&](perfetto::EventContext ctx) PERFETTO_NO_THREAD_SAFETY_ANALYSIS { \
+        using ::perfetto::internal::TrackEventLegacy;                      \
+        TrackEventLegacy::WriteLegacyEvent(std::move(ctx), phase, flags,   \
+                                           ##__VA_ARGS__);                 \
       })
 
 #define INTERNAL_TRACE_EVENT_ADD_WITH_ID_TID_AND_TIMESTAMP(                  \
     phase, category, name, id, thread_id, timestamp, flags, ...)             \
-  PERFETTO_INTERNAL_TRACK_EVENT(                                             \
-      category,                                                              \
-      ::perfetto::internal::GetStaticString(::perfetto::StaticString{name}), \
-      ::perfetto::internal::TrackEventLegacy::PhaseToType(phase), timestamp, \
+  PERFETTO_INTERNAL_LEGACY_EVENT(                                            \
+      phase, category, name, flags, thread_id, timestamp,                    \
       [&](perfetto::EventContext ctx) PERFETTO_NO_THREAD_SAFETY_ANALYSIS {   \
         using ::perfetto::internal::TrackEventLegacy;                        \
         ::perfetto::internal::LegacyTraceId PERFETTO_UID(trace_id){id};      \
@@ -540,18 +550,16 @@
             ##__VA_ARGS__);                                                  \
       })
 
-#define INTERNAL_TRACE_EVENT_ADD_WITH_ID(phase, category, name, id, flags,   \
-                                         ...)                                \
-  PERFETTO_INTERNAL_TRACK_EVENT(                                             \
-      category,                                                              \
-      ::perfetto::internal::GetStaticString(::perfetto::StaticString{name}), \
-      ::perfetto::internal::TrackEventLegacy::PhaseToType(phase),            \
-      [&](perfetto::EventContext ctx) PERFETTO_NO_THREAD_SAFETY_ANALYSIS {   \
-        using ::perfetto::internal::TrackEventLegacy;                        \
-        ::perfetto::internal::LegacyTraceId PERFETTO_UID(trace_id){id};      \
-        TrackEventLegacy::WriteLegacyEventWithIdAndTid(                      \
-            std::move(ctx), phase, flags, PERFETTO_UID(trace_id),            \
-            TRACE_EVENT_API_CURRENT_THREAD_ID, ##__VA_ARGS__);               \
+#define INTERNAL_TRACE_EVENT_ADD_WITH_ID(phase, category, name, id, flags, \
+                                         ...)                              \
+  PERFETTO_INTERNAL_LEGACY_EVENT(                                          \
+      phase, category, name, flags, ::perfetto::legacy::kCurrentThreadId,  \
+      [&](perfetto::EventContext ctx) PERFETTO_NO_THREAD_SAFETY_ANALYSIS { \
+        using ::perfetto::internal::TrackEventLegacy;                      \
+        ::perfetto::internal::LegacyTraceId PERFETTO_UID(trace_id){id};    \
+        TrackEventLegacy::WriteLegacyEventWithIdAndTid(                    \
+            std::move(ctx), phase, flags, PERFETTO_UID(trace_id),          \
+            TRACE_EVENT_API_CURRENT_THREAD_ID, ##__VA_ARGS__);             \
       })
 
 #define INTERNAL_TRACE_EVENT_METADATA_ADD(category, name, ...)         \
@@ -1155,10 +1163,18 @@
   } while (0)
 
 // Macro to efficiently determine, through polling, if a new trace has begun.
-// TODO(skyostil): Implement.
-#define TRACE_EVENT_IS_NEW_TRACE(ret) \
-  do {                                \
-    *ret = false;                     \
+#define TRACE_EVENT_IS_NEW_TRACE(ret)                                \
+  do {                                                               \
+    static int PERFETTO_UID(prev) = -1;                              \
+    int PERFETTO_UID(curr) =                                         \
+        ::perfetto::internal::TrackEventInternal::GetSessionCount(); \
+    if (::PERFETTO_TRACK_EVENT_NAMESPACE::TrackEvent::IsEnabled() && \
+        (PERFETTO_UID(prev) != PERFETTO_UID(curr))) {                \
+      *(ret) = true;                                                 \
+      PERFETTO_UID(prev) = PERFETTO_UID(curr);                       \
+    } else {                                                         \
+      *(ret) = false;                                                \
+    }                                                                \
   } while (0)
 
 // ----------------------------------------------------------------------------
diff --git a/infra/ci/Makefile b/infra/ci/Makefile
index d4a3e16..09c57a0 100644
--- a/infra/ci/Makefile
+++ b/infra/ci/Makefile
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-include $(shell python config.py makefile)
+include $(shell python3 config.py makefile)
 
 override COMMON_DEPS := Makefile *.py
 override GCE_LOCAL_STARTUP_SCRIPT := worker/gce-startup-script.sh
@@ -142,7 +142,7 @@
 .PHONY: cli
 cli:
 	GOOGLE_APPLICATION_CREDENTIALS=test-credentials.json \
-	python -i -c 'from common_utils import *; from config import *; \
+	python3 -i -c 'from common_utils import *; from config import *; \
 		SCOPES += ["https://www.googleapis.com/auth/firebase.database", \
 								"https://www.googleapis.com/auth/userinfo.email", \
 								"https://www.googleapis.com/auth/datastore"]'
diff --git a/infra/ci/config.py b/infra/ci/config.py
index f35178e..be325ab 100755
--- a/infra/ci/config.py
+++ b/infra/ci/config.py
@@ -145,6 +145,7 @@
 
   if len(sys.argv) > 1 and sys.argv[1] == 'js':
     jsn = json.dumps(vars, indent=2)
-    print('// Auto-generated by %s, do not edit.\n' % __file__)
+    print('// Auto-generated by %s, do not edit.\n' %
+          os.path.basename(__file__))
     print('\'use strict\';\n')
     print('const cfg = JSON.parse(`%s`);\n' % jsn.replace(r'\"', r'\\\"'))
diff --git a/infra/ci/frontend/Makefile b/infra/ci/frontend/Makefile
index 14431aa..400b5eb 100644
--- a/infra/ci/frontend/Makefile
+++ b/infra/ci/frontend/Makefile
@@ -36,26 +36,26 @@
 static/config.js: ../config.py
 	../config.py js > $@
 
-static/third_party/xterm-3.14.4.min.css:
-	curl -Sso $@ https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.4/xterm.min.css
-	echo "ad80f73df001c943cfcd98d706dba050704f715d  $@" | ${SHASUM} -c || rm $@
+static/third_party/xterm-4.11.0.min.css:
+	curl -Sso $@ https://unpkg.com/xterm@4.11.0/css/xterm.css
+	echo "4cee0ccb82803888905fefc8641f0613c0a9081b  $@" | ${SHASUM} -c || rm $@
 
-static/third_party/xterm-3.14.4.min.js:
-	curl -Sso $@ https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.4/xterm.min.js
-	echo "9a92b3fbb118fd2a672f7eb4e69598384ca91756  $@" | ${SHASUM} -c || rm $@
+static/third_party/xterm-4.11.0.min.js:
+	curl -Sso $@ https://unpkg.com/xterm@4.11.0/lib/xterm.js
+	echo "470644a5cd3e2a443a422c6ecddc85b37a85f329  $@" | ${SHASUM} -c || rm $@
 
-static/third_party/xterm-3.14.4-addon-search.min.js:
-	curl -Sso $@ https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.4/addons/search/search.min.js
-	echo "73f55082d00c98b372cac1264ba9da70cdf603d0  $@" | ${SHASUM} -c || rm $@
+static/third_party/xterm-0.8.0-addon-search.min.js:
+	curl -Sso $@ https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-search.js
+	echo "1eae1371cc07677c38f9d01eae4d6a3b996388f8  $@" | ${SHASUM} -c || rm $@
 
-static/third_party/xterm-3.14.4-addon-fit.min.js:
-	curl -Sso $@ https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.4/addons/fit/fit.min.js
-	echo "64835b1b71e8ca2d5bbb1a8e3c7f8a8f1edb2e5c  $@" | ${SHASUM} -c || rm $@
+static/third_party/xterm-0.5.0-addon-fit.min.js:
+	curl -Sso $@ https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js
+	echo "4c8942910554242336443126e349ac1e3897a64e  $@" | ${SHASUM} -c || rm $@
 
 static/third_party/mithril-1.1.6.min.js:
 	curl -Sso $@ https://cdnjs.cloudflare.com/ajax/libs/mithril/1.1.6/mithril.min.js
 	echo "a204c02ee15c347cf368c3481bdea967b443c8d0  $@" | ${SHASUM} -c || rm $@
 
-static_3p: static/third_party/xterm-3.14.4.min.css static/third_party/xterm-3.14.4.min.js static/third_party/xterm-3.14.4-addon-search.min.js static/third_party/xterm-3.14.4-addon-fit.min.js static/third_party/mithril-1.1.6.min.js
+static_3p: static/third_party/mithril-1.1.6.min.js static/third_party/xterm-4.11.0.min.css static/third_party/xterm-4.11.0.min.js static/third_party/xterm-0.8.0-addon-search.min.js static/third_party/xterm-0.5.0-addon-fit.min.js
 
 .PHONY: test deploy static_3p
diff --git a/infra/ci/frontend/static/index.html b/infra/ci/frontend/static/index.html
index 7c67790..f1995bc 100644
--- a/infra/ci/frontend/static/index.html
+++ b/infra/ci/frontend/static/index.html
@@ -21,7 +21,7 @@
     <title>Perfetto CI</title>
     <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
     <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:100,300,400" rel="stylesheet">
-    <link rel="stylesheet" type="text/css" href="/static/third_party/xterm-3.14.4.min.css">
+    <link rel="stylesheet" type="text/css" href="/static/third_party/xterm-4.11.0.min.css">
     <link rel="stylesheet" type="text/css" href="/static/style.css">
     <link href="static/icon.png" rel="shortcut icon">
 </head>
@@ -31,9 +31,9 @@
 
 <script src="https://www.gstatic.com/firebasejs/6.2.4/firebase-app.js"></script>
 <script src="https://www.gstatic.com/firebasejs/6.2.4/firebase-database.js"></script>
-<script src="/static/third_party/xterm-3.14.4.min.js"></script>
-<script src="/static/third_party/xterm-3.14.4-addon-fit.min.js"></script>
-<script src="/static/third_party/xterm-3.14.4-addon-search.min.js"></script>
+<script src="/static/third_party/xterm-4.11.0.min.js"></script>
+<script src="/static/third_party/xterm-0.5.0-addon-fit.min.js"></script>
+<script src="/static/third_party/xterm-0.8.0-addon-search.min.js"></script>
 <script src="/static/third_party/mithril-1.1.6.min.js"></script>
 <script src="/static/config.js"></script>
 <script src="/static/script.js"></script>
diff --git a/infra/ci/frontend/static/script.js b/infra/ci/frontend/static/script.js
index 3090e91..1520485 100644
--- a/infra/ci/frontend/static/script.js
+++ b/infra/ci/frontend/static/script.js
@@ -92,11 +92,11 @@
 };
 
 let term = undefined;
+let fitAddon = undefined;
+let searchAddon = undefined;
 
 function main() {
   firebase.initializeApp({ databaseURL: cfg.DB_ROOT });
-  Terminal.applyAddon(fit);
-  Terminal.applyAddon(search);
 
   m.route(document.body, '/cls', {
     '/cls': CLsPageRenderer,
@@ -367,18 +367,31 @@
 }
 
 const TermRenderer = {
-  oncreate: function (vnode) {
+  oncreate: function(vnode) {
     console.log('Creating terminal object');
-    term = new Terminal(
-        {rows: 6, fontFamily: 'monospace', fontSize: 12, scrollback: 100000});
+    fitAddon = new FitAddon.FitAddon();
+    searchAddon = new SearchAddon.SearchAddon();
+    term = new Terminal({
+      rows: 6,
+      fontFamily: 'monospace',
+      fontSize: 12,
+      scrollback: 100000,
+      disableStdin: true,
+    });
+    term.loadAddon(fitAddon);
+    term.loadAddon(searchAddon);
     term.open(vnode.dom);
-    term.fit();
-    if (vnode.attrs.focused) term.focus();
+    fitAddon.fit();
+    if (vnode.attrs.focused)
+      term.focus();
   },
-  onremove: function (vnode) {
-    term.destroy();
+  onremove: function(vnode) {
+    term.dispose();
+    fitAddon.dispose();
+    searchAddon.dispose();
   },
-  onupdate: function (vnode) {
+  onupdate: function(vnode) {
+    fitAddon.fit();
     if (state.termClear) {
       term.clear();
       state.termClear = false;
@@ -388,7 +401,7 @@
     }
     state.termLines = [];
   },
-  view: function () {
+  view: function() {
     return m('.term-container',
       {
         onkeydown: (e) => {
@@ -402,9 +415,9 @@
         onkeydown: (e) => {
           if (e.key !== 'Enter') return;
           if (e.shiftKey) {
-            term.findNext(e.target.value);
+            searchAddon.findNext(e.target.value);
           } else {
-            term.findPrevious(e.target.value);
+            searchAddon.findPrevious(e.target.value);
           }
           e.stopPropagation();
           e.preventDefault();
diff --git a/infra/ci/sandbox/Dockerfile b/infra/ci/sandbox/Dockerfile
index 6f14c90..79da8d4 100644
--- a/infra/ci/sandbox/Dockerfile
+++ b/infra/ci/sandbox/Dockerfile
@@ -45,6 +45,20 @@
     rm bazel-*-installer-linux-x86_64.sh; \
     bazel version;
 
+# Chrome/puppeteer deps.
+RUN set -ex; \
+    export DEBIAN_FRONTEND=noninteractive; \
+    apt-get update; \
+    apt-get -y install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 \
+               libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 \
+               libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
+               libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
+               libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 \
+               libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
+               libxtst6 ca-certificates libappindicator1 libnss3 lsb-release \
+               xdg-utils fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei \
+               fonts-thai-tlwg fonts-kacst fonts-freefont-ttf
+
 COPY testrunner.sh /ci/testrunner.sh
 COPY init.sh /ci/init.sh
 RUN chmod -R a+rx /ci/
diff --git a/infra/luci/README.recipes.md b/infra/luci/README.recipes.md
index 8d601e9..a2e6d75 100644
--- a/infra/luci/README.recipes.md
+++ b/infra/luci/README.recipes.md
@@ -2,22 +2,107 @@
 # Repo documentation for [perfetto]()
 ## Table of Contents
 
+**[Recipe Modules](#Recipe-Modules)**
+  * [macos_sdk](#recipe_modules-macos_sdk) &mdash; The `macos_sdk` module provides safe functions to access a semi-hermetic XCode installation.
+  * [windows_sdk](#recipe_modules-windows_sdk)
+
 **[Recipes](#Recipes)**
+  * [macos_sdk:examples/full](#recipes-macos_sdk_examples_full)
   * [perfetto](#recipes-perfetto) &mdash; Recipe for building Perfetto.
+  * [windows_sdk:examples/full](#recipes-windows_sdk_examples_full)
+## Recipe Modules
+
+### *recipe_modules* / [macos\_sdk](/infra/luci/recipe_modules/macos_sdk)
+
+[DEPS](/infra/luci/recipe_modules/macos_sdk/__init__.py#15): [recipe\_engine/cipd][recipe_engine/recipe_modules/cipd], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+
+The `macos_sdk` module provides safe functions to access a semi-hermetic
+XCode installation.
+
+Available only to Google-run bots.
+
+#### **class [MacOSSDKApi](/infra/luci/recipe_modules/macos_sdk/api.py#24)([RecipeApi][recipe_engine/wkt/RecipeApi]):**
+
+API for using OS X SDK distributed via CIPD.
+
+&emsp; **@contextmanager**<br>&mdash; **def [\_\_call\_\_](/infra/luci/recipe_modules/macos_sdk/api.py#40)(self):**
+
+Sets up the XCode SDK environment.
+
+This call is a no-op on non-Mac platforms.
+
+This will deploy the helper tool and the XCode.app bundle at
+`[START_DIR]/cache/macos_sdk`.
+
+To avoid machines rebuilding these on every run, set up a named cache in
+your cr-buildbucket.cfg file like:
+
+    caches: {
+      # Cache for mac_toolchain tool and XCode.app
+      name: "macos_sdk"
+      path: "macos_sdk"
+    }
+
+If you have builders which e.g. use a non-current SDK, you can give them
+a uniqely named cache:
+
+    caches: {
+      # Cache for N-1 version mac_toolchain tool and XCode.app
+      name: "macos_sdk_old"
+      path: "macos_sdk"
+    }
+
+Usage:
+  with api.macos_sdk():
+    # sdk with mac build bits
+
+Raises:
+    StepFailure or InfraFailure.
+
+&emsp; **@property**<br>&mdash; **def [sdk\_dir](/infra/luci/recipe_modules/macos_sdk/api.py#35)(self):**
+### *recipe_modules* / [windows\_sdk](/infra/luci/recipe_modules/windows_sdk)
+
+[DEPS](/infra/luci/recipe_modules/windows_sdk/__init__.py#15): [recipe\_engine/cipd][recipe_engine/recipe_modules/cipd], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+
+#### **class [WindowsSDKApi](/infra/luci/recipe_modules/windows_sdk/api.py#20)([RecipeApi][recipe_engine/wkt/RecipeApi]):**
+
+API for using Windows SDK distributed via CIPD.
+
+&emsp; **@contextmanager**<br>&mdash; **def [\_\_call\_\_](/infra/luci/recipe_modules/windows_sdk/api.py#29)(self):**
+
+Setups the Windows SDK environment.
+
+This call is a no-op on non-Windows platforms.
+
+Raises:
+    StepFailure or InfraFailure.
 ## Recipes
 
+### *recipes* / [macos\_sdk:examples/full](/infra/luci/recipe_modules/macos_sdk/examples/full.py)
+
+[DEPS](/infra/luci/recipe_modules/macos_sdk/examples/full.py#15): [macos\_sdk](#recipe_modules-macos_sdk), [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+
+&mdash; **def [RunSteps](/infra/luci/recipe_modules/macos_sdk/examples/full.py#23)(api):**
 ### *recipes* / [perfetto](/infra/luci/recipes/perfetto.py)
 
-[DEPS](/infra/luci/recipes/perfetto.py#18): [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/file][recipe_engine/recipe_modules/file], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+[DEPS](/infra/luci/recipes/perfetto.py#18): [macos\_sdk](#recipe_modules-macos_sdk), [windows\_sdk](#recipe_modules-windows_sdk), [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/file][recipe_engine/recipe_modules/file], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/step][recipe_engine/recipe_modules/step]
 
 Recipe for building Perfetto.
 
-&mdash; **def [RunSteps](/infra/luci/recipes/perfetto.py#39)(api, repository):**
+&mdash; **def [RunSteps](/infra/luci/recipes/perfetto.py#41)(api, repository):**
+### *recipes* / [windows\_sdk:examples/full](/infra/luci/recipe_modules/windows_sdk/examples/full.py)
+
+[DEPS](/infra/luci/recipe_modules/windows_sdk/examples/full.py#15): [windows\_sdk](#recipe_modules-windows_sdk), [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+
+&mdash; **def [RunSteps](/infra/luci/recipe_modules/windows_sdk/examples/full.py#23)(api):**
 
 [recipe_engine/recipe_modules/buildbucket]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/2030661a4ff2a6b64b0651f2c44aabed8c71223f/README.recipes.md#recipe_modules-buildbucket
+[recipe_engine/recipe_modules/cipd]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/2030661a4ff2a6b64b0651f2c44aabed8c71223f/README.recipes.md#recipe_modules-cipd
 [recipe_engine/recipe_modules/context]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/2030661a4ff2a6b64b0651f2c44aabed8c71223f/README.recipes.md#recipe_modules-context
 [recipe_engine/recipe_modules/file]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/2030661a4ff2a6b64b0651f2c44aabed8c71223f/README.recipes.md#recipe_modules-file
+[recipe_engine/recipe_modules/json]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/2030661a4ff2a6b64b0651f2c44aabed8c71223f/README.recipes.md#recipe_modules-json
 [recipe_engine/recipe_modules/path]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/2030661a4ff2a6b64b0651f2c44aabed8c71223f/README.recipes.md#recipe_modules-path
 [recipe_engine/recipe_modules/platform]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/2030661a4ff2a6b64b0651f2c44aabed8c71223f/README.recipes.md#recipe_modules-platform
 [recipe_engine/recipe_modules/properties]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/2030661a4ff2a6b64b0651f2c44aabed8c71223f/README.recipes.md#recipe_modules-properties
 [recipe_engine/recipe_modules/step]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/2030661a4ff2a6b64b0651f2c44aabed8c71223f/README.recipes.md#recipe_modules-step
+[recipe_engine/wkt/RecipeApi]: https://chromium.googlesource.com/infra/luci/recipes-py.git/+/2030661a4ff2a6b64b0651f2c44aabed8c71223f/recipe_engine/recipe_api.py#856
diff --git a/infra/luci/generated/cr-buildbucket.cfg b/infra/luci/generated/cr-buildbucket.cfg
new file mode 100644
index 0000000..e6fd024
--- /dev/null
+++ b/infra/luci/generated/cr-buildbucket.cfg
@@ -0,0 +1,76 @@
+# Auto-generated by lucicfg.
+# Do not modify manually.
+#
+# For the schema of this file, see BuildbucketCfg message:
+#   https://luci-config.appspot.com/schemas/projects:buildbucket.cfg
+
+buckets {
+  name: "official"
+  acls {
+    group: "all"
+  }
+  acls {
+    role: SCHEDULER
+    group: "mdb/chrome-troopers"
+  }
+  acls {
+    role: SCHEDULER
+    group: "mdb/perfetto-cloud-infra"
+  }
+  swarming {
+    builders {
+      name: "perfetto-official-builder-linux"
+      swarming_host: "chrome-swarming.appspot.com"
+      dimensions: "cpu:x86-64"
+      dimensions: "os:Linux"
+      dimensions: "pool:luci.perfetto.official"
+      exe {
+        cipd_package: "infra/recipe_bundles/android.googlesource.com/platform/external/perfetto"
+        cipd_version: "refs/heads/master"
+        cmd: "luciexe"
+      }
+      properties: "{\"recipe\":\"perfetto\"}"
+      service_account: "perfetto-luci-official-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.use_realms"
+        value: 100
+      }
+    }
+    builders {
+      name: "perfetto-official-builder-mac"
+      swarming_host: "chrome-swarming.appspot.com"
+      dimensions: "cpu:x86-64"
+      dimensions: "os:Mac"
+      dimensions: "pool:luci.perfetto.official"
+      exe {
+        cipd_package: "infra/recipe_bundles/android.googlesource.com/platform/external/perfetto"
+        cipd_version: "refs/heads/master"
+        cmd: "luciexe"
+      }
+      properties: "{\"recipe\":\"perfetto\"}"
+      service_account: "perfetto-luci-official-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.use_realms"
+        value: 100
+      }
+    }
+    builders {
+      name: "perfetto-official-builder-windows"
+      swarming_host: "chrome-swarming.appspot.com"
+      dimensions: "cpu:x86-64"
+      dimensions: "os:Windows"
+      dimensions: "pool:luci.perfetto.official"
+      exe {
+        cipd_package: "infra/recipe_bundles/android.googlesource.com/platform/external/perfetto"
+        cipd_version: "refs/heads/master"
+        cmd: "luciexe"
+      }
+      properties: "{\"recipe\":\"perfetto\"}"
+      service_account: "perfetto-luci-official-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.use_realms"
+        value: 100
+      }
+    }
+  }
+}
diff --git a/infra/luci/generated/luci-logdog.cfg b/infra/luci/generated/luci-logdog.cfg
new file mode 100644
index 0000000..adc75be
--- /dev/null
+++ b/infra/luci/generated/luci-logdog.cfg
@@ -0,0 +1,9 @@
+# Auto-generated by lucicfg.
+# Do not modify manually.
+#
+# For the schema of this file, see ProjectConfig message:
+#   https://luci-config.appspot.com/schemas/projects:luci-logdog.cfg
+
+reader_auth_groups: "all"
+writer_auth_groups: "luci-logdog-chromium-writers"
+archive_gs_bucket: "chromium-luci-logdog"
diff --git a/infra/luci/generated/luci-scheduler.cfg b/infra/luci/generated/luci-scheduler.cfg
new file mode 100644
index 0000000..6ebe15d
--- /dev/null
+++ b/infra/luci/generated/luci-scheduler.cfg
@@ -0,0 +1,62 @@
+# Auto-generated by lucicfg.
+# Do not modify manually.
+#
+# For the schema of this file, see ProjectConfig message:
+#   https://luci-config.appspot.com/schemas/projects:luci-scheduler.cfg
+
+job {
+  id: "perfetto-official-builder-linux"
+  realm: "official"
+  acl_sets: "official"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "luci.perfetto.official"
+    builder: "perfetto-official-builder-linux"
+  }
+}
+job {
+  id: "perfetto-official-builder-mac"
+  realm: "official"
+  acl_sets: "official"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "luci.perfetto.official"
+    builder: "perfetto-official-builder-mac"
+  }
+}
+job {
+  id: "perfetto-official-builder-windows"
+  realm: "official"
+  acl_sets: "official"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "luci.perfetto.official"
+    builder: "perfetto-official-builder-windows"
+  }
+}
+trigger {
+  id: "perfetto-gitiles-trigger"
+  realm: "official"
+  acl_sets: "official"
+  triggers: "perfetto-official-builder-linux"
+  triggers: "perfetto-official-builder-mac"
+  triggers: "perfetto-official-builder-windows"
+  gitiles {
+    repo: "https://android.googlesource.com/platform/external/perfetto"
+    refs: "regexp:refs/tags/v.+"
+  }
+}
+acl_sets {
+  name: "official"
+  acls {
+    role: OWNER
+    granted_to: "group:mdb/perfetto-cloud-infra"
+  }
+  acls {
+    granted_to: "group:all"
+  }
+  acls {
+    role: TRIGGERER
+    granted_to: "group:mdb/chrome-troopers"
+  }
+}
diff --git a/infra/luci/generated/project.cfg b/infra/luci/generated/project.cfg
new file mode 100644
index 0000000..fbf2a59
--- /dev/null
+++ b/infra/luci/generated/project.cfg
@@ -0,0 +1,8 @@
+# Auto-generated by lucicfg.
+# Do not modify manually.
+#
+# For the schema of this file, see ProjectCfg message:
+#   https://luci-config.appspot.com/schemas/projects:project.cfg
+
+name: "perfetto"
+access: "group:all"
diff --git a/infra/luci/generated/realms.cfg b/infra/luci/generated/realms.cfg
new file mode 100644
index 0000000..522d5c6
--- /dev/null
+++ b/infra/luci/generated/realms.cfg
@@ -0,0 +1,52 @@
+# Auto-generated by lucicfg.
+# Do not modify manually.
+#
+# For the schema of this file, see RealmsCfg message:
+#   https://luci-config.appspot.com/schemas/projects:realms.cfg
+
+realms {
+  name: "@root"
+  bindings {
+    role: "role/buildbucket.reader"
+    principals: "group:all"
+  }
+  bindings {
+    role: "role/configs.reader"
+    principals: "group:all"
+  }
+  bindings {
+    role: "role/logdog.reader"
+    principals: "group:all"
+  }
+  bindings {
+    role: "role/logdog.writer"
+    principals: "group:luci-logdog-chromium-writers"
+  }
+  bindings {
+    role: "role/scheduler.owner"
+    principals: "group:mdb/perfetto-cloud-infra"
+  }
+  bindings {
+    role: "role/scheduler.reader"
+    principals: "group:all"
+  }
+}
+realms {
+  name: "official"
+  bindings {
+    role: "role/buildbucket.builderServiceAccount"
+    principals: "user:perfetto-luci-official-builder@chops-service-accounts.iam.gserviceaccount.com"
+  }
+  bindings {
+    role: "role/buildbucket.triggerer"
+    principals: "group:mdb/chrome-troopers"
+    principals: "group:mdb/perfetto-cloud-infra"
+  }
+  bindings {
+    role: "role/scheduler.triggerer"
+    principals: "group:mdb/chrome-troopers"
+  }
+}
+realms {
+  name: "pools/official"
+}
diff --git a/infra/luci/main.star b/infra/luci/main.star
new file mode 100755
index 0000000..a4ce72b
--- /dev/null
+++ b/infra/luci/main.star
@@ -0,0 +1,105 @@
+#!/usr/bin/env lucicfg
+# 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.
+
+lucicfg.check_version("1.23.3", "Please update depot_tools")
+
+# Enable LUCI Realms support and launch all builds in realms-aware mode.
+lucicfg.enable_experiment("crbug.com/1085650")
+luci.builder.defaults.experiments.set({"luci.use_realms": 100})
+
+# Enable bbagent.
+luci.recipe.defaults.use_bbagent.set(True)
+
+lucicfg.config(
+    config_dir = "generated",
+    fail_on_warnings = True,
+)
+
+luci.project(
+    name = "perfetto",
+    buildbucket = "cr-buildbucket.appspot.com",
+    logdog = "luci-logdog",
+    milo = "luci-milo",
+    scheduler = "luci-scheduler",
+    swarming = "chrome-swarming.appspot.com",
+    acls = [
+        acl.entry(
+            [
+                acl.BUILDBUCKET_READER,
+                acl.LOGDOG_READER,
+                acl.PROJECT_CONFIGS_READER,
+                acl.SCHEDULER_READER,
+            ],
+            groups = ["all"],
+        ),
+        acl.entry(roles = acl.SCHEDULER_OWNER, groups = "mdb/perfetto-cloud-infra"),
+        acl.entry([acl.LOGDOG_WRITER], groups = ["luci-logdog-chromium-writers"]),
+    ],
+)
+
+# Use the default Chromium logdog instance as:
+# a) we expect our logs to be very minimal
+# b) we are open source so there's nothing special in our logs.
+luci.logdog(
+    gs_bucket = "chromium-luci-logdog",
+)
+
+# Create a realm for the official pool.
+# Used by LUCI infra (Googlers: see pools.cfg) to enforce ACLs.
+luci.realm(name = "pools/official")
+
+# Bucket used by all official builders.
+luci.bucket(
+    name = "official",
+    acls = [
+        acl.entry(
+            roles = [acl.BUILDBUCKET_TRIGGERER],
+            groups = ["mdb/perfetto-cloud-infra"],
+        ),
+        acl.entry(
+            roles = [acl.SCHEDULER_TRIGGERER, acl.BUILDBUCKET_TRIGGERER],
+            groups = ["mdb/chrome-troopers"],
+        ),
+    ],
+)
+
+def official_builder(name, os):
+    luci.builder(
+        name = name,
+        bucket = "official",
+        executable = luci.recipe(
+            name = "perfetto",
+            cipd_package = "infra/recipe_bundles/android.googlesource.com/platform/external/perfetto",
+            cipd_version = "refs/heads/master",
+        ),
+        dimensions = {
+            "pool": "luci.perfetto.official",
+            "os": os,
+            "cpu": "x86-64",
+        },
+        service_account = "perfetto-luci-official-builder@chops-service-accounts.iam.gserviceaccount.com",
+        triggered_by = [
+            luci.gitiles_poller(
+                name = "perfetto-gitiles-trigger",
+                bucket = "official",
+                repo = "https://android.googlesource.com/platform/external/perfetto",
+                refs = ["refs/tags/v.+"],
+            ),
+        ],
+    )
+
+official_builder("perfetto-official-builder-linux", "Linux")
+official_builder("perfetto-official-builder-mac", "Mac")
+official_builder("perfetto-official-builder-windows", "Windows")
diff --git a/infra/luci/recipe_modules/macos_sdk/__init__.py b/infra/luci/recipe_modules/macos_sdk/__init__.py
new file mode 100644
index 0000000..0bd7a5d
--- /dev/null
+++ b/infra/luci/recipe_modules/macos_sdk/__init__.py
@@ -0,0 +1,54 @@
+# 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.
+
+DEPS = [
+    'recipe_engine/cipd',
+    'recipe_engine/context',
+    'recipe_engine/json',
+    'recipe_engine/path',
+    'recipe_engine/platform',
+    'recipe_engine/step',
+]
+
+from recipe_engine.recipe_api import Property
+from recipe_engine.config import ConfigGroup, Single
+
+PROPERTIES = {
+    '$perfetto/macos_sdk':
+ Property(
+        help='Properties specifically for the macos_sdk module.',
+        param_name='sdk_properties',
+        kind=ConfigGroup(  # pylint: disable=line-too-long
+            # XCode build version number. Internally maps to an XCode build id like
+            # '9c40b'. See
+            #
+            #   https://chrome-infra-packages.appspot.com/p/infra_internal/ios/xcode/mac/+/
+            #
+            # For an up to date list of the latest SDK builds.
+            sdk_version=Single(str),
+
+            # The CIPD toolchain tool package and version.
+            tool_pkg=Single(str),
+            tool_ver=Single(str),
+        ),
+        default={
+            'sdk_version':
+ '12B5025f',
+            'tool_package':
+ 'infra/tools/mac_toolchain/${platform}',
+            'tool_version':
+ 'git_revision:e9b1fe29fe21a1cd36428c43ea2aba244bd31280',
+        },
+    )
+}
diff --git a/infra/luci/recipe_modules/macos_sdk/api.py b/infra/luci/recipe_modules/macos_sdk/api.py
new file mode 100644
index 0000000..06feafb
--- /dev/null
+++ b/infra/luci/recipe_modules/macos_sdk/api.py
@@ -0,0 +1,108 @@
+# 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.
+"""The `macos_sdk` module provides safe functions to access a semi-hermetic
+XCode installation.
+
+Available only to Google-run bots."""
+
+from contextlib import contextmanager
+
+from recipe_engine import recipe_api
+
+
+class MacOSSDKApi(recipe_api.RecipeApi):
+  """API for using OS X SDK distributed via CIPD."""
+
+  def __init__(self, sdk_properties, *args, **kwargs):
+    super(MacOSSDKApi, self).__init__(*args, **kwargs)
+
+    self._sdk_dir = None
+    self._sdk_version = sdk_properties['sdk_version'].lower()
+    self._tool_package = sdk_properties['tool_package']
+    self._tool_version = sdk_properties['tool_version']
+
+  @property
+  def sdk_dir(self):
+    assert self._sdk_dir
+    return self._sdk_dir
+
+  @contextmanager
+  def __call__(self):
+    """Sets up the XCode SDK environment.
+
+    This call is a no-op on non-Mac platforms.
+
+    This will deploy the helper tool and the XCode.app bundle at
+    `[START_DIR]/cache/macos_sdk`.
+
+    To avoid machines rebuilding these on every run, set up a named cache in
+    your cr-buildbucket.cfg file like:
+
+        caches: {
+          # Cache for mac_toolchain tool and XCode.app
+          name: "macos_sdk"
+          path: "macos_sdk"
+        }
+
+    If you have builders which e.g. use a non-current SDK, you can give them
+    a uniqely named cache:
+
+        caches: {
+          # Cache for N-1 version mac_toolchain tool and XCode.app
+          name: "macos_sdk_old"
+          path: "macos_sdk"
+        }
+
+    Usage:
+      with api.macos_sdk():
+        # sdk with mac build bits
+
+    Raises:
+        StepFailure or InfraFailure.
+    """
+    if not self.m.platform.is_mac:
+      yield
+      return
+
+    try:
+      with self.m.context(infra_steps=True):
+        self._sdk_dir = self._ensure_sdk()
+        self.m.step('select XCode',
+                    ['sudo', 'xcode-select', '--switch', self._sdk_dir])
+      yield
+    finally:
+      with self.m.context(infra_steps=True):
+        self.m.step('reset XCode', ['sudo', 'xcode-select', '--reset'])
+
+  def _ensure_sdk(self):
+    """Ensures the mac_toolchain tool and MacOS SDK packages are installed.
+
+    Returns Path to the installed sdk app bundle."""
+    cache_dir = self.m.path['cache'].join('macos_sdk')
+    pkgs = self.m.cipd.EnsureFile()
+    pkgs.add_package(self._tool_package, self._tool_version)
+    self.m.cipd.ensure(cache_dir, pkgs)
+
+    sdk_dir = cache_dir.join('XCode.app')
+    self.m.step('install xcode', [
+        cache_dir.join('mac_toolchain'),
+        'install',
+        '-kind',
+        'mac',
+        '-xcode-version',
+        self._sdk_version,
+        '-output-dir',
+        sdk_dir,
+    ])
+    return sdk_dir
diff --git a/infra/luci/recipe_modules/macos_sdk/examples/full.expected/linux.json b/infra/luci/recipe_modules/macos_sdk/examples/full.expected/linux.json
new file mode 100644
index 0000000..5b0f352
--- /dev/null
+++ b/infra/luci/recipe_modules/macos_sdk/examples/full.expected/linux.json
@@ -0,0 +1,21 @@
+[
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja"
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/infra/luci/recipe_modules/macos_sdk/examples/full.expected/mac.json b/infra/luci/recipe_modules/macos_sdk/examples/full.expected/mac.json
new file mode 100644
index 0000000..ab278fd
--- /dev/null
+++ b/infra/luci/recipe_modules/macos_sdk/examples/full.expected/mac.json
@@ -0,0 +1,83 @@
+[
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CACHE]/macos_sdk",
+      "-ensure-file",
+      "infra/tools/mac_toolchain/${platform} git_revision:e9b1fe29fe21a1cd36428c43ea2aba244bd31280",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "name": "ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-git_revision:e9b\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/mac_toolchain/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CACHE]/macos_sdk/mac_toolchain",
+      "install",
+      "-kind",
+      "mac",
+      "-xcode-version",
+      "12b5025f",
+      "-output-dir",
+      "[CACHE]/macos_sdk/XCode.app"
+    ],
+    "infra_step": true,
+    "name": "install xcode"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--switch",
+      "[CACHE]/macos_sdk/XCode.app"
+    ],
+    "infra_step": true,
+    "name": "select XCode"
+  },
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--reset"
+    ],
+    "infra_step": true,
+    "name": "reset XCode"
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/infra/luci/recipe_modules/macos_sdk/examples/full.expected/win.json b/infra/luci/recipe_modules/macos_sdk/examples/full.expected/win.json
new file mode 100644
index 0000000..5b0f352
--- /dev/null
+++ b/infra/luci/recipe_modules/macos_sdk/examples/full.expected/win.json
@@ -0,0 +1,21 @@
+[
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja"
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/infra/luci/recipe_modules/macos_sdk/examples/full.py b/infra/luci/recipe_modules/macos_sdk/examples/full.py
new file mode 100644
index 0000000..6d7ded3
--- /dev/null
+++ b/infra/luci/recipe_modules/macos_sdk/examples/full.py
@@ -0,0 +1,33 @@
+# 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.
+
+DEPS = [
+    'macos_sdk',
+    'recipe_engine/platform',
+    'recipe_engine/properties',
+    'recipe_engine/step',
+]
+
+
+def RunSteps(api):
+  with api.macos_sdk():
+    sdk_dir = api.macos_sdk.sdk_dir if api.platform.is_mac else None
+    api.step('gn', ['gn', 'gen', 'out/Release'])
+    api.step('ninja', ['ninja', '-C', 'out/Release'])
+
+
+def GenTests(api):
+  for platform in ('linux', 'mac', 'win'):
+    yield (api.test(platform) + api.platform.name(platform) +
+           api.properties.generic(buildername='test_builder'))
\ No newline at end of file
diff --git a/infra/luci/recipe_modules/windows_sdk/__init__.py b/infra/luci/recipe_modules/windows_sdk/__init__.py
new file mode 100644
index 0000000..f86fd54
--- /dev/null
+++ b/infra/luci/recipe_modules/windows_sdk/__init__.py
@@ -0,0 +1,41 @@
+# 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.
+
+DEPS = [
+    'recipe_engine/cipd',
+    'recipe_engine/context',
+    'recipe_engine/json',
+    'recipe_engine/path',
+    'recipe_engine/platform',
+    'recipe_engine/step',
+]
+
+from recipe_engine.recipe_api import Property
+from recipe_engine.config import ConfigGroup, Single
+
+PROPERTIES = {
+    '$perfetto/windows_sdk':
+        Property(
+            help='Properties specifically for the windows_sdk module.',
+            param_name='sdk_properties',
+            kind=ConfigGroup(
+                # The CIPD package and version.
+                sdk_package=Single(str),
+                sdk_version=Single(str)),
+            default={
+                'sdk_package': 'chrome_internal/third_party/sdk/windows',
+                'sdk_version': 'uploaded:2019-09-06'
+            },
+        )
+}
\ No newline at end of file
diff --git a/infra/luci/recipe_modules/windows_sdk/api.py b/infra/luci/recipe_modules/windows_sdk/api.py
new file mode 100644
index 0000000..5bd1c8a
--- /dev/null
+++ b/infra/luci/recipe_modules/windows_sdk/api.py
@@ -0,0 +1,120 @@
+# 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 contextlib import contextmanager
+
+from recipe_engine import recipe_api
+
+
+class WindowsSDKApi(recipe_api.RecipeApi):
+  """API for using Windows SDK distributed via CIPD."""
+
+  def __init__(self, sdk_properties, *args, **kwargs):
+    super(WindowsSDKApi, self).__init__(*args, **kwargs)
+
+    self._sdk_package = sdk_properties['sdk_package']
+    self._sdk_version = sdk_properties['sdk_version']
+
+  @contextmanager
+  def __call__(self):
+    """Setups the Windows SDK environment.
+
+    This call is a no-op on non-Windows platforms.
+
+    Raises:
+        StepFailure or InfraFailure.
+    """
+    if not self.m.platform.is_win:
+      yield
+      return
+
+    try:
+      with self.m.context(infra_steps=True):
+        sdk_dir = self._ensure_sdk()
+      with self.m.context(**self._sdk_env(sdk_dir)):
+        yield
+    finally:
+      # cl.exe automatically starts background mspdbsrv.exe daemon which
+      # needs to be manually stopped so Swarming can tidy up after itself.
+      self.m.step('taskkill mspdbsrv',
+                  ['taskkill.exe', '/f', '/t', '/im', 'mspdbsrv.exe'])
+
+  def _ensure_sdk(self):
+    """Ensures the Windows SDK CIPD package is installed.
+
+    Returns the directory where the SDK package has been installed.
+
+    Args:
+      path (path): Path to a directory.
+      version (str): CIPD instance ID, tag or ref.
+    """
+    sdk_dir = self.m.path['cache'].join('windows_sdk')
+    pkgs = self.m.cipd.EnsureFile()
+    pkgs.add_package(self._sdk_package, self._sdk_version)
+    self.m.cipd.ensure(sdk_dir, pkgs)
+    return sdk_dir
+
+  def _sdk_env(self, sdk_dir):
+    """Constructs the environment for the SDK.
+
+    Returns environment and environment prefixes.
+
+    Args:
+      sdk_dir (path): Path to a directory containing the SDK.
+    """
+    env = {}
+    env_prefixes = {}
+
+    # Load .../win_sdk/bin/SetEnv.${arch}.json to extract the required
+    # environment. It contains a dict that looks like this:
+    # {
+    #   "env": {
+    #     "VAR": [["..", "..", "x"], ["..", "..", "y"]],
+    #     ...
+    #   }
+    # }
+    # All these environment variables need to be added to the environment
+    # for the compiler and linker to work.
+    filename = 'SetEnv.%s.json' % {32: 'x86', 64: 'x64'}[self.m.platform.bits]
+    step_result = self.m.json.read(
+        'read %s' % filename,
+        sdk_dir.join('win_sdk', 'bin', filename),
+        step_test_data=lambda: self.m.json.test_api.output({
+            'env': {
+                'PATH': [['..', '..', 'win_sdk', 'bin', 'x64']],
+                'VSINSTALLDIR': [['..', '..\\']],
+            },
+        }))
+    data = step_result.json.output.get('env')
+    for key in data:
+      # recipes' Path() does not like .., ., \, or /, so this is cumbersome.
+      # What we want to do is:
+      #   [sdk_bin_dir.join(*e) for e in env[k]]
+      # Instead do that badly, and rely (but verify) on the fact that the paths
+      # are all specified relative to the root, but specified relative to
+      # win_sdk/bin (i.e. everything starts with "../../".)
+      results = []
+      for value in data[key]:
+        assert value[0] == '..' and (value[1] == '..' or value[1] == '..\\')
+        results.append('%s' % sdk_dir.join(*value[2:]))
+
+      # PATH is special-cased because we don't want to overwrite other things
+      # like C:\Windows\System32. Others are replacements because prepending
+      # doesn't necessarily makes sense, like VSINSTALLDIR.
+      if key.lower() == 'path':
+        env_prefixes[key] = results
+      else:
+        env[key] = ';'.join(results)
+
+    return {'env': env, 'env_prefixes': env_prefixes}
diff --git a/infra/luci/recipe_modules/windows_sdk/examples/full.expected/linux.json b/infra/luci/recipe_modules/windows_sdk/examples/full.expected/linux.json
new file mode 100644
index 0000000..5b0f352
--- /dev/null
+++ b/infra/luci/recipe_modules/windows_sdk/examples/full.expected/linux.json
@@ -0,0 +1,21 @@
+[
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja"
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/infra/luci/recipe_modules/windows_sdk/examples/full.expected/mac.json b/infra/luci/recipe_modules/windows_sdk/examples/full.expected/mac.json
new file mode 100644
index 0000000..5b0f352
--- /dev/null
+++ b/infra/luci/recipe_modules/windows_sdk/examples/full.expected/mac.json
@@ -0,0 +1,21 @@
+[
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja"
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/infra/luci/recipe_modules/windows_sdk/examples/full.expected/win.json b/infra/luci/recipe_modules/windows_sdk/examples/full.expected/win.json
new file mode 100644
index 0000000..f6c937f
--- /dev/null
+++ b/infra/luci/recipe_modules/windows_sdk/examples/full.expected/win.json
@@ -0,0 +1,108 @@
+[
+  {
+    "cmd": [
+      "cipd.bat",
+      "ensure",
+      "-root",
+      "[CACHE]\\windows_sdk",
+      "-ensure-file",
+      "chrome_internal/third_party/sdk/windows uploaded:2019-09-06",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "name": "ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-uploaded:2019-09\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"chrome_internal/third_party/sdk/windows\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "\nimport shutil\nimport sys\nshutil.copy(sys.argv[1], sys.argv[2])\n",
+      "[CACHE]\\windows_sdk\\win_sdk\\bin\\SetEnv.x64.json",
+      "/path/to/tmp/json"
+    ],
+    "name": "read SetEnv.x64.json",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"env\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"PATH\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      [@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"..\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"..\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"win_sdk\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"bin\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"x64\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      ]@@@",
+      "@@@STEP_LOG_LINE@json.output@    ], @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"VSINSTALLDIR\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      [@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"..\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"..\\\\\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      ]@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "env": {
+      "VSINSTALLDIR": "[CACHE]\\windows_sdk"
+    },
+    "env_prefixes": {
+      "PATH": [
+        "[CACHE]\\windows_sdk\\win_sdk\\bin\\x64"
+      ]
+    },
+    "name": "gn"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "env": {
+      "VSINSTALLDIR": "[CACHE]\\windows_sdk"
+    },
+    "env_prefixes": {
+      "PATH": [
+        "[CACHE]\\windows_sdk\\win_sdk\\bin\\x64"
+      ]
+    },
+    "name": "ninja"
+  },
+  {
+    "cmd": [
+      "taskkill.exe",
+      "/f",
+      "/t",
+      "/im",
+      "mspdbsrv.exe"
+    ],
+    "name": "taskkill mspdbsrv"
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/infra/luci/recipe_modules/windows_sdk/examples/full.py b/infra/luci/recipe_modules/windows_sdk/examples/full.py
new file mode 100644
index 0000000..82ab06b
--- /dev/null
+++ b/infra/luci/recipe_modules/windows_sdk/examples/full.py
@@ -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.
+
+DEPS = [
+    'windows_sdk',
+    'recipe_engine/platform',
+    'recipe_engine/properties',
+    'recipe_engine/step',
+]
+
+
+def RunSteps(api):
+  with api.windows_sdk():
+    api.step('gn', ['gn', 'gen', 'out/Release'])
+    api.step('ninja', ['ninja', '-C', 'out/Release'])
+
+
+def GenTests(api):
+  for platform in ('linux', 'mac', 'win'):
+    properties = {
+        'buildername': 'test_builder',
+    }
+    yield (api.test(platform) + api.platform.name(platform) +
+           api.properties.generic(**properties))
\ No newline at end of file
diff --git a/infra/luci/recipes/perfetto.expected/ci_linux.json b/infra/luci/recipes/perfetto.expected/ci_linux.json
index eebe765..aad1eae 100644
--- a/infra/luci/recipes/perfetto.expected/ci_linux.json
+++ b/infra/luci/recipes/perfetto.expected/ci_linux.json
@@ -63,8 +63,8 @@
   },
   {
     "cmd": [
+      "python3",
       "tools/install-build-deps",
-      "--ui",
       "--android"
     ],
     "cwd": "[CACHE]/builder/perfetto",
@@ -73,6 +73,7 @@
   },
   {
     "cmd": [
+      "python3",
       "tools/gn",
       "gen",
       "out/dist",
@@ -83,6 +84,7 @@
   },
   {
     "cmd": [
+      "python3",
       "tools/ninja",
       "-C",
       "out/dist"
diff --git a/infra/luci/recipes/perfetto.expected/ci_mac.json b/infra/luci/recipes/perfetto.expected/ci_mac.json
new file mode 100644
index 0000000..c4946ab
--- /dev/null
+++ b/infra/luci/recipes/perfetto.expected/ci_mac.json
@@ -0,0 +1,164 @@
+[
+  {
+    "cmd": [],
+    "name": "git"
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CACHE]/builder/perfetto"
+    ],
+    "infra_step": true,
+    "name": "git.ensure source dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "init",
+      "[CACHE]/builder/perfetto"
+    ],
+    "infra_step": true,
+    "name": "git.init",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "--tags",
+      "https://android.googlesource.com/platform/external/perfetto",
+      "2d72510e447ab60a9728aeea2362d8be2cbd7789"
+    ],
+    "cwd": "[CACHE]/builder/perfetto",
+    "infra_step": true,
+    "name": "git.fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[CACHE]/builder/perfetto",
+    "infra_step": true,
+    "name": "git.checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "tools/install-build-deps",
+      "--android"
+    ],
+    "cwd": "[CACHE]/builder/perfetto",
+    "infra_step": true,
+    "name": "build-deps"
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CACHE]/macos_sdk",
+      "-ensure-file",
+      "infra/tools/mac_toolchain/${platform} git_revision:e9b1fe29fe21a1cd36428c43ea2aba244bd31280",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "cwd": "[CACHE]/builder/perfetto",
+    "infra_step": true,
+    "name": "ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-git_revision:e9b\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/mac_toolchain/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CACHE]/macos_sdk/mac_toolchain",
+      "install",
+      "-kind",
+      "mac",
+      "-xcode-version",
+      "12b5025f",
+      "-output-dir",
+      "[CACHE]/macos_sdk/XCode.app"
+    ],
+    "cwd": "[CACHE]/builder/perfetto",
+    "infra_step": true,
+    "name": "install xcode"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--switch",
+      "[CACHE]/macos_sdk/XCode.app"
+    ],
+    "cwd": "[CACHE]/builder/perfetto",
+    "infra_step": true,
+    "name": "select XCode"
+  },
+  {
+    "cmd": [
+      "python3",
+      "tools/gn",
+      "gen",
+      "out/dist",
+      "--args=is_debug=false"
+    ],
+    "cwd": "[CACHE]/builder/perfetto",
+    "name": "gn gen"
+  },
+  {
+    "cmd": [
+      "python3",
+      "tools/ninja",
+      "-C",
+      "out/dist"
+    ],
+    "cwd": "[CACHE]/builder/perfetto",
+    "name": "ninja"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--reset"
+    ],
+    "cwd": "[CACHE]/builder/perfetto",
+    "infra_step": true,
+    "name": "reset XCode"
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/infra/luci/recipes/perfetto.expected/ci_win.json b/infra/luci/recipes/perfetto.expected/ci_win.json
new file mode 100644
index 0000000..1efd91e
--- /dev/null
+++ b/infra/luci/recipes/perfetto.expected/ci_win.json
@@ -0,0 +1,188 @@
+[
+  {
+    "cmd": [],
+    "name": "git"
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CACHE]\\builder\\perfetto"
+    ],
+    "infra_step": true,
+    "name": "git.ensure source dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "init",
+      "[CACHE]\\builder\\perfetto"
+    ],
+    "infra_step": true,
+    "name": "git.init",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "--tags",
+      "https://android.googlesource.com/platform/external/perfetto",
+      "2d72510e447ab60a9728aeea2362d8be2cbd7789"
+    ],
+    "cwd": "[CACHE]\\builder\\perfetto",
+    "infra_step": true,
+    "name": "git.fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[CACHE]\\builder\\perfetto",
+    "infra_step": true,
+    "name": "git.checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "tools/install-build-deps",
+      "--android"
+    ],
+    "cwd": "[CACHE]\\builder\\perfetto",
+    "infra_step": true,
+    "name": "build-deps"
+  },
+  {
+    "cmd": [
+      "cipd.bat",
+      "ensure",
+      "-root",
+      "[CACHE]\\windows_sdk",
+      "-ensure-file",
+      "chrome_internal/third_party/sdk/windows uploaded:2019-09-06",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "cwd": "[CACHE]\\builder\\perfetto",
+    "infra_step": true,
+    "name": "ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-uploaded:2019-09\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"chrome_internal/third_party/sdk/windows\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "\nimport shutil\nimport sys\nshutil.copy(sys.argv[1], sys.argv[2])\n",
+      "[CACHE]\\windows_sdk\\win_sdk\\bin\\SetEnv.x64.json",
+      "/path/to/tmp/json"
+    ],
+    "cwd": "[CACHE]\\builder\\perfetto",
+    "name": "read SetEnv.x64.json",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"env\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"PATH\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      [@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"..\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"..\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"win_sdk\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"bin\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"x64\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      ]@@@",
+      "@@@STEP_LOG_LINE@json.output@    ], @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"VSINSTALLDIR\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      [@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"..\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"..\\\\\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      ]@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "tools/gn",
+      "gen",
+      "out/dist",
+      "--args=is_debug=false"
+    ],
+    "cwd": "[CACHE]\\builder\\perfetto",
+    "env": {
+      "VSINSTALLDIR": "[CACHE]\\windows_sdk"
+    },
+    "env_prefixes": {
+      "PATH": [
+        "[CACHE]\\windows_sdk\\win_sdk\\bin\\x64"
+      ]
+    },
+    "name": "gn gen"
+  },
+  {
+    "cmd": [
+      "python3",
+      "tools/ninja",
+      "-C",
+      "out/dist"
+    ],
+    "cwd": "[CACHE]\\builder\\perfetto",
+    "env": {
+      "VSINSTALLDIR": "[CACHE]\\windows_sdk"
+    },
+    "env_prefixes": {
+      "PATH": [
+        "[CACHE]\\windows_sdk\\win_sdk\\bin\\x64"
+      ]
+    },
+    "name": "ninja"
+  },
+  {
+    "cmd": [
+      "taskkill.exe",
+      "/f",
+      "/t",
+      "/im",
+      "mspdbsrv.exe"
+    ],
+    "cwd": "[CACHE]\\builder\\perfetto",
+    "name": "taskkill mspdbsrv"
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/infra/luci/recipes/perfetto.py b/infra/luci/recipes/perfetto.py
index e4552d8..5ac4f17 100644
--- a/infra/luci/recipes/perfetto.py
+++ b/infra/luci/recipes/perfetto.py
@@ -23,6 +23,8 @@
     'recipe_engine/platform',
     'recipe_engine/properties',
     'recipe_engine/step',
+    'macos_sdk',
+    'windows_sdk',
 ]
 
 PROPERTIES = {
@@ -57,16 +59,18 @@
   # There should be no need for internet access for building Perfetto beyond
   # this point.
   with api.context(cwd=src_dir, infra_steps=True):
-    api.step('build-deps', ['tools/install-build-deps', '--ui', '--android'])
+    api.step('build-deps', ['python3', 'tools/install-build-deps', '--android'])
 
   # Buld Perfetto.
-  with api.context(cwd=src_dir):
-    api.step('gn gen', ['tools/gn', 'gen', 'out/dist', '--args=is_debug=false'])
-    api.step('ninja', ['tools/ninja', '-C', 'out/dist'])
+  with api.context(cwd=src_dir), api.macos_sdk(), api.windows_sdk():
+    api.step(
+        'gn gen',
+        ['python3', 'tools/gn', 'gen', 'out/dist', '--args=is_debug=false'])
+    api.step('ninja', ['python3', 'tools/ninja', '-C', 'out/dist'])
 
 
 def GenTests(api):
-  for platform in ('linux',):
+  for platform in ('linux', 'mac', 'win'):
     yield (api.test('ci_' + platform) + api.platform.name(platform) +
            api.buildbucket.ci_build(
                project='perfetto',
diff --git a/infra/oss-fuzz/build_fuzzers b/infra/oss-fuzz/build_fuzzers
index 7f1fe48..95d9383 100755
--- a/infra/oss-fuzz/build_fuzzers
+++ b/infra/oss-fuzz/build_fuzzers
@@ -11,9 +11,9 @@
 
 GN_ARGS="is_clang=true is_debug=false is_fuzzer=true use_libfuzzer=false \
 link_fuzzer=\"-lFuzzingEngine\" is_hermetic_clang=false \
-use_custom_libcxx=false \
-extra_cflags=\"$CFLAGS -Wno-implicit-int-float-conversion\" \
-extra_cxxflags=\"$CXXFLAGS\" extra_ldflags=\"$CXXFLAGS\" \
+use_custom_libcxx=false is_cross_compiling=true \
+extra_target_cflags=\"$CFLAGS -Wno-implicit-int-float-conversion\" \
+extra_target_cxxflags=\"$CXXFLAGS\" extra_target_ldflags=\"$CXXFLAGS\" \
 is_system_compiler=true cc=\"$CC\" cxx=\"$CXX\" linker=\"gold\""
 
 if [ "$SANITIZER" = "address" ]; then
diff --git a/infra/perfetto.dev/build.js b/infra/perfetto.dev/build.js
index 086234d..1fc657c 100644
--- a/infra/perfetto.dev/build.js
+++ b/infra/perfetto.dev/build.js
@@ -73,7 +73,11 @@
 
   // Check that deps are current before starting.
   const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps');
-  const depsArgs = ['--check-only', '/dev/null', '--ui'];
+
+  // --filter=nodejs is to match what cloud_build_entrypoint.sh passes to
+  // install-build-deps. It doesn't bother installing the full toolchains
+  // because, unlike the Perfetto UI, it doesn't need Wasm.
+  const depsArgs = ['--check-only=/dev/null', '--ui', '--filter=nodejs'];
   exec(installBuildDeps, depsArgs);
 
   console.log('Entering', cfg.outDir);
diff --git a/infra/perfetto.dev/src/markdown_render.js b/infra/perfetto.dev/src/markdown_render.js
index 8fe5cc9..21d03ed 100644
--- a/infra/perfetto.dev/src/markdown_render.js
+++ b/infra/perfetto.dev/src/markdown_render.js
@@ -158,6 +158,15 @@
   if (cssClass != '') {
     cssClass = ` class="callout ${cssClass}"`;
   }
+
+  // Rudimentary support of definition lists.
+  var colonStart = text.search("\n:")
+  if (colonStart != -1) {
+    var key = text.substring(0, colonStart);
+    var value = text.substring(colonStart + 2);
+    return `<dl><dt><p>${key}</p></dt><dd><p>${value}</p></dd></dl>`
+  }
+
   return `<p${cssClass}>${text}</p>\n`;
 }
 
diff --git a/protos/perfetto/common/trace_stats.proto b/protos/perfetto/common/trace_stats.proto
index e925ad4..c6a9fbf 100644
--- a/protos/perfetto/common/trace_stats.proto
+++ b/protos/perfetto/common/trace_stats.proto
@@ -158,4 +158,13 @@
   // Packets that failed validation of the TrustedPacket. If this is > 0, there
   // is a bug in the producer.
   optional uint64 invalid_packets = 10;
+
+  // This is set only when the TraceConfig specifies a TraceFilter.
+  message FilterStats {
+    optional uint64 input_packets = 1;
+    optional uint64 input_bytes = 2;
+    optional uint64 output_bytes = 3;
+    optional uint64 errors = 4;
+  }
+  optional FilterStats filter_stats = 11;
 }
diff --git a/protos/perfetto/common/tracing_service_state.proto b/protos/perfetto/common/tracing_service_state.proto
index 119cea8..1ef4c56 100644
--- a/protos/perfetto/common/tracing_service_state.proto
+++ b/protos/perfetto/common/tracing_service_state.proto
@@ -34,6 +34,12 @@
 
     // Unix uid of the remote process.
     optional int32 uid = 3;
+
+    // The version of the client library used by the producer.
+    // This is a human readable string with and its format varies depending on
+    // the build system and the repo (standalone vs AOSP).
+    // This is intended for human debugging only.
+    optional string sdk_version = 4;
   }
 
   // Describes a data source registered by a producer. Data sources are listed
@@ -57,4 +63,10 @@
 
   // Number of tracing sessions in the started state. Always <= num_sessions.
   optional int32 num_sessions_started = 4;
+
+  // The version of traced (the same returned by `traced --version`).
+  // This is a human readable string with and its format varies depending on
+  // the build system and the repo (standalone vs AOSP).
+  // This is intended for human debugging only.
+  optional string tracing_service_version = 5;
 }
diff --git a/protos/perfetto/config/data_source_config.proto b/protos/perfetto/config/data_source_config.proto
index bdb0411..22b18e3 100644
--- a/protos/perfetto/config/data_source_config.proto
+++ b/protos/perfetto/config/data_source_config.proto
@@ -39,6 +39,14 @@
 // The configuration that is passed to each data source when starting tracing.
 // Next id: 116
 message DataSourceConfig {
+  enum SessionInitiator {
+    SESSION_INITIATOR_UNSPECIFIED = 0;
+    // This trace was initiated from a trusted system app has DUMP and
+    // USAGE_STATS permission. This system app is expected to not expose the
+    // trace to the user of the device.
+    // This is determined by checking the UID initiating the trace.
+    SESSION_INITIATOR_TRUSTED_SYSTEM = 1;
+  };
   // Data source unique name, e.g., "linux.ftrace". This must match
   // the name passed by the data source when it registers (see
   // RegisterDataSource()).
@@ -65,6 +73,10 @@
   // DO NOT SET in consumer as this will be overridden by the service.
   optional bool enable_extra_guardrails = 6;
 
+  // Set by the service to indicate which user initiated this trace.
+  // DO NOT SET in consumer as this will be overridden by the service.
+  optional SessionInitiator session_initiator = 8;
+
   // Set by the service to indicate which tracing session the data source
   // belongs to. The intended use case for this is checking if two data sources,
   // one of which produces metadata for the other one, belong to the same trace
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index 706b574..c8b55e3 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -197,6 +197,12 @@
 
     // Unix uid of the remote process.
     optional int32 uid = 3;
+
+    // The version of the client library used by the producer.
+    // This is a human readable string with and its format varies depending on
+    // the build system and the repo (standalone vs AOSP).
+    // This is intended for human debugging only.
+    optional string sdk_version = 4;
   }
 
   // Describes a data source registered by a producer. Data sources are listed
@@ -220,6 +226,12 @@
 
   // Number of tracing sessions in the started state. Always <= num_sessions.
   optional int32 num_sessions_started = 4;
+
+  // The version of traced (the same returned by `traced --version`).
+  // This is a human readable string with and its format varies depending on
+  // the build system and the repo (standalone vs AOSP).
+  // This is intended for human debugging only.
+  optional string tracing_service_version = 5;
 }
 
 // End of protos/perfetto/common/tracing_service_state.proto
@@ -559,7 +571,7 @@
 // Begin of protos/perfetto/config/profiling/heapprofd_config.proto
 
 // Configuration for go/heapprofd.
-// Next id: 26
+// Next id: 27
 message HeapprofdConfig {
   message ContinuousDumpConfig {
     // ms to wait before first dump.
@@ -607,12 +619,26 @@
   // For watermark based triggering or local debugging.
   repeated uint64 pid = 4;
 
+  // Only profile target if it was installed by one of the packages given.
+  // Special values are:
+  // * @system: installed on the system partition
+  // * @product: installed on the product partition
+  // * @null: sideloaded
+  // Supported on Android 12+.
+  repeated string target_installed_by = 26;
+
   // Which heaps to sample, e.g. "libc.malloc". If left empty, only samples
   // "malloc".
   //
   // Introduced in Android 12.
   repeated string heaps = 20;
 
+  // Which heaps not to sample, e.g. "libc.malloc". This is useful when used in
+  // combination with all_heaps;
+  //
+  // Introduced in Android 12.
+  repeated string exclude_heaps = 27;
+
   optional bool stream_allocations = 23;
 
   // If given, needs to be the same length as heaps and gives the sampling
@@ -747,6 +773,14 @@
   // For watermark based triggering or local debugging.
   repeated uint64 pid = 2;
 
+  // Only profile target if it was installed by one of the packages given.
+  // Special values are:
+  // * @system: installed on the system partition
+  // * @product: installed on the product partition
+  // * @null: sideloaded
+  // Supported on Android 12+.
+  repeated string target_installed_by = 7;
+
   // Dump at a predefined interval.
   optional ContinuousDumpConfig continuous_dump_config = 3;
 
@@ -847,7 +881,7 @@
 //     }
 //   }
 //
-// Next id: 18
+// Next id: 19
 message PerfEventConfig {
   // What event to sample on, and how often.
   // Defined in common/perf_events.proto.
@@ -918,6 +952,14 @@
   optional bool kernel_frames = 12;
   repeated int32 target_pid = 4;
   repeated string target_cmdline = 5;
+
+  // Only profile target if it was installed by one of the packages given.
+  // Special values are:
+  // * @system: installed on the system partition
+  // * @product: installed on the product partition
+  // * @null: sideloaded
+  // Supported on Android 12+.
+  repeated string target_installed_by = 18;
   repeated int32 exclude_pid = 6;
   repeated string exclude_cmdline = 7;
   optional uint32 additional_cmdline_count = 11;
@@ -1190,6 +1232,12 @@
     STAT_FORK_COUNT = 4;
   }
   repeated StatCounters stat_counters = 6;
+
+  // Polls /sys/devfreq/*/curfreq every X ms, if non-zero.
+  // This is required to be > 10ms to avoid excessive CPU usage.
+  // This option can be used to record unchanging values.
+  // Updates from frequency changes can come from ftrace/set_clock_rate.
+  optional uint32 devfreq_period_ms = 7;
 }
 
 // End of protos/perfetto/config/sys_stats/sys_stats_config.proto
@@ -1285,7 +1333,7 @@
   // Default: []
   repeated string enabled_categories = 2;
 
-  // Default: [“slow”, “debug”]
+  // Default: ["slow", "debug"]
   repeated string disabled_tags = 3;
 
   // Default: []
@@ -1299,6 +1347,14 @@
 // The configuration that is passed to each data source when starting tracing.
 // Next id: 116
 message DataSourceConfig {
+  enum SessionInitiator {
+    SESSION_INITIATOR_UNSPECIFIED = 0;
+    // This trace was initiated from a trusted system app has DUMP and
+    // USAGE_STATS permission. This system app is expected to not expose the
+    // trace to the user of the device.
+    // This is determined by checking the UID initiating the trace.
+    SESSION_INITIATOR_TRUSTED_SYSTEM = 1;
+  };
   // Data source unique name, e.g., "linux.ftrace". This must match
   // the name passed by the data source when it registers (see
   // RegisterDataSource()).
@@ -1325,6 +1381,10 @@
   // DO NOT SET in consumer as this will be overridden by the service.
   optional bool enable_extra_guardrails = 6;
 
+  // Set by the service to indicate which user initiated this trace.
+  // DO NOT SET in consumer as this will be overridden by the service.
+  optional SessionInitiator session_initiator = 8;
+
   // Set by the service to indicate which tracing session the data source
   // belongs to. The intended use case for this is checking if two data sources,
   // one of which produces metadata for the other one, belong to the same trace
@@ -1414,7 +1474,7 @@
 // It contains the general config for the logging buffer(s) and the configs for
 // all the data source being enabled.
 //
-// Next id: 31.
+// Next id: 33.
 message TraceConfig {
   message BufferConfig {
     optional uint32 size_kb = 1;
@@ -1491,6 +1551,21 @@
     // only keeps track of the first and the most recent snapshot until
     // ReadBuffers() is called.
     optional uint32 snapshot_interval_ms = 6;
+
+    // Hints to the service that a suspend-aware (i.e. counting time in suspend)
+    // clock should be used for periodic snapshots of service-emitted events.
+    // This means, if a snapshot *should* have happened during suspend, it will
+    // happen immediately after the device resumes.
+    //
+    // Choosing a clock like this is done on best-effort basis; not all
+    // platforms (e.g. Windows) expose a clock which can be used for periodic
+    // tasks counting suspend. If such a clock is not available, the service
+    // falls back to the best-available alternative.
+    //
+    // Introduced in Android S.
+    // TODO(lalitm): deprecate this in T and make this the default if nothing
+    // crashes in S.
+    optional bool prefer_suspend_clock_for_snapshot = 7;
   }
   optional BuiltinDataSource builtin_data_sources = 20;
 
@@ -1794,6 +1869,15 @@
   // Alternative encoding of trace_uuid as two int64s.
   optional int64 trace_uuid_msb = 27;
   optional int64 trace_uuid_lsb = 28;
+
+  // When set applies a post-filter to the trace contents using the filter
+  // provided. The filter is applied at ReadBuffers() time and works both in the
+  // case of IPC readback and write_into_file. This filter can be generated
+  // using `tools/proto_filter -s schema.proto -F filter_out.bytes` or
+  // `-T filter_out.escaped_string` (for .pbtx).
+  // Introduced in Android S. See go/trace-filtering for design.
+  message TraceFilter { optional bytes bytecode = 1; }
+  optional TraceFilter trace_filter = 32;
 }
 
 // End of protos/perfetto/config/trace_config.proto
diff --git a/protos/perfetto/config/profiling/heapprofd_config.proto b/protos/perfetto/config/profiling/heapprofd_config.proto
index 397e4cc..00dd692 100644
--- a/protos/perfetto/config/profiling/heapprofd_config.proto
+++ b/protos/perfetto/config/profiling/heapprofd_config.proto
@@ -19,7 +19,7 @@
 package perfetto.protos;
 
 // Configuration for go/heapprofd.
-// Next id: 26
+// Next id: 27
 message HeapprofdConfig {
   message ContinuousDumpConfig {
     // ms to wait before first dump.
@@ -67,12 +67,26 @@
   // For watermark based triggering or local debugging.
   repeated uint64 pid = 4;
 
+  // Only profile target if it was installed by one of the packages given.
+  // Special values are:
+  // * @system: installed on the system partition
+  // * @product: installed on the product partition
+  // * @null: sideloaded
+  // Supported on Android 12+.
+  repeated string target_installed_by = 26;
+
   // Which heaps to sample, e.g. "libc.malloc". If left empty, only samples
   // "malloc".
   //
   // Introduced in Android 12.
   repeated string heaps = 20;
 
+  // Which heaps not to sample, e.g. "libc.malloc". This is useful when used in
+  // combination with all_heaps;
+  //
+  // Introduced in Android 12.
+  repeated string exclude_heaps = 27;
+
   optional bool stream_allocations = 23;
 
   // If given, needs to be the same length as heaps and gives the sampling
diff --git a/protos/perfetto/config/profiling/java_hprof_config.proto b/protos/perfetto/config/profiling/java_hprof_config.proto
index 5eb9118..d504678 100644
--- a/protos/perfetto/config/profiling/java_hprof_config.proto
+++ b/protos/perfetto/config/profiling/java_hprof_config.proto
@@ -40,6 +40,14 @@
   // For watermark based triggering or local debugging.
   repeated uint64 pid = 2;
 
+  // Only profile target if it was installed by one of the packages given.
+  // Special values are:
+  // * @system: installed on the system partition
+  // * @product: installed on the product partition
+  // * @null: sideloaded
+  // Supported on Android 12+.
+  repeated string target_installed_by = 7;
+
   // Dump at a predefined interval.
   optional ContinuousDumpConfig continuous_dump_config = 3;
 
diff --git a/protos/perfetto/config/profiling/perf_event_config.proto b/protos/perfetto/config/profiling/perf_event_config.proto
index 4ed7d4c..754d948 100644
--- a/protos/perfetto/config/profiling/perf_event_config.proto
+++ b/protos/perfetto/config/profiling/perf_event_config.proto
@@ -36,7 +36,7 @@
 //     }
 //   }
 //
-// Next id: 18
+// Next id: 19
 message PerfEventConfig {
   // What event to sample on, and how often.
   // Defined in common/perf_events.proto.
@@ -107,6 +107,14 @@
   optional bool kernel_frames = 12;
   repeated int32 target_pid = 4;
   repeated string target_cmdline = 5;
+
+  // Only profile target if it was installed by one of the packages given.
+  // Special values are:
+  // * @system: installed on the system partition
+  // * @product: installed on the product partition
+  // * @null: sideloaded
+  // Supported on Android 12+.
+  repeated string target_installed_by = 18;
   repeated int32 exclude_pid = 6;
   repeated string exclude_cmdline = 7;
   optional uint32 additional_cmdline_count = 11;
diff --git a/protos/perfetto/config/sys_stats/sys_stats_config.proto b/protos/perfetto/config/sys_stats/sys_stats_config.proto
index 4e037fc..0986924 100644
--- a/protos/perfetto/config/sys_stats/sys_stats_config.proto
+++ b/protos/perfetto/config/sys_stats/sys_stats_config.proto
@@ -57,4 +57,10 @@
     STAT_FORK_COUNT = 4;
   }
   repeated StatCounters stat_counters = 6;
+
+  // Polls /sys/devfreq/*/curfreq every X ms, if non-zero.
+  // This is required to be > 10ms to avoid excessive CPU usage.
+  // This option can be used to record unchanging values.
+  // Updates from frequency changes can come from ftrace/set_clock_rate.
+  optional uint32 devfreq_period_ms = 7;
 }
diff --git a/protos/perfetto/config/trace_config.proto b/protos/perfetto/config/trace_config.proto
index 190c06b..dbc1bab 100644
--- a/protos/perfetto/config/trace_config.proto
+++ b/protos/perfetto/config/trace_config.proto
@@ -26,7 +26,7 @@
 // It contains the general config for the logging buffer(s) and the configs for
 // all the data source being enabled.
 //
-// Next id: 31.
+// Next id: 33.
 message TraceConfig {
   message BufferConfig {
     optional uint32 size_kb = 1;
@@ -103,6 +103,21 @@
     // only keeps track of the first and the most recent snapshot until
     // ReadBuffers() is called.
     optional uint32 snapshot_interval_ms = 6;
+
+    // Hints to the service that a suspend-aware (i.e. counting time in suspend)
+    // clock should be used for periodic snapshots of service-emitted events.
+    // This means, if a snapshot *should* have happened during suspend, it will
+    // happen immediately after the device resumes.
+    //
+    // Choosing a clock like this is done on best-effort basis; not all
+    // platforms (e.g. Windows) expose a clock which can be used for periodic
+    // tasks counting suspend. If such a clock is not available, the service
+    // falls back to the best-available alternative.
+    //
+    // Introduced in Android S.
+    // TODO(lalitm): deprecate this in T and make this the default if nothing
+    // crashes in S.
+    optional bool prefer_suspend_clock_for_snapshot = 7;
   }
   optional BuiltinDataSource builtin_data_sources = 20;
 
@@ -406,4 +421,13 @@
   // Alternative encoding of trace_uuid as two int64s.
   optional int64 trace_uuid_msb = 27;
   optional int64 trace_uuid_lsb = 28;
+
+  // When set applies a post-filter to the trace contents using the filter
+  // provided. The filter is applied at ReadBuffers() time and works both in the
+  // case of IPC readback and write_into_file. This filter can be generated
+  // using `tools/proto_filter -s schema.proto -F filter_out.bytes` or
+  // `-T filter_out.escaped_string` (for .pbtx).
+  // Introduced in Android S. See go/trace-filtering for design.
+  message TraceFilter { optional bytes bytecode = 1; }
+  optional TraceFilter trace_filter = 32;
 }
diff --git a/protos/perfetto/config/track_event/track_event_config.proto b/protos/perfetto/config/track_event/track_event_config.proto
index 5f4160e..1729b7f 100644
--- a/protos/perfetto/config/track_event/track_event_config.proto
+++ b/protos/perfetto/config/track_event/track_event_config.proto
@@ -59,7 +59,7 @@
   // Default: []
   repeated string enabled_categories = 2;
 
-  // Default: [“slow”, “debug”]
+  // Default: ["slow", "debug"]
   repeated string disabled_tags = 3;
 
   // Default: []
diff --git a/protos/perfetto/ipc/producer_port.proto b/protos/perfetto/ipc/producer_port.proto
index 1b193eb..9be72ea 100644
--- a/protos/perfetto/ipc/producer_port.proto
+++ b/protos/perfetto/ipc/producer_port.proto
@@ -152,6 +152,21 @@
   // SetupTracing response. See TracingService::ConnectProducer() and
   // |using_shmem_provided_by_producer| in InitializeConnectionResponse.
   optional bool producer_provided_shmem = 6;
+
+  // ---------------------------------------------------
+  // All fields below have been introduced in Android S.
+  // ---------------------------------------------------
+
+  // The version of the client library used by the producer.
+  // This is a human readable string with and its format varies depending on
+  // the build system that is used to build the code and the repo (standalone
+  // vs AOSP). This is intended for human debugging only.
+  optional string sdk_version = 8;
+
+  // On Windows, when producer_provided_shmem = true, the client creates a named
+  // SHM region and passes the name (an unguessable token) back to the service.
+  // Introduced in v13.
+  optional string shm_key_windows = 7;
 }
 
 message InitializeConnectionResponse {
@@ -262,9 +277,16 @@
 
   message StopDataSource { optional uint64 instance_id = 1; }
 
-  // This message also transports the file descriptor for the shared memory
-  // buffer (not a proto field).
-  message SetupTracing { optional uint32 shared_buffer_page_size_kb = 1; }
+  // On Android/Linux/Mac this message also transports the file descriptor for
+  // the shared memory buffer (not a proto field).
+  message SetupTracing {
+    optional uint32 shared_buffer_page_size_kb = 1;
+
+    // On Windows, instead, we pass the name (an unguessable token) of a shared
+    // memory region that can be attached by the other process by name.
+    // Introduced in v13.
+    optional string shm_key_windows = 2;
+  }
 
   message Flush {
     // The instance id (i.e. StartDataSource.new_instance_id) of the data
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index 514901b..5e45f9c 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -27,7 +27,6 @@
     "fastrpc_metric.proto",
     "g2d_metric.proto",
     "gpu_metric.proto",
-    "heap_profile_callsites.proto",
     "hwcomposer.proto",
     "hwui_metric.proto",
     "ion_metric.proto",
diff --git a/protos/perfetto/metrics/android/display_metrics.proto b/protos/perfetto/metrics/android/display_metrics.proto
index 4c36b5b..0c6f163 100644
--- a/protos/perfetto/metrics/android/display_metrics.proto
+++ b/protos/perfetto/metrics/android/display_metrics.proto
@@ -30,4 +30,25 @@
 
   // Stat that reports the number of dpu underrrun occurs count.
   optional uint32 total_dpu_underrun_count = 3;
+
+
+  message RefreshRateStat {
+    // The refresh rate value (the number of frames per second)
+    optional uint32 refresh_rate_fps = 1;
+
+    // Calculate the number of refresh rate switches to this fps
+    optional uint32 count = 2;
+
+    // Calculate the total duration of refresh rate stays at this fps
+    optional double total_dur_ms = 3;
+
+    // Calculate the average duration of refresh rate stays at this fps
+    optional double avg_dur_ms = 4;
+  }
+
+  // Calculate the total number of refresh rate changes
+  optional uint32 refresh_rate_switches = 4;
+
+  // The statistics for each refresh rate value
+  repeated RefreshRateStat refresh_rate_stats = 5;
 }
diff --git a/protos/perfetto/metrics/android/heap_profile_callsites.proto b/protos/perfetto/metrics/android/heap_profile_callsites.proto
deleted file mode 100644
index b8480fa..0000000
--- a/protos/perfetto/metrics/android/heap_profile_callsites.proto
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-syntax = "proto2";
-
-package perfetto.protos;
-
-import "protos/perfetto/metrics/android/process_metadata.proto";
-
-message HeapProfileCallsites {
-  message Frame {
-    optional string name = 1;
-    optional string mapping_name = 2;
-  }
-
-  message Counters {
-    // Count of objects allocated
-    optional int64 total_count = 1;
-    // Count of bytes allocated
-    optional int64 total_bytes = 2;
-
-    // Count of allocated objects that were not freed
-    optional int64 delta_count = 3;
-    // Count of allocated bytes that were not freed
-    optional int64 delta_bytes = 4;
-  }
-
-  message Callsite {
-    // The hash unambiguously identifies a callsite in a heap profile (as a
-    // traversal from the root node). It is based on the symbol names (instead
-    // of the addresses).
-    optional int64 hash = 1;
-    optional int64 parent_hash = 2;
-
-    // Leaf frame of the callsite. Use parent_hash to traverse to parent nodes.
-    optional Frame frame = 3;
-
-    optional Counters self_allocs = 4;
-    optional Counters child_allocs = 5;
-  }
-
-  // Callsites per process instance.
-  // Next id: 8
-  message InstanceStats {
-    optional uint32 pid = 1;
-    // TODO(ilkos): Remove process_name in favour of the metadata.
-    optional string process_name = 2;
-    optional AndroidProcessMetadata process = 6;
-    repeated Callsite callsites = 3;
-
-    // Bytes allocated via malloc but not freed.
-    optional int64 profile_delta_bytes = 4;
-    // Bytes allocated via malloc irrespective of whether they were freed.
-    optional int64 profile_total_bytes = 5;
-    // Peak anon RSS + swap for this process
-    optional int64 max_anon_rss_and_swap_bytes = 7;
-  }
-
-  repeated InstanceStats instance_stats = 1;
-}
diff --git a/protos/perfetto/metrics/android/hwcomposer.proto b/protos/perfetto/metrics/android/hwcomposer.proto
index c6ecac4..d9a77cd 100644
--- a/protos/perfetto/metrics/android/hwcomposer.proto
+++ b/protos/perfetto/metrics/android/hwcomposer.proto
@@ -34,4 +34,46 @@
   // Counts the number of composition surfaceflinger cached layers in the trace.
   // (non-weighted average)
   optional double composition_sf_cached_layers = 5;
+
+  // Counts how many times validateDisplay is skipped.
+  optional int32 skipped_validation_count = 6;
+
+  // Counts how many times validateDisplay cannot be skipped.
+  optional int32 unskipped_validation_count = 7;
+
+  // Counts how many times validateDisplay is already separated from presentDisplay
+  // since the beginning.
+  optional int32 separated_validation_count = 8;
+
+  // Counts how many unhandled validation cases which might be caused by errors.
+  optional int32 unknown_validation_count = 9;
+
+  // the average of overall hwcomposer execution time.
+  optional double avg_all_execution_time_ms = 10;
+
+  // the average of hwcomposer execution time for skipped validation cases.
+  optional double avg_skipped_execution_time_ms = 11;
+
+  // the average of hwcomposer execution time for unskipped validation cases.
+  optional double avg_unskipped_execution_time_ms = 12;
+
+  // the average of hwcomposer execution time for separated validation cases.
+  optional double avg_separated_execution_time_ms = 13;
+
+  message DpuVoteMetrics {
+    // the thread ID that handles this track
+    optional uint32 tid = 1;
+
+    // the weighted average of DPU Vote Clock
+    optional double avg_dpu_vote_clock = 2;
+
+    // the weighted average of DPU Vote Avg Bandwidth
+    optional double avg_dpu_vote_avg_bw = 3;
+
+    // the weighted average of DPU Vote Peak Bandwidth
+    optional double avg_dpu_vote_peak_bw = 4;
+  }
+
+  // DPU Vote Metrics for each thread track
+  repeated DpuVoteMetrics dpu_vote_metrics = 14;
 }
diff --git a/protos/perfetto/metrics/android/startup_metric.proto b/protos/perfetto/metrics/android/startup_metric.proto
index c1ac949..aef7c10 100644
--- a/protos/perfetto/metrics/android/startup_metric.proto
+++ b/protos/perfetto/metrics/android/startup_metric.proto
@@ -45,7 +45,7 @@
 
   // Timing information spanning the intent received by the
   // activity manager to the first frame drawn.
-  // Next id: 29.
+  // Next id: 31.
   message ToFirstFrame {
     // The duration between the intent received and first frame.
     optional int64 dur_ns = 1;
@@ -101,6 +101,9 @@
     // Time spent running CPU on jit thread pool.
     optional Slice time_jit_thread_pool_on_cpu = 28;
 
+    // Time spent on garbage collection.
+    optional Slice time_gc_total = 29;
+    optional Slice time_gc_on_cpu = 30;
     // Deprecated was other_process_to_activity_cpu_ratio
     reserved 12;
 
@@ -119,7 +122,26 @@
   message Activity {
     optional string name = 1;
     optional string method = 2;
-    optional Slice slice = 3;
+    optional int64 ts_method_start = 4;
+
+    // Field 3 contained Slice with a sum of durations for matching slices.
+    reserved 3;
+  }
+
+  message BinderTransaction {
+    optional Slice duration = 1;
+    optional string thread = 2;
+    optional string destination_thread = 3;
+    optional string destination_process = 4;
+    // From
+    // https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/trace/ftrace/binder.proto;l=15;drc=7b6a788162a30802f4c9d8d7a30a54e25edd30f1
+    optional string flags = 5;
+    // From
+    // https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/trace/ftrace/binder.proto;l=14;drc=7b6a788162a30802f4c9d8d7a30a54e25edd30f1
+    optional string code = 6;
+    // From
+    // https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/trace/ftrace/binder.proto;l=37;drc=7b6a788162a30802f4c9d8d7a30a54e25edd30f1
+    optional int64 data_size = 7;
   }
 
   // Metrics with information about the status of odex files and the outcome
@@ -141,7 +163,7 @@
     optional int64 first_frame = 2;
   }
 
-  // Next id: 14
+  // Next id: 15
   message Startup {
     // Random id uniquely identifying an app startup in this trace.
     optional uint32 startup_id = 1;
@@ -155,6 +177,10 @@
     // Details about the activities launched
     repeated Activity activities = 11;
 
+    // Details about slow binder transactions during the startup. The definition
+    // of a slow transaction is an implementation detail.
+    repeated BinderTransaction long_binder_transactions = 14;
+
     // Did we ask the zygote for a new process
     optional bool zygote_new_process = 4;
 
diff --git a/protos/perfetto/metrics/android/surfaceflinger.proto b/protos/perfetto/metrics/android/surfaceflinger.proto
index 8b27db5..3cc2c9e 100644
--- a/protos/perfetto/metrics/android/surfaceflinger.proto
+++ b/protos/perfetto/metrics/android/surfaceflinger.proto
@@ -27,4 +27,31 @@
 
   // Counts the number of missed GPU frames in the trace.
   optional uint32 missed_gpu_frames = 3;
+
+  // Calculate the number of missed frames divided by
+  // total frames
+  optional double missed_frame_rate = 4;
+
+  // Calculate the number of missed HWC frames divided by
+  // total HWC frames
+  optional double missed_hwc_frame_rate = 5;
+
+  // Calculate the number of missed GPU frames divided by
+  // total GPU frames
+  optional double missed_gpu_frame_rate = 6;
+
+  // Count the number of times SurfaceFlinger needs to invoke GPU
+  // for rendering some layers
+  optional uint32 gpu_invocations = 7;
+
+  // Calculate the average duration of GPU request by SurfaceFlinger
+  // since it enters the FenceMonitor's queue until it gets completed
+  optional double avg_gpu_waiting_dur_ms = 8;
+
+  // Calculate the total duration when there is at least one GPU request
+  // by SurfaceFlinger that is still waiting for GPU to complete the
+  // request.
+  // This also equals to the total duration of
+  // "waiting for GPU completion <fence_num>" in SurfaceFlinger.
+  optional double total_non_empty_gpu_waiting_dur_ms = 9;
 }
diff --git a/protos/perfetto/metrics/android/thread_time_in_state_metric.proto b/protos/perfetto/metrics/android/thread_time_in_state_metric.proto
index 6d1e2d8..f664d29 100644
--- a/protos/perfetto/metrics/android/thread_time_in_state_metric.proto
+++ b/protos/perfetto/metrics/android/thread_time_in_state_metric.proto
@@ -22,7 +22,9 @@
 
 message AndroidThreadTimeInStateMetric {
   message MetricsByCoreType {
+    optional int32 time_in_state_cpu = 5;
     optional string core_type = 1;
+
     optional int64 runtime_ms = 2;
     // CPU megacycles (i.e. cycles divided by 1e6).
     optional int64 mcycles = 3;
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index fdbb07c..164377f 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -25,7 +25,6 @@
 import "protos/perfetto/metrics/android/fastrpc_metric.proto";
 import "protos/perfetto/metrics/android/g2d_metric.proto";
 import "protos/perfetto/metrics/android/gpu_metric.proto";
-import "protos/perfetto/metrics/android/heap_profile_callsites.proto";
 import "protos/perfetto/metrics/android/hwcomposer.proto";
 import "protos/perfetto/metrics/android/hwui_metric.proto";
 import "protos/perfetto/metrics/android/ion_metric.proto";
@@ -91,7 +90,7 @@
 //
 // Next id: 34
 message TraceMetrics {
-  reserved 4, 10, 13, 14, 19;
+  reserved 4, 10, 13, 14, 16, 19;
 
   // Battery counters metric on Android.
   optional AndroidBatteryMetric android_batt = 5;
@@ -126,9 +125,6 @@
   // Startup metrics on Android (owned by the Android Telemetry team).
   optional AndroidStartupMetric android_startup = 2;
 
-  // Heap profiler callsite statistics.
-  optional HeapProfileCallsites heap_profile_callsites = 16;
-
   // Trace metadata (applicable to all traces).
   optional TraceMetadata trace_metadata = 3;
 
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 4f77dcb..934a166 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -133,6 +133,27 @@
 
   // Stat that reports the number of dpu underrrun occurs count.
   optional uint32 total_dpu_underrun_count = 3;
+
+
+  message RefreshRateStat {
+    // The refresh rate value (the number of frames per second)
+    optional uint32 refresh_rate_fps = 1;
+
+    // Calculate the number of refresh rate switches to this fps
+    optional uint32 count = 2;
+
+    // Calculate the total duration of refresh rate stays at this fps
+    optional double total_dur_ms = 3;
+
+    // Calculate the average duration of refresh rate stays at this fps
+    optional double avg_dur_ms = 4;
+  }
+
+  // Calculate the total number of refresh rate changes
+  optional uint32 refresh_rate_switches = 4;
+
+  // The statistics for each refresh rate value
+  repeated RefreshRateStat refresh_rate_stats = 5;
 }
 
 // End of protos/perfetto/metrics/android/display_metrics.proto
@@ -230,96 +251,6 @@
 
 // End of protos/perfetto/metrics/android/gpu_metric.proto
 
-// Begin of protos/perfetto/metrics/android/process_metadata.proto
-
-message AndroidProcessMetadata {
-  // Process name. Usually, cmdline or <package_name>(:<custom_name>)?.
-  optional string name = 1;
-
-  // User id under which this process runs.
-  optional int64 uid = 2;
-
-  // Package metadata from Android package list.
-  message Package {
-    optional string package_name = 1;
-    optional int64 apk_version_code = 2;
-    optional bool debuggable = 3;
-  }
-
-  // Package that this process belongs to.
-  //
-  // If this process shares its uid (see `packages_for_uid` field), the package
-  // is determined based on the process name and package name. If there is no
-  // match this field is empty.
-  optional Package package = 7;
-
-  // All packages using this uid.
-  //
-  // Shared uid documentation:
-  // https://developer.android.com/guide/topics/manifest/manifest-element#uid
-  repeated Package packages_for_uid = 8;
-
-  reserved 3, 4, 5, 6;
-}
-
-// End of protos/perfetto/metrics/android/process_metadata.proto
-
-// Begin of protos/perfetto/metrics/android/heap_profile_callsites.proto
-
-message HeapProfileCallsites {
-  message Frame {
-    optional string name = 1;
-    optional string mapping_name = 2;
-  }
-
-  message Counters {
-    // Count of objects allocated
-    optional int64 total_count = 1;
-    // Count of bytes allocated
-    optional int64 total_bytes = 2;
-
-    // Count of allocated objects that were not freed
-    optional int64 delta_count = 3;
-    // Count of allocated bytes that were not freed
-    optional int64 delta_bytes = 4;
-  }
-
-  message Callsite {
-    // The hash unambiguously identifies a callsite in a heap profile (as a
-    // traversal from the root node). It is based on the symbol names (instead
-    // of the addresses).
-    optional int64 hash = 1;
-    optional int64 parent_hash = 2;
-
-    // Leaf frame of the callsite. Use parent_hash to traverse to parent nodes.
-    optional Frame frame = 3;
-
-    optional Counters self_allocs = 4;
-    optional Counters child_allocs = 5;
-  }
-
-  // Callsites per process instance.
-  // Next id: 8
-  message InstanceStats {
-    optional uint32 pid = 1;
-    // TODO(ilkos): Remove process_name in favour of the metadata.
-    optional string process_name = 2;
-    optional AndroidProcessMetadata process = 6;
-    repeated Callsite callsites = 3;
-
-    // Bytes allocated via malloc but not freed.
-    optional int64 profile_delta_bytes = 4;
-    // Bytes allocated via malloc irrespective of whether they were freed.
-    optional int64 profile_total_bytes = 5;
-    // Peak anon RSS + swap for this process
-    optional int64 max_anon_rss_and_swap_bytes = 7;
-  }
-
-  repeated InstanceStats instance_stats = 1;
-}
-
-// End of protos/perfetto/metrics/android/heap_profile_callsites.proto
-
 // Begin of protos/perfetto/metrics/android/hwcomposer.proto
 
 message AndroidHwcomposerMetrics {
@@ -338,6 +269,48 @@
   // Counts the number of composition surfaceflinger cached layers in the trace.
   // (non-weighted average)
   optional double composition_sf_cached_layers = 5;
+
+  // Counts how many times validateDisplay is skipped.
+  optional int32 skipped_validation_count = 6;
+
+  // Counts how many times validateDisplay cannot be skipped.
+  optional int32 unskipped_validation_count = 7;
+
+  // Counts how many times validateDisplay is already separated from presentDisplay
+  // since the beginning.
+  optional int32 separated_validation_count = 8;
+
+  // Counts how many unhandled validation cases which might be caused by errors.
+  optional int32 unknown_validation_count = 9;
+
+  // the average of overall hwcomposer execution time.
+  optional double avg_all_execution_time_ms = 10;
+
+  // the average of hwcomposer execution time for skipped validation cases.
+  optional double avg_skipped_execution_time_ms = 11;
+
+  // the average of hwcomposer execution time for unskipped validation cases.
+  optional double avg_unskipped_execution_time_ms = 12;
+
+  // the average of hwcomposer execution time for separated validation cases.
+  optional double avg_separated_execution_time_ms = 13;
+
+  message DpuVoteMetrics {
+    // the thread ID that handles this track
+    optional uint32 tid = 1;
+
+    // the weighted average of DPU Vote Clock
+    optional double avg_dpu_vote_clock = 2;
+
+    // the weighted average of DPU Vote Avg Bandwidth
+    optional double avg_dpu_vote_avg_bw = 3;
+
+    // the weighted average of DPU Vote Peak Bandwidth
+    optional double avg_dpu_vote_peak_bw = 4;
+  }
+
+  // DPU Vote Metrics for each thread track
+  repeated DpuVoteMetrics dpu_vote_metrics = 14;
 }
 
 // End of protos/perfetto/metrics/android/hwcomposer.proto
@@ -465,6 +438,40 @@
 }
 // End of protos/perfetto/metrics/android/jank_metric.proto
 
+// Begin of protos/perfetto/metrics/android/process_metadata.proto
+
+message AndroidProcessMetadata {
+  // Process name. Usually, cmdline or <package_name>(:<custom_name>)?.
+  optional string name = 1;
+
+  // User id under which this process runs.
+  optional int64 uid = 2;
+
+  // Package metadata from Android package list.
+  message Package {
+    optional string package_name = 1;
+    optional int64 apk_version_code = 2;
+    optional bool debuggable = 3;
+  }
+
+  // Package that this process belongs to.
+  //
+  // If this process shares its uid (see `packages_for_uid` field), the package
+  // is determined based on the process name and package name. If there is no
+  // match this field is empty.
+  optional Package package = 7;
+
+  // All packages using this uid.
+  //
+  // Shared uid documentation:
+  // https://developer.android.com/guide/topics/manifest/manifest-element#uid
+  repeated Package packages_for_uid = 8;
+
+  reserved 3, 4, 5, 6;
+}
+
+// End of protos/perfetto/metrics/android/process_metadata.proto
+
 // Begin of protos/perfetto/metrics/android/java_heap_histogram.proto
 
 message JavaHeapHistogram {
@@ -720,7 +727,7 @@
 
   // Timing information spanning the intent received by the
   // activity manager to the first frame drawn.
-  // Next id: 29.
+  // Next id: 31.
   message ToFirstFrame {
     // The duration between the intent received and first frame.
     optional int64 dur_ns = 1;
@@ -776,6 +783,9 @@
     // Time spent running CPU on jit thread pool.
     optional Slice time_jit_thread_pool_on_cpu = 28;
 
+    // Time spent on garbage collection.
+    optional Slice time_gc_total = 29;
+    optional Slice time_gc_on_cpu = 30;
     // Deprecated was other_process_to_activity_cpu_ratio
     reserved 12;
 
@@ -794,7 +804,26 @@
   message Activity {
     optional string name = 1;
     optional string method = 2;
-    optional Slice slice = 3;
+    optional int64 ts_method_start = 4;
+
+    // Field 3 contained Slice with a sum of durations for matching slices.
+    reserved 3;
+  }
+
+  message BinderTransaction {
+    optional Slice duration = 1;
+    optional string thread = 2;
+    optional string destination_thread = 3;
+    optional string destination_process = 4;
+    // From
+    // https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/trace/ftrace/binder.proto;l=15;drc=7b6a788162a30802f4c9d8d7a30a54e25edd30f1
+    optional string flags = 5;
+    // From
+    // https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/trace/ftrace/binder.proto;l=14;drc=7b6a788162a30802f4c9d8d7a30a54e25edd30f1
+    optional string code = 6;
+    // From
+    // https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/trace/ftrace/binder.proto;l=37;drc=7b6a788162a30802f4c9d8d7a30a54e25edd30f1
+    optional int64 data_size = 7;
   }
 
   // Metrics with information about the status of odex files and the outcome
@@ -816,7 +845,7 @@
     optional int64 first_frame = 2;
   }
 
-  // Next id: 14
+  // Next id: 15
   message Startup {
     // Random id uniquely identifying an app startup in this trace.
     optional uint32 startup_id = 1;
@@ -830,6 +859,10 @@
     // Details about the activities launched
     repeated Activity activities = 11;
 
+    // Details about slow binder transactions during the startup. The definition
+    // of a slow transaction is an implementation detail.
+    repeated BinderTransaction long_binder_transactions = 14;
+
     // Did we ask the zygote for a new process
     optional bool zygote_new_process = 4;
 
@@ -881,6 +914,33 @@
 
   // Counts the number of missed GPU frames in the trace.
   optional uint32 missed_gpu_frames = 3;
+
+  // Calculate the number of missed frames divided by
+  // total frames
+  optional double missed_frame_rate = 4;
+
+  // Calculate the number of missed HWC frames divided by
+  // total HWC frames
+  optional double missed_hwc_frame_rate = 5;
+
+  // Calculate the number of missed GPU frames divided by
+  // total GPU frames
+  optional double missed_gpu_frame_rate = 6;
+
+  // Count the number of times SurfaceFlinger needs to invoke GPU
+  // for rendering some layers
+  optional uint32 gpu_invocations = 7;
+
+  // Calculate the average duration of GPU request by SurfaceFlinger
+  // since it enters the FenceMonitor's queue until it gets completed
+  optional double avg_gpu_waiting_dur_ms = 8;
+
+  // Calculate the total duration when there is at least one GPU request
+  // by SurfaceFlinger that is still waiting for GPU to complete the
+  // request.
+  // This also equals to the total duration of
+  // "waiting for GPU completion <fence_num>" in SurfaceFlinger.
+  optional double total_non_empty_gpu_waiting_dur_ms = 9;
 }
 
 // End of protos/perfetto/metrics/android/surfaceflinger.proto
@@ -943,7 +1003,9 @@
 
 message AndroidThreadTimeInStateMetric {
   message MetricsByCoreType {
+    optional int32 time_in_state_cpu = 5;
     optional string core_type = 1;
+
     optional int64 runtime_ms = 2;
     // CPU megacycles (i.e. cycles divided by 1e6).
     optional int64 mcycles = 3;
@@ -1030,7 +1092,7 @@
 //
 // Next id: 34
 message TraceMetrics {
-  reserved 4, 10, 13, 14, 19;
+  reserved 4, 10, 13, 14, 16, 19;
 
   // Battery counters metric on Android.
   optional AndroidBatteryMetric android_batt = 5;
@@ -1065,9 +1127,6 @@
   // Startup metrics on Android (owned by the Android Telemetry team).
   optional AndroidStartupMetric android_startup = 2;
 
-  // Heap profiler callsite statistics.
-  optional HeapProfileCallsites heap_profile_callsites = 16;
-
   // Trace metadata (applicable to all traces).
   optional TraceMetadata trace_metadata = 3;
 
diff --git a/protos/perfetto/trace/BUILD.gn b/protos/perfetto/trace/BUILD.gn
index 7861978..a807061 100644
--- a/protos/perfetto/trace/BUILD.gn
+++ b/protos/perfetto/trace/BUILD.gn
@@ -29,6 +29,7 @@
 proto_sources_non_minimal = [
   "trace_packet_defaults.proto",
   "test_event.proto",
+  "test_extensions.proto",
   "trace_packet.proto",
   "trace.proto",
   "extension_descriptor.proto",
@@ -75,6 +76,7 @@
     "cpp",
     "lite",
     "zero",
+    "source_set",
   ]
   deps = [
     ":minimal_@TYPE@",
@@ -108,12 +110,11 @@
   sources = [ "perfetto_trace.proto" ]
 }
 
-if (perfetto_build_standalone) {
-  perfetto_proto_library("descriptor") {
-    proto_generators = [ "descriptor" ]
-    generate_descriptor = "trace.descriptor"
-    sources = [ "trace.proto" ]
-  }
+perfetto_proto_library("descriptor") {
+  proto_generators = [ "descriptor" ]
+  generate_descriptor = "trace.descriptor"
+  sources = [ "trace.proto" ]
+  deps = [ ":non_minimal_source_set" ]
 }
 
 # This target exports perfetto trace protos allowing both host and device
diff --git a/protos/perfetto/trace/android/frame_timeline_event.proto b/protos/perfetto/trace/android/frame_timeline_event.proto
index bc1983f..bee0bc7 100644
--- a/protos/perfetto/trace/android/frame_timeline_event.proto
+++ b/protos/perfetto/trace/android/frame_timeline_event.proto
@@ -41,6 +41,7 @@
     JANK_APP_DEADLINE_MISSED = 64;
     JANK_BUFFER_STUFFING = 128;
     JANK_UNKNOWN = 256;
+    JANK_SF_STUFFING = 512;
   };
 
   // Specifies how a frame was presented on screen w.r.t. timing.
diff --git a/protos/perfetto/trace/ftrace/BUILD.gn b/protos/perfetto/trace/ftrace/BUILD.gn
index 5684450..7edf7fe 100644
--- a/protos/perfetto/trace/ftrace/BUILD.gn
+++ b/protos/perfetto/trace/ftrace/BUILD.gn
@@ -21,6 +21,7 @@
     "cpp",
     "zero",
     "lite",
+    "source_set",
   ]
   sources = ftrace_proto_names
 }
diff --git a/protos/perfetto/trace/ftrace/cpuhp.proto b/protos/perfetto/trace/ftrace/cpuhp.proto
index 3214b51..dc45682 100644
--- a/protos/perfetto/trace/ftrace/cpuhp.proto
+++ b/protos/perfetto/trace/ftrace/cpuhp.proto
@@ -29,3 +29,9 @@
   optional uint32 state = 3;
   optional uint64 time = 4;
 }
+message CpuhpPauseFtraceEvent {
+  optional uint32 active_cpus = 1;
+  optional uint32 cpus = 2;
+  optional uint32 pause = 3;
+  optional uint32 time = 4;
+}
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index e8ac22a..31e5b92 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -440,5 +440,12 @@
     G2dTracingMarkWriteFtraceEvent g2d_tracing_mark_write = 349;
     MaliTracingMarkWriteFtraceEvent mali_tracing_mark_write = 350;
     DmaHeapStatFtraceEvent dma_heap_stat = 351;
+    CpuhpPauseFtraceEvent cpuhp_pause = 352;
+    SchedPiSetprioFtraceEvent sched_pi_setprio = 353;
+    SdeSdeEvtlogFtraceEvent sde_sde_evtlog = 354;
+    SdeSdePerfCalcCrtcFtraceEvent sde_sde_perf_calc_crtc = 355;
+    SdeSdePerfCrtcUpdateFtraceEvent sde_sde_perf_crtc_update = 356;
+    SdeSdePerfSetQosLutsFtraceEvent sde_sde_perf_set_qos_luts = 357;
+    SdeSdePerfUpdateBusFtraceEvent sde_sde_perf_update_bus = 358;
   }
 }
diff --git a/protos/perfetto/trace/ftrace/sched.proto b/protos/perfetto/trace/ftrace/sched.proto
index 9c31fac..87f409b 100644
--- a/protos/perfetto/trace/ftrace/sched.proto
+++ b/protos/perfetto/trace/ftrace/sched.proto
@@ -76,3 +76,9 @@
   optional int32 pid = 2;
   optional int32 prio = 3;
 }
+message SchedPiSetprioFtraceEvent {
+  optional string comm = 1;
+  optional int32 newprio = 2;
+  optional int32 oldprio = 3;
+  optional int32 pid = 4;
+}
diff --git a/protos/perfetto/trace/ftrace/sde.proto b/protos/perfetto/trace/ftrace/sde.proto
index 724a9a3..61c8d7b 100644
--- a/protos/perfetto/trace/ftrace/sde.proto
+++ b/protos/perfetto/trace/ftrace/sde.proto
@@ -12,3 +12,46 @@
   optional int32 value = 4;
   optional uint32 trace_begin = 5;
 }
+message SdeSdeEvtlogFtraceEvent {
+  optional string evtlog_tag = 1;
+  optional int32 pid = 2;
+  optional uint32 tag_id = 3;
+}
+message SdeSdePerfCalcCrtcFtraceEvent {
+  optional uint64 bw_ctl_ebi = 1;
+  optional uint64 bw_ctl_llcc = 2;
+  optional uint64 bw_ctl_mnoc = 3;
+  optional uint32 core_clk_rate = 4;
+  optional uint32 crtc = 5;
+  optional uint64 ib_ebi = 6;
+  optional uint64 ib_llcc = 7;
+  optional uint64 ib_mnoc = 8;
+}
+message SdeSdePerfCrtcUpdateFtraceEvent {
+  optional uint64 bw_ctl_ebi = 1;
+  optional uint64 bw_ctl_llcc = 2;
+  optional uint64 bw_ctl_mnoc = 3;
+  optional uint32 core_clk_rate = 4;
+  optional uint32 crtc = 5;
+  optional int32 params = 6;
+  optional uint64 per_pipe_ib_ebi = 7;
+  optional uint64 per_pipe_ib_llcc = 8;
+  optional uint64 per_pipe_ib_mnoc = 9;
+  optional uint32 stop_req = 10;
+  optional uint32 update_bus = 11;
+  optional uint32 update_clk = 12;
+}
+message SdeSdePerfSetQosLutsFtraceEvent {
+  optional uint32 fl = 1;
+  optional uint32 fmt = 2;
+  optional uint64 lut = 3;
+  optional uint32 lut_usage = 4;
+  optional uint32 pnum = 5;
+  optional uint32 rt = 6;
+}
+message SdeSdePerfUpdateBusFtraceEvent {
+  optional uint64 ab_quota = 1;
+  optional uint32 bus_id = 2;
+  optional int32 client = 3;
+  optional uint64 ib_quota = 4;
+}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 0ed3f48..4ee4df6 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -197,6 +197,12 @@
 
     // Unix uid of the remote process.
     optional int32 uid = 3;
+
+    // The version of the client library used by the producer.
+    // This is a human readable string with and its format varies depending on
+    // the build system and the repo (standalone vs AOSP).
+    // This is intended for human debugging only.
+    optional string sdk_version = 4;
   }
 
   // Describes a data source registered by a producer. Data sources are listed
@@ -220,6 +226,12 @@
 
   // Number of tracing sessions in the started state. Always <= num_sessions.
   optional int32 num_sessions_started = 4;
+
+  // The version of traced (the same returned by `traced --version`).
+  // This is a human readable string with and its format varies depending on
+  // the build system and the repo (standalone vs AOSP).
+  // This is intended for human debugging only.
+  optional string tracing_service_version = 5;
 }
 
 // End of protos/perfetto/common/tracing_service_state.proto
@@ -559,7 +571,7 @@
 // Begin of protos/perfetto/config/profiling/heapprofd_config.proto
 
 // Configuration for go/heapprofd.
-// Next id: 26
+// Next id: 27
 message HeapprofdConfig {
   message ContinuousDumpConfig {
     // ms to wait before first dump.
@@ -607,12 +619,26 @@
   // For watermark based triggering or local debugging.
   repeated uint64 pid = 4;
 
+  // Only profile target if it was installed by one of the packages given.
+  // Special values are:
+  // * @system: installed on the system partition
+  // * @product: installed on the product partition
+  // * @null: sideloaded
+  // Supported on Android 12+.
+  repeated string target_installed_by = 26;
+
   // Which heaps to sample, e.g. "libc.malloc". If left empty, only samples
   // "malloc".
   //
   // Introduced in Android 12.
   repeated string heaps = 20;
 
+  // Which heaps not to sample, e.g. "libc.malloc". This is useful when used in
+  // combination with all_heaps;
+  //
+  // Introduced in Android 12.
+  repeated string exclude_heaps = 27;
+
   optional bool stream_allocations = 23;
 
   // If given, needs to be the same length as heaps and gives the sampling
@@ -747,6 +773,14 @@
   // For watermark based triggering or local debugging.
   repeated uint64 pid = 2;
 
+  // Only profile target if it was installed by one of the packages given.
+  // Special values are:
+  // * @system: installed on the system partition
+  // * @product: installed on the product partition
+  // * @null: sideloaded
+  // Supported on Android 12+.
+  repeated string target_installed_by = 7;
+
   // Dump at a predefined interval.
   optional ContinuousDumpConfig continuous_dump_config = 3;
 
@@ -847,7 +881,7 @@
 //     }
 //   }
 //
-// Next id: 18
+// Next id: 19
 message PerfEventConfig {
   // What event to sample on, and how often.
   // Defined in common/perf_events.proto.
@@ -918,6 +952,14 @@
   optional bool kernel_frames = 12;
   repeated int32 target_pid = 4;
   repeated string target_cmdline = 5;
+
+  // Only profile target if it was installed by one of the packages given.
+  // Special values are:
+  // * @system: installed on the system partition
+  // * @product: installed on the product partition
+  // * @null: sideloaded
+  // Supported on Android 12+.
+  repeated string target_installed_by = 18;
   repeated int32 exclude_pid = 6;
   repeated string exclude_cmdline = 7;
   optional uint32 additional_cmdline_count = 11;
@@ -1190,6 +1232,12 @@
     STAT_FORK_COUNT = 4;
   }
   repeated StatCounters stat_counters = 6;
+
+  // Polls /sys/devfreq/*/curfreq every X ms, if non-zero.
+  // This is required to be > 10ms to avoid excessive CPU usage.
+  // This option can be used to record unchanging values.
+  // Updates from frequency changes can come from ftrace/set_clock_rate.
+  optional uint32 devfreq_period_ms = 7;
 }
 
 // End of protos/perfetto/config/sys_stats/sys_stats_config.proto
@@ -1285,7 +1333,7 @@
   // Default: []
   repeated string enabled_categories = 2;
 
-  // Default: [“slow”, “debug”]
+  // Default: ["slow", "debug"]
   repeated string disabled_tags = 3;
 
   // Default: []
@@ -1299,6 +1347,14 @@
 // The configuration that is passed to each data source when starting tracing.
 // Next id: 116
 message DataSourceConfig {
+  enum SessionInitiator {
+    SESSION_INITIATOR_UNSPECIFIED = 0;
+    // This trace was initiated from a trusted system app has DUMP and
+    // USAGE_STATS permission. This system app is expected to not expose the
+    // trace to the user of the device.
+    // This is determined by checking the UID initiating the trace.
+    SESSION_INITIATOR_TRUSTED_SYSTEM = 1;
+  };
   // Data source unique name, e.g., "linux.ftrace". This must match
   // the name passed by the data source when it registers (see
   // RegisterDataSource()).
@@ -1325,6 +1381,10 @@
   // DO NOT SET in consumer as this will be overridden by the service.
   optional bool enable_extra_guardrails = 6;
 
+  // Set by the service to indicate which user initiated this trace.
+  // DO NOT SET in consumer as this will be overridden by the service.
+  optional SessionInitiator session_initiator = 8;
+
   // Set by the service to indicate which tracing session the data source
   // belongs to. The intended use case for this is checking if two data sources,
   // one of which produces metadata for the other one, belong to the same trace
@@ -1414,7 +1474,7 @@
 // It contains the general config for the logging buffer(s) and the configs for
 // all the data source being enabled.
 //
-// Next id: 31.
+// Next id: 33.
 message TraceConfig {
   message BufferConfig {
     optional uint32 size_kb = 1;
@@ -1491,6 +1551,21 @@
     // only keeps track of the first and the most recent snapshot until
     // ReadBuffers() is called.
     optional uint32 snapshot_interval_ms = 6;
+
+    // Hints to the service that a suspend-aware (i.e. counting time in suspend)
+    // clock should be used for periodic snapshots of service-emitted events.
+    // This means, if a snapshot *should* have happened during suspend, it will
+    // happen immediately after the device resumes.
+    //
+    // Choosing a clock like this is done on best-effort basis; not all
+    // platforms (e.g. Windows) expose a clock which can be used for periodic
+    // tasks counting suspend. If such a clock is not available, the service
+    // falls back to the best-available alternative.
+    //
+    // Introduced in Android S.
+    // TODO(lalitm): deprecate this in T and make this the default if nothing
+    // crashes in S.
+    optional bool prefer_suspend_clock_for_snapshot = 7;
   }
   optional BuiltinDataSource builtin_data_sources = 20;
 
@@ -1794,6 +1869,15 @@
   // Alternative encoding of trace_uuid as two int64s.
   optional int64 trace_uuid_msb = 27;
   optional int64 trace_uuid_lsb = 28;
+
+  // When set applies a post-filter to the trace contents using the filter
+  // provided. The filter is applied at ReadBuffers() time and works both in the
+  // case of IPC readback and write_into_file. This filter can be generated
+  // using `tools/proto_filter -s schema.proto -F filter_out.bytes` or
+  // `-T filter_out.escaped_string` (for .pbtx).
+  // Introduced in Android S. See go/trace-filtering for design.
+  message TraceFilter { optional bytes bytecode = 1; }
+  optional TraceFilter trace_filter = 32;
 }
 
 // End of protos/perfetto/config/trace_config.proto
@@ -1940,6 +2024,15 @@
   // Packets that failed validation of the TrustedPacket. If this is > 0, there
   // is a bug in the producer.
   optional uint64 invalid_packets = 10;
+
+  // This is set only when the TraceConfig specifies a TraceFilter.
+  message FilterStats {
+    optional uint64 input_packets = 1;
+    optional uint64 input_bytes = 2;
+    optional uint64 output_bytes = 3;
+    optional uint64 errors = 4;
+  }
+  optional FilterStats filter_stats = 11;
 }
 
 // End of protos/perfetto/common/trace_stats.proto
@@ -2031,6 +2124,7 @@
     JANK_APP_DEADLINE_MISSED = 64;
     JANK_BUFFER_STUFFING = 128;
     JANK_UNKNOWN = 256;
+    JANK_SF_STUFFING = 512;
   };
 
   // Specifies how a frame was presented on screen w.r.t. timing.
@@ -3121,6 +3215,12 @@
   optional uint32 state = 3;
   optional uint64 time = 4;
 }
+message CpuhpPauseFtraceEvent {
+  optional uint32 active_cpus = 1;
+  optional uint32 cpus = 2;
+  optional uint32 pause = 3;
+  optional uint32 time = 4;
+}
 
 // End of protos/perfetto/trace/ftrace/cpuhp.proto
 
@@ -4868,6 +4968,12 @@
   optional int32 pid = 2;
   optional int32 prio = 3;
 }
+message SchedPiSetprioFtraceEvent {
+  optional string comm = 1;
+  optional int32 newprio = 2;
+  optional int32 oldprio = 3;
+  optional int32 pid = 4;
+}
 
 // End of protos/perfetto/trace/ftrace/sched.proto
 
@@ -4891,6 +4997,49 @@
   optional int32 value = 4;
   optional uint32 trace_begin = 5;
 }
+message SdeSdeEvtlogFtraceEvent {
+  optional string evtlog_tag = 1;
+  optional int32 pid = 2;
+  optional uint32 tag_id = 3;
+}
+message SdeSdePerfCalcCrtcFtraceEvent {
+  optional uint64 bw_ctl_ebi = 1;
+  optional uint64 bw_ctl_llcc = 2;
+  optional uint64 bw_ctl_mnoc = 3;
+  optional uint32 core_clk_rate = 4;
+  optional uint32 crtc = 5;
+  optional uint64 ib_ebi = 6;
+  optional uint64 ib_llcc = 7;
+  optional uint64 ib_mnoc = 8;
+}
+message SdeSdePerfCrtcUpdateFtraceEvent {
+  optional uint64 bw_ctl_ebi = 1;
+  optional uint64 bw_ctl_llcc = 2;
+  optional uint64 bw_ctl_mnoc = 3;
+  optional uint32 core_clk_rate = 4;
+  optional uint32 crtc = 5;
+  optional int32 params = 6;
+  optional uint64 per_pipe_ib_ebi = 7;
+  optional uint64 per_pipe_ib_llcc = 8;
+  optional uint64 per_pipe_ib_mnoc = 9;
+  optional uint32 stop_req = 10;
+  optional uint32 update_bus = 11;
+  optional uint32 update_clk = 12;
+}
+message SdeSdePerfSetQosLutsFtraceEvent {
+  optional uint32 fl = 1;
+  optional uint32 fmt = 2;
+  optional uint64 lut = 3;
+  optional uint32 lut_usage = 4;
+  optional uint32 pnum = 5;
+  optional uint32 rt = 6;
+}
+message SdeSdePerfUpdateBusFtraceEvent {
+  optional uint64 ab_quota = 1;
+  optional uint32 bus_id = 2;
+  optional int32 client = 3;
+  optional uint64 ib_quota = 4;
+}
 
 // End of protos/perfetto/trace/ftrace/sde.proto
 
@@ -5394,6 +5543,13 @@
     G2dTracingMarkWriteFtraceEvent g2d_tracing_mark_write = 349;
     MaliTracingMarkWriteFtraceEvent mali_tracing_mark_write = 350;
     DmaHeapStatFtraceEvent dma_heap_stat = 351;
+    CpuhpPauseFtraceEvent cpuhp_pause = 352;
+    SchedPiSetprioFtraceEvent sched_pi_setprio = 353;
+    SdeSdeEvtlogFtraceEvent sde_sde_evtlog = 354;
+    SdeSdePerfCalcCrtcFtraceEvent sde_sde_perf_calc_crtc = 355;
+    SdeSdePerfCrtcUpdateFtraceEvent sde_sde_perf_crtc_update = 356;
+    SdeSdePerfSetQosLutsFtraceEvent sde_sde_perf_set_qos_luts = 357;
+    SdeSdePerfUpdateBusFtraceEvent sde_sde_perf_update_bus = 358;
   }
 }
 
@@ -5974,30 +6130,48 @@
 
 // Begin of protos/perfetto/trace/track_event/debug_annotation.proto
 
-// Key/value annotations provided in untyped TRACE_EVENT macros. These
-// annotations are intended for debug use and are not considered a stable API
-// surface. As such, they should not be relied upon to implement (new) metrics.
+// Proto representation of untyped key/value annotations provided in TRACE_EVENT
+// macros. Users of the Perfetto SDK should prefer to use the
+// perfetto::TracedValue API to fill these protos, rather than filling them
+// manually.
 //
-// Next ID: 10.
+// Debug annotations are intended for debug use and are not considered a stable
+// API of the trace contents. Trace-based metrics that use debug annotation
+// values are prone to breakage, so please rely on typed TrackEvent fields for
+// these instead.
+//
+// DebugAnnotations support nested arrays and dictionaries. Each entry is
+// encoded as a single DebugAnnotation message. Only dictionary entries
+// set the "name" field. The TrackEvent message forms an implicit root
+// dictionary.
+//
+// Example TrackEvent with nested annotations:
+//   track_event {
+//     debug_annotations {
+//       name: "foo"
+//       dict_entries {
+//         name: "a"
+//         bool_value: true
+//       }
+//       dict_entries {
+//         name: "b"
+//         int_value: 123
+//       }
+//     }
+//     debug_annotations {
+//       name: "bar"
+//       array_values {
+//         string_value: "hello"
+//       }
+//       array_values {
+//         string_value: "world"
+//       }
+//     }
+//   }
+//
+// Next ID: 13.
 message DebugAnnotation {
-  message NestedValue {
-    enum NestedType {
-      // leaf value.
-      UNSPECIFIED = 0;
-      DICT = 1;
-      ARRAY = 2;
-    }
-    optional NestedType nested_type = 1;
-
-    repeated string dict_keys = 2;
-    repeated NestedValue dict_values = 3;
-    repeated NestedValue array_values = 4;
-    optional int64 int_value = 5;
-    optional double double_value = 6;
-    optional bool bool_value = 7;
-    optional string string_value = 8;
-  }
-
+  // Name fields are set only for dictionary entries.
   oneof name_field {
     // interned DebugAnnotationName.
     uint64 name_iid = 1;
@@ -6014,12 +6188,38 @@
     // Pointers are stored in a separate type as the JSON output treats them
     // differently from other uint64 values.
     uint64 pointer_value = 7;
+
+    // Deprecated. Use dict_entries / array_values instead.
     NestedValue nested_value = 8;
 
     // Legacy instrumentation may not support conversion of nested data to
     // NestedValue yet.
     string legacy_json_value = 9;
   }
+
+  repeated DebugAnnotation dict_entries = 11;
+  repeated DebugAnnotation array_values = 12;
+
+  // Deprecated legacy way to use nested values. Only kept for
+  // backwards-compatibility in TraceProcessor. May be removed in the future -
+  // code filling protos should use |dict_entries| and |array_values| instead.
+  message NestedValue {
+    enum NestedType {
+      // leaf value.
+      UNSPECIFIED = 0;
+      DICT = 1;
+      ARRAY = 2;
+    }
+    optional NestedType nested_type = 1;
+
+    repeated string dict_keys = 2;
+    repeated NestedValue dict_values = 3;
+    repeated NestedValue array_values = 4;
+    optional int64 int_value = 5;
+    optional double double_value = 6;
+    optional bool bool_value = 7;
+    optional string string_value = 8;
+  }
 }
 
 // --------------------
@@ -6423,6 +6623,9 @@
   // Whether the frame contained any missing content (i.e. whether there was
   // checkerboarding in the frame).
   optional bool has_missing_content = 10;
+
+  // The id of layer_tree_host that the frame has been produced for.
+  optional uint64 layer_tree_host_id = 11;
 }
 
 // End of protos/perfetto/trace/track_event/chrome_frame_reporter.proto
@@ -7397,8 +7600,9 @@
   optional float capacity_percent = 2;
 
   // Instantaneous battery current in microamperes(µA).
-  // Positive values indicate net current entering the battery from a charge
-  // source, negative values indicate net current discharging from the battery.
+  // Positive values indicate current drained from the battery,
+  // negative values current feeding the battery from a charge source (USB).
+  // See https://perfetto.dev/docs/data-sources/battery-counters for more.
   optional int64 current_ua = 3;
 
   // Instantaneous battery current in microamperes(µA).
@@ -8106,6 +8310,15 @@
   // the top-level packet timestamp is the time at which
   // we begin collection.
   optional uint64 collection_end_timestamp = 9;
+
+  // Frequencies for /sys/class/devfreq/ entries in kHz.
+  message DevfreqValue {
+    optional string key = 1;
+    optional uint64 value = 2;
+  };
+
+  // One entry per device.
+  repeated DevfreqValue devfreq = 10;
 }
 
 // End of protos/perfetto/trace/sys_stats/sys_stats.proto
@@ -8125,6 +8338,12 @@
 
   // Ticks per second - sysconf(_SC_CLK_TCK).
   optional int64 hz = 3;
+
+  // The version of traced (the same returned by `traced --version`).
+  // This is a human readable string with and its format varies depending on
+  // the build system and the repo (standalone vs AOSP).
+  // This is intended for human debugging only.
+  optional string tracing_service_version = 4;
 }
 
 // End of protos/perfetto/trace/system_info.proto
@@ -8171,8 +8390,15 @@
     repeated string str = 1;
     repeated TestPayload nested = 2;
 
+    optional string single_string = 4;
+
+    optional int32 single_int = 5;
+    repeated int32 repeated_ints = 6;
+
     // When 0 this is the bottom-most nested message.
     optional uint32 remaining_nesting_depth = 3;
+
+    repeated DebugAnnotation debug_annotations = 7;
   }
   optional TestPayload payload = 5;
 }
@@ -8626,13 +8852,23 @@
 
   // Indicates that the given process should be highlighted by the UI.
   message HighlightProcess {
-    // The pid of the process to highlight. This is useful for UIs to focus
-    // on tracks of a particular process in the trace.
-    //
-    // If more than one process in a trace has the same pid, it is UI
-    // implementation specific how the process to be focused will be
-    // chosen.
-    optional uint32 pid = 1;
+    oneof selector {
+      // The pid of the process to highlight. This is useful for UIs to focus
+      // on tracks of a particular process in the trace.
+      //
+      // If more than one process in a trace has the same pid, it is UI
+      // implementation specific how the process to be focused will be
+      // chosen.
+      uint32 pid = 1;
+
+      // The command line of the process to highlight; for most Android apps,
+      // this is the package name of the app. This is useful for UIs to focus
+      // on a particular app in the trace.
+      //
+      // If more than one process hasthe same cmdline, it is UI implementation
+      // specific how the process to be focused will be chosen.
+      string cmdline = 2;
+    }
   }
   optional HighlightProcess highlight_process = 3;
 }
diff --git a/protos/perfetto/trace/power/battery_counters.proto b/protos/perfetto/trace/power/battery_counters.proto
index 2337c41..719f9a8 100644
--- a/protos/perfetto/trace/power/battery_counters.proto
+++ b/protos/perfetto/trace/power/battery_counters.proto
@@ -25,8 +25,9 @@
   optional float capacity_percent = 2;
 
   // Instantaneous battery current in microamperes(µA).
-  // Positive values indicate net current entering the battery from a charge
-  // source, negative values indicate net current discharging from the battery.
+  // Positive values indicate current drained from the battery,
+  // negative values current feeding the battery from a charge source (USB).
+  // See https://perfetto.dev/docs/data-sources/battery-counters for more.
   optional int64 current_ua = 3;
 
   // Instantaneous battery current in microamperes(µA).
diff --git a/protos/perfetto/trace/sys_stats/sys_stats.proto b/protos/perfetto/trace/sys_stats/sys_stats.proto
index 0161a6e..30edac0 100644
--- a/protos/perfetto/trace/sys_stats/sys_stats.proto
+++ b/protos/perfetto/trace/sys_stats/sys_stats.proto
@@ -94,4 +94,13 @@
   // the top-level packet timestamp is the time at which
   // we begin collection.
   optional uint64 collection_end_timestamp = 9;
+
+  // Frequencies for /sys/class/devfreq/ entries in kHz.
+  message DevfreqValue {
+    optional string key = 1;
+    optional uint64 value = 2;
+  };
+
+  // One entry per device.
+  repeated DevfreqValue devfreq = 10;
 }
diff --git a/protos/perfetto/trace/system_info.proto b/protos/perfetto/trace/system_info.proto
index 90a7a57..ddce4ca 100644
--- a/protos/perfetto/trace/system_info.proto
+++ b/protos/perfetto/trace/system_info.proto
@@ -31,4 +31,10 @@
 
   // Ticks per second - sysconf(_SC_CLK_TCK).
   optional int64 hz = 3;
+
+  // The version of traced (the same returned by `traced --version`).
+  // This is a human readable string with and its format varies depending on
+  // the build system and the repo (standalone vs AOSP).
+  // This is intended for human debugging only.
+  optional string tracing_service_version = 4;
 }
diff --git a/protos/perfetto/trace/test_event.proto b/protos/perfetto/trace/test_event.proto
index c55b1a5..023343e 100644
--- a/protos/perfetto/trace/test_event.proto
+++ b/protos/perfetto/trace/test_event.proto
@@ -16,6 +16,8 @@
 
 syntax = "proto2";
 
+import "protos/perfetto/trace/track_event/debug_annotation.proto";
+
 package perfetto.protos;
 
 // Event used by testing code.
@@ -36,8 +38,15 @@
     repeated string str = 1;
     repeated TestPayload nested = 2;
 
+    optional string single_string = 4;
+
+    optional int32 single_int = 5;
+    repeated int32 repeated_ints = 6;
+
     // When 0 this is the bottom-most nested message.
     optional uint32 remaining_nesting_depth = 3;
+
+    repeated DebugAnnotation debug_annotations = 7;
   }
   optional TestPayload payload = 5;
 }
diff --git a/protos/perfetto/trace/test_extensions.proto b/protos/perfetto/trace/test_extensions.proto
index 648015d..dca2db6 100644
--- a/protos/perfetto/trace/test_extensions.proto
+++ b/protos/perfetto/trace/test_extensions.proto
@@ -16,7 +16,8 @@
 
 syntax = "proto2";
 
-import "protos/perfetto/trace/track_event/track_event.proto";
+import public "protos/perfetto/trace/track_event/track_event.proto";
+import "protos/perfetto/trace/track_event/debug_annotation.proto";
 
 package perfetto.protos;
 
@@ -35,4 +36,6 @@
 
 message TestExtensionChild {
   optional string child_field_for_testing = 1;
+
+  repeated DebugAnnotation debug_annotations = 99;
 }
diff --git a/protos/perfetto/trace/track_event/chrome_frame_reporter.proto b/protos/perfetto/trace/track_event/chrome_frame_reporter.proto
index 10a1293..00930c7 100644
--- a/protos/perfetto/trace/track_event/chrome_frame_reporter.proto
+++ b/protos/perfetto/trace/track_event/chrome_frame_reporter.proto
@@ -91,4 +91,7 @@
   // Whether the frame contained any missing content (i.e. whether there was
   // checkerboarding in the frame).
   optional bool has_missing_content = 10;
+
+  // The id of layer_tree_host that the frame has been produced for.
+  optional uint64 layer_tree_host_id = 11;
 }
diff --git a/protos/perfetto/trace/track_event/debug_annotation.proto b/protos/perfetto/trace/track_event/debug_annotation.proto
index 795b4a2..d6766cb 100644
--- a/protos/perfetto/trace/track_event/debug_annotation.proto
+++ b/protos/perfetto/trace/track_event/debug_annotation.proto
@@ -18,30 +18,48 @@
 
 package perfetto.protos;
 
-// Key/value annotations provided in untyped TRACE_EVENT macros. These
-// annotations are intended for debug use and are not considered a stable API
-// surface. As such, they should not be relied upon to implement (new) metrics.
+// Proto representation of untyped key/value annotations provided in TRACE_EVENT
+// macros. Users of the Perfetto SDK should prefer to use the
+// perfetto::TracedValue API to fill these protos, rather than filling them
+// manually.
 //
-// Next ID: 10.
+// Debug annotations are intended for debug use and are not considered a stable
+// API of the trace contents. Trace-based metrics that use debug annotation
+// values are prone to breakage, so please rely on typed TrackEvent fields for
+// these instead.
+//
+// DebugAnnotations support nested arrays and dictionaries. Each entry is
+// encoded as a single DebugAnnotation message. Only dictionary entries
+// set the "name" field. The TrackEvent message forms an implicit root
+// dictionary.
+//
+// Example TrackEvent with nested annotations:
+//   track_event {
+//     debug_annotations {
+//       name: "foo"
+//       dict_entries {
+//         name: "a"
+//         bool_value: true
+//       }
+//       dict_entries {
+//         name: "b"
+//         int_value: 123
+//       }
+//     }
+//     debug_annotations {
+//       name: "bar"
+//       array_values {
+//         string_value: "hello"
+//       }
+//       array_values {
+//         string_value: "world"
+//       }
+//     }
+//   }
+//
+// Next ID: 13.
 message DebugAnnotation {
-  message NestedValue {
-    enum NestedType {
-      // leaf value.
-      UNSPECIFIED = 0;
-      DICT = 1;
-      ARRAY = 2;
-    }
-    optional NestedType nested_type = 1;
-
-    repeated string dict_keys = 2;
-    repeated NestedValue dict_values = 3;
-    repeated NestedValue array_values = 4;
-    optional int64 int_value = 5;
-    optional double double_value = 6;
-    optional bool bool_value = 7;
-    optional string string_value = 8;
-  }
-
+  // Name fields are set only for dictionary entries.
   oneof name_field {
     // interned DebugAnnotationName.
     uint64 name_iid = 1;
@@ -58,12 +76,38 @@
     // Pointers are stored in a separate type as the JSON output treats them
     // differently from other uint64 values.
     uint64 pointer_value = 7;
+
+    // Deprecated. Use dict_entries / array_values instead.
     NestedValue nested_value = 8;
 
     // Legacy instrumentation may not support conversion of nested data to
     // NestedValue yet.
     string legacy_json_value = 9;
   }
+
+  repeated DebugAnnotation dict_entries = 11;
+  repeated DebugAnnotation array_values = 12;
+
+  // Deprecated legacy way to use nested values. Only kept for
+  // backwards-compatibility in TraceProcessor. May be removed in the future -
+  // code filling protos should use |dict_entries| and |array_values| instead.
+  message NestedValue {
+    enum NestedType {
+      // leaf value.
+      UNSPECIFIED = 0;
+      DICT = 1;
+      ARRAY = 2;
+    }
+    optional NestedType nested_type = 1;
+
+    repeated string dict_keys = 2;
+    repeated NestedValue dict_values = 3;
+    repeated NestedValue array_values = 4;
+    optional int64 int_value = 5;
+    optional double double_value = 6;
+    optional bool bool_value = 7;
+    optional string string_value = 8;
+  }
 }
 
 // --------------------
diff --git a/protos/perfetto/trace/ui_state.proto b/protos/perfetto/trace/ui_state.proto
index fc3e278..149730d 100644
--- a/protos/perfetto/trace/ui_state.proto
+++ b/protos/perfetto/trace/ui_state.proto
@@ -32,13 +32,23 @@
 
   // Indicates that the given process should be highlighted by the UI.
   message HighlightProcess {
-    // The pid of the process to highlight. This is useful for UIs to focus
-    // on tracks of a particular process in the trace.
-    //
-    // If more than one process in a trace has the same pid, it is UI
-    // implementation specific how the process to be focused will be
-    // chosen.
-    optional uint32 pid = 1;
+    oneof selector {
+      // The pid of the process to highlight. This is useful for UIs to focus
+      // on tracks of a particular process in the trace.
+      //
+      // If more than one process in a trace has the same pid, it is UI
+      // implementation specific how the process to be focused will be
+      // chosen.
+      uint32 pid = 1;
+
+      // The command line of the process to highlight; for most Android apps,
+      // this is the package name of the app. This is useful for UIs to focus
+      // on a particular app in the trace.
+      //
+      // If more than one process hasthe same cmdline, it is UI implementation
+      // specific how the process to be focused will be chosen.
+      string cmdline = 2;
+    }
   }
   optional HighlightProcess highlight_process = 3;
 }
\ No newline at end of file
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index f32745b..fbff9d7 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -35,6 +35,111 @@
 //    In this case these messages are used to {,un}marshall HTTP requests and
 //    response made through src/trace_processor/rpc/httpd.cc .
 
+enum TraceProcessorApiVersion {
+  // This variable has been introduced in v15 and is used to deal with API
+  // mismatches between UI and trace_processor_shell --httpd. Increment this
+  // 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 = 1;
+}
+
+// At lowest level, the wire-format of the RPC procol is a linear sequence of
+// TraceProcessorRpc messages on each side of the byte pipe
+// Each message is prefixed by a tag (field = 1, type = length delimited) and a
+// varint encoding its size (this is so the whole stream can also be read /
+// written as if it was a repeated field of TraceProcessorRpcStream).
+
+message TraceProcessorRpcStream {
+  repeated TraceProcessorRpc msg = 1;
+}
+
+message TraceProcessorRpc {
+  // A monotonic counter used only for debugging purposes, to detect if the
+  // underlying stream is missing or duping data. The counter starts at 0 on
+  // each side of the pipe and is incremented on each message.
+  // Do NOT expect that a response has the same |seq| of its corresponding
+  // request: some requests (e.g., a query returning many rows) can yield more
+  // than one response message, bringing the tx and rq seq our of sync.
+  optional int64 seq = 1;
+
+  // This is returned when some unrecoverable error has been detected by the
+  // peer. The typical case is TraceProcessor detecting that the |seq| sequence
+  // is broken (e.g. when having two tabs open with the same --httpd instance).
+  optional string fatal_error = 5;
+
+  enum TraceProcessorMethod {
+    TPM_UNSPECIFIED = 0;
+    TPM_APPEND_TRACE_DATA = 1;
+    TPM_FINALIZE_TRACE_DATA = 2;
+    TPM_QUERY_STREAMING = 3;
+    TPM_QUERY_RAW_DEPRECATED = 4;
+    TPM_COMPUTE_METRIC = 5;
+    TPM_GET_METRIC_DESCRIPTORS = 6;
+    TPM_RESTORE_INITIAL_TABLES = 7;
+    TPM_ENABLE_METATRACE = 8;
+    TPM_DISABLE_AND_READ_METATRACE = 9;
+    TPM_GET_STATUS = 10;
+  }
+
+  oneof type {
+    // Client -> TraceProcessor requests.
+    TraceProcessorMethod request = 2;
+
+    // TraceProcessor -> Client responses.
+    TraceProcessorMethod response = 3;
+
+    // This is sent back instead of filling |response| when the client sends a
+    // |request| which is not known by the TraceProcessor service. This can
+    // happen when the client is newer than the service.
+    TraceProcessorMethod invalid_request = 4;
+  }
+
+  // Request/Response arguments.
+  // Not all requests / responses require an argument.
+
+  oneof args {
+    // TraceProcessorMethod request args.
+
+    // For TPM_APPEND_TRACE_DATA.
+    bytes append_trace_data = 101;
+    // For TPM_QUERY_STREAMING.
+    QueryArgs query_args = 103;
+    // For TPM_QUERY_RAW_DEPRECATED.
+    RawQueryArgs raw_query_args = 104;
+    // For TPM_COMPUTE_METRIC.
+    ComputeMetricArgs compute_metric_args = 105;
+
+    // TraceProcessorMethod response args.
+    // For TPM_APPEND_TRACE_DATA.
+    AppendTraceDataResult append_result = 201;
+    // For TPM_QUERY_STREAMING.
+    QueryResult query_result = 203;
+    // For TPM_QUERY_RAW_DEPRECATED.
+    RawQueryResult raw_query_result = 204;
+    // For TPM_COMPUTE_METRIC.
+    ComputeMetricResult metric_result = 205;
+    // For TPM_GET_METRIC_DESCRIPTORS.
+    DescriptorSet metric_descriptors = 206;
+    // For TPM_DISABLE_AND_READ_METATRACE.
+    DisableAndReadMetatraceResult metatrace = 209;
+    // For TPM_GET_STATUS.
+    StatusResult status = 210;
+  }
+}
+
+message AppendTraceDataResult {
+  optional int64 total_bytes_parsed = 1;
+  optional string error = 2;
+}
+
+message QueryArgs {
+  optional string sql_query = 1;
+
+  // Wall time when the query was queued. Used only for query stats.
+  optional uint64 time_queued_ns = 2;
+}
+
 // Input for the /raw_query endpoint.
 message RawQueryArgs {
   optional string sql_query = 1;
@@ -117,7 +222,6 @@
     // NUL-terminated. This is because JS incurs into a non-negligible overhead
     // when decoding strings and one decode + split('\0') is measurably faster
     // than decoding N strings. See goto.google.com/postmessage-benchmark .
-    // \0-concatenated.
     optional string string_cells = 5;
 
     // If true this is the last batch for the query result.
@@ -138,6 +242,15 @@
   // when using the HTTP+RPC mode nad passing a trace file to the shell, via
   // trace_processor_shell -D trace_file.pftrace .
   optional string loaded_trace_name = 1;
+
+  // Typically something like "v11.0.123", but could be just "v11" or "unknown",
+  // for binaries built from Bazel or other build configurations. This is for
+  // human presentation only, don't attempt to parse and reason on it.
+  optional string human_readable_version = 2;
+
+  // The API version is incremented every time a change that the UI depends
+  // on is introduced (e.g. adding a new table that the UI queries).
+  optional int32 api_version = 3;
 }
 
 // Input for the /compute_metric endpoint.
@@ -189,11 +302,3 @@
 message DescriptorSet {
   repeated DescriptorProto descriptors = 1;
 }
-
-// Input for the /get_metric_descriptors endpoint.
-message GetMetricDescriptorsArgs {}
-
-// Output for the /get_metric_descriptors endpoint.
-message GetMetricDescriptorsResult {
-  optional DescriptorSet descriptor_set = 1;
-}
diff --git a/protos/third_party/chromium/chrome_track_event.proto b/protos/third_party/chromium/chrome_track_event.proto
index 5a8939e..54e11ce 100644
--- a/protos/third_party/chromium/chrome_track_event.proto
+++ b/protos/third_party/chromium/chrome_track_event.proto
@@ -28,6 +28,9 @@
 
 message ChromeTaskAnnotator {
   optional uint32 ipc_hash = 1;
+  // The delay in microseconds that was specified, if any, when this task was
+  // posted. This is only valid for delayed tasks.
+  optional uint64 task_delay_us = 2;
 }
 
 message ChromeBrowserContext {
@@ -50,7 +53,7 @@
   optional uint64 source_location_iid = 4;
 }
 
-message ChromeTaskGraphRunner {
+message ChromeRasterTask {
   optional int64 source_frame_number = 1;
 }
 
@@ -125,9 +128,23 @@
   optional bool has_speculative_render_frame_host = 3;
 }
 
+message ChromeHashedPerformanceMark {
+  optional uint32 site_hash = 1;
+  optional string site = 2;
+  optional uint32 mark_hash = 3;
+  optional string mark = 4;
+}
+
+// These IDs are generated at compile time and differ for each chrome version.
+// IDs are stable on for a given chrome version but are changing when resources
+// are added or removed to chrome.
+message ResourceBundle {
+  optional uint32 resource_id = 1;
+}
+
 message ChromeTrackEvent {
   // Extension range for Chrome: 1000-1999
-  // Next ID: 1011
+  // Next ID: 1017
   extend TrackEvent {
     optional ChromeAppState chrome_app_state = 1000;
 
@@ -143,7 +160,7 @@
     optional ChromeTaskPostedToDisabledQueue
         chrome_task_posted_to_disabled_queue = 1005;
 
-    optional ChromeTaskGraphRunner chrome_task_graph_runner = 1006;
+    optional ChromeRasterTask chrome_raster_task = 1006;
 
     optional ChromeMessagePumpForUI chrome_message_pump_for_ui = 1007;
 
@@ -153,5 +170,11 @@
         should_swap_browsing_instances_result = 1009;
 
     optional FrameTreeNodeInfo frame_tree_node_info = 1010;
+
+    optional ChromeHashedPerformanceMark chrome_hashed_performance_mark = 1011;
+
+    // reserved 1012 to 1015.
+
+    optional ResourceBundle resource_bundle = 1016;
   }
 }
diff --git a/src/android_stats/perfetto_atoms.h b/src/android_stats/perfetto_atoms.h
index de2ffa0..0a857b9 100644
--- a/src/android_stats/perfetto_atoms.h
+++ b/src/android_stats/perfetto_atoms.h
@@ -68,6 +68,8 @@
   kTracedEnableTracingOom = 34,
   kTracedEnableTracingUnknown = 35,
   kTracedStartTracingInvalidSessionState = 36,
+  kTracedEnableTracingInvalidFilter = 47,
+  kTracedEnableTracingOobTargetBuffer = 48,
 
   // Checkpoints inside perfetto_cmd after tracing has finished.
   kOnTracingDisabled = 4,
@@ -112,6 +114,9 @@
 
   kTracedLimitProbability = 5,
   kTracedLimitMaxPer24h = 6,
+
+  kProbesProducerTrigger = 7,
+  kProbesProducerTriggerFail = 8,
 };
 
 }  // namespace perfetto
diff --git a/src/base/BUILD.gn b/src/base/BUILD.gn
index 7100667..f761a53 100644
--- a/src/base/BUILD.gn
+++ b/src/base/BUILD.gn
@@ -38,6 +38,7 @@
     "logging.cc",
     "metatrace.cc",
     "paged_memory.cc",
+    "periodic_task.cc",
     "pipe.cc",
     "status.cc",
     "string_splitter.cc",
@@ -156,9 +157,11 @@
     "circular_queue_unittest.cc",
     "flat_set_unittest.cc",
     "getopt_compat_unittest.cc",
+    "logging_unittest.cc",
     "no_destructor_unittest.cc",
     "optional_unittest.cc",
     "paged_memory_unittest.cc",
+    "periodic_task_unittest.cc",
     "scoped_file_unittest.cc",
     "string_splitter_unittest.cc",
     "string_utils_unittest.cc",
diff --git a/src/base/ctrl_c_handler.cc b/src/base/ctrl_c_handler.cc
index 72b60a8..080f0cf 100644
--- a/src/base/ctrl_c_handler.cc
+++ b/src/base/ctrl_c_handler.cc
@@ -22,7 +22,6 @@
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
 #include <Windows.h>
-#include <consoleapi.h>
 #include <io.h>
 #else
 #include <signal.h>
diff --git a/src/base/debug_crash_stack_trace.cc b/src/base/debug_crash_stack_trace.cc
index 4da0fba..2a55d2b 100644
--- a/src/base/debug_crash_stack_trace.cc
+++ b/src/base/debug_crash_stack_trace.cc
@@ -107,6 +107,7 @@
 }
 
 void RestoreSignalHandlers() {
+  g_sighandler_registered = false;
   for (size_t i = 0; i < sizeof(g_signals) / sizeof(g_signals[0]); i++)
     sigaction(g_signals[i].sig_num, &g_signals[i].old_handler, nullptr);
 }
@@ -233,9 +234,13 @@
   }
 }
 
+}  // namespace
+
+namespace perfetto {
 // __attribute__((constructor)) causes a static initializer that automagically
 // early runs this function before the main().
-void __attribute__((constructor)) EnableStacktraceOnCrashForDebug();
+void PERFETTO_EXPORT __attribute__((constructor))
+EnableStacktraceOnCrashForDebug();
 
 void EnableStacktraceOnCrashForDebug() {
   if (g_sighandler_registered)
@@ -259,7 +264,6 @@
   // (ii) the output of death test is not visible.
   pthread_atfork(nullptr, nullptr, &RestoreSignalHandlers);
 }
-
-}  // namespace
+}  // namespace perfetto
 
 #pragma GCC diagnostic pop
diff --git a/src/base/getopt_compat.cc b/src/base/getopt_compat.cc
index 19c8adc..0c80dc6 100644
--- a/src/base/getopt_compat.cc
+++ b/src/base/getopt_compat.cc
@@ -30,6 +30,8 @@
 
 char* optarg = nullptr;
 int optind = 0;
+int optopt = 0;
+int opterr = 1;
 
 namespace {
 
@@ -47,7 +49,7 @@
 
 const option* LookupShortOpt(const std::vector<option>& opts, char c) {
   for (const option& opt : opts) {
-    if (opt.name == nullptr && opt.val == c)
+    if (!*opt.name && opt.val == c)
       return &opt;
   }
   return nullptr;
@@ -78,6 +80,7 @@
     }
     res->emplace_back();
     option& opt = res->back();
+    opt.name = "";
     opt.val = c;
     opt.has_arg = no_argument;
     if (*sopt == ':') {
@@ -108,6 +111,7 @@
     return '?';
 
   char* arg = argv[optind];
+  optopt = 0;
 
   if (!nextchar) {
     // If |nextchar| is null we are NOT in the middle of a short option and we
@@ -120,11 +124,14 @@
 
       size_t len = sep ? static_cast<size_t>(sep - arg) : strlen(arg);
       const option* opt = LookupLongOpt(opts, arg, len);
+
       if (!opt) {
-        fprintf(stderr, "unrecognized option '--%s'\n", arg);
+        if (opterr)
+          fprintf(stderr, "unrecognized option '--%s'\n", arg);
         return '?';
       }
 
+      optopt = opt->val;
       if (opt->has_arg == no_argument) {
         if (sep) {
           fprintf(stderr, "option '--%s' doesn't allow an argument\n", arg);
@@ -137,7 +144,8 @@
           optarg = sep + 1;
           return opt->val;
         } else if (optind >= argc) {
-          fprintf(stderr, "option '--%s' requires an argument\n", arg);
+          if (opterr)
+            fprintf(stderr, "option '--%s' requires an argument\n", arg);
           return '?';
         } else {
           optarg = argv[optind++];
@@ -173,8 +181,10 @@
     }
 
     const option* opt = LookupShortOpt(opts, cur_char);
+    optopt = cur_char;
     if (!opt) {
-      fprintf(stderr, "invalid option -- '%c'\n", cur_char);
+      if (opterr)
+        fprintf(stderr, "invalid option -- '%c'\n", cur_char);
       return '?';
     }
     if (opt->has_arg == no_argument) {
@@ -189,7 +199,8 @@
       if (!nextchar) {
         // Case 1.
         if (optind >= argc) {
-          fprintf(stderr, "option requires an argument -- '%c'\n", cur_char);
+          if (opterr)
+            fprintf(stderr, "option requires an argument -- '%c'\n", cur_char);
           return '?';
         } else {
           optarg = argv[optind++];
diff --git a/src/base/getopt_compat_unittest.cc b/src/base/getopt_compat_unittest.cc
index a7d4db3..aeeb2ed 100644
--- a/src/base/getopt_compat_unittest.cc
+++ b/src/base/getopt_compat_unittest.cc
@@ -47,6 +47,8 @@
   GetoptFn getopt = &getopt_compat::getopt;
   GetoptLongFn getopt_long = &getopt_compat::getopt_long;
   int& optind = getopt_compat::optind;
+  int& optopt = getopt_compat::optopt;
+  int& opterr = getopt_compat::opterr;
   char*& optarg = getopt_compat::optarg;
 };
 
@@ -58,6 +60,8 @@
   GetoptFn getopt = &::getopt;
   GetoptLongFn getopt_long = &::getopt_long;
   int& optind = ::optind;
+  int& optopt = ::optopt;
+  int& opterr = ::opterr;
   char*& optarg = ::optarg;
 };
 #endif
@@ -382,6 +386,33 @@
   }
 }
 
+TYPED_TEST(GetoptCompatTest, OpterrHandling) {
+  auto& t = this->impl;
+  t.opterr = 0;  // Make errors silent.
+
+  const char* sops = "ab:";
+  this->SetCmdline({"argv0", "-a", "-c", "-b"});
+  EXPECT_EQ(t.getopt(this->argc, this->argv, sops), 'a');
+  EXPECT_EQ(t.optarg, nullptr);
+  EXPECT_EQ(t.getopt(this->argc, this->argv, sops), '?');
+  EXPECT_EQ(t.optopt, 'c');
+  EXPECT_EQ(t.getopt(this->argc, this->argv, sops), '?');
+  EXPECT_EQ(t.optopt, 'b');
+  EXPECT_EQ(t.getopt(this->argc, this->argv, sops), -1);
+
+  using LongOptionType = typename decltype(this->impl)::LongOptionType;
+  LongOptionType lopts[]{
+      {"requires_arg", 1 /*required_argument*/, nullptr, 42},
+      {nullptr, 0, nullptr, 0},
+  };
+  this->SetCmdline({"argv0", "-a", "--unkonwn", "--requires_arg"});
+  EXPECT_EQ(t.getopt_long(this->argc, this->argv, sops, lopts, nullptr), 'a');
+  EXPECT_EQ(t.getopt_long(this->argc, this->argv, sops, lopts, nullptr), '?');
+  EXPECT_EQ(t.getopt_long(this->argc, this->argv, sops, lopts, nullptr), '?');
+  EXPECT_EQ(t.optopt, 42);
+  EXPECT_EQ(t.getopt_long(this->argc, this->argv, sops, lopts, nullptr), -1);
+}
+
 }  // namespace
 }  // namespace base
 }  // namespace perfetto
diff --git a/src/base/logging.cc b/src/base/logging.cc
index b733988..5b251d6 100644
--- a/src/base/logging.cc
+++ b/src/base/logging.cc
@@ -23,6 +23,7 @@
 #include <unistd.h>  // For isatty()
 #endif
 
+#include <atomic>
 #include <memory>
 
 #include "perfetto/base/time.h"
@@ -38,8 +39,14 @@
 const char kBoldGreen[] = "\x1b[1m\x1b[32m";
 const char kLightGray[] = "\x1b[90m";
 
+std::atomic<LogMessageCallback> g_log_callback{};
+
 }  // namespace
 
+void SetLogMessageCallback(LogMessageCallback callback) {
+  g_log_callback.store(callback, std::memory_order_relaxed);
+}
+
 void LogMessage(LogLev level,
                 const char* fname,
                 int line,
@@ -75,6 +82,12 @@
     log_msg = &large_buf[0];
   }
 
+  LogMessageCallback cb = g_log_callback.load(std::memory_order_relaxed);
+  if (cb) {
+    cb({level, line, fname, log_msg});
+    return;
+  }
+
   const char* color = kDefault;
   switch (level) {
     case kLogDebug:
diff --git a/src/base/logging_unittest.cc b/src/base/logging_unittest.cc
new file mode 100644
index 0000000..5349290
--- /dev/null
+++ b/src/base/logging_unittest.cc
@@ -0,0 +1,50 @@
+/*
+ * Copyright 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/base/logging.h"
+
+#include <stdint.h>
+
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+char g_last_line[256];
+
+TEST(LoggingTest, Basic) {
+  SetLogMessageCallback(nullptr);
+  LogMessage(kLogDebug, "file.cc", 100, "test message %d", 1);
+
+  SetLogMessageCallback(+[](LogMessageCallbackArgs log) {
+    sprintf(g_last_line, "%d:%s:%d:%s", log.level, log.filename, log.line,
+            log.message);
+  });
+
+  g_last_line[0] = 0;
+  LogMessage(kLogDebug, "file.cc", 101, "test message %d", 2);
+  ASSERT_STREQ(g_last_line, "0:file.cc:101:test message 2");
+
+  g_last_line[0] = 0;
+  SetLogMessageCallback(nullptr);
+  LogMessage(kLogDebug, "file.cc", 102, "test message %d", 3);
+  ASSERT_STREQ(g_last_line, "");
+}
+
+}  // namespace
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/periodic_task.cc b/src/base/periodic_task.cc
new file mode 100644
index 0000000..fcbc9de
--- /dev/null
+++ b/src/base/periodic_task.cc
@@ -0,0 +1,166 @@
+/*
+ * 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/periodic_task.h"
+
+#include <limits>
+
+#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/file_utils.h"
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
+    (PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) && __ANDROID_API__ >= 19)
+#include <sys/timerfd.h>
+#endif
+
+namespace perfetto {
+namespace base {
+
+namespace {
+base::ScopedPlatformHandle CreateTimerFd(uint32_t period_ms) {
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
+    (PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) && __ANDROID_API__ >= 19)
+  base::ScopedPlatformHandle tfd(
+      timerfd_create(CLOCK_BOOTTIME, TFD_CLOEXEC | TFD_NONBLOCK));
+  // The initial phase, aligned on wall clock.
+  uint32_t phase_ms =
+      period_ms -
+      static_cast<uint32_t>(base::GetBootTimeNs().count() % period_ms);
+  struct itimerspec its {};
+  // The "1 +" is to make sure that we never pass a zero it_value in the
+  // unlikely case of phase_ms being 0. That would cause the timer to be
+  // considered disarmed by timerfd_settime.
+  its.it_value.tv_sec = static_cast<time_t>(phase_ms / 1000u);
+  its.it_value.tv_nsec = 1 + static_cast<long>((phase_ms % 1000u) * 1000000u);
+  its.it_interval.tv_sec = static_cast<time_t>(period_ms / 1000u);
+  its.it_interval.tv_nsec = static_cast<long>((period_ms % 1000u) * 1000000u);
+  if (timerfd_settime(*tfd, 0, &its, nullptr) < 0)
+    return base::ScopedPlatformHandle();
+  return tfd;
+#else
+  base::ignore_result(period_ms);
+  return base::ScopedPlatformHandle();
+#endif
+}
+}  // namespace
+
+PeriodicTask::PeriodicTask(base::TaskRunner* task_runner)
+    : task_runner_(task_runner), weak_ptr_factory_(this) {}
+
+PeriodicTask::~PeriodicTask() {
+  Reset();
+}
+
+void PeriodicTask::Start(Args args) {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+  Reset();
+  if (args.period_ms == 0 || !args.task) {
+    PERFETTO_DCHECK(args.period_ms > 0);
+    PERFETTO_DCHECK(args.task);
+    return;
+  }
+  args_ = std::move(args);
+  if (args_.use_suspend_aware_timer) {
+    timer_fd_ = CreateTimerFd(args_.period_ms);
+    if (timer_fd_) {
+      auto weak_this = weak_ptr_factory_.GetWeakPtr();
+      task_runner_->AddFileDescriptorWatch(
+          *timer_fd_,
+          std::bind(PeriodicTask::RunTaskAndPostNext, weak_this, generation_));
+    } else {
+      PERFETTO_DPLOG("timerfd not supported, falling back on PostDelayedTask");
+    }
+  }  // if (use_suspend_aware_timer).
+
+  if (!timer_fd_)
+    PostNextTask();
+
+  if (args_.start_first_task_immediately)
+    args_.task();
+}
+
+void PeriodicTask::PostNextTask() {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+  PERFETTO_DCHECK(args_.period_ms > 0);
+  PERFETTO_DCHECK(!timer_fd_);
+  uint32_t delay_ms =
+      args_.period_ms -
+      static_cast<uint32_t>(base::GetWallTimeMs().count() % args_.period_ms);
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
+  task_runner_->PostDelayedTask(
+      std::bind(PeriodicTask::RunTaskAndPostNext, weak_this, generation_),
+      delay_ms);
+}
+
+// static
+// This function can be called in two ways (both from the TaskRunner):
+// 1. When using a timerfd, this task is registered as a FD watch.
+// 2. When using PostDelayedTask, this is the task posted on the TaskRunner.
+void PeriodicTask::RunTaskAndPostNext(base::WeakPtr<PeriodicTask> thiz,
+                                      uint32_t generation) {
+  if (!thiz || !thiz->args_.task || generation != thiz->generation_)
+    return;  // Destroyed or Reset() in the meanwhile.
+  PERFETTO_DCHECK_THREAD(thiz->thread_checker_);
+  if (thiz->timer_fd_) {
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+    PERFETTO_FATAL("timerfd for periodic tasks unsupported on Windows");
+#else
+    // If we are using a timerfd there is no need to repeatedly call
+    // PostDelayedTask(). The kernel will wakeup the timer fd periodically. We
+    // just need to read() it.
+    uint64_t ignored = 0;
+    errno = 0;
+    auto rsize = base::Read(*thiz->timer_fd_, &ignored, sizeof(&ignored));
+    if (rsize != sizeof(uint64_t)) {
+      if (errno == EAGAIN)
+        return;  // A spurious wakeup. Rare, but can happen, just ignore.
+      PERFETTO_PLOG("read(timerfd) failed, falling back on PostDelayedTask");
+      thiz->ResetTimerFd();
+    }
+#endif
+  }
+  // The repetition of the if() is to deal with the ResetTimerFd() case above.
+  if (!thiz->timer_fd_) {
+    thiz->PostNextTask();
+  }
+  // Create a copy of the task in the unlikely event that the task ends up
+  // up destroying the PeriodicTask object or calling Reset() on it. That would
+  // cause a reset of the args_.task itself, which would invalidate the task
+  // bind state while we are invoking it.
+  auto task = thiz->args_.task;
+  task();
+}
+
+void PeriodicTask::Reset() {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+  ++generation_;
+  args_ = Args();
+  PERFETTO_DCHECK(!args_.task);
+  ResetTimerFd();
+}
+
+void PeriodicTask::ResetTimerFd() {
+  if (!timer_fd_)
+    return;
+  task_runner_->RemoveFileDescriptorWatch(*timer_fd_);
+  timer_fd_.reset();
+}
+
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/periodic_task_unittest.cc b/src/base/periodic_task_unittest.cc
new file mode 100644
index 0000000..4919720
--- /dev/null
+++ b/src/base/periodic_task_unittest.cc
@@ -0,0 +1,199 @@
+/*
+ * 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/periodic_task.h"
+
+#include "perfetto/ext/base/file_utils.h"
+#include "src/base/test/test_task_runner.h"
+#include "test/gtest_and_gmock.h"
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+#include <unistd.h>
+#endif
+
+namespace perfetto {
+namespace base {
+
+namespace {
+
+TEST(PeriodicTaskTest, PostDelayedTaskMode) {
+  TestTaskRunner task_runner;
+  PeriodicTask pt(&task_runner);
+  uint32_t num_callbacks = 0;
+  auto quit_closure = task_runner.CreateCheckpoint("all_timers_done");
+
+  PeriodicTask::Args args;
+  args.task = [&] {
+    if (++num_callbacks == 3)
+      quit_closure();
+  };
+  args.period_ms = 1;
+  args.start_first_task_immediately = true;
+  pt.Start(std::move(args));
+  EXPECT_EQ(num_callbacks, 1u);
+  task_runner.RunUntilCheckpoint("all_timers_done");
+  EXPECT_EQ(num_callbacks, 3u);
+}
+
+// Call Reset() from a callback, ensure no further calls are made.
+TEST(PeriodicTaskTest, ResetFromCallback) {
+  TestTaskRunner task_runner;
+  PeriodicTask pt(&task_runner);
+  uint32_t num_callbacks = 0;
+  PeriodicTask::Args args;
+  auto quit_closure = task_runner.CreateCheckpoint("quit_closure");
+  args.task = [&] {
+    ++num_callbacks;
+    pt.Reset();
+    task_runner.PostDelayedTask(quit_closure, 5);
+  };
+  args.period_ms = 1;
+  pt.Start(std::move(args));
+  EXPECT_EQ(num_callbacks, 0u);  // No immediate execution.
+
+  task_runner.RunUntilCheckpoint("quit_closure");
+  EXPECT_EQ(num_callbacks, 1u);
+}
+
+// Invalidates the timerfd, by replacing it with /dev/null, in the middle of
+// the periodic ticks. That causes the next read() to fail and fall back on
+// PostDelayedTask().
+// On Mac and other systems where timerfd is not supported this will fall back
+// on PostDelayedTask() immediately (and work).
+TEST(PeriodicTaskTest, FallbackIfTimerfdFails) {
+  TestTaskRunner task_runner;
+  PeriodicTask pt(&task_runner);
+  uint32_t num_callbacks = 0;
+  auto quit_closure = task_runner.CreateCheckpoint("all_timers_done");
+
+  PeriodicTask::Args args;
+  args.task = [&] {
+    ++num_callbacks;
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+    if (num_callbacks == 3 && pt.timer_fd_for_testing() > 0) {
+      ScopedFile dev_null = OpenFile("/dev/null", O_RDONLY);
+      dup2(*dev_null, pt.timer_fd_for_testing());
+    }
+#else
+    EXPECT_FALSE(base::ScopedPlatformHandle::ValidityChecker::IsValid(
+        pt.timer_fd_for_testing()));
+#endif
+    if (num_callbacks == 6)
+      quit_closure();
+  };
+  args.period_ms = 1;
+  args.use_suspend_aware_timer = true;
+  pt.Start(std::move(args));
+  task_runner.RunUntilCheckpoint("all_timers_done");
+  EXPECT_EQ(num_callbacks, 6u);
+}
+
+TEST(PeriodicTaskTest, DestroyedFromCallback) {
+  TestTaskRunner task_runner;
+  std::unique_ptr<PeriodicTask> pt(new PeriodicTask(&task_runner));
+  uint32_t num_callbacks = 0;
+  PeriodicTask::Args args;
+  auto quit_closure = task_runner.CreateCheckpoint("quit_closure");
+  args.task = [&] {
+    ++num_callbacks;
+    pt.reset();
+    task_runner.PostDelayedTask(quit_closure, 5);
+  };
+  args.period_ms = 1;
+  args.use_suspend_aware_timer = true;
+  pt->Start(std::move(args));
+
+  task_runner.RunUntilCheckpoint("quit_closure");
+  EXPECT_EQ(num_callbacks, 1u);
+  EXPECT_FALSE(pt);
+}
+
+TEST(PeriodicTaskTest, DestroyedFromAnotherTask) {
+  TestTaskRunner task_runner;
+  std::unique_ptr<PeriodicTask> pt(new PeriodicTask(&task_runner));
+  uint32_t num_callbacks = 0;
+  PeriodicTask::Args args;
+  auto quit_closure = task_runner.CreateCheckpoint("quit_closure");
+  args.task = [&] {
+    if (++num_callbacks == 2) {
+      task_runner.PostTask([&] {
+        pt.reset();
+        task_runner.PostDelayedTask(quit_closure, 5);
+      });
+    }
+  };
+  args.period_ms = 1;
+  args.use_suspend_aware_timer = true;
+  pt->Start(std::move(args));
+
+  task_runner.RunUntilCheckpoint("quit_closure");
+  EXPECT_EQ(num_callbacks, 2u);
+  EXPECT_FALSE(pt);
+}
+
+// Checks the generation logic.
+TEST(PeriodicTaskTest, RestartWhileRunning) {
+  TestTaskRunner task_runner;
+  PeriodicTask pt(&task_runner);
+  uint32_t num_callbacks_a = 0;
+  uint32_t num_callbacks_b = 0;
+  auto quit_closure = task_runner.CreateCheckpoint("quit_closure");
+
+  auto reuse = [&] {
+    PeriodicTask::Args args;
+    args.period_ms = 1;
+    args.task = [&] {
+      if (++num_callbacks_b == 3)
+        quit_closure();
+    };
+    pt.Start(std::move(args));
+  };
+
+  PeriodicTask::Args args;
+  args.task = [&] {
+    if (++num_callbacks_a == 2)
+      task_runner.PostTask(reuse);
+  };
+  args.period_ms = 1;
+  args.use_suspend_aware_timer = true;
+  pt.Start(std::move(args));
+
+  task_runner.RunUntilCheckpoint("quit_closure");
+  EXPECT_EQ(num_callbacks_a, 2u);
+  EXPECT_EQ(num_callbacks_b, 3u);
+}
+
+TEST(PeriodicTaskTest, ImmediateExecution) {
+  TestTaskRunner task_runner;
+  PeriodicTask pt(&task_runner);
+  uint32_t num_callbacks = 0;
+
+  PeriodicTask::Args args;
+  args.task = [&] { ++num_callbacks; };
+  args.period_ms = 1;
+  pt.Start(args);
+  EXPECT_EQ(num_callbacks, 0u);  // No immediate execution.
+
+  args.start_first_task_immediately = true;
+  pt.Start(args);
+  EXPECT_EQ(num_callbacks, 1u);
+}
+
+}  // namespace
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/string_utils.cc b/src/base/string_utils.cc
index ac3b16b..c0b0780 100644
--- a/src/base/string_utils.cc
+++ b/src/base/string_utils.cc
@@ -98,6 +98,10 @@
   return haystack.find(needle) != std::string::npos;
 }
 
+bool Contains(const std::string& haystack, const char needle) {
+  return haystack.find(needle) != std::string::npos;
+}
+
 size_t Find(const StringView& needle, const StringView& haystack) {
   if (needle.empty())
     return 0;
diff --git a/src/base/string_view_unittest.cc b/src/base/string_view_unittest.cc
index bb00035..5bda159 100644
--- a/src/base/string_view_unittest.cc
+++ b/src/base/string_view_unittest.cc
@@ -34,6 +34,8 @@
   EXPECT_EQ(StringView("ax", 1), StringView("ay", 1));
   EXPECT_EQ(StringView("ax", 1), StringView("a"));
   EXPECT_EQ(StringView("ax", 1), "a");
+  EXPECT_EQ(StringView(reinterpret_cast<const char*>(0x100), 0).ToStdString(),
+            std::string(""));
   EXPECT_EQ(StringView("foo|", 3).ToStdString(), std::string("foo"));
   EXPECT_TRUE(StringView("x") != StringView(""));
   EXPECT_TRUE(StringView("") != StringView("y"));
diff --git a/src/base/subprocess_posix.cc b/src/base/subprocess_posix.cc
index c5321f6..5e09544 100644
--- a/src/base/subprocess_posix.cc
+++ b/src/base/subprocess_posix.cc
@@ -82,6 +82,12 @@
     _exit(128);
   };
 
+  if (args->create_args->posix_proc_group_id.has_value()) {
+    if (setpgid(0 /*self*/, args->create_args->posix_proc_group_id.value())) {
+      die("setpgid() failed");
+    }
+  }
+
   auto set_fd_close_on_exec = [&die](int fd, bool close_on_exec) {
     int flags = fcntl(fd, F_GETFD, 0);
     if (flags < 0)
@@ -150,12 +156,15 @@
     }
   }
 
-  // Clears O_CLOEXEC from stdin/out/err. These are the only FDs that we want
-  // to be preserved after the exec().
+  // Clears O_CLOEXEC from stdin/out/err and the |preserve_fds| list. These are
+  // the only FDs that we want to be preserved after the exec().
   set_fd_close_on_exec(STDIN_FILENO, false);
   set_fd_close_on_exec(STDOUT_FILENO, false);
   set_fd_close_on_exec(STDERR_FILENO, false);
 
+  for (auto fd : preserve_fds)
+    set_fd_close_on_exec(fd, false);
+
   // If the caller specified a std::function entrypoint, run that first.
   if (args->create_args->posix_entrypoint_for_testing)
     args->create_args->posix_entrypoint_for_testing();
@@ -381,7 +390,7 @@
     return;
 
   PERFETTO_DCHECK(args.input.empty() || s_->input_written < args.input.size());
-  if (args.input.size()) {
+  if (!args.input.empty()) {
     int64_t wsize =
         PERFETTO_EINTR(write(*s_->stdin_pipe.wr, &args.input[s_->input_written],
                              args.input.size() - s_->input_written));
diff --git a/src/base/test/utils.cc b/src/base/test/utils.cc
index a5ca116..ff00f64 100644
--- a/src/base/test/utils.cc
+++ b/src/base/test/utils.cc
@@ -18,58 +18,16 @@
 
 #include <stdlib.h>
 
+#include <memory>
+
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/file_utils.h"
-
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
-    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
-    PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE) ||   \
-    PERFETTO_BUILDFLAG(PERFETTO_OS_FUCHSIA)
-#include <limits.h>
-#include <unistd.h>
-#endif
-
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
-#include <Windows.h>
-#include <io.h>
-#endif
-
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
-#include <mach-o/dyld.h>
-#endif
+#include "perfetto/ext/base/utils.h"
 
 namespace perfetto {
 namespace base {
 
-std::string GetCurExecutableDir() {
-  std::string self_path;
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
-    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
-    PERFETTO_BUILDFLAG(PERFETTO_OS_FUCHSIA)
-  char buf[PATH_MAX];
-  ssize_t size = readlink("/proc/self/exe", buf, sizeof(buf));
-  PERFETTO_CHECK(size != -1);
-  // readlink does not null terminate.
-  self_path = std::string(buf, static_cast<size_t>(size));
-#elif PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
-  uint32_t size = 0;
-  PERFETTO_CHECK(_NSGetExecutablePath(nullptr, &size));
-  self_path.resize(size);
-  PERFETTO_CHECK(_NSGetExecutablePath(&self_path[0], &size) == 0);
-#elif PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
-  char buf[MAX_PATH];
-  auto len = ::GetModuleFileNameA(nullptr /*current*/, buf, sizeof(buf));
-  self_path = std::string(buf, len);
-  self_path = self_path.substr(0, self_path.find_last_of("\\"));
-#else
-  PERFETTO_FATAL(
-      "GetCurExecutableDir() not implemented on the current platform");
-#endif
-  // Cut binary name.
-  return self_path.substr(0, self_path.find_last_of("/"));
-}
-
 std::string GetTestDataPath(const std::string& path) {
   std::string self_path = GetCurExecutableDir();
   std::string full_path = self_path + "/../../" + path;
@@ -82,5 +40,28 @@
   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 365c93f..032194b 100644
--- a/src/base/test/utils.h
+++ b/src/base/test/utils.h
@@ -37,9 +37,9 @@
 // we just fall back on executing the code directly.
 #if defined(GTEST_EXECUTE_STATEMENT_)
 #define EXPECT_DCHECK_DEATH(statement) \
-    GTEST_EXECUTE_STATEMENT_(statement, "PERFETTO_CHECK")
+  GTEST_EXECUTE_STATEMENT_(statement, "PERFETTO_CHECK")
 #define ASSERT_DCHECK_DEATH(statement) \
-    GTEST_EXECUTE_STATEMENT_(statement, "PERFETTO_CHECK")
+  GTEST_EXECUTE_STATEMENT_(statement, "PERFETTO_CHECK")
 #else
 #define EXPECT_DCHECK_DEATH(statement) [&]() { statement }()
 #define ASSERT_DCHECK_DEATH(statement) [&]() { statement }()
@@ -50,9 +50,15 @@
 namespace perfetto {
 namespace base {
 
-std::string GetCurExecutableDir();
 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/utils.cc b/src/base/utils.cc
index 0e15bf6..041e010 100644
--- a/src/base/utils.cc
+++ b/src/base/utils.cc
@@ -16,19 +16,30 @@
 
 #include "perfetto/ext/base/utils.h"
 
+#include <string>
+
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/file_utils.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
     PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
-    PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
-#include <unistd.h>  // For getpagesize() and geteuid().
+    PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE) ||   \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_FUCHSIA)
+#include <limits.h>
+#include <unistd.h>  // For getpagesize() and geteuid() & fork()
 #endif
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
+#include <mach-o/dyld.h>
 #include <mach/vm_page_size.h>
 #endif
 
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+#include <Windows.h>
+#include <io.h>
+#endif
+
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
 #include <dlfcn.h>
 #include <malloc.h>
@@ -38,7 +49,7 @@
 #else
 // Only available in in-tree builds and on newer SDKs.
 #define PERFETTO_M_PURGE -101
-#endif
+#endif  // M_PURGE
 
 namespace {
 extern "C" {
@@ -108,5 +119,73 @@
 #endif
 }
 
+void Daemonize() {
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
+  pid_t pid;
+  switch (pid = fork()) {
+    case -1:
+      PERFETTO_FATAL("fork");
+    case 0: {
+      PERFETTO_CHECK(setsid() != -1);
+      base::ignore_result(chdir("/"));
+      base::ScopedFile null = base::OpenFile("/dev/null", O_RDONLY);
+      PERFETTO_CHECK(null);
+      PERFETTO_CHECK(dup2(*null, STDIN_FILENO) != -1);
+      PERFETTO_CHECK(dup2(*null, STDOUT_FILENO) != -1);
+      PERFETTO_CHECK(dup2(*null, STDERR_FILENO) != -1);
+      // Do not accidentally close stdin/stdout/stderr.
+      if (*null <= 2)
+        null.release();
+      break;
+    }
+    default:
+      printf("%d\n", pid);
+      exit(0);
+  }
+#else
+  // Avoid -Wunreachable warnings.
+  if (reinterpret_cast<intptr_t>(&Daemonize) != 16)
+    PERFETTO_FATAL("--background is only supported on Linux/Android/Mac");
+#endif  // OS_WIN
+}
+
+std::string GetCurExecutablePath() {
+  std::string self_path;
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_FUCHSIA)
+  char buf[PATH_MAX];
+  ssize_t size = readlink("/proc/self/exe", buf, sizeof(buf));
+  PERFETTO_CHECK(size != -1);
+  // readlink does not null terminate.
+  self_path = std::string(buf, static_cast<size_t>(size));
+#elif PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
+  uint32_t size = 0;
+  PERFETTO_CHECK(_NSGetExecutablePath(nullptr, &size));
+  self_path.resize(size);
+  PERFETTO_CHECK(_NSGetExecutablePath(&self_path[0], &size) == 0);
+#elif PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  char buf[MAX_PATH];
+  auto len = ::GetModuleFileNameA(nullptr /*current*/, buf, sizeof(buf));
+  self_path = std::string(buf, len);
+#else
+  PERFETTO_FATAL(
+      "GetCurExecutableDir() not implemented on the current platform");
+#endif
+  return self_path;
+}
+
+std::string GetCurExecutableDir() {
+  auto path = GetCurExecutablePath();
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  // Paths in Windows can have both kinds of slashes (mingw vs msvc).
+  path = path.substr(0, path.find_last_of("\\"));
+#endif
+  path = path.substr(0, path.find_last_of("/"));
+  return path;
+}
+
 }  // namespace base
 }  // namespace perfetto
diff --git a/src/ipc/client_impl.cc b/src/ipc/client_impl.cc
index 1b475be..540656f 100644
--- a/src/ipc/client_impl.cc
+++ b/src/ipc/client_impl.cc
@@ -184,8 +184,8 @@
 }
 
 void ClientImpl::OnDisconnect(base::UnixSocket*) {
-  for (auto it : service_bindings_) {
-    base::WeakPtr<ServiceProxy>& service_proxy = it.second;
+  for (const auto& it : service_bindings_) {
+    base::WeakPtr<ServiceProxy> service_proxy = it.second;
     task_runner_->PostTask([service_proxy] {
       if (service_proxy)
         service_proxy->OnDisconnect();
diff --git a/src/kallsyms/kernel_symbol_map.cc b/src/kallsyms/kernel_symbol_map.cc
index 920c897..126d6dd 100644
--- a/src/kallsyms/kernel_symbol_map.cc
+++ b/src/kallsyms/kernel_symbol_map.cc
@@ -202,7 +202,7 @@
     *(tok_wptr++) = token.at(i) & 0x7f;
   }
   *(tok_wptr++) = static_cast<char>(token.at(token_size - 1) | 0x80);
-  PERFETTO_DCHECK(tok_wptr == &buf_[buf_.size()]);
+  PERFETTO_DCHECK(tok_wptr == buf_.data() + buf_.size());
   return id;
 }
 
@@ -394,7 +394,7 @@
   uint32_t addr = it->first;
   uint32_t off = it->second;
   const uint8_t* rdptr = &buf_[off];
-  const uint8_t* const buf_end = &buf_[buf_.size()];
+  const uint8_t* const buf_end = buf_.data() + buf_.size();
   bool parsing_addr = true;
   const uint8_t* next_rdptr = nullptr;
   uint64_t sym_start_addr = 0;
diff --git a/src/perfetto_cmd/config.cc b/src/perfetto_cmd/config.cc
index 1dbba97..f2b40d4 100644
--- a/src/perfetto_cmd/config.cc
+++ b/src/perfetto_cmd/config.cc
@@ -19,6 +19,7 @@
 #include <stdlib.h>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "perfetto/tracing/core/data_source_config.h"
 #include "perfetto/tracing/core/trace_config.h"
 
@@ -116,10 +117,18 @@
   std::vector<std::string> atrace_apps = options.atrace_apps;
 
   for (const auto& category : options.categories) {
-    if (category.find('/') == std::string::npos) {
-      atrace_categories.push_back(category);
-    } else {
+    if (base::Contains(category, '/')) {
       ftrace_events.push_back(category);
+    } else {
+      atrace_categories.push_back(category);
+    }
+
+    // For the gfx category, also add the frame timeline data source
+    // as it's very useful for debugging gfx issues.
+    if (category == "gfx") {
+      auto* frame_timeline = config->add_data_sources();
+      frame_timeline->mutable_config()->set_name(
+          "android.surfaceflinger.frametimeline");
     }
   }
 
diff --git a/src/perfetto_cmd/pbtxt_to_pb.cc b/src/perfetto_cmd/pbtxt_to_pb.cc
index f4f0c00..b78d938 100644
--- a/src/perfetto_cmd/pbtxt_to_pb.cc
+++ b/src/perfetto_cmd/pbtxt_to_pb.cc
@@ -47,12 +47,20 @@
 
 namespace {
 
+constexpr bool IsOct(char c) {
+  return (c >= '0' && c <= '7');
+}
+
+constexpr bool IsDigit(char c) {
+  return (c >= '0' && c <= '9');
+}
+
 constexpr bool IsIdentifierStart(char c) {
   return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || c == '_';
 }
 
 constexpr bool IsIdentifierBody(char c) {
-  return IsIdentifierStart(c) || isdigit(c);
+  return IsIdentifierStart(c) || IsDigit(c);
 }
 
 const char* FieldToTypeName(const FieldDescriptorProto* field) {
@@ -150,19 +158,22 @@
   }
 
   void NumericField(Token key, Token value) {
-    const FieldDescriptorProto* field = FindFieldByName(
-        key, value,
-        {
-            FieldDescriptorProto::TYPE_UINT64,
-            FieldDescriptorProto::TYPE_UINT32, FieldDescriptorProto::TYPE_INT64,
-            FieldDescriptorProto::TYPE_SINT64, FieldDescriptorProto::TYPE_INT32,
-            FieldDescriptorProto::TYPE_SINT32,
-            FieldDescriptorProto::TYPE_FIXED64,
-            FieldDescriptorProto::TYPE_SFIXED64,
-            FieldDescriptorProto::TYPE_FIXED32,
-            FieldDescriptorProto::TYPE_SFIXED32,
-            FieldDescriptorProto::TYPE_DOUBLE, FieldDescriptorProto::TYPE_FLOAT,
-        });
+    const FieldDescriptorProto* field =
+        FindFieldByName(key, value,
+                        {
+                            FieldDescriptorProto::TYPE_UINT64,
+                            FieldDescriptorProto::TYPE_UINT32,
+                            FieldDescriptorProto::TYPE_INT64,
+                            FieldDescriptorProto::TYPE_SINT64,
+                            FieldDescriptorProto::TYPE_INT32,
+                            FieldDescriptorProto::TYPE_SINT32,
+                            FieldDescriptorProto::TYPE_FIXED64,
+                            FieldDescriptorProto::TYPE_SFIXED64,
+                            FieldDescriptorProto::TYPE_FIXED32,
+                            FieldDescriptorProto::TYPE_SFIXED32,
+                            FieldDescriptorProto::TYPE_DOUBLE,
+                            FieldDescriptorProto::TYPE_FLOAT,
+                        });
     if (!field)
       return;
     const auto& field_type = field->type();
@@ -202,11 +213,12 @@
   }
 
   void StringField(Token key, Token value) {
-    const FieldDescriptorProto* field = FindFieldByName(
-        key, value,
-        {
-            FieldDescriptorProto::TYPE_STRING, FieldDescriptorProto::TYPE_BYTES,
-        });
+    const FieldDescriptorProto* field =
+        FindFieldByName(key, value,
+                        {
+                            FieldDescriptorProto::TYPE_STRING,
+                            FieldDescriptorProto::TYPE_BYTES,
+                        });
     if (!field)
       return;
     uint32_t field_id = static_cast<uint32_t>(field->number());
@@ -217,15 +229,16 @@
     std::unique_ptr<char, base::FreeDeleter> s(
         static_cast<char*>(malloc(value.size())));
     size_t j = 0;
+    const char* const txt = value.txt.data();
     for (size_t i = 0; i < value.size(); i++) {
-      char c = value.txt.data()[i];
+      char c = txt[i];
       if (c == '\\') {
         if (i + 1 >= value.size()) {
           // This should be caught by the lexer.
           PERFETTO_FATAL("Escape at end of string.");
           return;
         }
-        char next = value.txt.data()[++i];
+        char next = txt[++i];
         switch (next) {
           case '\\':
           case '\'':
@@ -254,6 +267,44 @@
           case 'v':
             s.get()[j++] = '\v';
             break;
+          case '0':
+          case '1':
+          case '2':
+          case '3':
+          case '4':
+          case '5':
+          case '6':
+          case '7':
+          case '8':
+          case '9': {
+            // Cases 8 and 9 are not really required and are only added for the
+            // sake of error reporting.
+            bool oct_err = false;
+            if (i + 2 >= value.size() || !IsOct(txt[i + 1]) ||
+                !IsOct(txt[i + 2])) {
+              oct_err = true;
+            } else {
+              char buf[4]{next, txt[++i], txt[++i], '\0'};
+              auto octval = base::CStringToUInt32(buf, 8);
+              if (!octval.has_value() || *octval > 0xff) {
+                oct_err = true;
+              } else {
+                s.get()[j++] = static_cast<char>(static_cast<uint8_t>(*octval));
+              }
+            }
+            if (oct_err) {
+              AddError(value,
+                       "Malformed string escape in $k in proto $n on '$v'. "
+                       "\\NNN escapes must be exactly three octal digits <= "
+                       "\\377 (0xff).",
+                       std::map<std::string, std::string>{
+                           {"$k", key.ToStdString()},
+                           {"$n", descriptor_name()},
+                           {"$v", value.ToStdString()},
+                       });
+            }
+            break;
+          }
           default:
             AddError(value,
                      "Unknown string escape in $k in "
@@ -273,11 +324,12 @@
   }
 
   void IdentifierField(Token key, Token value) {
-    const FieldDescriptorProto* field = FindFieldByName(
-        key, value,
-        {
-            FieldDescriptorProto::TYPE_BOOL, FieldDescriptorProto::TYPE_ENUM,
-        });
+    const FieldDescriptorProto* field =
+        FindFieldByName(key, value,
+                        {
+                            FieldDescriptorProto::TYPE_BOOL,
+                            FieldDescriptorProto::TYPE_ENUM,
+                        });
     if (!field)
       return;
     uint32_t field_id = static_cast<uint32_t>(field->number());
@@ -415,7 +467,8 @@
     if (!field_descriptor) {
       AddError(key, "No field named \"$n\" in proto $p",
                {
-                   {"$n", field_name}, {"$p", descriptor_name()},
+                   {"$n", field_name},
+                   {"$p", descriptor_name()},
                });
       return nullptr;
     }
@@ -549,7 +602,7 @@
           state = kReadingStringValue;
           continue;
         }
-        if (c == '-' || isdigit(c) || c == '.') {
+        if (c == '-' || IsDigit(c) || c == '.') {
           state = kReadingNumericValue;
           continue;
         }
@@ -576,7 +629,7 @@
           delegate->NumericField(key, value);
           continue;
         }
-        if (isdigit(c) || c == '.')
+        if (IsDigit(c) || c == '.')
           continue;
         break;
 
diff --git a/src/perfetto_cmd/pbtxt_to_pb_unittest.cc b/src/perfetto_cmd/pbtxt_to_pb_unittest.cc
index 00dab16..251c8f0 100644
--- a/src/perfetto_cmd/pbtxt_to_pb_unittest.cc
+++ b/src/perfetto_cmd/pbtxt_to_pb_unittest.cc
@@ -403,6 +403,7 @@
       ftrace_events: "newline\nnewline"
       ftrace_events: "\"quoted\""
       ftrace_events: "\a\b\f\n\r\t\v\\\'\"\?"
+      ftrace_events: "\0127_\03422.\177"
     }
   }
 }
@@ -417,6 +418,7 @@
   EXPECT_THAT(events, Contains("newline\nnewline"));
   EXPECT_THAT(events, Contains("\"quoted\""));
   EXPECT_THAT(events, Contains("\a\b\f\n\r\t\v\\\'\"\?"));
+  EXPECT_THAT(events, Contains("\0127_\03422.\177"));
 }
 
 TEST(PbtxtToPb, UnknownField) {
diff --git a/src/perfetto_cmd/perfetto_cmd.cc b/src/perfetto_cmd/perfetto_cmd.cc
index c51f1b9..6bca082 100644
--- a/src/perfetto_cmd/perfetto_cmd.cc
+++ b/src/perfetto_cmd/perfetto_cmd.cc
@@ -70,7 +70,7 @@
 namespace perfetto {
 namespace {
 
-perfetto::PerfettoCmd* g_consumer_cmd;
+perfetto::PerfettoCmd* g_perfetto_cmd;
 
 uint32_t kOnTraceDataTimeoutMs = 3000;
 
@@ -166,54 +166,69 @@
 
 const char* kStateDir = "/data/misc/perfetto-traces";
 
-int PerfettoCmd::PrintUsage(const char* argv0) {
-  PERFETTO_ELOG(R"(
+PerfettoCmd::PerfettoCmd() {
+  PERFETTO_DCHECK(!g_perfetto_cmd);
+  g_perfetto_cmd = this;
+}
+
+PerfettoCmd::~PerfettoCmd() {
+  PERFETTO_DCHECK(g_perfetto_cmd == this);
+  g_perfetto_cmd = nullptr;
+}
+
+void PerfettoCmd::PrintUsage(const char* argv0) {
+  fprintf(stderr, R"(
 Usage: %s
-  --background     -d      : Exits immediately and continues tracing in
-                             background
+  --background     -d      : Exits immediately and continues in the background.
+                             Prints the PID of the bg process. The printed PID
+                             can used to gracefully terminate the tracing
+                             session by ussuing a `kill -TERM $PRINTED_PID`.
   --config         -c      : /path/to/trace/config/file or - for stdin
   --out            -o      : /path/to/out/trace/file or - for stdout
-  --upload                 : Upload field trace (Android only)
-  --dropbox        TAG     : DEPRECATED: Use --upload instead
-                             TAG should always be set to 'perfetto'
-  --no-guardrails          : Ignore guardrails triggered when using --dropbox
-                             (for testing).
   --txt                    : Parse config as pbtxt. Not for production use.
                              Not a stable API.
-  --reset-guardrails       : Resets the state of the guardails and exits
-                             (for testing).
   --query                  : Queries the service state and prints it as
                              human-readable text.
   --query-raw              : Like --query, but prints raw proto-encoded bytes
                              of tracing_service_state.proto.
-  --save-for-bugreport     : If a trace with bugreport_score > 0 is running, it
-                             saves it into a file. Outputs the path when done.
   --help           -h
 
-
-light configuration flags: (only when NOT using -c/--config)
+Light configuration flags: (only when NOT using -c/--config)
   --time           -t      : Trace duration N[s,m,h] (default: 10s)
   --buffer         -b      : Ring buffer size N[mb,gb] (default: 32mb)
-  --size           -s      : Max file size N[mb,gb] (default: in-memory ring-buffer only)
-  ATRACE_CAT               : Record ATRACE_CAT (e.g. wm)
+  --size           -s      : Max file size N[mb,gb]
+                            (default: in-memory ring-buffer only)
+  --app            -a      : Android (atrace) app name
   FTRACE_GROUP/FTRACE_NAME : Record ftrace event (e.g. sched/sched_switch)
+  ATRACE_CAT               : Record ATRACE_CAT (e.g. wm) (Android only)
 
-statsd-specific flags:
+Statsd-specific and other Android-only flags:
   --alert-id           : ID of the alert that triggered this trace.
   --config-id          : ID of the triggering config.
   --config-uid         : UID of app which registered the config.
   --subscription-id    : ID of the subscription that triggered this trace.
+  --upload             : Upload trace.
+  --dropbox        TAG : DEPRECATED: Use --upload instead
+                         TAG should always be set to 'perfetto'.
+  --save-for-bugreport : If a trace with bugreport_score > 0 is running, it
+                         saves it into a file. Outputs the path when done.
+  --no-guardrails      : Ignore guardrails triggered when using --upload
+                         (testing only).
+  --reset-guardrails   : Resets the state of the guardails and exits
+                         (testing only).
 
-Detach mode. DISCOURAGED, read https://perfetto.dev/docs/concepts/detached-mode :
+Detach mode. DISCOURAGED, read https://perfetto.dev/docs/concepts/detached-mode
   --detach=key          : Detach from the tracing session with the given key.
-  --attach=key [--stop] : Re-attach to the session (optionally stop tracing once reattached).
-  --is_detached=key     : Check if the session can be re-attached (0:Yes, 2:No, 1:Error).
+  --attach=key [--stop] : Re-attach to the session (optionally stop tracing
+                          once reattached).
+  --is_detached=key     : Check if the session can be re-attached.
+                          Exit code:  0:Yes, 2:No, 1:Error.
 )", /* this comment fixes syntax highlighting in some editors */
-                argv0);
-  return 1;
+          argv0);
 }
 
-int PerfettoCmd::Main(int argc, char** argv) {
+base::Optional<int> PerfettoCmd::ParseCmdlineAndMaybeDaemonize(int argc,
+                                                               char** argv) {
 #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
   umask(0000);  // make sure that file creation is not affected by umask.
 #endif
@@ -227,7 +242,6 @@
     OPT_PBTXT_CONFIG,
     OPT_DROPBOX,
     OPT_UPLOAD,
-    OPT_ATRACE_APP,
     OPT_IGNORE_GUARDRAILS,
     OPT_DETACH,
     OPT_ATTACH,
@@ -245,6 +259,7 @@
       {"time", required_argument, nullptr, 't'},
       {"buffer", required_argument, nullptr, 'b'},
       {"size", required_argument, nullptr, 's'},
+      {"app", required_argument, nullptr, 'a'},
       {"no-guardrails", no_argument, nullptr, OPT_IGNORE_GUARDRAILS},
       {"txt", no_argument, nullptr, OPT_PBTXT_CONFIG},
       {"upload", no_argument, nullptr, OPT_UPLOAD},
@@ -258,7 +273,6 @@
       {"attach", required_argument, nullptr, OPT_ATTACH},
       {"is_detached", required_argument, nullptr, OPT_IS_DETACHED},
       {"stop", no_argument, nullptr, OPT_STOP},
-      {"app", required_argument, nullptr, OPT_ATRACE_APP},
       {"query", no_argument, nullptr, OPT_QUERY},
       {"query-raw", no_argument, nullptr, OPT_QUERY_RAW},
       {"version", no_argument, nullptr, OPT_VERSION},
@@ -267,18 +281,21 @@
 
   std::string config_file_name;
   std::string trace_config_raw;
-  bool background = false;
-  bool ignore_guardrails = false;
   bool parse_as_pbtxt = false;
-  bool upload_flag = false;
   TraceConfig::StatsdMetadata statsd_metadata;
-  RateLimiter limiter;
+  limiter_.reset(new RateLimiter());
 
   ConfigOptions config_options;
   bool has_config_options = false;
 
+  if (argc <= 1) {
+    PrintUsage(argv[0]);
+    return 1;
+  }
+
   for (;;) {
-    int option = getopt_long(argc, argv, "hc:o:dt:b:s:", long_options, nullptr);
+    int option =
+        getopt_long(argc, argv, "hc:o:dt:b:s:a:", long_options, nullptr);
 
     if (option == -1)
       break;  // EOF.
@@ -313,7 +330,7 @@
     }
 
     if (option == 'd') {
-      background = true;
+      background_ = true;
       continue;
     }
     if (option == 't') {
@@ -334,9 +351,15 @@
       continue;
     }
 
+    if (option == 'a') {
+      config_options.atrace_apps.push_back(std::string(optarg));
+      has_config_options = true;
+      continue;
+    }
+
     if (option == OPT_UPLOAD) {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-      upload_flag = true;
+      upload_flag_ = true;
       continue;
 #else
       PERFETTO_ELOG("--upload is only supported on Android");
@@ -347,7 +370,7 @@
     if (option == OPT_DROPBOX) {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
       PERFETTO_CHECK(optarg);
-      upload_flag = true;
+      upload_flag_ = true;
       continue;
 #else
       PERFETTO_ELOG("--dropbox is only supported on Android");
@@ -361,12 +384,12 @@
     }
 
     if (option == OPT_IGNORE_GUARDRAILS) {
-      ignore_guardrails = true;
+      ignore_guardrails_ = true;
       continue;
     }
 
     if (option == OPT_RESET_GUARDRAILS) {
-      PERFETTO_CHECK(limiter.ClearState());
+      PERFETTO_CHECK(limiter_->ClearState());
       PERFETTO_ILOG("Guardrail state cleared");
       return 0;
     }
@@ -391,12 +414,6 @@
       continue;
     }
 
-    if (option == OPT_ATRACE_APP) {
-      config_options.atrace_apps.push_back(std::string(optarg));
-      has_config_options = true;
-      continue;
-    }
-
     if (option == OPT_DETACH) {
       detach_key_ = std::string(optarg);
       PERFETTO_CHECK(!detach_key_.empty());
@@ -442,7 +459,8 @@
       continue;
     }
 
-    return PrintUsage(argv[0]);
+    PrintUsage(argv[0]);
+    return 1;
   }
 
   for (ssize_t i = optind; i < argc; i++) {
@@ -450,7 +468,7 @@
     config_options.categories.push_back(argv[i]);
   }
 
-  if (query_service_ && (is_detach() || is_attach() || background)) {
+  if (query_service_ && (is_detach() || is_attach() || background_)) {
     PERFETTO_ELOG("--query cannot be combined with any other argument");
     return 1;
   }
@@ -460,7 +478,7 @@
     return 1;
   }
 
-  if (is_detach() && background) {
+  if (is_detach() && background_) {
     PERFETTO_ELOG("--detach and --background are mutually exclusive");
     return 1;
   }
@@ -484,7 +502,6 @@
   // For this we are just acting on already existing sessions.
   trace_config_.reset(new TraceConfig());
 
-  std::vector<std::string> triggers_to_activate;
   bool parsed = false;
   const bool will_trace = !is_attach() && !query_service_ && !bugreport_;
   if (!will_trace) {
@@ -539,7 +556,7 @@
   }
 
   if (!trace_config_->incident_report_config().destination_package().empty() &&
-      !upload_flag) {
+      !upload_flag_) {
     PERFETTO_ELOG(
         "Unexpected IncidentReportConfig without --dropbox / --upload.");
     return 1;
@@ -548,7 +565,7 @@
   if (trace_config_->activate_triggers().empty() &&
       trace_config_->incident_report_config().destination_package().empty() &&
       !trace_config_->incident_report_config().skip_incidentd() &&
-      upload_flag) {
+      upload_flag_) {
     PERFETTO_ELOG(
         "Missing IncidentReportConfig.destination_package with --dropbox / "
         "--upload.");
@@ -558,7 +575,7 @@
   // 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();
+      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.
@@ -570,7 +587,7 @@
       statsd_logging_ = false;
       break;
     case TraceConfig::STATSD_LOGGING_UNSPECIFIED:
-      statsd_logging_ = upload_flag;
+      statsd_logging_ = upload_flag_;
       break;
   }
   trace_config_->set_statsd_logging(statsd_logging_
@@ -580,7 +597,7 @@
   // Set up the output file. Either --out or --upload are expected, with the
   // only exception of --attach. In this case the output file is passed when
   // detaching.
-  if (!trace_out_path_.empty() && upload_flag) {
+  if (!trace_out_path_.empty() && upload_flag_) {
     PERFETTO_ELOG(
         "Can't log to a file (--out) and incidentd (--upload) at the same "
         "time");
@@ -588,7 +605,7 @@
   }
 
   if (!trace_config_->output_path().empty()) {
-    if (!trace_out_path_.empty() || upload_flag) {
+    if (!trace_out_path_.empty() || upload_flag_) {
       PERFETTO_ELOG(
           "Can't pass --out or --upload if output_path is set in the "
           "trace config");
@@ -610,7 +627,7 @@
   // and activate triggers.
   if (!trace_config_->activate_triggers().empty()) {
     for (const auto& trigger : trace_config_->activate_triggers()) {
-      triggers_to_activate.push_back(trigger);
+      triggers_to_activate_.push_back(trigger);
     }
     trace_config_.reset(new TraceConfig());
   }
@@ -618,15 +635,15 @@
   bool open_out_file = true;
   if (!will_trace) {
     open_out_file = false;
-    if (!trace_out_path_.empty() || upload_flag) {
+    if (!trace_out_path_.empty() || upload_flag_) {
       PERFETTO_ELOG("Can't pass an --out file (or --upload) with this option");
       return 1;
     }
-  } else if (!triggers_to_activate.empty() ||
+  } else if (!triggers_to_activate_.empty() ||
              (trace_config_->write_into_file() &&
               !trace_config_->output_path().empty())) {
     open_out_file = false;
-  } else if (trace_out_path_.empty() && !upload_flag) {
+  } else if (trace_out_path_.empty() && !upload_flag_) {
     PERFETTO_ELOG("Either --out or --upload is required");
     return 1;
   } else if (is_detach() && !trace_config_->write_into_file()) {
@@ -653,67 +670,6 @@
       packet_writer_ = CreateFilePacketWriter(trace_out_stream_.get());
   }
 
-  if (background) {
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
-    PERFETTO_FATAL("--background is not supported on Windows");
-#else
-    pid_t pid;
-    switch (pid = fork()) {
-      case -1:
-        PERFETTO_FATAL("fork");
-      case 0: {
-        PERFETTO_CHECK(setsid() != -1);
-        base::ignore_result(chdir("/"));
-        base::ScopedFile null = base::OpenFile("/dev/null", O_RDONLY);
-        PERFETTO_CHECK(null);
-        PERFETTO_CHECK(dup2(*null, STDIN_FILENO) != -1);
-        PERFETTO_CHECK(dup2(*null, STDOUT_FILENO) != -1);
-        PERFETTO_CHECK(dup2(*null, STDERR_FILENO) != -1);
-        // Do not accidentally close stdin/stdout/stderr.
-        if (*null <= 2)
-          null.release();
-        break;
-      }
-      default:
-        printf("%d\n", pid);
-        exit(0);
-    }
-#endif  // OS_WIN
-  }
-
-  // If we are just activating triggers then we don't need to rate limit,
-  // connect as a consumer or run the trace. So bail out after processing all
-  // the options.
-  if (!triggers_to_activate.empty()) {
-    LogUploadEvent(PerfettoStatsdAtom::kTriggerBegin);
-    LogTriggerEvents(PerfettoTriggerAtom::kCmdTrigger, triggers_to_activate);
-
-    bool finished_with_success = false;
-    TriggerProducer producer(
-        &task_runner_,
-        [this, &finished_with_success](bool success) {
-          finished_with_success = success;
-          task_runner_.Quit();
-        },
-        &triggers_to_activate);
-    task_runner_.Run();
-    if (finished_with_success) {
-      LogUploadEvent(PerfettoStatsdAtom::kTriggerSuccess);
-    } else {
-      LogUploadEvent(PerfettoStatsdAtom::kTriggerFailure);
-      LogTriggerEvents(PerfettoTriggerAtom::kCmdTriggerFail,
-                       triggers_to_activate);
-    }
-    return finished_with_success ? 0 : 1;
-  }
-
-  if (query_service_ || bugreport_) {
-    consumer_endpoint_ =
-        ConsumerIPCClient::Connect(GetConsumerSocket(), this, &task_runner_);
-    task_runner_.Run();
-    return 1;  // We can legitimately get here if the service disconnects.
-  }
-
   if (trace_config_->compression_type() ==
       TraceConfig::COMPRESSION_TYPE_DEFLATE) {
     if (packet_writer_) {
@@ -727,11 +683,59 @@
     }
   }
 
+  if (save_to_incidentd_ && !ignore_guardrails_ &&
+      (trace_config_->duration_ms() == 0 &&
+       trace_config_->trigger_config().trigger_timeout_ms() == 0)) {
+    PERFETTO_ELOG("Can't trace indefinitely when tracing to Incidentd.");
+    return 1;
+  }
+
+  if (background_) {
+    base::Daemonize();
+  }
+
+  return base::nullopt;  // Continues in ConnectToServiceAndRun() below.
+}
+
+int PerfettoCmd::ConnectToServiceAndRun() {
+  // If we are just activating triggers then we don't need to rate limit,
+  // connect as a consumer or run the trace. So bail out after processing all
+  // the options.
+  if (!triggers_to_activate_.empty()) {
+    LogUploadEvent(PerfettoStatsdAtom::kTriggerBegin);
+    LogTriggerEvents(PerfettoTriggerAtom::kCmdTrigger, triggers_to_activate_);
+
+    bool finished_with_success = false;
+    TriggerProducer producer(
+        &task_runner_,
+        [this, &finished_with_success](bool success) {
+          finished_with_success = success;
+          task_runner_.Quit();
+        },
+        &triggers_to_activate_);
+    task_runner_.Run();
+    if (finished_with_success) {
+      LogUploadEvent(PerfettoStatsdAtom::kTriggerSuccess);
+    } else {
+      LogUploadEvent(PerfettoStatsdAtom::kTriggerFailure);
+      LogTriggerEvents(PerfettoTriggerAtom::kCmdTriggerFail,
+                       triggers_to_activate_);
+    }
+    return finished_with_success ? 0 : 1;
+  }  // if (triggers_to_activate_)
+
+  if (query_service_ || bugreport_) {
+    consumer_endpoint_ =
+        ConsumerIPCClient::Connect(GetConsumerSocket(), this, &task_runner_);
+    task_runner_.Run();
+    return 1;  // We can legitimately get here if the service disconnects.
+  }            // if (query_service || bugreport_)
+
   RateLimiter::Args args{};
   args.is_user_build = IsUserBuild();
   args.is_uploading = save_to_incidentd_;
   args.current_time = base::GetWallTimeS();
-  args.ignore_guardrails = ignore_guardrails;
+  args.ignore_guardrails = ignore_guardrails_;
   args.allow_user_build_tracing = trace_config_->allow_user_build_tracing();
   args.unique_session_name = trace_config_->unique_session_name();
   args.max_upload_bytes_override =
@@ -740,13 +744,6 @@
   if (!args.unique_session_name.empty())
     base::MaybeSetThreadName("p-" + args.unique_session_name);
 
-  if (args.is_uploading && !args.ignore_guardrails &&
-      (trace_config_->duration_ms() == 0 &&
-       trace_config_->trigger_config().trigger_timeout_ms() == 0)) {
-    PERFETTO_ELOG("Can't trace indefinitely when tracing to Dropbox.");
-    return 1;
-  }
-
   expected_duration_ms_ = trace_config_->duration_ms();
   if (!expected_duration_ms_) {
     uint32_t timeout_ms = trace_config_->trigger_config().trigger_timeout_ms();
@@ -763,7 +760,7 @@
     LogUploadEvent(PerfettoStatsdAtom::kBackgroundTraceBegin);
   }
 
-  auto err_atom = ConvertRateLimiterResponseToAtom(limiter.ShouldTrace(args));
+  auto err_atom = ConvertRateLimiterResponseToAtom(limiter_->ShouldTrace(args));
   if (err_atom) {
     // TODO(lalitm): remove this once we're ready on server side.
     LogUploadEvent(PerfettoStatsdAtom::kHitGuardrails);
@@ -771,13 +768,26 @@
     return 1;
   }
 
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+  if (!background_ && !is_detach() && !upload_flag_ &&
+      triggers_to_activate_.empty() && !isatty(STDIN_FILENO) &&
+      !isatty(STDERR_FILENO)) {
+    fprintf(stderr,
+            "Warning: No PTY. CTRL+C won't gracefully stop the trace. If you "
+            "are running perfetto via adb shell, use the -tt arg (adb shell "
+            "-t perfetto ...) or consider using the helper script "
+            "tools/record_android_trace from the Perfetto repository.\n\n");
+  }
+#endif
+
   consumer_endpoint_ =
       ConsumerIPCClient::Connect(GetConsumerSocket(), this, &task_runner_);
   SetupCtrlCSignalHandler();
   task_runner_.Run();
 
-  return limiter.OnTraceDone(args, update_guardrail_state_, bytes_written_) ? 0
-                                                                            : 1;
+  return limiter_->OnTraceDone(args, update_guardrail_state_, bytes_written_)
+             ? 0
+             : 1;
 }
 
 void PerfettoCmd::OnConnect() {
@@ -926,6 +936,10 @@
   }
 
   if (save_to_incidentd_) {
+    if (!uuid_.empty()) {
+      base::Uuid uuid(uuid_);
+      PERFETTO_LOG("go/trace-uuid/%s", uuid.ToPrettyString().c_str());
+    }
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
     SaveTraceIntoDropboxAndIncidentOrCrash();
 #endif
@@ -948,7 +962,7 @@
   base::ScopedFile fd;
   if (trace_out_path_.empty()) {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-    fd = CreateUnlikedTmpFile();
+    fd = CreateUnlinkedTmpFile();
 #endif
   } else if (trace_out_path_ == "-") {
     fd.reset(dup(fileno(stdout)));
@@ -969,7 +983,7 @@
 }
 
 void PerfettoCmd::SetupCtrlCSignalHandler() {
-  base::InstallCtrCHandler([] { g_consumer_cmd->SignalCtrlC(); });
+  base::InstallCtrCHandler([] { g_perfetto_cmd->SignalCtrlC(); });
   task_runner_.AddFileDescriptorWatch(ctrl_c_evt_.fd(), [this] {
     PERFETTO_LOG("SIGINT/SIGTERM received: disabling tracing.");
     ctrl_c_evt_.Clear();
@@ -1044,6 +1058,7 @@
     printf("  id: %d\n", producer.id());
     printf("  name: \"%s\" \n", producer.name().c_str());
     printf("  uid: %d \n", producer.uid());
+    printf("  sdk_version: \"%s\" \n", producer.sdk_version().c_str());
     printf("}\n");
   }
 
@@ -1055,6 +1070,8 @@
     printf("  }\n");
     printf("}\n");
   }
+  printf("tracing_service_version: \"%s\"\n",
+         svc_state.tracing_service_version().c_str());
   printf("num_sessions: %d\n", svc_state.num_sessions());
   printf("num_sessions_started: %d\n", svc_state.num_sessions_started());
 }
@@ -1078,8 +1095,11 @@
 }
 
 int PERFETTO_EXPORT_ENTRYPOINT PerfettoCmdMain(int argc, char** argv) {
-  g_consumer_cmd = new perfetto::PerfettoCmd();
-  return g_consumer_cmd->Main(argc, argv);
+  perfetto::PerfettoCmd cmd;
+  auto opt_res = cmd.ParseCmdlineAndMaybeDaemonize(argc, argv);
+  if (opt_res.has_value())
+    return *opt_res;
+  return cmd.ConnectToServiceAndRun();
 }
 
 }  // namespace perfetto
diff --git a/src/perfetto_cmd/perfetto_cmd.h b/src/perfetto_cmd/perfetto_cmd.h
index 7f1b866..f1bfe5b 100644
--- a/src/perfetto_cmd/perfetto_cmd.h
+++ b/src/perfetto_cmd/perfetto_cmd.h
@@ -36,6 +36,7 @@
 namespace perfetto {
 
 class PacketWriter;
+class RateLimiter;
 
 // Directory for local state and temporary files. This is automatically
 // created by the system by setting setprop persist.traced.enable=1.
@@ -43,7 +44,17 @@
 
 class PerfettoCmd : public Consumer {
  public:
-  int Main(int argc, char** argv);
+  PerfettoCmd();
+  ~PerfettoCmd() override;
+
+  // The main() is split in two stages: cmdline parsing and actual interaction
+  // with traced. This is to allow tools like tracebox to avoid spawning the
+  // service for no reason if the cmdline parsing fails.
+  // Return value:
+  //   nullopt: no error, the caller should call ConnectToServiceAndRun.
+  //   0-N: the caller should exit() with the given exit code.
+  base::Optional<int> ParseCmdlineAndMaybeDaemonize(int argc, char** argv);
+  int ConnectToServiceAndRun();
 
   // perfetto::Consumer implementation.
   void OnConnect() override;
@@ -61,7 +72,7 @@
   bool OpenOutputFile();
   void SetupCtrlCSignalHandler();
   void FinalizeTraceAndExit();
-  int PrintUsage(const char* argv0);
+  void PrintUsage(const char* argv0);
   void PrintServiceState(bool success, const TracingServiceState&);
   void OnTimeout();
   bool is_detach() const { return !detach_key_.empty(); }
@@ -76,7 +87,7 @@
   void CheckTraceDataTimeout();
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-  static base::ScopedFile CreateUnlikedTmpFile();
+  static base::ScopedFile CreateUnlinkedTmpFile();
   void SaveTraceIntoDropboxAndIncidentOrCrash();
   void SaveOutputToIncidentTraceOrCrash();
 #endif
@@ -86,13 +97,13 @@
 
   base::UnixTaskRunner task_runner_;
 
+  std::unique_ptr<RateLimiter> limiter_;
   std::unique_ptr<perfetto::TracingService::ConsumerEndpoint>
       consumer_endpoint_;
   std::unique_ptr<TraceConfig> trace_config_;
-
   std::unique_ptr<PacketWriter> packet_writer_;
   base::ScopedFstream trace_out_stream_;
-
+  std::vector<std::string> triggers_to_activate_;
   std::string trace_out_path_;
   base::EventFd ctrl_c_evt_;
   bool save_to_incidentd_ = false;
@@ -106,6 +117,9 @@
   bool query_service_ = false;
   bool query_service_output_raw_ = false;
   bool bugreport_ = false;
+  bool background_ = false;
+  bool ignore_guardrails_ = false;
+  bool upload_flag_ = false;
   std::string uuid_;
 
   // How long we expect to trace for or 0 if the trace is indefinite.
diff --git a/src/perfetto_cmd/perfetto_cmd_android.cc b/src/perfetto_cmd/perfetto_cmd_android.cc
index 913e1c4..fe2572c 100644
--- a/src/perfetto_cmd/perfetto_cmd_android.cc
+++ b/src/perfetto_cmd/perfetto_cmd_android.cc
@@ -122,7 +122,7 @@
 }
 
 // static
-base::ScopedFile PerfettoCmd::CreateUnlikedTmpFile() {
+base::ScopedFile PerfettoCmd::CreateUnlinkedTmpFile() {
   // If we are tracing to DropBox, there's no need to make a
   // filesystem-visible temporary file.
   auto fd = base::OpenFile(kStateDir, O_TMPFILE | O_RDWR, 0600);
diff --git a/src/perfetto_cmd/trigger_perfetto.cc b/src/perfetto_cmd/trigger_perfetto.cc
index ae44018..3b7f891 100644
--- a/src/perfetto_cmd/trigger_perfetto.cc
+++ b/src/perfetto_cmd/trigger_perfetto.cc
@@ -42,7 +42,21 @@
   static const option long_options[] = {{"help", no_argument, nullptr, 'h'},
                                         {nullptr, 0, nullptr, 0}};
 
+  // Set opterror to zero to disable |getopt_long| from printing an error and
+  // exiting when it encounters an unknown option. Instead, |getopt_long|
+  // will return '?' which we silently ignore.
+  //
+  // We prefer ths behaviour rather than erroring on unknown options because
+  // trigger_perfetto can be called by apps so it's command line API needs to
+  // be backward and forward compatible. If we introduce an option here which
+  // apps will use in the future, we don't want to cause errors on older
+  // platforms where the command line flag did not exist.
+  //
+  // This behaviour was introduced in Android S.
+  opterr = 0;
+
   std::vector<std::string> triggers_to_activate;
+  bool seen_unknown_arg = false;
 
   for (;;) {
     int option = getopt_long(argc, argv, "h", long_options, nullptr);
@@ -50,10 +64,20 @@
     if (option == 'h')
       return PrintUsage(argv[0]);
 
+    if (option == '?') {
+      seen_unknown_arg = true;
+    }
+
     if (option == -1)
       break;  // EOF.
   }
 
+  // See above for rationale on why we just ignore unknown args instead of
+  // exiting.
+  if (seen_unknown_arg) {
+    PERFETTO_ELOG("Ignoring unknown arguments. See --help for usage.");
+  }
+
   for (int i = optind; i < argc; i++)
     triggers_to_activate.push_back(std::string(argv[i]));
 
diff --git a/src/profiling/common/proc_utils.cc b/src/profiling/common/proc_utils.cc
index 155d091..c2d1e55 100644
--- a/src/profiling/common/proc_utils.cc
+++ b/src/profiling/common/proc_utils.cc
@@ -193,5 +193,35 @@
   }
 }
 
+base::Optional<Uids> GetUids(const std::string& status) {
+  auto entry_idx = status.find("Uid:");
+  if (entry_idx == std::string::npos)
+    return base::nullopt;
+
+  Uids uids;
+  const char* str = &status[entry_idx + 4];
+  char* endptr;
+
+  uids.real = strtoull(str, &endptr, 10);
+  if (*endptr != ' ' && *endptr != '\t')
+    return base::nullopt;
+
+  str = endptr;
+  uids.effective = strtoull(str, &endptr, 10);
+  if (*endptr != ' ' && *endptr != '\t')
+    return base::nullopt;
+
+  str = endptr;
+  uids.saved_set = strtoull(str, &endptr, 10);
+  if (*endptr != ' ' && *endptr != '\t')
+    return base::nullopt;
+
+  str = endptr;
+  uids.filesystem = strtoull(str, &endptr, 10);
+  if (*endptr != '\n' && *endptr != '\0')
+    return base::nullopt;
+  return uids;
+}
+
 }  // namespace profiling
 }  // namespace perfetto
diff --git a/src/profiling/common/proc_utils.h b/src/profiling/common/proc_utils.h
index 61c35b4..facd10e 100644
--- a/src/profiling/common/proc_utils.h
+++ b/src/profiling/common/proc_utils.h
@@ -17,7 +17,9 @@
 #ifndef SRC_PROFILING_COMMON_PROC_UTILS_H_
 #define SRC_PROFILING_COMMON_PROC_UTILS_H_
 
+#include <inttypes.h>
 #include <sys/types.h>
+
 #include <set>
 #include <vector>
 
@@ -27,6 +29,13 @@
 namespace perfetto {
 namespace profiling {
 
+struct Uids {
+  uint64_t real;
+  uint64_t effective;
+  uint64_t saved_set;
+  uint64_t filesystem;
+};
+
 template <typename Fn>
 void ForEachPid(Fn callback) {
   base::ScopedDir proc_dir(opendir("/proc"));
@@ -58,6 +67,8 @@
 // entries satisfying the minimum size criteria for anonymous memory.
 void RemoveUnderAnonThreshold(uint32_t min_size_kb, std::set<pid_t>* pids);
 
+base::Optional<Uids> GetUids(const std::string&);
+
 }  // namespace profiling
 }  // namespace perfetto
 
diff --git a/src/profiling/common/proc_utils_unittest.cc b/src/profiling/common/proc_utils_unittest.cc
index f16c834..cec73d8 100644
--- a/src/profiling/common/proc_utils_unittest.cc
+++ b/src/profiling/common/proc_utils_unittest.cc
@@ -15,6 +15,7 @@
  */
 
 #include "src/profiling/common/proc_utils.h"
+#include "perfetto/ext/base/optional.h"
 #include "perfetto/profiling/normalize.h"
 
 #include "perfetto/ext/base/utils.h"
@@ -135,6 +136,35 @@
   EXPECT_EQ(GetRssAnonAndSwap("RssAnon: 10000 kB"), base::nullopt);
   EXPECT_EQ(GetRssAnonAndSwap("VmSwap: 10000"), base::nullopt);
 }
+
+TEST(ProcUtilsTest, GetUids) {
+  std::string status =
+      "Name: foo\nRssAnon:  10000 kB\nVmSwap:\t10000 kB\n"
+      "Uid: 1 2 3 4\n";
+  auto uids = GetUids(status);
+  ASSERT_NE(uids, base::nullopt);
+  EXPECT_EQ(uids->real, 1u);
+  EXPECT_EQ(uids->effective, 2u);
+  EXPECT_EQ(uids->saved_set, 3u);
+  EXPECT_EQ(uids->filesystem, 4u);
+}
+
+TEST(ProcUtilsTest, GetUidsInvalidInt) {
+  std::string status =
+      "Name: foo\nRssAnon:  10000 kB\nVmSwap:\t10000 kB\n"
+      "Uid: 1a 2 3 4\n";
+  auto uids = GetUids(status);
+  EXPECT_EQ(uids, base::nullopt);
+}
+
+TEST(ProcUtilsTest, GetUidsInvalidTooFew) {
+  std::string status =
+      "Name: foo\nRssAnon:  10000 kB\nVmSwap:\t10000 kB\n"
+      "Uid: 1 2 3\n";
+  auto uids = GetUids(status);
+  EXPECT_EQ(uids, base::nullopt);
+}
+
 }  // namespace
 }  // namespace profiling
 }  // namespace perfetto
diff --git a/src/profiling/common/producer_support.cc b/src/profiling/common/producer_support.cc
index d6a58ec..2347edd 100644
--- a/src/profiling/common/producer_support.cc
+++ b/src/profiling/common/producer_support.cc
@@ -20,6 +20,7 @@
 #include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/tracing/core/data_source_config.h"
 
+#include "perfetto/tracing/core/forward_decls.h"
 #include "src/traced/probes/packages_list/packages_list_parser.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
@@ -29,7 +30,9 @@
 namespace perfetto {
 namespace profiling {
 
-bool CanProfile(const DataSourceConfig& ds_config, uint64_t uid) {
+bool CanProfile(const DataSourceConfig& ds_config,
+                uint64_t uid,
+                const std::vector<std::string>& installed_by) {
 // We restrict by !PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD) because a
 // sideloaded heapprofd should not be restricted by this. Do note though that,
 // at the moment, there isn't really a way to sideload a functioning heapprofd
@@ -38,18 +41,20 @@
     !PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
   base::ignore_result(ds_config);
   base::ignore_result(uid);
+  base::ignore_result(installed_by);
   return true;
 #else
   char buf[PROP_VALUE_MAX + 1] = {};
   int ret = __system_property_get("ro.build.type", buf);
   PERFETTO_CHECK(ret >= 0);
-  return CanProfileAndroid(ds_config, uid, std::string(buf),
+  return CanProfileAndroid(ds_config, uid, installed_by, std::string(buf),
                            "/data/system/packages.list");
 #endif
 }
 
 bool CanProfileAndroid(const DataSourceConfig& ds_config,
                        uint64_t uid,
+                       const std::vector<std::string>& installed_by,
                        const std::string& build_type,
                        const std::string& packages_list_path) {
   // These are replicated constants from libcutils android_filesystem_config.h
@@ -61,10 +66,6 @@
     return true;
   }
 
-  if (ds_config.enable_extra_guardrails()) {
-    return false;  // no extra guardrails on user builds.
-  }
-
   uint64_t uid_without_profile = uid % kAidUserOffset;
   if (uid_without_profile < kAidAppStart || kAidAppEnd < uid_without_profile) {
     // TODO(fmayer): relax this.
@@ -82,11 +83,28 @@
       PERFETTO_ELOG("Failed to parse packages.list.");
       return false;
     }
-    if (pkg.uid == uid_without_profile &&
-        (pkg.profileable_from_shell || pkg.debuggable)) {
-      return true;
+    if (pkg.uid != uid_without_profile)
+      continue;
+    if (!installed_by.empty()) {
+      if (pkg.installed_by.empty()) {
+        PERFETTO_ELOG(
+            "installed_by given in TraceConfig, but cannot parse "
+            "installer from packages.list.");
+        return false;
+      }
+      if (std::find(installed_by.cbegin(), installed_by.cend(),
+                    pkg.installed_by) == installed_by.cend()) {
+        return false;
+      }
+    }
+    switch (ds_config.session_initiator()) {
+      case DataSourceConfig::SESSION_INITIATOR_UNSPECIFIED:
+        return pkg.profileable_from_shell || pkg.debuggable;
+      case DataSourceConfig::SESSION_INITIATOR_TRUSTED_SYSTEM:
+        return pkg.profileable || pkg.debuggable;
     }
   }
+  // Did not find package.
   return false;
 }
 
diff --git a/src/profiling/common/producer_support.h b/src/profiling/common/producer_support.h
index 6f63e47..4e6b856 100644
--- a/src/profiling/common/producer_support.h
+++ b/src/profiling/common/producer_support.h
@@ -25,9 +25,12 @@
 namespace perfetto {
 namespace profiling {
 
-bool CanProfile(const DataSourceConfig& ds_config, uint64_t uid);
+bool CanProfile(const DataSourceConfig& ds_config,
+                uint64_t uid,
+                const std::vector<std::string>& installed_by);
 bool CanProfileAndroid(const DataSourceConfig& ds_config,
                        uint64_t uid,
+                       const std::vector<std::string>& installed_by,
                        const std::string& build_type,
                        const std::string& packages_list_path);
 
diff --git a/src/profiling/common/producer_support_unittest.cc b/src/profiling/common/producer_support_unittest.cc
index df2129a..33fdf28 100644
--- a/src/profiling/common/producer_support_unittest.cc
+++ b/src/profiling/common/producer_support_unittest.cc
@@ -29,7 +29,7 @@
 TEST(CanProfileAndroidTest, NonUserSystemExtraGuardrails) {
   DataSourceConfig ds_config;
   ds_config.set_enable_extra_guardrails(true);
-  EXPECT_TRUE(CanProfileAndroid(ds_config, 1, "userdebug", "/dev/null"));
+  EXPECT_TRUE(CanProfileAndroid(ds_config, 1, {}, "userdebug", "/dev/null"));
 }
 
 TEST(CanProfileAndroidTest, NonUserNonProfileableApp) {
@@ -41,7 +41,7 @@
       "/data/user/0/invalid.example.profileable default:targetSdkVersion=10000 "
       "none 0 1\n";
   base::WriteAll(tmp.fd(), content, sizeof(content));
-  EXPECT_TRUE(CanProfileAndroid(ds_config, 10001, "userdebug", tmp.path()));
+  EXPECT_TRUE(CanProfileAndroid(ds_config, 10001, {}, "userdebug", tmp.path()));
 }
 
 TEST(CanProfileAndroidTest, NonUserNonProfileableAppExtraGuardrails) {
@@ -53,7 +53,7 @@
       "/data/user/0/invalid.example.profileable default:targetSdkVersion=10000 "
       "none 0 1\n";
   base::WriteAll(tmp.fd(), content, sizeof(content));
-  EXPECT_TRUE(CanProfileAndroid(ds_config, 10001, "userdebug", tmp.path()));
+  EXPECT_TRUE(CanProfileAndroid(ds_config, 10001, {}, "userdebug", tmp.path()));
 }
 
 TEST(CanProfileAndroidTest, UserProfileableApp) {
@@ -65,19 +65,7 @@
       "/data/user/0/invalid.example.profileable default:targetSdkVersion=10000 "
       "none 1 1\n";
   base::WriteAll(tmp.fd(), content, sizeof(content));
-  EXPECT_TRUE(CanProfileAndroid(ds_config, 10001, "user", tmp.path()));
-}
-
-TEST(CanProfileAndroidTest, UserProfileableAppExtraGuardrails) {
-  DataSourceConfig ds_config;
-  ds_config.set_enable_extra_guardrails(true);
-  auto tmp = base::TempFile::Create();
-  constexpr char content[] =
-      "invalid.example.profileable 10001 0 "
-      "/data/user/0/invalid.example.profileable default:targetSdkVersion=10000 "
-      "none 1 1\n";
-  base::WriteAll(tmp.fd(), content, sizeof(content));
-  EXPECT_FALSE(CanProfileAndroid(ds_config, 10001, "user", tmp.path()));
+  EXPECT_TRUE(CanProfileAndroid(ds_config, 10001, {}, "user", tmp.path()));
 }
 
 TEST(CanProfileAndroidTest, UserProfileableAppMultiuser) {
@@ -89,7 +77,7 @@
       "/data/user/0/invalid.example.profileable default:targetSdkVersion=10000 "
       "none 1 1\n";
   base::WriteAll(tmp.fd(), content, sizeof(content));
-  EXPECT_TRUE(CanProfileAndroid(ds_config, 210001, "user", tmp.path()));
+  EXPECT_TRUE(CanProfileAndroid(ds_config, 210001, {}, "user", tmp.path()));
 }
 
 TEST(CanProfileAndroidTest, UserNonProfileableApp) {
@@ -101,7 +89,7 @@
       "/data/user/0/invalid.example.profileable default:targetSdkVersion=10000 "
       "none 0 1\n";
   base::WriteAll(tmp.fd(), content, sizeof(content));
-  EXPECT_FALSE(CanProfileAndroid(ds_config, 10001, "user", tmp.path()));
+  EXPECT_FALSE(CanProfileAndroid(ds_config, 10001, {}, "user", tmp.path()));
 }
 
 TEST(CanProfileAndroidTest, UserDebuggableApp) {
@@ -113,7 +101,72 @@
       "/data/user/0/invalid.example.profileable default:targetSdkVersion=10000 "
       "none 0 1\n";
   base::WriteAll(tmp.fd(), content, sizeof(content));
-  EXPECT_TRUE(CanProfileAndroid(ds_config, 10001, "user", tmp.path()));
+  EXPECT_TRUE(CanProfileAndroid(ds_config, 10001, {}, "user", tmp.path()));
+}
+
+TEST(CanProfileAndroidTest, UserProfileableMatchingInstallerStatsd) {
+  DataSourceConfig ds_config;
+  ds_config.set_session_initiator(
+      DataSourceConfig::SESSION_INITIATOR_TRUSTED_SYSTEM);
+  auto tmp = base::TempFile::Create();
+  constexpr char content[] =
+      "invalid.example 10001 0 /data/user/0/invalid.example "
+      "default:targetSdkVersion=29 3002,3003 0 13030407 1 invalid.store";
+  base::WriteAll(tmp.fd(), content, sizeof(content));
+  EXPECT_TRUE(CanProfileAndroid(ds_config, 10001, {"invalid.store"}, "user",
+                                tmp.path()));
+}
+
+TEST(CanProfileAndroidTest, UserProfileableMatchingInstallerShell) {
+  DataSourceConfig ds_config;
+  ds_config.set_session_initiator(
+      DataSourceConfig::SESSION_INITIATOR_UNSPECIFIED);
+  auto tmp = base::TempFile::Create();
+  constexpr char content[] =
+      "invalid.example 10001 0 /data/user/0/invalid.example "
+      "default:targetSdkVersion=29 3002,3003 0 13030407 1 invalid.store";
+  base::WriteAll(tmp.fd(), content, sizeof(content));
+  EXPECT_FALSE(CanProfileAndroid(ds_config, 10001, {"invalid.store"}, "user",
+                                 tmp.path()));
+}
+
+TEST(CanProfileAndroidTest, UserProfileableNonMatchingInstallerStatsd) {
+  DataSourceConfig ds_config;
+  ds_config.set_session_initiator(
+      DataSourceConfig::SESSION_INITIATOR_TRUSTED_SYSTEM);
+  auto tmp = base::TempFile::Create();
+  constexpr char content[] =
+      "invalid.example 10001 0 /data/user/0/invalid.example "
+      "default:targetSdkVersion=29 3002,3003 0 13030407 1 invalid.store";
+  base::WriteAll(tmp.fd(), content, sizeof(content));
+  EXPECT_FALSE(CanProfileAndroid(ds_config, 10001, {"invalid.otherstore"},
+                                 "user", tmp.path()));
+}
+
+TEST(CanProfileAndroidTest, UserProfileableNonMatchingInstallerShell) {
+  DataSourceConfig ds_config;
+  ds_config.set_session_initiator(
+      DataSourceConfig::SESSION_INITIATOR_UNSPECIFIED);
+  auto tmp = base::TempFile::Create();
+  constexpr char content[] =
+      "invalid.example 10001 0 /data/user/0/invalid.example "
+      "default:targetSdkVersion=29 3002,3003 0 13030407 1 invalid.store";
+  base::WriteAll(tmp.fd(), content, sizeof(content));
+  EXPECT_FALSE(CanProfileAndroid(ds_config, 10001, {"invalid.otherstore"},
+                                 "user", tmp.path()));
+}
+
+TEST(CanProfileAndroidTest, UserProfileableFromShellWithInstallerOldPackages) {
+  DataSourceConfig ds_config;
+  ds_config.set_session_initiator(
+      DataSourceConfig::SESSION_INITIATOR_UNSPECIFIED);
+  auto tmp = base::TempFile::Create();
+  constexpr char content[] =
+      "invalid.example 10001 0 /data/user/0/invalid.example "
+      "default:targetSdkVersion=29 3002,3003 1 13030407";
+  base::WriteAll(tmp.fd(), content, sizeof(content));
+  EXPECT_FALSE(CanProfileAndroid(ds_config, 10001, {"invalid.otherstore"},
+                                 "user", tmp.path()));
 }
 
 }  // namespace
diff --git a/src/profiling/common/unwind_support.cc b/src/profiling/common/unwind_support.cc
index 1ab2460..5302015 100644
--- a/src/profiling/common/unwind_support.cc
+++ b/src/profiling/common/unwind_support.cc
@@ -47,8 +47,9 @@
 
 size_t FDMemory::Read(uint64_t addr, void* dst, size_t size) {
   ssize_t rd = pread64(*mem_fd_, dst, size, static_cast<off64_t>(addr));
-  if (rd == -1) {
-    PERFETTO_DPLOG("read of %zu at offset %" PRIu64, size, addr);
+  if (PERFETTO_UNLIKELY(rd == -1)) {
+    PERFETTO_PLOG("Failed remote pread of %zu bytes at address %" PRIx64, size,
+                  addr);
     return 0;
   }
   return static_cast<size_t>(rd);
@@ -66,6 +67,7 @@
   if (!base::ReadFileDescriptor(*fd_, &content))
     return false;
 
+  unwindstack::SharedString name("");
   unwindstack::MapInfo* prev_map = nullptr;
   unwindstack::MapInfo* prev_real_map = nullptr;
   return android::procinfo::ReadMapFileContent(
@@ -76,9 +78,13 @@
             strncmp(mapinfo.name.c_str() + 5, "ashmem/", 7) != 0) {
           flags |= unwindstack::MAPS_FLAGS_DEVICE_MAP;
         }
+        // Share the string if it matches for consecutive maps.
+        if (name != mapinfo.name) {
+          name = unwindstack::SharedString(mapinfo.name);
+        }
         maps_.emplace_back(new unwindstack::MapInfo(
             prev_map, prev_real_map, mapinfo.start, mapinfo.end, mapinfo.pgoff,
-            flags, mapinfo.name));
+            flags, name));
         prev_map = maps_.back().get();
         if (!prev_map->IsBlank()) {
           prev_real_map = prev_map;
diff --git a/src/profiling/deobfuscator.cc b/src/profiling/deobfuscator.cc
index 2a7b9c0..ba58dfa 100644
--- a/src/profiling/deobfuscator.cc
+++ b/src/profiling/deobfuscator.cc
@@ -16,6 +16,7 @@
 
 #include "src/profiling/deobfuscator.h"
 
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/string_splitter.h"
@@ -157,37 +158,36 @@
 
 // See https://www.guardsquare.com/en/products/proguard/manual/retrace for the
 // file format we are parsing.
-bool ProguardParser::AddLine(std::string line) {
-  if (line.length() == 0)
-    return true;
+base::Status ProguardParser::AddLine(std::string line) {
+  if (line.length() == 0 || line[0] == '#')
+    return base::Status();
   bool is_member = line[0] == ' ';
   if (is_member && !current_class_) {
-    PERFETTO_ELOG("Failed to parse proguard map. Saw member before class.");
-    return false;
+    return base::Status(
+        "Failed to parse proguard map. Saw member before class.");
   }
   if (!is_member) {
     auto opt_cls = ParseClass(std::move(line));
     if (!opt_cls)
-      return false;
+      return base::Status("Class not found.");
     auto p = mapping_.emplace(std::move(opt_cls->obfuscated_name),
                               std::move(opt_cls->deobfuscated_name));
     if (!p.second) {
-      PERFETTO_ELOG("Duplicate class.");
-      return false;
+      return base::Status("Duplicate class.");
     }
     current_class_ = &p.first->second;
   } else {
     auto opt_member = ParseMember(std::move(line));
     if (!opt_member)
-      return false;
+      return base::Status("Failed to parse member.");
     switch (opt_member->type) {
       case (ProguardMemberType::kField): {
         if (!current_class_->AddField(opt_member->obfuscated_name,
                                       opt_member->deobfuscated_name)) {
-          PERFETTO_ELOG("Member redefinition: %s.%s. Proguard map invalid",
-                        current_class_->deobfuscated_name().c_str(),
-                        opt_member->deobfuscated_name.c_str());
-          return false;
+          return base::Status(std::string("Member redefinition: ") +
+                              current_class_->deobfuscated_name().c_str() +
+                              "." + opt_member->deobfuscated_name.c_str() +
+                              " Proguard map invalid");
         }
         break;
       }
@@ -198,13 +198,19 @@
       }
     }
   }
-  return true;
+  return base::Status();
 }
 
 bool ProguardParser::AddLines(std::string contents) {
+  size_t lineno = 1;
   for (base::StringSplitter lines(std::move(contents), '\n'); lines.Next();) {
-    if (!AddLine(lines.cur_token()))
+    auto status = AddLine(lines.cur_token());
+    if (!status.ok()) {
+      PERFETTO_ELOG("Failed to parse proguard map (line %zu): %s", lineno,
+                    status.c_message());
       return false;
+    }
+    lineno++;
   }
   return true;
 }
diff --git a/src/profiling/deobfuscator.h b/src/profiling/deobfuscator.h
index 061b5da..b96f292 100644
--- a/src/profiling/deobfuscator.h
+++ b/src/profiling/deobfuscator.h
@@ -22,6 +22,7 @@
 #include <string>
 #include <utility>
 #include <vector>
+#include "perfetto/base/status.h"
 
 namespace perfetto {
 namespace profiling {
@@ -59,7 +60,7 @@
   bool AddField(std::string obfuscated_name, std::string deobfuscated_name) {
     auto p = deobfuscated_fields_.emplace(std::move(obfuscated_name),
                                           deobfuscated_name);
-    return p.second && p.first->second == deobfuscated_name;
+    return p.second || p.first->second == deobfuscated_name;
   }
 
   void AddMethod(std::string obfuscated_name, std::string deobfuscated_name) {
@@ -90,7 +91,7 @@
  public:
   // A return value of false means this line failed to parse. This leaves the
   // parser in an undefined state and it should no longer be used.
-  bool AddLine(std::string line);
+  base::Status AddLine(std::string line);
   bool AddLines(std::string contents);
 
   std::map<std::string, ObfuscatedClass> ConsumeMapping() {
diff --git a/src/profiling/deobfuscator_unittest.cc b/src/profiling/deobfuscator_unittest.cc
index 66001dd..02a0e8d 100644
--- a/src/profiling/deobfuscator_unittest.cc
+++ b/src/profiling/deobfuscator_unittest.cc
@@ -37,8 +37,10 @@
 
 TEST(ProguardParserTest, ReadClass) {
   ProguardParser p;
-  ASSERT_TRUE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:"));
+  ASSERT_TRUE(
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
   ASSERT_THAT(p.ConsumeMapping(),
               ElementsAre(std::pair<std::string, ObfuscatedClass>(
                   "android.arch.a.a.a",
@@ -47,22 +49,28 @@
 
 TEST(ProguardParserTest, MissingColon) {
   ProguardParser p;
-  ASSERT_FALSE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a"));
+  ASSERT_FALSE(
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a")
+          .ok());
 }
 
 TEST(ProguardParserTest, UnexpectedMember) {
   ProguardParser p;
   ASSERT_FALSE(
-      p.AddLine("    android.arch.core.executor.TaskExecutor mDelegate -> b"));
+      p.AddLine("    android.arch.core.executor.TaskExecutor mDelegate -> b")
+          .ok());
 }
 
 TEST(ProguardParserTest, Member) {
   ProguardParser p;
-  ASSERT_TRUE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:"));
   ASSERT_TRUE(
-      p.AddLine("    android.arch.core.executor.TaskExecutor mDelegate -> b"));
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
+  ASSERT_TRUE(
+      p.AddLine("    android.arch.core.executor.TaskExecutor mDelegate -> b")
+          .ok());
   std::map<std::string, std::string> deobfuscated_fields{{"b", "mDelegate"}};
   ASSERT_THAT(
       p.ConsumeMapping(),
@@ -74,9 +82,11 @@
 
 TEST(ProguardParserTest, Method) {
   ProguardParser p;
-  ASSERT_TRUE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:"));
-  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b"));
+  ASSERT_TRUE(
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
+  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b").ok());
   auto mapping = p.ConsumeMapping();
   ASSERT_THAT(mapping, ElementsAre(Pair("android.arch.a.a.a", _)));
   EXPECT_THAT(
@@ -87,11 +97,13 @@
 
 TEST(ProguardParserTest, AmbiguousMethodSameCls) {
   ProguardParser p;
-  ASSERT_TRUE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:"));
-  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b"));
   ASSERT_TRUE(
-      p.AddLine("    15:15:boolean somethingDifferent(int):116:116 -> b"));
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
+  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b").ok());
+  ASSERT_TRUE(
+      p.AddLine("    15:15:boolean somethingDifferent(int):116:116 -> b").ok());
   auto mapping = p.ConsumeMapping();
   ASSERT_THAT(mapping, ElementsAre(Pair("android.arch.a.a.a", _)));
   EXPECT_THAT(
@@ -102,11 +114,14 @@
 
 TEST(ProguardParserTest, AmbiguousMethodDifferentCls) {
   ProguardParser p;
-  ASSERT_TRUE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:"));
-  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b"));
   ASSERT_TRUE(
-      p.AddLine("    15:15:boolean Foo.somethingDifferent(int):116:116 -> b"));
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
+  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b").ok());
+  ASSERT_TRUE(
+      p.AddLine("    15:15:boolean Foo.somethingDifferent(int):116:116 -> b")
+          .ok());
   auto mapping = p.ConsumeMapping();
   ASSERT_THAT(mapping, ElementsAre(Pair("android.arch.a.a.a", _)));
   EXPECT_THAT(mapping.find("android.arch.a.a.a")->second.deobfuscated_methods(),
@@ -118,12 +133,15 @@
 
 TEST(ProguardParserTest, AmbiguousMethodSameAndDifferentCls) {
   ProguardParser p;
-  ASSERT_TRUE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:"));
-  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b"));
-  ASSERT_TRUE(p.AddLine("    15:15:boolean what(String):116:116 -> b"));
   ASSERT_TRUE(
-      p.AddLine("    15:15:boolean Foo.somethingDifferent(int):116:116 -> b"));
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
+  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b").ok());
+  ASSERT_TRUE(p.AddLine("    15:15:boolean what(String):116:116 -> b").ok());
+  ASSERT_TRUE(
+      p.AddLine("    15:15:boolean Foo.somethingDifferent(int):116:116 -> b")
+          .ok());
   auto mapping = p.ConsumeMapping();
   ASSERT_THAT(mapping, ElementsAre(Pair("android.arch.a.a.a", _)));
   EXPECT_THAT(mapping.find("android.arch.a.a.a")->second.deobfuscated_methods(),
@@ -135,13 +153,17 @@
 
 TEST(ProguardParserTest, AmbiguousMethodSameAndDifferentCls2) {
   ProguardParser p;
-  ASSERT_TRUE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:"));
-  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b"));
-  ASSERT_TRUE(p.AddLine("    15:15:boolean what(String):116:116 -> b"));
   ASSERT_TRUE(
-      p.AddLine("    15:15:boolean Foo.somethingDifferent(int):116:116 -> b"));
-  ASSERT_TRUE(p.AddLine("    15:15:boolean Foo.third(int,int):116:116 -> b"));
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
+  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b").ok());
+  ASSERT_TRUE(p.AddLine("    15:15:boolean what(String):116:116 -> b").ok());
+  ASSERT_TRUE(
+      p.AddLine("    15:15:boolean Foo.somethingDifferent(int):116:116 -> b")
+          .ok());
+  ASSERT_TRUE(
+      p.AddLine("    15:15:boolean Foo.third(int,int):116:116 -> b").ok());
   auto mapping = p.ConsumeMapping();
   ASSERT_THAT(mapping, ElementsAre(Pair("android.arch.a.a.a", _)));
   EXPECT_THAT(mapping.find("android.arch.a.a.a")->second.deobfuscated_methods(),
@@ -153,28 +175,53 @@
 
 TEST(ProguardParserTest, DuplicateClass) {
   ProguardParser p;
-  ASSERT_TRUE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:"));
-  ASSERT_FALSE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor2 -> android.arch.a.a.a:"));
+  ASSERT_TRUE(
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
+  ASSERT_FALSE(p.AddLine("android.arch.core.executor.ArchTaskExecutor2 -> "
+                         "android.arch.a.a.a:")
+                   .ok());
 }
 
 TEST(ProguardParserTest, DuplicateField) {
   ProguardParser p;
-  ASSERT_TRUE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:"));
   ASSERT_TRUE(
-      p.AddLine("    android.arch.core.executor.TaskExecutor mDelegate -> b"));
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
+  ASSERT_TRUE(
+      p.AddLine("    android.arch.core.executor.TaskExecutor mDelegate -> b")
+          .ok());
   ASSERT_FALSE(
-      p.AddLine("    android.arch.core.executor.TaskExecutor mDelegate2 -> b"));
+      p.AddLine("    android.arch.core.executor.TaskExecutor mDelegate2 -> b")
+          .ok());
 }
 
 TEST(ProguardParserTest, DuplicateMethod) {
   ProguardParser p;
-  ASSERT_TRUE(p.AddLine(
-      "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:"));
-  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b"));
-  ASSERT_TRUE(p.AddLine("    15:15:boolean doSomething(boolean):116:116 -> b"));
+  ASSERT_TRUE(
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
+  ASSERT_TRUE(p.AddLine("    15:15:boolean isMainThread():116:116 -> b").ok());
+  ASSERT_TRUE(
+      p.AddLine("    15:15:boolean doSomething(boolean):116:116 -> b").ok());
+}
+
+TEST(ProguardParserTest, DuplicateFieldSame) {
+  ProguardParser p;
+  ASSERT_TRUE(
+      p.AddLine(
+           "android.arch.core.executor.ArchTaskExecutor -> android.arch.a.a.a:")
+          .ok());
+  ASSERT_TRUE(
+      p.AddLine("    android.arch.core.executor.TaskExecutor mDelegate -> b")
+          .ok());
+  ASSERT_TRUE(
+      p.AddLine(
+           "    1:1:android.arch.core.executor.TaskExecutor mDelegate -> b")
+          .ok());
 }
 
 }  // namespace
diff --git a/src/profiling/memory/BUILD.gn b/src/profiling/memory/BUILD.gn
index f0e350f..804f70e 100644
--- a/src/profiling/memory/BUILD.gn
+++ b/src/profiling/memory/BUILD.gn
@@ -21,6 +21,14 @@
 # The Android heap profiling daemon.
 executable("heapprofd") {
   deps = [
+    ":heapprofd_main",
+    "../../../gn:default_deps",
+  ]
+  sources = [ "main.cc" ]
+}
+
+source_set("heapprofd_main") {
+  deps = [
     "../../../gn:default_deps",
     "../../../protos/perfetto/trace:zero",
     "../../../src/base",
@@ -29,7 +37,10 @@
     "../../../src/profiling/memory:wire_protocol",
     "../../../src/tracing/ipc/producer",
   ]
-  sources = [ "main.cc" ]
+  sources = [
+    "heapprofd.cc",
+    "heapprofd.h",
+  ]
 }
 
 # This library gets loaded into (and executes in) arbitrary android processes.
diff --git a/src/profiling/memory/bookkeeping.h b/src/profiling/memory/bookkeeping.h
index 6aa9086..77a1319 100644
--- a/src/profiling/memory/bookkeeping.h
+++ b/src/profiling/memory/bookkeeping.h
@@ -193,8 +193,9 @@
 
   void ClearFrameCache() { frame_cache_.clear(); }
 
-  uint64_t committed_timestamp() { return committed_timestamp_; }
-  uint64_t max_timestamp() { return max_timestamp_; }
+  uint64_t dump_timestamp() {
+    return dump_at_max_mode_ ? max_timestamp_ : committed_timestamp_;
+  }
 
   uint64_t GetSizeForTesting(const std::vector<unwindstack::FrameData>& stack,
                              std::vector<std::string> build_ids);
diff --git a/src/profiling/memory/bookkeeping_unittest.cc b/src/profiling/memory/bookkeeping_unittest.cc
index 79e0183..4dc16ca 100644
--- a/src/profiling/memory/bookkeeping_unittest.cc
+++ b/src/profiling/memory/bookkeeping_unittest.cc
@@ -145,7 +145,7 @@
   sequence_number++;
   hd.RecordMalloc(stack2(), DummyBuildIds(stack2().size()), 0x2, 1, 2,
                   sequence_number, 100 * sequence_number);
-  ASSERT_EQ(hd.max_timestamp(), 200u);
+  ASSERT_EQ(hd.dump_timestamp(), 200u);
   ASSERT_EQ(hd.GetMaxForTesting(stack(), DummyBuildIds(stack().size())), 5u);
   ASSERT_EQ(hd.GetMaxForTesting(stack2(), DummyBuildIds(stack2().size())), 2u);
   ASSERT_EQ(hd.GetMaxCountForTesting(stack(), DummyBuildIds(stack().size())),
@@ -171,7 +171,7 @@
                   sequence_number, 100 * sequence_number);
   sequence_number++;
   hd.RecordFree(0x2, sequence_number, 100 * sequence_number);
-  EXPECT_EQ(hd.max_timestamp(), 400u);
+  EXPECT_EQ(hd.dump_timestamp(), 400u);
   EXPECT_EQ(hd.GetMaxForTesting(stack(), DummyBuildIds(stack().size())), 0u);
   EXPECT_EQ(hd.GetMaxForTesting(stack2(), DummyBuildIds(stack2().size())), 15u);
   EXPECT_EQ(hd.GetMaxForTesting(stack3(), DummyBuildIds(stack3().size())), 15u);
diff --git a/src/profiling/memory/client_api.cc b/src/profiling/memory/client_api.cc
index bdede0d..fc5cc12 100644
--- a/src/profiling/memory/client_api.cc
+++ b/src/profiling/memory/client_api.cc
@@ -113,8 +113,9 @@
 }
 
 constexpr auto kMinHeapId = 1;
+constexpr auto kMaxNumHeaps = 256;
 
-AHeapInfo g_heaps[256];
+AHeapInfo g_heaps[kMaxNumHeaps];
 
 AHeapInfo& GetHeap(uint32_t id) {
   return g_heaps[id];
@@ -133,32 +134,49 @@
 
 std::atomic<uint32_t> g_next_heap_id{kMinHeapId};
 
+// This can get called while holding the spinlock (in normal operation), or
+// without holding the spinlock (from OnSpinlockTimeout).
 void DisableAllHeaps() {
-  for (uint32_t i = kMinHeapId; i < g_next_heap_id.load(); ++i) {
+  bool disabled[kMaxNumHeaps] = {};
+  uint32_t max_heap = g_next_heap_id.load();
+  // This has to be done in two passes, in case the disabled_callback for one
+  // enabled heap uses another. In that case, the callbacks for the other heap
+  // would time out trying to acquire the spinlock, which we hold here.
+  for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
     AHeapInfo& info = GetHeap(i);
     if (!info.ready.load(std::memory_order_acquire))
       continue;
-    if (info.enabled.load(std::memory_order_acquire)) {
-      info.enabled.store(false, std::memory_order_release);
-      if (info.disabled_callback) {
-        AHeapProfileDisableCallbackInfo disable_info;
-        info.disabled_callback(info.disabled_callback_data, &disable_info);
-      }
+    disabled[i] = info.enabled.exchange(false, std::memory_order_acq_rel);
+  }
+  for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
+    if (!disabled[i]) {
+      continue;
+    }
+    AHeapInfo& info = GetHeap(i);
+    if (info.disabled_callback) {
+      AHeapProfileDisableCallbackInfo disable_info;
+      info.disabled_callback(info.disabled_callback_data, &disable_info);
     }
   }
 }
 
+#pragma GCC diagnostic push
+#if PERFETTO_DCHECK_IS_ON()
+#pragma GCC diagnostic ignored "-Wmissing-noreturn"
+#endif
+
 void OnSpinlockTimeout() {
   // Give up on profiling the process but leave it running.
   // The process enters into a poisoned state and will reject all
   // subsequent profiling requests.  The current session is kept
   // running but no samples are reported to it.
-  PERFETTO_ELOG(
+  PERFETTO_DFATAL_OR_ELOG(
       "Timed out on the spinlock - something is horribly wrong. "
       "Leaking heapprofd client.");
   DisableAllHeaps();
   perfetto::profiling::PoisonSpinlock(&g_client_lock);
 }
+#pragma GCC diagnostic pop
 
 // Note: g_client can be reset by AHeapProfile_initSession without calling this
 // function.
@@ -280,7 +298,7 @@
   }
 
   uint32_t next_id = g_next_heap_id.fetch_add(1);
-  if (next_id >= perfetto::base::ArraySize(g_heaps)) {
+  if (next_id >= kMaxNumHeaps) {
     return nullptr;
   }
 
@@ -522,11 +540,8 @@
     return false;
   }
 
-  uint64_t heap_intervals[perfetto::base::ArraySize(g_heaps)] = {};
   uint32_t max_heap = g_next_heap_id.load();
-  for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
-    heap_intervals[i] = MaybeToggleHeap(i, client.get());
-  }
+  bool heaps_enabled[kMaxNumHeaps] = {};
 
   PERFETTO_LOG("%s: heapprofd_client initialized.", getprogname());
   {
@@ -540,15 +555,36 @@
     // random engine.
     for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
       AHeapInfo& heap = GetHeap(i);
-      if (heap_intervals[i]) {
-        heap.sampler.SetSamplingInterval(heap_intervals[i]);
+      if (!heap.ready.load(std::memory_order_acquire)) {
+        continue;
+      }
+      const uint64_t interval =
+          GetHeapSamplingInterval(client->client_config(), heap.heap_name);
+      if (interval) {
+        heaps_enabled[i] = true;
+        heap.sampler.SetSamplingInterval(interval);
       }
     }
 
     // This cannot have been set in the meantime. There are never two concurrent
     // calls to this function, as Bionic uses atomics to guard against that.
     PERFETTO_DCHECK(*GetClientLocked() == nullptr);
-    *GetClientLocked() = std::move(client);
+    *GetClientLocked() = client;
   }
+
+  // We want to run MaybeToggleHeap last to make sure we never enable a heap
+  // but subsequently return `false` from this function, which indicates to the
+  // caller that we did not enable anything.
+  //
+  // For startup profiles, `false` is used by Bionic to signal it can unload
+  // the library again.
+  for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
+    if (!heaps_enabled[i]) {
+      continue;
+    }
+    auto interval = MaybeToggleHeap(i, client.get());
+    PERFETTO_DCHECK(interval > 0);
+  }
+
   return true;
 }
diff --git a/src/profiling/memory/client_api_factory_standalone.cc b/src/profiling/memory/client_api_factory_standalone.cc
index f21ce0d..da2a083 100644
--- a/src/profiling/memory/client_api_factory_standalone.cc
+++ b/src/profiling/memory/client_api_factory_standalone.cc
@@ -45,6 +45,8 @@
 //   service. This happens in CreateClient.
 
 namespace perfetto {
+void EnableStacktraceOnCrashForDebug();
+
 namespace profiling {
 namespace {
 
@@ -107,6 +109,11 @@
 
   daemon(/* nochdir= */ 0, /* noclose= */ 1);
 
+  // On debug builds, we want to turn on crash reporting for heapprofd.
+#if PERFETTO_BUILDFLAG(PERFETTO_STDERR_CRASH_DUMP)
+  EnableStacktraceOnCrashForDebug();
+#endif
+
   cli_sock.ReleaseFd();
 
   // Leave stderr open for logging.
@@ -149,6 +156,9 @@
         }
       });
   task_runner.Run();
+  // We currently do not Quit the task_runner, but if we ever do it will be
+  // very hard to debug if we don't exit here.
+  exit(0);
 }
 
 // This is called by AHeapProfile_initSession (client_api.cc) to construct a
diff --git a/src/profiling/memory/heapprofd.cc b/src/profiling/memory/heapprofd.cc
new file mode 100644
index 0000000..d3a8cae
--- /dev/null
+++ b/src/profiling/memory/heapprofd.cc
@@ -0,0 +1,130 @@
+/*
+ * 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/profiling/memory/heapprofd.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <array>
+#include <memory>
+#include <vector>
+
+#include <signal.h>
+
+#include "perfetto/ext/base/event_fd.h"
+#include "perfetto/ext/base/getopt.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/unix_socket.h"
+#include "perfetto/ext/base/watchdog.h"
+#include "perfetto/ext/tracing/ipc/default_socket.h"
+#include "src/profiling/memory/heapprofd_producer.h"
+#include "src/profiling/memory/java_hprof_producer.h"
+#include "src/profiling/memory/wire_protocol.h"
+
+#include "perfetto/ext/base/unix_task_runner.h"
+
+// TODO(rsavitski): the task runner watchdog spawns a thread (normally for
+// tracking cpu/mem usage) that we don't strictly need.
+
+namespace perfetto {
+namespace profiling {
+namespace {
+
+int StartCentralHeapprofd();
+
+int GetListeningSocket() {
+  const char* sock_fd = getenv(kHeapprofdSocketEnvVar);
+  if (sock_fd == nullptr)
+    PERFETTO_FATAL("Did not inherit socket from init.");
+  char* end;
+  int raw_fd = static_cast<int>(strtol(sock_fd, &end, 10));
+  if (*end != '\0')
+    PERFETTO_FATAL("Invalid %s. Expected decimal integer.",
+                   kHeapprofdSocketEnvVar);
+  return raw_fd;
+}
+
+base::EventFd* g_dump_evt = nullptr;
+
+int StartCentralHeapprofd() {
+  // We set this up before launching any threads, so we do not have to use a
+  // std::atomic for g_dump_evt.
+  g_dump_evt = new base::EventFd();
+
+  base::UnixTaskRunner task_runner;
+  base::Watchdog::GetInstance()->Start();  // crash on exceedingly long tasks
+  HeapprofdProducer producer(HeapprofdMode::kCentral, &task_runner,
+                             /* exit_when_done= */ false);
+
+  int listening_raw_socket = GetListeningSocket();
+  auto listening_socket = base::UnixSocket::Listen(
+      base::ScopedFile(listening_raw_socket), &producer.socket_delegate(),
+      &task_runner, base::SockFamily::kUnix, base::SockType::kStream);
+
+  struct sigaction action = {};
+  action.sa_handler = [](int) { g_dump_evt->Notify(); };
+  // Allow to trigger a full dump by sending SIGUSR1 to heapprofd.
+  // This will allow manually deciding when to dump on userdebug.
+  PERFETTO_CHECK(sigaction(SIGUSR1, &action, nullptr) == 0);
+  task_runner.AddFileDescriptorWatch(g_dump_evt->fd(), [&producer] {
+    g_dump_evt->Clear();
+    producer.DumpAll();
+  });
+  producer.ConnectWithRetries(GetProducerSocket());
+  // TODO(fmayer): Create one producer that manages both heapprofd and Java
+  // producers, so we do not have two connections to traced.
+  JavaHprofProducer java_producer(&task_runner);
+  java_producer.ConnectWithRetries(GetProducerSocket());
+  task_runner.Run();
+  return 0;
+}
+
+}  // namespace
+
+int HeapprofdMain(int argc, char** argv) {
+  bool cleanup_crash = false;
+
+  enum { kCleanupCrash = 256, kTargetPid, kTargetCmd, kInheritFd };
+  static option long_options[] = {
+      {"cleanup-after-crash", no_argument, nullptr, kCleanupCrash},
+      {nullptr, 0, nullptr, 0}};
+  int c;
+  while ((c = getopt_long(argc, argv, "", long_options, nullptr)) != -1) {
+    switch (c) {
+      case kCleanupCrash:
+        cleanup_crash = true;
+        break;
+    }
+  }
+
+  if (cleanup_crash) {
+    PERFETTO_LOG(
+        "Recovering from crash: unsetting heapprofd system properties. "
+        "Expect SELinux denials for unrelated properties.");
+    SystemProperties::ResetHeapprofdProperties();
+    PERFETTO_LOG(
+        "Finished unsetting heapprofd system properties. "
+        "SELinux denials about properties are unexpected after "
+        "this point.");
+    return 0;
+  }
+
+  // start as a central daemon.
+  return StartCentralHeapprofd();
+}
+
+}  // namespace profiling
+}  // namespace perfetto
diff --git a/src/profiling/memory/heapprofd.h b/src/profiling/memory/heapprofd.h
new file mode 100644
index 0000000..e405585
--- /dev/null
+++ b/src/profiling/memory/heapprofd.h
@@ -0,0 +1,28 @@
+/*
+ * 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_PROFILING_MEMORY_HEAPPROFD_H_
+#define SRC_PROFILING_MEMORY_HEAPPROFD_H_
+
+namespace perfetto {
+namespace profiling {
+
+int HeapprofdMain(int argc, char** argv);
+
+}  // namespace profiling
+}  // namespace perfetto
+
+#endif  // SRC_PROFILING_MEMORY_HEAPPROFD_H_
diff --git a/src/profiling/memory/heapprofd_end_to_end_test.cc b/src/profiling/memory/heapprofd_end_to_end_test.cc
index d406ace..525d865 100644
--- a/src/profiling/memory/heapprofd_end_to_end_test.cc
+++ b/src/profiling/memory/heapprofd_end_to_end_test.cc
@@ -19,6 +19,7 @@
 #include <vector>
 
 #include <fcntl.h>
+#include <stdint.h>
 #include <string.h>
 #include <sys/stat.h>
 #include <sys/types.h>
@@ -454,6 +455,7 @@
   static std::atomic<bool> disabled{false};
   static std::atomic<uint64_t> sampling_interval;
 
+  static uint32_t other_heap_id = 0;
   auto enabled_callback = [](void*,
                              const AHeapProfileEnableCallbackInfo* info) {
     sampling_interval =
@@ -461,6 +463,8 @@
     initialized = true;
   };
   auto disabled_callback = [](void*, const AHeapProfileDisableCallbackInfo*) {
+    PERFETTO_CHECK(other_heap_id);
+    AHeapProfile_reportFree(other_heap_id, 0);
     disabled = true;
   };
   static uint32_t heap_id =
@@ -469,6 +473,7 @@
                                        enabled_callback, nullptr),
           disabled_callback, nullptr));
 
+  other_heap_id = AHeapProfile_registerHeap(AHeapInfo_create("othertest"));
   ChildFinishHandshake();
 
   // heapprofd_client needs malloc to see the signal.
@@ -1136,6 +1141,7 @@
     cfg->set_sampling_interval_bytes(1000000);
     cfg->add_pid(pid);
     cfg->add_heaps("test");
+    cfg->add_heaps("othertest");
   });
 
   auto helper = Trace(trace_config);
diff --git a/src/profiling/memory/heapprofd_producer.cc b/src/profiling/memory/heapprofd_producer.cc
index fa1527d..6a47344 100644
--- a/src/profiling/memory/heapprofd_producer.cc
+++ b/src/profiling/memory/heapprofd_producer.cc
@@ -39,6 +39,7 @@
 #include "perfetto/ext/tracing/ipc/producer_ipc_client.h"
 #include "perfetto/tracing/core/data_source_config.h"
 #include "perfetto/tracing/core/data_source_descriptor.h"
+#include "perfetto/tracing/core/forward_decls.h"
 #include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
 #include "src/profiling/common/producer_support.h"
 #include "src/profiling/common/profiler_guardrails.h"
@@ -62,8 +63,6 @@
 constexpr uint32_t kMaxConnectionBackoffMs = 30 * 1000;
 constexpr uint32_t kGuardrailIntervalMs = 30 * 1000;
 
-constexpr uint32_t kChildModeWatchdogPeriodMs = 10 * 1000;
-
 constexpr uint64_t kDefaultShmemSize = 8 * 1048576;  // ~8 MB
 constexpr uint64_t kMaxShmemSize = 500 * 1048576;    // ~500 MB
 
@@ -148,10 +147,13 @@
   cli_config->adaptive_sampling_max_sampling_interval_bytes =
       heapprofd_config.adaptive_sampling_max_sampling_interval_bytes();
   size_t n = 0;
+  const std::vector<std::string>& exclude_heaps = heapprofd_config.exclude_heaps();
+  // heaps[i] and heaps_interval[i] represent that the heap named in heaps[i]
+  // should be sampled with sampling interval of heap_interval[i].
   std::vector<std::string> heaps = heapprofd_config.heaps();
   std::vector<uint64_t> heap_intervals =
       heapprofd_config.heap_sampling_intervals();
-  if (heaps.empty()) {
+  if (heaps.empty() && !cli_config->all_heaps) {
     heaps.push_back("libc.malloc");
   }
 
@@ -168,6 +170,15 @@
     PERFETTO_ELOG("zero sampling interval.");
     return false;
   }
+  if (!exclude_heaps.empty()) {
+    // For disabled heaps, we add explicit entries but with sampling interval
+    // 0. The consumer of the sampling intervals in ClientConfiguration,
+    // GetSamplingInterval in wire_protocol.h, uses 0 to signal a heap is
+    // disabled, either because it isn't enabled (all_heaps is not set, and the
+    // heap isn't named), or because we explicitely set it here.
+    heaps.insert(heaps.end(), exclude_heaps.cbegin(), exclude_heaps.cend());
+    heap_intervals.insert(heap_intervals.end(), exclude_heaps.size(), 0u);
+  }
   if (heaps.size() > base::ArraySize(cli_config->heaps)) {
     heaps.resize(base::ArraySize(cli_config->heaps));
     PERFETTO_ELOG("Too many heaps requested. Truncating.");
@@ -322,29 +333,6 @@
   ConnectWithRetries(socket_name);
 }
 
-void HeapprofdProducer::ActiveDataSourceWatchdogCheck() {
-  PERFETTO_DCHECK(mode_ == HeapprofdMode::kChild);
-
-  // Fork mode heapprofd should be working on exactly one data source matching
-  // its target process.
-  if (data_sources_.empty()) {
-    PERFETTO_LOG(
-        "Child heapprofd exiting as it never received a data source for the "
-        "target process, or somehow lost/finished the task without exiting.");
-    TerminateProcess(/*exit_status=*/1);
-  } else {
-    // reschedule check.
-    auto weak_producer = weak_factory_.GetWeakPtr();
-    task_runner_->PostDelayedTask(
-        [weak_producer]() {
-          if (!weak_producer)
-            return;
-          weak_producer->ActiveDataSourceWatchdogCheck();
-        },
-        kChildModeWatchdogPeriodMs);
-  }
-}
-
 // TODO(rsavitski): would be cleaner to shut down the event loop instead
 // (letting main exit). One test-friendly approach is to supply a shutdown
 // callback in the constructor.
@@ -373,7 +361,12 @@
 
 void HeapprofdProducer::SetupDataSource(DataSourceInstanceID id,
                                         const DataSourceConfig& ds_config) {
-  PERFETTO_DLOG("Setting up data source.");
+  if (ds_config.session_initiator() ==
+      DataSourceConfig::SESSION_INITIATOR_TRUSTED_SYSTEM) {
+    PERFETTO_LOG("Setting up datasource: statsd initiator.");
+  } else {
+    PERFETTO_LOG("Setting up datasource: non-statsd initiator.");
+  }
   if (mode_ == HeapprofdMode::kChild && ds_config.enable_extra_guardrails()) {
     PERFETTO_ELOG("enable_extra_guardrails is not supported on user.");
     return;
@@ -580,7 +573,7 @@
     return;
   }
 
-  PERFETTO_DLOG("Stopping data source %" PRIu64, id);
+  PERFETTO_LOG("Stopping data source %" PRIu64, id);
 
   DataSource& data_source = it->second;
   data_source.was_stopped = true;
@@ -681,39 +674,26 @@
 
     bool from_startup = data_source->signaled_pids.find(pid) ==
                         data_source->signaled_pids.cend();
-    uint64_t dump_timestamp;
-    if (data_source->config.dump_at_max())
-      dump_timestamp = heap_info.heap_tracker.max_timestamp();
-    else
-      dump_timestamp = heap_info.heap_tracker.committed_timestamp();
 
-    const char* heap_name = nullptr;
-    if (!heap_info.heap_name.empty())
-      heap_name = heap_info.heap_name.c_str();
-    uint64_t sampling_interval = heap_info.sampling_interval;
-    uint64_t orig_sampling_interval = heap_info.orig_sampling_interval;
-
-    auto new_heapsamples =
-        [pid, from_startup, dump_timestamp, process_state, data_source,
-         heap_name, sampling_interval,
-         orig_sampling_interval](ProfilePacket::ProcessHeapSamples* proto) {
-          proto->set_pid(static_cast<uint64_t>(pid));
-          proto->set_timestamp(dump_timestamp);
-          proto->set_from_startup(from_startup);
-          proto->set_disconnected(process_state->disconnected);
-          proto->set_buffer_overran(process_state->error_state ==
-                                    SharedRingBuffer::kHitTimeout);
-          proto->set_client_error(
-              ErrorStateToProto(process_state->error_state));
-          proto->set_buffer_corrupted(process_state->buffer_corrupted);
-          proto->set_hit_guardrail(data_source->hit_guardrail);
-          if (heap_name)
-            proto->set_heap_name(heap_name);
-          proto->set_sampling_interval_bytes(sampling_interval);
-          proto->set_orig_sampling_interval_bytes(orig_sampling_interval);
-          auto* stats = proto->set_stats();
-          SetStats(stats, *process_state);
-        };
+    auto new_heapsamples = [pid, from_startup, process_state, data_source,
+                            &heap_info](
+                               ProfilePacket::ProcessHeapSamples* proto) {
+      proto->set_pid(static_cast<uint64_t>(pid));
+      proto->set_timestamp(heap_info.heap_tracker.dump_timestamp());
+      proto->set_from_startup(from_startup);
+      proto->set_disconnected(process_state->disconnected);
+      proto->set_buffer_overran(process_state->error_state ==
+                                SharedRingBuffer::kHitTimeout);
+      proto->set_client_error(ErrorStateToProto(process_state->error_state));
+      proto->set_buffer_corrupted(process_state->buffer_corrupted);
+      proto->set_hit_guardrail(data_source->hit_guardrail);
+      if (!heap_info.heap_name.empty())
+        proto->set_heap_name(heap_info.heap_name.c_str());
+      proto->set_sampling_interval_bytes(heap_info.sampling_interval);
+      proto->set_orig_sampling_interval_bytes(heap_info.orig_sampling_interval);
+      auto* stats = proto->set_stats();
+      SetStats(stats, *process_state);
+    };
 
     DumpState dump_state(data_source->trace_writer.get(),
                          std::move(new_heapsamples),
@@ -930,7 +910,8 @@
   // In fork mode, right now we check whether the target is not profileable
   // in the client, because we cannot read packages.list there.
   if (mode_ == HeapprofdMode::kCentral &&
-      !CanProfile(data_source->ds_config, new_connection->peer_uid_posix())) {
+      !CanProfile(data_source->ds_config, new_connection->peer_uid_posix(),
+                  data_source->config.target_installed_by())) {
     PERFETTO_ELOG("%d (%s) is not profileable.", process.pid,
                   process.cmdline.c_str());
     return;
@@ -1230,6 +1211,8 @@
     DataSource& ds = p.second;
     if (gr.IsOverCpuThreshold(ds.guardrail_config)) {
       ds.hit_guardrail = true;
+      PERFETTO_LOG("Data source %" PRIu64 " hit CPU guardrail. Shutting down.",
+                   ds.id);
       ShutdownDataSource(&ds);
     }
   }
@@ -1249,6 +1232,9 @@
     DataSource& ds = p.second;
     if (gr.IsOverMemoryThreshold(ds.guardrail_config)) {
       ds.hit_guardrail = true;
+      PERFETTO_LOG("Data source %" PRIu64
+                   " hit memory guardrail. Shutting down.",
+                   ds.id);
       ShutdownDataSource(&ds);
     }
   }
diff --git a/src/profiling/memory/heapprofd_producer.h b/src/profiling/memory/heapprofd_producer.h
index be6ae64..51cc0c7 100644
--- a/src/profiling/memory/heapprofd_producer.h
+++ b/src/profiling/memory/heapprofd_producer.h
@@ -186,7 +186,8 @@
     ProcessState(GlobalCallstackTrie* c, bool d)
         : callsites(c), dump_at_max_mode(d) {}
     bool disconnected = false;
-    SharedRingBuffer::ErrorState error_state;
+    SharedRingBuffer::ErrorState error_state =
+        SharedRingBuffer::ErrorState::kNoError;
     bool buffer_corrupted = false;
 
     uint64_t heap_samples = 0;
@@ -276,8 +277,6 @@
 
   // Specific to mode_ == kChild
   void TerminateProcess(int exit_status);
-  // Specific to mode_ == kChild
-  void ActiveDataSourceWatchdogCheck();
 
   void ShutdownDataSource(DataSource* ds);
   bool MaybeFinishDataSource(DataSource* ds);
diff --git a/src/profiling/memory/heapprofd_producer_unittest.cc b/src/profiling/memory/heapprofd_producer_unittest.cc
index 8a75145..fa08e95 100644
--- a/src/profiling/memory/heapprofd_producer_unittest.cc
+++ b/src/profiling/memory/heapprofd_producer_unittest.cc
@@ -202,5 +202,42 @@
             4 * 4096u);
 }
 
+TEST(HeapprofdConfigToClientConfigurationTest, AllHeaps) {
+  HeapprofdConfig cfg;
+  cfg.set_all_heaps(true);
+  cfg.set_sampling_interval_bytes(4096);
+  ClientConfiguration cli_config;
+  ASSERT_TRUE(HeapprofdConfigToClientConfiguration(cfg, &cli_config));
+  EXPECT_EQ(cli_config.num_heaps, 0u);
+  EXPECT_EQ(cli_config.default_interval, 4096u);
+}
+
+TEST(HeapprofdConfigToClientConfigurationTest, AllHeapsAndExplicit) {
+  HeapprofdConfig cfg;
+  cfg.set_all_heaps(true);
+  cfg.set_sampling_interval_bytes(4096);
+  cfg.add_heaps("foo");
+  cfg.add_heap_sampling_intervals(1024u);
+  ClientConfiguration cli_config;
+  ASSERT_TRUE(HeapprofdConfigToClientConfiguration(cfg, &cli_config));
+  EXPECT_EQ(cli_config.num_heaps, 1u);
+  EXPECT_STREQ(cli_config.heaps[0].name, "foo");
+  EXPECT_EQ(cli_config.heaps[0].interval, 1024u);
+  EXPECT_EQ(cli_config.default_interval, 4096u);
+}
+
+TEST(HeapprofdConfigToClientConfigurationTest, AllHeapsAndDisabled) {
+  HeapprofdConfig cfg;
+  cfg.set_all_heaps(true);
+  cfg.set_sampling_interval_bytes(4096);
+  cfg.add_exclude_heaps("foo");
+  ClientConfiguration cli_config;
+  ASSERT_TRUE(HeapprofdConfigToClientConfiguration(cfg, &cli_config));
+  EXPECT_EQ(cli_config.num_heaps, 1u);
+  EXPECT_STREQ(cli_config.heaps[0].name, "foo");
+  EXPECT_EQ(cli_config.heaps[0].interval, 0u);
+  EXPECT_EQ(cli_config.default_interval, 4096u);
+}
+
 }  // namespace profiling
 }  // namespace perfetto
diff --git a/src/profiling/memory/include/perfetto/heap_profile.h b/src/profiling/memory/include/perfetto/heap_profile.h
index 9d77db3..33e797b 100644
--- a/src/profiling/memory/include/perfetto/heap_profile.h
+++ b/src/profiling/memory/include/perfetto/heap_profile.h
@@ -82,7 +82,15 @@
 typedef struct AHeapProfileEnableCallbackInfo AHeapProfileEnableCallbackInfo;
 typedef struct AHeapProfileDisableCallbackInfo AHeapProfileDisableCallbackInfo;
 
-// Get sampling interval of the profiling session that was started.
+typedef void (*_Nonnull AHeapInfo_EnableCallback)(
+    void* _Nullable data,
+    const AHeapProfileEnableCallbackInfo* _Nonnull session_info);
+
+typedef void (*_Nonnull AHeapInfo_DisableCallback)(
+    void* _Nullable data,
+    const AHeapProfileDisableCallbackInfo* _Nonnull session_info);
+
+// Get sampling interval (in bytes) of the profiling session that was started.
 uint64_t AHeapProfileEnableCallbackInfo_getSamplingInterval(
     const AHeapProfileEnableCallbackInfo* _Nonnull session_info);
 
@@ -109,9 +117,7 @@
 // this callback is called when profiling of the heap is requested.
 AHeapInfo* _Nullable AHeapInfo_setEnabledCallback(
     AHeapInfo* _Nullable info,
-    void (*_Nonnull callback)(
-        void* _Nullable,
-        const AHeapProfileEnableCallbackInfo* _Nonnull session_info),
+    AHeapInfo_EnableCallback callback,
     void* _Nullable data);
 
 // Set disabled callback in AHeapInfo.
@@ -122,9 +128,7 @@
 // this callback is called when profiling of the heap ends.
 AHeapInfo* _Nullable AHeapInfo_setDisabledCallback(
     AHeapInfo* _Nullable info,
-    void (*_Nonnull callback)(
-        void* _Nullable,
-        const AHeapProfileDisableCallbackInfo* _Nonnull session_info),
+    AHeapInfo_DisableCallback callback,
     void* _Nullable data);
 
 // Register heap described in AHeapInfo.
@@ -139,6 +143,10 @@
 
 // Reports an allocation of |size| on the given |heap_id|.
 //
+// The |alloc_id| needs to be a unique identifier for the allocation, and can
+// can be used in AHeapProfile_reportFree to report the allocation has been
+// freed.
+//
 // If a profiling session is active, this function decides whether the reported
 // allocation should be sampled. If the allocation is sampled, it will be
 // associated to the current callstack in the profile.
diff --git a/src/profiling/memory/java_hprof_producer.cc b/src/profiling/memory/java_hprof_producer.cc
index 65bab10..aef9b5b 100644
--- a/src/profiling/memory/java_hprof_producer.cc
+++ b/src/profiling/memory/java_hprof_producer.cc
@@ -22,6 +22,7 @@
 #include "perfetto/ext/base/optional.h"
 #include "perfetto/ext/tracing/core/trace_writer.h"
 #include "src/profiling/common/proc_utils.h"
+#include "src/profiling/common/producer_support.h"
 
 namespace perfetto {
 namespace profiling {
@@ -55,10 +56,30 @@
 void JavaHprofProducer::SignalDataSource(const DataSource& ds) {
   const std::set<pid_t>& pids = ds.pids;
   for (pid_t pid : pids) {
+    auto opt_status = ReadStatus(pid);
+    if (!opt_status) {
+      PERFETTO_PLOG("Failed to read /proc/%d/status. Not signalling.", pid);
+      continue;
+    }
+    auto uids = GetUids(*opt_status);
+    if (!uids) {
+      PERFETTO_ELOG(
+          "Failed to read Uid from /proc/%d/status. "
+          "Not signalling.",
+          pid);
+      continue;
+    }
+    if (!CanProfile(ds.ds_config, uids->effective,
+                    ds.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.tracing_session_id % std::numeric_limits<int32_t>::max());
+    signal_value.sival_int =
+        static_cast<int32_t>(ds.ds_config.tracing_session_id() %
+                             std::numeric_limits<int32_t>::max());
     if (sigqueue(pid, kJavaHeapprofdSignal, signal_value) != 0) {
       PERFETTO_DPLOG("sigqueue");
     }
@@ -98,7 +119,7 @@
     RemoveUnderAnonThreshold(config.min_anonymous_memory_kb(), &ds.pids);
 
   ds.config = std::move(config);
-  ds.tracing_session_id = ds_config.tracing_session_id();
+  ds.ds_config = std::move(ds_config);
   data_sources_.emplace(id, std::move(ds));
 }
 
diff --git a/src/profiling/memory/java_hprof_producer.h b/src/profiling/memory/java_hprof_producer.h
index 2c3aa07..97ac07d 100644
--- a/src/profiling/memory/java_hprof_producer.h
+++ b/src/profiling/memory/java_hprof_producer.h
@@ -30,6 +30,7 @@
 #include "perfetto/tracing/core/data_source_config.h"
 #include "perfetto/tracing/core/data_source_descriptor.h"
 
+#include "perfetto/tracing/core/forward_decls.h"
 #include "protos/perfetto/config/profiling/java_hprof_config.gen.h"
 
 namespace perfetto {
@@ -69,10 +70,10 @@
   };
 
   struct DataSource {
-    TracingSessionID tracing_session_id;
     DataSourceInstanceID id;
     std::set<pid_t> pids;
     JavaHprofConfig config;
+    DataSourceConfig ds_config;
   };
 
   void ConnectService();
diff --git a/src/profiling/memory/main.cc b/src/profiling/memory/main.cc
index f0bb47b..078ca86 100644
--- a/src/profiling/memory/main.cc
+++ b/src/profiling/memory/main.cc
@@ -14,117 +14,7 @@
  * limitations under the License.
  */
 
-#include <stdlib.h>
-#include <unistd.h>
-#include <array>
-#include <memory>
-#include <vector>
-
-#include <signal.h>
-
-#include "perfetto/ext/base/event_fd.h"
-#include "perfetto/ext/base/getopt.h"
-#include "perfetto/ext/base/scoped_file.h"
-#include "perfetto/ext/base/unix_socket.h"
-#include "perfetto/ext/base/watchdog.h"
-#include "perfetto/ext/tracing/ipc/default_socket.h"
-#include "src/profiling/memory/heapprofd_producer.h"
-#include "src/profiling/memory/java_hprof_producer.h"
-#include "src/profiling/memory/wire_protocol.h"
-
-#include "perfetto/ext/base/unix_task_runner.h"
-
-// TODO(rsavitski): the task runner watchdog spawns a thread (normally for
-// tracking cpu/mem usage) that we don't strictly need.
-
-namespace perfetto {
-namespace profiling {
-namespace {
-
-int StartCentralHeapprofd();
-
-int GetListeningSocket() {
-  const char* sock_fd = getenv(kHeapprofdSocketEnvVar);
-  if (sock_fd == nullptr)
-    PERFETTO_FATAL("Did not inherit socket from init.");
-  char* end;
-  int raw_fd = static_cast<int>(strtol(sock_fd, &end, 10));
-  if (*end != '\0')
-    PERFETTO_FATAL("Invalid %s. Expected decimal integer.",
-                   kHeapprofdSocketEnvVar);
-  return raw_fd;
-}
-
-base::EventFd* g_dump_evt = nullptr;
-
-int HeapprofdMain(int argc, char** argv) {
-  bool cleanup_crash = false;
-
-  enum { kCleanupCrash = 256, kTargetPid, kTargetCmd, kInheritFd };
-  static option long_options[] = {
-      {"cleanup-after-crash", no_argument, nullptr, kCleanupCrash},
-      {nullptr, 0, nullptr, 0}};
-  int c;
-  while ((c = getopt_long(argc, argv, "", long_options, nullptr)) != -1) {
-    switch (c) {
-      case kCleanupCrash:
-        cleanup_crash = true;
-        break;
-    }
-  }
-
-  if (cleanup_crash) {
-    PERFETTO_LOG(
-        "Recovering from crash: unsetting heapprofd system properties. "
-        "Expect SELinux denials for unrelated properties.");
-    SystemProperties::ResetHeapprofdProperties();
-    PERFETTO_LOG(
-        "Finished unsetting heapprofd system properties. "
-        "SELinux denials about properties are unexpected after "
-        "this point.");
-    return 0;
-  }
-
-  // start as a central daemon.
-  return StartCentralHeapprofd();
-}
-
-int StartCentralHeapprofd() {
-  // We set this up before launching any threads, so we do not have to use a
-  // std::atomic for g_dump_evt.
-  g_dump_evt = new base::EventFd();
-
-  base::UnixTaskRunner task_runner;
-  base::Watchdog::GetInstance()->Start();  // crash on exceedingly long tasks
-  HeapprofdProducer producer(HeapprofdMode::kCentral, &task_runner,
-                             /* exit_when_done= */ false);
-
-  int listening_raw_socket = GetListeningSocket();
-  auto listening_socket = base::UnixSocket::Listen(
-      base::ScopedFile(listening_raw_socket), &producer.socket_delegate(),
-      &task_runner, base::SockFamily::kUnix, base::SockType::kStream);
-
-  struct sigaction action = {};
-  action.sa_handler = [](int) { g_dump_evt->Notify(); };
-  // Allow to trigger a full dump by sending SIGUSR1 to heapprofd.
-  // This will allow manually deciding when to dump on userdebug.
-  PERFETTO_CHECK(sigaction(SIGUSR1, &action, nullptr) == 0);
-  task_runner.AddFileDescriptorWatch(g_dump_evt->fd(), [&producer] {
-    g_dump_evt->Clear();
-    producer.DumpAll();
-  });
-  producer.ConnectWithRetries(GetProducerSocket());
-  // TODO(fmayer): Create one producer that manages both heapprofd and Java
-  // producers, so we do not have two connections to traced.
-  JavaHprofProducer java_producer(&task_runner);
-  java_producer.ConnectWithRetries(GetProducerSocket());
-  task_runner.Run();
-  return 0;
-}
-
-}  // namespace
-}  // namespace profiling
-}  // namespace perfetto
+#include "src/profiling/memory/heapprofd.h"
 
 int main(int argc, char** argv) {
   return perfetto::profiling::HeapprofdMain(argc, argv);
diff --git a/src/profiling/memory/sampler.cc b/src/profiling/memory/sampler.cc
index a5e59db..9a05fc3 100644
--- a/src/profiling/memory/sampler.cc
+++ b/src/profiling/memory/sampler.cc
@@ -19,33 +19,10 @@
 namespace perfetto {
 namespace profiling {
 
-namespace {
-
-// If the probability of getting less than one sample is less than this,
-// sidestep the sampler and treat the allocation as a sample.
-constexpr double kPassthroughError = 0.01;
-
-}  // namespace
-
-uint64_t GetPassthroughThreshold(uint64_t interval) {
-  if (interval <= 1)
-    return interval;
-  // (1 - 1 / interval)^x = kPassthroughError
-  // x = log_(1 - 1/interval)(kPassthroughError)
-  return 1 + uint64_t(log(kPassthroughError) / log(1.0 - 1 / double(interval)));
-}
-
 std::default_random_engine& GetGlobalRandomEngineLocked() {
   static std::default_random_engine engine;
   return engine;
 }
 
-void Sampler::SetSamplingInterval(uint64_t sampling_interval) {
-  sampling_interval_ = sampling_interval;
-  passthrough_threshold_ = GetPassthroughThreshold(sampling_interval_);
-  sampling_rate_ = 1.0 / static_cast<double>(sampling_interval_);
-  interval_to_next_sample_ = NextSampleInterval();
-}
-
 }  // namespace profiling
 }  // namespace perfetto
diff --git a/src/profiling/memory/sampler.h b/src/profiling/memory/sampler.h
index d462190..40e429f 100644
--- a/src/profiling/memory/sampler.h
+++ b/src/profiling/memory/sampler.h
@@ -29,8 +29,6 @@
 
 constexpr uint64_t kSamplerSeed = 1;
 
-uint64_t GetPassthroughThreshold(uint64_t interval);
-
 std::default_random_engine& GetGlobalRandomEngineLocked();
 
 // Poisson sampler for memory allocations. We apply sampling individually to
@@ -44,7 +42,11 @@
 // NB: not thread-safe, requires external synchronization.
 class Sampler {
  public:
-  void SetSamplingInterval(uint64_t sampling_interval);
+  void SetSamplingInterval(uint64_t sampling_interval) {
+    sampling_interval_ = sampling_interval;
+    sampling_rate_ = 1.0 / static_cast<double>(sampling_interval_);
+    interval_to_next_sample_ = NextSampleInterval();
+  }
 
   // Returns number of bytes that should be be attributed to the sample.
   // If returned size is 0, the allocation should not be sampled.
@@ -52,7 +54,7 @@
   // Due to how the poission sampling works, some samples should be accounted
   // multiple times.
   size_t SampleSize(size_t alloc_sz) {
-    if (PERFETTO_UNLIKELY(alloc_sz >= passthrough_threshold_))
+    if (PERFETTO_UNLIKELY(alloc_sz >= sampling_interval_))
       return alloc_sz;
     return static_cast<size_t>(sampling_interval_ * NumberOfSamples(alloc_sz));
   }
@@ -83,7 +85,6 @@
   }
 
   uint64_t sampling_interval_;
-  uint64_t passthrough_threshold_;
   double sampling_rate_;
   int64_t interval_to_next_sample_;
 };
diff --git a/src/profiling/memory/sampler_unittest.cc b/src/profiling/memory/sampler_unittest.cc
index b3eca7b..409fa4c 100644
--- a/src/profiling/memory/sampler_unittest.cc
+++ b/src/profiling/memory/sampler_unittest.cc
@@ -27,8 +27,8 @@
 TEST(SamplerTest, TestLarge) {
   GetGlobalRandomEngineLocked().seed(1);
   Sampler sampler;
-  sampler.SetSamplingInterval(32768);
-  EXPECT_EQ(sampler.SampleSize(160000u), 160000u);
+  sampler.SetSamplingInterval(512);
+  EXPECT_EQ(sampler.SampleSize(1024), 1024u);
 }
 
 TEST(SamplerTest, TestSmall) {
@@ -47,12 +47,6 @@
   EXPECT_EQ(sampler.SampleSize(5), 5u);
 }
 
-TEST(SamplerTest, TestGetPassthroughThreshold) {
-  EXPECT_EQ(GetPassthroughThreshold(32768u), 150900u);
-  EXPECT_EQ(GetPassthroughThreshold(1u), 1u);
-  EXPECT_EQ(GetPassthroughThreshold(2u), 7u);
-}
-
 }  // namespace
 }  // namespace profiling
 }  // namespace perfetto
diff --git a/src/profiling/memory/scoped_spinlock.h b/src/profiling/memory/scoped_spinlock.h
index aeda611..90aaf71 100644
--- a/src/profiling/memory/scoped_spinlock.h
+++ b/src/profiling/memory/scoped_spinlock.h
@@ -29,8 +29,8 @@
 namespace profiling {
 
 struct Spinlock {
-  std::atomic<bool> locked;
-  std::atomic<bool> poisoned;
+  std::atomic<uint8_t> locked;
+  std::atomic<uint8_t> poisoned;
 };
 
 static_assert(sizeof(Spinlock) == 2, "spinlock size must be ABI independent");
diff --git a/src/profiling/memory/shared_ring_buffer_fuzzer.cc b/src/profiling/memory/shared_ring_buffer_fuzzer.cc
index d3c4bd5..2a597a9 100644
--- a/src/profiling/memory/shared_ring_buffer_fuzzer.cc
+++ b/src/profiling/memory/shared_ring_buffer_fuzzer.cc
@@ -65,6 +65,7 @@
   SharedRingBuffer::MetadataPage header = {};
   memcpy(&header, data, sizeof(header));
   header.spinlock.locked = false;
+  header.spinlock.poisoned = false;
 
   PERFETTO_CHECK(ftruncate(*fd, static_cast<off_t>(total_size_pages *
                                                    base::kPageSize)) == 0);
diff --git a/src/profiling/memory/shared_ring_buffer_write_fuzzer.cc b/src/profiling/memory/shared_ring_buffer_write_fuzzer.cc
index 486033e..62b82eb 100644
--- a/src/profiling/memory/shared_ring_buffer_write_fuzzer.cc
+++ b/src/profiling/memory/shared_ring_buffer_write_fuzzer.cc
@@ -70,6 +70,7 @@
   memcpy(&header, data, sizeof(header));
   SharedRingBuffer::MetadataPage& metadata_page = header.metadata_page;
   metadata_page.spinlock.locked = false;
+  metadata_page.spinlock.poisoned = false;
 
   PERFETTO_CHECK(ftruncate(*fd, static_cast<off_t>(total_size_pages *
                                                    base::kPageSize)) == 0);
diff --git a/src/profiling/memory/unwinding.cc b/src/profiling/memory/unwinding.cc
index 5021949..12a2a02 100644
--- a/src/profiling/memory/unwinding.cc
+++ b/src/profiling/memory/unwinding.cc
@@ -228,14 +228,14 @@
   DataSourceInstanceID ds_id = client_data.data_source_instance_id;
 
   client_data_.erase(it);
+  // The erase invalidates the self pointer.
+  self = nullptr;
   if (client_data_.empty()) {
     // We got rid of the last client. Flush and destruct AllocRecords in
     // arena. Disable the arena (will not accept returning borrowed records)
     // in case there are pending AllocRecords on the main thread.
     alloc_record_arena_.Disable();
   }
-  // The erase invalidates the self pointer.
-  self = nullptr;
   delegate_->PostSocketDisconnected(this, ds_id, peer_pid, stats);
 }
 
diff --git a/src/profiling/memory/unwinding_unittest.cc b/src/profiling/memory/unwinding_unittest.cc
index 1684310..40cfc08 100644
--- a/src/profiling/memory/unwinding_unittest.cc
+++ b/src/profiling/memory/unwinding_unittest.cc
@@ -67,7 +67,7 @@
   unwindstack::MapInfo* map_info =
       maps.Find(reinterpret_cast<uint64_t>(&proc_maps));
   ASSERT_NE(map_info, nullptr);
-  ASSERT_EQ(map_info->name, "[stack]");
+  ASSERT_EQ(map_info->name(), "[stack]");
 }
 
 void __attribute__((noinline)) AssertFunctionOffset() {
diff --git a/src/profiling/memory/wire_protocol.cc b/src/profiling/memory/wire_protocol.cc
index 29ea042..9d30beb 100644
--- a/src/profiling/memory/wire_protocol.cc
+++ b/src/profiling/memory/wire_protocol.cc
@@ -58,6 +58,10 @@
 
 template <typename F>
 int64_t WithBuffer(SharedRingBuffer* shmem, size_t total_size, F fn) {
+  if (total_size > shmem->size()) {
+    errno = EMSGSIZE;
+    return -1;
+  }
   SharedRingBuffer::Buffer buf;
   {
     ScopedSpinlock lock = shmem->AcquireLock(ScopedSpinlock::Mode::Try);
diff --git a/src/profiling/memory/wire_protocol_unittest.cc b/src/profiling/memory/wire_protocol_unittest.cc
index 69df58f..873d977 100644
--- a/src/profiling/memory/wire_protocol_unittest.cc
+++ b/src/profiling/memory/wire_protocol_unittest.cc
@@ -95,12 +95,13 @@
   WireMessage recv_msg;
   ASSERT_TRUE(ReceiveWireMessage(reinterpret_cast<char*>(buf.data), buf.size,
                                  &recv_msg));
-  shmem_server->EndRead(std::move(buf));
 
   ASSERT_EQ(recv_msg.record_type, msg.record_type);
   ASSERT_EQ(*recv_msg.alloc_header, *msg.alloc_header);
   ASSERT_EQ(recv_msg.payload_size, msg.payload_size);
   ASSERT_STREQ(recv_msg.payload, msg.payload);
+
+  shmem_server->EndRead(std::move(buf));
 }
 
 TEST(WireProtocolTest, FreeMessage) {
@@ -123,11 +124,12 @@
   WireMessage recv_msg;
   ASSERT_TRUE(ReceiveWireMessage(reinterpret_cast<char*>(buf.data), buf.size,
                                  &recv_msg));
-  shmem_server->EndRead(std::move(buf));
 
   ASSERT_EQ(recv_msg.record_type, msg.record_type);
   ASSERT_EQ(*recv_msg.free_header, *msg.free_header);
   ASSERT_EQ(recv_msg.payload_size, msg.payload_size);
+
+  shmem_server->EndRead(std::move(buf));
 }
 
 TEST(GetHeapSamplingInterval, Default) {
@@ -160,6 +162,17 @@
   EXPECT_EQ(GetHeapSamplingInterval(cli_config, "else"), 1u);
 }
 
+TEST(GetHeapSamplingInterval, DisabledAndDefault) {
+  ClientConfiguration cli_config{};
+  cli_config.all_heaps = true;
+  cli_config.num_heaps = 1;
+  cli_config.default_interval = 1;
+  memcpy(cli_config.heaps[0].name, "something", sizeof("something"));
+  cli_config.heaps[0].interval = 0u;
+  EXPECT_EQ(GetHeapSamplingInterval(cli_config, "something"), 0u);
+  EXPECT_EQ(GetHeapSamplingInterval(cli_config, "else"), 1u);
+}
+
 }  // namespace
 }  // namespace profiling
 }  // namespace perfetto
diff --git a/src/profiling/perf/event_config.cc b/src/profiling/perf/event_config.cc
index e3a8b9c..7b5170e 100644
--- a/src/profiling/perf/event_config.cc
+++ b/src/profiling/perf/event_config.cc
@@ -20,6 +20,7 @@
 #include <time.h>
 
 #include <unwindstack/Regs.h>
+#include <vector>
 
 #include "perfetto/base/flat_set.h"
 #include "perfetto/ext/base/optional.h"
@@ -297,7 +298,7 @@
     // expected = rate * period, with a conversion of period from ms to s:
     uint64_t expected_samples_per_tick =
         1 + (sampling_frequency * read_tick_period_ms) / 1000;
-    // Double the the limit to account of actual sample rate uncertainties, as
+    // Double the limit to account of actual sample rate uncertainties, as
     // well as any other factors:
     samples_per_tick_limit = 2 * expected_samples_per_tick;
   } else {  // sampling_period
@@ -371,7 +372,8 @@
       raw_ds_config, pe, timebase_event, sample_callstacks,
       std::move(target_filter), kernel_frames, ring_buffer_pages.value(),
       read_tick_period_ms, samples_per_tick_limit, remote_descriptor_timeout_ms,
-      pb_config.unwind_state_clear_period_ms(), max_enqueued_footprint_bytes);
+      pb_config.unwind_state_clear_period_ms(), max_enqueued_footprint_bytes,
+      pb_config.target_installed_by());
 }
 
 EventConfig::EventConfig(const DataSourceConfig& raw_ds_config,
@@ -385,7 +387,8 @@
                          uint64_t samples_per_tick_limit,
                          uint32_t remote_descriptor_timeout_ms,
                          uint32_t unwind_state_clear_period_ms,
-                         uint64_t max_enqueued_footprint_bytes)
+                         uint64_t max_enqueued_footprint_bytes,
+                         std::vector<std::string> target_installed_by)
     : perf_event_attr_(pe),
       timebase_event_(timebase_event),
       sample_callstacks_(sample_callstacks),
@@ -397,6 +400,7 @@
       remote_descriptor_timeout_ms_(remote_descriptor_timeout_ms),
       unwind_state_clear_period_ms_(unwind_state_clear_period_ms),
       max_enqueued_footprint_bytes_(max_enqueued_footprint_bytes),
+      target_installed_by_(std::move(target_installed_by)),
       raw_ds_config_(raw_ds_config) /* full copy */ {}
 
 }  // namespace profiling
diff --git a/src/profiling/perf/event_config.h b/src/profiling/perf/event_config.h
index 7006d5d..d75c7c8 100644
--- a/src/profiling/perf/event_config.h
+++ b/src/profiling/perf/event_config.h
@@ -19,6 +19,7 @@
 
 #include <functional>
 #include <string>
+#include <vector>
 
 #include <inttypes.h>
 #include <linux/perf_event.h>
@@ -110,6 +111,9 @@
     return const_cast<perf_event_attr*>(&perf_event_attr_);
   }
   const PerfCounter& timebase_event() const { return timebase_event_; }
+  const std::vector<std::string>& target_installed_by() const {
+    return target_installed_by_;
+  }
   const DataSourceConfig& raw_ds_config() const { return raw_ds_config_; }
 
  private:
@@ -124,7 +128,8 @@
               uint64_t samples_per_tick_limit,
               uint32_t remote_descriptor_timeout_ms,
               uint32_t unwind_state_clear_period_ms,
-              uint64_t max_enqueued_footprint_bytes);
+              uint64_t max_enqueued_footprint_bytes,
+              std::vector<std::string> target_installed_by);
 
   // Parameter struct for the leader (timebase) perf_event_open syscall.
   perf_event_attr perf_event_attr_ = {};
@@ -164,6 +169,13 @@
 
   const uint64_t max_enqueued_footprint_bytes_;
 
+  // Only profile target if it was installed by one of the packages given.
+  // Special values are:
+  // * @system: installed on the system partition
+  // * @product: installed on the product partition
+  // * @null: sideloaded
+  const std::vector<std::string> target_installed_by_;
+
   // The raw data source config, as a pbzero-generated C++ class.
   const DataSourceConfig raw_ds_config_;
 };
diff --git a/src/profiling/perf/event_config_unittest.cc b/src/profiling/perf/event_config_unittest.cc
index c69d45d..30520f1 100644
--- a/src/profiling/perf/event_config_unittest.cc
+++ b/src/profiling/perf/event_config_unittest.cc
@@ -318,9 +318,10 @@
         EventConfig::Create(AsDataSourceConfig(cfg));
 
     ASSERT_TRUE(event_config.has_value());
-    EXPECT_EQ(event_config->perf_attr()->sample_type &
-                  (PERF_SAMPLE_STACK_USER | PERF_SAMPLE_REGS_USER),
-              PERF_SAMPLE_STACK_USER | PERF_SAMPLE_REGS_USER);
+    EXPECT_EQ(
+        event_config->perf_attr()->sample_type &
+            (PERF_SAMPLE_STACK_USER | PERF_SAMPLE_REGS_USER),
+        static_cast<uint64_t>(PERF_SAMPLE_STACK_USER | PERF_SAMPLE_REGS_USER));
 
     EXPECT_NE(event_config->perf_attr()->sample_regs_user, 0u);
     EXPECT_NE(event_config->perf_attr()->sample_stack_user, 0u);
diff --git a/src/profiling/perf/perf_producer.cc b/src/profiling/perf/perf_producer.cc
index d837494..b1c7e1f 100644
--- a/src/profiling/perf/perf_producer.cc
+++ b/src/profiling/perf/perf_producer.cc
@@ -538,7 +538,7 @@
     if (!ds->event_config.sample_callstacks()) {
       CompletedSample output;
       output.common = sample->common;
-      PostEmitSample(ds_id, std::move(output));
+      EmitSample(ds_id, std::move(output));
       continue;
     }
 
@@ -554,8 +554,8 @@
     if (process_state == ProcessTrackingStatus::kExpired) {
       PERFETTO_DLOG("Skipping sample for previously expired pid [%d]",
                     static_cast<int>(pid));
-      PostEmitSkippedSample(ds_id, std::move(sample.value()),
-                            SampleSkipReason::kReadStage);
+      EmitSkippedSample(ds_id, std::move(sample.value()),
+                        SampleSkipReason::kReadStage);
       continue;
     }
 
@@ -597,8 +597,8 @@
       uint64_t footprint_bytes = unwinding_worker_->GetEnqueuedFootprint();
       if (footprint_bytes + sample_stack_size >= max_footprint_bytes) {
         PERFETTO_DLOG("Skipping sample enqueueing due to footprint limit.");
-        PostEmitSkippedSample(ds_id, std::move(sample.value()),
-                              SampleSkipReason::kUnwindEnqueue);
+        EmitSkippedSample(ds_id, std::move(sample.value()),
+                          SampleSkipReason::kUnwindEnqueue);
         continue;
       }
     }
@@ -613,8 +613,8 @@
       unwinding_worker_->IncrementEnqueuedFootprint(sample_stack_size);
     } else {
       PERFETTO_DLOG("Unwinder queue full, skipping sample");
-      PostEmitSkippedSample(ds_id, std::move(sample.value()),
-                            SampleSkipReason::kUnwindEnqueue);
+      EmitSkippedSample(ds_id, std::move(sample.value()),
+                        SampleSkipReason::kUnwindEnqueue);
     }
   }
 
@@ -636,7 +636,8 @@
     if (proc_status_it == ds.process_states.end())
       continue;
 
-    if (!CanProfile(ds.event_config.raw_ds_config(), uid)) {
+    if (!CanProfile(ds.event_config.raw_ds_config(), uid,
+                    ds.event_config.target_installed_by())) {
       PERFETTO_DLOG("Not profileable: pid [%d], uid [%d] for DS [%zu]",
                     static_cast<int>(pid), static_cast<int>(uid),
                     static_cast<size_t>(it.first));
diff --git a/src/profiling/perf/unwinding.cc b/src/profiling/perf/unwinding.cc
index 533de7d..46dfc6b 100644
--- a/src/profiling/perf/unwinding.cc
+++ b/src/profiling/perf/unwinding.cc
@@ -323,7 +323,7 @@
         : error_code(e), warnings(w), frames(std::move(f)) {}
     UnwindResult(const UnwindResult&) = delete;
     UnwindResult& operator=(const UnwindResult&) = delete;
-    UnwindResult(UnwindResult&&) = default;
+    UnwindResult(UnwindResult&&) __attribute__((unused)) = default;
     UnwindResult& operator=(UnwindResult&&) = default;
   };
   auto attempt_unwind = [&sample, unwind_state, pid_unwound_before,
diff --git a/src/profiling/perf/unwinding.h b/src/profiling/perf/unwinding.h
index 2eb296f..398747a 100644
--- a/src/profiling/perf/unwinding.h
+++ b/src/profiling/perf/unwinding.h
@@ -17,6 +17,7 @@
 #ifndef SRC_PROFILING_PERF_UNWINDING_H_
 #define SRC_PROFILING_PERF_UNWINDING_H_
 
+#include <condition_variable>
 #include <map>
 #include <thread>
 
diff --git a/src/protozero/BUILD.gn b/src/protozero/BUILD.gn
index 5cacaf7..b11a569 100644
--- a/src/protozero/BUILD.gn
+++ b/src/protozero/BUILD.gn
@@ -44,9 +44,22 @@
   ]
 }
 
+source_set("proto_ring_buffer") {
+  deps = [
+    ":protozero",
+    "../../gn:default_deps",
+    "../base",
+  ]
+  sources = [
+    "proto_ring_buffer.cc",
+    "proto_ring_buffer.h",
+  ]
+}
+
 perfetto_unittest_source_set("unittests") {
   testonly = true
   deps = [
+    ":proto_ring_buffer",
     ":protozero",
     ":testing_messages_cpp",
     ":testing_messages_lite",
@@ -55,12 +68,14 @@
     "../../gn:gtest_and_gmock",
     "../base",
     "../base:test_support",
+    "filtering:unittests",
   ]
   sources = [
     "copyable_ptr_unittest.cc",
     "message_handle_unittest.cc",
     "message_unittest.cc",
     "proto_decoder_unittest.cc",
+    "proto_ring_buffer_unittest.cc",
     "proto_utils_unittest.cc",
     "scattered_stream_writer_unittest.cc",
     "test/cppgen_conformance_unittest.cc",
diff --git a/src/protozero/filtering/BUILD.gn b/src/protozero/filtering/BUILD.gn
new file mode 100644
index 0000000..ffd1c8c
--- /dev/null
+++ b/src/protozero/filtering/BUILD.gn
@@ -0,0 +1,144 @@
+# 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/fuzzer.gni")
+import("../../../gn/perfetto_host_executable.gni")
+import("../../../gn/proto_library.gni")
+import("../../../gn/test.gni")
+
+source_set("message_filter") {
+  sources = [
+    "message_filter.cc",
+    "message_filter.h",
+    "message_tokenizer.h",
+  ]
+  deps = [
+    ":bytecode_parser",
+    "..:protozero",
+    "../../../gn:default_deps",
+    "../../base",
+  ]
+}
+
+source_set("bytecode_common") {
+  sources = [ "filter_bytecode_common.h" ]
+  deps = [ "../../../gn:default_deps" ]
+}
+
+source_set("bytecode_parser") {
+  sources = [
+    "filter_bytecode_parser.cc",
+    "filter_bytecode_parser.h",
+  ]
+  deps = [
+    ":bytecode_common",
+    "..:protozero",
+    "../../../gn:default_deps",
+    "../../base",
+  ]
+}
+
+source_set("bytecode_generator") {
+  sources = [
+    "filter_bytecode_generator.cc",
+    "filter_bytecode_generator.h",
+  ]
+  deps = [
+    ":bytecode_common",
+    "..:protozero",
+    "../../../gn:default_deps",
+    "../../base",
+  ]
+}
+
+source_set("filter_util") {
+  testonly = true
+  sources = [
+    "filter_util.cc",
+    "filter_util.h",
+  ]
+  deps = [
+    ":bytecode_generator",
+    "..:protozero",
+    "../../../gn:default_deps",
+    "../../../gn:protobuf_full",
+    "../../base",
+  ]
+}
+
+perfetto_unittest_source_set("unittests") {
+  testonly = true
+  deps = [
+    ":bytecode_common",
+    ":bytecode_generator",
+    ":bytecode_parser",
+    ":message_filter",
+    "..:protozero",
+    "../../../gn:default_deps",
+    "../../../gn:gtest_and_gmock",
+    "../../../protos/perfetto/trace:lite",
+    "../../base",
+    "../../base:test_support",
+  ]
+  sources = [
+    "filter_bytecode_generator_unittest.cc",
+    "filter_bytecode_parser_unittest.cc",
+    "message_tokenizer_unittest.cc",
+  ]
+
+  # On chromium component build we cannot have a test target depening boh on
+  # protobuf-full and protobuf-lite. Just skip those targets there.
+  # See also https://crug.com/1210223 .
+  if (perfetto_build_standalone || perfetto_build_with_android) {
+    deps += [ ":filter_util" ]
+    sources += [
+      "filter_util_unittest.cc",
+      "message_filter_unittest.cc",
+    ]
+  }
+}
+
+if (enable_perfetto_benchmarks) {
+  source_set("benchmarks") {
+    testonly = true
+    deps = [
+      ":message_filter",
+      "../../../gn:benchmark",
+      "../../../gn:default_deps",
+      "../../base",
+      "../../base:test_support",
+    ]
+    sources = [ "message_filter_benchmark.cc" ]
+  }
+}
+
+perfetto_fuzzer_test("protozero_bytecode_parser_fuzzer") {
+  sources = [ "filter_bytecode_parser_fuzzer.cc" ]
+  deps = [
+    ":bytecode_parser",
+    "..:protozero",
+    "../../../gn:default_deps",
+    "../../base",
+  ]
+}
+
+perfetto_fuzzer_test("protozero_message_filter_fuzzer") {
+  sources = [ "message_filter_fuzzer.cc" ]
+  deps = [
+    ":message_filter",
+    "..:protozero",
+    "../../../gn:default_deps",
+    "../../base",
+  ]
+}
diff --git a/src/protozero/filtering/filter_bytecode_common.h b/src/protozero/filtering/filter_bytecode_common.h
new file mode 100644
index 0000000..9c0e6f2
--- /dev/null
+++ b/src/protozero/filtering/filter_bytecode_common.h
@@ -0,0 +1,42 @@
+/*
+ * 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_PROTOZERO_FILTERING_FILTER_BYTECODE_COMMON_H_
+#define SRC_PROTOZERO_FILTERING_FILTER_BYTECODE_COMMON_H_
+
+#include <stdint.h>
+
+namespace protozero {
+
+enum FilterOpcode : uint32_t {
+  // The immediate value is 0 in this case.
+  kFilterOpcode_EndOfMessage = 0,
+
+  // The immediate value is the id of the allowed field.
+  kFilterOpcode_SimpleField = 1,
+
+  // The immediate value is the start of the range. The next word (without
+  // any shifting) is the length of the range.
+  kFilterOpcode_SimpleFieldRange = 2,
+
+  // The immediate value is the id of the allowed field. The next word
+  // (without any shifting) is the index of the filter that should be used to
+  // recurse into the nested message.
+  kFilterOpcode_NestedField = 3,
+};
+}  // namespace protozero
+
+#endif  // SRC_PROTOZERO_FILTERING_FILTER_BYTECODE_COMMON_H_
diff --git a/src/protozero/filtering/filter_bytecode_generator.cc b/src/protozero/filtering/filter_bytecode_generator.cc
new file mode 100644
index 0000000..9362869
--- /dev/null
+++ b/src/protozero/filtering/filter_bytecode_generator.cc
@@ -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.
+ */
+
+#include "src/protozero/filtering/filter_bytecode_generator.h"
+
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/hash.h"
+#include "perfetto/protozero/packed_repeated_fields.h"
+#include "perfetto/protozero/proto_utils.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "src/protozero/filtering/filter_bytecode_common.h"
+
+namespace protozero {
+
+FilterBytecodeGenerator::FilterBytecodeGenerator() = default;
+FilterBytecodeGenerator::~FilterBytecodeGenerator() = default;
+
+void FilterBytecodeGenerator::EndMessage() {
+  endmessage_called_ = true;
+  bytecode_.push_back(kFilterOpcode_EndOfMessage);
+  last_field_id_ = 0;
+  ++num_messages_;
+}
+
+// Allows a simple field (varint, fixed32/64, string or bytes).
+void FilterBytecodeGenerator::AddSimpleField(uint32_t field_id) {
+  PERFETTO_CHECK(field_id > last_field_id_);
+  bytecode_.push_back(field_id << 3 | kFilterOpcode_SimpleField);
+  last_field_id_ = field_id;
+  endmessage_called_ = false;
+}
+
+// Allows a range of simple fields. |range_start| is the id of the first field
+// in range, |range_len| the number of fields in the range.
+// AddSimpleFieldRange(N,1) is semantically equivalent to AddSimpleField(N).
+void FilterBytecodeGenerator::AddSimpleFieldRange(uint32_t range_start,
+                                                  uint32_t range_len) {
+  PERFETTO_CHECK(range_start > last_field_id_);
+  PERFETTO_CHECK(range_len > 0);
+  bytecode_.push_back(range_start << 3 | kFilterOpcode_SimpleFieldRange);
+  bytecode_.push_back(range_len);
+  last_field_id_ = range_start + range_len - 1;
+  endmessage_called_ = false;
+}
+
+// Adds a nested field. |message_index| is the index of the message that the
+// parser must recurse into. This implies that at least |message_index| + 1
+// calls to EndMessage() will be made.
+// The Serialize() method will fail if any field points to an out of range
+// index.
+void FilterBytecodeGenerator::AddNestedField(uint32_t field_id,
+                                             uint32_t message_index) {
+  PERFETTO_CHECK(field_id > last_field_id_);
+  bytecode_.push_back(field_id << 3 | kFilterOpcode_NestedField);
+  bytecode_.push_back(message_index);
+  last_field_id_ = field_id;
+  max_msg_index_ = std::max(max_msg_index_, message_index);
+  endmessage_called_ = false;
+}
+
+// Returns the bytes that can be used into TraceConfig.trace_filter.bytecode.
+// The returned bytecode is a binary buffer which consists of a sequence of
+// varints (the opcodes) and a checksum.
+// The returned string can be passed as-is to FilterBytecodeParser.Load().
+std::string FilterBytecodeGenerator::Serialize() {
+  PERFETTO_CHECK(endmessage_called_);
+  PERFETTO_CHECK(max_msg_index_ < num_messages_);
+  protozero::PackedVarInt words;
+  perfetto::base::Hash hasher;
+  for (uint32_t word : bytecode_) {
+    words.Append(word);
+    hasher.Update(word);
+  }
+  words.Append(static_cast<uint32_t>(hasher.digest()));
+  return std::string(reinterpret_cast<const char*>(words.data()), words.size());
+}
+
+}  // namespace protozero
diff --git a/src/protozero/filtering/filter_bytecode_generator.h b/src/protozero/filtering/filter_bytecode_generator.h
new file mode 100644
index 0000000..809799c
--- /dev/null
+++ b/src/protozero/filtering/filter_bytecode_generator.h
@@ -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.
+ */
+
+#ifndef SRC_PROTOZERO_FILTERING_FILTER_BYTECODE_GENERATOR_H_
+#define SRC_PROTOZERO_FILTERING_FILTER_BYTECODE_GENERATOR_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+namespace protozero {
+
+// Creates a filter bytecode that can be passed to the FilterBytecodeParser.
+// This class is typically only used by offline tools (e.g. the proto_filter
+// cmdline tool). See go/trace-filtering for the full filtering design.
+class FilterBytecodeGenerator {
+ public:
+  FilterBytecodeGenerator();
+  ~FilterBytecodeGenerator();
+
+  // Call at the end of every message. It implicitly starts a new message, there
+  // is no corresponding BeginMessage().
+  void EndMessage();
+
+  // All the methods below must be called in monotonic field_id order or the
+  // generator will CHECK() and crash.
+
+  // Allows a simple field (varint, fixed32/64, string or bytes).
+  void AddSimpleField(uint32_t field_id);
+
+  // Allows a range of simple fields. |range_start| is the id of the first field
+  // in range, |range_len| the number of fields in the range.
+  // AddSimpleFieldRange(N,1) is semantically equivalent to AddSimpleField(N)
+  // (but it takes 2 words to encode, rather than just one).
+  void AddSimpleFieldRange(uint32_t range_start, uint32_t range_len);
+
+  // Adds a nested field. |message_index| is the index of the message that the
+  // parser must recurse into. This implies that at least |message_index| calls
+  // to Begin/EndMessage will be made.
+  // The Serialize() method will fail if any field points to an index that is
+  // out of range (e.g., if message_index = 5 but only 3 EndMessage() calls were
+  // made).
+  void AddNestedField(uint32_t field_id, uint32_t message_index);
+
+  // Returns the filter bytecode, which is a buffer containing a sequence of
+  // varints and a checksum. The returned string can be passed to
+  // FilterBytecodeParser.Load().
+  std::string Serialize();
+
+ private:
+  uint32_t num_messages_ = 0;
+  uint32_t last_field_id_ = 0;
+  uint32_t max_msg_index_ = 0;
+  bool endmessage_called_ = false;
+
+  std::vector<uint32_t> bytecode_;
+};
+
+}  // namespace protozero
+
+#endif  // SRC_PROTOZERO_FILTERING_FILTER_BYTECODE_GENERATOR_H_
diff --git a/src/protozero/filtering/filter_bytecode_generator_unittest.cc b/src/protozero/filtering/filter_bytecode_generator_unittest.cc
new file mode 100644
index 0000000..650120b
--- /dev/null
+++ b/src/protozero/filtering/filter_bytecode_generator_unittest.cc
@@ -0,0 +1,143 @@
+/*
+ * 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 "test/gtest_and_gmock.h"
+
+#include "perfetto/protozero/packed_repeated_fields.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "src/protozero/filtering/filter_bytecode_generator.h"
+#include "src/protozero/filtering/filter_bytecode_parser.h"
+
+// This file tests the generator, assuming the parser is good.
+// The parser is tested separately (without the generator) in
+// filter_bytecode_parser_unittest.cc
+
+namespace protozero {
+
+namespace {
+
+TEST(FilterBytecodeGeneratorTest, SimpleFields) {
+  FilterBytecodeGenerator gen;
+  gen.AddSimpleField(1u);
+  gen.AddSimpleField(127u);
+  gen.AddSimpleField(128u);
+  gen.AddSimpleField(1000u);
+  gen.EndMessage();
+
+  FilterBytecodeParser parser;
+  std::string bytecode = gen.Serialize();
+  ASSERT_TRUE(parser.Load(reinterpret_cast<const uint8_t*>(bytecode.data()),
+                          bytecode.size()));
+  EXPECT_FALSE(parser.Query(0, 0).allowed);
+  EXPECT_TRUE(parser.Query(0, 1).allowed);
+  EXPECT_FALSE(parser.Query(0, 126).allowed);
+  EXPECT_TRUE(parser.Query(0, 127).allowed);
+  EXPECT_TRUE(parser.Query(0, 128).allowed);
+  EXPECT_FALSE(parser.Query(0, 129).allowed);
+  EXPECT_TRUE(parser.Query(0, 1000).allowed);
+  EXPECT_FALSE(parser.Query(0, 1001).allowed);
+}
+
+TEST(FilterBytecodeGeneratorTest, SimpleAndRanges) {
+  FilterBytecodeGenerator gen;
+  gen.AddSimpleField(1u);
+  gen.AddSimpleFieldRange(10, 10);
+  gen.AddSimpleField(30u);
+  gen.AddSimpleFieldRange(120, 20);
+  gen.AddSimpleField(1000u);
+  gen.EndMessage();
+
+  FilterBytecodeParser parser;
+  std::string bytecode = gen.Serialize();
+  ASSERT_TRUE(parser.Load(reinterpret_cast<const uint8_t*>(bytecode.data()),
+                          bytecode.size()));
+  EXPECT_FALSE(parser.Query(0, 0).allowed);
+  EXPECT_TRUE(parser.Query(0, 1).allowed);
+  EXPECT_FALSE(parser.Query(0, 9).allowed);
+  for (uint32_t i = 10; i <= 19; ++i)
+    EXPECT_TRUE(parser.Query(0, i).allowed);
+  EXPECT_TRUE(parser.Query(0, 30).allowed);
+  for (uint32_t i = 120; i <= 139; ++i)
+    EXPECT_TRUE(parser.Query(0, i).allowed);
+  EXPECT_FALSE(parser.Query(0, 140).allowed);
+  EXPECT_FALSE(parser.Query(0, 999).allowed);
+  EXPECT_TRUE(parser.Query(0, 1000).allowed);
+  EXPECT_FALSE(parser.Query(0, 1001).allowed);
+}
+
+TEST(FilterBytecodeGeneratorTest, Nested) {
+  FilterBytecodeGenerator gen;
+  // Message 0.
+  gen.AddSimpleField(1u);
+  gen.AddSimpleFieldRange(10, 1);
+  gen.AddNestedField(11, 3);
+  gen.AddNestedField(12, 1);
+  gen.EndMessage();
+
+  // Message 1.
+  gen.AddNestedField(11, 1);  // Recursive
+  gen.AddNestedField(12, 2);  // Recursive
+  gen.AddNestedField(13, 3);  // Recursive
+  gen.EndMessage();
+
+  // Message 2.
+  gen.AddSimpleField(21);
+  gen.EndMessage();
+
+  // Message 3.
+  gen.AddNestedField(1, 0);  // Recurse in the root message (sneaky).
+  gen.AddSimpleField(31);
+  gen.EndMessage();
+
+  FilterBytecodeParser parser;
+  std::string bytecode = gen.Serialize();
+  ASSERT_TRUE(parser.Load(reinterpret_cast<const uint8_t*>(bytecode.data()),
+                          bytecode.size()));
+
+  // Check root message.
+  EXPECT_TRUE(parser.Query(0, 1).allowed);
+  EXPECT_TRUE(parser.Query(0, 1).simple_field());
+  EXPECT_TRUE(parser.Query(0, 10).allowed);
+  EXPECT_TRUE(parser.Query(0, 10).simple_field());
+  EXPECT_TRUE(parser.Query(0, 11).allowed);
+  EXPECT_EQ(parser.Query(0, 11).nested_msg_index, 3u);
+  EXPECT_TRUE(parser.Query(0, 12).allowed);
+  EXPECT_EQ(parser.Query(0, 12).nested_msg_index, 1u);
+  EXPECT_FALSE(parser.Query(0, 13).allowed);
+
+  // Check message 1.
+  EXPECT_FALSE(parser.Query(1, 10).allowed);
+  EXPECT_TRUE(parser.Query(1, 11).allowed);
+  EXPECT_EQ(parser.Query(1, 11).nested_msg_index, 1u);
+  EXPECT_TRUE(parser.Query(1, 12).allowed);
+  EXPECT_EQ(parser.Query(1, 12).nested_msg_index, 2u);
+  EXPECT_TRUE(parser.Query(1, 13).allowed);
+  EXPECT_EQ(parser.Query(1, 13).nested_msg_index, 3u);
+
+  // Check message 2.
+  EXPECT_FALSE(parser.Query(2, 11).allowed);
+  EXPECT_TRUE(parser.Query(2, 21).allowed);
+  EXPECT_TRUE(parser.Query(2, 21).simple_field());
+
+  // Check message 3.
+  EXPECT_TRUE(parser.Query(3, 1).allowed);
+  EXPECT_EQ(parser.Query(3, 1).nested_msg_index, 0u);
+  EXPECT_TRUE(parser.Query(3, 31).allowed);
+  EXPECT_TRUE(parser.Query(3, 31).simple_field());
+}
+
+}  // namespace
+}  // namespace protozero
diff --git a/src/protozero/filtering/filter_bytecode_parser.cc b/src/protozero/filtering/filter_bytecode_parser.cc
new file mode 100644
index 0000000..39c8bb6
--- /dev/null
+++ b/src/protozero/filtering/filter_bytecode_parser.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 "src/protozero/filtering/filter_bytecode_parser.h"
+
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/hash.h"
+#include "perfetto/protozero/packed_repeated_fields.h"
+#include "perfetto/protozero/proto_decoder.h"
+#include "perfetto/protozero/proto_utils.h"
+#include "src/protozero/filtering/filter_bytecode_common.h"
+
+namespace protozero {
+
+void FilterBytecodeParser::Reset() {
+  bool suppress = suppress_logs_for_fuzzer_;
+  *this = FilterBytecodeParser();
+  suppress_logs_for_fuzzer_ = suppress;
+}
+
+bool FilterBytecodeParser::Load(const void* filter_data, size_t len) {
+  Reset();
+  bool res = LoadInternal(static_cast<const uint8_t*>(filter_data), len);
+  // If load fails, don't leave the parser in a half broken state.
+  if (!res)
+    Reset();
+  return res;
+}
+
+bool FilterBytecodeParser::LoadInternal(const uint8_t* bytecode_data,
+                                        size_t len) {
+  // First unpack the varints into a plain uint32 vector, so it's easy to
+  // iterate through them and look ahead.
+  std::vector<uint32_t> words;
+  bool packed_parse_err = false;
+  words.reserve(len);  // An overestimation, but avoids reallocations.
+  using BytecodeDecoder =
+      PackedRepeatedFieldIterator<proto_utils::ProtoWireType::kVarInt,
+                                  uint32_t>;
+  for (BytecodeDecoder it(bytecode_data, len, &packed_parse_err); it; ++it)
+    words.emplace_back(*it);
+
+  if (packed_parse_err || words.empty())
+    return false;
+
+  perfetto::base::Hash hasher;
+  for (size_t i = 0; i < words.size() - 1; ++i)
+    hasher.Update(words[i]);
+
+  uint32_t expected_csum = static_cast<uint32_t>(hasher.digest());
+  if (expected_csum != words.back()) {
+    if (!suppress_logs_for_fuzzer_) {
+      PERFETTO_ELOG("Filter bytecode checksum failed. Expected: %x, actual: %x",
+                    expected_csum, words.back());
+    }
+    return false;
+  }
+
+  words.pop_back();  // Pop the checksum.
+
+  // Temporay storage for each message. Cleared on every END_OF_MESSAGE.
+  std::vector<uint32_t> direct_indexed_fields;
+  std::vector<uint32_t> ranges;
+  uint32_t max_msg_index = 0;
+
+  auto add_directly_indexed_field = [&](uint32_t field_id, uint32_t msg_id) {
+    PERFETTO_DCHECK(field_id > 0 && field_id < kDirectlyIndexLimit);
+    direct_indexed_fields.resize(std::max(direct_indexed_fields.size(),
+                                          static_cast<size_t>(field_id) + 1));
+    direct_indexed_fields[field_id] = kAllowed | msg_id;
+  };
+
+  auto add_range = [&](uint32_t id_start, uint32_t id_end, uint32_t msg_id) {
+    PERFETTO_DCHECK(id_end > id_start);
+    PERFETTO_DCHECK(id_start >= kDirectlyIndexLimit);
+    ranges.emplace_back(id_start);
+    ranges.emplace_back(id_end);
+    ranges.emplace_back(kAllowed | msg_id);
+  };
+
+  for (size_t i = 0; i < words.size(); ++i) {
+    const uint32_t word = words[i];
+    const bool has_next_word = i < words.size() - 1;
+    const uint32_t opcode = word & 0x7u;
+    const uint32_t field_id = word >> 3;
+
+    if (field_id == 0 && opcode != kFilterOpcode_EndOfMessage) {
+      PERFETTO_DLOG("bytecode error @ word %zu, invalid field id (0)", i);
+      return false;
+    }
+
+    if (opcode == kFilterOpcode_SimpleField ||
+        opcode == kFilterOpcode_NestedField) {
+      // Field words are organized as follow:
+      // MSB: 1 if allowed, 0 if not allowed.
+      // Remaining bits:
+      //   Message index in the case of nested (non-simple) messages.
+      //   0x7f..f in the case of simple messages.
+      uint32_t msg_id;
+      if (opcode == kFilterOpcode_SimpleField) {
+        msg_id = kSimpleField;
+      } else {  // FILTER_OPCODE_NESTED_FIELD
+        // The next word in the bytecode contains the message index.
+        if (!has_next_word) {
+          PERFETTO_DLOG("bytecode error @ word %zu: unterminated nested field",
+                        i);
+          return false;
+        }
+        msg_id = words[++i];
+        max_msg_index = std::max(max_msg_index, msg_id);
+      }
+
+      if (field_id < kDirectlyIndexLimit) {
+        add_directly_indexed_field(field_id, msg_id);
+      } else {
+        // In the case of a large field id (rare) we waste an extra word and
+        // represent it as a range. Doesn't make sense to introduce extra
+        // complexity to deal with rare cases like this.
+        add_range(field_id, field_id + 1, msg_id);
+      }
+    } else if (opcode == kFilterOpcode_SimpleFieldRange) {
+      if (!has_next_word) {
+        PERFETTO_DLOG("bytecode error @ word %zu: unterminated range", i);
+        return false;
+      }
+      const uint32_t range_len = words[++i];
+      const uint32_t range_end = field_id + range_len;  // STL-style, excl.
+      uint32_t id = field_id;
+
+      // Here's the subtle complexity: at the bytecode level, we don't know
+      // anything about the kDirectlyIndexLimit. It is legit to define a range
+      // that spans across the direct-indexing threshold (e.g. 126-132). In that
+      // case we want to add all the elements < the indexing to the O(1) bucket
+      // and add only the remaining range as a non-indexed range.
+      for (; id < range_end && id < kDirectlyIndexLimit; ++id)
+        add_directly_indexed_field(id, kAllowed | kSimpleField);
+      PERFETTO_DCHECK(id >= kDirectlyIndexLimit || id == range_end);
+      if (id < range_end)
+        add_range(id, range_end, kSimpleField);
+    } else if (opcode == kFilterOpcode_EndOfMessage) {
+      // For each message append:
+      // 1. The "header" word telling how many directly indexed fields there
+      //    are.
+      // 2. The words for the directly indexed fields (id < 128).
+      // 3. The rest of the fields, encoded as ranges.
+      // Also update the |message_offset_| index to remember the word offset for
+      // the current message.
+      message_offset_.emplace_back(static_cast<uint32_t>(words_.size()));
+      words_.emplace_back(static_cast<uint32_t>(direct_indexed_fields.size()));
+      words_.insert(words_.end(), direct_indexed_fields.begin(),
+                    direct_indexed_fields.end());
+      words_.insert(words_.end(), ranges.begin(), ranges.end());
+      direct_indexed_fields.clear();
+      ranges.clear();
+    } else {
+      PERFETTO_DLOG("bytecode error @ word %zu: invalid opcode (%x)", i, word);
+      return false;
+    }
+  }  // (for word in bytecode).
+
+  if (max_msg_index > 0 && max_msg_index >= message_offset_.size()) {
+    PERFETTO_DLOG(
+        "bytecode error: a message index (%u) is out of range "
+        "(num_messages=%zu)",
+        max_msg_index, message_offset_.size());
+    return false;
+  }
+
+  // Add a final entry to |message_offset_| so we can tell where the last
+  // message ends without an extra branch in the Query() hotpath.
+  message_offset_.emplace_back(static_cast<uint32_t>(words_.size()));
+
+  return true;
+}
+
+FilterBytecodeParser::QueryResult FilterBytecodeParser::Query(
+    uint32_t msg_index,
+    uint32_t field_id) {
+  FilterBytecodeParser::QueryResult res{false, 0u};
+  if (static_cast<uint64_t>(msg_index) + 1 >=
+      static_cast<uint64_t>(message_offset_.size())) {
+    return res;
+  }
+  const uint32_t start_offset = message_offset_[msg_index];
+  // These are DCHECKs and not just CHECKS because the |words_| is populated
+  // by the LoadInternal call above. These cannot be violated with a malformed
+  // bytecode.
+  PERFETTO_DCHECK(start_offset < words_.size());
+  const uint32_t* word = &words_[start_offset];
+  const uint32_t end_off = message_offset_[msg_index + 1];
+  const uint32_t* const end = words_.data() + end_off;
+  PERFETTO_DCHECK(end > word && end <= words_.data() + words_.size());
+  const uint32_t num_directly_indexed = *(word++);
+  PERFETTO_DCHECK(num_directly_indexed <= kDirectlyIndexLimit);
+  PERFETTO_DCHECK(word + num_directly_indexed <= end);
+  uint32_t field_state = 0;
+  if (PERFETTO_LIKELY(field_id < num_directly_indexed)) {
+    PERFETTO_DCHECK(&word[field_id] < end);
+    field_state = word[field_id];
+  } else {
+    for (word = word + num_directly_indexed; word + 2 < end;) {
+      const uint32_t range_start = *(word++);
+      const uint32_t range_end = *(word++);
+      const uint32_t range_state = *(word++);
+      if (field_id >= range_start && field_id < range_end) {
+        field_state = range_state;
+        break;
+      }
+    }  // for (word in ranges)
+  }    // if (field_id >= num_directly_indexed)
+
+  res.allowed = (field_state & kAllowed) != 0;
+  res.nested_msg_index = field_state & ~kAllowed;
+  PERFETTO_DCHECK(res.simple_field() ||
+                  res.nested_msg_index < message_offset_.size() - 1);
+  return res;
+}
+
+}  // namespace protozero
diff --git a/src/protozero/filtering/filter_bytecode_parser.h b/src/protozero/filtering/filter_bytecode_parser.h
new file mode 100644
index 0000000..6ecbdfb
--- /dev/null
+++ b/src/protozero/filtering/filter_bytecode_parser.h
@@ -0,0 +1,124 @@
+/*
+ * 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_PROTOZERO_FILTERING_FILTER_BYTECODE_PARSER_H_
+#define SRC_PROTOZERO_FILTERING_FILTER_BYTECODE_PARSER_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <vector>
+
+namespace protozero {
+
+// Loads the proto-encoded bytecode in memory and allows fast lookups for tuples
+// (msg_index, field_id) to tell if a given field should be allowed or not and,
+// in the case of nested fields, what is the next message index to recurse into.
+// This class does two things:
+// 1. Expands the array of varint from the proto into a vector<uint32_t>. This
+//    is to avoid performing varint decoding on every lookup, at the cost of
+//    some extra memory (2KB-4KB). Note that the expanded vector is not just a
+//    1:1 copy of the proto one (more below). This is to avoid O(Fields) linear
+//    lookup complexity.
+// 2. Creates an index of offsets to remember the start word for each message.
+//    This is so we can jump to O(1) to the N-th message when recursing into a
+//    nested fields, without having to scan and find the (N-1)-th END_OF_MESSAGE
+//    marker.
+// Overall lookups are O(1) for field ids < 128 (kDirectlyIndexLimit) and O(N),
+// with N being the number of allowed field ranges for other fields.
+// See comments around |word_| below for the structure of the word vector.
+class FilterBytecodeParser {
+ public:
+  // Result of a Query() operation
+  struct QueryResult {
+    bool allowed;  // Whether the field is allowed at all or no.
+
+    // If |allowed|==true && simple_field()==false, this tells the message index
+    // of the nested field that should be used when recursing in the parser.
+    uint32_t nested_msg_index;
+
+    // If |allowed|==true, specifies if the field is of a simple type (varint,
+    // fixed32/64, string or byte) or a nested field that needs recursion.
+    // In the latter case the caller is expected to use |nested_msg_index| for
+    // the next Query() calls.
+    bool simple_field() const { return nested_msg_index == kSimpleField; }
+  };
+
+  // Loads a filter. The filter data consists of a sequence of varints which
+  // contains the filter opcodes and a final checksum.
+  bool Load(const void* filter_data, size_t len);
+
+  // Checks wheter a given field is allowed or not.
+  // msg_index = 0 is the index of the root message, where all queries should
+  // start from (typically perfetto.protos.Trace).
+  QueryResult Query(uint32_t msg_index, uint32_t field_id);
+
+  void Reset();
+  void set_suppress_logs_for_fuzzer(bool x) { suppress_logs_for_fuzzer_ = x; }
+
+ private:
+  static constexpr uint32_t kDirectlyIndexLimit = 128;
+  static constexpr uint32_t kAllowed = 1u << 31u;
+  static constexpr uint32_t kSimpleField = 0x7fffffff;
+
+  bool LoadInternal(const uint8_t* filter_data, size_t len);
+
+  // The state of all fields for all messages is stored in one contiguous array.
+  // This is to avoid memory fragmentation and allocator overhead.
+  // We expect a high number of messages (hundreds), but each message is small.
+  // For each message we store two sets of uint32:
+  // 1. A set of "directly indexed" fields, for field ids < 128.
+  // 2. The remainder is a set of ranges.
+  // So each message descriptor consists of a sequence of words as follows:
+  //
+  // [0] -> how many directly indexed fields are stored next (up to 128)
+  //
+  // [1..N] -> One word per field id (See "field state" below).
+  //
+  // [N + 1] -> Start of field id range 1
+  // [N + 2] -> End of field id range 1 (exclusive, STL-style).
+  // [N + 3] -> Field state for fields in range 1 (below)
+  //
+  // [N + 4] -> Start of field id range 2
+  // [N + 5] -> End of field id range 2 (exclusive, STL-style).
+  // [N + 6] -> Field state for fields in range 2 (below)
+
+  // The "field state" word is as follows:
+  // Bit 31: 0 if the field is disallowed, 1 if allowed.
+  //         Only directly indexed fields can be 0 (it doesn't make sense to add
+  //         a range and then say "btw it's NOT allowed".. don't add it then.
+  //         0 is only used for filling gaps in the directly indexed bucket.
+  // Bits [30..0] (only when MSB == allowed):
+  //  0x7fffffff: The field is "simple" (varint, fixed32/64, string, bytes) and
+  //      can be directly passed through in output. No recursion is needed.
+  //  [0, 7ffffffe]: The field is a nested submessage. The value is the index
+  //     that must be passed as first argument to the next Query() calls.
+  //     Note that the message index is purely a monotonic counter in the
+  //     filter bytecode, has no proto-equivalent match (unlike field ids).
+  std::vector<uint32_t> words_;
+
+  // One entry for each message index stored in the filter plus a sentinel at
+  // the end. Maps each message index to the offset in |words_| where the
+  // Nth message start.
+  // message_offset_.size() - 2 == the max message id that can be parsed.
+  std::vector<uint32_t> message_offset_;
+
+  bool suppress_logs_for_fuzzer_ = false;
+};
+
+}  // namespace protozero
+
+#endif  // SRC_PROTOZERO_FILTERING_FILTER_BYTECODE_PARSER_H_
diff --git a/src/protozero/filtering/filter_bytecode_parser_fuzzer.cc b/src/protozero/filtering/filter_bytecode_parser_fuzzer.cc
new file mode 100644
index 0000000..578f6c9
--- /dev/null
+++ b/src/protozero/filtering/filter_bytecode_parser_fuzzer.cc
@@ -0,0 +1,94 @@
+/*
+ * 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 <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "perfetto/ext/base/hash.h"
+
+#include "perfetto/protozero/packed_repeated_fields.h"
+#include "src/protozero/filtering/filter_bytecode_parser.h"
+
+namespace protozero {
+namespace {
+
+// This function gives a little help to the fuzzer. The bytecode is really a
+// sequence of varint-encoded uint32 words, with a FNV1a checksum at the end.
+// It's very unlikely that the fuzzer on its own can work out the checksum, so
+// most fuzzer inputs are doomed to fail the checksum verification.
+// This takes the fuzzer input and builds a more plausible bytecode.
+void LoadBytecodeWithChecksum(FilterBytecodeParser* parser,
+                              const uint8_t* data,
+                              size_t size) {
+  protozero::PackedVarInt words;
+  perfetto::base::Hash hasher;
+  for (size_t i = 0; i < size; i += sizeof(uint32_t)) {
+    uint32_t word = 0;
+    memcpy(&word, data, sizeof(uint32_t));
+    words.Append(word);
+    hasher.Update(word);
+  }
+  words.Append(static_cast<uint32_t>(hasher.digest()));
+  parser->Load(words.data(), words.size());
+}
+
+int FuzzBytecodeParser(const uint8_t* data, size_t size) {
+  FilterBytecodeParser parser;
+  parser.set_suppress_logs_for_fuzzer(true);
+
+  if (size > 4 && data[0] < 192) {
+    // 75% of the times use the LoadBytecodeWithChecksum() which helps the
+    // fuzzer passing the checksum verification.
+    LoadBytecodeWithChecksum(&parser, data + 1, size - 1);
+  } else {
+    // In the remaining 25%, pass completely arbitrary inputs.
+    parser.Load(data, size);
+  }
+
+  // Smoke testing with known problematic values
+  for (uint32_t msg_index = 0; msg_index < 3; ++msg_index) {
+    parser.Query(msg_index, 0);
+    parser.Query(msg_index, 1);
+    parser.Query(msg_index, 127);
+    parser.Query(msg_index, 128);
+    parser.Query(msg_index, 129);
+    parser.Query(msg_index, 65536);
+    parser.Query(msg_index, 65536);
+    parser.Query(msg_index, 1u << 28);
+    parser.Query(msg_index, 1u << 31);
+  }
+
+  // Query using the random data at the end of the random buffer.
+  if (size > 8) {
+    uint32_t msg_index = 0;
+    uint32_t field_id = 0;
+    memcpy(&msg_index, &data[size - 8], 4);
+    memcpy(&field_id, &data[size - 4], 4);
+    parser.Query(msg_index, field_id);
+  }
+
+  return 0;
+}
+
+}  // namespace
+}  // namespace protozero
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size);
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  return protozero::FuzzBytecodeParser(data, size);
+}
diff --git a/src/protozero/filtering/filter_bytecode_parser_unittest.cc b/src/protozero/filtering/filter_bytecode_parser_unittest.cc
new file mode 100644
index 0000000..5b67ded
--- /dev/null
+++ b/src/protozero/filtering/filter_bytecode_parser_unittest.cc
@@ -0,0 +1,251 @@
+/*
+ * 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 "test/gtest_and_gmock.h"
+
+#include "perfetto/ext/base/hash.h"
+#include "perfetto/protozero/packed_repeated_fields.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "src/protozero/filtering/filter_bytecode_common.h"
+#include "src/protozero/filtering/filter_bytecode_parser.h"
+
+namespace protozero {
+
+namespace {
+
+
+bool LoadBytecode(FilterBytecodeParser* parser,
+                  std::initializer_list<uint32_t> bytecode) {
+  perfetto::base::Hash hasher;
+  protozero::PackedVarInt words;
+  for (uint32_t w : bytecode) {
+    words.Append(w);
+    hasher.Update(w);
+  }
+  words.Append(static_cast<uint32_t>(hasher.digest()));
+  return parser->Load(words.data(), words.size());
+}
+
+TEST(FilterBytecodeParserTest, ParserSimpleFields) {
+  FilterBytecodeParser parser;
+  EXPECT_FALSE(parser.Load(nullptr, 0));
+  EXPECT_FALSE(parser.Query(42, 42).allowed);
+
+  EXPECT_TRUE(LoadBytecode(&parser, {}));
+  EXPECT_FALSE(parser.Query(0, 0).allowed);
+  EXPECT_FALSE(parser.Query(0, 0xffffffff).allowed);
+  EXPECT_FALSE(parser.Query(1, 0).allowed);
+  EXPECT_FALSE(parser.Query(0, 1).allowed);
+  EXPECT_FALSE(parser.Query(1, 1).allowed);
+  EXPECT_FALSE(parser.Query(42, 42).allowed);
+
+  // An invalid field_id (0) in bytecode should cause a parse failure.
+  EXPECT_FALSE(LoadBytecode(
+      &parser, {kFilterOpcode_SimpleField | 0, kFilterOpcode_EndOfMessage}));
+
+  // A valid bytecode that has only one field.
+  EXPECT_TRUE(LoadBytecode(&parser, {kFilterOpcode_SimpleField | (2u << 3),
+                                     kFilterOpcode_EndOfMessage}));
+  EXPECT_FALSE(parser.Query(0, 0).allowed);
+  EXPECT_FALSE(parser.Query(0, 1).allowed);
+  EXPECT_TRUE(parser.Query(0, 2).allowed);
+  EXPECT_TRUE(parser.Query(0, 2).simple_field());
+  EXPECT_TRUE(parser.Query(0, 2).allowed);
+  EXPECT_FALSE(parser.Query(0, 3).allowed);
+  EXPECT_FALSE(parser.Query(1, 1).allowed);
+  EXPECT_FALSE(parser.Query(1, 2).allowed);
+  EXPECT_FALSE(parser.Query(1, 3).allowed);
+
+  // A valid bytecode that has few sparse fields < 128.
+  EXPECT_TRUE(LoadBytecode(&parser, {kFilterOpcode_SimpleField | (1u << 3),
+                                     kFilterOpcode_SimpleField | (7u << 3),
+                                     kFilterOpcode_SimpleField | (8u << 3),
+                                     kFilterOpcode_SimpleField | (127u << 3),
+                                     kFilterOpcode_EndOfMessage}));
+  EXPECT_FALSE(parser.Query(0, 0).allowed);
+  EXPECT_TRUE(parser.Query(0, 1).allowed);
+  EXPECT_FALSE(parser.Query(0, 2).allowed);
+  EXPECT_FALSE(parser.Query(0, 3).allowed);
+  EXPECT_FALSE(parser.Query(0, 6).allowed);
+  EXPECT_TRUE(parser.Query(0, 7).allowed);
+  EXPECT_TRUE(parser.Query(0, 8).allowed);
+  EXPECT_FALSE(parser.Query(0, 9).allowed);
+  EXPECT_FALSE(parser.Query(0, 126).allowed);
+  EXPECT_TRUE(parser.Query(0, 127).allowed);
+  EXPECT_FALSE(parser.Query(0, 128).allowed);
+
+  // A valid bytecode that has only fields > 128.
+  EXPECT_TRUE(LoadBytecode(&parser, {kFilterOpcode_SimpleField | (1000u << 3),
+                                     kFilterOpcode_SimpleField | (1001u << 3),
+                                     kFilterOpcode_SimpleField | (2000u << 3),
+                                     kFilterOpcode_EndOfMessage}));
+  for (uint32_t i = 0; i < 1000; ++i)
+    EXPECT_FALSE(parser.Query(0, i).allowed);
+  EXPECT_TRUE(parser.Query(0, 1000).allowed);
+  EXPECT_TRUE(parser.Query(0, 1001).allowed);
+  EXPECT_FALSE(parser.Query(0, 1002).allowed);
+  EXPECT_FALSE(parser.Query(0, 1999).allowed);
+  EXPECT_TRUE(parser.Query(0, 2000).allowed);
+  EXPECT_FALSE(parser.Query(0, 2001).allowed);
+}
+
+TEST(FilterBytecodeParserTest, ParserSimpleRanges) {
+  FilterBytecodeParser parser;
+
+  // Invalid, range length missing.
+  EXPECT_FALSE(
+      LoadBytecode(&parser, {
+                                kFilterOpcode_SimpleFieldRange | (2u << 3),
+                            }));
+
+  // Borderline valid: range length = 0.
+  EXPECT_TRUE(
+      LoadBytecode(&parser, {kFilterOpcode_SimpleFieldRange | (2u << 3), 0u,
+                             kFilterOpcode_SimpleFieldRange | (127u << 3), 0u,
+                             kFilterOpcode_SimpleFieldRange | (128u << 3), 0u,
+                             kFilterOpcode_SimpleFieldRange | (128u << 3), 0u,
+                             kFilterOpcode_EndOfMessage}));
+  for (uint32_t i = 0; i < 130; ++i)
+    EXPECT_FALSE(parser.Query(0, i).allowed) << i;
+
+  // A valid bytecode with two ranges [2,2], [10, 14].
+  EXPECT_TRUE(
+      LoadBytecode(&parser, {kFilterOpcode_SimpleFieldRange | (2u << 3),
+                             1u,  // length of the range,
+                             kFilterOpcode_SimpleFieldRange | (10u << 3),
+                             5u,  // length of the range,
+                             kFilterOpcode_EndOfMessage}));
+  EXPECT_FALSE(parser.Query(0, 0).allowed);
+  EXPECT_FALSE(parser.Query(0, 1).allowed);
+  EXPECT_TRUE(parser.Query(0, 2).allowed);
+  EXPECT_TRUE(parser.Query(0, 2).simple_field());
+  EXPECT_FALSE(parser.Query(0, 3).allowed);
+  EXPECT_FALSE(parser.Query(0, 9).allowed);
+  for (uint32_t i = 10; i <= 14; ++i)
+    EXPECT_TRUE(parser.Query(0, i).allowed);
+  EXPECT_FALSE(parser.Query(0, 15).allowed);
+}
+
+TEST(FilterBytecodeParserTest, ParserSimpleFieldsAndRanges) {
+  FilterBytecodeParser parser;
+
+  // Borderline valid: range length = 0.
+  EXPECT_TRUE(
+      LoadBytecode(&parser, {kFilterOpcode_SimpleFieldRange | (1u << 3),
+                             2u,  // [1,2]
+
+                             kFilterOpcode_SimpleField | (4u << 3),
+
+                             kFilterOpcode_SimpleFieldRange | (126u << 3),
+                             4u,  // [126, 129]
+
+                             kFilterOpcode_SimpleField | (150u << 3),
+
+                             kFilterOpcode_EndOfMessage}));
+  EXPECT_TRUE(parser.Query(0, 1).allowed);
+  EXPECT_TRUE(parser.Query(0, 2).allowed);
+  EXPECT_FALSE(parser.Query(0, 3).allowed);
+  EXPECT_TRUE(parser.Query(0, 4).allowed);
+  EXPECT_FALSE(parser.Query(0, 5).allowed);
+  EXPECT_FALSE(parser.Query(0, 125).allowed);
+  for (uint32_t i = 126; i <= 129; ++i)
+    EXPECT_TRUE(parser.Query(0, i).allowed) << i;
+  EXPECT_FALSE(parser.Query(0, 130).allowed);
+  EXPECT_TRUE(parser.Query(0, 150).allowed);
+}
+
+TEST(FilterBytecodeParserTest, ParserNestedMessages) {
+  FilterBytecodeParser parser;
+
+  // Invalid because there are 1 messages in total, and message index 1 is
+  // out of range.
+  EXPECT_FALSE(LoadBytecode(&parser, {kFilterOpcode_NestedField | (4u << 3),
+                                      1u,  // message index
+                                      kFilterOpcode_EndOfMessage}));
+
+  // A valid bytecode consisting of 4 message, with recursive / cylical
+  // dependencies between them.
+  EXPECT_TRUE(LoadBytecode(
+      &parser, {
+                   // Message 0 (root).
+                   kFilterOpcode_SimpleFieldRange | (1u << 3),
+                   2u,  // [1,2]
+                   kFilterOpcode_NestedField | (4u << 3),
+                   3u,  // message index
+                   kFilterOpcode_SimpleField | (10u << 3),
+                   kFilterOpcode_NestedField | (127u << 3),
+                   1u,  // message index
+                   kFilterOpcode_NestedField | (128u << 3),
+                   2u,  // message index
+                   kFilterOpcode_EndOfMessage,
+
+                   // Message 1.
+                   kFilterOpcode_NestedField | (2u << 3),
+                   1u,  // message index (recurse onto itself),
+                   kFilterOpcode_SimpleField | (11u << 3),
+                   kFilterOpcode_EndOfMessage,
+
+                   // Message 2.
+                   kFilterOpcode_NestedField | (2u << 3),
+                   3u,  // message index.
+                   kFilterOpcode_EndOfMessage,
+
+                   // Message 3.
+                   kFilterOpcode_NestedField | (2u << 3),
+                   2u,  // message index (create a cycle, 2->3, 3->2).
+                   kFilterOpcode_EndOfMessage,
+               }));
+
+  // Query message 0 fields.
+  EXPECT_TRUE(parser.Query(0, 1).allowed);
+  EXPECT_TRUE(parser.Query(0, 2).allowed);
+  EXPECT_TRUE(parser.Query(0, 2).simple_field());
+  EXPECT_TRUE(parser.Query(0, 4).allowed);
+  EXPECT_FALSE(parser.Query(0, 4).simple_field());
+  EXPECT_EQ(parser.Query(0, 4).nested_msg_index, 3u);
+  EXPECT_TRUE(parser.Query(0, 10).allowed);
+  EXPECT_TRUE(parser.Query(0, 10).simple_field());
+  EXPECT_TRUE(parser.Query(0, 127).allowed);
+  EXPECT_EQ(parser.Query(0, 127).nested_msg_index, 1u);
+  EXPECT_TRUE(parser.Query(0, 128).allowed);
+  EXPECT_EQ(parser.Query(0, 128).nested_msg_index, 2u);
+  EXPECT_FALSE(parser.Query(0, 129).allowed);
+
+  // Query message 1 fields.
+  EXPECT_FALSE(parser.Query(1, 1).allowed);
+  EXPECT_TRUE(parser.Query(1, 2).allowed);
+  EXPECT_EQ(parser.Query(1, 2).nested_msg_index, 1u);
+  EXPECT_FALSE(parser.Query(1, 3).allowed);
+  EXPECT_TRUE(parser.Query(1, 11).allowed);
+  EXPECT_TRUE(parser.Query(1, 11).simple_field());
+
+  // Query message 2 fields.
+  EXPECT_FALSE(parser.Query(2, 0).allowed);
+  EXPECT_FALSE(parser.Query(2, 1).allowed);
+  EXPECT_TRUE(parser.Query(2, 2).allowed);
+  EXPECT_EQ(parser.Query(2, 2).nested_msg_index, 3u);
+  EXPECT_FALSE(parser.Query(2, 4).allowed);
+
+  // Query message 3 fields.
+  EXPECT_FALSE(parser.Query(3, 0).allowed);
+  EXPECT_FALSE(parser.Query(3, 1).allowed);
+  EXPECT_TRUE(parser.Query(3, 2).allowed);
+  EXPECT_EQ(parser.Query(3, 2).nested_msg_index, 2u);
+  EXPECT_FALSE(parser.Query(3, 4).allowed);
+}
+
+}  // namespace
+}  // namespace protozero
diff --git a/src/protozero/filtering/filter_util.cc b/src/protozero/filtering/filter_util.cc
new file mode 100644
index 0000000..94d436e
--- /dev/null
+++ b/src/protozero/filtering/filter_util.cc
@@ -0,0 +1,323 @@
+/*
+ * 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/protozero/filtering/filter_util.h"
+
+#include <algorithm>
+#include <map>
+#include <memory>
+#include <set>
+
+#include <google/protobuf/compiler/importer.h>
+
+#include "perfetto/base/build_config.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/getopt.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/version.h"
+#include "perfetto/protozero/proto_utils.h"
+#include "src/protozero/filtering/filter_bytecode_generator.h"
+
+namespace protozero {
+
+namespace {
+
+class MultiFileErrorCollectorImpl
+    : public google::protobuf::compiler::MultiFileErrorCollector {
+ public:
+  ~MultiFileErrorCollectorImpl() override;
+  void AddError(const std::string&, int, int, const std::string&) override;
+  void AddWarning(const std::string&, int, int, const std::string&) override;
+};
+
+MultiFileErrorCollectorImpl::~MultiFileErrorCollectorImpl() = default;
+
+void MultiFileErrorCollectorImpl::AddError(const std::string& filename,
+                                           int line,
+                                           int column,
+                                           const std::string& message) {
+  PERFETTO_ELOG("Error %s %d:%d: %s", filename.c_str(), line, column,
+                message.c_str());
+}
+
+void MultiFileErrorCollectorImpl::AddWarning(const std::string& filename,
+                                             int line,
+                                             int column,
+                                             const std::string& message) {
+  PERFETTO_ELOG("Warning %s %d:%d: %s", filename.c_str(), line, column,
+                message.c_str());
+}
+
+}  // namespace
+
+FilterUtil::FilterUtil() = default;
+FilterUtil::~FilterUtil() = default;
+
+bool FilterUtil::LoadMessageDefinition(const std::string& proto_file,
+                                       const std::string& root_message,
+                                       const std::string& proto_dir_path) {
+  // The protobuf compiler doesn't like backslashes and prints an error like:
+  // Error C:\it7mjanpw3\perfetto-a16500 -1:0: Backslashes, consecutive slashes,
+  // ".", or ".." are not allowed in the virtual path.
+  // Given that C:\foo\bar is a legit path on windows, fix it at this level
+  // because the problem is really the protobuf compiler being too picky.
+  static auto normalize_for_win = [](const std::string& path) {
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+    return perfetto::base::ReplaceAll(path, "\\", "/");
+#else
+    return path;
+#endif
+  };
+
+  google::protobuf::compiler::DiskSourceTree dst;
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  // If the path is absolute, maps "C:/" -> "C:/" (without hardcoding 'C').
+  if (proto_file.size() > 3 && proto_file[1] == ':') {
+    char win_drive[4];
+    sprintf(win_drive, "%c:/", proto_file[0]);
+    dst.MapPath(win_drive, win_drive);
+  }
+#endif
+  dst.MapPath("/", "/");  // We might still need this on Win under cygwin.
+  dst.MapPath("", normalize_for_win(proto_dir_path));
+  MultiFileErrorCollectorImpl mfe;
+  google::protobuf::compiler::Importer importer(&dst, &mfe);
+  const google::protobuf::FileDescriptor* root_file =
+      importer.Import(normalize_for_win(proto_file));
+  const google::protobuf::Descriptor* root_msg = nullptr;
+  if (!root_message.empty()) {
+    root_msg = importer.pool()->FindMessageTypeByName(root_message);
+  } else if (root_file->message_type_count() > 0) {
+    // The user didn't specfy the root type. Pick the first type in the file,
+    // most times it's the right guess.
+    root_msg = root_file->message_type(0);
+    if (root_msg)
+      PERFETTO_LOG(
+          "The guessed root message name is \"%s\". Pass -r com.MyName to "
+          "override",
+          root_msg->full_name().c_str());
+  }
+
+  if (!root_msg) {
+    PERFETTO_ELOG("Could not find the root message \"%s\" in %s",
+                  root_message.c_str(), proto_file.c_str());
+    return false;
+  }
+
+  // |descriptors_by_full_name| is passed by argument rather than being a member
+  // field so that we don't risk leaving it out of sync (and depending on it in
+  // future without realizing) when performing the Dedupe() pass.
+  DescriptorsByNameMap descriptors_by_full_name;
+  ParseProtoDescriptor(root_msg, &descriptors_by_full_name);
+  return true;
+}
+
+// Generates a Message object for the given libprotobuf message descriptor.
+// Recurses as needed into nested fields.
+FilterUtil::Message* FilterUtil::ParseProtoDescriptor(
+    const google::protobuf::Descriptor* proto,
+    DescriptorsByNameMap* descriptors_by_full_name) {
+  auto descr_it = descriptors_by_full_name->find(proto->full_name());
+  if (descr_it != descriptors_by_full_name->end())
+    return descr_it->second;
+
+  descriptors_.emplace_back();
+  Message* msg = &descriptors_.back();
+  msg->full_name = proto->full_name();
+  (*descriptors_by_full_name)[msg->full_name] = msg;
+  for (int i = 0; i < proto->field_count(); ++i) {
+    const auto* proto_field = proto->field(i);
+    const uint32_t field_id = static_cast<uint32_t>(proto_field->number());
+    PERFETTO_CHECK(msg->fields.count(field_id) == 0);
+    auto& field = msg->fields[field_id];
+    field.name = proto_field->name();
+    field.type = proto_field->type_name();
+    if (proto_field->message_type()) {
+      msg->has_nested_fields = true;
+      // Recurse.
+      field.nested_type = ParseProtoDescriptor(proto_field->message_type(),
+                                               descriptors_by_full_name);
+    }
+  }
+  return msg;
+}
+
+void FilterUtil::Dedupe() {
+  std::map<std::string /*identity*/, Message*> index;
+
+  std::map<Message*, Message*> dupe_graph;  // K,V: K shall be duped against V.
+
+  // As a first pass, generate an |identity| string for each leaf message. The
+  // identity is simply the comma-separated stringification of its field ids.
+  // If another message with the same identity exists, add an edge to the graph.
+  const size_t initial_count = descriptors_.size();
+  size_t field_count = 0;
+  for (auto& descr : descriptors_) {
+    if (descr.has_nested_fields)
+      continue;  // Dedupe only leaf messages without nested fields.
+    std::string identity;
+    for (const auto& id_and_field : descr.fields)
+      identity.append(std::to_string(id_and_field.first) + ",");
+    auto it_and_inserted = index.emplace(identity, &descr);
+    if (!it_and_inserted.second) {
+      // insertion failed, a dupe exists already.
+      Message* dupe_against = it_and_inserted.first->second;
+      dupe_graph.emplace(&descr, dupe_against);
+    }
+  }
+
+  // Now apply de-duplications by re-directing the nested_type pointer to the
+  // equivalent descriptors that have the same set of allowed field ids.
+  std::set<Message*> referenced_descriptors;
+  referenced_descriptors.emplace(&descriptors_.front());  // The root.
+  for (auto& descr : descriptors_) {
+    for (auto& id_and_field : descr.fields) {
+      Message* target = id_and_field.second.nested_type;
+      if (!target)
+        continue;  // Only try to dedupe nested types.
+      auto it = dupe_graph.find(target);
+      if (it == dupe_graph.end()) {
+        referenced_descriptors.emplace(target);
+        continue;
+      }
+      ++field_count;
+      // Replace with the dupe.
+      id_and_field.second.nested_type = it->second;
+    }  // for (nested_fields).
+  }    // for (descriptors_).
+
+  // Remove unreferenced descriptors. We should much rather crash in the case of
+  // a logic bug rathern than trying to use them but don't emit them.
+  size_t removed_count = 0;
+  for (auto it = descriptors_.begin(); it != descriptors_.end();) {
+    if (referenced_descriptors.count(&*it)) {
+      ++it;
+    } else {
+      ++removed_count;
+      it = descriptors_.erase(it);
+    }
+  }
+  PERFETTO_LOG(
+      "Deduplication removed %zu duped descriptors out of %zu descriptors from "
+      "%zu fields",
+      removed_count, initial_count, field_count);
+}
+
+// Prints the list of messages and fields in a diff-friendly text format.
+void FilterUtil::PrintAsText() {
+  using perfetto::base::StripPrefix;
+  const std::string& root_name = descriptors_.front().full_name;
+  std::string root_prefix = root_name.substr(0, root_name.rfind('.'));
+  if (!root_prefix.empty())
+    root_prefix.append(".");
+
+  for (const auto& descr : descriptors_) {
+    for (const auto& id_and_field : descr.fields) {
+      const uint32_t field_id = id_and_field.first;
+      const auto& field = id_and_field.second;
+      const Message* nested_type = id_and_field.second.nested_type;
+      auto stripped_name = StripPrefix(descr.full_name, root_prefix);
+      std::string stripped_nested =
+          nested_type ? StripPrefix(nested_type->full_name, root_prefix) : "";
+      printf("%-60s %3u %-8s %-32s %s\n", stripped_name.c_str(), field_id,
+             field.type.c_str(), field.name.c_str(), stripped_nested.c_str());
+    }
+  }
+}
+
+std::string FilterUtil::GenerateFilterBytecode() {
+  protozero::FilterBytecodeGenerator bytecode_gen;
+
+  // Assign indexes to descriptors, simply by counting them in order;
+  std::map<Message*, uint32_t> descr_to_idx;
+  for (auto& descr : descriptors_)
+    descr_to_idx[&descr] = static_cast<uint32_t>(descr_to_idx.size());
+
+  for (auto& descr : descriptors_) {
+    for (auto it = descr.fields.begin(); it != descr.fields.end();) {
+      uint32_t field_id = it->first;
+      const Message::Field& field = it->second;
+      if (field.nested_type) {
+        // Append the index of the target submessage.
+        PERFETTO_CHECK(descr_to_idx.count(field.nested_type));
+        uint32_t nested_msg_index = descr_to_idx[field.nested_type];
+        bytecode_gen.AddNestedField(field_id, nested_msg_index);
+        ++it;
+        continue;
+      }
+      // Simple field. Lookahead to see if we have a range of contiguous simple
+      // fields.
+      for (uint32_t range_len = 1;; ++range_len) {
+        ++it;
+        if (it != descr.fields.end() && it->first == field_id + range_len &&
+            it->second.is_simple()) {
+          continue;
+        }
+        // At this point it points to either the end() of the vector or a
+        // non-contiguous or non-simple field (which will be picked up by the
+        // next iteration).
+        if (range_len == 1) {
+          bytecode_gen.AddSimpleField(field_id);
+        } else {
+          bytecode_gen.AddSimpleFieldRange(field_id, range_len);
+        }
+        break;
+      }  // for (range_len)
+    }    // for (descr.fields)
+    bytecode_gen.EndMessage();
+  }  // for (descriptors)
+  return bytecode_gen.Serialize();
+}
+
+std::string FilterUtil::LookupField(const std::string& varint_encoded_path) {
+  const uint8_t* ptr =
+      reinterpret_cast<const uint8_t*>(varint_encoded_path.data());
+  const uint8_t* const end = ptr + varint_encoded_path.size();
+
+  std::vector<uint32_t> fields;
+  while (ptr < end) {
+    uint64_t varint;
+    const uint8_t* next = proto_utils::ParseVarInt(ptr, end, &varint);
+    PERFETTO_CHECK(next != ptr);
+    fields.emplace_back(static_cast<uint32_t>(varint));
+    ptr = next;
+  }
+  return LookupField(fields.data(), fields.size());
+}
+
+std::string FilterUtil::LookupField(const uint32_t* field_ids,
+                                    size_t num_fields) {
+  const Message* msg = descriptors_.empty() ? nullptr : &descriptors_.front();
+  std::string res;
+  for (size_t i = 0; i < num_fields; ++i) {
+    const uint32_t field_id = field_ids[i];
+    const Message::Field* field = nullptr;
+    if (msg) {
+      auto it = msg->fields.find(field_id);
+      field = it == msg->fields.end() ? nullptr : &it->second;
+    }
+    res.append(".");
+    if (field) {
+      res.append(field->name);
+      msg = field->nested_type;
+    } else {
+      res.append(std::to_string(field_id));
+    }
+  }
+  return res;
+}
+
+}  // namespace protozero
diff --git a/src/protozero/filtering/filter_util.h b/src/protozero/filtering/filter_util.h
new file mode 100644
index 0000000..3cc7655
--- /dev/null
+++ b/src/protozero/filtering/filter_util.h
@@ -0,0 +1,107 @@
+/*
+ * 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_PROTOZERO_FILTERING_FILTER_UTIL_H_
+#define SRC_PROTOZERO_FILTERING_FILTER_UTIL_H_
+
+#include <stdint.h>
+
+#include <list>
+#include <map>
+#include <string>
+
+namespace google {
+namespace protobuf {
+class Descriptor;
+}
+}  // namespace google
+
+namespace protozero {
+
+// Parses a .proto message definition, recursing into its sub-messages, and
+// builds up a set of Messages and Field definitions.
+// Depends on libprotobuf-full and should be used only in host tools.
+// See the //tools/proto_filter for an executable that wraps this class with
+// a cmdline interface.
+class FilterUtil {
+ public:
+  FilterUtil();
+  ~FilterUtil();
+
+  // Loads a message schema from a .proto file, recursing into nested types.
+  // Args:
+  // proto_file: path to the .proto file.
+  // root_message: fully qualified message name (e.g., perfetto.protos.Trace).
+  //     If empty, the first message in the file will be used.
+  // proto_dir_path: the root for .proto includes. If empty uses CWD.
+  bool LoadMessageDefinition(const std::string& proto_file,
+                             const std::string& root_message,
+                             const std::string& proto_dir_path);
+
+  // Deduplicates leaf messages having the same sets of field ids.
+  // It changes the internal state and affects the behavior of next calls to
+  // GenerateFilterBytecode() and PrintAsText().
+  void Dedupe();
+
+  // Generates the filter bytecode for the root message previously loaded by
+  // LoadMessageDefinition() using FilterBytecodeGenerator.
+  // The returned string is a binary-encoded proto message of type
+  // perfetto.protos.ProtoFilter (see proto_filter.proto).
+  std::string GenerateFilterBytecode();
+
+  // Prints the list of messages and fields onto stdout in a diff-friendly text
+  // format. Example:
+  // PowerRails                 2 message  energy_data     PowerRails.EnergyData
+  // PowerRails.RailDescriptor  1 uint32   index
+  void PrintAsText();
+
+  // Resolves an array of field ids into a dot-concatenated field names.
+  // E.g., [2,5,1] -> ".trace.packet.timestamp".
+  std::string LookupField(const uint32_t* field_ids, size_t num_fields);
+
+  // Like the above but the array of field is passed as a buffer containing
+  // varints, e.g. "\x02\x05\0x01".
+  std::string LookupField(const std::string& varint_encoded_path);
+
+ private:
+  struct Message {
+    struct Field {
+      std::string name;
+      std::string type;  // "uint32", "string", "message"
+      // Only when type == "message". Note that when using Dedupe() this can
+      // be aliased against a different submessage which happens to have the
+      // same set of field ids.
+      Message* nested_type = nullptr;
+      bool is_simple() const { return nested_type == nullptr; }
+    };
+    std::string full_name;  // e.g., "perfetto.protos.Foo.Bar";
+    std::map<uint32_t /*field_id*/, Field> fields;
+
+    // True if at least one field has a non-null |nestd_type|.
+    bool has_nested_fields = false;
+  };
+
+  using DescriptorsByNameMap = std::map<std::string, Message*>;
+  Message* ParseProtoDescriptor(const google::protobuf::Descriptor*,
+                                DescriptorsByNameMap*);
+
+  // list<> because pointers need to be stable.
+  std::list<Message> descriptors_;
+};
+
+}  // namespace protozero
+
+#endif  // SRC_PROTOZERO_FILTERING_FILTER_UTIL_H_
diff --git a/src/protozero/filtering/filter_util_unittest.cc b/src/protozero/filtering/filter_util_unittest.cc
new file mode 100644
index 0000000..9c7cf0d
--- /dev/null
+++ b/src/protozero/filtering/filter_util_unittest.cc
@@ -0,0 +1,182 @@
+/*
+ * 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 "test/gtest_and_gmock.h"
+
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/temp_file.h"
+#include "src/protozero/filtering/filter_bytecode_parser.h"
+#include "src/protozero/filtering/filter_util.h"
+
+namespace protozero {
+
+namespace {
+
+perfetto::base::TempFile MkTemp(const char* str) {
+  auto tmp = perfetto::base::TempFile::Create();
+  perfetto::base::WriteAll(*tmp, str, strlen(str));
+  perfetto::base::FlushFile(*tmp);
+  return tmp;
+}
+
+TEST(SchemaParserTest, SchemaToBytecode_Simple) {
+  auto schema = MkTemp(R"(
+  syntax = "proto2";
+  message Root {
+    optional int32 i32 = 13;
+    optional fixed64 f64 = 5;
+    optional string str = 71;
+  }
+  )");
+  FilterUtil filter;
+  ASSERT_TRUE(filter.LoadMessageDefinition(schema.path(), "Root", ""));
+  std::string bytecode = filter.GenerateFilterBytecode();
+  FilterBytecodeParser fbp;
+  ASSERT_TRUE(fbp.Load(bytecode.data(), bytecode.size()));
+  EXPECT_TRUE(fbp.Query(0, 13).allowed);
+  EXPECT_TRUE(fbp.Query(0, 13).simple_field());
+  EXPECT_TRUE(fbp.Query(0, 5).allowed);
+  EXPECT_TRUE(fbp.Query(0, 5).simple_field());
+  EXPECT_TRUE(fbp.Query(0, 71).allowed);
+  EXPECT_TRUE(fbp.Query(0, 71).simple_field());
+  EXPECT_FALSE(fbp.Query(0, 1).allowed);
+  EXPECT_FALSE(fbp.Query(0, 12).allowed);
+  EXPECT_FALSE(fbp.Query(0, 70).allowed);
+}
+
+TEST(SchemaParserTest, SchemaToBytecode_Nested) {
+  auto schema = MkTemp(R"(
+  syntax = "proto2";
+  message Root {
+    message Child {
+      repeated fixed64 f64 = 3;
+      optional Child recurse = 4;
+    }
+    oneof xxx { int32 i32 = 1; }
+    optional Child chld = 2;
+  }
+  )");
+  FilterUtil filter;
+  ASSERT_TRUE(filter.LoadMessageDefinition(schema.path(), "", ""));
+  std::string bytecode = filter.GenerateFilterBytecode();
+  FilterBytecodeParser fbp;
+  ASSERT_TRUE(fbp.Load(bytecode.data(), bytecode.size()));
+  EXPECT_TRUE(fbp.Query(0, 1).allowed);
+  EXPECT_TRUE(fbp.Query(0, 1).simple_field());
+  EXPECT_TRUE(fbp.Query(0, 2).allowed);
+  EXPECT_FALSE(fbp.Query(0, 2).simple_field());
+  // False as those fields exist only in Child, not in the root (0).
+  EXPECT_FALSE(fbp.Query(0, 3).allowed);
+  EXPECT_FALSE(fbp.Query(0, 4).allowed);
+
+  EXPECT_TRUE(fbp.Query(1, 3).allowed);
+  EXPECT_TRUE(fbp.Query(1, 3).simple_field());
+  EXPECT_TRUE(fbp.Query(1, 4).allowed);
+  EXPECT_FALSE(fbp.Query(1, 4).simple_field());
+  EXPECT_EQ(fbp.Query(1, 4).nested_msg_index, 1u);  // Self
+}
+
+TEST(SchemaParserTest, SchemaToBytecode_Dedupe) {
+  auto schema = MkTemp(R"(
+  syntax = "proto2";
+  message Root {
+    message Nested {
+      message Child1 {
+        optional int32 f1 = 3;
+        optional int64 f2 = 4;
+      }
+      message Child2 {
+        optional string f1 = 3;
+        optional bytes f2 = 4;
+      }
+      message ChildNonDedupe {
+        optional string f1 = 3;
+        optional bytes f2 = 4;
+        optional int32 extra = 1;
+      }
+      optional Child1 chld1 = 1;
+      optional Child2 chld2 = 2;
+      optional ChildNonDedupe chld3 = 3;
+    }
+    repeated Nested nested = 1;
+  }
+  )");
+  FilterUtil filter;
+  ASSERT_TRUE(filter.LoadMessageDefinition(schema.path(), "Root", ""));
+  filter.Dedupe();
+  std::string bytecode = filter.GenerateFilterBytecode();
+  FilterBytecodeParser fbp;
+  ASSERT_TRUE(fbp.Load(bytecode.data(), bytecode.size()));
+
+  // 0: Root
+  EXPECT_TRUE(fbp.Query(0, 1).allowed);
+  EXPECT_FALSE(fbp.Query(0, 1).simple_field());
+
+  // 1: Nested
+  EXPECT_TRUE(fbp.Query(1, 1).allowed);
+  EXPECT_FALSE(fbp.Query(1, 1).simple_field());
+  EXPECT_TRUE(fbp.Query(1, 2).allowed);
+  EXPECT_FALSE(fbp.Query(1, 2).simple_field());
+  EXPECT_TRUE(fbp.Query(1, 3).allowed);
+  EXPECT_FALSE(fbp.Query(1, 3).simple_field());
+
+  // Check deduping.
+  // Fields chld1 and chld2 should point to the same sub-filter because they
+  // have the same field ids.
+  EXPECT_EQ(fbp.Query(1, 1).nested_msg_index, fbp.Query(1, 2).nested_msg_index);
+
+  // Field chld3 should point to a different one because it has an extra field.
+  EXPECT_NE(fbp.Query(1, 1).nested_msg_index, fbp.Query(1, 3).nested_msg_index);
+}
+
+TEST(SchemaParserTest, FieldLookup) {
+  auto schema = MkTemp(R"(
+  syntax = "proto2";
+  message Root {
+    message Nested {
+      message Child1 {
+        optional int32 f1 = 3;
+        optional int64 f2 = 4;
+        repeated Child2 c2 = 5;
+      }
+      message Child2 {
+        optional string f3 = 6;
+        optional bytes f4 = 7;
+        repeated Child1 c1 = 8;
+      }
+      optional Child1 x1 = 1;
+      optional Child2 x2 = 2;
+    }
+    repeated Nested n = 1;
+  }
+  )");
+
+  FilterUtil filter;
+  ASSERT_TRUE(filter.LoadMessageDefinition(schema.path(), "Root", ""));
+  std::vector<uint32_t> fld;
+
+  fld = {1, 1, 3};
+  ASSERT_EQ(filter.LookupField(fld.data(), fld.size()), ".n.x1.f1");
+
+  fld = {1, 2, 7};
+  ASSERT_EQ(filter.LookupField(fld.data(), fld.size()), ".n.x2.f4");
+
+  fld = {1, 2, 8, 5, 8, 5, 7};
+  ASSERT_EQ(filter.LookupField(fld.data(), fld.size()), ".n.x2.c1.c2.c1.c2.f4");
+}
+
+}  // namespace
+}  // namespace protozero
diff --git a/src/protozero/filtering/message_filter.cc b/src/protozero/filtering/message_filter.cc
new file mode 100644
index 0000000..a971dfa
--- /dev/null
+++ b/src/protozero/filtering/message_filter.cc
@@ -0,0 +1,310 @@
+/*
+ * 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/protozero/filtering/message_filter.h"
+
+#include "perfetto/base/logging.h"
+#include "perfetto/protozero/proto_utils.h"
+
+namespace protozero {
+
+namespace {
+
+// Inline helpers to append proto fields in output. They are the equivalent of
+// the protozero::Message::AppendXXX() fields but don't require building and
+// maintaining a full protozero::Message object or dealing with scattered
+// output slices.
+// All these functions assume there is enough space in the output buffer, which
+// should be always the case assuming that we don't end up generating more
+// output than input.
+
+inline void AppendVarInt(uint32_t field_id, uint64_t value, uint8_t** out) {
+  *out = proto_utils::WriteVarInt(proto_utils::MakeTagVarInt(field_id), *out);
+  *out = proto_utils::WriteVarInt(value, *out);
+}
+
+// For fixed32 / fixed64.
+template <typename INT_T /* uint32_t | uint64_t*/>
+inline void AppendFixed(uint32_t field_id, INT_T value, uint8_t** out) {
+  *out = proto_utils::WriteVarInt(proto_utils::MakeTagFixed<INT_T>(field_id),
+                                  *out);
+  memcpy(*out, &value, sizeof(value));
+  *out += sizeof(value);
+}
+
+// For length-delimited (string, bytes) fields. Note: this function appends only
+// the proto preamble and the varint field that states the length of the payload
+// not the payload itself.
+// In the case of submessages, the caller needs to re-write the length at the
+// end in the in the returned memory area.
+// The problem here is that, because of filtering, the length of a submessage
+// might be < original length (the original length is still an upper-bound).
+// Returns a pair with: (1) the pointer where the final length should be written
+// into, (2) the length of the size field.
+// The caller must write a redundant varint to match the original size (i.e.
+// needs to use WriteRedundantVarInt()).
+inline std::pair<uint8_t*, uint32_t> AppendLenDelim(uint32_t field_id,
+                                                    uint32_t len,
+                                                    uint8_t** out) {
+  *out = proto_utils::WriteVarInt(proto_utils::MakeTagLengthDelimited(field_id),
+                                  *out);
+  uint8_t* size_field_start = *out;
+  *out = proto_utils::WriteVarInt(len, *out);
+  const size_t size_field_len = static_cast<size_t>(*out - size_field_start);
+  return std::make_pair(size_field_start, size_field_len);
+}
+}  // namespace
+
+MessageFilter::MessageFilter() {
+  // Push a state on the stack for the implicit root message.
+  stack_.emplace_back();
+}
+
+MessageFilter::~MessageFilter() = default;
+
+bool MessageFilter::LoadFilterBytecode(const void* filter_data, size_t len) {
+  return filter_.Load(filter_data, len);
+}
+
+bool MessageFilter::SetFilterRoot(const uint32_t* field_ids,
+                                  size_t num_fields) {
+  uint32_t root_msg_idx = 0;
+  for (const uint32_t* it = field_ids; it < field_ids + num_fields; ++it) {
+    uint32_t field_id = *it;
+    auto res = filter_.Query(root_msg_idx, field_id);
+    if (!res.allowed || res.simple_field())
+      return false;
+    root_msg_idx = res.nested_msg_index;
+  }
+  root_msg_index_ = root_msg_idx;
+  return true;
+}
+
+MessageFilter::FilteredMessage MessageFilter::FilterMessageFragments(
+    const InputSlice* slices,
+    size_t num_slices) {
+  // First compute the upper bound for the output. The filtered message cannot
+  // be > the original message.
+  uint32_t total_len = 0;
+  for (size_t i = 0; i < num_slices; ++i)
+    total_len += slices[i].len;
+  out_buf_.reset(new uint8_t[total_len]);
+  out_ = out_buf_.get();
+  out_end_ = out_ + total_len;
+
+  // Reset the parser state.
+  tokenizer_ = MessageTokenizer();
+  error_ = false;
+  stack_.clear();
+  stack_.resize(2);
+  // stack_[0] is a sentinel and should never be hit in nominal cases. If we
+  // end up there we will just keep consuming the input stream and detecting
+  // at the end, without hurting the fastpath.
+  stack_[0].in_bytes_limit = UINT32_MAX;
+  stack_[0].eat_next_bytes = UINT32_MAX;
+  // stack_[1] is the actual root message.
+  stack_[1].in_bytes_limit = total_len;
+  stack_[1].msg_index = root_msg_index_;
+
+  // Process the input data and write the output.
+  for (size_t slice_idx = 0; slice_idx < num_slices; ++slice_idx) {
+    const InputSlice& slice = slices[slice_idx];
+    const uint8_t* data = static_cast<const uint8_t*>(slice.data);
+    for (size_t i = 0; i < slice.len; ++i)
+      FilterOneByte(data[i]);
+  }
+
+  // Construct the output object.
+  PERFETTO_CHECK(out_ >= out_buf_.get() && out_ <= out_end_);
+  auto used_size = static_cast<size_t>(out_ - out_buf_.get());
+  FilteredMessage res{std::move(out_buf_), used_size};
+  res.error = error_;
+  if (stack_.size() != 1 || !tokenizer_.idle() ||
+      stack_[0].in_bytes != total_len) {
+    res.error = true;
+  }
+  return res;
+}
+
+void MessageFilter::FilterOneByte(uint8_t octet) {
+  PERFETTO_DCHECK(!stack_.empty());
+
+  auto* state = &stack_.back();
+  StackState next_state{};
+  bool push_next_state = false;
+
+  if (state->eat_next_bytes > 0) {
+    // This is the case where the previous tokenizer_.Push() call returned a
+    // length delimited message which is NOT a submessage (a string or a bytes
+    // field). We just want to consume it, and pass it through in output
+    // if the field was allowed.
+    --state->eat_next_bytes;
+    if (state->passthrough_eaten_bytes)
+      *(out_++) = octet;
+  } else {
+    MessageTokenizer::Token token = tokenizer_.Push(octet);
+    // |token| will not be valid() in most cases and this is WAI. When pushing
+    // a varint field, only the last byte yields a token, all the other bytes
+    // return an invalid token, they just update the internal tokenizer state.
+    if (token.valid()) {
+      auto filter = filter_.Query(state->msg_index, token.field_id);
+      switch (token.type) {
+        case proto_utils::ProtoWireType::kVarInt:
+          if (filter.allowed && filter.simple_field())
+            AppendVarInt(token.field_id, token.value, &out_);
+          break;
+        case proto_utils::ProtoWireType::kFixed32:
+          if (filter.allowed && filter.simple_field())
+            AppendFixed(token.field_id, static_cast<uint32_t>(token.value),
+                        &out_);
+          break;
+        case proto_utils::ProtoWireType::kFixed64:
+          if (filter.allowed && filter.simple_field())
+            AppendFixed(token.field_id, static_cast<uint64_t>(token.value),
+                        &out_);
+          break;
+        case proto_utils::ProtoWireType::kLengthDelimited:
+          // Here we have two cases:
+          // A. A simple string/bytes field: we just want to consume the next
+          //    bytes (the string payload), optionally passing them through in
+          //    output if the field is allowed.
+          // B. This is a nested submessage. In this case we want to recurse and
+          //    push a new state on the stack.
+          // Note that we can't tell the difference between a
+          // "non-allowed string" and a "non-allowed submessage". But it doesn't
+          // matter because in both cases we just want to skip the next N bytes.
+          const auto submessage_len = static_cast<uint32_t>(token.value);
+          auto in_bytes_left = state->in_bytes_limit - state->in_bytes - 1;
+          if (PERFETTO_UNLIKELY(submessage_len > in_bytes_left)) {
+            // This is a malicious / malformed string/bytes/submessage that
+            // claims to be larger than the outer message that contains it.
+            return SetUnrecoverableErrorState();
+          }
+
+          if (filter.allowed && !filter.simple_field() && submessage_len > 0) {
+            // submessage_len == 0 is the edge case of a message with a 0-len
+            // (but present) submessage. In this case, if allowed, we don't want
+            // to push any further state (doing so would desync the FSM) but we
+            // still want to emit it.
+            // At this point |submessage_len| is only an upper bound. The
+            // final message written in output can be <= the one in input,
+            // only some of its fields might be allowed (also remember that
+            // this class implicitly removes redundancy varint encoding of
+            // len-delimited field lengths). The final length varint (the
+            // return value of AppendLenDelim()) will be filled when popping
+            // from |stack_|.
+            auto size_field =
+                AppendLenDelim(token.field_id, submessage_len, &out_);
+            push_next_state = true;
+            next_state.field_id = token.field_id;
+            next_state.msg_index = filter.nested_msg_index;
+            next_state.in_bytes_limit = submessage_len;
+            next_state.size_field = size_field.first;
+            next_state.size_field_len = size_field.second;
+            next_state.out_bytes_written_at_start = out_written();
+          } else {
+            // A string or bytes field, or a 0 length submessage.
+            state->eat_next_bytes = submessage_len;
+            state->passthrough_eaten_bytes = filter.allowed;
+            if (filter.allowed)
+              AppendLenDelim(token.field_id, submessage_len, &out_);
+          }
+          break;
+      }  // switch(type)
+
+      if (PERFETTO_UNLIKELY(track_field_usage_)) {
+        IncrementCurrentFieldUsage(token.field_id, filter.allowed);
+      }
+    }  // if (token.valid)
+  }    // if (eat_next_bytes == 0)
+
+  ++state->in_bytes;
+  while (state->in_bytes >= state->in_bytes_limit) {
+    PERFETTO_DCHECK(state->in_bytes == state->in_bytes_limit);
+    push_next_state = false;
+
+    // We can't possibly write more than we read.
+    const uint32_t msg_bytes_written = static_cast<uint32_t>(
+        out_written() - state->out_bytes_written_at_start);
+    PERFETTO_DCHECK(msg_bytes_written <= state->in_bytes_limit);
+
+    // Backfill the length field of the
+    proto_utils::WriteRedundantVarInt(msg_bytes_written, state->size_field,
+                                      state->size_field_len);
+
+    const uint32_t in_bytes_processes_for_last_msg = state->in_bytes;
+    stack_.pop_back();
+    PERFETTO_CHECK(!stack_.empty());
+    state = &stack_.back();
+    state->in_bytes += in_bytes_processes_for_last_msg;
+    if (PERFETTO_UNLIKELY(!tokenizer_.idle())) {
+      // If we hit this case, it means that we got to the end of a submessage
+      // while decoding a field. We can't recover from this and we don't want to
+      // propagate a broken sub-message.
+      return SetUnrecoverableErrorState();
+    }
+  }
+
+  if (push_next_state) {
+    PERFETTO_DCHECK(tokenizer_.idle());
+    stack_.emplace_back(std::move(next_state));
+    state = &stack_.back();
+  }
+}
+
+void MessageFilter::SetUnrecoverableErrorState() {
+  error_ = true;
+  stack_.clear();
+  stack_.resize(1);
+  auto& state = stack_[0];
+  state.eat_next_bytes = UINT32_MAX;
+  state.in_bytes_limit = UINT32_MAX;
+  state.passthrough_eaten_bytes = false;
+  out_ = out_buf_.get();  // Reset the write pointer.
+}
+
+void MessageFilter::IncrementCurrentFieldUsage(uint32_t field_id,
+                                               bool allowed) {
+  // Slowpath. Used mainly in offline tools and tests to workout used fields in
+  // a proto.
+  PERFETTO_DCHECK(track_field_usage_);
+
+  // Field path contains a concatenation of varints, one for each nesting level.
+  // e.g. y in message Root { Sub x = 2; }; message Sub { SubSub y = 7; }
+  // is encoded as [varint(2) + varint(7)].
+  // We use varint to take the most out of SSO (small string opt). In most cases
+  // the path will fit in the on-stack 22 bytes, requiring no heap.
+  std::string field_path;
+
+  auto append_field_id = [&field_path](uint32_t id) {
+    uint8_t buf[10];
+    uint8_t* end = proto_utils::WriteVarInt(id, buf);
+    field_path.append(reinterpret_cast<char*>(buf),
+                      static_cast<size_t>(end - buf));
+  };
+
+  // Append all the ancestors IDs from the state stack.
+  // The first entry of the stack has always ID 0 and we skip it (we don't know
+  // the ID of the root message itself).
+  PERFETTO_DCHECK(stack_.size() >= 2 && stack_[1].field_id == 0);
+  for (size_t i = 2; i < stack_.size(); ++i)
+    append_field_id(stack_[i].field_id);
+  // Append the id of the field in the current message.
+  append_field_id(field_id);
+  field_usage_[field_path] += allowed ? 1 : -1;
+}
+
+}  // namespace protozero
diff --git a/src/protozero/filtering/message_filter.h b/src/protozero/filtering/message_filter.h
new file mode 100644
index 0000000..80ddd0e
--- /dev/null
+++ b/src/protozero/filtering/message_filter.h
@@ -0,0 +1,208 @@
+/*
+ * 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_PROTOZERO_FILTERING_MESSAGE_FILTER_H_
+#define SRC_PROTOZERO_FILTERING_MESSAGE_FILTER_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+
+#include "src/protozero/filtering/filter_bytecode_parser.h"
+#include "src/protozero/filtering/message_tokenizer.h"
+
+namespace protozero {
+
+// A class to filter binary-encoded proto messages using an allow-list of field
+// ids, also known as "filter bytecode". The filter determines which fields are
+// allowed to be passed through in output and strips all the other fields.
+// See go/trace-filtering for full design.
+// This class takes in input:
+// 1) The filter bytecode, loaded once via the LoadFilterBytecode() method.
+// 2) A proto-encoded binary message. The message doesn't have to be contiguous,
+//    it can be passed as an array of arbitrarily chunked fragments.
+// The FilterMessage*() method returns in output a proto message, stripping out
+// all unknown fields. If the input is malformed (e.g., unknown proto field wire
+// types, lengths out of bound) the whole filtering failed and the |error| flag
+// of the FilteredMessage object is set to true.
+// The filtering operation is based on rewriting a copy of the message into a
+// self-allocated buffer, which is then returned in the output. The input buffer
+// is NOT altered.
+// Note also that the process of rewriting the protos gets rid of most redundant
+// varint encoding (if present). So even if all fields are allow-listed, the
+// output might NOT be bitwise identical to the input (but it will be
+// semantically equivalent).
+// Furthermore the enable_field_usage_tracking() method allows to keep track of
+// a histogram of allowed / denied fields. It slows down filtering and is
+// intended only on host tools.
+class MessageFilter {
+ public:
+  MessageFilter();
+  ~MessageFilter();
+
+  struct InputSlice {
+    const void* data;
+    size_t len;
+  };
+
+  struct FilteredMessage {
+    FilteredMessage(std::unique_ptr<uint8_t[]> d, size_t s)
+        : data(std::move(d)), size(s) {}
+    std::unique_ptr<uint8_t[]> data;
+    size_t size;  // The used bytes in |data|. This is <= sizeof(data).
+    bool error = false;
+  };
+
+  // Loads the filter bytecode that will be used to filter any subsequent
+  // message. Must be called before the first call to FilterMessage*().
+  // |filter_data| must point to a byte buffer for a proto-encoded ProtoFilter
+  // message (see proto_filter.proto).
+  bool LoadFilterBytecode(const void* filter_data, size_t len);
+
+  // This affects the filter starting point of the subsequent FilterMessage*()
+  // calls. By default the filtering process starts from the message @ index 0,
+  // the root message passed to proto_filter when generating the bytecode
+  // (in typical tracing use-cases, this is perfetto.protos.Trace). However, the
+  // caller (TracingServiceImpl) might want to filter packets from the 2nd level
+  // (perfetto.protos.TracePacket) because the root level is pre-pended after
+  // the fact. This call allows to change the root message for the filter.
+  // The argument |field_ids| is an array of proto field ids and determines the
+  // path to the new root. For instance, in the case of [1,2,3] SetFilterRoot
+  // will identify the sub-message for the field "root.1.2.3" and use that.
+  // In order for this to succeed all the fields in the path must be allowed
+  // in the filter and must be a nested message type.
+  bool SetFilterRoot(const uint32_t* field_ids, size_t num_fields);
+
+  // Takes an input message, fragmented in arbitrary slices, and returns a
+  // filtered message in output.
+  FilteredMessage FilterMessageFragments(const InputSlice*, size_t num_slices);
+
+  // Helper for tests, where the input is a contiguous buffer.
+  FilteredMessage FilterMessage(const void* data, size_t len) {
+    InputSlice slice{data, len};
+    return FilterMessageFragments(&slice, 1);
+  }
+
+  // When enabled returns a map of "field path" to "usage counter".
+  // The key (std::string) is a binary buffer (i.e. NOT an ASCII/UTF-8 string)
+  // which contains a varint for each field. Consider the following:
+  // message Root { Sub1 f1 = 1; };
+  // message Sub1 { Sub2 f2 = 7;}
+  // message Sub2 { string f3 = 5; }
+  // The field .f1.f2.f3 will be encoded as \x01\0x07\x05.
+  // The value is the number of times that field has been encountered. If the
+  // field is not allow-listed in the bytecode (the field is stripped in output)
+  // the count will be negative.
+  void enable_field_usage_tracking(bool x) { track_field_usage_ = x; }
+  const std::unordered_map<std::string, int32_t>& field_usage() const {
+    return field_usage_;
+  }
+
+  // Exposed only for DCHECKS in TracingServiceImpl.
+  uint32_t root_msg_index() { return root_msg_index_; }
+
+ private:
+  // This is called by FilterMessageFragments().
+  // Inlining allows the compiler turn the per-byte call/return into a for loop,
+  // while, at the same time, keeping the code easy to read and reason about.
+  // It gives a 20-25% speedup (265ms vs 215ms for a 25MB trace).
+  void FilterOneByte(uint8_t octet) PERFETTO_ALWAYS_INLINE;
+
+  // No-inline because this is a slowpath (only when usage tracking is enabled).
+  void IncrementCurrentFieldUsage(uint32_t field_id,
+                                  bool allowed) PERFETTO_NO_INLINE;
+
+  // Gets into an error state which swallows all the input and emits no output.
+  void SetUnrecoverableErrorState();
+
+  // We keep track of the nest of messages in a stack. Each StackState
+  // object corresponds to a level of nesting in the proto message structure.
+  // Every time a new field of type len-delimited that has a corresponding
+  // sub-message in the bytecode is encountered, a new StackState is pushed in
+  // |stack_|. stack_[0] is a sentinel to prevent over-popping without adding
+  // extra branches in the fastpath.
+  // |stack_|. stack_[1] is the state of the root message.
+  struct StackState {
+    uint32_t in_bytes = 0;  // Number of input bytes processed.
+
+    // When |in_bytes| reaches this value, the current state should be popped.
+    // This is set when recursing into nested submessages. This is 0 only for
+    // stack_[0] (we don't know the size of the root message upfront).
+    uint32_t in_bytes_limit = 0;
+
+    // This is set when a len-delimited message is encountered, either a string
+    // or a nested submessage that is NOT allow-listed in the bytecode.
+    // This causes input bytes to be consumed without being parsed from the
+    // input stream. If |passthrough_eaten_bytes| == true, they will be copied
+    // as-is in output (e.g. in the case of an allowed string/bytes field).
+    uint32_t eat_next_bytes = 0;
+
+    // Keeps tracks of the stream_writer output counter (out_.written()) then
+    // the StackState is pushed. This is used to work out, when popping, how
+    // many bytes have been written for the current submessage.
+    uint32_t out_bytes_written_at_start = 0;
+
+    uint32_t field_id = 0;   // The proto field id for the current message.
+    uint32_t msg_index = 0;  // The index of the message filter in the bytecode.
+
+    // This is a pointer to the proto preamble for the current submessage
+    // (it's nullptr for stack_[0] and non-null elsewhere). This will be filled
+    // with the actual size of the message (out_.written() -
+    // |out_bytes_written_at_start|) when finishing (popping) the message.
+    // This must be filled using WriteRedundantVarint(). Note that the
+    // |size_field_len| is variable and depends on the actual length of the
+    // input message. If the output message has roughly the same size of the
+    // input message, the length will not be redundant.
+    // In other words: the length of the field is reserved when the submessage
+    // starts. At that point we know the upper-bound for the output message
+    // (a filtered submessage can be <= the original one, but not >). So we
+    // reserve as many bytes it takes to write the input length in varint.
+    // Then, when the message is finalized and we know the actual output size
+    // we backfill the field.
+    // Consider the example of a submessage where the input size = 130 (>127,
+    // 2 varint bytes) and the output is 120 bytes. The length will be 2 bytes
+    // wide even though could have been encoded with just one byte.
+    uint8_t* size_field = nullptr;
+    uint32_t size_field_len = 0;
+
+    // When true the next |eat_next_bytes| are copied as-is in output.
+    // It seems that keeping this field at the end rather than next to
+    // |eat_next_bytes| makes the filter a little (but measurably) faster.
+    // (likely something related with struct layout vs cache sizes).
+    bool passthrough_eaten_bytes = false;
+  };
+
+  uint32_t out_written() { return static_cast<uint32_t>(out_ - &out_buf_[0]); }
+
+  std::unique_ptr<uint8_t[]> out_buf_;
+  uint8_t* out_ = nullptr;
+  uint8_t* out_end_ = nullptr;
+  uint32_t root_msg_index_ = 0;
+
+  FilterBytecodeParser filter_;
+  MessageTokenizer tokenizer_;
+  std::vector<StackState> stack_;
+
+  bool error_ = false;
+  bool track_field_usage_ = false;
+  std::unordered_map<std::string, int32_t> field_usage_;
+};
+
+}  // namespace protozero
+
+#endif  // SRC_PROTOZERO_FILTERING_MESSAGE_FILTER_H_
diff --git a/src/protozero/filtering/message_filter_benchmark.cc b/src/protozero/filtering/message_filter_benchmark.cc
new file mode 100644
index 0000000..c800c82
--- /dev/null
+++ b/src/protozero/filtering/message_filter_benchmark.cc
@@ -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.
+
+#include <benchmark/benchmark.h>
+
+#include <algorithm>
+#include <string>
+
+#include "perfetto/ext/base/file_utils.h"
+#include "src/base/test/utils.h"
+#include "src/protozero/filtering/message_filter.h"
+
+static void BM_ProtozeroMessageFilter(benchmark::State& state) {
+  std::string trace_data;
+  static const char kTestTrace[] = "test/data/example_android_trace_30s.pb";
+  perfetto::base::ReadFile(perfetto::base::GetTestDataPath(kTestTrace),
+                           &trace_data);
+  PERFETTO_CHECK(!trace_data.empty());
+
+  std::string filter;
+  static const char kFullTraceFilter[] = "test/data/full_trace_filter.bytecode";
+  perfetto::base::ReadFile(kFullTraceFilter, &filter);
+  PERFETTO_CHECK(!filter.empty());
+
+  protozero::MessageFilter filt;
+  filt.LoadFilterBytecode(filter.data(), filter.size());
+
+  for (auto _ : state) {
+    auto res = filt.FilterMessage(trace_data.data(), trace_data.size());
+    benchmark::DoNotOptimize(res);
+    benchmark::ClobberMemory();
+  }
+  state.SetBytesProcessed(
+      static_cast<int64_t>(state.iterations() * trace_data.size()));
+}
+
+BENCHMARK(BM_ProtozeroMessageFilter);
diff --git a/src/protozero/filtering/message_filter_fuzzer.cc b/src/protozero/filtering/message_filter_fuzzer.cc
new file mode 100644
index 0000000..60cfe5c
--- /dev/null
+++ b/src/protozero/filtering/message_filter_fuzzer.cc
@@ -0,0 +1,104 @@
+/*
+ * 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 <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "perfetto/base/logging.h"
+#include "src/protozero/filtering/message_filter.h"
+
+namespace protozero {
+namespace {
+
+// A valid filter bytecode obtained from a perfetto.protos.Trace message.
+uint8_t kValidFilter[] = {
+    0x0b, 0x01, 0x12, 0x04, 0x00, 0x0b, 0x02, 0x13, 0x0f, 0x19, 0x23, 0x13,
+    0x33, 0x14, 0x3b, 0x15, 0x41, 0x4b, 0x11, 0x51, 0x5b, 0x16, 0x63, 0x3b,
+    0x69, 0x8b, 0x02, 0x21, 0x93, 0x02, 0x2a, 0x9b, 0x02, 0x2c, 0xab, 0x02,
+    0x2e, 0xb3, 0x02, 0x09, 0xc3, 0x02, 0x30, 0xca, 0x02, 0x02, 0xdb, 0x02,
+    0x31, 0xe3, 0x02, 0x26, 0xeb, 0x02, 0x32, 0xf3, 0x02, 0x07, 0xfb, 0x02,
+    0x33, 0x8b, 0x03, 0x0a, 0x9b, 0x03, 0x34, 0xb3, 0x03, 0x06, 0xc3, 0x03,
+    0x37, 0xd1, 0x03, 0xdb, 0x03, 0x3c, 0xe3, 0x03, 0x39, 0x93, 0x04, 0x38,
+    0x9b, 0x04, 0x3a, 0x00, 0x09, 0x13, 0x03, 0x19, 0x00, 0x0a, 0x02, 0x1b,
+    0x04, 0x23, 0x05, 0x5b, 0x06, 0x6b, 0x06, 0x83, 0x01, 0x07, 0x8b, 0x01,
+    0x08, 0x93, 0x01, 0x07, 0x9b, 0x01, 0x07, 0xa3, 0x01, 0x08, 0x9b, 0x02,
+    0x08, 0xcb, 0x02, 0x08, 0xd3, 0x02, 0x08, 0xdb, 0x02, 0x09, 0xe3, 0x02,
+    0x07, 0xeb, 0x02, 0x0a, 0xf3, 0x02, 0x07, 0xfb, 0x02, 0x0b, 0x83, 0x03,
+    0x06, 0x8b, 0x03, 0x0b, 0x93, 0x03, 0x05, 0x9b, 0x03, 0x0b, 0xab, 0x03,
+    0x0c, 0xb3, 0x03, 0x0c, 0xbb, 0x03, 0x0c, 0x9b, 0x04, 0x0d, 0xa3, 0x04,
+    0x07, 0xab, 0x04, 0x06, 0xb3, 0x04, 0x07, 0xc3, 0x04, 0x06, 0xcb, 0x04,
+    0x07, 0xd3, 0x04, 0x07, 0xdb, 0x04, 0x06, 0x93, 0x07, 0x08, 0xeb, 0x07,
+    0x08, 0xcb, 0x09, 0x07, 0xd3, 0x09, 0x05, 0xf3, 0x0b, 0x06, 0xdb, 0x0e,
+    0x09, 0xe3, 0x0e, 0x09, 0xeb, 0x0e, 0x07, 0xf3, 0x0e, 0x09, 0xfb, 0x0e,
+    0x09, 0x83, 0x0f, 0x07, 0x8b, 0x0f, 0x06, 0x93, 0x0f, 0x07, 0xb3, 0x0f,
+    0x0a, 0xc3, 0x0f, 0x0e, 0xfb, 0x0f, 0x0e, 0x83, 0x10, 0x08, 0xfb, 0x10,
+    0x08, 0x8b, 0x11, 0x08, 0xcb, 0x13, 0x06, 0xd3, 0x13, 0x07, 0xdb, 0x13,
+    0x07, 0xb3, 0x14, 0x07, 0x00, 0x11, 0x00, 0x0a, 0x07, 0x00, 0x0a, 0x02,
+    0x00, 0x0a, 0x03, 0x00, 0x0a, 0x05, 0x00, 0x0a, 0x04, 0x00, 0x0a, 0x06,
+    0x00, 0x09, 0x00, 0x00, 0x0a, 0x03, 0x29, 0x00, 0x0a, 0x08, 0x00, 0x0b,
+    0x10, 0x13, 0x07, 0x19, 0x00, 0x0a, 0x03, 0x23, 0x07, 0x29, 0x00, 0x0b,
+    0x12, 0x11, 0x00, 0x0a, 0x0a, 0x5b, 0x07, 0x00, 0x0a, 0x02, 0x1b, 0x07,
+    0x00, 0x0b, 0x09, 0x11, 0x00, 0x0b, 0x06, 0x13, 0x06, 0x1b, 0x0e, 0x22,
+    0x02, 0x33, 0x06, 0x39, 0x43, 0x06, 0x49, 0x00, 0x0a, 0x03, 0x2b, 0x0b,
+    0x33, 0x20, 0x42, 0x05, 0x82, 0x01, 0x02, 0xa1, 0x01, 0xc3, 0x01, 0x17,
+    0xcb, 0x01, 0x04, 0xd3, 0x01, 0x0b, 0xdb, 0x01, 0x06, 0xe3, 0x01, 0x1e,
+    0xeb, 0x01, 0x1f, 0xf2, 0x01, 0x02, 0x00, 0x0b, 0x18, 0x12, 0x0c, 0x73,
+    0x1a, 0x7b, 0x1c, 0x83, 0x01, 0x1d, 0x8b, 0x01, 0x05, 0x00, 0x0b, 0x08,
+    0x13, 0x19, 0x00, 0x0a, 0x2e, 0x00, 0x0a, 0x03, 0x23, 0x1b, 0x2b, 0x1b,
+    0x33, 0x05, 0x00, 0x0a, 0x09, 0x53, 0x09, 0x00, 0x09, 0x13, 0x1b, 0x00,
+    0x0a, 0x03, 0x23, 0x1b, 0x00, 0x09, 0x19, 0x00, 0x0a, 0x03, 0x23, 0x06,
+    0x2a, 0x02, 0x00, 0x0a, 0x04, 0x31, 0x42, 0x08, 0x92, 0x01, 0x02, 0x00,
+    0x0b, 0x22, 0x13, 0x23, 0x1a, 0x03, 0x33, 0x07, 0x3b, 0x09, 0x42, 0x03,
+    0x5b, 0x0b, 0x62, 0x03, 0x81, 0x01, 0x8b, 0x01, 0x29, 0x92, 0x01, 0x02,
+    0xa3, 0x01, 0x07, 0xab, 0x01, 0x0b, 0xb2, 0x01, 0x03, 0xcb, 0x01, 0x09,
+    0xda, 0x01, 0x02, 0x00, 0x09, 0x21, 0x00, 0x0b, 0x24, 0x11, 0x00, 0x0a,
+    0x04, 0x31, 0xa3, 0x06, 0x25, 0xbb, 0x06, 0x26, 0xc3, 0x06, 0x0a, 0xcb,
+    0x06, 0x27, 0xd3, 0x06, 0x06, 0xeb, 0x06, 0x0b, 0x00, 0x0a, 0x03, 0x52,
+    0x02, 0x00, 0x0a, 0x04, 0x32, 0x03, 0x00, 0x0a, 0x02, 0x22, 0x02, 0x33,
+    0x28, 0x3a, 0x02, 0x00, 0x2a, 0x02, 0x00, 0x09, 0x13, 0x07, 0x19, 0x00,
+    0x09, 0x13, 0x2b, 0x00, 0x0a, 0x09, 0x00, 0x0b, 0x2d, 0x12, 0x08, 0x00,
+    0x0a, 0x12, 0x00, 0x0b, 0x06, 0x13, 0x09, 0x1b, 0x06, 0x23, 0x05, 0x2b,
+    0x2f, 0x32, 0x02, 0x00, 0x09, 0x13, 0x0a, 0x00, 0x0b, 0x09, 0x13, 0x07,
+    0x00, 0x09, 0x1a, 0x03, 0x00, 0x0b, 0x09, 0x12, 0x02, 0x00, 0x0b, 0x08,
+    0x12, 0x02, 0x00, 0x0b, 0x35, 0x11, 0x00, 0x0b, 0x36, 0x13, 0x36, 0x00,
+    0x09, 0x13, 0x07, 0x1b, 0x0b, 0x00, 0x09, 0x13, 0x08, 0x1b, 0x06, 0x23,
+    0x06, 0x2a, 0x02, 0x3b, 0x06, 0x00, 0x0a, 0x05, 0x82, 0x01, 0x03, 0x00,
+    0x09, 0x1b, 0x31, 0x23, 0x26, 0x29, 0x33, 0x09, 0x3b, 0x06, 0x43, 0x31,
+    0x00, 0x0b, 0x06, 0x00, 0x0b, 0x06, 0x13, 0x06, 0x23, 0x09, 0x2b, 0x06,
+    0x33, 0x09, 0x3b, 0x06, 0x83, 0x01, 0x06, 0x8b, 0x01, 0x06, 0x93, 0x01,
+    0x06, 0x9b, 0x01, 0x05, 0x00, 0x5b, 0x3d, 0xd1, 0x03, 0x00, 0x59, 0xf9,
+    0x01, 0x00, 0x8f, 0xf8, 0xf5, 0xcb, 0x06};
+
+int FuzzMessageFilter(const uint8_t* data, size_t size) {
+  MessageFilter filter;
+  PERFETTO_CHECK(filter.LoadFilterBytecode(kValidFilter, sizeof(kValidFilter)));
+
+  auto res = filter.FilterMessage(data, size);
+
+  // Either parsing fails or if it succeeds, the output data must be <= input.
+  PERFETTO_CHECK(res.error || res.size <= size);
+  return 0;
+}
+
+}  // namespace
+}  // namespace protozero
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size);
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  return protozero::FuzzMessageFilter(data, size);
+}
diff --git a/src/protozero/filtering/message_filter_unittest.cc b/src/protozero/filtering/message_filter_unittest.cc
new file mode 100644
index 0000000..83cb911
--- /dev/null
+++ b/src/protozero/filtering/message_filter_unittest.cc
@@ -0,0 +1,810 @@
+/*
+ * 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 "test/gtest_and_gmock.h"
+
+#include <random>
+
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/temp_file.h"
+#include "perfetto/protozero/proto_decoder.h"
+#include "perfetto/protozero/proto_utils.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "protos/perfetto/trace/trace.pb.h"
+#include "src/protozero/filtering/filter_util.h"
+#include "src/protozero/filtering/message_filter.h"
+
+namespace protozero {
+
+namespace {
+
+TEST(MessageFilterTest, EndToEnd) {
+  auto schema = perfetto::base::TempFile::Create();
+  static const char kSchema[] = R"(
+  syntax = "proto2";
+  message FilterSchema {
+    message Nested {
+      optional fixed32 f32 = 2;
+      repeated string ss = 5;
+    }
+    optional int32 i32 = 1;
+    optional string str = 3;
+    repeated Nested nest = 6;
+    repeated int32 f11 = 11;
+    repeated int64 f12 = 12;
+    repeated sint32 f13 = 13;
+    repeated sint64 f14 = 14;
+    repeated fixed32 f15 = 15;
+    repeated fixed32 f16 = 16;
+    repeated fixed64 f17 = 17;
+    repeated fixed64 f18 = 18;
+    repeated float f19 = 19;
+    repeated double f20 = 20;
+  };
+  )";
+
+  perfetto::base::WriteAll(*schema, kSchema, strlen(kSchema));
+  perfetto::base::FlushFile(*schema);
+
+  FilterUtil filter;
+  ASSERT_TRUE(filter.LoadMessageDefinition(schema.path(), "", ""));
+  std::string bytecode = filter.GenerateFilterBytecode();
+  ASSERT_GT(bytecode.size(), 0u);
+
+  HeapBuffered<Message> msg;
+  msg->AppendVarInt(/*field_id=*/1, -1000000000ll);
+  msg->AppendVarInt(/*field_id=*/2, 42);
+  msg->AppendString(/*field_id=*/3, "foobar");
+  msg->AppendFixed(/*field_id=*/4, 10);
+  msg->AppendVarInt(/*field_id=*/11, INT32_MIN);
+  msg->AppendVarInt(/*field_id=*/12, INT64_MIN);
+  msg->AppendSignedVarInt(/*field_id=*/13, INT32_MIN);
+  msg->AppendSignedVarInt(/*field_id=*/14, INT64_MIN);
+  msg->AppendFixed(/*field_id=*/15, static_cast<int32_t>(INT32_MIN));
+  msg->AppendFixed(/*field_id=*/16, static_cast<int32_t>(INT32_MAX));
+  msg->AppendFixed(/*field_id=*/17, static_cast<int64_t>(INT64_MIN));
+  msg->AppendFixed(/*field_id=*/18, static_cast<int64_t>(INT64_MAX));
+  msg->AppendFixed(/*field_id=*/19, FLT_EPSILON);
+  msg->AppendFixed(/*field_id=*/20, DBL_EPSILON);
+
+  auto* nest = msg->BeginNestedMessage<Message>(/*field_id=*/6);
+  nest->AppendFixed(/*field_id=*/1, 10);
+  nest->AppendFixed(/*field_id=*/2, static_cast<int32_t>(-2000000000ll));
+  nest->AppendString(/*field_id=*/4, "stripped");
+  nest->AppendString(/*field_id=*/5, "");
+  nest->Finalize();
+
+  MessageFilter flt;
+  ASSERT_TRUE(flt.LoadFilterBytecode(bytecode.data(), bytecode.size()));
+
+  std::vector<uint8_t> encoded = msg.SerializeAsArray();
+
+  for (int repetitions = 0; repetitions < 3; ++repetitions) {
+    auto filtered = flt.FilterMessage(encoded.data(), encoded.size());
+    ASSERT_LT(filtered.size, encoded.size());
+
+    ProtoDecoder dec(filtered.data.get(), filtered.size);
+    EXPECT_TRUE(dec.FindField(1).valid());
+    EXPECT_EQ(dec.FindField(1).as_int64(), -1000000000ll);
+    EXPECT_FALSE(dec.FindField(2).valid());
+    EXPECT_TRUE(dec.FindField(3).valid());
+    EXPECT_EQ(dec.FindField(3).as_std_string(), "foobar");
+    EXPECT_FALSE(dec.FindField(4).valid());
+    EXPECT_TRUE(dec.FindField(6).valid());
+    for (uint32_t i = 11; i <= 20; ++i)
+      EXPECT_TRUE(dec.FindField(i).valid());
+
+    EXPECT_EQ(dec.FindField(11).as_int32(), INT32_MIN);
+    EXPECT_EQ(dec.FindField(12).as_int64(), INT64_MIN);
+    EXPECT_EQ(dec.FindField(13).as_sint32(), INT32_MIN);
+    EXPECT_EQ(dec.FindField(14).as_sint64(), INT64_MIN);
+    EXPECT_EQ(dec.FindField(15).as_int32(), INT32_MIN);
+    EXPECT_EQ(dec.FindField(16).as_int32(), INT32_MAX);
+    EXPECT_EQ(dec.FindField(17).as_int64(), INT64_MIN);
+    EXPECT_EQ(dec.FindField(18).as_int64(), INT64_MAX);
+    EXPECT_EQ(dec.FindField(19).as_float(), FLT_EPSILON);
+    EXPECT_EQ(dec.FindField(20).as_double(), DBL_EPSILON);
+
+    ProtoDecoder nest_dec(dec.FindField(6).as_bytes());
+    EXPECT_FALSE(nest_dec.FindField(1).valid());
+    EXPECT_TRUE(nest_dec.FindField(2).valid());
+    EXPECT_EQ(nest_dec.FindField(2).as_int32(), -2000000000ll);
+    EXPECT_TRUE(nest_dec.FindField(5).valid());
+    EXPECT_EQ(nest_dec.FindField(5).as_bytes().size, 0u);
+  }
+}
+
+TEST(MessageFilterTest, ChangeRoot) {
+  auto schema = perfetto::base::TempFile::Create();
+  static const char kSchema[] = R"(
+  syntax = "proto2";
+  message FilterSchema {
+    message Nested {
+      message Nested2 {
+        optional int32 e = 5;
+      }
+      optional int32 c = 3;
+      repeated Nested2 d = 4;
+    }
+    optional int32 a = 1;
+    optional Nested b = 2;
+  };
+  )";
+
+  perfetto::base::WriteAll(*schema, kSchema, strlen(kSchema));
+  perfetto::base::FlushFile(*schema);
+
+  FilterUtil filter;
+  ASSERT_TRUE(filter.LoadMessageDefinition(schema.path(), "", ""));
+  std::string bytecode = filter.GenerateFilterBytecode();
+  ASSERT_GT(bytecode.size(), 0u);
+
+  HeapBuffered<Message> msg;
+  msg->AppendVarInt(/*field_id=*/1, 101);
+  msg->AppendVarInt(/*field_id=*/3, 103);
+  msg->AppendVarInt(/*field_id=*/5, 105);
+  auto* nest = msg->BeginNestedMessage<Message>(/*field_id=*/4);  // Nested b.
+  nest->AppendVarInt(/*field_id=*/5, 205);
+  nest->Finalize();
+  std::vector<uint8_t> encoded = msg.SerializeAsArray();
+
+  MessageFilter flt;
+  ASSERT_TRUE(flt.LoadFilterBytecode(bytecode.data(), bytecode.size()));
+  uint32_t roots[2]{2, 4};
+
+  // First set the root to field id ".2" (.b). The fliter should happen treating
+  // |Nested| as rot, so allowing only field 3 and 4 (Nested2) through.
+  {
+    flt.SetFilterRoot(roots, 1);
+    auto filtered = flt.FilterMessage(encoded.data(), encoded.size());
+    ASSERT_LT(filtered.size, encoded.size());
+    ProtoDecoder dec(filtered.data.get(), filtered.size);
+    EXPECT_FALSE(dec.FindField(1).valid());
+    EXPECT_TRUE(dec.FindField(3).valid());
+    EXPECT_EQ(dec.FindField(3).as_int32(), 103);
+    EXPECT_FALSE(dec.FindField(5).valid());
+    EXPECT_TRUE(dec.FindField(4).valid());
+    EXPECT_EQ(dec.FindField(4).as_std_string(), "(\xCD\x01");
+  }
+
+  // Now set the root to ".2.4" (.b.d). This should allow only the field "e"
+  // to pass through.
+  {
+    flt.SetFilterRoot(roots, 2);
+    auto filtered = flt.FilterMessage(encoded.data(), encoded.size());
+    ASSERT_LT(filtered.size, encoded.size());
+    ProtoDecoder dec(filtered.data.get(), filtered.size);
+    EXPECT_FALSE(dec.FindField(1).valid());
+    EXPECT_FALSE(dec.FindField(3).valid());
+    EXPECT_FALSE(dec.FindField(4).valid());
+    EXPECT_TRUE(dec.FindField(5).valid());
+    EXPECT_EQ(dec.FindField(5).as_int32(), 105);
+  }
+}
+
+TEST(MessageFilterTest, MalformedInput) {
+  // Create and load a simple filter.
+  auto schema = perfetto::base::TempFile::Create();
+  static const char kSchema[] = R"(
+  syntax = "proto2";
+  message FilterSchema {
+    message Nested {
+      optional fixed32 f32 = 4;
+      repeated string ss = 5;
+    }
+    optional int32 i32 = 1;
+    optional string str = 2;
+    repeated Nested nest = 3;
+  };
+  )";
+  perfetto::base::WriteAll(*schema, kSchema, strlen(kSchema));
+  perfetto::base::FlushFile(*schema);
+  FilterUtil filter;
+  ASSERT_TRUE(filter.LoadMessageDefinition(schema.path(), "", ""));
+  std::string bytecode = filter.GenerateFilterBytecode();
+  ASSERT_GT(bytecode.size(), 0u);
+  MessageFilter flt;
+  ASSERT_TRUE(flt.LoadFilterBytecode(bytecode.data(), bytecode.size()));
+
+  {
+    // A malformed message found by the fuzzer.
+    static const uint8_t kData[]{
+        0x52, 0x21,  // ID=10, type=len-delimited, len=33.
+        0xa0, 0xa4,  // Early terminating payload.
+    };
+    auto res = flt.FilterMessage(kData, sizeof(kData));
+    EXPECT_TRUE(res.error);
+  }
+
+  {
+    // A malformed message which contains a non-terminated varint.
+    static const uint8_t kData[]{
+        0x08, 0x2A,  // A valid varint field id=1 value=42 (0x2A).
+        0x08, 0xFF,  // An unterminated varint.
+    };
+    auto res = flt.FilterMessage(kData, sizeof(kData));
+    EXPECT_TRUE(res.error);
+  }
+
+  {
+    // A malformed message which contains a sub-message with a field that brings
+    // it out of the outer size.
+    static const uint8_t kData[]{
+        0x08, 0x2A,  // A valid varint field id=1 value=42 (0x2A).
+        0x1A, 0x04,  // A len-delim field, id=3, length=4.
+        // The nested message |nest| starts here.
+        0x25, 0x0, 0x0, 0x0, 0x01,  // A fixed32 field, id=4.
+        // Note that the fixed32 field has an expected length of 4 but that
+        // overflows the size of the |nest| method, because with its 0x25
+        // preamble it becomes 5 bytes. At this point this should cause a
+        // persistent failure.
+    };
+    auto res = flt.FilterMessage(kData, sizeof(kData));
+    EXPECT_TRUE(res.error);
+  }
+
+  // A parsing failure shoulnd't affect the ability to filter the following
+  // message. Try again but this time with a valid message.
+  {
+    static const uint8_t kData[]{
+        0x08, 0x2A,  // A valid varint field id=1 value=42 (0x2A).
+        0x1A, 0x05,  // A len-delim field, id=3, length=5.
+        0x25, 0x0,  0x0, 0x0, 0x01,  // A fixed32 field, id=4.
+        0x38, 0x42,  // A valid but not allowed varint field id=7.
+    };
+    auto res = flt.FilterMessage(kData, sizeof(kData));
+    EXPECT_FALSE(res.error);
+    EXPECT_EQ(res.size, sizeof(kData) - 2);  // last 2 bytes should be skipped.
+    EXPECT_EQ(memcmp(kData, res.data.get(), res.size), 0);
+  }
+}
+
+// It processes a real test trace with a real filter. The filter has been
+// obtained from the full upstream perfetto proto (+ re-adding the for_testing
+// field which got removed after adding most test traces). This covers the most
+// complex case of filtering a real trace with a filter that allows all possible
+// fields, hence re-entering deeply in most nested fields.
+TEST(MessageFilterTest, RealTracePassthrough) {
+  // This is test/data/android_log_ring_buffer_mode.pb. It's re-encoded as a
+  // constant because unittests cannot depend on test/data/, only integration
+  // tests can.
+  static const uint8_t kTraceData[]{
+      0x0a, 0x16, 0x18, 0x8f, 0x4e, 0xa2, 0x02, 0x10, 0x82, 0x47, 0x7a, 0x76,
+      0xb2, 0x8d, 0x42, 0xba, 0x81, 0xdc, 0x33, 0x32, 0x6d, 0x57, 0xa0, 0x79,
+      0x0a, 0x5f, 0x18, 0x8f, 0x4e, 0x32, 0x5a, 0x0a, 0x09, 0x08, 0x06, 0x10,
+      0xf4, 0xd3, 0xea, 0xbb, 0xba, 0x55, 0x0a, 0x0c, 0x08, 0x02, 0x10, 0xf9,
+      0xcc, 0xb4, 0xd1, 0xe8, 0xdc, 0xa5, 0xbc, 0x15, 0x0a, 0x09, 0x08, 0x04,
+      0x10, 0x86, 0xb9, 0x9c, 0xba, 0xba, 0x55, 0x0a, 0x0c, 0x08, 0x01, 0x10,
+      0xeb, 0xe9, 0x82, 0xd3, 0xe8, 0xdc, 0xa5, 0xbc, 0x15, 0x0a, 0x09, 0x08,
+      0x03, 0x10, 0xac, 0xd6, 0xea, 0xbb, 0xba, 0x55, 0x0a, 0x09, 0x08, 0x05,
+      0x10, 0x9b, 0xe1, 0xd8, 0xbb, 0xba, 0x55, 0x0a, 0x07, 0x08, 0x07, 0x10,
+      0xf5, 0xe6, 0xd9, 0x55, 0x0a, 0x07, 0x08, 0x08, 0x10, 0xc1, 0xcc, 0xa7,
+      0x41, 0x0a, 0x27, 0x18, 0x8f, 0x4e, 0x9a, 0x02, 0x21, 0x0a, 0x13, 0x08,
+      0xf0, 0x1f, 0x10, 0x01, 0x18, 0x00, 0x20, 0x00, 0x28, 0x00, 0x30, 0x00,
+      0x38, 0x00, 0x40, 0x00, 0x48, 0x00, 0x10, 0x01, 0x18, 0x07, 0x20, 0x06,
+      0x28, 0x0b, 0x30, 0x01, 0x38, 0x01, 0x0a, 0xd5, 0x01, 0x18, 0x8f, 0x4e,
+      0x8a, 0x02, 0xce, 0x01, 0x0a, 0x06, 0x08, 0x80, 0x80, 0x02, 0x20, 0x00,
+      0x12, 0xa5, 0x01, 0x0a, 0xa2, 0x01, 0x0a, 0x0b, 0x61, 0x6e, 0x64, 0x72,
+      0x6f, 0x69, 0x64, 0x2e, 0x6c, 0x6f, 0x67, 0x10, 0x00, 0x18, 0x00, 0x20,
+      0x00, 0xa2, 0x06, 0x04, 0x50, 0x00, 0x58, 0x00, 0xaa, 0x06, 0x02, 0x0a,
+      0x00, 0xb2, 0x06, 0x08, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x20, 0x00,
+      0xba, 0x06, 0x06, 0x10, 0x00, 0x18, 0x00, 0x20, 0x00, 0xc2, 0x06, 0x06,
+      0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0xca, 0x06, 0x0a, 0x08, 0x00, 0x28,
+      0x00, 0x32, 0x04, 0x28, 0x00, 0x30, 0x00, 0xd2, 0x06, 0x02, 0x08, 0x00,
+      0xda, 0x06, 0x02, 0x18, 0x00, 0xc2, 0x3e, 0x00, 0xfa, 0xff, 0xff, 0xff,
+      0x07, 0x46, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x20, 0x00, 0x28, 0x00,
+      0x32, 0x3a, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, 0x20, 0x00, 0x29, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00,
+      0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55,
+      0x00, 0x00, 0x00, 0x00, 0x58, 0x00, 0x60, 0x00, 0x6a, 0x00, 0x72, 0x00,
+      0x18, 0xe0, 0xd4, 0x03, 0x20, 0x00, 0x28, 0x00, 0x3a, 0x06, 0x08, 0x00,
+      0x10, 0x00, 0x18, 0x00, 0x40, 0x00, 0x48, 0x00, 0x50, 0x00, 0x5a, 0x02,
+      0x08, 0x00, 0x60, 0x00, 0x68, 0x00, 0x0a, 0x94, 0x01, 0x40, 0xd9, 0xf4,
+      0x98, 0x96, 0xbe, 0x54, 0xba, 0x02, 0x84, 0x81, 0x80, 0x00, 0x0a, 0xff,
+      0x80, 0x80, 0x00, 0x38, 0x04, 0x32, 0x08, 0x70, 0x65, 0x72, 0x66, 0x65,
+      0x74, 0x74, 0x6f, 0x42, 0x5d, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64,
+      0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x6f,
+      0x75, 0x72, 0x63, 0x65, 0x2e, 0x63, 0x63, 0x3a, 0x31, 0x35, 0x35, 0x20,
+      0x53, 0x74, 0x61, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x41, 0x6e, 0x64,
+      0x72, 0x6f, 0x69, 0x64, 0x20, 0x6c, 0x6f, 0x67, 0x20, 0x64, 0x61, 0x74,
+      0x61, 0x20, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x3a, 0x20, 0x73, 0x74,
+      0x72, 0x65, 0x61, 0x6d, 0x20, 0x74, 0x61, 0x69, 0x6c, 0x3d, 0x31, 0x20,
+      0x6c, 0x69, 0x64, 0x73, 0x3d, 0x30, 0x2c, 0x32, 0x2c, 0x33, 0x2c, 0x34,
+      0x2c, 0x37, 0x28, 0xdb, 0xe6, 0x9b, 0xfb, 0xeb, 0xdb, 0xa5, 0xbc, 0x15,
+      0x08, 0x00, 0x10, 0xb5, 0x58, 0x18, 0xb5, 0x58, 0x20, 0x00, 0x18, 0x8f,
+      0x4e, 0x0a, 0xf4, 0x0a, 0x40, 0xb2, 0x84, 0xde, 0xdd, 0x8f, 0x55, 0xba,
+      0x02, 0xe4, 0x8a, 0x80, 0x00, 0x0a, 0xe4, 0x80, 0x80, 0x00, 0x38, 0x04,
+      0x32, 0x12, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74,
+      0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x38, 0x72, 0x6d,
+      0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6f,
+      0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x62, 0x3a, 0x20, 0x63, 0x6c,
+      0x6e, 0x74, 0x5f, 0x68, 0x3d, 0x30, 0x78, 0x31, 0x34, 0x20, 0x63, 0x6f,
+      0x6e, 0x6e, 0x5f, 0x68, 0x3d, 0x30, 0x78, 0x37, 0x32, 0x31, 0x64, 0x61,
+      0x30, 0x37, 0x31, 0x30, 0x30, 0x0a, 0x28, 0x92, 0xad, 0xf6, 0xf0, 0xbd,
+      0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xe9, 0x06, 0x18, 0xe9, 0x06,
+      0x20, 0x00, 0x0a, 0xf3, 0x80, 0x80, 0x00, 0x38, 0x06, 0x32, 0x0b, 0x72,
+      0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x4e,
+      0x49, 0x4e, 0x46, 0x4f, 0x3a, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f,
+      0x72, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x77, 0x5f, 0x69, 0x6f, 0x76, 0x65,
+      0x63, 0x5f, 0x63, 0x62, 0x3a, 0x20, 0x57, 0x72, 0x69, 0x74, 0x65, 0x20,
+      0x69, 0x6f, 0x76, 0x65, 0x63, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73,
+      0x74, 0x20, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x20, 0x66,
+      0x6f, 0x72, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x6d, 0x6f, 0x64,
+      0x65, 0x6d, 0x5f, 0x66, 0x73, 0x63, 0x28, 0x9e, 0xa1, 0xea, 0xf0, 0xbd,
+      0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x07, 0x10, 0xe9, 0x06, 0x18, 0xe9, 0x06,
+      0x20, 0x00, 0x0a, 0xfd, 0x80, 0x80, 0x00, 0x38, 0x06, 0x32, 0x0b, 0x72,
+      0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x58,
+      0x49, 0x4e, 0x46, 0x4f, 0x3a, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f,
+      0x72, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f,
+      0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x3a, 0x20, 0x43, 0x61, 0x6c, 0x6c,
+      0x69, 0x6e, 0x67, 0x20, 0x57, 0x72, 0x69, 0x74, 0x65, 0x20, 0x5b, 0x6f,
+      0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x30, 0x2c, 0x20, 0x73, 0x69, 0x7a,
+      0x65, 0x3d, 0x36, 0x35, 0x35, 0x33, 0x36, 0x5d, 0x66, 0x6f, 0x72, 0x20,
+      0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f,
+      0x66, 0x73, 0x63, 0x21, 0x28, 0xe6, 0xc4, 0x80, 0xf1, 0xbd, 0xdc, 0xa5,
+      0xbc, 0x15, 0x08, 0x07, 0x10, 0xf6, 0x0b, 0x18, 0xf6, 0x0b, 0x20, 0x00,
+      0x0a, 0x80, 0x81, 0x80, 0x00, 0x38, 0x04, 0x32, 0x12, 0x76, 0x65, 0x6e,
+      0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72,
+      0x61, 0x67, 0x65, 0x42, 0x54, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f,
+      0x72, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x77, 0x5f, 0x69, 0x6f, 0x76, 0x65,
+      0x63, 0x5f, 0x63, 0x62, 0x3a, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f,
+      0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f, 0x66, 0x73, 0x63, 0x3a, 0x20, 0x72,
+      0x65, 0x71, 0x5f, 0x68, 0x3d, 0x30, 0x78, 0x31, 0x34, 0x20, 0x6d, 0x73,
+      0x67, 0x5f, 0x69, 0x64, 0x3d, 0x33, 0x3a, 0x20, 0x52, 0x2f, 0x57, 0x20,
+      0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x72, 0x65, 0x63, 0x65,
+      0x69, 0x76, 0x65, 0x64, 0x0a, 0x28, 0xc3, 0xe0, 0xf9, 0xf0, 0xbd, 0xdc,
+      0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xe9, 0x06, 0x18, 0xe9, 0x06, 0x20,
+      0x00, 0x0a, 0xc1, 0x80, 0x80, 0x00, 0x38, 0x04, 0x32, 0x12, 0x76, 0x65,
+      0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f,
+      0x72, 0x61, 0x67, 0x65, 0x42, 0x15, 0x77, 0x61, 0x6b, 0x65, 0x6c, 0x6f,
+      0x63, 0x6b, 0x20, 0x61, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x3a,
+      0x20, 0x31, 0x0a, 0x28, 0xb4, 0xc7, 0x8a, 0xf1, 0xbd, 0xdc, 0xa5, 0xbc,
+      0x15, 0x08, 0x00, 0x10, 0xe9, 0x06, 0x18, 0xe9, 0x06, 0x20, 0x00, 0x0a,
+      0x84, 0x81, 0x80, 0x00, 0x38, 0x04, 0x32, 0x12, 0x76, 0x65, 0x6e, 0x64,
+      0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61,
+      0x67, 0x65, 0x42, 0x58, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72,
+      0x61, 0x67, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74,
+      0x68, 0x72, 0x65, 0x61, 0x64, 0x3a, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74,
+      0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f, 0x66, 0x73, 0x63, 0x3a, 0x20,
+      0x55, 0x6e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x77, 0x6f, 0x72, 0x6b,
+      0x65, 0x72, 0x20, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x20, 0x28, 0x74,
+      0x68, 0x5f, 0x69, 0x64, 0x3a, 0x20, 0x34, 0x39, 0x30, 0x31, 0x31, 0x34,
+      0x39, 0x35, 0x34, 0x34, 0x36, 0x34, 0x29, 0x0a, 0x28, 0x8e, 0xe0, 0x8e,
+      0xf1, 0xbd, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xe9, 0x06, 0x18,
+      0xf6, 0x0b, 0x20, 0x00, 0x0a, 0x83, 0x81, 0x80, 0x00, 0x38, 0x04, 0x32,
+      0x12, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74, 0x5f,
+      0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x57, 0x72, 0x6d, 0x74,
+      0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6c, 0x69,
+      0x65, 0x6e, 0x74, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x3a, 0x20,
+      0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f,
+      0x66, 0x73, 0x63, 0x3a, 0x20, 0x72, 0x65, 0x71, 0x5f, 0x68, 0x3d, 0x30,
+      0x78, 0x31, 0x34, 0x20, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x3d, 0x33,
+      0x3a, 0x20, 0x42, 0x79, 0x74, 0x65, 0x73, 0x20, 0x77, 0x72, 0x69, 0x74,
+      0x74, 0x65, 0x6e, 0x20, 0x3d, 0x20, 0x36, 0x35, 0x35, 0x33, 0x36, 0x0a,
+      0x28, 0x8a, 0xe1, 0xa0, 0xf2, 0xbd, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00,
+      0x10, 0xe9, 0x06, 0x18, 0xf6, 0x0b, 0x20, 0x00, 0x0a, 0x88, 0x81, 0x80,
+      0x00, 0x38, 0x04, 0x32, 0x12, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x2e,
+      0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42,
+      0x5c, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+      0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x68, 0x72, 0x65,
+      0x61, 0x64, 0x3a, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x6d, 0x6f,
+      0x64, 0x65, 0x6d, 0x5f, 0x66, 0x73, 0x63, 0x3a, 0x20, 0x72, 0x65, 0x71,
+      0x5f, 0x68, 0x3d, 0x30, 0x78, 0x31, 0x34, 0x20, 0x6d, 0x73, 0x67, 0x5f,
+      0x69, 0x64, 0x3d, 0x33, 0x3a, 0x20, 0x53, 0x65, 0x6e, 0x64, 0x20, 0x72,
+      0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x3a, 0x20, 0x72, 0x65, 0x73,
+      0x3d, 0x30, 0x20, 0x65, 0x72, 0x72, 0x3d, 0x30, 0x0a, 0x28, 0xf8, 0x89,
+      0xa2, 0xf2, 0xbd, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xe9, 0x06,
+      0x18, 0xf6, 0x0b, 0x20, 0x00, 0x0a, 0xae, 0x81, 0x80, 0x00, 0x38, 0x04,
+      0x32, 0x12, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74,
+      0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x81, 0x01, 0x72,
+      0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x63,
+      0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64,
+      0x3a, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x6d, 0x6f, 0x64, 0x65,
+      0x6d, 0x5f, 0x66, 0x73, 0x63, 0x3a, 0x20, 0x41, 0x62, 0x6f, 0x75, 0x74,
+      0x20, 0x74, 0x6f, 0x20, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x72, 0x6d,
+      0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x20, 0x63, 0x6c,
+      0x69, 0x65, 0x6e, 0x74, 0x20, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x20,
+      0x28, 0x74, 0x68, 0x5f, 0x69, 0x64, 0x3a, 0x20, 0x34, 0x39, 0x30, 0x31,
+      0x31, 0x34, 0x39, 0x35, 0x34, 0x34, 0x36, 0x34, 0x29, 0x20, 0x77, 0x61,
+      0x6b, 0x65, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x72, 0x65, 0x6c, 0x65, 0x61,
+      0x73, 0x65, 0x64, 0x3a, 0x20, 0x31, 0x0a, 0x0a, 0x28, 0xb9, 0xcb, 0xa9,
+      0xf2, 0xbd, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xe9, 0x06, 0x18,
+      0xf6, 0x0b, 0x20, 0x00, 0x0a, 0xf4, 0x80, 0x80, 0x00, 0x38, 0x06, 0x32,
+      0x0b, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+      0x42, 0x4f, 0x49, 0x4e, 0x46, 0x4f, 0x3a, 0x72, 0x6d, 0x74, 0x5f, 0x73,
+      0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e,
+      0x74, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x3a, 0x20, 0x44, 0x6f,
+      0x6e, 0x65, 0x20, 0x57, 0x72, 0x69, 0x74, 0x65, 0x20, 0x28, 0x62, 0x79,
+      0x74, 0x65, 0x73, 0x20, 0x3d, 0x20, 0x36, 0x35, 0x35, 0x33, 0x36, 0x29,
+      0x20, 0x66, 0x6f, 0x72, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x6d,
+      0x6f, 0x64, 0x65, 0x6d, 0x5f, 0x66, 0x73, 0x63, 0x21, 0x28, 0xd6, 0xbd,
+      0x8f, 0xf2, 0xbd, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x07, 0x10, 0xf6, 0x0b,
+      0x18, 0xf6, 0x0b, 0x20, 0x00, 0x0a, 0xe7, 0x80, 0x80, 0x00, 0x38, 0x04,
+      0x32, 0x12, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74,
+      0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x3b, 0x72, 0x6d,
+      0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x64, 0x69,
+      0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x62, 0x3a,
+      0x20, 0x63, 0x6c, 0x6e, 0x74, 0x5f, 0x68, 0x3d, 0x30, 0x78, 0x31, 0x34,
+      0x20, 0x63, 0x6f, 0x6e, 0x6e, 0x5f, 0x68, 0x3d, 0x30, 0x78, 0x37, 0x32,
+      0x31, 0x64, 0x61, 0x30, 0x37, 0x31, 0x30, 0x30, 0x0a, 0x28, 0x92, 0xd5,
+      0xc7, 0xf2, 0xbd, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xe9, 0x06,
+      0x18, 0xe9, 0x06, 0x20, 0x00, 0x18, 0x8f, 0x4e, 0x0a, 0x70, 0x40, 0x92,
+      0xaf, 0xd5, 0x8d, 0x90, 0x55, 0xba, 0x02, 0xe0, 0x80, 0x80, 0x00, 0x0a,
+      0xdb, 0x80, 0x80, 0x00, 0x38, 0x04, 0x32, 0x08, 0x70, 0x65, 0x72, 0x66,
+      0x65, 0x74, 0x74, 0x6f, 0x42, 0x39, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69,
+      0x64, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73,
+      0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x63, 0x63, 0x3a, 0x32, 0x39, 0x31,
+      0x20, 0x53, 0x65, 0x65, 0x6e, 0x20, 0x31, 0x31, 0x20, 0x41, 0x6e, 0x64,
+      0x72, 0x6f, 0x69, 0x64, 0x20, 0x6c, 0x6f, 0x67, 0x20, 0x65, 0x76, 0x65,
+      0x6e, 0x74, 0x73, 0x28, 0xd0, 0x8f, 0xfa, 0xf4, 0xbd, 0xdc, 0xa5, 0xbc,
+      0x15, 0x08, 0x00, 0x10, 0xb5, 0x58, 0x18, 0xb5, 0x58, 0x20, 0x00, 0x18,
+      0x8f, 0x4e, 0x0a, 0xfa, 0x0a, 0x40, 0x9c, 0xc7, 0xf8, 0xbc, 0x90, 0x55,
+      0xba, 0x02, 0xea, 0x8a, 0x80, 0x00, 0x0a, 0xe4, 0x80, 0x80, 0x00, 0x38,
+      0x04, 0x32, 0x12, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d,
+      0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x38, 0x72,
+      0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x63,
+      0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x62, 0x3a, 0x20, 0x63,
+      0x6c, 0x6e, 0x74, 0x5f, 0x68, 0x3d, 0x30, 0x78, 0x31, 0x35, 0x20, 0x63,
+      0x6f, 0x6e, 0x6e, 0x5f, 0x68, 0x3d, 0x30, 0x78, 0x37, 0x32, 0x31, 0x64,
+      0x61, 0x30, 0x37, 0x31, 0x30, 0x30, 0x0a, 0x28, 0xb4, 0xa2, 0x8b, 0xa5,
+      0xbe, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xe9, 0x06, 0x18, 0xe9,
+      0x06, 0x20, 0x00, 0x0a, 0x80, 0x81, 0x80, 0x00, 0x38, 0x04, 0x32, 0x12,
+      0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74, 0x5f, 0x73,
+      0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x54, 0x72, 0x6d, 0x74, 0x5f,
+      0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x77, 0x5f, 0x69,
+      0x6f, 0x76, 0x65, 0x63, 0x5f, 0x63, 0x62, 0x3a, 0x20, 0x2f, 0x62, 0x6f,
+      0x6f, 0x74, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f, 0x66, 0x73, 0x31,
+      0x3a, 0x20, 0x72, 0x65, 0x71, 0x5f, 0x68, 0x3d, 0x30, 0x78, 0x31, 0x35,
+      0x20, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x3d, 0x33, 0x3a, 0x20, 0x52,
+      0x2f, 0x57, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x72,
+      0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x0a, 0x28, 0x87, 0xf1, 0x8e,
+      0xa5, 0xbe, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xe9, 0x06, 0x18,
+      0xe9, 0x06, 0x20, 0x00, 0x0a, 0xc1, 0x80, 0x80, 0x00, 0x38, 0x04, 0x32,
+      0x12, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74, 0x5f,
+      0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x15, 0x77, 0x61, 0x6b,
+      0x65, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x61, 0x63, 0x71, 0x75, 0x69, 0x72,
+      0x65, 0x64, 0x3a, 0x20, 0x31, 0x0a, 0x28, 0x8e, 0x8e, 0x9e, 0xa5, 0xbe,
+      0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xe9, 0x06, 0x18, 0xe9, 0x06,
+      0x20, 0x00, 0x0a, 0x84, 0x81, 0x80, 0x00, 0x38, 0x04, 0x32, 0x12, 0x76,
+      0x65, 0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74,
+      0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x58, 0x72, 0x6d, 0x74, 0x5f, 0x73,
+      0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e,
+      0x74, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x3a, 0x20, 0x2f, 0x62,
+      0x6f, 0x6f, 0x74, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f, 0x66, 0x73,
+      0x31, 0x3a, 0x20, 0x55, 0x6e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x77,
+      0x6f, 0x72, 0x6b, 0x65, 0x72, 0x20, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64,
+      0x20, 0x28, 0x74, 0x68, 0x5f, 0x69, 0x64, 0x3a, 0x20, 0x34, 0x39, 0x30,
+      0x31, 0x32, 0x38, 0x37, 0x30, 0x30, 0x36, 0x34, 0x30, 0x29, 0x0a, 0x28,
+      0x9a, 0xa4, 0xa1, 0xa5, 0xbe, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10,
+      0xe9, 0x06, 0x18, 0xf1, 0x0b, 0x20, 0x00, 0x0a, 0xf3, 0x80, 0x80, 0x00,
+      0x38, 0x06, 0x32, 0x0b, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72,
+      0x61, 0x67, 0x65, 0x42, 0x4e, 0x49, 0x4e, 0x46, 0x4f, 0x3a, 0x72, 0x6d,
+      0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x77,
+      0x5f, 0x69, 0x6f, 0x76, 0x65, 0x63, 0x5f, 0x63, 0x62, 0x3a, 0x20, 0x57,
+      0x72, 0x69, 0x74, 0x65, 0x20, 0x69, 0x6f, 0x76, 0x65, 0x63, 0x20, 0x72,
+      0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x72, 0x65, 0x63, 0x65, 0x69,
+      0x76, 0x65, 0x64, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x2f, 0x62, 0x6f, 0x6f,
+      0x74, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f, 0x66, 0x73, 0x31, 0x28,
+      0x8e, 0xb1, 0xff, 0xa4, 0xbe, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x07, 0x10,
+      0xe9, 0x06, 0x18, 0xe9, 0x06, 0x20, 0x00, 0x0a, 0xff, 0x80, 0x80, 0x00,
+      0x38, 0x06, 0x32, 0x0b, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72,
+      0x61, 0x67, 0x65, 0x42, 0x5a, 0x49, 0x4e, 0x46, 0x4f, 0x3a, 0x72, 0x6d,
+      0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6c,
+      0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x3a,
+      0x20, 0x43, 0x61, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x20, 0x57, 0x72, 0x69,
+      0x74, 0x65, 0x20, 0x5b, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x30,
+      0x2c, 0x20, 0x73, 0x69, 0x7a, 0x65, 0x3d, 0x32, 0x30, 0x39, 0x37, 0x31,
+      0x35, 0x32, 0x5d, 0x66, 0x6f, 0x72, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74,
+      0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f, 0x66, 0x73, 0x31, 0x21, 0x28,
+      0xe6, 0xae, 0x92, 0xa5, 0xbe, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x07, 0x10,
+      0xf1, 0x0b, 0x18, 0xf1, 0x0b, 0x20, 0x00, 0x0a, 0x85, 0x81, 0x80, 0x00,
+      0x38, 0x04, 0x32, 0x12, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72,
+      0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x59,
+      0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f,
+      0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x61,
+      0x64, 0x3a, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x6d, 0x6f, 0x64,
+      0x65, 0x6d, 0x5f, 0x66, 0x73, 0x31, 0x3a, 0x20, 0x72, 0x65, 0x71, 0x5f,
+      0x68, 0x3d, 0x30, 0x78, 0x31, 0x35, 0x20, 0x6d, 0x73, 0x67, 0x5f, 0x69,
+      0x64, 0x3d, 0x33, 0x3a, 0x20, 0x42, 0x79, 0x74, 0x65, 0x73, 0x20, 0x77,
+      0x72, 0x69, 0x74, 0x74, 0x65, 0x6e, 0x20, 0x3d, 0x20, 0x32, 0x30, 0x39,
+      0x37, 0x31, 0x35, 0x32, 0x0a, 0x28, 0x96, 0x87, 0xd7, 0xb3, 0xbe, 0xdc,
+      0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xe9, 0x06, 0x18, 0xf1, 0x0b, 0x20,
+      0x00, 0x0a, 0x88, 0x81, 0x80, 0x00, 0x38, 0x04, 0x32, 0x12, 0x76, 0x65,
+      0x6e, 0x64, 0x6f, 0x72, 0x2e, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f,
+      0x72, 0x61, 0x67, 0x65, 0x42, 0x5c, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74,
+      0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74,
+      0x5f, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x3a, 0x20, 0x2f, 0x62, 0x6f,
+      0x6f, 0x74, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f, 0x66, 0x73, 0x31,
+      0x3a, 0x20, 0x72, 0x65, 0x71, 0x5f, 0x68, 0x3d, 0x30, 0x78, 0x31, 0x35,
+      0x20, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x3d, 0x33, 0x3a, 0x20, 0x53,
+      0x65, 0x6e, 0x64, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+      0x3a, 0x20, 0x72, 0x65, 0x73, 0x3d, 0x30, 0x20, 0x65, 0x72, 0x72, 0x3d,
+      0x30, 0x0a, 0x28, 0xda, 0xb3, 0xd8, 0xb3, 0xbe, 0xdc, 0xa5, 0xbc, 0x15,
+      0x08, 0x00, 0x10, 0xe9, 0x06, 0x18, 0xf1, 0x0b, 0x20, 0x00, 0x0a, 0xae,
+      0x81, 0x80, 0x00, 0x38, 0x04, 0x32, 0x12, 0x76, 0x65, 0x6e, 0x64, 0x6f,
+      0x72, 0x2e, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+      0x65, 0x42, 0x81, 0x01, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72,
+      0x61, 0x67, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74,
+      0x68, 0x72, 0x65, 0x61, 0x64, 0x3a, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74,
+      0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f, 0x66, 0x73, 0x31, 0x3a, 0x20,
+      0x41, 0x62, 0x6f, 0x75, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x6c, 0x6f,
+      0x63, 0x6b, 0x20, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61,
+      0x67, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x74, 0x68,
+      0x72, 0x65, 0x61, 0x64, 0x20, 0x28, 0x74, 0x68, 0x5f, 0x69, 0x64, 0x3a,
+      0x20, 0x34, 0x39, 0x30, 0x31, 0x32, 0x38, 0x37, 0x30, 0x30, 0x36, 0x34,
+      0x30, 0x29, 0x20, 0x77, 0x61, 0x6b, 0x65, 0x6c, 0x6f, 0x63, 0x6b, 0x20,
+      0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x64, 0x3a, 0x20, 0x31, 0x0a,
+      0x0a, 0x28, 0xe4, 0xa2, 0xe0, 0xb3, 0xbe, 0xdc, 0xa5, 0xbc, 0x15, 0x08,
+      0x00, 0x10, 0xe9, 0x06, 0x18, 0xf1, 0x0b, 0x20, 0x00, 0x0a, 0xe7, 0x80,
+      0x80, 0x00, 0x38, 0x04, 0x32, 0x12, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72,
+      0x2e, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+      0x42, 0x3b, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+      0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
+      0x5f, 0x63, 0x62, 0x3a, 0x20, 0x63, 0x6c, 0x6e, 0x74, 0x5f, 0x68, 0x3d,
+      0x30, 0x78, 0x31, 0x35, 0x20, 0x63, 0x6f, 0x6e, 0x6e, 0x5f, 0x68, 0x3d,
+      0x30, 0x78, 0x37, 0x32, 0x31, 0x64, 0x61, 0x30, 0x37, 0x31, 0x30, 0x30,
+      0x0a, 0x28, 0xeb, 0xea, 0x8f, 0xb4, 0xbe, 0xdc, 0xa5, 0xbc, 0x15, 0x08,
+      0x00, 0x10, 0xe9, 0x06, 0x18, 0xe9, 0x06, 0x20, 0x00, 0x0a, 0xf6, 0x80,
+      0x80, 0x00, 0x38, 0x06, 0x32, 0x0b, 0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74,
+      0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x51, 0x49, 0x4e, 0x46, 0x4f, 0x3a,
+      0x72, 0x6d, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f,
+      0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x61,
+      0x64, 0x3a, 0x20, 0x44, 0x6f, 0x6e, 0x65, 0x20, 0x57, 0x72, 0x69, 0x74,
+      0x65, 0x20, 0x28, 0x62, 0x79, 0x74, 0x65, 0x73, 0x20, 0x3d, 0x20, 0x32,
+      0x30, 0x39, 0x37, 0x31, 0x35, 0x32, 0x29, 0x20, 0x66, 0x6f, 0x72, 0x20,
+      0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6d, 0x5f,
+      0x66, 0x73, 0x31, 0x21, 0x28, 0x9e, 0xa9, 0xc5, 0xb3, 0xbe, 0xdc, 0xa5,
+      0xbc, 0x15, 0x08, 0x07, 0x10, 0xf1, 0x0b, 0x18, 0xf1, 0x0b, 0x20, 0x00,
+      0x18, 0x8f, 0x4e, 0x0a, 0x70, 0x40, 0xd2, 0x9f, 0x8f, 0xed, 0x90, 0x55,
+      0xba, 0x02, 0xe0, 0x80, 0x80, 0x00, 0x0a, 0xdb, 0x80, 0x80, 0x00, 0x38,
+      0x04, 0x32, 0x08, 0x70, 0x65, 0x72, 0x66, 0x65, 0x74, 0x74, 0x6f, 0x42,
+      0x39, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x5f, 0x6c, 0x6f, 0x67,
+      0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+      0x2e, 0x63, 0x63, 0x3a, 0x32, 0x39, 0x31, 0x20, 0x53, 0x65, 0x65, 0x6e,
+      0x20, 0x31, 0x31, 0x20, 0x41, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x20,
+      0x6c, 0x6f, 0x67, 0x20, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x28, 0xf4,
+      0x9d, 0x96, 0xd4, 0xbe, 0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xb5,
+      0x58, 0x18, 0xb5, 0x58, 0x20, 0x00, 0x18, 0x8f, 0x4e, 0x0a, 0x4e, 0x40,
+      0xfe, 0xf3, 0x83, 0xd1, 0x9e, 0x55, 0xba, 0x02, 0xbe, 0x80, 0x80, 0x00,
+      0x0a, 0xb9, 0x80, 0x80, 0x00, 0x38, 0x06, 0x32, 0x00, 0x42, 0x1f, 0x74,
+      0x75, 0x69, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x3a, 0x73, 0x75, 0x69, 0x73,
+      0x76, 0x63, 0x20, 0x20, 0x65, 0x78, 0x69, 0x73, 0x20, 0x61, 0x6c, 0x72,
+      0x65, 0x61, 0x64, 0x79, 0x20, 0x30, 0x28, 0x9f, 0x92, 0xdf, 0xdf, 0xcc,
+      0xdc, 0xa5, 0xbc, 0x15, 0x08, 0x00, 0x10, 0xd9, 0x06, 0x18, 0xd9, 0x06,
+      0x20, 0x00, 0x18, 0x8f, 0x4e, 0x0a, 0x1b, 0x40, 0x9b, 0x83, 0xa7, 0xba,
+      0xba, 0x55, 0xba, 0x02, 0x8b, 0x80, 0x80, 0x00, 0x12, 0x86, 0x80, 0x80,
+      0x00, 0x08, 0x1a, 0x18, 0x00, 0x10, 0x00, 0x18, 0x8f, 0x4e};
+
+  static const uint8_t kFilterBytecode[]{
+      0x0b, 0x01, 0x00, 0x0b, 0x60, 0x13, 0x02, 0x19, 0x23, 0x08, 0x2b, 0x09,
+      0x33, 0x0f, 0x3b, 0x10, 0x41, 0x4b, 0x05, 0x51, 0x5b, 0x12, 0x63, 0x72,
+      0x69, 0x8b, 0x02, 0x21, 0x93, 0x02, 0x33, 0x9b, 0x02, 0x35, 0xa1, 0x02,
+      0xab, 0x02, 0x37, 0xb3, 0x02, 0x07, 0xbb, 0x02, 0x3e, 0xc3, 0x02, 0x3d,
+      0xca, 0x02, 0x02, 0xdb, 0x02, 0x1b, 0xe3, 0x02, 0x11, 0xeb, 0x02, 0x40,
+      0xf3, 0x02, 0x04, 0xfb, 0x02, 0x41, 0x83, 0x03, 0x34, 0x8b, 0x03, 0x42,
+      0x91, 0x03, 0x9b, 0x03, 0x43, 0xa3, 0x03, 0x46, 0xab, 0x03, 0x49, 0xb3,
+      0x03, 0x04, 0xbb, 0x03, 0x07, 0xc3, 0x03, 0x4b, 0xcb, 0x03, 0x4c, 0xd1,
+      0x03, 0xdb, 0x03, 0x73, 0xe3, 0x03, 0x5f, 0xeb, 0x03, 0x5b, 0xf3, 0x03,
+      0x4d, 0xfb, 0x03, 0x04, 0x83, 0x04, 0x5d, 0x8b, 0x04, 0x4e, 0x93, 0x04,
+      0x4f, 0x9b, 0x04, 0x50, 0xa3, 0x04, 0x51, 0xab, 0x04, 0x3c, 0xb3, 0x04,
+      0x0e, 0xbb, 0x04, 0x04, 0xc3, 0x04, 0x69, 0xcb, 0x04, 0x53, 0xd3, 0x04,
+      0x3c, 0xdb, 0x04, 0x04, 0xe3, 0x04, 0x56, 0xeb, 0x04, 0x58, 0xf3, 0x04,
+      0x5a, 0xa3, 0x38, 0x70, 0x00, 0x0b, 0x03, 0x13, 0x04, 0x19, 0x00, 0x0a,
+      0x03, 0x23, 0x04, 0x29, 0x00, 0x0a, 0x03, 0x00, 0x0b, 0x06, 0x11, 0x00,
+      0x0a, 0x0a, 0x5b, 0x07, 0x62, 0x03, 0x00, 0x0a, 0x04, 0x00, 0x0a, 0x02,
+      0x1b, 0x04, 0x00, 0x0b, 0x0a, 0x13, 0x0d, 0x1b, 0x0e, 0x21, 0x2b, 0x0e,
+      0x00, 0x0a, 0x0d, 0x73, 0x0b, 0x7a, 0x02, 0x00, 0x0a, 0x09, 0x53, 0x0c,
+      0x00, 0x0a, 0x02, 0x1b, 0x0c, 0x23, 0x0c, 0x2a, 0x04, 0x00, 0x0a, 0x05,
+      0x00, 0x0a, 0x02, 0x00, 0x0b, 0x07, 0x11, 0x00, 0x0b, 0x0e, 0x13, 0x0e,
+      0x1b, 0x11, 0x22, 0x02, 0x33, 0x0e, 0x39, 0x43, 0x0e, 0x49, 0x53, 0x0e,
+      0x00, 0x0a, 0x08, 0x00, 0x0a, 0x03, 0x23, 0x13, 0x2b, 0x15, 0x33, 0x20,
+      0x42, 0x05, 0x82, 0x01, 0x02, 0xa1, 0x01, 0xab, 0x01, 0x0e, 0xb2, 0x01,
+      0x02, 0xc3, 0x01, 0x16, 0xcb, 0x01, 0x0e, 0xd3, 0x01, 0x15, 0xdb, 0x01,
+      0x0e, 0xe3, 0x01, 0x07, 0xeb, 0x01, 0x1e, 0xf2, 0x01, 0x02, 0x83, 0x02,
+      0x1f, 0x8b, 0x02, 0x07, 0x91, 0x02, 0x9b, 0x02, 0x0e, 0xa1, 0x02, 0xb3,
+      0x02, 0x04, 0xbb, 0x02, 0x15, 0xc3, 0x02, 0x15, 0xcb, 0x02, 0x04, 0xd1,
+      0x02, 0xdb, 0x02, 0x15, 0xe2, 0x02, 0x03, 0x00, 0x0a, 0x07, 0x43, 0x14,
+      0x4a, 0x02, 0x5b, 0x13, 0x63, 0x13, 0x00, 0x0a, 0x02, 0x1b, 0x14, 0x23,
+      0x14, 0x2a, 0x04, 0x00, 0x09, 0x00, 0x0b, 0x17, 0x12, 0x0c, 0x73, 0x19,
+      0x7b, 0x1c, 0x83, 0x01, 0x1d, 0x8b, 0x01, 0x1b, 0x00, 0x0b, 0x0d, 0x13,
+      0x18, 0x00, 0x0a, 0x2e, 0x00, 0x0a, 0x03, 0x23, 0x1a, 0x2b, 0x1a, 0x33,
+      0x1b, 0x00, 0x0a, 0x09, 0x53, 0x07, 0x00, 0x0a, 0x07, 0x00, 0x09, 0x13,
+      0x1a, 0x00, 0x0a, 0x03, 0x23, 0x1a, 0x00, 0x0a, 0x03, 0x23, 0x0e, 0x2a,
+      0x02, 0x00, 0x0a, 0x0a, 0x00, 0x0a, 0x04, 0x32, 0x0a, 0x92, 0x01, 0x02,
+      0x00, 0x0b, 0x22, 0x13, 0x23, 0x1a, 0x03, 0x33, 0x04, 0x3b, 0x07, 0x42,
+      0x03, 0x5b, 0x15, 0x62, 0x03, 0x81, 0x01, 0x8b, 0x01, 0x32, 0x92, 0x01,
+      0x02, 0xa3, 0x01, 0x1b, 0xab, 0x01, 0x15, 0xb2, 0x01, 0x03, 0xcb, 0x01,
+      0x0d, 0xda, 0x01, 0x05, 0x83, 0x02, 0x15, 0x00, 0x09, 0x21, 0x00, 0x0b,
+      0x24, 0x12, 0x02, 0x00, 0x0a, 0x04, 0x32, 0x03, 0xa3, 0x06, 0x25, 0xab,
+      0x06, 0x0d, 0xb3, 0x06, 0x26, 0xbb, 0x06, 0x27, 0xc3, 0x06, 0x1b, 0xcb,
+      0x06, 0x28, 0xd3, 0x06, 0x07, 0xdb, 0x06, 0x2b, 0xe3, 0x06, 0x07, 0xeb,
+      0x06, 0x15, 0xf3, 0x06, 0x2a, 0xfb, 0x06, 0x2c, 0x83, 0x07, 0x0e, 0x8b,
+      0x07, 0x07, 0x93, 0x07, 0x15, 0x9b, 0x07, 0x2f, 0xc1, 0x3e, 0xcb, 0x3e,
+      0x30, 0xf9, 0xff, 0xff, 0xff, 0x07, 0x00, 0x0a, 0x03, 0x52, 0x02, 0x63,
+      0x15, 0x6a, 0x02, 0x00, 0x0a, 0x05, 0x33, 0x0e, 0x00, 0x0a, 0x04, 0x32,
+      0x03, 0x00, 0x0a, 0x02, 0x22, 0x02, 0x33, 0x29, 0x3a, 0x05, 0x6a, 0x0e,
+      0x00, 0x2a, 0x02, 0x00, 0x0a, 0x02, 0x1b, 0x0e, 0x22, 0x04, 0x00, 0x09,
+      0x1a, 0x02, 0x00, 0x0a, 0x0d, 0x7b, 0x2d, 0x83, 0x01, 0x2e, 0x8a, 0x01,
+      0x02, 0x00, 0x0a, 0x02, 0x1b, 0x0e, 0x21, 0x00, 0x0b, 0x0d, 0x11, 0x00,
+      0x09, 0xa3, 0x06, 0x0e, 0x00, 0x0a, 0x05, 0x33, 0x31, 0x00, 0x0a, 0x0e,
+      0x00, 0x09, 0x13, 0x0d, 0x19, 0x00, 0x09, 0x13, 0x34, 0x1a, 0x02, 0x00,
+      0x0a, 0x09, 0x00, 0x0b, 0x36, 0x12, 0x09, 0x5b, 0x0d, 0x00, 0x0a, 0x13,
+      0x00, 0x0b, 0x0e, 0x13, 0x07, 0x1b, 0x0e, 0x23, 0x11, 0x2b, 0x38, 0x32,
+      0x02, 0x00, 0x09, 0x13, 0x3b, 0x1a, 0x02, 0x2b, 0x39, 0x32, 0x09, 0x00,
+      0x0a, 0x03, 0x23, 0x3a, 0x2a, 0x02, 0x00, 0x0b, 0x04, 0x00, 0x0a, 0x06,
+      0x42, 0x02, 0x00, 0x0a, 0x06, 0x00, 0x0b, 0x07, 0x13, 0x04, 0x00, 0x0b,
+      0x3f, 0x13, 0x04, 0x00, 0x0a, 0x08, 0x4b, 0x07, 0x00, 0x0b, 0x07, 0x12,
+      0x02, 0x00, 0x0b, 0x0d, 0x12, 0x02, 0x00, 0x0a, 0x06, 0x3b, 0x0e, 0x42,
+      0x02, 0x00, 0x0b, 0x44, 0x12, 0x02, 0x00, 0x0b, 0x45, 0x13, 0x45, 0x00,
+      0x09, 0x13, 0x04, 0x1b, 0x0e, 0x00, 0x0b, 0x47, 0x13, 0x04, 0x19, 0x00,
+      0x0b, 0x48, 0x13, 0x0d, 0x1a, 0x03, 0x00, 0x0a, 0x03, 0x2a, 0x06, 0x00,
+      0x0a, 0x05, 0x33, 0x0e, 0x3b, 0x4a, 0x42, 0x08, 0x00, 0x0b, 0x0e, 0x13,
+      0x0e, 0x1b, 0x0e, 0x00, 0x09, 0x13, 0x1b, 0x23, 0x0e, 0x2a, 0x02, 0x3b,
+      0x0e, 0x43, 0x0e, 0x4b, 0x11, 0x00, 0x0b, 0x0d, 0x00, 0x0a, 0x08, 0x4b,
+      0x07, 0x82, 0x01, 0x05, 0x00, 0x0b, 0x0d, 0x13, 0x3c, 0x00, 0x0a, 0x06,
+      0x82, 0x01, 0x03, 0x9b, 0x01, 0x15, 0x00, 0x0b, 0x0e, 0x00, 0x09, 0x13,
+      0x52, 0x00, 0x0a, 0x0f, 0x00, 0x0a, 0x02, 0x1b, 0x54, 0x00, 0x09, 0x13,
+      0x55, 0x1b, 0x07, 0x00, 0x0a, 0x04, 0x2b, 0x07, 0x00, 0x0b, 0x04, 0x13,
+      0x11, 0x1b, 0x0d, 0x23, 0x57, 0x2b, 0x15, 0x00, 0x0a, 0x0b, 0x00, 0x0b,
+      0x59, 0x12, 0x02, 0x23, 0x0e, 0x00, 0x0b, 0x07, 0x00, 0x0a, 0x02, 0x1b,
+      0x0e, 0x00, 0x0a, 0x02, 0x1b, 0x5c, 0x00, 0x09, 0x13, 0x04, 0x00, 0x0a,
+      0x02, 0x1b, 0x5e, 0x00, 0x0a, 0x02, 0x1b, 0x0e, 0x23, 0x0e, 0x00, 0x0a,
+      0x02, 0x1b, 0x1b, 0x23, 0x11, 0x29, 0x33, 0x0d, 0x3b, 0x0e, 0x43, 0x3c,
+      0x00, 0x09, 0x13, 0x61, 0x19, 0x23, 0x57, 0x00, 0x0a, 0x02, 0x1b, 0x0e,
+      0x23, 0x1b, 0x5b, 0x0e, 0x63, 0x04, 0x6b, 0x0e, 0x73, 0x04, 0x7b, 0x04,
+      0x83, 0x01, 0x04, 0x8b, 0x01, 0x0d, 0x93, 0x01, 0x04, 0x9b, 0x01, 0x04,
+      0xa3, 0x01, 0x0d, 0xab, 0x01, 0x15, 0xb3, 0x01, 0x15, 0xbb, 0x01, 0x0e,
+      0xc3, 0x01, 0x15, 0xcb, 0x01, 0x15, 0xd3, 0x01, 0x15, 0xdb, 0x01, 0x0d,
+      0xe3, 0x01, 0x3c, 0xeb, 0x01, 0x04, 0xf3, 0x01, 0x3c, 0xfb, 0x01, 0x0d,
+      0x83, 0x02, 0x3c, 0x8b, 0x02, 0x1b, 0x93, 0x02, 0x3c, 0x9b, 0x02, 0x0d,
+      0xa3, 0x02, 0x04, 0xab, 0x02, 0x0e, 0xb3, 0x02, 0x0e, 0xbb, 0x02, 0x0e,
+      0xc3, 0x02, 0x04, 0xcb, 0x02, 0x0d, 0xd3, 0x02, 0x0d, 0xdb, 0x02, 0x07,
+      0xe3, 0x02, 0x04, 0xeb, 0x02, 0x1b, 0xf3, 0x02, 0x04, 0xfb, 0x02, 0x15,
+      0x83, 0x03, 0x0e, 0x8b, 0x03, 0x15, 0x93, 0x03, 0x1b, 0x9b, 0x03, 0x15,
+      0xa3, 0x03, 0x0d, 0xab, 0x03, 0x15, 0xb3, 0x03, 0x15, 0xbb, 0x03, 0x15,
+      0xc3, 0x03, 0x15, 0xcb, 0x03, 0x15, 0xd3, 0x03, 0x0e, 0xdb, 0x03, 0x0d,
+      0xe3, 0x03, 0x15, 0xeb, 0x03, 0x15, 0xf3, 0x03, 0x15, 0xfb, 0x03, 0x15,
+      0x83, 0x04, 0x15, 0x8b, 0x04, 0x04, 0x93, 0x04, 0x0e, 0x9b, 0x04, 0x0d,
+      0xa3, 0x04, 0x04, 0xab, 0x04, 0x04, 0xb3, 0x04, 0x04, 0xbb, 0x04, 0x0d,
+      0xc3, 0x04, 0x04, 0xcb, 0x04, 0x04, 0xd3, 0x04, 0x04, 0xdb, 0x04, 0x04,
+      0xe3, 0x04, 0x0e, 0xeb, 0x04, 0x07, 0xf3, 0x04, 0x07, 0xfb, 0x04, 0x62,
+      0x83, 0x05, 0x04, 0x8b, 0x05, 0x07, 0x93, 0x05, 0x11, 0x9b, 0x05, 0x0d,
+      0xa3, 0x05, 0x62, 0xab, 0x05, 0x0e, 0xb3, 0x05, 0x04, 0xbb, 0x05, 0x1b,
+      0xc3, 0x05, 0x04, 0xcb, 0x05, 0x15, 0xd3, 0x05, 0x15, 0xdb, 0x05, 0x11,
+      0xe3, 0x05, 0x0e, 0xeb, 0x05, 0x0e, 0xf3, 0x05, 0x1f, 0xfb, 0x05, 0x04,
+      0x83, 0x06, 0x15, 0x8b, 0x06, 0x0d, 0x93, 0x06, 0x0d, 0x9b, 0x06, 0x0d,
+      0xa3, 0x06, 0x3c, 0xab, 0x06, 0x3c, 0xb3, 0x06, 0x3c, 0xbb, 0x06, 0x3c,
+      0xc3, 0x06, 0x07, 0xcb, 0x06, 0x07, 0xd3, 0x06, 0x07, 0xdb, 0x06, 0x15,
+      0xe3, 0x06, 0x04, 0xeb, 0x06, 0x0e, 0xf3, 0x06, 0x07, 0xfb, 0x06, 0x04,
+      0x83, 0x07, 0x04, 0x8b, 0x07, 0x04, 0x93, 0x07, 0x0d, 0x9b, 0x07, 0x0d,
+      0xa3, 0x07, 0x0d, 0xab, 0x07, 0x0d, 0xb3, 0x07, 0x0d, 0xbb, 0x07, 0x0d,
+      0xc3, 0x07, 0x3c, 0xcb, 0x07, 0x04, 0xd3, 0x07, 0x0d, 0xdb, 0x07, 0x15,
+      0xe3, 0x07, 0x3c, 0xeb, 0x07, 0x3c, 0xf3, 0x07, 0x1b, 0x83, 0x08, 0x1b,
+      0x8b, 0x08, 0x3c, 0x93, 0x08, 0x0d, 0x9b, 0x08, 0x0d, 0xa3, 0x08, 0x04,
+      0xab, 0x08, 0x0e, 0xb3, 0x08, 0x07, 0xbb, 0x08, 0x57, 0xc3, 0x08, 0x07,
+      0xcb, 0x08, 0x04, 0xd3, 0x08, 0x07, 0xdb, 0x08, 0x11, 0xe3, 0x08, 0x1b,
+      0xeb, 0x08, 0x34, 0xf3, 0x08, 0x1f, 0xfb, 0x08, 0x0d, 0x83, 0x09, 0x0d,
+      0x8b, 0x09, 0x3c, 0x93, 0x09, 0x04, 0x9b, 0x09, 0x0e, 0xa3, 0x09, 0x04,
+      0xab, 0x09, 0x3c, 0xb3, 0x09, 0x04, 0xbb, 0x09, 0x3c, 0xc3, 0x09, 0x3c,
+      0xcb, 0x09, 0x04, 0xd3, 0x09, 0x1b, 0xdb, 0x09, 0x07, 0xe3, 0x09, 0x0d,
+      0xeb, 0x09, 0x04, 0xf3, 0x09, 0x04, 0xfb, 0x09, 0x04, 0x83, 0x0a, 0x04,
+      0x8b, 0x0a, 0x1b, 0x93, 0x0a, 0x1f, 0x9b, 0x0a, 0x11, 0xa3, 0x0a, 0x07,
+      0xab, 0x0a, 0x07, 0xb3, 0x0a, 0x0d, 0xbb, 0x0a, 0x11, 0xc3, 0x0a, 0x0d,
+      0xcb, 0x0a, 0x0d, 0xd3, 0x0a, 0x1b, 0xdb, 0x0a, 0x04, 0xe3, 0x0a, 0x1b,
+      0xeb, 0x0a, 0x0d, 0xf3, 0x0a, 0x3c, 0xfb, 0x0a, 0x0d, 0x83, 0x0b, 0x1b,
+      0x8b, 0x0b, 0x0d, 0x93, 0x0b, 0x3c, 0x9b, 0x0b, 0x3c, 0xa3, 0x0b, 0x3c,
+      0xab, 0x0b, 0x07, 0xb3, 0x0b, 0x0d, 0xbb, 0x0b, 0x11, 0xc3, 0x0b, 0x07,
+      0xcb, 0x0b, 0x0d, 0xd3, 0x0b, 0x0d, 0xdb, 0x0b, 0x04, 0xe3, 0x0b, 0x0d,
+      0xeb, 0x0b, 0x0d, 0xf3, 0x0b, 0x0e, 0xfb, 0x0b, 0x0e, 0x83, 0x0c, 0x04,
+      0x8b, 0x0c, 0x0e, 0x93, 0x0c, 0x0e, 0x9b, 0x0c, 0x0e, 0xa3, 0x0c, 0x0d,
+      0xab, 0x0c, 0x0d, 0xb3, 0x0c, 0x04, 0xbb, 0x0c, 0x07, 0xc3, 0x0c, 0x63,
+      0xcb, 0x0c, 0x0d, 0xd3, 0x0c, 0x0d, 0xdb, 0x0c, 0x1f, 0xe3, 0x0c, 0x3c,
+      0xeb, 0x0c, 0x0d, 0xf3, 0x0c, 0x0e, 0xfb, 0x0c, 0x04, 0x83, 0x0d, 0x04,
+      0x8b, 0x0d, 0x11, 0x93, 0x0d, 0x1f, 0x9b, 0x0d, 0x04, 0xa3, 0x0d, 0x0e,
+      0xab, 0x0d, 0x0d, 0xb3, 0x0d, 0x0d, 0xbb, 0x0d, 0x04, 0xc3, 0x0d, 0x04,
+      0xcb, 0x0d, 0x07, 0xd3, 0x0d, 0x04, 0xdb, 0x0d, 0x0d, 0xb3, 0x0e, 0x0d,
+      0xbb, 0x0e, 0x04, 0xc3, 0x0e, 0x1f, 0xcb, 0x0e, 0x1b, 0xd3, 0x0e, 0x0d,
+      0xdb, 0x0e, 0x07, 0xe3, 0x0e, 0x07, 0xeb, 0x0e, 0x04, 0xf3, 0x0e, 0x07,
+      0xfb, 0x0e, 0x07, 0x83, 0x0f, 0x04, 0x8b, 0x0f, 0x0e, 0x93, 0x0f, 0x04,
+      0x9b, 0x0f, 0x0d, 0xa3, 0x0f, 0x11, 0xab, 0x0f, 0x11, 0xb3, 0x0f, 0x3c,
+      0xbb, 0x0f, 0x1f, 0xc3, 0x0f, 0x11, 0xcb, 0x0f, 0x04, 0xd3, 0x0f, 0x04,
+      0xdb, 0x0f, 0x0d, 0xe3, 0x0f, 0x04, 0xeb, 0x0f, 0x3c, 0xf3, 0x0f, 0x0d,
+      0xfb, 0x0f, 0x11, 0x83, 0x10, 0x0d, 0x8b, 0x10, 0x04, 0x93, 0x10, 0x11,
+      0x9b, 0x10, 0x0d, 0xa3, 0x10, 0x04, 0xab, 0x10, 0x0d, 0xb3, 0x10, 0x0d,
+      0xbb, 0x10, 0x04, 0xc3, 0x10, 0x07, 0xcb, 0x10, 0x07, 0xd3, 0x10, 0x04,
+      0xdb, 0x10, 0x0d, 0xe3, 0x10, 0x0d, 0xeb, 0x10, 0x04, 0xf3, 0x10, 0x3c,
+      0xfb, 0x10, 0x0d, 0x83, 0x11, 0x04, 0x8b, 0x11, 0x0d, 0x93, 0x11, 0x0e,
+      0x9b, 0x11, 0x0e, 0xa3, 0x11, 0x0e, 0xab, 0x11, 0x0e, 0xb3, 0x11, 0x0e,
+      0xbb, 0x11, 0x0e, 0xc3, 0x11, 0x15, 0xcb, 0x11, 0x07, 0xd3, 0x11, 0x0d,
+      0xdb, 0x11, 0x0d, 0xe3, 0x11, 0x0d, 0xeb, 0x11, 0x3c, 0xf3, 0x11, 0x3c,
+      0xfb, 0x11, 0x0d, 0x83, 0x12, 0x15, 0x8b, 0x12, 0x07, 0x93, 0x12, 0x07,
+      0x9b, 0x12, 0x15, 0xa3, 0x12, 0x04, 0xab, 0x12, 0x04, 0xb3, 0x12, 0x07,
+      0xbb, 0x12, 0x07, 0xc3, 0x12, 0x0e, 0xcb, 0x12, 0x0e, 0xd3, 0x12, 0x0e,
+      0xdb, 0x12, 0x0d, 0xe3, 0x12, 0x3c, 0xeb, 0x12, 0x0d, 0xf3, 0x12, 0x3c,
+      0xfb, 0x12, 0x0e, 0x83, 0x13, 0x15, 0x8b, 0x13, 0x15, 0x93, 0x13, 0x15,
+      0x9b, 0x13, 0x0d, 0xa3, 0x13, 0x1b, 0xab, 0x13, 0x07, 0xb3, 0x13, 0x04,
+      0xbb, 0x13, 0x04, 0xc3, 0x13, 0x07, 0xcb, 0x13, 0x07, 0xd3, 0x13, 0x04,
+      0xdb, 0x13, 0x04, 0xe3, 0x13, 0x07, 0xeb, 0x13, 0x07, 0xf3, 0x13, 0x07,
+      0xfb, 0x13, 0x07, 0x83, 0x14, 0x15, 0x8b, 0x14, 0x15, 0x93, 0x14, 0x0e,
+      0x9b, 0x14, 0x04, 0xa3, 0x14, 0x04, 0xab, 0x14, 0x3c, 0xb3, 0x14, 0x04,
+      0xbb, 0x14, 0x64, 0xc3, 0x14, 0x07, 0xcb, 0x14, 0x15, 0xd3, 0x14, 0x0e,
+      0xdb, 0x14, 0x07, 0xe3, 0x14, 0x0e, 0xeb, 0x14, 0x0d, 0xf3, 0x14, 0x15,
+      0xfb, 0x14, 0x04, 0x83, 0x15, 0x0e, 0x8b, 0x15, 0x0e, 0x93, 0x15, 0x04,
+      0x9b, 0x15, 0x66, 0xa3, 0x15, 0x04, 0xab, 0x15, 0x07, 0xb3, 0x15, 0x0e,
+      0xbb, 0x15, 0x07, 0xc3, 0x15, 0x07, 0xcb, 0x15, 0x07, 0xd3, 0x15, 0x07,
+      0xdb, 0x15, 0x04, 0xe3, 0x15, 0x3c, 0xeb, 0x15, 0x67, 0xf3, 0x15, 0x07,
+      0xfb, 0x15, 0x04, 0x83, 0x16, 0x07, 0x8b, 0x16, 0x07, 0x93, 0x16, 0x04,
+      0x9b, 0x16, 0x11, 0xa3, 0x16, 0x68, 0xab, 0x16, 0x3c, 0xb3, 0x16, 0x07,
+      0x00, 0x0a, 0x10, 0x00, 0x0a, 0x14, 0x00, 0x09, 0x13, 0x65, 0x00, 0x09,
+      0x1a, 0x03, 0x00, 0x00, 0x09, 0x22, 0x03, 0x00, 0x0a, 0x0c, 0x00, 0x0b,
+      0x6a, 0x00, 0x0b, 0x6b, 0x00, 0x0a, 0x03, 0x23, 0x6c, 0x2b, 0x6e, 0x3b,
+      0x6d, 0x52, 0x02, 0x00, 0x09, 0x13, 0x6d, 0x1b, 0x6c, 0x23, 0x6e, 0x33,
+      0x6d, 0x43, 0x6f, 0x4b, 0x0e, 0x51, 0x00, 0x0a, 0x07, 0x49, 0x00, 0x09,
+      0x13, 0x0e, 0x29, 0x00, 0x09, 0x13, 0x66, 0x00, 0x0a, 0x04, 0x2b, 0x71,
+      0x00, 0x09, 0x13, 0x71, 0x1a, 0x04, 0x00, 0x0b, 0x0e, 0x13, 0x0e, 0x1b,
+      0x0e, 0x23, 0x07, 0x2b, 0x0e, 0x33, 0x07, 0x3b, 0x0e, 0x83, 0x01, 0x0e,
+      0x8b, 0x01, 0x0e, 0x93, 0x01, 0x0e, 0x9b, 0x01, 0x11, 0xa3, 0x01, 0x0e,
+      0xab, 0x01, 0x07, 0xb3, 0x01, 0x0e, 0xbb, 0x01, 0x04, 0xc3, 0x01, 0x07,
+      0xcb, 0x01, 0x0e, 0xd3, 0x01, 0x0e, 0x00, 0x5b, 0x74, 0x63, 0x75, 0xd1,
+      0x03, 0x00, 0x59, 0xf9, 0x01, 0xe9, 0x02, 0x00, 0x0b, 0x2d, 0x00, 0x89,
+      0x8c, 0xb0, 0x80, 0x08};
+
+  MessageFilter filt;
+  ASSERT_TRUE(
+      filt.LoadFilterBytecode(kFilterBytecode, sizeof(kFilterBytecode)));
+
+  // Pass the trace in input splitting it in slices of arbitrary size.
+  std::vector<MessageFilter::InputSlice> input_slices;
+  for (size_t i = 0; i < sizeof(kTraceData);) {
+    std::minstd_rand0 rnd_engine(0);
+    size_t slice_size = rnd_engine() % 4096;
+    slice_size = std::min(slice_size, sizeof(kTraceData) - i);
+    input_slices.emplace_back(
+        MessageFilter::InputSlice{kTraceData + i, slice_size});
+    i += slice_size;
+  }
+
+  auto filtered_data =
+      filt.FilterMessageFragments(input_slices.data(), input_slices.size());
+
+  EXPECT_GT(filtered_data.size, 0u);
+  EXPECT_LE(filtered_data.size, sizeof(kTraceData));
+
+  perfetto::protos::Trace original_trace;
+  ASSERT_TRUE(original_trace.ParseFromArray(kTraceData, sizeof(kTraceData)));
+
+  perfetto::protos::Trace filtered_trace;
+  ASSERT_TRUE(filtered_trace.ParseFromArray(
+      filtered_data.data.get(), static_cast<int>(filtered_data.size)));
+
+  // Check that the re-serialized traces are identical.
+  std::string original_ser = original_trace.SerializeAsString();
+  std::string filter_ser = filtered_trace.SerializeAsString();
+
+  EXPECT_EQ(filtered_trace.packet_size(), original_trace.packet_size());
+
+  // Don't use EXPECT_EQ, the string is too big. If this check fails, the gtest
+  // diffing algorithm will take several minutes to compute the diff.
+  // That would mistakenly look like a CI or timing-related issue as the gtest
+  // would fail due to timeout.
+  // If this check fails, use base::HexDump() to investigate.
+  EXPECT_TRUE(original_ser == filter_ser);
+}
+
+}  // namespace
+}  // namespace protozero
diff --git a/src/protozero/filtering/message_tokenizer.h b/src/protozero/filtering/message_tokenizer.h
new file mode 100644
index 0000000..124b461
--- /dev/null
+++ b/src/protozero/filtering/message_tokenizer.h
@@ -0,0 +1,199 @@
+/*
+ * 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_PROTOZERO_FILTERING_MESSAGE_TOKENIZER_H_
+#define SRC_PROTOZERO_FILTERING_MESSAGE_TOKENIZER_H_
+
+#include <stdint.h>
+
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/protozero/proto_utils.h"
+
+namespace protozero {
+
+// A helper class for schema-less tokenizing of protobuf messages.
+// This class takes a stream of proto-encoded bytes, pushed one by one in input
+// via Push(octet), and returns a stream of tokens (each Push() call can return
+// 0 or 1 token).
+// A "token" contains metadata about a field, specifically: its ID, its wire
+// type and:
+//  - For varint and fixed32/64 fields: its payload.
+//  - For string and bytes fields: the length of its payload.
+//    In this case the caller is supposed to "eat" those N bytes before calling
+//    Push() again.
+// Note that this class cannot differentiate between a string/bytes field or
+// a submessage, because they are encoded in the same way. The caller is
+// supposed to know whether a field can be recursed into by just keep calling
+// Push() or is a string that should be skipped.
+// This is inline to allow the compiler to see through the Push method and
+// avoid a function call for each byte.
+class MessageTokenizer {
+ public:
+  struct Token {
+    uint32_t field_id;  // 0 == not valid.
+    proto_utils::ProtoWireType type;
+
+    // For kLengthDelimited, |value| represent the length of the payload.
+    uint64_t value;
+
+    inline bool valid() const { return field_id != 0; }
+    bool operator==(const Token& o) const {
+      return field_id == o.field_id && type == o.type && value == o.value;
+    }
+  };
+
+  // Pushes a byte in input and returns a token, only when getting to the last
+  // byte of each field. Specifically:
+  // - For varint and fixed32 fields, the Token is returned after the last byte
+  //   of the numeric payload is pushed.
+  // - For length-delimited fields, this returns after the last byte of the
+  //   length is pushed (i.e. right before the payload starts). The caller is
+  //   expected to either skip the next |value| bytes (in the case of a string
+  //   or bytes fields) or keep calling Push, in the case of a submessage.
+  inline Token Push(uint8_t octet) {
+    using protozero::proto_utils::ProtoWireType;
+
+    // Parsing a fixed32/64 field is the only case where we don't have to do
+    // any varint decoding. This is why this block is before the remaining
+    // switch statement below (all the rest is a varint).
+    if (PERFETTO_UNLIKELY(state_ == kFixedIntValue)) {
+      PERFETTO_DCHECK(fixed_int_bits_ == 32 || fixed_int_bits_ == 64);
+      fixed_int_value_ |= static_cast<uint64_t>(octet) << fixed_int_shift_;
+      fixed_int_shift_ += 8;
+      if (fixed_int_shift_ < fixed_int_bits_)
+        return Token{};  // Intermediate byte of a fixed32/64.
+      auto wire_type = fixed_int_bits_ == 32 ? ProtoWireType::kFixed32
+                                             : ProtoWireType::kFixed64;
+      uint64_t fixed_int_value = fixed_int_value_;
+      fixed_int_value_ = fixed_int_shift_ = fixed_int_bits_ = 0;
+      state_ = kFieldPreamble;
+      return Token{field_id_, wire_type, fixed_int_value};
+    }
+
+    // At this point either we are: (i) parsing a field preamble; (ii) parsing a
+    // varint field paylod; (iii) parsing the length of a length-delimited
+    // field. In all cases, we need to decode a varint before proceeding.
+    varint_ |= static_cast<uint64_t>(octet & 0x7F) << varint_shift_;
+    if (octet & 0x80) {
+      varint_shift_ += 7;
+      if (PERFETTO_UNLIKELY(varint_shift_ >= 64)) {
+        varint_shift_ = 0;
+        state_ = kInvalidVarInt;
+      }
+      return Token{};  // Still parsing a varint.
+    }
+
+    uint64_t varint = varint_;
+    varint_ = 0;
+    varint_shift_ = 0;
+
+    switch (state_) {
+      case kFieldPreamble: {
+        auto field_type = static_cast<uint32_t>(varint & 7u);  // 7 = 0..0111
+        field_id_ = static_cast<uint32_t>(varint >> 3);
+
+        // The field type is legit, now check it's well formed and within
+        // boundaries.
+        if (field_type == static_cast<uint32_t>(ProtoWireType::kVarInt)) {
+          state_ = kVarIntValue;
+        } else if (field_type ==
+                       static_cast<uint32_t>(ProtoWireType::kFixed32) ||
+                   field_type ==
+                       static_cast<uint32_t>(ProtoWireType::kFixed64)) {
+          state_ = kFixedIntValue;
+          fixed_int_shift_ = 0;
+          fixed_int_value_ = 0;
+          fixed_int_bits_ =
+              field_type == static_cast<uint32_t>(ProtoWireType::kFixed32) ? 32
+                                                                           : 64;
+        } else if (field_type ==
+                   static_cast<uint32_t>(ProtoWireType::kLengthDelimited)) {
+          state_ = kLenDelimited;
+        } else {
+          state_ = kInvalidFieldType;
+        }
+        return Token{};
+      }
+
+      case kVarIntValue: {
+        // Return the varint field payload and go back to the next field.
+        state_ = kFieldPreamble;
+        return Token{field_id_, ProtoWireType::kVarInt, varint};
+      }
+
+      case kLenDelimited: {
+        const auto payload_len = varint;
+        if (payload_len > protozero::proto_utils::kMaxMessageLength) {
+          state_ = kMessageTooBig;
+          return Token{};
+        }
+        state_ = kFieldPreamble;
+        // At this point the caller is expected to consume the next
+        // |payload_len| bytes.
+        return Token{field_id_, ProtoWireType::kLengthDelimited, payload_len};
+      }
+
+      case kFixedIntValue:
+        // Unreacheable because of the if before the switch.
+        PERFETTO_DCHECK(false);
+        break;
+
+      // Unrecoverable error states.
+      case kInvalidFieldType:
+      case kMessageTooBig:
+      case kInvalidVarInt:
+        break;
+    }  // switch(state_)
+
+    return Token{};  // Keep GCC happy.
+  }
+
+  // Returns true if the tokenizer FSM has reached quiescence (i.e. if we are
+  // NOT in the middle of parsing a field).
+  bool idle() const {
+    return state_ == kFieldPreamble && varint_shift_ == 0 &&
+           fixed_int_shift_ == 0;
+  }
+
+  // Only for reporting parser errors in the trace.
+  uint32_t state() const { return static_cast<uint32_t>(state_); }
+
+ private:
+  enum State {
+    kFieldPreamble = 0,  // Parsing the varint for the field preamble.
+    kVarIntValue = 1,    // Parsing the payload of a varint field.
+    kFixedIntValue = 2,  // Parsing the payload of a fixed32/64 field.
+    kLenDelimited = 3,   // Parsing the length of a length-delimited field.
+
+    // Unrecoverable error states:
+    kInvalidFieldType = 4,  // Encountered an invalid field type.
+    kMessageTooBig = 5,     // Size of the length delimited message was too big.
+    kInvalidVarInt = 6,     // Varint larger than 64 bits.
+  };
+
+  State state_ = kFieldPreamble;
+  uint32_t field_id_ = 0;
+  uint64_t varint_ = 0;
+  uint32_t varint_shift_ = 0;
+  uint32_t fixed_int_shift_ = 0;
+  uint32_t fixed_int_bits_ = 0;
+  uint64_t fixed_int_value_ = 0;
+};
+
+}  // namespace protozero
+
+#endif  // SRC_PROTOZERO_FILTERING_MESSAGE_TOKENIZER_H_
diff --git a/src/protozero/filtering/message_tokenizer_unittest.cc b/src/protozero/filtering/message_tokenizer_unittest.cc
new file mode 100644
index 0000000..666c4cf
--- /dev/null
+++ b/src/protozero/filtering/message_tokenizer_unittest.cc
@@ -0,0 +1,179 @@
+/*
+ * 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 "test/gtest_and_gmock.h"
+
+#include "perfetto/protozero/message.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "src/protozero/filtering/message_tokenizer.h"
+
+namespace protozero {
+
+using proto_utils::ProtoWireType;
+using ::testing::ElementsAre;
+using Token = MessageTokenizer::Token;
+
+// For ASSERT_THAT(ElementsAre(...))
+inline std::ostream& operator<<(std::ostream& stream, const Token& t) {
+  stream << "{" << t.field_id << ", ";
+  switch (t.type) {
+    case ProtoWireType::kVarInt:
+      stream << "varint, ";
+      break;
+    case ProtoWireType::kFixed32:
+      stream << "fixed32, ";
+      break;
+    case ProtoWireType::kFixed64:
+      stream << "fixed64, ";
+      break;
+    case ProtoWireType::kLengthDelimited:
+      stream << "lendelim, ";
+      break;
+    default:
+      stream << "???, ";
+      break;
+  }
+  stream << t.value << "}";
+  return stream;
+}
+
+namespace {
+
+TEST(MessageTokenizerTest, FlatMessage) {
+  HeapBuffered<Message> msg;
+  msg->AppendVarInt(/*field_id*/ 1, 42u);
+  msg->AppendVarInt(/*field_id*/ 1, 1000u);
+  msg->AppendVarInt(/*field_id*/ 2, 1000000000ull);
+  msg->AppendVarInt(/*field_id*/ 3, 0xFF001234DEADBEEFull);
+  msg->AppendString(/*field_id*/ 4, "foo");
+  msg->AppendFixed(/*field_id*/ 5, 0xFFAAFFFFu);
+  msg->AppendString(/*field_id*/ 4, "foobar");
+  msg->AppendFixed(/*field_id*/ 6, uint64_t(1ull << 63));
+  msg->AppendVarInt(/*field_id*/ 1000, 1001ull);
+  msg->AppendVarInt(/*field_id*/ 1000000, 1000001ull);
+  msg->AppendVarInt(/*field_id*/ 1 << 28, uint64_t(1ull << 63));
+
+  // Treat all len-delimited fields as strings/bytes and just eat their payload.
+  MessageTokenizer tokenizer;
+  std::vector<Token> tokens;
+  size_t eat_bytes = 0;
+  for (uint8_t octet : msg.SerializeAsArray()) {
+    if (eat_bytes > 0) {
+      --eat_bytes;
+      continue;
+    }
+    auto token = tokenizer.Push(octet);
+    if (token.valid())
+      tokens.emplace_back(token);
+    if (token.type == ProtoWireType::kLengthDelimited) {
+      ASSERT_EQ(eat_bytes, 0u);
+      eat_bytes = static_cast<size_t>(token.value);
+    }
+  }
+  EXPECT_TRUE(tokenizer.idle());
+  EXPECT_THAT(
+      tokens,
+      ElementsAre(
+          Token{1, ProtoWireType::kVarInt, 42u},
+          Token{1, ProtoWireType::kVarInt, 1000u},
+          Token{2, ProtoWireType::kVarInt, 1000000000ull},
+          Token{3, ProtoWireType::kVarInt, 0xFF001234DEADBEEFull},
+          Token{4, ProtoWireType::kLengthDelimited, 3},
+          Token{5, ProtoWireType::kFixed32, 0xFFAAFFFFu},
+          Token{4, ProtoWireType::kLengthDelimited, 6},
+          Token{6, ProtoWireType::kFixed64, uint64_t(1ull << 63)},
+          Token{1000, ProtoWireType::kVarInt, 1001ull},
+          Token{1000000, ProtoWireType::kVarInt, 1000001ull},
+          Token{1 << 28, ProtoWireType::kVarInt, uint64_t(1ull << 63)}));
+}
+
+TEST(MessageTokenizerTest, NestedMessage) {
+  HeapBuffered<Message> msg;
+  msg->AppendVarInt(/*field_id*/ 1, 101u);
+  {
+    auto* nested = msg->BeginNestedMessage<Message>(2);
+    nested->AppendVarInt(/*field_id*/ 3, 103u);
+    nested->AppendFixed(/*field_id*/ 4, 104u);
+    {
+      auto* nested2 = nested->BeginNestedMessage<Message>(5);
+      nested2->AppendVarInt(/*field_id*/ 6, 106u);
+      nested2->AppendFixed(/*field_id*/ 7, 107u);
+      nested2->Finalize();
+    }
+    nested->AppendFixed(/*field_id*/ 8, 0x42420000u);
+    nested->Finalize();
+  }
+  msg->AppendFixed(/*field_id*/ 9, uint64_t(1ull << 63));
+
+  // Tokenize the message. This treat all len delimited fields as submessage
+  // and test the recursion logic.
+  MessageTokenizer tokenizer;
+  std::vector<Token> tokens;
+  for (uint8_t octet : msg.SerializeAsArray()) {
+    auto token = tokenizer.Push(octet);
+    if (token.valid())
+      tokens.emplace_back(token);
+  }
+  EXPECT_TRUE(tokenizer.idle());
+  EXPECT_THAT(
+      tokens,
+      ElementsAre(Token{1, ProtoWireType::kVarInt, 101u},
+                  Token{2, ProtoWireType::kLengthDelimited, 24u},
+                  Token{3, ProtoWireType::kVarInt, 103u},
+                  Token{4, ProtoWireType::kFixed32, 104u},
+                  Token{5, ProtoWireType::kLengthDelimited, 7},
+                  Token{6, ProtoWireType::kVarInt, 106u},
+                  Token{7, ProtoWireType::kFixed32, 107u},
+                  Token{8, ProtoWireType::kFixed32, 0x42420000u},
+                  Token{9, ProtoWireType::kFixed64, uint64_t(1ull << 63)}));
+}
+
+TEST(MessageTokenizerTest, InvlidCases) {
+  {
+    // A very large varint.
+    MessageTokenizer tokenizer;
+    EXPECT_FALSE(tokenizer.Push(0x08).valid());
+    for (int i = 0; i < 14; ++i)
+      EXPECT_FALSE(tokenizer.Push(0xff).valid());
+    EXPECT_FALSE(tokenizer.Push(0x0).valid());
+    EXPECT_FALSE(tokenizer.idle());
+    EXPECT_EQ(tokenizer.state(), 6u);
+  }
+  {
+    // A very large string.
+    MessageTokenizer tokenizer;
+    EXPECT_FALSE(tokenizer.Push(0x0A).valid());
+    EXPECT_FALSE(tokenizer.Push(0xFF).valid());
+    EXPECT_FALSE(tokenizer.Push(0xFF).valid());
+    EXPECT_FALSE(tokenizer.Push(0xFF).valid());
+    EXPECT_FALSE(tokenizer.Push(0xFF).valid());
+    EXPECT_FALSE(tokenizer.Push(0x20).valid());
+    EXPECT_FALSE(tokenizer.idle());
+    EXPECT_EQ(tokenizer.state(), 5u);
+  }
+  {
+    // A field of unknown type (wire type = 0x3).
+    MessageTokenizer tokenizer;
+    EXPECT_FALSE(tokenizer.Push(0x0B).valid());
+    EXPECT_FALSE(tokenizer.Push(0).valid());
+    EXPECT_FALSE(tokenizer.Push(0).valid());
+    EXPECT_FALSE(tokenizer.idle());
+    EXPECT_EQ(tokenizer.state(), 4u);
+  }
+}
+
+}  // namespace
+}  // namespace protozero
diff --git a/src/trace_processor/rpc/proto_ring_buffer.cc b/src/protozero/proto_ring_buffer.cc
similarity index 94%
rename from src/trace_processor/rpc/proto_ring_buffer.cc
rename to src/protozero/proto_ring_buffer.cc
index 3354efe..631d355 100644
--- a/src/trace_processor/rpc/proto_ring_buffer.cc
+++ b/src/protozero/proto_ring_buffer.cc
@@ -14,14 +14,13 @@
  * limitations under the License.
  */
 
-#include "src/trace_processor/rpc/proto_ring_buffer.h"
+#include "src/protozero/proto_ring_buffer.h"
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/paged_memory.h"
 #include "perfetto/protozero/proto_utils.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace protozero {
 
 namespace {
 constexpr size_t kGrowBytes = 128 * 1024;
@@ -74,7 +73,7 @@
 }  // namespace
 
 ProtoRingBuffer::ProtoRingBuffer()
-    : buf_(base::PagedMemory::Allocate(kGrowBytes)) {}
+    : buf_(perfetto::base::PagedMemory::Allocate(kGrowBytes)) {}
 ProtoRingBuffer::~ProtoRingBuffer() = default;
 
 void ProtoRingBuffer::Append(const void* data_void, size_t data_len) {
@@ -107,7 +106,7 @@
 
   size_t avail = buf_.size() - wr_;
   if (data_len > avail) {
-    // This whole section should be hit extremely rare.
+    // This whole section should be hit extremely rarely.
 
     // Try first just recompacting the buffer by moving everything to the left.
     // This can happen if we received "a message and a bit" on each Append call
@@ -137,7 +136,7 @@
         failed_ = true;
         return;
       }
-      auto new_buf = base::PagedMemory::Allocate(new_size);
+      auto new_buf = perfetto::base::PagedMemory::Allocate(new_size);
       memcpy(new_buf.Get(), buf_.Get(), buf_.size());
       buf_ = std::move(new_buf);
       avail = new_size - wr_;
@@ -184,5 +183,4 @@
   return msg;
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace protozero
diff --git a/src/trace_processor/rpc/proto_ring_buffer.h b/src/protozero/proto_ring_buffer.h
similarity index 80%
rename from src/trace_processor/rpc/proto_ring_buffer.h
rename to src/protozero/proto_ring_buffer.h
index 62934ca..d71a231 100644
--- a/src/trace_processor/rpc/proto_ring_buffer.h
+++ b/src/protozero/proto_ring_buffer.h
@@ -14,28 +14,28 @@
  * limitations under the License.
  */
 
-#ifndef SRC_TRACE_PROCESSOR_RPC_PROTO_RING_BUFFER_H_
-#define SRC_TRACE_PROCESSOR_RPC_PROTO_RING_BUFFER_H_
+#ifndef SRC_PROTOZERO_PROTO_RING_BUFFER_H_
+#define SRC_PROTOZERO_PROTO_RING_BUFFER_H_
 
 #include <stdint.h>
 
 #include "perfetto/ext/base/paged_memory.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace protozero {
 
-// This class buffers and tokenizes proto messages used for the TraceProcessor
-// RPC interface (See comments in trace_processor.proto).
-// From a logical level, the RPC is a sequence of protos like this.
+// This class buffers and tokenizes proto messages.
+//
+// From a logical level, it works with a sequence of protos like this.
 // [ header 1 ] [ payload 1   ]
 // [ header 2 ] [ payload 2  ]
 // [ header 3 ] [ payload 3     ]
 // Where [ header ] is a variable-length sequence of:
 // [ Field ID = 1, type = length-delimited] [ length (varint) ].
-// The RPC pipe is byte-oriented, not message-oriented (like a TCP stream).
-// The pipe is not required to respect the boundaries of each message, it only
-// guarantees that data is not lost or duplicated. The following sequence of
-// inbound events is possible:
+//
+// The input to this class is byte-oriented, not message-oriented (like a TCP
+// stream or a pipe). The caller is not required to respect the boundaries of
+// each message; only guarantee that data is not lost or duplicated. The
+// following sequence of inbound events is possible:
 // 1. [ hdr 1 (incomplete) ... ]
 // 2. [ ... hdr 1 ] [ payload 1 ] [ hdr 2 ] [ payoad 2 ] [ hdr 3 ] [ pay... ]
 // 3. [ ...load 3 ]
@@ -60,10 +60,10 @@
 // Internally this is similar to a ring-buffer, with the caveat that it never
 // wraps, it only expands. Expansions are rare. The deal is that in most cases
 // the read cursor follows very closely the write cursor. For instance, if the
-// uderlying behaves as a dgram socket, after each Append, the read cursor will
-// chase completely the write cursor. Even if the underyling stream is not
-// always atomic, the expectation is that the read cursor will eventually reach
-// the write one within few messages.
+// underlying transport behaves as a dgram socket, after each Append, the read
+// cursor will chase completely the write cursor. Even if the underlying stream
+// is not always atomic, the expectation is that the read cursor will eventually
+// reach the write one within few messages.
 // A visual example, imagine we have four messages: 2it 4will 2be 4fine
 // Visually:
 //
@@ -126,14 +126,13 @@
   size_t avail() const { return buf_.size() - (wr_ - rd_); }
 
  private:
-  base::PagedMemory buf_;
+  perfetto::base::PagedMemory buf_;
   Message fastpath_{};
   bool failed_ = false;  // Set in case of an unrecoverable framing faiulre.
   size_t rd_ = 0;        // Offset of the read cursor in |buf_|.
   size_t wr_ = 0;        // Offset of the write cursor in |buf_|.
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace protozero
 
-#endif  // SRC_TRACE_PROCESSOR_RPC_PROTO_RING_BUFFER_H_
+#endif  // SRC_PROTOZERO_PROTO_RING_BUFFER_H_
diff --git a/src/trace_processor/rpc/proto_ring_buffer_unittest.cc b/src/protozero/proto_ring_buffer_unittest.cc
similarity index 95%
rename from src/trace_processor/rpc/proto_ring_buffer_unittest.cc
rename to src/protozero/proto_ring_buffer_unittest.cc
index 0e3944c..263baf3 100644
--- a/src/trace_processor/rpc/proto_ring_buffer_unittest.cc
+++ b/src/protozero/proto_ring_buffer_unittest.cc
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "src/trace_processor/rpc/proto_ring_buffer.h"
+#include "src/protozero/proto_ring_buffer.h"
 
 #include <stdint.h>
 #include <sys/types.h>
@@ -30,8 +30,7 @@
 
 using testing::ElementsAre;
 
-namespace perfetto {
-namespace trace_processor {
+namespace protozero {
 
 // For ASSERT_EQ()
 inline bool operator==(const ProtoRingBuffer::Message& a,
@@ -58,6 +57,8 @@
 
 namespace {
 
+using ::perfetto::base::ArraySize;
+
 constexpr uint32_t kMaxMsgSize = ProtoRingBuffer::kMaxMsgSize;
 
 class ProtoRingBufferTest : public ::testing::Test {
@@ -137,14 +138,14 @@
 
   uint32_t frag_lens[] = {120, 20, 471, 1};
   uint32_t frag_sum = 0;
-  for (uint32_t i = 0; i < base::ArraySize(frag_lens); i++)
+  for (uint32_t i = 0; i < ArraySize(frag_lens); i++)
     frag_sum += frag_lens[i];
   ASSERT_EQ(frag_sum, last_msg_.size());
 
   // Append the messages in such a way that each appen either passes a portion
   // of a message (the 20 ones) or more than a message.
   uint32_t written = 0;
-  for (uint32_t i = 0; i < base::ArraySize(frag_lens); i++) {
+  for (uint32_t i = 0; i < ArraySize(frag_lens); i++) {
     buf.Append(&last_msg_[written], frag_lens[i]);
     written += frag_lens[i];
     for (;;) {
@@ -228,5 +229,4 @@
 }
 
 }  // namespace
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace protozero
diff --git a/src/protozero/protoc_plugin/protozero_plugin.cc b/src/protozero/protoc_plugin/protozero_plugin.cc
index 1906ffa..311672a 100644
--- a/src/protozero/protoc_plugin/protozero_plugin.cc
+++ b/src/protozero/protoc_plugin/protozero_plugin.cc
@@ -157,20 +157,6 @@
     return name;
   }
 
-  // Small enums can be written faster without involving VarInt encoder.
-  inline bool IsTinyEnumField(const FieldDescriptor* field) {
-    if (field->type() != FieldDescriptor::TYPE_ENUM)
-      return false;
-    const EnumDescriptor* enumeration = field->enum_type();
-
-    for (int i = 0; i < enumeration->value_count(); ++i) {
-      int32_t value = enumeration->value(i)->number();
-      if (value < 0 || value > 0x7F)
-        return false;
-    }
-    return true;
-  }
-
   // Note: intentionally avoiding depending on protozero sources, as well as
   // protobuf-internal WireFormat/WireFormatLite classes.
   const char* FieldTypeToProtozeroWireType(FieldDescriptor::Type proto_type) {
@@ -243,6 +229,102 @@
     return "";
   }
 
+  const char* FieldToProtoSchemaType(const FieldDescriptor* field) {
+    switch (field->type()) {
+      case FieldDescriptor::TYPE_BOOL:
+        return "kBool";
+      case FieldDescriptor::TYPE_INT32:
+        return "kInt32";
+      case FieldDescriptor::TYPE_INT64:
+        return "kInt64";
+      case FieldDescriptor::TYPE_UINT32:
+        return "kUint32";
+      case FieldDescriptor::TYPE_UINT64:
+        return "kUint64";
+      case FieldDescriptor::TYPE_SINT32:
+        return "kSint32";
+      case FieldDescriptor::TYPE_SINT64:
+        return "kSint64";
+      case FieldDescriptor::TYPE_FIXED32:
+        return "kFixed32";
+      case FieldDescriptor::TYPE_FIXED64:
+        return "kFixed64";
+      case FieldDescriptor::TYPE_SFIXED32:
+        return "kSfixed32";
+      case FieldDescriptor::TYPE_SFIXED64:
+        return "kSfixed64";
+      case FieldDescriptor::TYPE_FLOAT:
+        return "kFloat";
+      case FieldDescriptor::TYPE_DOUBLE:
+        return "kDouble";
+      case FieldDescriptor::TYPE_ENUM:
+        return "kEnum";
+      case FieldDescriptor::TYPE_STRING:
+        return "kString";
+      case FieldDescriptor::TYPE_MESSAGE:
+        return "kMessage";
+      case FieldDescriptor::TYPE_BYTES:
+        return "kBytes";
+
+      case FieldDescriptor::TYPE_GROUP:
+        Abort("Groups not supported.");
+        return "";
+    }
+    Abort("Unrecognized FieldDescriptor::Type.");
+    return "";
+  }
+
+  std::string FieldToCppTypeName(const FieldDescriptor* field) {
+    switch (field->type()) {
+      case FieldDescriptor::TYPE_BOOL:
+        return "bool";
+      case FieldDescriptor::TYPE_INT32:
+        return "int32_t";
+      case FieldDescriptor::TYPE_INT64:
+        return "int64_t";
+      case FieldDescriptor::TYPE_UINT32:
+        return "uint32_t";
+      case FieldDescriptor::TYPE_UINT64:
+        return "uint64_t";
+      case FieldDescriptor::TYPE_SINT32:
+        return "int32_t";
+      case FieldDescriptor::TYPE_SINT64:
+        return "int64_t";
+      case FieldDescriptor::TYPE_FIXED32:
+        return "uint32_t";
+      case FieldDescriptor::TYPE_FIXED64:
+        return "uint64_t";
+      case FieldDescriptor::TYPE_SFIXED32:
+        return "int32_t";
+      case FieldDescriptor::TYPE_SFIXED64:
+        return "int64_t";
+      case FieldDescriptor::TYPE_FLOAT:
+        return "float";
+      case FieldDescriptor::TYPE_DOUBLE:
+        return "double";
+      case FieldDescriptor::TYPE_ENUM:
+        return GetCppClassName(field->enum_type(), true);
+      case FieldDescriptor::TYPE_STRING:
+      case FieldDescriptor::TYPE_BYTES:
+        return "std::string";
+      case FieldDescriptor::TYPE_MESSAGE:
+        return GetCppClassName(field->message_type());
+      case FieldDescriptor::TYPE_GROUP:
+        Abort("Groups not supported.");
+        return "";
+    }
+    Abort("Unrecognized FieldDescriptor::Type.");
+    return "";
+  }
+
+  const char* FieldToRepetitionType(const FieldDescriptor* field) {
+    if (!field->is_repeated())
+      return "kNotRepeated";
+    if (field->is_packed())
+      return "kRepeatedPacked";
+    return "kRepeatedNotPacked";
+  }
+
   void CollectDescriptors() {
     // Collect message descriptors in DFS order.
     std::vector<const Descriptor*> stack;
@@ -382,6 +464,7 @@
         "#define $guard$\n\n"
         "#include <stddef.h>\n"
         "#include <stdint.h>\n\n"
+        "#include \"perfetto/protozero/field_writer.h\"\n"
         "#include \"perfetto/protozero/message.h\"\n"
         "#include \"perfetto/protozero/packed_repeated_fields.h\"\n"
         "#include \"perfetto/protozero/proto_decoder.h\"\n"
@@ -460,14 +543,15 @@
   // where the payload is the concatenation of invidually encoded elements.
   void GeneratePackedRepeatedFieldDescriptor(const FieldDescriptor* field) {
     std::map<std::string, std::string> setter;
-    setter["id"] = std::to_string(field->number());
     setter["name"] = field->lowercase_name();
+    setter["field_metadata"] = GetFieldMetadataTypeName(field);
     setter["action"] = "set";
     setter["buffer_type"] = FieldTypeToPackedBufferType(field->type());
     stub_h_->Print(
         setter,
         "void $action$_$name$(const $buffer_type$& packed_buffer) {\n"
-        "  AppendBytes($id$, packed_buffer.data(), packed_buffer.size());\n"
+        "  AppendBytes($field_metadata$::kFieldId, packed_buffer.data(),\n"
+        "              packed_buffer.size());\n"
         "}\n");
   }
 
@@ -475,110 +559,41 @@
     std::map<std::string, std::string> setter;
     setter["id"] = std::to_string(field->number());
     setter["name"] = field->lowercase_name();
+    setter["field_metadata"] = GetFieldMetadataTypeName(field);
     setter["action"] = field->is_repeated() ? "add" : "set";
+    setter["cpp_type"] = FieldToCppTypeName(field);
+    setter["proto_field_type"] = FieldToProtoSchemaType(field);
 
-    std::string appender;
-    std::string cpp_type;
     const char* code_stub =
         "void $action$_$name$($cpp_type$ value) {\n"
-        "  $appender$($id$, value);\n"
+        "  static constexpr uint32_t field_id = $field_metadata$::kFieldId;\n"
+        "  // Call the appropriate protozero::Message::Append(field_id, ...)\n"
+        "  // method based on the type of the field.\n"
+        "  ::protozero::internal::FieldWriter<\n"
+        "    ::protozero::proto_utils::ProtoSchemaType::$proto_field_type$>\n"
+        "      ::Append(*this, field_id, value);\n"
         "}\n";
 
-    switch (field->type()) {
-      case FieldDescriptor::TYPE_BOOL: {
-        appender = "AppendTinyVarInt";
-        cpp_type = "bool";
-        break;
-      }
-      case FieldDescriptor::TYPE_INT32: {
-        appender = "AppendVarInt";
-        cpp_type = "int32_t";
-        break;
-      }
-      case FieldDescriptor::TYPE_INT64: {
-        appender = "AppendVarInt";
-        cpp_type = "int64_t";
-        break;
-      }
-      case FieldDescriptor::TYPE_UINT32: {
-        appender = "AppendVarInt";
-        cpp_type = "uint32_t";
-        break;
-      }
-      case FieldDescriptor::TYPE_UINT64: {
-        appender = "AppendVarInt";
-        cpp_type = "uint64_t";
-        break;
-      }
-      case FieldDescriptor::TYPE_SINT32: {
-        appender = "AppendSignedVarInt";
-        cpp_type = "int32_t";
-        break;
-      }
-      case FieldDescriptor::TYPE_SINT64: {
-        appender = "AppendSignedVarInt";
-        cpp_type = "int64_t";
-        break;
-      }
-      case FieldDescriptor::TYPE_FIXED32: {
-        appender = "AppendFixed";
-        cpp_type = "uint32_t";
-        break;
-      }
-      case FieldDescriptor::TYPE_FIXED64: {
-        appender = "AppendFixed";
-        cpp_type = "uint64_t";
-        break;
-      }
-      case FieldDescriptor::TYPE_SFIXED32: {
-        appender = "AppendFixed";
-        cpp_type = "int32_t";
-        break;
-      }
-      case FieldDescriptor::TYPE_SFIXED64: {
-        appender = "AppendFixed";
-        cpp_type = "int64_t";
-        break;
-      }
-      case FieldDescriptor::TYPE_FLOAT: {
-        appender = "AppendFixed";
-        cpp_type = "float";
-        break;
-      }
-      case FieldDescriptor::TYPE_DOUBLE: {
-        appender = "AppendFixed";
-        cpp_type = "double";
-        break;
-      }
-      case FieldDescriptor::TYPE_ENUM: {
-        appender = IsTinyEnumField(field) ? "AppendTinyVarInt" : "AppendVarInt";
-        cpp_type = GetCppClassName(field->enum_type(), true);
-        break;
-      }
-      case FieldDescriptor::TYPE_STRING:
-      case FieldDescriptor::TYPE_BYTES: {
-        if (field->type() == FieldDescriptor::TYPE_STRING) {
-          cpp_type = "const char*";
-        } else {
-          cpp_type = "const uint8_t*";
-        }
-        code_stub =
-            "void $action$_$name$(const std::string& value) {\n"
-            "  AppendBytes($id$, value.data(), value.size());\n"
-            "}\n"
-            "void $action$_$name$($cpp_type$ data, size_t size) {\n"
-            "  AppendBytes($id$, data, size);\n"
-            "}\n";
-        break;
-      }
-      case FieldDescriptor::TYPE_GROUP:
-      case FieldDescriptor::TYPE_MESSAGE: {
-        Abort("Unsupported field type.");
-        return;
-      }
+    if (field->type() == FieldDescriptor::TYPE_STRING) {
+      // Strings and bytes should have an additional accessor which specifies
+      // the length explicitly.
+      const char* additional_method =
+          "void $action$_$name$(const char* data, size_t size) {\n"
+          "  AppendBytes($field_metadata$::kFieldId, data, size);\n"
+          "}\n";
+      stub_h_->Print(setter, additional_method);
+    } else if (field->type() == FieldDescriptor::TYPE_BYTES) {
+      const char* additional_method =
+          "void $action$_$name$(const uint8_t* data, size_t size) {\n"
+          "  AppendBytes($field_metadata$::kFieldId, data, size);\n"
+          "}\n";
+      stub_h_->Print(setter, additional_method);
+    } else if (field->type() == FieldDescriptor::TYPE_GROUP ||
+               field->type() == FieldDescriptor::TYPE_MESSAGE) {
+      Abort("Unsupported field type.");
+      return;
     }
-    setter["appender"] = appender;
-    setter["cpp_type"] = cpp_type;
+
     stub_h_->Print(setter, code_stub);
   }
 
@@ -795,14 +810,60 @@
 
     // Field descriptors.
     for (int i = 0; i < message->field_count(); ++i) {
-      GenerateFieldDescriptor(message->field(i));
+      GenerateFieldDescriptor(GetCppClassName(message), message->field(i));
     }
 
     stub_h_->Outdent();
     stub_h_->Print("};\n\n");
   }
 
-  void GenerateFieldDescriptor(const FieldDescriptor* field) {
+  std::string GetFieldMetadataTypeName(const FieldDescriptor* field) {
+    std::string name = field->camelcase_name();
+    if (isalpha(name[0]))
+      name[0] = static_cast<char>(toupper(name[0]));
+    return "FieldMetadata_" + name;
+  }
+
+  std::string GetFieldMetadataVariableName(const FieldDescriptor* field) {
+    std::string name = field->camelcase_name();
+    if (isalpha(name[0]))
+      name[0] = static_cast<char>(toupper(name[0]));
+    return "k" + name;
+  }
+
+  void GenerateFieldMetadata(const std::string& message_cpp_type,
+                             const FieldDescriptor* field) {
+    const char* code_stub = R"(
+using $field_metadata_type$ =
+  ::protozero::proto_utils::FieldMetadata<
+    $field_id$,
+    ::protozero::proto_utils::RepetitionType::$repetition_type$,
+    ::protozero::proto_utils::ProtoSchemaType::$proto_field_type$,
+    $cpp_type$,
+    $message_cpp_type$>;
+
+// Ceci n'est pas une pipe.
+// This is actually a variable of FieldMetadataHelper<FieldMetadata<...>>
+// type (and users are expected to use it as such, hence kCamelCase name).
+// It is declared as a function to keep protozero bindings header-only as
+// inline constexpr variables are not available until C++17 (while inline
+// functions are).
+// TODO(altimin): Use inline variable instead after adopting C++17.  
+static constexpr $field_metadata_type$ $field_metadata_var$() { return {}; }
+)";
+
+    stub_h_->Print(code_stub, "field_id", std::to_string(field->number()),
+                   "repetition_type", FieldToRepetitionType(field),
+                   "proto_field_type", FieldToProtoSchemaType(field),
+                   "cpp_type", FieldToCppTypeName(field), "message_cpp_type",
+                   message_cpp_type, "field_metadata_type",
+                   GetFieldMetadataTypeName(field), "field_metadata_var",
+                   GetFieldMetadataVariableName(field));
+  }
+
+  void GenerateFieldDescriptor(const std::string& message_cpp_type,
+                               const FieldDescriptor* field) {
+    GenerateFieldMetadata(message_cpp_type, field);
     if (field->is_packed()) {
       GeneratePackedRepeatedFieldDescriptor(field);
     } else if (field->type() != FieldDescriptor::TYPE_MESSAGE) {
@@ -852,7 +913,7 @@
         Abort("one wrapper should extend only one message");
         return;
       }
-      GenerateFieldDescriptor(field);
+      GenerateFieldDescriptor(extension_name, field);
     }
     stub_h_->Outdent();
     stub_h_->Print("};\n");
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index ece05ed..54acc3d 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -27,7 +27,7 @@
 if (enable_perfetto_trace_processor_sqlite) {
   static_library("trace_processor") {
     complete_static_lib = true
-    deps = [ ":lib" ]
+    public_deps = [ ":lib" ]
   }
 }
 
@@ -68,7 +68,6 @@
 
 source_set("storage_minimal") {
   sources = [
-    "chunked_trace_reader.h",
     "forwarding_trace_parser.cc",
     "forwarding_trace_parser.h",
     "importers/default_modules.cc",
@@ -77,14 +76,10 @@
     "importers/ftrace/ftrace_module.h",
     "importers/fuchsia/fuchsia_record.h",
     "importers/fuchsia/fuchsia_trace_utils.h",
-    "importers/gzip/gzip_utils.cc",
-    "importers/gzip/gzip_utils.h",
     "importers/json/json_utils.cc",
     "importers/json/json_utils.h",
     "importers/ninja/ninja_log_parser.cc",
     "importers/ninja/ninja_log_parser.h",
-    "importers/proto/args_table_utils.cc",
-    "importers/proto/args_table_utils.h",
     "importers/proto/async_track_set_tracker.cc",
     "importers/proto/async_track_set_tracker.h",
     "importers/proto/chrome_string_lookup.cc",
@@ -135,8 +130,6 @@
     "importers/syscalls/syscall_tracker.h",
     "importers/systrace/systrace_line.h",
     "timestamped_trace_piece.h",
-    "trace_blob_view.h",
-    "trace_parser.h",
     "trace_processor_context.cc",
     "trace_processor_storage.cc",
     "trace_processor_storage_impl.cc",
@@ -150,15 +143,18 @@
     "../base",
     "../protozero",
     "containers",
-    "importers:common",
     "importers:gen_cc_chrome_track_event_descriptor",
     "importers:gen_cc_track_event_descriptor",
+    "importers/common",
     "importers/memory_tracker:graph_processor",
     "storage",
     "tables",
     "types",
     "util",
     "util:descriptors",
+    "util:gzip",
+    "util:interned_message_view",
+    "util:proto_to_args_parser",
   ]
   public_deps = [
     "../../include/perfetto/trace_processor:storage",
@@ -176,17 +172,13 @@
     "../../protos/perfetto/trace/sys_stats:zero",
     "../../protos/perfetto/trace/system_info:zero",
     "../../protos/perfetto/trace/track_event:zero",
+    "util:trace_blob_view",
   ]
 
   # json_utils optionally depends on jsoncpp.
   if (enable_perfetto_trace_processor_json) {
     deps += [ "../../gn:jsoncpp" ]
   }
-
-  # gzip_utils optionally depends on zlib.
-  if (enable_perfetto_zlib) {
-    deps += [ "../../gn:zlib" ]
-  }
 }
 
 source_set("storage_full") {
@@ -269,11 +261,12 @@
     "../../protos/perfetto/trace/gpu:zero",
     "../../protos/perfetto/trace/interned_data:zero",
     "../protozero",
-    "importers:common",
+    "importers/common",
     "storage",
     "tables",
     "types",
     "util",
+    "util:gzip",
   ]
   if (enable_perfetto_trace_processor_json) {
     deps += [ "../../gn:jsoncpp" ]
@@ -309,10 +302,14 @@
       "dynamic/descendant_slice_generator.h",
       "dynamic/describe_slice_generator.cc",
       "dynamic/describe_slice_generator.h",
+      "dynamic/experimental_annotated_stack_generator.cc",
+      "dynamic/experimental_annotated_stack_generator.h",
       "dynamic/experimental_counter_dur_generator.cc",
       "dynamic/experimental_counter_dur_generator.h",
       "dynamic/experimental_flamegraph_generator.cc",
       "dynamic/experimental_flamegraph_generator.h",
+      "dynamic/experimental_flat_slice_generator.cc",
+      "dynamic/experimental_flat_slice_generator.h",
       "dynamic/experimental_sched_upid_generator.cc",
       "dynamic/experimental_sched_upid_generator.h",
       "dynamic/experimental_slice_layout_generator.cc",
@@ -336,14 +333,15 @@
       "../base",
       "../protozero",
       "analysis",
-      "db:lib",
-      "importers:common",
+      "db",
+      "importers/common",
       "metrics:lib",
       "sqlite",
       "storage",
       "tables",
       "types",
       "util",
+      "util:gzip",
       "util:protozero_to_text",
     ]
     public_deps = [
@@ -360,6 +358,7 @@
       ":lib",
       "../../gn:default_deps",
       "../../gn:protobuf_full",
+      "../../protos/perfetto/trace_processor:zero",
       "../../src/profiling:deobfuscator",
       "../../src/profiling/symbolizer",
       "../../src/profiling/symbolizer:symbolize_database",
@@ -398,7 +397,6 @@
     "importers/memory_tracker/graph_processor_unittest.cc",
     "importers/memory_tracker/graph_unittest.cc",
     "importers/memory_tracker/raw_process_memory_node_unittest.cc",
-    "importers/proto/args_table_utils_unittest.cc",
     "importers/proto/async_track_set_tracker_unittest.cc",
     "importers/proto/heap_graph_tracker_unittest.cc",
     "importers/proto/heap_profile_tracker_unittest.cc",
@@ -432,20 +430,22 @@
     "../protozero:testing_messages_zero",
     "containers:unittests",
     "db:unittests",
-    "importers:common",
-    "importers:unittests",
+    "importers/common",
+    "importers/common:unittests",
     "importers/memory_tracker:graph_processor",
     "rpc:unittests",
     "storage",
     "tables:unittests",
     "types",
     "types:unittests",
+    "util:descriptors",
     "util:unittests",
   ]
 
   if (enable_perfetto_trace_processor_sqlite) {
     sources += [
       "dynamic/experimental_counter_dur_generator_unittest.cc",
+      "dynamic/experimental_flat_slice_generator_unittest.cc",
       "dynamic/experimental_slice_layout_generator_unittest.cc",
       "dynamic/thread_state_generator_unittest.cc",
     ]
diff --git a/src/trace_processor/containers/BUILD.gn b/src/trace_processor/containers/BUILD.gn
index 836ab22..1f843e1 100644
--- a/src/trace_processor/containers/BUILD.gn
+++ b/src/trace_processor/containers/BUILD.gn
@@ -12,27 +12,33 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import("../../../gn/perfetto_component.gni")
 import("../../../gn/test.gni")
 
-source_set("containers") {
-  sources = [
-    "bit_vector.cc",
+# Used by two static libs - trace_processor and libpprofbuilder, hence using
+# perfetto_component to turn this into a cc_library in bazel builds.
+# The 'public' section gets converted to 'hdrs', and is necessary for the bazel
+# build to pass strict header checks.
+perfetto_component("containers") {
+  public = [
     "bit_vector.h",
-    "bit_vector_iterators.cc",
     "bit_vector_iterators.h",
     "null_term_string_view.h",
-    "nullable_vector.cc",
     "nullable_vector.h",
-    "row_map.cc",
     "row_map.h",
-    "string_pool.cc",
     "string_pool.h",
   ]
+  sources = [
+    "bit_vector.cc",
+    "bit_vector_iterators.cc",
+    "nullable_vector.cc",
+    "row_map.cc",
+    "string_pool.cc",
+  ]
   deps = [
     "../../../gn:default_deps",
-    "../../../include/perfetto/base",
-    "../../../include/perfetto/ext/base",
     "../../../include/perfetto/protozero",
+    "../../base",
   ]
 }
 
diff --git a/src/trace_processor/containers/bit_vector.h b/src/trace_processor/containers/bit_vector.h
index 20773dd..5f9c27a 100644
--- a/src/trace_processor/containers/bit_vector.h
+++ b/src/trace_processor/containers/bit_vector.h
@@ -491,7 +491,7 @@
       // mask: 00000000001111111
       uint64_t mask = MaskAllBitsSetUntil(idx);
 
-      // Finish up by anding the the atom with the computed msk.
+      // Finish up by and'ing the atom with the computed mask.
       return word_ & mask;
     }
 
diff --git a/src/trace_processor/db/BUILD.gn b/src/trace_processor/db/BUILD.gn
index f1a959f..ebbd478 100644
--- a/src/trace_processor/db/BUILD.gn
+++ b/src/trace_processor/db/BUILD.gn
@@ -14,7 +14,7 @@
 
 import("../../../gn/test.gni")
 
-source_set("lib") {
+source_set("db") {
   sources = [
     "column.cc",
     "column.h",
@@ -40,7 +40,7 @@
     "table_unittest.cc",
   ]
   deps = [
-    ":lib",
+    ":db",
     "../../../gn:default_deps",
     "../../../gn:gtest_and_gmock",
     "../../base:base",
diff --git a/src/trace_processor/db/table.cc b/src/trace_processor/db/table.cc
index ebcdf9e..940d8b1 100644
--- a/src/trace_processor/db/table.cc
+++ b/src/trace_processor/db/table.cc
@@ -68,41 +68,57 @@
   if (od.empty())
     return Copy();
 
+  // Return a copy if there is a single constraint to sort the table
+  // by a column which is already sorted.
+  const auto& first_col = GetColumn(od.front().col_idx);
+  if (od.size() == 1 && first_col.IsSorted() && !od.front().desc)
+    return Copy();
+
   // Build an index vector with all the indices for the first |size_| rows.
   std::vector<uint32_t> idx(row_count_);
-  std::iota(idx.begin(), idx.end(), 0);
 
-  // As our data is columnar, it's always more efficient to sort one column
-  // at a time rather than try and sort lexiographically all at once.
-  // To preserve correctness, we need to stably sort the index vector once
-  // for each order by in *reverse* order. Reverse order is important as it
-  // preserves the lexiographical property.
-  //
-  // For example, suppose we have the following:
-  // Table {
-  //   Column x;
-  //   Column y
-  //   Column z;
-  // }
-  //
-  // Then, to sort "y asc, x desc", we could do one of two things:
-  //  1) sort the index vector all at once and on each index, we compare
-  //     y then z. This is slow as the data is columnar and we need to
-  //     repeatedly branch inside each column.
-  //  2) we can stably sort first on x desc and then sort on y asc. This will
-  //     first put all the x in the correct order such that when we sort on
-  //     y asc, we will have the correct order of x where y is the same (since
-  //     the sort is stable).
-  //
-  // TODO(lalitm): it is possible that we could sort the last constraint (i.e.
-  // the first constraint in the below loop) in a non-stable way. However, this
-  // is more subtle than it appears as we would then need special handling where
-  // there are order bys on a column which is already sorted (e.g. ts, id).
-  // Investigate whether the performance gains from this are worthwhile. This
-  // also needs changes to the constraint modification logic in DbSqliteTable
-  // which currently eliminates constraints on sorted columns.
-  for (auto it = od.rbegin(); it != od.rend(); ++it) {
-    columns_[it->col_idx].StableSort(it->desc, &idx);
+  if (od.size() == 1 && first_col.IsSorted()) {
+    // We special case a single constraint in descending order as this
+    // happens any time the |max| function is used in SQLite. We can be
+    // more efficient as this column is already sorted so we simply need
+    // to reverse the order of this column.
+    PERFETTO_DCHECK(od.front().desc);
+    std::iota(idx.rbegin(), idx.rend(), 0);
+  } else {
+    // As our data is columnar, it's always more efficient to sort one column
+    // at a time rather than try and sort lexiographically all at once.
+    // To preserve correctness, we need to stably sort the index vector once
+    // for each order by in *reverse* order. Reverse order is important as it
+    // preserves the lexiographical property.
+    //
+    // For example, suppose we have the following:
+    // Table {
+    //   Column x;
+    //   Column y
+    //   Column z;
+    // }
+    //
+    // Then, to sort "y asc, x desc", we could do one of two things:
+    //  1) sort the index vector all at once and on each index, we compare
+    //     y then z. This is slow as the data is columnar and we need to
+    //     repeatedly branch inside each column.
+    //  2) we can stably sort first on x desc and then sort on y asc. This will
+    //     first put all the x in the correct order such that when we sort on
+    //     y asc, we will have the correct order of x where y is the same (since
+    //     the sort is stable).
+    //
+    // TODO(lalitm): it is possible that we could sort the last constraint (i.e.
+    // the first constraint in the below loop) in a non-stable way. However,
+    // this is more subtle than it appears as we would then need special
+    // handling where there are order bys on a column which is already sorted
+    // (e.g. ts, id). Investigate whether the performance gains from this are
+    // worthwhile. This also needs changes to the constraint modification logic
+    // in DbSqliteTable which currently eliminates constraints on sorted
+    // columns.
+    std::iota(idx.begin(), idx.end(), 0);
+    for (auto it = od.rbegin(); it != od.rend(); ++it) {
+      columns_[it->col_idx].StableSort(it->desc, &idx);
+    }
   }
 
   // Return a copy of this table with the RowMaps using the computed ordered
@@ -119,8 +135,11 @@
     col.flags_ &= ~Column::Flag::kSorted;
   }
 
-  // For the first order by, make the column flag itself as sorted.
-  table.columns_[od.front().col_idx].flags_ |= Column::Flag::kSorted;
+  // For the first order by, make the column flag itself as sorted but
+  // only if the sort was in ascending order.
+  if (!od.front().desc) {
+    table.columns_[od.front().col_idx].flags_ |= Column::Flag::kSorted;
+  }
 
   return table;
 }
diff --git a/src/trace_processor/dynamic/descendant_slice_generator.h b/src/trace_processor/dynamic/descendant_slice_generator.h
index e0d4975..c152f7a 100644
--- a/src/trace_processor/dynamic/descendant_slice_generator.h
+++ b/src/trace_processor/dynamic/descendant_slice_generator.h
@@ -47,7 +47,7 @@
   // ConnectedFlowGenerator to traverse flow indirectly connected flow events.
   static base::Optional<RowMap> GetDescendantSlices(
       const tables::SliceTable& slices,
-      SliceId slice_id);
+      SliceId start_id);
 
  private:
   TraceProcessorContext* context_ = nullptr;
diff --git a/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc b/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc
new file mode 100644
index 0000000..1af5ad1
--- /dev/null
+++ b/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc
@@ -0,0 +1,280 @@
+/*
+ * 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/trace_processor/dynamic/experimental_annotated_stack_generator.h"
+
+#include "perfetto/ext/base/optional.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/profiler_tables.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+#include "perfetto/ext/base/string_utils.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+namespace {
+
+enum class MapType {
+  kArtInterp,
+  kArtJit,
+  kArtAot,
+  kNativeLibart,
+  kNativeOther,
+  kOther
+};
+
+// Mapping examples:
+//   /system/lib64/libc.so
+//   /system/framework/framework.jar
+//   /memfd:jit-cache (deleted)
+//   [vdso]
+// TODO(rsavitski): consider moving this to a hidden column on
+// stack_profile_mapping, once this logic is sufficiently stable.
+MapType ClassifyMap(NullTermStringView map) {
+  if (map.empty())
+    return MapType::kOther;
+
+  // Primary mapping where modern ART puts jitted code.
+  // TODO(rsavitski): look into /memfd:jit-zygote-cache.
+  if (!strncmp(map.c_str(), "/memfd:jit-cache", 16))
+    return MapType::kArtJit;
+
+  size_t last_slash_pos = map.rfind('/');
+  if (last_slash_pos != NullTermStringView::npos) {
+    if (!strncmp(map.c_str() + last_slash_pos, "/libart.so", 10))
+      return MapType::kNativeLibart;
+    if (!strncmp(map.c_str() + last_slash_pos, "/libartd.so", 11))
+      return MapType::kNativeLibart;
+  }
+
+  size_t extension_pos = map.rfind('.');
+  if (extension_pos != NullTermStringView::npos) {
+    if (!strncmp(map.c_str() + extension_pos, ".so", 3))
+      return MapType::kNativeOther;
+    // dex with verification speedup info, produced by dex2oat
+    if (!strncmp(map.c_str() + extension_pos, ".vdex", 5))
+      return MapType::kArtInterp;
+    // possibly uncompressed dex in a jar archive
+    if (!strncmp(map.c_str() + extension_pos, ".jar", 4))
+      return MapType::kArtInterp;
+    // ahead of time compiled ELFs
+    if (!strncmp(map.c_str() + extension_pos, ".oat", 4))
+      return MapType::kArtAot;
+    // older/alternative name for .oat
+    if (!strncmp(map.c_str() + extension_pos, ".odex", 5))
+      return MapType::kArtAot;
+  }
+  return MapType::kOther;
+}
+
+uint32_t GetConstraintColumnIndex(TraceProcessorContext* context) {
+  // The dynamic table adds two columns on top of the callsite table. Last
+  // column is the hidden constrain (i.e. input arg) column.
+  return context->storage->stack_profile_callsite_table().GetColumnCount() + 1;
+}
+
+}  // namespace
+
+std::string ExperimentalAnnotatedStackGenerator::TableName() {
+  return "experimental_annotated_callstack";
+}
+
+Table::Schema ExperimentalAnnotatedStackGenerator::CreateSchema() {
+  auto schema = tables::StackProfileCallsiteTable::Schema();
+  schema.columns.push_back(Table::Schema::Column{
+      "annotation", SqlValue::Type::kString, /* is_id = */ false,
+      /* is_sorted = */ false, /* is_hidden = */ false});
+  schema.columns.push_back(Table::Schema::Column{
+      "start_id", SqlValue::Type::kLong, /* is_id = */ false,
+      /* is_sorted = */ false, /* is_hidden = */ true});
+  return schema;
+}
+
+util::Status ExperimentalAnnotatedStackGenerator::ValidateConstraints(
+    const QueryConstraints& qc) {
+  const auto& cs = qc.constraints();
+  int column = static_cast<int>(GetConstraintColumnIndex(context_));
+
+  auto id_fn = [column](const QueryConstraints::Constraint& c) {
+    return c.column == column && c.op == SQLITE_INDEX_CONSTRAINT_EQ;
+  };
+  bool has_id_cs = std::find_if(cs.begin(), cs.end(), id_fn) != cs.end();
+  return has_id_cs ? util::OkStatus()
+                   : util::ErrStatus("Failed to find required constraints");
+}
+
+std::unique_ptr<Table> ExperimentalAnnotatedStackGenerator::ComputeTable(
+    const std::vector<Constraint>& cs,
+    const std::vector<Order>&) {
+  const auto& cs_table = context_->storage->stack_profile_callsite_table();
+  const auto& f_table = context_->storage->stack_profile_frame_table();
+  const auto& m_table = context_->storage->stack_profile_mapping_table();
+
+  // Input (id of the callsite leaf) is the constraint on the hidden |start_id|
+  // column.
+  uint32_t constraint_col = GetConstraintColumnIndex(context_);
+  auto constraint_it =
+      std::find_if(cs.begin(), cs.end(), [constraint_col](const Constraint& c) {
+        return c.col_idx == constraint_col && c.op == FilterOp::kEq;
+      });
+  PERFETTO_DCHECK(constraint_it != cs.end());
+
+  auto start_id = static_cast<uint32_t>(constraint_it->value.AsLong());
+  base::Optional<uint32_t> start_row =
+      cs_table.id().IndexOf(CallsiteId(start_id));
+  if (!start_row)
+    return nullptr;
+
+  // Iteratively walk the parent_id chain to construct the list of callstack
+  // entries, each pointing at a frame.
+  std::vector<uint32_t> cs_rows;
+  cs_rows.push_back(*start_row);
+  base::Optional<CallsiteId> maybe_parent_id = cs_table.parent_id()[*start_row];
+  while (maybe_parent_id) {
+    uint32_t parent_row = cs_table.id().IndexOf(*maybe_parent_id).value();
+    cs_rows.push_back(parent_row);
+    maybe_parent_id = cs_table.parent_id()[parent_row];
+  }
+
+  // Walk the callsites root-to-leaf, annotating:
+  // * managed frames with their execution state (interpreted/jit/aot)
+  // * common ART frames, which are usually not relevant
+  //
+  // This is not a per-frame decision, because we do not want to filter out ART
+  // frames immediately after a JNI transition (such frames are often relevant).
+  //
+  // As a consequence of the logic being based on a root-to-leaf walk, a given
+  // callsite will always have the same annotation, as the parent path is always
+  // the same, and children callsites do not affect their parents' annotations.
+  //
+  // This could also be implemented as a hidden column on the callsite table
+  // (populated at import time), but we want to be more flexible for now.
+  StringId art_jni_trampoline =
+      context_->storage->InternString("art_jni_trampoline");
+
+  StringId common_frame = context_->storage->InternString("common-frame");
+  StringId art_interp = context_->storage->InternString("interp");
+  StringId art_jit = context_->storage->InternString("jit");
+  StringId art_aot = context_->storage->InternString("aot");
+
+  // Annotation FSM states:
+  // * kInitial: default, native-only callstacks never leave this state.
+  // * kEraseLibart: we've seen a managed frame, and will now "erase" (i.e. tag
+  //                 as a common-frame) frames belonging to the ART runtime.
+  // * kKeepNext: we've seen a special JNI trampoline for managed->native
+  //              transition, keep the immediate child (even if it is in ART),
+  //              and then go back to kEraseLibart.
+  // Regardless of the state, managed frames get annotated with their execution
+  // mode, based on the mapping.
+  enum class State { kInitial, kEraseLibart, kKeepNext };
+  State annotation_state = State::kInitial;
+
+  std::vector<StringPool::Id> annotations_reversed;
+  for (auto it = cs_rows.rbegin(); it != cs_rows.rend(); ++it) {
+    FrameId frame_id = cs_table.frame_id()[*it];
+    uint32_t frame_row = f_table.id().IndexOf(frame_id).value();
+
+    MappingId map_id = f_table.mapping()[frame_row];
+    uint32_t map_row = m_table.id().IndexOf(map_id).value();
+
+    // Keep immediate callee of a JNI trampoline, but keep tagging all
+    // successive libart frames as common.
+    if (annotation_state == State::kKeepNext) {
+      annotations_reversed.push_back(kNullStringId);
+      annotation_state = State::kEraseLibart;
+      continue;
+    }
+
+    // Special-case "art_jni_trampoline" frames, keeping their immediate callee
+    // even if it is in libart, as it could be a native implementation of a
+    // managed method. Example for "java.lang.reflect.Method.Invoke":
+    //   art_jni_trampoline
+    //   art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*)
+    //
+    // Simpleperf also relies on this frame name, so it should be fairly stable.
+    // TODO(rsavitski): consider detecting standard JNI upcall entrypoints -
+    // _JNIEnv::Call*. These are sometimes inlined into other DSOs, so erasing
+    // only the libart frames does not clean up all of the JNI-related frames.
+    StringId fname_id = f_table.name()[frame_row];
+    if (fname_id == art_jni_trampoline) {
+      annotations_reversed.push_back(common_frame);
+      annotation_state = State::kKeepNext;
+      continue;
+    }
+
+    NullTermStringView map_view =
+        context_->storage->GetString(m_table.name()[map_row]);
+    MapType map_type = ClassifyMap(map_view);
+
+    // Annotate managed frames.
+    if (map_type == MapType::kArtInterp ||  //
+        map_type == MapType::kArtJit ||     //
+        map_type == MapType::kArtAot) {
+      if (map_type == MapType::kArtInterp)
+        annotations_reversed.push_back(art_interp);
+      else if (map_type == MapType::kArtJit)
+        annotations_reversed.push_back(art_jit);
+      else if (map_type == MapType::kArtAot)
+        annotations_reversed.push_back(art_aot);
+
+      // Now know to be in a managed callstack - erase subsequent ART frames.
+      if (annotation_state == State::kInitial)
+        annotation_state = State::kEraseLibart;
+      continue;
+    }
+
+    if (annotation_state == State::kEraseLibart &&
+        map_type == MapType::kNativeLibart) {
+      annotations_reversed.push_back(common_frame);
+      continue;
+    }
+
+    annotations_reversed.push_back(kNullStringId);
+  }
+
+  // Build the dynamic table.
+  auto base_rowmap = RowMap(std::move(cs_rows));
+
+  PERFETTO_DCHECK(base_rowmap.size() == annotations_reversed.size());
+  std::unique_ptr<NullableVector<StringPool::Id>> annotation_vals(
+      new NullableVector<StringPool::Id>());
+  for (auto it = annotations_reversed.rbegin();
+       it != annotations_reversed.rend(); ++it) {
+    annotation_vals->Append(*it);
+  }
+
+  // Hidden column - always the input, i.e. the callsite leaf.
+  std::unique_ptr<NullableVector<uint32_t>> start_id_vals(
+      new NullableVector<uint32_t>());
+  for (uint32_t i = 0; i < base_rowmap.size(); i++)
+    start_id_vals->Append(start_id);
+
+  return std::unique_ptr<Table>(new Table(
+      cs_table.Apply(std::move(base_rowmap))
+          .ExtendWithColumn("annotation", std::move(annotation_vals),
+                            TypedColumn<StringPool::Id>::default_flags())
+          .ExtendWithColumn("start_id", std::move(start_id_vals),
+                            TypedColumn<uint32_t>::default_flags() |
+                                TypedColumn<uint32_t>::kHidden)));
+}
+
+uint32_t ExperimentalAnnotatedStackGenerator::EstimateRowCount() {
+  return 1;
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/dynamic/experimental_annotated_stack_generator.h b/src/trace_processor/dynamic/experimental_annotated_stack_generator.h
new file mode 100644
index 0000000..7e6a62d
--- /dev/null
+++ b/src/trace_processor/dynamic/experimental_annotated_stack_generator.h
@@ -0,0 +1,52 @@
+/*
+ * 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_TRACE_PROCESSOR_DYNAMIC_EXPERIMENTAL_ANNOTATED_STACK_GENERATOR_H_
+#define SRC_TRACE_PROCESSOR_DYNAMIC_EXPERIMENTAL_ANNOTATED_STACK_GENERATOR_H_
+
+#include "src/trace_processor/sqlite/db_sqlite_table.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class TraceProcessorContext;
+
+// The "experimental_annotated_callstack" dynamic table.
+//
+// Given a leaf callsite id, returns the full callstack (including the leaf),
+// with optional (currently Android-specific) annotations. A given callsite will
+// always have the same annotation.
+class ExperimentalAnnotatedStackGenerator
+    : public DbSqliteTable::DynamicTableGenerator {
+ public:
+  ExperimentalAnnotatedStackGenerator(TraceProcessorContext* context)
+      : context_(context) {}
+
+  Table::Schema CreateSchema() override;
+  std::string TableName() override;
+  uint32_t EstimateRowCount() override;
+  util::Status ValidateConstraints(const QueryConstraints&) override;
+  std::unique_ptr<Table> ComputeTable(const std::vector<Constraint>& cs,
+                                      const std::vector<Order>& ob) override;
+
+ private:
+  TraceProcessorContext* context_ = nullptr;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_DYNAMIC_EXPERIMENTAL_ANNOTATED_STACK_GENERATOR_H_
diff --git a/src/trace_processor/dynamic/experimental_flat_slice_generator.cc b/src/trace_processor/dynamic/experimental_flat_slice_generator.cc
new file mode 100644
index 0000000..5fec13d
--- /dev/null
+++ b/src/trace_processor/dynamic/experimental_flat_slice_generator.cc
@@ -0,0 +1,261 @@
+/*
+ * 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/trace_processor/dynamic/experimental_flat_slice_generator.h"
+
+#include <memory>
+#include <set>
+
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+ExperimentalFlatSliceGenerator::ExperimentalFlatSliceGenerator(
+    TraceProcessorContext* context)
+    : context_(context) {}
+
+util::Status ExperimentalFlatSliceGenerator::ValidateConstraints(
+    const QueryConstraints& qc) {
+  using CI = tables::ExperimentalFlatSliceTable::ColumnIndex;
+  bool has_start_bound = false;
+  bool has_end_bound = false;
+  for (const auto& c : qc.constraints()) {
+    has_start_bound |= c.column == static_cast<int>(CI::start_bound) &&
+                       c.op == SQLITE_INDEX_CONSTRAINT_EQ;
+    has_end_bound |= c.column == static_cast<int>(CI::end_bound) &&
+                     c.op == SQLITE_INDEX_CONSTRAINT_EQ;
+  }
+  return has_start_bound && has_end_bound
+             ? util::OkStatus()
+             : util::ErrStatus("Failed to find required constraints");
+}
+
+std::unique_ptr<Table> ExperimentalFlatSliceGenerator::ComputeTable(
+    const std::vector<Constraint>& cs,
+    const std::vector<Order>&) {
+  using CI = tables::ExperimentalFlatSliceTable::ColumnIndex;
+  auto start_it = std::find_if(cs.begin(), cs.end(), [](const Constraint& c) {
+    return c.col_idx == static_cast<uint32_t>(CI::start_bound) &&
+           c.op == FilterOp::kEq;
+  });
+  auto end_it = std::find_if(cs.begin(), cs.end(), [](const Constraint& c) {
+    return c.col_idx == static_cast<uint32_t>(CI::end_bound) &&
+           c.op == FilterOp::kEq;
+  });
+  int64_t start_bound = start_it->value.AsLong();
+  int64_t end_bound = end_it->value.AsLong();
+  return ComputeFlatSliceTable(context_->storage->slice_table(),
+                               context_->storage->mutable_string_pool(),
+                               start_bound, end_bound);
+}
+
+std::unique_ptr<tables::ExperimentalFlatSliceTable>
+ExperimentalFlatSliceGenerator::ComputeFlatSliceTable(
+    const tables::SliceTable& slice,
+    StringPool* pool,
+    int64_t start_bound,
+    int64_t end_bound) {
+  std::unique_ptr<tables::ExperimentalFlatSliceTable> out(
+      new tables::ExperimentalFlatSliceTable(pool, nullptr));
+
+  auto insert_slice = [&](uint32_t i, int64_t ts,
+                          tables::TrackTable::Id track_id) {
+    tables::ExperimentalFlatSliceTable::Row row;
+    row.ts = ts;
+    row.dur = -1;
+    row.track_id = track_id;
+    row.category = slice.category()[i];
+    row.name = slice.name()[i];
+    row.arg_set_id = slice.arg_set_id()[i];
+    row.source_id = slice.id()[i];
+    row.start_bound = start_bound;
+    row.end_bound = end_bound;
+    return out->Insert(row).row;
+  };
+  auto insert_sentinel = [&](int64_t ts, TrackId track_id) {
+    tables::ExperimentalFlatSliceTable::Row row;
+    row.ts = ts;
+    row.dur = -1;
+    row.track_id = track_id;
+    row.category = kNullStringId;
+    row.name = kNullStringId;
+    row.arg_set_id = kInvalidArgSetId;
+    row.source_id = base::nullopt;
+    row.start_bound = start_bound;
+    row.end_bound = end_bound;
+    return out->Insert(row).row;
+  };
+
+  auto terminate_slice = [&](uint32_t out_row, int64_t end_ts) {
+    PERFETTO_DCHECK(out->dur()[out_row] == -1);
+    int64_t out_ts = out->ts()[out_row];
+    out->mutable_dur()->Set(out_row, end_ts - out_ts);
+  };
+
+  struct ActiveSlice {
+    base::Optional<uint32_t> source_row;
+    uint32_t out_row = std::numeric_limits<uint32_t>::max();
+
+    bool is_sentinel() const { return !source_row; }
+  };
+  struct Track {
+    std::vector<uint32_t> parents;
+    ActiveSlice active;
+    bool initialized = false;
+  };
+  std::unordered_map<TrackId, Track> tracks;
+
+  auto maybe_terminate_active_slice = [&](const Track& t, int64_t fin_ts) {
+    int64_t ts = slice.ts()[t.active.source_row.value()];
+    int64_t dur = slice.dur()[t.active.source_row.value()];
+    if (dur == -1 || ts + dur > fin_ts)
+      return false;
+
+    terminate_slice(t.active.out_row, ts + dur);
+    return true;
+  };
+
+  // Post-condition: |tracks[track_id].active| will always point to
+  // a slice which finishes after |fin_ts| and has a |dur| == -1 in
+  // |out|.
+  auto output_slices_before = [&](TrackId track_id, int64_t fin_ts) {
+    auto& t = tracks[track_id];
+
+    // A sentinel slice cannot have parents.
+    PERFETTO_DCHECK(!t.active.is_sentinel() || t.parents.empty());
+
+    // If we have a sentinel slice active, we have nothing to output.
+    if (t.active.is_sentinel())
+      return;
+
+    // Try and terminate the current slice (if it ends before |fin_ts|)
+    // If we cannot terminate it, then we leave it as pending for the caller
+    // to terminate.
+    if (!maybe_terminate_active_slice(t, fin_ts))
+      return;
+
+    // Next, add any parents as appropriate.
+    for (int64_t i = static_cast<int64_t>(t.parents.size()) - 1; i >= 0; --i) {
+      uint32_t source_row = t.parents[static_cast<size_t>(i)];
+      t.parents.pop_back();
+
+      int64_t active_ts = out->ts()[t.active.out_row];
+      int64_t active_dur = out->dur()[t.active.out_row];
+      PERFETTO_DCHECK(active_dur != -1);
+
+      t.active.source_row = source_row;
+      t.active.out_row =
+          insert_slice(source_row, active_ts + active_dur, track_id);
+
+      if (!maybe_terminate_active_slice(t, fin_ts))
+        break;
+    }
+
+    if (!t.parents.empty())
+      return;
+
+    // If the active slice is a sentinel, the check at the top of this function
+    // should have caught it; all code only adds slices from source.
+    PERFETTO_DCHECK(!t.active.is_sentinel());
+
+    int64_t ts = out->ts()[t.active.out_row];
+    int64_t dur = out->dur()[t.active.out_row];
+
+    // If the active slice is unfinshed, we return that for the caller to
+    // terminate.
+    if (dur == -1)
+      return;
+
+    // Otherwise, Add a sentinel slice after the end of the active slice.
+    t.active.source_row = base::nullopt;
+    t.active.out_row = insert_sentinel(ts + dur, track_id);
+  };
+
+  for (uint32_t i = 0; i < slice.row_count(); ++i) {
+    // TODO(lalitm): this can be optimized using a O(logn) lower bound/filter.
+    // Not adding for now as a premature optimization but may be needed down the
+    // line.
+    int64_t ts = slice.ts()[i];
+    if (ts < start_bound)
+      continue;
+
+    if (ts >= end_bound)
+      break;
+
+    // Ignore instants as they don't factor into flat slice at all.
+    if (slice.dur()[i] == 0)
+      continue;
+
+    TrackId track_id = slice.track_id()[i];
+    Track& track = tracks[track_id];
+
+    // Initalize the track (if needed) by adding a sentinel slice starting at
+    // start_bound.
+    bool is_root = slice.depth()[i] == 0;
+    if (!track.initialized) {
+      // If we are unintialized and our start box picks up slices mid way
+      // through startup, wait until we reach a root slice.
+      if (!is_root)
+        continue;
+
+      track.active.out_row = insert_sentinel(start_bound, track_id);
+      track.initialized = true;
+    }
+    output_slices_before(track_id, ts);
+    terminate_slice(track.active.out_row, ts);
+
+    // We should have sentinel slices iff the slice is a root.
+    PERFETTO_DCHECK(track.active.is_sentinel() == is_root);
+
+    // If our current slice has a parent, that must be the current active slice.
+    if (!is_root) {
+      track.parents.push_back(*track.active.source_row);
+    }
+
+    // The depth of our slice should also match the depth of the parent stack
+    // (after adding the previous slice).
+    PERFETTO_DCHECK(track.parents.size() == slice.depth()[i]);
+
+    track.active.source_row = i;
+    track.active.out_row = insert_slice(i, ts, track_id);
+  }
+
+  for (const auto& track : tracks) {
+    // First, terminate any hanging slices.
+    output_slices_before(track.first, end_bound);
+
+    // Second, force terminate the final slice to the end bound.
+    terminate_slice(track.second.active.out_row, end_bound);
+  }
+
+  return out;
+}
+
+Table::Schema ExperimentalFlatSliceGenerator::CreateSchema() {
+  return tables::ExperimentalFlatSliceTable::Schema();
+}
+
+std::string ExperimentalFlatSliceGenerator::TableName() {
+  return "experimental_flat_slice";
+}
+
+uint32_t ExperimentalFlatSliceGenerator::EstimateRowCount() {
+  return context_->storage->slice_table().row_count();
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/dynamic/experimental_flat_slice_generator.h b/src/trace_processor/dynamic/experimental_flat_slice_generator.h
new file mode 100644
index 0000000..b0838c4
--- /dev/null
+++ b/src/trace_processor/dynamic/experimental_flat_slice_generator.h
@@ -0,0 +1,81 @@
+/*
+ * 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_TRACE_PROCESSOR_DYNAMIC_EXPERIMENTAL_FLAT_SLICE_GENERATOR_H_
+#define SRC_TRACE_PROCESSOR_DYNAMIC_EXPERIMENTAL_FLAT_SLICE_GENERATOR_H_
+
+#include "perfetto/ext/base/optional.h"
+#include "src/trace_processor/sqlite/db_sqlite_table.h"
+#include "src/trace_processor/storage/trace_storage.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class TraceProcessorContext;
+
+// Dynamic table generator for "flat slice" table.
+//
+// The concept of a "flat slice" is to take the data in the slice table and
+// remove all notion of nesting; we do this by, at any point in time, taking the
+// most specific active slice (i.e. the slice at the bottom of the stack) and
+// representing that as the *only* slice that was running during that period.
+//
+// This concept becomes very useful when you try and linearise a trace and
+// compare it with other traces spanning the same user action; "self time" (i.e.
+// time spent in a slice but *not* any children) is easily computed and span
+// joins with thread state become possible without limiting to only depth zero
+// slices.
+//
+// This table also adds "gap slices" which fill in the gap between top level
+// slices with a sentinal values so that comparision of the gap between slices
+// is also possible.
+//
+// As input, this generator takes a start and end timestamp between
+// which slices should be picked; we do this rather than just using the trace
+// bounds so that the "gap slices" start and end at the appropriate place.
+//
+// Note that for the start bound we will *not* pick any slice which started
+// before the bound even if it finished after. This is dissimilar to span join
+// (which picks all slices with ts + dur >= bound) and is more akin to doing
+// a simple ts >= bound. However, slices *will* be truncated at the end
+// if they would spill past the provided end bound.
+class ExperimentalFlatSliceGenerator
+    : public DbSqliteTable::DynamicTableGenerator {
+ public:
+  ExperimentalFlatSliceGenerator(TraceProcessorContext* context);
+
+  Table::Schema CreateSchema() override;
+  std::string TableName() override;
+  uint32_t EstimateRowCount() override;
+  util::Status ValidateConstraints(const QueryConstraints&) override;
+  std::unique_ptr<Table> ComputeTable(const std::vector<Constraint>& cs,
+                                      const std::vector<Order>& ob) override;
+
+  // Visibile for testing.
+  static std::unique_ptr<tables::ExperimentalFlatSliceTable>
+  ComputeFlatSliceTable(const tables::SliceTable&,
+                        StringPool*,
+                        int64_t start_bound,
+                        int64_t end_bound);
+
+ private:
+  TraceProcessorContext* context_ = nullptr;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_DYNAMIC_EXPERIMENTAL_FLAT_SLICE_GENERATOR_H_
diff --git a/src/trace_processor/dynamic/experimental_flat_slice_generator_unittest.cc b/src/trace_processor/dynamic/experimental_flat_slice_generator_unittest.cc
new file mode 100644
index 0000000..7bacf75
--- /dev/null
+++ b/src/trace_processor/dynamic/experimental_flat_slice_generator_unittest.cc
@@ -0,0 +1,118 @@
+/*
+ * 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/trace_processor/dynamic/experimental_flat_slice_generator.h"
+
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace {
+
+tables::SliceTable::Row SliceRow(int64_t ts,
+                                 int64_t dur,
+                                 uint32_t depth,
+                                 TrackId track_id) {
+  tables::SliceTable::Row row;
+  row.ts = ts;
+  row.dur = dur;
+  row.depth = depth;
+  row.track_id = track_id;
+  return row;
+}
+
+class TableAsserter {
+ public:
+  TableAsserter(Table table) : table_(std::move(table)) {}
+
+  void NextSlice(int64_t ts, int64_t dur) {
+    ++idx_;
+    ASSERT_EQ(table_.GetTypedColumnByName<int64_t>("ts")[idx_], ts)
+        << "where idx_ = " << idx_;
+    ASSERT_EQ(table_.GetTypedColumnByName<int64_t>("dur")[idx_], dur)
+        << "where idx_ = " << idx_;
+  }
+
+ private:
+  Table table_;
+  uint32_t idx_ = std::numeric_limits<uint32_t>::max();
+};
+
+TEST(ExperimentalFlatSliceGenerator, Smoke) {
+  StringPool pool;
+  tables::SliceTable table(&pool, nullptr);
+
+  // A simple stack on track 1.
+  table.Insert(SliceRow(100, 10, 0, TrackId{1}));
+  table.Insert(SliceRow(104, 6, 1, TrackId{1}));
+  table.Insert(SliceRow(107, 1, 2, TrackId{1}));
+
+  // Back to back slices with a gap on track 2.
+  table.Insert(SliceRow(200, 10, 0, TrackId{2}));
+  table.Insert(SliceRow(210, 10, 0, TrackId{2}));
+  table.Insert(SliceRow(230, 10, 0, TrackId{2}));
+
+  // Deep nesting on track 3.
+  table.Insert(SliceRow(300, 100, 0, TrackId{3}));
+  table.Insert(SliceRow(301, 98, 1, TrackId{3}));
+  table.Insert(SliceRow(302, 96, 2, TrackId{3}));
+  table.Insert(SliceRow(303, 94, 3, TrackId{3}));
+  table.Insert(SliceRow(304, 92, 4, TrackId{3}));
+  table.Insert(SliceRow(305, 90, 5, TrackId{3}));
+
+  auto out = ExperimentalFlatSliceGenerator::ComputeFlatSliceTable(table, &pool,
+                                                                   0, 400);
+  auto sorted = out->Sort({out->track_id().ascending(), out->ts().ascending()});
+
+  ASSERT_EQ(sorted.row_count(), 27u);
+  TableAsserter asserter(std::move(sorted));
+
+  // Track 1's slices.
+  asserter.NextSlice(0, 100);
+  asserter.NextSlice(100, 4);
+  asserter.NextSlice(104, 3);
+  asserter.NextSlice(107, 1);
+  asserter.NextSlice(108, 2);
+  asserter.NextSlice(110, 0);
+  asserter.NextSlice(110, 290);
+
+  // Track 2's slices.
+  asserter.NextSlice(0, 200);
+  asserter.NextSlice(200, 10);
+  asserter.NextSlice(210, 0);
+  asserter.NextSlice(210, 10);
+  asserter.NextSlice(220, 10);
+  asserter.NextSlice(230, 10);
+  asserter.NextSlice(240, 160);
+
+  // Track 3's slices.
+  asserter.NextSlice(0, 300);
+  asserter.NextSlice(300, 1);
+  asserter.NextSlice(301, 1);
+  asserter.NextSlice(302, 1);
+  asserter.NextSlice(303, 1);
+  asserter.NextSlice(304, 1);
+  asserter.NextSlice(305, 90);
+  asserter.NextSlice(395, 1);
+  asserter.NextSlice(396, 1);
+  asserter.NextSlice(397, 1);
+  asserter.NextSlice(398, 1);
+  asserter.NextSlice(399, 1);
+  asserter.NextSlice(400, 0);
+}
+
+}  // namespace
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/dynamic/experimental_slice_layout_generator.cc b/src/trace_processor/dynamic/experimental_slice_layout_generator.cc
index 297b7e0..0c864a1 100644
--- a/src/trace_processor/dynamic/experimental_slice_layout_generator.cc
+++ b/src/trace_processor/dynamic/experimental_slice_layout_generator.cc
@@ -197,7 +197,7 @@
     uint32_t depth = depth_col[i];
     int64_t start = ts_col[i];
     int64_t dur = dur_col[i];
-    int64_t end = start + dur;
+    int64_t end = dur == -1 ? std::numeric_limits<int64_t>::max() : start + dur;
     InsertSlice(id_map, id, parent_id);
     std::map<tables::SliceTable::Id, GroupInfo>::iterator it;
     bool inserted;
diff --git a/src/trace_processor/dynamic/thread_state_generator.cc b/src/trace_processor/dynamic/thread_state_generator.cc
index c6115e7..e2e7f89 100644
--- a/src/trace_processor/dynamic/thread_state_generator.cc
+++ b/src/trace_processor/dynamic/thread_state_generator.cc
@@ -132,9 +132,34 @@
   UniqueTid utid = sched.GetTypedColumnByName<uint32_t>("utid")[sched_idx];
   ThreadSchedInfo* info = &state_map[utid];
 
-  // Flush the info and reset so we don't have any leftover data on the next
-  // round.
-  FlushPendingEventsForThread(utid, *info, table, ts);
+  // Due to races in the kernel, it is possible for the same thread to be
+  // scheduled on different CPUs at the same time. This will manifest itself
+  // here by having |info->desched_ts| in the future of this scheduling slice
+  // (i.e. there was a scheduling slice in the past which ended after the start
+  // of the current scheduling slice).
+  //
+  // We work around this problem by truncating the previous slice to the start
+  // of this slice and not adding the descheduled slice (i.e. we don't call
+  // |FlushPendingEventsForThread| which adds this slice).
+  //
+  // See b/186509316 for details and an example on when this happens.
+  if (info->desched_ts && info->desched_ts.value() > ts) {
+    uint32_t prev_sched_row = info->scheduled_row.value();
+    int64_t prev_sched_start = table->ts()[prev_sched_row];
+
+    // Just a double check that descheduling slice would have started at the
+    // same time the scheduling slice would have ended.
+    PERFETTO_DCHECK(prev_sched_start + table->dur()[prev_sched_row] ==
+                    info->desched_ts.value());
+
+    // Truncate the duration of the old slice to end at the start of this
+    // scheduling slice.
+    table->mutable_dur()->Set(prev_sched_row, ts - prev_sched_start);
+  } else {
+    FlushPendingEventsForThread(utid, *info, table, ts);
+  }
+
+  // Reset so we don't have any leftover data on the next round.
   *info = {};
 
   // Undo the expansion of the final sched slice for each CPU to the end of the
@@ -155,7 +180,8 @@
   sched_row.cpu = sched.GetTypedColumnByName<uint32_t>("cpu")[sched_idx];
   sched_row.state = running_string_id_;
   sched_row.utid = utid;
-  table->Insert(sched_row);
+
+  auto id_and_row = table->Insert(sched_row);
 
   // If the sched row had a negative duration, don't add any descheduled slice
   // because it would be meaningless.
@@ -168,6 +194,7 @@
   info->desched_ts = ts + dur;
   info->desched_end_state =
       sched.GetTypedColumnByName<StringId>("end_state")[sched_idx];
+  info->scheduled_row = id_and_row.row;
 }
 
 void ThreadStateGenerator::AddWakingEvent(
@@ -179,7 +206,16 @@
       waking.GetTypedColumnByName<int64_t>("ref")[waking_idx]);
   ThreadSchedInfo* info = &state_map[utid];
 
-  // As counter-intuitive as it seems, occassionally we can get a waking
+  // Occasionally, it is possible to get a waking event for a thread
+  // which is already in a runnable state. When this happens, we just
+  // ignore the waking event.
+  // See b/186509316 for details and an example on when this happens.
+  if (info->desched_end_state &&
+      *info->desched_end_state == runnable_string_id_) {
+    return;
+  }
+
+  // As counter-intuitive as it seems, occasionally we can get a waking
   // event for a thread which is currently running.
   //
   // There are two cases when this can happen:
diff --git a/src/trace_processor/dynamic/thread_state_generator.h b/src/trace_processor/dynamic/thread_state_generator.h
index 4ebfda8..75895ba 100644
--- a/src/trace_processor/dynamic/thread_state_generator.h
+++ b/src/trace_processor/dynamic/thread_state_generator.h
@@ -49,6 +49,7 @@
   struct ThreadSchedInfo {
     base::Optional<int64_t> desched_ts;
     base::Optional<StringId> desched_end_state;
+    base::Optional<uint32_t> scheduled_row;
     base::Optional<bool> io_wait;
     base::Optional<int64_t> runnable_ts;
     base::Optional<StringId> blocked_function;
diff --git a/src/trace_processor/export_json.cc b/src/trace_processor/export_json.cc
index 6069b09..fe73813 100644
--- a/src/trace_processor/export_json.cc
+++ b/src/trace_processor/export_json.cc
@@ -874,9 +874,13 @@
         event["tid"] = Json::Int(pid_and_tid.second);
 
         if (duration_ns == 0) {
-          // Use "I" instead of "i" phase for backwards-compat with old
-          // consumers.
-          event["ph"] = "I";
+          if (legacy_phase.empty()) {
+            // Use "I" instead of "i" phase for backwards-compat with old
+            // consumers.
+            event["ph"] = "I";
+          } else {
+            event["ph"] = legacy_phase;
+          }
           if (thread_ts_ns && thread_ts_ns > 0) {
             event["tts"] = Json::Int64(*thread_ts_ns / 1000);
           }
@@ -1022,9 +1026,13 @@
           PERFETTO_DLOG(
               "skipping non-instant slice on global or process track");
         } else {
-          // Use "I" instead of "i" phase for backwards-compat with old
-          // consumers.
-          event["ph"] = "I";
+          if (legacy_phase.empty()) {
+            // Use "I" instead of "i" phase for backwards-compat with old
+            // consumers.
+            event["ph"] = "I";
+          } else {
+            event["ph"] = legacy_phase;
+          }
 
           auto opt_process_row = process_track.id().IndexOf(TrackId{track_id});
           if (opt_process_row.has_value()) {
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index 6583ead..4ba663e 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -161,7 +161,7 @@
     return kJsonTraceType;
 
   // Systrace with header but no leading HTML.
-  if (start.find("# tracer") != std::string::npos)
+  if (base::Contains(start, "# tracer"))
     return kSystraceTraceType;
 
   // Systrace with leading HTML.
@@ -170,7 +170,7 @@
     return kSystraceTraceType;
 
   // Ctrace is deflate'ed systrace.
-  if (start.find("TRACE:") != std::string::npos)
+  if (base::Contains(start, "TRACE:"))
     return kCtraceTraceType;
 
   // Ninja's buils log (.ninja_log).
diff --git a/src/trace_processor/forwarding_trace_parser.h b/src/trace_processor/forwarding_trace_parser.h
index 8b8a8bb..0da568a 100644
--- a/src/trace_processor/forwarding_trace_parser.h
+++ b/src/trace_processor/forwarding_trace_parser.h
@@ -17,7 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_FORWARDING_TRACE_PARSER_H_
 #define SRC_TRACE_PROCESSOR_FORWARDING_TRACE_PARSER_H_
 
-#include "src/trace_processor/chunked_trace_reader.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
 
 #include "src/trace_processor/types/trace_processor_context.h"
 
diff --git a/src/trace_processor/importers/BUILD.gn b/src/trace_processor/importers/BUILD.gn
index d25ff6b..fceb53f 100644
--- a/src/trace_processor/importers/BUILD.gn
+++ b/src/trace_processor/importers/BUILD.gn
@@ -12,66 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import("../../../gn/perfetto.gni")
 import("../../../gn/perfetto_cc_proto_descriptor.gni")
 
-source_set("common") {
-  sources = [
-    "common/args_tracker.cc",
-    "common/args_tracker.h",
-    "common/clock_tracker.cc",
-    "common/clock_tracker.h",
-    "common/event_tracker.cc",
-    "common/event_tracker.h",
-    "common/flow_tracker.cc",
-    "common/flow_tracker.h",
-    "common/global_args_tracker.cc",
-    "common/global_args_tracker.h",
-    "common/process_tracker.cc",
-    "common/process_tracker.h",
-    "common/slice_tracker.cc",
-    "common/slice_tracker.h",
-    "common/system_info_tracker.cc",
-    "common/system_info_tracker.h",
-    "common/track_tracker.cc",
-    "common/track_tracker.h",
-  ]
-  public_deps = [
-    ":gen_cc_config_descriptor",
-    "../util:protozero_to_text",
-  ]
-  deps = [
-    "../../../gn:default_deps",
-    "../../../protos/perfetto/common:zero",
-    "../../../protos/perfetto/trace:zero",
-    "../../../protos/perfetto/trace/profiling:zero",
-    "../../base",
-    "../storage",
-    "../types",
-  ]
-}
-
-source_set("unittests") {
-  sources = [
-    "common/clock_tracker_unittest.cc",
-    "common/event_tracker_unittest.cc",
-    "common/flow_tracker_unittest.cc",
-    "common/process_tracker_unittest.cc",
-    "common/slice_tracker_unittest.cc",
-  ]
-  testonly = true
-  deps = [
-    ":common",
-    "../../../gn:default_deps",
-    "../../../gn:gtest_and_gmock",
-    "../../../protos/perfetto/common:zero",
-    "../../../protos/perfetto/trace:zero",
-    "../../base",
-    "../storage",
-    "../types",
-  ]
-}
-
 perfetto_cc_proto_descriptor("gen_cc_config_descriptor") {
   descriptor_name = "config.descriptor"
   descriptor_target = "../../../protos/perfetto/config:descriptor"
diff --git a/src/trace_processor/importers/common/BUILD.gn b/src/trace_processor/importers/common/BUILD.gn
new file mode 100644
index 0000000..1ffb40a
--- /dev/null
+++ b/src/trace_processor/importers/common/BUILD.gn
@@ -0,0 +1,75 @@
+# 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")
+
+source_set("common") {
+  sources = [
+    "args_tracker.cc",
+    "args_tracker.h",
+    "chunked_trace_reader.h",
+    "clock_tracker.cc",
+    "clock_tracker.h",
+    "event_tracker.cc",
+    "event_tracker.h",
+    "flow_tracker.cc",
+    "flow_tracker.h",
+    "global_args_tracker.cc",
+    "global_args_tracker.h",
+    "process_tracker.cc",
+    "process_tracker.h",
+    "slice_tracker.cc",
+    "slice_tracker.h",
+    "system_info_tracker.cc",
+    "system_info_tracker.h",
+    "trace_parser.h",
+    "track_tracker.cc",
+    "track_tracker.h",
+  ]
+  public_deps = [
+    "../:gen_cc_config_descriptor",
+    "../../util:protozero_to_text",
+  ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../../../include/perfetto/trace_processor:basic_types",
+    "../../../../protos/perfetto/common:zero",
+    "../../../../protos/perfetto/trace:zero",
+    "../../../../protos/perfetto/trace/profiling:zero",
+    "../../../base",
+    "../../storage",
+    "../../types",
+  ]
+}
+
+source_set("unittests") {
+  sources = [
+    "clock_tracker_unittest.cc",
+    "event_tracker_unittest.cc",
+    "flow_tracker_unittest.cc",
+    "process_tracker_unittest.cc",
+    "slice_tracker_unittest.cc",
+  ]
+  testonly = true
+  deps = [
+    ":common",
+    "../../../../gn:default_deps",
+    "../../../../gn:gtest_and_gmock",
+    "../../../../protos/perfetto/common:zero",
+    "../../../../protos/perfetto/trace:zero",
+    "../../../base",
+    "../../storage",
+    "../../types",
+  ]
+}
diff --git a/src/trace_processor/importers/common/args_tracker.h b/src/trace_processor/importers/common/args_tracker.h
index 4c2c943..f891537 100644
--- a/src/trace_processor/importers/common/args_tracker.h
+++ b/src/trace_processor/importers/common/args_tracker.h
@@ -64,18 +64,19 @@
 
     // IncrementArrayEntryIndex() and GetNextArrayEntryIndex() provide a way to
     // track the next array index for an array under a specific key.
-    void IncrementArrayEntryIndex(StringId key) {
-      // Zero-initializes |key| in the map if it doesn't exist yet.
-      args_tracker_
-          ->array_indexes_[std::make_tuple(arg_set_id_column_, row_, key)]++;
-    }
-
     size_t GetNextArrayEntryIndex(StringId key) {
       // Zero-initializes |key| in the map if it doesn't exist yet.
       return args_tracker_
           ->array_indexes_[std::make_tuple(arg_set_id_column_, row_, key)];
     }
 
+    // Returns the next available array index after increment.
+    size_t IncrementArrayEntryIndex(StringId key) {
+      // Zero-initializes |key| in the map if it doesn't exist yet.
+      return ++args_tracker_->array_indexes_[std::make_tuple(arg_set_id_column_,
+                                                             row_, key)];
+    }
+
    protected:
     BoundInserter(ArgsTracker* args_tracker,
                   Column* arg_set_id_column,
diff --git a/src/trace_processor/chunked_trace_reader.h b/src/trace_processor/importers/common/chunked_trace_reader.h
similarity index 87%
rename from src/trace_processor/chunked_trace_reader.h
rename to src/trace_processor/importers/common/chunked_trace_reader.h
index 8dbdd4f..dc90344 100644
--- a/src/trace_processor/chunked_trace_reader.h
+++ b/src/trace_processor/importers/common/chunked_trace_reader.h
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-#ifndef SRC_TRACE_PROCESSOR_CHUNKED_TRACE_READER_H_
-#define SRC_TRACE_PROCESSOR_CHUNKED_TRACE_READER_H_
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_CHUNKED_TRACE_READER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_CHUNKED_TRACE_READER_H_
 
 #include <stddef.h>
 #include <stdint.h>
@@ -47,4 +47,4 @@
 }  // namespace trace_processor
 }  // namespace perfetto
 
-#endif  // SRC_TRACE_PROCESSOR_CHUNKED_TRACE_READER_H_
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_CHUNKED_TRACE_READER_H_
diff --git a/src/trace_processor/importers/common/clock_tracker.cc b/src/trace_processor/importers/common/clock_tracker.cc
index f59fbfc..75518a2 100644
--- a/src/trace_processor/importers/common/clock_tracker.cc
+++ b/src/trace_processor/importers/common/clock_tracker.cc
@@ -41,7 +41,7 @@
 
 ClockTracker::~ClockTracker() = default;
 
-void ClockTracker::AddSnapshot(const std::vector<ClockValue>& clocks) {
+uint32_t ClockTracker::AddSnapshot(const std::vector<ClockValue>& clocks) {
   const auto snapshot_id = cur_snapshot_id_++;
 
   // Clear the cache
@@ -65,7 +65,7 @@
                       "supported for sequence-scoped clocks.",
                       clock_id);
         context_->storage->IncrementStats(stats::invalid_clock_snapshots);
-        return;
+        return snapshot_id;
       }
       domain.unit_multiplier_ns = clock.unit_multiplier_ns;
       domain.is_incremental = clock.is_incremental;
@@ -79,7 +79,7 @@
                     clock_id, clock.unit_multiplier_ns, clock.is_incremental,
                     domain.unit_multiplier_ns, domain.is_incremental);
       context_->storage->IncrementStats(stats::invalid_clock_snapshots);
-      return;
+      return snapshot_id;
     }
     const int64_t timestamp_ns =
         clock.absolute_timestamp * domain.unit_multiplier_ns;
@@ -92,7 +92,7 @@
                     " at snapshot %" PRIu32 ".",
                     clock_id, snapshot_id);
       context_->storage->IncrementStats(stats::invalid_clock_snapshots);
-      return;
+      return snapshot_id;
     }
 
     // Clock ids in the range [64, 128) are sequence-scoped and must be
@@ -116,7 +116,7 @@
                       clock_id, snapshot_id, timestamp_ns,
                       vect.timestamps_ns.back());
         context_->storage->IncrementStats(stats::invalid_clock_snapshots);
-        return;
+        return snapshot_id;
       }
 
       PERFETTO_DLOG("Detected non-monotonic clock with ID %" PRIu64, clock_id);
@@ -159,6 +159,7 @@
         graph_.emplace(it2->clock_id, it1->clock_id, snapshot_hash);
     }
   }
+  return snapshot_id;
 }
 
 // Finds the shortest clock resolution path in the graph that allows to
diff --git a/src/trace_processor/importers/common/clock_tracker.h b/src/trace_processor/importers/common/clock_tracker.h
index 80b2a4e..16b9d79 100644
--- a/src/trace_processor/importers/common/clock_tracker.h
+++ b/src/trace_processor/importers/common/clock_tracker.h
@@ -154,7 +154,8 @@
 
   // Appends a new snapshot for the given clock domains.
   // This is typically called by the code that reads the ClockSnapshot packet.
-  void AddSnapshot(const std::vector<ClockValue>&);
+  // Returns the internal snapshot id of this set of clocks.
+  uint32_t AddSnapshot(const std::vector<ClockValue>&);
 
   // Converts a timestamp between two clock domains. Tries to use the cache
   // first (only for single-path resolutions), then falls back on path finding
diff --git a/src/trace_processor/importers/common/flow_tracker.h b/src/trace_processor/importers/common/flow_tracker.h
index c2baf93..f05d854 100644
--- a/src/trace_processor/importers/common/flow_tracker.h
+++ b/src/trace_processor/importers/common/flow_tracker.h
@@ -33,7 +33,7 @@
   explicit FlowTracker(TraceProcessorContext*);
   virtual ~FlowTracker();
 
-  void InsertFlow(SliceId outgoing_slice_id, SliceId incoming_slice_id);
+  void InsertFlow(SliceId slice_out_id, SliceId slice_in_id);
 
   // These methods assume you have created a FlowId via GetFlowIdForV1Event.
   // If you don't have a v1 event you should use the InsertFlow method above.
diff --git a/src/trace_processor/importers/common/slice_tracker.h b/src/trace_processor/importers/common/slice_tracker.h
index 5da033a..f574164 100644
--- a/src/trace_processor/importers/common/slice_tracker.h
+++ b/src/trace_processor/importers/common/slice_tracker.h
@@ -155,7 +155,7 @@
 
   // Timestamp of the previous event. Used to discard events arriving out
   // of order.
-  int64_t prev_timestamp_ = 0;
+  int64_t prev_timestamp_ = std::numeric_limits<int64_t>::min();
 
   const StringId legacy_unnestable_begin_count_string_id_;
   const StringId legacy_unnestable_last_begin_ts_string_id_;
diff --git a/src/trace_processor/importers/common/slice_tracker_unittest.cc b/src/trace_processor/importers/common/slice_tracker_unittest.cc
index 929e186..23e2bcf 100644
--- a/src/trace_processor/importers/common/slice_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/slice_tracker_unittest.cc
@@ -72,6 +72,28 @@
   EXPECT_EQ(slices.arg_set_id()[0], kInvalidArgSetId);
 }
 
+TEST(SliceTrackerTest, NegativeTimestamps) {
+  TraceProcessorContext context;
+  context.storage.reset(new TraceStorage());
+  SliceTracker tracker(&context);
+
+  constexpr TrackId track{22u};
+  tracker.Begin(-1000 /*ts*/, track, kNullStringId /*cat*/,
+                StringId::Raw(1) /*name*/);
+  tracker.End(-501 /*ts*/, track, kNullStringId /*cat*/,
+              StringId::Raw(1) /*name*/);
+
+  const auto& slices = context.storage->slice_table();
+  EXPECT_EQ(slices.row_count(), 1u);
+  EXPECT_EQ(slices.ts()[0], -1000);
+  EXPECT_EQ(slices.dur()[0], 499);
+  EXPECT_EQ(slices.track_id()[0], track);
+  EXPECT_EQ(slices.category()[0].raw_id(), 0u);
+  EXPECT_EQ(slices.name()[0].raw_id(), 1u);
+  EXPECT_EQ(slices.depth()[0], 0u);
+  EXPECT_EQ(slices.arg_set_id()[0], kInvalidArgSetId);
+}
+
 TEST(SliceTrackerTest, OneSliceWithArgs) {
   TraceProcessorContext context;
   context.storage.reset(new TraceStorage());
diff --git a/src/trace_processor/trace_parser.h b/src/trace_processor/importers/common/trace_parser.h
similarity index 82%
rename from src/trace_processor/trace_parser.h
rename to src/trace_processor/importers/common/trace_parser.h
index fa3b4ef..278a9d7 100644
--- a/src/trace_processor/trace_parser.h
+++ b/src/trace_processor/importers/common/trace_parser.h
@@ -14,16 +14,16 @@
  * limitations under the License.
  */
 
-#ifndef SRC_TRACE_PROCESSOR_TRACE_PARSER_H_
-#define SRC_TRACE_PROCESSOR_TRACE_PARSER_H_
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
 
 #include <stdint.h>
 
-#include "src/trace_processor/timestamped_trace_piece.h"
-
 namespace perfetto {
 namespace trace_processor {
 
+struct TimestampedTracePiece;
+
 class TraceParser {
  public:
   virtual ~TraceParser();
@@ -37,4 +37,4 @@
 }  // namespace trace_processor
 }  // namespace perfetto
 
-#endif  // SRC_TRACE_PROCESSOR_TRACE_PARSER_H_
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
diff --git a/src/trace_processor/importers/ftrace/binder_tracker.cc b/src/trace_processor/importers/ftrace/binder_tracker.cc
index f8e040d..d866c0a 100644
--- a/src/trace_processor/importers/ftrace/binder_tracker.cc
+++ b/src/trace_processor/importers/ftrace/binder_tracker.cc
@@ -106,7 +106,7 @@
                                  base::StringView(flag_str))));
     inserter->AddArg(code_, Variadic::String(code));
     inserter->AddArg(calling_tid_, Variadic::UnsignedInteger(tid));
-    // TODO(taylori): The legacy UI included the calling pid in the args,
+    // TODO(hjd): The legacy UI included the calling pid in the args,
     // is this necessary? It's complicated in our case because process
     // association might not happen until after the binder transaction slices
     // have been parsed. We would need to backfill the arg.
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index d493173..cb0d50e 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, 351> descriptors{{
+std::array<MessageDescriptor, 359> descriptors{{
     {nullptr, 0, {}},
     {nullptr, 0, {}},
     {nullptr, 0, {}},
@@ -3741,6 +3741,106 @@
             {"value", ProtoSchemaType::kInt32},
         },
     },
+    {
+        "dma_heap_stat",
+        3,
+        {
+            {},
+            {"inode", ProtoSchemaType::kUint64},
+            {"len", ProtoSchemaType::kInt64},
+            {"total_allocated", ProtoSchemaType::kUint64},
+        },
+    },
+    {
+        "cpuhp_pause",
+        4,
+        {
+            {},
+            {"active_cpus", ProtoSchemaType::kUint32},
+            {"cpus", ProtoSchemaType::kUint32},
+            {"pause", ProtoSchemaType::kUint32},
+            {"time", ProtoSchemaType::kUint32},
+        },
+    },
+    {
+        "sched_pi_setprio",
+        4,
+        {
+            {},
+            {"comm", ProtoSchemaType::kString},
+            {"newprio", ProtoSchemaType::kInt32},
+            {"oldprio", ProtoSchemaType::kInt32},
+            {"pid", ProtoSchemaType::kInt32},
+        },
+    },
+    {
+        "sde_sde_evtlog",
+        3,
+        {
+            {},
+            {"evtlog_tag", ProtoSchemaType::kString},
+            {"pid", ProtoSchemaType::kInt32},
+            {"tag_id", ProtoSchemaType::kUint32},
+        },
+    },
+    {
+        "sde_sde_perf_calc_crtc",
+        8,
+        {
+            {},
+            {"bw_ctl_ebi", ProtoSchemaType::kUint64},
+            {"bw_ctl_llcc", ProtoSchemaType::kUint64},
+            {"bw_ctl_mnoc", ProtoSchemaType::kUint64},
+            {"core_clk_rate", ProtoSchemaType::kUint32},
+            {"crtc", ProtoSchemaType::kUint32},
+            {"ib_ebi", ProtoSchemaType::kUint64},
+            {"ib_llcc", ProtoSchemaType::kUint64},
+            {"ib_mnoc", ProtoSchemaType::kUint64},
+        },
+    },
+    {
+        "sde_sde_perf_crtc_update",
+        12,
+        {
+            {},
+            {"bw_ctl_ebi", ProtoSchemaType::kUint64},
+            {"bw_ctl_llcc", ProtoSchemaType::kUint64},
+            {"bw_ctl_mnoc", ProtoSchemaType::kUint64},
+            {"core_clk_rate", ProtoSchemaType::kUint32},
+            {"crtc", ProtoSchemaType::kUint32},
+            {"params", ProtoSchemaType::kInt32},
+            {"per_pipe_ib_ebi", ProtoSchemaType::kUint64},
+            {"per_pipe_ib_llcc", ProtoSchemaType::kUint64},
+            {"per_pipe_ib_mnoc", ProtoSchemaType::kUint64},
+            {"stop_req", ProtoSchemaType::kUint32},
+            {"update_bus", ProtoSchemaType::kUint32},
+            {"update_clk", ProtoSchemaType::kUint32},
+        },
+    },
+    {
+        "sde_sde_perf_set_qos_luts",
+        6,
+        {
+            {},
+            {"fl", ProtoSchemaType::kUint32},
+            {"fmt", ProtoSchemaType::kUint32},
+            {"lut", ProtoSchemaType::kUint64},
+            {"lut_usage", ProtoSchemaType::kUint32},
+            {"pnum", ProtoSchemaType::kUint32},
+            {"rt", ProtoSchemaType::kUint32},
+        },
+    },
+    {
+        "sde_sde_perf_update_bus",
+        4,
+        {
+            {},
+            {"ab_quota", ProtoSchemaType::kUint64},
+            {"bus_id", ProtoSchemaType::kUint32},
+            {"client", ProtoSchemaType::kInt32},
+            {"ib_quota", ProtoSchemaType::kUint64},
+        },
+    },
 }};
 
 }  // namespace
diff --git a/src/trace_processor/importers/ftrace/ftrace_module_impl.cc b/src/trace_processor/importers/ftrace/ftrace_module_impl.cc
index 5d2a4e4..7464db5 100644
--- a/src/trace_processor/importers/ftrace/ftrace_module_impl.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_module_impl.cc
@@ -19,7 +19,7 @@
 #include "src/trace_processor/importers/ftrace/ftrace_parser.h"
 #include "src/trace_processor/importers/ftrace/ftrace_tokenizer.h"
 #include "src/trace_processor/timestamped_trace_piece.h"
-#include "src/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
diff --git a/src/trace_processor/importers/ftrace/ftrace_module_impl.h b/src/trace_processor/importers/ftrace/ftrace_module_impl.h
index 66f5fe48..488c2eb 100644
--- a/src/trace_processor/importers/ftrace/ftrace_module_impl.h
+++ b/src/trace_processor/importers/ftrace/ftrace_module_impl.h
@@ -23,7 +23,7 @@
 #include "src/trace_processor/importers/ftrace/ftrace_tokenizer.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/timestamped_trace_piece.h"
-#include "src/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index 69929a8..654b332 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -30,6 +30,7 @@
 
 #include "protos/perfetto/common/gpu_counter_descriptor.pbzero.h"
 #include "protos/perfetto/trace/ftrace/binder.pbzero.h"
+#include "protos/perfetto/trace/ftrace/cpuhp.pbzero.h"
 #include "protos/perfetto/trace/ftrace/dmabuf_heap.pbzero.h"
 #include "protos/perfetto/trace/ftrace/dpu.pbzero.h"
 #include "protos/perfetto/trace/ftrace/fastrpc.pbzero.h"
@@ -553,6 +554,10 @@
         ParseMaliTracingMarkWrite(ts, pid, data);
         break;
       }
+      case FtraceEvent::kCpuhpPauseFieldNumber: {
+        ParseCpuhpPause(ts, pid, data);
+        break;
+      }
       default:
         break;
     }
@@ -592,7 +597,7 @@
 
 void FtraceParser::ParseTypedFtraceToRaw(
     uint32_t ftrace_id,
-    int64_t ts,
+    int64_t timestamp,
     uint32_t cpu,
     uint32_t tid,
     ConstBytes blob,
@@ -610,9 +615,10 @@
   MessageDescriptor* m = GetMessageDescriptorForId(ftrace_id);
   const auto& message_strings = ftrace_message_strings_[ftrace_id];
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(tid);
-  RawId id = context_->storage->mutable_raw_table()
-                 ->Insert({ts, message_strings.message_name_id, cpu, utid})
-                 .id;
+  RawId id =
+      context_->storage->mutable_raw_table()
+          ->Insert({timestamp, message_strings.message_name_id, cpu, utid})
+          .id;
   auto inserter = context_->args_tracker->AddArgsTo(id);
 
   for (auto fld = decoder.ReadField(); fld.valid(); fld = decoder.ReadField()) {
@@ -700,81 +706,86 @@
 }
 
 PERFETTO_ALWAYS_INLINE
-void FtraceParser::ParseSchedSwitch(uint32_t cpu, int64_t ts, ConstBytes blob) {
+void FtraceParser::ParseSchedSwitch(uint32_t cpu,
+                                    int64_t timestamp,
+                                    ConstBytes blob) {
   protos::pbzero::SchedSwitchFtraceEvent::Decoder ss(blob.data, blob.size);
   uint32_t prev_pid = static_cast<uint32_t>(ss.prev_pid());
   uint32_t next_pid = static_cast<uint32_t>(ss.next_pid());
   SchedEventTracker::GetOrCreate(context_)->PushSchedSwitch(
-      cpu, ts, prev_pid, ss.prev_comm(), ss.prev_prio(), ss.prev_state(),
+      cpu, timestamp, prev_pid, ss.prev_comm(), ss.prev_prio(), ss.prev_state(),
       next_pid, ss.next_comm(), ss.next_prio());
 }
 
-void FtraceParser::ParseSchedWakeup(int64_t ts, ConstBytes blob) {
+void FtraceParser::ParseSchedWakeup(int64_t timestamp, 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(
       wakee_pid, name_id, ThreadNamePriority::kFtrace);
-  context_->event_tracker->PushInstant(ts, sched_wakeup_name_id_, utid,
+  context_->event_tracker->PushInstant(timestamp, sched_wakeup_name_id_, utid,
                                        RefType::kRefUtid);
 }
 
-void FtraceParser::ParseSchedWaking(int64_t ts, ConstBytes blob) {
+void FtraceParser::ParseSchedWaking(int64_t timestamp, 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(
       wakee_pid, name_id, ThreadNamePriority::kFtrace);
-  context_->event_tracker->PushInstant(ts, sched_waking_name_id_, utid,
+  context_->event_tracker->PushInstant(timestamp, sched_waking_name_id_, utid,
                                        RefType::kRefUtid);
 }
 
-void FtraceParser::ParseSchedProcessFree(int64_t ts, ConstBytes blob) {
+void FtraceParser::ParseSchedProcessFree(int64_t timestamp, ConstBytes blob) {
   protos::pbzero::SchedProcessFreeFtraceEvent::Decoder ex(blob.data, blob.size);
   uint32_t pid = static_cast<uint32_t>(ex.pid());
-  context_->process_tracker->EndThread(ts, pid);
+  context_->process_tracker->EndThread(timestamp, pid);
 }
 
-void FtraceParser::ParseCpuFreq(int64_t ts, ConstBytes blob) {
+void FtraceParser::ParseCpuFreq(int64_t timestamp, ConstBytes blob) {
   protos::pbzero::CpuFrequencyFtraceEvent::Decoder freq(blob.data, blob.size);
   uint32_t cpu = freq.cpu_id();
   uint32_t new_freq = freq.state();
   TrackId track =
       context_->track_tracker->InternCpuCounterTrack(cpu_freq_name_id_, cpu);
-  context_->event_tracker->PushCounter(ts, new_freq, track);
+  context_->event_tracker->PushCounter(timestamp, new_freq, track);
 }
 
-void FtraceParser::ParseGpuFreq(int64_t ts, ConstBytes blob) {
+void FtraceParser::ParseGpuFreq(int64_t timestamp, ConstBytes blob) {
   protos::pbzero::GpuFrequencyFtraceEvent::Decoder freq(blob.data, blob.size);
   uint32_t gpu = freq.gpu_id();
   uint32_t new_freq = freq.state();
   TrackId track =
       context_->track_tracker->InternGpuCounterTrack(gpu_freq_name_id_, gpu);
-  context_->event_tracker->PushCounter(ts, new_freq, track);
+  context_->event_tracker->PushCounter(timestamp, new_freq, track);
 }
 
-void FtraceParser::ParseCpuIdle(int64_t ts, ConstBytes blob) {
+void FtraceParser::ParseCpuIdle(int64_t timestamp, ConstBytes blob) {
   protos::pbzero::CpuIdleFtraceEvent::Decoder idle(blob.data, blob.size);
   uint32_t cpu = idle.cpu_id();
   uint32_t new_state = idle.state();
   TrackId track =
       context_->track_tracker->InternCpuCounterTrack(cpu_idle_name_id_, cpu);
-  context_->event_tracker->PushCounter(ts, new_state, track);
+  context_->event_tracker->PushCounter(timestamp, new_state, track);
 }
 
-void FtraceParser::ParsePrint(int64_t ts, uint32_t pid, ConstBytes blob) {
+void FtraceParser::ParsePrint(int64_t timestamp,
+                              uint32_t pid,
+                              ConstBytes blob) {
   protos::pbzero::PrintFtraceEvent::Decoder evt(blob.data, blob.size);
-  SystraceParser::GetOrCreate(context_)->ParsePrintEvent(ts, pid, evt.buf());
+  SystraceParser::GetOrCreate(context_)->ParsePrintEvent(timestamp, pid,
+                                                         evt.buf());
 }
 
-void FtraceParser::ParseZero(int64_t ts, uint32_t pid, ConstBytes blob) {
+void FtraceParser::ParseZero(int64_t timestamp, uint32_t pid, ConstBytes blob) {
   protos::pbzero::ZeroFtraceEvent::Decoder evt(blob.data, blob.size);
   uint32_t tgid = static_cast<uint32_t>(evt.pid());
   SystraceParser::GetOrCreate(context_)->ParseZeroEvent(
-      ts, pid, evt.flag(), evt.name(), tgid, evt.value());
+      timestamp, pid, evt.flag(), evt.name(), tgid, evt.value());
 }
 
-void FtraceParser::ParseSdeTracingMarkWrite(int64_t ts,
+void FtraceParser::ParseSdeTracingMarkWrite(int64_t timestamp,
                                             uint32_t pid,
                                             ConstBytes blob) {
   protos::pbzero::SdeTracingMarkWriteFtraceEvent::Decoder evt(blob.data,
@@ -786,11 +797,11 @@
 
   uint32_t tgid = static_cast<uint32_t>(evt.pid());
   SystraceParser::GetOrCreate(context_)->ParseTracingMarkWrite(
-      ts, pid, static_cast<char>(evt.trace_type()), evt.trace_begin(),
+      timestamp, pid, static_cast<char>(evt.trace_type()), evt.trace_begin(),
       evt.trace_name(), tgid, evt.value());
 }
 
-void FtraceParser::ParseDpuTracingMarkWrite(int64_t ts,
+void FtraceParser::ParseDpuTracingMarkWrite(int64_t timestamp,
                                             uint32_t pid,
                                             ConstBytes blob) {
   protos::pbzero::DpuTracingMarkWriteFtraceEvent::Decoder evt(blob.data,
@@ -802,11 +813,11 @@
 
   uint32_t tgid = static_cast<uint32_t>(evt.pid());
   SystraceParser::GetOrCreate(context_)->ParseTracingMarkWrite(
-      ts, pid, static_cast<char>(evt.type()), false /*trace_begin*/, evt.name(),
-      tgid, evt.value());
+      timestamp, pid, static_cast<char>(evt.type()), false /*trace_begin*/,
+      evt.name(), tgid, evt.value());
 }
 
-void FtraceParser::ParseG2dTracingMarkWrite(int64_t ts,
+void FtraceParser::ParseG2dTracingMarkWrite(int64_t timestamp,
                                             uint32_t pid,
                                             ConstBytes blob) {
   protos::pbzero::G2dTracingMarkWriteFtraceEvent::Decoder evt(blob.data,
@@ -818,11 +829,11 @@
 
   uint32_t tgid = static_cast<uint32_t>(evt.pid());
   SystraceParser::GetOrCreate(context_)->ParseTracingMarkWrite(
-      ts, pid, static_cast<char>(evt.type()), false /*trace_begin*/, evt.name(),
-      tgid, evt.value());
+      timestamp, pid, static_cast<char>(evt.type()), false /*trace_begin*/,
+      evt.name(), tgid, evt.value());
 }
 
-void FtraceParser::ParseMaliTracingMarkWrite(int64_t ts,
+void FtraceParser::ParseMaliTracingMarkWrite(int64_t timestamp,
                                              uint32_t pid,
                                              ConstBytes blob) {
   protos::pbzero::MaliTracingMarkWriteFtraceEvent::Decoder evt(blob.data,
@@ -834,12 +845,12 @@
 
   uint32_t tgid = static_cast<uint32_t>(evt.pid());
   SystraceParser::GetOrCreate(context_)->ParseTracingMarkWrite(
-      ts, pid, static_cast<char>(evt.type()), false /*trace_begin*/, evt.name(),
-      tgid, evt.value());
+      timestamp, pid, static_cast<char>(evt.type()), false /*trace_begin*/,
+      evt.name(), tgid, evt.value());
 }
 
 /** Parses ion heap events present in Pixel kernels. */
-void FtraceParser::ParseIonHeapGrowOrShrink(int64_t ts,
+void FtraceParser::ParseIonHeapGrowOrShrink(int64_t timestamp,
                                             uint32_t pid,
                                             ConstBytes blob,
                                             bool grow) {
@@ -865,16 +876,16 @@
   // Push the global counter.
   TrackId track =
       context_->track_tracker->InternGlobalCounterTrack(global_name_id);
-  context_->event_tracker->PushCounter(ts, static_cast<double>(total_bytes),
-                                       track);
+  context_->event_tracker->PushCounter(timestamp,
+                                       static_cast<double>(total_bytes), track);
 
   // Push the change counter.
   // TODO(b/121331269): these should really be instant events.
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
   track =
       context_->track_tracker->InternThreadCounterTrack(change_name_id, utid);
-  context_->event_tracker->PushCounter(ts, static_cast<double>(change_bytes),
-                                       track);
+  context_->event_tracker->PushCounter(
+      timestamp, static_cast<double>(change_bytes), track);
 
   // We are reusing the same function for ion_heap_grow and ion_heap_shrink.
   // It is fine as the arguments are the same, but we need to be sure that the
@@ -896,7 +907,7 @@
 }
 
 /** Parses ion heap events (introduced in 4.19 kernels). */
-void FtraceParser::ParseIonStat(int64_t ts,
+void FtraceParser::ParseIonStat(int64_t timestamp,
                                 uint32_t pid,
                                 protozero::ConstBytes data) {
   protos::pbzero::IonStatFtraceEvent::Decoder ion(data.data, data.size);
@@ -904,15 +915,15 @@
   TrackId track =
       context_->track_tracker->InternGlobalCounterTrack(ion_total_id_);
   context_->event_tracker->PushCounter(
-      ts, static_cast<double>(ion.total_allocated()), track);
+      timestamp, static_cast<double>(ion.total_allocated()), track);
 
   // Push the change counter.
   // TODO(b/121331269): these should really be instant events.
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
   track =
       context_->track_tracker->InternThreadCounterTrack(ion_change_id_, utid);
-  context_->event_tracker->PushCounter(ts, static_cast<double>(ion.len()),
-                                       track);
+  context_->event_tracker->PushCounter(timestamp,
+                                       static_cast<double>(ion.len()), track);
 
   // Global track for individual buffer tracking
   auto async_track =
@@ -922,16 +933,16 @@
         context_->async_track_set_tracker->Begin(async_track, ion.buffer_id());
     std::string buf = std::to_string(ion.len() / 1024) + " kB";
     context_->slice_tracker->Begin(
-        ts, start_id, kNullStringId,
+        timestamp, start_id, kNullStringId,
         context_->storage->InternString(base::StringView(buf)));
   } else {
     TrackId end_id =
         context_->async_track_set_tracker->End(async_track, ion.buffer_id());
-    context_->slice_tracker->End(ts, end_id);
+    context_->slice_tracker->End(timestamp, end_id);
   }
 }
 
-void FtraceParser::ParseDmaHeapStat(int64_t ts,
+void FtraceParser::ParseDmaHeapStat(int64_t timestamp,
                                     uint32_t pid,
                                     protozero::ConstBytes data) {
   protos::pbzero::DmaHeapStatFtraceEvent::Decoder dma_heap(data.data,
@@ -940,15 +951,15 @@
   TrackId track =
       context_->track_tracker->InternGlobalCounterTrack(dma_heap_total_id_);
   context_->event_tracker->PushCounter(
-      ts, static_cast<double>(dma_heap.total_allocated()), track);
+      timestamp, static_cast<double>(dma_heap.total_allocated()), track);
 
   // Push the change counter.
   // TODO(b/121331269): these should really be instant events.
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
   track = context_->track_tracker->InternThreadCounterTrack(dma_heap_change_id_,
                                                             utid);
-  context_->event_tracker->PushCounter(ts, static_cast<double>(dma_heap.len()),
-                                       track);
+  context_->event_tracker->PushCounter(
+      timestamp, static_cast<double>(dma_heap.len()), track);
 
   // Global track for individual buffer tracking
   auto async_track =
@@ -958,43 +969,43 @@
         async_track, static_cast<int64_t>(dma_heap.inode()));
     std::string buf = std::to_string(dma_heap.len() / 1024) + " kB";
     context_->slice_tracker->Begin(
-        ts, start_id, kNullStringId,
+        timestamp, start_id, kNullStringId,
         context_->storage->InternString(base::StringView(buf)));
   } else {
     TrackId end_id = context_->async_track_set_tracker->End(
         async_track, static_cast<int64_t>(dma_heap.inode()));
-    context_->slice_tracker->End(ts, end_id);
+    context_->slice_tracker->End(timestamp, end_id);
   }
 }
 
 // This event has both the pid of the thread that sent the signal and the
 // destination of the signal. Currently storing the pid of the destination.
-void FtraceParser::ParseSignalGenerate(int64_t ts, ConstBytes blob) {
+void FtraceParser::ParseSignalGenerate(int64_t timestamp, ConstBytes blob) {
   protos::pbzero::SignalGenerateFtraceEvent::Decoder sig(blob.data, blob.size);
 
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(
       static_cast<uint32_t>(sig.pid()));
-  InstantId id = context_->event_tracker->PushInstant(ts, signal_generate_id_,
-                                                      utid, RefType::kRefUtid);
+  InstantId id = context_->event_tracker->PushInstant(
+      timestamp, signal_generate_id_, utid, RefType::kRefUtid);
 
   context_->args_tracker->AddArgsTo(id).AddArg(signal_name_id_,
                                                Variadic::Integer(sig.sig()));
 }
 
-void FtraceParser::ParseSignalDeliver(int64_t ts,
+void FtraceParser::ParseSignalDeliver(int64_t timestamp,
                                       uint32_t pid,
                                       ConstBytes blob) {
   protos::pbzero::SignalDeliverFtraceEvent::Decoder sig(blob.data, blob.size);
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
-  InstantId id = context_->event_tracker->PushInstant(ts, signal_deliver_id_,
-                                                      utid, RefType::kRefUtid);
+  InstantId id = context_->event_tracker->PushInstant(
+      timestamp, signal_deliver_id_, utid, RefType::kRefUtid);
 
   context_->args_tracker->AddArgsTo(id).AddArg(signal_name_id_,
                                                Variadic::Integer(sig.sig()));
 }
 
-void FtraceParser::ParseLowmemoryKill(int64_t ts, ConstBytes blob) {
-  // TODO(taylori): Store the pagecache_size, pagecache_limit and free fields
+void FtraceParser::ParseLowmemoryKill(int64_t timestamp, ConstBytes blob) {
+  // TODO(hjd): Store the pagecache_size, pagecache_limit and free fields
   // in an args table
   protos::pbzero::LowmemoryKillFtraceEvent::Decoder lmk(blob.data, blob.size);
 
@@ -1008,7 +1019,7 @@
     return;
 
   InstantId id = context_->event_tracker->PushInstant(
-      ts, lmk_id_, opt_utid.value(), RefType::kRefUtid, true);
+      timestamp, lmk_id_, opt_utid.value(), RefType::kRefUtid, true);
 
   // Store the comm as an arg.
   auto comm_id = context_->storage->InternString(
@@ -1017,7 +1028,7 @@
                                                Variadic::String(comm_id));
 }
 
-void FtraceParser::ParseOOMScoreAdjUpdate(int64_t ts, ConstBytes blob) {
+void FtraceParser::ParseOOMScoreAdjUpdate(int64_t timestamp, ConstBytes blob) {
   protos::pbzero::OomScoreAdjUpdateFtraceEvent::Decoder evt(blob.data,
                                                             blob.size);
   // The int16_t static cast is because older version of the on-device tracer
@@ -1025,19 +1036,19 @@
   int16_t oom_adj = static_cast<int16_t>(evt.oom_score_adj());
   uint32_t tid = static_cast<uint32_t>(evt.pid());
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(tid);
-  context_->event_tracker->PushProcessCounterForThread(ts, oom_adj,
+  context_->event_tracker->PushProcessCounterForThread(timestamp, oom_adj,
                                                        oom_score_adj_id_, utid);
 }
 
-void FtraceParser::ParseOOMKill(int64_t ts, ConstBytes blob) {
+void FtraceParser::ParseOOMKill(int64_t timestamp, ConstBytes blob) {
   protos::pbzero::MarkVictimFtraceEvent::Decoder evt(blob.data, blob.size);
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(
       static_cast<uint32_t>(evt.pid()));
-  context_->event_tracker->PushInstant(ts, oom_kill_id_, utid,
+  context_->event_tracker->PushInstant(timestamp, oom_kill_id_, utid,
                                        RefType::kRefUtid, true);
 }
 
-void FtraceParser::ParseMmEventRecord(int64_t ts,
+void FtraceParser::ParseMmEventRecord(int64_t timestamp,
                                       uint32_t pid,
                                       ConstBytes blob) {
   protos::pbzero::MmEventRecordFtraceEvent::Decoder evt(blob.data, blob.size);
@@ -1051,14 +1062,14 @@
 
   const auto& counter_names = mm_event_counter_names_[type];
   context_->event_tracker->PushProcessCounterForThread(
-      ts, evt.count(), counter_names.count, utid);
+      timestamp, evt.count(), counter_names.count, utid);
   context_->event_tracker->PushProcessCounterForThread(
-      ts, evt.max_lat(), counter_names.max_lat, utid);
+      timestamp, evt.max_lat(), counter_names.max_lat, utid);
   context_->event_tracker->PushProcessCounterForThread(
-      ts, evt.avg_lat(), counter_names.avg_lat, utid);
+      timestamp, evt.avg_lat(), counter_names.avg_lat, utid);
 }
 
-void FtraceParser::ParseSysEvent(int64_t ts,
+void FtraceParser::ParseSysEvent(int64_t timestamp,
                                  uint32_t pid,
                                  bool is_enter,
                                  ConstBytes blob) {
@@ -1068,9 +1079,9 @@
 
   SyscallTracker* syscall_tracker = SyscallTracker::GetOrCreate(context_);
   if (is_enter) {
-    syscall_tracker->Enter(ts, utid, syscall_num);
+    syscall_tracker->Enter(timestamp, utid, syscall_num);
   } else {
-    syscall_tracker->Exit(ts, utid, syscall_num);
+    syscall_tracker->Exit(timestamp, utid, syscall_num);
   }
 
   // We are reusing the same function for sys_enter and sys_exit.
@@ -1082,7 +1093,7 @@
       "field mismatch");
 }
 
-void FtraceParser::ParseTaskNewTask(int64_t ts,
+void FtraceParser::ParseTaskNewTask(int64_t timestamp,
                                     uint32_t source_tid,
                                     ConstBytes blob) {
   protos::pbzero::TaskNewtaskFtraceEvent::Decoder evt(blob.data, blob.size);
@@ -1099,7 +1110,7 @@
   // kthreadd in which case just make it a new thread associated with kthreadd.
   if ((clone_flags & kCloneThread) == 0 && source_tid != kKthreaddPid) {
     // This is a plain-old fork() or equivalent.
-    proc_tracker->StartNewProcess(ts, source_tid, new_tid, new_comm);
+    proc_tracker->StartNewProcess(timestamp, source_tid, new_tid, new_comm);
     return;
   }
 
@@ -1111,7 +1122,7 @@
   // This is a pthread_create or similar. Bind the two threads together, so
   // they get resolved to the same process.
   auto source_utid = proc_tracker->GetOrCreateThread(source_tid);
-  auto new_utid = proc_tracker->StartNewThread(ts, new_tid);
+  auto new_utid = proc_tracker->StartNewThread(timestamp, new_tid);
   proc_tracker->UpdateThreadNameByUtid(new_utid, new_comm,
                                        ThreadNamePriority::kFtrace);
   proc_tracker->AssociateThreads(source_utid, new_utid);
@@ -1347,7 +1358,8 @@
   context_->slice_tracker->End(timestamp, track, irq_id_, {}, args_inserter);
 }
 
-void FtraceParser::ParseGpuMemTotal(int64_t ts, protozero::ConstBytes data) {
+void FtraceParser::ParseGpuMemTotal(int64_t timestamp,
+                                    protozero::ConstBytes data) {
   protos::pbzero::GpuMemTotalFtraceEvent::Decoder gpu_mem_total(data.data,
                                                                 data.size);
 
@@ -1367,7 +1379,7 @@
         gpu_mem_total_proc_desc_id_);
   }
   context_->event_tracker->PushCounter(
-      ts, static_cast<double>(gpu_mem_total.size()), track);
+      timestamp, static_cast<double>(gpu_mem_total.size()), track);
 }
 
 void FtraceParser::ParseThermalTemperature(int64_t timestamp,
@@ -1459,5 +1471,12 @@
       timestamp, static_cast<double>(evt.len()), delta_track);
 }
 
+void FtraceParser::ParseCpuhpPause(int64_t,
+                                   uint32_t,
+                                   protozero::ConstBytes blob) {
+  protos::pbzero::CpuhpPauseFtraceEvent::Decoder evt(blob.data, blob.size);
+  // TODO(b/183110813): Parse and visualize this event.
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index b1ba46e..79e1c65 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -23,8 +23,8 @@
 #include "src/trace_processor/importers/ftrace/rss_stat_tracker.h"
 #include "src/trace_processor/importers/ftrace/sched_event_tracker.h"
 #include "src/trace_processor/timestamped_trace_piece.h"
-#include "src/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -69,19 +69,23 @@
   void ParseMaliTracingMarkWrite(int64_t timestamp,
                                  uint32_t pid,
                                  protozero::ConstBytes);
-  void ParseIonHeapGrowOrShrink(int64_t ts,
+  void ParseIonHeapGrowOrShrink(int64_t timestamp,
                                 uint32_t pid,
                                 protozero::ConstBytes,
                                 bool grow);
-  void ParseIonStat(int64_t ts, uint32_t pid, protozero::ConstBytes);
-  void ParseDmaHeapStat(int64_t ts, uint32_t pid, protozero::ConstBytes);
-  void ParseSignalGenerate(int64_t ts, protozero::ConstBytes);
-  void ParseSignalDeliver(int64_t ts, uint32_t pid, protozero::ConstBytes);
-  void ParseLowmemoryKill(int64_t ts, protozero::ConstBytes);
-  void ParseOOMScoreAdjUpdate(int64_t ts, protozero::ConstBytes);
-  void ParseOOMKill(int64_t ts, protozero::ConstBytes);
-  void ParseMmEventRecord(int64_t ts, uint32_t pid, protozero::ConstBytes);
-  void ParseSysEvent(int64_t ts,
+  void ParseIonStat(int64_t timestamp, uint32_t pid, protozero::ConstBytes);
+  void ParseDmaHeapStat(int64_t timestamp, uint32_t pid, protozero::ConstBytes);
+  void ParseSignalGenerate(int64_t timestamp, protozero::ConstBytes);
+  void ParseSignalDeliver(int64_t timestamp,
+                          uint32_t pid,
+                          protozero::ConstBytes);
+  void ParseLowmemoryKill(int64_t timestamp, protozero::ConstBytes);
+  void ParseOOMScoreAdjUpdate(int64_t timestamp, protozero::ConstBytes);
+  void ParseOOMKill(int64_t timestamp, protozero::ConstBytes);
+  void ParseMmEventRecord(int64_t timestamp,
+                          uint32_t pid,
+                          protozero::ConstBytes);
+  void ParseSysEvent(int64_t timestamp,
                      uint32_t pid,
                      bool is_enter,
                      protozero::ConstBytes);
@@ -142,6 +146,7 @@
   void ParseFastRpcDmaStat(int64_t timestamp,
                            uint32_t pid,
                            protozero::ConstBytes);
+  void ParseCpuhpPause(int64_t, uint32_t, protozero::ConstBytes);
 
   TraceProcessorContext* context_;
   RssStatTracker rss_stat_tracker_;
diff --git a/src/trace_processor/importers/ftrace/ftrace_tokenizer.h b/src/trace_processor/importers/ftrace/ftrace_tokenizer.h
index 7e8248b..8f95fca 100644
--- a/src/trace_processor/importers/ftrace/ftrace_tokenizer.h
+++ b/src/trace_processor/importers/ftrace/ftrace_tokenizer.h
@@ -19,8 +19,8 @@
 
 #include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
 #include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 namespace perfetto {
 namespace trace_processor {
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_record.h b/src/trace_processor/importers/fuchsia/fuchsia_record.h
index 211785d..a5063ae 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_record.h
+++ b/src/trace_processor/importers/fuchsia/fuchsia_record.h
@@ -19,7 +19,7 @@
 
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_utils.h"
 #include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 #include <vector>
 
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h b/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h
index 581704d..a0e39c2 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h
@@ -17,9 +17,9 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_PARSER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_PARSER_H_
 
+#include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
 #include "src/trace_processor/timestamped_trace_piece.h"
-#include "src/trace_processor/trace_parser.h"
 
 namespace perfetto {
 namespace trace_processor {
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
index efec186..57c4718 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
@@ -17,10 +17,10 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_TOKENIZER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_TOKENIZER_H_
 
-#include "src/trace_processor/chunked_trace_reader.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_utils.h"
 #include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 namespace perfetto {
 namespace trace_processor {
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_utils.h b/src/trace_processor/importers/fuchsia/fuchsia_trace_utils.h
index 3af9649..73d4f0d 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_utils.h
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_utils.h
@@ -23,7 +23,7 @@
 
 #include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 namespace perfetto {
 namespace trace_processor {
diff --git a/src/trace_processor/importers/gzip/gzip_trace_parser.cc b/src/trace_processor/importers/gzip/gzip_trace_parser.cc
index bc42db3..cf8416e 100644
--- a/src/trace_processor/importers/gzip/gzip_trace_parser.cc
+++ b/src/trace_processor/importers/gzip/gzip_trace_parser.cc
@@ -22,6 +22,7 @@
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/forwarding_trace_parser.h"
+#include "src/trace_processor/util/gzip_utils.h"
 #include "src/trace_processor/util/status_macros.h"
 
 namespace perfetto {
@@ -29,7 +30,7 @@
 
 namespace {
 
-using ResultCode = GzipDecompressor::ResultCode;
+using ResultCode = util::GzipDecompressor::ResultCode;
 
 }  // namespace
 
diff --git a/src/trace_processor/importers/gzip/gzip_trace_parser.h b/src/trace_processor/importers/gzip/gzip_trace_parser.h
index b56fae3..051284a 100644
--- a/src/trace_processor/importers/gzip/gzip_trace_parser.h
+++ b/src/trace_processor/importers/gzip/gzip_trace_parser.h
@@ -17,8 +17,8 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_GZIP_GZIP_TRACE_PARSER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_GZIP_GZIP_TRACE_PARSER_H_
 
-#include "src/trace_processor/chunked_trace_reader.h"
-#include "src/trace_processor/importers/gzip/gzip_utils.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/util/gzip_utils.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -41,7 +41,7 @@
 
  private:
   TraceProcessorContext* const context_;
-  GzipDecompressor decompressor_;
+  util::GzipDecompressor decompressor_;
   std::unique_ptr<ChunkedTraceReader> inner_;
 
   std::unique_ptr<uint8_t[]> buffer_;
diff --git a/src/trace_processor/importers/json/json_trace_parser.cc b/src/trace_processor/importers/json/json_trace_parser.cc
index eabd57d..e7d029e 100644
--- a/src/trace_processor/importers/json/json_trace_parser.cc
+++ b/src/trace_processor/importers/json/json_trace_parser.cc
@@ -25,6 +25,7 @@
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/ext/base/utils.h"
+#include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
@@ -148,6 +149,59 @@
       MaybeAddFlow(track_id, value);
       break;
     }
+    case 'C': {  // TRACE_EVENT_COUNTER
+      auto args = value["args"];
+      if (!args.isObject()) {
+        context_->storage->IncrementStats(stats::json_parser_failure);
+        break;
+      }
+
+      std::string counter_name_prefix = name.ToStdString();
+      if (value.isMember("id")) {
+        counter_name_prefix += " id: " + value["id"].asString();
+      }
+
+      for (auto it = args.begin(); it != args.end(); ++it) {
+        std::string counter_name = counter_name_prefix + " " + it.name();
+        StringId counter_name_id =
+            context_->storage->InternString(base::StringView(counter_name));
+        context_->event_tracker->PushProcessCounterForThread(
+            timestamp, it->asDouble(), counter_name_id, utid);
+      }
+      break;
+    }
+    case 'i': {  // TRACE_EVENT_INSTANT
+      base::StringView scope;
+      if (value.isMember("s")) {
+        scope = value["s"].asCString();
+      }
+
+      TrackId track_id;
+      if (scope == "g") {
+        track_id = context_->track_tracker
+                       ->GetOrCreateLegacyChromeGlobalInstantTrack();
+      } else if (scope == "p") {
+        if (!opt_pid) {
+          context_->storage->IncrementStats(stats::json_parser_failure);
+          break;
+        }
+        UniquePid upid = context_->process_tracker->GetOrCreateProcess(pid);
+        track_id =
+            context_->track_tracker->InternLegacyChromeProcessInstantTrack(
+                upid);
+      } else if (scope == "t" || scope.data() == nullptr) {
+        if (!opt_tid) {
+          context_->storage->IncrementStats(stats::json_parser_failure);
+          break;
+        }
+        track_id = context_->track_tracker->InternThreadTrack(utid);
+      } else {
+        context_->storage->IncrementStats(stats::json_parser_failure);
+        break;
+      }
+      context_->slice_tracker->Scoped(timestamp, track_id, cat_id, name_id, 0);
+      break;
+    }
     case 's': {  // TRACE_EVENT_FLOW_START
       TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
       auto opt_source_id =
@@ -241,4 +295,3 @@
 
 }  // namespace trace_processor
 }  // namespace perfetto
-
diff --git a/src/trace_processor/importers/json/json_trace_parser.h b/src/trace_processor/importers/json/json_trace_parser.h
index d145953..2084c4b 100644
--- a/src/trace_processor/importers/json/json_trace_parser.h
+++ b/src/trace_processor/importers/json/json_trace_parser.h
@@ -23,10 +23,10 @@
 #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"
-#include "src/trace_processor/trace_parser.h"
 
 namespace Json {
 class Value;
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.cc b/src/trace_processor/importers/json/json_trace_tokenizer.cc
index 8ab4535..aa2b259 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.cc
@@ -24,17 +24,15 @@
 #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_blob_view.h"
 #include "src/trace_processor/trace_sorter.h"
 #include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 namespace perfetto {
 namespace trace_processor {
 
 namespace {
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
-
 util::Status AppendUnescapedCharacter(char c,
                                       bool is_escaping,
                                       std::string* key) {
@@ -103,11 +101,8 @@
   return ReadStringRes::kNeedsMoreData;
 }
 
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
-
 }  // namespace
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 ReadDictRes ReadOneJsonDict(const char* start,
                             const char* end,
                             base::StringView* value,
@@ -365,7 +360,54 @@
   }
   return ReadSystemLineRes::kNeedsMoreData;
 }
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
+
+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) {}
@@ -375,28 +417,23 @@
                                        size_t size) {
   PERFETTO_DCHECK(json::IsJsonSupported());
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
   buffer_.insert(buffer_.end(), data.get(), data.get() + size);
   const char* buf = buffer_.data();
   const char* next = buf;
   const char* end = buf + buffer_.size();
 
-  JsonTracker* json_tracker = JsonTracker::GetOrCreate(context_);
-
-  // 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.
-  const base::StringView view(buf, size);
-  if (view.find("\"displayTimeUnit\":\"ns\"") != base::StringView::npos) {
-    json_tracker->SetTimeUnit(json::TimeUnit::kNs);
-  } else if (view.find("\"displayTimeUnit\":\"ms\"") !=
-             base::StringView::npos) {
-    json_tracker->SetTimeUnit(json::TimeUnit::kMs);
-  }
-
   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, size));
+    if (timeunit) {
+      JsonTracker::GetOrCreate(context_)->SetTimeUnit(*timeunit);
+    }
+
     // Strip leading whitespace.
     while (next != end && isspace(*next)) {
       next++;
@@ -436,18 +473,8 @@
   offset_ += static_cast<uint64_t>(next - buf);
   buffer_.erase(buffer_.begin(), buffer_.begin() + (next - buf));
   return util::OkStatus();
-#else
-  perfetto::base::ignore_result(data);
-  perfetto::base::ignore_result(size);
-  perfetto::base::ignore_result(context_);
-  perfetto::base::ignore_result(format_);
-  perfetto::base::ignore_result(position_);
-  perfetto::base::ignore_result(offset_);
-  return util::ErrStatus("Cannot parse JSON trace due to missing JSON support");
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 }
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 util::Status JsonTraceTokenizer::ParseInternal(const char* start,
                                                const char* end,
                                                const char** out) {
@@ -491,6 +518,12 @@
           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);
+        }
         return ParseInternal(next, end, out);
       } else {
         // If we don't recognize the key, just ignore the rest of the trace and
@@ -595,7 +628,6 @@
   *out = next;
   return util::OkStatus();
 }
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 
 void JsonTraceTokenizer::NotifyEndOfFile() {}
 
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.h b/src/trace_processor/importers/json/json_trace_tokenizer.h
index 0ebf25c..4daf661 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.h
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.h
@@ -19,7 +19,8 @@
 
 #include <stdint.h>
 
-#include "src/trace_processor/chunked_trace_reader.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/importers/json/json_utils.h"
 #include "src/trace_processor/importers/systrace/systrace_line_tokenizer.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
@@ -32,7 +33,6 @@
 
 class TraceProcessorContext;
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 // Visible for testing.
 enum class ReadDictRes {
   kFoundDict,
@@ -93,7 +93,11 @@
                                          const char* end,
                                          std::string* line,
                                          const char** next);
-#endif
+
+// 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 {
@@ -139,11 +143,9 @@
     kEof,
   };
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
   util::Status ParseInternal(const char* start,
                              const char* end,
                              const char** next);
-#endif
 
   TraceProcessorContext* const context_;
 
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 051f52a..6a11a44 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
@@ -272,6 +272,58 @@
   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
index d786bf1..167881e 100644
--- a/src/trace_processor/importers/json/json_tracker.h
+++ b/src/trace_processor/importers/json/json_tracker.h
@@ -47,6 +47,9 @@
   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;
diff --git a/src/trace_processor/importers/json/json_utils.cc b/src/trace_processor/importers/json/json_utils.cc
index 9352c66..c44efa1 100644
--- a/src/trace_processor/importers/json/json_utils.cc
+++ b/src/trace_processor/importers/json/json_utils.cc
@@ -22,6 +22,7 @@
 
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 #include <json/reader.h>
+#include "perfetto/ext/base/string_utils.h"
 #endif
 
 namespace perfetto {
@@ -56,14 +57,8 @@
     case Json::uintValue:
     case Json::intValue:
       return value.asInt64() * TimeUnitToNs(unit);
-    case Json::stringValue: {
-      std::string s = value.asString();
-      char* end;
-      int64_t n = strtoll(s.c_str(), &end, 10);
-      if (end != s.data() + s.size())
-        return base::nullopt;
-      return n * TimeUnitToNs(unit);
-    }
+    case Json::stringValue:
+      return CoerceToTs(unit, value.asString());
     default:
       return base::nullopt;
   }
@@ -74,6 +69,29 @@
 #endif
 }
 
+base::Optional<int64_t> CoerceToTs(TimeUnit unit, const std::string& s) {
+  PERFETTO_DCHECK(IsJsonSupported());
+
+#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
+  size_t lhs_end = std::min<size_t>(s.find('.'), s.size());
+  size_t rhs_start = std::min<size_t>(lhs_end + 1, s.size());
+  base::Optional<int64_t> lhs = base::StringToInt64(s.substr(0, lhs_end));
+  base::Optional<double> rhs =
+      base::StringToDouble("0." + s.substr(rhs_start, std::string::npos));
+  if ((!lhs.has_value() && lhs_end > 0) ||
+      (!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));
+#else
+  perfetto::base::ignore_result(unit);
+  perfetto::base::ignore_result(s);
+  return base::nullopt;
+#endif
+}
+
 base::Optional<int64_t> CoerceToInt64(const Json::Value& value) {
   PERFETTO_DCHECK(IsJsonSupported());
 
diff --git a/src/trace_processor/importers/json/json_utils.h b/src/trace_processor/importers/json/json_utils.h
index f4a70b3..74a2c3a 100644
--- a/src/trace_processor/importers/json/json_utils.h
+++ b/src/trace_processor/importers/json/json_utils.h
@@ -42,6 +42,7 @@
 
 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> 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 4134cc6..a3846ea 100644
--- a/src/trace_processor/importers/json/json_utils_unittest.cc
+++ b/src/trace_processor/importers/json/json_utils_unittest.cc
@@ -46,9 +46,20 @@
   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());
diff --git a/src/trace_processor/importers/ninja/ninja_log_parser.cc b/src/trace_processor/importers/ninja/ninja_log_parser.cc
index 4ba518d..af668f1 100644
--- a/src/trace_processor/importers/ninja/ninja_log_parser.cc
+++ b/src/trace_processor/importers/ninja/ninja_log_parser.cc
@@ -15,6 +15,7 @@
  */
 
 #include "src/trace_processor/importers/ninja/ninja_log_parser.h"
+
 #include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
diff --git a/src/trace_processor/importers/ninja/ninja_log_parser.h b/src/trace_processor/importers/ninja/ninja_log_parser.h
index 280fa5a..f010e73 100644
--- a/src/trace_processor/importers/ninja/ninja_log_parser.h
+++ b/src/trace_processor/importers/ninja/ninja_log_parser.h
@@ -21,9 +21,10 @@
 
 #include <map>
 #include <string>
+#include <vector>
 
-#include "src/trace_processor/chunked_trace_reader.h"
-#include "src/trace_processor/trace_parser.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
 
 namespace perfetto {
 namespace trace_processor {
diff --git a/src/trace_processor/importers/proto/args_table_utils.cc b/src/trace_processor/importers/proto/args_table_utils.cc
deleted file mode 100644
index 391252d..0000000
--- a/src/trace_processor/importers/proto/args_table_utils.cc
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/proto/args_table_utils.h"
-#include "protos/perfetto/common/descriptor.pbzero.h"
-#include "src/trace_processor/util/descriptors.h"
-#include "src/trace_processor/util/status_macros.h"
-
-#include "protos/perfetto/common/descriptor.pbzero.h"
-#include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
-#include "protos/perfetto/trace/track_event/chrome_compositor_scheduler_state.pbzero.h"
-#include "protos/perfetto/trace/track_event/source_location.pbzero.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-ProtoToArgsTable::ProtoToArgsTable(TraceProcessorContext* context)
-    : context_(context) {
-  constexpr int kDefaultSize = 64;
-  key_prefix_.reserve(kDefaultSize);
-  flat_key_prefix_.reserve(kDefaultSize);
-}
-
-util::Status ProtoToArgsTable::AddProtoFileDescriptor(
-    const uint8_t* proto_descriptor_array,
-    size_t proto_descriptor_array_size,
-    bool merge_existing_messages) {
-  return pool_.AddFromFileDescriptorSet(proto_descriptor_array,
-                                        proto_descriptor_array_size,
-                                        merge_existing_messages);
-}
-
-util::Status ProtoToArgsTable::InternProtoFieldsIntoArgsTable(
-    const protozero::ConstBytes& cb,
-    const std::string& type,
-    const std::vector<uint16_t>* fields,
-    ArgsTracker::BoundInserter* inserter,
-    PacketSequenceStateGeneration* sequence_state) {
-  auto idx = pool_.FindDescriptorIdx(type);
-  if (!idx) {
-    return util::Status("Failed to find proto descriptor");
-  }
-
-  auto& descriptor = pool_.descriptors()[*idx];
-
-  std::unordered_map<size_t, int> repeated_field_index;
-
-  protozero::ProtoDecoder decoder(cb);
-  for (protozero::Field f = decoder.ReadField(); f.valid();
-       f = decoder.ReadField()) {
-    auto field = descriptor.FindFieldByTag(f.id());
-    if (!field) {
-      // Unknown field, possibly an unknown extension.
-      continue;
-    }
-
-    // If allowlist is not provided, reflect all fields. Otherwise, check if the
-    // current field either an extension or is in allowlist.
-    bool is_allowed =
-        field->is_extension() || !fields ||
-        std::find(fields->begin(), fields->end(), f.id()) != fields->end();
-
-    if (!is_allowed) {
-      // Field is neither an extension, nor is allowed to be
-      // reflected.
-      continue;
-    }
-    ParsingOverrideState state{context_, sequence_state};
-    RETURN_IF_ERROR(InternFieldIntoArgsTable(
-        *field, repeated_field_index[f.id()], state, inserter, f));
-    if (field->is_repeated()) {
-      repeated_field_index[f.id()]++;
-    }
-  }
-
-  return util::OkStatus();
-}
-
-util::Status ProtoToArgsTable::InternFieldIntoArgsTable(
-    const FieldDescriptor& field_descriptor,
-    int repeated_field_number,
-    ParsingOverrideState state,
-    ArgsTracker::BoundInserter* inserter,
-    protozero::Field field) {
-  std::string prefix_part = field_descriptor.name();
-  if (field_descriptor.is_repeated()) {
-    std::string number = std::to_string(repeated_field_number);
-    prefix_part.reserve(prefix_part.length() + number.length() + 2);
-    prefix_part.append("[");
-    prefix_part.append(number);
-    prefix_part.append("]");
-  }
-
-  // In the args table we build up message1.message2.field1 as the column
-  // name. This will append the ".field1" suffix to |key_prefix| and then
-  // remove it when it goes out of scope.
-  ScopedStringAppender scoped_prefix(prefix_part, &key_prefix_);
-  ScopedStringAppender scoped_flat_key_prefix(field_descriptor.name(),
-                                              &flat_key_prefix_);
-
-  // If we have an override parser then use that instead and move onto the
-  // next loop.
-  auto it = FindOverride(key_prefix_);
-  if (it != overrides_.end()) {
-    if (it->second(state, field, inserter)) {
-      return util::OkStatus();
-    }
-  }
-
-  // If this is not a message we can just immediately add the column name and
-  // get the value out of |field|. However if it is a message we need to
-  // recurse into it.
-  if (field_descriptor.type() ==
-      protos::pbzero::FieldDescriptorProto::TYPE_MESSAGE) {
-    return InternProtoIntoArgsTableInternal(
-        field.as_bytes(), field_descriptor.resolved_type_name(), inserter,
-        state);
-  }
-
-  const StringId key_id =
-      state.context->storage->InternString(base::StringView(key_prefix_));
-  const StringId flat_key_id =
-      state.context->storage->InternString(base::StringView(flat_key_prefix_));
-  inserter->AddArg(flat_key_id, key_id,
-                   ConvertProtoTypeToVariadic(field_descriptor, field, state));
-  return util::OkStatus();
-}
-
-util::Status ProtoToArgsTable::InternProtoIntoArgsTableInternal(
-    const protozero::ConstBytes& cb,
-    const std::string& type,
-    ArgsTracker::BoundInserter* inserter,
-    ParsingOverrideState state) {
-  // Given |type| field the proto descriptor for this proto message.
-  auto opt_proto_descriptor_idx = pool_.FindDescriptorIdx(type);
-  if (!opt_proto_descriptor_idx) {
-    return util::Status("Failed to find proto descriptor");
-  }
-  auto proto_descriptor = pool_.descriptors()[*opt_proto_descriptor_idx];
-
-  // For repeated fields, contains mapping from numeric field ID to
-  // current count of how many values have been serialized with this field.
-  std::unordered_map<size_t, int> repeated_field_index;
-
-  // Parse this message field by field until there are no bytes left.
-  protozero::ProtoDecoder decoder(cb.data, cb.size);
-  for (auto field = decoder.ReadField(); field.valid();
-       field = decoder.ReadField()) {
-    auto field_descriptor = proto_descriptor.FindFieldByTag(field.id());
-    if (!field_descriptor) {
-      // Since the binary descriptor is compiled in it is possible we're seeing
-      // a new message that our descriptors don't have. Just skip the field.
-      continue;
-    }
-    InternFieldIntoArgsTable(*field_descriptor,
-                             repeated_field_index[field.id()], state, inserter,
-                             field);
-    if (field_descriptor->is_repeated()) {
-      repeated_field_index[field.id()]++;
-    }
-  }
-  PERFETTO_DCHECK(decoder.bytes_left() == 0);
-  return util::OkStatus();
-}
-
-void ProtoToArgsTable::AddParsingOverride(std::string field,
-                                          ParsingOverride func) {
-  overrides_.emplace_back(std::move(field), func);
-}
-
-ProtoToArgsTable::OverrideIterator ProtoToArgsTable::FindOverride(
-    const std::string& field) {
-  return std::find_if(
-      overrides_.begin(), overrides_.end(),
-      [&field](const std::pair<std::string, ParsingOverride>& overrides) {
-        return overrides.first == field;
-      });
-}
-
-Variadic ProtoToArgsTable::ConvertProtoTypeToVariadic(
-    const FieldDescriptor& descriptor,
-    const protozero::Field& field,
-    ParsingOverrideState state) {
-  using FieldDescriptorProto = protos::pbzero::FieldDescriptorProto;
-  switch (descriptor.type()) {
-    case FieldDescriptorProto::TYPE_INT32:
-    case FieldDescriptorProto::TYPE_SFIXED32:
-    case FieldDescriptorProto::TYPE_FIXED32:
-      return Variadic::Integer(field.as_int32());
-    case FieldDescriptorProto::TYPE_SINT32:
-      return Variadic::Integer(field.as_sint32());
-    case FieldDescriptorProto::TYPE_INT64:
-    case FieldDescriptorProto::TYPE_SFIXED64:
-    case FieldDescriptorProto::TYPE_FIXED64:
-      return Variadic::Integer(field.as_int64());
-    case FieldDescriptorProto::TYPE_SINT64:
-      return Variadic::Integer(field.as_sint64());
-    case FieldDescriptorProto::TYPE_UINT32:
-      return Variadic::UnsignedInteger(field.as_uint32());
-    case FieldDescriptorProto::TYPE_UINT64:
-      return Variadic::UnsignedInteger(field.as_uint64());
-    case FieldDescriptorProto::TYPE_BOOL:
-      return Variadic::Boolean(field.as_bool());
-    case FieldDescriptorProto::TYPE_DOUBLE:
-      return Variadic::Real(field.as_double());
-    case FieldDescriptorProto::TYPE_FLOAT:
-      return Variadic::Real(static_cast<double>(field.as_float()));
-    case FieldDescriptorProto::TYPE_STRING:
-      return Variadic::String(
-          state.context->storage->InternString(field.as_string()));
-    case FieldDescriptorProto::TYPE_ENUM: {
-      auto opt_enum_descriptor_idx =
-          pool_.FindDescriptorIdx(descriptor.resolved_type_name());
-      if (!opt_enum_descriptor_idx) {
-        // Fall back to the integer representation of the field.
-        return Variadic::Integer(field.as_int32());
-      }
-      auto opt_enum_string =
-          pool_.descriptors()[*opt_enum_descriptor_idx].FindEnumString(
-              field.as_int32());
-      if (!opt_enum_string) {
-        // Fall back to the integer representation of the field.
-        return Variadic::Integer(field.as_int32());
-      }
-      return Variadic::String(state.context->storage->InternString(
-          base::StringView(*opt_enum_string)));
-    }
-    default: {
-      PERFETTO_FATAL(
-          "Tried to write value of type field %s (in proto type "
-          "%s) which has type enum %d",
-          descriptor.name().c_str(), descriptor.resolved_type_name().c_str(),
-          descriptor.type());
-    }
-  }
-  return Variadic{};
-}
-
-ProtoToArgsTable::ScopedStringAppender::ScopedStringAppender(
-    const std::string& append,
-    std::string* dest)
-    : old_size_(dest->size()), str_(dest) {
-  if (dest->empty()) {
-    str_->reserve(append.size());
-  } else {
-    str_->reserve(old_size_ + 1 + append.size());
-    str_->append(".");
-  }
-  str_->append(append);
-}
-
-ProtoToArgsTable::ScopedStringAppender::~ScopedStringAppender() {
-  str_->erase(old_size_);
-}
-
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/args_table_utils.h b/src/trace_processor/importers/proto/args_table_utils.h
deleted file mode 100644
index 10b6cd1..0000000
--- a/src/trace_processor/importers/proto/args_table_utils.h
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_ARGS_TABLE_UTILS_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_ARGS_TABLE_UTILS_H_
-
-#include "perfetto/protozero/proto_decoder.h"
-#include "perfetto/trace_processor/status.h"
-#include "src/trace_processor/importers/common/args_tracker.h"
-#include "src/trace_processor/importers/proto/packet_sequence_state.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/util/descriptors.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-// ProtoToArgsTable encapsulates the process of taking an arbitrary proto and
-// associating each field as a column in an args set. This is done by traversing
-// the proto using reflection (with descriptors provided by
-// AddProtoFileDescriptor()) and creating column names equal to this traversal.
-//
-// I.E. given a proto like
-//
-// package perfetto.protos;
-// message SubMessage {
-//   optional int32 field = 1;
-// }
-// message MainMessage {
-//   optional int32 field1 = 1;
-//   optional string field2 = 2;
-//   optional SubMessage field3 = 3;
-// }
-//
-// We will get the args set columns "field1", "field2", "field3.field" and will
-// store the values found inside as the result.
-//
-// Usage of this is as follows:
-//
-// ProtoToArgsTable helper( /* provide current parsing state */ ...);
-// helper.AddProtoFileDescriptor(
-//     /* provide descriptor generated by tools/gen_binary_descriptors */);
-// helper.InternProtoIntoArgs(const_bytes, ".perfetto.protos.MainMessage",
-// row_id);
-//
-// Optionally one can handle particular fields as well by providing a
-// ParsingOverride through AddParsingOverride.
-//
-// helper.AddParsingOverride("field3.field",
-// [](const ProtoToArgsTable::ParsingOverrideState& state,
-//    const protozero::Field& field) {
-//   if (!should_handle()) {
-//     return false;
-//   }
-//   // Parse |field| and add any rows for it to args using |state|.
-//   return true;
-// });
-class ProtoToArgsTable {
- public:
-  struct ParsingOverrideState {
-    TraceProcessorContext* context;
-    PacketSequenceStateGeneration* sequence_state;
-  };
-  using ParsingOverride = bool (*)(const ParsingOverrideState& state,
-                                   const protozero::Field&,
-                                   ArgsTracker::BoundInserter* inserter);
-
-  // ScopedStringAppender will add |append| to |dest| when constructed and
-  // erases the appended suffix from |dest| when it goes out of scope. Thus
-  // |dest| must be valid for the entire lifetime of ScopedStringAppender.
-  //
-  // This is useful as we descend into a proto since the column names just
-  // appended with ".field_name" as we go lower.
-  //
-  // I.E. message1.message2.field_name1 is a column, but we'll then need to
-  // append message1.message2.field_name2 afterwards so we only need to append
-  // "field_name1" within some scope. This is public so people implementing a
-  // ParsingOverride can follow this same behaviour.
-  class ScopedStringAppender {
-   public:
-    ScopedStringAppender(const std::string& append, std::string* dest);
-    ~ScopedStringAppender();
-
-   private:
-    size_t old_size_;
-    std::string* str_;
-  };
-
-  // |context| provides access to storage.
-  explicit ProtoToArgsTable(TraceProcessorContext* context);
-
-  // Adds a compile time reflection of a set of proto files. You must provide
-  // the descriptor before attempting to parse this with
-  // InternProtoIntoArgsTable().
-  //
-  // To generate |proto_descriptor_array| please see
-  // tools/gen_binary_descriptors and ensure the proto you are interested in is
-  // listed in the event_list file. You can then find your variable inside the
-  // header location specified inside that python script.
-  util::Status AddProtoFileDescriptor(const uint8_t* proto_descriptor_array,
-                                      size_t proto_descriptor_array_size,
-                                      bool merge_existing_messages = false);
-
-  // Given a view of bytes that represent a serialized protozero message of
-  // |type| we will parse each field into the Args table using RowId |row|.
-  //
-  // Returns on any error with a status describing the problem. However any
-  // added values before encountering the error will be added to the
-  // args_tracker.
-  //
-  // Fields with ids given in |fields| are parsed using reflection, as well
-  // as known (previously registered) extension fields. If |fields| is a
-  // nullptr, all fields are going to be parsed.
-  //
-  // Note:
-  // |type| must be the fully qualified name, but with a '.' added to the
-  // beginning. I.E. ".perfetto.protos.TrackEvent". And must match one of the
-  // descriptors already added through |AddProtoFileDescriptor|.
-  //
-  // IMPORTANT: currently bytes fields are not supported.
-  //
-  // TODO(b/145578432): Add support for byte fields.
-  util::Status InternProtoFieldsIntoArgsTable(
-      const protozero::ConstBytes& cb,
-      const std::string& type,
-      const std::vector<uint16_t>* fields,
-      ArgsTracker::BoundInserter* inserter,
-      PacketSequenceStateGeneration* sequence_state);
-
-  // Installs an override for the field at the specified path. We will invoke
-  // |parsing_override| when the field is encountered.
-  //
-  // If |parsing_override| returns false we will continue and fallback on
-  // default behaviour. However if it returns true we will assume that it added
-  // all required messages to the args_tracker.
-  //
-  // Note |field_path| must be the full path separated by periods. I.E. in the
-  // proto
-  //
-  // message SubMessage {
-  //   optional int32 field = 1;
-  // }
-  // message MainMessage {
-  //   optional SubMessage field1 = 1;
-  //   optional SubMessage field2 = 2;
-  // }
-  //
-  // To override the handling of both SubMessage fields you must add two parsing
-  // overrides. One with a |field_path| == "field1.field" and another with
-  // "field2.field".
-  void AddParsingOverride(std::string field_path,
-                          ParsingOverride parsing_override);
-
- private:
-  util::Status InternProtoIntoArgsTableInternal(
-      const protozero::ConstBytes& cb,
-      const std::string& type,
-      ArgsTracker::BoundInserter* inserter,
-      ParsingOverrideState state);
-
-  util::Status InternFieldIntoArgsTable(const FieldDescriptor& field_descriptor,
-                                        int repeated_field_number,
-                                        ParsingOverrideState state,
-                                        ArgsTracker::BoundInserter* inserter,
-                                        protozero::Field field);
-
-  using OverrideIterator =
-      std::vector<std::pair<std::string, ParsingOverride>>::iterator;
-  OverrideIterator FindOverride(const std::string& field);
-
-  Variadic ConvertProtoTypeToVariadic(const FieldDescriptor& descriptor,
-                                      const protozero::Field& field,
-                                      ParsingOverrideState state);
-
-  std::vector<std::pair<std::string, ParsingOverride>> overrides_;
-  DescriptorPool pool_;
-  std::string key_prefix_;
-  std::string flat_key_prefix_;
-  TraceProcessorContext* context_;
-};
-
-}  // namespace trace_processor
-}  // namespace perfetto
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_ARGS_TABLE_UTILS_H_
diff --git a/src/trace_processor/importers/proto/args_table_utils_unittest.cc b/src/trace_processor/importers/proto/args_table_utils_unittest.cc
deleted file mode 100644
index acf893e..0000000
--- a/src/trace_processor/importers/proto/args_table_utils_unittest.cc
+++ /dev/null
@@ -1,412 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/proto/args_table_utils.h"
-
-#include "perfetto/ext/base/string_view.h"
-#include "perfetto/protozero/scattered_heap_buffer.h"
-#include "protos/perfetto/common/descriptor.pbzero.h"
-#include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
-#include "protos/perfetto/trace/track_event/source_location.pbzero.h"
-#include "src/protozero/test/example_proto/test_messages.pbzero.h"
-#include "src/trace_processor/importers/common/args_tracker.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/test_messages.descriptor.h"
-#include "test/gtest_and_gmock.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace {
-
-constexpr size_t kChunkSize = 42;
-
-using ::testing::_;
-using ::testing::Eq;
-using ::testing::Invoke;
-using ::testing::NiceMock;
-
-class ArgsTableUtilsTest : public ::testing::Test {
- protected:
-  ArgsTableUtilsTest() {
-    context_.storage.reset(new TraceStorage);
-    storage_ = context_.storage.get();
-    context_.global_args_tracker.reset(new GlobalArgsTracker(&context_));
-    context_.args_tracker.reset(new ArgsTracker(&context_));
-    sequence_state_.reset(new PacketSequenceState(&context_));
-  }
-
-  /**
-   * Check whether the argument set contains the value with given flat_key and
-   * key and is equal to the given value.
-   */
-  bool HasArg(ArgSetId set_id,
-              const base::StringView& flat_key,
-              const base::StringView& key,
-              Variadic value) {
-    const auto& args = storage_->arg_table();
-    auto key_id = storage_->string_pool().GetId(key);
-    EXPECT_TRUE(key_id);
-    auto flat_key_id = storage_->string_pool().GetId(flat_key);
-    EXPECT_TRUE(flat_key_id);
-
-    RowMap rm = args.FilterToRowMap({args.arg_set_id().eq(set_id)});
-    bool found = false;
-    for (auto it = rm.IterateRows(); it; it.Next()) {
-      if (args.key()[it.row()] == key_id) {
-        EXPECT_EQ(args.flat_key()[it.row()], flat_key_id);
-        if (storage_->GetArgValue(it.row()) == value) {
-          found = true;
-          break;
-        }
-      }
-    }
-    return found;
-  }
-
-  /**
-   * Implementation of HasArg for a simple case when flat_key is equals to key,
-   * so that two won't have to be repeated for each assertion.
-   */
-  bool HasArg(ArgSetId set_id, const base::StringView& key, Variadic value) {
-    return HasArg(set_id, key, key, value);
-  }
-
-  uint32_t arg_set_id_ = 1;
-  std::unique_ptr<PacketSequenceState> sequence_state_;
-  TraceProcessorContext context_;
-  TraceStorage* storage_;
-};
-
-TEST_F(ArgsTableUtilsTest, EnsureTestMessageProtoParses) {
-  ProtoToArgsTable helper(&context_);
-  auto status = helper.AddProtoFileDescriptor(kTestMessagesDescriptor.data(),
-                                              kTestMessagesDescriptor.size());
-  EXPECT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
-                           << status.message();
-}
-
-TEST_F(ArgsTableUtilsTest, BasicSingleLayerProto) {
-  using namespace protozero::test::protos::pbzero;
-  protozero::HeapBuffered<EveryField> msg{kChunkSize, kChunkSize};
-  msg->set_field_int32(-1);
-  msg->set_field_int64(-333123456789ll);
-  msg->set_field_uint32(600);
-  msg->set_field_uint64(333123456789ll);
-  msg->set_field_sint32(-5);
-  msg->set_field_sint64(-9000);
-  msg->set_field_fixed32(12345);
-  msg->set_field_fixed64(444123450000ll);
-  msg->set_field_sfixed32(-69999);
-  msg->set_field_sfixed64(-200);
-  msg->set_field_double(0.5555);
-  msg->set_field_bool(true);
-  msg->set_small_enum(SmallEnum::TO_BE);
-  msg->set_signed_enum(SignedEnum::NEGATIVE);
-  msg->set_big_enum(BigEnum::BEGIN);
-  msg->set_nested_enum(EveryField::PONG);
-  msg->set_field_float(3.14f);
-  msg->set_field_string("FizzBuzz");
-  msg->add_repeated_int32(1);
-  msg->add_repeated_int32(-1);
-  msg->add_repeated_int32(100);
-  msg->add_repeated_int32(2000000);
-
-  auto binary_proto = msg.SerializeAsArray();
-
-  storage_->mutable_track_table()->Insert({});
-  ProtoToArgsTable helper(&context_);
-  auto status = helper.AddProtoFileDescriptor(kTestMessagesDescriptor.data(),
-                                              kTestMessagesDescriptor.size());
-  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
-                           << status.message();
-
-  auto inserter = context_.args_tracker->AddArgsTo(TrackId(0));
-  status = helper.InternProtoFieldsIntoArgsTable(
-      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
-      ".protozero.test.protos.EveryField", nullptr, &inserter,
-      sequence_state_->current_generation().get());
-
-  EXPECT_TRUE(status.ok())
-      << "InternProtoFieldsIntoArgsTable failed with error: "
-      << status.message();
-
-  context_.args_tracker->Flush();
-  EXPECT_TRUE(
-      HasArg(ArgSetId(arg_set_id_), "field_int32", Variadic::Integer(-1)));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "field_int64",
-                     Variadic::Integer(-333123456789ll)));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "field_uint32",
-                     Variadic::UnsignedInteger(600)));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "field_uint64",
-                     Variadic::UnsignedInteger(333123456789ll)));
-  EXPECT_TRUE(
-      HasArg(ArgSetId(arg_set_id_), "field_sint32", Variadic::Integer(-5)));
-  EXPECT_TRUE(
-      HasArg(ArgSetId(arg_set_id_), "field_sint64", Variadic::Integer(-9000)));
-  EXPECT_TRUE(
-      HasArg(ArgSetId(arg_set_id_), "field_fixed32", Variadic::Integer(12345)));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "field_fixed64",
-                     Variadic::Integer(444123450000ll)));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "field_sfixed32",
-                     Variadic::Integer(-69999)));
-  EXPECT_TRUE(
-      HasArg(ArgSetId(arg_set_id_), "field_sfixed64", Variadic::Integer(-200)));
-  EXPECT_TRUE(
-      HasArg(ArgSetId(arg_set_id_), "field_double", Variadic::Real(0.5555)));
-  EXPECT_TRUE(
-      HasArg(ArgSetId(arg_set_id_), "field_bool", Variadic::Boolean(true)));
-  EXPECT_TRUE(HasArg(
-      ArgSetId(arg_set_id_), "small_enum",
-      Variadic::String(*context_.storage->string_pool().GetId("TO_BE"))));
-  EXPECT_TRUE(HasArg(
-      ArgSetId(arg_set_id_), "signed_enum",
-      Variadic::String(*context_.storage->string_pool().GetId("NEGATIVE"))));
-  EXPECT_TRUE(HasArg(
-      ArgSetId(arg_set_id_), "big_enum",
-      Variadic::String(*context_.storage->string_pool().GetId("BEGIN"))));
-  EXPECT_TRUE(
-      HasArg(ArgSetId(arg_set_id_), "nested_enum",
-             Variadic::String(*context_.storage->string_pool().GetId("PONG"))));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "field_float",
-                     Variadic::Real(static_cast<double>(3.14f))));
-  ASSERT_TRUE(context_.storage->string_pool().GetId("FizzBuzz"));
-  EXPECT_TRUE(HasArg(
-      ArgSetId(arg_set_id_), "field_string",
-      Variadic::String(*context_.storage->string_pool().GetId("FizzBuzz"))));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "repeated_int32",
-                     "repeated_int32[0]", Variadic::Integer(1)));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "repeated_int32",
-                     "repeated_int32[1]", Variadic::Integer(-1)));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "repeated_int32",
-                     "repeated_int32[2]", Variadic::Integer(100)));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "repeated_int32",
-                     "repeated_int32[3]", Variadic::Integer(2000000)));
-}
-
-TEST_F(ArgsTableUtilsTest, NestedProto) {
-  using namespace protozero::test::protos::pbzero;
-  protozero::HeapBuffered<NestedA> msg{kChunkSize, kChunkSize};
-  msg->set_super_nested()->set_value_c(3);
-
-  auto binary_proto = msg.SerializeAsArray();
-
-  storage_->mutable_track_table()->Insert({});
-  ProtoToArgsTable helper(&context_);
-  auto status = helper.AddProtoFileDescriptor(kTestMessagesDescriptor.data(),
-                                              kTestMessagesDescriptor.size());
-  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
-                           << status.message();
-
-  auto inserter = context_.args_tracker->AddArgsTo(TrackId(0));
-  status = helper.InternProtoFieldsIntoArgsTable(
-      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
-      ".protozero.test.protos.NestedA", nullptr, &inserter,
-      sequence_state_->current_generation().get());
-  EXPECT_TRUE(status.ok())
-      << "InternProtoFieldsIntoArgsTable failed with error: "
-      << status.message();
-  context_.args_tracker->Flush();
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "super_nested.value_c",
-                     Variadic::Integer(3)));
-}
-
-TEST_F(ArgsTableUtilsTest, CamelCaseFieldsProto) {
-  using namespace protozero::test::protos::pbzero;
-  protozero::HeapBuffered<CamelCaseFields> msg{kChunkSize, kChunkSize};
-  msg->set_barbaz(true);
-  msg->set_moomoo(true);
-  msg->set___bigbang(true);
-
-  auto binary_proto = msg.SerializeAsArray();
-
-  storage_->mutable_track_table()->Insert({});
-  ProtoToArgsTable helper(&context_);
-  auto status = helper.AddProtoFileDescriptor(kTestMessagesDescriptor.data(),
-                                              kTestMessagesDescriptor.size());
-  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
-                           << status.message();
-
-  auto inserter = context_.args_tracker->AddArgsTo(TrackId(0));
-  status = helper.InternProtoFieldsIntoArgsTable(
-      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
-      ".protozero.test.protos.CamelCaseFields", nullptr, &inserter,
-      sequence_state_->current_generation().get());
-  EXPECT_TRUE(status.ok())
-      << "InternProtoFieldsIntoArgsTable failed with error: "
-      << status.message();
-  context_.args_tracker->Flush();
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "barBaz", Variadic::Boolean(true)));
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "MooMoo", Variadic::Boolean(true)));
-  EXPECT_TRUE(
-      HasArg(ArgSetId(arg_set_id_), "__bigBang", Variadic::Boolean(true)));
-}
-
-TEST_F(ArgsTableUtilsTest, NestedProtoParsingOverrideHandled) {
-  using namespace protozero::test::protos::pbzero;
-  protozero::HeapBuffered<NestedA> msg{kChunkSize, kChunkSize};
-  msg->set_super_nested()->set_value_c(3);
-
-  auto binary_proto = msg.SerializeAsArray();
-
-  storage_->mutable_track_table()->Insert({});
-  ProtoToArgsTable helper(&context_);
-  auto status = helper.AddProtoFileDescriptor(kTestMessagesDescriptor.data(),
-                                              kTestMessagesDescriptor.size());
-  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
-                           << status.message();
-
-  helper.AddParsingOverride(
-      "super_nested.value_c",
-      [](const ProtoToArgsTable::ParsingOverrideState& state,
-         const protozero::Field& field, ArgsTracker::BoundInserter* inserter) {
-        EXPECT_EQ(field.type(), protozero::proto_utils::ProtoWireType::kVarInt);
-        EXPECT_TRUE(state.context);
-        EXPECT_TRUE(state.sequence_state);
-        auto id = state.context->storage->InternString(
-            "super_nested.value_b.replaced");
-        int32_t val = field.as_int32();
-        inserter->AddArg(id, id, Variadic::Integer(val + 1));
-        // We've handled this field by adding the desired args.
-        return true;
-      });
-
-  auto inserter = context_.args_tracker->AddArgsTo(TrackId(0));
-  status = helper.InternProtoFieldsIntoArgsTable(
-      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
-      ".protozero.test.protos.NestedA", nullptr, &inserter,
-      sequence_state_->current_generation().get());
-  EXPECT_TRUE(status.ok())
-      << "InternProtoFieldsIntoArgsTable failed with error: "
-      << status.message();
-  context_.args_tracker->Flush();
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "super_nested.value_b.replaced",
-                     Variadic::Integer(4)));
-}
-
-TEST_F(ArgsTableUtilsTest, NestedProtoParsingOverrideSkipped) {
-  using namespace protozero::test::protos::pbzero;
-  protozero::HeapBuffered<NestedA> msg{kChunkSize, kChunkSize};
-  msg->set_super_nested()->set_value_c(3);
-
-  auto binary_proto = msg.SerializeAsArray();
-
-  storage_->mutable_track_table()->Insert({});
-  ProtoToArgsTable helper(&context_);
-  auto status = helper.AddProtoFileDescriptor(kTestMessagesDescriptor.data(),
-                                              kTestMessagesDescriptor.size());
-  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
-                           << status.message();
-
-  helper.AddParsingOverride(
-      "super_nested.value_c",
-      [](const ProtoToArgsTable::ParsingOverrideState& state,
-         const protozero::Field& field, ArgsTracker::BoundInserter*) {
-        static int val = 0;
-        ++val;
-        EXPECT_EQ(1, val);
-        EXPECT_EQ(field.type(), protozero::proto_utils::ProtoWireType::kVarInt);
-        EXPECT_TRUE(state.sequence_state);
-        EXPECT_TRUE(state.context);
-        // By returning false we expect this field to be handled like regular.
-        return false;
-      });
-
-  auto inserter = context_.args_tracker->AddArgsTo(TrackId(0));
-  status = helper.InternProtoFieldsIntoArgsTable(
-      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
-      ".protozero.test.protos.NestedA", nullptr, &inserter,
-      sequence_state_->current_generation().get());
-  EXPECT_TRUE(status.ok())
-      << "InternProtoFieldsIntoArgsTable failed with error: "
-      << status.message();
-  context_.args_tracker->Flush();
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "super_nested.value_c",
-                     Variadic::Integer(3)));
-}
-
-TEST_F(ArgsTableUtilsTest, LookingUpInternedStateParsingOverride) {
-  using namespace protozero::test::protos::pbzero;
-  // The test proto, we will use |value_c| as the source_location iid.
-  protozero::HeapBuffered<NestedA> msg{kChunkSize, kChunkSize};
-  msg->set_super_nested()->set_value_c(3);
-  auto binary_proto = msg.SerializeAsArray();
-
-  // The interned source location.
-  protozero::HeapBuffered<protos::pbzero::SourceLocation> src_loc{kChunkSize,
-                                                                  kChunkSize};
-  src_loc->set_iid(3);
-  src_loc->set_file_name("test_file_name");
-  // We need to update sequence_state to point to it.
-  auto binary_data = src_loc.SerializeAsArray();
-  std::unique_ptr<uint8_t[]> buffer(new uint8_t[binary_data.size()]);
-  for (size_t i = 0; i < binary_data.size(); ++i) {
-    buffer.get()[i] = binary_data[i];
-  }
-  TraceBlobView blob(std::move(buffer), 0, binary_data.size());
-  sequence_state_->InternMessage(
-      protos::pbzero::InternedData::kSourceLocationsFieldNumber,
-      std::move(blob));
-
-  ProtoToArgsTable helper(&context_);
-  // Now we override the behaviour of |value_c| so we can expand the iid into
-  // multiple args rows.
-  helper.AddParsingOverride(
-      "super_nested.value_c",
-      [](const ProtoToArgsTable::ParsingOverrideState& state,
-         const protozero::Field& field, ArgsTracker::BoundInserter* inserter) {
-        EXPECT_EQ(field.type(), protozero::proto_utils::ProtoWireType::kVarInt);
-        auto* decoder = state.sequence_state->LookupInternedMessage<
-            protos::pbzero::InternedData::kSourceLocationsFieldNumber,
-            protos::pbzero::SourceLocation>(field.as_uint64());
-        if (!decoder) {
-          // Lookup failed fall back on default behaviour.
-          return false;
-        }
-        TraceStorage* storage = state.context->storage.get();
-        auto file_name_id = storage->InternString("file_name");
-        auto line_number_id = storage->InternString("line_number");
-        auto file_id = storage->InternString(decoder->file_name());
-        inserter->AddArg(file_name_id, file_name_id, Variadic::String(file_id));
-        inserter->AddArg(line_number_id, line_number_id, Variadic::Integer(2));
-        return true;
-      });
-
-  storage_->mutable_track_table()->Insert({});
-  auto status = helper.AddProtoFileDescriptor(kTestMessagesDescriptor.data(),
-                                              kTestMessagesDescriptor.size());
-  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
-                           << status.message();
-
-  auto inserter = context_.args_tracker->AddArgsTo(TrackId(0));
-  status = helper.InternProtoFieldsIntoArgsTable(
-      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
-      ".protozero.test.protos.NestedA", nullptr, &inserter,
-      sequence_state_->current_generation().get());
-  EXPECT_TRUE(status.ok())
-      << "InternProtoFieldsIntoArgsTable failed with error: "
-      << status.message();
-  auto file_name_id = storage_->string_pool().GetId("test_file_name");
-  ASSERT_TRUE(file_name_id);
-  context_.args_tracker->Flush();
-  EXPECT_TRUE(HasArg(ArgSetId(arg_set_id_), "file_name",
-                     Variadic::String(*file_name_id)));
-  EXPECT_TRUE(
-      HasArg(ArgSetId(arg_set_id_), "line_number", Variadic::Integer(2)));
-}
-
-}  // namespace
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/chrome_string_lookup.h b/src/trace_processor/importers/proto/chrome_string_lookup.h
index de7e9e6..23f8eca 100644
--- a/src/trace_processor/importers/proto/chrome_string_lookup.h
+++ b/src/trace_processor/importers/proto/chrome_string_lookup.h
@@ -32,7 +32,7 @@
   explicit ChromeStringLookup(TraceStorage* storage);
 
   StringId GetProcessName(int32_t process_type) const;
-  StringId GetThreadName(int32_t index) const;
+  StringId GetThreadName(int32_t thread_type) const;
 
  public:
   std::map<int32_t /* ChromeProcessDescriptor::ProcessType */, StringId>
diff --git a/src/trace_processor/importers/proto/chrome_system_probes_parser.h b/src/trace_processor/importers/proto/chrome_system_probes_parser.h
index 2e86b6b..8c65fd4 100644
--- a/src/trace_processor/importers/proto/chrome_system_probes_parser.h
+++ b/src/trace_processor/importers/proto/chrome_system_probes_parser.h
@@ -33,7 +33,7 @@
 
   explicit ChromeSystemProbesParser(TraceProcessorContext*);
 
-  void ParseProcessStats(int64_t timestamp, ConstBytes);
+  void ParseProcessStats(int64_t ts, ConstBytes);
 
  private:
   TraceProcessorContext* const context_;
diff --git a/src/trace_processor/importers/proto/frame_timeline_event_parser.cc b/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
index 32c862c..f52931f 100644
--- a/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
+++ b/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
@@ -32,6 +32,17 @@
 
 namespace perfetto {
 namespace trace_processor {
+namespace {
+
+bool IsBadTimestamp(int64_t ts) {
+  // Very small or very large timestamps are likely a mistake.
+  // See b/185978397
+  constexpr int64_t kBadTimestamp =
+      std::numeric_limits<int64_t>::max() - (10LL * 1000 * 1000 * 1000);
+  return std::abs(ts) >= kBadTimestamp;
+}
+
+}  // namespace
 
 using ExpectedDisplayFrameStartDecoder =
     protos::pbzero::FrameTimelineEvent_ExpectedDisplayFrameStart_Decoder;
@@ -69,6 +80,8 @@
     jank_reasons.emplace_back("Buffer Stuffing");
   if (jank_type & FrameTimelineEvent::JANK_UNKNOWN)
     jank_reasons.emplace_back("Unknown Jank");
+  if (jank_type & FrameTimelineEvent::JANK_SF_STUFFING)
+    jank_reasons.emplace_back("SurfaceFlinger Stuffing");
 
   std::string jank_str(
       std::accumulate(jank_reasons.begin(), jank_reasons.end(), std::string(),
@@ -159,7 +172,9 @@
       jank_tag_other_id_(context->storage->InternString("Other Jank")),
       jank_tag_dropped_id_(context->storage->InternString("Dropped Frame")),
       jank_tag_buffer_stuffing_id_(
-          context->storage->InternString("Buffer Stuffing")) {}
+          context->storage->InternString("Buffer Stuffing")),
+      jank_tag_sf_stuffing_id_(
+          context->storage->InternString("SurfaceFlinger Stuffing")) {}
 
 void FrameTimelineEventParser::ParseExpectedDisplayFrameStart(
     int64_t timestamp,
@@ -268,10 +283,13 @@
         prediction_type_ids_[static_cast<size_t>(event.prediction_type())];
   }
   actual_row.prediction_type = prediction_type;
-  if (DisplayFrameJanky(event.jank_type()))
+  if (DisplayFrameJanky(event.jank_type())) {
     actual_row.jank_tag = jank_tag_self_id_;
-  else
+  } else if (event.jank_type() == FrameTimelineEvent::JANK_SF_STUFFING) {
+    actual_row.jank_tag = jank_tag_sf_stuffing_id_;
+  } else {
     actual_row.jank_tag = jank_tag_none_id_;
+  }
 
   base::Optional<SliceId> opt_slice_id =
       context_->slice_tracker->BeginTyped(
@@ -460,16 +478,18 @@
         prediction_type_ids_[static_cast<size_t>(event.prediction_type())];
   }
   actual_row.prediction_type = prediction_type;
-  if (SurfaceFrameJanky(event.jank_type()))
+  if (SurfaceFrameJanky(event.jank_type())) {
     actual_row.jank_tag = jank_tag_self_id_;
-  else if (DisplayFrameJanky(event.jank_type()))
+  } else if (DisplayFrameJanky(event.jank_type())) {
     actual_row.jank_tag = jank_tag_other_id_;
-  else if (event.jank_type() == FrameTimelineEvent::JANK_BUFFER_STUFFING)
+  } else if (event.jank_type() == FrameTimelineEvent::JANK_BUFFER_STUFFING) {
     actual_row.jank_tag = jank_tag_buffer_stuffing_id_;
-  else if (present_type_validated && event.present_type() == FrameTimelineEvent::PRESENT_DROPPED)
+  } else if (present_type_validated &&
+             event.present_type() == FrameTimelineEvent::PRESENT_DROPPED) {
     actual_row.jank_tag = jank_tag_dropped_id_;
-  else
+  } else {
     actual_row.jank_tag = jank_tag_none_id_;
+  }
   StringId is_buffer = context_->storage->InternString("Unspecified");
   if (event.has_is_buffer()) {
     if (event.is_buffer())
@@ -528,7 +548,13 @@
 void FrameTimelineEventParser::ParseFrameTimelineEvent(int64_t timestamp,
                                                        ConstBytes blob) {
   protos::pbzero::FrameTimelineEvent_Decoder frame_event(blob.data, blob.size);
-  context_->storage->InternString(base::StringView(std::to_string(timestamp)));
+
+  if (IsBadTimestamp(timestamp)) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
   if (frame_event.has_expected_display_frame_start()) {
     ParseExpectedDisplayFrameStart(timestamp,
                                    frame_event.expected_display_frame_start());
diff --git a/src/trace_processor/importers/proto/frame_timeline_event_parser.h b/src/trace_processor/importers/proto/frame_timeline_event_parser.h
index 648ce7b..c4e3ee8 100644
--- a/src/trace_processor/importers/proto/frame_timeline_event_parser.h
+++ b/src/trace_processor/importers/proto/frame_timeline_event_parser.h
@@ -84,6 +84,7 @@
   StringId jank_tag_other_id_;
   StringId jank_tag_dropped_id_;
   StringId jank_tag_buffer_stuffing_id_;
+  StringId jank_tag_sf_stuffing_id_;
 
   // upid -> set of tokens map. The expected timeline is the same for a given
   // token no matter how many times its seen. We can safely ignore duplicates
diff --git a/src/trace_processor/importers/proto/heap_graph_module.cc b/src/trace_processor/importers/proto/heap_graph_module.cc
index 9c7ce47..530c14b 100644
--- a/src/trace_processor/importers/proto/heap_graph_module.cc
+++ b/src/trace_processor/importers/proto/heap_graph_module.cc
@@ -224,10 +224,13 @@
 
     StringPool::Id kind = context_->storage->InternString(
         HeapGraphTypeKindToString(entry.kind()));
+    base::Optional<uint64_t> location_id;
+    if (entry.has_location_id())
+      location_id = entry.location_id();
 
     heap_graph_tracker->AddInternedType(
         seq_id, entry.id(), context_->storage->InternString(str_view),
-        entry.location_id(), entry.object_size(), std::move(field_name_ids),
+        location_id, entry.object_size(), std::move(field_name_ids),
         entry.superclass_id(), entry.classloader_id(), no_fields, kind);
   }
   for (auto it = heap_graph.field_names(); it; ++it) {
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.cc b/src/trace_processor/importers/proto/heap_graph_tracker.cc
index 1e45e52..f4e4f70 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.cc
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.cc
@@ -420,7 +420,7 @@
 void HeapGraphTracker::AddInternedType(uint32_t seq_id,
                                        uint64_t intern_id,
                                        StringPool::Id strid,
-                                       uint64_t location_id,
+                                       base::Optional<uint64_t> location_id,
                                        uint64_t object_size,
                                        std::vector<uint64_t> field_name_ids,
                                        uint64_t superclass_id,
@@ -580,7 +580,7 @@
                   current_type->field_name_ids[field_offset_in_cls++];
               auto it = sequence_state.interned_fields.find(field_id);
               if (it == sequence_state.interned_fields.end()) {
-                PERFETTO_ELOG("Invalid field id.");
+                PERFETTO_DLOG("Invalid field id.");
                 context_->storage->IncrementIndexedStats(
                     stats::heap_graph_malformed_packet,
                     static_cast<int>(sequence_state.current_upid));
@@ -727,7 +727,7 @@
   }
 }
 
-void FindPathFromRoot(const TraceStorage& storage,
+void FindPathFromRoot(TraceStorage* storage,
                       tables::HeapGraphObjectTable::Id id,
                       PathFromRoot* path) {
   // We have long retention chains (e.g. from LinkedList). If we use the stack
@@ -746,7 +746,7 @@
 
   while (!stack.empty()) {
     tables::HeapGraphObjectTable::Id n = stack.back().node;
-    uint32_t row = *storage.heap_graph_object_table().id().IndexOf(n);
+    uint32_t row = *storage->heap_graph_object_table().id().IndexOf(n);
     size_t parent_id = stack.back().parent_id;
     uint32_t depth = stack.back().depth;
     size_t& i = stack.back().i;
@@ -754,16 +754,24 @@
         stack.back().children;
 
     tables::HeapGraphClassTable::Id type_id =
-        storage.heap_graph_object_table().type_id()[row];
+        storage->heap_graph_object_table().type_id()[row];
 
-    uint32_t type_row = *storage.heap_graph_class_table().id().IndexOf(type_id);
+    uint32_t type_row =
+        *storage->heap_graph_class_table().id().IndexOf(type_id);
     base::Optional<StringPool::Id> opt_class_name_id =
-        storage.heap_graph_class_table().deobfuscated_name()[type_row];
+        storage->heap_graph_class_table().deobfuscated_name()[type_row];
     if (!opt_class_name_id) {
-      opt_class_name_id = storage.heap_graph_class_table().name()[type_row];
+      opt_class_name_id = storage->heap_graph_class_table().name()[type_row];
     }
     PERFETTO_CHECK(opt_class_name_id);
     StringPool::Id class_name_id = *opt_class_name_id;
+    base::Optional<StringPool::Id> root_type =
+        storage->heap_graph_object_table().root_type()[row];
+    if (root_type) {
+      class_name_id = storage->InternString(base::StringView(
+          storage->GetString(class_name_id).ToStdString() + " [" +
+          storage->GetString(*root_type).ToStdString() + "]"));
+    }
     auto it = path->nodes[parent_id].children.find(class_name_id);
     if (it == path->nodes[parent_id].children.end()) {
       size_t path_id = path->nodes.size();
@@ -781,10 +789,10 @@
       // This is the first time we are looking at this node, so add its
       // size to the relevant node in the resulting tree.
       output_tree_node->size +=
-          storage.heap_graph_object_table().self_size()[row];
+          storage->heap_graph_object_table().self_size()[row];
       output_tree_node->count++;
       std::set<tables::HeapGraphObjectTable::Id> children_set =
-          GetChildren(storage, n);
+          GetChildren(*storage, n);
       children.assign(children_set.cbegin(), children_set.cend());
       PERFETTO_CHECK(children.size() == children_set.size());
     }
@@ -794,14 +802,14 @@
       PERFETTO_CHECK(i < children.size());
       tables::HeapGraphObjectTable::Id child = children[i];
       uint32_t child_row =
-          *storage.heap_graph_object_table().id().IndexOf(child);
+          *storage->heap_graph_object_table().id().IndexOf(child);
       if (++i == children.size())
         stack.pop_back();
 
       int32_t child_distance =
-          storage.heap_graph_object_table().root_distance()[child_row];
+          storage->heap_graph_object_table().root_distance()[child_row];
       int32_t n_distance =
-          storage.heap_graph_object_table().root_distance()[row];
+          storage->heap_graph_object_table().root_distance()[row];
       PERFETTO_CHECK(n_distance >= 0);
       PERFETTO_CHECK(child_distance >= 0);
 
@@ -856,7 +864,7 @@
 
   PathFromRoot init_path;
   for (tables::HeapGraphObjectTable::Id root : roots) {
-    FindPathFromRoot(*context_->storage, root, &init_path);
+    FindPathFromRoot(context_->storage.get(), root, &init_path);
   }
 
   std::vector<int32_t> node_to_cumulative_size(init_path.nodes.size());
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.h b/src/trace_processor/importers/proto/heap_graph_tracker.h
index 8aba58b..1cf31e2 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.h
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.h
@@ -58,7 +58,7 @@
 void MarkRoot(TraceStorage* s,
               tables::HeapGraphObjectTable::Id id,
               StringPool::Id type);
-void FindPathFromRoot(const TraceStorage& s,
+void FindPathFromRoot(TraceStorage* storage,
                       tables::HeapGraphObjectTable::Id id,
                       PathFromRoot* path);
 
@@ -101,7 +101,7 @@
   void AddInternedType(uint32_t seq_id,
                        uint64_t intern_id,
                        StringPool::Id strid,
-                       uint64_t location_id,
+                       base::Optional<uint64_t> location_id,
                        uint64_t object_size,
                        std::vector<uint64_t> field_name_ids,
                        uint64_t superclass_id,
diff --git a/src/trace_processor/importers/proto/heap_profile_tracker.cc b/src/trace_processor/importers/proto/heap_profile_tracker.cc
index 8c86c85..79bf542 100644
--- a/src/trace_processor/importers/proto/heap_profile_tracker.cc
+++ b/src/trace_processor/importers/proto/heap_profile_tracker.cc
@@ -30,6 +30,8 @@
 struct MergedCallsite {
   StringId frame_name;
   StringId mapping_name;
+  base::Optional<StringId> source_file;
+  base::Optional<uint32_t> line_number;
   base::Optional<uint32_t> parent_idx;
   bool operator<(const MergedCallsite& o) const {
     return std::tie(frame_name, mapping_name, parent_idx) <
@@ -62,7 +64,7 @@
     base::Optional<StringId> deobfuscated_name =
         frames_tbl.deobfuscated_name()[frame_idx];
     return {{deobfuscated_name ? *deobfuscated_name : frame_name, mapping_name,
-             base::nullopt}};
+             base::nullopt, base::nullopt, base::nullopt}};
   }
 
   std::vector<MergedCallsite> result;
@@ -74,8 +76,9 @@
        i < symbols_tbl.row_count() &&
        symbols_tbl.symbol_set_id()[i] == *symbol_set_id;
        ++i) {
-    result.emplace_back(
-        MergedCallsite{symbols_tbl.name()[i], mapping_name, base::nullopt});
+    result.emplace_back(MergedCallsite{
+        symbols_tbl.name()[i], mapping_name, symbols_tbl.source_file()[i],
+        symbols_tbl.line_number()[i], base::nullopt});
   }
   std::reverse(result.begin(), result.end());
   return result;
@@ -118,6 +121,7 @@
     auto callsites = GetMergedCallsites(storage, i);
     // Loop below needs to run at least once for parent_idx to get updated.
     PERFETTO_CHECK(!callsites.empty());
+    std::map<MergedCallsite, uint32_t> callsites_to_rowid;
     for (MergedCallsite& merged_callsite : callsites) {
       merged_callsite.parent_idx = parent_idx;
       auto it = merged_callsites_to_table_idx.find(merged_callsite);
@@ -137,14 +141,35 @@
         row.map_name = merged_callsite.mapping_name;
         if (parent_idx)
           row.parent_id = tbl->id()[*parent_idx];
-
         parent_idx = tbl->Insert(std::move(row)).row;
+        callsites_to_rowid[merged_callsite] =
+            static_cast<uint32_t>(merged_callsites_to_table_idx.size());
+
         PERFETTO_CHECK(merged_callsites_to_table_idx.size() ==
                        tbl->row_count());
+      } else {
+        MergedCallsite saved_callsite = it->first;
+        callsites_to_rowid.erase(saved_callsite);
+        if (saved_callsite.source_file != merged_callsite.source_file) {
+          saved_callsite.source_file = base::nullopt;
+        }
+        if (saved_callsite.line_number != merged_callsite.line_number) {
+          saved_callsite.line_number = base::nullopt;
+        }
+        callsites_to_rowid[saved_callsite] = it->second;
       }
       parent_idx = it->second;
     }
 
+    for (const auto& it : callsites_to_rowid) {
+      if (it.first.source_file) {
+        tbl->mutable_source_file()->Set(it.second, *it.first.source_file);
+      }
+      if (it.first.line_number) {
+        tbl->mutable_line_number()->Set(it.second, *it.first.line_number);
+      }
+    }
+
     PERFETTO_CHECK(parent_idx);
     callsite_to_merged_callsite[i] = *parent_idx;
   }
@@ -177,9 +202,15 @@
     uint32_t merged_idx =
         callsite_to_merged_callsite[*callsites_tbl.id().IndexOf(
             CallsiteId(static_cast<uint32_t>(callsite_id)))];
-    if (count > 0) {
+    // On old heapprofd producers, the count field is incorrectly set and we
+    // zero it in proto_trace_parser.cc.
+    // As such, we cannot depend on count == 0 to imply size == 0, so we check
+    // for both of them separately.
+    if (size > 0) {
       tbl->mutable_alloc_size()->Set(merged_idx,
                                      tbl->alloc_size()[merged_idx] + size);
+    }
+    if (count > 0) {
       tbl->mutable_alloc_count()->Set(merged_idx,
                                       tbl->alloc_count()[merged_idx] + count);
     }
@@ -227,7 +258,9 @@
 }
 
 HeapProfileTracker::HeapProfileTracker(TraceProcessorContext* context)
-    : context_(context), empty_(context_->storage->InternString({"", 0})) {}
+    : context_(context),
+      empty_(context_->storage->InternString({"", 0})),
+      art_heap_(context_->storage->InternString("com.android.art")) {}
 
 HeapProfileTracker::~HeapProfileTracker() = default;
 
@@ -358,7 +391,12 @@
     context_->storage->mutable_heap_profile_allocation_table()->Insert(
         alloc_delta);
   }
-  if (free_delta.count || free_delta.size) {
+
+  // ART only reports allocations, and not frees. This throws off our logic
+  // that assumes that if a new object was allocated with the same address,
+  // the old one has to have been freed in the meantime.
+  // See HeapTracker::RecordMalloc in bookkeeping.cc.
+  if (alloc.heap_name != art_heap_ && (free_delta.count || free_delta.size)) {
     context_->storage->mutable_heap_profile_allocation_table()->Insert(
         free_delta);
   }
diff --git a/src/trace_processor/importers/proto/heap_profile_tracker.h b/src/trace_processor/importers/proto/heap_profile_tracker.h
index 066c238..7dd8b90 100644
--- a/src/trace_processor/importers/proto/heap_profile_tracker.h
+++ b/src/trace_processor/importers/proto/heap_profile_tracker.h
@@ -120,6 +120,7 @@
   std::map<uint32_t, SequenceState> sequence_state_;
   TraceProcessorContext* const context_;
   const StringId empty_;
+  const StringId art_heap_;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/packet_sequence_state.cc b/src/trace_processor/importers/proto/packet_sequence_state.cc
index c63c2d74..2eb2350 100644
--- a/src/trace_processor/importers/proto/packet_sequence_state.cc
+++ b/src/trace_processor/importers/proto/packet_sequence_state.cc
@@ -51,5 +51,21 @@
                           message_size) == 0));
 }
 
+InternedMessageView* PacketSequenceStateGeneration::GetInternedMessageView(
+    uint32_t field_id,
+    uint64_t iid) {
+  auto field_it = interned_data_.find(field_id);
+  if (field_it != interned_data_.end()) {
+    auto* message_map = &field_it->second;
+    auto it = message_map->find(iid);
+    if (it != message_map->end()) {
+      return &it->second;
+    }
+  }
+  state_->context()->storage->IncrementStats(
+      stats::interned_data_tokenizer_errors);
+  return nullptr;
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/packet_sequence_state.h b/src/trace_processor/importers/proto/packet_sequence_state.h
index d6e3e08..a999f9a 100644
--- a/src/trace_processor/importers/proto/packet_sequence_state.h
+++ b/src/trace_processor/importers/proto/packet_sequence_state.h
@@ -26,8 +26,9 @@
 #include "perfetto/protozero/proto_decoder.h"
 #include "src/trace_processor/importers/proto/stack_profile_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/interned_message_view.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 #include "protos/perfetto/trace/trace_packet_defaults.pbzero.h"
 #include "protos/perfetto/trace/track_event/track_event.pbzero.h"
@@ -35,114 +36,6 @@
 namespace perfetto {
 namespace trace_processor {
 
-#if PERFETTO_DCHECK_IS_ON()
-// When called from GetOrCreateDecoder(), should include the stringified name of
-// the MessageType.
-#define PERFETTO_TYPE_IDENTIFIER PERFETTO_DEBUG_FUNCTION_IDENTIFIER()
-#else  // PERFETTO_DCHECK_IS_ON()
-#define PERFETTO_TYPE_IDENTIFIER nullptr
-#endif  // PERFETTO_DCHECK_IS_ON()
-
-// Entry in an interning index, refers to the interned message.
-class InternedMessageView {
- public:
-  InternedMessageView(TraceBlobView msg) : message_(std::move(msg)) {}
-
-  InternedMessageView(InternedMessageView&&) = default;
-  InternedMessageView& operator=(InternedMessageView&&) = default;
-
-  // Allow copy by cloning the TraceBlobView. This is required for
-  // UpdateTracePacketDefaults().
-  InternedMessageView(const InternedMessageView& view)
-      : message_(view.message_.slice(0, view.message_.length())) {}
-  InternedMessageView& operator=(const InternedMessageView& view) {
-    this->message_ = view.message_.slice(0, view.message_.length());
-    this->decoder_ = nullptr;
-    this->decoder_type_ = nullptr;
-    this->submessages_.clear();
-    return *this;
-  }
-
-  // Lazily initializes and returns the decoder object for the message. The
-  // decoder is stored in the InternedMessageView to avoid having to parse the
-  // message multiple times.
-  template <typename MessageType>
-  typename MessageType::Decoder* GetOrCreateDecoder() {
-    if (!decoder_) {
-      // Lazy init the decoder and save it away, so that we don't have to
-      // reparse the message every time we access the interning entry.
-      decoder_ = std::unique_ptr<void, std::function<void(void*)>>(
-          new typename MessageType::Decoder(message_.data(), message_.length()),
-          [](void* obj) {
-            delete reinterpret_cast<typename MessageType::Decoder*>(obj);
-          });
-      decoder_type_ = PERFETTO_TYPE_IDENTIFIER;
-    }
-    // Verify that the type of the decoder didn't change.
-    if (PERFETTO_TYPE_IDENTIFIER &&
-        strcmp(decoder_type_,
-               // GCC complains if this arg can be null.
-               PERFETTO_TYPE_IDENTIFIER ? PERFETTO_TYPE_IDENTIFIER : "") != 0) {
-      PERFETTO_FATAL(
-          "Interning entry accessed under different types! previous type: "
-          "%s. new type: %s.",
-          decoder_type_, PERFETTO_DEBUG_FUNCTION_IDENTIFIER());
-    }
-    return reinterpret_cast<typename MessageType::Decoder*>(decoder_.get());
-  }
-
-  // Lookup a submessage of the interned message, which is then itself stored
-  // as InternedMessageView, so that we only need to parse it once. Returns
-  // nullptr if the field isn't set.
-  // 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* decoder = GetOrCreateDecoder<MessageType>();
-    // Calls the at() template method on the decoder.
-    auto field = decoder->template at<FieldId>().as_bytes();
-    if (!field.data)
-      return nullptr;
-    const size_t offset = message_.offset_of(field.data);
-    TraceBlobView submessage = message_.slice(offset, field.size);
-    InternedMessageView* submessage_view =
-        new InternedMessageView(std::move(submessage));
-    submessages_.emplace_hint(
-        it, FieldId, std::unique_ptr<InternedMessageView>(submessage_view));
-    return submessage_view;
-  }
-
-  const TraceBlobView& message() { return message_; }
-
- private:
-  using SubMessageViewMap =
-      std::unordered_map<uint32_t /*field_id*/,
-                         std::unique_ptr<InternedMessageView>>;
-
-  TraceBlobView message_;
-
-  // Stores the decoder for the message_, so that the message does not have to
-  // be re-decoded every time the interned message is looked up. Lazily
-  // initialized in GetOrCreateDecoder(). Since we don't know the type of the
-  // decoder until GetOrCreateDecoder() is called, we store the decoder as a
-  // void* unique_pointer with a destructor function that's supplied in
-  // GetOrCreateDecoder() when the decoder is created.
-  std::unique_ptr<void, std::function<void(void*)>> decoder_;
-
-  // Type identifier for the decoder. Only valid in debug builds and on
-  // supported platforms. Used to verify that GetOrCreateDecoder() is always
-  // called with the same template argument.
-  const char* decoder_type_ = nullptr;
-
-  // Views of submessages of the interned message. Submessages are lazily
-  // added by GetOrCreateSubmessageView(). By storing submessages and their
-  // decoders, we avoid having to decode submessages multiple times if they
-  // looked up often.
-  SubMessageViewMap submessages_;
-};
-
 using InternedMessageMap =
     std::unordered_map<uint64_t /*iid*/, InternedMessageView>;
 using InternedFieldMap =
@@ -157,8 +50,7 @@
   template <uint32_t FieldId, typename MessageType>
   typename MessageType::Decoder* LookupInternedMessage(uint64_t iid);
 
-  template <uint32_t FieldId>
-  InternedMessageView* GetInternedMessageView(uint64_t iid);
+  InternedMessageView* GetInternedMessageView(uint32_t field_id, uint64_t iid);
   // Returns |nullptr| if no defaults were set.
   InternedMessageView* GetTracePacketDefaultsView() {
     if (!trace_packet_defaults_)
@@ -354,26 +246,10 @@
   SequenceStackProfileTracker sequence_stack_profile_tracker_;
 };
 
-template <uint32_t FieldId>
-InternedMessageView* PacketSequenceStateGeneration::GetInternedMessageView(
-    uint64_t iid) {
-  auto field_it = interned_data_.find(FieldId);
-  if (field_it != interned_data_.end()) {
-    auto* message_map = &field_it->second;
-    auto it = message_map->find(iid);
-    if (it != message_map->end()) {
-      return &it->second;
-    }
-  }
-  state_->context()->storage->IncrementStats(
-      stats::interned_data_tokenizer_errors);
-  return nullptr;
-}
-
 template <uint32_t FieldId, typename MessageType>
 typename MessageType::Decoder*
 PacketSequenceStateGeneration::LookupInternedMessage(uint64_t iid) {
-  auto* interned_message_view = GetInternedMessageView<FieldId>(iid);
+  auto* interned_message_view = GetInternedMessageView(FieldId, iid);
   if (!interned_message_view)
     return nullptr;
 
diff --git a/src/trace_processor/importers/proto/profile_packet_utils.h b/src/trace_processor/importers/proto/profile_packet_utils.h
index eaa561e..2dde388 100644
--- a/src/trace_processor/importers/proto/profile_packet_utils.h
+++ b/src/trace_processor/importers/proto/profile_packet_utils.h
@@ -174,8 +174,8 @@
 
   base::Optional<SequenceStackProfileTracker::SourceCallstack> GetCallstack(
       SequenceStackProfileTracker::SourceCallstackId iid) const override {
-    auto* interned_message_view = seq_state_->GetInternedMessageView<
-        protos::pbzero::InternedData::kCallstacksFieldNumber>(iid);
+    auto* interned_message_view = seq_state_->GetInternedMessageView(
+        protos::pbzero::InternedData::kCallstacksFieldNumber, iid);
     if (!interned_message_view)
       return base::nullopt;
     protos::pbzero::Callstack::Decoder decoder(
diff --git a/src/trace_processor/importers/proto/profiler_util.cc b/src/trace_processor/importers/proto/profiler_util.cc
index ea44c1f..c28f2fc 100644
--- a/src/trace_processor/importers/proto/profiler_util.cc
+++ b/src/trace_processor/importers/proto/profiler_util.cc
@@ -17,6 +17,7 @@
 #include "src/trace_processor/importers/proto/profiler_util.h"
 
 #include "perfetto/ext/base/optional.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
 namespace perfetto {
@@ -92,9 +93,9 @@
     return "com.google.android.apps.wellbeing";
   }
 
-  base::StringView matchmaker("MatchMaker");
-  if (location.size() >= matchmaker.size() &&
-      location.find(matchmaker) != base::StringView::npos) {
+  if (location.find("DevicePersonalizationPrebuilt") !=
+          base::StringView::npos ||
+      location.find("MatchMaker") != base::StringView::npos) {
     return "com.google.android.as";
   }
 
@@ -103,7 +104,8 @@
     return "com.google.android.gm";
   }
 
-  if (location.find("PrebuiltGmsCore") != std::string::npos) {
+  if (location.find("PrebuiltGmsCore") != std::string::npos ||
+      location.find("com.google.android.gms") != std::string::npos) {
     return "com.google.android.gms";
   }
 
@@ -144,13 +146,13 @@
     protos::pbzero::ObfuscatedMember::Decoder& member) {
   std::string member_deobfuscated_name =
       member.deobfuscated_name().ToStdString();
-  if (member_deobfuscated_name.find('.') == std::string::npos) {
+  if (base::Contains(member_deobfuscated_name, '.')) {
+    // Fully qualified name.
+    return member_deobfuscated_name;
+  } else {
     // Name relative to class.
     return cls.deobfuscated_name().ToStdString() + "." +
            member_deobfuscated_name;
-  } else {
-    // Fully qualified name.
-    return member_deobfuscated_name;
   }
 }
 
diff --git a/src/trace_processor/importers/proto/proto_importer_module.h b/src/trace_processor/importers/proto/proto_importer_module.h
index dda2f85..9a1dd5b 100644
--- a/src/trace_processor/importers/proto/proto_importer_module.h
+++ b/src/trace_processor/importers/proto/proto_importer_module.h
@@ -19,7 +19,7 @@
 
 #include "perfetto/ext/base/optional.h"
 #include "perfetto/trace_processor/status.h"
-#include "src/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 namespace perfetto {
 
diff --git a/src/trace_processor/importers/proto/proto_trace_parser.cc b/src/trace_processor/importers/proto/proto_trace_parser.cc
index 4a2e0b9..9a70b3f 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser.cc
@@ -243,7 +243,7 @@
 }
 
 void ProtoTraceParser::ParseProfilePacket(
-    int64_t,
+    int64_t ts,
     PacketSequenceStateGeneration* sequence_state,
     uint32_t seq_id,
     ConstBytes blob) {
@@ -297,6 +297,8 @@
     int64_t timestamp = *maybe_timestamp;
 
     int pid = static_cast<int>(entry.pid());
+    context_->storage->SetIndexedStats(stats::heapprofd_last_profile_timestamp,
+                                       pid, ts);
 
     if (entry.disconnected())
       context_->storage->IncrementIndexedStats(
@@ -335,6 +337,14 @@
     context_->storage->IncrementIndexedStats(
         stats::heapprofd_unwind_samples, static_cast<int>(entry.pid()),
         static_cast<int64_t>(stats.heap_samples()));
+    context_->storage->IncrementIndexedStats(
+        stats::heapprofd_client_spinlock_blocked, static_cast<int>(entry.pid()),
+        static_cast<int64_t>(stats.client_spinlock_blocked_us()));
+
+    // orig_sampling_interval_bytes was introduced slightly after a bug with
+    // self_max_count was fixed in the producer. We use this as a proxy
+    // whether or not we are getting this data from a fixed producer or not.
+    bool trustworthy_max_count = entry.orig_sampling_interval_bytes() > 0;
 
     for (auto sample_it = entry.samples(); sample_it; ++sample_it) {
       protos::pbzero::ProfilePacket::HeapSample::Decoder sample(*sample_it);
@@ -351,7 +361,8 @@
       src_allocation.callstack_id = sample.callstack_id();
       if (sample.has_self_max()) {
         src_allocation.self_allocated = sample.self_max();
-        src_allocation.alloc_count = sample.self_max_count();
+        if (trustworthy_max_count)
+          src_allocation.alloc_count = sample.self_max_count();
       } else {
         src_allocation.self_allocated = sample.self_allocated();
         src_allocation.self_freed = sample.self_freed();
@@ -638,6 +649,7 @@
     StringId id = context_->storage->InternString(base::StringView(str));
     context_->metadata_tracker->SetMetadata(metadata::trace_uuid,
                                             Variadic::String(id));
+    context_->uuid_found_in_trace = true;
   }
 
   if (trace_config.has_unique_session_name()) {
diff --git a/src/trace_processor/importers/proto/proto_trace_parser.h b/src/trace_processor/importers/proto/proto_trace_parser.h
index 2558975..f33bc5f 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser.h
+++ b/src/trace_processor/importers/proto/proto_trace_parser.h
@@ -25,10 +25,10 @@
 #include "perfetto/ext/base/optional.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/protozero/field.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/timestamped_trace_piece.h"
-#include "src/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/trace_parser.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 namespace perfetto {
 
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc b/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc
index 9cab42b..c0f11cd 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc
@@ -29,13 +29,13 @@
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/default_modules.h"
 #include "src/trace_processor/importers/ftrace/sched_event_tracker.h"
-#include "src/trace_processor/importers/proto/args_table_utils.h"
 #include "src/trace_processor/importers/proto/metadata_tracker.h"
 #include "src/trace_processor/importers/proto/proto_trace_parser.h"
 #include "src/trace_processor/importers/proto/stack_profile_tracker.h"
 #include "src/trace_processor/storage/metadata.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/trace_sorter.h"
+#include "src/trace_processor/util/descriptors.h"
 #include "test/gtest_and_gmock.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
@@ -253,7 +253,7 @@
     clock_ = new ClockTracker(&context_);
     context_.clock_tracker.reset(clock_);
     context_.sorter.reset(new TraceSorter(CreateParser(), 0 /*window size*/));
-    context_.proto_to_args_table_.reset(new ProtoToArgsTable(&context_));
+    context_.descriptor_pool_.reset(new DescriptorPool());
 
     RegisterDefaultModules(&context_);
     RegisterAdditionalModules(&context_);
@@ -383,7 +383,7 @@
   ASSERT_EQ(args.int_value()[4], 20);
   ASSERT_STREQ(args.string_value().GetString(5).c_str(), buf_value);
 
-  // TODO(taylori): Add test ftrace event with all field types
+  // TODO(hjd): Add test ftrace event with all field types
   // and test here.
 }
 
@@ -2970,6 +2970,7 @@
   SqlValue value =
       context_.metadata_tracker->GetMetadataForTesting(metadata::trace_uuid);
   EXPECT_STREQ(value.string_value, "00000000-0000-0002-0000-000000000001");
+  ASSERT_TRUE(context_.uuid_found_in_trace);
 }
 
 TEST_F(ProtoTraceParserTest, ConfigPbtxt) {
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc
index 16e7908..be5f1ff 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -30,14 +30,14 @@
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/ftrace/ftrace_module.h"
-#include "src/trace_processor/importers/gzip/gzip_utils.h"
-#include "src/trace_processor/importers/proto/args_table_utils.h"
 #include "src/trace_processor/importers/proto/metadata_tracker.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state.h"
 #include "src/trace_processor/importers/proto/proto_incremental_state.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/trace_sorter.h"
+#include "src/trace_processor/util/descriptors.h"
+#include "src/trace_processor/util/gzip_utils.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "protos/perfetto/config/trace_config.pbzero.h"
@@ -67,7 +67,7 @@
                                                        descriptor.size);
 
   auto extension = decoder.extension_set();
-  return context_->proto_to_args_table_->AddProtoFileDescriptor(
+  return context_->descriptor_pool_->AddFromFileDescriptorSet(
       extension.data, extension.size,
       /*merge_existing_messages=*/true);
 }
@@ -195,16 +195,10 @@
           context_->clock_tracker->ToTraceTime(converted_clock_id, timestamp);
       if (!trace_ts.has_value()) {
         // ToTraceTime() will increase the |clock_sync_failure| stat on failure.
-        static const char seq_extra_err[] =
-            " Because the clock id is sequence-scoped, the ClockSnapshot must "
-            "be emitted on the same TraceWriter sequence of the packet that "
-            "refers to that clock id.";
-        return util::ErrStatus(
-            "Failed to convert TracePacket's timestamp from clock_id=%" PRIu32
-            " seq_id=%" PRIu32
-            ". This is usually due to the lack of a prior ClockSnapshot "
-            "proto.%s",
-            timestamp_clock_id, seq_id, is_seq_scoped ? seq_extra_err : "");
+        // We don't return an error here as it will cause the trace to stop
+        // parsing. Instead, we rely on the stat increment in ToTraceTime() to
+        // inform the user about the error.
+        return util::OkStatus();
       }
       timestamp = trace_ts.value();
     }
@@ -384,10 +378,61 @@
     clocks.emplace_back(clock_id, clk.timestamp(), unit_multiplier_ns,
                         clk.is_incremental());
   }
-  context_->clock_tracker->AddSnapshot(clocks);
+
+  uint32_t snapshot_id = context_->clock_tracker->AddSnapshot(clocks);
+
+  // Add the all the clock values to the clock snapshot table.
+  base::Optional<int64_t> trace_ts_for_check;
+  for (const auto& clock : clocks) {
+    // If the clock is incremental, we need to use 0 to map correctly to
+    // |absolute_timestamp|.
+    int64_t ts_to_convert = clock.is_incremental ? 0 : clock.absolute_timestamp;
+    base::Optional<int64_t> opt_trace_ts =
+        context_->clock_tracker->ToTraceTime(clock.clock_id, ts_to_convert);
+    if (!opt_trace_ts) {
+      // This can happen if |AddSnapshot| failed to resolve this clock. Just
+      // ignore this and move on.
+      continue;
+    }
+
+    // Double check that all the clocks in this snapshot resolve to the same
+    // trace timestamp value.
+    PERFETTO_DCHECK(!trace_ts_for_check || opt_trace_ts == trace_ts_for_check);
+    trace_ts_for_check = *opt_trace_ts;
+
+    tables::ClockSnapshotTable::Row row;
+    row.ts = *opt_trace_ts;
+    row.clock_id = static_cast<int64_t>(clock.clock_id);
+    row.clock_value = clock.absolute_timestamp;
+    row.clock_name = GetBuiltinClockNameOrNull(clock.clock_id);
+    row.snapshot_id = snapshot_id;
+
+    auto* snapshot_table = context_->storage->mutable_clock_snapshot_table();
+    snapshot_table->Insert(row);
+  }
   return util::OkStatus();
 }
 
+base::Optional<StringId> ProtoTraceReader::GetBuiltinClockNameOrNull(
+    uint64_t clock_id) {
+  switch (clock_id) {
+    case protos::pbzero::ClockSnapshot::Clock::REALTIME:
+      return context_->storage->InternString("REALTIME");
+    case protos::pbzero::ClockSnapshot::Clock::REALTIME_COARSE:
+      return context_->storage->InternString("REALTIME_COARSE");
+    case protos::pbzero::ClockSnapshot::Clock::MONOTONIC:
+      return context_->storage->InternString("MONOTONIC");
+    case protos::pbzero::ClockSnapshot::Clock::MONOTONIC_COARSE:
+      return context_->storage->InternString("MONOTONIC_COARSE");
+    case protos::pbzero::ClockSnapshot::Clock::MONOTONIC_RAW:
+      return context_->storage->InternString("MONOTONIC_RAW");
+    case protos::pbzero::ClockSnapshot::Clock::BOOTTIME:
+      return context_->storage->InternString("BOOTTIME");
+    default:
+      return base::nullopt;
+  }
+}
+
 util::Status ProtoTraceReader::ParseServiceEvent(int64_t ts, ConstBytes blob) {
   protos::pbzero::TracingServiceEvent::Decoder tse(blob);
   if (tse.tracing_started()) {
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.h b/src/trace_processor/importers/proto/proto_trace_reader.h
index 7252f62..c144c29 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.h
+++ b/src/trace_processor/importers/proto/proto_trace_reader.h
@@ -21,10 +21,10 @@
 
 #include <memory>
 
-#include "src/trace_processor/chunked_trace_reader.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/proto/proto_incremental_state.h"
 #include "src/trace_processor/importers/proto/proto_trace_tokenizer.h"
-#include "src/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 namespace protozero {
 struct ConstBytes;
@@ -75,6 +75,8 @@
                          TraceBlobView interned_data);
   void ParseTraceConfig(ConstBytes);
 
+  base::Optional<StringId> GetBuiltinClockNameOrNull(uint64_t clock_id);
+
   PacketSequenceState* GetIncrementalStateForPacketSequence(
       uint32_t sequence_id) {
     if (!incremental_state)
diff --git a/src/trace_processor/importers/proto/proto_trace_tokenizer.cc b/src/trace_processor/importers/proto/proto_trace_tokenizer.cc
index fdc7bcdd..9bf23df 100644
--- a/src/trace_processor/importers/proto/proto_trace_tokenizer.cc
+++ b/src/trace_processor/importers/proto/proto_trace_tokenizer.cc
@@ -25,7 +25,7 @@
 
 util::Status ProtoTraceTokenizer::Decompress(TraceBlobView input,
                                              TraceBlobView* output) {
-  PERFETTO_DCHECK(gzip::IsGzipSupported());
+  PERFETTO_DCHECK(util::IsGzipSupported());
 
   uint8_t out[4096];
 
@@ -36,7 +36,7 @@
   decompressor_.Reset();
   decompressor_.SetInput(input.data(), input.length());
 
-  using ResultCode = GzipDecompressor::ResultCode;
+  using ResultCode = util::GzipDecompressor::ResultCode;
   for (auto ret = ResultCode::kOk; ret != ResultCode::kEof;) {
     auto res = decompressor_.Decompress(out, base::ArraySize(out));
     ret = res.ret;
diff --git a/src/trace_processor/importers/proto/proto_trace_tokenizer.h b/src/trace_processor/importers/proto/proto_trace_tokenizer.h
index 647d9de..350cb34 100644
--- a/src/trace_processor/importers/proto/proto_trace_tokenizer.h
+++ b/src/trace_processor/importers/proto/proto_trace_tokenizer.h
@@ -21,9 +21,9 @@
 
 #include "perfetto/protozero/proto_utils.h"
 #include "perfetto/trace_processor/status.h"
-#include "src/trace_processor/importers/gzip/gzip_utils.h"
-#include "src/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/util/gzip_utils.h"
 #include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
@@ -143,7 +143,7 @@
     protos::pbzero::TracePacket::Decoder decoder(packet.data(),
                                                  packet.length());
     if (decoder.has_compressed_packets()) {
-      if (!gzip::IsGzipSupported()) {
+      if (!util::IsGzipSupported()) {
         return util::Status(
             "Cannot decode compressed packets. Zlib not enabled");
       }
@@ -185,7 +185,7 @@
   std::vector<uint8_t> partial_buf_;
 
   // Allows support for compressed trace packets.
-  GzipDecompressor decompressor_;
+  util::GzipDecompressor decompressor_;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/stack_profile_tracker.cc b/src/trace_processor/importers/proto/stack_profile_tracker.cc
index 52e4dbd..6bf9a73 100644
--- a/src/trace_processor/importers/proto/stack_profile_tracker.cc
+++ b/src/trace_processor/importers/proto/stack_profile_tracker.cc
@@ -185,7 +185,7 @@
       cur_id = frames->Insert(row).id;
       context_->global_stack_profile_tracker->InsertFrameRow(
           mapping_id, static_cast<uint64_t>(row.rel_pc), *cur_id);
-      if (name.find('.') != std::string::npos) {
+      if (base::Contains(name, '.')) {
         // Java frames always contain a '.'
         base::Optional<std::string> package =
             PackageFromLocation(context_->storage.get(), mapping_name);
diff --git a/src/trace_processor/importers/proto/system_probes_parser.cc b/src/trace_processor/importers/proto/system_probes_parser.cc
index 2319a13..214c0fa 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.cc
+++ b/src/trace_processor/importers/proto/system_probes_parser.cc
@@ -119,6 +119,20 @@
         ts, static_cast<double>(mi.value()) * 1024., track);
   }
 
+  for (auto it = sys_stats.devfreq(); it; ++it) {
+    protos::pbzero::SysStats::DevfreqValue::Decoder vm(*it);
+    auto key = static_cast<base::StringView>(vm.key());
+    // Append " Frequency" to align names with FtraceParser::ParseClockSetRate
+    base::StringView devfreq_subtitle("Frequency");
+    char counter_name[255];
+    snprintf(counter_name, sizeof(counter_name), "%.*s %.*s", int(key.size()),
+             key.data(), int(devfreq_subtitle.size()), devfreq_subtitle.data());
+    StringId name = context_->storage->InternString(counter_name);
+    TrackId track = context_->track_tracker->InternGlobalCounterTrack(name);
+    context_->event_tracker->PushCounter(ts, static_cast<double>(vm.value()),
+                                         track);
+  }
+
   for (auto it = sys_stats.vmstat(); it; ++it) {
     protos::pbzero::SysStats::VmstatValue::Decoder vm(*it);
     auto key = static_cast<size_t>(vm.key());
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 1809cad..6be84c5 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -23,19 +23,19 @@
 #include "perfetto/ext/base/optional.h"
 #include "perfetto/ext/base/string_writer.h"
 #include "perfetto/trace_processor/status.h"
-#include "src/trace_processor/importers/chrome_track_event.descriptor.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/json/json_utils.h"
-#include "src/trace_processor/importers/proto/args_table_utils.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state.h"
 #include "src/trace_processor/importers/proto/track_event_tracker.h"
-#include "src/trace_processor/importers/track_event.descriptor.h"
+#include "src/trace_processor/util/debug_annotation_parser.h"
+#include "src/trace_processor/util/proto_to_args_parser.h"
 #include "src/trace_processor/util/status_macros.h"
 
+#include "protos/perfetto/trace/extension_descriptor.pbzero.h"
 #include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
 #include "protos/perfetto/trace/track_event/chrome_compositor_scheduler_state.pbzero.h"
 #include "protos/perfetto/trace/track_event/chrome_histogram_sample.pbzero.h"
@@ -66,52 +66,102 @@
 constexpr int64_t kPendingThreadDuration = -1;
 constexpr int64_t kPendingThreadInstructionDelta = -1;
 
-void AddStringToArgsTable(const char* field,
-                          const protozero::ConstChars& str,
-                          const ProtoToArgsTable::ParsingOverrideState& state,
-                          BoundInserter* inserter) {
-  auto val = state.context->storage->InternString(base::StringView(str));
-  auto key = state.context->storage->InternString(base::StringView(field));
-  inserter->AddArg(key, Variadic::String(val));
-}
+class TrackEventArgsParser : public util::ProtoToArgsParser::Delegate {
+ public:
+  TrackEventArgsParser(BoundInserter& inserter,
+                       TraceStorage& storage,
+                       PacketSequenceStateGeneration& sequence_state)
+      : inserter_(inserter),
+        storage_(storage),
+        sequence_state_(sequence_state) {}
+  ~TrackEventArgsParser() override;
 
-bool MaybeParseSourceLocation(
+  using Key = util::ProtoToArgsParser::Key;
+
+  void AddInteger(const Key& key, int64_t value) final {
+    inserter_.AddArg(storage_.InternString(base::StringView(key.flat_key)),
+                     storage_.InternString(base::StringView(key.key)),
+                     Variadic::Integer(value));
+  }
+  void AddUnsignedInteger(const Key& key, uint64_t value) final {
+    inserter_.AddArg(storage_.InternString(base::StringView(key.flat_key)),
+                     storage_.InternString(base::StringView(key.key)),
+                     Variadic::UnsignedInteger(value));
+  }
+  void AddString(const Key& key, const protozero::ConstChars& value) final {
+    inserter_.AddArg(storage_.InternString(base::StringView(key.flat_key)),
+                     storage_.InternString(base::StringView(key.key)),
+                     Variadic::String(storage_.InternString(value)));
+  }
+  void AddDouble(const Key& key, double value) final {
+    inserter_.AddArg(storage_.InternString(base::StringView(key.flat_key)),
+                     storage_.InternString(base::StringView(key.key)),
+                     Variadic::Real(value));
+  }
+  void AddPointer(const Key& key, const void* value) final {
+    inserter_.AddArg(storage_.InternString(base::StringView(key.flat_key)),
+                     storage_.InternString(base::StringView(key.key)),
+                     Variadic::Pointer(reinterpret_cast<uintptr_t>(value)));
+  }
+  void AddBoolean(const Key& key, bool value) final {
+    inserter_.AddArg(storage_.InternString(base::StringView(key.flat_key)),
+                     storage_.InternString(base::StringView(key.key)),
+                     Variadic::Boolean(value));
+  }
+  bool AddJson(const Key& key, const protozero::ConstChars& value) final {
+    auto json_value = json::ParseJsonString(value);
+    if (!json_value)
+      return false;
+    return json::AddJsonValueToArgs(*json_value, base::StringView(key.flat_key),
+                                    base::StringView(key.key), &storage_,
+                                    &inserter_);
+  }
+
+  size_t GetArrayEntryIndex(const std::string& array_key) final {
+    return inserter_.GetNextArrayEntryIndex(
+        storage_.InternString(base::StringView(array_key)));
+  }
+
+  size_t IncrementArrayEntryIndex(const std::string& array_key) final {
+    return inserter_.IncrementArrayEntryIndex(
+        storage_.InternString(base::StringView(array_key)));
+  }
+
+  InternedMessageView* GetInternedMessageView(uint32_t field_id,
+                                              uint64_t iid) final {
+    return sequence_state_.GetInternedMessageView(field_id, iid);
+  }
+
+ private:
+  BoundInserter& inserter_;
+  TraceStorage& storage_;
+  PacketSequenceStateGeneration& sequence_state_;
+};
+
+TrackEventArgsParser::~TrackEventArgsParser() = default;
+
+base::Optional<base::Status> MaybeParseSourceLocation(
     std::string prefix,
-    const ProtoToArgsTable::ParsingOverrideState& state,
     const protozero::Field& field,
-    BoundInserter* inserter) {
-  auto* decoder = state.sequence_state->LookupInternedMessage<
-      protos::pbzero::InternedData::kSourceLocationsFieldNumber,
-      protos::pbzero::SourceLocation>(field.as_uint64());
+    util::ProtoToArgsParser::Delegate& delegate) {
+  auto* decoder = delegate.GetInternedMessage(
+      protos::pbzero::InternedData::kSourceLocations, field.as_uint64());
   if (!decoder) {
     // Lookup failed fall back on default behaviour which will just put
     // the source_location_iid into the args table.
-    return false;
+    return base::nullopt;
   }
-  {
-    ProtoToArgsTable::ScopedStringAppender scoped("file_name", &prefix);
-    AddStringToArgsTable(prefix.c_str(), decoder->file_name(), state, inserter);
-  }
-  {
-    ProtoToArgsTable::ScopedStringAppender scoped("function_name", &prefix);
-    AddStringToArgsTable(prefix.c_str(), decoder->function_name(), state,
-                         inserter);
-  }
-  ProtoToArgsTable::ScopedStringAppender scoped("line_number", &prefix);
-  auto key = state.context->storage->InternString(base::StringView(prefix));
-  inserter->AddArg(key, Variadic::Integer(decoder->line_number()));
-  // By returning false we expect this field to be handled like regular.
-  return true;
+
+  delegate.AddString(util::ProtoToArgsParser::Key(prefix + ".file_name"),
+                     decoder->file_name());
+  delegate.AddString(util::ProtoToArgsParser::Key(prefix + ".function_name"),
+                     decoder->function_name());
+  delegate.AddInteger(util::ProtoToArgsParser::Key(prefix + ".line_number"),
+                      decoder->line_number());
+
+  return base::OkStatus();
 }
 
-std::string SafeDebugAnnotationName(const std::string& raw_name) {
-  std::string result = raw_name;
-  std::replace(result.begin(), result.end(), '.', '_');
-  std::replace(result.begin(), result.end(), '[', '_');
-  std::replace(result.begin(), result.end(), ']', '_');
-  result = "debug." + result;
-  return result;
-}
 }  // namespace
 
 class TrackEventParser::EventImporter {
@@ -177,7 +227,7 @@
       case 'i':
       case 'I':  // TRACE_EVENT_PHASE_INSTANT.
       case 'R':  // TRACE_EVENT_PHASE_MARK.
-        return ParseThreadInstantEvent();
+        return ParseThreadInstantEvent(phase);
       case 'b':  // TRACE_EVENT_PHASE_NESTABLE_ASYNC_BEGIN
       case 'S':
         return ParseAsyncBeginEvent(phase);
@@ -759,14 +809,21 @@
     MaybeParseTrackEventFlows();
   }
 
-  util::Status ParseThreadInstantEvent() {
+  util::Status ParseThreadInstantEvent(char phase) {
     // Handle instant events as slices with zero duration, so that they end
     // up nested underneath their parent slices.
     int64_t duration_ns = 0;
     int64_t tidelta = 0;
     base::Optional<tables::SliceTable::Id> opt_slice_id;
-    auto args_inserter = [this](BoundInserter* inserter) {
+    auto args_inserter = [this, phase](BoundInserter* inserter) {
       ParseTrackEventArgs(inserter);
+      // For legacy MARK event, add phase for JSON exporter.
+      if (phase == 'R') {
+        std::string phase_string(1, static_cast<char>(phase));
+        StringId phase_id = storage_->InternString(phase_string.c_str());
+        inserter->AddArg(parser_->legacy_event_phase_key_id_,
+                         Variadic::String(phase_id));
+      }
     };
     if (utid_) {
       auto* thread_slices = storage_->mutable_thread_slice_table();
@@ -939,7 +996,8 @@
       auto process_name = annotation.string_value();
       if (!process_name.size)
         return util::OkStatus();
-      auto process_name_id = storage_->InternString(process_name);
+      auto process_name_id =
+          storage_->InternString(base::StringView(process_name));
       // Don't override system-provided names.
       procs->SetProcessNameIfUnset(*upid_, process_name_id);
       return util::OkStatus();
@@ -1041,10 +1099,6 @@
       PERFETTO_DLOG("ParseTrackEventArgs error: %s", status.c_message());
     };
 
-    for (auto it = event_.debug_annotations(); it; ++it) {
-      log_errors(ParseDebugAnnotationArgs(*it, inserter));
-    }
-
     if (event_.has_source_location_iid()) {
       log_errors(AddSourceLocationArgs(event_.source_location_iid(), inserter));
     }
@@ -1060,10 +1114,18 @@
           ParseHistogramName(event_.chrome_histogram_sample(), inserter));
     }
 
-    log_errors(
-        parser_->context_->proto_to_args_table_->InternProtoFieldsIntoArgsTable(
-            blob_, ".perfetto.protos.TrackEvent", &parser_->reflect_fields_,
-            inserter, sequence_state_));
+    TrackEventArgsParser args_writer(*inserter, *storage_, *sequence_state_);
+    log_errors(parser_->args_parser_.ParseMessage(
+        blob_, ".perfetto.protos.TrackEvent", &parser_->reflect_fields_,
+        args_writer));
+
+    {
+      auto key = parser_->args_parser_.EnterDictionary("debug");
+      util::DebugAnnotationParser parser(parser_->args_parser_);
+      for (auto it = event_.debug_annotations(); it; ++it) {
+        log_errors(parser.Parse(*it, args_writer));
+      }
+    }
 
     if (legacy_passthrough_utid_) {
       inserter->AddArg(parser_->legacy_event_passthrough_utid_id_,
@@ -1072,127 +1134,6 @@
     }
   }
 
-  util::Status ParseDebugAnnotationArgs(ConstBytes debug_annotation,
-                                        BoundInserter* inserter) {
-    protos::pbzero::DebugAnnotation::Decoder annotation(debug_annotation);
-
-    StringId name_id = kNullStringId;
-
-    uint64_t name_iid = annotation.name_iid();
-    if (PERFETTO_LIKELY(name_iid)) {
-      auto* decoder = sequence_state_->LookupInternedMessage<
-          protos::pbzero::InternedData::kDebugAnnotationNamesFieldNumber,
-          protos::pbzero::DebugAnnotationName>(name_iid);
-      if (!decoder)
-        return util::ErrStatus("Debug annotation with invalid name_iid");
-
-      std::string name_prefixed =
-          SafeDebugAnnotationName(decoder->name().ToStdString());
-      name_id = storage_->InternString(base::StringView(name_prefixed));
-    } else if (annotation.has_name()) {
-      name_id = storage_->InternString(annotation.name());
-    } else {
-      return util::ErrStatus("Debug annotation without name");
-    }
-
-    if (annotation.has_bool_value()) {
-      inserter->AddArg(name_id, Variadic::Boolean(annotation.bool_value()));
-    } else if (annotation.has_uint_value()) {
-      inserter->AddArg(name_id,
-                       Variadic::UnsignedInteger(annotation.uint_value()));
-    } else if (annotation.has_int_value()) {
-      inserter->AddArg(name_id, Variadic::Integer(annotation.int_value()));
-    } else if (annotation.has_double_value()) {
-      inserter->AddArg(name_id, Variadic::Real(annotation.double_value()));
-    } else if (annotation.has_string_value()) {
-      inserter->AddArg(
-          name_id,
-          Variadic::String(storage_->InternString(annotation.string_value())));
-    } else if (annotation.has_pointer_value()) {
-      inserter->AddArg(name_id, Variadic::Pointer(annotation.pointer_value()));
-    } else if (annotation.has_legacy_json_value()) {
-      if (!json::IsJsonSupported())
-        return util::ErrStatus("Ignoring legacy_json_value (no json support)");
-
-      auto value = json::ParseJsonString(annotation.legacy_json_value());
-      auto name = storage_->GetString(name_id);
-      json::AddJsonValueToArgs(*value, name, name, storage_, inserter);
-    } else if (annotation.has_nested_value()) {
-      auto name = storage_->GetString(name_id);
-      ParseNestedValueArgs(annotation.nested_value(), name, name, inserter);
-    }
-
-    return util::OkStatus();
-  }
-
-  bool ParseNestedValueArgs(ConstBytes nested_value,
-                            base::StringView flat_key,
-                            base::StringView key,
-                            BoundInserter* inserter) {
-    protos::pbzero::DebugAnnotation::NestedValue::Decoder value(nested_value);
-    switch (value.nested_type()) {
-      case protos::pbzero::DebugAnnotation::NestedValue::UNSPECIFIED: {
-        auto flat_key_id = storage_->InternString(flat_key);
-        auto key_id = storage_->InternString(key);
-        // Leaf value.
-        if (value.has_bool_value()) {
-          inserter->AddArg(flat_key_id, key_id,
-                           Variadic::Boolean(value.bool_value()));
-          return true;
-        }
-        if (value.has_int_value()) {
-          inserter->AddArg(flat_key_id, key_id,
-                           Variadic::Integer(value.int_value()));
-          return true;
-        }
-        if (value.has_double_value()) {
-          inserter->AddArg(flat_key_id, key_id,
-                           Variadic::Real(value.double_value()));
-          return true;
-        }
-        if (value.has_string_value()) {
-          inserter->AddArg(
-              flat_key_id, key_id,
-              Variadic::String(storage_->InternString(value.string_value())));
-          return true;
-        }
-        return false;
-      }
-      case protos::pbzero::DebugAnnotation::NestedValue::DICT: {
-        auto key_it = value.dict_keys();
-        auto value_it = value.dict_values();
-        bool inserted = false;
-        for (; key_it && value_it; ++key_it, ++value_it) {
-          std::string child_name = (*key_it).ToStdString();
-          std::string child_flat_key =
-              flat_key.ToStdString() + "." + child_name;
-          std::string child_key = key.ToStdString() + "." + child_name;
-          inserted |=
-              ParseNestedValueArgs(*value_it, base::StringView(child_flat_key),
-                                   base::StringView(child_key), inserter);
-        }
-        return inserted;
-      }
-      case protos::pbzero::DebugAnnotation::NestedValue::ARRAY: {
-        bool inserted_any = false;
-        std::string array_key = key.ToStdString();
-        StringId array_key_id = storage_->InternString(key);
-        for (auto value_it = value.array_values(); value_it; ++value_it) {
-          size_t array_index = inserter->GetNextArrayEntryIndex(array_key_id);
-          std::string child_key =
-              array_key + "[" + std::to_string(array_index) + "]";
-          bool inserted = ParseNestedValueArgs(
-              *value_it, flat_key, base::StringView(child_key), inserter);
-          if (inserted)
-            inserter->IncrementArrayEntryIndex(array_key_id);
-          inserted_any |= inserted;
-        }
-        return inserted_any;
-      }
-    }
-    return false;
-  }
-
   util::Status ParseTaskExecutionArgs(ConstBytes task_execution,
                                       BoundInserter* inserter) {
     protos::pbzero::TaskExecution::Decoder task(task_execution);
@@ -1344,7 +1285,8 @@
 
 TrackEventParser::TrackEventParser(TraceProcessorContext* context,
                                    TrackEventTracker* track_event_tracker)
-    : context_(context),
+    : args_parser_(*context->descriptor_pool_.get()),
+      context_(context),
       track_event_tracker_(track_event_tracker),
       counter_name_thread_time_id_(
           context->storage->InternString("thread_time")),
@@ -1419,45 +1361,47 @@
       counter_unit_ids_{{kNullStringId, context_->storage->InternString("ns"),
                          context_->storage->InternString("count"),
                          context_->storage->InternString("bytes")}} {
-  auto status = context_->proto_to_args_table_->AddProtoFileDescriptor(
-      kTrackEventDescriptor.data(), kTrackEventDescriptor.size());
-
-  PERFETTO_DCHECK(status.ok());
-
-  status = context_->proto_to_args_table_->AddProtoFileDescriptor(
-      kChromeTrackEventDescriptor.data(), kChromeTrackEventDescriptor.size());
-
-  PERFETTO_DCHECK(status.ok());
-
   // Switch |source_location_iid| into its interned data variant.
-  context_->proto_to_args_table_->AddParsingOverride(
+  args_parser_.AddParsingOverrideForField(
       "begin_impl_frame_args.current_args.source_location_iid",
-      [](const ProtoToArgsTable::ParsingOverrideState& state,
-         const protozero::Field& field, BoundInserter* inserter) {
+      [](const protozero::Field& field,
+         util::ProtoToArgsParser::Delegate& delegate) {
         return MaybeParseSourceLocation("begin_impl_frame_args.current_args",
-                                        state, field, inserter);
+                                        field, delegate);
       });
-  context_->proto_to_args_table_->AddParsingOverride(
+  args_parser_.AddParsingOverrideForField(
       "begin_impl_frame_args.last_args.source_location_iid",
-      [](const ProtoToArgsTable::ParsingOverrideState& state,
-         const protozero::Field& field, BoundInserter* inserter) {
+      [](const protozero::Field& field,
+         util::ProtoToArgsParser::Delegate& delegate) {
         return MaybeParseSourceLocation("begin_impl_frame_args.last_args",
-                                        state, field, inserter);
+                                        field, delegate);
       });
-  context_->proto_to_args_table_->AddParsingOverride(
+  args_parser_.AddParsingOverrideForField(
       "begin_frame_observer_state.last_begin_frame_args.source_location_iid",
-      [](const ProtoToArgsTable::ParsingOverrideState& state,
-         const protozero::Field& field, BoundInserter* inserter) {
+      [](const protozero::Field& field,
+         util::ProtoToArgsParser::Delegate& delegate) {
         return MaybeParseSourceLocation(
-            "begin_frame_observer_state.last_begin_frame_args", state, field,
-            inserter);
+            "begin_frame_observer_state.last_begin_frame_args", field,
+            delegate);
       });
-  context_->proto_to_args_table_->AddParsingOverride(
+  args_parser_.AddParsingOverrideForField(
       "chrome_memory_pressure_notification.creation_location_iid",
-      [](const ProtoToArgsTable::ParsingOverrideState& state,
-         const protozero::Field& field, BoundInserter* inserter) {
+      [](const protozero::Field& field,
+         util::ProtoToArgsParser::Delegate& delegate) {
         return MaybeParseSourceLocation("chrome_memory_pressure_notification",
-                                        state, field, inserter);
+                                        field, delegate);
+      });
+
+  // Parse DebugAnnotations.
+  args_parser_.AddParsingOverrideForType(
+      ".perfetto.protos.DebugAnnotation",
+      [&](util::ProtoToArgsParser::ScopedNestedKeyContext& key,
+          const protozero::ConstBytes& data,
+          util::ProtoToArgsParser::Delegate& delegate) {
+        // Do not add "debug_annotations" to the final key.
+        key.RemoveFieldSuffix();
+        util::DebugAnnotationParser annotation_parser(args_parser_);
+        return annotation_parser.Parse(data, delegate);
       });
 
   for (uint16_t index : kReflectFields) {
diff --git a/src/trace_processor/importers/proto/track_event_parser.h b/src/trace_processor/importers/proto/track_event_parser.h
index b6e70a8..2b80701 100644
--- a/src/trace_processor/importers/proto/track_event_parser.h
+++ b/src/trace_processor/importers/proto/track_event_parser.h
@@ -24,10 +24,10 @@
 #include "perfetto/protozero/field.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
-#include "src/trace_processor/importers/proto/args_table_utils.h"
 #include "src/trace_processor/importers/proto/chrome_string_lookup.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/timestamped_trace_piece.h"
+#include "src/trace_processor/util/proto_to_args_parser.h"
 
 #include "protos/perfetto/trace/track_event/track_event.pbzero.h"
 
@@ -68,6 +68,9 @@
   void ParseChromeThreadDescriptor(UniqueTid, protozero::ConstBytes);
   void ParseCounterDescriptor(TrackId, protozero::ConstBytes);
 
+  // Reflection-based proto TrackEvent field parser.
+  util::ProtoToArgsParser args_parser_;
+
   TraceProcessorContext* context_;
   TrackEventTracker* track_event_tracker_;
 
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index fdae8f4..056d342 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -25,8 +25,8 @@
 #include "src/trace_processor/importers/proto/track_event_tracker.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/trace_sorter.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.h b/src/trace_processor/importers/systrace/systrace_line_parser.h
index c80bc7f..071ee31 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.h
@@ -19,9 +19,9 @@
 
 #include "perfetto/trace_processor/status.h"
 
+#include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/ftrace/rss_stat_tracker.h"
 #include "src/trace_processor/importers/systrace/systrace_line.h"
-#include "src/trace_processor/trace_parser.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
 namespace perfetto {
diff --git a/src/trace_processor/importers/systrace/systrace_parser.cc b/src/trace_processor/importers/systrace/systrace_parser.cc
index 02f1d14..b9655d4 100644
--- a/src/trace_processor/importers/systrace/systrace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_parser.cc
@@ -55,12 +55,20 @@
                                     uint32_t pid,
                                     int32_t flag,
                                     base::StringView name,
-                                    uint32_t tgid,
+                                    uint32_t /* tgid */,
                                     int64_t value) {
   systrace_utils::SystraceTracePoint point{};
   point.name = name;
-  point.tgid = tgid;
-  point.value = static_cast<double>(value);
+  point.value = value;
+
+  // Hardcode the tgid to 0 (i.e. no tgid available) because zero events can
+  // come from kernel threads and as we group kernel threads into the kthreadd
+  // process, we would want |point.tgid == kKthreaddPid|. However, we don't have
+  // acces to the ppid of this process so we have to not associate to any
+  // process and leave the resolution of process to other events.
+  // TODO(lalitm): remove this hack once we move kernel thread grouping to
+  // the UI.
+  point.tgid = 0;
 
   // The value of these constants can be found in the msm-google kernel.
   constexpr int32_t kSystraceEventBegin = 1 << 0;
@@ -100,7 +108,7 @@
   // the UI.
   point.tgid = 0;
 
-  point.value = static_cast<double>(value);
+  point.value = value;
   // Some versions of this trace point fill trace_type with one of (B/E/C),
   // others use the trace_begin boolean and only support begin/end events:
   if (trace_type == 0) {
@@ -156,7 +164,7 @@
     case 'S':
     case 'F': {
       StringId name_id = context_->storage->InternString(point.name);
-      int64_t cookie = static_cast<int64_t>(point.value);
+      int64_t cookie = point.value;
       UniquePid upid =
           context_->process_tracker->GetOrCreateProcess(point.tgid);
 
@@ -215,7 +223,8 @@
         // Promote ScreenState to its own top level counter.
         TrackId track =
             context_->track_tracker->InternGlobalCounterTrack(screen_state_id_);
-        context_->event_tracker->PushCounter(ts, point.value, track);
+        context_->event_tracker->PushCounter(
+            ts, static_cast<double>(point.value), track);
         return;
       }
 
@@ -235,7 +244,8 @@
         track_id =
             context_->track_tracker->InternProcessCounterTrack(name_id, upid);
       }
-      context_->event_tracker->PushCounter(ts, point.value, track_id);
+      context_->event_tracker->PushCounter(ts, static_cast<double>(point.value),
+                                           track_id);
     }
   }
 }
diff --git a/src/trace_processor/importers/systrace/systrace_parser.h b/src/trace_processor/importers/systrace/systrace_parser.h
index f069019..a369af9 100644
--- a/src/trace_processor/importers/systrace/systrace_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_parser.h
@@ -48,30 +48,24 @@
 
   static SystraceTracePoint C(uint32_t tgid,
                               base::StringView name,
-                              double value,
-                              base::StringView category_group = "") {
-    return SystraceTracePoint('C', tgid, std::move(name), value,
-                              category_group);
+                              int64_t value) {
+    return SystraceTracePoint('C', tgid, std::move(name), value);
   }
 
   static SystraceTracePoint S(uint32_t tgid,
                               base::StringView name,
-                              double value) {
-    return SystraceTracePoint('S', tgid, std::move(name), value);
+                              int64_t cookie) {
+    return SystraceTracePoint('S', tgid, std::move(name), cookie);
   }
 
   static SystraceTracePoint F(uint32_t tgid,
                               base::StringView name,
-                              double value) {
-    return SystraceTracePoint('F', tgid, std::move(name), value);
+                              int64_t cookie) {
+    return SystraceTracePoint('F', tgid, std::move(name), cookie);
   }
 
-  SystraceTracePoint(char p,
-                     uint32_t tg,
-                     base::StringView n,
-                     double v,
-                     base::StringView c = "")
-      : phase(p), tgid(tg), name(std::move(n)), value(v), category_group(c) {}
+  SystraceTracePoint(char p, uint32_t tg, base::StringView n, int64_t v)
+      : phase(p), tgid(tg), name(std::move(n)), value(v) {}
 
   // Phase can be one of B, E, C, S, F.
   char phase = '\0';
@@ -81,11 +75,8 @@
   // For phase = 'B' and phase = 'C' only.
   base::StringView name;
 
-  // For phase = 'C' only.
-  double value = 0;
-
-  // For phase = 'C' only (from Chrome).
-  base::StringView category_group;
+  // For phase = 'C' (counter value) and 'B', 'F' (async cookie).
+  int64_t value = 0;
 
   // Visible for unittesting.
   friend std::ostream& operator<<(std::ostream& os,
@@ -103,108 +94,94 @@
 // 4. C|1636|wq:monitor|0
 // 5. S|1636|frame_capture|123
 // 6. F|1636|frame_capture|456
+// Counters emitted by chromium can have a further "category group" appended
+// ("Blob" in the example below). We ignore the category group.
 // 7. C|3209|TransfersBytesPendingOnDisk-value|0|Blob
-// Visible for unittesting.
-inline SystraceParseResult ParseSystraceTracePoint(base::StringView str,
-                                                   SystraceTracePoint* out) {
-  const char* s = str.data();
-  size_t len = str.size();
+inline SystraceParseResult ParseSystraceTracePoint(
+    base::StringView str_untrimmed,
+    SystraceTracePoint* out) {
   *out = {};
 
-  // We don't support empty events.
-  if (len == 0)
-    return SystraceParseResult::kFailure;
-
-  constexpr const char* kClockSyncPrefix = "trace_event_clock_sync:";
-  if (len >= strlen(kClockSyncPrefix) &&
-      strncmp(kClockSyncPrefix, s, strlen(kClockSyncPrefix)) == 0)
-    return SystraceParseResult::kUnsupported;
-
-  char ph = s[0];
-  if (ph != 'B' && ph != 'E' && ph != 'C' && ph != 'S' && ph != 'F')
-    return SystraceParseResult::kFailure;
-
-  out->phase = ph;
-
-  // We only support E events with no arguments.
-  if (len == 1 && ph != 'E')
-    return SystraceParseResult::kFailure;
-
-  // If str matches '[BEC]\|[0-9]+[\|\n]?' set tgid_length to the length of
-  // the number. Otherwise return kFailure.
-  if (len >= 2 && s[1] != '|' && s[1] != '\n')
-    return SystraceParseResult::kFailure;
-
-  size_t tgid_length = 0;
-  for (size_t i = 2; i < len; i++) {
-    if (s[i] == '|' || s[i] == '\n') {
+  // Strip trailing \n and \0. StringViews are not null-terminated, but the
+  // writer could have appended a stray \0 depending on where the trace comes
+  // from.
+  size_t len = str_untrimmed.size();
+  for (; len > 0; --len) {
+    char last_char = str_untrimmed.at(len - 1);
+    if (last_char != '\n' && last_char != '\0')
       break;
-    }
-    if (s[i] < '0' || s[i] > '9')
-      return SystraceParseResult::kFailure;
-    tgid_length++;
   }
+  base::StringView str = str_untrimmed.substr(0, len);
 
-  // If len == 1, tgid_length will be 0 which will ensure we don't do
-  // an out of bounds read.
-  std::string tgid_str(s + 2, tgid_length);
-  out->tgid = base::StringToUInt32(tgid_str).value_or(0);
+  size_t off = 0;
 
-  switch (ph) {
-    case 'B': {
-      size_t name_index = 2 + tgid_length + 1;
-      out->name = base::StringView(
-          s + name_index, len - name_index - (s[len - 1] == '\n' ? 1 : 0));
-      if (out->name.empty())
+  // This function reads the next field up to the next '|', '\0' or end(). It
+  // advances |off| as it goes through fields.
+  auto read_next_field = [&off, &str, len]() {
+    for (size_t field_start = off;; ++off) {
+      char c = off >= len ? '\0' : str.at(off);
+      if (c == '|' || c == '\0') {
+        auto res = str.substr(field_start, off - field_start);
+        ++off;  // Eat the separator.
+        return res;
+      }
+    }
+  };
+
+  auto f0_phase = read_next_field();
+  if (PERFETTO_UNLIKELY(f0_phase.empty()))
+    return SystraceParseResult::kFailure;
+  out->phase = f0_phase.at(0);
+
+  auto f1_tgid = read_next_field();
+  auto opt_tgid = base::StringToUInt32(f1_tgid.ToStdString());
+  out->tgid = opt_tgid.value_or(0);
+  const bool has_tgid = opt_tgid.has_value();
+
+  switch (out->phase) {
+    case 'B': {  // Begin thread-scoped synchronous slice.
+      if (!has_tgid)
         return SystraceParseResult::kFailure;
+      auto f2_name = str.substr(off);  // It's fine even if |off| >= end().
+      if (f2_name.empty()) {
+        out->name = base::StringView("[empty slice name]");
+      } else {
+        out->name = f2_name;
+      }
       return SystraceParseResult::kSuccess;
     }
-    case 'E': {
+    case 'E':  // End thread-scoped synchronous slice.
+      // Some non-Android traces (Flutter) use just "E" (aosp/1244409). Allow
+      // empty TGID on end slices. By design they are thread-scoped anyways.
+      return SystraceParseResult::kSuccess;
+    case 'S':    // Begin of async slice.
+    case 'F': {  // End of async slice.
+      auto f2_name = read_next_field();
+      auto f3_cookie = read_next_field();
+      auto maybe_cookie = base::StringToInt64(f3_cookie.ToStdString());
+      if (PERFETTO_UNLIKELY(!has_tgid || f2_name.empty() || f3_cookie.empty() ||
+                            !maybe_cookie)) {
+        return SystraceParseResult::kFailure;
+      }
+      out->name = f2_name;
+      out->value = *maybe_cookie;
       return SystraceParseResult::kSuccess;
     }
-    case 'S':
-    case 'F':
-    case 'C': {
-      size_t name_index = 2 + tgid_length + 1;
-      base::Optional<size_t> name_length;
-      for (size_t i = name_index; i < len; i++) {
-        if (s[i] == '|') {
-          name_length = i - name_index;
-          break;
-        }
-      }
-      if (!name_length.has_value())
-        return SystraceParseResult::kFailure;
-      out->name = base::StringView(s + name_index, name_length.value());
-
-      size_t value_index = name_index + name_length.value() + 1;
-      size_t value_pipe = str.find('|', value_index);
-      size_t value_len = value_pipe == base::StringView::npos
-                             ? len - value_index
-                             : value_pipe - value_index;
-      if (value_len == 0)
-        return SystraceParseResult::kFailure;
-      if (s[value_index + value_len - 1] == '\n')
-        value_len--;
-      std::string value_str(s + value_index, value_len);
-      base::Optional<double> maybe_value = base::StringToDouble(value_str);
-      if (!maybe_value.has_value()) {
+    case 'C': {  // Counter.
+      auto f2_name = read_next_field();
+      auto f3_value = read_next_field();
+      auto maybe_value = base::StringToInt64(f3_value.ToStdString());
+      if (PERFETTO_UNLIKELY(!has_tgid || f2_name.empty() || f3_value.empty() ||
+                            !maybe_value)) {
         return SystraceParseResult::kFailure;
       }
-      out->value = maybe_value.value();
-
-      if (value_pipe != base::StringView::npos) {
-        size_t group_len = len - value_pipe - 1;
-        if (group_len == 0)
-          return SystraceParseResult::kFailure;
-        if (s[len - 1] == '\n')
-          group_len--;
-        out->category_group = base::StringView(s + value_pipe + 1, group_len);
-      }
-
+      out->name = f2_name;
+      out->value = *maybe_value;
       return SystraceParseResult::kSuccess;
     }
     default:
+      if (str.find("trace_event_clock_sync:") == 0)
+        return SystraceParseResult::kUnsupported;
       return SystraceParseResult::kFailure;
   }
 }
diff --git a/src/trace_processor/importers/systrace/systrace_parser_unittest.cc b/src/trace_processor/importers/systrace/systrace_parser_unittest.cc
index 5c65514..d0c89cf 100644
--- a/src/trace_processor/importers/systrace/systrace_parser_unittest.cc
+++ b/src/trace_processor/importers/systrace/systrace_parser_unittest.cc
@@ -38,10 +38,16 @@
   ASSERT_EQ(ParseSystraceTracePoint("||\n", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("||\n", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("B", &result), Result::kFailure);
+  ASSERT_EQ(ParseSystraceTracePoint("B\n", &result), Result::kFailure);
+  ASSERT_EQ(ParseSystraceTracePoint("C\n", &result), Result::kFailure);
+  ASSERT_EQ(ParseSystraceTracePoint("S\n", &result), Result::kFailure);
+  ASSERT_EQ(ParseSystraceTracePoint("F\n", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("C", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("S", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("F", &result), Result::kFailure);
-  ASSERT_EQ(ParseSystraceTracePoint("B|42|\n", &result), Result::kFailure);
+
+  ASSERT_EQ(ParseSystraceTracePoint("B|42|\n", &result), Result::kSuccess);
+  EXPECT_EQ(result, SystraceTracePoint::B(42, "[empty slice name]"));
 
   ASSERT_EQ(ParseSystraceTracePoint("B|1|foo", &result), Result::kSuccess);
   EXPECT_EQ(result, SystraceTracePoint::B(1, "foo"));
@@ -61,14 +67,13 @@
   ASSERT_EQ(ParseSystraceTracePoint("E|42", &result), Result::kSuccess);
   EXPECT_EQ(result, SystraceTracePoint::E(42));
 
-  ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|8", &result), Result::kSuccess);
   EXPECT_EQ(result, SystraceTracePoint::C(543, "foo", 8));
 
-  ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|8|", &result), Result::kFailure);
-  ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|8|group", &result),
-            Result::kSuccess);
-  EXPECT_EQ(result, SystraceTracePoint::C(543, "foo", 8, "group"));
+  ASSERT_EQ(
+      ParseSystraceTracePoint("C|543|foo|8|chromium_group_ignored", &result),
+      Result::kSuccess);
+  EXPECT_EQ(result, SystraceTracePoint::C(543, "foo", 8));
 
   ASSERT_EQ(ParseSystraceTracePoint("S|", &result), Result::kFailure);
 
diff --git a/src/trace_processor/importers/systrace/systrace_trace_parser.h b/src/trace_processor/importers/systrace/systrace_trace_parser.h
index 1db04ee..bcb5ea8 100644
--- a/src/trace_processor/importers/systrace/systrace_trace_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_trace_parser.h
@@ -20,7 +20,7 @@
 #include <deque>
 #include <regex>
 
-#include "src/trace_processor/chunked_trace_reader.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/systrace/systrace_line_parser.h"
 #include "src/trace_processor/importers/systrace/systrace_line_tokenizer.h"
 #include "src/trace_processor/storage/trace_storage.h"
diff --git a/src/trace_processor/iterator_impl.cc b/src/trace_processor/iterator_impl.cc
index 6539e9a..c0b3aac 100644
--- a/src/trace_processor/iterator_impl.cc
+++ b/src/trace_processor/iterator_impl.cc
@@ -34,7 +34,7 @@
       db_(db),
       stmt_(std::move(stmt)),
       column_count_(column_count),
-      status_(status),
+      status_(std::move(status)),
       sql_stats_row_(sql_stats_row) {}
 
 IteratorImpl::~IteratorImpl() {
diff --git a/src/trace_processor/metrics/BUILD.gn b/src/trace_processor/metrics/BUILD.gn
index 08f9408..a1e440d 100644
--- a/src/trace_processor/metrics/BUILD.gn
+++ b/src/trace_processor/metrics/BUILD.gn
@@ -30,6 +30,7 @@
   "android/android_mem.sql",
   "android/android_mem_unagg.sql",
   "android/android_ion.sql",
+  "android/composer_execution.sql",
   "android/composition_layers.sql",
   "android/frame_missed.sql",
   "android/android_jank.sql",
@@ -46,7 +47,6 @@
   "android/display_metrics.sql",
   "android/g2d.sql",
   "android/g2d_duration.sql",
-  "android/heap_profile_callsites.sql",
   "android/hsc_startups.sql",
   "android/android_hwcomposer.sql",
   "android/android_hwui_metric.sql",
@@ -63,6 +63,7 @@
   "android/android_sysui_cuj.sql",
   "android/process_counter_span_view.sql",
   "android/global_counter_span_view.sql",
+  "android/thread_counter_span_view.sql",
   "android/unsymbolized_frames.sql",
   "chrome/actual_power_by_category.sql",
   "chrome/actual_power_by_rail_mode.sql",
diff --git a/src/trace_processor/metrics/android/android_hwcomposer.sql b/src/trace_processor/metrics/android/android_hwcomposer.sql
index 6e7d891..ad5019f 100644
--- a/src/trace_processor/metrics/android/android_hwcomposer.sql
+++ b/src/trace_processor/metrics/android/android_hwcomposer.sql
@@ -43,17 +43,86 @@
   'output', 'sf_cached_layers'
 );
 
+SELECT RUN_METRIC(
+  'android/composer_execution.sql',
+  'output', 'hwc_execution_spans'
+);
+
+
+SELECT RUN_METRIC('android/thread_counter_span_view.sql',
+  'table_name', 'dpu_vote_clock',
+  'counter_name', 'dpu_vote_clock'
+);
+
+SELECT RUN_METRIC('android/thread_counter_span_view.sql',
+  'table_name', 'dpu_vote_avg_bw',
+  'counter_name', 'dpu_vote_avg_bw'
+);
+
+SELECT RUN_METRIC('android/thread_counter_span_view.sql',
+  'table_name', 'dpu_vote_peak_bw',
+  'counter_name', 'dpu_vote_peak_bw'
+);
+
+DROP VIEW IF EXISTS dpu_vote_thread;
+CREATE VIEW dpu_vote_thread AS
+SELECT DISTINCT s.utid, t.tid
+FROM (
+  SELECT utid FROM dpu_vote_clock_span
+  UNION
+  SELECT utid FROM dpu_vote_avg_bw_span
+  UNION
+  SELECT utid FROM dpu_vote_peak_bw_span
+) s JOIN thread t ON s.utid = t.utid;
+
+DROP VIEW IF EXISTS dpu_vote_metrics;
+CREATE VIEW dpu_vote_metrics AS
+SELECT AndroidHwcomposerMetrics_DpuVoteMetrics(
+  'tid', tid,
+  'avg_dpu_vote_clock',
+      (SELECT SUM(dpu_vote_clock_val * dur) / SUM(dur)
+      FROM dpu_vote_clock_span s WHERE s.utid = t.utid),
+  'avg_dpu_vote_avg_bw',
+      (SELECT SUM(dpu_vote_avg_bw_val * dur) / SUM(dur)
+      FROM dpu_vote_avg_bw_span s WHERE s.utid = t.utid),
+  'avg_dpu_vote_peak_bw',
+      (SELECT SUM(dpu_vote_peak_bw_val * dur) / SUM(dur)
+      FROM dpu_vote_peak_bw_span s WHERE s.utid = t.utid)
+) AS proto
+FROM dpu_vote_thread t
+ORDER BY tid;
+
 DROP VIEW IF EXISTS android_hwcomposer_output;
 CREATE VIEW android_hwcomposer_output AS
 SELECT AndroidHwcomposerMetrics(
-    'composition_total_layers', (SELECT AVG(value)
-                            FROM total_layers),
-    'composition_dpu_layers', (SELECT AVG(value)
-                            FROM dpu_layers),
-    'composition_gpu_layers', (SELECT AVG(value)
-                            FROM gpu_layers),
-    'composition_dpu_cached_layers', (SELECT AVG(value)
-                            FROM dpu_cached_layers),
-    'composition_sf_cached_layers', (SELECT AVG(value)
-                            FROM sf_cached_layers)
+  'composition_total_layers', (SELECT AVG(value) FROM total_layers),
+  'composition_dpu_layers', (SELECT AVG(value) FROM dpu_layers),
+  'composition_gpu_layers', (SELECT AVG(value) FROM gpu_layers),
+  'composition_dpu_cached_layers', (SELECT AVG(value) FROM dpu_cached_layers),
+  'composition_sf_cached_layers', (SELECT AVG(value) FROM sf_cached_layers),
+  'skipped_validation_count',
+      (SELECT COUNT(*) FROM hwc_execution_spans
+      WHERE validation_type = 'skipped_validation'),
+  'unskipped_validation_count',
+      (SELECT COUNT(*) FROM hwc_execution_spans
+      WHERE validation_type = 'unskipped_validation'),
+  'separated_validation_count',
+      (SELECT COUNT(*) FROM hwc_execution_spans
+      WHERE validation_type = 'separated_validation'),
+  'unknown_validation_count',
+      (SELECT COUNT(*) FROM hwc_execution_spans
+      WHERE validation_type = 'unknown'),
+  'avg_all_execution_time_ms',
+      (SELECT AVG(execution_time_ns) / 1e6 FROM hwc_execution_spans
+      WHERE validation_type != 'unknown'),
+  'avg_skipped_execution_time_ms',
+      (SELECT AVG(execution_time_ns) / 1e6 FROM hwc_execution_spans
+      WHERE validation_type = 'skipped_validation'),
+  'avg_unskipped_execution_time_ms',
+      (SELECT AVG(execution_time_ns) / 1e6 FROM hwc_execution_spans
+      WHERE validation_type = 'unskipped_validation'),
+  'avg_separated_execution_time_ms',
+      (SELECT AVG(execution_time_ns) / 1e6 FROM hwc_execution_spans
+      WHERE validation_type = 'separated_validation'),
+  'dpu_vote_metrics', (SELECT RepeatedField(proto) FROM dpu_vote_metrics)
 );
diff --git a/src/trace_processor/metrics/android/android_jank.sql b/src/trace_processor/metrics/android/android_jank.sql
index e6a0cd9..44f7d81 100644
--- a/src/trace_processor/metrics/android/android_jank.sql
+++ b/src/trace_processor/metrics/android/android_jank.sql
@@ -265,9 +265,10 @@
   'slice' as track_type,
   process_name || ' warnings' as track_name,
   ts,
-  dur,
-  alert_name as slice_name
-FROM android_jank_alerts;
+  0 as dur,
+  group_concat(alert_name) as slice_name
+FROM android_jank_alerts
+GROUP BY track_type, track_name, ts;
 
 DROP VIEW IF EXISTS android_jank_output;
 CREATE VIEW android_jank_output AS
diff --git a/src/trace_processor/metrics/android/android_startup.sql b/src/trace_processor/metrics/android/android_startup.sql
index 364a635..d347a50 100644
--- a/src/trace_processor/metrics/android/android_startup.sql
+++ b/src/trace_processor/metrics/android/android_startup.sql
@@ -127,6 +127,8 @@
   launch_threads.launch_id AS launch_id,
   launch_threads.utid AS utid,
   launch_threads.thread_name AS thread_name,
+  slice.id AS slice_id,
+  slice.arg_set_id AS arg_set_id,
   slice.name AS slice_name,
   slice.ts AS slice_ts,
   slice.dur AS slice_dur
@@ -143,14 +145,18 @@
   'activityRestart',
   'activityResume',
   'inflate',
-  'ResourcesManager#getResources')
+  'ResourcesManager#getResources',
+  'binder transaction')
   OR slice.name LIKE 'performResume:%'
   OR slice.name LIKE 'performCreate:%'
   OR slice.name LIKE 'location=% status=% filter=% reason=%'
   OR slice.name LIKE 'OpenDexFilesFromOat%'
   OR slice.name LIKE 'VerifyClass%'
   OR slice.name LIKE 'Choreographer#doFrame%'
-  OR slice.name LIKE 'JIT compiling%';
+  OR slice.name LIKE 'JIT compiling%'
+  OR slice.name LIKE '%mark sweep GC'
+  OR slice.name LIKE '%concurrent copying GC'
+  OR slice.name LIKE '%semispace GC';
 
 DROP TABLE IF EXISTS main_process_slice;
 CREATE TABLE main_process_slice AS
@@ -160,6 +166,9 @@
     WHEN slice_name LIKE 'OpenDexFilesFromOat%' THEN 'OpenDexFilesFromOat'
     WHEN slice_name LIKE 'VerifyClass%' THEN 'VerifyClass'
     WHEN slice_name LIKE 'JIT compiling%' THEN 'JIT compiling'
+    WHEN slice_name LIKE '%mark sweep GC' THEN 'GC'
+    WHEN slice_name LIKE '%concurrent copying GC' THEN 'GC'
+    WHEN slice_name LIKE '%semispace GC' THEN 'GC'
     ELSE slice_name
   END AS name,
   AndroidStartupMetric_Slice(
@@ -206,6 +215,92 @@
   slice.track_id = thread_track.id
   AND slice.ts BETWEEN l.ts AND l.ts + l.dur);
 
+DROP VIEW IF EXISTS gc_slices;
+CREATE VIEW gc_slices AS
+  SELECT
+    slice_ts AS ts,
+    slice_dur AS dur,
+    utid,
+    launch_id
+  FROM main_process_slice_unaggregated
+  WHERE (
+    slice_name LIKE '%mark sweep GC'
+    OR slice_name LIKE '%concurrent copying GC'
+    OR slice_name LIKE '%semispace GC');
+
+DROP TABLE IF EXISTS gc_slices_by_state;
+CREATE VIRTUAL TABLE gc_slices_by_state
+USING SPAN_JOIN(gc_slices PARTITIONED utid, thread_state_extended PARTITIONED utid);
+
+DROP TABLE IF EXISTS gc_slices_by_state_materialized;
+CREATE TABLE gc_slices_by_state_materialized AS
+SELECT launch_id, SUM(dur) as sum_dur
+FROM gc_slices_by_state
+WHERE state = 'Running'
+GROUP BY launch_id;
+
+DROP TABLE IF EXISTS launch_threads_cpu;
+CREATE VIRTUAL TABLE launch_threads_cpu
+USING SPAN_JOIN(launch_threads PARTITIONED utid, thread_state_extended PARTITIONED utid);
+
+DROP TABLE IF EXISTS launch_threads_cpu_materialized;
+CREATE TABLE launch_threads_cpu_materialized AS
+SELECT launch_id, SUM(dur) as sum_dur
+FROM launch_threads_cpu
+WHERE thread_name = 'Jit thread pool' AND state = 'Running'
+GROUP BY launch_id;
+
+DROP TABLE IF EXISTS activity_names_materialized;
+CREATE TABLE activity_names_materialized AS
+SELECT launch_id, slice_name, slice_ts
+FROM main_process_slice_unaggregated
+WHERE (slice_name LIKE 'performResume:%' OR slice_name LIKE 'performCreate:%');
+
+DROP TABLE IF EXISTS jit_compiled_methods_materialized;
+CREATE TABLE jit_compiled_methods_materialized AS
+SELECT
+  launch_id,
+  COUNT(1) as count
+FROM main_process_slice_unaggregated
+WHERE
+  slice_name LIKE 'JIT compiling%'
+  AND thread_name = 'Jit thread pool'
+GROUP BY launch_id;
+
+DROP TABLE IF EXISTS long_binder_transactions;
+CREATE TABLE long_binder_transactions AS
+SELECT
+  slice_id, arg_set_id, launch_id, slice_dur, thread_name
+FROM
+  main_process_slice_unaggregated
+WHERE
+  slice_name = 'binder transaction'
+  AND slice_dur >= 5e7;
+
+DROP TABLE IF EXISTS binder_to_destination_process;
+CREATE TABLE binder_to_destination_process AS
+SELECT
+  s.slice_id, process.name AS destination_process
+FROM long_binder_transactions s
+JOIN args USING(arg_set_id)
+JOIN process ON(args.int_value = process.pid)
+WHERE args.key = 'destination process';
+
+-- Enriched binder transactions.
+DROP TABLE IF EXISTS long_binder_transactions_enriched;
+CREATE TABLE long_binder_transactions_enriched AS
+SELECT
+  s.launch_id,
+  s.slice_dur,
+  s.thread_name,
+  EXTRACT_ARG(s.arg_set_id, 'destination name') AS destination_thread,
+  bdp.destination_process,
+  EXTRACT_ARG(s.arg_set_id, 'flags') AS flags,
+  EXTRACT_ARG(s.arg_set_id, 'code') AS code,
+  EXTRACT_ARG(s.arg_set_id, 'data_size') AS data_size
+FROM long_binder_transactions s
+LEFT JOIN binder_to_destination_process bdp USING(slice_id);
+
 DROP VIEW IF EXISTS startup_view;
 CREATE VIEW startup_view AS
 SELECT
@@ -213,30 +308,43 @@
     'startup_id', launches.id,
     'package_name', launches.package,
     'process_name', (
-      SELECT name FROM process
-      WHERE upid IN (
-        SELECT upid FROM launch_processes p
-        WHERE p.launch_id = launches.id
-        LIMIT 1
-      )
+      SELECT p.name
+      FROM launch_processes lp
+      JOIN process p USING (upid)
+      WHERE lp.launch_id = launches.id
+      LIMIT 1
     ),
     'process', (
-      SELECT metadata FROM process_metadata
-      WHERE upid IN (
-        SELECT upid FROM launch_processes p
-        WHERE p.launch_id = launches.id
-        LIMIT 1
-      )
+      SELECT m.metadata
+      FROM process_metadata m
+      JOIN launch_processes p USING (upid)
+      WHERE p.launch_id = launches.id
+      LIMIT 1
     ),
     'activities', (
       SELECT RepeatedField(AndroidStartupMetric_Activity(
-        'name', (SELECT STR_SPLIT(s.name, ':', 1)),
-        'method', (SELECT STR_SPLIT(s.name, ':', 0)),
-        'slice', s.slice_proto
+        'name', (SELECT STR_SPLIT(s.slice_name, ':', 1)),
+        'method', (SELECT STR_SPLIT(s.slice_name, ':', 0)),
+        'ts_method_start', s.slice_ts
       ))
-      FROM main_process_slice s
+      FROM activity_names_materialized s
       WHERE s.launch_id = launches.id
-      AND (name LIKE 'performResume:%' OR name LIKE 'performCreate:%')
+    ),
+    'long_binder_transactions', (
+      SELECT RepeatedField(AndroidStartupMetric_BinderTransaction(
+        'duration', AndroidStartupMetric_Slice(
+          'dur_ns', lbt.slice_dur,
+          'dur_ms', lbt.slice_dur / 1e6
+        ),
+        'thread', lbt.thread_name,
+        'destination_thread', lbt.destination_thread,
+        'destination_process', lbt.destination_process,
+        'flags', lbt.flags,
+        'code', lbt.code,
+        'data_size', lbt.data_size
+      ))
+      FROM long_binder_transactions_enriched lbt
+      WHERE lbt.launch_id = launches.id
     ),
     'zygote_new_process', EXISTS(SELECT TRUE FROM zygote_forks_by_id WHERE id = launches.id),
     'activity_hosting_process_count', (
@@ -395,23 +503,32 @@
         WHERE s.launch_id = launches.id AND name = 'VerifyClass'
       ),
       'jit_compiled_methods', (
-        SELECT SUM(1)
-        FROM main_process_slice_unaggregated
-        WHERE slice_name LIKE 'JIT compiling%'
-          AND thread_name = 'Jit thread pool'
+        SELECT count
+        FROM jit_compiled_methods_materialized s
+        WHERE s.launch_id = launches.id
       ),
       'time_jit_thread_pool_on_cpu', (
         SELECT
-        NULL_IF_EMPTY(AndroidStartupMetric_Slice(
-          'dur_ns', SUM(states.dur),
-          'dur_ms', SUM(states.dur) / 1e6))
-        FROM launch_threads
-        JOIN thread_state_extended states USING(utid)
-        WHERE
-          launch_threads.launch_id = launches.id
-          AND launch_threads.thread_name = 'Jit thread pool'
-          AND states.state = 'Running'
-          AND states.ts BETWEEN launch_threads.ts AND launch_threads.ts + launch_threads.dur
+          NULL_IF_EMPTY(AndroidStartupMetric_Slice(
+            'dur_ns', sum_dur,
+            'dur_ms', sum_dur / 1e6
+          ))
+        FROM launch_threads_cpu_materialized
+        WHERE launch_id = launches.id
+      ),
+      'time_gc_total', (
+        SELECT slice_proto
+        FROM main_process_slice s
+        WHERE s.launch_id = launches.id AND name = 'GC'
+      ),
+      'time_gc_on_cpu', (
+        SELECT
+          NULL_IF_EMPTY(AndroidStartupMetric_Slice(
+            'dur_ns', sum_dur,
+            'dur_ms', sum_dur / 1e6
+          ))
+        FROM gc_slices_by_state_materialized
+        WHERE launch_id = launches.id
       )
     ),
     'hsc', (
diff --git a/src/trace_processor/metrics/android/android_startup_launches.sql b/src/trace_processor/metrics/android/android_startup_launches.sql
index 2cfb4b9..c5829d7 100644
--- a/src/trace_processor/metrics/android/android_startup_launches.sql
+++ b/src/trace_processor/metrics/android/android_startup_launches.sql
@@ -104,11 +104,11 @@
 DROP TABLE IF EXISTS launch_processes;
 CREATE TABLE launch_processes(launch_id INT, upid BIG INT);
 
--- We make the (not always correct) simplification that process == package
 INSERT INTO launch_processes
 SELECT launches.id, process.upid
 FROM launches
-  JOIN process ON launches.package = process.name
+  LEFT JOIN package_list ON (launches.package = package_list.package_name)
+  JOIN process ON (launches.package = process.name OR process.uid = package_list.uid)
   JOIN thread ON (process.upid = thread.upid AND process.pid = thread.tid)
 WHERE (process.start_ts IS NULL OR process.start_ts < launches.ts_end)
 AND (thread.end_ts IS NULL OR thread.end_ts > launches.ts_end)
diff --git a/src/trace_processor/metrics/android/android_surfaceflinger.sql b/src/trace_processor/metrics/android/android_surfaceflinger.sql
index ccf0b4b..6bd6d70 100644
--- a/src/trace_processor/metrics/android/android_surfaceflinger.sql
+++ b/src/trace_processor/metrics/android/android_surfaceflinger.sql
@@ -37,13 +37,67 @@
   ts,
   dur,
   'Frame missed' AS slice_name
-FROM frame_missed;
+FROM frame_missed
+WHERE value = 1 AND ts IS NOT NULL;
+
+DROP VIEW IF EXISTS surfaceflinger_track;
+CREATE VIEW surfaceflinger_track AS
+SELECT tr.id AS track_id, t.utid, t.tid
+FROM process p JOIN thread t ON p.upid = t.upid
+     JOIN thread_track tr ON tr.utid = t.utid
+WHERE p.cmdline='/system/bin/surfaceflinger';
+
+DROP VIEW IF EXISTS gpu_waiting_start;
+CREATE VIEW gpu_waiting_start AS
+SELECT
+  CAST(SUBSTRING(s.name, 28, LENGTH(s.name)) AS UINT32) AS fence_id,
+  ts AS start_ts
+FROM slices s JOIN surfaceflinger_track t ON s.track_id = t.track_id
+WHERE s.name LIKE 'Trace GPU completion fence %';
+
+DROP VIEW IF EXISTS gpu_waiting_end;
+CREATE VIEW gpu_waiting_end AS
+SELECT
+  CAST(SUBSTRING(s.name, 28, LENGTH(s.name)) AS UINT32) AS fence_id,
+  dur,
+  ts+dur AS end_ts
+FROM slices s JOIN surfaceflinger_track t ON s.track_id = t.track_id
+WHERE s.name LIKE 'waiting for GPU completion %';
+
+DROP VIEW IF EXISTS gpu_waiting_span;
+CREATE VIEW gpu_waiting_span AS
+SELECT
+  fence_id,
+  ts,
+  dur
+FROM (
+  SELECT
+    fence_id,
+    ts,
+    LEAD(ts) OVER (ORDER BY fence_id, event_type) - ts AS dur,
+    LEAD(fence_id) OVER (ORDER BY fence_id, event_type) AS next_fence_id,
+    event_type
+  FROM (
+    SELECT fence_id, start_ts AS ts, 0 AS event_type FROM gpu_waiting_start
+    UNION
+    SELECT fence_id, end_ts AS ts, 1 AS event_type FROM gpu_waiting_end
+  )
+  ORDER BY fence_id, event_type
+)
+WHERE event_type = 0 AND fence_id = next_fence_id;
 
 DROP VIEW IF EXISTS android_surfaceflinger_output;
 CREATE VIEW android_surfaceflinger_output AS
 SELECT
   AndroidSurfaceflingerMetric(
-    'missed_frames', (SELECT COUNT(1) FROM frame_missed),
-    'missed_hwc_frames', (SELECT COUNT(1) FROM hwc_frame_missed),
-    'missed_gpu_frames', (SELECT COUNT(1) FROM gpu_frame_missed)
+    'missed_frames', (SELECT COUNT(1) FROM frame_missed WHERE value=1),
+    'missed_hwc_frames', (SELECT COUNT(1) FROM hwc_frame_missed WHERE value=1),
+    'missed_gpu_frames', (SELECT COUNT(1) FROM gpu_frame_missed WHERE value=1),
+    'missed_frame_rate', (SELECT AVG(value) FROM frame_missed),
+    'missed_hwc_frame_rate', (SELECT AVG(value) FROM hwc_frame_missed),
+    'missed_gpu_frame_rate', (SELECT AVG(value) FROM gpu_frame_missed),
+    'gpu_invocations', (SELECT COUNT(1) FROM gpu_waiting_end),
+    'avg_gpu_waiting_dur_ms', (SELECT AVG(dur)/1e6 FROM gpu_waiting_span),
+    'total_non_empty_gpu_waiting_dur_ms',
+        (SELECT SUM(dur)/1e6 FROM gpu_waiting_end)
   );
diff --git a/src/trace_processor/metrics/android/android_sysui_cuj.sql b/src/trace_processor/metrics/android/android_sysui_cuj.sql
index 811683d..e5b4079 100644
--- a/src/trace_processor/metrics/android/android_sysui_cuj.sql
+++ b/src/trace_processor/metrics/android/android_sysui_cuj.sql
@@ -43,41 +43,73 @@
   'table_name_prefix', 'android_sysui_cuj',
   'process_allowlist_table', 'android_sysui_cuj_last_cuj');
 
+DROP VIEW IF EXISTS android_sysui_cuj_thread;
+CREATE VIEW android_sysui_cuj_thread AS
+SELECT
+  process.name as process_name,
+  thread.utid,
+  thread.name
+FROM thread
+JOIN android_sysui_cuj_last_cuj process USING (upid);
+
+DROP VIEW IF EXISTS android_sysui_cuj_slices_in_cuj;
+CREATE VIEW android_sysui_cuj_slices_in_cuj AS
+SELECT
+  process_name,
+  thread.utid,
+  thread.name as thread_name,
+  slice.*,
+  ts + slice.dur AS ts_end
+FROM slice
+JOIN thread_track ON slice.track_id = thread_track.id
+JOIN android_sysui_cuj_thread thread USING (utid)
+JOIN android_sysui_cuj_last_cuj last_cuj
+ON ts + slice.dur >= last_cuj.ts_start AND ts <= last_cuj.ts_end
+WHERE slice.dur > 0;
+
 DROP TABLE IF EXISTS android_sysui_cuj_main_thread_slices_in_cuj;
 CREATE TABLE android_sysui_cuj_main_thread_slices_in_cuj AS
 SELECT slices.* FROM android_sysui_cuj_main_thread_slices slices
 JOIN android_sysui_cuj_last_cuj last_cuj
-ON ts >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
+ON ts + slices.dur >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
 
 DROP TABLE IF EXISTS android_sysui_cuj_do_frame_slices_in_cuj;
 CREATE TABLE android_sysui_cuj_do_frame_slices_in_cuj AS
 SELECT slices.* FROM android_sysui_cuj_do_frame_slices slices
 JOIN android_sysui_cuj_last_cuj last_cuj
-ON ts >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
+ON ts + slices.dur >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
 
 DROP TABLE IF EXISTS android_sysui_cuj_render_thread_slices_in_cuj;
 CREATE TABLE android_sysui_cuj_render_thread_slices_in_cuj AS
 SELECT slices.* FROM android_sysui_cuj_render_thread_slices slices
 JOIN android_sysui_cuj_last_cuj last_cuj
-ON ts >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
+ON ts + slices.dur >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
 
 DROP TABLE IF EXISTS android_sysui_cuj_draw_frame_slices_in_cuj;
 CREATE TABLE android_sysui_cuj_draw_frame_slices_in_cuj AS
 SELECT slices.* FROM android_sysui_cuj_draw_frame_slices slices
 JOIN android_sysui_cuj_last_cuj last_cuj
-ON ts >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
+ON ts + slices.dur >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
 
 DROP TABLE IF EXISTS android_sysui_cuj_hwc_release_slices_in_cuj;
 CREATE TABLE android_sysui_cuj_hwc_release_slices_in_cuj AS
 SELECT slices.* FROM android_sysui_cuj_hwc_release_slices slices
 JOIN android_sysui_cuj_last_cuj last_cuj
-ON ts >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
+ON ts + slices.dur >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
 
 DROP TABLE IF EXISTS android_sysui_cuj_gpu_completion_slices_in_cuj;
 CREATE TABLE android_sysui_cuj_gpu_completion_slices_in_cuj AS
 SELECT slices.* FROM android_sysui_cuj_gpu_completion_slices slices
 JOIN android_sysui_cuj_last_cuj last_cuj
-ON ts >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
+ON ts + slices.dur >= last_cuj.ts_start AND ts <= last_cuj.ts_end;
+
+DROP TABLE IF EXISTS android_sysui_cuj_jit_slices;
+CREATE TABLE android_sysui_cuj_jit_slices AS
+SELECT *
+FROM android_sysui_cuj_slices_in_cuj
+WHERE thread_name LIKE 'Jit thread pool%'
+AND name LIKE 'JIT compiling%'
+AND parent_id IS NULL;
 
 DROP TABLE IF EXISTS android_sysui_cuj_frame_timeline_events;
 CREATE TABLE android_sysui_cuj_frame_timeline_events AS
@@ -85,6 +117,7 @@
     expected.ts as ts_expected,
     expected.dur as dur_expected,
     expected.layer_name as layer_name,
+    actual.name as vsync,
     actual.ts as ts_actual,
     actual.dur as dur_actual,
     actual.jank_type LIKE '%App Deadline Missed%' as app_missed,
@@ -93,8 +126,6 @@
   FROM expected_frame_timeline_slice expected
   JOIN android_sysui_cuj_last_cuj cuj
     ON expected.upid = cuj.upid
-    AND expected.ts + expected.dur > cuj.ts_start
-    AND expected.ts < cuj.ts_end
   JOIN actual_frame_timeline_slice actual
     ON expected.surface_frame_token = actual.surface_frame_token
     AND expected.upid = actual.upid
@@ -128,7 +159,7 @@
       MAX(gcs_rt.gcs_ts_end) as gcs_ts_end
     FROM android_sysui_cuj_do_frame_slices_in_cuj mts
     JOIN android_sysui_cuj_draw_frame_slices_in_cuj rts
-      ON mts.ts < rts.ts AND mts.ts_end >= rts.ts
+      ON mts.vsync = rts.vsync
     LEFT JOIN gcs_to_rt_match gcs_rt ON gcs_rt.rts_ts = rts.ts
     GROUP BY mts.ts, mts.ts_end, mts.dur
   )
@@ -148,32 +179,22 @@
     COUNT(DISTINCT(gcs_rt.gcs_ts)) as gpu_completions
   FROM frame_boundaries f
   JOIN android_sysui_cuj_draw_frame_slices_in_cuj rts
-    ON f.mts_ts < rts.ts AND f.mts_ts_end >= rts.ts
+    ON f.vsync = rts.vsync
   LEFT JOIN gcs_to_rt_match gcs_rt
     ON rts.ts = gcs_rt.rts_ts
   LEFT JOIN android_sysui_cuj_hwc_release_slices_in_cuj hwc USING (idx)
   GROUP BY f.mts_ts
   HAVING gpu_completions >= 1;
 
--- TODO(marcinoc): This matching does not work well. Fix by using VSYNC id.
-DROP TABLE IF EXISTS android_sysui_cuj_frame_timeline_match;
-CREATE TABLE android_sysui_cuj_frame_timeline_match AS
-  SELECT f.frame_number, MAX(fte.ts_actual) as ts_actual_match
-  FROM android_sysui_cuj_frames f
-  JOIN android_sysui_cuj_frame_timeline_events fte
-    ON f.ts_main_thread_start >= fte.ts_actual
-  GROUP BY f.frame_number;
-
 DROP TABLE IF EXISTS android_sysui_cuj_missed_frames;
 CREATE TABLE android_sysui_cuj_missed_frames AS
   SELECT
     f.*,
     (SELECT MAX(fte.app_missed)
      FROM android_sysui_cuj_frame_timeline_events fte
-     WHERE match.ts_actual_match = fte.ts_actual
+     WHERE f.vsync = fte.vsync
      AND fte.on_time_finish = 0) as app_missed
-  FROM android_sysui_cuj_frames f
-  JOIN android_sysui_cuj_frame_timeline_match match USING (frame_number);
+  FROM android_sysui_cuj_frames f;
 
 DROP VIEW IF EXISTS android_sysui_cuj_frame_main_thread_bounds;
 CREATE VIEW android_sysui_cuj_frame_main_thread_bounds AS
@@ -243,9 +264,9 @@
 DROP TABLE IF EXISTS android_sysui_cuj_sf_jank_causes;
 CREATE TABLE android_sysui_cuj_sf_jank_causes AS
   WITH RECURSIVE split_jank_type(frame_number, jank_cause, remainder) AS (
-    SELECT match.frame_number, "", fte.jank_type || ","
-    FROM android_sysui_cuj_frame_timeline_match match
-    JOIN android_sysui_cuj_frame_timeline_events fte ON match.ts_actual_match = fte.ts_actual
+    SELECT f.frame_number, "", fte.jank_type || ","
+    FROM android_sysui_cuj_frames f
+    JOIN android_sysui_cuj_frame_timeline_events fte ON f.vsync = fte.vsync
     UNION ALL SELECT
     frame_number,
     STR_SPLIT(remainder, ",", 0) AS jank_cause,
@@ -254,9 +275,21 @@
     WHERE remainder <> "")
   SELECT frame_number, jank_cause
   FROM split_jank_type
-  WHERE jank_cause NOT IN ('', 'App Deadline Missed', 'None')
+  WHERE jank_cause NOT IN ('', 'App Deadline Missed', 'None', 'Buffer Stuffing')
   ORDER BY frame_number ASC;
 
+DROP TABLE IF EXISTS android_sysui_cuj_missed_frames_hwui_times;
+CREATE TABLE android_sysui_cuj_missed_frames_hwui_times AS
+SELECT
+  *,
+  ts_main_thread_start AS ts,
+  ts_render_thread_end - ts_main_thread_start AS dur
+FROM android_sysui_cuj_missed_frames;
+
+DROP TABLE IF EXISTS android_sysui_cuj_jit_slices_join_table;
+CREATE VIRTUAL TABLE android_sysui_cuj_jit_slices_join_table
+USING span_join(android_sysui_cuj_missed_frames_hwui_times partitioned frame_number, android_sysui_cuj_jit_slices);
+
 DROP TABLE IF EXISTS android_sysui_cuj_jank_causes;
 CREATE TABLE android_sysui_cuj_jank_causes AS
   SELECT
@@ -354,6 +387,16 @@
     AND mts.dur + rts.dur > 15000000
 
   UNION ALL
+  SELECT
+  f.frame_number,
+  'JIT compiling' as jank_cause
+  FROM android_sysui_cuj_missed_frames f
+  JOIN android_sysui_cuj_jit_slices_join_table jit USING (frame_number)
+  WHERE f.app_missed
+  GROUP BY f.frame_number
+  HAVING SUM(jit.dur) > 8000000
+
+  UNION ALL
   SELECT frame_number, jank_cause FROM android_sysui_cuj_sf_jank_causes
   GROUP BY frame_number, jank_cause;
 
diff --git a/src/trace_processor/metrics/android/android_thread_time_in_state.sql b/src/trace_processor/metrics/android/android_thread_time_in_state.sql
index 9b5ca40..ace04ed 100644
--- a/src/trace_processor/metrics/android/android_thread_time_in_state.sql
+++ b/src/trace_processor/metrics/android/android_thread_time_in_state.sql
@@ -21,41 +21,44 @@
 CREATE TABLE android_thread_time_in_state_base AS
 SELECT
   base.*,
-  core_type_per_cpu.core_type core_type
+  IFNULL(core_type_per_cpu.core_type, 'unknown') core_type
 FROM (
   SELECT
     ts,
     utid,
-    EXTRACT_ARG(counter.arg_set_id, 'time_in_state_cpu_id') AS cpu,
+    EXTRACT_ARG(counter.arg_set_id, 'time_in_state_cpu_id') AS
+        time_in_state_cpu,
     EXTRACT_ARG(counter.arg_set_id, 'freq') AS freq,
     CAST(value AS INT) AS runtime_ms_counter
   FROM counter
   JOIN thread_counter_track ON (counter.track_id = thread_counter_track.id)
   WHERE thread_counter_track.name = 'time_in_state'
 ) base
-JOIN core_type_per_cpu USING (cpu);
+LEFT JOIN core_type_per_cpu ON (cpu = time_in_state_cpu);
 
 DROP VIEW IF EXISTS android_thread_time_in_state_raw;
 CREATE VIEW android_thread_time_in_state_raw AS
 SELECT
   utid,
+  time_in_state_cpu,
   core_type,
   freq,
   MAX(runtime_ms_counter) - MIN(runtime_ms_counter) runtime_ms_diff
 FROM android_thread_time_in_state_base
-GROUP BY utid, core_type, freq;
+GROUP BY utid, time_in_state_cpu, core_type, freq;
 
 DROP TABLE IF EXISTS android_thread_time_in_state_counters;
 CREATE TABLE android_thread_time_in_state_counters AS
 SELECT
   utid,
+  raw.time_in_state_cpu,
   raw.core_type,
   SUM(runtime_ms_diff) AS runtime_ms,
   SUM(raw.freq * runtime_ms_diff / 1000000) AS mcycles,
   SUM(power * runtime_ms_diff / 3600000) AS power_profile_mah
 FROM android_thread_time_in_state_raw AS raw
     LEFT OUTER JOIN cpu_cluster_power AS power USING(core_type, freq)
-GROUP BY utid, raw.core_type
+GROUP BY utid, raw.time_in_state_cpu, raw.core_type
 HAVING runtime_ms > 0;
 
 DROP VIEW IF EXISTS android_thread_time_in_state_thread_metrics;
@@ -63,6 +66,7 @@
 SELECT
   utid,
   RepeatedField(AndroidThreadTimeInStateMetric_MetricsByCoreType(
+    'time_in_state_cpu',  time_in_state_cpu,
     'core_type', core_type,
     'runtime_ms', runtime_ms,
     'mcycles', CAST(mcycles AS INT),
@@ -92,17 +96,19 @@
 WITH process_counters AS (
   SELECT
     upid,
+    time_in_state_cpu,
     core_type,
     SUM(runtime_ms) AS runtime_ms,
     SUM(mcycles) AS mcycles,
     SUM(power_profile_mah) AS power_profile_mah
   FROM android_thread_time_in_state_counters
   JOIN thread USING (utid)
-  GROUP BY upid, core_type
+  GROUP BY upid, time_in_state_cpu, core_type
 )
 SELECT
   upid,
   RepeatedField(AndroidThreadTimeInStateMetric_MetricsByCoreType(
+    'time_in_state_cpu', time_in_state_cpu,
     'core_type', core_type,
     'runtime_ms', runtime_ms,
     'mcycles', CAST(mcycles AS INT),
diff --git a/src/trace_processor/metrics/android/composer_execution.sql b/src/trace_processor/metrics/android/composer_execution.sql
new file mode 100644
index 0000000..bec4fdc
--- /dev/null
+++ b/src/trace_processor/metrics/android/composer_execution.sql
@@ -0,0 +1,67 @@
+--
+-- Copyright 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
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+
+-- The HWC execution time will be calculated based on the runtime of
+-- HwcPresentOrValidateDisplay, HwcValidateDisplay, and/or HwcPresentDisplay
+-- which are happened in the same onMessageRefresh.
+-- There are 3 possible combinations how those functions will be called in
+-- a single onMessageRefresh, i.e.:
+-- 1. HwcPresentOrValidateDisplay and then HwcPresentDisplay
+-- 2. HwcPresentOrValidateDisplay
+-- 3. HwcValidateDisplay and then HwcPresentDisplay
+DROP VIEW IF EXISTS raw_hwc_function_spans;
+CREATE VIEW raw_hwc_function_spans AS
+SELECT
+  id,
+  name,
+  ts AS begin_ts,
+  ts+dur AS end_ts,
+  dur,
+  LEAD(name, 1, '') OVER (PARTITION BY track_id ORDER BY ts) AS next_name,
+  LEAD(ts, 1, 0) OVER (PARTITION BY track_id ORDER BY ts) AS next_ts,
+  LEAD(dur, 1, 0) OVER (PARTITION BY track_id ORDER BY ts) AS next_dur,
+  LEAD(name, 2, '') OVER (PARTITION BY track_id ORDER BY ts) AS second_next_name,
+  LEAD(ts, 2, 0) OVER (PARTITION BY track_id ORDER BY ts) AS second_next_ts,
+  LEAD(dur, 2, 0) OVER (PARTITION BY track_id ORDER BY ts) AS second_next_dur
+FROM slice
+WHERE name = 'HwcPresentOrValidateDisplay' OR name = 'HwcValidateDisplay'
+  OR name = 'HwcPresentDisplay' OR name = 'onMessageRefresh'
+ORDER BY ts;
+
+DROP VIEW IF EXISTS {{output}};
+CREATE VIEW {{output}} AS
+SELECT
+  id,
+  CASE
+    WHEN begin_ts <= next_ts AND next_ts <= end_ts THEN
+      CASE
+        WHEN begin_ts <= second_next_ts AND second_next_ts <= end_ts
+          THEN next_dur + second_next_dur
+        ELSE next_dur
+      END
+    ELSE 0
+  END AS execution_time_ns,
+  CASE
+    WHEN next_name = 'HwcPresentOrValidateDisplay'
+      AND second_next_name = 'HwcPresentDisplay' THEN 'unskipped_validation'
+    WHEN next_name = 'HwcPresentOrValidateDisplay'
+      AND second_next_name != 'HwcPresentDisplay' THEN 'skipped_validation'
+    WHEN next_name = 'HwcValidateDisplay'
+      AND second_next_name = 'HwcPresentDisplay' THEN 'separated_validation'
+    ELSE 'unknown'
+  END AS validation_type
+FROM raw_hwc_function_spans
+WHERE name = 'onMessageRefresh' AND dur > 0;
diff --git a/src/trace_processor/metrics/android/display_metrics.sql b/src/trace_processor/metrics/android/display_metrics.sql
index 980b30e..daec213 100644
--- a/src/trace_processor/metrics/android/display_metrics.sql
+++ b/src/trace_processor/metrics/android/display_metrics.sql
@@ -33,13 +33,56 @@
 WHERE name='DPU_UNDERRUN'
 AND value=1;
 
+DROP VIEW IF EXISTS non_repeated_panel_fps;
+CREATE VIEW non_repeated_panel_fps AS
+SELECT *
+FROM (
+  SELECT
+    ts,
+    value,
+    track_id,
+    LAG(value, 1, 0) OVER (PARTITION BY track_id ORDER BY ts) AS prev_value
+  FROM counter c JOIN track t ON c.track_id = t.id
+  WHERE t.name = 'panel_fps'
+  ORDER BY ts
+)
+WHERE prev_value != value;
+
+DROP VIEW IF EXISTS panel_fps_spans;
+CREATE VIEW panel_fps_spans AS
+SELECT *
+FROM (
+  SELECT
+    ts,
+    value,
+    LEAD(ts) OVER (PARTITION BY track_id ORDER BY ts) - ts AS dur
+  FROM non_repeated_panel_fps
+  ORDER BY ts
+)
+WHERE dur > 0;
+
 DROP VIEW IF EXISTS display_metrics_output;
 CREATE VIEW display_metrics_output AS
 SELECT AndroidDisplayMetrics(
-    'total_duplicate_frames', (SELECT total_duplicate_frames
+  'total_duplicate_frames', (SELECT total_duplicate_frames
                             FROM same_frame),
-    'duplicate_frames_logged', (SELECT logs_found
+  'duplicate_frames_logged', (SELECT logs_found
                             FROM duplicate_frames_logged),
-    'total_dpu_underrun_count', (SELECT total_dpu_underrun_count
-                            FROM dpu_underrun)
+  'total_dpu_underrun_count', (SELECT total_dpu_underrun_count
+                              FROM dpu_underrun),
+  'refresh_rate_switches', (SELECT COUNT(*) FROM panel_fps_spans),
+  'refresh_rate_stats', (
+    SELECT RepeatedField(metric)
+    FROM (
+      SELECT AndroidDisplayMetrics_RefreshRateStat(
+        'refresh_rate_fps', CAST(value AS UINT32),
+        'count', COUNT(*),
+        'total_dur_ms', SUM(dur) / 1e6,
+        'avg_dur_ms', AVG(dur) / 1e6
+      ) AS metric
+      FROM panel_fps_spans
+      GROUP BY value
+      ORDER BY value
+    )
+  )
 );
diff --git a/src/trace_processor/metrics/android/frame_missed.sql b/src/trace_processor/metrics/android/frame_missed.sql
index ab1303b..45499fa 100644
--- a/src/trace_processor/metrics/android/frame_missed.sql
+++ b/src/trace_processor/metrics/android/frame_missed.sql
@@ -29,6 +29,6 @@
 )
 SELECT
   ts,
-  dur
-FROM frame_missed_counters
-WHERE value = 1 AND ts IS NOT NULL;
\ No newline at end of file
+  dur,
+  value
+FROM frame_missed_counters;
\ No newline at end of file
diff --git a/src/trace_processor/metrics/android/heap_profile_callsites.sql b/src/trace_processor/metrics/android/heap_profile_callsites.sql
deleted file mode 100644
index 48add86..0000000
--- a/src/trace_processor/metrics/android/heap_profile_callsites.sql
+++ /dev/null
@@ -1,259 +0,0 @@
---
--- Copyright 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
---
---     https://www.apache.org/licenses/LICENSE-2.0
---
--- Unless required by applicable law or agreed to in writing, software
--- distributed under the License is distributed on an "AS IS" BASIS,
--- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--- See the License for the specific language governing permissions and
--- limitations under the License.
---
-
-SELECT RUN_METRIC('android/process_metadata.sql');
-SELECT RUN_METRIC('android/process_mem.sql');
-
-DROP VIEW IF EXISTS memory_delta;
-CREATE VIEW memory_delta AS
-SELECT upid, SUM(size) AS delta
-FROM heap_profile_allocation
-GROUP BY 1;
-
-DROP VIEW IF EXISTS memory_total;
-CREATE VIEW memory_total AS
-SELECT upid, SUM(size) AS total
-FROM heap_profile_allocation
-WHERE size > 0
-GROUP BY 1;
-
--- Join frames with symbols and mappings to get a textual representation.
-DROP TABLE IF EXISTS symbolized_frame;
-CREATE TABLE symbolized_frame AS
-SELECT
-  frame_id,
-  symbol_name,
-  mapping_name,
-  HASH(symbol_name, mapping_name) frame_hash,
-  HeapProfileCallsites_Frame(
-    'name', symbol_name,
-    'mapping_name', mapping_name
-  ) AS frame_proto
-FROM (
-  SELECT
-    spf.id AS frame_id,
-    COALESCE(
-      (SELECT name FROM stack_profile_symbol symbol
-        WHERE symbol.symbol_set_id = spf.symbol_set_id
-        LIMIT 1),
-      spf.deobfuscated_name,
-      spf.name
-    ) AS symbol_name,
-    spm.name AS mapping_name
-  FROM stack_profile_frame spf
-  JOIN stack_profile_mapping spm
-  ON spf.mapping = spm.id
-);
-
--- Required to join with callsites
-DROP INDEX IF EXISTS symbolized_frame_idx;
-CREATE UNIQUE INDEX symbolized_frame_idx ON symbolized_frame(frame_id);
-
--- View that joins callsites with frames. Allocation-agnostic, only used to
--- generated the hashed callsites.
-DROP TABLE IF EXISTS callsites;
-CREATE TABLE callsites AS
-SELECT cs.id, cs.parent_id, cs.depth, sf.frame_hash
-FROM stack_profile_callsite cs
-JOIN symbolized_frame sf USING(frame_id);
-
-DROP INDEX symbolized_frame_idx;
-
--- heapprofd-based callsite ids are based on frame addresses, whereas we want
--- to group by symbol names.
--- Create a unique ID for each subtree by traversing from the root.
--- 1 self_hash can correspond to N callsite_ids (which can then be used to join
--- with allocs).
-DROP TABLE IF EXISTS hashed_callsites;
-CREATE TABLE hashed_callsites AS
-WITH RECURSIVE callsite_hasher(id, self_hash, parent_hash, frame_hash) AS (
-  SELECT
-    cs.id,
-    cs.frame_hash,
-    -1,
-    cs.frame_hash
-  FROM callsites cs
-  WHERE cs.depth = 0
-  UNION ALL
-  SELECT
-    child.id,
-    HASH(child.frame_hash, parent.self_hash),
-    parent.self_hash,
-    child.frame_hash
-  FROM callsite_hasher parent
-  JOIN callsites child
-  ON parent.id = child.parent_id
-)
-SELECT
-  self_hash,
-  parent_hash,
-  frame_hash,
-  id callsite_id
-FROM callsite_hasher;
-
-DROP TABLE callsites;
-
-DROP VIEW IF EXISTS hashed_callsite_tree;
-CREATE VIEW hashed_callsite_tree AS
-SELECT DISTINCT self_hash, parent_hash, frame_hash
-FROM hashed_callsites;
-
--- Required to join with allocs
-DROP INDEX IF EXISTS hashed_callsites_id_idx;
-CREATE INDEX hashed_callsites_id_idx ON hashed_callsites(callsite_id);
-
--- Computes the allocations for each hash-based callsite.
-DROP TABLE IF EXISTS self_allocs;
-CREATE TABLE self_allocs AS
-SELECT
-  hc.self_hash,
-  alloc.upid,
-  SUM(alloc.count) AS delta_count,
-  SUM(CASE WHEN alloc.count > 0 THEN alloc.count ELSE 0 END) AS total_count,
-  SUM(alloc.size) AS delta_bytes,
-  SUM(CASE WHEN alloc.size > 0 THEN alloc.size ELSE 0 END) AS total_bytes
-FROM hashed_callsites hc
-JOIN heap_profile_allocation alloc USING (callsite_id)
-GROUP BY 1, 2;
-
-DROP INDEX hashed_callsites_id_idx;
-
--- For each allocation (each self_alloc), emit a row for each ancestor and
--- aggregate them by self_hash.
-DROP TABLE IF EXISTS child_allocs;
-CREATE TABLE child_allocs AS
-WITH RECURSIVE parent_traversal(
-  self_hash, parent_hash, upid,
-  delta_count, total_count, delta_bytes, total_bytes) AS (
-  SELECT
-    sa.self_hash,
-    hc.parent_hash,
-    sa.upid,
-    sa.delta_count,
-    sa.total_count,
-    sa.delta_bytes,
-    sa.total_bytes
-  FROM self_allocs sa
-  JOIN hashed_callsite_tree hc ON sa.self_hash = hc.self_hash
-  UNION ALL
-  SELECT
-    parent.self_hash,
-    parent.parent_hash,
-    child.upid,
-    child.delta_count,
-    child.total_count,
-    child.delta_bytes,
-    child.total_bytes
-  FROM parent_traversal child
-  JOIN hashed_callsite_tree parent
-  ON child.parent_hash = parent.self_hash
-)
-SELECT
-  self_hash,
-  upid,
-  SUM(delta_count) AS delta_count,
-  SUM(total_count) AS total_count,
-  SUM(delta_bytes) AS delta_bytes,
-  SUM(total_bytes) AS total_bytes
-FROM parent_traversal
-GROUP BY 1, 2;
-
-DROP VIEW IF EXISTS self_allocs_proto;
-CREATE VIEW self_allocs_proto AS
-SELECT
-  self_hash,
-  upid,
-  HeapProfileCallsites_Counters(
-    'delta_count', delta_count, 'total_count', total_count,
-    'delta_bytes', delta_bytes, 'total_bytes', total_bytes
-  ) AS allocs_proto
-FROM self_allocs;
-
-DROP VIEW IF EXISTS child_allocs_proto;
-CREATE VIEW child_allocs_proto AS
-SELECT
-  self_hash,
-  upid,
-  HeapProfileCallsites_Counters(
-    'delta_count', delta_count, 'total_count', total_count,
-    'delta_bytes', delta_bytes, 'total_bytes', total_bytes
-  ) AS allocs_proto
-FROM child_allocs;
-
--- Required to map back to the symbol.
-DROP INDEX IF EXISTS symbolized_frame_hash_idx;
-CREATE INDEX symbolized_frame_hash_idx ON symbolized_frame(frame_hash);
-
-DROP TABLE IF EXISTS process_callsite;
-CREATE TABLE process_callsite AS
-SELECT
-  ca.upid,
-  ca.self_hash,
-  tree.parent_hash,
-  frame.frame_proto,
-  sa.allocs_proto AS self_allocs_proto,
-  ca.allocs_proto AS child_allocs_proto
-FROM hashed_callsite_tree tree
-JOIN (SELECT DISTINCT frame_hash, frame_proto FROM symbolized_frame) frame
-  USING (frame_hash)
-JOIN child_allocs_proto ca
-  USING (self_hash)
-LEFT JOIN self_allocs_proto sa
-  USING (self_hash, upid)
-ORDER BY 1, 2;
-
-DROP INDEX symbolized_frame_hash_idx;
-
-DROP VIEW IF EXISTS process_callsite_proto;
-CREATE VIEW process_callsite_proto AS
-SELECT
-  upid,
-  RepeatedField(HeapProfileCallsites_Callsite(
-    'hash', self_hash,
-    'parent_hash', parent_hash,
-    'frame', frame_proto,
-    'self_allocs', self_allocs_proto,
-    'child_allocs', child_allocs_proto
-  )) AS repeated_callsite_proto
-FROM process_callsite
-GROUP BY 1;
-
-DROP VIEW IF EXISTS instance_stats_view;
-CREATE VIEW instance_stats_view AS
-SELECT HeapProfileCallsites_InstanceStats(
-    'pid', process.pid,
-    'process_name', process.name,
-    'process', process_metadata.metadata,
-    'callsites', repeated_callsite_proto,
-    'profile_delta_bytes', memory_delta.delta,
-    'profile_total_bytes', memory_total.total,
-    'max_anon_rss_and_swap_bytes', (
-      SELECT CAST(MAX(anon_and_swap_val) AS INT)
-      FROM anon_and_swap_span s WHERE s.upid = process.upid
-    )
-) AS instance_stats_proto
-FROM process_callsite_proto
-JOIN memory_total USING (upid)
-JOIN memory_delta USING (upid)
-JOIN process USING (upid)
-JOIN process_metadata USING (upid);
-
-DROP VIEW IF EXISTS heap_profile_callsites_output;
-CREATE VIEW heap_profile_callsites_output AS
-SELECT HeapProfileCallsites(
-  'instance_stats',
-  (SELECT RepeatedField(instance_stats_proto) FROM instance_stats_view)
-);
diff --git a/src/trace_processor/metrics/android/hsc_startups.sql b/src/trace_processor/metrics/android/hsc_startups.sql
index b07f545..5c0a618 100644
--- a/src/trace_processor/metrics/android/hsc_startups.sql
+++ b/src/trace_processor/metrics/android/hsc_startups.sql
@@ -105,7 +105,7 @@
     frame_times.ts_end - launches.ts as ts_total
 FROM frame_times
 INNER JOIN launches on launches.package LIKE '%' || frame_times.name || '%'
-WHERE frame_times.ts > (SELECT ts + dur FROM animators WHERE animator_name="animator:translationZ" AND process_name LIKE "%id.deskclock" ORDER BY ts DESC LIMIT 1) AND frame_times.name LIKE "%id.deskclock" AND frame_times.launch_id = launches.id
+WHERE frame_times.ts > (SELECT ts + dur FROM animators WHERE animator_name="animator:translationZ" AND process_name LIKE "%id.deskclock" ORDER BY (ts+dur) DESC LIMIT 1) AND frame_times.name LIKE "%id.deskclock" AND frame_times.launch_id = launches.id
 ORDER BY ts_total LIMIT 1;
 
 -- Contacts
@@ -158,7 +158,7 @@
     frame_times.ts_end - launches.ts as ts_total
 FROM frame_times
 INNER JOIN launches on launches.package LIKE '%' || frame_times.name || '%'
-WHERE frame_times.ts > (SELECT ts + dur FROM animators WHERE animator_name="animator:elevation" AND process_name LIKE "%android.gm" ORDER BY ts DESC LIMIT 1) AND frame_times.name LIKE "%android.gm" AND frame_times.launch_id = launches.id
+WHERE frame_times.ts > (SELECT ts + dur FROM animators WHERE animator_name="animator:elevation" AND process_name LIKE "%android.gm" ORDER BY (ts+dur) DESC LIMIT 1) AND frame_times.name LIKE "%android.gm" AND frame_times.launch_id = launches.id
 ORDER BY ts_total LIMIT 1;
 
 -- Instagram
diff --git a/src/trace_processor/metrics/android/power_drain_in_watts.sql b/src/trace_processor/metrics/android/power_drain_in_watts.sql
index d866bb0..1fbfec3 100644
--- a/src/trace_processor/metrics/android/power_drain_in_watts.sql
+++ b/src/trace_processor/metrics/android/power_drain_in_watts.sql
@@ -31,7 +31,23 @@
   ('power.PPVAR_VPH_PWR_WLAN_uws', 'wifi'),
   ('power.PPVAR_VPH_PWR_OLED_uws', 'display'),
   ('power.PPVAR_VPH_PWR_QTM525_uws', 'cellular'),
-  ('power.PPVAR_VPH_PWR_RF_uws', 'cellular');
+  ('power.PPVAR_VPH_PWR_RF_uws', 'cellular'),
+  ('power.rails.aoc.logic', 'aoc'),
+  ('power.rails.aoc.memory', 'aoc'),
+  ('power.rails.cpu.big', 'cpu_big'),
+  ('power.rails.cpu.little', 'cpu_little'),
+  ('power.rails.cpu.mid', 'cpu_mid'),
+  ('power.rails.ddr.a', 'mem'),
+  ('power.rails.ddr.b', 'mem'),
+  ('power.rails.ddr.c', 'mem'),
+  ('power.rails.gpu', 'gpu'),
+  ('power.rails.display', 'display'),
+  ('power.rails.gps', 'gps'),
+  ('power.rails.memory.interface', 'mem'),
+  ('power.rails.modem', 'cellular'),
+  ('power.rails.radio.frontend', 'cellular'),
+  ('power.rails.system.fabric', 'soc'),
+  ('power.rails.wifi.bt', 'wifi');
 
 -- Convert power counter data into table of events, where each event has
 -- start timestamp, duration and the average power drain during its duration
@@ -72,3 +88,4 @@
   JOIN counter_track ON (counter.track_id = counter_track.id)
 WHERE counter_track.type = 'counter_track'
   AND name LIKE "power.%";
+
diff --git a/src/trace_processor/metrics/android/thread_counter_span_view.sql b/src/trace_processor/metrics/android/thread_counter_span_view.sql
new file mode 100644
index 0000000..da13734
--- /dev/null
+++ b/src/trace_processor/metrics/android/thread_counter_span_view.sql
@@ -0,0 +1,32 @@
+--
+-- Copyright 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
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+DROP VIEW IF EXISTS {{table_name}}_span;
+CREATE VIEW {{table_name}}_span AS
+SELECT
+  ts,
+  LEAD(ts, 1, (
+    SELECT IFNULL(
+      end_ts,
+      (SELECT end_ts FROM trace_bounds)
+    )
+    FROM thread th WHERE th.utid = t.utid) + 1
+  ) OVER(PARTITION BY track_id ORDER BY ts) - ts AS dur,
+  utid,
+  value AS {{table_name}}_val
+FROM counter c JOIN thread_counter_track t
+  ON t.id = c.track_id
+WHERE name = '{{counter_name}}' AND utid IS NOT NULL;
diff --git a/src/trace_processor/metrics/chrome/chrome_processes.sql b/src/trace_processor/metrics/chrome/chrome_processes.sql
index aead4ed..dd3be51 100644
--- a/src/trace_processor/metrics/chrome/chrome_processes.sql
+++ b/src/trace_processor/metrics/chrome/chrome_processes.sql
@@ -114,11 +114,11 @@
 
 CREATE VIEW chrome_thread AS
 SELECT thread.*,
-  IIF(
-    thread.name GLOB "Cr*Main",
-    "CrProcessMain",
-    thread.name
-  ) AS canonical_name
+  CASE
+    WHEN thread.name GLOB "Cr*Main" THEN "CrProcessMain"
+    WHEN thread.name IS NULL THEN "Unknown"
+    ELSE thread.name
+  END AS canonical_name
 FROM (
     SELECT t.utid,
       p.*
diff --git a/src/trace_processor/metrics/chrome/rail_modes.sql b/src/trace_processor/metrics/chrome/rail_modes.sql
index 466d7f7..d86c25f 100644
--- a/src/trace_processor/metrics/chrome/rail_modes.sql
+++ b/src/trace_processor/metrics/chrome/rail_modes.sql
@@ -507,8 +507,8 @@
 -- adding a unique id to each slice. Rather than directly merging slices
 -- together, this instead looks for all the transitions and uses this to
 -- reconstruct the slices that should occur between them.
-DROP VIEW IF EXISTS modified_rail_slices;
-CREATE VIEW modified_rail_slices AS
+DROP TABLE IF EXISTS modified_rail_slices;
+CREATE TABLE modified_rail_slices AS
 WITH const (end_ts) AS (SELECT ts + dur
               FROM unmerged_modified_rail_slices
               ORDER BY ts DESC
diff --git a/src/trace_processor/metrics/metrics.cc b/src/trace_processor/metrics/metrics.cc
index c58fb1f..4f34d3c 100644
--- a/src/trace_processor/metrics/metrics.cc
+++ b/src/trace_processor/metrics/metrics.cc
@@ -71,8 +71,9 @@
 
 }  // namespace
 
-ProtoBuilder::ProtoBuilder(const ProtoDescriptor* descriptor)
-    : descriptor_(descriptor) {}
+ProtoBuilder::ProtoBuilder(const DescriptorPool* pool,
+                           const ProtoDescriptor* descriptor)
+    : pool_(pool), descriptor_(descriptor) {}
 
 util::Status ProtoBuilder::AppendSqlValue(const std::string& field_name,
                                           const SqlValue& value) {
@@ -117,9 +118,32 @@
     case FieldDescriptorProto::TYPE_INT64:
     case FieldDescriptorProto::TYPE_UINT32:
     case FieldDescriptorProto::TYPE_BOOL:
-    case FieldDescriptorProto::TYPE_ENUM:
       message_->AppendVarInt(field->number(), value);
       break;
+    case FieldDescriptorProto::TYPE_ENUM: {
+      auto opt_enum_descriptor_idx =
+          pool_->FindDescriptorIdx(field->resolved_type_name());
+      if (!opt_enum_descriptor_idx) {
+        return util::ErrStatus(
+            "Unable to find enum type %s to fill field %s (in proto message "
+            "%s)",
+            field->resolved_type_name().c_str(), field->name().c_str(),
+            descriptor_->full_name().c_str());
+      }
+      const auto& enum_desc = pool_->descriptors()[*opt_enum_descriptor_idx];
+      auto opt_enum_str = enum_desc.FindEnumString(static_cast<int32_t>(value));
+      if (!opt_enum_str) {
+        return util::ErrStatus("Invalid enum value %" PRId64
+                               " "
+                               "in enum type %s; encountered while filling "
+                               "field %s (in proto message %s)",
+                               value, field->resolved_type_name().c_str(),
+                               field->name().c_str(),
+                               descriptor_->full_name().c_str());
+      }
+      message_->AppendVarInt(field->number(), value);
+      break;
+    }
     case FieldDescriptorProto::TYPE_SINT32:
     case FieldDescriptorProto::TYPE_SINT64:
       message_->AppendSignedVarInt(field->number(), value);
@@ -130,6 +154,12 @@
     case FieldDescriptorProto::TYPE_SFIXED64:
       message_->AppendFixed(field->number(), value);
       break;
+    case FieldDescriptorProto::TYPE_UINT64:
+      return util::ErrStatus(
+          "Field %s (in proto message %s) is using a uint64 type. uint64 in "
+          "metric messages is not supported by trace processor; use an int64 "
+          "field instead.",
+          field->name().c_str(), descriptor_->full_name().c_str());
     default: {
       return util::ErrStatus(
           "Tried to write value of type long into field %s (in proto type %s) "
@@ -201,6 +231,30 @@
       message_->AppendBytes(field->number(), data.data(), data.size());
       break;
     }
+    case FieldDescriptorProto::TYPE_ENUM: {
+      auto opt_enum_descriptor_idx =
+          pool_->FindDescriptorIdx(field->resolved_type_name());
+      if (!opt_enum_descriptor_idx) {
+        return util::ErrStatus(
+            "Unable to find enum type %s to fill field %s (in proto message "
+            "%s)",
+            field->resolved_type_name().c_str(), field->name().c_str(),
+            descriptor_->full_name().c_str());
+      }
+      const auto& enum_desc = pool_->descriptors()[*opt_enum_descriptor_idx];
+      std::string enum_str = data.ToStdString();
+      auto opt_enum_value = enum_desc.FindEnumValue(enum_str);
+      if (!opt_enum_value) {
+        return util::ErrStatus(
+            "Invalid enum string %s "
+            "in enum type %s; encountered while filling "
+            "field %s (in proto message %s)",
+            enum_str.c_str(), field->resolved_type_name().c_str(),
+            field->name().c_str(), descriptor_->full_name().c_str());
+      }
+      message_->AppendVarInt(field->number(), *opt_enum_value);
+      break;
+    }
     default: {
       return util::ErrStatus(
           "Tried to write value of type string into field %s (in proto type "
@@ -258,6 +312,14 @@
     return util::OkStatus();
   }
 
+  if (size > protozero::proto_utils::kMaxMessageLength) {
+    return util::ErrStatus(
+        "Message passed to field %s in proto message %s has size %zu which is "
+        "larger than the maximum allowed message size %zu",
+        field.name().c_str(), descriptor_->full_name().c_str(), size,
+        protozero::proto_utils::kMaxMessageLength);
+  }
+
   protos::pbzero::ProtoBuilderResult::Decoder decoder(ptr, size);
   if (decoder.is_repeated()) {
     return util::ErrStatus("Cannot handle nested repeated messages in field %s",
@@ -299,6 +361,14 @@
 util::Status ProtoBuilder::AppendRepeated(const FieldDescriptor& field,
                                           const uint8_t* ptr,
                                           size_t size) {
+  if (size > protozero::proto_utils::kMaxMessageLength) {
+    return util::ErrStatus(
+        "Message passed to field %s in proto message %s has size %zu which is "
+        "larger than the maximum allowed message size %zu",
+        field.name().c_str(), descriptor_->full_name().c_str(), size,
+        protozero::proto_utils::kMaxMessageLength);
+  }
+
   protos::pbzero::ProtoBuilderResult::Decoder decoder(ptr, size);
   if (!decoder.is_repeated()) {
     return util::ErrStatus(
@@ -524,7 +594,7 @@
     return;
   }
 
-  ProtoBuilder builder(fn_ctx->desc);
+  ProtoBuilder builder(fn_ctx->pool, fn_ctx->desc);
   for (int i = 0; i < argc; i += 2) {
     if (sqlite3_value_type(argv[i]) != SQLITE_TEXT) {
       sqlite3_result_error(ctx, "BuildProto: Invalid args", -1);
@@ -615,9 +685,10 @@
 util::Status ComputeMetrics(TraceProcessor* tp,
                             const std::vector<std::string> metrics_to_compute,
                             const std::vector<SqlMetricFile>& sql_metrics,
+                            const DescriptorPool& pool,
                             const ProtoDescriptor& root_descriptor,
                             std::vector<uint8_t>* metrics_proto) {
-  ProtoBuilder metric_builder(&root_descriptor);
+  ProtoBuilder metric_builder(&pool, &root_descriptor);
   for (const auto& name : metrics_to_compute) {
     auto metric_it =
         std::find_if(sql_metrics.begin(), sql_metrics.end(),
diff --git a/src/trace_processor/metrics/metrics.h b/src/trace_processor/metrics/metrics.h
index dea62f7..b999225 100644
--- a/src/trace_processor/metrics/metrics.h
+++ b/src/trace_processor/metrics/metrics.h
@@ -61,7 +61,7 @@
 // Visible for testing.
 class ProtoBuilder {
  public:
-  ProtoBuilder(const ProtoDescriptor*);
+  ProtoBuilder(const DescriptorPool*, const ProtoDescriptor*);
 
   util::Status AppendSqlValue(const std::string& field_name,
                               const SqlValue& value);
@@ -106,6 +106,7 @@
                               const uint8_t* ptr,
                               size_t size);
 
+  const DescriptorPool* pool_ = nullptr;
   const ProtoDescriptor* descriptor_ = nullptr;
   protozero::HeapBuffered<protozero::Message> message_;
 };
@@ -176,6 +177,7 @@
 util::Status ComputeMetrics(TraceProcessor* impl,
                             const std::vector<std::string> metrics_to_compute,
                             const std::vector<SqlMetricFile>& metrics,
+                            const DescriptorPool& pool,
                             const ProtoDescriptor& root_descriptor,
                             std::vector<uint8_t>* metrics_proto);
 
diff --git a/src/trace_processor/metrics/metrics_unittest.cc b/src/trace_processor/metrics/metrics_unittest.cc
index 661df33..29f8f6d 100644
--- a/src/trace_processor/metrics/metrics_unittest.cc
+++ b/src/trace_processor/metrics/metrics_unittest.cc
@@ -74,13 +74,14 @@
   // message TestProto {
   //   optional int64 int_value = 1;
   // }
+  DescriptorPool pool;
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, base::nullopt);
   descriptor.AddField(FieldDescriptor(
       "int_value", 1, FieldDescriptorProto::TYPE_INT64, "", false));
 
-  ProtoBuilder builder(&descriptor);
+  ProtoBuilder builder(&pool, &descriptor);
   ASSERT_TRUE(builder.AppendLong("int_value", 12345).ok());
 
   auto result_ser = builder.SerializeToProtoBuilderResult();
@@ -96,13 +97,14 @@
   // message TestProto {
   //   optional double double_value = 1;
   // }
+  DescriptorPool pool;
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, base::nullopt);
   descriptor.AddField(FieldDescriptor(
       "double_value", 1, FieldDescriptorProto::TYPE_DOUBLE, "", false));
 
-  ProtoBuilder builder(&descriptor);
+  ProtoBuilder builder(&pool, &descriptor);
   ASSERT_TRUE(builder.AppendDouble("double_value", 1.2345).ok());
 
   auto result_ser = builder.SerializeToProtoBuilderResult();
@@ -118,13 +120,14 @@
   // message TestProto {
   //   optional string string_value = 1;
   // }
+  DescriptorPool pool;
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, base::nullopt);
   descriptor.AddField(FieldDescriptor(
       "string_value", 1, FieldDescriptorProto::TYPE_STRING, "", false));
 
-  ProtoBuilder builder(&descriptor);
+  ProtoBuilder builder(&pool, &descriptor);
   ASSERT_TRUE(builder.AppendString("string_value", "hello world!").ok());
 
   auto result_ser = builder.SerializeToProtoBuilderResult();
@@ -143,6 +146,7 @@
   //   }
   //   optional NestedProto nested_value = 1;
   // }
+  DescriptorPool pool;
   ProtoDescriptor nested("file.proto", ".perfetto.protos",
                          ".perfetto.protos.TestProto.NestedProto",
                          ProtoDescriptor::Type::kMessage, base::nullopt);
@@ -158,12 +162,12 @@
   field.set_resolved_type_name(".perfetto.protos.TestProto.NestedProto");
   descriptor.AddField(field);
 
-  ProtoBuilder nest_builder(&nested);
+  ProtoBuilder nest_builder(&pool, &nested);
   ASSERT_TRUE(nest_builder.AppendLong("nested_int_value", 789).ok());
 
   auto nest_ser = nest_builder.SerializeToProtoBuilderResult();
 
-  ProtoBuilder builder(&descriptor);
+  ProtoBuilder builder(&pool, &descriptor);
   ASSERT_TRUE(
       builder.AppendBytes("nested_value", nest_ser.data(), nest_ser.size())
           .ok());
@@ -190,6 +194,7 @@
   // message TestProto {
   //   repeated int64 int_value = 1;
   // }
+  DescriptorPool pool;
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, base::nullopt);
@@ -202,7 +207,7 @@
 
   std::vector<uint8_t> rep_ser = rep_builder.SerializeToProtoBuilderResult();
 
-  ProtoBuilder builder(&descriptor);
+  ProtoBuilder builder(&pool, &descriptor);
   ASSERT_TRUE(
       builder.AppendBytes("rep_int_value", rep_ser.data(), rep_ser.size())
           .ok());
@@ -215,6 +220,55 @@
   ASSERT_FALSE(++it);
 }
 
+TEST_F(ProtoBuilderTest, AppendEnums) {
+  using FieldDescriptorProto = protos::pbzero::FieldDescriptorProto;
+
+  // Create the descriptor version of the following enum and message:
+  // enum TestEnum {
+  //   FIRST = 1,
+  //   SECOND = 2,
+  //   THIRD = 3
+  // }
+  // message TestMessage {
+  //   optional TestEnum enum_value = 1;
+  // }
+  DescriptorPool pool;
+  ProtoDescriptor enum_descriptor("file.proto", ".perfetto.protos",
+                                  ".perfetto.protos.TestEnum",
+                                  ProtoDescriptor::Type::kEnum, base::nullopt);
+  enum_descriptor.AddEnumValue(1, "FIRST");
+  enum_descriptor.AddEnumValue(2, "SECOND");
+  enum_descriptor.AddEnumValue(3, "THIRD");
+  pool.AddProtoDescriptorForTesting(enum_descriptor);
+
+  ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
+                             ".perfetto.protos.TestMessage",
+                             ProtoDescriptor::Type::kMessage, base::nullopt);
+  FieldDescriptor enum_field("enum_value", 1, FieldDescriptorProto::TYPE_ENUM,
+                             ".perfetto.protos.TestEnum", false);
+  enum_field.set_resolved_type_name(".perfetto.protos.TestEnum");
+  descriptor.AddField(enum_field);
+  pool.AddProtoDescriptorForTesting(descriptor);
+
+  ProtoBuilder value_builder(&pool, &descriptor);
+  ASSERT_FALSE(value_builder.AppendLong("enum_value", 4).ok());
+  ASSERT_TRUE(value_builder.AppendLong("enum_value", 3).ok());
+  ASSERT_FALSE(value_builder.AppendLong("enum_value", 6).ok());
+
+  auto value_proto = DecodeSingleFieldProto<false>(
+      value_builder.SerializeToProtoBuilderResult());
+  ASSERT_EQ(value_proto.Get(1).as_int32(), 3);
+
+  ProtoBuilder str_builder(&pool, &descriptor);
+  ASSERT_FALSE(str_builder.AppendString("enum_value", "FOURTH").ok());
+  ASSERT_TRUE(str_builder.AppendString("enum_value", "SECOND").ok());
+  ASSERT_FALSE(str_builder.AppendString("enum_value", "OTHER").ok());
+
+  auto str_proto = DecodeSingleFieldProto<false>(
+      str_builder.SerializeToProtoBuilderResult());
+  ASSERT_EQ(str_proto.Get(1).as_int32(), 2);
+}
+
 }  // namespace
 
 }  // namespace metrics
diff --git a/src/trace_processor/metrics/trace_metadata.sql b/src/trace_processor/metrics/trace_metadata.sql
index 770b7cb..838116b 100644
--- a/src/trace_processor/metrics/trace_metadata.sql
+++ b/src/trace_processor/metrics/trace_metadata.sql
@@ -17,7 +17,7 @@
 DROP VIEW IF EXISTS trace_metadata_output;
 CREATE VIEW trace_metadata_output AS
 SELECT TraceMetadata(
-  'trace_duration_ns', (SELECT end_ts - start_ts FROM trace_bounds),
+  'trace_duration_ns', CAST((SELECT end_ts - start_ts FROM trace_bounds) AS INT),
   'trace_uuid', (SELECT str_value FROM metadata WHERE name = 'trace_uuid'),
   'android_build_fingerprint', (
     SELECT str_value FROM metadata WHERE name = 'android_build_fingerprint'
diff --git a/src/trace_processor/metrics/trace_stats.sql b/src/trace_processor/metrics/trace_stats.sql
index f647442..702fc91 100644
--- a/src/trace_processor/metrics/trace_stats.sql
+++ b/src/trace_processor/metrics/trace_stats.sql
@@ -22,16 +22,16 @@
       'name', name,
       'idx', idx,
       'count', value,
-      -- TraceAnalysisStats.Source enum:
       'source', CASE source
-        WHEN 'trace' THEN 1
-        WHEN 'analysis' THEN 2
+        WHEN 'trace' THEN 'SOURCE_TRACE'
+        WHEN 'analysis' THEN 'SOURCE_ANALYSIS'
+        ELSE 'SOURCE_UNKNOWN'
       END,
-      -- TraceAnalysisStats.Severity enum:
       'severity', CASE severity
-        WHEN 'info' THEN 1
-        WHEN 'data_loss' THEN 2
-        WHEN 'error' THEN 3
+        WHEN 'info' THEN 'SEVERITY_INFO'
+        WHEN 'data_loss' THEN 'SEVERITY_DATA_LOSS'
+        WHEN 'error' THEN 'SEVERITY_ERROR'
+        ELSE 'SEVERITY_UNKNOWN'
       END
     ))
     FROM stats ORDER BY name ASC
diff --git a/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor b/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor
index 3db9029..68b2577 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/metrics.descriptor.sha1 b/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor.sha1
index 3e2c60e..93de9e1 100644
--- a/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor.sha1
+++ b/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor.sha1
@@ -1,6 +1,6 @@
 
 // SHA1(tools/gen_binary_descriptors)
-// 30f9a74885dae344b1a42f7ba94d8909c9d07ad0
+// 9fc6d77de57ec76a80b76aa282f4c7cf5ce55eec
 // SHA1(protos/perfetto/metrics/metrics.proto)
-// 10206b45132d74d6b7e9d72feb3af74eaecf3816
+// 64a90d4e7177aa7a8ba57919313037430848325c
   
\ No newline at end of file
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 3d2bf28..e3e9b24 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 5ac17dd..6e01bd0 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
@@ -1,6 +1,6 @@
 
 // SHA1(tools/gen_binary_descriptors)
-// 30f9a74885dae344b1a42f7ba94d8909c9d07ad0
+// 9fc6d77de57ec76a80b76aa282f4c7cf5ce55eec
 // SHA1(protos/perfetto/trace_processor/trace_processor.proto)
-// 8320f306d6d5bbcb5ef6ba8cd62cc70a0994d102
+// b4135d9bc2551939bfdaa4d03423cf34a8fbcffb
   
\ No newline at end of file
diff --git a/src/trace_processor/read_trace.cc b/src/trace_processor/read_trace.cc
index 8f5b6f0..4a9d8fd 100644
--- a/src/trace_processor/read_trace.cc
+++ b/src/trace_processor/read_trace.cc
@@ -25,8 +25,8 @@
 
 #include "src/trace_processor/forwarding_trace_parser.h"
 #include "src/trace_processor/importers/gzip/gzip_trace_parser.h"
-#include "src/trace_processor/importers/gzip/gzip_utils.h"
 #include "src/trace_processor/importers/proto/proto_trace_tokenizer.h"
+#include "src/trace_processor/util/gzip_utils.h"
 #include "src/trace_processor/util/status_macros.h"
 
 #include "protos/perfetto/trace/trace.pbzero.h"
@@ -210,7 +210,7 @@
   PERFETTO_CHECK(type == TraceType::kProtoTraceType);
 
   protos::pbzero::Trace::Decoder decoder(data, size);
-  GzipDecompressor decompressor;
+  util::GzipDecompressor decompressor;
   if (size > 0 && !decoder.packet()) {
     return util::ErrStatus("Trace does not contain valid packets");
   }
@@ -226,7 +226,7 @@
     decompressor.Reset();
     decompressor.SetInput(bytes.data, bytes.size);
 
-    using ResultCode = GzipDecompressor::ResultCode;
+    using ResultCode = util::GzipDecompressor::ResultCode;
     uint8_t out[4096];
     for (auto ret = ResultCode::kOk; ret != ResultCode::kEof;) {
       auto res = decompressor.Decompress(out, base::ArraySize(out));
diff --git a/src/trace_processor/rpc/BUILD.gn b/src/trace_processor/rpc/BUILD.gn
index 63cc752..09b9061 100644
--- a/src/trace_processor/rpc/BUILD.gn
+++ b/src/trace_processor/rpc/BUILD.gn
@@ -23,8 +23,6 @@
 # interface) and by the :httpd module for the HTTP interface.
 source_set("rpc") {
   sources = [
-    "proto_ring_buffer.cc",
-    "proto_ring_buffer.h",
     "query_result_serializer.cc",
     "query_result_serializer.h",
     "rpc.cc",
@@ -38,15 +36,13 @@
     "../../../protos/perfetto/trace_processor:zero",
     "../../base",
     "../../protozero",
+    "../../protozero:proto_ring_buffer",
   ]
 }
 
 perfetto_unittest_source_set("unittests") {
   testonly = true
-  sources = [
-    "proto_ring_buffer_unittest.cc",
-    "query_result_serializer_unittest.cc",
-  ]
+  sources = [ "query_result_serializer_unittest.cc" ]
   deps = [
     ":rpc",
     "..:lib",
diff --git a/src/trace_processor/rpc/httpd.cc b/src/trace_processor/rpc/httpd.cc
index 1a061aa..2cdf80f 100644
--- a/src/trace_processor/rpc/httpd.cc
+++ b/src/trace_processor/rpc/httpd.cc
@@ -71,6 +71,9 @@
   ~HttpServer() override;
   void Run(const char*, const char*);
 
+  // This is non-null only while serving an HTTP request.
+  Client* active_client() { return active_client_; }
+
  private:
   size_t ParseOneHttpRequest(Client* client);
   void HandleRequest(Client*, const HttpRequest&);
@@ -85,9 +88,12 @@
   base::UnixTaskRunner task_runner_;
   std::unique_ptr<base::UnixSocket> sock4_;
   std::unique_ptr<base::UnixSocket> sock6_;
-  std::vector<Client> clients_;
+  std::list<Client> clients_;
+  Client* active_client_ = nullptr;
 };
 
+HttpServer* g_httpd_instance;
+
 void Append(std::vector<char>& buf, const char* str) {
   buf.insert(buf.end(), str, str + strlen(str));
 }
@@ -197,7 +203,9 @@
   // 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);
@@ -282,12 +290,13 @@
   // 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",  //
-      transfer_encoding_hdr,                   //
-      allow_origin_hdr.c_str()};
+      "Connection: Keep-Alive",                          //
+      "Cache-Control: no-cache",                         //
+      "Keep-Alive: timeout=5, max=1000",                 //
+      "Content-Type: application/x-protobuf",            //
+      transfer_encoding_hdr,                             //
+      allow_origin_hdr.c_str(),
+  };
 
   if (req.method == "OPTIONS") {
     // CORS headers.
@@ -300,6 +309,38 @@
                      });
   }
 
+  if (req.uri == "/rpc") {
+    // Start the chunked reply.
+    strncpy(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);
+    trace_processor_rpc_.OnRpcRequest(req.body.data(), req.body.size());
+    trace_processor_rpc_.SetRpcResponseFunction(nullptr);
+
+    // Terminate chunked stream.
+    cli_sock->Send("0\r\n\r\n", 5);
+    return;
+  }
+
   if (req.uri == "/parse") {
     trace_processor_rpc_.Parse(
         reinterpret_cast<const uint8_t*>(req.body.data()), req.body.size());
@@ -360,12 +401,9 @@
   }
 
   if (req.uri == "/status") {
-    protozero::HeapBuffered<protos::pbzero::StatusResult> res;
-    res->set_loaded_trace_name(
-        trace_processor_rpc_.GetCurrentTraceName().c_str());
-    std::vector<uint8_t> buf = res.SerializeAsArray();
-    return HttpReply(client->sock.get(), "200 OK", headers, buf.data(),
-                     buf.size());
+    auto status = trace_processor_rpc_.GetStatus();
+    return HttpReply(client->sock.get(), "200 OK", headers, status.data(),
+                     status.size());
   }
 
   if (req.uri == "/compute_metric") {
@@ -375,13 +413,6 @@
                      res.size());
   }
 
-  if (req.uri == "/get_metric_descriptors") {
-    std::vector<uint8_t> res = trace_processor_rpc_.GetMetricDescriptors(
-        reinterpret_cast<const uint8_t*>(req.body.data()), req.body.size());
-    return HttpReply(client->sock.get(), "200 OK", headers, res.data(),
-                     res.size());
-  }
-
   if (req.uri == "/enable_metatrace") {
     trace_processor_rpc_.EnableMetatrace();
     return HttpReply(client->sock.get(), "200 OK", headers);
@@ -401,6 +432,7 @@
 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;
diff --git a/src/trace_processor/rpc/query_result_serializer.h b/src/trace_processor/rpc/query_result_serializer.h
index 9c05e0b..c29d66d 100644
--- a/src/trace_processor/rpc/query_result_serializer.h
+++ b/src/trace_processor/rpc/query_result_serializer.h
@@ -53,6 +53,7 @@
 // chunked-encoded HTTP response, or through a repetition of Wasm calls.
 class QueryResultSerializer {
  public:
+  static constexpr uint32_t kDefaultBatchSplitThreshold = 128 * 1024;
   explicit QueryResultSerializer(Iterator);
   ~QueryResultSerializer();
 
@@ -92,7 +93,7 @@
   // the limit (it splits on the next row *after* the limit is hit).
   // Overridable for testing only.
   uint32_t cells_per_batch_ = 50000;
-  uint32_t batch_split_threshold_ = 1024 * 128;
+  uint32_t batch_split_threshold_ = kDefaultBatchSplitThreshold;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/rpc/rpc.cc b/src/trace_processor/rpc/rpc.cc
index 7a9a595..a58cd7c 100644
--- a/src/trace_processor/rpc/rpc.cc
+++ b/src/trace_processor/rpc/rpc.cc
@@ -16,38 +16,258 @@
 
 #include "src/trace_processor/rpc/rpc.h"
 
+#include <string.h>
+
 #include <vector>
 
+#include "perfetto/base/logging.h"
 #include "perfetto/base/time.h"
+#include "perfetto/ext/base/utils.h"
+#include "perfetto/ext/base/version.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
+#include "perfetto/protozero/scattered_stream_writer.h"
 #include "perfetto/trace_processor/trace_processor.h"
-#include "protos/perfetto/trace_processor/trace_processor.pbzero.h"
+#include "src/protozero/proto_ring_buffer.h"
 #include "src/trace_processor/rpc/query_result_serializer.h"
 #include "src/trace_processor/tp_metatrace.h"
 
+#include "protos/perfetto/trace_processor/trace_processor.pbzero.h"
+
 namespace perfetto {
 namespace trace_processor {
 
-using ColumnValues = protos::pbzero::RawQueryResult::ColumnValues;
-using ColumnDesc = protos::pbzero::RawQueryResult::ColumnDesc;
-
+namespace {
 // Writes a "Loading trace ..." update every N bytes.
 constexpr size_t kProgressUpdateBytes = 50 * 1000 * 1000;
+using TraceProcessorRpcStream = protos::pbzero::TraceProcessorRpcStream;
+using RpcProto = protos::pbzero::TraceProcessorRpc;
+
+// Most RPC messages are either very small or a query results.
+// QueryResultSerializer splits rows into batches of approximately 128KB. Try
+// avoid extra heap allocations for the nominal case.
+constexpr auto kSliceSize =
+    QueryResultSerializer::kDefaultBatchSplitThreshold + 4096;
+
+// Holds a trace_processor::TraceProcessorRpc pbzero message. Avoids extra
+// copies by doing direct scattered calls from the fragmented heap buffer onto
+// the RpcResponseFunction (the receiver is expected to deal with arbitrary
+// fragmentation anyways). It also takes care of prefixing each message with
+// the proto preamble and varint size.
+class Response {
+ public:
+  Response(int64_t seq, int method);
+  Response(const Response&) = delete;
+  Response& operator=(const Response&) = delete;
+  RpcProto* operator->() { return msg_; }
+  void Send(Rpc::RpcResponseFunction);
+
+ private:
+  RpcProto* msg_ = nullptr;
+
+  // The reason why we use TraceProcessorRpcStream as root message is because
+  // the RPC wire protocol expects each message to be prefixed with a proto
+  // preamble and varint size. This happens to be the same serialization of a
+  // repeated field (this is really the same trick we use between
+  // Trace and TracePacket in trace.proto)
+  protozero::HeapBuffered<TraceProcessorRpcStream> buf_;
+};
+
+Response::Response(int64_t seq, int method) : buf_(kSliceSize, kSliceSize) {
+  msg_ = buf_->add_msg();
+  msg_->set_seq(seq);
+  msg_->set_response(static_cast<RpcProto::TraceProcessorMethod>(method));
+}
+
+void Response::Send(Rpc::RpcResponseFunction send_fn) {
+  buf_->Finalize();
+  for (const auto& slice : buf_.GetSlices()) {
+    auto range = slice.GetUsedRange();
+    send_fn(range.begin, static_cast<uint32_t>(range.size()));
+  }
+}
+
+}  // namespace
 
 Rpc::Rpc(std::unique_ptr<TraceProcessor> preloaded_instance)
-    : trace_processor_(std::move(preloaded_instance)) {}
+    : trace_processor_(std::move(preloaded_instance)) {
+  if (!trace_processor_)
+    ResetTraceProcessor();
+}
 
 Rpc::Rpc() : Rpc(nullptr) {}
-
 Rpc::~Rpc() = default;
 
+void Rpc::ResetTraceProcessor() {
+  trace_processor_ = TraceProcessor::CreateInstance(Config());
+  bytes_parsed_ = bytes_last_progress_ = 0;
+  t_parse_started_ = base::GetWallTimeNs().count();
+  // Deliberately not resetting the RPC channel state (rxbuf_, {tx,rx}_seq_id_).
+  // This is invoked from the same client to clear the current trace state
+  // before loading a new one. The IPC channel is orthogonal to that and the
+  // message numbering continues regardless of the reset.
+}
+
+void Rpc::OnRpcRequest(const void* data, size_t len) {
+  rxbuf_.Append(data, len);
+  for (;;) {
+    auto msg = rxbuf_.ReadMessage();
+    if (!msg.valid()) {
+      if (msg.fatal_framing_error) {
+        protozero::HeapBuffered<TraceProcessorRpcStream> err_msg;
+        err_msg->add_msg()->set_fatal_error("RPC framing error");
+        auto err = err_msg.SerializeAsArray();
+        rpc_response_fn_(err.data(), static_cast<uint32_t>(err.size()));
+        rpc_response_fn_(nullptr, 0);  // Disconnect.
+      }
+      break;
+    }
+    ParseRpcRequest(msg.start, msg.len);
+  }
+}
+
+// [data, len] here is a tokenized TraceProcessorRpc proto message, without the
+// size header.
+void Rpc::ParseRpcRequest(const uint8_t* data, size_t len) {
+  RpcProto::Decoder req(data, len);
+
+  // We allow restarting the sequence from 0. This happens when refreshing the
+  // browser while using the external trace_processor_shell --httpd.
+  if (req.seq() != 0 && rx_seq_id_ != 0 && req.seq() != rx_seq_id_ + 1) {
+    char err_str[255];
+    // "(ERR:rpc_seq)" is intercepted by error_dialog.ts in the UI.
+    sprintf(err_str,
+            "RPC request out of order. Expected %" PRId64 ", got %" PRId64
+            " (ERR:rpc_seq)",
+            rx_seq_id_ + 1, req.seq());
+    PERFETTO_ELOG("%s", err_str);
+    protozero::HeapBuffered<TraceProcessorRpcStream> err_msg;
+    err_msg->add_msg()->set_fatal_error(err_str);
+    auto err = err_msg.SerializeAsArray();
+    rpc_response_fn_(err.data(), static_cast<uint32_t>(err.size()));
+    rpc_response_fn_(nullptr, 0);  // Disconnect.
+    return;
+  }
+  rx_seq_id_ = req.seq();
+
+  // The static cast is to prevent that the compiler breaks future proofness.
+  const int req_type = static_cast<int>(req.request());
+  static const char kErrFieldNotSet[] = "RPC error: request field not set";
+  switch (req_type) {
+    case RpcProto::TPM_APPEND_TRACE_DATA: {
+      Response resp(tx_seq_id_++, req_type);
+      auto* result = resp->set_append_result();
+      if (!req.has_append_trace_data()) {
+        result->set_error(kErrFieldNotSet);
+      } else {
+        protozero::ConstBytes byte_range = req.append_trace_data();
+        util::Status res = Parse(byte_range.data, byte_range.size);
+        if (!res.ok()) {
+          result->set_error(res.message());
+        }
+      }
+      resp.Send(rpc_response_fn_);
+      break;
+    }
+    case RpcProto::TPM_FINALIZE_TRACE_DATA: {
+      Response resp(tx_seq_id_++, req_type);
+      NotifyEndOfFile();
+      resp.Send(rpc_response_fn_);
+      break;
+    }
+    case RpcProto::TPM_QUERY_STREAMING: {
+      if (!req.has_query_args()) {
+        Response resp(tx_seq_id_++, req_type);
+        auto* result = resp->set_query_result();
+        result->set_error(kErrFieldNotSet);
+        resp.Send(rpc_response_fn_);
+      } else {
+        protozero::ConstBytes args = req.query_args();
+        auto it = QueryInternal(args.data, args.size);
+        QueryResultSerializer serializer(std::move(it));
+        for (bool has_more = true; has_more;) {
+          Response resp(tx_seq_id_++, req_type);
+          has_more = serializer.Serialize(resp->set_query_result());
+          resp.Send(rpc_response_fn_);
+        }
+      }
+      break;
+    }
+    case RpcProto::TPM_QUERY_RAW_DEPRECATED: {
+      Response resp(tx_seq_id_++, req_type);
+      auto* result = resp->set_raw_query_result();
+      if (!req.has_raw_query_args()) {
+        result->set_error(kErrFieldNotSet);
+      } else {
+        protozero::ConstBytes args = req.raw_query_args();
+        RawQueryInternal(args.data, args.size, result);
+      }
+      resp.Send(rpc_response_fn_);
+      break;
+    }
+    case RpcProto::TPM_COMPUTE_METRIC: {
+      Response resp(tx_seq_id_++, req_type);
+      auto* result = resp->set_metric_result();
+      if (!req.has_compute_metric_args()) {
+        result->set_error(kErrFieldNotSet);
+      } else {
+        protozero::ConstBytes args = req.compute_metric_args();
+        ComputeMetricInternal(args.data, args.size, result);
+      }
+      resp.Send(rpc_response_fn_);
+      break;
+    }
+    case RpcProto::TPM_GET_METRIC_DESCRIPTORS: {
+      Response resp(tx_seq_id_++, req_type);
+      auto descriptor_set = trace_processor_->GetMetricDescriptors();
+      auto* result = resp->set_metric_descriptors();
+      result->AppendRawProtoBytes(descriptor_set.data(), descriptor_set.size());
+      resp.Send(rpc_response_fn_);
+      break;
+    }
+    case RpcProto::TPM_RESTORE_INITIAL_TABLES: {
+      trace_processor_->RestoreInitialTables();
+      Response resp(tx_seq_id_++, req_type);
+      resp.Send(rpc_response_fn_);
+      break;
+    }
+    case RpcProto::TPM_ENABLE_METATRACE: {
+      trace_processor_->EnableMetatrace();
+      Response resp(tx_seq_id_++, req_type);
+      resp.Send(rpc_response_fn_);
+      break;
+    }
+    case RpcProto::TPM_DISABLE_AND_READ_METATRACE: {
+      Response resp(tx_seq_id_++, req_type);
+      DisableAndReadMetatraceInternal(resp->set_metatrace());
+      resp.Send(rpc_response_fn_);
+      break;
+    }
+    case RpcProto::TPM_GET_STATUS: {
+      Response resp(tx_seq_id_++, req_type);
+      std::vector<uint8_t> status = GetStatus();
+      resp->set_status()->AppendRawProtoBytes(status.data(), status.size());
+      resp.Send(rpc_response_fn_);
+      break;
+    }
+    default: {
+      // This can legitimately happen if the client is newer. We reply with a
+      // generic "unkown request" response, so the client can do feature
+      // detection
+      PERFETTO_DLOG("[RPC] Uknown request type (%d), size=%zu", req_type, len);
+      Response resp(tx_seq_id_++, req_type);
+      resp->set_invalid_request(
+          static_cast<RpcProto::TraceProcessorMethod>(req_type));
+      resp.Send(rpc_response_fn_);
+      break;
+    }
+  }  // switch(req_type)
+}
+
 util::Status Rpc::Parse(const uint8_t* data, size_t len) {
   if (eof_) {
-    // Reset the trace processor state if this is either the first call ever or
-    // if another trace has been previously fully loaded.
-    trace_processor_ = TraceProcessor::CreateInstance(Config());
-    bytes_parsed_ = bytes_last_progress_ = 0;
-    t_parse_started_ = base::GetWallTimeNs().count();
+    // Reset the trace processor state if another trace has been previously
+    // loaded.
+    ResetTraceProcessor();
   }
 
   eof_ = false;
@@ -64,8 +284,6 @@
 }
 
 void Rpc::NotifyEndOfFile() {
-  if (!trace_processor_)
-    return;
   trace_processor_->NotifyEndOfFile();
   eof_ = true;
   MaybePrintProgress();
@@ -88,23 +306,7 @@
 void Rpc::Query(const uint8_t* args,
                 size_t len,
                 QueryResultBatchCallback result_callback) {
-  protos::pbzero::RawQueryArgs::Decoder query(args, len);
-  std::string sql = query.sql_query().ToStdString();
-  PERFETTO_DLOG("[RPC] Query < %s", sql.c_str());
-  PERFETTO_TP_TRACE("RPC_QUERY",
-                    [&](metatrace::Record* r) { r->AddArg("SQL", sql); });
-
-  if (!trace_processor_) {
-    static const char kErr[] = "Query() called before Parse()";
-    PERFETTO_ELOG("[RPC] %s", kErr);
-    protozero::HeapBuffered<protos::pbzero::QueryResult> result;
-    result->set_error(kErr);
-    auto vec = result.SerializeAsArray();
-    result_callback(vec.data(), vec.size(), /*has_more=*/false);
-    return;
-  }
-
-  auto it = trace_processor_->ExecuteQuery(sql.c_str());
+  auto it = QueryInternal(args, len);
   QueryResultSerializer serializer(std::move(it));
 
   std::vector<uint8_t> res;
@@ -115,21 +317,34 @@
   }
 }
 
+Iterator Rpc::QueryInternal(const uint8_t* args, size_t len) {
+  protos::pbzero::RawQueryArgs::Decoder query(args, len);
+  std::string sql = query.sql_query().ToStdString();
+  PERFETTO_DLOG("[RPC] Query < %s", sql.c_str());
+  PERFETTO_TP_TRACE("RPC_QUERY",
+                    [&](metatrace::Record* r) { r->AddArg("SQL", sql); });
+
+  return trace_processor_->ExecuteQuery(sql.c_str());
+}
+
 std::vector<uint8_t> Rpc::RawQuery(const uint8_t* args, size_t len) {
   protozero::HeapBuffered<protos::pbzero::RawQueryResult> result;
+  RawQueryInternal(args, len, result.get());
+  return result.SerializeAsArray();
+}
+
+void Rpc::RawQueryInternal(const uint8_t* args,
+                           size_t len,
+                           protos::pbzero::RawQueryResult* result) {
+  using ColumnValues = protos::pbzero::RawQueryResult::ColumnValues;
+  using ColumnDesc = protos::pbzero::RawQueryResult::ColumnDesc;
+
   protos::pbzero::RawQueryArgs::Decoder query(args, len);
   std::string sql = query.sql_query().ToStdString();
   PERFETTO_DLOG("[RPC] RawQuery < %s", sql.c_str());
   PERFETTO_TP_TRACE("RPC_RAW_QUERY",
                     [&](metatrace::Record* r) { r->AddArg("SQL", sql); });
 
-  if (!trace_processor_) {
-    static const char kErr[] = "RawQuery() called before Parse()";
-    PERFETTO_ELOG("[RPC] %s", kErr);
-    result->set_error(kErr);
-    return result.SerializeAsArray();
-  }
-
   auto it = trace_processor_->ExecuteQuery(sql.c_str());
 
   // This vector contains a standalone protozero message per column. The problem
@@ -244,28 +459,21 @@
   if (!status.ok())
     result->set_error(status.c_message());
   PERFETTO_DLOG("[RPC] RawQuery > %d rows (err: %d)", rows, !status.ok());
-
-  return result.SerializeAsArray();
-}
-
-std::string Rpc::GetCurrentTraceName() {
-  if (!trace_processor_)
-    return "";
-  return trace_processor_->GetCurrentTraceName();
 }
 
 void Rpc::RestoreInitialTables() {
-  if (trace_processor_)
-    trace_processor_->RestoreInitialTables();
+  trace_processor_->RestoreInitialTables();
 }
 
-std::vector<uint8_t> Rpc::ComputeMetric(const uint8_t* data, size_t len) {
+std::vector<uint8_t> Rpc::ComputeMetric(const uint8_t* args, size_t len) {
   protozero::HeapBuffered<protos::pbzero::ComputeMetricResult> result;
-  if (!trace_processor_) {
-    result->set_error("Null trace processor instance");
-    return result.SerializeAsArray();
-  }
+  ComputeMetricInternal(args, len, result.get());
+  return result.SerializeAsArray();
+}
 
+void Rpc::ComputeMetricInternal(const uint8_t* data,
+                                size_t len,
+                                protos::pbzero::ComputeMetricResult* result) {
   protos::pbzero::ComputeMetricArgs::Decoder args(data, len);
   std::vector<std::string> metric_names;
   for (auto it = args.metric_names(); it; ++it) {
@@ -279,15 +487,16 @@
     }
   });
 
+  PERFETTO_DLOG("[RPC] ComputeMetrics(%zu, %s), format=%d", metric_names.size(),
+                metric_names.empty() ? "" : metric_names.front().c_str(),
+                args.format());
   switch (args.format()) {
     case protos::pbzero::ComputeMetricArgs::BINARY_PROTOBUF: {
       std::vector<uint8_t> metrics_proto;
       util::Status status =
           trace_processor_->ComputeMetric(metric_names, &metrics_proto);
       if (status.ok()) {
-        result->AppendBytes(
-            protos::pbzero::ComputeMetricResult::kMetricsFieldNumber,
-            metrics_proto.data(), metrics_proto.size());
+        result->set_metrics(metrics_proto.data(), metrics_proto.size());
       } else {
         result->set_error(status.message());
       }
@@ -299,44 +508,27 @@
           metric_names, TraceProcessor::MetricResultFormat::kProtoText,
           &metrics_string);
       if (status.ok()) {
-        result->AppendString(
-            protos::pbzero::ComputeMetricResult::kMetricsAsPrototextFieldNumber,
-            metrics_string);
+        result->set_metrics_as_prototext(metrics_string);
       } else {
         result->set_error(status.message());
       }
       break;
     }
   }
-  return result.SerializeAsArray();
-}
-
-std::vector<uint8_t> Rpc::GetMetricDescriptors(const uint8_t*, size_t) {
-  protozero::HeapBuffered<protos::pbzero::GetMetricDescriptorsResult> result;
-  if (!trace_processor_) {
-    return result.SerializeAsArray();
-  }
-  std::vector<uint8_t> descriptor_set =
-      trace_processor_->GetMetricDescriptors();
-  result->AppendBytes(
-      protos::pbzero::GetMetricDescriptorsResult::kDescriptorSetFieldNumber,
-      descriptor_set.data(), descriptor_set.size());
-  return result.SerializeAsArray();
 }
 
 void Rpc::EnableMetatrace() {
-  if (!trace_processor_)
-    return;
   trace_processor_->EnableMetatrace();
 }
 
 std::vector<uint8_t> Rpc::DisableAndReadMetatrace() {
   protozero::HeapBuffered<protos::pbzero::DisableAndReadMetatraceResult> result;
-  if (!trace_processor_) {
-    result->set_error("Null trace processor instance");
-    return result.SerializeAsArray();
-  }
+  DisableAndReadMetatraceInternal(result.get());
+  return result.SerializeAsArray();
+}
 
+void Rpc::DisableAndReadMetatraceInternal(
+    protos::pbzero::DisableAndReadMetatraceResult* result) {
   std::vector<uint8_t> trace_proto;
   util::Status status = trace_processor_->DisableAndReadMetatrace(&trace_proto);
   if (status.ok()) {
@@ -344,7 +536,14 @@
   } else {
     result->set_error(status.message());
   }
-  return result.SerializeAsArray();
+}
+
+std::vector<uint8_t> Rpc::GetStatus() {
+  protozero::HeapBuffered<protos::pbzero::StatusResult> status;
+  status->set_loaded_trace_name(trace_processor_->GetCurrentTraceName());
+  status->set_human_readable_version(base::GetVersionString());
+  status->set_api_version(protos::pbzero::TRACE_PROCESSOR_CURRENT_API_VERSION);
+  return status.SerializeAsArray();
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/rpc/rpc.h b/src/trace_processor/rpc/rpc.h
index bddc1aa..8501b56 100644
--- a/src/trace_processor/rpc/rpc.h
+++ b/src/trace_processor/rpc/rpc.h
@@ -25,10 +25,21 @@
 #include <stdint.h>
 
 #include "perfetto/trace_processor/status.h"
+#include "src/protozero/proto_ring_buffer.h"
 
 namespace perfetto {
+
+namespace protos {
+namespace pbzero {
+class RawQueryResult;
+class ComputeMetricResult;
+class DisableAndReadMetatraceResult;
+}  // namespace pbzero
+}  // namespace protos
+
 namespace trace_processor {
 
+class Iterator;
 class TraceProcessor;
 
 // This class handles the binary {,un}marshalling for the Trace Processor RPC
@@ -54,17 +65,53 @@
   Rpc();
   ~Rpc();
 
+  // 1. TraceProcessor byte-pipe RPC interface.
+  // This is a bidirectional channel with a remote TraceProcessor instance. All
+  // it needs is a byte-oriented pipe (e.g., a TCP socket, a pipe(2) between two
+  // processes or a postmessage channel in the JS+Wasm case). The messages
+  // exchanged on these pipes are TraceProcessorRpc protos (defined in
+  // trace_processor.proto). This has been introduced in Perfetto v15.
+
+  // Pushes data received by the RPC channel into the parser. Inbound messages
+  // are tokenized and turned into TraceProcessor method invocations. |data|
+  // does not need to be a whole TraceProcessorRpc message. It can be a portion
+  // of it or a union of >1 messages.
+  // Responses are sent throught the RpcResponseFunction (below).
+  void OnRpcRequest(const void* data, size_t len);
+
+  // The size argument is a uint32_t and not size_t to avoid ABI mismatches
+  // with Wasm, where size_t = uint32_t.
+  // (nullptr, 0) has the semantic of "close the channel" and is issued when an
+  // unrecoverable wire-protocol framing error is detected.
+  using RpcResponseFunction = void (*)(const void* /*data*/, uint32_t /*len*/);
+  void SetRpcResponseFunction(RpcResponseFunction f) { rpc_response_fn_ = f; }
+
+  // 2. TraceProcessor legacy RPC endpoints.
+  // The methods below are exposed for the old RPC interfaces, where each RPC
+  // implementation deals with the method demuxing: (i) wasm_bridge.cc has one
+  // exported C function per method (going away soon); (ii) httpd.cc has one
+  // REST endpoint per method. Over time this turned out to have too much
+  // duplicated boilerplate and we moved to the byte-pipe model above.
+  // We still keep these endpoints around, because httpd.cc still  exposes the
+  // individual REST endpoints to legacy clients (TP's Python API). The
+  // mainteinance cost of those is very low. Both the new byte-pipe and the
+  // old endpoints run exactly the same code. The {de,}serialization format is
+  // the same, the only difference is only who does the method demuxing.
   // The methods of this class are mirrors (modulo {un,}marshalling of args) of
   // the corresponding names in trace_processor.h . See that header for docs.
 
   util::Status Parse(const uint8_t* data, size_t len);
   void NotifyEndOfFile();
-  void RestoreInitialTables();
   std::string GetCurrentTraceName();
   std::vector<uint8_t> ComputeMetric(const uint8_t* data, size_t len);
-  std::vector<uint8_t> GetMetricDescriptors(const uint8_t* data, size_t len);
   void EnableMetatrace();
   std::vector<uint8_t> DisableAndReadMetatrace();
+  std::vector<uint8_t> GetStatus();
+
+  // Creates a new RPC session by deleting all tables and views that have been
+  // created (by the UI or user) after the trace was loaded; built-in
+  // tables/view created by the ingestion process are preserved.
+  void RestoreInitialTables();
 
   // Runs a query and returns results in batch. Each batch is a proto-encoded
   // TraceProcessor.QueryResult message and contains a variable number of rows.
@@ -87,10 +134,25 @@
   std::vector<uint8_t> RawQuery(const uint8_t* args, size_t len);
 
  private:
+  void ParseRpcRequest(const uint8_t* data, size_t len);
+  void ResetTraceProcessor();
   void MaybePrintProgress();
+  Iterator QueryInternal(const uint8_t* args, size_t len);
+  void RawQueryInternal(const uint8_t* args,
+                        size_t len,
+                        protos::pbzero::RawQueryResult*);
+  void ComputeMetricInternal(const uint8_t* args,
+                             size_t len,
+                             protos::pbzero::ComputeMetricResult*);
+  void DisableAndReadMetatraceInternal(
+      protos::pbzero::DisableAndReadMetatraceResult*);
 
   std::unique_ptr<TraceProcessor> trace_processor_;
-  bool eof_ = true;  // Reset when calling Parse().
+  RpcResponseFunction rpc_response_fn_;
+  protozero::ProtoRingBuffer rxbuf_;
+  int64_t tx_seq_id_ = 0;
+  int64_t rx_seq_id_ = 0;
+  bool eof_ = false;
   int64_t t_parse_started_ = 0;
   size_t bytes_last_progress_ = 0;
   size_t bytes_parsed_ = 0;
diff --git a/src/trace_processor/rpc/wasm_bridge.cc b/src/trace_processor/rpc/wasm_bridge.cc
index 79e5dae..6e266b8 100644
--- a/src/trace_processor/rpc/wasm_bridge.cc
+++ b/src/trace_processor/rpc/wasm_bridge.cc
@@ -15,8 +15,6 @@
  */
 
 #include <emscripten/emscripten.h>
-#include <map>
-#include <string>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/trace_processor/trace_processor.h"
@@ -25,24 +23,12 @@
 namespace perfetto {
 namespace trace_processor {
 
-using RequestID = uint32_t;
-
-// Reply(): replies to a RPC method invocation.
-// Called asynchronously (i.e. in a separate task) by the C++ code inside the
-// trace processor to return data for a RPC method call.
-// The function is generic and thankfully we need just one for all methods
-// because the output is always a protobuf buffer.
-using ReplyFunction = void (*)(const char* /*proto_reply_data*/,
-                               uint32_t /*len*/);
-
 namespace {
 Rpc* g_trace_processor_rpc;
-ReplyFunction g_reply;
 
 // The buffer used to pass the request arguments. The caller (JS) decides how
 // big this buffer should be in the Initialize() call.
 uint8_t* g_req_buf;
-
 }  // namespace
 
 // +---------------------------------------------------------------------------+
@@ -51,71 +37,26 @@
 extern "C" {
 
 // Returns the address of the allocated request buffer.
-uint8_t* EMSCRIPTEN_KEEPALIVE Initialize(ReplyFunction, uint32_t);
-uint8_t* Initialize(ReplyFunction reply_function, uint32_t req_buffer_size) {
+uint8_t* EMSCRIPTEN_KEEPALIVE trace_processor_rpc_init(Rpc::RpcResponseFunction,
+                                                       uint32_t);
+uint8_t* trace_processor_rpc_init(Rpc::RpcResponseFunction resp_function,
+                                  uint32_t req_buffer_size) {
   g_trace_processor_rpc = new Rpc();
-  g_reply = reply_function;
+
+  // |resp_function| is a JS-bound function passed by wasm_bridge.ts. It will
+  // call back into JavaScript. There the JS code will copy the passed
+  // buffer with the response (a proto-encoded TraceProcessorRpc message) and
+  // postMessage() it to the controller. See the comment in wasm_bridge.ts for
+  // an overview of the JS<>Wasm callstack.
+  g_trace_processor_rpc->SetRpcResponseFunction(resp_function);
+
   g_req_buf = new uint8_t[req_buffer_size];
   return g_req_buf;
 }
 
-// Ingests trace data.
-void EMSCRIPTEN_KEEPALIVE trace_processor_parse(uint32_t);
-void trace_processor_parse(uint32_t size) {
-  // TODO(primiano): Parse() makes a copy of the data, which is unfortunate.
-  // Ideally there should be a way to take the Blob coming from JS and move it.
-  // See https://github.com/WebAssembly/design/issues/1162.
-  auto status = g_trace_processor_rpc->Parse(g_req_buf, size);
-  if (status.ok()) {
-    g_reply("", 0);
-  } else {
-    PERFETTO_FATAL("Fatal failure while parsing the trace: %s",
-                   status.c_message());
-  }
-}
-
-// We keep the same signature as other methods even though we don't take input
-// arguments for simplicity.
-void EMSCRIPTEN_KEEPALIVE trace_processor_notify_eof(uint32_t);
-void trace_processor_notify_eof(uint32_t /* size, not used. */) {
-  g_trace_processor_rpc->NotifyEndOfFile();
-  g_reply("", 0);
-}
-
-void EMSCRIPTEN_KEEPALIVE trace_processor_raw_query(uint32_t);
-void trace_processor_raw_query(uint32_t size) {
-  std::vector<uint8_t> res = g_trace_processor_rpc->RawQuery(g_req_buf, size);
-  g_reply(reinterpret_cast<const char*>(res.data()),
-          static_cast<uint32_t>(res.size()));
-}
-
-void EMSCRIPTEN_KEEPALIVE trace_processor_compute_metric(uint32_t);
-void trace_processor_compute_metric(uint32_t size) {
-  std::vector<uint8_t> res =
-      g_trace_processor_rpc->ComputeMetric(g_req_buf, size);
-  g_reply(reinterpret_cast<const char*>(res.data()),
-          static_cast<uint32_t>(res.size()));
-}
-
-void EMSCRIPTEN_KEEPALIVE trace_processor_get_metric_descriptors(uint32_t);
-void trace_processor_get_metric_descriptors(uint32_t size) {
-  std::vector<uint8_t> res =
-      g_trace_processor_rpc->GetMetricDescriptors(g_req_buf, size);
-  g_reply(reinterpret_cast<const char*>(res.data()),
-          static_cast<uint32_t>(res.size()));
-}
-
-void EMSCRIPTEN_KEEPALIVE trace_processor_enable_metatrace(uint32_t);
-void trace_processor_enable_metatrace(uint32_t) {
-  g_trace_processor_rpc->EnableMetatrace();
-  g_reply("", 0);
-}
-
-void EMSCRIPTEN_KEEPALIVE trace_processor_disable_and_read_metatrace(uint32_t);
-void trace_processor_disable_and_read_metatrace(uint32_t) {
-  std::vector<uint8_t> res = g_trace_processor_rpc->DisableAndReadMetatrace();
-  g_reply(reinterpret_cast<const char*>(res.data()),
-          static_cast<uint32_t>(res.size()));
+void EMSCRIPTEN_KEEPALIVE trace_processor_on_rpc_request(uint32_t);
+void trace_processor_on_rpc_request(uint32_t size) {
+  g_trace_processor_rpc->OnRpcRequest(g_req_buf, size);
 }
 
 }  // extern "C"
@@ -123,13 +64,13 @@
 }  // namespace perfetto
 
 int main(int, char**) {
-  // This is unused but is needed for the following series of reason:
+  // This is unused but is needed for the following reasons:
   // - We need the callMain() Emscripten JS helper function for traceconv (but
   //   not for trace_processor).
   // - Newer versions of emscripten require that callMain is explicitly exported
   //   via EXTRA_EXPORTED_RUNTIME_METHODS = ['callMain'].
   // - We have one set of EXTRA_EXPORTED_RUNTIME_METHODS for both
-  //   trace_processor.wasm (which does not need a main) and traceconv (which
+  //   trace_processor.wasm (which does not need a main()) and traceconv (which
   //   does).
   // - Without this main(), the Wasm bootstrap code will cause a JS error at
   //   runtime when trying to load trace_processor.js.
diff --git a/src/trace_processor/sqlite/BUILD.gn b/src/trace_processor/sqlite/BUILD.gn
index 52352b7..71b6918 100644
--- a/src/trace_processor/sqlite/BUILD.gn
+++ b/src/trace_processor/sqlite/BUILD.gn
@@ -47,8 +47,8 @@
       "../../../include/perfetto/trace_processor",
       "../../../protos/perfetto/trace/ftrace:zero",
       "../../base",
-      "../db:lib",
-      "../importers:common",
+      "../db",
+      "../importers/common",
       "../storage",
       "../types",
     ]
diff --git a/src/trace_processor/sqlite/span_join_operator_table.cc b/src/trace_processor/sqlite/span_join_operator_table.cc
index c6c3b78..13fea5e 100644
--- a/src/trace_processor/sqlite/span_join_operator_table.cc
+++ b/src/trace_processor/sqlite/span_join_operator_table.cc
@@ -210,7 +210,7 @@
   // Check if any column has : in its name. This often happens when SELECT *
   // is used to create a view with the same column name in two joined tables.
   for (const auto& col : cols) {
-    if (col.name().find(':') != std::string::npos) {
+    if (base::Contains(col.name(), ':')) {
       return util::ErrStatus("SPAN_JOIN: column %s has illegal character :",
                              col.name().c_str());
     }
diff --git a/src/trace_processor/sqlite/sqlite_raw_table.cc b/src/trace_processor/sqlite/sqlite_raw_table.cc
index 0b5b9e9..b02abf4 100644
--- a/src/trace_processor/sqlite/sqlite_raw_table.cc
+++ b/src/trace_processor/sqlite/sqlite_raw_table.cc
@@ -601,10 +601,6 @@
     name = "<idle>";
   } else if (name.empty()) {
     name = "<unknown>";
-  } else if (name == "CrRendererMain") {
-    // TODO(taylori): Remove this when crbug.com/978093 is fixed or
-    // when a better solution is found.
-    name = "CrRendererMainThread";
   }
 
   int64_t padding = 16 - static_cast<int64_t>(name.size());
diff --git a/src/trace_processor/storage/BUILD.gn b/src/trace_processor/storage/BUILD.gn
index 47e1195..844d228 100644
--- a/src/trace_processor/storage/BUILD.gn
+++ b/src/trace_processor/storage/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright (C) 20 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.
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index 28869c5..4a05a74 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -99,11 +99,11 @@
                                         kIndexed, kInfo,     kTrace,    ""),   \
   F(traced_buf_padding_bytes_cleared,   kIndexed, kInfo,     kTrace,    ""),   \
   F(traced_buf_padding_bytes_written,   kIndexed, kInfo,     kTrace,    ""),   \
-  F(traced_buf_patches_failed,          kIndexed, kInfo,     kTrace,    ""),   \
+  F(traced_buf_patches_failed,          kIndexed, kDataLoss, kTrace,    ""),   \
   F(traced_buf_patches_succeeded,       kIndexed, kInfo,     kTrace,    ""),   \
   F(traced_buf_readaheads_failed,       kIndexed, kInfo,     kTrace,    ""),   \
   F(traced_buf_readaheads_succeeded,    kIndexed, kInfo,     kTrace,    ""),   \
-  F(traced_buf_trace_writer_packet_loss,kIndexed, kInfo,     kTrace,    ""),   \
+  F(traced_buf_trace_writer_packet_loss,kIndexed, kDataLoss, kTrace,    ""),   \
   F(traced_buf_write_wrap_count,        kIndexed, kInfo,     kTrace,    ""),   \
   F(traced_chunks_discarded,            kSingle,  kInfo,     kTrace,    ""),   \
   F(traced_data_sources_registered,     kSingle,  kInfo,     kTrace,    ""),   \
@@ -124,6 +124,10 @@
   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(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,    ""),   \
@@ -155,6 +159,10 @@
       "Time spent unwinding callstacks."),                                     \
   F(heapprofd_unwind_samples,           kIndexed, kInfo,     kTrace,           \
       "Number of samples unwound."),                                           \
+  F(heapprofd_client_spinlock_blocked,  kIndexed, kInfo,     kTrace,           \
+       "Time (us) the heapprofd client was blocked on the spinlock."),         \
+  F(heapprofd_last_profile_timestamp,   kIndexed, kInfo,     kTrace,           \
+       "The timestamp (in trace time) for the last dump for a process"),       \
   F(metatrace_overruns,                 kSingle,  kError,    kTrace,    ""),   \
   F(packages_list_has_parse_errors,     kSingle,  kError,    kTrace,    ""),   \
   F(packages_list_has_read_errors,      kSingle,  kError,    kTrace,    ""),   \
diff --git a/src/trace_processor/storage/trace_storage.h b/src/trace_processor/storage/trace_storage.h
index 698aa7c..e2af2ce 100644
--- a/src/trace_processor/storage/trace_storage.h
+++ b/src/trace_processor/storage/trace_storage.h
@@ -442,6 +442,13 @@
   }
   tables::MetadataTable* mutable_metadata_table() { return &metadata_table_; }
 
+  const tables::ClockSnapshotTable& clock_snapshot_table() const {
+    return clock_snapshot_table_;
+  }
+  tables::ClockSnapshotTable* mutable_clock_snapshot_table() {
+    return &clock_snapshot_table_;
+  }
+
   const tables::ArgTable& arg_table() const { return arg_table_; }
   tables::ArgTable* mutable_arg_table() { return &arg_table_; }
 
@@ -719,6 +726,9 @@
   // * descriptions of android packages
   tables::MetadataTable metadata_table_{&string_pool_, nullptr};
 
+  // Contains data from all the clock snapshots in the trace.
+  tables::ClockSnapshotTable clock_snapshot_table_{&string_pool_, nullptr};
+
   // Metadata for tracks.
   tables::TrackTable track_table_{&string_pool_, nullptr};
   tables::GpuTrackTable gpu_track_table_{&string_pool_, &track_table_};
diff --git a/src/trace_processor/tables/BUILD.gn b/src/trace_processor/tables/BUILD.gn
index a6d7e86..66f0cfa 100644
--- a/src/trace_processor/tables/BUILD.gn
+++ b/src/trace_processor/tables/BUILD.gn
@@ -30,7 +30,7 @@
   ]
   deps = [
     "../../../gn:default_deps",
-    "../db:lib",
+    "../db",
   ]
 }
 
diff --git a/src/trace_processor/tables/metadata_tables.h b/src/trace_processor/tables/metadata_tables.h
index fa3136b..3f1bb90 100644
--- a/src/trace_processor/tables/metadata_tables.h
+++ b/src/trace_processor/tables/metadata_tables.h
@@ -113,6 +113,28 @@
 
 PERFETTO_TP_TABLE(PERFETTO_TP_CPU_FREQ_TABLE_DEF);
 
+// Contains all the mapping between clock snapshots and trace time.
+//
+// NOTE: this table is not sorted by timestamp; this is why we omit the
+// sorted flag on the ts column.
+//
+// @param ts            timestamp of the snapshot in trace time.
+// @param clock_id      id of the clock (corresponds to the id in the trace).
+// @param clock_name    the name of the clock for builtin clocks or null
+//                      otherwise.
+// @param clock_value   timestamp of the snapshot in clock time.
+// @param snapshot_id   the index of this snapshot (only useful for debugging)
+#define PERFETTO_TP_CLOCK_SNAPSHOT_TABLE_DEF(NAME, PARENT, C) \
+  NAME(ClockSnapshotTable, "clock_snapshot")                  \
+  PERFETTO_TP_ROOT_TABLE(PARENT, C)                           \
+  C(int64_t, ts)                                              \
+  C(int64_t, clock_id)                                        \
+  C(base::Optional<StringPool::Id>, clock_name)               \
+  C(int64_t, clock_value)                                     \
+  C(uint32_t, snapshot_id)
+
+PERFETTO_TP_TABLE(PERFETTO_TP_CLOCK_SNAPSHOT_TABLE_DEF);
+
 }  // namespace tables
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/tables/profiler_tables.h b/src/trace_processor/tables/profiler_tables.h
index e93c0b4..c51ac91 100644
--- a/src/trace_processor/tables/profiler_tables.h
+++ b/src/trace_processor/tables/profiler_tables.h
@@ -203,7 +203,7 @@
 PERFETTO_TP_TABLE(PERFETTO_TP_PERF_SAMPLE_DEF);
 
 // Symbolization data for a frame. Rows with the same symbol_set_id describe
-// one frame, with the bottom-most inlined frame having id == symbol_set_id.
+// one callframe, with the most-inlined symbol having id == symbol_set_id.
 //
 // For instance, if the function foo has an inlined call to the function bar,
 // which has an inlined call to baz, the stack_profile_symbol table would look
@@ -212,9 +212,9 @@
 // ```
 // |id|symbol_set_id|name         |source_file|line_number|
 // |--|-------------|-------------|-----------|-----------|
-// |1 |      1      |foo          |foo.cc     | 60        |
+// |1 |      1      |baz          |foo.cc     | 36        |
 // |2 |      1      |bar          |foo.cc     | 30        |
-// |3 |      1      |baz          |foo.cc     | 36        |
+// |3 |      1      |foo          |foo.cc     | 60        |
 // ```
 // @param name name of the function.
 // @param source_file name of the source file containing the function.
@@ -282,7 +282,9 @@
   C(int64_t, cumulative_alloc_count)                                      \
   C(int64_t, alloc_size)                                                  \
   C(int64_t, cumulative_alloc_size)                                       \
-  C(base::Optional<ExperimentalFlamegraphNodesTable::Id>, parent_id)
+  C(base::Optional<ExperimentalFlamegraphNodesTable::Id>, parent_id)      \
+  C(base::Optional<StringPool::Id>, source_file)                          \
+  C(base::Optional<uint32_t>, line_number)
 
 PERFETTO_TP_TABLE(PERFETTO_TP_EXPERIMENTAL_FLAMEGRAPH_NODES);
 
@@ -290,7 +292,7 @@
 // @param deobfuscated_name if class name was obfuscated and deobfuscation map
 // for it provided, the deobfuscated name.
 // @param location the APK / Dex / JAR file the class is contained in.
-// @tablegroup ART Heap Profiler
+// @tablegroup ART Heap Graphs
 //
 // classloader_id should really be HeapGraphObject::id, but that would
 // create a loop, which is currently not possible.
@@ -320,7 +322,7 @@
 // false, this object is uncollected garbage.
 // @param type_id class this object is an instance of.
 // @param root_type if not NULL, this object is a GC root.
-// @tablegroup ART Heap Profiler
+// @tablegroup ART Heap Graphs
 #define PERFETTO_TP_HEAP_GRAPH_OBJECT_DEF(NAME, PARENT, C)            \
   NAME(HeapGraphObjectTable, "heap_graph_object")                     \
   PERFETTO_TP_ROOT_TABLE(PARENT, C)                                   \
@@ -346,7 +348,7 @@
 // @param field_type_name the static type of the field. E.g. java.lang.String.
 // @param deobfuscated_field_name if field_name was obfuscated and a
 // deobfuscation mapping was provided for it, the deobfuscated name.
-// @tablegroup ART Heap Profiler
+// @tablegroup ART Heap Graphs
 #define PERFETTO_TP_HEAP_GRAPH_REFERENCE_DEF(NAME, PARENT, C) \
   NAME(HeapGraphReferenceTable, "heap_graph_reference")       \
   PERFETTO_TP_ROOT_TABLE(PARENT, C)                           \
diff --git a/src/trace_processor/tables/slice_tables.h b/src/trace_processor/tables/slice_tables.h
index 54272d8..1cf2cb2 100644
--- a/src/trace_processor/tables/slice_tables.h
+++ b/src/trace_processor/tables/slice_tables.h
@@ -164,6 +164,21 @@
 
 PERFETTO_TP_TABLE(PERFETTO_TP_THREAD_SLICE_DEF);
 
+#define PERFETTO_TP_EXPERIMENTAL_FLAT_SLICE_TABLE_DEF(NAME, PARENT, C) \
+  NAME(ExperimentalFlatSliceTable, "experimental_flat_slice")          \
+  PERFETTO_TP_ROOT_TABLE(PARENT, C)                                    \
+  C(int64_t, ts)                                                       \
+  C(int64_t, dur)                                                      \
+  C(TrackTable::Id, track_id)                                          \
+  C(StringPool::Id, category)                                          \
+  C(StringPool::Id, name)                                              \
+  C(uint32_t, arg_set_id)                                              \
+  C(base::Optional<SliceTable::Id>, source_id)                         \
+  C(int64_t, start_bound, Column::Flag::kHidden)                       \
+  C(int64_t, end_bound, Column::Flag::kHidden)
+
+PERFETTO_TP_TABLE(PERFETTO_TP_EXPERIMENTAL_FLAT_SLICE_TABLE_DEF);
+
 }  // namespace tables
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/tables/table_destructors.cc b/src/trace_processor/tables/table_destructors.cc
index d57b71b..eb9774e 100644
--- a/src/trace_processor/tables/table_destructors.cc
+++ b/src/trace_processor/tables/table_destructors.cc
@@ -45,6 +45,7 @@
 CpuFreqTable::~CpuFreqTable() = default;
 ThreadTable::~ThreadTable() = default;
 ProcessTable::~ProcessTable() = default;
+ClockSnapshotTable::~ClockSnapshotTable() = default;
 
 // profiler_tables.h
 StackProfileMappingTable::~StackProfileMappingTable() = default;
@@ -76,6 +77,7 @@
 ThreadStateTable::~ThreadStateTable() = default;
 ExpectedFrameTimelineSliceTable::~ExpectedFrameTimelineSliceTable() = default;
 ActualFrameTimelineSliceTable::~ActualFrameTimelineSliceTable() = default;
+ExperimentalFlatSliceTable::~ExperimentalFlatSliceTable() = default;
 
 // track_tables.h
 TrackTable::~TrackTable() = default;
diff --git a/src/trace_processor/tables/track_tables.h b/src/trace_processor/tables/track_tables.h
index afcd708..9ba2b4a 100644
--- a/src/trace_processor/tables/track_tables.h
+++ b/src/trace_processor/tables/track_tables.h
@@ -121,8 +121,8 @@
 // @param perf_session_id id of a distict profiling stream.
 //        {@joinable perf_sample.perf_session_id}
 // @param cpu the core the sample was taken on.
-// @is_timebase if true, this counter was the sampling timebase for this
-//              perf_session_id.
+// @param is_timebase if true, this counter was the sampling
+//        timebase for this perf_session_id.
 // @tablegroup Tracks
 #define PERFETTO_TP_PERF_COUNTER_TRACK_DEF(NAME, PARENT, C) \
   NAME(PerfCounterTrackTable, "perf_counter_track")         \
diff --git a/src/trace_processor/timestamped_trace_piece.h b/src/trace_processor/timestamped_trace_piece.h
index e9d49eb..303e234 100644
--- a/src/trace_processor/timestamped_trace_piece.h
+++ b/src/trace_processor/timestamped_trace_piece.h
@@ -24,8 +24,8 @@
 #include "src/trace_processor/importers/proto/packet_sequence_state.h"
 #include "src/trace_processor/importers/systrace/systrace_line.h"
 #include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 // GCC can't figure out the relationship between TimestampedTracePiece's type
 // and the union, and thus thinks that we may be moving or destroying
diff --git a/src/trace_processor/trace_database_integrationtest.cc b/src/trace_processor/trace_database_integrationtest.cc
index e963b54..c6a8c62 100644
--- a/src/trace_processor/trace_database_integrationtest.cc
+++ b/src/trace_processor/trace_database_integrationtest.cc
@@ -41,12 +41,14 @@
       : processor_(TraceProcessor::CreateInstance(Config())) {}
 
  protected:
-  util::Status LoadTrace(const char* name, size_t min_chunk_size = 512) {
-    EXPECT_LE(min_chunk_size, kMaxChunkSize);
+  util::Status LoadTrace(const char* name,
+                         size_t min_chunk_size = 512,
+                         size_t max_chunk_size = kMaxChunkSize) {
+    EXPECT_LE(min_chunk_size, max_chunk_size);
     base::ScopedFstream f(fopen(
         base::GetTestDataPath(std::string("test/data/") + name).c_str(), "rb"));
     std::minstd_rand0 rnd_engine(0);
-    std::uniform_int_distribution<size_t> dist(min_chunk_size, kMaxChunkSize);
+    std::uniform_int_distribution<size_t> dist(min_chunk_size, max_chunk_size);
     while (!feof(*f)) {
       size_t chunk_size = dist(rnd_engine);
       std::unique_ptr<uint8_t[]> buf(new uint8_t[chunk_size]);
@@ -363,6 +365,38 @@
   ASSERT_EQ(it.Get(0).long_value, 276174);
 }
 
+/*
+ * This trace does not have a uuid. The uuid will be generated from the first
+ * 4096 bytes, which will be read in one chunk.
+ */
+TEST_F(TraceProcessorIntegrationTest, TraceWithoutUuidReadInOneChunk) {
+  ASSERT_TRUE(LoadTrace("example_android_trace_30s.pb", kMaxChunkSize).ok());
+  auto it = Query("select str_value from metadata where name = 'trace_uuid'");
+  ASSERT_TRUE(it.Next());
+  EXPECT_STREQ(it.Get(0).string_value, "00000000-0000-0000-8906-ebb53e1d0738");
+}
+
+/*
+ * This trace does not have a uuid. The uuid will be generated from the first
+ * 4096 bytes, which will be read in multiple chunks.
+ */
+TEST_F(TraceProcessorIntegrationTest, TraceWithoutUuidReadInMultipleChuncks) {
+  ASSERT_TRUE(LoadTrace("example_android_trace_30s.pb", 512, 2048).ok());
+  auto it = Query("select str_value from metadata where name = 'trace_uuid'");
+  ASSERT_TRUE(it.Next());
+  EXPECT_STREQ(it.Get(0).string_value, "00000000-0000-0000-8906-ebb53e1d0738");
+}
+
+/*
+ * This trace has a uuid. It will not be overriden by the hash of the first 4096
+ * bytes.
+ */
+TEST_F(TraceProcessorIntegrationTest, TraceWithUuidReadInParts) {
+  ASSERT_TRUE(LoadTrace("trace_with_uuid.pftrace", 512, 2048).ok());
+  auto it = Query("select str_value from metadata where name = 'trace_uuid'");
+  ASSERT_TRUE(it.Next());
+  EXPECT_STREQ(it.Get(0).string_value, "123e4567-e89b-12d3-a456-426655443322");
+}
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/trace_processor_context.cc b/src/trace_processor/trace_processor_context.cc
index 1524bf0..412b63a 100644
--- a/src/trace_processor/trace_processor_context.cc
+++ b/src/trace_processor/trace_processor_context.cc
@@ -16,9 +16,9 @@
 
 #include "src/trace_processor/types/trace_processor_context.h"
 
-#include "src/trace_processor/chunked_trace_reader.h"
 #include "src/trace_processor/forwarding_trace_parser.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index ca74685..69c0183 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -27,8 +27,10 @@
 #include "src/trace_processor/dynamic/connected_flow_generator.h"
 #include "src/trace_processor/dynamic/descendant_slice_generator.h"
 #include "src/trace_processor/dynamic/describe_slice_generator.h"
+#include "src/trace_processor/dynamic/experimental_annotated_stack_generator.h"
 #include "src/trace_processor/dynamic/experimental_counter_dur_generator.h"
 #include "src/trace_processor/dynamic/experimental_flamegraph_generator.h"
+#include "src/trace_processor/dynamic/experimental_flat_slice_generator.h"
 #include "src/trace_processor/dynamic/experimental_sched_upid_generator.h"
 #include "src/trace_processor/dynamic/experimental_slice_layout_generator.h"
 #include "src/trace_processor/dynamic/thread_state_generator.h"
@@ -692,7 +694,7 @@
 
   context_.systrace_trace_parser.reset(new SystraceTraceParser(&context_));
 
-  if (gzip::IsGzipSupported())
+  if (util::IsGzipSupported())
     context_.gzip_trace_parser.reset(new GzipTraceParser(&context_));
 
   if (json::IsJsonSupported()) {
@@ -766,6 +768,10 @@
                                          storage->thread_table())));
   RegisterDynamicTable(std::unique_ptr<ThreadStateGenerator>(
       new ThreadStateGenerator(&context_)));
+  RegisterDynamicTable(std::unique_ptr<ExperimentalAnnotatedStackGenerator>(
+      new ExperimentalAnnotatedStackGenerator(&context_)));
+  RegisterDynamicTable(std::unique_ptr<ExperimentalFlatSliceGenerator>(
+      new ExperimentalFlatSliceGenerator(&context_)));
 
   // New style db-backed tables.
   RegisterDbTable(storage->arg_table());
@@ -822,6 +828,7 @@
   RegisterDbTable(storage->metadata_table());
   RegisterDbTable(storage->cpu_table());
   RegisterDbTable(storage->cpu_freq_table());
+  RegisterDbTable(storage->clock_snapshot_table());
 
   RegisterDbTable(storage->memory_snapshot_table());
   RegisterDbTable(storage->process_memory_snapshot_table());
@@ -873,6 +880,7 @@
 }
 
 size_t TraceProcessorImpl::RestoreInitialTables() {
+  // Step 1: figure out what tables/views/indices we need to delete.
   std::vector<std::pair<std::string, std::string>> deletion_list;
   std::string msg = "Resetting DB to initial state, deleting table/views:";
   for (auto it = ExecuteQuery(kAllTablesQuery); it.Next();) {
@@ -886,6 +894,8 @@
   }
 
   PERFETTO_LOG("%s", msg.c_str());
+
+  // Step 2: actually delete those tables/views/indices.
   for (const auto& tn : deletion_list) {
     std::string query = "DROP " + tn.first + " " + tn.second;
     auto it = ExecuteQuery(query);
@@ -1026,7 +1036,7 @@
     return util::Status("Root metrics proto descriptor not found");
 
   const auto& root_descriptor = pool_.descriptors()[opt_idx.value()];
-  return metrics::ComputeMetrics(this, metric_names, sql_metrics_,
+  return metrics::ComputeMetrics(this, metric_names, sql_metrics_, pool_,
                                  root_descriptor, metrics_proto);
 }
 
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index f7c656c..6925948 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -45,6 +45,8 @@
 #include "src/trace_processor/util/proto_to_json.h"
 #include "src/trace_processor/util/status_macros.h"
 
+#include "protos/perfetto/trace_processor/trace_processor.pbzero.h"
+
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_HTTPD)
 #include "src/trace_processor/rpc/httpd.h"
 #endif
@@ -265,7 +267,7 @@
       "SELECT name FROM sqlite_master WHERE type='table'");
   for (uint32_t rows = 0; tables_it.Next(); rows++) {
     std::string table_name = tables_it.Get(0).string_value;
-    PERFETTO_CHECK(table_name.find('\'') == std::string::npos);
+    PERFETTO_CHECK(!base::Contains(table_name, '\''));
     std::string export_sql = "CREATE TABLE perfetto_export." + table_name +
                              " AS SELECT * FROM " + table_name;
 
@@ -580,6 +582,7 @@
     if (it.Next()) {
       return util::ErrStatus("Unexpected result from a query.");
     }
+    RETURN_IF_ERROR(it.Status());
   }
   return util::OkStatus();
 }
@@ -600,6 +603,7 @@
     RETURN_IF_ERROR(it.Status());
     if (it.ColumnCount() == 0) {
       bool it_has_more = it.Next();
+      RETURN_IF_ERROR(it.Status());
       PERFETTO_DCHECK(!it_has_more);
       continue;
     }
@@ -754,6 +758,8 @@
 
     if (option == 'v') {
       printf("%s\n", base::GetVersionString());
+      printf("Trace Processor RPC API version: %d\n",
+             protos::pbzero::TRACE_PROCESSOR_CURRENT_API_VERSION);
       exit(0);
     }
 
diff --git a/src/trace_processor/trace_processor_storage_impl.cc b/src/trace_processor/trace_processor_storage_impl.cc
index 3cdddd7..b71eac7 100644
--- a/src/trace_processor/trace_processor_storage_impl.cc
+++ b/src/trace_processor/trace_processor_storage_impl.cc
@@ -17,7 +17,9 @@
 #include "src/trace_processor/trace_processor_storage_impl.h"
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/uuid.h"
 #include "src/trace_processor/forwarding_trace_parser.h"
+#include "src/trace_processor/importers/chrome_track_event.descriptor.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
@@ -26,7 +28,6 @@
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/default_modules.h"
-#include "src/trace_processor/importers/proto/args_table_utils.h"
 #include "src/trace_processor/importers/proto/async_track_set_tracker.h"
 #include "src/trace_processor/importers/proto/heap_profile_tracker.h"
 #include "src/trace_processor/importers/proto/metadata_tracker.h"
@@ -34,8 +35,9 @@
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 #include "src/trace_processor/importers/proto/stack_profile_tracker.h"
-#include "src/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/track_event.descriptor.h"
 #include "src/trace_processor/trace_sorter.h"
+#include "src/trace_processor/util/descriptors.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -57,7 +59,18 @@
   context_.global_stack_profile_tracker.reset(new GlobalStackProfileTracker());
   context_.metadata_tracker.reset(new MetadataTracker(&context_));
   context_.global_args_tracker.reset(new GlobalArgsTracker(&context_));
-  context_.proto_to_args_table_.reset(new ProtoToArgsTable(&context_));
+  {
+    context_.descriptor_pool_.reset(new DescriptorPool());
+    auto status = context_.descriptor_pool_->AddFromFileDescriptorSet(
+        kTrackEventDescriptor.data(), kTrackEventDescriptor.size());
+
+    PERFETTO_DCHECK(status.ok());
+
+    status = context_.descriptor_pool_->AddFromFileDescriptorSet(
+        kChromeTrackEventDescriptor.data(), kChromeTrackEventDescriptor.size());
+
+    PERFETTO_DCHECK(status.ok());
+  }
 
   context_.slice_tracker->SetOnSliceBeginCallback(
       [this](TrackId track_id, SliceId slice_id) {
@@ -81,6 +94,19 @@
 
   auto scoped_trace = context_.storage->TraceExecutionTimeIntoStats(
       stats::parse_trace_duration_ns);
+
+  if (hash_input_size_remaining_ > 0 && !context_.uuid_found_in_trace) {
+    const size_t hash_size = std::min(hash_input_size_remaining_, size);
+    hash_input_size_remaining_ -= hash_size;
+
+    trace_hash_.Update(reinterpret_cast<const char*>(data.get()), hash_size);
+    base::Uuid uuid(static_cast<int64_t>(trace_hash_.digest()), 0);
+    const StringId id_for_uuid =
+        context_.storage->InternString(base::StringView(uuid.ToPrettyString()));
+    context_.metadata_tracker->SetMetadata(metadata::trace_uuid,
+                                           Variadic::String(id_for_uuid));
+  }
+
   util::Status status = context_.chunk_reader->Parse(std::move(data), size);
   unrecoverable_parse_error_ |= !status.ok();
   return status;
diff --git a/src/trace_processor/trace_processor_storage_impl.h b/src/trace_processor/trace_processor_storage_impl.h
index 1c029c6..4b225d0 100644
--- a/src/trace_processor/trace_processor_storage_impl.h
+++ b/src/trace_processor/trace_processor_storage_impl.h
@@ -19,6 +19,7 @@
 
 #include <memory>
 
+#include "perfetto/ext/base/hash.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/status.h"
 #include "perfetto/trace_processor/trace_processor_storage.h"
@@ -38,8 +39,10 @@
   TraceProcessorContext* context() { return &context_; }
 
  protected:
+  base::Hash trace_hash_;
   TraceProcessorContext context_;
   bool unrecoverable_parse_error_ = false;
+  size_t hash_input_size_remaining_ = 4096;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/trace_sorter.cc b/src/trace_processor/trace_sorter.cc
index 677d31a..c1bc9e5 100644
--- a/src/trace_processor/trace_sorter.cc
+++ b/src/trace_processor/trace_sorter.cc
@@ -127,22 +127,11 @@
     int64_t extract_until_ts = std::min(extract_end_ts, min_queue_ts[1]);
     size_t num_extracted = 0;
     for (auto& event : events) {
-      int64_t timestamp = event.timestamp;
-      if (timestamp > extract_until_ts)
+      if (event.timestamp > extract_until_ts)
         break;
 
       ++num_extracted;
-      if (bypass_next_stage_for_testing_)
-        continue;
-
-      if (min_queue_idx == 0) {
-        // queues_[0] is for non-ftrace packets.
-        parser_->ParseTracePacket(timestamp, std::move(event));
-      } else {
-        // Ftrace queues start at offset 1. So queues_[1] = cpu[0] and so on.
-        uint32_t cpu = static_cast<uint32_t>(min_queue_idx - 1);
-        parser_->ParseFtracePacket(cpu, timestamp, std::move(event));
-      }
+      MaybePushEvent(min_queue_idx, std::move(event));
     }  // for (event: events)
 
     if (!num_extracted) {
@@ -192,5 +181,20 @@
 #endif
 }
 
+void TraceSorter::MaybePushEvent(size_t queue_idx, TimestampedTracePiece ttp) {
+  if (PERFETTO_UNLIKELY(bypass_next_stage_for_testing_))
+    return;
+
+  int64_t timestamp = ttp.timestamp;
+  if (queue_idx == 0) {
+    // queues_[0] is for non-ftrace packets.
+    parser_->ParseTracePacket(timestamp, std::move(ttp));
+  } else {
+    // Ftrace queues start at offset 1. So queues_[1] = cpu[0] and so on.
+    uint32_t cpu = static_cast<uint32_t>(queue_idx - 1);
+    parser_->ParseFtracePacket(cpu, timestamp, std::move(ttp));
+  }
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/trace_sorter.h b/src/trace_processor/trace_sorter.h
index db21656..a93cd26 100644
--- a/src/trace_processor/trace_sorter.h
+++ b/src/trace_processor/trace_sorter.h
@@ -23,7 +23,7 @@
 #include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/timestamped_trace_piece.h"
-#include "src/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/util/trace_blob_view.h"
 
 namespace Json {
 class Value;
@@ -71,36 +71,36 @@
                               PacketSequenceState* state,
                               TraceBlobView packet) {
     DCHECK_ftrace_batch_cpu(kNoBatch);
-    auto* queue = GetQueue(0);
-    queue->Append(TimestampedTracePiece(timestamp, packet_idx_++,
-                                        std::move(packet),
-                                        state->current_generation()));
-    MaybeExtractEvents(queue);
+    AppendNonFtraceAndMaybeExtractEvents(
+        TimestampedTracePiece(timestamp, packet_idx_++, std::move(packet),
+                              state->current_generation()));
   }
 
   inline void PushJsonValue(int64_t timestamp, std::string json_value) {
-    auto* queue = GetQueue(0);
-    queue->Append(
+    DCHECK_ftrace_batch_cpu(kNoBatch);
+    AppendNonFtraceAndMaybeExtractEvents(
         TimestampedTracePiece(timestamp, packet_idx_++, std::move(json_value)));
-    MaybeExtractEvents(queue);
   }
 
   inline void PushFuchsiaRecord(int64_t timestamp,
                                 std::unique_ptr<FuchsiaRecord> record) {
     DCHECK_ftrace_batch_cpu(kNoBatch);
-    auto* queue = GetQueue(0);
-    queue->Append(
+    AppendNonFtraceAndMaybeExtractEvents(
         TimestampedTracePiece(timestamp, packet_idx_++, std::move(record)));
-    MaybeExtractEvents(queue);
   }
 
   inline void PushSystraceLine(std::unique_ptr<SystraceLine> systrace_line) {
     DCHECK_ftrace_batch_cpu(kNoBatch);
-    auto* queue = GetQueue(0);
+
     int64_t timestamp = systrace_line->ts;
-    queue->Append(TimestampedTracePiece(timestamp, packet_idx_++,
-                                        std::move(systrace_line)));
-    MaybeExtractEvents(queue);
+    AppendNonFtraceAndMaybeExtractEvents(TimestampedTracePiece(
+        timestamp, packet_idx_++, std::move(systrace_line)));
+  }
+
+  inline void PushTrackEventPacket(int64_t timestamp,
+                                   std::unique_ptr<TrackEventData> data) {
+    AppendNonFtraceAndMaybeExtractEvents(
+        TimestampedTracePiece(timestamp, packet_idx_++, std::move(data)));
   }
 
   inline void PushFtraceEvent(uint32_t cpu,
@@ -117,20 +117,20 @@
     // global ordering and doing that in batches only after all ftrace events
     // for a bundle are pushed.
   }
-
-  // As with |PushFtraceEvent|, doesn't immediately sort the affected queues.
-  // TODO(rsavitski): if a trace has a mix of normal & "compact" events (being
-  // pushed through this function), the ftrace batches will no longer be fully
-  // sorted by timestamp. In such situations, we will have to sort at the end of
-  // the batch. We can do better as both sub-sequences are sorted however.
-  // Consider adding extra queues, or pushing them in a merge-sort fashion
-  // instead.
   inline void PushInlineFtraceEvent(uint32_t cpu,
                                     int64_t timestamp,
                                     InlineSchedSwitch inline_sched_switch) {
     set_ftrace_batch_cpu_for_DCHECK(cpu);
     GetQueue(cpu + 1)->Append(
         TimestampedTracePiece(timestamp, packet_idx_++, inline_sched_switch));
+
+    // As with |PushFtraceEvent|, doesn't immediately sort the affected queues.
+    // TODO(rsavitski): if a trace has a mix of normal & "compact" events (being
+    // pushed through this function), the ftrace batches will no longer be fully
+    // sorted by timestamp. In such situations, we will have to sort at the end
+    // of the batch. We can do better as both sub-sequences are sorted however.
+    // Consider adding extra queues, or pushing them in a merge-sort fashion
+    // instead.
   }
   inline void PushInlineFtraceEvent(uint32_t cpu,
                                     int64_t timestamp,
@@ -139,15 +139,6 @@
     GetQueue(cpu + 1)->Append(
         TimestampedTracePiece(timestamp, packet_idx_++, inline_sched_waking));
   }
-
-  inline void PushTrackEventPacket(int64_t timestamp,
-                                   std::unique_ptr<TrackEventData> data) {
-    auto* queue = GetQueue(0);
-    queue->Append(
-        TimestampedTracePiece(timestamp, packet_idx_++, std::move(data)));
-    MaybeExtractEvents(queue);
-  }
-
   inline void FinalizeFtraceEventBatch(uint32_t cpu) {
     DCHECK_ftrace_batch_cpu(cpu);
     set_ftrace_batch_cpu_for_DCHECK(kNoBatch);
@@ -229,6 +220,32 @@
     return &queues_[index];
   }
 
+  inline void AppendNonFtraceAndMaybeExtractEvents(TimestampedTracePiece ttp) {
+    // Fast path: if this event is before all other events in the sorter and
+    // happened more than the window size in the past, just push the event to
+    // the next stage. This saves all the sorting logic which would simply move
+    // this event to the head of the queue and then extract it out.
+    //
+    // In practice, these events will be rejected as being "out-of-order" later
+    // on in trace processor (i.e. in EventTracker or SliceTracker); we don't
+    // drop here to allow them to track packet drop stats.
+    //
+    // See b/188392852 for an example of where this condition would be hit in
+    // practice.
+    bool is_before_all_events = ttp.timestamp < global_max_ts_;
+    bool is_before_window = global_max_ts_ - ttp.timestamp >= window_size_ns_;
+    if (PERFETTO_UNLIKELY(is_before_all_events && is_before_window)) {
+      MaybePushEvent(0, std::move(ttp));
+      return;
+    }
+
+    // Slow path: append the event to the non-ftrace queue and extract any
+    // events if available.
+    Queue* queue = GetQueue(0);
+    queue->Append(std::move(ttp));
+    MaybeExtractEvents(queue);
+  }
+
   inline void MaybeExtractEvents(Queue* queue) {
     DCHECK_ftrace_batch_cpu(kNoBatch);
     global_max_ts_ = std::max(global_max_ts_, queue->max_ts_);
@@ -240,6 +257,9 @@
     SortAndExtractEventsBeyondWindow(window_size_ns_);
   }
 
+  void MaybePushEvent(size_t queue_idx,
+                      TimestampedTracePiece ttp) PERFETTO_ALWAYS_INLINE;
+
   std::unique_ptr<TraceParser> parser_;
 
   // queues_[0] is the general (non-ftrace) queue.
diff --git a/src/trace_processor/types/gfp_flags.cc b/src/trace_processor/types/gfp_flags.cc
index 30a5932..acb52fb 100644
--- a/src/trace_processor/types/gfp_flags.cc
+++ b/src/trace_processor/types/gfp_flags.cc
@@ -208,7 +208,7 @@
   } else if (version >= VersionNumber{4, 4} && version < VersionNumber{4, 14}) {
     return &v4_4;
   } else {  // version >= 4.14
-    // TODO(taylori): Add newer kernel versions once we have access to them.
+    // TODO(hjd): Add newer kernel versions once we have access to them.
     return &v4_14;
   }
 }
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index eb92381..1cf723c 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -49,7 +49,7 @@
 class TraceStorage;
 class TrackTracker;
 class JsonTracker;
-class ProtoToArgsTable;
+class DescriptorPool;
 
 class TraceProcessorContext {
  public:
@@ -109,14 +109,20 @@
   std::unique_ptr<TraceParser> json_trace_parser;
   std::unique_ptr<TraceParser> fuchsia_trace_parser;
 
-  // Reflection-based proto parser used to convert TrackEvent fields into SQL.
-  std::unique_ptr<ProtoToArgsTable> proto_to_args_table_;
+  // This field contains the list of proto descriptors that can be used by
+  // reflection-based parsers.
+  std::unique_ptr<DescriptorPool> descriptor_pool_;
 
   // The module at the index N is registered to handle field id N in
   // TracePacket.
   std::vector<std::vector<ProtoImporterModule*>> modules_by_field;
   std::vector<std::unique_ptr<ProtoImporterModule>> modules;
   FtraceModule* ftrace_module = nullptr;
+
+  // Marks whether the uuid was read from the trace.
+  // If the uuid was NOT read, the uuid will be made from the hash of the first
+  // 4KB of the trace.
+  bool uuid_found_in_trace = false;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/util/BUILD.gn b/src/trace_processor/util/BUILD.gn
index d763125..055cc81 100644
--- a/src/trace_processor/util/BUILD.gn
+++ b/src/trace_processor/util/BUILD.gn
@@ -14,6 +14,13 @@
 
 import("../../../gn/perfetto.gni")
 
+# Track event args parsing logic here is tentatitively planned to eventually
+# move to src/util and will be used to implement writing typed args in console
+# interceptor.
+# Do not add new dependencies to trace_processor code outside of this directory.
+#
+# TODO(altimin): Move it to src/util and use it in console interceptor.
+
 source_set("util") {
   sources = [ "status_macros.h" ]
   deps = [
@@ -22,6 +29,22 @@
   ]
 }
 
+source_set("gzip") {
+  sources = [
+    "gzip_utils.cc",
+    "gzip_utils.h",
+  ]
+  deps = [
+    "../../../gn:default_deps",
+    "../../../include/perfetto/base",
+  ]
+
+  # gzip_utils optionally depends on zlib.
+  if (enable_perfetto_zlib) {
+    deps += [ "../../../gn:zlib" ]
+  }
+}
+
 source_set("protozero_to_text") {
   sources = [
     "protozero_to_text.cc",
@@ -38,6 +61,23 @@
   ]
 }
 
+source_set("trace_blob_view") {
+  sources = [ "trace_blob_view.h" ]
+  deps = [
+    "../../../gn:default_deps",
+    "../../base",
+  ]
+}
+
+source_set("interned_message_view") {
+  sources = [ "interned_message_view.h" ]
+  deps = [
+    "../../../gn:default_deps",
+    "../../base",
+  ]
+  public_deps = [ ":trace_blob_view" ]
+}
+
 source_set("descriptors") {
   sources = [
     "descriptors.cc",
@@ -46,7 +86,6 @@
   deps = [
     ":util",
     "../../../gn:default_deps",
-    "../../../include/perfetto/trace_processor",
     "../../../protos/perfetto/common:zero",
     "../../../protos/perfetto/trace_processor:zero",
     "../../base",
@@ -55,16 +94,49 @@
   ]
 }
 
+source_set("proto_to_args_parser") {
+  sources = [
+    "debug_annotation_parser.cc",
+    "debug_annotation_parser.h",
+    "proto_to_args_parser.cc",
+    "proto_to_args_parser.h",
+  ]
+  deps = [
+    "../../../gn:default_deps",
+    "../../../protos/perfetto/common:zero",
+    "../../../protos/perfetto/trace/interned_data:zero",
+    "../../../protos/perfetto/trace/track_event:zero",
+    "../../../protos/perfetto/trace_processor:zero",
+    "../../protozero",
+    "../importers:gen_cc_track_event_descriptor",
+  ]
+
+  public_deps = [
+    ":descriptors",
+    ":interned_message_view",
+    ":util",
+    "../../base",
+  ]
+}
+
 source_set("unittests") {
-  sources = [ "protozero_to_text_unittests.cc" ]
+  sources = [
+    "debug_annotation_parser_unittest.cc",
+    "proto_to_args_parser_unittest.cc",
+    "protozero_to_text_unittests.cc",
+  ]
   testonly = true
   deps = [
     ":descriptors",
+    ":proto_to_args_parser",
     ":protozero_to_text",
+    "..:gen_cc_test_messages_descriptor",
     "../../../gn:default_deps",
     "../../../gn:gtest_and_gmock",
+    "../../../protos/perfetto/common:zero",
     "../../../protos/perfetto/trace/track_event:zero",
     "../../protozero",
+    "../../protozero:testing_messages_zero",
     "../importers:gen_cc_track_event_descriptor",
   ]
 }
diff --git a/src/trace_processor/util/debug_annotation_parser.cc b/src/trace_processor/util/debug_annotation_parser.cc
new file mode 100644
index 0000000..7fa359e
--- /dev/null
+++ b/src/trace_processor/util/debug_annotation_parser.cc
@@ -0,0 +1,226 @@
+/*
+ * 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/trace_processor/util/debug_annotation_parser.h"
+#include "perfetto/base/build_config.h"
+#include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
+#include "src/trace_processor/util/interned_message_view.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace util {
+
+namespace {
+
+std::string SanitizeDebugAnnotationName(const std::string& raw_name) {
+  std::string result = raw_name;
+  std::replace(result.begin(), result.end(), '.', '_');
+  std::replace(result.begin(), result.end(), '[', '_');
+  std::replace(result.begin(), result.end(), ']', '_');
+  return result;
+}
+
+bool IsJsonSupported() {
+#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
+  return true;
+#else
+  return false;
+#endif
+}
+
+}  // namespace
+
+DebugAnnotationParser::DebugAnnotationParser(ProtoToArgsParser& parser)
+    : proto_to_args_parser_(parser) {}
+
+base::Status DebugAnnotationParser::ParseDebugAnnotationName(
+    protos::pbzero::DebugAnnotation::Decoder& annotation,
+    ProtoToArgsParser::Delegate& delegate,
+    std::string& result) {
+  uint64_t name_iid = annotation.name_iid();
+  if (PERFETTO_LIKELY(name_iid)) {
+    auto* decoder = delegate.GetInternedMessage(
+        protos::pbzero::InternedData::kDebugAnnotationNames, name_iid);
+    if (!decoder)
+      return base::ErrStatus("Debug annotation with invalid name_iid");
+
+    result = SanitizeDebugAnnotationName(decoder->name().ToStdString());
+  } else if (annotation.has_name()) {
+    result = SanitizeDebugAnnotationName(annotation.name().ToStdString());
+  } else {
+    return base::ErrStatus("Debug annotation without name");
+  }
+  return base::OkStatus();
+}
+
+DebugAnnotationParser::ParseResult
+DebugAnnotationParser::ParseDebugAnnotationValue(
+    protos::pbzero::DebugAnnotation::Decoder& annotation,
+    ProtoToArgsParser::Delegate& delegate,
+    const ProtoToArgsParser::Key& context_name) {
+  if (annotation.has_bool_value()) {
+    delegate.AddBoolean(context_name, annotation.bool_value());
+  } else if (annotation.has_uint_value()) {
+    delegate.AddUnsignedInteger(context_name, annotation.uint_value());
+  } else if (annotation.has_int_value()) {
+    delegate.AddInteger(context_name, annotation.int_value());
+  } else if (annotation.has_double_value()) {
+    delegate.AddDouble(context_name, annotation.double_value());
+  } else if (annotation.has_string_value()) {
+    delegate.AddString(context_name, annotation.string_value());
+  } else if (annotation.has_pointer_value()) {
+    delegate.AddPointer(context_name, reinterpret_cast<const void*>(
+                                          annotation.pointer_value()));
+  } else if (annotation.has_dict_entries()) {
+    bool added_entry = false;
+    for (auto it = annotation.dict_entries(); it; ++it) {
+      protos::pbzero::DebugAnnotation::Decoder key_value(*it);
+      std::string key;
+      base::Status key_parse_result =
+          ParseDebugAnnotationName(key_value, delegate, key);
+      if (!key_parse_result.ok())
+        return {key_parse_result, added_entry};
+
+      auto nested_key = proto_to_args_parser_.EnterDictionary(key);
+      ParseResult value_parse_result =
+          ParseDebugAnnotationValue(key_value, delegate, nested_key.key());
+      added_entry |= value_parse_result.added_entry;
+      if (!value_parse_result.status.ok())
+        return {value_parse_result.status, added_entry};
+    }
+  } else if (annotation.has_array_values()) {
+    size_t index = delegate.GetArrayEntryIndex(context_name.key);
+    bool added_entry = false;
+    for (auto it = annotation.array_values(); it; ++it) {
+      std::string array_key = context_name.key;
+      protos::pbzero::DebugAnnotation::Decoder value(*it);
+
+      auto nested_key = proto_to_args_parser_.EnterArray(index);
+      ParseResult value_parse_result =
+          ParseDebugAnnotationValue(value, delegate, nested_key.key());
+
+      if (value_parse_result.added_entry) {
+        index = delegate.IncrementArrayEntryIndex(array_key);
+        added_entry = true;
+      }
+      if (!value_parse_result.status.ok())
+        return {value_parse_result.status, added_entry};
+    }
+  } else if (annotation.has_legacy_json_value()) {
+    if (!IsJsonSupported())
+      return {base::ErrStatus("Ignoring legacy_json_value (no json support)"),
+              false};
+
+    bool added_entry =
+        delegate.AddJson(context_name, annotation.legacy_json_value());
+    return {base::OkStatus(), added_entry};
+  } else if (annotation.has_nested_value()) {
+    return ParseNestedValueArgs(annotation.nested_value(), context_name,
+                                delegate);
+  } else {
+    return {base::OkStatus(), /*added_entry=*/false};
+  }
+
+  return {base::OkStatus(), /*added_entry=*/true};
+}
+
+// static
+base::Status DebugAnnotationParser::Parse(
+    protozero::ConstBytes data,
+    ProtoToArgsParser::Delegate& delegate) {
+  protos::pbzero::DebugAnnotation::Decoder annotation(data);
+
+  std::string name;
+  base::Status name_parse_result =
+      ParseDebugAnnotationName(annotation, delegate, name);
+  if (!name_parse_result.ok())
+    return name_parse_result;
+
+  auto context = proto_to_args_parser_.EnterDictionary(name);
+
+  return ParseDebugAnnotationValue(annotation, delegate, context.key()).status;
+}
+
+DebugAnnotationParser::ParseResult DebugAnnotationParser::ParseNestedValueArgs(
+    protozero::ConstBytes nested_value,
+    const ProtoToArgsParser::Key& context_name,
+    ProtoToArgsParser::Delegate& delegate) {
+  protos::pbzero::DebugAnnotation::NestedValue::Decoder value(nested_value);
+  switch (value.nested_type()) {
+    case protos::pbzero::DebugAnnotation::NestedValue::UNSPECIFIED: {
+      // Leaf value.
+      if (value.has_bool_value()) {
+        delegate.AddBoolean(context_name, value.bool_value());
+        return {base::OkStatus(), true};
+      }
+      if (value.has_int_value()) {
+        delegate.AddInteger(context_name, value.int_value());
+        return {base::OkStatus(), true};
+      }
+      if (value.has_double_value()) {
+        delegate.AddDouble(context_name, value.double_value());
+        return {base::OkStatus(), true};
+      }
+      if (value.has_string_value()) {
+        delegate.AddString(context_name, value.string_value());
+        return {base::OkStatus(), true};
+      }
+      return {base::OkStatus(), false};
+    }
+    case protos::pbzero::DebugAnnotation::NestedValue::DICT: {
+      bool added_entry = false;
+      auto key_it = value.dict_keys();
+      auto value_it = value.dict_values();
+      for (; key_it && value_it; ++key_it, ++value_it) {
+        std::string child_name =
+            SanitizeDebugAnnotationName((*key_it).ToStdString());
+        auto nested_key = proto_to_args_parser_.EnterDictionary(child_name);
+        ParseResult result =
+            ParseNestedValueArgs(*value_it, nested_key.key(), delegate);
+        added_entry |= result.added_entry;
+        if (!result.status.ok())
+          return {result.status, added_entry};
+      }
+      return {base::OkStatus(), true};
+    }
+
+    case protos::pbzero::DebugAnnotation::NestedValue::ARRAY: {
+      std::string array_key = context_name.key;
+      size_t array_index = delegate.GetArrayEntryIndex(context_name.key);
+      bool added_entry = false;
+
+      for (auto value_it = value.array_values(); value_it; ++value_it) {
+        auto nested_key = proto_to_args_parser_.EnterArray(array_index);
+        ParseResult result =
+            ParseNestedValueArgs(*value_it, nested_key.key(), delegate);
+
+        if (result.added_entry) {
+          ++array_index;
+          delegate.IncrementArrayEntryIndex(array_key);
+          added_entry = true;
+        }
+        if (!result.status.ok())
+          return {result.status, added_entry};
+      }
+      return {base::OkStatus(), added_entry};
+    }
+  }
+  return {base::OkStatus(), false};
+}
+
+}  // namespace util
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/util/debug_annotation_parser.h b/src/trace_processor/util/debug_annotation_parser.h
new file mode 100644
index 0000000..0e68eec
--- /dev/null
+++ b/src/trace_processor/util/debug_annotation_parser.h
@@ -0,0 +1,66 @@
+/*
+ * 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_TRACE_PROCESSOR_UTIL_DEBUG_ANNOTATION_PARSER_H_
+#define SRC_TRACE_PROCESSOR_UTIL_DEBUG_ANNOTATION_PARSER_H_
+
+#include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
+#include "src/trace_processor/util/proto_to_args_parser.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace util {
+
+// |DebugAnnotationParser| is responsible for parsing DebugAnnotation protos
+// and turning it into key-value arg pairs.
+// |DebugAnnotationParser| is a logical extension of |ProtoToArgsParser|:
+// it uses |ProtoToArgsParser::Delegate| for writing the results and uses
+// |ProtoToArgsParser| to parse arbitrary protos nested inside DebugAnnotation.
+class DebugAnnotationParser {
+ public:
+  explicit DebugAnnotationParser(ProtoToArgsParser& proto_to_args_parser);
+
+  base::Status Parse(protozero::ConstBytes data,
+                     ProtoToArgsParser::Delegate& delegate);
+
+ private:
+  struct ParseResult {
+    base::Status status;
+    // True if parsing of the annotation added at least one entry to the
+    // |delegate|.
+    bool added_entry;
+  };
+
+  base::Status ParseDebugAnnotationName(
+      protos::pbzero::DebugAnnotation::Decoder& annotation,
+      ProtoToArgsParser::Delegate& delegate,
+      std::string& result);
+  ParseResult ParseDebugAnnotationValue(
+      protos::pbzero::DebugAnnotation::Decoder& annotation,
+      ProtoToArgsParser::Delegate& delegate,
+      const ProtoToArgsParser::Key& context_name);
+  ParseResult ParseNestedValueArgs(protozero::ConstBytes nested_value,
+                                   const ProtoToArgsParser::Key& context_name,
+                                   ProtoToArgsParser::Delegate& delegate);
+
+  ProtoToArgsParser& proto_to_args_parser_;
+};
+
+}  // namespace util
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_UTIL_DEBUG_ANNOTATION_PARSER_H_
diff --git a/src/trace_processor/util/debug_annotation_parser_unittest.cc b/src/trace_processor/util/debug_annotation_parser_unittest.cc
new file mode 100644
index 0000000..e3f3380
--- /dev/null
+++ b/src/trace_processor/util/debug_annotation_parser_unittest.cc
@@ -0,0 +1,237 @@
+/*
+ * 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/trace_processor/util/debug_annotation_parser.h"
+
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "protos/perfetto/common/descriptor.pbzero.h"
+#include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
+#include "protos/perfetto/trace/track_event/source_location.pbzero.h"
+#include "src/protozero/test/example_proto/test_messages.pbzero.h"
+#include "src/trace_processor/test_messages.descriptor.h"
+#include "src/trace_processor/util/interned_message_view.h"
+#include "src/trace_processor/util/proto_to_args_parser.h"
+#include "src/trace_processor/util/trace_blob_view.h"
+#include "test/gtest_and_gmock.h"
+
+#include <sstream>
+
+namespace perfetto {
+namespace trace_processor {
+namespace util {
+namespace {
+
+base::Status ParseDebugAnnotation(
+    DebugAnnotationParser& parser,
+    protozero::HeapBuffered<protos::pbzero::DebugAnnotation>& msg,
+    ProtoToArgsParser::Delegate& delegate) {
+  std::vector<uint8_t> data = msg.SerializeAsArray();
+  return parser.Parse(protozero::ConstBytes{data.data(), data.size()},
+                      delegate);
+}
+
+class DebugAnnotationParserTest : public ::testing::Test,
+                                  public ProtoToArgsParser::Delegate {
+ protected:
+  DebugAnnotationParserTest() {}
+
+  const std::vector<std::string>& args() const { return args_; }
+
+ private:
+  using Key = ProtoToArgsParser::Key;
+
+  void AddInteger(const Key& key, int64_t value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << value;
+    args_.push_back(ss.str());
+  }
+
+  void AddUnsignedInteger(const Key& key, uint64_t value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << value;
+    args_.push_back(ss.str());
+  }
+
+  void AddString(const Key& key, const protozero::ConstChars& value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << value.ToStdString();
+    args_.push_back(ss.str());
+  }
+
+  void AddDouble(const Key& key, double value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << value;
+    args_.push_back(ss.str());
+  }
+
+  void AddPointer(const Key& key, const void* value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << std::hex
+       << reinterpret_cast<uintptr_t>(value) << std::dec;
+    args_.push_back(ss.str());
+  }
+
+  void AddBoolean(const Key& key, bool value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << (value ? "true" : "false");
+    args_.push_back(ss.str());
+  }
+
+  bool AddJson(const Key& key, const protozero::ConstChars& value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << std::hex
+       << value.ToStdString() << std::dec;
+    args_.push_back(ss.str());
+    return true;
+  }
+
+  size_t GetArrayEntryIndex(const std::string& array_key) final {
+    return array_indices_[array_key];
+  }
+
+  size_t IncrementArrayEntryIndex(const std::string& array_key) final {
+    return ++array_indices_[array_key];
+  }
+
+  InternedMessageView* GetInternedMessageView(uint32_t, uint64_t) override {
+    return nullptr;
+  }
+
+  std::vector<std::string> args_;
+  std::map<std::string, size_t> array_indices_;
+};
+
+// This test checks that in when an array is nested inside a dict which is
+// nested inside an array which is nested inside a dict, flat keys and non-flat
+// keys are parsed correctly.
+TEST_F(DebugAnnotationParserTest, DeeplyNestedDictsAndArrays) {
+  protozero::HeapBuffered<protos::pbzero::DebugAnnotation> msg;
+
+  msg->set_name("root");
+  auto* dict1 = msg->add_dict_entries();
+  dict1->set_name("k1");
+  auto* array1 = dict1->add_array_values();
+  auto* dict2 = array1->add_dict_entries();
+  dict2->set_name("k2");
+  auto* array2 = dict2->add_array_values();
+  array2->set_int_value(42);
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  EXPECT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+
+  ProtoToArgsParser args_parser(pool);
+  DebugAnnotationParser parser(args_parser);
+
+  status = ParseDebugAnnotation(parser, msg, *this);
+  EXPECT_TRUE(status.ok()) << "DebugAnnotationParser::Parse failed with error: "
+                           << status.message();
+
+  EXPECT_THAT(args(), testing::ElementsAre("root.k1.k2 root.k1[0].k2[0] 42"));
+}
+
+// This test checks that array indexes are correctly merged across messages.
+TEST_F(DebugAnnotationParserTest, MergeArrays) {
+  protozero::HeapBuffered<protos::pbzero::DebugAnnotation> msg1;
+  msg1->set_name("root");
+  auto* item1 = msg1->add_array_values();
+  item1->set_int_value(1);
+
+  protozero::HeapBuffered<protos::pbzero::DebugAnnotation> msg2;
+  msg2->set_name("root");
+  auto* item2 = msg1->add_array_values();
+  item2->set_int_value(2);
+
+  DescriptorPool pool;
+  ProtoToArgsParser args_parser(pool);
+  DebugAnnotationParser parser(args_parser);
+
+  base::Status status = ParseDebugAnnotation(parser, msg1, *this);
+  EXPECT_TRUE(status.ok()) << "DebugAnnotationParser::Parse failed with error: "
+                           << status.message();
+
+  status = ParseDebugAnnotation(parser, msg2, *this);
+  EXPECT_TRUE(status.ok()) << "DebugAnnotationParser::Parse failed with error: "
+                           << status.message();
+
+  EXPECT_THAT(args(), testing::ElementsAre("root root[0] 1", "root root[1] 2"));
+}
+
+// This test checks that nested empty dictionaries / arrays do not cause array
+// index to be incremented.
+TEST_F(DebugAnnotationParserTest, EmptyArrayIndexIsSkipped) {
+  protozero::HeapBuffered<protos::pbzero::DebugAnnotation> msg;
+  msg->set_name("root");
+
+  msg->add_array_values()->set_int_value(1);
+
+  // Empty item.
+  msg->add_array_values();
+
+  msg->add_array_values()->set_int_value(3);
+
+  // Empty dict.
+  msg->add_array_values()->add_dict_entries()->set_name("key1");
+
+  auto* nested_dict_entry = msg->add_array_values()->add_dict_entries();
+  nested_dict_entry->set_name("key2");
+  nested_dict_entry->set_string_value("value");
+
+  msg->add_array_values()->set_int_value(5);
+
+  DescriptorPool pool;
+  ProtoToArgsParser args_parser(pool);
+  DebugAnnotationParser parser(args_parser);
+
+  base::Status status = ParseDebugAnnotation(parser, msg, *this);
+  EXPECT_TRUE(status.ok()) << "DebugAnnotationParser::Parse failed with error: "
+                           << status.message();
+
+  EXPECT_THAT(args(), testing::ElementsAre("root root[0] 1", "root root[1] 3",
+                                           "root.key2 root[3].key2 value",
+                                           "root root[4] 5"));
+}
+
+TEST_F(DebugAnnotationParserTest, NestedArrays) {
+  protozero::HeapBuffered<protos::pbzero::DebugAnnotation> msg;
+  msg->set_name("root");
+  auto* item1 = msg->add_array_values();
+  item1->add_array_values()->set_int_value(1);
+  item1->add_array_values()->set_int_value(2);
+  auto* item2 = msg->add_array_values();
+  item2->add_array_values()->set_int_value(3);
+  item2->add_array_values()->set_int_value(4);
+
+  DescriptorPool pool;
+  ProtoToArgsParser args_parser(pool);
+  DebugAnnotationParser parser(args_parser);
+
+  base::Status status = ParseDebugAnnotation(parser, msg, *this);
+  EXPECT_TRUE(status.ok()) << "DebugAnnotationParser::Parse failed with error: "
+                           << status.message();
+
+  EXPECT_THAT(args(),
+              testing::ElementsAre("root root[0][0] 1", "root root[0][1] 2",
+                                   "root root[1][0] 3", "root root[1][1] 4"));
+}
+
+}  // namespace
+}  // namespace util
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/util/descriptors.cc b/src/trace_processor/util/descriptors.cc
index d99b0e2..068da6f 100644
--- a/src/trace_processor/util/descriptors.cc
+++ b/src/trace_processor/util/descriptors.cc
@@ -74,7 +74,10 @@
   auto field = CreateFieldFromDecoder(f_decoder, true);
 
   auto extendee_name = base::StringView(f_decoder.extendee()).ToStdString();
-  PERFETTO_CHECK(!extendee_name.empty());
+  if (extendee_name.empty()) {
+    return util::ErrStatus("Extendee name is empty");
+  }
+
   if (extendee_name[0] != '.') {
     // Only prepend if the extendee is not fully qualified
     extendee_name = package_name + "." + extendee_name;
@@ -145,8 +148,8 @@
 
   auto idx = static_cast<uint32_t>(descriptors_.size()) - 1;
   for (auto it = decoder.enum_type(); it; ++it) {
-    AddEnumProtoDescriptors(file_name, package_name, idx, *it,
-                            merge_existing_messages);
+    RETURN_IF_ERROR(AddEnumProtoDescriptors(file_name, package_name, idx, *it,
+                                            merge_existing_messages));
   }
   for (auto it = decoder.nested_type(); it; ++it) {
     RETURN_IF_ERROR(AddNestedProtoDescriptors(file_name, package_name, idx, *it,
@@ -226,8 +229,9 @@
           merge_existing_messages));
     }
     for (auto enum_it = file.enum_type(); enum_it; ++enum_it) {
-      AddEnumProtoDescriptors(file_name, package, base::nullopt, *enum_it,
-                              merge_existing_messages);
+      RETURN_IF_ERROR(AddEnumProtoDescriptors(file_name, package, base::nullopt,
+                                              *enum_it,
+                                              merge_existing_messages));
     }
     for (auto ext_it = file.extension(); ext_it; ++ext_it) {
       extensions.emplace_back(package, *ext_it);
diff --git a/src/trace_processor/util/descriptors.h b/src/trace_processor/util/descriptors.h
index 54a9ab1..a5c7cde 100644
--- a/src/trace_processor/util/descriptors.h
+++ b/src/trace_processor/util/descriptors.h
@@ -23,9 +23,8 @@
 #include <unordered_map>
 #include <vector>
 
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/optional.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "perfetto/trace_processor/status.h"
 #include "protos/perfetto/common/descriptor.pbzero.h"
 
 namespace protozero {
@@ -88,7 +87,9 @@
   void AddEnumValue(int32_t integer_representation,
                     std::string string_representation) {
     PERFETTO_DCHECK(type_ == Type::kEnum);
-    enum_values_[integer_representation] = std::move(string_representation);
+    enum_values_by_name_[string_representation] = integer_representation;
+    enum_names_by_value_[integer_representation] =
+        std::move(string_representation);
   }
 
   const FieldDescriptor* FindFieldByName(const std::string& name) const {
@@ -115,9 +116,16 @@
 
   base::Optional<std::string> FindEnumString(const int32_t value) const {
     PERFETTO_DCHECK(type_ == Type::kEnum);
-    auto it = enum_values_.find(value);
-    return it == enum_values_.end() ? base::nullopt
-                                    : base::Optional<std::string>(it->second);
+    auto it = enum_names_by_value_.find(value);
+    return it == enum_names_by_value_.end() ? base::nullopt
+                                            : base::make_optional(it->second);
+  }
+
+  base::Optional<int32_t> FindEnumValue(const std::string& value) const {
+    PERFETTO_DCHECK(type_ == Type::kEnum);
+    auto it = enum_values_by_name_.find(value);
+    return it == enum_values_by_name_.end() ? base::nullopt
+                                            : base::make_optional(it->second);
   }
 
   const std::string& file_name() const { return file_name_; }
@@ -142,14 +150,15 @@
   const Type type_;
   base::Optional<uint32_t> parent_id_;
   std::unordered_map<uint32_t, FieldDescriptor> fields_;
-  std::unordered_map<int32_t, std::string> enum_values_;
+  std::unordered_map<int32_t, std::string> enum_names_by_value_;
+  std::unordered_map<std::string, int32_t> enum_values_by_name_;
 };
 
 using ExtensionInfo = std::pair<std::string, protozero::ConstBytes>;
 
 class DescriptorPool {
  public:
-  util::Status AddFromFileDescriptorSet(
+  base::Status AddFromFileDescriptorSet(
       const uint8_t* file_descriptor_set_proto,
       size_t size,
       bool merge_existing_messages = false);
@@ -157,26 +166,30 @@
   base::Optional<uint32_t> FindDescriptorIdx(
       const std::string& full_name) const;
 
+  std::vector<uint8_t> SerializeAsDescriptorSet();
+
+  void AddProtoDescriptorForTesting(ProtoDescriptor descriptor) {
+    descriptors_.emplace_back(std::move(descriptor));
+  }
+
   const std::vector<ProtoDescriptor>& descriptors() const {
     return descriptors_;
   }
 
-  std::vector<uint8_t> SerializeAsDescriptorSet();
-
  private:
-  util::Status AddNestedProtoDescriptors(const std::string& file_name,
+  base::Status AddNestedProtoDescriptors(const std::string& file_name,
                                          const std::string& package_name,
                                          base::Optional<uint32_t> parent_idx,
                                          protozero::ConstBytes descriptor_proto,
                                          std::vector<ExtensionInfo>* extensions,
                                          bool merge_existing_messages);
-  util::Status AddEnumProtoDescriptors(const std::string& file_name,
+  base::Status AddEnumProtoDescriptors(const std::string& file_name,
                                        const std::string& package_name,
                                        base::Optional<uint32_t> parent_idx,
                                        protozero::ConstBytes descriptor_proto,
                                        bool merge_existing_messages);
 
-  util::Status AddExtensionField(const std::string& package_name,
+  base::Status AddExtensionField(const std::string& package_name,
                                  protozero::ConstBytes field_desc_proto);
 
   // Recursively searches for the given short type in all parent messages
diff --git a/src/trace_processor/importers/gzip/gzip_utils.cc b/src/trace_processor/util/gzip_utils.cc
similarity index 96%
rename from src/trace_processor/importers/gzip/gzip_utils.cc
rename to src/trace_processor/util/gzip_utils.cc
index 8a30c7f..79cf1c0 100644
--- a/src/trace_processor/importers/gzip/gzip_utils.cc
+++ b/src/trace_processor/util/gzip_utils.cc
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "src/trace_processor/importers/gzip/gzip_utils.h"
+#include "src/trace_processor/util/gzip_utils.h"
 
 // For bazel build.
 #include "perfetto/base/build_config.h"
@@ -28,7 +28,7 @@
 
 namespace perfetto {
 namespace trace_processor {
-namespace gzip {
+namespace util {
 
 bool IsGzipSupported() {
 #if PERFETTO_BUILDFLAG(PERFETTO_ZLIB)
@@ -38,8 +38,6 @@
 #endif
 }
 
-}  // namespace gzip
-
 #if PERFETTO_BUILDFLAG(PERFETTO_ZLIB)
 GzipDecompressor::GzipDecompressor() : z_stream_(new z_stream()) {
   z_stream_->zalloc = Z_NULL;
@@ -108,5 +106,6 @@
 #endif
 }
 
+}  // namespace util
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/gzip/gzip_utils.h b/src/trace_processor/util/gzip_utils.h
similarity index 88%
rename from src/trace_processor/importers/gzip/gzip_utils.h
rename to src/trace_processor/util/gzip_utils.h
index 624363f..189cfbd 100644
--- a/src/trace_processor/importers/gzip/gzip_utils.h
+++ b/src/trace_processor/util/gzip_utils.h
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_GZIP_GZIP_UTILS_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_GZIP_GZIP_UTILS_H_
+#ifndef SRC_TRACE_PROCESSOR_UTIL_GZIP_UTILS_H_
+#define SRC_TRACE_PROCESSOR_UTIL_GZIP_UTILS_H_
 
 #include <memory>
 
@@ -23,15 +23,12 @@
 
 namespace perfetto {
 namespace trace_processor {
-
-namespace gzip {
+namespace util {
 
 // Returns whether gzip related functioanlity is supported with the current
 // build flags.
 bool IsGzipSupported();
 
-}  // namespace gzip
-
 class GzipDecompressor {
  public:
   enum class ResultCode {
@@ -66,7 +63,8 @@
   std::unique_ptr<z_stream_s> z_stream_;
 };
 
+}  // namespace util
 }  // namespace trace_processor
 }  // namespace perfetto
 
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_GZIP_GZIP_UTILS_H_
+#endif  // SRC_TRACE_PROCESSOR_UTIL_GZIP_UTILS_H_
diff --git a/src/trace_processor/util/interned_message_view.h b/src/trace_processor/util/interned_message_view.h
new file mode 100644
index 0000000..7341aea
--- /dev/null
+++ b/src/trace_processor/util/interned_message_view.h
@@ -0,0 +1,138 @@
+/*
+ * 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_TRACE_PROCESSOR_UTIL_INTERNED_MESSAGE_VIEW_H_
+#define SRC_TRACE_PROCESSOR_UTIL_INTERNED_MESSAGE_VIEW_H_
+
+#include "src/trace_processor/util/trace_blob_view.h"
+
+#include <unordered_map>
+
+namespace perfetto {
+namespace trace_processor {
+
+#if PERFETTO_DCHECK_IS_ON()
+// When called from GetOrCreateDecoder(), should include the stringified name of
+// the MessageType.
+#define PERFETTO_TYPE_IDENTIFIER PERFETTO_DEBUG_FUNCTION_IDENTIFIER()
+#else  // PERFETTO_DCHECK_IS_ON()
+#define PERFETTO_TYPE_IDENTIFIER nullptr
+#endif  // PERFETTO_DCHECK_IS_ON()
+
+// Entry in an interning index, refers to the interned message.
+class InternedMessageView {
+ public:
+  InternedMessageView(TraceBlobView msg) : message_(std::move(msg)) {}
+
+  InternedMessageView(InternedMessageView&&) = default;
+  InternedMessageView& operator=(InternedMessageView&&) = default;
+
+  // Allow copy by cloning the TraceBlobView. This is required for
+  // UpdateTracePacketDefaults().
+  InternedMessageView(const InternedMessageView& view)
+      : message_(view.message_.slice(0, view.message_.length())) {}
+  InternedMessageView& operator=(const InternedMessageView& view) {
+    this->message_ = view.message_.slice(0, view.message_.length());
+    this->decoder_ = nullptr;
+    this->decoder_type_ = nullptr;
+    this->submessages_.clear();
+    return *this;
+  }
+
+  // Lazily initializes and returns the decoder object for the message. The
+  // decoder is stored in the InternedMessageView to avoid having to parse the
+  // message multiple times.
+  template <typename MessageType>
+  typename MessageType::Decoder* GetOrCreateDecoder() {
+    if (!decoder_) {
+      // Lazy init the decoder and save it away, so that we don't have to
+      // reparse the message every time we access the interning entry.
+      decoder_ = std::unique_ptr<void, std::function<void(void*)>>(
+          new typename MessageType::Decoder(message_.data(), message_.length()),
+          [](void* obj) {
+            delete reinterpret_cast<typename MessageType::Decoder*>(obj);
+          });
+      decoder_type_ = PERFETTO_TYPE_IDENTIFIER;
+    }
+    // Verify that the type of the decoder didn't change.
+    if (PERFETTO_TYPE_IDENTIFIER &&
+        strcmp(decoder_type_,
+               // GCC complains if this arg can be null.
+               PERFETTO_TYPE_IDENTIFIER ? PERFETTO_TYPE_IDENTIFIER : "") != 0) {
+      PERFETTO_FATAL(
+          "Interning entry accessed under different types! previous type: "
+          "%s. new type: %s.",
+          decoder_type_, PERFETTO_DEBUG_FUNCTION_IDENTIFIER());
+    }
+    return reinterpret_cast<typename MessageType::Decoder*>(decoder_.get());
+  }
+
+  // Lookup a submessage of the interned message, which is then itself stored
+  // as InternedMessageView, so that we only need to parse it once. Returns
+  // nullptr if the field isn't set.
+  // 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* decoder = GetOrCreateDecoder<MessageType>();
+    // Calls the at() template method on the decoder.
+    auto field = decoder->template at<FieldId>().as_bytes();
+    if (!field.data)
+      return nullptr;
+    const size_t offset = message_.offset_of(field.data);
+    TraceBlobView submessage = message_.slice(offset, field.size);
+    InternedMessageView* submessage_view =
+        new InternedMessageView(std::move(submessage));
+    submessages_.emplace_hint(
+        it, FieldId, std::unique_ptr<InternedMessageView>(submessage_view));
+    return submessage_view;
+  }
+
+  const TraceBlobView& message() { return message_; }
+
+ private:
+  using SubMessageViewMap =
+      std::unordered_map<uint32_t /*field_id*/,
+                         std::unique_ptr<InternedMessageView>>;
+
+  TraceBlobView message_;
+
+  // Stores the decoder for the message_, so that the message does not have to
+  // be re-decoded every time the interned message is looked up. Lazily
+  // initialized in GetOrCreateDecoder(). Since we don't know the type of the
+  // decoder until GetOrCreateDecoder() is called, we store the decoder as a
+  // void* unique_pointer with a destructor function that's supplied in
+  // GetOrCreateDecoder() when the decoder is created.
+  std::unique_ptr<void, std::function<void(void*)>> decoder_;
+
+  // Type identifier for the decoder. Only valid in debug builds and on
+  // supported platforms. Used to verify that GetOrCreateDecoder() is always
+  // called with the same template argument.
+  const char* decoder_type_ = nullptr;
+
+  // Views of submessages of the interned message. Submessages are lazily
+  // added by GetOrCreateSubmessageView(). By storing submessages and their
+  // decoders, we avoid having to decode submessages multiple times if they
+  // looked up often.
+  SubMessageViewMap submessages_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_UTIL_INTERNED_MESSAGE_VIEW_H_
diff --git a/src/trace_processor/util/proto_to_args_parser.cc b/src/trace_processor/util/proto_to_args_parser.cc
new file mode 100644
index 0000000..96e066d
--- /dev/null
+++ b/src/trace_processor/util/proto_to_args_parser.cc
@@ -0,0 +1,295 @@
+/*
+ * 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/trace_processor/util/proto_to_args_parser.h"
+
+#include "protos/perfetto/common/descriptor.pbzero.h"
+#include "src/trace_processor/util/descriptors.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace util {
+
+namespace {
+
+void AppendProtoType(std::string& target, const std::string& value) {
+  if (!target.empty())
+    target += '.';
+  target += value;
+}
+
+}  // namespace
+
+ProtoToArgsParser::Key::Key() = default;
+ProtoToArgsParser::Key::Key(const std::string& k) : flat_key(k), key(k) {}
+ProtoToArgsParser::Key::Key(const std::string& fk, const std::string& k)
+    : flat_key(fk), key(k) {}
+ProtoToArgsParser::Key::~Key() = default;
+
+ProtoToArgsParser::ScopedNestedKeyContext::ScopedNestedKeyContext(Key& key)
+    : key_(key),
+      old_flat_key_length_(key.flat_key.length()),
+      old_key_length_(key.key.length()) {}
+
+ProtoToArgsParser::ScopedNestedKeyContext::ScopedNestedKeyContext(
+    ProtoToArgsParser::ScopedNestedKeyContext&& other)
+    : key_(other.key_),
+      old_flat_key_length_(other.old_flat_key_length_),
+      old_key_length_(other.old_key_length_) {
+  other.old_flat_key_length_ = base::nullopt;
+  other.old_key_length_ = base::nullopt;
+}
+
+ProtoToArgsParser::ScopedNestedKeyContext::~ScopedNestedKeyContext() {
+  RemoveFieldSuffix();
+}
+
+void ProtoToArgsParser::ScopedNestedKeyContext::RemoveFieldSuffix() {
+  if (old_flat_key_length_)
+    key_.flat_key.resize(old_flat_key_length_.value());
+  if (old_key_length_)
+    key_.key.resize(old_key_length_.value());
+  old_flat_key_length_ = base::nullopt;
+  old_key_length_ = base::nullopt;
+}
+
+ProtoToArgsParser::Delegate::~Delegate() = default;
+
+ProtoToArgsParser::ProtoToArgsParser(const DescriptorPool& pool) : pool_(pool) {
+  constexpr int kDefaultSize = 64;
+  key_prefix_.key.reserve(kDefaultSize);
+  key_prefix_.flat_key.reserve(kDefaultSize);
+}
+
+base::Status ProtoToArgsParser::ParseMessage(
+    const protozero::ConstBytes& cb,
+    const std::string& type,
+    const std::vector<uint16_t>* allowed_fields,
+    Delegate& delegate) {
+  ScopedNestedKeyContext key_context(key_prefix_);
+  return ParseMessageInternal(key_context, cb, type, allowed_fields, delegate);
+}
+
+base::Status ProtoToArgsParser::ParseMessageInternal(
+    ScopedNestedKeyContext& key_context,
+    const protozero::ConstBytes& cb,
+    const std::string& type,
+    const std::vector<uint16_t>* allowed_fields,
+    Delegate& delegate) {
+  if (auto override_result =
+          MaybeApplyOverrideForType(type, key_context, cb, delegate)) {
+    return override_result.value();
+  }
+
+  auto idx = pool_.FindDescriptorIdx(type);
+  if (!idx) {
+    return base::Status("Failed to find proto descriptor");
+  }
+
+  auto& descriptor = pool_.descriptors()[*idx];
+
+  std::unordered_map<size_t, int> repeated_field_index;
+
+  protozero::ProtoDecoder decoder(cb);
+  for (protozero::Field f = decoder.ReadField(); f.valid();
+       f = decoder.ReadField()) {
+    auto field = descriptor.FindFieldByTag(f.id());
+    if (!field) {
+      // Unknown field, possibly an unknown extension.
+      continue;
+    }
+
+    // If allowlist is not provided, reflect all fields. Otherwise, check if the
+    // current field either an extension or is in allowlist.
+    bool is_allowed = field->is_extension() || !allowed_fields ||
+                      std::find(allowed_fields->begin(), allowed_fields->end(),
+                                f.id()) != allowed_fields->end();
+
+    if (!is_allowed) {
+      // Field is neither an extension, nor is allowed to be
+      // reflected.
+      continue;
+    }
+    RETURN_IF_ERROR(
+        ParseField(*field, repeated_field_index[f.id()], f, delegate));
+    if (field->is_repeated()) {
+      repeated_field_index[f.id()]++;
+    }
+  }
+
+  return base::OkStatus();
+}
+
+base::Status ProtoToArgsParser::ParseField(
+    const FieldDescriptor& field_descriptor,
+    int repeated_field_number,
+    protozero::Field field,
+    Delegate& delegate) {
+  std::string prefix_part = field_descriptor.name();
+  if (field_descriptor.is_repeated()) {
+    std::string number = std::to_string(repeated_field_number);
+    prefix_part.reserve(prefix_part.length() + number.length() + 2);
+    prefix_part.append("[");
+    prefix_part.append(number);
+    prefix_part.append("]");
+  }
+
+  // In the args table we build up message1.message2.field1 as the column
+  // name. This will append the ".field1" suffix to |key_prefix| and then
+  // remove it when it goes out of scope.
+  ScopedNestedKeyContext key_context(key_prefix_);
+  AppendProtoType(key_prefix_.flat_key, field_descriptor.name());
+  AppendProtoType(key_prefix_.key, prefix_part);
+
+  // If we have an override parser then use that instead and move onto the
+  // next loop.
+  if (base::Optional<base::Status> status =
+          MaybeApplyOverrideForField(field, delegate)) {
+    return *status;
+  }
+
+  // If this is not a message we can just immediately add the column name and
+  // get the value out of |field|. However if it is a message we need to
+  // recurse into it.
+  if (field_descriptor.type() ==
+      protos::pbzero::FieldDescriptorProto::TYPE_MESSAGE) {
+    return ParseMessageInternal(key_context, field.as_bytes(),
+                                field_descriptor.resolved_type_name(), nullptr,
+                                delegate);
+  }
+
+  return ParseSimpleField(field_descriptor, field, delegate);
+}
+
+void ProtoToArgsParser::AddParsingOverrideForField(
+    const std::string& field,
+    ParsingOverrideForField func) {
+  field_overrides_[field] = std::move(func);
+}
+
+void ProtoToArgsParser::AddParsingOverrideForType(const std::string& type,
+                                                  ParsingOverrideForType func) {
+  type_overrides_[type] = std::move(func);
+}
+
+base::Optional<base::Status> ProtoToArgsParser::MaybeApplyOverrideForField(
+    const protozero::Field& field,
+    Delegate& delegate) {
+  auto it = field_overrides_.find(key_prefix_.flat_key);
+  if (it == field_overrides_.end())
+    return base::nullopt;
+  return it->second(field, delegate);
+}
+
+base::Optional<base::Status> ProtoToArgsParser::MaybeApplyOverrideForType(
+    const std::string& message_type,
+    ScopedNestedKeyContext& key,
+    const protozero::ConstBytes& data,
+    Delegate& delegate) {
+  auto it = type_overrides_.find(message_type);
+  if (it == type_overrides_.end())
+    return base::nullopt;
+  return it->second(key, data, delegate);
+}
+
+base::Status ProtoToArgsParser::ParseSimpleField(
+    const FieldDescriptor& descriptor,
+    const protozero::Field& field,
+    Delegate& delegate) {
+  using FieldDescriptorProto = protos::pbzero::FieldDescriptorProto;
+  switch (descriptor.type()) {
+    case FieldDescriptorProto::TYPE_INT32:
+    case FieldDescriptorProto::TYPE_SFIXED32:
+    case FieldDescriptorProto::TYPE_FIXED32:
+      delegate.AddInteger(key_prefix_, field.as_int32());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_SINT32:
+      delegate.AddInteger(key_prefix_, field.as_sint32());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_INT64:
+    case FieldDescriptorProto::TYPE_SFIXED64:
+    case FieldDescriptorProto::TYPE_FIXED64:
+      delegate.AddInteger(key_prefix_, field.as_int64());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_SINT64:
+      delegate.AddInteger(key_prefix_, field.as_sint64());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_UINT32:
+      delegate.AddUnsignedInteger(key_prefix_, field.as_uint32());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_UINT64:
+      delegate.AddUnsignedInteger(key_prefix_, field.as_uint64());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_BOOL:
+      delegate.AddBoolean(key_prefix_, field.as_bool());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_DOUBLE:
+      delegate.AddDouble(key_prefix_, field.as_double());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_FLOAT:
+      delegate.AddDouble(key_prefix_, static_cast<double>(field.as_float()));
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_STRING:
+      delegate.AddString(key_prefix_, field.as_string());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_ENUM: {
+      auto opt_enum_descriptor_idx =
+          pool_.FindDescriptorIdx(descriptor.resolved_type_name());
+      if (!opt_enum_descriptor_idx) {
+        delegate.AddInteger(key_prefix_, field.as_int32());
+        return base::OkStatus();
+      }
+      auto opt_enum_string =
+          pool_.descriptors()[*opt_enum_descriptor_idx].FindEnumString(
+              field.as_int32());
+      if (!opt_enum_string) {
+        // Fall back to the integer representation of the field.
+        delegate.AddInteger(key_prefix_, field.as_int32());
+        return base::OkStatus();
+      }
+      delegate.AddString(key_prefix_,
+                         protozero::ConstChars{opt_enum_string->data(),
+                                               opt_enum_string->size()});
+      return base::OkStatus();
+    }
+    default:
+      return base::ErrStatus(
+          "Tried to write value of type field %s (in proto type "
+          "%s) which has type enum %d",
+          descriptor.name().c_str(), descriptor.resolved_type_name().c_str(),
+          descriptor.type());
+  }
+}
+
+ProtoToArgsParser::ScopedNestedKeyContext ProtoToArgsParser::EnterArray(
+    size_t index) {
+  auto context = ScopedNestedKeyContext(key_prefix_);
+  key_prefix_.key += "[" + std::to_string(index) + "]";
+  return context;
+}
+
+ProtoToArgsParser::ScopedNestedKeyContext ProtoToArgsParser::EnterDictionary(
+    const std::string& name) {
+  auto context = ScopedNestedKeyContext(key_prefix_);
+  AppendProtoType(key_prefix_.key, name);
+  AppendProtoType(key_prefix_.flat_key, name);
+  return context;
+}
+
+}  // namespace util
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/util/proto_to_args_parser.h b/src/trace_processor/util/proto_to_args_parser.h
new file mode 100644
index 0000000..629ab1f
--- /dev/null
+++ b/src/trace_processor/util/proto_to_args_parser.h
@@ -0,0 +1,273 @@
+/*
+ * 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_TRACE_PROCESSOR_UTIL_PROTO_TO_ARGS_PARSER_H_
+#define SRC_TRACE_PROCESSOR_UTIL_PROTO_TO_ARGS_PARSER_H_
+
+#include "perfetto/base/status.h"
+#include "perfetto/protozero/field.h"
+#include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
+#include "src/trace_processor/util/descriptors.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+// TODO(altimin): Move InternedMessageView into trace_processor/util.
+class InternedMessageView;
+
+namespace util {
+
+// ProtoToArgsParser encapsulates the process of taking an arbitrary proto and
+// parsing it into key-value arg pairs. This is done by traversing
+// the proto using reflection (with descriptors from |descriptor_pool|)
+// and passing the parsed data to |Delegate| callbacks.
+//
+// E.g. given a proto like
+//
+// package perfetto.protos;
+// message SubMessage {
+//   optional int32 field = 1;
+// }
+// message MainMessage {
+//   optional int32 field1 = 1;
+//   optional string field2 = 2;
+//   optional SubMessage field3 = 3;
+// }
+//
+// We will get the args set columns "field1", "field2", "field3.field" and will
+// store the values found inside as the result.
+//
+// Usage of this is as follows:
+//
+// DescriptorPool pool;
+// ProtoToArgsParser parser(&pool);
+// pool.AddProtoFileDescriptor(
+//     /* provide descriptor generated by tools/gen_binary_descriptors */);
+// parser.ParseMessage(const_bytes, ".perfetto.protos.MainMessage",
+//     /* fields */, /* delegate */);
+class ProtoToArgsParser {
+ public:
+  explicit ProtoToArgsParser(const DescriptorPool& descriptor_pool);
+
+  struct Key {
+    Key(const std::string& flat_key, const std::string& key);
+    Key(const std::string& key);
+    Key();
+    ~Key();
+
+    std::string flat_key;
+    std::string key;
+  };
+
+  class Delegate {
+   public:
+    virtual ~Delegate();
+
+    virtual void AddInteger(const Key& key, int64_t value) = 0;
+    virtual void AddUnsignedInteger(const Key& key, uint64_t value) = 0;
+    virtual void AddString(const Key& key,
+                           const protozero::ConstChars& value) = 0;
+    virtual void AddDouble(const Key& key, double value) = 0;
+    virtual void AddPointer(const Key& key, const void* value) = 0;
+    virtual void AddBoolean(const Key& key, bool value) = 0;
+    // Returns whether an entry was added or not.
+    virtual bool AddJson(const Key& key,
+                         const protozero::ConstChars& value) = 0;
+
+    virtual size_t GetArrayEntryIndex(const std::string& array_key) = 0;
+    virtual size_t IncrementArrayEntryIndex(const std::string& array_key) = 0;
+
+    template <typename FieldMetadata>
+    typename FieldMetadata::cpp_field_type::Decoder* GetInternedMessage(
+        protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
+        uint64_t iid) {
+      static_assert(std::is_base_of<protozero::proto_utils::FieldMetadataBase,
+                                    FieldMetadata>::value,
+                    "Field metadata should be a subclass of FieldMetadataBase");
+      static_assert(std::is_same<typename FieldMetadata::message_type,
+                                 protos::pbzero::InternedData>::value,
+                    "Field should belong to InternedData proto");
+      auto* interned_message_view =
+          GetInternedMessageView(FieldMetadata::kFieldId, iid);
+      if (!interned_message_view)
+        return nullptr;
+      return interned_message_view->template GetOrCreateDecoder<
+          typename FieldMetadata::cpp_field_type>();
+    }
+
+   protected:
+    virtual InternedMessageView* GetInternedMessageView(uint32_t field_id,
+                                                        uint64_t iid) = 0;
+  };
+
+  // Given a view of bytes that represent a serialized protozero message of
+  // |type| we will parse each field.
+  //
+  // Returns on any error with a status describing the problem. However any
+  // added values before encountering the error will be parsed and forwarded to
+  // the delegate.
+  //
+  // Fields with ids given in |fields| are parsed using reflection, as well
+  // as known (previously registered) extension fields. If |allowed_fields| is a
+  // nullptr, all fields are going to be parsed.
+  //
+  // Note:
+  // |type| must be the fully qualified name, but with a '.' added to the
+  // beginning. I.E. ".perfetto.protos.TrackEvent". And must match one of the
+  // descriptors already added through |AddProtoFileDescriptor|.
+  //
+  // IMPORTANT: currently bytes fields are not supported.
+  //
+  // TODO(b/145578432): Add support for byte fields.
+  base::Status ParseMessage(const protozero::ConstBytes& cb,
+                            const std::string& type,
+                            const std::vector<uint16_t>* allowed_fields,
+                            Delegate& delegate);
+
+  // This class is responsible for resetting the current key prefix to the old
+  // value when deleted or reset.
+  struct ScopedNestedKeyContext {
+   public:
+    ~ScopedNestedKeyContext();
+    ScopedNestedKeyContext(ScopedNestedKeyContext&&);
+    ScopedNestedKeyContext(const ScopedNestedKeyContext&) = delete;
+    ScopedNestedKeyContext& operator=(const ScopedNestedKeyContext&) = delete;
+
+    const Key& key() const { return key_; }
+
+    // Clear this context, which strips the latest suffix from |key_| and sets
+    // it to the state before the nested context was created.
+    void RemoveFieldSuffix();
+
+   private:
+    friend class ProtoToArgsParser;
+
+    ScopedNestedKeyContext(Key& old_value);
+
+    struct ScopedStringAppender;
+
+    Key& key_;
+    base::Optional<size_t> old_flat_key_length_ = base::nullopt;
+    base::Optional<size_t> old_key_length_ = base::nullopt;
+  };
+
+  // These methods can be called from parsing overrides to enter nested
+  // contexts. The contexts are left when the returned scope is destroyed or
+  // RemoveFieldSuffix() is called.
+  ScopedNestedKeyContext EnterDictionary(const std::string& key);
+  ScopedNestedKeyContext EnterArray(size_t index);
+
+  using ParsingOverrideForField =
+      std::function<base::Optional<base::Status>(const protozero::Field&,
+                                                 Delegate& delegate)>;
+
+  // Installs an override for the field at the specified path. We will invoke
+  // |parsing_override| when the field is encountered.
+  //
+  // The return value of |parsing_override| indicates whether the override
+  // parsed the sub-message and ProtoToArgsParser should skip it (base::nullopt)
+  // or the sub-message should continue to be parsed by ProtoToArgsParser using
+  // the descriptor (base::Status).
+  //
+  // Note |field_path| must be the full path separated by periods. I.E. in the
+  // proto
+  //
+  // message SubMessage {
+  //   optional int32 field = 1;
+  // }
+  // message MainMessage {
+  //   optional SubMessage field1 = 1;
+  //   optional SubMessage field2 = 2;
+  // }
+  //
+  // To override the handling of both SubMessage fields you must add two parsing
+  // overrides. One with a |field_path| == "field1.field" and another with
+  // "field2.field".
+  void AddParsingOverrideForField(const std::string& field_path,
+                                  ParsingOverrideForField parsing_override);
+
+  using ParsingOverrideForType = std::function<base::Optional<base::Status>(
+      ScopedNestedKeyContext& key,
+      const protozero::ConstBytes& data,
+      Delegate& delegate)>;
+
+  // Installs an override for all fields with the given type. We will invoke
+  // |parsing_override| when a field with the given message type is encountered.
+  // Note that the path-based overrides take precedence over type overrides.
+  //
+  // The return value of |parsing_override| indicates whether the override
+  // parsed the sub-message and ProtoToArgsParser should skip it (base::nullopt)
+  // or the sub-message should continue to be parsed by ProtoToArgsParser using
+  // the descriptor (base::Status).
+  //
+  //
+  // For example, given the following protos and a type override for SubMessage,
+  // all three fields will be parsed using this override.
+  //
+  // message SubMessage {
+  //   optional int32 value = 1;
+  // }
+  //
+  // message MainMessage1 {
+  //   optional SubMessage field1 = 1;
+  //   optional SubMessage field2 = 2;
+  // }
+  //
+  // message MainMessage2 {
+  //   optional SubMessage field3 = 1;
+  // }
+  void AddParsingOverrideForType(const std::string& message_type,
+                                 ParsingOverrideForType parsing_override);
+
+ private:
+  base::Status ParseField(const FieldDescriptor& field_descriptor,
+                          int repeated_field_number,
+                          protozero::Field field,
+                          Delegate& delegate);
+
+  base::Optional<base::Status> MaybeApplyOverrideForField(
+      const protozero::Field&,
+      Delegate& delegate);
+
+  base::Optional<base::Status> MaybeApplyOverrideForType(
+      const std::string& message_type,
+      ScopedNestedKeyContext& key,
+      const protozero::ConstBytes& data,
+      Delegate& delegate);
+
+  // A type override can call |key.RemoveFieldSuffix()| if it wants to exclude
+  // the overriden field's name from the parsed args' keys.
+  base::Status ParseMessageInternal(ScopedNestedKeyContext& key,
+                                    const protozero::ConstBytes& cb,
+                                    const std::string& type,
+                                    const std::vector<uint16_t>* fields,
+                                    Delegate& delegate);
+
+  base::Status ParseSimpleField(const FieldDescriptor& desciptor,
+                                const protozero::Field& field,
+                                Delegate& delegate);
+
+  std::unordered_map<std::string, ParsingOverrideForField> field_overrides_;
+  std::unordered_map<std::string, ParsingOverrideForType> type_overrides_;
+  const DescriptorPool& pool_;
+  Key key_prefix_;
+};
+
+}  // namespace util
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_UTIL_PROTO_TO_ARGS_PARSER_H_
diff --git a/src/trace_processor/util/proto_to_args_parser_unittest.cc b/src/trace_processor/util/proto_to_args_parser_unittest.cc
new file mode 100644
index 0000000..8171742
--- /dev/null
+++ b/src/trace_processor/util/proto_to_args_parser_unittest.cc
@@ -0,0 +1,445 @@
+/*
+ * 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/trace_processor/util/proto_to_args_parser.h"
+
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "protos/perfetto/common/descriptor.pbzero.h"
+#include "protos/perfetto/trace/track_event/source_location.pbzero.h"
+#include "src/protozero/test/example_proto/test_messages.pbzero.h"
+#include "src/trace_processor/test_messages.descriptor.h"
+#include "src/trace_processor/util/interned_message_view.h"
+#include "src/trace_processor/util/trace_blob_view.h"
+#include "test/gtest_and_gmock.h"
+
+#include <sstream>
+
+namespace perfetto {
+namespace trace_processor {
+namespace util {
+namespace {
+
+constexpr size_t kChunkSize = 42;
+
+protozero::ConstChars ToChars(const char* str) {
+  return protozero::ConstChars{str, strlen(str)};
+}
+
+class ProtoToArgsParserTest : public ::testing::Test,
+                              public ProtoToArgsParser::Delegate {
+ protected:
+  ProtoToArgsParserTest() {}
+
+  const std::vector<std::string>& args() const { return args_; }
+
+  void AddInternedSourceLocation(uint64_t iid, TraceBlobView data) {
+    interned_source_locations_[iid] = std::unique_ptr<InternedMessageView>(
+        new InternedMessageView(std::move(data)));
+  }
+
+ private:
+  using Key = ProtoToArgsParser::Key;
+
+  void AddInteger(const Key& key, int64_t value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << value;
+    args_.push_back(ss.str());
+  }
+
+  void AddUnsignedInteger(const Key& key, uint64_t value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << value;
+    args_.push_back(ss.str());
+  }
+
+  void AddString(const Key& key, const protozero::ConstChars& value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << value.ToStdString();
+    args_.push_back(ss.str());
+  }
+
+  void AddDouble(const Key& key, double value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << value;
+    args_.push_back(ss.str());
+  }
+
+  void AddPointer(const Key& key, const void* value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << std::hex
+       << reinterpret_cast<uintptr_t>(value) << std::dec;
+    args_.push_back(ss.str());
+  }
+
+  void AddBoolean(const Key& key, bool value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << (value ? "true" : "false");
+    args_.push_back(ss.str());
+  }
+
+  bool AddJson(const Key& key, const protozero::ConstChars& value) override {
+    std::stringstream ss;
+    ss << key.flat_key << " " << key.key << " " << std::hex
+       << value.ToStdString() << std::dec;
+    args_.push_back(ss.str());
+    return true;
+  }
+
+  size_t GetArrayEntryIndex(const std::string&) final { return 0; }
+
+  size_t IncrementArrayEntryIndex(const std::string&) final { return 0; }
+
+  InternedMessageView* GetInternedMessageView(uint32_t field_id,
+                                              uint64_t iid) override {
+    if (field_id != protos::pbzero::InternedData::kSourceLocationsFieldNumber)
+      return nullptr;
+    return interned_source_locations_.at(iid).get();
+  }
+
+  std::vector<std::string> args_;
+  std::map<uint64_t, std::unique_ptr<InternedMessageView>>
+      interned_source_locations_;
+};
+
+TEST_F(ProtoToArgsParserTest, EnsureTestMessageProtoParses) {
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  ProtoToArgsParser parser(pool);
+  EXPECT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+}
+
+TEST_F(ProtoToArgsParserTest, BasicSingleLayerProto) {
+  using namespace protozero::test::protos::pbzero;
+  protozero::HeapBuffered<EveryField> msg{kChunkSize, kChunkSize};
+  msg->set_field_int32(-1);
+  msg->set_field_int64(-333123456789ll);
+  msg->set_field_uint32(600);
+  msg->set_field_uint64(333123456789ll);
+  msg->set_field_sint32(-5);
+  msg->set_field_sint64(-9000);
+  msg->set_field_fixed32(12345);
+  msg->set_field_fixed64(444123450000ll);
+  msg->set_field_sfixed32(-69999);
+  msg->set_field_sfixed64(-200);
+  msg->set_field_double(0.5555);
+  msg->set_field_bool(true);
+  msg->set_small_enum(SmallEnum::TO_BE);
+  msg->set_signed_enum(SignedEnum::NEGATIVE);
+  msg->set_big_enum(BigEnum::BEGIN);
+  msg->set_nested_enum(EveryField::PONG);
+  msg->set_field_float(3.14f);
+  msg->set_field_string("FizzBuzz");
+  msg->add_repeated_int32(1);
+  msg->add_repeated_int32(-1);
+  msg->add_repeated_int32(100);
+  msg->add_repeated_int32(2000000);
+
+  auto binary_proto = msg.SerializeAsArray();
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  ProtoToArgsParser parser(pool);
+  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+
+  status = parser.ParseMessage(
+      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
+      ".protozero.test.protos.EveryField", nullptr, *this);
+
+  EXPECT_TRUE(status.ok())
+      << "InternProtoFieldsIntoArgsTable failed with error: "
+      << status.message();
+
+  EXPECT_THAT(
+      args(),
+      testing::ElementsAre(
+          "field_int32 field_int32 -1", "field_int64 field_int64 -333123456789",
+          "field_uint32 field_uint32 600",
+          "field_uint64 field_uint64 333123456789",
+          "field_sint32 field_sint32 -5", "field_sint64 field_sint64 -9000",
+          "field_fixed32 field_fixed32 12345",
+          "field_fixed64 field_fixed64 444123450000",
+          "field_sfixed32 field_sfixed32 -69999",
+          "field_sfixed64 field_sfixed64 -200",
+          "field_double field_double 0.5555", "field_bool field_bool true",
+          "small_enum small_enum TO_BE", "signed_enum signed_enum NEGATIVE",
+          "big_enum big_enum BEGIN", "nested_enum nested_enum PONG",
+          "field_float field_float 3.14", "field_string field_string FizzBuzz",
+          "repeated_int32 repeated_int32[0] 1",
+          "repeated_int32 repeated_int32[1] -1",
+          "repeated_int32 repeated_int32[2] 100",
+          "repeated_int32 repeated_int32[3] 2000000"));
+}
+
+TEST_F(ProtoToArgsParserTest, NestedProto) {
+  using namespace protozero::test::protos::pbzero;
+  protozero::HeapBuffered<NestedA> msg{kChunkSize, kChunkSize};
+  msg->set_super_nested()->set_value_c(3);
+
+  auto binary_proto = msg.SerializeAsArray();
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  ProtoToArgsParser parser(pool);
+  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+
+  status = parser.ParseMessage(
+      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
+      ".protozero.test.protos.NestedA", nullptr, *this);
+  EXPECT_TRUE(status.ok())
+      << "InternProtoFieldsIntoArgsTable failed with error: "
+      << status.message();
+  EXPECT_THAT(args(), testing::ElementsAre(
+                          "super_nested.value_c super_nested.value_c 3"));
+}
+
+TEST_F(ProtoToArgsParserTest, CamelCaseFieldsProto) {
+  using namespace protozero::test::protos::pbzero;
+  protozero::HeapBuffered<CamelCaseFields> msg{kChunkSize, kChunkSize};
+  msg->set_barbaz(true);
+  msg->set_moomoo(true);
+  msg->set___bigbang(true);
+
+  auto binary_proto = msg.SerializeAsArray();
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  ProtoToArgsParser parser(pool);
+  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+
+  status = parser.ParseMessage(
+      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
+      ".protozero.test.protos.CamelCaseFields", nullptr, *this);
+  EXPECT_TRUE(status.ok())
+      << "InternProtoFieldsIntoArgsTable failed with error: "
+      << status.message();
+  EXPECT_THAT(args(),
+              testing::ElementsAre("barBaz barBaz true", "MooMoo MooMoo true",
+                                   "__bigBang __bigBang true"));
+}
+
+TEST_F(ProtoToArgsParserTest, NestedProtoParsingOverrideHandled) {
+  using namespace protozero::test::protos::pbzero;
+  protozero::HeapBuffered<NestedA> msg{kChunkSize, kChunkSize};
+  msg->set_super_nested()->set_value_c(3);
+
+  auto binary_proto = msg.SerializeAsArray();
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  ProtoToArgsParser parser(pool);
+  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+
+  parser.AddParsingOverrideForField(
+      "super_nested.value_c",
+      [](const protozero::Field& field, ProtoToArgsParser::Delegate& writer) {
+        EXPECT_EQ(field.type(), protozero::proto_utils::ProtoWireType::kVarInt);
+        std::string key = "super_nested.value_b.replaced";
+        writer.AddInteger({key, key}, field.as_int32());
+        // We've handled this field by adding the desired args.
+        return base::OkStatus();
+      });
+
+  status = parser.ParseMessage(
+      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
+      ".protozero.test.protos.NestedA", nullptr, *this);
+  EXPECT_TRUE(status.ok())
+      << "InternProtoFieldsIntoArgsTable failed with error: "
+      << status.message();
+  EXPECT_THAT(
+      args(),
+      testing::ElementsAre(
+          "super_nested.value_b.replaced super_nested.value_b.replaced 3"));
+}
+
+TEST_F(ProtoToArgsParserTest, NestedProtoParsingOverrideSkipped) {
+  using namespace protozero::test::protos::pbzero;
+  protozero::HeapBuffered<NestedA> msg{kChunkSize, kChunkSize};
+  msg->set_super_nested()->set_value_c(3);
+
+  auto binary_proto = msg.SerializeAsArray();
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  ProtoToArgsParser parser(pool);
+  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+
+  parser.AddParsingOverrideForField(
+      "super_nested.value_c",
+      [](const protozero::Field& field, ProtoToArgsParser::Delegate&) {
+        static int val = 0;
+        ++val;
+        EXPECT_EQ(1, val);
+        EXPECT_EQ(field.type(), protozero::proto_utils::ProtoWireType::kVarInt);
+        return base::nullopt;
+      });
+
+  status = parser.ParseMessage(
+      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
+      ".protozero.test.protos.NestedA", nullptr, *this);
+  EXPECT_TRUE(status.ok())
+      << "InternProtoFieldsIntoArgsTable failed with error: "
+      << status.message();
+  EXPECT_THAT(args(), testing::ElementsAre(
+                          "super_nested.value_c super_nested.value_c 3"));
+}
+
+TEST_F(ProtoToArgsParserTest, LookingUpInternedStateParsingOverride) {
+  using namespace protozero::test::protos::pbzero;
+  // The test proto, we will use |value_c| as the source_location iid.
+  protozero::HeapBuffered<NestedA> msg{kChunkSize, kChunkSize};
+  msg->set_super_nested()->set_value_c(3);
+  auto binary_proto = msg.SerializeAsArray();
+
+  // The interned source location.
+  protozero::HeapBuffered<protos::pbzero::SourceLocation> src_loc{kChunkSize,
+                                                                  kChunkSize};
+  const uint64_t kIid = 3;
+  src_loc->set_iid(kIid);
+  src_loc->set_file_name("test_file_name");
+  // We need to update sequence_state to point to it.
+  auto binary_data = src_loc.SerializeAsArray();
+  std::unique_ptr<uint8_t[]> buffer(new uint8_t[binary_data.size()]);
+  for (size_t i = 0; i < binary_data.size(); ++i) {
+    buffer.get()[i] = binary_data[i];
+  }
+  TraceBlobView blob(std::move(buffer), 0, binary_data.size());
+  AddInternedSourceLocation(kIid, std::move(blob));
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+
+  ProtoToArgsParser parser(pool);
+  // Now we override the behaviour of |value_c| so we can expand the iid into
+  // multiple args rows.
+  parser.AddParsingOverrideForField(
+      "super_nested.value_c",
+      [](const protozero::Field& field, ProtoToArgsParser::Delegate& delegate)
+          -> base::Optional<base::Status> {
+        auto* decoder = delegate.GetInternedMessage(
+            protos::pbzero::InternedData::kSourceLocations, field.as_uint64());
+        if (!decoder) {
+          // Lookup failed fall back on default behaviour.
+          return base::nullopt;
+        }
+        delegate.AddString(ProtoToArgsParser::Key("file_name"),
+                           protozero::ConstChars{"file", 4});
+        delegate.AddInteger(ProtoToArgsParser::Key("line_number"), 2);
+        return base::OkStatus();
+      });
+
+  status = parser.ParseMessage(
+      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
+      ".protozero.test.protos.NestedA", nullptr, *this);
+  EXPECT_TRUE(status.ok())
+      << "InternProtoFieldsIntoArgsTable failed with error: "
+      << status.message();
+  EXPECT_THAT(args(), testing::ElementsAre("file_name file_name file",
+                                           "line_number line_number 2"));
+}
+
+TEST_F(ProtoToArgsParserTest, OverrideForType) {
+  using namespace protozero::test::protos::pbzero;
+  protozero::HeapBuffered<NestedA> msg{kChunkSize, kChunkSize};
+  msg->set_super_nested()->set_value_c(3);
+
+  auto binary_proto = msg.SerializeAsArray();
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+
+  ProtoToArgsParser parser(pool);
+
+  parser.AddParsingOverrideForType(
+      ".protozero.test.protos.NestedA.NestedB.NestedC",
+      [](ProtoToArgsParser::ScopedNestedKeyContext&,
+         const protozero::ConstBytes&, Delegate& delegate) {
+        delegate.AddInteger(ProtoToArgsParser::Key("arg"), 42);
+        return base::OkStatus();
+      });
+
+  status = parser.ParseMessage(
+      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
+      ".protozero.test.protos.NestedA", nullptr, *this);
+  EXPECT_TRUE(status.ok())
+      << "InternProtoFieldsIntoArgsTable failed with error: "
+      << status.message();
+  EXPECT_THAT(args(), testing::ElementsAre("arg arg 42"));
+}
+
+TEST_F(ProtoToArgsParserTest, FieldOverrideTakesPrecedence) {
+  using namespace protozero::test::protos::pbzero;
+  protozero::HeapBuffered<NestedA> msg{kChunkSize, kChunkSize};
+  msg->set_super_nested()->set_value_c(3);
+
+  auto binary_proto = msg.SerializeAsArray();
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+
+  ProtoToArgsParser parser(pool);
+
+  parser.AddParsingOverrideForField(
+      "super_nested",
+      [](const protozero::Field&, ProtoToArgsParser::Delegate& writer) {
+        writer.AddString(ProtoToArgsParser::Key("arg"),
+                         ToChars("override-for-field"));
+        return base::OkStatus();
+      });
+
+  parser.AddParsingOverrideForType(
+      ".protozero.test.protos.NestedA.NestedB.NestedC",
+      [](ProtoToArgsParser::ScopedNestedKeyContext&,
+         const protozero::ConstBytes&, Delegate& delegate) {
+        delegate.AddString(ProtoToArgsParser::Key("arg"),
+                           ToChars("override-for-type"));
+        return base::OkStatus();
+      });
+
+  status = parser.ParseMessage(
+      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
+      ".protozero.test.protos.NestedA", nullptr, *this);
+  EXPECT_TRUE(status.ok())
+      << "InternProtoFieldsIntoArgsTable failed with error: "
+      << status.message();
+  EXPECT_THAT(args(), testing::ElementsAre("arg arg override-for-field"));
+}
+
+}  // namespace
+}  // namespace util
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/trace_blob_view.h b/src/trace_processor/util/trace_blob_view.h
similarity index 96%
rename from src/trace_processor/trace_blob_view.h
rename to src/trace_processor/util/trace_blob_view.h
index 7a7889b..7d9c1a4 100644
--- a/src/trace_processor/trace_blob_view.h
+++ b/src/trace_processor/util/trace_blob_view.h
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-#ifndef SRC_TRACE_PROCESSOR_TRACE_BLOB_VIEW_H_
-#define SRC_TRACE_PROCESSOR_TRACE_BLOB_VIEW_H_
+#ifndef SRC_TRACE_PROCESSOR_UTIL_TRACE_BLOB_VIEW_H_
+#define SRC_TRACE_PROCESSOR_UTIL_TRACE_BLOB_VIEW_H_
 
 #include <stddef.h>
 #include <stdint.h>
@@ -145,4 +145,4 @@
 }  // namespace trace_processor
 }  // namespace perfetto
 
-#endif  // SRC_TRACE_PROCESSOR_TRACE_BLOB_VIEW_H_
+#endif  // SRC_TRACE_PROCESSOR_UTIL_TRACE_BLOB_VIEW_H_
diff --git a/src/trace_processor/virtual_destructors.cc b/src/trace_processor/virtual_destructors.cc
index 40cc0cd..db5225b 100644
--- a/src/trace_processor/virtual_destructors.cc
+++ b/src/trace_processor/virtual_destructors.cc
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-#include "src/trace_processor/chunked_trace_reader.h"
-#include "src/trace_processor/trace_parser.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
 
 namespace perfetto {
 namespace trace_processor {
diff --git a/ui/run-tests b/src/tracebox/BUILD.gn
old mode 100755
new mode 100644
similarity index 66%
copy from ui/run-tests
copy to src/tracebox/BUILD.gn
index d06f7d2..647ad5e0
--- a/ui/run-tests
+++ b/src/tracebox/BUILD.gn
@@ -1,4 +1,3 @@
-#!/bin/bash
 # Copyright (C) 2021 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,6 +12,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-UI_DIR="$(cd -P ${BASH_SOURCE[0]%/*}; pwd)"
+import("../../gn/perfetto.gni")
 
-$UI_DIR/node $UI_DIR/build.js --run-tests "$@"
+assert(perfetto_build_standalone)
+
+executable("tracebox") {
+  deps = [
+    "../../gn:default_deps",
+    "../base",
+    "../perfetto_cmd",
+    "../perfetto_cmd:trigger_perfetto_cmd",
+    "../traced/probes",
+    "../traced/service",
+  ]
+  sources = [ "tracebox.cc" ]
+}
diff --git a/src/tracebox/tracebox.cc b/src/tracebox/tracebox.cc
new file mode 100644
index 0000000..ab2cae8
--- /dev/null
+++ b/src/tracebox/tracebox.cc
@@ -0,0 +1,175 @@
+/*
+ * 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/file_utils.h"
+#include "perfetto/ext/base/pipe.h"
+#include "perfetto/ext/base/subprocess.h"
+#include "perfetto/ext/base/utils.h"
+#include "perfetto/ext/traced/traced.h"
+#include "src/perfetto_cmd/perfetto_cmd.h"
+
+#include <stdio.h>
+
+#include <tuple>
+
+namespace perfetto {
+namespace {
+
+struct Applet {
+  using MainFunction = int (*)(int /*argc*/, char** /*argv*/);
+  const char* name;
+  MainFunction entrypoint;
+};
+
+const Applet g_applets[]{
+    {"traced", ServiceMain},
+    {"traced_probes", ProbesMain},
+    {"perfetto", PerfettoCmdMain},
+    {"trigger_perfetto", TriggerPerfettoMain},
+};
+
+void PrintUsage() {
+  printf(R"(Welcome to Perfetto tracing!
+
+Tracebox is a bundle containing all the tracing services and the perfetto
+cmdline client in one binary. It can be used either to spawn manually the
+various subprocess or in "autostart" mode, which will take care of starting
+and tearing down the services for you.
+
+Usage in autostart mode:
+  tracebox -t 10s -o trace_file.perfetto-trace sched/sched_switch
+  See tracebox --help for more options.
+
+Usage in manual mode:
+  tracebox applet_name [args ...]  (e.g. ./tracebox traced --help)
+  Applets:)");
+
+  for (const Applet& applet : g_applets)
+    printf(" %s", applet.name);
+
+  printf(R"(
+
+See also:
+  * https://perfetto.dev/docs/
+  * The config editor in the record page of https://ui.perfetto.dev/
+)");
+}
+
+int TraceboxMain(int argc, char** argv) {
+  // Manual mode: if either the 1st argument (argv[1]) or the exe name (argv[0])
+  // match the name of an applet, directly invoke that without further
+  // modifications.
+
+  // Extract the file name from argv[0].
+  char* slash = strrchr(argv[0], '/');
+  char* argv0 = slash ? slash + 1 : argv[0];
+
+  for (const Applet& applet : g_applets) {
+    if (!strcmp(argv0, applet.name))
+      return applet.entrypoint(argc, argv);
+    if (argc > 1 && !strcmp(argv[1], applet.name))
+      return applet.entrypoint(argc - 1, &argv[1]);
+  }
+
+  // If no matching applet is found, switch to the autostart mode. In this mode
+  // we make tracebox behave like the cmdline client (without needing to prefix
+  // it with "perfetto"), but will also start traced and traced_probes.
+  // As part of this we also use a different namespace for the producer/consumer
+  // sockets, to avoid clashing with the system daemon.
+
+  if (argc <= 1) {
+    PrintUsage();
+    return 1;
+  }
+
+  auto pid_str = std::to_string(static_cast<uint64_t>(base::GetProcessId()));
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+  // Use an unlinked abstract domain socket on Linux/Android.
+  std::string consumer_socket = "@traced-c-" + pid_str;
+  std::string producer_socket = "@traced-p-" + pid_str;
+#elif PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
+  std::string consumer_socket = "/tmp/traced-c-" + pid_str;
+  std::string producer_socket = "/tmp/traced-p-" + pid_str;
+#else
+  PERFETTO_FATAL("The autostart mode is not supported on this platform");
+#endif
+
+  // If the caller has set the PERFETTO_*_SOCK_NAME, respect those.
+  const char* env;
+  if ((env = getenv("PERFETTO_CONSUMER_SOCK_NAME")))
+    consumer_socket = env;
+  if ((env = getenv("PERFETTO_PRODUCER_SOCK_NAME")))
+    consumer_socket = env;
+
+  base::SetEnv("PERFETTO_CONSUMER_SOCK_NAME", consumer_socket);
+  base::SetEnv("PERFETTO_PRODUCER_SOCK_NAME", producer_socket);
+
+  PerfettoCmd perfetto_cmd;
+
+  // If the cmdline parsing fails, stop here, no need to spawn services.
+  // It will daemonize if --background. In that case the subprocesses will be
+  // spawned by the damonized cmdline client, which is what we want so killing
+  // the backgrounded cmdline client will also kill the other services, as they
+  // will live in the same background session.
+  auto opt_res = perfetto_cmd.ParseCmdlineAndMaybeDaemonize(argc, argv);
+  if (opt_res.has_value())
+    return *opt_res;
+
+  std::string self_path = base::GetCurExecutablePath();
+  base::Subprocess traced({self_path, "traced"});
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  // |traced_sync_pipe| is used to synchronize with traced socket creation.
+  // traced will write "1" and close the FD when the IPC socket is listening
+  // (or traced crashed).
+  base::Pipe traced_sync_pipe = base::Pipe::Create();
+  int wr_fd = *traced_sync_pipe.wr;
+  base::SetEnv("TRACED_NOTIFY_FD", std::to_string(wr_fd));
+  traced.args.preserve_fds.emplace_back(wr_fd);
+  // Create a new process group so CTRL-C is delivered only to the cmdline
+  // process (the tracebox one) and not to traced. traced will still exit once
+  // the main process exits, but this allows graceful stopping of the trace
+  // without abruptedly killing traced{,probes} when hitting CTRL+C.
+  traced.args.posix_proc_group_id = 0;  // 0 = start a new process group.
+#endif
+  traced.Start();
+
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  traced_sync_pipe.wr.reset();
+
+  std::string traced_notify_msg;
+  base::ReadPlatformHandle(*traced_sync_pipe.rd, &traced_notify_msg);
+  if (traced_notify_msg != "1")
+    PERFETTO_FATAL("The tracing service failed unexpectedly. Check the logs");
+#endif
+
+  base::Subprocess traced_probes(
+      {self_path, "traced_probes", "--reset-ftrace"});
+  // Put traced_probes in the same process group as traced. Same reason (CTRL+C)
+  // but it's not worth creating a new group.
+  traced_probes.args.posix_proc_group_id = traced.pid();
+  traced_probes.Start();
+
+  perfetto_cmd.ConnectToServiceAndRun();
+  return 0;
+}
+
+}  // namespace
+}  // namespace perfetto
+
+int main(int argc, char** argv) {
+  return perfetto::TraceboxMain(argc, argv);
+}
diff --git a/src/traced/probes/BUILD.gn b/src/traced/probes/BUILD.gn
index dae05f4..db547fc 100644
--- a/src/traced/probes/BUILD.gn
+++ b/src/traced/probes/BUILD.gn
@@ -53,6 +53,7 @@
     "../../../protos/perfetto/config/ftrace:cpp",
     "../../../protos/perfetto/trace:zero",
     "../../../protos/perfetto/trace/ps:zero",
+    "../../android_stats",
     "../../base",
     "../../tracing/core",
     "../../tracing/ipc/producer",
diff --git a/src/traced/probes/filesystem/inode_file_data_source_unittest.cc b/src/traced/probes/filesystem/inode_file_data_source_unittest.cc
index edd208b..ea62a29 100644
--- a/src/traced/probes/filesystem/inode_file_data_source_unittest.cc
+++ b/src/traced/probes/filesystem/inode_file_data_source_unittest.cc
@@ -133,7 +133,7 @@
   EXPECT_CALL(*data_source, FillInodeEntry(_, buf.st_ino, Eq(value)));
 
   data_source->OnInodes({{buf.st_ino, buf.st_dev}});
-  // Expect that the found inode is not added the the LRU cache.
+  // Expect that the found inode is not added the LRU cache.
   EXPECT_THAT(cache_.Get(std::make_pair(buf.st_dev, buf.st_ino)), IsNull());
 }
 
diff --git a/src/traced/probes/ftrace/atrace_wrapper.cc b/src/traced/probes/ftrace/atrace_wrapper.cc
index 2522e47..43b9862 100644
--- a/src/traced/probes/ftrace/atrace_wrapper.cc
+++ b/src/traced/probes/ftrace/atrace_wrapper.cc
@@ -25,16 +25,24 @@
 #include <sys/wait.h>
 #include <unistd.h>
 
+#include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/time.h"
+#include "perfetto/ext/base/optional.h"
 #include "perfetto/ext/base/pipe.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/utils.h"
 
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+#include <sys/system_properties.h>
+#endif  // PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+
 namespace perfetto {
 
 namespace {
 
 RunAtraceFunction g_run_atrace_for_testing = nullptr;
+base::Optional<bool> g_is_old_atrace_for_testing{};
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
 // Args should include "atrace" for argv[0].
@@ -183,4 +191,29 @@
   g_run_atrace_for_testing = f;
 }
 
+bool IsOldAtrace() {
+  if (g_is_old_atrace_for_testing.has_value())
+    return *g_is_old_atrace_for_testing;
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) && \
+    !PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
+  // Sideloaded case. We could be sideloaded on a modern device or an older one.
+  char str_value[PROP_VALUE_MAX];
+  if (!__system_property_get("ro.build.version.sdk", str_value))
+    return false;
+  auto opt_value = base::CStringToUInt32(str_value);
+  return opt_value.has_value() && *opt_value < 28;  // 28 == Android P.
+#else
+  // In in-tree builds we know that atrace is current, no runtime checks needed.
+  return false;
+#endif
+}
+
+void SetIsOldAtraceForTesting(bool value) {
+  g_is_old_atrace_for_testing = value;
+}
+
+void ClearIsOldAtraceForTesting() {
+  g_is_old_atrace_for_testing.reset();
+}
+
 }  // namespace perfetto
diff --git a/src/traced/probes/ftrace/atrace_wrapper.h b/src/traced/probes/ftrace/atrace_wrapper.h
index 264cee0..91f02cb 100644
--- a/src/traced/probes/ftrace/atrace_wrapper.h
+++ b/src/traced/probes/ftrace/atrace_wrapper.h
@@ -26,6 +26,15 @@
 using RunAtraceFunction =
     std::add_pointer<bool(const std::vector<std::string>& /*args*/)>::type;
 
+// When we are sideloaded on an old version of Android (pre P), we cannot use
+// atrace --only_userspace because that option doesn't exist. In that case we:
+// - Just use atrace --async_start/stop, which will cause atrace to also
+//   poke at ftrace.
+// - Suppress the checks for "somebody else enabled ftrace unexpectedly".
+bool IsOldAtrace();
+void SetIsOldAtraceForTesting(bool);
+void ClearIsOldAtraceForTesting();
+
 bool RunAtrace(const std::vector<std::string>& args);
 void SetRunAtraceForTesting(RunAtraceFunction);
 
diff --git a/src/traced/probes/ftrace/cpu_reader.cc b/src/traced/probes/ftrace/cpu_reader.cc
index eaba2b2..facd4ce 100644
--- a/src/traced/probes/ftrace/cpu_reader.cc
+++ b/src/traced/probes/ftrace/cpu_reader.cc
@@ -634,7 +634,7 @@
     for (const Field& field : info.fields) {
       auto generic_field = nested->BeginNestedMessage<protozero::Message>(
           GenericFtraceEvent::kFieldFieldNumber);
-      // TODO(taylori): Avoid outputting field names every time.
+      // TODO(hjd): Avoid outputting field names every time.
       generic_field->AppendString(GenericFtraceEvent::Field::kNameFieldNumber,
                                   field.ftrace_name);
       success &= ParseField(field, start, end, table, generic_field, metadata);
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index 0bd85ba..b8c23e2 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -1085,6 +1085,25 @@
        kUnsetFtraceId,
        346,
        kUnsetSize},
+      {"cpuhp_pause",
+       "cpuhp",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "active_cpus", 1, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "cpus", 2, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "pause", 3, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "time", 4, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       352,
+       kUnsetSize},
       {"dma_heap_stat",
        "dmabuf_heap",
        {
@@ -6157,6 +6176,25 @@
        kUnsetFtraceId,
        242,
        kUnsetSize},
+      {"sched_pi_setprio",
+       "sched",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "comm", 1, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "newprio", 2, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "oldprio", 3, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "pid", 4, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       353,
+       kUnsetSize},
       {"scm_call_start",
        "scm",
        {
@@ -6196,6 +6234,140 @@
        kUnsetFtraceId,
        333,
        kUnsetSize},
+      {"sde_evtlog",
+       "sde",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "evtlog_tag", 1, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "pid", 2, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "tag_id", 3, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       354,
+       kUnsetSize},
+      {"sde_perf_calc_crtc",
+       "sde",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "bw_ctl_ebi", 1, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "bw_ctl_llcc", 2, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "bw_ctl_mnoc", 3, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "core_clk_rate", 4, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "crtc", 5, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "ib_ebi", 6, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "ib_llcc", 7, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "ib_mnoc", 8, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       355,
+       kUnsetSize},
+      {"sde_perf_crtc_update",
+       "sde",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "bw_ctl_ebi", 1, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "bw_ctl_llcc", 2, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "bw_ctl_mnoc", 3, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "core_clk_rate", 4, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "crtc", 5, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "params", 6, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "per_pipe_ib_ebi", 7, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "per_pipe_ib_llcc", 8, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "per_pipe_ib_mnoc", 9, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "stop_req", 10, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "update_bus", 11, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "update_clk", 12, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       356,
+       kUnsetSize},
+      {"sde_perf_set_qos_luts",
+       "sde",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "fl", 1, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "fmt", 2, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "lut", 3, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "lut_usage", 4, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "pnum", 5, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "rt", 6, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       357,
+       kUnsetSize},
+      {"sde_perf_update_bus",
+       "sde",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "ab_quota", 1, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "bus_id", 2, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "client", 3, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "ib_quota", 4, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       358,
+       kUnsetSize},
       {"signal_deliver",
        "signal",
        {
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index fa789ae..0af1f2c 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -269,6 +269,7 @@
         events.insert(GroupAndName("power", "suspend_resume"));
         events.insert(GroupAndName("cpuhp", "cpuhp_enter"));
         events.insert(GroupAndName("cpuhp", "cpuhp_exit"));
+        events.insert(GroupAndName("cpuhp", "cpuhp_pause"));
         AddEventGroup(table, "msm_bus", &events);
         events.insert(GroupAndName("msm_bus", "bus_update_request_end"));
         events.insert(GroupAndName("msm_bus", "bus_update_request"));
@@ -454,7 +455,7 @@
     PERFETTO_DCHECK(active_configs_.empty());
 
     // If someone outside of perfetto is using ftrace give up now.
-    if (is_ftrace_enabled) {
+    if (is_ftrace_enabled && !IsOldAtrace()) {
       PERFETTO_ELOG("ftrace in use by non-Perfetto.");
       return 0;
     }
@@ -465,7 +466,7 @@
     SetupBufferSize(request);
   } else {
     // Did someone turn ftrace off behind our back? If so give up.
-    if (!active_configs_.empty() && !is_ftrace_enabled) {
+    if (!active_configs_.empty() && !is_ftrace_enabled && !IsOldAtrace()) {
       PERFETTO_ELOG("ftrace disabled by non-Perfetto.");
       return 0;
     }
@@ -485,8 +486,15 @@
     }
   }
 
-  if (RequiresAtrace(request))
+  if (RequiresAtrace(request)) {
+    if (IsOldAtrace() && !ds_configs_.empty()) {
+      PERFETTO_ELOG(
+          "Concurrent atrace sessions are not supported before Android P, "
+          "bailing out.");
+      return 0;
+    }
     UpdateAtrace(request);
+  }
 
   for (const auto& group_and_name : events) {
     const Event* event = table_->GetOrCreateEvent(group_and_name);
@@ -532,7 +540,7 @@
   }
 
   if (active_configs_.empty()) {
-    if (ftrace_->IsTracingEnabled()) {
+    if (ftrace_->IsTracingEnabled() && !IsOldAtrace()) {
       // If someone outside of perfetto is using ftrace give up now.
       PERFETTO_ELOG("ftrace in use by non-Perfetto.");
       return false;
@@ -694,7 +702,8 @@
   std::vector<std::string> args;
   args.push_back("atrace");  // argv0 for exec()
   args.push_back("--async_start");
-  args.push_back("--only_userspace");
+  if (!IsOldAtrace())
+    args.push_back("--only_userspace");
 
   for (const auto& category : categories)
     args.push_back(category);
@@ -720,7 +729,10 @@
 
   PERFETTO_DLOG("Stop atrace...");
 
-  if (RunAtrace({"atrace", "--async_stop", "--only_userspace"})) {
+  std::vector<std::string> args{"atrace", "--async_stop"};
+  if (!IsOldAtrace())
+    args.push_back("--only_userspace");
+  if (RunAtrace(args)) {
     current_state_.atrace_categories.clear();
     current_state_.atrace_apps.clear();
     current_state_.atrace_on = false;
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.h b/src/traced/probes/ftrace/ftrace_config_muxer.h
index 3965371..5aee63a 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.h
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.h
@@ -53,7 +53,7 @@
   std::vector<std::string> atrace_apps;
   std::vector<std::string> atrace_categories;
 
-  // When enabled will turn on the the kallsyms symbolizer in CpuReader.
+  // When enabled will turn on the kallsyms symbolizer in CpuReader.
   const bool symbolize_ksyms;
 };
 
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
index c3602aa..312f849 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
@@ -26,11 +26,11 @@
 
 using testing::_;
 using testing::AnyNumber;
-using testing::MatchesRegex;
 using testing::Contains;
 using testing::ElementsAreArray;
 using testing::Eq;
 using testing::IsEmpty;
+using testing::MatchesRegex;
 using testing::NiceMock;
 using testing::Not;
 using testing::Return;
@@ -93,6 +93,14 @@
 
 class FtraceConfigMuxerTest : public ::testing::Test {
  protected:
+  void SetUp() override {
+    // Don't probe for older SDK levels, that would relax the atrace-related
+    // checks on older versions of Android (But some tests here test those).
+    // We want the unittests to behave consistently (as if we were on a post P
+    // device) regardless of the Android versions they run on.
+    SetIsOldAtraceForTesting(false);
+  }
+  void TearDown() override { ClearIsOldAtraceForTesting(); }
   std::unique_ptr<MockProtoTranslationTable> GetMockTable() {
     std::vector<Field> common_fields;
     std::vector<Event> events;
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index 53ac1e5..0193e49 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -33,6 +33,7 @@
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/metatrace.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/tracing/core/trace_writer.h"
 #include "src/kallsyms/kernel_symbol_map.h"
 #include "src/kallsyms/lazy_kernel_symbolizer.h"
@@ -84,15 +85,17 @@
   return drain_period_ms;
 }
 
-void WriteToFile(const char* path, const char* str) {
+bool WriteToFile(const char* path, const char* str) {
   auto fd = base::OpenFile(path, O_WRONLY);
   if (!fd)
-    return;
-  base::ignore_result(base::WriteAll(*fd, str, strlen(str)));
+    return false;
+  const size_t str_len = strlen(str);
+  return base::WriteAll(*fd, str, str_len) == static_cast<ssize_t>(str_len);
 }
 
-void ClearFile(const char* path) {
+bool ClearFile(const char* path) {
   auto fd = base::OpenFile(path, O_WRONLY | O_TRUNC);
+  return !!fd;
 }
 
 }  // namespace
@@ -100,18 +103,21 @@
 // Method of last resort to reset ftrace state.
 // We don't know what state the rest of the system and process is so as far
 // as possible avoid allocations.
-void HardResetFtraceState() {
-  PERFETTO_LOG("Hard resetting ftrace state.");
-
-  WriteToFile("/sys/kernel/debug/tracing/tracing_on", "0");
-  WriteToFile("/sys/kernel/debug/tracing/buffer_size_kb", "4");
-  WriteToFile("/sys/kernel/debug/tracing/events/enable", "0");
-  ClearFile("/sys/kernel/debug/tracing/trace");
-
-  WriteToFile("/sys/kernel/tracing/tracing_on", "0");
-  WriteToFile("/sys/kernel/tracing/buffer_size_kb", "4");
-  WriteToFile("/sys/kernel/tracing/events/enable", "0");
-  ClearFile("/sys/kernel/tracing/trace");
+bool HardResetFtraceState() {
+  for (const char* const* item = FtraceProcfs::kTracingPaths; *item; ++item) {
+    std::string prefix(*item);
+    PERFETTO_CHECK(base::EndsWith(prefix, "/"));
+    bool res = true;
+    res &= WriteToFile((prefix + "tracing_on").c_str(), "0");
+    res &= WriteToFile((prefix + "buffer_size_kb").c_str(), "4");
+    // We deliberately don't check for this as on some older versions of Android
+    // events/enable was not writable by the shell user.
+    WriteToFile((prefix + "events/enable").c_str(), "0");
+    res &= ClearFile((prefix + "trace").c_str());
+    if (res)
+      return true;
+  }
+  return false;
 }
 
 // static
diff --git a/src/traced/probes/ftrace/ftrace_controller.h b/src/traced/probes/ftrace/ftrace_controller.h
index 3e150a3..71250c8 100644
--- a/src/traced/probes/ftrace/ftrace_controller.h
+++ b/src/traced/probes/ftrace/ftrace_controller.h
@@ -45,7 +45,7 @@
 struct FtraceStats;
 
 // Method of last resort to reset ftrace state.
-void HardResetFtraceState();
+bool HardResetFtraceState();
 
 // Utility class for controlling ftrace.
 class FtraceController {
diff --git a/src/traced/probes/ftrace/test/data/synthetic/available_events b/src/traced/probes/ftrace/test/data/synthetic/available_events
index b20b698..2ee225a 100644
--- a/src/traced/probes/ftrace/test/data/synthetic/available_events
+++ b/src/traced/probes/ftrace/test/data/synthetic/available_events
@@ -11,3 +11,4 @@
 dpu:tracing_mark_write
 g2d:tracing_mark_write
 power:suspend_resume
+cpuhp:cpuhp_pause
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/cpuhp/cpuhp_pause/format b/src/traced/probes/ftrace/test/data/synthetic/events/cpuhp/cpuhp_pause/format
new file mode 100644
index 0000000..e0ff5e8
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/cpuhp/cpuhp_pause/format
@@ -0,0 +1,14 @@
+name: cpuhp_pause
+ID: 68
+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 cpus;	offset:8;	size:4;	signed:0;
+	field:unsigned int active_cpus;	offset:12;	size:4;	signed:0;
+	field:unsigned int time;	offset:16;	size:4;	signed:0;
+	field:unsigned char pause;	offset:20;	size:1;	signed:0;
+
+print fmt: "req_cpus=0x%x act_cpus=0x%x time=%u us paused=%d", REC->cpus, REC->active_cpus, REC->time, REC->pause
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/sched/sched_pi_setprio/format b/src/traced/probes/ftrace/test/data/synthetic/events/sched/sched_pi_setprio/format
new file mode 100644
index 0000000..818f484
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/sched/sched_pi_setprio/format
@@ -0,0 +1,14 @@
+name: sched_pi_setprio
+ID: 77
+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:char comm[16];	offset:8;	size:16;	signed:0;
+	field:pid_t pid;	offset:24;	size:4;	signed:1;
+	field:int oldprio;	offset:28;	size:4;	signed:1;
+	field:int newprio;	offset:32;	size:4;	signed:1;
+
+print fmt: "comm=%s pid=%d oldprio=%d newprio=%d", REC->comm, REC->pid, REC->oldprio, REC->newprio
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_evtlog/format b/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_evtlog/format
new file mode 100644
index 0000000..8b3cda1
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_evtlog/format
@@ -0,0 +1,14 @@
+name: sde_evtlog
+ID: 590
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:int pid;	offset:8;	size:4;	signed:1;
+	field:__data_loc char[] evtlog_tag;	offset:12;	size:4;	signed:0;
+	field:u32 tag_id;	offset:16;	size:4;	signed:0;
+	field:u32 data[15];	offset:20;	size:60;	signed:0;
+
+print fmt: "%d|%s:%d|%x|%x|%x|%x|%x|%x|%x|%x|%x|%x|%x|%x|%x|%x|%x", REC->pid, __get_str(evtlog_tag), REC->tag_id, REC->data[0], REC->data[1], REC->data[2], REC->data[3], REC->data[4], REC->data[5], REC->data[6], REC->data[7], REC->data[8], REC->data[9], REC->data[10], REC->data[11], REC->data[12], REC->data[13], REC->data[14]
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_calc_crtc/format b/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_calc_crtc/format
new file mode 100644
index 0000000..728bd05
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_calc_crtc/format
@@ -0,0 +1,18 @@
+name: sde_perf_calc_crtc
+ID: 591
+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:u32 crtc;	offset:8;	size:4;	signed:0;
+	field:u64 bw_ctl_mnoc;	offset:16;	size:8;	signed:0;
+	field:u64 bw_ctl_llcc;	offset:24;	size:8;	signed:0;
+	field:u64 bw_ctl_ebi;	offset:32;	size:8;	signed:0;
+	field:u64 ib_mnoc;	offset:40;	size:8;	signed:0;
+	field:u64 ib_llcc;	offset:48;	size:8;	signed:0;
+	field:u64 ib_ebi;	offset:56;	size:8;	signed:0;
+	field:u32 core_clk_rate;	offset:64;	size:4;	signed:0;
+
+print fmt: "crtc=%d mnoc=[%llu, %llu] llcc=[%llu %llu] ebi=[%llu, %llu] clk_rate=%u", REC->crtc, REC->bw_ctl_mnoc, REC->ib_mnoc, REC->bw_ctl_llcc, REC->ib_llcc, REC->bw_ctl_ebi, REC->ib_ebi, REC->core_clk_rate
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_crtc_update/format b/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_crtc_update/format
new file mode 100644
index 0000000..fda8a78
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_crtc_update/format
@@ -0,0 +1,22 @@
+name: sde_perf_crtc_update
+ID: 592
+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:u32 crtc;	offset:8;	size:4;	signed:0;
+	field:u64 bw_ctl_mnoc;	offset:16;	size:8;	signed:0;
+	field:u64 per_pipe_ib_mnoc;	offset:24;	size:8;	signed:0;
+	field:u64 bw_ctl_llcc;	offset:32;	size:8;	signed:0;
+	field:u64 per_pipe_ib_llcc;	offset:40;	size:8;	signed:0;
+	field:u64 bw_ctl_ebi;	offset:48;	size:8;	signed:0;
+	field:u64 per_pipe_ib_ebi;	offset:56;	size:8;	signed:0;
+	field:u32 core_clk_rate;	offset:64;	size:4;	signed:0;
+	field:bool stop_req;	offset:68;	size:1;	signed:0;
+	field:u32 update_bus;	offset:72;	size:4;	signed:0;
+	field:u32 update_clk;	offset:76;	size:4;	signed:0;
+	field:int params;	offset:80;	size:4;	signed:1;
+
+print fmt: "crtc=%d mnoc=[%llu %llu] llcc=[%llu %llu] ebi=[%llu %llu] clk=%u stop=%d ubus=%d uclk=%d %d", REC->crtc, REC->bw_ctl_mnoc, REC->per_pipe_ib_mnoc, REC->bw_ctl_llcc, REC->per_pipe_ib_llcc, REC->bw_ctl_ebi, REC->per_pipe_ib_ebi, REC->core_clk_rate, REC->stop_req, REC->update_bus, REC->update_clk, REC->params
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_set_qos_luts/format b/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_set_qos_luts/format
new file mode 100644
index 0000000..6762f3d
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_set_qos_luts/format
@@ -0,0 +1,16 @@
+name: sde_perf_set_qos_luts
+ID: 595
+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:u32 pnum;	offset:8;	size:4;	signed:0;
+	field:u32 fmt;	offset:12;	size:4;	signed:0;
+	field:bool rt;	offset:16;	size:1;	signed:0;
+	field:u32 fl;	offset:20;	size:4;	signed:0;
+	field:u64 lut;	offset:24;	size:8;	signed:0;
+	field:u32 lut_usage;	offset:32;	size:4;	signed:0;
+
+print fmt: "pnum=%d fmt=%x rt=%d fl=%d lut=0x%llx lut_usage=%d", REC->pnum, REC->fmt, REC->rt, REC->fl, REC->lut, REC->lut_usage
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_update_bus/format b/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_update_bus/format
new file mode 100644
index 0000000..f8a0e58
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/sde/sde_perf_update_bus/format
@@ -0,0 +1,14 @@
+name: sde_perf_update_bus
+ID: 596
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:int client;	offset:8;	size:4;	signed:1;
+	field:u32 bus_id;	offset:12;	size:4;	signed:0;
+	field:u64 ab_quota;	offset:16;	size:8;	signed:0;
+	field:u64 ib_quota;	offset:24;	size:8;	signed:0;
+
+print fmt: "Request client:%d bus_id:%d ab=%llu ib=%llu", REC->client, REC->bus_id, REC->ab_quota, REC->ib_quota
diff --git a/src/traced/probes/kmem_activity_trigger.cc b/src/traced/probes/kmem_activity_trigger.cc
index 15c007a..589cdab 100644
--- a/src/traced/probes/kmem_activity_trigger.cc
+++ b/src/traced/probes/kmem_activity_trigger.cc
@@ -77,7 +77,6 @@
 
   // Enable mm trace events
   ftrace_procfs_->DisableAllEvents();
-  ftrace_procfs_->EnableEvent("vmscan", "mm_vmscan_kswapd_wake");
   ftrace_procfs_->EnableEvent("vmscan", "mm_vmscan_direct_reclaim_begin");
   ftrace_procfs_->EnableEvent("compaction", "mm_compaction_begin");
   ftrace_procfs_->EnableTracing();
diff --git a/src/traced/probes/packages_list/packages_list_parser.cc b/src/traced/probes/packages_list/packages_list_parser.cc
index bcbccaa..9b58e20 100644
--- a/src/traced/probes/packages_list/packages_list_parser.cc
+++ b/src/traced/probes/packages_list/packages_list_parser.cc
@@ -69,6 +69,20 @@
         package->version_code = version_code;
         break;
       }
+      case 8: {
+        char* end;
+        long long profileable = strtoll(ss.cur_token(), &end, 10);
+        if ((*end != '\0' && *end != '\n') || *ss.cur_token() == '\0') {
+          PERFETTO_ELOG("Failed to parse packages.list profileable.");
+          return false;
+        }
+        package->profileable = profileable != 0;
+        break;
+      }
+      case 9:
+        package->installed_by =
+            std::string(ss.cur_token(), ss.cur_token_size());
+        break;
     }
     ++idx;
   }
diff --git a/src/traced/probes/packages_list/packages_list_parser.h b/src/traced/probes/packages_list/packages_list_parser.h
index 3deb3c5..9cbecd1 100644
--- a/src/traced/probes/packages_list/packages_list_parser.h
+++ b/src/traced/probes/packages_list/packages_list_parser.h
@@ -28,6 +28,8 @@
   bool debuggable = false;
   bool profileable_from_shell = false;
   int64_t version_code = 0;
+  bool profileable = false;
+  std::string installed_by;
 };
 
 bool ReadPackagesListLine(char* line, Package* package);
diff --git a/src/traced/probes/power/android_power_data_source.cc b/src/traced/probes/power/android_power_data_source.cc
index 0034453..1dccb1a 100644
--- a/src/traced/probes/power/android_power_data_source.cc
+++ b/src/traced/probes/power/android_power_data_source.cc
@@ -41,6 +41,7 @@
 
 namespace {
 constexpr uint32_t kMinPollIntervalMs = 100;
+constexpr uint32_t kDefaultPollIntervalMs = 1000;
 constexpr size_t kMaxNumRails = 32;
 constexpr size_t kMaxNumEnergyConsumer = 32;
 constexpr size_t kMaxNumPowerEntities = 256;
@@ -150,6 +151,9 @@
   energy_breakdown_collection_enabled_ =
       pcfg.collect_energy_estimation_breakdown();
 
+  if (poll_interval_ms_ == 0)
+    poll_interval_ms_ = kDefaultPollIntervalMs;
+
   if (poll_interval_ms_ < kMinPollIntervalMs) {
     PERFETTO_ELOG("Battery poll interval of %" PRIu32
                   " ms is too low. Capping to %" PRIu32 " ms",
diff --git a/src/traced/probes/probes.cc b/src/traced/probes/probes.cc
index 60d1cc7..3ec4da1 100644
--- a/src/traced/probes/probes.cc
+++ b/src/traced/probes/probes.cc
@@ -22,6 +22,7 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/getopt.h"
 #include "perfetto/ext/base/unix_task_runner.h"
+#include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/base/version.h"
 #include "perfetto/ext/traced/traced.h"
 #include "perfetto/ext/tracing/ipc/default_socket.h"
@@ -36,10 +37,17 @@
   enum LongOption {
     OPT_CLEANUP_AFTER_CRASH = 1000,
     OPT_VERSION,
+    OPT_BACKGROUND,
+    OPT_RESET_FTRACE,
   };
 
+  bool background = false;
+  bool reset_ftrace = false;
+
   static const option long_options[] = {
+      {"background", no_argument, nullptr, OPT_BACKGROUND},
       {"cleanup-after-crash", no_argument, nullptr, OPT_CLEANUP_AFTER_CRASH},
+      {"reset-ftrace", no_argument, nullptr, OPT_RESET_FTRACE},
       {"version", no_argument, nullptr, OPT_VERSION},
       {nullptr, 0, nullptr, 0}};
 
@@ -48,18 +56,41 @@
     if (option == -1)
       break;
     switch (option) {
+      case OPT_BACKGROUND:
+        background = true;
+        break;
       case OPT_CLEANUP_AFTER_CRASH:
+        // Used by perfetto.rc in Android.
+        PERFETTO_LOG("Hard resetting ftrace state.");
         HardResetFtraceState();
         return 0;
+      case OPT_RESET_FTRACE:
+        // This is like --cleanup-after-crash but doesn't quit.
+        reset_ftrace = true;
+        break;
       case OPT_VERSION:
         printf("%s\n", base::GetVersionString());
         return 0;
       default:
-        PERFETTO_ELOG("Usage: %s [--cleanup-after-crash|--version]", argv[0]);
+        fprintf(
+            stderr,
+            "Usage: %s [--background] [--reset-ftrace] [--cleanup-after-crash] "
+            "[--version]\n",
+            argv[0]);
         return 1;
     }
   }
 
+  if (reset_ftrace && !HardResetFtraceState()) {
+    PERFETTO_ELOG(
+        "Failed to reset ftrace. Either run this as root or run "
+        "`sudo chown -R $USER /sys/kernel/tracing`");
+  }
+
+  if (background) {
+    base::Daemonize();
+  }
+
   base::Watchdog* watchdog = base::Watchdog::GetInstance();
   // The memory watchdog will be updated soon after connect, once the shmem
   // buffer size is known, in ProbesProducer::OnTracingSetup().
diff --git a/src/traced/probes/probes_producer.cc b/src/traced/probes/probes_producer.cc
index cfa893d..9907a73 100644
--- a/src/traced/probes/probes_producer.cc
+++ b/src/traced/probes/probes_producer.cc
@@ -33,6 +33,7 @@
 #include "perfetto/tracing/core/data_source_config.h"
 #include "perfetto/tracing/core/data_source_descriptor.h"
 #include "perfetto/tracing/core/trace_config.h"
+#include "src/android_stats/statsd_logging_helper.h"
 #include "src/traced/probes/android_log/android_log_data_source.h"
 #include "src/traced/probes/common/cpu_freq_info.h"
 #include "src/traced/probes/filesystem/inode_file_data_source.h"
@@ -538,9 +539,16 @@
 }
 
 void ProbesProducer::ActivateTrigger(std::string trigger) {
+  android_stats::MaybeLogTriggerEvent(
+      PerfettoTriggerAtom::kProbesProducerTrigger, trigger);
+
   task_runner_->PostTask([this, trigger]() {
-    if (endpoint_)
-      endpoint_->ActivateTriggers({trigger});
+    if (!endpoint_) {
+      android_stats::MaybeLogTriggerEvent(
+          PerfettoTriggerAtom::kProbesProducerTriggerFail, trigger);
+      return;
+    }
+    endpoint_->ActivateTriggers({trigger});
   });
 }
 
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source.cc b/src/traced/probes/sys_stats/sys_stats_data_source.cc
index f900db8..3d75ff2 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source.cc
+++ b/src/traced/probes/sys_stats/sys_stats_data_source.cc
@@ -138,19 +138,21 @@
     stat_enabled_fields_ |= 1ul << static_cast<uint32_t>(*counter);
   }
 
-  std::array<uint32_t, 3> periods_ms{};
-  std::array<uint32_t, 3> ticks{};
+  std::array<uint32_t, 4> periods_ms{};
+  std::array<uint32_t, 4> ticks{};
   static_assert(periods_ms.size() == ticks.size(), "must have same size");
 
   periods_ms[0] = ClampTo10Ms(cfg.meminfo_period_ms(), "meminfo_period_ms");
   periods_ms[1] = ClampTo10Ms(cfg.vmstat_period_ms(), "vmstat_period_ms");
   periods_ms[2] = ClampTo10Ms(cfg.stat_period_ms(), "stat_period_ms");
+  periods_ms[3] = ClampTo10Ms(cfg.devfreq_period_ms(), "devfreq_period_ms");
 
   tick_period_ms_ = 0;
   for (uint32_t ms : periods_ms) {
     if (ms && (ms < tick_period_ms_ || tick_period_ms_ == 0))
       tick_period_ms_ = ms;
   }
+
   if (tick_period_ms_ == 0)
     return;  // No polling configured.
 
@@ -165,6 +167,7 @@
   meminfo_ticks_ = ticks[0];
   vmstat_ticks_ = ticks[1];
   stat_ticks_ = ticks[2];
+  devfreq_ticks_ = ticks[3];
 }
 
 void SysStatsDataSource::Start() {
@@ -205,12 +208,61 @@
   if (stat_ticks_ && tick_ % stat_ticks_ == 0)
     ReadStat(sys_stats);
 
+  if (devfreq_ticks_ && tick_ % devfreq_ticks_ == 0)
+    ReadDevfreq(sys_stats);
+
   sys_stats->set_collection_end_timestamp(
       static_cast<uint64_t>(base::GetBootTimeNs().count()));
 
   tick_++;
 }
 
+void SysStatsDataSource::ReadDevfreq(protos::pbzero::SysStats* sys_stats) {
+  base::ScopedDir devfreq_dir = OpenDevfreqDir();
+  if (devfreq_dir) {
+    while (struct dirent* dir_ent = readdir(*devfreq_dir)) {
+      // Entries in /sys/class/devfreq are symlinks to /devices/platform
+      if (dir_ent->d_type != DT_LNK)
+        continue;
+      const char* name = dir_ent->d_name;
+      const char* file_content = ReadDevfreqCurFreq(name);
+      auto value = static_cast<uint64_t>(strtoll(file_content, nullptr, 10));
+      auto* devfreq = sys_stats->add_devfreq();
+      devfreq->set_key(name);
+      devfreq->set_value(value);
+    }
+  }
+}
+
+base::ScopedDir SysStatsDataSource::OpenDevfreqDir() {
+  const char* base_dir = "/sys/class/devfreq/";
+  base::ScopedDir devfreq_dir(opendir(base_dir));
+  if (!devfreq_dir && !devfreq_error_logged_) {
+    devfreq_error_logged_ = true;
+    PERFETTO_PLOG("failed to opendir(/sys/class/devfreq)");
+  }
+  return devfreq_dir;
+}
+
+const char* SysStatsDataSource::ReadDevfreqCurFreq(
+    const std::string& deviceName) {
+  const char* devfreq_base_path = "/sys/class/devfreq";
+  const char* freq_file_name = "cur_freq";
+  char cur_freq_path[256];
+  snprintf(cur_freq_path, sizeof(cur_freq_path), "%s/%s/%s", devfreq_base_path,
+           deviceName.c_str(), freq_file_name);
+  base::ScopedFile fd = OpenReadOnly(cur_freq_path);
+  if (!fd && !devfreq_error_logged_) {
+    devfreq_error_logged_ = true;
+    PERFETTO_PLOG("Failed to open %s", cur_freq_path);
+    return "";
+  }
+  size_t rsize = ReadFile(&fd, cur_freq_path);
+  if (!rsize)
+    return "";
+  return static_cast<char*>(read_buf_.Get());
+}
+
 void SysStatsDataSource::ReadMeminfo(protos::pbzero::SysStats* sys_stats) {
   size_t rsize = ReadFile(&meminfo_fd_, "/proc/meminfo");
   if (!rsize)
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source.h b/src/traced/probes/sys_stats/sys_stats_data_source.h
index df47176..cf47770 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source.h
+++ b/src/traced/probes/sys_stats/sys_stats_data_source.h
@@ -64,6 +64,10 @@
   void set_ns_per_user_hz_for_testing(uint64_t ns) { ns_per_user_hz_ = ns; }
   uint32_t tick_for_testing() const { return tick_; }
 
+  // Virtual for testing
+  virtual base::ScopedDir OpenDevfreqDir();
+  virtual const char* ReadDevfreqCurFreq(const std::string& name);
+
  private:
   struct CStrCmp {
     bool operator()(const char* a, const char* b) const {
@@ -79,6 +83,7 @@
   void ReadMeminfo(protos::pbzero::SysStats* sys_stats);
   void ReadVmstat(protos::pbzero::SysStats* sys_stats);
   void ReadStat(protos::pbzero::SysStats* sys_stats);
+  void ReadDevfreq(protos::pbzero::SysStats* sys_stats);
   size_t ReadFile(base::ScopedFile*, const char* path);
 
   base::TaskRunner* const task_runner_;
@@ -97,6 +102,8 @@
   uint32_t vmstat_ticks_ = 0;
   uint32_t stat_ticks_ = 0;
   uint32_t stat_enabled_fields_ = 0;
+  uint32_t devfreq_ticks_ = 0;
+  bool devfreq_error_logged_ = false;
 
   base::WeakPtrFactory<SysStatsDataSource> weak_factory_;  // Keep last.
 };
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc b/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc
index 38a7414..b239ffa 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc
+++ b/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc
@@ -16,6 +16,7 @@
 
 #include <unistd.h>
 
+#include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/temp_file.h"
 #include "src/base/test/test_task_runner.h"
 #include "src/traced/probes/sys_stats/sys_stats_data_source.h"
@@ -186,6 +187,26 @@
 procs_blocked 0
 softirq 84611084 10220177 28299167 155083 3035679 6390543 66234 4396819 15604187 0 16443195)";
 
+const char kDevfreq1[] = "1000000";
+const char kDevfreq2[] = "20000000";
+
+class TestSysStatsDataSource : public SysStatsDataSource {
+ public:
+  TestSysStatsDataSource(base::TaskRunner* task_runner,
+                         TracingSessionID id,
+                         std::unique_ptr<TraceWriter> writer,
+                         const DataSourceConfig& config,
+                         OpenFunction open_fn)
+      : SysStatsDataSource(task_runner,
+                           id,
+                           std::move(writer),
+                           config,
+                           open_fn) {}
+
+  MOCK_METHOD0(OpenDevfreqDir, base::ScopedDir());
+  MOCK_METHOD1(ReadDevfreqCurFreq, const char*(const std::string& deviceName));
+};
+
 base::ScopedFile MockOpenReadOnly(const char* path) {
   base::TempFile tmp_ = base::TempFile::CreateUnlinked();
   if (!strcmp(path, "/proc/meminfo")) {
@@ -202,13 +223,14 @@
 
 class SysStatsDataSourceTest : public ::testing::Test {
  protected:
-  std::unique_ptr<SysStatsDataSource> GetSysStatsDataSource(
+  std::unique_ptr<TestSysStatsDataSource> GetSysStatsDataSource(
       const DataSourceConfig& cfg) {
     auto writer =
         std::unique_ptr<TraceWriterForTesting>(new TraceWriterForTesting());
     writer_raw_ = writer.get();
-    auto instance = std::unique_ptr<SysStatsDataSource>(new SysStatsDataSource(
-        &task_runner_, 0, std::move(writer), cfg, MockOpenReadOnly));
+    auto instance =
+        std::unique_ptr<TestSysStatsDataSource>(new TestSysStatsDataSource(
+            &task_runner_, 0, std::move(writer), cfg, MockOpenReadOnly));
     instance->set_ns_per_user_hz_for_testing(1000000000ull / 100);  // 100 Hz.
     instance->Start();
     return instance;
@@ -252,6 +274,7 @@
   const auto& sys_stats = packet.sys_stats();
   EXPECT_EQ(sys_stats.vmstat_size(), 0);
   EXPECT_EQ(sys_stats.cpu_stat_size(), 0);
+  EXPECT_EQ(sys_stats.devfreq_size(), 0);
 
   using KV = std::pair<int, uint64_t>;
   std::vector<KV> kvs;
@@ -280,6 +303,7 @@
   const auto& sys_stats = packet.sys_stats();
   EXPECT_EQ(sys_stats.vmstat_size(), 0);
   EXPECT_EQ(sys_stats.cpu_stat_size(), 0);
+  EXPECT_EQ(sys_stats.devfreq_size(), 0);
   EXPECT_GE(sys_stats.meminfo_size(), 10);
 }
 
@@ -302,6 +326,7 @@
   const auto& sys_stats = packet.sys_stats();
   EXPECT_EQ(sys_stats.meminfo_size(), 0);
   EXPECT_EQ(sys_stats.cpu_stat_size(), 0);
+  EXPECT_EQ(sys_stats.devfreq_size(), 0);
 
   using KV = std::pair<int, uint64_t>;
   std::vector<KV> kvs;
@@ -329,9 +354,70 @@
   const auto& sys_stats = packet.sys_stats();
   EXPECT_EQ(sys_stats.meminfo_size(), 0);
   EXPECT_EQ(sys_stats.cpu_stat_size(), 0);
+  EXPECT_EQ(sys_stats.devfreq_size(), 0);
   EXPECT_GE(sys_stats.vmstat_size(), 10);
 }
 
+TEST_F(SysStatsDataSourceTest, DevfreqAll) {
+  DataSourceConfig config;
+  protos::gen::SysStatsConfig sys_cfg;
+  sys_cfg.set_devfreq_period_ms(10);
+  config.set_sys_stats_config_raw(sys_cfg.SerializeAsString());
+  auto data_source = GetSysStatsDataSource(config);
+
+  // Create dirs and symlinks, but only read the symlinks.
+  std::vector<std::string> dirs_to_delete;
+  std::vector<std::string> symlinks_to_delete;
+  auto make_devfreq_paths = [&symlinks_to_delete, &dirs_to_delete](
+                                base::TempDir& temp_dir, base::TempDir& sym_dir,
+                                const char* name) {
+    char path[256];
+    sprintf(path, "%s/%s", temp_dir.path().c_str(), name);
+    dirs_to_delete.push_back(path);
+    mkdir(path, 0755);
+    char sym_path[256];
+    sprintf(sym_path, "%s/%s", sym_dir.path().c_str(), name);
+    symlinks_to_delete.push_back(sym_path);
+    symlink(path, sym_path);
+  };
+  auto fake_devfreq = base::TempDir::Create();
+  auto fake_devfreq_symdir = base::TempDir::Create();
+  static const char* const devfreq_names[] = {"10010.devfreq_device_a",
+                                              "10020.devfreq_device_b"};
+  for (auto dev : devfreq_names) {
+    make_devfreq_paths(fake_devfreq, fake_devfreq_symdir, dev);
+  }
+
+  EXPECT_CALL(*data_source, OpenDevfreqDir())
+      .WillRepeatedly(Invoke([&fake_devfreq_symdir] {
+        return base::ScopedDir(opendir(fake_devfreq_symdir.path().c_str()));
+      }));
+  EXPECT_CALL(*data_source, ReadDevfreqCurFreq("10010.devfreq_device_a"))
+      .WillRepeatedly(Return(kDevfreq1));
+  EXPECT_CALL(*data_source, ReadDevfreqCurFreq("10020.devfreq_device_b"))
+      .WillRepeatedly(Return(kDevfreq2));
+
+  WaitTick(data_source.get());
+
+  protos::gen::TracePacket packet = writer_raw_->GetOnlyTracePacket();
+  ASSERT_TRUE(packet.has_sys_stats());
+  const auto& sys_stats = packet.sys_stats();
+  EXPECT_EQ(sys_stats.meminfo_size(), 0);
+  EXPECT_EQ(sys_stats.cpu_stat_size(), 0);
+
+  using KV = std::pair<std::string, uint64_t>;
+  std::vector<KV> kvs;
+  for (const auto& kv : sys_stats.devfreq())
+    kvs.push_back({kv.key(), kv.value()});
+  EXPECT_THAT(kvs,
+              UnorderedElementsAre(KV{"10010.devfreq_device_a", 1000000},
+                                   KV{"10020.devfreq_device_b", 20000000}));
+  for (const std::string& path : dirs_to_delete)
+    base::Rmdir(path);
+  for (const std::string& path : symlinks_to_delete)
+    remove(path.c_str());
+}
+
 TEST_F(SysStatsDataSourceTest, StatAll) {
   DataSourceConfig config;
   protos::gen::SysStatsConfig sys_cfg;
diff --git a/src/traced/service/service.cc b/src/traced/service/service.cc
index 4881b75..f0a6c4a 100644
--- a/src/traced/service/service.cc
+++ b/src/traced/service/service.cc
@@ -17,9 +17,11 @@
 #include <stdio.h>
 #include <algorithm>
 
+#include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/getopt.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/unix_task_runner.h"
+#include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/base/version.h"
 #include "perfetto/ext/base/watchdog.h"
 #include "perfetto/ext/traced/traced.h"
@@ -74,9 +76,10 @@
 #endif  // defined(PERFETTO_SET_SOCKET_PERMISSIONS)
 
 void PrintUsage(const char* prog_name) {
-  PERFETTO_ELOG(R"(
+  fprintf(stderr, R"(
 Usage: %s [option] ...
 Options and arguments
+    --background : Exits immediately and continues running in the background
     --version : print the version number and exit.
     --set-socket-permissions <permissions> : sets group ownership and permission
         mode bits of the producer and consumer sockets.
@@ -85,12 +88,14 @@
         <prod_mode> is the mode bits (e.g. 0660) for chmod the produce socket,
         <cons_group> is the group name for chgrp the consumer socket, and
         <cons_mode> is the mode bits (e.g. 0660) for chmod the consumer socket.
-Example: %s --set-socket-permissions traced-producer:0660:traced-consumer:0660
+
+Example:
+    %s --set-socket-permissions traced-producer:0660:traced-consumer:0660
     starts the service and sets the group ownership of the producer and consumer
     sockets to "traced-producer" and "traced-consumer", respectively. Both
-    producer and consumer sockets are chmod with 0660  (rw-rw----) mode bits.
+    producer and consumer sockets are chmod with 0660 (rw-rw----) mode bits.
 )",
-                prog_name, prog_name);
+          prog_name, prog_name);
 }
 }  // namespace
 
@@ -98,9 +103,13 @@
   enum LongOption {
     OPT_VERSION = 1000,
     OPT_SET_SOCKET_PERMISSIONS = 1001,
+    OPT_BACKGROUND,
   };
 
+  bool background = false;
+
   static const option long_options[] = {
+      {"background", no_argument, nullptr, OPT_BACKGROUND},
       {"version", no_argument, nullptr, OPT_VERSION},
       {"set-socket-permissions", required_argument, nullptr,
        OPT_SET_SOCKET_PERMISSIONS},
@@ -114,6 +123,9 @@
     if (option == -1)
       break;
     switch (option) {
+      case OPT_BACKGROUND:
+        background = true;
+        break;
       case OPT_VERSION:
         printf("%s\n", base::GetVersionString());
         return 0;
@@ -136,6 +148,10 @@
     }
   }
 
+  if (background) {
+    base::Daemonize();
+  }
+
   base::UnixTaskRunner task_runner;
   std::unique_ptr<ServiceIPCHost> svc;
   svc = ServiceIPCHost::CreateInstance(&task_runner);
@@ -190,6 +206,16 @@
                         base::kWatchdogDefaultCpuWindow);
   watchdog->Start();
 
+  // If the TRACED_NOTIFY_FD env var is set, write 1 and close the FD. This is
+  // so tools can synchronize with the point where the IPC socket has been
+  // opened, without having to poll. This is used for //src/tracebox.
+  const char* env_notif = getenv("TRACED_NOTIFY_FD");
+  if (env_notif) {
+    int notif_fd = atoi(env_notif);
+    PERFETTO_CHECK(base::WriteAll(notif_fd, "1", 1) == 1);
+    PERFETTO_CHECK(base::CloseFile(notif_fd) == 0);
+  }
+
   PERFETTO_ILOG("Started traced, listening on %s %s", GetProducerSocket(),
                 GetConsumerSocket());
   task_runner.Run();
diff --git a/src/tracing/BUILD.gn b/src/tracing/BUILD.gn
index 2615660..ebf198c 100644
--- a/src/tracing/BUILD.gn
+++ b/src/tracing/BUILD.gn
@@ -50,14 +50,17 @@
   }
 }
 
-# Separate target because the embedder might not want this (e.g. on Windows).
+# Separate target because the embedder might not want this.
 source_set("platform_impl") {
   deps = [
     "../../gn:default_deps",
     "../../include/perfetto/tracing",
     "../base",
   ]
-  sources = [ "platform_posix.cc" ]
+  sources = [
+    "platform_posix.cc",
+    "platform_windows.cc",
+  ]
 }
 
 # Fake platform that allows buiding the client lib on all OSes. You can only use
@@ -112,6 +115,7 @@
     "internal/tracing_muxer_impl.cc",
     "internal/tracing_muxer_impl.h",
     "internal/track_event_internal.cc",
+    "internal/track_event_interned_fields.cc",
     "platform.cc",
     "traced_value.cc",
     "tracing.cc",
@@ -155,7 +159,11 @@
       ":client_api_without_backends",
       ":platform_impl",
     ]
-    sources += [ "traced_value_unittest.cc" ]
+
+    sources += [
+      "traced_proto_unittest.cc",
+      "traced_value_unittest.cc",
+    ]
   }
 }
 
diff --git a/src/tracing/console_interceptor.cc b/src/tracing/console_interceptor.cc
index 8dda4ff..99ce956 100644
--- a/src/tracing/console_interceptor.cc
+++ b/src/tracing/console_interceptor.cc
@@ -234,7 +234,8 @@
 
   // Print annotations.
   if (event.track_event.has_debug_annotations()) {
-    PrintAnnotations(context_, event.track_event, slice_color, highlight_color);
+    PrintDebugAnnotations(context_, event.track_event, slice_color,
+                          highlight_color);
   }
 
   // TODO(skyostil): Print typed arguments.
@@ -316,7 +317,7 @@
     va_start(args, format);
     written = vsnprintf(&tls.message_buffer[tls.buffer_pos],
                         static_cast<size_t>(remaining), format, args);
-    PERFETTO_DCHECK(written > 0);
+    PERFETTO_DCHECK(written >= 0);
     va_end(args);
   }
 
@@ -367,59 +368,27 @@
 }
 
 // static
-void ConsoleInterceptor::PrintAnnotations(
+void ConsoleInterceptor::PrintDebugAnnotations(
     InterceptorContext& context,
     const protos::pbzero::TrackEvent_Decoder& track_event,
     const ConsoleColor& slice_color,
     const ConsoleColor& highlight_color) {
-  auto& tls = context.GetThreadLocalState();
-
   SetColor(context, slice_color);
   Printf(context, "(");
 
   bool is_first = true;
   for (auto it = track_event.debug_annotations(); it; it++) {
     perfetto::protos::pbzero::DebugAnnotation::Decoder annotation(*it);
-    protozero::ConstChars name{};
-    if (annotation.name_iid()) {
-      name.data =
-          tls.sequence_state.debug_annotation_names[annotation.name_iid()]
-              .data();
-      name.size =
-          tls.sequence_state.debug_annotation_names[annotation.name_iid()]
-              .size();
-    } else if (annotation.has_name()) {
-      name.data = annotation.name().data;
-      name.size = annotation.name().size;
-    }
     SetColor(context, slice_color);
     if (!is_first)
       Printf(context, ", ");
-    Printf(context, "%.*s:", static_cast<int>(name.size), name.data);
+
+    PrintDebugAnnotationName(context, annotation);
+    Printf(context, ":");
+
     SetColor(context, highlight_color);
-    if (annotation.has_bool_value()) {
-      Printf(context, "%s", annotation.bool_value() ? "true" : "false");
-    } else if (annotation.has_uint_value()) {
-      Printf(context, "%" PRIu64, annotation.uint_value());
-    } else if (annotation.has_int_value()) {
-      Printf(context, "%" PRId64, annotation.int_value());
-    } else if (annotation.has_double_value()) {
-      Printf(context, "%f", annotation.double_value());
-    } else if (annotation.has_string_value()) {
-      Printf(context, "%.*s", static_cast<int>(annotation.string_value().size),
-             annotation.string_value().data);
-    } else if (annotation.has_pointer_value()) {
-      Printf(context, "%p",
-             reinterpret_cast<void*>(annotation.pointer_value()));
-    } else if (annotation.has_nested_value()) {
-      perfetto::protos::pbzero::DebugAnnotation::NestedValue::Decoder value(
-          annotation.nested_value());
-      PrintNestedValue(context, value);
-    } else if (annotation.has_legacy_json_value()) {
-      Printf(context, "%.*s",
-             static_cast<int>(annotation.legacy_json_value().size),
-             annotation.legacy_json_value().data);
-    }
+    PrintDebugAnnotationValue(context, annotation);
+
     is_first = false;
   }
   SetColor(context, slice_color);
@@ -427,53 +396,70 @@
 }
 
 // static
-void ConsoleInterceptor::PrintNestedValue(
+void ConsoleInterceptor::PrintDebugAnnotationName(
     InterceptorContext& context,
-    const perfetto::protos::pbzero::DebugAnnotation::NestedValue::Decoder&
-        value) {
-  if (value.nested_type() ==
-      protos::pbzero::DebugAnnotation::NestedValue::DICT) {
+    const perfetto::protos::pbzero::DebugAnnotation::Decoder& annotation) {
+  auto& tls = context.GetThreadLocalState();
+  protozero::ConstChars name{};
+  if (annotation.name_iid()) {
+    name.data =
+        tls.sequence_state.debug_annotation_names[annotation.name_iid()].data();
+    name.size =
+        tls.sequence_state.debug_annotation_names[annotation.name_iid()].size();
+  } else if (annotation.has_name()) {
+    name.data = annotation.name().data;
+    name.size = annotation.name().size;
+  }
+  Printf(context, "%.*s", static_cast<int>(name.size), name.data);
+}
+
+// static
+void ConsoleInterceptor::PrintDebugAnnotationValue(
+    InterceptorContext& context,
+    const perfetto::protos::pbzero::DebugAnnotation::Decoder& annotation) {
+  if (annotation.has_bool_value()) {
+    Printf(context, "%s", annotation.bool_value() ? "true" : "false");
+  } else if (annotation.has_uint_value()) {
+    Printf(context, "%" PRIu64, annotation.uint_value());
+  } else if (annotation.has_int_value()) {
+    Printf(context, "%" PRId64, annotation.int_value());
+  } else if (annotation.has_double_value()) {
+    Printf(context, "%f", annotation.double_value());
+  } else if (annotation.has_string_value()) {
+    Printf(context, "%.*s", static_cast<int>(annotation.string_value().size),
+           annotation.string_value().data);
+  } else if (annotation.has_pointer_value()) {
+    Printf(context, "%p", reinterpret_cast<void*>(annotation.pointer_value()));
+  } else if (annotation.has_legacy_json_value()) {
+    Printf(context, "%.*s",
+           static_cast<int>(annotation.legacy_json_value().size),
+           annotation.legacy_json_value().data);
+  } else if (annotation.has_dict_entries()) {
     Printf(context, "{");
-    auto key_it = value.dict_keys();
-    auto value_it = value.dict_values();
     bool is_first = true;
-    while (key_it && value_it) {
-      auto key = *key_it++;
+    for (auto it = annotation.dict_entries(); it; ++it) {
       if (!is_first)
         Printf(context, ", ");
-      Printf(context, "%.*s:", static_cast<int>(key.size), key.data);
-      perfetto::protos::pbzero::DebugAnnotation::NestedValue::Decoder
-          dict_value(*value_it++);
-      PrintNestedValue(context, dict_value);
+      perfetto::protos::pbzero::DebugAnnotation::Decoder key_value(*it);
+      PrintDebugAnnotationName(context, key_value);
+      Printf(context, ":");
+      PrintDebugAnnotationValue(context, key_value);
       is_first = false;
     }
     Printf(context, "}");
-    return;
-  }
-  if (value.nested_type() ==
-      protos::pbzero::DebugAnnotation::NestedValue::ARRAY) {
+  } else if (annotation.has_array_values()) {
     Printf(context, "[");
     bool is_first = true;
-    for (auto it = value.array_values(); it; it++) {
+    for (auto it = annotation.array_values(); it; ++it) {
       if (!is_first)
         Printf(context, ", ");
-      perfetto::protos::pbzero::DebugAnnotation::NestedValue::Decoder
-          array_value(*it);
-      PrintNestedValue(context, array_value);
+      perfetto::protos::pbzero::DebugAnnotation::Decoder key_value(*it);
+      PrintDebugAnnotationValue(context, key_value);
       is_first = false;
     }
     Printf(context, "]");
-    return;
-  }
-  if (value.has_int_value()) {
-    Printf(context, "%" PRId64, value.int_value());
-  } else if (value.has_double_value()) {
-    Printf(context, "%f", value.double_value());
-  } else if (value.has_bool_value()) {
-    Printf(context, "%s", value.bool_value() ? "true" : "false");
-  } else if (value.has_string_value()) {
-    Printf(context, "%.*s", static_cast<int>(value.string_value().size),
-           value.string_value().data);
+  } else {
+    Printf(context, "{}");
   }
 }
 
diff --git a/src/tracing/core/BUILD.gn b/src/tracing/core/BUILD.gn
index 1c042d3..c81c7163 100644
--- a/src/tracing/core/BUILD.gn
+++ b/src/tracing/core/BUILD.gn
@@ -61,6 +61,7 @@
     "../../../protos/perfetto/trace/perfetto:zero",  # For MetatraceWriter.
     "../../android_stats",
     "../../base",
+    "../../protozero/filtering:message_filter",
   ]
   sources = [
     "metatrace_writer.cc",
@@ -74,8 +75,8 @@
   ]
   if (is_android && perfetto_build_with_android) {
     deps += [
-      "../../android_internal:lazy_library_loader",
       "../../android_internal:headers",
+      "../../android_internal:lazy_library_loader",
     ]
   }
 }
diff --git a/src/tracing/core/tracing_service_impl.cc b/src/tracing/core/tracing_service_impl.cc
index ccfee0b..1a5b43d 100644
--- a/src/tracing/core/tracing_service_impl.cc
+++ b/src/tracing/core/tracing_service_impl.cc
@@ -17,6 +17,7 @@
 #include "src/tracing/core/tracing_service_impl.h"
 
 #include "perfetto/base/build_config.h"
+#include "perfetto/tracing/core/forward_decls.h"
 
 #include <errno.h>
 #include <inttypes.h>
@@ -37,8 +38,8 @@
 #if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
 #include "src/android_internal/lazy_library_loader.h"    // nogncheck
 #include "src/android_internal/tracing_service_proxy.h"  // nogncheck
-#endif // PERFETTO_ANDROID_BUILD
-#endif // PERFETTO_OS_ANDROID
+#endif  // PERFETTO_ANDROID_BUILD
+#endif  // PERFETTO_OS_ANDROID
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
     PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
@@ -56,6 +57,7 @@
 #include "perfetto/ext/base/metatrace.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/utils.h"
+#include "perfetto/ext/base/version.h"
 #include "perfetto/ext/base/watchdog.h"
 #include "perfetto/ext/tracing/core/basic_types.h"
 #include "perfetto/ext/tracing/core/consumer.h"
@@ -71,6 +73,7 @@
 #include "perfetto/tracing/core/tracing_service_capabilities.h"
 #include "perfetto/tracing/core/tracing_service_state.h"
 #include "src/android_stats/statsd_logging_helper.h"
+#include "src/protozero/filtering/message_filter.h"
 #include "src/tracing/core/packet_stream_validator.h"
 #include "src/tracing/core/shared_memory_arbiter_impl.h"
 #include "src/tracing/core/trace_buffer.h"
@@ -330,7 +333,8 @@
                                     bool in_process,
                                     ProducerSMBScrapingMode smb_scraping_mode,
                                     size_t shared_memory_page_size_hint_bytes,
-                                    std::unique_ptr<SharedMemory> shm) {
+                                    std::unique_ptr<SharedMemory> shm,
+                                    const std::string& sdk_version) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
 
   if (lockdown_mode_ && uid != base::GetCurrentUserId()) {
@@ -359,8 +363,8 @@
   }
 
   std::unique_ptr<ProducerEndpointImpl> endpoint(new ProducerEndpointImpl(
-      id, uid, this, task_runner_, producer, producer_name, in_process,
-      smb_scraping_enabled));
+      id, uid, this, task_runner_, producer, producer_name, sdk_version,
+      in_process, smb_scraping_enabled));
   auto it_and_inserted = producers_.emplace(id, endpoint.get());
   PERFETTO_DCHECK(it_and_inserted.second);
   endpoint->shmem_size_hint_bytes_ = shared_memory_size_hint_bytes;
@@ -440,7 +444,8 @@
 std::unique_ptr<TracingService::ConsumerEndpoint>
 TracingServiceImpl::ConnectConsumer(Consumer* consumer, uid_t uid) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
-  PERFETTO_DLOG("Consumer %p connected", reinterpret_cast<void*>(consumer));
+  PERFETTO_DLOG("Consumer %p connected from UID %" PRIu64,
+                reinterpret_cast<void*>(consumer), static_cast<uint64_t>(uid));
   std::unique_ptr<ConsumerEndpointImpl> endpoint(
       new ConsumerEndpointImpl(this, task_runner_, consumer, uid));
   auto it_and_inserted = consumers_.emplace(endpoint.get());
@@ -642,6 +647,21 @@
     return PERFETTO_SVC_ERR("Too many buffers configured (%d)",
                             cfg.buffers_size());
   }
+  // Check that the config specifies all buffers for its data sources. This
+  // is also checked in SetupDataSource, but it is simpler to return a proper
+  // error to the consumer from here (and there will be less state to undo).
+  for (const TraceConfig::DataSource& cfg_data_source : cfg.data_sources()) {
+    size_t num_buffers = static_cast<size_t>(cfg.buffers_size());
+    size_t target_buffer = cfg_data_source.config().target_buffer();
+    if (target_buffer >= num_buffers) {
+      MaybeLogUploadEvent(
+          cfg, PerfettoStatsdAtom::kTracedEnableTracingOobTargetBuffer);
+      return PERFETTO_SVC_ERR(
+          "Data source \"%s\" specified an out of bounds target_buffer (%zu >= "
+          "%zu)",
+          cfg_data_source.config().name().c_str(), target_buffer, num_buffers);
+    }
+  }
 
   if (!cfg.unique_session_name().empty()) {
     const std::string& name = cfg.unique_session_name();
@@ -716,11 +736,45 @@
                             tracing_sessions_.size());
   }
 
+  // If the trace config provides a filter bytecode, setup the filter now.
+  // If the filter loading fails, abort the tracing session rather than running
+  // unfiltered.
+  std::unique_ptr<protozero::MessageFilter> trace_filter;
+  if (cfg.has_trace_filter()) {
+    const auto& filt = cfg.trace_filter();
+    const std::string& bytecode = filt.bytecode();
+    trace_filter.reset(new protozero::MessageFilter());
+    if (!trace_filter->LoadFilterBytecode(bytecode.data(), bytecode.size())) {
+      MaybeLogUploadEvent(
+          cfg, PerfettoStatsdAtom::kTracedEnableTracingInvalidFilter);
+      return PERFETTO_SVC_ERR("Trace filter bytecode invalid, aborting");
+    }
+    // The filter is created using perfetto.protos.Trace as root message
+    // (because that makes it possible to play around with the `proto_filter`
+    // tool on actual traces). Here in the service, however, we deal with
+    // perfetto.protos.TracePacket(s), which are one level down (Trace.packet).
+    // The IPC client (or the write_into_filte logic in here) are responsible
+    // for pre-pending the packet preamble (See GetProtoPreamble() calls), but
+    // the preamble is not there at ReadBuffer time. Hence we change the root of
+    // the filtering to start at the Trace.packet level.
+    uint32_t packet_field_id = TracePacket::kPacketFieldNumber;
+    if (!trace_filter->SetFilterRoot(&packet_field_id, 1)) {
+      MaybeLogUploadEvent(
+          cfg, PerfettoStatsdAtom::kTracedEnableTracingInvalidFilter);
+      return PERFETTO_SVC_ERR("Failed to set filter root.");
+    }
+  }
+
   const TracingSessionID tsid = ++last_tracing_session_id_;
   TracingSession* tracing_session =
-      &tracing_sessions_.emplace(tsid, TracingSession(tsid, consumer, cfg))
+      &tracing_sessions_
+           .emplace(std::piecewise_construct, std::forward_as_tuple(tsid),
+                    std::forward_as_tuple(tsid, consumer, cfg, task_runner_))
            .first->second;
 
+  if (trace_filter)
+    tracing_session->trace_filter = std::move(trace_filter);
+
   if (cfg.write_into_file()) {
     if (!fd ^ !cfg.output_path().empty()) {
       tracing_sessions_.erase(tsid);
@@ -996,6 +1050,8 @@
 
 base::Status TracingServiceImpl::StartTracing(TracingSessionID tsid) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
+
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   TracingSession* tracing_session = GetTracingSession(tsid);
   if (!tracing_session) {
     return PERFETTO_SVC_ERR(
@@ -1041,12 +1097,24 @@
   //      snapshots that showed significant new drift between different clocks.
   //      The latter clock snapshots are sampled periodically and at lifecycle
   //      events.
-  PeriodicSnapshotTask(tracing_session);
+  base::PeriodicTask::Args snapshot_task_args;
+  snapshot_task_args.start_first_task_immediately = true;
+  snapshot_task_args.use_suspend_aware_timer =
+      tracing_session->config.builtin_data_sources()
+          .prefer_suspend_clock_for_snapshot();
+  snapshot_task_args.task = [weak_this, tsid] {
+    if (weak_this)
+      weak_this->PeriodicSnapshotTask(tsid);
+  };
+  snapshot_task_args.period_ms =
+      tracing_session->config.builtin_data_sources().snapshot_interval_ms();
+  if (!snapshot_task_args.period_ms)
+    snapshot_task_args.period_ms = kDefaultSnapshotsIntervalMs;
+  tracing_session->snapshot_periodic_task.Start(snapshot_task_args);
 
   // Trigger delayed task if the trace is time limited.
   const uint32_t trace_duration_ms = tracing_session->config.duration_ms();
   if (trace_duration_ms > 0) {
-    auto weak_this = weak_ptr_factory_.GetWeakPtr();
     task_runner_->PostDelayedTask(
         [weak_this, tsid] {
           // Skip entirely the flush if the trace session doesn't exist anymore.
@@ -1072,7 +1140,6 @@
 
   // Start the periodic drain tasks if we should to save the trace into a file.
   if (tracing_session->config.write_into_file()) {
-    auto weak_this = weak_ptr_factory_.GetWeakPtr();
     task_runner_->PostDelayedTask(
         [weak_this, tsid] {
           if (weak_this)
@@ -1188,6 +1255,11 @@
                            disable_immediately);
   }
 
+  // If the periodic task is running, we can stop the periodic snapshot timer
+  // here instead of waiting until FreeBuffers to prevent useless snapshots
+  // which won't be read.
+  tracing_session->snapshot_periodic_task.Reset();
+
   // Either this request is flagged with |disable_immediately| or there are no
   // data sources that are requesting a final handshake. In both cases just mark
   // the session as disabled immediately, notify the consumer and flush the
@@ -2051,6 +2123,48 @@
     total_slices += packets[i].slices().size();
   }
 
+  // +-------------------------------------------------------------------------+
+  // | NO MORE CHANGES TO |packets| AFTER THIS POINT.                          |
+  // +-------------------------------------------------------------------------+
+
+  // If the tracing session specified a filter, run all packets through the
+  // filter and replace them with the filter results.
+  // The process below mantains the cardinality of input packets. Even if an
+  // entire packet is filtered out, we emit a zero-sized TracePacket proto. That
+  // makes debugging and reasoning about the trace stats easier.
+  // This place swaps the contents of each |packets| entry in place.
+  if (tracing_session->trace_filter) {
+    auto& trace_filter = *tracing_session->trace_filter;
+    // The filter root shoud be reset from protos.Trace to protos.TracePacket
+    // by the earlier call to SetFilterRoot() in EnableTracing().
+    PERFETTO_DCHECK(trace_filter.root_msg_index() != 0);
+    std::vector<protozero::MessageFilter::InputSlice> filter_input;
+    for (auto it = packets.begin(); it != packets.end(); ++it) {
+      const auto& packet_slices = it->slices();
+      filter_input.clear();
+      filter_input.resize(packet_slices.size());
+      ++tracing_session->filter_input_packets;
+      tracing_session->filter_input_bytes += it->size();
+      for (size_t i = 0; i < packet_slices.size(); ++i)
+        filter_input[i] = {packet_slices[i].start, packet_slices[i].size};
+      auto filtered_packet = trace_filter.FilterMessageFragments(
+          &filter_input[0], filter_input.size());
+
+      // Replace the packet in-place with the filtered one (unless failed).
+      *it = TracePacket();
+      if (filtered_packet.error) {
+        ++tracing_session->filter_errors;
+        PERFETTO_DLOG("Trace packet filtering failed @ packet %" PRIu64,
+                      tracing_session->filter_input_packets);
+        continue;
+      }
+      tracing_session->filter_output_bytes += filtered_packet.size;
+      it->AddSlice(Slice::TakeOwnership(std::move(filtered_packet.data),
+                                        filtered_packet.size));
+
+    }  // for (packet)
+  }    // if (trace_filter)
+
   // If the caller asked us to write into a file by setting
   // |write_into_file| == true in the trace config, drain the packets read
   // (if any) into the given file descriptor.
@@ -2174,8 +2288,9 @@
     buffers_.erase(buffer_id);
   }
   bool notify_traceur = tracing_session->config.notify_traceur();
-  bool is_long_trace = (tracing_session->config.write_into_file() &&
-          tracing_session->config.file_write_period_ms() < kMillisPerDay);
+  bool is_long_trace =
+      (tracing_session->config.write_into_file() &&
+       tracing_session->config.file_write_period_ms() < kMillisPerDay);
   bool seized_for_bugreport = tracing_session->seized_for_bugreport;
   tracing_sessions_.erase(tsid);
   tracing_session = nullptr;
@@ -2379,6 +2494,27 @@
   ds_config.set_stop_timeout_ms(tracing_session->data_source_stop_timeout_ms());
   ds_config.set_enable_extra_guardrails(
       tracing_session->config.enable_extra_guardrails());
+  if (tracing_session->consumer_uid == 1066 /* AID_STATSD */ &&
+      tracing_session->config.statsd_metadata().triggering_config_uid() !=
+          2000 /* AID_SHELL */
+      && tracing_session->config.statsd_metadata().triggering_config_uid() !=
+             0 /* AID_ROOT */) {
+    // StatsD can be triggered either by shell, root or an app that has DUMP and
+    // USAGE_STATS permission. When triggered by shell or root, we do not want
+    // to consider the trace a trusted system trace, as it was initiated by the
+    // user. Otherwise, it has to come from an app with DUMP and
+    // PACKAGE_USAGE_STATS, which has to be preinstalled and trusted by the
+    // system.
+    // Check for shell / root: https://bit.ly/3b7oZNi
+    // Check for DUMP or PACKAGE_USAGE_STATS: https://bit.ly/3ep0NrR
+    ds_config.set_session_initiator(
+        DataSourceConfig::SESSION_INITIATOR_TRUSTED_SYSTEM);
+  } else {
+    // Unset in case the consumer set it.
+    // We need to be able to trust this field.
+    ds_config.set_session_initiator(
+        DataSourceConfig::SESSION_INITIATOR_UNSPECIFIED);
+  }
   ds_config.set_tracing_session_id(tracing_session->id);
   BufferID global_id = tracing_session->buffers_index[relative_buffer_id];
   PERFETTO_DCHECK(global_id);
@@ -2641,31 +2777,15 @@
 #endif
 }
 
-void TracingServiceImpl::PeriodicSnapshotTask(TracingSession* tracing_session) {
+void TracingServiceImpl::PeriodicSnapshotTask(TracingSessionID tsid) {
+  auto* tracing_session = GetTracingSession(tsid);
+  if (!tracing_session)
+    return;
+  if (tracing_session->state != TracingSession::STARTED)
+    return;
   tracing_session->should_emit_sync_marker = true;
   tracing_session->should_emit_stats = true;
   MaybeSnapshotClocksIntoRingBuffer(tracing_session);
-
-  uint32_t interval_ms =
-      tracing_session->config.builtin_data_sources().snapshot_interval_ms();
-  if (!interval_ms)
-    interval_ms = kDefaultSnapshotsIntervalMs;
-
-  TracingSessionID tsid = tracing_session->id;
-  auto weak_this = weak_ptr_factory_.GetWeakPtr();
-  task_runner_->PostDelayedTask(
-      [weak_this, tsid] {
-        if (!weak_this)
-          return;
-        auto* tracing_session_ptr = weak_this->GetTracingSession(tsid);
-        if (!tracing_session_ptr)
-          return;
-        if (tracing_session_ptr->state != TracingSession::STARTED)
-          return;
-        weak_this->PeriodicSnapshotTask(tracing_session_ptr);
-      },
-      interval_ms -
-          static_cast<uint32_t>(base::GetWallTimeMs().count() % interval_ms));
 }
 
 void TracingServiceImpl::SnapshotLifecyleEvent(TracingSession* tracing_session,
@@ -2896,6 +3016,14 @@
   trace_stats.set_patches_discarded(patches_discarded_);
   trace_stats.set_invalid_packets(tracing_session->invalid_packets);
 
+  if (tracing_session->trace_filter) {
+    auto* filt_stats = trace_stats.mutable_filter_stats();
+    filt_stats->set_input_packets(tracing_session->filter_input_packets);
+    filt_stats->set_input_bytes(tracing_session->filter_input_bytes);
+    filt_stats->set_output_bytes(tracing_session->filter_output_bytes);
+    filt_stats->set_errors(tracing_session->filter_errors);
+  }
+
   for (BufferID buf_id : tracing_session->buffers_index) {
     TraceBuffer* buf = GetBufferByID(buf_id);
     if (!buf) {
@@ -2928,7 +3056,7 @@
   tracing_session->did_emit_system_info = true;
   protozero::HeapBuffered<protos::pbzero::TracePacket> packet;
   auto* info = packet->set_system_info();
-  base::ignore_result(info);  // For PERFETTO_OS_WIN.
+  info->set_tracing_service_version(base::GetVersionString());
 #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) && \
     !PERFETTO_BUILDFLAG(PERFETTO_OS_NACL)
   struct utsname uname_info;
@@ -3368,6 +3496,7 @@
   TracingServiceState svc_state;
 
   const auto& sessions = service_->tracing_sessions_;
+  svc_state.set_tracing_service_version(base::GetVersionString());
   svc_state.set_num_sessions(static_cast<int>(sessions.size()));
 
   int num_started = 0;
@@ -3379,6 +3508,7 @@
     auto* producer = svc_state.add_producers();
     producer->set_id(static_cast<int>(kv.first));
     producer->set_name(kv.second->name_);
+    producer->set_sdk_version(kv.second->sdk_version_);
     producer->set_uid(static_cast<int32_t>(producer->uid()));
   }
 
@@ -3436,6 +3566,7 @@
     base::TaskRunner* task_runner,
     Producer* producer,
     const std::string& producer_name,
+    const std::string& sdk_version,
     bool in_process,
     bool smb_scraping_enabled)
     : id_(id),
@@ -3444,6 +3575,7 @@
       task_runner_(task_runner),
       producer_(producer),
       name_(producer_name),
+      sdk_version_(sdk_version),
       in_process_(in_process),
       smb_scraping_enabled_(smb_scraping_enabled),
       weak_ptr_factory_(this) {}
@@ -3723,11 +3855,13 @@
 TracingServiceImpl::TracingSession::TracingSession(
     TracingSessionID session_id,
     ConsumerEndpointImpl* consumer,
-    const TraceConfig& new_config)
+    const TraceConfig& new_config,
+    base::TaskRunner* task_runner)
     : id(session_id),
       consumer_maybe_null(consumer),
       consumer_uid(consumer->uid_),
-      config(new_config) {
+      config(new_config),
+      snapshot_periodic_task(task_runner) {
   // all_data_sources_flushed is special because we store up to 64 events of
   // this type. Other events will go through the default case in
   // SnapshotLifecycleEvent() where they will be given a max history of 1.
diff --git a/src/tracing/core/tracing_service_impl.h b/src/tracing/core/tracing_service_impl.h
index 60c1e1b..401aaa1 100644
--- a/src/tracing/core/tracing_service_impl.h
+++ b/src/tracing/core/tracing_service_impl.h
@@ -32,6 +32,7 @@
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/circular_queue.h"
 #include "perfetto/ext/base/optional.h"
+#include "perfetto/ext/base/periodic_task.h"
 #include "perfetto/ext/base/weak_ptr.h"
 #include "perfetto/ext/tracing/core/basic_types.h"
 #include "perfetto/ext/tracing/core/commit_data_request.h"
@@ -46,6 +47,10 @@
 #include "src/android_stats/perfetto_atoms.h"
 #include "src/tracing/core/id_allocator.h"
 
+namespace protozero {
+class MessageFilter;
+}
+
 namespace perfetto {
 
 namespace base {
@@ -82,6 +87,7 @@
                          base::TaskRunner*,
                          Producer*,
                          const std::string& producer_name,
+                         const std::string& sdk_version,
                          bool in_process,
                          bool smb_scraping_enabled);
     ~ProducerEndpointImpl() override;
@@ -149,6 +155,7 @@
     size_t shmem_page_size_hint_bytes_ = 0;
     bool is_shmem_provided_by_producer_ = false;
     const std::string name_;
+    std::string sdk_version_;
     bool in_process_;
     bool smb_scraping_enabled_;
 
@@ -286,7 +293,8 @@
       ProducerSMBScrapingMode smb_scraping_mode =
           ProducerSMBScrapingMode::kDefault,
       size_t shared_memory_page_size_hint_bytes = 0,
-      std::unique_ptr<SharedMemory> shm = nullptr) override;
+      std::unique_ptr<SharedMemory> shm = nullptr,
+      const std::string& sdk_version = {}) override;
 
   std::unique_ptr<TracingService::ConsumerEndpoint> ConnectConsumer(
       Consumer*,
@@ -373,7 +381,12 @@
       DISABLING_WAITING_STOP_ACKS
     };
 
-    TracingSession(TracingSessionID, ConsumerEndpointImpl*, const TraceConfig&);
+    TracingSession(TracingSessionID,
+                   ConsumerEndpointImpl*,
+                   const TraceConfig&,
+                   base::TaskRunner*);
+    TracingSession(TracingSession&&) = delete;
+    TracingSession& operator=(TracingSession&&) = delete;
 
     size_t num_buffers() const { return buffers_index.size(); }
 
@@ -564,6 +577,17 @@
     // when the tracing session ends and the data has been saved into the file.
     std::function<void()> on_disable_callback_for_bugreport;
     bool seized_for_bugreport = false;
+
+    // Periodic task for snapshotting service events (e.g. clocks, sync markers
+    // etc)
+    base::PeriodicTask snapshot_periodic_task;
+
+    // When non-NULL the packets should be post-processed using the filter.
+    std::unique_ptr<protozero::MessageFilter> trace_filter;
+    uint64_t filter_input_packets = 0;
+    uint64_t filter_input_bytes = 0;
+    uint64_t filter_output_bytes = 0;
+    uint64_t filter_errors = 0;
   };
 
   TracingServiceImpl(const TracingServiceImpl&) = delete;
@@ -596,7 +620,7 @@
                               TracingSession*,
                               DataSourceInstance*,
                               bool disable_immediately);
-  void PeriodicSnapshotTask(TracingSession*);
+  void PeriodicSnapshotTask(TracingSessionID);
   void MaybeSnapshotClocksIntoRingBuffer(TracingSession*);
   bool SnapshotClocks(TracingSession::ClockSnapshotData*);
   void SnapshotLifecyleEvent(TracingSession*,
@@ -661,7 +685,7 @@
 
   bool smb_scraping_enabled_ = false;
   bool lockdown_mode_ = false;
-  uint32_t min_write_period_ms_ = 100;  // Overridable for testing.
+  uint32_t min_write_period_ms_ = 100;       // Overridable for testing.
   int64_t trigger_window_ns_ = kOneDayInNs;  // Overridable for testing.
 
   std::minstd_rand trigger_probability_rand_;
diff --git a/src/tracing/debug_annotation.cc b/src/tracing/debug_annotation.cc
index 8b3b0ff..add115a 100644
--- a/src/tracing/debug_annotation.cc
+++ b/src/tracing/debug_annotation.cc
@@ -24,12 +24,7 @@
 DebugAnnotation::~DebugAnnotation() = default;
 
 void DebugAnnotation::WriteIntoTracedValue(TracedValue context) const {
-  if (!context.root_context_) {
-    PERFETTO_DFATAL("DebugAnnotation should not be used in non-root contexts.");
-    std::move(context).WriteString("<not supported>");
-    return;
-  }
-  Add(context.root_context_);
+  Add(context.context_);
 }
 
 }  // namespace perfetto
diff --git a/src/tracing/internal/tracing_muxer_impl.cc b/src/tracing/internal/tracing_muxer_impl.cc
index 280a990..1fb5ae8 100644
--- a/src/tracing/internal/tracing_muxer_impl.cc
+++ b/src/tracing/internal/tracing_muxer_impl.cc
@@ -58,6 +58,58 @@
 
 namespace {
 
+// A task runner which prevents calls to DataSource::Trace() while an operation
+// is in progress. Used to guard against unexpected re-entrancy where the
+// user-provided task runner implementation tries to enter a trace point under
+// the hood.
+class NonReentrantTaskRunner : public base::TaskRunner {
+ public:
+  NonReentrantTaskRunner(TracingMuxer* muxer,
+                         std::unique_ptr<base::TaskRunner> task_runner)
+      : muxer_(muxer), task_runner_(std::move(task_runner)) {}
+
+  // base::TaskRunner implementation.
+  void PostTask(std::function<void()> task) override {
+    CallWithGuard([&] { task_runner_->PostTask(std::move(task)); });
+  }
+
+  void PostDelayedTask(std::function<void()> task, uint32_t delay_ms) override {
+    CallWithGuard(
+        [&] { task_runner_->PostDelayedTask(std::move(task), delay_ms); });
+  }
+
+  void AddFileDescriptorWatch(base::PlatformHandle fd,
+                              std::function<void()> callback) override {
+    CallWithGuard(
+        [&] { task_runner_->AddFileDescriptorWatch(fd, std::move(callback)); });
+  }
+
+  void RemoveFileDescriptorWatch(base::PlatformHandle fd) override {
+    CallWithGuard([&] { task_runner_->RemoveFileDescriptorWatch(fd); });
+  }
+
+  bool RunsTasksOnCurrentThread() const override {
+    bool result;
+    CallWithGuard([&] { result = task_runner_->RunsTasksOnCurrentThread(); });
+    return result;
+  }
+
+ private:
+  template <typename T>
+  void CallWithGuard(T lambda) const {
+    auto* root_tls = muxer_->GetOrCreateTracingTLS();
+    if (PERFETTO_UNLIKELY(root_tls->is_in_trace_point)) {
+      lambda();
+      return;
+    }
+    ScopedReentrancyAnnotator scoped_annotator(*root_tls);
+    lambda();
+  }
+
+  TracingMuxer* const muxer_;
+  std::unique_ptr<base::TaskRunner> task_runner_;
+};
+
 class StopArgsImpl : public DataSourceBase::StopArgs {
  public:
   std::function<void()> HandleStopAsynchronously() const override {
@@ -642,9 +694,11 @@
     : TracingMuxer(args.platform ? args.platform
                                  : Platform::GetDefaultPlatform()) {
   PERFETTO_DETACH_FROM_THREAD(thread_checker_);
+  instance_ = this;
 
   // Create the thread where muxer, producers and service will live.
-  task_runner_ = platform_->CreateTaskRunner({});
+  task_runner_.reset(
+      new NonReentrantTaskRunner(this, platform_->CreateTaskRunner({})));
 
   // Run the initializer on that thread.
   task_runner_->PostTask([this, args] { Initialize(args); });
@@ -1561,7 +1615,7 @@
 void TracingMuxerImpl::InitializeInstance(const TracingInitArgs& args) {
   if (instance_ != TracingMuxerFake::Get())
     PERFETTO_FATAL("Tracing already initialized");
-  instance_ = new TracingMuxerImpl(args);
+  new TracingMuxerImpl(args);
 }
 
 TracingMuxer::~TracingMuxer() = default;
diff --git a/src/tracing/internal/track_event_internal.cc b/src/tracing/internal/track_event_internal.cc
index 57e92b3..5e4a154 100644
--- a/src/tracing/internal/track_event_internal.cc
+++ b/src/tracing/internal/track_event_internal.cc
@@ -20,6 +20,7 @@
 #include "perfetto/base/thread_utils.h"
 #include "perfetto/base/time.h"
 #include "perfetto/tracing/core/data_source_config.h"
+#include "perfetto/tracing/internal/track_event_interned_fields.h"
 #include "perfetto/tracing/track_event.h"
 #include "perfetto/tracing/track_event_category_registry.h"
 #include "perfetto/tracing/track_event_interned_data_index.h"
@@ -61,53 +62,6 @@
   }
 }
 
-struct InternedEventCategory
-    : public TrackEventInternedDataIndex<
-          InternedEventCategory,
-          perfetto::protos::pbzero::InternedData::kEventCategoriesFieldNumber,
-          const char*,
-          SmallInternedDataTraits> {
-  static void Add(protos::pbzero::InternedData* interned_data,
-                  size_t iid,
-                  const char* value,
-                  size_t length) {
-    auto category = interned_data->add_event_categories();
-    category->set_iid(iid);
-    category->set_name(value, length);
-  }
-};
-
-struct InternedEventName
-    : public TrackEventInternedDataIndex<
-          InternedEventName,
-          perfetto::protos::pbzero::InternedData::kEventNamesFieldNumber,
-          const char*,
-          SmallInternedDataTraits> {
-  static void Add(protos::pbzero::InternedData* interned_data,
-                  size_t iid,
-                  const char* value) {
-    auto name = interned_data->add_event_names();
-    name->set_iid(iid);
-    name->set_name(value);
-  }
-};
-
-struct InternedDebugAnnotationName
-    : public TrackEventInternedDataIndex<
-          InternedDebugAnnotationName,
-          perfetto::protos::pbzero::InternedData::
-              kDebugAnnotationNamesFieldNumber,
-          const char*,
-          SmallInternedDataTraits> {
-  static void Add(protos::pbzero::InternedData* interned_data,
-                  size_t iid,
-                  const char* value) {
-    auto name = interned_data->add_debug_annotation_names();
-    name->set_iid(iid);
-    name->set_name(value);
-  }
-};
-
 enum class MatchType { kExact, kPattern };
 
 bool NameMatchesPattern(const std::string& pattern,
@@ -141,6 +95,9 @@
 const Track TrackEventInternal::kDefaultTrack{};
 
 // static
+std::atomic<int> TrackEventInternal::session_count_{};
+
+// static
 bool TrackEventInternal::Initialize(
     const TrackEventCategoryRegistry& registry,
     bool (*register_data_source)(const DataSourceDescriptor&)) {
@@ -218,6 +175,7 @@
 
 // static
 void TrackEventInternal::OnStart(const DataSourceBase::StartArgs& args) {
+  session_count_.fetch_add(1);
   ForEachObserver([&](TrackEventSessionObserver*& o) {
     if (o)
       o->OnStart(args);
@@ -265,6 +223,14 @@
         }
         break;
       }
+      // No match? Must be a dynamic category.
+      DynamicCategory dyn_category(std::string(member_name, name_size));
+      Category ref_category{Category::FromDynamicCategory(dyn_category)};
+      if (IsCategoryEnabled(registry, config, ref_category)) {
+        result = true;
+        // Break ForEachGroupMember() loop.
+        return false;
+      }
       // No match found => keep iterating.
       return true;
     });
@@ -337,8 +303,13 @@
 }
 
 // static
+int TrackEventInternal::GetSessionCount() {
+  return session_count_.load();
+}
+
+// static
 void TrackEventInternal::ResetIncrementalState(TraceWriterBase* trace_writer,
-                                               uint64_t timestamp) {
+                                               TraceTimestamp timestamp) {
   auto default_track = ThreadTrack::Current();
   {
     // Mark any incremental state before this point invalid. Also set up
@@ -366,14 +337,18 @@
 // static
 protozero::MessageHandle<protos::pbzero::TracePacket>
 TrackEventInternal::NewTracePacket(TraceWriterBase* trace_writer,
-                                   uint64_t timestamp,
+                                   TraceTimestamp timestamp,
                                    uint32_t seq_flags) {
   auto packet = trace_writer->NewTracePacket();
-  packet->set_timestamp(timestamp);
-  // TODO(skyostil): Stop emitting this for every event once the trace
-  // processor understands trace packet defaults.
-  if (GetClockId() != protos::pbzero::BUILTIN_CLOCK_BOOTTIME)
+  packet->set_timestamp(timestamp.nanoseconds);
+  if (timestamp.clock_id != GetClockId()) {
+    packet->set_timestamp_clock_id(static_cast<uint32_t>(timestamp.clock_id));
+  } else if (GetClockId() != protos::pbzero::BUILTIN_CLOCK_BOOTTIME) {
+    // TODO(skyostil): Stop emitting the clock id for the default trace clock
+    // for every event once the trace processor understands trace packet
+    // defaults.
     packet->set_timestamp_clock_id(GetClockId());
+  }
   packet->set_sequence_flags(seq_flags);
   return packet;
 }
@@ -385,7 +360,7 @@
     const Category* category,
     const char* name,
     perfetto::protos::pbzero::TrackEvent::Type type,
-    uint64_t timestamp) {
+    TraceTimestamp timestamp) {
   PERFETTO_DCHECK(g_main_thread);
   PERFETTO_DCHECK(!incr_state->was_cleared);
 
@@ -409,7 +384,7 @@
           return true;
         });
   }
-  if (name) {
+  if (name && type != protos::pbzero::TrackEvent::TYPE_SLICE_END) {
     size_t name_iid = InternedEventName::Get(&ctx, name);
     track_event->set_name_iid(name_iid);
   }
diff --git a/src/tracing/internal/track_event_interned_fields.cc b/src/tracing/internal/track_event_interned_fields.cc
new file mode 100644
index 0000000..804aeb4
--- /dev/null
+++ b/src/tracing/internal/track_event_interned_fields.cc
@@ -0,0 +1,58 @@
+/*
+ * 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/tracing/internal/track_event_interned_fields.h"
+
+namespace perfetto {
+namespace internal {
+
+InternedEventCategory::~InternedEventCategory() = default;
+
+// static
+void InternedEventCategory::Add(protos::pbzero::InternedData* interned_data,
+                                size_t iid,
+                                const char* value,
+                                size_t length) {
+  auto category = interned_data->add_event_categories();
+  category->set_iid(iid);
+  category->set_name(value, length);
+}
+
+InternedEventName::~InternedEventName() = default;
+
+// static
+void InternedEventName::Add(protos::pbzero::InternedData* interned_data,
+                            size_t iid,
+                            const char* value) {
+  auto name = interned_data->add_event_names();
+  name->set_iid(iid);
+  name->set_name(value);
+}
+
+InternedDebugAnnotationName::~InternedDebugAnnotationName() = default;
+
+// static
+void InternedDebugAnnotationName::Add(
+    protos::pbzero::InternedData* interned_data,
+    size_t iid,
+    const char* value) {
+  auto name = interned_data->add_debug_annotation_names();
+  name->set_iid(iid);
+  name->set_name(value);
+}
+
+}  // namespace internal
+}  // namespace perfetto
diff --git a/src/tracing/ipc/BUILD.gn b/src/tracing/ipc/BUILD.gn
index 93c97a6..f86aeb5 100644
--- a/src/tracing/ipc/BUILD.gn
+++ b/src/tracing/ipc/BUILD.gn
@@ -32,6 +32,8 @@
     "memfd.h",
     "posix_shared_memory.cc",
     "posix_shared_memory.h",
+    "shared_memory_windows.cc",
+    "shared_memory_windows.h",
   ]
   deps = [
     "../../../gn:default_deps",
diff --git a/src/tracing/ipc/default_socket.cc b/src/tracing/ipc/default_socket.cc
index 3ad5cee..42b47e7 100644
--- a/src/tracing/ipc/default_socket.cc
+++ b/src/tracing/ipc/default_socket.cc
@@ -23,19 +23,22 @@
 #include "perfetto/ext/tracing/core/basic_types.h"
 
 #include <stdlib.h>
-#include <unistd.h>
 
-namespace perfetto {
-#if !PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-// On non-Android platforms, check /run/perfetto/ before using /tmp/ as the
-// socket base directory.
-namespace {
-const char* kRunPerfettoBaseDir = "/run/perfetto/";
-
-bool UseRunPerfettoBaseDir() {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
     PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
     PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
+#include <unistd.h>
+#endif
+
+namespace perfetto {
+namespace {
+
+const char* kRunPerfettoBaseDir = "/run/perfetto/";
+
+// On Linux and CrOS, check /run/perfetto/ before using /tmp/ as the socket
+// base directory.
+bool UseRunPerfettoBaseDir() {
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX)
   // Note that the trailing / in |kRunPerfettoBaseDir| ensures we are checking
   // against a directory, not a file.
   int res = PERFETTO_EINTR(access(kRunPerfettoBaseDir, X_OK));
@@ -50,12 +53,12 @@
   }
   return false;
 #else
+  base::ignore_result(kRunPerfettoBaseDir);
   return false;
 #endif
 }
 
 }  // anonymous namespace
-#endif  // !PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
 
 static_assert(kInvalidUid == ipc::kInvalidUid, "kInvalidUid mismatching");
 
@@ -74,6 +77,7 @@
     name = producer_socket;
 #endif
   }
+  base::ignore_result(UseRunPerfettoBaseDir);  // Silence unused func warnings.
   return name;
 }
 
diff --git a/src/tracing/ipc/producer/producer_ipc_client_impl.cc b/src/tracing/ipc/producer/producer_ipc_client_impl.cc
index 64b1023..97b622c 100644
--- a/src/tracing/ipc/producer/producer_ipc_client_impl.cc
+++ b/src/tracing/ipc/producer/producer_ipc_client_impl.cc
@@ -21,6 +21,7 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/task_runner.h"
+#include "perfetto/ext/base/version.h"
 #include "perfetto/ext/ipc/client.h"
 #include "perfetto/ext/tracing/core/commit_data_request.h"
 #include "perfetto/ext/tracing/core/producer.h"
@@ -29,7 +30,12 @@
 #include "perfetto/tracing/core/data_source_config.h"
 #include "perfetto/tracing/core/data_source_descriptor.h"
 #include "perfetto/tracing/core/trace_config.h"
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+#include "src/tracing/ipc/shared_memory_windows.h"
+#else
 #include "src/tracing/ipc/posix_shared_memory.h"
+#endif
 
 // TODO(fmayer): think to what happens when ProducerIPCClientImpl gets destroyed
 // w.r.t. the Producer pointer. Also think to lifetime of the Producer* during
@@ -158,8 +164,13 @@
 
   int shm_fd = -1;
   if (shared_memory_) {
-    shm_fd = static_cast<PosixSharedMemory*>(shared_memory_.get())->fd();
     req.set_producer_provided_shmem(true);
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+    auto key = static_cast<SharedMemoryWindows*>(shared_memory_.get())->key();
+    req.set_shm_key_windows(key);
+#else
+    shm_fd = static_cast<PosixSharedMemory*>(shared_memory_.get())->fd();
+#endif
   }
 
 #if PERFETTO_DCHECK_IS_ON()
@@ -169,6 +180,7 @@
   req.set_build_flags(
       protos::gen::InitializeConnectionRequest::BUILD_FLAGS_DCHECKS_OFF);
 #endif
+  req.set_sdk_version(base::GetVersionString());
   producer_port_.InitializeConnection(req, std::move(on_init), shm_fd);
 
   // Create the back channel to receive commands from the Service.
@@ -253,15 +265,25 @@
   }
 
   if (cmd.has_setup_tracing()) {
+    std::unique_ptr<SharedMemory> ipc_shared_memory;
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+    const std::string& shm_key = cmd.setup_tracing().shm_key_windows();
+    if (!shm_key.empty())
+      ipc_shared_memory = SharedMemoryWindows::Attach(shm_key);
+#else
     base::ScopedFile shmem_fd = ipc_channel_->TakeReceivedFD();
     if (shmem_fd) {
+      // TODO(primiano): handle mmap failure in case of OOM.
+      ipc_shared_memory =
+          PosixSharedMemory::AttachToFd(std::move(shmem_fd),
+                                        /*require_seals_if_supported=*/false);
+    }
+#endif
+    if (ipc_shared_memory) {
       // This is the nominal case used in most configurations, where the service
       // provides the SMB.
       PERFETTO_CHECK(!is_shmem_provided_by_producer_ && !shared_memory_);
-      // TODO(primiano): handle mmap failure in case of OOM.
-      shared_memory_ =
-          PosixSharedMemory::AttachToFd(std::move(shmem_fd),
-                                        /*require_seals_if_supported=*/false);
+      shared_memory_ = std::move(ipc_shared_memory);
       shared_buffer_page_size_kb_ =
           cmd.setup_tracing().shared_buffer_page_size_kb();
       shared_memory_arbiter_ = SharedMemoryArbiter::CreateInstance(
diff --git a/src/tracing/ipc/service/producer_ipc_service.cc b/src/tracing/ipc/service/producer_ipc_service.cc
index cfa79b1..71abd8c 100644
--- a/src/tracing/ipc/service/producer_ipc_service.cc
+++ b/src/tracing/ipc/service/producer_ipc_service.cc
@@ -26,7 +26,12 @@
 #include "perfetto/ext/tracing/core/tracing_service.h"
 #include "perfetto/tracing/core/data_source_config.h"
 #include "perfetto/tracing/core/data_source_descriptor.h"
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+#include "src/tracing/ipc/shared_memory_windows.h"
+#else
 #include "src/tracing/ipc/posix_shared_memory.h"
+#endif
 
 // The remote Producer(s) are not trusted. All the methods from the ProducerPort
 // IPC layer (e.g. RegisterDataSource()) must assume that the remote Producer is
@@ -93,7 +98,18 @@
   // If the producer provided an SMB, tell the service to attempt to adopt it.
   std::unique_ptr<SharedMemory> shmem;
   if (req.producer_provided_shmem()) {
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+    if (!req.has_shm_key_windows() || req.shm_key_windows().empty()) {
+      PERFETTO_ELOG(
+          "shm_key_windows must be non-empty when "
+          "producer_provided_shmem = true");
+    } else {
+      shmem = SharedMemoryWindows::Attach(req.shm_key_windows());
+      // Attach() does error logging if something fails, no need to extra ELOGs.
+    }
+#else
     base::ScopedFile shmem_fd = ipc::Service::TakeReceivedFD();
+
     if (shmem_fd) {
       shmem = PosixSharedMemory::AttachToFd(
           std::move(shmem_fd), /*require_seals_if_supported=*/true);
@@ -107,6 +123,7 @@
           "InitializeConnectionRequest's producer_provided_shmem flag is set "
           "but the producer didn't provide an FD");
     }
+#endif
   }
 
   // ConnectProducer will call OnConnect() on the next task.
@@ -114,7 +131,8 @@
       producer.get(), client_info.uid(), req.producer_name(),
       req.shared_memory_size_hint_bytes(),
       /*in_process=*/false, smb_scraping_mode,
-      req.shared_memory_page_size_hint_bytes(), std::move(shmem));
+      req.shared_memory_page_size_hint_bytes(), std::move(shmem),
+      req.sdk_version());
 
   // Could happen if the service has too many producers connected.
   if (!producer->service_endpoint) {
@@ -455,10 +473,17 @@
     // Nominal case (% Chrome): service provides SMB.
     setup_tracing->set_shared_buffer_page_size_kb(
         static_cast<uint32_t>(service_endpoint->shared_buffer_page_size_kb()));
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+    const std::string& shm_key =
+        static_cast<SharedMemoryWindows*>(service_endpoint->shared_memory())
+            ->key();
+    setup_tracing->set_shm_key_windows(shm_key);
+#else
     const int shm_fd =
         static_cast<PosixSharedMemory*>(service_endpoint->shared_memory())
             ->fd();
     cmd.set_fd(shm_fd);
+#endif
   }
   async_producer_commands.Resolve(std::move(cmd));
 }
diff --git a/src/tracing/ipc/service/service_ipc_host_impl.cc b/src/tracing/ipc/service/service_ipc_host_impl.cc
index d19fef6..5618b3c 100644
--- a/src/tracing/ipc/service/service_ipc_host_impl.cc
+++ b/src/tracing/ipc/service/service_ipc_host_impl.cc
@@ -20,10 +20,15 @@
 #include "perfetto/base/task_runner.h"
 #include "perfetto/ext/ipc/host.h"
 #include "perfetto/ext/tracing/core/tracing_service.h"
-#include "src/tracing/ipc/posix_shared_memory.h"
 #include "src/tracing/ipc/service/consumer_ipc_service.h"
 #include "src/tracing/ipc/service/producer_ipc_service.h"
 
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+#include "src/tracing/ipc/shared_memory_windows.h"
+#else
+#include "src/tracing/ipc/posix_shared_memory.h"
+#endif
+
 namespace perfetto {
 
 // TODO(fmayer): implement per-uid connection limit (b/69093705).
@@ -66,8 +71,13 @@
 
 bool ServiceIPCHostImpl::DoStart() {
   // Create and initialize the platform-independent tracing business logic.
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  std::unique_ptr<SharedMemory::Factory> shm_factory(
+      new SharedMemoryWindows::Factory());
+#else
   std::unique_ptr<SharedMemory::Factory> shm_factory(
       new PosixSharedMemory::Factory());
+#endif
   svc_ = TracingService::CreateInstance(std::move(shm_factory), task_runner_);
 
   if (!producer_ipc_port_ || !consumer_ipc_port_) {
diff --git a/src/tracing/ipc/shared_memory_windows.cc b/src/tracing/ipc/shared_memory_windows.cc
new file mode 100644
index 0000000..579d810
--- /dev/null
+++ b/src/tracing/ipc/shared_memory_windows.cc
@@ -0,0 +1,113 @@
+/*
+ * 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/tracing/ipc/shared_memory_windows.h"
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+
+#include <memory>
+#include <random>
+
+#include <Windows.h>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/string_utils.h"
+
+namespace perfetto {
+
+// static
+std::unique_ptr<SharedMemoryWindows> SharedMemoryWindows::Create(size_t size) {
+  base::ScopedPlatformHandle shmem_handle;
+  std::random_device rnd_dev;
+  uint64_t rnd_key = (static_cast<uint64_t>(rnd_dev()) << 32) | rnd_dev();
+  std::string key = "perfetto_shm_" + base::Uint64ToHexStringNoPrefix(rnd_key);
+  shmem_handle.reset(CreateFileMappingA(
+      INVALID_HANDLE_VALUE,  // Use paging file.
+      nullptr,               // Default security.
+      PAGE_READWRITE,
+      static_cast<DWORD>(size >> 32),  // maximum object size (high-order DWORD)
+      static_cast<DWORD>(size),        // maximum object size (low-order DWORD)
+      key.c_str()));
+
+  if (!shmem_handle) {
+    PERFETTO_PLOG("CreateFileMapping() call failed");
+    return nullptr;
+  }
+  void* start =
+      MapViewOfFile(*shmem_handle, FILE_MAP_ALL_ACCESS, /*offsetHigh=*/0,
+                    /*offsetLow=*/0, size);
+  if (!start) {
+    PERFETTO_PLOG("MapViewOfFile() failed");
+    return nullptr;
+  }
+
+  return std::unique_ptr<SharedMemoryWindows>(new SharedMemoryWindows(
+      start, size, std::move(key), std::move(shmem_handle)));
+}
+
+// static
+std::unique_ptr<SharedMemoryWindows> SharedMemoryWindows::Attach(
+    const std::string& key) {
+  base::ScopedPlatformHandle shmem_handle;
+  shmem_handle.reset(
+      OpenFileMappingA(FILE_MAP_ALL_ACCESS, /*inherit=*/false, key.c_str()));
+  if (!shmem_handle) {
+    PERFETTO_PLOG("Failed to OpenFileMapping()");
+    return nullptr;
+  }
+
+  void* start =
+      MapViewOfFile(*shmem_handle, FILE_MAP_ALL_ACCESS, /*offsetHigh=*/0,
+                    /*offsetLow=*/0, /*dwNumberOfBytesToMap=*/0);
+  if (!start) {
+    PERFETTO_PLOG("MapViewOfFile() failed");
+    return nullptr;
+  }
+
+  MEMORY_BASIC_INFORMATION info{};
+  if (!VirtualQuery(start, &info, sizeof(info))) {
+    PERFETTO_PLOG("VirtualQuery() failed");
+    return nullptr;
+  }
+  size_t size = info.RegionSize;
+  return std::unique_ptr<SharedMemoryWindows>(
+      new SharedMemoryWindows(start, size, key, std::move(shmem_handle)));
+}
+
+SharedMemoryWindows::SharedMemoryWindows(void* start,
+                                         size_t size,
+                                         std::string key,
+                                         base::ScopedPlatformHandle handle)
+    : start_(start),
+      size_(size),
+      key_(std::move(key)),
+      handle_(std::move(handle)) {}
+
+SharedMemoryWindows::~SharedMemoryWindows() {
+  if (start_)
+    UnmapViewOfFile(start_);
+}
+
+SharedMemoryWindows::Factory::~Factory() = default;
+
+std::unique_ptr<SharedMemory> SharedMemoryWindows::Factory::CreateSharedMemory(
+    size_t size) {
+  return SharedMemoryWindows::Create(size);
+}
+
+}  // namespace perfetto
+
+#endif  // !OS_WIN
diff --git a/src/tracing/ipc/shared_memory_windows.h b/src/tracing/ipc/shared_memory_windows.h
new file mode 100644
index 0000000..803c8a8
--- /dev/null
+++ b/src/tracing/ipc/shared_memory_windows.h
@@ -0,0 +1,69 @@
+/*
+ * 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_TRACING_IPC_SHARED_MEMORY_WINDOWS_H_
+#define SRC_TRACING_IPC_SHARED_MEMORY_WINDOWS_H_
+
+#include "perfetto/base/build_config.h"
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+
+#include "perfetto/base/build_config.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/tracing/core/shared_memory.h"
+
+namespace perfetto {
+
+// Implements the SharedMemory and its factory for the Windows IPC transport.
+// This used only for standalone builds and NOT in chromium, which instead uses
+// a custom Mojo wrapper (MojoSharedMemory in chromium's //services/tracing/).
+class SharedMemoryWindows : public SharedMemory {
+ public:
+  class Factory : public SharedMemory::Factory {
+   public:
+    ~Factory() override;
+    std::unique_ptr<SharedMemory> CreateSharedMemory(size_t) override;
+  };
+
+  // Create a brand new SHM region.
+  static std::unique_ptr<SharedMemoryWindows> Create(size_t size);
+  static std::unique_ptr<SharedMemoryWindows> Attach(const std::string& key);
+  ~SharedMemoryWindows() override;
+  const std::string& key() const { return key_; }
+
+  // SharedMemory implementation.
+  void* start() const override { return start_; }
+  size_t size() const override { return size_; }
+
+ private:
+  SharedMemoryWindows(void* start,
+                      size_t size,
+                      std::string,
+                      base::ScopedPlatformHandle);
+  SharedMemoryWindows(const SharedMemoryWindows&) = delete;
+  SharedMemoryWindows& operator=(const SharedMemoryWindows&) = delete;
+
+  void* const start_;
+  const size_t size_;
+  std::string key_;
+  base::ScopedPlatformHandle handle_;
+};
+
+}  // namespace perfetto
+
+#endif  // OS_WIN
+
+#endif  // SRC_TRACING_IPC_SHARED_MEMORY_WINDOWS_H_
diff --git a/src/tracing/platform_posix.cc b/src/tracing/platform_posix.cc
index 6b04aaf..d6413ee 100644
--- a/src/tracing/platform_posix.cc
+++ b/src/tracing/platform_posix.cc
@@ -39,6 +39,7 @@
   ~PlatformPosix() override;
 
   ThreadLocalObject* GetOrCreateThreadLocalObject() override;
+
   std::unique_ptr<base::TaskRunner> CreateTaskRunner(
       const CreateTaskRunnerArgs&) override;
   std::string GetCurrentProcessName() override;
@@ -47,22 +48,39 @@
   pthread_key_t tls_key_{};
 };
 
+PlatformPosix* g_instance = nullptr;
+
 using ThreadLocalObject = Platform::ThreadLocalObject;
 
 PlatformPosix::PlatformPosix() {
+  PERFETTO_CHECK(!g_instance);
+  g_instance = this;
   auto tls_dtor = [](void* obj) {
+    // The Posix TLS implementation resets the key before calling this dtor.
+    // Here we re-reset it to the object we are about to delete. This is to
+    // handle re-entrant usages of tracing in the PostTask done during the dtor
+    // (see comments in TracingTLS::~TracingTLS()). Chromium's platform
+    // implementation (which does NOT use this platform impl) has a similar
+    // workaround (https://crrev.com/c/2748300).
+    pthread_setspecific(g_instance->tls_key_, obj);
     delete static_cast<ThreadLocalObject*>(obj);
+    pthread_setspecific(g_instance->tls_key_, nullptr);
   };
   PERFETTO_CHECK(pthread_key_create(&tls_key_, tls_dtor) == 0);
 }
 
 PlatformPosix::~PlatformPosix() {
   pthread_key_delete(tls_key_);
+  g_instance = nullptr;
 }
 
 ThreadLocalObject* PlatformPosix::GetOrCreateThreadLocalObject() {
   // In chromium this should be implemented using base::ThreadLocalStorage.
-  auto tls = static_cast<ThreadLocalObject*>(pthread_getspecific(tls_key_));
+  void* tls_ptr = pthread_getspecific(tls_key_);
+
+  // This is needed to handle re-entrant calls during TLS dtor.
+  // See comments in platform.cc and aosp/1712371 .
+  ThreadLocalObject* tls = static_cast<ThreadLocalObject*>(tls_ptr);
   if (!tls) {
     tls = ThreadLocalObject::CreateInstance().release();
     pthread_setspecific(tls_key_, tls);
diff --git a/src/tracing/platform_windows.cc b/src/tracing/platform_windows.cc
new file mode 100644
index 0000000..f5db52d
--- /dev/null
+++ b/src/tracing/platform_windows.cc
@@ -0,0 +1,175 @@
+/*
+ * 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/base/build_config.h"
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+
+#include <Windows.h>
+
+#include "perfetto/ext/base/thread_task_runner.h"
+#include "perfetto/tracing/internal/tracing_tls.h"
+#include "perfetto/tracing/platform.h"
+
+// Thread Termination Callbacks.
+// Windows doesn't support a per-thread destructor with its
+// TLS primitives. So, we build it manually by inserting a
+// function to be called on each thread's exit.
+// This magic is from chromium's base/threading/thread_local_storage_win.cc
+// which in turn is from http://www.codeproject.com/threads/tls.asp.
+
+#ifdef _WIN64
+#pragma comment(linker, "/INCLUDE:_tls_used")
+#pragma comment(linker, "/INCLUDE:perfetto_thread_callback_base")
+#else
+#pragma comment(linker, "/INCLUDE:__tls_used")
+#pragma comment(linker, "/INCLUDE:_perfetto_thread_callback_base")
+#endif
+
+namespace perfetto {
+
+namespace {
+
+class PlatformWindows : public Platform {
+ public:
+  static PlatformWindows* instance;
+  PlatformWindows();
+  ~PlatformWindows() override;
+
+  ThreadLocalObject* GetOrCreateThreadLocalObject() override;
+  std::unique_ptr<base::TaskRunner> CreateTaskRunner(
+      const CreateTaskRunnerArgs&) override;
+  std::string GetCurrentProcessName() override;
+  void OnThreadExit();
+
+ private:
+  DWORD tls_key_{};
+};
+
+using ThreadLocalObject = Platform::ThreadLocalObject;
+
+// static
+PlatformWindows* PlatformWindows::instance = nullptr;
+
+PlatformWindows::PlatformWindows() {
+  instance = this;
+  tls_key_ = ::TlsAlloc();
+  PERFETTO_CHECK(tls_key_ != TLS_OUT_OF_INDEXES);
+}
+
+PlatformWindows::~PlatformWindows() {
+  ::TlsFree(tls_key_);
+  instance = nullptr;
+}
+
+void PlatformWindows::OnThreadExit() {
+  auto tls = static_cast<ThreadLocalObject*>(::TlsGetValue(tls_key_));
+  if (tls) {
+    // At this point we rely on the TLS object to be still set to the TracingTLS
+    // we are deleting. See comments in TracingTLS::~TracingTLS().
+    delete tls;
+  }
+}
+
+ThreadLocalObject* PlatformWindows::GetOrCreateThreadLocalObject() {
+  void* tls_ptr = ::TlsGetValue(tls_key_);
+
+  auto* tls = static_cast<ThreadLocalObject*>(tls_ptr);
+  if (!tls) {
+    tls = ThreadLocalObject::CreateInstance().release();
+    ::TlsSetValue(tls_key_, tls);
+  }
+  return tls;
+}
+
+std::unique_ptr<base::TaskRunner> PlatformWindows::CreateTaskRunner(
+    const CreateTaskRunnerArgs&) {
+  return std::unique_ptr<base::TaskRunner>(
+      new base::ThreadTaskRunner(base::ThreadTaskRunner::CreateAndStart()));
+}
+
+std::string PlatformWindows::GetCurrentProcessName() {
+  char buf[MAX_PATH];
+  auto len = ::GetModuleFileNameA(nullptr /*current*/, buf, sizeof(buf));
+  std::string name(buf, static_cast<size_t>(len));
+  size_t sep = name.find_last_of('\\');
+  if (sep != std::string::npos)
+    name = name.substr(sep + 1);
+  return name;
+}
+
+}  // namespace
+
+// static
+Platform* Platform::GetDefaultPlatform() {
+  static PlatformWindows* thread_safe_init_instance = new PlatformWindows();
+  return thread_safe_init_instance;
+}
+
+}  // namespace perfetto
+
+// -----------------------
+// Thread-local destructor
+// -----------------------
+
+// .CRT$XLA to .CRT$XLZ is an array of PIMAGE_TLS_CALLBACK pointers that are
+// called automatically by the OS loader code (not the CRT) when the module is
+// loaded and on thread creation. They are NOT called if the module has been
+// loaded by a LoadLibrary() call. It must have implicitly been loaded at
+// process startup.
+// See VC\crt\src\tlssup.c for reference.
+
+// extern "C" suppresses C++ name mangling so we know the symbol name for the
+// linker /INCLUDE:symbol pragma above.
+extern "C" {
+// The linker must not discard perfetto_thread_callback_base. (We force a
+// reference to this variable with a linker /INCLUDE:symbol pragma to ensure
+// that.) If this variable is discarded, the OnThreadExit function will never be
+// called.
+
+void NTAPI PerfettoOnThreadExit(PVOID, DWORD, PVOID);
+void NTAPI PerfettoOnThreadExit(PVOID module, DWORD reason, PVOID reserved) {
+  if (reason == DLL_THREAD_DETACH || reason == DLL_PROCESS_DETACH) {
+    if (perfetto::PlatformWindows::instance)
+      perfetto::PlatformWindows::instance->OnThreadExit();
+  }
+}
+
+#ifdef _WIN64
+
+// .CRT section is merged with .rdata on x64 so it must be constant data.
+#pragma const_seg(".CRT$XLP")
+
+// When defining a const variable, it must have external linkage to be sure the
+// linker doesn't discard it.
+extern const PIMAGE_TLS_CALLBACK perfetto_thread_callback_base;
+const PIMAGE_TLS_CALLBACK perfetto_thread_callback_base = PerfettoOnThreadExit;
+
+// Reset the default section.
+#pragma const_seg()
+
+#else  // _WIN64
+
+#pragma data_seg(".CRT$XLP")
+PIMAGE_TLS_CALLBACK perfetto_thread_callback_base = PerfettoOnThreadExit;
+// Reset the default section.
+#pragma data_seg()
+
+#endif  // _WIN64
+
+}  // extern "C"
+
+#endif  // OS_WIN
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 28f6110..ed08a17 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -65,6 +65,7 @@
 #include "protos/perfetto/trace/profiling/profile_common.gen.h"
 #include "protos/perfetto/trace/test_event.gen.h"
 #include "protos/perfetto/trace/test_event.pbzero.h"
+#include "protos/perfetto/trace/test_extensions.pbzero.h"
 #include "protos/perfetto/trace/trace.gen.h"
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.gen.h"
@@ -124,9 +125,8 @@
 // Represents an opaque (from Perfetto's point of view) thread identifier (e.g.,
 // base::PlatformThreadId in Chromium).
 struct MyThreadId {
-  MyThreadId(int pid_, int tid_) : pid(pid_), tid(tid_) {}
+  explicit MyThreadId(int tid_) : tid(tid_) {}
 
-  const int pid = 0;
   const int tid = 0;
 };
 
@@ -142,23 +142,9 @@
 namespace legacy {
 
 template <>
-bool ConvertThreadId(const MyThreadId& thread,
-                     uint64_t* track_uuid_out,
-                     int32_t* pid_override_out,
-                     int32_t* tid_override_out) {
-  if (!thread.pid && !thread.tid)
-    return false;
-  if (!thread.pid) {
-    // Thread in current process.
-    *track_uuid_out = perfetto::ThreadTrack::ForThread(
-                          static_cast<base::PlatformThreadId>(thread.tid))
-                          .uuid;
-  } else {
-    // Thread in another process.
-    *pid_override_out = thread.pid;
-    *tid_override_out = thread.tid;
-  }
-  return true;
+ThreadTrack ConvertThreadId(const MyThreadId& thread) {
+  return perfetto::ThreadTrack::ForThread(
+      static_cast<base::PlatformThreadId>(thread.tid));
 }
 
 }  // namespace legacy
@@ -1487,6 +1473,139 @@
   perfetto::TrackEvent::EraseTrackDescriptor(track);
 }
 
+TEST_P(PerfettoApiTest, TrackEventCustomTimestampClock) {
+  // Create a new trace session.
+  auto* tracing_session = NewTraceWithCategories({"foo"});
+  tracing_session->get()->StartBlocking();
+
+  const perfetto::protos::pbzero::BuiltinClock kMyClockId =
+      static_cast<perfetto::protos::pbzero::BuiltinClock>(700);
+  const uint64_t kTimestamp = 12345678;
+
+  // First emit a clock snapshot that maps our custom clock to regular trace
+  // time. Note that the clock snapshot should come before any events
+  // referencing that clock.
+  perfetto::TrackEvent::Trace([](perfetto::TrackEvent::TraceContext ctx) {
+    auto packet = ctx.NewTracePacket();
+    packet->set_timestamp_clock_id(perfetto::TrackEvent::GetTraceClockId());
+    packet->set_timestamp(perfetto::TrackEvent::GetTraceTimeNs());
+    auto* clock_snapshot = packet->set_clock_snapshot();
+    // First set the reference clock, i.e., the default trace clock in this
+    // case.
+    auto* clock = clock_snapshot->add_clocks();
+    clock->set_clock_id(perfetto::TrackEvent::GetTraceClockId());
+    clock->set_timestamp(perfetto::TrackEvent::GetTraceTimeNs());
+    // Then set the value of our reference clock at the same point in time. We
+    // pretend our clock is one second behind trace time.
+    clock = clock_snapshot->add_clocks();
+    clock->set_clock_id(kMyClockId);
+    clock->set_timestamp(kTimestamp + 1000000000ull);
+  });
+
+  // Next emit a trace event with a custom timestamp and a custom clock.
+  TRACE_EVENT_INSTANT("foo", "EventWithCustomTime",
+                      perfetto::TraceTimestamp{kMyClockId, kTimestamp});
+  TRACE_EVENT_INSTANT("foo", "EventWithNormalTime");
+
+  perfetto::TrackEvent::Flush();
+  tracing_session->get()->StopBlocking();
+
+  std::vector<char> raw_trace = tracing_session->get()->ReadTraceBlocking();
+  perfetto::protos::gen::Trace trace;
+  ASSERT_TRUE(trace.ParseFromArray(raw_trace.data(), raw_trace.size()));
+
+  // Check that both the clock id and the timestamp got written together with
+  // the packet. Note that we don't check the actual clock sync behavior here
+  // since that happens in the Trace Processor instead.
+  bool found_clock_snapshot = false;
+  bool found_event = false;
+  for (const auto& packet : trace.packet()) {
+    if (packet.has_clock_snapshot())
+      found_clock_snapshot = true;
+    if (!packet.has_track_event() || packet.timestamp() != kTimestamp)
+      continue;
+    found_event = true;
+    EXPECT_EQ(static_cast<uint32_t>(kMyClockId), packet.timestamp_clock_id());
+    EXPECT_EQ(kTimestamp, packet.timestamp());
+  }
+  EXPECT_TRUE(found_clock_snapshot);
+  EXPECT_TRUE(found_event);
+}
+
+TEST_P(PerfettoApiTest, LegacyEventWithThreadOverride) {
+  // Create a new trace session.
+  auto* tracing_session = NewTraceWithCategories({"cat"});
+  tracing_session->get()->StartBlocking();
+
+  TRACE_EVENT_BEGIN_WITH_ID_TID_AND_TIMESTAMP0("cat", "Name", 1,
+                                               MyThreadId(456), MyTimestamp{0});
+  perfetto::TrackEvent::Flush();
+  tracing_session->get()->StopBlocking();
+
+  std::vector<char> raw_trace = tracing_session->get()->ReadTraceBlocking();
+  perfetto::protos::gen::Trace trace;
+  ASSERT_TRUE(trace.ParseFromArray(raw_trace.data(), raw_trace.size()));
+
+  // Check that we wrote a track descriptor for the custom thread track, and
+  // that the event was associated with that track.
+  const auto track = perfetto::ThreadTrack::ForThread(456);
+  bool found_descriptor = false;
+  bool found_event = false;
+  for (const auto& packet : trace.packet()) {
+    if (packet.has_track_descriptor() &&
+        packet.track_descriptor().has_thread()) {
+      auto td = packet.track_descriptor().thread();
+      if (td.tid() == 456) {
+        EXPECT_EQ(track.uuid, packet.track_descriptor().uuid());
+        found_descriptor = true;
+      }
+    }
+
+    if (!packet.has_track_event())
+      continue;
+    auto track_event = packet.track_event();
+    if (track_event.legacy_event().phase() == TRACE_EVENT_PHASE_ASYNC_BEGIN) {
+      EXPECT_EQ(track.uuid, track_event.track_uuid());
+      found_event = true;
+    }
+  }
+  EXPECT_TRUE(found_descriptor);
+  EXPECT_TRUE(found_event);
+  perfetto::TrackEvent::EraseTrackDescriptor(track);
+}
+
+TEST_P(PerfettoApiTest, LegacyEventWithProcessOverride) {
+  // Create a new trace session.
+  auto* tracing_session = NewTraceWithCategories({"cat"});
+  tracing_session->get()->StartBlocking();
+
+  // Note: there's no direct entrypoint for adding trace events for another
+  // process, so we're using the internal support macro here.
+  INTERNAL_TRACE_EVENT_ADD_WITH_ID_TID_AND_TIMESTAMP(
+      TRACE_EVENT_PHASE_INSTANT, "cat", "Name", 0, MyThreadId{789},
+      MyTimestamp{0}, TRACE_EVENT_FLAG_HAS_PROCESS_ID);
+  perfetto::TrackEvent::Flush();
+  tracing_session->get()->StopBlocking();
+
+  std::vector<char> raw_trace = tracing_session->get()->ReadTraceBlocking();
+  perfetto::protos::gen::Trace trace;
+  ASSERT_TRUE(trace.ParseFromArray(raw_trace.data(), raw_trace.size()));
+
+  // Check that the event has a pid_override matching MyThread above.
+  bool found_event = false;
+  for (const auto& packet : trace.packet()) {
+    if (!packet.has_track_event())
+      continue;
+    auto track_event = packet.track_event();
+    if (track_event.type() == perfetto::protos::gen::TrackEvent::TYPE_INSTANT) {
+      EXPECT_EQ(789, track_event.legacy_event().pid_override());
+      EXPECT_EQ(-1, track_event.legacy_event().tid_override());
+      found_event = true;
+    }
+  }
+  EXPECT_TRUE(found_event);
+}
+
 TEST_P(PerfettoApiTest, TrackDescriptorWrittenBeforeEvent) {
   // Create a new trace session.
   auto* tracing_session = NewTraceWithCategories({"bar"});
@@ -1685,6 +1804,84 @@
   EXPECT_TRUE(found_args);
 }
 
+TEST_P(PerfettoApiTest, InlineTrackEventTypedArgs_SimpleRepeated) {
+  // Create a new trace session.
+  auto* tracing_session = NewTraceWithCategories({"foo"});
+  tracing_session->get()->StartBlocking();
+
+  std::vector<uint64_t> flow_ids{1, 2, 3};
+  TRACE_EVENT_BEGIN("foo", "EventWithTypedArg",
+                    perfetto::protos::pbzero::TrackEvent::kFlowIds, flow_ids);
+  TRACE_EVENT_END("foo");
+
+  tracing_session->get()->StopBlocking();
+  std::vector<char> raw_trace = tracing_session->get()->ReadTraceBlocking();
+  std::string trace(raw_trace.data(), raw_trace.size());
+
+  perfetto::protos::gen::Trace parsed_trace;
+  ASSERT_TRUE(parsed_trace.ParseFromArray(raw_trace.data(), raw_trace.size()));
+
+  bool found_args = false;
+  for (const auto& packet : parsed_trace.packet()) {
+    if (!packet.has_track_event())
+      continue;
+    const auto& track_event = packet.track_event();
+    if (track_event.type() !=
+        perfetto::protos::gen::TrackEvent::TYPE_SLICE_BEGIN) {
+      continue;
+    }
+
+    EXPECT_THAT(track_event.flow_ids(), testing::ElementsAre(1, 2, 3));
+    found_args = true;
+  }
+  EXPECT_TRUE(found_args);
+}
+
+TEST_P(PerfettoApiTest, InlineTrackEventTypedArgs_NestedSingle) {
+  struct LogMessage {
+    void WriteIntoTrace(
+        perfetto::TracedProto<perfetto::protos::pbzero::LogMessage> context)
+        const {
+      context->set_source_location_iid(1);
+      context->set_body_iid(2);
+    }
+  };
+
+  // Create a new trace session.
+  auto* tracing_session = NewTraceWithCategories({"foo"});
+  tracing_session->get()->StartBlocking();
+
+  TRACE_EVENT_BEGIN("foo", "EventWithTypedArg",
+                    perfetto::protos::pbzero::TrackEvent::kLogMessage,
+                    LogMessage());
+  TRACE_EVENT_END("foo");
+
+  tracing_session->get()->StopBlocking();
+  std::vector<char> raw_trace = tracing_session->get()->ReadTraceBlocking();
+  std::string trace(raw_trace.data(), raw_trace.size());
+
+  perfetto::protos::gen::Trace parsed_trace;
+  ASSERT_TRUE(parsed_trace.ParseFromArray(raw_trace.data(), raw_trace.size()));
+
+  bool found_args = false;
+  for (const auto& packet : parsed_trace.packet()) {
+    if (!packet.has_track_event())
+      continue;
+    const auto& track_event = packet.track_event();
+    if (track_event.type() !=
+        perfetto::protos::gen::TrackEvent::TYPE_SLICE_BEGIN) {
+      continue;
+    }
+
+    EXPECT_TRUE(track_event.has_log_message());
+    const auto& log = track_event.log_message();
+    EXPECT_EQ(1u, log.source_location_iid());
+    EXPECT_EQ(2u, log.body_iid());
+    found_args = true;
+  }
+  EXPECT_TRUE(found_args);
+}
+
 struct InternedLogMessageBody
     : public perfetto::TrackEventInternedDataIndex<
           InternedLogMessageBody,
@@ -1936,7 +2133,8 @@
   {
     TRACE_EVENT("test", "TestEventWithExtensionArgs",
                 [&](perfetto::EventContext ctx) {
-                  ctx.event<TestTrackEvent>()->set_extension_value(42);
+                  ctx.event<perfetto::protos::pbzero::TestExtension>()
+                      ->add_int_extension_for_testing(42);
                 });
   }
 
@@ -1962,7 +2160,51 @@
 
     for (protozero::Field f = decoder.ReadField(); f.valid();
          f = decoder.ReadField()) {
-      if (f.id() == TestTrackEvent::field_number) {
+      if (f.id() == perfetto::protos::pbzero::TestExtension::
+                        FieldMetadata_IntExtensionForTesting::kFieldId) {
+        found_extension = true;
+      }
+    }
+  }
+
+  EXPECT_TRUE(found_extension);
+}
+
+TEST_P(PerfettoApiTest, InlineTypedExtensionField) {
+  auto* tracing_session = NewTraceWithCategories({"test"});
+  tracing_session->get()->StartBlocking();
+
+  {
+    TRACE_EVENT(
+        "test", "TestEventWithExtensionArgs",
+        perfetto::protos::pbzero::TestExtension::kIntExtensionForTesting,
+        std::vector<int>{42});
+  }
+
+  perfetto::TrackEvent::Flush();
+  tracing_session->get()->StopBlocking();
+
+  std::vector<char> raw_trace = tracing_session->get()->ReadTraceBlocking();
+  EXPECT_GE(raw_trace.size(), 0u);
+
+  bool found_extension = false;
+  perfetto::protos::pbzero::Trace_Decoder trace(
+      reinterpret_cast<uint8_t*>(raw_trace.data()), raw_trace.size());
+
+  for (auto it = trace.packet(); it; ++it) {
+    perfetto::protos::pbzero::TracePacket_Decoder packet(it->data(),
+                                                         it->size());
+
+    if (!packet.has_track_event())
+      continue;
+
+    auto track_event = packet.track_event();
+    protozero::ProtoDecoder decoder(track_event.data, track_event.size);
+
+    for (protozero::Field f = decoder.ReadField(); f.valid();
+         f = decoder.ReadField()) {
+      if (f.id() == perfetto::protos::pbzero::TestExtension::
+                        FieldMetadata_IntExtensionForTesting::kFieldId) {
         found_extension = true;
       }
     }
@@ -2000,6 +2242,25 @@
               ElementsAre("I:test.ThreadEvent", "[track=0]I:test.GlobalEvent"));
 }
 
+TEST_P(PerfettoApiTest, TrackEventTrackFromPointer) {
+  // Create a new trace session.
+  auto* tracing_session = NewTraceWithCategories({"test"});
+  tracing_session->get()->StartBlocking();
+
+  perfetto::Track parent_track(1);
+  int* ptr = reinterpret_cast<int*>(2);
+  TRACE_EVENT_INSTANT("test", "Event",
+                      perfetto::Track::FromPointer(ptr, parent_track));
+  perfetto::TrackEvent::Flush();
+
+  perfetto::Track track(reinterpret_cast<uintptr_t>(ptr), parent_track);
+
+  tracing_session->get()->StopBlocking();
+  auto slices = ReadSlicesFromTrace(tracing_session->get());
+  EXPECT_THAT(slices, ElementsAre("[track=" + std::to_string(track.uuid) +
+                                  "]I:test.Event"));
+}
+
 TEST_P(PerfettoApiTest, TrackEventDebugAnnotations) {
   // Create a new trace session.
   auto* tracing_session = NewTraceWithCategories({"test"});
@@ -2255,7 +2516,8 @@
         slices,
         ElementsAre("B:foo.FooEvent", "B:bar.BarEvent", "B:foo,bar.MultiFooBar",
                     "B:baz,bar,quux.MultiBar", "B:red,green,blue,foo.MultiFoo",
-                    "B:test.TagEvent", "B:$dynamic,$foo.DynamicGroupFooEvent",
+                    "B:red,green,blue,yellow.MultiNone", "B:test.TagEvent",
+                    "B:$dynamic,$foo.DynamicGroupFooEvent",
                     "B:$dynamic,$bar.DynamicGroupBarEvent"));
   }
 
@@ -2270,6 +2532,16 @@
                                     "B:$dynamic,$foo.DynamicGroupFooEvent"));
   }
 
+  // Enable exactly one dynamic category.
+  {
+    perfetto::protos::gen::TrackEventConfig te_cfg;
+    te_cfg.add_disabled_categories("*");
+    te_cfg.add_enabled_categories("dynamic");
+    auto slices = check_config(te_cfg);
+    EXPECT_THAT(slices, ElementsAre("B:$dynamic,$foo.DynamicGroupFooEvent",
+                                    "B:$dynamic,$bar.DynamicGroupBarEvent"));
+  }
+
   // Enable two categories.
   {
     perfetto::protos::gen::TrackEventConfig te_cfg;
@@ -2295,7 +2567,8 @@
         slices,
         ElementsAre("B:foo.FooEvent", "B:bar.BarEvent", "B:foo,bar.MultiFooBar",
                     "B:baz,bar,quux.MultiBar", "B:red,green,blue,foo.MultiFoo",
-                    "B:test.TagEvent", "B:$dynamic,$foo.DynamicGroupFooEvent",
+                    "B:red,green,blue,yellow.MultiNone", "B:test.TagEvent",
+                    "B:$dynamic,$foo.DynamicGroupFooEvent",
                     "B:$dynamic,$bar.DynamicGroupBarEvent"));
   }
 
@@ -2337,14 +2610,15 @@
     te_cfg.add_enabled_tags("slow");
     te_cfg.add_enabled_tags("debug");
     auto slices = check_config(te_cfg);
-    EXPECT_THAT(slices,
-                ElementsAre("B:foo.FooEvent", "B:bar.BarEvent",
-                            "B:foo,bar.MultiFooBar", "B:baz,bar,quux.MultiBar",
-                            "B:red,green,blue,foo.MultiFoo", "B:cat.SlowEvent",
-                            "B:cat.verbose.DebugEvent", "B:test.TagEvent",
-                            "B:disabled-by-default-cat.SlowDisabledEvent",
-                            "B:$dynamic,$foo.DynamicGroupFooEvent",
-                            "B:$dynamic,$bar.DynamicGroupBarEvent"));
+    EXPECT_THAT(
+        slices,
+        ElementsAre("B:foo.FooEvent", "B:bar.BarEvent", "B:foo,bar.MultiFooBar",
+                    "B:baz,bar,quux.MultiBar", "B:red,green,blue,foo.MultiFoo",
+                    "B:red,green,blue,yellow.MultiNone", "B:cat.SlowEvent",
+                    "B:cat.verbose.DebugEvent", "B:test.TagEvent",
+                    "B:disabled-by-default-cat.SlowDisabledEvent",
+                    "B:$dynamic,$foo.DynamicGroupFooEvent",
+                    "B:$dynamic,$bar.DynamicGroupBarEvent"));
   }
 }
 
@@ -3044,10 +3318,19 @@
 }
 
 TEST_P(PerfettoApiTest, LegacyTraceEvents) {
+  auto is_new_session = [] {
+    bool result;
+    TRACE_EVENT_IS_NEW_TRACE(&result);
+    return result;
+  };
+
   // Create a new trace session.
+  EXPECT_FALSE(is_new_session());
   auto* tracing_session =
       NewTraceWithCategories({"cat", TRACE_DISABLED_BY_DEFAULT("cat")});
   tracing_session->get()->StartBlocking();
+  EXPECT_TRUE(is_new_session());
+  EXPECT_FALSE(is_new_session());
 
   // Basic events.
   TRACE_EVENT_INSTANT0("cat", "LegacyEvent", TRACE_EVENT_SCOPE_GLOBAL);
@@ -3069,7 +3352,7 @@
   // Event with id, thread id and timestamp (and dynamic name).
   TRACE_EVENT_COPY_BEGIN_WITH_ID_TID_AND_TIMESTAMP0(
       "cat", std::string("LegacyWithIdTidAndTimestamp").c_str(), 1,
-      MyThreadId(123, 456), MyTimestamp{3});
+      MyThreadId(123), MyTimestamp{3});
 
   // Event with id.
   TRACE_COUNTER1("cat", "LegacyCounter", 1234);
@@ -3085,14 +3368,14 @@
       slices,
       ElementsAre(
           "[track=0]I:cat.LegacyEvent", "B:cat.LegacyEvent(arg=(int)123)",
-          "E.LegacyEvent(arg=(string)string,arg2=(double)0.123)",
-          "B:cat.ScopedLegacyEvent", "E",
+          "E(arg=(string)string,arg2=(double)0.123)", "B:cat.ScopedLegacyEvent",
+          "E",
           "B(bind_id=3671771902)(flow_direction=1):disabled-by-default-cat."
           "LegacyFlowEvent",
           "[track=0]I:cat.LegacyInstantEvent",
-          "[track=0]Legacy_S(unscoped_id=1)(pid_override=123)(tid_override=456)"
-          ":cat."
-          "LegacyWithIdTidAndTimestamp",
+          std::string("[track=") +
+              std::to_string(perfetto::ThreadTrack::ForThread(123).uuid) +
+              "]Legacy_S(unscoped_id=1):cat.LegacyWithIdTidAndTimestamp",
           "Legacy_C:cat.LegacyCounter(value=(int)1234)",
           "Legacy_C(unscoped_id=1234):cat.LegacyCounterWithId(value=(int)9000)",
           "Legacy_M:cat.LegacyMetadata"));
@@ -3445,28 +3728,16 @@
   thread.join();
 
   TRACE_EVENT_INSTANT(
-      "foo", "More annotations", [](perfetto::EventContext ctx) {
-        {
-          auto* dbg = ctx.event()->add_debug_annotations();
-          dbg->set_name("dict");
-          auto* nested = dbg->set_nested_value();
-          nested->set_nested_type(
-              perfetto::protos::pbzero::DebugAnnotation::NestedValue::DICT);
-          nested->add_dict_keys("key");
-          auto* value = nested->add_dict_values();
-          value->set_int_value(123);
-        }
-        {
-          auto* dbg = ctx.event()->add_debug_annotations();
-          dbg->set_name("array");
-          auto* nested = dbg->set_nested_value();
-          nested->set_nested_type(
-              perfetto::protos::pbzero::DebugAnnotation::NestedValue::ARRAY);
-          auto* value = nested->add_array_values();
-          value->set_string_value("first");
-          value = nested->add_array_values();
-          value->set_string_value("second");
-        }
+      "foo", "More annotations", "dict",
+      [](perfetto::TracedValue context) {
+        auto dict = std::move(context).WriteDictionary();
+        dict.Add("key", 123);
+      },
+      "array",
+      [](perfetto::TracedValue context) {
+        auto array = std::move(context).WriteArray();
+        array.Append("first");
+        array.Append("second");
       });
 }
 
diff --git a/src/tracing/test/traced_value_test_support.cc b/src/tracing/test/traced_value_test_support.cc
index 2912d59..ad296d7 100644
--- a/src/tracing/test/traced_value_test_support.cc
+++ b/src/tracing/test/traced_value_test_support.cc
@@ -27,50 +27,6 @@
 
 namespace {
 
-void WriteAsJSON(const protos::DebugAnnotation::NestedValue& value,
-                 std::stringstream& ss) {
-  if (value.nested_type() ==
-      protos::DebugAnnotation_NestedValue_NestedType_DICT) {
-    ss << "{";
-    for (int i = 0; i < value.dict_keys_size() && i < value.dict_values_size();
-         ++i) {
-      if (i > 0)
-        ss << ",";
-      ss << value.dict_keys(i);
-      ss << ":";
-      WriteAsJSON(value.dict_values(i), ss);
-    }
-    ss << "}";
-    return;
-  } else if (value.nested_type() ==
-             protos::DebugAnnotation_NestedValue_NestedType_ARRAY) {
-    ss << "[";
-    for (int i = 0; i < value.array_values_size(); ++i) {
-      if (i > 0)
-        ss << ",";
-      WriteAsJSON(value.array_values(i), ss);
-    }
-    ss << "]";
-    return;
-  } else if (value.has_int_value()) {
-    ss << value.int_value();
-    return;
-  } else if (value.has_double_value()) {
-    ss << value.double_value();
-    return;
-  } else if (value.has_bool_value()) {
-    if (value.bool_value()) {
-      ss << "true";
-    } else {
-      ss << "false";
-    }
-    return;
-  } else if (value.has_string_value()) {
-    ss << value.string_value();
-    return;
-  }
-}
-
 void WriteAsJSON(const protos::DebugAnnotation& value, std::stringstream& ss) {
   if (value.has_bool_value()) {
     if (value.bool_value()) {
@@ -78,30 +34,40 @@
     } else {
       ss << "false";
     }
-    return;
   } else if (value.has_uint_value()) {
     ss << value.uint_value();
-    return;
   } else if (value.has_int_value()) {
     ss << value.int_value();
-    return;
   } else if (value.has_double_value()) {
     ss << value.double_value();
-    return;
   } else if (value.has_string_value()) {
     ss << value.string_value();
-    return;
   } else if (value.has_pointer_value()) {
     // Printing pointer values via ostream is really platform-specific, so do
     // not try to convert it to void* before printing.
     ss << "0x" << std::hex << value.pointer_value() << std::dec;
-    return;
-  } else if (value.has_nested_value()) {
-    WriteAsJSON(value.nested_value(), ss);
-    return;
-  } else if (value.has_legacy_json_value()) {
-    ss << value.legacy_json_value();
-    return;
+  } else if (value.dict_entries_size() > 0) {
+    ss << "{";
+    for (int i = 0; i < value.dict_entries_size(); ++i) {
+      if (i > 0)
+        ss << ",";
+      auto item = value.dict_entries(i);
+      ss << item.name();
+      ss << ":";
+      WriteAsJSON(item, ss);
+    }
+    ss << "}";
+  } else if (value.array_values_size() > 0) {
+    ss << "[";
+    for (int i = 0; i < value.array_values_size(); ++i) {
+      auto item = value.array_values(i);
+      if (i > 0)
+        ss << ",";
+      WriteAsJSON(item, ss);
+    }
+    ss << "]";
+  } else {
+    ss << "{}";
   }
 }
 
diff --git a/src/tracing/traced_proto_unittest.cc b/src/tracing/traced_proto_unittest.cc
new file mode 100644
index 0000000..ac55400
--- /dev/null
+++ b/src/tracing/traced_proto_unittest.cc
@@ -0,0 +1,138 @@
+/*
+ * 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/tracing/traced_proto.h"
+
+#include "perfetto/test/traced_value_test_support.h"
+#include "perfetto/tracing/track_event.h"
+#include "protos/perfetto/trace/test_event.gen.h"
+#include "protos/perfetto/trace/test_event.pb.h"
+#include "protos/perfetto/trace/test_event.pbzero.h"
+#include "protos/perfetto/trace/track_event/track_event.gen.h"
+#include "protos/perfetto/trace/track_event/track_event.pb.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+
+class TracedProtoTest : public ::testing::Test {
+ public:
+  TracedProtoTest() : context_(track_event_.get(), &incremental_state_) {}
+
+  EventContext& context() { return context_; }
+
+ private:
+  protozero::HeapBuffered<protos::pbzero::TrackEvent> track_event_;
+  internal::TrackEventIncrementalState incremental_state_;
+  EventContext context_;
+};
+
+using TestPayload = protos::pbzero::TestEvent::TestPayload;
+
+TEST_F(TracedProtoTest, SingleInt) {
+  protozero::HeapBuffered<TestPayload> event;
+  WriteIntoTracedProto(context().Wrap(event.get()), TestPayload::kSingleInt,
+                       42);
+
+  protos::TestEvent::TestPayload result;
+  result.ParseFromString(event.SerializeAsString());
+  EXPECT_TRUE(result.has_single_int());
+  EXPECT_EQ(result.single_int(), 42);
+}
+
+TEST_F(TracedProtoTest, RepeatedInt) {
+  protozero::HeapBuffered<TestPayload> event;
+  WriteIntoTracedProto(context().Wrap(event.get()), TestPayload::kRepeatedInts,
+                       std::vector<int>{1, 2, 3});
+
+  protos::TestEvent::TestPayload result;
+  result.ParseFromString(event.SerializeAsString());
+  EXPECT_THAT(result.repeated_ints(), ::testing::ElementsAre(1, 2, 3));
+}
+
+TEST_F(TracedProtoTest, SingleString) {
+  protozero::HeapBuffered<TestPayload> event;
+  WriteIntoTracedProto(context().Wrap(event.get()), TestPayload::kSingleString,
+                       "foo");
+
+  protos::TestEvent::TestPayload result;
+  result.ParseFromString(event.SerializeAsString());
+  EXPECT_TRUE(result.has_single_string());
+  EXPECT_EQ(result.single_string(), "foo");
+}
+
+TEST_F(TracedProtoTest, RepeatedString) {
+  protozero::HeapBuffered<TestPayload> event;
+  WriteIntoTracedProto(context().Wrap(event.get()), TestPayload::kStr,
+                       std::vector<std::string>{"foo", "bar"});
+
+  protos::TestEvent::TestPayload result;
+  result.ParseFromString(event.SerializeAsString());
+  EXPECT_THAT(result.str(), ::testing::ElementsAre("foo", "bar"));
+}
+
+namespace {
+
+struct Foo {
+  void WriteIntoTrace(TracedProto<TestPayload> message) const {
+    message->set_single_int(42);
+
+    auto dict = std::move(message).AddDebugAnnotations();
+    dict.Add("arg", "value");
+  }
+};
+
+}  // namespace
+
+TEST_F(TracedProtoTest, SingleNestedMessage) {
+  protozero::HeapBuffered<protos::pbzero::TestEvent> event;
+  WriteIntoTracedProto(context().Wrap(event.get()),
+                       protos::pbzero::TestEvent::kPayload, Foo());
+
+  protos::TestEvent result;
+  result.ParseFromString(event.SerializeAsString());
+  EXPECT_EQ(result.payload().single_int(), 42);
+}
+
+TEST_F(TracedProtoTest, RepeatedNestedMessage) {
+  protozero::HeapBuffered<TestPayload> event;
+  WriteIntoTracedProto(context().Wrap(event.get()), TestPayload::kNested,
+                       std::vector<Foo>{Foo(), Foo()});
+
+  protos::TestEvent::TestPayload result;
+  result.ParseFromString(event.SerializeAsString());
+  EXPECT_EQ(result.nested_size(), 2);
+  EXPECT_EQ(result.nested(0).single_int(), 42);
+  EXPECT_EQ(result.nested(1).single_int(), 42);
+}
+
+TEST_F(TracedProtoTest, WriteDebugAnnotations) {
+  protozero::HeapBuffered<protos::pbzero::TestEvent> event;
+  WriteIntoTracedProto(context().Wrap(event.get()),
+                       protos::pbzero::TestEvent::kPayload, Foo());
+
+  protos::TestEvent result;
+  result.ParseFromString(event.SerializeAsString());
+
+  protos::DebugAnnotation dict;
+  for (const auto& annotation : result.payload().debug_annotations()) {
+    *dict.add_dict_entries() = annotation;
+  }
+
+  EXPECT_EQ(internal::DebugAnnotationToString(dict.SerializeAsString()),
+            "{arg:value}");
+}
+
+}  // namespace perfetto
diff --git a/src/tracing/traced_value.cc b/src/tracing/traced_value.cc
index cba70c0..bd8b1ad 100644
--- a/src/tracing/traced_value.cc
+++ b/src/tracing/traced_value.cc
@@ -39,74 +39,42 @@
 
 void TracedValue::WriteInt64(int64_t value) && {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  if (nested_context_) {
-    nested_context_->set_int_value(value);
-  } else {
-    root_context_->set_int_value(value);
-  }
+  context_->set_int_value(value);
 }
 
 void TracedValue::WriteUInt64(uint64_t value) && {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  if (nested_context_) {
-    nested_context_->set_int_value(static_cast<int64_t>(value));
-  } else {
-    root_context_->set_uint_value(value);
-  }
+  context_->set_uint_value(value);
 }
 
 void TracedValue::WriteDouble(double value) && {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  if (nested_context_) {
-    nested_context_->set_double_value(value);
-  } else {
-    root_context_->set_double_value(value);
-  }
+  context_->set_double_value(value);
 }
 
 void TracedValue::WriteBoolean(bool value) && {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  if (nested_context_) {
-    nested_context_->set_bool_value(value);
-  } else {
-    root_context_->set_bool_value(value);
-  }
+  context_->set_bool_value(value);
 }
 
 void TracedValue::WriteString(const char* value) && {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  if (nested_context_) {
-    nested_context_->set_string_value(value);
-  } else {
-    root_context_->set_string_value(value);
-  }
+  context_->set_string_value(value);
 }
 
 void TracedValue::WriteString(const char* value, size_t len) && {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  if (nested_context_) {
-    nested_context_->set_string_value(value, len);
-  } else {
-    root_context_->set_string_value(value, len);
-  }
+  context_->set_string_value(value, len);
 }
 
 void TracedValue::WriteString(const std::string& value) && {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  if (nested_context_) {
-    nested_context_->set_string_value(value);
-  } else {
-    root_context_->set_string_value(value);
-  }
+  context_->set_string_value(value);
 }
 
 void TracedValue::WritePointer(const void* value) && {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  if (nested_context_) {
-    nested_context_->set_int_value(reinterpret_cast<int64_t>(value));
-  } else {
-    root_context_->set_pointer_value(reinterpret_cast<uint64_t>(value));
-  }
+  context_->set_pointer_value(reinterpret_cast<uint64_t>(value));
 }
 
 TracedDictionary TracedValue::WriteDictionary() && {
@@ -115,19 +83,10 @@
   PERFETTO_DCHECK(checked_scope_.is_active());
   checked_scope_.Reset();
 
-  if (nested_context_) {
-    PERFETTO_DCHECK(!nested_context_->is_finalized());
-    nested_context_->set_nested_type(
-        protos::pbzero::DebugAnnotation_NestedValue_NestedType_DICT);
-    return TracedDictionary(nested_context_, checked_scope_.parent_scope());
-  } else {
-    PERFETTO_DCHECK(!root_context_->is_finalized());
-    protos::pbzero::DebugAnnotation::NestedValue* value =
-        root_context_->set_nested_value();
-    value->set_nested_type(
-        protos::pbzero::DebugAnnotation_NestedValue_NestedType_DICT);
-    return TracedDictionary(value, checked_scope_.parent_scope());
-  }
+  PERFETTO_DCHECK(!context_->is_finalized());
+  return TracedDictionary(context_,
+                          protos::pbzero::DebugAnnotation::kDictEntries,
+                          checked_scope_.parent_scope());
 }
 
 TracedArray TracedValue::WriteArray() && {
@@ -136,24 +95,13 @@
   PERFETTO_DCHECK(checked_scope_.is_active());
   checked_scope_.Reset();
 
-  if (nested_context_) {
-    PERFETTO_DCHECK(!nested_context_->is_finalized());
-    nested_context_->set_nested_type(
-        protos::pbzero::DebugAnnotation_NestedValue_NestedType_ARRAY);
-    return TracedArray(nested_context_, checked_scope_.parent_scope());
-  } else {
-    PERFETTO_DCHECK(!root_context_->is_finalized());
-    protos::pbzero::DebugAnnotation::NestedValue* value =
-        root_context_->set_nested_value();
-    value->set_nested_type(
-        protos::pbzero::DebugAnnotation_NestedValue_NestedType_ARRAY);
-    return TracedArray(value, checked_scope_.parent_scope());
-  }
+  PERFETTO_DCHECK(!context_->is_finalized());
+  return TracedArray(context_, checked_scope_.parent_scope());
 }
 
 TracedValue TracedArray::AppendItem() {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  return TracedValue(value_->add_array_values(), &checked_scope_);
+  return TracedValue(context_->add_array_values(), &checked_scope_);
 }
 
 TracedDictionary TracedArray::AppendDictionary() {
@@ -168,14 +116,18 @@
 
 TracedValue TracedDictionary::AddItem(StaticString key) {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  value_->add_dict_keys(key.value);
-  return TracedValue(value_->add_dict_values(), &checked_scope_);
+  protos::pbzero::DebugAnnotation* item =
+      message_->BeginNestedMessage<protos::pbzero::DebugAnnotation>(field_id_);
+  item->set_name(key.value);
+  return TracedValue(item, &checked_scope_);
 }
 
 TracedValue TracedDictionary::AddItem(DynamicString key) {
   PERFETTO_DCHECK(checked_scope_.is_active());
-  value_->add_dict_keys(key.value, key.length);
-  return TracedValue(value_->add_dict_values(), &checked_scope_);
+  protos::pbzero::DebugAnnotation* item =
+      message_->BeginNestedMessage<protos::pbzero::DebugAnnotation>(field_id_);
+  item->set_name(key.value);
+  return TracedValue(item, &checked_scope_);
 }
 
 TracedDictionary TracedDictionary::AddDictionary(StaticString key) {
diff --git a/src/tracing/traced_value_unittest.cc b/src/tracing/traced_value_unittest.cc
index 5ad766a..66c86db 100644
--- a/src/tracing/traced_value_unittest.cc
+++ b/src/tracing/traced_value_unittest.cc
@@ -91,7 +91,7 @@
 ASSERT_TYPE_SUPPORTED(const int*);
 ASSERT_TYPE_SUPPORTED(void*);
 ASSERT_TYPE_SUPPORTED(const void*);
-ASSERT_TYPE_SUPPORTED(nullptr_t);
+ASSERT_TYPE_SUPPORTED(std::nullptr_t);
 ASSERT_TYPE_NOT_SUPPORTED(NonSupportedType*);
 ASSERT_TYPE_NOT_SUPPORTED(const NonSupportedType*);
 
@@ -147,11 +147,9 @@
     dict.AddItem("truncated_string").WriteString("truncated_string", 9);
     dict.AddItem("ptr").WritePointer(reinterpret_cast<void*>(0x1234));
   }
-  // TODO(altimin): Nested pointers are recorded as ints due to proto
-  // limitation. Fix after sorting out the NestedValue.
   EXPECT_EQ(
       "{bool:true,double:0,int:2014,string:string,truncated_string:truncated,"
-      "ptr:4660}",
+      "ptr:0x1234}",
       internal::DebugAnnotationToString(message.SerializeAsString()));
 }
 
@@ -166,9 +164,7 @@
     dict.Add("string", "string");
     dict.Add("ptr", reinterpret_cast<void*>(0x1234));
   }
-  // TODO(altimin): Nested pointers are recorded as ints due to proto
-  // limitation. Fix after sorting out the NestedValue.
-  EXPECT_EQ("{bool:true,double:0,int:2014,string:string,ptr:4660}",
+  EXPECT_EQ("{bool:true,double:0,int:2014,string:string,ptr:0x1234}",
             internal::DebugAnnotationToString(message.SerializeAsString()));
 }
 
@@ -254,7 +250,7 @@
 
 namespace {
 
-class HasConvertorMember {
+class HasWriteIntoTracedValueConvertorMember {
  public:
   void WriteIntoTracedValue(TracedValue context) const {
     auto dict = std::move(context).WriteDictionary();
@@ -263,7 +259,17 @@
   }
 };
 
-class HasExternalConvertor {};
+class HasWriteIntoTraceConvertorMember {
+ public:
+  void WriteIntoTrace(TracedValue context) const {
+    auto dict = std::move(context).WriteDictionary();
+    dict.Add("int", 42);
+    dict.Add("bool", false);
+  }
+};
+
+class HasExternalWriteIntoTraceConvertor {};
+class HasExternalWriteIntoTracedValueConvertor {};
 
 class HasAllConversionMethods {
  public:
@@ -306,9 +312,18 @@
 }  // namespace
 
 template <>
-struct TraceFormatTraits<HasExternalConvertor> {
-  static void WriteIntoTracedValue(TracedValue context,
-                                   const HasExternalConvertor&) {
+struct TraceFormatTraits<HasExternalWriteIntoTraceConvertor> {
+  static void WriteIntoTrace(TracedValue context,
+                             const HasExternalWriteIntoTraceConvertor&) {
+    std::move(context).WriteString("TraceFormatTraits::WriteIntoTrace");
+  }
+};
+
+template <>
+struct TraceFormatTraits<HasExternalWriteIntoTracedValueConvertor> {
+  static void WriteIntoTracedValue(
+      TracedValue context,
+      const HasExternalWriteIntoTracedValueConvertor&) {
     std::move(context).WriteString("TraceFormatTraits::WriteIntoTracedValue");
   }
 };
@@ -330,8 +345,10 @@
   return internal::DebugAnnotationToString(message.SerializeAsString());
 }
 
-ASSERT_TYPE_SUPPORTED(HasConvertorMember);
-ASSERT_TYPE_SUPPORTED(HasExternalConvertor);
+ASSERT_TYPE_SUPPORTED(HasWriteIntoTraceConvertorMember);
+ASSERT_TYPE_SUPPORTED(HasWriteIntoTracedValueConvertorMember);
+ASSERT_TYPE_SUPPORTED(HasExternalWriteIntoTraceConvertor);
+ASSERT_TYPE_SUPPORTED(HasExternalWriteIntoTracedValueConvertor);
 ASSERT_TYPE_SUPPORTED(HasAllConversionMethods);
 
 ASSERT_TYPE_SUPPORTED(HasConstWriteMember);
@@ -368,19 +385,27 @@
 ASSERT_TYPE_SUPPORTED(std::unique_ptr<const HasConstAndNonConstWriteMember*>);
 
 TEST(TracedValueTest, UserDefinedConvertors) {
-  HasConvertorMember value1;
+  HasWriteIntoTraceConvertorMember value1;
   EXPECT_EQ(TracedValueToString(value1), "{int:42,bool:false}");
   EXPECT_EQ(TracedValueToString(&value1), "{int:42,bool:false}");
 
-  HasExternalConvertor value2;
-  EXPECT_EQ(TracedValueToString(value2),
+  HasWriteIntoTracedValueConvertorMember value2;
+  EXPECT_EQ(TracedValueToString(value2), "{int:42,bool:false}");
+  EXPECT_EQ(TracedValueToString(&value2), "{int:42,bool:false}");
+
+  HasExternalWriteIntoTracedValueConvertor value3;
+  EXPECT_EQ(TracedValueToString(value3),
             "TraceFormatTraits::WriteIntoTracedValue");
-  EXPECT_EQ(TracedValueToString(&value2),
+  EXPECT_EQ(TracedValueToString(&value3),
             "TraceFormatTraits::WriteIntoTracedValue");
 
-  HasAllConversionMethods value3;
-  EXPECT_EQ(TracedValueToString(value3), "T::WriteIntoTracedValue");
-  EXPECT_EQ(TracedValueToString(&value3), "T::WriteIntoTracedValue");
+  HasExternalWriteIntoTraceConvertor value4;
+  EXPECT_EQ(TracedValueToString(value4), "TraceFormatTraits::WriteIntoTrace");
+  EXPECT_EQ(TracedValueToString(&value4), "TraceFormatTraits::WriteIntoTrace");
+
+  HasAllConversionMethods value5;
+  EXPECT_EQ(TracedValueToString(value5), "T::WriteIntoTracedValue");
+  EXPECT_EQ(TracedValueToString(&value5), "T::WriteIntoTracedValue");
 }
 
 TEST(TracedValueTest, WriteAsLambda) {
@@ -580,4 +605,18 @@
             }));
 }
 
+TEST(TracedValueTest, EmptyDict) {
+  EXPECT_EQ("{}", TracedValueToString([&](TracedValue context) {
+              auto dict = std::move(context).WriteDictionary();
+            }));
+}
+
+TEST(TracedValueTest, EmptyArray) {
+  // For now we do not distinguish between empty arrays and empty dicts on proto
+  // level as trace processor ignores them anyway.
+  EXPECT_EQ("{}", TracedValueToString([&](TracedValue context) {
+              auto array = std::move(context).WriteArray();
+            }));
+}
+
 }  // namespace perfetto
diff --git a/src/tracing/tracing.cc b/src/tracing/tracing.cc
index 7c61fb8..019341e 100644
--- a/src/tracing/tracing.cc
+++ b/src/tracing/tracing.cc
@@ -44,6 +44,9 @@
 
   // Make sure the headers and implementation files agree on the build config.
   PERFETTO_CHECK(args.dcheck_is_on_ == PERFETTO_DCHECK_IS_ON());
+  if (args.log_message_callback) {
+    SetLogMessageCallback(args.log_message_callback);
+  }
   internal::TracingMuxerImpl::InitializeInstance(args);
   internal::TrackRegistry::InitializeInstance();
   g_was_initialized = true;
diff --git a/src/tracing/track_event_legacy.cc b/src/tracing/track_event_legacy.cc
index 9a687c6..744416f 100644
--- a/src/tracing/track_event_legacy.cc
+++ b/src/tracing/track_event_legacy.cc
@@ -22,12 +22,11 @@
 namespace legacy {
 
 template <>
-bool ConvertThreadId(const PerfettoLegacyCurrentThreadId&,
-                     uint64_t*,
-                     int32_t*,
-                     int32_t*) {
-  // No need to override anything for events on to the current thread.
-  return false;
+ThreadTrack ConvertThreadId(const PerfettoLegacyCurrentThreadId&) {
+  // Because of the short-circuit in PERFETTO_INTERNAL_LEGACY_EVENT, we should
+  // never get here.
+  PERFETTO_DCHECK(false);
+  return ThreadTrack::Current();
 }
 
 }  // namespace legacy
diff --git a/src/tracing/track_event_state_tracker.cc b/src/tracing/track_event_state_tracker.cc
index 4385f62..7cb1b09 100644
--- a/src/tracing/track_event_state_tracker.cc
+++ b/src/tracing/track_event_state_tracker.cc
@@ -143,8 +143,10 @@
   parsed_event.name_hash = name_hash;
   delegate.OnTrackEvent(*track, parsed_event);
 
-  if (track_event.type() == protos::pbzero::TrackEvent::TYPE_SLICE_END)
+  if (track_event.type() == protos::pbzero::TrackEvent::TYPE_SLICE_END &&
+      !track->stack.empty()) {
     track->stack.pop_back();
+  }
 }
 
 // static
diff --git a/src/tracing/virtual_destructors.cc b/src/tracing/virtual_destructors.cc
index d548c8e..272d013 100644
--- a/src/tracing/virtual_destructors.cc
+++ b/src/tracing/virtual_destructors.cc
@@ -26,7 +26,22 @@
 namespace perfetto {
 namespace internal {
 
-TracingTLS::~TracingTLS() = default;
+TracingTLS::~TracingTLS() {
+  // Avoid entering trace points while the thread is being torn down.
+  // This is the problem: when a thread exits, the at-thread-exit destroys the
+  // TracingTLS. As part of that the various TraceWriter for the active data
+  // sources are destroyd. A TraceWriter dtor will issue a PostTask on the IPC
+  // thread to issue a final flush and unregister its ID with the service.
+  // The PostTask, in chromium, might have a trace event that will try to
+  // re-enter the tracing system.
+  // We fix this by resetting the TLS key to the TracingTLS object that is
+  // being destroyed in the platform impl (platform_posix.cc,
+  // platform_windows.cc, chromium's platform.cc). We carefully rely on the fact
+  // that all the tracing path that will be invoked during thread exit will
+  // early out if |is_in_trace_point| == true and will not depend on the other
+  // TLS state that has been destroyed.
+  is_in_trace_point = true;
+}
 
 }  // namespace internal
 
diff --git a/test/ci/ui_tests.sh b/test/ci/ui_tests.sh
index 9a1fa3a..fe8b1aa 100755
--- a/test/ci/ui_tests.sh
+++ b/test/ci/ui_tests.sh
@@ -16,8 +16,8 @@
 INSTALL_BUILD_DEPS_ARGS="--ui"
 source $(dirname ${BASH_SOURCE[0]})/common.sh
 
-tools/node ui/build.js --out ${OUT_PATH}
+ui/build --out ${OUT_PATH}
 
 cp -a ${OUT_PATH}/ui/dist/ /ci/artifacts/ui
 
-tools/node ui/build.js --out ${OUT_PATH} --no-build --run-tests
+ui/run-unittests --out ${OUT_PATH} --no-build
diff --git a/test/configs/BUILD.gn b/test/configs/BUILD.gn
index 4f77441..4c93db1a 100644
--- a/test/configs/BUILD.gn
+++ b/test/configs/BUILD.gn
@@ -35,11 +35,16 @@
     "client_api.cfg",
     "ftrace.cfg",
     "ftrace_largebuffer.cfg",
+    "ftrace_with_filter.cfg",
+    "ftrace_with_ksyms.cfg",
     "heapprofd.cfg",
     "long_trace.cfg",
+    "mm_events.cfg",
     "scheduling.cfg",
     "summary.cfg",
     "sys_stats.cfg",
+    "thermal.cfg",
+    "traced_perf.cfg",
   ]
 
   outputs = [ "$root_out_dir/{{source_file_part}}.protobuf" ]
diff --git a/test/configs/ftrace_with_filter.cfg b/test/configs/ftrace_with_filter.cfg
new file mode 100644
index 0000000..54c3161
--- /dev/null
+++ b/test/configs/ftrace_with_filter.cfg
@@ -0,0 +1,36 @@
+buffers {
+  size_kb: 65536
+}
+
+data_sources {
+  config {
+    name: "linux.ftrace"
+    target_buffer: 0
+    ftrace_config {
+      ftrace_events: "sched/sched_process_exec"
+      ftrace_events: "sched/sched_process_exit"
+      ftrace_events: "sched/sched_process_fork"
+      ftrace_events: "sched/sched_process_free"
+      ftrace_events: "sched/sched_process_hang"
+      ftrace_events: "sched/sched_process_wait"
+      ftrace_events: "sched/sched_switch"
+      ftrace_events: "sched/sched_wakeup_new"
+      ftrace_events: "sched/sched_wakeup"
+      ftrace_events: "sched/sched_waking"
+    }
+  }
+}
+
+data_sources {
+  config {
+    name: "linux.process_stats"
+    target_buffer: 0
+  }
+}
+
+trace_filter {
+  # A very minimal bytecode which allows only sched_switch and nothing more.
+  bytecode: "\013\001\000\013\002\101\121\151\321\002\000\011\023\003\031\000\012\002\043\004\000\012\007\000\273\341\337\347\016"
+}
+
+duration_ms: 10000
diff --git a/test/cts/heapprofd_java_test_cts.cc b/test/cts/heapprofd_java_test_cts.cc
index 8888009..a0205f1 100644
--- a/test/cts/heapprofd_java_test_cts.cc
+++ b/test/cts/heapprofd_java_test_cts.cc
@@ -128,11 +128,61 @@
   std::string app_name = "android.perfetto.cts.app.release";
   const auto& packets = ProfileRuntime(app_name);
 
-  if (IsDebuggableBuild())
+  if (!IsUserBuild())
     AssertGraphPresent(packets);
   else
     AssertNoProfileContents(packets);
 }
 
+TEST(HeapprofdJavaCtsTest, DebuggableAppRuntimeByPid) {
+  std::string app_name = "android.perfetto.cts.app.debuggable";
+
+  base::TestTaskRunner task_runner;
+
+  // (re)start the target app's main activity
+  if (IsAppRunning(app_name)) {
+    StopApp(app_name, "old.app.stopped", &task_runner);
+    task_runner.RunUntilCheckpoint("old.app.stopped", 1000 /*ms*/);
+  }
+  StartAppActivity(app_name, "MainActivity", "target.app.running", &task_runner,
+                   /*delay_ms=*/100);
+  task_runner.RunUntilCheckpoint("target.app.running", 1000 /*ms*/);
+  // If we try to dump too early in app initialization, we sometimes deadlock.
+  sleep(1);
+
+  int target_pid = PidForProcessName(app_name);
+  ASSERT_NE(target_pid, -1);
+
+  // set up tracing
+  TestHelper helper(&task_runner);
+  helper.ConnectConsumer();
+  helper.WaitForConsumerConnect();
+
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(40 * 1024);
+  trace_config.set_duration_ms(6000);
+  trace_config.set_unique_session_name(RandomSessionName().c_str());
+
+  auto* ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("android.java_hprof");
+  ds_config->set_target_buffer(0);
+
+  protos::gen::JavaHprofConfig java_hprof_config;
+  java_hprof_config.add_pid(static_cast<uint64_t>(target_pid));
+  ds_config->set_java_hprof_config_raw(java_hprof_config.SerializeAsString());
+
+  // start tracing
+  helper.StartTracing(trace_config);
+  helper.WaitForTracingDisabled();
+  helper.ReadData();
+  helper.WaitForReadData();
+  PERFETTO_CHECK(IsAppRunning(app_name));
+  StopApp(app_name, "new.app.stopped", &task_runner);
+  task_runner.RunUntilCheckpoint("new.app.stopped", 1000 /*ms*/);
+
+  const auto& packets = helper.trace();
+  AssertGraphPresent(packets);
+}
+
 }  // namespace
 }  // namespace perfetto
diff --git a/test/cts/heapprofd_test_cts.cc b/test/cts/heapprofd_test_cts.cc
index 0559b47..69aa344 100644
--- a/test/cts/heapprofd_test_cts.cc
+++ b/test/cts/heapprofd_test_cts.cc
@@ -36,14 +36,14 @@
 namespace perfetto {
 namespace {
 
-constexpr uint64_t kTestSamplingInterval = 512;
-// Size of individual (repeated) allocations done by the test apps (must be
-// kept in sync with their sources).
-// Tests rely on the sampling behaviour where large allocations are recorded
-// at their actual size, so kExpectedIndividualAllocSz needs to be greater
-// than GetPassthroughTreshold(kExpectedIndividualAllocSz). See
-// src/profiling/memory/sampler.h.
+// Size of individual (repeated) allocations done by the test apps (must be kept
+// in sync with their sources).
+constexpr uint64_t kTestSamplingInterval = 4096;
 constexpr uint64_t kExpectedIndividualAllocSz = 4153;
+// Tests rely on the sampling behaviour where allocations larger than the
+// sampling interval are recorded at their actual size.
+static_assert(kExpectedIndividualAllocSz > kTestSamplingInterval,
+              "kTestSamplingInterval invalid");
 
 std::string RandomSessionName() {
   std::random_device rd;
@@ -58,8 +58,7 @@
 }
 
 std::vector<protos::gen::TracePacket> ProfileRuntime(
-    const std::string& app_name,
-    const bool enable_extra_guardrails = false) {
+    const std::string& app_name) {
   base::TestTaskRunner task_runner;
 
   // (re)start the target app's main activity
@@ -79,7 +78,6 @@
   TraceConfig trace_config;
   trace_config.add_buffers()->set_size_kb(10 * 1024);
   trace_config.set_duration_ms(4000);
-  trace_config.set_enable_extra_guardrails(enable_extra_guardrails);
   trace_config.set_unique_session_name(RandomSessionName().c_str());
 
   auto* ds_config = trace_config.add_data_sources()->mutable_config();
@@ -214,30 +212,6 @@
   StopApp(app_name);
 }
 
-TEST(HeapprofdCtsTest, ProfileableAppRuntimeExtraGuardrails) {
-  std::string app_name = "android.perfetto.cts.app.profileable";
-  const auto& packets = ProfileRuntime(app_name,
-                                       /*enable_extra_guardrails=*/true);
-
-  if (IsUserBuild())
-    AssertNoProfileContents(packets);
-  else
-    AssertExpectedAllocationsPresent(packets);
-  StopApp(app_name);
-}
-
-TEST(HeapprofdCtsTest, ProfileableAppStartupExtraGuardrails) {
-  std::string app_name = "android.perfetto.cts.app.profileable";
-  const auto& packets = ProfileStartup(app_name,
-                                       /*enable_extra_guardrails=*/
-                                       true);
-  if (IsUserBuild())
-    AssertNoProfileContents(packets);
-  else
-    AssertExpectedAllocationsPresent(packets);
-  StopApp(app_name);
-}
-
 TEST(HeapprofdCtsTest, ReleaseAppRuntime) {
   std::string app_name = "android.perfetto.cts.app.release";
   const auto& packets = ProfileRuntime(app_name);
diff --git a/test/cts/traced_perf_test_cts.cc b/test/cts/traced_perf_test_cts.cc
index f0a2631..7b867cd 100644
--- a/test/cts/traced_perf_test_cts.cc
+++ b/test/cts/traced_perf_test_cts.cc
@@ -57,9 +57,7 @@
   return result;
 }
 
-std::vector<protos::gen::TracePacket> ProfileSystemWide(
-    std::string app_name,
-    bool enable_extra_guardrails = false) {
+std::vector<protos::gen::TracePacket> ProfileSystemWide(std::string app_name) {
   base::TestTaskRunner task_runner;
 
   // (re)start the target app's main activity
@@ -81,7 +79,6 @@
   trace_config.add_buffers()->set_size_kb(20 * 1024);
   trace_config.set_duration_ms(3000);
   trace_config.set_data_source_stop_timeout_ms(8000);
-  trace_config.set_enable_extra_guardrails(enable_extra_guardrails);
   trace_config.set_unique_session_name(RandomSessionName().c_str());
 
   auto* ds_config = trace_config.add_data_sources()->mutable_config();
@@ -173,24 +170,6 @@
   StopApp(app_name);
 }
 
-TEST(TracedPerfCtsTest, SystemWideDebuggableAppExtraGuardrails) {
-  if (!HasPerfLsmHooks())
-    GTEST_SKIP() << "skipped due to lack of perf_event_open LSM hooks";
-
-  std::string app_name = "android.perfetto.cts.app.debuggable";
-  const auto& packets =
-      ProfileSystemWide(app_name, /*enable_extra_guardrails=*/true);
-  int app_pid = PidForProcessName(app_name);
-  ASSERT_GT(app_pid, 0) << "failed to find pid for target process";
-
-  if (!IsUserBuild())
-    AssertHasSampledStacksForPid(packets, app_pid);
-  else
-    AssertNoStacksForPid(packets, app_pid);
-  PERFETTO_CHECK(IsAppRunning(app_name));
-  StopApp(app_name);
-}
-
 TEST(TracedPerfCtsTest, SystemWideProfileableApp) {
   if (!HasPerfLsmHooks())
     GTEST_SKIP() << "skipped due to lack of perf_event_open LSM hooks";
@@ -205,24 +184,6 @@
   StopApp(app_name);
 }
 
-TEST(TracedPerfCtsTest, SystemWideProfileableAppExtraGuardrails) {
-  if (!HasPerfLsmHooks())
-    GTEST_SKIP() << "skipped due to lack of perf_event_open LSM hooks";
-
-  std::string app_name = "android.perfetto.cts.app.profileable";
-  const auto& packets =
-      ProfileSystemWide(app_name, /*enable_extra_guardrails=*/true);
-  int app_pid = PidForProcessName(app_name);
-  ASSERT_GT(app_pid, 0) << "failed to find pid for target process";
-
-  if (!IsUserBuild())
-    AssertHasSampledStacksForPid(packets, app_pid);
-  else
-    AssertNoStacksForPid(packets, app_pid);
-  PERFETTO_CHECK(IsAppRunning(app_name));
-  StopApp(app_name);
-}
-
 TEST(TracedPerfCtsTest, SystemWideReleaseApp) {
   if (!HasPerfLsmHooks())
     GTEST_SKIP() << "skipped due to lack of perf_event_open LSM hooks";
@@ -241,24 +202,5 @@
   StopApp(app_name);
 }
 
-TEST(TracedPerfCtsTest, SystemWideReleaseAppExtraGuardrails) {
-  if (!HasPerfLsmHooks())
-    GTEST_SKIP() << "skipped due to lack of perf_event_open LSM hooks";
-
-  std::string app_name = "android.perfetto.cts.app.release";
-  const auto& packets =
-      ProfileSystemWide(app_name, /*enable_extra_guardrails=*/true);
-  int app_pid = PidForProcessName(app_name);
-  ASSERT_GT(app_pid, 0) << "failed to find pid for target process";
-
-  if (!IsUserBuild())
-    AssertHasSampledStacksForPid(packets, app_pid);
-  else
-    AssertNoStacksForPid(packets, app_pid);
-
-  PERFETTO_CHECK(IsAppRunning(app_name));
-  StopApp(app_name);
-}
-
 }  // namespace
 }  // namespace perfetto
diff --git a/test/end_to_end_integrationtest.cc b/test/end_to_end_integrationtest.cc
index 64a7c80..435690d 100644
--- a/test/end_to_end_integrationtest.cc
+++ b/test/end_to_end_integrationtest.cc
@@ -30,6 +30,7 @@
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/subprocess.h"
 #include "perfetto/ext/base/temp_file.h"
+#include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/ipc/basic_types.h"
 #include "perfetto/ext/traced/traced.h"
 #include "perfetto/ext/tracing/core/commit_data_request.h"
@@ -1175,6 +1176,11 @@
   EXPECT_THAT(stderr_, HasSubstr("Cannot specify a trace config"));
 }
 
+TEST_F(PerfettoCmdlineTest, NoSanitizers(Version)) {
+  auto perfetto = ExecPerfetto({"--version"});
+  EXPECT_EQ(0, perfetto.Run(&stderr_)) << stderr_;
+}
+
 TEST_F(PerfettoCmdlineTest, NoSanitizers(TxtConfig)) {
   std::string cfg("duration_ms: 100");
   auto perfetto = ExecPerfetto({"-c", "-", "--txt", "-o", "-"}, cfg);
diff --git a/test/synth_common.py b/test/synth_common.py
index 0cf5fdd..6a94c34 100644
--- a/test/synth_common.py
+++ b/test/synth_common.py
@@ -214,6 +214,35 @@
     battery_count.current_ua = curr_ua
     battery_count.current_avg_ua = curr_avg_ua
 
+  def add_binder_transaction(self, transaction_id, ts_start, ts_end, tid, pid,
+                             reply_id, reply_ts_start, reply_ts_end, reply_tid,
+                             reply_pid):
+    # Binder transaction start.
+    ftrace = self.__add_ftrace_event(ts_start, tid)
+    binder_transaction = ftrace.binder_transaction
+    binder_transaction.debug_id = transaction_id
+    binder_transaction.to_proc = reply_pid
+    binder_transaction.to_thread = reply_tid
+    binder_transaction.reply = False
+
+    # Binder reply start
+    ftrace = self.__add_ftrace_event(reply_ts_start, reply_tid)
+    binder_transaction_received = ftrace.binder_transaction_received
+    binder_transaction_received.debug_id = transaction_id
+
+    # Binder reply finish
+    ftrace = self.__add_ftrace_event(reply_ts_end, reply_tid)
+    reply_binder_transaction = ftrace.binder_transaction
+    reply_binder_transaction.debug_id = reply_id
+    reply_binder_transaction.to_proc = pid
+    reply_binder_transaction.to_thread = tid
+    reply_binder_transaction.reply = True
+
+    # Binder transaction finish
+    ftrace = self.__add_ftrace_event(ts_end, tid)
+    reply_binder_transaction_received = ftrace.binder_transaction_received
+    reply_binder_transaction_received.debug_id = reply_id
+
   def add_battery_counters_no_curr_ua(self, ts, charge_uah, cap_prct,
                                       curr_avg_ua):
     self.packet = self.trace.packet.add()
diff --git a/test/test_helper.h b/test/test_helper.h
index 4ac4c46..d9a99b9 100644
--- a/test/test_helper.h
+++ b/test/test_helper.h
@@ -34,8 +34,7 @@
 #include "test/fake_producer.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
-// TODO(primiano): uncomment in next CL.
-// #include "src/tracing/ipc/shared_memory_windows.h"
+#include "src/tracing/ipc/shared_memory_windows.h"
 #else
 #include "src/traced/probes/probes_producer.h"
 #include "src/tracing/ipc/posix_shared_memory.h"
@@ -168,12 +167,12 @@
 
   void CreateProducerProvidedSmb() {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
-    // TODO(primiano): in next CLs introduce SharedMemoryWindows.
+    SharedMemoryWindows::Factory factory;
 #else
     PosixSharedMemory::Factory factory;
+#endif
     shm_ = factory.CreateSharedMemory(1024 * 1024);
     shm_arbiter_ = SharedMemoryArbiter::CreateUnboundInstance(shm_.get(), 4096);
-#endif
   }
 
   void ProduceStartupEventBatch(const protos::gen::TestConfig& config,
diff --git a/test/trace_processor/chrome/chrome_threads.out b/test/trace_processor/chrome/chrome_threads.out
index 6e6069e..23e62c8 100644
--- a/test/trace_processor/chrome/chrome_threads.out
+++ b/test/trace_processor/chrome/chrome_threads.out
@@ -1,8 +1,8 @@
 
 "tid","name","is_main_thread","canonical_name"
-0,"[NULL]",0,"[NULL]"
-0,"[NULL]",0,"[NULL]"
-0,"[NULL]",0,"[NULL]"
+0,"[NULL]",0,"Unknown"
+0,"[NULL]",0,"Unknown"
+0,"[NULL]",0,"Unknown"
 0,"swapper",0,"swapper"
 17547,"CrBrowserMain",1,"CrProcessMain"
 17578,"CrBrowserMain",1,"CrProcessMain"
@@ -11,7 +11,7 @@
 18247,"Chrome_IOThread",0,"Chrome_IOThread"
 18248,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
 18249,"MemoryInfra",0,"MemoryInfra"
-18250,"[NULL]",1,"[NULL]"
+18250,"[NULL]",1,"Unknown"
 18255,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
 18256,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
 18259,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
@@ -20,7 +20,7 @@
 18273,"BrowserWatchdog",0,"BrowserWatchdog"
 18274,"NetworkService",0,"NetworkService"
 18275,"ThreadPoolSingleThreadSharedForegroundBlocking0",0,"ThreadPoolSingleThreadSharedForegroundBlocking0"
-18277,"[NULL]",1,"[NULL]"
+18277,"[NULL]",1,"Unknown"
 18283,"CrRendererMain",0,"CrProcessMain"
 18304,"CrGpuMain",0,"CrProcessMain"
 18305,"CompositorTileWorker1",0,"CompositorTileWorker1"
diff --git a/test/trace_processor/chrome/chrome_threads_android_systrace.out b/test/trace_processor/chrome/chrome_threads_android_systrace.out
index 19ec5e4..ab31779 100644
--- a/test/trace_processor/chrome/chrome_threads_android_systrace.out
+++ b/test/trace_processor/chrome/chrome_threads_android_systrace.out
@@ -1,241 +1,241 @@
 
 "tid","name","is_main_thread","canonical_name"
-0,"[NULL]",0,"[NULL]"
-0,"[NULL]",0,"[NULL]"
-0,"[NULL]",0,"[NULL]"
-0,"[NULL]",0,"[NULL]"
-0,"[NULL]",0,"[NULL]"
-0,"[NULL]",0,"[NULL]"
-0,"[NULL]",0,"[NULL]"
+0,"[NULL]",0,"Unknown"
+0,"[NULL]",0,"Unknown"
+0,"[NULL]",0,"Unknown"
+0,"[NULL]",0,"Unknown"
+0,"[NULL]",0,"Unknown"
+0,"[NULL]",0,"Unknown"
+0,"[NULL]",0,"Unknown"
 0,"swapper",0,"swapper"
 8982,"com.chrome.dev",1,"com.chrome.dev"
-8991,"[NULL]",0,"[NULL]"
-8992,"[NULL]",0,"[NULL]"
-8993,"[NULL]",0,"[NULL]"
-8994,"[NULL]",0,"[NULL]"
-8995,"[NULL]",0,"[NULL]"
-8996,"[NULL]",0,"[NULL]"
-8997,"[NULL]",0,"[NULL]"
-8998,"[NULL]",0,"[NULL]"
-9001,"[NULL]",0,"[NULL]"
+8991,"[NULL]",0,"Unknown"
+8992,"[NULL]",0,"Unknown"
+8993,"[NULL]",0,"Unknown"
+8994,"[NULL]",0,"Unknown"
+8995,"[NULL]",0,"Unknown"
+8996,"[NULL]",0,"Unknown"
+8997,"[NULL]",0,"Unknown"
+8998,"[NULL]",0,"Unknown"
+9001,"[NULL]",0,"Unknown"
 9002,"Binder:8982_2",0,"Binder:8982_2"
-9006,"[NULL]",0,"[NULL]"
-9030,"[NULL]",0,"[NULL]"
-9032,"[NULL]",0,"[NULL]"
-9037,"[NULL]",0,"[NULL]"
+9006,"[NULL]",0,"Unknown"
+9030,"[NULL]",0,"Unknown"
+9032,"[NULL]",0,"Unknown"
+9037,"[NULL]",0,"Unknown"
 9057,"GoogleApiHandle",0,"GoogleApiHandle"
-9063,"[NULL]",0,"[NULL]"
-9064,"[NULL]",0,"[NULL]"
-9065,"[NULL]",1,"[NULL]"
+9063,"[NULL]",0,"Unknown"
+9064,"[NULL]",0,"Unknown"
+9065,"[NULL]",1,"Unknown"
 9076,"HangWatcher",0,"HangWatcher"
 9077,"ThreadPoolServi",0,"ThreadPoolServi"
-9079,"[NULL]",0,"[NULL]"
-9080,"[NULL]",0,"[NULL]"
-9081,"[NULL]",0,"[NULL]"
+9079,"[NULL]",0,"Unknown"
+9080,"[NULL]",0,"Unknown"
+9081,"[NULL]",0,"Unknown"
 9082,"Chrome_IOThread",0,"Chrome_IOThread"
 9084,"MemoryInfra",0,"MemoryInfra"
-9085,"[NULL]",0,"[NULL]"
+9085,"[NULL]",0,"Unknown"
 9086,"PCScan",0,"PCScan"
-9089,"[NULL]",0,"[NULL]"
-9092,"[NULL]",0,"[NULL]"
-9093,"[NULL]",0,"[NULL]"
+9089,"[NULL]",0,"Unknown"
+9092,"[NULL]",0,"Unknown"
+9093,"[NULL]",0,"Unknown"
 9094,"NetworkService",0,"NetworkService"
-9095,"[NULL]",0,"[NULL]"
+9095,"[NULL]",0,"Unknown"
 9096,"ThreadPoolForeg",0,"ThreadPoolForeg"
-9101,"[NULL]",0,"[NULL]"
-9102,"[NULL]",1,"[NULL]"
-9107,"[NULL]",0,"[NULL]"
-9108,"[NULL]",0,"[NULL]"
-9109,"[NULL]",0,"[NULL]"
-9110,"[NULL]",0,"[NULL]"
-9111,"[NULL]",0,"[NULL]"
-9112,"[NULL]",0,"[NULL]"
-9114,"[NULL]",0,"[NULL]"
-9115,"[NULL]",0,"[NULL]"
-9116,"[NULL]",0,"[NULL]"
-9117,"[NULL]",0,"[NULL]"
-9118,"[NULL]",0,"[NULL]"
-9119,"[NULL]",1,"[NULL]"
-9121,"[NULL]",0,"[NULL]"
-9124,"[NULL]",0,"[NULL]"
-9132,"[NULL]",0,"[NULL]"
-9133,"[NULL]",0,"[NULL]"
-9134,"[NULL]",0,"[NULL]"
-9135,"[NULL]",0,"[NULL]"
-9136,"[NULL]",0,"[NULL]"
-9139,"[NULL]",0,"[NULL]"
-9140,"[NULL]",0,"[NULL]"
-9141,"[NULL]",0,"[NULL]"
-9142,"[NULL]",0,"[NULL]"
-9143,"[NULL]",0,"[NULL]"
-9144,"[NULL]",0,"[NULL]"
-9145,"[NULL]",0,"[NULL]"
-9146,"[NULL]",0,"[NULL]"
+9101,"[NULL]",0,"Unknown"
+9102,"[NULL]",1,"Unknown"
+9107,"[NULL]",0,"Unknown"
+9108,"[NULL]",0,"Unknown"
+9109,"[NULL]",0,"Unknown"
+9110,"[NULL]",0,"Unknown"
+9111,"[NULL]",0,"Unknown"
+9112,"[NULL]",0,"Unknown"
+9114,"[NULL]",0,"Unknown"
+9115,"[NULL]",0,"Unknown"
+9116,"[NULL]",0,"Unknown"
+9117,"[NULL]",0,"Unknown"
+9118,"[NULL]",0,"Unknown"
+9119,"[NULL]",1,"Unknown"
+9121,"[NULL]",0,"Unknown"
+9124,"[NULL]",0,"Unknown"
+9132,"[NULL]",0,"Unknown"
+9133,"[NULL]",0,"Unknown"
+9134,"[NULL]",0,"Unknown"
+9135,"[NULL]",0,"Unknown"
+9136,"[NULL]",0,"Unknown"
+9139,"[NULL]",0,"Unknown"
+9140,"[NULL]",0,"Unknown"
+9141,"[NULL]",0,"Unknown"
+9142,"[NULL]",0,"Unknown"
+9143,"[NULL]",0,"Unknown"
+9144,"[NULL]",0,"Unknown"
+9145,"[NULL]",0,"Unknown"
+9146,"[NULL]",0,"Unknown"
 9147,"SAFE_BROWSING_U",0,"SAFE_BROWSING_U"
 9148,"CrGpuMain",0,"CrProcessMain"
 9149,"CrRendererMain",0,"CrProcessMain"
-9150,"[NULL]",0,"[NULL]"
+9150,"[NULL]",0,"Unknown"
 9155,"ThreadPoolServi",0,"ThreadPoolServi"
 9156,"ThreadPoolForeg",0,"ThreadPoolForeg"
-9157,"[NULL]",0,"[NULL]"
-9159,"[NULL]",0,"[NULL]"
+9157,"[NULL]",0,"Unknown"
+9159,"[NULL]",0,"Unknown"
 9160,"ThreadPoolServi",0,"ThreadPoolServi"
 9161,"ThreadPoolForeg",0,"ThreadPoolForeg"
 9162,"ThreadPoolBackg",0,"ThreadPoolBackg"
 9165,"Chrome_ChildIOT",0,"Chrome_ChildIOT"
-9166,"[NULL]",0,"[NULL]"
+9166,"[NULL]",0,"Unknown"
 9167,"Chrome_ChildIOT",0,"Chrome_ChildIOT"
-9168,"[NULL]",0,"[NULL]"
-9169,"[NULL]",0,"[NULL]"
-9171,"[NULL]",0,"[NULL]"
-9172,"[NULL]",0,"[NULL]"
-9173,"[NULL]",0,"[NULL]"
-9174,"[NULL]",0,"[NULL]"
-9175,"[NULL]",0,"[NULL]"
+9168,"[NULL]",0,"Unknown"
+9169,"[NULL]",0,"Unknown"
+9171,"[NULL]",0,"Unknown"
+9172,"[NULL]",0,"Unknown"
+9173,"[NULL]",0,"Unknown"
+9174,"[NULL]",0,"Unknown"
+9175,"[NULL]",0,"Unknown"
 9176,"StackSamplingPr",0,"StackSamplingPr"
 9177,"StackSamplingPr",0,"StackSamplingPr"
 9181,"StackSamplingPr",0,"StackSamplingPr"
-9184,"[NULL]",0,"[NULL]"
-9185,"[NULL]",0,"[NULL]"
-9204,"[NULL]",0,"[NULL]"
+9184,"[NULL]",0,"Unknown"
+9185,"[NULL]",0,"Unknown"
+9204,"[NULL]",0,"Unknown"
 9221,"Database thread",0,"Database thread"
-9239,"[NULL]",0,"[NULL]"
-9240,"[NULL]",0,"[NULL]"
-9241,"[NULL]",0,"[NULL]"
-9242,"[NULL]",0,"[NULL]"
-9243,"[NULL]",0,"[NULL]"
-9267,"[NULL]",1,"[NULL]"
-9272,"[NULL]",0,"[NULL]"
-9273,"[NULL]",0,"[NULL]"
-9274,"[NULL]",0,"[NULL]"
-9275,"[NULL]",0,"[NULL]"
-9276,"[NULL]",0,"[NULL]"
-9277,"[NULL]",0,"[NULL]"
-9278,"[NULL]",0,"[NULL]"
-9279,"[NULL]",0,"[NULL]"
-9280,"[NULL]",0,"[NULL]"
-9281,"[NULL]",0,"[NULL]"
-9282,"[NULL]",0,"[NULL]"
-9283,"[NULL]",0,"[NULL]"
-9284,"[NULL]",0,"[NULL]"
-9285,"[NULL]",0,"[NULL]"
+9239,"[NULL]",0,"Unknown"
+9240,"[NULL]",0,"Unknown"
+9241,"[NULL]",0,"Unknown"
+9242,"[NULL]",0,"Unknown"
+9243,"[NULL]",0,"Unknown"
+9267,"[NULL]",1,"Unknown"
+9272,"[NULL]",0,"Unknown"
+9273,"[NULL]",0,"Unknown"
+9274,"[NULL]",0,"Unknown"
+9275,"[NULL]",0,"Unknown"
+9276,"[NULL]",0,"Unknown"
+9277,"[NULL]",0,"Unknown"
+9278,"[NULL]",0,"Unknown"
+9279,"[NULL]",0,"Unknown"
+9280,"[NULL]",0,"Unknown"
+9281,"[NULL]",0,"Unknown"
+9282,"[NULL]",0,"Unknown"
+9283,"[NULL]",0,"Unknown"
+9284,"[NULL]",0,"Unknown"
+9285,"[NULL]",0,"Unknown"
 9287,"CrRendererMain",0,"CrProcessMain"
 9289,"StackSamplingPr",0,"StackSamplingPr"
 9290,"ThreadPoolServi",0,"ThreadPoolServi"
 9291,"ThreadPoolForeg",0,"ThreadPoolForeg"
-9292,"[NULL]",0,"[NULL]"
+9292,"[NULL]",0,"Unknown"
 9293,"ThreadPoolForeg",0,"ThreadPoolForeg"
 9295,"Chrome_ChildIOT",0,"Chrome_ChildIOT"
-9296,"[NULL]",0,"[NULL]"
-9297,"[NULL]",0,"[NULL]"
-9298,"[NULL]",0,"[NULL]"
-9299,"[NULL]",0,"[NULL]"
-9300,"[NULL]",0,"[NULL]"
+9296,"[NULL]",0,"Unknown"
+9297,"[NULL]",0,"Unknown"
+9298,"[NULL]",0,"Unknown"
+9299,"[NULL]",0,"Unknown"
+9300,"[NULL]",0,"Unknown"
 9301,"ThreadPoolForeg",0,"ThreadPoolForeg"
-9302,"[NULL]",0,"[NULL]"
-9303,"[NULL]",0,"[NULL]"
+9302,"[NULL]",0,"Unknown"
+9303,"[NULL]",0,"Unknown"
 9523,"CrBrowserMain",1,"CrProcessMain"
-9533,"[NULL]",0,"[NULL]"
-9534,"[NULL]",0,"[NULL]"
-9535,"[NULL]",0,"[NULL]"
-9536,"[NULL]",0,"[NULL]"
-9537,"[NULL]",0,"[NULL]"
-9538,"[NULL]",0,"[NULL]"
-9539,"[NULL]",0,"[NULL]"
-9540,"[NULL]",0,"[NULL]"
-9542,"[NULL]",0,"[NULL]"
-9543,"[NULL]",0,"[NULL]"
-9544,"[NULL]",0,"[NULL]"
-9545,"[NULL]",0,"[NULL]"
-9551,"[NULL]",0,"[NULL]"
-9558,"[NULL]",0,"[NULL]"
-9559,"[NULL]",0,"[NULL]"
-9562,"[NULL]",0,"[NULL]"
-9563,"[NULL]",0,"[NULL]"
-9564,"[NULL]",1,"[NULL]"
+9533,"[NULL]",0,"Unknown"
+9534,"[NULL]",0,"Unknown"
+9535,"[NULL]",0,"Unknown"
+9536,"[NULL]",0,"Unknown"
+9537,"[NULL]",0,"Unknown"
+9538,"[NULL]",0,"Unknown"
+9539,"[NULL]",0,"Unknown"
+9540,"[NULL]",0,"Unknown"
+9542,"[NULL]",0,"Unknown"
+9543,"[NULL]",0,"Unknown"
+9544,"[NULL]",0,"Unknown"
+9545,"[NULL]",0,"Unknown"
+9551,"[NULL]",0,"Unknown"
+9558,"[NULL]",0,"Unknown"
+9559,"[NULL]",0,"Unknown"
+9562,"[NULL]",0,"Unknown"
+9563,"[NULL]",0,"Unknown"
+9564,"[NULL]",1,"Unknown"
 9574,"HangWatcher",0,"HangWatcher"
 9575,"ThreadPoolServiceThread",0,"ThreadPoolServiceThread"
 9576,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
 9579,"Chrome_IOThread",0,"Chrome_IOThread"
-9580,"[NULL]",0,"[NULL]"
-9581,"[NULL]",0,"[NULL]"
-9584,"[NULL]",0,"[NULL]"
-9585,"[NULL]",0,"[NULL]"
+9580,"[NULL]",0,"Unknown"
+9581,"[NULL]",0,"Unknown"
+9584,"[NULL]",0,"Unknown"
+9585,"[NULL]",0,"Unknown"
 9587,"NetworkService",0,"NetworkService"
 9588,"ThreadPoolSingleThreadSharedForegroundBlocking0",0,"ThreadPoolSingleThreadSharedForegroundBlocking0"
-9591,"[NULL]",0,"[NULL]"
-9592,"[NULL]",1,"[NULL]"
-9597,"[NULL]",1,"[NULL]"
-9600,"[NULL]",0,"[NULL]"
-9601,"[NULL]",0,"[NULL]"
-9603,"[NULL]",0,"[NULL]"
-9604,"[NULL]",0,"[NULL]"
-9605,"[NULL]",0,"[NULL]"
-9611,"[NULL]",0,"[NULL]"
-9612,"[NULL]",0,"[NULL]"
-9613,"[NULL]",0,"[NULL]"
-9614,"[NULL]",0,"[NULL]"
-9615,"[NULL]",0,"[NULL]"
-9616,"[NULL]",0,"[NULL]"
-9621,"[NULL]",0,"[NULL]"
-9622,"[NULL]",0,"[NULL]"
-9623,"[NULL]",0,"[NULL]"
-9624,"[NULL]",0,"[NULL]"
-9625,"[NULL]",0,"[NULL]"
-9627,"[NULL]",0,"[NULL]"
-9630,"[NULL]",0,"[NULL]"
-9637,"[NULL]",0,"[NULL]"
-9638,"[NULL]",0,"[NULL]"
-9639,"[NULL]",0,"[NULL]"
-9641,"[NULL]",0,"[NULL]"
-9642,"[NULL]",0,"[NULL]"
-9643,"[NULL]",0,"[NULL]"
-9645,"[NULL]",0,"[NULL]"
-9646,"[NULL]",0,"[NULL]"
+9591,"[NULL]",0,"Unknown"
+9592,"[NULL]",1,"Unknown"
+9597,"[NULL]",1,"Unknown"
+9600,"[NULL]",0,"Unknown"
+9601,"[NULL]",0,"Unknown"
+9603,"[NULL]",0,"Unknown"
+9604,"[NULL]",0,"Unknown"
+9605,"[NULL]",0,"Unknown"
+9611,"[NULL]",0,"Unknown"
+9612,"[NULL]",0,"Unknown"
+9613,"[NULL]",0,"Unknown"
+9614,"[NULL]",0,"Unknown"
+9615,"[NULL]",0,"Unknown"
+9616,"[NULL]",0,"Unknown"
+9621,"[NULL]",0,"Unknown"
+9622,"[NULL]",0,"Unknown"
+9623,"[NULL]",0,"Unknown"
+9624,"[NULL]",0,"Unknown"
+9625,"[NULL]",0,"Unknown"
+9627,"[NULL]",0,"Unknown"
+9630,"[NULL]",0,"Unknown"
+9637,"[NULL]",0,"Unknown"
+9638,"[NULL]",0,"Unknown"
+9639,"[NULL]",0,"Unknown"
+9641,"[NULL]",0,"Unknown"
+9642,"[NULL]",0,"Unknown"
+9643,"[NULL]",0,"Unknown"
+9645,"[NULL]",0,"Unknown"
+9646,"[NULL]",0,"Unknown"
 9647,"CrRendererMain",0,"CrProcessMain"
-9654,"[NULL]",0,"[NULL]"
-9655,"[NULL]",0,"[NULL]"
+9654,"[NULL]",0,"Unknown"
+9655,"[NULL]",0,"Unknown"
 9666,"ThreadPoolServiceThread",0,"ThreadPoolServiceThread"
 9667,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
-9668,"[NULL]",0,"[NULL]"
+9668,"[NULL]",0,"Unknown"
 9670,"Chrome_ChildIOThread",0,"Chrome_ChildIOThread"
-9672,"[NULL]",0,"[NULL]"
-9675,"[NULL]",0,"[NULL]"
-9699,"[NULL]",0,"[NULL]"
-9700,"[NULL]",0,"[NULL]"
+9672,"[NULL]",0,"Unknown"
+9675,"[NULL]",0,"Unknown"
+9699,"[NULL]",0,"Unknown"
+9700,"[NULL]",0,"Unknown"
 9702,"ThreadPoolServiceThread",0,"ThreadPoolServiceThread"
-9706,"[NULL]",0,"[NULL]"
-9707,"[NULL]",0,"[NULL]"
-9708,"[NULL]",0,"[NULL]"
-9709,"[NULL]",0,"[NULL]"
-9710,"[NULL]",0,"[NULL]"
-9711,"[NULL]",0,"[NULL]"
-9712,"[NULL]",0,"[NULL]"
-9713,"[NULL]",0,"[NULL]"
-9761,"[NULL]",0,"[NULL]"
-9934,"[NULL]",0,"[NULL]"
-9990,"[NULL]",0,"[NULL]"
-9994,"[NULL]",0,"[NULL]"
-10016,"[NULL]",0,"[NULL]"
+9706,"[NULL]",0,"Unknown"
+9707,"[NULL]",0,"Unknown"
+9708,"[NULL]",0,"Unknown"
+9709,"[NULL]",0,"Unknown"
+9710,"[NULL]",0,"Unknown"
+9711,"[NULL]",0,"Unknown"
+9712,"[NULL]",0,"Unknown"
+9713,"[NULL]",0,"Unknown"
+9761,"[NULL]",0,"Unknown"
+9934,"[NULL]",0,"Unknown"
+9990,"[NULL]",0,"Unknown"
+9994,"[NULL]",0,"Unknown"
+10016,"[NULL]",0,"Unknown"
 10070,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
 10090,"CrBrowserMain",1,"CrProcessMain"
-10099,"[NULL]",0,"[NULL]"
-10100,"[NULL]",0,"[NULL]"
-10101,"[NULL]",0,"[NULL]"
+10099,"[NULL]",0,"Unknown"
+10100,"[NULL]",0,"Unknown"
+10101,"[NULL]",0,"Unknown"
 10102,"Jit thread pool",0,"Jit thread pool"
 10104,"HeapTaskDaemon",0,"HeapTaskDaemon"
-10105,"[NULL]",0,"[NULL]"
-10106,"[NULL]",0,"[NULL]"
+10105,"[NULL]",0,"Unknown"
+10106,"[NULL]",0,"Unknown"
 10107,"FinalizerWatchd",0,"FinalizerWatchd"
-10108,"[NULL]",0,"[NULL]"
+10108,"[NULL]",0,"Unknown"
 10109,"Binder:10090_2",0,"Binder:10090_2"
-10110,"[NULL]",0,"[NULL]"
+10110,"[NULL]",0,"Unknown"
 10111,"Profile Saver",0,"Profile Saver"
 10123,"queued-work-loo",0,"queued-work-loo"
 10124,"GoogleApiHandle",0,"GoogleApiHandle"
 10125,"RenderThread",0,"RenderThread"
 10126,"Chrome_ProcessL",0,"Chrome_ProcessL"
-10134,"[NULL]",0,"[NULL]"
+10134,"[NULL]",0,"Unknown"
 10150,"GPU completion",0,"GPU completion"
 10152,"HangWatcher",0,"HangWatcher"
 10153,"ThreadPoolServiceThread",0,"ThreadPoolServiceThread"
@@ -249,20 +249,20 @@
 10163,"BrowserWatchdog",0,"BrowserWatchdog"
 10164,"NetworkService",0,"NetworkService"
 10165,"ThreadPoolSingleThreadSharedForegroundBlocking0",0,"ThreadPoolSingleThreadSharedForegroundBlocking0"
-10168,"[NULL]",1,"[NULL]"
-10178,"[NULL]",0,"[NULL]"
-10179,"[NULL]",0,"[NULL]"
-10180,"[NULL]",0,"[NULL]"
-10181,"[NULL]",0,"[NULL]"
-10182,"[NULL]",0,"[NULL]"
-10183,"[NULL]",0,"[NULL]"
-10184,"[NULL]",0,"[NULL]"
-10185,"[NULL]",0,"[NULL]"
-10186,"[NULL]",0,"[NULL]"
-10187,"[NULL]",0,"[NULL]"
-10188,"[NULL]",0,"[NULL]"
+10168,"[NULL]",1,"Unknown"
+10178,"[NULL]",0,"Unknown"
+10179,"[NULL]",0,"Unknown"
+10180,"[NULL]",0,"Unknown"
+10181,"[NULL]",0,"Unknown"
+10182,"[NULL]",0,"Unknown"
+10183,"[NULL]",0,"Unknown"
+10184,"[NULL]",0,"Unknown"
+10185,"[NULL]",0,"Unknown"
+10186,"[NULL]",0,"Unknown"
+10187,"[NULL]",0,"Unknown"
+10188,"[NULL]",0,"Unknown"
 10189,"Binder:10168_3",0,"Binder:10168_3"
-10199,"[NULL]",0,"[NULL]"
+10199,"[NULL]",0,"Unknown"
 10212,"CrGpuMain",0,"CrProcessMain"
 10213,"SAFE_BROWSING_U",0,"SAFE_BROWSING_U"
 10221,"GpuWatchdog",0,"GpuWatchdog"
@@ -271,35 +271,35 @@
 10233,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
 10235,"Chrome_ChildIOThread",0,"Chrome_ChildIOThread"
 10236,"VizCompositorThread",0,"VizCompositorThread"
-10239,"[NULL]",0,"[NULL]"
-10245,"[NULL]",0,"[NULL]"
-10246,"[NULL]",0,"[NULL]"
-10255,"[NULL]",0,"[NULL]"
-10837,"[NULL]",0,"[NULL]"
-10838,"[NULL]",0,"[NULL]"
-10839,"[NULL]",0,"[NULL]"
-10840,"[NULL]",0,"[NULL]"
-11045,"[NULL]",0,"[NULL]"
-11134,"[NULL]",0,"[NULL]"
-11354,"[NULL]",0,"[NULL]"
-11355,"[NULL]",0,"[NULL]"
-11661,"[NULL]",0,"[NULL]"
-11662,"[NULL]",0,"[NULL]"
-11663,"[NULL]",0,"[NULL]"
-11664,"[NULL]",0,"[NULL]"
-11897,"[NULL]",0,"[NULL]"
+10239,"[NULL]",0,"Unknown"
+10245,"[NULL]",0,"Unknown"
+10246,"[NULL]",0,"Unknown"
+10255,"[NULL]",0,"Unknown"
+10837,"[NULL]",0,"Unknown"
+10838,"[NULL]",0,"Unknown"
+10839,"[NULL]",0,"Unknown"
+10840,"[NULL]",0,"Unknown"
+11045,"[NULL]",0,"Unknown"
+11134,"[NULL]",0,"Unknown"
+11354,"[NULL]",0,"Unknown"
+11355,"[NULL]",0,"Unknown"
+11661,"[NULL]",0,"Unknown"
+11662,"[NULL]",0,"Unknown"
+11663,"[NULL]",0,"Unknown"
+11664,"[NULL]",0,"Unknown"
+11897,"[NULL]",0,"Unknown"
 11910,"MemoryInfra",0,"MemoryInfra"
 11911,"MemoryInfra",0,"MemoryInfra"
 11912,"MemoryInfra",0,"MemoryInfra"
 12527,"Binder:10090_4",0,"Binder:10090_4"
-12529,"[NULL]",0,"[NULL]"
+12529,"[NULL]",0,"Unknown"
 12534,"Binder:10090_5",0,"Binder:10090_5"
-12535,"[NULL]",0,"[NULL]"
+12535,"[NULL]",0,"Unknown"
 12548,"ThreadPoolForeg",0,"ThreadPoolForeg"
-13212,"[NULL]",0,"[NULL]"
-13213,"[NULL]",0,"[NULL]"
-13248,"[NULL]",0,"[NULL]"
-13299,"[NULL]",0,"[NULL]"
+13212,"[NULL]",0,"Unknown"
+13213,"[NULL]",0,"Unknown"
+13248,"[NULL]",0,"Unknown"
+13299,"[NULL]",0,"Unknown"
 13315,"ocessService0:7",1,"ocessService0:7"
 13326,"Runtime worker ",0,"Runtime worker "
 13327,"Runtime worker ",0,"Runtime worker "
@@ -326,7 +326,7 @@
 13350,"ThreadPoolSingl",0,"ThreadPoolSingl"
 13351,"CompositorTileWorker1",0,"CompositorTileWorker1"
 13353,"CompositorTileWorkerBackground",0,"CompositorTileWorkerBackground"
-13371,"[NULL]",0,"[NULL]"
+13371,"[NULL]",0,"Unknown"
 13517,"HeapTaskDaemon",0,"HeapTaskDaemon"
 13518,"ReferenceQueueD",0,"ReferenceQueueD"
 13519,"FinalizerDaemon",0,"FinalizerDaemon"
@@ -338,9 +338,9 @@
 13655,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
 13669,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
 13670,"ThreadPoolForegroundWorker",0,"ThreadPoolForegroundWorker"
-13671,"[NULL]",0,"[NULL]"
-13672,"[NULL]",0,"[NULL]"
-13673,"[NULL]",0,"[NULL]"
+13671,"[NULL]",0,"Unknown"
+13672,"[NULL]",0,"Unknown"
+13673,"[NULL]",0,"Unknown"
 13696,"ThreadPoolForeg",0,"ThreadPoolForeg"
 13697,"CrGpuMain",0,"CrProcessMain"
 13698,"cessService0:12",1,"cessService0:12"
diff --git a/test/trace_processor/dynamic/annotated_callstack.sql b/test/trace_processor/dynamic/annotated_callstack.sql
new file mode 100644
index 0000000..a23f3cc
--- /dev/null
+++ b/test/trace_processor/dynamic/annotated_callstack.sql
@@ -0,0 +1,25 @@
+--
+-- Copyright 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
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+select eac.id, eac.depth, eac.frame_id, eac.annotation,
+       spf.name
+from experimental_annotated_callstack eac
+join perf_sample ps
+  on (eac.start_id == ps.callsite_id)
+join stack_profile_frame spf
+  on (eac.frame_id == spf.id)
+order by eac.start_id asc, eac.depth asc;
+
diff --git a/test/trace_processor/dynamic/index b/test/trace_processor/dynamic/index
index 8e76690..3c5953b 100644
--- a/test/trace_processor/dynamic/index
+++ b/test/trace_processor/dynamic/index
@@ -7,4 +7,7 @@
 relationship_tables.textproto descendant_slice.sql descendant_slice.out
 
 # Connected/Following/Perceeding flow table.
-connected_flow_data.json connected_flow.sql connected_flow.out
\ No newline at end of file
+connected_flow_data.json connected_flow.sql connected_flow.out
+
+# Annotated callstacks.
+../../data/perf_sample_sc.pb annotated_callstack.sql perf_sample_sc_annotated_callstack.out
diff --git a/test/trace_processor/dynamic/perf_sample_sc_annotated_callstack.out b/test/trace_processor/dynamic/perf_sample_sc_annotated_callstack.out
new file mode 100644
index 0000000..633c97e
--- /dev/null
+++ b/test/trace_processor/dynamic/perf_sample_sc_annotated_callstack.out
@@ -0,0 +1,96 @@
+"id","depth","frame_id","annotation","name"
+0,0,0,"[NULL]","__start_thread"
+1,1,1,"[NULL]","_ZL15__pthread_startPv"
+2,2,2,"[NULL]","_ZN13thread_data_t10trampolineEPKS_"
+3,3,3,"[NULL]","_ZN7android14AndroidRuntime15javaThreadShellEPv"
+4,4,4,"[NULL]","_ZN7android6Thread11_threadLoopEPv"
+5,5,5,"[NULL]","_ZN7android10PoolThread10threadLoopEv"
+6,6,6,"[NULL]","_ZN7android14IPCThreadState14joinThreadPoolEb"
+7,7,7,"[NULL]","_ZN7android14IPCThreadState14executeCommandEi"
+8,8,8,"[NULL]","_ZN7android7BBinder8transactEjRKNS_6ParcelEPS1_j"
+9,9,9,"[NULL]","_ZN11JavaBBinder10onTransactEjRKN7android6ParcelEPS1_j"
+10,10,10,"[NULL]","_ZN7_JNIEnv17CallBooleanMethodEP8_jobjectP10_jmethodIDz"
+11,11,11,"[NULL]","_ZN3art3JNIILb0EE18CallBooleanMethodVEP7_JNIEnvP8_jobjectP10_jmethodIDSt9__va_list"
+12,12,12,"[NULL]","_ZN3art35InvokeVirtualOrInterfaceWithVarArgsIPNS_9ArtMethodEEENS_6JValueERKNS_33ScopedObjectAccessAlreadyRunnableEP8_jobjectT_St9__va_list"
+13,13,13,"[NULL]","art_quick_invoke_stub"
+14,14,14,"aot","android.os.Binder.execTransact"
+15,15,15,"aot","android.os.Binder.execTransactInternal"
+16,16,16,"aot","com.android.server.wm.Session.onTransact"
+17,17,17,"jit","android.view.IWindowSession$Stub.onTransact"
+18,18,18,"aot","com.android.server.wm.Session.sendWallpaperCommand"
+19,19,19,"aot","com.android.server.ThreadPriorityBooster.boost"
+20,20,20,"common-frame","art_jni_trampoline"
+21,21,21,"[NULL]","_Z36android_os_Process_getThreadPriorityP7_JNIEnvP8_jobjecti"
+22,22,22,"[NULL]","getpriority"
+23,23,23,"[NULL]","__getpriority"
+0,0,0,"[NULL]","__start_thread"
+1,1,1,"[NULL]","_ZL15__pthread_startPv"
+2,2,2,"[NULL]","_ZN13thread_data_t10trampolineEPKS_"
+3,3,3,"[NULL]","_ZN7android14AndroidRuntime15javaThreadShellEPv"
+24,4,24,"[NULL]","_ZN7android6Thread11_threadLoopEPv"
+25,5,25,"[NULL]","_ZN7android12_GLOBAL__N_115InputThreadImpl10threadLoopEv"
+26,6,26,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher12dispatchOnceEv"
+27,7,27,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher23dispatchOnceInnerLockedEPl"
+28,8,28,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher20dispatchMotionLockedElNSt3__110shared_ptrINS0_11MotionEntryEEEPNS1_10DropReasonEPl"
+29,9,29,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher19dispatchEventLockedElNSt3__110shared_ptrINS0_10EventEntryEEERKNS2_6vectorINS0_11InputTargetENS2_9allocatorIS7_EEEE"
+30,10,30,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher26prepareDispatchCycleLockedElRKNS_2spINS0_10ConnectionEEENSt3__110shared_ptrINS0_10EventEntryEEERKNS0_11InputTargetE"
+31,11,31,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher28enqueueDispatchEntriesLockedElRKNS_2spINS0_10ConnectionEEENSt3__110shared_ptrINS0_10EventEntryEEERKNS0_11InputTargetE"
+32,12,32,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher24startDispatchCycleLockedElRKNS_2spINS0_10ConnectionEEE"
+33,13,33,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher29reportTouchEventForStatisticsERKNS0_11MotionEntryE"
+34,14,34,"[NULL]","_ZN7android17LatencyStatistics12shouldReportEv"
+0,0,0,"[NULL]","__start_thread"
+1,1,1,"[NULL]","_ZL15__pthread_startPv"
+2,2,2,"[NULL]","_ZN13thread_data_t10trampolineEPKS_"
+3,3,3,"[NULL]","_ZN7android14AndroidRuntime15javaThreadShellEPv"
+4,4,4,"[NULL]","_ZN7android6Thread11_threadLoopEPv"
+35,5,35,"[NULL]","_ZThn32_N7android13SensorService10threadLoopEv"
+36,6,36,"[NULL]","_ZN7android13SensorService10threadLoopEv"
+37,7,37,"[NULL]","_ZN7android12SensorDevice7pollFmqEP15sensors_event_tm"
+38,8,38,"[NULL]","_ZN7android8hardware9EventFlag10waitHelperEjPjl"
+39,9,39,"[NULL]","syscall"
+0,0,0,"[NULL]","__start_thread"
+1,1,1,"[NULL]","_ZL15__pthread_startPv"
+2,2,2,"[NULL]","_ZN13thread_data_t10trampolineEPKS_"
+3,3,3,"[NULL]","_ZN7android14AndroidRuntime15javaThreadShellEPv"
+24,4,24,"[NULL]","_ZN7android6Thread11_threadLoopEPv"
+25,5,25,"[NULL]","_ZN7android12_GLOBAL__N_115InputThreadImpl10threadLoopEv"
+26,6,26,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher12dispatchOnceEv"
+27,7,27,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher23dispatchOnceInnerLockedEPl"
+28,8,28,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher20dispatchMotionLockedElNSt3__110shared_ptrINS0_11MotionEntryEEEPNS1_10DropReasonEPl"
+29,9,29,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher19dispatchEventLockedElNSt3__110shared_ptrINS0_10EventEntryEEERKNS2_6vectorINS0_11InputTargetENS2_9allocatorIS7_EEEE"
+30,10,30,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher26prepareDispatchCycleLockedElRKNS_2spINS0_10ConnectionEEENSt3__110shared_ptrINS0_10EventEntryEEERKNS0_11InputTargetE"
+31,11,31,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher28enqueueDispatchEntriesLockedElRKNS_2spINS0_10ConnectionEEENSt3__110shared_ptrINS0_10EventEntryEEERKNS0_11InputTargetE"
+40,12,40,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher24startDispatchCycleLockedElRKNS_2spINS0_10ConnectionEEE"
+41,13,41,"[NULL]","_ZN7android15inputdispatcher34verifiedMotionEventFromMotionEntryERKNS0_11MotionEntryE"
+0,0,0,"[NULL]","__start_thread"
+1,1,1,"[NULL]","_ZL15__pthread_startPv"
+2,2,2,"[NULL]","_ZN13thread_data_t10trampolineEPKS_"
+3,3,3,"[NULL]","_ZN7android14AndroidRuntime15javaThreadShellEPv"
+24,4,24,"[NULL]","_ZN7android6Thread11_threadLoopEPv"
+25,5,25,"[NULL]","_ZN7android12_GLOBAL__N_115InputThreadImpl10threadLoopEv"
+26,6,26,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher12dispatchOnceEv"
+27,7,27,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher23dispatchOnceInnerLockedEPl"
+42,8,42,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher20dispatchMotionLockedElNSt3__110shared_ptrINS0_11MotionEntryEEEPNS1_10DropReasonEPl"
+43,9,43,"[NULL]","_ZN7android15inputdispatcher15InputDispatcher30findTouchedWindowTargetsLockedElRKNS0_11MotionEntryERNSt3__16vectorINS0_11InputTargetENS5_9allocatorIS7_EEEEPlPb"
+44,10,44,"[NULL]","_ZNK7android15inputdispatcher15InputDispatcher29hasResponsiveConnectionLockedERNS_17InputWindowHandleE"
+45,11,45,"[NULL]","_ZNK7android7RefBase9incStrongEPKv"
+0,0,0,"[NULL]","__start_thread"
+1,1,1,"[NULL]","_ZL15__pthread_startPv"
+46,2,46,"[NULL]","_ZN3art6Thread14CreateCallbackEPv"
+47,3,47,"[NULL]","_ZN3art35InvokeVirtualOrInterfaceWithJValuesIPNS_9ArtMethodEEENS_6JValueERKNS_33ScopedObjectAccessAlreadyRunnableEP8_jobjectT_PK6jvalue"
+48,4,48,"[NULL]","_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc"
+49,5,13,"[NULL]","art_quick_invoke_stub"
+50,6,49,"aot","com.android.server.UiThread.run"
+51,7,50,"aot","android.os.HandlerThread.run"
+52,8,51,"aot","android.os.Looper.loop"
+53,9,52,"aot","android.os.Looper.loopOnce"
+54,10,53,"aot","android.os.MessageQueue.next"
+55,11,54,"common-frame","art_jni_trampoline"
+56,12,55,"[NULL]","_ZN7androidL38android_os_MessageQueue_nativePollOnceEP7_JNIEnvP8_jobjectli"
+57,13,56,"[NULL]","_ZN7android6Looper8pollOnceEiPiS1_PPv"
+58,14,57,"[NULL]","_ZN7android6Looper9pollInnerEi"
+59,15,58,"[NULL]","_ZN7android24NativeInputEventReceiver11handleEventEiiPv"
+60,16,59,"[NULL]","_ZN7android24NativeInputEventReceiver13consumeEventsEP7_JNIEnvblPb"
+61,17,60,"common-frame","_ZN3art3JNIILb0EE9FindClassEP7_JNIEnvPKc"
+62,18,61,"common-frame","_ZN3art11ClassLinker9FindClassEPNS_6ThreadEPKcNS_6HandleINS_6mirror11ClassLoaderEEE"
+63,19,62,"common-frame",""
diff --git a/test/trace_processor/graphics/actual_frame_timeline_events.out b/test/trace_processor/graphics/actual_frame_timeline_events.out
index 1bc5a2d..0eccd74 100644
--- a/test/trace_processor/graphics/actual_frame_timeline_events.out
+++ b/test/trace_processor/graphics/actual_frame_timeline_events.out
@@ -12,3 +12,4 @@
 150,17,1000,15,14,"Layer2","On-time Present",1,0,"None","Valid Prediction","No Jank"
 170,6,666,15,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank"
 200,6,666,17,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank"
+245,15,666,18,0,"[NULL]","Late Present",0,0,"SurfaceFlinger Stuffing","Valid Prediction","SurfaceFlinger Stuffing"
diff --git a/test/trace_processor/graphics/android_sysui_cuj.out b/test/trace_processor/graphics/android_sysui_cuj.out
index a94f0ed..fffda29 100644
--- a/test/trace_processor/graphics/android_sysui_cuj.out
+++ b/test/trace_processor/graphics/android_sysui_cuj.out
@@ -1,6 +1,6 @@
 android_sysui_cuj {
  cuj_name: "SHADE_ROW_EXPAND"
-  cuj_start: 0
+  cuj_start: 10
   cuj_dur: 1000000000
   process {
     name: "com.android.systemui"
@@ -10,7 +10,7 @@
    number: 1
    ts: 0
    dur: 15000000
-   vsync: 1
+   vsync: 10
  }
  frames {
    number: 2
@@ -18,7 +18,7 @@
    dur: 27000000
    jank_cause: "MainThread - binder transaction time"
    jank_cause: "SurfaceFlinger Scheduling"
-   vsync: 2
+   vsync: 20
  }
  frames {
    number: 3
@@ -26,7 +26,7 @@
    dur: 22000000
    jank_cause: "RenderThread - long flush layers"
    jank_cause: "MainThread - binder calls count"
-   vsync: 3
+   vsync: 30
  }
  frames {
    number: 4
@@ -34,21 +34,22 @@
    dur: 38000000
    jank_cause: "GPU completion - long completion time"
    jank_cause: "Long running time"
-   vsync: 4
+   jank_cause: "JIT compiling"
+   vsync: 40
  }
  frames {
    number: 5
    ts: 70000000
    dur: 18000000
    jank_cause: "RenderThread - scheduler"
-   vsync: 6
+   vsync: 60
  }
   frames {
     number: 6
     ts: 100000000
     dur: 22000000
     jank_cause: "GPU completion - long completion time"
-    vsync: 9
+    vsync: 90
   }
   frames {
     number: 7
@@ -56,13 +57,12 @@
     dur: 10000000
     jank_cause: "SurfaceFlinger GPU Deadline Missed"
     jank_cause: "SurfaceFlinger Scheduling"
-    vsync: 10
+    vsync: 100
   }
   frames {
     number: 8
     ts: 400000000
     dur: 10000000
-    jank_cause: "Buffer Stuffing"
-    vsync: 12
+    vsync: 120
   }
 }
diff --git a/test/trace_processor/graphics/android_sysui_cuj.py b/test/trace_processor/graphics/android_sysui_cuj.py
index e496431..14dc2b0 100644
--- a/test/trace_processor/graphics/android_sysui_cuj.py
+++ b/test/trace_processor/graphics/android_sysui_cuj.py
@@ -19,6 +19,7 @@
 
 PID = 1000
 RTID = 1555
+JITID = 1777
 LAYER = "TX - NotificationShade#0"
 
 
@@ -37,6 +38,11 @@
   trace.add_atrace_end(ts=ts_end, tid=1666, pid=PID)
 
 
+def add_jit_thread_atrace(trace, ts, ts_end, buf):
+  trace.add_atrace_begin(ts=ts, tid=JITID, pid=PID, buf=buf)
+  trace.add_atrace_end(ts=ts_end, tid=JITID, pid=PID)
+
+
 def add_frame(trace, vsync, ts_do_frame, ts_end_do_frame, ts_draw_frame,
               ts_end_draw_frame, ts_gpu, ts_end_gpu):
   add_main_thread_atrace(trace, ts_do_frame, ts_end_do_frame,
@@ -75,7 +81,7 @@
   trace.add_expected_surface_frame_start_event(
       ts=ts,
       cookie=token_start + 2,
-      token=token_start + 1,
+      token=token_start,
       display_frame_token=token_start,
       pid=PID,
       layer_name=LAYER)
@@ -83,7 +89,7 @@
   trace.add_actual_surface_frame_start_event(
       ts=ts,
       cookie=token_start + 3,
-      token=token_start + 1,
+      token=token_start,
       display_frame_token=token_start,
       pid=PID,
       layer_name=LAYER,
@@ -106,15 +112,16 @@
     tid=RTID, tgid=PID, cmdline="RenderThread", name="RenderThread")
 trace.add_thread(
     tid=1666, tgid=PID, cmdline="GPU completion", name="GPU completion")
-
+trace.add_thread(
+    tid=JITID, tgid=PID, cmdline="Jit thread pool", name="Jit thread pool")
 trace.add_ftrace_packet(cpu=0)
-trace.add_atrace_async_begin(ts=0, tid=PID, pid=PID, buf="J<SHADE_ROW_EXPAND>")
+trace.add_atrace_async_begin(ts=10, tid=PID, pid=PID, buf="J<SHADE_ROW_EXPAND>")
 trace.add_atrace_async_end(
-    ts=1_000_000_000, tid=PID, pid=PID, buf="J<SHADE_ROW_EXPAND>")
+    ts=1_000_000_010, tid=PID, pid=PID, buf="J<SHADE_ROW_EXPAND>")
 
 add_frame(
     trace,
-    vsync=1,
+    vsync=10,
     ts_do_frame=0,
     ts_end_do_frame=5_000_000,
     ts_draw_frame=4_000_000,
@@ -129,7 +136,7 @@
 
 add_frame(
     trace,
-    vsync=2,
+    vsync=20,
     ts_do_frame=8_000_000,
     ts_end_do_frame=23_000_000,
     ts_draw_frame=22_000_000,
@@ -143,7 +150,7 @@
 
 add_frame(
     trace,
-    vsync=3,
+    vsync=30,
     ts_do_frame=30_000_000,
     ts_end_do_frame=33_000_000,
     ts_draw_frame=31_000_000,
@@ -173,7 +180,7 @@
 
 add_frame(
     trace,
-    vsync=4,
+    vsync=40,
     ts_do_frame=40_000_000,
     ts_end_do_frame=53_000_000,
     ts_draw_frame=52_000_000,
@@ -181,6 +188,27 @@
     ts_gpu=66_500_000,
     ts_end_gpu=78_000_000)
 
+add_jit_thread_atrace(
+    trace,
+    ts=39_000_000,
+    ts_end=45_000_000,
+    buf="JIT compiling void aa.aa(java.lang.Object, bb) (kind=Baseline)")
+add_jit_thread_atrace(
+    trace,
+    ts=46_000_000,
+    ts_end=47_000_000,
+    buf="Lock contention on Jit code cache (owner tid: 12345)")
+add_jit_thread_atrace(
+    trace,
+    ts=52_500_000,
+    ts_end=54_000_000,
+    buf="JIT compiling void cc.bb(java.lang.Object, bb) (kind=Osr)")
+add_jit_thread_atrace(
+    trace,
+    ts=56_500_000,
+    ts_end=60_000_000,
+    buf="JIT compiling void ff.zz(java.lang.Object, bb) (kind=Baseline)")
+
 # Main thread Running for 14 millis
 trace.add_sched(ts=39_000_000, prev_pid=0, next_pid=PID)
 trace.add_sched(ts=53_000_000, prev_pid=PID, next_pid=0, prev_state='R')
@@ -191,7 +219,7 @@
 
 add_frame(
     trace,
-    vsync=6,
+    vsync=60,
     ts_do_frame=70_000_000,
     ts_end_do_frame=80_000_000,
     ts_draw_frame=78_000_000,
@@ -212,7 +240,7 @@
 
 add_frame(
     trace,
-    vsync=9,
+    vsync=90,
     ts_do_frame=100_000_000,
     ts_end_do_frame=115_000_000,
     ts_draw_frame=102_000_000,
@@ -221,7 +249,7 @@
     ts_end_gpu=115_600_000)
 
 add_render_thread_atrace(
-    trace, ts=108_000_000, ts_end=114_000_000, buf="DrawFrames 6")
+    trace, ts=108_000_000, ts_end=114_000_000, buf="DrawFrames 90")
 add_gpu_thread_atrace(
     trace,
     ts=121_500_000,
@@ -230,7 +258,7 @@
 
 add_frame(
     trace,
-    vsync=10,
+    vsync=100,
     ts_do_frame=200_000_000,
     ts_end_do_frame=215_000_000,
     ts_draw_frame=202_000_000,
@@ -239,11 +267,11 @@
     ts_end_gpu=210_000_000)
 
 add_render_thread_atrace(
-    trace, ts=208_000_000, ts_end=214_000_000, buf="DrawFrames 7")
+    trace, ts=208_000_000, ts_end=214_000_000, buf="DrawFrames 100")
 
 add_frame(
     trace,
-    vsync=11,
+    vsync=110,
     ts_do_frame=300_000_000,
     ts_end_do_frame=315_000_000,
     ts_draw_frame=302_000_000,
@@ -256,7 +284,7 @@
 
 add_frame(
     trace,
-    vsync=12,
+    vsync=120,
     ts_do_frame=400_000_000,
     ts_end_do_frame=415_000_000,
     ts_draw_frame=402_000_000,
@@ -270,7 +298,7 @@
 # One more frame after the CUJ is finished
 add_frame(
     trace,
-    vsync=13,
+    vsync=130,
     ts_do_frame=1_100_000_000,
     ts_end_do_frame=1_200_000_000,
     ts_draw_frame=1_150_000_000,
@@ -278,23 +306,23 @@
     ts_gpu=1_400_000_000,
     ts_end_gpu=1_500_000_000)
 
-add_display_frame_events(ts=0, dur=16_000_000, token_start=10)
+add_display_frame_events(ts=1, dur=16_000_000, token_start=10)
 add_display_frame_events(ts=8_000_000, dur=28_000_000, token_start=20, jank=66)
 add_display_frame_events(ts=30_000_000, dur=25_000_000, token_start=30, jank=64)
 add_display_frame_events(ts=40_000_000, dur=40_000_000, token_start=40, jank=64)
-add_display_frame_events(ts=70_000_000, dur=20_000_000, token_start=50, jank=64)
+add_display_frame_events(ts=70_000_000, dur=20_000_000, token_start=60, jank=64)
 add_display_frame_events(
-    ts=100_000_000, dur=23_000_000, token_start=60, jank=64)
+    ts=100_000_000, dur=23_000_000, token_start=90, jank=64)
 add_display_frame_events(
-    ts=200_000_000, dur=12_000_000, token_start=70, jank=34)
-add_display_frame_events(ts=300_000_000, dur=61_000_000, token_start=80)
+    ts=200_000_000, dur=22_000_000, token_start=100, jank=34)
+add_display_frame_events(ts=300_000_000, dur=61_000_000, token_start=110)
 add_display_frame_events(
     ts=400_000_000,
     dur=61_000_000,
-    token_start=90,
+    token_start=120,
     jank=128,
     on_time_finish_override=1)
 add_display_frame_events(
-    ts=1_100_000_000, dur=500_000_000, token_start=100, jank=64)
+    ts=1_100_000_000, dur=500_000_000, token_start=130, jank=64)
 
 sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/graphics/android_sysui_cuj_event.out b/test/trace_processor/graphics/android_sysui_cuj_event.out
index 44ac4d2..4519815 100644
--- a/test/trace_processor/graphics/android_sysui_cuj_event.out
+++ b/test/trace_processor/graphics/android_sysui_cuj_event.out
@@ -2,8 +2,7 @@
 "track_type","track_name","ts","dur","slice_name"
 "slice","SHADE_ROW_EXPAND - jank cause",8000000,15000000,"MainThread - binder transaction time,SurfaceFlinger Scheduling"
 "slice","SHADE_ROW_EXPAND - jank cause",30000000,3000000,"MainThread - binder calls count,RenderThread - long flush layers"
-"slice","SHADE_ROW_EXPAND - jank cause",40000000,13000000,"GPU completion - long completion time,Long running time"
+"slice","SHADE_ROW_EXPAND - jank cause",40000000,13000000,"GPU completion - long completion time,JIT compiling,Long running time"
 "slice","SHADE_ROW_EXPAND - jank cause",70000000,10000000,"RenderThread - scheduler"
 "slice","SHADE_ROW_EXPAND - jank cause",100000000,15000000,"GPU completion - long completion time"
 "slice","SHADE_ROW_EXPAND - jank cause",200000000,15000000,"SurfaceFlinger GPU Deadline Missed,SurfaceFlinger Scheduling"
-"slice","SHADE_ROW_EXPAND - jank cause",400000000,15000000,"Buffer Stuffing"
diff --git a/test/trace_processor/graphics/android_sysui_cuj_old.out b/test/trace_processor/graphics/android_sysui_cuj_old.out
deleted file mode 100644
index 1e94109..0000000
--- a/test/trace_processor/graphics/android_sysui_cuj_old.out
+++ /dev/null
@@ -1,54 +0,0 @@
-android_sysui_cuj {
- cuj_name: "SHADE_ROW_EXPAND"
-  cuj_start: 0
-  cuj_dur: 1000000000
-  process {
-    name: "com.android.systemui"
-    uid: 10001
- }
- frames {
-   number: 1
-   ts: 0
-   dur: 15000000
- }
- frames {
-   number: 2
-   ts: 8000000
-   dur: 27000000
-   jank_cause: "MainThread - binder transaction time"
-   jank_cause: "SurfaceFlinger Scheduling"
- }
- frames {
-   number: 3
-   ts: 30000000
-   dur: 22000000
-   jank_cause: "RenderThread - long flush layers"
-   jank_cause: "MainThread - binder calls count"
- }
- frames {
-   number: 4
-   ts: 40000000
-   dur: 38000000
-   jank_cause: "GPU completion - long completion time"
-   jank_cause: "Long running time"
- }
- frames {
-   number: 5
-   ts: 70000000
-   dur: 18000000
-   jank_cause: "RenderThread - scheduler"
- }
-  frames {
-    number: 6
-    ts: 100000000
-    dur: 22000000
-   jank_cause: "GPU completion - long completion time"
-  }
-  frames {
-    number: 7
-    ts: 200000000
-    dur: 10000000
-    jank_cause: "SurfaceFlinger GPU Deadline Missed"
-    jank_cause: "SurfaceFlinger Scheduling"
-  }
-}
\ No newline at end of file
diff --git a/test/trace_processor/graphics/android_sysui_cuj_old.py b/test/trace_processor/graphics/android_sysui_cuj_old.py
deleted file mode 100644
index 1c7ea0b..0000000
--- a/test/trace_processor/graphics/android_sysui_cuj_old.py
+++ /dev/null
@@ -1,263 +0,0 @@
-#!/usr/bin/env python3
-# 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.
-
-from os import sys, path
-
-import synth_common
-
-PID = 1000
-RTID = 1555
-LAYER = "TX - NotificationShade#0"
-
-
-def add_main_thread_atrace(trace, ts, ts_end, buf):
-  trace.add_atrace_begin(ts=ts, tid=PID, pid=PID, buf=buf)
-  trace.add_atrace_end(ts=ts_end, tid=PID, pid=PID)
-
-
-def add_render_thread_atrace(trace, ts, ts_end, buf):
-  trace.add_atrace_begin(ts=ts, tid=RTID, pid=PID, buf=buf)
-  trace.add_atrace_end(ts=ts_end, tid=RTID, pid=PID)
-
-
-def add_gpu_thread_atrace(trace, ts, ts_end, buf):
-  trace.add_atrace_begin(ts=ts, tid=1666, pid=PID, buf=buf)
-  trace.add_atrace_end(ts=ts_end, tid=1666, pid=PID)
-
-
-def add_frame(trace, ts_do_frame, ts_end_do_frame, ts_draw_frame,
-              ts_end_draw_frame, ts_gpu, ts_end_gpu):
-  add_main_thread_atrace(trace, ts_do_frame, ts_end_do_frame,
-                         "Choreographer#doFrame")
-  add_render_thread_atrace(trace, ts_draw_frame, ts_end_draw_frame, "DrawFrame")
-  add_gpu_thread_atrace(trace, ts_gpu, ts_end_gpu,
-                        "waiting for GPU completion 123")
-
-
-def add_display_frame_events(ts, dur, token_start, jank=None):
-  jank_type = jank if jank is not None else 1
-  present_type = 2 if jank is not None else 1
-  on_time_finish = 1 if jank is None else 0
-  trace.add_expected_display_frame_start_event(
-      ts=ts, cookie=token_start, token=token_start, pid=PID)
-  trace.add_frame_end_event(ts=ts + 20_500_000, cookie=token_start)
-  trace.add_actual_display_frame_start_event(
-      ts=ts,
-      cookie=token_start + 1,
-      token=token_start,
-      pid=PID,
-      present_type=present_type,
-      on_time_finish=on_time_finish,
-      gpu_composition=0,
-      jank_type=jank_type,
-      prediction_type=3)
-  trace.add_frame_end_event(ts=ts + dur, cookie=token_start + 1)
-  trace.add_expected_surface_frame_start_event(
-      ts=ts,
-      cookie=token_start + 2,
-      token=token_start + 1,
-      display_frame_token=token_start,
-      pid=PID,
-      layer_name=LAYER)
-  trace.add_frame_end_event(ts=ts + 20_500_000, cookie=token_start + 2)
-  trace.add_actual_surface_frame_start_event(
-      ts=ts,
-      cookie=token_start + 3,
-      token=token_start + 1,
-      display_frame_token=token_start,
-      pid=PID,
-      layer_name=LAYER,
-      present_type=present_type,
-      on_time_finish=on_time_finish,
-      gpu_composition=0,
-      jank_type=jank_type,
-      prediction_type=3)
-  trace.add_frame_end_event(ts=ts + dur, cookie=token_start + 3)
-
-
-trace = synth_common.create_trace()
-
-trace.add_packet()
-trace.add_package_list(
-    ts=0, name="com.android.systemui", uid=10001, version_code=1)
-
-trace.add_process(pid=PID, ppid=1, cmdline="com.android.systemui", uid=10001)
-trace.add_thread(
-    tid=RTID, tgid=PID, cmdline="RenderThread", name="RenderThread")
-trace.add_thread(
-    tid=1666, tgid=PID, cmdline="GPU completion", name="GPU completion")
-
-trace.add_ftrace_packet(cpu=0)
-trace.add_atrace_async_begin(ts=0, tid=PID, pid=PID, buf="J<SHADE_ROW_EXPAND>")
-trace.add_atrace_async_end(
-    ts=1_000_000_000, tid=PID, pid=PID, buf="J<SHADE_ROW_EXPAND>")
-
-add_frame(
-    trace,
-    ts_do_frame=0,
-    ts_end_do_frame=5_000_000,
-    ts_draw_frame=4_000_000,
-    ts_end_draw_frame=5_000_000,
-    ts_gpu=10_000_000,
-    ts_end_gpu=15_000_000)
-add_main_thread_atrace(
-    trace, ts=1_500_000, ts_end=2_000_000, buf="binder transaction")
-add_render_thread_atrace(
-    trace, ts=4_500_000, ts_end=4_800_000, buf="flush layers")
-
-add_frame(
-    trace,
-    ts_do_frame=8_000_000,
-    ts_end_do_frame=23_000_000,
-    ts_draw_frame=22_000_000,
-    ts_end_draw_frame=26_000_000,
-    ts_gpu=27_500_000,
-    ts_end_gpu=35_000_000)
-add_main_thread_atrace(
-    trace, ts=9_000_000, ts_end=20_000_000, buf="binder transaction")
-add_render_thread_atrace(
-    trace, ts=24_000_000, ts_end=25_000_000, buf="flush layers")
-
-add_frame(
-    trace,
-    ts_do_frame=30_000_000,
-    ts_end_do_frame=33_000_000,
-    ts_draw_frame=31_000_000,
-    ts_end_draw_frame=50_000_000,
-    ts_gpu=51_500_000,
-    ts_end_gpu=52_000_000)
-add_main_thread_atrace(
-    trace, ts=31_000_000, ts_end=31_050_000, buf="binder transaction")
-add_main_thread_atrace(
-    trace, ts=31_100_000, ts_end=31_150_000, buf="binder transaction")
-add_main_thread_atrace(
-    trace, ts=31_200_000, ts_end=31_250_000, buf="binder transaction")
-add_main_thread_atrace(
-    trace, ts=31_300_000, ts_end=31_350_000, buf="binder transaction")
-add_main_thread_atrace(
-    trace, ts=31_400_000, ts_end=31_450_000, buf="binder transaction")
-add_main_thread_atrace(
-    trace, ts=31_500_000, ts_end=31_550_000, buf="binder transaction")
-add_main_thread_atrace(
-    trace, ts=31_600_000, ts_end=31_650_000, buf="binder transaction")
-add_main_thread_atrace(
-    trace, ts=31_700_000, ts_end=31_750_000, buf="binder transaction")
-add_main_thread_atrace(
-    trace, ts=31_800_000, ts_end=31_850_000, buf="binder transaction")
-add_render_thread_atrace(
-    trace, ts=38_000_000, ts_end=50_000_000, buf="flush layers")
-
-add_frame(
-    trace,
-    ts_do_frame=40_000_000,
-    ts_end_do_frame=53_000_000,
-    ts_draw_frame=52_000_000,
-    ts_end_draw_frame=59_000_000,
-    ts_gpu=66_500_000,
-    ts_end_gpu=78_000_000)
-
-# Main thread Running for 14 millis
-trace.add_sched(ts=39_000_000, prev_pid=0, next_pid=PID)
-trace.add_sched(ts=53_000_000, prev_pid=PID, next_pid=0, prev_state='R')
-
-# RenderThread Running for 5 millis
-trace.add_sched(ts=54_000_000, prev_pid=0, next_pid=RTID)
-trace.add_sched(ts=59_000_000, prev_pid=RTID, next_pid=0, prev_state='R')
-
-add_frame(
-    trace,
-    ts_do_frame=70_000_000,
-    ts_end_do_frame=80_000_000,
-    ts_draw_frame=78_000_000,
-    ts_end_draw_frame=87_000_000,
-    ts_gpu=86_500_000,
-    ts_end_gpu=88_000_000)
-
-# Main thread Running for 1 millis
-trace.add_sched(ts=70_000_000, prev_pid=0, next_pid=PID)
-trace.add_sched(ts=71_000_000, prev_pid=PID, next_pid=0, prev_state='R')
-
-# RenderThread Running for 1 millis and R for 9.5 millis
-trace.add_sched(ts=78_000_000, prev_pid=0, next_pid=RTID)
-trace.add_sched(ts=78_500_000, prev_pid=RTID, next_pid=0, prev_state='R')
-trace.add_sched(ts=78_500_000, prev_pid=0, next_pid=0)
-trace.add_sched(ts=88_000_000, prev_pid=0, next_pid=RTID)
-trace.add_sched(ts=88_500_000, prev_pid=RTID, next_pid=0, prev_state='R')
-
-add_frame(
-    trace,
-    ts_do_frame=100_000_000,
-    ts_end_do_frame=115_000_000,
-    ts_draw_frame=102_000_000,
-    ts_end_draw_frame=104_000_000,
-    ts_gpu=108_000_000,
-    ts_end_gpu=115_600_000)
-
-add_render_thread_atrace(
-    trace, ts=108_000_000, ts_end=114_000_000, buf="DrawFrame")
-add_gpu_thread_atrace(
-    trace,
-    ts=121_500_000,
-    ts_end=122_000_000,
-    buf="waiting for GPU completion 123")
-
-add_frame(
-    trace,
-    ts_do_frame=200_000_000,
-    ts_end_do_frame=215_000_000,
-    ts_draw_frame=202_000_000,
-    ts_end_draw_frame=204_000_000,
-    ts_gpu=208_000_000,
-    ts_end_gpu=210_000_000)
-
-add_render_thread_atrace(
-    trace, ts=208_000_000, ts_end=214_000_000, buf="DrawFrame")
-
-add_frame(
-    trace,
-    ts_do_frame=300_000_000,
-    ts_end_do_frame=315_000_000,
-    ts_draw_frame=302_000_000,
-    ts_end_draw_frame=304_000_000,
-    ts_gpu=308_000_000,
-    ts_end_gpu=310_000_000)
-
-add_render_thread_atrace(
-    trace, ts=305_000_000, ts_end=308_000_000, buf="dispatchFrameCallbacks")
-
-# One more frame after the CUJ is finished
-add_frame(
-    trace,
-    ts_do_frame=1_100_000_000,
-    ts_end_do_frame=1_200_000_000,
-    ts_draw_frame=1_150_000_000,
-    ts_end_draw_frame=1_300_000_000,
-    ts_gpu=1_400_000_000,
-    ts_end_gpu=1_500_000_000)
-
-add_display_frame_events(ts=0, dur=16_000_000, token_start=10)
-add_display_frame_events(ts=8_000_000, dur=28_000_000, token_start=20, jank=66)
-add_display_frame_events(ts=30_000_000, dur=25_000_000, token_start=30, jank=64)
-add_display_frame_events(ts=40_000_000, dur=40_000_000, token_start=40, jank=64)
-add_display_frame_events(ts=70_000_000, dur=20_000_000, token_start=50, jank=64)
-add_display_frame_events(
-    ts=100_000_000, dur=23_000_000, token_start=60, jank=64)
-add_display_frame_events(
-    ts=200_000_000, dur=12_000_000, token_start=70, jank=34)
-add_display_frame_events(ts=300_000_000, dur=61_000_000, token_start=80)
-add_display_frame_events(
-    ts=1_100_000_000, dur=500_000_000, token_start=100, jank=64)
-
-sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/graphics/composer_execution.out b/test/trace_processor/graphics/composer_execution.out
new file mode 100644
index 0000000..1ec79a8
--- /dev/null
+++ b/test/trace_processor/graphics/composer_execution.out
@@ -0,0 +1,5 @@
+
+"validation_type","count","total"
+"separated_validation",1,200
+"skipped_validation",2,200
+"unskipped_validation",1,200
diff --git a/test/trace_processor/graphics/composer_execution.py b/test/trace_processor/graphics/composer_execution.py
new file mode 100644
index 0000000..7ff5524
--- /dev/null
+++ b/test/trace_processor/graphics/composer_execution.py
@@ -0,0 +1,70 @@
+#!/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.
+
+# This synthetic trace tests handling of the mm_id field in the rss_stat
+# event when mm_structs are reused on process death.
+
+from os import sys, path
+
+import synth_common
+
+trace = synth_common.create_trace()
+
+trace.add_packet(ts=1)
+trace.add_process(10, 1, "parent_process")
+trace.add_process(10335, 10, "child_process")
+
+trace.add_ftrace_packet(1)
+
+# unskipped validation
+trace.add_atrace_begin(ts=100, tid=10335, pid=10335, buf="onMessageRefresh")
+trace.add_atrace_begin(ts=200, tid=10335, pid=10335, buf="HwcPresentOrValidateDisplay")
+trace.add_atrace_end(ts=300, tid=10335, pid=10335)
+trace.add_atrace_begin(ts=400, tid=10335, pid=10335, buf="HwcPresentDisplay")
+trace.add_atrace_end(ts=500, tid=10335, pid=10335)
+trace.add_atrace_end(ts=600, tid=10335, pid=10335)
+
+# skipped validation
+trace.add_atrace_begin(ts=1_100, tid=10335, pid=10335, buf="onMessageRefresh")
+trace.add_atrace_begin(ts=1_200, tid=10335, pid=10335, buf="HwcPresentOrValidateDisplay")
+trace.add_atrace_end(ts=1_300, tid=10335, pid=10335)
+trace.add_atrace_end(ts=1_400, tid=10335, pid=10335)
+
+# separated validation
+trace.add_atrace_begin(ts=2_100, tid=10335, pid=10335, buf="onMessageRefresh")
+trace.add_atrace_begin(ts=2_200, tid=10335, pid=10335, buf="otherFunction")
+trace.add_atrace_begin(ts=2_300, tid=10335, pid=10335, buf="HwcValidateDisplay")
+trace.add_atrace_end(ts=2_400, tid=10335, pid=10335)
+trace.add_atrace_end(ts=2_500, tid=10335, pid=10335)
+trace.add_atrace_begin(ts=2_600, tid=10335, pid=10335, buf="HwcPresentDisplay")
+trace.add_atrace_end(ts=2_700, tid=10335, pid=10335)
+trace.add_atrace_end(ts=2_800, tid=10335, pid=10335)
+
+# skipped validation
+trace.add_atrace_begin(ts=3_100, tid=10335, pid=10335, buf="AnotherFunction")
+trace.add_atrace_begin(ts=3_200, tid=10335, pid=10335, buf="onMessageRefresh")
+trace.add_atrace_begin(ts=3_300, tid=10335, pid=10335, buf="HwcPresentOrValidateDisplay")
+trace.add_atrace_end(ts=3_400, tid=10335, pid=10335)
+trace.add_atrace_end(ts=3_500, tid=10335, pid=10335)
+trace.add_atrace_end(ts=3_600, tid=10335, pid=10335)
+
+# incomplete (ignored)
+trace.add_atrace_begin(ts=4_100, tid=10335, pid=10335, buf="onMessageRefresh")
+trace.add_atrace_begin(ts=4_200, tid=10335, pid=10335, buf="HwcPresentOrValidateDisplay")
+trace.add_atrace_end(ts=4_300, tid=10335, pid=10335)
+trace.add_atrace_begin(ts=4_400, tid=10335, pid=10335, buf="HwcPresentDisplay")
+
+
+sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/graphics/composer_execution.sql b/test/trace_processor/graphics/composer_execution.sql
new file mode 100644
index 0000000..874e12b
--- /dev/null
+++ b/test/trace_processor/graphics/composer_execution.sql
@@ -0,0 +1,25 @@
+--
+-- Copyright 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
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+SELECT RUN_METRIC('android/composer_execution.sql',
+  'output', 'hwc_execution_spans') AS suppress_query_output;
+
+SELECT
+  validation_type,
+  COUNT(*) as count,
+  SUM(execution_time_ns) as total
+FROM hwc_execution_spans
+GROUP BY validation_type
+ORDER BY validation_type;
diff --git a/test/trace_processor/graphics/dpu_vote_clock_bw.out b/test/trace_processor/graphics/dpu_vote_clock_bw.out
new file mode 100644
index 0000000..54cba12
--- /dev/null
+++ b/test/trace_processor/graphics/dpu_vote_clock_bw.out
@@ -0,0 +1,16 @@
+android_hwcomposer {
+  skipped_validation_count: 0
+  unskipped_validation_count: 0
+  separated_validation_count: 0
+  unknown_validation_count: 0
+  dpu_vote_metrics {
+    tid: 237
+    avg_dpu_vote_clock: 206250
+    avg_dpu_vote_avg_bw: 210000
+    avg_dpu_vote_peak_bw: 205000
+  }
+  dpu_vote_metrics {
+    tid: 299
+    avg_dpu_vote_clock: 250000
+  }
+}
diff --git a/test/trace_processor/graphics/dpu_vote_clock_bw.textproto b/test/trace_processor/graphics/dpu_vote_clock_bw.textproto
new file mode 100644
index 0000000..984af63
--- /dev/null
+++ b/test/trace_processor/graphics/dpu_vote_clock_bw.textproto
@@ -0,0 +1,117 @@
+packet {
+  ftrace_events {
+    cpu: 2
+    event {
+      timestamp: 1000000
+      pid: 237
+      dpu_tracing_mark_write {
+        pid: 237
+        name: "dpu_vote_clock"
+        type: 67
+        value: 200000
+      }
+    }
+    event {
+      timestamp: 2000000
+      pid: 237
+      dpu_tracing_mark_write {
+        pid: 237
+        name: "dpu_vote_avg_bw"
+        type: 67
+        value: 210000
+      }
+    }
+    event {
+      timestamp: 3000000
+      pid: 237
+      dpu_tracing_mark_write {
+        pid: 237
+        name: "dpu_vote_peak_bw"
+        type: 67
+        value: 210000
+      }
+    }
+    event {
+      timestamp: 4000000
+      pid: 237
+      dpu_tracing_mark_write {
+        pid: 237
+        name: "dpu_vote_clock"
+        type: 67
+        value: 210000
+      }
+    }
+    event {
+      timestamp: 5000000
+      pid: 237
+      dpu_tracing_mark_write {
+        pid: 237
+        name: "dpu_vote_avg_bw"
+        type: 67
+        value: 210000
+      }
+    }
+    event {
+      timestamp: 6000000
+      pid: 237
+      dpu_tracing_mark_write {
+        pid: 237
+        name: "dpu_vote_peak_bw"
+        type: 67
+        value: 200000
+      }
+    }
+    event {
+      timestamp: 7000000
+      pid: 237
+      dpu_tracing_mark_write {
+        pid: 237
+        name: "dpu_vote_clock"
+        type: 67
+        value: 210000
+      }
+    }
+    event {
+      timestamp: 7000000
+      pid: 299
+      dpu_tracing_mark_write {
+        pid: 299
+        name: "dpu_vote_clock"
+        type: 67
+        value: 200000
+      }
+    }
+    event {
+      timestamp: 8000000
+      pid: 237
+      dpu_tracing_mark_write {
+        pid: 237
+        name: "dpu_vote_avg_bw"
+        type: 67
+        value: 210000
+      }
+    }
+    event {
+      timestamp: 8000000
+      pid: 299
+      dpu_tracing_mark_write {
+        pid: 299
+        name: "dpu_vote_clock"
+        type: 67
+        value: 300000
+      }
+    }
+    event {
+      timestamp: 8999999
+      pid: 237
+      dpu_tracing_mark_write {
+        pid: 237
+        name: "dpu_vote_peak_bw"
+        type: 67
+        value: 200000
+      }
+    }
+  }
+  trusted_uid: 9999
+  trusted_packet_sequence_id: 3
+}
\ No newline at end of file
diff --git a/test/trace_processor/graphics/expected_frame_timeline_events.out b/test/trace_processor/graphics/expected_frame_timeline_events.out
index 96ac12d..bbfb5bd 100644
--- a/test/trace_processor/graphics/expected_frame_timeline_events.out
+++ b/test/trace_processor/graphics/expected_frame_timeline_events.out
@@ -10,3 +10,4 @@
 150,20,1000,15,14,"Layer1"
 170,6,666,15,0,"[NULL]"
 200,6,666,17,0,"[NULL]"
+220,10,666,18,0,"[NULL]"
diff --git a/test/trace_processor/graphics/frame_missed_metrics.out b/test/trace_processor/graphics/frame_missed_metrics.out
new file mode 100644
index 0000000..aad41d2
--- /dev/null
+++ b/test/trace_processor/graphics/frame_missed_metrics.out
@@ -0,0 +1,7 @@
+android_surfaceflinger {
+  missed_frames: 3
+  missed_hwc_frames: 0
+  missed_gpu_frames: 0
+  missed_frame_rate: 0.42857142857142855 # = 3/7
+  gpu_invocations: 0
+}
diff --git a/test/trace_processor/graphics/frame_timeline_events.py b/test/trace_processor/graphics/frame_timeline_events.py
index 02cef94..0dcba8a 100644
--- a/test/trace_processor/graphics/frame_timeline_events.py
+++ b/test/trace_processor/graphics/frame_timeline_events.py
@@ -28,6 +28,7 @@
   JANK_APP_DEADLINE_MISSED = 64;
   JANK_BUFFER_STUFFING = 128;
   JANK_UNKNOWN = 256;
+  JANK_SF_STUFFING = 512;
 
 class PresentType:
   PRESENT_UNSPECIFIED = 0;
@@ -111,4 +112,10 @@
 trace.add_actual_surface_frame_start_event(ts=80, cookie=25, token=16, display_frame_token=17, pid=1000, layer_name="Layer1", present_type=PresentType.PRESENT_UNKNOWN, on_time_finish=0, gpu_composition=0, jank_type=JankType.JANK_UNKNOWN, prediction_type=PredictionType.PREDICTION_EXPIRED)
 trace.add_frame_end_event(ts=190, cookie=25)
 
+# DisplayFrame with SF Stuffing jank
+trace.add_expected_display_frame_start_event(ts=220, cookie=26, token=18, pid=666)
+trace.add_frame_end_event(ts=230, cookie=26)
+trace.add_actual_display_frame_start_event(ts=245, cookie=27, token=18, pid=666, present_type=PresentType.PRESENT_LATE, on_time_finish=0, gpu_composition=0, jank_type=JankType.JANK_SF_STUFFING, prediction_type=PredictionType.PREDICTION_VALID)
+trace.add_frame_end_event(ts=260, cookie=27)
+
 sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/graphics/index b/test/trace_processor/graphics/index
index a5871b4..d3377c8 100644
--- a/test/trace_processor/graphics/index
+++ b/test/trace_processor/graphics/index
@@ -17,15 +17,16 @@
 # Clock sync
 clock_sync.py clock_sync.sql clock_sync.out
 
-# Missed frames
+# Android SurfaceFlinger metrics
 frame_missed.py frame_missed_event.sql frame_missed_event_frame_missed.out
+frame_missed.py android_surfaceflinger frame_missed_metrics.out
+surfaceflinger_gpu_invocation.py android_surfaceflinger surfaceflinger_gpu_invocation.out
 
 # GPU metrics
 gpu_metric.py android_gpu gpu_metric.out
 
 # Android SysUI CUJs metrics
 android_sysui_cuj.py android_sysui_cuj android_sysui_cuj.out
-android_sysui_cuj_old.py android_sysui_cuj android_sysui_cuj_old.out
 android_sysui_cuj.py android_sysui_cuj_event.sql android_sysui_cuj_event.out
 
 # Frame Timeline event trace tests
@@ -40,3 +41,12 @@
 
 # G2D metrics
 g2d_metrics.textproto g2d g2d_metrics.out
+
+# Composer execution
+composer_execution.py composer_execution.sql composer_execution.out
+
+# Display metrics
+panel_fps.py display_metrics panel_fps.out
+
+# DPU vote clock and bandwidth
+dpu_vote_clock_bw.textproto android_hwcomposer dpu_vote_clock_bw.out
diff --git a/test/trace_processor/graphics/panel_fps.out b/test/trace_processor/graphics/panel_fps.out
new file mode 100644
index 0000000..c4e06d9
--- /dev/null
+++ b/test/trace_processor/graphics/panel_fps.out
@@ -0,0 +1,24 @@
+display_metrics {
+  total_duplicate_frames: 0
+  duplicate_frames_logged: 0
+  total_dpu_underrun_count: 0
+  refresh_rate_switches: 5
+  refresh_rate_stats {
+    refresh_rate_fps: 60
+    count: 2
+    total_dur_ms: 2
+    avg_dur_ms: 1
+  }
+  refresh_rate_stats {
+    refresh_rate_fps: 90
+    count: 2
+    total_dur_ms: 2
+    avg_dur_ms: 1
+  }
+  refresh_rate_stats {
+    refresh_rate_fps: 120
+    count: 1
+    total_dur_ms: 2
+    avg_dur_ms: 2
+  }
+}
diff --git a/test/trace_processor/graphics/panel_fps.py b/test/trace_processor/graphics/panel_fps.py
new file mode 100644
index 0000000..0c9e293
--- /dev/null
+++ b/test/trace_processor/graphics/panel_fps.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+# 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.
+
+# This synthetic trace tests handling of the mm_id field in the rss_stat
+# event when mm_structs are reused on process death.
+
+from os import sys, path
+
+import synth_common
+
+trace = synth_common.create_trace()
+
+trace.add_packet(ts=1)
+trace.add_process(10, 1, "parent_process")
+trace.add_process(11, 10, "child_process")
+
+trace.add_ftrace_packet(1)
+
+trace.add_print(ts=99_000_000, tid=11, buf='C|10|panel_fps|60')
+trace.add_print(ts=100_000_000, tid=11, buf='C|10|panel_fps|90')
+trace.add_print(ts=101_000_000, tid=11, buf='C|10|panel_fps|60')
+trace.add_print(ts=102_000_000, tid=11, buf='C|10|panel_fps|120')
+
+# The duplicated fps will be ignored
+trace.add_print(ts=103_000_000, tid=11, buf='C|10|panel_fps|120')
+
+trace.add_print(ts=104_000_000, tid=11, buf='C|10|panel_fps|90')
+
+# The last fps and its following duplicates will be ignored, and will
+# only be used for the calculation of duration of the previous fps
+trace.add_print(ts=105_000_000, tid=11, buf='C|10|panel_fps|24')
+trace.add_print(ts=106_000_000, tid=11, buf='C|10|panel_fps|24')
+
+sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/graphics/surfaceflinger_gpu_invocation.out b/test/trace_processor/graphics/surfaceflinger_gpu_invocation.out
new file mode 100644
index 0000000..9495fed
--- /dev/null
+++ b/test/trace_processor/graphics/surfaceflinger_gpu_invocation.out
@@ -0,0 +1,8 @@
+android_surfaceflinger {
+  missed_frames: 0
+  missed_hwc_frames: 0
+  missed_gpu_frames: 0
+  gpu_invocations: 4
+  avg_gpu_waiting_dur_ms: 4
+  total_non_empty_gpu_waiting_dur_ms: 11
+}
diff --git a/test/trace_processor/graphics/surfaceflinger_gpu_invocation.py b/test/trace_processor/graphics/surfaceflinger_gpu_invocation.py
new file mode 100644
index 0000000..53c6326
--- /dev/null
+++ b/test/trace_processor/graphics/surfaceflinger_gpu_invocation.py
@@ -0,0 +1,51 @@
+#!/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.
+
+# This synthetic trace tests handling of the mm_id field in the rss_stat
+# event when mm_structs are reused on process death.
+
+from os import sys, path
+
+import synth_common
+
+trace = synth_common.create_trace()
+
+trace.add_packet(ts=1)
+trace.add_process(pid=10, ppid=1, cmdline='/system/bin/surfaceflinger', uid=None)
+trace.add_thread(tid=10, tgid=10, cmdline='', name='Main thread')
+trace.add_thread(tid=33, tgid=10, cmdline='', name='GPU completion')
+trace.add_ftrace_packet(1)
+
+trace.add_atrace_begin(ts=1_000_000, tid=33, pid=10, buf='waiting for GPU completion 4')
+trace.add_atrace_end(ts=2_000_000, tid=33, pid=10)
+
+trace.add_atrace_begin(ts=3_000_000, tid=10, pid=10, buf='Trace GPU completion fence 5')
+trace.add_atrace_begin(ts=3_000_000, tid=33, pid=10, buf='waiting for GPU completion 5')
+trace.add_atrace_end(ts=3_000_500, tid=10, pid=10)
+trace.add_atrace_end(ts=6_000_000, tid=33, pid=10)
+
+trace.add_atrace_begin(ts=7_000_000, tid=10, pid=10, buf='Trace GPU completion fence 6')
+trace.add_atrace_begin(ts=7_000_000, tid=33, pid=10, buf='waiting for GPU completion 6')
+trace.add_atrace_end(ts=7_000_500, tid=10, pid=10)
+trace.add_atrace_begin(ts=10_000_000, tid=10, pid=10, buf='Trace GPU completion fence 7')
+trace.add_atrace_end(ts=10_000_500, tid=10, pid=10)
+trace.add_atrace_end(ts=12_000_000, tid=33, pid=10)
+trace.add_atrace_begin(ts=12_000_000, tid=33, pid=10, buf='waiting for GPU completion 7')
+trace.add_atrace_end(ts=14_000_000, tid=33, pid=10)
+
+trace.add_atrace_begin(ts=15_000_000, tid=10, pid=10, buf='Trace GPU completion fence 8')
+trace.add_atrace_end(ts=15_000_500, tid=10, pid=10)
+
+sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/memory/trace_metadata.out b/test/trace_processor/memory/trace_metadata.out
index b66bf1a..1f64280 100644
--- a/test/trace_processor/memory/trace_metadata.out
+++ b/test/trace_processor/memory/trace_metadata.out
@@ -1,5 +1,6 @@
 trace_metadata {
   trace_duration_ns: 9519159074,
+  trace_uuid: "00000000-0000-0000-e77f-20a2204c2a49",
   trace_size_bytes: 6365447
   trace_config_pbtxt: "buffers: {\n  size_kb: 32768\n  fill_policy: UNSPECIFIED\n}\ndata_sources: {\n  config: {\n    name: \"linux.ftrace\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config: {\n      ftrace_events: \"print\"\n      ftrace_events: \"sched_switch\"\n      ftrace_events: \"rss_stat\"\n      ftrace_events: \"ion_heap_shrink\"\n      ftrace_events: \"ion_heap_grow\"\n      atrace_categories: \"am\"\n      atrace_categories: \"dalvik\"\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config: {\n      trace_config: \"\"\n    }\n    inode_file_config: {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config: {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 0\n    }\n    sys_stats_config: {\n      meminfo_period_ms: 0\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config: {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config: {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\ndata_sources: {\n  config: {\n    name: \"linux.process_stats\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config: {\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config: {\n      trace_config: \"\"\n    }\n    inode_file_config: {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config: {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 100\n    }\n    sys_stats_config: {\n      meminfo_period_ms: 0\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config: {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config: {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\ndata_sources: {\n  config: {\n    name: \"linux.sys_stats\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config: {\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config: {\n      trace_config: \"\"\n    }\n    inode_file_config: {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config: {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 0\n    }\n    sys_stats_config: {\n      meminfo_period_ms: 50\n      meminfo_counters: MEMINFO_MEM_AVAILABLE\n      meminfo_counters: MEMINFO_SWAP_CACHED\n      meminfo_counters: MEMINFO_ACTIVE\n      meminfo_counters: MEMINFO_INACTIVE\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config: {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config: {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\nduration_ms: 10000\nenable_extra_guardrails: false\nlockdown_mode: LOCKDOWN_UNCHANGED\nstatsd_metadata: {\n  triggering_alert_id: 0\n  triggering_config_uid: 0\n  triggering_config_id: 0\n}\nwrite_into_file: false\nfile_write_period_ms: 0\nmax_file_size_bytes: 0\nguardrail_overrides: {\n  max_upload_per_day_bytes: 0\n}\ndeferred_start: false",
   sched_duration_ns: 9452761359
diff --git a/test/trace_processor/parsing/android_sched_and_ps_stats.out b/test/trace_processor/parsing/android_sched_and_ps_stats.out
index 609c875..4ff60f7 100644
--- a/test/trace_processor/parsing/android_sched_and_ps_stats.out
+++ b/test/trace_processor/parsing/android_sched_and_ps_stats.out
@@ -139,9 +139,9 @@
 "traced_buf_chunks_committed_out_of_order",0,"info","trace",0
 "traced_buf_padding_bytes_cleared",0,"info","trace",0
 "traced_buf_padding_bytes_written",0,"info","trace",0
-"traced_buf_patches_failed",0,"info","trace",0
+"traced_buf_patches_failed",0,"data_loss","trace",0
 "traced_buf_patches_succeeded",0,"info","trace",8215
 "traced_buf_readaheads_failed",0,"info","trace",0
 "traced_buf_readaheads_succeeded",0,"info","trace",0
-"traced_buf_trace_writer_packet_loss",0,"info","trace",0
+"traced_buf_trace_writer_packet_loss",0,"data_loss","trace",0
 "traced_buf_write_wrap_count",0,"info","trace",0
diff --git a/test/trace_processor/parsing/android_thread_time_in_state.out b/test/trace_processor/parsing/android_thread_time_in_state.out
index e02191d..7878ba5 100644
--- a/test/trace_processor/parsing/android_thread_time_in_state.out
+++ b/test/trace_processor/parsing/android_thread_time_in_state.out
@@ -4,6 +4,7 @@
       name: "com.google.pid5"
     }
     metrics_by_core_type {
+      time_in_state_cpu: 0
       core_type: "little"
       runtime_ms: 20
       mcycles: 3
@@ -11,6 +12,7 @@
     threads {
       main_thread: true
       metrics_by_core_type {
+        time_in_state_cpu: 0
         core_type: "little"
         runtime_ms: 20
         mcycles: 3
@@ -22,6 +24,7 @@
       name: "com.google.pid11"
     }
     metrics_by_core_type {
+      time_in_state_cpu: 2
       core_type: "little"
       runtime_ms: 20
       mcycles: 40
@@ -30,6 +33,7 @@
       name: "tid11"
       main_thread: true
       metrics_by_core_type {
+        time_in_state_cpu: 2
         core_type: "little"
         runtime_ms: 10
         mcycles: 20
@@ -39,6 +43,7 @@
       name: "tid12"
       main_thread: false
       metrics_by_core_type {
+        time_in_state_cpu: 2
         core_type: "little"
         runtime_ms: 10
         mcycles: 20
@@ -50,6 +55,7 @@
       name: "com.google.pid17"
     }
     metrics_by_core_type {
+      time_in_state_cpu: 0
       core_type: "little"
       runtime_ms: 10
       mcycles: 1
@@ -57,6 +63,7 @@
     threads {
       main_thread: true
       metrics_by_core_type {
+        time_in_state_cpu: 0
         core_type: "little"
         runtime_ms: 10
         mcycles: 1
diff --git a/test/trace_processor/parsing/android_thread_time_in_state_unknown.out b/test/trace_processor/parsing/android_thread_time_in_state_unknown.out
new file mode 100644
index 0000000..1c7c1c5
--- /dev/null
+++ b/test/trace_processor/parsing/android_thread_time_in_state_unknown.out
@@ -0,0 +1,22 @@
+android_thread_time_in_state {
+  processes {
+    metadata {
+      name: "com.google.pid5"
+    }
+    metrics_by_core_type {
+      time_in_state_cpu: 0
+      core_type: "unknown"
+      runtime_ms: 20
+      mcycles: 3
+    }
+    threads {
+      main_thread: true
+      metrics_by_core_type {
+        time_in_state_cpu: 0
+        core_type: "unknown"
+        runtime_ms: 20
+        mcycles: 3
+      }
+    }
+  }
+}
diff --git a/test/trace_processor/parsing/chrome_metadata.out b/test/trace_processor/parsing/chrome_metadata.out
index 71e8908..ffdc373 100644
--- a/test/trace_processor/parsing/chrome_metadata.out
+++ b/test/trace_processor/parsing/chrome_metadata.out
@@ -1,4 +1,5 @@
 "id","type","name","key_type","int_value","str_value"
-0,"metadata","cr-playstore_version_code","single",101,"[NULL]"
-1,"metadata","cr-enabled_categories","single","[NULL]","cat1,cat2,cat3"
-2,"metadata","trace_size_bytes","single",50,"[NULL]"
+0,"metadata","trace_uuid","single","[NULL]","00000000-0000-0000-23b6-9c184f48509d"
+1,"metadata","cr-playstore_version_code","single",101,"[NULL]"
+2,"metadata","cr-enabled_categories","single","[NULL]","cat1,cat2,cat3"
+3,"metadata","trace_size_bytes","single",50,"[NULL]"
diff --git a/test/trace_processor/parsing/counters_json_counters.out b/test/trace_processor/parsing/counters_json_counters.out
new file mode 100644
index 0000000..dfe4747d
--- /dev/null
+++ b/test/trace_processor/parsing/counters_json_counters.out
@@ -0,0 +1,4 @@
+"name","ts","value"
+"ctr cats",0,0.000000
+"ctr cats",10000,10.000000
+"ctr cats",20000,0.000000
diff --git a/test/trace_processor/parsing/decimal_timestamp_slices.out b/test/trace_processor/parsing/decimal_timestamp_slices.out
new file mode 100644
index 0000000..b64b606
--- /dev/null
+++ b/test/trace_processor/parsing/decimal_timestamp_slices.out
@@ -0,0 +1,2 @@
+"ts","dur","name"
+5100,500100,"name.exec"
diff --git a/test/trace_processor/parsing/index b/test/trace_processor/parsing/index
index e5f89c4..65ef807 100644
--- a/test/trace_processor/parsing/index
+++ b/test/trace_processor/parsing/index
@@ -102,6 +102,7 @@
 thread_time_in_state.textproto thread_time_in_state.sql thread_time_in_state.out
 thread_time_in_state_event.py thread_time_in_state_event.sql thread_time_in_state_event.out
 thread_time_in_state.textproto android_thread_time_in_state android_thread_time_in_state.out
+thread_time_in_state_unknown.textproto android_thread_time_in_state android_thread_time_in_state_unknown.out
 
 # Initial display state
 initial_display_state.textproto initial_display_state.sql initial_display_state.out
@@ -138,3 +139,10 @@
 # Kernel symbolization
 sched_blocked_reason_symbolized.textproto sched_blocked_reason_function.sql sched_blocked_reason_symbolized_sched_blocked_reason_function.out
 sched_blocked_reason_symbolized.textproto ../common/to_systrace.sql sched_blocked_reason_symbolized_to_systrace.out
+
+# Floating point numbers
+../../data/decimal_timestamp.json slices.sql decimal_timestamp_slices.out
+
+# JSON instants and counters
+../../data/counters.json json_counters.sql counters_json_counters.out
+../../data/instants.json json_instants.sql instants_json_instants.out
diff --git a/test/trace_processor/parsing/instants_json_instants.out b/test/trace_processor/parsing/instants_json_instants.out
new file mode 100644
index 0000000..2fb9fb4
--- /dev/null
+++ b/test/trace_processor/parsing/instants_json_instants.out
@@ -0,0 +1,5 @@
+"ts","slice_name","tid","pid"
+1234523300,"Thread",2347,"[NULL]"
+1235523300,"Global","[NULL]","[NULL]"
+1236523300,"Process","[NULL]",2320
+1237523300,"None",6790,"[NULL]"
diff --git a/test/trace_processor/parsing/json_counters.sql b/test/trace_processor/parsing/json_counters.sql
new file mode 100644
index 0000000..604c2c1
--- /dev/null
+++ b/test/trace_processor/parsing/json_counters.sql
@@ -0,0 +1,6 @@
+select
+  process_counter_track.name,
+  counter.ts,
+  counter.value
+from counter
+join process_counter_track on (counter.track_id = process_counter_track.id);
\ No newline at end of file
diff --git a/test/trace_processor/parsing/json_instants.sql b/test/trace_processor/parsing/json_instants.sql
new file mode 100644
index 0000000..d76ef89
--- /dev/null
+++ b/test/trace_processor/parsing/json_instants.sql
@@ -0,0 +1,12 @@
+select
+  slice.ts,
+  slice.name as slice_name,
+  thread.tid,
+  process.pid
+from slice
+join track on (slice.track_id = track.id)
+left join thread_track on (slice.track_id = thread_track.id)
+left join thread on (thread_track.utid = thread.utid)
+left join process_track on (slice.track_id = process_track.id)
+left join process on (process_track.upid = process.upid)
+where dur = 0;
\ No newline at end of file
diff --git a/test/trace_processor/parsing/rss_stat_mm_id_clone.py b/test/trace_processor/parsing/rss_stat_mm_id_clone.py
index 3b36d88..a12f516 100644
--- a/test/trace_processor/parsing/rss_stat_mm_id_clone.py
+++ b/test/trace_processor/parsing/rss_stat_mm_id_clone.py
@@ -81,7 +81,7 @@
 # In this packet, we check what happens to kernel threads in RSS stat.
 trace.add_ftrace_packet(1)
 
-# Emit an rss stat event for the the existing kernel thread.
+# Emit an rss stat event for the existing kernel thread.
 trace.add_rss_stat(100, tid=3, member=0, size=10, mm_id=0x2345, curr=1)
 
 # Start a new kernel thread.
diff --git a/test/trace_processor/parsing/thread_time_in_state_unknown.textproto b/test/trace_processor/parsing/thread_time_in_state_unknown.textproto
new file mode 100644
index 0000000..a85a462
--- /dev/null
+++ b/test/trace_processor/parsing/thread_time_in_state_unknown.textproto
@@ -0,0 +1,96 @@
+packet {
+  system_info {
+    # No android_build_fingerprint to test the metric without the CPU core to
+    # cluster mapping.
+    hz: 100
+  }
+}
+packet {
+  timestamp: 1
+  process_tree {
+    processes {
+      pid: 5
+      ppid: 1
+      cmdline: "com.google.pid5"
+    }
+    threads {
+      tid: 5
+      tgid: 5
+    }
+    threads {
+      tid: 7
+      tgid: 5
+      name: "tid7"
+    }
+  }
+}
+packet {
+  timestamp: 2
+  process_stats {
+    processes {
+      pid: 5
+      threads {
+        tid: 5
+        cpu_freq_indices: 1
+        cpu_freq_ticks: 1
+        cpu_freq_indices: 2
+        cpu_freq_ticks: 1
+      }
+    }
+  }
+}
+packet {
+  timestamp: 3
+  process_stats {
+    processes {
+      pid: 5
+      threads {
+        tid: 5
+        cpu_freq_indices: 1
+        cpu_freq_ticks: 2
+        cpu_freq_indices: 2
+        cpu_freq_ticks: 1
+      }
+      threads {
+        tid: 7
+        cpu_freq_indices: 5
+        cpu_freq_ticks: 1
+      }
+    }
+  }
+}
+packet {
+  timestamp: 4
+  process_stats {
+    processes {
+      pid: 5
+      threads {
+        tid: 5
+        # cpu_freq_indices: 1 was skipped because it did not change.
+        cpu_freq_indices: 2
+        cpu_freq_ticks: 2
+      }
+    }
+  }
+}
+packet {
+  timestamp: 1
+  cpu_info {
+    cpus {
+      frequencies: 100000
+      frequencies: 200000
+    }
+    cpus {
+      frequencies: 100000
+      frequencies: 200000
+    }
+    cpus {
+      frequencies: 1000000
+      frequencies: 2000000
+    }
+    cpus {
+      frequencies: 1000000
+      frequencies: 2000000
+    }
+  }
+}
diff --git a/test/trace_processor/profiling/heap_graph_duplicate_flamegraph.out b/test/trace_processor/profiling/heap_graph_duplicate_flamegraph.out
index a25ad46..74094d8 100644
--- a/test/trace_processor/profiling/heap_graph_duplicate_flamegraph.out
+++ b/test/trace_processor/profiling/heap_graph_duplicate_flamegraph.out
@@ -1,2 +1,2 @@
 "id","depth","name","map_name","count","cumulative_count","size","cumulative_size","parent_id"
-0,0,"FactoryProducerDelegateImplActor","JAVA",1,1,64,64,"[NULL]"
+0,0,"FactoryProducerDelegateImplActor [ROOT_JAVA_FRAME]","JAVA",1,1,64,64,"[NULL]"
diff --git a/test/trace_processor/profiling/heap_graph_flamegraph.out b/test/trace_processor/profiling/heap_graph_flamegraph.out
index 8ee3f57..f0d87db 100644
--- a/test/trace_processor/profiling/heap_graph_flamegraph.out
+++ b/test/trace_processor/profiling/heap_graph_flamegraph.out
@@ -1,4 +1,4 @@
 "id","depth","name","map_name","count","cumulative_count","size","cumulative_size","parent_id"
-0,0,"FactoryProducerDelegateImplActor","JAVA",1,2,64,96,"[NULL]"
+0,0,"FactoryProducerDelegateImplActor [ROOT_JAVA_FRAME]","JAVA",1,2,64,96,"[NULL]"
 1,1,"Foo","JAVA",1,1,32,32,0
-2,0,"DeobfuscatedA[]","JAVA",1,1,256,256,"[NULL]"
+2,0,"DeobfuscatedA[] [ROOT_JAVA_FRAME]","JAVA",1,1,256,256,"[NULL]"
diff --git a/test/trace_processor/profiling/heap_graph_flamegraph_focused.out b/test/trace_processor/profiling/heap_graph_flamegraph_focused.out
index bb26353..85a537d 100644
--- a/test/trace_processor/profiling/heap_graph_flamegraph_focused.out
+++ b/test/trace_processor/profiling/heap_graph_flamegraph_focused.out
@@ -1,4 +1,4 @@
 "id","depth","name","count","cumulative_count","size","cumulative_size","parent_id"
-0,0,"RootNode",1,3,100,700,"[NULL]"
+0,0,"RootNode [ROOT_JAVA_FRAME]",1,3,100,700,"[NULL]"
 1,1,"LeftChild0",1,2,200,600,0
 2,2,"LeftChild1",1,1,400,400,1
diff --git a/test/trace_processor/profiling/heap_graph_flamegraph_system-server-heap-graph.out b/test/trace_processor/profiling/heap_graph_flamegraph_system-server-heap-graph.out
index 810924b..1aa70e7 100644
--- a/test/trace_processor/profiling/heap_graph_flamegraph_system-server-heap-graph.out
+++ b/test/trace_processor/profiling/heap_graph_flamegraph_system-server-heap-graph.out
@@ -1,11 +1,11 @@
 "id","depth","name","map_name","count","cumulative_count","size","cumulative_size","parent_id"
-0,0,"java.lang.Class<java.lang.Object[]>","JAVA",1,3,224,292,"[NULL]"
+0,0,"java.lang.Class<java.lang.Object[]> [ROOT_JNI_GLOBAL]","JAVA",1,3,224,292,"[NULL]"
 1,1,"java.lang.Object[]","JAVA",1,1,28,28,0
 2,1,"java.lang.String","JAVA",1,1,40,40,0
-3,0,"java.lang.String","JAVA",41233,41233,1846824,1846824,"[NULL]"
-4,0,"java.lang.Class<android.content.pm.ApplicationInfo>","JAVA",1,5,1152,1268,"[NULL]"
+3,0,"java.lang.String [ROOT_INTERNED_STRING]","JAVA",41197,41197,1845640,1845640,"[NULL]"
+4,0,"java.lang.Class<android.content.pm.ApplicationInfo> [ROOT_STICKY_CLASS]","JAVA",1,5,1152,1268,"[NULL]"
 5,1,"java.lang.String","JAVA",1,1,56,56,4
 6,1,"android.content.pm.ApplicationInfo$1","JAVA",1,1,8,8,4
 7,1,"java.lang.Object[]","JAVA",1,2,20,52,4
 8,2,"long[]","JAVA",1,1,32,32,7
-9,0,"java.lang.Class<java.io.File>","JAVA",1,11,645,997,"[NULL]"
+9,0,"java.lang.Class<java.io.File> [ROOT_STICKY_CLASS]","JAVA",1,11,645,997,"[NULL]"
diff --git a/test/trace_processor/profiling/heap_profile_callsites.out b/test/trace_processor/profiling/heap_profile_callsites.out
deleted file mode 100644
index 6079714..0000000
--- a/test/trace_processor/profiling/heap_profile_callsites.out
+++ /dev/null
@@ -1,68 +0,0 @@
-heap_profile_callsites {
-  instance_stats {
-    pid: 2
-    process_name: "system_server"
-    process {
-      name: "system_server"
-      uid: 1000
-    }
-    callsites {
-      hash: -2067767828047084124
-      parent_hash: 3061552492032359760
-
-      frame {
-        name: "symbolized f3"
-        mapping_name: "/liblib.so"
-      }
-      self_allocs {
-        total_count: 2
-        total_bytes: 2000
-        delta_count: 1
-        delta_bytes: 1000
-      }
-      child_allocs {
-        total_count: 2
-        total_bytes: 2000
-        delta_count: 1
-        delta_bytes: 1000
-      }
-    }
-    callsites {
-      hash: 3061552492032359760
-      parent_hash: 6947621464292123521
-
-      frame {
-        name: "symbolized f2"
-        mapping_name: "/liblib.so"
-      }
-      self_allocs {
-        total_count: 10
-        total_bytes: 100
-        delta_count: 9
-        delta_bytes: 90
-      }
-      child_allocs {
-        total_count: 12
-        total_bytes: 2100
-        delta_count: 10
-        delta_bytes: 1090
-      }
-    }
-    callsites {
-      hash: 6947621464292123521
-      parent_hash: -1
-      frame {
-        name: "f1"
-        mapping_name: "/liblib.so"
-      }
-      child_allocs {
-        total_count: 12
-        total_bytes: 2100
-        delta_count: 10
-        delta_bytes: 1090
-      }
-    }
-    profile_delta_bytes: 1090
-    profile_total_bytes: 2100
-  }
-}
diff --git a/test/trace_processor/profiling/heap_profile_dump_max.out b/test/trace_processor/profiling/heap_profile_dump_max.out
new file mode 100644
index 0000000..1ef5076
--- /dev/null
+++ b/test/trace_processor/profiling/heap_profile_dump_max.out
@@ -0,0 +1,3 @@
+"id","type","ts","upid","heap_name","callsite_id","count","size"
+0,"heap_profile_allocation",-10,2,"malloc",2,6,1000
+1,"heap_profile_allocation",-10,2,"malloc",3,1,90
diff --git a/test/trace_processor/profiling/heap_profile_dump_max.textproto b/test/trace_processor/profiling/heap_profile_dump_max.textproto
new file mode 100644
index 0000000..6efbfe1
--- /dev/null
+++ b/test/trace_processor/profiling/heap_profile_dump_max.textproto
@@ -0,0 +1,149 @@
+packet {
+  process_tree {
+    processes {
+      pid: 1
+      ppid: 0
+      cmdline: "init"
+      uid: 0
+    }
+    processes {
+      pid: 2
+      ppid: 1
+      cmdline: "system_server"
+      uid: 1000
+    }
+  }
+}
+
+packet {
+  clock_snapshot {
+    clocks: {
+      clock_id: 6 # BOOTTIME
+      timestamp: 0
+    }
+    clocks: {
+      clock_id: 4 # MONOTONIC_COARSE
+      timestamp: 10
+    }
+  }
+}
+
+packet {
+  trusted_packet_sequence_id: 999
+  previous_packet_dropped: true
+  incremental_state_cleared: true
+  timestamp: 10
+  profile_packet {
+    strings {
+      iid: 1
+      str: "f1"
+    }
+    strings {
+      iid: 2
+      str: "f2"
+    }
+    strings {
+      iid: 3
+      str: "f3"
+    }
+    strings {
+      iid: 4
+      str: "liblib.so"
+    }
+    strings {
+      iid: 5
+      str: "build-id"
+    }
+    frames {
+      iid: 1
+      function_name_id: 1
+      mapping_id: 1
+      rel_pc: 0x1000
+    }
+    frames {
+      iid: 2
+      function_name_id: 2
+      mapping_id: 1
+      rel_pc: 0x2000
+    }
+    frames {
+      iid: 3
+      function_name_id: 3
+      mapping_id: 1
+      rel_pc: 0x3000
+    }
+    frames {
+      iid: 4
+      function_name_id: 2
+      mapping_id: 2
+      rel_pc: 0x4000
+    }
+    callstacks {
+      iid: 1
+      frame_ids: 1
+      frame_ids: 2
+      frame_ids: 3
+    }
+    callstacks {
+      iid: 2
+      frame_ids: 1
+      frame_ids: 4
+    }
+    mappings {
+      iid: 1
+      path_string_ids: 4
+      build_id: 5
+    }
+    mappings {
+      iid: 2
+      path_string_ids: 4
+      build_id: 5
+    }
+    process_dumps {
+      pid: 2
+      orig_sampling_interval_bytes: 1024
+      sampling_interval_bytes: 1024
+      samples {
+        callstack_id: 1
+        self_max: 1000
+        self_max_count: 6
+      }
+      samples {
+        callstack_id: 2
+        self_max: 90
+        self_max_count: 1
+      }
+    }
+  }
+}
+# Add some symbolization packets
+packet {
+  module_symbols {
+    path: "/liblib.so"
+    build_id: "build-id"
+    address_symbols {
+      address: 0x3000
+      lines {
+        function_name: "symbolized f3"
+        source_file_name: "f3.cc"
+        line_number: 33
+      }
+    }
+    address_symbols {
+      address: 0x2000
+      lines {
+        function_name: "symbolized f2"
+        source_file_name: "f2.cc"
+        line_number: 22
+      }
+    }
+    address_symbols {
+      address: 0x4000
+      lines {
+        function_name: "symbolized f2"
+        source_file_name: "f2.cc"
+        line_number: 23
+      }
+    }
+  }
+}
diff --git a/test/trace_processor/profiling/heap_profile_dump_max_legacy.out b/test/trace_processor/profiling/heap_profile_dump_max_legacy.out
new file mode 100644
index 0000000..a5e6319
--- /dev/null
+++ b/test/trace_processor/profiling/heap_profile_dump_max_legacy.out
@@ -0,0 +1,3 @@
+"id","type","ts","upid","heap_name","callsite_id","count","size"
+0,"heap_profile_allocation",-10,2,"malloc",2,0,1000
+1,"heap_profile_allocation",-10,2,"malloc",3,0,90
diff --git a/test/trace_processor/profiling/heap_profile_dump_max_legacy.textproto b/test/trace_processor/profiling/heap_profile_dump_max_legacy.textproto
new file mode 100644
index 0000000..86cadb4
--- /dev/null
+++ b/test/trace_processor/profiling/heap_profile_dump_max_legacy.textproto
@@ -0,0 +1,147 @@
+packet {
+  process_tree {
+    processes {
+      pid: 1
+      ppid: 0
+      cmdline: "init"
+      uid: 0
+    }
+    processes {
+      pid: 2
+      ppid: 1
+      cmdline: "system_server"
+      uid: 1000
+    }
+  }
+}
+
+packet {
+  clock_snapshot {
+    clocks: {
+      clock_id: 6 # BOOTTIME
+      timestamp: 0
+    }
+    clocks: {
+      clock_id: 4 # MONOTONIC_COARSE
+      timestamp: 10
+    }
+  }
+}
+
+packet {
+  trusted_packet_sequence_id: 999
+  previous_packet_dropped: true
+  incremental_state_cleared: true
+  timestamp: 10
+  profile_packet {
+    strings {
+      iid: 1
+      str: "f1"
+    }
+    strings {
+      iid: 2
+      str: "f2"
+    }
+    strings {
+      iid: 3
+      str: "f3"
+    }
+    strings {
+      iid: 4
+      str: "liblib.so"
+    }
+    strings {
+      iid: 5
+      str: "build-id"
+    }
+    frames {
+      iid: 1
+      function_name_id: 1
+      mapping_id: 1
+      rel_pc: 0x1000
+    }
+    frames {
+      iid: 2
+      function_name_id: 2
+      mapping_id: 1
+      rel_pc: 0x2000
+    }
+    frames {
+      iid: 3
+      function_name_id: 3
+      mapping_id: 1
+      rel_pc: 0x3000
+    }
+    frames {
+      iid: 4
+      function_name_id: 2
+      mapping_id: 2
+      rel_pc: 0x4000
+    }
+    callstacks {
+      iid: 1
+      frame_ids: 1
+      frame_ids: 2
+      frame_ids: 3
+    }
+    callstacks {
+      iid: 2
+      frame_ids: 1
+      frame_ids: 4
+    }
+    mappings {
+      iid: 1
+      path_string_ids: 4
+      build_id: 5
+    }
+    mappings {
+      iid: 2
+      path_string_ids: 4
+      build_id: 5
+    }
+    process_dumps {
+      pid: 2
+      samples {
+        callstack_id: 1
+        self_max: 1000
+        self_max_count: 6
+      }
+      samples {
+        callstack_id: 2
+        self_max: 90
+        self_max_count: 1
+      }
+    }
+  }
+}
+# Add some symbolization packets
+packet {
+  module_symbols {
+    path: "/liblib.so"
+    build_id: "build-id"
+    address_symbols {
+      address: 0x3000
+      lines {
+        function_name: "symbolized f3"
+        source_file_name: "f3.cc"
+        line_number: 33
+      }
+    }
+    address_symbols {
+      address: 0x2000
+      lines {
+        function_name: "symbolized f2"
+        source_file_name: "f2.cc"
+        line_number: 22
+      }
+    }
+    address_symbols {
+      address: 0x4000
+      lines {
+        function_name: "symbolized f2"
+        source_file_name: "f2.cc"
+        line_number: 23
+      }
+    }
+  }
+}
diff --git a/test/trace_processor/profiling/heap_profile_flamegraph_system-server-native-profile.out b/test/trace_processor/profiling/heap_profile_flamegraph_system-server-native-profile.out
index 42ab65d..b521691 100644
--- a/test/trace_processor/profiling/heap_profile_flamegraph_system-server-native-profile.out
+++ b/test/trace_processor/profiling/heap_profile_flamegraph_system-server-native-profile.out
@@ -1,11 +1,11 @@
-"id","type","depth","name","map_name","count","cumulative_count","size","cumulative_size","alloc_count","cumulative_alloc_count","alloc_size","cumulative_alloc_size","parent_id"
-0,"experimental_flamegraph_nodes",0,"__start_thread","/apex/com.android.runtime/lib64/bionic/libc.so",0,8,0,84848,0,210,0,1084996,"[NULL]"
-1,"experimental_flamegraph_nodes",1,"_ZL15__pthread_startPv","/apex/com.android.runtime/lib64/bionic/libc.so",0,8,0,84848,0,210,0,1084996,0
-2,"experimental_flamegraph_nodes",2,"_ZN7android14AndroidRuntime15javaThreadShellEPv","/system/lib64/libandroid_runtime.so",0,5,0,27704,0,77,0,348050,1
-3,"experimental_flamegraph_nodes",3,"_ZN7android6Thread11_threadLoopEPv","/system/lib64/libutils.so",0,5,0,27704,0,77,0,348050,2
-4,"experimental_flamegraph_nodes",4,"_ZN7android10PoolThread10threadLoopEv","/system/lib64/libbinder.so",0,1,0,4096,0,64,0,279182,3
-5,"experimental_flamegraph_nodes",5,"_ZN7android14IPCThreadState14joinThreadPoolEb","/system/lib64/libbinder.so",0,1,0,4096,0,64,0,279182,4
-6,"experimental_flamegraph_nodes",6,"_ZN7android14IPCThreadState20getAndExecuteCommandEv","/system/lib64/libbinder.so",0,1,0,4096,0,64,0,279182,5
-7,"experimental_flamegraph_nodes",7,"_ZN7android14IPCThreadState14executeCommandEi","/system/lib64/libbinder.so",0,1,0,4096,0,64,0,279182,6
-8,"experimental_flamegraph_nodes",8,"_ZN7android7BBinder8transactEjRKNS_6ParcelEPS1_j","/system/lib64/libbinder.so",0,1,0,4096,0,64,0,279182,7
-9,"experimental_flamegraph_nodes",9,"_ZN11JavaBBinder10onTransactEjRKN7android6ParcelEPS1_j","/system/lib64/libandroid_runtime.so",0,0,0,0,0,60,0,262730,8
+"id","type","depth","name","map_name","count","cumulative_count","size","cumulative_size","alloc_count","cumulative_alloc_count","alloc_size","cumulative_alloc_size","parent_id","source_file","line_number"
+0,"experimental_flamegraph_nodes",0,"__start_thread","/apex/com.android.runtime/lib64/bionic/libc.so",0,8,0,84848,0,210,0,1084996,"[NULL]","[NULL]","[NULL]"
+1,"experimental_flamegraph_nodes",1,"_ZL15__pthread_startPv","/apex/com.android.runtime/lib64/bionic/libc.so",0,8,0,84848,0,210,0,1084996,0,"[NULL]","[NULL]"
+2,"experimental_flamegraph_nodes",2,"_ZN7android14AndroidRuntime15javaThreadShellEPv","/system/lib64/libandroid_runtime.so",0,5,0,27704,0,77,0,348050,1,"[NULL]","[NULL]"
+3,"experimental_flamegraph_nodes",3,"_ZN7android6Thread11_threadLoopEPv","/system/lib64/libutils.so",0,5,0,27704,0,77,0,348050,2,"[NULL]","[NULL]"
+4,"experimental_flamegraph_nodes",4,"_ZN7android10PoolThread10threadLoopEv","/system/lib64/libbinder.so",0,1,0,4096,0,64,0,279182,3,"[NULL]","[NULL]"
+5,"experimental_flamegraph_nodes",5,"_ZN7android14IPCThreadState14joinThreadPoolEb","/system/lib64/libbinder.so",0,1,0,4096,0,64,0,279182,4,"[NULL]","[NULL]"
+6,"experimental_flamegraph_nodes",6,"_ZN7android14IPCThreadState20getAndExecuteCommandEv","/system/lib64/libbinder.so",0,1,0,4096,0,64,0,279182,5,"[NULL]","[NULL]"
+7,"experimental_flamegraph_nodes",7,"_ZN7android14IPCThreadState14executeCommandEi","/system/lib64/libbinder.so",0,1,0,4096,0,64,0,279182,6,"[NULL]","[NULL]"
+8,"experimental_flamegraph_nodes",8,"_ZN7android7BBinder8transactEjRKNS_6ParcelEPS1_j","/system/lib64/libbinder.so",0,1,0,4096,0,64,0,279182,7,"[NULL]","[NULL]"
+9,"experimental_flamegraph_nodes",9,"_ZN11JavaBBinder10onTransactEjRKN7android6ParcelEPS1_j","/system/lib64/libandroid_runtime.so",0,0,0,0,0,60,0,262730,8,"[NULL]","[NULL]"
diff --git a/test/trace_processor/profiling/index b/test/trace_processor/profiling/index
index ddb7ab5..66ae8e4 100644
--- a/test/trace_processor/profiling/index
+++ b/test/trace_processor/profiling/index
@@ -3,6 +3,8 @@
 heap_profile_jit.textproto heap_profile_frames.sql heap_profile_jit.out
 heap_profile_deobfuscate.textproto heap_profile_deobfuscate.sql heap_profile_deobfuscate.out
 heap_profile_deobfuscate_memfd.textproto heap_profile_deobfuscate.sql heap_profile_deobfuscate.out
+heap_profile_dump_max_legacy.textproto heap_profile_tracker_new_stack.sql heap_profile_dump_max_legacy.out
+heap_profile_dump_max.textproto heap_profile_tracker_new_stack.sql heap_profile_dump_max.out
 
 
 profiler_smaps.textproto profiler_smaps.sql profiler_smaps.out
@@ -36,15 +38,15 @@
 stack_profile_tracker_empty_callstack.textproto stack_profile_tracker_empty_callstack.sql stack_profile_tracker_empty_callstack.out
 
 # Metrics
-heap_profile.textproto heap_profile_callsites heap_profile_callsites.out
 heap_profile_no_symbols.textproto unsymbolized_frames unsymbolized_frames.out
 
 heap_graph.textproto java_heap_stats java_heap_stats.out
 heap_graph_closest_proc.textproto java_heap_stats heap_stats_closest_proc.out
 heap_graph.textproto java_heap_histogram java_heap_histogram.out
 
-# perf_sample table (traced_perf trace as an input).
-../../data/perf_sample.pb perf_sample.sql perf_sample_perf_sample.out
+# perf_sample table (traced_perf) with android R and S trace inputs.
+../../data/perf_sample.pb perf_sample.sql perf_sample_rvc.out
+../../data/perf_sample_sc.pb perf_sample.sql perf_sample_sc.out
 
 # this uses llvm-symbolizer to test the offline symbolization built into
 # trace_processor_shell.
diff --git a/test/trace_processor/profiling/perf_sample.sql b/test/trace_processor/profiling/perf_sample.sql
index 362f6ff..5c0f1b4 100644
--- a/test/trace_processor/profiling/perf_sample.sql
+++ b/test/trace_processor/profiling/perf_sample.sql
@@ -13,30 +13,19 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
-select perf_sample.ts, thread.tid, perf_sample.cpu, perf_sample.cpu_mode,
-       perf_sample.unwind_error, concatenated_callsite.frames_str
-from perf_sample
+
+select ps.ts, ps.cpu, ps.cpu_mode, ps.unwind_error, ps.perf_session_id,
+       pct.name cntr_name, pct.is_timebase,
+       thread.tid,
+       spf.name
+from experimental_annotated_callstack eac
+join perf_sample ps
+  on (eac.start_id == ps.callsite_id)
+join perf_counter_track pct
+  using(perf_session_id, cpu)
 join thread
   using(utid)
-left join (
-  select flattened_callsite.id, group_concat(spf.name) frames_str
-  from (
-    with recursive rec(id, parent_id, frame_id, depth)
-      as (
-        select id, parent_id, frame_id, depth
-        from stack_profile_callsite
-        union all
-          select rec.id, spc.parent_id, spc.frame_id, spc.depth
-          from stack_profile_callsite spc
-          join rec
-            on spc.id == rec.parent_id
-      )
-    select id, frame_id, depth
-    from rec
-    order by id asc, depth desc
-  ) as flattened_callsite
-  left join stack_profile_frame spf
-    on flattened_callsite.frame_id == spf.id
-  group by flattened_callsite.id
-) as concatenated_callsite
-  on perf_sample.callsite_id == concatenated_callsite.id;
+join stack_profile_frame spf
+  on (eac.frame_id == spf.id)
+order by ps.ts asc, eac.depth asc
+
diff --git a/test/trace_processor/profiling/perf_sample_perf_sample.out b/test/trace_processor/profiling/perf_sample_perf_sample.out
deleted file mode 100644
index 64ae205..0000000
--- a/test/trace_processor/profiling/perf_sample_perf_sample.out
+++ /dev/null
@@ -1,4 +0,0 @@
-"ts","tid","cpu","cpu_mode","unwind_error","frames_str"
-260406202383309,1102,3,"kernel","[NULL]","__ppoll,poll,_ZN8perfetto4base14UnixTaskRunner3RunEv,_ZN8perfetto11ServiceMainEiPPc,__libc_init"
-260406295220349,1102,0,"kernel","[NULL]","__connect,_ZN12_GLOBAL__N_117netdClientConnectEiPK8sockaddrj.cfi,_ZL21HandleProfilingSignaliP7siginfoPv,__kernel_rt_sigreturn,__ppoll,poll,_ZN8perfetto4base14UnixTaskRunner3RunEv,_ZN8perfetto11ServiceMainEiPPc,__libc_init"
-260406802020973,1102,2,"user","[NULL]","_ZN8perfetto3ipc25BufferedFrameDeserializer12BeginReceiveEv,_ZN8perfetto4base10UnixSocket7OnEventEv,_ZN8perfetto4base14UnixTaskRunner22RunFileDescriptorWatchEi,_ZN8perfetto4base14UnixTaskRunner26RunImmediateAndDelayedTaskEv,_ZN8perfetto4base14UnixTaskRunner3RunEv,_ZN8perfetto11ServiceMainEiPPc,__libc_init"
diff --git a/test/trace_processor/profiling/perf_sample_rvc.out b/test/trace_processor/profiling/perf_sample_rvc.out
new file mode 100644
index 0000000..00f7325
--- /dev/null
+++ b/test/trace_processor/profiling/perf_sample_rvc.out
@@ -0,0 +1,22 @@
+"ts","cpu","cpu_mode","unwind_error","perf_session_id","cntr_name","is_timebase","tid","name"
+260406202383309,3,"kernel","[NULL]",0,"cpu-clock",1,1102,"__libc_init"
+260406202383309,3,"kernel","[NULL]",0,"cpu-clock",1,1102,"_ZN8perfetto11ServiceMainEiPPc"
+260406202383309,3,"kernel","[NULL]",0,"cpu-clock",1,1102,"_ZN8perfetto4base14UnixTaskRunner3RunEv"
+260406202383309,3,"kernel","[NULL]",0,"cpu-clock",1,1102,"poll"
+260406202383309,3,"kernel","[NULL]",0,"cpu-clock",1,1102,"__ppoll"
+260406295220349,0,"kernel","[NULL]",0,"cpu-clock",1,1102,"__libc_init"
+260406295220349,0,"kernel","[NULL]",0,"cpu-clock",1,1102,"_ZN8perfetto11ServiceMainEiPPc"
+260406295220349,0,"kernel","[NULL]",0,"cpu-clock",1,1102,"_ZN8perfetto4base14UnixTaskRunner3RunEv"
+260406295220349,0,"kernel","[NULL]",0,"cpu-clock",1,1102,"poll"
+260406295220349,0,"kernel","[NULL]",0,"cpu-clock",1,1102,"__ppoll"
+260406295220349,0,"kernel","[NULL]",0,"cpu-clock",1,1102,"__kernel_rt_sigreturn"
+260406295220349,0,"kernel","[NULL]",0,"cpu-clock",1,1102,"_ZL21HandleProfilingSignaliP7siginfoPv"
+260406295220349,0,"kernel","[NULL]",0,"cpu-clock",1,1102,"_ZN12_GLOBAL__N_117netdClientConnectEiPK8sockaddrj.cfi"
+260406295220349,0,"kernel","[NULL]",0,"cpu-clock",1,1102,"__connect"
+260406802020973,2,"user","[NULL]",0,"cpu-clock",1,1102,"__libc_init"
+260406802020973,2,"user","[NULL]",0,"cpu-clock",1,1102,"_ZN8perfetto11ServiceMainEiPPc"
+260406802020973,2,"user","[NULL]",0,"cpu-clock",1,1102,"_ZN8perfetto4base14UnixTaskRunner3RunEv"
+260406802020973,2,"user","[NULL]",0,"cpu-clock",1,1102,"_ZN8perfetto4base14UnixTaskRunner26RunImmediateAndDelayedTaskEv"
+260406802020973,2,"user","[NULL]",0,"cpu-clock",1,1102,"_ZN8perfetto4base14UnixTaskRunner22RunFileDescriptorWatchEi"
+260406802020973,2,"user","[NULL]",0,"cpu-clock",1,1102,"_ZN8perfetto4base10UnixSocket7OnEventEv"
+260406802020973,2,"user","[NULL]",0,"cpu-clock",1,1102,"_ZN8perfetto3ipc25BufferedFrameDeserializer12BeginReceiveEv"
diff --git a/test/trace_processor/profiling/perf_sample_sc.out b/test/trace_processor/profiling/perf_sample_sc.out
new file mode 100644
index 0000000..4ddbb3a
--- /dev/null
+++ b/test/trace_processor/profiling/perf_sample_sc.out
@@ -0,0 +1,96 @@
+"ts","cpu","cpu_mode","unwind_error","perf_session_id","cntr_name","is_timebase","tid","name"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"__start_thread"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZL15__pthread_startPv"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN13thread_data_t10trampolineEPKS_"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN7android14AndroidRuntime15javaThreadShellEPv"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN7android6Thread11_threadLoopEPv"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN7android10PoolThread10threadLoopEv"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN7android14IPCThreadState14joinThreadPoolEb"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN7android14IPCThreadState14executeCommandEi"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN7android7BBinder8transactEjRKNS_6ParcelEPS1_j"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN11JavaBBinder10onTransactEjRKN7android6ParcelEPS1_j"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN7_JNIEnv17CallBooleanMethodEP8_jobjectP10_jmethodIDz"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN3art3JNIILb0EE18CallBooleanMethodVEP7_JNIEnvP8_jobjectP10_jmethodIDSt9__va_list"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_ZN3art35InvokeVirtualOrInterfaceWithVarArgsIPNS_9ArtMethodEEENS_6JValueERKNS_33ScopedObjectAccessAlreadyRunnableEP8_jobjectT_St9__va_list"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"art_quick_invoke_stub"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"android.os.Binder.execTransact"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"android.os.Binder.execTransactInternal"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"com.android.server.wm.Session.onTransact"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"android.view.IWindowSession$Stub.onTransact"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"com.android.server.wm.Session.sendWallpaperCommand"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"com.android.server.ThreadPriorityBooster.boost"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"art_jni_trampoline"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"_Z36android_os_Process_getThreadPriorityP7_JNIEnvP8_jobjecti"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"getpriority"
+105089621851721,7,"kernel","[NULL]",0,"cpu-clock",1,8817,"__getpriority"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"__start_thread"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZL15__pthread_startPv"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN13thread_data_t10trampolineEPKS_"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android14AndroidRuntime15javaThreadShellEPv"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android6Thread11_threadLoopEPv"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android12_GLOBAL__N_115InputThreadImpl10threadLoopEv"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher12dispatchOnceEv"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher23dispatchOnceInnerLockedEPl"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher20dispatchMotionLockedElNSt3__110shared_ptrINS0_11MotionEntryEEEPNS1_10DropReasonEPl"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher19dispatchEventLockedElNSt3__110shared_ptrINS0_10EventEntryEEERKNS2_6vectorINS0_11InputTargetENS2_9allocatorIS7_EEEE"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher26prepareDispatchCycleLockedElRKNS_2spINS0_10ConnectionEEENSt3__110shared_ptrINS0_10EventEntryEEERKNS0_11InputTargetE"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher28enqueueDispatchEntriesLockedElRKNS_2spINS0_10ConnectionEEENSt3__110shared_ptrINS0_10EventEntryEEERKNS0_11InputTargetE"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher24startDispatchCycleLockedElRKNS_2spINS0_10ConnectionEEE"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher29reportTouchEventForStatisticsERKNS0_11MotionEntryE"
+105090420158231,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android17LatencyStatistics12shouldReportEv"
+105090420368127,3,"user","[NULL]",0,"cpu-clock",1,2379,"__start_thread"
+105090420368127,3,"user","[NULL]",0,"cpu-clock",1,2379,"_ZL15__pthread_startPv"
+105090420368127,3,"user","[NULL]",0,"cpu-clock",1,2379,"_ZN13thread_data_t10trampolineEPKS_"
+105090420368127,3,"user","[NULL]",0,"cpu-clock",1,2379,"_ZN7android14AndroidRuntime15javaThreadShellEPv"
+105090420368127,3,"user","[NULL]",0,"cpu-clock",1,2379,"_ZN7android6Thread11_threadLoopEPv"
+105090420368127,3,"user","[NULL]",0,"cpu-clock",1,2379,"_ZThn32_N7android13SensorService10threadLoopEv"
+105090420368127,3,"user","[NULL]",0,"cpu-clock",1,2379,"_ZN7android13SensorService10threadLoopEv"
+105090420368127,3,"user","[NULL]",0,"cpu-clock",1,2379,"_ZN7android12SensorDevice7pollFmqEP15sensors_event_tm"
+105090420368127,3,"user","[NULL]",0,"cpu-clock",1,2379,"_ZN7android8hardware9EventFlag10waitHelperEjPjl"
+105090420368127,3,"user","[NULL]",0,"cpu-clock",1,2379,"syscall"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"__start_thread"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZL15__pthread_startPv"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN13thread_data_t10trampolineEPKS_"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android14AndroidRuntime15javaThreadShellEPv"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android6Thread11_threadLoopEPv"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android12_GLOBAL__N_115InputThreadImpl10threadLoopEv"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher12dispatchOnceEv"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher23dispatchOnceInnerLockedEPl"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher20dispatchMotionLockedElNSt3__110shared_ptrINS0_11MotionEntryEEEPNS1_10DropReasonEPl"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher19dispatchEventLockedElNSt3__110shared_ptrINS0_10EventEntryEEERKNS2_6vectorINS0_11InputTargetENS2_9allocatorIS7_EEEE"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher26prepareDispatchCycleLockedElRKNS_2spINS0_10ConnectionEEENSt3__110shared_ptrINS0_10EventEntryEEERKNS0_11InputTargetE"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher28enqueueDispatchEntriesLockedElRKNS_2spINS0_10ConnectionEEENSt3__110shared_ptrINS0_10EventEntryEEERKNS0_11InputTargetE"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher24startDispatchCycleLockedElRKNS_2spINS0_10ConnectionEEE"
+105090445130783,1,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher34verifiedMotionEventFromMotionEntryERKNS0_11MotionEntryE"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"__start_thread"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZL15__pthread_startPv"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN13thread_data_t10trampolineEPKS_"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android14AndroidRuntime15javaThreadShellEPv"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android6Thread11_threadLoopEPv"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android12_GLOBAL__N_115InputThreadImpl10threadLoopEv"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher12dispatchOnceEv"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher23dispatchOnceInnerLockedEPl"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher20dispatchMotionLockedElNSt3__110shared_ptrINS0_11MotionEntryEEEPNS1_10DropReasonEPl"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZN7android15inputdispatcher15InputDispatcher30findTouchedWindowTargetsLockedElRKNS0_11MotionEntryERNSt3__16vectorINS0_11InputTargetENS5_9allocatorIS7_EEEEPlPb"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZNK7android15inputdispatcher15InputDispatcher29hasResponsiveConnectionLockedERNS_17InputWindowHandleE"
+105091020156148,2,"user","[NULL]",0,"cpu-clock",1,2395,"_ZNK7android7RefBase9incStrongEPKv"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"__start_thread"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZL15__pthread_startPv"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZN3art6Thread14CreateCallbackEPv"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZN3art35InvokeVirtualOrInterfaceWithJValuesIPNS_9ArtMethodEEENS_6JValueERKNS_33ScopedObjectAccessAlreadyRunnableEP8_jobjectT_PK6jvalue"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"art_quick_invoke_stub"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"com.android.server.UiThread.run"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"android.os.HandlerThread.run"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"android.os.Looper.loop"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"android.os.Looper.loopOnce"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"android.os.MessageQueue.next"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"art_jni_trampoline"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZN7androidL38android_os_MessageQueue_nativePollOnceEP7_JNIEnvP8_jobjectli"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZN7android6Looper8pollOnceEiPiS1_PPv"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZN7android6Looper9pollInnerEi"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZN7android24NativeInputEventReceiver11handleEventEiiPv"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZN7android24NativeInputEventReceiver13consumeEventsEP7_JNIEnvblPb"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZN3art3JNIILb0EE9FindClassEP7_JNIEnvPKc"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,"_ZN3art11ClassLinker9FindClassEPNS_6ThreadEPKcNS_6HandleINS_6mirror11ClassLoaderEEE"
+105091321851304,7,"user","[NULL]",0,"cpu-clock",1,2029,""
diff --git a/test/trace_processor/python/api_unittest.py b/test/trace_processor/python/api_unittest.py
index 52e2c41..9feada4 100755
--- a/test/trace_processor/python/api_unittest.py
+++ b/test/trace_processor/python/api_unittest.py
@@ -20,7 +20,7 @@
 
 
 class TestQueryResultIterator(unittest.TestCase):
-  # The numbers input into cells correspond the the CellType enum values
+  # The numbers input into cells correspond the CellType enum values
   # defined under trace_processor.proto
   CELL_VARINT = ProtoFactory().CellsBatch().CELL_VARINT
   CELL_STRING = ProtoFactory().CellsBatch().CELL_STRING
diff --git a/test/trace_processor/startup/android_startup_attribution.out b/test/trace_processor/startup/android_startup_attribution.out
index e48e3db..87b1a30 100644
--- a/test/trace_processor/startup/android_startup_attribution.out
+++ b/test/trace_processor/startup/android_startup_attribution.out
@@ -5,7 +5,7 @@
     process_name: "com.some.app"
     zygote_new_process: false
     to_first_frame {
-      dur_ns: 200
+      dur_ns: 999999900
       main_thread_by_task_state {
         running_dur_ns: 0
         runnable_dur_ns: 0
@@ -17,7 +17,7 @@
         dur_ns: 2
         dur_ms: 2e-06
       }
-      dur_ms: 0.0002
+      dur_ms: 999.9999
       time_dex_open {
         dur_ns: 20
         dur_ms: 2e-05
@@ -33,6 +33,14 @@
         dur_ns: 20
         dur_ms: 2e-05
       }
+      time_gc_total {
+        dur_ns: 130
+        dur_ms: 0.00013
+      }
+      time_gc_on_cpu {
+        dur_ns: 50
+        dur_ms: 5e-05
+      }
     }
     activity_hosting_process_count: 1
     process {
@@ -40,7 +48,28 @@
     }
     event_timestamps {
       intent_received: 100
-      first_frame: 300
+      first_frame: 1000000000
+    }
+    long_binder_transactions {
+      duration {
+        dur_ns: 100000000
+        dur_ms: 100
+      }
+      thread: "Binder"
+      destination_process: "system_server"
+      flags: "0x00 No Flags Set"
+      code: "0x00 Java Layer Dependent"
+    }
+    long_binder_transactions {
+      duration {
+        dur_ns: 200000000
+        dur_ms: 200
+      }
+      thread: "fonts"
+      destination_thread: "Binder"
+      destination_process: "com.some.app"
+      flags: "0x00 No Flags Set"
+      code: "0x00 Java Layer Dependent"
     }
   }
 }
diff --git a/test/trace_processor/startup/android_startup_attribution.py b/test/trace_processor/startup/android_startup_attribution.py
index b4bd7e6..cf3ab8e 100644
--- a/test/trace_processor/startup/android_startup_attribution.py
+++ b/test/trace_processor/startup/android_startup_attribution.py
@@ -21,10 +21,14 @@
 APP_TID = 1
 SECOND_APP_TID = 3
 JIT_TID = 4
+GC_TID = 5
+GC2_TID = 6
+BINDER_TID = 7
+FONTS_TID = 8
 SYSTEM_SERVER_PID = 2
 SYSTEM_SERVER_TID = 2
 LAUNCH_START_TS = 100
-LAUNCH_END_TS = 300
+LAUNCH_END_TS = 10**9
 
 trace = synth_common.create_trace()
 trace.add_packet()
@@ -37,6 +41,12 @@
     tgid=APP_PID,
     cmdline='Jit thread pool',
     name='Jit thread pool')
+trace.add_thread(
+    tid=GC_TID, tgid=APP_PID, cmdline='HeapTaskDaemon', name='HeapTaskDaemon')
+trace.add_thread(
+    tid=GC2_TID, tgid=APP_PID, cmdline='HeapTaskDaemon', name='HeapTaskDaemon')
+trace.add_thread(tid=BINDER_TID, tgid=APP_PID, cmdline='Binder', name='Binder')
+trace.add_thread(tid=FONTS_TID, tgid=APP_PID, cmdline='fonts', name='fonts')
 
 trace.add_ftrace_packet(cpu=0)
 # Start intent.
@@ -118,6 +128,41 @@
     ts=200, pid=APP_PID, tid=JIT_TID, buf='JIT compiled something')
 trace.add_atrace_end(ts=210, pid=APP_PID, tid=JIT_TID)
 
+# GC slices.
+trace.add_atrace_begin(
+    ts=300, pid=APP_PID, tid=GC_TID, buf='Background concurrent copying GC')
+trace.add_atrace_end(ts=330, pid=APP_PID, tid=GC_TID)
+
+trace.add_atrace_begin(
+    ts=340, pid=APP_PID, tid=GC_TID, buf='CollectorTransition mark sweep GC')
+trace.add_atrace_end(ts=390, pid=APP_PID, tid=GC_TID)
+
+trace.add_atrace_begin(ts=320, pid=APP_PID, tid=GC2_TID, buf='semispace GC')
+trace.add_atrace_end(ts=370, pid=APP_PID, tid=GC2_TID)
+
+# Start running copying slice on the first thread
+trace.add_sched(ts=310, prev_pid=0, next_pid=GC_TID)
+# Switch to the second thread to run semispace slice
+trace.add_sched(ts=325, prev_pid=GC_TID, next_pid=GC2_TID)
+# Switch back to the first thread to run mark sweep slice
+trace.add_sched(ts=350, prev_pid=GC2_TID, next_pid=GC_TID)
+# Finish running for GC.
+trace.add_sched(ts=360, prev_pid=GC_TID, next_pid=0)
+
+# Long binder transactions.
+trace.add_binder_transaction(1, 10**8, 2 * (10**8), BINDER_TID, APP_PID, 2,
+                             10**8 + 1, 2 * (10**8) - 1, SYSTEM_SERVER_TID,
+                             SYSTEM_SERVER_PID)
+
+trace.add_binder_transaction(3, 3 * (10**8), 5 * (10**8), FONTS_TID, APP_PID, 4,
+                             3 * (10**8) + 1, 5 * (10**8) - 1, BINDER_TID,
+                             APP_PID)
+
+# A short binder transaction.
+trace.add_binder_transaction(5, 10**7, 5 * (10**7), BINDER_TID, APP_TID, 6,
+                             10**7 + 1, 5 * (10**7) - 1, SYSTEM_SERVER_TID,
+                             SYSTEM_SERVER_PID)
+
 # Intent successful.
 trace.add_atrace_begin(
     ts=LAUNCH_END_TS + 1,
diff --git a/test/trace_processor/startup/android_startup_breakdown.out b/test/trace_processor/startup/android_startup_breakdown.out
index 298773d..cebd9d0 100644
--- a/test/trace_processor/startup/android_startup_breakdown.out
+++ b/test/trace_processor/startup/android_startup_breakdown.out
@@ -53,10 +53,7 @@
     activities {
       name: "com.google.android.calendar.MainActivity"
       method: "performCreate"
-      slice {
-        dur_ns: 4000000000
-        dur_ms: 4000
-      }
+      ts_method_start: 188000000000
     }
     optimization_status {
       odex_status: "up-to-date"
diff --git a/test/trace_processor/tables/trace_metadata.json.out b/test/trace_processor/tables/trace_metadata.json.out
index 0d3f5d0..2a363cf 100644
--- a/test/trace_processor/tables/trace_metadata.json.out
+++ b/test/trace_processor/tables/trace_metadata.json.out
@@ -1,6 +1,7 @@
 {
   "trace_metadata": {
     "trace_duration_ns": 9519159074,
+    "trace_uuid": "00000000-0000-0000-e77f-20a2204c2a49",
     "trace_size_bytes": 6365447,
     "trace_config_pbtxt": "buffers: {\n  size_kb: 32768\n  fill_policy: UNSPECIFIED\n}\ndata_sources: {\n  config: {\n    name: \"linux.ftrace\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config: {\n      ftrace_events: \"print\"\n      ftrace_events: \"sched_switch\"\n      ftrace_events: \"rss_stat\"\n      ftrace_events: \"ion_heap_shrink\"\n      ftrace_events: \"ion_heap_grow\"\n      atrace_categories: \"am\"\n      atrace_categories: \"dalvik\"\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config: {\n      trace_config: \"\"\n    }\n    inode_file_config: {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config: {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 0\n    }\n    sys_stats_config: {\n      meminfo_period_ms: 0\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config: {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config: {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\ndata_sources: {\n  config: {\n    name: \"linux.process_stats\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config: {\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config: {\n      trace_config: \"\"\n    }\n    inode_file_config: {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config: {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 100\n    }\n    sys_stats_config: {\n      meminfo_period_ms: 0\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config: {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config: {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\ndata_sources: {\n  config: {\n    name: \"linux.sys_stats\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config: {\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config: {\n      trace_config: \"\"\n    }\n    inode_file_config: {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config: {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 0\n    }\n    sys_stats_config: {\n      meminfo_period_ms: 50\n      meminfo_counters: MEMINFO_MEM_AVAILABLE\n      meminfo_counters: MEMINFO_SWAP_CACHED\n      meminfo_counters: MEMINFO_ACTIVE\n      meminfo_counters: MEMINFO_INACTIVE\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config: {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config: {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\nduration_ms: 10000\nenable_extra_guardrails: false\nlockdown_mode: LOCKDOWN_UNCHANGED\nstatsd_metadata: {\n  triggering_alert_id: 0\n  triggering_config_uid: 0\n  triggering_config_id: 0\n}\nwrite_into_file: false\nfile_write_period_ms: 0\nmax_file_size_bytes: 0\nguardrail_overrides: {\n  max_upload_per_day_bytes: 0\n}\ndeferred_start: false",
     "sched_duration_ns": 9452761359
diff --git a/test/trace_processor/track_event/legacy_async_event.out b/test/trace_processor/track_event/legacy_async_event.out
index 3a5b6f3..ddf1b1f 100644
--- a/test/trace_processor/track_event/legacy_async_event.out
+++ b/test/trace_processor/track_event/legacy_async_event.out
@@ -1,15 +1,15 @@
 "track","process","thread","thread_process","ts","dur","category","name","key","string_value","int_value"
 "name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","legacy_event.passthrough_utid","[NULL]",1
 "name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","legacy_event.phase","S","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","arg1","value1","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","arg2","value2","[NULL]"
+"name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","debug.arg1","value1","[NULL]"
+"name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","debug.arg2","value2","[NULL]"
 "name1","[NULL]","[NULL]","[NULL]",2000,1000,"cat","name1","legacy_event.passthrough_utid","[NULL]",2
 "name1","[NULL]","[NULL]","[NULL]",2000,1000,"cat","name1","legacy_event.phase","S","[NULL]"
 "name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","legacy_event.passthrough_utid","[NULL]",1
 "name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","legacy_event.phase","T","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","arg3","value3","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","step","Step1","[NULL]"
+"name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","debug.arg3","value3","[NULL]"
+"name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","debug.step","Step1","[NULL]"
 "name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","legacy_event.passthrough_utid","[NULL]",1
 "name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","legacy_event.phase","p","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","step","Step2","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","arg4","value4","[NULL]"
+"name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","debug.step","Step2","[NULL]"
+"name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","debug.arg4","value4","[NULL]"
diff --git a/test/trace_processor/track_event/track_event_merged_debug_annotations.textproto b/test/trace_processor/track_event/track_event_merged_debug_annotations.textproto
index 41d3565..3ad016b 100644
--- a/test/trace_processor/track_event/track_event_merged_debug_annotations.textproto
+++ b/test/trace_processor/track_event/track_event_merged_debug_annotations.textproto
@@ -64,6 +64,22 @@
       name: "debug3"
       int_value: 31
     }
+    debug_annotations {
+      name: "debug4"
+      dict_entries {
+        name: "key1"
+        int_value: 10
+      }
+      dict_entries {
+        name: "key2"
+        array_values {
+          int_value: 20
+        }
+        array_values {
+          int_value: 21
+        }
+      }
+    }
     legacy_event {
       phase: 98  # 'b'
       global_id: 1234
diff --git a/test/trace_processor/track_event/track_event_merged_debug_annotations_args.out b/test/trace_processor/track_event/track_event_merged_debug_annotations_args.out
index 472d06d..8cecb1d 100644
--- a/test/trace_processor/track_event/track_event_merged_debug_annotations_args.out
+++ b/test/trace_processor/track_event/track_event_merged_debug_annotations_args.out
@@ -6,19 +6,22 @@
 "source_id","source_id",1234,"[NULL]"
 "source_id_is_process_scoped","source_id_is_process_scoped",0,"[NULL]"
 "source_scope","source_scope","[NULL]","cat"
-"debug1.key1","debug1.key1",10,"[NULL]"
-"debug1.key2","debug1.key2[0]",20,"[NULL]"
-"debug1.key2","debug1.key2[1]",21,"[NULL]"
-"debug1.key2","debug1.key2[2]",22,"[NULL]"
-"debug1.key2","debug1.key2[3]",23,"[NULL]"
-"debug1.key3","debug1.key3",30,"[NULL]"
-"debug2.key1","debug2.key1",10,"[NULL]"
-"debug2.key2","debug2.key2[0]",20,"[NULL]"
-"debug2.key2","debug2.key2[1]",21,"[NULL]"
-"debug2.key2","debug2.key2[2]",22,"[NULL]"
-"debug2.key2","debug2.key2[3]",23,"[NULL]"
-"debug2.key3.key31","debug2.key3.key31",31,"[NULL]"
-"debug2.key3.key32","debug2.key3.key32",32,"[NULL]"
-"debug2.key4","debug2.key4",40,"[NULL]"
-"debug3","debug3",32,"[NULL]"
+"debug.debug1.key1","debug.debug1.key1",10,"[NULL]"
+"debug.debug1.key2","debug.debug1.key2[0]",20,"[NULL]"
+"debug.debug1.key2","debug.debug1.key2[1]",21,"[NULL]"
+"debug.debug1.key2","debug.debug1.key2[2]",22,"[NULL]"
+"debug.debug1.key2","debug.debug1.key2[3]",23,"[NULL]"
+"debug.debug1.key3","debug.debug1.key3",30,"[NULL]"
+"debug.debug2.key1","debug.debug2.key1",10,"[NULL]"
+"debug.debug2.key2","debug.debug2.key2[0]",20,"[NULL]"
+"debug.debug2.key2","debug.debug2.key2[1]",21,"[NULL]"
+"debug.debug2.key2","debug.debug2.key2[2]",22,"[NULL]"
+"debug.debug2.key2","debug.debug2.key2[3]",23,"[NULL]"
+"debug.debug2.key3.key31","debug.debug2.key3.key31",31,"[NULL]"
+"debug.debug2.key3.key32","debug.debug2.key3.key32",32,"[NULL]"
+"debug.debug2.key4","debug.debug2.key4",40,"[NULL]"
+"debug.debug3","debug.debug3",32,"[NULL]"
+"debug.debug4.key1","debug.debug4.key1",10,"[NULL]"
+"debug.debug4.key2","debug.debug4.key2[0]",20,"[NULL]"
+"debug.debug4.key2","debug.debug4.key2[1]",21,"[NULL]"
 "legacy_event.passthrough_utid","legacy_event.passthrough_utid",1,"[NULL]"
diff --git a/test/trace_processor/track_event/track_event_typed_args.textproto b/test/trace_processor/track_event/track_event_typed_args.textproto
index 6ff29b1..b2d9208 100644
--- a/test/trace_processor/track_event/track_event_typed_args.textproto
+++ b/test/trace_processor/track_event/track_event_typed_args.textproto
@@ -78,6 +78,17 @@
         "should be absent from result"
     [perfetto.protos.TestExtension.nested_message_extension_for_testing] {
       child_field_for_testing: "nesting test"
+      debug_annotations {
+        name: "arg1"
+        string_value: "value"
+      }
+      debug_annotations {
+        name: "arg2"
+        dict_entries {
+          name: "key"
+          string_value: "value"
+        }
+      }
     }
   }
 }
@@ -120,6 +131,13 @@
             type: TYPE_STRING
             label: LABEL_OPTIONAL
           }
+          field {
+            name: "debug_annotations"
+            number: 99
+            type: TYPE_MESSAGE
+            label: LABEL_REPEATED
+            type_name: ".perfetto.protos.DebugAnnotation"
+          }
         }
       }
     }
diff --git a/test/trace_processor/track_event/track_event_typed_args_args.out b/test/trace_processor/track_event/track_event_typed_args_args.out
index ac7d608..fae18d7 100644
--- a/test/trace_processor/track_event/track_event_typed_args_args.out
+++ b/test/trace_processor/track_event/track_event_typed_args_args.out
@@ -13,6 +13,8 @@
 "chrome_latency_info.trace_id","chrome_latency_info.trace_id",7,"[NULL]"
 "int_extension_for_testing","int_extension_for_testing[0]",42,"[NULL]"
 "int_extension_for_testing","int_extension_for_testing[1]",1337,"[NULL]"
+"nested_message_extension_for_testing.arg1","nested_message_extension_for_testing.arg1","[NULL]","value"
+"nested_message_extension_for_testing.arg2.key","nested_message_extension_for_testing.arg2.key","[NULL]","value"
 "nested_message_extension_for_testing.child_field_for_testing","nested_message_extension_for_testing.child_field_for_testing","[NULL]","nesting test"
 "string_extension_for_testing","string_extension_for_testing","[NULL]","an extension string!"
 "chrome_app_state","chrome_app_state","[NULL]","APP_STATE_FOREGROUND"
diff --git a/tools/BUILD.gn b/tools/BUILD.gn
index a8342f9..772f6a2 100644
--- a/tools/BUILD.gn
+++ b/tools/BUILD.gn
@@ -24,6 +24,7 @@
     ":copy_protoc",
     "compact_reencode",
     "ftrace_proto_gen",
+    "proto_filter",
     "protoprofile",
   ]
   if (is_linux || is_android) {
diff --git a/tools/add_test_trace.sh b/tools/add_test_data
similarity index 81%
rename from tools/add_test_trace.sh
rename to tools/add_test_data
index 1eea759..9e6b8c7 100755
--- a/tools/add_test_trace.sh
+++ b/tools/add_test_data
@@ -13,8 +13,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# Adds a file or directory to /test/data/, uploads a new test_data.zip to GCS
+# and updates the sha1 in tools/install-build-deps.
+
 set -e
 
+ROOT_DIR="$(dirname $(cd -P ${BASH_SOURCE[0]%/*}; pwd))"
+
 echo ""
 echo "Downloading latest copy of test data"
 echo ""
@@ -28,9 +33,19 @@
 unzip /tmp/latest-test-data.zip -d /tmp/latest-test-data
 
 echo ""
-echo "Copying trace to temp folder"
+echo "Copying $1 to temp folder"
 echo ""
-cp $1 /tmp/latest-test-data
+
+set -x
+if [ -d "$1" ]; then
+  DIR_NAME="$(basename $1)"
+  rm -rf "/tmp/latest-test-data/$DIR_NAME"
+  mkdir -p "/tmp/latest-test-data/$DIR_NAME"
+  cp -r "$1/" "/tmp/latest-test-data/$DIR_NAME/"
+else
+  cp "$1" /tmp/latest-test-data
+fi
+set +x
 
 echo ""
 echo "Zipping file back up"
diff --git a/tools/add_tp_diff_test.py b/tools/add_tp_diff_test.py
index 83ce9d0..419d4c0 100755
--- a/tools/add_tp_diff_test.py
+++ b/tools/add_tp_diff_test.py
@@ -84,7 +84,7 @@
   trace_file = ''
   if trace_type == 'proto':
     print('Proto traces should be added to the test-data zip '
-          'using the tools/add_test_trace.sh')
+          'using the tools/add_test_data')
     stdout_write('Provide the name of the trace (including any '
                  'extension) relative to test/data: ')
 
diff --git a/tools/check_include_violations b/tools/check_include_violations
index d86931b..7402a4d 100755
--- a/tools/check_include_violations
+++ b/tools/check_include_violations
@@ -36,8 +36,8 @@
   include_root = os.path.join(ROOT_DIR, 'include')
   for root, _, files in os.walk(include_root):
     for fname in files:
-      fpath = os.path.join(root, fname)
-      rel_path = os.path.relpath(fpath, ROOT_DIR)
+      fpath = os.path.join(root, fname).replace('\\', '/')  # For Windows.
+      rel_path = os.path.relpath(fpath, ROOT_DIR).replace('\\', '/')
       if not os.path.isfile(fpath):
         continue
       if fpath.endswith('.cc'):
diff --git a/tools/check_proto_comments b/tools/check_proto_comments
index 6e7392f..74b584b 100755
--- a/tools/check_proto_comments
+++ b/tools/check_proto_comments
@@ -35,7 +35,7 @@
         continue
       if not fpath.endswith('.proto'):
         continue
-      with open(fpath) as f:
+      with open(fpath, encoding='UTF-8') as f:
         lines = f.readlines()
       for line in lines:
         comm = line.find('//')
diff --git a/tools/ftrace_proto_gen/event_list b/tools/ftrace_proto_gen/event_list
index 45727f0..a4193d4 100644
--- a/tools/ftrace_proto_gen/event_list
+++ b/tools/ftrace_proto_gen/event_list
@@ -346,3 +346,10 @@
 g2d/tracing_mark_write
 mali/tracing_mark_write
 dmabuf_heap/dma_heap_stat
+cpuhp/cpuhp_pause
+sched/sched_pi_setprio
+sde/sde_evtlog
+sde/sde_perf_calc_crtc
+sde/sde_perf_crtc_update
+sde/sde_perf_set_qos_luts
+sde/sde_perf_update_bus
diff --git a/tools/gen_all b/tools/gen_all
index b065236..bcf6bd3 100755
--- a/tools/gen_all
+++ b/tools/gen_all
@@ -14,22 +14,25 @@
 # limitations under the License.
 
 from __future__ import print_function
+
 import os
 import argparse
 import subprocess
+import sys
 
 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+IS_WIN = sys.platform.startswith('win')
 
 
 def protoc_path(out_directory):
-  path = os.path.join(out_directory, 'protoc')
+  path = os.path.join(out_directory, 'protoc') + ('.exe' if IS_WIN else '')
   assert os.path.isfile(path)
   return path
 
 
 def call(cmd, *args):
   path = os.path.join('tools', cmd)
-  command = [path] + list(args)
+  command = ['python3', path] + list(args)
   print('Running', ' '.join(command))
   try:
     subprocess.check_call(command, cwd=ROOT_DIR)
@@ -51,12 +54,18 @@
     call('check_include_violations')
     call('check_proto_comments')
     call('fix_include_guards', *check_only)
-    call('gen_bazel', *check_only)
-    call('gen_android_bp', *check_only)
+    if not IS_WIN:
+      call('gen_bazel', *check_only)
+      call('gen_android_bp', *check_only)
     call('gen_merged_protos', *check_only)
     call('ninja', '-C', out, 'protoc')
     call('gen_binary_descriptors', '--protoc', protoc_path(out), *check_only)
 
+    if IS_WIN:
+      print('WARNING: Cannot generate BUILD / Android.bp from Windows. ' +
+            'They might be left stale and fail in the CI if you edited any ' +
+            'BUILD.gn file')
+
   except AssertionError as e:
     if not str(e):
       raise
diff --git a/tools/gen_amalgamated b/tools/gen_amalgamated
index bff4641..e2faa61 100755
--- a/tools/gen_amalgamated
+++ b/tools/gen_amalgamated
@@ -580,7 +580,7 @@
   args = parser.parse_args()
   targets = args.targets or default_targets
 
-  # The CHANGELOG mtime triggers the the perfetto_version.gen.h genrule. This is
+  # The CHANGELOG mtime triggers the perfetto_version.gen.h genrule. This is
   # to avoid emitting a stale version information in the remote case of somebody
   # running gen_amalgamated incrementally after having moved to another commit.
   changelog_path = os.path.join(project_root, 'CHANGELOG')
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index 9c4f408..cc8094b 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -163,11 +163,11 @@
   for line in (line.strip() for line in lines if not line.startswith('#')):
     assert os.path.exists(line), 'file %s should exist' % line
     if line.startswith('test/data/'):
-        # Skip test data files that require GCS. They are only for benchmarks.
-        # We don't run benchmarks in the android tree.
-        continue
-    if line.endswith('/'):
-      yield line + '**/*'
+      # Skip test data files that require GCS. They are only for benchmarks.
+      # We don't run benchmarks in the android tree.
+      continue
+    if line.endswith('/.'):
+      yield line[:-1] + '**/*'
     else:
       yield line
 
@@ -182,16 +182,16 @@
         ('cflags', {'-Wglobal-constructors', '-Werror=global-constructors'}),
         ('version_script', 'src/profiling/memory/heapprofd_client_api.map.txt'),
         ('stubs', {
-          'versions': ['S'],
-          'symbol_file': 'src/profiling/memory/heapprofd_client_api.map.txt',
+            'versions': ['S'],
+            'symbol_file': 'src/profiling/memory/heapprofd_client_api.map.txt',
         }),
         ('export_include_dirs', {'src/profiling/memory/include'}),
     ],
     'heapprofd_api_noop': [
         ('version_script', 'src/profiling/memory/heapprofd_client_api.map.txt'),
         ('stubs', {
-          'versions': ['S'],
-          'symbol_file': 'src/profiling/memory/heapprofd_client_api.map.txt',
+            'versions': ['S'],
+            'symbol_file': 'src/profiling/memory/heapprofd_client_api.map.txt',
         }),
         ('export_include_dirs', {'src/profiling/memory/include'}),
     ],
@@ -210,42 +210,42 @@
         ('include_dirs', {'bionic/libc/kernel'}),
     ],
     'perfetto_integrationtests': [
-      ('test_suites', {'general-tests'}),
-      ('test_config', 'PerfettoIntegrationTests.xml'),
+        ('test_suites', {'general-tests'}),
+        ('test_config', 'PerfettoIntegrationTests.xml'),
     ],
-    'traced_probes': [
-        ('required', {'libperfetto_android_internal',
-                      'trigger_perfetto',
-                      'traced_perf',
-                      'mm_events'}),
-    ],
+    'traced_probes': [('required', {
+        'libperfetto_android_internal', 'trigger_perfetto', 'traced_perf',
+        'mm_events'
+    }),],
     'libperfetto_android_internal': [('static_libs', {'libhealthhalutils'}),],
     'trace_processor_shell': [
-      ('strip', {'all': True}),
-      ('host', {
-        'stl': 'libc++_static',
-        'dist': {'targets': ['sdk_repo']},
-      }),
+        ('strip', {
+            'all': True
+        }),
+        ('host', {
+            'stl': 'libc++_static',
+            'dist': {
+                'targets': ['sdk_repo']
+            },
+        }),
     ],
     'libperfetto_client_experimental': [
-      ('apex_available', {
-        '//apex_available:platform',
-        'com.android.art',
-        'com.android.art.debug'}),
-      ('min_sdk_version', 'S'),
-      ('shared_libs', {'liblog'}),
-      ('export_include_dirs', {'include', buildflags_dir}),
+        ('apex_available', {
+            '//apex_available:platform', 'com.android.art',
+            'com.android.art.debug'
+        }),
+        ('min_sdk_version', 'S'),
+        ('shared_libs', {'liblog'}),
+        ('export_include_dirs', {'include', buildflags_dir}),
     ],
     'perfetto_trace_protos': [
-      ('apex_available', {
-        '//apex_available:platform',
-        'com.android.art',
-        'com.android.art.debug'}),
-      ('min_sdk_version', 'S'),
+        ('apex_available', {
+            '//apex_available:platform', 'com.android.art',
+            'com.android.art.debug'
+        }),
+        ('min_sdk_version', 'S'),
     ],
-    'libperfetto': [
-      ('export_include_dirs', {'include', buildflags_dir}),
-    ],
+    'libperfetto': [('export_include_dirs', {'include', buildflags_dir}),],
 }
 
 
@@ -325,6 +325,7 @@
 def enable_uapi_headers(module):
   module.include_dirs.add('bionic/libc/kernel')
 
+
 def enable_bionic_libc_platform_headers_on_android(module):
   module.header_libs.add('bionic_libc_platform_headers')
 
@@ -332,20 +333,32 @@
 # Android equivalents for third-party libraries that the upstream project
 # depends on.
 builtin_deps = {
-    '//gn:default_deps': lambda x: None,
-    '//gn:gtest_main': lambda x: None,
-    '//gn:protoc': lambda x: None,
-    '//gn:gtest_and_gmock': enable_gtest_and_gmock,
-    '//gn:libunwind': enable_libunwind,
-    '//gn:protobuf_full': enable_protobuf_full,
-    '//gn:protobuf_lite': enable_protobuf_lite,
-    '//gn:protoc_lib': enable_protoc_lib,
-    '//gn:libunwindstack': enable_libunwindstack,
-    '//gn:sqlite': enable_sqlite,
-    '//gn:zlib': enable_zlib,
-    '//gn:bionic_kernel_uapi_headers' : enable_uapi_headers,
+    '//gn:default_deps':
+        lambda x: None,
+    '//gn:gtest_main':
+        lambda x: None,
+    '//gn:protoc':
+        lambda x: None,
+    '//gn:gtest_and_gmock':
+        enable_gtest_and_gmock,
+    '//gn:libunwind':
+        enable_libunwind,
+    '//gn:protobuf_full':
+        enable_protobuf_full,
+    '//gn:protobuf_lite':
+        enable_protobuf_lite,
+    '//gn:protoc_lib':
+        enable_protoc_lib,
+    '//gn:libunwindstack':
+        enable_libunwindstack,
+    '//gn:sqlite':
+        enable_sqlite,
+    '//gn:zlib':
+        enable_zlib,
+    '//gn:bionic_kernel_uapi_headers':
+        enable_uapi_headers,
     '//src/profiling/memory:bionic_libc_platform_headers_on_android':
-      enable_bionic_libc_platform_headers_on_android,
+        enable_bionic_libc_platform_headers_on_android,
 }
 
 # ----------------------------------------------------------------------------
@@ -546,7 +559,6 @@
     output.append('}')
     output.append('')
 
-
   def add_android_static_lib(self, lib):
     if self.type == 'cc_binary_host':
       raise Exception('Adding Android static lib for host tool is unsupported')
@@ -555,7 +567,6 @@
     else:
       self.static_libs.add(lib)
 
-
   def add_android_shared_lib(self, lib):
     if self.type == 'cc_binary_host':
       raise Exception('Adding Android shared lib for host tool is unsupported')
@@ -564,7 +575,6 @@
     else:
       self.shared_libs.add(lib)
 
-
   def _output_field(self, output, name, sort=True):
     value = getattr(self, name)
     return write_blueprint_key_value(output, name, value, sort)
@@ -767,10 +777,8 @@
       'tools/gen_cc_proto_descriptor.py',
   ]
   module.cmd = ' '.join([
-      '$(location tools/gen_cc_proto_descriptor.py)',
-      '--gen_dir=$(genDir)',
-      '--cpp_out=$(out)',
-      '$(in)'
+      '$(location tools/gen_cc_proto_descriptor.py)', '--gen_dir=$(genDir)',
+      '--cpp_out=$(out)', '$(in)'
   ])
   module.genrule_headers.add(module.name)
   module.srcs.update(
@@ -784,14 +792,12 @@
   module = Module('genrule', bp_module_name, gn_utils.GEN_VERSION_TARGET)
   script_path = gn_utils.label_to_path(target.script)
   module.genrule_headers.add(bp_module_name)
-  module.tool_files = [ script_path ]
+  module.tool_files = [script_path]
   module.out.update(target.outputs)
   module.srcs.update(gn_utils.label_to_path(src) for src in target.inputs)
   module.cmd = ' '.join([
-        'python3 $(location %s)' % script_path,
-        '--no_git',
-        '--changelog=$(location CHANGELOG)',
-        '--cpp_out=$(out)'
+      'python3 $(location %s)' % script_path, '--no_git',
+      '--changelog=$(location CHANGELOG)', '--cpp_out=$(out)'
   ])
   blueprint.add_module(module)
   return module
@@ -821,6 +827,7 @@
     return blueprint.modules[bp_module_name]
   target = gn.get_target(gn_target_name)
 
+  name_without_toolchain = gn_utils.label_without_toolchain(target.name)
   if target.type == 'executable':
     if target.toolchain == gn_utils.HOST_TOOLCHAIN:
       module_type = 'cc_binary_host'
@@ -846,10 +853,10 @@
   elif target.type == 'action':
     if 'gen_merged_sql_metrics' in target.name:
       module = create_merged_sql_metrics_module(blueprint, target)
-    elif re.match('.*gen_cc_.*_descriptor$', target.name):
+    elif re.match('.*gen_cc_.*_descriptor$', name_without_toolchain):
       module = create_cc_proto_descriptor_module(blueprint, target)
-    elif target.type == 'action' and gn_utils.label_without_toolchain(
-        target.name) == gn_utils.GEN_VERSION_TARGET:
+    elif target.type == 'action' and \
+        name_without_toolchain == gn_utils.GEN_VERSION_TARGET:
       module = create_gen_version_module(blueprint, target, bp_module_name)
     else:
       raise Error('Unhandled action: {}'.format(target.name))
@@ -857,8 +864,7 @@
     raise Error('Unknown target %s (%s)' % (target.name, target.type))
 
   blueprint.add_module(module)
-  module.host_supported = (gn_utils.label_without_toolchain(target.name) in
-                           target_host_supported)
+  module.host_supported = (name_without_toolchain in target_host_supported)
   module.init_rc = target_initrc.get(target.name, [])
   module.srcs.update(
       gn_utils.label_to_path(src)
@@ -1033,7 +1039,7 @@
   # perfetto_component.gni is fixed.
   # Check for ODR violations
   # for target_name in default_targets:
-    # checker = gn_utils.ODRChecker(gn, target_name)
+  # checker = gn_utils.ODRChecker(gn, target_name)
 
   output = [
       """// Copyright (C) 2017 The Android Open Source Project
diff --git a/tools/gen_bazel b/tools/gen_bazel
index 11719bb..a8b7309 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -43,6 +43,8 @@
     'enable_perfetto_watchdog=true',
     'monolithic_binaries=true',
     'target_os="linux"',
+    'enable_perfetto_heapprofd=false',
+    'enable_perfetto_traced_perf=false',
 ])
 
 # Default targets to translate to the blueprint file.
@@ -318,13 +320,13 @@
   plugin_label.comment = target.name
   plugin_label.deps += [':' + sources_label_name]
 
-  # When using the cppgen plugin we need to pass down also the transitive deps.
+  # When using the plugins we need to pass down also the transitive deps.
   # For instance consider foo.proto including common.proto. The generated
   # foo.cc will #include "common.gen.h". Hence the generated cc_protocpp_library
   # rule need to pass down the dependency on the target that generates
-  # common.gen.{cc,h}. This is not needed for protozero because protozero
-  # headers are fully hermetic deps-wise and use only on forward declarations.
-  if target.proto_deps and target.proto_plugin in ('cppgen', 'ipc'):
+  # common.gen.{cc,h}.
+  if target.proto_deps and target.proto_plugin in (
+      'cppgen', 'ipc', 'protozero'):
     plugin_label.deps += [
         ':' + get_bazel_label_name(x) for x in target.proto_deps
     ]
@@ -361,10 +363,19 @@
   label = BazelLabel(get_bazel_label_name(gn_target.name), bazel_type)
   label.comment = gn_target.name
 
+  # Supporting 'public' on source_sets would require not converting them to
+  # filegroups in bazel.
+  if gn_target.public_headers:
+    if bazel_type == 'perfetto_cc_library':
+      label.hdrs += [x[2:] for x in gn_target.public_headers]
+    else:
+      raise Error('%s: \'public\' currently supported only for cc_library' %
+              gn_target.name)
+
   raw_srcs = [x[2:] for x in gn_target.sources]
   if bazel_type == 'perfetto_cc_library':
-    label.srcs = [x for x in raw_srcs if not x.startswith('include')]
-    label.hdrs = [x for x in raw_srcs if x.startswith('include')]
+    label.srcs += [x for x in raw_srcs if not x.startswith('include')]
+    label.hdrs += [x for x in raw_srcs if x.startswith('include')]
 
     # Most Perfetto libraries cannot by dynamically linked as they would
     # cause ODR violations.
diff --git a/tools/gen_binary_descriptors b/tools/gen_binary_descriptors
index d25d9bd..5388c48 100755
--- a/tools/gen_binary_descriptors
+++ b/tools/gen_binary_descriptors
@@ -25,14 +25,11 @@
 from compat import iteritems
 
 SOURCE_TARGET = [
-    (
-      'protos/perfetto/trace_processor/trace_processor.proto',
-      'src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor'
+    ('protos/perfetto/trace_processor/trace_processor.proto',
+     'src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor'
     ),
-    (
-      'protos/perfetto/metrics/metrics.proto',
-      'src/trace_processor/python/perfetto/trace_processor/metrics.descriptor'
-    ),
+    ('protos/perfetto/metrics/metrics.proto',
+     'src/trace_processor/python/perfetto/trace_processor/metrics.descriptor'),
 ]
 
 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
@@ -54,7 +51,6 @@
   return None
 
 
-
 def check(source, target):
   assert os.path.exists(os.path.join(ROOT_DIR, target)), \
       'Output file {} does not exist and so cannot be checked'.format(target)
@@ -75,7 +71,9 @@
 
 
 def generate(source, target, protoc_path):
-  with tempfile.NamedTemporaryFile() as fdescriptor:
+  # delete=False + manual unlink is required for Windows. Otherwise the temp
+  # file is kept locked exclusively and unaccassible until it's destroyed.
+  with tempfile.NamedTemporaryFile(delete=False) as fdescriptor:
     subprocess.check_call([
         protoc_path,
         '--include_imports',
@@ -84,10 +82,11 @@
             os.path.join(ROOT_DIR, "buildtools", "protobuf", "src"),
         '--descriptor_set_out={}'.format(fdescriptor.name),
         source,
-    ],
-                          cwd=ROOT_DIR)
+    ], cwd=ROOT_DIR)
 
     s = fdescriptor.read()
+    fdescriptor.close()
+    os.remove(fdescriptor.name)
     with open(target, 'wb') as out:
       out.write(s)
 
@@ -105,6 +104,7 @@
           source_hash=hash_path(os.path.join(source)),
       ).encode())
 
+
 def main():
   parser = argparse.ArgumentParser()
   parser.add_argument('--check-only', action='store_true')
diff --git a/tools/gen_merged_protos b/tools/gen_merged_protos
index 67adf4c..76040b4 100755
--- a/tools/gen_merged_protos
+++ b/tools/gen_merged_protos
@@ -22,7 +22,7 @@
 from codecs import open
 
 PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
-SELF_PATH = os.path.relpath(__file__, PROJECT_ROOT)
+SELF_PATH = os.path.relpath(__file__, PROJECT_ROOT).replace('\\', '/')
 
 CONFIG_PROTO_ROOTS = [
     'protos/perfetto/common/data_source_descriptor.proto',
@@ -32,7 +32,7 @@
 MERGED_CONFIG_PROTO = 'protos/perfetto/config/perfetto_config.proto'
 
 TRACE_PROTO_ROOTS = CONFIG_PROTO_ROOTS + [
-  'protos/perfetto/trace/trace.proto',
+    'protos/perfetto/trace/trace.proto',
 ]
 MERGED_TRACE_PROTO = 'protos/perfetto/trace/perfetto_trace.proto'
 
diff --git a/tools/gn b/tools/gn
index fc0f354..51141bf 100755
--- a/tools/gn
+++ b/tools/gn
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env python3
 # Copyright (C) 2017 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,5 +13,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-CMD="gn"
-source "$(dirname "${BASH_SOURCE[0]}")/run-buildtools-binary.sh"
+import os
+import sys
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.append(ROOT_DIR)
+__package__ = 'tools'
+from .run_buildtools_binary import run_buildtools_binary
+run_buildtools_binary(['gn'] + sys.argv[1:])
diff --git a/tools/gn_utils.py b/tools/gn_utils.py
index 45c5885..2a0f8c6 100644
--- a/tools/gn_utils.py
+++ b/tools/gn_utils.py
@@ -60,7 +60,9 @@
 
 
 def _tool_path(name):
-  return os.path.join(repo_root(), 'tools', name)
+  wrapper = os.path.abspath(
+      os.path.join(repo_root(), 'tools', 'run_buildtools_binary.py'))
+  return ['python3', wrapper, name]
 
 
 def prepare_out_directory(gn_args, name, root=repo_root()):
@@ -75,18 +77,17 @@
   except OSError as e:
     if e.errno != errno.EEXIST:
       raise
-  _check_command_output([_tool_path('gn'), 'gen', out,
-                         '--args=%s' % gn_args],
-                        cwd=repo_root())
+  _check_command_output(
+      _tool_path('gn') + ['gen', out, '--args=%s' % gn_args], cwd=repo_root())
   return out
 
 
 def load_build_description(out):
   """Creates the JSON build description by running GN."""
-  desc = _check_command_output([
-      _tool_path('gn'), 'desc', out, '--format=json', '--all-toolchains', '//*'
-  ],
-                               cwd=repo_root())
+  desc = _check_command_output(
+      _tool_path('gn') +
+      ['desc', out, '--format=json', '--all-toolchains', '//*'],
+      cwd=repo_root())
   return json.loads(desc)
 
 
@@ -111,14 +112,14 @@
   targets = [t.replace('//', '') for t in targets]
   with open(os.devnull, 'w') as devnull:
     stdout = devnull if quiet else None
-    subprocess.check_call(
-        [_tool_path('ninja')] + targets, cwd=out, stdout=stdout)
+    cmd = _tool_path('ninja') + targets
+    subprocess.check_call(cmd, cwd=os.path.abspath(out), stdout=stdout)
 
 
 def compute_source_dependencies(out):
   """For each source file, computes a set of headers it depends on."""
-  ninja_deps = _check_command_output([_tool_path('ninja'), '-t', 'deps'],
-                                     cwd=out)
+  ninja_deps = _check_command_output(
+      _tool_path('ninja') + ['-t', 'deps'], cwd=out)
   deps = {}
   current_source = None
   for line in ninja_deps.split('\n'):
@@ -311,6 +312,9 @@
       self.proto_paths = set()
 
       self.sources = set()
+      # TODO(primiano): consider whether the public section should be part of
+      # bubbled-up sources.
+      self.public_headers = set()  # 'public'
 
       # These are valid only for type == 'action'
       self.inputs = set()
@@ -416,6 +420,14 @@
       # because root build dir is typically out/xxx/).
       target.args = [re.sub('^../../', '//', x) for x in desc['args']]
 
+    # Default for 'public' is //* - all headers in 'sources' are public.
+    # TODO(primiano): if a 'public' section is specified (even if empty), then
+    # the rest of 'sources' is considered inaccessible by gn. Consider
+    # emulating that, so that generated build files don't end up with overly
+    # accessible headers.
+    public_headers = [x for x in desc.get('public', []) if x != '*']
+    target.public_headers.update(public_headers)
+
     target.cflags.update(desc.get('cflags', []) + desc.get('cflags_cc', []))
     target.libs.update(desc.get('libs', []))
     target.ldflags.update(desc.get('ldflags', []))
diff --git a/tools/heap_profile b/tools/heap_profile
index 41b386d..f3ee35a 100755
--- a/tools/heap_profile
+++ b/tools/heap_profile
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # Copyright (C) 2017 The Android Open Source Project
 #
@@ -33,8 +33,8 @@
 
 
 TRACE_TO_TEXT_SHAS = {
-    'linux': '0df4f571099135a29659caf35fd5e343ef59bdc3',
-    'mac': '71b86dfd10409fd9ad9c41cc06cd8533bd2ffa85',
+    'linux': '7e3e10dfb324e31723efd63ac25037856e06eba0',
+    'mac': '21f0f42dd019b4f09addd404a114fbf2322ca8a4',
 }
 TRACE_TO_TEXT_PATH = tempfile.gettempdir()
 TRACE_TO_TEXT_BASE_URL = ('https://storage.googleapis.com/perfetto/')
@@ -164,6 +164,25 @@
   ).decode('utf-8').strip()
   return codename == release
 
+ORDER = ['-n', '-p', '-i', '-o']
+
+def arg_order(action):
+  result = len(ORDER)
+  for opt in action.option_strings:
+    if opt in ORDER:
+      result = min(ORDER.index(opt), result)
+  return result, action.option_strings[0].strip('-')
+
+def print_options(parser):
+  for action in sorted(parser._actions, key=arg_order):
+    if action.help is argparse.SUPPRESS:
+      continue
+    opts = ', '.join('`' + x + '`' for x in action.option_strings)
+    metavar = '' if action.metavar is None else ' _'     + action.metavar + '_'
+    print('{}{}'.format(opts, metavar))
+    print(':    {}'.format(action.help))
+    print()
+
 def main(argv):
   parser = argparse.ArgumentParser()
   parser.add_argument(
@@ -294,8 +313,16 @@
       help="Output directory.",
       metavar="DIRECTORY",
       default=None)
+  parser.add_argument(
+      "--print-options",
+      action="store_true",
+      help=argparse.SUPPRESS
+  )
 
   args = parser.parse_args()
+  if args.print_options:
+    print_options(parser)
+    return 0
   fail = False
   if args.block_client and args.no_block_client:
     print(
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 7e5c0e3..835ee9c 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -25,7 +25,7 @@
 import zipfile
 
 from collections import namedtuple
-from platform import system
+from platform import system, machine
 
 # The format for the deps below is the following:
 # (target_folder, source_url, sha1, target_platform)
@@ -36,11 +36,14 @@
 # |target_folder|, taking care of stripping the root folder if it's a single
 # root (to avoid ending up with buildtools/protobuf/protobuf-1.2.3/... and have
 # instead just buildtools/protobuf).
-# |target_platform| is either 'darwin', 'linux' or 'all' and applies the dep
-# only on the given platform
+# |target_os| is either 'darwin', 'linux', 'windows' or 'all'
+# |target_arch| is either 'x64', 'aarch64' or 'all'
+# in both cases the dep only applies on matching platforms
+# |target_arch| can be 'all' when 'target_os' is not 'all' for example in the
+# case of MacOS universal binaries.
 Dependency = namedtuple(
     'Dependency',
-    ['target_folder', 'source_url', 'checksum', 'target_platform'])
+    ['target_folder', 'source_url', 'checksum', 'target_os', 'target_arch'])
 
 # This is to remove old directories when build tools get {re,}moved. This is to
 # avoid accidentally referring to stale dir in custom user scripts.
@@ -52,24 +55,24 @@
 ]
 
 # Dependencies required to build code on the host or when targeting desktop OS.
-BUILD_DEPS_HOST = [
+BUILD_DEPS_TOOLCHAIN_HOST = [
     # GN. From https://chrome-infra-packages.appspot.com/dl/gn/gn/.
     # git_revision:83dad00afb232d7235dd70dff1ee90292d72a01e .
     Dependency(
         'buildtools/mac/gn',
         'https://storage.googleapis.com/perfetto/gn-mac-1695-83dad00a',
         '513d3adeb56b745e62af4e3ccb76b76f023c6aaa25d6a2be9a89e44cd10a4c1a',
-        'darwin'),
+        'darwin', 'x64'),
     Dependency(
         'buildtools/linux64/gn',
         'https://storage.googleapis.com/perfetto/gn-linux64-1695-83dad00a',
         '4f589364153f182b05cd845e93407489d6ce8acc03290c897928a7bd22b20cce',
-        'linux'),
+        'linux', 'x64'),
     Dependency(
         'buildtools/win/gn.exe',
         'https://storage.googleapis.com/perfetto/gn-win-1695-83dad00a',
         '908c29556539292203d2952ebf55df03697cbc7cf526a3e295f31ba2576e4cac',
-        'windows'),
+        'windows', 'x64'),
 
     # clang-format
     # From https://chromium.googlesource.com/chromium/src/buildtools/+/refs/heads/master/mac/clang-format.sha1
@@ -77,70 +80,42 @@
         'buildtools/mac/clang-format',
         'https://storage.googleapis.com/chromium-clang-format/62bde1baa7196ad9df969fc1f06b66360b1a927b',
         '6df686a937443cbe6efc013467a7ba5f98d3f187eb7765bb7abc6ce47626cf66',
-        'darwin'),
+        'darwin', 'x64'),
     # From https://chromium.googlesource.com/chromium/src/buildtools/+/refs/heads/master/linux64/clang-format.sha1
     Dependency(
         'buildtools/linux64/clang-format',
         'https://storage.googleapis.com/chromium-clang-format/1baf0089e895c989a311b6a38ed94d0e8be4c0a7',
         'd02a97a87e8c28898033aaf5986967b24dc47ebd5b376e1cd93e5009f22cd75e',
-        'linux'),
+        'linux', 'x64'),
     # From https://chromium.googlesource.com/chromium/src/buildtools/+/refs/heads/master/win/clang-format.exe.sha1
     Dependency(
         'buildtools/win/clang-format.exe',
         'https://storage.googleapis.com/chromium-clang-format/d4afd4eba27022f5f6d518133aebde57281677c9',
         '2ba1b4d3ade90ea80316890b598ab5fc16777572be26afec6ce23117da121b80',
-        'windows'),
+        'windows', 'x64'),
 
     # Keep the SHA1 in sync with |clang_format_rev| in chromium //buildtools/DEPS.
     Dependency(
         'buildtools/clang_format/script',
         'https://chromium.googlesource.com/chromium/llvm-project/cfe/tools/clang-format.git',
-        '96636aa0e9f047f17447f2d45a094d0b59ed7917', 'all'),
+        '96636aa0e9f047f17447f2d45a094d0b59ed7917', 'all', 'all'),
 
     # Ninja
     Dependency(
         'buildtools/mac/ninja',
         'https://storage.googleapis.com/perfetto/ninja-mac-c15b0698da038b2bd2e8970c14c75fadc06b1add',
         '4224b90734590b0148ad8ee63ee7b295e88e0652e4d1f4271ef2b91d880b0e19',
-        'darwin'),
+        'darwin', 'x64'),
     Dependency(
         'buildtools/linux64/ninja',
         'https://storage.googleapis.com/perfetto/ninja-linux64-c866952bda50c29a669222477309287119bbb7e8',
         '54ac6a01362190aaabf4cf276f9c8982cdf11b225438940fdde3339be0f2ecdc',
-        'linux'),
+        'linux', 'x64'),
     Dependency(
         'buildtools/win/ninja.exe',
         'https://storage.googleapis.com/perfetto/ninja-win-4a5f05c24afef05ef03329a1bbfedee0678b524a',
         '6f8af488be74ed8787d04e107080d05330587a4198ba047bd5b7f5b0c3150d61',
-        'windows'),
-
-    # Keep in sync with Android's //external/googletest/README.version.
-    Dependency(
-        'buildtools/googletest',
-        'https://android.googlesource.com/platform/external/googletest.git',
-        '3f05f651ae3621db58468153e32016bc1397800b', 'all'),
-
-    # Keep in sync with Chromium's //third_party/protobuf.
-    Dependency(
-        'buildtools/protobuf',
-        'https://chromium.googlesource.com/external/github.com/google/protobuf.git',
-        '6a59a2ad1f61d9696092f79b6d74368b4d7970a3',  # refs/tags/v3.9.0
-        'all'),
-
-    # libc++, libc++abi and libunwind for Linux where we need to rebuild the C++
-    # lib from sources. Keep the SHA1s in sync with Chrome's src/buildtools/DEPS.
-    Dependency(
-        'buildtools/libcxx',
-        'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxx.git',
-        'd9040c75cfea5928c804ab7c235fed06a63f743a', 'all'),
-    Dependency(
-        'buildtools/libcxxabi',
-        'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxxabi.git',
-        '196ba1aaa8ac285d94f4ea8d9836390a45360533', 'all'),
-    Dependency(
-        'buildtools/libunwind',
-        'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libunwind.git',
-        'd999d54f4bca789543a2eb6c995af2d9b5a1f3ed', 'all'),
+        'windows', 'x64'),
 
     # Keep the revision in sync with Chrome's PACKAGE_VERSION in
     # tools/clang/scripts/update.py.
@@ -148,24 +123,54 @@
         'buildtools/linux64/clang.tgz',
         'https://commondatastorage.googleapis.com/chromium-browser-clang/Linux_x64/clang-llvmorg-12-init-5035-gd0abc757-3.tgz',
         'b0c3015209b6d624844ad230064eb5c9b4429a2eafd4854981e73217c563d93d',
-        'linux'),
+        'linux', 'x64'),
     Dependency(
         'buildtools/win/clang.tgz',
         'https://commondatastorage.googleapis.com/chromium-browser-clang/Win/clang-llvmorg-12-init-5035-gd0abc757-3.tgz',
         'b2854d871a466e3a060469b5edb24ca355ef64576d38778f64acbd3c6d7cf530',
-        'windows'),
+        'windows', 'x64'),
+]
+
+BUILD_DEPS_HOST = [
+    # Keep in sync with Android's //external/googletest/README.version.
+    Dependency(
+        'buildtools/googletest',
+        'https://android.googlesource.com/platform/external/googletest.git',
+        '3f05f651ae3621db58468153e32016bc1397800b', 'all', 'all'),
+
+    # Keep in sync with Chromium's //third_party/protobuf.
+    Dependency(
+        'buildtools/protobuf',
+        'https://chromium.googlesource.com/external/github.com/google/protobuf.git',
+        '6a59a2ad1f61d9696092f79b6d74368b4d7970a3',  # refs/tags/v3.9.0
+        'all', 'all'),
+
+    # libc++, libc++abi and libunwind for Linux where we need to rebuild the C++
+    # lib from sources. Keep the SHA1s in sync with Chrome's src/buildtools/DEPS.
+    Dependency(
+        'buildtools/libcxx',
+        'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxx.git',
+        'd9040c75cfea5928c804ab7c235fed06a63f743a', 'all', 'all'),
+    Dependency(
+        'buildtools/libcxxabi',
+        'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxxabi.git',
+        '196ba1aaa8ac285d94f4ea8d9836390a45360533', 'all', 'all'),
+    Dependency(
+        'buildtools/libunwind',
+        'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libunwind.git',
+        'd999d54f4bca789543a2eb6c995af2d9b5a1f3ed', 'all', 'all'),
 
     # Keep in sync with chromium DEPS.
     Dependency(
         'buildtools/libfuzzer',
         'https://chromium.googlesource.com/chromium/llvm-project/compiler-rt/lib/fuzzer.git',
-        'debe7d2d1982e540fbd6bd78604bf001753f9e74', 'linux'),
+        'debe7d2d1982e540fbd6bd78604bf001753f9e74', 'linux', 'all'),
 
     # Benchmarking tool.
     Dependency(
         'buildtools/benchmark',
         'https://chromium.googlesource.com/external/github.com/google/benchmark.git',
-        '090faecb454fbd6e6e17a75ef8146acb037118d4', 'all'),
+        '090faecb454fbd6e6e17a75ef8146acb037118d4', 'all', 'all'),
 
     # Libbacktrace, for stacktraces in Linux/Android debug builds.
     # From https://github.com/ianlancetaylor/libbacktrace/archive/177940370e4a6b2509e92a0aaa9749184e64af43.zip
@@ -173,7 +178,7 @@
         'buildtools/libbacktrace.zip',
         'https://storage.googleapis.com/perfetto/libbacktrace-177940370e4a6b2509e92a0aaa9749184e64af43.zip',
         '21ac9a4209f7aeef766c482be53a7fa365063c031c7077e2070b491202983b31',
-        'all'),
+        'all', 'all'),
 
     # Sqlite for the trace processing library.
     # This is the amalgamated source whose compiled output is meant to be faster.
@@ -183,12 +188,12 @@
         'buildtools/sqlite.zip',
         'https://storage.googleapis.com/perfetto/sqlite-amalgamation-3350400.zip',
         'f3bf0df69f5de0675196f4644e05d07dbc698d674dc563a12eff17d5b215cdf5',
-        'all'),
+        'all', 'all'),
     Dependency(
         'buildtools/sqlite_src',
         'https://chromium.googlesource.com/external/github.com/sqlite/sqlite.git',
         'ee3686eb50c0e3dbb087c9a0976f7e37e1b014ae',  # refs/tags/version-3.32.3.
-        'all'),
+        'all', 'all'),
 
     # JsonCpp for legacy json import. Used only by the trace processor in
     # standalone builds.
@@ -196,48 +201,48 @@
         'buildtools/jsoncpp',
         'https://chromium.googlesource.com/external/github.com/open-source-parsers/jsoncpp.git',
         '6aba23f4a8628d599a9ef7fa4811c4ff6e4070e2',  # refs/tags/1.9.3.
-        'all'),
+        'all', 'all'),
 
     # These dependencies are for libunwindstack, which is used by src/profiling.
     Dependency('buildtools/android-core',
                'https://android.googlesource.com/platform/system/core.git',
-               '9e6cef7f07d8c11b3ea820938aeb7ff2e9dbaa52', 'all'),
+               '9e6cef7f07d8c11b3ea820938aeb7ff2e9dbaa52', 'all', 'all'),
     Dependency(
         'buildtools/android-unwinding',
         'https://android.googlesource.com/platform/system/unwinding.git',
-        '045391306dc6d9fd16a40af612df0400def0e865', 'all'),
+        '5150e1292ec6b16e4717e86b9e3cfb855eec18a3', 'all', 'all'),
     Dependency('buildtools/android-logging',
                'https://android.googlesource.com/platform/system/logging.git',
-               '7b36b566c9113fc703d68f76e8f40c0c2432481c', 'all'),
+               '7b36b566c9113fc703d68f76e8f40c0c2432481c', 'all', 'all'),
     Dependency('buildtools/android-libbase',
                'https://android.googlesource.com/platform/system/libbase.git',
-               '78f1c2f83e625bdf66d55b48bdb3a301c20d2fb3', 'all'),
+               '78f1c2f83e625bdf66d55b48bdb3a301c20d2fb3', 'all', 'all'),
     Dependency(
         'buildtools/android-libprocinfo',
         'https://android.googlesource.com/platform/system/libprocinfo.git',
-        'fd214c13ededecae97a3b15b5fccc8925a749a84', 'all'),
+        'fd214c13ededecae97a3b15b5fccc8925a749a84', 'all', 'all'),
     Dependency('buildtools/lzma',
                'https://android.googlesource.com/platform/external/lzma.git',
-               '7851dce6f4ca17f5caa1c93a4e0a45686b1d56c3', 'all'),
+               '7851dce6f4ca17f5caa1c93a4e0a45686b1d56c3', 'all', 'all'),
     Dependency('buildtools/zlib',
                'https://android.googlesource.com/platform/external/zlib.git',
-               '5c85a2da4c13eda07f69d81a1579a5afddd35f59', 'all'),
+               '5c85a2da4c13eda07f69d81a1579a5afddd35f59', 'all', 'all'),
     Dependency('buildtools/bionic',
                'https://android.googlesource.com/platform/bionic.git',
-               '332065d57e734b65f56474d136d22d767e36cbcd', 'all'),
+               '332065d57e734b65f56474d136d22d767e36cbcd', 'all', 'all'),
 
     # Example traces for regression tests.
     Dependency(
         'test/data.zip',
-        'https://storage.googleapis.com/perfetto/test-data-20210331-182333.zip',
-        'a9b6afb4b33f6aa9dd7ffa0a8c84fb2853c44a10f7f4d918d626f948ef602083',
-        'all',
+        'https://storage.googleapis.com/perfetto/test-data-20210604-141038.zip',
+        'f202d92ea541b7072562b579470771a5e2b414572a5421c501ea0785a57726eb',
+        'all', 'all',
     ),
 
     # Linenoise, used only by trace_processor in standalone builds.
     Dependency('buildtools/linenoise',
                'https://fuchsia.googlesource.com/third_party/linenoise.git',
-               'c894b9e59f02203dbe4e2be657572cf88c4230c3', 'all'),
+               'c894b9e59f02203dbe4e2be657572cf88c4230c3', 'all', 'all'),
 ]
 
 # Dependencies required to build Android code.
@@ -250,12 +255,12 @@
         'buildtools/ndk.zip',
         'https://dl.google.com/android/repository/android-ndk-r21e-darwin-x86_64.zip',
         '437278103a3db12632c05b1be5c41bbb8522791a67e415cc54411a65366f499d',
-        'darwin'),
+        'darwin', 'x64'),
     Dependency(
         'buildtools/ndk.zip',
         'https://dl.google.com/android/repository/android-ndk-r21e-linux-x86_64.zip',
         'ad7ce5467e18d40050dc51b8e7affc3e635c85bd8c59be62de32352328ed467e',
-        'linux'),
+        'linux', 'x64'),
 ]
 
 # Dependencies required to run Android tests.
@@ -265,25 +270,25 @@
         'buildtools/aosp-arm.zip',
         'https://storage.googleapis.com/perfetto/aosp-02022018-arm.zip',
         'f5c7a3a22ad7aa0bd14ba467e8697e1e917d306699bd25622aa4419a413b9b67',
-        'all'),
+        'all', 'all'),
 
     # platform-tools.zip contains adb binaries.
     Dependency(
         'buildtools/android_sdk/platform-tools.zip',
         'https://dl.google.com/android/repository/platform-tools_r26.0.0-darwin.zip',
         '98d392cbd21ca20d643c7e1605760cc49075611e317c534096b5564053f4ac8e',
-        'darwin'),
+        'darwin', 'x64'),
     Dependency(
         'buildtools/android_sdk/platform-tools.zip',
         'https://dl.google.com/android/repository/platform-tools_r26.0.0-linux.zip',
         '90208207521d85abf0d46e3374aa4e04b7aff74e4f355c792ac334de7a77e50b',
-        'linux'),
+        'linux', 'x64'),
 
     # Android emulator binaries.
     Dependency(
         'buildtools/emulator',
         'https://android.googlesource.com/platform/prebuilts/android-emulator.git',
-        '4b260028dc27bc92c39bee9129cb2ba839970956', 'all'),
+        '4b260028dc27bc92c39bee9129cb2ba839970956', 'all', 'x64'),
 ]
 
 # This variable is updated by tools/roll-catapult-trace-viewer.
@@ -296,30 +301,30 @@
         'buildtools/mac/nodejs.tgz',
         'https://storage.googleapis.com/chromium-nodejs/14.15.4/17ba7216e09de1bffb9dc80b7ec617a1cee40330',
         'b81a466347d2ae34b1370b6681ba173e9fb082338170a41624b37be7a2052b7e',
-        'darwin'),
+        'darwin', 'x64'),
     Dependency(
         'buildtools/linux64/nodejs.tgz',
         'https://storage.googleapis.com/chromium-nodejs/14.15.4/b2e40ddbac04d05baafbb007f203c6663c9d4ca9',
         '5aa88f1e2bf036950790277f3431634f64044ec78362f3e4f0dc8da28d61e9a4',
-        'linux'),
+        'linux', 'x64'),
     Dependency(
         'buildtools/mac/emsdk.tgz',
         'https://storage.googleapis.com/perfetto/emscripten-2.0.12-mac.tgz',
         'aa125f8c8ff8a386d43e18c8ea0c98c875cc19160a899403e8967a5478f96f31',
-        'darwin'),
+        'darwin', 'x64'),
     Dependency(
         'buildtools/linux64/emsdk.tgz',
         'https://storage.googleapis.com/perfetto/emscripten-2.0.12-linux.tgz',
         'bfff9fb0326363c12e19b542f27a5f12cedbfc310f30621dc497c9af51d2d2e3',
-        'linux'),
+        'linux', 'x64'),
     Dependency(
         'buildtools/catapult_trace_viewer.tgz',
         'https://storage.googleapis.com/perfetto/catapult_trace_viewer-%s.tar.gz'
-        % CATAPULT_SHA256, CATAPULT_SHA256, 'all'),
+        % CATAPULT_SHA256, CATAPULT_SHA256, 'all', 'all'),
     Dependency(
         'buildtools/typefaces.tgz',
         'https://storage.googleapis.com/perfetto/typefaces-%s.tar.gz' %
-        TYPEFACES_SHA256, TYPEFACES_SHA256, 'all')
+        TYPEFACES_SHA256, TYPEFACES_SHA256, 'all', 'all')
 ]
 
 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -332,6 +337,15 @@
   subprocess.check_call(['curl', '-L', '-#', '-o', out_file, url])
 
 
+def GetArch():
+  arch = machine()
+  if arch == 'aarch64':
+    return 'aarch64'
+  else:
+    # Assume everything else is x64 matching previous behaviour.
+    return 'x64'
+
+
 def ReadFile(path):
   if not os.path.exists(path):
     return None
@@ -414,9 +428,9 @@
     logging.info('Clearing %s', node_modules)
     subprocess.check_call(['git', 'clean', '-qxffd', node_modules],
                           cwd=ROOT_DIR)
-  logging.info("Running npm install in {0}".format(UI_DIR))
-  subprocess.check_call(
-      [os.path.join(TOOLS_DIR, 'npm'), 'install', '--no-save'], cwd=UI_DIR)
+  logging.info("Running `npm ci` in {0}".format(UI_DIR))
+  # `npm ci` is like `npm install` but respects package-lock.json.
+  subprocess.check_call([os.path.join(TOOLS_DIR, 'npm'), 'ci'], cwd=UI_DIR)
   # pbjs has the bad habit of installing extra packages on its first run. Run
   # it here, so we avoid fetches while building.
   node_bin = os.path.join(TOOLS_DIR, 'node')
@@ -464,11 +478,15 @@
   parser.add_argument('--check-only')
   parser.add_argument('--filter', default='')
   parser.add_argument('--verify', help='Check all URLs', action='store_true')
+  parser.add_argument('--no-toolchain', help='Do not download toolchain',
+                      action='store_true')
   args = parser.parse_args()
   if args.verify:
     CheckHashes()
     return 0
   deps = BUILD_DEPS_HOST
+  if not args.no_toolchain:
+    deps += BUILD_DEPS_TOOLCHAIN_HOST
   if args.android:
     deps += BUILD_DEPS_ANDROID + TEST_DEPS_ANDROID
   if args.ui:
@@ -480,8 +498,11 @@
     RmtreeIfExists(os.path.join(ROOT_DIR, old_dir))
 
   for dep in deps:
-    if (dep.target_platform != 'all' and
-        dep.target_platform != system().lower()):
+    target_os = system().lower()
+    target_arch = GetArch()
+    matches_os = dep.target_os == 'all' or target_os == dep.target_os
+    matches_arch = dep.target_arch == 'all' or target_arch == dep.target_arch
+    if not matches_os or not matches_arch:
       continue
     if args.filter and args.filter not in dep.target_folder:
       continue
@@ -550,7 +571,7 @@
   if args.ui:
     # Needs to happen after nodejs is installed above.
     if args.check_only:
-      deps_updated = not CheckNodeModules()
+      deps_updated |= not CheckNodeModules()
     else:
       InstallNodeModules(force_clean=nodejs_updated)
 
@@ -559,7 +580,7 @@
       with open(args.check_only, 'w') as f:
         f.write('OK')  # The content is irrelevant, just keep GN happy.
       return 0
-    argz = ' '.join([x for x in sys.argv[1:] if '--check-only' not in x])
+    argz = ' '.join([x for x in sys.argv[1:] if not x.startswith('--check-only')])
     print('\033[91mBuild deps are stale. ' +
           'Please run tools/install-build-deps %s\033[0m' % argz)
     return 1
diff --git a/tools/java_heap_dump b/tools/java_heap_dump
index ced597e..a0e69220 100755
--- a/tools/java_heap_dump
+++ b/tools/java_heap_dump
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # Copyright (C) 2020 The Android Open Source Project
 #
@@ -24,6 +24,7 @@
 import sys
 import tempfile
 import time
+import uuid
 
 NULL = open(os.devnull)
 
@@ -61,10 +62,26 @@
       }}
 """
 
-PERFETTO_CMD = ('CFG=\'{cfg}\'; echo ${{CFG}} | '
-                'perfetto --txt -c - -o '
-                '/data/misc/perfetto-traces/java-profile-{user} -d')
+UUID = str(uuid.uuid4())[-6:]
+PROFILE_PATH = '/data/misc/perfetto-traces/java-profile-' +  UUID
 
+PERFETTO_CMD = ('CFG=\'{cfg}\'; echo ${{CFG}} | '
+                'perfetto --txt -c - -o ' + PROFILE_PATH + ' -d')
+
+SDK = {
+    'S': 31,
+}
+
+def release_or_newer(release):
+  sdk = int(subprocess.check_output(
+    ['adb', 'shell', 'getprop', 'ro.system.build.version.sdk']
+  ).decode('utf-8').strip())
+  if sdk >= SDK[release]:
+    return True
+  codename = subprocess.check_output(
+    ['adb', 'shell', 'getprop', 'ro.build.version.codename']
+  ).decode('utf-8').strip()
+  return codename == release
 
 def main(argv):
   parser = argparse.ArgumentParser()
@@ -107,11 +124,20 @@
   parser.add_argument(
       "--stop-when-done",
       action="store_true",
-      help="On recent builds of S, use a new method to stop the profile when "
-           "the dump is done. Previously, we would hardcode a duration.")
+      default=None,
+      help="Use a new method to stop the profile when the dump is done. "
+      "Previously, we would hardcode a duration. Available and default on S.")
+  parser.add_argument(
+      "--no-stop-when-done",
+      action="store_false",
+      dest='stop_when_done',
+      help="Do not use a new method to stop the profile when the dump is done.")
 
   args = parser.parse_args()
 
+  if args.stop_when_done is None:
+    args.stop_when_done = release_or_newer('S')
+
   fail = False
   if args.pid is None and args.name is None:
     print("FATAL: Neither PID nor NAME given.", file=sys.stderr)
@@ -147,8 +173,6 @@
     continuous_dump_cfg = CONTINUOUS_DUMP.format(
         dump_interval=args.continuous_dump)
 
-  # TODO(fmayer): Once the changes have been in S for long enough, make this
-  #               the default for S+.
   if args.stop_when_done:
     duration_ms = 1000
     data_source_stop_timeout_ms = 100000
@@ -168,10 +192,11 @@
     print(cfg)
     return 0
 
-  user = subprocess.check_output(['adb', 'shell', 'whoami']).strip()
+  user = subprocess.check_output(
+    ['adb', 'shell', 'whoami']).strip().decode('utf8')
   perfetto_pid = subprocess.check_output(
       ['adb', 'exec-out',
-       PERFETTO_CMD.format(cfg=cfg, user=user)]).strip()
+       PERFETTO_CMD.format(cfg=cfg, user=user)]).strip().decode('utf8')
   try:
     int(perfetto_pid.strip())
   except ValueError:
@@ -188,8 +213,10 @@
     time.sleep(1)
 
   subprocess.check_call(
-    ['adb', 'pull', '/data/misc/perfetto-traces/java-profile-{}'.format(user),
-     output_file], stdout=NULL)
+    ['adb', 'pull', PROFILE_PATH, output_file], stdout=NULL)
+
+  subprocess.check_call(
+        ['adb', 'shell', 'rm', PROFILE_PATH], stdout=NULL)
 
   print("Wrote profile to {}".format(output_file))
   print("This can be viewed using https://ui.perfetto.dev.")
diff --git a/tools/ninja b/tools/ninja
index 724ccb0..67b8ed9 100755
--- a/tools/ninja
+++ b/tools/ninja
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env python3
 # Copyright (C) 2017 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,5 +13,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-CMD="ninja"
-source "$(dirname "${BASH_SOURCE[0]}")/run-buildtools-binary.sh"
+import os
+import sys
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.append(ROOT_DIR)
+__package__ = 'tools'
+from .run_buildtools_binary import run_buildtools_binary
+run_buildtools_binary(['ninja'] + sys.argv[1:])
diff --git a/tools/proto_filter/BUILD.gn b/tools/proto_filter/BUILD.gn
new file mode 100644
index 0000000..b2b6997
--- /dev/null
+++ b/tools/proto_filter/BUILD.gn
@@ -0,0 +1,29 @@
+# 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_host_executable.gni")
+
+perfetto_host_executable("proto_filter") {
+  testonly = true
+  deps = [
+    "../../gn:default_deps",
+    "../../gn:protobuf_full",
+    "../../src/base",
+    "../../src/protozero",
+    "../../src/protozero/filtering:bytecode_generator",
+    "../../src/protozero/filtering:filter_util",
+    "../../src/protozero/filtering:message_filter",
+  ]
+  sources = [ "proto_filter.cc" ]
+}
diff --git a/tools/proto_filter/proto_filter.cc b/tools/proto_filter/proto_filter.cc
new file mode 100644
index 0000000..e7443cb
--- /dev/null
+++ b/tools/proto_filter/proto_filter.cc
@@ -0,0 +1,294 @@
+/*
+ * 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/file_utils.h"
+#include "perfetto/ext/base/getopt.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/version.h"
+#include "src/protozero/filtering/filter_util.h"
+#include "src/protozero/filtering/message_filter.h"
+
+namespace perfetto {
+namespace proto_filter {
+namespace {
+
+const char kUsage[] =
+    R"(Usage: proto_filter [-s schema_in] [-i message in] [-o message out] [-f filter in] [-F filter out] [-T filter_oct_out] [-d --dedupe] [-I proto include path] [-r root message]
+
+-s --schema-in:      Path to the root .proto file. Required for most operations
+-I --proto_path:     Extra include directory for proto includes. If omitted assumed CWD.
+-r --root_message:   Fully qualified name for the root proto message (e.g. perfetto.protos.Trace)
+                     If omitted the first message defined in the schema will be used.
+-i --msg_in:         Path of a binary-encoded proto message which will be filtered.
+-o --msg_out:        Path of the binary-encoded filtered proto message written in output.
+-f --filter_in:      Path of a filter bytecode file previously generated by this tool.
+-F --filter_out:     Path of the filter bytecode file generated from the --schema-in definition.
+-T --filter_oct_out: Like --filter_out, but emits a octal-escaped C string suitable for .pbtx.
+-d --dedupe:         Minimize filter size by deduping leaf messages with same field ids.
+
+Example usage:
+
+# Convert a .proto schema file into a diff-friendly list of messages/fields>
+
+  proto_filter -r perfetto.protos.Trace -s protos/perfetto/trace/trace.proto
+
+# Generate the filter bytecode from a .proto schema
+
+  proto_filter -r perfetto.protos.Trace -s protos/perfetto/trace/trace.proto \
+               -F /tmp/bytecode [--dedupe]
+
+# List the used/filtered fields from a trace file
+
+  proto_filter -r perfetto.protos.Trace -s protos/perfetto/trace/trace.proto \
+               -i test/data/example_android_trace_30s.pb -f /tmp/bytecode
+
+# Filter a trace using a filter bytecode
+
+  proto_filter -i test/data/example_android_trace_30s.pb -f /tmp/bytecode \
+               -o /tmp/filtered_trace
+)";
+
+int Main(int argc, char** argv) {
+  static const option long_options[] = {
+      {"help", no_argument, nullptr, 'h'},
+      {"version", no_argument, nullptr, 'v'},
+      {"dedupe", no_argument, nullptr, 'd'},
+      {"proto_path", no_argument, nullptr, 'I'},
+      {"schema_in", no_argument, nullptr, 's'},
+      {"root_message", no_argument, nullptr, 'r'},
+      {"msg_in", no_argument, nullptr, 'i'},
+      {"msg_out", no_argument, nullptr, 'o'},
+      {"filter_in", no_argument, nullptr, 'f'},
+      {"filter_out", no_argument, nullptr, 'F'},
+      {"filter_oct_out", no_argument, nullptr, 'T'},
+      {nullptr, 0, nullptr, 0}};
+
+  std::string msg_in;
+  std::string msg_out;
+  std::string filter_in;
+  std::string schema_in;
+  std::string filter_out;
+  std::string filter_oct_out;
+  std::string proto_path;
+  std::string root_message_arg;
+  bool dedupe = false;
+
+  for (;;) {
+    int option =
+        getopt_long(argc, argv, "hvdI:s:r:i:o:f:F:T:", long_options, nullptr);
+
+    if (option == -1)
+      break;  // EOF.
+
+    if (option == 'v') {
+      printf("%s\n", base::GetVersionString());
+      exit(0);
+    }
+
+    if (option == 'd') {
+      dedupe = true;
+      continue;
+    }
+
+    if (option == 'I') {
+      proto_path = optarg;
+      continue;
+    }
+
+    if (option == 's') {
+      schema_in = optarg;
+      continue;
+    }
+
+    if (option == 'r') {
+      root_message_arg = optarg;
+      continue;
+    }
+
+    if (option == 'i') {
+      msg_in = optarg;
+      continue;
+    }
+
+    if (option == 'o') {
+      msg_out = optarg;
+      continue;
+    }
+
+    if (option == 'f') {
+      filter_in = optarg;
+      continue;
+    }
+
+    if (option == 'F') {
+      filter_out = optarg;
+      continue;
+    }
+
+    if (option == 'T') {
+      filter_oct_out = optarg;
+      continue;
+    }
+
+    if (option == 'h') {
+      fprintf(stdout, kUsage);
+      exit(0);
+    }
+
+    fprintf(stderr, kUsage);
+    exit(1);
+  }
+
+  if (msg_in.empty() && filter_in.empty() && schema_in.empty()) {
+    fprintf(stderr, kUsage);
+    return 1;
+  }
+
+  std::string msg_in_data;
+  if (!msg_in.empty()) {
+    PERFETTO_LOG("Loading proto-encoded message from %s", msg_in.c_str());
+    if (!base::ReadFile(msg_in, &msg_in_data)) {
+      PERFETTO_ELOG("Could not open message file %s", msg_in.c_str());
+      return 1;
+    }
+  }
+
+  protozero::FilterUtil filter;
+  if (!schema_in.empty()) {
+    PERFETTO_LOG("Loading proto schema from %s", schema_in.c_str());
+    if (!filter.LoadMessageDefinition(schema_in, root_message_arg,
+                                      proto_path)) {
+      PERFETTO_ELOG("Failed to parse proto schema from %s", schema_in.c_str());
+      return 1;
+    }
+    if (dedupe)
+      filter.Dedupe();
+  }
+
+  protozero::MessageFilter msg_filter;
+  std::string filter_data;
+  std::string filter_data_src;
+  if (!filter_in.empty()) {
+    PERFETTO_LOG("Loading filter bytecode from %s", filter_in.c_str());
+    if (!base::ReadFile(filter_in, &filter_data)) {
+      PERFETTO_ELOG("Could not open filter file %s", filter_in.c_str());
+      return 1;
+    }
+    filter_data_src = filter_in;
+  } else if (!schema_in.empty()) {
+    PERFETTO_LOG("Generating filter bytecode from %s", schema_in.c_str());
+    filter_data = filter.GenerateFilterBytecode();
+    filter_data_src = schema_in;
+  }
+
+  if (!filter_data.empty()) {
+    const uint8_t* data = reinterpret_cast<const uint8_t*>(filter_data.data());
+    if (!msg_filter.LoadFilterBytecode(data, filter_data.size())) {
+      PERFETTO_ELOG("Failed to parse filter bytecode from %s",
+                    filter_data_src.c_str());
+      return 1;
+    }
+  }
+
+  // Write the filter bytecode in output.
+  if (!filter_out.empty()) {
+    auto fd = base::OpenFile(filter_out, O_WRONLY | O_TRUNC | O_CREAT, 0644);
+    if (!fd) {
+      PERFETTO_ELOG("Could not open filter out path %s", filter_out.c_str());
+      return 1;
+    }
+    PERFETTO_LOG("Writing filter bytecode (%zu bytes) into %s",
+                 filter_data.size(), filter_out.c_str());
+    base::WriteAll(*fd, filter_data.data(), filter_data.size());
+  }
+
+  if (!filter_oct_out.empty()) {
+    auto fd =
+        base::OpenFile(filter_oct_out, O_WRONLY | O_TRUNC | O_CREAT, 0644);
+    if (!fd) {
+      PERFETTO_ELOG("Could not open filter out path %s",
+                    filter_oct_out.c_str());
+      return 1;
+    }
+    std::string oct_str;
+    oct_str.reserve(filter_data.size() * 4 + 64);
+    oct_str.append("trace_filter{\n  bytecode: \"");
+    for (char c : filter_data) {
+      uint8_t octect = static_cast<uint8_t>(c);
+      char buf[5]{'\\', '0', '0', '0', 0};
+      for (uint8_t i = 0; i < 3; ++i) {
+        buf[3 - i] = static_cast<char>('0' + static_cast<uint8_t>(octect) % 8);
+        octect /= 8;
+      }
+      oct_str.append(buf);
+    }
+    oct_str.append("\"\n}\n");
+    PERFETTO_LOG("Writing filter bytecode (%zu bytes) into %s", oct_str.size(),
+                 filter_oct_out.c_str());
+    base::WriteAll(*fd, oct_str.data(), oct_str.size());
+  }
+
+  // Apply the filter to the input message (if any).
+  std::vector<uint8_t> msg_filtered_data;
+  if (!msg_in.empty()) {
+    PERFETTO_LOG("Applying filter %s to proto message %s",
+                 filter_data_src.c_str(), msg_in.c_str());
+    msg_filter.enable_field_usage_tracking(true);
+    auto res = msg_filter.FilterMessage(msg_in_data.data(), msg_in_data.size());
+    if (res.error)
+      PERFETTO_FATAL("Filtering failed");
+    msg_filtered_data.insert(msg_filtered_data.end(), res.data.get(),
+                             res.data.get() + res.size);
+  }
+
+  // Write out the filtered message.
+  if (!msg_out.empty()) {
+    PERFETTO_LOG("Writing filtered proto bytes (%zu bytes) into %s",
+                 msg_filtered_data.size(), msg_out.c_str());
+    auto fd = base::OpenFile(msg_out, O_WRONLY | O_CREAT, 0644);
+    base::WriteAll(*fd, msg_filtered_data.data(), msg_filtered_data.size());
+  }
+
+  if (!msg_in.empty()) {
+    const auto& field_usage_map = msg_filter.field_usage();
+    for (const auto& it : field_usage_map) {
+      const std::string& field_path_varint = it.first;
+      int32_t num_occurrences = it.second;
+      std::string path_str = filter.LookupField(field_path_varint);
+      printf("%-100s %s %d\n", path_str.c_str(),
+             num_occurrences < 0 ? "DROP" : "PASS", std::abs(num_occurrences));
+    }
+  } else if (!schema_in.empty()) {
+    filter.PrintAsText();
+  }
+
+  if ((!filter_out.empty() || !filter_oct_out.empty()) && !dedupe) {
+    PERFETTO_ELOG(
+        "Warning: looks like you are generating a filter without --dedupe. For "
+        "production use cases, --dedupe can make the output bytecode "
+        "significantly smaller.");
+  }
+  return 0;
+}
+
+}  // namespace
+}  // namespace proto_filter
+}  // namespace perfetto
+
+int main(int argc, char** argv) {
+  return perfetto::proto_filter::Main(argc, argv);
+}
diff --git a/tools/record_android_trace b/tools/record_android_trace
index 40cae66..f4c2d4b 100755
--- a/tools/record_android_trace
+++ b/tools/record_android_trace
@@ -1,14 +1,29 @@
 #!/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.
 
 import atexit
 import argparse
 import datetime
+import hashlib
 import http.server
 import os
 import shutil
 import socketserver
 import subprocess
 import sys
+import tempfile
 import time
 import webbrowser
 
@@ -18,6 +33,26 @@
 # PATH. It's fine if it doesn't exist so this script can be copied elsewhere.
 HERMETIC_ADB_PATH = ROOT_DIR + '/buildtools/android_sdk/platform-tools/adb'
 
+# For downloading and sideloading tracebox on older devices, or when the
+# --sideload argument is passed.
+TRACEBOX_BASE_URL = 'https://storage.googleapis.com/perfetto/'
+TRACEBOX_SHA1S = {
+    'android-arm': '6e9dfee326468fc6858c6d95e00be110efd187a3',  # v15.0.248
+    'android-arm64': 'ca2d4a02511f73dac32a2ae49964f3e5cd59d252',  # v15.0.248
+    'android-x86': '3fdbc9246412e460d0a373140c114cff858b9b7c',  # v15.0.248
+    'android-x64': 'be85f6f4a2d014d425246b85941c137c28d158cc',  # v15.0.248
+}
+
+# Translates the Android ro.product.cpu.abi into the GN's target_cpu.
+ABI_TO_ARCH = {
+    'armeabi-v7a': 'arm',
+    'arm64-v8a': 'arm64',
+    'x86': 'x86',
+    'x86_64': 'x64',
+}
+
+MAX_ADB_FAILURES = 15  # 2 seconds between retries, 30 seconds total.
+
 devnull = open(os.devnull, 'rb')
 adb_path = None
 procs = []
@@ -54,7 +89,7 @@
   default_out_dir = os.path.expanduser(default_out_dir_str)
 
   examples = '\n'.join([
-      ANSI.BOLD + 'Examples' + ANSI.END, '  -t 10s -b 32mb sched gfx wm',
+      ANSI.BOLD + 'Examples' + ANSI.END, '  -t 10s -b 32mb sched gfx wm -a*',
       '  -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit',
       '  -c /path/to/full-textual-trace.config', '',
       ANSI.BOLD + 'Long traces' + ANSI.END,
@@ -71,6 +106,16 @@
   help = 'Don\'t open in the browser'
   parser.add_argument('-n', '--no-open', action='store_true', help=help)
 
+  help = 'Force the use of the sideloaded binaries rather than system daemons'
+  parser.add_argument('--sideload', action='store_true', help=help)
+
+  help = ('Sideload the given binary rather than downloading it. ' +
+          'Implies --sideload')
+  parser.add_argument('--sideload-path', default=None, help=help)
+
+  help = 'Don\'t run `adb root` run as user (only when sideloading)'
+  parser.add_argument('-u', '--user', action='store_true', help=help)
+
   grp = parser.add_argument_group(
       'Short options: (only when not using -c/--config)')
 
@@ -80,6 +125,16 @@
   help = 'Ring buffer size N[mb,gb] (default: 32mb)'
   grp.add_argument('-b', '--buffer', default='32mb', help=help)
 
+  help = ('Android (atrace) app names. Can be specified multiple times.\n-a*' +
+          'for all apps (without space between a and * or bash will expand it)')
+  grp.add_argument(
+      '-a',
+      '--app',
+      metavar='com.myapp',
+      action='append',
+      default=[],
+      help=help)
+
   help = 'sched, gfx, am, wm (see --list)'
   grp.add_argument('events', metavar='Atrace events', nargs='*', help=help)
 
@@ -98,10 +153,7 @@
   help = 'Can be generated with https://ui.perfetto.dev/#!/record'
   grp.add_argument('-c', '--config', default=None, help=help)
   args = parser.parse_args()
-
-  tstamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')
-  fname = '%s.pftrace' % tstamp
-  device_file = '/data/misc/perfetto-traces/' + fname
+  args.sideload = args.sideload or args.sideload_path is not None
 
   find_adb()
 
@@ -128,11 +180,52 @@
          'Did you mean to pass -c / --config ?'), ANSI.RED)
     sys.exit(1)
 
-  cmd = ['perfetto', '--background', '--txt', '-o', device_file]
+  perfetto_cmd = 'perfetto'
+  device_dir = '/data/misc/perfetto-traces/'
+
+  # Check the version of android. If too old (< Q) sideload tracebox. Also use
+  # use /data/local/tmp as /data/misc/perfetto-traces was introduced only later.
+  probe_cmd = 'getprop ro.build.version.sdk; getprop ro.product.cpu.abi; whoami'
+  probe = adb('shell', probe_cmd, stdout=subprocess.PIPE)
+  lines = probe.communicate()[0].decode().strip().split('\n')
+  lines = [x.strip() for x in lines]  # To strip \r(s) on Windows.
+  if probe.returncode != 0:
+    prt('ADB connection failed', ANSI.RED)
+    sys.exit(1)
+  api_level = int(lines[0])
+  abi = lines[1]
+  arch = ABI_TO_ARCH.get(abi)
+  if arch is None:
+    prt('Unsupported ABI: ' + abi)
+    sys.exit(1)
+  shell_user = lines[2]
+  if api_level < 29 or args.sideload:  # 29: Android Q.
+    tracebox_bin = args.sideload_path or download_tracebox_if_needed(arch)
+    perfetto_cmd = '/data/local/tmp/tracebox'
+    exit_code = adb('push', '--sync', tracebox_bin, perfetto_cmd).wait()
+    exit_code |= adb('shell', 'chmod 755 ' + perfetto_cmd).wait()
+    if exit_code != 0:
+      prt('ADB push failed', ANSI.RED)
+      sys.exit(1)
+    device_dir = '/data/local/tmp/'
+    if shell_user != 'root' and not args.user:
+      # Run as root if possible as that will give access to more tracing
+      # capabilities. Non-root still works, but some ftrace events might not be
+      # available.
+      adb('root').wait()
+
+  tstamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')
+  fname = '%s-%s.pftrace' % (tstamp, os.urandom(3).hex())
+  device_file = device_dir + fname
+
+  cmd = [perfetto_cmd, '--background', '--txt', '-o', device_file]
   if args.config is not None:
     cmd += ['-c', '-']
   else:
-    cmd += ['-t', args.time, '-b', args.buffer] + args.events
+    cmd += ['-t', args.time, '-b', args.buffer]
+    for app in args.app:
+      cmd += ['--app', '\'' + app + '\'']
+    cmd += args.events
 
   # Perfetto will error out with a proper message if both a config file and
   # short options are specified. No need to replicate that logic.
@@ -165,17 +258,34 @@
                '1')
 
   ctrl_c_count = 0
+  adb_failure_count = 0
   while ctrl_c_count < 2:
     try:
-      poll = adb('shell', 'test -d /proc/' + bg_pid)
-      if poll.wait() != 0:
-        break
-      time.sleep(0.5)
+      poll = adb('shell', 'test -d /proc/%s || exit 42' % bg_pid)
+      poll_res = poll.wait()
+      if poll_res == 42:
+        break  # Process terminated
+      if poll_res == 0:
+        # The 'perfetto' cmdline client is still running. If previously we had
+        # an ADB error, tell the user now it's all right again.
+        if adb_failure_count > 0:
+          adb_failure_count = 0
+          prt('ADB connection re-established, the trace is still ongoing',
+              ANSI.BLUE)
+        time.sleep(0.5)
+        continue
+      # Some ADB error happened. This can happen when tracing soon after boot,
+      # before logging in, when adb gets restarted.
+      adb_failure_count += 1
+      if adb_failure_count >= MAX_ADB_FAILURES:
+        prt('Too many unrecoverable ADB failures, bailing out', ANSI.RED)
+        sys.exit(1)
+      time.sleep(2)
     except KeyboardInterrupt:
       sig = 'TERM' if ctrl_c_count == 0 else 'KILL'
       ctrl_c_count += 1
       prt('Stopping the trace (SIG%s)' % sig, ANSI.BLACK + ANSI.BG_YELLOW)
-      res = adb('shell', 'kill -%s %s' % (sig, bg_pid)).wait()
+      adb('shell', 'kill -%s %s' % (sig, bg_pid)).wait()
 
   logcat.kill()
   logcat.wait()
@@ -246,5 +356,32 @@
     p.kill()
 
 
+def check_hash(file_name, sha_value):
+  with open(file_name, 'rb') as fd:
+    file_hash = hashlib.sha1(fd.read()).hexdigest()
+    return file_hash == sha_value
+
+
+def download_tracebox_if_needed(arch):
+  sha_value = TRACEBOX_SHA1S.get('android-' + arch)
+  if sha_value is None:
+    prt('Unsupported architecture ' + arch)
+    sys.exit(1)
+  file_name = 'tracebox-android-' + arch + '-' + sha_value
+  local_file = os.path.join(tempfile.gettempdir(), file_name)
+  if os.path.exists(local_file):
+    if not check_hash(local_file, sha_value):
+      os.remove(local_file)
+    else:
+      return local_file
+  url = TRACEBOX_BASE_URL + file_name
+  prt('Downloading %s' % url)
+  subprocess.check_call(['curl', '-L', '-#', '-o', local_file, url])
+  if not check_hash(local_file, sha_value):
+    os.remove(local_file)
+    raise ValueError('Invalid SHA1 for %s' % local_file)
+  return local_file
+
+
 if __name__ == '__main__':
   sys.exit(main())
diff --git a/tools/run_android_test b/tools/run_android_test
index 64cc560..5f38951 100755
--- a/tools/run_android_test
+++ b/tools/run_android_test
@@ -143,8 +143,10 @@
   AdbCall('root')
   AdbCall('wait-for-device')
 
-  target_dir = '/data/local/tmp/' + args.test_name
-  AdbCall('shell', 'rm -rf "%s"; mkdir -p "%s"' % (2 * (target_dir,)))
+  target_dir = '/data/local/tmp/perfetto_tests'
+  if not args.no_cleanup:
+    AdbCall('shell', 'rm -rf "%s"' % target_dir)
+  AdbCall('shell', 'mkdir -p "%s"' % target_dir)
   # Some tests require the trace directory to exist, while true for android
   # devices in general some emulators might not have it set up. So we check to
   # see if it exists, and if not create it.
diff --git a/tools/run_buildtools_binary.py b/tools/run_buildtools_binary.py
new file mode 100644
index 0000000..6fcfe1a
--- /dev/null
+++ b/tools/run_buildtools_binary.py
@@ -0,0 +1,59 @@
+#!/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.
+""" A wrapper to run gn, ninja and other buildtools/ for all platforms. """
+
+from __future__ import print_function
+
+import os
+import subprocess
+import sys
+
+from platform import system, machine
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+def run_buildtools_binary(args):
+  if len(args) < 1:
+    print('Usage %s command [args]\n' % sys.argv[0])
+    return 1
+
+  sys_name = system().lower()
+  os_dir = None
+  ext = ''
+  if sys_name == 'windows':
+    os_dir = 'win'
+    ext = '.exe'
+  elif sys_name == 'darwin':
+    os_dir = 'mac'
+  elif sys_name == 'linux':
+    os_dir = 'linux64'
+  else:
+    print('OS not supported: %s\n' % sys_name)
+    return 1
+
+  cmd = args[0]
+  args = args[1:]
+  exe_path = os.path.join(ROOT_DIR, 'buildtools', os_dir, cmd) + ext
+  if sys_name == 'windows':
+    # execl() behaves oddly on Windows: the spawned process doesn't seem to
+    # receive CTRL+C. Use subprocess instead.
+    return subprocess.call([exe_path] + args)
+  else:
+    os.execl(exe_path, os.path.basename(exe_path), *args)
+
+
+if __name__ == '__main__':
+  sys.exit(run_buildtools_binary(sys.argv[1:]))
diff --git a/tools/test_data.txt b/tools/test_data.txt
index abcb65d..35a5977 100644
--- a/tools/test_data.txt
+++ b/tools/test_data.txt
@@ -1,5 +1,12 @@
 # List of test deps that should be pushed on the device. Paths are relative
 # to the root.
-src/traced/probes/ftrace/test/data/
-src/traced/probes/filesystem/testdata/
+# The trailing /. at the end of a directory is to avoid creating a nesting
+# directory when pushing a 2nd-time. adb push has a slightly different behavior
+# than `cp` on directoriesm, trailing slash is not enough.
+src/traced/probes/filesystem/testdata/.
+src/traced/probes/ftrace/test/data/.
+test/data/android_log_ring_buffer_mode.pb
+test/data/example_android_trace_30s.pb
+test/data/full_trace_filter.bytecode
 test/data/kallsyms.txt
+test/data/trace_with_uuid.pftrace
diff --git a/tools/trace_processor b/tools/trace_processor
index 5b84f0c..22aaab6 100755
--- a/tools/trace_processor
+++ b/tools/trace_processor
@@ -21,7 +21,7 @@
 # cat ./trace_processor | python -
 
 BASH_FALLBACK = """ "
-exec python - "$@" <<'#'EOF
+exec python3 - "$@" <<'#'EOF
 #"""
 
 import hashlib
@@ -32,8 +32,8 @@
 import platform
 
 TRACE_PROCESSOR_SHELL_SHAS = {
-    'linux': 'cb07e9dddf2a6a67bcc8062d1a07209411baaf7e',
-    'mac': '6bffa69b46d3da52ca9257fefcb95d3114544d82',
+    'linux': 'a3ce2cbf4cbe4f86cc10b02957db727cecfafae8',
+    'mac': 'c39a5be9a3831911ef2e50d66d11c12d877688f3',
 }
 TRACE_PROCESSOR_SHELL_PATH = tempfile.gettempdir()
 TRACE_PROCESSOR_SHELL_BASE_URL = ('https://storage.googleapis.com/perfetto/')
diff --git a/tools/trace_to_text/BUILD.gn b/tools/trace_to_text/BUILD.gn
index c1674d8..1abb0a8 100644
--- a/tools/trace_to_text/BUILD.gn
+++ b/tools/trace_to_text/BUILD.gn
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 import("../../gn/perfetto.gni")
+import("../../gn/perfetto_cc_proto_descriptor.gni")
 import("../../gn/perfetto_host_executable.gni")
 import("../../gn/wasm.gni")
 
@@ -71,6 +72,7 @@
     "../../protos/third_party/pprof:zero",
     "../../src/profiling/symbolizer",
     "../../src/profiling/symbolizer:symbolize_database",
+    "../../src/trace_processor/containers:containers",
   ]
   sources = [ "pprof_builder.cc" ]
 }
@@ -78,7 +80,7 @@
 # Exposed in bazel builds.
 static_library("libpprofbuilder") {
   complete_static_lib = true
-  deps = [ ":pprofbuilder" ]
+  public_deps = [ ":pprofbuilder" ]
 }
 
 # The core source files that are used both by the "full" version (the host
@@ -130,10 +132,12 @@
   testonly = true
   deps = [
     ":common",
+    ":gen_cc_trace_descriptor",
     ":utils",
     "../../gn:default_deps",
     "../../gn:protobuf_full",
     "../../protos/perfetto/trace:zero",
+    "../../src/protozero:proto_ring_buffer",
   ]
   if (enable_perfetto_zlib) {
     deps += [ "../../gn:zlib" ]
@@ -154,3 +158,8 @@
     ]
   }
 }
+
+perfetto_cc_proto_descriptor("gen_cc_trace_descriptor") {
+  descriptor_name = "trace.descriptor"
+  descriptor_target = "../../protos/perfetto/trace:descriptor"
+}
diff --git a/tools/trace_to_text/main.cc b/tools/trace_to_text/main.cc
index 8384b7a..e4a9f1d 100644
--- a/tools/trace_to_text/main.cc
+++ b/tools/trace_to_text/main.cc
@@ -32,7 +32,6 @@
 #include "tools/trace_to_text/trace_to_systrace.h"
 #include "tools/trace_to_text/trace_to_text.h"
 
-
 #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
 #include <unistd.h>
 #endif
@@ -43,17 +42,19 @@
 
 int Usage(const char* argv0) {
   fprintf(stderr,
-          "Usage: %s systrace|json|ctrace|text|profile [--pid PID] "
-          "[--timestamps TIMESTAMP1,TIMESTAMP2,...] "
-          "[--truncate start|end] "
-          "[--full-sort] "
-          "[trace.pb] "
-          "[trace.txt]\n"
-          "\nProfile mode only:\n"
-          "\t--perf generate a perf profile\n"
-          "\t--timestamps TIMESTAMP1,TIMESTAMP2,... generate profiles "
-          "only for these timestamps\n"
-          "\t--pid PID generate profiles only for this process id\n",
+          "Usage: %s MODE [OPTIONS] [input file] [output file]\n"
+          "modes:\n"
+          "  systrace|json|ctrace|text|profile|hprof|symbolize|deobfuscate\n"
+          "options:\n"
+          "  [--truncate start|end]\n"
+          "  [--full-sort]\n"
+          "\"profile\" mode options:\n"
+          "  [--perf] generate a perf profile instead of a heap profile\n"
+          "  [--no-annotations] do not suffix frame names with derived "
+          "annotations\n"
+          "  [--timestamps TIMESTAMP1,TIMESTAMP2,...] generate profiles "
+          "only for these *specific* timestamps\n"
+          "  [--pid PID] generate profiles only for this process id\n",
           argv0);
   return 1;
 }
@@ -75,6 +76,7 @@
   std::vector<uint64_t> timestamps;
   bool full_sort = false;
   bool perf_profile = false;
+  bool profile_no_annotations = false;
   for (int i = 1; i < argc; i++) {
     if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) {
       printf("%s\n", base::GetVersionString());
@@ -103,6 +105,8 @@
       }
     } else if (strcmp(argv[i], "--perf") == 0) {
       perf_profile = true;
+    } else if (strcmp(argv[i], "--no-annotations") == 0) {
+      profile_no_annotations = true;
     } else if (strcmp(argv[i], "--full-sort") == 0) {
       full_sort = true;
     } else {
@@ -187,10 +191,11 @@
     return TraceToText(input_stream, output_stream);
 
   if (format == "profile") {
-    return perf_profile ? TraceToPerfProfile(input_stream, output_stream, pid,
-                                             timestamps)
-                        : TraceToHeapProfile(input_stream, output_stream, pid,
-                                             timestamps);
+    return perf_profile
+               ? TraceToPerfProfile(input_stream, output_stream, pid,
+                                    timestamps, !profile_no_annotations)
+               : TraceToHeapProfile(input_stream, output_stream, pid,
+                                    timestamps, !profile_no_annotations);
   }
 
   if (format == "hprof")
diff --git a/tools/trace_to_text/pprof_builder.cc b/tools/trace_to_text/pprof_builder.cc
index fdb8717..588c438 100644
--- a/tools/trace_to_text/pprof_builder.cc
+++ b/tools/trace_to_text/pprof_builder.cc
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-#include "perfetto/profiling/pprof_builder.h"
-
 #include "perfetto/base/build_config.h"
 
+#include "perfetto/profiling/pprof_builder.h"
+
 #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
 #include <cxxabi.h>
 #endif
@@ -27,29 +27,120 @@
 #include <algorithm>
 #include <map>
 #include <set>
+#include <unordered_map>
 #include <utility>
 #include <vector>
 
 #include "tools/trace_to_text/utils.h"
 
 #include "perfetto/base/logging.h"
-#include "perfetto/base/time.h"
+#include "perfetto/ext/base/hash.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/protozero/packed_repeated_fields.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
 #include "perfetto/trace_processor/trace_processor.h"
-
 #include "src/profiling/symbolizer/symbolize_database.h"
 #include "src/profiling/symbolizer/symbolizer.h"
+#include "src/trace_processor/containers/string_pool.h"
 
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 #include "protos/third_party/pprof/profile.pbzero.h"
 
+// Quick hint on navigating the file:
+// Conversions for both perf and heap profiles start with |TraceToPprof|.
+// Non-shared logic is in the |heap_profile| and |perf_profile| namespaces.
+//
+// To build one or more profiles, first the callstack information is queried
+// from the SQL tables, and converted into an in-memory representation by
+// |PreprocessLocations|. Then an instance of |GProfileBuilder| is used to
+// accumulate samples for that profile, and emit all additional information as a
+// serialized proto. Only the entities referenced by that particular
+// |GProfileBuilder| instance are emitted.
+//
+// See protos/third_party/pprof/profile.proto for the meaning of terms like
+// function/location/line.
+
+namespace {
+using StringId = ::perfetto::trace_processor::StringPool::Id;
+
+// In-memory representation of a Profile.Function.
+struct Function {
+  StringId name_id = StringId::Null();
+  StringId system_name_id = StringId::Null();
+  StringId filename_id = StringId::Null();
+
+  Function(StringId n, StringId s, StringId f)
+      : name_id(n), system_name_id(s), filename_id(f) {}
+
+  bool operator==(const Function& other) const {
+    return std::tie(name_id, system_name_id, filename_id) ==
+           std::tie(other.name_id, other.system_name_id, other.filename_id);
+  }
+};
+
+// In-memory representation of a Profile.Line.
+struct Line {
+  int64_t function_id = 0;  // LocationTracker's interned Function id
+  int64_t line_no = 0;
+
+  Line(int64_t func, int64_t line) : function_id(func), line_no(line) {}
+
+  bool operator==(const Line& other) const {
+    return function_id == other.function_id && line_no == other.line_no;
+  }
+};
+
+// In-memory representation of a Profile.Location.
+struct Location {
+  int64_t mapping_id = 0;  // sqlite row id
+  // Common case: location references a single function.
+  int64_t single_function_id = 0;  // interned Function id
+  // Alternatively: multiple inlined functions, recovered via offline
+  // symbolisation. Leaf-first ordering.
+  std::vector<Line> inlined_functions;
+
+  Location(int64_t map, int64_t func, std::vector<Line> inlines)
+      : mapping_id(map),
+        single_function_id(func),
+        inlined_functions(std::move(inlines)) {}
+
+  bool operator==(const Location& other) const {
+    return std::tie(mapping_id, single_function_id, inlined_functions) ==
+           std::tie(other.mapping_id, other.single_function_id,
+                    other.inlined_functions);
+  }
+};
+}  // namespace
+
+template <>
+struct std::hash<Function> {
+  size_t operator()(const Function& loc) const {
+    perfetto::base::Hash hasher;
+    hasher.Update(loc.name_id.raw_id());
+    hasher.Update(loc.system_name_id.raw_id());
+    hasher.Update(loc.filename_id.raw_id());
+    return static_cast<size_t>(hasher.digest());
+  }
+};
+
+template <>
+struct std::hash<Location> {
+  size_t operator()(const Location& loc) const {
+    perfetto::base::Hash hasher;
+    hasher.Update(loc.mapping_id);
+    hasher.Update(loc.single_function_id);
+    for (auto line : loc.inlined_functions) {
+      hasher.Update(line.function_id);
+      hasher.Update(line.line_no);
+    }
+    return static_cast<size_t>(hasher.digest());
+  }
+};
+
 namespace perfetto {
 namespace trace_to_text {
-
 namespace {
 
 using ::perfetto::trace_processor::Iterator;
@@ -83,84 +174,6 @@
   return ret;
 }
 
-// Return map from callsite_id to list of frame_ids that make up the callstack.
-std::vector<std::vector<int64_t>> GetCallsiteToFrames(
-    trace_processor::TraceProcessor* tp) {
-  Iterator count_it =
-      tp->ExecuteQuery("select count(*) from stack_profile_callsite;");
-  if (!count_it.Next()) {
-    PERFETTO_DFATAL_OR_ELOG("Failed to get number of callsites: %s",
-                            count_it.Status().message().c_str());
-    return {};
-  }
-  int64_t count = count_it.Get(0).AsLong();
-
-  Iterator it = tp->ExecuteQuery(
-      "select id, parent_id, frame_id from stack_profile_callsite order by "
-      "depth;");
-  std::vector<std::vector<int64_t>> result(static_cast<size_t>(count));
-  while (it.Next()) {
-    int64_t id = it.Get(0).AsLong();
-    int64_t frame_id = it.Get(2).AsLong();
-    std::vector<int64_t>& path = result[static_cast<size_t>(id)];
-    path.push_back(frame_id);
-
-    auto parent_id_value = it.Get(1);
-    if (!parent_id_value.is_null()) {
-      const std::vector<int64_t>& parent_path =
-          result[static_cast<size_t>(parent_id_value.AsLong())];
-      path.insert(path.end(), parent_path.begin(), parent_path.end());
-    }
-  }
-
-  if (!it.Status().ok()) {
-    PERFETTO_DFATAL_OR_ELOG("Invalid iterator: %s",
-                            it.Status().message().c_str());
-    return {};
-  }
-  return result;
-}
-
-base::Optional<int64_t> GetMaxSymbolId(trace_processor::TraceProcessor* tp) {
-  auto max_symbol_id_it =
-      tp->ExecuteQuery("select max(id) from stack_profile_symbol");
-  if (!max_symbol_id_it.Next()) {
-    PERFETTO_DFATAL_OR_ELOG("Failed to get max symbol set id: %s",
-                            max_symbol_id_it.Status().message().c_str());
-    return base::nullopt;
-  }
-  auto value = max_symbol_id_it.Get(0);
-  if (value.is_null())
-    return base::nullopt;
-  return base::make_optional(value.AsLong());
-}
-
-struct Line {
-  int64_t symbol_id;
-  uint32_t line_number;
-};
-
-std::map<int64_t, std::vector<Line>> GetSymbolSetIdToLines(
-    trace_processor::TraceProcessor* tp) {
-  std::map<int64_t, std::vector<Line>> result;
-  Iterator it = tp->ExecuteQuery(
-      "SELECT symbol_set_id, id, line_number FROM stack_profile_symbol;");
-  while (it.Next()) {
-    int64_t symbol_set_id = it.Get(0).AsLong();
-    int64_t id = it.Get(1).AsLong();
-    int64_t line_number = it.Get(2).AsLong();
-    result[symbol_set_id].emplace_back(
-        Line{id, static_cast<uint32_t>(line_number)});
-  }
-
-  if (!it.Status().ok()) {
-    PERFETTO_DFATAL_OR_ELOG("Invalid iterator: %s",
-                            it.Status().message().c_str());
-    return {};
-  }
-  return result;
-}
-
 base::Optional<int64_t> GetStatsEntry(
     trace_processor::TraceProcessor* tp,
     const std::string& name,
@@ -182,73 +195,373 @@
   return base::make_optional(it.Get(0).AsLong());
 }
 
-// Helper for constructing |perftools.profiles.Profile| protos.
+// Interns Locations, Lines, and Functions. Interning is done by the entity's
+// contents, and has no relation to the row ids in the SQL tables.
+// Contains all data for the trace, so can be reused when emitting multiple
+// profiles.
+//
+// TODO(rsavitski): consider moving mappings into here as well. For now, they're
+// still emitted in a single scan during profile building. Mappings should be
+// unique-enough already in the SQL tables, with only incremental state clearing
+// duplicating entries.
+class LocationTracker {
+ public:
+  int64_t InternLocation(Location loc) {
+    auto it = locations_.find(loc);
+    if (it == locations_.end()) {
+      bool inserted = false;
+      std::tie(it, inserted) = locations_.emplace(
+          std::move(loc), static_cast<int64_t>(locations_.size()));
+      PERFETTO_DCHECK(inserted);
+    }
+    return it->second;
+  }
+
+  int64_t InternFunction(Function func) {
+    auto it = functions_.find(func);
+    if (it == functions_.end()) {
+      bool inserted = false;
+      std::tie(it, inserted) =
+          functions_.emplace(func, static_cast<int64_t>(functions_.size()));
+      PERFETTO_DCHECK(inserted);
+    }
+    return it->second;
+  }
+
+  bool IsCallsiteProcessed(int64_t callstack_id) const {
+    return callsite_to_locations_.find(callstack_id) !=
+           callsite_to_locations_.end();
+  }
+
+  void MaybeSetCallsiteLocations(int64_t callstack_id,
+                                 const std::vector<int64_t>& locs) {
+    // nop if already set
+    callsite_to_locations_.emplace(callstack_id, locs);
+  }
+
+  const std::vector<int64_t>& LocationsForCallstack(
+      int64_t callstack_id) const {
+    auto it = callsite_to_locations_.find(callstack_id);
+    PERFETTO_CHECK(callstack_id >= 0 && it != callsite_to_locations_.end());
+    return it->second;
+  }
+
+  const std::unordered_map<Location, int64_t>& AllLocations() const {
+    return locations_;
+  }
+  const std::unordered_map<Function, int64_t>& AllFunctions() const {
+    return functions_;
+  }
+
+ private:
+  // Root-first location ids for a given callsite id.
+  std::unordered_map<int64_t, std::vector<int64_t>> callsite_to_locations_;
+  std::unordered_map<Location, int64_t> locations_;
+  std::unordered_map<Function, int64_t> functions_;
+};
+
+struct PreprocessedInline {
+  StringId system_name_id = StringId::Null();
+  StringId filename_id = StringId::Null();
+  int64_t line_no = 0;
+
+  PreprocessedInline(StringId s, StringId f, int64_t line)
+      : system_name_id(s), filename_id(f), line_no(line) {}
+};
+
+std::unordered_map<int64_t, std::vector<PreprocessedInline>>
+PreprocessInliningInfo(trace_processor::TraceProcessor* tp,
+                       trace_processor::StringPool* interner) {
+  std::unordered_map<int64_t, std::vector<PreprocessedInline>> inlines;
+
+  // Most-inlined function (leaf) has the lowest id within a symbol set. Query
+  // such that the per-set line vectors are built up leaf-first.
+  Iterator it = tp->ExecuteQuery(
+      "select symbol_set_id, name, source_file, line_number from "
+      "stack_profile_symbol order by symbol_set_id asc, id asc;");
+  while (it.Next()) {
+    int64_t symbol_set_id = it.Get(0).AsLong();
+    auto func_sysname = it.Get(1).is_null() ? "" : it.Get(1).AsString();
+    auto filename = it.Get(2).is_null() ? "" : it.Get(2).AsString();
+    int64_t line_no = it.Get(3).AsLong();
+
+    inlines[symbol_set_id].emplace_back(interner->InternString(func_sysname),
+                                        interner->InternString(filename),
+                                        line_no);
+  }
+
+  if (!it.Status().ok()) {
+    PERFETTO_DFATAL_OR_ELOG("Invalid iterator: %s",
+                            it.Status().message().c_str());
+    return {};
+  }
+  return inlines;
+}
+
+// Extracts and interns the unique frames and locations (as defined by the proto
+// format) from the callstack SQL tables.
+//
+// Approach:
+//   * for each callstack (callsite ids of the leaves):
+//     * use experimental_annotated_callstack to build the full list of
+//       constituent frames
+//     * for each frame (root to leaf):
+//         * intern the location and function(s)
+//         * remember the mapping from callsite_id to the callstack so far (from
+//            the root and including the frame being considered)
+//
+// Optionally mixes in the annotations as a frame name suffix (since there's no
+// good way to attach extra info to locations in the proto format). This relies
+// on the annotations (produced by experimental_annotated_callstack) to be
+// stable for a given callsite (equivalently: dependent only on their parents).
+LocationTracker PreprocessLocations(trace_processor::TraceProcessor* tp,
+                                    trace_processor::StringPool* interner,
+                                    bool annotate_frames) {
+  LocationTracker tracker;
+
+  // Keyed by symbol_set_id, discarded once this function converts the inlines
+  // into Line and Function entries.
+  std::unordered_map<int64_t, std::vector<PreprocessedInline>> inlining_info =
+      PreprocessInliningInfo(tp, interner);
+
+  // Higher callsite ids most likely correspond to the deepest stacks, so we'll
+  // fill more of the overall callsite->location map by visiting the callsited
+  // in decreasing id order. Since processing a callstack also fills in the data
+  // for all parent callsites.
+  Iterator cid_it = tp->ExecuteQuery(
+      "select id from stack_profile_callsite order by id desc;");
+  while (cid_it.Next()) {
+    int64_t query_cid = cid_it.Get(0).AsLong();
+
+    // If the leaf has been processed, the rest of the stack is already known.
+    if (tracker.IsCallsiteProcessed(query_cid))
+      continue;
+
+    std::string annotated_query =
+        "select sp.id, sp.annotation, spf.mapping, "
+        "ifnull(spf.deobfuscated_name, spf.name), spf.symbol_set_id from "
+        "experimental_annotated_callstack(" +
+        std::to_string(query_cid) +
+        ") sp join stack_profile_frame spf on (sp.frame_id == spf.id) "
+        "order by depth asc";
+    Iterator c_it = tp->ExecuteQuery(annotated_query);
+
+    std::vector<int64_t> callstack_loc_ids;
+    while (c_it.Next()) {
+      int64_t cid = c_it.Get(0).AsLong();
+      int64_t mapping_id = c_it.Get(2).AsLong();
+      auto annotation = c_it.Get(1).is_null() ? "" : c_it.Get(1).AsString();
+      auto func_sysname = c_it.Get(3).is_null() ? "" : c_it.Get(3).AsString();
+      base::Optional<int64_t> symbol_set_id =
+          c_it.Get(4).is_null() ? base::nullopt
+                                : base::make_optional(c_it.Get(4).AsLong());
+
+      Location loc(mapping_id, /*single_function_id=*/-1, {});
+
+      auto intern_function = [interner, &tracker, annotate_frames](
+                                 StringId func_sysname_id, StringId filename_id,
+                                 const std::string& anno) {
+        std::string func_name = interner->Get(func_sysname_id).ToStdString();
+        MaybeDemangle(&func_name);
+        if (annotate_frames && !anno.empty() && !func_name.empty())
+          func_name = func_name + " [" + anno + "]";
+        StringId func_name_id =
+            interner->InternString(base::StringView(func_name));
+        Function func(func_name_id, func_sysname_id, filename_id);
+        return tracker.InternFunction(func);
+      };
+
+      // Inlining information available
+      if (symbol_set_id.has_value()) {
+        auto it = inlining_info.find(*symbol_set_id);
+        if (it == inlining_info.end()) {
+          PERFETTO_DFATAL_OR_ELOG(
+              "Failed to find stack_profile_symbol entry for symbol_set_id "
+              "%" PRIi64 "",
+              *symbol_set_id);
+          return {};
+        }
+
+        // N inlined functions
+        for (const auto& line : it->second) {
+          int64_t func_id = intern_function(line.system_name_id,
+                                            line.filename_id, annotation);
+
+          loc.inlined_functions.emplace_back(func_id, line.line_no);
+        }
+      } else {
+        // Otherwise - single function
+        int64_t func_id =
+            intern_function(interner->InternString(func_sysname),
+                            /*filename_id=*/StringId::Null(), annotation);
+        loc.single_function_id = func_id;
+      }
+
+      int64_t loc_id = tracker.InternLocation(std::move(loc));
+
+      // Update the tracker with the locations so far (for example, at depth 2,
+      // we'll have 3 root-most locations in |callstack_loc_ids|).
+      callstack_loc_ids.push_back(loc_id);
+      tracker.MaybeSetCallsiteLocations(cid, callstack_loc_ids);
+    }
+
+    if (!c_it.Status().ok()) {
+      PERFETTO_DFATAL_OR_ELOG("Invalid iterator: %s",
+                              c_it.Status().message().c_str());
+      return {};
+    }
+  }
+
+  if (!cid_it.Status().ok()) {
+    PERFETTO_DFATAL_OR_ELOG("Invalid iterator: %s",
+                            cid_it.Status().message().c_str());
+    return {};
+  }
+
+  return tracker;
+}
+
+// Builds the |perftools.profiles.Profile| proto.
 class GProfileBuilder {
  public:
-  GProfileBuilder(
-      const std::vector<std::vector<int64_t>>& callsite_to_frames,
-      const std::map<int64_t, std::vector<Line>>& symbol_set_id_to_lines,
-      int64_t max_symbol_id)
-      : callsite_to_frames_(callsite_to_frames),
-        symbol_set_id_to_lines_(symbol_set_id_to_lines),
-        max_symbol_id_(max_symbol_id) {
-    // The pprof format expects the first entry in the string table to be the
+  GProfileBuilder(const LocationTracker& locations,
+                  trace_processor::StringPool* interner)
+      : locations_(locations), interner_(interner) {
+    // The pprof format requires the first entry in the string table to be the
     // empty string.
-    int64_t empty_id = Intern("");
+    int64_t empty_id = ToStringTableId(StringId::Null());
     PERFETTO_CHECK(empty_id == 0);
   }
 
   void WriteSampleTypes(
       const std::vector<std::pair<std::string, std::string>>& sample_types) {
-    // The interner might eagerly append to the profile proto, prevent it from
-    // breaking up other messages by making a separate pass.
-    for (const auto& st : sample_types) {
-      Intern(st.first);
-      Intern(st.second);
-    }
     for (const auto& st : sample_types) {
       auto* sample_type = result_->add_sample_type();
-      sample_type->set_type(Intern(st.first));
-      sample_type->set_unit(Intern(st.second));
+      sample_type->set_type(
+          ToStringTableId(interner_->InternString(base::StringView(st.first))));
+      sample_type->set_unit(ToStringTableId(
+          interner_->InternString(base::StringView(st.second))));
     }
   }
 
   bool AddSample(const protozero::PackedVarInt& values, int64_t callstack_id) {
-    const auto& frames = FramesForCallstack(callstack_id);
-    if (frames.empty()) {
+    const auto& location_ids = locations_.LocationsForCallstack(callstack_id);
+    if (location_ids.empty()) {
       PERFETTO_DFATAL_OR_ELOG(
           "Failed to find frames for callstack id %" PRIi64 "", callstack_id);
       return false;
     }
-    protozero::PackedVarInt location_ids;
-    for (int64_t frame : frames)
-      location_ids.Append(ToPprofId(frame));
+
+    // LocationTracker stores location lists root-first, but the pprof format
+    // requires leaf-first.
+    protozero::PackedVarInt packed_locs;
+    for (auto it = location_ids.rbegin(); it != location_ids.rend(); ++it)
+      packed_locs.Append(ToPprofId(*it));
 
     auto* gsample = result_->add_sample();
     gsample->set_value(values);
-    gsample->set_location_id(location_ids);
+    gsample->set_location_id(packed_locs);
 
-    // remember frames to be emitted
-    seen_frames_.insert(frames.cbegin(), frames.cend());
-
+    // Remember the locations s.t. we only serialize the referenced ones.
+    seen_locations_.insert(location_ids.cbegin(), location_ids.cend());
     return true;
   }
 
   std::string CompleteProfile(trace_processor::TraceProcessor* tp) {
     std::set<int64_t> seen_mappings;
-    std::set<int64_t> seen_symbol_ids;
+    std::set<int64_t> seen_functions;
 
-    // Write the location info for frames referenced by the added samples.
-    if (!WriteFrames(tp, &seen_mappings, &seen_symbol_ids))
+    if (!WriteLocations(&seen_mappings, &seen_functions))
+      return {};
+    if (!WriteFunctions(seen_functions))
       return {};
     if (!WriteMappings(tp, seen_mappings))
       return {};
-    if (!WriteSymbols(tp, seen_symbol_ids))
-      return {};
+
+    WriteStringTable();
     return result_.SerializeAsString();
   }
 
  private:
+  // Serializes the Profile.Location entries referenced by this profile.
+  bool WriteLocations(std::set<int64_t>* seen_mappings,
+                      std::set<int64_t>* seen_functions) {
+    const std::unordered_map<Location, int64_t>& locations =
+        locations_.AllLocations();
+
+    size_t written_locations = 0;
+    for (const auto& loc_and_id : locations) {
+      const auto& loc = loc_and_id.first;
+      int64_t id = loc_and_id.second;
+
+      if (seen_locations_.find(id) == seen_locations_.end())
+        continue;
+
+      written_locations += 1;
+      seen_mappings->emplace(loc.mapping_id);
+
+      auto* glocation = result_->add_location();
+      glocation->set_id(ToPprofId(id));
+      glocation->set_mapping_id(ToPprofId(loc.mapping_id));
+
+      if (!loc.inlined_functions.empty()) {
+        for (const auto& line : loc.inlined_functions) {
+          seen_functions->insert(line.function_id);
+
+          auto* gline = glocation->add_line();
+          gline->set_function_id(ToPprofId(line.function_id));
+          gline->set_line(line.line_no);
+        }
+      } else {
+        seen_functions->insert(loc.single_function_id);
+
+        glocation->add_line()->set_function_id(
+            ToPprofId(loc.single_function_id));
+      }
+    }
+
+    if (written_locations != seen_locations_.size()) {
+      PERFETTO_DFATAL_OR_ELOG(
+          "Found only %zu/%zu locations during serialization.",
+          written_locations, seen_locations_.size());
+      return false;
+    }
+    return true;
+  }
+
+  // Serializes the Profile.Function entries referenced by this profile.
+  bool WriteFunctions(const std::set<int64_t>& seen_functions) {
+    const std::unordered_map<Function, int64_t>& functions =
+        locations_.AllFunctions();
+
+    size_t written_functions = 0;
+    for (const auto& func_and_id : functions) {
+      const auto& func = func_and_id.first;
+      int64_t id = func_and_id.second;
+
+      if (seen_functions.find(id) == seen_functions.end())
+        continue;
+
+      written_functions += 1;
+
+      auto* gfunction = result_->add_function();
+      gfunction->set_id(ToPprofId(id));
+      gfunction->set_name(ToStringTableId(func.name_id));
+      gfunction->set_system_name(ToStringTableId(func.system_name_id));
+      if (!func.filename_id.is_null())
+        gfunction->set_filename(ToStringTableId(func.filename_id));
+    }
+
+    if (written_functions != seen_functions.size()) {
+      PERFETTO_DFATAL_OR_ELOG(
+          "Found only %zu/%zu functions during serialization.",
+          written_functions, seen_functions.size());
+      return false;
+    }
+    return true;
+  }
+
+  // Serializes the Profile.Mapping entries referenced by this profile.
   bool WriteMappings(trace_processor::TraceProcessor* tp,
                      const std::set<int64_t>& seen_mappings) {
     Iterator mapping_it = tp->ExecuteQuery(
@@ -260,7 +573,8 @@
       if (seen_mappings.find(id) == seen_mappings.end())
         continue;
       ++mappings_no;
-      auto interned_filename = Intern(mapping_it.Get(4).AsString());
+      auto interned_filename = ToStringTableId(
+          interner_->InternString(mapping_it.Get(4).AsString()));
       auto* gmapping = result_->add_mapping();
       gmapping->set_id(ToPprofId(id));
       // Do not set the build_id here to avoid downstream services
@@ -285,145 +599,48 @@
     return true;
   }
 
-  bool WriteSymbols(trace_processor::TraceProcessor* tp,
-                    const std::set<int64_t>& seen_symbol_ids) {
-    Iterator symbol_it = tp->ExecuteQuery(
-        "SELECT id, name, source_file FROM stack_profile_symbol");
-    size_t symbols_no = 0;
-    while (symbol_it.Next()) {
-      int64_t id = symbol_it.Get(0).AsLong();
-      if (seen_symbol_ids.find(id) == seen_symbol_ids.end())
-        continue;
-      ++symbols_no;
-      const std::string& name = symbol_it.Get(1).AsString();
-      std::string demangled_name = name;
-      MaybeDemangle(&demangled_name);
-
-      auto interned_demangled_name = Intern(demangled_name);
-      auto interned_system_name = Intern(name);
-      auto interned_filename = Intern(symbol_it.Get(2).AsString());
-      auto* gfunction = result_->add_function();
-      gfunction->set_id(ToPprofId(id));
-      gfunction->set_name(interned_demangled_name);
-      gfunction->set_system_name(interned_system_name);
-      gfunction->set_filename(interned_filename);
+  void WriteStringTable() {
+    for (StringId id : string_table_) {
+      trace_processor::NullTermStringView s = interner_->Get(id);
+      result_->add_string_table(s.data(), s.size());
     }
-
-    if (!symbol_it.Status().ok()) {
-      PERFETTO_DFATAL_OR_ELOG("Invalid iterator: %s",
-                              symbol_it.Status().message().c_str());
-      return false;
-    }
-
-    if (symbols_no != seen_symbol_ids.size()) {
-      PERFETTO_DFATAL_OR_ELOG("Missing symbols.");
-      return false;
-    }
-    return true;
   }
 
-  bool WriteFrames(trace_processor::TraceProcessor* tp,
-                   std::set<int64_t>* seen_mappings,
-                   std::set<int64_t>* seen_symbol_ids) {
-    Iterator frame_it = tp->ExecuteQuery(
-        "SELECT spf.id, IFNULL(spf.deobfuscated_name, spf.name), spf.mapping, "
-        "spf.rel_pc, spf.symbol_set_id "
-        "FROM stack_profile_frame spf;");
-    size_t frames_no = 0;
-    while (frame_it.Next()) {
-      int64_t frame_id = frame_it.Get(0).AsLong();
-      if (seen_frames_.find(frame_id) == seen_frames_.end())
-        continue;
-      frames_no++;
-      std::string frame_name = frame_it.Get(1).AsString();
-      int64_t mapping_id = frame_it.Get(2).AsLong();
-      int64_t rel_pc = frame_it.Get(3).AsLong();
-      base::Optional<int64_t> symbol_set_id;
-      if (!frame_it.Get(4).is_null())
-        symbol_set_id = frame_it.Get(4).AsLong();
-
-      seen_mappings->emplace(mapping_id);
-      auto* glocation = result_->add_location();
-      glocation->set_id(ToPprofId(frame_id));
-      glocation->set_mapping_id(ToPprofId(mapping_id));
-      // TODO(fmayer): Convert to abspc.
-      // relpc + (mapping.start - (mapping.exact_offset -
-      //                           mapping.start_offset)).
-      glocation->set_address(static_cast<uint64_t>(rel_pc));
-      if (symbol_set_id) {
-        for (const Line& line : LineForSymbolSetId(*symbol_set_id)) {
-          seen_symbol_ids->emplace(line.symbol_id);
-          auto* gline = glocation->add_line();
-          gline->set_line(line.line_number);
-          gline->set_function_id(ToPprofId(line.symbol_id));
-        }
-      } else {
-        int64_t synthesized_symbol_id = ++max_symbol_id_;
-        std::string demangled_name = frame_name;
-        MaybeDemangle(&demangled_name);
-
-        auto* gline = glocation->add_line();
-        gline->set_line(0);
-        gline->set_function_id(ToPprofId(synthesized_symbol_id));
-
-        auto interned_demangled_name = Intern(demangled_name);
-        auto interned_system_name = Intern(frame_name);
-        auto* gfunction = result_->add_function();
-        gfunction->set_id(ToPprofId(synthesized_symbol_id));
-        gfunction->set_name(interned_demangled_name);
-        gfunction->set_system_name(interned_system_name);
-      }
-    }
-
-    if (!frame_it.Status().ok()) {
-      PERFETTO_DFATAL_OR_ELOG("Invalid iterator: %s",
-                              frame_it.Status().message().c_str());
-      return false;
-    }
-    if (frames_no != seen_frames_.size()) {
-      PERFETTO_DFATAL_OR_ELOG("Missing frames.");
-      return false;
-    }
-    return true;
-  }
-
-  const std::vector<int64_t>& FramesForCallstack(int64_t callstack_id) {
-    size_t callsite_idx = static_cast<size_t>(callstack_id);
-    PERFETTO_CHECK(callstack_id >= 0 &&
-                   callsite_idx < callsite_to_frames_.size());
-    return callsite_to_frames_[callsite_idx];
-  }
-
-  const std::vector<Line>& LineForSymbolSetId(int64_t symbol_set_id) {
-    auto it = symbol_set_id_to_lines_.find(symbol_set_id);
-    if (it == symbol_set_id_to_lines_.end())
-      return empty_line_vector_;
-    return it->second;
-  }
-
-  int64_t Intern(const std::string& s) {
-    auto it = string_table_.find(s);
-    if (it == string_table_.end()) {
-      std::tie(it, std::ignore) =
-          string_table_.emplace(s, string_table_.size());
-      result_->add_string_table(s);
+  int64_t ToStringTableId(StringId interned_id) {
+    auto it = interning_remapper_.find(interned_id);
+    if (it == interning_remapper_.end()) {
+      int64_t table_id = static_cast<int64_t>(string_table_.size());
+      string_table_.push_back(interned_id);
+      bool inserted = false;
+      std::tie(it, inserted) =
+          interning_remapper_.emplace(interned_id, table_id);
+      PERFETTO_DCHECK(inserted);
     }
     return it->second;
   }
 
+  // Contains all locations, lines, functions (in memory):
+  const LocationTracker& locations_;
+
+  // String interner, strings referenced by LocationTracker are already
+  // interned. The new internings will come from mappings, and sample types.
+  trace_processor::StringPool* interner_;
+
+  // The profile format uses the repeated string_table field's index as an
+  // implicit id, so these structures remap the interned strings into sequential
+  // ids. Only the strings referenced by this GProfileBuilder instance will be
+  // added to the table.
+  std::unordered_map<StringId, int64_t> interning_remapper_;
+  std::vector<StringId> string_table_;
+
+  // Profile proto being serialized.
   protozero::HeapBuffered<third_party::perftools::profiles::pbzero::Profile>
       result_;
-  std::map<std::string, int64_t> string_table_;
-  const std::vector<std::vector<int64_t>>& callsite_to_frames_;
-  const std::map<int64_t, std::vector<Line>>& symbol_set_id_to_lines_;
-  const std::vector<Line> empty_line_vector_;
-  int64_t max_symbol_id_;
 
-  std::set<int64_t> seen_frames_;
+  // Set of locations referenced by the added samples.
+  std::set<int64_t> seen_locations_;
 };
 
-}  // namespace
-
 namespace heap_profile {
 struct View {
   const char* type;
@@ -543,13 +760,12 @@
 
 static bool TraceToHeapPprof(trace_processor::TraceProcessor* tp,
                              std::vector<SerializedProfile>* output,
+                             bool annotate_frames,
                              uint64_t target_pid,
                              const std::vector<uint64_t>& target_timestamps) {
-  const auto callsite_to_frames = GetCallsiteToFrames(tp);
-  const auto symbol_set_id_to_lines = GetSymbolSetIdToLines(tp);
-  base::Optional<int64_t> max_symbol_id = GetMaxSymbolId(tp);
-  if (!max_symbol_id.has_value())
-    return false;
+  trace_processor::StringPool interner;
+  LocationTracker locations =
+      PreprocessLocations(tp, &interner, annotate_frames);
 
   bool any_fail = false;
   Iterator it = tp->ExecuteQuery(
@@ -557,8 +773,7 @@
       "from heap_profile_allocation hpa, "
       "process p where p.upid = hpa.upid;");
   while (it.Next()) {
-    GProfileBuilder builder(callsite_to_frames, symbol_set_id_to_lines,
-                            max_symbol_id.value());
+    GProfileBuilder builder(locations, &interner);
     uint64_t upid = static_cast<uint64_t>(it.Get(0).AsLong());
     uint64_t ts = static_cast<uint64_t>(it.Get(1).AsLong());
     uint64_t profile_pid = static_cast<uint64_t>(it.Get(2).AsLong());
@@ -686,12 +901,11 @@
 // perf and heap profiles.
 static bool TraceToPerfPprof(trace_processor::TraceProcessor* tp,
                              std::vector<SerializedProfile>* output,
+                             bool annotate_frames,
                              uint64_t target_pid) {
-  const auto callsite_to_frames = GetCallsiteToFrames(tp);
-  const auto symbol_set_id_to_lines = GetSymbolSetIdToLines(tp);
-  base::Optional<int64_t> max_symbol_id = GetMaxSymbolId(tp);
-  if (!max_symbol_id.has_value())
-    return false;
+  trace_processor::StringPool interner;
+  LocationTracker locations =
+      PreprocessLocations(tp, &interner, annotate_frames);
 
   LogTracePerfEventIssues(tp);
 
@@ -703,9 +917,7 @@
     if (target_pid != 0 && process.pid != target_pid)
       continue;
 
-    GProfileBuilder builder(callsite_to_frames, symbol_set_id_to_lines,
-                            max_symbol_id.value());
-
+    GProfileBuilder builder(locations, &interner);
     builder.WriteSampleTypes({{"samples", "count"}});
 
     std::string query = "select callsite_id from perf_sample where utid in (" +
@@ -733,17 +945,22 @@
   return true;
 }
 }  // namespace perf_profile
+}  // namespace
 
 bool TraceToPprof(trace_processor::TraceProcessor* tp,
                   std::vector<SerializedProfile>* output,
                   ConversionMode mode,
+                  uint64_t flags,
                   uint64_t pid,
                   const std::vector<uint64_t>& timestamps) {
+  bool annotate_frames =
+      flags & static_cast<uint64_t>(ConversionFlags::kAnnotateFrames);
   switch (mode) {
     case (ConversionMode::kHeapProfile):
-      return heap_profile::TraceToHeapPprof(tp, output, pid, timestamps);
+      return heap_profile::TraceToHeapPprof(tp, output, annotate_frames, pid,
+                                            timestamps);
     case (ConversionMode::kPerfProfile):
-      return perf_profile::TraceToPerfPprof(tp, output, pid);
+      return perf_profile::TraceToPerfPprof(tp, output, annotate_frames, pid);
   }
   PERFETTO_FATAL("unknown conversion option");  // for gcc
 }
diff --git a/tools/trace_to_text/trace_to_profile.cc b/tools/trace_to_text/trace_to_profile.cc
index a394e2b..5b3fc4f 100644
--- a/tools/trace_to_text/trace_to_profile.cc
+++ b/tools/trace_to_text/trace_to_profile.cc
@@ -50,13 +50,18 @@
 namespace trace_to_text {
 namespace {
 
+uint64_t ToConversionFlags(bool annotate_frames) {
+  return static_cast<uint64_t>(annotate_frames
+                                   ? ConversionFlags::kAnnotateFrames
+                                   : ConversionFlags::kNone);
+}
+
 std::string GetRandomString(size_t n) {
   std::random_device r;
   auto rng = std::default_random_engine(r());
-  std::uniform_int_distribution<char> dist('a', 'z');
   std::string result(n, ' ');
   for (size_t i = 0; i < n; ++i) {
-    result[i] = dist(rng);
+    result[i] = 'a' + (rng() % ('z' - 'a'));
   }
   return result;
 }
@@ -92,6 +97,7 @@
     uint64_t pid,
     std::vector<uint64_t> timestamps,
     ConversionMode conversion_mode,
+    uint64_t conversion_flags,
     std::string dirname_prefix,
     std::function<std::string(const SerializedProfile&)> filename_fn) {
   std::vector<SerializedProfile> profiles;
@@ -106,7 +112,8 @@
   MaybeSymbolize(tp.get());
   MaybeDeobfuscate(tp.get());
 
-  TraceToPprof(tp.get(), &profiles, conversion_mode, pid, timestamps);
+  TraceToPprof(tp.get(), &profiles, conversion_mode, conversion_flags, pid,
+               timestamps);
   if (profiles.empty()) {
     return 0;
   }
@@ -132,31 +139,33 @@
 int TraceToHeapProfile(std::istream* input,
                        std::ostream* output,
                        uint64_t pid,
-                       std::vector<uint64_t> timestamps) {
+                       std::vector<uint64_t> timestamps,
+                       bool annotate_frames) {
   int file_idx = 0;
   auto filename_fn = [&file_idx](const SerializedProfile& profile) {
     return "heap_dump." + std::to_string(++file_idx) + "." +
            std::to_string(profile.pid) + "." + profile.heap_name + ".pb";
   };
 
-  return TraceToProfile(input, output, pid, timestamps,
-                        ConversionMode::kHeapProfile, "heap_profile-",
-                        filename_fn);
+  return TraceToProfile(
+      input, output, pid, timestamps, ConversionMode::kHeapProfile,
+      ToConversionFlags(annotate_frames), "heap_profile-", filename_fn);
 }
 
 int TraceToPerfProfile(std::istream* input,
                        std::ostream* output,
                        uint64_t pid,
-                       std::vector<uint64_t> timestamps) {
+                       std::vector<uint64_t> timestamps,
+                       bool annotate_frames) {
   int file_idx = 0;
   auto filename_fn = [&file_idx](const SerializedProfile& profile) {
     return "profile." + std::to_string(++file_idx) + ".pid." +
            std::to_string(profile.pid) + ".pb";
   };
 
-  return TraceToProfile(input, output, pid, timestamps,
-                        ConversionMode::kPerfProfile, "perf_profile-",
-                        filename_fn);
+  return TraceToProfile(
+      input, output, pid, timestamps, ConversionMode::kPerfProfile,
+      ToConversionFlags(annotate_frames), "perf_profile-", filename_fn);
 }
 
 }  // namespace trace_to_text
diff --git a/tools/trace_to_text/trace_to_profile.h b/tools/trace_to_text/trace_to_profile.h
index 1c41aad..313b32b 100644
--- a/tools/trace_to_text/trace_to_profile.h
+++ b/tools/trace_to_text/trace_to_profile.h
@@ -27,13 +27,15 @@
 int TraceToHeapProfile(std::istream* input,
                        std::ostream* output,
                        uint64_t pid,
-                       std::vector<uint64_t> timestamps);
+                       std::vector<uint64_t> timestamps,
+                       bool annotate_frames);
 
 // 0: success
 int TraceToPerfProfile(std::istream* input,
                        std::ostream* output,
                        uint64_t pid,
-                       std::vector<uint64_t> timestamps);
+                       std::vector<uint64_t> timestamps,
+                       bool annotate_frames);
 
 }  // namespace trace_to_text
 }  // namespace perfetto
diff --git a/tools/trace_to_text/trace_to_systrace.cc b/tools/trace_to_text/trace_to_systrace.cc
index 498b6c9..03d8aa0 100644
--- a/tools/trace_to_text/trace_to_systrace.cc
+++ b/tools/trace_to_text/trace_to_systrace.cc
@@ -47,6 +47,8 @@
 
 const char kThreadHeader[] = "USER           PID   TID CMD \\n";
 
+const char kProcessDumpFooter[] = "\"";
+
 const char kSystemTraceEvents[] =
     "  \"systemTraceEvents\": \"";
 
@@ -160,6 +162,114 @@
   TraceWriter* trace_writer_;
 };
 
+int ExtractRawEvents(TraceWriter* trace_writer,
+                     QueryWriter& q_writer,
+                     bool wrapped_in_json,
+                     Keep truncate_keep) {
+  using trace_processor::Iterator;
+
+  static const char kRawEventsCountSql[] =
+      "select count(1) from raw" FILTER_RAW_EVENTS;
+  uint32_t raw_events = 0;
+  auto e_callback = [&raw_events](Iterator* it, base::StringWriter*) {
+    raw_events = static_cast<uint32_t>(it->Get(0).long_value);
+  };
+  if (!q_writer.RunQuery(kRawEventsCountSql, e_callback))
+    return 1;
+
+  if (raw_events == 0) {
+    if (!wrapped_in_json) {
+      // Write out the normal header even if we won't actually have
+      // any events under it.
+      trace_writer->Write(kFtraceHeader);
+    }
+    return 0;
+  }
+
+  fprintf(stderr, "Converting ftrace events%c", kProgressChar);
+  fflush(stderr);
+
+  auto raw_callback = [wrapped_in_json](Iterator* it,
+                                        base::StringWriter* writer) {
+    const char* line = it->Get(0 /* col */).string_value;
+    if (wrapped_in_json) {
+      for (uint32_t i = 0; line[i] != '\0'; i++) {
+        char c = line[i];
+        switch (c) {
+          case '\n':
+            writer->AppendLiteral("\\n");
+            break;
+          case '\f':
+            writer->AppendLiteral("\\f");
+            break;
+          case '\b':
+            writer->AppendLiteral("\\b");
+            break;
+          case '\r':
+            writer->AppendLiteral("\\r");
+            break;
+          case '\t':
+            writer->AppendLiteral("\\t");
+            break;
+          case '\\':
+            writer->AppendLiteral("\\\\");
+            break;
+          case '"':
+            writer->AppendLiteral("\\\"");
+            break;
+          default:
+            writer->AppendChar(c);
+            break;
+        }
+      }
+      writer->AppendChar('\\');
+      writer->AppendChar('n');
+    } else {
+      writer->AppendString(line);
+      writer->AppendChar('\n');
+    }
+  };
+
+  // An estimate of 130b per ftrace event, allowing some space for the processes
+  // and threads.
+  const uint32_t max_ftrace_events = (140 * 1024 * 1024) / 130;
+
+  static const char kRawEventsQuery[] =
+      "select to_ftrace(id) from raw" FILTER_RAW_EVENTS;
+
+  // 1. Write the appropriate header for the file type.
+  if (wrapped_in_json) {
+    trace_writer->Write(",\n");
+    trace_writer->Write(kSystemTraceEvents);
+    trace_writer->Write(kFtraceJsonHeader);
+  } else {
+    trace_writer->Write(kFtraceHeader);
+  }
+
+  // 2. Write the actual events.
+  if (truncate_keep == Keep::kEnd && raw_events > max_ftrace_events) {
+    char end_truncate[150];
+    sprintf(end_truncate, "%s limit %d offset %d", kRawEventsQuery,
+            max_ftrace_events, raw_events - max_ftrace_events);
+    if (!q_writer.RunQuery(end_truncate, raw_callback))
+      return 1;
+  } else if (truncate_keep == Keep::kStart) {
+    char start_truncate[150];
+    sprintf(start_truncate, "%s limit %d", kRawEventsQuery, max_ftrace_events);
+    if (!q_writer.RunQuery(start_truncate, raw_callback))
+      return 1;
+  } else {
+    if (!q_writer.RunQuery(kRawEventsQuery, raw_callback))
+      return 1;
+  }
+
+  // 3. Write the footer for JSON.
+  if (wrapped_in_json)
+    trace_writer->Write(kSystemTraceEventsFooter);
+
+  return 0;
+}
+
 }  // namespace
 
 int TraceToSystrace(std::istream* input,
@@ -232,93 +342,10 @@
     if (!q_writer.RunQuery(kTSql, t_callback))
       return 1;
 
-    trace_writer->Write("\",\n");
-    trace_writer->Write(kSystemTraceEvents);
-    trace_writer->Write(kFtraceJsonHeader);
-  } else {
-    trace_writer->Write(kFtraceHeader);
+    trace_writer->Write(kProcessDumpFooter);
   }
-
-  fprintf(stderr, "Converting ftrace events%c", kProgressChar);
-  fflush(stderr);
-
-  static const char kEstimateSql[] =
-      "select count(1) from raw" FILTER_RAW_EVENTS;
-  uint32_t raw_events = 0;
-  auto e_callback = [&raw_events](Iterator* it, base::StringWriter*) {
-    raw_events = static_cast<uint32_t>(it->Get(0).long_value);
-  };
-  if (!q_writer.RunQuery(kEstimateSql, e_callback))
-    return 1;
-
-  auto raw_callback = [wrapped_in_json](Iterator* it,
-                                        base::StringWriter* writer) {
-    const char* line = it->Get(0 /* col */).string_value;
-    if (wrapped_in_json) {
-      for (uint32_t i = 0; line[i] != '\0'; i++) {
-        char c = line[i];
-        switch (c) {
-          case '\n':
-            writer->AppendLiteral("\\n");
-            break;
-          case '\f':
-            writer->AppendLiteral("\\f");
-            break;
-          case '\b':
-            writer->AppendLiteral("\\b");
-            break;
-          case '\r':
-            writer->AppendLiteral("\\r");
-            break;
-          case '\t':
-            writer->AppendLiteral("\\t");
-            break;
-          case '\\':
-            writer->AppendLiteral("\\\\");
-            break;
-          case '"':
-            writer->AppendLiteral("\\\"");
-            break;
-          default:
-            writer->AppendChar(c);
-            break;
-        }
-      }
-      writer->AppendChar('\\');
-      writer->AppendChar('n');
-    } else {
-      writer->AppendString(line);
-      writer->AppendChar('\n');
-    }
-  };
-
-  // An estimate of 130b per ftrace event, allowing some space for the processes
-  // and threads.
-  const uint32_t max_ftrace_events = (140 * 1024 * 1024) / 130;
-
-  static const char kRawEventsQuery[] =
-      "select to_ftrace(id) from raw" FILTER_RAW_EVENTS;
-
-  if (truncate_keep == Keep::kEnd && raw_events > max_ftrace_events) {
-    char end_truncate[150];
-    sprintf(end_truncate, "%s limit %d offset %d", kRawEventsQuery,
-            max_ftrace_events, raw_events - max_ftrace_events);
-    if (!q_writer.RunQuery(end_truncate, raw_callback))
-      return 1;
-  } else if (truncate_keep == Keep::kStart) {
-    char start_truncate[150];
-    sprintf(start_truncate, "%s limit %d", kRawEventsQuery, max_ftrace_events);
-    if (!q_writer.RunQuery(start_truncate, raw_callback))
-      return 1;
-  } else {
-    if (!q_writer.RunQuery(kRawEventsQuery, raw_callback))
-      return 1;
-  }
-
-  if (wrapped_in_json)
-    trace_writer->Write(kSystemTraceEventsFooter);
-
-  return 0;
+  return ExtractRawEvents(trace_writer, q_writer, wrapped_in_json,
+                          truncate_keep);
 }
 
 }  // namespace trace_to_text
diff --git a/tools/trace_to_text/trace_to_text.cc b/tools/trace_to_text/trace_to_text.cc
index 2fef5a5..1199d3a 100644
--- a/tools/trace_to_text/trace_to_text.cc
+++ b/tools/trace_to_text/trace_to_text.cc
@@ -16,7 +16,6 @@
 
 #include "tools/trace_to_text/trace_to_text.h"
 
-#include <google/protobuf/compiler/importer.h>
 #include <google/protobuf/dynamic_message.h>
 #include <google/protobuf/io/zero_copy_stream_impl.h>
 #include <google/protobuf/text_format.h>
@@ -24,7 +23,9 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/scoped_file.h"
+#include "src/protozero/proto_ring_buffer.h"
 #include "tools/trace_to_text/proto_full_utils.h"
+#include "tools/trace_to_text/trace.descriptor.h"
 #include "tools/trace_to_text/utils.h"
 
 #include "protos/perfetto/trace/trace.pbzero.h"
@@ -36,17 +37,17 @@
 
 namespace perfetto {
 namespace trace_to_text {
-
 namespace {
+
 using google::protobuf::Descriptor;
+using google::protobuf::DescriptorPool;
 using google::protobuf::DynamicMessageFactory;
 using google::protobuf::FieldDescriptor;
 using google::protobuf::FileDescriptor;
+using google::protobuf::FileDescriptorSet;
 using google::protobuf::Message;
 using google::protobuf::Reflection;
 using google::protobuf::TextFormat;
-using google::protobuf::compiler::DiskSourceTree;
-using google::protobuf::compiler::Importer;
 using google::protobuf::io::OstreamOutputStream;
 using google::protobuf::io::ZeroCopyOutputStream;
 
@@ -147,60 +148,87 @@
 }  // namespace
 
 int TraceToText(std::istream* input, std::ostream* output) {
-  const std::string proto_path = "protos/perfetto/trace/trace_packet.proto";
-
-  if (!base::OpenFile(proto_path, O_RDONLY)) {
-    PERFETTO_ELOG("Cannot open %s.", proto_path.c_str());
-    PERFETTO_ELOG(
-        "Text mode only works from the perfetto directory. Googlers, see "
-        "b/131425913");
-    return 1;
+  DescriptorPool pool;
+  FileDescriptorSet desc_set;
+  desc_set.ParseFromArray(kTraceDescriptor.data(), kTraceDescriptor.size());
+  for (const auto& desc : desc_set.file()) {
+    pool.BuildFile(desc);
   }
 
-  DiskSourceTree dst;
-  dst.MapPath("", "");
-  MultiFileErrorCollectorImpl mfe;
-  Importer importer(&dst, &mfe);
-  const FileDescriptor* parsed_file =
-      importer.Import("protos/perfetto/trace/trace_packet.proto");
+  DynamicMessageFactory factory(&pool);
+  const Descriptor* trace_descriptor =
+      pool.FindMessageTypeByName("perfetto.protos.TracePacket");
+  const Message* prototype = factory.GetPrototype(trace_descriptor);
+  std::unique_ptr<Message> msg(prototype->New());
 
-  DynamicMessageFactory dmf;
-  const Descriptor* trace_descriptor = parsed_file->message_type(0);
-  const Message* root = dmf.GetPrototype(trace_descriptor);
   OstreamOutputStream zero_copy_output(output);
   OstreamOutputStream* zero_copy_output_ptr = &zero_copy_output;
-  Message* msg = root->New();
 
   constexpr uint32_t kCompressedPacketFieldDescriptor = 50;
   const Reflection* reflect = msg->GetReflection();
   const FieldDescriptor* compressed_desc =
       trace_descriptor->FindFieldByNumber(kCompressedPacketFieldDescriptor);
-  Message* compressed_msg_scratch = root->New();
-  std::string compressed_packet_scratch;
+
+  std::unique_ptr<Message> compressed_packets_msg(prototype->New());
+  std::string compressed_packets;
 
   TextFormat::Printer printer;
   printer.SetInitialIndentLevel(1);
-  ForEachPacketBlobInTrace(
-      input, [msg, reflect, compressed_desc, zero_copy_output_ptr,
-              &compressed_packet_scratch, compressed_msg_scratch,
-              &printer](std::unique_ptr<char[]> buf, size_t size) {
-        if (!msg->ParseFromArray(buf.get(), static_cast<int>(size))) {
-          PERFETTO_ELOG("Skipping invalid packet");
-          return;
-        }
-        if (reflect->HasField(*msg, compressed_desc)) {
-          const auto& compressed_packets = reflect->GetStringReference(
-              *msg, compressed_desc, &compressed_packet_scratch);
-          PrintCompressedPackets(compressed_packets, compressed_msg_scratch,
-                                 zero_copy_output_ptr);
-        } else {
-          WriteToZeroCopyOutput(zero_copy_output_ptr, kPacketPrefix,
-                                sizeof(kPacketPrefix) - 1);
-          printer.Print(*msg, zero_copy_output_ptr);
-          WriteToZeroCopyOutput(zero_copy_output_ptr, kPacketSuffix,
-                                sizeof(kPacketSuffix) - 1);
-        }
-      });
+
+  static constexpr size_t kMaxMsgSize = protozero::ProtoRingBuffer::kMaxMsgSize;
+  std::unique_ptr<char> data(new char[kMaxMsgSize]);
+  protozero::ProtoRingBuffer ring_buffer;
+
+  uint32_t packet = 0;
+  size_t bytes_processed = 0;
+  while (!input->eof()) {
+    input->read(data.get(), kMaxMsgSize);
+    if (input->bad() || (input->fail() && !input->eof())) {
+      PERFETTO_ELOG("Failed while reading trace");
+      return 1;
+    }
+    ring_buffer.Append(data.get(), static_cast<size_t>(input->gcount()));
+
+    for (;;) {
+      auto token = ring_buffer.ReadMessage();
+      if (token.fatal_framing_error) {
+        PERFETTO_ELOG("Failed to tokenize trace packet");
+        return 1;
+      }
+      if (!token.valid())
+        break;
+      bytes_processed += token.len;
+
+      if (token.field_id != 1) {
+        PERFETTO_ELOG("Skipping invalid field");
+        continue;
+      }
+
+      if ((packet++ & 0x3f) == 0) {
+        fprintf(stderr, "Processing trace: %8zu KB%c", bytes_processed / 1024,
+                kProgressChar);
+        fflush(stderr);
+      }
+
+      if (!msg->ParseFromArray(token.start, static_cast<int>(token.len))) {
+        PERFETTO_ELOG("Skipping invalid packet");
+        continue;
+      }
+
+      if (reflect->HasField(*msg, compressed_desc)) {
+        compressed_packets = reflect->GetStringReference(*msg, compressed_desc,
+                                                         &compressed_packets);
+        PrintCompressedPackets(compressed_packets, compressed_packets_msg.get(),
+                               zero_copy_output_ptr);
+      } else {
+        WriteToZeroCopyOutput(zero_copy_output_ptr, kPacketPrefix,
+                              sizeof(kPacketPrefix) - 1);
+        printer.Print(*msg, zero_copy_output_ptr);
+        WriteToZeroCopyOutput(zero_copy_output_ptr, kPacketSuffix,
+                              sizeof(kPacketSuffix) - 1);
+      }
+    }
+  }
   return 0;
 }
 
diff --git a/tools/trace_to_text/utils.cc b/tools/trace_to_text/utils.cc
index ff1ae61..fb0505c 100644
--- a/tools/trace_to_text/utils.cc
+++ b/tools/trace_to_text/utils.cc
@@ -48,52 +48,6 @@
 
 }  // namespace
 
-void ForEachPacketBlobInTrace(
-    std::istream* input,
-    const std::function<void(std::unique_ptr<char[]>, size_t)>& f) {
-  size_t bytes_processed = 0;
-  // The trace stream can be very large. We cannot just pass it in one go to
-  // libprotobuf as that will refuse to parse messages > 64MB. However we know
-  // that a trace is merely a sequence of TracePackets. Here we just manually
-  // tokenize the repeated TracePacket messages and parse them individually
-  // using libprotobuf.
-  for (uint32_t i = 0;; i++) {
-    if ((i & 0x3f) == 0) {
-      fprintf(stderr, "Processing trace: %8zu KB%c", bytes_processed / 1024,
-              kProgressChar);
-      fflush(stderr);
-    }
-    // A TracePacket consists in one byte stating its field id and type ...
-    char preamble;
-    input->get(preamble);
-    if (!input->good())
-      break;
-    bytes_processed++;
-    PERFETTO_DCHECK(preamble == 0x0a);  // Field ID:1, type:length delimited.
-
-    // ... a varint stating its size ...
-    uint32_t field_size = 0;
-    uint32_t shift = 0;
-    for (;;) {
-      char c = 0;
-      input->get(c);
-      field_size |= static_cast<uint32_t>(c & 0x7f) << shift;
-      shift += 7;
-      bytes_processed++;
-      if (!(c & 0x80))
-        break;
-    }
-
-    // ... and the actual TracePacket itself.
-    std::unique_ptr<char[]> buf(new char[field_size]);
-    input->read(buf.get(), static_cast<std::streamsize>(field_size));
-    bytes_processed += field_size;
-
-    f(std::move(buf), field_size);
-  }
-}
-
-
 bool ReadTrace(trace_processor::TraceProcessor* tp, std::istream* input) {
   // 1MB chunk size seems the best tradeoff on a MacBook Pro 2013 - i7 2.8 GHz.
   constexpr size_t kChunkSize = 1024 * 1024;
diff --git a/tools/trace_to_text/utils.h b/tools/trace_to_text/utils.h
index f95c3d8..063b891 100644
--- a/tools/trace_to_text/utils.h
+++ b/tools/trace_to_text/utils.h
@@ -53,11 +53,6 @@
 constexpr char kProgressChar = '\r';
 #endif
 
-void ForEachPacketBlobInTrace(
-    std::istream* input,
-    const std::function<void(std::unique_ptr<char[]>, size_t)>&);
-
-
 bool ReadTrace(trace_processor::TraceProcessor* tp, std::istream* input);
 void IngestTraceOrDie(trace_processor::TraceProcessor* tp,
                       const std::string& trace_proto);
diff --git a/tools/traceconv b/tools/traceconv
index c0cff60..9887ec1 100755
--- a/tools/traceconv
+++ b/tools/traceconv
@@ -21,7 +21,7 @@
 # cat ./traceconv | python -
 
 BASH_FALLBACK = """ "
-exec python - "$@" <<'#'EOF
+exec python3 - "$@" <<'#'EOF
 #"""
 
 import hashlib
@@ -33,8 +33,8 @@
 
 
 TRACE_TO_TEXT_SHAS = {
-    'linux': 'a37bad26ec5f216e5a249db4ff6ce8b1ecb35ec1',
-    'mac': 'ffb4ecb6e0d100188f203821f16b3972ac179a7d',
+    'linux': '7e3e10dfb324e31723efd63ac25037856e06eba0',
+    'mac': '21f0f42dd019b4f09addd404a114fbf2322ca8a4',
 }
 TRACE_TO_TEXT_PATH = tempfile.gettempdir()
 TRACE_TO_TEXT_BASE_URL = ('https://storage.googleapis.com/perfetto/')
diff --git a/tools/update_trace_processor b/tools/update_trace_processor
index 3968efa..4ab27f6 100755
--- a/tools/update_trace_processor
+++ b/tools/update_trace_processor
@@ -3,6 +3,7 @@
 set -e
 
 DIR=$(mktemp -d out/perfetto.XXXXXX)
+STRIPPED_DIR=$DIR/stripped
 
 function cleanup {
   rm -rf "$DIR"
@@ -23,18 +24,17 @@
   platform=mac
 else
   platform=linux
-  strip $DIR/trace_processor_shell
 fi
 
 if which shasum; then
-  NEW_SHA=$(shasum $DIR/trace_processor_shell | cut -f1 -d' ') # Mac OS
+  NEW_SHA=$(shasum $STRIPPED_DIR/trace_processor_shell | cut -f1 -d' ') # Mac OS
 else
-  NEW_SHA=$(sha1sum $DIR/trace_processor_shell | cut -f1 -d' ') # Linux
+  NEW_SHA=$(sha1sum $STRIPPED_DIR/trace_processor_shell | cut -f1 -d' ') # Linux
 fi
 
 name=trace_processor_shell-$platform-$NEW_SHA
 
-gsutil cp $DIR/trace_processor_shell gs://perfetto/$name
+gsutil cp $STRIPPED_DIR/trace_processor_shell gs://perfetto/$name
 gsutil acl ch -u AllUsers:R gs://perfetto/$name
 
 echo 'Now run the following command to update tools/trace_processor:'
diff --git a/tools/update_tracebox b/tools/update_tracebox
new file mode 100755
index 0000000..0836b25
--- /dev/null
+++ b/tools/update_tracebox
@@ -0,0 +1,76 @@
+#!/bin/bash
+# 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.
+
+set -e
+
+# cd into the repo root so all commands are independent of the cwd.
+cd -P ${BASH_SOURCE[0]%/*}/..
+
+GN_ARGS="$GN_ARGS"  # Allow to expand GN_ARGS from env for ccache.
+
+set -u
+
+DIR=out/tmp_tracebox
+mkdir -p $DIR
+
+function cleanup {
+  rm -rf "$DIR"
+  echo "Deleted temp working directory $DIR"
+}
+
+#trap cleanup EXIT
+
+function is_mac {
+  ! test -d /proc
+  return $?
+}
+
+VERSION="$(tools/write_version_header.py --stdout)"
+
+# Allow overriding the build targets via the cmdline (for testing).
+if [ "$#" -gt 0 ]; then
+  COMBOS="$@"
+else
+  COMBOS="android-arm android-arm64 android-x86 android-x64"
+fi
+
+for COMBO in $COMBOS; do
+  IFS=- read PLATFORM ARCH <<< "$COMBO"
+  echo "Building for $COMBO"
+  rm -rf $DIR
+  mkdir -p $DIR
+  GN_ARGS="$GN_ARGS is_debug=false monolithic_binaries = true"
+  GN_ARGS="$GN_ARGS target_os = \"$PLATFORM\" target_cpu = \"$ARCH\""
+  set -x
+  tools/gn gen $DIR --args="$GN_ARGS"
+  tools/ninja -C $DIR tracebox
+  set +x
+  BINARY=$DIR/stripped/tracebox
+if which shasum; then
+  NEW_SHA=$(shasum $BINARY | cut -f1 -d' ') # Mac OS
+else
+  NEW_SHA=$(sha1sum $BINARY | cut -f1 -d' ') # Linux
+fi
+  FULL_NAME=tracebox-$COMBO-$NEW_SHA
+
+  set -x
+  gsutil cp -n -a public-read $BINARY gs://perfetto/$FULL_NAME
+  sed -e \
+      "s/'$COMBO': '.*/'$COMBO': '$NEW_SHA',  # $VERSION/" \
+      -i .tmp tools/record_android_trace
+  set +x
+  rm -f tools/record_android_trace.tmp
+  echo ""
+done
diff --git a/tools/update_traceconv b/tools/update_traceconv
index e7ee66b..236068c 100755
--- a/tools/update_traceconv
+++ b/tools/update_traceconv
@@ -3,6 +3,7 @@
 set -e
 
 DIR=$(mktemp -d out/perfetto.XXXXXX)
+STRIPPED_DIR=$DIR/stripped
 
 function cleanup {
   rm -rf "$DIR"
@@ -23,22 +24,23 @@
   platform=mac
 else
   platform=linux
-  strip $DIR/trace_to_text
 fi
 
 if which shasum; then
-  NEW_SHA=$(shasum $DIR/trace_to_text | cut -f1 -d' ') # Mac OS
+  NEW_SHA=$(shasum $STRIPPED_DIR/trace_to_text | cut -f1 -d' ') # Mac OS
 else
-  NEW_SHA=$(sha1sum $DIR/trace_to_text | cut -f1 -d' ') # Linux
+  NEW_SHA=$(sha1sum $STRIPPED_DIR/trace_to_text | cut -f1 -d' ') # Linux
 fi
 
 name=trace_to_text-$platform-$NEW_SHA
 
-gsutil cp $DIR/trace_to_text gs://perfetto/$name
-gsutil cp $DIR/trace_to_text gs://chromium-telemetry/binary_dependencies/$name
+gsutil cp $STRIPPED_DIR/trace_to_text gs://perfetto/$name
+gsutil cp $STRIPPED_DIR/trace_to_text gs://chromium-telemetry/binary_dependencies/$name
 gsutil acl ch -u AllUsers:R gs://perfetto/$name
 gsutil acl ch -u AllUsers:R gs://chromium-telemetry/binary_dependencies/$name
 
 echo 'Now run the following command to update tools/traceconv:'
 echo "sed \"s/'$platform': '[^']*',/'$platform': '$NEW_SHA',/\" --in-place tools/traceconv"
 
+echo 'Now run the following command to update tools/heap_profile:'
+echo "sed \"s/'$platform': '[^']*',/'$platform': '$NEW_SHA',/\" --in-place tools/heap_profile"
diff --git a/tools/write_version_header.py b/tools/write_version_header.py
index 0325f6f..dd02a69 100755
--- a/tools/write_version_header.py
+++ b/tools/write_version_header.py
@@ -123,7 +123,7 @@
     guard = '%s_' % args.cpp_out.upper()
     guard = re.sub(r'[^\w]', '_', guard)
     lines = []
-    lines.append('// Generated by %s' % __file__)
+    lines.append('// Generated by %s' % os.path.basename(__file__))
     lines.append('')
     lines.append('#ifndef %s' % guard)
     lines.append('#define %s' % guard)
diff --git a/ui/build.js b/ui/build.js
index 9d1a7f5..9867ad2 100644
--- a/ui/build.js
+++ b/ui/build.js
@@ -82,7 +82,6 @@
   debug: false,
   startHttpServer: false,
   wasmModules: ['trace_processor', 'trace_to_text'],
-  testConfigs: ['jest.unit.config.js'],
 
   // The fields below will be changed by main() after cmdline parsing.
   // Directory structure:
@@ -117,12 +116,11 @@
 
 let tasks = [];
 let tasksTot = 0, tasksRan = 0;
-let serverStarted = false;
 let httpWatches = [];
 let tStart = Date.now();
 let subprocesses = [];
 
-function main() {
+async function main() {
   const parser = new argparse.ArgumentParser();
   parser.addArgument('--out', {help: 'Output directory'});
   parser.addArgument(['--watch', '-w'], {action: 'storeTrue'});
@@ -130,8 +128,9 @@
   parser.addArgument(['--verbose', '-v'], {action: 'storeTrue'});
   parser.addArgument(['--no-build', '-n'], {action: 'storeTrue'});
   parser.addArgument(['--no-wasm', '-W'], {action: 'storeTrue'});
-  parser.addArgument(['--run-tests', '-t'], {action: 'storeTrue'});
+  parser.addArgument(['--run-unittests', '-t'], {action: 'storeTrue'});
   parser.addArgument(['--debug', '-d'], {action: 'storeTrue'});
+  parser.addArgument(['--interactive', '-i'], {action: 'storeTrue'});
 
   const args = parser.parseArgs();
   const clean = !args.no_build;
@@ -148,6 +147,9 @@
   cfg.verbose = !!args.verbose;
   cfg.debug = !!args.debug;
   cfg.startHttpServer = args.serve;
+  if (args.interactive) {
+    process.env.PERFETTO_UI_TESTS_INTERACTIVE = '1';
+  }
 
   process.on('SIGINT', () => {
     console.log('\nSIGINT received. Killing all child processes and exiting');
@@ -159,7 +161,8 @@
 
   // Check that deps are current before starting.
   const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps');
-  const depsArgs = ['--check-only', pjoin(cfg.outDir, '.check_deps'), '--ui'];
+  const checkDepsPath = pjoin(cfg.outDir, '.check_deps');
+  const depsArgs = [`--check-only=${checkDepsPath}`, '--ui'];
   exec(installBuildDeps, depsArgs);
 
   console.log('Entering', cfg.outDir);
@@ -191,8 +194,22 @@
     scanDir(cfg.outDistRootDir);
   }
 
-  if (args.run_tests) {
-    runTests();
+
+  // We should enter the loop only in watch mode, where tsc and rollup are
+  // asynchronous because they run in watch mode.
+  const tStart = Date.now();
+  while (!isDistComplete()) {
+    const secs = Math.ceil((Date.now() - tStart) / 1000);
+    process.stdout.write(`Waiting for first build to complete... ${secs} s\r`);
+    await new Promise(r => setTimeout(r, 500));
+  }
+  if (cfg.watch) console.log('\nFirst build completed!');
+
+  if (cfg.startHttpServer) {
+    startServer();
+  }
+  if (args.run_unittests) {
+    runTests('jest.unittest.config.js');
   }
 }
 
@@ -200,12 +217,17 @@
 // Build rules
 // -----------
 
-function runTests() {
-  const args =
-      ['--rootDir', cfg.outTscDir, '--verbose', '--runInBand', '--forceExit'];
-  for (const cfgFile of cfg.testConfigs) {
-    args.push('--projects', pjoin(ROOT_DIR, 'ui/config', cfgFile));
-  }
+function runTests(cfgFile) {
+  const args = [
+    '--rootDir',
+    cfg.outTscDir,
+    '--verbose',
+    '--runInBand',
+    '--detectOpenHandles',
+    '--forceExit',
+    '--projects',
+    pjoin(ROOT_DIR, 'ui/config', cfgFile)
+  ];
   if (cfg.watch) {
     args.push('--watchAll');
     addTask(execNode, ['jest', args, {async: true}]);
@@ -283,8 +305,14 @@
 }
 
 function updateSymlinks() {
+  // /ui/out -> /out/ui.
   mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out'));
 
+  // /out/ui/test/data -> /test/data (For UI tests).
+  mklink(
+      pjoin(ROOT_DIR, 'test/data'),
+      pjoin(ensureDir(pjoin(cfg.outDir, 'test')), 'data'));
+
   // Creates a out/dist_version -> out/dist/v1.2.3 symlink, so rollup config
   // can point to that without having to know the current version number.
   mklink(
@@ -426,6 +454,24 @@
       .listen(port, '127.0.0.1');
 }
 
+function isDistComplete() {
+  const requiredArtifacts = [
+    'controller_bundle.js',
+    'frontend_bundle.js',
+    'engine_bundle.js',
+    'trace_processor.wasm',
+    'perfetto.css',
+  ];
+  const relPaths = new Set();
+  walk(cfg.outDistDir, absPath => {
+    relPaths.add(path.relative(cfg.outDistDir, absPath));
+  });
+  for (const fName of requiredArtifacts) {
+    if (!relPaths.has(fName)) return false;
+  }
+  return true;
+}
+
 // Called whenever a change in the out/dist directory is detected. It sends a
 // Server-Side-Event to the live_reload.ts script.
 function notifyLiveServer(changedFile) {
@@ -475,11 +521,6 @@
     console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`);
     task.func.apply(/*this=*/ undefined, task.args);
   }
-  // Start the web server once reaching quiescence.
-  if (tasks.length === 0 && !serverStarted && cfg.startHttpServer) {
-    serverStarted = true;
-    startServer();
-  }
 }
 
 // Executes all the RULES that match the given |absPath|.
diff --git a/ui/config/.gitignore b/ui/config/.gitignore
new file mode 100644
index 0000000..acd163c
--- /dev/null
+++ b/ui/config/.gitignore
@@ -0,0 +1 @@
+/jest.individual.unit.config.js
diff --git a/ui/config/headless_environment.js b/ui/config/headless_environment.js
deleted file mode 100644
index 6c4f3ec..0000000
--- a/ui/config/headless_environment.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2018 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.
-
-const NodeEnvironment = require('jest-environment-node');
-const puppeteer = require('puppeteer');
-
-module.exports = class HeadlessEnvironment extends NodeEnvironment {
-  constructor(config) {
-    super(config);
-  }
-
-  async setup() {
-    await super.setup();
-    this.global.__BROWSER__ = await puppeteer.launch();
-  }
-
-  async teardown() {
-    await this.global.__BROWSER__.close();
-    await super.teardown();
-  }
-
-  runScript(script) {
-    return super.runScript(script);
-  }
-}
diff --git a/ui/config/headless_teardown.js b/ui/config/headless_teardown.js
deleted file mode 100644
index 5fb4f1a..0000000
--- a/ui/config/headless_teardown.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (C) 2018 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.
-
-module.exports = async function() {
-  console.log("End!");
-}
diff --git a/ui/config/jest.headless.config.js b/ui/config/jest.headless.config.js
deleted file mode 100644
index 723ab25..0000000
--- a/ui/config/jest.headless.config.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-module.exports = {
-  transform: {},
-  testRegex: '.*_headlesstest.js$',
-  globalSetup: './headless_setup.js',
-  globalTeardown: './headless_teardown.js',
-  testEnvironment: './headless_environment.js'
-}
diff --git a/ui/config/jest.unit.config.js b/ui/config/jest.unit.config.js
deleted file mode 100644
index a2ddb95..0000000
--- a/ui/config/jest.unit.config.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-module.exports = {
-  transform: {},
-  testRegex: '.*_unittest.js$',
-  testEnvironment: 'node',
-  verbose: true,
-}
diff --git a/ui/config/jest.jsdom.config.js b/ui/config/jest.unittest.config.js
similarity index 93%
rename from ui/config/jest.jsdom.config.js
rename to ui/config/jest.unittest.config.js
index dd07d83..fc1bd35 100644
--- a/ui/config/jest.jsdom.config.js
+++ b/ui/config/jest.unittest.config.js
@@ -14,6 +14,6 @@
 
 module.exports = {
   transform: {},
-  testRegex: '_jsdomtest.js$',
+  testRegex: '_(unittest|jsdomtest)[.]js$',
   testEnvironment: 'jsdom'
 }
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 2a5f87f..c0236d7 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -13,35 +13,35 @@
         "@babel/highlight": "^7.12.13"
       }
     },
+    "@babel/compat-data": {
+      "version": "7.14.4",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.4.tgz",
+      "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==",
+      "dev": true
+    },
     "@babel/core": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz",
-      "integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==",
+      "version": "7.14.3",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz",
+      "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.12.13",
-        "@babel/generator": "^7.12.13",
-        "@babel/helper-module-transforms": "^7.12.13",
-        "@babel/helpers": "^7.12.13",
-        "@babel/parser": "^7.12.13",
+        "@babel/generator": "^7.14.3",
+        "@babel/helper-compilation-targets": "^7.13.16",
+        "@babel/helper-module-transforms": "^7.14.2",
+        "@babel/helpers": "^7.14.0",
+        "@babel/parser": "^7.14.3",
         "@babel/template": "^7.12.13",
-        "@babel/traverse": "^7.12.13",
-        "@babel/types": "^7.12.13",
+        "@babel/traverse": "^7.14.2",
+        "@babel/types": "^7.14.2",
         "convert-source-map": "^1.7.0",
         "debug": "^4.1.0",
-        "gensync": "^1.0.0-beta.1",
+        "gensync": "^1.0.0-beta.2",
         "json5": "^2.1.2",
-        "lodash": "^4.17.19",
-        "semver": "^5.4.1",
+        "semver": "^6.3.0",
         "source-map": "^0.5.0"
       },
       "dependencies": {
-        "semver": {
-          "version": "5.7.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-          "dev": true
-        },
         "source-map": {
           "version": "0.5.7",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -51,12 +51,12 @@
       }
     },
     "@babel/generator": {
-      "version": "7.12.15",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz",
-      "integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==",
+      "version": "7.14.3",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz",
+      "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.13",
+        "@babel/types": "^7.14.2",
         "jsesc": "^2.5.1",
         "source-map": "^0.5.0"
       },
@@ -69,15 +69,27 @@
         }
       }
     },
+    "@babel/helper-compilation-targets": {
+      "version": "7.14.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.4.tgz",
+      "integrity": "sha512-JgdzOYZ/qGaKTVkn5qEDV/SXAh8KcyUVkCoSWGN8T3bwrgd6m+/dJa2kVGi6RJYJgEYPBdZ84BZp9dUjNWkBaA==",
+      "dev": true,
+      "requires": {
+        "@babel/compat-data": "^7.14.4",
+        "@babel/helper-validator-option": "^7.12.17",
+        "browserslist": "^4.16.6",
+        "semver": "^6.3.0"
+      }
+    },
     "@babel/helper-function-name": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz",
-      "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==",
+      "version": "7.14.2",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz",
+      "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==",
       "dev": true,
       "requires": {
         "@babel/helper-get-function-arity": "^7.12.13",
         "@babel/template": "^7.12.13",
-        "@babel/types": "^7.12.13"
+        "@babel/types": "^7.14.2"
       }
     },
     "@babel/helper-get-function-arity": {
@@ -90,38 +102,45 @@
       }
     },
     "@babel/helper-member-expression-to-functions": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz",
-      "integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==",
+      "version": "7.13.12",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz",
+      "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.13"
+        "@babel/types": "^7.13.12"
       }
     },
     "@babel/helper-module-imports": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz",
-      "integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==",
+      "version": "7.13.12",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz",
+      "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.13"
+        "@babel/types": "^7.13.12"
       }
     },
     "@babel/helper-module-transforms": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.13.tgz",
-      "integrity": "sha512-acKF7EjqOR67ASIlDTupwkKM1eUisNAjaSduo5Cz+793ikfnpe7p4Q7B7EWU2PCoSTPWsQkR7hRUWEIZPiVLGA==",
+      "version": "7.14.2",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz",
+      "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==",
       "dev": true,
       "requires": {
-        "@babel/helper-module-imports": "^7.12.13",
-        "@babel/helper-replace-supers": "^7.12.13",
-        "@babel/helper-simple-access": "^7.12.13",
+        "@babel/helper-module-imports": "^7.13.12",
+        "@babel/helper-replace-supers": "^7.13.12",
+        "@babel/helper-simple-access": "^7.13.12",
         "@babel/helper-split-export-declaration": "^7.12.13",
-        "@babel/helper-validator-identifier": "^7.12.11",
+        "@babel/helper-validator-identifier": "^7.14.0",
         "@babel/template": "^7.12.13",
-        "@babel/traverse": "^7.12.13",
-        "@babel/types": "^7.12.13",
-        "lodash": "^4.17.19"
+        "@babel/traverse": "^7.14.2",
+        "@babel/types": "^7.14.2"
+      },
+      "dependencies": {
+        "@babel/helper-validator-identifier": {
+          "version": "7.14.0",
+          "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
+          "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
+          "dev": true
+        }
       }
     },
     "@babel/helper-optimise-call-expression": {
@@ -134,30 +153,30 @@
       }
     },
     "@babel/helper-plugin-utils": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz",
-      "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==",
+      "version": "7.13.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz",
+      "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==",
       "dev": true
     },
     "@babel/helper-replace-supers": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz",
-      "integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==",
+      "version": "7.14.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.4.tgz",
+      "integrity": "sha512-zZ7uHCWlxfEAAOVDYQpEf/uyi1dmeC7fX4nCf2iz9drnCwi1zvwXL3HwWWNXUQEJ1k23yVn3VbddiI9iJEXaTQ==",
       "dev": true,
       "requires": {
-        "@babel/helper-member-expression-to-functions": "^7.12.13",
+        "@babel/helper-member-expression-to-functions": "^7.13.12",
         "@babel/helper-optimise-call-expression": "^7.12.13",
-        "@babel/traverse": "^7.12.13",
-        "@babel/types": "^7.12.13"
+        "@babel/traverse": "^7.14.2",
+        "@babel/types": "^7.14.4"
       }
     },
     "@babel/helper-simple-access": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz",
-      "integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==",
+      "version": "7.13.12",
+      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz",
+      "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.12.13"
+        "@babel/types": "^7.13.12"
       }
     },
     "@babel/helper-split-export-declaration": {
@@ -175,15 +194,21 @@
       "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
       "dev": true
     },
+    "@babel/helper-validator-option": {
+      "version": "7.12.17",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz",
+      "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==",
+      "dev": true
+    },
     "@babel/helpers": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.13.tgz",
-      "integrity": "sha512-oohVzLRZ3GQEk4Cjhfs9YkJA4TdIDTObdBEZGrd6F/T0GPSnuV6l22eMcxlvcvzVIPH3VTtxbseudM1zIE+rPQ==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz",
+      "integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==",
       "dev": true,
       "requires": {
         "@babel/template": "^7.12.13",
-        "@babel/traverse": "^7.12.13",
-        "@babel/types": "^7.12.13"
+        "@babel/traverse": "^7.14.0",
+        "@babel/types": "^7.14.0"
       }
     },
     "@babel/highlight": {
@@ -250,9 +275,9 @@
       }
     },
     "@babel/parser": {
-      "version": "7.12.15",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz",
-      "integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==",
+      "version": "7.14.4",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz",
+      "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==",
       "dev": true
     },
     "@babel/plugin-syntax-async-generators": {
@@ -354,6 +379,15 @@
         "@babel/helper-plugin-utils": "^7.8.0"
       }
     },
+    "@babel/plugin-syntax-top-level-await": {
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz",
+      "integrity": "sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.12.13"
+      }
+    },
     "@babel/template": {
       "version": "7.12.13",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz",
@@ -366,31 +400,37 @@
       }
     },
     "@babel/traverse": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz",
-      "integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==",
+      "version": "7.14.2",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz",
+      "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.12.13",
-        "@babel/generator": "^7.12.13",
-        "@babel/helper-function-name": "^7.12.13",
+        "@babel/generator": "^7.14.2",
+        "@babel/helper-function-name": "^7.14.2",
         "@babel/helper-split-export-declaration": "^7.12.13",
-        "@babel/parser": "^7.12.13",
-        "@babel/types": "^7.12.13",
+        "@babel/parser": "^7.14.2",
+        "@babel/types": "^7.14.2",
         "debug": "^4.1.0",
-        "globals": "^11.1.0",
-        "lodash": "^4.17.19"
+        "globals": "^11.1.0"
       }
     },
     "@babel/types": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz",
-      "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==",
+      "version": "7.14.4",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+      "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
       "dev": true,
       "requires": {
-        "@babel/helper-validator-identifier": "^7.12.11",
-        "lodash": "^4.17.19",
+        "@babel/helper-validator-identifier": "^7.14.0",
         "to-fast-properties": "^2.0.0"
+      },
+      "dependencies": {
+        "@babel/helper-validator-identifier": {
+          "version": "7.14.0",
+          "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
+          "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
+          "dev": true
+        }
       }
     },
     "@bcoe/v8-coverage": {
@@ -423,132 +463,135 @@
       }
     },
     "@istanbuljs/schema": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz",
-      "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==",
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+      "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
       "dev": true
     },
     "@jest/console": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz",
-      "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz",
+      "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
-        "chalk": "^3.0.0",
-        "jest-message-util": "^25.5.0",
-        "jest-util": "^25.5.0",
+        "@jest/types": "^26.6.2",
+        "@types/node": "*",
+        "chalk": "^4.0.0",
+        "jest-message-util": "^26.6.2",
+        "jest-util": "^26.6.2",
         "slash": "^3.0.0"
       }
     },
     "@jest/core": {
-      "version": "25.5.4",
-      "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.5.4.tgz",
-      "integrity": "sha512-3uSo7laYxF00Dg/DMgbn4xMJKmDdWvZnf89n8Xj/5/AeQ2dOQmn6b6Hkj/MleyzZWXpwv+WSdYWl4cLsy2JsoA==",
+      "version": "26.6.3",
+      "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz",
+      "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==",
       "dev": true,
       "requires": {
-        "@jest/console": "^25.5.0",
-        "@jest/reporters": "^25.5.1",
-        "@jest/test-result": "^25.5.0",
-        "@jest/transform": "^25.5.1",
-        "@jest/types": "^25.5.0",
+        "@jest/console": "^26.6.2",
+        "@jest/reporters": "^26.6.2",
+        "@jest/test-result": "^26.6.2",
+        "@jest/transform": "^26.6.2",
+        "@jest/types": "^26.6.2",
+        "@types/node": "*",
         "ansi-escapes": "^4.2.1",
-        "chalk": "^3.0.0",
+        "chalk": "^4.0.0",
         "exit": "^0.1.2",
         "graceful-fs": "^4.2.4",
-        "jest-changed-files": "^25.5.0",
-        "jest-config": "^25.5.4",
-        "jest-haste-map": "^25.5.1",
-        "jest-message-util": "^25.5.0",
-        "jest-regex-util": "^25.2.6",
-        "jest-resolve": "^25.5.1",
-        "jest-resolve-dependencies": "^25.5.4",
-        "jest-runner": "^25.5.4",
-        "jest-runtime": "^25.5.4",
-        "jest-snapshot": "^25.5.1",
-        "jest-util": "^25.5.0",
-        "jest-validate": "^25.5.0",
-        "jest-watcher": "^25.5.0",
+        "jest-changed-files": "^26.6.2",
+        "jest-config": "^26.6.3",
+        "jest-haste-map": "^26.6.2",
+        "jest-message-util": "^26.6.2",
+        "jest-regex-util": "^26.0.0",
+        "jest-resolve": "^26.6.2",
+        "jest-resolve-dependencies": "^26.6.3",
+        "jest-runner": "^26.6.3",
+        "jest-runtime": "^26.6.3",
+        "jest-snapshot": "^26.6.2",
+        "jest-util": "^26.6.2",
+        "jest-validate": "^26.6.2",
+        "jest-watcher": "^26.6.2",
         "micromatch": "^4.0.2",
         "p-each-series": "^2.1.0",
-        "realpath-native": "^2.0.0",
         "rimraf": "^3.0.0",
         "slash": "^3.0.0",
         "strip-ansi": "^6.0.0"
       }
     },
     "@jest/environment": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz",
-      "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz",
+      "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==",
       "dev": true,
       "requires": {
-        "@jest/fake-timers": "^25.5.0",
-        "@jest/types": "^25.5.0",
-        "jest-mock": "^25.5.0"
+        "@jest/fake-timers": "^26.6.2",
+        "@jest/types": "^26.6.2",
+        "@types/node": "*",
+        "jest-mock": "^26.6.2"
       }
     },
     "@jest/fake-timers": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz",
-      "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz",
+      "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
-        "jest-message-util": "^25.5.0",
-        "jest-mock": "^25.5.0",
-        "jest-util": "^25.5.0",
-        "lolex": "^5.0.0"
+        "@jest/types": "^26.6.2",
+        "@sinonjs/fake-timers": "^6.0.1",
+        "@types/node": "*",
+        "jest-message-util": "^26.6.2",
+        "jest-mock": "^26.6.2",
+        "jest-util": "^26.6.2"
       }
     },
     "@jest/globals": {
-      "version": "25.5.2",
-      "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-25.5.2.tgz",
-      "integrity": "sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz",
+      "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==",
       "dev": true,
       "requires": {
-        "@jest/environment": "^25.5.0",
-        "@jest/types": "^25.5.0",
-        "expect": "^25.5.0"
+        "@jest/environment": "^26.6.2",
+        "@jest/types": "^26.6.2",
+        "expect": "^26.6.2"
       }
     },
     "@jest/reporters": {
-      "version": "25.5.1",
-      "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.5.1.tgz",
-      "integrity": "sha512-3jbd8pPDTuhYJ7vqiHXbSwTJQNavczPs+f1kRprRDxETeE3u6srJ+f0NPuwvOmk+lmunZzPkYWIFZDLHQPkviw==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz",
+      "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==",
       "dev": true,
       "requires": {
         "@bcoe/v8-coverage": "^0.2.3",
-        "@jest/console": "^25.5.0",
-        "@jest/test-result": "^25.5.0",
-        "@jest/transform": "^25.5.1",
-        "@jest/types": "^25.5.0",
-        "chalk": "^3.0.0",
+        "@jest/console": "^26.6.2",
+        "@jest/test-result": "^26.6.2",
+        "@jest/transform": "^26.6.2",
+        "@jest/types": "^26.6.2",
+        "chalk": "^4.0.0",
         "collect-v8-coverage": "^1.0.0",
         "exit": "^0.1.2",
         "glob": "^7.1.2",
         "graceful-fs": "^4.2.4",
         "istanbul-lib-coverage": "^3.0.0",
-        "istanbul-lib-instrument": "^4.0.0",
+        "istanbul-lib-instrument": "^4.0.3",
         "istanbul-lib-report": "^3.0.0",
         "istanbul-lib-source-maps": "^4.0.0",
         "istanbul-reports": "^3.0.2",
-        "jest-haste-map": "^25.5.1",
-        "jest-resolve": "^25.5.1",
-        "jest-util": "^25.5.0",
-        "jest-worker": "^25.5.0",
-        "node-notifier": "^6.0.0",
+        "jest-haste-map": "^26.6.2",
+        "jest-resolve": "^26.6.2",
+        "jest-util": "^26.6.2",
+        "jest-worker": "^26.6.2",
+        "node-notifier": "^8.0.0",
         "slash": "^3.0.0",
         "source-map": "^0.6.0",
-        "string-length": "^3.1.0",
+        "string-length": "^4.0.1",
         "terminal-link": "^2.0.0",
-        "v8-to-istanbul": "^4.1.3"
+        "v8-to-istanbul": "^7.0.0"
       }
     },
     "@jest/source-map": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz",
-      "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz",
+      "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==",
       "dev": true,
       "requires": {
         "callsites": "^3.0.0",
@@ -557,64 +600,64 @@
       }
     },
     "@jest/test-result": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz",
-      "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz",
+      "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==",
       "dev": true,
       "requires": {
-        "@jest/console": "^25.5.0",
-        "@jest/types": "^25.5.0",
+        "@jest/console": "^26.6.2",
+        "@jest/types": "^26.6.2",
         "@types/istanbul-lib-coverage": "^2.0.0",
         "collect-v8-coverage": "^1.0.0"
       }
     },
     "@jest/test-sequencer": {
-      "version": "25.5.4",
-      "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.5.4.tgz",
-      "integrity": "sha512-pTJGEkSeg1EkCO2YWq6hbFvKNXk8ejqlxiOg1jBNLnWrgXOkdY6UmqZpwGFXNnRt9B8nO1uWMzLLZ4eCmhkPNA==",
+      "version": "26.6.3",
+      "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz",
+      "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==",
       "dev": true,
       "requires": {
-        "@jest/test-result": "^25.5.0",
+        "@jest/test-result": "^26.6.2",
         "graceful-fs": "^4.2.4",
-        "jest-haste-map": "^25.5.1",
-        "jest-runner": "^25.5.4",
-        "jest-runtime": "^25.5.4"
+        "jest-haste-map": "^26.6.2",
+        "jest-runner": "^26.6.3",
+        "jest-runtime": "^26.6.3"
       }
     },
     "@jest/transform": {
-      "version": "25.5.1",
-      "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz",
-      "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz",
+      "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==",
       "dev": true,
       "requires": {
         "@babel/core": "^7.1.0",
-        "@jest/types": "^25.5.0",
+        "@jest/types": "^26.6.2",
         "babel-plugin-istanbul": "^6.0.0",
-        "chalk": "^3.0.0",
+        "chalk": "^4.0.0",
         "convert-source-map": "^1.4.0",
         "fast-json-stable-stringify": "^2.0.0",
         "graceful-fs": "^4.2.4",
-        "jest-haste-map": "^25.5.1",
-        "jest-regex-util": "^25.2.6",
-        "jest-util": "^25.5.0",
+        "jest-haste-map": "^26.6.2",
+        "jest-regex-util": "^26.0.0",
+        "jest-util": "^26.6.2",
         "micromatch": "^4.0.2",
         "pirates": "^4.0.1",
-        "realpath-native": "^2.0.0",
         "slash": "^3.0.0",
         "source-map": "^0.6.1",
         "write-file-atomic": "^3.0.0"
       }
     },
     "@jest/types": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz",
-      "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
+      "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==",
       "dev": true,
       "requires": {
         "@types/istanbul-lib-coverage": "^2.0.0",
-        "@types/istanbul-reports": "^1.1.1",
+        "@types/istanbul-reports": "^3.0.0",
+        "@types/node": "*",
         "@types/yargs": "^15.0.0",
-        "chalk": "^3.0.0"
+        "chalk": "^4.0.0"
       }
     },
     "@protobufjs/aspromise": {
@@ -713,23 +756,38 @@
       }
     },
     "@sinonjs/commons": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz",
-      "integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==",
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
+      "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==",
       "dev": true,
       "requires": {
         "type-detect": "4.0.8"
       }
     },
+    "@sinonjs/fake-timers": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz",
+      "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.7.0"
+      }
+    },
+    "@tootallnate/once": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
+      "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
+      "dev": true
+    },
     "@tsundoku/micromodal_types": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/@tsundoku/micromodal_types/-/micromodal_types-0.0.1.tgz",
       "integrity": "sha512-9k95tyHczZp/Uwu7SysnekpA2/o/y5gb/jMwqoLuTlJqwIVwnxfpsfmxc/bMfHnct7ESSqmRUJ1qYnUPD9Z7og=="
     },
     "@types/babel__core": {
-      "version": "7.1.12",
-      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz",
-      "integrity": "sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ==",
+      "version": "7.1.14",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz",
+      "integrity": "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==",
       "dev": true,
       "requires": {
         "@babel/parser": "^7.1.0",
@@ -759,9 +817,9 @@
       }
     },
     "@types/babel__traverse": {
-      "version": "7.11.0",
-      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.0.tgz",
-      "integrity": "sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg==",
+      "version": "7.11.1",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.1.tgz",
+      "integrity": "sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==",
       "dev": true,
       "requires": {
         "@babel/types": "^7.3.0"
@@ -808,9 +866,9 @@
       "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM="
     },
     "@types/graceful-fs": {
-      "version": "4.1.4",
-      "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz",
-      "integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==",
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
+      "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==",
       "dev": true,
       "requires": {
         "@types/node": "*"
@@ -832,20 +890,23 @@
       }
     },
     "@types/istanbul-reports": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz",
-      "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
+      "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
       "dev": true,
       "requires": {
-        "@types/istanbul-lib-coverage": "*",
         "@types/istanbul-lib-report": "*"
       }
     },
     "@types/jest": {
-      "version": "22.2.3",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-22.2.3.tgz",
-      "integrity": "sha512-e74sM9W/4qqWB6D4TWV9FQk0WoHtX1X4FJpbjxucMSVJHtFjbQOH3H6yp+xno4br0AKG0wz/kPtaN599GUOvAg==",
-      "dev": true
+      "version": "26.0.23",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
+      "integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
+      "dev": true,
+      "requires": {
+        "jest-diff": "^26.0.0",
+        "pretty-format": "^26.0.0"
+      }
     },
     "@types/long": {
       "version": "4.0.1",
@@ -874,15 +935,15 @@
       "integrity": "sha512-GdZbRSJ3Cv5fiwT6I0SQ3ckeN2PWNqxd26W9Z2fCK1tGrrasGy4puvNFtnddqH9UJFMQYXxEuuB7B8UK+LLwSg=="
     },
     "@types/prettier": {
-      "version": "1.19.1",
-      "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz",
-      "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==",
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.2.3.tgz",
+      "integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==",
       "dev": true
     },
     "@types/puppeteer": {
-      "version": "1.20.7",
-      "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-1.20.7.tgz",
-      "integrity": "sha512-LCfP/Zf/y4I/hG8ARR8htPYa1wpLpUkysJo9TffmQssVz8c1b9uDNU4benDHSldiz7HVAMek1DCWz7KbqEUg3w==",
+      "version": "5.4.3",
+      "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.3.tgz",
+      "integrity": "sha512-3nE8YgR9DIsgttLW+eJf6mnXxq8Ge+27m5SU3knWmrlfl6+KOG0Bf9f7Ua7K+C4BnaTMAh3/UpySqdAYvrsvjg==",
       "dev": true,
       "requires": {
         "@types/node": "*"
@@ -898,9 +959,9 @@
       }
     },
     "@types/stack-utils": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
-      "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
+      "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==",
       "dev": true
     },
     "@types/uuid": {
@@ -928,6 +989,16 @@
       "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==",
       "dev": true
     },
+    "@types/yauzl": {
+      "version": "2.9.1",
+      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz",
+      "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "abab": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
@@ -941,42 +1012,42 @@
       "dev": true
     },
     "acorn": {
-      "version": "7.4.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
-      "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.3.0.tgz",
+      "integrity": "sha512-tqPKHZ5CaBJw0Xmy0ZZvLs1qTV+BNFSyvn77ASXkpBNfIRk8ev26fKrD9iLGwGA9zedPao52GSHzq8lyZG0NUw==",
       "dev": true
     },
     "acorn-globals": {
-      "version": "4.3.4",
-      "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz",
-      "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==",
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
+      "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
       "dev": true,
       "requires": {
-        "acorn": "^6.0.1",
-        "acorn-walk": "^6.0.1"
+        "acorn": "^7.1.1",
+        "acorn-walk": "^7.1.1"
       },
       "dependencies": {
         "acorn": {
-          "version": "6.4.2",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
-          "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==",
+          "version": "7.4.1",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+          "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
           "dev": true
         }
       }
     },
     "acorn-walk": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
-      "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+      "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
       "dev": true
     },
     "agent-base": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
-      "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
       "dev": true,
       "requires": {
-        "es6-promisify": "^5.0.0"
+        "debug": "4"
       }
     },
     "ajv": {
@@ -998,18 +1069,18 @@
       "dev": true
     },
     "ansi-escapes": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
-      "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+      "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
       "dev": true,
       "requires": {
-        "type-fest": "^0.11.0"
+        "type-fest": "^0.21.3"
       },
       "dependencies": {
         "type-fest": {
-          "version": "0.11.0",
-          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
-          "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
+          "version": "0.21.3",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+          "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
           "dev": true
         }
       }
@@ -1030,9 +1101,9 @@
       }
     },
     "anymatch": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
-      "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+      "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
       "dev": true,
       "requires": {
         "normalize-path": "^3.0.0",
@@ -1082,12 +1153,6 @@
       "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
       "dev": true
     },
-    "array-equal": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz",
-      "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=",
-      "dev": true
-    },
     "array-filter": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz",
@@ -1126,24 +1191,12 @@
       "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
       "dev": true
     },
-    "astral-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
-      "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
-      "dev": true
-    },
     "async-foreach": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
       "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
       "dev": true
     },
-    "async-limiter": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
-      "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
-      "dev": true
-    },
     "asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1177,17 +1230,17 @@
       "dev": true
     },
     "babel-jest": {
-      "version": "25.5.1",
-      "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz",
-      "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==",
+      "version": "26.6.3",
+      "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz",
+      "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==",
       "dev": true,
       "requires": {
-        "@jest/transform": "^25.5.1",
-        "@jest/types": "^25.5.0",
+        "@jest/transform": "^26.6.2",
+        "@jest/types": "^26.6.2",
         "@types/babel__core": "^7.1.7",
         "babel-plugin-istanbul": "^6.0.0",
-        "babel-preset-jest": "^25.5.0",
-        "chalk": "^3.0.0",
+        "babel-preset-jest": "^26.6.2",
+        "chalk": "^4.0.0",
         "graceful-fs": "^4.2.4",
         "slash": "^3.0.0"
       }
@@ -1206,20 +1259,21 @@
       }
     },
     "babel-plugin-jest-hoist": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz",
-      "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz",
+      "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==",
       "dev": true,
       "requires": {
         "@babel/template": "^7.3.3",
         "@babel/types": "^7.3.3",
+        "@types/babel__core": "^7.0.0",
         "@types/babel__traverse": "^7.0.6"
       }
     },
     "babel-preset-current-node-syntax": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.4.tgz",
-      "integrity": "sha512-5/INNCYhUGqw7VbVjT/hb3ucjgkVHKXY7lX3ZjlN4gm565VyFmJUrJ/h+h16ECVB38R/9SF6aACydpKMLZ/c9w==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
+      "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==",
       "dev": true,
       "requires": {
         "@babel/plugin-syntax-async-generators": "^7.8.4",
@@ -1232,17 +1286,18 @@
         "@babel/plugin-syntax-numeric-separator": "^7.8.3",
         "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
         "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
-        "@babel/plugin-syntax-optional-chaining": "^7.8.3"
+        "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+        "@babel/plugin-syntax-top-level-await": "^7.8.3"
       }
     },
     "babel-preset-jest": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz",
-      "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz",
+      "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==",
       "dev": true,
       "requires": {
-        "babel-plugin-jest-hoist": "^25.5.0",
-        "babel-preset-current-node-syntax": "^0.1.2"
+        "babel-plugin-jest-hoist": "^26.6.2",
+        "babel-preset-current-node-syntax": "^1.0.0"
       }
     },
     "balanced-match": {
@@ -1306,6 +1361,12 @@
         }
       }
     },
+    "base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "dev": true
+    },
     "bcrypt-pbkdf": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -1315,6 +1376,30 @@
         "tweetnacl": "^0.14.3"
       }
     },
+    "bl": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+      "dev": true,
+      "requires": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "dev": true,
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
     "block-stream": {
       "version": "0.0.9",
       "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
@@ -1349,21 +1434,17 @@
       "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
       "dev": true
     },
-    "browser-resolve": {
-      "version": "1.11.3",
-      "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz",
-      "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==",
+    "browserslist": {
+      "version": "4.16.6",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
+      "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
       "dev": true,
       "requires": {
-        "resolve": "1.1.7"
-      },
-      "dependencies": {
-        "resolve": {
-          "version": "1.1.7",
-          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
-          "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
-          "dev": true
-        }
+        "caniuse-lite": "^1.0.30001219",
+        "colorette": "^1.2.2",
+        "electron-to-chromium": "^1.3.723",
+        "escalade": "^3.1.1",
+        "node-releases": "^1.1.71"
       }
     },
     "bser": {
@@ -1375,6 +1456,16 @@
         "node-int64": "^0.4.0"
       }
     },
+    "buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "dev": true,
+      "requires": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
     "buffer-crc32": {
       "version": "0.2.13",
       "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -1449,6 +1540,12 @@
         }
       }
     },
+    "caniuse-lite": {
+      "version": "1.0.30001233",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001233.tgz",
+      "integrity": "sha512-BmkbxLfStqiPA7IEzQpIk0UFZFf3A4E6fzjPJ6OR+bFC2L8ES9J8zGA/asoi47p8XDVkev+WJo2I2Nc8c/34Yg==",
+      "dev": true
+    },
     "capture-exit": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
@@ -1465,21 +1562,39 @@
       "dev": true
     },
     "chalk": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
-      "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
+      "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
       "dev": true,
       "requires": {
         "ansi-styles": "^4.1.0",
         "supports-color": "^7.1.0"
       }
     },
+    "char-regex": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+      "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+      "dev": true
+    },
+    "chownr": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+      "dev": true
+    },
     "ci-info": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
       "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
       "dev": true
     },
+    "cjs-module-lexer": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz",
+      "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==",
+      "dev": true
+    },
     "class-utils": {
       "version": "0.3.6",
       "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@@ -1555,6 +1670,12 @@
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
     },
+    "colorette": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
+      "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
+      "dev": true
+    },
     "combined-stream": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1588,18 +1709,6 @@
       "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
       "dev": true
     },
-    "concat-stream": {
-      "version": "1.6.2",
-      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
-      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
-      "dev": true,
-      "requires": {
-        "buffer-from": "^1.0.0",
-        "inherits": "^2.0.3",
-        "readable-stream": "^2.2.2",
-        "typedarray": "^0.0.6"
-      }
-    },
     "console-control-strings": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@@ -1653,15 +1762,6 @@
           "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
           "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
           "dev": true
-        },
-        "which": {
-          "version": "1.3.1",
-          "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-          "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-          "dev": true,
-          "requires": {
-            "isexe": "^2.0.0"
-          }
         }
       }
     },
@@ -1710,14 +1810,14 @@
       }
     },
     "data-urls": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz",
-      "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
+      "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==",
       "dev": true,
       "requires": {
-        "abab": "^2.0.0",
-        "whatwg-mimetype": "^2.2.0",
-        "whatwg-url": "^7.0.0"
+        "abab": "^2.0.3",
+        "whatwg-mimetype": "^2.3.0",
+        "whatwg-url": "^8.0.0"
       }
     },
     "debug": {
@@ -1735,6 +1835,12 @@
       "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
       "dev": true
     },
+    "decimal.js": {
+      "version": "10.2.1",
+      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz",
+      "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==",
+      "dev": true
+    },
     "decode-uri-component": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@@ -1827,9 +1933,9 @@
       "dev": true
     },
     "devtools-protocol": {
-      "version": "0.0.681549",
-      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.681549.tgz",
-      "integrity": "sha512-YVwTu4T4zzgf88Y8+t9lIDT+qAn2YDcig4zRgqq5+4bFACn8WDzbqAhct5zVUhefRMOwLGPXyLFTim1FV7keVg=="
+      "version": "0.0.847576",
+      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.847576.tgz",
+      "integrity": "sha512-0M8kobnSQE0Jmly7Mhbeq0W/PpZfnuK+WjN2ZRVPbGqYwCHCioAVp84H0TcLimgECcN5H976y5QiXMGBC9JKmg=="
     },
     "diff": {
       "version": "4.0.2",
@@ -1838,9 +1944,9 @@
       "dev": true
     },
     "diff-sequences": {
-      "version": "25.2.6",
-      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz",
-      "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz",
+      "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==",
       "dev": true
     },
     "dingusjs": {
@@ -1850,12 +1956,20 @@
       "dev": true
     },
     "domexception": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
-      "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
+      "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
       "dev": true,
       "requires": {
-        "webidl-conversions": "^4.0.2"
+        "webidl-conversions": "^5.0.0"
+      },
+      "dependencies": {
+        "webidl-conversions": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
+          "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
+          "dev": true
+        }
       }
     },
     "ecc-jsbn": {
@@ -1868,6 +1982,18 @@
         "safer-buffer": "^2.1.0"
       }
     },
+    "electron-to-chromium": {
+      "version": "1.3.747",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.747.tgz",
+      "integrity": "sha512-+K1vnBc08GNYxCWwdRe9o3Ml30DhsNyK/qIl/NE1Dic+qCy9ZREcqGNiV4jiLiAdALK1DUG3pakJHGkJUd9QQw==",
+      "dev": true
+    },
+    "emittery": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz",
+      "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==",
+      "dev": true
+    },
     "emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -1923,21 +2049,12 @@
         "is-symbol": "^1.0.2"
       }
     },
-    "es6-promise": {
-      "version": "4.2.8",
-      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
-      "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+    "escalade": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
       "dev": true
     },
-    "es6-promisify": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
-      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
-      "dev": true,
-      "requires": {
-        "es6-promise": "^4.0.3"
-      }
-    },
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -1945,13 +2062,13 @@
       "dev": true
     },
     "escodegen": {
-      "version": "1.14.3",
-      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
-      "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
+      "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
       "dev": true,
       "requires": {
         "esprima": "^4.0.1",
-        "estraverse": "^4.2.0",
+        "estraverse": "^5.2.0",
         "esutils": "^2.0.2",
         "optionator": "^0.8.1",
         "source-map": "~0.6.1"
@@ -1964,9 +2081,9 @@
       "dev": true
     },
     "estraverse": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
-      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+      "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
       "dev": true
     },
     "estree-walker": {
@@ -1987,9 +2104,9 @@
       "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg=="
     },
     "exec-sh": {
-      "version": "0.3.4",
-      "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",
-      "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==",
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz",
+      "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==",
       "dev": true
     },
     "execa": {
@@ -2064,17 +2181,17 @@
       }
     },
     "expect": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz",
-      "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz",
+      "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
+        "@jest/types": "^26.6.2",
         "ansi-styles": "^4.0.0",
-        "jest-get-type": "^25.2.6",
-        "jest-matcher-utils": "^25.5.0",
-        "jest-message-util": "^25.5.0",
-        "jest-regex-util": "^25.2.6"
+        "jest-get-type": "^26.3.0",
+        "jest-matcher-utils": "^26.6.2",
+        "jest-message-util": "^26.6.2",
+        "jest-regex-util": "^26.0.0"
       }
     },
     "extend": {
@@ -2170,31 +2287,25 @@
       }
     },
     "extract-zip": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
-      "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+      "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
       "dev": true,
       "requires": {
-        "concat-stream": "^1.6.2",
-        "debug": "^2.6.9",
-        "mkdirp": "^0.5.4",
+        "@types/yauzl": "^2.9.1",
+        "debug": "^4.1.1",
+        "get-stream": "^5.1.0",
         "yauzl": "^2.10.0"
       },
       "dependencies": {
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+        "get-stream": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+          "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
           "dev": true,
           "requires": {
-            "ms": "2.0.0"
+            "pump": "^3.0.0"
           }
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
-          "dev": true
         }
       }
     },
@@ -2296,6 +2407,12 @@
         "map-cache": "^0.2.2"
       }
     },
+    "fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+      "dev": true
+    },
     "fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2612,18 +2729,23 @@
       }
     },
     "hosted-git-info": {
-      "version": "2.8.8",
-      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
-      "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+      "version": "2.8.9",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
       "dev": true
     },
+    "hsluv": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/hsluv/-/hsluv-0.1.0.tgz",
+      "integrity": "sha512-ERcanKLAszD2XN3Vh5r5Szkrv9q0oSTudmP0rkiKAGM/3NMc9FLmMZBB7TSqTaXJfSDBOreYTfjezCOYbRKqlw=="
+    },
     "html-encoding-sniffer": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz",
-      "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
+      "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==",
       "dev": true,
       "requires": {
-        "whatwg-encoding": "^1.0.1"
+        "whatwg-encoding": "^1.0.5"
       }
     },
     "html-escaper": {
@@ -2632,6 +2754,17 @@
       "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
       "dev": true
     },
+    "http-proxy-agent": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
+      "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
+      "dev": true,
+      "requires": {
+        "@tootallnate/once": "1",
+        "agent-base": "6",
+        "debug": "4"
+      }
+    },
     "http-signature": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -2644,24 +2777,13 @@
       }
     },
     "https-proxy-agent": {
-      "version": "2.2.4",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
-      "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
+      "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
       "dev": true,
       "requires": {
-        "agent-base": "^4.3.0",
-        "debug": "^3.1.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "3.2.7",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
-          "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
-          "dev": true,
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        }
+        "agent-base": "6",
+        "debug": "4"
       }
     },
     "human-signals": {
@@ -2679,6 +2801,12 @@
         "safer-buffer": ">= 2.1.2 < 3"
       }
     },
+    "ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "dev": true
+    },
     "immer": {
       "version": "1.12.1",
       "resolved": "https://registry.npmjs.org/immer/-/immer-1.12.1.tgz",
@@ -2730,12 +2858,6 @@
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
-    "ip-regex": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
-      "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
-      "dev": true
-    },
     "is-accessor-descriptor": {
       "version": "0.1.6",
       "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
@@ -2844,9 +2966,9 @@
       }
     },
     "is-docker": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz",
-      "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==",
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+      "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
       "dev": true,
       "optional": true
     },
@@ -2905,6 +3027,12 @@
         "isobject": "^3.0.1"
       }
     },
+    "is-potential-custom-element-name": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+      "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+      "dev": true
+    },
     "is-reference": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@@ -3052,48 +3180,47 @@
       }
     },
     "jest": {
-      "version": "25.5.4",
-      "resolved": "https://registry.npmjs.org/jest/-/jest-25.5.4.tgz",
-      "integrity": "sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ==",
+      "version": "26.6.3",
+      "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz",
+      "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==",
       "dev": true,
       "requires": {
-        "@jest/core": "^25.5.4",
+        "@jest/core": "^26.6.3",
         "import-local": "^3.0.2",
-        "jest-cli": "^25.5.4"
+        "jest-cli": "^26.6.3"
       },
       "dependencies": {
         "jest-cli": {
-          "version": "25.5.4",
-          "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.5.4.tgz",
-          "integrity": "sha512-rG8uJkIiOUpnREh1768/N3n27Cm+xPFkSNFO91tgg+8o2rXeVLStz+vkXkGr4UtzH6t1SNbjwoiswd7p4AhHTw==",
+          "version": "26.6.3",
+          "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz",
+          "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==",
           "dev": true,
           "requires": {
-            "@jest/core": "^25.5.4",
-            "@jest/test-result": "^25.5.0",
-            "@jest/types": "^25.5.0",
-            "chalk": "^3.0.0",
+            "@jest/core": "^26.6.3",
+            "@jest/test-result": "^26.6.2",
+            "@jest/types": "^26.6.2",
+            "chalk": "^4.0.0",
             "exit": "^0.1.2",
             "graceful-fs": "^4.2.4",
             "import-local": "^3.0.2",
             "is-ci": "^2.0.0",
-            "jest-config": "^25.5.4",
-            "jest-util": "^25.5.0",
-            "jest-validate": "^25.5.0",
+            "jest-config": "^26.6.3",
+            "jest-util": "^26.6.2",
+            "jest-validate": "^26.6.2",
             "prompts": "^2.0.1",
-            "realpath-native": "^2.0.0",
-            "yargs": "^15.3.1"
+            "yargs": "^15.4.1"
           }
         }
       }
     },
     "jest-changed-files": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.5.0.tgz",
-      "integrity": "sha512-EOw9QEqapsDT7mKF162m8HFzRPbmP8qJQny6ldVOdOVBz3ACgPm/1nAn5fPQ/NDaYhX/AHkrGwwkCncpAVSXcw==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz",
+      "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
-        "execa": "^3.2.0",
+        "@jest/types": "^26.6.2",
+        "execa": "^4.0.0",
         "throat": "^5.0.0"
       },
       "dependencies": {
@@ -3109,9 +3236,9 @@
           }
         },
         "execa": {
-          "version": "3.4.0",
-          "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz",
-          "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==",
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
+          "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
           "dev": true,
           "requires": {
             "cross-spawn": "^7.0.0",
@@ -3121,7 +3248,6 @@
             "merge-stream": "^2.0.0",
             "npm-run-path": "^4.0.0",
             "onetime": "^5.1.0",
-            "p-finally": "^2.0.0",
             "signal-exit": "^3.0.2",
             "strip-final-newline": "^2.0.0"
           }
@@ -3150,12 +3276,6 @@
             "path-key": "^3.0.0"
           }
         },
-        "p-finally": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
-          "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==",
-          "dev": true
-        },
         "path-key": {
           "version": "3.1.1",
           "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -3176,195 +3296,208 @@
           "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
           "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
           "dev": true
+        },
+        "which": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+          "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+          "dev": true,
+          "requires": {
+            "isexe": "^2.0.0"
+          }
         }
       }
     },
     "jest-config": {
-      "version": "25.5.4",
-      "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz",
-      "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==",
+      "version": "26.6.3",
+      "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz",
+      "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==",
       "dev": true,
       "requires": {
         "@babel/core": "^7.1.0",
-        "@jest/test-sequencer": "^25.5.4",
-        "@jest/types": "^25.5.0",
-        "babel-jest": "^25.5.1",
-        "chalk": "^3.0.0",
+        "@jest/test-sequencer": "^26.6.3",
+        "@jest/types": "^26.6.2",
+        "babel-jest": "^26.6.3",
+        "chalk": "^4.0.0",
         "deepmerge": "^4.2.2",
         "glob": "^7.1.1",
         "graceful-fs": "^4.2.4",
-        "jest-environment-jsdom": "^25.5.0",
-        "jest-environment-node": "^25.5.0",
-        "jest-get-type": "^25.2.6",
-        "jest-jasmine2": "^25.5.4",
-        "jest-regex-util": "^25.2.6",
-        "jest-resolve": "^25.5.1",
-        "jest-util": "^25.5.0",
-        "jest-validate": "^25.5.0",
+        "jest-environment-jsdom": "^26.6.2",
+        "jest-environment-node": "^26.6.2",
+        "jest-get-type": "^26.3.0",
+        "jest-jasmine2": "^26.6.3",
+        "jest-regex-util": "^26.0.0",
+        "jest-resolve": "^26.6.2",
+        "jest-util": "^26.6.2",
+        "jest-validate": "^26.6.2",
         "micromatch": "^4.0.2",
-        "pretty-format": "^25.5.0",
-        "realpath-native": "^2.0.0"
+        "pretty-format": "^26.6.2"
       }
     },
     "jest-diff": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz",
-      "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz",
+      "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==",
       "dev": true,
       "requires": {
-        "chalk": "^3.0.0",
-        "diff-sequences": "^25.2.6",
-        "jest-get-type": "^25.2.6",
-        "pretty-format": "^25.5.0"
+        "chalk": "^4.0.0",
+        "diff-sequences": "^26.6.2",
+        "jest-get-type": "^26.3.0",
+        "pretty-format": "^26.6.2"
       }
     },
     "jest-docblock": {
-      "version": "25.3.0",
-      "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz",
-      "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==",
+      "version": "26.0.0",
+      "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz",
+      "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==",
       "dev": true,
       "requires": {
         "detect-newline": "^3.0.0"
       }
     },
     "jest-each": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz",
-      "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz",
+      "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
-        "chalk": "^3.0.0",
-        "jest-get-type": "^25.2.6",
-        "jest-util": "^25.5.0",
-        "pretty-format": "^25.5.0"
+        "@jest/types": "^26.6.2",
+        "chalk": "^4.0.0",
+        "jest-get-type": "^26.3.0",
+        "jest-util": "^26.6.2",
+        "pretty-format": "^26.6.2"
       }
     },
     "jest-environment-jsdom": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz",
-      "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz",
+      "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==",
       "dev": true,
       "requires": {
-        "@jest/environment": "^25.5.0",
-        "@jest/fake-timers": "^25.5.0",
-        "@jest/types": "^25.5.0",
-        "jest-mock": "^25.5.0",
-        "jest-util": "^25.5.0",
-        "jsdom": "^15.2.1"
+        "@jest/environment": "^26.6.2",
+        "@jest/fake-timers": "^26.6.2",
+        "@jest/types": "^26.6.2",
+        "@types/node": "*",
+        "jest-mock": "^26.6.2",
+        "jest-util": "^26.6.2",
+        "jsdom": "^16.4.0"
       }
     },
     "jest-environment-node": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz",
-      "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz",
+      "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==",
       "dev": true,
       "requires": {
-        "@jest/environment": "^25.5.0",
-        "@jest/fake-timers": "^25.5.0",
-        "@jest/types": "^25.5.0",
-        "jest-mock": "^25.5.0",
-        "jest-util": "^25.5.0",
-        "semver": "^6.3.0"
+        "@jest/environment": "^26.6.2",
+        "@jest/fake-timers": "^26.6.2",
+        "@jest/types": "^26.6.2",
+        "@types/node": "*",
+        "jest-mock": "^26.6.2",
+        "jest-util": "^26.6.2"
       }
     },
     "jest-get-type": {
-      "version": "25.2.6",
-      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz",
-      "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==",
+      "version": "26.3.0",
+      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz",
+      "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==",
       "dev": true
     },
     "jest-haste-map": {
-      "version": "25.5.1",
-      "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz",
-      "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz",
+      "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
+        "@jest/types": "^26.6.2",
         "@types/graceful-fs": "^4.1.2",
+        "@types/node": "*",
         "anymatch": "^3.0.3",
         "fb-watchman": "^2.0.0",
         "fsevents": "^2.1.2",
         "graceful-fs": "^4.2.4",
-        "jest-serializer": "^25.5.0",
-        "jest-util": "^25.5.0",
-        "jest-worker": "^25.5.0",
+        "jest-regex-util": "^26.0.0",
+        "jest-serializer": "^26.6.2",
+        "jest-util": "^26.6.2",
+        "jest-worker": "^26.6.2",
         "micromatch": "^4.0.2",
         "sane": "^4.0.3",
-        "walker": "^1.0.7",
-        "which": "^2.0.2"
+        "walker": "^1.0.7"
       }
     },
     "jest-jasmine2": {
-      "version": "25.5.4",
-      "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz",
-      "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==",
+      "version": "26.6.3",
+      "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz",
+      "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==",
       "dev": true,
       "requires": {
         "@babel/traverse": "^7.1.0",
-        "@jest/environment": "^25.5.0",
-        "@jest/source-map": "^25.5.0",
-        "@jest/test-result": "^25.5.0",
-        "@jest/types": "^25.5.0",
-        "chalk": "^3.0.0",
+        "@jest/environment": "^26.6.2",
+        "@jest/source-map": "^26.6.2",
+        "@jest/test-result": "^26.6.2",
+        "@jest/types": "^26.6.2",
+        "@types/node": "*",
+        "chalk": "^4.0.0",
         "co": "^4.6.0",
-        "expect": "^25.5.0",
+        "expect": "^26.6.2",
         "is-generator-fn": "^2.0.0",
-        "jest-each": "^25.5.0",
-        "jest-matcher-utils": "^25.5.0",
-        "jest-message-util": "^25.5.0",
-        "jest-runtime": "^25.5.4",
-        "jest-snapshot": "^25.5.1",
-        "jest-util": "^25.5.0",
-        "pretty-format": "^25.5.0",
+        "jest-each": "^26.6.2",
+        "jest-matcher-utils": "^26.6.2",
+        "jest-message-util": "^26.6.2",
+        "jest-runtime": "^26.6.3",
+        "jest-snapshot": "^26.6.2",
+        "jest-util": "^26.6.2",
+        "pretty-format": "^26.6.2",
         "throat": "^5.0.0"
       }
     },
     "jest-leak-detector": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz",
-      "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz",
+      "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==",
       "dev": true,
       "requires": {
-        "jest-get-type": "^25.2.6",
-        "pretty-format": "^25.5.0"
+        "jest-get-type": "^26.3.0",
+        "pretty-format": "^26.6.2"
       }
     },
     "jest-matcher-utils": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz",
-      "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz",
+      "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==",
       "dev": true,
       "requires": {
-        "chalk": "^3.0.0",
-        "jest-diff": "^25.5.0",
-        "jest-get-type": "^25.2.6",
-        "pretty-format": "^25.5.0"
+        "chalk": "^4.0.0",
+        "jest-diff": "^26.6.2",
+        "jest-get-type": "^26.3.0",
+        "pretty-format": "^26.6.2"
       }
     },
     "jest-message-util": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz",
-      "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz",
+      "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
-        "@jest/types": "^25.5.0",
-        "@types/stack-utils": "^1.0.1",
-        "chalk": "^3.0.0",
+        "@jest/types": "^26.6.2",
+        "@types/stack-utils": "^2.0.0",
+        "chalk": "^4.0.0",
         "graceful-fs": "^4.2.4",
         "micromatch": "^4.0.2",
+        "pretty-format": "^26.6.2",
         "slash": "^3.0.0",
-        "stack-utils": "^1.0.1"
+        "stack-utils": "^2.0.2"
       }
     },
     "jest-mock": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz",
-      "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz",
+      "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0"
+        "@jest/types": "^26.6.2",
+        "@types/node": "*"
       }
     },
     "jest-pnp-resolver": {
@@ -3374,179 +3507,219 @@
       "dev": true
     },
     "jest-regex-util": {
-      "version": "25.2.6",
-      "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz",
-      "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==",
+      "version": "26.0.0",
+      "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz",
+      "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==",
       "dev": true
     },
     "jest-resolve": {
-      "version": "25.5.1",
-      "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz",
-      "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz",
+      "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
-        "browser-resolve": "^1.11.3",
-        "chalk": "^3.0.0",
+        "@jest/types": "^26.6.2",
+        "chalk": "^4.0.0",
         "graceful-fs": "^4.2.4",
-        "jest-pnp-resolver": "^1.2.1",
+        "jest-pnp-resolver": "^1.2.2",
+        "jest-util": "^26.6.2",
         "read-pkg-up": "^7.0.1",
-        "realpath-native": "^2.0.0",
-        "resolve": "^1.17.0",
+        "resolve": "^1.18.1",
         "slash": "^3.0.0"
       }
     },
     "jest-resolve-dependencies": {
-      "version": "25.5.4",
-      "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.5.4.tgz",
-      "integrity": "sha512-yFmbPd+DAQjJQg88HveObcGBA32nqNZ02fjYmtL16t1xw9bAttSn5UGRRhzMHIQbsep7znWvAvnD4kDqOFM0Uw==",
+      "version": "26.6.3",
+      "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz",
+      "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
-        "jest-regex-util": "^25.2.6",
-        "jest-snapshot": "^25.5.1"
+        "@jest/types": "^26.6.2",
+        "jest-regex-util": "^26.0.0",
+        "jest-snapshot": "^26.6.2"
       }
     },
     "jest-runner": {
-      "version": "25.5.4",
-      "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz",
-      "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==",
+      "version": "26.6.3",
+      "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz",
+      "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==",
       "dev": true,
       "requires": {
-        "@jest/console": "^25.5.0",
-        "@jest/environment": "^25.5.0",
-        "@jest/test-result": "^25.5.0",
-        "@jest/types": "^25.5.0",
-        "chalk": "^3.0.0",
+        "@jest/console": "^26.6.2",
+        "@jest/environment": "^26.6.2",
+        "@jest/test-result": "^26.6.2",
+        "@jest/types": "^26.6.2",
+        "@types/node": "*",
+        "chalk": "^4.0.0",
+        "emittery": "^0.7.1",
         "exit": "^0.1.2",
         "graceful-fs": "^4.2.4",
-        "jest-config": "^25.5.4",
-        "jest-docblock": "^25.3.0",
-        "jest-haste-map": "^25.5.1",
-        "jest-jasmine2": "^25.5.4",
-        "jest-leak-detector": "^25.5.0",
-        "jest-message-util": "^25.5.0",
-        "jest-resolve": "^25.5.1",
-        "jest-runtime": "^25.5.4",
-        "jest-util": "^25.5.0",
-        "jest-worker": "^25.5.0",
+        "jest-config": "^26.6.3",
+        "jest-docblock": "^26.0.0",
+        "jest-haste-map": "^26.6.2",
+        "jest-leak-detector": "^26.6.2",
+        "jest-message-util": "^26.6.2",
+        "jest-resolve": "^26.6.2",
+        "jest-runtime": "^26.6.3",
+        "jest-util": "^26.6.2",
+        "jest-worker": "^26.6.2",
         "source-map-support": "^0.5.6",
         "throat": "^5.0.0"
       }
     },
     "jest-runtime": {
-      "version": "25.5.4",
-      "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz",
-      "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==",
+      "version": "26.6.3",
+      "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz",
+      "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==",
       "dev": true,
       "requires": {
-        "@jest/console": "^25.5.0",
-        "@jest/environment": "^25.5.0",
-        "@jest/globals": "^25.5.2",
-        "@jest/source-map": "^25.5.0",
-        "@jest/test-result": "^25.5.0",
-        "@jest/transform": "^25.5.1",
-        "@jest/types": "^25.5.0",
+        "@jest/console": "^26.6.2",
+        "@jest/environment": "^26.6.2",
+        "@jest/fake-timers": "^26.6.2",
+        "@jest/globals": "^26.6.2",
+        "@jest/source-map": "^26.6.2",
+        "@jest/test-result": "^26.6.2",
+        "@jest/transform": "^26.6.2",
+        "@jest/types": "^26.6.2",
         "@types/yargs": "^15.0.0",
-        "chalk": "^3.0.0",
+        "chalk": "^4.0.0",
+        "cjs-module-lexer": "^0.6.0",
         "collect-v8-coverage": "^1.0.0",
         "exit": "^0.1.2",
         "glob": "^7.1.3",
         "graceful-fs": "^4.2.4",
-        "jest-config": "^25.5.4",
-        "jest-haste-map": "^25.5.1",
-        "jest-message-util": "^25.5.0",
-        "jest-mock": "^25.5.0",
-        "jest-regex-util": "^25.2.6",
-        "jest-resolve": "^25.5.1",
-        "jest-snapshot": "^25.5.1",
-        "jest-util": "^25.5.0",
-        "jest-validate": "^25.5.0",
-        "realpath-native": "^2.0.0",
+        "jest-config": "^26.6.3",
+        "jest-haste-map": "^26.6.2",
+        "jest-message-util": "^26.6.2",
+        "jest-mock": "^26.6.2",
+        "jest-regex-util": "^26.0.0",
+        "jest-resolve": "^26.6.2",
+        "jest-snapshot": "^26.6.2",
+        "jest-util": "^26.6.2",
+        "jest-validate": "^26.6.2",
         "slash": "^3.0.0",
         "strip-bom": "^4.0.0",
-        "yargs": "^15.3.1"
+        "yargs": "^15.4.1"
       }
     },
     "jest-serializer": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz",
-      "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz",
+      "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==",
       "dev": true,
       "requires": {
+        "@types/node": "*",
         "graceful-fs": "^4.2.4"
       }
     },
     "jest-snapshot": {
-      "version": "25.5.1",
-      "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz",
-      "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz",
+      "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==",
       "dev": true,
       "requires": {
         "@babel/types": "^7.0.0",
-        "@jest/types": "^25.5.0",
-        "@types/prettier": "^1.19.0",
-        "chalk": "^3.0.0",
-        "expect": "^25.5.0",
+        "@jest/types": "^26.6.2",
+        "@types/babel__traverse": "^7.0.4",
+        "@types/prettier": "^2.0.0",
+        "chalk": "^4.0.0",
+        "expect": "^26.6.2",
         "graceful-fs": "^4.2.4",
-        "jest-diff": "^25.5.0",
-        "jest-get-type": "^25.2.6",
-        "jest-matcher-utils": "^25.5.0",
-        "jest-message-util": "^25.5.0",
-        "jest-resolve": "^25.5.1",
-        "make-dir": "^3.0.0",
+        "jest-diff": "^26.6.2",
+        "jest-get-type": "^26.3.0",
+        "jest-haste-map": "^26.6.2",
+        "jest-matcher-utils": "^26.6.2",
+        "jest-message-util": "^26.6.2",
+        "jest-resolve": "^26.6.2",
         "natural-compare": "^1.4.0",
-        "pretty-format": "^25.5.0",
-        "semver": "^6.3.0"
+        "pretty-format": "^26.6.2",
+        "semver": "^7.3.2"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "semver": {
+          "version": "7.3.5",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+          "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+          "dev": true
+        }
       }
     },
     "jest-util": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz",
-      "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz",
+      "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
-        "chalk": "^3.0.0",
+        "@jest/types": "^26.6.2",
+        "@types/node": "*",
+        "chalk": "^4.0.0",
         "graceful-fs": "^4.2.4",
         "is-ci": "^2.0.0",
-        "make-dir": "^3.0.0"
+        "micromatch": "^4.0.2"
       }
     },
     "jest-validate": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz",
-      "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz",
+      "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
-        "camelcase": "^5.3.1",
-        "chalk": "^3.0.0",
-        "jest-get-type": "^25.2.6",
+        "@jest/types": "^26.6.2",
+        "camelcase": "^6.0.0",
+        "chalk": "^4.0.0",
+        "jest-get-type": "^26.3.0",
         "leven": "^3.1.0",
-        "pretty-format": "^25.5.0"
+        "pretty-format": "^26.6.2"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "6.2.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
+          "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==",
+          "dev": true
+        }
       }
     },
     "jest-watcher": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.5.0.tgz",
-      "integrity": "sha512-XrSfJnVASEl+5+bb51V0Q7WQx65dTSk7NL4yDdVjPnRNpM0hG+ncFmDYJo9O8jaSRcAitVbuVawyXCRoxGrT5Q==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz",
+      "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==",
       "dev": true,
       "requires": {
-        "@jest/test-result": "^25.5.0",
-        "@jest/types": "^25.5.0",
+        "@jest/test-result": "^26.6.2",
+        "@jest/types": "^26.6.2",
+        "@types/node": "*",
         "ansi-escapes": "^4.2.1",
-        "chalk": "^3.0.0",
-        "jest-util": "^25.5.0",
-        "string-length": "^3.1.0"
+        "chalk": "^4.0.0",
+        "jest-util": "^26.6.2",
+        "string-length": "^4.0.1"
       }
     },
     "jest-worker": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz",
-      "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
+      "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==",
       "dev": true,
       "requires": {
+        "@types/node": "*",
         "merge-stream": "^2.0.0",
         "supports-color": "^7.0.0"
       }
@@ -3580,37 +3753,51 @@
       "dev": true
     },
     "jsdom": {
-      "version": "15.2.1",
-      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz",
-      "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==",
+      "version": "16.6.0",
+      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.6.0.tgz",
+      "integrity": "sha512-Ty1vmF4NHJkolaEmdjtxTfSfkdb8Ywarwf63f+F8/mDD1uLSSWDxDuMiZxiPhwunLrn9LOSVItWj4bLYsLN3Dg==",
       "dev": true,
       "requires": {
-        "abab": "^2.0.0",
-        "acorn": "^7.1.0",
-        "acorn-globals": "^4.3.2",
-        "array-equal": "^1.0.0",
-        "cssom": "^0.4.1",
-        "cssstyle": "^2.0.0",
-        "data-urls": "^1.1.0",
-        "domexception": "^1.0.1",
-        "escodegen": "^1.11.1",
-        "html-encoding-sniffer": "^1.0.2",
+        "abab": "^2.0.5",
+        "acorn": "^8.2.4",
+        "acorn-globals": "^6.0.0",
+        "cssom": "^0.4.4",
+        "cssstyle": "^2.3.0",
+        "data-urls": "^2.0.0",
+        "decimal.js": "^10.2.1",
+        "domexception": "^2.0.1",
+        "escodegen": "^2.0.0",
+        "form-data": "^3.0.0",
+        "html-encoding-sniffer": "^2.0.1",
+        "http-proxy-agent": "^4.0.1",
+        "https-proxy-agent": "^5.0.0",
+        "is-potential-custom-element-name": "^1.0.1",
         "nwsapi": "^2.2.0",
-        "parse5": "5.1.0",
-        "pn": "^1.1.0",
-        "request": "^2.88.0",
-        "request-promise-native": "^1.0.7",
-        "saxes": "^3.1.9",
-        "symbol-tree": "^3.2.2",
-        "tough-cookie": "^3.0.1",
-        "w3c-hr-time": "^1.0.1",
-        "w3c-xmlserializer": "^1.1.2",
-        "webidl-conversions": "^4.0.2",
+        "parse5": "6.0.1",
+        "saxes": "^5.0.1",
+        "symbol-tree": "^3.2.4",
+        "tough-cookie": "^4.0.0",
+        "w3c-hr-time": "^1.0.2",
+        "w3c-xmlserializer": "^2.0.0",
+        "webidl-conversions": "^6.1.0",
         "whatwg-encoding": "^1.0.5",
         "whatwg-mimetype": "^2.3.0",
-        "whatwg-url": "^7.0.0",
-        "ws": "^7.0.0",
+        "whatwg-url": "^8.5.0",
+        "ws": "^7.4.5",
         "xml-name-validator": "^3.0.0"
+      },
+      "dependencies": {
+        "form-data": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
+          "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
+          "dev": true,
+          "requires": {
+            "asynckit": "^0.4.0",
+            "combined-stream": "^1.0.8",
+            "mime-types": "^2.1.12"
+          }
+        }
       }
     },
     "jsesc": {
@@ -3741,26 +3928,11 @@
       }
     },
     "lodash": {
-      "version": "4.17.20",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
       "dev": true
     },
-    "lodash.sortby": {
-      "version": "4.7.0",
-      "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
-      "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
-      "dev": true
-    },
-    "lolex": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz",
-      "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==",
-      "dev": true,
-      "requires": {
-        "@sinonjs/commons": "^1.7.0"
-      }
-    },
     "long": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@@ -3901,13 +4073,21 @@
       "dev": true
     },
     "micromatch": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
-      "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+      "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
       "dev": true,
       "requires": {
         "braces": "^3.0.1",
-        "picomatch": "^2.0.5"
+        "picomatch": "^2.2.3"
+      },
+      "dependencies": {
+        "picomatch": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
+          "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
+          "dev": true
+        }
       }
     },
     "micromodal": {
@@ -3915,12 +4095,6 @@
       "resolved": "https://registry.npmjs.org/micromodal/-/micromodal-0.4.6.tgz",
       "integrity": "sha512-2VDso2a22jWPpqwuWT/4RomVpoU3Bl9qF9D01xzwlNp5UVsImeA0gY4nSpF44vqcQtQOtkiMUV9EZkAJSRxBsg=="
     },
-    "mime": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz",
-      "integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==",
-      "dev": true
-    },
     "mime-db": {
       "version": "1.45.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
@@ -4035,6 +4209,12 @@
       "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
       "dev": true
     },
+    "node-fetch": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
+      "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
+      "dev": true
+    },
     "node-gyp": {
       "version": "3.8.0",
       "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
@@ -4094,31 +4274,72 @@
       "dev": true
     },
     "node-notifier": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz",
-      "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==",
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz",
+      "integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==",
       "dev": true,
       "optional": true,
       "requires": {
         "growly": "^1.3.0",
-        "is-wsl": "^2.1.1",
-        "semver": "^6.3.0",
+        "is-wsl": "^2.2.0",
+        "semver": "^7.3.2",
         "shellwords": "^0.1.1",
-        "which": "^1.3.1"
+        "uuid": "^8.3.0",
+        "which": "^2.0.2"
       },
       "dependencies": {
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "semver": {
+          "version": "7.3.5",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+          "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "uuid": {
+          "version": "8.3.2",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+          "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+          "dev": true,
+          "optional": true
+        },
         "which": {
-          "version": "1.3.1",
-          "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-          "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+          "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
           "dev": true,
           "optional": true,
           "requires": {
             "isexe": "^2.0.0"
           }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+          "dev": true,
+          "optional": true
         }
       }
     },
+    "node-releases": {
+      "version": "1.1.72",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz",
+      "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==",
+      "dev": true
+    },
     "node-sass": {
       "version": "4.14.1",
       "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz",
@@ -4474,9 +4695,9 @@
       }
     },
     "parse5": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz",
-      "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+      "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
       "dev": true
     },
     "pascalcase": {
@@ -4577,12 +4798,6 @@
         "find-up": "^4.0.0"
       }
     },
-    "pn": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz",
-      "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==",
-      "dev": true
-    },
     "posix-character-classes": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
@@ -4596,15 +4811,15 @@
       "dev": true
     },
     "pretty-format": {
-      "version": "25.5.0",
-      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz",
-      "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==",
+      "version": "26.6.2",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
+      "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.5.0",
+        "@jest/types": "^26.6.2",
         "ansi-regex": "^5.0.0",
         "ansi-styles": "^4.0.0",
-        "react-is": "^16.12.0"
+        "react-is": "^17.0.1"
       },
       "dependencies": {
         "ansi-regex": {
@@ -4622,15 +4837,15 @@
       "dev": true
     },
     "progress": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
-      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz",
+      "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
       "dev": true
     },
     "prompts": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz",
-      "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==",
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz",
+      "integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==",
       "dev": true,
       "requires": {
         "kleur": "^3.0.3",
@@ -4699,38 +4914,36 @@
       "dev": true
     },
     "puppeteer": {
-      "version": "1.20.0",
-      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.20.0.tgz",
-      "integrity": "sha512-bt48RDBy2eIwZPrkgbcwHtb51mj2nKvHOPMaSH2IsWiv7lOG9k9zhaRzpDZafrk05ajMc3cu+lSQYYOfH2DkVQ==",
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-10.0.0.tgz",
+      "integrity": "sha512-AxHvCb9IWmmP3gMW+epxdj92Gglii+6Z4sb+W+zc2hTTu10HF0yg6hGXot5O74uYkVqG3lfDRLfnRpi6WOwi5A==",
       "dev": true,
       "requires": {
-        "debug": "^4.1.0",
-        "extract-zip": "^1.6.6",
-        "https-proxy-agent": "^2.2.1",
-        "mime": "^2.0.3",
-        "progress": "^2.0.1",
-        "proxy-from-env": "^1.0.0",
-        "rimraf": "^2.6.1",
-        "ws": "^6.1.0"
+        "debug": "4.3.1",
+        "devtools-protocol": "0.0.883894",
+        "extract-zip": "2.0.1",
+        "https-proxy-agent": "5.0.0",
+        "node-fetch": "2.6.1",
+        "pkg-dir": "4.2.0",
+        "progress": "2.0.1",
+        "proxy-from-env": "1.1.0",
+        "rimraf": "3.0.2",
+        "tar-fs": "2.0.0",
+        "unbzip2-stream": "1.3.3",
+        "ws": "7.4.6"
       },
       "dependencies": {
-        "rimraf": {
-          "version": "2.7.1",
-          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
-          "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
-          "dev": true,
-          "requires": {
-            "glob": "^7.1.3"
-          }
+        "devtools-protocol": {
+          "version": "0.0.883894",
+          "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.883894.tgz",
+          "integrity": "sha512-33idhm54QJzf3Q7QofMgCvIVSd2o9H3kQPWaKT/fhoZh+digc+WSiMhbkeG3iN79WY4Hwr9G05NpbhEVrsOYAg==",
+          "dev": true
         },
         "ws": {
-          "version": "6.2.1",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
-          "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
-          "dev": true,
-          "requires": {
-            "async-limiter": "~1.0.0"
-          }
+          "version": "7.4.6",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
+          "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
+          "dev": true
         }
       }
     },
@@ -4741,9 +4954,9 @@
       "dev": true
     },
     "react-is": {
-      "version": "16.13.1",
-      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
-      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+      "version": "17.0.2",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+      "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
       "dev": true
     },
     "read-pkg": {
@@ -4800,12 +5013,6 @@
         }
       }
     },
-    "realpath-native": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz",
-      "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==",
-      "dev": true
-    },
     "redent": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
@@ -4833,9 +5040,9 @@
       "dev": true
     },
     "repeat-element": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
-      "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
+      "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==",
       "dev": true
     },
     "repeat-string": {
@@ -4893,38 +5100,6 @@
         }
       }
     },
-    "request-promise-core": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz",
-      "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==",
-      "dev": true,
-      "requires": {
-        "lodash": "^4.17.19"
-      }
-    },
-    "request-promise-native": {
-      "version": "1.0.9",
-      "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz",
-      "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==",
-      "dev": true,
-      "requires": {
-        "request-promise-core": "1.1.4",
-        "stealthy-require": "^1.1.1",
-        "tough-cookie": "^2.3.3"
-      },
-      "dependencies": {
-        "tough-cookie": {
-          "version": "2.5.0",
-          "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
-          "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
-          "dev": true,
-          "requires": {
-            "psl": "^1.1.28",
-            "punycode": "^2.1.1"
-          }
-        }
-      }
-    },
     "require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -5374,12 +5549,12 @@
       }
     },
     "saxes": {
-      "version": "3.1.11",
-      "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz",
-      "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==",
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+      "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
       "dev": true,
       "requires": {
-        "xmlchars": "^2.1.1"
+        "xmlchars": "^2.2.0"
       }
     },
     "scss-tokenizer": {
@@ -5712,9 +5887,9 @@
       }
     },
     "stack-utils": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.4.tgz",
-      "integrity": "sha512-IPDJfugEGbfizBwBZRZ3xpccMdRyP5lqsBWXGQWimVjua/ccLCeMOAVjlc1R7LxFjo5sEDhyNIXd8mo/AiDS9w==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz",
+      "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==",
       "dev": true,
       "requires": {
         "escape-string-regexp": "^2.0.0"
@@ -5758,37 +5933,20 @@
         "readable-stream": "^2.0.1"
       }
     },
-    "stealthy-require": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
-      "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
-      "dev": true
-    },
     "string-length": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz",
-      "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+      "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
       "dev": true,
       "requires": {
-        "astral-regex": "^1.0.0",
-        "strip-ansi": "^5.2.0"
-      },
-      "dependencies": {
-        "strip-ansi": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^4.1.0"
-          }
-        }
+        "char-regex": "^1.0.2",
+        "strip-ansi": "^6.0.0"
       }
     },
     "string-width": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
-      "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
+      "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
       "dev": true,
       "requires": {
         "emoji-regex": "^8.0.0",
@@ -5885,9 +6043,9 @@
       }
     },
     "supports-hyperlinks": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz",
-      "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz",
+      "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==",
       "dev": true,
       "requires": {
         "has-flag": "^4.0.0",
@@ -5911,6 +6069,44 @@
         "inherits": "2"
       }
     },
+    "tar-fs": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz",
+      "integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==",
+      "dev": true,
+      "requires": {
+        "chownr": "^1.1.1",
+        "mkdirp": "^0.5.1",
+        "pump": "^3.0.0",
+        "tar-stream": "^2.0.0"
+      }
+    },
+    "tar-stream": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+      "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+      "dev": true,
+      "requires": {
+        "bl": "^4.0.3",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "dev": true,
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
     "terminal-link": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
@@ -5938,6 +6134,12 @@
       "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==",
       "dev": true
     },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
     "tmpl": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
@@ -5992,23 +6194,23 @@
       }
     },
     "tough-cookie": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
-      "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
+      "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
       "dev": true,
       "requires": {
-        "ip-regex": "^2.1.0",
-        "psl": "^1.1.28",
-        "punycode": "^2.1.1"
+        "psl": "^1.1.33",
+        "punycode": "^2.1.1",
+        "universalify": "^0.1.2"
       }
     },
     "tr46": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
-      "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
+      "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
       "dev": true,
       "requires": {
-        "punycode": "^2.1.0"
+        "punycode": "^2.1.1"
       }
     },
     "trim-newlines": {
@@ -6027,9 +6229,9 @@
       }
     },
     "tslib": {
-      "version": "1.14.1",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
-      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
+      "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
       "dev": true
     },
     "tslint": {
@@ -6114,6 +6316,12 @@
           "requires": {
             "has-flag": "^3.0.0"
           }
+        },
+        "tslib": {
+          "version": "1.14.1",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+          "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+          "dev": true
         }
       }
     },
@@ -6124,6 +6332,14 @@
       "dev": true,
       "requires": {
         "tslib": "^1.8.1"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "1.14.1",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+          "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+          "dev": true
+        }
       }
     },
     "tunnel-agent": {
@@ -6162,12 +6378,6 @@
       "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
       "dev": true
     },
-    "typedarray": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
-      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
-      "dev": true
-    },
     "typedarray-to-buffer": {
       "version": "3.1.5",
       "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -6178,11 +6388,21 @@
       }
     },
     "typescript": {
-      "version": "3.9.7",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
-      "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
+      "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
       "dev": true
     },
+    "unbzip2-stream": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz",
+      "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==",
+      "dev": true,
+      "requires": {
+        "buffer": "^5.2.1",
+        "through": "^2.3.8"
+      }
+    },
     "union-value": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -6195,6 +6415,12 @@
         "set-value": "^2.0.1"
       }
     },
+    "universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+      "dev": true
+    },
     "unset-value": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
@@ -6281,9 +6507,9 @@
       "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
     },
     "v8-to-istanbul": {
-      "version": "4.1.4",
-      "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz",
-      "integrity": "sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==",
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz",
+      "integrity": "sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==",
       "dev": true,
       "requires": {
         "@types/istanbul-lib-coverage": "^2.0.1",
@@ -6336,13 +6562,11 @@
       }
     },
     "w3c-xmlserializer": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz",
-      "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
+      "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==",
       "dev": true,
       "requires": {
-        "domexception": "^1.0.1",
-        "webidl-conversions": "^4.0.2",
         "xml-name-validator": "^3.0.0"
       }
     },
@@ -6356,9 +6580,9 @@
       }
     },
     "webidl-conversions": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
-      "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
+      "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
       "dev": true
     },
     "whatwg-encoding": {
@@ -6377,20 +6601,20 @@
       "dev": true
     },
     "whatwg-url": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
-      "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
+      "version": "8.5.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.5.0.tgz",
+      "integrity": "sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg==",
       "dev": true,
       "requires": {
-        "lodash.sortby": "^4.7.0",
-        "tr46": "^1.0.1",
-        "webidl-conversions": "^4.0.2"
+        "lodash": "^4.7.0",
+        "tr46": "^2.0.2",
+        "webidl-conversions": "^6.1.0"
       }
     },
     "which": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
-      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
       "dev": true,
       "requires": {
         "isexe": "^2.0.0"
@@ -6494,9 +6718,9 @@
       }
     },
     "ws": {
-      "version": "7.4.3",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz",
-      "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==",
+      "version": "7.4.6",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
+      "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
       "dev": true
     },
     "xml-name-validator": {
diff --git a/ui/package.json b/ui/package.json
index 1c9cbe4..1347b2a 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -19,6 +19,7 @@
     "custom_utils": "file:src/base/utils",
     "devtools-protocol": "0.0.847576",
     "events": "^3.1.0",
+    "hsluv": "^0.1.0",
     "immer": "^1.12.1",
     "micromodal": "^0.4.6",
     "mithril": "^2.0.4",
@@ -31,19 +32,19 @@
   "devDependencies": {
     "@rollup/plugin-commonjs": "^14.0.0",
     "@rollup/plugin-node-resolve": "^8.4.0",
-    "@types/jest": "^22.2.3",
-    "@types/puppeteer": "^1.20.6",
+    "@types/jest": "^26.0.23",
+    "@types/puppeteer": "^5.4.3",
     "dingusjs": "^0.0.3",
-    "jest": "^25.5.4",
+    "jest": "^26.6.3",
     "node-sass": "^4.14.1",
     "node-watch": "^0.7.1",
-    "puppeteer": "^1.20.0",
+    "puppeteer": "^10.0.0",
     "rollup": "^2.38.5",
     "rollup-plugin-re": "^1.0.7",
     "rollup-plugin-sourcemaps": "^0.6.3",
-    "tslib": "^1.13.0",
+    "tslib": "^2.2.0",
     "tslint": "^5.20.1",
-    "typescript": "^3.9.3"
+    "typescript": "^4.2.4"
   },
   "scripts": {
     "build": "node build.js",
diff --git a/ui/release/channels.json b/ui/release/channels.json
index 6d53d32..3d50e0b 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,11 +2,11 @@
   "channels": [
     {
       "name": "stable",
-      "rev": "b7ab2469bd84ac0a4883c7f6a8a67e73903459b0"
+      "rev": "37000ac8159becf902f59b56233b22555823d411"
     },
     {
       "name": "canary",
-      "rev": "06350fe212f5051d9e890411ca24ab4a84b33ae1"
+      "rev": "39b17791a5aa65bc22c7862e2f19b65c413fab2b"
     },
     {
       "name": "autopush",
diff --git a/ui/run-tests b/ui/run-unittests
similarity index 92%
rename from ui/run-tests
rename to ui/run-unittests
index d06f7d2..7ebdf42 100755
--- a/ui/run-tests
+++ b/ui/run-unittests
@@ -15,4 +15,4 @@
 
 UI_DIR="$(cd -P ${BASH_SOURCE[0]%/*}; pwd)"
 
-$UI_DIR/node $UI_DIR/build.js --run-tests "$@"
+$UI_DIR/node $UI_DIR/build.js --run-unittests "$@"
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 1675e71..0324899 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -367,7 +367,7 @@
   overflow-x: hidden;
   overflow-y: auto;
   flex: 1 1 auto;
-  // TODO(taylori): This causes the sticky header to flicker when scrolling.
+  // TODO(hjd): This causes the sticky header to flicker when scrolling.
   // Is will-change necessary in the details panel?
   // will-change: transform;
   display: grid;
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index 802d0e6..f5196f0 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -220,6 +220,14 @@
         justify-self: end;
         margin-right: 10px;
       }
+      .selected {
+        justify-self: end;
+        margin-right: 10px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        width: 200px;
+      }
     }
   }
 
@@ -228,6 +236,8 @@
     font-size: 14px;
     line-height: 18px;
     width: 100%;
+    // Aggregation panel uses multiple table elements that need to be aligned,
+    // which is done by using fixed table layout.
     table-layout: fixed;
     word-wrap: break-word;
     padding: 0px 10px;
@@ -239,6 +249,13 @@
       width: 30%;
       font-weight: normal;
     }
+    .array-index {
+      text-align: right;
+    }
+  }
+
+  .auto-layout {
+    table-layout: auto;
   }
 
   button {
diff --git a/ui/src/assets/rec_frame_timeline.png b/ui/src/assets/rec_frame_timeline.png
new file mode 100644
index 0000000..2c83762
--- /dev/null
+++ b/ui/src/assets/rec_frame_timeline.png
Binary files differ
diff --git a/ui/src/assets/rec_syscalls.png b/ui/src/assets/rec_syscalls.png
new file mode 100644
index 0000000..734854a
--- /dev/null
+++ b/ui/src/assets/rec_syscalls.png
Binary files differ
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 86f0b70..cd25c9e 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -16,7 +16,11 @@
 
 import {assertExists, assertTrue} from '../base/logging';
 import {randomColor} from '../common/colorizer';
-import {ConvertTrace, ConvertTraceToPprof} from '../controller/trace_converter';
+import {
+  ConvertTrace,
+  ConvertTraceAndDownload,
+  ConvertTraceToPprof
+} from '../controller/trace_converter';
 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';
@@ -48,24 +52,36 @@
   Status,
   TraceSource,
   TraceTime,
+  TrackKindPriority,
   TrackState,
   VisibleState,
 } from './state';
 
 type StateDraft = Draft<State>;
 
+const highPriorityTrackOrder = [
+  PROCESS_SCHEDULING_TRACK_KIND,
+  PROCESS_SUMMARY_TRACK,
+  EXPECTED_FRAMES_SLICE_TRACK_KIND,
+  ACTUAL_FRAMES_SLICE_TRACK_KIND
+];
+
+const lowPriorityTrackOrder =
+    [HEAP_PROFILE_TRACK_KIND, COUNTER_TRACK_KIND, ASYNC_SLICE_TRACK_KIND];
+
 export interface AddTrackArgs {
   id?: string;
   engineId: string;
   kind: string;
   name: string;
-  isMainThread?: boolean;
+  trackKindPriority: TrackKindPriority;
   trackGroup?: string;
   config: {};
 }
 
 export interface PostedTrace {
   title: string;
+  fileName?: string;
   url?: string;
   buffer: ArrayBuffer;
 }
@@ -93,6 +109,20 @@
   state.newEngineMode = newEngineMode;
 }
 
+function rank(ts: TrackState): number[] {
+  const hpRank = rankIndex(ts.kind, highPriorityTrackOrder);
+  const lpRank = rankIndex(ts.kind, lowPriorityTrackOrder);
+  // TODO(hjd): Create sortBy object on TrackState to avoid this cast.
+  const tid = (ts.config as {tid?: number}).tid || 0;
+  return [hpRank, ts.trackKindPriority.valueOf(), lpRank, tid];
+}
+
+function rankIndex<T>(element: T, array: T[]): number {
+  const index = array.indexOf(element);
+  if (index === -1) return array.length;
+  return index;
+}
+
 export const StateActions = {
 
   navigate(state: StateDraft, args: {route: string}): void {
@@ -150,9 +180,16 @@
 
   // TODO(b/141359485): Actions should only modify state.
   convertTraceToJson(
-      state: StateDraft, args: {file: Blob, truncate?: 'start'|'end'}): void {
-    state.traceConversionInProgress = true;
-    ConvertTrace(args.file, args.truncate);
+      _: StateDraft, args: {file: Blob, truncate?: 'start'|'end'}): void {
+    ConvertTrace(args.file, 'json', args.truncate);
+  },
+
+  convertTraceToSystraceAndDownload(_: StateDraft, args: {file: Blob}): void {
+    ConvertTraceAndDownload(args.file, 'systrace');
+  },
+
+  convertTraceToJsonAndDownload(_: StateDraft, args: {file: Blob}): void {
+    ConvertTraceAndDownload(args.file, 'json');
   },
 
   convertTraceToPprof(
@@ -161,10 +198,6 @@
     ConvertTraceToPprof(args.pid, args.src, args.ts1, args.ts2);
   },
 
-  clearConversionInProgress(state: StateDraft, _args: {}): void {
-    state.traceConversionInProgress = false;
-  },
-
   addTracks(state: StateDraft, args: {tracks: AddTrackArgs[]}) {
     args.tracks.forEach(track => {
       const id = track.id === undefined ? `${state.nextId++}` : track.id;
@@ -180,7 +213,7 @@
 
   addTrack(state: StateDraft, args: {
     id?: string; engineId: string; kind: string; name: string;
-    trackGroup?: string; config: {}; isMainThread: boolean;
+    trackGroup?: string; config: {}; trackKindPriority: TrackKindPriority;
   }): void {
     const id = args.id !== undefined ? args.id : `${state.nextId++}`;
     state.tracks[id] = {
@@ -188,7 +221,7 @@
       engineId: args.engineId,
       kind: args.kind,
       name: args.name,
-      isMainThread: args.isMainThread,
+      trackKindPriority: args.trackKindPriority,
       trackGroup: args.trackGroup,
       config: args.config,
     };
@@ -226,7 +259,7 @@
           engineId: args.engineId,
           kind: DEBUG_SLICE_TRACK_KIND,
           name: args.name,
-          isMainThread: false,
+          trackKindPriority: TrackKindPriority.ORDINARY,
           trackGroup: SCROLLING_TRACK_GROUP,
           config: {
             maxDepth: 1,
@@ -246,43 +279,24 @@
   },
 
   sortThreadTracks(state: StateDraft, _: {}): void {
-    const threadTrackOrder = [
-      PROCESS_SCHEDULING_TRACK_KIND,
-      PROCESS_SUMMARY_TRACK,
-      EXPECTED_FRAMES_SLICE_TRACK_KIND,
-      ACTUAL_FRAMES_SLICE_TRACK_KIND,
-      HEAP_PROFILE_TRACK_KIND,
-      COUNTER_TRACK_KIND,
-      ASYNC_SLICE_TRACK_KIND
-    ];
     // Use a numeric collator so threads are sorted as T1, T2, ..., T10, T11,
     // rather than T1, T10, T11, ..., T2, T20, T21 .
     const coll = new Intl.Collator([], {sensitivity: 'base', numeric: true});
     for (const group of Object.values(state.trackGroups)) {
       group.tracks.sort((a: string, b: string) => {
-        const aKind = threadTrackOrder.indexOf(state.tracks[a].kind);
-        const bKind = threadTrackOrder.indexOf(state.tracks[b].kind);
+        const aRank = rank(state.tracks[a]);
+        const bRank = rank(state.tracks[b]);
+        for (let i = 0; i < aRank.length; i++) {
+          if (aRank[i] !== bRank[i]) return aRank[i] - bRank[i];
+        }
+
         const aName = state.tracks[a].name.toLocaleLowerCase();
         const bName = state.tracks[b].name.toLocaleLowerCase();
-        if (aKind === bKind) {
-          if (state.tracks[a].isMainThread && state.tracks[b].isMainThread) {
-            return 0;
-          } else if (state.tracks[a].isMainThread) {
-            return -1;
-          } else if (state.tracks[b].isMainThread) {
-            return 1;
-          }
-          return coll.compare(aName, bName);
-        } else {
-          if (aKind === -1) return 1;
-          if (bKind === -1) return -1;
-          return aKind - bKind;
-        }
+        return coll.compare(aName, bName);
       });
     }
   },
 
-
   updateAggregateSorting(
       state: StateDraft, args: {id: string, column: string}) {
     let prefs = state.aggregatePreferences[args.id];
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index 9255a93..8cf6acf 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -14,27 +14,71 @@
 
 import {produce} from 'immer';
 
+import {SLICE_TRACK_KIND} from '../tracks/chrome_slices/common';
+import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile/common';
+import {
+  PROCESS_SCHEDULING_TRACK_KIND
+} from '../tracks/process_scheduling/common';
+import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
+
 import {StateActions} from './actions';
 import {
   createEmptyState,
   SCROLLING_TRACK_GROUP,
   State,
   TraceUrlSource,
-  TrackState,
+  TrackKindPriority,
 } from './state';
 
-function fakeTrack(state: State, id: string): TrackState {
-  const track: TrackState = {
-    id,
-    engineId: '1',
-    kind: 'SOME_TRACK_KIND',
-    name: 'A track',
-    isMainThread: false,
-    trackGroup: SCROLLING_TRACK_GROUP,
-    config: {},
-  };
-  state.tracks[id] = track;
-  return track;
+function fakeTrack(state: State, args: {
+  id: string,
+  kind?: string,
+  trackGroup?: string,
+  trackKindPriority?: TrackKindPriority,
+  name?: string,
+  tid?: string
+}): State {
+  return produce(state, draft => {
+    StateActions.addTrack(draft, {
+      id: args.id,
+      engineId: '0',
+      kind: args.kind || 'SOME_TRACK_KIND',
+      name: args.name || 'A track',
+      trackKindPriority: args.trackKindPriority === undefined ?
+          TrackKindPriority.ORDINARY :
+          args.trackKindPriority,
+      trackGroup: args.trackGroup || SCROLLING_TRACK_GROUP,
+      config: {tid: args.tid || '0'}
+    });
+  });
+}
+
+function fakeTrackGroup(
+    state: State, args: {id: string, summaryTrackId: string}): State {
+  return produce(state, draft => {
+    StateActions.addTrackGroup(draft, {
+      name: 'A group',
+      id: args.id,
+      engineId: '0',
+      collapsed: false,
+      summaryTrackId: args.summaryTrackId
+    });
+  });
+}
+
+function pinnedAndScrollingTracks(
+    state: State,
+    ids: string[],
+    pinnedTracks: string[],
+    scrollingTracks: string[]): State {
+  for (const id of ids) {
+    state = fakeTrack(state, {id});
+  }
+  state = produce(state, draft => {
+    draft.pinnedTracks = pinnedTracks;
+    draft.scrollingTracks = scrollingTracks;
+  });
+  return state;
 }
 
 test('navigate', () => {
@@ -50,7 +94,7 @@
       engineId: '1',
       kind: 'cpu',
       name: 'Cpu 1',
-      isMainThread: false,
+      trackKindPriority: TrackKindPriority.ORDINARY,
       trackGroup: SCROLLING_TRACK_GROUP,
       config: {},
     });
@@ -60,7 +104,7 @@
       engineId: '2',
       kind: 'cpu',
       name: 'Cpu 2',
-      isMainThread: false,
+      trackKindPriority: TrackKindPriority.ORDINARY,
       trackGroup: SCROLLING_TRACK_GROUP,
       config: {},
     });
@@ -71,8 +115,8 @@
 });
 
 test('add track to track group', () => {
-  const state = createEmptyState();
-  fakeTrack(state, 's');
+  let state = createEmptyState();
+  state = fakeTrack(state, {id: 's'});
 
   const afterGroup = produce(state, draft => {
     StateActions.addTrackGroup(draft, {
@@ -90,7 +134,7 @@
       engineId: '1',
       kind: 'slices',
       name: 'renderer 1',
-      isMainThread: false,
+      trackKindPriority: TrackKindPriority.ORDINARY,
       trackGroup: '123-123-123',
       config: {},
     });
@@ -106,14 +150,14 @@
       engineId: '1',
       kind: 'cpu',
       name: 'Cpu 1',
-      isMainThread: false,
+      trackKindPriority: TrackKindPriority.ORDINARY,
       config: {},
     });
     StateActions.addTrack(draft, {
       engineId: '2',
       kind: 'cpu',
       name: 'Cpu 2',
-      isMainThread: false,
+      trackKindPriority: TrackKindPriority.ORDINARY,
       config: {},
     });
   });
@@ -134,12 +178,8 @@
 });
 
 test('reorder pinned to scrolling', () => {
-  const state = createEmptyState();
-  fakeTrack(state, 'a');
-  fakeTrack(state, 'b');
-  fakeTrack(state, 'c');
-  state.pinnedTracks = ['a', 'b'];
-  state.scrollingTracks = ['c'];
+  let state = createEmptyState();
+  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
 
   const after = produce(state, draft => {
     StateActions.moveTrack(draft, {
@@ -154,12 +194,8 @@
 });
 
 test('reorder scrolling to pinned', () => {
-  const state = createEmptyState();
-  fakeTrack(state, 'a');
-  fakeTrack(state, 'b');
-  fakeTrack(state, 'c');
-  state.pinnedTracks = ['a'];
-  state.scrollingTracks = ['b', 'c'];
+  let state = createEmptyState();
+  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
 
   const after = produce(state, draft => {
     StateActions.moveTrack(draft, {
@@ -174,12 +210,8 @@
 });
 
 test('reorder clamp bottom', () => {
-  const state = createEmptyState();
-  fakeTrack(state, 'a');
-  fakeTrack(state, 'b');
-  fakeTrack(state, 'c');
-  state.pinnedTracks = ['a', 'b'];
-  state.scrollingTracks = ['c'];
+  let state = createEmptyState();
+  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
 
   const after = produce(state, draft => {
     StateActions.moveTrack(draft, {
@@ -192,12 +224,8 @@
 });
 
 test('reorder clamp top', () => {
-  const state = createEmptyState();
-  fakeTrack(state, 'a');
-  fakeTrack(state, 'b');
-  fakeTrack(state, 'c');
-  state.pinnedTracks = ['a'];
-  state.scrollingTracks = ['b', 'c'];
+  let state = createEmptyState();
+  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
 
   const after = produce(state, draft => {
     StateActions.moveTrack(draft, {
@@ -210,12 +238,8 @@
 });
 
 test('pin', () => {
-  const state = createEmptyState();
-  fakeTrack(state, 'a');
-  fakeTrack(state, 'b');
-  fakeTrack(state, 'c');
-  state.pinnedTracks = ['a'];
-  state.scrollingTracks = ['b', 'c'];
+  let state = createEmptyState();
+  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
 
   const after = produce(state, draft => {
     StateActions.toggleTrackPinned(draft, {
@@ -227,12 +251,8 @@
 });
 
 test('unpin', () => {
-  const state = createEmptyState();
-  fakeTrack(state, 'a');
-  fakeTrack(state, 'b');
-  fakeTrack(state, 'c');
-  state.pinnedTracks = ['a', 'b'];
-  state.scrollingTracks = ['c'];
+  let state = createEmptyState();
+  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
 
   const after = produce(state, draft => {
     StateActions.toggleTrackPinned(draft, {
@@ -274,7 +294,7 @@
       engineId: '1',
       kind: 'cpu',
       name: 'Cpu 1',
-      isMainThread: false,
+      trackKindPriority: TrackKindPriority.ORDINARY,
       config: {},
     });
   });
@@ -314,3 +334,95 @@
   });
   expect(after.engines['100'].ready).toBe(true);
 });
+
+test('sortTracksByPriority', () => {
+  let state = createEmptyState();
+  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
+  state = fakeTrack(
+      state, {id: 'b', kind: HEAP_PROFILE_TRACK_KIND, trackGroup: 'g'});
+  state = fakeTrack(
+      state, {id: 'a', kind: PROCESS_SCHEDULING_TRACK_KIND, trackGroup: 'g'});
+
+  const after = produce(state, draft => {
+    StateActions.sortThreadTracks(draft, {});
+  });
+
+  // High Priority tracks should be sorted before Low Priority tracks:
+  // 'b' appears twice because it's the summary track
+  expect(after.trackGroups['g'].tracks).toEqual(['a', 'b', 'b']);
+});
+
+test('sortTracksByPriorityAndKindAndName', () => {
+  let state = createEmptyState();
+  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
+  state = fakeTrack(
+      state, {id: 'a', kind: PROCESS_SCHEDULING_TRACK_KIND, trackGroup: 'g'});
+  state = fakeTrack(state, {
+    id: 'b',
+    kind: SLICE_TRACK_KIND,
+    trackGroup: 'g',
+    trackKindPriority: TrackKindPriority.MAIN_THREAD
+  });
+  state = fakeTrack(state, {
+    id: 'c',
+    kind: SLICE_TRACK_KIND,
+    trackGroup: 'g',
+    trackKindPriority: TrackKindPriority.RENDER_THREAD
+  });
+  state = fakeTrack(state, {
+    id: 'd',
+    kind: SLICE_TRACK_KIND,
+    trackGroup: 'g',
+    trackKindPriority: TrackKindPriority.GPU_COMPLETION
+  });
+  state = fakeTrack(
+      state, {id: 'e', kind: HEAP_PROFILE_TRACK_KIND, trackGroup: 'g'});
+  state = fakeTrack(
+      state, {id: 'f', kind: SLICE_TRACK_KIND, trackGroup: 'g', name: 'T2'});
+  state = fakeTrack(
+      state, {id: 'g', kind: SLICE_TRACK_KIND, trackGroup: 'g', name: 'T10'});
+
+  const after = produce(state, draft => {
+    StateActions.sortThreadTracks(draft, {});
+  });
+
+  // The order should be determined by:
+  // 1.High priority
+  // 2.Non ordinary track kinds
+  // 3.Low priority
+  // 4.Collated name string (ie. 'T2' will be before 'T10')
+  expect(after.trackGroups['g'].tracks)
+      .toEqual(['a', 'b', 'b', 'c', 'd', 'e', 'f', 'g']);
+});
+
+test('sortTracksByTidThenName', () => {
+  let state = createEmptyState();
+  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'a'});
+  state = fakeTrack(state, {
+    id: 'a',
+    kind: SLICE_TRACK_KIND,
+    trackGroup: 'g',
+    name: 'aaa',
+    tid: '1'
+  });
+  state = fakeTrack(state, {
+    id: 'b',
+    kind: SLICE_TRACK_KIND,
+    trackGroup: 'g',
+    name: 'bbb',
+    tid: '2'
+  });
+  state = fakeTrack(state, {
+    id: 'c',
+    kind: THREAD_STATE_TRACK_KIND,
+    trackGroup: 'g',
+    name: 'ccc',
+    tid: '1'
+  });
+
+  const after = produce(state, draft => {
+    StateActions.sortThreadTracks(draft, {});
+  });
+
+  expect(after.trackGroups['g'].tracks).toEqual(['a', 'a', 'c', 'b']);
+});
diff --git a/ui/src/common/arg_types.ts b/ui/src/common/arg_types.ts
new file mode 100644
index 0000000..6ca6a04
--- /dev/null
+++ b/ui/src/common/arg_types.ts
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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 type Arg = string|{kind: 'SLICE', trackId: string, sliceId: number};
+export type Args = Map<string, Arg>;
+
+export type ArgsTree = ArgsTreeMap|ArgsTreeArray|string;
+export type ArgsTreeArray = ArgsTree[];
+export interface ArgsTreeMap {
+  [key: string]: ArgsTree;
+}
+
+export function isArgTreeArray(item: ArgsTree): item is ArgsTreeArray {
+  return typeof item === 'object' && item.length !== undefined;
+}
+
+export function isArgTreeMap(item: ArgsTree): item is ArgsTreeMap {
+  return typeof item === 'object' && item.length === undefined;
+}
diff --git a/ui/src/common/colorizer.ts b/ui/src/common/colorizer.ts
index 79105cf..9adde38 100644
--- a/ui/src/common/colorizer.ts
+++ b/ui/src/common/colorizer.ts
@@ -127,10 +127,6 @@
   return Object.assign({}, MD_PALETTE[colorIdx]);
 }
 
-export function hueForSlice(sliceName: string): number {
-  return hash(sliceName, 360);
-}
-
 export function colorForThread(thread?: {pid?: number, tid: number}): Color {
   if (thread === undefined) {
     return Object.assign({}, GREY_COLOR);
@@ -144,3 +140,22 @@
   const hue = Math.floor(Math.random() * 40) * 9;
   return '#' + hsl.hex([hue, 90, 30]);
 }
+
+// Chooses a color uniform at random based on hash(sliceName).  Returns [hue,
+// saturation, lightness].
+//
+// Prefer converting this to an RGB color using hsluv, not the browser's
+// built-in vanilla HSL handling.  This is because this function chooses
+// hue/lightness uniform at random, but HSL is not perceptually uniform.  See
+// https://www.boronine.com/2012/03/26/Color-Spaces-for-Human-Beings/.
+//
+// If isSelected, the color will be particularly dark, making it stand out.
+export function hslForSlice(
+    sliceName: string, isSelected: boolean|null): [number, number, number] {
+  const hue = hash(sliceName, 360);
+  // Saturation 100 would give the most differentiation between colors, but it's
+  // garish.
+  const saturation = 80;
+  const lightness = isSelected ? 30 : hash(sliceName + 'x', 40) + 40;
+  return [hue, saturation, lightness];
+}
diff --git a/ui/config/headless_setup.js b/ui/src/common/constants.ts
similarity index 78%
rename from ui/config/headless_setup.js
rename to ui/src/common/constants.ts
index 7f8f93c..30a7a2d 100644
--- a/ui/config/headless_setup.js
+++ b/ui/src/common/constants.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2018 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.
@@ -12,6 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-module.exports = async function() {
-}
+export const TRACE_SUFFIX = '.perfetto-trace';
 
+export const TRACE_MARGIN_TIME_S = 1 / 1e7;
diff --git a/ui/src/common/conversion_jobs.ts b/ui/src/common/conversion_jobs.ts
new file mode 100644
index 0000000..efb56e2
--- /dev/null
+++ b/ui/src/common/conversion_jobs.ts
@@ -0,0 +1,26 @@
+// 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 enum ConversionJobStatus {
+  InProgress = 'InProgress',
+  NotRunning = 'NotRunning',
+}
+
+export type ConversionJobName = 'convert_systrace'|'convert_json'|
+    'open_in_legacy'|'convert_pprof'|'create_permalink';
+
+export interface ConversionJobStatusUpdate {
+  jobName: ConversionJobName;
+  jobStatus: ConversionJobStatus;
+}
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index c270010..6d77b0f 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -12,15 +12,30 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {defer, Deferred} from '../base/deferred';
+import {assertExists, assertTrue} from '../base/logging';
+import {perfetto} from '../gen/protos';
+
+import {ProtoRingBuffer} from './proto_ring_buffer';
 import {
   ComputeMetricArgs,
   ComputeMetricResult,
+  QueryArgs,
   RawQueryArgs,
   RawQueryResult
 } from './protos';
-import {slowlyCountRows} from './query_iterator';
+import {NUM, NUM_NULL, slowlyCountRows, STR} from './query_iterator';
+import {
+  createQueryResult,
+  QueryResult,
+  WritableQueryResult
+} from './query_result';
 import {TimeSpan} from './time';
 
+import TraceProcessorRpc = perfetto.protos.TraceProcessorRpc;
+import TraceProcessorRpcStream = perfetto.protos.TraceProcessorRpcStream;
+import TPM = perfetto.protos.TraceProcessorRpc.TraceProcessorMethod;
+
 export interface LoadingTracker {
   beginLoading(): void;
   endLoading(): void;
@@ -33,54 +48,229 @@
 
 export class QueryError extends Error {}
 
+// This is used to skip the decoding of queryResult from protobufjs and deal
+// with it ourselves. See the comment below around `QueryResult.decode = ...`.
+interface QueryResultBypass {
+  rawQueryResult: Uint8Array;
+}
+
 /**
  * Abstract interface of a trace proccessor.
  * This is the TypeScript equivalent of src/trace_processor/rpc.h.
- *
- * Engine also defines helpers for the most common service methods
- * (e.g. query).
+ * There are two concrete implementations:
+ *   1. WasmEngineProxy: creates a Wasm module and interacts over postMessage().
+ *   2. HttpRpcEngine: connects to an external `trace_processor_shell --httpd`.
+ *      and interacts via fetch().
+ * In both cases, we have a byte-oriented pipe to interact with TraceProcessor.
+ * The derived class is only expected to deal with these two functions:
+ * 1. Implement the abstract rpcSendRequestBytes() function, sending the
+ *    proto-encoded TraceProcessorRpc requests to the TraceProcessor instance.
+ * 2. Call onRpcResponseBytes() when response data is received.
  */
 export abstract class Engine {
   abstract readonly id: string;
   private _cpus?: number[];
   private _numGpus?: number;
   private loadingTracker: LoadingTracker;
+  private txSeqId = 0;
+  private rxSeqId = 0;
+  private rxBuf = new ProtoRingBuffer();
+  private pendingParses = new Array<Deferred<void>>();
+  private pendingEOFs = new Array<Deferred<void>>();
+  private pendingQueries = new Array<WritableQueryResult>();
+  private pendingRawQueries = new Array<Deferred<RawQueryResult>>();
+  private pendingRestoreTables = new Array<Deferred<void>>();
+  private pendingComputeMetrics = new Array<Deferred<ComputeMetricResult>>();
 
   constructor(tracker?: LoadingTracker) {
     this.loadingTracker = tracker ? tracker : new NullLoadingTracker();
   }
 
   /**
+   * Called to send data to the TraceProcessor instance. This turns into a
+   * postMessage() or a HTTP request, depending on the Engine implementation.
+   */
+  abstract rpcSendRequestBytes(data: Uint8Array): void;
+
+  /**
+   * Called when an inbound message is received by the Engine implementation
+   * (e.g. onmessage for the Wasm case, on when HTTP replies are received for
+   * the HTTP+RPC case).
+   */
+  onRpcResponseBytes(dataWillBeRetained: Uint8Array) {
+    // Note: when hitting the fastpath inside ProtoRingBuffer, the |data| buffer
+    // is returned back by readMessage() (% subarray()-ing it) and held onto by
+    // other classes (e.g., QueryResult). For both fetch() and Wasm we are fine
+    // because every response creates a new buffer.
+    this.rxBuf.append(dataWillBeRetained);
+    for (;;) {
+      const msg = this.rxBuf.readMessage();
+      if (msg === undefined) break;
+      this.onRpcResponseMessage(msg);
+    }
+  }
+
+  /*
+   * Parses a response message.
+   * |rpcMsgEncoded| is a sub-array to to the start of a TraceProcessorRpc
+   * proto-encoded message (without the proto preamble and varint size).
+   */
+  private onRpcResponseMessage(rpcMsgEncoded: Uint8Array) {
+    // Here we override the protobufjs-generated code to skip the parsing of the
+    // new streaming QueryResult and instead passing it through like a buffer.
+    // This is the overall problem: All trace processor responses are wrapped
+    // into a perfetto.protos.TraceProcessorRpc proto message. In all cases %
+    // TPM_QUERY_STREAMING, we want protobufjs to decode the proto bytes and
+    // give us a structured object. In the case of TPM_QUERY_STREAMING, instead,
+    // we want to deal with the proto parsing ourselves using the new
+    // QueryResult.appendResultBatch() method, because that handled streaming
+    // results more efficiently and skips several copies.
+    // By overriding the decode method below, we achieve two things:
+    // 1. We avoid protobufjs decoding the TraceProcessorRpc.query_result field.
+    // 2. We stash (a view of) the original buffer into the |rawQueryResult| so
+    //    the `case TPM_QUERY_STREAMING` below can take it.
+    perfetto.protos.QueryResult.decode =
+        (reader: protobuf.Reader, length: number) => {
+          const res =
+              perfetto.protos.QueryResult.create() as {} as QueryResultBypass;
+          res.rawQueryResult =
+              reader.buf.subarray(reader.pos, reader.pos + length);
+          // All this works only if protobufjs returns the original ArrayBuffer
+          // from |rpcMsgEncoded|. It should be always the case given the
+          // current implementation. This check mainly guards against future
+          // behavioral changes of protobufjs. We don't want to accidentally
+          // hold onto some internal protobufjs buffer. We are fine holding
+          // onto |rpcMsgEncoded| because those come from ProtoRingBuffer which
+          // is buffer-retention-friendly.
+          assertTrue(res.rawQueryResult.buffer === rpcMsgEncoded.buffer);
+          reader.pos += length;
+          return res as {} as perfetto.protos.QueryResult;
+        };
+
+    const rpc = TraceProcessorRpc.decode(rpcMsgEncoded);
+    this.loadingTracker.endLoading();
+
+    if (rpc.fatalError !== undefined && rpc.fatalError.length > 0) {
+      throw new Error(`${rpc.fatalError}`);
+    }
+
+    // Allow restarting sequences from zero (when reloading the browser).
+    if (rpc.seq !== this.rxSeqId + 1 && this.rxSeqId !== 0 && rpc.seq !== 0) {
+      // "(ERR:rpc_seq)" is intercepted by error_dialog.ts to show a more
+      // graceful and actionable error.
+      throw new Error(`RPC sequence id mismatch cur=${rpc.seq} last=${
+          this.rxSeqId} (ERR:rpc_seq)`);
+    }
+
+    this.rxSeqId = rpc.seq;
+
+    switch (rpc.response) {
+      case TPM.TPM_APPEND_TRACE_DATA:
+        const appendResult = assertExists(rpc.appendResult);
+        const pendingPromise = assertExists(this.pendingParses.shift());
+        if (appendResult.error && appendResult.error.length > 0) {
+          pendingPromise.reject(appendResult.error);
+        } else {
+          pendingPromise.resolve();
+        }
+        break;
+      case TPM.TPM_FINALIZE_TRACE_DATA:
+        assertExists(this.pendingEOFs.shift()).resolve();
+        break;
+      case TPM.TPM_RESTORE_INITIAL_TABLES:
+        assertExists(this.pendingRestoreTables.shift()).resolve();
+        break;
+      case TPM.TPM_QUERY_STREAMING:
+        const qRes = assertExists(rpc.queryResult) as {} as QueryResultBypass;
+        const pendingQuery = assertExists(this.pendingQueries[0]);
+        pendingQuery.appendResultBatch(qRes.rawQueryResult);
+        if (pendingQuery.isComplete()) {
+          this.pendingQueries.shift();
+        }
+        break;
+      case TPM.TPM_QUERY_RAW_DEPRECATED:
+        const queryRes = assertExists(rpc.rawQueryResult) as RawQueryResult;
+        assertExists(this.pendingRawQueries.shift()).resolve(queryRes);
+        break;
+      case TPM.TPM_COMPUTE_METRIC:
+        const metricRes = assertExists(rpc.metricResult) as ComputeMetricResult;
+        if (metricRes.error && metricRes.error.length > 0) {
+          throw new QueryError(`ComputeMetric() error: ${metricRes.error}`);
+        }
+        assertExists(this.pendingComputeMetrics.shift()).resolve(metricRes);
+        break;
+      default:
+        console.log(
+            'Unexpected TraceProcessor response received: ', rpc.response);
+        break;
+    }  // switch(rpc.response);
+  }
+
+  /**
+   * TraceProcessor methods below this point.
+   * The methods below are called by the various controllers in the UI and
+   * deal with marshalling / unmarshaling requests to/from TraceProcessor.
+   */
+
+
+  /**
    * Push trace data into the engine. The engine is supposed to automatically
    * figure out the type of the trace (JSON vs Protobuf).
    */
-  abstract parse(data: Uint8Array): Promise<void>;
+  parse(data: Uint8Array): Promise<void> {
+    const asyncRes = defer<void>();
+    this.pendingParses.push(asyncRes);
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_APPEND_TRACE_DATA;
+    rpc.appendTraceData = data;
+    this.rpcSendRequest(rpc);
+    return asyncRes;  // Linearize with the worker.
+  }
 
   /**
-   * Notify the engine no more data is coming.
+   * Notify the engine that we reached the end of the trace.
+   * Called after the last parse() call.
    */
-  abstract notifyEof(): void;
+  notifyEof(): Promise<void> {
+    const asyncRes = defer<void>();
+    this.pendingEOFs.push(asyncRes);
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_FINALIZE_TRACE_DATA;
+    this.rpcSendRequest(rpc);
+    return asyncRes;  // Linearize with the worker.
+  }
 
   /**
    * Resets the trace processor state by destroying any table/views created by
    * the UI after loading.
    */
-  abstract restoreInitialTables(): void;
-
-  /*
-   * Performs a SQL query and retruns a proto-encoded RawQueryResult object.
-   */
-  abstract rawQuery(rawQueryArgs: Uint8Array): Promise<Uint8Array>;
-
-  /*
-   * Performs computation of metrics and returns metric result and any errors.
-   * Metric result is a proto binary or text encoded TraceMetrics object.
-   */
-  abstract rawComputeMetric(computeMetricArgs: Uint8Array): Promise<Uint8Array>;
+  restoreInitialTables(): Promise<void> {
+    const asyncRes = defer<void>();
+    this.pendingRestoreTables.push(asyncRes);
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_RESTORE_INITIAL_TABLES;
+    this.rpcSendRequest(rpc);
+    return asyncRes;  // Linearize with the worker.
+  }
 
   /**
-   * Shorthand for sending a SQL query to the engine.
-   * Deals with {,un}marshalling of request/response args.
+   * Shorthand for sending a compute metrics request to the engine.
+   */
+  async computeMetric(metrics: string[]): Promise<ComputeMetricResult> {
+    const asyncRes = defer<ComputeMetricResult>();
+    this.pendingComputeMetrics.push(asyncRes);
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_COMPUTE_METRIC;
+    const args = rpc.computeMetricArgs = new ComputeMetricArgs();
+    args.metricNames = metrics;
+    args.format = ComputeMetricArgs.ResultFormat.TEXTPROTO;
+    this.rpcSendRequest(rpc);
+    return asyncRes;
+  }
+
+  /**
+   * Runs a SQL query and throws if the query failed.
+   * Queries performed by the controller logic should use this.
    */
   async query(sqlQuery: string): Promise<RawQueryResult> {
     const result = await this.uncheckedQuery(sqlQuery);
@@ -90,38 +280,21 @@
     return result;
   }
 
-  // This method is for noncritical queries that shouldn't throw an error
-  // on failure. The caller must handle the failure.
-  async uncheckedQuery(sqlQuery: string): Promise<RawQueryResult> {
-    this.loadingTracker.beginLoading();
-    try {
-      const args = new RawQueryArgs();
-      args.sqlQuery = sqlQuery;
-      args.timeQueuedNs = Math.floor(performance.now() * 1e6);
-      const argsEncoded = RawQueryArgs.encode(args).finish();
-      const respEncoded = await this.rawQuery(argsEncoded);
-      const result = RawQueryResult.decode(respEncoded);
-      return result;
-    } finally {
-      this.loadingTracker.endLoading();
-    }
-  }
-
   /**
-   * Shorthand for sending a compute metrics request to the engine.
-   * Deals with {,un}marshalling of request/response args.
+   * Runs a SQL query. Does not throw if the query fails.
+   * The caller must handle this failure. This is so this function can be safely
+   * used for user-typed SQL.
    */
-  async computeMetric(metrics: string[]): Promise<ComputeMetricResult> {
-    const args = new ComputeMetricArgs();
-    args.metricNames = metrics;
-    args.format = ComputeMetricArgs.ResultFormat.TEXTPROTO;
-    const argsEncoded = ComputeMetricArgs.encode(args).finish();
-    const respEncoded = await this.rawComputeMetric(argsEncoded);
-    const result = ComputeMetricResult.decode(respEncoded);
-    if (result.error.length > 0) {
-      throw new QueryError(result.error);
-    }
-    return result;
+  uncheckedQuery(sqlQuery: string): Promise<RawQueryResult> {
+    const asyncRes = defer<RawQueryResult>();
+    this.pendingRawQueries.push(asyncRes);
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_QUERY_RAW_DEPRECATED;
+    rpc.rawQueryArgs = new RawQueryArgs();
+    rpc.rawQueryArgs.sqlQuery = sqlQuery;
+    rpc.rawQueryArgs.timeQueuedNs = Math.floor(performance.now() * 1e6);
+    this.rpcSendRequest(rpc);
+    return asyncRes;
   }
 
   async queryOneRow(query: string): Promise<number[]> {
@@ -142,13 +315,63 @@
     return res;
   }
 
+  /*
+   * Issues a streaming query and retrieve results in batches.
+   * The returned QueryResult object will be populated over time with batches
+   * of rows (each batch conveys ~128KB of data and a variable number of rows).
+   * The caller can decide whether to wait that all batches have been received
+   * (by awaiting the returned object or calling result.waitAllRows()) or handle
+   * the rows incrementally.
+   *
+   * Example usage:
+   * const res = engine.queryV2('SELECT foo, bar FROM table');
+   * console.log(res.numRows());  // Will print 0 because we didn't await.
+   * await(res.waitAllRows());
+   * console.log(res.numRows());  // Will print the total number of rows.
+   *
+   * for (const it = res.iter({foo: NUM, bar:STR}); it.valid(); it.next()) {
+   *   console.log(it.foo, it.bar);
+   * }
+   * TODO(primiano): in next CLs move everything on queryV2, then rename it to
+   * just query(), and delete the old (columnar, non-streaming) query() method.
+   */
+  queryV2(sqlQuery: string): Promise<QueryResult>&QueryResult {
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_QUERY_STREAMING;
+    rpc.queryArgs = new QueryArgs();
+    rpc.queryArgs.sqlQuery = sqlQuery;
+    rpc.queryArgs.timeQueuedNs = Math.floor(performance.now() * 1e6);
+    const result = createQueryResult();
+    this.pendingQueries.push(result);
+    this.rpcSendRequest(rpc);
+    return result;
+  }
+
+  /**
+   * Marshals the TraceProcessorRpc request arguments and sends the request
+   * to the concrete Engine (Wasm or HTTP).
+   */
+  private rpcSendRequest(rpc: TraceProcessorRpc) {
+    rpc.seq = this.txSeqId++;
+    // Each message is wrapped in a TraceProcessorRpcStream to add the varint
+    // preamble with the size, which allows tokenization on the other end.
+    const outerProto = TraceProcessorRpcStream.create();
+    outerProto.msg.push(rpc);
+    const buf = TraceProcessorRpcStream.encode(outerProto).finish();
+    this.loadingTracker.beginLoading();
+    this.rpcSendRequestBytes(buf);
+  }
+
   // TODO(hjd): When streaming must invalidate this somehow.
   async getCpus(): Promise<number[]> {
     if (!this._cpus) {
-      const result =
-          await this.query('select distinct(cpu) from sched order by cpu;');
-      if (slowlyCountRows(result) === 0) return [];
-      this._cpus = result.columns[0].longValues!.map(n => +n);
+      const cpus = [];
+      const queryRes = await this.queryV2(
+          'select distinct(cpu) as cpu from sched order by cpu;');
+      for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
+        cpus.push(it.cpu);
+      }
+      this._cpus = cpus;
     }
     return this._cpus;
   }
@@ -168,8 +391,8 @@
   // TODO: This should live in code that's more specific to chrome, instead of
   // in engine.
   async getNumberOfProcesses(): Promise<number> {
-    const result = await this.query('select count(*) from process;');
-    return +result.columns[0].longValues![0];
+    const result = await this.queryV2('select count(*) as cnt from process;');
+    return result.firstRow({cnt: NUM}).cnt;
   }
 
   async getTraceTimeBounds(): Promise<TimeSpan> {
@@ -177,4 +400,25 @@
     const res = (await this.queryOneRow(query));
     return new TimeSpan(res[0] / 1e9, res[1] / 1e9);
   }
+
+  async getTracingMetadataTimeBounds(): Promise<TimeSpan> {
+    const queryRes = await this.queryV2(`select name, int_value from metadata
+         where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
+         or name = 'all_data_source_started_ns'`);
+    let startBound = -Infinity;
+    let endBound = Infinity;
+    const it = queryRes.iter({'name': STR, 'int_value': NUM_NULL});
+    for (; it.valid(); it.next()) {
+      const columnName = it.name;
+      const timestamp = it.int_value;
+      if (timestamp === null) continue;
+      if (columnName === 'tracing_disabled_ns') {
+        endBound = Math.min(endBound, timestamp / 1e9);
+      } else {
+        startBound = Math.max(startBound, timestamp / 1e9);
+      }
+    }
+
+    return new TimeSpan(startBound, endBound);
+  }
 }
diff --git a/ui/src/common/flamegraph_util.ts b/ui/src/common/flamegraph_util.ts
index f083428..2910952 100644
--- a/ui/src/common/flamegraph_util.ts
+++ b/ui/src/common/flamegraph_util.ts
@@ -103,7 +103,8 @@
     mapping: callsite.mapping,
     selfSize: callsite.selfSize,
     merged: callsite.merged,
-    highlighted: callsite.highlighted
+    highlighted: callsite.highlighted,
+    location: callsite.location
   };
 }
 
diff --git a/ui/src/common/http_rpc_engine.ts b/ui/src/common/http_rpc_engine.ts
index bd1f6dd..d404ea9 100644
--- a/ui/src/common/http_rpc_engine.ts
+++ b/ui/src/common/http_rpc_engine.ts
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {defer, Deferred} from '../base/deferred';
 import {fetchWithTimeout} from '../base/http_utils';
-import {assertExists, assertTrue} from '../base/logging';
+import {assertTrue} from '../base/logging';
 import {StatusResult} from '../common/protos';
 
 import {Engine, LoadingTracker} from './engine';
@@ -24,22 +23,14 @@
 
 export interface HttpRpcState {
   connected: boolean;
-  loadedTraceName?: string;
+  status?: StatusResult;
   failure?: string;
 }
 
-interface QueuedRequest {
-  methodName: string;
-  reqData?: Uint8Array;
-  resp: Deferred<Uint8Array>;
-  id: number;
-}
-
 export class HttpRpcEngine extends Engine {
   readonly id: string;
-  private nextReqId = 0;
-  private requestQueue = new Array<QueuedRequest>();
-  private pendingRequest?: QueuedRequest = undefined;
+  private requestQueue = new Array<Uint8Array>();
+  private requestPending = false;
   errorHandler: (err: string) => void = () => {};
 
   constructor(id: string, loadingTracker?: LoadingTracker) {
@@ -47,50 +38,17 @@
     this.id = id;
   }
 
-  async parse(data: Uint8Array): Promise<void> {
-    await this.enqueueRequest('parse', data);
-  }
-
-  async notifyEof(): Promise<void> {
-    await this.enqueueRequest('notify_eof');
-  }
-
-  async restoreInitialTables(): Promise<void> {
-    await this.enqueueRequest('restore_initial_tables');
-  }
-
-  rawQuery(rawQueryArgs: Uint8Array): Promise<Uint8Array> {
-    return this.enqueueRequest('raw_query', rawQueryArgs);
-  }
-
-  rawComputeMetric(rawComputeMetricArgs: Uint8Array): Promise<Uint8Array> {
-    return this.enqueueRequest('compute_metric', rawComputeMetricArgs);
-  }
-
-  async enableMetatrace(): Promise<void> {
-    await this.enqueueRequest('enable_metatrace');
-  }
-
-  disableAndReadMetatrace(): Promise<Uint8Array> {
-    return this.enqueueRequest('disable_and_read_metatrace');
-  }
-
-  enqueueRequest(methodName: string, data?: Uint8Array): Promise<Uint8Array> {
-    const resp = defer<Uint8Array>();
-    const req:
-        QueuedRequest = {methodName, reqData: data, resp, id: this.nextReqId++};
-    if (this.pendingRequest === undefined) {
-      this.beginFetch(req);
+  rpcSendRequestBytes(data: Uint8Array): void {
+    if (!this.requestPending && this.requestQueue.length === 0) {
+      this.beginFetch(data);
     } else {
-      this.requestQueue.push(req);
+      this.requestQueue.push(data);
     }
-    return resp;
   }
 
-  private beginFetch(req: QueuedRequest) {
-    assertTrue(this.pendingRequest === undefined);
-    this.pendingRequest = req;
-    const methodName = req.methodName.toLowerCase();
+  private beginFetch(data: Uint8Array) {
+    assertTrue(!this.requestPending);
+    this.requestPending = true;
     // Deliberately not using fetchWithTimeout() here. These queries can be
     // arbitrarily long.
     // Deliberately not setting cache: no-cache. Doing so invalidates also the
@@ -98,42 +56,33 @@
     // no-cache is also useless because trace-processor's replies are already
     // marked as no-cache and browsers generally already assume that POST
     // requests are not idempotent.
-    fetch(RPC_URL + methodName, {
+    fetch(RPC_URL + 'rpc', {
       method: 'post',
-      headers: {
-        'Content-Type': 'application/x-protobuf',
-        'X-Seq-Id': `${req.id}`,  // Used only for debugging.
-      },
-      body: req.reqData || new Uint8Array(),
+      headers: {'Content-Type': 'application/x-protobuf'},
+      body: data,
     })
-        .then(resp => this.endFetch(resp, req.id))
+        .then(resp => this.endFetch(resp))
         .catch(err => this.errorHandler(err));
   }
 
-  private endFetch(resp: Response, expectedReqId: number) {
-    const req = assertExists(this.pendingRequest);
-    this.pendingRequest = undefined;
-    assertTrue(expectedReqId === req.id);
+  private endFetch(resp: Response) {
+    assertTrue(this.requestPending);
     if (resp.status !== 200) {
-      req.resp.reject(`HTTP ${resp.status} - ${resp.statusText}`);
-      return;
+      throw new Error(`HTTP ${resp.status} - ${resp.statusText}`);
     }
     resp.arrayBuffer().then(arrBuf => {
       // Note: another request can sneak in via enqueueRequest() between the
       // arrayBuffer() call and this continuation. At this point
       // this.pendingRequest might be set again.
       // If not (the most common case) submit the next queued request, if any.
-      this.maybeSubmitNextQueuedRequest();
-      req.resp.resolve(new Uint8Array(arrBuf));
+      this.requestPending = false;
+      if (this.requestQueue.length > 0) {
+        this.beginFetch(this.requestQueue.shift()!);
+      }
+      super.onRpcResponseBytes(new Uint8Array(arrBuf));
     });
   }
 
-  private maybeSubmitNextQueuedRequest() {
-    if (this.pendingRequest === undefined && this.requestQueue.length > 0) {
-      this.beginFetch(this.requestQueue.shift()!);
-    }
-  }
-
   static async checkConnection(): Promise<HttpRpcState> {
     const httpRpcState: HttpRpcState = {connected: false};
     console.info(
@@ -149,11 +98,8 @@
         httpRpcState.failure = `${resp.status} - ${resp.statusText}`;
       } else {
         const buf = new Uint8Array(await resp.arrayBuffer());
-        const status = StatusResult.decode(buf);
         httpRpcState.connected = true;
-        if (status.loadedTraceName) {
-          httpRpcState.loadedTraceName = status.loadedTraceName;
-        }
+        httpRpcState.status = StatusResult.decode(buf);
       }
     } catch (err) {
       httpRpcState.failure = `${err}`;
diff --git a/ui/src/common/proto_ring_buffer.ts b/ui/src/common/proto_ring_buffer.ts
new file mode 100644
index 0000000..b03c1c9
--- /dev/null
+++ b/ui/src/common/proto_ring_buffer.ts
@@ -0,0 +1,156 @@
+// 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 {assertTrue} from '../base/logging';
+
+// This class is the TypeScript equivalent of the identically-named C++ class in
+// //protozero/proto_ring_buffer.h. See comments in that header for a detailed
+// description. The architecture is identical.
+
+const kGrowBytes = 128 * 1024;
+const kMaxMsgSize = 64 * 1024 * 1024;
+
+export class ProtoRingBuffer {
+  private buf = new Uint8Array(kGrowBytes);
+  private fastpath?: Uint8Array;
+  private rd = 0;
+  private wr = 0;
+
+  // The caller must call ReadMessage() after each append() call.
+  // The |data| might be either copied in the internal ring buffer or returned
+  // (% subarray()) to the next ReadMessage() call.
+  append(data: Uint8Array) {
+    assertTrue(this.wr <= this.buf.length);
+    assertTrue(this.rd <= this.wr);
+
+    // If the last call to ReadMessage() consumed all the data in the buffer and
+    // there are no incomplete messages pending, restart from the beginning
+    // rather than keep ringing. This is the most common case.
+    if (this.rd === this.wr) {
+      this.rd = this.wr = 0;
+    }
+
+    // The caller is expected to issue a ReadMessage() after each append().
+    const dataLen = data.length;
+    if (dataLen === 0) return;
+    assertTrue(this.fastpath === undefined);
+    if (this.rd === this.wr) {
+      const msg = ProtoRingBuffer.tryReadMessage(data, 0, dataLen);
+      if (msg !== undefined &&
+          ((msg.byteOffset + msg.length) === (data.byteOffset + dataLen))) {
+        // Fastpath: in many cases, the underlying stream will effectively
+        // preserve the atomicity of messages for most small messages.
+        // In this case we can avoid the extra buffer roundtrip and return the
+        // original array (actually a subarray that skips the proto header).
+        // The next call to ReadMessage() will return this.
+        this.fastpath = msg;
+        return;
+      }
+    }
+
+    let avail = this.buf.length - this.wr;
+    if (dataLen > avail) {
+      // This whole section should be hit extremely rarely.
+
+      // Try first just recompacting the buffer by moving everything to the
+      // left. This can happen if we received "a message and a bit" on each
+      // append() call.
+      this.buf.copyWithin(0, this.rd, this.wr);
+      avail += this.rd;
+      this.wr -= this.rd;
+      this.rd = 0;
+      if (dataLen > avail) {
+        // Still not enough, expand the buffer.
+        let newSize = this.buf.length;
+        while (dataLen > newSize - this.wr) {
+          newSize += kGrowBytes;
+        }
+        assertTrue(newSize <= kMaxMsgSize * 2);
+        const newBuf = new Uint8Array(newSize);
+        newBuf.set(this.buf);
+        this.buf = newBuf;
+        // No need to touch rd / wr.
+      }
+    }
+
+    // Append the received data at the end of the ring buffer.
+    this.buf.set(data, this.wr);
+    this.wr += dataLen;
+  }
+
+  // Tries to extract a message from the ring buffer. If there is no message,
+  // or if the current message is still incomplete, returns undefined.
+  // The caller is expected to call this in a loop until it returns undefined.
+  // Note that a single write to Append() can yield more than one message
+  // (see ProtoRingBufferTest.CoalescingStream in the unittest).
+  readMessage(): Uint8Array|undefined {
+    if (this.fastpath !== undefined) {
+      assertTrue(this.rd === this.wr);
+      const msg = this.fastpath;
+      this.fastpath = undefined;
+      return msg;
+    }
+    assertTrue(this.rd <= this.wr);
+    if (this.rd >= this.wr) {
+      return undefined;  // Completely empty.
+    }
+    const msg = ProtoRingBuffer.tryReadMessage(this.buf, this.rd, this.wr);
+    if (msg === undefined) return undefined;
+    assertTrue(msg.buffer === this.buf.buffer);
+    assertTrue(this.buf.byteOffset === 0);
+    this.rd = msg.byteOffset + msg.length;
+
+    // Deliberately returning a copy of the data with slice(). In various cases
+    // (streaming query response) the caller will hold onto the returned buffer.
+    // If we get to this point, |msg| is a view of the circular buffer that we
+    // will overwrite on the next calls to append().
+    return msg.slice();
+  }
+
+  private static tryReadMessage(
+      data: Uint8Array, dataStart: number, dataEnd: number): Uint8Array
+      |undefined {
+    assertTrue(dataEnd <= data.length);
+    let pos = dataStart;
+    if (pos >= dataEnd) return undefined;
+    const tag = data[pos++];  // Assume one-byte tag.
+    if (tag >= 0x80 || (tag & 0x07) !== 2 /* len delimited */) {
+      throw new Error(
+          `RPC framing error, unexpected tag ${tag} @ offset ${pos - 1}`);
+    }
+
+    let len = 0;
+    for (let shift = 0;; shift += 7) {
+      if (pos >= dataEnd) {
+        return undefined;  // Not enough data to read varint.
+      }
+      const val = data[pos++];
+      len |= ((val & 0x7f) << shift) >>> 0;
+      if (val < 0x80) break;
+    }
+
+    if (len >= kMaxMsgSize) {
+      throw new Error(
+          `RPC framing error, message too large (${len} > ${kMaxMsgSize}`);
+    }
+    const end = pos + len;
+    if (end > dataEnd) return undefined;
+
+    // This is a subarray() and not a slice() because in the |fastpath| case
+    // we want to just return the original buffer pushed by append().
+    // In the slow-path (ring-buffer) case, the readMessage() above will create
+    // a copy via slice() before returning it.
+    return data.subarray(pos, end);
+  }
+}
diff --git a/ui/src/common/proto_ring_buffer_unittest.ts b/ui/src/common/proto_ring_buffer_unittest.ts
new file mode 100644
index 0000000..6c06ae0
--- /dev/null
+++ b/ui/src/common/proto_ring_buffer_unittest.ts
@@ -0,0 +1,140 @@
+// 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 * as protobuf from 'protobufjs/minimal';
+import {assertTrue} from '../base/logging';
+
+import {ProtoRingBuffer} from './proto_ring_buffer';
+
+let seed = 1;
+
+// For reproducibility.
+function Rnd(max: number) {
+  seed = seed * 16807 % 2147483647;
+  return seed % max;
+}
+
+function MakeProtoMessage(fieldId: number, len: number) {
+  const writer = protobuf.Writer.create();
+  const tag = (fieldId << 3) | 2 /*length-delimited*/;
+  assertTrue(tag < 0x80 && (tag & 7) === 2);
+  writer.uint32(tag);
+  const data = new Uint8Array(len);
+  for (let i = 0; i < len; i++) {
+    data[i] = 48 + ((fieldId + i) % 73);
+  }
+  writer.bytes(data);
+  const res = writer.finish();
+  // For whatever reason the object returned by protobufjs' Writer cannot be
+  // directly .toEqual()-ed with Uint8Arrays.
+  const buf = new Uint8Array(res.length);
+  buf.set(res);
+  return buf;
+}
+
+test('ProtoRingBufferTest.Fastpath', () => {
+  const buf = new ProtoRingBuffer();
+
+  for (let rep = 0; rep < 3; rep++) {
+    let inputBuf = MakeProtoMessage(1, 32);
+    buf.append(inputBuf);
+    let msg = buf.readMessage();
+    expect(msg).toBeDefined();
+    expect(msg).toBeInstanceOf(Uint8Array);
+    expect(msg!.length).toBe(32);
+
+    // subarray(2) is to strip the proto preamble. The returned buffer starts at
+    // the start of the payload.
+    expect(msg).toEqual(inputBuf.subarray(2));
+
+    // When we hit the fastpath, the returned message should be a subarray of
+    // the same ArrayBuffer passed to append.
+    expect(msg!.buffer).toBe(inputBuf.buffer);
+
+    inputBuf = MakeProtoMessage(2, 32);
+    buf.append(inputBuf.subarray(0, 13));
+    expect(buf.readMessage()).toBeUndefined();
+    buf.append(inputBuf.subarray(13));
+    msg = buf.readMessage();
+    expect(msg).toBeDefined();
+    expect(msg).toBeInstanceOf(Uint8Array);
+    expect(msg).toEqual(inputBuf.subarray(2));
+    expect(msg!.buffer !== inputBuf.buffer).toBeTruthy();
+  }
+});
+
+test('ProtoRingBufferTest.CoalescingStream', () => {
+  const buf = new ProtoRingBuffer();
+
+  const mergedBuf = new Uint8Array(612);
+  const expected = new Array<Uint8Array>();
+  for (let i = 1, pos = 0; i <= 6; i++) {
+    const msg = MakeProtoMessage(i, 100);
+    expected.push(msg);
+    mergedBuf.set(msg, pos);
+    pos += msg.length;
+  }
+
+  const fragLens = [120, 20, 471, 1];
+  let fragSum = 0;
+  fragLens.map(fragLen => {
+    buf.append(mergedBuf.subarray(fragSum, fragSum + fragLen));
+    fragSum += fragLen;
+    for (;;) {
+      const msg = buf.readMessage();
+      if (msg === undefined) break;
+      const exp = expected.shift();
+      expect(exp).toBeDefined();
+      expect(msg).toEqual(exp!.subarray(-1 * msg.length));
+    }
+  });
+  expect(expected.length).toEqual(0);
+});
+
+
+test('ProtoRingBufferTest.RandomSizes', () => {
+  const buf = new ProtoRingBuffer();
+  const kNumMsg = 100;
+  const mergedBuf = new Uint8Array(1024 * 1024 * 32);
+  const expectedLengths = [];
+  let mergedLen = 0;
+  for (let i = 0; i < kNumMsg; i++) {
+    const fieldId = 1 + Rnd(15);  // We support only one byte tag.
+    const rndVal = Rnd(1024);
+    let len = 1 + rndVal;
+    if ((rndVal % 100) < 5) {
+      len *= 1000;
+    }
+    const msg = MakeProtoMessage(fieldId, len);
+    assertTrue(mergedBuf.length >= mergedLen + msg.length);
+    expectedLengths.push(len);
+    mergedBuf.set(msg, mergedLen);
+    mergedLen += msg.length;
+  }
+
+  for (let fragSum = 0; fragSum < mergedLen;) {
+      let fragLen = 1 + Rnd(1024 * 32);
+      fragLen = Math.min(fragLen, mergedLen - fragSum);
+      buf.append(mergedBuf.subarray(fragSum, fragSum + fragLen));
+      fragSum += fragLen;
+      for (;;) {
+        const msg = buf.readMessage();
+        if (msg === undefined) break;
+        const expLen = expectedLengths.shift();
+        expect(expLen).toBeDefined();
+        expect(msg.length).toEqual(expLen);
+      }
+    }
+  expect(expectedLengths.length).toEqual(0);
+});
diff --git a/ui/src/common/protos.ts b/ui/src/common/protos.ts
index 8b0a5fd..832c838 100644
--- a/ui/src/common/protos.ts
+++ b/ui/src/common/protos.ts
@@ -47,6 +47,7 @@
 
 // Trace Processor protos.
 import IRawQueryArgs = protos.perfetto.protos.IRawQueryArgs;
+import QueryArgs = protos.perfetto.protos.QueryArgs;
 import RawQueryArgs = protos.perfetto.protos.RawQueryArgs;
 import RawQueryResult = protos.perfetto.protos.RawQueryResult;
 import StatusResult = protos.perfetto.protos.StatusResult;
@@ -138,6 +139,7 @@
   MeminfoCounters,
   NativeContinuousDumpConfig,
   ProcessStatsConfig,
+  QueryArgs,
   RawQueryArgs,
   RawQueryResult,
   StatCounters,
diff --git a/ui/src/common/query_iterator.ts b/ui/src/common/query_iterator.ts
index ccea06f..8e1fa81 100644
--- a/ui/src/common/query_iterator.ts
+++ b/ui/src/common/query_iterator.ts
@@ -12,42 +12,74 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {assertTrue} from '../base/logging';
+
 import {RawQueryResult} from './protos';
 
-// Union of all the query result formats that we can turn into forward
-// iterators.
-// TODO(hjd): Replace someOtherEncoding place holder with the real new
-// format.
-type QueryResult = RawQueryResult|{someOtherEncoding: string};
-
-// One row extracted from an SQL result:
-interface Row {
-  [key: string]: string|number|null;
-}
-
-// API:
-// const result = await engine.query("select 42 as n;");
-// const it = getRowIterator({"answer": NUM}, result);
-// for (; it.valid(); it.next()) {
-//   console.log(it.row.answer);
-// }
-export interface RowIterator<T extends Row> {
-  valid(): boolean;
-  next(): void;
-  row: T;
-}
+// These types are used both for the new streaming query iterator and the old
+// columnar RawQueryResult.
 
 export const NUM = 0;
 export const STR = 'str';
 export const NUM_NULL: number|null = 1;
 export const STR_NULL: string|null = 'str_null';
-export type ColumnType =
-    (typeof NUM)|(typeof STR)|(typeof NUM_NULL)|(typeof STR_NULL);
+
+export type ColumnType = string|number|null;
+
+// One row extracted from an SQL result:
+export interface Row {
+  [key: string]: ColumnType;
+}
+
+// The methods that any iterator has to implement.
+export interface RowIteratorBase {
+  valid(): boolean;
+  next(): void;
+}
+
+// A RowIterator is a type that has all the fields defined in the query spec
+// plus the valid() and next() operators. This is to ultimately allow the
+// clients to do:
+// const result = await engine.queryV2("select name, surname, id from people;");
+// const iter = queryResult.iter({name: STR, surname: STR, id: NUM});
+// for (; iter.valid(); iter.next())
+//  console.log(iter.name, iter.surname);
+export type RowIterator<T extends Row> = RowIteratorBase&T;
+
+// The old iterator for non-batched queries. Going away. Usage.
+//   const result = await engine.query("select 42 as n;");
+//   const it = getRowIterator({"answer": NUM}, result);
+//   for (; it.valid(); it.next()) {
+//     console.log(it.row.answer);
+//   }
+export interface LegacyRowIterator<T extends Row> {
+  valid(): boolean;
+  next(): void;
+  row: T;
+}
+
+export function columnTypeToString(t: ColumnType): string {
+  switch (t) {
+    case NUM:
+      return 'NUM';
+    case NUM_NULL:
+      return 'NUM_NULL';
+    case STR:
+      return 'STR';
+    case STR_NULL:
+      return 'STR_NULL';
+    default:
+      return `INVALID(${t})`;
+  }
+}
+
+// TODO(primiano): the types and helpers in the rest of this file are
+// transitional and will be removed once we migrate everything to the streaming
+// query API.
 
 // Exported for testing
 export function findColumnIndex(
-    result: RawQueryResult, name: string, columnType: number|null|string):
-    number {
+    result: RawQueryResult, name: string, columnType: ColumnType): number {
   let matchingDescriptorIndex = -1;
   const disallowNulls = columnType === STR || columnType === NUM;
   const expectsStrings = columnType === STR || columnType === STR_NULL;
@@ -162,31 +194,68 @@
 // Deliberately not exported, use iter() below to make code easy to switch
 // to other queryResult formats.
 function iterFromColumns<T extends Row>(
-    querySpec: T, queryResult: RawQueryResult): RowIterator<T> {
+    querySpec: T, queryResult: RawQueryResult): LegacyRowIterator<T> {
   const iter = new ColumnarRowIterator(querySpec, queryResult);
-  return iter as unknown as RowIterator<T>;
+  return iter as unknown as LegacyRowIterator<T>;
 }
 
+// Deliberately not exported, use iterUntyped() below to make code easy to
+// switch to other queryResult formats.
+function iterUntypedFromColumns(result: RawQueryResult):
+    LegacyRowIterator<Row> {
+  const spec: Row = {};
+  const desc = result.columnDescriptors;
+  for (let i = 0; i < desc.length; ++i) {
+    const name = desc[i].name;
+    if (!name) {
+      continue;
+    }
+    spec[name] = desc[i].type === 3 ? STR_NULL : NUM_NULL;
+  }
+  const iter = new ColumnarRowIterator(spec, result);
+  return iter as unknown as LegacyRowIterator<Row>;
+}
 
-function isColumnarQueryResult(result: QueryResult): result is RawQueryResult {
-  return (result as RawQueryResult).columnDescriptors !== undefined;
+export function iterUntyped(result: RawQueryResult): LegacyRowIterator<Row> {
+  return iterUntypedFromColumns(result);
 }
 
 export function iter<T extends Row>(
-    spec: T, result: QueryResult): RowIterator<T> {
-  if (isColumnarQueryResult(result)) {
-    return iterFromColumns(spec, result);
-  } else {
-    throw new Error('Unsuported format');
-  }
+    spec: T, result: RawQueryResult): LegacyRowIterator<T> {
+  return iterFromColumns(spec, result);
 }
 
-export function slowlyCountRows(result: QueryResult): number {
-  if (isColumnarQueryResult(result)) {
-    // This isn't actually slow for columnar data but it might be for other
-    // formats.
-    return +result.numRecords;
-  } else {
-    throw new Error('Unsuported format');
+export function slowlyCountRows(result: RawQueryResult): number {
+  // This isn't actually slow for columnar data but it might be for other
+  // formats.
+  return +result.numRecords;
+}
+
+export function singleRow<T extends Row>(spec: T, result: RawQueryResult): T|
+    undefined {
+  const numRows = slowlyCountRows(result);
+  if (numRows === 0) {
+    return undefined;
   }
+  if (numRows > 1) {
+    throw new Error(
+        `Attempted to extract single row but more than ${numRows} rows found.`);
+  }
+  const it = iter(spec, result);
+  assertTrue(it.valid());
+  return it.row;
+}
+
+export function singleRowUntyped(result: RawQueryResult): Row|undefined {
+  const numRows = slowlyCountRows(result);
+  if (numRows === 0) {
+    return undefined;
+  }
+  if (numRows > 1) {
+    throw new Error(
+        `Attempted to extract single row but more than ${numRows} rows found.`);
+  }
+  const it = iterUntyped(result);
+  assertTrue(it.valid());
+  return it.row;
 }
diff --git a/ui/src/common/query_result.ts b/ui/src/common/query_result.ts
new file mode 100644
index 0000000..82afc75
--- /dev/null
+++ b/ui/src/common/query_result.ts
@@ -0,0 +1,677 @@
+// 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.
+
+// This file deals with deserialization and iteration of the proto-encoded
+// byte buffer that is returned by TraceProcessor when invoking the
+// TPM_QUERY_STREAMING method. The returned |query_result| buffer is optimized
+// for being moved cheaply across workers and decoded on-the-flight as we step
+// through the iterator.
+// See comments around QueryResult in trace_processor.proto for more details.
+
+// The classes in this file are organized as follows:
+//
+// QueryResultImpl:
+// The object returned by the Engine.queryV2(sql) method.
+// This object is a holder of row data. Batches of raw get appended
+// incrementally as they are received by the remote TraceProcessor instance.
+// QueryResultImpl also deals with asynchronicity of queries and allows callers
+// to obtain a promise that waits for more (or all) rows.
+// At any point in time the following objects hold a reference to QueryResult:
+// - The Engine: for appending row batches.
+// - UI code, typically controllers, who make queries.
+//
+// ResultBatch:
+// Hold the data, returned by the remote TraceProcessor instance, for a number
+// of rows (TP typically chunks the results in batches of 128KB).
+// A QueryResultImpl holds exclusively ResultBatches for a given query.
+// ResultBatch is not exposed externally, it's just an internal representation
+// that helps with proto decoding. ResultBatch is immutable after it gets
+// appended and decoded. The iteration state is held by the RowIteratorImpl.
+//
+// RowIteratorImpl:
+// Decouples the data owned by QueryResultImpl (and its ResultBatch(es)) from
+// the iteration state. The iterator effectively is the union of a ResultBatch
+// and the row number in it. Rows within the batch are decoded as the user calls
+// next(). When getting at the end of the batch, it takes care of switching to
+// the next batch (if any) within the QueryResultImpl.
+// This object is part of the API exposed to tracks / controllers.
+
+import * as protobuf from 'protobufjs/minimal';
+
+import {defer, Deferred} from '../base/deferred';
+import {assertExists, assertFalse, assertTrue} from '../base/logging';
+import {utf8Decode} from '../base/string_utils';
+
+import {
+  columnTypeToString,
+  NUM,
+  NUM_NULL,
+  Row,
+  RowIterator,
+  RowIteratorBase,
+  STR,
+  STR_NULL
+} from './query_iterator';
+
+// Disable Long.js support in protobuf. This seems to be enabled only in tests
+// but not in production code. In any case, for now we want casting to number
+// accepting the 2**53 limitation. This is consistent with passing
+// --force-number in the protobuf.js codegen invocation in //ui/BUILD.gn .
+// See also https://github.com/protobufjs/protobuf.js/issues/1253 .
+(protobuf.util as {} as {Long: undefined}).Long = undefined;
+protobuf.configure();
+
+// This has to match CellType in trace_processor.proto.
+enum CellType {
+  CELL_NULL = 1,
+  CELL_VARINT = 2,
+  CELL_FLOAT64 = 3,
+  CELL_STRING = 4,
+  CELL_BLOB = 5,
+}
+
+const CELL_TYPE_NAMES =
+    ['UNKNOWN', 'NULL', 'VARINT', 'FLOAT64', 'STRING', 'BLOB'];
+
+const TAG_LEN_DELIM = 2;
+
+// This is the interface exposed to readers (e.g. tracks). The underlying object
+// (QueryResultImpl) owns the result data. This allows to obtain iterators on
+// that. In future it will allow to wait for incremental updates (new rows being
+// fetched) for streaming queries.
+export interface QueryResult {
+  // Obtains an iterator.
+  // TODO(primiano): this should have an option to destruct data as we read. In
+  // the case of a long query (e.g. `SELECT * FROM sched` in the query prompt)
+  // we don't want to accumulate everything in memory. OTOH UI tracks want to
+  // keep the data around so they can redraw them on each animation frame. For
+  // now we keep everything in memory in the QueryResultImpl object.
+  // iter<T extends Row>(spec: T): RowIterator<T>;
+  iter<T extends Row>(spec: T): RowIterator<T>;
+
+  // Like iter() for queries that expect only one row. It embeds the valid()
+  // check (i.e. throws if no rows are available) and returns directly the
+  // first result.
+  firstRow<T extends Row>(spec: T): T;
+
+  // If != undefined the query errored out and error() contains the message.
+  error(): string|undefined;
+
+  // Returns the number of rows accumulated so far. Note that this number can
+  // change over time as more batches are received. It becomes stable only
+  // when isComplete() returns true or after waitAllRows() is resolved.
+  numRows(): number;
+
+  // If true all rows have been fetched. Calling iter() will iterate through the
+  // last row. If false, iter() will return an iterator which might iterate
+  // through some rows (or none) but will surely not reach the end.
+
+  isComplete(): boolean;
+
+  // Returns a promise that is resolved only when all rows (i.e. all batches)
+  // have been fetched. The promise return value is always the object iself.
+  waitAllRows(): Promise<QueryResult>;
+
+  // TODO(primiano): next CLs will introduce a waitMoreRows() to allow tracks
+  // to await until some more data (but not necessarily all) is available. For
+  // now everything uses waitAllRows().
+}
+
+// Interface exposed to engine.ts to pump in the data as new row batches arrive.
+export interface WritableQueryResult extends QueryResult {
+  // |resBytes| is a proto-encoded trace_processor.QueryResult message.
+  //  The overall flow looks as follows:
+  // - The user calls engine.queryV2('select ...') and gets a QueryResult back.
+  // - The query call posts a message to the worker that runs the SQL engine (
+  //   or sends a HTTP request in case of the RPC+HTTP interface).
+  // - The returned QueryResult object is initially empty.
+  // - Over time, the sql engine will postMessage() back results in batches.
+  // - Each bach will end up calling this appendResultBatch() method.
+  // - If there is any pending promise (e.g. the caller called
+  //   queryResult.waitAllRows()), this call will awake them (if this is the
+  //   last batch).
+  appendResultBatch(resBytes: Uint8Array): void;
+}
+
+// The actual implementation, which bridges together the reader side and the
+// writer side (the one exposed to the Engine). This is the same object so that
+// when the engine pumps new row batches we can resolve pending promises that
+// readers (e.g. track code) are waiting for.
+class QueryResultImpl implements QueryResult, WritableQueryResult {
+  columnNames: string[] = [];
+  private _error?: string;
+  private _numRows = 0;
+  private _isComplete = false;
+
+  // --- QueryResult implementation.
+
+  // TODO(primiano): for the moment new batches are appended but old batches
+  // are never removed. This won't work with abnormally large result sets, as
+  // it will stash all rows in memory. We could switch to a model where the
+  // iterator is destructive and deletes batch objects once iterating past the
+  // end of each batch. If we do that, than we need to assign monotonic IDs to
+  // batches. Also if we do that, we should prevent creating more than one
+  // iterator for a QueryResult.
+  batches: ResultBatch[] = [];
+
+  // Promise awaiting on waitAllRows(). This should be resolved only when the
+  // last result batch has been been retrieved.
+  private allRowsPromise?: Deferred<QueryResult>;
+
+  isComplete(): boolean {
+    return this._isComplete;
+  }
+  numRows(): number {
+    return this._numRows;
+  }
+  error(): string|undefined {
+    return this._error;
+  }
+
+  iter<T extends Row>(spec: T): RowIterator<T> {
+    const impl = new RowIteratorImplWithRowData(spec, this);
+    return impl as {} as RowIterator<T>;
+  }
+
+  firstRow<T extends Row>(spec: T): T {
+    const impl = new RowIteratorImplWithRowData(spec, this);
+    assertTrue(impl.valid());
+    return impl as {} as RowIterator<T>as T;
+  }
+
+  // Can be called only once.
+  waitAllRows(): Promise<QueryResult> {
+    assertTrue(this.allRowsPromise === undefined);
+    this.allRowsPromise = defer<QueryResult>();
+    if (this._isComplete) {
+      this.resolveOrReject(this.allRowsPromise, this);
+    }
+    return this.allRowsPromise;
+  }
+
+  // --- WritableQueryResult implementation.
+
+  // Called by the engine when a new QueryResult is available. Note that a
+  // single Query() call can yield >1 QueryResult due to result batching
+  // if more than ~64K of data are returned, e.g. when returning O(M) rows.
+  // |resBytes| is a proto-encoded trace_processor.QueryResult message.
+  // It is fine to retain the resBytes without slicing a copy, because
+  // ProtoRingBuffer does the slice() for us (or passes through the buffer
+  // coming from postMessage() (Wasm case) of fetch() (HTTP+RPC case).
+  appendResultBatch(resBytes: Uint8Array) {
+    const reader = protobuf.Reader.create(resBytes);
+    assertTrue(reader.pos === 0);
+    const columnNamesEmptyAtStartOfBatch = this.columnNames.length === 0;
+    while (reader.pos < reader.len) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:  // column_names
+          // Only the first batch should contain the column names. If this fires
+          // something is going wrong in the handling of the batch stream.
+          assertTrue(columnNamesEmptyAtStartOfBatch);
+          this.columnNames.push(reader.string());
+          break;
+        case 2:  // error
+          // The query has errored only if the |error| field is non-empty.
+          // In protos, we don't distinguish between non-present and empty.
+          // Make sure we don't propagate ambiguous empty strings to JS.
+          const err = reader.string();
+          this._error = (err !== undefined && err.length) ? err : undefined;
+          break;
+        case 3:  // batch
+          const batchLen = reader.uint32();
+          const batchRaw = resBytes.subarray(reader.pos, reader.pos + batchLen);
+          reader.pos += batchLen;
+
+          // The ResultBatch ctor parses the CellsBatch submessage.
+          const parsedBatch = new ResultBatch(batchRaw);
+          this.batches.push(parsedBatch);
+          this._isComplete = parsedBatch.isLastBatch;
+
+          // In theory one could construct a valid proto serializing the column
+          // names after the cell batches. In practice the QueryResultSerializer
+          // doesn't do that so it's not worth complicating the code.
+          const numColumns = this.columnNames.length;
+          if (numColumns !== 0) {
+            assertTrue(parsedBatch.numCells % numColumns === 0);
+            this._numRows += parsedBatch.numCells / numColumns;
+          } else {
+            // numColumns == 0 is  plausible for queries like CREATE TABLE ... .
+            assertTrue(parsedBatch.numCells === 0);
+          }
+          break;
+        default:
+          console.warn(`Unexpected QueryResult field ${tag >>> 3}`);
+          reader.skipType(tag & 7);
+          break;
+      }  // switch (tag)
+    }    // while (pos < end)
+
+    if (this._isComplete && this.allRowsPromise !== undefined) {
+      this.resolveOrReject(this.allRowsPromise, this);
+    }
+  }
+
+  ensureAllRowsPromise(): Promise<QueryResult> {
+    if (this.allRowsPromise === undefined) {
+      this.waitAllRows();  // Will populate |this.allRowsPromise|.
+    }
+    return assertExists(this.allRowsPromise);
+  }
+
+  private resolveOrReject(promise: Deferred<QueryResult>, arg: QueryResult) {
+    if (this._error === undefined) {
+      promise.resolve(arg);
+    } else {
+      promise.reject(new Error(this._error));
+    }
+  }
+}
+
+// This class holds onto a received result batch (a Uint8Array) and does some
+// partial parsing to tokenize the various cell groups. This parsing mainly
+// consists of identifying and caching the offsets of each cell group and
+// initializing the varint decoders. This half parsing is done to keep the
+// iterator's next() fast, without decoding everything into memory.
+// This is an internal implementation detail and is not exposed outside. The
+// RowIteratorImpl uses this class to iterate through batches (this class takes
+// care of iterating within a batch, RowIteratorImpl takes care of switching
+// batches when needed).
+// Note: at any point in time there can be more than one ResultIterator
+// referencing the same batch. The batch must be immutable.
+class ResultBatch {
+  readonly isLastBatch: boolean = false;
+  readonly batchBytes: Uint8Array;
+  readonly cellTypesOff: number = 0;
+  readonly cellTypesLen: number = 0;
+  readonly varintOff: number = 0;
+  readonly varintLen: number = 0;
+  readonly float64Cells = new Float64Array();
+  readonly blobCells: Uint8Array[] = [];
+  readonly stringCells: string[] = [];
+
+  // batchBytes is a trace_processor.QueryResult.CellsBatch proto.
+  constructor(batchBytes: Uint8Array) {
+    this.batchBytes = batchBytes;
+    const reader = protobuf.Reader.create(batchBytes);
+    assertTrue(reader.pos === 0);
+    const end = reader.len;
+
+    // Here we deconstruct the proto by hand. The CellsBatch is carefully
+    // designed to allow a very fast parsing from the TS side. We pack all cells
+    // of the same types together, so we can do only one call (per batch) to
+    // TextDecoder.decode(), we can overlay a memory-aligned typedarray for
+    // float values and can quickly tell and type-check the cell types.
+    // One row = N cells (we know the number upfront from the outer message).
+    // Each bach contains always an integer multiple of N cells (i.e. rows are
+    // never fragmented across different batches).
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:  // cell types, a packed array containing one CellType per cell.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);  // Must be packed varint.
+          this.cellTypesLen = reader.uint32();
+          this.cellTypesOff = reader.pos;
+          reader.pos += this.cellTypesLen;
+          break;
+
+        case 2:  // varint_cells, a packed varint buffer.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);  // Must be packed varint.
+          const packLen = reader.uint32();
+          this.varintOff = reader.pos;
+          this.varintLen = packLen;
+          assertTrue(reader.buf === batchBytes);
+          assertTrue(
+              this.varintOff + this.varintLen <=
+              batchBytes.byteOffset + batchBytes.byteLength);
+          reader.pos += packLen;
+          break;
+
+        case 3:  // float64_cells, a 64-bit aligned packed fixed64 buffer.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);  // Must be packed varint.
+          const f64Len = reader.uint32();
+          assertTrue(f64Len % 8 === 0);
+          // Float64Array's constructor is evil: the offset is in bytes but the
+          // length is in 8-byte words.
+          const f64Words = f64Len / 8;
+          const f64Off = batchBytes.byteOffset + reader.pos;
+          if (f64Off % 8 === 0) {
+            this.float64Cells =
+                new Float64Array(batchBytes.buffer, f64Off, f64Words);
+          } else {
+            // When using the production code in trace_processor's rpc.cc, the
+            // float64 should be 8-bytes aligned. The slow-path case is only for
+            // tests.
+            const slice = batchBytes.buffer.slice(f64Off, f64Off + f64Len);
+            this.float64Cells = new Float64Array(slice);
+          }
+          reader.pos += f64Len;
+          break;
+
+        case 4:  // blob_cells: one entry per blob.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);
+          // protobufjs's bytes() under the hoods calls slice() and creates
+          // a copy. Fine here as blobs are rare and not a fastpath.
+          this.blobCells.push(new Uint8Array(reader.bytes()));
+          break;
+
+        case 5:  // string_cells: all the string cells concatenated with \0s.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);
+          const strLen = reader.uint32();
+          assertTrue(reader.pos + strLen <= end);
+          const subArr = batchBytes.subarray(reader.pos, reader.pos + strLen);
+          assertTrue(subArr.length === strLen);
+          // The reason why we do this split rather than creating one string
+          // per entry is that utf8 decoding has some non-negligible cost. See
+          // go/postmessage-benchmark .
+          this.stringCells = utf8Decode(subArr).split('\0');
+          reader.pos += strLen;
+          break;
+
+        case 6:  // is_last_batch (boolean).
+          this.isLastBatch = !!reader.bool();
+          break;
+
+        case 7:  // padding for realignment, skip silently.
+          reader.skipType(tag & 7);
+          break;
+
+        default:
+          console.warn(`Unexpected QueryResult.CellsBatch field ${tag >>> 3}`);
+          reader.skipType(tag & 7);
+          break;
+      }  // switch(tag)
+    }    // while (pos < end)
+  }
+
+  get numCells() {
+    return this.cellTypesLen;
+  }
+}
+
+class RowIteratorImpl implements RowIteratorBase {
+  // The spec passed to the iter call containing the expected types, e.g.:
+  // {'colA': NUM, 'colB': NUM_NULL, 'colC': STRING}.
+  // This doesn't ever change.
+  readonly rowSpec: Row;
+
+  // The object that holds the current row. This points to the parent
+  // RowIteratorImplWithRowData instance that created this class.
+  rowData: Row;
+
+  // The QueryResult object we are reading data from. The engine will pump
+  // batches over time into this object.
+  private resultObj: QueryResultImpl;
+
+  // All the member variables in the group below point to the identically-named
+  // members in result.batch[batchIdx]. This is to avoid indirection layers in
+  // the next() hotpath, so we can do this.float64Cells vs
+  // this.resultObj.batch[this.batchIdx].float64Cells.
+  // These are re-set every time tryMoveToNextBatch() is called (and succeeds).
+  private batchIdx = -1;  // The batch index within |result.batches[]|.
+  private batchBytes = new Uint8Array();
+  private columnNames: string[] = [];
+  private numColumns = 0;
+  private cellTypesEnd = -1;  // -1 so the 1st next() hits tryMoveToNextBatch().
+  private float64Cells = new Float64Array();
+  private varIntReader = protobuf.Reader.create(this.batchBytes);
+  private blobCells: Uint8Array[] = [];
+  private stringCells: string[] = [];
+
+  // These members instead are incremented as we read cells from next(). They
+  // are the mutable state of the iterator.
+  private nextCellTypeOff = 0;
+  private nextFloat64Cell = 0;
+  private nextStringCell = 0;
+  private nextBlobCell = 0;
+  private isValid = false;
+
+  constructor(querySpec: Row, rowData: Row, res: QueryResultImpl) {
+    Object.assign(this, querySpec);
+    this.rowData = rowData;
+    this.rowSpec = {...querySpec};  // ... -> Copy all the key/value pairs.
+    this.resultObj = res;
+    this.next();
+  }
+
+  valid(): boolean {
+    return this.isValid;
+  }
+
+  // Moves the cursor next by one row and updates |isValid|.
+  // When this fails to move, two cases are possible:
+  // 1. We reached the end of the result set (this is the case if
+  //    QueryResult.isComplete() == true when this fails).
+  // 2. We reached the end of the current batch, but more rows might come later
+  //    (if QueryResult.isComplete() == false).
+  next() {
+    // At some point we might reach the end of the current batch, but the next
+    // batch might be available already. In this case we want next() to
+    // transparently move on to the next batch.
+    while (this.nextCellTypeOff + this.numColumns > this.cellTypesEnd) {
+      // If TraceProcessor is behaving well, we should never end up in a
+      // situation where we have leftover cells. TP is expected to serialize
+      // whole rows in each QueryResult batch and NOT truncate them midway.
+      // If this assert fires the TP RPC logic has a bug.
+      assertTrue(
+          this.nextCellTypeOff === this.cellTypesEnd ||
+          this.cellTypesEnd === -1);
+      if (!this.tryMoveToNextBatch()) {
+        this.isValid = false;
+        return;
+      }
+    }
+
+    const rowData = this.rowData;
+    const numColumns = this.numColumns;
+
+    // Read the current row.
+    for (let i = 0; i < numColumns; i++) {
+      const cellType = this.batchBytes[this.nextCellTypeOff++];
+      const colName = this.columnNames[i];
+
+      switch (cellType) {
+        case CellType.CELL_NULL:
+          rowData[colName] = null;
+          break;
+
+        case CellType.CELL_VARINT:
+          const val = this.varIntReader.int64();
+          // This is very subtle. The return type of int64 can be either a
+          // number or a Long.js {high:number, low:number} if Long.js support is
+          // enabled. The default state seems different in node and browser.
+          // We force-disable Long.js support in the top of this source file.
+          rowData[colName] = val as {} as number;
+          break;
+
+        case CellType.CELL_FLOAT64:
+          rowData[colName] = this.float64Cells[this.nextFloat64Cell++];
+          break;
+
+        case CellType.CELL_STRING:
+          rowData[colName] = this.stringCells[this.nextStringCell++];
+          break;
+
+        case CellType.CELL_BLOB:
+          const blob = this.blobCells[this.nextBlobCell++];
+          throw new Error(`TODO implement BLOB support (${blob})`);
+          // outRow[colName] = blob;
+          break;
+
+        default:
+          throw new Error(`Invalid cell type ${cellType}`);
+      }
+    }  // For (cells)
+    this.isValid = true;
+  }
+
+  private tryMoveToNextBatch(): boolean {
+    const nextBatchIdx = this.batchIdx + 1;
+    if (nextBatchIdx >= this.resultObj.batches.length) {
+      return false;
+    }
+
+    this.columnNames = this.resultObj.columnNames;
+    this.numColumns = this.columnNames.length;
+
+    this.batchIdx = nextBatchIdx;
+    const batch = assertExists(this.resultObj.batches[nextBatchIdx]);
+    this.batchBytes = batch.batchBytes;
+    this.nextCellTypeOff = batch.cellTypesOff;
+    this.cellTypesEnd = batch.cellTypesOff + batch.cellTypesLen;
+    this.float64Cells = batch.float64Cells;
+    this.blobCells = batch.blobCells;
+    this.stringCells = batch.stringCells;
+    this.varIntReader = protobuf.Reader.create(batch.batchBytes);
+    this.varIntReader.pos = batch.varintOff;
+    this.varIntReader.len = batch.varintOff + batch.varintLen;
+    this.nextFloat64Cell = 0;
+    this.nextStringCell = 0;
+    this.nextBlobCell = 0;
+
+    // Check that all the expected columns are present.
+    for (const expectedCol of Object.keys(this.rowSpec)) {
+      if (this.columnNames.indexOf(expectedCol) < 0) {
+        throw new Error(
+            `Column ${expectedCol} not found in the SQL result ` +
+            `set {${this.columnNames.join(' ')}}`);
+      }
+    }
+
+    // Check that the cells types are consistent.
+    const numColumns = this.numColumns;
+    if (numColumns === 0) {
+      assertTrue(batch.numCells === 0);
+    } else {
+      for (let i = this.nextCellTypeOff; i < this.cellTypesEnd; i++) {
+        const col = (i - this.nextCellTypeOff) % numColumns;
+        const colName = this.columnNames[col];
+        const actualType = this.batchBytes[i] as CellType;
+        const expType = this.rowSpec[colName];
+
+        // If undefined, the caller doesn't want to read this column at all, so
+        // it can be whatever.
+        if (expType === undefined) continue;
+
+        let err = '';
+        if (actualType === CellType.CELL_NULL &&
+            (expType !== STR_NULL && expType !== NUM_NULL)) {
+          err = 'SQL value is NULL but that was not expected' +
+              ` (expected type: ${columnTypeToString(expType)}).` +
+              'Did you intend to use NUM_NULL or STRING_NULL?';
+        } else if (
+            ((actualType === CellType.CELL_VARINT ||
+              actualType === CellType.CELL_FLOAT64) &&
+             (expType !== NUM && expType !== NUM_NULL)) ||
+            ((actualType === CellType.CELL_STRING) &&
+             (expType !== STR && expType !== STR_NULL))) {
+          err = `Incompatible cell type. Expected: ${
+              columnTypeToString(
+                  expType)} actual: ${CELL_TYPE_NAMES[actualType]}`;
+        }
+        if (err.length > 0) {
+          throw new Error(
+              `Error @ row: ${Math.floor(i / numColumns)} col: '` +
+              `${colName}': ${err}`);
+        }
+      }
+    }
+    return true;
+  }
+}
+
+// This is the object ultimately returned to the client when calling
+// QueryResult.iter(...).
+// The only reason why this is disjoint from RowIteratorImpl is to avoid
+// naming collisions between the members variables required by RowIteratorImpl
+// and the column names returned by the iterator.
+class RowIteratorImplWithRowData implements RowIteratorBase {
+  private _impl: RowIteratorImpl;
+
+  next: () => void;
+  valid: () => boolean;
+
+  constructor(querySpec: Row, res: QueryResultImpl) {
+    const thisAsRow = this as {} as Row;
+    Object.assign(thisAsRow, querySpec);
+    this._impl = new RowIteratorImpl(querySpec, thisAsRow, res);
+    this.next = this._impl.next.bind(this._impl);
+    this.valid = this._impl.valid.bind(this._impl);
+  }
+}
+
+// This is a proxy object that wraps QueryResultImpl, adding await-ability.
+// This is so that:
+// 1. Clients that just want to await for the full result set can just call
+//    await engine.query('...') and will get a QueryResult that is guaranteed
+//    to be complete.
+// 2. Clients that know how to handle the streaming can use it straight away.
+class WaitableQueryResultImpl implements QueryResult, WritableQueryResult,
+                                         PromiseLike<QueryResult> {
+  private impl = new QueryResultImpl();
+  private thenCalled = false;
+
+  // QueryResult implementation. Proxies all calls to the impl object.
+  iter<T extends Row>(spec: T) {
+     return this.impl.iter(spec);
+  }
+  firstRow<T extends Row>(spec: T) {
+     return this.impl.firstRow(spec);
+  }
+  waitAllRows() {
+     return this.impl.waitAllRows();
+  }
+  isComplete() {
+     return this.impl.isComplete();
+  }
+  numRows() {
+     return this.impl.numRows();
+  }
+  error() {
+     return this.impl.error();
+  }
+
+  // WritableQueryResult implementation.
+  appendResultBatch(resBytes: Uint8Array) {
+    return this.impl.appendResultBatch(resBytes);
+  }
+
+  // PromiseLike<QueryResult> implementaton.
+
+  // tslint:disable-next-line no-any
+  then(onfulfilled: any, onrejected: any): any {
+    assertFalse(this.thenCalled);
+    this.thenCalled = true;
+    return this.impl.ensureAllRowsPromise().then(onfulfilled, onrejected);
+  }
+
+  // tslint:disable-next-line no-any
+  catch(error: any): any {
+    return this.impl.ensureAllRowsPromise().catch(error);
+  }
+
+  // tslint:disable-next-line no-any
+  finally(callback: () => void): any {
+    return this.impl.ensureAllRowsPromise().finally(callback);
+  }
+
+  get[Symbol.toStringTag](): string {
+    return 'Promise<WaitableQueryResult>';
+  }
+}
+
+export function createQueryResult(): QueryResult&Promise<QueryResult>&
+    WritableQueryResult {
+  return new WaitableQueryResultImpl();
+}
diff --git a/ui/src/common/query_result_unittest.ts b/ui/src/common/query_result_unittest.ts
new file mode 100644
index 0000000..bacf480
--- /dev/null
+++ b/ui/src/common/query_result_unittest.ts
@@ -0,0 +1,260 @@
+// 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 * as protoNamespace from '../gen/protos';
+
+import {NUM, NUM_NULL, STR, STR_NULL} from './query_iterator';
+import {createQueryResult} from './query_result';
+
+const T = protoNamespace.perfetto.protos.QueryResult.CellsBatch.CellType;
+const QueryResultProto = protoNamespace.perfetto.protos.QueryResult;
+
+test('QueryResult.SimpleOneRow', () => {
+  const batch = QueryResultProto.CellsBatch.create({
+    cells: [T.CELL_STRING, T.CELL_VARINT, T.CELL_STRING, T.CELL_FLOAT64],
+    varintCells: [42],
+    stringCells: ['the foo', 'the bar'].join('\0'),
+    float64Cells: [42.42],
+    isLastBatch: true,
+  });
+  const resProto = QueryResultProto.create({
+    columnNames: ['a_str', 'b_int', 'c_str', 'd_float'],
+    batch: [batch],
+  });
+
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  expect(qr.isComplete()).toBe(true);
+  expect(qr.numRows()).toBe(1);
+
+  // First try iterating without selecting any column.
+  {
+    const iter = qr.iter({});
+    expect(iter.valid()).toBe(true);
+    iter.next();
+    expect(iter.valid()).toBe(false);
+  }
+
+  // Then select only two of them.
+  {
+    const iter = qr.iter({c_str: STR, d_float: NUM});
+    expect(iter.valid()).toBe(true);
+    expect(iter.c_str).toBe('the bar');
+    expect(iter.d_float).toBeCloseTo(42.42);
+    iter.next();
+    expect(iter.valid()).toBe(false);
+  }
+
+  // If a column is not present in the result set, iter() should throw.
+  expect(() => qr.iter({nx: NUM})).toThrowError(/\bnx\b.*not found/);
+});
+
+test('QueryResult.BigNumbers', () => {
+  const numAndExpectedStr = [
+    [0, '0'],
+    [-1, '-1'],
+    [-1000, '-1000'],
+    [1e12, '1000000000000'],
+    [1e12 * -1, '-1000000000000'],
+    [((1 << 31) - 1) | 0, '2147483647'],
+    [1 << 31, '-2147483648'],
+    [Number.MAX_SAFE_INTEGER, '9007199254740991'],
+    [Number.MIN_SAFE_INTEGER, '-9007199254740991'],
+  ];
+  const batch = QueryResultProto.CellsBatch.create({
+    cells: new Array<number>(numAndExpectedStr.length).fill(T.CELL_VARINT),
+    varintCells: numAndExpectedStr.map(x => x[0]) as number[],
+    isLastBatch: true,
+  });
+  const resProto = QueryResultProto.create({
+    columnNames: ['n'],
+    batch: [batch],
+  });
+
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  const actual: string[] = [];
+  for (const iter = qr.iter({n: NUM}); iter.valid(); iter.next()) {
+    actual.push(BigInt(iter.n).toString());
+  }
+  expect(actual).toEqual(numAndExpectedStr.map(x => x[1]) as string[]);
+});
+
+test('QueryResult.Floats', () => {
+  const floats = [
+    0.0,
+    1.0,
+    -1.0,
+    3.14159265358,
+    Number.MIN_SAFE_INTEGER,
+    Number.MAX_SAFE_INTEGER,
+    Number.NEGATIVE_INFINITY,
+    Number.POSITIVE_INFINITY,
+    Number.NaN,
+  ];
+  const batch = QueryResultProto.CellsBatch.create({
+    cells: new Array<number>(floats.length).fill(T.CELL_FLOAT64),
+    float64Cells: floats,
+    isLastBatch: true,
+  });
+  const resProto = QueryResultProto.create({
+    columnNames: ['n'],
+    batch: [batch],
+  });
+
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  const actual: number[] = [];
+  for (const iter = qr.iter({n: NUM}); iter.valid(); iter.next()) {
+    actual.push(iter.n);
+  }
+  expect(actual).toEqual(floats);
+});
+
+test('QueryResult.Strings', () => {
+  const strings = [
+    'a',
+    '',
+    '',
+    'hello world',
+    'In einem Bächlein helle da schoß in froher Eil',
+    '色は匂へど散りぬるを我が世誰ぞ常ならん有為の奥山今日越えて浅き夢見じ酔ひもせず'
+  ];
+  const batch = QueryResultProto.CellsBatch.create({
+    cells: new Array<number>(strings.length).fill(T.CELL_STRING),
+    stringCells: strings.join('\0'),
+    isLastBatch: true,
+  });
+  const resProto = QueryResultProto.create({
+    columnNames: ['s'],
+    batch: [batch],
+  });
+
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  const actual: string[] = [];
+  for (const iter = qr.iter({s: STR}); iter.valid(); iter.next()) {
+    actual.push(iter.s);
+  }
+  expect(actual).toEqual(strings);
+});
+
+test('QueryResult.NullChecks', () => {
+  const cells: number[] = [];
+  cells.push(T.CELL_VARINT, T.CELL_NULL);
+  cells.push(T.CELL_NULL, T.CELL_STRING);
+  cells.push(T.CELL_VARINT, T.CELL_STRING);
+  const batch = QueryResultProto.CellsBatch.create({
+    cells,
+    varintCells: [1, 2],
+    stringCells: ['a', 'b'].join('\0'),
+    isLastBatch: true,
+  });
+  const resProto = QueryResultProto.create({
+    columnNames: ['n', 's'],
+    batch: [batch],
+  });
+
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  const actualNums = new Array<number|null>();
+  const actualStrings = new Array<string|null>();
+  for (const iter = qr.iter({n: NUM_NULL, s: STR_NULL}); iter.valid();
+       iter.next()) {
+    actualNums.push(iter.n);
+    actualStrings.push(iter.s);
+  }
+  expect(actualNums).toEqual([1, null, 2]);
+  expect(actualStrings).toEqual([null, 'a', 'b']);
+
+  // Check that using NUM / STR throws.
+  expect(() => qr.iter({n: NUM_NULL, s: STR}))
+      .toThrowError(/col: 's'.*is NULL.*not expected/);
+  expect(() => qr.iter({n: NUM, s: STR_NULL}))
+      .toThrowError(/col: 'n'.*is NULL.*not expected/);
+  expect(qr.iter({n: NUM_NULL})).toBeTruthy();
+  expect(qr.iter({s: STR_NULL})).toBeTruthy();
+});
+
+test('QueryResult.EarlyError', () => {
+  const resProto = QueryResultProto.create({
+    columnNames: ['n', 's'],
+    batch: [{isLastBatch: true}],
+    error: 'Oh dear, this SQL query is too complicated, I give up',
+  });
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  expect(qr.error()).toContain('Oh dear');
+  expect(qr.isComplete()).toBe(true);
+});
+
+test('QueryResult.LateError', () => {
+  const resProto = QueryResultProto.create({
+    columnNames: ['n'],
+    batch: [
+      {
+        cells: [T.CELL_VARINT],
+        varintCells: [1],
+      },
+      {
+        cells: [T.CELL_VARINT],
+        varintCells: [2],
+        isLastBatch: true,
+      },
+    ],
+    error: 'I tried, I was getting there, but then I failed',
+  });
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  expect(qr.error()).toContain('I failed');
+  const rows: number[] = [];
+  for (const iter = qr.iter({n: NUM}); iter.valid(); iter.next()) {
+    rows.push(iter.n);
+  }
+  expect(rows).toEqual([1, 2]);
+  expect(qr.isComplete()).toBe(true);
+});
+
+
+test('QueryResult.MultipleBatches', async () => {
+  const batch1 = QueryResultProto.create({
+    columnNames: ['n'],
+    batch: [{
+      cells: [T.CELL_VARINT],
+      varintCells: [1],
+      isLastBatch: false,
+    }],
+  });
+  const batch2 = QueryResultProto.create({
+    batch: [{
+      cells: [T.CELL_VARINT],
+      varintCells: [2],
+      isLastBatch: true,
+    }],
+  });
+
+  const qr = createQueryResult();
+  expect(qr.isComplete()).toBe(false);
+
+  qr.appendResultBatch(QueryResultProto.encode(batch1).finish());
+  qr.appendResultBatch(QueryResultProto.encode(batch2).finish());
+
+  const awaitRes = await qr;
+
+  expect(awaitRes.isComplete()).toBe(true);
+  expect(qr.isComplete()).toBe(true);
+
+  expect(awaitRes.numRows()).toBe(2);
+  expect(qr.numRows()).toBe(2);
+});
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index e48fb87..e38f4b2 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -51,7 +51,8 @@
 
 export const MAX_TIME = 180;
 
-export const STATE_VERSION = 2;
+// 3: TrackKindPriority and related sorting changes.
+export const STATE_VERSION = 3;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -59,6 +60,13 @@
 
 export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE'|'FORCE_BUILTIN_WASM';
 
+export enum TrackKindPriority {
+  'MAIN_THREAD' = 0,
+  'RENDER_THREAD' = 1,
+  'GPU_COMPLETION' = 2,
+  'ORDINARY' = 3
+}
+
 export type HeapProfileFlamegraphViewingOption =
     'SPACE'|'ALLOC_SPACE'|'OBJECTS'|'ALLOC_OBJECTS';
 
@@ -72,6 +80,7 @@
   mapping: string;
   merged: boolean;
   highlighted: boolean;
+  location?: string;
 }
 
 export interface TraceFileSource {
@@ -83,6 +92,7 @@
   type: 'ARRAY_BUFFER';
   title: string;
   url?: string;
+  fileName?: string;
   buffer: ArrayBuffer;
 }
 
@@ -103,7 +113,7 @@
   engineId: string;
   kind: string;
   name: string;
-  isMainThread: boolean;
+  trackKindPriority: TrackKindPriority;
   trackGroup?: string;
   config: {};
 }
@@ -365,6 +375,17 @@
   return false;
 }
 
+export function hasActiveProbes(config: RecordConfig) {
+  const fieldsWithEmptyResult = new Set<string>(['hpBlockClient']);
+  for (const key in config) {
+    if (typeof (config[key]) === 'boolean' && config[key] === true &&
+        !fieldsWithEmptyResult.has(key)) {
+      return true;
+    }
+  }
+  return false;
+}
+
 export interface RecordConfig {
   [key: string]: null|number|boolean|string|string[];
 
@@ -396,6 +417,7 @@
   ftraceDrainPeriodMs: number;
   androidLogs: boolean;
   androidLogBuffers: string[];
+  androidFrameTimeline: boolean;
 
   batteryDrain: boolean;
   batteryDrainPollMs: number;
@@ -458,6 +480,7 @@
     ftraceDrainPeriodMs: 250,
     androidLogs: false,
     androidLogBuffers: [],
+    androidFrameTimeline: false,
 
     cpuCoarse: false,
     cpuCoarsePollMs: 1000,
@@ -516,11 +539,13 @@
     'accessibility',
     'AccountFetcherService',
     'android_webview',
+    'aogh',
     'audio',
     'base',
     'benchmark',
     'blink',
     'blink.animations',
+    'blink.bindings',
     'blink.console',
     'blink_gc',
     'blink.net',
@@ -531,8 +556,13 @@
     'browser',
     'browsing_data',
     'CacheStorage',
+    'Calculators',
+    'CameraStream',
     'camera',
+    'cast_app',
     'cast_perf_test',
+    'cast.mdns',
+    'cast.mdns.socket',
     'cast.stream',
     'cc',
     'cc.debug',
@@ -542,14 +572,17 @@
     'compositor',
     'content',
     'content_capture',
+    'device',
     'devtools',
+    'devtools.contrast',
     'devtools.timeline',
+    'disk_cache',
     'download',
     'download_service',
     'drm',
     'drmcursor',
     'dwrite',
-    'DXVA Decoding',
+    'DXVA_Decoding',
     'EarlyJava',
     'evdev',
     'event',
@@ -560,10 +593,12 @@
     'fonts',
     'GAMEPAD',
     'gpu',
+    'gpu.angle',
     'gpu.capture',
     'headless',
     'hwoverlays',
     'identity',
+    'ime',
     'IndexedDB',
     'input',
     'io',
@@ -591,12 +626,17 @@
     'omnibox',
     'oobe',
     'ozone',
+    'partition_alloc',
     'passwords',
     'p2p',
     'page-serialization',
+    'paint_preview',
     'pepper',
+    'PlatformMalloc',
+    'power',
     'ppapi',
-    'ppapi proxy',
+    'ppapi_proxy',
+    'print',
     'rail',
     'renderer',
     'renderer_host',
@@ -607,21 +647,26 @@
     'sequence_manager',
     'service_manager',
     'ServiceWorker',
+    'sharing',
     'shell',
     'shortcut_viewer',
     'shutdown',
     'SiteEngagement',
     'skia',
+    'sql',
+    'stadia_media',
+    'stadia_rtc',
     'startup',
     'sync',
     'sync_lock_contention',
-    'thread_pool',
     'test_gpu',
-    'test_tracing',
+    'thread_pool',
     'toplevel',
+    'toplevel.flow',
     'ui',
     'v8',
     'v8.execute',
+    'v8.wasm',
     'ValueStoreFrontend::Backend',
     'views',
     'views.frame',
@@ -634,6 +679,7 @@
     'webrtc',
     'xr',
     'disabled-by-default-animation-worklet',
+    'disabled-by-default-audio',
     'disabled-by-default-audio-worklet',
     'disabled-by-default-blink.debug',
     'disabled-by-default-blink.debug.display_lock',
@@ -651,6 +697,7 @@
     'disabled-by-default-cc.debug.scheduler',
     'disabled-by-default-cc.debug.scheduler.frames',
     'disabled-by-default-cc.debug.scheduler.now',
+    'disabled-by-default-content.verbose',
     'disabled-by-default-cpu_profiler',
     'disabled-by-default-cpu_profiler.debug',
     'disabled-by-default-devtools.screenshot',
@@ -665,34 +712,41 @@
     'disabled-by-default-gpu_cmd_queue',
     'disabled-by-default-gpu.dawn',
     'disabled-by-default-gpu.debug',
-    'disabled-by-default-gpu_decoder',
+    'disabled-by-default-gpu.decoder',
     'disabled-by-default-gpu.device',
     'disabled-by-default-gpu.service',
+    'disabled-by-default-gpu.vulkan.vma',
     'disabled-by-default-histogram_samples',
     'disabled-by-default-ipc.flow',
     'disabled-by-default-java-heap-profiler',
     'disabled-by-default-layer-element',
+    'disabled-by-default-layout_shift.debug',
     'disabled-by-default-lifecycles',
     'disabled-by-default-loading',
+    'disabled-by-default-mediastream',
     'disabled-by-default-memory-infra',
     'disabled-by-default-memory-infra.v8.code_stats',
+    'disabled-by-default-mojom',
     'disabled-by-default-net',
     'disabled-by-default-network',
     'disabled-by-default-paint-worklet',
     'disabled-by-default-power',
     'disabled-by-default-renderer.scheduler',
     'disabled-by-default-renderer.scheduler.debug',
+    'disabled-by-default-sandbox',
     'disabled-by-default-sequence_manager',
     'disabled-by-default-sequence_manager.debug',
     'disabled-by-default-sequence_manager.verbose_snapshots',
     'disabled-by-default-skia',
     'disabled-by-default-skia.gpu',
     'disabled-by-default-skia.gpu.cache',
+    'disabled-by-default-skia.shaders',
     'disabled-by-default-SyncFileSystem',
     'disabled-by-default-system_stats',
     'disabled-by-default-thread_pool_diagnostics',
     'disabled-by-default-toplevel.flow',
     'disabled-by-default-toplevel.ipc',
+    'disabled-by-default-user_action_samples',
     'disabled-by-default-v8.compile',
     'disabled-by-default-v8.cpu_profiler',
     'disabled-by-default-v8.cpu_profiler.hires',
@@ -702,17 +756,24 @@
     'disabled-by-default-v8.runtime',
     'disabled-by-default-v8.runtime_stats',
     'disabled-by-default-v8.runtime_stats_sampling',
+    'disabled-by-default-v8.stack_trace',
     'disabled-by-default-v8.turbofan',
     'disabled-by-default-v8.wasm',
+    'disabled-by-default-v8.wasm.detailed',
+    'disabled-by-default-v8.wasm.turbofan',
     'disabled-by-default-video_and_image_capture',
     'disabled-by-default-viz.debug.overlay_planes',
+    'disabled-by-default-viz.gpu_composite_time',
     'disabled-by-default-viz.hit_testing_flow',
     'disabled-by-default-viz.overdraw',
     'disabled-by-default-viz.quads',
     'disabled-by-default-viz.surface_id_flow',
     'disabled-by-default-viz.surface_lifetime',
     'disabled-by-default-viz.triangles',
+    'disabled-by-default-webaudio.audionode',
+    'disabled-by-default-webrtc',
     'disabled-by-default-worker.scheduler',
+    'disabled-by-default-xr.debug',
   ];
 }
 
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
index 60d6835..2f94a6e 100644
--- a/ui/src/common/state_unittest.ts
+++ b/ui/src/common/state_unittest.ts
@@ -12,7 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {createEmptyState, getContainingTrackId, State} from './state';
+import {
+  createEmptyState,
+  getContainingTrackId,
+  State,
+  TrackKindPriority
+} from './state';
 
 test('createEmptyState', () => {
   const state: State = createEmptyState();
@@ -26,7 +31,7 @@
     engineId: 'engine',
     kind: 'Foo',
     name: 'a track',
-    isMainThread: false,
+    trackKindPriority: TrackKindPriority.ORDINARY,
     config: {},
   };
 
@@ -35,7 +40,7 @@
     engineId: 'engine',
     kind: 'Foo',
     name: 'b track',
-    isMainThread: false,
+    trackKindPriority: TrackKindPriority.ORDINARY,
     config: {},
     trackGroup: 'containsB',
   };
diff --git a/ui/src/common/wasm_engine_proxy.ts b/ui/src/common/wasm_engine_proxy.ts
index 43f7b85..5f7715d 100644
--- a/ui/src/common/wasm_engine_proxy.ts
+++ b/ui/src/common/wasm_engine_proxy.ts
@@ -12,145 +12,33 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {defer, Deferred} from '../base/deferred';
 import {assertTrue} from '../base/logging';
-import {WasmBridgeRequest, WasmBridgeResponse} from '../engine/wasm_bridge';
-
 import {Engine, LoadingTracker} from './engine';
 
-const activeWorkers = new Map<string, Worker>();
-let warmWorker: null|Worker = null;
-
-function createWorker(): Worker {
-  return new Worker('engine_bundle.js');
-}
-
-// Take the warm engine and start creating a new WASM engine in the background
-// for the next call.
-export function createWasmEngine(id: string): Worker {
-  if (warmWorker === null) {
-    throw new Error('warmupWasmEngine() not called');
-  }
-  if (activeWorkers.has(id)) {
-    throw new Error(`Duplicate worker ID ${id}`);
-  }
-  const activeWorker = warmWorker;
-  warmWorker = createWorker();
-  activeWorkers.set(id, activeWorker);
-  return activeWorker;
-}
-
-export function destroyWasmEngine(id: string) {
-  if (!activeWorkers.has(id)) {
-    throw new Error(`Cannot find worker ID ${id}`);
-  }
-  activeWorkers.get(id)!.terminate();
-  activeWorkers.delete(id);
-}
-
-/**
- * It's quite slow to compile WASM and (in Chrome) this happens every time
- * a worker thread attempts to load a WASM module since there is no way to
- * cache the compiled code currently. To mitigate this we can always keep a
- * WASM backend 'ready to go' just waiting to be provided with a trace file.
- * warmupWasmEngineWorker (together with getWasmEngineWorker)
- * implement this behaviour.
- */
-export function warmupWasmEngine(): void {
-  if (warmWorker !== null) {
-    throw new Error('warmupWasmEngine() already called');
-  }
-  warmWorker = createWorker();
-}
-
-interface PendingRequest {
-  id: number;
-  respHandler: Deferred<Uint8Array>;
-}
-
 /**
  * This implementation of Engine uses a WASM backend hosted in a separate
  * worker thread.
  */
 export class WasmEngineProxy extends Engine {
   readonly id: string;
-  private readonly worker: Worker;
-  private pendingRequests = new Array<PendingRequest>();
-  private nextRequestId = 0;
+  private port: MessagePort;
 
-  constructor(id: string, worker: Worker, loadingTracker?: LoadingTracker) {
+  constructor(id: string, port: MessagePort, loadingTracker?: LoadingTracker) {
     super(loadingTracker);
     this.id = id;
-    this.worker = worker;
-    this.worker.onmessage = this.onMessage.bind(this);
-  }
-
-  async parse(reqData: Uint8Array): Promise<void> {
-    // We don't care about the response data (the method is actually a void). We
-    // just want to linearize and wait for the call to have been completed on
-    // the worker.
-    await this.queueRequest('trace_processor_parse', reqData);
-  }
-
-  async notifyEof(): Promise<void> {
-    // We don't care about the response data (the method is actually a void). We
-    // just want to linearize and wait for the call to have been completed on
-    // the worker.
-    await this.queueRequest('trace_processor_notify_eof', new Uint8Array());
-  }
-
-  restoreInitialTables(): Promise<void> {
-    // We should never get here, restoreInitialTables() should be called only
-    // when using the HttpRpcEngine.
-    throw new Error('restoreInitialTables() not supported by the WASM engine');
-  }
-
-  rawQuery(rawQueryArgs: Uint8Array): Promise<Uint8Array> {
-    return this.queueRequest('trace_processor_raw_query', rawQueryArgs);
-  }
-
-  rawComputeMetric(rawComputeMetric: Uint8Array): Promise<Uint8Array> {
-    return this.queueRequest(
-        'trace_processor_compute_metric', rawComputeMetric);
-  }
-
-  async enableMetatrace(): Promise<void> {
-    await this.queueRequest(
-        'trace_processor_enable_metatrace', new Uint8Array());
-  }
-
-  disableAndReadMetatrace(): Promise<Uint8Array> {
-    return this.queueRequest(
-        'trace_processor_disable_and_read_metatrace', new Uint8Array());
-  }
-
-  // Enqueues a request to the worker queue via postMessage(). The returned
-  // promised will be resolved once the worker replies to the postMessage()
-  // with the paylad of the response, a proto-encoded object which wraps the
-  // method return value (e.g., RawQueryResult for SQL query results).
-  private queueRequest(methodName: string, reqData: Uint8Array):
-      Deferred<Uint8Array> {
-    const respHandler = defer<Uint8Array>();
-    const id = this.nextRequestId++;
-    const request: WasmBridgeRequest = {id, methodName, data: reqData};
-    this.pendingRequests.push({id, respHandler});
-    this.worker.postMessage(request);
-    return respHandler;
+    this.port = port;
+    this.port.onmessage = this.onMessage.bind(this);
   }
 
   onMessage(m: MessageEvent) {
-    const response = m.data as WasmBridgeResponse;
-    assertTrue(this.pendingRequests.length > 0);
-    const request = this.pendingRequests.shift()!;
+    assertTrue(m.data instanceof Uint8Array);
+    super.onRpcResponseBytes(m.data as Uint8Array);
+  }
 
-    // Requests should be executed and ACKed by the worker in the same order
-    // they came in.
-    assertTrue(request.id === response.id);
-
-    // If the Wasm call fails (e.g. hits a PERFETTO_CHECK) it will throw an
-    // error in wasm_bridge.ts and show the crash dialog. In no case we can
-    // gracefully handle a Wasm crash, so we fail fast there rather than
-    // propagating the error here rejecting the promise.
-    request.respHandler.resolve(response.data);
+  rpcSendRequestBytes(data: Uint8Array): void {
+    // We deliberately don't use a transfer list because protobufjs reuses the
+    // same buffer when encoding messages (which is good, because creating a new
+    // TypedArray for each decode operation would be too expensive).
+    this.port.postMessage(data);
   }
 }
diff --git a/ui/src/common/worker_messages.ts b/ui/src/common/worker_messages.ts
new file mode 100644
index 0000000..b60a656
--- /dev/null
+++ b/ui/src/common/worker_messages.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.
+
+// This file defines the API of messages exchanged between frontend and
+// {engine, controller} worker when bootstrapping the workers.
+// Those messages are sent only once. The rest of the communication happens
+// over the MessagePort(s) that are sent in the init message.
+
+// This is so we can create all the workers in a central place in the frontend
+// (Safari still doesn't spawning workers from other workers) but then let them
+// communicate by sending the right MessagePort to them.
+
+// Frontend -> Engine initialization message.
+export interface EngineWorkerInitMessage {
+  // The port used to receive engine messages (e.g., query commands).
+  // The controller owns the other end of the MessageChannel
+  // (see resetEngineWorker()).
+  enginePort: MessagePort;
+}
+
+// Frontend -> Controller initialization message.
+export interface ControllerWorkerInitMessage {
+  // For receiving dispatch() commands from the frontend. This is where most of
+  // the frontend <> controller interaction happens.
+  controllerPort: MessagePort;
+
+  // For publishing results back to the frontend. This is used for one-way
+  // non-retained publish() operations (e.g. track data after a query).
+  frontendPort: MessagePort;
+
+  // For controller <> Chrome extension communication.
+  extensionPort: MessagePort;
+
+  // For reporting errors back to the frontend. This is a dedicated port to
+  // reduce depdencies on the business logic behind the other ports.
+  errorReportingPort: MessagePort;
+}
diff --git a/ui/src/controller/adb.ts b/ui/src/controller/adb.ts
index 5ed6152..5c1cdbb 100644
--- a/ui/src/controller/adb.ts
+++ b/ui/src/controller/adb.ts
@@ -114,8 +114,6 @@
     this.key = await AdbOverWebUsb.initKey();
 
     await this.dev.open();
-    await this.dev.reset();  // The reset is done so that we can claim the
-                             // device before adb server can.
 
     const {configValue, usbInterfaceNumber, endpoints} =
         this.findInterfaceAndEndpoint();
@@ -352,9 +350,7 @@
     };
 
     const key = await crypto.subtle.generateKey(
-                    keySpec, /*extractable=*/ true, ['sign', 'verify']) as
-        CryptoKeyPair;
-
+        keySpec, /*extractable=*/ true, ['sign', 'verify']);
     return key;
   }
 
diff --git a/ui/src/controller/adb_socket_controller.ts b/ui/src/controller/adb_socket_controller.ts
index f517f7a..88440b5 100644
--- a/ui/src/controller/adb_socket_controller.ts
+++ b/ui/src/controller/adb_socket_controller.ts
@@ -302,7 +302,7 @@
       msgBindService: new perfetto.protos.IPCFrame.BindService(
           {serviceName: 'ConsumerPort'})
     });
-    return new Promise((resolve, _) => {
+    return new Promise<void>((resolve, _) => {
       this.resolveBindingPromise = resolve;
       this.sendFrame(frame);
     });
diff --git a/ui/src/controller/aggregation/aggregation_controller.ts b/ui/src/controller/aggregation/aggregation_controller.ts
index 7b2d941..d920dca 100644
--- a/ui/src/controller/aggregation/aggregation_controller.ts
+++ b/ui/src/controller/aggregation/aggregation_controller.ts
@@ -36,11 +36,9 @@
   private requestingData = false;
   private queuedRequest = false;
 
-  abstract async createAggregateView(engine: Engine, area: Area):
-      Promise<boolean>;
+  abstract createAggregateView(engine: Engine, area: Area): Promise<boolean>;
 
-  abstract async getExtra(engine: Engine, area: Area):
-      Promise<ThreadStateExtra|void>;
+  abstract getExtra(engine: Engine, area: Area): Promise<ThreadStateExtra|void>;
 
   abstract getTabName(): string;
   abstract getDefaultSorting(): Sorting;
@@ -166,7 +164,7 @@
   }
 
   columnFromColumnDef(def: ColumnDef, numRows: number): Column {
-    // TODO(taylori): The Column type should be based on the
+    // TODO(hjd): The Column type should be based on the
     // ColumnDef type or vice versa to avoid this cast.
     return {
       title: def.title,
diff --git a/ui/src/controller/aggregation/counter_aggregation_controller.ts b/ui/src/controller/aggregation/counter_aggregation_controller.ts
index ff2dbc3..24d4a94 100644
--- a/ui/src/controller/aggregation/counter_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/counter_aggregation_controller.ts
@@ -31,7 +31,7 @@
       // Track will be undefined for track groups.
       if (track !== undefined && track.kind === COUNTER_TRACK_KIND) {
         const config = track.config as Config;
-        // TODO(taylori): Also aggregate annotation (with namespace) counters.
+        // TODO(hjd): Also aggregate annotation (with namespace) counters.
         if (config.namespace === undefined) {
           ids.push(config.trackId);
         }
diff --git a/ui/src/controller/args_parser.ts b/ui/src/controller/args_parser.ts
new file mode 100644
index 0000000..b5e1b56
--- /dev/null
+++ b/ui/src/controller/args_parser.ts
@@ -0,0 +1,136 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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 {
+  Args,
+  ArgsTree,
+  ArgsTreeArray,
+  ArgsTreeMap,
+  isArgTreeArray,
+  isArgTreeMap
+} from '../common/arg_types';
+
+// Converts a flats sequence of key-value pairs into a JSON-like nested
+// structure. Dots in keys are used to create a nested dictionary, indices in
+// brackets used to create nested array. For example, consider the following
+// sequence of key-value pairs:
+//
+// simple_key = simple_value
+// thing.key = value
+// thing.point[0].x = 10
+// thing.point[0].y = 20
+// thing.point[1].x = 0
+// thing.point[1].y = -10
+//
+// It's going to be converted to a following object:
+//
+// {
+//   "simple_key": "simple_value",
+//   "thing": {
+//     "key": "value",
+//     "point": [
+//       { "x": "10", "y": "20" },
+//       { "x": "0", "y": "-10" }
+//     ]
+//   }
+// }
+export function parseArgs(args: Args): ArgsTree|undefined {
+  const result: ArgsTreeMap = {};
+  for (const [key, value] of args) {
+    if (typeof value === 'string') {
+      fillObject(result, key.split('.'), value);
+    }
+  }
+  return result;
+}
+
+function getOrCreateMap(
+    object: ArgsTreeMap|ArgsTreeArray, key: string|number): ArgsTreeMap {
+  let value: ArgsTree;
+  if (isArgTreeMap(object) && typeof key === 'string') {
+    value = object[key];
+  } else if (isArgTreeArray(object) && typeof key === 'number') {
+    value = object[key];
+  } else {
+    throw new Error('incompatible parameters to getOrCreateSubmap');
+  }
+
+  if (value !== undefined) {
+    if (isArgTreeMap(value)) {
+      return value;
+    } else {
+      // There is a value, but it's not a map - something wrong with the key set
+      throw new Error('inconsistent keys');
+    }
+  }
+
+  value = {};
+  if (isArgTreeMap(object) && typeof key === 'string') {
+    object[key] = value;
+  } else if (isArgTreeArray(object) && typeof key === 'number') {
+    object[key] = value;
+  }
+
+  return value;
+}
+
+function getOrCreateArray(object: ArgsTreeMap, key: string): ArgsTree[] {
+  let value = object[key];
+  if (value !== undefined) {
+    if (isArgTreeArray(value)) {
+      return value;
+    } else {
+      // There is a value, but it's not an array - something wrong with the key
+      // set
+      throw new Error('inconsistent keys');
+    }
+  }
+
+  value = [];
+  object[key] = value;
+  return value;
+}
+
+function fillObject(object: ArgsTreeMap, path: string[], value: string) {
+  let current = object;
+  for (let i = 0; i < path.length - 1; i++) {
+    const [part, index] = parsePathSegment(path[i]);
+    if (index === undefined) {
+      current = getOrCreateMap(current, part);
+    } else {
+      const array = getOrCreateArray(current, part);
+      current = getOrCreateMap(array, index);
+    }
+  }
+
+  const [part, index] = parsePathSegment(path[path.length - 1]);
+  if (index === undefined) {
+    current[part] = value;
+  } else {
+    const array = getOrCreateArray(current, part);
+    array[index] = value;
+  }
+}
+
+// Segment is either a simple key (e.g. "foo") or a key with an index (e.g.
+// "bar[42]"). This function returns a pair of key and index (if present).
+function parsePathSegment(segment: string): [string, number?] {
+  if (!segment.endsWith(']')) {
+    return [segment, undefined];
+  }
+
+  const indexStart = segment.indexOf('[');
+  const indexString = segment.substring(indexStart + 1, segment.length - 1);
+  return [segment.substring(0, indexStart), Math.floor(Number(indexString))];
+}
\ No newline at end of file
diff --git a/ui/src/controller/chrome_proxy_record_controller.ts b/ui/src/controller/chrome_proxy_record_controller.ts
index 8defb2b..4934cc0 100644
--- a/ui/src/controller/chrome_proxy_record_controller.ts
+++ b/ui/src/controller/chrome_proxy_record_controller.ts
@@ -14,6 +14,7 @@
 
 import {binaryDecode, binaryEncode} from '../base/string_utils';
 import {Actions} from '../common/actions';
+import {TRACE_SUFFIX} from '../common/constants';
 
 import {
   ConsumerPortResponse,
@@ -95,4 +96,8 @@
     const reqEncoded = binaryEncode(requestData);
     this.extensionPort.postMessage({method, requestData: reqEncoded});
   }
+
+  getRecordedTraceSuffix(): string {
+    return `${TRACE_SUFFIX}.gz`;
+  }
 }
diff --git a/ui/src/controller/globals.ts b/ui/src/controller/globals.ts
index 606dab4..d47af46 100644
--- a/ui/src/controller/globals.ts
+++ b/ui/src/controller/globals.ts
@@ -25,7 +25,8 @@
     'HeapProfileFlamegraph'|'FileDownload'|'Loading'|'Search'|'BufferUsage'|
     'RecordingLog'|'SearchResult'|'AggregateData'|'CpuProfileDetails'|
     'TraceErrors'|'UpdateChromeCategories'|'ConnectedFlows'|'SelectedFlows'|
-    'ThreadStateDetails'|'MetricError'|'MetricResult';
+    'ThreadStateDetails'|'MetricError'|'MetricResult'|'HasFtrace'|
+    'ConversionJobStatusUpdate';
 
 export interface App {
   state: State;
@@ -99,6 +100,19 @@
         .send<void>(`publish${what}`, [data], transferList);
   }
 
+  // Returns the port of the MessageChannel that can be used to communicate with
+  // the Wasm Engine (issue SQL queries and retrieve results).
+  resetEngineWorker() {
+    const chan = new MessageChannel();
+    const port = chan.port1;
+    // Invokes resetEngineWorker() in frontend/index.ts. It will spawn a new
+    // worker and assign it the passed |port|.
+    assertExists(this._frontend).send<void>('resetEngineWorker', [port], [
+      port
+    ]);
+    return chan.port2;
+  }
+
   get state(): State {
     return assertExists(this._state);
   }
diff --git a/ui/src/controller/heap_profile_controller.ts b/ui/src/controller/heap_profile_controller.ts
index c152590..37af362 100644
--- a/ui/src/controller/heap_profile_controller.ts
+++ b/ui/src/controller/heap_profile_controller.ts
@@ -210,7 +210,7 @@
     if (this.flamegraphDatasets.has(key)) {
       currentData = this.flamegraphDatasets.get(key)!;
     } else {
-      // TODO(taylori): Show loading state.
+      // TODO(hjd): Show loading state.
 
       // Collecting data for drawing flamegraph for selected heap profile.
       // Data needs to be in following format:
@@ -269,8 +269,9 @@
     const callsites = await this.args.engine.query(
         `SELECT id, IFNULL(DEMANGLE(name), name), IFNULL(parent_id, -1), depth,
         cumulative_size, cumulative_alloc_size, cumulative_count,
-        cumulative_alloc_count, map_name, size, count from ${tableName} ${
-            orderBy}`);
+        cumulative_alloc_count, map_name, size, count,
+        IFNULL(source_file, ''), IFNULL(line_number, -1)
+        from ${tableName} ${orderBy}`);
 
     const flamegraphData: CallsiteInfo[] = new Array();
     const hashToindex: Map<number, number> = new Map();
@@ -286,6 +287,17 @@
           name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase());
       const parentId =
           hashToindex.has(+parentHash) ? hashToindex.get(+parentHash)! : -1;
+
+      let location: string|undefined;
+      if (callsites.columns[11].stringValues != null &&
+          /[a-zA-Z]/i.test(callsites.columns[11].stringValues[i])) {
+        location = callsites.columns[11].stringValues[i];
+        if (callsites.columns[12].longValues != null &&
+            callsites.columns[12].longValues[i] !== -1) {
+          location += `:${callsites.columns[12].longValues[i].toString()}`;
+        }
+      }
+
       if (depth === maxDepth - 1) {
         name += ' [tree truncated]';
       }
@@ -302,7 +314,8 @@
         selfSize,
         mapping,
         merged: false,
-        highlighted
+        highlighted,
+        location
       });
     }
     return flamegraphData;
@@ -321,7 +334,7 @@
     return this.cache.getTableName(
         `select id, name, map_name, parent_id, depth, cumulative_size,
           cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
-          size, alloc_size, count, alloc_count
+          size, alloc_size, count, alloc_count, source_file, line_number
           from experimental_flamegraph(${ts}, ${upid}, '${type}') ${
             whereClause}`);
   }
diff --git a/ui/src/controller/index.ts b/ui/src/controller/index.ts
index bf655b4..9f5add7 100644
--- a/ui/src/controller/index.ts
+++ b/ui/src/controller/index.ts
@@ -16,30 +16,21 @@
 
 import {reportError, setErrorHandler} from '../base/logging';
 import {Remote} from '../base/remote';
-import {warmupWasmEngine} from '../common/wasm_engine_proxy';
-
+import {ControllerWorkerInitMessage} from '../common/worker_messages';
 import {AppController} from './app_controller';
 import {globals} from './globals';
 
-interface OnMessageArg {
-  data: {
-    frontendPort: MessagePort; controllerPort: MessagePort;
-    extensionPort: MessagePort;
-    errorReportingPort: MessagePort;
-  };
-}
-
 function main() {
   self.addEventListener('error', e => reportError(e));
   self.addEventListener('unhandledrejection', e => reportError(e));
-  warmupWasmEngine();
   let initialized = false;
-  self.onmessage = ({data}: OnMessageArg) => {
+  self.onmessage = (e: MessageEvent) => {
     if (initialized) {
       console.error('Already initialized');
       return;
     }
     initialized = true;
+    const data = e.data as ControllerWorkerInitMessage;
     const frontendPort = data.frontendPort;
     const controllerPort = data.controllerPort;
     const extensionPort = data.extensionPort;
diff --git a/ui/src/controller/permalink_controller.ts b/ui/src/controller/permalink_controller.ts
index a390443..f20cb18 100644
--- a/ui/src/controller/permalink_controller.ts
+++ b/ui/src/controller/permalink_controller.ts
@@ -16,6 +16,7 @@
 
 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 {
@@ -48,10 +49,22 @@
       const isRecordingConfig =
           assertExists(globals.state.permalink.isRecordingConfig);
 
+      const jobName = 'create_permalink';
+      globals.publish('ConversionJobStatusUpdate', {
+        jobName,
+        jobStatus: ConversionJobStatus.InProgress,
+      });
+
       PermalinkController.createPermalink(isRecordingConfig)
-          .then(((hash: string) => {
+          .then(hash => {
             globals.dispatch(Actions.setPermalink({requestId, hash}));
-          }));
+          })
+          .finally(() => {
+            globals.publish('ConversionJobStatusUpdate', {
+              jobName,
+              jobStatus: ConversionJobStatus.NotRunning,
+            });
+          });
       return;
     }
 
@@ -99,7 +112,8 @@
         stateOrConfig.mode);
   }
 
-  private static async createPermalink(isRecordingConfig: boolean) {
+  private static async createPermalink(isRecordingConfig: boolean):
+      Promise<string> {
     let uploadState: State|RecordConfig = globals.state;
 
     if (isRecordingConfig) {
diff --git a/ui/src/controller/record_controller.ts b/ui/src/controller/record_controller.ts
index 6147578..a124ecf 100644
--- a/ui/src/controller/record_controller.ts
+++ b/ui/src/controller/record_controller.ts
@@ -18,6 +18,7 @@
   base64Encode,
 } from '../base/string_utils';
 import {Actions} from '../common/actions';
+import {TRACE_SUFFIX} from '../common/constants';
 import {
   AndroidLogConfig,
   AndroidLogId,
@@ -245,7 +246,7 @@
 
   let heapprofd: HeapprofdConfig|undefined = undefined;
   if (uiCfg.heapProfiling) {
-    // TODO(taylori): Check or inform user if buffer size are too small.
+    // TODO(hjd): Check or inform user if buffer size are too small.
     const cfg = new HeapprofdConfig();
     cfg.samplingIntervalBytes = uiCfg.hpSamplingIntervalBytes;
     if (uiCfg.hpSharedMemoryBuffer >= 8192 &&
@@ -329,6 +330,15 @@
     }
   }
 
+  if (uiCfg.androidFrameTimeline) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.name = 'android.surfaceflinger.frametimeline';
+    if (!isChromeTarget(target) || isCrOSTarget(target)) {
+      protoCfg.dataSources.push(ds);
+    }
+  }
+
   if (uiCfg.chromeLogs) {
     chromeCategories.add('log');
   }
@@ -396,7 +406,12 @@
     };
     if (chromeCategories.has('disabled-by-default-memory-infra')) {
       configStruct.memory_dump_config = {
-        triggers: [{mode: 'detailed', periodic_interval_ms: 10000}]
+        allowed_dump_modes: ['background', 'light', 'detailed'],
+        triggers: [{
+          min_time_between_dumps_ms: 10000,
+          mode: 'detailed',
+          type: 'periodic_interval',
+        }],
       };
     }
     const traceConfigJson = JSON.stringify(configStruct);
@@ -575,6 +590,7 @@
   private traceBuffer: Uint8Array[] = [];
   private bufferUpdateInterval: ReturnType<typeof setTimeout>|undefined;
   private adb = new AdbOverWebUsb();
+  private recordedTraceSuffix = TRACE_SUFFIX;
 
   // We have a different controller for each targetOS. The correct one will be
   // created when needed, and stored here. When the key is a string, it is the
@@ -694,8 +710,11 @@
       return;
     }
     const trace = this.generateTrace();
-    globals.dispatch(Actions.openTraceFromBuffer(
-        {title: 'Recorded trace', buffer: trace.buffer}));
+    globals.dispatch(Actions.openTraceFromBuffer({
+      title: 'Recorded trace',
+      buffer: trace.buffer,
+      fileName: `recorded_trace${this.recordedTraceSuffix}`,
+    }));
     this.traceBuffer = [];
   }
 
@@ -796,8 +815,12 @@
       _callback: RPCImplCallback) {
     try {
       const state = this.app.state;
-      (await this.getTargetController(state.recordingTarget))
-          .handleCommand(method.name, requestData);
+      // TODO(hjd): This is a bit weird. We implicity send each RPC message to
+      // whichever target is currently selected (creating that target if needed)
+      // it would be nicer if the setup/teardown was more explicit.
+      const target = await this.getTargetController(state.recordingTarget);
+      this.recordedTraceSuffix = target.getRecordedTraceSuffix();
+      target.handleCommand(method.name, requestData);
     } catch (e) {
       console.error(`error invoking ${method}: ${e.message}`);
     }
diff --git a/ui/src/controller/record_controller_interfaces.ts b/ui/src/controller/record_controller_interfaces.ts
index 8c336e0..2dd2c91 100644
--- a/ui/src/controller/record_controller_interfaces.ts
+++ b/ui/src/controller/record_controller_interfaces.ts
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {TRACE_SUFFIX} from '../common/constants';
 import {ConsumerPortResponse} from './consumer_port_types';
 
 export type ConsumerPortCallback = (_: ConsumerPortResponse) => void;
@@ -41,10 +42,18 @@
   sendStatus(status: string) {
     this.consumerPortListener.onStatus(status);
   }
+
+  // Allows the recording controller to customise the suffix added to recorded
+  // traces when they are downloaded. In the general case this will be
+  // .perfetto-trace however if the trace is recorded compressed if could be
+  // .perfetto-trace.gz etc.
+  getRecordedTraceSuffix(): string {
+    return TRACE_SUFFIX;
+  }
 }
 
 export interface Consumer {
   onConsumerPortResponse(data: ConsumerPortResponse): void;
   onError: ErrorCallback;
   onStatus: StatusCallback;
-}
\ No newline at end of file
+}
diff --git a/ui/src/controller/record_controller_jsdomtest.ts b/ui/src/controller/record_controller_jsdomtest.ts
index 6d6e780..b98f868 100644
--- a/ui/src/controller/record_controller_jsdomtest.ts
+++ b/ui/src/controller/record_controller_jsdomtest.ts
@@ -23,10 +23,10 @@
 
 test('encodeConfig', () => {
   const config = createEmptyRecordConfig();
-  config.durationSeconds = 10;
+  config.durationMs = 20000;
   const result =
       TraceConfig.decode(genConfigProto(config, {os: 'Q', name: 'Android Q'}));
-  expect(result.durationMs).toBe(10000);
+  expect(result.durationMs).toBe(20000);
 });
 
 test('SysConfig', () => {
@@ -132,7 +132,7 @@
   expect(traceConfig).toEqual(expectedTraceConfig);
 });
 
-test('ChromeMemoryConfig', () => {
+test.skip('ChromeMemoryConfig', () => {
   const config = createEmptyRecordConfig();
   config.chromeCategoriesSelected = ['disabled-by-default-memory-infra'];
   const result =
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index c5ee827..8f71701 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -12,8 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {TRACE_MARGIN_TIME_S} from '../common/constants';
 import {Engine} from '../common/engine';
-import {slowlyCountRows} from '../common/query_iterator';
+import {NUM, STR} from '../common/query_iterator';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
 import {TimeSpan} from '../common/time';
 
@@ -58,11 +59,11 @@
   }
 
   private async setup() {
-    await this.query(`create virtual table search_summary_window
+    await this.queryV2(`create virtual table search_summary_window
       using window;`);
-    await this.query(`create virtual table search_summary_sched_span using
+    await this.queryV2(`create virtual table search_summary_sched_span using
       span_join(sched PARTITIONED cpu, search_summary_window);`);
-    await this.query(`create virtual table search_summary_slice_span using
+    await this.queryV2(`create virtual table search_summary_slice_span using
       span_join(slice PARTITIONED track_id, search_summary_window);`);
   }
 
@@ -79,14 +80,14 @@
     }
     const newSpan = new TimeSpan(visibleState.startSec, visibleState.endSec);
     const newSearch = omniboxState.omnibox;
-    const newResolution = visibleState.resolution;
+    let newResolution = visibleState.resolution;
     if (this.previousSpan.contains(newSpan) &&
         this.previousResolution === newResolution &&
         newSearch === this.previousSearch) {
       return;
     }
     this.previousSpan = new TimeSpan(
-        Math.max(newSpan.start - newSpan.duration, 0),
+        Math.max(newSpan.start - newSpan.duration, -TRACE_MARGIN_TIME_S),
         newSpan.end + newSpan.duration);
     this.previousResolution = newResolution;
     this.previousSearch = newSearch;
@@ -107,8 +108,20 @@
       return;
     }
 
-    const startNs = Math.round(newSpan.start * 1e9);
-    const endNs = Math.round(newSpan.end * 1e9);
+    let startNs = Math.round(newSpan.start * 1e9);
+    let endNs = Math.round(newSpan.end * 1e9);
+
+    // TODO(hjd): We shouldn't need to be so defensive here:
+    if (!Number.isFinite(startNs)) {
+      startNs = 0;
+    }
+    if (!Number.isFinite(endNs)) {
+      endNs = 1;
+    }
+    if (!Number.isFinite(newResolution)) {
+      newResolution = 1;
+    }
+
     this.updateInProgress = true;
     const computeSummary =
         this.update(newSearch, startNs, endNs, newResolution).then(summary => {
@@ -138,22 +151,25 @@
 
     startNs = Math.floor(startNs / quantumNs) * quantumNs;
 
-    await this.query(`update search_summary_window set
+    await this.queryV2(`update search_summary_window set
       window_start=${startNs},
       window_dur=${endNs - startNs},
       quantum=${quantumNs}
       where rowid = 0;`);
 
-    const rawUtidResult = await this.query(`select utid from thread join process
+    const utidRes = await this.queryV2(`select utid from thread join process
       using(upid) where thread.name like ${searchLiteral}
       or process.name like ${searchLiteral}`);
 
-    const utids = [...rawUtidResult.columns[0].longValues!];
+    const utids = [];
+    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
+      utids.push(it.utid);
+    }
 
     const cpus = await this.engine.getCpus();
     const maxCpu = Math.max(...cpus, -1);
 
-    const rawResult = await this.query(`
+    const res = await this.queryV2(`
         select
           (quantum_ts * ${quantumNs} + ${startNs})/1e9 as tsStart,
           ((quantum_ts+1) * ${quantumNs} + ${startNs})/1e9 as tsEnd,
@@ -172,18 +188,18 @@
           group by quantum_ts
           order by quantum_ts;`);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = res.numRows();
     const summary = {
       tsStarts: new Float64Array(numRows),
       tsEnds: new Float64Array(numRows),
       count: new Uint8Array(numRows)
     };
 
-    const columns = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      summary.tsStarts[row] = +columns[0].doubleValues![row];
-      summary.tsEnds[row] = +columns[1].doubleValues![row];
-      summary.count[row] = +columns[2].longValues![row];
+    const it = res.iter({tsStart: NUM, tsEnd: NUM, count: NUM});
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      summary.tsStarts[row] = it.tsStart;
+      summary.tsEnds[row] = it.tsEnd;
+      summary.count[row] = it.count;
     }
     return summary;
   }
@@ -213,80 +229,78 @@
       }
     }
 
-    const rawUtidResult = await this.query(`select utid from thread join process
+    const utidRes = await this.queryV2(`select utid from thread join process
     using(upid) where
       thread.name like ${searchLiteral} or
       process.name like ${searchLiteral}`);
-    const utids = [...rawUtidResult.columns[0].longValues!];
+    const utids = [];
+    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
+      utids.push(it.utid);
+    }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
     select
-      id as slice_id,
+      id as sliceId,
       ts,
       'cpu' as source,
-      cpu as source_id,
+      cpu as sourceId,
       utid
     from sched where utid in (${utids.join(',')})
     union
     select
-      slice_id,
+      slice_id as sliceId,
       ts,
       'track' as source,
-      track_id as source_id,
+      track_id as sourceId,
       0 as utid
       from slice
       where slice.name like ${searchLiteral}
     union
     select
-      slice_id,
+      slice_id as sliceId,
       ts,
       'track' as source,
-      track_id as source_id,
+      track_id as sourceId,
       0 as utid
       from slice
       join args using(arg_set_id)
       where string_value like ${searchLiteral}
     order by ts`);
 
-    const numRows = slowlyCountRows(rawResult);
-
     const searchResults: CurrentSearchResults = {
       sliceIds: [],
       tsStarts: [],
       utids: [],
       trackIds: [],
       sources: [],
-      totalResults: +numRows,
+      totalResults: queryRes.numRows(),
     };
 
-    const columns = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const source = columns[2].stringValues![row];
-      const sourceId = +columns[3].longValues![row];
+    const spec = {sliceId: NUM, ts: NUM, source: STR, sourceId: NUM, utid: NUM};
+    for (const it = queryRes.iter(spec); it.valid(); it.next()) {
       let trackId = undefined;
-      if (source === 'cpu') {
-        trackId = cpuToTrackId.get(sourceId);
-      } else if (source === 'track') {
-        trackId = engineTrackIdToTrackId.get(sourceId);
+      if (it.source === 'cpu') {
+        trackId = cpuToTrackId.get(it.sourceId);
+      } else if (it.source === 'track') {
+        trackId = engineTrackIdToTrackId.get(it.sourceId);
       }
 
+      // The .get() calls above could return undefined, this isn't just an else.
       if (trackId === undefined) {
         searchResults.totalResults--;
         continue;
       }
-
       searchResults.trackIds.push(trackId);
-      searchResults.sources.push(source);
-      searchResults.sliceIds.push(+columns[0].longValues![row]);
-      searchResults.tsStarts.push(+columns[1].longValues![row]);
-      searchResults.utids.push(+columns[4].longValues![row]);
+      searchResults.sources.push(it.source);
+      searchResults.sliceIds.push(it.sliceId);
+      searchResults.tsStarts.push(it.ts);
+      searchResults.utids.push(it.utid);
     }
     return searchResults;
   }
 
-
-  private async query(query: string) {
-    const result = await this.engine.query(query);
+  private async queryV2(query: string) {
+    const result = await this.engine.queryV2(query);
     return result;
   }
 }
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 99cd49d..0366df1 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -12,20 +12,26 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {Arg, Args} from '../common/arg_types';
 import {Engine} from '../common/engine';
-import {slowlyCountRows} from '../common/query_iterator';
+import {
+  NUM,
+  singleRow,
+  singleRowUntyped,
+  slowlyCountRows,
+  STR
+} from '../common/query_iterator';
 import {ChromeSliceSelection} from '../common/state';
 import {translateState} from '../common/thread_state';
 import {fromNs, toNs} from '../common/time';
 import {
-  Arg,
-  Args,
   CounterDetails,
   SliceDetails,
   ThreadStateDetails
 } from '../frontend/globals';
 import {SLICE_TRACK_KIND} from '../tracks/chrome_slices/common';
 
+import {parseArgs} from './args_parser';
 import {Controller} from './controller';
 import {globals} from './globals';
 
@@ -84,44 +90,94 @@
   async chromeSliceDetails(selection: ChromeSliceSelection) {
     const selectedId = selection.id;
     const table = selection.table;
-    let sqlQuery = `
-      SELECT ts, dur, name, cat, arg_set_id
-      FROM slice
-      WHERE id = ${selectedId}
-    `;
+
+    let leafTable: string;
+    let promisedDescription: Promise<Map<string, string>>;
+    let promisedArgs: Promise<Args>;
     // TODO(b/155483804): This is a hack to ensure annotation slices are
     // selectable for now. We should tidy this up when improving this class.
     if (table === 'annotation') {
-      sqlQuery = `
-      select ts, dur, name, cat, -1
-      from annotation_slice
-      where id = ${selectedId}`;
+      leafTable = 'annotation_slice';
+      promisedDescription = Promise.resolve(new Map());
+      promisedArgs = Promise.resolve(new Map());
+    } else {
+      const typeResult = singleRow(
+          {
+            leafTable: STR,
+            argSetId: NUM,
+          },
+          await this.args.engine.query(`
+        SELECT
+          type as leafTable,
+          arg_set_id as argSetId
+        FROM slice WHERE id = ${selectedId}`));
+
+      if (typeResult === undefined) {
+        return;
+      }
+
+      leafTable = typeResult.leafTable;
+      const argSetId = typeResult.argSetId;
+      promisedDescription = this.describeSlice(selectedId);
+      promisedArgs = this.getArgs(argSetId);
     }
-    const result = await this.args.engine.query(sqlQuery);
+
+    const promisedDetails = this.args.engine.query(`
+      SELECT * FROM ${leafTable} WHERE id = ${selectedId};
+    `);
+
+    const [details, args, description] =
+        await Promise.all([promisedDetails, promisedArgs, promisedDescription]);
+
+    const row = singleRowUntyped(details);
+    if (row === undefined) {
+      return;
+    }
+
+    // A few columns are hard coded as part of the SliceDetails interface.
+    // Long term these should be handled generically as args but for now
+    // handle them specially:
+    let ts = undefined;
+    let dur = undefined;
+    let name = undefined;
+    let category = undefined;
+
+    for (const [k, v] of Object.entries(row)) {
+      switch (k) {
+        case 'id':
+          break;
+        case 'ts':
+          ts = fromNs(Number(v)) - globals.state.traceTime.startSec;
+          break;
+        case 'name':
+          name = `${v}`;
+          break;
+        case 'dur':
+          dur = fromNs(Number(v));
+          break;
+        case 'category':
+        case 'cat':
+          category = `${v}`;
+          break;
+        default:
+          args.set(k, `${v}`);
+      }
+    }
+
+    const argsTree = parseArgs(args);
+    const selected: SliceDetails = {
+      id: selectedId,
+      ts,
+      dur,
+      name,
+      category,
+      args,
+      argsTree,
+      description,
+    };
+
     // Check selection is still the same on completion of query.
-    if (slowlyCountRows(result) === 1 &&
-        selection === globals.state.currentSelection) {
-      const ts = result.columns[0].longValues![0];
-      const timeFromStart = fromNs(ts) - globals.state.traceTime.startSec;
-      const name = result.columns[2].stringValues![0];
-      const dur = fromNs(result.columns[1].longValues![0]);
-      const category = result.columns[3].stringValues![0];
-      const argId = result.columns[4].longValues![0];
-      const argsAsync = this.getArgs(argId);
-      // Don't fetch descriptions for annotation slices.
-      const describeId = table === 'annotation' ? -1 : +selectedId;
-      const descriptionAsync = this.describeSlice(describeId);
-      const [args, description] =
-          await Promise.all([argsAsync, descriptionAsync]);
-      const selected: SliceDetails = {
-        ts: timeFromStart,
-        dur,
-        category,
-        name,
-        id: selectedId,
-        args,
-        description,
-      };
+    if (selection === globals.state.currentSelection) {
       globals.publish('SliceDetails', selected);
     }
   }
@@ -174,7 +230,7 @@
     where slice_id = ${sliceId}`;
     const destResult = await this.args.engine.query(trackIdQuery);
     const trackIdTp = destResult.columns[0].longValues![0];
-    // TODO(taylori): If we had a consistent mapping from TP track_id
+    // TODO(hjd): If we had a consistent mapping from TP track_id
     // UI track id for slice tracks this would be unnecessary.
     let trackId = '';
     for (const track of Object.values(globals.state.tracks)) {
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index a7b1e38..52690e6 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -19,17 +19,14 @@
   Actions,
   DeferredAction,
 } from '../common/actions';
+import {TRACE_MARGIN_TIME_S} from '../common/constants';
 import {Engine, QueryError} from '../common/engine';
 import {HttpRpcEngine} from '../common/http_rpc_engine';
 import {slowlyCountRows} from '../common/query_iterator';
 import {EngineMode} from '../common/state';
 import {toNs, toNsCeil, toNsFloor} from '../common/time';
 import {TimeSpan} from '../common/time';
-import {
-  createWasmEngine,
-  destroyWasmEngine,
-  WasmEngineProxy
-} from '../common/wasm_engine_proxy';
+import {WasmEngineProxy} from '../common/wasm_engine_proxy';
 import {QuantizedLoad, ThreadDesc} from '../frontend/globals';
 
 import {
@@ -84,8 +81,6 @@
 
 type States = 'init'|'loading_trace'|'ready';
 
-const TRACE_MARGIN_TIME_S = 1 / 1e7;
-
 // TraceController handles handshakes with the frontend for everything that
 // concerns a single trace. It owns the WASM trace processor engine, handles
 // tracks data and SQL queries. There is one TraceController instance for each
@@ -99,12 +94,6 @@
     this.engineId = engineId;
   }
 
-  onDestroy() {
-    if (this.engine instanceof WasmEngineProxy) {
-      destroyWasmEngine(this.engine.id);
-    }
-  }
-
   run() {
     const engineCfg = assertExists(globals.state.engines[this.engineId]);
     switch (this.state) {
@@ -232,10 +221,9 @@
     } else {
       console.log('Opening trace using built-in WASM engine');
       engineMode = 'WASM';
+      const enginePort = globals.resetEngineWorker();
       this.engine = new WasmEngineProxy(
-          this.engineId,
-          createWasmEngine(this.engineId),
-          LoadingManager.getInstance);
+          this.engineId, enginePort, LoadingManager.getInstance);
     }
 
     globals.dispatch(Actions.setEngineReady({
@@ -299,11 +287,26 @@
       Actions.navigate({route: '/viewer'}),
     ];
 
+    let visibleStartSec = startSec;
+    let visibleEndSec = endSec;
+    const mdTime = await this.engine.getTracingMetadataTimeBounds();
+    // make sure the bounds hold
+    if (Math.max(visibleStartSec, mdTime.start - TRACE_MARGIN_TIME_S) <
+        Math.min(visibleEndSec, mdTime.end + TRACE_MARGIN_TIME_S)) {
+      visibleStartSec =
+          Math.max(visibleStartSec, mdTime.start - TRACE_MARGIN_TIME_S);
+      visibleEndSec = Math.min(visibleEndSec, mdTime.end + TRACE_MARGIN_TIME_S);
+    }
+
     // We don't know the resolution at this point. However this will be
     // replaced in 50ms so a guess is fine.
-    const resolution = (traceTime.end - traceTime.start) / 1000;
-    actions.push(Actions.setVisibleTraceTime(
-        {...traceTimeState, lastUpdate: Date.now() / 1000, resolution}));
+    const resolution = (visibleStartSec - visibleEndSec) / 1000;
+    actions.push(Actions.setVisibleTraceTime({
+      startSec: visibleStartSec,
+      endSec: visibleEndSec,
+      lastUpdate: Date.now() / 1000,
+      resolution
+    }));
 
     globals.dispatchMultiple(actions);
 
@@ -320,6 +323,24 @@
 
     await this.listThreads();
     await this.loadTimelineOverview(traceTime);
+
+    {
+      // A quick heuristic to check if the trace has ftrace events. This is
+      // based on the assumption that most traces that have ftrace either:
+      // - Are proto traces captured via perfetto, in which case traced_probes
+      //   emits ftrace per-cpu stats that end up in the stats table.
+      // - Have a raw event with non-zero cpu or utid.
+      // Notes:
+      // - The "+1 > 1" is to avoid pushing down the constraints to the "raw"
+      //   table, which would compute a full column filter without being aware
+      //   of the limit 1, and instead delegate the filtering to the iterator.
+      const query = `select '_' as _ from raw
+          where cpu + 1 > 1 or utid + 1 > 1 limit 1`;
+      const result = await assertExists(this.engine).query(query);
+      const hasFtrace = !!slowlyCountRows(result);
+      globals.publish('HasFtrace', hasFtrace);
+    }
+
     globals.dispatch(Actions.sortThreadTracks({}));
     await this.selectFirstHeapProfile();
 
@@ -518,13 +539,18 @@
 
       this.updateStatus(`Inserting data for ${metric} metric`);
       try {
-        const result = await engine.query(`
-        SELECT * FROM ${metric}_event LIMIT 1`);
-
-        const hasSliceName =
-            result.columnDescriptors.some(x => x.name === 'slice_name');
-        const hasDur = result.columnDescriptors.some(x => x.name === 'dur');
-        const hasUpid = result.columnDescriptors.some(x => x.name === 'upid');
+        const result = await engine.query(`pragma table_info(${metric}_event)`);
+        let hasSliceName = false;
+        let hasDur = false;
+        let hasUpid = false;
+        let hasValue = false;
+        for (let i = 0; i < slowlyCountRows(result); i++) {
+          const name = result.columns[1].stringValues![i];
+          hasSliceName = hasSliceName || name === 'slice_name';
+          hasDur = hasDur || name === 'dur';
+          hasUpid = hasUpid || name === 'upid';
+          hasValue = hasValue || name === 'value';
+        }
 
         const upidColumnSelect = hasUpid ? 'upid' : '0 AS upid';
         const upidColumnWhere = hasUpid ? 'upid' : '0';
@@ -554,7 +580,6 @@
           `);
         }
 
-        const hasValue = result.columnDescriptors.some(x => x.name === 'value');
         if (hasValue) {
           const minMax = await engine.query(`
           SELECT MIN(value) as min_value, MAX(value) as max_value
diff --git a/ui/src/controller/trace_converter.ts b/ui/src/controller/trace_converter.ts
index 1e9539a..f839100 100644
--- a/ui/src/controller/trace_converter.ts
+++ b/ui/src/controller/trace_converter.ts
@@ -15,29 +15,77 @@
 import {defer} from '../base/deferred';
 import {assertExists} from '../base/logging';
 import {Actions} from '../common/actions';
+import {ConversionJobStatus} from '../common/conversion_jobs';
 import {TraceSource} from '../common/state';
 import * as trace_to_text from '../gen/trace_to_text';
 
 import {globals} from './globals';
 
-export function ConvertTrace(trace: Blob, truncate?: 'start'|'end') {
+type Format = 'json'|'systrace';
+
+export function ConvertTrace(
+    trace: Blob, format: Format, truncate?: 'start'|'end') {
+  const jobName = 'open_in_legacy';
+  globals.publish('ConversionJobStatusUpdate', {
+    jobName,
+    jobStatus: ConversionJobStatus.InProgress,
+  });
   const outPath = '/trace.json';
-  const args = ['json'];
+  const args: string[] = [format];
   if (truncate !== undefined) {
     args.push('--truncate', truncate);
   }
   args.push('/fs/trace.proto', outPath);
-  runTraceconv(trace, args).then(module => {
-    const fsNode = module.FS.lookupPath(outPath).node;
-    const data = fsNode.contents.buffer;
-    const size = fsNode.usedBytes;
-    globals.publish('LegacyTrace', {data, size}, /*transfer=*/[data]);
-    module.FS.unlink(outPath);
+  runTraceconv(trace, args)
+      .then(module => {
+        const fsNode = module.FS.lookupPath(outPath).node;
+        const data = fsNode.contents.buffer;
+        const size = fsNode.usedBytes;
+        globals.publish('LegacyTrace', {data, size}, /*transfer=*/[data]);
+        module.FS.unlink(outPath);
+      })
+      .finally(() => {
+        globals.publish('ConversionJobStatusUpdate', {
+          jobName,
+          jobStatus: ConversionJobStatus.NotRunning,
+        });
+      });
+}
+
+export function ConvertTraceAndDownload(
+    trace: Blob, format: Format, truncate?: 'start'|'end') {
+  const jobName = `convert_${format}`;
+  globals.publish('ConversionJobStatusUpdate', {
+    jobName,
+    jobStatus: ConversionJobStatus.InProgress,
   });
+  const outPath = '/trace.json';
+  const args: string[] = [format];
+  if (truncate !== undefined) {
+    args.push('--truncate', truncate);
+  }
+  args.push('/fs/trace.proto', outPath);
+  runTraceconv(trace, args)
+      .then(module => {
+        const fsNode = module.FS.lookupPath(outPath).node;
+        downloadFile(fsNodeToBlob(fsNode), `trace.${format}`);
+        module.FS.unlink(outPath);
+      })
+      .finally(() => {
+        globals.publish('ConversionJobStatusUpdate', {
+          jobName,
+          jobStatus: ConversionJobStatus.NotRunning,
+        });
+      });
 }
 
 export function ConvertTraceToPprof(
     pid: number, src: TraceSource, ts1: number, ts2?: number) {
+  const jobName = 'convert_pprof';
+  globals.publish('ConversionJobStatusUpdate', {
+    jobName,
+    jobStatus: ConversionJobStatus.InProgress,
+  });
   const timestamps = `${ts1}${ts2 === undefined ? '' : `,${ts2}`}`;
   const args = [
     'profile',
@@ -47,24 +95,30 @@
     timestamps,
     '/fs/trace.proto'
   ];
-  generateBlob(src).then(traceBlob => {
-    runTraceconv(traceBlob, args).then(module => {
-      const heapDirName =
-          Object.keys(module.FS.lookupPath('/tmp/').node.contents)[0];
-      const heapDirContents =
-          module.FS.lookupPath(`/tmp/${heapDirName}`).node.contents;
-      const heapDumpFiles = Object.keys(heapDirContents);
-      let fileNum = 0;
-      heapDumpFiles.forEach(heapDump => {
-        const fileContents =
-            module.FS.lookupPath(`/tmp/${heapDirName}/${heapDump}`)
-                .node.contents;
-        fileNum++;
-        const fileName = `/heap_dump.${fileNum}.${pid}.pb`;
-        downloadFile(new Blob([fileContents]), fileName);
+  generateBlob(src)
+      .then(traceBlob => {
+        runTraceconv(traceBlob, args).then(module => {
+          const heapDirName =
+              Object.keys(module.FS.lookupPath('/tmp/').node.contents)[0];
+          const heapDirContents =
+              module.FS.lookupPath(`/tmp/${heapDirName}`).node.contents;
+          const heapDumpFiles = Object.keys(heapDirContents);
+          let fileNum = 0;
+          heapDumpFiles.forEach(heapDump => {
+            const fileNode =
+                module.FS.lookupPath(`/tmp/${heapDirName}/${heapDump}`).node;
+            fileNum++;
+            const fileName = `/heap_dump.${fileNum}.${pid}.pb`;
+            downloadFile(fsNodeToBlob(fileNode), fileName);
+          });
+        });
+      })
+      .finally(() => {
+        globals.publish('ConversionJobStatusUpdate', {
+          jobName,
+          jobStatus: ConversionJobStatus.NotRunning,
+        });
       });
-    });
-  });
 }
 
 async function runTraceconv(trace: Blob, args: string[]) {
@@ -106,6 +160,12 @@
   return blob;
 }
 
+function fsNodeToBlob(fsNode: trace_to_text.FileSystemNode): Blob {
+  const fileSize = assertExists(fsNode.usedBytes);
+  const bufView = new Uint8Array(fsNode.contents.buffer, 0, fileSize);
+  return new Blob([bufView]);
+}
+
 function downloadFile(file: Blob, name: string) {
   globals.publish('FileDownload', {file, name});
 }
diff --git a/ui/src/controller/trace_stream.ts b/ui/src/controller/trace_stream.ts
index 34c3861..f6c9da6 100644
--- a/ui/src/controller/trace_stream.ts
+++ b/ui/src/controller/trace_stream.ts
@@ -100,7 +100,7 @@
   private bytesRead = 0;
   private bytesTotal = 0;
   private uri: string;
-  private httpStream?: ReadableStreamReader;
+  private httpStream?: ReadableStreamReader<Uint8Array>;
 
   constructor(uri: string) {
     assertTrue(uri.startsWith('http://') || uri.startsWith('https://'));
@@ -116,8 +116,7 @@
       }
       const len = response.headers.get('Content-Length');
       this.bytesTotal = len ? Number.parseInt(len, 10) : 0;
-      // tslint:disable-next-line no-any
-      this.httpStream = (response.body as any).getReader();
+      this.httpStream = response.body!.getReader();
     }
 
     let eof = false;
@@ -128,8 +127,7 @@
     // TraceProcessor. Here we accumulate chunks until we get at least 32mb
     // or hit EOF.
     while (!eof && bytesRead < 32 * 1024 * 1024) {
-      const res = (await this.httpStream!.read()) as
-          {value?: Uint8Array, done: boolean};
+      const res = await this.httpStream.read();
       if (res.value) {
         chunks.push(res.value);
         bytesRead += res.value.length;
diff --git a/ui/src/controller/track_controller.ts b/ui/src/controller/track_controller.ts
index ff27d17..de05b94 100644
--- a/ui/src/controller/track_controller.ts
+++ b/ui/src/controller/track_controller.ts
@@ -16,7 +16,7 @@
 import {Engine} from '../common/engine';
 import {Registry} from '../common/registry';
 import {TraceTime, TrackState} from '../common/state';
-import {toNs} from '../common/time';
+import {fromNs, toNs} from '../common/time';
 import {LIMIT, TrackData} from '../common/track_data';
 
 import {Controller} from './controller';
@@ -68,7 +68,7 @@
   // Must be overridden by the track implementation. Is invoked when the track
   // frontend runs out of cached data. The derived track controller is expected
   // to publish new track data in response to this call.
-  abstract async onBoundsChange(start: number, end: number, resolution: number):
+  abstract onBoundsChange(start: number, end: number, resolution: number):
       Promise<Data>;
 
   get trackState(): TrackState {
@@ -118,6 +118,11 @@
     return result;
   }
 
+  protected async queryV2(query: string) {
+    const result = await this.engine.queryV2(query);
+    return result;
+  }
+
   private shouldReload(): boolean {
     const {lastTrackReloadRequest} = globals.state;
     return !!lastTrackReloadRequest &&
@@ -151,7 +156,7 @@
         globals.state.frontendLocalState.visibleState.resolution;
   }
 
-  // Decides, based on the the length of the trace and the number of rows
+  // Decides, based on the length of the trace and the number of rows
   // provided whether a TrackController subclass should cache its quantized
   // data. Returns the bucket size (in ns) if caching should happen and
   // undefined otherwise.
@@ -252,10 +257,18 @@
         promise
             .then(() => {
               this.isSetup = true;
+              let resolution = visibleState.resolution;
+              // TODO(hjd): We shouldn't have to be so defensive here.
+              if (Math.log2(toNs(resolution)) % 1 !== 0) {
+                // resolution is in pixels per second so 1000 means
+                // 1px = 1ms.
+                resolution =
+                    fromNs(Math.pow(2, Math.floor(Math.log2(toNs(1000)))));
+              }
               return this.onBoundsChange(
                   visibleState.startSec - dur,
                   visibleState.endSec + dur,
-                  visibleState.resolution);
+                  resolution);
             })
             .then(data => {
               this.publish(data);
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 66ef218..c3d904c 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -29,7 +29,7 @@
   STR,
   STR_NULL,
 } from '../common/query_iterator';
-import {SCROLLING_TRACK_GROUP} from '../common/state';
+import {SCROLLING_TRACK_GROUP, TrackKindPriority} from '../common/state';
 import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames/common';
 import {ANDROID_LOGS_TRACK_KIND} from '../tracks/android_log/common';
 import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices/common';
@@ -48,6 +48,10 @@
 import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
 
+const MEM_DMA_COUNTER_NAME = 'mem.dma_heap';
+const MEM_DMA = 'mem.dma_buffer';
+const MEM_ION = 'mem.ion';
+
 export async function decideTracks(
     engineId: string, engine: Engine): Promise<DeferredAction[]> {
   return (new TrackDecider(engineId, engine)).decideTracks();
@@ -121,7 +125,7 @@
     } else if (hasUtid) {
       return `utid: ${utid}${kindSuffix}`;
     } else if (hasKind) {
-      return `${kind}`;
+      return `Unnamed ${kind}`;
     }
     return 'Unknown';
   }
@@ -132,6 +136,7 @@
       this.tracksToAdd.push({
         engineId: this.engineId,
         kind: CPU_SLICE_TRACK_KIND,
+        trackKindPriority: TrackKindPriority.ORDINARY,
         name: `Cpu ${cpu}`,
         trackGroup: SCROLLING_TRACK_GROUP,
         config: {
@@ -154,7 +159,7 @@
     for (const cpu of cpus) {
       // Only add a cpu freq track if we have
       // cpu freq data.
-      // TODO(taylori): Find a way to display cpu idle
+      // TODO(hjd): Find a way to display cpu idle
       // events even if there are no cpu freq events.
       const cpuFreqIdle = await this.engine.query(`
       select
@@ -181,6 +186,7 @@
         this.tracksToAdd.push({
           engineId: this.engineId,
           kind: CPU_FREQ_TRACK_KIND,
+          trackKindPriority: TrackKindPriority.ORDINARY,
           name: `Cpu ${cpu} Frequency`,
           trackGroup: SCROLLING_TRACK_GROUP,
           config: {
@@ -203,22 +209,27 @@
     FROM (
       SELECT name, GROUP_CONCAT(track.id) AS track_ids
       FROM track
-      WHERE track.type = "track"
+      WHERE track.type = "track" or track.type = "gpu_track"
       GROUP BY name
     ) AS t CROSS JOIN experimental_slice_layout
     WHERE t.track_ids = experimental_slice_layout.filter_track_ids
-    GROUP BY t.track_ids;
+    GROUP BY t.track_ids
+    ORDER BY t.name;
   `);
     for (let i = 0; i < slowlyCountRows(rawGlobalAsyncTracks); i++) {
-      const name = rawGlobalAsyncTracks.columns[0].stringValues![i];
+      const name = rawGlobalAsyncTracks.columns[0].isNulls![i] ?
+          undefined :
+          rawGlobalAsyncTracks.columns[0].stringValues![i];
       const rawTrackIds = rawGlobalAsyncTracks.columns[1].stringValues![i];
       const trackIds = rawTrackIds.split(',').map(v => Number(v));
       const maxDepth = +rawGlobalAsyncTracks.columns[2].longValues![i];
+      const kind = ASYNC_SLICE_TRACK_KIND;
       const track = {
         engineId: this.engineId,
-        kind: ASYNC_SLICE_TRACK_KIND,
+        kind,
+        trackKindPriority: TrackDecider.inferTrackKindPriority(name),
         trackGroup: SCROLLING_TRACK_GROUP,
-        name,
+        name: TrackDecider.getTrackName({name, kind}),
         config: {
           maxDepth,
           trackIds,
@@ -251,6 +262,7 @@
           engineId: this.engineId,
           kind: COUNTER_TRACK_KIND,
           name: `Gpu ${gpu} Frequency`,
+          trackKindPriority: TrackKindPriority.ORDINARY,
           trackGroup: SCROLLING_TRACK_GROUP,
           config: {
             trackId: +freqExists.columns[0].longValues![0],
@@ -265,12 +277,16 @@
     // Add global or GPU counter tracks that are not bound to any pid/tid.
     const globalCounters = await this.engine.query(`
     select name, id
-    from counter_track
-    where type = 'counter_track'
-    union
-    select name, id
-    from gpu_counter_track
-    where name != 'gpufreq'
+    from (
+      select name, id
+      from counter_track
+      where type = 'counter_track'
+      union
+      select name, id
+      from gpu_counter_track
+      where name != 'gpufreq'
+    )
+    order by name
   `);
     for (let i = 0; i < slowlyCountRows(globalCounters); i++) {
       const name = globalCounters.columns[0].stringValues![i];
@@ -279,6 +295,7 @@
         engineId: this.engineId,
         kind: COUNTER_TRACK_KIND,
         name,
+        trackKindPriority: TrackDecider.inferTrackKindPriority(name),
         trackGroup: SCROLLING_TRACK_GROUP,
         config: {
           name,
@@ -288,6 +305,50 @@
     }
   }
 
+  async groupGlobalIonTracks(): Promise<void> {
+    const ionTracks: AddTrackArgs[] = [];
+    let hasSummary = false;
+    for (const track of this.tracksToAdd) {
+      const isIon = track.name.startsWith(MEM_ION);
+      const isIonCounter = track.name === MEM_ION;
+      const isDmaHeapCounter = track.name === MEM_DMA_COUNTER_NAME;
+      const isDmaBuffferSlices = track.name === MEM_DMA;
+      if (isIon || isIonCounter || isDmaHeapCounter || isDmaBuffferSlices) {
+        ionTracks.push(track);
+      }
+      hasSummary = hasSummary || isIonCounter;
+      hasSummary = hasSummary || isDmaHeapCounter;
+    }
+
+    if (ionTracks.length === 0 || !hasSummary) {
+      return;
+    }
+
+    const id = uuidv4();
+    const summaryTrackId = uuidv4();
+    let foundSummary = false;
+
+    for (const track of ionTracks) {
+      if (!foundSummary &&
+          [MEM_DMA_COUNTER_NAME, MEM_ION].includes(track.name)) {
+        foundSummary = true;
+        track.id = summaryTrackId;
+        track.trackGroup = undefined;
+      } else {
+        track.trackGroup = id;
+      }
+    }
+
+    const addGroup = Actions.addTrackGroup({
+      engineId: this.engineId,
+      summaryTrackId,
+      name: MEM_DMA_COUNTER_NAME,
+      id,
+      collapsed: true,
+    });
+    this.addTrackGroupActions.push(addGroup);
+  }
+
   async addLogsTrack(): Promise<void> {
     const logCount =
         await this.engine.query(`select count(1) from android_logs`);
@@ -296,6 +357,7 @@
         engineId: this.engineId,
         kind: ANDROID_LOGS_TRACK_KIND,
         name: 'Android logs',
+        trackKindPriority: TrackKindPriority.ORDINARY,
         trackGroup: SCROLLING_TRACK_GROUP,
         config: {}
       });
@@ -313,6 +375,7 @@
         engineId: this.engineId,
         kind: SLICE_TRACK_KIND,
         name,
+        trackKindPriority: TrackDecider.inferTrackKindPriority(name),
         trackGroup: upid === 0 ? SCROLLING_TRACK_GROUP :
                                  this.upidToUuid.get(upid),
         config: {
@@ -340,6 +403,7 @@
         engineId: this.engineId,
         kind: 'CounterTrack',
         name,
+        trackKindPriority: TrackDecider.inferTrackKindPriority(name),
         trackGroup: upid === 0 ? SCROLLING_TRACK_GROUP :
                                  this.upidToUuid.get(upid),
         config: {
@@ -384,7 +448,6 @@
       const upid = row.upid;
       const pid = row.pid;
       const threadName = row.threadName;
-      const isMainThread = tid === pid;
       const uuid = this.getUuidUnchecked(utid, upid);
       if (uuid === undefined) {
         // If a thread has no scheduling activity (i.e. the sched table has zero
@@ -398,8 +461,9 @@
         kind,
         name: TrackDecider.getTrackName({utid, tid, threadName, kind}),
         trackGroup: uuid,
-        isMainThread,
-        config: {utid}
+        trackKindPriority:
+            TrackDecider.inferTrackKindPriority(threadName, tid, pid),
+        config: {utid, tid}
       });
     }
   }
@@ -438,6 +502,7 @@
         engineId: this.engineId,
         kind: CPU_PROFILE_TRACK_KIND,
         // TODO(hjd): The threadName can be null, use  instead.
+        trackKindPriority: TrackDecider.inferTrackKindPriority(threadName),
         name: `${threadName} (CPU Stack Samples)`,
         trackGroup: uuid,
         config: {utid},
@@ -492,13 +557,9 @@
         engineId: this.engineId,
         kind,
         name,
+        trackKindPriority: TrackDecider.inferTrackKindPriority(threadName),
         trackGroup: uuid,
-        config: {
-          name,
-          trackId,
-          startTs,
-          endTs,
-        }
+        config: {name, trackId, startTs, endTs, tid}
       });
     }
   }
@@ -513,7 +574,9 @@
           process.pid as pid
         from process_track
         left join process using(upid)
-        where process_track.name not like "% Timeline"
+        where
+            process_track.name is null or
+            process_track.name not like "% Timeline"
         group by
           process_track.upid,
           process_track.name
@@ -553,6 +616,7 @@
         engineId: this.engineId,
         kind,
         name,
+        trackKindPriority: TrackDecider.inferTrackKindPriority(name),
         trackGroup: uuid,
         config: {
           trackIds,
@@ -617,6 +681,7 @@
         engineId: this.engineId,
         kind,
         name,
+        trackKindPriority: TrackDecider.inferTrackKindPriority(trackName),
         trackGroup: uuid,
         config: {
           trackIds,
@@ -681,6 +746,7 @@
         engineId: this.engineId,
         kind,
         name,
+        trackKindPriority: TrackDecider.inferTrackKindPriority(trackName),
         trackGroup: uuid,
         config: {
           trackIds,
@@ -730,7 +796,8 @@
       const upid = row.upid;
       const pid = row.pid;
       const maxDepth = row.maxDepth;
-      const isMainThread = tid === pid;
+      const trackKindPriority =
+          TrackDecider.inferTrackKindPriority(threadName, tid, pid);
 
       const uuid = this.getUuid(utid, upid);
 
@@ -742,11 +809,8 @@
         kind,
         name,
         trackGroup: uuid,
-        isMainThread,
-        config: {
-          trackId,
-          maxDepth,
-        }
+        trackKindPriority,
+        config: {trackId, maxDepth, tid}
       });
     }
   }
@@ -792,6 +856,7 @@
         engineId: this.engineId,
         kind,
         name,
+        trackKindPriority: TrackDecider.inferTrackKindPriority(trackName),
         trackGroup: uuid,
         config: {
           name,
@@ -816,6 +881,7 @@
       this.tracksToAdd.push({
         engineId: this.engineId,
         kind: HEAP_PROFILE_TRACK_KIND,
+        trackKindPriority: TrackKindPriority.ORDINARY,
         name: `Heap Profile`,
         trackGroup: uuid,
         config: {upid}
@@ -952,8 +1018,9 @@
           id: summaryTrackId,
           engineId: this.engineId,
           kind,
+          trackKindPriority: TrackDecider.inferTrackKindPriority(threadName),
           name: `${upid === null ? tid : pid} summary`,
-          config: {pidForColor, upid, utid},
+          config: {pidForColor, upid, utid, tid},
         });
 
         const name = TrackDecider.getTrackName(
@@ -978,11 +1045,12 @@
     await this.addGlobalAsyncTracks();
     await this.addGpuFreqTracks();
     await this.addGlobalCounterTracks();
+    await this.groupGlobalIonTracks();
 
     // Create the per-process track groups. Note that this won't necessarily
     // create a track per process. If a process has been completely idle and has
     // no sched events, no track group will be emitted.
-    // Will populate this.addTrackGroupActions().
+    // Will populate this.addTrackGroupActions
     await this.addProcessTrackGroups();
 
     await this.addProcessHeapProfileTracks();
@@ -1001,4 +1069,24 @@
         Actions.addTracks({tracks: this.tracksToAdd}));
     return this.addTrackGroupActions;
   }
+
+  private static inferTrackKindPriority(
+      threadName?: string|null, tid?: number|null,
+      pid?: number|null): TrackKindPriority {
+    if (pid !== undefined && pid !== null && pid === tid) {
+      return TrackKindPriority.MAIN_THREAD;
+    }
+    if (threadName === undefined || threadName === null) {
+      return TrackKindPriority.ORDINARY;
+    }
+
+    switch (true) {
+      case /.*RenderThread.*/.test(threadName):
+        return TrackKindPriority.RENDER_THREAD;
+      case /.*GPU completion.*/.test(threadName):
+        return TrackKindPriority.GPU_COMPLETION;
+      default:
+        return TrackKindPriority.ORDINARY;
+    }
+  }
 }
diff --git a/ui/src/engine/index.ts b/ui/src/engine/index.ts
index 7c4916b..8a7c27e 100644
--- a/ui/src/engine/index.ts
+++ b/ui/src/engine/index.ts
@@ -12,32 +12,30 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import * as init_trace_processor from '../gen/trace_processor';
+import {assertExists} from '../base/logging';
+import {EngineWorkerInitMessage} from '../common/worker_messages';
+import {WasmBridge} from './wasm_bridge';
 
-import {WasmBridge, WasmBridgeRequest} from './wasm_bridge';
+const selfWorker = self as {} as Worker;
+const wasmBridge = new WasmBridge();
 
-// tslint:disable no-any
-// Proxy all messages to WasmBridge#callWasm.
-const anySelf = (self as any);
+// There are two message handlers here:
+// 1. The Worker (self.onmessage) handler.
+// 2. The MessagePort handler.
+// When the app bootstraps, frontend/index.ts creates a MessageChannel and sends
+// one end to the controller (the other worker) and the other end to us, so that
+// the controller can interact with the Wasm worker without roundtrips through
+// the frontend.
+// The sequence of actions is the following:
+// 1. The frontend does one postMessage({port: MessagePort}) on the Worker
+//    scope. This message transfers the MessagePort (whose other end is
+//    connected to the Conotroller). This is the only postMessage we'll ever
+//    receive here.
+// 2. All the other messages (i.e. the TraceProcessor RPC binary pipe) will be
+//    received on the MessagePort.
 
-// Messages can arrive before we are initialized, queue these for later.
-const msgQueue: MessageEvent[] = [];
-anySelf.onmessage = (msg: MessageEvent) => {
-  msgQueue.push(msg);
+// Receives the boostrap message from the frontend with the MessagePort.
+selfWorker.onmessage = (msg: MessageEvent) => {
+  const port = assertExists((msg.data as EngineWorkerInitMessage).enginePort);
+  wasmBridge.initialize(port);
 };
-
-const bridge = new WasmBridge(init_trace_processor);
-bridge.whenInitialized.then(() => {
-  const handleMsg = (msg: MessageEvent) => {
-    const request: WasmBridgeRequest = msg.data;
-    anySelf.postMessage(bridge.callWasm(request));
-  };
-
-  // Dispatch queued messages.
-  let msg;
-  while (msg = msgQueue.shift()) {
-    handleMsg(msg);
-  }
-
-  anySelf.onmessage = handleMsg;
-});
diff --git a/ui/src/engine/wasm_bridge.ts b/ui/src/engine/wasm_bridge.ts
index fe896b1..79e7e65 100644
--- a/ui/src/engine/wasm_bridge.ts
+++ b/ui/src/engine/wasm_bridge.ts
@@ -23,33 +23,29 @@
 // HEAPU8[reqBufferAddr, +REQ_BUFFER_SIZE].
 const REQ_BUF_SIZE = 32 * 1024 * 1024;
 
-export interface WasmBridgeRequest {
-  id: number;
-  methodName: string;
-  data: Uint8Array;
-}
-
-export interface WasmBridgeResponse {
-  id: number;
-  data: Uint8Array;
-}
-
+// The end-to-end interaction between JS and Wasm is as follows:
+// - [JS] Inbound data received by the worker (onmessage() in engine/index.ts).
+//   - [JS] onRpcDataReceived() (this file)
+//     - [C++] trace_processor_on_rpc_request (wasm_bridge.cc)
+//       - [C++] some TraceProcessor::method()
+//         for (batch in result_rows)
+//           - [C++] RpcResponseFunction(bytes) (wasm_bridge.cc)
+//             - [JS] onReply() (this file)
+//               - [JS] postMessage() (this file)
 export class WasmBridge {
   // When this promise has resolved it is safe to call callWasm.
   whenInitialized: Promise<void>;
 
   private aborted: boolean;
-  private currentRequestResult: WasmBridgeResponse|null;
   private connection: init_trace_processor.Module;
   private reqBufferAddr = 0;
   private lastStderr: string[] = [];
+  private messagePort?: MessagePort;
 
-  constructor(init: init_trace_processor.InitWasm) {
+  constructor() {
     this.aborted = false;
-    this.currentRequestResult = null;
-
     const deferredRuntimeInitialized = defer<void>();
-    this.connection = init({
+    this.connection = init_trace_processor({
       locateFile: (s: string) => s,
       print: (line: string) => console.log(line),
       printErr: (line: string) => this.appendAndLogErr(line),
@@ -58,49 +54,61 @@
     this.whenInitialized = deferredRuntimeInitialized.then(() => {
       const fn = this.connection.addFunction(this.onReply.bind(this), 'vii');
       this.reqBufferAddr = this.connection.ccall(
-          'Initialize',
+          'trace_processor_rpc_init',
           /*return=*/ 'number',
           /*args=*/['number', 'number'],
           [fn, REQ_BUF_SIZE]);
     });
   }
 
-  callWasm(req: WasmBridgeRequest): WasmBridgeResponse {
+  initialize(port: MessagePort) {
+    // Ensure that initialize() is called only once.
+    assertTrue(this.messagePort === undefined);
+    this.messagePort = port;
+    // Note: setting .onmessage implicitly calls port.start() and dispatches the
+    // queued messages. addEventListener('message') doesn't.
+    this.messagePort.onmessage = this.onMessage.bind(this);
+  }
+
+  onMessage(msg: MessageEvent) {
     if (this.aborted) {
       throw new Error('Wasm module crashed');
     }
-    assertTrue(req.data.length <= REQ_BUF_SIZE);
-    const endAddr = this.reqBufferAddr + req.data.length;
-    this.connection.HEAPU8.subarray(this.reqBufferAddr, endAddr).set(req.data);
-    try {
-      this.connection.ccall(
-          req.methodName,    // C method name.
-          'void',            // Return type.
-          ['number'],        // Arg types.
-          [req.data.length]  // Args.
-      );
-      const result = assertExists(this.currentRequestResult);
-      this.currentRequestResult = null;
-      result.id = req.id;
-      return result;
-    } catch (err) {
-      this.aborted = true;
-      let abortReason = `${err}`;
-      if (err instanceof Error) {
-        abortReason = `${err.name}: ${err.message}\n${err.stack}`;
+    assertTrue(msg.data instanceof Uint8Array);
+    const data = msg.data as Uint8Array;
+    let wrSize = 0;
+    // If the request data is larger than our JS<>Wasm interop buffer, split it
+    // into multiple writes. The RPC channel is byte-oriented and is designed to
+    // deal with arbitrary fragmentations.
+    while (wrSize < data.length) {
+      const sliceLen = Math.min(data.length - wrSize, REQ_BUF_SIZE);
+      const dataSlice = data.subarray(wrSize, wrSize + sliceLen);
+      this.connection.HEAPU8.set(dataSlice, this.reqBufferAddr);
+      wrSize += sliceLen;
+      try {
+        this.connection.ccall(
+            'trace_processor_on_rpc_request',  // C function name.
+            'void',                            // Return type.
+            ['number'],                        // Arg types.
+            [sliceLen]                         // Args.
+        );
+      } catch (err) {
+        this.aborted = true;
+        let abortReason = `${err}`;
+        if (err instanceof Error) {
+          abortReason = `${err.name}: ${err.message}\n${err.stack}`;
+        }
+        abortReason += '\n\nstderr: \n' + this.lastStderr.join('\n');
+        throw new Error(abortReason);
       }
-      abortReason += '\n\nstderr: \n' + this.lastStderr.join('\n');
-      throw new Error(abortReason);
-    }
+    }  // while(wrSize < data.length)
   }
 
-  // This is invoked from ccall in the same call stack as callWasm.
+  // This function is bound and passed to Initialize and is called by the C++
+  // code while in the ccall(trace_processor_on_rpc_request).
   private onReply(heapPtr: number, size: number) {
     const data = this.connection.HEAPU8.slice(heapPtr, heapPtr + size);
-    this.currentRequestResult = {
-      id: 0,  // Will be set by callWasm()'s epilogue.
-      data,
-    };
+    assertExists(this.messagePort).postMessage(data, [data.buffer]);
   }
 
   private appendAndLogErr(line: string) {
diff --git a/ui/src/frontend/analytics.ts b/ui/src/frontend/analytics.ts
index 076ed48..c253154 100644
--- a/ui/src/frontend/analytics.ts
+++ b/ui/src/frontend/analytics.ts
@@ -20,10 +20,12 @@
 const PAGE_TITLE = 'no-page-title';
 
 export function initAnalytics() {
-  // Only initialize logging on prod or staging
-  if (window.location.origin.startsWith('http://localhost:') ||
-      window.location.origin.endsWith('.perfetto.dev') ||
-      window.location.origin.endsWith('staging-dot-perfetto-ui.appspot.com')) {
+  // Only initialize logging on the official site and on localhost (to catch
+  // analytics bugs when testing locally).
+  // Skip analytics is the fragment has "testing=1", this is used by UI tests.
+  if ((window.location.origin.startsWith('http://localhost:') ||
+       window.location.origin.endsWith('.perfetto.dev')) &&
+      window.location.search.indexOf('testing=1') < 0) {
     return new AnalyticsImpl();
   }
   return new NullAnalytics();
@@ -40,6 +42,7 @@
   updatePath(_: string): void;
   logEvent(_x: TraceCategories|null, _y: string): void;
   logError(_x: string, _y?: boolean): void;
+  isEnabled(): boolean;
 }
 
 export class NullAnalytics implements Analytics {
@@ -47,6 +50,9 @@
   updatePath(_: string) {}
   logEvent(_x: TraceCategories|null, _y: string) {}
   logError(_x: string) {}
+  isEnabled(): boolean {
+    return false;
+  }
 }
 
 class AnalyticsImpl implements Analytics {
@@ -114,4 +120,8 @@
   logError(description: string, fatal = true) {
     gtagGlobals.gtag('event', 'exception', {description, fatal});
   }
+
+  isEnabled(): boolean {
+    return true;
+  }
 }
diff --git a/ui/src/frontend/chrome_slice_panel.ts b/ui/src/frontend/chrome_slice_panel.ts
index f211cb7..0fa99bb 100644
--- a/ui/src/frontend/chrome_slice_panel.ts
+++ b/ui/src/frontend/chrome_slice_panel.ts
@@ -15,44 +15,140 @@
 import * as m from 'mithril';
 
 import {Actions} from '../common/actions';
+import {Arg, ArgsTree, isArgTreeArray, isArgTreeMap} from '../common/arg_types';
 import {timeToCode, toNs} from '../common/time';
 
-import {Args, globals} from './globals';
+import {globals, SliceDetails} from './globals';
 import {Panel, PanelSize} from './panel';
 import {verticalScrollToTrack} from './scroll_helper';
 
+// Table row contents is one of two things:
+// 1. Key-value pair
+interface KVPair {
+  kind: 'KVPair';
+  key: string;
+  value: Arg;
+}
+
+// 2. Common prefix for values in an array
+interface TableHeader {
+  kind: 'TableHeader';
+  header: string;
+}
+
+type RowContents = KVPair|TableHeader;
+
+function isTableHeader(contents: RowContents): contents is TableHeader {
+  return contents.kind === 'TableHeader';
+}
+
+interface Row {
+  // How many columns (empty or with an index) precede a key
+  indentLevel: number;
+  // Index if the current row is an element of array
+  index: number;
+  contents: RowContents;
+}
+
+class TableBuilder {
+  // Stack contains indices inside repeated fields, or -1 if the appropriate
+  // index is already displayed.
+  stack: number[] = [];
+
+  // Row data generated by builder
+  rows: Row[] = [];
+
+  // Maximum indent level of a key, used to determine total number of columns
+  maxIndent = 0;
+
+  // Add a key-value pair into the table
+  add(key: string, value: Arg) {
+    this.rows.push(
+        {indentLevel: 0, index: -1, contents: {kind: 'KVPair', key, value}});
+  }
+
+  // Add arguments tree into the table
+  addTree(tree: ArgsTree) {
+    this.addTreeInternal(tree, '');
+  }
+
+  // Return indent level and index for a fresh row
+  private prepareRow(): [number, number] {
+    const level = this.stack.length;
+    let index = -1;
+    if (level > 0) {
+      index = this.stack[level - 1];
+      if (index !== -1) {
+        this.stack[level - 1] = -1;
+      }
+    }
+    this.maxIndent = Math.max(this.maxIndent, level);
+    return [level, index];
+  }
+
+  private addTreeInternal(record: ArgsTree, prefix: string) {
+    if (isArgTreeArray(record)) {
+      // Add the current prefix as a separate row
+      const row = this.prepareRow();
+      this.rows.push({
+        indentLevel: row[0],
+        index: row[1],
+        contents: {kind: 'TableHeader', header: prefix}
+      });
+
+      for (let i = 0; i < record.length; i++) {
+        // Push the current array index to the stack.
+        this.stack.push(i);
+        // Prefix is empty for array elements because we don't want to repeat
+        // the common prefix
+        this.addTreeInternal(record[i], '');
+        this.stack.pop();
+      }
+    } else if (isArgTreeMap(record)) {
+      for (const [key, value] of Object.entries(record)) {
+        // If the prefix was non-empty, we have to add dot at the end as well.
+        const newPrefix = (prefix === '') ? key : prefix + '.' + key;
+        this.addTreeInternal(value, newPrefix);
+      }
+    } else {
+      // Leaf value in the tree: add to the table
+      const row = this.prepareRow();
+      this.rows.push({
+        indentLevel: row[0],
+        index: row[1],
+        contents: {kind: 'KVPair', key: prefix, value: record}
+      });
+    }
+  }
+}
+
 export class ChromeSliceDetailsPanel extends Panel {
   view() {
     const sliceInfo = globals.sliceDetails;
     if (sliceInfo.ts !== undefined && sliceInfo.dur !== undefined &&
         sliceInfo.name !== undefined) {
+      const builder = new TableBuilder();
+      builder.add('Name', sliceInfo.name);
+      builder.add(
+          'Category',
+          !sliceInfo.category || sliceInfo.category === '[NULL]' ?
+              'N/A' :
+              sliceInfo.category);
+      builder.add('Start time', timeToCode(sliceInfo.ts));
+      builder.add(
+          'Duration',
+          toNs(sliceInfo.dur) === -1 ? '-1 (Did not end)' :
+                                       timeToCode(sliceInfo.dur));
+      builder.add(
+          'Slice ID', sliceInfo.id ? sliceInfo.id.toString() : 'Unknown');
+      if (sliceInfo.description) {
+        this.fillDescription(sliceInfo.description, builder);
+      }
+      this.fillArgs(sliceInfo, builder);
       return m(
           '.details-panel',
           m('.details-panel-heading', m('h2', `Slice Details`)),
-          m(
-              '.details-table',
-              m('table.half-width',
-                m('tr', m('th', `Name`), m('td', `${sliceInfo.name}`)),
-                m('tr',
-                  m('th', `Category`),
-                  m('td',
-                    `${
-                        sliceInfo.category === '[NULL]' ?
-                            'N/A' :
-                            sliceInfo.category}`)),
-                m('tr',
-                  m('th', `Start time`),
-                  m('td', `${timeToCode(sliceInfo.ts)}`)),
-                m('tr',
-                  m('th', `Duration`),
-                  m('td',
-                    `${
-                        toNs(sliceInfo.dur) === -1 ?
-                            '-1 (Did not end)' :
-                            timeToCode(sliceInfo.dur)}`)),
-                this.getDescription(sliceInfo.description),
-                this.getArgs(sliceInfo.args)),
-              ));
+          m('.details-table', this.renderTable(builder)));
     } else {
       return m(
           '.details-panel',
@@ -66,45 +162,87 @@
 
   renderCanvas(_ctx: CanvasRenderingContext2D, _size: PanelSize) {}
 
-  getArgs(args?: Args): m.Vnode[] {
-    if (!args || args.size === 0) return [];
-    const result = [];
-    for (const [key, value] of args) {
-      if (typeof value === 'string') {
-        result.push(m('tr', m('th', key), m('td', value)));
+  fillArgs(slice: SliceDetails, builder: TableBuilder) {
+    if (slice.argsTree && slice.args) {
+      // Parsed arguments are available, need only to iterate over them to get
+      // slice references
+      for (const [key, value] of slice.args) {
+        if (typeof value !== 'string') {
+          builder.add(key, value);
+        }
+      }
+      builder.addTree(slice.argsTree);
+    } else if (slice.args) {
+      // Parsing has failed, but arguments are available: display them in a flat
+      // 2-column table
+      for (const [key, value] of slice.args) {
+        builder.add(key, value);
+      }
+    }
+  }
+
+  renderTable(builder: TableBuilder): m.Vnode {
+    const rows: m.Vnode[] = [];
+    const keyColumnCount = builder.maxIndent + 1;
+    for (const row of builder.rows) {
+      const renderedRow: m.Vnode[] = [];
+      let indent = row.indentLevel;
+      if (row.index !== -1) {
+        indent--;
+      }
+
+      if (indent > 0) {
+        renderedRow.push(m('td', {colspan: indent}));
+      }
+      if (row.index !== -1) {
+        renderedRow.push(m('td', {class: 'array-index'}, `[${row.index}]`));
+      }
+      if (isTableHeader(row.contents)) {
+        renderedRow.push(
+            m('th',
+              {colspan: keyColumnCount + 1 - row.indentLevel},
+              row.contents.header));
       } else {
-        result.unshift(
-            m('tr',
-              m('th', key),
+        renderedRow.push(
+            m('th',
+              {colspan: keyColumnCount - row.indentLevel},
+              row.contents.key));
+        const value = row.contents.value;
+        if (typeof value === 'string') {
+          renderedRow.push(m('td', value));
+        } else {
+          // Type of value being a record is not propagated into the callback
+          // for some reason, extracting necessary parts as constants instead.
+          const sliceId = value.sliceId;
+          const trackId = value.trackId;
+          renderedRow.push(
               m('td',
                 m('i.material-icons.grey',
                   {
                     onclick: () => {
-                      globals.makeSelection(Actions.selectChromeSlice({
-                        id: value.sliceId,
-                        trackId: value.trackId,
-                        table: 'slice'
-                      }));
+                      globals.makeSelection(Actions.selectChromeSlice(
+                          {id: sliceId, trackId, table: 'slice'}));
                       // Ideally we want to have a callback to
                       // findCurrentSelection after this selection has been
                       // made. Here we do not have the info for horizontally
                       // scrolling to ts.
-                      verticalScrollToTrack(value.trackId, true);
+                      verticalScrollToTrack(trackId, true);
                     },
                     title: 'Go to destination slice'
                   },
-                  'call_made'))));
+                  'call_made')));
+        }
       }
+
+      rows.push(m('tr', renderedRow));
     }
-    return result;
+
+    return m('table.half-width.auto-layout', rows);
   }
 
-  getDescription(description?: Map<string, string>): m.Vnode[] {
-    if (!description) return [];
-    const result = [];
+  fillDescription(description: Map<string, string>, builder: TableBuilder) {
     for (const [key, value] of description) {
-      result.push(m('tr', m('th', key), m('td', value)));
+      builder.add(key, value);
     }
-    return result;
   }
 }
diff --git a/ui/src/frontend/cookie_consent.ts b/ui/src/frontend/cookie_consent.ts
index 3ea5ecf..607b46f 100644
--- a/ui/src/frontend/cookie_consent.ts
+++ b/ui/src/frontend/cookie_consent.ts
@@ -21,10 +21,15 @@
 export class CookieConsent implements m.ClassComponent {
   private showCookieConsent = true;
 
-  view() {
-    if (this.showCookieConsent) {
-      this.showCookieConsent = localStorage.getItem(COOKIE_ACK_KEY) === null;
+  oninit() {
+    this.showCookieConsent = true;
+    if (!globals.logging.isEnabled() ||
+        localStorage.getItem(COOKIE_ACK_KEY) === 'true') {
+      this.showCookieConsent = false;
     }
+  }
+
+  view() {
     if (!this.showCookieConsent) return;
     return m(
         '.cookie-consent',
diff --git a/ui/src/frontend/css_constants.ts b/ui/src/frontend/css_constants.ts
index 3b95eab..5ecaa93 100644
--- a/ui/src/frontend/css_constants.ts
+++ b/ui/src/frontend/css_constants.ts
@@ -16,11 +16,13 @@
 // Also we cannot have global constructors beacause when the javascript is
 // loaded, the CSS might not be ready yet.
 export let TRACK_SHELL_WIDTH = 100;
+export let SIDEBAR_WIDTH = 100;
 export let TRACK_BORDER_COLOR = '#ffc0cb';
 export let TOPBAR_HEIGHT = 48;
 
 export function initCssConstants() {
   TRACK_SHELL_WIDTH = getCssNum('--track-shell-width') || TRACK_SHELL_WIDTH;
+  SIDEBAR_WIDTH = getCssNum('--sidebar-width') || SIDEBAR_WIDTH;
   TRACK_BORDER_COLOR = getCssStr('--track-border-color') || TRACK_BORDER_COLOR;
   TOPBAR_HEIGHT = getCssNum('--topbar-height') || TOPBAR_HEIGHT;
 }
diff --git a/ui/src/frontend/drag/border_drag_strategy.ts b/ui/src/frontend/drag/border_drag_strategy.ts
index b91169d..f186d1f 100644
--- a/ui/src/frontend/drag/border_drag_strategy.ts
+++ b/ui/src/frontend/drag/border_drag_strategy.ts
@@ -16,12 +16,10 @@
 import {DragStrategy} from './drag_strategy';
 
 export class BorderDragStrategy extends DragStrategy {
-  private pixelBounds: [number, number];
   private moveStart = false;
 
-  constructor(timeScale: TimeScale, pixelBounds: [number, number]) {
+  constructor(timeScale: TimeScale, private pixelBounds: [number, number]) {
     super(timeScale);
-    this.pixelBounds = pixelBounds;
   }
 
   onDrag(x: number) {
@@ -30,10 +28,12 @@
     let tEnd =
         this.timeScale.pxToTime(!this.moveStart ? x : this.pixelBounds[1]);
     if (tStart > tEnd) {
-      [tStart, tEnd] = [tEnd, tStart];
       this.moveStart = !this.moveStart;
+      [tEnd, tStart] = [tStart, tEnd];
     }
     super.updateGlobals(tStart, tEnd);
+    this.pixelBounds =
+        [this.timeScale.timeToPx(tStart), this.timeScale.timeToPx(tEnd)];
   }
 
   onDragStart(x: number) {
diff --git a/ui/src/frontend/drag/drag_strategy.ts b/ui/src/frontend/drag/drag_strategy.ts
index 1d272d7..2896849 100644
--- a/ui/src/frontend/drag/drag_strategy.ts
+++ b/ui/src/frontend/drag/drag_strategy.ts
@@ -16,11 +16,7 @@
 import {TimeScale} from '../time_scale';
 
 export abstract class DragStrategy {
-  protected timeScale: TimeScale;
-
-  constructor(timeScale: TimeScale) {
-    this.timeScale = timeScale;
-  }
+  constructor(protected timeScale: TimeScale) {}
 
   abstract onDrag(x: number): void;
 
diff --git a/ui/src/frontend/drag/inner_drag_strategy.ts b/ui/src/frontend/drag/inner_drag_strategy.ts
index a4a0d44..2af1b39 100644
--- a/ui/src/frontend/drag/inner_drag_strategy.ts
+++ b/ui/src/frontend/drag/inner_drag_strategy.ts
@@ -16,11 +16,9 @@
 
 export class InnerDragStrategy extends DragStrategy {
   private dragStartPx = 0;
-  private pixelBounds: [number, number];
 
-  constructor(timeScale: TimeScale, pixelBounds: [number, number]) {
+  constructor(timeScale: TimeScale, private pixelBounds: [number, number]) {
     super(timeScale);
-    this.pixelBounds = pixelBounds;
   }
 
   onDrag(x: number) {
diff --git a/ui/src/frontend/drag/outer_drag_strategy.ts b/ui/src/frontend/drag/outer_drag_strategy.ts
index bde417c..88d127d 100644
--- a/ui/src/frontend/drag/outer_drag_strategy.ts
+++ b/ui/src/frontend/drag/outer_drag_strategy.ts
@@ -17,9 +17,10 @@
   private dragStartPx = 0;
 
   onDrag(x: number) {
-    let tStart = this.timeScale.pxToTime(this.dragStartPx);
-    let tEnd = this.timeScale.pxToTime(x);
-    if (tStart > tEnd) [tStart, tEnd] = [tEnd, tStart];
+    const dragBeginTime = this.timeScale.pxToTime(this.dragStartPx);
+    const dragEndTime = this.timeScale.pxToTime(x);
+    const tStart = Math.min(dragBeginTime, dragEndTime);
+    const tEnd = Math.max(dragBeginTime, dragEndTime);
     super.updateGlobals(tStart, tEnd);
   }
 
diff --git a/ui/src/frontend/drag_gesture_handler.ts b/ui/src/frontend/drag_gesture_handler.ts
index d29cac9..30c0a38 100644
--- a/ui/src/frontend/drag_gesture_handler.ts
+++ b/ui/src/frontend/drag_gesture_handler.ts
@@ -18,6 +18,7 @@
   private readonly boundOnMouseUp = this.onMouseUp.bind(this);
   private clientRect?: DOMRect;
   private pendingMouseDownEvent?: MouseEvent;
+  private _isDragging = false;
 
   constructor(
       private element: HTMLElement,
@@ -28,6 +29,7 @@
   }
 
   private onMouseDown(e: MouseEvent) {
+    this._isDragging = true;
     document.body.addEventListener('mousemove', this.boundOnMouseMove);
     document.body.addEventListener('mouseup', this.boundOnMouseUp);
     this.pendingMouseDownEvent = e;
@@ -62,6 +64,7 @@
   }
 
   private onMouseUp(e: MouseEvent) {
+    this._isDragging = false;
     document.body.removeEventListener('mousemove', this.boundOnMouseMove);
     document.body.removeEventListener('mouseup', this.boundOnMouseUp);
     if (!this.pendingMouseDownEvent) {
@@ -69,4 +72,8 @@
     }
     e.stopPropagation();
   }
+
+  get isDragging() {
+    return this._isDragging;
+  }
 }
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index 9fba9a0..aa763bd 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -52,6 +52,11 @@
     return;
   }
 
+  if (errLog.includes('(ERR:rpc_seq)')) {
+    showRpcSequencingError();
+    return;
+  }
+
   if (timeLastReport > 0 && now - timeLastReport <= MIN_REPORT_PERIOD_MS) {
     queuedErrors.unshift(errLog);
     if (queuedErrors.length > ERR_QUEUE_MAX_LEN) queuedErrors.pop();
@@ -250,3 +255,19 @@
     buttons: []
   });
 }
+
+function showRpcSequencingError() {
+  showModal({
+    title: 'A TraceProcessor RPC error occurred',
+    content: m(
+        'div',
+        m('p', 'The trace processor RPC sequence ID was broken'),
+        m('p', `This can happen when using a HTTP trace processor instance and
+either accidentally sharing this between multiple tabs or
+restarting the trace processor while still in use by UI.`),
+        m('p', `Please refresh this tab and ensure that trace processor is used
+at most one tab at a time.`),
+        ),
+    buttons: []
+  });
+}
diff --git a/ui/src/frontend/flamegraph.ts b/ui/src/frontend/flamegraph.ts
index 77e209a..8836c11 100644
--- a/ui/src/frontend/flamegraph.ts
+++ b/ui/src/frontend/flamegraph.ts
@@ -267,19 +267,21 @@
       const offsetPx = 4;
 
       const lines: string[] = [];
-      let lineSplitter: LineSplitter;
-      const nameText = this.getCallsiteName(this.hoveredCallsite);
-      const nameTextSize = ctx.measureText(nameText);
-      lineSplitter =
-          splitIfTooBig(nameText, width - paddingPx, nameTextSize.width);
-      let textWidth = lineSplitter.lineWidth;
-      lines.push(...lineSplitter.lines);
 
-      const mappingText = this.hoveredCallsite.mapping;
-      lineSplitter =
-          splitIfTooBig(mappingText, width, ctx.measureText(mappingText).width);
-      textWidth = Math.max(textWidth, lineSplitter.lineWidth);
-      lines.push(...lineSplitter.lines);
+      let textWidth = this.addToTooltip(
+          this.getCallsiteName(this.hoveredCallsite),
+          width - paddingPx,
+          ctx,
+          lines);
+      if (this.hoveredCallsite.location != null) {
+        textWidth = Math.max(
+            textWidth,
+            this.addToTooltip(
+                this.hoveredCallsite.location, width, ctx, lines));
+      }
+      textWidth = Math.max(
+          textWidth,
+          this.addToTooltip(this.hoveredCallsite.mapping, width, ctx, lines));
 
       if (this.nodeRendering.totalSize !== undefined) {
         const percentage =
@@ -289,10 +291,8 @@
                 this.hoveredCallsite.totalSize,
                 unit,
                 unit === 'B' ? 1024 : 1000)} (${percentage.toFixed(2)}%)`;
-        lineSplitter = splitIfTooBig(
-            totalSizeText, width, ctx.measureText(totalSizeText).width);
-        textWidth = Math.max(textWidth, lineSplitter.lineWidth);
-        lines.push(...lineSplitter.lines);
+        textWidth = Math.max(
+            textWidth, this.addToTooltip(totalSizeText, width, ctx, lines));
       }
 
       if (this.nodeRendering.selfSize !== undefined &&
@@ -304,10 +304,8 @@
                 this.hoveredCallsite.selfSize,
                 unit,
                 unit === 'B' ? 1024 : 1000)} (${selfPercentage.toFixed(2)}%)`;
-        lineSplitter = splitIfTooBig(
-            selfSizeText, width, ctx.measureText(selfSizeText).width);
-        textWidth = Math.max(textWidth, lineSplitter.lineWidth);
-        lines.push(...lineSplitter.lines);
+        textWidth = Math.max(
+            textWidth, this.addToTooltip(selfSizeText, width, ctx, lines));
       }
 
       // Compute a line height as the bounding box height + 50%:
@@ -344,6 +342,15 @@
     }
   }
 
+  private addToTooltip(
+      text: string, width: number, ctx: CanvasRenderingContext2D,
+      lines: string[]): number {
+    const lineSplitter: LineSplitter =
+        splitIfTooBig(text, width, ctx.measureText(text).width);
+    lines.push(...lineSplitter.lines);
+    return lineSplitter.lineWidth;
+  }
+
   private getCallsiteName(value: CallsiteInfo): string {
     return value.name === undefined || value.name === '' ? 'unknown' :
                                                            value.name;
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index b8e04d0..67ba6ea 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -15,12 +15,17 @@
 import {assertExists} from '../base/logging';
 import {DeferredAction} from '../common/actions';
 import {AggregateData} from '../common/aggregation_data';
+import {Args, ArgsTree} from '../common/arg_types';
+import {
+  ConversionJobName,
+  ConversionJobStatus
+} from '../common/conversion_jobs';
 import {MetricResult} from '../common/metric_data';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
 import {CallsiteInfo, createEmptyState, State} from '../common/state';
 import {fromNs, toNs} from '../common/time';
-import {Analytics, initAnalytics} from '../frontend/analytics';
 
+import {Analytics, initAnalytics} from './analytics';
 import {FrontendLocalState} from './frontend_local_state';
 import {RafScheduler} from './raf_scheduler';
 import {ServiceWorkerController} from './service_worker_controller';
@@ -30,8 +35,6 @@
 type QueryResultsStore = Map<string, {}>;
 type AggregateDataStore = Map<string, AggregateData>;
 type Description = Map<string, string>;
-export type Arg = string|{kind: 'SLICE', trackId: string, sliceId: number};
-export type Args = Map<string, Arg>;
 export interface SliceDetails {
   ts?: number;
   dur?: number;
@@ -47,6 +50,7 @@
   category?: string;
   name?: string;
   args?: Args;
+  argsTree?: ArgsTree;
   description?: Description;
 }
 
@@ -129,7 +133,14 @@
 function getRoot() {
   // Works out the root directory where the content should be served from
   // e.g. `http://origin/v1.2.3/`.
-  let root = (document.currentScript as HTMLScriptElement).src;
+  const script = document.currentScript as HTMLScriptElement;
+
+  // Needed for DOM tests, that do not have script element.
+  if (script === null) {
+    return '';
+  }
+
+  let root = script.src;
   root = root.substr(0, root.lastIndexOf('/') + 1);
   return root;
 }
@@ -170,6 +181,8 @@
   private _traceErrors?: number = undefined;
   private _metricError?: string = undefined;
   private _metricResult?: MetricResult = undefined;
+  private _hasFtrace?: boolean = undefined;
+  private _jobStatus?: Map<ConversionJobName, ConversionJobStatus> = undefined;
 
   private _currentSearchResults: CurrentSearchResults = {
     sliceIds: [],
@@ -371,6 +384,34 @@
     this._currentSearchResults = results;
   }
 
+  get hasFtrace(): boolean {
+    return !!this._hasFtrace;
+  }
+
+  set hasFtrace(value: boolean) {
+    this._hasFtrace = value;
+  }
+
+  getConversionJobStatus(name: ConversionJobName): ConversionJobStatus {
+    return this.getJobStatusMap().get(name) || ConversionJobStatus.NotRunning;
+  }
+
+  setConversionJobStatus(name: ConversionJobName, status: ConversionJobStatus) {
+    const map = this.getJobStatusMap();
+    if (status === ConversionJobStatus.NotRunning) {
+      map.delete(name);
+    } else {
+      map.set(name, status);
+    }
+  }
+
+  private getJobStatusMap(): Map<ConversionJobName, ConversionJobStatus> {
+    if (!this._jobStatus) {
+      this._jobStatus = new Map();
+    }
+    return this._jobStatus;
+  }
+
   setBufferUsage(bufferUsage: number) {
     this._bufferUsage = bufferUsage;
   }
@@ -399,8 +440,23 @@
     // window span). Therefore, zooming out by six levels is 1.1^6 ~= 2.
     // Similarily, zooming in six levels is 0.9^6 ~= 0.5.
     const pxToSec = this.frontendLocalState.timeScale.deltaPxToDuration(1);
+    // TODO(b/186265930): Remove once fixed:
+    if (!isFinite(pxToSec)) {
+      // Resolution is in pixels per second so 1000 means 1px = 1ms.
+      console.error(`b/186265930: Bad pxToSec suppressed ${pxToSec}`);
+      return fromNs(Math.pow(2, Math.floor(Math.log2(toNs(1000)))));
+    }
     const pxToNs = Math.max(toNs(pxToSec), 1);
-    return fromNs(Math.pow(2, Math.floor(Math.log2(pxToNs))));
+    const resolution = fromNs(Math.pow(2, Math.floor(Math.log2(pxToNs))));
+    const log2 = Math.log2(toNs(resolution));
+    if (log2 % 1 !== 0) {
+      throw new Error(`Resolution should be a power of two.
+        pxToSec: ${pxToSec},
+        pxToNs: ${pxToNs},
+        resolution: ${resolution},
+        log2: ${Math.log2(toNs(resolution))}`);
+    }
+    return resolution;
   }
 
   makeSelection(action: DeferredAction<{}>, tabToOpen = 'current_selection') {
diff --git a/ui/src/frontend/heap_profile_panel.ts b/ui/src/frontend/heap_profile_panel.ts
index 3957dce..c444268 100644
--- a/ui/src/frontend/heap_profile_panel.ts
+++ b/ui/src/frontend/heap_profile_panel.ts
@@ -21,7 +21,10 @@
   OBJECTS_ALLOCATED_NOT_FREED_KEY,
   SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
 } from '../common/flamegraph_util';
-import {HeapProfileFlamegraphViewingOption} from '../common/state';
+import {
+  CallsiteInfo,
+  HeapProfileFlamegraphViewingOption
+} from '../common/state';
 import {timeToCode} from '../common/time';
 
 import {PerfettoMouseEvent} from './events';
@@ -50,6 +53,13 @@
   return s;
 }
 
+function toSelectedCallsite(c: CallsiteInfo|undefined): string {
+  if (c !== undefined && c.name !== undefined) {
+    return c.name;
+  }
+  return '(none)';
+}
+
 const RENDER_SELF_AND_TOTAL: NodeRendering = {
   selfSize: 'Self',
   totalSize: 'Total',
@@ -116,6 +126,9 @@
                 ]),
               m('div.details',
                 [
+                  m('div.selected',
+                    `Selected function: ${
+                        toSelectedCallsite(heapDumpInfo.expandedCallsite)}`),
                   m('div.time',
                     `Snapshot time: ${timeToCode(heapDumpInfo.ts)}`),
                   m('input[type=text][placeholder=Focus]', {
diff --git a/ui/src/frontend/help_modal.ts b/ui/src/frontend/help_modal.ts
index a562f47..eac16aa 100644
--- a/ui/src/frontend/help_modal.ts
+++ b/ui/src/frontend/help_modal.ts
@@ -91,6 +91,9 @@
                 keycap('m'),
                 ' (with event or area selected)'),
               m('td', 'Mark the area (persistently)')),
+            m('tr',
+              m('td', keycap('Ctrl'), ' + ', keycap('b')),
+              m('td', 'Toggle display of sidebar')),
             m('tr', m('td', keycap('?')), m('td', 'Show help')),
             )),
     buttons: [],
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 553b830..80712cf 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -22,6 +22,7 @@
 import {forwardRemoteCalls} from '../base/remote';
 import {Actions} from '../common/actions';
 import {AggregateData} from '../common/aggregation_data';
+import {ConversionJobStatusUpdate} from '../common/conversion_jobs';
 import {
   LogBoundsKey,
   LogEntriesKey,
@@ -30,6 +31,10 @@
 } from '../common/logs';
 import {MetricResult} from '../common/metric_data';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
+import {
+  ControllerWorkerInitMessage,
+  EngineWorkerInitMessage
+} from '../common/worker_messages';
 
 import {AnalyzePage} from './analyze_page';
 import {loadAndroidBugToolInfo} from './android_bug_tool';
@@ -61,6 +66,13 @@
 
 const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine';
 
+function isLocalhostTraceUrl(url: string): boolean {
+  return ['127.0.0.1', 'localhost'].includes((new URL(url)).hostname);
+}
+
+let idleWasmWorker: Worker;
+let activeWasmWorker: Worker;
+
 /**
  * The API the main thread exposes to the controller.
  */
@@ -184,6 +196,16 @@
     this.redraw();
   }
 
+  publishHasFtrace(hasFtrace: boolean) {
+    globals.hasFtrace = hasFtrace;
+    this.redraw();
+  }
+
+  publishConversionJobStatusUpdate(job: ConversionJobStatusUpdate) {
+    globals.setConversionJobStatus(job.jobName, job.jobStatus);
+    this.redraw();
+  }
+
   publishFileDownload(args: {file: File, name?: string}) {
     const url = URL.createObjectURL(args.file);
     const a = document.createElement('a');
@@ -207,7 +229,6 @@
     const arr = new Uint8Array(args.data, 0, args.size);
     const str = (new TextDecoder('utf-8')).decode(arr);
     openBufferWithLegacyTraceViewer('trace.json', str, 0);
-    globals.dispatch(Actions.clearConversionInProgress({}));
   }
 
   publishBufferUsage(args: {percentage: number}) {
@@ -251,6 +272,24 @@
     this.redraw();
   }
 
+  // This method is called by the controller via the Remote<> interface whenver
+  // a new trace is loaded. This creates a new worker and passes it the
+  // MessagePort received by the controller. This is because on Safari, all
+  // workers must be spawned from the main thread.
+  resetEngineWorker(port: MessagePort) {
+    // We keep always an idle worker around, the first one is created by the
+    // main() below, so we can hide the latency of the Wasm initialization.
+    if (activeWasmWorker !== undefined) {
+      activeWasmWorker.terminate();
+    }
+    // Swap the active worker with the idle one and create a new idle worker
+    // for the next trace.
+    activeWasmWorker = assertExists(idleWasmWorker);
+    const msg: EngineWorkerInitMessage = {enginePort: port};
+    activeWasmWorker.postMessage(msg, [port]);
+    idleWasmWorker = new Worker(globals.root + 'engine_bundle.js');
+  }
+
   private redraw(): void {
     if (globals.state.route &&
         globals.state.route !== this.router.getRouteFromHash()) {
@@ -298,7 +337,7 @@
       'https://www.google-analytics.com',
       'https://www.googletagmanager.com',
     ],
-    'navigate-to': ['https://*.perfetto.dev']
+    'navigate-to': ['https://*.perfetto.dev', 'self'],
   };
   const meta = document.createElement('meta');
   meta.httpEquiv = 'Content-Security-Policy';
@@ -342,6 +381,7 @@
   window.addEventListener('unhandledrejection', e => reportError(e));
 
   const controller = new Worker(globals.root + 'controller_bundle.js');
+  idleWasmWorker = new Worker(globals.root + 'engine_bundle.js');
   const frontendChannel = new MessageChannel();
   const controllerChannel = new MessageChannel();
   const extensionLocalChannel = new MessageChannel();
@@ -350,20 +390,18 @@
   errorReportingChannel.port2.onmessage = (e) =>
       maybeShowErrorDialog(`${e.data}`);
 
-  controller.postMessage(
-      {
-        frontendPort: frontendChannel.port1,
-        controllerPort: controllerChannel.port1,
-        extensionPort: extensionLocalChannel.port1,
-        errorReportingPort: errorReportingChannel.port1,
-      },
-      [
-        frontendChannel.port1,
-        controllerChannel.port1,
-        extensionLocalChannel.port1,
-        errorReportingChannel.port1,
-      ]);
-
+  const msg: ControllerWorkerInitMessage = {
+    frontendPort: frontendChannel.port1,
+    controllerPort: controllerChannel.port1,
+    extensionPort: extensionLocalChannel.port1,
+    errorReportingPort: errorReportingChannel.port1,
+  };
+  controller.postMessage(msg, [
+    msg.frontendPort,
+    msg.controllerPort,
+    msg.extensionPort,
+    msg.errorReportingPort,
+  ]);
   const dispatch =
       controllerChannel.port2.postMessage.bind(controllerChannel.port2);
   globals.initialize(dispatch, controller);
@@ -442,9 +480,22 @@
       hash: stateHash,
     }));
   } else if (typeof urlHash === 'string' && urlHash) {
-    globals.dispatch(Actions.openTraceFromUrl({
-      url: urlHash,
-    }));
+    if (isLocalhostTraceUrl(urlHash)) {
+      const fileName = urlHash.split('/').pop() || 'local_trace.pftrace';
+      const request = fetch(urlHash)
+                          .then(response => response.blob())
+                          .then(blob => {
+                            globals.dispatch(Actions.openTraceFromFile({
+                              file: new File([blob], fileName),
+                            }));
+                          })
+                          .catch(e => alert(`Could not load local trace ${e}`));
+      taskTracker.trackPromise(request, 'Downloading local trace');
+    } else {
+      globals.dispatch(Actions.openTraceFromUrl({
+        url: urlHash,
+      }));
+    }
   } else if (androidBugTool) {
     // TODO(hjd): Unify updateStatus and TaskTracker
     globals.dispatch(Actions.updateStatus({
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 7deea7d..55dc47a 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -94,6 +94,8 @@
 
     for (const note of Object.values(globals.state.notes)) {
       const timestamp = getStartTimestamp(note);
+      // TODO(hjd): We should still render area selection marks in viewport is
+      // *within* the area (e.g. both lhs and rhs are out of bounds).
       if ((note.noteType !== 'AREA' && !timeScale.timeInBounds(timestamp)) ||
           (note.noteType === 'AREA' &&
            !timeScale.timeInBounds(globals.state.areas[note.areaId].endSec) &&
@@ -139,7 +141,7 @@
     }
 
     // A real note is hovered so we don't need to see the preview line.
-    // TODO(taylori): Change cursor to pointer here.
+    // TODO(hjd): Change cursor to pointer here.
     if (aNoteIsHovered) globals.frontendLocalState.setHoveredNoteTimestamp(-1);
 
     // View preview note flag when hovering on notes panel.
@@ -182,12 +184,12 @@
     ctx.stroke();
 
     // Start line after track shell section, join triangles.
-    const startDraw =
-        Math.max(
-            x,
-            globals.frontendLocalState.timeScale.startPx + TRACK_SHELL_WIDTH) -
-        1;
-    ctx.fillRect(startDraw, topOffset - 1, xEnd - startDraw + 1, 1);
+    const startDraw = Math.max(
+        x, globals.frontendLocalState.timeScale.startPx + TRACK_SHELL_WIDTH);
+    ctx.beginPath();
+    ctx.moveTo(startDraw, topOffset);
+    ctx.lineTo(xEnd, topOffset);
+    ctx.stroke();
   }
 
   private drawFlag(
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index 37fd1a9..d870729 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -18,7 +18,7 @@
 import {hueForCpu} from '../common/colorizer';
 import {TimeSpan, timeToString} from '../common/time';
 
-import {TRACK_SHELL_WIDTH} from './css_constants';
+import {SIDEBAR_WIDTH, TRACK_SHELL_WIDTH} from './css_constants';
 import {BorderDragStrategy} from './drag/border_drag_strategy';
 import {DragStrategy} from './drag/drag_strategy';
 import {InnerDragStrategy} from './drag/inner_drag_strategy';
@@ -29,12 +29,14 @@
 import {TimeScale} from './time_scale';
 
 export class OverviewTimelinePanel extends Panel {
+  private static HANDLE_SIZE_PX = 7;
+
   private width = 0;
   private gesture?: DragGestureHandler;
   private timeScale?: TimeScale;
   private totTime = new TimeSpan(0, 0);
-  private static BORDER_PIXEL_DELTA = 30;
   private dragStrategy?: DragStrategy;
+  private readonly boundOnMouseMove = this.onMouseMove.bind(this);
 
   // Must explicitly type now; arguments types are no longer auto-inferred.
   // https://github.com/Microsoft/TypeScript/issues/1373
@@ -56,6 +58,13 @@
 
   oncreate(vnode: m.CVnodeDOM) {
     this.onupdate(vnode);
+    (vnode.dom as HTMLElement)
+        .addEventListener('mousemove', this.boundOnMouseMove);
+  }
+
+  onremove({dom}: m.CVnodeDOM) {
+    (dom as HTMLElement)
+        .removeEventListener('mousemove', this.boundOnMouseMove);
   }
 
   view() {
@@ -109,9 +118,8 @@
     ctx.fillRect(0, size.height - 1, this.width, 1);
 
     // Draw semi-opaque rects that occlude the non-visible time range.
-    const vizTime = globals.frontendLocalState.visibleWindowTime;
-    const vizStartPx = Math.floor(this.timeScale.timeToPx(vizTime.start));
-    const vizEndPx = Math.ceil(this.timeScale.timeToPx(vizTime.end));
+    const [vizStartPx, vizEndPx] =
+        OverviewTimelinePanel.extractBounds(this.timeScale);
 
     ctx.fillStyle = 'rgba(200, 200, 200, 0.8)';
     ctx.fillRect(
@@ -125,6 +133,45 @@
     ctx.fillStyle = '#999';
     ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight);
     ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight);
+
+    const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX;
+    const hbarDivisionFactor = 3.5;
+    // Draw handlebar
+    ctx.fillRect(
+        vizStartPx - Math.floor(hbarWidth / 2) - 1,
+        headerHeight,
+        hbarWidth,
+        tracksHeight / hbarDivisionFactor);
+    ctx.fillRect(
+        vizEndPx - Math.floor(hbarWidth / 2),
+        headerHeight,
+        hbarWidth,
+        tracksHeight / hbarDivisionFactor);
+  }
+
+  private onMouseMove(e: MouseEvent) {
+    if (this.gesture === undefined || this.gesture.isDragging) {
+      return;
+    }
+    (e.target as HTMLElement).style.cursor = this.chooseCursor(e.x);
+  }
+
+  private chooseCursor(x: number) {
+    if (this.timeScale === undefined) return 'default';
+    const [vizStartPx, vizEndPx] =
+        OverviewTimelinePanel.extractBounds(this.timeScale);
+    const startBound = vizStartPx - 1 + SIDEBAR_WIDTH;
+    const endBound = vizEndPx + SIDEBAR_WIDTH;
+    if (OverviewTimelinePanel.inBorderRange(x, startBound) ||
+        OverviewTimelinePanel.inBorderRange(x, endBound)) {
+      return 'ew-resize';
+    } else if (x < SIDEBAR_WIDTH + TRACK_SHELL_WIDTH) {
+      return 'default';
+    } else if (x < startBound || endBound < x) {
+      return 'crosshair';
+    } else {
+      return 'all-scroll';
+    }
   }
 
   onDrag(x: number) {
@@ -134,11 +181,7 @@
 
   onDragStart(x: number) {
     if (this.timeScale === undefined) return;
-    const timeSpan = globals.frontendLocalState.getVisibleStateBounds();
-    const pixelBounds: [number, number] = [
-      this.timeScale.timeToPx(timeSpan[0]),
-      this.timeScale.timeToPx(timeSpan[1])
-    ];
+    const pixelBounds = OverviewTimelinePanel.extractBounds(this.timeScale);
     if (OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) ||
         OverviewTimelinePanel.inBorderRange(x, pixelBounds[1])) {
       this.dragStrategy = new BorderDragStrategy(this.timeScale, pixelBounds);
@@ -154,7 +197,15 @@
     this.dragStrategy = undefined;
   }
 
+  private static extractBounds(timeScale: TimeScale): [number, number] {
+    const vizTime = globals.frontendLocalState.getVisibleStateBounds();
+    return [
+      Math.floor(timeScale.timeToPx(vizTime[0])),
+      Math.ceil(timeScale.timeToPx(vizTime[1]))
+    ];
+  }
+
   private static inBorderRange(a: number, b: number): boolean {
-    return Math.abs(a - b) < this.BORDER_PIXEL_DELTA;
+    return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2;
   }
 }
diff --git a/ui/src/frontend/pan_and_zoom_handler.ts b/ui/src/frontend/pan_and_zoom_handler.ts
index b5a814d..166a3ce 100644
--- a/ui/src/frontend/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/pan_and_zoom_handler.ts
@@ -279,7 +279,7 @@
     handleKey(e, false);
   }
 
-  // TODO(taylori): Move this shift handling into the viewer page.
+  // TODO(hjd): Move this shift handling into the viewer page.
   private updateShift(down: boolean) {
     if (down === this.shiftDown) return;
     this.shiftDown = down;
diff --git a/ui/src/frontend/raf_scheduler.ts b/ui/src/frontend/raf_scheduler.ts
index dd708c2..eccf094 100644
--- a/ui/src/frontend/raf_scheduler.ts
+++ b/ui/src/frontend/raf_scheduler.ts
@@ -115,6 +115,10 @@
     }
   }
 
+  get hasPendingRedraws(): boolean {
+    return this.isRedrawing || this.hasScheduledNextFrame;
+  }
+
   private syncCanvasRedraw(nowMs: number) {
     const redrawStart = debugNow();
     if (this.isRedrawing) return;
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index 4de254a..575b2c8 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -22,6 +22,7 @@
   AdbRecordingTarget,
   getBuiltinChromeCategoryList,
   getDefaultRecordingTargets,
+  hasActiveProbes,
   isAdbTarget,
   isAndroidP,
   isAndroidTarget,
@@ -55,44 +56,43 @@
 const POLL_INTERVAL_MS = [250, 500, 1000, 2500, 5000, 30000, 60000];
 
 const ATRACE_CATEGORIES = new Map<string, string>();
+ATRACE_CATEGORIES.set('adb', 'ADB');
+ATRACE_CATEGORIES.set('aidl', 'AIDL calls');
+ATRACE_CATEGORIES.set('am', 'Activity Manager');
+ATRACE_CATEGORIES.set('audio', 'Audio');
+ATRACE_CATEGORIES.set('binder_driver', 'Binder Kernel driver');
+ATRACE_CATEGORIES.set('binder_lock', 'Binder global lock trace');
+ATRACE_CATEGORIES.set('bionic', 'Bionic C library');
+ATRACE_CATEGORIES.set('camera', 'Camera');
+ATRACE_CATEGORIES.set('dalvik', 'ART & Dalvik');
+ATRACE_CATEGORIES.set('database', 'Database');
 ATRACE_CATEGORIES.set('gfx', 'Graphics');
+ATRACE_CATEGORIES.set('hal', 'Hardware Modules');
 ATRACE_CATEGORIES.set('input', 'Input');
+ATRACE_CATEGORIES.set('network', 'Network');
+ATRACE_CATEGORIES.set('nnapi', 'Neural Network API');
+ATRACE_CATEGORIES.set('pm', 'Package Manager');
+ATRACE_CATEGORIES.set('power', 'Power Management');
+ATRACE_CATEGORIES.set('res', 'Resource Loading');
+ATRACE_CATEGORIES.set('rro', 'Resource Overlay');
+ATRACE_CATEGORIES.set('rs', 'RenderScript');
+ATRACE_CATEGORIES.set('sm', 'Sync Manager');
+ATRACE_CATEGORIES.set('ss', 'System Server');
+ATRACE_CATEGORIES.set('vibrator', 'Vibrator');
+ATRACE_CATEGORIES.set('video', 'Video');
 ATRACE_CATEGORIES.set('view', 'View System');
 ATRACE_CATEGORIES.set('webview', 'WebView');
 ATRACE_CATEGORIES.set('wm', 'Window Manager');
-ATRACE_CATEGORIES.set('am', 'Activity Manager');
-ATRACE_CATEGORIES.set('sm', 'Sync Manager');
-ATRACE_CATEGORIES.set('audio', 'Audio');
-ATRACE_CATEGORIES.set('video', 'Video');
-ATRACE_CATEGORIES.set('camera', 'Camera');
-ATRACE_CATEGORIES.set('hal', 'Hardware Modules');
-ATRACE_CATEGORIES.set('res', 'Resource Loading');
-ATRACE_CATEGORIES.set('dalvik', 'ART & Dalvik');
-ATRACE_CATEGORIES.set('rs', 'RenderScript');
-ATRACE_CATEGORIES.set('bionic', 'Bionic C library');
-ATRACE_CATEGORIES.set('gfx', 'Graphics');
-ATRACE_CATEGORIES.set('power', 'Power Management');
-ATRACE_CATEGORIES.set('pm', 'Package Manager');
-ATRACE_CATEGORIES.set('ss', 'System Server');
-ATRACE_CATEGORIES.set('database', 'Database');
-ATRACE_CATEGORIES.set('network', 'Network');
-ATRACE_CATEGORIES.set('adb', 'ADB');
-ATRACE_CATEGORIES.set('vibrator', 'Vibrator');
-ATRACE_CATEGORIES.set('aidl', 'AIDL calls');
-ATRACE_CATEGORIES.set('nnapi', 'Neural Network API');
-ATRACE_CATEGORIES.set('rro', 'Resource Overlay');
-ATRACE_CATEGORIES.set('binder_driver', 'Binder Kernel driver');
-ATRACE_CATEGORIES.set('binder_lock', 'Binder global lock trace');
 
 const LOG_BUFFERS = new Map<string, string>();
-LOG_BUFFERS.set('LID_DEFAULT', 'Main');
-LOG_BUFFERS.set('LID_RADIO', 'Radio');
-LOG_BUFFERS.set('LID_EVENTS', 'Binary events');
-LOG_BUFFERS.set('LID_SYSTEM', 'System');
 LOG_BUFFERS.set('LID_CRASH', 'Crash');
-LOG_BUFFERS.set('LID_STATS', 'Stats');
-LOG_BUFFERS.set('LID_SECURITY', 'Security');
+LOG_BUFFERS.set('LID_DEFAULT', 'Main');
+LOG_BUFFERS.set('LID_EVENTS', 'Binary events');
 LOG_BUFFERS.set('LID_KERNEL', 'Kernel');
+LOG_BUFFERS.set('LID_RADIO', 'Radio');
+LOG_BUFFERS.set('LID_SECURITY', 'Security');
+LOG_BUFFERS.set('LID_STATS', 'Stats');
+LOG_BUFFERS.set('LID_SYSTEM', 'System');
 
 const FTRACE_CATEGORIES = new Map<string, string>();
 FTRACE_CATEGORIES.set('binder/*', 'binder');
@@ -290,8 +290,9 @@
       } as ProbeAttrs),
       m(Probe, {
         title: 'Syscalls',
-        img: null,
-        descr: `Tracks the enter and exit of all syscalls.`,
+        img: 'rec_syscalls.png',
+        descr: `Tracks the enter and exit of all syscalls. On Android
+                requires a userdebug or eng build.`,
         setEnabled: (cfg, val) => cfg.cpuSyscall = val,
         isEnabled: (cfg) => cfg.cpuSyscall
       } as ProbeAttrs));
@@ -406,7 +407,7 @@
         setEnabled: (cfg, val) => cfg.hpAllHeaps = val,
         isEnabled: (cfg) => cfg.hpAllHeaps
       } as ToggleAttrs)
-      // TODO(taylori): Add advanced options.
+      // TODO(hjd): Add advanced options.
   );
 }
 
@@ -627,7 +628,15 @@
           options: LOG_BUFFERS,
           set: (cfg, val) => cfg.androidLogBuffers = val,
           get: (cfg) => cfg.androidLogBuffers
-        } as DropdownAttrs)));
+        } as DropdownAttrs)),
+      m(Probe, {
+        title: 'Frame timeline',
+        img: 'rec_frame_timeline.png',
+        descr: `Records expected/actual frame timings from surface_flinger.
+                    Requires Android 12 (S) or above.`,
+        setEnabled: (cfg, val) => cfg.androidFrameTimeline = val,
+        isEnabled: (cfg) => cfg.androidFrameTimeline
+      } as ProbeAttrs));
 }
 
 
@@ -709,26 +718,32 @@
 
   // Show "disabled-by-default" categories last.
   const categoriesMap = new Map<string, string>();
-  const disabledByDefaultCategories: string[] = [];
   const disabledPrefix = 'disabled-by-default-';
+  const overheadSuffix = '(high overhead)';
   categories.forEach(cat => {
     if (cat.startsWith(disabledPrefix)) {
-      disabledByDefaultCategories.push(cat);
+      categoriesMap.set(
+          cat, `${cat.replace(disabledPrefix, '')} ${overheadSuffix}`);
     } else {
       categoriesMap.set(cat, cat);
     }
   });
-  disabledByDefaultCategories.forEach(cat => {
-    categoriesMap.set(
-        cat, `${cat.replace(disabledPrefix, '')} (high overhead)`);
-  });
 
   return m(Dropdown, {
     title: 'Additional Chrome categories',
     cssClass: '.multicolumn.two-columns',
     options: categoriesMap,
     set: (cfg, val) => cfg.chromeCategoriesSelected = val,
-    get: (cfg) => cfg.chromeCategoriesSelected
+    get: (cfg) => cfg.chromeCategoriesSelected,
+    sort: (a, b) => {
+      const aIsDisabled = a.includes(overheadSuffix);
+      const bIsDisabled = b.includes(overheadSuffix);
+      if (aIsDisabled === bIsDisabled) {
+        return a.localeCompare(b);
+      } else {
+        return Number(aIsDisabled) - Number(bIsDisabled);
+      }
+    },
   } as DropdownAttrs);
 }
 
@@ -878,7 +893,7 @@
 function Instructions(cssClass: string) {
   return m(
       `.record-section.instructions${cssClass}`,
-      m('header', 'Trace command'),
+      m('header', 'Recording command'),
       localStorage.hasOwnProperty(LOCAL_STORAGE_SHOW_CONFIG) ?
           m('button.permalinkconfig',
             {
@@ -1037,6 +1052,15 @@
         {href: cmdlineUrl, target: '_blank'},
         `collect the trace using ADB.`));
 
+  const msgZeroProbes =
+      m('.note',
+        'It looks like you didn\'t add any probes. ' +
+            'Please add at least one to get a non-empty trace.');
+
+  if (!hasActiveProbes(globals.state.recordConfig)) {
+    notes.push(msgZeroProbes);
+  }
+
   if (isAdbTarget(globals.state.recordingTarget)) {
     notes.push(msgRecordingNotSupported);
   }
@@ -1302,7 +1326,7 @@
         m('a[href="#!/record?p=instructions"]',
           m(`li${routePage === 'instructions' ? '.active' : ''}`,
             m('i.material-icons.rec', 'fiber_manual_record'),
-            m('.title', 'Trace command'),
+            m('.title', 'Recording command'),
             m('.sub', 'Manually record trace'))),
         localStorage.hasOwnProperty(LOCAL_STORAGE_SHOW_CONFIG) ?
             m('a[href="#!/record?p=config"]',
diff --git a/ui/src/frontend/record_widgets.ts b/ui/src/frontend/record_widgets.ts
index fd919a8..4f71447 100644
--- a/ui/src/frontend/record_widgets.ts
+++ b/ui/src/frontend/record_widgets.ts
@@ -22,10 +22,13 @@
 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;
 
+function defaultSort(a: string, b: string) {
+  return a.localeCompare(b);
+}
+
 // +---------------------------------------------------------------------------+
 // | Docs link with 'i' in circle icon.                                        |
 // +---------------------------------------------------------------------------+
@@ -220,13 +223,14 @@
   title: string;
   cssClass?: string;
   options: Map<string, string>;
+  sort?: (a: string, b: string) => number;
   get: Getter<string[]>;
   set: Setter<string[]>;
 }
 
 export class Dropdown implements m.ClassComponent<DropdownAttrs> {
   resetScroll(dom: HTMLSelectElement) {
-    // Chrome seems to override the scroll offset on creation without this,
+    // Chrome seems to override the scroll offset on creationa, b without this,
     // even though we call it after having marked the options as selected.
     setTimeout(() => {
       // Don't reset the scroll position if the element is still focused.
@@ -251,7 +255,10 @@
     const options: m.Children = [];
     const selItems = attrs.get(globals.state.recordConfig);
     let numSelected = 0;
-    for (const [key, label] of attrs.options) {
+    const entries = [...attrs.options.entries()];
+    const f = attrs.sort === undefined ? defaultSort : attrs.sort;
+    entries.sort((a, b) => f(a[1], b[1]));
+    for (const [key, label] of entries) {
       const opts = {value: key, selected: false};
       if (selItems.includes(key)) {
         opts.selected = true;
diff --git a/ui/src/frontend/rpc_http_dialog.ts b/ui/src/frontend/rpc_http_dialog.ts
index 40e9036..77ee0fe 100644
--- a/ui/src/frontend/rpc_http_dialog.ts
+++ b/ui/src/frontend/rpc_http_dialog.ts
@@ -13,14 +13,22 @@
 // limitations under the License.
 
 import '../tracks/all_frontend';
+
 import * as m from 'mithril';
 
+import {assertExists} from '../base/logging';
 import {Actions} from '../common/actions';
 import {HttpRpcEngine, RPC_URL} from '../common/http_rpc_engine';
+import {StatusResult} from '../common/protos';
+import * as version from '../gen/perfetto_version';
+import {perfetto} from '../gen/protos';
 
 import {globals} from './globals';
 import {showModal} from './modal';
 
+const CURRENT_API_VERSION = perfetto.protos.TraceProcessorApiVersion
+                                .TRACE_PROCESSOR_CURRENT_API_VERSION;
+
 const PROMPT = `Trace Processor Native Accelerator detected on ${RPC_URL} with:
 $loadedTraceName
 
@@ -43,6 +51,28 @@
 too old. Get the latest version from get.perfetto.dev/trace_processor.
 `;
 
+
+const MSG_TOO_OLD = `The Trace Processor instance on ${RPC_URL} is too old.
+
+This UI requires TraceProcessor features that are not present in the
+Trace Processor native accelerator you are currently running.
+If you continue, this is almost surely going to cause UI failures.
+
+Please update your local Trace Processor binary:
+
+curl -LO https://get.perfetto.dev/trace_processor
+chmod +x ./trace_processor
+./trace_processor --httpd
+
+UI version: ${version.VERSION}
+TraceProcessor RPC API required: ${CURRENT_API_VERSION} or higher
+
+TraceProcessor version: $tpVersion
+RPC API: $tpApi
+`;
+
+let forceUseOldVersion = false;
+
 // Try to connect to the external Trace Processor HTTP RPC accelerator (if
 // available, often it isn't). If connected it will populate the
 // |httpRpcState| in the frontend local state. In turn that will show the UI
@@ -53,41 +83,81 @@
 export async function CheckHttpRpcConnection(): Promise<void> {
   const state = await HttpRpcEngine.checkConnection();
   globals.frontendLocalState.setHttpRpcState(state);
+  if (!state.connected) return;
+  const tpStatus = assertExists(state.status);
 
-  // If a trace is already loaded in the trace processor (e.g., the user
-  // launched trace_processor_shell -D trace_file.pftrace), prompt the user to
-  // initialize the UI with the already-loaded trace.
-  if (state.connected && state.loadedTraceName) {
-    showModal({
-      title: 'Use Trace Processor Native Acceleration?',
-      content:
-          m('.modal-pre',
-            PROMPT.replace('$loadedTraceName', state.loadedTraceName)),
-      buttons: [
-        {
-          text: 'YES, use loaded trace',
-          primary: true,
-          id: 'rpc_load',
-          action: () => {
-            globals.dispatch(Actions.openTraceFromHttpRpc({}));
-          }
-        },
-        {
-          text: 'YES, but reset state',
-          primary: false,
-          id: 'rpc_reset',
-          action: () => {}
-        },
-        {
-          text: 'NO, Use builtin WASM',
-          primary: false,
-          id: 'rpc_force_wasm',
-          action: () => {
-            globals.dispatch(
-                Actions.setNewEngineMode({mode: 'FORCE_BUILTIN_WASM'}));
-          }
-        },
-      ],
-    });
+  if (tpStatus.apiVersion < CURRENT_API_VERSION) {
+    await showDialogTraceProcessorTooOld(tpStatus);
+    if (!forceUseOldVersion) return;
   }
+
+  if (tpStatus.loadedTraceName) {
+    // If a trace is already loaded in the trace processor (e.g., the user
+    // launched trace_processor_shell -D trace_file.pftrace), prompt the user to
+    // initialize the UI with the already-loaded trace.
+    return showDialogToUsePreloadedTrace(tpStatus);
+  }
+}
+
+async function showDialogTraceProcessorTooOld(tpStatus: StatusResult) {
+  return showModal({
+    title: 'Your Trace Processor binary is outdated',
+    content:
+        m('.modal-pre',
+          MSG_TOO_OLD.replace('$tpVersion', tpStatus.humanReadableVersion)
+              .replace('$tpApi', `${tpStatus.apiVersion}`)),
+    buttons: [
+      {
+        text: 'Use builtin Wasm',
+        primary: true,
+        id: 'tp_old_wasm',
+        action: () => {
+          globals.dispatch(
+              Actions.setNewEngineMode({mode: 'FORCE_BUILTIN_WASM'}));
+        }
+      },
+      {
+        text: 'Use old version regardless (might crash)',
+        primary: false,
+        id: 'tp_old_cont',
+        action: () => {
+          forceUseOldVersion = true;
+        }
+      },
+    ],
+  });
+}
+
+async function showDialogToUsePreloadedTrace(tpStatus: StatusResult) {
+  return showModal({
+    title: 'Use Trace Processor Native Acceleration?',
+    content:
+        m('.modal-pre',
+          PROMPT.replace('$loadedTraceName', tpStatus.loadedTraceName)),
+    buttons: [
+      {
+        text: 'YES, use loaded trace',
+        primary: true,
+        id: 'rpc_load',
+        action: () => {
+          globals.dispatch(Actions.openTraceFromHttpRpc({}));
+        }
+      },
+      {
+        text: 'YES, but reset state',
+        primary: false,
+        id: 'rpc_reset',
+        action: () => {}
+      },
+      {
+        text: 'NO, Use builtin Wasm',
+        primary: false,
+        id: 'rpc_force_wasm',
+        action: () => {
+          globals.dispatch(
+              Actions.setNewEngineMode({mode: 'FORCE_BUILTIN_WASM'}));
+        }
+      },
+    ],
+  });
 }
diff --git a/ui/src/frontend/scroll_helper.ts b/ui/src/frontend/scroll_helper.ts
index 53874c7..be24399 100644
--- a/ui/src/frontend/scroll_helper.ts
+++ b/ui/src/frontend/scroll_helper.ts
@@ -29,7 +29,7 @@
   const endNs = toNs(globals.frontendLocalState.visibleWindowTime.end);
   const currentViewNs = endNs - startNs;
   if (ts < startNs || ts > endNs) {
-    // TODO(taylori): This is an ugly jump, we should do a smooth pan instead.
+    // TODO(hjd): This is an ugly jump, we should do a smooth pan instead.
     globals.frontendLocalState.updateVisibleTime(new TimeSpan(
         fromNs(ts - currentViewNs / 2), fromNs(ts + currentViewNs / 2)));
   }
diff --git a/ui/src/frontend/search_handler.ts b/ui/src/frontend/search_handler.ts
index 091f54f..cfdc259 100644
--- a/ui/src/frontend/search_handler.ts
+++ b/ui/src/frontend/search_handler.ts
@@ -61,7 +61,7 @@
   }
   selectCurrentSearchResult();
 
-  // TODO(taylori): If the user does a search before any other selection,
+  // TODO(hjd): If the user does a search before any other selection,
   // the details panel will pop up when the search is executed. If the search
   // result is behind where the details panel appears then it won't get scrolled
   // to. This time delay is a workaround for this specific situation.
diff --git a/ui/src/frontend/service_worker_controller.ts b/ui/src/frontend/service_worker_controller.ts
index bc11ee3..66abfc9 100644
--- a/ui/src/frontend/service_worker_controller.ts
+++ b/ui/src/frontend/service_worker_controller.ts
@@ -43,6 +43,9 @@
       }
     } else {
       await caches.delete(BYPASS_ID);
+      if (window.localStorage) {
+        window.localStorage.setItem('bypassDisabled', '1');
+      }
       this.install();
     }
     globals.rafScheduler.scheduleFullRedraw();
@@ -81,6 +84,16 @@
       return;
     }
 
+    // If this is localhost disable the service worker by default, unless the
+    // user manually re-enabled it (in which case bypassDisabled = '1').
+    const hostname = location.hostname;
+    const isLocalhost = ['127.0.0.1', '::1', 'localhost'].includes(hostname);
+    const bypassDisabled = window.localStorage &&
+        window.localStorage.getItem('bypassDisabled') === '1';
+    if (isLocalhost && !bypassDisabled) {
+      await this.setBypass(true);  // Will cause the check below to bail out.
+    }
+
     if (await caches.has(BYPASS_ID)) {
       this._bypassed = true;
       console.log('Skipping service worker registration, disabled by the user');
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 4633a73..ae44c70 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -16,6 +16,8 @@
 
 import {assertExists, assertTrue} from '../base/logging';
 import {Actions} from '../common/actions';
+import {TRACE_SUFFIX} from '../common/constants';
+import {ConversionJobStatus} from '../common/conversion_jobs';
 import {QueryResponse} from '../common/queries';
 import {EngineMode, TraceArrayBufferSource} from '../common/state';
 import * as version from '../gen/perfetto_version';
@@ -126,7 +128,27 @@
 const EXAMPLE_CHROME_TRACE_URL =
     'https://storage.googleapis.com/perfetto-misc/example_chrome_trace_4s_1.json';
 
-const SECTIONS = [
+interface SectionItem {
+  t: string;
+  a: string|((e: Event) => void);
+  i: string;
+  isPending?: () => boolean;
+  isVisible?: () => boolean;
+  internalUserOnly?: boolean;
+  checkDownloadDisabled?: boolean;
+}
+
+interface Section {
+  title: string;
+  summary: string;
+  items: SectionItem[];
+  expanded?: boolean;
+  hideIfNoTraceLoaded?: boolean;
+  appendOpenedTraceTitle?: boolean;
+}
+
+const SECTIONS: Section[] = [
+
   {
     title: 'Navigation',
     summary: 'Open or record a new trace',
@@ -141,6 +163,7 @@
       {t: 'Record new trace', a: navigateRecord, i: 'fiber_smart_record'},
     ],
   },
+
   {
     title: 'Current Trace',
     summary: 'Actions on the current trace',
@@ -154,6 +177,8 @@
         a: shareTrace,
         i: 'share',
         internalUserOnly: true,
+        isPending: () => globals.getConversionJobStatus('create_permalink') ===
+            ConversionJobStatus.InProgress,
       },
       {
         t: 'Download',
@@ -161,12 +186,47 @@
         i: 'file_download',
         checkDownloadDisabled: true,
       },
-      {t: 'Legacy UI', a: openCurrentTraceWithOldUI, i: 'filter_none'},
       {t: 'Query (SQL)', a: navigateAnalyze, i: 'control_camera'},
       {t: 'Metrics', a: navigateMetrics, i: 'speed'},
       {t: 'Info and stats', a: navigateInfo, i: 'info'},
     ],
   },
+
+  {
+    title: 'Convert trace',
+    summary: 'Convert to other formats',
+    expanded: true,
+    hideIfNoTraceLoaded: true,
+    items: [
+      {
+        t: 'Switch to legacy UI',
+        a: openCurrentTraceWithOldUI,
+        i: 'filter_none',
+        isPending: () => globals.getConversionJobStatus('open_in_legacy') ===
+            ConversionJobStatus.InProgress,
+      },
+      {
+        t: 'Convert to .json',
+        a: convertTraceToJson,
+        i: 'file_download',
+        isPending: () => globals.getConversionJobStatus('convert_json') ===
+            ConversionJobStatus.InProgress,
+        checkDownloadDisabled: true,
+      },
+
+      {
+        t: 'Convert to .systrace',
+        a: convertTraceToSystrace,
+        i: 'file_download',
+        isVisible: () => globals.hasFtrace,
+        isPending: () => globals.getConversionJobStatus('convert_systrace') ===
+            ConversionJobStatus.InProgress,
+        checkDownloadDisabled: true,
+      },
+
+    ],
+  },
+
   {
     title: 'Example Traces',
     expanded: true,
@@ -184,6 +244,7 @@
       },
     ],
   },
+
   {
     title: 'Sample queries',
     summary: 'Compute summary statistics',
@@ -225,6 +286,7 @@
       },
     ],
   },
+
   {
     title: 'Support',
     summary: 'Documentation & Bugs',
@@ -264,7 +326,8 @@
 }
 
 function getFileElement(): HTMLInputElement {
-  return document.querySelector('input[type=file]')! as HTMLInputElement;
+  return assertExists(
+      document.querySelector<HTMLInputElement>('input[type=file]'));
 }
 
 function popupFileSelectionDialog(e: Event) {
@@ -281,44 +344,85 @@
   getFileElement().click();
 }
 
-function openCurrentTraceWithOldUI(e: Event) {
-  e.preventDefault();
-  console.assert(isTraceLoaded());
-  globals.logging.logEvent('Trace Actions', 'Open current trace in legacy UI');
-  if (!isTraceLoaded) return;
-  const engine = Object.values(globals.state.engines)[0];
+function downloadTraceFromUrl(url: string): Promise<File> {
+  return m.request({
+    method: 'GET',
+    url,
+    // TODO(hjd): Once mithril is updated we can use responseType here rather
+    // than using config and remove the extract below.
+    config: xhr => {
+      xhr.responseType = 'blob';
+      xhr.onprogress = progress => {
+        const percent = (100 * progress.loaded / progress.total).toFixed(1);
+        globals.dispatch(Actions.updateStatus({
+          msg: `Downloading trace ${percent}%`,
+          timestamp: Date.now() / 1000,
+        }));
+      };
+    },
+    extract: xhr => {
+      return xhr.response;
+    }
+  });
+}
+
+async function getCurrentTrace(): Promise<Blob> {
+  // Caller must check engine exists.
+  const engine = assertExists(Object.values(globals.state.engines)[0]);
   const src = engine.source;
   if (src.type === 'ARRAY_BUFFER') {
-    openInOldUIWithSizeCheck(new Blob([src.buffer]));
+    return new Blob([src.buffer]);
   } else if (src.type === 'FILE') {
-    openInOldUIWithSizeCheck(src.file);
+    return src.file;
   } else if (src.type === 'URL') {
-    m.request({
-       method: 'GET',
-       url: src.url,
-       // TODO(hjd): Once mithril is updated we can use responseType here rather
-       // than using config and remove the extract below.
-       config: xhr => {
-         xhr.responseType = 'blob';
-         xhr.onprogress = progress => {
-           const percent = (100 * progress.loaded / progress.total).toFixed(1);
-           globals.dispatch(Actions.updateStatus({
-             msg: `Downloading trace ${percent}%`,
-             timestamp: Date.now() / 1000,
-           }));
-         };
-       },
-       extract: xhr => {
-         return xhr.response;
-       }
-     }).then(result => {
-      openInOldUIWithSizeCheck(result as Blob);
-    });
+    return downloadTraceFromUrl(src.url);
   } else {
     throw new Error(`Loading to catapult from source with type ${src.type}`);
   }
 }
 
+function openCurrentTraceWithOldUI(e: Event) {
+  e.preventDefault();
+  assertTrue(isTraceLoaded());
+  globals.logging.logEvent('Trace Actions', 'Open current trace in legacy UI');
+  if (!isTraceLoaded) return;
+  getCurrentTrace()
+      .then(file => {
+        openInOldUIWithSizeCheck(file);
+      })
+      .catch(error => {
+        throw new Error(`Failed to get current trace ${error}`);
+      });
+}
+
+function convertTraceToSystrace(e: Event) {
+  e.preventDefault();
+  assertTrue(isTraceLoaded());
+  globals.logging.logEvent('Trace Actions', 'Convert to .systrace');
+  if (!isTraceLoaded) return;
+  getCurrentTrace()
+      .then(file => {
+        globals.dispatch(Actions.convertTraceToSystraceAndDownload({file}));
+      })
+      .catch(error => {
+        throw new Error(`Failed to get current trace ${error}`);
+      });
+}
+
+function convertTraceToJson(e: Event) {
+  e.preventDefault();
+  assertTrue(isTraceLoaded());
+  globals.logging.logEvent('Trace Actions', 'Convert to .json');
+  if (!isTraceLoaded) return;
+  getCurrentTrace()
+      .then(file => {
+        globals.dispatch(Actions.convertTraceToJsonAndDownload({file}));
+      })
+      .catch(error => {
+        throw new Error(`Failed to get current trace ${error}`);
+      });
+}
+
 function isTraceLoaded(): boolean {
   const engine = Object.values(globals.state.engines)[0];
   return engine !== undefined;
@@ -358,6 +462,7 @@
 
   if (e.target.dataset['video'] === '1') {
     // TODO(hjd): Update this to use a controller and publish.
+    globals.logging.logEvent('Trace Actions', 'Open video');
     globals.dispatch(Actions.executeQuery({
       engineId: '0', queryId: 'command',
       query: `select ts from slices where name = 'first_frame' union ` +
@@ -503,7 +608,7 @@
   if (!isShareable() || !isTraceLoaded()) return;
 
   const result = confirm(
-      `Upload the trace and generate a permalink. ` +
+      `Upload UI state and generate a permalink. ` +
       `The trace will be accessible by anybody with the permalink.`);
   if (result) {
     globals.logging.logEvent('Trace Actions', 'Create permalink');
@@ -519,13 +624,16 @@
   const engine = Object.values(globals.state.engines)[0];
   if (!engine) return;
   let url = '';
-  let fileName = 'trace.pftrace';
+  let fileName = `trace${TRACE_SUFFIX}`;
   const src = engine.source;
   if (src.type === 'URL') {
     url = src.url;
     fileName = url.split('/').slice(-1)[0];
   } else if (src.type === 'ARRAY_BUFFER') {
     const blob = new Blob([src.buffer], {type: 'application/octet-stream'});
+    if (src.fileName) {
+      fileName = src.fileName;
+    }
     url = URL.createObjectURL(blob);
   } else if (src.type === 'FILE') {
     const file = src.file;
@@ -707,6 +815,9 @@
       if (section.hideIfNoTraceLoaded && !isTraceLoaded()) continue;
       const vdomItems = [];
       for (const item of section.items) {
+        if (item.isVisible !== undefined && !item.isVisible()) {
+          continue;
+        }
         let css = '';
         let attrs = {
           onclick: typeof item.a === 'function' ? item.a : null,
@@ -714,15 +825,14 @@
           target: typeof item.a === 'string' ? '_blank' : null,
           disabled: false,
         };
-        if (item.a === openCurrentTraceWithOldUI &&
-            globals.state.traceConversionInProgress) {
+        if (item.isPending && item.isPending()) {
           attrs.onclick = e => e.preventDefault();
           css = '.pending';
         }
-        if ((item as {internalUserOnly: boolean}).internalUserOnly === true) {
-          if (!globals.isInternalUser) continue;
+        if (item.internalUserOnly && !globals.isInternalUser) {
+          continue;
         }
-        if (!isDownloadable() && item.hasOwnProperty('checkDownloadDisabled')) {
+        if (item.checkDownloadDisabled && !isDownloadable()) {
           attrs = {
             onclick: e => {
               e.preventDefault();
@@ -836,7 +946,8 @@
                 },
                 'menu')),
             ),
-        m('input[type=file]', {onchange: onInputElementFileSelectionChanged}),
+        m('input.trace_file[type=file]',
+          {onchange: onInputElementFileSelectionChanged}),
         m('.sidebar-scroll',
           m(
               '.sidebar-scroll-container',
@@ -848,9 +959,12 @@
 }
 
 function createTraceLink(title: string, url: string) {
+  if (url === '') {
+    return m('a.trace-file-name', title);
+  }
   const linkProps = {
     href: url,
-    title: url !== '' ? 'Click to copy the URL' : '',
+    title: 'Click to copy the URL',
     target: '_blank',
     onclick: (e: Event) => {
       e.preventDefault();
diff --git a/ui/src/frontend/slice_panel.ts b/ui/src/frontend/slice_panel.ts
index 775c829..e83c9bf 100644
--- a/ui/src/frontend/slice_panel.ts
+++ b/ui/src/frontend/slice_panel.ts
@@ -68,7 +68,10 @@
               m('tr', m('th', `Prio`), m('td', `${sliceInfo.priority}`)),
               m('tr',
                 m('th', `End State`),
-                m('td', translateState(sliceInfo.endState)))
+                m('td', translateState(sliceInfo.endState))),
+              m('tr',
+                m('th', `Slice ID`),
+                m('td', sliceInfo.id ? sliceInfo.id.toString() : 'Unknown'))
             ]),
       );
     }
diff --git a/ui/src/frontend/thread_state_panel.ts b/ui/src/frontend/thread_state_panel.ts
index 048c4e1..164b3a5 100644
--- a/ui/src/frontend/thread_state_panel.ts
+++ b/ui/src/frontend/thread_state_panel.ts
@@ -75,10 +75,11 @@
 
     return [
       `${state} on CPU ${cpu}`,
-      m('i.material-icons.grey',
-        {
-          onclick: () => {
-              // TODO(taylori): Use trackId from TP.
+      m(
+          'i.material-icons.grey',
+          {
+            onclick: () => {
+              // TODO(hjd): Use trackId from TP.
               let trackId;
               for (const track of Object.values(globals.state.tracks)) {
                 if (track.kind === 'CpuSliceTrack' &&
@@ -92,10 +93,10 @@
                 scrollToTrackAndTs(
                     trackId, toNs(ts + globals.state.traceTime.startSec));
               }
+            },
+            title: 'Go to CPU slice'
           },
-          title: 'Go to CPU slice'
-        },
-        'call_made')
+          'call_made')
     ];
   }
 
diff --git a/ui/src/frontend/topbar.ts b/ui/src/frontend/topbar.ts
index 0af8781..1f9d351 100644
--- a/ui/src/frontend/topbar.ts
+++ b/ui/src/frontend/topbar.ts
@@ -191,12 +191,6 @@
 
 class NewVersionNotification implements m.ClassComponent {
   view() {
-    const engine: EngineConfig = globals.state.engines['0'];
-    // Don't show the new version toast if a trace is loading (engine exists).
-    if (!globals.frontendLocalState.newVersionAvailable ||
-        engine !== undefined) {
-      return;
-    }
     return m(
         '.new-version-toast',
         `Updated to ${version.VERSION} and ready for offline use!`,
diff --git a/ui/src/test/example_async_unittest.ts b/ui/src/test/example_async_unittest.ts
deleted file mode 100644
index c6868c1..0000000
--- a/ui/src/test/example_async_unittest.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (C) 2018 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.
-
-test('example async unittest', async () => {
-  const result = await Promise.resolve('foo');
-  expect(result).toEqual('foo');
-});
diff --git a/ui/src/test/example_dingus_unittest.ts b/ui/src/test/example_dingus_unittest.ts
deleted file mode 100644
index be42bcb..0000000
--- a/ui/src/test/example_dingus_unittest.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 2018 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 {dingus} from 'dingusjs';
-
-interface VoidFunctionWithNumberArg {
-  (_: number): void;
-}
-
-class ExampleClass {
-  isEven(n: number): boolean {
-    return n % 2 === 0;
-  }
-}
-
-function* evenNumbers(n: number, math: ExampleClass) {
-  for (let i = 0; i < n; i++) {
-    if (math.isEven(i)) yield i;
-  }
-}
-
-/**
- * Call |f| |n| times (once with each number [0, n)).
- */
-function iterMap(f: VoidFunctionWithNumberArg, n: number): void {
-  for (let i = 0; i < n; i++) {
-    f(i);
-  }
-}
-
-test('example dingus test', () => {
-  const d = dingus<VoidFunctionWithNumberArg>();
-  iterMap(d, 3);
-  expect(d.calls.length).toBe(3);
-  expect(d.calls.filter(([_a, args, _b]) => args[0] === 1).length).toBe(1);
-});
-
-test('example dingus test class', () => {
-  const d = dingus<ExampleClass>('math');
-  // Dingus returns a truthy dingus in all situations - so bear that in mind!
-  expect([...evenNumbers(3, d)]).toEqual([0, 1, 2]);
-});
diff --git a/ui/src/test/example_headlesstest.ts b/ui/src/test/example_headlesstest.ts
deleted file mode 100644
index 7d71c18..0000000
--- a/ui/src/test/example_headlesstest.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-import * as puppeteer from 'puppeteer';
-
-declare var global: {__BROWSER__: puppeteer.Browser;};
-
-test('example headless test', async () => {
-  const browser = global.__BROWSER__;
-  await browser.newPage();
-});
diff --git a/ui/src/test/example_jsdomtest.ts b/ui/src/test/example_jsdomtest.ts
deleted file mode 100644
index e8fd60e..0000000
--- a/ui/src/test/example_jsdomtest.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (C) 2018 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.
-
-test('example jsdom test', () => {
-  const element = document.createElement('div');
-  expect(element).not.toBeNull();
-});
diff --git a/ui/src/test/example_unittest.ts b/ui/src/test/example_unittest.ts
deleted file mode 100644
index 5855f40..0000000
--- a/ui/src/test/example_unittest.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (C) 2018 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.
-
-test('example unit test', () => {
-  expect(1 + 1).toBe(2);
-});
diff --git a/ui/src/tracks/actual_frames/controller.ts b/ui/src/tracks/actual_frames/controller.ts
index 94a1401..95c9201 100644
--- a/ui/src/tracks/actual_frames/controller.ts
+++ b/ui/src/tracks/actual_frames/controller.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {assertTrue} from '../../base/logging';
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -45,31 +45,33 @@
     const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
 
     if (this.maxDurNs === 0) {
-      const maxDurResult = await this.query(`
-        select max(dur)
+      const maxDurResult = await this.queryV2(`
+        select
+          max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
+            as maxDur
         from experimental_slice_layout
         where filter_track_ids = '${this.config.trackIds.join(',')}'
       `);
-      if (slowlyCountRows(maxDurResult) === 1) {
-        this.maxDurNs = maxDurResult.columns[0].longValues![0];
-      }
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
-    const rawResult = await this.query(`
+    const rawResult = await this.queryV2(`
       SELECT
         (s.ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
-        s.ts,
-        max(s.dur) as dur,
-        s.layout_depth,
-        s.name,
-        s.id,
-        s.dur = 0 as is_instant,
-        s.dur = -1 as is_incomplete,
+        s.ts as ts,
+        max(iif(s.dur = -1, (SELECT end_ts FROM trace_bounds) - s.ts, s.dur))
+            as dur,
+        s.layout_depth as layoutDepth,
+        s.name as name,
+        s.id as id,
+        s.dur = 0 as isInstant,
+        s.dur = -1 as isIncomplete,
         CASE afs.jank_tag
           WHEN 'Self Jank' THEN '${RED_COLOR}'
           WHEN 'Other Jank' THEN '${YELLOW_COLOR}'
           WHEN 'Dropped Frame' THEN '${BLUE_COLOR}'
           WHEN 'Buffer Stuffing' THEN '${LIGHT_GREEN_COLOR}'
+          WHEN 'SurfaceFlinger Stuffing' THEN '${LIGHT_GREEN_COLOR}'
           WHEN 'No Jank' THEN '${GREEN_COLOR}'
           ELSE '${PINK_COLOR}'
         END as color
@@ -83,7 +85,7 @@
       order by tsq, s.layout_depth
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = rawResult.numRows();
     const slices: Data = {
       start,
       end,
@@ -110,11 +112,21 @@
       return idx;
     }
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = rawResult.iter({
+      'tsq': NUM,
+      'ts': NUM,
+      'dur': NUM,
+      'layoutDepth': NUM,
+      'id': NUM,
+      'name': STR,
+      'isInstant': NUM,
+      'isIncomplete': NUM,
+      'color': STR,
+    });
+    for (let i = 0; it.valid(); i++, it.next()) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
@@ -122,14 +134,14 @@
 
       assertTrue(startNsQ !== endNsQ);
 
-      slices.starts[row] = fromNs(startNsQ);
-      slices.ends[row] = fromNs(endNsQ);
-      slices.depths[row] = +cols[3].longValues![row];
-      slices.titles[row] = internString(cols[4].stringValues![row]);
-      slices.colors![row] = internString(cols[8].stringValues![row]);
-      slices.sliceIds[row] = +cols[5].longValues![row];
-      slices.isInstant[row] = +cols[6].longValues![row];
-      slices.isIncomplete[row] = +cols[7].longValues![row];
+      slices.starts[i] = fromNs(startNsQ);
+      slices.ends[i] = fromNs(endNsQ);
+      slices.depths[i] = it.layoutDepth;
+      slices.titles[i] = internString(it.name);
+      slices.colors![i] = internString(it.color);
+      slices.sliceIds[i] = it.id;
+      slices.isInstant[i] = it.isInstant;
+      slices.isIncomplete[i] = it.isIncomplete;
     }
     return slices;
   }
diff --git a/ui/src/tracks/android_log/controller.ts b/ui/src/tracks/android_log/controller.ts
index afd235c..3f1401a 100644
--- a/ui/src/tracks/android_log/controller.ts
+++ b/ui/src/tracks/android_log/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {fromNs, toNsCeil, toNsFloor} from '../../common/time';
 import {LIMIT} from '../../common/track_data';
 import {
@@ -33,17 +33,17 @@
     // |resolution| is in s/px the frontend wants.
     const quantNs = toNsCeil(resolution);
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       select
-        cast(ts / ${quantNs} as integer) * ${quantNs} as ts_quant,
+        cast(ts / ${quantNs} as integer) * ${quantNs} as tsQuant,
         prio,
-        count(prio)
+        count(prio) as numEvents
       from android_logs
       where ts >= ${startNs} and ts <= ${endNs}
-      group by ts_quant, prio
-      order by ts_quant, prio limit ${LIMIT};`);
+      group by tsQuant, prio
+      order by tsQuant, prio limit ${LIMIT};`);
 
-    const rowCount = slowlyCountRows(rawResult);
+    const rowCount = queryRes.numRows();
     const result = {
       start,
       end,
@@ -53,12 +53,14 @@
       timestamps: new Float64Array(rowCount),
       priorities: new Uint8Array(rowCount),
     };
-    const cols = rawResult.columns;
-    for (let i = 0; i < rowCount; i++) {
-      result.timestamps[i] = fromNs(+cols[0].longValues![i]);
-      const prio = Math.min(+cols[1].longValues![i], 7);
-      result.priorities[i] |= (1 << prio);
-      result.numEvents += +cols[2].longValues![i];
+
+
+    const it = queryRes.iter({tsQuant: NUM, prio: NUM, numEvents: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      result.timestamps[row] = fromNs(it.tsQuant);
+      const prio = Math.min(it.prio, 7);
+      result.priorities[row] |= (1 << prio);
+      result.numEvents += it.numEvents;
     }
     return result;
   }
diff --git a/ui/src/tracks/async_slices/controller.ts b/ui/src/tracks/async_slices/controller.ts
index 96bd6e4..0cfc609 100644
--- a/ui/src/tracks/async_slices/controller.ts
+++ b/ui/src/tracks/async_slices/controller.ts
@@ -12,7 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {assertTrue} from '../../base/logging';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -37,36 +38,34 @@
     const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
 
     if (this.maxDurNs === 0) {
-      const maxDurResult = await this.query(`
-        select max(dur)
-        from experimental_slice_layout
+      const maxDurResult = await this.queryV2(`
+        select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
+        as maxDur from experimental_slice_layout
         where filter_track_ids = '${this.config.trackIds.join(',')}'
       `);
-      if (slowlyCountRows(maxDurResult) === 1) {
-        this.maxDurNs = maxDurResult.columns[0].longValues![0];
-      }
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       SELECT
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
-        max(dur) as dur,
-        layout_depth,
+        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as dur,
+        layout_depth as depth,
         name,
         id,
-        dur = 0 as is_instant,
-        dur = -1 as is_incomplete
+        dur = 0 as isInstant,
+        dur = -1 as isIncomplete
       from experimental_slice_layout
       where
         filter_track_ids = '${this.config.trackIds.join(',')}' and
         ts >= ${startNs - this.maxDurNs} and
         ts <= ${endNs}
-      group by tsq, layout_depth
-      order by tsq, layout_depth
+      group by tsq, depth
+      order by tsq, depth
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -92,31 +91,37 @@
       return idx;
     }
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      depth: NUM,
+      name: STR,
+      id: NUM,
+      isInstant: NUM,
+      isIncomplete: NUM
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.depths[row] = +cols[3].longValues![row];
-      slices.titles[row] = internString(cols[4].stringValues![row]);
-      slices.sliceIds[row] = +cols[5].longValues![row];
-      slices.isInstant[row] = +cols[6].longValues![row];
-      slices.isIncomplete[row] = +cols[7].longValues![row];
+      slices.depths[row] = it.depth;
+      slices.titles[row] = internString(it.name);
+      slices.sliceIds[row] = it.id;
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
     }
     return slices;
   }
 }
 
-
 trackControllerRegistry.register(AsyncSliceTrackController);
diff --git a/ui/src/tracks/chrome_slices/controller.ts b/ui/src/tracks/chrome_slices/controller.ts
index b1f2101..e702fd6 100644
--- a/ui/src/tracks/chrome_slices/controller.ts
+++ b/ui/src/tracks/chrome_slices/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -39,32 +39,31 @@
     const tableName = this.namespaceTable('slice');
 
     if (this.maxDurNs === 0) {
-      const query = `SELECT max(dur) FROM ${tableName} WHERE track_id = ${
-          this.config.trackId}`;
-      const rawResult = await this.query(query);
-      if (slowlyCountRows(rawResult) === 1) {
-        this.maxDurNs = rawResult.columns[0].longValues![0];
-      }
+      const query = `
+          SELECT max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
+          AS maxDur FROM ${tableName} WHERE track_id = ${this.config.trackId}`;
+      const queryRes = await this.queryV2(query);
+      this.maxDurNs = queryRes.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
     const query = `
       SELECT
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
-        max(dur),
+        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as dur,
         depth,
-        id as slice_id,
+        id as sliceId,
         name,
-        dur = 0 as is_instant,
-        dur = -1 as is_incomplete
+        dur = 0 as isInstant,
+        dur = -1 as isIncomplete
       FROM ${tableName}
       WHERE track_id = ${this.config.trackId} AND
         ts >= (${startNs - this.maxDurNs}) AND
         ts <= ${endNs}
       GROUP BY depth, tsq`;
-    const rawResult = await this.query(query);
+    const queryRes = await this.queryV2(query);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -90,19 +89,26 @@
       return idx;
     }
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      depth: NUM,
+      sliceId: NUM,
+      name: STR,
+      isInstant: NUM,
+      isIncomplete: NUM
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
-      const isInstant = +cols[6].longValues![row];
-      const isIncomplete = +cols[7].longValues![row];
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (!isInstant && startNsQ === endNsQ) {
+      if (!it.isInstant && startNsQ === endNsQ) {
         throw new Error(
             'Expected startNsQ and endNsQ to differ (' +
             `startNsQ: ${startNsQ}, startNs: ${startNs},` +
@@ -112,11 +118,11 @@
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.depths[row] = +cols[3].longValues![row];
-      slices.sliceIds[row] = +cols[4].longValues![row];
-      slices.titles[row] = internString(cols[5].stringValues![row]);
-      slices.isInstant[row] = isInstant;
-      slices.isIncomplete[row] = isIncomplete;
+      slices.depths[row] = it.depth;
+      slices.sliceIds[row] = it.sliceId;
+      slices.titles[row] = internString(it.name);
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
     }
     return slices;
   }
diff --git a/ui/src/tracks/chrome_slices/frontend.ts b/ui/src/tracks/chrome_slices/frontend.ts
index 1fc3358..403f01b 100644
--- a/ui/src/tracks/chrome_slices/frontend.ts
+++ b/ui/src/tracks/chrome_slices/frontend.ts
@@ -12,9 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {hsluvToHex} from 'hsluv';
+
 import {Actions} from '../../common/actions';
 import {cropText, drawIncompleteSlice} from '../../common/canvas_utils';
-import {hueForSlice} from '../../common/colorizer';
+import {hslForSlice} from '../../common/colorizer';
+import {TRACE_MARGIN_TIME_S} from '../../common/constants';
 import {TrackState} from '../../common/state';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
@@ -25,7 +28,6 @@
 
 const SLICE_HEIGHT = 18;
 const TRACK_PADDING = 4;
-const INCOMPLETE_SLICE_TIME_S = 0.00003;
 const CHEVRON_WIDTH_PX = 10;
 const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
 const INNER_CHEVRON_OFFSET = -3;
@@ -84,7 +86,7 @@
       const title = data.strings[titleId];
       const colorOverride = data.colors && data.strings[data.colors[i]];
       if (isIncomplete) {  // incomplete slice
-        tEnd = tStart + INCOMPLETE_SLICE_TIME_S;
+        tEnd = visibleWindowTime.end;
       }
 
       const rect = this.getSliceRect(tStart, tEnd, depth);
@@ -98,14 +100,15 @@
           currentSelection.id !== undefined && currentSelection.id === sliceId;
 
       const name = title.replace(/( )?\d+/g, '');
-      const hue = hueForSlice(name);
-      const saturation = isSelected ? 80 : 50;
       const highlighted = titleId === this.hoveredTitleId ||
           globals.frontendLocalState.highlightedSliceId === sliceId;
 
+      const [hue, saturation, lightness] =
+          hslForSlice(name, highlighted || isSelected);
+
       let color: string;
       if (colorOverride === undefined) {
-        color = `hsl(${hue}, ${saturation}%, ${highlighted ? 30 : 65}%)`;
+        color = hsluvToHex([hue, saturation, lightness]);
       } else {
         color = colorOverride;
       }
@@ -128,7 +131,8 @@
             ctx.save();
             ctx.translate(0, INNER_CHEVRON_OFFSET);
             ctx.scale(INNER_CHEVRON_SCALE, INNER_CHEVRON_SCALE);
-            ctx.fillStyle = `hsl(${hue}, ${saturation}%, 30%)`;
+            ctx.fillStyle = hsluvToHex([hue, 100, 10]);
+
             this.drawChevron(ctx);
             ctx.restore();
 
@@ -155,7 +159,7 @@
       // Selected case
       if (isSelected) {
         drawRectOnSelected = () => {
-          ctx.strokeStyle = `hsl(${hue}, ${saturation}%, 30%)`;
+          ctx.strokeStyle = hsluvToHex([hue, 100, 10]);
           ctx.beginPath();
           ctx.lineWidth = 3;
           ctx.strokeRect(
@@ -164,7 +168,7 @@
         };
       }
 
-      ctx.fillStyle = 'white';
+      ctx.fillStyle = lightness > 65 ? '#404040' : 'white';
       const displayText = cropText(title, charWidth, rect.width);
       const rectXCenter = rect.left + rect.width / 2;
       ctx.textBaseline = "middle";
@@ -205,7 +209,7 @@
       } else {
         let tEnd = data.ends[i];
         if (data.isIncomplete[i]) {
-          tEnd = tStart + INCOMPLETE_SLICE_TIME_S;
+          tEnd = globals.frontendLocalState.visibleWindowTime.end;
         }
         if (tStart <= t && t <= tEnd) {
           return i;
@@ -255,7 +259,7 @@
       |undefined {
     const {timeScale, visibleWindowTime} = globals.frontendLocalState;
     const pxEnd = timeScale.timeToPx(visibleWindowTime.end);
-    const left = Math.max(timeScale.timeToPx(tStart), 0);
+    const left = Math.max(timeScale.timeToPx(tStart), -TRACE_MARGIN_TIME_S);
     const right = Math.min(timeScale.timeToPx(tEnd), pxEnd);
     return {
       left,
diff --git a/ui/src/tracks/counter/controller.ts b/ui/src/tracks/counter/controller.ts
index 22dffe2..5b230f3 100644
--- a/ui/src/tracks/counter/controller.ts
+++ b/ui/src/tracks/counter/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {iter, NUM, slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -47,7 +47,7 @@
 
     if (!this.setup) {
       if (this.config.namespace === undefined) {
-        await this.query(`
+        await this.queryV2(`
           create view ${this.tableName('counter_view')} as
           select
             id,
@@ -59,7 +59,7 @@
           where track_id = ${this.config.trackId};
         `);
       } else {
-        await this.query(`
+        await this.queryV2(`
           create view ${this.tableName('counter_view')} as
           select
             id,
@@ -72,33 +72,33 @@
         `);
       }
 
-      const maxDurResult = await this.query(`
+      const maxDurResult = await this.queryV2(`
           select
             max(
               iif(dur != -1, dur, (select end_ts from trace_bounds) - ts)
-            )
+            ) as maxDur
           from ${this.tableName('counter_view')}
       `);
-      if (slowlyCountRows(maxDurResult) === 1) {
-        this.maxDurNs = maxDurResult.columns[0].longValues![0];
-      }
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
 
-      const result = await this.query(`
+      const queryRes = await this.queryV2(`
         select
-          max(value) as maxValue,
-          min(value) as minValue,
-          max(delta) as maxDelta,
-          min(delta) as minDelta
+          ifnull(max(value), 0) as maxValue,
+          ifnull(min(value), 0) as minValue,
+          ifnull(max(delta), 0) as maxDelta,
+          ifnull(min(delta), 0) as minDelta
         from ${this.tableName('counter_view')}`);
-      this.maximumValueSeen = +result.columns[0].doubleValues![0];
-      this.minimumValueSeen = +result.columns[1].doubleValues![0];
-      this.maximumDeltaSeen = +result.columns[2].doubleValues![0];
-      this.minimumDeltaSeen = +result.columns[3].doubleValues![0];
+      const row = queryRes.firstRow(
+          {maxValue: NUM, minValue: NUM, maxDelta: NUM, minDelta: NUM});
+      this.maximumValueSeen = row.maxValue;
+      this.minimumValueSeen = row.minValue;
+      this.maximumDeltaSeen = row.maxDelta;
+      this.minimumDeltaSeen = row.minDelta;
 
       this.setup = true;
     }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         min(value) as minValue,
@@ -112,7 +112,7 @@
       order by tsq
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
 
     const data: Data = {
       start,
@@ -131,25 +131,22 @@
       totalDeltas: new Float64Array(numRows),
     };
 
-    const it = iter(
-        {
-          'tsq': NUM,
-          'lastId': NUM,
-          'minValue': NUM,
-          'maxValue': NUM,
-          'lastValue': NUM,
-          'totalDelta': NUM,
-        },
-        rawResult);
-    for (let i = 0; it.valid(); ++i, it.next()) {
-      data.timestamps[i] = fromNs(it.row.tsq);
-      data.lastIds[i] = it.row.lastId;
-      data.minValues[i] = it.row.minValue;
-      data.maxValues[i] = it.row.maxValue;
-      data.lastValues[i] = it.row.lastValue;
-      data.totalDeltas[i] = it.row.totalDelta;
+    const it = queryRes.iter({
+      'tsq': NUM,
+      'lastId': NUM,
+      'minValue': NUM,
+      'maxValue': NUM,
+      'lastValue': NUM,
+      'totalDelta': NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      data.timestamps[row] = fromNs(it.tsq);
+      data.lastIds[row] = it.lastId;
+      data.minValues[row] = it.minValue;
+      data.maxValues[row] = it.maxValue;
+      data.lastValues[row] = it.lastValue;
+      data.totalDeltas[row] = it.totalDelta;
     }
-
     return data;
   }
 
diff --git a/ui/src/tracks/cpu_freq/controller.ts b/ui/src/tracks/cpu_freq/controller.ts
index 08ca0b5..c457da7 100644
--- a/ui/src/tracks/cpu_freq/controller.ts
+++ b/ui/src/tracks/cpu_freq/controller.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import {assertTrue} from '../../base/logging';
-import {RawQueryResult} from '../../common/protos';
-import {iter, NUM, slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL} from '../../common/query_iterator';
+import {QueryResult} from '../../common/query_result';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -41,30 +41,37 @@
     this.maximumValueSeen = await this.queryMaxFrequency();
     this.maxDurNs = await this.queryMaxSourceDur();
 
-    const result = await this.query(`
-      select max(ts), dur, count(1)
+    const iter = (await this.queryV2(`
+      select max(ts) as maxTs, dur, count(1) as rowCount
       from ${this.tableName('freq_idle')}
-    `);
-    this.maxTsEndNs =
-        result.columns[0].longValues![0] + result.columns[1].longValues![0];
+    `)).firstRow({maxTs: NUM_NULL, dur: NUM_NULL, rowCount: NUM});
+    if (iter.maxTs === null || iter.dur === null) {
+      // We shoulnd't really hit this because trackDecider shouldn't create
+      // the track in the first place if there are no entries. But could happen
+      // if only one cpu has no cpufreq data.
+      return;
+    }
+    this.maxTsEndNs = iter.maxTs + iter.dur;
 
-    const rowCount = result.columns[2].longValues![0];
+    const rowCount = iter.rowCount;
     const bucketNs = this.cachedBucketSizeNs(rowCount);
     if (bucketNs === undefined) {
       return;
     }
-    await this.query(`
+
+    await this.queryV2(`
       create table ${this.tableName('freq_idle_cached')} as
       select
-        (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
-        min(freq_value) as min_freq,
-        max(freq_value) as max_freq,
-        value_at_max_ts(ts, freq_value) as last_freq,
-        value_at_max_ts(ts, idle_value) as last_idle_value
+        (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cachedTsq,
+        min(freqValue) as minFreq,
+        max(freqValue) as maxFreq,
+        value_at_max_ts(ts, freqValue) as lastFreq,
+        value_at_max_ts(ts, idleValue) as lastIdleValue
       from ${this.tableName('freq_idle')}
-      group by cached_tsq
-      order by cached_tsq
+      group by cachedTsq
+      order by cachedTsq
     `);
+
     this.cachedBucketNs = bucketNs;
   }
 
@@ -82,10 +89,10 @@
     // be an even number, so we can snap in the middle.
     const bucketNs =
         Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
-
     const freqResult = await this.queryData(startNs, endNs, bucketNs);
+    assertTrue(freqResult.isComplete());
 
-    const numRows = slowlyCountRows(freqResult);
+    const numRows = freqResult.numRows();
     const data: Data = {
       start,
       end,
@@ -100,68 +107,68 @@
       lastIdleValues: new Int8Array(numRows),
     };
 
-    const it = iter(
-        {
-          'tsq': NUM,
-          'minFreq': NUM,
-          'maxFreq': NUM,
-          'lastFreq': NUM,
-          'lastIdleValue': NUM,
-        },
-        freqResult);
+    const it = freqResult.iter({
+      'tsq': NUM,
+      'minFreq': NUM,
+      'maxFreq': NUM,
+      'lastFreq': NUM,
+      'lastIdleValue': NUM,
+    });
     for (let i = 0; it.valid(); ++i, it.next()) {
-      data.timestamps[i] = fromNs(it.row.tsq);
-      data.minFreqKHz[i] = it.row.minFreq;
-      data.maxFreqKHz[i] = it.row.maxFreq;
-      data.lastFreqKHz[i] = it.row.lastFreq;
-      data.lastIdleValues[i] = it.row.lastIdleValue;
+      data.timestamps[i] = fromNs(it.tsq);
+      data.minFreqKHz[i] = it.minFreq;
+      data.maxFreqKHz[i] = it.maxFreq;
+      data.lastFreqKHz[i] = it.lastFreq;
+      data.lastIdleValues[i] = it.lastIdleValue;
     }
+
     return data;
   }
 
   private async queryData(startNs: number, endNs: number, bucketNs: number):
-      Promise<RawQueryResult> {
+      Promise<QueryResult> {
     const isCached = this.cachedBucketNs <= bucketNs;
 
     if (isCached) {
-      return this.query(`
+      return this.queryV2(`
         select
-          cached_tsq / ${bucketNs} * ${bucketNs} as tsq,
-          min(min_freq) as minFreq,
-          max(max_freq) as maxFreq,
-          value_at_max_ts(cached_tsq, last_freq) as lastFreq,
-          value_at_max_ts(cached_tsq, last_idle_value) as lastIdleValue
+          cachedTsq / ${bucketNs} * ${bucketNs} as tsq,
+          min(minFreq) as minFreq,
+          max(maxFreq) as maxFreq,
+          value_at_max_ts(cachedTsq, lastFreq) as lastFreq,
+          value_at_max_ts(cachedTsq, lastIdleValue) as lastIdleValue
         from ${this.tableName('freq_idle_cached')}
         where
-          cached_tsq >= ${startNs - this.maxDurNs} and
-          cached_tsq <= ${endNs}
+          cachedTsq >= ${startNs - this.maxDurNs} and
+          cachedTsq <= ${endNs}
         group by tsq
         order by tsq
       `);
     }
-
-    const minTsFreq = await this.query(`
-      select ifnull(max(ts), 0) from ${this.tableName('freq')}
+    const minTsFreq = await this.queryV2(`
+      select ifnull(max(ts), 0) as minTs from ${this.tableName('freq')}
       where ts < ${startNs}
     `);
-    let minTs = minTsFreq.columns[0].longValues![0];
+
+    let minTs = minTsFreq.iter({minTs: NUM}).minTs;
     if (this.config.idleTrackId !== undefined) {
-      const minTsIdle = await this.query(`
-        select ifnull(max(ts), 0) from ${this.tableName('idle')}
+      const minTsIdle = await this.queryV2(`
+        select ifnull(max(ts), 0) as minTs from ${this.tableName('idle')}
         where ts < ${startNs}
       `);
-      minTs = Math.min(minTsIdle.columns[0].longValues![0], minTs);
+      minTs = Math.min(minTsIdle.iter({minTs: NUM}).minTs, minTs);
     }
+
     const geqConstraint = this.config.idleTrackId === undefined ?
         `ts >= ${minTs}` :
         `source_geq(ts, ${minTs})`;
-    return this.query(`
+    return this.queryV2(`
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
-        min(freq_value) as minFreq,
-        max(freq_value) as maxFreq,
-        value_at_max_ts(ts, freq_value) as lastFreq,
-        value_at_max_ts(ts, idle_value) as lastIdleValue
+        min(freqValue) as minFreq,
+        max(freqValue) as maxFreq,
+        value_at_max_ts(ts, freqValue) as lastFreq,
+        value_at_max_ts(ts, idleValue) as lastIdleValue
       from ${this.tableName('freq_idle')}
       where
         ${geqConstraint} and
@@ -172,59 +179,59 @@
   }
 
   private async queryMaxFrequency(): Promise<number> {
-    const result = await this.query(`
-      select max(freq_value)
+    const result = await this.queryV2(`
+      select max(freqValue) as maxFreq
       from ${this.tableName('freq')}
     `);
-    return result.columns[0].doubleValues![0];
+    return result.firstRow({'maxFreq': NUM_NULL}).maxFreq || 0;
   }
 
   private async queryMaxSourceDur(): Promise<number> {
-    const maxDurFreqResult =
-        await this.query(`select max(dur) from ${this.tableName('freq')}`);
-    const maxFreqDurNs = maxDurFreqResult.columns[0].longValues![0];
+    const maxDurFreqResult = await this.queryV2(
+        `select ifnull(max(dur), 0) as maxDur from ${this.tableName('freq')}`);
+    const maxDurNs = maxDurFreqResult.firstRow({'maxDur': NUM}).maxDur;
     if (this.config.idleTrackId === undefined) {
-      return maxFreqDurNs;
+      return maxDurNs;
     }
 
-    const maxDurIdleResult =
-        await this.query(`select max(dur) from ${this.tableName('idle')}`);
-    return Math.max(maxFreqDurNs, maxDurIdleResult.columns[0].longValues![0]);
+    const maxDurIdleResult = await this.queryV2(
+        `select ifnull(max(dur), 0) as maxDur from ${this.tableName('idle')}`);
+    return Math.max(maxDurNs, maxDurIdleResult.firstRow({maxDur: NUM}).maxDur);
   }
 
   private async createFreqIdleViews() {
-    await this.query(`create view ${this.tableName('freq')} as
+    await this.queryV2(`create view ${this.tableName('freq')} as
       select
         ts,
         dur,
-        value as freq_value
+        value as freqValue
       from experimental_counter_dur c
       where track_id = ${this.config.freqTrackId};
     `);
 
     if (this.config.idleTrackId === undefined) {
-      await this.query(`create view ${this.tableName('freq_idle')} as
+      await this.queryV2(`create view ${this.tableName('freq_idle')} as
         select
           ts,
           dur,
-          -1 as idle_value,
-          freq_value
+          -1 as idleValue,
+          freqValue
         from ${this.tableName('freq')};
       `);
       return;
     }
 
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('idle')} as
       select
         ts,
         dur,
-        iif(value = 4294967295, -1, cast(value as int)) as idle_value
+        iif(value = 4294967295, -1, cast(value as int)) as idleValue
       from experimental_counter_dur c
       where track_id = ${this.config.idleTrackId};
     `);
 
-    await this.query(`
+    await this.queryV2(`
       create virtual table ${this.tableName('freq_idle')}
       using span_join(${this.tableName('freq')}, ${this.tableName('idle')});
     `);
diff --git a/ui/src/tracks/cpu_profile/controller.ts b/ui/src/tracks/cpu_profile/controller.ts
index 070cedb..a6c5456 100644
--- a/ui/src/tracks/cpu_profile/controller.ts
+++ b/ui/src/tracks/cpu_profile/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {
   TrackController,
   trackControllerRegistry
@@ -28,13 +28,16 @@
   static readonly kind = CPU_PROFILE_TRACK_KIND;
   async onBoundsChange(start: number, end: number, resolution: number):
       Promise<Data> {
-    const query = `select id, ts, callsite_id from cpu_profile_stack_sample
-        where utid = ${this.config.utid}
-        order by ts`;
+    const query = `select
+        id,
+        ts,
+        callsite_id as callsiteId
+      from cpu_profile_stack_sample
+      where utid = ${this.config.utid}
+      order by ts`;
 
-    const result = await this.query(query);
-
-    const numRows = slowlyCountRows(result);
+    const result = await this.queryV2(query);
+    const numRows = result.numRows();
     const data: Data = {
       start,
       end,
@@ -45,10 +48,11 @@
       callsiteId: new Uint32Array(numRows),
     };
 
-    for (let row = 0; row < numRows; row++) {
-      data.ids[row] = +result.columns[0].longValues![row];
-      data.tsStarts[row] = +result.columns[1].longValues![row];
-      data.callsiteId[row] = +result.columns[2].longValues![row];
+    const it = result.iter({id: NUM, ts: NUM, callsiteId: NUM});
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      data.ids[row] = it.id;
+      data.tsStarts[row] = it.ts;
+      data.callsiteId[row] = it.callsiteId;
     }
 
     return data;
diff --git a/ui/src/tracks/cpu_profile/frontend.ts b/ui/src/tracks/cpu_profile/frontend.ts
index 0fe5c57..f0cc6af 100644
--- a/ui/src/tracks/cpu_profile/frontend.ts
+++ b/ui/src/tracks/cpu_profile/frontend.ts
@@ -12,9 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {hsluvToHex} from 'hsluv';
+
 import {searchSegment} from '../../base/binary_search';
 import {Actions} from '../../common/actions';
-import {hueForSlice} from '../../common/colorizer';
+import {hslForSlice} from '../../common/colorizer';
 import {TrackState} from '../../common/state';
 import {fromNs, toNs} from '../../common/time';
 import {globals} from '../../frontend/globals';
@@ -28,9 +30,7 @@
 const RECT_HEIGHT = 30.5;
 
 function colorForSample(callsiteId: number, isHovered: boolean): string {
-  const hue = hueForSlice(String(callsiteId));
-  const lightness = isHovered ? '55%' : '70%';
-  return `hsl(${hue}, 45%, ${lightness})`;
+  return hsluvToHex(hslForSlice(String(callsiteId), isHovered));
 }
 
 class CpuProfileTrack extends Track<Config, Data> {
diff --git a/ui/src/tracks/cpu_slices/controller.ts b/ui/src/tracks/cpu_slices/controller.ts
index 1389cb4..2d891ef 100644
--- a/ui/src/tracks/cpu_slices/controller.ts
+++ b/ui/src/tracks/cpu_slices/controller.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {assertTrue} from '../../base/logging';
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -29,7 +29,7 @@
   private maxDurNs = 0;
 
   async onSetup() {
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('sched')} as
       select
         ts,
@@ -40,18 +40,18 @@
       where cpu = ${this.config.cpu} and utid != 0
     `);
 
-    const rawResult = await this.query(`
-      select max(dur), count(1)
+    const queryRes = await this.queryV2(`
+      select ifnull(max(dur), 0) as maxDur, count(1) as rowCount
       from ${this.tableName('sched')}
     `);
-    this.maxDurNs = rawResult.columns[0].longValues![0];
-
-    const rowCount = rawResult.columns[1].longValues![0];
+    const row = queryRes.firstRow({maxDur: NUM, rowCount: NUM});
+    this.maxDurNs = row.maxDur;
+    const rowCount = row.rowCount;
     const bucketNs = this.cachedBucketSizeNs(rowCount);
     if (bucketNs === undefined) {
       return;
     }
-    await this.query(`
+    await this.queryV2(`
       create table ${this.tableName('sched_cached')} as
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
@@ -88,9 +88,9 @@
         `(ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs}`;
     const queryTable =
         isCached ? this.tableName('sched_cached') : this.tableName('sched');
-    const constainColumn = isCached ? 'cached_tsq' : 'ts';
+    const constraintColumn = isCached ? 'cached_tsq' : 'ts';
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       select
         ${queryTsq} as tsq,
         ts,
@@ -99,13 +99,13 @@
         id
       from ${queryTable}
       where
-        ${constainColumn} >= ${startNs - this.maxDurNs} and
-        ${constainColumn} <= ${endNs}
+        ${constraintColumn} >= ${startNs - this.maxDurNs} and
+        ${constraintColumn} <= ${endNs}
       group by tsq
       order by tsq
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -117,31 +117,30 @@
       utids: new Uint32Array(numRows),
     };
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({tsq: NUM, ts: NUM, dur: NUM, utid: NUM, id: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.utids[row] = +cols[3].longValues![row];
-      slices.ids[row] = +cols[4].longValues![row];
+      slices.utids[row] = it.utid;
+      slices.ids[row] = it.id;
     }
 
     return slices;
   }
 
   async onDestroy() {
-    await this.query(`drop table if exists ${this.tableName('sched_cached')}`);
+    await this.queryV2(
+        `drop table if exists ${this.tableName('sched_cached')}`);
   }
 }
 
diff --git a/ui/src/tracks/debug_slices/controller.ts b/ui/src/tracks/debug_slices/controller.ts
index 6471de0..8400af8 100644
--- a/ui/src/tracks/debug_slices/controller.ts
+++ b/ui/src/tracks/debug_slices/controller.ts
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertTrue} from '../../base/logging';
 import {Actions} from '../../common/actions';
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {globals} from '../../controller/globals';
 import {
@@ -28,27 +27,26 @@
   static readonly kind = DEBUG_SLICE_TRACK_KIND;
 
   async onReload() {
-    const rawResult = await this.query(`select max(depth) from debug_slices`);
-    const maxDepth = (slowlyCountRows(rawResult) === 0) ?
-        1 :
-        rawResult.columns[0].longValues![0];
+    const rawResult = await this.queryV2(
+        `select ifnull(max(depth), 1) as maxDepth from debug_slices`);
+    const maxDepth = rawResult.firstRow({maxDepth: NUM}).maxDepth;
     globals.dispatch(
         Actions.updateTrackConfig({id: this.trackId, config: {maxDepth}}));
   }
 
   async onBoundsChange(start: number, end: number, resolution: number):
       Promise<Data> {
-    const rawResult = await this.query(
-        `select id, name, ts, dur, depth from debug_slices where
-        (ts + dur) >= ${toNs(start)} and ts <= ${toNs(end)}`);
+    const queryRes = await this.queryV2(`select
+      ifnull(id, -1) as id,
+      ifnull(name, '[null]') as name,
+      ts,
+      iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur) as dur,
+      ifnull(depth, 0) as depth
+      from debug_slices
+      where (ts + dur) >= ${toNs(start)} and ts <= ${toNs(end)}`);
 
-    assertTrue(rawResult.columns.length === 5);
-    const [idCol, nameCol, tsCol, durCol, depthCol] = rawResult.columns;
-    const idValues = idCol.longValues! || idCol.doubleValues!;
-    const tsValues = tsCol.longValues! || tsCol.doubleValues!;
-    const durValues = durCol.longValues! || durCol.doubleValues!;
+    const numRows = queryRes.numRows();
 
-    const numRows = slowlyCountRows(rawResult);
     const slices: Data = {
       start,
       end,
@@ -74,24 +72,24 @@
       return idx;
     }
 
-    for (let i = 0; i < slowlyCountRows(rawResult); i++) {
+    const it = queryRes.iter(
+        {id: NUM, name: STR, ts: NUM_NULL, dur: NUM_NULL, depth: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
       let sliceStart: number, sliceEnd: number;
-      if (tsCol.isNulls![i] || durCol.isNulls![i]) {
+      if (it.ts === null || it.dur === null) {
         sliceStart = sliceEnd = -1;
       } else {
-        sliceStart = tsValues[i];
-        const sliceDur = durValues[i];
-        sliceEnd = sliceStart + sliceDur;
+        sliceStart = it.ts;
+        sliceEnd = sliceStart + it.dur;
       }
-      slices.sliceIds[i] = idCol.isNulls![i] ? -1 : idValues[i];
-      slices.starts[i] = fromNs(sliceStart);
-      slices.ends[i] = fromNs(sliceEnd);
-      slices.depths[i] = depthCol.isNulls![i] ? 0 : depthCol.longValues![i];
-      const sliceName =
-          nameCol.isNulls![i] ? '[null]' : nameCol.stringValues![i];
-      slices.titles[i] = internString(sliceName);
-      slices.isInstant[i] = 0;
-      slices.isIncomplete[i] = 0;
+      slices.sliceIds[row] = it.id;
+      slices.starts[row] = fromNs(sliceStart);
+      slices.ends[row] = fromNs(sliceEnd);
+      slices.depths[row] = it.depth;
+      const sliceName = it.name;
+      slices.titles[row] = internString(sliceName);
+      slices.isInstant[row] = 0;
+      slices.isIncomplete[row] = 0;
     }
 
     return slices;
diff --git a/ui/src/tracks/expected_frames/controller.ts b/ui/src/tracks/expected_frames/controller.ts
index 97ab2d9..5227a81 100644
--- a/ui/src/tracks/expected_frames/controller.ts
+++ b/ui/src/tracks/expected_frames/controller.ts
@@ -12,7 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {assertTrue} from '../../base/logging';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -37,26 +38,25 @@
     const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
 
     if (this.maxDurNs === 0) {
-      const maxDurResult = await this.query(`
-        select max(dur)
+      const maxDurResult = await this.queryV2(`
+        select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
+          as maxDur
         from experimental_slice_layout
         where filter_track_ids = '${this.config.trackIds.join(',')}'
       `);
-      if (slowlyCountRows(maxDurResult) === 1) {
-        this.maxDurNs = maxDurResult.columns[0].longValues![0];
-      }
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       SELECT
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
-        max(dur) as dur,
-        layout_depth,
+        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as dur,
+        layout_depth as layoutDepth,
         name,
         id,
-        dur = 0 as is_instant,
-        dur = -1 as is_incomplete
+        dur = 0 as isInstant,
+        dur = -1 as isIncomplete
       from experimental_slice_layout
       where
         filter_track_ids = '${this.config.trackIds.join(',')}' and
@@ -66,7 +66,7 @@
       order by tsq, layout_depth
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -94,27 +94,34 @@
     }
     const greenIndex = internString('#4CAF50');
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      layoutDepth: NUM,
+      id: NUM,
+      name: STR,
+      isInstant: NUM,
+      isIncomplete: NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.depths[row] = +cols[3].longValues![row];
-      slices.titles[row] = internString(cols[4].stringValues![row]);
-      slices.sliceIds[row] = +cols[5].longValues![row];
-      slices.isInstant[row] = +cols[6].longValues![row];
-      slices.isIncomplete[row] = +cols[7].longValues![row];
+      slices.depths[row] = it.layoutDepth;
+      slices.titles[row] = internString(it.name);
+      slices.sliceIds[row] = it.id;
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
       slices.colors![row] = greenIndex;
     }
     return slices;
diff --git a/ui/src/tracks/heap_profile/controller.ts b/ui/src/tracks/heap_profile/controller.ts
index 2422af1..9873744 100644
--- a/ui/src/tracks/heap_profile/controller.ts
+++ b/ui/src/tracks/heap_profile/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM, STR} from '../../common/query_iterator';
 import {
   TrackController,
   trackControllerRegistry
@@ -38,7 +38,7 @@
         types: new Array<string>()
       };
     }
-    const result = await this.query(`
+    const queryRes = await this.queryV2(`
     select * from
     (select distinct(ts) as ts, 'native' as type from heap_profile_allocation
      where upid = ${this.config.upid}
@@ -46,7 +46,7 @@
         select distinct(graph_sample_ts) as ts, 'graph' as type from
         heap_graph_object
         where upid = ${this.config.upid}) order by ts`);
-    const numRows = slowlyCountRows(result);
+    const numRows = queryRes.numRows();
     const data: Data = {
       start,
       end,
@@ -56,11 +56,11 @@
       types: new Array<string>(numRows),
     };
 
-    for (let row = 0; row < numRows; row++) {
-      data.tsStarts[row] = +result.columns[0].longValues![row];
-      data.types[row] = result.columns[1].stringValues![row];
+    const it = queryRes.iter({ts: NUM, type: STR});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      data.tsStarts[row] = it.ts;
+      data.types[row] = it.type;
     }
-
     return data;
   }
 }
diff --git a/ui/src/tracks/process_scheduling/controller.ts b/ui/src/tracks/process_scheduling/controller.ts
index c7676ed..9441448 100644
--- a/ui/src/tracks/process_scheduling/controller.ts
+++ b/ui/src/tracks/process_scheduling/controller.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import {assertTrue} from '../../base/logging';
-import {RawQueryResult} from '../../common/protos';
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
+import {QueryResult} from '../../common/query_result';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -45,18 +45,19 @@
     assertTrue(cpus.length > 0);
     this.maxCpu = Math.max(...cpus) + 1;
 
-    const result = await this.query(`
-      select max(dur), count(1)
+    const result = (await this.queryV2(`
+      select ifnull(max(dur), 0) as maxDur, count(1) as count
       from ${this.tableName('process_sched')}
-    `);
-    this.maxDurNs = result.columns[0].longValues![0];
+    `)).iter({maxDur: NUM, count: NUM});
+    assertTrue(result.valid());
+    this.maxDurNs = result.maxDur;
 
-    const rowCount = result.columns[1].longValues![0];
+    const rowCount = result.count;
     const bucketNs = this.cachedBucketSizeNs(rowCount);
     if (bucketNs === undefined) {
       return;
     }
-    await this.query(`
+    await this.queryV2(`
       create table ${this.tableName('process_sched_cached')} as
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
@@ -88,9 +89,8 @@
     const bucketNs =
         Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
 
-    const rawResult = await this.queryData(startNs, endNs, bucketNs);
-
-    const numRows = slowlyCountRows(rawResult);
+    const queryRes = await this.queryData(startNs, endNs, bucketNs);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       kind: 'slice',
       start,
@@ -104,38 +104,43 @@
       utids: new Uint32Array(numRows),
     };
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      cpu: NUM,
+      utid: NUM,
+    });
+
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.cpus[row] = +cols[3].longValues![row];
-      slices.utids[row] = +cols[4].longValues![row];
+      slices.cpus[row] = it.cpu;
+      slices.utids[row] = it.utid;
       slices.end = Math.max(slices.ends[row], slices.end);
     }
     return slices;
   }
 
   private queryData(startNs: number, endNs: number, bucketNs: number):
-      Promise<RawQueryResult> {
+      Promise<QueryResult> {
     const isCached = this.cachedBucketNs <= bucketNs;
     const tsq = isCached ? `cached_tsq / ${bucketNs} * ${bucketNs}` :
                            `(ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs}`;
     const queryTable = isCached ? this.tableName('process_sched_cached') :
                                   this.tableName('process_sched');
-    const constainColumn = isCached ? 'cached_tsq' : 'ts';
-    return this.query(`
+    const constraintColumn = isCached ? 'cached_tsq' : 'ts';
+    return this.queryV2(`
       select
         ${tsq} as tsq,
         ts,
@@ -144,15 +149,15 @@
         utid
       from ${queryTable}
       where
-        ${constainColumn} >= ${startNs - this.maxDurNs} and
-        ${constainColumn} <= ${endNs}
+        ${constraintColumn} >= ${startNs - this.maxDurNs} and
+        ${constraintColumn} <= ${endNs}
       group by tsq, cpu
       order by tsq, cpu
     `);
   }
 
   private async createSchedView() {
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('process_sched')} as
       select ts, dur, cpu, utid
       from experimental_sched_upid
diff --git a/ui/src/tracks/process_summary/controller.ts b/ui/src/tracks/process_summary/controller.ts
index 75242fe..9e6ec5e 100644
--- a/ui/src/tracks/process_summary/controller.ts
+++ b/ui/src/tracks/process_summary/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {LIMIT} from '../../common/track_data';
 import {
@@ -39,29 +39,35 @@
     const endNs = toNs(end);
 
     if (this.setup === false) {
-      await this.query(
+      await this.queryV2(
           `create virtual table ${this.tableName('window')} using window;`);
 
       let utids = [this.config.utid];
       if (this.config.upid) {
-        const threadQuery = await this.query(
+        const threadQuery = await this.queryV2(
             `select utid from thread where upid=${this.config.upid}`);
-        utids = threadQuery.columns[0].longValues!;
+        utids = [];
+        for (const it = threadQuery.iter({utid: NUM}); it.valid(); it.next()) {
+          utids.push(it.utid);
+        }
       }
 
-      const trackQuery = await this.query(
+      const trackQuery = await this.queryV2(
           `select id from thread_track where utid in (${utids.join(',')})`);
-      const tracks = trackQuery.columns[0].longValues!;
+      const tracks = [];
+      for (const it = trackQuery.iter({id: NUM}); it.valid(); it.next()) {
+        tracks.push(it.id);
+      }
 
       const processSliceView = this.tableName('process_slice_view');
-      await this.query(
+      await this.queryV2(
           `create view ${processSliceView} as ` +
           // 0 as cpu is a dummy column to perform span join on.
           `select ts, dur/${utids.length} as dur ` +
           `from slice s ` +
           `where depth = 0 and track_id in ` +
           `(${tracks.join(',')})`);
-      await this.query(`create virtual table ${this.tableName('span')}
+      await this.queryV2(`create virtual table ${this.tableName('span')}
           using span_join(${processSliceView},
                           ${this.tableName('window')});`);
       this.setup = true;
@@ -73,7 +79,7 @@
     const windowStartNs = Math.floor(startNs / bucketSizeNs) * bucketSizeNs;
     const windowDurNs = Math.max(1, endNs - windowStartNs);
 
-    this.query(`update ${this.tableName('window')} set
+    await this.queryV2(`update ${this.tableName('window')} set
       window_start=${windowStartNs},
       window_dur=${windowDurNs},
       quantum=${bucketSizeNs}
@@ -98,9 +104,6 @@
       group by quantum_ts
       limit ${LIMIT}`;
 
-    const rawResult = await this.query(query);
-    const numRows = slowlyCountRows(rawResult);
-
     const summary: Data = {
       start,
       end,
@@ -109,21 +112,24 @@
       bucketSizeSeconds: fromNs(bucketSizeNs),
       utilizations: new Float64Array(numBuckets),
     };
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const bucket = +cols[0].longValues![row];
+
+    const queryRes = await this.queryV2(query);
+    const it = queryRes.iter({bucket: NUM, utilization: NUM});
+    for (; it.valid(); it.next()) {
+      const bucket = it.bucket;
       if (bucket > numBuckets) {
         continue;
       }
-      summary.utilizations[bucket] = +cols[1].doubleValues![row];
+      summary.utilizations[bucket] = it.utilization;
     }
+
     return summary;
   }
 
   onDestroy(): void {
     if (this.setup) {
-      this.query(`drop table ${this.tableName('window')}`);
-      this.query(`drop table ${this.tableName('span')}`);
+      this.queryV2(`drop table ${this.tableName('window')}`);
+      this.queryV2(`drop table ${this.tableName('span')}`);
       this.setup = false;
     }
   }
diff --git a/ui/src/tracks/thread_state/controller.ts b/ui/src/tracks/thread_state/controller.ts
index a4718c6..0de3a3e 100644
--- a/ui/src/tracks/thread_state/controller.ts
+++ b/ui/src/tracks/thread_state/controller.ts
@@ -14,10 +14,8 @@
 
 import {assertFalse} from '../../base/logging';
 import {
-  iter,
   NUM,
   NUM_NULL,
-  slowlyCountRows,
   STR_NULL
 } from '../../common/query_iterator';
 import {translateState} from '../../common/thread_state';
@@ -39,7 +37,7 @@
   private maxDurNs = 0;
 
   async onSetup() {
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('thread_state')} as
       select
         id,
@@ -52,11 +50,11 @@
       where utid = ${this.config.utid} and utid != 0
     `);
 
-    const rawResult = await this.query(`
-      select max(dur)
+    const queryRes = await this.queryV2(`
+      select ifnull(max(dur), 0) as maxDur
       from ${this.tableName('thread_state')}
     `);
-    this.maxDurNs = rawResult.columns[0].longValues![0];
+    this.maxDurNs = queryRes.firstRow({maxDur: NUM}).maxDur;
   }
 
   async onBoundsChange(start: number, end: number, resolution: number):
@@ -75,10 +73,10 @@
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
         max(dur) as dur,
-        cast(cpu as integer) as cpu,
+        ifnull(cast(cpu as integer), -1) as cpu,
         state,
         io_wait,
-        id
+        ifnull(id, -1) as id
       from ${this.tableName('thread_state')}
       where
         ts >= ${startNs - this.maxDurNs} and
@@ -87,8 +85,8 @@
       order by tsq, state, io_wait
     `;
 
-    const result = await this.query(query);
-    const numRows = slowlyCountRows(result);
+    const queryRes = await this.queryV2(query);
+    const numRows = queryRes.numRows();
 
     const data: Data = {
       start,
@@ -113,31 +111,28 @@
       stringIndexes.set({shortState, ioWait}, idx);
       return idx;
     }
-    iter(
-        {
-          'ts': NUM,
-          'dur': NUM,
-          'cpu': NUM_NULL,
-          'state': STR_NULL,
-          'io_wait': NUM_NULL,
-          'id': NUM_NULL,
-        },
-        result);
-    for (let row = 0; row < numRows; row++) {
-      const cols = result.columns;
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      'tsq': NUM,
+      'ts': NUM,
+      'dur': NUM,
+      'cpu': NUM,
+      'state': STR_NULL,
+      'io_wait': NUM_NULL,
+      'id': NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      const cpu = cols[3].isNulls![row] ? -1 : cols[3].longValues![row];
-      const state = cols[4].stringValues![row];
-      const ioWait =
-          cols[5].isNulls![row] ? undefined : !!cols[5].longValues![row];
-      const id = cols[6].isNulls![row] ? -1 : cols[6].longValues![row];
+      const cpu = it.cpu;
+      const state = it.state || '[null]';
+      const ioWait = it.io_wait === null ? undefined : !!it.io_wait;
+      const id = it.id;
 
       // We should never have the end timestamp being the same as the bucket
       // start.
@@ -153,7 +148,8 @@
   }
 
   async onDestroy() {
-    await this.query(`drop table if exists ${this.tableName('thread_state')}`);
+    await this.queryV2(
+        `drop table if exists ${this.tableName('thread_state')}`);
   }
 }
 
diff --git a/ui/tsconfig.base.json b/ui/tsconfig.base.json
index 075b1b3..cd52245 100644
--- a/ui/tsconfig.base.json
+++ b/ui/tsconfig.base.json
@@ -8,6 +8,7 @@
     "allowJs": true,
     "declaration": false,                  // Generates corresponding '.d.ts' file.
     "sourceMap": true,                     // Generates corresponding '.map' file.
+    "inlineSources": true,                 // Adds source code to the '.map' file.
     "removeComments": false,               // Do not emit comments to output.
     "importHelpers": true,                 // Import emit helpers from 'tslib'.
     "downlevelIteration": true,            // Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'.