Skip to content

Commit

Permalink
Refresh graph logging: use graphviz syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
cben committed Sep 11, 2017
1 parent cc2ed9d commit fb6732f
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 8 deletions.
37 changes: 37 additions & 0 deletions app/models/manager_refresh/graph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@ def initialize(nodes)
construct_graph!(@nodes)
end

def to_graphviz(layers: nil)
node_names = friendly_unique_node_names
s = []

s << "digraph {"
(layers || [nodes]).each_with_index do |layer_nodes, i|
s << " subgraph cluster_#{i} { label = \"Layer #{i}\";" unless layers.nil?

layer_nodes.each do |n|
s << " #{node_names[n]}; \t// #{n.inspect}"
end

s << " }" unless layers.nil?
end

s << " // edges:"
edges.each do |from, to|
s << " #{node_names[from]} -> #{node_names[to]};"
end
s << "}"
s.join("\n") + "\n"
end

protected

attr_writer :nodes, :edges, :fixed_edges
Expand Down Expand Up @@ -71,5 +94,19 @@ def traverse_dependecies(traversed_nodes, starting_node, current_node, edges, de
def node_edges(edges, node)
edges.select { |e| e.second == node }
end

# Hash of {node => name}, appending numbers if needed to make unique, quoted if needed.
def friendly_unique_node_names
node_names = {}
# Try to use shorter .name method that InventoryCollection has.
nodes.group_by { |n| n.respond_to?(:name) ? n.name.to_s : n.to_s }.each do |base_name, ns|
ns.each_with_index do |n, i|
name = ns.size == 1 ? base_name : "#{base_name}_#{i}"
name = '"' + name.gsub(/["\\]/) { |c| "\\" + c } + '"' unless name =~ /^[A-Za-z0-9_]+$/
node_names[n] = name
end
end
node_names
end
end
end
10 changes: 2 additions & 8 deletions app/models/manager_refresh/save_collection/topological_sort.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,8 @@ def save_collections(ems, inventory_collections)

layers = ManagerRefresh::Graph::TopologicalSort.new(graph).topological_sort

sorted_graph_log = "Topological sorting of manager #{ems.name} with ---nodes---:\n#{graph.nodes.join("\n")}\n"
sorted_graph_log += "---edges---:\n#{graph.edges.map { |x| "<#{x.first}, #{x.last}>" }.join("\n")}\n"
sorted_graph_log += "---resulted in these layers processable in parallel:"

layers.each_with_index do |layer, index|
sorted_graph_log += "\n----- Layer #{index} -----: \n#{layer.select { |x| !x.saved? }.join("\n")}"
end

sorted_graph_log = "Topological sorting of manager #{ems.name} resulted in these layers processable in parallel:\n"
sorted_graph_log += graph.to_graphviz(:layers => layers)
_log.info(sorted_graph_log)

layers.each_with_index do |layer, index|
Expand Down
58 changes: 58 additions & 0 deletions spec/models/manager_refresh/graph_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
describe ManagerRefresh::Graph do
let(:node1) { OpenStruct.new(:whatever => 'foo') }
let(:node2) { OpenStruct.new(:name => 'bar', :x => 2) }
let(:node3) { OpenStruct.new(:name => 'bar', :x => 3) }
let(:node4) { OpenStruct.new(:name => 'quux') }
let(:edges) { [[node1, node2], [node1, node3], [node2, node4]] }
let(:fixed_edges) { [] }

class TestGraph < described_class
def initialize(nodes, edges, fixed_edges)
@nodes = nodes
@edges = edges
@fixed_edges = fixed_edges
end
end

let(:graph) { TestGraph.new([node1, node2, node3, node4], edges, fixed_edges) }

describe '#to_graphviz' do
it 'prints the graph' do
# sensitive to node and edge order, but test controls this
expect(graph.to_graphviz).to eq(<<-'DOT'.strip_heredoc)
digraph {
"#<OpenStruct whatever=\"foo\">"; // #<OpenStruct whatever="foo">
bar_0; // #<OpenStruct name="bar", x=2>
bar_1; // #<OpenStruct name="bar", x=3>
quux; // #<OpenStruct name="quux">
// edges:
"#<OpenStruct whatever=\"foo\">" -> bar_0;
"#<OpenStruct whatever=\"foo\">" -> bar_1;
bar_0 -> quux;
}
DOT
end

it 'prints the graph with layers' do
layers = ManagerRefresh::Graph::TopologicalSort.new(graph).topological_sort
expect(graph.to_graphviz(:layers => layers)).to eq(<<-'DOT'.strip_heredoc)
digraph {
subgraph cluster_0 { label = "Layer 0";
"#<OpenStruct whatever=\"foo\">"; // #<OpenStruct whatever="foo">
}
subgraph cluster_1 { label = "Layer 1";
bar_0; // #<OpenStruct name="bar", x=2>
bar_1; // #<OpenStruct name="bar", x=3>
}
subgraph cluster_2 { label = "Layer 2";
quux; // #<OpenStruct name="quux">
}
// edges:
"#<OpenStruct whatever=\"foo\">" -> bar_0;
"#<OpenStruct whatever=\"foo\">" -> bar_1;
bar_0 -> quux;
}
DOT
end
end
end
22 changes: 22 additions & 0 deletions tools/log_processing/graph_refresh_render_digraph.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash

# During graph refresh something like this is printed to log:
# ... Topological sorting of manager ... resulted in these layers ...:
# digraph {
# ...
# }

# This is suggested command to render a pretty graph from it.
# Requires Graphviz installed.

echo 'Paste the lines from `digraph {` to `}` inclusive on stdin.'

set -v

# unflatten's -l2 flag is arbitrary heuristic, might be better without.
unflatten -l2 -f |
dot -Gstyle=dotted -Grankdir=LR -Granksep=1 -Gfontname=sans -Nshape=box -Nstyle=rounded -Ncolor=gray -Nfontname=monospace |
edgepaint |
dot -Tsvg -o refresh-graph.svg

xdg-open refresh-graph.svg

0 comments on commit fb6732f

Please sign in to comment.