Add thread state support to Chrome JSON trace format

This change enables third-party tools to emit thread scheduling state
information in Chrome JSON format that Perfetto can parse and visualize.

Changes:
- json_trace_parser: Add phase 'T' handler for thread state events
  Parses thread state data (Running, Sleeping, Runnable, etc.) and
  inserts into thread_state table. Supports optional fields: cpu,
  io_wait, blocked_function, waker_tid, irq_context.

- Sched plugin: Fix thread state track creation for JSON traces
  Modified hasSched() to check thread_state table in addition to sched.
  Updated addThreadStateTracks() query to include threads with only
  thread_state data (no sched_slice data), enabling visualization of
  thread states from Chrome JSON traces.

- Add test coverage for JSON thread state parsing
- Add sample trace demonstrating thread state events

This allows instrumentation libraries in various languages to emit
thread scheduling information using the established Chrome JSON format,
which can then be analyzed in Perfetto UI alongside other trace data.

Test: test_json_thread_state
Change-Id: Id262ef670357f8f5f8a1b86502642487a3065641
diff --git a/src/trace_processor/importers/json/json_trace_parser.cc b/src/trace_processor/importers/json/json_trace_parser.cc
index a537427..9d675e9 100644
--- a/src/trace_processor/importers/json/json_trace_parser.cc
+++ b/src/trace_processor/importers/json/json_trace_parser.cc
@@ -31,6 +31,7 @@
 #include "perfetto/ext/base/variant.h"
 #include "src/trace_processor/containers/null_term_string_view.h"
 #include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/importers/common/cpu_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
 #include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
@@ -390,6 +391,73 @@
       }
       break;
     }
+    case 'T': {  // Thread state event.
+      if (event.dur == std::numeric_limits<int64_t>::max()) {
+        context_->storage->IncrementStats(stats::json_parser_failure);
+        return;
+      }
+
+      // Create thread state row
+      tables::ThreadStateTable::Row row;
+      row.ts = timestamp;
+      row.dur = event.dur;
+      row.utid = utid;
+      row.state = slice_name_id;  // Use event name as state
+
+      // Parse optional fields from args if present
+      if (event.args_size > 0) {
+        it_.Reset(event.args.get(), event.args.get() + event.args_size);
+        if (it_.ParseStart()) {
+          for (;;) {
+            switch (it_.ParseObjectFieldWithoutRecursing()) {
+              case json::ReturnCode::kEndOfScope:
+              case json::ReturnCode::kOk:
+                break;
+              case json::ReturnCode::kError:
+              case json::ReturnCode::kIncompleteInput:
+                continue;
+            }
+            if (it_.eof()) {
+              break;
+            }
+
+            std::string_view key = it_.key();
+            if (key == "io_wait") {
+              if (const auto* val = std::get_if<int64_t>(&it_.value())) {
+                row.io_wait = static_cast<uint32_t>(*val);
+              }
+            } else if (key == "blocked_function") {
+              std::string_view func = GetStringValue(it_.value());
+              if (!func.empty()) {
+                row.blocked_function = storage->InternString(func);
+              }
+            } else if (key == "waker_tid") {
+              if (const auto* val = std::get_if<int64_t>(&it_.value())) {
+                uint32_t waker_tid = static_cast<uint32_t>(*val);
+                uint32_t waker_pid = event.pid;  // Default to same process
+                UniqueTid waker_utid =
+                    procs->UpdateThread(waker_tid, waker_pid);
+                row.waker_utid = waker_utid;
+              }
+            } else if (key == "waker_pid") {
+              // Store for potential use with waker_tid
+            } else if (key == "cpu") {
+              if (const auto* val = std::get_if<int64_t>(&it_.value())) {
+                row.ucpu = context_->cpu_tracker->GetOrCreateCpu(
+                    static_cast<uint32_t>(*val));
+              }
+            } else if (key == "irq_context") {
+              if (const auto* val = std::get_if<int64_t>(&it_.value())) {
+                row.irq_context = static_cast<uint32_t>(*val);
+              }
+            }
+          }
+        }
+      }
+
+      storage->mutable_thread_state_table()->Insert(row);
+      break;
+    }
     case 'M': {  // Metadata events (process and thread names).
       if (event.args_size == 0) {
         break;
diff --git a/test/trace_processor/diff_tests/parser/json/tests.py b/test/trace_processor/diff_tests/parser/json/tests.py
index 83fcdac..7b62692 100644
--- a/test/trace_processor/diff_tests/parser/json/tests.py
+++ b/test/trace_processor/diff_tests/parser/json/tests.py
@@ -1618,3 +1618,106 @@
           "02 03 ",2000,2000
           "Routine Identifier",2000,2000
         """))
+
+  def test_json_thread_state(self):
+    return DiffTestBlueprint(
+        trace=Json('''
+          {
+            "traceEvents": [
+              {
+                "name": "process_name",
+                "ph": "M",
+                "pid": 100,
+                "args": {"name": "TestProcess"}
+              },
+              {
+                "name": "thread_name",
+                "ph": "M",
+                "pid": 100,
+                "tid": 101,
+                "args": {"name": "TestThread"}
+              },
+              {
+                "name": "Running",
+                "cat": "thread_state",
+                "ph": "T",
+                "ts": 1000,
+                "dur": 5000,
+                "pid": 100,
+                "tid": 101,
+                "args": {
+                  "cpu": 0
+                }
+              },
+              {
+                "name": "S",
+                "cat": "thread_state",
+                "ph": "T",
+                "ts": 6000,
+                "dur": 10000,
+                "pid": 100,
+                "tid": 101,
+                "args": {
+                  "io_wait": 0
+                }
+              },
+              {
+                "name": "D",
+                "cat": "thread_state",
+                "ph": "T",
+                "ts": 16000,
+                "dur": 8000,
+                "pid": 100,
+                "tid": 101,
+                "args": {
+                  "io_wait": 1,
+                  "blocked_function": "wait_on_page_locked"
+                }
+              },
+              {
+                "name": "R",
+                "cat": "thread_state",
+                "ph": "T",
+                "ts": 24000,
+                "dur": 2000,
+                "pid": 100,
+                "tid": 101
+              },
+              {
+                "name": "Running",
+                "cat": "thread_state",
+                "ph": "T",
+                "ts": 26000,
+                "dur": 4000,
+                "pid": 100,
+                "tid": 101,
+                "args": {
+                  "cpu": 1
+                }
+              }
+            ]
+          }
+        '''),
+        query='''
+          SELECT
+            ts.ts,
+            ts.dur,
+            ts.state,
+            ts.cpu as ucpu,
+            ts.io_wait,
+            ts.blocked_function,
+            thread.tid,
+            process.pid
+          FROM thread_state ts
+          JOIN thread USING (utid)
+          JOIN process USING (upid)
+          ORDER BY ts.ts
+        ''',
+        out=Csv("""
+          "ts","dur","state","ucpu","io_wait","blocked_function","tid","pid"
+          1000000,5000000,"Running",0,"[NULL]","[NULL]",101,100
+          6000000,10000000,"S","[NULL]",0,"[NULL]",101,100
+          16000000,8000000,"D","[NULL]",1,"wait_on_page_locked",101,100
+          24000000,2000000,"R","[NULL]","[NULL]","[NULL]",101,100
+          26000000,4000000,"Running",1,"[NULL]","[NULL]",101,100
+        """))
diff --git a/ui/src/plugins/dev.perfetto.Sched/index.ts b/ui/src/plugins/dev.perfetto.Sched/index.ts
index 19ab90e..1c067fe 100644
--- a/ui/src/plugins/dev.perfetto.Sched/index.ts
+++ b/ui/src/plugins/dev.perfetto.Sched/index.ts
@@ -261,6 +261,10 @@
       );
     }
 
+    // Query for threads that have either CPU scheduling data (from sched table)
+    // or thread state data (from thread_state table). This allows thread state
+    // tracks to be created for traces that only contain thread_state entries
+    // (e.g., from Chrome JSON traces) without requiring sched data.
     const result = await engine.query(`
       include perfetto module viz.threads;
       include perfetto module viz.summary.threads;
@@ -274,7 +278,11 @@
         is_main_thread as isMainThread,
         is_kernel_thread as isKernelThread
       from _threads_with_kernel_flag t
-      join _sched_summary using (utid)
+      where utid in (
+        select distinct utid from _sched_summary
+        union
+        select distinct utid from thread_state
+      )
     `);
 
     const it = result.iter({
@@ -417,8 +425,13 @@
   }
 
   private async hasSched(engine: Engine): Promise<boolean> {
-    const result = await engine.query(`SELECT ts FROM sched LIMIT 1`);
-    return result.numRows() > 0;
+    const schedResult = await engine.query(`SELECT ts FROM sched LIMIT 1`);
+    if (schedResult.numRows() > 0) return true;
+
+    const threadStateResult = await engine.query(
+      `SELECT ts FROM thread_state LIMIT 1`,
+    );
+    return threadStateResult.numRows() > 0;
   }
 
   private addSchedulingSummaryTracks(ctx: Trace) {