1use fuchsia_inspect::Inspector;
17use fuchsia_inspect_contrib::nodes::BoundedListNode;
18use fuchsia_sync::Mutex;
19use futures_util::FutureExt;
20use std::cmp::Eq;
21pub use std::collections::HashMap;
22pub use std::ffi::{CStr, CString};
23use std::fmt::{Debug, Display};
24use std::fs::{self as fs, OpenOptions};
25use std::hash::Hash;
26use std::io::Write as OtherWrite;
27use std::marker::PhantomData;
28use std::path::Path;
29use std::str::FromStr;
30use std::sync::{Arc, LazyLock};
31use strum::IntoEnumIterator;
32use {fuchsia_inspect as inspect, fuchsia_trace as ftrace, zx};
33
34static CSTR_POOL: LazyLock<Mutex<HashMap<String, &'static CStr>>> =
35 LazyLock::new(|| Mutex::new(HashMap::new()));
36
37fn lazy_static_cstr(s: &str) -> Result<&'static CStr, StateRecorderError> {
47 let mut pool = CSTR_POOL.lock();
48
49 if let Some(existing_cstr) = pool.get(s) {
51 return Ok(existing_cstr);
52 }
53
54 let c_string = CString::new(s)
56 .map_err(|_| StateRecorderError::IncompatibleString(s.to_owned()))?
57 .into_boxed_c_str();
58
59 #[cfg(any(feature = "variant_asan", feature = "variant_hwasan"))]
66 fn disable_lsan() {
67 unsafe extern "C" {
68 fn __lsan_disable();
69 }
70 unsafe {
71 __lsan_disable();
72 }
73 }
74
75 #[cfg(not(any(feature = "variant_asan", feature = "variant_hwasan")))]
76 fn disable_lsan() {}
77
78 #[cfg(any(feature = "variant_asan", feature = "variant_hwasan"))]
79 fn enable_lsan() {
80 unsafe extern "C" {
81 fn __lsan_enable();
82 }
83 unsafe {
84 __lsan_enable();
85 }
86 }
87
88 #[cfg(not(any(feature = "variant_asan", feature = "variant_hwasan")))]
89 fn enable_lsan() {}
90
91 disable_lsan();
92 let static_cstr: &'static CStr = Box::leak(c_string);
93 enable_lsan();
94
95 pool.insert(s.to_owned(), static_cstr);
96
97 Ok(static_cstr)
98}
99
100static ROOT_NODE_NAME: &str = "power_observability_state_recorders";
101
102static SINGLETON_MANAGER: LazyLock<Arc<Mutex<StateRecorderManager>>> =
104 LazyLock::new(|| StateRecorderManager::new(inspect::component::inspector()));
105
106pub fn manager() -> Arc<Mutex<StateRecorderManager>> {
107 SINGLETON_MANAGER.clone()
108}
109
110#[derive(thiserror::Error, Debug)]
111pub enum StateRecorderError {
112 #[error("The name \"{0}\" is already in use")]
113 DuplicateName(String),
114 #[error("String \"{0}\" cannot be converted to a CString")]
115 IncompatibleString(String),
116 #[error("Invalid options: {0}")]
117 InvalidOptions(String),
118}
119
120pub struct StateRecorderManager {
123 pub node: inspect::Node,
124 names_in_use: Vec<String>,
126}
127
128impl StateRecorderManager {
129 pub fn new(inspector: &inspect::Inspector) -> Arc<Mutex<Self>> {
130 Arc::new(Mutex::new(Self {
131 node: inspector.root().create_child(ROOT_NODE_NAME),
132 names_in_use: Vec::new(),
133 }))
134 }
135
136 fn register_name(&mut self, name: &str) -> Result<(), StateRecorderError> {
137 if self.names_in_use.iter().any(|s| s == name) {
138 return Err(StateRecorderError::DuplicateName(name.to_owned()));
139 }
140 self.names_in_use.push(name.to_owned());
141 Ok(())
142 }
143
144 fn unregister_name(&mut self, name: &str) {
145 match self.names_in_use.iter().position(|s| s == name) {
146 Some(index) => {
147 self.names_in_use.remove(index);
148 }
149 None => {
150 log::error!("unregister_name called with nonexistent name \"{}\"", name);
151 }
152 }
153 }
154}
155
156fn register_with_manager(
158 manager: &Arc<Mutex<StateRecorderManager>>,
159 name: &str,
160) -> Result<inspect::Node, StateRecorderError> {
161 let mut manager = manager.lock();
162 if let Err(e) = manager.register_name(name) {
163 return Err(e);
164 }
165 Ok(manager.node.create_child(name))
166}
167
168fn setup_recording_backend<T, F>(
169 node: &inspect::Node,
170 options: &RecorderOptions,
171 record_item: F,
172) -> Result<(RecorderHistory<T>, Option<PersistenceHandler<T>>), StateRecorderError>
173where
174 T: Copy + std::fmt::Debug + std::fmt::Display + std::str::FromStr + Send + Sync + 'static,
175 F: Fn(&inspect::Node, &T) + Send + Sync + Clone + 'static,
176{
177 if options.lazy_record {
178 let shared_buffer = if let Some(config) = &options.persistence {
179 let (handler, buffer) = PersistenceHandler::new(config.clone(), options.capacity);
180
181 let prev_data = PersistenceHandler::<T>::read_log(&config.previous_path);
183 if !prev_data.is_empty() {
184 let data_arc = Arc::new(prev_data);
185 let record_item = record_item.clone();
186 node.record_lazy_child("previous_boot_history", move || {
187 let data = data_arc.clone();
188 let record_item = record_item.clone();
189 async move {
190 let inspector = Inspector::default();
191 let root = inspector.root();
192 for (i, (ts, val)) in data.iter().enumerate() {
193 root.record_child(i.to_string(), |child| {
194 child.record_int("@time", *ts);
195 record_item(child, val);
196 });
197 }
198 Ok(inspector)
199 }
200 .boxed()
201 });
202 }
203 (Some(handler), buffer)
204 } else {
205 (None, Arc::new(Mutex::new(TimestampRingBuffer::<T>::with_capacity(options.capacity))))
206 };
207
208 let buffer_cloned = shared_buffer.1.clone();
210 node.record_lazy_child("reset_info", move || {
211 let history = buffer_cloned.clone();
212 async move {
213 let inspector = Inspector::default();
214 let node = inspector.root();
215 let (count, last_reset_ns) = history.lock().get_reset_info();
216 node.record_int("count", count as i64);
217 node.record_int("last_reset_ns", last_reset_ns);
218 Ok(inspector)
219 }
220 .boxed()
221 });
222
223 let buffer_cloned = shared_buffer.1.clone();
225 node.record_lazy_child("history", move || {
226 let history = buffer_cloned.clone();
227 let record_item = record_item.clone();
228 async move {
229 let inspector = Inspector::default();
230 let node = inspector.root();
231 for (i, (timestamp, state_value)) in history.lock().iter().enumerate() {
232 node.record_child(format!("{}", i), |node| {
233 node.record_int("@time", timestamp);
234 record_item(node, &state_value);
235 });
236 }
237 Ok(inspector)
238 }
239 .boxed()
240 });
241
242 Ok((RecorderHistory::Lazy(shared_buffer.1), shared_buffer.0))
243 } else {
244 if options.persistence.is_some() {
245 return Err(StateRecorderError::InvalidOptions(
246 "Persistence not supported in eager mode".to_string(),
247 ));
248 }
249
250 node.record_child("reset_info", |node| {
251 node.record_int("count", 0);
252 node.record_int("last_reset_ns", zx::BootInstant::get().into_nanos());
253 });
254
255 let history_node = BoundedListNode::new(node.create_child("history"), options.capacity);
256 Ok((RecorderHistory::Eager(history_node), None))
257 }
258}
259
260pub trait RecordableEnum:
262 Copy + Debug + Display + Eq + Hash + IntoEnumIterator + Into<u64> + Send + Sync
263{
264}
265impl<T: Copy + Debug + Display + Eq + Hash + IntoEnumIterator + Into<u64> + Send + Sync>
266 RecordableEnum for T
267{
268}
269
270#[derive(Clone)]
273struct StateName {
274 trace_name: &'static CStr,
275 inspect_name: Arc<String>,
281}
282
283pub struct NamedU64StateRecorder {
287 manager: Arc<Mutex<StateRecorderManager>>,
288 name: String,
289 trace_category: &'static CStr,
290 state_names: HashMap<u64, StateName>,
291 history: RecorderHistory<u64>,
292 persistence: Option<PersistenceHandler<u64>>,
293 _root_node: inspect::Node,
294 vthread: ftrace::VThread<String>,
295 current_state_trace_name: Option<&'static CStr>,
296}
297
298impl std::fmt::Debug for NamedU64StateRecorder {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 f.debug_struct("NamedU64StateRecorder")
301 .field("metadata", &self.name)
302 .field("trace_category", &self.trace_category)
303 .field("history", &self.history)
304 .finish()
305 }
306}
307
308impl Drop for NamedU64StateRecorder {
309 fn drop(&mut self) {
310 self.manager.lock().unregister_name(&self.name);
311 }
312}
313
314impl NamedU64StateRecorder {
315 pub fn new(
326 name: String,
327 trace_category: &'static CStr,
328 state_names_map: HashMap<u64, String>,
329 options: RecorderOptions,
330 ) -> Result<Self, StateRecorderError> {
331 let manager = options.manager.clone().unwrap_or_else(|| SINGLETON_MANAGER.clone());
332 let node = register_with_manager(&manager, &name)?;
333
334 let mut state_names = HashMap::new();
337 for (value, name_str) in state_names_map {
338 let inspect_name = Arc::new(name_str);
339 let trace_name = lazy_static_cstr(&inspect_name)?;
340 state_names.insert(value, StateName { inspect_name, trace_name });
341 }
342
343 node.record_child("metadata", |metadata_node| {
344 metadata_node.record_string("name", &name);
345 metadata_node.record_string("type", "enum");
346 metadata_node.record_child("states", |states_node| {
347 for (state_value, state_name) in state_names.iter() {
348 states_node.record_uint(state_name.inspect_name.as_ref(), *state_value);
349 }
350 });
351 });
352
353 let names_map: HashMap<u64, Arc<String>> =
356 state_names.iter().map(|(k, v)| (*k, v.inspect_name.clone())).collect();
357 let names_map_arc = Arc::new(names_map);
358 let record_item = move |node: &inspect::Node, val: &u64| {
359 let name_str = names_map_arc.get(val).map(|s| s.as_str()).unwrap_or("<Unknown>");
360 node.record_string("value", name_str);
361 };
362
363 let (history, persistence) = setup_recording_backend(&node, &options, record_item)?;
364
365 let vthread = ftrace::VThread::new(name.clone(), ftrace::Id::random().into());
366
367 Ok(Self {
368 manager,
369 name,
370 trace_category,
371 state_names,
372 history,
373 persistence,
374 _root_node: node,
375 vthread,
376 current_state_trace_name: None,
377 })
378 }
379
380 fn get_state_name(&self, val: u64) -> StateName {
381 static UNKNOWN_NAME: LazyLock<StateName> = LazyLock::new(|| StateName {
382 trace_name: c"<Unknown>",
383 inspect_name: Arc::new("<Unknown>".to_string()),
384 });
385 self.state_names.get(&val).unwrap_or(&UNKNOWN_NAME).clone()
386 }
387
388 pub fn record(&mut self, val: u64) {
389 static CACHE: ftrace::trace_site_t = ftrace::trace_site_t::new(0);
390 let context = ftrace::TraceCategoryContext::acquire_cached(self.trace_category, &CACHE);
391
392 if let Some(context) = context.as_ref() {
393 if let Some(name) = self.current_state_trace_name {
394 ftrace::vthread_duration_end(context, &name, &self.vthread, &[]);
395 }
396 }
397
398 let StateName { inspect_name, trace_name } = self.get_state_name(val);
399 self.current_state_trace_name = Some(trace_name);
400
401 if let Some(context) = context.as_ref() {
402 ftrace::vthread_duration_begin(context, &trace_name, &self.vthread, &[]);
403 }
404
405 let timestamp = zx::BootInstant::get().into_nanos();
406
407 if let Some(handler) = &mut self.persistence {
409 handler.append(timestamp, val);
411 } else {
412 match &mut self.history {
414 RecorderHistory::Eager(history) => {
415 history.add_entry(|node| {
416 node.record_int("@time", timestamp);
417 node.record_string("value", inspect_name.as_ref());
418 });
419 }
420 RecorderHistory::Lazy(history) => {
421 history.lock().insert(timestamp, val);
422 }
423 }
424 }
425 }
426}
427
428pub struct EnumStateRecorder<T: RecordableEnum> {
432 inner: NamedU64StateRecorder,
433 _phantom: PhantomData<T>,
434}
435
436impl<T: RecordableEnum> std::fmt::Debug for EnumStateRecorder<T> {
437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438 f.debug_struct("EnumStateRecorder").field("inner", &self.inner).finish()
439 }
440}
441
442impl<T: RecordableEnum + 'static> EnumStateRecorder<T> {
443 pub fn new(
454 name: String,
455 trace_category: &'static CStr,
456 options: RecorderOptions,
457 ) -> Result<Self, StateRecorderError> {
458 let mut map = HashMap::new();
459 for variant in T::iter() {
460 map.insert(variant.into(), variant.to_string());
461 }
462 let inner = NamedU64StateRecorder::new(name, trace_category, map, options)?;
463
464 Ok(Self { inner, _phantom: PhantomData })
465 }
466
467 pub fn record(&mut self, state_enum: T) {
468 self.inner.record(state_enum.into());
469 }
470}
471
472pub trait RecordableNumericType:
476 Copy + Debug + Display + FromStr + Sized + Send + Sync + 'static
477{
478 type TraceType: ftrace::ArgValue;
479
480 fn trace_value(&self) -> Self::TraceType;
481 fn record(&self, node: &inspect::Node, name: &str);
482 fn record_range(range: &(Self, Self), node: &inspect::Node);
483}
484
485macro_rules! impl_recordable_numeric_type {
486 ($numeric_type:ty, $trace_type:ty, u64) => {
487 impl RecordableNumericType for $numeric_type {
488 type TraceType = $trace_type;
489
490 fn trace_value(&self) -> Self::TraceType {
491 *self as Self::TraceType
492 }
493 fn record(&self, node: &inspect::Node, name: &str) {
494 node.record_uint(name, *self as u64);
495 }
496 fn record_range(range: &(Self, Self), node: &inspect::Node) {
497 node.record_uint("min_inc", range.0 as u64);
498 node.record_uint("max_inc", range.1 as u64);
499 }
500 }
501 };
502 ($numeric_type:ty, $trace_type:ty, i64) => {
503 impl RecordableNumericType for $numeric_type {
504 type TraceType = $trace_type;
505
506 fn trace_value(&self) -> Self::TraceType {
507 *self as Self::TraceType
508 }
509 fn record(&self, node: &inspect::Node, name: &str) {
510 node.record_int(name, *self as i64);
511 }
512 fn record_range(range: &(Self, Self), node: &inspect::Node) {
513 node.record_int("min_inc", range.0 as i64);
514 node.record_int("max_inc", range.1 as i64);
515 }
516 }
517 };
518 ($numeric_type:ty, $trace_type:ty, f64) => {
519 impl RecordableNumericType for $numeric_type {
520 type TraceType = $trace_type;
521
522 fn trace_value(&self) -> Self::TraceType {
523 *self as Self::TraceType
524 }
525 fn record(&self, node: &inspect::Node, name: &str) {
526 node.record_double(name, *self as f64);
527 }
528 fn record_range(range: &(Self, Self), node: &inspect::Node) {
529 node.record_double("min_inc", range.0 as f64);
530 node.record_double("max_inc", range.1 as f64);
531 }
532 }
533 };
534}
535
536impl_recordable_numeric_type!(u8, u32, u64);
537impl_recordable_numeric_type!(u16, u32, u64);
538impl_recordable_numeric_type!(u32, u32, u64);
539impl_recordable_numeric_type!(u64, u64, u64);
540impl_recordable_numeric_type!(i8, i32, i64);
541impl_recordable_numeric_type!(i16, i32, i64);
542impl_recordable_numeric_type!(i32, i32, i64);
543impl_recordable_numeric_type!(i64, i64, i64);
544impl_recordable_numeric_type!(f32, f64, f64);
545impl_recordable_numeric_type!(f64, f64, f64);
546
547#[derive(Copy, Clone, Debug, PartialEq, Eq)]
553pub enum Units {
554 Amps(Option<DecimalPrefix>),
555 AmpHours(Option<DecimalPrefix>),
556 Hertz(Option<DecimalPrefix>),
557 Joules(Option<DecimalPrefix>),
558 Watts(Option<DecimalPrefix>),
559 Volts(Option<DecimalPrefix>),
560 Celsius(Option<DecimalPrefix>),
561 Number(Option<DecimalPrefix>),
562 Percent,
563}
564
565impl Display for Units {
566 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
567 fn write_helper(
568 f: &mut std::fmt::Formatter<'_>,
569 prefix: &Option<DecimalPrefix>,
570 unit_str: &str,
571 ) -> std::fmt::Result {
572 match prefix {
573 Some(p) => write!(f, "{}{}", p, unit_str),
574 None => write!(f, "{}", unit_str),
575 }
576 }
577
578 match self {
579 Units::Amps(prefix) => write_helper(f, prefix, "A"),
580 Units::AmpHours(prefix) => write_helper(f, prefix, "AH"),
581 Units::Hertz(prefix) => write_helper(f, prefix, "Hz"),
582 Units::Joules(prefix) => write_helper(f, prefix, "J"),
583 Units::Watts(prefix) => write_helper(f, prefix, "W"),
584 Units::Volts(prefix) => write_helper(f, prefix, "V"),
585 Units::Celsius(prefix) => write_helper(f, prefix, "C"),
586 Units::Number(prefix) => write_helper(f, prefix, "#"),
587 Units::Percent => write!(f, "%"),
588 }
589 }
590}
591
592#[derive(Copy, Clone, Debug, PartialEq, Eq)]
594pub enum DecimalPrefix {
595 Nano,
596 Micro,
597 Milli,
598 Centi,
599 Deci,
600 Kilo,
601 Mega,
602 Giga,
603}
604
605impl Display for DecimalPrefix {
606 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607 match self {
608 DecimalPrefix::Nano => write!(f, "n"),
609 DecimalPrefix::Micro => write!(f, "u"),
610 DecimalPrefix::Milli => write!(f, "m"),
611 DecimalPrefix::Centi => write!(f, "c"),
612 DecimalPrefix::Deci => write!(f, "d"),
613 DecimalPrefix::Kilo => write!(f, "k"),
614 DecimalPrefix::Mega => write!(f, "M"),
615 DecimalPrefix::Giga => write!(f, "G"),
616 }
617 }
618}
619
620#[macro_export]
629macro_rules! units {
630 (Percent) => {
631 $crate::Units::Percent
632 };
633 ($base_unit:ident) => {
634 $crate::Units::$base_unit(None)
635 };
636 ($prefix:ident, $base_unit:ident) => {
637 $crate::Units::$base_unit(Some($crate::DecimalPrefix::$prefix))
638 };
639}
640
641#[derive(Clone, Debug)]
643pub struct PersistenceOptions {
644 name: String,
646 current_path: String,
648 previous_path: String,
650 rename_path: String,
652}
653
654impl PersistenceOptions {
655 pub fn new(name: impl Into<String>) -> Self {
657 let name = name.into();
658 Self {
659 current_path: format!("/data/{}.csv", name),
660 previous_path: format!("/tmp/{}.csv", name),
661 rename_path: format!("/data/{}.tmp", name),
662 name,
663 }
664 }
665
666 pub fn storage_dir(mut self, dir: &str) -> Self {
667 self.current_path = format!("{}/{}.csv", dir, self.name);
668 self.rename_path = format!("{}/{}.tmp", dir, self.name);
669 self
670 }
671
672 pub fn volatile_dir(mut self, dir: &str) -> Self {
673 self.previous_path = format!("{}/{}.csv", dir, self.name);
674 self
675 }
676
677 fn paths(&self) -> (&str, &str, &str) {
679 (&self.current_path, &self.previous_path, &self.rename_path)
680 }
681}
682
683struct PersistenceHandler<T: Copy> {
685 config: PersistenceOptions,
686 buffer: Arc<Mutex<TimestampRingBuffer<T>>>,
688}
689
690impl<T: Copy + FromStr + Display> PersistenceHandler<T> {
691 fn new(
692 config: PersistenceOptions,
693 capacity: usize,
694 ) -> (Self, Arc<Mutex<TimestampRingBuffer<T>>>) {
695 let (curr, prev, _) = config.paths();
696
697 Self::prepare_files(&curr, &prev);
699
700 let initial_data = Self::read_log(&curr);
702
703 let mut buffer = TimestampRingBuffer::with_capacity(capacity);
705 for (ts, val) in initial_data {
706 buffer.insert(ts, val);
707 }
708
709 let shared_buffer = Arc::new(Mutex::new(buffer));
710
711 (Self { config, buffer: shared_buffer.clone() }, shared_buffer)
712 }
713
714 fn prepare_files(curr_path: &str, prev_path: &str) {
718 if Path::new(prev_path).exists() {
719 log::warn!("Not moving history, {} already exists", prev_path);
722 return;
723 }
724
725 let Ok(content) = std::fs::read_to_string(curr_path).map_err(|e| {
727 log::info!("Could not read current history, not moving: {}", e);
728 }) else {
729 return;
730 };
731
732 if let Err(e) = std::fs::write(prev_path, &content) {
733 log::warn!("Could not write previous boot history: {}", e);
734 return;
735 }
736
737 if let Err(e) = std::fs::File::create(curr_path) {
738 log::warn!("Could not clear current boot history: {}", e);
739 }
740 }
741
742 fn flush(&self, buffer_guard: &TimestampRingBuffer<T>) {
743 let (curr, _, temp) = self.config.paths();
744 let try_write = || -> std::io::Result<()> {
745 let mut file =
746 OpenOptions::new().write(true).create(true).truncate(true).open(&temp)?;
747
748 for (ts, val) in buffer_guard.iter() {
750 writeln!(file, "{},{}", ts, val)?;
751 }
752
753 file.sync_data()?;
754 fs::rename(&temp, &curr)?;
755 Ok(())
756 };
757
758 if let Err(e) = try_write() {
759 log::error!("StateRecorder: Persist failed for {}: {:?}", self.config.name, e);
760 }
761 }
762
763 fn append(&mut self, timestamp: i64, value: T) {
765 let mut guard = self.buffer.lock();
766 guard.insert(timestamp, value);
767 self.flush(&guard);
768 }
769
770 fn read_log(path: &str) -> Vec<(i64, T)> {
772 let Ok(content) = fs::read_to_string(path) else {
773 return Vec::new();
774 };
775 content
776 .lines()
777 .filter_map(|line| {
778 let line = line.trim();
779 let mut parts = line.splitn(2, ',');
780 let ts = parts.next()?.trim().parse::<i64>().ok()?;
781 let val = parts.next()?.trim().parse::<T>().ok()?;
782 Some((ts, val))
783 })
784 .collect()
785 }
786}
787
788#[derive(Default)]
790pub struct RecorderOptions {
791 pub lazy_record: bool,
793 pub capacity: usize,
795 pub manager: Option<Arc<Mutex<StateRecorderManager>>>,
799 pub persistence: Option<PersistenceOptions>,
801}
802
803#[derive(Debug)]
804enum RecorderHistory<T: Copy + Debug> {
805 Eager(BoundedListNode),
806 Lazy(Arc<Mutex<TimestampRingBuffer<T>>>),
807}
808
809#[derive(Debug)]
810struct TimestampRingBuffer<T: Copy> {
818 start_timestamp_ms: i64,
820 last_timestamp_ms: i64,
822 next_index: usize,
824 offset_ms: Vec<i32>,
826 data: Vec<T>,
828 reset_count: u32,
830 last_reset_ms: i64,
832}
833
834const NANOSECONDS_PER_MILLISECOND: i64 = 1_000_000;
835
836fn ms_to_ns(ms: i64) -> i64 {
837 ms * NANOSECONDS_PER_MILLISECOND
838}
839
840fn ns_to_ms(ns: i64) -> i64 {
841 ns / NANOSECONDS_PER_MILLISECOND
842}
843
844impl<T: Copy> TimestampRingBuffer<T> {
845 fn with_capacity(capacity: usize) -> Self {
846 let now_ms = ns_to_ms(zx::BootInstant::get().into_nanos());
847 Self {
848 start_timestamp_ms: now_ms,
849 last_timestamp_ms: now_ms,
850 next_index: 0,
851 offset_ms: Vec::with_capacity(capacity),
852 data: Vec::with_capacity(capacity),
853 reset_count: 0,
854 last_reset_ms: now_ms,
855 }
856 }
857
858 fn insert(&mut self, timestamp_ns: i64, value: T) {
859 let timestamp_ms = ns_to_ms(timestamp_ns);
860 let offset_ms = match i32::try_from(timestamp_ms - self.last_timestamp_ms) {
862 Ok(offset_ms) => offset_ms,
863 Err(_) => {
864 self.offset_ms.clear();
867 self.data.clear();
868 self.start_timestamp_ms = timestamp_ms;
869 self.next_index = 0;
870 self.reset_count += 1;
871 self.last_reset_ms = self.start_timestamp_ms;
872 0
873 }
874 };
875 if self.offset_ms.len() < self.offset_ms.capacity() {
876 self.offset_ms.push(offset_ms);
878 self.data.push(value);
879 } else {
880 self.start_timestamp_ms += self.offset_ms[self.next_index] as i64;
883 self.offset_ms[self.next_index] = offset_ms;
884 self.data[self.next_index] = value;
885 }
886 self.last_timestamp_ms = timestamp_ms;
887 self.next_index = (self.next_index + 1) % self.offset_ms.capacity();
888 }
889
890 fn get_reset_info(&self) -> (u32, i64) {
892 (self.reset_count, ms_to_ns(self.last_reset_ms))
893 }
894
895 fn iter(&self) -> TimestampRingBufferIter<'_, T> {
898 TimestampRingBufferIter::new(self)
899 }
900}
901
902struct TimestampRingBufferIter<'a, T: Copy> {
903 buffer: &'a TimestampRingBuffer<T>,
904 index: usize,
905 last_timestamp_ms: i64,
906}
907
908impl<'a, T: Copy> TimestampRingBufferIter<'a, T> {
909 fn new(buffer: &'a TimestampRingBuffer<T>) -> Self {
910 Self { buffer, index: 0, last_timestamp_ms: buffer.start_timestamp_ms }
911 }
912}
913
914impl<T: Copy> Iterator for TimestampRingBufferIter<'_, T> {
917 type Item = (i64, T);
918
919 fn next(&mut self) -> Option<(i64, T)> {
920 if self.index >= self.buffer.offset_ms.len() {
921 return None;
922 }
923 let index = (self.index + self.buffer.next_index) % self.buffer.offset_ms.len();
925 let timestamp_ms = self.last_timestamp_ms + self.buffer.offset_ms[index] as i64;
926 self.index += 1;
927 self.last_timestamp_ms = timestamp_ms;
928 Some((ms_to_ns(timestamp_ms), self.buffer.data[index]))
929 }
930}
931
932pub struct NumericStateRecorder<T: RecordableNumericType> {
933 manager: Arc<Mutex<StateRecorderManager>>,
934 name: String,
935 trace_category: &'static CStr,
936 trace_name: &'static CStr,
937 units: String,
938 history: RecorderHistory<T>,
939 persistence: Option<PersistenceHandler<T>>,
940 _root_node: inspect::Node,
941 trace_id: ftrace::Id,
942 _phantom: PhantomData<T>,
943}
944
945impl<T: RecordableNumericType> NumericStateRecorder<T> {
946 pub fn new(
957 name: String,
958 trace_category: &'static CStr,
959 units: Units,
960 range: Option<(T, T)>,
961 options: RecorderOptions,
962 ) -> Result<Self, StateRecorderError> {
963 let manager = options.manager.clone().unwrap_or_else(|| SINGLETON_MANAGER.clone());
964 let node = register_with_manager(&manager, &name)?;
965
966 let trace_name = lazy_static_cstr(&name)?;
967 let units_str = format!("{}", units);
968
969 node.record_child("metadata", |metadata_node| {
970 metadata_node.record_string("name", &name);
971 metadata_node.record_string("type", "numeric");
972 metadata_node.record_string("units", &units_str);
973 match range {
974 Some(r) => metadata_node.record_child("range", |node| T::record_range(&r, node)),
975 None => metadata_node.record_string("range", "<Unspecified>"),
976 }
977 });
978
979 let record_item = |node: &inspect::Node, val: &T| {
980 val.record(node, "value");
981 };
982
983 let (history, persistence) = setup_recording_backend(&node, &options, record_item)?;
984
985 Ok(Self {
986 manager,
987 name,
988 trace_category,
989 trace_name,
990 units: units_str,
991 history,
992 persistence,
993 _root_node: node,
994 trace_id: ftrace::Id::random(),
995 _phantom: PhantomData,
996 })
997 }
998
999 pub fn record(&mut self, state_value: T) {
1000 let timestamp = zx::BootInstant::get().into_nanos();
1001
1002 ftrace::counter!(
1003 self.trace_category,
1004 self.trace_name,
1005 self.trace_id.into(),
1006 &self.units.to_string() => state_value.trace_value()
1007 );
1008
1009 if let Some(handler) = &mut self.persistence {
1011 handler.append(timestamp, state_value);
1012 } else {
1013 match &mut self.history {
1014 RecorderHistory::Eager(history) => {
1015 history.add_entry(|node| {
1016 node.record_int("@time", timestamp);
1017 state_value.record(node, "value");
1018 });
1019 }
1020 RecorderHistory::Lazy(history) => {
1021 history.lock().insert(timestamp, state_value);
1022 }
1023 }
1024 }
1025 }
1026}
1027
1028impl<T: RecordableNumericType> Drop for NumericStateRecorder<T> {
1029 fn drop(&mut self) {
1030 self.manager.lock().unregister_name(&self.name);
1031 }
1032}
1033
1034#[cfg(test)]
1035mod tests {
1036 use super::*;
1037 use diagnostics_assertions::{AnyIntProperty, assert_data_tree};
1038 use fuchsia_inspect::Inspector;
1039 use strum_macros::{Display, EnumIter, EnumString};
1040 use test_case::test_case;
1041
1042 #[derive(Copy, Clone, Debug, Display, EnumIter, EnumString, Eq, PartialEq, Hash)]
1043 #[repr(u8)]
1044 enum SwitchState {
1045 OFF = 0,
1046 ON = 1,
1047 }
1048
1049 impl From<SwitchState> for u64 {
1050 fn from(value: SwitchState) -> Self {
1051 value as Self
1052 }
1053 }
1054
1055 #[fuchsia::test]
1056 async fn test_timestamp_ring_buffer() {
1057 let mut buffer = TimestampRingBuffer::<i32>::with_capacity(3);
1058 let start_ms = buffer.start_timestamp_ms;
1059
1060 let t1 = (ms_to_ns(start_ms + 1000), 1);
1061 let t2 = (ms_to_ns(start_ms + 900), 2);
1063 let t3 = (ms_to_ns(start_ms + 3000), 3);
1064
1065 buffer.insert(t1.0, t1.1);
1066 buffer.insert(t2.0, t2.1);
1067 buffer.insert(t3.0, t3.1);
1068
1069 assert_eq!(vec![t1, t2, t3], buffer.iter().collect::<Vec<_>>());
1070 assert_eq!((0, ms_to_ns(start_ms)), buffer.get_reset_info());
1071
1072 let t4 = (ms_to_ns(start_ms + 4000), 4);
1074 buffer.insert(t4.0, t4.1);
1075 assert_eq!(vec![t2, t3, t4], buffer.iter().collect::<Vec<_>>());
1076 assert_eq!((0, ms_to_ns(start_ms)), buffer.get_reset_info());
1077 }
1078
1079 #[fuchsia::test]
1080 async fn test_timestamp_ring_buffer_resets_on_maximum_offset() {
1081 let mut buffer = TimestampRingBuffer::<i32>::with_capacity(3);
1082 let start_ms = buffer.start_timestamp_ms;
1083
1084 const MAX_OFFSET_MS: i64 = i32::MAX as i64;
1085 let t1 = (ms_to_ns(start_ms + 1000), 1);
1086 let t2 = (t1.0 + ms_to_ns(MAX_OFFSET_MS), 2);
1087
1088 buffer.insert(t1.0, t1.1);
1089 buffer.insert(t2.0, t2.1);
1090
1091 assert_eq!(vec![t1, t2], buffer.iter().collect::<Vec<_>>());
1092 assert_eq!((0, ms_to_ns(start_ms)), buffer.get_reset_info());
1093
1094 let t3 = (t2.0 + ms_to_ns(MAX_OFFSET_MS + 1), 3);
1097 buffer.insert(t3.0, t3.1);
1098 assert_eq!(vec![t3], buffer.iter().collect::<Vec<_>>());
1099 assert_eq!((1, t3.0), buffer.get_reset_info());
1100 }
1101
1102 #[test_case(false; "eager")]
1103 #[test_case(true; "lazy")]
1104 #[fuchsia::test]
1105 async fn test_enum_off_on(lazy_record: bool) {
1106 let inspector = Inspector::default();
1107 let manager = StateRecorderManager::new(&inspector);
1108
1109 let mut recorder = EnumStateRecorder::new(
1110 "my_switch".into(),
1111 c"power_test",
1112 RecorderOptions {
1113 lazy_record,
1114 capacity: 10,
1115 manager: Some(manager),
1116 persistence: None,
1117 },
1118 )
1119 .unwrap();
1120
1121 recorder.record(SwitchState::OFF);
1122 recorder.record(SwitchState::ON);
1123 recorder.record(SwitchState::OFF);
1124 recorder.record(SwitchState::ON);
1125 assert_data_tree!(inspector, root: {
1126 power_observability_state_recorders: {
1127 my_switch: {
1128 metadata: {
1129 name: "my_switch",
1130 type: "enum",
1131 states: {
1132 "OFF": 0u64,
1133 "ON": 1u64,
1134 }
1135 },
1136 history: {
1137 "0": {
1138 "@time": AnyIntProperty,
1139 "value": "OFF",
1140 },
1141 "1": {
1142 "@time": AnyIntProperty,
1143 "value": "ON",
1144 },
1145 "2": {
1146 "@time": AnyIntProperty,
1147 "value": "OFF",
1148 },
1149 "3": {
1150 "@time": AnyIntProperty,
1151 "value": "ON",
1152 },
1153 },
1154 reset_info: {
1155 count: 0,
1156 last_reset_ns: AnyIntProperty,
1157 }
1158 }
1159 }
1160 });
1161 }
1162
1163 #[test_case(false; "eager")]
1164 #[test_case(true; "lazy")]
1165 #[fuchsia::test]
1166 async fn test_multiple_recorders(lazy_record: bool) {
1167 #[derive(Copy, Clone, Debug, Display, EnumIter, EnumString, Eq, PartialEq, Hash)]
1168 #[repr(u8)]
1169 enum EnablementState {
1170 DISABLED = 0,
1171 ENABLED = 1,
1172 }
1173 impl From<EnablementState> for u64 {
1174 fn from(value: EnablementState) -> Self {
1175 value as Self
1176 }
1177 }
1178
1179 let inspector = Inspector::default();
1180 let manager = StateRecorderManager::new(&inspector);
1181
1182 let mut recorder_0 = EnumStateRecorder::new(
1183 "switch_0".into(),
1184 c"power_test",
1185 RecorderOptions {
1186 lazy_record,
1187 capacity: 10,
1188 manager: Some(manager.clone()),
1189 persistence: None,
1190 },
1191 )
1192 .unwrap();
1193 let mut recorder_1 = EnumStateRecorder::new(
1194 "switch_1".into(),
1195 c"power_test",
1196 RecorderOptions {
1197 lazy_record,
1198 capacity: 10,
1199 manager: Some(manager),
1200 persistence: None,
1201 },
1202 )
1203 .unwrap();
1204 recorder_0.record(SwitchState::OFF);
1205 recorder_0.record(SwitchState::ON);
1206 recorder_1.record(EnablementState::ENABLED);
1207 recorder_1.record(EnablementState::DISABLED);
1208
1209 assert_data_tree!(inspector, root: {
1210 power_observability_state_recorders: {
1211 switch_0: {
1212 metadata: {
1213 name: "switch_0",
1214 type: "enum",
1215 states: {
1216 "OFF": 0u64,
1217 "ON": 1u64,
1218 }
1219 },
1220 history: {
1221 "0": {
1222 "@time": AnyIntProperty,
1223 "value": "OFF",
1224 },
1225 "1": {
1226 "@time": AnyIntProperty,
1227 "value": "ON",
1228 },
1229 },
1230 reset_info: {
1231 count: 0,
1232 last_reset_ns: AnyIntProperty,
1233 }
1234 },
1235 switch_1: {
1236 metadata: {
1237 name: "switch_1",
1238 type: "enum",
1239 states: {
1240 "DISABLED": 0u64,
1241 "ENABLED": 1u64,
1242 }
1243 },
1244 history: {
1245 "0": {
1246 "@time": AnyIntProperty,
1247 "value": "ENABLED",
1248 },
1249 "1": {
1250 "@time": AnyIntProperty,
1251 "value": "DISABLED",
1252 },
1253 },
1254 reset_info: {
1255 count: 0,
1256 last_reset_ns: AnyIntProperty,
1257 }
1258 }
1259 }
1260 })
1261 }
1262
1263 #[test_case(false; "eager")]
1264 #[test_case(true; "lazy")]
1265 #[fuchsia::test]
1266 async fn test_enum_three_states(lazy_record: bool) {
1267 #[derive(Copy, Clone, Debug, Display, EnumIter, EnumString, Eq, PartialEq, Hash)]
1268 #[repr(u8)]
1269 enum FanSpeed {
1270 OFF = 0,
1271 LOW = 1,
1272 HIGH = 2,
1273 }
1274
1275 impl From<FanSpeed> for u64 {
1276 fn from(value: FanSpeed) -> Self {
1277 value as Self
1278 }
1279 }
1280
1281 let inspector = Inspector::default();
1282 let manager = StateRecorderManager::new(&inspector);
1283
1284 let mut recorder = EnumStateRecorder::new(
1285 "the_best_fan".into(),
1286 c"power_test",
1287 RecorderOptions {
1288 lazy_record,
1289 capacity: 10,
1290 manager: Some(manager),
1291 persistence: None,
1292 },
1293 )
1294 .unwrap();
1295
1296 recorder.record(FanSpeed::OFF);
1297 recorder.record(FanSpeed::LOW);
1298 recorder.record(FanSpeed::HIGH);
1299 recorder.record(FanSpeed::OFF);
1300 recorder.record(FanSpeed::HIGH);
1301 assert_data_tree!(inspector, root: {
1302 power_observability_state_recorders: {
1303 the_best_fan: {
1304 metadata: {
1305 name: "the_best_fan",
1306 type: "enum",
1307 states: {
1308 "OFF": 0u64,
1309 "LOW": 1u64,
1310 "HIGH": 2u64,
1311 }
1312 },
1313 history: {
1314 "0": {
1315 "@time": AnyIntProperty,
1316 "value": "OFF",
1317 },
1318 "1": {
1319 "@time": AnyIntProperty,
1320 "value": "LOW",
1321 },
1322 "2": {
1323 "@time": AnyIntProperty,
1324 "value": "HIGH",
1325 },
1326 "3": {
1327 "@time": AnyIntProperty,
1328 "value": "OFF",
1329 },
1330 "4": {
1331 "@time": AnyIntProperty,
1332 "value": "HIGH",
1333 },
1334 },
1335 reset_info: {
1336 count: 0,
1337 last_reset_ns: AnyIntProperty,
1338 }
1339 }
1340 }
1341 });
1342 }
1343
1344 #[test_case(false; "eager")]
1345 #[test_case(true; "lazy")]
1346 #[fuchsia::test]
1347 async fn test_name_reuse_not_allowed(lazy_record: bool) {
1348 let inspector = Inspector::default();
1349 let manager = StateRecorderManager::new(&inspector);
1350
1351 let recorder = EnumStateRecorder::<SwitchState>::new(
1352 "my_switch".into(),
1353 c"power_test",
1354 RecorderOptions {
1355 lazy_record,
1356 capacity: 10,
1357 manager: Some(manager.clone()),
1358 persistence: None,
1359 },
1360 )
1361 .unwrap();
1362
1363 let result = EnumStateRecorder::<SwitchState>::new(
1365 "my_switch".into(),
1366 c"power_test",
1367 RecorderOptions {
1368 lazy_record,
1369 capacity: 10,
1370 manager: Some(manager.clone()),
1371 persistence: None,
1372 },
1373 );
1374 assert!(result.is_err());
1375
1376 drop(recorder);
1378 let result = EnumStateRecorder::<SwitchState>::new(
1379 "my_switch".into(),
1380 c"power_test",
1381 RecorderOptions {
1382 lazy_record,
1383 capacity: 10,
1384 manager: Some(manager.clone()),
1385 persistence: None,
1386 },
1387 );
1388 assert!(result.is_ok());
1389 }
1390
1391 #[test_case(false; "eager")]
1392 #[test_case(true; "lazy")]
1393 #[fuchsia::test]
1394 async fn test_singleton_manager(lazy_record: bool) {
1395 let mut recorder = EnumStateRecorder::new(
1396 "my_switch".into(),
1397 c"power_test",
1398 RecorderOptions { lazy_record, capacity: 10, manager: None, persistence: None },
1399 )
1400 .unwrap();
1401
1402 recorder.record(SwitchState::OFF);
1403 recorder.record(SwitchState::ON);
1404 assert_data_tree!(inspect::component::inspector(), root: {
1405 power_observability_state_recorders: {
1406 my_switch: {
1407 metadata: {
1408 name: "my_switch",
1409 type: "enum",
1410 states: {
1411 "OFF": 0u64,
1412 "ON": 1u64,
1413 }
1414 },
1415 history: {
1416 "0": {
1417 "@time": AnyIntProperty,
1418 "value": "OFF",
1419 },
1420 "1": {
1421 "@time": AnyIntProperty,
1422 "value": "ON",
1423 },
1424 },
1425 reset_info: {
1426 count: 0,
1427 last_reset_ns: AnyIntProperty,
1428 }
1429 }
1430 }
1431 });
1432 }
1433
1434 #[fuchsia::test]
1435 async fn test_recorder_is_send() {
1436 fn assert_send<T: Send>() {}
1437 assert_send::<EnumStateRecorder<SwitchState>>();
1438 }
1439
1440 async fn test_uint_numeric_type<T: RecordableNumericType>(lazy_record: bool)
1441 where
1442 T: Into<u64> + From<u8>,
1443 {
1444 let inspector = Inspector::default();
1445 let manager = StateRecorderManager::new(&inspector);
1446 let mut recorder = NumericStateRecorder::new(
1447 "my_stateful_thing".into(),
1448 c"power_test",
1449 units!(Percent),
1450 Some((T::from(0), T::from(255))),
1451 RecorderOptions {
1452 lazy_record,
1453 capacity: 10,
1454 manager: Some(manager),
1455 persistence: None,
1456 },
1457 )
1458 .unwrap();
1459
1460 recorder.record(T::from(10));
1461 recorder.record(T::from(0));
1462 assert_data_tree!(inspector, root: {
1463 power_observability_state_recorders: {
1464 my_stateful_thing: {
1465 metadata: {
1466 name: "my_stateful_thing",
1467 type: "numeric",
1468 units: "%",
1469 range: {
1470 min_inc: 0u64,
1471 max_inc: 255u64
1472 },
1473 },
1474 history: {
1475 "0": {
1476 "@time": AnyIntProperty,
1477 "value": 10u64,
1478 },
1479 "1": {
1480 "@time": AnyIntProperty,
1481 "value": 0u64,
1482 },
1483 },
1484 reset_info: {
1485 count: 0,
1486 last_reset_ns: AnyIntProperty,
1487 }
1488 }
1489 }
1490 });
1491 }
1492
1493 #[test_case(false; "eager")]
1494 #[test_case(true; "lazy")]
1495 #[fuchsia::test]
1496 async fn test_uint_numeric_types(lazy_record: bool) {
1497 test_uint_numeric_type::<u8>(lazy_record).await;
1498 test_uint_numeric_type::<u16>(lazy_record).await;
1499 test_uint_numeric_type::<u32>(lazy_record).await;
1500 test_uint_numeric_type::<u64>(lazy_record).await;
1501 }
1502
1503 async fn test_int_numeric_type<T: RecordableNumericType>(lazy_record: bool)
1504 where
1505 T: Into<i64> + From<i8>,
1506 {
1507 let inspector = Inspector::default();
1508 let manager = StateRecorderManager::new(&inspector);
1509 let mut recorder = NumericStateRecorder::new(
1510 "my_stateful_thing".into(),
1511 c"power_test",
1512 units!(Number),
1513 Some((T::from(-128), T::from(127))),
1514 RecorderOptions {
1515 lazy_record,
1516 capacity: 10,
1517 manager: Some(manager),
1518 persistence: None,
1519 },
1520 )
1521 .unwrap();
1522
1523 recorder.record(T::from(10));
1524 recorder.record(T::from(0));
1525 assert_data_tree!(inspector, root: {
1526 power_observability_state_recorders: {
1527 my_stateful_thing: {
1528 metadata: {
1529 name: "my_stateful_thing",
1530 type: "numeric",
1531 units: "#",
1532 range: {
1533 min_inc: -128i64,
1534 max_inc: 127i64
1535 },
1536 },
1537 history: {
1538 "0": {
1539 "@time": AnyIntProperty,
1540 "value": 10i64,
1541 },
1542 "1": {
1543 "@time": AnyIntProperty,
1544 "value": 0i64,
1545 },
1546 },
1547 reset_info: {
1548 count: 0,
1549 last_reset_ns: AnyIntProperty,
1550 }
1551 }
1552 }
1553 });
1554 }
1555
1556 #[test_case(false; "eager")]
1557 #[test_case(true; "lazy")]
1558 #[fuchsia::test]
1559 async fn test_int_numeric_types(lazy_record: bool) {
1560 test_int_numeric_type::<i8>(lazy_record).await;
1561 test_int_numeric_type::<i16>(lazy_record).await;
1562 test_int_numeric_type::<i32>(lazy_record).await;
1563 test_int_numeric_type::<i64>(lazy_record).await;
1564 }
1565
1566 async fn test_float_numeric_type<T: RecordableNumericType>(lazy_record: bool)
1567 where
1568 T: Into<f64> + From<u8>,
1569 {
1570 let inspector = Inspector::default();
1571 let manager = StateRecorderManager::new(&inspector);
1572 let mut recorder = NumericStateRecorder::new(
1573 "my_stateful_thing".into(),
1574 c"power_test",
1575 units!(Kilo, Hertz),
1576 Some((T::from(0), T::from(255))),
1577 RecorderOptions {
1578 lazy_record,
1579 capacity: 10,
1580 manager: Some(manager),
1581 persistence: None,
1582 },
1583 )
1584 .unwrap();
1585
1586 recorder.record(T::from(10));
1587 recorder.record(T::from(0));
1588 assert_data_tree!(inspector, root: {
1589 power_observability_state_recorders: {
1590 my_stateful_thing: {
1591 metadata: {
1592 name: "my_stateful_thing",
1593 type: "numeric",
1594 units: "kHz",
1595 range: {
1596 min_inc: 0.0,
1597 max_inc: 255.0
1598 },
1599 },
1600 history: {
1601 "0": {
1602 "@time": AnyIntProperty,
1603 "value": 10.0,
1604 },
1605 "1": {
1606 "@time": AnyIntProperty,
1607 "value": 0.0,
1608 },
1609 },
1610 reset_info: {
1611 count: 0,
1612 last_reset_ns: AnyIntProperty,
1613 }
1614 }
1615 }
1616 });
1617 }
1618
1619 #[test_case(false; "eager")]
1620 #[test_case(true; "lazy")]
1621 #[fuchsia::test]
1622 async fn test_float_numeric_types(lazy_record: bool) {
1623 test_float_numeric_type::<f32>(lazy_record).await;
1624 test_float_numeric_type::<f64>(lazy_record).await;
1625 }
1626
1627 #[test_case(true; "lazy")]
1628 #[fuchsia::test]
1629 async fn test_persistence_crash_recovery(lazy_record: bool) {
1630 use std::fs;
1631 use tempfile::tempdir;
1632
1633 let dir = tempdir().unwrap();
1635 let storage_path = dir.path().join("data");
1636 let volatile_path = dir.path().join("tmp");
1637 fs::create_dir(&storage_path).unwrap();
1638 fs::create_dir(&volatile_path).unwrap();
1639
1640 let inspector = Inspector::default();
1641 let manager = StateRecorderManager::new(&inspector);
1642
1643 let create_options = |manager_ref| RecorderOptions {
1645 lazy_record, capacity: 10,
1647 manager: Some(manager_ref),
1648 persistence: Some(
1649 PersistenceOptions::new("crash_test".to_string())
1650 .storage_dir(storage_path.to_str().unwrap())
1651 .volatile_dir(volatile_path.to_str().unwrap()),
1652 ),
1653 };
1654
1655 {
1657 let mut recorder = EnumStateRecorder::<SwitchState>::new(
1658 "crash_test".into(),
1659 c"power_test",
1660 create_options(manager.clone()),
1661 )
1662 .unwrap();
1663
1664 recorder.record(SwitchState::ON);
1665 recorder.record(SwitchState::OFF);
1666
1667 }
1669
1670 let curr_csv = storage_path.join("crash_test.csv");
1672 let content = fs::read_to_string(curr_csv).unwrap();
1673 let lines: Vec<&str> = content.trim().lines().collect();
1675 assert_eq!(lines.len(), 2, "Expected 2 lines of recorded history");
1676
1677 let parts0: Vec<&str> = lines[0].split(',').collect();
1679 assert_eq!(parts0.len(), 2, "Invalid CSV format in line 1");
1680 assert_eq!(parts0[1], "1", "First record should be ON (1)");
1681
1682 let parts1: Vec<&str> = lines[1].split(',').collect();
1684 assert_eq!(parts1.len(), 2, "Invalid CSV format in line 2");
1685 assert_eq!(parts1[1], "0", "Second record should be OFF (0)");
1686
1687 let prev_csv = volatile_path.join("crash_test.csv");
1691 fs::write(&prev_csv, "").unwrap();
1692
1693 let mut recorder_restarted = EnumStateRecorder::<SwitchState>::new(
1696 "crash_test".into(),
1697 c"power_test",
1698 create_options(manager),
1699 )
1700 .unwrap();
1701
1702 assert_data_tree!(inspector, root: {
1704 power_observability_state_recorders: {
1705 crash_test: {
1706 metadata: {
1707 name: "crash_test",
1708 type: "enum",
1709 states: {
1710 "OFF": 0u64,
1711 "ON": 1u64,
1712 }
1713 },
1714 history: {
1715 "0": {
1716 "@time": AnyIntProperty,
1717 "value": "ON",
1718 },
1719 "1": {
1720 "@time": AnyIntProperty,
1721 "value": "OFF",
1722 },
1723 },
1724 reset_info: {
1725 count: 0i64, last_reset_ns: AnyIntProperty,
1727 },
1728 }
1729 }
1730 });
1731
1732 recorder_restarted.record(SwitchState::ON);
1734 assert_data_tree!(inspector, root: {
1735 power_observability_state_recorders: {
1736 crash_test: {
1737 metadata: {
1738 name: "crash_test",
1739 type: "enum",
1740 states: {
1741 "OFF": 0u64,
1742 "ON": 1u64,
1743 }
1744 },
1745 history: {
1746 "0": {
1747 "@time": AnyIntProperty,
1748 "value": "ON",
1749 },
1750 "1": {
1751 "@time": AnyIntProperty,
1752 "value": "OFF",
1753 },
1754 "2": {
1755 "@time": AnyIntProperty,
1756 "value": "ON",
1757 },
1758 },
1759 reset_info: {
1760 count: 0i64,
1761 last_reset_ns: AnyIntProperty,
1762 },
1763 }
1764 }
1765 });
1766 }
1767
1768 #[test_case(true; "lazy")]
1769 #[fuchsia::test]
1770 async fn test_persistence_reboot(lazy_record: bool) {
1771 use std::fs;
1772 use tempfile::tempdir;
1773
1774 let dir = tempdir().unwrap();
1776 let storage_path = dir.path().join("data");
1777 let volatile_path = dir.path().join("tmp");
1778 fs::create_dir(&storage_path).unwrap();
1779 fs::create_dir(&volatile_path).unwrap();
1780
1781 let inspector = Inspector::default();
1782 let manager = StateRecorderManager::new(&inspector);
1783
1784 let create_options = |manager_ref| RecorderOptions {
1786 lazy_record,
1787 capacity: 10,
1788 manager: Some(manager_ref),
1789 persistence: Some(
1790 PersistenceOptions::new("reboot_test".to_string())
1791 .storage_dir(storage_path.to_str().unwrap())
1792 .volatile_dir(volatile_path.to_str().unwrap()),
1793 ),
1794 };
1795
1796 let curr_csv = storage_path.join("reboot_test.csv");
1800 fs::write(&curr_csv, "1000,1\n2000,0\n").unwrap();
1802
1803 let prev_csv = volatile_path.join("reboot_test.csv");
1805 assert!(!prev_csv.exists());
1806
1807 let mut recorder = EnumStateRecorder::<SwitchState>::new(
1809 "reboot_test".into(),
1810 c"power_test",
1811 create_options(manager),
1812 )
1813 .unwrap();
1814
1815 assert!(prev_csv.exists(), "Library should have rotated curr -> prev");
1818 assert!(curr_csv.exists(), "Library should have create a new current file");
1819
1820 let rotated_content = fs::read_to_string(&prev_csv).unwrap();
1821 assert_eq!(rotated_content, "1000,1\n2000,0\n");
1822
1823 assert_data_tree!(inspector, root: {
1825 power_observability_state_recorders: {
1826 reboot_test: {
1827 metadata: {
1828 name: "reboot_test",
1829 type: "enum",
1830 states: {
1831 "OFF": 0u64,
1832 "ON": 1u64,
1833 }
1834 },
1835 previous_boot_history: {
1837 "0": {
1838 "@time": 1000i64,
1839 "value": "ON",
1840 },
1841 "1": {
1842 "@time": 2000i64,
1843 "value": "OFF",
1844 },
1845 },
1846 history: {},
1848 reset_info: {
1849 count: 0i64,
1850 last_reset_ns: AnyIntProperty,
1851 },
1852 }
1853 }
1854 });
1855
1856 recorder.record(SwitchState::ON);
1858 recorder.record(SwitchState::OFF);
1859 assert_data_tree!(inspector, root: {
1860 power_observability_state_recorders: {
1861 reboot_test: {
1862 metadata: {
1863 name: "reboot_test",
1864 type: "enum",
1865 states: {
1866 "OFF": 0u64,
1867 "ON": 1u64,
1868 }
1869 },
1870 previous_boot_history: {
1872 "0": {
1873 "@time": 1000i64,
1874 "value": "ON",
1875 },
1876 "1": {
1877 "@time": 2000i64,
1878 "value": "OFF",
1879 },
1880 },
1881 history: {
1883 "0": {
1884 "@time": AnyIntProperty, "value": "ON",
1886 },
1887 "1": {
1888 "@time": AnyIntProperty, "value": "OFF",
1890 },
1891 },
1892 reset_info: {
1893 count: 0i64,
1894 last_reset_ns: AnyIntProperty,
1895 },
1896 }
1897 }
1898 });
1899 }
1900
1901 #[test_case(false; "eager")]
1902 #[test_case(true; "lazy")]
1903 #[fuchsia::test]
1904 async fn test_named_u64_recorder(lazy_record: bool) {
1905 use std::fs;
1906 use tempfile::tempdir;
1907
1908 let dir = tempdir().unwrap();
1910 let storage_path = dir.path().join("data");
1911 let volatile_path = dir.path().join("tmp");
1912 fs::create_dir(&storage_path).unwrap();
1913 fs::create_dir(&volatile_path).unwrap();
1914
1915 let inspector = Inspector::default();
1916 let manager = StateRecorderManager::new(&inspector);
1917
1918 let mut map = HashMap::new();
1919 map.insert(100, "Hundred".to_string());
1920 map.insert(200, "TwoHundred".to_string());
1921
1922 let persistence_opts = if lazy_record {
1923 Some(
1924 PersistenceOptions::new("my_u64_metrics_p".to_string())
1925 .storage_dir(storage_path.to_str().unwrap())
1926 .volatile_dir(volatile_path.to_str().unwrap()),
1927 )
1928 } else {
1929 None
1930 };
1931
1932 let mut recorder = NamedU64StateRecorder::new(
1934 "my_u64_metrics_p".into(),
1935 c"power_test",
1936 map.clone(),
1937 RecorderOptions {
1938 lazy_record,
1939 capacity: 10,
1940 manager: Some(manager.clone()),
1941 persistence: persistence_opts.clone(),
1942 },
1943 )
1944 .unwrap();
1945
1946 recorder.record(100);
1947 recorder.record(200);
1948 recorder.record(300); if lazy_record {
1952 drop(recorder); let curr_csv = storage_path.join("my_u64_metrics_p.csv");
1955 let content = fs::read_to_string(&curr_csv).unwrap();
1956 let lines: Vec<&str> = content.trim().lines().collect();
1957 assert_eq!(lines.len(), 3, "Expected 3 lines of recorded history");
1958
1959 let parts0: Vec<&str> = lines[0].split(',').collect();
1961 assert_eq!(parts0.len(), 2, "Invalid CSV format in line 1");
1962 assert_eq!(parts0[1], "100", "First record should be 100");
1963
1964 let parts1: Vec<&str> = lines[1].split(',').collect();
1966 assert_eq!(parts1.len(), 2, "Invalid CSV format in line 2");
1967 assert_eq!(parts1[1], "200", "Second record should be 200");
1968
1969 let parts2: Vec<&str> = lines[2].split(',').collect();
1971 assert_eq!(parts2.len(), 2, "Invalid CSV format in line 3");
1972 assert_eq!(parts2[1], "300", "Third record should be 300");
1973
1974 let mut _recorder_restarted = NamedU64StateRecorder::new(
1976 "my_u64_metrics_p".into(),
1977 c"power_test",
1978 map,
1979 RecorderOptions {
1980 lazy_record,
1981 capacity: 10,
1982 manager: Some(manager),
1983 persistence: persistence_opts,
1984 },
1985 )
1986 .unwrap();
1987
1988 assert_data_tree!(inspector, root: {
1989 power_observability_state_recorders: {
1990 my_u64_metrics_p: {
1991 metadata: {
1992 name: "my_u64_metrics_p",
1993 type: "enum",
1994 states: {
1995 "Hundred": 100u64,
1996 "TwoHundred": 200u64,
1997 }
1998 },
1999 previous_boot_history: {
2000 "0": {
2001 "@time": AnyIntProperty,
2002 "value": "Hundred",
2003 },
2004 "1": {
2005 "@time": AnyIntProperty,
2006 "value": "TwoHundred",
2007 },
2008 "2": {
2009 "@time": AnyIntProperty,
2010 "value": "<Unknown>",
2011 },
2012 },
2013 history: {},
2014 reset_info: {
2015 count: 0,
2016 last_reset_ns: AnyIntProperty,
2017 }
2018 }
2019 }
2020 });
2021 } else {
2022 assert_data_tree!(inspector, root: {
2025 power_observability_state_recorders: {
2026 my_u64_metrics_p: {
2027 metadata: {
2028 name: "my_u64_metrics_p",
2029 type: "enum",
2030 states: {
2031 "Hundred": 100u64,
2032 "TwoHundred": 200u64,
2033 }
2034 },
2035 history: {
2036 "0": {
2037 "@time": AnyIntProperty,
2038 "value": "Hundred",
2039 },
2040 "1": {
2041 "@time": AnyIntProperty,
2042 "value": "TwoHundred",
2043 },
2044 "2": {
2045 "@time": AnyIntProperty,
2046 "value": "<Unknown>",
2047 },
2048 },
2049 reset_info: {
2050 count: 0,
2051 last_reset_ns: AnyIntProperty,
2052 }
2053 }
2054 }
2055 });
2056 }
2057 }
2058
2059 #[test_case(true; "lazy")]
2060 #[fuchsia::test]
2061 async fn test_numeric_persistence_reboot(lazy_record: bool) {
2062 use std::fs;
2063 use tempfile::tempdir;
2064
2065 let dir = tempdir().unwrap();
2067 let storage_path = dir.path().join("data");
2068 let volatile_path = dir.path().join("tmp");
2069 fs::create_dir(&storage_path).unwrap();
2070 fs::create_dir(&volatile_path).unwrap();
2071
2072 let inspector = Inspector::default();
2073 let manager = StateRecorderManager::new(&inspector);
2074
2075 let create_options = |manager_ref| RecorderOptions {
2076 lazy_record,
2077 capacity: 10,
2078 manager: Some(manager_ref),
2079 persistence: Some(
2080 PersistenceOptions::new("num_reboot_test".to_string())
2081 .storage_dir(storage_path.to_str().unwrap())
2082 .volatile_dir(volatile_path.to_str().unwrap()),
2083 ),
2084 };
2085
2086 let curr_csv = storage_path.join("num_reboot_test.csv");
2090 fs::write(&curr_csv, "1000,42\n2000,100\n").unwrap();
2092
2093 let prev_csv = volatile_path.join("num_reboot_test.csv");
2094 assert!(!prev_csv.exists());
2095
2096 let mut _recorder = NumericStateRecorder::new(
2098 "num_reboot_test".into(),
2099 c"power_test",
2100 units!(Number),
2101 Some((0u64, 200u64)),
2102 create_options(manager),
2103 )
2104 .unwrap();
2105
2106 assert!(prev_csv.exists(), "Library should have rotated curr -> prev");
2109 assert!(curr_csv.exists(), "Library should have create a new current file");
2110
2111 let rotated_content = fs::read_to_string(&prev_csv).unwrap();
2112 assert_eq!(rotated_content, "1000,42\n2000,100\n");
2113
2114 assert_data_tree!(inspector, root: {
2116 power_observability_state_recorders: {
2117 num_reboot_test: {
2118 metadata: {
2119 name: "num_reboot_test",
2120 type: "numeric",
2121 units: "#",
2122 range: {
2123 min_inc: 0u64,
2124 max_inc: 200u64,
2125 }
2126 },
2127 previous_boot_history: {
2128 "0": {
2129 "@time": 1000i64,
2130 "value": 42u64,
2131 },
2132 "1": {
2133 "@time": 2000i64,
2134 "value": 100u64,
2135 },
2136 },
2137 history: {},
2138 reset_info: {
2139 count: 0i64,
2140 last_reset_ns: AnyIntProperty,
2141 }
2142 }
2143 }
2144 });
2145 }
2146}