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 Seconds(Option<DecimalPrefix>),
559 Watts(Option<DecimalPrefix>),
560 Volts(Option<DecimalPrefix>),
561 Celsius(Option<DecimalPrefix>),
562 Number(Option<DecimalPrefix>),
563 Percent,
564}
565
566impl Display for Units {
567 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
568 fn write_helper(
569 f: &mut std::fmt::Formatter<'_>,
570 prefix: &Option<DecimalPrefix>,
571 unit_str: &str,
572 ) -> std::fmt::Result {
573 match prefix {
574 Some(p) => write!(f, "{}{}", p, unit_str),
575 None => write!(f, "{}", unit_str),
576 }
577 }
578
579 match self {
580 Units::Amps(prefix) => write_helper(f, prefix, "A"),
581 Units::AmpHours(prefix) => write_helper(f, prefix, "AH"),
582 Units::Hertz(prefix) => write_helper(f, prefix, "Hz"),
583 Units::Joules(prefix) => write_helper(f, prefix, "J"),
584 Units::Seconds(prefix) => write_helper(f, prefix, "s"),
585 Units::Watts(prefix) => write_helper(f, prefix, "W"),
586 Units::Volts(prefix) => write_helper(f, prefix, "V"),
587 Units::Celsius(prefix) => write_helper(f, prefix, "C"),
588 Units::Number(prefix) => write_helper(f, prefix, "#"),
589 Units::Percent => write!(f, "%"),
590 }
591 }
592}
593
594#[derive(Copy, Clone, Debug, PartialEq, Eq)]
596pub enum DecimalPrefix {
597 Nano,
598 Micro,
599 Milli,
600 Centi,
601 Deci,
602 Kilo,
603 Mega,
604 Giga,
605}
606
607impl Display for DecimalPrefix {
608 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
609 match self {
610 DecimalPrefix::Nano => write!(f, "n"),
611 DecimalPrefix::Micro => write!(f, "u"),
612 DecimalPrefix::Milli => write!(f, "m"),
613 DecimalPrefix::Centi => write!(f, "c"),
614 DecimalPrefix::Deci => write!(f, "d"),
615 DecimalPrefix::Kilo => write!(f, "k"),
616 DecimalPrefix::Mega => write!(f, "M"),
617 DecimalPrefix::Giga => write!(f, "G"),
618 }
619 }
620}
621
622#[macro_export]
631macro_rules! units {
632 (Percent) => {
633 $crate::Units::Percent
634 };
635 ($base_unit:ident) => {
636 $crate::Units::$base_unit(None)
637 };
638 ($prefix:ident, $base_unit:ident) => {
639 $crate::Units::$base_unit(Some($crate::DecimalPrefix::$prefix))
640 };
641}
642
643#[derive(Clone, Debug)]
645pub struct PersistenceOptions {
646 name: String,
648 current_path: String,
650 previous_path: String,
652 rename_path: String,
654}
655
656impl PersistenceOptions {
657 pub fn new(name: impl Into<String>) -> Self {
659 let name = name.into();
660 Self {
661 current_path: format!("/data/{}.csv", name),
662 previous_path: format!("/tmp/{}.csv", name),
663 rename_path: format!("/data/{}.tmp", name),
664 name,
665 }
666 }
667
668 pub fn storage_dir(mut self, dir: &str) -> Self {
669 self.current_path = format!("{}/{}.csv", dir, self.name);
670 self.rename_path = format!("{}/{}.tmp", dir, self.name);
671 self
672 }
673
674 pub fn volatile_dir(mut self, dir: &str) -> Self {
675 self.previous_path = format!("{}/{}.csv", dir, self.name);
676 self
677 }
678
679 fn paths(&self) -> (&str, &str, &str) {
681 (&self.current_path, &self.previous_path, &self.rename_path)
682 }
683}
684
685struct PersistenceHandler<T: Copy> {
687 config: PersistenceOptions,
688 buffer: Arc<Mutex<TimestampRingBuffer<T>>>,
690}
691
692impl<T: Copy + FromStr + Display> PersistenceHandler<T> {
693 fn new(
694 config: PersistenceOptions,
695 capacity: usize,
696 ) -> (Self, Arc<Mutex<TimestampRingBuffer<T>>>) {
697 let (curr, prev, _) = config.paths();
698
699 Self::prepare_files(&curr, &prev);
701
702 let initial_data = Self::read_log(&curr);
704
705 let mut buffer = TimestampRingBuffer::with_capacity(capacity);
707 for (ts, val) in initial_data {
708 buffer.insert(ts, val);
709 }
710
711 let shared_buffer = Arc::new(Mutex::new(buffer));
712
713 (Self { config, buffer: shared_buffer.clone() }, shared_buffer)
714 }
715
716 fn prepare_files(curr_path: &str, prev_path: &str) {
720 if Path::new(prev_path).exists() {
721 log::warn!("Not moving history, {} already exists", prev_path);
724 return;
725 }
726
727 let Ok(content) = std::fs::read_to_string(curr_path).map_err(|e| {
729 log::info!("Could not read current history, not moving: {}", e);
730 }) else {
731 return;
732 };
733
734 if let Err(e) = std::fs::write(prev_path, &content) {
735 log::warn!("Could not write previous boot history: {}", e);
736 return;
737 }
738
739 if let Err(e) = std::fs::File::create(curr_path) {
740 log::warn!("Could not clear current boot history: {}", e);
741 }
742 }
743
744 fn flush(&self, buffer_guard: &TimestampRingBuffer<T>) {
745 let (curr, _, temp) = self.config.paths();
746 let try_write = || -> std::io::Result<()> {
747 let mut file =
748 OpenOptions::new().write(true).create(true).truncate(true).open(&temp)?;
749
750 for (ts, val) in buffer_guard.iter() {
752 writeln!(file, "{},{}", ts, val)?;
753 }
754
755 file.sync_data()?;
756 fs::rename(&temp, &curr)?;
757 Ok(())
758 };
759
760 if let Err(e) = try_write() {
761 log::error!("StateRecorder: Persist failed for {}: {:?}", self.config.name, e);
762 }
763 }
764
765 fn append(&mut self, timestamp: i64, value: T) {
767 let mut guard = self.buffer.lock();
768 guard.insert(timestamp, value);
769 self.flush(&guard);
770 }
771
772 fn read_log(path: &str) -> Vec<(i64, T)> {
774 let Ok(content) = fs::read_to_string(path) else {
775 return Vec::new();
776 };
777 content
778 .lines()
779 .filter_map(|line| {
780 let line = line.trim();
781 let mut parts = line.splitn(2, ',');
782 let ts = parts.next()?.trim().parse::<i64>().ok()?;
783 let val = parts.next()?.trim().parse::<T>().ok()?;
784 Some((ts, val))
785 })
786 .collect()
787 }
788}
789
790#[derive(Default)]
792pub struct RecorderOptions {
793 pub lazy_record: bool,
795 pub capacity: usize,
797 pub manager: Option<Arc<Mutex<StateRecorderManager>>>,
801 pub persistence: Option<PersistenceOptions>,
803}
804
805#[derive(Debug)]
806enum RecorderHistory<T: Copy + Debug> {
807 Eager(BoundedListNode),
808 Lazy(Arc<Mutex<TimestampRingBuffer<T>>>),
809}
810
811#[derive(Debug)]
812struct TimestampRingBuffer<T: Copy> {
820 start_timestamp_ms: i64,
822 last_timestamp_ms: i64,
824 next_index: usize,
826 offset_ms: Vec<i32>,
828 data: Vec<T>,
830 reset_count: u32,
832 last_reset_ms: i64,
834}
835
836const NANOSECONDS_PER_MILLISECOND: i64 = 1_000_000;
837
838fn ms_to_ns(ms: i64) -> i64 {
839 ms * NANOSECONDS_PER_MILLISECOND
840}
841
842fn ns_to_ms(ns: i64) -> i64 {
843 ns / NANOSECONDS_PER_MILLISECOND
844}
845
846impl<T: Copy> TimestampRingBuffer<T> {
847 fn with_capacity(capacity: usize) -> Self {
848 let now_ms = ns_to_ms(zx::BootInstant::get().into_nanos());
849 Self {
850 start_timestamp_ms: now_ms,
851 last_timestamp_ms: now_ms,
852 next_index: 0,
853 offset_ms: Vec::with_capacity(capacity),
854 data: Vec::with_capacity(capacity),
855 reset_count: 0,
856 last_reset_ms: now_ms,
857 }
858 }
859
860 fn insert(&mut self, timestamp_ns: i64, value: T) {
861 let timestamp_ms = ns_to_ms(timestamp_ns);
862 let offset_ms = match i32::try_from(timestamp_ms - self.last_timestamp_ms) {
864 Ok(offset_ms) => offset_ms,
865 Err(_) => {
866 self.offset_ms.clear();
869 self.data.clear();
870 self.start_timestamp_ms = timestamp_ms;
871 self.next_index = 0;
872 self.reset_count += 1;
873 self.last_reset_ms = self.start_timestamp_ms;
874 0
875 }
876 };
877 if self.offset_ms.len() < self.offset_ms.capacity() {
878 self.offset_ms.push(offset_ms);
880 self.data.push(value);
881 } else {
882 self.start_timestamp_ms += self.offset_ms[self.next_index] as i64;
885 self.offset_ms[self.next_index] = offset_ms;
886 self.data[self.next_index] = value;
887 }
888 self.last_timestamp_ms = timestamp_ms;
889 self.next_index = (self.next_index + 1) % self.offset_ms.capacity();
890 }
891
892 fn get_reset_info(&self) -> (u32, i64) {
894 (self.reset_count, ms_to_ns(self.last_reset_ms))
895 }
896
897 fn iter(&self) -> TimestampRingBufferIter<'_, T> {
900 TimestampRingBufferIter::new(self)
901 }
902}
903
904struct TimestampRingBufferIter<'a, T: Copy> {
905 buffer: &'a TimestampRingBuffer<T>,
906 index: usize,
907 last_timestamp_ms: i64,
908}
909
910impl<'a, T: Copy> TimestampRingBufferIter<'a, T> {
911 fn new(buffer: &'a TimestampRingBuffer<T>) -> Self {
912 Self { buffer, index: 0, last_timestamp_ms: buffer.start_timestamp_ms }
913 }
914}
915
916impl<T: Copy> Iterator for TimestampRingBufferIter<'_, T> {
919 type Item = (i64, T);
920
921 fn next(&mut self) -> Option<(i64, T)> {
922 if self.index >= self.buffer.offset_ms.len() {
923 return None;
924 }
925 let index = (self.index + self.buffer.next_index) % self.buffer.offset_ms.len();
927 let timestamp_ms = self.last_timestamp_ms + self.buffer.offset_ms[index] as i64;
928 self.index += 1;
929 self.last_timestamp_ms = timestamp_ms;
930 Some((ms_to_ns(timestamp_ms), self.buffer.data[index]))
931 }
932}
933
934pub struct NumericStateRecorder<T: RecordableNumericType> {
935 manager: Arc<Mutex<StateRecorderManager>>,
936 name: String,
937 trace_category: &'static CStr,
938 trace_name: &'static CStr,
939 units: String,
940 history: RecorderHistory<T>,
941 persistence: Option<PersistenceHandler<T>>,
942 _root_node: inspect::Node,
943 trace_id: ftrace::Id,
944 _phantom: PhantomData<T>,
945}
946
947impl<T: RecordableNumericType> NumericStateRecorder<T> {
948 pub fn new(
959 name: String,
960 trace_category: &'static CStr,
961 units: Units,
962 range: Option<(T, T)>,
963 options: RecorderOptions,
964 ) -> Result<Self, StateRecorderError> {
965 let manager = options.manager.clone().unwrap_or_else(|| SINGLETON_MANAGER.clone());
966 let node = register_with_manager(&manager, &name)?;
967
968 let trace_name = lazy_static_cstr(&name)?;
969 let units_str = format!("{}", units);
970
971 node.record_child("metadata", |metadata_node| {
972 metadata_node.record_string("name", &name);
973 metadata_node.record_string("type", "numeric");
974 metadata_node.record_string("units", &units_str);
975 match range {
976 Some(r) => metadata_node.record_child("range", |node| T::record_range(&r, node)),
977 None => metadata_node.record_string("range", "<Unspecified>"),
978 }
979 });
980
981 let record_item = |node: &inspect::Node, val: &T| {
982 val.record(node, "value");
983 };
984
985 let (history, persistence) = setup_recording_backend(&node, &options, record_item)?;
986
987 Ok(Self {
988 manager,
989 name,
990 trace_category,
991 trace_name,
992 units: units_str,
993 history,
994 persistence,
995 _root_node: node,
996 trace_id: ftrace::Id::random(),
997 _phantom: PhantomData,
998 })
999 }
1000
1001 pub fn record(&mut self, state_value: T) {
1002 let timestamp = zx::BootInstant::get().into_nanos();
1003
1004 ftrace::counter!(
1005 self.trace_category,
1006 self.trace_name,
1007 self.trace_id.into(),
1008 &self.units.to_string() => state_value.trace_value()
1009 );
1010
1011 if let Some(handler) = &mut self.persistence {
1013 handler.append(timestamp, state_value);
1014 } else {
1015 match &mut self.history {
1016 RecorderHistory::Eager(history) => {
1017 history.add_entry(|node| {
1018 node.record_int("@time", timestamp);
1019 state_value.record(node, "value");
1020 });
1021 }
1022 RecorderHistory::Lazy(history) => {
1023 history.lock().insert(timestamp, state_value);
1024 }
1025 }
1026 }
1027 }
1028}
1029
1030impl<T: RecordableNumericType> Drop for NumericStateRecorder<T> {
1031 fn drop(&mut self) {
1032 self.manager.lock().unregister_name(&self.name);
1033 }
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038 use super::*;
1039 use diagnostics_assertions::{AnyIntProperty, assert_data_tree};
1040 use fuchsia_inspect::Inspector;
1041 use strum_macros::{Display, EnumIter, EnumString};
1042 use test_case::test_case;
1043
1044 #[derive(Copy, Clone, Debug, Display, EnumIter, EnumString, Eq, PartialEq, Hash)]
1045 #[repr(u8)]
1046 enum SwitchState {
1047 OFF = 0,
1048 ON = 1,
1049 }
1050
1051 impl From<SwitchState> for u64 {
1052 fn from(value: SwitchState) -> Self {
1053 value as Self
1054 }
1055 }
1056
1057 #[fuchsia::test]
1058 async fn test_timestamp_ring_buffer() {
1059 let mut buffer = TimestampRingBuffer::<i32>::with_capacity(3);
1060 let start_ms = buffer.start_timestamp_ms;
1061
1062 let t1 = (ms_to_ns(start_ms + 1000), 1);
1063 let t2 = (ms_to_ns(start_ms + 900), 2);
1065 let t3 = (ms_to_ns(start_ms + 3000), 3);
1066
1067 buffer.insert(t1.0, t1.1);
1068 buffer.insert(t2.0, t2.1);
1069 buffer.insert(t3.0, t3.1);
1070
1071 assert_eq!(vec![t1, t2, t3], buffer.iter().collect::<Vec<_>>());
1072 assert_eq!((0, ms_to_ns(start_ms)), buffer.get_reset_info());
1073
1074 let t4 = (ms_to_ns(start_ms + 4000), 4);
1076 buffer.insert(t4.0, t4.1);
1077 assert_eq!(vec![t2, t3, t4], buffer.iter().collect::<Vec<_>>());
1078 assert_eq!((0, ms_to_ns(start_ms)), buffer.get_reset_info());
1079 }
1080
1081 #[fuchsia::test]
1082 async fn test_timestamp_ring_buffer_resets_on_maximum_offset() {
1083 let mut buffer = TimestampRingBuffer::<i32>::with_capacity(3);
1084 let start_ms = buffer.start_timestamp_ms;
1085
1086 const MAX_OFFSET_MS: i64 = i32::MAX as i64;
1087 let t1 = (ms_to_ns(start_ms + 1000), 1);
1088 let t2 = (t1.0 + ms_to_ns(MAX_OFFSET_MS), 2);
1089
1090 buffer.insert(t1.0, t1.1);
1091 buffer.insert(t2.0, t2.1);
1092
1093 assert_eq!(vec![t1, t2], buffer.iter().collect::<Vec<_>>());
1094 assert_eq!((0, ms_to_ns(start_ms)), buffer.get_reset_info());
1095
1096 let t3 = (t2.0 + ms_to_ns(MAX_OFFSET_MS + 1), 3);
1099 buffer.insert(t3.0, t3.1);
1100 assert_eq!(vec![t3], buffer.iter().collect::<Vec<_>>());
1101 assert_eq!((1, t3.0), buffer.get_reset_info());
1102 }
1103
1104 #[test_case(false; "eager")]
1105 #[test_case(true; "lazy")]
1106 #[fuchsia::test]
1107 async fn test_enum_off_on(lazy_record: bool) {
1108 let inspector = Inspector::default();
1109 let manager = StateRecorderManager::new(&inspector);
1110
1111 let mut recorder = EnumStateRecorder::new(
1112 "my_switch".into(),
1113 c"power_test",
1114 RecorderOptions {
1115 lazy_record,
1116 capacity: 10,
1117 manager: Some(manager),
1118 persistence: None,
1119 },
1120 )
1121 .unwrap();
1122
1123 recorder.record(SwitchState::OFF);
1124 recorder.record(SwitchState::ON);
1125 recorder.record(SwitchState::OFF);
1126 recorder.record(SwitchState::ON);
1127 assert_data_tree!(inspector, root: {
1128 power_observability_state_recorders: {
1129 my_switch: {
1130 metadata: {
1131 name: "my_switch",
1132 type: "enum",
1133 states: {
1134 "OFF": 0u64,
1135 "ON": 1u64,
1136 }
1137 },
1138 history: {
1139 "0": {
1140 "@time": AnyIntProperty,
1141 "value": "OFF",
1142 },
1143 "1": {
1144 "@time": AnyIntProperty,
1145 "value": "ON",
1146 },
1147 "2": {
1148 "@time": AnyIntProperty,
1149 "value": "OFF",
1150 },
1151 "3": {
1152 "@time": AnyIntProperty,
1153 "value": "ON",
1154 },
1155 },
1156 reset_info: {
1157 count: 0,
1158 last_reset_ns: AnyIntProperty,
1159 }
1160 }
1161 }
1162 });
1163 }
1164
1165 #[test_case(false; "eager")]
1166 #[test_case(true; "lazy")]
1167 #[fuchsia::test]
1168 async fn test_multiple_recorders(lazy_record: bool) {
1169 #[derive(Copy, Clone, Debug, Display, EnumIter, EnumString, Eq, PartialEq, Hash)]
1170 #[repr(u8)]
1171 enum EnablementState {
1172 DISABLED = 0,
1173 ENABLED = 1,
1174 }
1175 impl From<EnablementState> for u64 {
1176 fn from(value: EnablementState) -> Self {
1177 value as Self
1178 }
1179 }
1180
1181 let inspector = Inspector::default();
1182 let manager = StateRecorderManager::new(&inspector);
1183
1184 let mut recorder_0 = EnumStateRecorder::new(
1185 "switch_0".into(),
1186 c"power_test",
1187 RecorderOptions {
1188 lazy_record,
1189 capacity: 10,
1190 manager: Some(manager.clone()),
1191 persistence: None,
1192 },
1193 )
1194 .unwrap();
1195 let mut recorder_1 = EnumStateRecorder::new(
1196 "switch_1".into(),
1197 c"power_test",
1198 RecorderOptions {
1199 lazy_record,
1200 capacity: 10,
1201 manager: Some(manager),
1202 persistence: None,
1203 },
1204 )
1205 .unwrap();
1206 recorder_0.record(SwitchState::OFF);
1207 recorder_0.record(SwitchState::ON);
1208 recorder_1.record(EnablementState::ENABLED);
1209 recorder_1.record(EnablementState::DISABLED);
1210
1211 assert_data_tree!(inspector, root: {
1212 power_observability_state_recorders: {
1213 switch_0: {
1214 metadata: {
1215 name: "switch_0",
1216 type: "enum",
1217 states: {
1218 "OFF": 0u64,
1219 "ON": 1u64,
1220 }
1221 },
1222 history: {
1223 "0": {
1224 "@time": AnyIntProperty,
1225 "value": "OFF",
1226 },
1227 "1": {
1228 "@time": AnyIntProperty,
1229 "value": "ON",
1230 },
1231 },
1232 reset_info: {
1233 count: 0,
1234 last_reset_ns: AnyIntProperty,
1235 }
1236 },
1237 switch_1: {
1238 metadata: {
1239 name: "switch_1",
1240 type: "enum",
1241 states: {
1242 "DISABLED": 0u64,
1243 "ENABLED": 1u64,
1244 }
1245 },
1246 history: {
1247 "0": {
1248 "@time": AnyIntProperty,
1249 "value": "ENABLED",
1250 },
1251 "1": {
1252 "@time": AnyIntProperty,
1253 "value": "DISABLED",
1254 },
1255 },
1256 reset_info: {
1257 count: 0,
1258 last_reset_ns: AnyIntProperty,
1259 }
1260 }
1261 }
1262 })
1263 }
1264
1265 #[test_case(false; "eager")]
1266 #[test_case(true; "lazy")]
1267 #[fuchsia::test]
1268 async fn test_enum_three_states(lazy_record: bool) {
1269 #[derive(Copy, Clone, Debug, Display, EnumIter, EnumString, Eq, PartialEq, Hash)]
1270 #[repr(u8)]
1271 enum FanSpeed {
1272 OFF = 0,
1273 LOW = 1,
1274 HIGH = 2,
1275 }
1276
1277 impl From<FanSpeed> for u64 {
1278 fn from(value: FanSpeed) -> Self {
1279 value as Self
1280 }
1281 }
1282
1283 let inspector = Inspector::default();
1284 let manager = StateRecorderManager::new(&inspector);
1285
1286 let mut recorder = EnumStateRecorder::new(
1287 "the_best_fan".into(),
1288 c"power_test",
1289 RecorderOptions {
1290 lazy_record,
1291 capacity: 10,
1292 manager: Some(manager),
1293 persistence: None,
1294 },
1295 )
1296 .unwrap();
1297
1298 recorder.record(FanSpeed::OFF);
1299 recorder.record(FanSpeed::LOW);
1300 recorder.record(FanSpeed::HIGH);
1301 recorder.record(FanSpeed::OFF);
1302 recorder.record(FanSpeed::HIGH);
1303 assert_data_tree!(inspector, root: {
1304 power_observability_state_recorders: {
1305 the_best_fan: {
1306 metadata: {
1307 name: "the_best_fan",
1308 type: "enum",
1309 states: {
1310 "OFF": 0u64,
1311 "LOW": 1u64,
1312 "HIGH": 2u64,
1313 }
1314 },
1315 history: {
1316 "0": {
1317 "@time": AnyIntProperty,
1318 "value": "OFF",
1319 },
1320 "1": {
1321 "@time": AnyIntProperty,
1322 "value": "LOW",
1323 },
1324 "2": {
1325 "@time": AnyIntProperty,
1326 "value": "HIGH",
1327 },
1328 "3": {
1329 "@time": AnyIntProperty,
1330 "value": "OFF",
1331 },
1332 "4": {
1333 "@time": AnyIntProperty,
1334 "value": "HIGH",
1335 },
1336 },
1337 reset_info: {
1338 count: 0,
1339 last_reset_ns: AnyIntProperty,
1340 }
1341 }
1342 }
1343 });
1344 }
1345
1346 #[test_case(false; "eager")]
1347 #[test_case(true; "lazy")]
1348 #[fuchsia::test]
1349 async fn test_name_reuse_not_allowed(lazy_record: bool) {
1350 let inspector = Inspector::default();
1351 let manager = StateRecorderManager::new(&inspector);
1352
1353 let recorder = EnumStateRecorder::<SwitchState>::new(
1354 "my_switch".into(),
1355 c"power_test",
1356 RecorderOptions {
1357 lazy_record,
1358 capacity: 10,
1359 manager: Some(manager.clone()),
1360 persistence: None,
1361 },
1362 )
1363 .unwrap();
1364
1365 let result = EnumStateRecorder::<SwitchState>::new(
1367 "my_switch".into(),
1368 c"power_test",
1369 RecorderOptions {
1370 lazy_record,
1371 capacity: 10,
1372 manager: Some(manager.clone()),
1373 persistence: None,
1374 },
1375 );
1376 assert!(result.is_err());
1377
1378 drop(recorder);
1380 let result = EnumStateRecorder::<SwitchState>::new(
1381 "my_switch".into(),
1382 c"power_test",
1383 RecorderOptions {
1384 lazy_record,
1385 capacity: 10,
1386 manager: Some(manager.clone()),
1387 persistence: None,
1388 },
1389 );
1390 assert!(result.is_ok());
1391 }
1392
1393 #[test_case(false; "eager")]
1394 #[test_case(true; "lazy")]
1395 #[fuchsia::test]
1396 async fn test_singleton_manager(lazy_record: bool) {
1397 let mut recorder = EnumStateRecorder::new(
1398 "my_switch".into(),
1399 c"power_test",
1400 RecorderOptions { lazy_record, capacity: 10, manager: None, persistence: None },
1401 )
1402 .unwrap();
1403
1404 recorder.record(SwitchState::OFF);
1405 recorder.record(SwitchState::ON);
1406 assert_data_tree!(inspect::component::inspector(), root: {
1407 power_observability_state_recorders: {
1408 my_switch: {
1409 metadata: {
1410 name: "my_switch",
1411 type: "enum",
1412 states: {
1413 "OFF": 0u64,
1414 "ON": 1u64,
1415 }
1416 },
1417 history: {
1418 "0": {
1419 "@time": AnyIntProperty,
1420 "value": "OFF",
1421 },
1422 "1": {
1423 "@time": AnyIntProperty,
1424 "value": "ON",
1425 },
1426 },
1427 reset_info: {
1428 count: 0,
1429 last_reset_ns: AnyIntProperty,
1430 }
1431 }
1432 }
1433 });
1434 }
1435
1436 #[fuchsia::test]
1437 async fn test_recorder_is_send() {
1438 fn assert_send<T: Send>() {}
1439 assert_send::<EnumStateRecorder<SwitchState>>();
1440 }
1441
1442 async fn test_uint_numeric_type<T: RecordableNumericType>(lazy_record: bool)
1443 where
1444 T: Into<u64> + From<u8>,
1445 {
1446 let inspector = Inspector::default();
1447 let manager = StateRecorderManager::new(&inspector);
1448 let mut recorder = NumericStateRecorder::new(
1449 "my_stateful_thing".into(),
1450 c"power_test",
1451 units!(Percent),
1452 Some((T::from(0), T::from(255))),
1453 RecorderOptions {
1454 lazy_record,
1455 capacity: 10,
1456 manager: Some(manager),
1457 persistence: None,
1458 },
1459 )
1460 .unwrap();
1461
1462 recorder.record(T::from(10));
1463 recorder.record(T::from(0));
1464 assert_data_tree!(inspector, root: {
1465 power_observability_state_recorders: {
1466 my_stateful_thing: {
1467 metadata: {
1468 name: "my_stateful_thing",
1469 type: "numeric",
1470 units: "%",
1471 range: {
1472 min_inc: 0u64,
1473 max_inc: 255u64
1474 },
1475 },
1476 history: {
1477 "0": {
1478 "@time": AnyIntProperty,
1479 "value": 10u64,
1480 },
1481 "1": {
1482 "@time": AnyIntProperty,
1483 "value": 0u64,
1484 },
1485 },
1486 reset_info: {
1487 count: 0,
1488 last_reset_ns: AnyIntProperty,
1489 }
1490 }
1491 }
1492 });
1493 }
1494
1495 #[test_case(false; "eager")]
1496 #[test_case(true; "lazy")]
1497 #[fuchsia::test]
1498 async fn test_uint_numeric_types(lazy_record: bool) {
1499 test_uint_numeric_type::<u8>(lazy_record).await;
1500 test_uint_numeric_type::<u16>(lazy_record).await;
1501 test_uint_numeric_type::<u32>(lazy_record).await;
1502 test_uint_numeric_type::<u64>(lazy_record).await;
1503 }
1504
1505 async fn test_int_numeric_type<T: RecordableNumericType>(lazy_record: bool)
1506 where
1507 T: Into<i64> + From<i8>,
1508 {
1509 let inspector = Inspector::default();
1510 let manager = StateRecorderManager::new(&inspector);
1511 let mut recorder = NumericStateRecorder::new(
1512 "my_stateful_thing".into(),
1513 c"power_test",
1514 units!(Number),
1515 Some((T::from(-128), T::from(127))),
1516 RecorderOptions {
1517 lazy_record,
1518 capacity: 10,
1519 manager: Some(manager),
1520 persistence: None,
1521 },
1522 )
1523 .unwrap();
1524
1525 recorder.record(T::from(10));
1526 recorder.record(T::from(0));
1527 assert_data_tree!(inspector, root: {
1528 power_observability_state_recorders: {
1529 my_stateful_thing: {
1530 metadata: {
1531 name: "my_stateful_thing",
1532 type: "numeric",
1533 units: "#",
1534 range: {
1535 min_inc: -128i64,
1536 max_inc: 127i64
1537 },
1538 },
1539 history: {
1540 "0": {
1541 "@time": AnyIntProperty,
1542 "value": 10i64,
1543 },
1544 "1": {
1545 "@time": AnyIntProperty,
1546 "value": 0i64,
1547 },
1548 },
1549 reset_info: {
1550 count: 0,
1551 last_reset_ns: AnyIntProperty,
1552 }
1553 }
1554 }
1555 });
1556 }
1557
1558 #[test_case(false; "eager")]
1559 #[test_case(true; "lazy")]
1560 #[fuchsia::test]
1561 async fn test_int_numeric_types(lazy_record: bool) {
1562 test_int_numeric_type::<i8>(lazy_record).await;
1563 test_int_numeric_type::<i16>(lazy_record).await;
1564 test_int_numeric_type::<i32>(lazy_record).await;
1565 test_int_numeric_type::<i64>(lazy_record).await;
1566 }
1567
1568 async fn test_float_numeric_type<T: RecordableNumericType>(lazy_record: bool)
1569 where
1570 T: Into<f64> + From<u8>,
1571 {
1572 let inspector = Inspector::default();
1573 let manager = StateRecorderManager::new(&inspector);
1574 let mut recorder = NumericStateRecorder::new(
1575 "my_stateful_thing".into(),
1576 c"power_test",
1577 units!(Kilo, Hertz),
1578 Some((T::from(0), T::from(255))),
1579 RecorderOptions {
1580 lazy_record,
1581 capacity: 10,
1582 manager: Some(manager),
1583 persistence: None,
1584 },
1585 )
1586 .unwrap();
1587
1588 recorder.record(T::from(10));
1589 recorder.record(T::from(0));
1590 assert_data_tree!(inspector, root: {
1591 power_observability_state_recorders: {
1592 my_stateful_thing: {
1593 metadata: {
1594 name: "my_stateful_thing",
1595 type: "numeric",
1596 units: "kHz",
1597 range: {
1598 min_inc: 0.0,
1599 max_inc: 255.0
1600 },
1601 },
1602 history: {
1603 "0": {
1604 "@time": AnyIntProperty,
1605 "value": 10.0,
1606 },
1607 "1": {
1608 "@time": AnyIntProperty,
1609 "value": 0.0,
1610 },
1611 },
1612 reset_info: {
1613 count: 0,
1614 last_reset_ns: AnyIntProperty,
1615 }
1616 }
1617 }
1618 });
1619 }
1620
1621 #[test_case(false; "eager")]
1622 #[test_case(true; "lazy")]
1623 #[fuchsia::test]
1624 async fn test_float_numeric_types(lazy_record: bool) {
1625 test_float_numeric_type::<f32>(lazy_record).await;
1626 test_float_numeric_type::<f64>(lazy_record).await;
1627 }
1628
1629 #[test_case(true; "lazy")]
1630 #[fuchsia::test]
1631 async fn test_persistence_crash_recovery(lazy_record: bool) {
1632 use std::fs;
1633 use tempfile::tempdir;
1634
1635 let dir = tempdir().unwrap();
1637 let storage_path = dir.path().join("data");
1638 let volatile_path = dir.path().join("tmp");
1639 fs::create_dir(&storage_path).unwrap();
1640 fs::create_dir(&volatile_path).unwrap();
1641
1642 let inspector = Inspector::default();
1643 let manager = StateRecorderManager::new(&inspector);
1644
1645 let create_options = |manager_ref| RecorderOptions {
1647 lazy_record, capacity: 10,
1649 manager: Some(manager_ref),
1650 persistence: Some(
1651 PersistenceOptions::new("crash_test".to_string())
1652 .storage_dir(storage_path.to_str().unwrap())
1653 .volatile_dir(volatile_path.to_str().unwrap()),
1654 ),
1655 };
1656
1657 {
1659 let mut recorder = EnumStateRecorder::<SwitchState>::new(
1660 "crash_test".into(),
1661 c"power_test",
1662 create_options(manager.clone()),
1663 )
1664 .unwrap();
1665
1666 recorder.record(SwitchState::ON);
1667 recorder.record(SwitchState::OFF);
1668
1669 }
1671
1672 let curr_csv = storage_path.join("crash_test.csv");
1674 let content = fs::read_to_string(curr_csv).unwrap();
1675 let lines: Vec<&str> = content.trim().lines().collect();
1677 assert_eq!(lines.len(), 2, "Expected 2 lines of recorded history");
1678
1679 let parts0: Vec<&str> = lines[0].split(',').collect();
1681 assert_eq!(parts0.len(), 2, "Invalid CSV format in line 1");
1682 assert_eq!(parts0[1], "1", "First record should be ON (1)");
1683
1684 let parts1: Vec<&str> = lines[1].split(',').collect();
1686 assert_eq!(parts1.len(), 2, "Invalid CSV format in line 2");
1687 assert_eq!(parts1[1], "0", "Second record should be OFF (0)");
1688
1689 let prev_csv = volatile_path.join("crash_test.csv");
1693 fs::write(&prev_csv, "").unwrap();
1694
1695 let mut recorder_restarted = EnumStateRecorder::<SwitchState>::new(
1698 "crash_test".into(),
1699 c"power_test",
1700 create_options(manager),
1701 )
1702 .unwrap();
1703
1704 assert_data_tree!(inspector, root: {
1706 power_observability_state_recorders: {
1707 crash_test: {
1708 metadata: {
1709 name: "crash_test",
1710 type: "enum",
1711 states: {
1712 "OFF": 0u64,
1713 "ON": 1u64,
1714 }
1715 },
1716 history: {
1717 "0": {
1718 "@time": AnyIntProperty,
1719 "value": "ON",
1720 },
1721 "1": {
1722 "@time": AnyIntProperty,
1723 "value": "OFF",
1724 },
1725 },
1726 reset_info: {
1727 count: 0i64, last_reset_ns: AnyIntProperty,
1729 },
1730 }
1731 }
1732 });
1733
1734 recorder_restarted.record(SwitchState::ON);
1736 assert_data_tree!(inspector, root: {
1737 power_observability_state_recorders: {
1738 crash_test: {
1739 metadata: {
1740 name: "crash_test",
1741 type: "enum",
1742 states: {
1743 "OFF": 0u64,
1744 "ON": 1u64,
1745 }
1746 },
1747 history: {
1748 "0": {
1749 "@time": AnyIntProperty,
1750 "value": "ON",
1751 },
1752 "1": {
1753 "@time": AnyIntProperty,
1754 "value": "OFF",
1755 },
1756 "2": {
1757 "@time": AnyIntProperty,
1758 "value": "ON",
1759 },
1760 },
1761 reset_info: {
1762 count: 0i64,
1763 last_reset_ns: AnyIntProperty,
1764 },
1765 }
1766 }
1767 });
1768 }
1769
1770 #[test_case(true; "lazy")]
1771 #[fuchsia::test]
1772 async fn test_persistence_reboot(lazy_record: bool) {
1773 use std::fs;
1774 use tempfile::tempdir;
1775
1776 let dir = tempdir().unwrap();
1778 let storage_path = dir.path().join("data");
1779 let volatile_path = dir.path().join("tmp");
1780 fs::create_dir(&storage_path).unwrap();
1781 fs::create_dir(&volatile_path).unwrap();
1782
1783 let inspector = Inspector::default();
1784 let manager = StateRecorderManager::new(&inspector);
1785
1786 let create_options = |manager_ref| RecorderOptions {
1788 lazy_record,
1789 capacity: 10,
1790 manager: Some(manager_ref),
1791 persistence: Some(
1792 PersistenceOptions::new("reboot_test".to_string())
1793 .storage_dir(storage_path.to_str().unwrap())
1794 .volatile_dir(volatile_path.to_str().unwrap()),
1795 ),
1796 };
1797
1798 let curr_csv = storage_path.join("reboot_test.csv");
1802 fs::write(&curr_csv, "1000,1\n2000,0\n").unwrap();
1804
1805 let prev_csv = volatile_path.join("reboot_test.csv");
1807 assert!(!prev_csv.exists());
1808
1809 let mut recorder = EnumStateRecorder::<SwitchState>::new(
1811 "reboot_test".into(),
1812 c"power_test",
1813 create_options(manager),
1814 )
1815 .unwrap();
1816
1817 assert!(prev_csv.exists(), "Library should have rotated curr -> prev");
1820 assert!(curr_csv.exists(), "Library should have create a new current file");
1821
1822 let rotated_content = fs::read_to_string(&prev_csv).unwrap();
1823 assert_eq!(rotated_content, "1000,1\n2000,0\n");
1824
1825 assert_data_tree!(inspector, root: {
1827 power_observability_state_recorders: {
1828 reboot_test: {
1829 metadata: {
1830 name: "reboot_test",
1831 type: "enum",
1832 states: {
1833 "OFF": 0u64,
1834 "ON": 1u64,
1835 }
1836 },
1837 previous_boot_history: {
1839 "0": {
1840 "@time": 1000i64,
1841 "value": "ON",
1842 },
1843 "1": {
1844 "@time": 2000i64,
1845 "value": "OFF",
1846 },
1847 },
1848 history: {},
1850 reset_info: {
1851 count: 0i64,
1852 last_reset_ns: AnyIntProperty,
1853 },
1854 }
1855 }
1856 });
1857
1858 recorder.record(SwitchState::ON);
1860 recorder.record(SwitchState::OFF);
1861 assert_data_tree!(inspector, root: {
1862 power_observability_state_recorders: {
1863 reboot_test: {
1864 metadata: {
1865 name: "reboot_test",
1866 type: "enum",
1867 states: {
1868 "OFF": 0u64,
1869 "ON": 1u64,
1870 }
1871 },
1872 previous_boot_history: {
1874 "0": {
1875 "@time": 1000i64,
1876 "value": "ON",
1877 },
1878 "1": {
1879 "@time": 2000i64,
1880 "value": "OFF",
1881 },
1882 },
1883 history: {
1885 "0": {
1886 "@time": AnyIntProperty, "value": "ON",
1888 },
1889 "1": {
1890 "@time": AnyIntProperty, "value": "OFF",
1892 },
1893 },
1894 reset_info: {
1895 count: 0i64,
1896 last_reset_ns: AnyIntProperty,
1897 },
1898 }
1899 }
1900 });
1901 }
1902
1903 #[test_case(false; "eager")]
1904 #[test_case(true; "lazy")]
1905 #[fuchsia::test]
1906 async fn test_named_u64_recorder(lazy_record: bool) {
1907 use std::fs;
1908 use tempfile::tempdir;
1909
1910 let dir = tempdir().unwrap();
1912 let storage_path = dir.path().join("data");
1913 let volatile_path = dir.path().join("tmp");
1914 fs::create_dir(&storage_path).unwrap();
1915 fs::create_dir(&volatile_path).unwrap();
1916
1917 let inspector = Inspector::default();
1918 let manager = StateRecorderManager::new(&inspector);
1919
1920 let mut map = HashMap::new();
1921 map.insert(100, "Hundred".to_string());
1922 map.insert(200, "TwoHundred".to_string());
1923
1924 let persistence_opts = if lazy_record {
1925 Some(
1926 PersistenceOptions::new("my_u64_metrics_p".to_string())
1927 .storage_dir(storage_path.to_str().unwrap())
1928 .volatile_dir(volatile_path.to_str().unwrap()),
1929 )
1930 } else {
1931 None
1932 };
1933
1934 let mut recorder = NamedU64StateRecorder::new(
1936 "my_u64_metrics_p".into(),
1937 c"power_test",
1938 map.clone(),
1939 RecorderOptions {
1940 lazy_record,
1941 capacity: 10,
1942 manager: Some(manager.clone()),
1943 persistence: persistence_opts.clone(),
1944 },
1945 )
1946 .unwrap();
1947
1948 recorder.record(100);
1949 recorder.record(200);
1950 recorder.record(300); if lazy_record {
1954 drop(recorder); let curr_csv = storage_path.join("my_u64_metrics_p.csv");
1957 let content = fs::read_to_string(&curr_csv).unwrap();
1958 let lines: Vec<&str> = content.trim().lines().collect();
1959 assert_eq!(lines.len(), 3, "Expected 3 lines of recorded history");
1960
1961 let parts0: Vec<&str> = lines[0].split(',').collect();
1963 assert_eq!(parts0.len(), 2, "Invalid CSV format in line 1");
1964 assert_eq!(parts0[1], "100", "First record should be 100");
1965
1966 let parts1: Vec<&str> = lines[1].split(',').collect();
1968 assert_eq!(parts1.len(), 2, "Invalid CSV format in line 2");
1969 assert_eq!(parts1[1], "200", "Second record should be 200");
1970
1971 let parts2: Vec<&str> = lines[2].split(',').collect();
1973 assert_eq!(parts2.len(), 2, "Invalid CSV format in line 3");
1974 assert_eq!(parts2[1], "300", "Third record should be 300");
1975
1976 let mut _recorder_restarted = NamedU64StateRecorder::new(
1978 "my_u64_metrics_p".into(),
1979 c"power_test",
1980 map,
1981 RecorderOptions {
1982 lazy_record,
1983 capacity: 10,
1984 manager: Some(manager),
1985 persistence: persistence_opts,
1986 },
1987 )
1988 .unwrap();
1989
1990 assert_data_tree!(inspector, root: {
1991 power_observability_state_recorders: {
1992 my_u64_metrics_p: {
1993 metadata: {
1994 name: "my_u64_metrics_p",
1995 type: "enum",
1996 states: {
1997 "Hundred": 100u64,
1998 "TwoHundred": 200u64,
1999 }
2000 },
2001 previous_boot_history: {
2002 "0": {
2003 "@time": AnyIntProperty,
2004 "value": "Hundred",
2005 },
2006 "1": {
2007 "@time": AnyIntProperty,
2008 "value": "TwoHundred",
2009 },
2010 "2": {
2011 "@time": AnyIntProperty,
2012 "value": "<Unknown>",
2013 },
2014 },
2015 history: {},
2016 reset_info: {
2017 count: 0,
2018 last_reset_ns: AnyIntProperty,
2019 }
2020 }
2021 }
2022 });
2023 } else {
2024 assert_data_tree!(inspector, root: {
2027 power_observability_state_recorders: {
2028 my_u64_metrics_p: {
2029 metadata: {
2030 name: "my_u64_metrics_p",
2031 type: "enum",
2032 states: {
2033 "Hundred": 100u64,
2034 "TwoHundred": 200u64,
2035 }
2036 },
2037 history: {
2038 "0": {
2039 "@time": AnyIntProperty,
2040 "value": "Hundred",
2041 },
2042 "1": {
2043 "@time": AnyIntProperty,
2044 "value": "TwoHundred",
2045 },
2046 "2": {
2047 "@time": AnyIntProperty,
2048 "value": "<Unknown>",
2049 },
2050 },
2051 reset_info: {
2052 count: 0,
2053 last_reset_ns: AnyIntProperty,
2054 }
2055 }
2056 }
2057 });
2058 }
2059 }
2060
2061 #[test_case(true; "lazy")]
2062 #[fuchsia::test]
2063 async fn test_numeric_persistence_reboot(lazy_record: bool) {
2064 use std::fs;
2065 use tempfile::tempdir;
2066
2067 let dir = tempdir().unwrap();
2069 let storage_path = dir.path().join("data");
2070 let volatile_path = dir.path().join("tmp");
2071 fs::create_dir(&storage_path).unwrap();
2072 fs::create_dir(&volatile_path).unwrap();
2073
2074 let inspector = Inspector::default();
2075 let manager = StateRecorderManager::new(&inspector);
2076
2077 let create_options = |manager_ref| RecorderOptions {
2078 lazy_record,
2079 capacity: 10,
2080 manager: Some(manager_ref),
2081 persistence: Some(
2082 PersistenceOptions::new("num_reboot_test".to_string())
2083 .storage_dir(storage_path.to_str().unwrap())
2084 .volatile_dir(volatile_path.to_str().unwrap()),
2085 ),
2086 };
2087
2088 let curr_csv = storage_path.join("num_reboot_test.csv");
2092 fs::write(&curr_csv, "1000,42\n2000,100\n").unwrap();
2094
2095 let prev_csv = volatile_path.join("num_reboot_test.csv");
2096 assert!(!prev_csv.exists());
2097
2098 let mut _recorder = NumericStateRecorder::new(
2100 "num_reboot_test".into(),
2101 c"power_test",
2102 units!(Number),
2103 Some((0u64, 200u64)),
2104 create_options(manager),
2105 )
2106 .unwrap();
2107
2108 assert!(prev_csv.exists(), "Library should have rotated curr -> prev");
2111 assert!(curr_csv.exists(), "Library should have create a new current file");
2112
2113 let rotated_content = fs::read_to_string(&prev_csv).unwrap();
2114 assert_eq!(rotated_content, "1000,42\n2000,100\n");
2115
2116 assert_data_tree!(inspector, root: {
2118 power_observability_state_recorders: {
2119 num_reboot_test: {
2120 metadata: {
2121 name: "num_reboot_test",
2122 type: "numeric",
2123 units: "#",
2124 range: {
2125 min_inc: 0u64,
2126 max_inc: 200u64,
2127 }
2128 },
2129 previous_boot_history: {
2130 "0": {
2131 "@time": 1000i64,
2132 "value": 42u64,
2133 },
2134 "1": {
2135 "@time": 2000i64,
2136 "value": 100u64,
2137 },
2138 },
2139 history: {},
2140 reset_info: {
2141 count: 0i64,
2142 last_reset_ns: AnyIntProperty,
2143 }
2144 }
2145 }
2146 });
2147 }
2148}