1use fuchsia_inspect::{self as inspect, ArrayProperty, Inspector, Property};
17use fuchsia_sync::Mutex;
18use fuchsia_trace as ftrace;
19use futures_util::FutureExt;
20use std::cmp::{Eq, max, min};
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 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";
101pub const FORMAT_VERSION: &str = "2.0";
102
103static SINGLETON_MANAGER: LazyLock<Arc<Mutex<StateRecorderManager>>> =
105 LazyLock::new(|| StateRecorderManager::new(inspect::component::inspector()));
106
107pub fn manager() -> Arc<Mutex<StateRecorderManager>> {
108 SINGLETON_MANAGER.clone()
109}
110
111#[derive(thiserror::Error, Debug)]
112pub enum StateRecorderError {
113 #[error("The name \"{0}\" is already in use")]
114 DuplicateName(String),
115 #[error("String \"{0}\" cannot be converted to a CString")]
116 IncompatibleString(String),
117 #[error("Invalid options: {0}")]
118 InvalidOptions(String),
119}
120
121pub struct StateRecorderManager {
124 pub node: inspect::Node,
125 names_in_use: Vec<String>,
127}
128
129impl StateRecorderManager {
130 pub fn new(inspector: &inspect::Inspector) -> Arc<Mutex<Self>> {
131 Arc::new(Mutex::new(Self {
132 node: inspector.root().create_child(ROOT_NODE_NAME),
133 names_in_use: Vec::new(),
134 }))
135 }
136
137 fn register_name(&mut self, name: &str) -> Result<(), StateRecorderError> {
138 if self.names_in_use.iter().any(|s| s == name) {
139 return Err(StateRecorderError::DuplicateName(name.to_owned()));
140 }
141 self.names_in_use.push(name.to_owned());
142 Ok(())
143 }
144
145 fn unregister_name(&mut self, name: &str) {
146 match self.names_in_use.iter().position(|s| s == name) {
147 Some(index) => {
148 self.names_in_use.remove(index);
149 }
150 None => {
151 log::error!("unregister_name called with nonexistent name \"{}\"", name);
152 }
153 }
154 }
155}
156
157fn register_with_manager(
159 manager: &Arc<Mutex<StateRecorderManager>>,
160 name: &str,
161) -> Result<inspect::Node, StateRecorderError> {
162 let mut manager = manager.lock();
163 if let Err(e) = manager.register_name(name) {
164 return Err(e);
165 }
166 Ok(manager.node.create_child(name))
167}
168
169pub const SHARD_CAPACITY: usize = 200;
177
178#[derive(Debug)]
179pub enum InspectValueArray {
180 Uint(inspect::UintArrayProperty),
181 Int(inspect::IntArrayProperty),
182 Double(inspect::DoubleArrayProperty),
183}
184
185impl InspectValueArray {
186 pub fn record_property(self, node: &inspect::Node) {
187 match self {
188 Self::Uint(p) => node.record(p),
189 Self::Int(p) => node.record(p),
190 Self::Double(p) => node.record(p),
191 }
192 }
193}
194
195#[derive(Debug)]
196pub struct InspectShard {
197 _node: inspect::Node,
198 pub times: inspect::IntArrayProperty,
199 pub values: InspectValueArray,
200}
201
202impl InspectShard {
203 pub fn new(
204 parent_node: &inspect::Node,
205 shard_index: usize,
206 size: usize,
207 create_values: impl FnOnce(&inspect::Node, usize) -> InspectValueArray,
208 ) -> Self {
209 let node = parent_node.create_child(shard_index.to_string());
210 let times = node.create_int_array("times", size);
211 let values = create_values(&node, size);
212 Self { _node: node, times, values }
213 }
214}
215
216#[derive(Debug)]
217pub struct EagerShardedBuffer {
218 _history_node: inspect::Node,
219 _shards_node: inspect::Node,
220 current_index: inspect::UintProperty,
221 current_size: inspect::UintProperty,
222 shards: Vec<InspectShard>,
223 capacity: usize,
224 index_tracker: usize,
225 size_tracker: usize,
226}
227
228impl EagerShardedBuffer {
229 pub fn new(
230 parent_node: &inspect::Node,
231 capacity: usize,
232 create_values: impl Fn(&inspect::Node, usize) -> InspectValueArray,
233 ) -> Self {
234 let history_node = parent_node.create_child("history");
235 let current_index = history_node.create_uint("current_index", 0);
236 let current_size = history_node.create_uint("current_size", 0);
237 let shards_node = history_node.create_child("shards");
238
239 let active_capacity = max(capacity, 1);
240 let total_shards = (active_capacity + SHARD_CAPACITY - 1) / SHARD_CAPACITY;
241 let mut shards = Vec::with_capacity(total_shards);
242
243 for s in 0..total_shards {
244 let start_idx = s * SHARD_CAPACITY;
245 let end_idx = min(start_idx + SHARD_CAPACITY, capacity);
246 let shard_size = max(end_idx - start_idx, 1);
247 shards
248 .push(InspectShard::new(&shards_node, s, shard_size, |n, sz| create_values(n, sz)));
249 }
250
251 Self {
252 _history_node: history_node,
253 _shards_node: shards_node,
254 current_index,
255 current_size,
256 shards,
257 capacity,
258 index_tracker: 0,
259 size_tracker: 0,
260 }
261 }
262
263 pub fn record<T>(
264 &mut self,
265 timestamp_ns: i64,
266 val: T,
267 set_val: impl FnOnce(&InspectValueArray, usize, T),
268 ) {
269 if self.capacity == 0 {
270 return;
271 }
272 let shard_idx = self.index_tracker / SHARD_CAPACITY;
273 let slot_idx = self.index_tracker % SHARD_CAPACITY;
274
275 let shard = &self.shards[shard_idx];
276 shard.times.set(slot_idx, timestamp_ns);
277 set_val(&shard.values, slot_idx, val);
278
279 self.index_tracker = (self.index_tracker + 1) % self.capacity;
280 self.size_tracker = min(self.size_tracker + 1, self.capacity);
281
282 self.current_index.set(self.index_tracker as u64);
283 self.current_size.set(self.size_tracker as u64);
284 }
285}
286
287fn build_sharded_history_inspector<T>(
288 items: &[(i64, T)],
289 create_values: impl Fn(&inspect::Node, usize) -> InspectValueArray,
290 set_value: impl Fn(&InspectValueArray, usize, &T),
291) -> Inspector {
292 let inspector = Inspector::default();
293 let local_root = inspector.root();
294 let size = items.len();
295
296 local_root.record_uint("current_index", 0);
297 local_root.record_uint("current_size", size as u64);
298
299 let shards_node = local_root.create_child("shards");
300 let active_capacity = max(size, 1);
301 let num_shards = (active_capacity + SHARD_CAPACITY - 1) / SHARD_CAPACITY;
302
303 for s in 0..num_shards {
304 let shard_start_idx = s * SHARD_CAPACITY;
305 let shard_end_idx = min(shard_start_idx + SHARD_CAPACITY, size);
306 let shard_size =
307 if shard_end_idx > shard_start_idx { shard_end_idx - shard_start_idx } else { 0 };
308
309 let shard_node = shards_node.create_child(s.to_string());
310 let times_prop = shard_node.create_int_array("times", max(shard_size, 1));
311 let values_prop = create_values(&shard_node, max(shard_size, 1));
312
313 for i in 0..shard_size {
314 let (ts, val) = &items[shard_start_idx + i];
315 times_prop.set(i, *ts);
316 set_value(&values_prop, i, val);
317 }
318
319 shard_node.record(times_prop);
320 values_prop.record_property(&shard_node);
321 shards_node.record(shard_node);
322 }
323
324 local_root.record(shards_node);
325 inspector
326}
327
328fn setup_lazy_recording_backend<T, FCreate, FSet>(
329 node: &inspect::Node,
330 options: &RecorderOptions,
331 create_values: FCreate,
332 set_value: FSet,
333) -> Result<(RecorderHistory<T>, Option<PersistenceHandler<T>>), StateRecorderError>
334where
335 T: Copy + std::fmt::Debug + std::fmt::Display + std::str::FromStr + Send + Sync + 'static,
336 FCreate: Fn(&inspect::Node, usize) -> InspectValueArray + Send + Sync + 'static,
337 FSet: Fn(&InspectValueArray, usize, &T) + Send + Sync + Clone + 'static,
338{
339 let create_values_arc = Arc::new(create_values);
340
341 let shared_buffer = if let Some(config) = &options.persistence {
342 let (handler, buffer) = PersistenceHandler::new(config.clone(), options.capacity);
343
344 let prev_data = PersistenceHandler::<T>::read_log(&config.previous_path);
346 if !prev_data.is_empty() {
347 let data_arc = Arc::new(prev_data);
348 let create_values = create_values_arc.clone();
349 let set_value = set_value.clone();
350 node.record_lazy_child("previous_boot_history", move || {
351 let data = data_arc.clone();
352 let create_values = create_values.clone();
353 let set_value = set_value.clone();
354 async move {
355 Ok(build_sharded_history_inspector(
356 &data,
357 move |n, s| create_values(n, s),
358 &set_value,
359 ))
360 }
361 .boxed()
362 });
363 }
364 (Some(handler), buffer)
365 } else {
366 (None, Arc::new(Mutex::new(TimestampRingBuffer::<T>::with_capacity(options.capacity))))
367 };
368
369 let buffer_cloned = shared_buffer.1.clone();
371 node.record_lazy_child("reset_info", move || {
372 let history = buffer_cloned.clone();
373 async move {
374 let inspector = Inspector::default();
375 let node = inspector.root();
376 let (count, last_reset_ns) = history.lock().get_reset_info();
377 node.record_int("count", count as i64);
378 node.record_int("last_reset_ns", last_reset_ns);
379 Ok(inspector)
380 }
381 .boxed()
382 });
383
384 let buffer_cloned = shared_buffer.1.clone();
386 let create_values = create_values_arc;
387 node.record_lazy_child("history", move || {
388 let history = buffer_cloned.clone();
389 let create_values = create_values.clone();
390 let set_value = set_value.clone();
391 async move {
392 let items: Vec<(i64, T)> = history.lock().iter().collect();
393 Ok(build_sharded_history_inspector(&items, move |n, s| create_values(n, s), &set_value))
394 }
395 .boxed()
396 });
397
398 Ok((RecorderHistory::Lazy(shared_buffer.1), shared_buffer.0))
399}
400
401fn setup_eager_recording_backend<T, FCreate>(
402 node: &inspect::Node,
403 options: &RecorderOptions,
404 create_values: FCreate,
405) -> Result<(RecorderHistory<T>, Option<PersistenceHandler<T>>), StateRecorderError>
406where
407 T: Copy + std::fmt::Debug,
408 FCreate: Fn(&inspect::Node, usize) -> InspectValueArray + Send + Sync + 'static,
409{
410 if options.persistence.is_some() {
411 return Err(StateRecorderError::InvalidOptions(
412 "Persistence not supported in eager mode".to_string(),
413 ));
414 }
415
416 node.record_child("reset_info", |node| {
417 node.record_int("count", 0);
418 node.record_int("last_reset_ns", zx::BootInstant::get().into_nanos());
419 });
420
421 let eager_buffer = EagerShardedBuffer::new(node, options.capacity, create_values);
422 Ok((RecorderHistory::Eager(eager_buffer), None))
423}
424
425fn setup_recording_backend<T, FCreate, FSet>(
426 node: &inspect::Node,
427 options: &RecorderOptions,
428 create_values: FCreate,
429 set_value: FSet,
430) -> Result<(RecorderHistory<T>, Option<PersistenceHandler<T>>), StateRecorderError>
431where
432 T: Copy + std::fmt::Debug + std::fmt::Display + std::str::FromStr + Send + Sync + 'static,
433 FCreate: Fn(&inspect::Node, usize) -> InspectValueArray + Send + Sync + 'static,
434 FSet: Fn(&InspectValueArray, usize, &T) + Send + Sync + Clone + 'static,
435{
436 if options.lazy_record {
437 setup_lazy_recording_backend(node, options, create_values, set_value)
438 } else {
439 setup_eager_recording_backend(node, options, create_values)
440 }
441}
442
443pub trait RecordableEnum:
445 Copy + Debug + Display + Eq + Hash + IntoEnumIterator + Into<u64> + Send + Sync
446{
447}
448impl<T: Copy + Debug + Display + Eq + Hash + IntoEnumIterator + Into<u64> + Send + Sync>
449 RecordableEnum for T
450{
451}
452
453#[derive(Clone)]
456struct StateName {
457 trace_name: &'static CStr,
458 inspect_name: Arc<String>,
464}
465
466pub struct NamedU64StateRecorder {
470 manager: Arc<Mutex<StateRecorderManager>>,
471 name: String,
472 trace_category: &'static CStr,
473 state_names: HashMap<u64, StateName>,
474 history: RecorderHistory<u64>,
475 persistence: Option<PersistenceHandler<u64>>,
476 _root_node: inspect::Node,
477 vthread: ftrace::VThread<String>,
478 current_state_trace_name: Option<&'static CStr>,
479}
480
481impl std::fmt::Debug for NamedU64StateRecorder {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 f.debug_struct("NamedU64StateRecorder")
484 .field("metadata", &self.name)
485 .field("trace_category", &self.trace_category)
486 .field("history", &self.history)
487 .finish()
488 }
489}
490
491impl Drop for NamedU64StateRecorder {
492 fn drop(&mut self) {
493 self.manager.lock().unregister_name(&self.name);
494 }
495}
496
497impl NamedU64StateRecorder {
498 pub fn new(
509 name: String,
510 trace_category: &'static CStr,
511 state_names_map: HashMap<u64, String>,
512 options: RecorderOptions,
513 ) -> Result<Self, StateRecorderError> {
514 let manager = options.manager.clone().unwrap_or_else(|| SINGLETON_MANAGER.clone());
515 let node = register_with_manager(&manager, &name)?;
516
517 let mut state_names = HashMap::new();
520 for (value, name_str) in state_names_map {
521 let inspect_name = Arc::new(name_str);
522 let trace_name = lazy_static_cstr(&inspect_name)?;
523 state_names.insert(value, StateName { inspect_name, trace_name });
524 }
525
526 node.record_child("metadata", |metadata_node| {
527 metadata_node.record_string("format_version", FORMAT_VERSION);
528 metadata_node.record_string("name", &name);
529 metadata_node.record_string("type", "enum");
530 metadata_node.record_child("states", |states_node| {
531 for (state_value, state_name) in state_names.iter() {
532 states_node.record_uint(state_name.inspect_name.as_ref(), *state_value);
533 }
534 });
535 });
536
537 let create_values = |n: &inspect::Node, sz: usize| {
538 InspectValueArray::Uint(n.create_uint_array("values", sz))
539 };
540 let set_value = |arr: &InspectValueArray, idx: usize, val: &u64| {
541 if let InspectValueArray::Uint(a) = arr {
542 a.set(idx, *val);
543 }
544 };
545
546 let (history, persistence) =
547 setup_recording_backend(&node, &options, create_values, set_value)?;
548
549 let vthread = ftrace::VThread::new(name.clone(), ftrace::Id::new().into());
550
551 Ok(Self {
552 manager,
553 name,
554 trace_category,
555 state_names,
556 history,
557 persistence,
558 _root_node: node,
559 vthread,
560 current_state_trace_name: None,
561 })
562 }
563
564 fn get_state_name(&self, val: u64) -> StateName {
565 static UNKNOWN_NAME: LazyLock<StateName> = LazyLock::new(|| StateName {
566 trace_name: c"<Unknown>",
567 inspect_name: Arc::new("<Unknown>".to_string()),
568 });
569 self.state_names.get(&val).unwrap_or(&UNKNOWN_NAME).clone()
570 }
571
572 pub fn record(&mut self, val: u64) {
573 static CACHE: ftrace::trace_site_t = ftrace::trace_site_t::new(0);
574 let context = ftrace::TraceCategoryContext::acquire_cached(self.trace_category, &CACHE);
575
576 if let Some(context) = context.as_ref() {
577 if let Some(name) = self.current_state_trace_name {
578 ftrace::vthread_duration_end(context, &name, &self.vthread, &[]);
579 }
580 }
581
582 let StateName { trace_name, .. } = self.get_state_name(val);
583 self.current_state_trace_name = Some(trace_name);
584
585 if let Some(context) = context.as_ref() {
586 ftrace::vthread_duration_begin(context, &trace_name, &self.vthread, &[]);
587 }
588
589 let timestamp = zx::BootInstant::get().into_nanos();
590
591 if let Some(handler) = &mut self.persistence {
593 handler.append(timestamp, val);
595 } else {
596 match &mut self.history {
598 RecorderHistory::Eager(history) => {
599 history.record(timestamp, val, |arr, idx, v| {
600 if let InspectValueArray::Uint(a) = arr {
601 a.set(idx, v);
602 }
603 });
604 }
605 RecorderHistory::Lazy(history) => {
606 history.lock().insert(timestamp, val);
607 }
608 }
609 }
610 }
611}
612
613pub struct EnumStateRecorder<T: RecordableEnum> {
617 inner: NamedU64StateRecorder,
618 _phantom: PhantomData<T>,
619}
620
621impl<T: RecordableEnum> std::fmt::Debug for EnumStateRecorder<T> {
622 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
623 f.debug_struct("EnumStateRecorder").field("inner", &self.inner).finish()
624 }
625}
626
627impl<T: RecordableEnum + 'static> EnumStateRecorder<T> {
628 pub fn new(
639 name: String,
640 trace_category: &'static CStr,
641 options: RecorderOptions,
642 ) -> Result<Self, StateRecorderError> {
643 let mut map = HashMap::new();
644 for variant in T::iter() {
645 map.insert(variant.into(), variant.to_string());
646 }
647 let inner = NamedU64StateRecorder::new(name, trace_category, map, options)?;
648
649 Ok(Self { inner, _phantom: PhantomData })
650 }
651
652 pub fn record(&mut self, state_enum: T) {
653 self.inner.record(state_enum.into());
654 }
655}
656
657pub trait RecordableNumericType:
661 Copy + Debug + Display + FromStr + Sized + Send + Sync + 'static
662{
663 type TraceType: ftrace::ArgValue;
664
665 fn trace_value(&self) -> Self::TraceType;
666 fn record(&self, node: &inspect::Node, name: &str);
667 fn record_range(range: &(Self, Self), node: &inspect::Node);
668 fn create_array_property(node: &inspect::Node, name: &str, size: usize) -> InspectValueArray;
669 fn set_array_value(array: &InspectValueArray, index: usize, val: Self);
670}
671
672macro_rules! impl_recordable_numeric_type {
673 ($numeric_type:ty, $trace_type:ty, u64) => {
674 impl RecordableNumericType for $numeric_type {
675 type TraceType = $trace_type;
676
677 fn trace_value(&self) -> Self::TraceType {
678 *self as Self::TraceType
679 }
680 fn record(&self, node: &inspect::Node, name: &str) {
681 node.record_uint(name, *self as u64);
682 }
683 fn record_range(range: &(Self, Self), node: &inspect::Node) {
684 node.record_uint("min_inc", range.0 as u64);
685 node.record_uint("max_inc", range.1 as u64);
686 }
687 fn create_array_property(
688 node: &inspect::Node,
689 name: &str,
690 size: usize,
691 ) -> InspectValueArray {
692 InspectValueArray::Uint(node.create_uint_array(name, size))
693 }
694 fn set_array_value(array: &InspectValueArray, index: usize, val: Self) {
695 if let InspectValueArray::Uint(arr) = array {
696 arr.set(index, val as u64);
697 }
698 }
699 }
700 };
701 ($numeric_type:ty, $trace_type:ty, i64) => {
702 impl RecordableNumericType for $numeric_type {
703 type TraceType = $trace_type;
704
705 fn trace_value(&self) -> Self::TraceType {
706 *self as Self::TraceType
707 }
708 fn record(&self, node: &inspect::Node, name: &str) {
709 node.record_int(name, *self as i64);
710 }
711 fn record_range(range: &(Self, Self), node: &inspect::Node) {
712 node.record_int("min_inc", range.0 as i64);
713 node.record_int("max_inc", range.1 as i64);
714 }
715 fn create_array_property(
716 node: &inspect::Node,
717 name: &str,
718 size: usize,
719 ) -> InspectValueArray {
720 InspectValueArray::Int(node.create_int_array(name, size))
721 }
722 fn set_array_value(array: &InspectValueArray, index: usize, val: Self) {
723 if let InspectValueArray::Int(arr) = array {
724 arr.set(index, val as i64);
725 }
726 }
727 }
728 };
729 ($numeric_type:ty, $trace_type:ty, f64) => {
730 impl RecordableNumericType for $numeric_type {
731 type TraceType = $trace_type;
732
733 fn trace_value(&self) -> Self::TraceType {
734 *self as Self::TraceType
735 }
736 fn record(&self, node: &inspect::Node, name: &str) {
737 node.record_double(name, *self as f64);
738 }
739 fn record_range(range: &(Self, Self), node: &inspect::Node) {
740 node.record_double("min_inc", range.0 as f64);
741 node.record_double("max_inc", range.1 as f64);
742 }
743 fn create_array_property(
744 node: &inspect::Node,
745 name: &str,
746 size: usize,
747 ) -> InspectValueArray {
748 InspectValueArray::Double(node.create_double_array(name, size))
749 }
750 fn set_array_value(array: &InspectValueArray, index: usize, val: Self) {
751 if let InspectValueArray::Double(arr) = array {
752 arr.set(index, val as f64);
753 }
754 }
755 }
756 };
757}
758
759impl_recordable_numeric_type!(u8, u32, u64);
760impl_recordable_numeric_type!(u16, u32, u64);
761impl_recordable_numeric_type!(u32, u32, u64);
762impl_recordable_numeric_type!(u64, u64, u64);
763impl_recordable_numeric_type!(i8, i32, i64);
764impl_recordable_numeric_type!(i16, i32, i64);
765impl_recordable_numeric_type!(i32, i32, i64);
766impl_recordable_numeric_type!(i64, i64, i64);
767impl_recordable_numeric_type!(f32, f64, f64);
768impl_recordable_numeric_type!(f64, f64, f64);
769
770#[derive(Copy, Clone, Debug, PartialEq, Eq)]
776pub enum Units {
777 Amps(Option<DecimalPrefix>),
778 AmpHours(Option<DecimalPrefix>),
779 Hertz(Option<DecimalPrefix>),
780 Joules(Option<DecimalPrefix>),
781 Seconds(Option<DecimalPrefix>),
782 Watts(Option<DecimalPrefix>),
783 Volts(Option<DecimalPrefix>),
784 Celsius(Option<DecimalPrefix>),
785 Number(Option<DecimalPrefix>),
786 Percent,
787}
788
789impl Display for Units {
790 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
791 fn write_helper(
792 f: &mut std::fmt::Formatter<'_>,
793 prefix: &Option<DecimalPrefix>,
794 unit_str: &str,
795 ) -> std::fmt::Result {
796 match prefix {
797 Some(p) => write!(f, "{}{}", p, unit_str),
798 None => write!(f, "{}", unit_str),
799 }
800 }
801
802 match self {
803 Units::Amps(prefix) => write_helper(f, prefix, "A"),
804 Units::AmpHours(prefix) => write_helper(f, prefix, "AH"),
805 Units::Hertz(prefix) => write_helper(f, prefix, "Hz"),
806 Units::Joules(prefix) => write_helper(f, prefix, "J"),
807 Units::Seconds(prefix) => write_helper(f, prefix, "s"),
808 Units::Watts(prefix) => write_helper(f, prefix, "W"),
809 Units::Volts(prefix) => write_helper(f, prefix, "V"),
810 Units::Celsius(prefix) => write_helper(f, prefix, "C"),
811 Units::Number(prefix) => write_helper(f, prefix, "#"),
812 Units::Percent => write!(f, "%"),
813 }
814 }
815}
816
817#[derive(Copy, Clone, Debug, PartialEq, Eq)]
819pub enum DecimalPrefix {
820 Nano,
821 Micro,
822 Milli,
823 Centi,
824 Deci,
825 Kilo,
826 Mega,
827 Giga,
828}
829
830impl Display for DecimalPrefix {
831 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
832 match self {
833 DecimalPrefix::Nano => write!(f, "n"),
834 DecimalPrefix::Micro => write!(f, "u"),
835 DecimalPrefix::Milli => write!(f, "m"),
836 DecimalPrefix::Centi => write!(f, "c"),
837 DecimalPrefix::Deci => write!(f, "d"),
838 DecimalPrefix::Kilo => write!(f, "k"),
839 DecimalPrefix::Mega => write!(f, "M"),
840 DecimalPrefix::Giga => write!(f, "G"),
841 }
842 }
843}
844
845#[macro_export]
854macro_rules! units {
855 (Percent) => {
856 $crate::Units::Percent
857 };
858 ($base_unit:ident) => {
859 $crate::Units::$base_unit(None)
860 };
861 ($prefix:ident, $base_unit:ident) => {
862 $crate::Units::$base_unit(Some($crate::DecimalPrefix::$prefix))
863 };
864}
865
866#[derive(Clone, Debug)]
868pub struct PersistenceOptions {
869 name: String,
871 current_path: String,
873 previous_path: String,
875 rename_path: String,
877}
878
879impl PersistenceOptions {
880 pub fn new(name: impl Into<String>) -> Self {
882 let name = name.into();
883 Self {
884 current_path: format!("/data/{}.csv", name),
885 previous_path: format!("/tmp/{}.csv", name),
886 rename_path: format!("/data/{}.tmp", name),
887 name,
888 }
889 }
890
891 pub fn storage_dir(mut self, dir: &str) -> Self {
892 self.current_path = format!("{}/{}.csv", dir, self.name);
893 self.rename_path = format!("{}/{}.tmp", dir, self.name);
894 self
895 }
896
897 pub fn volatile_dir(mut self, dir: &str) -> Self {
898 self.previous_path = format!("{}/{}.csv", dir, self.name);
899 self
900 }
901
902 fn paths(&self) -> (&str, &str, &str) {
904 (&self.current_path, &self.previous_path, &self.rename_path)
905 }
906}
907
908struct PersistenceHandler<T: Copy> {
910 config: PersistenceOptions,
911 buffer: Arc<Mutex<TimestampRingBuffer<T>>>,
913}
914
915impl<T: Copy + FromStr + Display> PersistenceHandler<T> {
916 fn new(
917 config: PersistenceOptions,
918 capacity: usize,
919 ) -> (Self, Arc<Mutex<TimestampRingBuffer<T>>>) {
920 let (curr, prev, _) = config.paths();
921
922 Self::prepare_files(&curr, &prev);
924
925 let initial_data = Self::read_log(&curr);
927
928 let mut buffer = TimestampRingBuffer::with_capacity(capacity);
930 for (ts, val) in initial_data {
931 buffer.insert(ts, val);
932 }
933
934 let shared_buffer = Arc::new(Mutex::new(buffer));
935
936 (Self { config, buffer: shared_buffer.clone() }, shared_buffer)
937 }
938
939 fn prepare_files(curr_path: &str, prev_path: &str) {
943 if Path::new(prev_path).exists() {
944 log::warn!("Not moving history, {} already exists", prev_path);
947 return;
948 }
949
950 let Ok(content) = std::fs::read_to_string(curr_path).map_err(|e| {
952 log::info!("Could not read current history from {}, not moving: {}", curr_path, e);
953 }) else {
954 return;
955 };
956
957 if let Err(e) = std::fs::write(prev_path, &content) {
958 log::warn!("Could not write previous boot history to {}: {}", prev_path, e);
959 return;
960 }
961
962 if let Err(e) = std::fs::File::create(curr_path) {
963 log::warn!("Could not clear current boot history at {}: {}", curr_path, e);
964 }
965 }
966
967 fn flush(&self, buffer_guard: &TimestampRingBuffer<T>) {
968 let (curr, _, temp) = self.config.paths();
969 let try_write = || -> std::io::Result<()> {
970 let mut file =
971 OpenOptions::new().write(true).create(true).truncate(true).open(&temp)?;
972
973 for (ts, val) in buffer_guard.iter() {
975 writeln!(file, "{},{}", ts, val)?;
976 }
977
978 file.sync_data()?;
979 fs::rename(&temp, &curr)?;
980 Ok(())
981 };
982
983 if let Err(e) = try_write() {
984 log::error!("StateRecorder: Persist failed for {}: {:?}", self.config.name, e);
985 }
986 }
987
988 fn append(&mut self, timestamp: i64, value: T) {
990 let mut guard = self.buffer.lock();
991 guard.insert(timestamp, value);
992 self.flush(&guard);
993 }
994
995 fn read_log(path: &str) -> Vec<(i64, T)> {
997 let Ok(content) = fs::read_to_string(path) else {
998 return Vec::new();
999 };
1000 content
1001 .lines()
1002 .filter_map(|line| {
1003 let line = line.trim();
1004 let mut parts = line.splitn(2, ',');
1005 let ts = parts.next()?.trim().parse::<i64>().ok()?;
1006 let val = parts.next()?.trim().parse::<T>().ok()?;
1007 Some((ts, val))
1008 })
1009 .collect()
1010 }
1011}
1012
1013#[derive(Default)]
1015pub struct RecorderOptions {
1016 pub lazy_record: bool,
1018 pub capacity: usize,
1020 pub manager: Option<Arc<Mutex<StateRecorderManager>>>,
1024 pub persistence: Option<PersistenceOptions>,
1026}
1027
1028#[derive(Debug)]
1029enum RecorderHistory<T: Copy + Debug> {
1030 Eager(EagerShardedBuffer),
1031 Lazy(Arc<Mutex<TimestampRingBuffer<T>>>),
1032}
1033
1034#[derive(Debug)]
1035struct TimestampRingBuffer<T: Copy> {
1043 start_timestamp_ms: i64,
1045 last_timestamp_ms: i64,
1047 next_index: usize,
1049 offset_ms: Vec<i32>,
1051 data: Vec<T>,
1053 reset_count: u32,
1055 last_reset_ms: i64,
1057}
1058
1059const NANOSECONDS_PER_MILLISECOND: i64 = 1_000_000;
1060
1061fn ms_to_ns(ms: i64) -> i64 {
1062 ms * NANOSECONDS_PER_MILLISECOND
1063}
1064
1065fn ns_to_ms(ns: i64) -> i64 {
1066 ns / NANOSECONDS_PER_MILLISECOND
1067}
1068
1069impl<T: Copy> TimestampRingBuffer<T> {
1070 fn with_capacity(capacity: usize) -> Self {
1071 let now_ms = ns_to_ms(zx::BootInstant::get().into_nanos());
1072 Self {
1073 start_timestamp_ms: now_ms,
1074 last_timestamp_ms: now_ms,
1075 next_index: 0,
1076 offset_ms: Vec::with_capacity(capacity),
1077 data: Vec::with_capacity(capacity),
1078 reset_count: 0,
1079 last_reset_ms: now_ms,
1080 }
1081 }
1082
1083 fn insert(&mut self, timestamp_ns: i64, value: T) {
1084 if self.offset_ms.capacity() == 0 {
1085 return;
1086 }
1087 let timestamp_ms = ns_to_ms(timestamp_ns);
1088 let offset_ms = match i32::try_from(timestamp_ms - self.last_timestamp_ms) {
1090 Ok(offset_ms) => offset_ms,
1091 Err(_) => {
1092 self.offset_ms.clear();
1095 self.data.clear();
1096 self.start_timestamp_ms = timestamp_ms;
1097 self.next_index = 0;
1098 self.reset_count += 1;
1099 self.last_reset_ms = self.start_timestamp_ms;
1100 0
1101 }
1102 };
1103 if self.offset_ms.len() < self.offset_ms.capacity() {
1104 self.offset_ms.push(offset_ms);
1106 self.data.push(value);
1107 } else {
1108 self.start_timestamp_ms += self.offset_ms[self.next_index] as i64;
1111 self.offset_ms[self.next_index] = offset_ms;
1112 self.data[self.next_index] = value;
1113 }
1114 self.last_timestamp_ms = timestamp_ms;
1115 self.next_index = (self.next_index + 1) % self.offset_ms.capacity();
1116 }
1117
1118 fn get_reset_info(&self) -> (u32, i64) {
1120 (self.reset_count, ms_to_ns(self.last_reset_ms))
1121 }
1122
1123 fn iter(&self) -> TimestampRingBufferIter<'_, T> {
1126 TimestampRingBufferIter::new(self)
1127 }
1128}
1129
1130struct TimestampRingBufferIter<'a, T: Copy> {
1131 buffer: &'a TimestampRingBuffer<T>,
1132 index: usize,
1133 last_timestamp_ms: i64,
1134}
1135
1136impl<'a, T: Copy> TimestampRingBufferIter<'a, T> {
1137 fn new(buffer: &'a TimestampRingBuffer<T>) -> Self {
1138 Self { buffer, index: 0, last_timestamp_ms: buffer.start_timestamp_ms }
1139 }
1140}
1141
1142impl<T: Copy> Iterator for TimestampRingBufferIter<'_, T> {
1145 type Item = (i64, T);
1146
1147 fn next(&mut self) -> Option<(i64, T)> {
1148 if self.index >= self.buffer.offset_ms.len() {
1149 return None;
1150 }
1151 let index = (self.index + self.buffer.next_index) % self.buffer.offset_ms.len();
1153 let timestamp_ms = self.last_timestamp_ms + self.buffer.offset_ms[index] as i64;
1154 self.index += 1;
1155 self.last_timestamp_ms = timestamp_ms;
1156 Some((ms_to_ns(timestamp_ms), self.buffer.data[index]))
1157 }
1158}
1159
1160pub struct NumericStateRecorder<T: RecordableNumericType> {
1161 manager: Arc<Mutex<StateRecorderManager>>,
1162 name: String,
1163 trace_category: &'static CStr,
1164 trace_name: &'static CStr,
1165 units: String,
1166 history: RecorderHistory<T>,
1167 persistence: Option<PersistenceHandler<T>>,
1168 _root_node: inspect::Node,
1169 trace_id: ftrace::Id,
1170 _phantom: PhantomData<T>,
1171}
1172
1173impl<T: RecordableNumericType> NumericStateRecorder<T> {
1174 pub fn new(
1185 name: String,
1186 trace_category: &'static CStr,
1187 units: Units,
1188 range: Option<(T, T)>,
1189 options: RecorderOptions,
1190 ) -> Result<Self, StateRecorderError> {
1191 let manager = options.manager.clone().unwrap_or_else(|| SINGLETON_MANAGER.clone());
1192 let node = register_with_manager(&manager, &name)?;
1193
1194 let trace_name = lazy_static_cstr(&name)?;
1195 let units_str = format!("{}", units);
1196
1197 node.record_child("metadata", |metadata_node| {
1198 metadata_node.record_string("format_version", FORMAT_VERSION);
1199 metadata_node.record_string("name", &name);
1200 metadata_node.record_string("type", "numeric");
1201 metadata_node.record_string("units", &units_str);
1202 match range {
1203 Some(r) => metadata_node.record_child("range", |node| T::record_range(&r, node)),
1204 None => metadata_node.record_string("range", "<Unspecified>"),
1205 }
1206 });
1207
1208 let create_values =
1209 |n: &inspect::Node, sz: usize| T::create_array_property(n, "values", sz);
1210 let set_value = |arr: &InspectValueArray, idx: usize, val: &T| {
1211 T::set_array_value(arr, idx, *val);
1212 };
1213
1214 let (history, persistence) =
1215 setup_recording_backend(&node, &options, create_values, set_value)?;
1216
1217 Ok(Self {
1218 manager,
1219 name,
1220 trace_category,
1221 trace_name,
1222 units: units_str,
1223 history,
1224 persistence,
1225 _root_node: node,
1226 trace_id: ftrace::Id::new(),
1227 _phantom: PhantomData,
1228 })
1229 }
1230
1231 pub fn record(&mut self, state_value: T) {
1232 let timestamp = zx::BootInstant::get().into_nanos();
1233
1234 ftrace::counter!(
1235 self.trace_category,
1236 self.trace_name,
1237 self.trace_id.into(),
1238 &self.units.to_string() => state_value.trace_value()
1239 );
1240
1241 if let Some(handler) = &mut self.persistence {
1243 handler.append(timestamp, state_value);
1244 } else {
1245 match &mut self.history {
1246 RecorderHistory::Eager(history) => {
1247 history.record(timestamp, state_value, |arr, idx, val| {
1248 T::set_array_value(arr, idx, val);
1249 });
1250 }
1251 RecorderHistory::Lazy(history) => {
1252 history.lock().insert(timestamp, state_value);
1253 }
1254 }
1255 }
1256 }
1257}
1258
1259impl<T: RecordableNumericType> Drop for NumericStateRecorder<T> {
1260 fn drop(&mut self) {
1261 self.manager.lock().unregister_name(&self.name);
1262 }
1263}
1264
1265#[cfg(test)]
1266mod tests {
1267 use super::*;
1268 use diagnostics_assertions::{AnyIntProperty, AnyProperty, assert_data_tree};
1269 use fuchsia_inspect::Inspector;
1270 use strum_macros::{Display, EnumIter, EnumString};
1271 use test_case::test_case;
1272
1273 #[derive(Copy, Clone, Debug, Display, EnumIter, EnumString, Eq, PartialEq, Hash)]
1274 #[repr(u8)]
1275 enum SwitchState {
1276 OFF = 0,
1277 ON = 1,
1278 }
1279
1280 impl From<SwitchState> for u64 {
1281 fn from(value: SwitchState) -> Self {
1282 value as Self
1283 }
1284 }
1285
1286 #[fuchsia::test]
1287 async fn test_timestamp_ring_buffer() {
1288 let mut buffer = TimestampRingBuffer::<i32>::with_capacity(3);
1289 let start_ms = buffer.start_timestamp_ms;
1290
1291 let t1 = (ms_to_ns(start_ms + 1000), 1);
1292 let t2 = (ms_to_ns(start_ms + 900), 2);
1294 let t3 = (ms_to_ns(start_ms + 3000), 3);
1295
1296 buffer.insert(t1.0, t1.1);
1297 buffer.insert(t2.0, t2.1);
1298 buffer.insert(t3.0, t3.1);
1299
1300 assert_eq!(vec![t1, t2, t3], buffer.iter().collect::<Vec<_>>());
1301 assert_eq!((0, ms_to_ns(start_ms)), buffer.get_reset_info());
1302
1303 let t4 = (ms_to_ns(start_ms + 4000), 4);
1305 buffer.insert(t4.0, t4.1);
1306 assert_eq!(vec![t2, t3, t4], buffer.iter().collect::<Vec<_>>());
1307 assert_eq!((0, ms_to_ns(start_ms)), buffer.get_reset_info());
1308 }
1309
1310 #[fuchsia::test]
1311 async fn test_timestamp_ring_buffer_resets_on_maximum_offset() {
1312 let mut buffer = TimestampRingBuffer::<i32>::with_capacity(3);
1313 let start_ms = buffer.start_timestamp_ms;
1314
1315 const MAX_OFFSET_MS: i64 = i32::MAX as i64;
1316 let t1 = (ms_to_ns(start_ms + 1000), 1);
1317 let t2 = (t1.0 + ms_to_ns(MAX_OFFSET_MS), 2);
1318
1319 buffer.insert(t1.0, t1.1);
1320 buffer.insert(t2.0, t2.1);
1321
1322 assert_eq!(vec![t1, t2], buffer.iter().collect::<Vec<_>>());
1323 assert_eq!((0, ms_to_ns(start_ms)), buffer.get_reset_info());
1324
1325 let t3 = (t2.0 + ms_to_ns(MAX_OFFSET_MS + 1), 3);
1328 buffer.insert(t3.0, t3.1);
1329 assert_eq!(vec![t3], buffer.iter().collect::<Vec<_>>());
1330 assert_eq!((1, t3.0), buffer.get_reset_info());
1331 }
1332
1333 #[test_case(false; "eager")]
1334 #[test_case(true; "lazy")]
1335 #[fuchsia::test]
1336 async fn test_enum_off_on(lazy_record: bool) {
1337 let inspector = Inspector::default();
1338 let manager = StateRecorderManager::new(&inspector);
1339
1340 let mut recorder = EnumStateRecorder::new(
1341 "my_switch".into(),
1342 c"power_test",
1343 RecorderOptions {
1344 lazy_record,
1345 capacity: 10,
1346 manager: Some(manager),
1347 persistence: None,
1348 },
1349 )
1350 .unwrap();
1351
1352 recorder.record(SwitchState::OFF);
1353 recorder.record(SwitchState::ON);
1354 recorder.record(SwitchState::OFF);
1355 recorder.record(SwitchState::ON);
1356 if lazy_record {
1357 assert_data_tree!(inspector, root: {
1358 power_observability_state_recorders: {
1359 my_switch: {
1360 metadata: {
1361 format_version: "2.0",
1362 name: "my_switch",
1363 type: "enum",
1364 states: {
1365 "OFF": 0u64,
1366 "ON": 1u64,
1367 }
1368 },
1369 history: {
1370 current_index: 0u64,
1371 current_size: 4u64,
1372 shards: {
1373 "0": {
1374 times: AnyProperty,
1375 values: vec![0u64, 1u64, 0u64, 1u64],
1376 }
1377 }
1378 },
1379 reset_info: {
1380 count: 0,
1381 last_reset_ns: AnyIntProperty,
1382 }
1383 }
1384 }
1385 });
1386 } else {
1387 assert_data_tree!(inspector, root: {
1388 power_observability_state_recorders: {
1389 my_switch: {
1390 metadata: {
1391 format_version: "2.0",
1392 name: "my_switch",
1393 type: "enum",
1394 states: {
1395 "OFF": 0u64,
1396 "ON": 1u64,
1397 }
1398 },
1399 history: {
1400 current_index: 4u64,
1401 current_size: 4u64,
1402 shards: {
1403 "0": {
1404 times: AnyProperty,
1405 values: vec![0u64, 1u64, 0u64, 1u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64],
1406 }
1407 }
1408 },
1409 reset_info: {
1410 count: 0,
1411 last_reset_ns: AnyIntProperty,
1412 }
1413 }
1414 }
1415 });
1416 }
1417 }
1418
1419 #[test_case(false; "eager")]
1420 #[test_case(true; "lazy")]
1421 #[fuchsia::test]
1422 async fn test_multiple_recorders(lazy_record: bool) {
1423 #[derive(Copy, Clone, Debug, Display, EnumIter, EnumString, Eq, PartialEq, Hash)]
1424 #[repr(u8)]
1425 enum EnablementState {
1426 DISABLED = 0,
1427 ENABLED = 1,
1428 }
1429 impl From<EnablementState> for u64 {
1430 fn from(value: EnablementState) -> Self {
1431 value as Self
1432 }
1433 }
1434
1435 let inspector = Inspector::default();
1436 let manager = StateRecorderManager::new(&inspector);
1437
1438 let mut recorder_0 = EnumStateRecorder::new(
1439 "switch_0".into(),
1440 c"power_test",
1441 RecorderOptions {
1442 lazy_record,
1443 capacity: 10,
1444 manager: Some(manager.clone()),
1445 persistence: None,
1446 },
1447 )
1448 .unwrap();
1449 let mut recorder_1 = EnumStateRecorder::new(
1450 "switch_1".into(),
1451 c"power_test",
1452 RecorderOptions {
1453 lazy_record,
1454 capacity: 10,
1455 manager: Some(manager),
1456 persistence: None,
1457 },
1458 )
1459 .unwrap();
1460 recorder_0.record(SwitchState::OFF);
1461 recorder_0.record(SwitchState::ON);
1462 recorder_1.record(EnablementState::ENABLED);
1463 recorder_1.record(EnablementState::DISABLED);
1464
1465 if lazy_record {
1466 assert_data_tree!(inspector, root: {
1467 power_observability_state_recorders: {
1468 switch_0: {
1469 metadata: {
1470 format_version: "2.0",
1471 name: "switch_0",
1472 type: "enum",
1473 states: {
1474 "OFF": 0u64,
1475 "ON": 1u64,
1476 }
1477 },
1478 history: {
1479 current_index: 0u64,
1480 current_size: 2u64,
1481 shards: {
1482 "0": {
1483 times: AnyProperty,
1484 values: vec![0u64, 1u64],
1485 }
1486 }
1487 },
1488 reset_info: {
1489 count: 0,
1490 last_reset_ns: AnyIntProperty,
1491 }
1492 },
1493 switch_1: {
1494 metadata: {
1495 format_version: "2.0",
1496 name: "switch_1",
1497 type: "enum",
1498 states: {
1499 "DISABLED": 0u64,
1500 "ENABLED": 1u64,
1501 }
1502 },
1503 history: {
1504 current_index: 0u64,
1505 current_size: 2u64,
1506 shards: {
1507 "0": {
1508 times: AnyProperty,
1509 values: vec![1u64, 0u64],
1510 }
1511 }
1512 },
1513 reset_info: {
1514 count: 0,
1515 last_reset_ns: AnyIntProperty,
1516 }
1517 }
1518 }
1519 });
1520 } else {
1521 assert_data_tree!(inspector, root: {
1522 power_observability_state_recorders: {
1523 switch_0: {
1524 metadata: {
1525 format_version: "2.0",
1526 name: "switch_0",
1527 type: "enum",
1528 states: {
1529 "OFF": 0u64,
1530 "ON": 1u64,
1531 }
1532 },
1533 history: {
1534 current_index: 2u64,
1535 current_size: 2u64,
1536 shards: {
1537 "0": {
1538 times: AnyProperty,
1539 values: vec![0u64, 1u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64],
1540 }
1541 }
1542 },
1543 reset_info: {
1544 count: 0,
1545 last_reset_ns: AnyIntProperty,
1546 }
1547 },
1548 switch_1: {
1549 metadata: {
1550 format_version: "2.0",
1551 name: "switch_1",
1552 type: "enum",
1553 states: {
1554 "DISABLED": 0u64,
1555 "ENABLED": 1u64,
1556 }
1557 },
1558 history: {
1559 current_index: 2u64,
1560 current_size: 2u64,
1561 shards: {
1562 "0": {
1563 times: AnyProperty,
1564 values: vec![1u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64],
1565 }
1566 }
1567 },
1568 reset_info: {
1569 count: 0,
1570 last_reset_ns: AnyIntProperty,
1571 }
1572 }
1573 }
1574 });
1575 }
1576 }
1577
1578 #[test_case(false; "eager")]
1579 #[test_case(true; "lazy")]
1580 #[fuchsia::test]
1581 async fn test_enum_three_states(lazy_record: bool) {
1582 #[derive(Copy, Clone, Debug, Display, EnumIter, EnumString, Eq, PartialEq, Hash)]
1583 #[repr(u8)]
1584 enum FanSpeed {
1585 OFF = 0,
1586 LOW = 1,
1587 HIGH = 2,
1588 }
1589
1590 impl From<FanSpeed> for u64 {
1591 fn from(value: FanSpeed) -> Self {
1592 value as Self
1593 }
1594 }
1595
1596 let inspector = Inspector::default();
1597 let manager = StateRecorderManager::new(&inspector);
1598
1599 let mut recorder = EnumStateRecorder::new(
1600 "the_best_fan".into(),
1601 c"power_test",
1602 RecorderOptions {
1603 lazy_record,
1604 capacity: 10,
1605 manager: Some(manager),
1606 persistence: None,
1607 },
1608 )
1609 .unwrap();
1610
1611 recorder.record(FanSpeed::OFF);
1612 recorder.record(FanSpeed::LOW);
1613 recorder.record(FanSpeed::HIGH);
1614 recorder.record(FanSpeed::OFF);
1615 recorder.record(FanSpeed::HIGH);
1616 if lazy_record {
1617 assert_data_tree!(inspector, root: {
1618 power_observability_state_recorders: {
1619 the_best_fan: {
1620 metadata: {
1621 format_version: "2.0",
1622 name: "the_best_fan",
1623 type: "enum",
1624 states: {
1625 "OFF": 0u64,
1626 "LOW": 1u64,
1627 "HIGH": 2u64,
1628 }
1629 },
1630 history: {
1631 current_index: 0u64,
1632 current_size: 5u64,
1633 shards: {
1634 "0": {
1635 times: AnyProperty,
1636 values: vec![0u64, 1u64, 2u64, 0u64, 2u64],
1637 }
1638 }
1639 },
1640 reset_info: {
1641 count: 0,
1642 last_reset_ns: AnyIntProperty,
1643 }
1644 }
1645 }
1646 });
1647 } else {
1648 assert_data_tree!(inspector, root: {
1649 power_observability_state_recorders: {
1650 the_best_fan: {
1651 metadata: {
1652 format_version: "2.0",
1653 name: "the_best_fan",
1654 type: "enum",
1655 states: {
1656 "OFF": 0u64,
1657 "LOW": 1u64,
1658 "HIGH": 2u64,
1659 }
1660 },
1661 history: {
1662 current_index: 5u64,
1663 current_size: 5u64,
1664 shards: {
1665 "0": {
1666 times: AnyProperty,
1667 values: vec![0u64, 1u64, 2u64, 0u64, 2u64, 0u64, 0u64, 0u64, 0u64, 0u64],
1668 }
1669 }
1670 },
1671 reset_info: {
1672 count: 0,
1673 last_reset_ns: AnyIntProperty,
1674 }
1675 }
1676 }
1677 });
1678 }
1679 }
1680
1681 #[test_case(false; "eager")]
1682 #[test_case(true; "lazy")]
1683 #[fuchsia::test]
1684 async fn test_name_reuse_not_allowed(lazy_record: bool) {
1685 let inspector = Inspector::default();
1686 let manager = StateRecorderManager::new(&inspector);
1687
1688 let recorder = EnumStateRecorder::<SwitchState>::new(
1689 "my_switch".into(),
1690 c"power_test",
1691 RecorderOptions {
1692 lazy_record,
1693 capacity: 10,
1694 manager: Some(manager.clone()),
1695 persistence: None,
1696 },
1697 )
1698 .unwrap();
1699
1700 let result = EnumStateRecorder::<SwitchState>::new(
1702 "my_switch".into(),
1703 c"power_test",
1704 RecorderOptions {
1705 lazy_record,
1706 capacity: 10,
1707 manager: Some(manager.clone()),
1708 persistence: None,
1709 },
1710 );
1711 assert!(result.is_err());
1712
1713 drop(recorder);
1715 let result = EnumStateRecorder::<SwitchState>::new(
1716 "my_switch".into(),
1717 c"power_test",
1718 RecorderOptions {
1719 lazy_record,
1720 capacity: 10,
1721 manager: Some(manager.clone()),
1722 persistence: None,
1723 },
1724 );
1725 assert!(result.is_ok());
1726 }
1727
1728 #[test_case(false; "eager")]
1729 #[test_case(true; "lazy")]
1730 #[fuchsia::test]
1731 async fn test_zero_capacity(lazy_record: bool) {
1732 let inspector = Inspector::default();
1733 let manager = StateRecorderManager::new(&inspector);
1734 let mut recorder = EnumStateRecorder::<SwitchState>::new(
1735 "zero_cap_switch".into(),
1736 c"power_test",
1737 RecorderOptions { lazy_record, capacity: 0, manager: Some(manager), persistence: None },
1738 )
1739 .unwrap();
1740
1741 recorder.record(SwitchState::OFF);
1743 recorder.record(SwitchState::ON);
1744 }
1745
1746 #[test_case(false; "eager")]
1747 #[test_case(true; "lazy")]
1748 #[fuchsia::test]
1749 async fn test_multi_shard_and_wraparound(lazy_record: bool) {
1750 let inspector = Inspector::default();
1751 let manager = StateRecorderManager::new(&inspector);
1752 let capacity = SHARD_CAPACITY + 50;
1753 let mut recorder = EnumStateRecorder::<SwitchState>::new(
1754 "multi_shard_switch".into(),
1755 c"power_test",
1756 RecorderOptions { lazy_record, capacity, manager: Some(manager), persistence: None },
1757 )
1758 .unwrap();
1759
1760 for i in 0..220 {
1762 let state = if i % 2 == 0 { SwitchState::OFF } else { SwitchState::ON };
1763 recorder.record(state);
1764 }
1765
1766 let expected_shard_0: Vec<u64> = (0..SHARD_CAPACITY).map(|i| (i % 2) as u64).collect();
1767 let expected_shard_1: Vec<u64> = (SHARD_CAPACITY..220).map(|i| (i % 2) as u64).collect();
1768
1769 if lazy_record {
1770 assert_data_tree!(inspector, root: {
1771 power_observability_state_recorders: {
1772 multi_shard_switch: {
1773 metadata: {
1774 format_version: "2.0",
1775 name: "multi_shard_switch",
1776 type: "enum",
1777 states: {
1778 "OFF": 0u64,
1779 "ON": 1u64,
1780 }
1781 },
1782 history: {
1783 current_index: 0u64,
1784 current_size: 220u64,
1785 shards: {
1786 "0": {
1787 times: AnyProperty,
1788 values: expected_shard_0.clone(),
1789 },
1790 "1": {
1791 times: AnyProperty,
1792 values: expected_shard_1.clone(),
1793 }
1794 }
1795 },
1796 reset_info: {
1797 count: 0,
1798 last_reset_ns: AnyIntProperty,
1799 }
1800 }
1801 }
1802 });
1803 } else {
1804 let mut expected_eager_shard_1 = expected_shard_1.clone();
1805 expected_eager_shard_1.resize(50, 0u64);
1806 assert_data_tree!(inspector, root: {
1807 power_observability_state_recorders: {
1808 multi_shard_switch: {
1809 metadata: {
1810 format_version: "2.0",
1811 name: "multi_shard_switch",
1812 type: "enum",
1813 states: {
1814 "OFF": 0u64,
1815 "ON": 1u64,
1816 }
1817 },
1818 history: {
1819 current_index: 220u64,
1820 current_size: 220u64,
1821 shards: {
1822 "0": {
1823 times: AnyProperty,
1824 values: expected_shard_0.clone(),
1825 },
1826 "1": {
1827 times: AnyProperty,
1828 values: expected_eager_shard_1,
1829 }
1830 }
1831 },
1832 reset_info: {
1833 count: 0,
1834 last_reset_ns: AnyIntProperty,
1835 }
1836 }
1837 }
1838 });
1839 }
1840
1841 for i in 220..300 {
1843 let state = if i % 2 == 0 { SwitchState::OFF } else { SwitchState::ON };
1844 recorder.record(state);
1845 }
1846
1847 if lazy_record {
1848 let lazy_shard_0: Vec<u64> = (50..250).map(|i| (i % 2) as u64).collect();
1849 let lazy_shard_1: Vec<u64> = (250..300).map(|i| (i % 2) as u64).collect();
1850 assert_data_tree!(inspector, root: {
1851 power_observability_state_recorders: {
1852 multi_shard_switch: {
1853 metadata: {
1854 format_version: "2.0",
1855 name: "multi_shard_switch",
1856 type: "enum",
1857 states: {
1858 "OFF": 0u64,
1859 "ON": 1u64,
1860 }
1861 },
1862 history: {
1863 current_index: 0u64,
1864 current_size: 250u64,
1865 shards: {
1866 "0": {
1867 times: AnyProperty,
1868 values: lazy_shard_0,
1869 },
1870 "1": {
1871 times: AnyProperty,
1872 values: lazy_shard_1,
1873 }
1874 }
1875 },
1876 reset_info: {
1877 count: 0,
1878 last_reset_ns: AnyIntProperty,
1879 }
1880 }
1881 }
1882 });
1883 } else {
1884 let mut eager_shard_0: Vec<u64> = Vec::with_capacity(SHARD_CAPACITY);
1885 for i in 250..300 {
1886 eager_shard_0.push((i % 2) as u64);
1887 }
1888 for i in 50..200 {
1889 eager_shard_0.push((i % 2) as u64);
1890 }
1891 let eager_shard_1: Vec<u64> = (200..250).map(|i| (i % 2) as u64).collect();
1892
1893 assert_data_tree!(inspector, root: {
1894 power_observability_state_recorders: {
1895 multi_shard_switch: {
1896 metadata: {
1897 format_version: "2.0",
1898 name: "multi_shard_switch",
1899 type: "enum",
1900 states: {
1901 "OFF": 0u64,
1902 "ON": 1u64,
1903 }
1904 },
1905 history: {
1906 current_index: 50u64,
1907 current_size: 250u64,
1908 shards: {
1909 "0": {
1910 times: AnyProperty,
1911 values: eager_shard_0,
1912 },
1913 "1": {
1914 times: AnyProperty,
1915 values: eager_shard_1,
1916 }
1917 }
1918 },
1919 reset_info: {
1920 count: 0,
1921 last_reset_ns: AnyIntProperty,
1922 }
1923 }
1924 }
1925 });
1926 }
1927 }
1928
1929 #[test_case(false; "eager")]
1930 #[test_case(true; "lazy")]
1931 #[fuchsia::test]
1932 async fn test_singleton_manager(lazy_record: bool) {
1933 let mut recorder = EnumStateRecorder::new(
1934 "my_switch".into(),
1935 c"power_test",
1936 RecorderOptions { lazy_record, capacity: 10, manager: None, persistence: None },
1937 )
1938 .unwrap();
1939
1940 recorder.record(SwitchState::OFF);
1941 recorder.record(SwitchState::ON);
1942 if lazy_record {
1943 assert_data_tree!(inspect::component::inspector(), root: {
1944 power_observability_state_recorders: {
1945 my_switch: {
1946 metadata: {
1947 format_version: "2.0",
1948 name: "my_switch",
1949 type: "enum",
1950 states: {
1951 "OFF": 0u64,
1952 "ON": 1u64,
1953 }
1954 },
1955 history: {
1956 current_index: 0u64,
1957 current_size: 2u64,
1958 shards: {
1959 "0": {
1960 times: AnyProperty,
1961 values: vec![0u64, 1u64],
1962 }
1963 }
1964 },
1965 reset_info: {
1966 count: 0,
1967 last_reset_ns: AnyIntProperty,
1968 }
1969 }
1970 }
1971 });
1972 } else {
1973 assert_data_tree!(inspect::component::inspector(), root: {
1974 power_observability_state_recorders: {
1975 my_switch: {
1976 metadata: {
1977 format_version: "2.0",
1978 name: "my_switch",
1979 type: "enum",
1980 states: {
1981 "OFF": 0u64,
1982 "ON": 1u64,
1983 }
1984 },
1985 history: {
1986 current_index: 2u64,
1987 current_size: 2u64,
1988 shards: {
1989 "0": {
1990 times: AnyProperty,
1991 values: vec![0u64, 1u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64],
1992 }
1993 }
1994 },
1995 reset_info: {
1996 count: 0,
1997 last_reset_ns: AnyIntProperty,
1998 }
1999 }
2000 }
2001 });
2002 }
2003 }
2004
2005 #[fuchsia::test]
2006 async fn test_recorder_is_send() {
2007 fn assert_send<T: Send>() {}
2008 assert_send::<EnumStateRecorder<SwitchState>>();
2009 }
2010
2011 async fn test_uint_numeric_type<T: RecordableNumericType>(lazy_record: bool)
2012 where
2013 T: Into<u64> + From<u8>,
2014 {
2015 let inspector = Inspector::default();
2016 let manager = StateRecorderManager::new(&inspector);
2017 let mut recorder = NumericStateRecorder::new(
2018 "my_stateful_thing".into(),
2019 c"power_test",
2020 units!(Percent),
2021 Some((T::from(0), T::from(255))),
2022 RecorderOptions {
2023 lazy_record,
2024 capacity: 10,
2025 manager: Some(manager),
2026 persistence: None,
2027 },
2028 )
2029 .unwrap();
2030
2031 recorder.record(T::from(10));
2032 recorder.record(T::from(0));
2033 if lazy_record {
2034 assert_data_tree!(inspector, root: {
2035 power_observability_state_recorders: {
2036 my_stateful_thing: {
2037 metadata: {
2038 format_version: "2.0",
2039 name: "my_stateful_thing",
2040 type: "numeric",
2041 units: "%",
2042 range: {
2043 min_inc: 0u64,
2044 max_inc: 255u64
2045 },
2046 },
2047 history: {
2048 current_index: 0u64,
2049 current_size: 2u64,
2050 shards: {
2051 "0": {
2052 times: AnyProperty,
2053 values: vec![10u64, 0u64],
2054 }
2055 }
2056 },
2057 reset_info: {
2058 count: 0,
2059 last_reset_ns: AnyIntProperty,
2060 }
2061 }
2062 }
2063 });
2064 } else {
2065 assert_data_tree!(inspector, root: {
2066 power_observability_state_recorders: {
2067 my_stateful_thing: {
2068 metadata: {
2069 format_version: "2.0",
2070 name: "my_stateful_thing",
2071 type: "numeric",
2072 units: "%",
2073 range: {
2074 min_inc: 0u64,
2075 max_inc: 255u64
2076 },
2077 },
2078 history: {
2079 current_index: 2u64,
2080 current_size: 2u64,
2081 shards: {
2082 "0": {
2083 times: AnyProperty,
2084 values: vec![10u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64],
2085 }
2086 }
2087 },
2088 reset_info: {
2089 count: 0,
2090 last_reset_ns: AnyIntProperty,
2091 }
2092 }
2093 }
2094 });
2095 }
2096 }
2097
2098 #[test_case(false; "eager")]
2099 #[test_case(true; "lazy")]
2100 #[fuchsia::test]
2101 async fn test_uint_numeric_types(lazy_record: bool) {
2102 test_uint_numeric_type::<u8>(lazy_record).await;
2103 test_uint_numeric_type::<u16>(lazy_record).await;
2104 test_uint_numeric_type::<u32>(lazy_record).await;
2105 test_uint_numeric_type::<u64>(lazy_record).await;
2106 }
2107
2108 async fn test_int_numeric_type<T: RecordableNumericType>(lazy_record: bool)
2109 where
2110 T: Into<i64> + From<i8>,
2111 {
2112 let inspector = Inspector::default();
2113 let manager = StateRecorderManager::new(&inspector);
2114 let mut recorder = NumericStateRecorder::new(
2115 "my_stateful_thing".into(),
2116 c"power_test",
2117 units!(Number),
2118 Some((T::from(-128), T::from(127))),
2119 RecorderOptions {
2120 lazy_record,
2121 capacity: 10,
2122 manager: Some(manager),
2123 persistence: None,
2124 },
2125 )
2126 .unwrap();
2127
2128 recorder.record(T::from(10));
2129 recorder.record(T::from(0));
2130 if lazy_record {
2131 assert_data_tree!(inspector, root: {
2132 power_observability_state_recorders: {
2133 my_stateful_thing: {
2134 metadata: {
2135 format_version: "2.0",
2136 name: "my_stateful_thing",
2137 type: "numeric",
2138 units: "#",
2139 range: {
2140 min_inc: -128i64,
2141 max_inc: 127i64
2142 },
2143 },
2144 history: {
2145 current_index: 0u64,
2146 current_size: 2u64,
2147 shards: {
2148 "0": {
2149 times: AnyProperty,
2150 values: vec![10i64, 0i64],
2151 }
2152 }
2153 },
2154 reset_info: {
2155 count: 0,
2156 last_reset_ns: AnyIntProperty,
2157 }
2158 }
2159 }
2160 });
2161 } else {
2162 assert_data_tree!(inspector, root: {
2163 power_observability_state_recorders: {
2164 my_stateful_thing: {
2165 metadata: {
2166 format_version: "2.0",
2167 name: "my_stateful_thing",
2168 type: "numeric",
2169 units: "#",
2170 range: {
2171 min_inc: -128i64,
2172 max_inc: 127i64
2173 },
2174 },
2175 history: {
2176 current_index: 2u64,
2177 current_size: 2u64,
2178 shards: {
2179 "0": {
2180 times: AnyProperty,
2181 values: vec![10i64, 0i64, 0i64, 0i64, 0i64, 0i64, 0i64, 0i64, 0i64, 0i64],
2182 }
2183 }
2184 },
2185 reset_info: {
2186 count: 0,
2187 last_reset_ns: AnyIntProperty,
2188 }
2189 }
2190 }
2191 });
2192 }
2193 }
2194
2195 #[test_case(false; "eager")]
2196 #[test_case(true; "lazy")]
2197 #[fuchsia::test]
2198 async fn test_int_numeric_types(lazy_record: bool) {
2199 test_int_numeric_type::<i8>(lazy_record).await;
2200 test_int_numeric_type::<i16>(lazy_record).await;
2201 test_int_numeric_type::<i32>(lazy_record).await;
2202 test_int_numeric_type::<i64>(lazy_record).await;
2203 }
2204
2205 async fn test_float_numeric_type<T: RecordableNumericType>(lazy_record: bool)
2206 where
2207 T: Into<f64> + From<u8>,
2208 {
2209 let inspector = Inspector::default();
2210 let manager = StateRecorderManager::new(&inspector);
2211 let mut recorder = NumericStateRecorder::new(
2212 "my_stateful_thing".into(),
2213 c"power_test",
2214 units!(Kilo, Hertz),
2215 Some((T::from(0), T::from(255))),
2216 RecorderOptions {
2217 lazy_record,
2218 capacity: 10,
2219 manager: Some(manager),
2220 persistence: None,
2221 },
2222 )
2223 .unwrap();
2224
2225 recorder.record(T::from(10));
2226 recorder.record(T::from(0));
2227 if lazy_record {
2228 assert_data_tree!(inspector, root: {
2229 power_observability_state_recorders: {
2230 my_stateful_thing: {
2231 metadata: {
2232 format_version: "2.0",
2233 name: "my_stateful_thing",
2234 type: "numeric",
2235 units: "kHz",
2236 range: {
2237 min_inc: 0.0,
2238 max_inc: 255.0
2239 },
2240 },
2241 history: {
2242 current_index: 0u64,
2243 current_size: 2u64,
2244 shards: {
2245 "0": {
2246 times: AnyProperty,
2247 values: vec![10.0f64, 0.0f64],
2248 }
2249 }
2250 },
2251 reset_info: {
2252 count: 0,
2253 last_reset_ns: AnyIntProperty,
2254 }
2255 }
2256 }
2257 });
2258 } else {
2259 assert_data_tree!(inspector, root: {
2260 power_observability_state_recorders: {
2261 my_stateful_thing: {
2262 metadata: {
2263 format_version: "2.0",
2264 name: "my_stateful_thing",
2265 type: "numeric",
2266 units: "kHz",
2267 range: {
2268 min_inc: 0.0,
2269 max_inc: 255.0
2270 },
2271 },
2272 history: {
2273 current_index: 2u64,
2274 current_size: 2u64,
2275 shards: {
2276 "0": {
2277 times: AnyProperty,
2278 values: vec![10.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64],
2279 }
2280 }
2281 },
2282 reset_info: {
2283 count: 0,
2284 last_reset_ns: AnyIntProperty,
2285 }
2286 }
2287 }
2288 });
2289 }
2290 }
2291
2292 #[test_case(false; "eager")]
2293 #[test_case(true; "lazy")]
2294 #[fuchsia::test]
2295 async fn test_float_numeric_types(lazy_record: bool) {
2296 test_float_numeric_type::<f32>(lazy_record).await;
2297 test_float_numeric_type::<f64>(lazy_record).await;
2298 }
2299
2300 #[test_case(true; "lazy")]
2301 #[fuchsia::test]
2302 async fn test_persistence_crash_recovery(lazy_record: bool) {
2303 use std::fs;
2304 use tempfile::tempdir;
2305
2306 let dir = tempdir().unwrap();
2308 let storage_path = dir.path().join("data");
2309 let volatile_path = dir.path().join("tmp");
2310 fs::create_dir(&storage_path).unwrap();
2311 fs::create_dir(&volatile_path).unwrap();
2312
2313 let inspector = Inspector::default();
2314 let manager = StateRecorderManager::new(&inspector);
2315
2316 let create_options = |manager_ref| RecorderOptions {
2318 lazy_record, capacity: 10,
2320 manager: Some(manager_ref),
2321 persistence: Some(
2322 PersistenceOptions::new("crash_test".to_string())
2323 .storage_dir(storage_path.to_str().unwrap())
2324 .volatile_dir(volatile_path.to_str().unwrap()),
2325 ),
2326 };
2327
2328 {
2330 let mut recorder = EnumStateRecorder::<SwitchState>::new(
2331 "crash_test".into(),
2332 c"power_test",
2333 create_options(manager.clone()),
2334 )
2335 .unwrap();
2336
2337 recorder.record(SwitchState::ON);
2338 recorder.record(SwitchState::OFF);
2339
2340 }
2342
2343 let curr_csv = storage_path.join("crash_test.csv");
2345 let content = fs::read_to_string(curr_csv).unwrap();
2346 let lines: Vec<&str> = content.trim().lines().collect();
2348 assert_eq!(lines.len(), 2, "Expected 2 lines of recorded history");
2349
2350 let parts0: Vec<&str> = lines[0].split(',').collect();
2352 assert_eq!(parts0.len(), 2, "Invalid CSV format in line 1");
2353 assert_eq!(parts0[1], "1", "First record should be ON (1)");
2354
2355 let parts1: Vec<&str> = lines[1].split(',').collect();
2357 assert_eq!(parts1.len(), 2, "Invalid CSV format in line 2");
2358 assert_eq!(parts1[1], "0", "Second record should be OFF (0)");
2359
2360 let prev_csv = volatile_path.join("crash_test.csv");
2364 fs::write(&prev_csv, "").unwrap();
2365
2366 let mut recorder_restarted = EnumStateRecorder::<SwitchState>::new(
2369 "crash_test".into(),
2370 c"power_test",
2371 create_options(manager),
2372 )
2373 .unwrap();
2374
2375 assert_data_tree!(inspector, root: {
2377 power_observability_state_recorders: {
2378 crash_test: {
2379 metadata: {
2380 format_version: "2.0",
2381 name: "crash_test",
2382 type: "enum",
2383 states: {
2384 "OFF": 0u64,
2385 "ON": 1u64,
2386 }
2387 },
2388 history: {
2389 current_index: 0u64,
2390 current_size: 2u64,
2391 shards: {
2392 "0": {
2393 times: AnyProperty,
2394 values: vec![1u64, 0u64],
2395 }
2396 }
2397 },
2398 reset_info: {
2399 count: 0i64, last_reset_ns: AnyIntProperty,
2401 },
2402 }
2403 }
2404 });
2405
2406 recorder_restarted.record(SwitchState::ON);
2408 assert_data_tree!(inspector, root: {
2409 power_observability_state_recorders: {
2410 crash_test: {
2411 metadata: {
2412 format_version: "2.0",
2413 name: "crash_test",
2414 type: "enum",
2415 states: {
2416 "OFF": 0u64,
2417 "ON": 1u64,
2418 }
2419 },
2420 history: {
2421 current_index: 0u64,
2422 current_size: 3u64,
2423 shards: {
2424 "0": {
2425 times: AnyProperty,
2426 values: vec![1u64, 0u64, 1u64],
2427 }
2428 }
2429 },
2430 reset_info: {
2431 count: 0i64,
2432 last_reset_ns: AnyIntProperty,
2433 },
2434 }
2435 }
2436 });
2437 }
2438
2439 #[test_case(true; "lazy")]
2440 #[fuchsia::test]
2441 async fn test_persistence_reboot(lazy_record: bool) {
2442 use std::fs;
2443 use tempfile::tempdir;
2444
2445 let dir = tempdir().unwrap();
2447 let storage_path = dir.path().join("data");
2448 let volatile_path = dir.path().join("tmp");
2449 fs::create_dir(&storage_path).unwrap();
2450 fs::create_dir(&volatile_path).unwrap();
2451
2452 let inspector = Inspector::default();
2453 let manager = StateRecorderManager::new(&inspector);
2454
2455 let create_options = |manager_ref| RecorderOptions {
2457 lazy_record,
2458 capacity: 10,
2459 manager: Some(manager_ref),
2460 persistence: Some(
2461 PersistenceOptions::new("reboot_test".to_string())
2462 .storage_dir(storage_path.to_str().unwrap())
2463 .volatile_dir(volatile_path.to_str().unwrap()),
2464 ),
2465 };
2466
2467 let curr_csv = storage_path.join("reboot_test.csv");
2471 fs::write(&curr_csv, "1000,1\n2000,0\n").unwrap();
2473
2474 let prev_csv = volatile_path.join("reboot_test.csv");
2476 assert!(!prev_csv.exists());
2477
2478 let mut recorder = EnumStateRecorder::<SwitchState>::new(
2480 "reboot_test".into(),
2481 c"power_test",
2482 create_options(manager),
2483 )
2484 .unwrap();
2485
2486 assert!(prev_csv.exists(), "Library should have rotated curr -> prev");
2489 assert!(curr_csv.exists(), "Library should have create a new current file");
2490
2491 let rotated_content = fs::read_to_string(&prev_csv).unwrap();
2492 assert_eq!(rotated_content, "1000,1\n2000,0\n");
2493
2494 assert_data_tree!(inspector, root: {
2496 power_observability_state_recorders: {
2497 reboot_test: {
2498 metadata: {
2499 format_version: "2.0",
2500 name: "reboot_test",
2501 type: "enum",
2502 states: {
2503 "OFF": 0u64,
2504 "ON": 1u64,
2505 }
2506 },
2507 previous_boot_history: {
2509 current_index: 0u64,
2510 current_size: 2u64,
2511 shards: {
2512 "0": {
2513 times: vec![1000i64, 2000i64],
2514 values: vec![1u64, 0u64],
2515 }
2516 }
2517 },
2518 history: {
2520 current_index: 0u64,
2521 current_size: 0u64,
2522 shards: {
2523 "0": {
2524 times: vec![0i64],
2525 values: vec![0u64],
2526 }
2527 }
2528 },
2529 reset_info: {
2530 count: 0i64,
2531 last_reset_ns: AnyIntProperty,
2532 },
2533 }
2534 }
2535 });
2536
2537 recorder.record(SwitchState::ON);
2539 recorder.record(SwitchState::OFF);
2540 assert_data_tree!(inspector, root: {
2541 power_observability_state_recorders: {
2542 reboot_test: {
2543 metadata: {
2544 format_version: "2.0",
2545 name: "reboot_test",
2546 type: "enum",
2547 states: {
2548 "OFF": 0u64,
2549 "ON": 1u64,
2550 }
2551 },
2552 previous_boot_history: {
2554 current_index: 0u64,
2555 current_size: 2u64,
2556 shards: {
2557 "0": {
2558 times: vec![1000i64, 2000i64],
2559 values: vec![1u64, 0u64],
2560 }
2561 }
2562 },
2563 history: {
2565 current_index: 0u64,
2566 current_size: 2u64,
2567 shards: {
2568 "0": {
2569 times: AnyProperty,
2570 values: vec![1u64, 0u64],
2571 }
2572 }
2573 },
2574 reset_info: {
2575 count: 0i64,
2576 last_reset_ns: AnyIntProperty,
2577 },
2578 }
2579 }
2580 });
2581 }
2582
2583 #[test_case(false; "eager")]
2584 #[test_case(true; "lazy")]
2585 #[fuchsia::test]
2586 async fn test_named_u64_recorder(lazy_record: bool) {
2587 use std::fs;
2588 use tempfile::tempdir;
2589
2590 let dir = tempdir().unwrap();
2592 let storage_path = dir.path().join("data");
2593 let volatile_path = dir.path().join("tmp");
2594 fs::create_dir(&storage_path).unwrap();
2595 fs::create_dir(&volatile_path).unwrap();
2596
2597 let inspector = Inspector::default();
2598 let manager = StateRecorderManager::new(&inspector);
2599
2600 let mut map = HashMap::new();
2601 map.insert(100, "Hundred".to_string());
2602 map.insert(200, "TwoHundred".to_string());
2603
2604 let persistence_opts = if lazy_record {
2605 Some(
2606 PersistenceOptions::new("my_u64_metrics_p".to_string())
2607 .storage_dir(storage_path.to_str().unwrap())
2608 .volatile_dir(volatile_path.to_str().unwrap()),
2609 )
2610 } else {
2611 None
2612 };
2613
2614 let mut recorder = NamedU64StateRecorder::new(
2616 "my_u64_metrics_p".into(),
2617 c"power_test",
2618 map.clone(),
2619 RecorderOptions {
2620 lazy_record,
2621 capacity: 10,
2622 manager: Some(manager.clone()),
2623 persistence: persistence_opts.clone(),
2624 },
2625 )
2626 .unwrap();
2627
2628 recorder.record(100);
2629 recorder.record(200);
2630 recorder.record(300); if lazy_record {
2634 drop(recorder); let curr_csv = storage_path.join("my_u64_metrics_p.csv");
2637 let content = fs::read_to_string(&curr_csv).unwrap();
2638 let lines: Vec<&str> = content.trim().lines().collect();
2639 assert_eq!(lines.len(), 3, "Expected 3 lines of recorded history");
2640
2641 let parts0: Vec<&str> = lines[0].split(',').collect();
2643 assert_eq!(parts0.len(), 2, "Invalid CSV format in line 1");
2644 assert_eq!(parts0[1], "100", "First record should be 100");
2645
2646 let parts1: Vec<&str> = lines[1].split(',').collect();
2648 assert_eq!(parts1.len(), 2, "Invalid CSV format in line 2");
2649 assert_eq!(parts1[1], "200", "Second record should be 200");
2650
2651 let parts2: Vec<&str> = lines[2].split(',').collect();
2653 assert_eq!(parts2.len(), 2, "Invalid CSV format in line 3");
2654 assert_eq!(parts2[1], "300", "Third record should be 300");
2655
2656 let mut _recorder_restarted = NamedU64StateRecorder::new(
2658 "my_u64_metrics_p".into(),
2659 c"power_test",
2660 map,
2661 RecorderOptions {
2662 lazy_record,
2663 capacity: 10,
2664 manager: Some(manager),
2665 persistence: persistence_opts,
2666 },
2667 )
2668 .unwrap();
2669
2670 assert_data_tree!(inspector, root: {
2671 power_observability_state_recorders: {
2672 my_u64_metrics_p: {
2673 metadata: {
2674 format_version: "2.0",
2675 name: "my_u64_metrics_p",
2676 type: "enum",
2677 states: {
2678 "Hundred": 100u64,
2679 "TwoHundred": 200u64,
2680 }
2681 },
2682 previous_boot_history: {
2683 current_index: 0u64,
2684 current_size: 3u64,
2685 shards: {
2686 "0": {
2687 times: AnyProperty,
2688 values: vec![100u64, 200u64, 300u64],
2689 }
2690 }
2691 },
2692 history: {
2693 current_index: 0u64,
2694 current_size: 0u64,
2695 shards: {
2696 "0": {
2697 times: vec![0i64],
2698 values: vec![0u64],
2699 }
2700 }
2701 },
2702 reset_info: {
2703 count: 0,
2704 last_reset_ns: AnyIntProperty,
2705 }
2706 }
2707 }
2708 });
2709 } else {
2710 assert_data_tree!(inspector, root: {
2713 power_observability_state_recorders: {
2714 my_u64_metrics_p: {
2715 metadata: {
2716 format_version: "2.0",
2717 name: "my_u64_metrics_p",
2718 type: "enum",
2719 states: {
2720 "Hundred": 100u64,
2721 "TwoHundred": 200u64,
2722 }
2723 },
2724 history: {
2725 current_index: 3u64,
2726 current_size: 3u64,
2727 shards: {
2728 "0": {
2729 times: AnyProperty,
2730 values: vec![100u64, 200u64, 300u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64],
2731 }
2732 }
2733 },
2734 reset_info: {
2735 count: 0,
2736 last_reset_ns: AnyIntProperty,
2737 }
2738 }
2739 }
2740 });
2741 }
2742 }
2743
2744 #[test_case(true; "lazy")]
2745 #[fuchsia::test]
2746 async fn test_numeric_persistence_reboot(lazy_record: bool) {
2747 use std::fs;
2748 use tempfile::tempdir;
2749
2750 let dir = tempdir().unwrap();
2752 let storage_path = dir.path().join("data");
2753 let volatile_path = dir.path().join("tmp");
2754 fs::create_dir(&storage_path).unwrap();
2755 fs::create_dir(&volatile_path).unwrap();
2756
2757 let inspector = Inspector::default();
2758 let manager = StateRecorderManager::new(&inspector);
2759
2760 let create_options = |manager_ref| RecorderOptions {
2761 lazy_record,
2762 capacity: 10,
2763 manager: Some(manager_ref),
2764 persistence: Some(
2765 PersistenceOptions::new("num_reboot_test".to_string())
2766 .storage_dir(storage_path.to_str().unwrap())
2767 .volatile_dir(volatile_path.to_str().unwrap()),
2768 ),
2769 };
2770
2771 let curr_csv = storage_path.join("num_reboot_test.csv");
2775 fs::write(&curr_csv, "1000,42\n2000,100\n").unwrap();
2777
2778 let prev_csv = volatile_path.join("num_reboot_test.csv");
2779 assert!(!prev_csv.exists());
2780
2781 let mut _recorder = NumericStateRecorder::new(
2783 "num_reboot_test".into(),
2784 c"power_test",
2785 units!(Number),
2786 Some((0u64, 200u64)),
2787 create_options(manager),
2788 )
2789 .unwrap();
2790
2791 assert!(prev_csv.exists(), "Library should have rotated curr -> prev");
2794 assert!(curr_csv.exists(), "Library should have create a new current file");
2795
2796 let rotated_content = fs::read_to_string(&prev_csv).unwrap();
2797 assert_eq!(rotated_content, "1000,42\n2000,100\n");
2798
2799 assert_data_tree!(inspector, root: {
2801 power_observability_state_recorders: {
2802 num_reboot_test: {
2803 metadata: {
2804 format_version: "2.0",
2805 name: "num_reboot_test",
2806 type: "numeric",
2807 units: "#",
2808 range: {
2809 min_inc: 0u64,
2810 max_inc: 200u64,
2811 }
2812 },
2813 previous_boot_history: {
2814 current_index: 0u64,
2815 current_size: 2u64,
2816 shards: {
2817 "0": {
2818 times: vec![1000i64, 2000i64],
2819 values: vec![42u64, 100u64],
2820 }
2821 }
2822 },
2823 history: {
2824 current_index: 0u64,
2825 current_size: 0u64,
2826 shards: {
2827 "0": {
2828 times: vec![0i64],
2829 values: vec![0u64],
2830 }
2831 }
2832 },
2833 reset_info: {
2834 count: 0i64,
2835 last_reset_ns: AnyIntProperty,
2836 }
2837 }
2838 }
2839 });
2840 }
2841}