Merge "add auto_user_switch metric;" into main
diff --git a/Android.bp b/Android.bp
index 88026d5..585df73 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2376,6 +2376,7 @@
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_interface",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":perfetto_src_trace_processor_sorter_sorter",
+        ":perfetto_src_trace_processor_sqlite_bindings_bindings",
         ":perfetto_src_trace_processor_sqlite_query_constraints",
         ":perfetto_src_trace_processor_sqlite_sqlite",
         ":perfetto_src_trace_processor_storage_minimal",
@@ -10718,6 +10719,8 @@
         "src/protozero/test/example_proto/extensions.proto",
         "src/protozero/test/example_proto/library.proto",
         "src/protozero/test/example_proto/library_internals/galaxies.proto",
+        "src/protozero/test/example_proto/other_package/test_messages.proto",
+        "src/protozero/test/example_proto/subpackage/test_messages.proto",
         "src/protozero/test/example_proto/test_messages.proto",
         "src/protozero/test/example_proto/upper_import.proto",
     ],
@@ -10747,6 +10750,8 @@
     name: "perfetto_src_protozero_testing_messages_cpp_gen",
     srcs: [
         ":perfetto_src_protozero_testing_messages_cpp",
+        ":perfetto_src_protozero_testing_messages_other_package_cpp",
+        ":perfetto_src_protozero_testing_messages_subpackage_cpp",
     ],
     tools: [
         "aprotoc",
@@ -10767,6 +10772,8 @@
     name: "perfetto_src_protozero_testing_messages_cpp_gen_headers",
     srcs: [
         ":perfetto_src_protozero_testing_messages_cpp",
+        ":perfetto_src_protozero_testing_messages_other_package_cpp",
+        ":perfetto_src_protozero_testing_messages_subpackage_cpp",
     ],
     tools: [
         "aprotoc",
@@ -10803,6 +10810,8 @@
     name: "perfetto_src_protozero_testing_messages_lite_gen",
     srcs: [
         ":perfetto_src_protozero_testing_messages_lite",
+        ":perfetto_src_protozero_testing_messages_other_package_lite",
+        ":perfetto_src_protozero_testing_messages_subpackage_lite",
     ],
     tools: [
         "aprotoc",
@@ -10822,6 +10831,8 @@
     name: "perfetto_src_protozero_testing_messages_lite_gen_headers",
     srcs: [
         ":perfetto_src_protozero_testing_messages_lite",
+        ":perfetto_src_protozero_testing_messages_other_package_lite",
+        ":perfetto_src_protozero_testing_messages_subpackage_lite",
     ],
     tools: [
         "aprotoc",
@@ -10840,6 +10851,266 @@
     ],
 }
 
+// GN: //src/protozero:testing_messages_other_package_cpp
+filegroup {
+    name: "perfetto_src_protozero_testing_messages_other_package_cpp",
+    srcs: [
+        "src/protozero/test/example_proto/other_package/test_messages.proto",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_other_package_cpp
+genrule {
+    name: "perfetto_src_protozero_testing_messages_other_package_cpp_gen",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_other_package_cpp",
+    ],
+    tools: [
+        "aprotoc",
+        "perfetto_src_protozero_protoc_plugin_cppgen_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location perfetto_src_protozero_protoc_plugin_cppgen_plugin) --plugin_out=wrapper_namespace=gen:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_other_package_cpp)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/other_package/test_messages.gen.cc",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_other_package_cpp
+genrule {
+    name: "perfetto_src_protozero_testing_messages_other_package_cpp_gen_headers",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_other_package_cpp",
+    ],
+    tools: [
+        "aprotoc",
+        "perfetto_src_protozero_protoc_plugin_cppgen_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location perfetto_src_protozero_protoc_plugin_cppgen_plugin) --plugin_out=wrapper_namespace=gen:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_other_package_cpp)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/other_package/test_messages.gen.h",
+    ],
+    export_include_dirs: [
+        ".",
+        "protos",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_other_package_lite
+filegroup {
+    name: "perfetto_src_protozero_testing_messages_other_package_lite",
+    srcs: [
+        "src/protozero/test/example_proto/other_package/test_messages.proto",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_other_package_lite
+genrule {
+    name: "perfetto_src_protozero_testing_messages_other_package_lite_gen",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_other_package_lite",
+    ],
+    tools: [
+        "aprotoc",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --cpp_out=lite=true:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_other_package_lite)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/other_package/test_messages.pb.cc",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_other_package_lite
+genrule {
+    name: "perfetto_src_protozero_testing_messages_other_package_lite_gen_headers",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_other_package_lite",
+    ],
+    tools: [
+        "aprotoc",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --cpp_out=lite=true:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_other_package_lite)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/other_package/test_messages.pb.h",
+    ],
+    export_include_dirs: [
+        ".",
+        "protos",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_other_package_zero
+filegroup {
+    name: "perfetto_src_protozero_testing_messages_other_package_zero",
+    srcs: [
+        "src/protozero/test/example_proto/other_package/test_messages.proto",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_other_package_zero
+genrule {
+    name: "perfetto_src_protozero_testing_messages_other_package_zero_gen",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_other_package_zero",
+    ],
+    tools: [
+        "aprotoc",
+        "protozero_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location protozero_plugin) --plugin_out=wrapper_namespace=pbzero:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_other_package_zero)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/other_package/test_messages.pbzero.cc",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_other_package_zero
+genrule {
+    name: "perfetto_src_protozero_testing_messages_other_package_zero_gen_headers",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_other_package_zero",
+    ],
+    tools: [
+        "aprotoc",
+        "protozero_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location protozero_plugin) --plugin_out=wrapper_namespace=pbzero:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_other_package_zero)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/other_package/test_messages.pbzero.h",
+    ],
+    export_include_dirs: [
+        ".",
+        "protos",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_subpackage_cpp
+filegroup {
+    name: "perfetto_src_protozero_testing_messages_subpackage_cpp",
+    srcs: [
+        "src/protozero/test/example_proto/subpackage/test_messages.proto",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_subpackage_cpp
+genrule {
+    name: "perfetto_src_protozero_testing_messages_subpackage_cpp_gen",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_subpackage_cpp",
+    ],
+    tools: [
+        "aprotoc",
+        "perfetto_src_protozero_protoc_plugin_cppgen_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location perfetto_src_protozero_protoc_plugin_cppgen_plugin) --plugin_out=wrapper_namespace=gen:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_subpackage_cpp)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/subpackage/test_messages.gen.cc",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_subpackage_cpp
+genrule {
+    name: "perfetto_src_protozero_testing_messages_subpackage_cpp_gen_headers",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_subpackage_cpp",
+    ],
+    tools: [
+        "aprotoc",
+        "perfetto_src_protozero_protoc_plugin_cppgen_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location perfetto_src_protozero_protoc_plugin_cppgen_plugin) --plugin_out=wrapper_namespace=gen:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_subpackage_cpp)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/subpackage/test_messages.gen.h",
+    ],
+    export_include_dirs: [
+        ".",
+        "protos",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_subpackage_lite
+filegroup {
+    name: "perfetto_src_protozero_testing_messages_subpackage_lite",
+    srcs: [
+        "src/protozero/test/example_proto/subpackage/test_messages.proto",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_subpackage_lite
+genrule {
+    name: "perfetto_src_protozero_testing_messages_subpackage_lite_gen",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_subpackage_lite",
+    ],
+    tools: [
+        "aprotoc",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --cpp_out=lite=true:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_subpackage_lite)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/subpackage/test_messages.pb.cc",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_subpackage_lite
+genrule {
+    name: "perfetto_src_protozero_testing_messages_subpackage_lite_gen_headers",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_subpackage_lite",
+    ],
+    tools: [
+        "aprotoc",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --cpp_out=lite=true:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_subpackage_lite)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/subpackage/test_messages.pb.h",
+    ],
+    export_include_dirs: [
+        ".",
+        "protos",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_subpackage_zero
+filegroup {
+    name: "perfetto_src_protozero_testing_messages_subpackage_zero",
+    srcs: [
+        "src/protozero/test/example_proto/subpackage/test_messages.proto",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_subpackage_zero
+genrule {
+    name: "perfetto_src_protozero_testing_messages_subpackage_zero_gen",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_subpackage_zero",
+    ],
+    tools: [
+        "aprotoc",
+        "protozero_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location protozero_plugin) --plugin_out=wrapper_namespace=pbzero:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_subpackage_zero)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/subpackage/test_messages.pbzero.cc",
+    ],
+}
+
+// GN: //src/protozero:testing_messages_subpackage_zero
+genrule {
+    name: "perfetto_src_protozero_testing_messages_subpackage_zero_gen_headers",
+    srcs: [
+        ":perfetto_src_protozero_testing_messages_subpackage_zero",
+    ],
+    tools: [
+        "aprotoc",
+        "protozero_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location protozero_plugin) --plugin_out=wrapper_namespace=pbzero:$(genDir)/external/perfetto/ $(locations :perfetto_src_protozero_testing_messages_subpackage_zero)",
+    out: [
+        "external/perfetto/src/protozero/test/example_proto/subpackage/test_messages.pbzero.h",
+    ],
+    export_include_dirs: [
+        ".",
+        "protos",
+    ],
+}
+
 // GN: //src/protozero:testing_messages_zero
 filegroup {
     name: "perfetto_src_protozero_testing_messages_zero",
@@ -10856,6 +11127,8 @@
 genrule {
     name: "perfetto_src_protozero_testing_messages_zero_gen",
     srcs: [
+        ":perfetto_src_protozero_testing_messages_other_package_zero",
+        ":perfetto_src_protozero_testing_messages_subpackage_zero",
         ":perfetto_src_protozero_testing_messages_zero",
     ],
     tools: [
@@ -10876,6 +11149,8 @@
 genrule {
     name: "perfetto_src_protozero_testing_messages_zero_gen_headers",
     srcs: [
+        ":perfetto_src_protozero_testing_messages_other_package_zero",
+        ":perfetto_src_protozero_testing_messages_subpackage_zero",
         ":perfetto_src_protozero_testing_messages_zero",
     ],
     tools: [
@@ -11948,6 +12223,7 @@
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/connected_flow.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs.cc",
+        "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/dominator_tree.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc",
@@ -12041,6 +12317,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/battery_stats.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/binder.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/broadcasts.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/dvfs.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/freezer.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/garbage_collection.sql",
@@ -12169,6 +12446,11 @@
     ],
 }
 
+// GN: //src/trace_processor/sqlite/bindings:bindings
+filegroup {
+    name: "perfetto_src_trace_processor_sqlite_bindings_bindings",
+}
+
 // GN: //src/trace_processor/sqlite:query_constraints
 filegroup {
     name: "perfetto_src_trace_processor_sqlite_query_constraints",
@@ -12515,11 +12797,15 @@
 filegroup {
     name: "perfetto_src_trace_redaction_trace_redaction",
     srcs: [
+        "src/trace_redaction/build_timeline.cc",
         "src/trace_redaction/find_package_uid.cc",
+        "src/trace_redaction/optimize_timeline.cc",
         "src/trace_redaction/populate_allow_lists.cc",
+        "src/trace_redaction/process_thread_timeline.cc",
         "src/trace_redaction/proto_util.cc",
         "src/trace_redaction/prune_package_list.cc",
         "src/trace_redaction/scrub_ftrace_events.cc",
+        "src/trace_redaction/scrub_process_trees.cc",
         "src/trace_redaction/scrub_trace_packet.cc",
         "src/trace_redaction/trace_redaction_framework.cc",
         "src/trace_redaction/trace_redactor.cc",
@@ -12530,7 +12816,9 @@
 filegroup {
     name: "perfetto_src_trace_redaction_unittests",
     srcs: [
+        "src/trace_redaction/build_timeline_unittest.cc",
         "src/trace_redaction/find_package_uid_unittest.cc",
+        "src/trace_redaction/process_thread_timeline_unittest.cc",
         "src/trace_redaction/proto_util_unittest.cc",
         "src/trace_redaction/prune_package_list_unittest.cc",
         "src/trace_redaction/scrub_ftrace_events_unittest.cc",
@@ -13928,6 +14216,12 @@
         ":perfetto_src_protozero_protozero",
         ":perfetto_src_protozero_testing_messages_cpp_gen",
         ":perfetto_src_protozero_testing_messages_lite_gen",
+        ":perfetto_src_protozero_testing_messages_other_package_cpp_gen",
+        ":perfetto_src_protozero_testing_messages_other_package_lite_gen",
+        ":perfetto_src_protozero_testing_messages_other_package_zero_gen",
+        ":perfetto_src_protozero_testing_messages_subpackage_cpp_gen",
+        ":perfetto_src_protozero_testing_messages_subpackage_lite_gen",
+        ":perfetto_src_protozero_testing_messages_subpackage_zero_gen",
         ":perfetto_src_protozero_testing_messages_zero_gen",
         ":perfetto_src_protozero_unittests",
         ":perfetto_src_shared_lib_intern_map",
@@ -13996,6 +14290,7 @@
         ":perfetto_src_trace_processor_rpc_unittests",
         ":perfetto_src_trace_processor_sorter_sorter",
         ":perfetto_src_trace_processor_sorter_unittests",
+        ":perfetto_src_trace_processor_sqlite_bindings_bindings",
         ":perfetto_src_trace_processor_sqlite_query_constraints",
         ":perfetto_src_trace_processor_sqlite_sqlite",
         ":perfetto_src_trace_processor_sqlite_unittests",
@@ -14226,6 +14521,12 @@
         "perfetto_src_perfetto_cmd_protos_cpp_gen_headers",
         "perfetto_src_protozero_testing_messages_cpp_gen_headers",
         "perfetto_src_protozero_testing_messages_lite_gen_headers",
+        "perfetto_src_protozero_testing_messages_other_package_cpp_gen_headers",
+        "perfetto_src_protozero_testing_messages_other_package_lite_gen_headers",
+        "perfetto_src_protozero_testing_messages_other_package_zero_gen_headers",
+        "perfetto_src_protozero_testing_messages_subpackage_cpp_gen_headers",
+        "perfetto_src_protozero_testing_messages_subpackage_lite_gen_headers",
+        "perfetto_src_protozero_testing_messages_subpackage_zero_gen_headers",
         "perfetto_src_protozero_testing_messages_zero_gen_headers",
         "perfetto_src_trace_processor_gen_cc_test_messages_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
@@ -14704,6 +15005,7 @@
         ":perfetto_src_trace_processor_rpc_rpc",
         ":perfetto_src_trace_processor_rpc_stdiod",
         ":perfetto_src_trace_processor_sorter_sorter",
+        ":perfetto_src_trace_processor_sqlite_bindings_bindings",
         ":perfetto_src_trace_processor_sqlite_query_constraints",
         ":perfetto_src_trace_processor_sqlite_sqlite",
         ":perfetto_src_trace_processor_storage_minimal",
@@ -14832,6 +15134,204 @@
     },
 }
 
+// GN: //src/trace_redaction:trace_redactor
+cc_binary {
+    name: "trace_redactor",
+    srcs: [
+        ":perfetto_base_default_platform",
+        ":perfetto_include_perfetto_base_base",
+        ":perfetto_include_perfetto_ext_base_base",
+        ":perfetto_include_perfetto_ext_trace_processor_importers_memory_tracker_memory_tracker",
+        ":perfetto_include_perfetto_protozero_protozero",
+        ":perfetto_include_perfetto_public_abi_base",
+        ":perfetto_include_perfetto_public_base",
+        ":perfetto_include_perfetto_public_protozero",
+        ":perfetto_include_perfetto_trace_processor_basic_types",
+        ":perfetto_include_perfetto_trace_processor_storage",
+        ":perfetto_include_perfetto_trace_processor_trace_processor",
+        ":perfetto_protos_perfetto_common_cpp_gen",
+        ":perfetto_protos_perfetto_common_zero_gen",
+        ":perfetto_protos_perfetto_config_android_cpp_gen",
+        ":perfetto_protos_perfetto_config_android_zero_gen",
+        ":perfetto_protos_perfetto_config_cpp_gen",
+        ":perfetto_protos_perfetto_config_ftrace_cpp_gen",
+        ":perfetto_protos_perfetto_config_ftrace_zero_gen",
+        ":perfetto_protos_perfetto_config_gpu_cpp_gen",
+        ":perfetto_protos_perfetto_config_gpu_zero_gen",
+        ":perfetto_protos_perfetto_config_inode_file_cpp_gen",
+        ":perfetto_protos_perfetto_config_inode_file_zero_gen",
+        ":perfetto_protos_perfetto_config_interceptors_cpp_gen",
+        ":perfetto_protos_perfetto_config_interceptors_zero_gen",
+        ":perfetto_protos_perfetto_config_power_cpp_gen",
+        ":perfetto_protos_perfetto_config_power_zero_gen",
+        ":perfetto_protos_perfetto_config_process_stats_cpp_gen",
+        ":perfetto_protos_perfetto_config_process_stats_zero_gen",
+        ":perfetto_protos_perfetto_config_profiling_cpp_gen",
+        ":perfetto_protos_perfetto_config_profiling_zero_gen",
+        ":perfetto_protos_perfetto_config_statsd_cpp_gen",
+        ":perfetto_protos_perfetto_config_statsd_zero_gen",
+        ":perfetto_protos_perfetto_config_sys_stats_cpp_gen",
+        ":perfetto_protos_perfetto_config_sys_stats_zero_gen",
+        ":perfetto_protos_perfetto_config_system_info_cpp_gen",
+        ":perfetto_protos_perfetto_config_system_info_zero_gen",
+        ":perfetto_protos_perfetto_config_track_event_cpp_gen",
+        ":perfetto_protos_perfetto_config_track_event_zero_gen",
+        ":perfetto_protos_perfetto_config_zero_gen",
+        ":perfetto_protos_perfetto_trace_android_cpp_gen",
+        ":perfetto_protos_perfetto_trace_android_zero_gen",
+        ":perfetto_protos_perfetto_trace_chrome_cpp_gen",
+        ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_cpp_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
+        ":perfetto_protos_perfetto_trace_filesystem_cpp_gen",
+        ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
+        ":perfetto_protos_perfetto_trace_ftrace_cpp_gen",
+        ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
+        ":perfetto_protos_perfetto_trace_gpu_cpp_gen",
+        ":perfetto_protos_perfetto_trace_gpu_zero_gen",
+        ":perfetto_protos_perfetto_trace_interned_data_cpp_gen",
+        ":perfetto_protos_perfetto_trace_interned_data_zero_gen",
+        ":perfetto_protos_perfetto_trace_minimal_cpp_gen",
+        ":perfetto_protos_perfetto_trace_minimal_zero_gen",
+        ":perfetto_protos_perfetto_trace_non_minimal_cpp_gen",
+        ":perfetto_protos_perfetto_trace_non_minimal_zero_gen",
+        ":perfetto_protos_perfetto_trace_perfetto_cpp_gen",
+        ":perfetto_protos_perfetto_trace_perfetto_zero_gen",
+        ":perfetto_protos_perfetto_trace_power_cpp_gen",
+        ":perfetto_protos_perfetto_trace_power_zero_gen",
+        ":perfetto_protos_perfetto_trace_processor_zero_gen",
+        ":perfetto_protos_perfetto_trace_profiling_cpp_gen",
+        ":perfetto_protos_perfetto_trace_profiling_zero_gen",
+        ":perfetto_protos_perfetto_trace_ps_cpp_gen",
+        ":perfetto_protos_perfetto_trace_ps_zero_gen",
+        ":perfetto_protos_perfetto_trace_statsd_cpp_gen",
+        ":perfetto_protos_perfetto_trace_statsd_zero_gen",
+        ":perfetto_protos_perfetto_trace_sys_stats_cpp_gen",
+        ":perfetto_protos_perfetto_trace_sys_stats_zero_gen",
+        ":perfetto_protos_perfetto_trace_system_info_cpp_gen",
+        ":perfetto_protos_perfetto_trace_system_info_zero_gen",
+        ":perfetto_protos_perfetto_trace_track_event_cpp_gen",
+        ":perfetto_protos_perfetto_trace_track_event_zero_gen",
+        ":perfetto_protos_perfetto_trace_translation_cpp_gen",
+        ":perfetto_protos_perfetto_trace_translation_zero_gen",
+        ":perfetto_src_base_base",
+        ":perfetto_src_protozero_protozero",
+        ":perfetto_src_trace_processor_containers_containers",
+        ":perfetto_src_trace_processor_db_column_column",
+        ":perfetto_src_trace_processor_db_minimal",
+        ":perfetto_src_trace_processor_importers_common_common",
+        ":perfetto_src_trace_processor_importers_common_parser_types",
+        ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
+        ":perfetto_src_trace_processor_importers_ftrace_minimal",
+        ":perfetto_src_trace_processor_importers_fuchsia_fuchsia_record",
+        ":perfetto_src_trace_processor_importers_json_minimal",
+        ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
+        ":perfetto_src_trace_processor_importers_proto_minimal",
+        ":perfetto_src_trace_processor_importers_proto_packet_sequence_state_generation_hdr",
+        ":perfetto_src_trace_processor_importers_proto_proto_importer_module",
+        ":perfetto_src_trace_processor_importers_systrace_systrace_line",
+        ":perfetto_src_trace_processor_metatrace",
+        ":perfetto_src_trace_processor_sorter_sorter",
+        ":perfetto_src_trace_processor_storage_minimal",
+        ":perfetto_src_trace_processor_storage_storage",
+        ":perfetto_src_trace_processor_tables_tables",
+        ":perfetto_src_trace_processor_types_types",
+        ":perfetto_src_trace_processor_util_build_id",
+        ":perfetto_src_trace_processor_util_bump_allocator",
+        ":perfetto_src_trace_processor_util_descriptors",
+        ":perfetto_src_trace_processor_util_glob",
+        ":perfetto_src_trace_processor_util_gzip",
+        ":perfetto_src_trace_processor_util_interned_message_view",
+        ":perfetto_src_trace_processor_util_profiler_util",
+        ":perfetto_src_trace_processor_util_proto_to_args_parser",
+        ":perfetto_src_trace_processor_util_protozero_to_text",
+        ":perfetto_src_trace_processor_util_regex",
+        ":perfetto_src_trace_processor_util_util",
+        ":perfetto_src_trace_redaction_trace_redaction",
+        "src/trace_redaction/main.cc",
+    ],
+    shared_libs: [
+        "liblog",
+        "libz",
+    ],
+    generated_headers: [
+        "perfetto_protos_perfetto_common_cpp_gen_headers",
+        "perfetto_protos_perfetto_common_zero_gen_headers",
+        "perfetto_protos_perfetto_config_android_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_android_zero_gen_headers",
+        "perfetto_protos_perfetto_config_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_ftrace_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_ftrace_zero_gen_headers",
+        "perfetto_protos_perfetto_config_gpu_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_gpu_zero_gen_headers",
+        "perfetto_protos_perfetto_config_inode_file_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_inode_file_zero_gen_headers",
+        "perfetto_protos_perfetto_config_interceptors_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_interceptors_zero_gen_headers",
+        "perfetto_protos_perfetto_config_power_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_power_zero_gen_headers",
+        "perfetto_protos_perfetto_config_process_stats_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_process_stats_zero_gen_headers",
+        "perfetto_protos_perfetto_config_profiling_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_profiling_zero_gen_headers",
+        "perfetto_protos_perfetto_config_statsd_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_statsd_zero_gen_headers",
+        "perfetto_protos_perfetto_config_sys_stats_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_sys_stats_zero_gen_headers",
+        "perfetto_protos_perfetto_config_system_info_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_system_info_zero_gen_headers",
+        "perfetto_protos_perfetto_config_track_event_cpp_gen_headers",
+        "perfetto_protos_perfetto_config_track_event_zero_gen_headers",
+        "perfetto_protos_perfetto_config_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_android_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_android_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_chrome_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_filesystem_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_ftrace_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_gpu_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_interned_data_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_interned_data_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_minimal_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_minimal_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_non_minimal_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_non_minimal_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_perfetto_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_perfetto_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_power_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_power_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_processor_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_profiling_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_profiling_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_ps_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_ps_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_statsd_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_statsd_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_sys_stats_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_sys_stats_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_system_info_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_system_info_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_track_event_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_track_event_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_translation_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_translation_zero_gen_headers",
+        "perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
+        "perfetto_src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
+        "perfetto_src_trace_processor_tables_tables_python",
+    ],
+    defaults: [
+        "perfetto_defaults",
+    ],
+    cflags: [
+        "-DZLIB_IMPLEMENTATION",
+    ],
+}
+
 // GN: //src/traceconv:traceconv
 cc_binary_host {
     name: "traceconv",
@@ -14938,6 +15438,7 @@
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_interface",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":perfetto_src_trace_processor_sorter_sorter",
+        ":perfetto_src_trace_processor_sqlite_bindings_bindings",
         ":perfetto_src_trace_processor_sqlite_query_constraints",
         ":perfetto_src_trace_processor_sqlite_sqlite",
         ":perfetto_src_trace_processor_storage_minimal",
@@ -15257,12 +15758,6 @@
     defaults: [
         "perfetto_defaults",
     ],
-    required: [
-        "libperfetto_android_internal",
-        "mm_events",
-        "traced_perf",
-        "trigger_perfetto",
-    ],
     target: {
         android: {
             shared_libs: [
@@ -15630,3 +16125,13 @@
     sub_dir: "perfetto",
     src: "persistent_cfg.pbtxt",
 }
+
+phony {
+    name: "perfetto-extras",
+    required: [
+        "libperfetto_android_internal",
+        "mm_events",
+        "traced_perf",
+        "trigger_perfetto",
+    ],
+}
diff --git a/Android.bp.extras b/Android.bp.extras
index 2ce8e95..3aab5af 100644
--- a/Android.bp.extras
+++ b/Android.bp.extras
@@ -205,3 +205,13 @@
     sub_dir: "perfetto",
     src: "persistent_cfg.pbtxt",
 }
+
+phony {
+    name: "perfetto-extras",
+    required: [
+        "libperfetto_android_internal",
+        "mm_events",
+        "traced_perf",
+        "trigger_perfetto",
+    ],
+}
diff --git a/BUILD b/BUILD
index 26d2e55..d678720 100644
--- a/BUILD
+++ b/BUILD
@@ -259,6 +259,7 @@
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_tables",
         ":src_trace_processor_rpc_rpc",
         ":src_trace_processor_sorter_sorter",
+        ":src_trace_processor_sqlite_bindings_bindings",
         ":src_trace_processor_sqlite_query_constraints",
         ":src_trace_processor_sqlite_sqlite",
         ":src_trace_processor_storage_minimal",
@@ -2305,6 +2306,8 @@
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.h",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs.h",
+        "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc",
+        "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/dominator_tree.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/dominator_tree.h",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.cc",
@@ -2385,6 +2388,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/battery_stats.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/binder.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/broadcasts.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/dvfs.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/freezer.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/garbage_collection.sql",
@@ -2636,6 +2640,17 @@
     ],
 )
 
+# GN target: //src/trace_processor/sqlite/bindings:bindings
+perfetto_filegroup(
+    name = "src_trace_processor_sqlite_bindings_bindings",
+    srcs = [
+        "src/trace_processor/sqlite/bindings/sqlite_aggregate_function.h",
+        "src/trace_processor/sqlite/bindings/sqlite_module.h",
+        "src/trace_processor/sqlite/bindings/sqlite_result.h",
+        "src/trace_processor/sqlite/bindings/sqlite_window_function.h",
+    ],
+)
+
 # GN target: //src/trace_processor/sqlite:query_constraints
 perfetto_filegroup(
     name = "src_trace_processor_sqlite_query_constraints",
@@ -5683,6 +5698,7 @@
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_tables",
         ":src_trace_processor_sorter_sorter",
+        ":src_trace_processor_sqlite_bindings_bindings",
         ":src_trace_processor_sqlite_query_constraints",
         ":src_trace_processor_sqlite_sqlite",
         ":src_trace_processor_storage_minimal",
@@ -5854,6 +5870,7 @@
         ":src_trace_processor_rpc_rpc",
         ":src_trace_processor_rpc_stdiod",
         ":src_trace_processor_sorter_sorter",
+        ":src_trace_processor_sqlite_bindings_bindings",
         ":src_trace_processor_sqlite_query_constraints",
         ":src_trace_processor_sqlite_sqlite",
         ":src_trace_processor_storage_minimal",
@@ -6077,6 +6094,7 @@
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_tables",
         ":src_trace_processor_sorter_sorter",
+        ":src_trace_processor_sqlite_bindings_bindings",
         ":src_trace_processor_sqlite_query_constraints",
         ":src_trace_processor_sqlite_sqlite",
         ":src_trace_processor_storage_minimal",
diff --git a/docs/concepts/concurrent-tracing-sessions.md b/docs/concepts/concurrent-tracing-sessions.md
new file mode 100644
index 0000000..63f6dd4
--- /dev/null
+++ b/docs/concepts/concurrent-tracing-sessions.md
@@ -0,0 +1,76 @@
+# Concurrent tracing sessions
+
+Perfetto supports multiple concurrent tracing sessions.
+Sessions are isolated from each other each and each session can choose a different mix of producers and data sources in its [config](config.md) and, in general, it will only receive events specified by that config.
+This is a powerful mechanism which allows great flexibility when collecting traces from the lab or field.
+However there are a few caveats to bear in mind with concurrent tracing sessions:
+1. [Some data sources do not support concurrent sessions](#some-data-sources-do-not-support-concurrent-sessions)
+2. [Some settings are per session while others are per producer](#some-settings-are-per-session-while-others-are-per-producer)
+3. Due to the [way atrace works works](#atrace) if a session requests *any* atrace category or app it receives *all* atrace events enabled on the device
+4. [Various limits](#various-limits) apply
+
+## Some data sources do not support concurrent sessions
+
+Whilst most data sources implemented with the Perfetto SDK as well as most data sources provided by the Perfetto team, do support concurrent tracing sessions some do not.
+This can be due to:
+
+- Hardware or driver constraints
+- Difficulty of implementing the config muxing
+- Perfetto SDK: users may [opt-out of multiple session support](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/include/perfetto/tracing/data_source.h;l=266;drc=f988c792c18f93841b14ffa71019fdedf7ab2f03)
+
+### Known to work
+- `traced_probes` data sources ([linux.ftrace](/docs/reference/trace-config-proto.autogen#FtraceConfig), [linux.process_stats](/docs/reference/trace-config-proto.autogen#ProcessStatsConfig), [linux.sys_stats](/docs/reference/trace-config-proto.autogen#SysStatsConfig), [linux.system_info](https://perfetto.dev/docs/reference/trace-config-proto.autogen#SystemInfoConfig), etc)
+
+### Known to work with caveats
+- `heapprofd` supports multiple sessions but each process can only be in one session.
+- `traced_perf` in general supports multiple sessions but the kernel has a limit on counters so may reject a config.
+
+### Known not to work
+- `traced metatracing`
+
+## Some settings are per session while others are per producer
+
+Most buffer sizes and timings specified in the config are per session.
+For example the buffer [sizes](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/config/trace_config.proto;l=32?q=f:perfetto%20f:trace_config&ss=android%2Fplatform%2Fsuperproject%2Fmain).
+
+However some parameters configure per-producer settings: for example the [size and layout](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/config/trace_config.proto;l=182;drc=488df1649781de42b72e981c5e79ad922508d1e5) of the shmem buffer between the producer and traced.
+While that is general data source setting the same can apply to data source specific settings.
+For example the ftrace [kernel buffer size and drain period](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/config/ftrace/ftrace_config.proto;l=32;drc=6a3d3540e68f3d5949b5d86ca736bfd7f811deff) are settings that have to be shared between all users of `traced_probes`.
+
+Bear in mind that
+- Some resources like the shmem buffers are shared by all sessions
+- As suggested by the comments in linked code above some settings are best treated as 'hints' since another config may have already set them before you get a chance to.
+
+## Atrace
+
+Atrace is an Android specific mechanism for doing userland instrumentation and the only available tracing method prior to the introduction of the Perfetto SDK into Android.
+It still powers [os.Trace](https://developer.android.com/reference/android/os/Trace) (as used by platform and application Java code) and [ATRACE_*](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/trace.h;l=188;drc=0c44d8d68d56c7aecb828d8d87fba7dcb114f3d9) (as used by platform C++).
+
+
+Atrace (both prior to Perfetto and via Perfetto) works as follows:
+- Configuration:
+  - Users choose zero or more 'categories' from a hardcoded list
+  - Users choose zero or more package names including globs
+- This sets:
+  - Some kernel ftrace events
+  - A [system property bitmask](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/cmds/atrace/atrace.cpp;l=306;drc=c8af4d3407f3d6be46fafdfc044ace55944fb4b7) (for the atrace categories)
+  - A [system property](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/cmds/atrace/atrace.cpp;l=306;bpv=1;bpt=1) for each package.
+- When the Java or C++ tracing APIs are called we examine the system props.
+- If the relevant category or package is enabled we write the event to `trace_marker`
+
+As mentioned, each category may enable a number of kernel ftrace events.
+For example the 'sched' atrace category enables the `sched/sched_switch` ftrace event.
+Kernel ftrace events do not suffer from the current session issues so will not be described further.
+
+For the userland instrumentation:
+- Perfetto ensures the union of all atrace packages categories are installed
+- However since:
+  - the atrace system properties are global
+  - we cannot tell which event comes from which category/package
+Every session that requests *any* atrace event gets *all* enabled atrace events.
+
+## Various limits
+- Perfetto SDK: Max 8 datasource instances per datasource type per producer
+- `traced`: Limit of [15 concurrent sessions](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/src/tracing/service/tracing_service_impl.cc;l=114?q=kMaxConcurrentTracingSessions%20)
+- `traced`: Limit of [5 (10 for statsd) concurrent sessions per UID](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/src/tracing/service/tracing_service_impl.cc;l=115;drc=17d5806d458e214bdb829deeeb08b098c2b5254d)
+
diff --git a/docs/contributing/common-tasks.md b/docs/contributing/common-tasks.md
index feb7aaa..11d4f14 100644
--- a/docs/contributing/common-tasks.md
+++ b/docs/contributing/common-tasks.md
@@ -16,22 +16,23 @@
 
 ## Contribute to SQL standard library
 
-1. Add or edit an SQL file inside `perfetto/src/trace_processor/stdlib/`.
-2. For a new file inside an existing module add the file to the corresponding `BUILD.gn`.
-3. For a new module (subdirectory of `/stdlib/`), module name (directory name) has to be added to the list in `/stdlib/BUILD.gn`.
+1. Add or edit an SQL file inside `perfetto/src/trace_processor/stdlib/`. This SQL file will be a new standard library module.
+2. For a new file inside an existing package add the file to the corresponding `BUILD.gn`.
+3. For a new package (subdirectory of `/stdlib/`), the package name (directory name) has to be added to the list in `/stdlib/BUILD.gn`.
 
 Files inside the standard library have to be formatted in a very specific way, as its structure is used to generate documentation. There are presubmit checks, but they are not infallible.
 
-- Running the file cannot generate any data. There can be only
-  `CREATE PERFETTO FUNCTION`, `CREATE PERFETTO TABLE` and `CREATE PERFETTO VIEW` statements inside.
-- The name of each table/view/function needs to start with `{module_name}_` or `{internal_}`.
-  The names of views/tables/functions are must be `[a-z_]`. When a module is imported (using the `IMPORT` function), objects prefixed with internal should not be used.
-  - The only exception is the `common` module. The name of functions/views/tables inside should not be prefixed with `common_`, as they are supposed to be module agnostic and widely used.
+- Running the file cannot generate any data. There can be only `CREATE PERFETTO {FUNCTION|TABLE|VIEW|MACRO}` statements inside.
+- The name of each standard library object needs to start with `{module_name}_` or be prefixed with an underscore(`_`) for internal objects.
+  The names must only contain lower and upper case letters and underscores. When a module is included (using the `INCLUDE PERFETTO MODULE`) the internal objects  should not be treated as an API. 
 - Every table or view should have [a schema](/docs/analysis/perfetto-sql-syntax.md#tableview-schema).
+
+### Documentation
+
 - Every non internal object, as well as its function arguments and columns in its schema have to be prefixed with an SQL comment documenting it.
-  Any text is going to be parsed as markdown, so usage of markdown functionality (code, links, lists) is encouraged.
-  Whitespaces in anything apart from descriptions are ignored, so comments can be formatted neatly.
-  If the line with description exceeds 80 chars, description can be continued in following lines.
+- Any text is going to be parsed as markdown, so usage of markdown functionality (code, links, lists) is encouraged.
+Whitespaces in anything apart from descriptions are ignored, so comments can be formatted neatly.
+If the line with description exceeds 80 chars, description can be continued in following lines.
   - **Table/view**: each has to have schema, object description and a comment above each column's definition in the schema.
     - Description is any text in the comment above `CREATE PERFETTO {TABLE,VIEW}` statement.
     - Column's comment is the text immediately above column definition in the schema.
@@ -65,9 +66,9 @@
   slice.name AS slice_name,
   COUNT(*) AS event_count
 FROM slice
-INNER JOIN thread_track ON slice.track_id = thread_track.id
-INNER JOIN thread ON thread.utid = thread_track.utid
-INNER JOIN process ON thread.upid = process.upid
+JOIN thread_track ON slice.track_id = thread_track.id
+JOIN thread ON thread.utid = thread_track.utid
+JOIN process ON thread.upid = process.upid
 WHERE
   slice.name GLOB 'binder*'
 GROUP BY
@@ -75,17 +76,6 @@
   slice_name;
 ```
 
-Example of function in module `common`:
-```sql
--- Extracts an int value with the given name from the metadata table.
-CREATE PERFETTO FUNCTION extract_int_metadata(
-  -- The name of the metadata entry.
-  name STRING)
--- int_value for the given name. NULL if there's no such entry.
-RETURNS LONG
-AS SELECT int_value FROM metadata WHERE name = ($name)
-```
-
 Example of table function in module `android`:
 ```sql
 -- Given a launch id and GLOB for a slice name, returns columns for matching slices.
@@ -108,7 +98,12 @@
   arg_set_id INT
 )
 AS
-SELECT slice_name, slice_ts, slice_dur, thread_name, arg_set_id
+SELECT 
+  slice_name, 
+  slice_ts, 
+  slice_dur, 
+  thread_name, 
+  arg_set_id
 FROM thread_slices_for_all_launches
 WHERE launch_id = $launch_id AND slice_name GLOB $slice_name;
 ```
diff --git a/docs/toc.md b/docs/toc.md
index 2ac2751..f828874 100644
--- a/docs/toc.md
+++ b/docs/toc.md
@@ -66,6 +66,7 @@
   * [Service model](concepts/service-model.md)
   * [Clock synchronization](concepts/clock-sync.md)
   * [Detached mode](concepts/detached-mode.md)
+  * [Concurrent tracing sessions](concepts/concurrent-tracing-sessions.md)
 
 * [Reference](#)
   * [Trace Config proto](reference/trace-config-proto.autogen)
diff --git a/include/perfetto/ext/base/sys_types.h b/include/perfetto/ext/base/sys_types.h
index cbb085d..bcb6433 100644
--- a/include/perfetto/ext/base/sys_types.h
+++ b/include/perfetto/ext/base/sys_types.h
@@ -29,7 +29,7 @@
 
 #if !PERFETTO_BUILDFLAG(PERFETTO_COMPILER_GCC)
 // MinGW has these. clang-cl and MSVC, which use just the Windows SDK, don't.
-using uid_t = unsigned int;
+using uid_t = int;
 using pid_t = int;
 #endif  // !GCC
 
diff --git a/include/perfetto/public/protos/common/data_source_descriptor.pzc.h b/include/perfetto/public/protos/common/data_source_descriptor.pzc.h
index 21e62fb..e2cd133 100644
--- a/include/perfetto/public/protos/common/data_source_descriptor.pzc.h
+++ b/include/perfetto/public/protos/common/data_source_descriptor.pzc.h
@@ -56,6 +56,11 @@
                   handles_incremental_state_clear,
                   4);
 PERFETTO_PB_FIELD(perfetto_protos_DataSourceDescriptor,
+                  VARINT,
+                  bool,
+                  no_flush,
+                  9);
+PERFETTO_PB_FIELD(perfetto_protos_DataSourceDescriptor,
                   MSG,
                   perfetto_protos_GpuCounterDescriptor,
                   gpu_counter_descriptor,
diff --git a/include/perfetto/public/protos/config/data_source_config.pzc.h b/include/perfetto/public/protos/config/data_source_config.pzc.h
index 718a3bc..ed7632b 100644
--- a/include/perfetto/public/protos/config/data_source_config.pzc.h
+++ b/include/perfetto/public/protos/config/data_source_config.pzc.h
@@ -26,12 +26,14 @@
 #include "perfetto/public/pb_macros.h"
 
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidGameInterventionListConfig);
+PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidInputEventConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidLogConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidPolledStateConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidPowerConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidSdkSyspropGuardConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidSystemPropertyConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeConfig);
+PERFETTO_PB_MSG_DECL(perfetto_protos_EtwConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_FtraceConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_GpuCounterConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_HeapprofdConfig);
@@ -42,6 +44,7 @@
 PERFETTO_PB_MSG_DECL(perfetto_protos_PackagesListConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_PerfEventConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_ProcessStatsConfig);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ProtoLogConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_StatsdTracingConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_SurfaceFlingerLayersConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_SurfaceFlingerTransactionsConfig);
@@ -49,6 +52,7 @@
 PERFETTO_PB_MSG_DECL(perfetto_protos_SystemInfoConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_TestConfig);
 PERFETTO_PB_MSG_DECL(perfetto_protos_TrackEventConfig);
+PERFETTO_PB_MSG_DECL(perfetto_protos_V8Config);
 PERFETTO_PB_MSG_DECL(perfetto_protos_VulkanMemoryConfig);
 
 PERFETTO_PB_ENUM_IN_MSG(perfetto_protos_DataSourceConfig, SessionInitiator){
@@ -196,6 +200,11 @@
                   101);
 PERFETTO_PB_FIELD(perfetto_protos_DataSourceConfig,
                   MSG,
+                  perfetto_protos_V8Config,
+                  v8_config,
+                  127);
+PERFETTO_PB_FIELD(perfetto_protos_DataSourceConfig,
+                  MSG,
                   perfetto_protos_InterceptorConfig,
                   interceptor_config,
                   115);
@@ -220,6 +229,21 @@
                   android_sdk_sysprop_guard_config,
                   124);
 PERFETTO_PB_FIELD(perfetto_protos_DataSourceConfig,
+                  MSG,
+                  perfetto_protos_EtwConfig,
+                  etw_config,
+                  125);
+PERFETTO_PB_FIELD(perfetto_protos_DataSourceConfig,
+                  MSG,
+                  perfetto_protos_ProtoLogConfig,
+                  protolog_config,
+                  126);
+PERFETTO_PB_FIELD(perfetto_protos_DataSourceConfig,
+                  MSG,
+                  perfetto_protos_AndroidInputEventConfig,
+                  android_input_event_config,
+                  128);
+PERFETTO_PB_FIELD(perfetto_protos_DataSourceConfig,
                   STRING,
                   const char*,
                   legacy_config,
diff --git a/include/perfetto/public/protos/config/trace_config.pzc.h b/include/perfetto/public/protos/config/trace_config.pzc.h
index a2b8432..76492e3 100644
--- a/include/perfetto/public/protos/config/trace_config.pzc.h
+++ b/include/perfetto/public/protos/config/trace_config.pzc.h
@@ -80,6 +80,8 @@
                                   SFP_MATCH_BREAK) = 3,
     PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TraceConfig_TraceFilter,
                                   SFP_ATRACE_MATCH_BREAK) = 4,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TraceConfig_TraceFilter,
+                                  SFP_ATRACE_REPEATED_SEARCH_REDACT_GROUPS) = 5,
 };
 
 PERFETTO_PB_ENUM_IN_MSG(perfetto_protos_TraceConfig_TriggerConfig, TriggerMode){
@@ -204,6 +206,11 @@
                   bugreport_score,
                   30);
 PERFETTO_PB_FIELD(perfetto_protos_TraceConfig,
+                  STRING,
+                  const char*,
+                  bugreport_filename,
+                  38);
+PERFETTO_PB_FIELD(perfetto_protos_TraceConfig,
                   MSG,
                   perfetto_protos_TraceConfig_TriggerConfig,
                   trigger_config,
@@ -234,11 +241,6 @@
                   compression_type,
                   24);
 PERFETTO_PB_FIELD(perfetto_protos_TraceConfig,
-                  VARINT,
-                  bool,
-                  compress_from_cli,
-                  37);
-PERFETTO_PB_FIELD(perfetto_protos_TraceConfig,
                   MSG,
                   perfetto_protos_TraceConfig_IncidentReportConfig,
                   incident_report_config,
diff --git a/include/perfetto/public/protos/trace/interned_data/interned_data.pzc.h b/include/perfetto/public/protos/trace/interned_data/interned_data.pzc.h
index 3c8f88a..71e26fa 100644
--- a/include/perfetto/public/protos/trace/interned_data/interned_data.pzc.h
+++ b/include/perfetto/public/protos/trace/interned_data/interned_data.pzc.h
@@ -35,6 +35,11 @@
 PERFETTO_PB_MSG_DECL(perfetto_protos_InternedGpuRenderStageSpecification);
 PERFETTO_PB_MSG_DECL(perfetto_protos_InternedGraphicsContext);
 PERFETTO_PB_MSG_DECL(perfetto_protos_InternedString);
+PERFETTO_PB_MSG_DECL(perfetto_protos_InternedV8Isolate);
+PERFETTO_PB_MSG_DECL(perfetto_protos_InternedV8JsFunction);
+PERFETTO_PB_MSG_DECL(perfetto_protos_InternedV8JsScript);
+PERFETTO_PB_MSG_DECL(perfetto_protos_InternedV8String);
+PERFETTO_PB_MSG_DECL(perfetto_protos_InternedV8WasmScript);
 PERFETTO_PB_MSG_DECL(perfetto_protos_LogMessageBody);
 PERFETTO_PB_MSG_DECL(perfetto_protos_Mapping);
 PERFETTO_PB_MSG_DECL(perfetto_protos_NetworkPacketContext);
@@ -153,5 +158,40 @@
                   perfetto_protos_NetworkPacketContext,
                   packet_context,
                   30);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedV8String,
+                  v8_js_function_name,
+                  31);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedV8JsFunction,
+                  v8_js_function,
+                  32);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedV8JsScript,
+                  v8_js_script,
+                  33);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedV8WasmScript,
+                  v8_wasm_script,
+                  34);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedV8Isolate,
+                  v8_isolate,
+                  35);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedString,
+                  protolog_string_args,
+                  36);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedString,
+                  protolog_stacktrace,
+                  37);
 
 #endif  // INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_INTERNED_DATA_INTERNED_DATA_PZC_H_
diff --git a/include/perfetto/public/protos/trace/trace_packet.pzc.h b/include/perfetto/public/protos/trace/trace_packet.pzc.h
index 52df003..d83ab18 100644
--- a/include/perfetto/public/protos/trace/trace_packet.pzc.h
+++ b/include/perfetto/public/protos/trace/trace_packet.pzc.h
@@ -29,6 +29,7 @@
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidCameraSessionStats);
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidEnergyEstimationBreakdown);
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidGameInterventionList);
+PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidInputEvent);
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidLogPacket);
 PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidSystemProperty);
 PERFETTO_PB_MSG_DECL(perfetto_protos_BatteryCounters);
@@ -39,6 +40,7 @@
 PERFETTO_PB_MSG_DECL(perfetto_protos_CpuInfo);
 PERFETTO_PB_MSG_DECL(perfetto_protos_DeobfuscationMapping);
 PERFETTO_PB_MSG_DECL(perfetto_protos_EntityStateResidency);
+PERFETTO_PB_MSG_DECL(perfetto_protos_EtwTraceEventBundle);
 PERFETTO_PB_MSG_DECL(perfetto_protos_ExtensionDescriptor);
 PERFETTO_PB_MSG_DECL(perfetto_protos_FrameTimelineEvent);
 PERFETTO_PB_MSG_DECL(perfetto_protos_FtraceEventBundle);
@@ -66,6 +68,11 @@
 PERFETTO_PB_MSG_DECL(perfetto_protos_ProcessTree);
 PERFETTO_PB_MSG_DECL(perfetto_protos_ProfilePacket);
 PERFETTO_PB_MSG_DECL(perfetto_protos_ProfiledFrameSymbols);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ProtoLogMessage);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ProtoLogViewerConfig);
+PERFETTO_PB_MSG_DECL(perfetto_protos_RemoteClockSync);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ShellHandlerMappings);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ShellTransition);
 PERFETTO_PB_MSG_DECL(perfetto_protos_SmapsPacket);
 PERFETTO_PB_MSG_DECL(perfetto_protos_StatsdAtom);
 PERFETTO_PB_MSG_DECL(perfetto_protos_StreamingAllocation);
@@ -87,6 +94,11 @@
 PERFETTO_PB_MSG_DECL(perfetto_protos_TranslationTable);
 PERFETTO_PB_MSG_DECL(perfetto_protos_Trigger);
 PERFETTO_PB_MSG_DECL(perfetto_protos_UiState);
+PERFETTO_PB_MSG_DECL(perfetto_protos_V8CodeMove);
+PERFETTO_PB_MSG_DECL(perfetto_protos_V8InternalCode);
+PERFETTO_PB_MSG_DECL(perfetto_protos_V8JsCode);
+PERFETTO_PB_MSG_DECL(perfetto_protos_V8RegExpCode);
+PERFETTO_PB_MSG_DECL(perfetto_protos_V8WasmCode);
 PERFETTO_PB_MSG_DECL(perfetto_protos_VulkanApiEvent);
 PERFETTO_PB_MSG_DECL(perfetto_protos_VulkanMemoryEvent);
 
@@ -423,6 +435,66 @@
                   94);
 PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
                   MSG,
+                  perfetto_protos_ShellTransition,
+                  shell_transition,
+                  96);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_ShellHandlerMappings,
+                  shell_handler_mappings,
+                  97);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_ProtoLogMessage,
+                  protolog_message,
+                  104);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_ProtoLogViewerConfig,
+                  protolog_viewer_config,
+                  105);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_EtwTraceEventBundle,
+                  etw_events,
+                  95);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_V8JsCode,
+                  v8_js_code,
+                  99);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_V8InternalCode,
+                  v8_internal_code,
+                  100);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_V8WasmCode,
+                  v8_wasm_code,
+                  101);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_V8RegExpCode,
+                  v8_reg_exp_code,
+                  102);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_V8CodeMove,
+                  v8_code_move,
+                  103);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_AndroidInputEvent,
+                  android_input_event,
+                  106);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
+                  perfetto_protos_RemoteClockSync,
+                  remote_clock_sync,
+                  107);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  MSG,
                   perfetto_protos_TestEvent,
                   for_testing,
                   900);
@@ -467,5 +539,10 @@
                   bool,
                   first_packet_on_sequence,
                   87);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+                  VARINT,
+                  uint32_t,
+                  machine_id,
+                  98);
 
 #endif  // INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACE_PACKET_PZC_H_
diff --git a/include/perfetto/public/protos/trace/track_event/track_event.pzc.h b/include/perfetto/public/protos/trace/track_event/track_event.pzc.h
index 5553376..2ee81f1 100644
--- a/include/perfetto/public/protos/trace/track_event/track_event.pzc.h
+++ b/include/perfetto/public/protos/trace/track_event/track_event.pzc.h
@@ -41,6 +41,7 @@
 PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeWindowHandleEventInfo);
 PERFETTO_PB_MSG_DECL(perfetto_protos_DebugAnnotation);
 PERFETTO_PB_MSG_DECL(perfetto_protos_LogMessage);
+PERFETTO_PB_MSG_DECL(perfetto_protos_Screenshot);
 PERFETTO_PB_MSG_DECL(perfetto_protos_SourceLocation);
 PERFETTO_PB_MSG_DECL(perfetto_protos_TaskExecution);
 PERFETTO_PB_MSG_DECL(perfetto_protos_TrackEvent_LegacyEvent);
@@ -246,6 +247,11 @@
                   49);
 PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
                   MSG,
+                  perfetto_protos_Screenshot,
+                  screenshot,
+                  50);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
                   perfetto_protos_SourceLocation,
                   source_location,
                   33);
diff --git a/infra/perfetto.dev/src/gen_stdlib_docs_md.py b/infra/perfetto.dev/src/gen_stdlib_docs_md.py
index 6fc5283..e1e60bd 100644
--- a/infra/perfetto.dev/src/gen_stdlib_docs_md.py
+++ b/infra/perfetto.dev/src/gen_stdlib_docs_md.py
@@ -22,216 +22,7 @@
 import json
 from typing import Any, List, Dict
 
-
-# Escapes special characters in a markdown table.
-def escape_in_table(desc: str):
-  return desc.replace('|', '\\|')
-
-
-# Responsible for module level markdown generation.
-class ModuleMd:
-
-  def __init__(self, module_name: str, module_files: List[Dict[str,
-                                                               Any]]) -> None:
-    self.module_name = module_name
-    self.files_md = sorted([
-        FileMd(module_name, file_dict) for file_dict in module_files
-    ], key=lambda x: x.import_key)
-    self.summary_objs = '\n'.join(
-        file.summary_objs for file in self.files_md if file.summary_objs)
-    self.summary_funs = '\n'.join(
-        file.summary_funs for file in self.files_md if file.summary_funs)
-    self.summary_view_funs = '\n'.join(file.summary_view_funs
-                                       for file in self.files_md
-                                       if file.summary_view_funs)
-    self.summary_macros = '\n'.join(
-        file.summary_macros for file in self.files_md if file.summary_macros)
-
-  def print_description(self):
-    if not self.files_md:
-      return ''
-
-    long_s = []
-    long_s.append(f'## Module: {self.module_name}')
-
-    if self.module_name == 'prelude':
-      # Prelude is a special module which is automatically imported and doesn't
-      # have any include keys.
-      objs = '\n'.join(obj for file in self.files_md for obj in file.objs)
-      if objs:
-        long_s.append('#### Views/Tables')
-        long_s.append(objs)
-      funs = '\n'.join(fun for file in self.files_md for fun in file.funs)
-      if funs:
-        long_s.append('#### Functions')
-        long_s.append(funs)
-      table_funs = '\n'.join(
-          view_fun for file in self.files_md for view_fun in file.view_funs)
-      if table_funs:
-        long_s.append('#### Table Functions')
-        long_s.append(table_funs)
-      macros = '\n'.join(
-          macro for file in self.files_md for macro in file.macros)
-      if macros:
-        long_s.append('#### Macros')
-        long_s.append(macros)
-      return '\n'.join(long_s)
-
-    for file in self.files_md:
-      if not any((file.objs, file.funs, file.view_funs, file.macros)):
-        continue
-
-      long_s.append(f'### {file.import_key}')
-      if file.objs:
-        long_s.append('#### Views/Tables')
-        long_s.append('\n'.join(file.objs))
-      if file.funs:
-        long_s.append('#### Functions')
-        long_s.append('\n'.join(file.funs))
-      if file.view_funs:
-        long_s.append('#### Table Functions')
-        long_s.append('\n'.join(file.view_funs))
-      if file.macros:
-        long_s.append('#### Macros')
-        long_s.append('\n'.join(file.macros))
-
-    return '\n'.join(long_s)
-
-
-# Responsible for file level markdown generation.
-class FileMd:
-
-  def __init__(self, module_name, file_dict):
-    self.import_key = file_dict['import_key']
-    import_key_name = self.import_key if module_name != 'prelude' else 'N/A'
-    self.objs, self.funs, self.view_funs, self.macros = [], [], [], []
-    summary_objs_list, summary_funs_list, summary_view_funs_list, summary_macros_list = [], [], [], []
-
-    # Add imports if in file.
-    for data in file_dict['imports']:
-      # Anchor
-      anchor = f'''obj/{module_name}/{data['name']}'''
-
-      # Add summary of imported view/table
-      summary_objs_list.append(f'''[{data['name']}](#{anchor})|'''
-                               f'''{import_key_name}|'''
-                               f'''{escape_in_table(data['summary_desc'])}''')
-
-      self.objs.append(f'''\n\n<a name="{anchor}"></a>'''
-                       f'''**{data['name']}**, {data['type']}\n\n'''
-                       f'''{escape_in_table(data['desc'])}\n''')
-
-      self.objs.append(
-          'Column | Type | Description\n------ | --- | -----------')
-      for name, info in data['cols'].items():
-        self.objs.append(
-            f'{name} | {info["type"]} | {escape_in_table(info["desc"])}')
-
-      self.objs.append('\n\n')
-
-    # Add functions if in file
-    for data in file_dict['functions']:
-      # Anchor
-      anchor = f'''fun/{module_name}/{data['name']}'''
-
-      # Add summary of imported function
-      summary_funs_list.append(f'''[{data['name']}](#{anchor})|'''
-                               f'''{import_key_name}|'''
-                               f'''{data['return_type']}|'''
-                               f'''{escape_in_table(data['summary_desc'])}''')
-      self.funs.append(
-          f'''\n\n<a name="{anchor}"></a>'''
-          f'''**{data['name']}**\n\n'''
-          f'''{data['desc']}\n\n'''
-          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
-      if data['args']:
-        self.funs.append('Argument | Type | Description\n'
-                         '-------- | ---- | -----------')
-        for name, arg_dict in data['args'].items():
-          self.funs.append(
-              f'''{name} | {arg_dict['type']} | {escape_in_table(arg_dict['desc'])}'''
-          )
-
-        self.funs.append('\n\n')
-
-    # Add table functions if in file
-    for data in file_dict['table_functions']:
-      # Anchor
-      anchor = rf'''view_fun/{module_name}/{data['name']}'''
-      # Add summary of imported view function
-      summary_view_funs_list.append(
-          f'''[{data['name']}](#{anchor})|'''
-          f'''{import_key_name}|'''
-          f'''{escape_in_table(data['summary_desc'])}''')
-
-      self.view_funs.append(f'''\n\n<a name="{anchor}"></a>'''
-                            f'''**{data['name']}**\n'''
-                            f'''{data['desc']}\n\n''')
-      if data['args']:
-        self.view_funs.append('Argument | Type | Description\n'
-                              '-------- | ---- | -----------')
-        for name, arg_dict in data['args'].items():
-          self.view_funs.append(
-              f'''{name} | {arg_dict['type']} | {escape_in_table(arg_dict['desc'])}'''
-          )
-        self.view_funs.append('\n')
-      self.view_funs.append('Column | Type | Description\n'
-                            '------ | -- | -----------')
-      for name, column in data['cols'].items():
-        self.view_funs.append(f'{name} | {column["type"]} | {column["desc"]}')
-
-      self.view_funs.append('\n\n')
-
-    # Add macros if in file
-    for data in file_dict['macros']:
-      # Anchor
-      anchor = rf'''macro/{module_name}/{data['name']}'''
-      # Add summary of imported view function
-      summary_macros_list.append(f'''[{data['name']}](#{anchor})|'''
-                                 f'''{import_key_name}|'''
-                                 f'''{escape_in_table(data['summary_desc'])}''')
-
-      self.macros.append(
-          f'''\n\n<a name="{anchor}"></a>'''
-          f'''**{data['name']}**\n'''
-          f'''{data['desc']}\n\n'''
-          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
-      if data['args']:
-        self.macros.append('Argument | Type | Description\n'
-                           '-------- | ---- | -----------')
-        for name, arg_dict in data['args'].items():
-          self.macros.append(
-              f'''{name} | {arg_dict['type']} | {escape_in_table(arg_dict['desc'])}'''
-          )
-        self.macros.append('\n')
-      self.macros.append('\n\n')
-
-    self.summary_objs = '\n'.join(summary_objs_list)
-    self.summary_funs = '\n'.join(summary_funs_list)
-    self.summary_view_funs = '\n'.join(summary_view_funs_list)
-    self.summary_macros = '\n'.join(summary_macros_list)
-
-
-def main():
-  parser = argparse.ArgumentParser()
-  parser.add_argument('--input', required=True)
-  parser.add_argument('--output', required=True)
-  args = parser.parse_args()
-
-  with open(args.input) as f:
-    modules_json_dict = json.load(f)
-
-  modules_dict: Dict[str, ModuleMd] = {}
-
-  for module_name, module_files in modules_json_dict.items():
-    # Remove 'common' when it has been removed from the code.
-    if module_name not in ['deprecated', 'common']:
-      modules_dict[module_name] = ModuleMd(module_name, module_files)
-
-  prelude_module = modules_dict.pop('prelude')
-
-  with open(args.output, 'w') as f:
-    f.write('''
+INTRODUCTION = '''
 # PerfettoSQL standard library
 *This page documents the PerfettoSQL standard library.*
 
@@ -271,71 +62,294 @@
 <!-- TODO(b/290185551): talk about experimental module and contributions. -->
 
 ## Summary
-''')
+'''
+
+
+def _escape_in_table(desc: str):
+  """Escapes special characters in a markdown table."""
+  return desc.replace('|', '\\|')
+
+
+def _md_table(cols: List[str]):
+  col_str = ' | '.join(cols) + '\n'
+  lines = ['-' * len(col) for col in cols]
+  underlines = ' | '.join(lines)
+  return col_str + underlines
+
+
+def _write_summary(sql_type: str, table_cols: List[str],
+                   summary_objs: List[str]) -> str:
+  table_data = '\n'.join(s.strip() for s in summary_objs if s)
+  return f"""
+### {sql_type}
+
+{_md_table(table_cols)}
+{table_data}
+
+"""
+
+
+class FileMd:
+  """Responsible for file level markdown generation."""
+
+  def __init__(self, module_name, file_dict):
+    self.import_key = file_dict['import_key']
+    import_key_name = self.import_key if module_name != 'prelude' else 'N/A'
+    self.objs, self.funs, self.view_funs, self.macros = [], [], [], []
+    summary_objs_list, summary_funs_list, summary_view_funs_list, summary_macros_list = [], [], [], []
+
+    # Add imports if in file.
+    for data in file_dict['imports']:
+      # Anchor
+      anchor = f'''obj/{module_name}/{data['name']}'''
+
+      # Add summary of imported view/table
+      summary_objs_list.append(f'''[{data['name']}](#{anchor})|'''
+                               f'''{import_key_name}|'''
+                               f'''{_escape_in_table(data['summary_desc'])}''')
+
+      self.objs.append(f'''\n\n<a name="{anchor}"></a>'''
+                       f'''**{data['name']}**, {data['type']}\n\n'''
+                       f'''{_escape_in_table(data['desc'])}\n''')
+
+      self.objs.append(_md_table(['Column', 'Type', 'Description']))
+      for name, info in data['cols'].items():
+        self.objs.append(
+            f'{name} | {info["type"]} | {_escape_in_table(info["desc"])}')
+
+      self.objs.append('\n\n')
+
+    # Add functions if in file
+    for data in file_dict['functions']:
+      # Anchor
+      anchor = f'''fun/{module_name}/{data['name']}'''
+
+      # Add summary of imported function
+      summary_funs_list.append(f'''[{data['name']}](#{anchor})|'''
+                               f'''{import_key_name}|'''
+                               f'''{data['return_type']}|'''
+                               f'''{_escape_in_table(data['summary_desc'])}''')
+      self.funs.append(
+          f'''\n\n<a name="{anchor}"></a>'''
+          f'''**{data['name']}**\n\n'''
+          f'''{data['desc']}\n\n'''
+          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
+      if data['args']:
+        self.funs.append(_md_table(['Argument', 'Type', 'Description']))
+        for name, arg_dict in data['args'].items():
+          self.funs.append(
+              f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}'''
+          )
+
+        self.funs.append('\n\n')
+
+    # Add table functions if in file
+    for data in file_dict['table_functions']:
+      # Anchor
+      anchor = rf'''view_fun/{module_name}/{data['name']}'''
+      # Add summary of imported view function
+      summary_view_funs_list.append(
+          f'''[{data['name']}](#{anchor})|'''
+          f'''{import_key_name}|'''
+          f'''{_escape_in_table(data['summary_desc'])}''')
+
+      self.view_funs.append(f'''\n\n<a name="{anchor}"></a>'''
+                            f'''**{data['name']}**\n'''
+                            f'''{data['desc']}\n\n''')
+      if data['args']:
+        self.funs.append(_md_table(['Argument', 'Type', 'Description']))
+        for name, arg_dict in data['args'].items():
+          self.view_funs.append(
+              f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}'''
+          )
+        self.view_funs.append('\n')
+        self.view_funs.append(_md_table(['Column', 'Type', 'Description']))
+      for name, column in data['cols'].items():
+        self.view_funs.append(f'{name} | {column["type"]} | {column["desc"]}')
+
+      self.view_funs.append('\n\n')
+
+    # Add macros if in file
+    for data in file_dict['macros']:
+      # Anchor
+      anchor = rf'''macro/{module_name}/{data['name']}'''
+      # Add summary of imported view function
+      summary_macros_list.append(
+          f'''[{data['name']}](#{anchor})|'''
+          f'''{import_key_name}|'''
+          f'''{_escape_in_table(data['summary_desc'])}''')
+
+      self.macros.append(
+          f'''\n\n<a name="{anchor}"></a>'''
+          f'''**{data['name']}**\n'''
+          f'''{data['desc']}\n\n'''
+          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
+      if data['args']:
+        self.macros.append(_md_table(['Argument', 'Type', 'Description']))
+        for name, arg_dict in data['args'].items():
+          self.macros.append(
+              f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}'''
+          )
+        self.macros.append('\n')
+      self.macros.append('\n\n')
+
+    self.summary_objs = '\n'.join(summary_objs_list)
+    self.summary_funs = '\n'.join(summary_funs_list)
+    self.summary_view_funs = '\n'.join(summary_view_funs_list)
+    self.summary_macros = '\n'.join(summary_macros_list)
+
+
+class ModuleMd:
+  """Responsible for module level markdown generation."""
+
+  def __init__(self, module_name: str, module_files: List[Dict[str,
+                                                               Any]]) -> None:
+    self.module_name = module_name
+    self.files_md = sorted(
+        [FileMd(module_name, file_dict) for file_dict in module_files],
+        key=lambda x: x.import_key)
+    self.summary_objs = '\n'.join(
+        file.summary_objs for file in self.files_md if file.summary_objs)
+    self.summary_funs = '\n'.join(
+        file.summary_funs for file in self.files_md if file.summary_funs)
+    self.summary_view_funs = '\n'.join(file.summary_view_funs
+                                       for file in self.files_md
+                                       if file.summary_view_funs)
+    self.summary_macros = '\n'.join(
+        file.summary_macros for file in self.files_md if file.summary_macros)
+
+  def get_prelude_description(self) -> str:
+    if not self.module_name == 'prelude':
+      raise ValueError("Only callable on prelude module")
+
+    lines = []
+    lines.append(f'## Module: {self.module_name}')
+
+    # Prelude is a special module which is automatically imported and doesn't
+    # have any include keys.
+    objs = '\n'.join(obj for file in self.files_md for obj in file.objs)
+    if objs:
+      lines.append('#### Views/Tables')
+      lines.append(objs)
+
+    funs = '\n'.join(fun for file in self.files_md for fun in file.funs)
+    if funs:
+      lines.append('#### Functions')
+      lines.append(funs)
+
+    table_funs = '\n'.join(
+        view_fun for file in self.files_md for view_fun in file.view_funs)
+    if table_funs:
+      lines.append('#### Table Functions')
+      lines.append(table_funs)
+
+    macros = '\n'.join(macro for file in self.files_md for macro in file.macros)
+    if macros:
+      lines.append('#### Macros')
+      lines.append(macros)
+
+    return '\n'.join(lines)
+
+  def get_description(self) -> str:
+    if not self.files_md:
+      return ''
+
+    if self.module_name == 'prelude':
+      raise ValueError("Can't be called with prelude module")
+
+    lines = []
+    lines.append(f'## Module: {self.module_name}')
+
+    for file in self.files_md:
+      if not any((file.objs, file.funs, file.view_funs, file.macros)):
+        continue
+
+      lines.append(f'### {file.import_key}')
+      if file.objs:
+        lines.append('#### Views/Tables')
+        lines.append('\n'.join(file.objs))
+      if file.funs:
+        lines.append('#### Functions')
+        lines.append('\n'.join(file.funs))
+      if file.view_funs:
+        lines.append('#### Table Functions')
+        lines.append('\n'.join(file.view_funs))
+      if file.macros:
+        lines.append('#### Macros')
+        lines.append('\n'.join(file.macros))
+
+    return '\n'.join(lines)
+
+
+def main():
+  parser = argparse.ArgumentParser()
+  parser.add_argument('--input', required=True)
+  parser.add_argument('--output', required=True)
+  args = parser.parse_args()
+
+  with open(args.input) as f:
+    modules_json_dict = json.load(f)
+
+  # Fetch the modules from json documentation.
+  modules_dict: Dict[str, ModuleMd] = {}
+  for module_name, module_files in modules_json_dict.items():
+    # Remove 'common' when it has been removed from the code.
+    if module_name not in ['deprecated', 'common']:
+      modules_dict[module_name] = ModuleMd(module_name, module_files)
+
+  prelude_module = modules_dict.pop('prelude')
+
+  with open(args.output, 'w') as f:
+    f.write(INTRODUCTION)
 
     summary_objs = [prelude_module.summary_objs
                    ] if prelude_module.summary_objs else []
     summary_objs += [
         module.summary_objs
-        for name, module in modules_dict.items()
-        if (module.summary_objs and name != 'experimental')
+        for module in modules_dict.values()
+        if (module.summary_objs)
     ]
 
     summary_funs = [prelude_module.summary_funs
                    ] if prelude_module.summary_funs else []
-    summary_funs += [
-        module.summary_funs
-        for name, module in modules_dict.items()
-        if (module.summary_funs and name != 'experimental')
-    ]
+    summary_funs += [module.summary_funs for module in modules_dict.values()]
     summary_view_funs = [prelude_module.summary_view_funs
                         ] if prelude_module.summary_view_funs else []
     summary_view_funs += [
-        module.summary_view_funs
-        for name, module in modules_dict.items()
-        if (module.summary_view_funs and name != 'experimental')
+        module.summary_view_funs for module in modules_dict.values()
     ]
     summary_macros = [prelude_module.summary_macros
                      ] if prelude_module.summary_macros else []
     summary_macros += [
-        module.summary_macros
-        for name, module in modules_dict.items()
-        if (module.summary_macros and name != 'experimental')
+        module.summary_macros for module in modules_dict.values()
     ]
 
     if summary_objs:
-      f.write('### Views/tables\n\n'
-              'Name | Import | Description\n'
-              '---- | ------ | -----------\n')
-      f.write('\n'.join(summary_objs))
-      f.write('\n')
+      f.write(
+          _write_summary('Views/tables', ['Name', 'Import', 'Description'],
+                         summary_objs))
 
     if summary_funs:
-      f.write('### Functions\n\n'
-              'Name | Import | Return type | Description\n'
-              '---- | ------ | ----------- | -----------\n')
-      f.write('\n'.join(summary_funs))
-      f.write('\n')
+      f.write(
+          _write_summary('Functions',
+                         ['Name', 'Import', 'Return type', 'Description'],
+                         summary_funs))
 
     if summary_view_funs:
-      f.write('### Table Functions\n\n'
-              'Name | Import |  Description\n'
-              '---- | ------ |  -----------\n')
-      f.write('\n'.join(summary_view_funs))
-      f.write('\n')
+      f.write(
+          _write_summary('Table functions', ['Name', 'Import', 'Description'],
+                         summary_view_funs))
 
     if summary_macros:
-      f.write('### Macros\n\n'
-              'Name | Import |  Description\n'
-              '---- | ------ |  -----------\n')
-      f.write('\n'.join(summary_macros))
-      f.write('\n')
+      f.write(
+          _write_summary('Macros', ['Name', 'Import', 'Description'],
+                         summary_macros))
 
     f.write('\n\n')
-    f.write(prelude_module.print_description())
+    f.write(prelude_module.get_prelude_description())
     f.write('\n')
     f.write('\n'.join(
-        module.print_description() for module in modules_dict.values()))
+        module.get_description() for module in modules_dict.values()))
 
   return 0
 
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index def2a72..5c0cc3e 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -606,5 +606,6 @@
     SchedSwitchWithCtrsFtraceEvent sched_switch_with_ctrs = 487;
     GpuWorkPeriodFtraceEvent gpu_work_period = 488;
     RpmStatusFtraceEvent rpm_status = 489;
+    PanelWriteGenericFtraceEvent panel_write_generic = 490;
   }
 }
diff --git a/protos/perfetto/trace/ftrace/panel.proto b/protos/perfetto/trace/ftrace/panel.proto
index 5b84b3b..fde72d9 100644
--- a/protos/perfetto/trace/ftrace/panel.proto
+++ b/protos/perfetto/trace/ftrace/panel.proto
@@ -18,3 +18,11 @@
   optional uint32 tx_buf = 2;
   optional uint32 type = 3;
 }
+message PanelWriteGenericFtraceEvent {
+  optional int32 pid = 1;
+  optional string trace_name = 2;
+  optional uint32 trace_begin = 3;
+  optional string name = 4;
+  optional uint32 type = 5;
+  optional int32 value = 6;
+}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index b657572..97962a7 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -9256,6 +9256,14 @@
   optional uint32 tx_buf = 2;
   optional uint32 type = 3;
 }
+message PanelWriteGenericFtraceEvent {
+  optional int32 pid = 1;
+  optional string trace_name = 2;
+  optional uint32 trace_begin = 3;
+  optional string name = 4;
+  optional uint32 type = 5;
+  optional int32 value = 6;
+}
 
 // End of protos/perfetto/trace/ftrace/panel.proto
 
@@ -10584,6 +10592,7 @@
     SchedSwitchWithCtrsFtraceEvent sched_switch_with_ctrs = 487;
     GpuWorkPeriodFtraceEvent gpu_work_period = 488;
     RpmStatusFtraceEvent rpm_status = 489;
+    PanelWriteGenericFtraceEvent panel_write_generic = 490;
   }
 }
 
diff --git a/python/generators/sql_processing/docs_parse.py b/python/generators/sql_processing/docs_parse.py
index 6e8cd60..9062973 100644
--- a/python/generators/sql_processing/docs_parse.py
+++ b/python/generators/sql_processing/docs_parse.py
@@ -17,7 +17,7 @@
 from dataclasses import dataclass
 import re
 import sys
-from typing import Any, Dict, List, Optional, Set, Tuple, NamedTuple
+from typing import Dict, List, Optional, Set, NamedTuple
 
 from python.generators.sql_processing.docs_extractor import DocsExtractor
 from python.generators.sql_processing.utils import ObjKind
@@ -31,30 +31,21 @@
 from python.generators.sql_processing.utils import ARG_ANNOTATION_PATTERN
 
 
-def is_internal(name: str) -> bool:
+def _is_internal(name: str) -> bool:
   return re.match(r'^_.*', name, re.IGNORECASE) is not None
 
 
-def is_snake_case(s: str) -> bool:
-  """Returns true if the string is snake_case."""
+def _is_snake_case(s: str) -> bool:
   return re.fullmatch(r'^[a-z_0-9]*$', s) is not None
 
 
-# Parse a SQL comment (i.e. -- Foo\n -- bar.) into a string (i.e. "Foo bar.").
 def parse_comment(comment: str) -> str:
+  """Parse a SQL comment (i.e. -- Foo\n -- bar.) into a string (i.e. "Foo bar.")."""
   return ' '.join(line.strip().lstrip('--').lstrip()
                   for line in comment.strip().split('\n'))
 
-
-class Arg(NamedTuple):
-  # TODO(b/307926059): the type is missing on old-style documentation for
-  # tables. Make it "str" after stdlib is migrated.
-  type: Optional[str]
-  description: str
-
-
-# Returns: error message if the name is not correct, None otherwise.
 def get_module_prefix_error(name: str, path: str, module: str) -> Optional[str]:
+  """Returns error message if the name is not correct, None otherwise."""
   prefix = name.lower().split('_')[0]
   if module in ["common", "prelude", "deprecated"]:
     if prefix == module:
@@ -77,6 +68,13 @@
       f'with one of following names: {", ".join(allowed_prefixes)}')
 
 
+class Arg(NamedTuple):
+  # TODO(b/307926059): the type is missing on old-style documentation for
+  # tables. Make it "str" after stdlib is migrated.
+  type: Optional[str]
+  description: str
+
+
 class AbstractDocParser(ABC):
 
   @dataclass
@@ -244,7 +242,7 @@
           f'{type} "{self.name}": CREATE OR REPLACE is not allowed in stdlib '
           f'as standard library modules can only included once. Please just '
           f'use CREATE instead.')
-    if is_internal(self.name):
+    if _is_internal(self.name):
       return None
 
     is_perfetto_table_or_view = (
@@ -294,12 +292,12 @@
           f'use CREATE instead.')
 
     # Ignore internal functions.
-    if is_internal(self.name):
+    if _is_internal(self.name):
       return None
 
     name = self._parse_name()
 
-    if not is_snake_case(name):
+    if not _is_snake_case(name):
       self._error(f'Function name "{name}" is not snake_case'
                   f' (should be {name.casefold()})')
 
@@ -345,14 +343,14 @@
           f'use CREATE instead.')
 
     # Ignore internal functions.
-    if is_internal(self.name):
+    if _is_internal(self.name):
       return None
 
     self._validate_only_contains_annotations(doc.annotations,
                                              {'@arg', '@column'})
     name = self._parse_name()
 
-    if not is_snake_case(name):
+    if not _is_snake_case(name):
       self._error(f'Function name "{name}" is not snake_case'
                   f' (should be "{name.casefold()}")')
 
@@ -396,13 +394,13 @@
           f'use CREATE instead.')
 
     # Ignore internal macros.
-    if is_internal(self.name):
+    if _is_internal(self.name):
       return None
 
     self._validate_only_contains_annotations(doc.annotations, set())
     name = self._parse_name()
 
-    if not is_snake_case(name):
+    if not _is_snake_case(name):
       self._error(f'Macro name "{name}" is not snake_case'
                   f' (should be "{name.casefold()}")')
 
@@ -416,6 +414,7 @@
 
 
 class ParsedFile:
+  """Data class containing all of the docmentation of single SQL file"""
   errors: List[str] = []
   table_views: List[TableOrView] = []
   functions: List[Function] = []
@@ -432,9 +431,9 @@
     self.macros = macros
 
 
-# Reads the provided SQL and, if possible, generates a dictionary with data
-# from documentation together with errors from validation of the schema.
 def parse_file(path: str, sql: str) -> Optional[ParsedFile]:
+  """Reads the provided SQL and, if possible, generates a dictionary with data
+    from documentation together with errors from validation of the schema."""
   if sys.platform.startswith('win'):
     path = path.replace('\\', '/')
 
diff --git a/src/protozero/BUILD.gn b/src/protozero/BUILD.gn
index 4d940bc..d1285bb 100644
--- a/src/protozero/BUILD.gn
+++ b/src/protozero/BUILD.gn
@@ -61,6 +61,12 @@
     ":protozero",
     ":testing_messages_cpp",
     ":testing_messages_lite",
+    ":testing_messages_other_package_cpp",
+    ":testing_messages_other_package_lite",
+    ":testing_messages_other_package_zero",
+    ":testing_messages_subpackage_cpp",
+    ":testing_messages_subpackage_lite",
+    ":testing_messages_subpackage_zero",
     ":testing_messages_zero",
     "../../gn:default_deps",
     "../../gn:gtest_and_gmock",
@@ -87,6 +93,10 @@
 # Generates both xxx.pbzero.h and xxx.pb.h (official proto).
 
 perfetto_proto_library("testing_messages_@TYPE@") {
+  deps = [
+    ":testing_messages_other_package_@TYPE@",
+    ":testing_messages_subpackage_@TYPE@",
+  ]
   sources = [
     "test/example_proto/extensions.proto",
     "test/example_proto/library.proto",
@@ -97,6 +107,16 @@
   proto_path = perfetto_root_path
 }
 
+perfetto_proto_library("testing_messages_other_package_@TYPE@") {
+  sources = [ "test/example_proto/other_package/test_messages.proto" ]
+  proto_path = perfetto_root_path
+}
+
+perfetto_proto_library("testing_messages_subpackage_@TYPE@") {
+  sources = [ "test/example_proto/subpackage/test_messages.proto" ]
+  proto_path = perfetto_root_path
+}
+
 perfetto_proto_library("test_messages_descriptor") {
   proto_generators = [ "descriptor" ]
   generate_descriptor = "test_messages.descriptor"
diff --git a/src/protozero/protoc_plugin/cppgen_plugin.cc b/src/protozero/protoc_plugin/cppgen_plugin.cc
index f134caa..4257613 100644
--- a/src/protozero/protoc_plugin/cppgen_plugin.cc
+++ b/src/protozero/protoc_plugin/cppgen_plugin.cc
@@ -95,7 +95,13 @@
     return full_type;
   }
 
+  template <class T>
+  bool HasSamePackage(const T* descriptor) const {
+    return descriptor->file()->package() == package_;
+  }
+
   mutable std::string wrapper_namespace_;
+  mutable std::string package_;
 };
 
 CppObjGenerator::CppObjGenerator() = default;
@@ -116,6 +122,8 @@
     }
   }
 
+  package_ = file->package();
+
   auto get_file_name = [](const FileDescriptor* proto) {
     return StripSuffix(proto->name(), ".proto") + ".gen";
   };
@@ -372,10 +380,16 @@
       return constref ? "const std::string&" : "std::string";
     case FieldDescriptor::TYPE_MESSAGE:
       assert(!field->options().lazy());
-      return constref ? "const " + GetFullName(field->message_type()) + "&"
-                      : GetFullName(field->message_type());
+      return constref
+                 ? "const " +
+                       GetFullName(field->message_type(),
+                                   !HasSamePackage(field->message_type())) +
+                       "&"
+                 : GetFullName(field->message_type(),
+                               !HasSamePackage(field->message_type()));
     case FieldDescriptor::TYPE_ENUM:
-      return GetFullName(field->enum_type());
+      return GetFullName(field->enum_type(),
+                         !HasSamePackage(field->enum_type()));
     case FieldDescriptor::TYPE_GROUP:
       abort();
   }
diff --git a/src/protozero/protoc_plugin/protozero_c_plugin.cc b/src/protozero/protoc_plugin/protozero_c_plugin.cc
index 27fe24f..5bd05dd 100644
--- a/src/protozero/protoc_plugin/protozero_c_plugin.cc
+++ b/src/protozero/protoc_plugin/protozero_c_plugin.cc
@@ -127,24 +127,12 @@
       error_ = reason;
   }
 
-  // Get full name (including outer descriptors) of proto descriptor.
-  template <class T>
-  inline std::string GetDescriptorName(const T* descriptor) {
-    if (!package_.empty()) {
-      return StripPrefix(descriptor->full_name(), package_ + ".");
-    }
-    return descriptor->full_name();
-  }
-
   // Get C++ class name corresponding to proto descriptor.
   // Nested names are splitted by underscores. Underscores in type names aren't
   // prohibited but not recommended in order to avoid name collisions.
   template <class T>
-  inline std::string GetCppClassName(const T* descriptor, bool full = false) {
-    std::string name = StripChars(GetDescriptorName(descriptor), ".", '_');
-    if (full)
-      name = full_namespace_prefix_ + "_" + name;
-    return name;
+  inline std::string GetCppClassName(const T* descriptor) {
+    return StripChars(descriptor->full_name(), ".", '_');
   }
 
   const char* FieldTypeToPackedBufferType(FieldDescriptor::Type type) {
@@ -213,7 +201,7 @@
       case FieldDescriptor::TYPE_DOUBLE:
         return "double";
       case FieldDescriptor::TYPE_ENUM:
-        return "enum " + GetCppClassName(field->enum_type(), true);
+        return "enum " + GetCppClassName(field->enum_type());
       case FieldDescriptor::TYPE_STRING:
       case FieldDescriptor::TYPE_BYTES:
         return "const char*";
@@ -305,10 +293,6 @@
     while (!stack.empty()) {
       const FileDescriptor* imp = stack.back();
       stack.pop_back();
-      // Having imports under different packages leads to unnecessary
-      // complexity with namespaces.
-      if (imp->package() != package_)
-        Abort("Imported proto must be in the same package.");
 
       for (int i = 0; i < imp->public_dependency_count(); ++i) {
         stack.push_back(imp->public_dependency(i));
@@ -427,7 +411,7 @@
     // Print forward declarations.
     for (const Descriptor* message : referenced_messages_) {
       stub_h_->Print("PERFETTO_PB_MSG_DECL($class$);\n", "class",
-                     GetCppClassName(message, true));
+                     GetCppClassName(message));
     }
 
     stub_h_->Print("\n");
@@ -436,11 +420,11 @@
   void GenerateEnumDescriptor(const EnumDescriptor* enumeration) {
     if (enumeration->containing_type()) {
       stub_h_->Print("PERFETTO_PB_ENUM_IN_MSG($msg$, $class$){\n", "msg",
-                     GetCppClassName(enumeration->containing_type(), true),
-                     "class", enumeration->name());
+                     GetCppClassName(enumeration->containing_type()), "class",
+                     enumeration->name());
     } else {
       stub_h_->Print("PERFETTO_PB_ENUM($class$){\n", "class",
-                     GetCppClassName(enumeration, true));
+                     GetCppClassName(enumeration));
     }
     stub_h_->Indent();
 
@@ -451,8 +435,8 @@
       if (enumeration->containing_type()) {
         stub_h_->Print(
             "PERFETTO_PB_ENUM_IN_MSG_ENTRY($msg$, $val$) = $number$,\n", "msg",
-            GetCppClassName(enumeration->containing_type(), true), "val",
-            value_name, "number", std::to_string(value->number()));
+            GetCppClassName(enumeration->containing_type()), "val", value_name,
+            "number", std::to_string(value->number()));
       } else {
         stub_h_->Print("PERFETTO_PB_ENUM_ENTRY($val$) = $number$, \n", "val",
                        full_namespace_prefix_ + "_" + value_name, "number",
@@ -532,8 +516,7 @@
 
   void GenerateNestedMessageFieldDescriptor(const std::string& message_cpp_type,
                                             const FieldDescriptor* field) {
-    std::string inner_class =
-        full_namespace_prefix_ + "_" + GetCppClassName(field->message_type());
+    std::string inner_class = GetCppClassName(field->message_type());
     stub_h_->Print(
         "PERFETTO_PB_FIELD($class$, MSG, $inner_class$, $name$, $id$);\n",
         "class", message_cpp_type, "id", std::to_string(field->number()),
@@ -542,12 +525,11 @@
 
   void GenerateMessageDescriptor(const Descriptor* message) {
     stub_h_->Print("PERFETTO_PB_MSG($name$);\n", "name",
-                   GetCppClassName(message, true));
+                   GetCppClassName(message));
 
     // Field descriptors.
     for (int i = 0; i < message->field_count(); ++i) {
-      GenerateFieldDescriptor(GetCppClassName(message, true),
-                              message->field(i));
+      GenerateFieldDescriptor(GetCppClassName(message), message->field(i));
     }
     stub_h_->Print("\n");
   }
diff --git a/src/protozero/protoc_plugin/protozero_plugin.cc b/src/protozero/protoc_plugin/protozero_plugin.cc
index 7d46a50..5b08292 100644
--- a/src/protozero/protoc_plugin/protozero_plugin.cc
+++ b/src/protozero/protoc_plugin/protozero_plugin.cc
@@ -42,6 +42,7 @@
 using google::protobuf::compiler::GeneratorContext;
 using google::protobuf::io::Printer;
 using google::protobuf::io::ZeroCopyOutputStream;
+using perfetto::base::ReplaceAll;
 using perfetto::base::SplitString;
 using perfetto::base::StripChars;
 using perfetto::base::StripPrefix;
@@ -126,14 +127,9 @@
       error_ = reason;
   }
 
-  // Get full name (including outer descriptors) of proto descriptor.
   template <class T>
-  inline std::string GetDescriptorName(const T* descriptor) {
-    if (!package_.empty()) {
-      return StripPrefix(descriptor->full_name(), package_ + ".");
-    } else {
-      return descriptor->full_name();
-    }
+  bool HasSamePackage(const T* descriptor) const {
+    return descriptor->file()->package() == package_;
   }
 
   // Get C++ class name corresponding to proto descriptor.
@@ -141,9 +137,28 @@
   // prohibited but not recommended in order to avoid name collisions.
   template <class T>
   inline std::string GetCppClassName(const T* descriptor, bool full = false) {
-    std::string name = StripChars(GetDescriptorName(descriptor), ".", '_');
-    if (full)
-      name = full_namespace_prefix_ + name;
+    std::string package = descriptor->file()->package();
+    std::string name = StripPrefix(descriptor->full_name(), package + ".");
+    name = StripChars(name, ".", '_');
+
+    if (full && !package.empty()) {
+      auto get_full_namespace = [&]() {
+        std::vector<std::string> namespaces = SplitString(package, ".");
+        if (!wrapper_namespace_.empty())
+          namespaces.push_back(wrapper_namespace_);
+
+        std::string result = "";
+        for (const std::string& ns : namespaces) {
+          result += "::";
+          result += ns;
+        }
+        return result;
+      };
+
+      std::string namespaces = ReplaceAll(package, ".", "::");
+      name = get_full_namespace() + "::" + name;
+    }
+
     return name;
   }
 
@@ -305,12 +320,14 @@
       case FieldDescriptor::TYPE_DOUBLE:
         return "double";
       case FieldDescriptor::TYPE_ENUM:
-        return GetCppClassName(field->enum_type(), true);
+        return GetCppClassName(field->enum_type(),
+                               !HasSamePackage(field->enum_type()));
       case FieldDescriptor::TYPE_STRING:
       case FieldDescriptor::TYPE_BYTES:
         return "std::string";
       case FieldDescriptor::TYPE_MESSAGE:
-        return GetCppClassName(field->message_type());
+        return GetCppClassName(field->message_type(),
+                               !HasSamePackage(field->message_type()));
       case FieldDescriptor::TYPE_GROUP:
         Abort("Groups not supported.");
         return "";
@@ -405,11 +422,6 @@
     while (!stack.empty()) {
       const FileDescriptor* import = stack.back();
       stack.pop_back();
-      // Having imports under different packages leads to unnecessary
-      // complexity with namespaces.
-      if (import->package() != package_)
-        Abort("Imported proto must be in the same package.");
-
       for (int i = 0; i < import->public_dependency_count(); ++i) {
         stack.push_back(import->public_dependency(i));
       }
@@ -504,32 +516,70 @@
     }
     stub_h_->Print("\n");
 
+    PrintForwardDeclarations();
+
     // Print namespaces.
     for (const std::string& ns : namespaces_) {
       stub_h_->Print("namespace $ns$ {\n", "ns", ns);
     }
     stub_h_->Print("\n");
+  }
 
-    // Print forward declarations.
+  void PrintForwardDeclarations() {
+    struct Descriptors {
+      std::vector<const Descriptor*> messages_;
+      std::vector<const EnumDescriptor*> enums_;
+    };
+    std::map<std::string, Descriptors> package_to_descriptors;
+
     for (const Descriptor* message : referenced_messages_) {
-      stub_h_->Print("class $class$;\n", "class", GetCppClassName(message));
+      package_to_descriptors[message->file()->package()].messages_.push_back(
+          message);
     }
-    for (const EnumDescriptor* enumeration : referenced_enums_) {
-      if (enumeration->containing_type()) {
-        stub_h_->Print("namespace $namespace_name$ {\n", "namespace_name",
-                       GetNamespaceNameForInnerEnum(enumeration));
-      }
-      stub_h_->Print("enum $class$ : int32_t;\n", "class", enumeration->name());
 
-      if (enumeration->containing_type()) {
-        stub_h_->Print("}  // namespace $namespace_name$\n", "namespace_name",
-                       GetNamespaceNameForInnerEnum(enumeration));
-        stub_h_->Print("using $alias$ = $namespace_name$::$short_name$;\n",
-                       "alias", GetCppClassName(enumeration), "namespace_name",
-                       GetNamespaceNameForInnerEnum(enumeration), "short_name",
+    for (const EnumDescriptor* enumeration : referenced_enums_) {
+      package_to_descriptors[enumeration->file()->package()].enums_.push_back(
+          enumeration);
+    }
+
+    for (const auto& [package, descriptors] : package_to_descriptors) {
+      std::vector<std::string> namespaces = SplitString(package, ".");
+      namespaces.push_back(wrapper_namespace_);
+
+      // open namespaces
+      for (const auto& ns : namespaces) {
+        stub_h_->Print("namespace $ns$ {\n", "ns", ns);
+      }
+
+      for (const Descriptor* message : descriptors.messages_) {
+        stub_h_->Print("class $class$;\n", "class", GetCppClassName(message));
+      }
+
+      for (const EnumDescriptor* enumeration : descriptors.enums_) {
+        if (enumeration->containing_type()) {
+          stub_h_->Print("namespace $namespace_name$ {\n", "namespace_name",
+                         GetNamespaceNameForInnerEnum(enumeration));
+        }
+        stub_h_->Print("enum $class$ : int32_t;\n", "class",
                        enumeration->name());
+
+        if (enumeration->containing_type()) {
+          stub_h_->Print("}  // namespace $namespace_name$\n", "namespace_name",
+                         GetNamespaceNameForInnerEnum(enumeration));
+          stub_h_->Print("using $alias$ = $namespace_name$::$short_name$;\n",
+                         "alias", GetCppClassName(enumeration),
+                         "namespace_name",
+                         GetNamespaceNameForInnerEnum(enumeration),
+                         "short_name", enumeration->name());
+        }
+      }
+
+      // close namespaces
+      for (auto it = namespaces.crbegin(); it != namespaces.crend(); ++it) {
+        stub_h_->Print("} // Namespace $ns$.\n", "ns", *it);
       }
     }
+
     stub_h_->Print("\n");
   }
 
@@ -675,7 +725,8 @@
 
   void GenerateNestedMessageFieldDescriptor(const FieldDescriptor* field) {
     std::string action = field->is_repeated() ? "add" : "set";
-    std::string inner_class = GetCppClassName(field->message_type());
+    std::string inner_class = GetCppClassName(
+        field->message_type(), !HasSamePackage(field->message_type()));
     stub_h_->Print(
         "template <typename T = $inner_class$> T* $action$_$name$() {\n"
         "  return BeginNestedMessage<T>($id$);\n"
diff --git a/src/protozero/test/cppgen_conformance_unittest.cc b/src/protozero/test/cppgen_conformance_unittest.cc
index 5ac43aa..d8d284e 100644
--- a/src/protozero/test/cppgen_conformance_unittest.cc
+++ b/src/protozero/test/cppgen_conformance_unittest.cc
@@ -24,14 +24,20 @@
 
 // Autogenerated headers in out/*/gen/
 #include "src/protozero/test/example_proto/library.gen.h"
+#include "src/protozero/test/example_proto/other_package/test_messages.gen.h"
+#include "src/protozero/test/example_proto/subpackage/test_messages.gen.h"
 #include "src/protozero/test/example_proto/test_messages.gen.h"
 #include "src/protozero/test/example_proto/test_messages.pb.h"
 
 // Generated by the cppgen compiler.
 namespace pbtest = protozero::test::protos::gen;
+namespace pbtest_subpackage = protozero::test::protos::subpackage::gen;
+namespace pbtest_otherpackage = other_package::gen;
 
 // Generated by the official protobuf compiler.
 namespace pbgold = protozero::test::protos;
+namespace pbgold_subpackage = protozero::test::protos::subpackage;
+namespace pbgold_other_package = other_package;
 
 namespace protozero {
 namespace {
@@ -339,5 +345,57 @@
     ASSERT_THAT(msg.field_sfixed64(), ElementsAreArray(exp_sfixed64));
   }
 }
+
+TEST(ProtoCppConformanceTest, DifferentPackages) {
+  pbtest::DifferentPackages msg;
+
+  // Pupulate fields defined in "protozero.test.protos.subpackage"
+  pbtest_subpackage::Message* msgSubpackage = msg.mutable_subpackage_message();
+  msgSubpackage->set_field_int32(1);
+  msgSubpackage->set_field_enum(pbtest_subpackage::Enum::A);
+  msgSubpackage->set_field_nested_enum(pbtest_subpackage::Message_NestedEnum_C);
+  msg.mutable_subpackage_nested_message()->set_field_int32(2);
+  msg.set_subpackage_enum(pbtest_subpackage::Enum::B);
+  msg.set_subpackage_nested_enum(pbtest_subpackage::Message_NestedEnum_D);
+
+  // Pupulate fields defined in "other_package"
+  pbtest_otherpackage::Message* msgOtherPackage =
+      msg.mutable_otherpackage_message();
+  msgOtherPackage->set_field_int32(11);
+  msgOtherPackage->set_field_enum(pbtest_otherpackage::Enum::A);
+  msgOtherPackage->set_field_nested_enum(
+      pbtest_otherpackage::Message_NestedEnum_C);
+  msg.mutable_otherpackage_nested_message()->set_field_int32(12);
+  msg.set_otherpackage_enum(pbtest_otherpackage::Enum::B);
+  msg.set_otherpackage_nested_enum(pbtest_otherpackage::Message_NestedEnum_D);
+
+  // Deserialize into golden proto
+  std::string serialized = msg.SerializeAsString();
+  pbgold::DifferentPackages gold_msg;
+  gold_msg.ParseFromString(serialized);
+  EXPECT_EQ(serialized.size(), static_cast<size_t>(gold_msg.ByteSizeLong()));
+
+  // Check fields defined in "protozero.test.protos.subpackage"
+  EXPECT_EQ(1, gold_msg.subpackage_message().field_int32());
+  EXPECT_EQ(pbgold_subpackage::Enum::A,
+            gold_msg.subpackage_message().field_enum());
+  EXPECT_EQ(pbgold_subpackage::Message_NestedEnum_C,
+            gold_msg.subpackage_message().field_nested_enum());
+  EXPECT_EQ(2, gold_msg.subpackage_nested_message().field_int32());
+  EXPECT_EQ(pbgold_subpackage::Enum::B, gold_msg.subpackage_enum());
+  EXPECT_EQ(pbgold_subpackage::Message_NestedEnum_D,
+            gold_msg.subpackage_nested_enum());
+
+  // Check fields defined in "other_package"
+  EXPECT_EQ(11, gold_msg.otherpackage_message().field_int32());
+  EXPECT_EQ(pbgold_other_package::Enum::A,
+            gold_msg.otherpackage_message().field_enum());
+  EXPECT_EQ(pbgold_other_package::Message_NestedEnum_C,
+            gold_msg.otherpackage_message().field_nested_enum());
+  EXPECT_EQ(12, gold_msg.otherpackage_nested_message().field_int32());
+  EXPECT_EQ(pbgold_other_package::Enum::B, gold_msg.otherpackage_enum());
+  EXPECT_EQ(pbgold_other_package::Message_NestedEnum_D,
+            gold_msg.otherpackage_nested_enum());
+}
 }  // namespace
 }  // namespace protozero
diff --git a/src/protozero/test/example_proto/other_package/test_messages.proto b/src/protozero/test/example_proto/other_package/test_messages.proto
new file mode 100644
index 0000000..d299082
--- /dev/null
+++ b/src/protozero/test/example_proto/other_package/test_messages.proto
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package other_package;
+
+enum Enum {
+  A = 10;
+  B = 11;
+}
+
+message Message {
+  message NestedMessage {
+    optional int32 field_int32 = 1;
+  }
+
+  enum NestedEnum {
+    C = 12;
+    D = 13;
+  }
+
+  optional int32 field_int32 = 1;
+  optional Enum field_enum = 2;
+  optional NestedEnum field_nested_enum = 3;
+  optional NestedMessage field_nested_message = 4;
+}
diff --git a/src/protozero/test/example_proto/subpackage/test_messages.proto b/src/protozero/test/example_proto/subpackage/test_messages.proto
new file mode 100644
index 0000000..6f7c0cc
--- /dev/null
+++ b/src/protozero/test/example_proto/subpackage/test_messages.proto
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package protozero.test.protos.subpackage;
+
+enum Enum {
+  A = 1;
+  B = 2;
+}
+
+message Message {
+  message NestedMessage {
+    optional int32 field_int32 = 1;
+  }
+
+  enum NestedEnum {
+    C = 3;
+    D = 4;
+  }
+
+  optional int32 field_int32 = 1;
+  optional Enum field_enum = 2;
+  optional NestedEnum field_nested_enum = 3;
+  optional NestedMessage field_nested_message = 4;
+}
diff --git a/src/protozero/test/example_proto/test_messages.proto b/src/protozero/test/example_proto/test_messages.proto
index e6ece33..b54d883 100644
--- a/src/protozero/test/example_proto/test_messages.proto
+++ b/src/protozero/test/example_proto/test_messages.proto
@@ -19,6 +19,8 @@
 package protozero.test.protos;
 
 import "src/protozero/test/example_proto/library.proto";
+import "src/protozero/test/example_proto/other_package/test_messages.proto";
+import "src/protozero/test/example_proto/subpackage/test_messages.proto";
 
 // This file contains comprehensive set of supported message structures and
 // data types. Unit tests depends on the plugin-processed version of this file.
@@ -179,3 +181,15 @@
   repeated Sub2_V2 sub2_rep = 12;
   optional Sub2_V2 sub2_lazy = 13 [lazy = true];
 }
+
+message DifferentPackages {
+  optional subpackage.Message subpackage_message = 1;
+  optional subpackage.Message.NestedMessage subpackage_nested_message = 2;
+  optional subpackage.Enum subpackage_enum = 3;
+  optional subpackage.Message.NestedEnum subpackage_nested_enum = 4;
+
+  optional .other_package.Message otherpackage_message = 5;
+  optional .other_package.Message.NestedMessage otherpackage_nested_message = 6;
+  optional .other_package.Enum otherpackage_enum = 7;
+  optional .other_package.Message.NestedEnum otherpackage_nested_enum = 8;
+}
diff --git a/src/protozero/test/protozero_conformance_unittest.cc b/src/protozero/test/protozero_conformance_unittest.cc
index 74e2204..3a4b7cf 100644
--- a/src/protozero/test/protozero_conformance_unittest.cc
+++ b/src/protozero/test/protozero_conformance_unittest.cc
@@ -27,14 +27,20 @@
 #include "src/protozero/test/example_proto/extensions.pb.h"
 #include "src/protozero/test/example_proto/extensions.pbzero.h"
 #include "src/protozero/test/example_proto/library.pbzero.h"
+#include "src/protozero/test/example_proto/other_package/test_messages.pbzero.h"
+#include "src/protozero/test/example_proto/subpackage/test_messages.pbzero.h"
 #include "src/protozero/test/example_proto/test_messages.pb.h"
 #include "src/protozero/test/example_proto/test_messages.pbzero.h"
 
 // Generated by the protozero plugin.
 namespace pbtest = protozero::test::protos::pbzero;
+namespace pbtest_subpackage = protozero::test::protos::subpackage::pbzero;
+namespace pbtest_otherpackage = other_package::pbzero;
 
 // Generated by the official protobuf compiler.
 namespace pbgold = protozero::test::protos;
+namespace pbgold_subpackage = protozero::test::protos::subpackage;
+namespace pbgold_other_package = other_package;
 
 namespace protozero {
 namespace {
@@ -310,5 +316,58 @@
                "PING");
 }
 
+TEST(ProtoZeroConformanceTest, DifferentPackages) {
+  HeapBuffered<pbtest::DifferentPackages> msg{kChunkSize, kChunkSize};
+
+  // Pupulate fields defined in "protozero.test.protos.subpackage"
+  pbtest_subpackage::Message* msgSubpackage = msg->set_subpackage_message();
+  msgSubpackage->set_field_int32(1);
+  msgSubpackage->set_field_enum(pbtest_subpackage::Enum::A);
+  msgSubpackage->set_field_nested_enum(
+      pbtest_subpackage::Message::NestedEnum::C);
+  msg->set_subpackage_nested_message()->set_field_int32(2);
+  msg->set_subpackage_enum(pbtest_subpackage::Enum::B);
+  msg->set_subpackage_nested_enum(pbtest_subpackage::Message_NestedEnum::D);
+
+  // Pupulate fields defined in "other_package"
+  pbtest_otherpackage::Message* msgOtherPackage =
+      msg->set_otherpackage_message();
+  msgOtherPackage->set_field_int32(11);
+  msgOtherPackage->set_field_enum(pbtest_otherpackage::Enum::A);
+  msgOtherPackage->set_field_nested_enum(
+      pbtest_otherpackage::Message::NestedEnum::C);
+  msg->set_otherpackage_nested_message()->set_field_int32(12);
+  msg->set_otherpackage_enum(pbtest_otherpackage::Enum::B);
+  msg->set_otherpackage_nested_enum(pbtest_otherpackage::Message_NestedEnum::D);
+
+  // Deserialize into golden proto
+  std::string serialized = msg.SerializeAsString();
+  pbgold::DifferentPackages gold_msg;
+  gold_msg.ParseFromString(serialized);
+  EXPECT_EQ(serialized.size(), static_cast<size_t>(gold_msg.ByteSizeLong()));
+
+  // Check fields defined in "protozero.test.protos.subpackage"
+  EXPECT_EQ(1, gold_msg.subpackage_message().field_int32());
+  EXPECT_EQ(pbgold_subpackage::Enum::A,
+            gold_msg.subpackage_message().field_enum());
+  EXPECT_EQ(pbgold_subpackage::Message_NestedEnum_C,
+            gold_msg.subpackage_message().field_nested_enum());
+  EXPECT_EQ(2, gold_msg.subpackage_nested_message().field_int32());
+  EXPECT_EQ(pbgold_subpackage::Enum::B, gold_msg.subpackage_enum());
+  EXPECT_EQ(pbgold_subpackage::Message_NestedEnum_D,
+            gold_msg.subpackage_nested_enum());
+
+  // Check fields defined in "other_package"
+  EXPECT_EQ(11, gold_msg.otherpackage_message().field_int32());
+  EXPECT_EQ(pbgold_other_package::Enum::A,
+            gold_msg.otherpackage_message().field_enum());
+  EXPECT_EQ(pbgold_other_package::Message_NestedEnum_C,
+            gold_msg.otherpackage_message().field_nested_enum());
+  EXPECT_EQ(12, gold_msg.otherpackage_nested_message().field_int32());
+  EXPECT_EQ(pbgold_other_package::Enum::B, gold_msg.otherpackage_enum());
+  EXPECT_EQ(pbgold_other_package::Message_NestedEnum_D,
+            gold_msg.otherpackage_nested_enum());
+}
+
 }  // namespace
 }  // namespace protozero
diff --git a/src/shared_lib/test/protos/other_package/test_messages.pzc.h b/src/shared_lib/test/protos/other_package/test_messages.pzc.h
new file mode 100644
index 0000000..f6a8260
--- /dev/null
+++ b/src/shared_lib/test/protos/other_package/test_messages.pzc.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Autogenerated by the ProtoZero C compiler plugin.
+// Invoked by tools/gen_c_protos
+// DO NOT EDIT.
+#ifndef SRC_SHARED_LIB_TEST_PROTOS_OTHER_PACKAGE_TEST_MESSAGES_PZC_H_
+#define SRC_SHARED_LIB_TEST_PROTOS_OTHER_PACKAGE_TEST_MESSAGES_PZC_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "perfetto/public/pb_macros.h"
+
+PERFETTO_PB_MSG_DECL(other_package_Message_NestedMessage);
+
+PERFETTO_PB_ENUM(other_package_Enum){
+    PERFETTO_PB_ENUM_ENTRY(other_package_A) = 10,
+    PERFETTO_PB_ENUM_ENTRY(other_package_B) = 11,
+};
+
+PERFETTO_PB_ENUM_IN_MSG(other_package_Message, NestedEnum){
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(other_package_Message, C) = 12,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(other_package_Message, D) = 13,
+};
+
+PERFETTO_PB_MSG(other_package_Message);
+PERFETTO_PB_FIELD(other_package_Message, VARINT, int32_t, field_int32, 1);
+PERFETTO_PB_FIELD(other_package_Message,
+                  VARINT,
+                  enum other_package_Enum,
+                  field_enum,
+                  2);
+PERFETTO_PB_FIELD(other_package_Message,
+                  VARINT,
+                  enum other_package_Message_NestedEnum,
+                  field_nested_enum,
+                  3);
+PERFETTO_PB_FIELD(other_package_Message,
+                  MSG,
+                  other_package_Message_NestedMessage,
+                  field_nested_message,
+                  4);
+
+PERFETTO_PB_MSG(other_package_Message_NestedMessage);
+PERFETTO_PB_FIELD(other_package_Message_NestedMessage,
+                  VARINT,
+                  int32_t,
+                  field_int32,
+                  1);
+
+#endif  // SRC_SHARED_LIB_TEST_PROTOS_OTHER_PACKAGE_TEST_MESSAGES_PZC_H_
diff --git a/src/shared_lib/test/protos/subpackage/test_messages.pzc.h b/src/shared_lib/test/protos/subpackage/test_messages.pzc.h
new file mode 100644
index 0000000..24f963a
--- /dev/null
+++ b/src/shared_lib/test/protos/subpackage/test_messages.pzc.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Autogenerated by the ProtoZero C compiler plugin.
+// Invoked by tools/gen_c_protos
+// DO NOT EDIT.
+#ifndef SRC_SHARED_LIB_TEST_PROTOS_SUBPACKAGE_TEST_MESSAGES_PZC_H_
+#define SRC_SHARED_LIB_TEST_PROTOS_SUBPACKAGE_TEST_MESSAGES_PZC_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "perfetto/public/pb_macros.h"
+
+PERFETTO_PB_MSG_DECL(protozero_test_protos_subpackage_Message_NestedMessage);
+
+PERFETTO_PB_ENUM(protozero_test_protos_subpackage_Enum){
+    PERFETTO_PB_ENUM_ENTRY(protozero_test_protos_subpackage_A) = 1,
+    PERFETTO_PB_ENUM_ENTRY(protozero_test_protos_subpackage_B) = 2,
+};
+
+PERFETTO_PB_ENUM_IN_MSG(protozero_test_protos_subpackage_Message, NestedEnum){
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(protozero_test_protos_subpackage_Message,
+                                  C) = 3,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(protozero_test_protos_subpackage_Message,
+                                  D) = 4,
+};
+
+PERFETTO_PB_MSG(protozero_test_protos_subpackage_Message);
+PERFETTO_PB_FIELD(protozero_test_protos_subpackage_Message,
+                  VARINT,
+                  int32_t,
+                  field_int32,
+                  1);
+PERFETTO_PB_FIELD(protozero_test_protos_subpackage_Message,
+                  VARINT,
+                  enum protozero_test_protos_subpackage_Enum,
+                  field_enum,
+                  2);
+PERFETTO_PB_FIELD(protozero_test_protos_subpackage_Message,
+                  VARINT,
+                  enum protozero_test_protos_subpackage_Message_NestedEnum,
+                  field_nested_enum,
+                  3);
+PERFETTO_PB_FIELD(protozero_test_protos_subpackage_Message,
+                  MSG,
+                  protozero_test_protos_subpackage_Message_NestedMessage,
+                  field_nested_message,
+                  4);
+
+PERFETTO_PB_MSG(protozero_test_protos_subpackage_Message_NestedMessage);
+PERFETTO_PB_FIELD(protozero_test_protos_subpackage_Message_NestedMessage,
+                  VARINT,
+                  int32_t,
+                  field_int32,
+                  1);
+
+#endif  // SRC_SHARED_LIB_TEST_PROTOS_SUBPACKAGE_TEST_MESSAGES_PZC_H_
diff --git a/src/shared_lib/test/protos/test_messages.pzc.h b/src/shared_lib/test/protos/test_messages.pzc.h
index 5198ee7..e566576 100644
--- a/src/shared_lib/test/protos/test_messages.pzc.h
+++ b/src/shared_lib/test/protos/test_messages.pzc.h
@@ -25,7 +25,11 @@
 
 #include "perfetto/public/pb_macros.h"
 #include "src/shared_lib/test/protos/library.pzc.h"
+#include "src/shared_lib/test/protos/other_package/test_messages.pzc.h"
+#include "src/shared_lib/test/protos/subpackage/test_messages.pzc.h"
 
+PERFETTO_PB_MSG_DECL(other_package_Message);
+PERFETTO_PB_MSG_DECL(other_package_Message_NestedMessage);
 PERFETTO_PB_MSG_DECL(protozero_test_protos_EveryField);
 PERFETTO_PB_MSG_DECL(protozero_test_protos_NestedA_NestedB);
 PERFETTO_PB_MSG_DECL(protozero_test_protos_NestedA_NestedB_NestedC);
@@ -33,6 +37,8 @@
 PERFETTO_PB_MSG_DECL(protozero_test_protos_TestVersioning_V2_Sub1_V2);
 PERFETTO_PB_MSG_DECL(protozero_test_protos_TestVersioning_V2_Sub2_V2);
 PERFETTO_PB_MSG_DECL(protozero_test_protos_TransgalacticMessage);
+PERFETTO_PB_MSG_DECL(protozero_test_protos_subpackage_Message);
+PERFETTO_PB_MSG_DECL(protozero_test_protos_subpackage_Message_NestedMessage);
 
 PERFETTO_PB_ENUM(protozero_test_protos_SmallEnum){
     PERFETTO_PB_ENUM_ENTRY(protozero_test_protos_TO_BE) = 1,
@@ -71,6 +77,48 @@
     PERFETTO_PB_ENUM_IN_MSG_ENTRY(protozero_test_protos_EveryField, PONG) = 2,
 };
 
+PERFETTO_PB_MSG(protozero_test_protos_DifferentPackages);
+PERFETTO_PB_FIELD(protozero_test_protos_DifferentPackages,
+                  MSG,
+                  protozero_test_protos_subpackage_Message,
+                  subpackage_message,
+                  1);
+PERFETTO_PB_FIELD(protozero_test_protos_DifferentPackages,
+                  MSG,
+                  protozero_test_protos_subpackage_Message_NestedMessage,
+                  subpackage_nested_message,
+                  2);
+PERFETTO_PB_FIELD(protozero_test_protos_DifferentPackages,
+                  VARINT,
+                  enum protozero_test_protos_subpackage_Enum,
+                  subpackage_enum,
+                  3);
+PERFETTO_PB_FIELD(protozero_test_protos_DifferentPackages,
+                  VARINT,
+                  enum protozero_test_protos_subpackage_Message_NestedEnum,
+                  subpackage_nested_enum,
+                  4);
+PERFETTO_PB_FIELD(protozero_test_protos_DifferentPackages,
+                  MSG,
+                  other_package_Message,
+                  otherpackage_message,
+                  5);
+PERFETTO_PB_FIELD(protozero_test_protos_DifferentPackages,
+                  MSG,
+                  other_package_Message_NestedMessage,
+                  otherpackage_nested_message,
+                  6);
+PERFETTO_PB_FIELD(protozero_test_protos_DifferentPackages,
+                  VARINT,
+                  enum other_package_Enum,
+                  otherpackage_enum,
+                  7);
+PERFETTO_PB_FIELD(protozero_test_protos_DifferentPackages,
+                  VARINT,
+                  enum other_package_Message_NestedEnum,
+                  otherpackage_nested_enum,
+                  8);
+
 PERFETTO_PB_MSG(protozero_test_protos_TestVersioning_V2);
 PERFETTO_PB_FIELD(protozero_test_protos_TestVersioning_V2,
                   VARINT,
diff --git a/src/tools/ftrace_proto_gen/event_list b/src/tools/ftrace_proto_gen/event_list
index b4c80bc..ccb0eee 100644
--- a/src/tools/ftrace_proto_gen/event_list
+++ b/src/tools/ftrace_proto_gen/event_list
@@ -484,3 +484,4 @@
 perf_trace_counters/sched_switch_with_ctrs
 power/gpu_work_period
 rpm/rpm_status
+panel/panel_write_generic
\ No newline at end of file
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index e37c9c6..84c09ce 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -156,6 +156,7 @@
       "../../protos/perfetto/common:zero",
       "../../protos/perfetto/trace:zero",
       "../../protos/perfetto/trace/perfetto:zero",
+      "../../protos/perfetto/trace_processor:zero",
       "../base",
       "../protozero",
       "db",
diff --git a/src/trace_processor/importers/common/sched_event_tracker.h b/src/trace_processor/importers/common/sched_event_tracker.h
index f83ffb6..3eba6ae 100644
--- a/src/trace_processor/importers/common/sched_event_tracker.h
+++ b/src/trace_processor/importers/common/sched_event_tracker.h
@@ -17,12 +17,10 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_SCHED_EVENT_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_SCHED_EVENT_TRACKER_H_
 
-#include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
-#include "src/trace_processor/importers/common/system_info_tracker.h"
+
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/destructible.h"
-#include "src/trace_processor/types/task_state.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
 namespace perfetto {
@@ -84,20 +82,6 @@
     slices->mutable_end_state()->Set(pending_slice_idx, prev_state);
   }
 
-  // TODO(rsavitski): fold back into ftrace parser, this is specific to Linux.
-  PERFETTO_ALWAYS_INLINE
-  StringId TaskStateToStringId(int64_t task_state_int) {
-    using ftrace_utils::TaskState;
-
-    std::optional<VersionNumber> kernel_version =
-        SystemInfoTracker::GetOrCreate(context_)->GetKernelVersion();
-    TaskState task_state = TaskState::FromRawPrevState(
-        static_cast<uint16_t>(task_state_int), kernel_version);
-    return task_state.is_valid()
-               ? context_->storage->InternString(task_state.ToString().data())
-               : kNullStringId;
-  }
-
  private:
   TraceProcessorContext* const context_;
 };
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index a88cc26..21435f2 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<FtraceMessageDescriptor, 490> descriptors{{
+std::array<FtraceMessageDescriptor, 491> descriptors{{
     {nullptr, 0, {}},
     {nullptr, 0, {}},
     {nullptr, 0, {}},
@@ -5396,6 +5396,19 @@
             {"status", ProtoSchemaType::kInt32},
         },
     },
+    {
+        "panel_write_generic",
+        6,
+        {
+            {},
+            {"pid", ProtoSchemaType::kInt32},
+            {"trace_name", ProtoSchemaType::kString},
+            {"trace_begin", ProtoSchemaType::kUint32},
+            {"name", ProtoSchemaType::kString},
+            {"type", ProtoSchemaType::kUint32},
+            {"value", ProtoSchemaType::kInt32},
+        },
+    },
 }};
 
 }  // namespace
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index 95cf0d8..6e1e1bb 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -1319,7 +1319,7 @@
     StringId name_id = message_strings.field_name_ids[field_id];
 
     // Check if this field represents a kernel function.
-    const auto* it = std::find_if(
+    const auto it = std::find_if(
         kKernelFunctionFields.begin(), kKernelFunctionFields.end(),
         [ftrace_id, field_id](const FtraceEventAndFieldId& ev) {
           return ev.event_id == ftrace_id && ev.field_id == field_id;
diff --git a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
index 67c3691..1a4f5d7 100644
--- a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
@@ -18,14 +18,17 @@
 
 #include <limits>
 
+#include "perfetto/ext/base/string_view.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/process_tracker.h"
 #include "src/trace_processor/importers/common/sched_event_state.h"
 #include "src/trace_processor/importers/common/sched_event_tracker.h"
+#include "src/trace_processor/importers/common/system_info_tracker.h"
 #include "src/trace_processor/importers/common/thread_state_tracker.h"
 #include "src/trace_processor/importers/ftrace/ftrace_descriptors.h"
 #include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/types/task_state.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
 
@@ -84,8 +87,7 @@
   bool prev_pid_match_prev_next_pid = false;
   auto* pending_sched = sched_event_state_.GetPendingSchedInfoForCpu(cpu);
   uint32_t pending_slice_idx = pending_sched->pending_slice_storage_idx;
-  StringId prev_state_string_id = context_->sched_event_tracker
-                                      ->TaskStateToStringId(prev_state);
+  StringId prev_state_string_id = TaskStateToStringId(prev_state);
   if (prev_state_string_id == kNullStringId) {
     context_->storage->IncrementStats(stats::task_state_invalid);
   }
@@ -158,8 +160,7 @@
   // Close the pending slice if any (we won't have one when processing the first
   // two compact events for a given cpu).
   uint32_t pending_slice_idx = pending_sched->pending_slice_storage_idx;
-  StringId prev_state_str_id =
-      context_->sched_event_tracker->TaskStateToStringId(prev_state);
+  StringId prev_state_str_id = TaskStateToStringId(prev_state);
   if (prev_state_str_id == kNullStringId) {
     context_->storage->IncrementStats(stats::task_state_invalid);
   }
@@ -303,5 +304,17 @@
   }
 }
 
+StringId FtraceSchedEventTracker::TaskStateToStringId(int64_t task_state_int) {
+  using ftrace_utils::TaskState;
+  std::optional<VersionNumber> kernel_version =
+      SystemInfoTracker::GetOrCreate(context_)->GetKernelVersion();
+
+  TaskState task_state = TaskState::FromRawPrevState(
+      static_cast<uint16_t>(task_state_int), kernel_version);
+  return task_state.is_valid()
+             ? context_->storage->InternString(task_state.ToString().data())
+             : kNullStringId;
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.h b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.h
index 42b0d4f..7a03dfa 100644
--- a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.h
+++ b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.h
@@ -38,10 +38,8 @@
   explicit FtraceSchedEventTracker(TraceProcessorContext*);
   ~FtraceSchedEventTracker() override;
 
-  FtraceSchedEventTracker(
-      const FtraceSchedEventTracker& ftrace_sched_event_tracker) = delete;
-  FtraceSchedEventTracker& operator=(
-      const FtraceSchedEventTracker& ftrace_sched_event_tracker) = delete;
+  FtraceSchedEventTracker(const FtraceSchedEventTracker&) = delete;
+  FtraceSchedEventTracker& operator=(const FtraceSchedEventTracker&) = delete;
 
   static FtraceSchedEventTracker* GetOrCreate(TraceProcessorContext* context) {
     if (!context->ftrace_sched_tracker) {
@@ -97,6 +95,8 @@
                               bool parse_only_into_raw);
 
  private:
+  StringId TaskStateToStringId(int64_t task_state_int);
+
   static constexpr uint8_t kSchedSwitchMaxFieldId = 7;
   std::array<StringId, kSchedSwitchMaxFieldId + 1> sched_switch_field_ids_;
   StringId sched_switch_id_;
diff --git a/src/trace_processor/importers/perf/perf_data_parser.cc b/src/trace_processor/importers/perf/perf_data_parser.cc
index bb79209..71912f2 100644
--- a/src/trace_processor/importers/perf/perf_data_parser.cc
+++ b/src/trace_processor/importers/perf/perf_data_parser.cc
@@ -59,8 +59,9 @@
 
   // First instruction pointer in the callchain should be from kernel space, so
   // it shouldn't be available in mappings.
+  UniquePid upid = context_->process_tracker->GetOrCreateProcess(*sample.pid);
   if (context_->mapping_tracker->FindUserMappingForAddress(
-          *sample.pid, sample.callchain.front())) {
+          upid, sample.callchain.front())) {
     context_->storage->IncrementStats(stats::perf_samples_skipped);
     return;
   }
@@ -74,7 +75,7 @@
   for (uint32_t i = 1; i < sample.callchain.size(); i++) {
     UserMemoryMapping* mapping =
         context_->mapping_tracker->FindUserMappingForAddress(
-            *sample.pid, sample.callchain[i]);
+            upid, sample.callchain[i]);
     if (!mapping) {
       context_->storage->IncrementStats(stats::perf_samples_skipped);
       return;
@@ -121,7 +122,6 @@
   }
   if (sample.tid) {
     auto utid = context_->process_tracker->GetOrCreateThread(*sample.tid);
-    context_->process_tracker->GetOrCreateProcess(*sample.pid);
     perf_sample_row.utid = utid;
   }
   context_->storage->mutable_perf_sample_table()->Insert(perf_sample_row);
diff --git a/src/trace_processor/importers/proto/system_probes_parser.cc b/src/trace_processor/importers/proto/system_probes_parser.cc
index 2654fdb..8812674 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.cc
+++ b/src/trace_processor/importers/proto/system_probes_parser.cc
@@ -107,6 +107,10 @@
 SystemProbesParser::SystemProbesParser(TraceProcessorContext* context)
     : context_(context),
       utid_name_id_(context->storage->InternString("utid")),
+      ns_unit_id_(context->storage->InternString("ns")),
+      bytes_unit_id_(context->storage->InternString("bytes")),
+      available_chunks_unit_id_(
+          context->storage->InternString("available chunks")),
       num_forks_name_id_(context->storage->InternString("num_forks")),
       num_irq_total_name_id_(context->storage->InternString("num_irq_total")),
       num_softirq_total_name_id_(
@@ -208,6 +212,8 @@
     context_->event_tracker->PushCounter(ts, value, track);
   };
 
+  // TODO(rsavitski): with the UI now supporting rate mode for counter tracks,
+  // this is likely redundant.
   auto calculate_throughput = [](double amount, int64_t diff) {
     return diff == 0 ? 0 : amount * MS_PER_SEC / static_cast<double>(diff);
   };
@@ -277,9 +283,10 @@
     }
     // /proc/meminfo counters are in kB, convert to bytes
     TrackId track = context_->track_tracker->InternGlobalCounterTrack(
-        TrackTracker::Group::kMemory, meminfo_strs_id_[key]);
+        TrackTracker::Group::kMemory, meminfo_strs_id_[key], {},
+        bytes_unit_id_);
     context_->event_tracker->PushCounter(
-        ts, static_cast<double>(mi.value()) * 1024., track);
+        ts, static_cast<double>(mi.value()) * 1024, track);
   }
 
   for (auto it = sys_stats.devfreq(); it; ++it) {
@@ -402,20 +409,23 @@
         ts, static_cast<double>(sys_stats.num_softirq_total()), track);
   }
 
+  // Fragmentation of the kernel binary buddy memory allocator.
+  // See /proc/buddyinfo in `man 5 proc`.
   for (auto it = sys_stats.buddy_info(); it; ++it) {
     protos::pbzero::SysStats::BuddyInfo::Decoder bi(*it);
     int order = 0;
     for (auto order_it = bi.order_pages(); order_it; ++order_it) {
       std::string node = bi.node().ToStdString();
       std::string zone = bi.zone().ToStdString();
-      uint32_t size_kb =
+      uint32_t chunk_size_kb =
           static_cast<uint32_t>(((1 << order) * page_size_) / 1024);
       base::StackString<255> counter_name("mem.buddyinfo[%s][%s][%u kB]",
-                                          node.c_str(), zone.c_str(), size_kb);
+                                          node.c_str(), zone.c_str(),
+                                          chunk_size_kb);
       StringId name =
           context_->storage->InternString(counter_name.string_view());
       TrackId track = context_->track_tracker->InternGlobalCounterTrack(
-          TrackTracker::Group::kMemory, name);
+          TrackTracker::Group::kMemory, name, {}, available_chunks_unit_id_);
       context_->event_tracker->PushCounter(ts, static_cast<double>(*order_it),
                                            track);
       order++;
@@ -426,6 +436,8 @@
     ParseDiskStats(ts, *it);
   }
 
+  // Pressure Stall Information. See
+  // https://docs.kernel.org/accounting/psi.html.
   for (auto it = sys_stats.psi(); it; ++it) {
     protos::pbzero::SysStats::PsiSample::Decoder psi(*it);
 
@@ -436,11 +448,12 @@
       continue;
     }
 
+    // Unit = total blocked time on this resource in nanoseconds.
     // TODO(b/315152880): Consider moving psi entries for cpu/io/memory into
     // groups specific to that resource (e.g., `Group::kMemory`).
     TrackId track = context_->track_tracker->InternGlobalCounterTrack(
         TrackTracker::Group::kDeviceState,
-        sys_stats_psi_resource_names_[resource]);
+        sys_stats_psi_resource_names_[resource], {}, ns_unit_id_);
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(psi.total_ns()), track);
   }
diff --git a/src/trace_processor/importers/proto/system_probes_parser.h b/src/trace_processor/importers/proto/system_probes_parser.h
index 776b2c6..a54beb7 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.h
+++ b/src/trace_processor/importers/proto/system_probes_parser.h
@@ -50,6 +50,10 @@
   TraceProcessorContext* const context_;
 
   const StringId utid_name_id_;
+  const StringId ns_unit_id_;
+  const StringId bytes_unit_id_;
+  const StringId available_chunks_unit_id_;
+
   const StringId num_forks_name_id_;
   const StringId num_irq_total_name_id_;
   const StringId num_softirq_total_name_id_;
diff --git a/src/trace_processor/iterator_impl.h b/src/trace_processor/iterator_impl.h
index 89f300b..47e2e5f 100644
--- a/src/trace_processor/iterator_impl.h
+++ b/src/trace_processor/iterator_impl.h
@@ -18,22 +18,18 @@
 #define SRC_TRACE_PROCESSOR_ITERATOR_IMPL_H_
 
 #include <sqlite3.h>
+#include <cstddef>
+#include <cstdint>
+#include <string>
 
-#include <memory>
-#include <optional>
-#include <vector>
-
-#include "perfetto/base/build_config.h"
-#include "perfetto/base/export.h"
+#include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/status_or.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/iterator.h"
-#include "perfetto/trace_processor/status.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
-#include "src/trace_processor/sqlite/scoped_db.h"
 #include "src/trace_processor/sqlite/sqlite_engine.h"
-#include "src/trace_processor/sqlite/sqlite_utils.h"
 
 namespace perfetto {
 namespace trace_processor {
diff --git a/src/trace_processor/metrics/metrics.cc b/src/trace_processor/metrics/metrics.cc
index 639e252..ff492f4 100644
--- a/src/trace_processor/metrics/metrics.cc
+++ b/src/trace_processor/metrics/metrics.cc
@@ -465,8 +465,8 @@
   if (repeated_field_type_ && repeated_field_type_ != type) {
     return base::ErrStatus(
         "Inconsistent type in RepeatedField: was %s but now seen value %s",
-        sqlite_utils::SqliteTypeToFriendlyString(*repeated_field_type_),
-        sqlite_utils::SqliteTypeToFriendlyString(type));
+        sqlite::utils::SqliteTypeToFriendlyString(*repeated_field_type_),
+        sqlite::utils::SqliteTypeToFriendlyString(type));
   }
   repeated_field_type_ = type;
   return base::OkStatus();
@@ -514,13 +514,13 @@
     return base::OkStatus();
   }
 
-  out = sqlite_utils::SqliteValueToSqlValue(argv[0]);
+  out = sqlite::utils::SqliteValueToSqlValue(argv[0]);
   return base::OkStatus();
 }
 
-void RepeatedFieldStep(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
+void RepeatedField::Step(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
   if (argc != 1) {
-    sqlite3_result_error(ctx, "RepeatedField: only expected one arg", -1);
+    sqlite::result::Error(ctx, "RepeatedField: only expected one arg");
     return;
   }
 
@@ -538,15 +538,15 @@
     *builder_ptr_ptr = new RepeatedFieldBuilder();
   }
 
-  auto value = sqlite_utils::SqliteValueToSqlValue(argv[0]);
+  auto value = sqlite::utils::SqliteValueToSqlValue(argv[0]);
   RepeatedFieldBuilder* builder = *builder_ptr_ptr;
   auto status = builder->AddSqlValue(value);
   if (!status.ok()) {
-    sqlite3_result_error(ctx, status.c_message(), -1);
+    sqlite::result::Error(ctx, status.c_message());
   }
 }
 
-void RepeatedFieldFinal(sqlite3_context* ctx) {
+void RepeatedField::Final(sqlite3_context* ctx) {
   // Note: we choose the size intentionally to be zero because we don't want to
   // allocate if the Step has never been called.
   auto** builder_ptr_ptr =
@@ -554,7 +554,7 @@
 
   // If Step has never been called, |builder_ptr_ptr| will be null.
   if (builder_ptr_ptr == nullptr) {
-    sqlite3_result_null(ctx);
+    sqlite::result::Null(ctx);
     return;
   }
 
@@ -563,14 +563,15 @@
   std::unique_ptr<RepeatedFieldBuilder> builder(*builder_ptr_ptr);
   std::vector<uint8_t> raw = builder->SerializeToProtoBuilderResult();
   if (raw.empty()) {
-    sqlite3_result_null(ctx);
+    sqlite::result::Null(ctx);
     return;
   }
 
   std::unique_ptr<uint8_t[], base::FreeDeleter> data(
       static_cast<uint8_t*>(malloc(raw.size())));
   memcpy(data.get(), raw.data(), raw.size());
-  sqlite3_result_blob(ctx, data.release(), static_cast<int>(raw.size()), free);
+  return sqlite::result::RawBytes(ctx, data.release(),
+                                  static_cast<int>(raw.size()), free);
 }
 
 // SQLite function implementation used to build a proto directly in SQL. The
@@ -600,7 +601,7 @@
 
     const char* key =
         reinterpret_cast<const char*>(sqlite3_value_text(argv[i]));
-    SqlValue value = sqlite_utils::SqliteValueToSqlValue(argv[i + 1]);
+    SqlValue value = sqlite::utils::SqliteValueToSqlValue(argv[i + 1]);
     RETURN_IF_ERROR(builder.AppendSqlValue(key, value));
   }
 
@@ -610,7 +611,7 @@
   if (raw.empty()) {
     // Passing nullptr to SQLite feels dangerous so just pass an empty string
     // and zero as the size so we don't deref nullptr accidentially somewhere.
-    destructors.bytes_destructor = sqlite_utils::kSqliteStatic;
+    destructors.bytes_destructor = sqlite::utils::kSqliteStatic;
     out = SqlValue::Bytes("", 0);
     return base::OkStatus();
   }
@@ -647,10 +648,10 @@
       return base::ErrStatus("RUN_METRIC: all keys must be strings");
     }
 
-    std::optional<std::string> key_str = sqlite_utils::SqlValueToString(
-        sqlite_utils::SqliteValueToSqlValue(argv[i]));
-    std::optional<std::string> value_str = sqlite_utils::SqlValueToString(
-        sqlite_utils::SqliteValueToSqlValue(argv[i + 1]));
+    std::optional<std::string> key_str = sqlite::utils::SqlValueToString(
+        sqlite::utils::SqliteValueToSqlValue(argv[i]));
+    std::optional<std::string> value_str = sqlite::utils::SqlValueToString(
+        sqlite::utils::SqliteValueToSqlValue(argv[i + 1]));
 
     if (!value_str) {
       return base::ErrStatus(
@@ -682,8 +683,8 @@
         "arguments");
   }
 
-  SqlValue proto = sqlite_utils::SqliteValueToSqlValue(argv[0]);
-  SqlValue message_type = sqlite_utils::SqliteValueToSqlValue(argv[1]);
+  SqlValue proto = sqlite::utils::SqliteValueToSqlValue(argv[0]);
+  SqlValue message_type = sqlite::utils::SqliteValueToSqlValue(argv[1]);
 
   if (proto.type != SqlValue::Type::kBytes) {
     return base::ErrStatus("UNWRAP_METRIC_PROTO: proto is not a blob");
@@ -696,7 +697,7 @@
   const uint8_t* ptr = static_cast<const uint8_t*>(proto.AsBytes());
   size_t size = proto.bytes_count;
   if (size == 0) {
-    destructors.bytes_destructor = sqlite_utils::kSqliteStatic;
+    destructors.bytes_destructor = sqlite::utils::kSqliteStatic;
     out = SqlValue::Bytes("", 0);
     return base::OkStatus();
   }
@@ -766,7 +767,7 @@
                              sql_metric.output_table_name.value().c_str());
     }
 
-    SqlValue col = sqlite_utils::SqliteValueToSqlValue(
+    SqlValue col = sqlite::utils::SqliteValueToSqlValue(
         sqlite3_column_value(it->stmt.sqlite_stmt(), 0));
     if (col.type != SqlValue::kBytes) {
       return base::ErrStatus("Output table %s column has invalid type",
diff --git a/src/trace_processor/metrics/metrics.h b/src/trace_processor/metrics/metrics.h
index 7eead1f..3918489 100644
--- a/src/trace_processor/metrics/metrics.h
+++ b/src/trace_processor/metrics/metrics.h
@@ -36,6 +36,7 @@
 #include "perfetto/trace_processor/trace_processor.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_aggregate_function.h"
 #include "src/trace_processor/util/descriptors.h"
 
 #include "protos/perfetto/trace_processor/metrics_impl.pbzero.h"
@@ -194,8 +195,10 @@
 };
 
 // These functions implement the RepeatedField SQL aggregate functions.
-void RepeatedFieldStep(sqlite3_context* ctx, int argc, sqlite3_value** argv);
-void RepeatedFieldFinal(sqlite3_context* ctx);
+struct RepeatedField : public SqliteAggregateFunction {
+  static void Step(sqlite3_context* ctx, int argc, sqlite3_value** argv);
+  static void Final(sqlite3_context* ctx);
+};
 
 base::Status ComputeMetrics(PerfettoSqlEngine*,
                             const std::vector<std::string>& metrics_to_compute,
diff --git a/src/trace_processor/perfetto_sql/engine/created_function.cc b/src/trace_processor/perfetto_sql/engine/created_function.cc
index 7df16a8..be4f956 100644
--- a/src/trace_processor/perfetto_sql/engine/created_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/created_function.cc
@@ -75,7 +75,7 @@
   }
 
   SqlValue result =
-      sqlite_utils::SqliteValueToSqlValue(sqlite3_column_value(stmt, 0));
+      sqlite::utils::SqliteValueToSqlValue(sqlite3_column_value(stmt, 0));
 
   // If we return a bytes type but have a null pointer, SQLite will convert this
   // to an SQL null. However, for proto build functions, we actively want to
@@ -205,7 +205,7 @@
     if (argc != 1) {
       return std::nullopt;
     }
-    SqlValue arg = sqlite_utils::SqliteValueToSqlValue(argv[0]);
+    SqlValue arg = sqlite::utils::SqliteValueToSqlValue(argv[0]);
     if (arg.type != SqlValue::Type::kLong) {
       return std::nullopt;
     }
@@ -611,7 +611,7 @@
   for (size_t i = 0; i < argc; ++i) {
     sqlite3_value* arg = argv[i];
     sql_argument::Type type = state->prototype().arguments[i].type();
-    base::Status status = sqlite_utils::TypeCheckSqliteValue(
+    base::Status status = sqlite::utils::TypeCheckSqliteValue(
         arg, sql_argument::TypeToSqlValueType(type),
         sql_argument::TypeToHumanFriendlyString(type));
     if (!status.ok()) {
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
index 6d7f965..8959989 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
@@ -182,8 +182,8 @@
   }
 
   engine_->RegisterVirtualTableModule<RuntimeTableFunction>(
-      "runtime_table_function", this, SqliteTable::TableType::kExplicitCreate,
-      false);
+      "runtime_table_function", this,
+      SqliteTableLegacy::TableType::kExplicitCreate, false);
   auto context = std::make_unique<DbSqliteTable::Context>(
       query_cache_.get(),
       [this](const std::string& name) {
@@ -197,7 +197,7 @@
       });
   engine_->RegisterVirtualTableModule<DbSqliteTable>(
       "runtime_table", std::move(context),
-      SqliteTable::TableType::kExplicitCreate, false);
+      SqliteTableLegacy::TableType::kExplicitCreate, false);
 }
 
 PerfettoSqlEngine::~PerfettoSqlEngine() {
@@ -215,7 +215,7 @@
       query_cache_.get(), &table, std::move(schema));
   static_tables_.Insert(table_name, &table);
   engine_->RegisterVirtualTableModule<DbSqliteTable>(
-      table_name, std::move(context), SqliteTable::kEponymousOnly, false);
+      table_name, std::move(context), SqliteTableLegacy::kEponymousOnly, false);
 
   // Register virtual tables into an internal 'perfetto_tables' table.
   // This is used for iterating through all the tables during a database
@@ -237,7 +237,7 @@
   auto context = std::make_unique<DbSqliteTable::Context>(query_cache_.get(),
                                                           std::move(fn));
   engine_->RegisterVirtualTableModule<DbSqliteTable>(
-      table_name, std::move(context), SqliteTable::kEponymousOnly, false);
+      table_name, std::move(context), SqliteTableLegacy::kEponymousOnly, false);
 }
 
 base::StatusOr<PerfettoSqlEngine::ExecutionStats> PerfettoSqlEngine::Execute(
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
index 69e6b58..8fe5425 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
@@ -38,6 +38,9 @@
 #include "src/trace_processor/perfetto_sql/engine/runtime_table_function.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_aggregate_function.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_window_function.h"
 #include "src/trace_processor/sqlite/query_cache.h"
 #include "src/trace_processor/sqlite/sql_source.h"
 #include "src/trace_processor/sqlite/sqlite_engine.h"
@@ -86,14 +89,15 @@
   //
   // The format of the function is given by the |SqlFunction|.
   //
-  // |name|:        name of the function in SQL
-  // |argc|:        number of arguments for this function. This can be -1 if
-  //                the number of arguments is variable.
-  // |ctx|:         context object for the function (see SqlFunction::Run);
-  //                this object *must* outlive the function so should likely be
-  //                either static or scoped to the lifetime of TraceProcessor.
-  // |determistic|: whether this function has deterministic output given the
-  //                same set of arguments.
+  // |name|:          name of the function in SQL.
+  // |argc|:          number of arguments for this function. This can be -1 if
+  //                  the number of arguments is variable.
+  // |ctx|:           context object for the function (see SqlFunction::Run);
+  //                  this object *must* outlive the function so should likely
+  //                  be either static or scoped to the lifetime of
+  //                  TraceProcessor.
+  // |deterministic|: whether this function has deterministic output given the
+  //                  same set of arguments.
   template <typename Function = SqlFunction>
   base::Status RegisterStaticFunction(const char* name,
                                       int argc,
@@ -113,6 +117,42 @@
       std::unique_ptr<typename Function::Context> ctx,
       bool deterministic = true);
 
+  // Registers a trace processor C++ aggregate function to be runnable from SQL.
+  //
+  // The format of the function is given by the |SqliteAggregateFunction|.
+  //
+  // |name|:          name of the function in SQL
+  // |argc|:          number of arguments for this function. This can be -1 if
+  //                  the number of arguments is variable.
+  // |ctx|:           context object for the function; this object *must*
+  //                  outlive the function so should likely be either static or
+  //                  scoped to the lifetime of TraceProcessor.
+  // |deterministic|: whether this function has deterministic output given the
+  //                  same set of arguments.
+  template <typename Function = SqliteAggregateFunction>
+  base::Status RegisterSqliteAggregateFunction(const char* name,
+                                               int argc,
+                                               typename Function::Context* ctx,
+                                               bool deterministic = true);
+
+  // Registers a trace processor C++ window function to be runnable from SQL.
+  //
+  // The format of the function is given by the |SqliteWindowFunction|.
+  //
+  // |name|:          name of the function in SQL.
+  // |argc|:          number of arguments for this function. This can be -1 if
+  //                  the number of arguments is variable.
+  // |ctx|:           context object for the function; this object *must*
+  //                  outlive the function so should likely be either static or
+  //                  scoped to the lifetime of TraceProcessor.
+  // |deterministic|: whether this function has deterministic output given the
+  //                  same set of arguments.
+  template <typename Function = SqliteWindowFunction>
+  base::Status RegisterSqliteWindowFunction(const char* name,
+                                            int argc,
+                                            typename Function::Context* ctx,
+                                            bool deterministic = true);
+
   // Registers a function with the prototype |prototype| which returns a value
   // of |return_type| and is implemented by executing the SQL statement |sql|.
   base::Status RegisterRuntimeFunction(bool replace,
@@ -171,8 +211,9 @@
 
     // The missing objects from the above query are static functions, runtime
     // functions and macros. Add those in now.
-    return query_count + static_function_count_ + runtime_function_count_ +
-           macros_.size();
+    return query_count + static_function_count_ +
+           static_window_function_count_ + static_aggregate_function_count_ +
+           runtime_function_count_ + macros_.size();
   }
 
   // Find RuntimeTable registered with engine with provided name.
@@ -232,6 +273,8 @@
   StringPool* pool_ = nullptr;
 
   uint64_t static_function_count_ = 0;
+  uint64_t static_aggregate_function_count_ = 0;
+  uint64_t static_window_function_count_ = 0;
   uint64_t runtime_function_count_ = 0;
 
   base::FlatHashMap<std::string, std::unique_ptr<RuntimeTableFunction::State>>
@@ -267,13 +310,13 @@
   base::Status status =
       Function::Run(ud, static_cast<size_t>(argc), argv, value, destructors);
   if (!status.ok()) {
-    sqlite3_result_error(ctx, status.c_message(), -1);
+    sqlite::result::Error(ctx, status.c_message());
     return;
   }
 
   if (Function::kVoidReturn) {
     if (!value.is_null()) {
-      sqlite3_result_error(ctx, "void SQL function returned value", -1);
+      sqlite::result::Error(ctx, "void SQL function returned value");
       return;
     }
 
@@ -283,15 +326,15 @@
     // if we don't actually read it - just set it to a pointer to an empty
     // string for this reason.
     static char kVoidValue[] = "";
-    sqlite3_result_pointer(ctx, kVoidValue, "VOID", nullptr);
+    sqlite::result::StaticPointer(ctx, kVoidValue, "VOID");
   } else {
-    sqlite_utils::ReportSqlValue(ctx, value, destructors.string_destructor,
-                                 destructors.bytes_destructor);
+    sqlite::utils::ReportSqlValue(ctx, value, destructors.string_destructor,
+                                  destructors.bytes_destructor);
   }
 
   status = Function::VerifyPostConditions(ud);
   if (!status.ok()) {
-    sqlite3_result_error(ctx, status.c_message(), -1);
+    sqlite::result::Error(ctx, status.c_message());
     return;
   }
 }
@@ -315,6 +358,29 @@
 }
 
 template <typename Function>
+base::Status PerfettoSqlEngine::RegisterSqliteAggregateFunction(
+    const char* name,
+    int argc,
+    typename Function::Context* ctx,
+    bool deterministic) {
+  static_aggregate_function_count_++;
+  return engine_->RegisterAggregateFunction(
+      name, argc, Function::Step, Function::Final, ctx, nullptr, deterministic);
+}
+
+template <typename Function>
+base::Status PerfettoSqlEngine::RegisterSqliteWindowFunction(
+    const char* name,
+    int argc,
+    typename Function::Context* ctx,
+    bool deterministic) {
+  static_window_function_count_++;
+  return engine_->RegisterWindowFunction(
+      name, argc, Function::Step, Function::Inverse, Function::Value,
+      Function::Final, ctx, nullptr, deterministic);
+}
+
+template <typename Function>
 base::Status PerfettoSqlEngine::RegisterStaticFunction(
     const char* name,
     int argc,
diff --git a/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc b/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
index 247d79b..3d0af93 100644
--- a/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
@@ -20,6 +20,7 @@
 #include <utility>
 
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
 #include "src/trace_processor/util/status_macros.h"
 
 namespace perfetto {
@@ -51,7 +52,7 @@
   return base::OkStatus();
 }
 
-SqliteTable::Schema RuntimeTableFunction::CreateSchema() {
+SqliteTableLegacy::Schema RuntimeTableFunction::CreateSchema() {
   std::vector<Column> columns;
   for (size_t i = 0; i < state_->return_values.size(); ++i) {
     const auto& ret = state_->return_values[i];
@@ -79,10 +80,11 @@
       Column(columns.size(), "_primary_key", SqlValue::kLong, true));
   primary_keys.emplace_back(columns.size() - 1);
 
-  return SqliteTable::Schema(std::move(columns), std::move(primary_keys));
+  return SqliteTableLegacy::Schema(std::move(columns), std::move(primary_keys));
 }
 
-std::unique_ptr<SqliteTable::BaseCursor> RuntimeTableFunction::CreateCursor() {
+std::unique_ptr<SqliteTableLegacy::BaseCursor>
+RuntimeTableFunction::CreateCursor() {
   return std::unique_ptr<Cursor>(new Cursor(this, state_));
 }
 
@@ -108,7 +110,7 @@
 }
 
 RuntimeTableFunction::Cursor::Cursor(RuntimeTableFunction* table, State* state)
-    : SqliteTable::BaseCursor(table), table_(table), state_(state) {
+    : SqliteTableLegacy::BaseCursor(table), table_(table), state_(state) {
   if (state->reusable_stmt) {
     stmt_ = std::move(state->reusable_stmt);
     state->reusable_stmt = std::nullopt;
@@ -148,13 +150,13 @@
 
     // We only support equality constraints as we're expecting "input arguments"
     // to our "function".
-    if (!sqlite_utils::IsOpEq(cs.op)) {
+    if (!sqlite::utils::IsOpEq(cs.op)) {
       return base::ErrStatus("%s: non-equality constraint passed",
                              state_->prototype.function_name.c_str());
     }
 
     const auto& arg = state_->prototype.arguments[col_to_arg_idx(cs.column)];
-    base::Status status = sqlite_utils::TypeCheckSqliteValue(
+    base::Status status = sqlite::utils::TypeCheckSqliteValue(
         argv[i], sql_argument::TypeToSqlValueType(arg.type()),
         sql_argument::TypeToHumanFriendlyString(arg.type()));
     if (!status.ok()) {
@@ -229,16 +231,16 @@
 base::Status RuntimeTableFunction::Cursor::Column(sqlite3_context* ctx, int i) {
   size_t idx = static_cast<size_t>(i);
   if (state_->IsReturnValueColumn(idx)) {
-    sqlite3_result_value(ctx, sqlite3_column_value(stmt_->sqlite_stmt(), i));
+    sqlite::result::Value(ctx, sqlite3_column_value(stmt_->sqlite_stmt(), i));
   } else if (state_->IsArgumentColumn(idx)) {
     // TODO(lalitm): it may be more appropriate to keep a note of the arguments
     // which we passed in and return them here. Not doing this to because it
     // doesn't seem necessary for any useful thing but something which may need
     // to be changed in the future.
-    sqlite3_result_null(ctx);
+    sqlite::result::Null(ctx);
   } else {
     PERFETTO_DCHECK(state_->IsPrimaryKeyColumn(idx));
-    sqlite3_result_int(ctx, next_call_count_);
+    sqlite::result::Long(ctx, next_call_count_);
   }
   return base::OkStatus();
 }
diff --git a/src/trace_processor/perfetto_sql/engine/runtime_table_function.h b/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
index 7c92fcc..80edf72 100644
--- a/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
+++ b/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
@@ -27,8 +27,8 @@
 
 class PerfettoSqlEngine;
 
-// The implementation of the SqliteTable interface for table functions defined
-// at runtime using SQL.
+// The implementation of the SqliteTableLegacy interface for table functions
+// defined at runtime using SQL.
 class RuntimeTableFunction final
     : public TypedSqliteTable<RuntimeTableFunction, PerfettoSqlEngine*> {
  public:
@@ -65,7 +65,7 @@
              kPrimaryKeyColumns;
     }
   };
-  class Cursor final : public SqliteTable::BaseCursor {
+  class Cursor final : public SqliteTableLegacy::BaseCursor {
    public:
     explicit Cursor(RuntimeTableFunction* table, State* state);
     ~Cursor() final;
@@ -92,7 +92,7 @@
   ~RuntimeTableFunction() final;
 
   base::Status Init(int argc, const char* const* argv, Schema*) final;
-  std::unique_ptr<SqliteTable::BaseCursor> CreateCursor() final;
+  std::unique_ptr<SqliteTableLegacy::BaseCursor> CreateCursor() final;
   int BestIndex(const QueryConstraints& qc, BestIndexInfo* info) final;
 
  private:
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/base64.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/base64.cc
index 9c16baf..cc0eddc 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/base64.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/base64.cc
@@ -39,7 +39,7 @@
       return base::ErrStatus("BASE64: expected one arg but got %zu", argc);
     }
 
-    auto in = sqlite_utils::SqliteValueToSqlValue(argv[0]);
+    auto in = sqlite::utils::SqliteValueToSqlValue(argv[0]);
 
     const char* src = nullptr;
     size_t src_size = 0;
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/create_function.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/create_function.cc
index 18394d7..a7fc1d4 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/create_function.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/create_function.cc
@@ -38,7 +38,7 @@
                                  sqlite3_value** argv,
                                  SqlValue&,
                                  Destructors&) {
-  RETURN_IF_ERROR(sqlite_utils::CheckArgCount("CREATE_FUNCTION", argc, 3u));
+  RETURN_IF_ERROR(sqlite::utils::CheckArgCount("CREATE_FUNCTION", argc, 3u));
 
   sqlite3_value* prototype_value = argv[0];
   sqlite3_value* return_type_value = argv[1];
@@ -48,7 +48,7 @@
   {
     auto type_check = [prototype_value](sqlite3_value* value,
                                         SqlValue::Type type, const char* desc) {
-      base::Status status = sqlite_utils::TypeCheckSqliteValue(value, type);
+      base::Status status = sqlite::utils::TypeCheckSqliteValue(value, type);
       if (!status.ok()) {
         return base::ErrStatus("CREATE_FUNCTION[prototype=%s]: %s %s",
                                sqlite3_value_text(prototype_value), desc,
@@ -85,9 +85,10 @@
                                       sqlite3_value** argv,
                                       SqlValue&,
                                       Destructors&) {
-  RETURN_IF_ERROR(sqlite_utils::CheckArgCount("EXPERIMENTAL_MEMOIZE", argc, 1));
+  RETURN_IF_ERROR(
+      sqlite::utils::CheckArgCount("EXPERIMENTAL_MEMOIZE", argc, 1));
   base::StatusOr<std::string> function_name =
-      sqlite_utils::ExtractStringArg("MEMOIZE", "function_name", argv[0]);
+      sqlite::utils::ExtractStringArg("MEMOIZE", "function_name", argv[0]);
   RETURN_IF_ERROR(function_name.status());
   return engine->EnableSqlFunctionMemoization(*function_name);
 }
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.cc
index 1a19bd4..44782e7 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.cc
@@ -54,7 +54,7 @@
   {
     auto type_check = [prototype_value](sqlite3_value* value,
                                         SqlValue::Type type, const char* desc) {
-      base::Status status = sqlite_utils::TypeCheckSqliteValue(value, type);
+      base::Status status = sqlite::utils::TypeCheckSqliteValue(value, type);
       if (!status.ok()) {
         return base::ErrStatus("CREATE_VIEW_FUNCTION[prototype=%s]: %s %s",
                                sqlite3_value_text(prototype_value), desc,
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/import.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/import.cc
index 30d363e..a660723 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/import.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/import.cc
@@ -48,8 +48,8 @@
 
   // Type check
   {
-    base::Status status =
-        sqlite_utils::TypeCheckSqliteValue(import_val, SqlValue::Type::kString);
+    base::Status status = sqlite::utils::TypeCheckSqliteValue(
+        import_val, SqlValue::Type::kString);
     if (!status.ok()) {
       return base::ErrStatus("IMPORT(%s): %s", sqlite3_value_text(import_val),
                              status.c_message());
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.cc
index cc5cc8a..3384376 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.cc
@@ -14,10 +14,17 @@
 
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.h"
 
+#include <cstddef>
+#include <cstdint>
 #include <queue>
 #include <vector>
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/status_or.h"
 #include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_window_function.h"
 #include "src/trace_processor/sqlite/sqlite_utils.h"
 #include "src/trace_processor/util/status_macros.h"
 
@@ -118,7 +125,7 @@
 
 base::StatusOr<SlicePacker*> GetOrCreateAggregationContext(
     sqlite3_context* ctx) {
-  SlicePacker** packer = static_cast<SlicePacker**>(
+  auto** packer = static_cast<SlicePacker**>(
       sqlite3_aggregate_context(ctx, sizeof(SlicePacker*)));
   if (!packer) {
     return base::ErrStatus("Failed to allocate aggregate context");
@@ -130,74 +137,68 @@
   return *packer;
 }
 
-base::Status Step(sqlite3_context* ctx, size_t argc, sqlite3_value** argv) {
+base::Status StepStatus(sqlite3_context* ctx,
+                        size_t argc,
+                        sqlite3_value** argv) {
   base::StatusOr<SlicePacker*> slice_packer =
       GetOrCreateAggregationContext(ctx);
   RETURN_IF_ERROR(slice_packer.status());
 
   base::StatusOr<SqlValue> ts =
-      sqlite_utils::ExtractArgument(argc, argv, "ts", 0, SqlValue::kLong);
+      sqlite::utils::ExtractArgument(argc, argv, "ts", 0, SqlValue::kLong);
   RETURN_IF_ERROR(ts.status());
 
   base::StatusOr<SqlValue> dur =
-      sqlite_utils::ExtractArgument(argc, argv, "dur", 1, SqlValue::kLong);
+      sqlite::utils::ExtractArgument(argc, argv, "dur", 1, SqlValue::kLong);
   RETURN_IF_ERROR(dur.status());
 
   return slice_packer.value()->AddSlice(ts->AsLong(), dur.value().AsLong());
 }
 
-void StepWrapper(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
-  PERFETTO_CHECK(argc >= 0);
+struct InternalLayout : public SqliteWindowFunction {
+  static void Step(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
+    PERFETTO_CHECK(argc >= 0);
 
-  base::Status status = Step(ctx, static_cast<size_t>(argc), argv);
-  if (!status.ok()) {
-    sqlite_utils::SetSqliteError(ctx, kFunctionName, status);
-    return;
+    base::Status status = StepStatus(ctx, static_cast<size_t>(argc), argv);
+    if (!status.ok()) {
+      return sqlite::utils::SetError(ctx, kFunctionName, status);
+    }
   }
-}
 
-void FinalWrapper(sqlite3_context* ctx) {
-  SlicePacker** slice_packer = static_cast<SlicePacker**>(
-      sqlite3_aggregate_context(ctx, sizeof(SlicePacker*)));
-  if (!slice_packer || !*slice_packer) {
-    return;
-  }
-  sqlite3_result_int64(ctx,
-                       static_cast<int64_t>((*slice_packer)->GetLastDepth()));
-  delete *slice_packer;
-}
-
-void ValueWrapper(sqlite3_context* ctx) {
-  base::StatusOr<SlicePacker*> slice_packer =
-      GetOrCreateAggregationContext(ctx);
-  if (!slice_packer.ok()) {
-    sqlite_utils::SetSqliteError(ctx, kFunctionName, slice_packer.status());
-    return;
-  }
-  sqlite3_result_int64(
-      ctx, static_cast<int64_t>(slice_packer.value()->GetLastDepth()));
-}
-
-void InverseWrapper(sqlite3_context* ctx, int, sqlite3_value**) {
-  sqlite_utils::SetSqliteError(ctx, kFunctionName, base::ErrStatus(R"(
+  static void Inverse(sqlite3_context* ctx, int, sqlite3_value**) {
+    sqlite::utils::SetError(ctx, kFunctionName, base::ErrStatus(R"(
 The inverse step is not supported: the window clause should be
 "BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW".
 )"));
-}
+  }
+
+  static void Value(sqlite3_context* ctx) {
+    base::StatusOr<SlicePacker*> slice_packer =
+        GetOrCreateAggregationContext(ctx);
+    if (!slice_packer.ok()) {
+      return sqlite::utils::SetError(ctx, kFunctionName, slice_packer.status());
+    }
+    return sqlite::result::Long(
+        ctx, static_cast<int64_t>(slice_packer.value()->GetLastDepth()));
+  }
+
+  static void Final(sqlite3_context* ctx) {
+    auto** slice_packer = static_cast<SlicePacker**>(
+        sqlite3_aggregate_context(ctx, sizeof(SlicePacker*)));
+    if (!slice_packer || !*slice_packer) {
+      return;
+    }
+    sqlite::result::Long(ctx,
+                         static_cast<int64_t>((*slice_packer)->GetLastDepth()));
+    delete *slice_packer;
+  }
+};
 
 }  // namespace
 
-base::Status LayoutFunctions::Register(sqlite3* db,
-                                       TraceProcessorContext* context) {
-  int flags = SQLITE_UTF8 | SQLITE_DETERMINISTIC;
-  int ret = sqlite3_create_window_function(
-      db, kFunctionName, 2, flags, context, StepWrapper, FinalWrapper,
-      ValueWrapper, InverseWrapper, nullptr);
-  if (ret != SQLITE_OK) {
-    return base::ErrStatus("Unable to register function with name %s",
-                           kFunctionName);
-  }
-  return base::OkStatus();
+base::Status RegisterLayoutFunctions(PerfettoSqlEngine& engine) {
+  return engine.RegisterSqliteWindowFunction<InternalLayout>(kFunctionName, 2,
+                                                             nullptr);
 }
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.h b/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.h
index 8d3543c..3b1dc4c 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.h
@@ -18,6 +18,7 @@
 #include <sqlite3.h>
 
 #include "perfetto/base/status.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 
 namespace perfetto::trace_processor {
 
@@ -26,14 +27,12 @@
 // Implements INTERNAL_LAYOUT(ts, dur) window aggregate function.
 // This function takes a set of slices (ordered by ts) and computes depths
 // allowing them to be displayed on a single track in a non-overlapping manner,
-// while trying to minimising total height.
+// while trying to minimise the total height.
 //
 // TODO(altimin): this should support grouping sets of sets of slices (aka
 // "tracks") by passing 'track_id' parameter. The complication is that we will
 // need to know the max depth for each "track", so it's punted for now.
-struct LayoutFunctions {
-  static base::Status Register(sqlite3* db, TraceProcessorContext* context);
-};
+base::Status RegisterLayoutFunctions(PerfettoSqlEngine& engine);
 
 }  // namespace perfetto::trace_processor
 
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc
index c3126e0..8fda79e 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc
@@ -21,6 +21,7 @@
 #include <cstddef>
 #include <cstdint>
 #include <limits>
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -28,10 +29,12 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/utils.h"
 #include "perfetto/protozero/packed_repeated_fields.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/status.h"
 #include "protos/perfetto/trace_processor/stack.pbzero.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 #include "src/trace_processor/sqlite/sqlite_utils.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/util/profile_builder.h"
@@ -41,8 +44,7 @@
 // should cache this somewhere maybe even have a helper table that stores all
 // this data.
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 namespace {
 
 using protos::pbzero::Stack;
@@ -69,8 +71,8 @@
   base::Status Step(size_t argc, sqlite3_value** argv) {
     RETURN_IF_ERROR(UpdateSampleValue(argc, argv));
 
-    base::StatusOr<SqlValue> value =
-        sqlite_utils::ExtractArgument(argc, argv, "stack", 0, SqlValue::kBytes);
+    base::StatusOr<SqlValue> value = sqlite::utils::ExtractArgument(
+        argc, argv, "stack", 0, SqlValue::kBytes);
     if (!value.ok()) {
       return value.status();
     }
@@ -78,7 +80,7 @@
     Stack::Decoder stack(static_cast<const uint8_t*>(value->bytes_value),
                          value->bytes_count);
     if (stack.bytes_left() != 0) {
-      return sqlite_utils::ToInvalidArgumentError(
+      return sqlite::utils::ToInvalidArgumentError(
           "stack", 0, base::ErrStatus("failed to deserialize Stack proto"));
     }
     if (!builder_.AddSample(stack, sample_values_)) {
@@ -94,8 +96,8 @@
     std::unique_ptr<uint8_t[], base::FreeDeleter> data(
         static_cast<uint8_t*>(malloc(profile_proto.size())));
     memcpy(data.get(), profile_proto.data(), profile_proto.size());
-    sqlite3_result_blob(ctx, data.release(),
-                        static_cast<int>(profile_proto.size()), free);
+    return sqlite::result::RawBytes(
+        ctx, data.release(), static_cast<int>(profile_proto.size()), free);
   }
 
  private:
@@ -109,13 +111,13 @@
     }
 
     for (size_t i = 1; i < argc; i += 3) {
-      base::StatusOr<SqlValue> type = sqlite_utils::ExtractArgument(
+      base::StatusOr<SqlValue> type = sqlite::utils::ExtractArgument(
           argc, argv, "sample_type", i, SqlValue::kString);
       if (!type.ok()) {
         return type.status();
       }
 
-      base::StatusOr<SqlValue> units = sqlite_utils::ExtractArgument(
+      base::StatusOr<SqlValue> units = sqlite::utils::ExtractArgument(
           argc, argv, "sample_units", i + 1, SqlValue::kString);
       if (!units.ok()) {
         return units.status();
@@ -140,7 +142,7 @@
 
     PERFETTO_CHECK(argc == 1 + (sample_values_.size() * 3));
     for (size_t i = 0; i < sample_values_.size(); ++i) {
-      base::StatusOr<SqlValue> value = sqlite_utils::ExtractArgument(
+      base::StatusOr<SqlValue> value = sqlite::utils::ExtractArgument(
           argc, argv, "sample_value", 3 + i * 3, SqlValue::kLong);
       if (!value.ok()) {
         return value.status();
@@ -155,17 +157,17 @@
   std::vector<int64_t> sample_values_;
 };
 
-static base::Status Step(sqlite3_context* ctx,
-                         size_t argc,
-                         sqlite3_value** argv) {
-  AggregateContext** agg_context_ptr = static_cast<AggregateContext**>(
+base::Status StepStatus(sqlite3_context* ctx,
+                        size_t argc,
+                        sqlite3_value** argv) {
+  auto** agg_context_ptr = static_cast<AggregateContext**>(
       sqlite3_aggregate_context(ctx, sizeof(AggregateContext*)));
   if (!agg_context_ptr) {
     return base::ErrStatus("Failed to allocate aggregate context");
   }
 
   if (!*agg_context_ptr) {
-    TraceProcessorContext* tp_context =
+    auto* tp_context =
         static_cast<TraceProcessorContext*>(sqlite3_user_data(ctx));
     base::StatusOr<std::unique_ptr<AggregateContext>> agg_context =
         AggregateContext::Create(tp_context, argc, argv);
@@ -179,43 +181,39 @@
   return (*agg_context_ptr)->Step(argc, argv);
 }
 
-static void StepWrapper(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
-  PERFETTO_CHECK(argc >= 0);
+struct ProfileBuilder {
+  using Context = TraceProcessorContext;
 
-  base::Status status = Step(ctx, static_cast<size_t>(argc), argv);
+  static void Step(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
+    PERFETTO_CHECK(argc >= 0);
 
-  if (!status.ok()) {
-    sqlite_utils::SetSqliteError(ctx, kFunctionName, status);
-  }
-}
+    base::Status status = StepStatus(ctx, static_cast<size_t>(argc), argv);
 
-static void FinalWrapper(sqlite3_context* ctx) {
-  AggregateContext** agg_context_ptr =
-      static_cast<AggregateContext**>(sqlite3_aggregate_context(ctx, 0));
-
-  if (!agg_context_ptr) {
-    return;
+    if (!status.ok()) {
+      sqlite::utils::SetError(ctx, kFunctionName, status);
+    }
   }
 
-  (*agg_context_ptr)->Final(ctx);
+  static void Final(sqlite3_context* ctx) {
+    auto** agg_context_ptr =
+        static_cast<AggregateContext**>(sqlite3_aggregate_context(ctx, 0));
 
-  delete (*agg_context_ptr);
-}
+    if (!agg_context_ptr) {
+      return;
+    }
+
+    (*agg_context_ptr)->Final(ctx);
+
+    delete (*agg_context_ptr);
+  }
+};
 
 }  // namespace
 
-base::Status PprofFunctions::Register(sqlite3* db,
+base::Status PprofFunctions::Register(PerfettoSqlEngine& engine,
                                       TraceProcessorContext* context) {
-  int flags = SQLITE_UTF8 | SQLITE_DETERMINISTIC;
-  int ret =
-      sqlite3_create_function_v2(db, kFunctionName, -1, flags, context, nullptr,
-                                 StepWrapper, FinalWrapper, nullptr);
-  if (ret != SQLITE_OK) {
-    return base::ErrStatus("Unable to register function with name %s",
-                           kFunctionName);
-  }
-  return base::OkStatus();
+  return engine.RegisterSqliteAggregateFunction<ProfileBuilder>(kFunctionName,
+                                                                -1, context);
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.h b/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.h
index 24e6d72..ca5b459 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.h
@@ -20,17 +20,16 @@
 #include <sqlite3.h>
 
 #include "perfetto/base/status.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
 struct PprofFunctions {
-  static base::Status Register(sqlite3* db, TraceProcessorContext* context);
+  static base::Status Register(PerfettoSqlEngine&, TraceProcessorContext*);
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_FUNCTIONS_PPROF_FUNCTIONS_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.cc
index 04a9996..9553947 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.cc
@@ -16,8 +16,9 @@
 
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.h"
 
-namespace perfetto {
-namespace trace_processor {
+#include "perfetto/base/status.h"
+
+namespace perfetto::trace_processor {
 
 base::Status SqlFunction::VerifyPostConditions(Context*) {
   return base::OkStatus();
@@ -25,5 +26,4 @@
 
 void SqlFunction::Cleanup(Context*) {}
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/sqlite3_str_split.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/sqlite3_str_split.cc
index 3cd9325..f504b2a 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/sqlite3_str_split.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/sqlite3_str_split.cc
@@ -32,27 +32,27 @@
                       sqlite3_value** argv) {
   PERFETTO_DCHECK(argc == 3);
   if (sqlite3_value_type(argv[1]) != SQLITE_TEXT) {
-    sqlite3_result_error(context, kDelimiterError, -1);
+    sqlite::result::Error(context, kDelimiterError);
     return;
   }
   const char* delimiter =
       reinterpret_cast<const char*>(sqlite3_value_text(argv[1]));
   const size_t delimiter_len = strlen(delimiter);
   if (delimiter_len == 0) {
-    sqlite3_result_error(context, kDelimiterError, -1);
+    sqlite::result::Error(context, kDelimiterError);
     return;
   }
   if (sqlite3_value_type(argv[2]) != SQLITE_INTEGER) {
-    sqlite3_result_error(context, kSplitFieldIndexError, -1);
+    sqlite::result::Error(context, kSplitFieldIndexError);
     return;
   }
   int fld = sqlite3_value_int(argv[2]);
   if (fld < 0) {
-    sqlite3_result_error(context, kSplitFieldIndexError, -1);
+    sqlite::result::Error(context, kSplitFieldIndexError);
     return;
   }
   if (sqlite3_value_type(argv[0]) != SQLITE_TEXT) {
-    sqlite3_result_null(context);
+    sqlite::result::Null(context);
     return;
   }
   const char* in = reinterpret_cast<const char*>(sqlite3_value_text(argv[0]));
@@ -62,7 +62,8 @@
     if (fld == 0) {
       int size = next != nullptr ? static_cast<int>(next - in)
                                  : static_cast<int>(strlen(in));
-      sqlite3_result_text(context, in, size, sqlite_utils::kSqliteTransient);
+      sqlite::result::RawString(context, in, size,
+                                sqlite::utils::kSqliteTransient);
       return;
     } else if (next == nullptr) {
       break;
@@ -70,7 +71,7 @@
     in = next + delimiter_len;
     --fld;
   } while (fld >= 0);
-  sqlite3_result_null(context);
+  sqlite::result::Null(context);
 }
 }  // namespace
 
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc
index 3fa7bd0..1babe3d 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc
@@ -87,7 +87,7 @@
     // Stack expects the opposite, thus iterates the args in reverse order.
     for (size_t i = argc; i > 0; --i) {
       size_t arg_index = i - 1;
-      SqlValue value = sqlite_utils::SqliteValueToSqlValue(argv[arg_index]);
+      SqlValue value = sqlite::utils::SqliteValueToSqlValue(argv[arg_index]);
       switch (value.type) {
         case SqlValue::kBytes: {
           stack->AppendRawProtoBytes(value.bytes_value, value.bytes_count);
@@ -101,7 +101,7 @@
           break;
         case SqlValue::kLong:
         case SqlValue::kDouble:
-          return sqlite_utils::InvalidArgumentTypeError(
+          return sqlite::utils::InvalidArgumentTypeError(
               "entry", arg_index, value.type, SqlValue::kBytes,
               SqlValue::kString, SqlValue::kNull);
       }
@@ -148,7 +148,7 @@
           kFunctionName, argc);
     }
 
-    base::StatusOr<SqlValue> value = sqlite_utils::ExtractArgument(
+    base::StatusOr<SqlValue> value = sqlite::utils::ExtractArgument(
         argc, argv, "callsite_id", 0, SqlValue::kNull, SqlValue::kLong);
     if (!value.ok()) {
       return value.status();
@@ -162,7 +162,7 @@
              .FindById(tables::StackProfileCallsiteTable::Id(
                  static_cast<uint32_t>(value->AsLong())))
              .has_value()) {
-      return sqlite_utils::ToInvalidArgumentError(
+      return sqlite::utils::ToInvalidArgumentError(
           "callsite_id", 0,
           base::ErrStatus("callsite_id does not exist: %" PRId64,
                           value->AsLong()));
@@ -172,8 +172,8 @@
 
     bool annotate = false;
     if (argc == 2) {
-      value = sqlite_utils::ExtractArgument(argc, argv, "annotate", 1,
-                                            SqlValue::Type::kLong);
+      value = sqlite::utils::ExtractArgument(argc, argv, "annotate", 1,
+                                             SqlValue::Type::kLong);
       if (!value.ok()) {
         return value.status();
       }
@@ -215,7 +215,7 @@
                               sqlite3_value** argv,
                               SqlValue& out,
                               Destructors& destructors) {
-    base::StatusOr<SqlValue> value = sqlite_utils::ExtractArgument(
+    base::StatusOr<SqlValue> value = sqlite::utils::ExtractArgument(
         argc, argv, "frame_id", 0, SqlValue::kNull, SqlValue::kLong);
 
     if (!value.ok()) {
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h b/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
index 57bd8dc..cc502ff 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
@@ -230,13 +230,13 @@
   }
 
   base::Status status =
-      sqlite_utils::TypeCheckSqliteValue(argv[0], SqlValue::kString);
+      sqlite::utils::TypeCheckSqliteValue(argv[0], SqlValue::kString);
   if (!status.ok()) {
     return base::ErrStatus("WRITE_FILE: argument 1, filename; %s",
                            status.c_message());
   }
 
-  status = sqlite_utils::TypeCheckSqliteValue(argv[1], SqlValue::kBytes);
+  status = sqlite::utils::TypeCheckSqliteValue(argv[1], SqlValue::kBytes);
   if (!status.ok()) {
     return base::ErrStatus("WRITE_FILE: argument 2, content; %s",
                            status.c_message());
@@ -305,7 +305,7 @@
 
   // This function always returns static strings (i.e. scoped to lifetime
   // of the TraceStorage thread pool) so prevent SQLite from making copies.
-  destructors.string_destructor = sqlite_utils::kSqliteStatic;
+  destructors.string_destructor = sqlite::utils::kSqliteStatic;
 
   switch (opt_value->type) {
     case Variadic::kNull:
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/window_functions.h b/src/trace_processor/perfetto_sql/intrinsics/functions/window_functions.h
index 3b90896..c9f606d 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/window_functions.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/window_functions.h
@@ -18,18 +18,16 @@
 #define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_FUNCTIONS_WINDOW_FUNCTIONS_H_
 
 #include <sqlite3.h>
-#include <unordered_map>
-#include "perfetto/ext/base/base64.h"
-#include "perfetto/ext/base/file_utils.h"
-#include "perfetto/ext/trace_processor/demangle.h"
-#include "protos/perfetto/common/builtin_clock.pbzero.h"
-#include "src/trace_processor/export_json.h"
-#include "src/trace_processor/importers/common/clock_tracker.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.h"
-#include "src/trace_processor/util/status_macros.h"
+#include <cstdint>
+#include <type_traits>
 
-namespace perfetto {
-namespace trace_processor {
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_window_function.h"
+
+namespace perfetto::trace_processor {
+
 // Keeps track of the latest non null value and its position withing the
 // window. Every time the window shrinks (`xInverse` is called) the window size
 // is reduced by one and the position of the value moves one back, if it gets
@@ -89,66 +87,59 @@
   sqlite3_value* last_non_null_value_;
 };
 
-static_assert(std::is_standard_layout<LastNonNullAggregateContext>::value,
+static_assert(std::is_standard_layout_v<LastNonNullAggregateContext>,
               "Must be able to be initialized by sqlite3_aggregate_context "
               "(similar to calloc, i.e. no constructor called)");
-static_assert(std::is_trivial<LastNonNullAggregateContext>::value,
+static_assert(std::is_trivial_v<LastNonNullAggregateContext>,
               "Must be able to be destroyed by just calling free (i.e. no "
               "destructor called)");
 
-inline void LastNonNullStep(sqlite3_context* ctx,
-                            int argc,
-                            sqlite3_value** argv) {
-  if (argc != 1) {
-    sqlite3_result_error(
-        ctx, "Unsupported number of args passed to LAST_NON_NULL", -1);
-    return;
+class LastNonNull : public SqliteWindowFunction {
+ public:
+  static void Step(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
+    if (argc != 1) {
+      return sqlite::result::Error(
+          ctx, "Unsupported number of args passed to LAST_NON_NULL");
+    }
+
+    auto* ptr = LastNonNullAggregateContext::GetOrCreate(ctx);
+    if (!ptr) {
+      return sqlite::result::Error(ctx,
+                                   "LAST_NON_NULL: Failed to allocate context");
+    }
+
+    ptr->PushBack(argv[0]);
   }
 
-  auto* ptr = LastNonNullAggregateContext::GetOrCreate(ctx);
-  if (!ptr) {
-    sqlite3_result_error(ctx, "LAST_NON_NULL: Failed to allocate context", -1);
-    return;
+  static void Inverse(sqlite3_context* ctx, int, sqlite3_value**) {
+    auto* ptr = LastNonNullAggregateContext::GetOrCreate(ctx);
+    PERFETTO_CHECK(ptr != nullptr);
+    ptr->PopFront();
   }
 
-  ptr->PushBack(argv[0]);
-}
-
-inline void LastNonNullInverse(sqlite3_context* ctx, int, sqlite3_value**) {
-  auto* ptr = LastNonNullAggregateContext::GetOrCreate(ctx);
-  PERFETTO_CHECK(ptr != nullptr);
-  ptr->PopFront();
-}
-
-inline void LastNonNullValue(sqlite3_context* ctx) {
-  auto* ptr = LastNonNullAggregateContext::GetOrCreate(ctx);
-  if (!ptr || !ptr->last_non_null_value()) {
-    sqlite3_result_null(ctx);
-  } else {
+  static void Value(sqlite3_context* ctx) {
+    auto* ptr = LastNonNullAggregateContext::GetOrCreate(ctx);
+    if (!ptr || !ptr->last_non_null_value()) {
+      return sqlite::result::Null(ctx);
+    }
     sqlite3_result_value(ctx, ptr->last_non_null_value());
   }
-}
 
-inline void LastNonNullFinal(sqlite3_context* ctx) {
-  auto* ptr = LastNonNullAggregateContext::Get(ctx);
-  if (!ptr || !ptr->last_non_null_value()) {
-    sqlite3_result_null(ctx);
-  } else {
-    sqlite3_result_value(ctx, ptr->last_non_null_value());
+  static void Final(sqlite3_context* ctx) {
+    auto* ptr = LastNonNullAggregateContext::Get(ctx);
+    if (!ptr || !ptr->last_non_null_value()) {
+      return sqlite::result::Null(ctx);
+    }
+    sqlite::result::Value(ctx, ptr->last_non_null_value());
     ptr->Destroy();
   }
+};
+
+inline base::Status RegisterLastNonNullFunction(PerfettoSqlEngine& engine) {
+  return engine.RegisterSqliteWindowFunction<LastNonNull>("LAST_NON_NULL", 1,
+                                                          nullptr);
 }
 
-inline void RegisterLastNonNullFunction(sqlite3* db) {
-  auto ret = sqlite3_create_window_function(
-      db, "LAST_NON_NULL", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr,
-      &LastNonNullStep, &LastNonNullFinal, &LastNonNullValue,
-      &LastNonNullInverse, nullptr);
-  if (ret) {
-    PERFETTO_ELOG("Error initializing LAST_NON_NULL");
-  }
-}
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_FUNCTIONS_WINDOW_FUNCTIONS_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc
index a1d9630..66ffa4b 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc
@@ -47,7 +47,7 @@
 }
 
 std::optional<std::string> HasDuplicateColumns(
-    const std::vector<SqliteTable::Column>& cols) {
+    const std::vector<SqliteTableLegacy::Column>& cols) {
   std::set<std::string> names;
   for (const auto& col : cols) {
     if (names.count(col.name()) > 0)
@@ -180,7 +180,7 @@
   if (!status.ok())
     return status;
 
-  std::vector<SqliteTable::Column> cols;
+  std::vector<SqliteTableLegacy::Column> cols;
   // Ensure the shared columns are consistently ordered and are not
   // present twice in the final schema
   cols.emplace_back(Column::kTimestamp, kTsColumnName, SqlValue::Type::kLong);
@@ -217,7 +217,7 @@
 
 void SpanJoinOperatorTable::CreateSchemaColsForDefn(
     const TableDefinition& defn,
-    std::vector<SqliteTable::Column>* cols) {
+    std::vector<SqliteTableLegacy::Column>* cols) {
   for (size_t i = 0; i < defn.columns().size(); i++) {
     const auto& n = defn.columns()[i].name();
     if (IsRequiredColumn(n) || n == defn.partition_col())
@@ -231,7 +231,8 @@
   }
 }
 
-std::unique_ptr<SqliteTable::BaseCursor> SpanJoinOperatorTable::CreateCursor() {
+std::unique_ptr<SqliteTableLegacy::BaseCursor>
+SpanJoinOperatorTable::CreateCursor() {
   return std::unique_ptr<SpanJoinOperatorTable::Cursor>(
       new Cursor(this, engine_));
 }
@@ -274,7 +275,7 @@
                                         void**) {
   if (base::CaseInsensitiveEqual(name, "source_geq")) {
     *fn = [](sqlite3_context* ctx, int, sqlite3_value**) {
-      sqlite3_result_error(ctx, "Should not be called.", -1);
+      return sqlite::result::Error(ctx, "Should not be called.");
     };
     return kSourceGeqOpCode;
   }
@@ -297,7 +298,7 @@
     // affect the span join computation. Similarily, source_geq constraints
     // explicitly request that they are passed as geq constraints to the source
     // tables.
-    if (col_name == kTsColumnName && !sqlite_utils::IsOpLe(cs.op) &&
+    if (col_name == kTsColumnName && !sqlite::utils::IsOpLe(cs.op) &&
         cs.op != kSourceGeqOpCode)
       continue;
 
@@ -331,8 +332,8 @@
         desc.name.c_str());
   }
 
-  std::vector<SqliteTable::Column> cols;
-  RETURN_IF_ERROR(sqlite_utils::GetColumnsForTable(
+  std::vector<SqliteTableLegacy::Column> cols;
+  RETURN_IF_ERROR(sqlite::utils::GetColumnsForTable(
       engine_->sqlite_engine()->db(), desc.name, cols));
 
   uint32_t required_columns_found = 0;
@@ -396,7 +397,7 @@
 
 SpanJoinOperatorTable::Cursor::Cursor(SpanJoinOperatorTable* table,
                                       PerfettoSqlEngine* engine)
-    : SqliteTable::BaseCursor(table),
+    : SqliteTableLegacy::BaseCursor(table),
       t1_(table, &table->t1_defn_, engine),
       t2_(table, &table->t2_defn_, engine),
       table_(table) {}
@@ -558,14 +559,14 @@
   switch (N) {
     case Column::kTimestamp: {
       auto max_ts = std::max(t1_.ts(), t2_.ts());
-      sqlite3_result_int64(context, static_cast<sqlite3_int64>(max_ts));
+      sqlite::result::Long(context, static_cast<sqlite3_int64>(max_ts));
       break;
     }
     case Column::kDuration: {
       auto max_start = std::max(t1_.ts(), t2_.ts());
       auto min_end = std::min(t1_.raw_ts_end(), t2_.raw_ts_end());
       auto dur = min_end - max_start;
-      sqlite3_result_int64(context, static_cast<sqlite3_int64>(dur));
+      sqlite::result::Long(context, static_cast<sqlite3_int64>(dur));
       break;
     }
     case Column::kPartition: {
@@ -576,7 +577,7 @@
         } else {
           partition = t1_.IsReal() ? t1_.partition() : t2_.partition();
         }
-        sqlite3_result_int64(context, static_cast<sqlite3_int64>(partition));
+        sqlite::result::Long(context, static_cast<sqlite3_int64>(partition));
         break;
       }
       PERFETTO_FALLTHROUGH;
@@ -783,7 +784,7 @@
 std::string SpanJoinOperatorTable::Query::CreateSqlQuery(
     const std::vector<std::string>& cs) const {
   std::vector<std::string> col_names;
-  for (const SqliteTable::Column& c : defn_->columns()) {
+  for (const SqliteTableLegacy::Column& c : defn_->columns()) {
     col_names.push_back("`" + c.name() + "`");
   }
 
@@ -803,33 +804,29 @@
 
 void SpanJoinOperatorTable::Query::ReportSqliteResult(sqlite3_context* context,
                                                       size_t index) {
-  const auto kSqliteTransient = reinterpret_cast<sqlite3_destructor_type>(-1);
   if (state_ != State::kReal) {
-    sqlite3_result_null(context);
-    return;
+    return sqlite::result::Null(context);
   }
 
   sqlite3_stmt* stmt = stmt_->sqlite_stmt();
   int idx = static_cast<int>(index);
   switch (sqlite3_column_type(stmt, idx)) {
     case SQLITE_INTEGER:
-      sqlite3_result_int64(context, sqlite3_column_int64(stmt, idx));
-      break;
+      return sqlite::result::Long(context, sqlite3_column_int64(stmt, idx));
     case SQLITE_FLOAT:
-      sqlite3_result_double(context, sqlite3_column_double(stmt, idx));
-      break;
+      return sqlite::result::Double(context, sqlite3_column_double(stmt, idx));
     case SQLITE_TEXT: {
       // TODO(lalitm): note for future optimizations: if we knew the addresses
       // of the string intern pool, we could check if the string returned here
       // comes from the pool, and pass it as non-transient.
-      auto ptr = reinterpret_cast<const char*>(sqlite3_column_text(stmt, idx));
-      sqlite3_result_text(context, ptr, -1, kSqliteTransient);
-      break;
+      const auto* ptr =
+          reinterpret_cast<const char*>(sqlite3_column_text(stmt, idx));
+      return sqlite::result::TransientString(context, ptr);
     }
     case SQLITE_BLOB: {
-      sqlite3_result_blob(context, sqlite3_column_blob(stmt, idx),
-                          sqlite3_column_bytes(stmt, idx), kSqliteTransient);
-      break;
+      return sqlite::result::TransientBytes(context,
+                                            sqlite3_column_blob(stmt, idx),
+                                            sqlite3_column_bytes(stmt, idx));
     }
   }
 }
@@ -837,7 +834,7 @@
 SpanJoinOperatorTable::TableDefinition::TableDefinition(
     std::string name,
     std::string partition_col,
-    std::vector<SqliteTable::Column> cols,
+    std::vector<SqliteTableLegacy::Column> cols,
     EmitShadowType emit_shadow_type,
     uint32_t ts_idx,
     uint32_t dur_idx,
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.h b/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.h
index 2c663c8..c734b7f 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.h
@@ -98,7 +98,7 @@
 
     TableDefinition(std::string name,
                     std::string partition_col,
-                    std::vector<SqliteTable::Column> cols,
+                    std::vector<SqliteTableLegacy::Column> cols,
                     EmitShadowType emit_shadow_type,
                     uint32_t ts_idx,
                     uint32_t dur_idx,
@@ -120,7 +120,9 @@
 
     const std::string& name() const { return name_; }
     const std::string& partition_col() const { return partition_col_; }
-    const std::vector<SqliteTable::Column>& columns() const { return cols_; }
+    const std::vector<SqliteTableLegacy::Column>& columns() const {
+      return cols_;
+    }
 
     uint32_t ts_idx() const { return ts_idx_; }
     uint32_t dur_idx() const { return dur_idx_; }
@@ -131,7 +133,7 @@
 
     std::string name_;
     std::string partition_col_;
-    std::vector<SqliteTable::Column> cols_;
+    std::vector<SqliteTableLegacy::Column> cols_;
 
     uint32_t ts_idx_ = std::numeric_limits<uint32_t>::max();
     uint32_t dur_idx_ = std::numeric_limits<uint32_t>::max();
@@ -323,7 +325,7 @@
   };
 
   // Base class for a cursor on the span table.
-  class Cursor final : public SqliteTable::BaseCursor {
+  class Cursor final : public SqliteTableLegacy::BaseCursor {
    public:
     Cursor(SpanJoinOperatorTable*, PerfettoSqlEngine*);
     ~Cursor() final;
@@ -361,8 +363,8 @@
   ~SpanJoinOperatorTable() final;
 
   // Table implementation.
-  util::Status Init(int, const char* const*, SqliteTable::Schema*) final;
-  std::unique_ptr<SqliteTable::BaseCursor> CreateCursor() final;
+  util::Status Init(int, const char* const*, SqliteTableLegacy::Schema*) final;
+  std::unique_ptr<SqliteTableLegacy::BaseCursor> CreateCursor() final;
   int BestIndex(const QueryConstraints& qc, BestIndexInfo* info) final;
   int FindFunction(const char* name, FindFunctionFn* fn, void** args) final;
 
@@ -430,7 +432,7 @@
                                           int global_column);
 
   void CreateSchemaColsForDefn(const TableDefinition& defn,
-                               std::vector<SqliteTable::Column>* cols);
+                               std::vector<SqliteTableLegacy::Column>* cols);
 
   TableDefinition t1_defn_;
   TableDefinition t2_defn_;
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator_unittest.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator_unittest.cc
index bcd3ea9..2695e43 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator_unittest.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator_unittest.cc
@@ -28,10 +28,11 @@
  public:
   SpanJoinOperatorTableTest() {
     engine_.sqlite_engine()->RegisterVirtualTableModule<SpanJoinOperatorTable>(
-        "span_join", &engine_, SqliteTable::TableType::kExplicitCreate, false);
-    engine_.sqlite_engine()->RegisterVirtualTableModule<SpanJoinOperatorTable>(
-        "span_left_join", &engine_, SqliteTable::TableType::kExplicitCreate,
+        "span_join", &engine_, SqliteTableLegacy::TableType::kExplicitCreate,
         false);
+    engine_.sqlite_engine()->RegisterVirtualTableModule<SpanJoinOperatorTable>(
+        "span_left_join", &engine_,
+        SqliteTableLegacy::TableType::kExplicitCreate, false);
   }
 
   void PrepareValidStatement(const std::string& sql) {
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.cc
index ba591a9..eb31bb3 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.cc
@@ -16,145 +16,204 @@
 
 #include "src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.h"
 
-#include "perfetto/base/status.h"
+#include <sqlite3.h>
+#include <cstdint>
+#include <memory>
+
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
 #include "src/trace_processor/sqlite/sqlite_utils.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 namespace {
-using namespace sqlite_utils;
-}  // namespace
-
-WindowOperatorTable::WindowOperatorTable(sqlite3*, const TraceStorage*) {}
-WindowOperatorTable::~WindowOperatorTable() = default;
-
-base::Status WindowOperatorTable::Init(int,
-                                       const char* const*,
-                                       Schema* schema) {
-  const bool kHidden = true;
-  *schema = Schema(
-      {
-          // These are the operator columns:
-          SqliteTable::Column(Column::kRowId, "rowid", SqlValue::Type::kLong,
-                              kHidden),
-          SqliteTable::Column(Column::kQuantum, "quantum",
-                              SqlValue::Type::kLong, kHidden),
-          SqliteTable::Column(Column::kWindowStart, "window_start",
-                              SqlValue::Type::kLong, kHidden),
-          SqliteTable::Column(Column::kWindowDur, "window_dur",
-                              SqlValue::Type::kLong, kHidden),
-          // These are the ouput columns:
-          SqliteTable::Column(Column::kTs, "ts", SqlValue::Type::kLong),
-          SqliteTable::Column(Column::kDuration, "dur", SqlValue::Type::kLong),
-          SqliteTable::Column(Column::kQuantumTs, "quantum_ts",
-                              SqlValue::Type::kLong),
-      },
-      {Column::kRowId});
-  return base::OkStatus();
+constexpr char kSchema[] = R"(
+    CREATE TABLE x(
+      rowid BIGINT HIDDEN,
+      quantum BIGINT HIDDEN,
+      window_start BIGINT HIDDEN,
+      window_dur BIGINT HIDDEN,
+      ts BIGINT,
+      dur BIGINT,
+      quantum_ts BIGINT,
+      PRIMARY KEY(rowid)
+    ) WITHOUT ROWID
+  )";
 }
 
-std::unique_ptr<SqliteTable::BaseCursor> WindowOperatorTable::CreateCursor() {
-  return std::unique_ptr<SqliteTable::BaseCursor>(new Cursor(this));
-}
+int WindowOperatorModule::Create(sqlite3* db,
+                                 void* raw_ctx,
+                                 int argc,
+                                 const char* const* argv,
+                                 sqlite3_vtab** vtab,
+                                 char**) {
+  PERFETTO_CHECK(argc == 3);
+  if (int ret = sqlite3_declare_vtab(db, kSchema); ret != SQLITE_OK) {
+    return ret;
+  }
+  auto* ctx = GetContext(raw_ctx);
+  auto it_and_inserted = ctx->state_by_name.Insert(argv[2], nullptr);
+  PERFETTO_CHECK(
+      it_and_inserted.second ||
+      (it_and_inserted.first && it_and_inserted.first->get()->disconnected));
+  *it_and_inserted.first = std::make_unique<State>();
 
-int WindowOperatorTable::BestIndex(const QueryConstraints&, BestIndexInfo*) {
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->context = ctx;
+  res->name = argv[2];
+  res->state = it_and_inserted.first->get();
+  *vtab = res.release();
   return SQLITE_OK;
 }
 
-base::Status WindowOperatorTable::ModifyConstraints(QueryConstraints* qc) {
-  // Remove ordering on timestamp if it is the only ordering as we are already
-  // sorted on TS. This makes span joining significantly faster.
-  const auto& ob = qc->order_by();
-  if (ob.size() == 1 && ob[0].iColumn == Column::kTs && !ob[0].desc) {
-    qc->mutable_order_by()->clear();
-  }
-  return base::OkStatus();
+int WindowOperatorModule::Destroy(sqlite3_vtab* vtab) {
+  auto* tab = GetVtab(vtab);
+  PERFETTO_CHECK(tab->context->state_by_name.Erase(tab->name));
+  delete tab;
+  return SQLITE_OK;
 }
 
-base::Status WindowOperatorTable::Update(int argc,
-                                         sqlite3_value** argv,
-                                         sqlite3_int64*) {
-  // We only support updates to ts and dur. Disallow deletes (argc == 1) and
-  // inserts (argv[0] == null).
-  if (argc < 2 || sqlite3_value_type(argv[0]) == SQLITE_NULL) {
-    return base::ErrStatus(
-        "Invalid number/value of arguments when updating window table");
+int WindowOperatorModule::Connect(sqlite3* db,
+                                  void* raw_ctx,
+                                  int argc,
+                                  const char* const* argv,
+                                  sqlite3_vtab** vtab,
+                                  char**) {
+  PERFETTO_CHECK(argc == 3);
+  if (int ret = sqlite3_declare_vtab(db, kSchema); ret != SQLITE_OK) {
+    return ret;
   }
+  auto* ctx = GetContext(raw_ctx);
+  auto* ptr = ctx->state_by_name.Find(argv[2]);
+  PERFETTO_CHECK(ptr);
+  ptr->get()->disconnected = false;
 
-  int64_t new_quantum = sqlite3_value_int64(argv[3]);
-  int64_t new_start = sqlite3_value_int64(argv[4]);
-  int64_t new_dur = sqlite3_value_int64(argv[5]);
-  if (new_dur == 0) {
-    return base::ErrStatus("Cannot set duration of window table to zero.");
-  }
-
-  quantum_ = new_quantum;
-  window_start_ = new_start;
-  window_dur_ = new_dur;
-
-  return base::OkStatus();
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->context = ctx;
+  res->name = argv[2];
+  res->state = ptr->get();
+  *vtab = res.release();
+  return SQLITE_OK;
 }
 
-WindowOperatorTable::Cursor::Cursor(WindowOperatorTable* table)
-    : SqliteTable::BaseCursor(table), table_(table) {}
-WindowOperatorTable::Cursor::~Cursor() = default;
+int WindowOperatorModule::Disconnect(sqlite3_vtab* vtab) {
+  auto* tab = GetVtab(vtab);
+  auto* ptr = tab->context->state_by_name.Find(tab->name);
+  PERFETTO_CHECK(ptr);
+  ptr->get()->disconnected = true;
+  delete tab;
+  return SQLITE_OK;
+}
 
-base::Status WindowOperatorTable::Cursor::Filter(const QueryConstraints& qc,
-                                                 sqlite3_value** argv,
-                                                 FilterHistory) {
-  *this = Cursor(table_);
-  window_start_ = table_->window_start_;
-  window_end_ = table_->window_start_ + table_->window_dur_;
-  step_size_ = table_->quantum_ == 0 ? table_->window_dur_ : table_->quantum_;
-
-  current_ts_ = window_start_;
+int WindowOperatorModule::BestIndex(sqlite3_vtab*, sqlite3_index_info* info) {
+  info->orderByConsumed = info->nOrderBy == 1 &&
+                          info->aOrderBy[0].iColumn == Column::kTs &&
+                          !info->aOrderBy[0].desc;
 
   // Set return first if there is a equals constraint on the row id asking to
   // return the first row.
-  bool return_first = qc.constraints().size() == 1 &&
-                      qc.constraints()[0].column == Column::kRowId &&
-                      IsOpEq(qc.constraints()[0].op) &&
-                      sqlite3_value_int(argv[0]) == 0;
-  if (return_first) {
-    filter_type_ = FilterType::kReturnFirst;
+  bool is_row_id_constraint = info->nConstraint == 1 &&
+                              info->aConstraint[0].iColumn == Column::kRowId &&
+                              info->aConstraint[0].usable &&
+                              sqlite::utils::IsOpEq(info->aConstraint[0].op);
+  if (is_row_id_constraint) {
+    info->idxNum = 1;
+    info->aConstraintUsage[0].argvIndex = 1;
   } else {
-    filter_type_ = FilterType::kReturnAll;
+    info->idxNum = 0;
   }
-  return base::OkStatus();
+  return SQLITE_OK;
 }
 
-base::Status WindowOperatorTable::Cursor::Column(sqlite3_context* context,
-                                                 int N) {
+int WindowOperatorModule::Open(sqlite3_vtab*, sqlite3_vtab_cursor** cursor) {
+  std::unique_ptr<Cursor> c = std::make_unique<Cursor>();
+  *cursor = c.release();
+  return SQLITE_OK;
+}
+
+int WindowOperatorModule::Close(sqlite3_vtab_cursor* cursor) {
+  delete GetCursor(cursor);
+  return SQLITE_OK;
+}
+
+int WindowOperatorModule::Filter(sqlite3_vtab_cursor* cursor,
+                                 int is_row_id_constraint,
+                                 const char*,
+                                 int argc,
+                                 sqlite3_value** argv) {
+  auto* t = GetVtab(cursor->pVtab);
+  auto* c = GetCursor(cursor);
+
+  c->window_start = t->state->window_start;
+  c->window_end = t->state->window_start + t->state->window_dur;
+  c->step_size =
+      t->state->quantum == 0 ? t->state->window_dur : t->state->quantum;
+
+  c->current_ts = c->window_start;
+
+  if (is_row_id_constraint) {
+    PERFETTO_CHECK(argc == 1);
+    c->filter_type = sqlite3_value_int(argv[0]) == 0 ? FilterType::kReturnFirst
+                                                     : FilterType::kReturnAll;
+  } else {
+    c->filter_type = FilterType::kReturnAll;
+  }
+  return SQLITE_OK;
+}
+
+int WindowOperatorModule::Next(sqlite3_vtab_cursor* cursor) {
+  auto* c = GetCursor(cursor);
+  switch (c->filter_type) {
+    case FilterType::kReturnFirst:
+      c->current_ts = c->window_end;
+      break;
+    case FilterType::kReturnAll:
+      c->current_ts += c->step_size;
+      c->quantum_ts++;
+      break;
+  }
+  c->row_id++;
+  return SQLITE_OK;
+}
+
+int WindowOperatorModule::Eof(sqlite3_vtab_cursor* cursor) {
+  auto* c = GetCursor(cursor);
+  return c->current_ts >= c->window_end;
+}
+
+int WindowOperatorModule::Column(sqlite3_vtab_cursor* cursor,
+                                 sqlite3_context* ctx,
+                                 int N) {
+  auto* t = GetVtab(cursor->pVtab);
+  auto* c = GetCursor(cursor);
   switch (N) {
     case Column::kQuantum: {
-      sqlite3_result_int64(context,
-                           static_cast<sqlite_int64>(table_->quantum_));
+      sqlite::result::Long(ctx, static_cast<sqlite_int64>(t->state->quantum));
       break;
     }
     case Column::kWindowStart: {
-      sqlite3_result_int64(context,
-                           static_cast<sqlite_int64>(table_->window_start_));
+      sqlite::result::Long(ctx,
+                           static_cast<sqlite_int64>(t->state->window_start));
       break;
     }
     case Column::kWindowDur: {
-      sqlite3_result_int(context, static_cast<int>(table_->window_dur_));
+      sqlite::result::Long(ctx, static_cast<int>(t->state->window_dur));
       break;
     }
     case Column::kTs: {
-      sqlite3_result_int64(context, static_cast<sqlite_int64>(current_ts_));
+      sqlite::result::Long(ctx, static_cast<sqlite_int64>(c->current_ts));
       break;
     }
     case Column::kDuration: {
-      sqlite3_result_int64(context, static_cast<sqlite_int64>(step_size_));
+      sqlite::result::Long(ctx, static_cast<sqlite_int64>(c->step_size));
       break;
     }
     case Column::kQuantumTs: {
-      sqlite3_result_int64(context, static_cast<sqlite_int64>(quantum_ts_));
+      sqlite::result::Long(ctx, static_cast<sqlite_int64>(c->quantum_ts));
       break;
     }
     case Column::kRowId: {
-      sqlite3_result_int64(context, static_cast<sqlite_int64>(row_id_));
+      sqlite::result::Long(ctx, static_cast<sqlite_int64>(c->row_id));
       break;
     }
     default: {
@@ -162,26 +221,39 @@
       break;
     }
   }
-  return base::OkStatus();
+  return SQLITE_OK;
 }
 
-base::Status WindowOperatorTable::Cursor::Next() {
-  switch (filter_type_) {
-    case FilterType::kReturnFirst:
-      current_ts_ = window_end_;
-      break;
-    case FilterType::kReturnAll:
-      current_ts_ += step_size_;
-      quantum_ts_++;
-      break;
+int WindowOperatorModule::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
+  return SQLITE_ERROR;
+}
+
+int WindowOperatorModule::Update(sqlite3_vtab* tab,
+                                 int argc,
+                                 sqlite3_value** argv,
+                                 sqlite_int64*) {
+  auto* t = GetVtab(tab);
+
+  // We only support updates to ts and dur. Disallow deletes (argc == 1) and
+  // inserts (argv[0] == null).
+  if (argc < 2 || sqlite3_value_type(argv[0]) == SQLITE_NULL) {
+    return sqlite::utils::SetError(
+        tab, "Invalid number/value of arguments when updating window table");
   }
-  row_id_++;
-  return base::OkStatus();
+
+  int64_t new_quantum = sqlite3_value_int64(argv[3]);
+  int64_t new_start = sqlite3_value_int64(argv[4]);
+  int64_t new_dur = sqlite3_value_int64(argv[5]);
+  if (new_dur == 0) {
+    return sqlite::utils::SetError(
+        tab, "Cannot set duration of window table to zero.");
+  }
+
+  t->state->quantum = new_quantum;
+  t->state->window_start = new_start;
+  t->state->window_dur = new_dur;
+
+  return SQLITE_OK;
 }
 
-bool WindowOperatorTable::Cursor::Eof() {
-  return current_ts_ >= window_end_;
-}
-
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.h b/src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.h
index 8e26961..d023042 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.h
@@ -17,20 +17,55 @@
 #ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_WINDOW_OPERATOR_H_
 #define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_WINDOW_OPERATOR_H_
 
+#include <cstdint>
 #include <limits>
 #include <memory>
+#include <string>
 
-#include "perfetto/base/status.h"
-#include "src/trace_processor/sqlite/sqlite_table.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceStorage;
 
-class WindowOperatorTable final
-    : public TypedSqliteTable<WindowOperatorTable, const TraceStorage*> {
- public:
+// Operator table which can emit spans of a configurable duration.
+struct WindowOperatorModule : sqlite::Module<WindowOperatorModule> {
+  // Defines the data to be generated by the table.
+  enum FilterType {
+    // Returns all the spans.
+    kReturnAll = 0,
+    // Only returns the first span of the table. Useful for UPDATE operations.
+    kReturnFirst = 1,
+  };
+  struct State {
+    bool disconnected = false;
+    int64_t quantum = 0;
+    int64_t window_start = 0;
+
+    // max of int64_t because SQLite technically only supports int64s and not
+    // uint64s.
+    int64_t window_dur = std::numeric_limits<int64_t>::max();
+  };
+  struct Context {
+    base::FlatHashMap<std::string, std::unique_ptr<State>> state_by_name;
+  };
+  struct Vtab : sqlite::Module<WindowOperatorModule>::Vtab {
+    Context* context;
+    std::string name;
+    State* state = nullptr;
+  };
+  struct Cursor : sqlite::Module<WindowOperatorModule>::Cursor {
+    int64_t window_start = 0;
+    int64_t window_end = 0;
+    int64_t step_size = 0;
+
+    int64_t current_ts = 0;
+    int64_t quantum_ts = 0;
+    int64_t row_id = 0;
+
+    FilterType filter_type = FilterType::kReturnAll;
+  };
   enum Column {
     kRowId = 0,
     kQuantum = 1,
@@ -40,64 +75,44 @@
     kDuration = 5,
     kQuantumTs = 6
   };
-  class Cursor final : public SqliteTable::BaseCursor {
-   public:
-    explicit Cursor(WindowOperatorTable*);
-    ~Cursor() final;
 
-    Cursor(Cursor&&) = default;
-    Cursor& operator=(Cursor&&) = default;
+  static constexpr auto kType = kCreateOnly;
+  static constexpr bool kDoesOverloadFunctions = false;
 
-    // Implementation of SqliteTable::Cursor.
-    base::Status Filter(const QueryConstraints& qc,
-                        sqlite3_value**,
-                        FilterHistory);
-    base::Status Next();
-    bool Eof();
-    base::Status Column(sqlite3_context*, int N);
+  static int Create(sqlite3*,
+                    void*,
+                    int,
+                    const char* const*,
+                    sqlite3_vtab**,
+                    char**);
+  static int Destroy(sqlite3_vtab*);
 
-   private:
-    // Defines the data to be generated by the table.
-    enum FilterType {
-      // Returns all the spans.
-      kReturnAll = 0,
-      // Only returns the first span of the table. Useful for UPDATE operations.
-      kReturnFirst = 1,
-    };
+  static int Connect(sqlite3*,
+                     void*,
+                     int,
+                     const char* const*,
+                     sqlite3_vtab**,
+                     char**);
+  static int Disconnect(sqlite3_vtab*);
 
-    int64_t window_start_ = 0;
-    int64_t window_end_ = 0;
-    int64_t step_size_ = 0;
+  static int BestIndex(sqlite3_vtab*, sqlite3_index_info*);
 
-    int64_t current_ts_ = 0;
-    int64_t quantum_ts_ = 0;
-    int64_t row_id_ = 0;
+  static int Open(sqlite3_vtab*, sqlite3_vtab_cursor**);
+  static int Close(sqlite3_vtab_cursor*);
 
-    FilterType filter_type_ = FilterType::kReturnAll;
+  static int Filter(sqlite3_vtab_cursor*,
+                    int,
+                    const char*,
+                    int,
+                    sqlite3_value**);
+  static int Next(sqlite3_vtab_cursor*);
+  static int Eof(sqlite3_vtab_cursor*);
+  static int Column(sqlite3_vtab_cursor*, sqlite3_context*, int);
+  static int Rowid(sqlite3_vtab_cursor*, sqlite_int64*);
 
-    WindowOperatorTable* table_ = nullptr;
-  };
-
-  WindowOperatorTable(sqlite3*, const TraceStorage*);
-  ~WindowOperatorTable() final;
-
-  // Table implementation.
-  base::Status Init(int, const char* const*, Schema* schema) final;
-  std::unique_ptr<SqliteTable::BaseCursor> CreateCursor() final;
-  int BestIndex(const QueryConstraints&, BestIndexInfo*) final;
-  base::Status ModifyConstraints(QueryConstraints* qc) final;
-  base::Status Update(int, sqlite3_value**, sqlite3_int64*) final;
-
- private:
-  int64_t quantum_ = 0;
-  int64_t window_start_ = 0;
-
-  // max of int64_t because SQLite technically only supports int64s and not
-  // uint64s.
-  int64_t window_dur_ = std::numeric_limits<int64_t>::max();
+  static int Update(sqlite3_vtab*, int, sqlite3_value**, sqlite_int64*);
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_WINDOW_OPERATOR_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
index 6edd50b..814b6ba 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
@@ -27,6 +27,8 @@
     "descendant.h",
     "dfs.cc",
     "dfs.h",
+    "dfs_weight_bounded.cc",
+    "dfs_weight_bounded.h",
     "dominator_tree.cc",
     "dominator_tree.h",
     "experimental_annotated_stack.cc",
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs.h b/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs.h
index 8dad16d..43e3239 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs.h
@@ -35,9 +35,9 @@
 //
 // Arguments:
 //  1) |source_node_ids|: RepeatedBuilderResult proto containing a column of
-//     int64 values corresponding to the source of edges.
+//     uint32 values corresponding to the source of edges.
 //  2) |dest_node_ids|:  RepeatedBuilderResult proto containing a column of
-//     int64 values corresponding to the destination of edges. This number of
+//     uint32 values corresponding to the destination of edges. This number of
 //     values should be the same as |source_node_ids| with each index in
 //     |source_node_ids| acting as the source for the corresponding index in
 //     |dest_node_ids|.
@@ -47,7 +47,7 @@
 // Returns:
 //  A table with the nodes reachable from the start node and their "parent" in
 //  the tree generated by the DFS. The schema of the table
-//  is (node_id int64_t, parent_node_id optional<int64_t>).
+//  is (node_id uint32_t, parent_node_id optional<uint32_t>).
 //
 // Note: this function is not intended to be used directly from SQL: instead
 // macros exist in the standard library, wrapping it and making it
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc
new file mode 100644
index 0000000..4ff33cc
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/protozero/proto_decoder.h"
+#include "perfetto/trace_processor/basic_types.h"
+#include "protos/perfetto/trace_processor/metrics_impl.pbzero.h"
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/db/column.h"
+#include "src/trace_processor/db/table.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/tables_py.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::trace_processor {
+namespace tables {
+DfsWeightBoundedTable::~DfsWeightBoundedTable() = default;
+}  // namespace tables
+
+namespace {
+struct Edge {
+  uint32_t id;
+  uint32_t weight;
+};
+using Destinations = std::vector<Edge>;
+
+base::StatusOr<std::vector<Destinations>> ParseSourceToDestionationsMap(
+    protos::pbzero::RepeatedBuilderResult::Decoder& source,
+    protos::pbzero::RepeatedBuilderResult::Decoder& dest,
+    protos::pbzero::RepeatedBuilderResult::Decoder& weight) {
+  std::vector<Destinations> source_to_destinations_map;
+  bool parse_error = false;
+  auto source_node_ids = source.int_values(&parse_error);
+  auto dest_node_ids = dest.int_values(&parse_error);
+  auto edge_weights = weight.int_values(&parse_error);
+
+  for (; source_node_ids && dest_node_ids && edge_weights;
+       ++source_node_ids, ++dest_node_ids, ++edge_weights) {
+    source_to_destinations_map.resize(
+        std::max(source_to_destinations_map.size(),
+                 std::max(static_cast<size_t>(*source_node_ids + 1),
+                          static_cast<size_t>(*dest_node_ids + 1))));
+    source_to_destinations_map[static_cast<uint32_t>(*source_node_ids)]
+        .push_back(Edge{static_cast<uint32_t>(*dest_node_ids),
+                        static_cast<uint32_t>(*edge_weights)});
+  }
+  if (parse_error) {
+    return base::ErrStatus("Failed while parsing source or dest ids");
+  }
+  if (static_cast<bool>(source_node_ids) != static_cast<bool>(dest_node_ids)) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: length of source and destination columns is not "
+        "the same");
+  }
+  return source_to_destinations_map;
+}
+
+base::StatusOr<std::vector<Edge>> ParseRootToMaxWeightMap(
+    protos::pbzero::RepeatedBuilderResult::Decoder& start,
+    protos::pbzero::RepeatedBuilderResult::Decoder& end) {
+  std::vector<Edge> roots;
+  bool parse_error = false;
+  auto root_node_ids = start.int_values(&parse_error);
+  auto max_weights = end.int_values(&parse_error);
+
+  for (; root_node_ids && max_weights; ++root_node_ids, ++max_weights) {
+    roots.push_back(Edge{static_cast<uint32_t>(*root_node_ids),
+                         static_cast<uint32_t>(*max_weights)});
+  }
+
+  if (parse_error) {
+    return base::ErrStatus(
+        "Failed while parsing root_node_ids or root_max_weights");
+  }
+  if (static_cast<bool>(root_node_ids) != static_cast<bool>(max_weights)) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: length of root_node_ids and root_max_weights "
+        "columns is not the same");
+  }
+  return roots;
+}
+
+void DfsWeightBoundedImpl(
+    tables::DfsWeightBoundedTable* table,
+    const std::vector<Destinations>& source_to_destinations_map,
+    const std::vector<Edge>& roots) {
+  struct StackState {
+    uint32_t id;
+    uint32_t weight;
+    std::optional<uint32_t> parent_id;
+  };
+
+  std::vector<uint8_t> seen_node_ids(source_to_destinations_map.size());
+  std::vector<StackState> stack;
+
+  for (const auto& root : roots) {
+    stack.clear();
+    stack.push_back({root.id, 0, std::nullopt});
+    std::fill(seen_node_ids.begin(), seen_node_ids.end(), 0);
+
+    for (uint32_t total_weight = 0; !stack.empty();) {
+      StackState stack_state = stack.back();
+      stack.pop_back();
+
+      if (seen_node_ids[stack_state.id]) {
+        continue;
+      }
+      seen_node_ids[stack_state.id] = true;
+
+      // We want to greedily return all possible edges that are reachable within
+      // the target weight. If an edge already fails the requirement, skip it
+      // and don't include it's weight but continue the search, some other edges
+      // might fit.
+      if (total_weight + stack_state.weight > root.weight) {
+        continue;
+      }
+      total_weight += stack_state.weight;
+
+      tables::DfsWeightBoundedTable::Row row;
+      row.root_node_id = root.id;
+      row.node_id = stack_state.id;
+      row.parent_node_id = stack_state.parent_id;
+      table->Insert(row);
+
+      PERFETTO_DCHECK(stack_state.id < source_to_destinations_map.size());
+
+      const auto& children = source_to_destinations_map[stack_state.id];
+      for (auto it = children.rbegin(); it != children.rend(); ++it) {
+        stack.emplace_back(StackState{(*it).id, (*it).weight, stack_state.id});
+      }
+    }
+  }
+}
+}  // namespace
+
+DfsWeightBounded::DfsWeightBounded(StringPool* pool) : pool_(pool) {}
+DfsWeightBounded::~DfsWeightBounded() = default;
+
+Table::Schema DfsWeightBounded::CreateSchema() {
+  return tables::DfsWeightBoundedTable::ComputeStaticSchema();
+}
+
+std::string DfsWeightBounded::TableName() {
+  return tables::DfsWeightBoundedTable::Name();
+}
+
+uint32_t DfsWeightBounded::EstimateRowCount() {
+  // TODO(lalitm): improve this estimate.
+  return 1024;
+}
+
+base::StatusOr<std::unique_ptr<Table>> DfsWeightBounded::ComputeTable(
+    const std::vector<SqlValue>& arguments) {
+  PERFETTO_CHECK(arguments.size() == 5);
+
+  const SqlValue& raw_source_ids = arguments[0];
+  const SqlValue& raw_dest_ids = arguments[1];
+  const SqlValue& raw_edge_weights = arguments[2];
+  const SqlValue& raw_root_ids = arguments[3];
+  const SqlValue& raw_root_max_weights = arguments[4];
+
+  if (raw_source_ids.is_null() && raw_dest_ids.is_null() &&
+      raw_edge_weights.is_null() && raw_root_ids.is_null() &&
+      raw_root_max_weights.is_null()) {
+    return std::unique_ptr<Table>(
+        std::make_unique<tables::DfsWeightBoundedTable>(pool_));
+  }
+
+  if (raw_source_ids.is_null() || raw_dest_ids.is_null() ||
+      raw_edge_weights.is_null() || raw_root_ids.is_null() ||
+      raw_root_max_weights.is_null()) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: either all arguments should be null or none "
+        "should be");
+  }
+  if (raw_source_ids.type != SqlValue::kBytes) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: source_node_ids should be a repeated field");
+  }
+  if (raw_dest_ids.type != SqlValue::kBytes) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: dest_node_ids should be a repeated field");
+  }
+  if (raw_edge_weights.type != SqlValue::kBytes) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: edge_weights should be a repeated field");
+  }
+  if (raw_root_ids.type != SqlValue::kBytes) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: root_ids should be a repeated field");
+  }
+  if (raw_root_max_weights.type != SqlValue::kBytes) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: root_max_weights should be a repeated field");
+  }
+
+  protos::pbzero::ProtoBuilderResult::Decoder proto_source_ids(
+      static_cast<const uint8_t*>(raw_source_ids.AsBytes()),
+      raw_source_ids.bytes_count);
+  if (!proto_source_ids.is_repeated()) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: source_node_ids is not generated by RepeatedField "
+        "function");
+  }
+  protos::pbzero::RepeatedBuilderResult::Decoder source_ids(
+      proto_source_ids.repeated());
+
+  protos::pbzero::ProtoBuilderResult::Decoder proto_dest_ids(
+      static_cast<const uint8_t*>(raw_dest_ids.AsBytes()),
+      raw_dest_ids.bytes_count);
+  if (!proto_dest_ids.is_repeated()) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: dest_node_ids is not generated by RepeatedField "
+        "function");
+  }
+  protos::pbzero::RepeatedBuilderResult::Decoder dest_ids(
+      proto_dest_ids.repeated());
+
+  protos::pbzero::ProtoBuilderResult::Decoder proto_edge_weights(
+      static_cast<const uint8_t*>(raw_edge_weights.AsBytes()),
+      raw_edge_weights.bytes_count);
+  if (!proto_edge_weights.is_repeated()) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: edge_weights is not generated by RepeatedField "
+        "function");
+  }
+  protos::pbzero::RepeatedBuilderResult::Decoder edge_weights(
+      proto_edge_weights.repeated());
+
+  protos::pbzero::ProtoBuilderResult::Decoder proto_root_ids(
+      static_cast<const uint8_t*>(raw_root_ids.AsBytes()),
+      raw_root_ids.bytes_count);
+  if (!proto_root_ids.is_repeated()) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: root_ids is not generated by RepeatedField "
+        "function");
+  }
+  protos::pbzero::RepeatedBuilderResult::Decoder root_ids(
+      proto_root_ids.repeated());
+
+  protos::pbzero::ProtoBuilderResult::Decoder proto_root_max_weights(
+      static_cast<const uint8_t*>(raw_root_max_weights.AsBytes()),
+      raw_root_max_weights.bytes_count);
+  if (!proto_root_max_weights.is_repeated()) {
+    return base::ErrStatus(
+        "dfs_weight_bounded: root_max_weights is not generated by "
+        "RepeatedField function");
+  }
+  protos::pbzero::RepeatedBuilderResult::Decoder root_max_weights(
+      proto_root_max_weights.repeated());
+
+  ASSIGN_OR_RETURN(auto map, ParseSourceToDestionationsMap(source_ids, dest_ids,
+                                                           edge_weights));
+
+  ASSIGN_OR_RETURN(auto roots,
+                   ParseRootToMaxWeightMap(root_ids, root_max_weights));
+
+  auto table = std::make_unique<tables::DfsWeightBoundedTable>(pool_);
+  DfsWeightBoundedImpl(table.get(), map, roots);
+  return std::unique_ptr<Table>(std::move(table));
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h b/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h
new file mode 100644
index 0000000..f1af772
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_DFS_WEIGHT_BOUNDED_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_DFS_WEIGHT_BOUNDED_H_
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/db/table.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
+
+namespace perfetto::trace_processor {
+
+// An SQL table-function which performs a weight bounded DFS from a set of start
+// nodes in a graph and returns all the nodes which are reachable from each
+// start node independently.
+//
+// Arguments:
+//  1) |source_node_ids|: RepeatedBuilderResult proto containing a column of
+//     uint32 values corresponding to the source of edges.
+//  2) |dest_node_ids|:  RepeatedBuilderResult proto containing a column of
+//     uint32 values corresponding to the destination of edges. This number of
+//     values should be the same as |source_node_ids| with each index in
+//     |source_node_ids| acting as the source for the corresponding index in
+//     |dest_node_ids|.
+//  3) |edge_weights|:  RepeatedBuilderResult proto containing a column of
+//     uint32 values corresponding to the weight of edges. This number of
+//     values should be the same as |source_node_ids| with each index in
+//     |source_node_ids| acting as the source for the corresponding index in
+//     |edge_weights|.
+//  4) |root_node_ids|: RepeatedBuilderResult proto containing a column of
+//     uint32 values corresponding to the ID of the start nodes in the graph
+//     from which reachability should be computed.
+//  5) |root_max_weights|: RepeatedBuilderResult proto containing a column of
+//     uint32 values corresponding to the max sum of edge weights inclusive,
+//     at which point the DFS from the |root_node_ids| stops. This number of
+//     values should be the same as |root_node_ids|.
+//
+// Returns:
+//  A table with the nodes reachable from the start node, their "parent" in
+//  the tree generated by the DFS and the starting node itself "root". The
+//  schema of the table is (root_node_id uint32_t, node_id uint32_t,
+//  parent_node_id optional<uint32_t>).
+//
+// Note: this function is not intended to be used directly from SQL: instead
+// macros exist in the standard library, wrapping it and making it
+// user-friendly.
+class DfsWeightBounded : public StaticTableFunction {
+ public:
+  explicit DfsWeightBounded(StringPool*);
+  virtual ~DfsWeightBounded() override;
+
+  // StaticTableFunction implementation.
+  Table::Schema CreateSchema() override;
+  std::string TableName() override;
+  uint32_t EstimateRowCount() override;
+  base::StatusOr<std::unique_ptr<Table>> ComputeTable(
+      const std::vector<SqlValue>& arguments) override;
+
+ private:
+  StringPool* pool_ = nullptr;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_DFS_WEIGHT_BOUNDED_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
index 3c86e50..0570082 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
@@ -187,6 +187,27 @@
         C("in_right_durs", CppOptional(CppString()), flags=ColumnFlag.HIDDEN),
     ])
 
+DFS_WEIGHT_BOUNDED_TABLE = Table(
+    python_module=__file__,
+    class_name="DfsWeightBoundedTable",
+    sql_name="__intrinsic_dfs_weight_bounded",
+    columns=[
+        C("root_node_id", CppUint32()),
+        C("node_id", CppUint32()),
+        C("parent_node_id", CppOptional(CppUint32())),
+        C("in_source_node_ids",
+          CppOptional(CppUint32()),
+          flags=ColumnFlag.HIDDEN),
+        C("in_dest_node_ids", CppOptional(CppUint32()),
+          flags=ColumnFlag.HIDDEN),
+        C("in_edge_weights", CppOptional(CppUint32()), flags=ColumnFlag.HIDDEN),
+        C("in_root_node_ids", CppOptional(CppUint32()),
+          flags=ColumnFlag.HIDDEN),
+        C("in_root_max_weights",
+          CppOptional(CppUint32()),
+          flags=ColumnFlag.HIDDEN),
+    ])
+
 # Keep this list sorted.
 ALL_TABLES = [
     ANCESTOR_SLICE_BY_STACK_TABLE,
@@ -196,6 +217,7 @@
     DESCENDANT_SLICE_BY_STACK_TABLE,
     DESCENDANT_SLICE_TABLE,
     DFS_TABLE,
+    DFS_WEIGHT_BOUNDED_TABLE,
     DOMINATOR_TREE_TABLE,
     EXPERIMENTAL_ANNOTATED_CALLSTACK_TABLE,
     EXPERIMENTAL_COUNTER_DUR_TABLE,
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
index 29c4b89..87f8dec 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
@@ -23,6 +23,7 @@
     "battery_stats.sql",
     "binder.sql",
     "broadcasts.sql",
+    "critical_blocking_calls.sql",
     "dvfs.sql",
     "freezer.sql",
     "garbage_collection.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql b/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
new file mode 100644
index 0000000..ba5a3f6
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
@@ -0,0 +1,84 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     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.
+--
+INCLUDE PERFETTO MODULE android.slices;
+INCLUDE PERFETTO MODULE android.binder;
+INCLUDE PERFETTO MODULE slices.with_context;
+
+CREATE PERFETTO FUNCTION _is_relevant_blocking_call(name STRING, depth INT)
+RETURNS BOOL AS SELECT
+  $name = 'measure'
+  OR $name = 'layout'
+  OR $name = 'configChanged'
+  OR $name = 'animation'
+  OR $name = 'input'
+  OR $name = 'traversal'
+  OR $name = 'Contending for pthread mutex'
+  OR $name = 'postAndWait'
+  OR $name GLOB 'monitor contention with*'
+  OR $name GLOB 'SuspendThreadByThreadId*'
+  OR $name GLOB 'LoadApkAssetsFd*'
+  OR $name GLOB '*binder transaction*'
+  OR $name GLOB 'inflate*'
+  OR $name GLOB 'Lock contention on*'
+  OR $name GLOB 'android.os.Handler: kotlinx.coroutines*'
+  OR $name GLOB 'relayoutWindow*'
+  OR $name GLOB 'ImageDecoder#decode*'
+  OR $name GLOB 'NotificationStackScrollLayout#onMeasure'
+  OR $name GLOB 'ExpNotRow#*'
+  OR $name GLOB 'GC: Wait For*'
+  OR (
+    -- Some top level handler slices
+    $depth = 0
+    AND $name NOT GLOB '*Choreographer*'
+    AND $name NOT GLOB '*Input*'
+    AND $name NOT GLOB '*input*'
+    AND $name NOT GLOB 'android.os.Handler: #*'
+    AND (
+      -- Handler pattern heuristics
+      $name GLOB '*Handler: *$*'
+      OR $name GLOB '*.*.*: *$*'
+      OR $name GLOB '*.*$*: #*'
+    )
+  );
+
+
+--Extract all slice data on main thread for all processes.
+CREATE PERFETTO TABLE _android_critical_blocking_calls AS
+SELECT
+  android_standardize_slice_name(s.name) AS name,
+  s.ts,
+  s.dur,
+  s.id,
+  s.process_name,
+  thread.utid,
+  s.upid
+FROM thread_slice s JOIN
+thread USING (utid)
+WHERE
+  thread.is_main_thread AND _is_relevant_blocking_call(s.name, s.depth)
+UNION ALL
+-- As binder names are not included in slice table, extract these directly from the
+-- android_binder_txns table.
+SELECT
+  tx.aidl_name AS name,
+  tx.client_ts AS ts,
+  tx.client_dur AS dur,
+  tx.binder_txn_id AS id,
+  tx.client_process as process_name,
+  tx.client_utid as utid,
+  tx.client_upid as upid
+FROM android_binder_txns AS tx
+WHERE is_main_thread AND aidl_name IS NOT NULL AND is_sync = 1;
diff --git a/src/trace_processor/perfetto_sql/stdlib/graphs/search.sql b/src/trace_processor/perfetto_sql/stdlib/graphs/search.sql
index 4bba25a..6214716 100644
--- a/src/trace_processor/perfetto_sql/stdlib/graphs/search.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/graphs/search.sql
@@ -99,3 +99,82 @@
   SELECT node_id, lead(node_id) OVER (PARTITION BY node_parent_id ORDER BY sort_key) AS next_node_id
     FROM $graph_table
 );
+
+-- Computes the "reachable" set of nodes in a directed graph from a set of
+-- starting (root) nodes by performing a depth-first search from each root node on the graph.
+-- The search is bounded by the sum of edge weights on the path and the root node specifies the
+-- max weight (inclusive) allowed before stopping the search.
+-- The returned nodes are structured as a tree with parent-child relationships corresponding
+-- to the order in which nodes were encountered by the DFS. Each row also has the root node from
+-- which where the edge was encountered.
+--
+-- While this macro can be used directly by end users (hence being public),
+-- it is primarily intended as a lower-level building block upon which higher
+-- level functions/macros in the standard library can be built.
+--
+-- Example usage on traces with sched info:
+-- ```
+-- -- Compute the reachable nodes from a sched wakeup chain
+-- INCLUDE PERFETTO MODULE sched.thread_executing_spans;
+--
+-- SELECT *
+-- FROM
+--   graph_reachable_dfs_bounded
+--    !(
+--      (
+--        SELECT
+--          id AS source_node_id,
+--          COALESCE(parent_id, id) AS dest_node_id,
+--          id - COALESCE(parent_id, id) AS edge_weight
+--        FROM _wakeup_chain
+--      ),
+--      (
+--        SELECT
+--          id AS root_node_id,
+--          id - COALESCE(prev_id, id) AS root_max_weight
+--        FROM _wakeup_chain
+--      ));
+-- ```
+CREATE PERFETTO MACRO graph_reachable_weight_bounded_dfs(
+  -- A table/view/subquery corresponding to a directed graph on which the
+  -- reachability search should be performed. This table must have the columns
+  -- "source_node_id" and "dest_node_id" corresponding to the two nodes on
+  -- either end of the edges in the graph and an "edge_weight" corresponding to the
+  -- weight of the edge between the node.
+  --
+  -- Note: the columns must contain uint32 similar to ids in trace processor
+  -- tables (i.e. the values should be relatively dense and close to zero). The
+  -- implementation makes assumptions on this for performance reasons and, if
+  -- this criteria is not, can lead to enormous amounts of memory being
+  -- allocated.
+  graph_table TableOrSubquery,
+  -- A table/view/subquery corresponding to start nodes to |graph_table| which will be the
+  -- roots of the reachability trees. This table must have the columns
+  -- "root_node_id" and "root_max_weight" corresponding to the starting node id and the max
+  -- weight allowed on the tree.
+  --
+  -- Note: the columns must contain uint32 similar to ids in trace processor
+  -- tables (i.e. the values should be relatively dense and close to zero). The
+  -- implementation makes assumptions on this for performance reasons and, if
+  -- this criteria is not, can lead to enormous amounts of memory being
+  -- allocated.
+  root_table TableOrSubquery
+)
+-- The returned table has the schema (root_node_id, node_id UINT32, parent_node_id UINT32).
+-- |root_node_id| is the id of the starting node under which this edge was encountered.
+-- |node_id| is the id of the node from the input graph and |parent_node_id|
+-- is the id of the node which was the first encountered predecessor in a DFS
+-- search of the graph.
+RETURNS TableOrSubquery AS
+(
+  WITH __temp_graph_table AS (SELECT * FROM $graph_table),
+  __temp_root_table AS (SELECT * FROM $root_table)
+  SELECT dt.root_node_id, dt.node_id, dt.parent_node_id
+  FROM __intrinsic_dfs_weight_bounded(
+    (SELECT RepeatedField(source_node_id) FROM __temp_graph_table),
+    (SELECT RepeatedField(dest_node_id) FROM __temp_graph_table),
+    (SELECT RepeatedField(edge_weight) FROM __temp_graph_table),
+    (SELECT RepeatedField(root_node_id) FROM __temp_root_table),
+    (SELECT RepeatedField(root_max_weight) FROM __temp_root_table)
+  ) dt
+);
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql b/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql
index 19f68c2..348d261 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql
@@ -41,10 +41,13 @@
   thread_state.dur,
   thread_state.state,
   thread_state.utid,
+  thread_state.waker_id,
   thread_state.waker_utid
 FROM thread_state
-WHERE thread_state.dur != -1 AND thread_state.waker_utid IS NOT NULL
-   AND (thread_state.irq_context = 0 OR thread_state.irq_context IS NULL);
+WHERE
+  thread_state.dur != -1
+  AND thread_state.waker_utid IS NOT NULL
+  AND (thread_state.irq_context = 0 OR thread_state.irq_context IS NULL);
 
 -- Similar to |_runnable_state| but finds the first runnable state at thread.
 CREATE PERFETTO VIEW _first_runnable_state
@@ -62,10 +65,15 @@
   thread_state.dur,
   thread_state.state,
   thread_state.utid,
+  thread_state.waker_id,
   thread_state.waker_utid
 FROM thread_state
-JOIN first_state USING (id)
-WHERE thread_state.dur != -1 AND thread_state.state = 'R' AND (thread_state.irq_context = 0 OR thread_state.irq_context IS NULL);
+JOIN first_state
+  USING (id)
+WHERE
+  thread_state.dur != -1
+  AND thread_state.state = 'R'
+  AND (thread_state.irq_context = 0 OR thread_state.irq_context IS NULL);
 
 --
 -- Finds all sleep states including interruptible (S) and uninterruptible (D).
@@ -125,82 +133,71 @@
 --
 -- We define the following markers in this table:
 --
--- prev_start_id    = R0_id.
--- prev_start_ts    = R0_ts.
--- prev_start_dur   = R0_dur.
--- prev_start_state = 'R'.
+-- prev_id          = R0_id.
 --
--- prev_end_id      = S0_id.
 -- prev_end_ts      = S0_ts.
--- prev_end_dur     = S0_dur.
--- prev_end_state   = 'S' or 'D'.
+-- state            = 'S' or 'D'.
+-- blocked_function = <kernel blocking function>
 --
--- start_id         = R1_id.
--- start_ts         = R1_ts.
--- start_dur        = R1_dur.
--- start_state      = 'R'.
+-- id               = R1_id.
+-- ts               = R1_ts.
 --
--- end_id           = S1_id.
 -- end_ts           = S1_ts.
--- end_dur          = S1_dur.
--- end_state        = 'S' or 'D'.
-CREATE TABLE _wakeup AS
-  SELECT
-  LAG(r.id, 1) OVER (PARTITION BY r.utid ORDER BY r.ts) AS prev_start_id,
-  LAG(r.ts, 1) OVER (PARTITION BY r.utid ORDER BY r.ts) AS prev_start_ts,
-  LAG(r.dur, 1) OVER (PARTITION BY r.utid ORDER BY r.ts) AS prev_start_dur,
-  LAG(r.state, 1) OVER (PARTITION BY r.utid ORDER BY r.ts) AS prev_start_state,
-  s.id AS prev_end_id,
-  s.ts AS prev_end_ts,
-  s.dur AS prev_end_dur,
-  s.state AS prev_end_state,
-  s.blocked_function AS prev_blocked_function,
-  r.id AS start_id,
-  r.ts AS start_ts,
-  r.dur AS start_dur,
-  r.state AS start_state,
+CREATE PERFETTO TABLE _wakeup
+AS
+SELECT
+  s.state,
+  s.blocked_function,
+  r.id,
+  r.ts AS ts,
   r.utid AS utid,
+  r.waker_id,
   r.waker_utid,
-  LEAD(s.id, 1) OVER (PARTITION BY r.utid ORDER BY r.ts) AS end_id,
-  IFNULL(LEAD(s.ts, 1) OVER (PARTITION BY r.utid ORDER BY r.ts), thread_end.end_ts)  AS end_ts,
-  LEAD(s.dur, 1) OVER (PARTITION BY r.utid ORDER BY r.ts) AS end_dur,
-  LEAD(s.state, 1) OVER (PARTITION BY r.utid ORDER BY r.ts) AS end_state,
-  LEAD(s.blocked_function, 1) OVER (PARTITION BY r.utid ORDER BY r.ts) AS blocked_function
+  IFNULL(LEAD(s.ts) OVER (PARTITION BY r.utid ORDER BY r.ts), thread_end.end_ts) AS end_ts,
+  s.ts AS prev_end_ts,
+  LAG(r.id) OVER (PARTITION BY r.utid ORDER BY r.ts) AS prev_id
 FROM _runnable_state r
 JOIN _sleep_state s
   ON s.utid = r.utid AND (s.ts + s.dur = r.ts)
-LEFT JOIN _thread_end_ts thread_end USING(utid)
+LEFT JOIN _thread_end_ts thread_end
+  USING (utid)
 UNION ALL
-  SELECT
-  NULL AS prev_start_id,
-  NULL AS prev_start_ts,
-  NULL AS prev_start_dur,
-  NULL AS prev_start_state,
-  NULL AS prev_end_id,
-  NULL AS prev_end_ts,
-  NULL AS prev_end_dur,
-  NULL AS prev_end_state,
-  NULL AS prev_blocked_function,
-  r.id AS start_id,
-  r.ts AS start_ts,
-  r.dur AS start_dur,
-  r.state AS start_state,
+SELECT
+  NULL AS state,
+  NULL AS blocked_function,
+  r.id,
+  r.ts,
   r.utid AS utid,
+  r.waker_id,
   r.waker_utid,
-  s.id AS end_id,
-  IFNULL(s.ts, thread_end.end_ts)  AS end_ts,
-  s.dur AS end_dur,
-  s.state AS end_state,
-  s.blocked_function AS blocked_function
+  IFNULL(s.ts, thread_end.end_ts) AS end_ts,
+  NULL AS prev_end_ts,
+  NULL AS prev_id
 FROM _first_runnable_state r
 LEFT JOIN _first_sleep_state s
   ON s.utid = r.utid
-LEFT JOIN _thread_end_ts thread_end USING(utid);
+LEFT JOIN _thread_end_ts thread_end
+  USING (utid);
 
--- Improves performance of |_wakeup_chain| computation.
-CREATE
-  INDEX _wakeup_idx
-ON _wakeup(waker_utid, start_ts);
+-- Mapping from running thread state to runnable
+CREATE PERFETTO TABLE _waker_map
+AS
+WITH x AS (
+SELECT id, waker_id, utid, state FROM thread_state WHERE state = 'Running' AND dur != -1
+UNION ALL
+SELECT id, waker_id, utid, state FROM _first_runnable_state
+UNION ALL
+SELECT id, waker_id, utid, state FROM _runnable_state
+), y AS (
+    SELECT
+      id AS waker_id,
+      state,
+      max(id)
+        filter(WHERE state = 'R')
+          OVER (PARTITION BY utid ORDER BY id) AS id
+    FROM x
+  )
+SELECT id, waker_id FROM y WHERE state = 'Running' AND id IS NOT NULL ORDER BY waker_id;
 
 --
 -- Builds the parent-child chain from all thread_executing_spans. The parent is the waker and
@@ -209,14 +206,38 @@
 -- Note that this doesn't include the roots. We'll compute the roots below.
 -- This two step process improves performance because it's more efficient to scan
 -- parent and find a child between than to scan child and find the parent it lies between.
-CREATE PERFETTO VIEW _wakeup_chain
+CREATE PERFETTO TABLE _wakeup_chain
 AS
-SELECT parent.start_id AS parent_id, child.*
-FROM _wakeup parent
-JOIN _wakeup child
-  ON (
-    parent.utid = child.waker_utid
-    AND child.start_ts BETWEEN parent.start_ts AND parent.end_ts);
+SELECT
+  _waker_map.id AS parent_id,
+  prev_id,
+  prev_end_ts,
+  _wakeup.id AS id,
+  _wakeup.ts AS ts,
+  _wakeup.end_ts,
+  IIF(_wakeup.state IS NULL OR _wakeup.state = 'S', 0, 1) AS is_kernel,
+  _wakeup.utid,
+  _wakeup.state,
+  _wakeup.blocked_function
+FROM _wakeup
+JOIN _waker_map USING(waker_id);
+
+-- The inverse of thread_executing_spans. All the sleeping periods between thread_executing_spans.
+CREATE PERFETTO TABLE _sleeping_span
+AS
+WITH
+  x AS (
+    SELECT
+      id,
+      ts,
+      lag(end_ts) OVER (PARTITION BY utid ORDER BY id) AS prev_end_ts,
+      utid
+    FROM _wakeup_chain
+  )
+SELECT
+  ts - prev_end_ts AS dur, prev_end_ts AS ts, id AS root_id, utid AS root_utid
+FROM x
+WHERE ts IS NOT NULL;
 
 --
 -- Finds the roots of the |_wakeup_chain|.
@@ -226,37 +247,56 @@
   _wakeup_root_id AS (
     SELECT DISTINCT parent_id AS id FROM _wakeup_chain
     EXCEPT
-    SELECT DISTINCT start_id AS id FROM _wakeup_chain
+    SELECT DISTINCT id FROM _wakeup_chain
   )
 SELECT NULL AS parent_id, _wakeup.*
 FROM _wakeup
-JOIN _wakeup_root_id
-  ON _wakeup_root_id.id = _wakeup.start_id;
+JOIN _wakeup_root_id USING(id);
 
 --
 -- Finds the leafs of the |_wakeup_chain|.
 CREATE PERFETTO TABLE _wakeup_leaf AS
 WITH
   _wakeup_leaf_id AS (
-    SELECT DISTINCT start_id AS id FROM _wakeup_chain
+    SELECT DISTINCT id AS id FROM _wakeup_chain
     EXCEPT
     SELECT DISTINCT parent_id AS id FROM _wakeup_chain
   )
 SELECT _wakeup_chain.*
 FROM _wakeup_chain
-JOIN _wakeup_leaf_id
-  ON _wakeup_leaf_id.id = _wakeup_chain.start_id;
+JOIN _wakeup_leaf_id USING(id);
 
 --
 -- Merges the roots, leafs and the rest of the chain.
 CREATE TABLE _wakeup_graph
 AS
-SELECT _wakeup_chain.*, 0 AS is_root, (_wakeup_leaf.start_id IS NOT NULL) AS is_leaf
+SELECT
+  _wakeup_chain.parent_id,
+  _wakeup_chain.id,
+  _wakeup_chain.ts,
+  _wakeup_chain.end_ts - _wakeup_chain.ts AS dur,
+  _wakeup_chain.utid,
+  _wakeup_chain.prev_end_ts,
+  _wakeup_chain.state,
+  _wakeup_chain.blocked_function,
+  0 AS is_root,
+  (_wakeup_leaf.id IS NOT NULL) AS is_leaf
 FROM _wakeup_chain
 LEFT JOIN _wakeup_leaf
-  USING (start_id)
+  USING (id)
 UNION ALL
-SELECT *, 1 AS is_root, 0 AS is_leaf FROM _wakeup_root;
+SELECT
+  _wakeup_root.parent_id,
+  _wakeup_root.id,
+  _wakeup_root.ts,
+  _wakeup_root.end_ts - _wakeup_root.ts AS dur,
+  _wakeup_root.utid,
+  _wakeup_root.prev_end_ts,
+  _wakeup_root.state,
+  _wakeup_root.blocked_function,
+  1 AS is_root,
+  0 AS is_leaf
+FROM _wakeup_root;
 
 -- Thread_executing_span graph of all wakeups across all processes.
 --
@@ -266,7 +306,6 @@
 -- @column ts                 Timestamp of first thread_state in thread_executing_span.
 -- @column dur                Duration of thread_executing_span.
 -- @column utid               Utid of thread with thread_state.
--- @column waker_utid         Utid of thread that woke the first thread_state in thread_executing_span.
 -- @column blocked_dur        Duration of blocking thread state before waking up.
 -- @column blocked_state      Thread state ('D' or 'S') of blocked thread_state before waking up.
 -- @column blocked_function   Kernel blocked_function of thread state before waking up.
@@ -275,16 +314,15 @@
 CREATE TABLE _thread_executing_span_graph AS
 WITH roots AS (
 SELECT
-  start_id AS root_id,
+  id AS root_id,
   parent_id,
-  start_id AS id,
-  start_ts AS ts,
-  end_ts - start_ts AS dur,
+  id,
+  ts,
+  end_ts - ts AS dur,
   utid,
-  waker_utid,
-  prev_end_dur AS blocked_dur,
-  prev_end_state AS blocked_state,
-  prev_blocked_function AS blocked_function,
+  ts - prev_end_ts AS blocked_dur,
+  state AS blocked_state,
+  blocked_function AS blocked_function,
   1 AS is_root,
   0 AS depth
 FROM _wakeup_root
@@ -294,14 +332,13 @@
   SELECT
     chain.root_id,
     graph.parent_id,
-    graph.start_id AS id,
-    graph.start_ts AS ts,
-    graph.end_ts - graph.start_ts AS dur,
+    graph.id,
+    graph.ts,
+    graph.dur,
     graph.utid,
-    graph.waker_utid,
-    graph.prev_end_dur AS blocked_dur,
-    graph.prev_end_state AS blocked_state,
-    graph.prev_blocked_function AS blocked_function,
+    graph.ts - graph.prev_end_ts AS blocked_dur,
+    graph.state AS blocked_state,
+    graph.blocked_function AS blocked_function,
     0 AS is_root,
     chain.depth + 1 AS depth
   FROM _wakeup_graph graph
diff --git a/src/trace_processor/perfetto_sql/stdlib/time/conversion.sql b/src/trace_processor/perfetto_sql/stdlib/time/conversion.sql
index 9a284d0..0aa736d 100644
--- a/src/trace_processor/perfetto_sql/stdlib/time/conversion.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/time/conversion.sql
@@ -83,3 +83,75 @@
 -- Time duration in nanoseconds.
 RETURNS INT AS
 SELECT $days * 24 * 60 * 60 * 1000 * 1000 * 1000;
+
+-- Returns the provided nanosecond duration, which is the default
+-- representation of time durations in trace processor. Provided for
+-- consistency with other functions.
+CREATE PERFETTO FUNCTION time_to_ns(
+  -- Time duration in nanoseconds.
+  nanos INT
+)
+-- Time duration in nanoseconds.
+RETURNS INT AS
+SELECT $nanos;
+
+-- Converts a duration in nanoseconds to microseconds. Nanoseconds is the default
+-- representation of time durations in trace processor.
+CREATE PERFETTO FUNCTION time_to_us(
+-- Time duration in nanoseconds.
+  nanos INT
+)
+-- Time duration in microseconds.
+RETURNS INT AS
+SELECT $nanos / 1000;
+
+-- Converts a duration in nanoseconds to millseconds. Nanoseconds is the default
+-- representation of time durations in trace processor.
+CREATE PERFETTO FUNCTION time_to_ms(
+  -- Time duration in nanoseconds.
+  nanos INT
+)
+-- Time duration in milliseconds.
+RETURNS INT AS
+SELECT $nanos / (1000 * 1000);
+
+-- Converts a duration in nanoseconds to seconds. Nanoseconds is the default
+-- representation of time durations in trace processor.
+CREATE PERFETTO FUNCTION time_to_s(
+  -- Time duration in nanoseconds.
+  nanos INT
+)
+-- Time duration in seconds.
+RETURNS INT AS
+SELECT $nanos / (1000 * 1000 * 1000);
+
+-- Converts a duration in nanoseconds to minutes. Nanoseconds is the default
+-- representation of time durations in trace processor.
+CREATE PERFETTO FUNCTION time_to_min(
+  -- Time duration in nanoseconds.
+  nanos INT
+)
+-- Time duration in minutes.
+RETURNS INT AS
+SELECT $nanos / (60 * 1000 * 1000 * 1000);
+
+-- Converts a duration in nanoseconds to hours. Nanoseconds is the default
+-- representation of time durations in trace processor.
+CREATE PERFETTO FUNCTION time_to_hours(
+  -- Time duration in nanoseconds.
+  nanos INT
+)
+-- Time duration in hours.
+RETURNS INT AS
+SELECT $nanos / (60 * 60 * 1000 * 1000 * 1000);
+
+-- Converts a duration in nanoseconds to days. Nanoseconds is the default
+-- representation of time durations in trace processor.
+CREATE PERFETTO FUNCTION time_to_days(
+  -- Time duration in nanoseconds.
+  nanos INT
+)
+-- Time duration in days.
+RETURNS INT AS
+SELECT $nanos / (24 * 60 * 60 * 1000 * 1000 * 1000);
+
diff --git a/src/trace_processor/sqlite/BUILD.gn b/src/trace_processor/sqlite/BUILD.gn
index e16e591..0a3beef 100644
--- a/src/trace_processor/sqlite/BUILD.gn
+++ b/src/trace_processor/sqlite/BUILD.gn
@@ -34,7 +34,6 @@
     "sqlite_tokenizer.h",
     "sqlite_utils.cc",
     "sqlite_utils.h",
-    "sqlite_utils.h",
     "stats_table.cc",
     "stats_table.h",
   ]
@@ -59,6 +58,7 @@
     "../util:profile_builder",
     "../util:regex",
   ]
+  public_deps = [ "bindings" ]
 }
 
 source_set("query_constraints") {
diff --git a/src/trace_processor/sqlite/bindings/BUILD.gn b/src/trace_processor/sqlite/bindings/BUILD.gn
new file mode 100644
index 0000000..b61cfde
--- /dev/null
+++ b/src/trace_processor/sqlite/bindings/BUILD.gn
@@ -0,0 +1,31 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../gn/test.gni")
+
+assert(enable_perfetto_trace_processor_sqlite)
+
+source_set("bindings") {
+  sources = [
+    "sqlite_aggregate_function.h",
+    "sqlite_module.h",
+    "sqlite_result.h",
+    "sqlite_window_function.h",
+  ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../../../gn:sqlite",
+  ]
+  visibility = [ "..:sqlite" ]
+}
diff --git a/src/trace_processor/sqlite/bindings/README.md b/src/trace_processor/sqlite/bindings/README.md
new file mode 100644
index 0000000..fff42e8
--- /dev/null
+++ b/src/trace_processor/sqlite/bindings/README.md
@@ -0,0 +1,3 @@
+This folder contains very lightweight bindings around SQLite to adapt
+it C++. Any non-trivial code should not live in this folder but in some
+higher layer.
\ No newline at end of file
diff --git a/src/trace_processor/sqlite/bindings/sqlite_aggregate_function.h b/src/trace_processor/sqlite/bindings/sqlite_aggregate_function.h
new file mode 100644
index 0000000..103dc32
--- /dev/null
+++ b/src/trace_processor/sqlite/bindings/sqlite_aggregate_function.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_AGGREGATE_FUNCTION_H_
+#define SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_AGGREGATE_FUNCTION_H_
+
+struct sqlite3_context;
+struct sqlite3_value;
+
+namespace perfetto::trace_processor {
+
+// Prototype for a aggregate function which can be registered with SQLite.
+//
+// See https://www.sqlite.org/c3ref/create_function.html for details on how to
+// implement the methods of this class.
+struct SqliteAggregateFunction {
+  // The type of the context object which will be passed to the function.
+  // Can be redefined in any sub-classes to override the context.
+  using Context = void;
+
+  // The xStep function which will be executed by SQLite to add a row of values
+  // to the aggregate.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static void Step(sqlite3_context*, int argc, sqlite3_value** argv);
+
+  // The xFinal function which will be executed by SQLite to obtain the current
+  // value of the aggregate *and* free all resources allocated by previous calls
+  // to Step.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static void Final(sqlite3_context* ctx);
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_AGGREGATE_FUNCTION_H_
diff --git a/src/trace_processor/sqlite/bindings/sqlite_module.h b/src/trace_processor/sqlite/bindings/sqlite_module.h
new file mode 100644
index 0000000..0da20cc
--- /dev/null
+++ b/src/trace_processor/sqlite/bindings/sqlite_module.h
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache, 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, software
+ * distributed under the License is distributed on an "AS IS",
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_MODULE_H_
+#define SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_MODULE_H_
+
+#include <sqlite3.h>
+
+namespace perfetto::trace_processor::sqlite {
+
+// Prototype for a virtual table (vtab) module which can be registered with
+// SQLite.
+//
+// See https://www.sqlite.org/vtab.html for how to implement this class.
+template <typename Impl>
+struct Module {
+  // Specifies the type of module: implementations can override this field by
+  // declaring and defining it.
+  //
+  // Specifying this to kCreateOnly requires that the |Create| and |Destroy|
+  // functions are defined.
+  //
+  // See the SQLite documentation on what these types mean.
+  static constexpr enum { kEponymousOnly, kCreateOnly } kType = kCreateOnly;
+
+  // Specifies whether this table is supports making changes to it:
+  // implementations can override this field by declaring and defining it.
+  //
+  // Setting this to true requires the |Update| function to be defined.
+  static constexpr bool kSupportsWrites = true;
+
+  // Specifies whether this table supports overloading functions:
+  // implementations can override this field by declaring and defining it.
+  //
+  // Setting this to true requires that the |FindFunction| function is defined.
+  static constexpr bool kDoesOverloadFunctions = true;
+
+  // sqlite3_module object corresponding to the module. Used to pass information
+  // about this module to SQLite.
+  //
+  // Note: this has to be defined here to allow referencing the functions
+  // defined above.
+  static constexpr sqlite3_module kModule = []() {
+    sqlite3_module module{};
+    module.xBestIndex = &Impl::BestIndex;
+    module.xOpen = &Impl::Open;
+    module.xClose = &Impl::Close;
+    module.xFilter = &Impl::Filter;
+    module.xNext = &Impl::Next;
+    module.xEof = &Impl::Eof;
+    module.xColumn = &Impl::Column;
+    module.xRowid = &Impl::Rowid;
+    if constexpr (Impl::kType == kCreateOnly) {
+      module.xCreate = &Impl::Create;
+      module.xDestroy = &Impl::Destroy;
+      module.xConnect = &Impl::Connect;
+      module.xDisconnect = &Impl::Disconnect;
+    } else {
+      module.xCreate = nullptr;
+      module.xDestroy = [](sqlite3_vtab*) -> int {
+        __builtin_trap();
+        __builtin_unreachable();
+      };
+      module.xConnect = &Impl::Connect;
+      module.xDisconnect = &Impl::Disconnect;
+    }
+    if constexpr (Impl::kSupportsWrites) {
+      module.xUpdate = &Impl::Update;
+    }
+    if constexpr (Impl::kDoesOverloadFunctions) {
+      module.xFindFunction = &Impl::FindFunction;
+    }
+    return module;
+  }();
+
+  // Specifies the type of context for the module. Implementations should define
+  // this type to match the context type which is expected to be passed into
+  // |sqlite3_create_module|.
+  using Context = void;
+
+  // Specifies the type for the vtab created by this module.
+  //
+  // Implementations should define this type to match the vtab type they use in
+  // |Create| and |Connect|.
+  using Vtab = sqlite3_vtab;
+
+  // Specifies the type for the cursor created by this module.
+  //
+  // Implementations should define this type to match the cursor type they use
+  // in |Open| and |Close|.
+  using Cursor = sqlite3_vtab_cursor;
+
+  // Creates a new instance of a virtual table and its backing storage.
+  //
+  // Implementations MUST define this function themselves if
+  // |kType| == |kCreateOnly|; this function is declared but *not* defined so
+  // linker errors will be thrown if not defined.
+  static int Create(sqlite3*,
+                    void*,
+                    int,
+                    const char* const*,
+                    sqlite3_vtab**,
+                    char**);
+
+  // Destroys the virtual table and its backing storage.
+  //
+  // Implementations MUST define this function themselves if
+  // |kType| == |kCreateOnly|; this function is declared but *not* defined so
+  // linker errors will be thrown if not defined.
+  static int Destroy(sqlite3_vtab*);
+
+  // Creates a new instance of the virtual table, connecting to existing
+  // backing storage.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static int Connect(sqlite3*,
+                     void*,
+                     int,
+                     const char* const*,
+                     sqlite3_vtab**,
+                     char**);
+
+  // Destroys the virtual table but *not* its backing storage.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static int Disconnect(sqlite3_vtab*);
+
+  // Specifies filtering and cost information for the query planner.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static int BestIndex(sqlite3_vtab*, sqlite3_index_info*);
+
+  // Opens a cursor into the given vtab.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static int Open(sqlite3_vtab*, sqlite3_vtab_cursor**);
+
+  // Closes the cursor.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static int Close(sqlite3_vtab_cursor*);
+
+  // Resets this cursor to filter rows matching the provided set of filter
+  // constraints and order by clauses.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static int Filter(sqlite3_vtab_cursor*,
+                    int,
+                    const char*,
+                    int,
+                    sqlite3_value**);
+
+  // Forwards the cursor to point to the next row.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static int Next(sqlite3_vtab_cursor*);
+
+  // Returns 1 if the cursor has reached its end or 0 otherwise.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static int Eof(sqlite3_vtab_cursor*);
+
+  // Returns the value column at the given index for the current row the cursor
+  // points to.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static int Column(sqlite3_vtab_cursor*, sqlite3_context*, int);
+
+  // Returns the rowid for the current row.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static int Rowid(sqlite3_vtab_cursor*, sqlite_int64*);
+
+  // Inserts/deletes/updates one row.
+  //
+  // Implementations MUST define this function themselves if
+  // |kSupportsWrites| == |true|; this function is declared but *not* defined so
+  // linker errors will be thrown if not defined.
+  static int Update(sqlite3_vtab*, int, sqlite3_value**, sqlite_int64*);
+
+  // Overloads a function with the given name when executed with a vtab column
+  // as the first argument.
+  //
+  // Implementations MUST define this function themselves if
+  // |kDoesOverloadFunctions| == |true|; this function is declared but *not*
+  // defined so linker errors will be thrown if not defined.
+  static int FindFunction(sqlite3_vtab*,
+                          int,
+                          const char*,
+                          void (**)(sqlite3_context*, int, sqlite3_value**),
+                          void**);
+
+  // Helper function to cast the module context pointer to the correct type.
+  static auto GetContext(void* ctx) {
+    return static_cast<typename Impl::Context*>(ctx);
+  }
+
+  // Helper function to cast the vtab pointer to the correct type.
+  static auto GetVtab(sqlite3_vtab* vtab) {
+    return static_cast<typename Impl::Vtab*>(vtab);
+  }
+
+  // Helper function to cast the cursor pointer to the correct type.
+  static auto GetCursor(sqlite3_vtab_cursor* cursor) {
+    return static_cast<typename Impl::Cursor*>(cursor);
+  }
+};
+
+}  // namespace perfetto::trace_processor::sqlite
+
+#endif  // SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_MODULE_H_
diff --git a/src/trace_processor/sqlite/bindings/sqlite_result.h b/src/trace_processor/sqlite/bindings/sqlite_result.h
new file mode 100644
index 0000000..5c1fb21
--- /dev/null
+++ b/src/trace_processor/sqlite/bindings/sqlite_result.h
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_RESULT_H_
+#define SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_RESULT_H_
+
+#include <sqlite3.h>
+#include <cstdint>
+
+namespace perfetto::trace_processor::sqlite::result {
+
+// This file contains wraps the sqlite3_result_* functions which tell SQLite
+// about the result of executing a function or calling a xColumn on a virtual
+// table.
+
+const auto kSqliteStatic = reinterpret_cast<sqlite3_destructor_type>(0);
+const auto kSqliteTransient = reinterpret_cast<sqlite3_destructor_type>(-1);
+
+inline void Null(sqlite3_context* ctx) {
+  sqlite3_result_null(ctx);
+}
+
+inline void Long(sqlite3_context* ctx, int64_t res) {
+  sqlite3_result_int64(ctx, res);
+}
+
+inline void Double(sqlite3_context* ctx, double res) {
+  sqlite3_result_double(ctx, res);
+}
+
+inline void RawString(sqlite3_context* ctx,
+                      const char* str,
+                      int size,
+                      sqlite3_destructor_type destructor) {
+  sqlite3_result_text(ctx, str, size, destructor);
+}
+inline void RawString(sqlite3_context* ctx,
+                      const char* str,
+                      sqlite3_destructor_type destructor) {
+  RawString(ctx, str, -1, destructor);
+}
+inline void StaticString(sqlite3_context* ctx, const char* str) {
+  RawString(ctx, str, kSqliteStatic);
+}
+inline void TransientString(sqlite3_context* ctx, const char* str) {
+  RawString(ctx, str, kSqliteTransient);
+}
+
+inline void RawBytes(sqlite3_context* ctx,
+                     const void* bytes,
+                     int size,
+                     sqlite3_destructor_type destructor) {
+  sqlite3_result_blob(ctx, bytes, size, destructor);
+}
+inline void StaticBytes(sqlite3_context* ctx, const void* bytes, int size) {
+  RawBytes(ctx, bytes, size, kSqliteStatic);
+}
+inline void TransientBytes(sqlite3_context* ctx, const void* bytes, int size) {
+  RawBytes(ctx, bytes, size, kSqliteTransient);
+}
+
+inline void Error(sqlite3_context* ctx, const char* error) {
+  sqlite3_result_error(ctx, error, -1);
+}
+
+inline void Value(sqlite3_context* ctx, sqlite3_value* value) {
+  sqlite3_result_value(ctx, value);
+}
+
+inline void RawPointer(sqlite3_context* ctx,
+                       void* ptr,
+                       const char* name,
+                       sqlite3_destructor_type destructor) {
+  sqlite3_result_pointer(ctx, ptr, name, destructor);
+}
+inline void StaticPointer(sqlite3_context* ctx, void* ptr, const char* name) {
+  RawPointer(ctx, ptr, name, nullptr);
+}
+
+}  // namespace perfetto::trace_processor::sqlite::result
+
+#endif  // SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_RESULT_H_
diff --git a/src/trace_processor/sqlite/bindings/sqlite_window_function.h b/src/trace_processor/sqlite/bindings/sqlite_window_function.h
new file mode 100644
index 0000000..cba290b
--- /dev/null
+++ b/src/trace_processor/sqlite/bindings/sqlite_window_function.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_WINDOW_FUNCTION_H_
+#define SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_WINDOW_FUNCTION_H_
+
+struct sqlite3_context;
+struct sqlite3_value;
+
+namespace perfetto::trace_processor {
+
+// Prototype for a window function which can be registered with SQLite.
+//
+// See https://www.sqlite.org/windowfunctions.html#udfwinfunc for details on how
+// to implement the methods of this class.
+class SqliteWindowFunction {
+ public:
+  // The type of the context object which will be passed to the function.
+  // Can be redefined in any sub-classes to override the context.
+  using Context = void;
+
+  // The xStep function which will be executed by SQLite to add a row of values
+  // to the current window.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static void Step(sqlite3_context*, int argc, sqlite3_value** argv);
+
+  // The xStep function which will be executed by SQLite to remove a row of
+  // values from the current window.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static void Inverse(sqlite3_context* ctx, int argc, sqlite3_value** argv);
+
+  // The xValue function which will be executed by SQLite to obtain the current
+  // value of the aggregate.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static void Value(sqlite3_context* ctx);
+
+  // The xInverse function which will be executed by SQLite to obtain the
+  // current value of the aggregate *and* free all resources allocated by
+  // previous calls to Step, Inverse and Value.
+  //
+  // Implementations MUST define this function themselves; this function is
+  // declared but *not* defined so linker errors will be thrown if not defined.
+  static void Final(sqlite3_context* ctx);
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_SQLITE_BINDINGS_SQLITE_WINDOW_FUNCTION_H_
diff --git a/src/trace_processor/sqlite/db_sqlite_table.cc b/src/trace_processor/sqlite/db_sqlite_table.cc
index 8d18365..8c1d9a0 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table.cc
@@ -210,9 +210,10 @@
   return base::OkStatus();
 }
 
-SqliteTable::Schema DbSqliteTable::ComputeSchema(const Table::Schema& schema,
-                                                 const char* table_name) {
-  std::vector<SqliteTable::Column> schema_cols;
+SqliteTableLegacy::Schema DbSqliteTable::ComputeSchema(
+    const Table::Schema& schema,
+    const char* table_name) {
+  std::vector<SqliteTableLegacy::Column> schema_cols;
   for (uint32_t i = 0; i < schema.columns.size(); ++i) {
     const auto& col = schema.columns[i];
     schema_cols.emplace_back(i, col.name, col.type, col.is_hidden);
@@ -319,7 +320,7 @@
   {
     auto p = [&cs](const QueryConstraints::OrderBy& o) {
       auto inner_p = [&o](const QueryConstraints::Constraint& c) {
-        return c.column == o.iColumn && sqlite_utils::IsOpEq(c.op);
+        return c.column == o.iColumn && sqlite::utils::IsOpEq(c.op);
       };
       return std::any_of(cs->begin(), cs->end(), inner_p);
     };
@@ -373,7 +374,7 @@
     if (current_row_count < 2)
       break;
     const auto& col_schema = schema.columns[static_cast<uint32_t>(c.column)];
-    if (sqlite_utils::IsOpEq(c.op) && col_schema.is_id) {
+    if (sqlite::utils::IsOpEq(c.op) && col_schema.is_id) {
       // If we have an id equality constraint, we can very efficiently filter
       // down to a single row in C++. However, if we're joining with another
       // table, SQLite will do this once per row which can be extremely
@@ -382,7 +383,7 @@
       // entire filter call is ~10x the cost of iterating a single row.
       filter_cost += 10;
       current_row_count = 1;
-    } else if (sqlite_utils::IsOpEq(c.op)) {
+    } else if (sqlite::utils::IsOpEq(c.op)) {
       // If there is only a single equality constraint, we have special logic
       // to sort by that column and then binary search if we see the constraint
       // set often. Model this by dividing by the log of the number of rows as
@@ -400,8 +401,8 @@
       double estimated_rows = current_row_count / (2 * log2(current_row_count));
       current_row_count = std::max(static_cast<uint32_t>(estimated_rows), 1u);
     } else if (col_schema.is_sorted &&
-               (sqlite_utils::IsOpLe(c.op) || sqlite_utils::IsOpLt(c.op) ||
-                sqlite_utils::IsOpGt(c.op) || sqlite_utils::IsOpGe(c.op))) {
+               (sqlite::utils::IsOpLe(c.op) || sqlite::utils::IsOpLt(c.op) ||
+                sqlite::utils::IsOpGt(c.op) || sqlite::utils::IsOpGe(c.op))) {
       // On a sorted column, if we see any partition constraints, we can do this
       // filter very efficiently. Model this using the log of the  number of
       // rows as a good approximation.
@@ -437,12 +438,12 @@
   return QueryCost{final_cost, current_row_count};
 }
 
-std::unique_ptr<SqliteTable::BaseCursor> DbSqliteTable::CreateCursor() {
+std::unique_ptr<SqliteTableLegacy::BaseCursor> DbSqliteTable::CreateCursor() {
   return std::make_unique<Cursor>(this, context_->cache);
 }
 
 DbSqliteTable::Cursor::Cursor(DbSqliteTable* sqlite_table, QueryCache* cache)
-    : SqliteTable::BaseCursor(sqlite_table),
+    : SqliteTableLegacy::BaseCursor(sqlite_table),
       db_sqlite_table_(sqlite_table),
       cache_(cache) {
   switch (db_sqlite_table_->context_->computation) {
@@ -506,7 +507,7 @@
   // If the constraing is not an equality constraint, there's little
   // benefit to caching
   const auto& c = qc.constraints().front();
-  if (!sqlite_utils::IsOpEq(c.op))
+  if (!sqlite::utils::IsOpEq(c.op))
     return;
 
   // If the column is already sorted, we don't need to cache at all.
diff --git a/src/trace_processor/sqlite/db_sqlite_table.h b/src/trace_processor/sqlite/db_sqlite_table.h
index 45bc124..ded0f84 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.h
+++ b/src/trace_processor/sqlite/db_sqlite_table.h
@@ -89,7 +89,7 @@
   using Context = DbSqliteTableContext;
   using TableComputation = Context::Computation;
 
-  class Cursor final : public SqliteTable::BaseCursor {
+  class Cursor final : public SqliteTableLegacy::BaseCursor {
    public:
     Cursor(DbSqliteTable*, QueryCache*);
     ~Cursor() final;
@@ -100,7 +100,7 @@
     Cursor(Cursor&&) noexcept = delete;
     Cursor& operator=(Cursor&&) = delete;
 
-    // Implementation of SqliteTable::Cursor.
+    // Implementation of SqliteTableLegacy::Cursor.
     base::Status Filter(const QueryConstraints& qc,
                         sqlite3_value** argv,
                         FilterHistory);
@@ -128,8 +128,8 @@
       // long as we don't call Next(). However, that only happens when Next() is
       // called on the Cursor itself, at which point SQLite no longer cares
       // about the bytes pointer.
-      sqlite_utils::ReportSqlValue(ctx, value, sqlite_utils::kSqliteStatic,
-                                   sqlite_utils::kSqliteStatic);
+      sqlite::utils::ReportSqlValue(ctx, value, sqlite::utils::kSqliteStatic,
+                                    sqlite::utils::kSqliteStatic);
     }
 
    private:
@@ -197,15 +197,15 @@
   virtual ~DbSqliteTable() final;
 
   // Table implementation.
-  base::Status Init(int, const char* const*, SqliteTable::Schema*) final;
-  std::unique_ptr<SqliteTable::BaseCursor> CreateCursor() final;
+  base::Status Init(int, const char* const*, SqliteTableLegacy::Schema*) final;
+  std::unique_ptr<SqliteTableLegacy::BaseCursor> CreateCursor() final;
   base::Status ModifyConstraints(QueryConstraints*) final;
   int BestIndex(const QueryConstraints&, BestIndexInfo*) final;
 
   // These static functions are useful to allow other callers to make use
   // of them.
-  static SqliteTable::Schema ComputeSchema(const Table::Schema&,
-                                           const char* table_name);
+  static SqliteTableLegacy::Schema ComputeSchema(const Table::Schema&,
+                                                 const char* table_name);
   static void ModifyConstraints(const Table::Schema&, QueryConstraints*);
   static void BestIndex(const Table::Schema&,
                         uint32_t row_count,
diff --git a/src/trace_processor/sqlite/query_constraints.h b/src/trace_processor/sqlite/query_constraints.h
index 6ff7e5b..8fc08e7 100644
--- a/src/trace_processor/sqlite/query_constraints.h
+++ b/src/trace_processor/sqlite/query_constraints.h
@@ -46,8 +46,8 @@
     int op;
 
     // The original index of this constraint in the aConstraint array.
-    // Used internally by SqliteTable for xBestIndex - this should not be
-    // read or modified by subclasses of SqliteTable.
+    // Used internally by SqliteTableLegacy for xBestIndex - this should not be
+    // read or modified by subclasses of SqliteTableLegacy.
     int a_constraint_idx;
   };
   struct OrderBy {
diff --git a/src/trace_processor/sqlite/sql_source.cc b/src/trace_processor/sqlite/sql_source.cc
index ee86e8c..9de3a03 100644
--- a/src/trace_processor/sqlite/sql_source.cc
+++ b/src/trace_processor/sqlite/sql_source.cc
@@ -18,6 +18,7 @@
 
 #include <sqlite3.h>
 #include <algorithm>
+#include <cstddef>
 #include <cstdint>
 #include <iterator>
 #include <limits>
@@ -28,12 +29,16 @@
 #include <vector>
 
 #include "perfetto/base/logging.h"
-#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/string_utils.h"
-#include "perfetto/ext/base/sys_types.h"
 
-namespace perfetto {
-namespace trace_processor {
+#if SQLITE_VERSION_NUMBER < 3041002
+// There is a bug in pre-3.41.2 versions of SQLite where sqlite3_error_offset
+// can return an offset out of bounds. Make it a hard compiler error to prevent
+// us from hitting this bug.
+#error "SQLite version is too old."
+#endif
+
+namespace perfetto::trace_processor {
 
 namespace {
 
@@ -47,7 +52,7 @@
 
   const char* new_start = sql.c_str() + offset;
   size_t prev_nl = sql.rfind('\n', offset - 1);
-  ssize_t nl_count = std::count(sql.c_str(), new_start, '\n');
+  int64_t nl_count = std::count(sql.c_str(), new_start, '\n');
   PERFETTO_DCHECK((nl_count == 0) == (prev_nl == std::string_view::npos));
 
   if (prev_nl == std::string::npos) {
@@ -55,7 +60,7 @@
                           column + static_cast<uint32_t>(offset));
   }
 
-  ssize_t new_column = std::distance(sql.c_str() + prev_nl, new_start);
+  int64_t new_column = std::distance(sql.c_str() + prev_nl, new_start);
   return std::make_pair(line + static_cast<uint32_t>(nl_count),
                         static_cast<uint32_t>(new_column));
 }
@@ -100,24 +105,24 @@
 }
 
 SqlSource SqlSource::FromExecuteQuery(std::string sql) {
-  return SqlSource(std::move(sql), "File \"stdin\"", true);
+  return {std::move(sql), "File \"stdin\"", true};
 }
 
 SqlSource SqlSource::FromMetric(std::string sql, const std::string& name) {
-  return SqlSource(std::move(sql), "Metric \"" + name + "\"", true);
+  return {std::move(sql), "Metric \"" + name + "\"", true};
 }
 
 SqlSource SqlSource::FromMetricFile(std::string sql, const std::string& name) {
-  return SqlSource(std::move(sql), "Metric file \"" + name + "\"", false);
+  return {std::move(sql), "Metric file \"" + name + "\"", false};
 }
 
 SqlSource SqlSource::FromModuleInclude(std::string sql,
                                        const std::string& module) {
-  return SqlSource(std::move(sql), "Module include \"" + module + "\"", false);
+  return {std::move(sql), "Module include \"" + module + "\"", false};
 }
 
 SqlSource SqlSource::FromTraceProcessorImplementation(std::string sql) {
-  return SqlSource(std::move(sql), "Trace Processor Internal", false);
+  return {std::move(sql), "Trace Processor Internal", false};
 }
 
 std::string SqlSource::AsTraceback(uint32_t offset) const {
@@ -127,16 +132,7 @@
 std::string SqlSource::AsTracebackForSqliteOffset(
     std::optional<uint32_t> opt_offset) const {
   uint32_t offset = opt_offset.value_or(0);
-  // Unfortunately, there is a bug in pre-3.41.2 versions of SQLite where
-  // sqlite3_error_offset can return an offset out of bounds. In these
-  // situations, zero the offset.
-#if SQLITE_VERSION_NUMBER < 3041002
-  if (offset >= sql().size()) {
-    offset = 0;
-  }
-#else
   PERFETTO_CHECK(offset <= sql().size());
-#endif
   return AsTraceback(offset);
 }
 
@@ -389,5 +385,4 @@
   return SqlSource(std::move(orig_));
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/sqlite/sql_stats_table.cc b/src/trace_processor/sqlite/sql_stats_table.cc
index e6cdf87..3a7b1ab 100644
--- a/src/trace_processor/sqlite/sql_stats_table.cc
+++ b/src/trace_processor/sqlite/sql_stats_table.cc
@@ -17,87 +17,107 @@
 #include "src/trace_processor/sqlite/sql_stats_table.h"
 
 #include <sqlite3.h>
+#include <memory>
 
-#include <algorithm>
-#include <bitset>
-#include <numeric>
-
-#include "perfetto/base/status.h"
-#include "src/trace_processor/sqlite/sqlite_utils.h"
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
-SqlStatsTable::SqlStatsTable(sqlite3*, const TraceStorage* storage)
-    : storage_(storage) {}
-SqlStatsTable::~SqlStatsTable() = default;
-
-base::Status SqlStatsTable::Init(int, const char* const*, Schema* schema) {
-  *schema = Schema(
-      {
-          SqliteTable::Column(Column::kQuery, "query", SqlValue::Type::kString),
-          SqliteTable::Column(Column::kTimeStarted, "started",
-                              SqlValue::Type::kLong),
-          SqliteTable::Column(Column::kTimeFirstNext, "first_next",
-                              SqlValue::Type::kLong),
-          SqliteTable::Column(Column::kTimeEnded, "ended",
-                              SqlValue::Type::kLong),
-      },
-      {Column::kTimeStarted});
-  return util::OkStatus();
-}
-
-std::unique_ptr<SqliteTable::BaseCursor> SqlStatsTable::CreateCursor() {
-  return std::unique_ptr<SqliteTable::BaseCursor>(new Cursor(this));
-}
-
-int SqlStatsTable::BestIndex(const QueryConstraints&, BestIndexInfo*) {
+int SqlStatsModule::Connect(sqlite3* db,
+                            void* aux,
+                            int,
+                            const char* const*,
+                            sqlite3_vtab** vtab,
+                            char**) {
+  static constexpr char kSchema[] = R"(
+    CREATE TABLE x(
+      query TEXT,
+      started BIGINT,
+      first_next BIGINT,
+      ended BIGINT,
+      PRIMARY KEY(started)
+    ) WITHOUT ROWID
+  )";
+  if (int ret = sqlite3_declare_vtab(db, kSchema); ret != SQLITE_OK) {
+    return ret;
+  }
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->storage = GetContext(aux);
+  *vtab = res.release();
   return SQLITE_OK;
 }
 
-SqlStatsTable::Cursor::Cursor(SqlStatsTable* table)
-    : SqliteTable::BaseCursor(table),
-      storage_(table->storage_),
-      table_(table) {}
-SqlStatsTable::Cursor::~Cursor() = default;
-
-base::Status SqlStatsTable::Cursor::Filter(const QueryConstraints&,
-                                           sqlite3_value**,
-                                           FilterHistory) {
-  *this = Cursor(table_);
-  num_rows_ = storage_->sql_stats().size();
-  return base::OkStatus();
+int SqlStatsModule::Disconnect(sqlite3_vtab* vtab) {
+  delete GetVtab(vtab);
+  return SQLITE_OK;
 }
 
-base::Status SqlStatsTable::Cursor::Next() {
-  row_++;
-  return base::OkStatus();
+int SqlStatsModule::BestIndex(sqlite3_vtab*, sqlite3_index_info*) {
+  return SQLITE_OK;
 }
 
-bool SqlStatsTable::Cursor::Eof() {
-  return row_ >= num_rows_;
+int SqlStatsModule::Open(sqlite3_vtab* raw_vtab, sqlite3_vtab_cursor** cursor) {
+  std::unique_ptr<Cursor> c = std::make_unique<Cursor>();
+  c->storage = GetVtab(raw_vtab)->storage;
+  *cursor = c.release();
+  return SQLITE_OK;
 }
 
-base::Status SqlStatsTable::Cursor::Column(sqlite3_context* context, int col) {
-  const TraceStorage::SqlStats& stats = storage_->sql_stats();
-  switch (col) {
+int SqlStatsModule::Close(sqlite3_vtab_cursor* cursor) {
+  delete GetCursor(cursor);
+  return SQLITE_OK;
+}
+
+int SqlStatsModule::Filter(sqlite3_vtab_cursor* cursor,
+                           int,
+                           const char*,
+                           int,
+                           sqlite3_value**) {
+  auto* c = GetCursor(cursor);
+  c->row = 0;
+  c->num_rows = c->storage->sql_stats().size();
+  return SQLITE_OK;
+}
+
+int SqlStatsModule::Next(sqlite3_vtab_cursor* cursor) {
+  GetCursor(cursor)->row++;
+  return SQLITE_OK;
+}
+
+int SqlStatsModule::Eof(sqlite3_vtab_cursor* cursor) {
+  auto* c = GetCursor(cursor);
+  return c->row >= c->num_rows;
+}
+
+int SqlStatsModule::Column(sqlite3_vtab_cursor* cursor,
+                           sqlite3_context* ctx,
+                           int N) {
+  auto* c = GetCursor(cursor);
+  const TraceStorage::SqlStats& stats = c->storage->sql_stats();
+  switch (N) {
     case Column::kQuery:
-      sqlite3_result_text(context, stats.queries()[row_].c_str(), -1,
-                          sqlite_utils::kSqliteStatic);
+      sqlite::result::StaticString(ctx, stats.queries()[c->row].c_str());
       break;
     case Column::kTimeStarted:
-      sqlite3_result_int64(context, stats.times_started()[row_]);
+      sqlite::result::Long(ctx, stats.times_started()[c->row]);
       break;
     case Column::kTimeFirstNext:
-      sqlite3_result_int64(context, stats.times_first_next()[row_]);
+      sqlite::result::Long(ctx, stats.times_first_next()[c->row]);
       break;
     case Column::kTimeEnded:
-      sqlite3_result_int64(context, stats.times_ended()[row_]);
+      sqlite::result::Long(ctx, stats.times_ended()[c->row]);
+      break;
+    default:
+      PERFETTO_FATAL("Unknown column %d", N);
       break;
   }
-  return base::OkStatus();
+  return SQLITE_OK;
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+int SqlStatsModule::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
+  return SQLITE_ERROR;
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/sqlite/sql_stats_table.h b/src/trace_processor/sqlite/sql_stats_table.h
index d78224c..45d5b14 100644
--- a/src/trace_processor/sqlite/sql_stats_table.h
+++ b/src/trace_processor/sqlite/sql_stats_table.h
@@ -17,23 +17,27 @@
 #ifndef SRC_TRACE_PROCESSOR_SQLITE_SQL_STATS_TABLE_H_
 #define SRC_TRACE_PROCESSOR_SQLITE_SQL_STATS_TABLE_H_
 
-#include <limits>
-#include <memory>
+#include <cstddef>
 
-#include "perfetto/base/status.h"
-#include "src/trace_processor/sqlite/sqlite_table.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class QueryConstraints;
 class TraceStorage;
 
 // A virtual table that allows to introspect performances of the SQL engine
 // for the kMaxLogEntries queries.
-class SqlStatsTable final
-    : public TypedSqliteTable<SqlStatsTable, const TraceStorage*> {
- public:
+struct SqlStatsModule : sqlite::Module<SqlStatsModule> {
+  using Context = TraceStorage;
+  struct Vtab : sqlite::Module<SqlStatsModule>::Vtab {
+    TraceStorage* storage = nullptr;
+  };
+  struct Cursor : sqlite::Module<SqlStatsModule>::Cursor {
+    const TraceStorage* storage = nullptr;
+    size_t row = 0;
+    size_t num_rows = 0;
+  };
   enum Column {
     kQuery = 0,
     kTimeStarted = 1,
@@ -41,46 +45,34 @@
     kTimeEnded = 3,
   };
 
-  // Implementation of the SQLite cursor interface.
-  class Cursor final : public SqliteTable::BaseCursor {
-   public:
-    explicit Cursor(SqlStatsTable* storage);
-    ~Cursor() final;
+  static constexpr auto kType = kEponymousOnly;
+  static constexpr bool kSupportsWrites = false;
+  static constexpr bool kDoesOverloadFunctions = false;
 
-    // Implementation of SqliteTable::Cursor.
-    base::Status Filter(const QueryConstraints&,
-                        sqlite3_value**,
-                        FilterHistory);
-    base::Status Next();
-    bool Eof();
-    base::Status Column(sqlite3_context*, int N);
+  static int Connect(sqlite3*,
+                     void*,
+                     int,
+                     const char* const*,
+                     sqlite3_vtab**,
+                     char**);
+  static int Disconnect(sqlite3_vtab*);
 
-   private:
-    Cursor(Cursor&) = delete;
-    Cursor& operator=(const Cursor&) = delete;
+  static int BestIndex(sqlite3_vtab*, sqlite3_index_info*);
 
-    Cursor(Cursor&&) noexcept = default;
-    Cursor& operator=(Cursor&&) = default;
+  static int Open(sqlite3_vtab*, sqlite3_vtab_cursor**);
+  static int Close(sqlite3_vtab_cursor*);
 
-    size_t row_ = 0;
-    size_t num_rows_ = 0;
-    const TraceStorage* storage_ = nullptr;
-    SqlStatsTable* table_ = nullptr;
-  };
-
-  SqlStatsTable(sqlite3*, const TraceStorage* storage);
-  ~SqlStatsTable() final;
-
-  // Table implementation.
-  base::Status Init(int, const char* const*, Schema*) final;
-  std::unique_ptr<SqliteTable::BaseCursor> CreateCursor() final;
-  int BestIndex(const QueryConstraints&, BestIndexInfo*) final;
-
- private:
-  const TraceStorage* const storage_;
+  static int Filter(sqlite3_vtab_cursor*,
+                    int,
+                    const char*,
+                    int,
+                    sqlite3_value**);
+  static int Next(sqlite3_vtab_cursor*);
+  static int Eof(sqlite3_vtab_cursor*);
+  static int Column(sqlite3_vtab_cursor*, sqlite3_context*, int);
+  static int Rowid(sqlite3_vtab_cursor*, sqlite_int64*);
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_SQLITE_SQL_STATS_TABLE_H_
diff --git a/src/trace_processor/sqlite/sqlite_engine.cc b/src/trace_processor/sqlite/sqlite_engine.cc
index 9c5da35..00defc2 100644
--- a/src/trace_processor/sqlite/sqlite_engine.cc
+++ b/src/trace_processor/sqlite/sqlite_engine.cc
@@ -123,7 +123,7 @@
   for (auto it = all_created_sqlite_tables_.rbegin();
        it != all_created_sqlite_tables_.rend(); it++) {
     if (auto* type = sqlite_tables_.Find(*it);
-        !type || *type != SqliteTable::TableType::kExplicitCreate) {
+        !type || *type != SqliteTableLegacy::TableType::kExplicitCreate) {
       continue;
     }
     if (auto it_and_ins = dropped_tables.insert(*it); !it_and_ins.second) {
@@ -199,6 +199,43 @@
   return base::OkStatus();
 }
 
+base::Status SqliteEngine::RegisterAggregateFunction(
+    const char* name,
+    int argc,
+    AggregateFnStep* step,
+    AggregateFnFinal* final,
+    void* ctx,
+    FnCtxDestructor* destructor,
+    bool deterministic) {
+  int flags = SQLITE_UTF8 | (deterministic ? SQLITE_DETERMINISTIC : 0);
+  int ret =
+      sqlite3_create_function_v2(db_.get(), name, static_cast<int>(argc), flags,
+                                 ctx, nullptr, step, final, destructor);
+  if (ret != SQLITE_OK) {
+    return base::ErrStatus("Unable to register function with name %s", name);
+  }
+  return base::OkStatus();
+}
+
+base::Status SqliteEngine::RegisterWindowFunction(const char* name,
+                                                  int argc,
+                                                  WindowFnStep* step,
+                                                  WindowFnInverse* inverse,
+                                                  WindowFnValue* value,
+                                                  WindowFnFinal* final,
+                                                  void* ctx,
+                                                  FnCtxDestructor* destructor,
+                                                  bool deterministic) {
+  int flags = SQLITE_UTF8 | (deterministic ? SQLITE_DETERMINISTIC : 0);
+  int ret = sqlite3_create_window_function(
+      db_.get(), name, static_cast<int>(argc), flags, ctx, step, final, value,
+      inverse, destructor);
+  if (ret != SQLITE_OK) {
+    return base::ErrStatus("Unable to register function with name %s", name);
+  }
+  return base::OkStatus();
+}
+
 base::Status SqliteEngine::UnregisterFunction(const char* name, int argc) {
   int ret = sqlite3_create_function_v2(db_.get(), name, static_cast<int>(argc),
                                        SQLITE_UTF8, nullptr, nullptr, nullptr,
@@ -219,8 +256,9 @@
   return base::OkStatus();
 }
 
-base::Status SqliteEngine::SaveSqliteTable(const std::string& table_name,
-                                           std::unique_ptr<SqliteTable> table) {
+base::Status SqliteEngine::SaveSqliteTable(
+    const std::string& table_name,
+    std::unique_ptr<SqliteTableLegacy> table) {
   auto res = saved_tables_.Insert(table_name, {});
   if (!res.second) {
     return base::ErrStatus("Table with name %s already is saved",
@@ -230,14 +268,14 @@
   return base::OkStatus();
 }
 
-base::StatusOr<std::unique_ptr<SqliteTable>> SqliteEngine::RestoreSqliteTable(
-    const std::string& table_name) {
+base::StatusOr<std::unique_ptr<SqliteTableLegacy>>
+SqliteEngine::RestoreSqliteTable(const std::string& table_name) {
   auto* res = saved_tables_.Find(table_name);
   if (!res) {
     return base::ErrStatus("Table with name %s does not exist in saved state",
                            table_name.c_str());
   }
-  std::unique_ptr<SqliteTable> table = std::move(*res);
+  std::unique_ptr<SqliteTableLegacy> table = std::move(*res);
   PERFETTO_CHECK(saved_tables_.Erase(table_name));
   return std::move(table);
 }
@@ -252,7 +290,7 @@
 }
 
 void SqliteEngine::OnSqliteTableCreated(const std::string& name,
-                                        SqliteTable::TableType type) {
+                                        SqliteTableLegacy::TableType type) {
   auto it_and_inserted = sqlite_tables_.Insert(name, type);
   PERFETTO_CHECK(it_and_inserted.second);
   all_created_sqlite_tables_.push_back(name);
diff --git a/src/trace_processor/sqlite/sqlite_engine.h b/src/trace_processor/sqlite/sqlite_engine.h
index 9af23fe..55e991f 100644
--- a/src/trace_processor/sqlite/sqlite_engine.h
+++ b/src/trace_processor/sqlite/sqlite_engine.h
@@ -26,10 +26,12 @@
 #include <type_traits>
 #include <vector>
 
+#include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/hash.h"
 #include "src/trace_processor/db/table.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
 #include "src/trace_processor/sqlite/query_cache.h"
 #include "src/trace_processor/sqlite/scoped_db.h"
 #include "src/trace_processor/sqlite/sql_source.h"
@@ -51,6 +53,18 @@
 class SqliteEngine {
  public:
   using Fn = void(sqlite3_context* ctx, int argc, sqlite3_value** argv);
+  using AggregateFnStep = void(sqlite3_context* ctx,
+                               int argc,
+                               sqlite3_value** argv);
+  using AggregateFnFinal = void(sqlite3_context* ctx);
+  using WindowFnStep = void(sqlite3_context* ctx,
+                            int argc,
+                            sqlite3_value** argv);
+  using WindowFnInverse = void(sqlite3_context* ctx,
+                               int argc,
+                               sqlite3_value** argv);
+  using WindowFnValue = void(sqlite3_context* ctx);
+  using WindowFnFinal = void(sqlite3_context* ctx);
   using FnCtxDestructor = void(void*);
 
   // Wrapper class for SQLite's |sqlite3_stmt| struct and associated functions.
@@ -90,14 +104,44 @@
                                 FnCtxDestructor* ctx_destructor,
                                 bool deterministic);
 
+  // Registers a C++ aggregate function to be runnable from SQL.
+  base::Status RegisterAggregateFunction(const char* name,
+                                         int argc,
+                                         AggregateFnStep* step,
+                                         AggregateFnFinal* final,
+                                         void* ctx,
+                                         FnCtxDestructor* ctx_destructor,
+                                         bool deterministic);
+
+  // Registers a C++ window function to be runnable from SQL.
+  base::Status RegisterWindowFunction(const char* name,
+                                      int argc,
+                                      WindowFnStep* step,
+                                      WindowFnInverse* inverse,
+                                      WindowFnValue* value,
+                                      WindowFnFinal* final,
+                                      void* ctx,
+                                      FnCtxDestructor* ctx_destructor,
+                                      bool deterministic);
+
   // Unregisters a C++ function from SQL.
   base::Status UnregisterFunction(const char* name, int argc);
 
   // Registers a SQLite virtual table module with the given name.
+  template <typename Module>
+  void RegisterVirtualTableModule(const std::string& module_name,
+                                  typename Module::Context* ctx);
+
+  // Registers a SQLite virtual table module with the given name.
+  template <typename Module>
+  void RegisterVirtualTableModule(const std::string& module_name,
+                                  std::unique_ptr<typename Module::Context>);
+
+  // Registers a SQLite virtual table module with the given name.
   template <typename Vtab, typename Context>
   void RegisterVirtualTableModule(const std::string& module_name,
                                   Context ctx,
-                                  SqliteTable::TableType table_type,
+                                  SqliteTableLegacy::TableType table_type,
                                   bool updatable);
 
   // Declares a virtual table with SQLite.
@@ -105,19 +149,20 @@
 
   // Saves a SQLite table across a pair of xDisconnect/xConnect callbacks.
   base::Status SaveSqliteTable(const std::string& table_name,
-                               std::unique_ptr<SqliteTable>);
+                               std::unique_ptr<SqliteTableLegacy>);
 
   // Restores a SQLite table across a pair of xDisconnect/xConnect callbacks.
-  base::StatusOr<std::unique_ptr<SqliteTable>> RestoreSqliteTable(
+  base::StatusOr<std::unique_ptr<SqliteTableLegacy>> RestoreSqliteTable(
       const std::string& table_name);
 
   // Gets the context for a registered SQL function.
   void* GetFunctionContext(const std::string& name, int argc);
 
-  // Should be called when a SqliteTable instance is created.
-  void OnSqliteTableCreated(const std::string& name, SqliteTable::TableType);
+  // Should be called when a SqliteTableLegacy instance is created.
+  void OnSqliteTableCreated(const std::string& name,
+                            SqliteTableLegacy::TableType);
 
-  // Should be called when a SqliteTable instance is destroyed.
+  // Should be called when a SqliteTableLegacy instance is destroyed.
   void OnSqliteTableDestroyed(const std::string& name);
 
   sqlite3* db() const { return db_.get(); }
@@ -137,9 +182,10 @@
   SqliteEngine(SqliteEngine&&) noexcept = delete;
   SqliteEngine& operator=(SqliteEngine&&) = delete;
 
-  base::FlatHashMap<std::string, SqliteTable::TableType> sqlite_tables_;
+  base::FlatHashMap<std::string, SqliteTableLegacy::TableType> sqlite_tables_;
   std::vector<std::string> all_created_sqlite_tables_;
-  base::FlatHashMap<std::string, std::unique_ptr<SqliteTable>> saved_tables_;
+  base::FlatHashMap<std::string, std::unique_ptr<SqliteTableLegacy>>
+      saved_tables_;
   base::FlatHashMap<std::pair<std::string, int>, void*, FnHasher> fn_ctx_;
 
   ScopedDb db_;
@@ -152,15 +198,37 @@
 // in the header file because it is templated code. We separate it out
 // like this to keep the API people actually care about easy to read.
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
+
+template <typename Module>
+void SqliteEngine::RegisterVirtualTableModule(const std::string& module_name,
+                                              typename Module::Context* ctx) {
+  static_assert(std::is_base_of_v<sqlite::Module<Module>, Module>,
+                "Must subclass sqlite::Module");
+  int res = sqlite3_create_module_v2(db_.get(), module_name.c_str(),
+                                     &Module::kModule, ctx, nullptr);
+  PERFETTO_CHECK(res == SQLITE_OK);
+}
+
+template <typename Module>
+void SqliteEngine::RegisterVirtualTableModule(
+    const std::string& module_name,
+    std::unique_ptr<typename Module::Context> ctx) {
+  static_assert(std::is_base_of_v<sqlite::Module<Module>, Module>,
+                "Must subclass sqlite::Module");
+  int res = sqlite3_create_module_v2(
+      db_.get(), module_name.c_str(), &Module::kModule, ctx.release(),
+      [](void* arg) { delete static_cast<typename Module::Context*>(arg); });
+  PERFETTO_CHECK(res == SQLITE_OK);
+}
 
 template <typename Vtab, typename Context>
-void SqliteEngine::RegisterVirtualTableModule(const std::string& module_name,
-                                              Context ctx,
-                                              SqliteTable::TableType table_type,
-                                              bool updatable) {
-  static_assert(std::is_base_of_v<SqliteTable, Vtab>,
+void SqliteEngine::RegisterVirtualTableModule(
+    const std::string& module_name,
+    Context ctx,
+    SqliteTableLegacy::TableType table_type,
+    bool updatable) {
+  static_assert(std::is_base_of_v<SqliteTableLegacy, Vtab>,
                 "Must subclass TypedSqliteTable");
 
   auto module_arg =
@@ -172,7 +240,6 @@
   PERFETTO_CHECK(res == SQLITE_OK);
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_SQLITE_SQLITE_ENGINE_H_
diff --git a/src/trace_processor/sqlite/sqlite_table.cc b/src/trace_processor/sqlite/sqlite_table.cc
index c599b2f..7411657 100644
--- a/src/trace_processor/sqlite/sqlite_table.cc
+++ b/src/trace_processor/sqlite/sqlite_table.cc
@@ -82,7 +82,7 @@
       return "limit";
     case SQLITE_INDEX_CONSTRAINT_OFFSET:
       return "offset";
-    case SqliteTable::CustomFilterOpcode::kSourceGeqOpCode:
+    case SqliteTableLegacy::CustomFilterOpcode::kSourceGeqOpCode:
       return "source_geq";
     default:
       PERFETTO_FATAL("Operator to string conversion not impemented for %d", op);
@@ -90,7 +90,7 @@
 }
 
 void ConstraintsToString(const QueryConstraints& qc,
-                         const SqliteTable::Schema& schema,
+                         const SqliteTableLegacy::Schema& schema,
                          std::string& out) {
   bool is_first = true;
   for (const auto& cs : qc.constraints()) {
@@ -105,7 +105,7 @@
 }
 
 void OrderByToString(const QueryConstraints& qc,
-                     const SqliteTable::Schema& schema,
+                     const SqliteTableLegacy::Schema& schema,
                      std::string& out) {
   bool is_first = true;
   for (const auto& ob : qc.order_by()) {
@@ -120,7 +120,7 @@
 }
 
 std::string QcDebugStr(const QueryConstraints& qc,
-                       const SqliteTable::Schema& schema) {
+                       const SqliteTableLegacy::Schema& schema) {
   std::string str_result;
   str_result.reserve(512);
 
@@ -144,7 +144,7 @@
 
 void WriteQueryConstraintsToMetatrace(metatrace::Record* r,
                                       const QueryConstraints& qc,
-                                      const SqliteTable::Schema& schema) {
+                                      const SqliteTableLegacy::Schema& schema) {
   r->AddArg("constraint_count", std::to_string(qc.constraints().size()));
   std::string constraints;
   ConstraintsToString(qc, schema, constraints);
@@ -159,24 +159,26 @@
 }  // namespace
 
 // static
-bool SqliteTable::debug = false;
+bool SqliteTableLegacy::debug = false;
 
-SqliteTable::SqliteTable() = default;
-SqliteTable::~SqliteTable() = default;
+SqliteTableLegacy::SqliteTableLegacy() = default;
+SqliteTableLegacy::~SqliteTableLegacy() = default;
 
-base::Status SqliteTable::ModifyConstraints(QueryConstraints*) {
+base::Status SqliteTableLegacy::ModifyConstraints(QueryConstraints*) {
   return base::OkStatus();
 }
 
-int SqliteTable::FindFunction(const char*, FindFunctionFn*, void**) {
+int SqliteTableLegacy::FindFunction(const char*, FindFunctionFn*, void**) {
   return 0;
 }
 
-base::Status SqliteTable::Update(int, sqlite3_value**, sqlite3_int64*) {
+base::Status SqliteTableLegacy::Update(int, sqlite3_value**, sqlite3_int64*) {
   return base::ErrStatus("Updating not supported");
 }
 
-bool SqliteTable::ReadConstraints(int idxNum, const char* idxStr, int argc) {
+bool SqliteTableLegacy::ReadConstraints(int idxNum,
+                                        const char* idxStr,
+                                        int argc) {
   bool cache_hit = true;
   if (idxNum != qc_hash_) {
     qc_cache_ = QueryConstraints::FromString(idxStr);
@@ -196,7 +198,7 @@
   // Logging this every ReadConstraints just leads to log spam on joins making
   // it unusable. Instead, only print this out when we miss the cache (which
   // happens precisely when the constraint set from SQLite changes.)
-  if (SqliteTable::debug && !cache_hit) {
+  if (SqliteTableLegacy::debug && !cache_hit) {
     PERFETTO_LOG("[%s::ParseConstraints] constraints=%s argc=%d", name_.c_str(),
                  QcDebugStr(qc_cache_, schema_).c_str(), argc);
   }
@@ -204,34 +206,35 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////
-// SqliteTable::BaseCursor implementation
+// SqliteTableLegacy::BaseCursor implementation
 ////////////////////////////////////////////////////////////////////////////////
 
-SqliteTable::BaseCursor::BaseCursor(SqliteTable* table) : table_(table) {
+SqliteTableLegacy::BaseCursor::BaseCursor(SqliteTableLegacy* table)
+    : table_(table) {
   // This is required to prevent us from leaving this field uninitialised if
   // we ever move construct the Cursor.
   pVtab = table;
 }
-SqliteTable::BaseCursor::~BaseCursor() = default;
+SqliteTableLegacy::BaseCursor::~BaseCursor() = default;
 
 ////////////////////////////////////////////////////////////////////////////////
-// SqliteTable::Column implementation
+// SqliteTableLegacy::Column implementation
 ////////////////////////////////////////////////////////////////////////////////
 
-SqliteTable::Column::Column(size_t index,
-                            std::string name,
-                            SqlValue::Type type,
-                            bool hidden)
+SqliteTableLegacy::Column::Column(size_t index,
+                                  std::string name,
+                                  SqlValue::Type type,
+                                  bool hidden)
     : index_(index), name_(name), type_(type), hidden_(hidden) {}
 
 ////////////////////////////////////////////////////////////////////////////////
-// SqliteTable::Schema implementation
+// SqliteTableLegacy::Schema implementation
 ////////////////////////////////////////////////////////////////////////////////
 
-SqliteTable::Schema::Schema() = default;
+SqliteTableLegacy::Schema::Schema() = default;
 
-SqliteTable::Schema::Schema(std::vector<Column> columns,
-                            std::vector<size_t> primary_keys)
+SqliteTableLegacy::Schema::Schema(std::vector<Column> columns,
+                                  std::vector<size_t> primary_keys)
     : columns_(std::move(columns)), primary_keys_(std::move(primary_keys)) {
   for (size_t i = 0; i < columns_.size(); i++) {
     PERFETTO_CHECK(columns_[i].index() == i);
@@ -241,10 +244,11 @@
   }
 }
 
-SqliteTable::Schema::Schema(const Schema&) = default;
-SqliteTable::Schema& SqliteTable::Schema::operator=(const Schema&) = default;
+SqliteTableLegacy::Schema::Schema(const Schema&) = default;
+SqliteTableLegacy::Schema& SqliteTableLegacy::Schema::operator=(const Schema&) =
+    default;
 
-std::string SqliteTable::Schema::ToCreateTableStmt() const {
+std::string SqliteTableLegacy::Schema::ToCreateTableStmt() const {
   std::string stmt = "CREATE TABLE x(";
   for (size_t i = 0; i < columns_.size(); ++i) {
     const Column& col = columns_[i];
@@ -279,7 +283,7 @@
 TypedSqliteTableBase::~TypedSqliteTableBase() = default;
 
 base::Status TypedSqliteTableBase::DeclareAndAssignVtab(
-    std::unique_ptr<SqliteTable> table,
+    std::unique_ptr<SqliteTableLegacy> table,
     sqlite3_vtab** tab) {
   auto create_stmt = table->schema().ToCreateTableStmt();
   PERFETTO_DLOG("Create table statement: %s", create_stmt.c_str());
@@ -289,7 +293,7 @@
 }
 
 int TypedSqliteTableBase::xDestroy(sqlite3_vtab* t) {
-  auto* table = static_cast<SqliteTable*>(t);
+  auto* table = static_cast<SqliteTableLegacy*>(t);
   table->engine_->OnSqliteTableDestroyed(table->name_);
   delete table;
   return SQLITE_OK;
@@ -309,7 +313,7 @@
 
   // SQLite guarantees that argv[2] contains the name of the table.
   std::string table_name = argv[2];
-  base::StatusOr<std::unique_ptr<SqliteTable>> table =
+  base::StatusOr<std::unique_ptr<SqliteTableLegacy>> table =
       xArg->engine->RestoreSqliteTable(table_name);
   if (!table.status().ok()) {
     *pzErr = sqlite3_mprintf("%s", table.status().c_message());
@@ -326,7 +330,7 @@
 int TypedSqliteTableBase::xDisconnectSaveTable(sqlite3_vtab* t) {
   auto* table = static_cast<TypedSqliteTableBase*>(t);
   base::Status status = table->engine_->SaveSqliteTable(
-      table->name(), std::unique_ptr<SqliteTable>(table));
+      table->name(), std::unique_ptr<SqliteTableLegacy>(table));
   return table->SetStatusAndReturn(status);
 }
 
@@ -423,7 +427,7 @@
       });
 
   auto out_qc_str = qc.ToNewSqlite3String();
-  if (SqliteTable::debug) {
+  if (SqliteTableLegacy::debug) {
     PERFETTO_LOG(
         "[%s::BestIndex] constraints=%s orderByConsumed=%d estimatedCost=%f "
         "estimatedRows=%" PRId64,
diff --git a/src/trace_processor/sqlite/sqlite_table.h b/src/trace_processor/sqlite/sqlite_table.h
index b2a7e40..d3b1c32 100644
--- a/src/trace_processor/sqlite/sqlite_table.h
+++ b/src/trace_processor/sqlite/sqlite_table.h
@@ -35,9 +35,9 @@
 // Abstract base class representing a SQLite virtual table. Implements the
 // common bookeeping required across all tables and allows subclasses to
 // implement a friendlier API than that required by SQLite.
-class SqliteTable : public sqlite3_vtab {
+class SqliteTableLegacy : public sqlite3_vtab {
  public:
-  // Custom opcodes used by subclasses of SqliteTable.
+  // Custom opcodes used by subclasses of SqliteTableLegacy.
   // Stored here as we need a central repository of opcodes to prevent clashes
   // between different sub-classes.
   enum CustomFilterOpcode {
@@ -82,7 +82,7 @@
       kSame = 1,
     };
 
-    explicit BaseCursor(SqliteTable* table);
+    explicit BaseCursor(SqliteTableLegacy* table);
     virtual ~BaseCursor();
 
     // Methods to be implemented by derived table classes.
@@ -105,7 +105,7 @@
     // Used to extract the value from the column at index |N|.
     void Column(sqlite3_context* context, int N);
 
-    SqliteTable* table() const { return table_; }
+    SqliteTableLegacy* table() const { return table_; }
 
    protected:
     BaseCursor(BaseCursor&) = delete;
@@ -115,7 +115,7 @@
     BaseCursor& operator=(BaseCursor&&) = default;
 
    private:
-    SqliteTable* table_ = nullptr;
+    SqliteTableLegacy* table_ = nullptr;
   };
 
   // The schema of the table. Created by subclasses to allow the table class to
@@ -157,7 +157,7 @@
   };
 
   // Public for unique_ptr destructor calls.
-  virtual ~SqliteTable();
+  virtual ~SqliteTableLegacy();
 
   // When set it logs all BestIndex and Filter actions on the console.
   static bool debug;
@@ -188,7 +188,7 @@
     int64_t estimated_rows = 0;
   };
 
-  SqliteTable();
+  SqliteTableLegacy();
 
   // Methods to be implemented by derived table classes.
   virtual base::Status Init(int argc, const char* const* argv, Schema*) = 0;
@@ -214,8 +214,8 @@
   friend class TypedSqliteTable;
   friend class TypedSqliteTableBase;
 
-  SqliteTable(const SqliteTable&) = delete;
-  SqliteTable& operator=(const SqliteTable&) = delete;
+  SqliteTableLegacy(const SqliteTableLegacy&) = delete;
+  SqliteTableLegacy& operator=(const SqliteTableLegacy&) = delete;
 
   // The engine class this table is registered with. Used for restoring/saving
   // the table.
@@ -238,7 +238,7 @@
   int best_index_num_ = 0;
 };
 
-class TypedSqliteTableBase : public SqliteTable {
+class TypedSqliteTableBase : public SqliteTableLegacy {
  protected:
   struct BaseModuleArg {
     sqlite3_module module;
@@ -262,8 +262,9 @@
   static int xOpen(sqlite3_vtab*, sqlite3_vtab_cursor**);
   static int xBestIndex(sqlite3_vtab*, sqlite3_index_info*);
 
-  static base::Status DeclareAndAssignVtab(std::unique_ptr<SqliteTable> table,
-                                           sqlite3_vtab** tab);
+  static base::Status DeclareAndAssignVtab(
+      std::unique_ptr<SqliteTableLegacy> table,
+      sqlite3_vtab** tab);
 
   base::Status InitInternal(SqliteEngine* engine,
                             int argc,
diff --git a/src/trace_processor/sqlite/sqlite_utils.cc b/src/trace_processor/sqlite/sqlite_utils.cc
index 4165f2d..4e6277d 100644
--- a/src/trace_processor/sqlite/sqlite_utils.cc
+++ b/src/trace_processor/sqlite/sqlite_utils.cc
@@ -15,15 +15,24 @@
  */
 
 #include "src/trace_processor/sqlite/sqlite_utils.h"
-#include <bitset>
+
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <optional>
 #include <sstream>
+#include <string>
+#include <vector>
+
+#include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/sqlite/scoped_db.h"
+#include "src/trace_processor/sqlite/sqlite_table.h"
 
-namespace perfetto {
-namespace trace_processor {
-namespace sqlite_utils {
+namespace perfetto::trace_processor::sqlite::utils {
 namespace internal {
 namespace {
 std::string ToExpectedTypesString(ExpectedTypesSet expected_types) {
@@ -66,14 +75,12 @@
     return MissingArgumentError(argument_name);
   }
 
-  SqlValue value = sqlite_utils::SqliteValueToSqlValue(argv[arg_index]);
-
+  SqlValue value = sqlite::utils::SqliteValueToSqlValue(argv[arg_index]);
   if (!expected_types.test(value.type)) {
     return InvalidArgumentTypeError(argument_name, arg_index, value.type,
                                     expected_types);
   }
-
-  return std::move(value);
+  return value;
 }
 }  // namespace internal
 
@@ -82,13 +89,13 @@
   int len = sqlite3_value_bytes16(value);
   PERFETTO_CHECK(len >= 0);
   size_t count = static_cast<size_t>(len) / sizeof(wchar_t);
-  return std::wstring(
-      reinterpret_cast<const wchar_t*>(sqlite3_value_text16(value)), count);
+  return {reinterpret_cast<const wchar_t*>(sqlite3_value_text16(value)), count};
 }
 
-base::Status GetColumnsForTable(sqlite3* db,
-                                const std::string& raw_table_name,
-                                std::vector<SqliteTable::Column>& columns) {
+base::Status GetColumnsForTable(
+    sqlite3* db,
+    const std::string& raw_table_name,
+    std::vector<SqliteTableLegacy::Column>& columns) {
   PERFETTO_DCHECK(columns.empty());
   char sql[1024];
   const char kRawSql[] = "SELECT name, type from pragma_table_info(\"%s\")";
@@ -244,7 +251,7 @@
                                   SqlValue::Type expected_type,
                                   const char* expected_type_str) {
   SqlValue::Type actual_type =
-      sqlite_utils::SqliteTypeToSqlValueType(sqlite3_value_type(value));
+      sqlite::utils::SqliteTypeToSqlValueType(sqlite3_value_type(value));
   if (actual_type != SqlValue::Type::kNull && actual_type != expected_type) {
     return base::ErrStatus(
         "does not have expected type: expected %s, actual %s",
@@ -328,11 +335,9 @@
 
 base::Status ToInvalidArgumentError(const char* argument_name,
                                     size_t arg_index,
-                                    const base::Status error) {
+                                    const base::Status& error) {
   return base::ErrStatus("argument %s at pos %zu: %s", argument_name,
                          arg_index + 1, error.message().c_str());
 }
 
-}  // namespace sqlite_utils
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::sqlite::utils
diff --git a/src/trace_processor/sqlite/sqlite_utils.h b/src/trace_processor/sqlite/sqlite_utils.h
index 7ca61b2..f3196a8 100644
--- a/src/trace_processor/sqlite/sqlite_utils.h
+++ b/src/trace_processor/sqlite/sqlite_utils.h
@@ -17,27 +17,23 @@
 #ifndef SRC_TRACE_PROCESSOR_SQLITE_SQLITE_UTILS_H_
 #define SRC_TRACE_PROCESSOR_SQLITE_SQLITE_UTILS_H_
 
-#include <math.h>
 #include <sqlite3.h>
 #include <bitset>
 #include <cstddef>
+#include <cstdint>
 #include <cstring>
 #include <optional>
 #include <string>
-#include <utility>
+#include <vector>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/status_or.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "perfetto/ext/base/string_view.h"
 #include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/sqlite/scoped_db.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
 #include "src/trace_processor/sqlite/sqlite_table.h"
 
-namespace perfetto {
-namespace trace_processor {
-namespace sqlite_utils {
+namespace perfetto::trace_processor::sqlite::utils {
 
 const auto kSqliteStatic = reinterpret_cast<sqlite3_destructor_type>(0);
 const auto kSqliteTransient = reinterpret_cast<sqlite3_destructor_type>(-1);
@@ -121,36 +117,42 @@
     sqlite3_destructor_type bytes_destructor = kSqliteTransient) {
   switch (value.type) {
     case SqlValue::Type::kLong:
-      sqlite3_result_int64(ctx, value.long_value);
+      sqlite::result::Long(ctx, value.long_value);
       break;
     case SqlValue::Type::kDouble:
-      sqlite3_result_double(ctx, value.double_value);
+      sqlite::result::Double(ctx, value.double_value);
       break;
     case SqlValue::Type::kString: {
-      sqlite3_result_text(ctx, value.string_value, -1, string_destructor);
+      sqlite::result::RawString(ctx, value.string_value, string_destructor);
       break;
     }
     case SqlValue::Type::kBytes:
-      sqlite3_result_blob(ctx, value.bytes_value,
-                          static_cast<int>(value.bytes_count),
-                          bytes_destructor);
+      sqlite::result::RawBytes(ctx, value.bytes_value,
+                               static_cast<int>(value.bytes_count),
+                               bytes_destructor);
       break;
     case SqlValue::Type::kNull:
-      sqlite3_result_null(ctx);
+      sqlite::result::Null(ctx);
       break;
   }
 }
 
-inline void SetSqliteError(sqlite3_context* ctx, const base::Status& status) {
-  PERFETTO_CHECK(!status.ok());
-  sqlite3_result_error(ctx, status.c_message(), -1);
+inline int SetError(sqlite3_vtab* tab, const char* status) {
+  sqlite3_free(tab->zErrMsg);
+  tab->zErrMsg = sqlite3_mprintf("%s", status);
+  return SQLITE_ERROR;
 }
 
-inline void SetSqliteError(sqlite3_context* ctx,
-                           const std::string& function_name,
-                           const base::Status& status) {
-  SetSqliteError(ctx, base::ErrStatus("%s: %s", function_name.c_str(),
-                                      status.c_message()));
+inline void SetError(sqlite3_context* ctx, const base::Status& status) {
+  PERFETTO_CHECK(!status.ok());
+  sqlite::result::Error(ctx, status.c_message());
+}
+
+inline void SetError(sqlite3_context* ctx,
+                     const std::string& function_name,
+                     const base::Status& status) {
+  SetError(ctx, base::ErrStatus("%s: %s", function_name.c_str(),
+                                status.c_message()));
 }
 
 // Exracts the given type from the SqlValue if |value| can fit
@@ -171,9 +173,10 @@
                                  std::optional<const char*>&);
 
 // Returns the column names for the table named by |raw_table_name|.
-base::Status GetColumnsForTable(sqlite3* db,
-                                const std::string& raw_table_name,
-                                std::vector<SqliteTable::Column>& columns);
+base::Status GetColumnsForTable(
+    sqlite3* db,
+    const std::string& raw_table_name,
+    std::vector<SqliteTableLegacy::Column>& columns);
 
 // Reads a `SQLITE_TEXT` value and returns it as a wstring (UTF-16) in the
 // default byte order. `value` must be a `SQLITE_TEXT`.
@@ -250,7 +253,7 @@
 
 base::Status ToInvalidArgumentError(const char* argument_name,
                                     size_t arg_index,
-                                    const base::Status error);
+                                    const base::Status& error);
 
 template <typename... args>
 base::StatusOr<SqlValue> ExtractArgument(size_t argc,
@@ -264,8 +267,6 @@
       internal::ToExpectedTypesSet(expected_type, expected_type_args...));
 }
 
-}  // namespace sqlite_utils
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::sqlite::utils
 
 #endif  // SRC_TRACE_PROCESSOR_SQLITE_SQLITE_UTILS_H_
diff --git a/src/trace_processor/sqlite/sqlite_utils_unittest.cc b/src/trace_processor/sqlite/sqlite_utils_unittest.cc
index be28af1..4e879d3 100644
--- a/src/trace_processor/sqlite/sqlite_utils_unittest.cc
+++ b/src/trace_processor/sqlite/sqlite_utils_unittest.cc
@@ -16,11 +16,10 @@
 
 #include "src/trace_processor/sqlite/sqlite_utils.h"
 
+#include "src/trace_processor/sqlite/scoped_db.h"
 #include "test/gtest_and_gmock.h"
 
-namespace perfetto {
-namespace trace_processor {
-namespace sqlite_utils {
+namespace perfetto::trace_processor::sqlite::utils {
 
 namespace {
 
@@ -53,8 +52,8 @@
 
 TEST_F(GetColumnsForTableTest, ValidInput) {
   RunStatement("CREATE TABLE foo (name STRING, ts INT, dur INT);");
-  std::vector<SqliteTable::Column> columns;
-  auto status = sqlite_utils::GetColumnsForTable(*db_, "foo", columns);
+  std::vector<SqliteTableLegacy::Column> columns;
+  auto status = sqlite::utils::GetColumnsForTable(*db_, "foo", columns);
   ASSERT_TRUE(status.ok());
 }
 
@@ -63,14 +62,15 @@
   // doesn't recognise. This just ensures that the query fails rather than
   // crashing.
   RunStatement("CREATE TABLE foo (name NUM, ts INT, dur INT);");
-  std::vector<SqliteTable::Column> columns;
-  auto status = sqlite_utils::GetColumnsForTable(*db_, "foo", columns);
+  std::vector<SqliteTableLegacy::Column> columns;
+  auto status = sqlite::utils::GetColumnsForTable(*db_, "foo", columns);
   ASSERT_FALSE(status.ok());
 }
 
 TEST_F(GetColumnsForTableTest, UnknownTableName) {
-  std::vector<SqliteTable::Column> columns;
-  auto status = sqlite_utils::GetColumnsForTable(*db_, "unknowntable", columns);
+  std::vector<SqliteTableLegacy::Column> columns;
+  auto status =
+      sqlite::utils::GetColumnsForTable(*db_, "unknowntable", columns);
   ASSERT_FALSE(status.ok());
 }
 
@@ -178,6 +178,4 @@
 }
 
 }  // namespace
-}  // namespace sqlite_utils
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::sqlite::utils
diff --git a/src/trace_processor/sqlite/sqlite_vtable_benchmark.cc b/src/trace_processor/sqlite/sqlite_vtable_benchmark.cc
index f231abf..c2ca5d6 100644
--- a/src/trace_processor/sqlite/sqlite_vtable_benchmark.cc
+++ b/src/trace_processor/sqlite/sqlite_vtable_benchmark.cc
@@ -19,13 +19,18 @@
 // in a buffer. This is to have a fair estimate w.r.t. cache-misses and pointer
 // chasing of what an upper-bound can be for a virtual table implementation.
 
-#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <cstdlib>
 #include <random>
+#include <string>
+#include <vector>
 
 #include <benchmark/benchmark.h>
 #include <sqlite3.h>
 
 #include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
 #include "src/trace_processor/sqlite/scoped_db.h"
 
 namespace {
@@ -76,7 +81,7 @@
   }
   PERFETTO_NO_INLINE int Next();
   PERFETTO_NO_INLINE int Column(sqlite3_context* ctx, int);
-  PERFETTO_NO_INLINE int Eof();
+  PERFETTO_NO_INLINE int Eof() const;
   void RandomFill();
 
  private:
@@ -113,7 +118,7 @@
   return SQLITE_OK;
 }
 
-int BenchmarkCursor::Eof() {
+int BenchmarkCursor::Eof() const {
   return eof_;
 }
 
@@ -202,9 +207,9 @@
   return db;
 }
 
-static void BM_SqliteStepAndResult(benchmark::State& state) {
-  size_t batch_size = static_cast<size_t>(state.range(0));
-  size_t num_cols = static_cast<size_t>(state.range(1));
+void BM_SqliteStepAndResult(benchmark::State& state) {
+  auto batch_size = static_cast<size_t>(state.range(0));
+  auto num_cols = static_cast<size_t>(state.range(1));
 
   // Make sure the module outlives the ScopedDb. SQLite calls xDisconnect in
   // the database close function and so this struct needs to be available then.
@@ -236,8 +241,8 @@
 
 BENCHMARK(BM_SqliteStepAndResult)->Apply(BenchmarkArgs);
 
-static void BM_SqliteCountOne(benchmark::State& state) {
-  size_t batch_size = static_cast<size_t>(state.range(0));
+void BM_SqliteCountOne(benchmark::State& state) {
+  auto batch_size = static_cast<size_t>(state.range(0));
 
   // Make sure the module outlives the ScopedDb. SQLite calls xDisconnect in
   // the database close function and so this struct needs to be available then.
diff --git a/src/trace_processor/sqlite/stats_table.cc b/src/trace_processor/sqlite/stats_table.cc
index 296a2f5..4c3aa6d 100644
--- a/src/trace_processor/sqlite/stats_table.cc
+++ b/src/trace_processor/sqlite/stats_table.cc
@@ -16,133 +16,157 @@
 
 #include "src/trace_processor/sqlite/stats_table.h"
 
-#include "perfetto/base/status.h"
-#include "src/trace_processor/sqlite/sqlite_utils.h"
+#include <sqlite3.h>
+#include <memory>
 
-namespace perfetto {
-namespace trace_processor {
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/storage/trace_storage.h"
 
-StatsTable::StatsTable(sqlite3*, const TraceStorage* storage)
-    : storage_(storage) {}
+namespace perfetto::trace_processor {
 
-StatsTable::~StatsTable() = default;
-
-util::Status StatsTable::Init(int, const char* const*, Schema* schema) {
-  *schema = Schema(
-      {
-          SqliteTable::Column(Column::kName, "name", SqlValue::Type::kString),
-          // Calling a column "index" causes sqlite to silently fail, hence idx.
-          SqliteTable::Column(Column::kIndex, "idx", SqlValue::Type::kLong),
-          SqliteTable::Column(Column::kSeverity, "severity",
-                              SqlValue::Type::kString),
-          SqliteTable::Column(Column::kSource, "source",
-                              SqlValue::Type::kString),
-          SqliteTable::Column(Column::kValue, "value", SqlValue::Type::kLong),
-          SqliteTable::Column(Column::kDescription, "description",
-                              SqlValue::Type::kString),
-      },
-      {Column::kName});
-  return util::OkStatus();
-}
-
-std::unique_ptr<SqliteTable::BaseCursor> StatsTable::CreateCursor() {
-  return std::unique_ptr<SqliteTable::BaseCursor>(new Cursor(this));
-}
-
-int StatsTable::BestIndex(const QueryConstraints&, BestIndexInfo*) {
+int StatsModule::Connect(sqlite3* db,
+                         void* aux,
+                         int,
+                         const char* const*,
+                         sqlite3_vtab** vtab,
+                         char**) {
+  static constexpr char kSchema[] = R"(
+    CREATE TABLE x(
+      name TEXT,
+      idx BIGINT,
+      severity TEXT,
+      source TEXT,
+      value BIGINT,
+      description TEXT,
+      PRIMARY KEY(name)
+    ) WITHOUT ROWID
+  )";
+  if (int ret = sqlite3_declare_vtab(db, kSchema); ret != SQLITE_OK) {
+    return ret;
+  }
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->storage = GetContext(aux);
+  *vtab = res.release();
   return SQLITE_OK;
 }
 
-StatsTable::Cursor::Cursor(StatsTable* table)
-    : SqliteTable::BaseCursor(table),
-      table_(table),
-      storage_(table->storage_) {}
-
-StatsTable::Cursor::~Cursor() = default;
-
-base::Status StatsTable::Cursor::Filter(const QueryConstraints&,
-                                        sqlite3_value**,
-                                        FilterHistory) {
-  *this = Cursor(table_);
-  return base::OkStatus();
+int StatsModule::Disconnect(sqlite3_vtab* vtab) {
+  delete GetVtab(vtab);
+  return SQLITE_OK;
 }
 
-base::Status StatsTable::Cursor::Column(sqlite3_context* ctx, int N) {
-  const auto kSqliteStatic = sqlite_utils::kSqliteStatic;
+int StatsModule::BestIndex(sqlite3_vtab*, sqlite3_index_info*) {
+  return SQLITE_OK;
+}
+
+int StatsModule::Open(sqlite3_vtab* raw_vtab, sqlite3_vtab_cursor** cursor) {
+  std::unique_ptr<Cursor> c = std::make_unique<Cursor>();
+  c->storage = GetVtab(raw_vtab)->storage;
+  *cursor = c.release();
+  return SQLITE_OK;
+}
+
+int StatsModule::Close(sqlite3_vtab_cursor* cursor) {
+  delete GetCursor(cursor);
+  return SQLITE_OK;
+}
+
+int StatsModule::Filter(sqlite3_vtab_cursor* cursor,
+                        int,
+                        const char*,
+                        int,
+                        sqlite3_value**) {
+  auto* c = GetCursor(cursor);
+  c->key = {};
+  c->it = {};
+  return SQLITE_OK;
+}
+
+int StatsModule::Next(sqlite3_vtab_cursor* cursor) {
+  static_assert(stats::kTypes[0] == stats::kSingle,
+                "the first stats entry cannot be indexed");
+
+  auto* c = GetCursor(cursor);
+  const auto* cur_entry = &c->storage->stats()[c->key];
+  if (stats::kTypes[c->key] == stats::kIndexed) {
+    if (++c->it != cur_entry->indexed_values.end()) {
+      return SQLITE_OK;
+    }
+  }
+  while (++c->key < stats::kNumKeys) {
+    cur_entry = &c->storage->stats()[c->key];
+    c->it = cur_entry->indexed_values.begin();
+    if (stats::kTypes[c->key] == stats::kSingle ||
+        !cur_entry->indexed_values.empty()) {
+      break;
+    }
+  }
+  return SQLITE_OK;
+}
+
+int StatsModule::Eof(sqlite3_vtab_cursor* cursor) {
+  return GetCursor(cursor)->key >= stats::kNumKeys;
+}
+
+int StatsModule::Column(sqlite3_vtab_cursor* cursor,
+                        sqlite3_context* ctx,
+                        int N) {
+  auto* c = GetCursor(cursor);
   switch (N) {
     case Column::kName:
-      sqlite3_result_text(ctx, stats::kNames[key_], -1, kSqliteStatic);
+      sqlite::result::StaticString(ctx, stats::kNames[c->key]);
       break;
     case Column::kIndex:
-      if (stats::kTypes[key_] == stats::kIndexed) {
-        sqlite3_result_int(ctx, index_->first);
+      if (stats::kTypes[c->key] == stats::kIndexed) {
+        sqlite::result::Long(ctx, c->it->first);
       } else {
-        sqlite3_result_null(ctx);
+        sqlite::result::Null(ctx);
       }
       break;
     case Column::kSeverity:
-      switch (stats::kSeverities[key_]) {
+      switch (stats::kSeverities[c->key]) {
         case stats::kInfo:
-          sqlite3_result_text(ctx, "info", -1, kSqliteStatic);
+          sqlite::result::StaticString(ctx, "info");
           break;
         case stats::kDataLoss:
-          sqlite3_result_text(ctx, "data_loss", -1, kSqliteStatic);
+          sqlite::result::StaticString(ctx, "data_loss");
           break;
         case stats::kError:
-          sqlite3_result_text(ctx, "error", -1, kSqliteStatic);
+          sqlite::result::StaticString(ctx, "error");
           break;
       }
       break;
     case Column::kSource:
-      switch (stats::kSources[key_]) {
+      switch (stats::kSources[c->key]) {
         case stats::kTrace:
-          sqlite3_result_text(ctx, "trace", -1, kSqliteStatic);
+          sqlite::result::StaticString(ctx, "trace");
           break;
         case stats::kAnalysis:
-          sqlite3_result_text(ctx, "analysis", -1, kSqliteStatic);
+          sqlite::result::StaticString(ctx, "analysis");
           break;
       }
       break;
     case Column::kValue:
-      if (stats::kTypes[key_] == stats::kIndexed) {
-        sqlite3_result_int64(ctx, index_->second);
+      if (stats::kTypes[c->key] == stats::kIndexed) {
+        sqlite::result::Long(ctx, c->it->second);
       } else {
-        sqlite3_result_int64(ctx, storage_->stats()[key_].value);
+        sqlite::result::Long(ctx, c->storage->stats()[c->key].value);
       }
       break;
     case Column::kDescription:
-      sqlite3_result_text(ctx, stats::kDescriptions[key_], -1, kSqliteStatic);
+      sqlite::result::StaticString(ctx, stats::kDescriptions[c->key]);
       break;
     default:
       PERFETTO_FATAL("Unknown column %d", N);
       break;
   }
-  return base::OkStatus();
+  return SQLITE_OK;
 }
 
-base::Status StatsTable::Cursor::Next() {
-  static_assert(stats::kTypes[0] == stats::kSingle,
-                "the first stats entry cannot be indexed");
-  const auto* cur_entry = &storage_->stats()[key_];
-  if (stats::kTypes[key_] == stats::kIndexed) {
-    if (++index_ != cur_entry->indexed_values.end()) {
-      return base::OkStatus();
-    }
-  }
-  while (++key_ < stats::kNumKeys) {
-    cur_entry = &storage_->stats()[key_];
-    index_ = cur_entry->indexed_values.begin();
-    if (stats::kTypes[key_] == stats::kSingle ||
-        !cur_entry->indexed_values.empty()) {
-      break;
-    }
-  }
-  return base::OkStatus();
+int StatsModule::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
+  return SQLITE_ERROR;
 }
 
-bool StatsTable::Cursor::Eof() {
-  return key_ >= stats::kNumKeys;
-}
-
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/sqlite/stats_table.h b/src/trace_processor/sqlite/stats_table.h
index 2824cb6..bff7ae8 100644
--- a/src/trace_processor/sqlite/stats_table.h
+++ b/src/trace_processor/sqlite/stats_table.h
@@ -17,61 +17,56 @@
 #ifndef SRC_TRACE_PROCESSOR_SQLITE_STATS_TABLE_H_
 #define SRC_TRACE_PROCESSOR_SQLITE_STATS_TABLE_H_
 
-#include <limits>
-#include <memory>
+#include <cstddef>
 
-#include "src/trace_processor/sqlite/sqlite_table.h"
-#include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // The stats table contains diagnostic info and errors that are either:
 // - Collected at trace time (e.g., ftrace buffer overruns).
 // - Generated at parsing time (e.g., clock events out-of-order).
-class StatsTable final
-    : public TypedSqliteTable<StatsTable, const TraceStorage*> {
- public:
-  enum Column { kName = 0, kIndex, kSeverity, kSource, kValue, kDescription };
-  class Cursor final : public SqliteTable::BaseCursor {
-   public:
-    explicit Cursor(StatsTable*);
-    ~Cursor() final;
-
-    // Implementation of SqliteTable::Cursor.
-    base::Status Filter(const QueryConstraints&,
-                        sqlite3_value**,
-                        FilterHistory);
-    base::Status Next();
-    bool Eof();
-    base::Status Column(sqlite3_context*, int N);
-
-   private:
-    Cursor(Cursor&) = delete;
-    Cursor& operator=(const Cursor&) = delete;
-
-    Cursor(Cursor&&) noexcept = default;
-    Cursor& operator=(Cursor&&) = default;
-
-    StatsTable* table_ = nullptr;
-    const TraceStorage* storage_ = nullptr;
-    size_t key_ = 0;
-    TraceStorage::Stats::IndexMap::const_iterator index_{};
+struct StatsModule : sqlite::Module<StatsModule> {
+  using Context = TraceStorage;
+  struct Vtab : sqlite::Module<StatsModule>::Vtab {
+    TraceStorage* storage = nullptr;
   };
+  struct Cursor : sqlite::Module<StatsModule>::Cursor {
+    const TraceStorage* storage = nullptr;
+    size_t key = 0;
+    TraceStorage::Stats::IndexMap::const_iterator it{};
+  };
+  enum Column { kName = 0, kIndex, kSeverity, kSource, kValue, kDescription };
 
-  StatsTable(sqlite3*, const TraceStorage*);
-  ~StatsTable() final;
+  static constexpr auto kType = kEponymousOnly;
+  static constexpr bool kSupportsWrites = false;
+  static constexpr bool kDoesOverloadFunctions = false;
 
-  // Table implementation.
-  util::Status Init(int, const char* const*, SqliteTable::Schema*) final;
-  std::unique_ptr<SqliteTable::BaseCursor> CreateCursor() final;
-  int BestIndex(const QueryConstraints&, BestIndexInfo*) final;
+  static int Connect(sqlite3*,
+                     void*,
+                     int,
+                     const char* const*,
+                     sqlite3_vtab**,
+                     char**);
+  static int Disconnect(sqlite3_vtab*);
 
- private:
-  const TraceStorage* const storage_;
+  static int BestIndex(sqlite3_vtab*, sqlite3_index_info*);
+
+  static int Open(sqlite3_vtab*, sqlite3_vtab_cursor**);
+  static int Close(sqlite3_vtab_cursor*);
+
+  static int Filter(sqlite3_vtab_cursor*,
+                    int,
+                    const char*,
+                    int,
+                    sqlite3_value**);
+  static int Next(sqlite3_vtab_cursor*);
+  static int Eof(sqlite3_vtab_cursor*);
+  static int Column(sqlite3_vtab_cursor*, sqlite3_context*, int);
+  static int Rowid(sqlite3_vtab_cursor*, sqlite_int64*);
 };
-}  // namespace trace_processor
-}  // namespace perfetto
+
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_SQLITE_STATS_TABLE_H_
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index ae6f683..7085055 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -247,7 +247,7 @@
        "the file name is not found or no permission to access the file"),      \
   F(compact_sched_has_parse_errors,       kSingle,  kError,    kTrace,    ""), \
   F(misplaced_end_event,                  kSingle,  kDataLoss, kAnalysis, ""), \
-  F(truncated_sys_write_duration,         kSingle,  kDataLoss,  kAnalysis,     \
+  F(truncated_sys_write_duration,         kSingle,  kInfo,     kAnalysis,      \
       "Count of sys_write slices that have a truncated duration to resolve "   \
       "nesting incompatibilities with atrace slices. Real durations "          \
       "can be recovered via the |raw| table."),                                \
diff --git a/src/trace_processor/trace_processor.cc b/src/trace_processor/trace_processor.cc
index cd6ebda..4d022f7 100644
--- a/src/trace_processor/trace_processor.cc
+++ b/src/trace_processor/trace_processor.cc
@@ -36,7 +36,7 @@
 void EnableSQLiteVtableDebugging() {
   // This level of indirection is required to avoid clients to depend on table.h
   // which in turn requires sqlite headers.
-  SqliteTable::debug = true;
+  SqliteTableLegacy::debug = true;
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index d0d335f..b8b7850 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -17,20 +17,33 @@
 #include "src/trace_processor/trace_processor_impl.h"
 
 #include <algorithm>
+#include <chrono>
+#include <cinttypes>
 #include <cstddef>
 #include <cstdint>
+#include <limits>
 #include <memory>
+#include <optional>
 #include <string>
 #include <unordered_map>
 #include <utility>
+#include <vector>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/small_vector.h"
+#include "perfetto/ext/base/status_or.h"
 #include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/basic_types.h"
+#include "perfetto/trace_processor/iterator.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "perfetto/trace_processor/trace_processor.h"
+#include "sqlite/sqlite_utils.h"
 #include "src/trace_processor/importers/android_bugreport/android_bugreport_parser.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
@@ -72,6 +85,7 @@
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/connected_flow.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/dominator_tree.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h"
@@ -83,15 +97,21 @@
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.h"
 #include "src/trace_processor/perfetto_sql/prelude/tables_views.h"
 #include "src/trace_processor/perfetto_sql/stdlib/stdlib.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_aggregate_function.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
 #include "src/trace_processor/sqlite/scoped_db.h"
 #include "src/trace_processor/sqlite/sql_source.h"
 #include "src/trace_processor/sqlite/sql_stats_table.h"
 #include "src/trace_processor/sqlite/sqlite_table.h"
 #include "src/trace_processor/sqlite/stats_table.h"
+#include "src/trace_processor/storage/metadata.h"
+#include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/tp_metatrace.h"
+#include "src/trace_processor/trace_processor_storage_impl.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
 #include "src/trace_processor/util/descriptors.h"
+#include "src/trace_processor/util/gzip_utils.h"
 #include "src/trace_processor/util/protozero_to_json.h"
 #include "src/trace_processor/util/protozero_to_text.h"
 #include "src/trace_processor/util/regex.h"
@@ -103,6 +123,7 @@
 #include "protos/perfetto/trace/perfetto/perfetto_metatrace.pbzero.h"
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
+#include "protos/perfetto/trace_processor/metatrace_categories.pbzero.h"
 
 namespace perfetto::trace_processor {
 namespace {
@@ -130,8 +151,8 @@
     std::replace(fn_name.begin(), fn_name.end(), '.', '_');
     RegisterFunction<metrics::BuildProto>(
         engine, fn_name.c_str(), -1,
-        std::unique_ptr<metrics::BuildProto::Context>(
-            new metrics::BuildProto::Context{tp, pool, i}));
+        std::make_unique<metrics::BuildProto::Context>(
+            metrics::BuildProto::Context{tp, pool, i}));
   }
 }
 
@@ -154,90 +175,88 @@
   }
 }
 
-struct ValueAtMaxTsContext {
-  bool initialized;
-  int value_type;
+class ValueAtMaxTs : public SqliteAggregateFunction {
+ public:
+  struct Context {
+    bool initialized;
+    int value_type;
 
-  int64_t max_ts;
-  int64_t int_value_at_max_ts;
-  double double_value_at_max_ts;
-};
+    int64_t max_ts;
+    int64_t int_value_at_max_ts;
+    double double_value_at_max_ts;
+  };
 
-void ValueAtMaxTsStep(sqlite3_context* ctx, int, sqlite3_value** argv) {
-  sqlite3_value* ts = argv[0];
-  sqlite3_value* value = argv[1];
+  static void Step(sqlite3_context* ctx, int, sqlite3_value** argv) {
+    sqlite3_value* ts = argv[0];
+    sqlite3_value* value = argv[1];
 
-  // Note that sqlite3_aggregate_context zeros the memory for us so all the
-  // variables of the struct should be zero.
-  ValueAtMaxTsContext* fn_ctx = reinterpret_cast<ValueAtMaxTsContext*>(
-      sqlite3_aggregate_context(ctx, sizeof(ValueAtMaxTsContext)));
+    // Note that sqlite3_aggregate_context zeros the memory for us so all the
+    // variables of the struct should be zero.
+    auto* fn_ctx = reinterpret_cast<Context*>(
+        sqlite3_aggregate_context(ctx, sizeof(Context)));
 
-  // For performance reasons, we only do the check for the type of ts and value
-  // on the first call of the function.
-  if (PERFETTO_UNLIKELY(!fn_ctx->initialized)) {
-    if (sqlite3_value_type(ts) != SQLITE_INTEGER) {
-      sqlite3_result_error(ctx, "VALUE_AT_MAX_TS: ts passed was not an integer",
-                           -1);
-      return;
+    // For performance reasons, we only do the check for the type of ts and
+    // value on the first call of the function.
+    if (PERFETTO_UNLIKELY(!fn_ctx->initialized)) {
+      if (sqlite3_value_type(ts) != SQLITE_INTEGER) {
+        return sqlite::result::Error(
+            ctx, "VALUE_AT_MAX_TS: ts passed was not an integer");
+      }
+
+      fn_ctx->value_type = sqlite3_value_type(value);
+      if (fn_ctx->value_type != SQLITE_INTEGER &&
+          fn_ctx->value_type != SQLITE_FLOAT) {
+        return sqlite::result::Error(
+            ctx, "VALUE_AT_MAX_TS: value passed was not an integer or float");
+      }
+
+      fn_ctx->max_ts = std::numeric_limits<int64_t>::min();
+      fn_ctx->initialized = true;
     }
 
-    fn_ctx->value_type = sqlite3_value_type(value);
-    if (fn_ctx->value_type != SQLITE_INTEGER &&
-        fn_ctx->value_type != SQLITE_FLOAT) {
-      sqlite3_result_error(
-          ctx, "VALUE_AT_MAX_TS: value passed was not an integer or float", -1);
-      return;
-    }
-
-    fn_ctx->max_ts = std::numeric_limits<int64_t>::min();
-    fn_ctx->initialized = true;
-  }
-
-  // On dcheck builds however, we check every passed ts and value.
+    // On dcheck builds however, we check every passed ts and value.
 #if PERFETTO_DCHECK_IS_ON()
-  if (sqlite3_value_type(ts) != SQLITE_INTEGER) {
-    sqlite3_result_error(ctx, "VALUE_AT_MAX_TS: ts passed was not an integer",
-                         -1);
-    return;
-  }
-  if (sqlite3_value_type(value) != fn_ctx->value_type) {
-    sqlite3_result_error(ctx, "VALUE_AT_MAX_TS: value type is inconsistent",
-                         -1);
-    return;
-  }
+    if (sqlite3_value_type(ts) != SQLITE_INTEGER) {
+      return sqlite::result::Error(
+          ctx, "VALUE_AT_MAX_TS: ts passed was not an integer");
+    }
+    if (sqlite3_value_type(value) != fn_ctx->value_type) {
+      return sqlite::result::Error(
+          ctx, "VALUE_AT_MAX_TS: value type is inconsistent");
+    }
 #endif
 
-  int64_t ts_int = sqlite3_value_int64(ts);
-  if (PERFETTO_LIKELY(fn_ctx->max_ts <= ts_int)) {
-    fn_ctx->max_ts = ts_int;
+    int64_t ts_int = sqlite3_value_int64(ts);
+    if (PERFETTO_LIKELY(fn_ctx->max_ts <= ts_int)) {
+      fn_ctx->max_ts = ts_int;
 
-    if (fn_ctx->value_type == SQLITE_INTEGER) {
-      fn_ctx->int_value_at_max_ts = sqlite3_value_int64(value);
-    } else {
-      fn_ctx->double_value_at_max_ts = sqlite3_value_double(value);
+      if (fn_ctx->value_type == SQLITE_INTEGER) {
+        fn_ctx->int_value_at_max_ts = sqlite3_value_int64(value);
+      } else {
+        fn_ctx->double_value_at_max_ts = sqlite3_value_double(value);
+      }
     }
   }
-}
 
-void ValueAtMaxTsFinal(sqlite3_context* ctx) {
-  ValueAtMaxTsContext* fn_ctx =
-      reinterpret_cast<ValueAtMaxTsContext*>(sqlite3_aggregate_context(ctx, 0));
-  if (!fn_ctx) {
-    sqlite3_result_null(ctx);
-    return;
+  static void Final(sqlite3_context* ctx) {
+    Context* fn_ctx =
+        reinterpret_cast<Context*>(sqlite3_aggregate_context(ctx, 0));
+    if (!fn_ctx) {
+      sqlite::result::Null(ctx);
+      return;
+    }
+    if (fn_ctx->value_type == SQLITE_INTEGER) {
+      sqlite::result::Long(ctx, fn_ctx->int_value_at_max_ts);
+    } else {
+      sqlite::result::Double(ctx, fn_ctx->double_value_at_max_ts);
+    }
   }
-  if (fn_ctx->value_type == SQLITE_INTEGER) {
-    sqlite3_result_int64(ctx, fn_ctx->int_value_at_max_ts);
-  } else {
-    sqlite3_result_double(ctx, fn_ctx->double_value_at_max_ts);
-  }
-}
+};
 
-void RegisterValueAtMaxTsFunction(sqlite3* db) {
-  auto ret = sqlite3_create_function_v2(
-      db, "VALUE_AT_MAX_TS", 2, SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr,
-      nullptr, &ValueAtMaxTsStep, &ValueAtMaxTsFinal, nullptr);
-  if (ret) {
+void RegisterValueAtMaxTsFunction(PerfettoSqlEngine& engine) {
+  base::Status status = engine.RegisterSqliteAggregateFunction<ValueAtMaxTs>(
+      "VALUE_AT_MAX_TS", 2, nullptr);
+  if (!status.ok()) {
     PERFETTO_ELOG("Error initializing VALUE_AT_MAX_TS");
   }
 }
@@ -318,27 +337,33 @@
 
 TraceProcessorImpl::TraceProcessorImpl(const Config& cfg)
     : TraceProcessorStorageImpl(cfg), config_(cfg) {
-  context_.fuchsia_trace_tokenizer.reset(new FuchsiaTraceTokenizer(&context_));
-  context_.fuchsia_trace_parser.reset(new FuchsiaTraceParser(&context_));
-  context_.ninja_log_parser.reset(new NinjaLogParser(&context_));
-  context_.systrace_trace_parser.reset(new SystraceTraceParser(&context_));
-  context_.perf_data_trace_tokenizer.reset(
-      new perf_importer::PerfDataTokenizer(&context_));
-  context_.perf_data_parser.reset(new perf_importer::PerfDataParser(&context_));
+  context_.fuchsia_trace_tokenizer =
+      std::make_unique<FuchsiaTraceTokenizer>(&context_);
+  context_.fuchsia_trace_parser =
+      std::make_unique<FuchsiaTraceParser>(&context_);
+  context_.ninja_log_parser = std::make_unique<NinjaLogParser>(&context_);
+  context_.systrace_trace_parser =
+      std::make_unique<SystraceTraceParser>(&context_);
+  context_.perf_data_trace_tokenizer =
+      std::make_unique<perf_importer::PerfDataTokenizer>(&context_);
+  context_.perf_data_parser =
+      std::make_unique<perf_importer::PerfDataParser>(&context_);
 
   if (util::IsGzipSupported()) {
-    context_.gzip_trace_parser.reset(new GzipTraceParser(&context_));
-    context_.android_bugreport_parser.reset(
-        new AndroidBugreportParser(&context_));
+    context_.gzip_trace_parser = std::make_unique<GzipTraceParser>(&context_);
+    context_.android_bugreport_parser =
+        std::make_unique<AndroidBugreportParser>(&context_);
   }
 
   if (json::IsJsonSupported()) {
-    context_.json_trace_tokenizer.reset(new JsonTraceTokenizer(&context_));
-    context_.json_trace_parser.reset(new JsonTraceParser(&context_));
+    context_.json_trace_tokenizer =
+        std::make_unique<JsonTraceTokenizer>(&context_);
+    context_.json_trace_parser = std::make_unique<JsonTraceParser>(&context_);
   }
 
   if (context_.config.analyze_trace_proto_content) {
-    context_.content_analyzer.reset(new ProtoContentAnalyzer(&context_));
+    context_.content_analyzer =
+        std::make_unique<ProtoContentAnalyzer>(&context_);
   }
 
   // Add metrics to descriptor pool
@@ -480,7 +505,8 @@
       pool_.FindDescriptorIdx(".perfetto.protos.TraceMetrics");
   if (!desc_idx.has_value())
     return false;
-  auto field_idx = pool_.descriptors()[*desc_idx].FindFieldByName(metric_name);
+  const auto* field_idx =
+      pool_.descriptors()[*desc_idx].FindFieldByName(metric_name);
   return field_idx != nullptr;
 }
 
@@ -661,10 +687,10 @@
                                         1, engine_.get());
   RegisterFunction<Import>(
       engine_.get(), "IMPORT", 1,
-      std::unique_ptr<Import::Context>(new Import::Context{engine_.get()}));
+      std::make_unique<Import::Context>(Import::Context{engine_.get()}));
   RegisterFunction<ToFtrace>(
       engine_.get(), "TO_FTRACE", 1,
-      std::unique_ptr<ToFtrace::Context>(new ToFtrace::Context{
+      std::make_unique<ToFtrace::Context>(ToFtrace::Context{
           context_.storage.get(), SystraceSerializer(&context_)}));
 
   if constexpr (regex::IsRegexSupported()) {
@@ -673,48 +699,52 @@
   // Old style function registration.
   // TODO(lalitm): migrate this over to using RegisterFunction once aggregate
   // functions are supported.
-  RegisterLastNonNullFunction(db);
-  RegisterValueAtMaxTsFunction(db);
+  RegisterValueAtMaxTsFunction(*engine_);
+  {
+    base::Status status = RegisterLastNonNullFunction(*engine_);
+    if (!status.ok())
+      PERFETTO_ELOG("%s", status.c_message());
+  }
   {
     base::Status status = RegisterStackFunctions(engine_.get(), &context_);
     if (!status.ok())
       PERFETTO_ELOG("%s", status.c_message());
   }
   {
-    base::Status status = PprofFunctions::Register(db, &context_);
+    base::Status status = PprofFunctions::Register(*engine_, &context_);
     if (!status.ok())
       PERFETTO_ELOG("%s", status.c_message());
   }
   {
-    base::Status status = LayoutFunctions::Register(db, &context_);
+    base::Status status = RegisterLayoutFunctions(*engine_);
     if (!status.ok())
       PERFETTO_ELOG("%s", status.c_message());
   }
   {
-    base::Status status = RegisterMathFunctions(*engine_.get());
+    base::Status status = RegisterMathFunctions(*engine_);
     if (!status.ok())
       PERFETTO_ELOG("%s", status.c_message());
   }
   {
-    base::Status status = RegisterBase64Functions(*engine_.get());
+    base::Status status = RegisterBase64Functions(*engine_);
     if (!status.ok())
       PERFETTO_ELOG("%s", status.c_message());
   }
 
-  const TraceStorage* storage = context_.storage.get();
+  TraceStorage* storage = context_.storage.get();
 
   // Operator tables.
   engine_->sqlite_engine()->RegisterVirtualTableModule<SpanJoinOperatorTable>(
-      "span_join", engine_.get(), SqliteTable::TableType::kExplicitCreate,
+      "span_join", engine_.get(), SqliteTableLegacy::TableType::kExplicitCreate,
       false);
   engine_->sqlite_engine()->RegisterVirtualTableModule<SpanJoinOperatorTable>(
-      "span_left_join", engine_.get(), SqliteTable::TableType::kExplicitCreate,
-      false);
+      "span_left_join", engine_.get(),
+      SqliteTableLegacy::TableType::kExplicitCreate, false);
   engine_->sqlite_engine()->RegisterVirtualTableModule<SpanJoinOperatorTable>(
-      "span_outer_join", engine_.get(), SqliteTable::TableType::kExplicitCreate,
-      false);
-  engine_->sqlite_engine()->RegisterVirtualTableModule<WindowOperatorTable>(
-      "window", storage, SqliteTable::TableType::kExplicitCreate, true);
+      "span_outer_join", engine_.get(),
+      SqliteTableLegacy::TableType::kExplicitCreate, false);
+  engine_->sqlite_engine()->RegisterVirtualTableModule<WindowOperatorModule>(
+      "window", std::make_unique<WindowOperatorModule::Context>());
 
   // Initalize the tables and views in the prelude.
   InitializePreludeTablesViews(db);
@@ -729,14 +759,12 @@
   }
 
   // Register metrics functions.
-  // TODO(lalitm): migrate this over to using RegisterFunction once aggregate
-  // functions are supported.
   {
-    auto ret = sqlite3_create_function_v2(
-        db, "RepeatedField", 1, SQLITE_UTF8, nullptr, nullptr,
-        metrics::RepeatedFieldStep, metrics::RepeatedFieldFinal, nullptr);
-    if (ret)
-      PERFETTO_FATAL("Error initializing RepeatedField");
+    base::Status status =
+        engine_->RegisterSqliteAggregateFunction<metrics::RepeatedField>(
+            "RepeatedField", 1, nullptr);
+    if (!status.ok())
+      PERFETTO_ELOG("%s", status.c_message());
   }
 
   RegisterFunction<metrics::NullIfEmpty>(engine_.get(), "NULL_IF_EMPTY", 1);
@@ -744,14 +772,14 @@
                                                "UNWRAP_METRIC_PROTO", 2);
   RegisterFunction<metrics::RunMetric>(
       engine_.get(), "RUN_METRIC", -1,
-      std::unique_ptr<metrics::RunMetric::Context>(
-          new metrics::RunMetric::Context{engine_.get(), &sql_metrics_}));
+      std::make_unique<metrics::RunMetric::Context>(
+          metrics::RunMetric::Context{engine_.get(), &sql_metrics_}));
 
   // Legacy tables.
-  engine_->sqlite_engine()->RegisterVirtualTableModule<SqlStatsTable>(
-      "sqlstats", storage, SqliteTable::TableType::kEponymousOnly, false);
-  engine_->sqlite_engine()->RegisterVirtualTableModule<StatsTable>(
-      "stats", storage, SqliteTable::TableType::kEponymousOnly, false);
+  engine_->sqlite_engine()->RegisterVirtualTableModule<SqlStatsModule>(
+      "sqlstats", storage);
+  engine_->sqlite_engine()->RegisterVirtualTableModule<StatsModule>("stats",
+                                                                    storage);
 
   // New style db-backed tables.
   // Note: if adding a table here which might potentially contain many rows
@@ -855,48 +883,45 @@
   RegisterStaticTable(storage->experimental_missing_chrome_processes_table());
 
   // Tables dynamically generated at query time.
-  engine_->RegisterStaticTableFunction(std::unique_ptr<ExperimentalFlamegraph>(
-      new ExperimentalFlamegraph(&context_)));
-  engine_->RegisterStaticTableFunction(std::unique_ptr<ExperimentalCounterDur>(
-      new ExperimentalCounterDur(storage->counter_table())));
-  engine_->RegisterStaticTableFunction(std::unique_ptr<ExperimentalSliceLayout>(
-      new ExperimentalSliceLayout(context_.storage.get()->mutable_string_pool(),
-                                  &storage->slice_table())));
-  engine_->RegisterStaticTableFunction(std::unique_ptr<TableInfo>(new TableInfo(
-      context_.storage.get()->mutable_string_pool(), engine_.get())));
-  engine_->RegisterStaticTableFunction(std::unique_ptr<Ancestor>(
-      new Ancestor(Ancestor::Type::kSlice, context_.storage.get())));
-  engine_->RegisterStaticTableFunction(std::unique_ptr<Ancestor>(new Ancestor(
-      Ancestor::Type::kStackProfileCallsite, context_.storage.get())));
-  engine_->RegisterStaticTableFunction(std::unique_ptr<Ancestor>(
-      new Ancestor(Ancestor::Type::kSliceByStack, context_.storage.get())));
-  engine_->RegisterStaticTableFunction(std::unique_ptr<Descendant>(
-      new Descendant(Descendant::Type::kSlice, context_.storage.get())));
-  engine_->RegisterStaticTableFunction(std::unique_ptr<Descendant>(
-      new Descendant(Descendant::Type::kSliceByStack, context_.storage.get())));
-  engine_->RegisterStaticTableFunction(std::unique_ptr<ConnectedFlow>(
-      new ConnectedFlow(ConnectedFlow::Mode::kDirectlyConnectedFlow,
-                        context_.storage.get())));
   engine_->RegisterStaticTableFunction(
-      std::unique_ptr<ConnectedFlow>(new ConnectedFlow(
-          ConnectedFlow::Mode::kPrecedingFlow, context_.storage.get())));
+      std::make_unique<ExperimentalFlamegraph>(&context_));
   engine_->RegisterStaticTableFunction(
-      std::unique_ptr<ConnectedFlow>(new ConnectedFlow(
-          ConnectedFlow::Mode::kFollowingFlow, context_.storage.get())));
+      std::make_unique<ExperimentalCounterDur>(storage->counter_table()));
   engine_->RegisterStaticTableFunction(
-      std::unique_ptr<ExperimentalSchedUpid>(new ExperimentalSchedUpid(
-          storage->sched_slice_table(), storage->thread_table())));
+      std::make_unique<ExperimentalSliceLayout>(
+          context_.storage->mutable_string_pool(), &storage->slice_table()));
+  engine_->RegisterStaticTableFunction(std::make_unique<TableInfo>(
+      context_.storage->mutable_string_pool(), engine_.get()));
+  engine_->RegisterStaticTableFunction(std::make_unique<Ancestor>(
+      Ancestor::Type::kSlice, context_.storage.get()));
+  engine_->RegisterStaticTableFunction(std::make_unique<Ancestor>(
+      Ancestor::Type::kStackProfileCallsite, context_.storage.get()));
+  engine_->RegisterStaticTableFunction(std::make_unique<Ancestor>(
+      Ancestor::Type::kSliceByStack, context_.storage.get()));
+  engine_->RegisterStaticTableFunction(std::make_unique<Descendant>(
+      Descendant::Type::kSlice, context_.storage.get()));
+  engine_->RegisterStaticTableFunction(std::make_unique<Descendant>(
+      Descendant::Type::kSliceByStack, context_.storage.get()));
+  engine_->RegisterStaticTableFunction(std::make_unique<ConnectedFlow>(
+      ConnectedFlow::Mode::kDirectlyConnectedFlow, context_.storage.get()));
+  engine_->RegisterStaticTableFunction(std::make_unique<ConnectedFlow>(
+      ConnectedFlow::Mode::kPrecedingFlow, context_.storage.get()));
+  engine_->RegisterStaticTableFunction(std::make_unique<ConnectedFlow>(
+      ConnectedFlow::Mode::kFollowingFlow, context_.storage.get()));
+  engine_->RegisterStaticTableFunction(std::make_unique<ExperimentalSchedUpid>(
+      storage->sched_slice_table(), storage->thread_table()));
   engine_->RegisterStaticTableFunction(
-      std::unique_ptr<ExperimentalAnnotatedStack>(
-          new ExperimentalAnnotatedStack(&context_)));
-  engine_->RegisterStaticTableFunction(std::unique_ptr<ExperimentalFlatSlice>(
-      new ExperimentalFlatSlice(&context_)));
+      std::make_unique<ExperimentalAnnotatedStack>(&context_));
+  engine_->RegisterStaticTableFunction(
+      std::make_unique<ExperimentalFlatSlice>(&context_));
   engine_->RegisterStaticTableFunction(
       std::make_unique<DominatorTree>(context_.storage->mutable_string_pool()));
   engine_->RegisterStaticTableFunction(std::make_unique<IntervalIntersect>(
       context_.storage->mutable_string_pool()));
   engine_->RegisterStaticTableFunction(
       std::make_unique<Dfs>(context_.storage->mutable_string_pool()));
+  engine_->RegisterStaticTableFunction(std::make_unique<DfsWeightBounded>(
+      context_.storage->mutable_string_pool()));
 
   // Metrics.
   RegisterAllProtoBuilderFunctions(&pool_, engine_.get(), this);
@@ -982,7 +1007,7 @@
   base::FlatHashMap<std::string, uint64_t> interned_strings;
   metatrace::DisableAndReadBuffer([&trace, &interned_strings](
                                       metatrace::Record* record) {
-    auto packet = trace->add_packet();
+    auto* packet = trace->add_packet();
     packet->set_timestamp(record->timestamp_ns);
     auto* evt = packet->set_perfetto_metatrace();
 
diff --git a/src/trace_processor/trace_processor_impl.h b/src/trace_processor/trace_processor_impl.h
index 4c08f23..249ceb9 100644
--- a/src/trace_processor/trace_processor_impl.h
+++ b/src/trace_processor/trace_processor_impl.h
@@ -29,7 +29,9 @@
 
 #include "perfetto/base/status.h"
 #include "perfetto/trace_processor/basic_types.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
 #include "perfetto/trace_processor/trace_processor.h"
+#include "src/trace_processor/iterator_impl.h"
 #include "src/trace_processor/metrics/metrics.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/create_function.h"
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index 9a89eb3..164df00 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -113,6 +113,7 @@
   // type is only available in storage_full target. To access these fields use
   // the GetOrCreate() method on their subclass type, e.g.
   // SyscallTracker::GetOrCreate(context)
+  // clang-format off
   std::unique_ptr<Destructible> android_probes_tracker;  // AndroidProbesTracker
   std::unique_ptr<Destructible> binder_tracker;          // BinderTracker
   std::unique_ptr<Destructible> heap_graph_tracker;      // HeapGraphTracker
@@ -125,12 +126,11 @@
   std::unique_ptr<Destructible> i2c_tracker;             // I2CTracker
   std::unique_ptr<Destructible> perf_data_tracker;       // PerfDataTracker
   std::unique_ptr<Destructible> content_analyzer;        // ProtoContentAnalyzer
-  std::unique_ptr<Destructible>
-      shell_transitions_tracker;  // ShellTransitionsTracker
-  std::unique_ptr<Destructible>
-      ftrace_sched_tracker;  // FtraceSchedEventTracker
-  std::unique_ptr<Destructible> v8_tracker;   // V8Tracker
-  std::unique_ptr<Destructible> jit_tracker;  // JitTracker
+  std::unique_ptr<Destructible> shell_transitions_tracker; // ShellTransitionsTracker
+  std::unique_ptr<Destructible> ftrace_sched_tracker;    // FtraceSchedEventTracker
+  std::unique_ptr<Destructible> v8_tracker;              // V8Tracker
+  std::unique_ptr<Destructible> jit_tracker;             // JitTracker
+  // clang-format on
 
   // These fields are trace readers which will be called by |forwarding_parser|
   // once the format of the trace is discovered. They are placed here as they
diff --git a/src/trace_redaction/BUILD.gn b/src/trace_redaction/BUILD.gn
index b662902..c73d36f 100644
--- a/src/trace_redaction/BUILD.gn
+++ b/src/trace_redaction/BUILD.gn
@@ -28,16 +28,24 @@
 
 source_set("trace_redaction") {
   sources = [
+    "build_timeline.cc",
+    "build_timeline.h",
     "find_package_uid.cc",
     "find_package_uid.h",
+    "optimize_timeline.cc",
+    "optimize_timeline.h",
     "populate_allow_lists.cc",
     "populate_allow_lists.h",
+    "process_thread_timeline.cc",
+    "process_thread_timeline.h",
     "proto_util.cc",
     "proto_util.h",
     "prune_package_list.cc",
     "prune_package_list.h",
     "scrub_ftrace_events.cc",
     "scrub_ftrace_events.h",
+    "scrub_process_trees.cc",
+    "scrub_process_trees.h",
     "scrub_trace_packet.cc",
     "scrub_trace_packet.h",
     "trace_redaction_framework.cc",
@@ -56,6 +64,7 @@
     "../../protos/perfetto/trace/android:cpp",
     "../../protos/perfetto/trace/android:zero",
     "../../protos/perfetto/trace/ftrace:zero",
+    "../../protos/perfetto/trace/ps:zero",
     "../trace_processor:storage_minimal",
   ]
 }
@@ -64,6 +73,7 @@
   testonly = true
   sources = [
     "scrub_ftrace_events_integrationtest.cc",
+    "scrub_process_trees_integrationtest.cc",
     "trace_redactor_integrationtest.cc",
   ]
   deps = [
@@ -74,6 +84,7 @@
     "../../protos/perfetto/trace:non_minimal_zero",
     "../../protos/perfetto/trace/android:zero",
     "../../protos/perfetto/trace/ftrace:zero",
+    "../../protos/perfetto/trace/ps:zero",
     "../base:test_support",
   ]
 }
@@ -81,7 +92,9 @@
 perfetto_unittest_source_set("unittests") {
   testonly = true
   sources = [
+    "build_timeline_unittest.cc",
     "find_package_uid_unittest.cc",
+    "process_thread_timeline_unittest.cc",
     "proto_util_unittest.cc",
     "prune_package_list_unittest.cc",
     "scrub_ftrace_events_unittest.cc",
diff --git a/src/trace_redaction/README.md b/src/trace_redaction/README.md
new file mode 100644
index 0000000..225f261
--- /dev/null
+++ b/src/trace_redaction/README.md
@@ -0,0 +1,134 @@
+
+# Timeline
+
+## Intro
+
+The timeline is at the center of the redaction system. It provides an
+efficient method to find which package a thread/process belongs to.
+
+The timeline allows queries to be connected to time. Without this, there's a
+significant privacy conern because a pid can be recycled. Just because the pid
+is excluded from redaction before time T, doesn't mean it should be redacted
+after time T.
+
+## General Structure
+
+The timeline uses an event-based pattern using two events:
+
+- __Open Event:__ Marks the begining of a pid's new lifespan.
+- __Close Event:__ Marks the end of a pids's lifespan.
+
+An event-based structure (compared to a span-based structure) is used as it is
+better suited to handle errors/issues in the underlying data. For example, if a
+pid doesn't explictly ends before being reused (e.g. two back-to-back open
+events), the event-based structure "just works".
+
+Open events contain the thread's full state. The close event only contains the
+information needed to reference the thread's previous event.
+
+```c++
+struct Open {
+    uint64_t ts;
+    int32_t  pid;
+    int32_t  ppid;
+    uint64_t uid;
+};
+
+struct Close {
+    uint64_t ts;
+    int32_t  pid;
+};
+```
+
+The vast majory of threads will have one event, an open event provided by the
+`ProcessTree`. For some threads, they will have multiple open (`ProcessTree`,
+`NewTask`) and close events (`ProcFree`) in alternating order.
+
+## Query
+
+```c++
+struct Slice {
+    int32_t  pid;
+    uint64_t uid;
+};
+
+class Timeline {
+  Slice Query(uint64_t ts, int32_t pid) const;
+};
+
+```
+
+Events, regardless of type, are stored in contiguous memory and are ordered
+first by pid and second by time. This is done to allow events to be found
+via a binary search.
+
+The vast majory of threads will have one event, the open event. Some threads
+may have close and re-open events.
+
+To handle a query,
+
+1. Use a binary search to find the lower bound for `pid` (the first instance of
+ `pid`)
+1. Scan forward to find the last event before `ts` (for `pid`)
+
+If an event was found:
+
+```c++
+if (e.type == kOpen && uid != 0)
+  return Slice(pid, e.uid);
+
+// The pid is active, check the parent for a uid.
+if (e.type == kOpen && uid == 0)
+  return Query(ts, e.ppid);
+
+return Slice(pid, kNoPackage);
+```
+
+If `pid` does not have an immediate package (`uid`), the parent must be
+searched. The parent-child tree is short, so the recursive search will be
+relatively short. To minimize this even more, a union-find operation is applied
+because any queries can be made.
+
+__Simple runtime overview:__
+
+Initialization:
+
+- $sort + union\ find$
+
+- $nlogn + mlogn$
+  - where $n=events$
+  - and $m=approx\ average\ depth$
+
+Query:
+
+- $P(p) = m_p * (logn + e_p)$
+  - where $m_p=\ distance\ from\ pid\ to\ uid$
+  - and $n=events$
+  - and $e_p=number\ of\ events\ for\ process\ p$
+
+- Because of the union-find in initialization, $m_p \to 0$
+
+To further reduce the runtime, the search domain is reduces by remove all open
+events for $pids$ that don't connect to a target $uid$. By removing open events,
+and close events, there are two advantages:
+
+1. Removing open events are safe and simple. By removing open events, those pids
+can never be marked by active. Keeping the close events effectively reminds the
+system that the pid is not active.
+
+1. The number of open events exceeds the number of close events. Removing open
+events will have a greater effect on the number of events.
+
+__Example:__
+
+|Name|Value|Notes|
+|-|-|-|
+|tids|3666|Total number of threads.|
+|freed threads|5|Number of threads that were freed.|
+|reused threads|0|No threads were used more than one time.|
+|process tids|64|Total number of threads connected to the target process.|
+
+After initialization, there would only be 64 open events and 5 close events.
+This means that every uid lookup would be $logn\ |\ n=64 = 6$. Finding the uid
+given a pid is one of the most common operations during redaction because uid
+determines if something needs to be redacted.
diff --git a/src/trace_redaction/build_timeline.cc b/src/trace_redaction/build_timeline.cc
new file mode 100644
index 0000000..e94ddaa
--- /dev/null
+++ b/src/trace_redaction/build_timeline.cc
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_redaction/build_timeline.h"
+
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_redaction/process_thread_timeline.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
+#include "protos/perfetto/trace/ftrace/task.pbzero.h"
+#include "protos/perfetto/trace/ps/process_tree.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+namespace {
+
+using TracePacket = protos::pbzero::TracePacket;
+using ProcessTree = protos::pbzero::ProcessTree;
+using FtraceEvent = protos::pbzero::FtraceEvent;
+using FtraceEventBundle = protos::pbzero::FtraceEventBundle;
+using SchedProcessFreeFtraceEvent = protos::pbzero::SchedProcessFreeFtraceEvent;
+using TaskNewtaskFtraceEvent = protos::pbzero::TaskNewtaskFtraceEvent;
+
+void MarkOpen(uint64_t ts,
+              ProcessTree::Process::Decoder process,
+              ProcessThreadTimeline* timeline) {
+  // The uid in the process tree is a int32_t, but in the package list, the uid
+  // is a uint64_t.
+  auto uid = static_cast<uint64_t>(process.uid());
+  auto e = ProcessThreadTimeline::Event::Open(ts, process.pid(), process.ppid(),
+                                              uid);
+  timeline->Append(e);
+}
+
+void MarkOpen(uint64_t ts,
+              ProcessTree::Thread::Decoder thread,
+              ProcessThreadTimeline* timeline) {
+  auto e = ProcessThreadTimeline::Event::Open(ts, thread.tid(), thread.tgid());
+  timeline->Append(e);
+}
+
+void MarkClose(const FtraceEvent::Decoder& event,
+               SchedProcessFreeFtraceEvent::Decoder process_free,
+               ProcessThreadTimeline* timeline) {
+  auto e = ProcessThreadTimeline::Event::Close(event.timestamp(),
+                                               process_free.pid());
+  timeline->Append(e);
+}
+
+void MarkOpen(const FtraceEvent::Decoder& event,
+              TaskNewtaskFtraceEvent::Decoder new_task,
+              ProcessThreadTimeline* timeline) {
+  // Event though pid() is uint32_t. all other pid values use int32_t, so it's
+  // assumed to be safe to narrow-cast it.
+  auto ppid = static_cast<int32_t>(event.pid());
+  auto e = ProcessThreadTimeline::Event::Open(event.timestamp(), new_task.pid(),
+                                              ppid);
+  timeline->Append(e);
+}
+
+void AppendEvents(uint64_t ts,
+                  ProcessTree::Decoder tree,
+                  ProcessThreadTimeline* timeline) {
+  for (auto it = tree.processes(); it; ++it) {
+    MarkOpen(ts, ProcessTree::Process::Decoder(*it), timeline);
+  }
+
+  for (auto it = tree.threads(); it; ++it) {
+    MarkOpen(ts, ProcessTree::Thread::Decoder(*it), timeline);
+  }
+}
+
+void AppendEvents(FtraceEventBundle::Decoder ftrace_events,
+                  ProcessThreadTimeline* timeline) {
+  for (auto it = ftrace_events.event(); it; ++it) {
+    FtraceEvent::Decoder event(*it);
+
+    if (event.has_task_newtask()) {
+      MarkOpen(event, TaskNewtaskFtraceEvent::Decoder(event.task_newtask()),
+               timeline);
+      continue;
+    }
+
+    if (event.has_sched_process_free()) {
+      MarkClose(
+          event,
+          SchedProcessFreeFtraceEvent::Decoder(event.sched_process_free()),
+          timeline);
+      continue;
+    }
+  }
+}
+
+}  // namespace
+
+base::StatusOr<CollectPrimitive::ContinueCollection> BuildTimeline::Collect(
+    const TracePacket::Decoder& packet,
+    Context* context) const {
+  // TODO(vaage): This should only be true on the first call. However, that
+  // means a branch is called N times when N-1 times it will be false. This may
+  // be common across Collect primitives. Having a "begin" and "end" end-points.
+  if (!context->timeline) {
+    context->timeline = std::make_unique<ProcessThreadTimeline>();
+  }
+
+  // Unlike ftrace events, process trees do not provide per-process or
+  // per-thread timing information. The packet has timestamp and the process
+  // tree has collection_end_timestamp (collection_end_timestamp > timestamp).
+  //
+  // The packet's timestamp based on the assumption that in order to be
+  // collected, the processes and threads had to exist before "now".
+  if (packet.has_process_tree()) {
+    AppendEvents(packet.timestamp(),
+                 ProcessTree::Decoder(packet.process_tree()),
+                 context->timeline.get());
+    return ContinueCollection::kNextPacket;
+  }
+
+  if (packet.has_ftrace_events()) {
+    AppendEvents(FtraceEventBundle::Decoder(packet.ftrace_events()),
+                 context->timeline.get());
+    return ContinueCollection::kNextPacket;
+  }
+
+  return ContinueCollection::kNextPacket;
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/build_timeline.h b/src/trace_redaction/build_timeline.h
new file mode 100644
index 0000000..49b92bf
--- /dev/null
+++ b/src/trace_redaction/build_timeline.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_REDACTION_BUILD_TIMELINE_H_
+#define SRC_TRACE_REDACTION_BUILD_TIMELINE_H_
+
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+// Creates events from process_tree, task_newtask, and sched_process_free
+// packets and stores them in a timeline.
+class BuildTimeline : public CollectPrimitive {
+ public:
+  base::StatusOr<ContinueCollection> Collect(
+      const protos::pbzero::TracePacket::Decoder& packet,
+      Context* context) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_BUILD_TIMELINE_H_
diff --git a/src/trace_redaction/build_timeline_unittest.cc b/src/trace_redaction/build_timeline_unittest.cc
new file mode 100644
index 0000000..39eb877
--- /dev/null
+++ b/src/trace_redaction/build_timeline_unittest.cc
@@ -0,0 +1,225 @@
+
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <string>
+
+#include "src/base/test/status_matchers.h"
+#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.gen.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h"
+#include "protos/perfetto/trace/ftrace/sched.gen.h"
+#include "protos/perfetto/trace/ps/process_tree.gen.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+
+namespace perfetto::trace_redaction {
+
+// Test packet (a small clip of a later trace):
+//
+// packet {
+//  process_tree{
+//    processes {
+//      pid: 1093
+//      ppid: 1
+//      cmdline: "zygote"
+//      uid: 0
+//    }
+//    processes {
+//      pid: 7105
+//      ppid: 1093
+//      cmdline: "com.Unity.com.unity.multiplayer.samples.coop"
+//      uid: 10252
+//    }
+//    threads {
+//      tid: 7127
+//      tgid: 7105
+//    }
+//    collection_end_timestamp: 6702093738547594
+//  }
+//  trusted_uid: 9999
+//  timestamp: 6702093635419927
+//  trusted_packet_sequence_id: 6
+//  incremental_state_cleared: true
+//  previous_packet_dropped: true
+// }
+
+namespace {
+
+constexpr uint64_t kNoPackage = 0;
+constexpr uint64_t kUnityPackage = 10252;
+
+constexpr uint64_t kZygotePid = 1093;
+constexpr uint64_t kUnityPid = 7105;
+constexpr uint64_t kUnityTid = 7127;
+
+constexpr uint64_t kProcessTreeTimestamp = 6702093635419927;
+constexpr uint64_t kThreadFreeTimestamp = 6702094703928940;
+
+class TestParams {
+ public:
+  TestParams(uint64_t ts, int32_t pid, uint64_t uid)
+      : ts_(ts), pid_(pid), uid_(uid) {}
+
+  uint64_t ts() const { return ts_; }
+  int32_t pid() const { return pid_; }
+  uint64_t uid() const { return uid_; }
+
+ private:
+  uint64_t ts_;
+  int32_t pid_;
+  uint64_t uid_;
+};
+
+}  // namespace
+
+class BuildTimelineTest : public testing::Test,
+                          public testing::WithParamInterface<TestParams> {
+ protected:
+  base::StatusOr<CollectPrimitive::ContinueCollection> PushProcessTreePacket(
+      uint64_t timestamp) {
+    protos::gen::TracePacket packet;
+    packet.set_trusted_uid(9999);
+    packet.set_timestamp(timestamp);
+    packet.set_trusted_packet_sequence_id(6);
+    packet.set_incremental_state_cleared(true);
+    packet.set_previous_packet_dropped(true);
+
+    auto* process_tree = packet.mutable_process_tree();
+
+    auto* zygote = process_tree->add_processes();
+    zygote->set_pid(kZygotePid);
+    zygote->set_ppid(1);
+    zygote->add_cmdline("zygote");
+    zygote->set_uid(0);
+
+    auto* unity = process_tree->add_processes();
+    unity->set_pid(kUnityPid);
+    unity->set_ppid(1093);
+    unity->add_cmdline("com.Unity.com.unity.multiplayer.samples.coop");
+    unity->set_uid(kUnityPackage);
+
+    auto* thread = process_tree->add_threads();
+    thread->set_tid(kUnityTid);
+    thread->set_tgid(kUnityPid);
+
+    process_tree->set_collection_end_timestamp(timestamp);
+
+    std::string packet_str = packet.SerializeAsString();
+    return build_.Collect(protos::pbzero::TracePacket::Decoder(packet_str),
+                          &context_);
+  }
+
+  base::StatusOr<CollectPrimitive::ContinueCollection>
+  PushSchedProcessFreePacket(uint64_t timestamp) {
+    protos::gen::TracePacket packet;
+
+    packet.set_trusted_uid(9999);
+    packet.set_timestamp(timestamp);
+    packet.set_trusted_packet_sequence_id(6);
+    packet.set_incremental_state_cleared(true);
+    packet.set_previous_packet_dropped(true);
+
+    auto* ftrace_events = packet.mutable_ftrace_events();
+    auto* ftrace_event = ftrace_events->add_event();
+    ftrace_event->set_timestamp(timestamp);
+    ftrace_event->set_pid(10);  // kernel thread - e.g. "rcuop/0"
+
+    auto* process_free = ftrace_event->mutable_sched_process_free();
+    process_free->set_comm("UnityMain");
+    process_free->set_pid(kUnityTid);
+    process_free->set_prio(120);
+
+    std::string packet_str = packet.SerializeAsString();
+    return build_.Collect(protos::pbzero::TracePacket::Decoder(packet_str),
+                          &context_);
+  }
+
+  BuildTimeline build_;
+  Context context_;
+};
+
+class BuildTimelineWithProcessTree : public BuildTimelineTest {};
+
+TEST_P(BuildTimelineWithProcessTree, FindsOpenSpans) {
+  auto params = GetParam();
+
+  auto result = PushProcessTreePacket(kProcessTreeTimestamp);
+  ASSERT_OK(result) << result.status().message();
+
+  context_.timeline->Sort();
+
+  auto slice = context_.timeline->Search(params.ts(), params.pid());
+  ASSERT_EQ(slice.pid, params.pid());
+  ASSERT_EQ(slice.uid, params.uid());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    AcrossWholeTimeline,
+    BuildTimelineWithProcessTree,
+    testing::Values(
+        // Before the processes/threads existed.
+        TestParams(0, kZygotePid, kNoPackage),
+        TestParams(0, kUnityPid, kNoPackage),
+        TestParams(0, kUnityTid, kNoPackage),
+
+        // When the process tree started.
+        TestParams(kProcessTreeTimestamp, kZygotePid, kNoPackage),
+        TestParams(kProcessTreeTimestamp, kUnityPid, kUnityPackage),
+        TestParams(kProcessTreeTimestamp, kUnityTid, kUnityPackage),
+
+        // After the process tree started.
+        TestParams(kProcessTreeTimestamp + 1, kZygotePid, kNoPackage),
+        TestParams(kProcessTreeTimestamp + 1, kUnityPid, kUnityPackage),
+        TestParams(kProcessTreeTimestamp + 1, kUnityTid, kUnityPackage)));
+
+// Assumes all BuildTimelineWithProcessTree tests pass.
+class BuildTimelineWithFreeProcess : public BuildTimelineTest {};
+
+TEST_P(BuildTimelineWithFreeProcess, FindsClosedSpans) {
+  auto params = GetParam();
+
+  auto result = PushProcessTreePacket(kProcessTreeTimestamp);
+  ASSERT_OK(result) << result.status().message();
+
+  result = PushSchedProcessFreePacket(kThreadFreeTimestamp);
+  ASSERT_OK(result) << result.status().message();
+
+  context_.timeline->Sort();
+
+  auto slice = context_.timeline->Search(params.ts(), params.pid());
+  ASSERT_EQ(slice.pid, params.pid());
+  ASSERT_EQ(slice.uid, params.uid());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    AcrossWholeTimeline,
+    BuildTimelineWithFreeProcess,
+    testing::Values(
+        TestParams(kThreadFreeTimestamp - 1, kZygotePid, kNoPackage),
+        TestParams(kThreadFreeTimestamp - 1, kUnityPid, kUnityPackage),
+        TestParams(kThreadFreeTimestamp - 1, kUnityTid, kUnityPackage),
+
+        TestParams(kThreadFreeTimestamp, kZygotePid, kNoPackage),
+        TestParams(kThreadFreeTimestamp, kUnityPid, kUnityPackage),
+        TestParams(kThreadFreeTimestamp, kUnityTid, kNoPackage),
+
+        TestParams(kThreadFreeTimestamp + 1, kZygotePid, kNoPackage),
+        TestParams(kThreadFreeTimestamp + 1, kUnityPid, kUnityPackage),
+        TestParams(kThreadFreeTimestamp + 1, kUnityTid, kNoPackage)));
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/main.cc b/src/trace_redaction/main.cc
index 153e437..a5c3564 100644
--- a/src/trace_redaction/main.cc
+++ b/src/trace_redaction/main.cc
@@ -16,9 +16,13 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
+#include "src/trace_redaction/build_timeline.h"
 #include "src/trace_redaction/find_package_uid.h"
+#include "src/trace_redaction/optimize_timeline.h"
+#include "src/trace_redaction/populate_allow_lists.h"
 #include "src/trace_redaction/prune_package_list.h"
 #include "src/trace_redaction/scrub_ftrace_events.h"
+#include "src/trace_redaction/scrub_process_trees.h"
 #include "src/trace_redaction/scrub_trace_packet.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 #include "src/trace_redaction/trace_redactor.h"
@@ -33,13 +37,17 @@
 
   // Add all collectors.
   redactor.collectors()->emplace_back(new FindPackageUid());
+  redactor.collectors()->emplace_back(new BuildTimeline());
 
-  // TODO(vaage): Add all builders.
+  // Add all builders.
+  redactor.builders()->emplace_back(new PopulateAllowlists());
+  redactor.builders()->emplace_back(new OptimizeTimeline());
 
   // Add all transforms.
   redactor.transformers()->emplace_back(new PrunePackageList());
   redactor.transformers()->emplace_back(new ScrubTracePacket());
   redactor.transformers()->emplace_back(new ScrubFtraceEvents());
+  redactor.transformers()->emplace_back(new ScrubProcessTrees());
 
   Context context;
   context.package_name = package_name;
diff --git a/src/trace_redaction/optimize_timeline.cc b/src/trace_redaction/optimize_timeline.cc
new file mode 100644
index 0000000..f4f09e6
--- /dev/null
+++ b/src/trace_redaction/optimize_timeline.cc
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_redaction/optimize_timeline.h"
+
+#include "perfetto/base/status.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+base::Status OptimizeTimeline::Build(Context* context) const {
+  if (!context->timeline) {
+    return base::ErrStatus(
+        "Cannot optimize a null timeline. Are you missing BuildTimeline or an "
+        "alternative?");
+  }
+
+  if (!context->package_uid.has_value()) {
+    return base::ErrStatus(
+        "Missing package uid. Are you missing FindPackageUid or an "
+        "alternative?");
+  }
+
+  auto* timeline = context->timeline.get();
+
+  // Change the timeline from read-only to write only mode.
+  timeline->Sort();
+
+  // Goes over the whole timeline, reducing the distance between a pid and its
+  // uid.
+  timeline->Flatten();
+
+  // Reduce the number of events. This makes the timeline specific to the
+  // package uid (i.e. either 0 or package_uid will be returned).
+  timeline->Reduce(*context->package_uid);
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/optimize_timeline.h b/src/trace_redaction/optimize_timeline.h
new file mode 100644
index 0000000..e142851
--- /dev/null
+++ b/src/trace_redaction/optimize_timeline.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_REDACTION_OPTIMIZE_TIMELINE_H_
+#define SRC_TRACE_REDACTION_OPTIMIZE_TIMELINE_H_
+
+#include "perfetto/base/status.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+// Converts a timeline from a write-only structure to a read-only structure.
+class OptimizeTimeline : public BuildPrimitive {
+ public:
+  base::Status Build(Context* context) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_OPTIMIZE_TIMELINE_H_
diff --git a/src/trace_redaction/process_thread_timeline.cc b/src/trace_redaction/process_thread_timeline.cc
new file mode 100644
index 0000000..7bd4ea4
--- /dev/null
+++ b/src/trace_redaction/process_thread_timeline.cc
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_redaction/process_thread_timeline.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+
+namespace perfetto::trace_redaction {
+namespace {
+// Limit the number of iterations to avoid an infinite loop. 10 is a generous
+// number of iterations.
+constexpr size_t kMaxSearchDepth = 10;
+
+bool OrderByPid(const ProcessThreadTimeline::Event& left,
+                const ProcessThreadTimeline::Event& right) {
+  return left.pid() < right.pid();
+}
+
+}  // namespace
+
+void ProcessThreadTimeline::Append(const Event& event) {
+  write_only_events_.push_back(event);
+}
+
+void ProcessThreadTimeline::Sort() {
+  write_only_events_.sort(OrderByPid);
+
+  // Copy all events that don't match adjacent events. This should reduce the
+  // number of events because process trees may contain the same data
+  // back-to-back.
+  read_only_events_.reserve(write_only_events_.size());
+
+  for (auto event : write_only_events_) {
+    if (read_only_events_.empty() || event != read_only_events_.back()) {
+      read_only_events_.push_back(event);
+    }
+  }
+
+  // Events have been moved from the write-only list to the read-only vector.
+  // The resources backing the write-only list can be release.
+  write_only_events_.clear();
+}
+
+void ProcessThreadTimeline::Flatten() {
+  // Union-find-like action to collapse the tree.
+  for (auto& event : read_only_events_) {
+    if (event.type() != Event::Type::kOpen) {
+      continue;
+    }
+
+    auto event_with_package = Search(0, event.ts(), event.pid());
+
+    if (event_with_package.has_value()) {
+      event = Event::Open(event.ts(), event.pid(), event.ppid(),
+                          event_with_package->uid());
+    }
+  }
+}
+
+void ProcessThreadTimeline::Reduce(uint64_t package_uid) {
+  auto remove_open_events = [package_uid](const Event& event) {
+    return event.uid() != package_uid && event.type() == Event::Type::kOpen;
+  };
+
+  read_only_events_.erase(
+      std::remove_if(read_only_events_.begin(), read_only_events_.end(),
+                     remove_open_events),
+      read_only_events_.end());
+}
+
+ProcessThreadTimeline::Slice ProcessThreadTimeline::Search(uint64_t ts,
+                                                           int32_t pid) const {
+  Slice s;
+  s.pid = pid;
+  s.uid = 0;
+
+  auto e = Search(0, ts, pid);
+  if (e.has_value()) {
+    s.uid = e->uid();
+  }
+
+  return s;
+}
+
+std::optional<ProcessThreadTimeline::Event>
+ProcessThreadTimeline::Search(size_t depth, uint64_t ts, int32_t pid) const {
+  if (depth >= kMaxSearchDepth) {
+    return std::nullopt;
+  }
+
+  auto event = FindPreviousEvent(ts, pid);
+
+  if (!TestEvent(event)) {
+    return event;
+  }
+
+  if (event->uid() != 0) {
+    return event;
+  }
+
+  return Search(depth + 1, ts, event->ppid());
+}
+
+std::optional<size_t> ProcessThreadTimeline::GetDepth(uint64_t ts,
+                                                      int32_t pid) const {
+  return GetDepth(0, ts, pid);
+}
+
+std::optional<size_t> ProcessThreadTimeline::GetDepth(size_t depth,
+                                                      uint64_t ts,
+                                                      int32_t pid) const {
+  if (depth >= kMaxSearchDepth) {
+    return std::nullopt;
+  }
+
+  auto event = FindPreviousEvent(ts, pid);
+
+  if (!TestEvent(event)) {
+    return std::nullopt;
+  }
+
+  if (event->uid() != 0) {
+    return depth;
+  }
+
+  return GetDepth(depth + 1, ts, event->ppid());
+}
+
+std::optional<ProcessThreadTimeline::Event>
+ProcessThreadTimeline::FindPreviousEvent(uint64_t ts, int32_t pid) const {
+  Event fake = Event::Close(ts, pid);
+
+  // Events are in ts-order within each pid-group. See Optimize(), Because each
+  // group is small (the vast majority will have two events [start + event, no
+  // reuse]).
+  //
+  // Find the first process event. Then perform a linear search. There won't be
+  // many events per process.
+  auto at = std::lower_bound(read_only_events_.begin(), read_only_events_.end(),
+                             fake, OrderByPid);
+
+  // `pid` was not found in `read_only_events_`.
+  if (at == read_only_events_.end()) {
+    return std::nullopt;
+  }
+
+  // "no best option".
+  std::optional<Event> best;
+
+  // Run through all events (related to this pid) and find the last event that
+  // comes before ts. If the events were in order by time, the search could be
+  // more efficient, but the gains are margin because:
+  //
+  // 1. The number of edge cases go up.
+  //
+  // 2. The code is harder to read.
+  //
+  // 3. The performance gains are minimal or non-existant because of the small
+  //    number of events.
+  for (; at != read_only_events_.end() && at->pid() == pid; ++at) {
+    if (at->ts() > ts) {
+      continue;  // Ignore events in the future.
+    }
+
+    // All ts values are positive. However, ts_at and ts_best are both less than
+    // ts (see early condition), meaning they can be considered negative values.
+    //
+    //      at        best            ts
+    //   <---+-----------+-------------+---->
+    //      31          64            93
+    //
+    //      at        best            ts
+    //   <---+-----------+-------------+---->
+    //     -62         -29             0
+    //
+    // This means that the latest ts value under ts is the closest to ts.
+    if (!best.has_value() || at->ts() > best->ts()) {
+      best = *at;
+    }
+  }
+
+  if (best.has_value() &&
+      best->type() != ProcessThreadTimeline::Event::Type::kOpen) {
+    return std::nullopt;
+  }
+
+  return best;
+}
+
+bool ProcessThreadTimeline::TestEvent(std::optional<Event> event) const {
+  if (!event.has_value()) {
+    return false;
+  }
+
+  // The thread/process was freed. It won't exist until a new open event.
+  if (event->type() != Event::Type::kOpen) {
+    return false;
+  }
+
+  // It is a rare case in production, but a common case in tests, the top-level
+  // event will have no parent but will have the uid. So, to avoid make the
+  // tests fragile and without taking on any risk, the uid should be checked
+  // before the ppid.
+  if (event->uid() != 0) {
+    return true;
+  }
+
+  return event->ppid() != 0;
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/process_thread_timeline.h b/src/trace_redaction/process_thread_timeline.h
new file mode 100644
index 0000000..3a6f6ac
--- /dev/null
+++ b/src/trace_redaction/process_thread_timeline.h
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_REDACTION_PROCESS_THREAD_TIMELINE_H_
+#define SRC_TRACE_REDACTION_PROCESS_THREAD_TIMELINE_H_
+
+#include <cstdint>
+#include <list>
+#include <optional>
+#include <vector>
+
+namespace perfetto::trace_redaction {
+
+class ProcessThreadTimeline {
+ public:
+  // Opened and closed events are used to mark the start and end of lifespans.
+  class Event {
+   public:
+    enum class Type { kInvalid, kOpen, kClose };
+
+    Event()
+        : type_(ProcessThreadTimeline::Event::Type::kInvalid),
+          ts_(0),
+          pid_(0),
+          ppid_(0),
+          uid_(0) {}
+
+    Type type() const { return type_; }
+
+    uint64_t ts() const { return ts_; }
+
+    int32_t pid() const { return pid_; }
+
+    int32_t ppid() const { return ppid_; }
+
+    uint64_t uid() const { return uid_; }
+
+    bool operator==(const Event& o) const {
+      switch (type_) {
+        case Type::kOpen:
+          return o.type_ == Type::kOpen && ts_ == o.ts_ && pid_ == o.pid_ &&
+                 ppid_ == o.ppid_ && uid_ == o.uid_;
+
+        case Type::kClose:
+          return o.type_ == Type::kClose && ts_ == o.ts_ && pid_ == o.pid_;
+
+        case Type::kInvalid:
+          return o.type_ == Type::kInvalid;
+      }
+
+      return false;
+    }
+
+    bool operator!=(const Event& o) const { return !(*this == o); }
+
+    static Event Open(uint64_t ts, int32_t pid, int32_t ppid, uint64_t uid) {
+      return Event(ProcessThreadTimeline::Event::Type::kOpen, ts, pid, ppid,
+                   uid);
+    }
+
+    static Event Open(uint64_t ts, int32_t pid, int32_t ppid) {
+      return Event(ProcessThreadTimeline::Event::Type::kOpen, ts, pid, ppid, 0);
+    }
+
+    static Event Close(uint64_t ts, int32_t pid) {
+      return Event(ProcessThreadTimeline::Event::Type::kClose, ts, pid, 0, 0);
+    }
+
+   private:
+    Event(Type type, uint64_t ts, int32_t pid, int32_t ppid, uint64_t uid)
+        : type_(type), ts_(ts), pid_(pid), ppid_(ppid), uid_(uid) {}
+
+    Type type_ = Type::kInvalid;
+
+    // Valid: open & close
+    uint64_t ts_ = 0;
+
+    // Valid: open & close
+    int32_t pid_ = -1;
+
+    // Valid: open
+    int32_t ppid_ = -1;
+
+    // Valid: open
+    uint64_t uid_ = 0;
+  };
+
+  // The state of a process at a specific point in time.
+  struct Slice {
+    int32_t pid = -1;
+
+    // It is safe to use 0 as the invalid value because that's effectively
+    // what happening in the trace.
+    uint64_t uid = 0;
+  };
+
+  ProcessThreadTimeline() = default;
+
+  ProcessThreadTimeline(const ProcessThreadTimeline&) = delete;
+  ProcessThreadTimeline& operator=(const ProcessThreadTimeline&) = delete;
+  ProcessThreadTimeline(ProcessThreadTimeline&&) = delete;
+  ProcessThreadTimeline& operator=(ProcessThreadTimeline&&) = delete;
+
+  void Append(const Event& event);
+
+  // REQUIRED: Sorts all events by pid, making it possible to locate the subset
+  // of events connected to a pid. Events are not sorted by time because the
+  // subset of events will, on average, be trivally small.
+  void Sort();
+
+  // OPTIONAL: minimizes the distance between the leaf nodes and the package
+  // nodes (a node with a uid value not equal to zero).
+  void Flatten();
+
+  // OPTIONAL: Removes events from the timeline that:
+  //
+  //  1. Reduces the number of events in the timeline to shrink the search
+  //  space.
+  //
+  //  2. Does not invalidate the timeline.
+  //
+  // This can only be called after calling Sort(). Calling Reduce() before
+  // Sort() has undefined behaviour. Calling Reduce() after AppendOpen() if
+  // AppendClose() (without a call to Sort() call) has undefined behaviour.
+  void Reduce(uint64_t package_uid);
+
+  // Returns a snapshot that contains a process's pid and ppid, but contains the
+  // first uid found in its parent-child chain. If a uid cannot be found, uid=0
+  // is returned.
+  //
+  // `Sort()` must be called before this.
+  Slice Search(uint64_t ts, int32_t pid) const;
+
+  // Finds the distance between pid and its uid.
+  //
+  // Returns -1 it pid has no connection to a uid.
+  // Returns 0 if pid has an immediately connection to a uid.
+  //
+  // Return n where:  n is the number of pids between the given pid and the pid
+  //                  connected to the uid. For example, assume D() is a
+  //                  function that measures the distance between two nodes in
+  //                  the same chain:
+  //
+  //                    | pid | depth
+  //                    | a   : 0
+  //                    | b   : 1
+  //                    | c   : 2 --> uid = 98
+  //
+  //                    D(a) = 2
+  //                    D(b) = 1
+  //                    D(c) = 0
+  std::optional<size_t> GetDepth(uint64_t ts, int32_t pid) const;
+
+ private:
+  // Effectively this is the same as:
+  //
+  //  events_for(pid).before(ts).sort_by_time().last()
+  std::optional<Event> FindPreviousEvent(uint64_t ts, int32_t pid) const;
+
+  std::optional<Event> Search(size_t depth, uint64_t ts, int32_t pid) const;
+
+  std::optional<size_t> GetDepth(size_t depth, uint64_t ts, int32_t pid) const;
+
+  bool TestEvent(std::optional<Event> event) const;
+
+  // The number of events are unclear. Use a list when in "write-only" mode and
+  // then change to a vector for "read-only" mode.
+  std::list<Event> write_only_events_;
+  std::vector<Event> read_only_events_;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_PROCESS_THREAD_TIMELINE_H_
diff --git a/src/trace_redaction/process_thread_timeline_unittest.cc b/src/trace_redaction/process_thread_timeline_unittest.cc
new file mode 100644
index 0000000..ea7e5e5
--- /dev/null
+++ b/src/trace_redaction/process_thread_timeline_unittest.cc
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cstdint>
+
+#include "src/trace_redaction/process_thread_timeline.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+
+class SliceTestParams {
+ public:
+  SliceTestParams(uint64_t ts, int32_t pid, uint64_t uid)
+      : ts_(ts), pid_(pid), uid_(uid) {}
+
+  uint64_t ts() const { return ts_; }
+  int32_t pid() const { return pid_; }
+  uint64_t uid() const { return uid_; }
+
+ private:
+  uint64_t ts_;
+  int32_t pid_;
+  uint64_t uid_;
+};
+
+class DepthTestParams {
+ public:
+  DepthTestParams(uint64_t ts,
+                  int32_t pid,
+                  std::optional<size_t> raw_depth,
+                  std::optional<size_t> flat_depth)
+      : ts_(ts), pid_(pid), raw_depth_(raw_depth), flat_depth_(flat_depth) {}
+
+  uint64_t ts() const { return ts_; }
+  int32_t pid() const { return pid_; }
+  std::optional<size_t> raw_depth() const { return raw_depth_; }
+  std::optional<size_t> flat_depth() const { return flat_depth_; }
+
+ private:
+  uint64_t ts_;
+  int32_t pid_;
+  std::optional<size_t> raw_depth_;
+  std::optional<size_t> flat_depth_;
+};
+
+constexpr uint64_t kTimeA = 0;
+constexpr uint64_t kTimeB = 10;
+constexpr uint64_t kTimeC = 20;
+constexpr uint64_t kTimeD = 30;
+constexpr uint64_t kTimeE = 40;
+constexpr uint64_t kTimeF = 50;
+constexpr uint64_t kTimeG = 60;
+constexpr uint64_t kTimeH = 70;
+constexpr uint64_t kTimeI = 70;
+
+constexpr int32_t kPidA = 1;
+constexpr int32_t kPidB = 2;
+constexpr int32_t kPidC = 3;
+
+constexpr uint64_t kNoPackage = 0;
+
+constexpr int32_t kUidA = 98;
+constexpr int32_t kUidB = 99;
+
+}  // namespace
+
+class TimelineEventsTest : public testing::Test,
+                           public testing::WithParamInterface<SliceTestParams> {
+ protected:
+  ProcessThreadTimeline timeline_;
+};
+
+class TimelineEventsOpenAndCloseSingleTest : public TimelineEventsTest {};
+
+TEST_P(TimelineEventsOpenAndCloseSingleTest, PidsEndOnClose) {
+  auto params = GetParam();
+
+  timeline_.Append(
+      ProcessThreadTimeline::Event::Open(kTimeB, kPidB, kPidA, kUidA));
+  timeline_.Append(ProcessThreadTimeline::Event::Close(kTimeD, kPidB));
+
+  timeline_.Sort();
+  timeline_.Flatten();
+
+  auto slice = timeline_.Search(params.ts(), params.pid());
+  ASSERT_EQ(slice.pid, params.pid());
+  ASSERT_EQ(slice.uid, params.uid());
+}
+
+INSTANTIATE_TEST_SUITE_P(AcrossWholeTimeline,
+                         TimelineEventsOpenAndCloseSingleTest,
+                         testing::Values(
+                             // No UID found before opening event.
+                             SliceTestParams(kTimeA, kPidB, kNoPackage),
+
+                             // UID found when opening event starts.
+                             SliceTestParams(kTimeB, kPidB, kUidA),
+
+                             // UID found between opening and close events.
+                             SliceTestParams(kTimeC, kPidB, kUidA),
+
+                             // UID is no longer found at the close event.
+                             SliceTestParams(kTimeD, kPidB, kNoPackage),
+
+                             // UID is no longer found after the close event.
+                             SliceTestParams(kTimeE, kPidB, kNoPackage)));
+
+class TimelineEventsOpenAfterOpenTest : public TimelineEventsTest {};
+
+// |--- PID A --- >
+//                 |--- PID A --- >
+TEST_P(TimelineEventsOpenAfterOpenTest, FindsUid) {
+  auto params = GetParam();
+
+  timeline_.Append(
+      ProcessThreadTimeline::Event::Open(kTimeB, kPidB, kPidA, kUidA));
+  timeline_.Append(
+      ProcessThreadTimeline::Event::Open(kTimeD, kPidB, kPidA, kUidB));
+
+  timeline_.Sort();
+
+  auto slice = timeline_.Search(params.ts(), params.pid());
+  ASSERT_EQ(slice.pid, params.pid());
+  ASSERT_EQ(slice.uid, params.uid());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    AcrossWholeTimeline,
+    TimelineEventsOpenAfterOpenTest,
+    testing::Values(SliceTestParams(kTimeA, kPidB, kNoPackage),
+                    SliceTestParams(kTimeB, kPidB, kUidA),
+                    SliceTestParams(kTimeC, kPidB, kUidA),
+                    SliceTestParams(kTimeD, kPidB, kUidB),
+                    SliceTestParams(kTimeE, kPidB, kUidB)));
+
+class TimelineEventsOverlappingRangesTest : public TimelineEventsTest {};
+
+TEST_P(TimelineEventsOverlappingRangesTest, FindsUid) {
+  auto params = GetParam();
+
+  // |----- PID_A -----|
+  //          |----- PID_B -----|
+  timeline_.Append(ProcessThreadTimeline::Event::Open(kTimeA, kPidA, 0, kUidA));
+  timeline_.Append(ProcessThreadTimeline::Event::Open(kTimeC, kPidB, 0, kUidB));
+  timeline_.Append(ProcessThreadTimeline::Event::Close(kTimeE, kPidA));
+  timeline_.Append(ProcessThreadTimeline::Event::Close(kTimeG, kPidB));
+
+  timeline_.Sort();
+
+  auto slice = timeline_.Search(params.ts(), params.pid());
+  ASSERT_EQ(slice.pid, params.pid());
+  ASSERT_EQ(slice.uid, params.uid());
+}
+
+INSTANTIATE_TEST_SUITE_P(AcrossWholeTimeline,
+                         TimelineEventsOverlappingRangesTest,
+                         testing::Values(
+                             // When pid A starts and before pid B starts.
+                             SliceTestParams(kTimeA, kPidA, kUidA),
+                             SliceTestParams(kTimeA, kPidB, kNoPackage),
+
+                             // After pid A starts and before pid B starts.
+                             SliceTestParams(kTimeB, kPidA, kUidA),
+                             SliceTestParams(kTimeB, kPidB, kNoPackage),
+
+                             // After pid A starts and when pid B starts.
+                             SliceTestParams(kTimeC, kPidA, kUidA),
+                             SliceTestParams(kTimeC, kPidB, kUidB),
+
+                             // After pid A and pid starts.
+                             SliceTestParams(kTimeD, kPidA, kUidA),
+                             SliceTestParams(kTimeD, kPidB, kUidB),
+
+                             // When pid A closes but before pid B closes.
+                             SliceTestParams(kTimeE, kPidA, kNoPackage),
+                             SliceTestParams(kTimeE, kPidB, kUidB),
+
+                             // After pid A closes but before pid B closes.
+                             SliceTestParams(kTimeF, kPidA, kNoPackage),
+                             SliceTestParams(kTimeF, kPidB, kUidB),
+
+                             // After pid A closes and when pid B closes.
+                             SliceTestParams(kTimeG, kPidA, kNoPackage),
+                             SliceTestParams(kTimeG, kPidB, kNoPackage)));
+
+class TimelineEventsParentChildTest : public TimelineEventsTest {};
+
+TEST_P(TimelineEventsParentChildTest, FindsUid) {
+  auto params = GetParam();
+
+  // |------------- PID_A ------------->
+  //         |----- PID_B -----|
+  timeline_.Append(ProcessThreadTimeline::Event::Open(kTimeA, kPidA, 0, kUidA));
+  timeline_.Append(ProcessThreadTimeline::Event::Open(kTimeC, kPidB, kPidA));
+  timeline_.Append(ProcessThreadTimeline::Event::Close(kTimeE, kPidB));
+
+  timeline_.Sort();
+
+  auto slice = timeline_.Search(params.ts(), params.pid());
+  ASSERT_EQ(slice.pid, params.pid());
+  ASSERT_EQ(slice.uid, params.uid());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    AcrossWholeTimeline,
+    TimelineEventsParentChildTest,
+    testing::Values(SliceTestParams(kTimeB, kPidB, kNoPackage),
+                    SliceTestParams(kTimeC, kPidB, kUidA),
+                    SliceTestParams(kTimeD, kPidB, kUidA),
+                    SliceTestParams(kTimeE, kPidB, kNoPackage)));
+
+class TimelineEventsFlattenTest
+    : public testing::Test,
+      public testing::WithParamInterface<DepthTestParams> {
+ protected:
+  ProcessThreadTimeline timeline_;
+};
+
+TEST_P(TimelineEventsFlattenTest, BeforeFlatten) {
+  auto params = GetParam();
+
+  // |---------- PID_A ----------|
+  //      |----- PID_B -----|
+  //         |-- PID_C --|
+  timeline_.Append(ProcessThreadTimeline::Event::Open(kTimeB, kPidA, 0, kUidA));
+  timeline_.Append(ProcessThreadTimeline::Event::Open(kTimeC, kPidB, kPidA));
+  timeline_.Append(ProcessThreadTimeline::Event::Open(kTimeD, kPidC, kPidB));
+
+  // Time E is when all spans are valid.
+
+  timeline_.Append(ProcessThreadTimeline::Event::Close(kTimeF, kPidC));
+  timeline_.Append(ProcessThreadTimeline::Event::Close(kTimeG, kPidB));
+  timeline_.Append(ProcessThreadTimeline::Event::Close(kTimeH, kPidA));
+
+  timeline_.Sort();
+
+  auto depth = timeline_.GetDepth(params.ts(), params.pid());
+  ASSERT_EQ(depth, params.raw_depth());
+}
+
+TEST_P(TimelineEventsFlattenTest, AfterFlatten) {
+  auto params = GetParam();
+
+  // |---------- PID_A ----------|
+  //      |----- PID_B -----|
+  //         |-- PID_C --|
+  timeline_.Append(ProcessThreadTimeline::Event::Open(kTimeB, kPidA, 0, kUidA));
+  timeline_.Append(ProcessThreadTimeline::Event::Open(kTimeC, kPidB, kPidA));
+  timeline_.Append(ProcessThreadTimeline::Event::Open(kTimeD, kPidC, kPidB));
+
+  // Time E is when all spans are valid.
+
+  timeline_.Append(ProcessThreadTimeline::Event::Close(kTimeF, kPidC));
+  timeline_.Append(ProcessThreadTimeline::Event::Close(kTimeG, kPidB));
+  timeline_.Append(ProcessThreadTimeline::Event::Close(kTimeH, kPidA));
+
+  timeline_.Sort();
+  timeline_.Flatten();
+
+  auto depth = timeline_.GetDepth(params.ts(), params.pid());
+  ASSERT_EQ(depth, params.flat_depth());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    AcrossWholeTimeline,
+    TimelineEventsFlattenTest,
+    testing::Values(
+        // Pid A
+        DepthTestParams(kTimeA, kPidA, std::nullopt, std::nullopt),
+        DepthTestParams(kTimeB, kPidA, 0, 0),
+        DepthTestParams(kTimeC, kPidA, 0, 0),
+        DepthTestParams(kTimeD, kPidA, 0, 0),
+        DepthTestParams(kTimeE, kPidA, 0, 0),
+        DepthTestParams(kTimeF, kPidA, 0, 0),
+        DepthTestParams(kTimeG, kPidA, 0, 0),
+        DepthTestParams(kTimeH,
+                        kPidA,
+                        std::nullopt,
+                        std::nullopt),  // pid A ends
+        DepthTestParams(kTimeI, kPidA, std::nullopt, std::nullopt),
+
+        // Pid B
+        DepthTestParams(kTimeA, kPidB, std::nullopt, std::nullopt),
+        DepthTestParams(kTimeB, kPidB, std::nullopt, std::nullopt),
+        DepthTestParams(kTimeC, kPidB, 1, 0),
+        DepthTestParams(kTimeD, kPidB, 1, 0),
+        DepthTestParams(kTimeE, kPidB, 1, 0),
+        DepthTestParams(kTimeF, kPidB, 1, 0),
+        DepthTestParams(kTimeG,
+                        kPidB,
+                        std::nullopt,
+                        std::nullopt),  // pid B ends
+        DepthTestParams(kTimeH, kPidB, std::nullopt, std::nullopt),
+        DepthTestParams(kTimeI, kPidB, std::nullopt, std::nullopt),
+
+        // Pid C
+        DepthTestParams(kTimeA, kPidC, std::nullopt, std::nullopt),
+        DepthTestParams(kTimeB, kPidC, std::nullopt, std::nullopt),
+        DepthTestParams(kTimeC, kPidC, std::nullopt, std::nullopt),
+        DepthTestParams(kTimeD, kPidC, 2, 0),
+        DepthTestParams(kTimeE, kPidC, 2, 0),
+        DepthTestParams(kTimeF,
+                        kPidC,
+                        std::nullopt,
+                        std::nullopt),  // pid C ends
+        DepthTestParams(kTimeG, kPidC, std::nullopt, std::nullopt),
+        DepthTestParams(kTimeH, kPidC, std::nullopt, std::nullopt),
+        DepthTestParams(kTimeI, kPidC, std::nullopt, std::nullopt)));
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/prune_package_list.cc b/src/trace_redaction/prune_package_list.cc
index 83d2355..a85a18f 100644
--- a/src/trace_redaction/prune_package_list.cc
+++ b/src/trace_redaction/prune_package_list.cc
@@ -34,8 +34,9 @@
     return base::ErrStatus("PrunePackageList: missing package uid.");
   }
 
-  if (protos::pbzero::TracePacket::Decoder trace_packet_decoder(*packet);
-      !trace_packet_decoder.has_packages_list()) {
+  protos::pbzero::TracePacket::Decoder trace_packet_decoder(*packet);
+
+  if (!trace_packet_decoder.has_packages_list()) {
     return base::OkStatus();
   }
 
diff --git a/src/trace_redaction/scrub_process_trees.cc b/src/trace_redaction/scrub_process_trees.cc
new file mode 100644
index 0000000..5c54d83
--- /dev/null
+++ b/src/trace_redaction/scrub_process_trees.cc
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_redaction/scrub_process_trees.h"
+
+#include <cstdint>
+#include <string>
+
+#include "perfetto/base/status.h"
+#include "perfetto/protozero/field.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "src/trace_redaction/proto_util.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+#include "protos/perfetto/trace/ps/process_tree.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+namespace {
+
+constexpr auto kThreadsFieldNumber =
+    protos::pbzero::ProcessTree::kThreadsFieldNumber;
+constexpr auto kTimestampFieldNumber =
+    protos::pbzero::TracePacket::kTimestampFieldNumber;
+constexpr auto kProcessTreeFieldNumber =
+    protos::pbzero::TracePacket::kProcessTreeFieldNumber;
+constexpr auto kProcessesFieldNumber =
+    protos::pbzero::ProcessTree::kProcessesFieldNumber;
+
+// Skips the cmdline fields.
+void ClearProcessName(protozero::ConstBytes bytes,
+                      protos::pbzero::ProcessTree::Process* message) {
+  protozero::ProtoDecoder decoder(bytes);
+
+  for (auto field = decoder.ReadField(); field; field = decoder.ReadField()) {
+    if (field.id() !=
+        protos::pbzero::ProcessTree::Process::kCmdlineFieldNumber) {
+      proto_util::AppendField(field, message);
+    }
+  }
+}
+
+void ScrubProcess(protozero::Field field,
+                  const ProcessThreadTimeline& timeline,
+                  uint64_t now,
+                  uint64_t uid,
+                  protos::pbzero::ProcessTree* message) {
+  if (field.id() != kProcessesFieldNumber) {
+    PERFETTO_FATAL(
+        "ScrubProcess() should only be called with a ProcessTree::Processes");
+  }
+
+  protos::pbzero::ProcessTree::Process::Decoder decoder(field.as_bytes());
+  auto slice = timeline.Search(now, decoder.pid());
+
+  if (NormalizeUid(slice.uid) == NormalizeUid(uid)) {
+    proto_util::AppendField(field, message);
+  } else {
+    ClearProcessName(field.as_bytes(), message->add_processes());
+  }
+}
+
+// The thread name is unused, but it's safer to remove it.
+void ClearThreadName(protozero::ConstBytes bytes,
+                     protos::pbzero::ProcessTree::Thread* message) {
+  protozero::ProtoDecoder decoder(bytes);
+
+  for (auto field = decoder.ReadField(); field; field = decoder.ReadField()) {
+    if (field.id() != protos::pbzero::ProcessTree::Thread::kNameFieldNumber) {
+      proto_util::AppendField(field, message);
+    }
+  }
+}
+
+void ScrubThread(protozero::Field field,
+                 const ProcessThreadTimeline& timeline,
+                 uint64_t now,
+                 uint64_t uid,
+                 protos::pbzero::ProcessTree* message) {
+  if (field.id() != kThreadsFieldNumber) {
+    PERFETTO_FATAL(
+        "ScrubThread() should only be called with a ProcessTree::Threads");
+  }
+
+  protos::pbzero::ProcessTree::Thread::Decoder thread_decoder(field.as_bytes());
+  auto slice = timeline.Search(now, thread_decoder.tid());
+
+  if (NormalizeUid(slice.uid) == NormalizeUid(uid)) {
+    proto_util::AppendField(field, message);
+  } else {
+    ClearThreadName(field.as_bytes(), message->add_threads());
+  }
+}
+
+}  // namespace
+
+base::Status ScrubProcessTrees::Transform(const Context& context,
+                                          std::string* packet) const {
+  if (!context.package_uid.has_value()) {
+    return base::ErrStatus("Missing package uid.");
+  }
+
+  if (context.timeline == nullptr) {
+    return base::ErrStatus("Missing timeline.");
+  }
+
+  protozero::ProtoDecoder decoder(*packet);
+
+  if (!decoder.FindField(kProcessTreeFieldNumber).valid()) {
+    return base::OkStatus();
+  }
+
+  auto timestamp_field = decoder.FindField(kTimestampFieldNumber);
+
+  if (!timestamp_field.valid()) {
+    return base::ErrStatus("Could not find timestamp in trace packet");
+  }
+
+  auto timestamp = timestamp_field.as_uint64();
+
+  auto uid = context.package_uid.value();
+
+  const auto& timeline = *context.timeline.get();
+
+  protozero::HeapBuffered<protos::pbzero::TracePacket> message;
+
+  for (auto packet_field = decoder.ReadField(); packet_field.valid();
+       packet_field = decoder.ReadField()) {
+    if (packet_field.id() != kProcessTreeFieldNumber) {
+      proto_util::AppendField(packet_field, message.get());
+      continue;
+    }
+
+    auto* process_tree_message = message->set_process_tree();
+
+    protozero::ProtoDecoder process_tree_decoder(packet_field.as_bytes());
+
+    for (auto process_tree_field = process_tree_decoder.ReadField();
+         process_tree_field.valid();
+         process_tree_field = process_tree_decoder.ReadField()) {
+      switch (process_tree_field.id()) {
+        case kProcessesFieldNumber:
+          ScrubProcess(process_tree_field, timeline, timestamp, uid,
+                       process_tree_message);
+          break;
+
+        case kThreadsFieldNumber:
+          ScrubThread(process_tree_field, timeline, timestamp, uid,
+                      process_tree_message);
+          break;
+
+        default:
+          proto_util::AppendField(process_tree_field, process_tree_message);
+          break;
+      }
+    }
+  }
+
+  packet->assign(message.SerializeAsString());
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/scrub_process_trees.h b/src/trace_redaction/scrub_process_trees.h
new file mode 100644
index 0000000..61cb85a
--- /dev/null
+++ b/src/trace_redaction/scrub_process_trees.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_REDACTION_SCRUB_PROCESS_TREES_H_
+#define SRC_TRACE_REDACTION_SCRUB_PROCESS_TREES_H_
+
+#include <string>
+
+#include "perfetto/base/status.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+// Removes process names and thread names from process_trees if their pids/tids
+// are not connected to the target package.
+class ScrubProcessTrees final : public TransformPrimitive {
+ public:
+  base::Status Transform(const Context& context,
+                         std::string* packet) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_SCRUB_PROCESS_TREES_H_
diff --git a/src/trace_redaction/scrub_process_trees_integrationtest.cc b/src/trace_redaction/scrub_process_trees_integrationtest.cc
new file mode 100644
index 0000000..c1ca7b8
--- /dev/null
+++ b/src/trace_redaction/scrub_process_trees_integrationtest.cc
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "protos/perfetto/trace/ps/process_tree.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+#include "src/base/test/status_matchers.h"
+#include "src/base/test/tmp_dir_tree.h"
+#include "src/base/test/utils.h"
+#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/find_package_uid.h"
+#include "src/trace_redaction/optimize_timeline.h"
+#include "src/trace_redaction/scrub_process_trees.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redactor.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+
+constexpr std::string_view kTracePath =
+    "test/data/trace-redaction-general.pftrace";
+constexpr std::string_view kProcessName =
+    "com.Unity.com.unity.multiplayer.samples.coop";
+
+}  // namespace
+
+class ScrubProcessTreesIntegrationTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    // ScrubProcessTrees depends on:
+    //    - FindPackageUid    (creates: uid)
+    //    - OptimizeTimeline  (creates: optimized timeline)
+    //
+    // OptimizeTimeline depends on:
+    //    - FindPackageUid (uses: uid)
+    //    - BuildTimeline  (uses: timeline)
+    //
+    // BuildTimeline depends on.... nothing
+    // FindPackageUid depends on... nothing
+
+    redactor_.collectors()->emplace_back(new FindPackageUid());
+    redactor_.collectors()->emplace_back(new BuildTimeline());
+    redactor_.builders()->emplace_back(new OptimizeTimeline());
+    redactor_.transformers()->emplace_back(new ScrubProcessTrees());
+
+    // In this case, the process and package have the same name.
+    context_.package_name = kProcessName;
+
+    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
+
+    dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
+    tmp_dir_.TrackFile("dst.pftrace");
+  }
+
+  base::Status Redact() {
+    return redactor_.Redact(src_trace_, dest_trace_, &context_);
+  }
+
+  base::StatusOr<std::string> LoadOriginal() const {
+    return ReadRawTrace(src_trace_);
+  }
+
+  base::StatusOr<std::string> LoadRedacted() const {
+    return ReadRawTrace(dest_trace_);
+  }
+
+  std::vector<std::string> CollectProcessNames(
+      protos::pbzero::Trace::Decoder trace) const {
+    std::vector<std::string> names;
+
+    for (auto packet_it = trace.packet(); packet_it; ++packet_it) {
+      protos::pbzero::TracePacket::Decoder packet(*packet_it);
+
+      if (!packet.has_process_tree()) {
+        continue;
+      }
+
+      protos::pbzero::ProcessTree::Decoder process_tree(packet.process_tree());
+
+      for (auto process_it = process_tree.processes(); process_it;
+           ++process_it) {
+        protos::pbzero::ProcessTree::Process::Decoder process(*process_it);
+
+        if (process.has_cmdline()) {
+          names.push_back(process.cmdline()->as_std_string());
+        }
+      }
+    }
+
+    return names;
+  }
+
+ private:
+  base::StatusOr<std::string> ReadRawTrace(const std::string& path) const {
+    std::string redacted_buffer;
+
+    if (base::ReadFile(path, &redacted_buffer)) {
+      return redacted_buffer;
+    }
+
+    return base::ErrStatus("Failed to read %s", path.c_str());
+  }
+
+  Context context_;
+  TraceRedactor redactor_;
+
+  base::TmpDirTree tmp_dir_;
+
+  std::string src_trace_;
+  std::string dest_trace_;
+};
+
+TEST_F(ScrubProcessTreesIntegrationTest, RemovesProcessNamesFromProcessTrees) {
+  ASSERT_OK(Redact());
+
+  auto original_trace_str = LoadOriginal();
+  ASSERT_OK(original_trace_str);
+
+  auto redacted_trace_str = LoadRedacted();
+  ASSERT_OK(redacted_trace_str);
+
+  protos::pbzero::Trace::Decoder original_trace(original_trace_str.value());
+  auto original_processes = CollectProcessNames(std::move(original_trace));
+
+  ASSERT_GT(original_processes.size(), 1u);
+
+  protos::pbzero::Trace::Decoder redacted_trace(redacted_trace_str.value());
+  auto redacted_processes = CollectProcessNames(std::move(redacted_trace));
+
+  ASSERT_EQ(redacted_processes.size(), 1u);
+  ASSERT_EQ(redacted_processes.at(0), kProcessName);
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/trace_redaction_framework.h b/src/trace_redaction/trace_redaction_framework.h
index d22ecd2..96f7bf1 100644
--- a/src/trace_redaction/trace_redaction_framework.h
+++ b/src/trace_redaction/trace_redaction_framework.h
@@ -18,14 +18,15 @@
 #define SRC_TRACE_REDACTION_TRACE_REDACTION_FRAMEWORK_H_
 
 #include <cstdint>
+#include <memory>
 #include <optional>
 #include <string>
 
 #include "perfetto/base/flat_set.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/status_or.h"
-
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
+#include "src/trace_redaction/process_thread_timeline.h"
 
 namespace perfetto::trace_redaction {
 
@@ -156,6 +157,24 @@
   //  3.  In this example, a cpu_idle event populates the one-of slot in the
   //      ftrace event
   base::FlatSet<uint32_t> ftrace_packet_allow_list;
+
+  // The timeline is a query-focused data structure that connects a pid to a
+  // uid at specific point in time.
+  //
+  // A timeline has two modes:
+  //
+  //    1. write-only
+  //    2. read-only
+  //
+  // Attempting to use the timeline incorrectly results in undefined behaviour.
+  //
+  // To use a timeline, the primitive needs to be "built" (add events) and then
+  // "sealed" (transition to read-only).
+  //
+  // A timeline must have Sort() called to change from write-only to read-only.
+  // After Sort(), Flatten() and Reduce() can be called (optional) to improve
+  // the practical look-up times (compared to theoretical look-up times).
+  std::unique_ptr<ProcessThreadTimeline> timeline;
 };
 
 // Responsible for extracting low-level data from the trace and storing it in
diff --git a/src/trace_redaction/trace_redactor_integrationtest.cc b/src/trace_redaction/trace_redactor_integrationtest.cc
index bd7847d..3ee73da 100644
--- a/src/trace_redaction/trace_redactor_integrationtest.cc
+++ b/src/trace_redaction/trace_redactor_integrationtest.cc
@@ -41,61 +41,31 @@
 
 namespace perfetto::trace_redaction {
 
+// TODO(vaage): Add tests for the untested and/or included primitives:
+//
+//              1. Scrub process tree
+
 namespace {
 using FtraceEvent = protos::pbzero::FtraceEvent;
-using PackagesList = protos::pbzero::PackagesList;
-using PackageInfo = protos::pbzero::PackagesList::PackageInfo;
-using Trace = protos::pbzero::Trace;
-using TracePacket = protos::pbzero::TracePacket;
 
 constexpr std::string_view kTracePath =
     "test/data/trace-redaction-general.pftrace";
 
+// Set the package name to "just some package name". If a specific package name
+// is needed, the test it should overwrite this value.
+constexpr std::string_view kPackageName =
+    "com.Unity.com.unity.multiplayer.samples.coop";
 constexpr uint64_t kPackageUid = 10252;
 
 class TraceRedactorIntegrationTest : public testing::Test {
- public:
-  TraceRedactorIntegrationTest() = default;
-  ~TraceRedactorIntegrationTest() override = default;
-
  protected:
   void SetUp() override {
     src_trace_ = base::GetTestDataPath(std::string(kTracePath));
-
-    // Add every primitive to the redactor. This should mirror the production
-    // configuration. This configuration may differ to help with verifying the
-    // results.
-    redactor_.collectors()->emplace_back(new FindPackageUid());
-    redactor_.builders()->emplace_back(new PopulateAllowlists());
-    redactor_.transformers()->emplace_back(new PrunePackageList());
-    redactor_.transformers()->emplace_back(new ScrubTracePacket());
-    redactor_.transformers()->emplace_back(new ScrubFtraceEvents());
-
-    // Set the package name to "just some package name". If a specific package
-    // name is needed, it should overwrite this value.
-    context_.package_name = "com.google.omadm.trigger";
+    context_.package_name = kPackageName;
   }
 
   const std::string& src_trace() const { return src_trace_; }
 
-  std::vector<protozero::ConstBytes> GetPackageInfos(
-      const Trace::Decoder& trace) const {
-    std::vector<protozero::ConstBytes> infos;
-
-    for (auto packet_it = trace.packet(); packet_it; ++packet_it) {
-      TracePacket::Decoder packet_decoder(*packet_it);
-      if (packet_decoder.has_packages_list()) {
-        PackagesList::Decoder list_it(packet_decoder.packages_list());
-        for (auto info_it = list_it.packages(); info_it; ++info_it) {
-          PackageInfo::Decoder info(*info_it);
-          infos.push_back(*info_it);
-        }
-      }
-    }
-
-    return infos;
-  }
-
   static base::StatusOr<std::string> ReadRawTrace(const std::string& path) {
     std::string redacted_buffer;
 
@@ -106,11 +76,164 @@
     return base::ErrStatus("Failed to read %s", path.c_str());
   }
 
+  std::string src_trace_;
+  base::TmpDirTree tmp_dir_;
+
+  Context context_;
+  TraceRedactor redactor_;
+};
+
+class PackageListTraceRedactorIntegrationTest
+    : public TraceRedactorIntegrationTest {
+ protected:
+  void SetUp() override {
+    TraceRedactorIntegrationTest::SetUp();
+
+    redactor_.collectors()->emplace_back(new FindPackageUid());
+    redactor_.transformers()->emplace_back(new PrunePackageList());
+  }
+
+  std::vector<protozero::ConstBytes> GetPackageInfos(
+      const protos::pbzero::Trace::Decoder& trace) const {
+    std::vector<protozero::ConstBytes> infos;
+
+    for (auto packet_it = trace.packet(); packet_it; ++packet_it) {
+      protos::pbzero::TracePacket::Decoder packet_decoder(*packet_it);
+      if (packet_decoder.has_packages_list()) {
+        protos::pbzero::PackagesList::Decoder list_it(
+            packet_decoder.packages_list());
+        for (auto info_it = list_it.packages(); info_it; ++info_it) {
+          protos::pbzero::PackagesList::PackageInfo::Decoder info(*info_it);
+          infos.push_back(*info_it);
+        }
+      }
+    }
+
+    return infos;
+  }
+};
+
+TEST_F(PackageListTraceRedactorIntegrationTest,
+       FindsPackageAndFiltersPackageList) {
+  auto result = redactor_.Redact(
+      src_trace(), tmp_dir_.AbsolutePath("dst.pftrace"), &context_);
+
+  ASSERT_OK(result) << result.message();
+
+  tmp_dir_.TrackFile("dst.pftrace");
+
+  ASSERT_OK_AND_ASSIGN(auto redacted_buffer,
+                       ReadRawTrace(tmp_dir_.AbsolutePath("dst.pftrace")));
+
+  protos::pbzero::Trace::Decoder redacted_trace(redacted_buffer);
+  std::vector<protozero::ConstBytes> infos = GetPackageInfos(redacted_trace);
+
+  ASSERT_TRUE(context_.package_uid.has_value());
+  ASSERT_EQ(NormalizeUid(context_.package_uid.value()),
+            NormalizeUid(kPackageUid));
+
+  // It is possible for two packages_list to appear in the trace. The
+  // find_package_uid will stop after the first one is found. Package uids are
+  // appear as n * 1,000,000 where n is some integer. It is also possible for
+  // two packages_list to contain copies of each other - for example
+  // "com.Unity.com.unity.multiplayer.samples.coop" appears in both
+  // packages_list.
+  ASSERT_EQ(infos.size(), 2u);
+
+  std::vector<protos::pbzero::PackagesList::PackageInfo::Decoder> decoders;
+  decoders.emplace_back(infos[0]);
+  decoders.emplace_back(infos[1]);
+
+  for (auto& decoder : decoders) {
+    ASSERT_TRUE(decoder.has_name());
+    ASSERT_EQ(decoder.name().ToStdString(),
+              "com.Unity.com.unity.multiplayer.samples.coop");
+
+    ASSERT_TRUE(decoder.has_uid());
+    ASSERT_EQ(NormalizeUid(decoder.uid()), NormalizeUid(kPackageUid));
+  }
+}
+
+// It is possible for multiple packages to share a uid. The names will appears
+// across multiple package lists. The only time the package name appears is in
+// the package list, so there is no way to differentiate these packages (only
+// the uid is used later), so each entry should remain.
+TEST_F(PackageListTraceRedactorIntegrationTest, RetainsAllInstancesOfUid) {
+  context_.package_name = "com.google.android.networkstack.tethering";
+
+  auto result = redactor_.Redact(
+      src_trace(), tmp_dir_.AbsolutePath("dst.pftrace"), &context_);
+
+  ASSERT_OK(result) << result.message();
+
+  tmp_dir_.TrackFile("dst.pftrace");
+
+  ASSERT_OK_AND_ASSIGN(auto redacted_buffer,
+                       ReadRawTrace(tmp_dir_.AbsolutePath("dst.pftrace")));
+
+  protos::pbzero::Trace::Decoder redacted_trace(redacted_buffer);
+  std::vector<protozero::ConstBytes> infos = GetPackageInfos(redacted_trace);
+
+  ASSERT_EQ(infos.size(), 8u);
+
+  std::array<std::string, 8> package_names;
+
+  for (size_t i = 0; i < infos.size(); ++i) {
+    protos::pbzero::PackagesList::PackageInfo::Decoder info(infos[i]);
+    ASSERT_TRUE(info.has_name());
+    package_names[i] = info.name().ToStdString();
+  }
+
+  std::sort(package_names.begin(), package_names.end());
+  ASSERT_EQ(package_names[0], "com.google.android.cellbroadcastservice");
+  ASSERT_EQ(package_names[1], "com.google.android.cellbroadcastservice");
+  ASSERT_EQ(package_names[2], "com.google.android.networkstack");
+  ASSERT_EQ(package_names[3], "com.google.android.networkstack");
+  ASSERT_EQ(package_names[4],
+            "com.google.android.networkstack.permissionconfig");
+  ASSERT_EQ(package_names[5],
+            "com.google.android.networkstack.permissionconfig");
+  ASSERT_EQ(package_names[6], "com.google.android.networkstack.tethering");
+  ASSERT_EQ(package_names[7], "com.google.android.networkstack.tethering");
+}
+
+// Broadphase redactions are meant to remove large pieces of information (e.g.
+// whole packets). They need to be tested on their own because finer-grain
+// redactions can make it harder to verify the results. For example, broadphase
+// redactions should not remove rename events. There is a finer grain redaction
+// that takes care of those events. However, it the "scrub rename events"
+// redaction removes all the rename events, it could look like the broadphase
+// primitive did it.
+class BroadphaseTraceRedactorIntegrationTest
+    : public TraceRedactorIntegrationTest {
+ protected:
+  void SetUp() override {
+    TraceRedactorIntegrationTest::SetUp();
+
+    redactor_.collectors()->emplace_back(new FindPackageUid());
+    redactor_.builders()->emplace_back(new PopulateAllowlists());
+    redactor_.transformers()->emplace_back(new ScrubTracePacket());
+    redactor_.transformers()->emplace_back(new ScrubFtraceEvents());
+  }
+
+  static base::StatusOr<protozero::ConstBytes> FindFirstFtraceEvents(
+      const protos::pbzero::Trace::Decoder& trace) {
+    for (auto packet_it = trace.packet(); packet_it; ++packet_it) {
+      protos::pbzero::TracePacket::Decoder packet(*packet_it);
+
+      if (packet.has_ftrace_events()) {
+        return packet.ftrace_events();
+      }
+    }
+
+    return base::ErrStatus("Failed to find ftrace events");
+  }
+
   // NOTE - this will include fields like "timestamp" and "pid".
-  static void GetEventFields(const Trace::Decoder& trace,
+  static void GetEventFields(const protos::pbzero::Trace::Decoder& trace,
                              base::FlatSet<uint32_t>* set) {
     for (auto packet_it = trace.packet(); packet_it; ++packet_it) {
-      TracePacket::Decoder packet(*packet_it);
+      protos::pbzero::TracePacket::Decoder packet(*packet_it);
 
       if (!packet.has_ftrace_events()) {
         continue;
@@ -132,110 +255,10 @@
       }
     }
   }
-
-  static base::StatusOr<protozero::ConstBytes> FindFirstFtraceEvents(
-      const Trace::Decoder& trace) {
-    for (auto packet_it = trace.packet(); packet_it; ++packet_it) {
-      TracePacket::Decoder packet(*packet_it);
-
-      if (packet.has_ftrace_events()) {
-        return packet.ftrace_events();
-      }
-    }
-
-    return base::ErrStatus("Failed to find ftrace events");
-  }
-
-  std::string src_trace_;
-  base::TmpDirTree tmp_dir_;
-
-  Context context_;
-  TraceRedactor redactor_;
 };
 
-TEST_F(TraceRedactorIntegrationTest, FindsPackageAndFiltersPackageList) {
-  context_.package_name = "com.Unity.com.unity.multiplayer.samples.coop";
-
-  auto result = redactor_.Redact(
-      src_trace(), tmp_dir_.AbsolutePath("dst.pftrace"), &context_);
-  tmp_dir_.TrackFile("dst.pftrace");
-
-  ASSERT_OK(result);
-
-  ASSERT_OK_AND_ASSIGN(auto redacted_buffer,
-                       ReadRawTrace(tmp_dir_.AbsolutePath("dst.pftrace")));
-
-  Trace::Decoder redacted_trace(redacted_buffer);
-  std::vector<protozero::ConstBytes> infos = GetPackageInfos(redacted_trace);
-
-  ASSERT_TRUE(context_.package_uid.has_value());
-  ASSERT_EQ(NormalizeUid(context_.package_uid.value()),
-            NormalizeUid(kPackageUid));
-
-  // It is possible for two packages_list to appear in the trace. The
-  // find_package_uid will stop after the first one is found. Package uids are
-  // appear as n * 1,000,000 where n is some integer. It is also possible for
-  // two packages_list to contain copies of each other - for example
-  // "com.Unity.com.unity.multiplayer.samples.coop" appears in both
-  // packages_list.
-  ASSERT_EQ(infos.size(), 2u);
-
-  std::array<PackageInfo::Decoder, 2> decoders = {
-      PackageInfo::Decoder(infos[0]), PackageInfo::Decoder(infos[1])};
-
-  for (auto& decoder : decoders) {
-    ASSERT_TRUE(decoder.has_name());
-    ASSERT_EQ(decoder.name().ToStdString(),
-              "com.Unity.com.unity.multiplayer.samples.coop");
-
-    ASSERT_TRUE(decoder.has_uid());
-    ASSERT_EQ(NormalizeUid(decoder.uid()), NormalizeUid(kPackageUid));
-  }
-}
-
-// It is possible for multiple packages to share a uid. The names will appears
-// across multiple package lists. The only time the package name appears is in
-// the package list, so there is no way to differentiate these packages (only
-// the uid is used later), so each entry should remain.
-TEST_F(TraceRedactorIntegrationTest, RetainsAllInstancesOfUid) {
-  context_.package_name = "com.google.android.networkstack.tethering";
-
-  auto result = redactor_.Redact(
-      src_trace(), tmp_dir_.AbsolutePath("dst.pftrace"), &context_);
-  tmp_dir_.TrackFile("dst.pftrace");
-  ASSERT_OK(result);
-
-  ASSERT_OK_AND_ASSIGN(auto redacted_buffer,
-                       ReadRawTrace(tmp_dir_.AbsolutePath("dst.pftrace")));
-
-  Trace::Decoder redacted_trace(redacted_buffer);
-  std::vector<protozero::ConstBytes> infos = GetPackageInfos(redacted_trace);
-
-  ASSERT_EQ(infos.size(), 8u);
-
-  std::array<std::string, 8> package_names;
-
-  for (size_t i = 0; i < infos.size(); ++i) {
-    PackageInfo::Decoder info(infos[i]);
-    ASSERT_TRUE(info.has_name());
-    package_names[i] = info.name().ToStdString();
-  }
-
-  std::sort(package_names.begin(), package_names.end());
-  ASSERT_EQ(package_names[0], "com.google.android.cellbroadcastservice");
-  ASSERT_EQ(package_names[1], "com.google.android.cellbroadcastservice");
-  ASSERT_EQ(package_names[2], "com.google.android.networkstack");
-  ASSERT_EQ(package_names[3], "com.google.android.networkstack");
-  ASSERT_EQ(package_names[4],
-            "com.google.android.networkstack.permissionconfig");
-  ASSERT_EQ(package_names[5],
-            "com.google.android.networkstack.permissionconfig");
-  ASSERT_EQ(package_names[6], "com.google.android.networkstack.tethering");
-  ASSERT_EQ(package_names[7], "com.google.android.networkstack.tethering");
-}
-
 // Makes sure all not-allowed ftrace event is removed from a trace.
-TEST_F(TraceRedactorIntegrationTest, RemovesFtraceEvents) {
+TEST_F(BroadphaseTraceRedactorIntegrationTest, RemovesFtraceEvents) {
   auto pre_redaction_file = src_trace();
   auto post_redaction_file = tmp_dir_.AbsolutePath("dst.pftrace");
 
@@ -243,7 +266,7 @@
   // events are not in the allowlist and should be dropped.
   auto pre_redaction_buffer = ReadRawTrace(pre_redaction_file);
   ASSERT_OK(pre_redaction_buffer) << pre_redaction_buffer.status().message();
-  Trace::Decoder pre_redaction_trace(*pre_redaction_buffer);
+  protos::pbzero::Trace::Decoder pre_redaction_trace(*pre_redaction_buffer);
 
   base::FlatSet<uint32_t> pre_redaction_event_types;
   GetEventFields(pre_redaction_trace, &pre_redaction_event_types);
@@ -258,7 +281,7 @@
 
   auto post_redaction_buffer = ReadRawTrace(post_redaction_file);
   ASSERT_OK(post_redaction_buffer) << post_redaction_buffer.status().message();
-  Trace::Decoder post_redaction_trace(*post_redaction_buffer);
+  protos::pbzero::Trace::Decoder post_redaction_trace(*post_redaction_buffer);
 
   base::FlatSet<uint32_t> post_redaction_event_types;
   GetEventFields(post_redaction_trace, &post_redaction_event_types);
@@ -269,7 +292,7 @@
 
 // When a event is dropped from ftrace_events, only that event should be droped,
 // the other events in the ftrace_events should be retained.
-TEST_F(TraceRedactorIntegrationTest,
+TEST_F(BroadphaseTraceRedactorIntegrationTest,
        RetainsFtraceEventsWhenRemovingFtraceEvent) {
   auto pre_redaction_file = src_trace();
   auto post_redaction_file = tmp_dir_.AbsolutePath("dst.pftrace");
@@ -277,7 +300,7 @@
   auto pre_redaction_buffer = ReadRawTrace(pre_redaction_file);
   ASSERT_OK(pre_redaction_buffer) << pre_redaction_buffer.status().message();
 
-  Trace::Decoder pre_redaction_trace(*pre_redaction_buffer);
+  protos::pbzero::Trace::Decoder pre_redaction_trace(*pre_redaction_buffer);
 
   auto pre_redaction_first_events = FindFirstFtraceEvents(pre_redaction_trace);
   ASSERT_OK(pre_redaction_first_events)
@@ -291,7 +314,7 @@
   auto post_redaction_buffer = ReadRawTrace(post_redaction_file);
   ASSERT_OK(post_redaction_buffer) << post_redaction_buffer.status().message();
 
-  Trace::Decoder post_redaction_trace(*post_redaction_buffer);
+  protos::pbzero::Trace::Decoder post_redaction_trace(*post_redaction_buffer);
 
   auto post_redaction_ftrace_events =
       FindFirstFtraceEvents(post_redaction_trace);
diff --git a/src/traced/probes/ftrace/cpu_reader.cc b/src/traced/probes/ftrace/cpu_reader.cc
index 462c992..66b9b1b 100644
--- a/src/traced/probes/ftrace/cpu_reader.cc
+++ b/src/traced/probes/ftrace/cpu_reader.cc
@@ -297,13 +297,16 @@
   if (pages_read == 0)
     return pages_read;
 
+  uint64_t last_read_ts = last_read_event_ts_;
   for (FtraceDataSource* data_source : started_data_sources) {
+    last_read_ts = last_read_event_ts_;
     ProcessPagesForDataSource(
         data_source->trace_writer(), data_source->mutable_metadata(), cpu_,
         data_source->parsing_config(), data_source->mutable_parse_errors(),
-        &last_read_event_ts_, parsing_buf, pages_read, compact_sched_buf,
-        table_, symbolizer_, ftrace_clock_snapshot_, ftrace_clock_);
+        &last_read_ts, parsing_buf, pages_read, compact_sched_buf, table_,
+        symbolizer_, ftrace_clock_snapshot_, ftrace_clock_);
   }
+  last_read_event_ts_ = last_read_ts;
 
   return pages_read;
 }
@@ -407,6 +410,13 @@
 // event bundle proto with a timestamp, letting the trace processor decide
 // whether to discard or keep the post-error data. Previously, we crashed as
 // soon as we encountered such an error.
+// TODO(rsavitski, b/192586066): consider moving last_read_event_ts tracking to
+// be per-datasource. The current implementation can be pessimistic if there are
+// multiple concurrent data sources, one of which is only interested in sparse
+// events (imagine a print filter and one matching event every minute, while the
+// buffers are read - advancing the last read timestamp - multiple times per
+// second). Tracking the timestamp of the last event *written into the
+// datasource* can be more accurate.
 // static
 bool CpuReader::ProcessPagesForDataSource(
     TraceWriter* trace_writer,
diff --git a/src/traced/probes/ftrace/cpu_reader_unittest.cc b/src/traced/probes/ftrace/cpu_reader_unittest.cc
index 33a8ca3..02d2962 100644
--- a/src/traced/probes/ftrace/cpu_reader_unittest.cc
+++ b/src/traced/probes/ftrace/cpu_reader_unittest.cc
@@ -1021,6 +1021,7 @@
   EXPECT_EQ(last_read_event_ts_, 1'045'157'726'697'236ULL);
 
   auto bundle = GetBundle();
+  EXPECT_EQ(0u, bundle.last_read_event_timestamp());
   ASSERT_EQ(bundle.event().size(), 6u);
   {
     const protos::gen::FtraceEvent& event = bundle.event()[1];
@@ -1078,6 +1079,7 @@
   auto bundle = GetBundle();
 
   const auto& compact_sched = bundle.compact_sched();
+  EXPECT_EQ(0u, bundle.last_read_event_timestamp());
 
   EXPECT_EQ(6u, compact_sched.switch_timestamp().size());
   EXPECT_EQ(6u, compact_sched.switch_prev_state().size());
@@ -3625,6 +3627,7 @@
 
   const uint64_t kSecondPrintTs = 1308020252356549ULL;
   EXPECT_EQ(kSecondPrintTs, first_bundle.event()[1].timestamp());
+  EXPECT_EQ(0u, first_bundle.last_read_event_timestamp());
 
   // 1 print + lost_events + updated last_read_event_timestamp
   auto const& second_bundle = packets[1].ftrace_events();
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index 46b7d24..6db79ff 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -7361,6 +7361,31 @@
        kUnsetFtraceId,
        430,
        kUnsetSize},
+      {"panel_write_generic",
+       "panel",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "pid", 1, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "trace_name", 2, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "trace_begin", 3, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "name", 4, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "type", 5, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "value", 6, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       490,
+       kUnsetSize},
       {"sched_switch_with_ctrs",
        "perf_trace_counters",
        {
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index 7fcfef1..97969be 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -203,6 +203,9 @@
         AddEventGroup(table, "g2d", &events);
         InsertEvent("g2d", "tracing_mark_write", &events);
         InsertEvent("g2d", "g2d_perf_update_qos", &events);
+
+        AddEventGroup(table, "panel", &events);
+        InsertEvent("panel", "panel_write_generic", &events);
         continue;
       }
 
diff --git a/src/traced/probes/ftrace/test/data/synthetic/available_events b/src/traced/probes/ftrace/test/data/synthetic/available_events
index c57e8f9..299c4ac 100644
--- a/src/traced/probes/ftrace/test/data/synthetic/available_events
+++ b/src/traced/probes/ftrace/test/data/synthetic/available_events
@@ -13,3 +13,4 @@
 power:suspend_resume
 cpuhp:cpuhp_pause
 lwis:tracing_mark_write
+panel:panel_write_generic
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/panel/panel_write_generic/format b/src/traced/probes/ftrace/test/data/synthetic/events/panel/panel_write_generic/format
new file mode 100644
index 0000000..550e6ef
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/panel/panel_write_generic/format
@@ -0,0 +1,14 @@
+name: panel_write_generic
+ID: 1122
+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 type;	offset:8;	size:1;	signed:0;
+	field:int pid;	offset:12;	size:4;	signed:1;
+	field:__data_loc char[] name;	offset:16;	size:4;	signed:0;
+	field:int value;	offset:20;	size:4;	signed:1;
+
+print fmt: "%c|%d|%s|%d", REC->type, REC->pid, __get_str(name), REC->value
diff --git a/src/tracing/service/trace_buffer_unittest.cc b/src/tracing/service/trace_buffer_unittest.cc
index 906b0df..65cb425 100644
--- a/src/tracing/service/trace_buffer_unittest.cc
+++ b/src/tracing/service/trace_buffer_unittest.cc
@@ -812,24 +812,24 @@
   TraceBuffer::PacketSequenceProperties sequence_properties;
   ASSERT_THAT(ReadPacket(&sequence_properties),
               ElementsAre(FakePacketFragment(10, 'a')));
-  ASSERT_EQ(11u, sequence_properties.producer_uid_trusted());
+  ASSERT_EQ(static_cast<uid_t>(11), sequence_properties.producer_uid_trusted());
 
   ASSERT_THAT(
       ReadPacket(&sequence_properties),
       ElementsAre(FakePacketFragment(10, 'b'), FakePacketFragment(10, 'e')));
-  ASSERT_EQ(11u, sequence_properties.producer_uid_trusted());
+  ASSERT_EQ(static_cast<uid_t>(11), sequence_properties.producer_uid_trusted());
 
   ASSERT_THAT(ReadPacket(&sequence_properties),
               ElementsAre(FakePacketFragment(10, 'f')));
-  ASSERT_EQ(11u, sequence_properties.producer_uid_trusted());
+  ASSERT_EQ(static_cast<uid_t>(11), sequence_properties.producer_uid_trusted());
 
   ASSERT_THAT(ReadPacket(&sequence_properties),
               ElementsAre(FakePacketFragment(10, 'c')));
-  ASSERT_EQ(22u, sequence_properties.producer_uid_trusted());
+  ASSERT_EQ(static_cast<uid_t>(22), sequence_properties.producer_uid_trusted());
 
   ASSERT_THAT(ReadPacket(&sequence_properties),
               ElementsAre(FakePacketFragment(10, 'd')));
-  ASSERT_EQ(22u, sequence_properties.producer_uid_trusted());
+  ASSERT_EQ(static_cast<uid_t>(22), sequence_properties.producer_uid_trusted());
 
   ASSERT_THAT(ReadPacket(), IsEmpty());
 }
diff --git a/test/cts/heapprofd_test_cts.cc b/test/cts/heapprofd_test_cts.cc
index 6c87cf4..fec7aea 100644
--- a/test/cts/heapprofd_test_cts.cc
+++ b/test/cts/heapprofd_test_cts.cc
@@ -20,11 +20,14 @@
 #include <sys/wait.h>
 
 #include <random>
+#include <string>
+#include <string_view>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/tracing/core/data_source_config.h"
 #include "src/base/test/test_task_runner.h"
+#include "src/base/test/tmp_dir_tree.h"
 #include "test/android_test_utils.h"
 #include "test/gtest_and_gmock.h"
 #include "test/test_helper.h"
@@ -71,25 +74,47 @@
   return result;
 }
 
-std::optional<int64_t> ReadInt64FromFile(const std::string& path) {
-  std::string contents;
-  if (!base::ReadFile(path, &contents)) {
-    return std::nullopt;
+// Asks FileContentProvider.java inside the app to read a file.
+class ContentProviderReader {
+ public:
+  explicit ContentProviderReader(const std::string& app,
+                                 const std::string& path) {
+    tmp_dir_.TrackFile("contents.txt");
+    tempfile_ = tmp_dir_.AbsolutePath("contents.txt");
+    cmd_ = std::string("content read --uri content://") + app +
+           std::string("/") + path + " >" + tempfile_;
   }
-  return base::StringToInt64(contents);
-}
+  std::optional<int64_t> ReadInt64() {
+    if (system(cmd_.c_str()) != 0) {
+      return std::nullopt;
+    }
+    return ReadInt64FromFile(tempfile_);
+  }
+
+ private:
+  std::optional<int64_t> ReadInt64FromFile(const std::string& path) {
+    std::string contents;
+    if (!base::ReadFile(path, &contents)) {
+      return std::nullopt;
+    }
+    return base::StringToInt64(contents);
+  }
+
+  base::TmpDirTree tmp_dir_;
+  std::string tempfile_;
+  std::string cmd_;
+};
 
 bool WaitForAppAllocationCycle(const std::string& app_name, size_t timeout_ms) {
   const size_t sleep_per_attempt_us = 100 * 1000;
   const size_t max_attempts = timeout_ms * 1000 / sleep_per_attempt_us;
 
-  std::string path = std::string("/sdcard/Android/data/") + app_name +
-                     std::string("/files/") + std::string(kReportCyclePath);
+  ContentProviderReader app_reader(app_name, std::string(kReportCyclePath));
 
   for (size_t attempts = 0; attempts < max_attempts;) {
     int64_t first_value;
     for (; attempts < max_attempts; attempts++) {
-      std::optional<int64_t> val = ReadInt64FromFile(path);
+      std::optional<int64_t> val = app_reader.ReadInt64();
       if (val) {
         first_value = *val;
         break;
@@ -98,7 +123,7 @@
     }
 
     for (; attempts < max_attempts; attempts++) {
-      std::optional<int64_t> val = ReadInt64FromFile(path);
+      std::optional<int64_t> val = app_reader.ReadInt64();
       if (!val || *val < first_value) {
         break;
       }
diff --git a/test/cts/test_apps/AndroidManifest_debuggable.xml b/test/cts/test_apps/AndroidManifest_debuggable.xml
index 291469e..79993f8 100755
--- a/test/cts/test_apps/AndroidManifest_debuggable.xml
+++ b/test/cts/test_apps/AndroidManifest_debuggable.xml
@@ -71,5 +71,10 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <provider
+          android:name="android.perfetto.cts.app.FileContentProvider"
+          android:authorities="android.perfetto.cts.app.debuggable"
+          android:exported="true"
+          android:grantUriPermissions="true" />
     </application>
 </manifest>
diff --git a/test/cts/test_apps/AndroidManifest_nonprofileable.xml b/test/cts/test_apps/AndroidManifest_nonprofileable.xml
index a332175..8322daf 100755
--- a/test/cts/test_apps/AndroidManifest_nonprofileable.xml
+++ b/test/cts/test_apps/AndroidManifest_nonprofileable.xml
@@ -59,6 +59,11 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <provider
+          android:name="android.perfetto.cts.app.FileContentProvider"
+          android:authorities="android.perfetto.cts.app.nonprofileable"
+          android:exported="true"
+          android:grantUriPermissions="true" />
     </application>
 </manifest>
 
diff --git a/test/cts/test_apps/AndroidManifest_profileable.xml b/test/cts/test_apps/AndroidManifest_profileable.xml
index 077fd95..cd434d4 100755
--- a/test/cts/test_apps/AndroidManifest_profileable.xml
+++ b/test/cts/test_apps/AndroidManifest_profileable.xml
@@ -72,6 +72,11 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <provider
+          android:name="android.perfetto.cts.app.FileContentProvider"
+          android:authorities="android.perfetto.cts.app.profileable"
+          android:exported="true"
+          android:grantUriPermissions="true" />
     </application>
 </manifest>
 
diff --git a/test/cts/test_apps/AndroidManifest_release.xml b/test/cts/test_apps/AndroidManifest_release.xml
index 417a539..1795a59 100755
--- a/test/cts/test_apps/AndroidManifest_release.xml
+++ b/test/cts/test_apps/AndroidManifest_release.xml
@@ -71,5 +71,10 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <provider
+          android:name="android.perfetto.cts.app.FileContentProvider"
+          android:authorities="android.perfetto.cts.app.release"
+          android:exported="true"
+          android:grantUriPermissions="true" />
     </application>
 </manifest>
diff --git a/test/cts/test_apps/src/android/perfetto/cts/app/FileContentProvider.java b/test/cts/test_apps/src/android/perfetto/cts/app/FileContentProvider.java
new file mode 100644
index 0000000..5419cfe
--- /dev/null
+++ b/test/cts/test_apps/src/android/perfetto/cts/app/FileContentProvider.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.perfetto.cts.app;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+public class FileContentProvider extends ContentProvider {
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        String filePath = uri.getPath();
+        File file = new File(getContext().getExternalFilesDir(null), filePath);
+        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
+    }
+}
diff --git a/test/trace_processor/diff_tests/stdlib/graphs/search_tests.py b/test/trace_processor/diff_tests/stdlib/graphs/search_tests.py
index 1e3b96b..48cc56f 100644
--- a/test/trace_processor/diff_tests/stdlib/graphs/search_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/graphs/search_tests.py
@@ -153,3 +153,48 @@
         3,2
         2,"[NULL]"
         """))
+
+  def test_weight_bounded_dfs(self):
+    return DiffTestBlueprint(
+        trace=DataPath('counters.json'),
+        query="""
+          INCLUDE PERFETTO MODULE graphs.search;
+
+          CREATE PERFETTO TABLE foo AS
+          SELECT 0 AS source_node_id, 0 AS dest_node_id, 0 AS edge_weight
+          UNION ALL
+          VALUES (1, 2, 1)
+          UNION ALL
+          VALUES (1, 3, 1)
+          UNION ALL
+          VALUES (3, 4, 1)
+          UNION ALL
+          VALUES (3, 5, 0)
+          UNION ALL
+          VALUES (5, 6, 0);
+
+          CREATE PERFETTO TABLE roots AS
+          SELECT 0 AS root_node_id, 0 AS root_max_weight
+          UNION ALL
+          VALUES (1, 2)
+          UNION ALL
+          VALUES (3, 1)
+          UNION ALL
+          VALUES (2, 0);
+
+          SELECT * FROM graph_reachable_weight_bounded_dfs!(foo, roots);
+        """,
+        out=Csv("""
+        "root_node_id","node_id","parent_node_id"
+        0,0,"[NULL]"
+        1,1,"[NULL]"
+        1,2,1
+        1,3,1
+        1,5,3
+        1,6,5
+        3,3,"[NULL]"
+        3,4,3
+        3,5,3
+        3,6,5
+        2,2,"[NULL]"
+        """))
diff --git a/test/trace_processor/diff_tests/tables/tests_sched.py b/test/trace_processor/diff_tests/tables/tests_sched.py
index 46ff428..71ca6ab 100644
--- a/test/trace_processor/diff_tests/tables/tests_sched.py
+++ b/test/trace_processor/diff_tests/tables/tests_sched.py
@@ -152,7 +152,6 @@
           ts,
           dur,
           utid,
-          waker_utid,
           blocked_dur,
           blocked_state,
           blocked_function,
@@ -164,17 +163,17 @@
         LIMIT 10
         """,
         out=Csv("""
-        "root_id","parent_id","id","ts","dur","utid","waker_utid","blocked_dur","blocked_state","blocked_function","is_root","depth"
-        357,377,380,1735842234188,283571,46,427,351402620,"I","worker_thread",0,5
-        394,402,405,1735843726296,8545303,46,427,1208537,"I","worker_thread",0,3
-        357,419,432,1735850643698,16245,95,1465,154087,"I","worker_thread",0,4
-        357,443,446,1735851953029,554638012,95,427,1103252,"I","worker_thread",0,6
-        357,500,503,1735886367018,191863,46,427,34095419,"I","worker_thread",0,10
-        357,446,667,1736125372478,52493,46,95,238813597,"I","worker_thread",0,7
-        357,835,838,1736405409972,278036,46,427,279985001,"I","worker_thread",0,12
-        357,862,865,1736406817672,7959441,46,427,1129664,"I","worker_thread",0,10
-        357,882,889,1736413734042,25870,95,1467,7143001,"I","worker_thread",0,11
-        357,882,894,1736413763072,31692550,11,1467,4413060,"I","rcu_gp_fqs_loop",0,11
+        "root_id","parent_id","id","ts","dur","utid","blocked_dur","blocked_state","blocked_function","is_root","depth"
+        357,377,380,1735842234188,283571,46,351402620,"I","worker_thread",0,5
+        394,402,405,1735843726296,8545303,46,1208537,"I","worker_thread",0,3
+        357,419,432,1735850643698,16245,95,154087,"I","worker_thread",0,4
+        357,443,446,1735851953029,554638012,95,1103252,"I","worker_thread",0,6
+        357,500,503,1735886367018,191863,46,34095419,"I","worker_thread",0,10
+        357,446,667,1736125372478,52493,46,238813597,"I","worker_thread",0,7
+        357,835,838,1736405409972,278036,46,279985001,"I","worker_thread",0,12
+        357,862,865,1736406817672,7959441,46,1129664,"I","worker_thread",0,10
+        357,882,889,1736413734042,25870,95,7143001,"I","worker_thread",0,11
+        357,882,894,1736413763072,31692550,11,4413060,"I","rcu_gp_fqs_loop",0,11
         """))
 
   def test_thread_executing_span_graph_contains_forked_states(self):
@@ -189,7 +188,6 @@
           ts,
           dur,
           utid,
-          waker_utid,
           blocked_dur,
           blocked_state,
           blocked_function,
@@ -199,8 +197,8 @@
           WHERE ts = 1735842081507 AND dur = 293868
         """,
         out=Csv("""
-        "root_id","parent_id","id","ts","dur","utid","waker_utid","blocked_dur","blocked_state","blocked_function","is_root","depth"
-        357,369,376,1735842081507,293868,1465,230,"[NULL]","[NULL]","[NULL]",0,4
+        "root_id","parent_id","id","ts","dur","utid","blocked_dur","blocked_state","blocked_function","is_root","depth"
+        357,369,376,1735842081507,293868,1465,"[NULL]","[NULL]","[NULL]",0,4
         """))
 
   def test_thread_executing_span_runnable_state_has_no_running(self):
diff --git a/tools/check_sql_modules.py b/tools/check_sql_modules.py
index ff21b4f..935bf9e 100755
--- a/tools/check_sql_modules.py
+++ b/tools/check_sql_modules.py
@@ -44,10 +44,8 @@
         '_chrome_mojo_slices', '_chrome_java_views', '_chrome_scheduler_tasks',
         '_chrome_tasks'
     ],
-    '/sched/thread_executing_span.sql': [
-        '_wakeup', '_thread_executing_span_graph', '_critical_path',
-        '_wakeup_graph', '_thread_executing_span_graph'
-    ],
+    '/sched/thread_executing_span.sql': ['_wakeup_graph', '_thread_executing_span_graph',
+        '_critical_path'],
     '/slices/flat_slices.sql': ['_slice_flattened']
 }
 
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index 561657d..c8db4ec 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -71,6 +71,7 @@
     '//src/traced/probes:traced_probes',
     '//src/traced/service:traced',
     '//src/trace_processor:trace_processor_shell',
+    '//src/trace_redaction:trace_redactor',
     '//test/cts:perfetto_cts_deps',
     '//test/cts:perfetto_cts_jni_deps',
     '//test:perfetto_gtest_logcat_printer',
@@ -245,10 +246,6 @@
         ('test_suites', {'general-tests'}),
         ('test_config', 'PerfettoIntegrationTests.xml'),
     ],
-    'traced_probes': [('required', {
-        'libperfetto_android_internal', 'trigger_perfetto', 'traced_perf',
-        'mm_events'
-    }),],
     'libperfetto_android_internal': [('static_libs', {'libhealthhalutils'}),],
     'trace_processor_shell': [
         ('strip', {
diff --git a/tools/gen_c_protos b/tools/gen_c_protos
index 38a7491..370e59a 100755
--- a/tools/gen_c_protos
+++ b/tools/gen_c_protos
@@ -49,6 +49,8 @@
     'files': [
       'src/protozero/test/example_proto/library.proto',
       'src/protozero/test/example_proto/library_internals/galaxies.proto',
+      'src/protozero/test/example_proto/other_package/test_messages.proto',
+      'src/protozero/test/example_proto/subpackage/test_messages.proto',
       'src/protozero/test/example_proto/test_messages.proto',
       'src/protozero/test/example_proto/upper_import.proto',
     ],
diff --git a/tools/tmux b/tools/tmux
index 48c625e..06594b8 100755
--- a/tools/tmux
+++ b/tools/tmux
@@ -56,18 +56,16 @@
 
 function reset_tracing() {
   if is_android "$OUT"; then
-    # Newer versions of Android don't have debugfs mounted at all
-    # anymore so use /sys/kernel/tracing if /d/tracing doesn't exist
-    adb shell 'test -d /sys/kernel/tracing && echo 0 > /sys/kernel/tracing/tracing_on || echo 0 > /sys/kernel/debug/tracing/tracing_on'
+    adb shell 'test -d /sys/kernel/tracing && echo 0 > /sys/kernel/tracing/tracing_on'
   elif ! is_mac; then
     # shellcheck disable=SC2016
     local script='
-    if [ ! -w /sys/kernel/debug ]; then
-      echo "debugfs not accessible, try sudo chown -R $USER /sys/kernel/debug"
-      sudo chown -R "$USER" /sys/kernel/debug
+    if [ ! -w /sys/kernel/tracing ]; then
+      echo "tracefs not accessible, try sudo chown -R $USER /sys/kernel/tracing"
+      sudo chown -R "$USER" /sys/kernel/tracing
     fi
 
-    echo 0 > /sys/kernel/debug/tracing/tracing_on
+    echo 0 > /sys/kernel/tracing/tracing_on
     '
 
     if is_ssh_target; then
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 139fc7b..54151fb 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -40,7 +40,6 @@
 import {defaultViewingOption} from './flamegraph_util';
 import {
   MetatraceTrackId,
-  traceEvent,
   traceEventBegin,
   traceEventEnd,
   TraceEventScope,
@@ -516,9 +515,12 @@
 
   selectNote(state: StateDraft, args: {id: string}): void {
     if (args.id) {
-      state.currentSelection = {
-        kind: 'NOTE',
-        id: args.id,
+      state.selection = {
+        kind: 'legacy',
+        legacySelection: {
+          kind: 'NOTE',
+          id: args.id,
+        },
       };
     }
   },
@@ -553,32 +555,33 @@
     state: StateDraft,
     args: {color: string; persistent: boolean},
   ): void {
-    if (
-      state.currentSelection === null ||
-      state.currentSelection.kind !== 'AREA'
-    ) {
+    if (state.selection.kind !== 'legacy') {
       return;
     }
+    if (state.selection.legacySelection.kind !== 'AREA') {
+      return;
+    }
+    const legacySelection = state.selection.legacySelection;
     const id = args.persistent ? generateNextId(state) : '0';
     const color = args.persistent ? args.color : '#344596';
     state.notes[id] = {
       noteType: 'AREA',
       id,
-      areaId: state.currentSelection.areaId,
+      areaId: legacySelection.areaId,
       color,
       text: '',
     };
-    state.currentSelection.noteId = id;
+    legacySelection.noteId = id;
   },
 
   toggleMarkCurrentArea(state: StateDraft, args: {persistent: boolean}) {
-    const selection = state.currentSelection;
+    const selection = state.selection;
     if (
-      selection != null &&
-      selection.kind === 'AREA' &&
-      selection.noteId !== undefined
+      selection.kind === 'legacy' &&
+      selection.legacySelection.kind === 'AREA' &&
+      selection.legacySelection.noteId !== undefined
     ) {
-      this.removeNote(state, {id: selection.noteId});
+      this.removeNote(state, {id: selection.legacySelection.noteId});
     } else {
       const color = randomColor();
       this.markCurrentArea(state, {color, persistent: args.persistent});
@@ -621,17 +624,19 @@
     delete state.notes[args.id];
     // For regular notes, we clear the current selection but for an area note
     // we only want to clear the note/marking and leave the area selected.
-    if (state.currentSelection === null) return;
+    if (state.selection.kind !== 'legacy') return;
     if (
-      state.currentSelection.kind === 'NOTE' &&
-      state.currentSelection.id === args.id
+      state.selection.legacySelection.kind === 'NOTE' &&
+      state.selection.legacySelection.id === args.id
     ) {
-      state.currentSelection = null;
+      state.selection = {
+        kind: 'empty',
+      };
     } else if (
-      state.currentSelection.kind === 'AREA' &&
-      state.currentSelection.noteId === args.id
+      state.selection.legacySelection.kind === 'AREA' &&
+      state.selection.legacySelection.noteId === args.id
     ) {
-      state.currentSelection.noteId = undefined;
+      state.selection.legacySelection.noteId = undefined;
     }
   },
 
@@ -639,10 +644,13 @@
     state: StateDraft,
     args: {id: number; trackKey: string; scroll?: boolean},
   ): void {
-    state.currentSelection = {
-      kind: 'SLICE',
-      id: args.id,
-      trackKey: args.trackKey,
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'SLICE',
+        id: args.id,
+        trackKey: args.trackKey,
+      },
     };
     state.pendingScrollId = args.scroll ? args.id : undefined;
   },
@@ -651,12 +659,15 @@
     state: StateDraft,
     args: {leftTs: time; rightTs: time; id: number; trackKey: string},
   ): void {
-    state.currentSelection = {
-      kind: 'COUNTER',
-      leftTs: args.leftTs,
-      rightTs: args.rightTs,
-      id: args.id,
-      trackKey: args.trackKey,
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'COUNTER',
+        leftTs: args.leftTs,
+        rightTs: args.rightTs,
+        id: args.id,
+        trackKey: args.trackKey,
+      },
     };
   },
 
@@ -664,12 +675,15 @@
     state: StateDraft,
     args: {id: number; upid: number; ts: time; type: ProfileType},
   ): void {
-    state.currentSelection = {
-      kind: 'HEAP_PROFILE',
-      id: args.id,
-      upid: args.upid,
-      ts: args.ts,
-      type: args.type,
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'HEAP_PROFILE',
+        id: args.id,
+        upid: args.upid,
+        ts: args.ts,
+        type: args.type,
+      },
     };
     this.openFlamegraph(state, {
       type: args.type,
@@ -690,13 +704,16 @@
       type: ProfileType;
     },
   ): void {
-    state.currentSelection = {
-      kind: 'PERF_SAMPLES',
-      id: args.id,
-      upid: args.upid,
-      leftTs: args.leftTs,
-      rightTs: args.rightTs,
-      type: args.type,
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'PERF_SAMPLES',
+        id: args.id,
+        upid: args.upid,
+        leftTs: args.leftTs,
+        rightTs: args.rightTs,
+        type: args.type,
+      },
     };
     this.openFlamegraph(state, {
       type: args.type,
@@ -732,11 +749,14 @@
     state: StateDraft,
     args: {id: number; utid: number; ts: time},
   ): void {
-    state.currentSelection = {
-      kind: 'CPU_PROFILE_SAMPLE',
-      id: args.id,
-      utid: args.utid,
-      ts: args.ts,
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'CPU_PROFILE_SAMPLE',
+        id: args.id,
+        utid: args.utid,
+        ts: args.ts,
+      },
     };
   },
 
@@ -768,11 +788,14 @@
     state: StateDraft,
     args: {id: number; trackKey: string; table?: string; scroll?: boolean},
   ): void {
-    state.currentSelection = {
-      kind: 'CHROME_SLICE',
-      id: args.id,
-      trackKey: args.trackKey,
-      table: args.table,
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'CHROME_SLICE',
+        id: args.id,
+        trackKey: args.trackKey,
+        table: args.table,
+      },
     };
     state.pendingScrollId = args.scroll ? args.id : undefined;
   },
@@ -796,16 +819,19 @@
       ...args.detailsPanelConfig.config,
     };
 
-    state.currentSelection = {
-      kind: 'GENERIC_SLICE',
-      id: args.id,
-      sqlTableName: args.sqlTableName,
-      start: args.start,
-      duration: args.duration,
-      trackKey: args.trackKey,
-      detailsPanelConfig: {
-        kind: args.detailsPanelConfig.kind,
-        config: detailsPanelConfig,
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'GENERIC_SLICE',
+        id: args.id,
+        sqlTableName: args.sqlTableName,
+        start: args.start,
+        duration: args.duration,
+        trackKey: args.trackKey,
+        detailsPanelConfig: {
+          kind: args.detailsPanelConfig.kind,
+          config: detailsPanelConfig,
+        },
       },
     };
   },
@@ -818,10 +844,13 @@
     state: StateDraft,
     args: {id: number; trackKey: string},
   ): void {
-    state.currentSelection = {
-      kind: 'THREAD_STATE',
-      id: args.id,
-      trackKey: args.trackKey,
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'THREAD_STATE',
+        id: args.id,
+        trackKey: args.trackKey,
+      },
     };
   },
 
@@ -829,16 +858,21 @@
     state: StateDraft,
     args: {id: number; trackKey: string; scroll?: boolean},
   ): void {
-    state.currentSelection = {
-      kind: 'LOG',
-      id: args.id,
-      trackKey: args.trackKey,
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'LOG',
+        id: args.id,
+        trackKey: args.trackKey,
+      },
     };
     state.pendingScrollId = args.scroll ? args.id : undefined;
   },
 
   deselect(state: StateDraft, _: {}): void {
-    state.currentSelection = null;
+    state.selection = {
+      kind: 'empty',
+    };
   },
 
   updateLogsPagination(state: StateDraft, args: Pagination): void {
@@ -919,7 +953,10 @@
     assertTrue(start <= end);
     const areaId = generateNextId(state);
     state.areas[areaId] = {id: areaId, start, end, tracks};
-    state.currentSelection = {kind: 'AREA', areaId};
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {kind: 'AREA', areaId},
+    };
   },
 
   editArea(state: StateDraft, args: {area: Area; areaId: string}): void {
@@ -932,10 +969,13 @@
     state: StateDraft,
     args: {areaId: string; noteId: string},
   ): void {
-    state.currentSelection = {
-      kind: 'AREA',
-      areaId: args.areaId,
-      noteId: args.noteId,
+    state.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'AREA',
+        areaId: args.areaId,
+        noteId: args.noteId,
+      },
     };
   },
 
@@ -943,9 +983,14 @@
     state: StateDraft,
     args: {id: string; isTrackGroup: boolean},
   ) {
-    const selection = state.currentSelection;
-    if (selection === null || selection.kind !== 'AREA') return;
-    const areaId = selection.areaId;
+    const selection = state.selection;
+    if (
+      selection.kind !== 'legacy' ||
+      selection.legacySelection.kind !== 'AREA'
+    ) {
+      return;
+    }
+    const areaId = selection.legacySelection.areaId;
     const index = state.areas[areaId].tracks.indexOf(args.id);
     if (index > -1) {
       state.areas[areaId].tracks.splice(index, 1);
@@ -973,7 +1018,7 @@
     // selection to be updated and this leads to bugs for people who do:
     // if (oldSelection !== state.selection) etc.
     // To solve this re-create the selection object here:
-    state.currentSelection = Object.assign({}, state.currentSelection);
+    state.selection = Object.assign({}, state.selection);
   },
 
   setVisibleTraceTime(state: StateDraft, args: VisibleState): void {
@@ -1031,22 +1076,6 @@
     state.hoveredNoteTimestamp = args.ts;
   },
 
-  // Tab V1 specific
-  setCurrentTab(state: StateDraft, args: {tab: string | undefined}) {
-    traceEvent(
-      'setCurrentTab',
-      () => {
-        state.currentTab = args.tab;
-      },
-      {
-        args: {
-          tab: args.tab ?? '<undefined>',
-        },
-      },
-    );
-  },
-
-  // Specific to tabs V2.
   // Add a tab with a given URI to the tab bar and show it.
   // If the tab is already present in the tab bar, just show it.
   showTab(state: StateDraft, args: {uri: string}) {
@@ -1060,7 +1089,6 @@
     state.tabs.currentTab = args.uri;
   },
 
-  // Specific to tabs V2.
   // Hide a tab in the tab bar pick a new tab to show.
   // Note: Attempting to hide the "current_selection" tab doesn't work. This tab
   // is special and cannot be removed.
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index f3f8a0e..f6b2fee 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -139,7 +139,9 @@
     },
 
     status: {msg: '', timestamp: 0},
-    currentSelection: null,
+    selection: {
+      kind: 'empty',
+    },
     currentFlamegraphState: null,
     traceConversionInProgress: false,
 
diff --git a/ui/src/common/selection_observer.ts b/ui/src/common/selection_observer.ts
deleted file mode 100644
index 5bf8dfb..0000000
--- a/ui/src/common/selection_observer.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2022 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Selection} from './state';
-
-export type SelectionChangedObserver = (
-  selection: Selection | undefined,
-  openCurrentSelectionTab: boolean,
-) => void;
-
-const selectionObservers: SelectionChangedObserver[] = [];
-
-export function onSelectionChanged(
-  selection: Selection | undefined,
-  openCurrentSelectionTab: boolean,
-) {
-  for (const observer of selectionObservers) {
-    observer(selection, openCurrentSelectionTab);
-  }
-}
-
-export function addSelectionChangeObserver(
-  observer: SelectionChangedObserver,
-): void {
-  selectionObservers.push(observer);
-}
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index f7c2562..9aa747b 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -15,7 +15,6 @@
 import {BigintMath} from '../base/bigint_math';
 import {duration, Time, time} from '../base/time';
 import {RecordConfig} from '../controller/record_config_types';
-import {GenericSliceDetailsTabConfigBase} from '../frontend/generic_slice_details_tab';
 import {
   Aggregation,
   PivotTree,
@@ -25,6 +24,28 @@
 
 import {Direction} from './event_set';
 
+import {
+  selectionToLegacySelection,
+  Selection,
+  LegacySelection,
+  ProfileType,
+} from '../core/selection_manager';
+
+export {
+  Selection,
+  SelectionKind,
+  NoteSelection,
+  SliceSelection,
+  CounterSelection,
+  HeapProfileSelection,
+  PerfSamplesSelection,
+  LegacySelection,
+  AreaSelection,
+  ProfileType,
+  ChromeSliceSelection,
+  CpuProfileSampleSelection,
+} from '../core/selection_manager';
+
 /**
  * A plain js object, holding objects of type |Class| keyed by string id.
  * We use this instead of using |Map| object since it is simpler and faster to
@@ -60,18 +81,6 @@
   resolution: duration;
 }
 
-export interface AreaSelection {
-  kind: 'AREA';
-  areaId: string;
-  // When an area is marked it will be assigned a unique note id and saved as
-  // an AreaNote for the user to return to later. id = 0 is the special id that
-  // is overwritten when a new area is marked. Any other id is a persistent
-  // marking that will not be overwritten.
-  // When not set, the area selection will be replaced with any
-  // new area selection (i.e. not saved anywhere).
-  noteId?: string;
-}
-
 export type AreaById = Area & {id: string};
 
 export interface Area {
@@ -134,7 +143,10 @@
 // 44. Add TabsV2 state.
 // 45. Remove v1 tracks.
 // 46. Remove trackKeyByTrackId.
-export const STATE_VERSION = 46;
+// 47. Selection V2
+// 48. Rename legacySelection -> selection and introduce new Selection type.
+// 49. Remove currentTab, which is only relevant to TabsV1.
+export const STATE_VERSION = 49;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -170,15 +182,6 @@
   };
 };
 
-export enum ProfileType {
-  HEAP_PROFILE = 'heap_profile',
-  MIXED_HEAP_PROFILE = 'heap_profile:com.android.art,libc.malloc',
-  NATIVE_HEAP_PROFILE = 'heap_profile:libc.malloc',
-  JAVA_HEAP_SAMPLES = 'heap_profile:com.android.art',
-  JAVA_HEAP_GRAPH = 'graph',
-  PERF_SAMPLE = 'perf',
-}
-
 export enum FlamegraphStateViewingOption {
   SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'SPACE',
   ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'ALLOC_SPACE',
@@ -187,6 +190,17 @@
   PERF_SAMPLES_KEY = 'PERF_SAMPLES',
 }
 
+export interface FlamegraphState {
+  kind: 'FLAMEGRAPH_STATE';
+  upids: number[];
+  start: time;
+  end: time;
+  type: ProfileType;
+  viewingOption: FlamegraphStateViewingOption;
+  focusRegex: string;
+  expandedCallsite?: CallsiteInfo;
+}
+
 export interface CallsiteInfo {
   id: number;
   parentId: number;
@@ -307,100 +321,6 @@
   text: string;
 }
 
-export interface NoteSelection {
-  kind: 'NOTE';
-  id: string;
-}
-
-export interface SliceSelection {
-  kind: 'SLICE';
-  id: number;
-}
-
-export interface CounterSelection {
-  kind: 'COUNTER';
-  leftTs: time;
-  rightTs: time;
-  id: number;
-}
-
-export interface HeapProfileSelection {
-  kind: 'HEAP_PROFILE';
-  id: number;
-  upid: number;
-  ts: time;
-  type: ProfileType;
-}
-
-export interface PerfSamplesSelection {
-  kind: 'PERF_SAMPLES';
-  id: number;
-  upid: number;
-  leftTs: time;
-  rightTs: time;
-  type: ProfileType;
-}
-
-export interface FlamegraphState {
-  kind: 'FLAMEGRAPH_STATE';
-  upids: number[];
-  start: time;
-  end: time;
-  type: ProfileType;
-  viewingOption: FlamegraphStateViewingOption;
-  focusRegex: string;
-  expandedCallsite?: CallsiteInfo;
-}
-
-export interface CpuProfileSampleSelection {
-  kind: 'CPU_PROFILE_SAMPLE';
-  id: number;
-  utid: number;
-  ts: time;
-}
-
-export interface ChromeSliceSelection {
-  kind: 'CHROME_SLICE';
-  id: number;
-  table?: string;
-}
-
-export interface ThreadStateSelection {
-  kind: 'THREAD_STATE';
-  id: number;
-}
-
-export interface LogSelection {
-  kind: 'LOG';
-  id: number;
-  trackKey: string;
-}
-
-export interface GenericSliceSelection {
-  kind: 'GENERIC_SLICE';
-  id: number;
-  sqlTableName: string;
-  start: time;
-  duration: duration;
-  // NOTE: this config can be expanded for multiple details panel types.
-  detailsPanelConfig: {kind: string; config: GenericSliceDetailsTabConfigBase};
-}
-
-export type Selection = (
-  | NoteSelection
-  | SliceSelection
-  | CounterSelection
-  | HeapProfileSelection
-  | CpuProfileSampleSelection
-  | ChromeSliceSelection
-  | ThreadStateSelection
-  | AreaSelection
-  | PerfSamplesSelection
-  | LogSelection
-  | GenericSliceSelection
-) & {trackKey?: string};
-export type SelectionKind = Selection['kind']; // 'THREAD_STATE' | 'SLICE' ...
-
 export interface Pagination {
   offset: number;
   count: number;
@@ -573,7 +493,7 @@
   permalink: PermalinkConfig;
   notes: ObjectById<Note | AreaNote>;
   status: Status;
-  currentSelection: Selection | null;
+  selection: Selection;
   currentFlamegraphState: FlamegraphState | null;
   logsPagination: Pagination;
   ftracePagination: Pagination;
@@ -606,8 +526,6 @@
 
   searchIndex: number;
 
-  currentTab?: string;
-
   tabs: TabsV2State;
 
   /**
@@ -997,3 +915,7 @@
   }
   return parentId;
 }
+
+export function getLegacySelection(state: State): LegacySelection | null {
+  return selectionToLegacySelection(state.selection);
+}
diff --git a/ui/src/controller/aggregation/aggregation_controller.ts b/ui/src/controller/aggregation/aggregation_controller.ts
index 1809a9c..2e8cdb2 100644
--- a/ui/src/controller/aggregation/aggregation_controller.ts
+++ b/ui/src/controller/aggregation/aggregation_controller.ts
@@ -19,7 +19,7 @@
   ColumnDef,
   ThreadStateExtra,
 } from '../../common/aggregation_data';
-import {Area, Sorting} from '../../common/state';
+import {Area, Sorting, getLegacySelection} from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {publishAggregateData} from '../../frontend/publish';
 import {Engine} from '../../trace_processor/engine';
@@ -61,7 +61,7 @@
   }
 
   run() {
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (selection === null || selection.kind !== 'AREA') {
       publishAggregateData({
         data: {
diff --git a/ui/src/controller/area_selection_handler.ts b/ui/src/controller/area_selection_handler.ts
index 3efaca8..0104ca8 100644
--- a/ui/src/controller/area_selection_handler.ts
+++ b/ui/src/controller/area_selection_handler.ts
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Area, AreaById} from '../common/state';
+import {Area, AreaById, getLegacySelection} from '../common/state';
 import {globals} from '../frontend/globals';
 
 export class AreaSelectionHandler {
   private previousArea?: Area;
 
   getAreaChange(): [boolean, AreaById | undefined] {
-    const currentSelection = globals.state.currentSelection;
+    const currentSelection = getLegacySelection(globals.state);
     if (currentSelection === null || currentSelection.kind !== 'AREA') {
       return [false, undefined];
     }
diff --git a/ui/src/controller/area_selection_handler_unittest.ts b/ui/src/controller/area_selection_handler_unittest.ts
index 6b7d312..7f27a03 100644
--- a/ui/src/controller/area_selection_handler_unittest.ts
+++ b/ui/src/controller/area_selection_handler_unittest.ts
@@ -33,7 +33,10 @@
     id: areaId,
   };
   globals.store.edit((draft) => {
-    draft.currentSelection = {kind: 'AREA', areaId: areaId};
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {kind: 'AREA', areaId: areaId},
+    };
     draft.areas[areaId] = latestArea;
   });
 
@@ -53,9 +56,12 @@
     id: previousAreaId,
   };
   globals.store.edit((draft) => {
-    draft.currentSelection = {
-      kind: 'AREA',
-      areaId: previousAreaId,
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'AREA',
+        areaId: previousAreaId,
+      },
     };
     draft.areas[previousAreaId] = previous;
   });
@@ -64,9 +70,12 @@
 
   const currentAreaId = '1';
   globals.store.edit((draft) => {
-    draft.currentSelection = {
-      kind: 'AREA',
-      areaId: currentAreaId,
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'AREA',
+        areaId: currentAreaId,
+      },
     };
   });
   const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
@@ -77,13 +86,19 @@
 
 test('UndefinedAreaAfterUndefinedArea', () => {
   globals.store.edit((draft) => {
-    draft.currentSelection = {kind: 'AREA', areaId: '0'};
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {kind: 'AREA', areaId: '0'},
+    };
   });
   const areaSelectionHandler = new AreaSelectionHandler();
   areaSelectionHandler.getAreaChange();
 
   globals.store.edit((draft) => {
-    draft.currentSelection = {kind: 'AREA', areaId: '1'};
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {kind: 'AREA', areaId: '1'},
+    };
   });
   const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
 
@@ -100,9 +115,12 @@
     id: previousAreaId,
   };
   globals.store.edit((draft) => {
-    draft.currentSelection = {
-      kind: 'AREA',
-      areaId: previousAreaId,
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'AREA',
+        areaId: previousAreaId,
+      },
     };
     draft.areas[previousAreaId] = previous;
   });
@@ -117,9 +135,12 @@
     id: currentAreaId,
   };
   globals.store.edit((draft) => {
-    draft.currentSelection = {
-      kind: 'AREA',
-      areaId: currentAreaId,
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'AREA',
+        areaId: currentAreaId,
+      },
     };
     draft.areas[currentAreaId] = current;
   });
@@ -138,9 +159,12 @@
     id: previousAreaId,
   };
   globals.store.edit((draft) => {
-    draft.currentSelection = {
-      kind: 'AREA',
-      areaId: previousAreaId,
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'AREA',
+        areaId: previousAreaId,
+      },
     };
     draft.areas[previousAreaId] = previous;
   });
@@ -155,9 +179,12 @@
     id: currentAreaId,
   };
   globals.store.edit((draft) => {
-    draft.currentSelection = {
-      kind: 'AREA',
-      areaId: currentAreaId,
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'AREA',
+        areaId: currentAreaId,
+      },
     };
     draft.areas[currentAreaId] = current;
   });
@@ -169,17 +196,23 @@
 
 test('NonAreaSelectionAfterUndefinedArea', () => {
   globals.store.edit((draft) => {
-    draft.currentSelection = {kind: 'AREA', areaId: '0'};
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {kind: 'AREA', areaId: '0'},
+    };
   });
   const areaSelectionHandler = new AreaSelectionHandler();
   areaSelectionHandler.getAreaChange();
 
   globals.store.edit((draft) => {
-    draft.currentSelection = {
-      kind: 'COUNTER',
-      leftTs: Time.fromRaw(0n),
-      rightTs: Time.fromRaw(0n),
-      id: 1,
+    draft.selection = {
+      kind: 'legacy',
+      legacySelection: {
+        kind: 'COUNTER',
+        leftTs: Time.fromRaw(0n),
+        rightTs: Time.fromRaw(0n),
+        id: 1,
+      },
     };
   });
   const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
diff --git a/ui/src/controller/cpu_profile_controller.ts b/ui/src/controller/cpu_profile_controller.ts
index 9b7aca2..fd410bf 100644
--- a/ui/src/controller/cpu_profile_controller.ts
+++ b/ui/src/controller/cpu_profile_controller.ts
@@ -12,7 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {CallsiteInfo, CpuProfileSampleSelection} from '../common/state';
+import {
+  CallsiteInfo,
+  CpuProfileSampleSelection,
+  getLegacySelection,
+} from '../common/state';
 import {CpuProfileDetails, globals} from '../frontend/globals';
 import {publishCpuProfileDetails} from '../frontend/publish';
 import {Engine} from '../trace_processor/engine';
@@ -34,7 +38,7 @@
   }
 
   run() {
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (!selection || selection.kind !== 'CPU_PROFILE_SAMPLE') {
       return;
     }
diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts
index 8eaa585..d53142c 100644
--- a/ui/src/controller/flow_events_controller.ts
+++ b/ui/src/controller/flow_events_controller.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {Time} from '../base/time';
-import {Area} from '../common/state';
+import {Area, getLegacySelection} from '../common/state';
 import {featureFlags} from '../core/feature_flags';
 import {Flow, globals} from '../frontend/globals';
 import {publishConnectedFlows, publishSelectedFlows} from '../frontend/publish';
@@ -443,7 +443,7 @@
   }
 
   refreshVisibleFlows() {
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (!selection) {
       this.lastSelectedKind = 'NONE';
       publishConnectedFlows([]);
diff --git a/ui/src/controller/pivot_table_controller.ts b/ui/src/controller/pivot_table_controller.ts
index d01b642..b1eb386 100644
--- a/ui/src/controller/pivot_table_controller.ts
+++ b/ui/src/controller/pivot_table_controller.ts
@@ -21,6 +21,7 @@
   PivotTableQueryMetadata,
   PivotTableResult,
   PivotTableState,
+  getLegacySelection,
 } from '../common/state';
 import {featureFlags} from '../core/feature_flags';
 import {globals} from '../frontend/globals';
@@ -269,7 +270,6 @@
         queryResult: {tree: treeBuilder.build(), metadata: query.metadata},
       }),
     );
-    globals.dispatch(Actions.setCurrentTab({tab: 'pivot_table'}));
   }
 
   async requestArgumentNames() {
@@ -298,7 +298,7 @@
     }
 
     const pivotTableState = globals.state.nonSerializableState.pivotTable;
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
 
     if (
       pivotTableState.queryRequested ||
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index ea1db14..d30b458 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -15,7 +15,7 @@
 import {assertTrue} from '../base/logging';
 import {Time, time} from '../base/time';
 import {Args, ArgValue} from '../common/arg_types';
-import {ChromeSliceSelection} from '../common/state';
+import {ChromeSliceSelection, getLegacySelection} from '../common/state';
 import {
   CounterDetails,
   globals,
@@ -68,7 +68,7 @@
   }
 
   run() {
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (!selection || selection.kind === 'AREA') return;
 
     const selectWithId = [
@@ -281,7 +281,7 @@
     }
 
     // Check selection is still the same on completion of query.
-    if (selection === globals.state.currentSelection) {
+    if (selection === getLegacySelection(globals.state)) {
       publishSliceDetails(selected);
     }
   }
@@ -352,7 +352,7 @@
     `;
     const result = await this.args.engine.query(query);
 
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (result.numRows() > 0 && selection) {
       const row = result.firstRow({
         ts: LONG,
@@ -379,7 +379,7 @@
     WHERE sched.id = ${id}`;
     const result = await this.args.engine.query(sqlQuery);
     // Check selection is still the same on completion of query.
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (result.numRows() > 0 && selection) {
       const row = result.firstRow({
         ts: LONG,
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 56507d9..72d1498 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -26,7 +26,6 @@
   isMetatracingEnabled,
 } from '../common/metatracing';
 import {pluginManager} from '../common/plugins';
-import {onSelectionChanged} from '../common/selection_observer';
 import {
   defaultTraceTime,
   EngineMode,
@@ -34,7 +33,6 @@
   ProfileType,
 } from '../common/state';
 import {featureFlags, Flag, PERF_SAMPLE_FLAG} from '../core/feature_flags';
-import {BottomTabList} from '../frontend/bottom_tab';
 import {
   FtraceStat,
   globals,
@@ -420,7 +418,6 @@
         assertExists(getEnabledMetatracingCategories()),
       );
     }
-    globals.bottomTabList = new BottomTabList(engine.getProxy('BottomTabList'));
 
     globals.engines.set(this.engineId, engine);
     globals.dispatch(
@@ -672,13 +669,6 @@
       }
     }
 
-    // If the trace was shared via a permalink, it might already have a
-    // selection. Emit onSelectionChanged to ensure that the components (like
-    // current selection details) react to it.
-    if (globals.state.currentSelection !== null) {
-      onSelectionChanged(globals.state.currentSelection, true);
-    }
-
     globals.dispatch(Actions.maybeExpandOnlyTrackGroup({}));
 
     // Trace Processor doesn't support the reliable range feature for JSON
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 206d1a9..7d1039b 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -450,7 +450,7 @@
       if (!devMap.has(groupName)) {
         devMap.set(groupName, uuidv4());
       }
-      track.name = 'Size: ' + size;
+      track.name = 'Chunk size: ' + size;
       track.trackGroup = devMap.get(groupName);
     }
 
diff --git a/ui/src/core/feature_flags.ts b/ui/src/core/feature_flags.ts
index 647b546..e0253cb 100644
--- a/ui/src/core/feature_flags.ts
+++ b/ui/src/core/feature_flags.ts
@@ -247,10 +247,3 @@
   description: 'Record using V2 interface',
   defaultValue: false,
 });
-
-export const TABS_V2_FLAG = featureFlags.register({
-  id: 'tabsv2',
-  name: 'Tabs V2',
-  description: 'Use Tabs V2',
-  defaultValue: true,
-});
diff --git a/ui/src/core/generic_slice_details_types.ts b/ui/src/core/generic_slice_details_types.ts
new file mode 100644
index 0000000..186f6e5
--- /dev/null
+++ b/ui/src/core/generic_slice_details_types.ts
@@ -0,0 +1,32 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export interface ColumnConfig {
+  displayName?: string;
+}
+
+export type Columns = {
+  [columnName: string]: ColumnConfig;
+};
+
+export interface GenericSliceDetailsTabConfigBase {
+  sqlTableName: string;
+  title: string;
+  // All columns are rendered if |columns| is undefined.
+  columns?: Columns;
+}
+
+export type GenericSliceDetailsTabConfig = GenericSliceDetailsTabConfigBase & {
+  id: number;
+};
diff --git a/ui/src/core/selection_manager.ts b/ui/src/core/selection_manager.ts
new file mode 100644
index 0000000..961ce95
--- /dev/null
+++ b/ui/src/core/selection_manager.ts
@@ -0,0 +1,165 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {duration, time} from '../base/time';
+import {GenericSliceDetailsTabConfigBase} from './generic_slice_details_types';
+
+export enum ProfileType {
+  HEAP_PROFILE = 'heap_profile',
+  MIXED_HEAP_PROFILE = 'heap_profile:com.android.art,libc.malloc',
+  NATIVE_HEAP_PROFILE = 'heap_profile:libc.malloc',
+  JAVA_HEAP_SAMPLES = 'heap_profile:com.android.art',
+  JAVA_HEAP_GRAPH = 'graph',
+  PERF_SAMPLE = 'perf',
+}
+
+// LEGACY Selection types:
+export interface AreaSelection {
+  kind: 'AREA';
+  areaId: string;
+  // When an area is marked it will be assigned a unique note id and saved as
+  // an AreaNote for the user to return to later. id = 0 is the special id that
+  // is overwritten when a new area is marked. Any other id is a persistent
+  // marking that will not be overwritten.
+  // When not set, the area selection will be replaced with any
+  // new area selection (i.e. not saved anywhere).
+  noteId?: string;
+}
+
+export interface NoteSelection {
+  kind: 'NOTE';
+  id: string;
+}
+
+export interface SliceSelection {
+  kind: 'SLICE';
+  id: number;
+}
+
+export interface CounterSelection {
+  kind: 'COUNTER';
+  leftTs: time;
+  rightTs: time;
+  id: number;
+}
+
+export interface HeapProfileSelection {
+  kind: 'HEAP_PROFILE';
+  id: number;
+  upid: number;
+  ts: time;
+  type: ProfileType;
+}
+
+export interface PerfSamplesSelection {
+  kind: 'PERF_SAMPLES';
+  id: number;
+  upid: number;
+  leftTs: time;
+  rightTs: time;
+  type: ProfileType;
+}
+
+export interface CpuProfileSampleSelection {
+  kind: 'CPU_PROFILE_SAMPLE';
+  id: number;
+  utid: number;
+  ts: time;
+}
+
+export interface ChromeSliceSelection {
+  kind: 'CHROME_SLICE';
+  id: number;
+  table?: string;
+}
+
+export interface ThreadStateSelection {
+  kind: 'THREAD_STATE';
+  id: number;
+}
+
+export interface LogSelection {
+  kind: 'LOG';
+  id: number;
+  trackKey: string;
+}
+
+export interface GenericSliceSelection {
+  kind: 'GENERIC_SLICE';
+  id: number;
+  sqlTableName: string;
+  start: time;
+  duration: duration;
+  // NOTE: this config can be expanded for multiple details panel types.
+  detailsPanelConfig: {kind: string; config: GenericSliceDetailsTabConfigBase};
+}
+
+export type LegacySelection = (
+  | NoteSelection
+  | SliceSelection
+  | CounterSelection
+  | HeapProfileSelection
+  | CpuProfileSampleSelection
+  | ChromeSliceSelection
+  | ThreadStateSelection
+  | AreaSelection
+  | PerfSamplesSelection
+  | LogSelection
+  | GenericSliceSelection
+) & {trackKey?: string};
+export type SelectionKind = LegacySelection['kind']; // 'THREAD_STATE' | 'SLICE' ...
+
+// New Selection types:
+export interface LegacySelectionWrapper {
+  kind: 'legacy';
+  legacySelection: LegacySelection;
+}
+
+export interface SingleSelection {
+  kind: 'single';
+  trackKey: string;
+  eventId: string;
+}
+
+export interface NewAreaSelection {
+  kind: 'area';
+  trackKey: string;
+  start: time;
+  end: time;
+}
+
+export interface UnionSelection {
+  kind: 'union';
+  selections: Selection[];
+}
+
+export interface EmptySelection {
+  kind: 'empty';
+}
+
+export type Selection =
+  | SingleSelection
+  | NewAreaSelection
+  | UnionSelection
+  | EmptySelection
+  | LegacySelectionWrapper;
+
+export function selectionToLegacySelection(
+  selection: Selection,
+): LegacySelection | null {
+  if (selection.kind === 'legacy') {
+    return selection.legacySelection;
+  }
+  return null;
+}
diff --git a/ui/src/frontend/aggregation_panel.ts b/ui/src/frontend/aggregation_panel.ts
index cbd97d9..6f7f2ce 100644
--- a/ui/src/frontend/aggregation_panel.ts
+++ b/ui/src/frontend/aggregation_panel.ts
@@ -15,6 +15,7 @@
 import m from 'mithril';
 
 import {Actions} from '../common/actions';
+import {getLegacySelection} from '../common/state';
 import {
   AggregateData,
   Column,
@@ -39,7 +40,7 @@
   implements m.ClassComponent<AggregationPanelAttrs>
 {
   view({attrs}: m.CVnode<AggregationPanelAttrs>) {
-    if (!globals.state.currentSelection) {
+    if (!getLegacySelection(globals.state)) {
       return m(
         EmptyState,
         {
@@ -156,7 +157,7 @@
   }
 
   showTimeRange() {
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (selection === null || selection.kind !== 'AREA') return undefined;
     const selectedArea = globals.state.areas[selection.areaId];
     const duration = selectedArea.end - selectedArea.start;
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index f9adf10..c6c46f6 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -23,6 +23,7 @@
 import {EmptyState} from '../widgets/empty_state';
 import {FlowEventsAreaSelectedPanel} from './flow_events_panel';
 import {PivotTable} from './pivot_table';
+import {FlamegraphDetailsPanel} from './flamegraph_panel';
 
 interface View {
   key: string;
@@ -54,6 +55,14 @@
   private getViews(): View[] {
     const views = [];
 
+    if (globals.flamegraphDetails.isInAreaSelection) {
+      views.push({
+        key: 'flamegraph_selection',
+        name: 'Flamegraph Selection',
+        content: m(FlamegraphDetailsPanel, {key: 'flamegraph'}),
+      });
+    }
+
     for (const [key, value] of globals.aggregateDataStore.entries()) {
       if (!isEmptyData(value)) {
         views.push({
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 0c63e9e..17b0ad9 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -22,6 +22,7 @@
 import {undoCommonChatAppReplacements} from '../base/string_utils';
 import {duration, Span, Time, time, TimeSpan} from '../base/time';
 import {Actions} from '../common/actions';
+import {getLegacySelection} from '../common/state';
 import {runQuery} from '../common/queries';
 import {
   DurationPrecision,
@@ -237,7 +238,7 @@
   }
 
   private getFirstUtidOfSelectionOrVisibleWindow(): number {
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (selection && selection.kind === 'AREA') {
       const selectedArea = globals.state.areas[selection.areaId];
       const firstThreadStateTrack = selectedArea.tracks.find((trackId) => {
@@ -562,7 +563,7 @@
       id: 'perfetto.MarkArea',
       name: 'Mark area',
       callback: () => {
-        const selection = globals.state.currentSelection;
+        const selection = getLegacySelection(globals.state);
         if (selection && selection.kind === 'AREA') {
           globals.dispatch(Actions.toggleMarkCurrentArea({persistent: false}));
         } else if (selection) {
@@ -575,7 +576,7 @@
       id: 'perfetto.MarkAreaPersistent',
       name: 'Mark area (persistent)',
       callback: () => {
-        const selection = globals.state.currentSelection;
+        const selection = getLegacySelection(globals.state);
         if (selection && selection.kind === 'AREA') {
           globals.dispatch(Actions.toggleMarkCurrentArea({persistent: true}));
         } else if (selection) {
@@ -588,25 +589,25 @@
       id: 'perfetto.NextFlow',
       name: 'Next flow',
       callback: () => focusOtherFlow('Forward'),
-      defaultHotkey: ']',
+      defaultHotkey: 'Mod+]',
     },
     {
       id: 'perfetto.PrevFlow',
       name: 'Prev flow',
       callback: () => focusOtherFlow('Backward'),
-      defaultHotkey: '[',
+      defaultHotkey: 'Mod+[',
     },
     {
       id: 'perfetto.MoveNextFlow',
       name: 'Move next flow',
       callback: () => moveByFocusedFlow('Forward'),
-      defaultHotkey: 'Mod+]',
+      defaultHotkey: ']',
     },
     {
       id: 'perfetto.MovePrevFlow',
       name: 'Move prev flow',
       callback: () => moveByFocusedFlow('Backward'),
-      defaultHotkey: 'Mod+[',
+      defaultHotkey: '[',
     },
     {
       id: 'perfetto.SelectAll',
@@ -614,7 +615,7 @@
       callback: () => {
         let tracksToSelect: string[] = [];
 
-        const selection = globals.state.currentSelection;
+        const selection = getLegacySelection(globals.state);
         if (selection !== null && selection.kind === 'AREA') {
           const area = globals.state.areas[selection.areaId];
           const coversEntireTimeRange =
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index e4203f0..ef3b60b 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -25,7 +25,11 @@
 } from '../common/canvas_utils';
 import {colorCompare} from '../core/color';
 import {UNEXPECTED_PINK} from '../core/colorizer';
-import {Selection, SelectionKind} from '../common/state';
+import {
+  LegacySelection,
+  SelectionKind,
+  getLegacySelection,
+} from '../common/state';
 import {featureFlags} from '../core/feature_flags';
 import {raf} from '../core/raf_scheduler';
 import {EngineProxy, Slice, SliceRect, Track} from '../public';
@@ -300,7 +304,7 @@
     }
   }
 
-  protected isSelectionHandled(selection: Selection): boolean {
+  protected isSelectionHandled(selection: LegacySelection): boolean {
     // TODO(hjd): Remove when updating selection.
     // We shouldn't know here about CHROME_SLICE. Maybe should be set by
     // whatever deals with that. Dunno the namespace of selection is weird. For
@@ -360,7 +364,7 @@
       vizTime.end.toTime('ceil'),
     );
 
-    let selection = globals.state.currentSelection;
+    let selection = getLegacySelection(globals.state);
     if (!selection || !this.isSelectionHandled(selection)) {
       selection = null;
     }
diff --git a/ui/src/frontend/bottom_tab.ts b/ui/src/frontend/bottom_tab.ts
index 4b82001..d8ed117 100644
--- a/ui/src/frontend/bottom_tab.ts
+++ b/ui/src/frontend/bottom_tab.ts
@@ -13,18 +13,9 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {v4 as uuidv4} from 'uuid';
 
-import {stringifyJsonWithBigints} from '../base/json_utils';
-import {exists} from '../base/utils';
-import {Actions} from '../common/actions';
-import {traceEvent} from '../common/metatracing';
-import {Registry} from '../base/registry';
-import {raf} from '../core/raf_scheduler';
 import {EngineProxy} from '../trace_processor/engine';
 
-import {globals} from './globals';
-
 export interface NewBottomTabArgs<Config> {
   engine: EngineProxy;
   tag?: string;
@@ -32,22 +23,6 @@
   config: Config;
 }
 
-// Interface for allowing registration and creation of bottom tabs.
-// See comments on |TrackCreator| for more details.
-export interface BottomTabCreator {
-  readonly kind: string;
-
-  create(args: NewBottomTabArgs<unknown>): BottomTab;
-}
-
-export const bottomTabRegistry = Registry.kindRegistry<BottomTabCreator>();
-
-// Period to wait for the newly-added tabs which are loading before showing
-// them to the user. This period is short enough to not be user-visible,
-// while being long enough for most of the simple queries to complete, reducing
-// flickering in the UI.
-const NEW_LOADING_TAB_DELAY_MS = 50;
-
 // An interface representing a bottom tab displayed on the panel in the bottom
 // of the ui (e.g. "Current Selection").
 //
@@ -115,10 +90,6 @@
 
   abstract viewTab(): m.Children;
 
-  close(): void {
-    closeTab(this.uuid);
-  }
-
   renderPanel(): m.Children {
     return m(BottomTabAdapter, {
       key: this.uuid,
@@ -136,222 +107,3 @@
     return vnode.attrs.panel.viewTab();
   }
 }
-
-export type AddTabArgs = {
-  kind: string;
-  config: {};
-  tag?: string;
-  // Whether to make the new tab current. True by default.
-  select?: boolean;
-};
-
-export type AddTabResult = {
-  uuid: string;
-};
-
-// Shorthand for globals.bottomTabList.addTab(...) & redraw.
-// Ignored when bottomTabList does not exist (e.g. no trace is open in the UI).
-export function addTab(args: AddTabArgs) {
-  const tabList = globals.bottomTabList;
-  if (!tabList) {
-    return;
-  }
-  tabList.addTab(args);
-  raf.scheduleFullRedraw();
-}
-
-// Shorthand for globals.bottomTabList.closeTabById(...) & redraw.
-// Ignored when bottomTabList does not exist (e.g. no trace is open in the UI).
-export function closeTab(uuid: string) {
-  const tabList = globals.bottomTabList;
-  if (!tabList) {
-    return;
-  }
-  tabList.closeTabById(uuid);
-  raf.scheduleFullRedraw();
-}
-
-interface PendingTab {
-  tab: BottomTabBase;
-  args: AddTabArgs;
-  startTime: number;
-}
-
-function tabSelectionKey(tab: BottomTabBase) {
-  return tab.tag ?? tab.uuid;
-}
-
-export class BottomTabList {
-  private tabs: BottomTabBase[] = [];
-  private pendingTabs: PendingTab[] = [];
-  private engine: EngineProxy;
-  private scheduledFlushSetTimeoutId?: number;
-
-  constructor(engine: EngineProxy) {
-    this.engine = engine;
-  }
-
-  getTabs(): BottomTabBase[] {
-    this.flushPendingTabs();
-    return this.tabs;
-  }
-
-  // Add and create a new panel with given kind and config, replacing an
-  // existing panel with the same tag if needed. Returns the uuid of a newly
-  // created panel (which can be used in the future to close it).
-  addTab(args: AddTabArgs): AddTabResult {
-    const uuid = uuidv4();
-    return traceEvent(
-      'addTab',
-      () => {
-        const newPanel = bottomTabRegistry.get(args.kind).create({
-          engine: this.engine,
-          uuid,
-          config: args.config,
-          tag: args.tag,
-        });
-
-        this.pendingTabs.push({
-          tab: newPanel,
-          args,
-          startTime: window.performance.now(),
-        });
-        this.flushPendingTabs();
-
-        return {
-          uuid,
-        };
-      },
-      {
-        args: {
-          uuid: uuid,
-          kind: args.kind,
-          tag: args.tag ?? '<undefined>',
-          config: stringifyJsonWithBigints(args.config),
-        },
-      },
-    );
-  }
-
-  closeTabByTag(tag: string) {
-    const index = this.tabs.findIndex((tab) => tab.tag === tag);
-    if (index !== -1) {
-      this.removeTabAtIndex(index);
-    }
-    // User closing a tab by tag should affect pending tabs as well, as these
-    // tabs were requested to be added to the tab list before this call.
-    this.pendingTabs = this.pendingTabs.filter(({tab}) => tab.tag !== tag);
-  }
-
-  closeTabById(uuid: string) {
-    const index = this.tabs.findIndex((tab) => tab.uuid === uuid);
-    if (index !== -1) {
-      this.removeTabAtIndex(index);
-    }
-    // User closing a tab by id should affect pending tabs as well, as these
-    // tabs were requested to be added to the tab list before this call.
-    this.pendingTabs = this.pendingTabs.filter(({tab}) => tab.uuid !== uuid);
-  }
-
-  private removeTabAtIndex(index: number) {
-    const tab = this.tabs[index];
-    this.tabs.splice(index, 1);
-    // If the current tab was closed, select the tab to the right of it.
-    // If the closed tab was current and last in the tab list, select the tab
-    // that became last.
-    if (tab.uuid === globals.state.currentTab && this.tabs.length > 0) {
-      const newActiveIndex = index === this.tabs.length ? index - 1 : index;
-      globals.dispatch(
-        Actions.setCurrentTab({
-          tab: tabSelectionKey(this.tabs[newActiveIndex]),
-        }),
-      );
-    }
-    raf.scheduleFullRedraw();
-  }
-
-  // Check the list of the pending tabs and add the ones that are ready
-  // (either tab.isLoading returns false or NEW_LOADING_TAB_DELAY_MS ms elapsed
-  // since this tab was added).
-  // Note: the pending tabs are stored in a queue to preserve the action order,
-  // which matters for cases like adding tabs with the same tag.
-  private flushPendingTabs() {
-    const currentTime = window.performance.now();
-    while (this.pendingTabs.length > 0) {
-      const {tab, args, startTime} = this.pendingTabs[0];
-
-      // This is a dirty hack^W^W low-lift solution for the world where some
-      // "current selection" panels are implemented by BottomTabs and some by
-      // details_panel.ts computing vnodes dynamically. Naive implementation
-      // will: a) stop showing the old panel (because
-      // globals.state.currentSelection changes). b) not showing the new
-      // 'current_selection' tab yet. This will result in temporary shifting
-      // focus to another tab (as no tab with 'current_selection' tag will
-      // exist).
-      //
-      // To counteract this, short-circuit this logic and when:
-      // a) no tag with 'current_selection' tag exists in the list of currently
-      // displayed tabs and b) we are adding a tab with 'current_selection' tag.
-      // add it immediately without waiting.
-      // TODO(altimin): Remove this once all places have switched to be using
-      // BottomTab to display panels.
-      const currentSelectionTabAlreadyExists =
-        this.tabs.filter((tab) => tab.tag === 'current_selection').length > 0;
-      const dirtyHackForCurrentSelectionApplies =
-        tab.tag === 'current_selection' && !currentSelectionTabAlreadyExists;
-
-      const elapsedTimeMs = currentTime - startTime;
-      if (
-        tab.isLoading() &&
-        elapsedTimeMs < NEW_LOADING_TAB_DELAY_MS &&
-        !dirtyHackForCurrentSelectionApplies
-      ) {
-        this.schedulePendingTabsFlush(NEW_LOADING_TAB_DELAY_MS - elapsedTimeMs);
-        // The first tab is not ready yet, wait.
-        return;
-      }
-
-      traceEvent(
-        'addPendingTab',
-        () => {
-          this.pendingTabs.shift();
-
-          const index = args.tag
-            ? this.tabs.findIndex((tab) => tab.tag === args.tag)
-            : -1;
-          if (index === -1) {
-            this.tabs.push(tab);
-          } else {
-            this.tabs[index] = tab;
-          }
-
-          if (args.select === undefined || args.select === true) {
-            globals.dispatch(
-              Actions.setCurrentTab({tab: tabSelectionKey(tab)}),
-            );
-          }
-          // setCurrentTab will usually schedule a redraw, but not if we replace
-          // the tab with the same tag, so we force an update here.
-          raf.scheduleFullRedraw();
-        },
-        {
-          args: {
-            uuid: tab.uuid,
-            is_loading: tab.isLoading().toString(),
-          },
-        },
-      );
-    }
-  }
-
-  private schedulePendingTabsFlush(waitTimeMs: number) {
-    if (exists(this.scheduledFlushSetTimeoutId)) {
-      // The flush is already pending, no action is required.
-      return;
-    }
-    setTimeout(() => {
-      this.scheduledFlushSetTimeoutId = undefined;
-      this.flushPendingTabs();
-    }, waitTimeMs);
-  }
-}
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 85d94b5..90471ed 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -28,7 +28,7 @@
 import {Section} from '../widgets/section';
 import {Tree, TreeNode} from '../widgets/tree';
 
-import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from './bottom_tab';
+import {BottomTab, NewBottomTabArgs} from './bottom_tab';
 import {FlowPoint, globals} from './globals';
 import {hasArgs, renderArguments} from './slice_args';
 import {renderDetails} from './slice_details';
@@ -416,5 +416,3 @@
     }
   }
 }
-
-bottomTabRegistry.register(ChromeSliceDetailsTab);
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
deleted file mode 100644
index a93418d..0000000
--- a/ui/src/frontend/details_panel.ts
+++ /dev/null
@@ -1,298 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {Gate} from '../base/mithril_utils';
-import {exists} from '../base/utils';
-import {Actions} from '../common/actions';
-import {isEmptyData} from '../common/aggregation_data';
-import {LogExists, LogExistsKey} from '../common/logs';
-import {addSelectionChangeObserver} from '../common/selection_observer';
-import {Selection} from '../common/state';
-
-import {AggregationPanel} from './aggregation_panel';
-import {ChromeSliceDetailsTab} from './chrome_slice_details_tab';
-import {CounterDetailsPanel} from './counter_panel';
-import {CpuProfileDetailsPanel} from './cpu_profile_panel';
-import {DragHandle, getDefaultDetailsHeight} from './drag_handle';
-import {FlamegraphDetailsPanel} from './flamegraph_panel';
-import {
-  FlowEventsAreaSelectedPanel,
-  FlowEventsPanel,
-} from './flow_events_panel';
-import {FtracePanel} from './ftrace_panel';
-import {globals} from './globals';
-import {LogPanel} from './logs_panel';
-import {NotesEditorTab} from './notes_panel';
-import {PivotTable} from './pivot_table';
-import {SliceDetailsPanel} from './slice_details_panel';
-import {ThreadStateTab} from './thread_state_tab';
-
-export const CURRENT_SELECTION_TAG = 'current_selection';
-
-function hasLogs(): boolean {
-  const data = globals.trackDataStore.get(LogExistsKey) as
-    | LogExists
-    | undefined;
-  return Boolean(data?.exists);
-}
-
-function handleSelectionChange(
-  newSelection: Selection | undefined,
-  openCurrentSelectionTab: boolean,
-): void {
-  const currentSelectionTag = CURRENT_SELECTION_TAG;
-  const bottomTabList = globals.bottomTabList;
-  if (!bottomTabList) return;
-  if (newSelection === undefined) {
-    bottomTabList.closeTabByTag(currentSelectionTag);
-    return;
-  }
-  switch (newSelection.kind) {
-    case 'NOTE':
-      bottomTabList.addTab({
-        kind: NotesEditorTab.kind,
-        tag: currentSelectionTag,
-        config: {
-          id: newSelection.id,
-        },
-        select: openCurrentSelectionTab,
-      });
-      break;
-    case 'AREA':
-      if (newSelection.noteId !== undefined) {
-        bottomTabList.addTab({
-          kind: NotesEditorTab.kind,
-          tag: currentSelectionTag,
-          config: {
-            id: newSelection.noteId,
-          },
-          select: openCurrentSelectionTab,
-        });
-      }
-      break;
-    case 'THREAD_STATE':
-      bottomTabList.addTab({
-        kind: ThreadStateTab.kind,
-        tag: currentSelectionTag,
-        config: {
-          id: newSelection.id,
-        },
-        select: openCurrentSelectionTab,
-      });
-      break;
-    case 'GENERIC_SLICE':
-      bottomTabList.addTab({
-        kind: newSelection.detailsPanelConfig.kind,
-        tag: currentSelectionTag,
-        config: newSelection.detailsPanelConfig.config,
-        select: openCurrentSelectionTab,
-      });
-      break;
-    case 'CHROME_SLICE':
-      bottomTabList.addTab({
-        kind: ChromeSliceDetailsTab.kind,
-        tag: currentSelectionTag,
-        config: {
-          id: newSelection.id,
-          table: newSelection.table,
-        },
-        select: openCurrentSelectionTab,
-      });
-      break;
-    default:
-      bottomTabList.closeTabByTag(currentSelectionTag);
-  }
-}
-addSelectionChangeObserver(handleSelectionChange);
-
-export class DetailsPanel implements m.ClassComponent {
-  private detailsHeight = getDefaultDetailsHeight();
-
-  view() {
-    interface DetailsPanel {
-      key: string;
-      name: string;
-      vnode: m.Children;
-    }
-
-    const detailsPanels: DetailsPanel[] = [];
-
-    if (globals.bottomTabList) {
-      for (const tab of globals.bottomTabList.getTabs()) {
-        detailsPanels.push({
-          key: tab.tag ?? tab.uuid,
-          name: tab.getTitle(),
-          vnode: tab.renderPanel(),
-        });
-      }
-    }
-
-    const curSelection = globals.state.currentSelection;
-    if (curSelection) {
-      switch (curSelection.kind) {
-        case 'NOTE':
-          // Handled in handleSelectionChange.
-          break;
-        case 'AREA':
-          if (globals.flamegraphDetails.isInAreaSelection) {
-            detailsPanels.push({
-              key: 'flamegraph_selection',
-              name: 'Flamegraph Selection',
-              vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'}),
-            });
-          }
-          break;
-        case 'SLICE':
-          detailsPanels.push({
-            key: 'current_selection',
-            name: 'Current Selection',
-            vnode: m(SliceDetailsPanel, {
-              key: 'slice',
-            }),
-          });
-          break;
-        case 'COUNTER':
-          detailsPanels.push({
-            key: 'current_selection',
-            name: 'Current Selection',
-            vnode: m(CounterDetailsPanel, {
-              key: 'counter',
-            }),
-          });
-          break;
-        case 'PERF_SAMPLES':
-        case 'HEAP_PROFILE':
-          detailsPanels.push({
-            key: 'current_selection',
-            name: 'Current Selection',
-            vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'}),
-          });
-          break;
-        case 'CPU_PROFILE_SAMPLE':
-          detailsPanels.push({
-            key: 'current_selection',
-            name: 'Current Selection',
-            vnode: m(CpuProfileDetailsPanel, {
-              key: 'cpu_profile_sample',
-            }),
-          });
-          break;
-        default:
-          break;
-      }
-    }
-    if (hasLogs()) {
-      detailsPanels.push({
-        key: 'android_logs',
-        name: 'Android Logs',
-        vnode: m(LogPanel, {key: 'logs_panel'}),
-      });
-    }
-
-    const trackGroup = globals.state.trackGroups['ftrace-track-group'];
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (trackGroup) {
-      const {collapsed} = trackGroup;
-      if (!collapsed) {
-        detailsPanels.push({
-          key: 'ftrace_events',
-          name: 'Ftrace Events',
-          vnode: m(FtracePanel, {key: 'ftrace_panel'}),
-        });
-      }
-    }
-
-    if (
-      globals.state.nonSerializableState.pivotTable.selectionArea !== undefined
-    ) {
-      detailsPanels.push({
-        key: 'pivot_table',
-        name: 'Pivot Table',
-        vnode: m(PivotTable, {
-          key: 'pivot_table',
-          selectionArea:
-            globals.state.nonSerializableState.pivotTable.selectionArea,
-        }),
-      });
-    }
-
-    if (globals.connectedFlows.length > 0) {
-      detailsPanels.push({
-        key: 'bound_flows',
-        name: 'Flow Events',
-        vnode: m(FlowEventsPanel, {key: 'flow_events'}),
-      });
-    }
-
-    for (const [key, value] of globals.aggregateDataStore.entries()) {
-      if (!isEmptyData(value)) {
-        detailsPanels.push({
-          key: value.tabName,
-          name: value.tabName,
-          vnode: m(AggregationPanel, {kind: key, key, data: value}),
-        });
-      }
-    }
-
-    // Add this after all aggregation panels, to make it appear after 'Slices'
-    if (globals.selectedFlows.length > 0) {
-      detailsPanels.push({
-        key: 'selected_flows',
-        name: 'Flow Events',
-        vnode: m(FlowEventsAreaSelectedPanel, {key: 'flow_events_area'}),
-      });
-    }
-
-    let currentTabDetails = detailsPanels.find(
-      (tab) => tab.key === globals.state.currentTab,
-    );
-    if (currentTabDetails === undefined && detailsPanels.length > 0) {
-      currentTabDetails = detailsPanels[0];
-    }
-
-    const panel = currentTabDetails?.vnode;
-
-    if (!exists(panel)) {
-      return null;
-    }
-
-    return [
-      m(DragHandle, {
-        resize: (height: number) => {
-          this.detailsHeight = Math.max(height, 0);
-        },
-        height: this.detailsHeight,
-        tabs: detailsPanels.map((tab) => {
-          return {key: tab.key, title: tab.name};
-        }),
-        currentTabKey: currentTabDetails?.key,
-        onTabClick: (key) => {
-          globals.dispatch(Actions.setCurrentTab({tab: key}));
-        },
-      }),
-      m(
-        '.details-panel-container',
-        {
-          style: {height: `${this.detailsHeight}px`},
-        },
-        detailsPanels.map((tab) => {
-          const active = tab === currentTabDetails;
-          return m(Gate, {open: active}, tab.vnode);
-        }),
-      ),
-    ];
-  }
-}
diff --git a/ui/src/frontend/drag_handle.ts b/ui/src/frontend/drag_handle.ts
index cfe7ceb..70aea03 100644
--- a/ui/src/frontend/drag_handle.ts
+++ b/ui/src/frontend/drag_handle.ts
@@ -21,6 +21,7 @@
 
 import {DEFAULT_DETAILS_CONTENT_HEIGHT} from './css_constants';
 import {DragGestureHandler} from './drag_gesture_handler';
+import {globals} from './globals';
 
 const DRAG_HANDLE_HEIGHT_PX = 28;
 const UP_ICON = 'keyboard_arrow_up';
@@ -102,7 +103,7 @@
   // We can't get real fullscreen height until the pan_and_zoom_handler
   // exists.
   private fullscreenHeight = 0;
-  private trash: Trash = new Trash();
+  private trash = new Trash();
 
   oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) {
     this.resize = attrs.resize;
@@ -118,6 +119,31 @@
         this.onDragEnd.bind(this),
       ),
     );
+    const cmd = globals.commandManager.registerCommand({
+      id: 'perfetto.ToggleDrawer',
+      name: 'Toggle drawer',
+      defaultHotkey: 'Q',
+      callback: () => {
+        this.toggleVisibility();
+      },
+    });
+    this.trash.add(cmd);
+  }
+
+  private toggleVisibility() {
+    if (this.height === 0) {
+      this.isClosed = false;
+      if (this.previousHeight === 0) {
+        this.previousHeight = getDefaultDetailsHeight();
+      }
+      this.resize(this.previousHeight);
+    } else {
+      this.isFullscreen = false;
+      this.isClosed = true;
+      this.previousHeight = this.height;
+      this.resize(0);
+    }
+    raf.scheduleFullRedraw();
   }
 
   onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) {
@@ -216,19 +242,7 @@
         }),
         m(Button, {
           onclick: () => {
-            if (this.height === 0) {
-              this.isClosed = false;
-              if (this.previousHeight === 0) {
-                this.previousHeight = getDefaultDetailsHeight();
-              }
-              this.resize(this.previousHeight);
-            } else {
-              this.isFullscreen = false;
-              this.isClosed = true;
-              this.previousHeight = this.height;
-              this.resize(0);
-            }
-            raf.scheduleFullRedraw();
+            this.toggleVisibility();
           },
           title,
           icon,
diff --git a/ui/src/frontend/flow_events_panel.ts b/ui/src/frontend/flow_events_panel.ts
index ee34b88..02bf691 100644
--- a/ui/src/frontend/flow_events_panel.ts
+++ b/ui/src/frontend/flow_events_panel.ts
@@ -16,6 +16,7 @@
 
 import {Icons} from '../base/semantic_icons';
 import {Actions} from '../common/actions';
+import {getLegacySelection} from '../common/state';
 import {raf} from '../core/raf_scheduler';
 
 import {Flow, globals} from './globals';
@@ -39,7 +40,7 @@
 
 export class FlowEventsPanel implements m.ClassComponent {
   view() {
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (!selection) {
       return m(
         EmptyState,
@@ -145,7 +146,7 @@
 
 export class FlowEventsAreaSelectedPanel implements m.ClassComponent {
   view() {
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (!selection || selection.kind !== 'AREA') {
       return;
     }
diff --git a/ui/src/frontend/generic_slice_details_tab.ts b/ui/src/frontend/generic_slice_details_tab.ts
index b6d72f5..fdc5bb8 100644
--- a/ui/src/frontend/generic_slice_details_tab.ts
+++ b/ui/src/frontend/generic_slice_details_tab.ts
@@ -22,27 +22,17 @@
 import {SqlRef} from '../widgets/sql_ref';
 import {dictToTree, Tree, TreeNode} from '../widgets/tree';
 
-import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from './bottom_tab';
+import {BottomTab, NewBottomTabArgs} from './bottom_tab';
 import {sqlValueToString} from './sql_utils';
 
-export interface ColumnConfig {
-  displayName?: string;
-}
+import {GenericSliceDetailsTabConfig} from '../core/generic_slice_details_types';
 
-export type Columns = {
-  [columnName: string]: ColumnConfig;
-};
-
-export interface GenericSliceDetailsTabConfigBase {
-  sqlTableName: string;
-  title: string;
-  // All columns are rendered if |columns| is undefined.
-  columns?: Columns;
-}
-
-export type GenericSliceDetailsTabConfig = GenericSliceDetailsTabConfigBase & {
-  id: number;
-};
+export {
+  ColumnConfig,
+  Columns,
+  GenericSliceDetailsTabConfigBase,
+  GenericSliceDetailsTabConfig,
+} from '../core/generic_slice_details_types';
 
 // A details tab, which fetches slice-like object from a given SQL table by id
 // and renders it according to the provided config, specifying which columns
@@ -126,5 +116,3 @@
     return this.data === undefined;
   }
 }
-
-bottomTabRegistry.register(GenericSliceDetailsTab);
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 35cd660..b0d0982 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -14,7 +14,6 @@
 
 import {BigintMath} from '../base/bigint_math';
 import {assertExists} from '../base/logging';
-import {createStore, Store} from '../base/store';
 import {duration, Span, Time, time, TimeSpan} from '../base/time';
 import {Actions, DeferredAction} from '../common/actions';
 import {AggregateData} from '../common/aggregation_data';
@@ -31,30 +30,29 @@
 } from '../common/high_precision_time';
 import {MetricResult} from '../common/metric_data';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
-import {onSelectionChanged} from '../common/selection_observer';
 import {
   CallsiteInfo,
   EngineConfig,
   ProfileType,
   RESOLUTION_DEFAULT,
   State,
+  getLegacySelection,
 } from '../common/state';
 import {TabManager} from '../common/tab_registry';
 import {TimestampFormat, timestampFormat} from '../core/timestamp_format';
 import {TrackManager} from '../common/track_cache';
-import {TABS_V2_FLAG} from '../core/feature_flags';
 import {setPerfHooks} from '../core/perf';
 import {raf} from '../core/raf_scheduler';
 import {Engine} from '../trace_processor/engine';
 import {HttpRpcState} from '../trace_processor/http_rpc_engine';
 
 import {Analytics, initAnalytics} from './analytics';
-import {BottomTabList} from './bottom_tab';
 import {Timeline} from './frontend_local_state';
 import {Router} from './router';
 import {horizontalScrollToTs} from './scroll_helper';
 import {ServiceWorkerController} from './service_worker_controller';
 import {SliceSqlId} from './sql_types';
+import {createStore, Store} from '../base/store';
 import {PxSpan, TimeScale} from './time_scale';
 
 const INSTANT_FOCUS_DURATION = 1n;
@@ -238,8 +236,6 @@
 class Globals {
   readonly root = getRoot();
 
-  bottomTabList?: BottomTabList = undefined;
-
   private _testing = false;
   private _dispatch?: Dispatch = undefined;
   private _store = createStore(createEmptyState());
@@ -622,37 +618,15 @@
   makeSelection(action: DeferredAction<{}>, opts: MakeSelectionOpts = {}) {
     const {switchToCurrentSelectionTab = true, clearSearch = true} = opts;
 
-    const previousState = this.state;
-
     const currentSelectionTabUri = 'current_selection';
 
     // A new selection should cancel the current search selection.
     clearSearch && globals.dispatch(Actions.setSearchIndex({index: -1}));
 
-    if (TABS_V2_FLAG.get()) {
-      if (action.type !== 'deselect' && switchToCurrentSelectionTab) {
-        globals.dispatch(Actions.showTab({uri: currentSelectionTabUri}));
-      }
-    } else {
-      if (action.type === 'deselect') {
-        globals.dispatch(Actions.setCurrentTab({tab: undefined}));
-      } else if (switchToCurrentSelectionTab) {
-        globals.dispatch(Actions.setCurrentTab({tab: currentSelectionTabUri}));
-      }
+    if (action.type !== 'deselect' && switchToCurrentSelectionTab) {
+      globals.dispatch(Actions.showTab({uri: currentSelectionTabUri}));
     }
     globals.dispatch(action);
-
-    // HACK(stevegolton + altimin): This is a workaround to allow passing the
-    // next tab state to the Bottom Tab API
-    if (this.state.currentSelection !== previousState.currentSelection) {
-      // TODO(altimin): Currently we are not triggering this when changing
-      // the set of selected tracks via toggling per-track checkboxes.
-      // Fix that.
-      onSelectionChanged(
-        this.state.currentSelection ?? undefined,
-        switchToCurrentSelectionTab,
-      );
-    }
   }
 
   resetForTesting() {
@@ -809,7 +783,7 @@
   }
 
   findTimeRangeOfSelection(): {start: time; end: time} {
-    const selection = this.state.currentSelection;
+    const selection = getLegacySelection(this.state);
     let start = Time.INVALID;
     let end = Time.INVALID;
     if (selection === null) {
diff --git a/ui/src/frontend/help_modal.ts b/ui/src/frontend/help_modal.ts
index 9d6f3e6..23f1c32 100644
--- a/ui/src/frontend/help_modal.ts
+++ b/ui/src/frontend/help_modal.ts
@@ -252,6 +252,7 @@
           m('td', keycap(ctrlOrCmd), ' + ', keycap('s')),
           m('td', 'Search'),
         ),
+        m('tr', m('td', keycap('q')), m('td', 'Toggle tab drawer')),
         ...sidebarInstructions,
         m('tr', m('td', keycap('?')), m('td', 'Show help')),
       ),
diff --git a/ui/src/frontend/keyboard_event_handler.ts b/ui/src/frontend/keyboard_event_handler.ts
index e73a5d1..60b7c8f 100644
--- a/ui/src/frontend/keyboard_event_handler.ts
+++ b/ui/src/frontend/keyboard_event_handler.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {Actions} from '../common/actions';
-import {Area} from '../common/state';
+import {Area, getLegacySelection} from '../common/state';
 
 import {Flow, globals} from './globals';
 import {focusHorizontalRange, verticalScrollToTrack} from './scroll_helper';
@@ -44,13 +44,11 @@
 
 // Change focus to the next flow event (matching the direction)
 export function focusOtherFlow(direction: Direction) {
-  if (
-    !globals.state.currentSelection ||
-    globals.state.currentSelection.kind !== 'CHROME_SLICE'
-  ) {
+  const currentSelection = getLegacySelection(globals.state);
+  if (!currentSelection || currentSelection.kind !== 'CHROME_SLICE') {
     return;
   }
-  const sliceId = globals.state.currentSelection.id;
+  const sliceId = currentSelection.id;
   if (sliceId === -1) {
     return;
   }
@@ -78,14 +76,12 @@
 
 // Select the slice connected to the flow in focus
 export function moveByFocusedFlow(direction: Direction): void {
-  if (
-    !globals.state.currentSelection ||
-    globals.state.currentSelection.kind !== 'CHROME_SLICE'
-  ) {
+  const currentSelection = getLegacySelection(globals.state);
+  if (!currentSelection || currentSelection.kind !== 'CHROME_SLICE') {
     return;
   }
 
-  const sliceId = globals.state.currentSelection.id;
+  const sliceId = currentSelection.id;
   const flowId =
     direction === 'Backward'
       ? globals.state.focusedFlowIdLeft
@@ -117,21 +113,16 @@
 
 export function lockSliceSpan(persistent = false) {
   const range = globals.findTimeRangeOfSelection();
-  if (
-    range.start !== -1n &&
-    range.end !== -1n &&
-    globals.state.currentSelection !== null
-  ) {
-    const tracks = globals.state.currentSelection.trackKey
-      ? [globals.state.currentSelection.trackKey]
-      : [];
+  const currentSelection = getLegacySelection(globals.state);
+  if (range.start !== -1n && range.end !== -1n && currentSelection !== null) {
+    const tracks = currentSelection.trackKey ? [currentSelection.trackKey] : [];
     const area: Area = {start: range.start, end: range.end, tracks};
     globals.dispatch(Actions.markArea({area, persistent}));
   }
 }
 
 export function findCurrentSelection() {
-  const selection = globals.state.currentSelection;
+  const selection = getLegacySelection(globals.state);
   if (selection === null) return;
 
   const range = globals.findTimeRangeOfSelection();
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index dfc7800..69362e4 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -19,11 +19,11 @@
 import {Time} from '../base/time';
 import {Actions} from '../common/actions';
 import {randomColor} from '../core/colorizer';
-import {AreaNote, Note} from '../common/state';
+import {AreaNote, Note, getLegacySelection} from '../common/state';
 import {raf} from '../core/raf_scheduler';
 import {Button} from '../widgets/button';
 
-import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from './bottom_tab';
+import {BottomTab, NewBottomTabArgs} from './bottom_tab';
 import {TRACK_SHELL_WIDTH} from './css_constants';
 import {globals} from './globals';
 import {
@@ -177,7 +177,7 @@
         this.hoveredX !== null && this.mouseOverNote(this.hoveredX, note);
       if (currentIsHovered) aNoteIsHovered = true;
 
-      const selection = globals.state.currentSelection;
+      const selection = getLegacySelection(globals.state);
       const isSelected =
         selection !== null &&
         ((selection.kind === 'NOTE' && selection.id === note.id) ||
@@ -404,7 +404,6 @@
           minimal: true,
           onclick: () => {
             globals.dispatch(Actions.removeNote({id: this.config.id}));
-            globals.dispatch(Actions.setCurrentTab({tab: undefined}));
             raf.scheduleFullRedraw();
           },
         }),
@@ -412,5 +411,3 @@
     );
   }
 }
-
-bottomTabRegistry.register(NotesEditorTab);
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index ee9f55e..bf8b3a5 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -14,7 +14,7 @@
 
 import {time} from '../base/time';
 import {Actions} from '../common/actions';
-import {AggregateData, isEmptyData} from '../common/aggregation_data';
+import {AggregateData} from '../common/aggregation_data';
 import {ConversionJobStatusUpdate} from '../common/conversion_jobs';
 import {
   LogBoundsKey,
@@ -26,6 +26,7 @@
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
 import {raf} from '../core/raf_scheduler';
 import {HttpRpcState} from '../trace_processor/http_rpc_engine';
+import {getLegacySelection} from '../common/state';
 
 import {
   CounterDetails,
@@ -169,15 +170,11 @@
   kind: string;
 }) {
   globals.setAggregateData(args.kind, args.data);
-  if (!isEmptyData(args.data)) {
-    globals.dispatch(Actions.setCurrentTab({tab: args.data.tabName}));
-  }
   globals.publishRedraw();
 }
 
 export function publishQueryResult(args: {id: string; data?: {}}) {
   globals.queryResults.set(args.id, args.data);
-  globals.dispatch(Actions.setCurrentTab({tab: `query_result_${args.id}`}));
   globals.publishRedraw();
 }
 
@@ -194,7 +191,6 @@
   const id = click.id;
   if (id !== undefined && id === globals.state.pendingScrollId) {
     findCurrentSelection();
-    globals.dispatch(Actions.setCurrentTab({tab: 'slice'}));
     globals.dispatch(Actions.clearPendingScrollId({id: undefined}));
   }
   globals.publishRedraw();
@@ -212,8 +208,9 @@
   // focus. In all other cases the focusedFlowId(Left|Right) will be set to -1.
   globals.dispatch(Actions.setHighlightedFlowLeftId({flowId: -1}));
   globals.dispatch(Actions.setHighlightedFlowRightId({flowId: -1}));
-  if (globals.state.currentSelection?.kind === 'CHROME_SLICE') {
-    const sliceId = globals.state.currentSelection.id;
+  const currentSelection = getLegacySelection(globals.state);
+  if (currentSelection?.kind === 'CHROME_SLICE') {
+    const sliceId = currentSelection.id;
     for (const flow of globals.connectedFlows) {
       if (flow.begin.sliceId === sliceId) {
         globals.dispatch(Actions.setHighlightedFlowRightId({flowId: flow.id}));
diff --git a/ui/src/frontend/query_page.ts b/ui/src/frontend/query_page.ts
index b4b467d..7afcbaf 100644
--- a/ui/src/frontend/query_page.ts
+++ b/ui/src/frontend/query_page.ts
@@ -128,11 +128,6 @@
         : m(QueryTable, {
             query: state.executedQuery,
             resp: state.queryResult,
-            onClose: () => {
-              state.executedQuery = undefined;
-              state.queryResult = undefined;
-              raf.scheduleFullRedraw();
-            },
             fillParent: false,
           }),
       m(QueryHistoryComponent, {
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index 27e03af..f7fbf62 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -27,15 +27,8 @@
 import {PopupMenu2} from '../widgets/menu';
 import {PopupPosition} from '../widgets/popup';
 
-import {
-  addTab,
-  BottomTab,
-  bottomTabRegistry,
-  closeTab,
-  NewBottomTabArgs,
-} from './bottom_tab';
+import {BottomTab, NewBottomTabArgs} from './bottom_tab';
 import {QueryTable} from './query_table';
-import {TABS_V2_FLAG} from '../core/feature_flags';
 import {globals} from './globals';
 import {Actions} from '../common/actions';
 import {BottomTabToTabAdapter} from '../public/utils';
@@ -55,29 +48,21 @@
   config: QueryResultTabConfig,
   tag?: string,
 ): void {
-  if (TABS_V2_FLAG.get()) {
-    const queryResultsTab = new QueryResultTab({
-      config,
-      engine: getEngine(),
-      uuid: uuidv4(),
-    });
+  const queryResultsTab = new QueryResultTab({
+    config,
+    engine: getEngine(),
+    uuid: uuidv4(),
+  });
 
-    const uri = 'queryResults#' + (tag ?? uuidv4());
+  const uri = 'queryResults#' + (tag ?? uuidv4());
 
-    globals.tabManager.registerTab({
-      uri,
-      content: new BottomTabToTabAdapter(queryResultsTab),
-      isEphemeral: true,
-    });
+  globals.tabManager.registerTab({
+    uri,
+    content: new BottomTabToTabAdapter(queryResultsTab),
+    isEphemeral: true,
+  });
 
-    globals.dispatch(Actions.showTab({uri}));
-  } else {
-    return addTab({
-      kind: QueryResultTab.kind,
-      tag,
-      config,
-    });
-  }
+  globals.dispatch(Actions.showTab({uri}));
 }
 
 // TODO(stevegolton): Find a way to make this more elegant.
@@ -139,7 +124,6 @@
       query: this.config.query,
       resp: this.queryResponse,
       fillParent: true,
-      onClose: () => closeTab(this.uuid),
       contextButtons: [
         this.sqlViewName === undefined
           ? null
@@ -192,5 +176,3 @@
     return viewId;
   }
 }
-
-bottomTabRegistry.register(QueryResultTab);
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index 0bbd710..4affa45 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -211,7 +211,6 @@
 
 interface QueryTableAttrs {
   query: string;
-  onClose: () => void;
   resp?: QueryResponse;
   contextButtons?: m.Child[];
   fillParent: boolean;
@@ -219,14 +218,14 @@
 
 export class QueryTable implements m.ClassComponent<QueryTableAttrs> {
   view({attrs}: m.CVnode<QueryTableAttrs>) {
-    const {resp, query, onClose, contextButtons = [], fillParent} = attrs;
+    const {resp, query, contextButtons = [], fillParent} = attrs;
 
     return m(
       DetailsShell,
       {
         title: this.renderTitle(resp),
         description: query,
-        buttons: this.renderButtons(query, onClose, contextButtons, resp),
+        buttons: this.renderButtons(query, contextButtons, resp),
         fillParent,
       },
       resp && this.renderTableContent(resp),
@@ -243,7 +242,6 @@
 
   renderButtons(
     query: string,
-    onClose: () => void,
     contextButtons: m.Child[],
     resp?: QueryResponse,
   ) {
@@ -265,11 +263,6 @@
             queryResponseToClipboard(resp);
           },
         }),
-      m(Button, {
-        minimal: true,
-        label: 'Close',
-        onclick: onClose,
-      }),
     ];
   }
 
diff --git a/ui/src/frontend/slice_details.ts b/ui/src/frontend/slice_details.ts
index 08fd411..21fb39d 100644
--- a/ui/src/frontend/slice_details.ts
+++ b/ui/src/frontend/slice_details.ts
@@ -16,7 +16,6 @@
 
 import {BigintMath} from '../base/bigint_math';
 import {sqliteString} from '../base/string_utils';
-import {duration, time} from '../base/time';
 import {exists} from '../base/utils';
 import {Anchor} from '../widgets/anchor';
 import {MenuItem, PopupMenu2} from '../widgets/menu';
@@ -24,7 +23,6 @@
 import {SqlRef} from '../widgets/sql_ref';
 import {Tree, TreeNode} from '../widgets/tree';
 
-import {globals} from './globals';
 import {SliceDetails} from './sql/slice';
 import {
   BreakdownByThreadState,
@@ -36,15 +34,6 @@
 import {DurationWidget} from './widgets/duration';
 import {Timestamp} from './widgets/timestamp';
 
-function computeDuration(ts: time, dur: duration): m.Children {
-  if (dur === -1n) {
-    const minDuration = globals.state.traceTime.end - ts;
-    return [m(DurationWidget, {dur: minDuration}), ' (Did not end)'];
-  } else {
-    return m(DurationWidget, {dur});
-  }
-}
-
 // Renders a widget storing all of the generic details for a slice from the
 // slice table.
 export function renderDetails(
@@ -92,7 +81,7 @@
         TreeNode,
         {
           left: 'Duration',
-          right: computeDuration(slice.ts, slice.dur),
+          right: m(DurationWidget, {dur: slice.dur}),
         },
         exists(durationBreakdown) &&
           slice.dur > 0 &&
@@ -148,7 +137,7 @@
     return m(TreeNode, {
       left: 'Thread duration',
       right: [
-        computeDuration(sliceInfo.threadTs, sliceInfo.threadDur),
+        m(DurationWidget, {dur: sliceInfo.threadDur}),
         threadDurFractionSuffix,
       ],
     });
diff --git a/ui/src/frontend/slice_details_panel.ts b/ui/src/frontend/slice_details_panel.ts
index e9658c2..fb5d3a2 100644
--- a/ui/src/frontend/slice_details_panel.ts
+++ b/ui/src/frontend/slice_details_panel.ts
@@ -126,7 +126,7 @@
       return m(TreeNode, {
         icon: 'timer',
         left: 'Thread Duration',
-        right: this.computeDuration(sliceInfo.threadTs, sliceInfo.threadDur),
+        right: m(DurationWidget, {dur: sliceInfo.threadDur}),
       });
     } else {
       return null;
@@ -189,7 +189,7 @@
         }),
         m(TreeNode, {
           left: 'Duration',
-          right: this.computeDuration(sliceInfo.ts, sliceInfo.dur),
+          right: m(DurationWidget, {dur: sliceInfo.dur}),
         }),
         this.renderThreadDuration(sliceInfo),
         m(TreeNode, {
diff --git a/ui/src/frontend/slice_panel.ts b/ui/src/frontend/slice_panel.ts
index c087e19..aef97ca 100644
--- a/ui/src/frontend/slice_panel.ts
+++ b/ui/src/frontend/slice_panel.ts
@@ -14,11 +14,9 @@
 
 import m from 'mithril';
 
-import {duration, time} from '../base/time';
 import {exists} from '../base/utils';
 
-import {globals, SliceDetails} from './globals';
-import {DurationWidget} from './widgets/duration';
+import {SliceDetails} from './globals';
 
 // To display process or thread, we want to concatenate their name with ID, but
 // either can be undefined and all the cases need to be considered carefully to
@@ -39,15 +37,6 @@
 }
 
 export abstract class SlicePanel implements m.ClassComponent {
-  protected computeDuration(ts: time, dur: duration): m.Children {
-    if (dur === -1n) {
-      const minDuration = globals.state.traceTime.end - ts;
-      return [m(DurationWidget, {dur: minDuration}), ' (Did not end)'];
-    } else {
-      return m(DurationWidget, {dur});
-    }
-  }
-
   protected getProcessThreadDetails(sliceInfo: SliceDetails) {
     return new Map<string, string | undefined>([
       ['Thread', getDisplayName(sliceInfo.threadName, sliceInfo.tid)],
diff --git a/ui/src/frontend/slice_track.ts b/ui/src/frontend/slice_track.ts
index 2f9eb40..cde7027 100644
--- a/ui/src/frontend/slice_track.ts
+++ b/ui/src/frontend/slice_track.ts
@@ -20,6 +20,7 @@
 import {TrackData} from '../common/track_data';
 import {TimelineFetcher} from '../common/track_helper';
 import {SliceRect, Track} from '../public';
+import {getLegacySelection} from '../common/state';
 
 import {CROP_INCOMPLETE_SLICE_FLAG} from './base_slice_track';
 import {checkerboardExcept} from './checkerboard';
@@ -153,7 +154,7 @@
         height: SLICE_HEIGHT,
       };
 
-      const currentSelection = globals.state.currentSelection;
+      const currentSelection = getLegacySelection(globals.state);
       const isSelected =
         currentSelection &&
         currentSelection.kind === 'CHROME_SLICE' &&
diff --git a/ui/src/frontend/sql_table/tab.ts b/ui/src/frontend/sql_table/tab.ts
index ac9353b..01c87af 100644
--- a/ui/src/frontend/sql_table/tab.ts
+++ b/ui/src/frontend/sql_table/tab.ts
@@ -21,23 +21,17 @@
 import {Button} from '../../widgets/button';
 import {DetailsShell} from '../../widgets/details_shell';
 import {Popup, PopupPosition} from '../../widgets/popup';
-import {
-  addTab,
-  BottomTab,
-  bottomTabRegistry,
-  NewBottomTabArgs,
-} from '../bottom_tab';
 
 import {Filter, SqlTableState} from './state';
 import {SqlTable} from './table';
 import {SqlTableDescription, tableDisplayName} from './table_description';
-import {TABS_V2_FLAG} from '../../core/feature_flags';
 import {EngineProxy} from '../../public';
 import {globals} from '../globals';
 import {assertExists} from '../../base/logging';
 import {uuidv4} from '../../base/uuid';
 import {BottomTabToTabAdapter} from '../../public/utils';
 import {Actions} from '../../common/actions';
+import {BottomTab, NewBottomTabArgs} from '../bottom_tab';
 
 interface SqlTableTabConfig {
   table: SqlTableDescription;
@@ -46,28 +40,21 @@
 }
 
 export function addSqlTableTab(config: SqlTableTabConfig): void {
-  if (TABS_V2_FLAG.get()) {
-    const queryResultsTab = new SqlTableTab({
-      config,
-      engine: getEngine(),
-      uuid: uuidv4(),
-    });
+  const queryResultsTab = new SqlTableTab({
+    config,
+    engine: getEngine(),
+    uuid: uuidv4(),
+  });
 
-    const uri = 'sqlTable#' + uuidv4();
+  const uri = 'sqlTable#' + uuidv4();
 
-    globals.tabManager.registerTab({
-      uri,
-      content: new BottomTabToTabAdapter(queryResultsTab),
-      isEphemeral: true,
-    });
+  globals.tabManager.registerTab({
+    uri,
+    content: new BottomTabToTabAdapter(queryResultsTab),
+    isEphemeral: true,
+  });
 
-    globals.dispatch(Actions.showTab({uri}));
-  } else {
-    return addTab({
-      kind: SqlTableTab.kind,
-      config,
-    });
-  }
+  globals.dispatch(Actions.showTab({uri}));
 }
 
 // TODO(stevegolton): Find a way to make this more elegant.
@@ -145,10 +132,6 @@
             onclick: () =>
               copyToClipboard(this.state.getNonPaginatedSQLQuery()),
           }),
-          m(Button, {
-            label: 'Close',
-            onclick: () => this.close(),
-          }),
         ],
       },
       m(SqlTable, {
@@ -171,5 +154,3 @@
     return this.state.isLoading();
   }
 }
-
-bottomTabRegistry.register(SqlTableTab);
diff --git a/ui/src/frontend/tab_panel.ts b/ui/src/frontend/tab_panel.ts
index 82414eb..23e8095 100644
--- a/ui/src/frontend/tab_panel.ts
+++ b/ui/src/frontend/tab_panel.ts
@@ -16,6 +16,7 @@
 
 import {Gate} from '../base/mithril_utils';
 import {Actions} from '../common/actions';
+import {getLegacySelection} from '../common/state';
 import {EmptyState} from '../widgets/empty_state';
 
 import {
@@ -62,7 +63,7 @@
 
     if (
       !this.hasBeenDragged &&
-      (tabs.length > 0 || globals.state.currentSelection)
+      (tabs.length > 0 || getLegacySelection(globals.state))
     ) {
       this.detailsHeight = getDefaultDetailsHeight();
     }
@@ -128,7 +129,7 @@
   }
 
   private renderCSTabContent(): {isLoading: boolean; content: m.Children} {
-    const cs = globals.state.currentSelection;
+    const cs = getLegacySelection(globals.state);
     if (!cs) {
       return {
         isLoading: false,
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index c34c92d..30209ab 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -25,7 +25,7 @@
 import {SqlRef} from '../widgets/sql_ref';
 import {Tree, TreeNode} from '../widgets/tree';
 
-import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from './bottom_tab';
+import {BottomTab, NewBottomTabArgs} from './bottom_tab';
 import {SchedSqlId, ThreadStateSqlId} from './sql_types';
 import {
   getFullThreadName,
@@ -387,5 +387,3 @@
     return this.state === undefined || this.relatedStates === undefined;
   }
 }
-
-bottomTabRegistry.register(ThreadStateTab);
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index 0da9a23..d77b095 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -32,6 +32,7 @@
 import {PanelSize} from './panel';
 import {Panel} from './panel_container';
 import {renderDuration} from './widgets/duration';
+import {getLegacySelection} from '../common/state';
 
 export interface BBox {
   x: number;
@@ -172,7 +173,7 @@
     }
 
     const localArea = globals.timeline.selectedArea;
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (localArea !== undefined) {
       const start = Time.min(localArea.start, localArea.end);
       const end = Time.max(localArea.start, localArea.end);
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 627ac4e..0f0f401 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -16,7 +16,7 @@
 
 import {Icons} from '../base/semantic_icons';
 import {Actions} from '../common/actions';
-import {getContainingTrackId} from '../common/state';
+import {getContainingTrackId, getLegacySelection} from '../common/state';
 import {TrackCacheEntry} from '../common/track_cache';
 import {TrackTags} from '../public';
 
@@ -77,7 +77,7 @@
       }
     }
 
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
 
     const trackGroup = globals.state.trackGroups[trackGroupId];
     let checkBox = Icons.BlankCheckbox;
@@ -173,7 +173,7 @@
 
   highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
     const {visibleTimeScale} = globals.timeline;
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (!selection || selection.kind !== 'AREA') return;
     const selectedArea = globals.state.areas[selection.areaId];
     const selectedAreaDuration = selectedArea.end - selectedArea.start;
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index d186fdb..2e15d67 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -36,6 +36,7 @@
 import {Popup} from '../widgets/popup';
 import {canvasClip} from '../common/canvas_utils';
 import {TimeScale} from './time_scale';
+import {getLegacySelection} from '../common/state';
 
 function getTitleSize(title: string): string | undefined {
   const length = title.length;
@@ -62,7 +63,7 @@
 }
 
 function isSelected(id: string) {
-  const selection = globals.state.currentSelection;
+  const selection = getLegacySelection(globals.state);
   if (selection === null || selection.kind !== 'AREA') return false;
   const selectedArea = globals.state.areas[selection.areaId];
   return selectedArea.tracks.includes(id);
@@ -143,6 +144,8 @@
       }
     }
 
+    const currentSelection = getLegacySelection(globals.state);
+
     return m(
       `.track-shell[draggable=true]`,
       {
@@ -183,8 +186,7 @@
           showButton: isPinned(attrs.trackKey),
           fullHeight: true,
         }),
-        globals.state.currentSelection !== null &&
-          globals.state.currentSelection.kind === 'AREA'
+        currentSelection !== null && currentSelection.kind === 'AREA'
           ? m(TrackButton, {
               action: (e: MouseEvent) => {
                 globals.dispatch(
@@ -473,7 +475,7 @@
 
   highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
     const {visibleTimeScale} = globals.timeline;
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     if (!selection || selection.kind !== 'AREA') {
       return;
     }
@@ -574,9 +576,10 @@
   visibleTimeScale: TimeScale,
   size: PanelSize,
 ) {
-  if (globals.state.currentSelection !== null) {
+  const currentSelection = getLegacySelection(globals.state);
+  if (currentSelection !== null) {
     if (
-      globals.state.currentSelection.kind === 'SLICE' &&
+      currentSelection.kind === 'SLICE' &&
       globals.sliceDetails.wakeupTs !== undefined
     ) {
       drawVerticalLineAtTime(
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 0c8a185..6cd0fb4 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -19,12 +19,11 @@
 import {Time} from '../base/time';
 import {Actions} from '../common/actions';
 import {TrackCacheEntry} from '../common/track_cache';
-import {TABS_V2_FLAG, featureFlags} from '../core/feature_flags';
+import {featureFlags} from '../core/feature_flags';
 import {raf} from '../core/raf_scheduler';
 import {TrackTags} from '../public';
 
 import {TRACK_SHELL_WIDTH} from './css_constants';
-import {DetailsPanel} from './details_panel';
 import {globals} from './globals';
 import {NotesPanel} from './notes_panel';
 import {OverviewTimelinePanel} from './overview_timeline_panel';
@@ -40,6 +39,7 @@
 import {TrackGroupPanel} from './track_group_panel';
 import {TrackPanel} from './track_panel';
 import {assertExists} from '../base/logging';
+import {getLegacySelection} from '../common/state';
 
 const OVERVIEW_PANEL_FLAG = featureFlags.register({
   id: 'overviewVisible',
@@ -51,7 +51,7 @@
 // Checks if the mousePos is within 3px of the start or end of the
 // current selected time range.
 function onTimeRangeBoundary(mousePos: number): 'START' | 'END' | null {
-  const selection = globals.state.currentSelection;
+  const selection = getLegacySelection(globals.state);
   if (selection !== null && selection.kind === 'AREA') {
     // If frontend selectedArea exists then we are in the process of editing the
     // time range and need to use that value instead.
@@ -152,7 +152,7 @@
         const {visibleTimeScale} = timeline;
         this.keepCurrentSelection = true;
         if (editing) {
-          const selection = globals.state.currentSelection;
+          const selection = getLegacySelection(globals.state);
           if (selection !== null && selection.kind === 'AREA') {
             const area = globals.timeline.selectedArea
               ? globals.timeline.selectedArea
@@ -208,7 +208,7 @@
         // If we are editing we need to pass the current id through to ensure
         // the marked area with that id is also updated.
         if (edit) {
-          const selection = globals.state.currentSelection;
+          const selection = getLegacySelection(globals.state);
           if (selection !== null && selection.kind === 'AREA' && area) {
             globals.dispatch(
               Actions.editArea({area, areaId: selection.areaId}),
@@ -365,11 +365,7 @@
   }
 
   private renderTabPanel() {
-    if (TABS_V2_FLAG.get()) {
-      return m(TabPanel);
-    } else {
-      return m(DetailsPanel);
-    }
+    return m(TabPanel);
   }
 }
 
diff --git a/ui/src/frontend/widgets/duration.ts b/ui/src/frontend/widgets/duration.ts
index c0357fb..6a05ab4 100644
--- a/ui/src/frontend/widgets/duration.ts
+++ b/ui/src/frontend/widgets/duration.ts
@@ -38,6 +38,9 @@
 export class DurationWidget implements m.ClassComponent<DurationWidgetAttrs> {
   view({attrs}: m.Vnode<DurationWidgetAttrs>) {
     const {dur} = attrs;
+    if (dur === -1n) {
+      return '(Did not end)';
+    }
     return m(
       PopupMenu2,
       {
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 6bc1b49..a1a2842 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -1047,16 +1047,13 @@
     from step3 left join app_package_list pl using(uid)
 `;
 
+// See go/bt_system_context_report for reference on the bit-twiddling.
 const BT_ACTIVITY = `
+  create perfetto table bt_activity as
   with step1 as (
     select
         EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.timestamp_millis') * 1000000 as ts,
-        case EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.bluetooth_stack_state')
-        when 1 then 'active'
-        when 2 then 'scanning'
-        when 3 then 'idle'
-        else 'invalid'
-        end as bluetooth_stack_state,
+        EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.bluetooth_stack_state') as bluetooth_stack_state,
         EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.controller_idle_time_millis') * 1000000 as controller_idle_dur,
         EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.controller_tx_time_millis') * 1000000 as controller_tx_dur,
         EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.controller_rx_time_millis') * 1000000 as controller_rx_dur
@@ -1076,11 +1073,27 @@
   )
   select
     ts,
-    dur * controller_rx_dur / dur as dur,
-    cast(100.0 * controller_rx_dur / dur as int) || '% RX, ' ||
-        cast(100.0 * controller_tx_dur / dur as int) || '% TX, ' || bluetooth_stack_state as name
+    dur,
+    bluetooth_stack_state & 0x0000000F as acl_active_count,
+    bluetooth_stack_state & 0x000000F0 >> 4 as acl_sniff_count,
+    bluetooth_stack_state & 0x00000F00 >> 8 as acl_ble_count,
+    bluetooth_stack_state & 0x0000F000 >> 12 as advertising_count,
+    case bluetooth_stack_state & 0x000F0000 >> 16
+      when 0 then 'disabled'
+      when 1 then '<5%'
+      when 2 then '5% to 10%'
+      when 3 then '10% to 25%'
+      when 4 then '25% to 100%'
+      else 'invalid'
+    end as le_scan_duty_cycle,
+    bluetooth_stack_state & 0x00100000 >> 20 as inquiry_active,
+    bluetooth_stack_state & 0x00200000 >> 21 as sco_active,
+    bluetooth_stack_state & 0x00400000 >> 22 as a2dp_active,
+    bluetooth_stack_state & 0x00800000 >> 23 as le_audio_active,
+    max(0, 100.0 * controller_idle_dur / dur) as controller_idle_pct,
+    max(0, 100.0 * controller_tx_dur / dur) as controller_tx_pct,
+    max(0, 100.0 * controller_rx_dur / dur) as controller_rx_pct
   from step2
-  where controller_rx_dur > 0
 `;
 
 class AndroidLongBatteryTracing implements Plugin {
@@ -1564,7 +1577,82 @@
       BT_BYTES,
       groupName,
     );
-    this.addSliceTrack(ctx, 'Activity info', BT_ACTIVITY, groupName);
+    await ctx.engine.query(BT_ACTIVITY);
+    this.addCounterTrack(
+      ctx,
+      'ACL Classic Active Count',
+      'select ts, dur, acl_active_count as value from bt_activity',
+      groupName,
+    );
+    this.addCounterTrack(
+      ctx,
+      'ACL Classic Sniff Count',
+      'select ts, dur, acl_sniff_count as value from bt_activity',
+      groupName,
+    );
+    this.addCounterTrack(
+      ctx,
+      'ACL BLE Count',
+      'select ts, dur, acl_ble_count as value from bt_activity',
+      groupName,
+    );
+    this.addCounterTrack(
+      ctx,
+      'Advertising Instance Count',
+      'select ts, dur, advertising_count as value from bt_activity',
+      groupName,
+    );
+    this.addSliceTrack(
+      ctx,
+      'LE Scan Duty Cycle',
+      'select ts, dur, le_scan_duty_cycle as name from bt_activity',
+      groupName,
+    );
+    this.addSliceTrack(
+      ctx,
+      'Inquiry Active',
+      "select ts, dur, 'Active' as name from bt_activity where inquiry_active",
+      groupName,
+    );
+    this.addSliceTrack(
+      ctx,
+      'SCO Active',
+      "select ts, dur, 'Active' as name from bt_activity where sco_active",
+      groupName,
+    );
+    this.addSliceTrack(
+      ctx,
+      'A2DP Active',
+      "select ts, dur, 'Active' as name from bt_activity where a2dp_active",
+      groupName,
+    );
+    this.addSliceTrack(
+      ctx,
+      'LE Audio Active',
+      "select ts, dur, 'Active' as name from bt_activity where le_audio_active",
+      groupName,
+    );
+    this.addCounterTrack(
+      ctx,
+      'Controller Idle Time',
+      'select ts, dur, controller_idle_pct as value from bt_activity',
+      groupName,
+      {yRangeSharingKey: 'bt_controller_time', unit: '%'},
+    );
+    this.addCounterTrack(
+      ctx,
+      'Controller TX Time',
+      'select ts, dur, controller_tx_pct as value from bt_activity',
+      groupName,
+      {yRangeSharingKey: 'bt_controller_time', unit: '%'},
+    );
+    this.addCounterTrack(
+      ctx,
+      'Controller RX Time',
+      'select ts, dur, controller_rx_pct as value from bt_activity',
+      groupName,
+      {yRangeSharingKey: 'bt_controller_time', unit: '%'},
+    );
     this.addSliceTrack(
       ctx,
       'Quality reports',
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 3ba4c6a..19c1867 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -18,9 +18,10 @@
 import {duration, time} from '../base/time';
 import {Migrate, Store} from '../base/store';
 import {ColorScheme} from '../core/colorizer';
-import {Selection} from '../common/state';
+import {LegacySelection} from '../common/state';
 import {PanelSize} from '../frontend/panel';
 import {EngineProxy} from '../trace_processor/engine';
+import {UntypedEventSet} from '../common/event_set';
 
 export {EngineProxy} from '../trace_processor/engine';
 export {
@@ -208,6 +209,11 @@
   onMouseMove?(position: {x: number; y: number}): void;
   onMouseClick?(position: {x: number; y: number}): boolean;
   onMouseOut?(): void;
+
+  /**
+   * Optional: Get the event set that represents this track's data.
+   */
+  getEventSet?(): UntypedEventSet;
 }
 
 // A definition of a track, including a renderer implementation and metadata.
@@ -325,7 +331,7 @@
 }
 
 export interface DetailsPanel {
-  render(selection: Selection): m.Children;
+  render(selection: LegacySelection): m.Children;
   isLoading?(): boolean;
 }
 
diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts
index 55e6bce..60c4487 100644
--- a/ui/src/public/utils.ts
+++ b/ui/src/public/utils.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 
-import {Selection} from '../common/state';
+import {LegacySelection} from '../common/state';
 import {BottomTab} from '../frontend/bottom_tab';
 
 import {DetailsPanel, Tab} from '.';
@@ -97,7 +97,7 @@
 }
 
 export interface BottomTabAdapterAttrs {
-  tabFactory: (sel: Selection) => BottomTab | undefined;
+  tabFactory: (sel: LegacySelection) => BottomTab | undefined;
 }
 
 /**
@@ -129,7 +129,7 @@
     })
  */
 export class BottomTabToSCSAdapter implements DetailsPanel {
-  private oldSelection?: Selection;
+  private oldSelection?: LegacySelection;
   private bottomTab?: BottomTab;
   private attrs: BottomTabAdapterAttrs;
 
@@ -137,7 +137,7 @@
     this.attrs = attrs;
   }
 
-  render(selection: Selection): m.Children {
+  render(selection: LegacySelection): m.Children {
     // Detect selection changes, assuming selection is immutable
     if (selection !== this.oldSelection) {
       this.oldSelection = selection;
diff --git a/ui/src/tracks/chrome_critical_user_interactions/page_load_details_panel.ts b/ui/src/tracks/chrome_critical_user_interactions/page_load_details_panel.ts
index 34b5451..4aee5ce 100644
--- a/ui/src/tracks/chrome_critical_user_interactions/page_load_details_panel.ts
+++ b/ui/src/tracks/chrome_critical_user_interactions/page_load_details_panel.ts
@@ -14,11 +14,7 @@
 
 import m from 'mithril';
 
-import {
-  BottomTab,
-  bottomTabRegistry,
-  NewBottomTabArgs,
-} from '../../frontend/bottom_tab';
+import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {Details, DetailsSchema} from '../../frontend/sql/details/details';
 import {wellKnownTypes} from '../../frontend/sql/details/well_known_types';
@@ -93,5 +89,3 @@
     return this.data.isLoading();
   }
 }
-
-bottomTabRegistry.register(PageLoadDetailsPanel);
diff --git a/ui/src/tracks/chrome_critical_user_interactions/startup_details_panel.ts b/ui/src/tracks/chrome_critical_user_interactions/startup_details_panel.ts
index 621ec36..4312079 100644
--- a/ui/src/tracks/chrome_critical_user_interactions/startup_details_panel.ts
+++ b/ui/src/tracks/chrome_critical_user_interactions/startup_details_panel.ts
@@ -15,11 +15,7 @@
 import m from 'mithril';
 
 import {duration, Time, time} from '../../base/time';
-import {
-  BottomTab,
-  bottomTabRegistry,
-  NewBottomTabArgs,
-} from '../../frontend/bottom_tab';
+import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {DurationWidget} from '../../frontend/widgets/duration';
 import {Timestamp} from '../../frontend/widgets/timestamp';
@@ -148,5 +144,3 @@
     return !this.loaded;
   }
 }
-
-bottomTabRegistry.register(StartupDetailsPanel);
diff --git a/ui/src/tracks/chrome_critical_user_interactions/web_content_interaction_details_panel.ts b/ui/src/tracks/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
index 26ac119..7cf7b48 100644
--- a/ui/src/tracks/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
+++ b/ui/src/tracks/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
@@ -29,11 +29,7 @@
 import m from 'mithril';
 
 import {duration, Time, time} from '../../base/time';
-import {
-  BottomTab,
-  bottomTabRegistry,
-  NewBottomTabArgs,
-} from '../../frontend/bottom_tab';
+import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {asUpid, Upid} from '../../frontend/sql_types';
 import {DurationWidget} from '../../frontend/widgets/duration';
@@ -149,5 +145,3 @@
     return !this.loaded;
   }
 }
-
-bottomTabRegistry.register(WebContentInteractionPanel);
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts
index d638580..5040802 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts
@@ -16,11 +16,7 @@
 
 import {Duration, duration, time} from '../../base/time';
 import {raf} from '../../core/raf_scheduler';
-import {
-  BottomTab,
-  bottomTabRegistry,
-  NewBottomTabArgs,
-} from '../../frontend/bottom_tab';
+import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {hasArgs, renderArguments} from '../../frontend/slice_args';
 import {renderDetails} from '../../frontend/slice_details';
@@ -547,5 +543,3 @@
     return `Current Selection`;
   }
 }
-
-bottomTabRegistry.register(EventLatencySliceDetailsPanel);
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
index dac1878..85a2a70 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
@@ -29,6 +29,7 @@
   ScrollJankTracks as DecideTracksResult,
 } from './index';
 import {JANK_COLOR} from './jank_colors';
+import {getLegacySelection} from '../../common/state';
 
 export const JANKY_LATENCY_NAME = 'Janky EventLatency';
 
@@ -85,7 +86,7 @@
 
   onUpdatedSlices(slices: EventLatencyTrackTypes['slice'][]) {
     for (const slice of slices) {
-      const currentSelection = globals.state.currentSelection;
+      const currentSelection = getLegacySelection(globals.state);
       const isSelected =
         currentSelection &&
         currentSelection.kind === 'GENERIC_SLICE' &&
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts b/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts
index 41cef53..d1bfb8c 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts
@@ -17,11 +17,7 @@
 import {duration, Time, time} from '../../base/time';
 import {exists} from '../../base/utils';
 import {raf} from '../../core/raf_scheduler';
-import {
-  BottomTab,
-  bottomTabRegistry,
-  NewBottomTabArgs,
-} from '../../frontend/bottom_tab';
+import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {sqlValueToString} from '../../frontend/sql_utils';
 import {
@@ -447,5 +443,3 @@
     return !this.loaded;
   }
 }
-
-bottomTabRegistry.register(ScrollDetailsPanel);
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_details_panel.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
index 4818327..e47e75c 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
@@ -17,11 +17,7 @@
 import {duration, Time, time} from '../../base/time';
 import {exists} from '../../base/utils';
 import {raf} from '../../core/raf_scheduler';
-import {
-  BottomTab,
-  bottomTabRegistry,
-  NewBottomTabArgs,
-} from '../../frontend/bottom_tab';
+import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {getSlice, SliceDetails} from '../../frontend/sql/slice';
 import {asSliceSqlId} from '../../frontend/sql_types';
@@ -342,5 +338,3 @@
     return !this.loaded;
   }
 }
-
-bottomTabRegistry.register(ScrollJankV3DetailsPanel);
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
index fb77174..ba44b9b 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -31,6 +31,7 @@
 import {JANK_COLOR} from './jank_colors';
 import {ScrollJankV3DetailsPanel} from './scroll_jank_v3_details_panel';
 import {getColorForSlice} from '../../core/colorizer';
+import {getLegacySelection} from '../../common/state';
 
 const UNKNOWN_SLICE_NAME = 'Unknown';
 const JANK_SLICE_NAME = ' Jank';
@@ -100,7 +101,7 @@
 
   onUpdatedSlices(slices: EventLatencyTrackTypes['slice'][]) {
     for (const slice of slices) {
-      const currentSelection = globals.state.currentSelection;
+      const currentSelection = getLegacySelection(globals.state);
       const isSelected =
         currentSelection &&
         currentSelection.kind === 'GENERIC_SLICE' &&
diff --git a/ui/src/tracks/cpu_profile/index.ts b/ui/src/tracks/cpu_profile/index.ts
index fb7c7c5..c3a2b57 100644
--- a/ui/src/tracks/cpu_profile/index.ts
+++ b/ui/src/tracks/cpu_profile/index.ts
@@ -14,6 +14,7 @@
 
 import {searchSegment} from '../../base/binary_search';
 import {duration, Time, time} from '../../base/time';
+import {getLegacySelection} from '../../common/state';
 import {Actions} from '../../common/actions';
 import {colorForSample} from '../../core/colorizer';
 import {TrackData} from '../../common/track_data';
@@ -117,7 +118,7 @@
 
     for (let i = 0; i < data.tsStarts.length; i++) {
       const centerX = Time.fromRaw(data.tsStarts[i]);
-      const selection = globals.state.currentSelection;
+      const selection = getLegacySelection(globals.state);
       const isHovered = this.hoveredTs === centerX;
       const isSelected =
         selection !== null &&
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 647baed..b4c5fe0 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -20,6 +20,7 @@
 import {Duration, duration, Time, time} from '../../base/time';
 import {Actions} from '../../common/actions';
 import {calcCachedBucketSize} from '../../common/cache_utils';
+import {getLegacySelection} from '../../common/state';
 import {
   cropText,
   drawDoubleHeadedArrow,
@@ -392,7 +393,7 @@
       ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
     }
 
-    const selection = globals.state.currentSelection;
+    const selection = getLegacySelection(globals.state);
     const details = globals.sliceDetails;
     if (selection !== null && selection.kind === 'SLICE') {
       const [startIndex, endIndex] = searchEq(data.ids, selection.id);
diff --git a/ui/src/tracks/custom_sql_table_slices/index.ts b/ui/src/tracks/custom_sql_table_slices/index.ts
index 88795af..2222f9b 100644
--- a/ui/src/tracks/custom_sql_table_slices/index.ts
+++ b/ui/src/tracks/custom_sql_table_slices/index.ts
@@ -17,7 +17,7 @@
 import {Disposable, DisposableCallback} from '../../base/disposable';
 import {Actions} from '../../common/actions';
 import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
-import {Selection} from '../../common/state';
+import {LegacySelection} from '../../common/state';
 import {OnSliceClickArgs} from '../../frontend/base_slice_track';
 import {GenericSliceDetailsTabConfigBase} from '../../frontend/generic_slice_details_tab';
 import {globals} from '../../frontend/globals';
@@ -105,7 +105,7 @@
     return `SELECT * FROM ${this.tableName}`;
   }
 
-  isSelectionHandled(selection: Selection) {
+  isSelectionHandled(selection: LegacySelection) {
     if (selection.kind !== 'GENERIC_SLICE') {
       return false;
     }
diff --git a/ui/src/tracks/debug/details_tab.ts b/ui/src/tracks/debug/details_tab.ts
index 91ae02b..659c48c 100644
--- a/ui/src/tracks/debug/details_tab.ts
+++ b/ui/src/tracks/debug/details_tab.ts
@@ -16,11 +16,7 @@
 
 import {duration, Time, time} from '../../base/time';
 import {raf} from '../../core/raf_scheduler';
-import {
-  BottomTab,
-  bottomTabRegistry,
-  NewBottomTabArgs,
-} from '../../frontend/bottom_tab';
+import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {hasArgs, renderArguments} from '../../frontend/slice_args';
 import {getSlice, SliceDetails, sliceRef} from '../../frontend/sql/slice';
@@ -279,5 +275,3 @@
     return this.data === undefined;
   }
 }
-
-bottomTabRegistry.register(DebugSliceDetailsTab);
diff --git a/ui/src/tracks/heap_profile/index.ts b/ui/src/tracks/heap_profile/index.ts
index d734327..b905ef7 100644
--- a/ui/src/tracks/heap_profile/index.ts
+++ b/ui/src/tracks/heap_profile/index.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {Actions} from '../../common/actions';
-import {ProfileType, Selection} from '../../common/state';
+import {ProfileType, LegacySelection} from '../../common/state';
 import {profileType} from '../../controller/flamegraph_controller';
 import {
   BASE_ROW,
@@ -109,7 +109,7 @@
     );
   }
 
-  protected isSelectionHandled(selection: Selection): boolean {
+  protected isSelectionHandled(selection: LegacySelection): boolean {
     return selection.kind === 'HEAP_PROFILE';
   }
 }
diff --git a/ui/src/tracks/perf_samples_profile/index.ts b/ui/src/tracks/perf_samples_profile/index.ts
index 37422d4..6720142 100644
--- a/ui/src/tracks/perf_samples_profile/index.ts
+++ b/ui/src/tracks/perf_samples_profile/index.ts
@@ -15,7 +15,7 @@
 import {searchSegment} from '../../base/binary_search';
 import {duration, Time, time} from '../../base/time';
 import {Actions} from '../../common/actions';
-import {ProfileType} from '../../common/state';
+import {ProfileType, getLegacySelection} from '../../common/state';
 import {TrackData} from '../../common/track_data';
 import {TimelineFetcher} from '../../common/track_helper';
 import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
@@ -114,7 +114,7 @@
 
     for (let i = 0; i < data.tsStarts.length; i++) {
       const centerX = Time.fromRaw(data.tsStarts[i]);
-      const selection = globals.state.currentSelection;
+      const selection = getLegacySelection(globals.state);
       const isHovered = this.hoveredTs === centerX;
       const isSelected =
         selection !== null &&
diff --git a/ui/src/tracks/screenshots/screenshot_panel.ts b/ui/src/tracks/screenshots/screenshot_panel.ts
index 444ca30..066d14c 100644
--- a/ui/src/tracks/screenshots/screenshot_panel.ts
+++ b/ui/src/tracks/screenshots/screenshot_panel.ts
@@ -16,11 +16,7 @@
 
 import {assertTrue} from '../../base/logging';
 import {exists} from '../../base/utils';
-import {
-  BottomTab,
-  bottomTabRegistry,
-  NewBottomTabArgs,
-} from '../../frontend/bottom_tab';
+import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {getSlice, SliceDetails} from '../../frontend/sql/slice';
 import {asSliceSqlId} from '../../frontend/sql_types';
@@ -74,5 +70,3 @@
     );
   }
 }
-
-bottomTabRegistry.register(ScreenshotTab);
diff --git a/ui/src/tracks/thread_state/thread_state_v2.ts b/ui/src/tracks/thread_state/thread_state_v2.ts
index 631160d..4ab000f 100644
--- a/ui/src/tracks/thread_state/thread_state_v2.ts
+++ b/ui/src/tracks/thread_state/thread_state_v2.ts
@@ -14,7 +14,7 @@
 
 import {Actions} from '../../common/actions';
 import {colorForState} from '../../core/colorizer';
-import {Selection} from '../../common/state';
+import {LegacySelection} from '../../common/state';
 import {translateState} from '../../common/thread_state';
 import {
   BASE_ROW,
@@ -102,7 +102,7 @@
     );
   }
 
-  protected isSelectionHandled(selection: Selection): boolean {
+  protected isSelectionHandled(selection: LegacySelection): boolean {
     return selection.kind === 'THREAD_STATE';
   }
 }