Merge "threading: add Future<T>/Stream<T> helpers for Channel<T> and ThreadPool"
diff --git a/CHANGELOG b/CHANGELOG
index 5c92fd9..8194fa8 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
 Unreleased:
   Tracing service and probes:
-    *
+    * --continuous-dump in tools/java_heap_dump now keeps recording until it
+      receives CTRL+C.
   Trace Processor:
     *
   UI:
diff --git a/src/base/threading/stream_unittest.cc b/src/base/threading/stream_unittest.cc
index 3036bd9..a9ac9aa 100644
--- a/src/base/threading/stream_unittest.cc
+++ b/src/base/threading/stream_unittest.cc
@@ -18,7 +18,9 @@
 
 #include <vector>
 
+#include "perfetto/base/platform_handle.h"
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/event_fd.h"
 #include "perfetto/ext/base/threading/future_combinators.h"
 #include "perfetto/ext/base/threading/poll.h"
 #include "test/gtest_and_gmock.h"
@@ -30,6 +32,7 @@
 using testing::_;
 using testing::ElementsAre;
 using testing::Return;
+using testing::UnorderedElementsAre;
 
 template <typename T>
 class MockPollable : public FuturePollable<T> {
@@ -258,10 +261,13 @@
 }
 
 TEST_F(StreamUnittest, FlattenStreams) {
+  EventFd event_fd1, event_fd2, event_fd3, event_fd4;
+  const PlatformHandle fd1 = event_fd1.fd(), fd2 = event_fd2.fd(),
+                       fd3 = event_fd3.fd(), fd4 = event_fd4.fd();
   std::unique_ptr<MockStreamPollable<int>> a(new MockStreamPollable<int>());
   EXPECT_CALL(*a, PollNext(_))
-      .WillOnce([](PollContext* ctx) {
-        ctx->RegisterInterested(1);
+      .WillOnce([fd1](PollContext* ctx) {
+        ctx->RegisterInterested(fd1);
         return PendingPollResult();
       })
       .WillOnce(Return(StreamPollResult<int>(1)))
@@ -269,12 +275,12 @@
 
   std::unique_ptr<MockStreamPollable<int>> b(new MockStreamPollable<int>());
   EXPECT_CALL(*b, PollNext(_))
-      .WillOnce([](PollContext* ctx) {
-        ctx->RegisterInterested(2);
+      .WillOnce([fd2](PollContext* ctx) {
+        ctx->RegisterInterested(fd2);
         return PendingPollResult();
       })
-      .WillOnce([](PollContext* ctx) {
-        ctx->RegisterInterested(2);
+      .WillOnce([fd2](PollContext* ctx) {
+        ctx->RegisterInterested(fd2);
         return PendingPollResult();
       })
       .WillOnce(Return(StreamPollResult<int>(2)))
@@ -283,9 +289,9 @@
   std::unique_ptr<MockStreamPollable<int>> c(new MockStreamPollable<int>());
   EXPECT_CALL(*c, PollNext(_))
       .WillOnce(Return(StreamPollResult<int>(3)))
-      .WillOnce([](PollContext* ctx) {
-        ctx->RegisterInterested(3);
-        ctx->RegisterInterested(4);
+      .WillOnce([fd3, fd4](PollContext* ctx) {
+        ctx->RegisterInterested(fd3);
+        ctx->RegisterInterested(fd4);
         return PendingPollResult();
       })
       .WillOnce(Return(DonePollResult()));
@@ -300,25 +306,25 @@
   ASSERT_THAT(interested_, ElementsAre());
 
   ASSERT_TRUE(stream.PollNext(&ctx_).IsPending());
-  ASSERT_THAT(interested_, ElementsAre(1, 2, 3, 4));
+  ASSERT_THAT(interested_, UnorderedElementsAre(fd1, fd2, fd3, fd4));
 
   interested_.clear();
   ASSERT_TRUE(stream.PollNext(&ctx_).IsPending());
-  ASSERT_THAT(interested_, ElementsAre(1, 2, 3, 4));
+  ASSERT_THAT(interested_, UnorderedElementsAre(fd1, fd2, fd3, fd4));
 
   interested_.clear();
-  ready_ = {1};
+  ready_ = {fd1};
   ASSERT_EQ(stream.PollNext(&ctx_).item(), 1);
   ASSERT_TRUE(stream.PollNext(&ctx_).IsPending());
-  ASSERT_THAT(interested_, ElementsAre(2, 3, 4));
+  ASSERT_THAT(interested_, UnorderedElementsAre(fd2, fd3, fd4));
 
   interested_.clear();
   ready_ = {};
   ASSERT_TRUE(stream.PollNext(&ctx_).IsPending());
-  ASSERT_THAT(interested_, ElementsAre(2, 3, 4));
+  ASSERT_THAT(interested_, ElementsAre(fd2, fd3, fd4));
 
   interested_.clear();
-  ready_ = {1, 2, 3};
+  ready_ = {fd1, fd2, fd3};
   ASSERT_TRUE(stream.PollNext(&ctx_).IsPending());
   ASSERT_EQ(stream.PollNext(&ctx_).item(), 2);
   ASSERT_TRUE(stream.PollNext(&ctx_).IsDone());
diff --git a/src/profiling/memory/unwinding.cc b/src/profiling/memory/unwinding.cc
index 4a7a5d3..d0c90be 100644
--- a/src/profiling/memory/unwinding.cc
+++ b/src/profiling/memory/unwinding.cc
@@ -204,6 +204,29 @@
   return true;
 }
 
+UnwindingWorker::~UnwindingWorker() {
+  if (thread_task_runner_.get() == nullptr) {
+    return;
+  }
+  std::mutex mutex;
+  std::condition_variable cv;
+
+  std::unique_lock<std::mutex> lock(mutex);
+  bool done = false;
+  thread_task_runner_.PostTask([&mutex, &cv, &done, this] {
+    for (auto& it : client_data_) {
+      auto& client_data = it.second;
+      client_data.sock->Shutdown(false);
+    }
+    client_data_.clear();
+
+    std::lock_guard<std::mutex> inner_lock(mutex);
+    done = true;
+    cv.notify_one();
+  });
+  cv.wait(lock, [&done] { return done; });
+}
+
 void UnwindingWorker::OnDisconnect(base::UnixSocket* self) {
   pid_t peer_pid = self->peer_pid_linux();
   auto it = client_data_.find(peer_pid);
diff --git a/src/profiling/memory/unwinding.h b/src/profiling/memory/unwinding.h
index fd2e9f9..b5e2e01 100644
--- a/src/profiling/memory/unwinding.h
+++ b/src/profiling/memory/unwinding.h
@@ -85,6 +85,9 @@
       : delegate_(delegate),
         thread_task_runner_(std::move(thread_task_runner)) {}
 
+  ~UnwindingWorker() override;
+  UnwindingWorker(UnwindingWorker&&) = default;
+
   // Public API safe to call from other threads.
   void PostDisconnectSocket(pid_t pid);
   void PostPurgeProcess(pid_t pid);
@@ -149,19 +152,11 @@
   std::map<pid_t, ClientData> client_data_;
   Delegate* delegate_;
 
-  // Task runner with a dedicated thread. Keep last as instances this class are
-  // currently (incorrectly) being destroyed on the main thread, instead of the
-  // task thread. By destroying this task runner first, we ensure that the
-  // UnwindingWorker is not active while the rest of its state is being
-  // destroyed. Additionally this ensures that the destructing thread sees a
-  // consistent view of the memory due to the ThreadTaskRunner's destructor
-  // joining a thread.
-  //
-  // Additionally, keep the destructor defaulted, as its body would still race
-  // against an active task thread.
-  //
-  // TODO(rsavitski): make the task thread own the object's lifetime (likely by
-  // refactoring base::ThreadTaskRunner).
+  // Task runner with a dedicated thread. Keep last. By destroying this task
+  // runner first, we ensure that the UnwindingWorker is not active while the
+  // rest of its state is being destroyed. Additionally this ensures that the
+  // destructing thread sees a consistent view of the memory due to the
+  // ThreadTaskRunner's destructor joining a thread.
   base::ThreadTaskRunner thread_task_runner_;
 };
 
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc b/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
index 9fa99a5..92ccb20 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
@@ -421,6 +421,132 @@
   context_.sorter->ExtractEventsForced();
 }
 
+TEST_F(FuchsiaTraceParserTest, SchedulerEvents) {
+  uint64_t thread1_tid = 0x1AAA'AAAA'AAAA'AAAA;
+  uint64_t thread2_tid = 0x2CCC'CCCC'CCCC'CCCC;
+
+  // We'll emit a wake up for thread 1, a switch to thread 2, and a switch back
+  // to thread 1 and expect to see that the process tracker was properly updated
+
+  uint64_t wakeup_record_type = uint64_t{2} << 60;
+  uint64_t context_switch_record_type = uint64_t{1} << 60;
+  uint64_t cpu = 1 << 20;
+  uint64_t record_type = 8;
+
+  uint64_t wakeup_size = uint64_t{3} << 4;
+  uint64_t context_switch_size = uint64_t{4} << 4;
+
+  uint64_t wakeup_header = wakeup_record_type | cpu | record_type | wakeup_size;
+  push_word(wakeup_header);
+  // Timestamp
+  push_word(0x1);
+  // wakeup tid
+  push_word(thread1_tid);
+
+  uint64_t context_switch_header =
+      context_switch_record_type | cpu | record_type | context_switch_size;
+  push_word(context_switch_header);
+  // Timestamp
+  push_word(0x2);
+  // outgoing tid
+  push_word(thread1_tid);
+  // incoming tid
+  push_word(thread2_tid);
+
+  push_word(context_switch_header);
+  // Timestamp
+  push_word(0x3);
+  // outgoing tid
+  push_word(thread2_tid);
+  // incoming tid
+  push_word(thread1_tid);
+
+  // We should get:
+  // - A thread1 update call on wake up
+  // - thread1 & thread2 update calls on the first context switch
+  // - thread2 & thread1 update cals on the second context switch
+  EXPECT_CALL(*process_, UpdateThread(static_cast<uint32_t>(thread1_tid), _))
+      .Times(3);
+  EXPECT_CALL(*process_, UpdateThread(static_cast<uint32_t>(thread2_tid), _))
+      .Times(2);
+
+  EXPECT_TRUE(Tokenize().ok());
+  EXPECT_EQ(context_.storage->stats()[stats::fuchsia_invalid_event].value, 0);
+
+  context_.sorter->ExtractEventsForced();
+}
+
+TEST_F(FuchsiaTraceParserTest, LegacySchedulerEvents) {
+  uint64_t thread1_pid = 0x1AAA'AAAA'AAAA'AAAA;
+  uint64_t thread1_tid = 0x1BBB'BBBB'BBBB'BBBB;
+  uint64_t thread2_pid = 0x2CCC'CCCC'CCCC'CCCC;
+  uint64_t thread2_tid = 0x2DDD'DDDD'DDDD'DDDD;
+
+  // We'll emit a wake up for thread 1, a switch to thread 2, and a switch back
+  // to thread 1 and expect to see that the process tracker was properly updated
+
+  uint64_t context_switch_size = uint64_t{6} << 4;
+  uint64_t cpu = 1 << 16;
+  uint64_t record_type = 8;
+  uint64_t outoing_state = 2 << 24;
+  uint64_t outoing_thread = 0;   // Inline thread-ref
+  uint64_t incoming_thread = 0;  // Inline thread-ref
+  uint64_t outgoing_prio = uint64_t{1} << 44;
+  uint64_t incoming_prio = uint64_t{1} << 52;
+  uint64_t outgoing_idle_prio = uint64_t{0} << 44;
+
+  uint64_t context_switch_header =
+      record_type | context_switch_size | cpu | outoing_state | outoing_thread |
+      incoming_thread | outgoing_prio | incoming_prio;
+  uint64_t wakeup_header = record_type | context_switch_size | cpu |
+                           outoing_state | outoing_thread | incoming_thread |
+                           outgoing_idle_prio | incoming_prio;
+
+  push_word(wakeup_header);
+  // Timestamp
+  push_word(0x1);
+  // outgoing pid+tid
+  push_word(0);  // Idle thread
+  push_word(0);  // Idle thread
+  // incoming pid+tid
+  push_word(thread1_pid);
+  push_word(thread1_tid);
+
+  push_word(context_switch_header);
+  // Timestamp
+  push_word(0x2);
+  // outgoing pid+tid
+  push_word(thread1_pid);
+  push_word(thread1_tid);
+  // incoming pid+tid
+  push_word(thread2_pid);
+  push_word(thread2_tid);
+
+  push_word(context_switch_header);
+  // Timestamp
+  push_word(0x3);
+  // outgoing pid+tid
+  push_word(thread2_pid);
+  push_word(thread2_tid);
+  // incoming pid+tid
+  push_word(thread1_pid);
+  push_word(thread1_tid);
+
+  // We should get:
+  // - A thread1 update call on wake up
+  // - thread1 & thread2 update calls on the first context switch
+  // - thread2 & thread1 update cals on the second context switch
+  EXPECT_CALL(*process_, UpdateThread(static_cast<uint32_t>(thread1_tid), _))
+      .Times(3);
+  EXPECT_CALL(*process_, UpdateThread(static_cast<uint32_t>(thread2_tid), _))
+      .Times(2);
+
+  EXPECT_TRUE(Tokenize().ok());
+  EXPECT_EQ(context_.storage->stats()[stats::fuchsia_invalid_event].value, 0);
+
+  context_.sorter->ExtractEventsForced();
+}
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.cc b/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.cc
index a4dc251..0143060 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.cc
@@ -56,10 +56,6 @@
 constexpr uint32_t kPointer = 7;
 constexpr uint32_t kKoid = 8;
 
-struct Arg {
-  StringId name;
-  fuchsia_trace_utils::ArgValue value;
-};
 }  // namespace
 
 FuchsiaTraceParser::FuchsiaTraceParser(TraceProcessorContext* context)
@@ -75,9 +71,119 @@
   proto_parser_->ParseTracePacket(ts, std::move(data));
 }
 
+std::optional<std::vector<FuchsiaTraceParser::Arg>>
+FuchsiaTraceParser::ParseArgs(
+    fuchsia_trace_utils::RecordCursor& cursor,
+    uint32_t n_args,
+    std::function<StringId(base::StringView string)> intern_string,
+    std::function<StringId(uint32_t index)> get_string) {
+  std::vector<Arg> args;
+  for (uint32_t i = 0; i < n_args; i++) {
+    size_t arg_base = cursor.WordIndex();
+    uint64_t arg_header;
+    if (!cursor.ReadUint64(&arg_header)) {
+      return std::nullopt;
+    }
+    uint32_t arg_type =
+        fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 0, 3);
+    uint32_t arg_size_words =
+        fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 4, 15);
+    uint32_t arg_name_ref =
+        fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 16, 31);
+    Arg arg;
+    if (fuchsia_trace_utils::IsInlineString(arg_name_ref)) {
+      base::StringView arg_name_view;
+      if (!cursor.ReadInlineString(arg_name_ref, &arg_name_view)) {
+        return std::nullopt;
+      }
+      arg.name = intern_string(arg_name_view);
+    } else {
+      arg.name = get_string(arg_name_ref);
+    }
+
+    switch (arg_type) {
+      case kNull:
+        arg.value = fuchsia_trace_utils::ArgValue::Null();
+        break;
+      case kInt32:
+        arg.value = fuchsia_trace_utils::ArgValue::Int32(
+            fuchsia_trace_utils::ReadField<int32_t>(arg_header, 32, 63));
+        break;
+      case kUint32:
+        arg.value = fuchsia_trace_utils::ArgValue::Uint32(
+            fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 32, 63));
+        break;
+      case kInt64: {
+        int64_t value;
+        if (!cursor.ReadInt64(&value)) {
+          return std::nullopt;
+        }
+        arg.value = fuchsia_trace_utils::ArgValue::Int64(value);
+        break;
+      }
+      case kUint64: {
+        uint64_t value;
+        if (!cursor.ReadUint64(&value)) {
+          return std::nullopt;
+        }
+        arg.value = fuchsia_trace_utils::ArgValue::Uint64(value);
+        break;
+      }
+      case kDouble: {
+        double value;
+        if (!cursor.ReadDouble(&value)) {
+          return std::nullopt;
+        }
+        arg.value = fuchsia_trace_utils::ArgValue::Double(value);
+        break;
+      }
+      case kString: {
+        uint32_t arg_value_ref =
+            fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 32, 47);
+        StringId value;
+        if (fuchsia_trace_utils::IsInlineString(arg_value_ref)) {
+          base::StringView arg_value_view;
+          if (!cursor.ReadInlineString(arg_value_ref, &arg_value_view)) {
+            return std::nullopt;
+          }
+          value = intern_string(arg_value_view);
+        } else {
+          value = get_string(arg_value_ref);
+        }
+        arg.value = fuchsia_trace_utils::ArgValue::String(value);
+        break;
+      }
+      case kPointer: {
+        uint64_t value;
+        if (!cursor.ReadUint64(&value)) {
+          return std::nullopt;
+        }
+        arg.value = fuchsia_trace_utils::ArgValue::Pointer(value);
+        break;
+      }
+      case kKoid: {
+        uint64_t value;
+        if (!cursor.ReadUint64(&value)) {
+          return std::nullopt;
+        }
+        arg.value = fuchsia_trace_utils::ArgValue::Koid(value);
+        break;
+      }
+      default:
+        arg.value = fuchsia_trace_utils::ArgValue::Unknown();
+        break;
+    }
+
+    args.push_back(arg);
+    cursor.SetWordIndex(arg_base + arg_size_words);
+  }
+
+  return {std::move(args)};
+}
+
 void FuchsiaTraceParser::ParseFuchsiaRecord(int64_t, FuchsiaRecord fr) {
-  // The timestamp is also present in the record, so we'll ignore the one passed
-  // as an argument.
+  // The timestamp is also present in the record, so we'll ignore the one
+  // passed as an argument.
   fuchsia_trace_utils::RecordCursor cursor(fr.record_view()->data(),
                                            fr.record_view()->length());
   ProcessTracker* procs = context_->process_tracker.get();
@@ -140,121 +246,28 @@
       }
 
       // Read arguments
