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