Merge "Add --stdlib-sources argument to check_sql_modules"
diff --git a/include/perfetto/public/protos/BUILD.gn b/include/perfetto/public/protos/BUILD.gn
index 5432dd5..5901a27 100644
--- a/include/perfetto/public/protos/BUILD.gn
+++ b/include/perfetto/public/protos/BUILD.gn
@@ -17,8 +17,13 @@
     "common/builtin_clock.pzc.h",
     "config/data_source_config.pzc.h",
     "config/trace_config.pzc.h",
+    "trace/interned_data/interned_data.pzc.h",
     "trace/test_event.pzc.h",
     "trace/trace.pzc.h",
     "trace/trace_packet.pzc.h",
+    "trace/track_event/counter_descriptor.pzc.h",
+    "trace/track_event/debug_annotation.pzc.h",
+    "trace/track_event/track_descriptor.pzc.h",
+    "trace/track_event/track_event.pzc.h",
   ]
 }
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
new file mode 100644
index 0000000..b5147a5
--- /dev/null
+++ b/include/perfetto/public/protos/trace/interned_data/interned_data.pzc.h
@@ -0,0 +1,148 @@
+/*
+ * 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 INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_INTERNED_DATA_INTERNED_DATA_PZC_H_
+#define INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_INTERNED_DATA_INTERNED_DATA_PZC_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "perfetto/public/pb_macros.h"
+
+PERFETTO_PB_MSG_DECL(perfetto_protos_Callstack);
+PERFETTO_PB_MSG_DECL(perfetto_protos_DebugAnnotationName);
+PERFETTO_PB_MSG_DECL(perfetto_protos_DebugAnnotationValueTypeName);
+PERFETTO_PB_MSG_DECL(perfetto_protos_EventCategory);
+PERFETTO_PB_MSG_DECL(perfetto_protos_EventName);
+PERFETTO_PB_MSG_DECL(perfetto_protos_Frame);
+PERFETTO_PB_MSG_DECL(perfetto_protos_HistogramName);
+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_LogMessageBody);
+PERFETTO_PB_MSG_DECL(perfetto_protos_Mapping);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ProfiledFrameSymbols);
+PERFETTO_PB_MSG_DECL(perfetto_protos_SourceLocation);
+PERFETTO_PB_MSG_DECL(perfetto_protos_UnsymbolizedSourceLocation);
+
+PERFETTO_PB_MSG(perfetto_protos_InternedData);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_EventCategory,
+                  event_categories,
+                  1);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_EventName,
+                  event_names,
+                  2);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_DebugAnnotationName,
+                  debug_annotation_names,
+                  3);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_DebugAnnotationValueTypeName,
+                  debug_annotation_value_type_names,
+                  27);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_SourceLocation,
+                  source_locations,
+                  4);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_UnsymbolizedSourceLocation,
+                  unsymbolized_source_locations,
+                  28);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_LogMessageBody,
+                  log_message_body,
+                  20);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_HistogramName,
+                  histogram_names,
+                  25);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedString,
+                  build_ids,
+                  16);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedString,
+                  mapping_paths,
+                  17);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedString,
+                  source_paths,
+                  18);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedString,
+                  function_names,
+                  5);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_ProfiledFrameSymbols,
+                  profiled_frame_symbols,
+                  21);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_Mapping,
+                  mappings,
+                  19);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_Frame,
+                  frames,
+                  6);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_Callstack,
+                  callstacks,
+                  7);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedString,
+                  vulkan_memory_keys,
+                  22);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedGraphicsContext,
+                  graphics_contexts,
+                  23);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedGpuRenderStageSpecification,
+                  gpu_specifications,
+                  24);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedString,
+                  kernel_symbols,
+                  26);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+                  MSG,
+                  perfetto_protos_InternedString,
+                  debug_annotation_string_values,
+                  29);
+
+#endif  // INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_INTERNED_DATA_INTERNED_DATA_PZC_H_
diff --git a/include/perfetto/public/protos/trace/track_event/counter_descriptor.pzc.h b/include/perfetto/public/protos/trace/track_event/counter_descriptor.pzc.h
new file mode 100644
index 0000000..a5d075a
--- /dev/null
+++ b/include/perfetto/public/protos/trace/track_event/counter_descriptor.pzc.h
@@ -0,0 +1,77 @@
+/*
+ * 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 INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_COUNTER_DESCRIPTOR_PZC_H_
+#define INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_COUNTER_DESCRIPTOR_PZC_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "perfetto/public/pb_macros.h"
+
+PERFETTO_PB_ENUM_IN_MSG(perfetto_protos_CounterDescriptor, BuiltinCounterType){
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_CounterDescriptor,
+                                  COUNTER_UNSPECIFIED) = 0,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_CounterDescriptor,
+                                  COUNTER_THREAD_TIME_NS) = 1,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_CounterDescriptor,
+                                  COUNTER_THREAD_INSTRUCTION_COUNT) = 2,
+};
+
+PERFETTO_PB_ENUM_IN_MSG(perfetto_protos_CounterDescriptor, Unit){
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_CounterDescriptor,
+                                  UNIT_UNSPECIFIED) = 0,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_CounterDescriptor,
+                                  UNIT_TIME_NS) = 1,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_CounterDescriptor,
+                                  UNIT_COUNT) = 2,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_CounterDescriptor,
+                                  UNIT_SIZE_BYTES) = 3,
+};
+
+PERFETTO_PB_MSG(perfetto_protos_CounterDescriptor);
+PERFETTO_PB_FIELD(perfetto_protos_CounterDescriptor,
+                  VARINT,
+                  enum perfetto_protos_CounterDescriptor_BuiltinCounterType,
+                  type,
+                  1);
+PERFETTO_PB_FIELD(perfetto_protos_CounterDescriptor,
+                  STRING,
+                  const char*,
+                  categories,
+                  2);
+PERFETTO_PB_FIELD(perfetto_protos_CounterDescriptor,
+                  VARINT,
+                  enum perfetto_protos_CounterDescriptor_Unit,
+                  unit,
+                  3);
+PERFETTO_PB_FIELD(perfetto_protos_CounterDescriptor,
+                  STRING,
+                  const char*,
+                  unit_name,
+                  6);
+PERFETTO_PB_FIELD(perfetto_protos_CounterDescriptor,
+                  VARINT,
+                  int64_t,
+                  unit_multiplier,
+                  4);
+PERFETTO_PB_FIELD(perfetto_protos_CounterDescriptor,
+                  VARINT,
+                  bool,
+                  is_incremental,
+                  5);
+
+#endif  // INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_COUNTER_DESCRIPTOR_PZC_H_
diff --git a/include/perfetto/public/protos/trace/track_event/debug_annotation.pzc.h b/include/perfetto/public/protos/trace/track_event/debug_annotation.pzc.h
new file mode 100644
index 0000000..d4152ae
--- /dev/null
+++ b/include/perfetto/public/protos/trace/track_event/debug_annotation.pzc.h
@@ -0,0 +1,182 @@
+/*
+ * 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 INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_DEBUG_ANNOTATION_PZC_H_
+#define INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_DEBUG_ANNOTATION_PZC_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "perfetto/public/pb_macros.h"
+
+PERFETTO_PB_MSG_DECL(perfetto_protos_DebugAnnotation);
+PERFETTO_PB_MSG_DECL(perfetto_protos_DebugAnnotation_NestedValue);
+
+PERFETTO_PB_ENUM_IN_MSG(perfetto_protos_DebugAnnotation_NestedValue,
+                        NestedType){
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_DebugAnnotation_NestedValue,
+                                  UNSPECIFIED) = 0,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_DebugAnnotation_NestedValue,
+                                  DICT) = 1,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_DebugAnnotation_NestedValue,
+                                  ARRAY) = 2,
+};
+
+PERFETTO_PB_MSG(perfetto_protos_DebugAnnotationValueTypeName);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotationValueTypeName,
+                  VARINT,
+                  uint64_t,
+                  iid,
+                  1);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotationValueTypeName,
+                  STRING,
+                  const char*,
+                  name,
+                  2);
+
+PERFETTO_PB_MSG(perfetto_protos_DebugAnnotationName);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotationName,
+                  VARINT,
+                  uint64_t,
+                  iid,
+                  1);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotationName,
+                  STRING,
+                  const char*,
+                  name,
+                  2);
+
+PERFETTO_PB_MSG(perfetto_protos_DebugAnnotation);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  VARINT,
+                  uint64_t,
+                  name_iid,
+                  1);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  STRING,
+                  const char*,
+                  name,
+                  10);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation, VARINT, bool, bool_value, 2);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  VARINT,
+                  uint64_t,
+                  uint_value,
+                  3);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  VARINT,
+                  int64_t,
+                  int_value,
+                  4);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  FIXED64,
+                  double,
+                  double_value,
+                  5);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  VARINT,
+                  uint64_t,
+                  pointer_value,
+                  7);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  MSG,
+                  perfetto_protos_DebugAnnotation_NestedValue,
+                  nested_value,
+                  8);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  STRING,
+                  const char*,
+                  legacy_json_value,
+                  9);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  STRING,
+                  const char*,
+                  string_value,
+                  6);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  VARINT,
+                  uint64_t,
+                  string_value_iid,
+                  17);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  STRING,
+                  const char*,
+                  proto_type_name,
+                  16);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  VARINT,
+                  uint64_t,
+                  proto_type_name_iid,
+                  13);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  STRING,
+                  const char*,
+                  proto_value,
+                  14);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  MSG,
+                  perfetto_protos_DebugAnnotation,
+                  dict_entries,
+                  11);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation,
+                  MSG,
+                  perfetto_protos_DebugAnnotation,
+                  array_values,
+                  12);
+
+PERFETTO_PB_MSG(perfetto_protos_DebugAnnotation_NestedValue);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation_NestedValue,
+                  VARINT,
+                  enum perfetto_protos_DebugAnnotation_NestedValue_NestedType,
+                  nested_type,
+                  1);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation_NestedValue,
+                  STRING,
+                  const char*,
+                  dict_keys,
+                  2);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation_NestedValue,
+                  MSG,
+                  perfetto_protos_DebugAnnotation_NestedValue,
+                  dict_values,
+                  3);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation_NestedValue,
+                  MSG,
+                  perfetto_protos_DebugAnnotation_NestedValue,
+                  array_values,
+                  4);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation_NestedValue,
+                  VARINT,
+                  int64_t,
+                  int_value,
+                  5);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation_NestedValue,
+                  FIXED64,
+                  double,
+                  double_value,
+                  6);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation_NestedValue,
+                  VARINT,
+                  bool,
+                  bool_value,
+                  7);
+PERFETTO_PB_FIELD(perfetto_protos_DebugAnnotation_NestedValue,
+                  STRING,
+                  const char*,
+                  string_value,
+                  8);
+
+#endif  // INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_DEBUG_ANNOTATION_PZC_H_
diff --git a/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h b/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h
new file mode 100644
index 0000000..0069082
--- /dev/null
+++ b/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h
@@ -0,0 +1,69 @@
+/*
+ * 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 INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_TRACK_DESCRIPTOR_PZC_H_
+#define INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_TRACK_DESCRIPTOR_PZC_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "perfetto/public/pb_macros.h"
+
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeProcessDescriptor);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeThreadDescriptor);
+PERFETTO_PB_MSG_DECL(perfetto_protos_CounterDescriptor);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ProcessDescriptor);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ThreadDescriptor);
+
+PERFETTO_PB_MSG(perfetto_protos_TrackDescriptor);
+PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor, VARINT, uint64_t, uuid, 1);
+PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
+                  VARINT,
+                  uint64_t,
+                  parent_uuid,
+                  5);
+PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
+                  STRING,
+                  const char*,
+                  name,
+                  2);
+PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
+                  MSG,
+                  perfetto_protos_ProcessDescriptor,
+                  process,
+                  3);
+PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
+                  MSG,
+                  perfetto_protos_ChromeProcessDescriptor,
+                  chrome_process,
+                  6);
+PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
+                  MSG,
+                  perfetto_protos_ThreadDescriptor,
+                  thread,
+                  4);
+PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
+                  MSG,
+                  perfetto_protos_ChromeThreadDescriptor,
+                  chrome_thread,
+                  7);
+PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
+                  MSG,
+                  perfetto_protos_CounterDescriptor,
+                  counter,
+                  8);
+
+#endif  // INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_TRACK_DESCRIPTOR_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
new file mode 100644
index 0000000..5811154
--- /dev/null
+++ b/include/perfetto/public/protos/trace/track_event/track_event.pzc.h
@@ -0,0 +1,382 @@
+/*
+ * 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 INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_TRACK_EVENT_PZC_H_
+#define INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_TRACK_EVENT_PZC_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "perfetto/public/pb_macros.h"
+
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeActiveProcesses);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeApplicationStateInfo);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeCompositorSchedulerState);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeContentSettingsEventInfo);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeFrameReporter);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeHistogramSample);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeKeyedService);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeLatencyInfo);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeLegacyIpc);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeMessagePump);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeMojoEventInfo);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeRendererSchedulerState);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeUserEvent);
+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_SourceLocation);
+PERFETTO_PB_MSG_DECL(perfetto_protos_TaskExecution);
+PERFETTO_PB_MSG_DECL(perfetto_protos_TrackEvent_LegacyEvent);
+
+PERFETTO_PB_ENUM_IN_MSG(perfetto_protos_TrackEvent, Type){
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent,
+                                  TYPE_UNSPECIFIED) = 0,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent,
+                                  TYPE_SLICE_BEGIN) = 1,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent,
+                                  TYPE_SLICE_END) = 2,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent, TYPE_INSTANT) = 3,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent, TYPE_COUNTER) = 4,
+};
+
+PERFETTO_PB_ENUM_IN_MSG(perfetto_protos_TrackEvent_LegacyEvent, FlowDirection){
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent_LegacyEvent,
+                                  FLOW_UNSPECIFIED) = 0,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent_LegacyEvent,
+                                  FLOW_IN) = 1,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent_LegacyEvent,
+                                  FLOW_OUT) = 2,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent_LegacyEvent,
+                                  FLOW_INOUT) = 3,
+};
+
+PERFETTO_PB_ENUM_IN_MSG(perfetto_protos_TrackEvent_LegacyEvent,
+                        InstantEventScope){
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent_LegacyEvent,
+                                  SCOPE_UNSPECIFIED) = 0,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent_LegacyEvent,
+                                  SCOPE_GLOBAL) = 1,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent_LegacyEvent,
+                                  SCOPE_PROCESS) = 2,
+    PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TrackEvent_LegacyEvent,
+                                  SCOPE_THREAD) = 3,
+};
+
+PERFETTO_PB_MSG(perfetto_protos_EventName);
+PERFETTO_PB_FIELD(perfetto_protos_EventName, VARINT, uint64_t, iid, 1);
+PERFETTO_PB_FIELD(perfetto_protos_EventName, STRING, const char*, name, 2);
+
+PERFETTO_PB_MSG(perfetto_protos_EventCategory);
+PERFETTO_PB_FIELD(perfetto_protos_EventCategory, VARINT, uint64_t, iid, 1);
+PERFETTO_PB_FIELD(perfetto_protos_EventCategory, STRING, const char*, name, 2);
+
+PERFETTO_PB_MSG(perfetto_protos_TrackEventDefaults);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEventDefaults,
+                  VARINT,
+                  uint64_t,
+                  track_uuid,
+                  11);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEventDefaults,
+                  VARINT,
+                  uint64_t,
+                  extra_counter_track_uuids,
+                  31);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEventDefaults,
+                  VARINT,
+                  uint64_t,
+                  extra_double_counter_track_uuids,
+                  45);
+
+PERFETTO_PB_MSG(perfetto_protos_TrackEvent);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  uint64_t,
+                  category_iids,
+                  3);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  STRING,
+                  const char*,
+                  categories,
+                  22);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent, VARINT, uint64_t, name_iid, 10);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent, STRING, const char*, name, 23);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  enum perfetto_protos_TrackEvent_Type,
+                  type,
+                  9);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent, VARINT, uint64_t, track_uuid, 11);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  int64_t,
+                  counter_value,
+                  30);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  FIXED64,
+                  double,
+                  double_counter_value,
+                  44);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  uint64_t,
+                  extra_counter_track_uuids,
+                  31);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  int64_t,
+                  extra_counter_values,
+                  12);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  uint64_t,
+                  extra_double_counter_track_uuids,
+                  45);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  FIXED64,
+                  double,
+                  extra_double_counter_values,
+                  46);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  uint64_t,
+                  flow_ids_old,
+                  36);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent, FIXED64, uint64_t, flow_ids, 47);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  uint64_t,
+                  terminating_flow_ids_old,
+                  42);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  FIXED64,
+                  uint64_t,
+                  terminating_flow_ids,
+                  48);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_DebugAnnotation,
+                  debug_annotations,
+                  4);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_TaskExecution,
+                  task_execution,
+                  5);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_LogMessage,
+                  log_message,
+                  21);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeCompositorSchedulerState,
+                  cc_scheduler_state,
+                  24);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeUserEvent,
+                  chrome_user_event,
+                  25);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeKeyedService,
+                  chrome_keyed_service,
+                  26);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeLegacyIpc,
+                  chrome_legacy_ipc,
+                  27);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeHistogramSample,
+                  chrome_histogram_sample,
+                  28);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeLatencyInfo,
+                  chrome_latency_info,
+                  29);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeFrameReporter,
+                  chrome_frame_reporter,
+                  32);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeApplicationStateInfo,
+                  chrome_application_state_info,
+                  39);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeRendererSchedulerState,
+                  chrome_renderer_scheduler_state,
+                  40);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeWindowHandleEventInfo,
+                  chrome_window_handle_event_info,
+                  41);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeContentSettingsEventInfo,
+                  chrome_content_settings_event_info,
+                  43);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeActiveProcesses,
+                  chrome_active_processes,
+                  49);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_SourceLocation,
+                  source_location,
+                  33);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  uint64_t,
+                  source_location_iid,
+                  34);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeMessagePump,
+                  chrome_message_pump,
+                  35);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_ChromeMojoEventInfo,
+                  chrome_mojo_event_info,
+                  38);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  int64_t,
+                  timestamp_delta_us,
+                  1);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  int64_t,
+                  timestamp_absolute_us,
+                  16);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  int64_t,
+                  thread_time_delta_us,
+                  2);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  int64_t,
+                  thread_time_absolute_us,
+                  17);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  int64_t,
+                  thread_instruction_count_delta,
+                  8);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  VARINT,
+                  int64_t,
+                  thread_instruction_count_absolute,
+                  20);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+                  MSG,
+                  perfetto_protos_TrackEvent_LegacyEvent,
+                  legacy_event,
+                  6);
+
+PERFETTO_PB_MSG(perfetto_protos_TrackEvent_LegacyEvent);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  uint64_t,
+                  name_iid,
+                  1);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  int32_t,
+                  phase,
+                  2);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  int64_t,
+                  duration_us,
+                  3);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  int64_t,
+                  thread_duration_us,
+                  4);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  int64_t,
+                  thread_instruction_delta,
+                  15);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  uint64_t,
+                  unscoped_id,
+                  6);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  uint64_t,
+                  local_id,
+                  10);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  uint64_t,
+                  global_id,
+                  11);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  STRING,
+                  const char*,
+                  id_scope,
+                  7);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  bool,
+                  use_async_tts,
+                  9);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  uint64_t,
+                  bind_id,
+                  8);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  bool,
+                  bind_to_enclosing,
+                  12);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  enum perfetto_protos_TrackEvent_LegacyEvent_FlowDirection,
+                  flow_direction,
+                  13);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  enum perfetto_protos_TrackEvent_LegacyEvent_InstantEventScope,
+                  instant_event_scope,
+                  14);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  int32_t,
+                  pid_override,
+                  18);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent_LegacyEvent,
+                  VARINT,
+                  int32_t,
+                  tid_override,
+                  19);
+
+#endif  // INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_TRACK_EVENT_TRACK_EVENT_PZC_H_
diff --git a/protos/perfetto/trace/ftrace/scm.proto b/protos/perfetto/trace/ftrace/scm.proto
index 0613ce0..cc4e122 100644
--- a/protos/perfetto/trace/ftrace/scm.proto
+++ b/protos/perfetto/trace/ftrace/scm.proto
@@ -10,5 +10,4 @@
   optional uint64 x0 = 2;
   optional uint64 x5 = 3;
 }
-message ScmCallEndFtraceEvent {
-}
+message ScmCallEndFtraceEvent {}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 8422621..b93244d 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -7269,8 +7269,7 @@
   optional uint64 x0 = 2;
   optional uint64 x5 = 3;
 }
-message ScmCallEndFtraceEvent {
-}
+message ScmCallEndFtraceEvent {}
 
 // End of protos/perfetto/trace/ftrace/scm.proto
 
diff --git a/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc b/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
index 712e3d1..cf9654c 100644
--- a/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+++ b/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
@@ -223,9 +223,10 @@
   s += std::string("// ") + __FILE__ + "\n";
   s += "// Do not edit.\n";
   s += R"(
-#include "perfetto/protozero/proto_utils.h"
 #include "src/traced/probes/ftrace/event_info.h"
 
+#include "perfetto/protozero/proto_utils.h"
+
 namespace perfetto {
 
 using protozero::proto_utils::ProtoSchemaType;
diff --git a/src/tools/ftrace_proto_gen/proto_gen_utils.cc b/src/tools/ftrace_proto_gen/proto_gen_utils.cc
index 99c65e8..d734e88 100644
--- a/src/tools/ftrace_proto_gen/proto_gen_utils.cc
+++ b/src/tools/ftrace_proto_gen/proto_gen_utils.cc
@@ -32,12 +32,7 @@
 namespace {
 
 std::string RunClangFmt(const std::string& input) {
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_MAC)
-  const std::string platform = "mac";
-#else
-  const std::string platform = "linux64";
-#endif
-  base::Subprocess clang_fmt({"buildtools/" + platform + "/clang-format"});
+  base::Subprocess clang_fmt({"third_party/clang-format/clang-format"});
   clang_fmt.args.stdout_mode = base::Subprocess::OutputMode::kBuffer;
   clang_fmt.args.stderr_mode = base::Subprocess::OutputMode::kInherit;
   clang_fmt.args.input = input;
diff --git a/src/trace_processor/containers/row_map.h b/src/trace_processor/containers/row_map.h
index 45eedf5..a2a6a41 100644
--- a/src/trace_processor/containers/row_map.h
+++ b/src/trace_processor/containers/row_map.h
@@ -451,6 +451,12 @@
     NoVariantMatched();
   }
 
+  // Returns the data in RowMap BitVector, nullptr if RowMap is in a different
+  // mode.
+  const BitVector* GetIfBitVector() const {
+    return std::get_if<BitVector>(&data_);
+  }
+
   // Returns the iterator over the rows in this RowMap.
   Iterator IterateRows() const { return Iterator(this); }
 
diff --git a/src/trace_processor/db/overlays/null_overlay.cc b/src/trace_processor/db/overlays/null_overlay.cc
index ee27295..204f113 100644
--- a/src/trace_processor/db/overlays/null_overlay.cc
+++ b/src/trace_processor/db/overlays/null_overlay.cc
@@ -28,7 +28,7 @@
   uint32_t start = non_null_->CountSetBits(t_range.range.start);
   uint32_t end = non_null_->CountSetBits(t_range.range.end);
 
-  return StorageRange({Range(start, end)});
+  return StorageRange(start, end);
 }
 
 TableBitVector NullOverlay::MapToTableBitVector(StorageBitVector s_bv,
diff --git a/src/trace_processor/db/overlays/null_overlay_unittest.cc b/src/trace_processor/db/overlays/null_overlay_unittest.cc
index 0c02ad7..5782c3e 100644
--- a/src/trace_processor/db/overlays/null_overlay_unittest.cc
+++ b/src/trace_processor/db/overlays/null_overlay_unittest.cc
@@ -25,7 +25,7 @@
 TEST(NullOverlay, MapToStorageRangeOutsideBoundary) {
   BitVector bv{0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0};
   NullOverlay overlay(&bv);
-  StorageRange r = overlay.MapToStorageRange({RowMap::Range(1, 6)});
+  StorageRange r = overlay.MapToStorageRange(TableRange(1, 6));
 
   ASSERT_EQ(r.range.start, 0u);
   ASSERT_EQ(r.range.end, 2u);
@@ -34,7 +34,7 @@
 TEST(NullOverlay, MapToStorageRangeOnBoundary) {
   BitVector bv{0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0};
   NullOverlay overlay(&bv);
-  StorageRange r = overlay.MapToStorageRange({RowMap::Range(3, 8)});
+  StorageRange r = overlay.MapToStorageRange(TableRange(3, 8));
 
   ASSERT_EQ(r.range.start, 1u);
   ASSERT_EQ(r.range.end, 4u);
diff --git a/src/trace_processor/db/overlays/selector_overlay.h b/src/trace_processor/db/overlays/selector_overlay.h
index 1cad92c..73499b2 100644
--- a/src/trace_processor/db/overlays/selector_overlay.h
+++ b/src/trace_processor/db/overlays/selector_overlay.h
@@ -27,7 +27,7 @@
 // Overlay responsible for selecting specific rows from Storage.
 class SelectorOverlay : public StorageOverlay {
  public:
-  explicit SelectorOverlay(BitVector* selected) : selected_(selected) {}
+  explicit SelectorOverlay(const BitVector* selected) : selected_(selected) {}
 
   StorageRange MapToStorageRange(TableRange) const override;
 
@@ -44,7 +44,7 @@
   CostEstimatePerRow EstimateCostPerRow(OverlayOp) const override;
 
  private:
-  BitVector* selected_;
+  const BitVector* selected_;
 };
 
 }  // namespace overlays
diff --git a/src/trace_processor/db/overlays/selector_overlay_unittest.cc b/src/trace_processor/db/overlays/selector_overlay_unittest.cc
index 8c743e8..ccc6980 100644
--- a/src/trace_processor/db/overlays/selector_overlay_unittest.cc
+++ b/src/trace_processor/db/overlays/selector_overlay_unittest.cc
@@ -25,7 +25,7 @@
 TEST(SelectorOverlay, MapToStorageRangeFirst) {
   BitVector selector{0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1};
   SelectorOverlay overlay(&selector);
-  StorageRange r = overlay.MapToStorageRange({RowMap::Range(1, 4)});
+  StorageRange r = overlay.MapToStorageRange(TableRange(1, 4));
 
   ASSERT_EQ(r.range.start, 4u);
   ASSERT_EQ(r.range.end, 8u);
@@ -34,7 +34,7 @@
 TEST(SelectorOverlay, MapToStorageRangeSecond) {
   BitVector selector{0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0};
   SelectorOverlay overlay(&selector);
-  StorageRange r = overlay.MapToStorageRange({RowMap::Range(1, 3)});
+  StorageRange r = overlay.MapToStorageRange(TableRange(1, 3));
 
   ASSERT_EQ(r.range.start, 4u);
   ASSERT_EQ(r.range.end, 7u);
diff --git a/src/trace_processor/db/overlays/types.h b/src/trace_processor/db/overlays/types.h
index 7978ada..0ea50c5 100644
--- a/src/trace_processor/db/overlays/types.h
+++ b/src/trace_processor/db/overlays/types.h
@@ -26,11 +26,17 @@
 
 // A range of indices in the table space.
 struct TableRange {
+  TableRange(uint32_t start, uint32_t end) : range(start, end) {}
+  explicit TableRange(RowMap::Range r) : range(r) {}
+
   RowMap::Range range;
 };
 
 // A range of indices in the storage space.
 struct StorageRange {
+  StorageRange(uint32_t start, uint32_t end) : range(start, end) {}
+  explicit StorageRange(RowMap::Range r) : range(r) {}
+
   RowMap::Range range;
 };
 
diff --git a/src/trace_processor/db/query_executor.cc b/src/trace_processor/db/query_executor.cc
index 46cbd29..923e321 100644
--- a/src/trace_processor/db/query_executor.cc
+++ b/src/trace_processor/db/query_executor.cc
@@ -23,6 +23,7 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/status_or.h"
 #include "src/trace_processor/db/overlays/null_overlay.h"
+#include "src/trace_processor/db/overlays/selector_overlay.h"
 #include "src/trace_processor/db/overlays/storage_overlay.h"
 #include "src/trace_processor/db/query_executor.h"
 #include "src/trace_processor/db/storage/numeric_storage.h"
@@ -157,13 +158,13 @@
                                       const SimpleColumn& col,
                                       RowMap* rm) {
   // TODO(b/283763282): We should align these to word boundaries.
-  TableRange table_range{Range(rm->Get(0), rm->Get(rm->size() - 1) + 1)};
+  TableRange table_range(rm->Get(0), rm->Get(rm->size() - 1) + 1);
   base::SmallVector<Range, kMaxOverlayCount> overlay_bounds;
 
   for (const auto& overlay : col.overlays) {
     StorageRange storage_range = overlay->MapToStorageRange(table_range);
     overlay_bounds.emplace_back(storage_range.range);
-    table_range = TableRange({storage_range.range});
+    table_range = TableRange(storage_range.range);
   }
 
   // Use linear search algorithm on storage.
@@ -252,10 +253,10 @@
     use_legacy = use_legacy || (overlays::FilterOpToOverlayOp(c.op) ==
                                     overlays::OverlayOp::kOther &&
                                 col.type() != c.value.type);
-    use_legacy = use_legacy ||
-                 col.overlay().row_map().size() != col.storage_base().size();
     use_legacy = use_legacy || col.IsSorted() || col.IsDense() || col.IsSetId();
-    use_legacy = use_legacy || col.overlay().row_map().IsIndexVector();
+    use_legacy =
+        use_legacy || (col.overlay().size() != col.storage_base().size() &&
+                       !col.overlay().row_map().IsBitVector());
     if (use_legacy) {
       col.FilterInto(c.op, c.value, &rm);
       continue;
@@ -265,9 +266,14 @@
     uint32_t s_size = col.storage_base().non_null_size();
 
     storage::NumericStorage storage(s_data, s_size, col.col_type());
-    overlays::NullOverlay null_overlay(col.storage_base().bv());
-
     SimpleColumn s_col{OverlaysVec(), &storage};
+
+    overlays::SelectorOverlay selector_overlay(
+        col.overlay().row_map().GetIfBitVector());
+    if (col.overlay().size() != col.storage_base().size())
+      s_col.overlays.emplace_back(&selector_overlay);
+
+    overlays::NullOverlay null_overlay(col.storage_base().bv());
     if (col.IsNullable()) {
       s_col.overlays.emplace_back(&null_overlay);
     }
diff --git a/src/trace_processor/db/query_executor_benchmark.cc b/src/trace_processor/db/query_executor_benchmark.cc
index 55a7251..9024a50 100644
--- a/src/trace_processor/db/query_executor_benchmark.cc
+++ b/src/trace_processor/db/query_executor_benchmark.cc
@@ -20,6 +20,8 @@
 #include "perfetto/ext/base/string_utils.h"
 #include "src/base/test/utils.h"
 #include "src/trace_processor/db/query_executor.h"
+#include "src/trace_processor/db/table.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/tables/slice_tables_py.h"
 
 namespace perfetto {
@@ -27,6 +29,24 @@
 namespace {
 
 using SliceTable = tables::SliceTable;
+using ThreadTrackTable = tables::ThreadTrackTable;
+using ExpectedFrameTimelineSliceTable = tables::ExpectedFrameTimelineSliceTable;
+using RawTable = tables::RawTable;
+using FtraceEventTable = tables::FtraceEventTable;
+
+// `SELECT * FROM SLICE` on android_monitor_contention_trace.at
+static char kSliceTable[] = "test/data/slice_table_for_benchmarks.csv";
+
+// `SELECT * FROM SLICE` on android_monitor_contention_trace.at
+static char kExpectedFrameTimelineTable[] =
+    "test/data/expected_frame_timeline_for_benchmarks.csv";
+
+// `SELECT id, cpu FROM raw` on chrome_android_systrace.pftrace.
+static char kRawTable[] = "test/data/raw_cpu_for_benchmarks.csv";
+
+// `SELECT id, cpu FROM ftrace_event` on chrome_android_systrace.pftrace.
+static char kFtraceEventTable[] =
+    "test/data/ftrace_event_cpu_for_benchmarks.csv";
 
 enum DB { V1, V2 };
 
@@ -51,12 +71,10 @@
   return output;
 }
 
-std::vector<SliceTable::Row> LoadRowsFromCSVToSliceTable(
-    benchmark::State& state) {
-  std::vector<SliceTable::Row> rows;
+std::vector<std::string> ReadCSV(benchmark::State& state,
+                                 std::string file_name) {
   std::string table_csv;
-  static const char kTestTrace[] = "test/data/example_android_trace_30s.csv";
-  perfetto::base::ReadFile(perfetto::base::GetTestDataPath(kTestTrace),
+  perfetto::base::ReadFile(perfetto::base::GetTestDataPath(file_name),
                            &table_csv);
   if (table_csv.empty()) {
     state.SkipWithError(
@@ -65,44 +83,112 @@
     return {};
   }
   PERFETTO_CHECK(!table_csv.empty());
-
-  std::vector<std::string> rows_strings = base::SplitString(table_csv, "\n");
-  for (size_t i = 1; i < rows_strings.size(); ++i) {
-    std::vector<std::string> row_vec = SplitCSVLine(rows_strings[i]);
-    SliceTable::Row row;
-    PERFETTO_CHECK(row_vec.size() >= 12);
-    row.ts = *base::StringToInt64(row_vec[2]);
-    row.dur = *base::StringToInt64(row_vec[3]);
-    row.track_id =
-        tables::ThreadTrackTable::Id(*base::StringToUInt32(row_vec[4]));
-    row.depth = *base::StringToUInt32(row_vec[7]);
-    row.stack_id = *base::StringToInt32(row_vec[8]);
-    row.parent_stack_id = *base::StringToInt32(row_vec[9]);
-    row.parent_id = base::StringToUInt32(row_vec[11]).has_value()
-                        ? std::make_optional<SliceTable::Id>(
-                              *base::StringToUInt32(row_vec[11]))
-                        : std::nullopt;
-    row.arg_set_id = *base::StringToUInt32(row_vec[11]);
-    row.thread_ts = base::StringToInt64(row_vec[12]);
-    row.thread_dur = base::StringToInt64(row_vec[13]);
-    rows.emplace_back(row);
-  }
-  return rows;
+  return base::SplitString(table_csv, "\n");
 }
 
-struct BenchmarkSliceTable {
-  explicit BenchmarkSliceTable(benchmark::State& state) : table_{&pool_} {
-    auto rows = LoadRowsFromCSVToSliceTable(state);
-    for (uint32_t i = 0; i < rows.size(); ++i) {
-      table_.Insert(rows[i]);
+SliceTable::Row GetSliceTableRow(std::string string_row) {
+  std::vector<std::string> row_vec = SplitCSVLine(string_row);
+  SliceTable::Row row;
+  PERFETTO_CHECK(row_vec.size() >= 12);
+  row.ts = *base::StringToInt64(row_vec[2]);
+  row.dur = *base::StringToInt64(row_vec[3]);
+  row.track_id = ThreadTrackTable::Id(*base::StringToUInt32(row_vec[4]));
+  row.depth = *base::StringToUInt32(row_vec[7]);
+  row.stack_id = *base::StringToInt32(row_vec[8]);
+  row.parent_stack_id = *base::StringToInt32(row_vec[9]);
+  row.parent_id = base::StringToUInt32(row_vec[11]).has_value()
+                      ? std::make_optional<SliceTable::Id>(
+                            *base::StringToUInt32(row_vec[11]))
+                      : std::nullopt;
+  row.arg_set_id = *base::StringToUInt32(row_vec[11]);
+  row.thread_ts = base::StringToInt64(row_vec[12]);
+  row.thread_dur = base::StringToInt64(row_vec[13]);
+  return row;
+}
+
+struct SliceTableForBenchmark {
+  explicit SliceTableForBenchmark(benchmark::State& state) : table_{&pool_} {
+    std::vector<std::string> rows_strings = ReadCSV(state, kSliceTable);
+
+    for (size_t i = 1; i < rows_strings.size(); ++i) {
+      table_.Insert(GetSliceTableRow(rows_strings[i]));
     }
   }
+
   StringPool pool_;
   SliceTable table_;
 };
 
-void SliceTableBenchmark(benchmark::State& state,
-                         BenchmarkSliceTable& table,
+struct ExpectedFrameTimelineTableForBenchmark {
+  explicit ExpectedFrameTimelineTableForBenchmark(benchmark::State& state)
+      : table_{&pool_, &parent_} {
+    std::vector<std::string> table_rows_as_string =
+        ReadCSV(state, kExpectedFrameTimelineTable);
+    std::vector<std::string> parent_rows_as_string =
+        ReadCSV(state, kSliceTable);
+
+    uint32_t cur_idx = 0;
+    for (size_t i = 1; i < table_rows_as_string.size(); ++i, ++cur_idx) {
+      std::vector<std::string> row_vec = SplitCSVLine(table_rows_as_string[i]);
+
+      uint32_t idx = *base::StringToUInt32(row_vec[0]);
+      while (cur_idx < idx) {
+        parent_.Insert(GetSliceTableRow(parent_rows_as_string[cur_idx + 1]));
+        cur_idx++;
+      }
+
+      ExpectedFrameTimelineSliceTable::Row row;
+      row.ts = *base::StringToInt64(row_vec[2]);
+      row.dur = *base::StringToInt64(row_vec[3]);
+      row.track_id = ThreadTrackTable::Id(*base::StringToUInt32(row_vec[4]));
+      row.depth = *base::StringToUInt32(row_vec[7]);
+      row.stack_id = *base::StringToInt32(row_vec[8]);
+      row.parent_stack_id = *base::StringToInt32(row_vec[9]);
+      row.parent_id = base::StringToUInt32(row_vec[11]).has_value()
+                          ? std::make_optional<SliceTable::Id>(
+                                *base::StringToUInt32(row_vec[11]))
+                          : std::nullopt;
+      row.arg_set_id = *base::StringToUInt32(row_vec[11]);
+      row.thread_ts = base::StringToInt64(row_vec[12]);
+      row.thread_dur = base::StringToInt64(row_vec[13]);
+      table_.Insert(row);
+    }
+  }
+  StringPool pool_;
+  SliceTable parent_{&pool_};
+  ExpectedFrameTimelineSliceTable table_;
+};
+
+struct FtraceEventTableForBenchmark {
+  explicit FtraceEventTableForBenchmark(benchmark::State& state) {
+    std::vector<std::string> raw_rows = ReadCSV(state, kRawTable);
+    std::vector<std::string> ftrace_event_rows =
+        ReadCSV(state, kFtraceEventTable);
+
+    uint32_t cur_idx = 0;
+    for (size_t i = 1; i < ftrace_event_rows.size(); ++i, cur_idx++) {
+      std::vector<std::string> row_vec = SplitCSVLine(ftrace_event_rows[i]);
+      uint32_t idx = *base::StringToUInt32(row_vec[0]);
+      while (cur_idx < idx) {
+        std::vector<std::string> raw_row = SplitCSVLine(raw_rows[cur_idx + 1]);
+        RawTable::Row r;
+        r.cpu = *base::StringToUInt32(raw_row[1]);
+        raw_.Insert(r);
+        cur_idx++;
+      }
+      FtraceEventTable::Row row;
+      row.cpu = *base::StringToUInt32(row_vec[1]);
+      table_.Insert(row);
+    }
+  }
+
+  StringPool pool_;
+  RawTable raw_{&pool_};
+  tables::FtraceEventTable table_{&pool_, &raw_};
+};
+
+void BenchmarkSliceTable(benchmark::State& state,
+                         SliceTableForBenchmark& table,
                          Constraint c) {
   Table::kUseFilterV2 = state.range(0) == 1;
   for (auto _ : state) {
@@ -114,26 +200,66 @@
                              benchmark::Counter::kInvert);
 }
 
-static void BM_DBv2SliceTableTrackIdEquals(benchmark::State& state) {
-  BenchmarkSliceTable table(state);
-  SliceTableBenchmark(state, table, table.table_.track_id().eq(100));
+void BenchmarkExpectedFrameTable(benchmark::State& state,
+                                 ExpectedFrameTimelineTableForBenchmark& table,
+                                 Constraint c) {
+  Table::kUseFilterV2 = state.range(0) == 1;
+  for (auto _ : state) {
+    benchmark::DoNotOptimize(table.table_.FilterToRowMap({c}));
+  }
+  state.counters["s/row"] =
+      benchmark::Counter(static_cast<double>(table.table_.row_count()),
+                         benchmark::Counter::kIsIterationInvariantRate |
+                             benchmark::Counter::kInvert);
 }
 
-BENCHMARK(BM_DBv2SliceTableTrackIdEquals)->ArgsProduct({{DB::V1, DB::V2}});
-
-static void BM_DBv2SliceTableParentIdIsNotNull(benchmark::State& state) {
-  BenchmarkSliceTable table(state);
-  SliceTableBenchmark(state, table, table.table_.parent_id().is_not_null());
+void BenchmarkFtraceEventTable(benchmark::State& state,
+                               FtraceEventTableForBenchmark& table,
+                               Constraint c) {
+  Table::kUseFilterV2 = state.range(0) == 1;
+  for (auto _ : state) {
+    benchmark::DoNotOptimize(table.table_.FilterToRowMap({c}));
+  }
+  state.counters["s/row"] =
+      benchmark::Counter(static_cast<double>(table.table_.row_count()),
+                         benchmark::Counter::kIsIterationInvariantRate |
+                             benchmark::Counter::kInvert);
 }
 
-BENCHMARK(BM_DBv2SliceTableParentIdIsNotNull)->ArgsProduct({{DB::V1, DB::V2}});
-
-static void BM_DBv2SliceTableParentIdEq(benchmark::State& state) {
-  BenchmarkSliceTable table(state);
-  SliceTableBenchmark(state, table, table.table_.parent_id().eq(88));
+static void BM_QESliceTableTrackIdEq(benchmark::State& state) {
+  SliceTableForBenchmark table(state);
+  BenchmarkSliceTable(state, table, table.table_.track_id().eq(100));
 }
 
-BENCHMARK(BM_DBv2SliceTableParentIdEq)->ArgsProduct({{DB::V1, DB::V2}});
+BENCHMARK(BM_QESliceTableTrackIdEq)->ArgsProduct({{DB::V1, DB::V2}});
+
+static void BM_QESliceTableParentIdIsNotNull(benchmark::State& state) {
+  SliceTableForBenchmark table(state);
+  BenchmarkSliceTable(state, table, table.table_.parent_id().is_not_null());
+}
+
+BENCHMARK(BM_QESliceTableParentIdIsNotNull)->ArgsProduct({{DB::V1, DB::V2}});
+
+static void BM_QESliceTableParentIdEq(benchmark::State& state) {
+  SliceTableForBenchmark table(state);
+  BenchmarkSliceTable(state, table, table.table_.parent_id().eq(88));
+}
+
+BENCHMARK(BM_QESliceTableParentIdEq)->ArgsProduct({{DB::V1, DB::V2}});
+
+static void BM_QEFilterWithSparseSelector(benchmark::State& state) {
+  ExpectedFrameTimelineTableForBenchmark table(state);
+  BenchmarkExpectedFrameTable(state, table, table.table_.track_id().eq(88));
+}
+
+BENCHMARK(BM_QEFilterWithSparseSelector)->ArgsProduct({{DB::V1, DB::V2}});
+
+static void BM_QEFilterWithDenseSelector(benchmark::State& state) {
+  FtraceEventTableForBenchmark table(state);
+  BenchmarkFtraceEventTable(state, table, table.table_.cpu().eq(4));
+}
+
+BENCHMARK(BM_QEFilterWithDenseSelector)->ArgsProduct({{DB::V1, DB::V2}});
 
 }  // namespace
 }  // namespace trace_processor
diff --git a/src/trace_processor/db/query_executor_unittest.cc b/src/trace_processor/db/query_executor_unittest.cc
index 01fff7a..fa914c0 100644
--- a/src/trace_processor/db/query_executor_unittest.cc
+++ b/src/trace_processor/db/query_executor_unittest.cc
@@ -224,61 +224,63 @@
 }
 
 TEST(QueryExecutor, SingleConstraintWithNullAndSelector) {
-  std::vector<int64_t> storage_data{0, 1, 2, 3, 4, 0, 1, 2, 3, 4};
+  std::vector<int64_t> storage_data{0, 1, 2, 3, 0, 1, 2, 3};
   NumericStorage storage(storage_data.data(), 10, ColumnType::kInt64);
 
-  // Select 6 elements from storage, resulting in a vector {0, 1, 3, 4, 1, 2}.
-  BitVector selector_bv{1, 1, 0, 1, 1, 0, 1, 1, 0, 0};
-  SelectorOverlay selector_overlay(&selector_bv);
-
-  // Add nulls, final vector {0, 1, NULL, 3, 4, NULL, 1, 2, NULL}.
-  BitVector null_bv{1, 1, 0, 1, 1, 0, 1, 1, 0};
+  // Current vector
+  // 0, 1, NULL, 2, 3, 0, NULL, NULL, 1, 2, 3, NULL
+  BitVector null_bv{1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0};
   NullOverlay null_overlay(&null_bv);
 
+  // Final vector
+  // 0, NULL, 3, NULL, 1, 3
+  BitVector selector_bv{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0};
+  SelectorOverlay selector_overlay(&selector_bv);
+
   // Create the column.
   OverlaysVec overlays_vec;
-  overlays_vec.emplace_back(&null_overlay);
   overlays_vec.emplace_back(&selector_overlay);
+  overlays_vec.emplace_back(&null_overlay);
   SimpleColumn col{overlays_vec, &storage};
 
   // Filter.
   Constraint c{0, FilterOp::kGe, SqlValue::Long(2)};
-  QueryExecutor exec({col}, 9);
+  QueryExecutor exec({col}, 6);
   RowMap res = exec.Filter({c});
 
-  ASSERT_EQ(res.size(), 3u);
-  ASSERT_EQ(res.Get(0), 3u);
-  ASSERT_EQ(res.Get(1), 4u);
-  ASSERT_EQ(res.Get(2), 7u);
+  ASSERT_EQ(res.size(), 2u);
+  ASSERT_EQ(res.Get(0), 2u);
+  ASSERT_EQ(res.Get(1), 5u);
 }
 
 TEST(QueryExecutor, IsNull) {
-  std::vector<int64_t> storage_data{0, 1, 2, 3, 4, 0, 1, 2, 3, 4};
+  std::vector<int64_t> storage_data{0, 1, 2, 3, 0, 1, 2, 3};
   NumericStorage storage(storage_data.data(), 10, ColumnType::kInt64);
 
-  // Select 6 elements from storage, resulting in a vector {0, 1, 3, 4, 1, 2}.
-  BitVector selector_bv{1, 1, 0, 1, 1, 0, 1, 1, 0, 0};
-  SelectorOverlay selector_overlay(&selector_bv);
-
-  // Add nulls, final vector {0, 1, NULL, 3, 4, NULL, 1, 2, NULL}.
-  BitVector null_bv{1, 1, 0, 1, 1, 0, 1, 1, 0};
+  // Current vector
+  // 0, 1, NULL, 2, 3, 0, NULL, NULL, 1, 2, 3, NULL
+  BitVector null_bv{1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0};
   NullOverlay null_overlay(&null_bv);
 
+  // Final vector
+  // 0, NULL, 3, NULL, 1, 3
+  BitVector selector_bv{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0};
+  SelectorOverlay selector_overlay(&selector_bv);
+
   // Create the column.
   OverlaysVec overlays_vec;
-  overlays_vec.emplace_back(&null_overlay);
   overlays_vec.emplace_back(&selector_overlay);
+  overlays_vec.emplace_back(&null_overlay);
   SimpleColumn col{overlays_vec, &storage};
 
   // Filter.
   Constraint c{0, FilterOp::kIsNull, SqlValue::Long(0)};
-  QueryExecutor exec({col}, 9);
+  QueryExecutor exec({col}, 6);
   RowMap res = exec.Filter({c});
 
-  ASSERT_EQ(res.size(), 3u);
-  ASSERT_EQ(res.Get(0), 2u);
-  ASSERT_EQ(res.Get(1), 5u);
-  ASSERT_EQ(res.Get(2), 8u);
+  ASSERT_EQ(res.size(), 2u);
+  ASSERT_EQ(res.Get(0), 1u);
+  ASSERT_EQ(res.Get(1), 3u);
 }
 
 }  // namespace
diff --git a/src/trace_processor/stdlib/chrome/chrome_scroll_janks.sql b/src/trace_processor/stdlib/chrome/chrome_scroll_janks.sql
index 9f0f29e..b12e6a7 100644
--- a/src/trace_processor/stdlib/chrome/chrome_scroll_janks.sql
+++ b/src/trace_processor/stdlib/chrome/chrome_scroll_janks.sql
@@ -13,11 +13,11 @@
 -- limitations under the License.
 
 -- TODO(b/286187288): Move this dependency to stdlib.
-SELECT RUN_METRIC('chrome/event_latency_scroll_jank_cause.sql');
+SELECT RUN_METRIC('chrome/scroll_jank_v3.sql');
 SELECT IMPORT('common.slices');
 
 -- Selects EventLatency slices that correspond with janks in a scroll. This is
--- based on the V2 version of scroll jank metrics.
+-- based on the V3 version of scroll jank metrics.
 --
 -- @column id INT                     The slice id.
 -- @column ts INT                     The start timestamp of the slice.
@@ -28,36 +28,38 @@
 --                                    the jank.
 -- @column sub_cause_of_jank STRING   The stage of cause_of_jank that caused the
 --                                    jank.
-CREATE TABLE chrome_janky_event_latencies_v2 AS
-  SELECT
+-- @column frame_jank_ts INT          The start timestamp where frame
+--                                    frame presentation was delayed.
+-- @column frame_jank_dur INT         The duration in ms of the delay in frame
+--                                    presentation.
+CREATE TABLE chrome_janky_event_latencies_v3 AS
+SELECT
     s.id,
     s.ts,
     s.dur,
     s.track_id,
     s.name,
     e.cause_of_jank,
-    e.sub_cause_of_jank
+    e.sub_cause_of_jank,
+    CAST(s.ts + s.dur - ((e.delay_since_last_frame - e.vsync_interval) * 1e6) AS INT) AS frame_jank_ts,
+    CAST((e.delay_since_last_frame - e.vsync_interval) * 1e6 AS INT) AS frame_jank_dur
 FROM slice s
-JOIN event_latency_scroll_jank_cause e
-  ON s.id = e.slice_id
-WHERE
-  HAS_DESCENDANT_SLICE_WITH_NAME(
-    s.id,
-    'SubmitCompositorFrameToPresentationCompositorFrame');
+JOIN chrome_janky_frames e
+  ON s.id = e. event_latency_id;
 
 -- Defines slices for all of janky scrolling intervals in a trace.
 --
 -- @column id            The unique identifier of the janky interval.
 -- @column ts            The start timestamp of the janky interval.
 -- @column dur           The duration of the janky interval.
-CREATE TABLE chrome_scroll_jank_intervals_v2 AS
+CREATE TABLE chrome_scroll_jank_intervals_v3 AS
 -- Sub-table to retrieve all janky slice timestamps. Ordering calculations are
 -- based on timestamps rather than durations.
 WITH janky_latencies AS (
   SELECT
-    s.ts AS start_ts,
-    s.ts + s.dur AS end_ts
-  FROM chrome_janky_event_latencies_v2 s),
+    s.frame_jank_ts AS start_ts,
+    s.frame_jank_ts + s.frame_jank_dur AS end_ts
+  FROM chrome_janky_event_latencies_v3 s),
 -- Determine the local maximum timestamp for janks thus far; this will allow
 -- us to coalesce all earlier events up to the maximum.
 ordered_jank_end_ts AS (
diff --git a/src/trace_processor/stdlib/experimental/slices.sql b/src/trace_processor/stdlib/experimental/slices.sql
index 95f1510..c09a849 100644
--- a/src/trace_processor/stdlib/experimental/slices.sql
+++ b/src/trace_processor/stdlib/experimental/slices.sql
@@ -13,6 +13,8 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
+SELECT IMPORT('common.slices');
+
 -- All slices with related process and thread info if available. Unlike
 -- `thread_slice` and `process_slice`, this view contains all slices,
 -- with thread- and process-related columns set to NULL if the slice
@@ -64,4 +66,92 @@
 LEFT JOIN thread USING (utid)
 LEFT JOIN process process1 ON thread.upid = process1.upid
 LEFT JOIN process_track ON slice.track_id = process_track.id
-LEFT JOIN process process2 ON process_track.upid = process2.upid;
\ No newline at end of file
+LEFT JOIN process process2 ON process_track.upid = process2.upid;
+
+-- The concept of a "flat slice" is to take the data in the slice table and
+-- remove all notion of nesting; we do this by projecting every slice in a stack to
+-- their ancestor slice, i.e at any point in time, taking the  most specific active
+-- slice (i.e. the slice at the bottom of the stack) and representing that as the
+-- *only* slice that was running during that period.
+--
+-- This concept becomes very useful when you try and linearise a trace and
+-- compare it with other traces spanning the same user action; "self time" (i.e.
+-- time spent in a slice but *not* any children) is easily computed and span
+-- joins with thread state become possible without limiting to only depth zero
+--- slices.
+--
+-- Note that, no slices will be generated for intervals without without any slices.
+--
+-- As an example, consider the following slice stack:
+-- A-------------B.
+-- ----C----D----.
+-- The flattened slice will be: A----C----D----B.
+--
+-- @column slice_id           Id of most active slice.
+-- @column ts                 Timestamp when `slice.id` became the most active slice.
+-- @column dur                Duration of `slice.id` as the most active slice until the next active slice.
+-- @column depth              Depth of `slice.id` in the original stack.
+-- @column name               Name of `slice.id`.
+-- @column root_name          Name of the top most slice of the stack.
+-- @column root_id            Id of of the top most slice of the stack.
+-- @column track_id           Alias for `slice.track_id`.
+-- @column utid               Alias for `thread.utid`.
+-- @column tid                Alias for `thread.tid`
+-- @column thread_name        Alias for `thread.name`.
+-- @column upid               Alias for `process.upid`.
+-- @column pid                Alias for `process.pid`.
+-- @column process_name       Alias for `process.name`.
+CREATE TABLE experimental_slice_flattened
+AS
+-- The algorithm proceeds as follows:
+-- 1. Find the start and end timestamps of all slices.
+-- 2. Iterate the generated timestamps within a stack in chronoligical order.
+-- 3. Generate a slice for each timestamp pair (regardless of if it was a start or end)  .
+-- 4. If the first timestamp in the pair was originally a start, the slice is the 'current' slice,
+-- otherwise, the slice is the parent slice.
+WITH
+  begins AS (
+    SELECT id AS slice_id, ts, name, track_id, depth
+    FROM slice
+    WHERE dur != -1
+  ),
+  ends AS (
+    SELECT
+      COALESCE(parent.id, current.id) AS slice_id,
+      current.ts + current.dur AS ts,
+      COALESCE(parent.name, current.name) AS name,
+      current.track_id,
+      COALESCE(parent.depth, 0) AS depth
+    FROM slice current
+    LEFT JOIN slice parent
+      ON current.parent_id = parent.id
+    WHERE current.dur != -1
+  ),
+  events AS (
+    SELECT * FROM begins
+    UNION ALL
+    SELECT * FROM ends
+  ),
+  data AS (
+    SELECT
+      events.slice_id,
+      events.ts,
+      LEAD(events.ts)
+        OVER (PARTITION BY COALESCE(anc.id, events.slice_id) ORDER BY events.ts) - events.ts AS dur,
+      events.depth,
+      events.name,
+      COALESCE(anc.name, events.name) AS root_name,
+      COALESCE(anc.id, events.slice_id) AS root_id,
+      events.track_id,
+      thread_slice.utid,
+      thread_slice.tid,
+      thread_slice.thread_name,
+      thread_slice.upid,
+      thread_slice.pid,
+      thread_slice.process_name
+    FROM events
+    LEFT JOIN ANCESTOR_SLICE(events.slice_id) anc
+      ON anc.depth = 0
+    JOIN thread_slice ON thread_slice.id = events.slice_id
+  )
+SELECT * FROM data WHERE ts IS NOT NULL AND dur IS NOT NULL AND name IS NOT NULL;
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index 6a8cca3..a223464 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -19,6 +19,7 @@
 // Do not edit.
 
 #include "src/traced/probes/ftrace/event_info.h"
+
 #include "perfetto/protozero/proto_utils.h"
 
 namespace perfetto {
diff --git a/test/trace_processor/diff_tests/slices/tests.py b/test/trace_processor/diff_tests/slices/tests.py
index 9a73cce..4142127 100644
--- a/test/trace_processor/diff_tests/slices/tests.py
+++ b/test/trace_processor/diff_tests/slices/tests.py
@@ -138,3 +138,25 @@
         "end_ts"
         174797566610797
         """))
+
+  def test_slice_flattened(self):
+    return DiffTestBlueprint(
+        trace=DataPath('chrome_input_with_frame_view.pftrace'),
+        query="""
+        SELECT import('experimental.slices');
+
+        SELECT name, root_name, ts, dur, depth, thread_name, tid, process_name, pid
+        FROM experimental_slice_flattened WHERE tid = 30944;
+      """,
+        out=Csv("""
+        "name","root_name","ts","dur","depth","thread_name","tid","process_name","pid"
+        "ThreadControllerImpl::RunTask","ThreadControllerImpl::RunTask",174793737042797,3937000,0,"CrBrowserMain",30944,"Browser",30944
+        "ThreadControllerImpl::RunTask","ThreadControllerImpl::RunTask",174793741016797,5930000,0,"CrBrowserMain",30944,"Browser",30944
+        "ThreadControllerImpl::RunTask","ThreadControllerImpl::RunTask",174793747000797,47000,0,"CrBrowserMain",30944,"Browser",30944
+        "Receive mojo message","ThreadControllerImpl::RunTask",174793747047797,136000,1,"CrBrowserMain",30944,"Browser",30944
+        "ThreadControllerImpl::RunTask","ThreadControllerImpl::RunTask",174793747183797,17000,0,"CrBrowserMain",30944,"Browser",30944
+        "Looper.dispatch: android.os.Handler(Kx3@57873a8)","Looper.dispatch: android.os.Handler(Kx3@57873a8)",174793747546797,119000,0,"CrBrowserMain",30944,"Browser",30944
+        "ThreadControllerImpl::RunTask","ThreadControllerImpl::RunTask",174796099970797,186000,0,"CrBrowserMain",30944,"Browser",30944
+        "Looper.dispatch: jy3(null)","Looper.dispatch: jy3(null)",174800056530797,1368000,0,"CrBrowserMain",30944,"Browser",30944
+        "ThreadControllerImpl::RunTask","ThreadControllerImpl::RunTask",174800107962797,132000,0,"CrBrowserMain",30944,"Browser",30944
+      """))
diff --git a/tools/install-build-deps b/tools/install-build-deps
index c72eae9..50cb1ae 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -25,6 +25,7 @@
 import tempfile
 import time
 import zipfile
+import bz2
 
 from collections import namedtuple
 from platform import system, machine
@@ -35,7 +36,7 @@
 # |source_url| can be either a git repo or a http url.
 # If a git repo, |checksum| is the SHA1 committish that will be checked out.
 # If a http url, |checksum| is the SHA256 of the downloaded file.
-# If the url is a .zip or .tgz file it will be automatically deflated under
+# If the url is a .zip, .tgz, or .tbz2 file it will be automatically deflated under
 # |target_folder|, taking care of stripping the root folder if it's a single
 # root (to avoid ending up with buildtools/protobuf/protobuf-1.2.3/... and have
 # instead just buildtools/protobuf).
@@ -100,30 +101,38 @@
         'windows', 'x64'),
 
     # clang-format
-    # From https://chromium.googlesource.com/chromium/src/buildtools/+/refs/heads/master/mac/clang-format.sha1
+    # From
+    # https://chromium.googlesource.com/chromium/src/buildtools/+/refs/heads/master/mac/clang-format.arm64.sha1
     Dependency(
         'third_party/clang-format/clang-format',
-        'https://storage.googleapis.com/chromium-clang-format/62bde1baa7196ad9df969fc1f06b66360b1a927b',
-        '6df686a937443cbe6efc013467a7ba5f98d3f187eb7765bb7abc6ce47626cf66',
-        'darwin', 'all'),
+        'https://storage.googleapis.com/chromium-clang-format/5553d7a3d912b7d49381ad68c9a56740601a57a0',
+        'e077990b2ea6807f6abc71b4cf1e487719d7e40574baddd2b51187fdcc8db803',
+        'darwin', 'arm64'),
+    # From
+    # https://chromium.googlesource.com/chromium/src/buildtools/+/refs/heads/master/mac/clang-format.x64.sha1
+    Dependency(
+        'third_party/clang-format/clang-format',
+        'https://storage.googleapis.com/chromium-clang-format/87d69aeff220c916b85b5d6d162fa5668aa9d64f',
+        '71179a8788a009cfd589636d50f0eb9f95f58b0cfda4351430bed7c0a48c080b',
+        'darwin', 'x64'),
     # From https://chromium.googlesource.com/chromium/src/buildtools/+/refs/heads/master/linux64/clang-format.sha1
     Dependency(
         'third_party/clang-format/clang-format',
-        'https://storage.googleapis.com/chromium-clang-format/1baf0089e895c989a311b6a38ed94d0e8be4c0a7',
-        'd02a97a87e8c28898033aaf5986967b24dc47ebd5b376e1cd93e5009f22cd75e',
+        'https://storage.googleapis.com/chromium-clang-format/1facab3101fc6da6b9467543f27f0622b966bc19',
+        '5e459118d8ac825452e9e1f2717e4de5a36399dc6cc6aec7ec483ad27a0c927e',
         'linux', 'x64'),
     # From https://chromium.googlesource.com/chromium/src/buildtools/+/refs/heads/master/win/clang-format.exe.sha1
     Dependency(
         'third_party/clang-format/clang-format.exe',
-        'https://storage.googleapis.com/chromium-clang-format/d4afd4eba27022f5f6d518133aebde57281677c9',
-        '2ba1b4d3ade90ea80316890b598ab5fc16777572be26afec6ce23117da121b80',
+        'https://storage.googleapis.com/chromium-clang-format/2e569921b9884daf157021d6ae9e21991cd6cf81',
+        '2227376ada89ea832995b832222b722a27c4d5d8d59e9c4d7842877f99a1e30d',
         'windows', 'x64'),
 
     # Keep the SHA1 in sync with |clang_format_rev| in chromium //buildtools/DEPS.
     Dependency(
         'buildtools/clang_format/script',
-        'https://chromium.googlesource.com/chromium/llvm-project/cfe/tools/clang-format.git',
-        '96636aa0e9f047f17447f2d45a094d0b59ed7917', 'all', 'all'),
+        'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/clang/tools/clang-format.git',
+        'f97059df7f8b205064625cdb5f97b56668a125ef', 'all', 'all'),
 
     # Ninja
     Dependency(
@@ -650,12 +659,12 @@
       deps_updated |= CheckoutGitRepo(local_path, dep.source_url, dep.checksum,
                                       args.check_only)
       continue
-    is_zip = local_path.endswith('.zip') or local_path.endswith('.tgz')
-    zip_target_dir = local_path[:-4] if is_zip else None
-    zip_dir_stamp = os.path.join(zip_target_dir, '.stamp') if is_zip else None
+    is_compressed = any([local_path.endswith(ext) for ext in ['.zip', '.tgz', '.tbz2']])
+    compressed_target_dir = os.path.splitext(local_path)[0] if is_compressed else None
+    compressed_dir_stamp = os.path.join(compressed_target_dir, '.stamp') if is_compressed else None
 
-    if ((not is_zip and HashLocalFile(local_path) == dep.checksum) or
-        (is_zip and ReadFile(zip_dir_stamp) == dep.checksum)):
+    if ((not is_compressed and HashLocalFile(local_path) == dep.checksum) or
+        (is_compressed and ReadFile(compressed_dir_stamp) == dep.checksum)):
       continue
     deps_updated = True
     if args.check_only:
@@ -678,32 +687,39 @@
 
     assert (HashLocalFile(local_path) == dep.checksum)
 
-    if is_zip:
-      logging.info('Extracting %s into %s' % (local_path, zip_target_dir))
-      assert (os.path.commonprefix((ROOT_DIR, zip_target_dir)) == ROOT_DIR)
-      RmtreeIfExists(zip_target_dir)
+    if is_compressed:
+      logging.info('Extracting %s into %s' % (local_path, compressed_target_dir))
+      assert (os.path.commonprefix((ROOT_DIR, compressed_target_dir)) == ROOT_DIR)
+      RmtreeIfExists(compressed_target_dir)
 
       # Decompress the archive.
       if local_path.endswith('.tgz'):
-        MkdirRecursive(zip_target_dir)
-        subprocess.check_call(['tar', '-oxf', local_path], cwd=zip_target_dir)
+        MkdirRecursive(compressed_target_dir)
+        subprocess.check_call(['tar', '-oxf', local_path], cwd=compressed_target_dir)
       elif local_path.endswith('.zip'):
         with zipfile.ZipFile(local_path, 'r') as zf:
           for info in zf.infolist():
-            ExtractZipfilePreservePermissions(zf, info, zip_target_dir)
+            ExtractZipfilePreservePermissions(zf, info, compressed_target_dir)
+      elif local_path.endswith('.tbz2'):
+        tar_path = '{}.tar.tmp'.format(local_path)
+        with open(tar_path, 'w') as f:
+          with bz2.open(local_path, 'r') as bf:
+            f.write(bf.read())
+        MkdirRecursive(compressed_target_dir)
+        subprocess.check_call(['tar', '-oxf', tar_path], cwd=compressed_target_dir)
 
       # If the zip contains one root folder, rebase one level up moving all
       # its sub files and folders inside |target_dir|.
-      subdir = os.listdir(zip_target_dir)
+      subdir = os.listdir(compressed_target_dir)
       if len(subdir) == 1:
-        subdir = os.path.join(zip_target_dir, subdir[0])
+        subdir = os.path.join(compressed_target_dir, subdir[0])
         if os.path.isdir(subdir):
           for subf in os.listdir(subdir):
-            shutil.move(os.path.join(subdir, subf), zip_target_dir)
+            shutil.move(os.path.join(subdir, subf), compressed_target_dir)
           os.rmdir(subdir)
 
       # Create stamp and remove the archive.
-      with open(zip_dir_stamp, 'w') as stamp_file:
+      with open(compressed_dir_stamp, 'w') as stamp_file:
         stamp_file.write(dep.checksum)
       os.remove(local_path)
 
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 08c2ac6..94a9c0e 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -219,7 +219,8 @@
   }
 
   td {
-    padding: 2px 1px;
+    padding: 3px 5px;
+    white-space: nowrap;
 
     i.material-icons {
       // Shrink the icons inside the table cells to increase the information
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 640b30e..95ed422 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -26,20 +26,21 @@
 @import "flags_page";
 @import "hiring_banner";
 @import "widgets_page";
+@import "widgets/anchor";
 @import "widgets/button";
 @import "widgets/checkbox";
-@import "widgets/text_input";
-@import "widgets/empty_state";
-@import "widgets/anchor";
-@import "widgets/popup";
-@import "widgets/multiselect";
-@import "widgets/select";
-@import "widgets/menu";
-@import "widgets/spinner";
-@import "widgets/tree";
-@import "widgets/switch";
-@import "widgets/form";
 @import "widgets/details_shell";
+@import "widgets/empty_state";
+@import "widgets/error";
+@import "widgets/form";
 @import "widgets/grid_layout";
+@import "widgets/menu";
+@import "widgets/multiselect";
+@import "widgets/popup";
 @import "widgets/section";
 @import "widgets/timestamp";
+@import "widgets/select";
+@import "widgets/spinner";
+@import "widgets/switch";
+@import "widgets/text_input";
+@import "widgets/tree";
diff --git a/ui/src/assets/widgets/error.scss b/ui/src/assets/widgets/error.scss
new file mode 100644
index 0000000..dfee07c
--- /dev/null
+++ b/ui/src/assets/widgets/error.scss
@@ -0,0 +1,20 @@
+// 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.
+
+.pf-error {
+  padding: 20px 10px;
+  color: hsl(-10, 50%, 50%);
+  font-family: $pf-font;
+  font-weight: 300;
+}
diff --git a/ui/src/base/array_utils.ts b/ui/src/base/array_utils.ts
index a3a9980..7236a43 100644
--- a/ui/src/base/array_utils.ts
+++ b/ui/src/base/array_utils.ts
@@ -31,3 +31,12 @@
 export function allUnique(x: string[]): boolean {
   return x.length == new Set(x).size;
 }
+
+export function arrayEquals(a: any[]|undefined, b: any[]|undefined): boolean {
+  if (a === undefined || b === undefined) return false;
+  if (a.length !== b.length) return false;
+  for (let i = 0; i < a.length; i++) {
+    if (a[i] !== b[i]) return false;
+  }
+  return true;
+}
diff --git a/ui/src/common/queries.ts b/ui/src/common/queries.ts
index 1d63819..bf2518a 100644
--- a/ui/src/common/queries.ts
+++ b/ui/src/common/queries.ts
@@ -28,8 +28,14 @@
   statementWithOutputCount: number;
 }
 
+export interface QueryRunParams {
+  // If true, replaces nulls with "NULL" string. Default is true.
+  convertNullsToString?: boolean;
+}
+
 export async function runQuery(
-    sqlQuery: string, engine: EngineProxy): Promise<QueryResponse> {
+    sqlQuery: string, engine: EngineProxy, params?: QueryRunParams):
+    Promise<QueryResponse> {
   const startMs = performance.now();
   const queryRes = engine.query(sqlQuery);
 
@@ -47,6 +53,8 @@
     // errored, the frontend will show a graceful message instead.
   }
 
+  const convertNullsToString = params?.convertNullsToString ?? true;
+
   const durationMs = performance.now() - startMs;
   const rows: Row[] = [];
   const columns = queryRes.columns();
@@ -55,7 +63,7 @@
     const row: Row = {};
     for (const colName of columns) {
       const value = iter.get(colName);
-      row[colName] = value === null ? 'NULL' : value;
+      row[colName] = value === null && convertNullsToString ? 'NULL' : value;
     }
     rows.push(row);
     if (++numRows >= MAX_DISPLAY_ROWS) break;
diff --git a/ui/src/common/query_result.ts b/ui/src/common/query_result.ts
index 90fb8f4..9a9883c 100644
--- a/ui/src/common/query_result.ts
+++ b/ui/src/common/query_result.ts
@@ -71,6 +71,7 @@
 export const LONG_NULL: bigint|null = 1n;
 
 export type ColumnType = string|number|bigint|null|Uint8Array;
+export type SqlValue = ColumnType;
 
 const SHIFT_32BITS = 32n;
 
@@ -159,7 +160,7 @@
 
 // One row extracted from an SQL result:
 export interface Row {
-  [key: string]: ColumnType|undefined;
+  [key: string]: ColumnType;
 }
 
 // The methods that any iterator has to implement.
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 8e50c57..d9ad58b 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -67,6 +67,7 @@
   INPUT_LATENCY_TRACK,
 } from '../tracks/scroll_jank';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state';
+import {THREAD_STATE_TRACK_V2_KIND} from '../tracks/thread_state_v2';
 
 const TRACKS_V2_FLAG = featureFlags.register({
   id: 'tracksV2.1',
@@ -75,6 +76,22 @@
   defaultValue: false,
 });
 
+const TRACKS_V2_COMPARE_FLAG = featureFlags.register({
+  id: 'tracksV2Compare',
+  name: 'Tracks V2: Also show V1 tracks',
+  description:
+      'Show V1 tracks side by side with V2 tracks. Does nothing if TracksV2 is not enabled.',
+  defaultValue: false,
+});
+
+function showV2(): boolean {
+  return TRACKS_V2_FLAG.get();
+}
+
+function showV1(): boolean {
+  return !showV2() || (showV2() && TRACKS_V2_COMPARE_FLAG.get());
+}
+
 const MEM_DMA_COUNTER_NAME = 'mem.dma_heap';
 const MEM_DMA = 'mem.dma_buffer';
 const MEM_ION = 'mem.ion';
@@ -931,18 +948,38 @@
         // the track creation as well.
         continue;
       }
-      const kind = THREAD_STATE_TRACK_KIND;
-      this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind,
-        name: TrackDecider.getTrackName({utid, tid, threadName, kind}),
-        trackGroup: uuid,
-        trackSortKey: {
-          utid,
-          priority: InThreadTrackSortKey.THREAD_SCHEDULING_STATE_TRACK,
-        },
-        config: {utid, tid},
-      });
+
+      const priority = InThreadTrackSortKey.THREAD_SCHEDULING_STATE_TRACK;
+
+      if (showV1()) {
+        const kind = THREAD_STATE_TRACK_KIND;
+        this.tracksToAdd.push({
+          engineId: this.engineId,
+          kind: THREAD_STATE_TRACK_KIND,
+          name: TrackDecider.getTrackName({utid, tid, threadName, kind}),
+          trackGroup: uuid,
+          trackSortKey: {
+            utid,
+            priority,
+          },
+          config: {utid, tid},
+        });
+      }
+
+      if (showV2()) {
+        const kind = THREAD_STATE_TRACK_V2_KIND;
+        this.tracksToAdd.push({
+          engineId: this.engineId,
+          kind,
+          name: TrackDecider.getTrackName({utid, tid, threadName, kind}),
+          trackGroup: uuid,
+          trackSortKey: {
+            utid,
+            priority,
+          },
+          config: {utid, tid},
+        });
+      }
     }
   }
 
@@ -1272,25 +1309,27 @@
       const kind = SLICE_TRACK_KIND;
       const name = TrackDecider.getTrackName(
           {name: trackName, utid, tid, threadName, kind});
-      this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind,
-        name,
-        trackGroup: uuid,
-        trackSortKey: {
-          utid,
-          priority: isDefaultTrackForScope ?
-              InThreadTrackSortKey.DEFAULT_TRACK :
-              InThreadTrackSortKey.ORDINARY,
-        },
-        config: {
-          trackId,
-          maxDepth,
-          tid,
-        },
-      });
+      if (showV1()) {
+        this.tracksToAdd.push({
+          engineId: this.engineId,
+          kind,
+          name,
+          trackGroup: uuid,
+          trackSortKey: {
+            utid,
+            priority: isDefaultTrackForScope ?
+                InThreadTrackSortKey.DEFAULT_TRACK :
+                InThreadTrackSortKey.ORDINARY,
+          },
+          config: {
+            trackId,
+            maxDepth,
+            tid,
+          },
+        });
+      }
 
-      if (TRACKS_V2_FLAG.get()) {
+      if (showV2()) {
         this.tracksToAdd.push({
           engineId: this.engineId,
           kind: 'GenericSliceTrack',
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index a941a31..2e610d6 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -193,6 +193,11 @@
   // there are at most |depth| slices.
   private incomplete = new Array<CastInternal<T['slice']>>();
 
+  // The currently selected slice.
+  // TODO(hjd): We should fetch this from the underlying data rather
+  // than just remembering it when we see it.
+  private selectedSlice?: CastInternal<T['slice']>;
+
   protected readonly tableName: string;
   private maxDurNs: TPDuration = 0n;
 
@@ -271,6 +276,9 @@
     // Give a chance to the embedder to change colors and other stuff.
     this.onUpdatedSlices(this.slices);
     this.onUpdatedSlices(this.incomplete);
+    if (this.selectedSlice !== undefined) {
+      this.onUpdatedSlices([this.selectedSlice]);
+    }
   }
 
   protected isSelectionHandled(selection: Selection): boolean {
@@ -283,6 +291,16 @@
     return supportedSelectionKinds.includes(selection.kind);
   }
 
+  private getTitleFont(): string {
+    const size = this.sliceLayout.titleSizePx ?? 12;
+    return `${size}px Roboto Condensed`;
+  }
+
+  private getSubtitleFont(): string {
+    const size = this.sliceLayout.subtitleSizePx ?? 8;
+    return `${size}px Roboto Condensed`;
+  }
+
   renderCanvas(ctx: CanvasRenderingContext2D): void {
     // TODO(hjd): fonts and colors should come from the CSS and not hardcoded
     // here.
@@ -307,7 +325,7 @@
     let charWidth = this.charWidth;
     if (charWidth < 0) {
       // TODO(hjd): Centralize font measurement/invalidation.
-      ctx.font = '12px Roboto Condensed';
+      ctx.font = this.getTitleFont();
       charWidth = this.charWidth = ctx.measureText('dbpqaouk').width / 8;
     }
 
@@ -318,22 +336,21 @@
         vizTime.start.toTPTime('floor'), vizTime.end.toTPTime('ceil'));
 
     let selection = globals.state.currentSelection;
-
     if (!selection || !this.isSelectionHandled(selection)) {
       selection = null;
     }
+    const selectedId = selection ? (selection as {id: number}).id : undefined;
+    let discoveredSelection: CastInternal<T['slice']>|undefined;
 
     // Believe it or not, doing 4xO(N) passes is ~2x faster than trying to draw
     // everything in one go. The key is that state changes operations on the
     // canvas (e.g., color, fonts) dominate any number crunching we do in JS.
 
-    this.updateSliceAndTrackHeight();
     const sliceHeight = this.computedSliceHeight;
     const padding = this.sliceLayout.padding;
     const rowSpacing = this.computedRowSpacing;
 
     // First pass: compute geometry of slices.
-    let selSlice: CastInternal<T['slice']>|undefined;
 
     // pxEnd is the last visible pixel in the visible viewport. Drawing
     // anything < 0 or > pxEnd doesn't produce any visible effect as it goes
@@ -370,8 +387,8 @@
         slice.w = sliceVizLimit - slice.x;
       }
 
-      if (selection && (selection as {id: number}).id === slice.id) {
-        selSlice = slice;
+      if (selectedId === slice.id) {
+        discoveredSelection = slice;
       }
     }
 
@@ -399,7 +416,7 @@
     // Third pass, draw the titles (e.g., process name for sched slices).
     ctx.fillStyle = '#fff';
     ctx.textAlign = 'center';
-    ctx.font = '12px Roboto Condensed';
+    ctx.font = this.getTitleFont();
     ctx.textBaseline = 'middle';
     for (const slice of vizSlices) {
       if ((slice.flags & SLICE_FLAGS_INSTANT) || !slice.title ||
@@ -411,13 +428,13 @@
       const rectXCenter = slice.x + slice.w / 2;
       const y = padding + slice.depth * (sliceHeight + rowSpacing);
       const yDiv = slice.subTitle ? 3 : 2;
-      const yMidPoint = Math.floor(y + sliceHeight / yDiv) - 0.5;
+      const yMidPoint = Math.floor(y + sliceHeight / yDiv) + 0.5;
       ctx.fillText(title, rectXCenter, yMidPoint);
     }
 
     // Fourth pass, draw the subtitles (e.g., thread name for sched slices).
     ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
-    ctx.font = '10px Roboto Condensed';
+    ctx.font = this.getSubtitleFont();
     for (const slice of vizSlices) {
       if (slice.w < SLICE_MIN_WIDTH_FOR_TEXT_PX || !slice.subTitle ||
           (slice.flags & SLICE_FLAGS_INSTANT)) {
@@ -430,16 +447,24 @@
       ctx.fillText(subTitle, rectXCenter, yMidPoint);
     }
 
+    // Update the selectedSlice if required.
+    if (discoveredSelection !== undefined) {
+      this.selectedSlice = discoveredSelection;
+    } else if (selectedId === undefined) {
+      this.selectedSlice = undefined;
+    }
+
     // Draw a thicker border around the selected slice (or chevron).
-    if (selSlice !== undefined) {
-      const color = selSlice.color;
-      const y = padding + selSlice.depth * (sliceHeight + rowSpacing);
+    if (this.selectedSlice !== undefined) {
+      const slice = this.selectedSlice;
+      const color = slice.color;
+      const y = padding + slice.depth * (sliceHeight + rowSpacing);
       ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`;
       ctx.beginPath();
       const THICKNESS = 3;
       ctx.lineWidth = THICKNESS;
       ctx.strokeRect(
-          selSlice.x, y - THICKNESS / 2, selSlice.w, sliceHeight + THICKNESS);
+          slice.x, y - THICKNESS / 2, slice.w, sliceHeight + THICKNESS);
       ctx.closePath();
     }
 
@@ -457,7 +482,7 @@
     // The only thing this does is drawing the sched latency arrow. We should
     // have some abstraction for that arrow (ideally the same we'd use for
     // flows).
-    this.drawSchedLatencyArrow(ctx, selSlice);
+    this.drawSchedLatencyArrow(ctx, this.selectedSlice);
 
     // If a slice is hovered, draw the tooltip.
     const tooltip = this.hoverTooltip;
@@ -762,9 +787,14 @@
 
   private getVisibleSlicesInternal(start: TPTime, end: TPTime):
       Array<CastInternal<T['slice']>> {
-    const slices =
+    let slices =
         filterVisibleSlices<CastInternal<T['slice']>>(this.slices, start, end);
-    return slices.concat(this.incomplete);
+    slices = slices.concat(this.incomplete);
+    // The selected slice is always visible:
+    if (this.selectedSlice && !this.slices.includes(this.selectedSlice)) {
+      slices.push(this.selectedSlice);
+    }
+    return slices;
   }
 
   private updateSliceAndTrackHeight() {
diff --git a/ui/src/frontend/bottom_tab.ts b/ui/src/frontend/bottom_tab.ts
index f78bd0c..e192969 100644
--- a/ui/src/frontend/bottom_tab.ts
+++ b/ui/src/frontend/bottom_tab.ts
@@ -118,6 +118,10 @@
       void;
   abstract viewTab(): void|m.Children;
 
+  close(): void {
+    closeTab(this.uuid);
+  }
+
   createPanelVnode(): m.Vnode<any, any> {
     return m(
         BottomTabAdapter,
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 4dd2fdf..54bfb5d 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -21,19 +21,27 @@
 import {runQuery} from '../common/queries';
 import {LONG, LONG_NULL, NUM, STR_NULL} from '../common/query_result';
 import {
+  formatDuration,
   TPDuration,
   TPTime,
 } from '../common/time';
 import {ArgNode, convertArgsToTree, Key} from '../controller/args_parser';
 
 import {Anchor} from './anchor';
-import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from './bottom_tab';
+import {
+  addTab,
+  BottomTab,
+  bottomTabRegistry,
+  NewBottomTabArgs,
+} from './bottom_tab';
 import {FlowPoint, globals} from './globals';
 import {PanelSize} from './panel';
 import {runQueryInNewTab} from './query_result_tab';
 import {Icons} from './semantic_icons';
 import {Arg} from './sql/args';
 import {getSlice, SliceDetails, SliceRef} from './sql/slice';
+import {SqlTableTab} from './sql_table/tab';
+import {SqlTables} from './sql_table/well_known_tables';
 import {asSliceSqlId, asTPTimestamp} from './sql_types';
 import {getProcessName, getThreadName} from './thread_and_process_info';
 import {Button} from './widgets/button';
@@ -266,7 +274,7 @@
 function computeDuration(ts: TPTime, dur: TPDuration): m.Children {
   if (dur === -1n) {
     const minDuration = globals.state.traceTime.end - ts;
-    return [m(Duration, {dur: minDuration}), ' (Did not end)'];
+    return `${formatDuration(minDuration)} (Did not end)`;
   } else {
     return m(Duration, {dur});
   }
@@ -398,7 +406,28 @@
         {title: 'Details'},
         m(
             Tree,
-            m(TreeNode, {left: 'Name', right: slice.name}),
+            m(TreeNode, {
+              left: 'Name',
+              right: m(
+                  PopupMenu2,
+                  {
+                    trigger: m(Anchor, slice.name),
+                  },
+                  m(MenuItem, {
+                    label: 'Slices with the same name',
+                    onclick: () => {
+                      addTab({
+                        kind: SqlTableTab.kind,
+                        config: {
+                          table: SqlTables.slice,
+                          displayName: 'slice',
+                          filters: [`name = ${sqliteString(slice.name)}`],
+                        },
+                      });
+                    },
+                  }),
+                  ),
+            }),
             m(TreeNode, {
               left: 'Category',
               right: !slice.category || slice.category === '[NULL]' ?
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index be1f6cd..dfa4e0f 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -578,12 +578,16 @@
     this._ftracePanelData = data;
   }
 
-  makeSelection(action: DeferredAction<{}>, tabToOpen = 'current_selection') {
+  makeSelection(
+      action: DeferredAction<{}>, tab: string|null = 'current_selection') {
     const previousState = this.state;
     // A new selection should cancel the current search selection.
     globals.dispatch(Actions.setSearchIndex({index: -1}));
-    const tab = action.type === 'deselect' ? undefined : tabToOpen;
-    globals.dispatch(Actions.setCurrentTab({tab}));
+    if (action.type === 'deselect') {
+      globals.dispatch(Actions.setCurrentTab({tab: undefined}));
+    } else if (tab !== null) {
+      globals.dispatch(Actions.setCurrentTab({tab: tab}));
+    }
     globals.dispatch(action);
 
     // HACK(stevegolton + altimin): This is a workaround to allow passing the
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index 4c3b1db..19fde6a 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -24,7 +24,6 @@
 
 import {
   OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
-  SIDEBAR_WIDTH,
   TRACK_SHELL_WIDTH,
 } from './css_constants';
 import {BorderDragStrategy} from './drag/border_drag_strategy';
@@ -179,19 +178,17 @@
     if (this.gesture === undefined || this.gesture.isDragging) {
       return;
     }
-    (e.target as HTMLElement).style.cursor = this.chooseCursor(e.x);
+    (e.target as HTMLElement).style.cursor = this.chooseCursor(e.offsetX);
   }
 
   private chooseCursor(x: number) {
     if (this.timeScale === undefined) return 'default';
-    const [vizStartPx, vizEndPx] =
+    const [startBound, endBound] =
         OverviewTimelinePanel.extractBounds(this.timeScale);
-    const startBound = vizStartPx - 1 + SIDEBAR_WIDTH;
-    const endBound = vizEndPx + SIDEBAR_WIDTH;
     if (OverviewTimelinePanel.inBorderRange(x, startBound) ||
         OverviewTimelinePanel.inBorderRange(x, endBound)) {
       return 'ew-resize';
-    } else if (x < SIDEBAR_WIDTH + TRACK_SHELL_WIDTH) {
+    } else if (x < TRACK_SHELL_WIDTH) {
       return 'default';
     } else if (x < startBound || endBound < x) {
       return 'crosshair';
diff --git a/ui/src/frontend/semantic_icons.ts b/ui/src/frontend/semantic_icons.ts
index a80acca..da6326e 100644
--- a/ui/src/frontend/semantic_icons.ts
+++ b/ui/src/frontend/semantic_icons.ts
@@ -19,4 +19,12 @@
   static readonly ContextMenu = 'arrow_drop_down';  // Could be 'more_vert'
   static readonly Copy = 'content_copy';
   static readonly Delete = 'delete';
+  static readonly SortedAsc = 'arrow_upward';
+  static readonly SortedDesc = 'arrow_downward';
+  static readonly GoBack = 'chevron_left';
+  static readonly GoForward = 'chevron_right';
+  static readonly AddColumn = 'add';
+  static readonly Close = 'close';
+  static readonly Hide = 'visibility_off';
+  static readonly Filter = 'filter_list';
 }
diff --git a/ui/src/frontend/slice_layout.ts b/ui/src/frontend/slice_layout.ts
index 1f28617..ca655bd 100644
--- a/ui/src/frontend/slice_layout.ts
+++ b/ui/src/frontend/slice_layout.ts
@@ -20,6 +20,8 @@
   // We have a optimization for when maxDepth - minDepth == 1 so it is useful
   // to set this correctly:
   maxDepth: number;
+  titleSizePx?: number;
+  subtitleSizePx?: number;
 }
 
 export const SLICE_LAYOUT_BASE_DEFAULTS: SliceLayoutBase = Object.freeze({
@@ -66,7 +68,9 @@
   minDepth: 0,
   maxDepth: 1,
   heightMode: 'FIXED',
-  fixedHeight: 30,
+  fixedHeight: 18,
+  titleSizePx: 10,
+  padding: 3,
 });
 
 export type SliceLayout =
diff --git a/ui/src/frontend/sql/slice.ts b/ui/src/frontend/sql/slice.ts
index d29706b..fa9c5d6 100644
--- a/ui/src/frontend/sql/slice.ts
+++ b/ui/src/frontend/sql/slice.ts
@@ -185,10 +185,16 @@
   readonly ts: TPTimestamp;
   readonly dur: TPDuration;
   readonly sqlTrackId: number;
+
+  // Whether clicking on the reference should change the current tab
+  // to "current selection" tab in addition to updating the selection
+  // and changing the viewport. True by default.
+  readonly switchToCurrentSelectionTab?: boolean;
 }
 
 export class SliceRef implements m.ClassComponent<SliceRefAttrs> {
   view(vnode: m.Vnode<SliceRefAttrs>) {
+    const switchTab = vnode.attrs.switchToCurrentSelectionTab ?? true;
     return m(
         Anchor,
         {
@@ -201,8 +207,10 @@
             // Clamp duration to 1 - i.e. for instant events
             const dur = BigintMath.max(1n, vnode.attrs.dur);
             focusHorizontalRange(vnode.attrs.ts, vnode.attrs.ts + dur);
-            globals.makeSelection(Actions.selectChromeSlice(
-                {id: vnode.attrs.id, trackId: uiTrackId, table: 'slice'}));
+            globals.makeSelection(
+                Actions.selectChromeSlice(
+                    {id: vnode.attrs.id, trackId: uiTrackId, table: 'slice'}),
+                switchTab ? 'current_selection' : null);
           },
         },
         vnode.attrs.name);
diff --git a/ui/src/frontend/sql_table/argument_selector.ts b/ui/src/frontend/sql_table/argument_selector.ts
new file mode 100644
index 0000000..0189244
--- /dev/null
+++ b/ui/src/frontend/sql_table/argument_selector.ts
@@ -0,0 +1,85 @@
+// 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.
+
+import m from 'mithril';
+
+import {EngineProxy} from '../../common/engine';
+import {STR} from '../../common/query_result';
+import {globals} from '../globals';
+import {constraintsToQueryFragment} from '../sql_utils';
+import {FilterableSelect} from '../widgets/select';
+import {Spinner} from '../widgets/spinner';
+
+import {argColumn} from './column';
+import {ArgSetIdColumn} from './table_description';
+
+const MAX_ARGS_TO_DISPLAY = 15;
+
+interface ArgumentSelectorAttrs {
+  engine: EngineProxy;
+  argSetId: ArgSetIdColumn;
+  tableName: string;
+  filters: string[];
+  // List of aliases for existing columns by the table.
+  alreadySelectedColumns: Set<string>;
+  onArgumentSelected: (argument: string) => void;
+}
+
+// A widget which allows the user to select a new argument to display.
+// Dinamically queries Trace Processor to find the relevant set of arg_set_ids
+// and which args are present in these arg sets.
+export class ArgumentSelector implements
+    m.ClassComponent<ArgumentSelectorAttrs> {
+  argList?: string[];
+
+  constructor({attrs}: m.Vnode<ArgumentSelectorAttrs>) {
+    this.load(attrs);
+  }
+
+  private async load(attrs: ArgumentSelectorAttrs) {
+    const queryResult = await attrs.engine.query(`
+      -- Encapsulate the query in a CTE to avoid clashes between filters
+      -- and columns of the 'args' table.
+      WITH arg_sets AS (
+        SELECT DISTINCT ${attrs.argSetId.name} as arg_set_id
+        FROM ${attrs.tableName}
+        ${constraintsToQueryFragment({
+      filters: attrs.filters,
+    })}
+      )
+      SELECT
+        DISTINCT args.key as key
+      FROM arg_sets
+      JOIN args USING (arg_set_id)
+    `);
+    this.argList = [];
+    const it = queryResult.iter({key: STR});
+    for (; it.valid(); it.next()) {
+      const arg = argColumn(attrs.argSetId, it.key);
+      if (attrs.alreadySelectedColumns.has(arg.alias)) continue;
+      this.argList.push(it.key);
+    }
+    globals.rafScheduler.scheduleFullRedraw();
+  }
+
+  view({attrs}: m.Vnode<ArgumentSelectorAttrs>) {
+    if (this.argList === undefined) return m(Spinner);
+    return m(FilterableSelect, {
+      values: this.argList,
+      onSelected: (value: string) => attrs.onArgumentSelected(value),
+      maxDisplayedItems: MAX_ARGS_TO_DISPLAY,
+      autofocusInput: true,
+    });
+  }
+}
diff --git a/ui/src/frontend/sql_table/column.ts b/ui/src/frontend/sql_table/column.ts
new file mode 100644
index 0000000..a57a85b
--- /dev/null
+++ b/ui/src/frontend/sql_table/column.ts
@@ -0,0 +1,66 @@
+// 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.
+
+import {sqliteString} from '../../base/string_utils';
+
+import {
+  ArgSetIdColumn,
+  dependendentColumns,
+  DisplayConfig,
+  RegularSqlTableColumn,
+} from './table_description';
+
+// This file contains the defintions of different column types that can be
+// displayed in the table viewer.
+
+export interface Column {
+  // SQL expression calculating the value of this column.
+  expression: string;
+  // Unique name for this column.
+  // The relevant bit of SQL fetching this column will be ${expression} as
+  // ${alias}.
+  alias: string;
+  // Title to be displayed in the table header.
+  title: string;
+  // How the value of this column should be rendered.
+  display?: DisplayConfig;
+}
+
+export function columnFromSqlTableColumn(c: RegularSqlTableColumn): Column {
+  return {
+    expression: c.name,
+    alias: c.name,
+    title: c.title || c.name,
+    display: c.display,
+  };
+}
+
+export function argColumn(c: ArgSetIdColumn, argName: string): Column {
+  const escape = (name: string) => name.replace(/\.|\[|\]/g, '_');
+  return {
+    expression: `extract_arg(${c.name}, ${sqliteString(argName)}`,
+    alias: `_arg_${c.name}_${escape(argName)}`,
+    title: `${c.title ?? c.name} ${argName}`,
+  };
+}
+
+// Returns a list of projections (i.e. parts of the SELECT clause) that should
+// be added to the query fetching the data to be able to display the given
+// column (e.g. `foo` or `f(bar) as baz`).
+// Some table columns are backed by multiple SQL columns (e.g. slice_id is
+// backed by id, ts, dur and track_id), so we need to return a list.
+export function sqlProjectionsForColumn(column: Column): string[] {
+  return [`${column.expression} as ${column.alias}`].concat(
+      dependendentColumns(column.display).map((c) => `${c} as ${c}`));
+}
diff --git a/ui/src/frontend/sql_table/render_cell.ts b/ui/src/frontend/sql_table/render_cell.ts
new file mode 100644
index 0000000..4fdc99b
--- /dev/null
+++ b/ui/src/frontend/sql_table/render_cell.ts
@@ -0,0 +1,206 @@
+// 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.
+
+import m from 'mithril';
+
+import {sqliteString} from '../../base/string_utils';
+import {Row, SqlValue} from '../../common/query_result';
+import {formatDuration, TPTime} from '../../common/time';
+import {Anchor} from '../anchor';
+import {copyToClipboard} from '../clipboard';
+import {Icons} from '../semantic_icons';
+import {SliceRef} from '../sql/slice';
+import {asSliceSqlId, asTPTimestamp} from '../sql_types';
+import {sqlValueToString} from '../sql_utils';
+import {Err} from '../widgets/error';
+import {MenuItem, PopupMenu2} from '../widgets/menu';
+import {renderTimecode} from '../widgets/timestamp';
+
+import {Column} from './column';
+import {SqlTableState} from './state';
+import {SliceIdDisplayConfig} from './table_description';
+
+// This file is responsible for rendering a value in a given sell based on the
+// column type.
+
+function filterOptionMenuItem(
+    label: string, filter: string, state: SqlTableState): m.Child {
+  return m(MenuItem, {
+    label,
+    onclick: () => {
+      state.addFilter(filter);
+    },
+  });
+}
+
+function getStandardFilters(
+    c: Column, value: SqlValue, state: SqlTableState): m.Child[] {
+  if (value === null) {
+    return [
+      filterOptionMenuItem('is null', `${c.expression} is null`, state),
+      filterOptionMenuItem('is not null', `${c.expression} is not null`, state),
+    ];
+  }
+  if (typeof value === 'string') {
+    return [
+      filterOptionMenuItem(
+          'equals to', `${c.expression} = ${sqliteString(value)}`, state),
+      filterOptionMenuItem(
+          'not equals to', `${c.expression} != ${sqliteString(value)}`, state),
+    ];
+  }
+  if (typeof value === 'bigint' || typeof value === 'number') {
+    return [
+      filterOptionMenuItem('equals to', `${c.expression} = ${value}`, state),
+      filterOptionMenuItem(
+          'not equals to', `${c.expression} != ${value}`, state),
+      filterOptionMenuItem('greater than', `${c.expression} > ${value}`, state),
+      filterOptionMenuItem(
+          'greater or equals than', `${c.expression} >= ${value}`, state),
+      filterOptionMenuItem('less than', `${c.expression} < ${value}`, state),
+      filterOptionMenuItem(
+          'less or equals than', `${c.expression} <= ${value}`, state),
+    ];
+  }
+  return [];
+}
+
+function displayValue(value: SqlValue): m.Child {
+  if (value === null) {
+    return m('i', 'NULL');
+  }
+  return sqlValueToString(value);
+}
+
+function displayTimestamp(value: SqlValue): m.Children {
+  if (typeof value !== 'bigint') return displayValue(value);
+  return renderTimecode(asTPTimestamp(value));
+}
+
+function displayDuration(value: TPTime): string;
+function displayDuration(value: SqlValue): m.Children;
+function displayDuration(value: SqlValue): m.Children {
+  if (typeof value !== 'bigint') return displayValue(value);
+  return formatDuration(value);
+}
+
+function display(column: Column, row: Row): m.Children {
+  const value = row[column.alias];
+
+  // Handle all cases when we have non-trivial formatting.
+  switch (column.display?.type) {
+    case 'timestamp':
+      return displayTimestamp(value);
+    case 'duration':
+    case 'thread_duration':
+      return displayDuration(value);
+  }
+
+  return displayValue(value);
+}
+
+function copyMenuItem(label: string, value: string): m.Child {
+  return m(MenuItem, {
+    icon: Icons.Copy,
+    label,
+    onclick: () => {
+      copyToClipboard(value);
+    },
+  });
+}
+
+function getContextMenuItems(
+    column: Column, row: Row, state: SqlTableState): m.Child[] {
+  const result: m.Child[] = [];
+  const value = row[column.alias];
+
+  if (column.display?.type === 'timestamp' && typeof value === 'bigint') {
+    result.push(copyMenuItem('Copy raw timestamp', `${value}`));
+    // result.push(
+    //    copyMenuItem('Copy formatted timestamp', displayTimestamp(value)));
+  }
+  if ((column.display?.type === 'duration' ||
+       column.display?.type === 'thread_duration') &&
+      typeof value === 'bigint') {
+    result.push(copyMenuItem('Copy raw duration', `${value}`));
+    result.push(
+        copyMenuItem('Copy formatted duration', displayDuration(value)));
+  }
+  if (typeof value === 'string') {
+    result.push(copyMenuItem('Copy', value));
+  }
+
+  const filters = getStandardFilters(column, value, state);
+  if (filters.length > 0) {
+    result.push(
+        m(MenuItem, {label: 'Add filter', icon: Icons.Filter}, ...filters));
+  }
+
+  return result;
+}
+
+function renderSliceIdColumn(
+    column: {alias: string, display: SliceIdDisplayConfig},
+    row: Row): m.Children {
+  const config = column.display;
+  const id = row[column.alias];
+  const ts = row[config.ts];
+  const dur = row[config.dur] === null ? -1n : row[config.dur];
+  const trackId = row[config.trackId];
+
+  const columnNotFoundError = (type: string, name: string) =>
+      m(Err, `${type} column ${name} not found`);
+  const wrongTypeError = (type: string, name: string, value: SqlValue) =>
+      m(Err,
+        `Wrong type for ${type} column ${name}: bigint expected, ${
+            typeof value} found`);
+
+  if (typeof id !== 'bigint') return sqlValueToString(id);
+  if (ts === undefined) return columnNotFoundError('Timestamp', config.ts);
+  if (typeof ts !== 'bigint') return wrongTypeError('timestamp', config.ts, ts);
+  if (dur === undefined) return columnNotFoundError('Duration', config.dur);
+  if (typeof dur !== 'bigint') {
+    return wrongTypeError('duration', config.dur, ts);
+  }
+  if (trackId === undefined) return columnNotFoundError('Track id', trackId);
+  if (typeof trackId !== 'bigint') {
+    return wrongTypeError('track id', config.trackId, trackId);
+  }
+
+  return m(SliceRef, {
+    id: asSliceSqlId(Number(id)),
+    name: `${id}`,
+    ts: asTPTimestamp(ts as bigint),
+    dur: dur,
+    sqlTrackId: Number(trackId),
+    switchToCurrentSelectionTab: false,
+  });
+}
+
+export function renderCell(
+    column: Column, row: Row, state: SqlTableState): m.Children {
+  if (column.display && column.display.type === 'slice_id') {
+    return renderSliceIdColumn(
+        {alias: column.alias, display: column.display}, row);
+  }
+  const displayValue = display(column, row);
+  const contextMenuItems: m.Child[] = getContextMenuItems(column, row, state);
+  return m(
+      PopupMenu2,
+      {
+        trigger: m(Anchor, displayValue),
+      },
+      ...contextMenuItems,
+  );
+}
diff --git a/ui/src/frontend/sql_table/state.ts b/ui/src/frontend/sql_table/state.ts
new file mode 100644
index 0000000..81fd2e7
--- /dev/null
+++ b/ui/src/frontend/sql_table/state.ts
@@ -0,0 +1,295 @@
+// 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.
+
+import {arrayEquals} from '../../base/array_utils';
+import {SortDirection} from '../../base/comparison_utils';
+import {EngineProxy} from '../../common/engine';
+import {NUM, Row} from '../../common/query_result';
+import {globals} from '../globals';
+import {constraintsToQueryFragment} from '../sql_utils';
+import {
+  Column,
+  columnFromSqlTableColumn,
+  sqlProjectionsForColumn,
+} from './column';
+import {SqlTableDescription} from './table_description';
+
+interface ColumnOrderClause {
+  // We only allow the table to be sorted by the columns which are displayed to
+  // the user to avoid confusion, so we use a reference to the underlying Column
+  // here and compare it by reference down the line.
+  column: Column;
+  direction: SortDirection;
+}
+
+const ROW_LIMIT = 100;
+
+// Result of the execution of the query.
+interface Data {
+  // Rows to show, including pagination.
+  rows: Row[];
+  error?: string;
+}
+
+interface RowCount {
+  // Total number of rows in view, excluding the pagination.
+  // Undefined if the query returned an error.
+  count: number;
+  // Filters which were used to compute this row count.
+  // We need to recompute the totalRowCount only when filters change and not
+  // when the set of columns / order by changes.
+  filters: string[];
+}
+
+export class SqlTableState {
+  private readonly engine_: EngineProxy;
+  private readonly table_: SqlTableDescription;
+
+  get engine() {
+    return this.engine_;
+  }
+  get table() {
+    return this.table_;
+  }
+
+  private filters: string[];
+  private columns: Column[];
+  private orderBy: ColumnOrderClause[];
+  private offset = 0;
+  private data?: Data;
+  private rowCount?: RowCount;
+
+  constructor(
+      engine: EngineProxy, table: SqlTableDescription, filters?: string[]) {
+    this.engine_ = engine;
+    this.table_ = table;
+
+    this.filters = filters || [];
+    this.columns = [];
+    for (const column of this.table.columns) {
+      if (column.startsHidden) continue;
+      this.columns.push(columnFromSqlTableColumn(column));
+    }
+    this.orderBy = [];
+
+    this.reload();
+  }
+
+  // Compute the actual columns to fetch.
+  private getSQLProjections(): string[] {
+    const result = new Set<string>();
+    for (const column of this.columns) {
+      for (const p of sqlProjectionsForColumn(column)) {
+        result.add(p);
+      }
+    }
+    return Array.from(result);
+  }
+
+  private getSQLImports() {
+    return (this.table.imports || [])
+        .map((i) => `SELECT IMPORT("${i}");`)
+        .join('\n');
+  }
+
+  private getCountRowsSQLQuery(): string {
+    return `
+      ${this.getSQLImports()}
+
+      SELECT
+        COUNT() AS count
+      FROM ${this.table.name}
+      ${constraintsToQueryFragment({
+      filters: this.filters,
+    })}
+    `;
+  }
+
+  getNonPaginatedSQLQuery(): string {
+    const orderBy = this.orderBy.map((c) => ({
+                                       fieldName: c.column.alias,
+                                       direction: c.direction,
+                                     }));
+    return `
+      ${this.getSQLImports()}
+
+      SELECT
+        ${this.getSQLProjections().join(',\n')}
+      FROM ${this.table.name}
+      ${constraintsToQueryFragment({
+      filters: this.filters,
+      orderBy: orderBy,
+    })}
+    `;
+  }
+
+  getPaginatedSQLQuery():
+      string {  // We fetch one more row to determine if we can go forward.
+    return `
+      ${this.getNonPaginatedSQLQuery()}
+      LIMIT ${ROW_LIMIT + 1}
+      OFFSET ${this.offset}
+    `;
+  }
+
+  canGoForward(): boolean {
+    if (this.data === undefined) return false;
+    return this.data.rows.length > ROW_LIMIT;
+  }
+
+  canGoBack(): boolean {
+    if (this.data === undefined) return false;
+    return this.offset > 0;
+  }
+
+  goForward() {
+    if (!this.canGoForward()) return;
+    this.offset += ROW_LIMIT;
+    this.reload({offset: 'keep'});
+  }
+
+  goBack() {
+    if (!this.canGoBack()) return;
+    this.offset -= ROW_LIMIT;
+    this.reload({offset: 'keep'});
+  }
+
+  getDisplayedRange(): {from: number, to: number}|undefined {
+    if (this.data === undefined) return undefined;
+    return {
+      from: this.offset + 1,
+      to: this.offset + Math.min(this.data.rows.length, ROW_LIMIT),
+    };
+  }
+
+  private async loadRowCount(): Promise<RowCount|undefined> {
+    const filters = Array.from(this.filters);
+    const res = await this.engine.query(this.getCountRowsSQLQuery());
+    if (res.error() !== undefined) return undefined;
+    return {
+      count: res.firstRow({count: NUM}).count,
+      filters: filters,
+    };
+  }
+
+  private async loadData(): Promise<Data> {
+    const queryRes = await this.engine.query(this.getPaginatedSQLQuery());
+    const rows: Row[] = [];
+    for (const it = queryRes.iter({}); it.valid(); it.next()) {
+      const row: Row = {};
+      for (const column of queryRes.columns()) {
+        row[column] = it.get(column);
+      }
+      rows.push(row);
+    }
+
+    return {
+      rows,
+      error: queryRes.error(),
+    };
+  }
+
+  private async reload(params?: {offset: 'reset'|'keep'}) {
+    if ((params?.offset ?? 'reset') === 'reset') {
+      this.offset = 0;
+    }
+    const updateRowCount = !arrayEquals(this.rowCount?.filters, this.filters);
+    this.data = undefined;
+    if (updateRowCount) {
+      this.rowCount = undefined;
+    }
+
+    // Delay the visual update by 50ms to avoid flickering (if the query returns
+    // before the data is loaded.
+    setTimeout(() => globals.rafScheduler.scheduleFullRedraw(), 50);
+
+    if (updateRowCount) {
+      this.rowCount = await this.loadRowCount();
+    }
+    this.data = await this.loadData();
+
+    globals.rafScheduler.scheduleFullRedraw();
+  }
+
+  getTotalRowCount(): number|undefined {
+    return this.rowCount?.count;
+  }
+
+  getDisplayedRows(): Row[] {
+    return this.data?.rows || [];
+  }
+
+  getQueryError(): string|undefined {
+    return this.data?.error;
+  }
+
+  isLoading() {
+    return this.data === undefined;
+  }
+
+  removeFilter(filter: string) {
+    this.filters.splice(this.filters.indexOf(filter), 1);
+    this.reload();
+  }
+
+  addFilter(filter: string) {
+    this.filters.push(filter);
+    this.reload();
+  }
+
+  getFilters(): string[] {
+    return this.filters;
+  }
+
+  sortBy(clause: ColumnOrderClause) {
+    this.orderBy = this.orderBy || [];
+    // Remove previous sort by the same column.
+    this.orderBy = this.orderBy.filter((c) => c.column !== clause.column);
+    // Add the new sort clause to the front, so we effectively stable-sort the
+    // data currently displayed to the user.
+    this.orderBy.unshift(clause);
+    this.reload();
+  }
+
+  unsort() {
+    this.orderBy = [];
+    this.reload();
+  }
+
+  isSortedBy(column: Column): SortDirection|undefined {
+    if (!this.orderBy) return undefined;
+    if (this.orderBy.length === 0) return undefined;
+    if (this.orderBy[0].column !== column) return undefined;
+    return this.orderBy[0].direction;
+  }
+
+  addColumn(column: Column, index: number) {
+    this.columns.splice(index + 1, 0, column);
+    this.reload({offset: 'keep'});
+  }
+
+  hideColumnAtIndex(index: number) {
+    const column = this.columns[index];
+    this.columns.splice(index, 1);
+    // We can only filter by the visibile columns to avoid confusing the user,
+    // so we remove order by clauses that refer to the hidden column.
+    this.orderBy = this.orderBy.filter((c) => c.column !== column);
+    // TODO(altimin): we can avoid the fetch here if the orderBy hasn't changed.
+    this.reload({offset: 'keep'});
+  }
+
+  getSelectedColumns(): Column[] {
+    return this.columns;
+  }
+};
diff --git a/ui/src/frontend/sql_table/tab.ts b/ui/src/frontend/sql_table/tab.ts
new file mode 100644
index 0000000..177ce43
--- /dev/null
+++ b/ui/src/frontend/sql_table/tab.ts
@@ -0,0 +1,106 @@
+// 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.
+
+import m from 'mithril';
+
+import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from '../bottom_tab';
+import {copyToClipboard} from '../clipboard';
+import {Icons} from '../semantic_icons';
+import {Button} from '../widgets/button';
+import {DetailsShell} from '../widgets/details_shell';
+import {exists} from '../widgets/utils';
+
+import {SqlTableState} from './state';
+import {SqlTable} from './table';
+import {SqlTableDescription} from './table_description';
+
+interface SqlTableTabConfig {
+  table: SqlTableDescription;
+  displayName?: string;
+  filters?: string[];
+}
+
+export class SqlTableTab extends BottomTab<SqlTableTabConfig> {
+  static readonly kind = 'org.perfetto.SqlTableTab';
+
+  private state: SqlTableState;
+
+  constructor(args: NewBottomTabArgs) {
+    super(args);
+
+    this.state =
+        new SqlTableState(this.engine, this.config.table, this.config.filters);
+  }
+
+  static create(args: NewBottomTabArgs): SqlTableTab {
+    return new SqlTableTab(args);
+  }
+
+  viewTab() {
+    const range = this.state.getDisplayedRange();
+    const rowCount = this.state.getTotalRowCount();
+    const navigation = [
+      exists(range) && exists(rowCount) &&
+          `Showing rows ${range.from}-${range.to} of ${rowCount}`,
+      m(Button, {
+        icon: Icons.GoBack,
+        disabled: !this.state.canGoBack(),
+        onclick: () => this.state.goBack(),
+        minimal: true,
+      }),
+      m(Button, {
+        icon: Icons.GoForward,
+        disabled: !this.state.canGoForward(),
+        onclick: () => this.state.goForward(),
+        minimal: true,
+      }),
+    ];
+
+    return m(
+        DetailsShell,
+        {
+          title: 'Table',
+          description: this.config.displayName ?? this.config.table.name,
+          buttons: [
+            ...navigation,
+            m(Button, {
+              label: 'Copy SQL query',
+              onclick: () =>
+                  copyToClipboard(this.state.getNonPaginatedSQLQuery()),
+            }),
+            m(Button, {
+              label: 'Close',
+              onclick: () => this.close(),
+            }),
+          ],
+        },
+        m(SqlTable, {
+          state: this.state,
+        }));
+  }
+
+  renderTabCanvas() {}
+
+  getTitle(): string {
+    const rowCount = this.state.getTotalRowCount();
+    const rows = rowCount === undefined ? '' : `(${rowCount})`;
+    return `Table ${this.config.displayName ?? this.config.table.name} ${rows}`;
+  }
+
+  isLoading(): boolean {
+    return this.state.isLoading();
+  }
+}
+
+bottomTabRegistry.register(SqlTableTab);
diff --git a/ui/src/frontend/sql_table/table.ts b/ui/src/frontend/sql_table/table.ts
new file mode 100644
index 0000000..a42ad89
--- /dev/null
+++ b/ui/src/frontend/sql_table/table.ts
@@ -0,0 +1,165 @@
+// 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.
+
+import m from 'mithril';
+
+import {EngineProxy} from '../../common/engine';
+import {Row} from '../../common/query_result';
+import {Anchor} from '../anchor';
+import {Icons} from '../semantic_icons';
+import {BasicTable} from '../widgets/basic_table';
+import {Button} from '../widgets/button';
+import {MenuDivider, MenuItem, PopupMenu2} from '../widgets/menu';
+
+import {ArgumentSelector} from './argument_selector';
+import {argColumn, Column, columnFromSqlTableColumn} from './column';
+import {renderCell} from './render_cell';
+import {SqlTableState} from './state';
+import {isArgSetIdColumn, SqlTableDescription} from './table_description';
+
+export interface SqlTableConfig {
+  readonly state: SqlTableState;
+}
+
+export class SqlTable implements m.ClassComponent<SqlTableConfig> {
+  private readonly table: SqlTableDescription;
+  private readonly engine: EngineProxy;
+
+  private state: SqlTableState;
+
+  constructor(vnode: m.Vnode<SqlTableConfig>) {
+    this.state = vnode.attrs.state;
+    this.table = this.state.table;
+    this.engine = this.state.engine;
+  }
+
+  renderFilters(): m.Children {
+    const filters: m.Child[] = [];
+    for (const filter of this.state.getFilters()) {
+      filters.push(m(Button, {
+        label: filter,
+        icon: 'close',
+        onclick: () => {
+          this.state.removeFilter(filter);
+        },
+      }));
+    }
+    return filters;
+  }
+
+  renderAddColumnOptions(addColumn: (column: Column) => void): m.Children {
+    // We do not want to add columns which already exist, so we track the
+    // columns which we are already showing here.
+    // TODO(altimin): Theoretically a single table can have two different
+    // arg_set_ids, so we should track (arg_set_id_column, arg_name) pairs here.
+    const existingColumns = new Set<string>();
+
+    for (const column of this.state.getSelectedColumns()) {
+      existingColumns.add(column.alias);
+    }
+
+    const result = [];
+    for (const column of this.table.columns) {
+      if (existingColumns.has(column.name)) continue;
+      if (isArgSetIdColumn(column)) {
+        result.push(
+            m(MenuItem,
+              {
+                label: column.name,
+              },
+              m(ArgumentSelector, {
+                engine: this.engine,
+                argSetId: column,
+                tableName: this.table.name,
+                filters: this.state.getFilters(),
+                alreadySelectedColumns: existingColumns,
+                onArgumentSelected: (argument: string) => {
+                  addColumn(argColumn(column, argument));
+                },
+              })));
+        continue;
+      }
+      result.push(m(MenuItem, {
+        label: column.name,
+        onclick: () => addColumn(
+            columnFromSqlTableColumn(column),
+            ),
+      }));
+    }
+    return result;
+  }
+
+  renderColumnHeader(column: Column, index: number) {
+    const sorted = this.state.isSortedBy(column);
+    const icon = sorted === 'ASC' ?
+        Icons.SortedAsc :
+        sorted === 'DESC' ? Icons.SortedDesc : Icons.ContextMenu;
+    return m(
+        PopupMenu2,
+        {
+          trigger: m(Anchor, {icon}, column.title),
+        },
+        sorted !== 'DESC' && m(MenuItem, {
+          label: 'Sort: highest first',
+          icon: Icons.SortedDesc,
+          onclick: () => {
+            this.state.sortBy({column, direction: 'DESC'});
+          },
+        }),
+        sorted !== 'ASC' && m(MenuItem, {
+          label: 'Sort: lowest first',
+          icon: Icons.SortedAsc,
+          onclick: () => {
+            this.state.sortBy({column, direction: 'ASC'});
+          },
+        }),
+        sorted !== undefined && m(MenuItem, {
+          label: 'Unsort',
+          icon: Icons.Close,
+          onclick: () => this.state.unsort(),
+        }),
+        this.state.getSelectedColumns().length > 1 && m(MenuItem, {
+          label: 'Hide',
+          icon: Icons.Hide,
+          onclick: () => this.state.hideColumnAtIndex(index),
+        }),
+        m(MenuDivider),
+        m(MenuItem,
+          {label: 'Add column', icon: Icons.AddColumn},
+          this.renderAddColumnOptions((column) => {
+            this.state.addColumn(column, index);
+          })),
+    );
+  }
+
+  view() {
+    const rows = this.state.getDisplayedRows();
+
+    return [
+      m('div', this.renderFilters()),
+      m(BasicTable, {
+        data: rows,
+        columns: this.state.getSelectedColumns().map(
+            (column, i) => ({
+              title: this.renderColumnHeader(column, i),
+              render: (row: Row) => renderCell(column, row, this.state),
+            })),
+      }),
+      this.state.getQueryError() !== undefined &&
+          m('.query-error', this.state.getQueryError()),
+    ];
+  }
+};
+
+export {SqlTableDescription};
diff --git a/ui/src/frontend/sql_table/table_description.ts b/ui/src/frontend/sql_table/table_description.ts
new file mode 100644
index 0000000..b3fa312
--- /dev/null
+++ b/ui/src/frontend/sql_table/table_description.ts
@@ -0,0 +1,97 @@
+// 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.
+
+
+// Definition of the SQL table to be displayed in the SQL table widget,
+// including the semantic definitions of the columns (e.g. timestamp
+// column which requires special formatting). Also note that some of the
+// columns require other columns for advanced display features (e.g. timestamp
+// and duration taken together define "a time range", which can be used for
+// additional filtering.
+
+export type DisplayConfig =
+    SliceIdDisplayConfig|Timestamp|Duration|ThreadDuration|ArgSetId;
+
+// Common properties for all columns.
+interface SqlTableColumnBase {
+  // Name of the column in the SQL table.
+  name: string;
+  // Display name of the column in the UI.
+  title?: string;
+  // Whether the column should be hidden by default.
+  startsHidden?: boolean;
+}
+
+export interface ArgSetIdColumn extends SqlTableColumnBase {
+  type: 'arg_set_id';
+}
+
+export interface RegularSqlTableColumn extends SqlTableColumnBase {
+  // Special rendering instructions for this column, including the list
+  // of additional columns required for the rendering.
+  display?: DisplayConfig;
+}
+
+export type SqlTableColumn = RegularSqlTableColumn|ArgSetIdColumn;
+
+export function isArgSetIdColumn(c: SqlTableColumn): c is ArgSetIdColumn {
+  return (c as {type?: string}).type === 'arg_set_id';
+}
+
+export interface SqlTableDescription {
+  readonly imports?: string[];
+  name: string;
+  columns: SqlTableColumn[];
+}
+
+// Additional columns needed to display the given column.
+export function dependendentColumns(display?: DisplayConfig): string[] {
+  switch (display?.type) {
+    case 'slice_id':
+      return [display.ts, display.dur, display.trackId];
+    default:
+      return [];
+  }
+}
+
+// Column displaying ids into the `slice` table. Requires the ts, dur and
+// track_id columns to be able to display the value, including the
+// "go-to-slice-on-click" functionality.
+export interface SliceIdDisplayConfig {
+  type: 'slice_id';
+  ts: string;
+  dur: string;
+  trackId: string;
+}
+
+// Column displaying timestamps.
+interface Timestamp {
+  type: 'timestamp';
+}
+
+// Column displaying durations.
+export interface Duration {
+  type: 'duration';
+}
+
+// Column displaying thread durations.
+export interface ThreadDuration {
+  type: 'thread_duration';
+}
+
+// Column corresponding to an arg_set_id. Will never be directly displayed,
+// but will allow the user select an argument to display from the arg_set.
+export interface ArgSetId {
+  type: 'arg_set_id';
+}
diff --git a/ui/src/frontend/sql_table/well_known_tables.ts b/ui/src/frontend/sql_table/well_known_tables.ts
new file mode 100644
index 0000000..4b503a4
--- /dev/null
+++ b/ui/src/frontend/sql_table/well_known_tables.ts
@@ -0,0 +1,114 @@
+// 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.
+
+import {SqlTableDescription} from './table';
+
+const sliceTable: SqlTableDescription = {
+  imports: ['experimental.slices'],
+  name: 'experimental_slice_with_thread_and_process_info',
+  columns: [
+    {
+      name: 'id',
+      title: 'ID',
+      display: {
+        type: 'slice_id',
+        ts: 'ts',
+        dur: 'dur',
+        trackId: 'track_id',
+      },
+    },
+    {
+      name: 'ts',
+      title: 'Timestamp',
+      display: {
+        type: 'timestamp',
+      },
+    },
+    {
+      name: 'dur',
+      title: 'Duration',
+      display: {
+        type: 'duration',
+      },
+    },
+    {
+      name: 'thread_dur',
+      title: 'Thread duration',
+      display: {
+        type: 'thread_duration',
+      },
+    },
+    {
+      name: 'category',
+      title: 'Category',
+    },
+    {
+      name: 'name',
+      title: 'Name',
+    },
+    {
+      name: 'track_id',
+      title: 'Track ID',
+      startsHidden: true,
+    },
+    {
+      name: 'track_name',
+      title: 'Track name',
+      startsHidden: true,
+    },
+    {
+      name: 'thread_name',
+      title: 'Thread name',
+    },
+    {
+      name: 'utid',
+      startsHidden: true,
+    },
+    {
+      name: 'tid',
+    },
+    {
+      name: 'process_name',
+      title: 'Process name',
+    },
+    {
+      name: 'upid',
+      startsHidden: true,
+    },
+    {
+      name: 'pid',
+    },
+    {
+      name: 'depth',
+      title: 'Depth',
+      startsHidden: true,
+    },
+    {
+      name: 'parent_id',
+      title: 'Parent slice ID',
+      startsHidden: true,
+    },
+    {
+      name: 'arg_set_id',
+      title: 'Arg',
+      display: {
+        type: 'arg_set_id',
+      },
+    },
+  ],
+};
+
+export class SqlTables {
+  static readonly slice = sliceTable;
+}
diff --git a/ui/src/frontend/sql_utils.ts b/ui/src/frontend/sql_utils.ts
index 2bbcd0e..6ef80f6 100644
--- a/ui/src/frontend/sql_utils.ts
+++ b/ui/src/frontend/sql_utils.ts
@@ -12,10 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {EngineProxy} from '../common/engine';
+import {ColumnType, NUM} from '../common/query_result';
 import {SortDirection} from '../common/state';
-import {ColumnType} from '../common/query_result';
 
-interface OrderClause {
+export interface OrderClause {
   fieldName: string;
   direction?: SortDirection;
 }
@@ -81,7 +82,10 @@
   return n;
 }
 
-export function sqlValueToString(val: ColumnType): string {
+export function sqlValueToString(val: ColumnType): string;
+export function sqlValueToString(val?: ColumnType): string|undefined;
+export function sqlValueToString(val?: ColumnType): string|undefined {
+  if (val === undefined) return undefined;
   if (val instanceof Uint8Array) {
     return `<blob length=${val.length}>`;
   }
@@ -90,3 +94,17 @@
   }
   return val.toString();
 }
+
+export async function getTableRowCount(
+    engine: EngineProxy, tableName: string): Promise<number|undefined> {
+  const result =
+      await engine.query(`SELECT COUNT() as count FROM ${tableName}`);
+  if (result.numRows() === 0) {
+    return undefined;
+  }
+  return result
+      .firstRow({
+        count: NUM,
+      })
+      .count;
+}
diff --git a/ui/src/frontend/widgets/basic_table.ts b/ui/src/frontend/widgets/basic_table.ts
new file mode 100644
index 0000000..578b9d7
--- /dev/null
+++ b/ui/src/frontend/widgets/basic_table.ts
@@ -0,0 +1,56 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+export interface ColumnDescriptor<T> {
+  title: m.Children;
+  render: (row: T) => m.Children;
+}
+
+export interface TableAttrs<T> {
+  data: T[];
+  columns: ColumnDescriptor<T>[];
+}
+
+export class BasicTable implements m.ClassComponent<TableAttrs<any>> {
+  renderColumnHeader(
+      _vnode: m.Vnode<TableAttrs<any>>,
+      column: ColumnDescriptor<any>): m.Children {
+    return m('td', column.title);
+  }
+
+  view(vnode: m.Vnode<TableAttrs<any>>): m.Child {
+    const attrs = vnode.attrs;
+
+    return m(
+        'table.generic-table',
+        {
+          // TODO(altimin, stevegolton): this should be the default for
+          // generic-table, but currently it is overriden by
+          // .pf-details-shell .pf-content table, so specify this here for now.
+          style: {
+            'table-layout': 'auto',
+          },
+        },
+        m('thead',
+          m('tr.header',
+            attrs.columns.map(
+                (column) => this.renderColumnHeader(vnode, column)))),
+        attrs.data.map(
+            (row) =>
+                m('tr',
+                  attrs.columns.map((column) => m('td', column.render(row))))));
+  }
+}
diff --git a/ui/src/frontend/widgets/error.ts b/ui/src/frontend/widgets/error.ts
new file mode 100644
index 0000000..17c91df
--- /dev/null
+++ b/ui/src/frontend/widgets/error.ts
@@ -0,0 +1,21 @@
+// 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.
+
+import m from 'mithril';
+
+export class Err implements m.Component {
+  view(vnode: m.Vnode) {
+    return m('.pf-error', vnode.children);
+  }
+}
diff --git a/ui/src/frontend/widgets/select.ts b/ui/src/frontend/widgets/select.ts
index 7fdbdd4..2ec2bc2 100644
--- a/ui/src/frontend/widgets/select.ts
+++ b/ui/src/frontend/widgets/select.ts
@@ -14,8 +14,16 @@
 
 import m from 'mithril';
 
+import {globals} from '../globals';
+
+import {Menu, MenuItem} from './menu';
+import {TextInput} from './text_input';
+import {exists} from './utils';
+
 export interface SelectAttrs {
   disabled?: boolean;
+  // Whether to show a search box. Defaults to false.
+  filterable?: boolean;
   [htmlAttrs: string]: any;
 }
 
@@ -29,3 +37,50 @@
         children);
   }
 }
+
+export interface FilterableSelectAttrs extends SelectAttrs {
+  values: string[];
+  onSelected: (value: string) => void;
+  maxDisplayedItems?: number;
+  autofocusInput?: boolean;
+}
+
+export class FilterableSelect implements
+    m.ClassComponent<FilterableSelectAttrs> {
+  searchText = '';
+
+  view({attrs}: m.CVnode<FilterableSelectAttrs>) {
+    const filteredValues = attrs.values.filter((name) => {
+      return name.toLowerCase().includes(this.searchText.toLowerCase());
+    });
+
+    const extraItems = exists(attrs.maxDisplayedItems) &&
+        Math.max(0, filteredValues.length - attrs.maxDisplayedItems);
+
+    // TODO(altimin): when the user presses enter and there is only one item,
+    // select the first one.
+    // MAYBE(altimin): when the user presses enter and there are multiple items,
+    // select the first one.
+    return m(
+        'div',
+        m('.pf-search-bar',
+          m(TextInput, {
+            autofocus: attrs.autofocusInput,
+            oninput: (event: Event) => {
+              const eventTarget = event.target as HTMLTextAreaElement;
+              this.searchText = eventTarget.value;
+              globals.rafScheduler.scheduleFullRedraw();
+            },
+            value: this.searchText,
+            placeholder: 'Filter options...',
+            extraClasses: 'pf-search-box',
+          }),
+          m(Menu,
+            ...filteredValues.map(
+                (value) => m(MenuItem, {
+                  label: value,
+                  onclick: () => attrs.onSelected(value),
+                }),
+                extraItems && m('i', `+${extraItems} more`)))));
+  }
+}
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
index b945489..a138f17 100644
--- a/ui/src/frontend/widgets/timestamp.ts
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -57,7 +57,7 @@
   }
 }
 
-function renderTimecode(ts: TPTimestamp): m.Children {
+export function renderTimecode(ts: TPTimestamp): m.Children {
   const relTime = toDomainTime(ts);
   const {dhhmmss, millis, micros, nanos} = new Timecode(relTime);
   return [
diff --git a/ui/src/tracks/custom_sql_table_slices/index.ts b/ui/src/tracks/custom_sql_table_slices/index.ts
index 1211917..aba4d4c 100644
--- a/ui/src/tracks/custom_sql_table_slices/index.ts
+++ b/ui/src/tracks/custom_sql_table_slices/index.ts
@@ -90,7 +90,7 @@
     (detailsPanelConfig.config as GenericSliceDetailsTabConfig).id =
         args.slice.id;
 
-    globals.dispatch(Actions.selectGenericSlice({
+    globals.makeSelection(Actions.selectGenericSlice({
       id: args.slice.id,
       sqlTableName: this.tableName,
       start: args.slice.start,
diff --git a/ui/src/tracks/debug/slice_track.ts b/ui/src/tracks/debug/slice_track.ts
index 664840d..2a40482 100644
--- a/ui/src/tracks/debug/slice_track.ts
+++ b/ui/src/tracks/debug/slice_track.ts
@@ -75,7 +75,7 @@
   }
 
   onSliceClick(args: OnSliceClickArgs<DebugTrackV2Types['slice']>) {
-    globals.dispatch(Actions.selectDebugSlice({
+    globals.makeSelection(Actions.selectDebugSlice({
       id: args.slice.id,
       sqlTableName: this.config.sqlTableName,
       start: args.slice.start,
diff --git a/ui/src/tracks/scroll_jank/event_latency_track.ts b/ui/src/tracks/scroll_jank/event_latency_track.ts
index 4db9620..3a786a8 100644
--- a/ui/src/tracks/scroll_jank/event_latency_track.ts
+++ b/ui/src/tracks/scroll_jank/event_latency_track.ts
@@ -76,7 +76,7 @@
   // Table name must be unique - it cannot include '-' characters or begin with
   // a numeric value.
   const baseTable =
-      `table_${uuidv4().split('-').join('_')}_janky_event_latencies_v2`;
+      `table_${uuidv4().split('-').join('_')}_janky_event_latencies_v3`;
   const tableDefSql = `CREATE TABLE ${baseTable} AS
       WITH event_latencies AS (
         ${subTableSql}
@@ -100,7 +100,7 @@
       dur,
       CASE
         WHEN id IN (
-          SELECT id FROM chrome_janky_event_latencies_v2)
+          SELECT id FROM chrome_janky_event_latencies_v3)
         THEN 'Janky EventLatency'
         ELSE name
       END
diff --git a/ui/src/tracks/scroll_jank/top_level_jank_track.ts b/ui/src/tracks/scroll_jank/top_level_jank_track.ts
index a2b7268..31b1550 100644
--- a/ui/src/tracks/scroll_jank/top_level_jank_track.ts
+++ b/ui/src/tracks/scroll_jank/top_level_jank_track.ts
@@ -93,7 +93,7 @@
         "Janky Scrolling Time" AS name,
         ts,
         dur
-      FROM chrome_scroll_jank_intervals_v2
+      FROM chrome_scroll_jank_intervals_v3
      )
      SELECT
        ROW_NUMBER() OVER(ORDER BY ts) AS id,
diff --git a/ui/src/tracks/scroll_jank/top_level_janky_event_latencies.ts b/ui/src/tracks/scroll_jank/top_level_janky_event_latencies.ts
index 0ba2785..f57da22 100644
--- a/ui/src/tracks/scroll_jank/top_level_janky_event_latencies.ts
+++ b/ui/src/tracks/scroll_jank/top_level_janky_event_latencies.ts
@@ -65,7 +65,7 @@
         'name AS type',
         'sub_cause_of_jank',
       ],
-      sqlTableName: 'chrome_janky_event_latencies_v2',
+      sqlTableName: 'chrome_janky_event_latencies_v3',
     };
   }
 
diff --git a/ui/src/tracks/thread_state_v2/index.ts b/ui/src/tracks/thread_state_v2/index.ts
new file mode 100644
index 0000000..b198693
--- /dev/null
+++ b/ui/src/tracks/thread_state_v2/index.ts
@@ -0,0 +1,137 @@
+// 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.
+
+import {Actions} from '../../common/actions';
+import {colorForState} from '../../common/colorizer';
+import {
+  Color,
+} from '../../common/colorizer';
+import {PluginContext} from '../../common/plugin_api';
+import {NUM_NULL, STR} from '../../common/query_result';
+import {Selection} from '../../common/state';
+import {translateState} from '../../common/thread_state';
+import {
+  BASE_SLICE_ROW,
+  BaseSliceTrack,
+  BaseSliceTrackTypes,
+  OnSliceClickArgs,
+} from '../../frontend/base_slice_track';
+import {globals} from '../../frontend/globals';
+import {
+  SLICE_LAYOUT_FLAT_DEFAULTS,
+  SliceLayout,
+} from '../../frontend/slice_layout';
+import {NewTrackArgs} from '../../frontend/track';
+
+export const THREAD_STATE_ROW = {
+  ...BASE_SLICE_ROW,
+  state: STR,
+  ioWait: NUM_NULL,
+};
+export type ThreadStateRow = typeof THREAD_STATE_ROW;
+
+
+export interface ThreadStateTrackConfig {
+  utid: number;
+}
+
+export interface ThreadStateTrackTypes extends BaseSliceTrackTypes {
+  row: ThreadStateRow;
+  config: ThreadStateTrackConfig;
+}
+
+export const THREAD_STATE_TRACK_V2_KIND = 'ThreadStateTrackV2';
+
+export class ThreadStateTrack extends BaseSliceTrack<ThreadStateTrackTypes> {
+  static readonly kind = THREAD_STATE_TRACK_V2_KIND;
+  static create(args: NewTrackArgs) {
+    return new ThreadStateTrack(args);
+  }
+
+  protected sliceLayout: SliceLayout = {...SLICE_LAYOUT_FLAT_DEFAULTS};
+
+  constructor(args: NewTrackArgs) {
+    super(args);
+  }
+
+  // This is used by the base class to call iter().
+  getRowSpec(): ThreadStateTrackTypes['row'] {
+    return THREAD_STATE_ROW;
+  }
+
+  async initSqlTable(tableName: string): Promise<void> {
+    // Do not display states 'x' and 'S' (dead & sleeping).
+    const sql = `
+      create view ${tableName} as
+      select
+        id,
+        ts,
+        dur,
+        cpu,
+        state,
+        io_wait as ioWait,
+        0 as depth
+      from thread_state
+      where
+        utid = ${this.config.utid} and
+        state != 'x' and
+        state != 'S'
+    `;
+    await this.engine.query(sql);
+  }
+
+  rowToSlice(row: ThreadStateTrackTypes['row']):
+      ThreadStateTrackTypes['slice'] {
+    const baseSlice = super.rowToSlice(row);
+    const ioWait = row.ioWait === null ? undefined : !!row.ioWait;
+    const title = translateState(row.state, ioWait);
+    const baseColor: Color = colorForState(title);
+    return {...baseSlice, title, baseColor};
+  }
+
+  onUpdatedSlices(slices: ThreadStateTrackTypes['slice'][]) {
+    for (const slice of slices) {
+      if (slice === this.hoveredSlice) {
+        slice.color = {
+          c: slice.baseColor.c,
+          h: slice.baseColor.h,
+          s: slice.baseColor.s,
+          l: 30,
+        };
+      } else {
+        slice.color = slice.baseColor;
+      }
+    }
+  }
+
+  onSliceClick(args: OnSliceClickArgs<ThreadStateTrackTypes['slice']>) {
+    globals.makeSelection(Actions.selectThreadState({
+      id: args.slice.id,
+      trackId: this.trackId,
+    }));
+  }
+
+  protected isSelectionHandled(selection: Selection): boolean {
+    return selection.kind === 'THREAD_STATE';
+  }
+}
+
+function activate(ctx: PluginContext) {
+  ctx.registerTrack(ThreadStateTrack);
+}
+
+export const plugin = {
+  pluginId: 'perfetto.ThreadStateTrackV2',
+  activate,
+};