-      std::vector<Arg> args;
-      for (uint32_t i = 0; i < n_args; i++) {
-        size_t arg_base = cursor.WordIndex();
-        uint64_t arg_header;
-        if (!cursor.ReadUint64(&arg_header)) {
-          context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-          return;
-        }
-        uint32_t arg_type =
-            fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 0, 3);
-        uint32_t arg_size_words =
-            fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 4, 15);
-        uint32_t arg_name_ref =
-            fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 16, 31);
-        Arg arg;
-        if (fuchsia_trace_utils::IsInlineString(arg_name_ref)) {
-          base::StringView arg_name_view;
-          if (!cursor.ReadInlineString(arg_name_ref, &arg_name_view)) {
-            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-            return;
-          }
-          arg.name = context_->storage->InternString(arg_name_view);
-        } else {
-          arg.name = fr.GetString(arg_name_ref);
-        }
-
-        switch (arg_type) {
-          case kNull:
-            arg.value = fuchsia_trace_utils::ArgValue::Null();
-            break;
-          case kInt32:
-            arg.value = fuchsia_trace_utils::ArgValue::Int32(
-                fuchsia_trace_utils::ReadField<int32_t>(arg_header, 32, 63));
-            break;
-          case kUint32:
-            arg.value = fuchsia_trace_utils::ArgValue::Uint32(
-                fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 32, 63));
-            break;
-          case kInt64: {
-            int64_t value;
-            if (!cursor.ReadInt64(&value)) {
-              context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-              return;
-            }
-            arg.value = fuchsia_trace_utils::ArgValue::Int64(value);
-            break;
-          }
-          case kUint64: {
-            uint64_t value;
-            if (!cursor.ReadUint64(&value)) {
-              context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-              return;
-            }
-            arg.value = fuchsia_trace_utils::ArgValue::Uint64(value);
-            break;
-          }
-          case kDouble: {
-            double value;
-            if (!cursor.ReadDouble(&value)) {
-              context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-              return;
-            }
-            arg.value = fuchsia_trace_utils::ArgValue::Double(value);
-            break;
-          }
-          case kString: {
-            uint32_t arg_value_ref =
-                fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 32, 47);
-            StringId value;
-            if (fuchsia_trace_utils::IsInlineString(arg_value_ref)) {
-              base::StringView arg_value_view;
-              if (!cursor.ReadInlineString(arg_value_ref, &arg_value_view)) {
-                context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-                return;
-              }
-              value = context_->storage->InternString(arg_value_view);
-            } else {
-              value = fr.GetString(arg_value_ref);
-            }
-            arg.value = fuchsia_trace_utils::ArgValue::String(value);
-            break;
-          }
-          case kPointer: {
-            uint64_t value;
-            if (!cursor.ReadUint64(&value)) {
-              context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-              return;
-            }
-            arg.value = fuchsia_trace_utils::ArgValue::Pointer(value);
-            break;
-          }
-          case kKoid: {
-            uint64_t value;
-            if (!cursor.ReadUint64(&value)) {
-              context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-              return;
-            }
-            arg.value = fuchsia_trace_utils::ArgValue::Koid(value);
-            break;
-          }
-          default:
-            arg.value = fuchsia_trace_utils::ArgValue::Unknown();
-            break;
-        }
-
-        args.push_back(arg);
-        cursor.SetWordIndex(arg_base + arg_size_words);
-      }
-      auto insert_args = [this, args](ArgsTracker::BoundInserter* inserter) {
-        for (const Arg& arg : args) {
-          inserter->AddArg(
-              arg.name, arg.name,
-              arg.value.ToStorageVariadic(context_->storage.get()));
-        }
+      const auto intern_string = [this](base::StringView string) {
+        return context_->storage->InternString(string);
       };
+      const auto get_string = [&fr](uint32_t index) {
+        return fr.GetString(index);
+      };
+
+      auto maybe_args = FuchsiaTraceParser::ParseArgs(
+          cursor, n_args, intern_string, get_string);
+      if (!maybe_args.has_value()) {
+        context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+        return;
+      }
+
+      auto insert_args =
+          [this, args = *maybe_args](ArgsTracker::BoundInserter* inserter) {
+            for (const Arg& arg : args) {
+              inserter->AddArg(
+                  arg.name, arg.name,
+                  arg.value.ToStorageVariadic(context_->storage.get()));
+            }
+          };
 
       switch (event_type) {
         case kInstant: {
@@ -262,7 +275,7 @@
               procs->UpdateThread(static_cast<uint32_t>(tinfo.tid),
                                   static_cast<uint32_t>(tinfo.pid));
           TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
-          slices->Scoped(ts, track_id, cat, name, 0, insert_args);
+          slices->Scoped(ts, track_id, cat, name, 0, std::move(insert_args));
           break;
         }
         case kCounter: {
@@ -275,11 +288,12 @@
             context_->storage->IncrementStats(stats::fuchsia_invalid_event);
             return;
           }
-          // Note: In the Fuchsia trace format, counter values are stored in the
-          // arguments for the record, with the data series defined by both the
-          // record name and the argument name. In Perfetto, counters only have
-          // one name, so we combine both names into one here.
-          for (const Arg& arg : args) {
+          // Note: In the Fuchsia trace format, counter values are stored
+          // in the arguments for the record, with the data series defined
+          // by both the record name and the argument name. In Perfetto,
+          // counters only have one name, so we combine both names into
+          // one here.
+          for (const Arg& arg : *maybe_args) {
             std::string counter_name_str = name_str + ":";
             counter_name_str +=
                 context_->storage->GetString(arg.name).ToStdString();
@@ -332,7 +346,7 @@
               procs->UpdateThread(static_cast<uint32_t>(tinfo.tid),
                                   static_cast<uint32_t>(tinfo.pid));
           TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
-          slices->Begin(ts, track_id, cat, name, insert_args);
+          slices->Begin(ts, track_id, cat, name, std::move(insert_args));
           break;
         }
         case kDurationEnd: {
@@ -340,10 +354,11 @@
               procs->UpdateThread(static_cast<uint32_t>(tinfo.tid),
                                   static_cast<uint32_t>(tinfo.pid));
           TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
-          // TODO(b/131181693): |cat| and |name| are not passed here so that
-          // if two slices end at the same timestep, the slices get closed in
-          // the correct order regardless of which end event is processed first.
-          slices->End(ts, track_id, {}, {}, insert_args);
+          // TODO(b/131181693): |cat| and |name| are not passed here so
+          // that if two slices end at the same timestep, the slices get
+          // closed in the correct order regardless of which end event is
+          // processed first.
+          slices->End(ts, track_id, {}, {}, std::move(insert_args));
           break;
         }
         case kDurationComplete: {
@@ -361,7 +376,8 @@
               procs->UpdateThread(static_cast<uint32_t>(tinfo.tid),
                                   static_cast<uint32_t>(tinfo.pid));
           TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
-          slices->Scoped(ts, track_id, cat, name, duration, insert_args);
+          slices->Scoped(ts, track_id, cat, name, duration,
+                         std::move(insert_args));
           break;
         }
         case kAsyncBegin: {
@@ -374,7 +390,7 @@
               procs->GetOrCreateProcess(static_cast<uint32_t>(tinfo.pid));
           TrackId track_id = context_->track_tracker->InternFuchsiaAsyncTrack(
               name, upid, correlation_id);
-          slices->Begin(ts, track_id, cat, name, insert_args);
+          slices->Begin(ts, track_id, cat, name, std::move(insert_args));
           break;
         }
         case kAsyncInstant: {
@@ -387,7 +403,7 @@
               procs->GetOrCreateProcess(static_cast<uint32_t>(tinfo.pid));
           TrackId track_id = context_->track_tracker->InternFuchsiaAsyncTrack(
               name, upid, correlation_id);
-          slices->Scoped(ts, track_id, cat, name, 0, insert_args);
+          slices->Scoped(ts, track_id, cat, name, 0, std::move(insert_args));
           break;
         }
         case kAsyncEnd: {
@@ -400,7 +416,7 @@
               procs->GetOrCreateProcess(static_cast<uint32_t>(tinfo.pid));
           TrackId track_id = context_->track_tracker->InternFuchsiaAsyncTrack(
               name, upid, correlation_id);
-          slices->End(ts, track_id, cat, name, insert_args);
+          slices->End(ts, track_id, cat, name, std::move(insert_args));
           break;
         }
         case kFlowBegin: {
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h b/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h
index 01cf3e3..d36a9d7 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h
@@ -17,8 +17,13 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_PARSER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_PARSER_H_
 
+#include <functional>
+#include <optional>
+#include <vector>
+
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
+#include "src/trace_processor/importers/fuchsia/fuchsia_trace_utils.h"
 #include "src/trace_processor/importers/proto/proto_trace_parser.h"
 
 namespace perfetto {
@@ -36,6 +41,22 @@
   void ParseTrackEvent(int64_t, TrackEventData) override;
   void ParseTracePacket(int64_t ts, TracePacketData data) override;
 
+  struct Arg {
+    StringId name;
+    fuchsia_trace_utils::ArgValue value;
+  };
+
+  // Utility to parse record arguments. Exposed here to provide consistent
+  // parsing between trace parsing and tokenization.
+  //
+  // Returns an empty optional on error, otherwise a vector containing zero or
+  // more arguments.
+  static std::optional<std::vector<Arg>> ParseArgs(
+      fuchsia_trace_utils::RecordCursor& cursor,
+      uint32_t n_args,
+      std::function<StringId(base::StringView string)> intern_string,
+      std::function<StringId(uint32_t index)> get_string);
+
  private:
   TraceProcessorContext* const context_;
   std::unique_ptr<ProtoTraceParser> proto_parser_;
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
index 8b7f14f..514bfcb 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
@@ -17,6 +17,7 @@
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h"
 
 #include <cinttypes>
+#include <limits>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/string_view.h"
@@ -24,6 +25,7 @@
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
+#include "src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h"
 #include "src/trace_processor/importers/proto/proto_trace_parser.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
@@ -34,6 +36,9 @@
 namespace trace_processor {
 
 namespace {
+
+using fuchsia_trace_utils::ArgValue;
+
 // Record types
 constexpr uint32_t kMetadata = 0;
 constexpr uint32_t kInitialization = 1;
@@ -42,7 +47,11 @@
 constexpr uint32_t kEvent = 4;
 constexpr uint32_t kBlob = 5;
 constexpr uint32_t kKernelObject = 7;
-constexpr uint32_t kContextSwitch = 8;
+constexpr uint32_t kSchedulerEvent = 8;
+
+constexpr uint32_t kSchedulerEventLegacyContextSwitch = 0;
+constexpr uint32_t kSchedulerEventContextSwitch = 1;
+constexpr uint32_t kSchedulerEventThreadWakeup = 2;
 
 // Metadata types
 constexpr uint32_t kProviderInfo = 1;
@@ -61,9 +70,8 @@
 constexpr uint32_t kZxObjTypeProcess = 1;
 constexpr uint32_t kZxObjTypeThread = 2;
 
-// Argument types
-constexpr uint32_t kArgString = 6;
-constexpr uint32_t kArgKernelObject = 8;
+constexpr int32_t kIdleWeight = std::numeric_limits<int32_t>::min();
+
 }  // namespace
 
 FuchsiaTraceTokenizer::FuchsiaTraceTokenizer(TraceProcessorContext* context)
@@ -72,10 +80,15 @@
       running_string_id_(context->storage->InternString("Running")),
       runnable_string_id_(context->storage->InternString("R")),
       preempted_string_id_(context->storage->InternString("R+")),
+      waking_string_id_(context->storage->InternString("W")),
       blocked_string_id_(context->storage->InternString("S")),
       suspended_string_id_(context->storage->InternString("T")),
       exit_dying_string_id_(context->storage->InternString("Z")),
-      exit_dead_string_id_(context->storage->InternString("X")) {
+      exit_dead_string_id_(context->storage->InternString("X")),
+      incoming_weight_id_(context->storage->InternString("incoming_weight")),
+      outgoing_weight_id_(context->storage->InternString("outgoing_weight")),
+      weight_id_(context->storage->InternString("weight")),
+      process_id_(context->storage->InternString("process")) {
   RegisterProvider(0, "");
 }
 
@@ -210,6 +223,124 @@
   }
 }
 
+void FuchsiaTraceTokenizer::SwitchFrom(Thread* thread,
+                                       int64_t ts,
+                                       uint32_t cpu,
+                                       uint32_t thread_state) {
+  TraceStorage* storage = context_->storage.get();
+  ProcessTracker* procs = context_->process_tracker.get();
+
+  StringId state = IdForOutgoingThreadState(thread_state);
+  UniqueTid utid = procs->UpdateThread(static_cast<uint32_t>(thread->info.tid),
+                                       static_cast<uint32_t>(thread->info.pid));
+
+  const auto duration = ts - thread->last_ts;
+  thread->last_ts = ts;
+
+  // Close the slice record if one is open for this thread.
+  if (thread->last_slice_row.has_value()) {
+    auto row_ref = thread->last_slice_row->ToRowReference(
+        storage->mutable_sched_slice_table());
+    row_ref.set_dur(duration);
+    row_ref.set_end_state(state);
+    thread->last_slice_row.reset();
+  }
+
+  // Close the state record if one is open for this thread.
+  if (thread->last_state_row.has_value()) {
+    auto row_ref = thread->last_state_row->ToRowReference(
+        storage->mutable_thread_state_table());
+    row_ref.set_dur(duration);
+    thread->last_state_row.reset();
+  }
+
+  // Open a new state record to track the duration of the outgoing
+  // state.
+  tables::ThreadStateTable::Row state_row;
+  state_row.ts = ts;
+  state_row.cpu = cpu;
+  state_row.dur = -1;
+  state_row.state = state;
+  state_row.utid = utid;
+  auto state_row_number =
+      storage->mutable_thread_state_table()->Insert(state_row).row_number;
+  thread->last_state_row = state_row_number;
+}
+
+void FuchsiaTraceTokenizer::SwitchTo(Thread* thread,
+                                     int64_t ts,
+                                     uint32_t cpu,
+                                     int32_t weight) {
+  TraceStorage* storage = context_->storage.get();
+  ProcessTracker* procs = context_->process_tracker.get();
+
+  UniqueTid utid = procs->UpdateThread(static_cast<uint32_t>(thread->info.tid),
+                                       static_cast<uint32_t>(thread->info.pid));
+
+  const auto duration = ts - thread->last_ts;
+  thread->last_ts = ts;
+
+  // Close the state record if one is open for this thread.
+  if (thread->last_state_row.has_value()) {
+    auto row_ref = thread->last_state_row->ToRowReference(
+        storage->mutable_thread_state_table());
+    row_ref.set_dur(duration);
+    thread->last_state_row.reset();
+  }
+
+  // Open a new slice record for this thread.
+  tables::SchedSliceTable::Row slice_row;
+  slice_row.ts = ts;
+  slice_row.cpu = cpu;
+  slice_row.dur = -1;
+  slice_row.utid = utid;
+  slice_row.priority = weight;
+  auto slice_row_number =
+      storage->mutable_sched_slice_table()->Insert(slice_row).row_number;
+  thread->last_slice_row = slice_row_number;
+
+  // Open a new state record for this thread.
+  tables::ThreadStateTable::Row state_row;
+  state_row.ts = ts;
+  state_row.cpu = cpu;
+  state_row.dur = -1;
+  state_row.state = running_string_id_;
+  state_row.utid = utid;
+  auto state_row_number =
+      storage->mutable_thread_state_table()->Insert(state_row).row_number;
+  thread->last_state_row = state_row_number;
+}
+
+void FuchsiaTraceTokenizer::Wake(Thread* thread, int64_t ts, uint32_t cpu) {
+  TraceStorage* storage = context_->storage.get();
+  ProcessTracker* procs = context_->process_tracker.get();
+
+  UniqueTid utid = procs->UpdateThread(static_cast<uint32_t>(thread->info.tid),
+                                       static_cast<uint32_t>(thread->info.pid));
+
+  const auto duration = ts - thread->last_ts;
+  thread->last_ts = ts;
+
+  // Close the state record if one is open for this thread.
+  if (thread->last_state_row.has_value()) {
+    auto row_ref = thread->last_state_row->ToRowReference(
+        storage->mutable_thread_state_table());
+    row_ref.set_dur(duration);
+    thread->last_state_row.reset();
+  }
+
+  // Open a new state record for this thread.
+  tables::ThreadStateTable::Row state_row;
+  state_row.ts = ts;
+  state_row.cpu = cpu;
+  state_row.dur = -1;
+  state_row.state = waking_string_id_;
+  state_row.utid = utid;
+  auto state_row_number =
+      storage->mutable_thread_state_table()->Insert(state_row).row_number;
+  thread->last_state_row = state_row_number;
+}
+
 // Most record types are read and recorded in |TraceStorage| here directly.
 // Event records are sorted by timestamp before processing, so instead of
 // recording them in |TraceStorage| they are given to |TraceSorter|. In order to
@@ -235,6 +366,14 @@
     return;
   }
 
+  // Adapters for FuchsiaTraceParser::ParseArgs.
+  const auto intern_string = [this](base::StringView string) {
+    return context_->storage->InternString(string);
+  };
+  const auto get_string = [this](uint16_t index) {
+    return current_provider_->GetString(index);
+  };
+
   switch (record_type) {
     case kMetadata: {
       uint32_t metadata_type =
@@ -334,22 +473,21 @@
         cursor.ReadInlineThread(nullptr);
       } else {
         record.InsertThread(thread_ref,
-                            current_provider_->thread_table[thread_ref]);
+                            current_provider_->GetThread(thread_ref));
       }
 
       if (fuchsia_trace_utils::IsInlineString(cat_ref)) {
         // Skip over inline string
         cursor.ReadInlineString(cat_ref, nullptr);
       } else {
-        record.InsertString(cat_ref, current_provider_->string_table[cat_ref]);
+        record.InsertString(cat_ref, current_provider_->GetString(cat_ref));
       }
 
       if (fuchsia_trace_utils::IsInlineString(name_ref)) {
         // Skip over inline string
         cursor.ReadInlineString(name_ref, nullptr);
       } else {
-        record.InsertString(name_ref,
-                            current_provider_->string_table[name_ref]);
+        record.InsertString(name_ref, current_provider_->GetString(name_ref));
       }
 
       uint32_t n_args =
@@ -373,10 +511,10 @@
           cursor.ReadInlineString(arg_name_ref, nullptr);
         } else {
           record.InsertString(arg_name_ref,
-                              current_provider_->string_table[arg_name_ref]);
+                              current_provider_->GetString(arg_name_ref));
         }
 
-        if (arg_type == kArgString) {
+        if (arg_type == ArgValue::ArgType::kString) {
           uint32_t arg_value_ref =
               fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 32, 47);
           if (fuchsia_trace_utils::IsInlineString(arg_value_ref)) {
@@ -384,7 +522,7 @@
             cursor.ReadInlineString(arg_value_ref, nullptr);
           } else {
             record.InsertString(arg_value_ref,
-                                current_provider_->string_table[arg_value_ref]);
+                                current_provider_->GetString(arg_value_ref));
           }
         }
 
@@ -445,7 +583,7 @@
         }
         name = storage->InternString(name_view);
       } else {
-        name = current_provider_->string_table[name_ref];
+        name = current_provider_->GetString(name_ref);
       }
 
       switch (obj_type) {
@@ -462,43 +600,23 @@
         case kZxObjTypeThread: {
           uint32_t n_args =
               fuchsia_trace_utils::ReadField<uint32_t>(header, 40, 43);
+
+          auto maybe_args = FuchsiaTraceParser::ParseArgs(
+              cursor, n_args, intern_string, get_string);
+          if (!maybe_args.has_value()) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
+
           uint64_t pid = 0;
-
-          // Scan for a Kernel Object argument named "process"
-          for (uint32_t i = 0; i < n_args; i++) {
-            const size_t arg_base = cursor.WordIndex();
-            uint64_t arg_header;
-            if (!cursor.ReadUint64(&arg_header)) {
-              storage->IncrementStats(stats::fuchsia_invalid_event);
-              return;
-            }
-            uint32_t arg_type =
-                fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 0, 3);
-            uint32_t arg_size =
-                fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 4, 15);
-            if (arg_type == kArgKernelObject) {
-              uint32_t arg_name_ref =
-                  fuchsia_trace_utils::ReadField<uint32_t>(arg_header, 16, 31);
-              base::StringView arg_name;
-              if (fuchsia_trace_utils::IsInlineString(arg_name_ref)) {
-                if (!cursor.ReadInlineString(arg_name_ref, &arg_name)) {
-                  storage->IncrementStats(stats::fuchsia_invalid_event);
-                  return;
-                }
-              } else {
-                arg_name = storage->GetString(
-                    current_provider_->string_table[arg_name_ref]);
+          for (const auto arg : *maybe_args) {
+            if (arg.name == process_id_) {
+              if (arg.value.Type() != ArgValue::ArgType::kKoid) {
+                storage->IncrementStats(stats::fuchsia_invalid_event);
+                return;
               }
-
-              if (arg_name == "process") {
-                if (!cursor.ReadUint64(&pid)) {
-                  storage->IncrementStats(stats::fuchsia_invalid_event);
-                  return;
-                }
-              }
+              pid = arg.value.Koid();
             }
-
-            cursor.SetWordIndex(arg_base + arg_size);
           }
 
           Thread& thread = GetThread(obj_id);
@@ -516,140 +634,200 @@
       }
       break;
     }
-    case kContextSwitch: {
+    case kSchedulerEvent: {
       // Context switch records come in order, so they do not need to go through
       // TraceSorter.
-      uint32_t cpu = fuchsia_trace_utils::ReadField<uint32_t>(header, 16, 23);
-      uint32_t outgoing_state =
-          fuchsia_trace_utils::ReadField<uint32_t>(header, 24, 27);
-      uint32_t outgoing_thread_ref =
-          fuchsia_trace_utils::ReadField<uint32_t>(header, 28, 35);
-      int32_t outgoing_priority =
-          fuchsia_trace_utils::ReadField<int32_t>(header, 44, 51);
-      uint32_t incoming_thread_ref =
-          fuchsia_trace_utils::ReadField<uint32_t>(header, 36, 43);
-      int32_t incoming_priority =
-          fuchsia_trace_utils::ReadField<int32_t>(header, 52, 59);
+      uint32_t event_type =
+          fuchsia_trace_utils::ReadField<uint32_t>(header, 60, 63);
+      switch (event_type) {
+        case kSchedulerEventLegacyContextSwitch: {
+          uint32_t cpu =
+              fuchsia_trace_utils::ReadField<uint32_t>(header, 16, 23);
+          uint32_t outgoing_state =
+              fuchsia_trace_utils::ReadField<uint32_t>(header, 24, 27);
+          uint32_t outgoing_thread_ref =
+              fuchsia_trace_utils::ReadField<uint32_t>(header, 28, 35);
+          int32_t outgoing_priority =
+              fuchsia_trace_utils::ReadField<int32_t>(header, 44, 51);
+          uint32_t incoming_thread_ref =
+              fuchsia_trace_utils::ReadField<uint32_t>(header, 36, 43);
+          int32_t incoming_priority =
+              fuchsia_trace_utils::ReadField<int32_t>(header, 52, 59);
 
-      int64_t ts;
-      if (!cursor.ReadTimestamp(current_provider_->ticks_per_second, &ts)) {
-        context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-        return;
-      }
-      if (ts == -1) {
-        context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-        return;
-      }
+          int64_t ts;
+          if (!cursor.ReadTimestamp(current_provider_->ticks_per_second, &ts)) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
+          if (ts == -1) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
 
-      FuchsiaThreadInfo outgoing_thread_info;
-      if (fuchsia_trace_utils::IsInlineThread(outgoing_thread_ref)) {
-        if (!cursor.ReadInlineThread(&outgoing_thread_info)) {
-          context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-          return;
+          FuchsiaThreadInfo outgoing_thread_info;
+          if (fuchsia_trace_utils::IsInlineThread(outgoing_thread_ref)) {
+            if (!cursor.ReadInlineThread(&outgoing_thread_info)) {
+              context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+              return;
+            }
+          } else {
+            outgoing_thread_info =
+                current_provider_->GetThread(outgoing_thread_ref);
+          }
+          Thread& outgoing_thread = GetThread(outgoing_thread_info.tid);
+
+          FuchsiaThreadInfo incoming_thread_info;
+          if (fuchsia_trace_utils::IsInlineThread(incoming_thread_ref)) {
+            if (!cursor.ReadInlineThread(&incoming_thread_info)) {
+              context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+              return;
+            }
+          } else {
+            incoming_thread_info =
+                current_provider_->GetThread(incoming_thread_ref);
+          }
+          Thread& incoming_thread = GetThread(incoming_thread_info.tid);
+
+          // Idle threads are identified by pid == 0 and prio == 0.
+          const bool incoming_is_idle =
+              incoming_thread.info.pid == 0 && incoming_priority == 0;
+          const bool outgoing_is_idle =
+              outgoing_thread.info.pid == 0 && outgoing_priority == 0;
+
+          // Handle switching away from the currently running thread.
+          if (!outgoing_is_idle) {
+            SwitchFrom(&outgoing_thread, ts, cpu, outgoing_state);
+          }
+
+          // Handle switching to the new currently running thread.
+          if (!incoming_is_idle) {
+            SwitchTo(&incoming_thread, ts, cpu, incoming_priority);
+          }
+          break;
         }
-      } else {
-        outgoing_thread_info =
-            current_provider_->thread_table[outgoing_thread_ref];
-      }
-      Thread& outgoing_thread = GetThread(outgoing_thread_info.tid);
+        case kSchedulerEventContextSwitch: {
+          const uint32_t argument_count =
+              fuchsia_trace_utils::ReadField<uint32_t>(header, 16, 19);
+          const uint32_t cpu =
+              fuchsia_trace_utils::ReadField<uint32_t>(header, 20, 35);
+          const uint32_t outgoing_state =
+              fuchsia_trace_utils::ReadField<uint32_t>(header, 36, 39);
 
-      FuchsiaThreadInfo incoming_thread_info;
-      if (fuchsia_trace_utils::IsInlineThread(incoming_thread_ref)) {
-        if (!cursor.ReadInlineThread(&incoming_thread_info)) {
-          context_->storage->IncrementStats(stats::fuchsia_invalid_event);
-          return;
+          int64_t ts;
+          if (!cursor.ReadTimestamp(current_provider_->ticks_per_second, &ts)) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
+          if (ts < 0) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
+
+          uint64_t outgoing_tid;
+          if (!cursor.ReadUint64(&outgoing_tid)) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
+          Thread& outgoing_thread = GetThread(outgoing_tid);
+
+          uint64_t incoming_tid;
+          if (!cursor.ReadUint64(&incoming_tid)) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
+          Thread& incoming_thread = GetThread(incoming_tid);
+
+          auto maybe_args = FuchsiaTraceParser::ParseArgs(
+              cursor, argument_count, intern_string, get_string);
+          if (!maybe_args.has_value()) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
+
+          int32_t incoming_weight = 0;
+          int32_t outgoing_weight = 0;
+
+          for (const auto& arg : *maybe_args) {
+            if (arg.name == incoming_weight_id_) {
+              if (arg.value.Type() != ArgValue::ArgType::kInt32) {
+                context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+                return;
+              }
+              incoming_weight = arg.value.Int32();
+            } else if (arg.name == outgoing_weight_id_) {
+              if (arg.value.Type() != ArgValue::ArgType::kInt32) {
+                context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+                return;
+              }
+              outgoing_weight = arg.value.Int32();
+            }
+          }
+
+          const bool incoming_is_idle = incoming_weight == kIdleWeight;
+          const bool outgoing_is_idle = outgoing_weight == kIdleWeight;
+
+          // Handle switching away from the currently running thread.
+          if (!outgoing_is_idle) {
+            SwitchFrom(&outgoing_thread, ts, cpu, outgoing_state);
+          }
+
+          // Handle switching to the new currently running thread.
+          if (!incoming_is_idle) {
+            SwitchTo(&incoming_thread, ts, cpu, incoming_weight);
+          }
+          break;
         }
-      } else {
-        incoming_thread_info =
-            current_provider_->thread_table[incoming_thread_ref];
-      }
-      Thread& incoming_thread = GetThread(incoming_thread_info.tid);
+        case kSchedulerEventThreadWakeup: {
+          const uint32_t argument_count =
+              fuchsia_trace_utils::ReadField<uint32_t>(header, 16, 19);
+          const uint32_t cpu =
+              fuchsia_trace_utils::ReadField<uint32_t>(header, 20, 35);
 
-      // Idle threads are identified by pid == 0 and prio == 0.
-      const bool incoming_is_idle =
-          incoming_thread.info.pid == 0 && incoming_priority == 0;
-      const bool outgoing_is_idle =
-          outgoing_thread.info.pid == 0 && outgoing_priority == 0;
+          int64_t ts;
+          if (!cursor.ReadTimestamp(current_provider_->ticks_per_second, &ts)) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
+          if (ts < 0) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
 
-      // Handle switching away from the currently running thread.
-      if (!outgoing_is_idle) {
-        UniqueTid utid = procs->UpdateThread(
-            static_cast<uint32_t>(outgoing_thread.info.tid),
-            static_cast<uint32_t>(outgoing_thread.info.pid));
+          uint64_t waking_tid;
+          if (!cursor.ReadUint64(&waking_tid)) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
+          Thread& waking_thread = GetThread(waking_tid);
 
-        StringId state = IdForOutgoingThreadState(outgoing_state);
+          auto maybe_args = FuchsiaTraceParser::ParseArgs(
+              cursor, argument_count, intern_string, get_string);
+          if (!maybe_args.has_value()) {
+            context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+            return;
+          }
 
-        const auto duration = ts - outgoing_thread.last_ts;
-        outgoing_thread.last_ts = ts;
+          int32_t waking_weight = 0;
 
-        // Close the slice record if one is open for this thread.
-        if (outgoing_thread.last_slice_row.has_value()) {
-          auto row_ref = outgoing_thread.last_slice_row->ToRowReference(
-              storage->mutable_sched_slice_table());
-          row_ref.set_dur(duration);
-          row_ref.set_end_state(state);
-          row_ref.set_priority(outgoing_priority);
-          outgoing_thread.last_slice_row.reset();
+          for (const auto& arg : *maybe_args) {
+            if (arg.name == weight_id_) {
+              if (arg.value.Type() != ArgValue::ArgType::kInt32) {
+                context_->storage->IncrementStats(stats::fuchsia_invalid_event);
+                return;
+              }
+              waking_weight = arg.value.Int32();
+            }
+          }
+
+          const bool waking_is_idle = waking_weight == kIdleWeight;
+          if (!waking_is_idle) {
+            Wake(&waking_thread, ts, cpu);
+          }
+          break;
         }
-
-        // Close the state record if one is open for this thread.
-        if (outgoing_thread.last_state_row.has_value()) {
-          auto row_ref = outgoing_thread.last_state_row->ToRowReference(
-              storage->mutable_thread_state_table());
-          row_ref.set_dur(duration);
-          outgoing_thread.last_state_row.reset();
-        }
-
-        // Open a new state record to track the duration of the outgoing state.
-        tables::ThreadStateTable::Row state_row;
-        state_row.ts = ts;
-        state_row.cpu = cpu;
-        state_row.dur = -1;
-        state_row.state = state;
-        state_row.utid = utid;
-        auto state_row_number =
-            storage->mutable_thread_state_table()->Insert(state_row).row_number;
-        outgoing_thread.last_state_row = state_row_number;
-      }
-
-      // Handle switching to the new currently running thread.
-      if (!incoming_is_idle) {
-        UniqueTid utid = procs->UpdateThread(
-            static_cast<uint32_t>(incoming_thread.info.tid),
-            static_cast<uint32_t>(incoming_thread.info.pid));
-
-        const auto duration = ts - incoming_thread.last_ts;
-        incoming_thread.last_ts = ts;
-
-        // Close the state record if one is open for this thread.
-        if (incoming_thread.last_state_row.has_value()) {
-          auto row_ref = incoming_thread.last_state_row->ToRowReference(
-              storage->mutable_thread_state_table());
-          row_ref.set_dur(duration);
-          incoming_thread.last_state_row.reset();
-        }
-
-        // Open a new slice record for this thread.
-        tables::SchedSliceTable::Row slice_row;
-        slice_row.ts = ts;
-        slice_row.cpu = cpu;
-        slice_row.dur = -1;
-        slice_row.utid = utid;
-        slice_row.priority = incoming_priority;
-        auto slice_row_number =
-            storage->mutable_sched_slice_table()->Insert(slice_row).row_number;
-        incoming_thread.last_slice_row = slice_row_number;
-
-        // Open a new state record for this thread.
-        tables::ThreadStateTable::Row state_row;
-        state_row.ts = ts;
-        state_row.cpu = cpu;
-        state_row.dur = -1;
-        state_row.state = running_string_id_;
-        state_row.utid = utid;
-        auto state_row_number =
-            storage->mutable_thread_state_table()->Insert(state_row).row_number;
-        incoming_thread.last_state_row = state_row_number;
+        default:
+          PERFETTO_DLOG("Skipping unknown scheduler event type %d", event_type);
+          break;
       }
 
       break;
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
index 35e66f5..4b4d610 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
@@ -46,6 +46,24 @@
     std::unordered_map<uint64_t, StringId> string_table;
     std::unordered_map<uint64_t, FuchsiaThreadInfo> thread_table;
 
+    // Returns a StringId for the given FXT string ref id.
+    StringId GetString(uint64_t string_ref) {
+      auto search = string_table.find(string_ref);
+      if (search != string_table.end()) {
+        return search->second;
+      }
+      return kNullStringId;
+    }
+
+    // Returns a FuchsiaThreadInfo for the given FXT thread ref id.
+    FuchsiaThreadInfo GetThread(uint64_t thread_ref) {
+      auto search = thread_table.find(thread_ref);
+      if (search != thread_table.end()) {
+        return search->second;
+      }
+      return {0, 0};
+    }
+
     uint64_t ticks_per_second = 1000000000;
   };
 
@@ -59,6 +77,13 @@
     base::Optional<tables::ThreadStateTable::RowNumber> last_state_row;
   };
 
+  void SwitchFrom(Thread* thread,
+                  int64_t ts,
+                  uint32_t cpu,
+                  uint32_t thread_state);
+  void SwitchTo(Thread* thread, int64_t ts, uint32_t cpu, int32_t weight);
+  void Wake(Thread* thread, int64_t ts, uint32_t cpu);
+
   // Allocates or returns an existing Thread instance for the given tid.
   Thread& GetThread(uint64_t tid) {
     auto search = threads_.find(tid);
@@ -88,11 +113,18 @@
   StringId running_string_id_;
   StringId runnable_string_id_;
   StringId preempted_string_id_;
+  StringId waking_string_id_;
   StringId blocked_string_id_;
   StringId suspended_string_id_;
   StringId exit_dying_string_id_;
   StringId exit_dead_string_id_;
 
+  // Interned string ids for record arguments.
+  StringId incoming_weight_id_;
+  StringId outgoing_weight_id_;
+  StringId weight_id_;
+  StringId process_id_;
+
   // Map from tid to Thread.
   std::unordered_map<uint64_t, Thread> threads_;
 };
diff --git a/src/trace_processor/metrics/sql/android/OWNERS b/src/trace_processor/metrics/sql/android/OWNERS
index 329c8a2..3053a78 100644
--- a/src/trace_processor/metrics/sql/android/OWNERS
+++ b/src/trace_processor/metrics/sql/android/OWNERS
@@ -1,2 +1,4 @@
 ilkos@google.com
-timmurray@google.com
\ No newline at end of file
+timmurray@google.com
+
+per-file hsc.sql=steventerrell@google.com,carmenjackson@google.com
diff --git a/src/trace_processor/metrics/sql/android/startup/hsc.sql b/src/trace_processor/metrics/sql/android/startup/hsc.sql
index c632673..9813b69 100644
--- a/src/trace_processor/metrics/sql/android/startup/hsc.sql
+++ b/src/trace_processor/metrics/sql/android/startup/hsc.sql
@@ -55,6 +55,18 @@
 JOIN android_startups launches ON launches.package GLOB '*' || functions.process_name || '*'
 WHERE functions.function_name GLOB "Choreographer#doFrame*" AND functions.ts > launches.ts;
 
+DROP VIEW IF EXISTS android_render_frame_times;
+CREATE VIEW android_render_frame_times AS
+SELECT
+  functions.ts AS ts,
+  functions.ts + functions.dur AS ts_end,
+  launches.package AS name,
+  launches.startup_id,
+  ROW_NUMBER() OVER(PARTITION BY launches.startup_id ORDER BY functions.ts ASC) AS number
+FROM functions
+JOIN android_startups launches ON launches.package GLOB '*' || functions.process_name || '*'
+WHERE functions.function_name GLOB "DrawFrame*" AND functions.ts > launches.ts;
+
 DROP VIEW IF EXISTS frame_times;
 CREATE VIEW frame_times AS
 SELECT startup_id AS launch_id, * FROM android_frame_times;
@@ -73,15 +85,15 @@
 WHERE android_frame_times.number = 2 AND android_frame_times.name GLOB "*roid.calcul*" AND android_frame_times.startup_id = launches.startup_id;
 
 -- Calendar
+-- Using the DrawFrame slice from the render thread due to Calendar delaying its rendering
 INSERT INTO hsc_based_startup_times
 SELECT
   launches.package AS package,
   launches.startup_id AS id,
-  android_frame_times.ts_end - launches.ts AS ts_total
-FROM android_frame_times
-JOIN android_startups launches ON launches.package GLOB '*' || android_frame_times.name || '*'
-WHERE android_frame_times.name GLOB "*id.calendar*" AND android_frame_times.startup_id = launches.startup_id
-ORDER BY ABS(android_frame_times.ts_end - (SELECT ts + dur FROM functions WHERE function_name GLOB "DrawFrame*" AND process_name GLOB "*id.calendar" ORDER BY ts LIMIT 1)) LIMIT 1;
+  android_render_frame_times.ts_end - launches.ts AS ts_total
+FROM android_render_frame_times
+JOIN android_startups launches ON launches.package GLOB '*' || android_render_frame_times.name || '*'
+WHERE android_render_frame_times.number = 5 AND android_render_frame_times.name GLOB "*id.calendar*" AND android_render_frame_times.startup_id = launches.startup_id;
 
 -- Camera
 INSERT INTO hsc_based_startup_times
@@ -125,6 +137,7 @@
 WHERE android_frame_times.number = 3 AND android_frame_times.name GLOB "*id.contacts" AND android_frame_times.startup_id = launches.startup_id;
 
 -- Dialer
+-- Dialer only runs one animation at startup, use the last animation frame to indicate startup.
 INSERT INTO hsc_based_startup_times
 SELECT
   launches.package AS package,
@@ -132,7 +145,7 @@
   android_frame_times.ts_end - launches.ts AS ts_total
 FROM android_frame_times
 JOIN android_startups launches ON launches.package GLOB '*' || android_frame_times.name || '*'
-WHERE android_frame_times.number = 1 AND android_frame_times.name GLOB "*id.dialer" AND android_frame_times.startup_id = launches.startup_id;
+WHERE android_frame_times.ts > (SELECT ts + dur FROM animators WHERE process_name GLOB "*id.dialer" AND animator_name GLOB "*animator*" ORDER BY (ts + dur) DESC LIMIT 1) AND android_frame_times.name GLOB "*id.dialer" AND android_frame_times.startup_id = launches.startup_id LIMIT 1;
 
 -- Facebook
 INSERT INTO hsc_based_startup_times
@@ -179,6 +192,7 @@
 ORDER BY ts_total LIMIT 1;
 
 -- Maps
+-- Use the 8th choreographer frame to indicate startup.
 INSERT INTO hsc_based_startup_times
 SELECT
   launches.package AS package,
@@ -186,7 +200,7 @@
   android_frame_times.ts_end - launches.ts AS ts_total
 FROM android_frame_times
 JOIN android_startups launches ON launches.package GLOB '*' || android_frame_times.name || '*'
-WHERE android_frame_times.number = 1 AND android_frame_times.name GLOB "*maps*" AND android_frame_times.startup_id = launches.startup_id;
+WHERE android_frame_times.number = 8 AND android_frame_times.name GLOB "*maps*" AND android_frame_times.startup_id = launches.startup_id;
 
 -- Messages
 INSERT INTO hsc_based_startup_times
@@ -211,6 +225,7 @@
 ORDER BY ts_total DESC LIMIT 1;
 
 -- Photos
+-- Use the animator:translationZ slice as startup indicator.
 INSERT INTO hsc_based_startup_times
 SELECT
   launches.package AS package,
@@ -218,7 +233,7 @@
   android_frame_times.ts_end - launches.ts AS ts_total
 FROM android_frame_times
 JOIN android_startups launches ON launches.package GLOB '*' || android_frame_times.name || '*'
-WHERE android_frame_times.number = 1 AND android_frame_times.name GLOB "*apps.photos*" AND android_frame_times.startup_id = launches.startup_id;
+WHERE android_frame_times.ts > (SELECT ts + dur FROM animators WHERE process_name GLOB "*apps.photos" AND animator_name GLOB "animator:translationZ" ORDER BY (ts + dur) DESC LIMIT 1) AND android_frame_times.name GLOB "*apps.photos*" AND android_frame_times.startup_id = launches.startup_id LIMIT 1;
 
 -- Settings was deprecated in favor of reportFullyDrawn b/169694037.
 
@@ -255,11 +270,12 @@
 ORDER BY ts_total LIMIT 1;
 
 -- Youtube
+-- Use the 10th frame that is rendered
 INSERT INTO hsc_based_startup_times
 SELECT
   launches.package AS package,
   launches.startup_id AS id,
-  android_frame_times.ts_end - launches.ts AS ts_total
-FROM android_frame_times
-JOIN android_startups launches ON launches.package GLOB '*' || android_frame_times.name || '*'
-WHERE android_frame_times.number = 2 AND android_frame_times.name GLOB "*id.youtube" AND android_frame_times.startup_id = launches.startup_id;
+  android_render_frame_times.ts_end - launches.ts AS ts_total
+FROM android_render_frame_times
+JOIN android_startups launches ON launches.package GLOB '*' || android_render_frame_times.name || '*'
+WHERE android_render_frame_times.number = 10 AND android_render_frame_times.name GLOB "*id.youtube" AND android_render_frame_times.startup_id = launches.startup_id;
diff --git a/test/cts/Android.bp b/test/cts/Android.bp
index c5a8ed6..c05189a 100644
--- a/test/cts/Android.bp
+++ b/test/cts/Android.bp
@@ -14,7 +14,6 @@
     "end_to_end_integrationtest_cts.cc",
     "heapprofd_java_test_cts.cc",
     "heapprofd_test_cts.cc",
-    "reporter_test_cts.cc",
     "traced_perf_test_cts.cc",
     ":perfetto_protos_perfetto_config_cpp_gen",
     ":perfetto_protos_perfetto_common_cpp_gen"
@@ -59,7 +58,6 @@
         ":CtsPerfettoProducerApp",
         ":CtsPerfettoReleaseApp",
         ":CtsPerfettoDebuggableApp",
-        ":CtsPerfettoReporterApp"
   ],
   stl: "libc++_static",
   // This test is also run via Mainline Testing against the ART Mainline
diff --git a/test/cts/AndroidTest.xml b/test/cts/AndroidTest.xml
index 84e72bc..27ccef1 100644
--- a/test/cts/AndroidTest.xml
+++ b/test/cts/AndroidTest.xml
@@ -28,7 +28,6 @@
         <option name="test-file-name" value="CtsPerfettoReleaseApp.apk" />
         <option name="test-file-name" value="CtsPerfettoProfileableApp.apk" />
         <option name="test-file-name" value="CtsPerfettoNonProfileableApp.apk" />
-        <option name="test-file-name" value="CtsPerfettoReporterApp.apk" />
     </target_preparer>
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
         <option name="cleanup" value="true" />
diff --git a/test/cts/producer/Android.bp b/test/cts/producer/Android.bp
index 3c07ce4..c2695e9 100644
--- a/test/cts/producer/Android.bp
+++ b/test/cts/producer/Android.bp
@@ -32,9 +32,9 @@
 
     compile_multilib: "both",
     srcs: ["src/**/*.java"],
-    sdk_version: "current",
     // This test is also run via Mainline Testing against the ART Mainline
     // Module, which is updatable since Android 12 (API level 31).
+    sdk_version: "31",
     min_sdk_version: "31",
     jni_libs: [
         "libperfettocts_jni",
diff --git a/test/cts/producer/jni/Android.bp b/test/cts/producer/jni/Android.bp
index 696dc97..70cecbf 100644
--- a/test/cts/producer/jni/Android.bp
+++ b/test/cts/producer/jni/Android.bp
@@ -14,8 +14,6 @@
   ],
   header_libs: ["jni_headers"],
   static_libs: [
-    "libgtest",
-    "libprotobuf-cpp-lite",
     "perfetto_cts_jni_deps",
     "libperfetto_client_experimental",
   ],
diff --git a/test/cts/reporter/Android.bp b/test/cts/reporter/Android.bp
new file mode 100644
index 0000000..597426c
--- /dev/null
+++ b/test/cts/reporter/Android.bp
@@ -0,0 +1,57 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "external_perfetto_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["external_perfetto_license"],
+}
+
+cc_test {
+  name: "CtsPerfettoReporterTestCases",
+  srcs: [
+    "reporter_test_cts.cc",
+    ":perfetto_protos_perfetto_config_cpp_gen",
+    ":perfetto_protos_perfetto_common_cpp_gen"
+  ],
+  generated_headers: [
+    "perfetto_protos_perfetto_config_cpp_gen_headers",
+    "perfetto_protos_perfetto_common_cpp_gen_headers",
+  ],
+  static_libs: [
+    "libgmock",
+    "libprotobuf-cpp-lite",
+    "libperfetto_client_experimental",
+    "perfetto_cts_deps",
+    "perfetto_trace_protos",
+  ],
+  whole_static_libs: [
+    "perfetto_gtest_logcat_printer",
+  ],
+  shared_libs: [
+    "libandroid",
+    "liblog",
+  ],
+  test_suites: [
+    "cts",
+    "vts10",
+    "general-tests",
+  ],
+  compile_multilib: "both",
+  multilib: {
+    lib32: {
+        suffix: "32",
+    },
+    lib64: {
+        suffix: "64",
+    },
+  },
+  per_testcase_directory: true,
+  data: [
+        ":CtsPerfettoReporterApp"
+  ],
+  stl: "libc++_static",
+  defaults: [
+    "perfetto_defaults",
+  ],
+}
diff --git a/test/cts/reporter/AndroidTest.xml b/test/cts/reporter/AndroidTest.xml
new file mode 100644
index 0000000..0fe8c0e
--- /dev/null
+++ b/test/cts/reporter/AndroidTest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config for CTS Perfetto Reporter test cases">
+    <option name="config-descriptor:metadata" key="component" value="metrics" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="test-suite-tag" value="cts" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsPerfettoReporterApp.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="CtsPerfettoReporterTestCases->/data/local/tmp/CtsPerfettoReporterTestCases" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="CtsPerfettoReporterTestCases" />
+        <option name="runtime-hint" value="0m40s" />
+        <!-- test-timeout unit is ms -->
+        <option name="native-test-timeout" value="40000" />
+    </test>
+</configuration>
diff --git a/test/cts/reporter/app/Android.bp b/test/cts/reporter/app/Android.bp
index 6e7c880..ab2633b 100644
--- a/test/cts/reporter/app/Android.bp
+++ b/test/cts/reporter/app/Android.bp
@@ -27,7 +27,4 @@
     srcs: ["src/**/*.java"],
     platform_apis: true,
     privileged: true,
-    // This test is also run via Mainline Testing against the ART Mainline
-    // Module, which is updatable since Android 12 (API level 31).
-    min_sdk_version: "31",
 }
diff --git a/test/cts/reporter_test_cts.cc b/test/cts/reporter/reporter_test_cts.cc
similarity index 100%
rename from test/cts/reporter_test_cts.cc
rename to test/cts/reporter/reporter_test_cts.cc
diff --git a/test/cts/test_apps/Android.bp b/test/cts/test_apps/Android.bp
index 648a23c..a11e7cb 100644
--- a/test/cts/test_apps/Android.bp
+++ b/test/cts/test_apps/Android.bp
@@ -34,9 +34,9 @@
 
     compile_multilib: "both",
     srcs: ["src/**/*.java"],
-    sdk_version: "current",
     // This test is also run via Mainline Testing against the ART Mainline
     // Module, which is updatable since Android 12 (API level 31).
+    sdk_version: "31",
     min_sdk_version: "31",
     jni_libs: [
         "libperfettocts_native",
@@ -57,9 +57,9 @@
 
     compile_multilib: "both",
     srcs: ["src/**/*.java"],
-    sdk_version: "current",
     // This test is also run via Mainline Testing against the ART Mainline
     // Module, which is updatable since Android 12 (API level 31).
+    sdk_version: "31",
     min_sdk_version: "31",
     jni_libs: [
         "libperfettocts_native",
@@ -80,9 +80,9 @@
 
     compile_multilib: "both",
     srcs: ["src/**/*.java"],
-    sdk_version: "current",
     // This test is also run via Mainline Testing against the ART Mainline
     // Module, which is updatable since Android 12 (API level 31).
+    sdk_version: "31",
     min_sdk_version: "31",
     jni_libs: [
         "libperfettocts_native",
@@ -103,9 +103,9 @@
 
     compile_multilib: "both",
     srcs: ["src/**/*.java"],
-    sdk_version: "current",
     // This test is also run via Mainline Testing against the ART Mainline
     // Module, which is updatable since Android 12 (API level 31).
+    sdk_version: "31",
     min_sdk_version: "31",
     jni_libs: [
         "libperfettocts_native",
diff --git a/test/data/fuchsia_trace_sched.fxt.sha256 b/test/data/fuchsia_trace_sched.fxt.sha256
new file mode 100644
index 0000000..55b5cf8
--- /dev/null
+++ b/test/data/fuchsia_trace_sched.fxt.sha256
@@ -0,0 +1 @@
+e796127dd16d1f4676dc33016f133010fcf138aa68fb5b85443167527f8a8602
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/fuchsia/tests.py b/test/trace_processor/diff_tests/fuchsia/tests.py
index 86d08a7..54d0773 100644
--- a/test/trace_processor/diff_tests/fuchsia/tests.py
+++ b/test/trace_processor/diff_tests/fuchsia/tests.py
@@ -52,6 +52,36 @@
         19677791779,3,96082,"S",20,1680
         """))
 
+  def test_fuchsia_sched(self):
+    return DiffTestBlueprint(
+        trace=DataPath('fuchsia_trace_sched.fxt'),
+        query="""
+        SELECT
+          ts,
+          cpu,
+          dur,
+          end_state,
+          priority,
+          tid
+        FROM sched
+        JOIN thread USING(utid)
+        ORDER BY ts
+        LIMIT 10;
+        """,
+        out=Csv("""
+        "ts","cpu","dur","end_state","priority","tid"
+        68988611421,3,313611,"S",3122,3196
+        68988925032,3,98697,"S",3122,23416
+        68988957574,0,632536,"S",3122,3189
+        68989023729,3,51371,"S",3122,3196
+        68989075100,3,46773,"R",3122,25332
+        68989121873,3,53620,"S",2147483647,24654
+        68989175493,3,5241,"S",3122,25332
+        68989180734,3,138507,"S",3122,30933
+        68989319241,3,25028,"S",3122,30297
+        68989344269,3,52723,"S",3122,28343
+        """))
+
   def test_fuchsia_smoke_slices(self):
     return DiffTestBlueprint(
         trace=DataPath('fuchsia_trace.fxt'),
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index b7da65d..450acf8 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -133,7 +133,7 @@
     'android.hardware.health@2.0',
     'android.hardware.health-V2-ndk',
     'android.hardware.power.stats@1.0',
-    "android.hardware.power.stats-V1-cpp",
+    'android.hardware.power.stats-V1-cpp',
     'base',
     'binder',
     'binder_ndk',
@@ -145,7 +145,7 @@
     'log',
     'services',
     'statssocket',
-    "tracingproxy",
+    'tracingproxy',
     'utils',
 ]
 
diff --git a/tools/java_heap_dump b/tools/java_heap_dump
index c14dde4..5ef3cfd 100755
--- a/tools/java_heap_dump
+++ b/tools/java_heap_dump
@@ -151,11 +151,22 @@
     continuous_dump_cfg = CONTINUOUS_DUMP.format(
         dump_interval=args.continuous_dump)
 
-  if args.stop_when_done:
+  if args.continuous_dump:
+    # Unlimited trace duration
+    duration_ms = 0
+  elif args.stop_when_done:
+    # Oneshot heapdump and the system supports data_source_stop_timeout_ms, we
+    # can use a short duration.
     duration_ms = 1000
+  else:
+    # Oneshot heapdump, but the system doesn't supports
+    # data_source_stop_timeout_ms, we have to use a longer duration in the hope
+    # of giving enough time to capture the whole dump.
+    duration_ms = 20000
+
+  if args.stop_when_done:
     data_source_stop_timeout_ms = 100000
   else:
-    duration_ms = 20000
     data_source_stop_timeout_ms = 0
 
   return CFG.format(
@@ -230,7 +241,8 @@
   parser.add_argument(
       "-c",
       "--continuous-dump",
-      help="Dump interval in ms. 0 to disable continuous dump.",
+      help="Dump interval in ms. 0 to disable continuous dump. When continuous "
+      "dump is enabled, use CTRL+C to stop",
       type=int,
       default=0)
   parser.add_argument(
@@ -314,11 +326,21 @@
     print("Dumping Java Heap.")
 
   exists = True
+  ctrl_c_count = 0
   # Wait for perfetto cmd to return.
   while exists:
-    exists = subprocess.call(
-        ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0
-    time.sleep(1)
+    try:
+      exists = subprocess.call(
+          ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0
+      time.sleep(1)
+    except KeyboardInterrupt as e:
+      ctrl_c_count += 1
+      subprocess.check_call(
+          ['adb', 'shell', 'kill -TERM {}'.format(perfetto_pid)])
+      if ctrl_c_count == 1:
+        print("Stopping perfetto and waiting for data...")
+      else:
+        raise e
 
   subprocess.check_call(['adb', 'pull', PROFILE_PATH, output_file], stdout=NULL)