Merge changes I38fb8474,I61db4fc4 into main

* changes:
  service: Add buffer.clear_before_clone
  service: Add buffer.transfer_on_clone
diff --git a/Android.bp b/Android.bp
index cc6473d..5af8aec 100644
--- a/Android.bp
+++ b/Android.bp
@@ -5297,6 +5297,7 @@
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
+        "protos/perfetto/trace/ftrace/samsung.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
         "protos/perfetto/trace/ftrace/scm.proto",
         "protos/perfetto/trace/ftrace/sde.proto",
@@ -5541,6 +5542,7 @@
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
+        "protos/perfetto/trace/ftrace/samsung.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
         "protos/perfetto/trace/ftrace/scm.proto",
         "protos/perfetto/trace/ftrace/sde.proto",
@@ -5613,6 +5615,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/printk.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.gen.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/samsung.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/scm.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sde.gen.cc",
@@ -5685,6 +5688,7 @@
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
+        "protos/perfetto/trace/ftrace/samsung.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
         "protos/perfetto/trace/ftrace/scm.proto",
         "protos/perfetto/trace/ftrace/sde.proto",
@@ -5757,6 +5761,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/printk.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.gen.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/samsung.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/scm.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sde.gen.h",
@@ -5833,6 +5838,7 @@
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
+        "protos/perfetto/trace/ftrace/samsung.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
         "protos/perfetto/trace/ftrace/scm.proto",
         "protos/perfetto/trace/ftrace/sde.proto",
@@ -5904,6 +5910,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.pb.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/samsung.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/scm.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sde.pb.cc",
@@ -5976,6 +5983,7 @@
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
+        "protos/perfetto/trace/ftrace/samsung.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
         "protos/perfetto/trace/ftrace/scm.proto",
         "protos/perfetto/trace/ftrace/sde.proto",
@@ -6047,6 +6055,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.pb.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/samsung.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/scm.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sde.pb.h",
@@ -6123,6 +6132,7 @@
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
+        "protos/perfetto/trace/ftrace/samsung.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
         "protos/perfetto/trace/ftrace/scm.proto",
         "protos/perfetto/trace/ftrace/sde.proto",
@@ -6195,6 +6205,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/samsung.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/scm.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sde.pbzero.cc",
@@ -6267,6 +6278,7 @@
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
+        "protos/perfetto/trace/ftrace/samsung.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
         "protos/perfetto/trace/ftrace/scm.proto",
         "protos/perfetto/trace/ftrace/sde.proto",
@@ -6339,6 +6351,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/samsung.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/scm.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sde.pbzero.h",
@@ -9798,6 +9811,7 @@
         "src/trace_processor/db/column_storage_overlay_unittest.cc",
         "src/trace_processor/db/compare_unittest.cc",
         "src/trace_processor/db/query_executor_unittest.cc",
+        "src/trace_processor/db/runtime_table_unittest.cc",
         "src/trace_processor/db/view_unittest.cc",
     ],
 }
@@ -10773,6 +10787,7 @@
         "src/trace_processor/perfetto_sql/stdlib/experimental/proto_path.sql",
         "src/trace_processor/perfetto_sql/stdlib/experimental/slices.sql",
         "src/trace_processor/perfetto_sql/stdlib/experimental/thread_executing_span.sql",
+        "src/trace_processor/perfetto_sql/stdlib/experimental/thread_state_flattened.sql",
         "src/trace_processor/perfetto_sql/stdlib/pkvm/hypervisor.sql",
     ],
     cmd: "$(location tools/gen_amalgamated_sql.py) --namespace=stdlib --cpp-out=$(out) $(in)",
@@ -12087,6 +12102,7 @@
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
+        "protos/perfetto/trace/ftrace/samsung.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
         "protos/perfetto/trace/ftrace/scm.proto",
         "protos/perfetto/trace/ftrace/sde.proto",
diff --git a/BUILD b/BUILD
index f47bcc3..be8d14e 100644
--- a/BUILD
+++ b/BUILD
@@ -2309,6 +2309,7 @@
         "src/trace_processor/perfetto_sql/stdlib/experimental/proto_path.sql",
         "src/trace_processor/perfetto_sql/stdlib/experimental/slices.sql",
         "src/trace_processor/perfetto_sql/stdlib/experimental/thread_executing_span.sql",
+        "src/trace_processor/perfetto_sql/stdlib/experimental/thread_state_flattened.sql",
     ],
 )
 
@@ -4382,6 +4383,7 @@
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
+        "protos/perfetto/trace/ftrace/samsung.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
         "protos/perfetto/trace/ftrace/scm.proto",
         "protos/perfetto/trace/ftrace/sde.proto",
diff --git a/include/perfetto/public/abi/data_source_abi.h b/include/perfetto/public/abi/data_source_abi.h
index 612c945..d20d351 100644
--- a/include/perfetto/public/abi/data_source_abi.h
+++ b/include/perfetto/public/abi/data_source_abi.h
@@ -50,6 +50,10 @@
 // PerfettoDsImplRegister().
 PERFETTO_SDK_EXPORT struct PerfettoDsImpl* PerfettoDsImplCreate(void);
 
+// Opaque handle used to perform operations from the OnSetup callback. Unused
+// for now.
+struct PerfettoDsOnSetupArgs;
+
 // Called when a data source instance of a specific type is created. `ds_config`
 // points to a serialized perfetto.protos.DataSourceConfig message,
 // `ds_config_size` bytes long. `user_arg` is the value passed to
@@ -60,7 +64,12 @@
                                      PerfettoDsInstanceIndex inst_id,
                                      void* ds_config,
                                      size_t ds_config_size,
-                                     void* user_arg);
+                                     void* user_arg,
+                                     struct PerfettoDsOnSetupArgs* args);
+
+// Opaque handle used to perform operations from the OnSetup callback. Unused
+// for now.
+struct PerfettoDsOnStartArgs;
 
 // Called when tracing starts for a data source instance. `user_arg` is the
 // value passed to PerfettoDsSetCbUserArg(). `inst_ctx` is the return
@@ -68,12 +77,13 @@
 typedef void (*PerfettoDsOnStartCb)(struct PerfettoDsImpl*,
                                     PerfettoDsInstanceIndex inst_id,
                                     void* user_arg,
-                                    void* inst_ctx);
+                                    void* inst_ctx,
+                                    struct PerfettoDsOnStartArgs* args);
 
-// Internal handle used to perform operations from the OnStop callback.
+// Opaque handle used to perform operations from the OnStop callback.
 struct PerfettoDsOnStopArgs;
 
-// Internal handle used to signal when the data source stop operation is
+// Opaque handle used to signal when the data source stop operation is
 // complete.
 struct PerfettoDsAsyncStopper;
 
@@ -106,10 +116,10 @@
                                       void* user_arg,
                                       void* inst_ctx);
 
-// Internal handle used to perform operations from the OnFlush callback.
+// Opaque handle used to perform operations from the OnFlush callback.
 struct PerfettoDsOnFlushArgs;
 
-// Internal handle used to signal when the data source flush operation is
+// Opaque handle used to signal when the data source flush operation is
 // complete.
 struct PerfettoDsAsyncFlusher;
 
@@ -124,8 +134,9 @@
 // PerfettoDsOnFlushArgsPostpone).
 PERFETTO_SDK_EXPORT void PerfettoDsFlushDone(struct PerfettoDsAsyncFlusher*);
 
-// Called when tracing stops for a data source instance. `user_arg` is the value
-// passed to PerfettoDsSetCbUserArg(). `inst_ctx` is the return value of
+// Called when the tracing service requires all the pending tracing data to be
+// flushed for a data source instance. `user_arg` is the value passed to
+// PerfettoDsSetCbUserArg(). `inst_ctx` is the return value of
 // PerfettoDsOnSetupCb. `args` can be used to postpone stopping this data source
 // instance.
 typedef void (*PerfettoDsOnFlushCb)(struct PerfettoDsImpl*,
diff --git a/protos/perfetto/trace/ftrace/all_protos.gni b/protos/perfetto/trace/ftrace/all_protos.gni
index 54a226c..bd055ab 100644
--- a/protos/perfetto/trace/ftrace/all_protos.gni
+++ b/protos/perfetto/trace/ftrace/all_protos.gni
@@ -61,6 +61,7 @@
   "printk.proto",
   "raw_syscalls.proto",
   "regulator.proto",
+  "samsung.proto",
   "sched.proto",
   "scm.proto",
   "sde.proto",
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index 88d3641..f9815cd 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -61,6 +61,7 @@
 import "protos/perfetto/trace/ftrace/printk.proto";
 import "protos/perfetto/trace/ftrace/raw_syscalls.proto";
 import "protos/perfetto/trace/ftrace/regulator.proto";
+import "protos/perfetto/trace/ftrace/samsung.proto";
 import "protos/perfetto/trace/ftrace/sched.proto";
 import "protos/perfetto/trace/ftrace/scm.proto";
 import "protos/perfetto/trace/ftrace/sde.proto";
@@ -597,5 +598,6 @@
     SuspendResumeMinimalFtraceEvent suspend_resume_minimal = 481;
     MaliMaliCSFINTERRUPTSTARTFtraceEvent mali_mali_CSF_INTERRUPT_START = 482;
     MaliMaliCSFINTERRUPTENDFtraceEvent mali_mali_CSF_INTERRUPT_END = 483;
+    SamsungTracingMarkWriteFtraceEvent samsung_tracing_mark_write = 484;
   }
 }
diff --git a/protos/perfetto/trace/ftrace/samsung.proto b/protos/perfetto/trace/ftrace/samsung.proto
new file mode 100644
index 0000000..5da0b4c
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/samsung.proto
@@ -0,0 +1,14 @@
+// Autogenerated by:
+// ../../src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message SamsungTracingMarkWriteFtraceEvent {
+  optional int32 pid = 1;
+  optional string trace_name = 2;
+  optional uint32 trace_begin = 3;
+  optional uint32 trace_type = 4;
+  optional int32 value = 5;
+}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 9e88d34..3e12a3a 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -8006,6 +8006,18 @@
 
 // End of protos/perfetto/trace/ftrace/regulator.proto
 
+// Begin of protos/perfetto/trace/ftrace/samsung.proto
+
+message SamsungTracingMarkWriteFtraceEvent {
+  optional int32 pid = 1;
+  optional string trace_name = 2;
+  optional uint32 trace_begin = 3;
+  optional uint32 trace_type = 4;
+  optional int32 value = 5;
+}
+
+// End of protos/perfetto/trace/ftrace/samsung.proto
+
 // Begin of protos/perfetto/trace/ftrace/sched.proto
 
 message SchedSwitchFtraceEvent {
@@ -9173,6 +9185,7 @@
     SuspendResumeMinimalFtraceEvent suspend_resume_minimal = 481;
     MaliMaliCSFINTERRUPTSTARTFtraceEvent mali_mali_CSF_INTERRUPT_START = 482;
     MaliMaliCSFINTERRUPTENDFtraceEvent mali_mali_CSF_INTERRUPT_END = 483;
+    SamsungTracingMarkWriteFtraceEvent samsung_tracing_mark_write = 484;
   }
 }
 
@@ -10886,8 +10899,11 @@
   // Contact perfetto-dev@googlegroups.com if you are interested in a subrange
   // for your project.
 
+  // Extension range reserved for chromium:
+  // https://source.chromium.org/chromium/chromium/src/+/main:base/tracing/protos/chrome_track_event.proto
+  extensions 1000 to 1999;
   // Extension range for future use.
-  extensions 1000 to 9899;
+  extensions 2000 to 9899;
   // Reserved for Perfetto unit and integration tests.
   extensions 9900 to 10000;
 
diff --git a/protos/perfetto/trace/track_event/track_event.proto b/protos/perfetto/trace/track_event/track_event.proto
index e57e014..9991979 100644
--- a/protos/perfetto/trace/track_event/track_event.proto
+++ b/protos/perfetto/trace/track_event/track_event.proto
@@ -280,8 +280,11 @@
   // Contact perfetto-dev@googlegroups.com if you are interested in a subrange
   // for your project.
 
+  // Extension range reserved for chromium:
+  // https://source.chromium.org/chromium/chromium/src/+/main:base/tracing/protos/chrome_track_event.proto
+  extensions 1000 to 1999;
   // Extension range for future use.
-  extensions 1000 to 9899;
+  extensions 2000 to 9899;
   // Reserved for Perfetto unit and integration tests.
   extensions 9900 to 10000;
 
diff --git a/src/shared_lib/data_source.cc b/src/shared_lib/data_source.cc
index b5864ba..a5886e4 100644
--- a/src/shared_lib/data_source.cc
+++ b/src/shared_lib/data_source.cc
@@ -106,7 +106,7 @@
       std::vector<uint8_t> serialized_config = args.config->SerializeAsArray();
       inst_ctx_ = type_.on_setup_cb(
           &type_, args.internal_instance_index, serialized_config.data(),
-          serialized_config.size(), type_.cb_user_arg);
+          serialized_config.size(), type_.cb_user_arg, nullptr);
     }
     std::lock_guard<std::mutex> lock(type_.mu);
     const bool was_enabled = type_.enabled_instances.any();
