Add graphviz dump

This commit is contained in:
Shadowfacts 2024-11-03 13:12:05 -05:00
parent dd1143aa9b
commit 712b528ca8
5 changed files with 177 additions and 3 deletions

View File

@ -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<O: 'static, S: Synchronicity> Graph<O, S> {
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>(W);
impl<W: std::fmt::Write> std::fmt::Write for Escaper<W> {
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>(T);
impl<T: std::fmt::Debug> std::fmt::Debug for Escaped<T> {
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<S>);
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<O: 'static> Graph<O, Synchronous> {
@ -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<i32> (id=0)"]
1 [label ="ConstNode<i32> (id=1)"]
2 [label ="RuleNode<compute_graph::tests::Add> (id=2)"]
0 -> 2 []
1 -> 2 []
}
"#
)
}
}

View File

@ -11,6 +11,7 @@ pub(crate) struct ErasedNode<Synch: Synchronicity> {
invalidate: Box<dyn Fn(&mut Box<dyn Any>) -> ()>,
visit_inputs: Box<dyn Fn(&Box<dyn Any>, &mut dyn FnMut(NodeId) -> ()) -> ()>,
update: Box<dyn for<'a> Fn(&'a mut Box<dyn Any>) -> Synch::UpdateResult<'a>>,
debug_fmt: Box<dyn Fn(&Box<dyn Any>, &mut std::fmt::Formatter<'_>) -> std::fmt::Result>,
}
impl<S: Synchronicity> ErasedNode<S> {
@ -36,6 +37,10 @@ impl<S: Synchronicity> ErasedNode<S> {
let x = any.downcast_mut::<Box<dyn Node<V, S>>>().unwrap();
x.update()
}),
debug_fmt: Box::new(|any, f| {
let x = any.downcast_ref::<Box<dyn Node<V, S>>>().unwrap();
x.fmt(f)
}),
}
}
@ -62,7 +67,13 @@ impl ErasedNode<Asynchronous> {
}
}
pub(crate) trait Node<Value: NodeValue, Synch: Synchronicity> {
impl<S: Synchronicity> std::fmt::Debug for ErasedNode<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
(self.debug_fmt)(&self.any, f)
}
}
pub(crate) trait Node<Value: NodeValue, Synch: Synchronicity>: 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<V: NodeValue, S: Synchronicity> Node<V, S> for ConstNode<V, S> {
}
}
impl<V, S> std::fmt::Debug for ConstNode<V, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ConstNode<{}>", std::any::type_name::<V>())
}
}
pub(crate) struct InvalidatableConstNode<V, S> {
value: Rc<RefCell<Option<V>>>,
valid: bool,
@ -175,6 +192,12 @@ impl<V: NodeValue, S: Synchronicity> Node<V, S> for InvalidatableConstNode<V, S>
}
}
impl<V, S> std::fmt::Debug for InvalidatableConstNode<V, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "InvalidatableConstNode<{}>", std::any::type_name::<V>())
}
}
pub(crate) struct RuleNode<R, V, S> {
rule: R,
value: Rc<RefCell<Option<V>>>,
@ -233,6 +256,12 @@ impl<R: Rule, S: Synchronicity> Node<R::Output, S> for RuleNode<R, R::Output, S>
}
}
impl<R, V, S> std::fmt::Debug for RuleNode<R, V, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "RuleNode<{}>", std::any::type_name::<R>())
}
}
pub(crate) struct AsyncConstNode<V, P: FnOnce() -> F, F: Future<Output = V>> {
provider: Option<P>,
value: Rc<RefCell<Option<V>>>,
@ -279,6 +308,12 @@ impl<V: NodeValue, P: FnOnce() -> F, F: Future<Output = V>> Node<V, Asynchronous
}
}
impl<V, P: FnOnce() -> F, F: Future<Output = V>> std::fmt::Debug for AsyncConstNode<V, P, F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "AsyncConstNode<{}>", std::any::type_name::<V>())
}
}
pub(crate) struct AsyncRuleNode<R, V> {
rule: R,
value: Rc<RefCell<Option<V>>>,
@ -338,3 +373,9 @@ impl<R: AsyncRule> Node<R::Output, Asynchronous> for AsyncRuleNode<R, R::Output>
&self.value
}
}
impl<R, V> std::fmt::Debug for AsyncRuleNode<R, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "AsyncRuleNode<{}>", std::any::type_name::<R>())
}
}

View File

@ -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<T> {
pub(crate) node_idx: NodeId,
pub(crate) value: Rc<RefCell<Option<T>>>,
@ -116,6 +115,17 @@ impl<T> Clone for Input<T> {
}
}
impl<T> std::fmt::Debug for Input<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Input<{}>({:?})",
std::any::type_name::<T>(),
self.node_idx
)
}
}
// TODO: i really want Input to be able to implement Deref somehow
/// A type that can visit arbitrary [`Input`]s.

View File

@ -0,0 +1,7 @@
digraph {
0 [label ="ConstNode<i32> (id=0)"]
1 [label ="ConstNode<i32> (id=1)"]
2 [label ="RuleNode<compute_graph::tests::Add> (id=2)"]
0 -> 2 []
1 -> 2 []
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 10.0.1 (20240210.2158)
-->
<!-- Pages: 1 -->
<svg width="432pt" height="116pt"
viewBox="0.00 0.00 432.02 116.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 112)">
<polygon fill="white" stroke="none" points="-4,4 -4,-112 428.02,-112 428.02,4 -4,4"/>
<!-- 0 -->
<g id="node1" class="node">
<title>0</title>
<ellipse fill="none" stroke="black" cx="101.51" cy="-90" rx="101.51" ry="18"/>
<text text-anchor="middle" x="101.51" y="-84.95" font-family="Times,serif" font-size="14.00">ConstNode&lt;i32&gt; (id=0)</text>
</g>
<!-- 2 -->
<g id="node3" class="node">
<title>2</title>
<ellipse fill="none" stroke="black" cx="211.51" cy="-18" rx="185.96" ry="18"/>
<text text-anchor="middle" x="211.51" y="-12.95" font-family="Times,serif" font-size="14.00">RuleNode&lt;compute_graph::tests::Add&gt; (id=2)</text>
</g>
<!-- 0&#45;&gt;2 -->
<g id="edge1" class="edge">
<title>0&#45;&gt;2</title>
<path fill="none" stroke="black" d="M127.86,-72.23C142.07,-63.19 159.82,-51.89 175.31,-42.03"/>
<polygon fill="black" stroke="black" points="176.78,-45.25 183.33,-36.93 173.02,-39.34 176.78,-45.25"/>
</g>
<!-- 1 -->
<g id="node2" class="node">
<title>1</title>
<ellipse fill="none" stroke="black" cx="322.51" cy="-90" rx="101.51" ry="18"/>
<text text-anchor="middle" x="322.51" y="-84.95" font-family="Times,serif" font-size="14.00">ConstNode&lt;i32&gt; (id=1)</text>
</g>
<!-- 1&#45;&gt;2 -->
<g id="edge2" class="edge">
<title>1&#45;&gt;2</title>
<path fill="none" stroke="black" d="M295.92,-72.23C281.58,-63.19 263.67,-51.89 248.04,-42.03"/>
<polygon fill="black" stroke="black" points="250.26,-39.3 239.93,-36.92 246.52,-45.22 250.26,-39.3"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB