Skip to main content

diagnostics/task_metrics/
component_tree_stats.rs

1// Copyright 2021 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
5use crate::task_metrics::component_stats::ComponentStats;
6use crate::task_metrics::constants::*;
7use crate::task_metrics::measurement::{Measurement, MeasurementsQueue};
8use crate::task_metrics::runtime_stats_source::{
9    ComponentStartedInfo, RuntimeStatsContainer, RuntimeStatsSource,
10};
11use crate::task_metrics::task_info::{TaskInfo, create_cpu_histogram};
12use async_trait::async_trait;
13use errors::ModelError;
14use fidl_fuchsia_component_runner::Task as DiagnosticsTask;
15use fuchsia_async as fasync;
16use fuchsia_inspect::{self as inspect, ArrayProperty, HistogramProperty};
17use fuchsia_sync::Mutex;
18use futures::channel::{mpsc, oneshot};
19use futures::{FutureExt, StreamExt};
20use hooks::{Event, EventPayload, EventType, HasEventType, Hook, HooksRegistration};
21use injectable_time::{BootInstant, TimeSource};
22use log::warn;
23use moniker::{ExtendedMoniker, Moniker};
24use std::collections::{BTreeMap, VecDeque};
25use std::fmt::Debug;
26use std::sync::{Arc, Weak};
27use zx::sys as zx_sys;
28
29macro_rules! maybe_return {
30    ($e:expr) => {
31        match $e {
32            None => return,
33            Some(v) => v,
34        }
35    };
36}
37
38const MAX_INSPECT_SIZE : usize = 2 * 1024 * 1024 /* 2MB */;
39
40const AGGREGATE_SAMPLES: &'static str = "@aggregated";
41
42/// Provides stats for all components running in the system.
43pub struct ComponentTreeStats<T: RuntimeStatsSource + Debug> {
44    /// Map from a moniker of a component running in the system to its stats.
45    tree: Mutex<BTreeMap<ExtendedMoniker, Arc<Mutex<ComponentStats<T>>>>>,
46
47    /// Stores all the tasks we know about. This provides direct access for updating a task's
48    /// children.
49    tasks: Mutex<BTreeMap<zx_sys::zx_koid_t, Weak<TaskInfo<T>>>>,
50
51    /// The root of the tree stats.
52    node: inspect::Node,
53
54    /// The node under which CPU usage histograms will be stored.
55    histograms_node: inspect::Node,
56
57    /// A histogram storing stats about the time it took to process the CPU stats measurements.
58    processing_times: inspect::IntExponentialHistogramProperty,
59
60    /// The task that takes CPU samples every minute.
61    sampler_task: Mutex<Option<fasync::Task<()>>>,
62
63    /// Aggregated CPU stats.
64    totals: Mutex<AggregatedStats>,
65
66    _wait_diagnostics_drain: fasync::Task<()>,
67
68    diagnostics_waiter_task_sender: mpsc::UnboundedSender<fasync::Task<()>>,
69
70    time_source: Arc<dyn TimeSource + Send + Sync>,
71
72    /// A queue of data taken from tasks which have been terminated.
73    /// If the ComponentTreeStats object has too many dead tasks, it will begin to drop
74    /// the individual `TaskInfo<T>` objects and aggregate their data into this queue.
75    aggregated_dead_task_data: Mutex<MeasurementsQueue>,
76
77    /// Cumulative CPU time of tasks that are terminated.
78    exited_measurements: Mutex<Measurement>,
79}
80
81impl<T: 'static + RuntimeStatsSource + Debug + Send + Sync> ComponentTreeStats<T> {
82    pub fn new(node: inspect::Node) -> Arc<Self> {
83        Self::new_with_timesource(node, Arc::new(BootInstant::new()))
84    }
85
86    fn new_with_timesource(
87        node: inspect::Node,
88        time_source: Arc<dyn TimeSource + Send + Sync>,
89    ) -> Arc<Self> {
90        let processing_times = node.create_int_exponential_histogram(
91            "processing_times_ns",
92            inspect::ExponentialHistogramParams {
93                floor: 1000,
94                initial_step: 1000,
95                step_multiplier: 2,
96                buckets: 16,
97            },
98        );
99
100        let histograms_node = node.create_child("histograms");
101        let totals = Mutex::new(AggregatedStats::new());
102        let (snd, rcv) = mpsc::unbounded();
103        let this = Arc::new(Self {
104            tree: Mutex::new(BTreeMap::new()),
105            tasks: Mutex::new(BTreeMap::new()),
106            node,
107            histograms_node,
108            processing_times,
109            sampler_task: Mutex::new(None),
110            totals,
111            diagnostics_waiter_task_sender: snd,
112            _wait_diagnostics_drain: fasync::Task::spawn(async move {
113                rcv.for_each_concurrent(None, |rx| async move { rx.await }).await;
114            }),
115            time_source: time_source.clone(),
116            aggregated_dead_task_data: Mutex::new(MeasurementsQueue::new(
117                COMPONENT_CPU_MAX_SAMPLES,
118                time_source,
119            )),
120            exited_measurements: Mutex::new(Measurement::default()),
121        });
122
123        let weak_self = Arc::downgrade(&this);
124
125        let weak_self_for_fut = weak_self.clone();
126        this.node.record_lazy_child("measurements", move || {
127            let weak_self_clone = weak_self_for_fut.clone();
128            async move {
129                if let Some(this) = weak_self_clone.upgrade() {
130                    Ok(this.write_measurements_to_inspect())
131                } else {
132                    Ok(inspect::Inspector::default())
133                }
134            }
135            .boxed()
136        });
137
138        let weak_self_clone_for_fut = weak_self.clone();
139        this.node.record_lazy_child("recent_usage", move || {
140            let weak_self_clone = weak_self_clone_for_fut.clone();
141            async move {
142                if let Some(this) = weak_self_clone.upgrade() {
143                    Ok(this.write_recent_usage_to_inspect())
144                } else {
145                    Ok(inspect::Inspector::default())
146                }
147            }
148            .boxed()
149        });
150        let weak_self_for_fut = weak_self;
151        this.node.record_lazy_child("@total", move || {
152            let weak_self_clone = weak_self_for_fut.clone();
153            async move {
154                if let Some(this) = weak_self_clone.upgrade() {
155                    Ok(this.write_totals_to_inspect())
156                } else {
157                    Ok(inspect::Inspector::default())
158                }
159            }
160            .boxed()
161        });
162
163        this
164    }
165
166    /// Perform an initial measurement followed by spawning a task that will perform a measurement
167    /// every `CPU_SAMPLE_PERIOD` seconds.
168    pub fn start_measuring(self: &Arc<Self>) {
169        let weak_self = Arc::downgrade(self);
170        self.measure();
171        *(self.sampler_task.lock()) = Some(fasync::Task::spawn(async move {
172            loop {
173                fasync::Timer::new(CPU_SAMPLE_PERIOD).await;
174                match weak_self.upgrade() {
175                    None => break,
176                    Some(this) => {
177                        this.measure();
178                    }
179                }
180            }
181        }));
182    }
183
184    /// Initializes a new component stats with the given task.
185    fn track_ready(&self, moniker: ExtendedMoniker, task: T) {
186        let stats = {
187            let mut tree_guard = self.tree.lock();
188            tree_guard
189                .entry(moniker.clone())
190                .or_insert_with(|| {
191                    let histogram = create_cpu_histogram(&self.histograms_node, &moniker);
192                    Arc::new(Mutex::new(ComponentStats::new(histogram)))
193                })
194                .clone()
195        };
196
197        let histogram = stats.lock().histogram();
198
199        if let Ok(task_info) = TaskInfo::try_from(task, Some(histogram), self.time_source.clone()) {
200            let koid = task_info.koid();
201            let arc_task_info = Arc::new(task_info);
202
203            let mut tasks_guard = self.tasks.lock();
204
205            stats.lock().add_task(arc_task_info.clone());
206
207            tasks_guard.insert(koid, Arc::downgrade(&arc_task_info));
208        }
209    }
210
211    fn write_measurements_to_inspect(self: &Arc<Self>) -> inspect::Inspector {
212        let inspector =
213            inspect::Inspector::new(inspect::InspectorConfig::default().size(MAX_INSPECT_SIZE));
214        let components = inspector.root().create_child("components");
215        let (component_count, task_count) = self.write_measurements(&components);
216        self.write_aggregate_measurements(&components);
217        inspector.root().record_uint("component_count", component_count);
218        inspector.root().record_uint("task_count", task_count);
219        inspector.root().record(components);
220
221        let stats_node = inspect::stats::StatsNode::new(&inspector);
222        stats_node.record_data_to(inspector.root());
223
224        inspector
225    }
226
227    fn write_recent_usage_to_inspect(self: &Arc<Self>) -> inspect::Inspector {
228        let inspector = inspect::Inspector::default();
229        self.totals.lock().write_recents_to(inspector.root());
230        inspector
231    }
232
233    fn write_totals_to_inspect(self: &Arc<Self>) -> inspect::Inspector {
234        let inspector = inspect::Inspector::default();
235        self.totals.lock().write_totals_to(inspector.root());
236        inspector
237    }
238
239    fn write_aggregate_measurements(&self, components_node: &inspect::Node) {
240        let locked_aggregate = self.aggregated_dead_task_data.lock();
241        if locked_aggregate.no_true_measurements() {
242            return;
243        }
244
245        let aggregate = components_node.create_child(&*AGGREGATE_SAMPLES);
246        locked_aggregate.record_to_node(&aggregate);
247        components_node.record(aggregate);
248    }
249
250    fn write_measurements(&self, node: &inspect::Node) -> (u64, u64) {
251        let mut task_count = 0;
252        let tree = self.tree.lock();
253        for (moniker, stats) in tree.iter() {
254            let stats_guard = stats.lock();
255            let key = match moniker {
256                ExtendedMoniker::ComponentManager => moniker.to_string(),
257                ExtendedMoniker::ComponentInstance(m) => {
258                    if *m == Moniker::root() {
259                        "<root>".to_string()
260                    } else {
261                        m.to_string()
262                    }
263                }
264            };
265            let child = node.create_child(key);
266            task_count += stats_guard.record_to_node(&child);
267            node.record(child);
268        }
269        (tree.len() as u64, task_count)
270    }
271
272    /// Takes a measurement of all tracked tasks and updated the totals. If any task is not alive
273    /// anymore it deletes it. If any component is not alive any more and no more historical
274    /// measurements are available for it, deletes it too.
275    pub fn measure(self: &Arc<Self>) {
276        let start = zx::BootInstant::get();
277
278        // Copy the stats and release the lock.
279        let stats = self
280            .tree
281            .lock()
282            .iter()
283            .map(|(k, v)| (k.clone(), Arc::downgrade(&v)))
284            .collect::<Vec<_>>();
285
286        let mut aggregated = Measurement::clone_with_time(&*self.exited_measurements.lock(), start);
287        let mut stats_to_remove = vec![];
288        let mut koids_to_remove = vec![];
289
290        for (moniker, weak_stats) in stats.into_iter() {
291            if let Some(stats) = weak_stats.upgrade() {
292                let mut stat_guard = stats.lock();
293                // Order is important: measure, then measure_tracked_dead_tasks, then clean_stale
294                aggregated += &stat_guard.measure();
295                aggregated += &stat_guard.measure_tracked_dead_tasks();
296                let (mut stale_koids, exited_cpu_of_deleted) = stat_guard.clean_stale();
297                aggregated += &exited_cpu_of_deleted;
298
299                *self.exited_measurements.lock() += &exited_cpu_of_deleted;
300
301                koids_to_remove.append(&mut stale_koids);
302                if !stat_guard.is_alive() {
303                    stats_to_remove.push(moniker);
304                }
305            }
306        }
307
308        // Lock the tree so that we ensure no modifications are made while we are deleting
309        let mut stats = self.tree.lock();
310        for moniker in stats_to_remove {
311            // Ensure that they are still not alive (if a component restarted it might be alive
312            // again).
313            if let Some(stat) = stats.get(&moniker) {
314                if !stat.lock().is_alive() {
315                    stats.remove(&moniker);
316                }
317            }
318        }
319
320        let mut tasks = self.tasks.lock();
321        for koid in koids_to_remove {
322            tasks.remove(&koid);
323        }
324
325        self.totals.lock().insert(aggregated);
326        self.processing_times.insert((zx::BootInstant::get() - start).into_nanos());
327    }
328
329    fn prune_dead_tasks(self: &Arc<Self>, max_dead_tasks: usize) {
330        let mut all_dead_tasks = BTreeMap::new();
331        for (_moniker, component) in self.tree.lock().iter() {
332            let dead_tasks = component.lock().gather_dead_tasks();
333            for (timestamp, task) in dead_tasks {
334                all_dead_tasks.insert(timestamp, task);
335            }
336        }
337
338        if all_dead_tasks.len() <= max_dead_tasks {
339            return;
340        }
341
342        let remove_count = all_dead_tasks.len() - (max_dead_tasks / 2);
343        let to_remove = all_dead_tasks.iter().take(remove_count);
344
345        let mut koids_to_remove = Vec::with_capacity(remove_count);
346
347        for (_, task) in to_remove {
348            if let Ok(measurements) = task.take_measurements_queue() {
349                koids_to_remove.push(task.koid());
350                *self.aggregated_dead_task_data.lock() += measurements;
351            }
352        }
353
354        self.tree.lock().retain(|_, stats| {
355            let mut stat_guard = stats.lock();
356            stat_guard.remove_by_koids(&koids_to_remove);
357            stat_guard.is_alive()
358        });
359
360        let mut tasks = self.tasks.lock();
361        for koid in &koids_to_remove {
362            tasks.remove(koid);
363        }
364    }
365
366    fn on_component_started<P, C>(self: &Arc<Self>, moniker: &Moniker, runtime: &P)
367    where
368        P: ComponentStartedInfo<C, T>,
369        C: RuntimeStatsContainer<T> + Send + Sync + 'static,
370    {
371        if let Some(receiver) = runtime.get_receiver() {
372            let task = fasync::Task::spawn(Self::diagnostics_waiter_task(
373                Arc::downgrade(&self),
374                moniker.clone().into(),
375                receiver,
376                runtime.start_time(),
377            ));
378            let _ = self.diagnostics_waiter_task_sender.unbounded_send(task);
379        }
380    }
381
382    async fn diagnostics_waiter_task<C>(
383        weak_self: Weak<Self>,
384        moniker: ExtendedMoniker,
385        receiver: oneshot::Receiver<C>,
386        start_time: zx::BootInstant,
387    ) where
388        C: RuntimeStatsContainer<T> + Send + Sync + 'static,
389    {
390        let mut source = maybe_return!(receiver.await.ok());
391        let this = maybe_return!(weak_self.upgrade());
392
393        let stats = {
394            let mut tree_lock = this.tree.lock();
395            if let Some(stats) = tree_lock.get(&moniker) {
396                stats.clone()
397            } else {
398                let histogram = create_cpu_histogram(&this.histograms_node, &moniker);
399                let stats = Arc::new(Mutex::new(ComponentStats::new(histogram)));
400                tree_lock.insert(moniker.clone(), stats.clone());
401                stats
402            }
403        };
404
405        let task = maybe_return!(source.take_component_task());
406
407        let histogram = stats.lock().histogram();
408        let task_info =
409            maybe_return!(TaskInfo::try_from(task, Some(histogram), this.time_source.clone()).ok());
410
411        let parent_koid = source
412            .take_parent_task()
413            .and_then(|task| TaskInfo::try_from(task, None, this.time_source.clone()).ok())
414            .map(|task| task.koid());
415
416        let koid = task_info.koid();
417
418        // At this point we haven't set the parent yet.
419        // We take two types of initial measurement for the task:
420        //  1) a zero-valued measurement that anchors the data at the provided start_time
421        //     with a cpu_time and queue_time of 0.
422        //  2) a "real" measurement that captures the first changed CPU data
423        task_info.record_measurement_with_start_time(start_time);
424        task_info.measure_if_no_parent();
425
426        let task_info = {
427            let mut task_guard = this.tasks.lock();
428            let task_info = match parent_koid {
429                None => {
430                    // If there's no parent task measure this task directly, otherwise
431                    // we'll measure on the parent.
432                    Arc::new(task_info)
433                }
434                Some(parent_koid) => {
435                    task_info.stats.lock().has_parent_task = true;
436                    let task_info = Arc::new(task_info);
437                    if let Some(parent) = task_guard.get(&parent_koid).and_then(|p| p.upgrade()) {
438                        parent.add_child(Arc::downgrade(&task_info));
439                    }
440                    task_info
441                }
442            };
443            task_guard.insert(koid, Arc::downgrade(&task_info));
444            task_info
445        };
446
447        stats.lock().add_task(task_info);
448
449        this.prune_dead_tasks(MAX_DEAD_TASKS);
450    }
451}
452
453impl ComponentTreeStats<DiagnosticsTask> {
454    pub fn hooks(self: &Arc<Self>) -> Vec<HooksRegistration> {
455        vec![HooksRegistration::new(
456            "ComponentTreeStats",
457            vec![EventType::Started],
458            Arc::downgrade(self) as Weak<dyn Hook>,
459        )]
460    }
461
462    /// Starts tracking component manager own stats.
463    pub fn track_component_manager_stats(&self) {
464        match fuchsia_runtime::job_default().duplicate_handle(zx::Rights::SAME_RIGHTS) {
465            Ok(job) => {
466                self.track_ready(ExtendedMoniker::ComponentManager, DiagnosticsTask::Job(job));
467            }
468            Err(err) => warn!(
469                "Failed to duplicate component manager job. Not tracking its own stats: {:?}",
470                err
471            ),
472        }
473    }
474}
475
476#[async_trait]
477impl Hook for ComponentTreeStats<DiagnosticsTask> {
478    async fn on(self: Arc<Self>, event: &Event) -> Result<(), ModelError> {
479        let target_moniker = event
480            .target_moniker
481            .unwrap_instance_moniker_or(ModelError::UnexpectedComponentManagerMoniker)?;
482        match event.event_type() {
483            EventType::Started => {
484                if let EventPayload::Started { runtime, .. } = &event.payload {
485                    self.on_component_started(target_moniker, &**runtime);
486                }
487            }
488            _ => {}
489        }
490        Ok(())
491    }
492}
493
494struct AggregatedStats {
495    /// A queue storing all total measurements. The last one is the most recent.
496    measurements: VecDeque<Measurement>,
497}
498
499impl AggregatedStats {
500    fn new() -> Self {
501        Self { measurements: VecDeque::with_capacity(COMPONENT_CPU_MAX_SAMPLES) }
502    }
503
504    fn insert(&mut self, measurement: Measurement) {
505        while self.measurements.len() >= COMPONENT_CPU_MAX_SAMPLES {
506            self.measurements.pop_front();
507        }
508        self.measurements.push_back(measurement);
509    }
510
511    fn write_totals_to(&self, node: &inspect::Node) {
512        let count = self.measurements.len();
513        let timestamps = node.create_int_array(TIMESTAMPS, count);
514        let cpu_times = node.create_int_array(CPU_TIMES, count);
515        let queue_times = node.create_int_array(QUEUE_TIMES, count);
516        for (i, measurement) in self.measurements.iter().enumerate() {
517            timestamps.set(i, measurement.timestamp().into_nanos());
518            cpu_times.set(i, measurement.cpu_time().into_nanos());
519            queue_times.set(i, measurement.queue_time().into_nanos());
520        }
521        node.record(timestamps);
522        node.record(cpu_times);
523        node.record(queue_times);
524    }
525
526    fn write_recents_to(&self, node: &inspect::Node) {
527        if self.measurements.is_empty() {
528            return;
529        }
530        if self.measurements.len() >= 2 {
531            let measurement = self.measurements.get(self.measurements.len() - 2).unwrap();
532            node.record_int("previous_cpu_time", measurement.cpu_time().into_nanos());
533            node.record_int("previous_queue_time", measurement.queue_time().into_nanos());
534            node.record_int("previous_timestamp", measurement.timestamp().into_nanos());
535        }
536        let measurement = self.measurements.get(self.measurements.len() - 1).unwrap();
537        node.record_int("recent_cpu_time", measurement.cpu_time().into_nanos());
538        node.record_int("recent_queue_time", measurement.queue_time().into_nanos());
539        node.record_int("recent_timestamp", measurement.timestamp().into_nanos());
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546    use crate::task_metrics::testing::{FakeDiagnosticsContainer, FakeRuntime, FakeTask};
547    use diagnostics_assertions::{AnyProperty, assert_data_tree};
548    use diagnostics_hierarchy::DiagnosticsHierarchy;
549    use fuchsia_inspect::DiagnosticsHierarchyGetter;
550
551    use injectable_time::{FakeTime, IncrementingFakeTime};
552
553    #[fuchsia::test]
554    async fn total_tracks_cpu_after_termination() {
555        let inspector = inspect::Inspector::default();
556        let clock = Arc::new(FakeTime::new());
557        let stats = ComponentTreeStats::new_with_timesource(
558            inspector.root().create_child("stats"),
559            clock.clone(),
560        );
561
562        let mut previous_task_count = 0;
563        for i in 0..10 {
564            clock.add_ticks(1);
565            let component_task = FakeTask::new(
566                i as u64,
567                create_measurements_vec_for_fake_task(COMPONENT_CPU_MAX_SAMPLES as i64 * 3, 2, 4),
568            );
569
570            let moniker = Moniker::try_from([format!("moniker-{}", i).as_ref()]).unwrap();
571            let fake_runtime =
572                Box::new(FakeRuntime::new(FakeDiagnosticsContainer::new(component_task, None)));
573            stats.on_component_started(&moniker, &*fake_runtime);
574
575            loop {
576                let current = stats.tree.lock().len();
577                if current != previous_task_count {
578                    previous_task_count = current;
579                    break;
580                }
581                fasync::Timer::new(fasync::MonotonicInstant::after(
582                    zx::MonotonicDuration::from_millis(100i64),
583                ))
584                .await;
585            }
586        }
587
588        assert_eq!(stats.tasks.lock().len(), 10);
589        assert_eq!(stats.tree.lock().len(), 10);
590
591        for _ in 0..=COMPONENT_CPU_MAX_SAMPLES - 2 {
592            stats.measure();
593            clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
594        }
595
596        // Data is produced by `measure`
597        // Both recent and previous exist
598        {
599            let totals = stats.totals.lock();
600            let recent_measurement = totals
601                .measurements
602                .get(totals.measurements.len() - 1)
603                .expect("there's at least one measurement");
604            assert_eq!(recent_measurement.cpu_time().into_nanos(), 1180);
605            assert_eq!(recent_measurement.queue_time().into_nanos(), 2360);
606
607            let previous_measurement = totals
608                .measurements
609                .get(totals.measurements.len() - 2)
610                .expect("there's a previous measurement");
611            assert_eq!(previous_measurement.cpu_time().into_nanos(), 1160);
612            assert_eq!(previous_measurement.queue_time().into_nanos(), 2320,);
613        }
614
615        // Terminate all tasks
616        for i in 0..10 {
617            let moniker = Moniker::try_from([format!("moniker-{}", i).as_ref()]).unwrap();
618
619            let tasks_to_terminate: Vec<_> = {
620                let tree_guard = stats.tree.lock();
621                let mut node_guard = tree_guard.get(&moniker.into()).unwrap().lock();
622
623                node_guard.tasks_mut().iter().map(|t| t.clone()).collect()
624            };
625
626            for task in tasks_to_terminate {
627                task.force_terminate().await;
628                // the timestamp for termination is used as a key when pruning,
629                // so all of the tasks cannot be removed at exactly the same time
630                clock.add_ticks(1);
631            }
632        }
633
634        // Data is produced by measure_dead_tasks
635        {
636            let totals = stats.totals.lock();
637            let recent_measurement = totals
638                .measurements
639                .get(totals.measurements.len() - 1)
640                .expect("there's at least one measurement");
641            assert_eq!(recent_measurement.cpu_time().into_nanos(), 1180);
642            assert_eq!(recent_measurement.queue_time().into_nanos(), 2360);
643
644            let previous_measurement = totals
645                .measurements
646                .get(totals.measurements.len() - 2)
647                .expect("there's a previous measurement");
648            assert_eq!(previous_measurement.cpu_time().into_nanos(), 1160);
649            assert_eq!(previous_measurement.queue_time().into_nanos(), 2320);
650        }
651
652        // Data is produced by measure_dead_tasks
653        stats.measure();
654        clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
655
656        {
657            let totals = stats.totals.lock();
658            let recent_measurement = totals
659                .measurements
660                .get(totals.measurements.len() - 1)
661                .expect("there's at least one measurement");
662            assert_eq!(recent_measurement.cpu_time().into_nanos(), 1200);
663            assert_eq!(recent_measurement.queue_time().into_nanos(), 2400);
664
665            let previous_measurement = totals
666                .measurements
667                .get(totals.measurements.len() - 2)
668                .expect("there's a previous measurement");
669            assert_eq!(previous_measurement.cpu_time().into_nanos(), 1180);
670            assert_eq!(previous_measurement.queue_time().into_nanos(), 2360);
671        }
672
673        stats.measure();
674        clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
675
676        {
677            let totals = stats.totals.lock();
678            let recent_measurement = totals
679                .measurements
680                .get(totals.measurements.len() - 1)
681                .expect("there's at least one measurement");
682            assert_eq!(recent_measurement.cpu_time().into_nanos(), 1200);
683            assert_eq!(recent_measurement.queue_time().into_nanos(), 2400);
684
685            let previous_measurement = totals
686                .measurements
687                .get(totals.measurements.len() - 2)
688                .expect("there's a previous measurement");
689            assert_eq!(previous_measurement.cpu_time().into_nanos(), 1200);
690            assert_eq!(previous_measurement.queue_time().into_nanos(), 2400);
691        }
692
693        // Push all the measurements in the queues out. @totals should still be accurate
694        for _ in 0..COMPONENT_CPU_MAX_SAMPLES {
695            stats.measure();
696            clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
697        }
698
699        // Data is produced by clean_stale
700        assert_eq!(stats.tasks.lock().len(), 0);
701        assert_eq!(stats.tree.lock().len(), 0);
702
703        // Expect that cumulative totals are still around, plus a post-termination measurement
704        {
705            let totals = stats.totals.lock();
706            let recent_measurement = totals
707                .measurements
708                .get(totals.measurements.len() - 1)
709                .expect("there's at least one measurement");
710            assert_eq!(recent_measurement.cpu_time().into_nanos(), 1200);
711            assert_eq!(recent_measurement.queue_time().into_nanos(), 2400);
712
713            let previous_measurement = totals
714                .measurements
715                .get(totals.measurements.len() - 2)
716                .expect("there's a previous measurement");
717            assert_eq!(previous_measurement.cpu_time().into_nanos(), 1200);
718            assert_eq!(previous_measurement.queue_time().into_nanos(), 2400);
719        }
720    }
721
722    #[fuchsia::test]
723    async fn components_are_deleted_when_all_tasks_are_gone() {
724        let inspector = inspect::Inspector::default();
725        let clock = Arc::new(FakeTime::new());
726        let stats = ComponentTreeStats::new_with_timesource(
727            inspector.root().create_child("stats"),
728            clock.clone(),
729        );
730        let moniker: Moniker = ["a"].try_into().unwrap();
731        let moniker: ExtendedMoniker = moniker.into();
732        stats.track_ready(moniker.clone(), FakeTask::default());
733        for _ in 0..=COMPONENT_CPU_MAX_SAMPLES {
734            stats.measure();
735            clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
736        }
737        assert_eq!(stats.tree.lock().len(), 1);
738        assert_eq!(stats.tasks.lock().len(), 1);
739        assert_eq!(
740            stats.tree.lock().get(&moniker).unwrap().lock().total_measurements(),
741            COMPONENT_CPU_MAX_SAMPLES
742        );
743
744        // Invalidate the handle, to simulate that the component stopped.
745        let tasks_to_terminate: Vec<_> = {
746            let tree_guard = stats.tree.lock();
747            let mut node_guard = tree_guard.get(&moniker).unwrap().lock();
748            node_guard.tasks_mut().iter().cloned().collect()
749        };
750
751        for task in tasks_to_terminate {
752            task.force_terminate().await;
753            clock.add_ticks(1);
754        }
755
756        // All post-invalidation measurements; this will push out true measurements
757        for i in 0..COMPONENT_CPU_MAX_SAMPLES {
758            stats.measure();
759            clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
760            assert_eq!(
761                stats.tree.lock().get(&moniker).unwrap().lock().total_measurements(),
762                COMPONENT_CPU_MAX_SAMPLES - i,
763            );
764        }
765        stats.measure();
766        clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
767        assert!(stats.tree.lock().get(&moniker).is_none());
768        assert_eq!(stats.tree.lock().len(), 0);
769        assert_eq!(stats.tasks.lock().len(), 0);
770    }
771
772    fn create_measurements_vec_for_fake_task(
773        num_measurements: i64,
774        init_cpu: i64,
775        init_queue: i64,
776    ) -> Vec<zx::TaskRuntimeInfo> {
777        let mut v = vec![];
778        for i in 0..num_measurements {
779            v.push(zx::TaskRuntimeInfo {
780                cpu_time: i * init_cpu,
781                queue_time: i * init_queue,
782                ..zx::TaskRuntimeInfo::default()
783            });
784        }
785
786        v
787    }
788
789    #[fuchsia::test]
790    async fn dead_tasks_are_pruned() {
791        let clock = Arc::new(FakeTime::new());
792        let inspector = inspect::Inspector::default();
793        let stats = Arc::new(ComponentTreeStats::new_with_timesource(
794            inspector.root().create_child("stats"),
795            clock.clone(),
796        ));
797
798        let mut previous_task_count = 0;
799        for i in 0..(MAX_DEAD_TASKS * 2) {
800            clock.add_ticks(1);
801            let component_task =
802                FakeTask::new(i as u64, create_measurements_vec_for_fake_task(300, 2, 4));
803
804            let moniker = Moniker::try_from([format!("moniker-{}", i).as_ref()]).unwrap();
805            let fake_runtime =
806                Box::new(FakeRuntime::new(FakeDiagnosticsContainer::new(component_task, None)));
807            stats.on_component_started(&moniker, &*fake_runtime);
808
809            loop {
810                let current = stats.tree.lock().len();
811                if current != previous_task_count {
812                    previous_task_count = current;
813                    break;
814                }
815                fasync::Timer::new(fasync::MonotonicInstant::after(
816                    zx::MonotonicDuration::from_millis(100i64),
817                ))
818                .await;
819            }
820
821            let extended_moniker: ExtendedMoniker = moniker.clone().into();
822
823            let tasks_to_terminate: Vec<_> = {
824                let tree_guard = stats.tree.lock();
825                let mut node_guard = tree_guard.get(&extended_moniker).unwrap().lock();
826
827                node_guard.tasks_mut().iter().cloned().collect()
828            };
829
830            for task in tasks_to_terminate {
831                task.force_terminate().await;
832                clock.add_ticks(1);
833            }
834        }
835
836        let task_count = stats.tasks.lock().len();
837        let moniker_count = stats.tree.lock().len();
838        assert_eq!(task_count, 88);
839        assert_eq!(moniker_count, 88);
840    }
841
842    #[fuchsia::test]
843    async fn aggregated_data_available_inspect() {
844        let max_dead_tasks = 4;
845        let clock = Arc::new(FakeTime::new());
846        let inspector = inspect::Inspector::default();
847        let stats = Arc::new(ComponentTreeStats::new_with_timesource(
848            inspector.root().create_child("stats"),
849            clock.clone(),
850        ));
851
852        let mut moniker_list = vec![];
853        for i in 0..(max_dead_tasks * 2) {
854            clock.add_ticks(1);
855            let moniker = Moniker::try_from([format!("moniker-{}", i).as_ref()]).unwrap();
856            moniker_list.push(moniker.clone());
857            let component_task =
858                FakeTask::new(i as u64, create_measurements_vec_for_fake_task(5, 1, 1));
859            stats.track_ready(moniker.into(), component_task);
860        }
861
862        clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
863        stats.measure();
864        clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
865        stats.measure();
866        clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
867        stats.measure();
868
869        assert_data_tree!(inspector, root: {
870            stats: contains {
871                measurements: contains {
872                    components: {
873                        "moniker-0": contains {},
874                        "moniker-1": contains {},
875                        "moniker-2": contains {},
876                        "moniker-3": contains {},
877                        "moniker-4": contains {},
878                        "moniker-5": contains {},
879                        "moniker-6": contains {},
880                        "moniker-7": contains {},
881                    }
882                }
883            }
884        });
885
886        for moniker in moniker_list {
887            let extended_moniker = moniker.clone().into();
888
889            let tasks_to_terminate = {
890                let tree_guard = stats.tree.lock();
891                let mut node_guard = tree_guard.get(&extended_moniker).unwrap().lock();
892
893                node_guard.tasks_mut().to_vec()
894            };
895
896            for task in tasks_to_terminate {
897                task.force_terminate().await;
898                clock.add_ticks(1);
899            }
900        }
901
902        stats.prune_dead_tasks(max_dead_tasks);
903
904        let hierarchy = inspector.get_diagnostics_hierarchy().await;
905        assert_data_tree!(inspector, root: {
906            stats: contains {
907                measurements: contains {
908                    components: {
909                        "@aggregated": {
910                            "timestamps": AnyProperty,
911                            "cpu_times": vec![0i64, 6i64, 12i64],
912                            "queue_times": vec![0i64, 6i64, 12i64],
913                        },
914                        "moniker-6": contains {},
915                        "moniker-7": contains {},
916                    }
917                }
918            }
919        });
920        let (timestamps, _, _) = get_data(&hierarchy, "@aggregated", None);
921        assert_eq!(timestamps.len(), 3);
922        assert!(timestamps[1] > timestamps[0]);
923        assert!(timestamps[2] > timestamps[1]);
924    }
925
926    #[fuchsia::test]
927    async fn total_holds_sum_of_stats() {
928        let inspector = inspect::Inspector::default();
929        let stats = ComponentTreeStats::new(inspector.root().create_child("stats"));
930        stats.measure();
931        stats.track_ready(
932            ExtendedMoniker::ComponentInstance(["a"].try_into().unwrap()),
933            FakeTask::new(
934                1,
935                vec![
936                    zx::TaskRuntimeInfo {
937                        cpu_time: 2,
938                        queue_time: 4,
939                        ..zx::TaskRuntimeInfo::default()
940                    },
941                    zx::TaskRuntimeInfo {
942                        cpu_time: 6,
943                        queue_time: 8,
944                        ..zx::TaskRuntimeInfo::default()
945                    },
946                ],
947            ),
948        );
949        stats.track_ready(
950            ExtendedMoniker::ComponentInstance(["b"].try_into().unwrap()),
951            FakeTask::new(
952                2,
953                vec![
954                    zx::TaskRuntimeInfo {
955                        cpu_time: 1,
956                        queue_time: 3,
957                        ..zx::TaskRuntimeInfo::default()
958                    },
959                    zx::TaskRuntimeInfo {
960                        cpu_time: 5,
961                        queue_time: 7,
962                        ..zx::TaskRuntimeInfo::default()
963                    },
964                ],
965            ),
966        );
967
968        stats.measure();
969        let hierarchy = inspect::reader::read(&inspector).await.expect("read inspect hierarchy");
970        let (timestamps, cpu_times, queue_times) = get_data_at(&hierarchy, &["stats", "@total"]);
971        assert_eq!(timestamps.len(), 2);
972        assert_eq!(cpu_times, vec![0, 2 + 1]);
973        assert_eq!(queue_times, vec![0, 4 + 3]);
974
975        stats.measure();
976        let hierarchy = inspect::reader::read(&inspector).await.expect("read inspect hierarchy");
977        let (timestamps, cpu_times, queue_times) = get_data_at(&hierarchy, &["stats", "@total"]);
978        assert_eq!(timestamps.len(), 3);
979        assert_eq!(cpu_times, vec![0, 2 + 1, 6 + 5]);
980        assert_eq!(queue_times, vec![0, 4 + 3, 8 + 7]);
981    }
982
983    #[fuchsia::test]
984    async fn recent_usage() {
985        // Set up the test
986        let inspector = inspect::Inspector::default();
987        let stats = ComponentTreeStats::new(inspector.root().create_child("stats"));
988        stats.measure();
989
990        stats.track_ready(
991            ExtendedMoniker::ComponentInstance(["a"].try_into().unwrap()),
992            FakeTask::new(
993                1,
994                vec![
995                    zx::TaskRuntimeInfo {
996                        cpu_time: 2,
997                        queue_time: 4,
998                        ..zx::TaskRuntimeInfo::default()
999                    },
1000                    zx::TaskRuntimeInfo {
1001                        cpu_time: 6,
1002                        queue_time: 8,
1003                        ..zx::TaskRuntimeInfo::default()
1004                    },
1005                ],
1006            ),
1007        );
1008        stats.track_ready(
1009            ExtendedMoniker::ComponentInstance(["b"].try_into().unwrap()),
1010            FakeTask::new(
1011                2,
1012                vec![
1013                    zx::TaskRuntimeInfo {
1014                        cpu_time: 1,
1015                        queue_time: 3,
1016                        ..zx::TaskRuntimeInfo::default()
1017                    },
1018                    zx::TaskRuntimeInfo {
1019                        cpu_time: 5,
1020                        queue_time: 7,
1021                        ..zx::TaskRuntimeInfo::default()
1022                    },
1023                ],
1024            ),
1025        );
1026
1027        stats.measure();
1028        let hierarchy = inspect::reader::read(&inspector).await.expect("read inspect hierarchy");
1029
1030        // Verify initially there's no second most recent measurement since we only
1031        // have the initial measurement written.
1032        assert_data_tree!(&hierarchy, root: contains {
1033            stats: contains {
1034                recent_usage: {
1035                    previous_cpu_time: 0i64,
1036                    previous_queue_time: 0i64,
1037                    previous_timestamp: AnyProperty,
1038                    recent_cpu_time: 2 + 1i64,
1039                    recent_queue_time: 4 + 3i64,
1040                    recent_timestamp: AnyProperty,
1041                }
1042            }
1043        });
1044
1045        // Verify that the recent values are equal to the total values.
1046        let initial_timestamp = get_recent_property(&hierarchy, "recent_timestamp");
1047        let (timestamps, cpu_times, queue_times) = get_data_at(&hierarchy, &["stats", "@total"]);
1048        assert_eq!(timestamps.len(), 2);
1049        assert_eq!(timestamps[1], initial_timestamp);
1050        assert_eq!(cpu_times, vec![0, 2 + 1]);
1051        assert_eq!(queue_times, vec![0, 4 + 3]);
1052
1053        // Add one measurement
1054        stats.measure();
1055        let hierarchy = inspect::reader::read(&inspector).await.expect("read inspect hierarchy");
1056
1057        // Verify that previous is now there and holds the previously recent values.
1058        assert_data_tree!(&hierarchy, root: contains {
1059            stats: contains {
1060                recent_usage: {
1061                    previous_cpu_time: 2 + 1i64,
1062                    previous_queue_time: 4 + 3i64,
1063                    previous_timestamp: initial_timestamp,
1064                    recent_cpu_time: 6 + 5i64,
1065                    recent_queue_time: 8 + 7i64,
1066                    recent_timestamp: AnyProperty,
1067                }
1068            }
1069        });
1070
1071        // Verify that the recent timestamp is higher than the previous timestamp.
1072        let recent_timestamp = get_recent_property(&hierarchy, "recent_timestamp");
1073        assert!(recent_timestamp > initial_timestamp);
1074    }
1075
1076    #[fuchsia::test]
1077    async fn component_stats_are_available_in_inspect() {
1078        let inspector = inspect::Inspector::default();
1079        let stats = ComponentTreeStats::new(inspector.root().create_child("stats"));
1080        stats.track_ready(
1081            ExtendedMoniker::ComponentInstance(["a"].try_into().unwrap()),
1082            FakeTask::new(
1083                1,
1084                vec![
1085                    zx::TaskRuntimeInfo {
1086                        cpu_time: 2,
1087                        queue_time: 4,
1088                        ..zx::TaskRuntimeInfo::default()
1089                    },
1090                    zx::TaskRuntimeInfo {
1091                        cpu_time: 6,
1092                        queue_time: 8,
1093                        ..zx::TaskRuntimeInfo::default()
1094                    },
1095                ],
1096            ),
1097        );
1098
1099        stats.measure();
1100
1101        let hierarchy = inspector.get_diagnostics_hierarchy().await;
1102        assert_data_tree!(hierarchy, root: {
1103            stats: contains {
1104                measurements: contains {
1105                    components: {
1106                        "a": {
1107                            "1": {
1108                                timestamps: AnyProperty,
1109                                cpu_times: vec![2i64],
1110                                queue_times: vec![4i64],
1111                            }
1112                        }
1113                    }
1114                }
1115            }
1116        });
1117        let (timestamps, _, _) = get_data(&hierarchy, "a", Some("1"));
1118        assert_eq!(timestamps.len(), 1);
1119
1120        // Add another measurement
1121        stats.measure();
1122
1123        let hierarchy = inspector.get_diagnostics_hierarchy().await;
1124        assert_data_tree!(hierarchy, root: {
1125            stats: contains {
1126                measurements: contains {
1127                    components: {
1128                        "a": {
1129                            "1": {
1130                                timestamps: AnyProperty,
1131                                cpu_times: vec![2i64, 6],
1132                                queue_times: vec![4i64, 8],
1133                            }
1134                        }
1135                    }
1136                }
1137            }
1138        });
1139        let (timestamps, _, _) = get_data(&hierarchy, "a", Some("1"));
1140        assert_eq!(timestamps.len(), 2);
1141        assert!(timestamps[1] > timestamps[0]);
1142    }
1143
1144    #[fuchsia::test]
1145    async fn on_started_handles_parent_task() {
1146        let inspector = inspect::Inspector::default();
1147        let clock = Arc::new(FakeTime::new());
1148        // set ticks to 20 to avoid interfering with the start times reported
1149        // by FakeRuntime
1150        clock.add_ticks(20);
1151        let stats = Arc::new(ComponentTreeStats::new_with_timesource(
1152            inspector.root().create_child("stats"),
1153            clock.clone(),
1154        ));
1155        let parent_task = FakeTask::new(
1156            1,
1157            vec![
1158                zx::TaskRuntimeInfo {
1159                    cpu_time: 20,
1160                    queue_time: 40,
1161                    ..zx::TaskRuntimeInfo::default()
1162                },
1163                zx::TaskRuntimeInfo {
1164                    cpu_time: 60,
1165                    queue_time: 80,
1166                    ..zx::TaskRuntimeInfo::default()
1167                },
1168            ],
1169        );
1170        let component_task = FakeTask::new(
1171            2,
1172            vec![
1173                zx::TaskRuntimeInfo {
1174                    cpu_time: 2,
1175                    queue_time: 4,
1176                    ..zx::TaskRuntimeInfo::default()
1177                },
1178                zx::TaskRuntimeInfo {
1179                    cpu_time: 6,
1180                    queue_time: 8,
1181                    ..zx::TaskRuntimeInfo::default()
1182                },
1183            ],
1184        );
1185
1186        let fake_runtime = Box::new(FakeRuntime::new_with_start_times(
1187            FakeDiagnosticsContainer::new(parent_task.clone(), None),
1188            IncrementingFakeTime::new(3, std::time::Duration::from_nanos(5)),
1189        ));
1190        stats.on_component_started(&Moniker::try_from(["parent"]).unwrap(), &*fake_runtime);
1191
1192        let fake_runtime = Box::new(FakeRuntime::new_with_start_times(
1193            FakeDiagnosticsContainer::new(component_task, Some(parent_task)),
1194            IncrementingFakeTime::new(8, std::time::Duration::from_nanos(5)),
1195        ));
1196        stats.on_component_started(&Moniker::try_from(["child"]).unwrap(), &*fake_runtime);
1197
1198        // Wait for diagnostics data to be received since it's done in a non-blocking way on
1199        // started.
1200        loop {
1201            if stats.tree.lock().len() == 2 {
1202                break;
1203            }
1204            fasync::Timer::new(fasync::MonotonicInstant::after(
1205                zx::MonotonicDuration::from_millis(100i64),
1206            ))
1207            .await;
1208        }
1209
1210        assert_data_tree!(inspector, root: {
1211            stats: contains {
1212                measurements: contains {
1213                    components: {
1214                        "parent": {
1215                            "1": {
1216                                "timestamps": AnyProperty,
1217                                "cpu_times": vec![0i64, 20],
1218                                "queue_times": vec![0i64, 40],
1219                            },
1220                        },
1221                        "child": {
1222                            "2": {
1223                                "timestamps": AnyProperty,
1224                                "cpu_times": vec![0i64, 2],
1225                                "queue_times": vec![0i64, 4],
1226                            }
1227                        }
1228                    }
1229                }
1230            }
1231        });
1232    }
1233
1234    #[fuchsia::test]
1235    async fn child_tasks_garbage_collection() {
1236        let inspector = inspect::Inspector::default();
1237        let clock = Arc::new(FakeTime::new());
1238        let stats = Arc::new(ComponentTreeStats::new_with_timesource(
1239            inspector.root().create_child("stats"),
1240            clock.clone(),
1241        ));
1242        let parent_task = FakeTask::new(
1243            1,
1244            vec![
1245                zx::TaskRuntimeInfo {
1246                    cpu_time: 20,
1247                    queue_time: 40,
1248                    ..zx::TaskRuntimeInfo::default()
1249                },
1250                zx::TaskRuntimeInfo {
1251                    cpu_time: 60,
1252                    queue_time: 80,
1253                    ..zx::TaskRuntimeInfo::default()
1254                },
1255            ],
1256        );
1257        let component_task = FakeTask::new(
1258            2,
1259            vec![zx::TaskRuntimeInfo {
1260                cpu_time: 2,
1261                queue_time: 4,
1262                ..zx::TaskRuntimeInfo::default()
1263            }],
1264        );
1265        let fake_parent_runtime =
1266            Box::new(FakeRuntime::new(FakeDiagnosticsContainer::new(parent_task.clone(), None)));
1267        stats.on_component_started(&Moniker::try_from(["parent"]).unwrap(), &*fake_parent_runtime);
1268
1269        let child_moniker = Moniker::try_from(["child"]).unwrap();
1270        let fake_runtime = Box::new(FakeRuntime::new(FakeDiagnosticsContainer::new(
1271            component_task,
1272            Some(parent_task),
1273        )));
1274        stats.on_component_started(&child_moniker, &*fake_runtime);
1275
1276        // Wait for diagnostics data to be received since it's done in a non-blocking way on
1277        // started.
1278        loop {
1279            if stats.tree.lock().len() == 2 {
1280                break;
1281            }
1282            fasync::Timer::new(fasync::MonotonicInstant::after(
1283                zx::MonotonicDuration::from_millis(100i64),
1284            ))
1285            .await;
1286        }
1287
1288        assert_eq!(stats.tree.lock().len(), 2);
1289        assert_eq!(stats.tasks.lock().len(), 2);
1290
1291        let extended_moniker = child_moniker.into();
1292        // Mark as terminated, to simulate that the component completely stopped.
1293        let tasks_to_terminate: Vec<_> = {
1294            let tree_guard = stats.tree.lock();
1295            let mut node_guard = tree_guard.get(&extended_moniker).unwrap().lock();
1296            node_guard.tasks_mut().iter().cloned().collect()
1297        };
1298
1299        for task in tasks_to_terminate {
1300            task.force_terminate().await;
1301            clock.add_ticks(1);
1302        }
1303
1304        // This will perform the (last) post-termination sample.
1305        stats.measure();
1306        clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
1307
1308        // These will start incrementing the counter of post-termination samples, but won't sample.
1309        for _ in 0..COMPONENT_CPU_MAX_SAMPLES {
1310            stats.measure();
1311            clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
1312        }
1313
1314        // Causes the task to be gone since it has been terminated for long enough.
1315        stats.measure();
1316
1317        // Child is gone and only the parent exists now.
1318        assert!(stats.tree.lock().get(&extended_moniker).is_none());
1319        assert_eq!(stats.tree.lock().len(), 1);
1320        assert_eq!(stats.tasks.lock().len(), 1);
1321    }
1322
1323    fn get_recent_property(hierarchy: &DiagnosticsHierarchy, name: &str) -> i64 {
1324        hierarchy.get_property_by_path(&vec!["stats", "recent_usage", name]).unwrap().int().unwrap()
1325    }
1326
1327    fn get_data(
1328        hierarchy: &DiagnosticsHierarchy,
1329        moniker: &str,
1330        task: Option<&str>,
1331    ) -> (Vec<i64>, Vec<i64>, Vec<i64>) {
1332        let mut path = vec!["stats", "measurements", "components", moniker];
1333        if let Some(task) = task {
1334            path.push(task);
1335        }
1336        get_data_at(&hierarchy, &path)
1337    }
1338
1339    fn get_data_at(
1340        hierarchy: &DiagnosticsHierarchy,
1341        path: &[&str],
1342    ) -> (Vec<i64>, Vec<i64>, Vec<i64>) {
1343        let node = hierarchy.get_child_by_path(&path).expect("found stats node");
1344        let cpu_times = node
1345            .get_property("cpu_times")
1346            .expect("found cpu")
1347            .int_array()
1348            .expect("cpu are ints")
1349            .raw_values();
1350        let queue_times = node
1351            .get_property("queue_times")
1352            .expect("found queue")
1353            .int_array()
1354            .expect("queue are ints")
1355            .raw_values();
1356        let timestamps = node
1357            .get_property("timestamps")
1358            .expect("found timestamps")
1359            .int_array()
1360            .expect("timestamps are ints")
1361            .raw_values();
1362        (timestamps.into_owned(), cpu_times.into_owned(), queue_times.into_owned())
1363    }
1364
1365    #[fuchsia::test]
1366    async fn component_restart_shares_cpu_histogram() {
1367        let inspector = inspect::Inspector::default();
1368        let clock = Arc::new(FakeTime::new());
1369        let stats = Arc::new(ComponentTreeStats::new_with_timesource(
1370            inspector.root().create_child("stats"),
1371            clock.clone(),
1372        ));
1373
1374        let moniker = Moniker::try_from(["restarting-component"]).unwrap();
1375        let ext_moniker: ExtendedMoniker = moniker.clone().into();
1376
1377        // 1. Start first instance of the component.
1378        let task1 = FakeTask::new(1, create_measurements_vec_for_fake_task(10, 1, 1));
1379        let fake_runtime1 =
1380            Box::new(FakeRuntime::new(FakeDiagnosticsContainer::new(task1.clone(), None)));
1381        stats.on_component_started(&moniker, &*fake_runtime1);
1382
1383        loop {
1384            if stats.tree.lock().len() == 1 {
1385                break;
1386            }
1387            fasync::Timer::new(fasync::MonotonicInstant::after(
1388                zx::MonotonicDuration::from_millis(10),
1389            ))
1390            .await;
1391        }
1392
1393        clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
1394        stats.measure();
1395
1396        // 2. Terminate first instance.
1397        task1.terminate();
1398        clock.add_ticks(1);
1399
1400        // 3. Start second instance (restart) before task 1 is expired/pruned.
1401        let task2 = FakeTask::new(2, create_measurements_vec_for_fake_task(10, 1, 1));
1402        let fake_runtime2 =
1403            Box::new(FakeRuntime::new(FakeDiagnosticsContainer::new(task2.clone(), None)));
1404        stats.on_component_started(&moniker, &*fake_runtime2);
1405
1406        loop {
1407            if stats
1408                .tree
1409                .lock()
1410                .get(&ext_moniker)
1411                .is_some_and(|comp_stats| comp_stats.lock().tasks().len() == 2)
1412            {
1413                break;
1414            }
1415            fasync::Timer::new(fasync::MonotonicInstant::after(
1416                zx::MonotonicDuration::from_millis(10),
1417            ))
1418            .await;
1419        }
1420
1421        clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
1422        stats.measure();
1423
1424        // 4. Verify Inspect hierarchy contains exactly ONE histogram property for this moniker.
1425        let hierarchy = inspector.get_diagnostics_hierarchy().await;
1426        let histograms_node = hierarchy
1427            .get_child_by_path(&vec!["stats", "histograms"])
1428            .expect("found histograms node");
1429
1430        let matching_props: Vec<_> = histograms_node
1431            .properties
1432            .iter()
1433            .filter(|p| p.name() == "restarting-component")
1434            .collect();
1435
1436        assert_eq!(
1437            matching_props.len(),
1438            1,
1439            "Expected exactly 1 histogram property for moniker, found {}",
1440            matching_props.len()
1441        );
1442    }
1443}