trace_processor: Add cpuidle state residences

Introduce cpuidle states cpu_counter_tracks. Counter tracks are named
cpuidle.{state}, with the state name determined by
/sys/devices/system/cpu/cpu*/cpuidle/state*/name.

Bug: 337135369
Test: build trace_processor locally, run a trace with `cpuidle_period_ms` enabled and check counters/cpu_counter_tracks
Test: diff tests
Change-Id: Ia12f5578d6a754e6ebe7d4fea1d70ab56fddb07a
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc b/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
index e549fa5..efa53c9 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
@@ -649,6 +649,24 @@
   EXPECT_EQ(row->ucpu().value, 1u);
 }
 
+TEST_F(ProtoTraceParserTest, LoadCpuIdleStats) {
+  auto* packet = trace_->add_packet();
+  uint64_t ts = 1000;
+  packet->set_timestamp(ts);
+  auto* bundle = packet->set_sys_stats();
+  auto* cpuidle_state = bundle->add_cpuidle_state();
+  cpuidle_state->set_cpu_id(0);
+  auto* cpuidle_state_entry = cpuidle_state->add_cpuidle_state_entry();
+  cpuidle_state_entry->set_state("mock_state0");
+  cpuidle_state_entry->set_duration_us(20000);
+  EXPECT_CALL(*event_, PushCounter(static_cast<int64_t>(ts),
+                                   static_cast<double>(20000), TrackId{0u}));
+  Tokenize();
+  context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(context_.storage->track_table().row_count(), 1u);
+}
+
 TEST_F(ProtoTraceParserTest, LoadMemInfo) {
   auto* packet = trace_->add_packet();
   uint64_t ts = 1000;
diff --git a/src/trace_processor/importers/proto/system_probes_parser.cc b/src/trace_processor/importers/proto/system_probes_parser.cc
index 3cfe982..162aa1b 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.cc
+++ b/src/trace_processor/importers/proto/system_probes_parser.cc
@@ -486,6 +486,27 @@
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(thermal.temp()), track);
   }
+
+  for (auto it = sys_stats.cpuidle_state(); it; ++it) {
+    ParseCpuIdleStats(ts, *it);
+  }
+}
+
+void SystemProbesParser::ParseCpuIdleStats(int64_t ts, ConstBytes blob) {
+  protos::pbzero::SysStats::CpuIdleState::Decoder cpuidle_state(blob);
+  uint32_t cpu_id = cpuidle_state.cpu_id();
+  for (auto cpuidle_field = cpuidle_state.cpuidle_state_entry(); cpuidle_field;
+       ++cpuidle_field) {
+    protos::pbzero::SysStats::CpuIdleStateEntry::Decoder idle(*cpuidle_field);
+    std::string state = idle.state().ToStdString();
+    uint64_t time = idle.duration_us();
+
+    std::string track_name = "cpuidle." + state;
+    StringId string_id = context_->storage->InternString(track_name.c_str());
+    TrackId track =
+        context_->track_tracker->InternCpuCounterTrack(string_id, cpu_id);
+    context_->event_tracker->PushCounter(ts, static_cast<double>(time), track);
+  }
 }
 
 void SystemProbesParser::ParseProcessTree(ConstBytes blob) {
diff --git a/src/trace_processor/importers/proto/system_probes_parser.h b/src/trace_processor/importers/proto/system_probes_parser.h
index 124c003..c888400 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.h
+++ b/src/trace_processor/importers/proto/system_probes_parser.h
@@ -46,6 +46,7 @@
   void ParseThreadStats(int64_t timestamp, uint32_t pid, ConstBytes);
   void ParseDiskStats(int64_t ts, ConstBytes blob);
   void ParseProcessFds(int64_t ts, uint32_t pid, ConstBytes);
+  void ParseCpuIdleStats(int64_t ts, ConstBytes);
 
   TraceProcessorContext* const context_;
 
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index d6a1960..5840800 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -79,6 +79,7 @@
 from diff_tests.parser.parsing.tests_debug_annotation import ParsingDebugAnnotation
 from diff_tests.parser.parsing.tests_memory_counters import ParsingMemoryCounters
 from diff_tests.parser.parsing.tests_rss_stats import ParsingRssStats
+from diff_tests.parser.parsing.tests_sys_stats import ParsingSysStats
 from diff_tests.parser.parsing.tests_traced_stats import ParsingTracedStats
 from diff_tests.parser.power.tests_energy_breakdown import PowerEnergyBreakdown
 from diff_tests.parser.power.tests_entity_state_residency import EntityStateResidency
@@ -228,6 +229,7 @@
       *ParsingDebugAnnotation(index_path, 'parser/parsing',
                               'ParsingDebugAnnotation').fetch(),
       *ParsingRssStats(index_path, 'parser/parsing', 'ParsingRssStats').fetch(),
+      *ParsingSysStats(index_path, 'parser/parsing', 'ParsingSysStats').fetch(),
       *ParsingMemoryCounters(index_path, 'parser/parsing',
                              'ParsingMemoryCounters').fetch(),
       *FtraceCrop(index_path, 'parser/ftrace', 'FtraceCrop').fetch(),
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests_sys_stats.py b/test/trace_processor/diff_tests/parser/parsing/tests_sys_stats.py
new file mode 100644
index 0000000..8e00929
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/parsing/tests_sys_stats.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      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.
+
+from python.generators.diff_tests.testing import Path, DataPath, Metric
+from python.generators.diff_tests.testing import Csv, Json, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class ParsingSysStats(TestSuite):
+
+  def test_cpuidle_stats(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          sys_stats {
+            cpuidle_state {
+              cpu_id: 0
+              cpuidle_state_entry {
+                state: "C8"
+                duration_us: 486626084
+              }
+            }
+          }
+          timestamp: 71625871363623
+          trusted_packet_sequence_id: 2
+        }
+        packet {
+          sys_stats {
+            cpuidle_state {
+              cpu_id: 0
+              cpuidle_state_entry {
+                state: "C8"
+                duration_us: 486636254
+              }
+            }
+          }
+          timestamp: 71626000387166
+          trusted_packet_sequence_id: 2
+        }
+        """),
+        query="""
+        SELECT ts, cct.name, value, cct.cpu
+        FROM counter c
+        JOIN cpu_counter_track cct on c.track_id = cct.id
+        ORDER BY ts;
+        """,
+        out=Csv("""
+        "ts","name","value","cpu"
+        71625871363623,"cpuidle.C8",486626084.000000,0
+        71626000387166,"cpuidle.C8",486636254.000000,0
+        """))
+
+  def test_thermal_zones(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          sys_stats {
+            thermal_zone {
+              name: "thermal_zone0"
+              temp: 29
+              type: "x86_pkg_temp"
+            }
+          }
+          timestamp: 71625871363623
+          trusted_packet_sequence_id: 2
+        }
+        packet {
+          sys_stats {
+            thermal_zone {
+              name: "thermal_zone0"
+              temp: 31
+              type: "x86_pkg_temp"
+            }
+          }
+          timestamp: 71626000387166
+          trusted_packet_sequence_id: 2
+        }
+        """),
+        query="""
+        SELECT c.ts,
+               t.name,
+               c.value
+        FROM counter_track t
+        JOIN counter c ON t.id = c.track_id
+        """,
+        out=Csv("""
+        "ts","name","value"
+        71625871363623,"x86_pkg_temp",29.000000
+        71626000387166,"x86_pkg_temp",31.000000
+        """))