1use 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 ;
39
40const AGGREGATE_SAMPLES: &'static str = "@aggregated";
41
42pub struct ComponentTreeStats<T: RuntimeStatsSource + Debug> {
44 tree: Mutex<BTreeMap<ExtendedMoniker, Arc<Mutex<ComponentStats<T>>>>>,
46
47 tasks: Mutex<BTreeMap<zx_sys::zx_koid_t, Weak<TaskInfo<T>>>>,
50
51 node: inspect::Node,
53
54 histograms_node: inspect::Node,
56
57 processing_times: inspect::IntExponentialHistogramProperty,
59
60 sampler_task: Mutex<Option<fasync::Task<()>>>,
62
63 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 aggregated_dead_task_data: Mutex<MeasurementsQueue>,
76
77 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 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 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 pub fn measure(self: &Arc<Self>) {
276 let start = zx::BootInstant::get();
277
278 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 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 let mut stats = self.tree.lock();
310 for moniker in stats_to_remove {
311 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 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 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 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 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 {
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 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 clock.add_ticks(1);
631 }
632 }
633
634 {
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 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 for _ in 0..COMPONENT_CPU_MAX_SAMPLES {
695 stats.measure();
696 clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
697 }
698
699 assert_eq!(stats.tasks.lock().len(), 0);
701 assert_eq!(stats.tree.lock().len(), 0);
702
703 {
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 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 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 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 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 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 stats.measure();
1055 let hierarchy = inspect::reader::read(&inspector).await.expect("read inspect hierarchy");
1056
1057 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 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 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 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 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 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 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 stats.measure();
1306 clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
1307
1308 for _ in 0..COMPONENT_CPU_MAX_SAMPLES {
1310 stats.measure();
1311 clock.add_ticks(CPU_SAMPLE_PERIOD.as_nanos() as i64);
1312 }
1313
1314 stats.measure();
1316
1317 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 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 task1.terminate();
1398 clock.add_ticks(1);
1399
1400 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 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}