Skip to main content

state_recorder/
lib.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Standardized reporting of time series data via Inspect and trace. It supports recording of
6//! **enum states** and **numeric states**.
7//!
8//! For example use, see the [example code][strc].
9//!
10//! For the intro to the library, see the [README.md][rdme].
11//!
12//! [rdme]: https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/power/state_recorder/README.md
13//! [strc]: https://cs.opensource.google/fuchsia/fuchsia/+/main:examples/power/state_recorder
14//!
15
16use 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
37/// Lazily creates &'static CStr values.
38///
39/// Each reference is backed by a CString value that is leaked (and can thus never be deallocated)
40/// to achieve static lifetime. Each value is indexed by its corresponding `str` value, so a given
41/// value will only be created once.
42///
43/// Errors:
44///  - StateRecorderError::IncompatibleString: The provided string could not be converted to a
45///    CString.
46fn lazy_static_cstr(s: &str) -> Result<&'static CStr, StateRecorderError> {
47    let mut pool = CSTR_POOL.lock();
48
49    // If the string is already in our pool, return the existing CStr.
50    if let Some(existing_cstr) = pool.get(s) {
51        return Ok(existing_cstr);
52    }
53
54    // Create the CString and leak it in a box to give it static lifetime.
55    let c_string = CString::new(s)
56        .map_err(|_| StateRecorderError::IncompatibleString(s.to_owned()))?
57        .into_boxed_c_str();
58
59    // We are going to leak the `c_string`, which may trip up the LeakSanitizer. So we need to
60    // explicitly disable and enable when we're running in the sanitizer variant.
61    //
62    // Note that the variant is named variant_asan (for AddressSanitizer), but the specific
63    // sanitizer we are targeting is lsan (LeakSanitizer), which is enabled as part of the asan
64    // variant.
65    #[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
103// StateRecorderManager for use with the singleton inspector.
104static 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
121/// Manages the parent node shared by StateRecorder instances, providing protection against name
122/// collisions.
123pub struct StateRecorderManager {
124    pub node: inspect::Node,
125    // Represents a set, but implemented using a Vec due to expected small number of elements.
126    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
157// Helpers for sharing logic between Recorders
158fn 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
169/// The number of entries per Inspect array shard.
170///
171/// In Inspect VMO format, the maximum allocation block size is Order 7 (2048 bytes).
172/// Subtracting an 8-byte block header and an 8-byte array metadata header leaves 2032 bytes
173/// for elements. For 64-bit entries (8 bytes each), a single array block can hold at most
174/// 254 slots ((2048 - 16) / 8 = 254). 200 is chosen as a round capacity safely under this limit
175/// so that each shard fits within a single max-order Inspect VMO block.
176pub 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        // Handle Previous Boot Node
345        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    // reset_info
370    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    // history
385    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
443/// Supertrait that combines traits an enum type must satisfy to be compatible with StateRecorder.
444pub 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// To simplify lookups, StateRecorder stores each state name as both CStr (for tracing) and
454// String (for Inspect).
455#[derive(Clone)]
456struct StateName {
457    trace_name: &'static CStr,
458    // This is wrapped in an Arc so that StateRecorder can clone a reference to it that is separated
459    // from a borrow of `self`.
460    //
461    // The alternative -- while preserving `Send` for StateRecorder -- would be to wrap
462    // StateRecorder::trace_state_event and StateRecorder::history in Mutexes.
463    inspect_name: Arc<String>,
464}
465
466/// Records time series data for an named-u64 value state. This is best-suited for categorical
467/// observations, where the name of the state and not a numeric value will be most relevant for
468/// diagnostic and forensic purposes.
469pub 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    /// Creates a new NamedU64StateRecorder with a given name and a map of u64 to state names.
499    ///
500    /// See `RecorderOptions` for more details on options that can be specified.
501    ///
502    /// Errors:
503    ///   - StateRecorderError::DuplicateName: `metadata.name` is already in use by a StateRecorder
504    ///     associated with `manager`.
505    ///   - StateRecorderError::IncompatibleString: Either `name` or the display name of a state
506    ///     cannot be converted to a CString.
507    ///   - StateRecorderError::InvalidOptions: `options` is invalid for the given mode.
508    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        // Build up the map of u64 to state names, returning an error if any name is not a valid
518        // str.
519        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 Persistence is on (Lazy), the handler OWNS the buffer update.
592        if let Some(handler) = &mut self.persistence {
593            // Updates the shared buffer inside its lock and handle persistence.
594            handler.append(timestamp, val);
595        } else {
596            // Update manually
597            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
613/// Records time series data for an enum-valued state. This is best-suited for categorical
614/// observations, where the name of the state and not a numeric value will be most relevant for
615/// diagnostic and forensic purposes.
616pub 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    /// Creates a new EnumStateRecorder with a given name.
629    ///
630    /// See `RecorderOptions` for more details on options that can be specified.
631    ///
632    /// Errors:
633    ///   - StateRecorderError::DuplicateName: `metadata.name` is already in use by a StateRecorder
634    ///     associated with `manager`.
635    ///   - StateRecorderError::IncompatibleString: Either `name` or the display name of a state
636    ///     cannot be converted to a CString.
637    ///   - StateRecorderError::InvalidOptions: `options` is invalid for the given mode.
638    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
657/// To be recordable, a numeric type must, in essence, be able to widen into a trace-compatible
658/// type and an Inspect-compatible type. Users are not expected to implement this trait; this
659/// module implements it for common numeric types below.
660pub 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/// Units supported by NumericStateRecorder. The `units!` macro is recommended for construction.
771///
772/// Bytes and bit-rates are specifically not included yet because they invite the question of
773/// whether they should be restricted to binary prefixes. We'll address that once we instrument a
774/// specific use case.
775#[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/// Decimal prefixes for use with certain `Units`.
818#[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/// Assembles fully-specified measurement units for NumericStateRecorder, combining a base unit
846/// with an optional prefix.
847///
848/// Examples:
849///     - units!(Volt)
850///     - units!(Percent)
851///     - units!(Kilo, Hertz)
852///     - units!(Milli, Amp)
853#[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// Holds information for persistence
867#[derive(Clone, Debug)]
868pub struct PersistenceOptions {
869    /// Unique name for this recorder (e.g., "battery_level").
870    name: String,
871    /// For current history.
872    current_path: String,
873    /// For previous history.
874    previous_path: String,
875    // A temporary name for current history to achieve atomic persistence.
876    rename_path: String,
877}
878
879impl PersistenceOptions {
880    // Unique name and path to storage and path to volatile directory.
881    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    // Helper to generate paths
903    fn paths(&self) -> (&str, &str, &str) {
904        (&self.current_path, &self.previous_path, &self.rename_path)
905    }
906}
907
908/// Handles persistence using TimestampRingBuffer as the backing store to save memory.
909struct PersistenceHandler<T: Copy> {
910    config: PersistenceOptions,
911    // We reuse TimestampRingBuffer for memory-optimized storage (i32 offsets)
912    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        // Perform rotation before loading data
923        Self::prepare_files(&curr, &prev);
924
925        // Load any data remaining in 'current' (crash recovery)
926        let initial_data = Self::read_log(&curr);
927
928        // 3. Hydrate our internal ring buffer
929        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    /// Handles the rotation logic:
940    /// If PREV doesn't exist (reboot), move CURR to PREV.
941    /// If PREV exists (crash), leave CURR alone (it contains valuable pre-crash data).
942    fn prepare_files(curr_path: &str, prev_path: &str) {
943        if Path::new(prev_path).exists() {
944            // Previous file exists -> Crash recovery.
945            // Do not overwrite it. Do not touch current file.
946            log::warn!("Not moving history, {} already exists", prev_path);
947            return;
948        }
949
950        // Move content by reading then writing it for moves from /data to /tmp.
951        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            // Iterate the ring buffer (which converts internal 32-bit offsets back to 64-bit TS)
974            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    /// Appends data to memory and syncs to disk.
989    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    /// Static helper to read log from disk into a vector.
996    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/// Options for NumericStateRecorder and EnumStateRecorder
1014#[derive(Default)]
1015pub struct RecorderOptions {
1016    // If true, recorder will lazily record values to inspect. Otherwise, will record eagerly.
1017    pub lazy_record: bool,
1018    /// Maximum number of recorded values to store on a rolling basis.
1019    pub capacity: usize,
1020    /// Optional. If not set, the Recorder will be linked to this module's singleton
1021    /// StateRecorderManager, which in turn corresponds to the singleton Inspector.
1022    /// If set, the manager supplied here will be used.
1023    pub manager: Option<Arc<Mutex<StateRecorderManager>>>,
1024    // Optional persistence config
1025    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)]
1035/// A fixed-size ring buffer with timestamps for each insertion.
1036/// All input and output are in nanoseconds, but will be rounded down to
1037/// the nearest millisecond and stored as milliseconds internally.
1038/// When the capacity is reached, insertions will wrap around and continue
1039/// from the beginning of the buffer. There is a maximum delta of ~24.8 days
1040/// between insertions. If this maximum is exceeded, the buffer will drop
1041/// all data except for the newest insertion.
1042struct TimestampRingBuffer<T: Copy> {
1043    /// Initial timestamp in milliseconds, used as basis for offsets.
1044    start_timestamp_ms: i64,
1045    /// Last timestamp inserted, in milliseconds.
1046    last_timestamp_ms: i64,
1047    /// Index where the next element should be inserted.
1048    next_index: usize,
1049    /// Store timestamps as millisecond offsets from `last_timestamp_ms`.
1050    offset_ms: Vec<i32>,
1051    /// Data to be stored in the buffer.
1052    data: Vec<T>,
1053    /// Number of times the buffer has been reset (due to max delta exceeded).
1054    reset_count: u32,
1055    /// Timestamp of the last buffer reset
1056    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        // Attempt to down-convert the offset from last_timestamp_ms to an i32
1089        let offset_ms = match i32::try_from(timestamp_ms - self.last_timestamp_ms) {
1090            Ok(offset_ms) => offset_ms,
1091            Err(_) => {
1092                // Offset from last_timestamp_ms exceeds maximum allowable,
1093                // reset the buffer.
1094                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            // Buffer isn't full yet, just append.
1105            self.offset_ms.push(offset_ms);
1106            self.data.push(value);
1107        } else {
1108            // Buffer is full, shift `start_timestamp_ms` forward by the oldest
1109            // offset, then overwrite that entry with the new data.
1110            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    /// Returns the reset count, and the timestamp of the last reset in nanoseconds.
1119    fn get_reset_info(&self) -> (u32, i64) {
1120        (self.reset_count, ms_to_ns(self.last_reset_ms))
1121    }
1122
1123    /// Returns an Iterator of (timestamp in nanoseconds, T), starting
1124    /// from the oldest entry.
1125    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
1142/// Iterate over the wrapped buffer, returning (timestamp in nanoseconds, T),
1143/// starting from the oldest entry.
1144impl<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        // Start from the oldest insertion and wrap around.
1152        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    /// Creates a new NumericStateRecorder.
1175    ///
1176    /// See `RecorderOptions` for more details on options that can be specified.
1177    ///
1178    /// Errors:
1179    ///   - StateRecorderError::DuplicateName: `metadata.name` is already in use by a StateRecorder
1180    ///     associated with `manager`.
1181    ///   - StateRecorderError::IncompatibleString: Either `name` or the display name of a state
1182    ///     cannot be converted to a CString.
1183    ///   - StateRecorderError::InvalidOptions: `options` is invalid for the given mode.
1184    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 Persistence is on (Lazy), the handler OWNS the shared buffer update.
1242        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        // t2's timestamp is before t1, which will result in a negative offset.
1293        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        // Buffer is already at capacity, so this should overwrite the first element.
1304        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        // This should exceed the maximum allowable timestamp offset,
1326        // causing the buffer to reset.
1327        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        // While `recorder` is still in scope, its name cannot be reused.
1701        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        // After `recorder` is dropped, its name can be used again.
1714        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        // Recording with 0 capacity should not panic.
1742        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        // 1. Record 220 items to cross SHARD_CAPACITY (200) into the second shard.
1761        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        // 2. Record items 220..300 (total 300 into capacity 250) to trigger ring buffer wraparound.
1842        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        // 1. Setup isolated environment
2307        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        // Helper to generate options
2317        let create_options = |manager_ref| RecorderOptions {
2318            lazy_record, // Passed from test_case argument
2319            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        // 2. START RECORDER 1 (Fill data)
2329        {
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            // Scope ends, data is persisted to disk
2341        }
2342
2343        // Verify disk content
2344        let curr_csv = storage_path.join("crash_test.csv");
2345        let content = fs::read_to_string(curr_csv).unwrap();
2346        // Should contain integer values (ON=1, OFF=0) in order
2347        let lines: Vec<&str> = content.trim().lines().collect();
2348        assert_eq!(lines.len(), 2, "Expected 2 lines of recorded history");
2349
2350        // First record: ON (1)
2351        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        // Second record: OFF (0)
2356        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        // 3. FORCE "CRASH" STATE
2361        // Create 'Previous' file so library thinks this is a crash restart, not a reboot.
2362        // This forces it to READ from storage_path without overwriting it.
2363        let prev_csv = volatile_path.join("crash_test.csv");
2364        fs::write(&prev_csv, "").unwrap();
2365
2366        // 4. START RECORDER 2 (Simulate Restart)
2367        // This triggers hydration from disk into (Lazy: Dynamic nodes) or (Eager: EagerShardedBuffer)
2368        let mut recorder_restarted = EnumStateRecorder::<SwitchState>::new(
2369            "crash_test".into(),
2370            c"power_test",
2371            create_options(manager),
2372        )
2373        .unwrap();
2374
2375        // ASSERTIONS
2376        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, // Matches both lazy (casted i64) and eager (0 literal)
2400                        last_reset_ns: AnyIntProperty,
2401                    },
2402                }
2403            }
2404        });
2405
2406        // 5. RECORD NEW DATA
2407        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        // 1. Setup isolated environment
2446        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        // Helper to generate options pointing to our temp dirs
2456        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        // 2. SIMULATE FRESH REBOOT STATE
2468        // - "Current" file exists in persistent storage (saved from previous run).
2469        // - "Previous" file in volatile storage is MISSING (cleared by OS reboot).
2470        let curr_csv = storage_path.join("reboot_test.csv");
2471        // Write raw CSV data simulating timestamps 1000 and 2000 with integers (ON=1, OFF=0)
2472        fs::write(&curr_csv, "1000,1\n2000,0\n").unwrap();
2473
2474        // Ensure volatile file doesn't exist (simulating clean /tmp)
2475        let prev_csv = volatile_path.join("reboot_test.csv");
2476        assert!(!prev_csv.exists());
2477
2478        // 3. START RECORDER (Trigger Logic)
2479        let mut recorder = EnumStateRecorder::<SwitchState>::new(
2480            "reboot_test".into(),
2481            c"power_test",
2482            create_options(manager),
2483        )
2484        .unwrap();
2485
2486        // 4. VERIFY FILESYSTEM (Rotation)
2487        // The file should have been moved from 'data' to 'tmp'.
2488        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        // 5. ASSERTIONS (Inspect)
2495        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                    // DATA FROM FILE IS HERE (Read Only / Static)
2508                    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                    // ACTIVE HISTORY IS EMPTY (Fresh start)
2519                    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        // 6. RECORD NEW DATA AFTER REBOOT
2538        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                    // DATA FROM FILE IS HERE (Read Only / Static)
2553                    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                    // ACTIVE HISTORY IS NOW POPULATED WITH NEW DATA
2564                    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        // Setup isolated persistence environment
2591        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        // 1. Start Recorder and Record Data
2615        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); // Unknown
2631
2632        // 2. Verify Persistence (Lazy mode only)
2633        if lazy_record {
2634            drop(recorder); // Drop to ensure flush and release name
2635
2636            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            // First record: 100
2642            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            // Second record: 200
2647            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            // Third record: 300
2652            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            // 3. Restart Recorder (Simulate Reboot)
2657            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            // Eager mode
2711            // Recorder IS ALIVE here, so node exists.
2712            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        // 1. Setup isolated persistence environment
2751        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        // 2. SIMULATE FRESH REBOOT STATE
2772        // - "Current" file exists (saved from previous run).
2773        // - "Previous" file in volatile is MISSING.
2774        let curr_csv = storage_path.join("num_reboot_test.csv");
2775        // Write raw CSV data: time,value
2776        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        // 3. START RECORDER
2782        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        // 4. VERIFY FILESYSTEM (Rotation)
2792        // The file should have been moved from 'data' to 'tmp'.
2793        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        // 5. ASSERTIONS (Inspect)
2800        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}