@@ -119,7 +119,7 @@
   void OnStart(const StartArgs& args) override {
     if (type_.on_start_cb) {
       type_.on_start_cb(&type_, args.internal_instance_index, type_.cb_user_arg,
-                        inst_ctx_);
+                        inst_ctx_, nullptr);
     }
   }
 
diff --git a/src/shared_lib/test/api_integrationtest.cc b/src/shared_lib/test/api_integrationtest.cc
index 6861c58..24b2ec1 100644
--- a/src/shared_lib/test/api_integrationtest.cc
+++ b/src/shared_lib/test/api_integrationtest.cc
@@ -84,13 +84,15 @@
                PerfettoDsInstanceIndex inst_id,
                void* ds_config,
                size_t ds_config_size,
-               void* user_arg));
+               void* user_arg,
+               struct PerfettoDsOnSetupArgs* args));
   MOCK_METHOD(void,
               OnStart,
               (struct PerfettoDsImpl*,
                PerfettoDsInstanceIndex inst_id,
                void* user_arg,
-               void* inst_ctx));
+               void* inst_ctx,
+               struct PerfettoDsOnStartArgs* args));
   MOCK_METHOD(void,
               OnStop,
               (struct PerfettoDsImpl*,
@@ -232,17 +234,20 @@
     struct PerfettoDsParams params = PerfettoDsParamsDefault();
     params.on_setup_cb = [](struct PerfettoDsImpl* ds_impl,
                             PerfettoDsInstanceIndex inst_id, void* ds_config,
-                            size_t ds_config_size, void* user_arg) -> void* {
+                            size_t ds_config_size, void* user_arg,
+                            struct PerfettoDsOnSetupArgs* args) -> void* {
       auto* thiz = static_cast<SharedLibDataSourceTest*>(user_arg);
       return thiz->ds2_callbacks_.OnSetup(ds_impl, inst_id, ds_config,
-                                          ds_config_size, thiz->ds2_user_arg_);
+                                          ds_config_size, thiz->ds2_user_arg_,
+                                          args);
     };
     params.on_start_cb = [](struct PerfettoDsImpl* ds_impl,
                             PerfettoDsInstanceIndex inst_id, void* user_arg,
-                            void* inst_ctx) -> void {
+                            void* inst_ctx,
+                            struct PerfettoDsOnStartArgs* args) -> void {
       auto* thiz = static_cast<SharedLibDataSourceTest*>(user_arg);
       return thiz->ds2_callbacks_.OnStart(ds_impl, inst_id, thiz->ds2_user_arg_,
-                                          inst_ctx);
+                                          inst_ctx, args);
     };
     params.on_stop_cb =
         [](struct PerfettoDsImpl* ds_impl, PerfettoDsInstanceIndex inst_id,
@@ -464,9 +469,10 @@
   void* const kInstancePtr = reinterpret_cast<void*>(0x44);
   testing::InSequence seq;
   PerfettoDsInstanceIndex setup_inst, start_inst, stop_inst;
-  EXPECT_CALL(ds2_callbacks_, OnSetup(_, _, _, _, kDataSource2UserArg))
+  EXPECT_CALL(ds2_callbacks_, OnSetup(_, _, _, _, kDataSource2UserArg, _))
       .WillOnce(DoAll(SaveArg<1>(&setup_inst), Return(kInstancePtr)));
-  EXPECT_CALL(ds2_callbacks_, OnStart(_, _, kDataSource2UserArg, kInstancePtr))
+  EXPECT_CALL(ds2_callbacks_,
+              OnStart(_, _, kDataSource2UserArg, kInstancePtr, _))
       .WillOnce(SaveArg<1>(&start_inst));
 
   TracingSession tracing_session =
diff --git a/src/tools/ftrace_proto_gen/event_list b/src/tools/ftrace_proto_gen/event_list
index 40dd949..af52c8a 100644
--- a/src/tools/ftrace_proto_gen/event_list
+++ b/src/tools/ftrace_proto_gen/event_list
@@ -478,3 +478,4 @@
 synthetic/suspend_resume_minimal
 mali/mali_CSF_INTERRUPT_START
 mali/mali_CSF_INTERRUPT_END
+samsung/tracing_mark_write
diff --git a/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc b/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
index cf9654c..789d5bf 100644
--- a/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+++ b/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
@@ -59,7 +59,7 @@
   // These groups have events where the name alone conflicts with an existing
   // proto:
   if (group == "sde" || group == "g2d" || group == "dpu" || group == "mali" ||
-      group == "lwis") {
+      group == "lwis" || group == "samsung") {
     event_name = group + "_" + event_name;
   }
   return event_name;
diff --git a/src/trace_processor/db/BUILD.gn b/src/trace_processor/db/BUILD.gn
index 50f572c..067d768 100644
--- a/src/trace_processor/db/BUILD.gn
+++ b/src/trace_processor/db/BUILD.gn
@@ -59,6 +59,7 @@
     "column_storage_overlay_unittest.cc",
     "compare_unittest.cc",
     "query_executor_unittest.cc",
+    "runtime_table_unittest.cc",
     "view_unittest.cc",
   ]
   deps = [
diff --git a/src/trace_processor/db/runtime_table.cc b/src/trace_processor/db/runtime_table.cc
index a982b8a..f2559f9 100644
--- a/src/trace_processor/db/runtime_table.cc
+++ b/src/trace_processor/db/runtime_table.cc
@@ -15,11 +15,29 @@
  */
 
 #include "src/trace_processor/db/runtime_table.h"
+#include <cstdint>
+#include <optional>
+#include "perfetto/base/status.h"
 
 namespace perfetto {
 namespace trace_processor {
+namespace {
 
-RuntimeTable::~RuntimeTable() = default;
+template <typename T, typename U>
+T Fill(uint32_t leading_nulls, U value) {
+  T res;
+  for (uint32_t i = 0; i < leading_nulls; ++i) {
+    res.Append(value);
+  }
+  return res;
+}
+
+bool IsPerfectlyRepresentableAsDouble(int64_t res) {
+  static constexpr int64_t kMaxDoubleRepresentible = 1ull << 53;
+  return res >= -kMaxDoubleRepresentible && res <= kMaxDoubleRepresentible;
+}
+
+}  // namespace
 
 RuntimeTable::RuntimeTable(StringPool* pool, std::vector<std::string> col_names)
     : Table(pool), col_names_(col_names), storage_(col_names_.size()) {
@@ -27,6 +45,8 @@
     storage_[i] = std::make_unique<VariantStorage>();
 }
 
+RuntimeTable::~RuntimeTable() = default;
+
 base::Status RuntimeTable::AddNull(uint32_t idx) {
   auto* col = storage_[idx].get();
   if (auto* leading_nulls = std::get_if<uint32_t>(col)) {
@@ -46,11 +66,21 @@
 base::Status RuntimeTable::AddInteger(uint32_t idx, int64_t res) {
   auto* col = storage_[idx].get();
   if (auto* leading_nulls_ptr = std::get_if<uint32_t>(col)) {
-    RETURN_IF_ERROR(Fill<IntStorage>(col, *leading_nulls_ptr, std::nullopt));
+    *col = Fill<IntStorage>(*leading_nulls_ptr, std::nullopt);
+  }
+  if (auto* doubles = std::get_if<DoubleStorage>(col)) {
+    if (!IsPerfectlyRepresentableAsDouble(res)) {
+      return base::ErrStatus("Column %s contains %" PRId64
+                             " which cannot be represented as a double",
+                             col_names_[idx].c_str(), res);
+    }
+    doubles->Append(static_cast<double>(res));
+    return base::OkStatus();
   }
   auto* ints = std::get_if<IntStorage>(col);
   if (!ints) {
-    return base::ErrStatus("Column %u does not have consistent types", idx);
+    return base::ErrStatus("Column %s does not have consistent types",
+                           col_names_[idx].c_str());
   }
   ints->Append(res);
   return base::OkStatus();
@@ -59,11 +89,29 @@
 base::Status RuntimeTable::AddFloat(uint32_t idx, double res) {
   auto* col = storage_[idx].get();
   if (auto* leading_nulls_ptr = std::get_if<uint32_t>(col)) {
-    RETURN_IF_ERROR(Fill<DoubleStorage>(col, *leading_nulls_ptr, std::nullopt));
+    *col = Fill<DoubleStorage>(*leading_nulls_ptr, std::nullopt);
+  }
+  if (auto* ints = std::get_if<IntStorage>(col)) {
+    DoubleStorage storage;
+    for (uint32_t i = 0; i < ints->size(); ++i) {
+      std::optional<int64_t> int_val = ints->Get(i);
+      if (!int_val) {
+        storage.Append(std::nullopt);
+        continue;
+      }
+      if (int_val && !IsPerfectlyRepresentableAsDouble(*int_val)) {
+        return base::ErrStatus("Column %s contains %" PRId64
+                               " which cannot be represented as a double",
+                               col_names_[idx].c_str(), *int_val);
+      }
+      storage.Append(static_cast<double>(*int_val));
+    }
+    *col = std::move(storage);
   }
   auto* doubles = std::get_if<DoubleStorage>(col);
   if (!doubles) {
-    return base::ErrStatus("Column %u does not have consistent types", idx);
+    return base::ErrStatus("Column %s does not have consistent types",
+                           col_names_[idx].c_str());
   }
   doubles->Append(res);
   return base::OkStatus();
@@ -72,12 +120,12 @@
 base::Status RuntimeTable::AddText(uint32_t idx, const char* ptr) {
   auto* col = storage_[idx].get();
   if (auto* leading_nulls_ptr = std::get_if<uint32_t>(col)) {
-    RETURN_IF_ERROR(
-        Fill<StringStorage>(col, *leading_nulls_ptr, StringPool::Id::Null()));
+    *col = Fill<StringStorage>(*leading_nulls_ptr, StringPool::Id::Null());
   }
   auto* strings = std::get_if<StringStorage>(col);
   if (!strings) {
-    return base::ErrStatus("Column %u does not have consistent types", idx);
+    return base::ErrStatus("Column %s does not have consistent types",
+                           col_names_[idx].c_str());
   }
   strings->Append(string_pool_->InternString(ptr));
   return base::OkStatus();
@@ -88,15 +136,19 @@
   for (uint32_t i = 0; i < col_names_.size(); ++i) {
     auto* col = storage_[i].get();
     if (auto* leading_nulls = std::get_if<uint32_t>(col)) {
-      RETURN_IF_ERROR(Fill<IntStorage>(col, *leading_nulls, std::nullopt));
+      PERFETTO_CHECK(*leading_nulls == rows);
+      *col = Fill<IntStorage>(*leading_nulls, std::nullopt);
     }
     if (auto* ints = std::get_if<IntStorage>(col)) {
+      PERFETTO_CHECK(ints->size() == rows);
       columns_.push_back(Column(col_names_[i].c_str(), ints,
                                 Column::Flag::kNoFlag, this, i, 0));
     } else if (auto* strings = std::get_if<StringStorage>(col)) {
+      PERFETTO_CHECK(strings->size() == rows);
       columns_.push_back(Column(col_names_[i].c_str(), strings,
                                 Column::Flag::kNonNull, this, i, 0));
     } else if (auto* doubles = std::get_if<DoubleStorage>(col)) {
+      PERFETTO_CHECK(doubles->size() == rows);
       columns_.push_back(Column(col_names_[i].c_str(), doubles,
                                 Column::Flag::kNoFlag, this, i, 0));
     } else {
diff --git a/src/trace_processor/db/runtime_table.h b/src/trace_processor/db/runtime_table.h
index 6e14a53..cef31a3 100644
--- a/src/trace_processor/db/runtime_table.h
+++ b/src/trace_processor/db/runtime_table.h
@@ -55,15 +55,6 @@
   base::Status AddColumnsAndOverlays(uint32_t rows);
 
  private:
-  template <typename T, typename U>
-  base::Status Fill(VariantStorage* col, uint32_t leading_nulls, U value) {
-    *col = T();
-    auto* storage = std::get_if<T>(col);
-    for (uint32_t i = 0; i < leading_nulls; ++i) {
-      storage->Append(value);
-    }
-    return base::OkStatus();
-  }
   std::vector<std::string> col_names_;
   std::vector<std::unique_ptr<VariantStorage>> storage_;
 };
diff --git a/src/trace_processor/db/runtime_table_unittest.cc b/src/trace_processor/db/runtime_table_unittest.cc
new file mode 100644
index 0000000..85ca8ec
--- /dev/null
+++ b/src/trace_processor/db/runtime_table_unittest.cc
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/db/runtime_table.h"
+
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace {
+
+class RuntimeTableTest : public ::testing::Test {
+ protected:
+  StringPool pool_;
+  std::vector<std::string> names_{{"foo"}};
+  RuntimeTable table_{&pool_, names_};
+};
+
+TEST_F(RuntimeTableTest, DoubleThenIntValid) {
+  ASSERT_TRUE(table_.AddFloat(0, 1024.3).ok());
+  ASSERT_TRUE(table_.AddInteger(0, 1ll << 53).ok());
+  ASSERT_TRUE(table_.AddColumnsAndOverlays(2).ok());
+
+  const auto& col = table_.columns()[0];
+  ASSERT_EQ(col.Get(0).AsDouble(), 1024.3);
+  ASSERT_EQ(col.Get(1).AsDouble(), static_cast<double>(1ll << 53));
+}
+
+TEST_F(RuntimeTableTest, DoubleThenIntInvalid) {
+  ASSERT_TRUE(table_.AddFloat(0, 1024.0).ok());
+  ASSERT_FALSE(table_.AddInteger(0, (1ll << 53) + 1).ok());
+  ASSERT_FALSE(table_.AddInteger(0, -(1ll << 53) - 1).ok());
+}
+
+TEST_F(RuntimeTableTest, IntThenDouble) {
+  ASSERT_TRUE(table_.AddInteger(0, 1024).ok());
+  ASSERT_TRUE(table_.AddFloat(0, 1.3).ok());
+  ASSERT_TRUE(table_.AddColumnsAndOverlays(2).ok());
+
+  const auto& col = table_.columns()[0];
+  ASSERT_EQ(col.Get(0).AsDouble(), 1024.0);
+  ASSERT_EQ(col.Get(1).AsDouble(), 1.3);
+}
+
+}  // namespace
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/importers/json/json_utils.cc b/src/trace_processor/importers/json/json_utils.cc
index be9492b..d2e1c18 100644
--- a/src/trace_processor/importers/json/json_utils.cc
+++ b/src/trace_processor/importers/json/json_utils.cc
@@ -62,17 +62,37 @@
   PERFETTO_DCHECK(IsJsonSupported());
 
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
-  size_t lhs_end = std::min<size_t>(s.find('.'), s.size());
-  size_t rhs_start = std::min<size_t>(lhs_end + 1, s.size());
-  std::optional<int64_t> lhs = base::StringToInt64(s.substr(0, lhs_end));
-  std::optional<double> rhs =
-      base::StringToDouble("0." + s.substr(rhs_start, std::string::npos));
-  if ((!lhs.has_value() && lhs_end > 0) ||
-      (!rhs.has_value() && rhs_start < s.size())) {
-    return std::nullopt;
+  // 's' is formatted as a JSON Number, in microseconds
+  // goal: reformat 's' to be as an int, in nanoseconds
+  std::string s_as_ns = s;
+
+  // detect and remove scientific notation's exponents
+  int32_t exp_shift = 0;
+  if (size_t exp_start = s.find_first_of("eE");
+      exp_start != std::string::npos) {
+    const std::string exp_s = s.substr(exp_start + 1, s.size());
+    const std::optional<int32_t> exp = base::StringToInt32(exp_s);
+    if (!exp.has_value()) {
+      return std::nullopt;
+    }
+    s_as_ns.erase(exp_start);
+    exp_shift = *exp;
   }
-  return lhs.value_or(0) * 1000 +
-         static_cast<int64_t>(rhs.value_or(0) * 1000.0);
+
+  // detect and remove decimal separator
+  size_t int_size = s_as_ns.size();
+  if (size_t frac_start = s.find('.'); frac_start != std::string::npos) {
+    s_as_ns.erase(frac_start, 1);
+    int_size = frac_start;
+  }
+
+  // expand or shrink to the new size
+  constexpr int us_to_ns_shift = 3;
+  const size_t s_as_ns_size = size_t(
+      std::max<ptrdiff_t>(1, ptrdiff_t(int_size) + exp_shift + us_to_ns_shift));
+  s_as_ns.resize(s_as_ns_size, '0');  // pads or truncates
+
+  return base::StringToInt64(s_as_ns);
 #else
   perfetto::base::ignore_result(s);
   return std::nullopt;
diff --git a/src/trace_processor/importers/json/json_utils_unittest.cc b/src/trace_processor/importers/json/json_utils_unittest.cc
index 7f01ad6..5b17cc3 100644
--- a/src/trace_processor/importers/json/json_utils_unittest.cc
+++ b/src/trace_processor/importers/json/json_utils_unittest.cc
@@ -52,8 +52,29 @@
   ASSERT_EQ(CoerceToTs(Json::Value("42.0")).value_or(-1), 42000);
   ASSERT_EQ(CoerceToTs(Json::Value("0.2")).value_or(-1), 200);
   ASSERT_EQ(CoerceToTs(Json::Value("0.2e-1")).value_or(-1), 20);
+  ASSERT_EQ(CoerceToTs(Json::Value("0.2e-2")).value_or(-1), 2);
+  ASSERT_EQ(CoerceToTs(Json::Value("0.2e-3")).value_or(-1), 0);
+  ASSERT_EQ(CoerceToTs(Json::Value("1.692108548132154500e+15")).value_or(-1),
+            1'692'108'548'132'154'500);
+  ASSERT_EQ(CoerceToTs(Json::Value("1692108548132154.500")).value_or(-1),
+            1'692'108'548'132'154'500);
+  ASSERT_EQ(CoerceToTs(Json::Value("1.692108548132154501e+15")).value_or(-1),
+            1'692'108'548'132'154'501);
+  ASSERT_EQ(CoerceToTs(Json::Value("1692108548132154.501")).value_or(-1),
+            1'692'108'548'132'154'501);
+  ASSERT_EQ(CoerceToTs(Json::Value("-1.692108548132154500E+15")).value_or(-1),
+            -1'692'108'548'132'154'500);
+  ASSERT_EQ(CoerceToTs(Json::Value("-1692108548132154.500")).value_or(-1),
+            -1'692'108'548'132'154'500);
+  ASSERT_EQ(CoerceToTs(Json::Value("-1.692108548132154501E+15")).value_or(-1),
+            -1'692'108'548'132'154'501);
+  ASSERT_EQ(CoerceToTs(Json::Value("-1692108548132154.501")).value_or(-1),
+            -1'692'108'548'132'154'501);
+  ASSERT_EQ(CoerceToTs(Json::Value("-0")).value_or(-1), 0);
+  ASSERT_EQ(CoerceToTs(Json::Value("0")).value_or(-1), 0);
   ASSERT_EQ(CoerceToTs(Json::Value(".")).value_or(-1), 0);
   ASSERT_FALSE(CoerceToTs(Json::Value("1234!")).has_value());
+  ASSERT_FALSE(CoerceToTs(Json::Value("123e4!")).has_value());
 }
 
 }  // namespace
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/utils.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/utils.sql
index 8b969bb..167bca2 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/utils.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/utils.sql
@@ -89,8 +89,14 @@
 -- names. For example, LongTaskTracker slices may have associated IPC
 -- metadata, or InterestingTask slices for input may have associated IPC to
 -- determine whether the task is fling/etc.
+--
+-- @arg name STRING            The name of slice.
+-- @column interface_name      Name of the interface of the IPC call.
+-- @column ipc_hash            Hash of the IPC call.
+-- @column message_type        Message type (e.g. reply).
+-- @column id                  The slice ID.
 SELECT CREATE_VIEW_FUNCTION(
-  'SELECT_LONG_TASK_SLICES(name STRING)',
+  'CHROME_SELECT_LONG_TASK_SLICES(name STRING)',
   'interface_name STRING, ipc_hash INT, message_type STRING, id INT',
   'SELECT
       EXTRACT_ARG(s.arg_set_id, "chrome_mojo_event_info.mojo_interface_tag") AS interface_name,
diff --git a/src/trace_processor/perfetto_sql/stdlib/experimental/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/experimental/BUILD.gn
index caae0a9..d9a48c1 100644
--- a/src/trace_processor/perfetto_sql/stdlib/experimental/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/experimental/BUILD.gn
@@ -21,5 +21,6 @@
     "proto_path.sql",
     "slices.sql",
     "thread_executing_span.sql",
+    "thread_state_flattened.sql",
   ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/experimental/flat_slices.sql b/src/trace_processor/perfetto_sql/stdlib/experimental/flat_slices.sql
index 98063d0..4b1d69e 100644
--- a/src/trace_processor/perfetto_sql/stdlib/experimental/flat_slices.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/experimental/flat_slices.sql
@@ -88,7 +88,13 @@
       events.track_id
     FROM events
   )
-SELECT * FROM data WHERE depth != -1;
+SELECT data.slice_id, data.ts, data.dur, data.depth,
+ data.name, data.track_id, thread.utid, thread.tid, thread.name as thread_name,
+ process.upid, process.pid, process.name as process_name
+ FROM data JOIN thread_track ON data.track_id = thread_track.id
+JOIN thread USING(utid)
+JOIN process USING(upid)
+WHERE depth != -1;
 
 CREATE
   INDEX experimental_slice_flattened_id_idx
diff --git a/src/trace_processor/perfetto_sql/stdlib/experimental/thread_executing_span.sql b/src/trace_processor/perfetto_sql/stdlib/experimental/thread_executing_span.sql
index 4632457..8a1a337 100644
--- a/src/trace_processor/perfetto_sql/stdlib/experimental/thread_executing_span.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/experimental/thread_executing_span.sql
@@ -13,6 +13,7 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
+SELECT IMPORT('common.slices');
 
 -- A 'thread_executing_span' is thread_state span starting with a runnable slice
 -- until the next runnable slice that's woken up by a process (as opposed
@@ -645,3 +646,161 @@
     leaf_blocked_function
   FROM experimental_thread_executing_span_ancestors($thread_executing_span_id, $leaf_utid),
     trace_bounds;
+
+-- Critical path of thread_executing_spans 'span joined' with their thread states.
+-- See |experimental_thread_executing_span_critical_path|.
+--
+-- @arg leaf_utid INT                       Thread utid to filter critical paths for.
+--
+-- @column id                               Id of the first (runnable) thread state in thread_executing_span.
+-- @column thread_state_id                  Id of thread_state in the critical path.
+-- @column ts                               Timestamp of thread_state in the critical path.
+-- @column dur                              Duration of thread_state in the critical path.
+-- @column tid                              Tid of thread with thread_state.
+-- @column pid                              Pid of process with thread_state.
+-- @column utid                             Utid of thread with thread_state.
+-- @column upid                             Upid of process with thread_state.
+-- @column thread_name                      Name of thread with thread_state.
+-- @column process_name                     Name of process with thread_state.
+-- @column state                            Thread state of thread in the critical path.
+-- @column blocked_function                 Blocked function of thread in the critical path.
+-- @column height                           Tree height of thread_executing_span thread_state belongs to.
+-- @column leaf_utid                        Thread Utid the critical path was filtered to.
+CREATE PERFETTO FUNCTION experimental_thread_executing_span_critical_path_thread_states(leaf_utid INT)
+RETURNS TABLE(
+  id INT,
+  thread_state_id INT,
+  ts LONG,
+  dur LONG,
+  tid INT,
+  pid INT,
+  utid INT,
+  upid INT,
+  thread_name STRING,
+  process_name STRING,
+  state STRING,
+  blocked_function STRING,
+  height INT,
+  leaf_utid INT
+) AS
+WITH
+  span_starts AS (
+    SELECT
+      span.id,
+      thread_state.id AS thread_state_id,
+      MAX(thread_state.ts, span.ts) AS ts,
+      span.ts + span.dur AS span_end_ts,
+      thread_state.ts + thread_state.dur AS thread_state_end_ts,
+      span.tid,
+      span.pid,
+      span.utid,
+      span.upid,
+      span.thread_name,
+      span.process_name,
+      thread_state.state,
+      thread_state.blocked_function,
+      span.height,
+      span.leaf_utid
+    FROM experimental_thread_executing_span_critical_path(NULL, $leaf_utid) span
+    JOIN thread_state
+      ON
+        thread_state.utid = span.utid
+        AND ((thread_state.ts BETWEEN span.ts AND span.ts + span.dur)
+             OR (span.ts BETWEEN thread_state.ts AND thread_state.ts + thread_state.dur))
+  )
+SELECT
+  id,
+  thread_state_id,
+  ts,
+  MIN(span_end_ts, thread_state_end_ts) - ts AS dur,
+  tid,
+  pid,
+  utid,
+  upid,
+  thread_name,
+  process_name,
+  state,
+  blocked_function,
+  height,
+  leaf_utid
+FROM span_starts
+WHERE MIN(span_end_ts, thread_state_end_ts) - ts > 0;
+
+-- Critical path of thread_executing_spans 'span joined' with their slices.
+-- See |experimental_thread_executing_span_critical_path|.
+--
+-- @arg leaf_utid INT                       Thread utid to filter critical paths for.
+--
+-- @column id                               Id of the first (runnable) thread state in thread_executing_span.
+-- @column slice_id                         Id of slice in the critical path.
+-- @column ts                               Timestamp of slice in the critical path.
+-- @column dur                              Duration of slice in the critical path.
+-- @column tid                              Tid of thread that emitted the slice.
+-- @column pid                              Pid of process that emitted the slice.
+-- @column utid                             Utid of thread that emitted the slice.
+-- @column upid                             Upid of process that emitted the slice.
+-- @column thread_name                      Name of thread that emitted the slice.
+-- @column process_name                     Name of process that emitted the slice.
+-- @column slice_name                       Name of slice in the critical path.
+-- @column slice_depth                      Depth of slice in its slice stack in the critical path.
+-- @column height                           Tree height of thread_executing_span the slice belongs to.
+-- @column leaf_utid                        Thread Utid the critical path was filtered to.
+CREATE PERFETTO FUNCTION experimental_thread_executing_span_critical_path_slices(leaf_utid INT)
+RETURNS TABLE(
+  id INT,
+  slice_id INT,
+  ts LONG,
+  dur LONG,
+  tid INT,
+  pid INT,
+  utid INT,
+  upid INT,
+  thread_name STRING,
+  process_name STRING,
+  slice_name STRING,
+  slice_depth INT,
+  height INT,
+  leaf_utid INT
+) AS
+WITH
+  span_start AS (
+    SELECT
+      span.id,
+      slice.id AS slice_id,
+      MAX(slice.ts, span.ts) AS ts,
+      span.ts + span.dur AS span_end_ts,
+      slice.ts + slice.dur AS slice_end_ts,
+      span.tid,
+      span.pid,
+      span.utid,
+      span.upid,
+      span.thread_name,
+      span.process_name,
+      slice.name AS slice_name,
+      slice.depth AS slice_depth,
+      span.height,
+      span.leaf_utid
+    FROM experimental_thread_executing_span_critical_path(NULL, $leaf_utid) span
+    JOIN thread_slice slice
+      ON
+        slice.utid = span.utid
+        AND ((slice.ts BETWEEN span.ts AND span.ts + span.dur)
+             OR (span.ts BETWEEN slice.ts AND slice.ts + slice.dur))
+  )
+SELECT
+  id,
+  slice_id,
+  ts,
+  MIN(span_end_ts, slice_end_ts) - ts AS dur,
+  tid,
+  pid,
+  utid,
+  upid,
+  thread_name,
+  process_name,
+  slice_name,
+  slice_depth,
+  height,
+  leaf_utid
+FROM span_start
+WHERE MIN(span_end_ts, slice_end_ts) - ts > 0;
diff --git a/src/trace_processor/perfetto_sql/stdlib/experimental/thread_state_flattened.sql b/src/trace_processor/perfetto_sql/stdlib/experimental/thread_state_flattened.sql
new file mode 100644
index 0000000..e7f6abb
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/experimental/thread_state_flattened.sql
@@ -0,0 +1,175 @@
+--
+-- Copyright 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
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+SELECT import('experimental.flat_slices');
+
+-- Create a table which joins the thread state across the flattened slices.
+CREATE VIRTUAL TABLE internal_experimental_span_joined_thread USING
+  SPAN_JOIN(experimental_slice_flattened PARTITIONED utid, thread_state PARTITIONED utid);
+
+-- Get the thread state breakdown of a flattened slice from it's slice id.
+-- This table pivoted and summed for better visualization and aggragation.
+-- The concept of a "flat slice" is to take the data in the slice table and
+-- remove all notion of nesting. For more information, read the description
+-- of experimental_slice_flattened.
+--
+-- @arg slice_id LONG         Id of the slice of interest.
+--
+-- @column ts                 Id of a slice.
+-- @column dur                Name of the slice
+-- @column utid               Time (ns) spent in Uninterruptible Sleep (non-IO)
+-- @column depth              Time (ns) spent in Uninterruptible Sleep (IO)
+-- @column name               Time (ns) spent in Runnable
+-- @column slice_id           Time (ns) spent in Sleeping
+-- @column track_id           Time (ns) spent in Stopped
+-- @column cpu                Time (ns) spent in Exit (Zombie)
+-- @column state              Time (ns) spent in Task Dead
+-- @column io_wait            Time (ns) spent in Wake Kill
+-- @column blocked_function   Time (ns) spent in Waking
+-- @column waker_utid         Time (ns) spent in Parked
+-- @column irq_context        Time (ns) spent in No Load
+CREATE PERFETTO FUNCTION experimental_get_flattened_thread_state(
+  slice_id LONG, utid LONG)
+RETURNS
+  TABLE(
+    ts LONG,
+    dur LONG,
+    utid LONG,
+    depth LONG,
+    name STRING,
+    slice_id LONG,
+    track_id LONG,
+    cpu INT,
+    state STRING,
+    io_wait INT,
+    blocked_function STRING,
+    waker_utid LONG,
+    irq_context LONG)
+AS
+WITH
+  interesting_slice AS (
+    SELECT ts, dur, slice.track_id AS track_id
+    FROM slice
+    JOIN thread_track
+      ON slice.track_id = thread_track.id
+    JOIN thread
+      USING (utid)
+    WHERE
+      (($slice_id IS NOT NULL AND slice.id = $slice_id) OR ($slice_id IS NULL))
+      AND (($utid IS NOT NULL AND utid = $utid) OR ($utid IS NULL))
+  )
+SELECT
+  ts,
+  dur,
+  utid,
+  depth,
+  name,
+  slice_id,
+  track_id,
+  cpu,
+  state,
+  io_wait,
+  blocked_function,
+  waker_utid,
+  irq_context
+FROM internal_experimental_span_joined_thread
+WHERE
+  track_id = (SELECT track_id FROM interesting_slice)
+  AND ts >= (SELECT ts FROM interesting_slice)
+  AND ts < (SELECT ts + dur FROM interesting_slice);
+
+-- Get the thread state breakdown of a flattened slice from slice id.
+-- This table pivoted and summed for better visualization and aggragation.
+-- The concept of a "flat slice" is to take the data in the slice table and
+-- remove all notion of nesting. For more information, read the description
+-- of experimental_slice_flattened.
+--
+-- @arg slice_id LONG                  Id of the slice of interest.
+--
+-- @column slice_id                    Id of a slice.
+-- @column slice_name                  Name of the slice
+-- @column Uninterruptible_Sleep_nonIO Time (ns) spent in Uninterruptible Sleep (non-IO)
+-- @column Uninterruptible_Sleep_IO    Time (ns) spent in Uninterruptible Sleep (IO)
+-- @column Runnable                    Time (ns) spent in Runnable
+-- @column Sleeping                    Time (ns) spent in Sleeping
+-- @column Stopped                     Time (ns) spent in Stopped
+-- @column Traced                      Time (ns) spent in Traced
+-- @column Exit_Dead                   Time (ns) spent in Exit (Dead)
+-- @column Exit_Zombie                 Time (ns) spent in Exit (Zombie)
+-- @column Task_Dead                   Time (ns) spent in Task Dead
+-- @column Wake_Kill                   Time (ns) spent in Wake Kill
+-- @column Waking                      Time (ns) spent in Waking
+-- @column Parked                      Time (ns) spent in Parked
+-- @column No_Load                     Time (ns) spent in No Load
+-- @column Runnable_Preempted          Time (ns) spent in Runnable (Preempted)
+-- @column Running                     Time (ns) spent in Running
+-- @column Idle                        Time (ns) spent in Idle
+-- @column dur                         Total duration of the slice
+-- @column depth                       Depth of the slice in Perfetto
+CREATE PERFETTO FUNCTION experimental_get_flattened_thread_state_aggregated(
+  slice_id LONG, utid LONG)
+RETURNS
+  TABLE(
+    slice_id LONG,
+    slice_name STRING,
+    Uninterruptible_Sleep_nonIO LONG,
+    Uninterruptible_Sleep_IO LONG,
+    Runnable LONG,
+    Sleeping LONG,
+    Stopped LONG,
+    Traced LONG,
+    Exit_Dead LONG,
+    Exit_Zombie LONG,
+    Task_Dead LONG,
+    Wake_Kill LONG,
+    Waking LONG,
+    Parked LONG,
+    No_Load LONG,
+    Runnable_Preempted LONG,
+    Running LONG,
+    Idle LONG,
+    dur LONG,
+    depth LONG)
+AS
+WITH
+  final_table AS (
+    SELECT *
+    FROM experimental_get_flattened_thread_state($slice_id, $utid)
+  )
+SELECT
+  fs.slice_id,
+  fs.name AS slice_name,
+  SUM(CASE WHEN fs.state = 'D' AND io_wait = 0 THEN fs.dur END)
+    Uninterruptible_Sleep_nonIO,
+  SUM(CASE WHEN fs.state = 'D' AND io_wait = 1 THEN fs.dur END)
+    Uninterruptible_Sleep_IO,
+  SUM(CASE WHEN fs.state = 'R' THEN fs.dur END) Runnable,
+  SUM(CASE WHEN fs.state = 'S' THEN fs.dur END) Sleeping,
+  SUM(CASE WHEN fs.state = 'T' THEN fs.dur END) Stopped,
+  SUM(CASE WHEN fs.state = 't' THEN fs.dur END) Traced,
+  SUM(CASE WHEN fs.state = 'X' THEN fs.dur END) Exit_Dead,
+  SUM(CASE WHEN fs.state = 'Z' THEN fs.dur END) Exit_Zombie,
+  SUM(CASE WHEN fs.state = 'x' THEN fs.dur END) Task_Dead,
+  SUM(CASE WHEN fs.state = 'K' THEN fs.dur END) Wake_Kill,
+  SUM(CASE WHEN fs.state = 'W' THEN fs.dur END) Waking,
+  SUM(CASE WHEN fs.state = 'P' THEN fs.dur END) Parked,
+  SUM(CASE WHEN fs.state = 'N' THEN fs.dur END) No_Load,
+  SUM(CASE WHEN fs.state = 'R+' THEN fs.dur END) Runnable_Preempted,
+  SUM(CASE WHEN fs.state = 'Running' THEN fs.dur END) Running,
+  SUM(CASE WHEN fs.state = 'I' THEN fs.dur END) Idle,
+  SUM(fs.dur) dur,
+  fs.depth
+FROM final_table fs
+GROUP BY fs.slice_id;
\ No newline at end of file
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index a223464..ed72316 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -7601,6 +7601,28 @@
        kUnsetFtraceId,
        66,
        kUnsetSize},
+      {"tracing_mark_write",
+       "samsung",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "pid", 1, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "trace_name", 2, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "trace_begin", 3, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "trace_type", 4, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "value", 5, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       484,
+       kUnsetSize},
       {"sched_switch",
        "sched",
        {
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/samsung/tracing_mark_write/format b/src/traced/probes/ftrace/test/data/synthetic/events/samsung/tracing_mark_write/format
new file mode 100644
index 0000000..553dc87
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/samsung/tracing_mark_write/format
@@ -0,0 +1,14 @@
+name: tracing_mark_write
+ID: 926
+format:
+        field:unsigned short common_type;       offset:0;       size:2; signed:0;
+        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
+        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
+        field:int common_pid;   offset:4;       size:4; signed:1;
+
+        field:int pid;  offset:8;       size:4; signed:1;
+        field:__data_loc char[] trace_name;     offset:12;      size:4; signed:0;
+        field:unsigned int trace_type;  offset:16;      size:4; signed:0;
+        field:int value;        offset:20;      size:4; signed:1;
+
+print fmt: "%c|%d|%s|%d", REC->trace_type, REC->pid, __get_str(trace_name), REC->value
diff --git a/test/trace_processor/diff_tests/parsing/tests.py b/test/trace_processor/diff_tests/parsing/tests.py
index 30aea94..83a986c 100644
--- a/test/trace_processor/diff_tests/parsing/tests.py
+++ b/test/trace_processor/diff_tests/parsing/tests.py
@@ -833,14 +833,14 @@
             "pid": 1,
             "tid": 1,
             "ph": "B",
-            "ts": 1597071955492308000
+            "ts": 1597071955492308
           },
           {
             "name": "add_graph",
             "pid": 1,
             "tid": 1,
             "ph": "E",
-            "ts": 1597071955703771000
+            "ts": 1597071955703771
           }
         ]
         }
@@ -850,7 +850,7 @@
         """,
         out=Csv("""
         "ts","dur","name"
-        -7794778920422990592,211463000000,"add_graph"
+        1597071955492308000,211463000,"add_graph"
         """))
 
   # Parsing sched_blocked_reason
diff --git a/test/trace_processor/diff_tests/slices/tests.py b/test/trace_processor/diff_tests/slices/tests.py
index d03cfff..63d2ebe 100644
--- a/test/trace_processor/diff_tests/slices/tests.py
+++ b/test/trace_processor/diff_tests/slices/tests.py
@@ -162,4 +162,4 @@
         "ThreadControllerImpl::RunTask",174796099970797,186000,0
         "Looper.dispatch: jy3(null)",174800056530797,1368000,0
         "ThreadControllerImpl::RunTask",174800107962797,132000,0
-      """))
+      """))
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/tables/tests.py b/test/trace_processor/diff_tests/tables/tests.py
index 82fe9c3..4cf4e55 100644
--- a/test/trace_processor/diff_tests/tables/tests.py
+++ b/test/trace_processor/diff_tests/tables/tests.py
@@ -263,3 +263,21 @@
         "cpu_track",0
         "cpu_track",1
         """))
+
+  def test_thread_state_flattened_aggregated(self):
+    return DiffTestBlueprint(
+      trace=DataPath('android_monitor_contention_trace.atr'),
+      query="""
+      SELECT import('experimental.thread_state_flattened');
+      select * from experimental_get_flattened_thread_state_aggregated(11155, NULL);
+      """,
+      out=Path('thread_state_flattened_aggregated_csv.out'))
+
+  def test_thread_state_flattened(self):
+    return DiffTestBlueprint(
+      trace=DataPath('android_monitor_contention_trace.atr'),
+      query="""
+      SELECT import('experimental.thread_state_flattened');
+      select * from experimental_get_flattened_thread_state(11155, NULL);
+      """,
+      out=Path('thread_state_flattened_csv.out'))
diff --git a/test/trace_processor/diff_tests/tables/tests_sched.py b/test/trace_processor/diff_tests/tables/tests_sched.py
index f6fc2ef..216c2e8 100644
--- a/test/trace_processor/diff_tests/tables/tests_sched.py
+++ b/test/trace_processor/diff_tests/tables/tests_sched.py
@@ -507,3 +507,69 @@
         1735489999987,45838,158,1,"init","/system/bin/init","traced_probes","/system/bin/traced_probes",4178,"S","[NULL]",30,0,1737061943856,1572057416,"S","[NULL]"
         1735490039439,570799,544,527,"adbd","/apex/com.android.adbd/bin/adbd","init","/system/bin/init","[NULL]","[NULL]","[NULL]",0,1,1735490039439,"[NULL]","[NULL]","[NULL]"
         """))
+
+  def test_thread_executing_span_critical_path_thread_states(self):
+    return DiffTestBlueprint(
+        trace=DataPath('sched_wakeup_trace.atr'),
+        query="""
+        SELECT IMPORT('experimental.thread_executing_span');
+        SELECT
+          ts,
+          dur,
+          tid,
+          pid,
+          thread_name,
+          process_name,
+          state,
+          blocked_function,
+          height
+        FROM experimental_thread_executing_span_critical_path_thread_states(257)
+        ORDER BY ts
+        LIMIT 10
+        """,
+        out=Csv("""
+        "ts","dur","tid","pid","thread_name","process_name","state","blocked_function","height"
+        1736109621029,34116,1469,1469,"m.android.phone","com.android.phone","R","[NULL]",0
+        1736109655145,680044,1469,1469,"m.android.phone","com.android.phone","Running","[NULL]",0
+        1736110335189,83413,657,642,"binder:642_1","system_server","R","[NULL]",1
+        1736110418602,492287,657,642,"binder:642_1","system_server","Running","[NULL]",1
+        1736110910889,122878,1469,1469,"m.android.phone","com.android.phone","R","[NULL]",0
+        1736111033767,282646,1469,1469,"m.android.phone","com.android.phone","Running","[NULL]",0
+        1736111316413,19907,657,642,"binder:642_1","system_server","R","[NULL]",1
+        1736111336320,370659,657,642,"binder:642_1","system_server","Running","[NULL]",1
+        1736111706979,44391,1469,1469,"m.android.phone","com.android.phone","R","[NULL]",0
+        1736111751370,143860,1469,1469,"m.android.phone","com.android.phone","Running","[NULL]",0
+        """))
+
+  def test_thread_executing_span_critical_path_slices(self):
+    return DiffTestBlueprint(
+        trace=DataPath('sched_wakeup_trace.atr'),
+        query="""
+        SELECT IMPORT('experimental.thread_executing_span');
+        SELECT
+          ts,
+          dur,
+          tid,
+          pid,
+          thread_name,
+          process_name,
+          slice_name,
+          slice_depth,
+          height
+        FROM experimental_thread_executing_span_critical_path_slices(257)
+        ORDER BY ts
+        LIMIT 10
+        """,
+        out=Csv("""
+        "ts","dur","tid","pid","thread_name","process_name","slice_name","slice_depth","height"
+        1736110278076,57113,1469,1469,"m.android.phone","com.android.phone","binder transaction",0,0
+        1736110435876,462664,657,642,"binder:642_1","system_server","binder reply",0,1
+        1736110692464,135281,657,642,"binder:642_1","system_server","AIDL::java::INetworkStatsService::getMobileIfaces::server",1,1
+        1736110910889,132674,1469,1469,"m.android.phone","com.android.phone","binder transaction",0,0
+        1736111274404,42009,1469,1469,"m.android.phone","com.android.phone","binder transaction",0,0
+        1736111340019,361607,657,642,"binder:642_1","system_server","binder reply",0,1
+        1736111417370,249758,657,642,"binder:642_1","system_server","AIDL::java::INetworkStatsService::getIfaceStats::server",1,1
+        1736111706979,48463,1469,1469,"m.android.phone","com.android.phone","binder transaction",0,0
+        1736111874030,21200,1469,1469,"m.android.phone","com.android.phone","binder transaction",0,0
+        1736111923740,159330,657,642,"binder:642_1","system_server","binder reply",0,1
+        """))
diff --git a/test/trace_processor/diff_tests/tables/thread_state_flattened_aggregated_csv.out b/test/trace_processor/diff_tests/tables/thread_state_flattened_aggregated_csv.out
new file mode 100644
index 0000000..4982ec3
--- /dev/null
+++ b/test/trace_processor/diff_tests/tables/thread_state_flattened_aggregated_csv.out
@@ -0,0 +1,55 @@
+"slice_id","slice_name","Uninterruptible_Sleep_nonIO","Uninterruptible_Sleep_IO","Runnable","Sleeping","Stopped","Traced","Exit_Dead","Exit_Zombie","Task_Dead","Wake_Kill","Waking","Parked","No_Load","Runnable_Preempted","Running","Idle","dur","depth"
+11155,"android.view.Choreographer$FrameHandler: android.view.Choreographer$FrameDisplayEventReceiver","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",15728,"[NULL]",15728,0
+11156,"Choreographer#doFrame 10831","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",31280,"[NULL]",31280,1
+11158,"animation","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",74845,"[NULL]",74845,2
+11159,"ShadeListBuilder.buildList","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",239109,"[NULL]",239109,3
+11160,"ShadeListBuilder.filterNotifs","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",11966,"[NULL]",11966,4
+11161,"ShadeListBuilder.groupNotifs","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",3080,"[NULL]",3080,4
+11162,"ShadeListBuilder.pruneIncompleteGroups","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",25794,"[NULL]",25794,4
+11163,"ShadeListBuilder.dispatchOnBeforeTransformGroups","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",31921,"[NULL]",31921,4
+11164,"ShadeListBuilder.promoteNotifs","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",2000,"[NULL]",2000,4
+11165,"ShadeListBuilder.pruneIncompleteGroups","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",2867,"[NULL]",2867,4
+11166,"ShadeListBuilder.dispatchOnBeforeSort","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",2397,"[NULL]",2397,4
+11167,"ShadeListBuilder.assignSections","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",2257,"[NULL]",2257,4
+11168,"ShadeListBuilder.notifySectionEntriesUpdated","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",201950,"[NULL]",201950,4
+11171,"Choreographer#scheduleVsyncLocked","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",11216,"[NULL]",11216,5
+11172,"AIDL::cpp::IDisplayEventConnection::requestNextVsync::cppClient","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",24872,"[NULL]",24872,6
+11175,"ShadeListBuilder.sortListAndGroups","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",23215,"[NULL]",23215,4
+11177,"ShadeListBuilder.dispatchOnBeforeFinalizeFilter","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",102928,"[NULL]",102928,4
+11180,"ShadeListBuilder.filterNotifs","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",3361,"[NULL]",3361,4
+11181,"ShadeListBuilder.pruneIncompleteGroups","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",4862,"[NULL]",4862,4
+11182,"ShadeListBuilder.logChanges","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",8882,"[NULL]",8882,4
+11183,"ShadeListBuilder.freeEmptyGroups","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",17089,"[NULL]",17089,4
+11184,"ShadeListBuilder.cleanupPluggables","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",33465,"[NULL]",33465,4
+11185,"ShadeListBuilder.dispatchOnBeforeRenderList","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",214867,"[NULL]",214867,4
+11186,"ShadeListBuilder.onRenderList","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",24558,"[NULL]",24558,4
+11187,"RenderStageManager.onRenderList","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",60009,"[NULL]",60009,5
+11188,"ShadeViewManager.onRenderList","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",29508,"[NULL]",29508,6
+11189,"NodeSpecBuilder.buildNodeSpec","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",57434,"[NULL]",57434,7
+11190,"ShadeViewDiffer.applySpec","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",284569,2382651,"[NULL]",2667220,7
+11191,"Defining Lcom/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger$logDetachingChild$2;","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",108424,"[NULL]",108424,8
+11192,"binder transaction","[NULL]","[NULL]",25131,311364,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",33158,"[NULL]",369653,8
+11195,"Defining Lcom/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper$2$$ExternalSyntheticLambda0;","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",82320,"[NULL]",82320,8
+11203,"NSSLC.updateShowEmptyShadeView","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",87035,"[NULL]",87035,8
+11206,"RenderStageManager.dispatchOnAfterRenderList","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",49003,"[NULL]",49003,6
+11207,"NotifLiveDataStore.setActiveNotifList","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",105577,"[NULL]",105577,7
+11208,"NotifLiveData(hasActiveNotifs).dispatchToSyncObservers","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",57076,"[NULL]",57076,8
+11209,"StackCoordinator.onAfterRenderList","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",69084,"[NULL]",69084,7
+11210,"NSSLC.updateFooter","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",685660,"[NULL]",685660,8
+11211,"NSSLC.updateShowEmptyShadeView","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",248061,"[NULL]",248061,8
+11212,"NotificationIconAreaController.updateNotificationIcons","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",768503,"[NULL]",768503,8
+11214,"Defining Lcom/android/systemui/statusbar/phone/NotificationIconContainer$$ExternalSyntheticLambda0;","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",74629,"[NULL]",74629,9
+11219,"RenderStageManager.dispatchOnAfterRenderGroups","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",20355,"[NULL]",20355,6
+11220,"RenderStageManager.dispatchOnAfterRenderEntries","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",3565,"[NULL]",3565,6
+11221,"ShadeListBuilder.logEndBuildList","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",40367,81151,"[NULL]",121518,4
+11223,"traversal","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",688007,"[NULL]",688007,2
+11224,"measure","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",11410,"[NULL]",11410,3
+11225,"NotificationShadeWindowView#onMeasure","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",470142,"[NULL]",470142,4
+11226,"NotificationStackScrollLayout#onMeasure","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",141670,"[NULL]",141670,5
+11227,"NotificationStackScrollLayout#onMeasure","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",12506,"[NULL]",12506,5
+11228,"layout","[NULL]","[NULL]",6177,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",2622099,"[NULL]",2628276,3
+11241,"measure","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",402484,"[NULL]",402484,3
+11242,"layout","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",317593,"[NULL]",317593,3
+11243,"draw","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",26905,"[NULL]",26905,3
+11244,"Record View#draw()","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",172280,"[NULL]",172280,4
+11245,"postAndWait","[NULL]","[NULL]",25748,147072,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",31682,"[NULL]",204502,4
diff --git a/test/trace_processor/diff_tests/tables/thread_state_flattened_csv.out b/test/trace_processor/diff_tests/tables/thread_state_flattened_csv.out
new file mode 100644
index 0000000..162c42f
--- /dev/null
+++ b/test/trace_processor/diff_tests/tables/thread_state_flattened_csv.out
@@ -0,0 +1,126 @@
+"ts","dur","utid","depth","name","slice_id","track_id","cpu","state","io_wait","blocked_function","waker_utid","irq_context"
+1738888555782,12607,251,0,"android.view.Choreographer$FrameHandler: android.view.Choreographer$FrameDisplayEventReceiver",11155,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888568389,17991,251,1,"Choreographer#doFrame 10831",11156,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888586380,71961,251,2,"animation",11158,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888658341,65346,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888723687,11966,251,4,"ShadeListBuilder.filterNotifs",11160,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888735653,9631,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888745284,3080,251,4,"ShadeListBuilder.groupNotifs",11161,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888748364,11493,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888759857,25794,251,4,"ShadeListBuilder.pruneIncompleteGroups",11162,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888785651,10182,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888795833,31921,251,4,"ShadeListBuilder.dispatchOnBeforeTransformGroups",11163,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888827754,8463,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888836217,2000,251,4,"ShadeListBuilder.promoteNotifs",11164,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888838217,6360,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888844577,2867,251,4,"ShadeListBuilder.pruneIncompleteGroups",11165,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888847444,10753,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888858197,2397,251,4,"ShadeListBuilder.dispatchOnBeforeSort",11166,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888860594,8103,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888868697,2257,251,4,"ShadeListBuilder.assignSections",11167,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888870954,7849,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738888878803,145337,251,4,"ShadeListBuilder.notifySectionEntriesUpdated",11168,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889024140,8745,251,5,"Choreographer#scheduleVsyncLocked",11171,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889032885,24872,251,6,"AIDL::cpp::IDisplayEventConnection::requestNextVsync::cppClient",11172,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889057757,2471,251,5,"Choreographer#scheduleVsyncLocked",11171,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889060228,56613,251,4,"ShadeListBuilder.notifySectionEntriesUpdated",11168,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889116841,9059,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889125900,23215,251,4,"ShadeListBuilder.sortListAndGroups",11175,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889149115,7998,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889157113,102928,251,4,"ShadeListBuilder.dispatchOnBeforeFinalizeFilter",11177,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889260041,8768,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889268809,3361,251,4,"ShadeListBuilder.filterNotifs",11180,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889272170,7933,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889280103,4862,251,4,"ShadeListBuilder.pruneIncompleteGroups",11181,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889284965,26046,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889311011,8882,251,4,"ShadeListBuilder.logChanges",11182,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889319893,7628,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889327521,17089,251,4,"ShadeListBuilder.freeEmptyGroups",11183,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889344610,7672,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889352282,33465,251,4,"ShadeListBuilder.cleanupPluggables",11184,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889385747,7698,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889393445,214867,251,4,"ShadeListBuilder.dispatchOnBeforeRenderList",11185,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889608312,7685,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889615997,22375,251,4,"ShadeListBuilder.onRenderList",11186,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889638372,12093,251,5,"RenderStageManager.onRenderList",11187,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889650465,16751,251,6,"ShadeViewManager.onRenderList",11188,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889667216,57434,251,7,"NodeSpecBuilder.buildNodeSpec",11189,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889724650,10641,251,6,"ShadeViewManager.onRenderList",11188,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738889735291,281631,251,7,"ShadeViewDiffer.applySpec",11190,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738890016922,108424,251,8,"Defining Lcom/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger$logDetachingChild$2;",11191,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738890125346,542783,251,7,"ShadeViewDiffer.applySpec",11190,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738890668129,30726,251,8,"binder transaction",11192,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738890698855,311364,251,8,"binder transaction",11192,1257,"[NULL]","S","[NULL]","[NULL]","[NULL]","[NULL]"
+1738891010219,25131,251,8,"binder transaction",11192,1257,"[NULL]","R","[NULL]","[NULL]",495,0
+1738891035350,2432,251,8,"binder transaction",11192,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738891037782,275245,251,7,"ShadeViewDiffer.applySpec",11190,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738891313027,82320,251,8,"Defining Lcom/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper$2$$ExternalSyntheticLambda0;",11195,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738891395347,62797,251,7,"ShadeViewDiffer.applySpec",11190,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738891458144,241288,251,7,"ShadeViewDiffer.applySpec",11190,1257,"[NULL]","R+","[NULL]","[NULL]","[NULL]","[NULL]"
+1738891699432,648477,251,7,"ShadeViewDiffer.applySpec",11190,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738892347909,87035,251,8,"NSSLC.updateShowEmptyShadeView",11203,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738892434944,126991,251,7,"ShadeViewDiffer.applySpec",11190,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738892561935,35098,251,7,"ShadeViewDiffer.applySpec",11190,1257,"[NULL]","R+","[NULL]","[NULL]","[NULL]","[NULL]"
+1738892597033,4186,251,7,"ShadeViewDiffer.applySpec",11190,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738892601219,8183,251,7,"ShadeViewDiffer.applySpec",11190,1257,"[NULL]","R+","[NULL]","[NULL]","[NULL]","[NULL]"
+1738892609402,440541,251,7,"ShadeViewDiffer.applySpec",11190,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738893049943,2116,251,6,"ShadeViewManager.onRenderList",11188,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738893052059,12460,251,5,"RenderStageManager.onRenderList",11187,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738893064519,29926,251,6,"RenderStageManager.dispatchOnAfterRenderList",11206,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738893094445,103005,251,7,"NotifLiveDataStore.setActiveNotifList",11207,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738893197450,57076,251,8,"NotifLiveData(hasActiveNotifs).dispatchToSyncObservers",11208,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738893254526,2572,251,7,"NotifLiveDataStore.setActiveNotifList",11207,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738893257098,14001,251,6,"RenderStageManager.dispatchOnAfterRenderList",11206,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738893271099,41258,251,7,"StackCoordinator.onAfterRenderList",11209,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738893312357,685660,251,8,"NSSLC.updateFooter",11210,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738893998017,9168,251,7,"StackCoordinator.onAfterRenderList",11209,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738894007185,248061,251,8,"NSSLC.updateShowEmptyShadeView",11211,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738894255246,15482,251,7,"StackCoordinator.onAfterRenderList",11209,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738894270728,349348,251,8,"NotificationIconAreaController.updateNotificationIcons",11212,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738894620076,74629,251,9,"Defining Lcom/android/systemui/statusbar/phone/NotificationIconContainer$$ExternalSyntheticLambda0;",11214,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738894694705,419155,251,8,"NotificationIconAreaController.updateNotificationIcons",11212,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895113860,3176,251,7,"StackCoordinator.onAfterRenderList",11209,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895117036,5076,251,6,"RenderStageManager.dispatchOnAfterRenderList",11206,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895122112,16102,251,5,"RenderStageManager.onRenderList",11187,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895138214,20355,251,6,"RenderStageManager.dispatchOnAfterRenderGroups",11219,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895158569,9440,251,5,"RenderStageManager.onRenderList",11187,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895168009,3565,251,6,"RenderStageManager.dispatchOnAfterRenderEntries",11220,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895171574,9914,251,5,"RenderStageManager.onRenderList",11187,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895181488,2183,251,4,"ShadeListBuilder.onRenderList",11186,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895183671,7205,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895190876,72666,251,4,"ShadeListBuilder.logEndBuildList",11221,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895263542,32040,251,4,"ShadeListBuilder.logEndBuildList",11221,1257,"[NULL]","R+","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895295582,4470,251,4,"ShadeListBuilder.logEndBuildList",11221,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895300052,8327,251,4,"ShadeListBuilder.logEndBuildList",11221,1257,"[NULL]","R+","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895308379,4015,251,4,"ShadeListBuilder.logEndBuildList",11221,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895312394,3237,251,3,"ShadeListBuilder.buildList",11159,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895315631,2884,251,2,"animation",11158,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895318515,9168,251,1,"Choreographer#doFrame 10831",11156,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895327683,54243,251,2,"traversal",11223,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895381926,9329,251,3,"measure",11224,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895391255,333001,251,4,"NotificationShadeWindowView#onMeasure",11225,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895724256,141670,251,5,"NotificationStackScrollLayout#onMeasure",11226,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895865926,111203,251,4,"NotificationShadeWindowView#onMeasure",11225,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895977129,12506,251,5,"NotificationStackScrollLayout#onMeasure",11227,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738895989635,25938,251,4,"NotificationShadeWindowView#onMeasure",11225,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738896015573,2081,251,3,"measure",11224,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738896017654,7655,251,2,"traversal",11223,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738896025309,1293949,251,3,"layout",11228,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738897319258,6177,251,3,"layout",11228,1257,"[NULL]","R","[NULL]","[NULL]","[NULL]","[NULL]"
+1738897325435,1328150,251,3,"layout",11228,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738898653585,597557,251,2,"traversal",11223,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738899251142,402484,251,3,"measure",11241,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738899653626,11058,251,2,"traversal",11223,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738899664684,317593,251,3,"layout",11242,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738899982277,14746,251,2,"traversal",11223,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738899997023,14681,251,3,"draw",11243,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738900011704,172280,251,4,"Record View#draw()",11244,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738900183984,10210,251,3,"draw",11243,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738900194194,27982,251,4,"postAndWait",11245,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738900222176,147072,251,4,"postAndWait",11245,1257,"[NULL]","S","[NULL]","[NULL]","[NULL]","[NULL]"
+1738900369248,25748,251,4,"postAndWait",11245,1257,"[NULL]","R","[NULL]","[NULL]",703,0
+1738900394996,3700,251,4,"postAndWait",11245,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738900398696,2014,251,3,"draw",11243,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738900400710,2748,251,2,"traversal",11223,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738900403458,4121,251,1,"Choreographer#doFrame 10831",11156,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
+1738900407579,3121,251,0,"android.view.Choreographer$FrameHandler: android.view.Choreographer$FrameDisplayEventReceiver",11155,1257,1,"Running","[NULL]","[NULL]","[NULL]","[NULL]"
diff --git a/ui/release/channels.json b/ui/release/channels.json
index dd3a4fb..d8b0bf8 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -6,7 +6,7 @@
     },
     {
       "name": "canary",
-      "rev": "7254c51a1d5a5a81aeb7c790d2bac38af162f6d7"
+      "rev": "c69b33b9abcc20fad9ad5f39de883216e4b43130"
     },
     {
       "name": "autopush",
diff --git a/ui/src/common/colorizer.ts b/ui/src/common/colorizer.ts
index 2b9006f..c527f02 100644
--- a/ui/src/common/colorizer.ts
+++ b/ui/src/common/colorizer.ts
@@ -190,3 +190,16 @@
 export function colorCompare(x: Color, y: Color) {
   return (x.h - y.h) || (x.s - y.s) || (x.l - y.l);
 }
+
+export function getColorForSlice(
+    sliceName: string, hasFocus: boolean|null): Color {
+  const name = sliceName.replace(/( )?\d+/g, '');
+  const [hue, saturation, lightness] = hslForSlice(name, hasFocus);
+
+  return {
+    c: cachedHsluvToHex(hue, saturation, lightness),
+    h: hue,
+    s: saturation,
+    l: lightness,
+  };
+}
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 4b5c6d9..05b4498 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -22,6 +22,7 @@
 import {TrackCreator} from '../frontend/track';
 import {trackRegistry} from '../frontend/track_registry';
 import {
+  BasePlugin,
   Command,
   EngineProxy,
   MetricVisualisation,
@@ -29,6 +30,7 @@
   PluginClass,
   PluginContext,
   PluginInfo,
+  StatefulPlugin,
   Store,
   TracePluginContext,
   TrackInfo,
@@ -41,6 +43,7 @@
 // Every plugin gets its own PluginContext. This is how we keep track
 // what each plugin is doing and how we can blame issues on particular
 // plugins.
+// The PluginContext exists for the whole duration a plugin is active.
 export class PluginContextImpl implements PluginContext, Disposable {
   readonly pluginId: string;
   readonly viewer: ViewerProxy;
@@ -68,7 +71,10 @@
   }
 }
 
-// Implementation the trace plugin context with trace-relevant properties.
+// This TracePluginContext implementation provides the plugin access to trace
+// related resources, such as the engine and the store.
+// The TracePluginContext exists for the whole duration a plugin is active AND a
+// trace is loaded.
 class TracePluginContextImpl<T> implements TracePluginContext<T>, Disposable {
   private ctx: PluginContext;
   readonly engine: EngineProxy;
@@ -111,8 +117,8 @@
 
 interface PluginDetails<T> {
   plugin: Plugin<T>;
-  context: PluginContextImpl;
-  traceContext?: TracePluginContextImpl<T>;
+  context: PluginContext&Disposable;
+  traceContext?: TracePluginContext<T>&Disposable;
 }
 
 function isPluginClass<T>(v: unknown): v is PluginClass<T> {
@@ -151,14 +157,12 @@
       return;
     }
 
-    // This is where the plugin context is created, and where we call
-    // onInit() on the plugin.
     const pluginInfo = this.registry.get(id);
     const plugin = makePlugin(pluginInfo);
-    const viewerProxy = viewer.getProxy(id);
 
-    // Create a proxy store for our plugin to use.
+    const viewerProxy = viewer.getProxy(id);
     const context = new PluginContextImpl(id, viewerProxy);
+
     plugin.onActivate && plugin.onActivate(context);
 
     const pluginDetails: PluginDetails<unknown> = {
@@ -166,30 +170,28 @@
       context,
     };
 
-    // If we already have a trace when the plugin is activated, call
-    // onTraceLoad() on the plugin and store the traceContext.
+    // If a trace is already loaded when plugin is activated, make sure to
+    // call onTraceLoad().
     if (this.engine) {
-      this.initTracePlugin(pluginDetails, this.engine, id);
+      doPluginTraceLoad(pluginDetails, this.engine, id);
     }
 
     this.plugins.set(id, pluginDetails);
   }
 
-  deactivatePlugin(pluginId: string): void {
-    const pluginDetails = this.getPluginContext(pluginId);
+  deactivatePlugin(id: string): void {
+    const pluginDetails = this.getPluginContext(id);
     if (pluginDetails === undefined) {
       return;
     }
-    const {context, plugin, traceContext} = pluginDetails;
+    const {context, plugin} = pluginDetails;
 
-    if (traceContext) {
-      plugin.onTraceUnload && plugin.onTraceUnload(traceContext);
-    }
+    maybeDoPluginTraceUnload(pluginDetails);
 
     plugin.onDeactivate && plugin.onDeactivate(context);
     context.dispose();
 
-    this.plugins.delete(pluginId);
+    this.plugins.delete(id);
   }
 
   isActive(pluginId: string): boolean {
@@ -214,47 +216,13 @@
   onTraceLoad(engine: Engine): void {
     this.engine = engine;
     for (const [id, pluginDetails] of this.plugins) {
-      this.initTracePlugin(pluginDetails, engine, id);
+      doPluginTraceLoad(pluginDetails, engine, id);
     }
   }
 
-  private initTracePlugin(
-      pluginDetails: PluginDetails<unknown>, engine: Engine, id: string): void {
-    const {plugin, context} = pluginDetails;
-
-    const engineProxy = engine.getProxy(id);
-    if (plugin.migrate) {
-      // Extract the initial state and migrate.
-      const initialState = globals.store.state.plugins[id];
-      const migratedState = plugin.migrate(initialState);
-
-      // Write the the migrated state back to our root store.
-      globals.store.edit((draft) => {
-        draft.plugins[id] = migratedState;
-      });
-    }
-
-    const proxyStore = globals.store.createProxy<unknown>(['plugins', id]);
-    const traceCtx =
-        new TracePluginContextImpl(context, proxyStore, engineProxy);
-
-    // TODO(stevegolton): We should probably wait for this to complete.
-    plugin.onTraceLoad && plugin.onTraceLoad(traceCtx);
-    pluginDetails.traceContext = traceCtx;
-  }
-
   onTraceClose() {
     for (const pluginDetails of this.plugins.values()) {
-      const {traceContext, plugin} = pluginDetails;
-
-      if (traceContext) {
-        if (plugin.onTraceUnload) {
-          plugin.onTraceUnload(traceContext);
-        }
-        traceContext.dispose();
-      }
-
-      pluginDetails.traceContext = undefined;
+      maybeDoPluginTraceUnload(pluginDetails);
     }
     this.engine = undefined;
   }
@@ -264,7 +232,7 @@
       const plugin = ctx.plugin;
       let commands: Command[] = [];
 
-      if (plugin && plugin.commands) {
+      if (plugin.commands) {
         commands = commands.concat(plugin.commands(ctx.context));
       }
 
@@ -288,6 +256,59 @@
   }
 }
 
+function isStatefulPlugin<T>(plugin: BasePlugin<T>|
+                             StatefulPlugin<T>): plugin is StatefulPlugin<T> {
+  return 'migrate' in plugin;
+}
+
+function doPluginTraceLoad<T>(
+    pluginDetails: PluginDetails<T>, engine: Engine, pluginId: string): void {
+  const {plugin, context} = pluginDetails;
+
+  const engineProxy = engine.getProxy(pluginId);
+
+  // Migrate state & write back to store.
+  if (isStatefulPlugin(plugin)) {
+    const initialState = globals.store.state.plugins[pluginId];
+    const migratedState = plugin.migrate(initialState);
+    globals.store.edit((draft) => {
+      draft.plugins[pluginId] = migratedState;
+    });
+
+    const proxyStore = globals.store.createProxy<T>(['plugins', pluginId]);
+    const traceCtx =
+        new TracePluginContextImpl(context, proxyStore, engineProxy);
+    pluginDetails.traceContext = traceCtx;
+
+    // TODO(stevegolton): Await onTraceLoad.
+    plugin.onTraceLoad && plugin.onTraceLoad(traceCtx);
+  } else {
+    // Stateless plugin i.e. the plugin's state type is undefined.
+    // Just provide a store proxy over this plugin's state, the plugin can work
+    // the state out for itself if it wants to, but we're not going to help it
+    // out by calling migrate().
+    const proxyStore = globals.store.createProxy<T>(['plugins', pluginId]);
+    const traceCtx =
+        new TracePluginContextImpl(context, proxyStore, engineProxy);
+    pluginDetails.traceContext = traceCtx;
+
+    // TODO(stevegolton): Await onTraceLoad.
+    plugin.onTraceLoad && plugin.onTraceLoad(traceCtx);
+  }
+}
+
+function maybeDoPluginTraceUnload(pluginDetails: PluginDetails<unknown>): void {
+  const {traceContext, plugin} = pluginDetails;
+
+  if (traceContext) {
+    // TODO(stevegolton): Await onTraceUnload.
+    plugin.onTraceUnload && plugin.onTraceUnload(traceContext);
+    traceContext.dispose();
+    pluginDetails.traceContext = undefined;
+  }
+}
+
+
 // TODO(hjd): Sort out the story for global singletons like these:
 export const pluginRegistry = new PluginRegistry();
 export const pluginManager = new PluginManager(pluginRegistry);
diff --git a/ui/src/common/plugins_unittest.ts b/ui/src/common/plugins_unittest.ts
index 0c7e84e..5be1f4a 100644
--- a/ui/src/common/plugins_unittest.ts
+++ b/ui/src/common/plugins_unittest.ts
@@ -12,36 +12,96 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Plugin, PluginContext} from '../public';
+import {globals} from '../frontend/globals';
+import {Plugin} from '../public';
 
+import {createEmptyState} from './empty_state';
+import {Engine} from './engine';
 import {PluginManager, PluginRegistry} from './plugins';
 import {ViewerImpl} from './viewer';
 
-const viewer = new ViewerImpl();
+class FakeEngine extends Engine {
+  id: string = 'TestEngine';
 
-class FooPlugin implements Plugin {
-  onActivate(_: PluginContext): void {}
+  rpcSendRequestBytes(_data: Uint8Array) {}
 }
 
-test('can activate plugin', () => {
-  const registry = new PluginRegistry();
-  registry.register({
-    pluginId: 'foo',
-    plugin: FooPlugin,
-  });
-  const manager = new PluginManager(registry);
-  manager.activatePlugin('foo', viewer);
-  expect(manager.isActive('foo')).toBe(true);
-});
+function makeMockPlugin(): Plugin<any> {
+  return {
+    migrate: jest.fn(),
+    onActivate: jest.fn(),
+    onDeactivate: jest.fn(),
+    onTraceLoad: jest.fn(),
+    onTraceUnload: jest.fn(),
+  };
+}
 
-test('can deactivate plugin', () => {
-  const registry = new PluginRegistry();
-  registry.register({
-    pluginId: 'foo',
-    plugin: FooPlugin,
+const viewer = new ViewerImpl();
+const engine = new FakeEngine();
+globals.initStore(createEmptyState());
+
+// We use `any` here to avoid checking possibly undefined types in tests.
+let mockPlugin: any;
+let manager: any;
+
+describe('PluginManger', () => {
+  beforeEach(() => {
+    mockPlugin = makeMockPlugin();
+    const registry = new PluginRegistry();
+    registry.register({
+      pluginId: 'foo',
+      plugin: mockPlugin,
+    });
+    manager = new PluginManager(registry);
   });
-  const manager = new PluginManager(registry);
-  manager.activatePlugin('foo', viewer);
-  manager.deactivatePlugin('foo');
-  expect(manager.isActive('foo')).toBe(false);
+
+  it('can activate plugin', () => {
+    manager.activatePlugin('foo', viewer);
+
+    expect(manager.isActive('foo')).toBe(true);
+    expect(mockPlugin.onActivate).toHaveBeenCalledTimes(1);
+  });
+
+  it('can deactivate plugin', () => {
+    manager.activatePlugin('foo', viewer);
+    manager.deactivatePlugin('foo');
+
+    expect(manager.isActive('foo')).toBe(false);
+    expect(mockPlugin.onDeactivate).toHaveBeenCalledTimes(1);
+  });
+
+  it('invokes onTraceLoad when trace is loaded', () => {
+    manager.activatePlugin('foo', viewer);
+    manager.onTraceLoad(engine);
+
+    expect(mockPlugin.onTraceLoad).toHaveBeenCalledTimes(1);
+  });
+
+  it('invokes onTraceLoad when plugin activated while trace loaded', () => {
+    manager.onTraceLoad(engine);
+    manager.activatePlugin('foo', viewer);
+
+    expect(mockPlugin.onTraceLoad).toHaveBeenCalledTimes(1);
+  });
+
+  it('invokes onTraceUnload when plugin deactivated while trace loaded', () => {
+    manager.activatePlugin('foo', viewer);
+    manager.onTraceLoad(engine);
+    manager.deactivatePlugin('foo');
+
+    expect(mockPlugin.onTraceUnload).toHaveBeenCalledTimes(1);
+  });
+
+  it('does not invoke migrate at activation time', () => {
+    manager.activatePlugin('foo', viewer);
+
+    expect(mockPlugin.migrate).not.toHaveBeenCalled();
+  });
+
+  it('invokes migrate when trace is loaded', () => {
+    manager.activatePlugin('foo', viewer);
+    manager.onTraceLoad(engine);
+
+    expect(mockPlugin.migrate).toHaveBeenCalledTimes(1);
+  });
 });
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index ec6961e..881c26f 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -17,7 +17,6 @@
 import {cropText, drawIncompleteSlice} from '../common/canvas_utils';
 import {
   colorCompare,
-  colorToStr,
   UNEXPECTED_PINK_COLOR,
 } from '../common/colorizer';
 import {LONG, NUM} from '../common/query_result';
@@ -32,6 +31,7 @@
 
 import {checkerboardExcept} from './checkerboard';
 import {globals} from './globals';
+import {cachedHsluvToHex} from './hsluv_cache';
 import {Slice} from './slice';
 import {DEFAULT_SLICE_LAYOUT, SliceLayout} from './slice_layout';
 import {constraintsToQuerySuffix} from './sql_utils';
@@ -401,7 +401,7 @@
     for (const slice of vizSlicesByColor) {
       if (slice.color !== lastColor) {
         lastColor = slice.color;
-        ctx.fillStyle = colorToStr(slice.color);
+        ctx.fillStyle = slice.color.c;
       }
       const y = padding + slice.depth * (sliceHeight + rowSpacing);
       if (slice.flags & SLICE_FLAGS_INSTANT) {
@@ -461,7 +461,7 @@
       const slice = this.selectedSlice;
       const color = slice.color;
       const y = padding + slice.depth * (sliceHeight + rowSpacing);
-      ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`;
+      ctx.strokeStyle = cachedHsluvToHex(color.h, 100, 10);
       ctx.beginPath();
       const THICKNESS = 3;
       ctx.lineWidth = THICKNESS;
diff --git a/ui/src/frontend/named_slice_track.ts b/ui/src/frontend/named_slice_track.ts
index ac1ba8e..1601b70 100644
--- a/ui/src/frontend/named_slice_track.ts
+++ b/ui/src/frontend/named_slice_track.ts
@@ -14,8 +14,7 @@
 
 import {Actions} from '../common/actions';
 import {
-  Color,
-  hslForSlice,
+  getColorForSlice,
 } from '../common/colorizer';
 import {STR_NULL} from '../common/query_result';
 
@@ -59,10 +58,7 @@
     const baseSlice = super.rowToSlice(row);
     // Ignore PIDs or numeric arguments when hashing.
     const name = row.name || '';
-    const nameForHashing = name.replace(/\s?\d+/g, '');
-    const hsl = hslForSlice(nameForHashing, /* isSelected=*/ false);
-    // We cache the color so we hash only once per query.
-    const baseColor: Color = {c: '', h: hsl[0], s: hsl[1], l: hsl[2]};
+    const baseColor = getColorForSlice(name, false);
     return {...baseSlice, title: name, baseColor};
   }
 
diff --git a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
index 441a28b..5a9dec2 100644
--- a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
+++ b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
@@ -16,6 +16,7 @@
   Command,
   Plugin,
   PluginContext,
+  PluginInfo,
 } from '../../public';
 
 // This is just an example plugin, used to prove that the plugin system works.
@@ -35,7 +36,7 @@
   }
 }
 
-export const plugin = {
+export const plugin: PluginInfo = {
   pluginId: 'dev.perfetto.ExampleSimpleCommand',
   plugin: ExampleSimpleCommand,
 };
diff --git a/ui/src/plugins/dev.perfetto.ExampleState/index.ts b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
index 4303419..ca7779e 100644
--- a/ui/src/plugins/dev.perfetto.ExampleState/index.ts
+++ b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
@@ -27,12 +27,13 @@
 // This example plugin shows using state that is persisted in the
 // permalink.
 class ExampleState implements Plugin<State> {
-  migrate(_initialState: unknown): State {
-    // TODO(hjd): Show validation example.
-
-    return {
-      counter: 0,
-    };
+  migrate(initialState: unknown): State {
+    if (initialState && typeof initialState === 'object' &&
+        'counter' in initialState && typeof initialState.counter === 'number') {
+      return {counter: initialState.counter};
+    } else {
+      return {counter: 0};
+    }
   }
 
   onActivate(_: PluginContext): void {
@@ -56,7 +57,7 @@
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginInfo<State> = {
   pluginId: 'dev.perfetto.ExampleState',
   plugin: ExampleState,
 };
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 3a3c67a..527c582 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -163,33 +163,36 @@
 // Similar to PluginContext but with additional properties to operate on the
 // currently loaded trace. Passed to trace-relevant hooks instead of
 // PluginContext.
-// TODO(stevegolton): I'm not entirely sold on this approach. For one, it means
-// commands don't get to access the engine as they only get a PluginContext,
-// whereas if they got a TracePluginContext they could only get evaluated
-// when a trace is loaded. Thus, this needs a rethink.
-export interface TracePluginContext<T = unknown> extends PluginContext {
+export interface TracePluginContext<T = undefined> extends PluginContext {
   readonly engine: EngineProxy;
   readonly store: Store<T>;
 }
 
-// All trace plugins must implement this interface.
-export interface Plugin<T = unknown> {
-  // Function to migrate the persistent state.
-  migrate?(initialState: unknown): T;
-
+export interface BasePlugin<State> {
   // Lifecycle methods.
   onActivate(ctx: PluginContext): void;
-  onTraceLoad?(ctx: TracePluginContext<T>): Promise<void>;
-  onTraceUnload?(ctx: TracePluginContext<T>): Promise<void>;
+  onTraceLoad?(ctx: TracePluginContext<State>): Promise<void>;
+  onTraceUnload?(ctx: TracePluginContext<State>): Promise<void>;
   onDeactivate?(ctx: PluginContext): void;
 
-  // Legacy extension points.
+  // Extension points.
   commands?(ctx: PluginContext): Command[];
-  traceCommands?(ctx: TracePluginContext<T>): Command[];
+  traceCommands?(ctx: TracePluginContext<State>): Command[];
   metricVisualisations?(ctx: PluginContext): MetricVisualisation[];
-  findPotentialTracks?(ctx: TracePluginContext<T>): Promise<TrackInfo[]>;
+  findPotentialTracks?(ctx: TracePluginContext<State>): Promise<TrackInfo[]>;
 }
 
+export interface StatefulPlugin<State> extends BasePlugin<State> {
+  // Function to migrate the persistent state.
+  migrate(initialState: unknown): State;
+}
+
+// Generic interface all plugins must implement.
+// If a state type is passed, the plugin must implement migrate(). Otherwise if
+// the state type is omitted, migrate need not be defined.
+export type Plugin<State = undefined> =
+    State extends undefined ? BasePlugin<State>: StatefulPlugin<State>;
+
 // This interface defines what a plugin factory should look like.
 // This can be defined in the plugin class definition by defining a constructor
 // and the relevant static methods:
@@ -236,7 +239,7 @@
 // implementations.
 export type PluginFactory<T> = PluginClass<T>|Plugin<T>|(() => Plugin<T>);
 
-export interface PluginInfo<T = unknown> {
+export interface PluginInfo<T = undefined> {
   // A unique string for your plugin. To ensure the name is unique you
   // may wish to use a URL with reversed components in the manner of
   // Java package names.
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
index 5f8ae03..370c754 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
@@ -14,11 +14,15 @@
 
 import {v4 as uuidv4} from 'uuid';
 
+import {
+  getColorForSlice,
+} from '../../common/colorizer';
 import {Engine} from '../../common/engine';
 import {
   generateSqlWithInternalLayout,
 } from '../../common/internal_layout_utils';
 import {PrimaryTrackSortKey, SCROLLING_TRACK_GROUP} from '../../common/state';
+import {globals} from '../../frontend/globals';
 import {
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
@@ -30,8 +34,13 @@
 } from '../custom_sql_table_slices';
 
 import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
-import {ScrollJankTracks as DecideTracksResult} from './index';
-import {ScrollJankPluginState} from './index';
+import {
+  ScrollJankPluginState,
+  ScrollJankTracks as DecideTracksResult,
+} from './index';
+import {DEEP_RED_COLOR, RED_COLOR} from './jank_colors';
+
+const JANKY_LATENCY_NAME = 'Janky EventLatency';
 
 export interface EventLatencyTrackTypes extends NamedSliceTrackTypes {
   config: {baseTable: string;}
@@ -80,6 +89,29 @@
     };
   }
 
+  onUpdatedSlices(slices: EventLatencyTrackTypes['slice'][]) {
+    for (const slice of slices) {
+      const currentSelection = globals.state.currentSelection;
+      const isSelected = currentSelection &&
+          currentSelection.kind === 'GENERIC_SLICE' &&
+          currentSelection.id !== undefined && currentSelection.id === slice.id;
+
+      const highlighted = globals.state.highlightedSliceId === slice.id;
+      const hasFocus = highlighted || isSelected;
+
+      if (slice.title === JANKY_LATENCY_NAME) {
+        if (hasFocus) {
+          slice.baseColor = DEEP_RED_COLOR;
+        } else {
+          slice.baseColor = RED_COLOR;
+        }
+      } else {
+        slice.baseColor = getColorForSlice(slice.title, hasFocus);
+      }
+    }
+    super.onUpdatedSlices(slices);
+  }
+
   // At the moment we will just display the slice details. However, on select,
   // this behavior should be customized to show jank-related data.
 }
@@ -133,7 +165,7 @@
       CASE
         WHEN id IN (
           SELECT id FROM chrome_janky_event_latencies_v3)
-        THEN 'Janky EventLatency'
+        THEN '${JANKY_LATENCY_NAME}'
         ELSE name
       END
       AS name,
diff --git a/ui/src/tracks/chrome_scroll_jank/jank_colors.ts b/ui/src/tracks/chrome_scroll_jank/jank_colors.ts
new file mode 100644
index 0000000..7df9ecf
--- /dev/null
+++ b/ui/src/tracks/chrome_scroll_jank/jank_colors.ts
@@ -0,0 +1,29 @@
+// 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 {Color} from '../../common/colorizer';
+
+export const RED_COLOR: Color = {
+  c: '#C41E3A',
+  h: 196,
+  s: 30,
+  l: 58,
+};
+
+export const DEEP_RED_COLOR: Color = {
+  c: '#880808',
+  h: 136,
+  s: 8,
+  l: 8,
+};
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
index 0c766b3..a065e29 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -14,11 +14,15 @@
 
 import {v4 as uuidv4} from 'uuid';
 
+import {
+  getColorForSlice,
+} from '../../common/colorizer';
 import {Engine} from '../../common/engine';
 import {
   PrimaryTrackSortKey,
   SCROLLING_TRACK_GROUP,
 } from '../../common/state';
+import {globals} from '../../frontend/globals';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {NewTrackArgs, Track} from '../../frontend/track';
 import {
@@ -27,14 +31,19 @@
   CustomSqlTableSliceTrack,
 } from '../custom_sql_table_slices';
 
+import {EventLatencyTrackTypes} from './event_latency_track';
 import {
   ScrollJankPluginState,
   ScrollJankTracks as DecideTracksResult,
 } from './index';
+import {DEEP_RED_COLOR, RED_COLOR} from './jank_colors';
 import {ScrollJankV3DetailsPanel} from './scroll_jank_v3_details_panel';
 
 export {Data} from '../chrome_slices';
 
+const UNKNOWN_SLICE_NAME = 'Unknown';
+const JANK_SLICE_NAME = ' Jank';
+
 export class ScrollJankV3Track extends
     CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
   static readonly kind = 'org.chromium.ScrollJank.scroll_jank_v3_track';
@@ -45,7 +54,6 @@
 
   constructor(args: NewTrackArgs) {
     super(args);
-
     ScrollJankPluginState.getInstance().registerTrack({
       kind: ScrollJankV3Track.kind,
       trackId: this.trackId,
@@ -61,7 +69,7 @@
           cause_of_jank IS NOT NULL,
           cause_of_jank || IIF(
             sub_cause_of_jank IS NOT NULL, "::" || sub_cause_of_jank, ""
-            ), "Unknown") || " Jank" AS name`,
+            ), "${UNKNOWN_SLICE_NAME}") || "${JANK_SLICE_NAME}" AS name`,
         'id',
         'ts',
         'dur',
@@ -84,6 +92,38 @@
     super.onDestroy();
     ScrollJankPluginState.getInstance().unregisterTrack(ScrollJankV3Track.kind);
   }
+
+  onUpdatedSlices(slices: EventLatencyTrackTypes['slice'][]) {
+    for (const slice of slices) {
+      const currentSelection = globals.state.currentSelection;
+      const isSelected = currentSelection &&
+          currentSelection.kind === 'GENERIC_SLICE' &&
+          currentSelection.id !== undefined && currentSelection.id === slice.id;
+
+      const highlighted = globals.state.highlightedSliceId === slice.id;
+      const hasFocus = highlighted || isSelected;
+
+      let stage =
+          slice.title.substring(0, slice.title.indexOf(JANK_SLICE_NAME));
+      // Stage may include substage, in which case we use the substage for
+      // color selection.
+      const separator = '::';
+      if (stage.indexOf(separator) != -1) {
+        stage = stage.substring(stage.indexOf(separator) + separator.length);
+      }
+
+      if (stage == UNKNOWN_SLICE_NAME) {
+        if (hasFocus) {
+          slice.baseColor = DEEP_RED_COLOR;
+        } else {
+          slice.baseColor = RED_COLOR;
+        }
+      } else {
+        slice.baseColor = getColorForSlice(stage, hasFocus);
+      }
+    }
+    super.onUpdatedSlices(slices);
+  }
 }
 
 export async function addScrollJankV3ScrollTrack(engine: Engine):
diff --git a/ui/src/tracks/chrome_slices/index.ts b/ui/src/tracks/chrome_slices/index.ts
index 9c26d59..232b917 100644
--- a/ui/src/tracks/chrome_slices/index.ts
+++ b/ui/src/tracks/chrome_slices/index.ts
@@ -15,7 +15,10 @@
 import {BigintMath as BIMath} from '../../base/bigint_math';
 import {Actions} from '../../common/actions';
 import {cropText, drawIncompleteSlice} from '../../common/canvas_utils';
-import {colorForThreadIdleSlice, hslForSlice} from '../../common/colorizer';
+import {
+  colorForThreadIdleSlice,
+  getColorForSlice,
+} from '../../common/colorizer';
 import {HighPrecisionTime} from '../../common/high_precision_time';
 import {LONG, LONG_NULL, NUM, STR} from '../../common/query_result';
 import {duration, Span, Time, time} from '../../common/time';
@@ -236,17 +239,15 @@
           currentSelection.kind === 'CHROME_SLICE' &&
           currentSelection.id !== undefined && currentSelection.id === sliceId;
 
-      const name = title.replace(/( )?\d+/g, '');
       const highlighted = titleId === this.hoveredTitleId ||
           globals.state.highlightedSliceId === sliceId;
 
       const hasFocus = highlighted || isSelected;
-
-      const [hue, saturation, lightness] = hslForSlice(name, hasFocus);
+      const colorObj = getColorForSlice(title, hasFocus);
 
       let color: string;
       if (colorOverride === undefined) {
-        color = cachedHsluvToHex(hue, saturation, lightness);
+        color = colorObj.c;
       } else {
         color = colorOverride;
       }
@@ -269,7 +270,7 @@
             ctx.save();
             ctx.translate(0, INNER_CHEVRON_OFFSET);
             ctx.scale(INNER_CHEVRON_SCALE, INNER_CHEVRON_SCALE);
-            ctx.fillStyle = cachedHsluvToHex(hue, 100, 10);
+            ctx.fillStyle = cachedHsluvToHex(colorObj.h, 100, 10);
 
             this.drawChevron(ctx);
             ctx.restore();
@@ -299,8 +300,8 @@
         const firstPartWidth = rect.width * cpuTimeRatio;
         const secondPartWidth = rect.width * (1 - cpuTimeRatio);
         ctx.fillRect(rect.left, rect.top, firstPartWidth, SLICE_HEIGHT);
-        ctx.fillStyle =
-            colorForThreadIdleSlice(hue, saturation, lightness, hasFocus);
+        ctx.fillStyle = colorForThreadIdleSlice(
+            colorObj.h, colorObj.s, colorObj.l, hasFocus);
         ctx.fillRect(
             rect.left + firstPartWidth,
             rect.top,
@@ -313,7 +314,7 @@
       // Selected case
       if (isSelected) {
         drawRectOnSelected = () => {
-          ctx.strokeStyle = cachedHsluvToHex(hue, 100, 10);
+          ctx.strokeStyle = cachedHsluvToHex(colorObj.h, 100, 10);
           ctx.beginPath();
           ctx.lineWidth = 3;
           ctx.strokeRect(
@@ -324,7 +325,7 @@
 
       // Don't render text when we have less than 5px to play with.
       if (rect.width >= 5) {
-        ctx.fillStyle = lightness > 65 ? '#404040' : 'white';
+        ctx.fillStyle = colorObj.l > 65 ? '#404040' : 'white';
         const displayText = cropText(title, charWidth, rect.width);
         const rectXCenter = rect.left + rect.width / 2;
         ctx.textBaseline = 'middle';