blob: 19cc50a41d0578ed4e682d77e461a43c5bcb312f [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::{
model::error::ModelError,
model::hooks::{Event, EventPayload, EventType, HasEventType, Hook, HooksRegistration},
},
async_trait::async_trait,
fuchsia_inspect as inspect, fuchsia_inspect_contrib as inspect_contrib,
fuchsia_sync::Mutex,
fuchsia_zircon as zx,
moniker::Moniker,
std::sync::{Arc, Weak},
};
const MAX_NUMBER_OF_LIFECYCLE_EVENTS: usize = 150;
const MONIKER: &str = "moniker";
const TYPE: &str = "type";
const STARTED: &str = "started";
const STOPPED: &str = "stopped";
const TIME: &str = "time";
const EARLY: &str = "early";
const LATE: &str = "late";
/// Tracks start and stop timestamps of components.
pub struct ComponentLifecycleTimeStats {
// Keeps the inspect node alive.
_node: inspect::Node,
inner: Mutex<Inner>,
}
/// `early` maintains the first `MAX_NUMBER_OF_LIFECYCLE_EVENTS` start/stop events of
/// components. After more than `MAX_NUMBER_OF_LIFECYCLE_EVENTS` events have occurred,
/// `early` will stay unchanged, and `late` will maintain the the last
/// `MAX_NUMBER_OF_LIFECYCLE_EVENTS` start/stop events of components. When more events are
/// added, the earliest ones in `late` will be discarded. This enables our feedback
/// snapshots to contain a recent history of started and stopped components.
struct Inner {
early: inspect_contrib::nodes::BoundedListNode,
late: inspect_contrib::nodes::BoundedListNode,
}
impl Inner {
fn new(early: inspect::Node, late: inspect::Node) -> Self {
let early =
inspect_contrib::nodes::BoundedListNode::new(early, MAX_NUMBER_OF_LIFECYCLE_EVENTS);
let late =
inspect_contrib::nodes::BoundedListNode::new(late, MAX_NUMBER_OF_LIFECYCLE_EVENTS);
Self { early, late }
}
fn add_entry(&mut self, moniker: &Moniker, kind: &str, time: zx::Time) {
let node =
if self.early.len() < self.early.capacity() { &mut self.early } else { &mut self.late };
node.add_entry(|node| {
node.record_string(MONIKER, moniker.to_string());
node.record_string(TYPE, kind);
node.record_int(TIME, time.into_nanos());
});
}
}
impl ComponentLifecycleTimeStats {
/// Creates a new startup time tracker. Data will be written to the given inspect node.
pub fn new(node: inspect::Node) -> Self {
let early = node.create_child(EARLY);
let late = node.create_child(LATE);
Self { _node: node, inner: Mutex::new(Inner::new(early, late)) }
}
/// Provides the hook events that are needed to work.
pub fn hooks(self: &Arc<Self>) -> Vec<HooksRegistration> {
vec![HooksRegistration::new(
"ComponentLifecycleTimeStats",
vec![EventType::Started, EventType::Stopped],
Arc::downgrade(self) as Weak<dyn Hook>,
)]
}
fn on_component_started(self: &Arc<Self>, moniker: &Moniker, start_time: zx::Time) {
self.inner.lock().add_entry(moniker, STARTED, start_time);
}
fn on_component_stopped(self: &Arc<Self>, moniker: &Moniker, stop_time: zx::Time) {
self.inner.lock().add_entry(moniker, STOPPED, stop_time);
}
}
#[async_trait]
impl Hook for ComponentLifecycleTimeStats {
async fn on(self: Arc<Self>, event: &Event) -> Result<(), ModelError> {
let target_moniker = event
.target_moniker
.unwrap_instance_moniker_or(ModelError::UnexpectedComponentManagerMoniker)?;
match event.event_type() {
EventType::Started => {
if let EventPayload::Started { runtime, .. } = &event.payload {
self.on_component_started(target_moniker, runtime.start_time);
}
}
EventType::Stopped => {
if let EventPayload::Stopped { stop_time, .. } = &event.payload {
self.on_component_stopped(target_moniker, *stop_time);
}
}
_ => {}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{
component::{ComponentInstance, StartReason},
testing::test_helpers::{component_decl_with_test_runner, ActionsTest},
};
use cm_rust_testing::ComponentDeclBuilder;
use diagnostics_assertions::{assert_data_tree, AnyProperty};
use fuchsia_inspect::DiagnosticsHierarchyGetter;
use itertools::Itertools;
use moniker::{ChildName, ChildNameBase, MonikerBase};
#[fuchsia::test]
async fn tracks_events() {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("a").build()),
("a", ComponentDeclBuilder::new().child_default("b").build()),
("b", component_decl_with_test_runner()),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let inspector = inspect::Inspector::default();
let stats =
Arc::new(ComponentLifecycleTimeStats::new(inspector.root().create_child("lifecycle")));
root.hooks.install(stats.hooks()).await;
let root_timestamp = start_and_get_timestamp(root, &Moniker::root()).await.into_nanos();
let a_timestamp = start_and_get_timestamp(root, &"a".parse().unwrap()).await.into_nanos();
let b_timestamp = start_and_get_timestamp(root, &"a/b".parse().unwrap()).await.into_nanos();
root.find(&"a/b".parse().unwrap()).await.unwrap().stop().await.unwrap();
assert_data_tree!(inspector, root: {
lifecycle: {
early: {
"0": {
moniker: ".",
time: root_timestamp,
"type": "started",
},
"1": {
moniker: "a",
time: a_timestamp,
"type": "started",
},
"2": {
moniker: "a/b",
time: b_timestamp,
"type": "started",
},
"3": contains {
moniker: "a/b",
"type": "stopped",
time: AnyProperty,
}
},
late: {
}
}
});
}
#[fuchsia::test]
async fn early_doesnt_track_more_than_limit() {
let inspector = inspect::Inspector::default();
let stats =
Arc::new(ComponentLifecycleTimeStats::new(inspector.root().create_child("lifecycle")));
for i in 0..2 * MAX_NUMBER_OF_LIFECYCLE_EVENTS {
stats.on_component_started(
&Moniker::new(vec![ChildName::parse(format!("{}", i)).unwrap()]),
zx::Time::from_nanos(i as i64),
);
}
let hierarchy = inspector.get_diagnostics_hierarchy();
let node = &hierarchy.children[0];
let early = node.children.iter().find_or_first(|c| c.name == "early").unwrap();
assert_eq!(early.children.len(), MAX_NUMBER_OF_LIFECYCLE_EVENTS);
assert_eq!(
early.children.iter().map(|c| c.name.parse::<i32>().unwrap()).sorted().last().unwrap(),
149
);
}
#[fuchsia::test]
async fn early_overflow_to_late() {
let inspector = inspect::Inspector::default();
let stats =
Arc::new(ComponentLifecycleTimeStats::new(inspector.root().create_child("lifecycle")));
for i in 0..MAX_NUMBER_OF_LIFECYCLE_EVENTS + 1 {
stats.on_component_started(
&Moniker::new(vec![ChildName::parse(format!("{}", i)).unwrap()]),
zx::Time::from_nanos(i as i64),
);
}
let hierarchy = inspector.get_diagnostics_hierarchy();
let node = &hierarchy.children[0];
let early = node.children.iter().find_or_first(|c| c.name == "early").unwrap();
let late = node.children.iter().find_or_first(|c| c.name == "late").unwrap();
assert_eq!(early.children.len(), MAX_NUMBER_OF_LIFECYCLE_EVENTS);
assert_eq!(
early.children.iter().map(|c| c.name.parse::<i32>().unwrap()).sorted().last().unwrap(),
149
);
assert_eq!(late.children.len(), 1);
assert_data_tree!(late, late: {
"0": contains {
moniker: "150",
"type": "started",
}
});
}
#[fuchsia::test]
async fn late_doesnt_track_more_than_limit() {
let inspector = inspect::Inspector::default();
let stats =
Arc::new(ComponentLifecycleTimeStats::new(inspector.root().create_child("lifecycle")));
for i in 0..4 * MAX_NUMBER_OF_LIFECYCLE_EVENTS {
stats.on_component_started(
&Moniker::new(vec![ChildName::parse(format!("{}", i)).unwrap()]),
zx::Time::from_nanos(i as i64),
);
}
let hierarchy = inspector.get_diagnostics_hierarchy();
let node = &hierarchy.children[0];
let early = node.children.iter().find_or_first(|c| c.name == "early").unwrap();
let late = node.children.iter().find_or_first(|c| c.name == "late").unwrap();
assert_eq!(early.children.len(), MAX_NUMBER_OF_LIFECYCLE_EVENTS);
assert_eq!(late.children.len(), MAX_NUMBER_OF_LIFECYCLE_EVENTS);
assert_eq!(
late.children.iter().map(|c| c.name.parse::<i32>().unwrap()).sorted().last().unwrap(),
449
);
}
async fn start_and_get_timestamp(
root_component: &Arc<ComponentInstance>,
moniker: &Moniker,
) -> zx::Time {
let component = root_component
.start_instance(moniker, &StartReason::Root)
.await
.expect("failed to bind");
let state = component.lock_state().await;
state.get_started_state().unwrap().timestamp
}
}