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