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