From 712b528ca8c6fc39e48b53a3465c6df6a2234068 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 3 Nov 2024 13:12:05 -0500 Subject: [PATCH] Add graphviz dump --- crates/compute_graph/src/lib.rs | 76 +++++++++++++++++++++++++++++++- crates/compute_graph/src/node.rs | 43 +++++++++++++++++- crates/compute_graph/src/rule.rs | 12 ++++- crates/compute_graph/test.dot | 7 +++ crates/compute_graph/test.svg | 42 ++++++++++++++++++ 5 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 crates/compute_graph/test.dot create mode 100644 crates/compute_graph/test.svg diff --git a/crates/compute_graph/src/lib.rs b/crates/compute_graph/src/lib.rs index cc39f12..f81d0f6 100644 --- a/crates/compute_graph/src/lib.rs +++ b/crates/compute_graph/src/lib.rs @@ -50,7 +50,7 @@ mod util; use builder::{BuildGraphError, GraphBuilder}; use node::{ErasedNode, NodeValue}; -use petgraph::visit::{IntoEdgeReferences, NodeIndexable}; +use petgraph::visit::{IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef}; use petgraph::{stable_graph::StableDiGraph, visit::EdgeRef}; use rule::{AsyncRule, Input, InputVisitor, Rule}; use std::cell::{Cell, RefCell}; @@ -192,6 +192,59 @@ impl Graph { is_valid: Rc::clone(&self.is_valid), } } + + /// Creates a graphviz representation of this graph. + /// + /// The exact format is not guaranteed to be stable. + pub fn as_dot_string(&self) -> String { + // Escaper/Escaped are cribbed from petgraph (MIT licensed). + use std::fmt::Write; + struct Escaper(W); + impl std::fmt::Write for Escaper { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + for c in s.chars() { + self.write_char(c)?; + } + Ok(()) + } + fn write_char(&mut self, c: char) -> std::fmt::Result { + match c { + '"' | '\\' => self.0.write_char('\\')?, + // \l is for left justified linebreak + '\n' => return self.0.write_str("\\l"), + _ => {} + } + self.0.write_char(c) + } + } + struct Escaped(T); + impl std::fmt::Debug for Escaped { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(&mut Escaper(f), "{:?}", &self.0) + } + } + + // Our own dot formatter, to include both node ids and labels. + struct Dot<'a, S: Synchronicity>(&'a NodeGraph); + impl<'a, S: Synchronicity> std::fmt::Debug for Dot<'a, S> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "digraph {{")?; + for node in self.0.node_references() { + let id = self.0.to_index(node.id()); + let label = Escaped(node.weight()); + writeln!(f, "\t{id} [label =\"{label:?} (id={id})\"]")?; + } + for edge in self.0.edge_references() { + let source = self.0.to_index(edge.source()); + let target = self.0.to_index(edge.target()); + writeln!(f, "\t{source} -> {target} []")?; + } + writeln!(f, "}}") + } + } + let graph = self.node_graph.borrow(); + format!("{:?}", Dot(&*graph)) + } } impl Graph { @@ -630,4 +683,25 @@ mod tests { let mut graph = builder.build().unwrap(); assert_eq!(*graph.evaluate_async().await, 43); } + + #[test] + fn graphviz() { + let mut builder = GraphBuilder::new(); + let a = builder.add_value(1); + let b = builder.add_value(2); + builder.set_output(Add(a, b)); + let graph = builder.build().unwrap(); + println!("{}", graph.as_dot_string()); + assert_eq!( + graph.as_dot_string(), + r#"digraph { + 0 [label ="ConstNode (id=0)"] + 1 [label ="ConstNode (id=1)"] + 2 [label ="RuleNode (id=2)"] + 0 -> 2 [] + 1 -> 2 [] +} +"# + ) + } } diff --git a/crates/compute_graph/src/node.rs b/crates/compute_graph/src/node.rs index 20401a5..32f54b0 100644 --- a/crates/compute_graph/src/node.rs +++ b/crates/compute_graph/src/node.rs @@ -11,6 +11,7 @@ pub(crate) struct ErasedNode { invalidate: Box) -> ()>, visit_inputs: Box, &mut dyn FnMut(NodeId) -> ()) -> ()>, update: Box Fn(&'a mut Box) -> Synch::UpdateResult<'a>>, + debug_fmt: Box, &mut std::fmt::Formatter<'_>) -> std::fmt::Result>, } impl ErasedNode { @@ -36,6 +37,10 @@ impl ErasedNode { let x = any.downcast_mut::>>().unwrap(); x.update() }), + debug_fmt: Box::new(|any, f| { + let x = any.downcast_ref::>>().unwrap(); + x.fmt(f) + }), } } @@ -62,7 +67,13 @@ impl ErasedNode { } } -pub(crate) trait Node { +impl std::fmt::Debug for ErasedNode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + (self.debug_fmt)(&self.any, f) + } +} + +pub(crate) trait Node: std::fmt::Debug { fn is_valid(&self) -> bool; fn invalidate(&mut self); fn visit_inputs(&self, visitor: &mut dyn FnMut(NodeId) -> ()); @@ -136,6 +147,12 @@ impl Node for ConstNode { } } +impl std::fmt::Debug for ConstNode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ConstNode<{}>", std::any::type_name::()) + } +} + pub(crate) struct InvalidatableConstNode { value: Rc>>, valid: bool, @@ -175,6 +192,12 @@ impl Node for InvalidatableConstNode } } +impl std::fmt::Debug for InvalidatableConstNode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "InvalidatableConstNode<{}>", std::any::type_name::()) + } +} + pub(crate) struct RuleNode { rule: R, value: Rc>>, @@ -233,6 +256,12 @@ impl Node for RuleNode } } +impl std::fmt::Debug for RuleNode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "RuleNode<{}>", std::any::type_name::()) + } +} + pub(crate) struct AsyncConstNode F, F: Future> { provider: Option

, value: Rc>>, @@ -279,6 +308,12 @@ impl F, F: Future> Node F, F: Future> std::fmt::Debug for AsyncConstNode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "AsyncConstNode<{}>", std::any::type_name::()) + } +} + pub(crate) struct AsyncRuleNode { rule: R, value: Rc>>, @@ -338,3 +373,9 @@ impl Node for AsyncRuleNode &self.value } } + +impl std::fmt::Debug for AsyncRuleNode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "AsyncRuleNode<{}>", std::any::type_name::()) + } +} diff --git a/crates/compute_graph/src/rule.rs b/crates/compute_graph/src/rule.rs index 9a95971..6674b1d 100644 --- a/crates/compute_graph/src/rule.rs +++ b/crates/compute_graph/src/rule.rs @@ -88,7 +88,6 @@ pub trait InputVisitable { /// To obtain an input, add a value or rule to a [`GraphBuilder`](`crate::builder::GraphBuilder`). /// /// Note that this type implements `Clone`, so can be cloned and used as an input for multiple nodes. -#[derive(Debug)] pub struct Input { pub(crate) node_idx: NodeId, pub(crate) value: Rc>>, @@ -116,6 +115,17 @@ impl Clone for Input { } } +impl std::fmt::Debug for Input { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Input<{}>({:?})", + std::any::type_name::(), + self.node_idx + ) + } +} + // TODO: i really want Input to be able to implement Deref somehow /// A type that can visit arbitrary [`Input`]s. diff --git a/crates/compute_graph/test.dot b/crates/compute_graph/test.dot new file mode 100644 index 0000000..dbce12a --- /dev/null +++ b/crates/compute_graph/test.dot @@ -0,0 +1,7 @@ +digraph { + 0 [label ="ConstNode (id=0)"] + 1 [label ="ConstNode (id=1)"] + 2 [label ="RuleNode (id=2)"] + 0 -> 2 [] + 1 -> 2 [] +} diff --git a/crates/compute_graph/test.svg b/crates/compute_graph/test.svg new file mode 100644 index 0000000..cd50597 --- /dev/null +++ b/crates/compute_graph/test.svg @@ -0,0 +1,42 @@ + + + + + + + + + +0 + +ConstNode<i32> (id=0) + + + +2 + +RuleNode<compute_graph::tests::Add> (id=2) + + + +0->2 + + + + + +1 + +ConstNode<i32> (id=1) + + + +1->2 + + + + +