Skip to main content

component_debug/cli/
graph.rs

1// Copyright 2023 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::realm::{Instance, get_all_instances};
6use anyhow::Result;
7use std::collections::HashSet;
8use std::fmt::Write;
9use std::str::FromStr;
10use url::Url;
11
12use flex_fuchsia_sys2 as fsys;
13
14/// The starting part of our Graphviz graph output. This should be printed before any contents.
15static GRAPHVIZ_START: &str = r##"digraph {
16    graph [ pad = 0.2 ]
17    node [ shape = "box" color = "#2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 target = "_parent" margin = 0.22, ordering = out ];
18    edge [ color = "#37474f" penwidth = 1 arrowhead = none target = "_parent" fontname = "roboto mono" fontsize = 10 ]
19    splines = "ortho"
20"##;
21
22/// The ending part of our Graphviz graph output. This should be printed after `GRAPHVIZ_START` and the
23/// contents of the graph.
24static GRAPHVIZ_END: &str = "}";
25
26/// Filters that can be applied when creating component graphs
27#[derive(Debug, PartialEq)]
28pub enum GraphFilter {
29    /// Filters components that are an ancestor of the component with the given name.
30    /// Includes the named component.
31    Ancestor(String),
32    /// Filters components that are a descendant of the component with the given name.
33    /// Includes the named component.
34    Descendant(String),
35    /// Filters components that are a relative (either an ancestor or a descendant) of the
36    /// component with the given name. Includes the named component.
37    Relative(String),
38}
39
40impl FromStr for GraphFilter {
41    type Err = &'static str;
42
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        match s.split_once(":") {
45            Some((function, arg)) => match function {
46                "ancestor" | "ancestors" => Ok(Self::Ancestor(arg.to_string())),
47                "descendant" | "descendants" => Ok(Self::Descendant(arg.to_string())),
48                "relative" | "relatives" => Ok(Self::Relative(arg.to_string())),
49                _ => Err("unknown function for list filter."),
50            },
51            None => Err(
52                "list filter should be 'ancestors:<component_name>', 'descendants:<component_name>', or 'relatives:<component_name>'.",
53            ),
54        }
55    }
56}
57
58/// Determines the visual orientation of the graph's nodes.
59#[cfg_attr(feature = "serde", derive(serde::Serialize))]
60#[derive(Debug, Clone, Copy, PartialEq)]
61pub enum GraphOrientation {
62    /// The graph's nodes should be ordered from top to bottom.
63    TopToBottom,
64    /// The graph's nodes should be ordered from left to right.
65    LeftToRight,
66}
67
68impl FromStr for GraphOrientation {
69    type Err = &'static str;
70
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        match s.to_lowercase().replace("_", "").replace("-", "").as_str() {
73            "tb" | "toptobottom" => Ok(GraphOrientation::TopToBottom),
74            "lr" | "lefttoright" => Ok(GraphOrientation::LeftToRight),
75            _ => Err("graph orientation should be 'toptobottom' or 'lefttoright'."),
76        }
77    }
78}
79
80#[cfg_attr(feature = "serde", derive(serde::Serialize))]
81#[derive(Debug, Clone)]
82pub struct GraphResult {
83    pub instances: Vec<Instance>,
84    pub orientation: GraphOrientation,
85}
86
87impl std::fmt::Display for GraphResult {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        let output = create_dot_graph(&self.instances, self.orientation);
90        write!(f, "{}", output)
91    }
92}
93
94pub async fn graph_cmd(
95    filter: Option<GraphFilter>,
96    orientation: GraphOrientation,
97    realm_query: fsys::RealmQueryProxy,
98) -> Result<GraphResult> {
99    let mut instances = get_all_instances(&realm_query).await?;
100
101    instances = match filter {
102        Some(GraphFilter::Ancestor(m)) => filter_ancestors(instances, m),
103        Some(GraphFilter::Descendant(m)) => filter_descendants(instances, m),
104        Some(GraphFilter::Relative(m)) => filter_relatives(instances, m),
105        _ => instances,
106    };
107
108    Ok(GraphResult { instances, orientation })
109}
110
111fn filter_ancestors(instances: Vec<Instance>, child_str: String) -> Vec<Instance> {
112    let mut ancestors = HashSet::new();
113
114    // Find monikers with this child as the leaf.
115    for instance in &instances {
116        if let Some(child) = instance.moniker.leaf() {
117            if *child == child_str {
118                // Add this moniker to ancestor list.
119                let mut cur_moniker = instance.moniker.clone();
120                ancestors.insert(cur_moniker.clone());
121
122                // Loop over parents of this moniker and add them to ancestor list.
123                while let Some(parent) = cur_moniker.parent() {
124                    ancestors.insert(parent.clone());
125                    cur_moniker = parent;
126                }
127            }
128        }
129    }
130
131    instances.into_iter().filter(|i| ancestors.contains(&i.moniker)).collect()
132}
133
134fn filter_descendants(instances: Vec<Instance>, child_str: String) -> Vec<Instance> {
135    let mut descendants = HashSet::new();
136
137    // Find monikers with this child as the leaf.
138    for instance in &instances {
139        if let Some(child) = instance.moniker.leaf() {
140            if *child == child_str {
141                // Get all descendants of this moniker.
142                for possible_child_instance in &instances {
143                    if possible_child_instance.moniker.has_prefix(&instance.moniker) {
144                        descendants.insert(possible_child_instance.moniker.clone());
145                    }
146                }
147            }
148        }
149    }
150
151    instances.into_iter().filter(|i| descendants.contains(&i.moniker)).collect()
152}
153
154fn filter_relatives(instances: Vec<Instance>, child_str: String) -> Vec<Instance> {
155    let mut relatives = HashSet::new();
156
157    // Find monikers with this child as the leaf.
158    for instance in &instances {
159        if let Some(child) = instance.moniker.leaf() {
160            if *child == child_str {
161                // Loop over parents of this moniker and add them to relatives list.
162                let mut cur_moniker = instance.moniker.clone();
163                while let Some(parent) = cur_moniker.parent() {
164                    relatives.insert(parent.clone());
165                    cur_moniker = parent;
166                }
167
168                // Get all descendants of this moniker and add them to relatives list.
169                for possible_child_instance in &instances {
170                    if possible_child_instance.moniker.has_prefix(&instance.moniker) {
171                        relatives.insert(possible_child_instance.moniker.clone());
172                    }
173                }
174            }
175        }
176    }
177
178    instances.into_iter().filter(|i| relatives.contains(&i.moniker)).collect()
179}
180
181fn construct_codesearch_url(component_url: &str) -> String {
182    // Extract the last part of the component URL
183    let mut name_with_filetype = match component_url.rsplit_once("/") {
184        Some(parts) => parts.1.to_string(),
185        // No parts of the path contain `/`, this is already the last part of the component URL.
186        // Out-of-tree components may be standalone.
187        None => component_url.to_string(),
188    };
189    if name_with_filetype.ends_with(".cm") {
190        name_with_filetype.push('l');
191    }
192
193    // We mix dashes and underscores between the manifest name and the instance name
194    // sometimes, so search using both.
195    let name_with_underscores = name_with_filetype.replace("-", "_");
196    let name_with_dashes = name_with_filetype.replace("_", "-");
197
198    let query = if name_with_underscores == name_with_dashes {
199        format!("f:{}", name_with_underscores)
200    } else {
201        format!("f:{}|{}", name_with_underscores, name_with_dashes)
202    };
203
204    let mut code_search_url = Url::parse("https://cs.opensource.google/search").unwrap();
205    code_search_url.query_pairs_mut().append_pair("q", &query).append_pair("ss", "fuchsia/fuchsia");
206
207    code_search_url.into()
208}
209
210/// Create a graphviz dot graph from component instance information.
211pub fn create_dot_graph(instances: &[Instance], orientation: GraphOrientation) -> String {
212    let mut output = GRAPHVIZ_START.to_string();
213
214    // Switch the orientation of the graph.
215    match orientation {
216        GraphOrientation::TopToBottom => writeln!(output, r#"    rankdir = "TB""#).unwrap(),
217        GraphOrientation::LeftToRight => writeln!(output, r#"    rankdir = "LR""#).unwrap(),
218    };
219
220    for instance in instances {
221        let moniker = instance.moniker.to_string();
222        let label = if let Some(leaf) = instance.moniker.leaf() {
223            leaf.to_string()
224        } else {
225            ".".to_string()
226        };
227
228        // Running components are filled.
229        let running_attrs =
230            if instance.resolved_info.as_ref().map_or(false, |r| r.execution_info.is_some()) {
231                r##"style = "filled" fontcolor = "#ffffff""##
232            } else {
233                ""
234            };
235
236        // Components can be clicked to search for them on Code Search.
237        let url_attrs = if !instance.url.is_empty() {
238            let code_search_url = construct_codesearch_url(&instance.url);
239            format!(r#"href = "{}""#, code_search_url.as_str())
240        } else {
241            String::new()
242        };
243
244        // Draw the component.
245        writeln!(
246            output,
247            r#"    "{}" [ label = "{}" {} {} ]"#,
248            moniker, label, running_attrs, url_attrs
249        )
250        .unwrap();
251
252        // Component has a parent and the parent is also in the list of components
253        if let Some(parent_moniker) = instance.moniker.parent() {
254            if let Some(parent) = instances.iter().find(|i| i.moniker == parent_moniker) {
255                // Connect parent to component
256                writeln!(output, r#"    "{}" -> "{}""#, parent.moniker, moniker).unwrap();
257            }
258        }
259    }
260
261    writeln!(output, "{}", GRAPHVIZ_END).unwrap();
262    output
263}
264
265#[cfg(test)]
266mod test {
267    use super::*;
268    use crate::realm::{ExecutionInfo, ResolvedInfo};
269    use moniker::Moniker;
270
271    fn instances_for_test() -> Vec<Instance> {
272        vec![
273            Instance {
274                moniker: Moniker::root(),
275                url: "fuchsia-boot:///#meta/root.cm".to_owned(),
276                environment: None,
277                instance_id: None,
278                resolved_info: Some(ResolvedInfo {
279                    resolved_url: "fuchsia-boot:///#meta/root.cm".to_owned(),
280                    execution_info: None,
281                }),
282            },
283            Instance {
284                moniker: Moniker::parse_str("appmgr").unwrap(),
285                url: "fuchsia-pkg://fuchsia.com/appmgr#meta/appmgr.cm".to_owned(),
286                environment: None,
287                instance_id: None,
288                resolved_info: Some(ResolvedInfo {
289                    resolved_url: "fuchsia-pkg://fuchsia.com/appmgr#meta/appmgr.cm".to_owned(),
290                    execution_info: Some(ExecutionInfo {
291                        start_reason: "Debugging Workflow".to_owned(),
292                    }),
293                }),
294            },
295            Instance {
296                moniker: Moniker::parse_str("sys").unwrap(),
297                url: "fuchsia-pkg://fuchsia.com/sys#meta/sys.cm".to_owned(),
298                environment: None,
299                instance_id: None,
300                resolved_info: Some(ResolvedInfo {
301                    resolved_url: "fuchsia-pkg://fuchsia.com/sys#meta/sys.cm".to_owned(),
302                    execution_info: None,
303                }),
304            },
305            Instance {
306                moniker: Moniker::parse_str("sys/baz").unwrap(),
307                url: "fuchsia-pkg://fuchsia.com/baz#meta/baz.cm".to_owned(),
308                environment: None,
309                instance_id: None,
310                resolved_info: Some(ResolvedInfo {
311                    resolved_url: "fuchsia-pkg://fuchsia.com/baz#meta/baz.cm".to_owned(),
312                    execution_info: Some(ExecutionInfo {
313                        start_reason: "Debugging Workflow".to_owned(),
314                    }),
315                }),
316            },
317            Instance {
318                moniker: Moniker::parse_str("sys/fuzz").unwrap(),
319                url: "fuchsia-pkg://fuchsia.com/fuzz#meta/fuzz.cm".to_owned(),
320                environment: None,
321                instance_id: None,
322                resolved_info: Some(ResolvedInfo {
323                    resolved_url: "fuchsia-pkg://fuchsia.com/fuzz#meta/fuzz.cm".to_owned(),
324                    execution_info: None,
325                }),
326            },
327            Instance {
328                moniker: Moniker::parse_str("sys/fuzz/hello").unwrap(),
329                url: "fuchsia-pkg://fuchsia.com/hello#meta/hello.cm".to_owned(),
330                environment: None,
331                instance_id: None,
332                resolved_info: Some(ResolvedInfo {
333                    resolved_url: "fuchsia-pkg://fuchsia.com/hello#meta/hello.cm".to_owned(),
334                    execution_info: None,
335                }),
336            },
337        ]
338    }
339
340    // The tests in this file are change-detectors because they will fail on
341    // any style changes to the graph. This isn't great, but it makes it easy
342    // to view the changes in a Graphviz visualizer.
343    async fn test_graph_orientation(orientation: GraphOrientation, expected_rankdir: &str) {
344        let instances = instances_for_test();
345
346        let graph = create_dot_graph(&instances, orientation);
347        pretty_assertions::assert_eq!(
348            graph,
349            format!(
350                r##"digraph {{
351    graph [ pad = 0.2 ]
352    node [ shape = "box" color = "#2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 target = "_parent" margin = 0.22, ordering = out ];
353    edge [ color = "#37474f" penwidth = 1 arrowhead = none target = "_parent" fontname = "roboto mono" fontsize = 10 ]
354    splines = "ortho"
355    rankdir = "{}"
356    "." [ label = "."  href = "https://cs.opensource.google/search?q=f%3Aroot.cml&ss=fuchsia%2Ffuchsia" ]
357    "appmgr" [ label = "appmgr" style = "filled" fontcolor = "#ffffff" href = "https://cs.opensource.google/search?q=f%3Aappmgr.cml&ss=fuchsia%2Ffuchsia" ]
358    "." -> "appmgr"
359    "sys" [ label = "sys"  href = "https://cs.opensource.google/search?q=f%3Asys.cml&ss=fuchsia%2Ffuchsia" ]
360    "." -> "sys"
361    "sys/baz" [ label = "baz" style = "filled" fontcolor = "#ffffff" href = "https://cs.opensource.google/search?q=f%3Abaz.cml&ss=fuchsia%2Ffuchsia" ]
362    "sys" -> "sys/baz"
363    "sys/fuzz" [ label = "fuzz"  href = "https://cs.opensource.google/search?q=f%3Afuzz.cml&ss=fuchsia%2Ffuchsia" ]
364    "sys" -> "sys/fuzz"
365    "sys/fuzz/hello" [ label = "hello"  href = "https://cs.opensource.google/search?q=f%3Ahello.cml&ss=fuchsia%2Ffuchsia" ]
366    "sys/fuzz" -> "sys/fuzz/hello"
367}}
368"##,
369                expected_rankdir
370            )
371        );
372    }
373
374    #[fuchsia_async::run_singlethreaded(test)]
375    async fn test_graph_top_to_bottom_orientation() {
376        test_graph_orientation(GraphOrientation::TopToBottom, "TB").await;
377    }
378
379    #[fuchsia_async::run_singlethreaded(test)]
380    async fn test_graph_left_to_right_orientation() {
381        test_graph_orientation(GraphOrientation::LeftToRight, "LR").await;
382    }
383}