From fb6732fbd6ef3432538aaccd842dd59ec31ea611 Mon Sep 17 00:00:00 2001 From: Beni Cherniavsky-Paskin Date: Tue, 15 Aug 2017 11:31:34 +0300 Subject: [PATCH] Refresh graph logging: use graphviz syntax --- app/models/manager_refresh/graph.rb | 37 ++++++++++++ .../save_collection/topological_sort.rb | 10 +--- spec/models/manager_refresh/graph_spec.rb | 58 +++++++++++++++++++ .../graph_refresh_render_digraph.sh | 22 +++++++ 4 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 spec/models/manager_refresh/graph_spec.rb create mode 100755 tools/log_processing/graph_refresh_render_digraph.sh diff --git a/app/models/manager_refresh/graph.rb b/app/models/manager_refresh/graph.rb index 1c4e534d038..a7f2f7f84ee 100644 --- a/app/models/manager_refresh/graph.rb +++ b/app/models/manager_refresh/graph.rb @@ -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 @@ -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 diff --git a/app/models/manager_refresh/save_collection/topological_sort.rb b/app/models/manager_refresh/save_collection/topological_sort.rb index 06b28ba87cd..8381d665ff3 100644 --- a/app/models/manager_refresh/save_collection/topological_sort.rb +++ b/app/models/manager_refresh/save_collection/topological_sort.rb @@ -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| diff --git a/spec/models/manager_refresh/graph_spec.rb b/spec/models/manager_refresh/graph_spec.rb new file mode 100644 index 00000000000..6dc42ec2b04 --- /dev/null +++ b/spec/models/manager_refresh/graph_spec.rb @@ -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 { + "#"; // # + bar_0; // # + bar_1; // # + quux; // # + // edges: + "#" -> bar_0; + "#" -> 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"; + "#"; // # + } + subgraph cluster_1 { label = "Layer 1"; + bar_0; // # + bar_1; // # + } + subgraph cluster_2 { label = "Layer 2"; + quux; // # + } + // edges: + "#" -> bar_0; + "#" -> bar_1; + bar_0 -> quux; + } + DOT + end + end +end diff --git a/tools/log_processing/graph_refresh_render_digraph.sh b/tools/log_processing/graph_refresh_render_digraph.sh new file mode 100755 index 00000000000..42a4d417fd3 --- /dev/null +++ b/tools/log_processing/graph_refresh_render_digraph.sh @@ -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