1use quote::ToTokens;
17use syn::{Ident, Path};
18
19use indexmap::IndexMap;
20use petgraph::{graph::NodeIndex, Graph};
21use std::collections::{hash_map::RandomState, HashMap, HashSet};
22
23use super::*;
24
25pub(crate) struct ConnectionGraph<'a> {
27 pub(crate) graph: Graph<Ident, Path>,
33 #[cfg_attr(not(feature = "dotgraph"), allow(dead_code))]
35 pub(crate) sccs: Vec<Vec<NodeIndex>>,
36 #[cfg_attr(not(feature = "dotgraph"), allow(dead_code))]
39 pub(crate) unsent_messages: IndexMap<&'a Path, (&'a Ident, NodeIndex)>,
40 #[cfg_attr(not(feature = "dotgraph"), allow(dead_code))]
43 pub(crate) unconsumed_messages: IndexMap<&'a Path, Vec<(&'a Ident, NodeIndex)>>,
44}
45
46impl<'a> ConnectionGraph<'a> {
47 pub(crate) fn construct(ssfs: &'a [SubSysField]) -> Self {
49 let mut outgoing_lut = IndexMap::<&Path, Vec<(&Ident, NodeIndex)>>::with_capacity(128);
53 let mut consuming_lut = IndexMap::<&Path, (&Ident, NodeIndex)>::with_capacity(128);
55
56 let mut graph = Graph::<Ident, Path>::new();
57
58 for ssf in ssfs {
60 let node_index = graph.add_node(ssf.generic.clone());
61 for outgoing in ssf.messages_to_send.iter() {
62 outgoing_lut.entry(outgoing).or_default().push((&ssf.generic, node_index));
63 }
64 if let Some(ref consumes) = ssf.message_to_consume {
65 if let Some(_first_consument) =
66 consuming_lut.insert(consumes, (&ssf.generic, node_index))
67 {
68 }
70 }
71 }
72
73 for (message_ty, (_consuming_subsystem_ident, consuming_node_index)) in consuming_lut.iter()
74 {
75 if let Some(origin_subsystems) = outgoing_lut.get(message_ty) {
77 for (_origin_subsystem_ident, sending_node_index) in origin_subsystems.iter() {
78 graph.add_edge(
79 *sending_node_index,
80 *consuming_node_index,
81 (*message_ty).clone(),
82 );
83 }
84 }
85 }
86
87 let outgoing_set = HashSet::<_, RandomState>::from_iter(outgoing_lut.keys().cloned());
89 let consuming_set = HashSet::<_, RandomState>::from_iter(consuming_lut.keys().cloned());
90
91 let mut unsent_messages = consuming_lut;
92 unsent_messages.retain(|k, _v| !outgoing_set.contains(k));
93
94 let mut unconsumed_messages = outgoing_lut;
95 unconsumed_messages.retain(|k, _v| !consuming_set.contains(k));
96
97 let scc = Self::extract_scc(&graph);
98
99 Self { graph, sccs: scc, unsent_messages, unconsumed_messages }
100 }
101
102 fn extract_scc(graph: &Graph<Ident, Path>) -> Vec<Vec<NodeIndex>> {
105 use petgraph::visit::EdgeRef;
106
107 let sccs = petgraph::algo::kosaraju_scc(&graph);
109 let sccs = Vec::from_iter(sccs.into_iter().filter(|scc| {
110 match scc.len() {
111 1 => {
112 let node_idx = scc[0];
115 graph
116 .edges_directed(node_idx, petgraph::Direction::Outgoing)
117 .find(|edge| edge.target() == node_idx)
118 .is_some()
119 },
120 0 => false,
121 _n => true,
122 }
123 }));
124 match sccs.len() {
125 0 => eprintln!("✅ Found no strongly connected components, hence no cycles exist"),
126 1 => eprintln!(
127 "⚡ Found 1 strongly connected component which includes at least one cycle"
128 ),
129 n => eprintln!(
130 "⚡ Found {n} strongly connected components which includes at least one cycle each"
131 ),
132 }
133
134 let greek_alphabet = greek_alphabet();
135
136 for (scc_idx, scc) in sccs.iter().enumerate() {
137 let scc_tag = greek_alphabet.get(scc_idx).copied().unwrap_or('_');
138 let mut acc = Vec::with_capacity(scc.len());
139 assert!(scc.len() > 0);
140 let mut node_idx = scc[0].clone();
141 let print_idx = scc_idx + 1;
142 let mut visited = HashMap::new();
146 for step in 0..scc.len() {
147 if let Some(edge) =
148 graph.edges_directed(node_idx, petgraph::Direction::Outgoing).find(|edge| {
149 scc.iter().find(|&scc_node_idx| *scc_node_idx == edge.target()).is_some()
150 }) {
151 let next = edge.target();
152 visited.insert(node_idx, step);
153
154 let subsystem_name = &graph[node_idx].to_string();
155 let message_name = &graph[edge.id()].to_token_stream().to_string();
156 acc.push(format!("{subsystem_name} ~~{{{message_name:?}}}~~> "));
157 node_idx = next;
158
159 if let Some(step) = visited.get(&next) {
160 assert!(acc.len() >= *step);
163 acc.drain(..step);
164 break
168 }
169 } else {
170 eprintln!("cycle({print_idx:03}) ∈ {scc_tag}: Missing connection in hypothesized cycle after {step} steps, this is a bug 🐛");
171 break
172 }
173 }
174 let acc = String::from_iter(acc);
175 eprintln!("cycle({print_idx:03}) ∈ {scc_tag}: {acc} *");
176 }
177
178 sccs
179 }
180
181 #[cfg(feature = "dotgraph")]
182 fn convert_and_write_all<'b>(
183 dot: &petgraph::dot::Dot<'b, &'b petgraph::graph::Graph<Ident, Path>>,
184 dest: impl AsRef<std::path::Path>,
185 ) -> anyhow::Result<()> {
186 use self::graph_helpers::color_scheme;
187
188 let dot_content = format!(
189 r#"digraph {{
190 fontname="Cantarell"
191 bgcolor="white"
192 label = "orchestra message flow between subsystems"
193node [colorscheme={}]
194{:?}
195}}"#,
196 color_scheme(),
197 &dot
198 );
199
200 let dest = dest.as_ref();
201 let dest = dest.to_path_buf();
202
203 write_to_disk(&dest, "dot", &dot_content)?;
204
205 let svg_content = convert_dot_to_svg(&dot_content)?;
206
207 write_to_disk(&dest, "svg", &svg_content)?;
208
209 Ok(())
210 }
211
212 #[cfg(feature = "dotgraph")]
216 pub(crate) fn render_graphs<'b>(self, dest: impl AsRef<std::path::Path>) -> anyhow::Result<()> {
217 use self::graph_helpers::*;
218 use petgraph::{
219 dot::{self, Dot},
220 visit::{EdgeRef, IntoEdgeReferences, IntoNodeReferences},
221 };
222
223 let config = &[
225 dot::Config::GraphContentOnly,
226 dot::Config::EdgeNoLabel,
227 dot::Config::NodeNoLabel,
228 ][..];
229
230 let Self { mut graph, unsent_messages, unconsumed_messages, sccs } = self;
231
232 let greek_alphabet = greek_alphabet();
234
235 const COLOR_SCHEME_N: usize = 10; const UPPER_BOUND: usize = 10;
239
240 assert!(UPPER_BOUND <= GREEK_ALPHABET_SIZE);
241 assert!(UPPER_BOUND <= COLOR_SCHEME_N);
242
243 let n = sccs.len();
244 let n = if n > UPPER_BOUND {
245 eprintln!("Too many ({n}) strongly connected components, only annotating the first {UPPER_BOUND}");
246 UPPER_BOUND
247 } else {
248 n
249 };
250
251 let mut scc_lut = HashMap::<NodeIndex, HashSet<char>>::with_capacity(n);
253 let mut color_lut = HashMap::<char, usize>::with_capacity(COLOR_SCHEME_N);
256 for (scc_idx, scc) in sccs.into_iter().take(UPPER_BOUND).enumerate() {
257 for node_idx in scc {
258 let _ = scc_lut.entry(node_idx).or_default().insert(greek_alphabet[scc_idx]);
259 }
260 color_lut.insert(greek_alphabet[scc_idx], scc_idx + 1);
261 }
262 let color_lut = &color_lut;
263
264 let unconsumed_idx = graph.add_node(quote::format_ident!("SENT_TO_NONONE"));
269 for (message_name, subsystems) in unconsumed_messages {
270 for (_sub_name, sub_node_idx) in subsystems {
272 graph.add_edge(sub_node_idx, unconsumed_idx, message_name.clone());
273 }
274 }
275
276 let unsent_idx = graph.add_node(quote::format_ident!("NEVER_SENT_ANYWHERE"));
280 for (message_name, (_sub_name, sub_node_idx)) in unsent_messages {
281 graph.add_edge(unsent_idx, sub_node_idx, message_name.clone());
282 }
283 let unsent_node_label = r#"label="✨",fillcolor=black,shape=doublecircle,style=filled,fontname="NotoColorEmoji""#;
284 let unconsumed_node_label = r#"label="💀",fillcolor=black,shape=doublecircle,style=filled,fontname="NotoColorEmoji""#;
285
286 let get_edge_attributes = {
287 |_graph: &Graph<Ident, Path>,
288 edge: <&Graph<Ident, Path> as IntoEdgeReferences>::EdgeRef|
289 -> String {
290 let source = edge.source();
291 let sink = edge.target();
292
293 let message_name =
294 edge.weight().get_ident().expect("Must have a trailing identifier. qed");
295
296 if let Some(edge_intersecting_scc_tags) =
298 scc_lut.get(&source).and_then(|source_set| {
299 scc_lut.get(&sink).and_then(move |sink_set| {
300 let intersection = HashSet::<_, RandomState>::from_iter(
301 source_set.intersection(sink_set),
302 );
303 if intersection.is_empty() {
304 None
305 } else {
306 Some(intersection)
307 }
308 })
309 }) {
310 if edge_intersecting_scc_tags.len() != 1 {
311 unreachable!(
312 "Strongly connected components are disjunct by definition. qed"
313 );
314 }
315 let scc_tag = edge_intersecting_scc_tags.iter().next().unwrap();
316 let color = get_color_by_tag(scc_tag, color_lut);
317 let scc_tag_str =
318 cycle_tags_to_annotation(edge_intersecting_scc_tags, color_lut);
319 format!(
320 r#"color="{color}",fontcolor="{color}",xlabel=<{scc_tag_str}>,label="{message_name}""#,
321 )
322 } else {
323 format!(r#"label="{message_name}""#,)
324 }
325 }
326 };
327 let get_node_attributes = {
328 |_graph: &Graph<Ident, Path>,
329 (node_index, subsystem_name): <&Graph<Ident, Path> as IntoNodeReferences>::NodeRef|
330 -> String {
331 if node_index == unsent_idx {
332 unsent_node_label.to_owned().clone()
333 } else if node_index == unconsumed_idx {
334 unconsumed_node_label.to_owned().clone()
335 } else if let Some(edge_intersecting_scc_tags) = scc_lut.get(&node_index) {
336 if edge_intersecting_scc_tags.len() != 1 {
337 unreachable!(
338 "Strongly connected components are disjunct by definition. qed"
339 );
340 };
341 let scc_tag = edge_intersecting_scc_tags.iter().next().unwrap();
342 let color = get_color_by_tag(scc_tag, color_lut);
343
344 let scc_tag_str =
345 cycle_tags_to_annotation(edge_intersecting_scc_tags, color_lut);
346 format!(
347 r#"color="{color}",fontcolor="{color}",xlabel=<{scc_tag_str}>,label="{subsystem_name}""#,
348 )
349 } else {
350 format!(r#"label="{subsystem_name}""#)
351 }
352 }
353 };
354 let dotgraph = Dot::with_attr_getters(
356 &graph,
357 config,
358 &get_edge_attributes, &get_node_attributes,
360 );
361
362 Self::convert_and_write_all(&dotgraph, dest)?;
363
364 Ok(())
365 }
366}
367
368#[cfg(feature = "dotgraph")]
369fn convert_dot_to_svg(dot_content: impl AsRef<str>) -> anyhow::Result<String> {
370 let mut parser = dotlay::gv::DotParser::new(dot_content.as_ref());
371 let graph = parser.process().map_err(|err_msg| {
372 parser.print_error();
373 anyhow::anyhow!(err_msg).context("Failed to parse dotfile content")
374 })?;
375 let mut svg = dotlay::backends::svg::SVGWriter::new();
376 let mut builder = dotlay::gv::GraphBuilder::default();
377 builder.visit_graph(&graph);
378 let mut vg = builder.get();
379 vg.do_it(true, false, false, &mut svg);
380 Ok(svg.finalize())
381}
382
383#[cfg(feature = "dotgraph")]
384fn write_to_disk(
385 dest: impl AsRef<std::path::Path>,
386 ext: &str,
387 content: impl AsRef<[u8]>,
388) -> std::io::Result<()> {
389 use fs_err as fs;
390 let dest = dest.as_ref().with_extension(ext);
391 print!("Writing {} to {} ..", ext, dest.display());
392 fs::write(dest, content.as_ref())?;
393 println!(" OK");
394 Ok(())
395}
396
397const GREEK_ALPHABET_SIZE: usize = 24;
398
399fn greek_alphabet() -> [char; GREEK_ALPHABET_SIZE] {
400 let mut alphabet = ['\u{03B1}'; 24];
401 alphabet
402 .iter_mut()
403 .enumerate()
404 .for_each(|(i, c)| {
407 *c = char::from_u32(*c as u32 + i as u32).unwrap();
408 });
409 alphabet
410}
411
412#[cfg(feature = "dotgraph")]
413mod graph_helpers {
414 use super::HashMap;
415
416 pub(crate) const fn color_scheme() -> &'static str {
417 "rdylgn10"
418 }
419
420 pub(crate) fn get_color_by_idx(color_idx: usize) -> String {
421 let scheme = color_scheme();
422 format!("/{scheme}/{color_idx}")
423 }
424
425 pub(crate) fn get_color_by_tag(scc_tag: &char, color_lut: &HashMap<char, usize>) -> String {
426 get_color_by_idx(color_lut.get(scc_tag).copied().unwrap_or_default())
427 }
428
429 pub(crate) fn cycle_tags_to_annotation<'a>(
432 cycle_tags: impl IntoIterator<Item = &'a char>,
433 color_lut: &HashMap<char, usize>,
434 ) -> String {
435 let cycle_annotation = String::from_iter(itertools::Itertools::intersperse(
438 cycle_tags.into_iter().map(|scc_tag| {
439 let color = get_color_by_tag(scc_tag, color_lut);
440 format!(r#"<B><FONT COLOR="{color}">{scc_tag}</FONT></B>"#)
441 }),
442 ",".to_owned(),
443 ));
444 cycle_annotation
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 #[test]
457 #[should_panic]
458 fn check_ident() {
459 let _ident = quote::format_ident!("x💀x");
460 }
461
462 #[test]
463 fn kosaraju_scc_check_nodes_cannot_be_part_of_two_clusters() {
464 let mut graph = petgraph::graph::DiGraph::<char, &str>::new();
465
466 let a_idx = graph.add_node('A');
467 let b_idx = graph.add_node('B');
468 let c_idx = graph.add_node('C');
469 let d_idx = graph.add_node('D');
470 let e_idx = graph.add_node('E');
471 let f_idx = graph.add_node('F');
472
473 graph.add_edge(a_idx, b_idx, "10");
474 graph.add_edge(b_idx, c_idx, "11");
475 graph.add_edge(c_idx, a_idx, "12");
476
477 graph.add_edge(a_idx, d_idx, "20");
478 graph.add_edge(d_idx, c_idx, "21");
479
480 graph.add_edge(b_idx, e_idx, "30");
481 graph.add_edge(e_idx, c_idx, "31");
482
483 graph.add_edge(c_idx, f_idx, "40");
484
485 let mut sccs = dbg!(petgraph::algo::kosaraju_scc(&graph));
486
487 dbg!(graph);
488
489 sccs.sort_by(|a, b| {
490 if a.len() < b.len() {
491 std::cmp::Ordering::Greater
492 } else {
493 std::cmp::Ordering::Less
494 }
495 });
496 assert_eq!(sccs.len(), 2); assert_eq!(sccs[0].len(), 5); }
499
500 #[cfg(feature = "dotgraph")]
501 #[ignore]
502 #[test]
503 fn dot_to_svg_works() {
504 use super::convert_dot_to_svg;
505
506 let s = include_str!("../tests/assets/sample.dot");
507 convert_dot_to_svg(s).unwrap();
508 }
509}