Fix up legend colors and labels on pipeline_instances > compare > graph.
[arvados.git] / apps / workbench / app / helpers / provenance_helper.rb
1 module ProvenanceHelper
2
3   class GenerateGraph
4     def initialize(pdata, opts)
5       @pdata = pdata
6       @opts = opts
7       @visited = {}
8       @jobs = {}
9     end
10
11     def self.collection_uuid(uuid)
12       m = /^([a-f0-9]{32}(\+[0-9]+)?)(\+.*)?$/.match(uuid.to_s)
13       if m
14         #if m[2]
15         return m[1]
16         #else
17         #  Collection.where(uuid: ['contains', m[1]]).each do |u|
18         #    puts "fixup #{uuid} to #{u.uuid}"
19         #    return u.uuid
20         #  end
21         #end
22       else
23         nil
24       end
25     end
26
27     def determine_fillcolor(n)
28       fillcolor = %w(aaaaaa aaffaa aaaaff aaaaaa ffaaaa)[n || 0] || 'aaaaaa'
29       "style=filled,fillcolor=\"##{fillcolor}\""
30     end
31
32     def describe_node(uuid)
33       bgcolor = determine_fillcolor @opts[:pips][uuid] if @opts[:pips]
34
35       rsc = ArvadosBase::resource_class_for_uuid uuid.to_s
36       if rsc
37         href = "/#{rsc.to_s.underscore.pluralize rsc}/#{uuid}"
38       
39         #"\"#{uuid}\" [label=\"#{rsc}\\n#{uuid}\",href=\"#{href}\"];\n"
40         if rsc == Collection
41           #puts uuid
42           if uuid == :"d41d8cd98f00b204e9800998ecf8427e+0"
43             # special case
44             #puts "empty!"
45             return "\"#{uuid}\" [label=\"(empty collection)\"];\n"
46           end
47           if @pdata[uuid] 
48             #puts @pdata[uuid]
49             if @pdata[uuid][:name]
50               return "\"#{uuid}\" [label=\"#{@pdata[uuid][:name]}\",href=\"#{href}\",shape=oval,#{bgcolor}];\n"
51             else
52               files = nil
53               if @pdata[uuid].respond_to? :files
54                 files = @pdata[uuid].files
55               elsif @pdata[uuid][:files]
56                 files = @pdata[uuid][:files]
57               end
58               
59               if files
60                 i = 0
61                 label = ""
62                 while i < 3 and i < files.length
63                   label += "\\n" unless label == ""
64                   label += files[i][1]
65                   i += 1
66                 end
67                 if i < files.length
68                   label += "\\n&vellip;"
69                 end
70                 return "\"#{uuid}\" [label=\"#{label}\",href=\"#{href}\",shape=oval,#{bgcolor}];\n"
71               end
72             end  
73           end
74           return "\"#{uuid}\" [label=\"#{rsc}\",href=\"#{href}\",#{bgcolor}];\n"
75         end
76       end
77       "\"#{uuid}\" [#{bgcolor}];\n"
78     end
79
80     def job_uuid(job)
81       if @opts[:combine_jobs] == :script_only
82         uuid = "#{job[:script]}"
83       elsif @opts[:combine_jobs] == :script_and_version
84         uuid = "#{job[:script]}_#{job[:script_version]}"
85       else
86         uuid = "#{job[:uuid]}"
87       end
88
89       @jobs[uuid] = [] unless @jobs[uuid]
90       @jobs[uuid] << job unless @jobs[uuid].include? job
91
92       uuid
93     end
94
95     def edge(tail, head, extra)
96       if @opts[:direction] == :bottom_up
97         gr = "\"#{tail}\" -> \"#{head}\""
98       else
99         gr = "\"#{head}\" -> \"#{tail}\""
100       end
101       if extra.length > 0
102         gr += "["
103         extra.each do |k, v|
104           gr += "#{k}=\"#{v}\","
105         end
106         gr += "]"
107       end
108       gr += ";\n"
109       gr
110     end
111
112     def script_param_edges(job, prefix, sp)
113       gr = ""
114       if sp and not sp.empty?
115         case sp
116         when Hash
117           sp.each do |k, v|
118             if prefix.size > 0
119               k = prefix + "::" + k.to_s
120             end
121             gr += script_param_edges(job, k.to_s, v)
122           end
123         when Array
124           i = 0
125           node = ""
126           sp.each do |v|
127             if GenerateGraph::collection_uuid(v)
128               gr += script_param_edges(job, "#{prefix}[#{i}]", v)
129             elsif @opts[:all_script_parameters]
130               node += "', '" unless node == ""
131               node = "['" if node == ""
132               node += "#{v}"
133             end
134             i += 1
135           end
136           unless node == ""
137             node += "']"
138             #puts node
139             #id = "#{job[:uuid]}_#{prefix}"
140             gr += "\"#{node}\" [label=\"#{node}\"];\n"
141             gr += edge(job_uuid(job), node, {:label => prefix})        
142           end
143         else
144           m = GenerateGraph::collection_uuid(sp)
145           #puts "#{m} pdata is #{@pdata[m.intern]}"
146           if m and (@pdata[m.intern] or (not @opts[:pdata_only]))
147             gr += edge(job_uuid(job), m, {:label => prefix})
148             gr += generate_provenance_edges(m)
149           elsif @opts[:all_script_parameters]
150             #id = "#{job[:uuid]}_#{prefix}"
151             gr += "\"#{sp}\" [label=\"#{sp}\"];\n"
152             gr += edge(job_uuid(job), sp, {:label => prefix})
153           end
154         end
155       end
156       gr
157     end
158
159     def generate_provenance_edges(uuid)
160       gr = ""
161       m = GenerateGraph::collection_uuid(uuid)
162       uuid = m if m
163
164       uuid = uuid.intern if uuid
165
166       if (not uuid) or uuid.empty? or @visited[uuid]
167
168         #puts "already @visited #{uuid}"
169         return ""
170       end
171
172       if not @pdata[uuid] then 
173         return describe_node(uuid)
174       else
175         @visited[uuid] = true
176       end
177
178       #puts "visiting #{uuid}"
179
180       if m  
181         # uuid is a collection
182         gr += describe_node(uuid)
183
184         if m == :"d41d8cd98f00b204e9800998ecf8427e+0"
185           # empty collection, don't follow any further
186           return gr
187         end
188
189         @pdata.each do |k, job|
190           if job[:output] == uuid.to_s
191             gr += edge(uuid, job_uuid(job), {:label => "output"})
192             gr += generate_provenance_edges(job[:uuid])
193           end
194           if job[:log] == uuid.to_s
195             gr += edge(uuid, job_uuid(job), {:label => "log"})
196             gr += generate_provenance_edges(job[:uuid])
197           end
198         end
199       else
200         # uuid is something else
201         rsc = ArvadosBase::resource_class_for_uuid uuid.to_s
202
203         if rsc == Job
204           job = @pdata[uuid]
205           if job
206             gr += script_param_edges(job, "", job[:script_parameters])
207
208             if @opts[:script_version_nodes]
209               gr += describe_node(job[:script_version])
210               gr += edge(job_uuid(job), job[:script_version], {:label => "script_version"})
211             end
212           end
213         else
214           gr += describe_node(uuid)
215         end
216       end
217
218       @pdata.each do |k, link|
219         if link[:head_uuid] == uuid.to_s and link[:link_class] == "provenance"
220           gr += describe_node(link[:tail_uuid])
221           gr += edge(link[:head_uuid], link[:tail_uuid], {:label => link[:name], :href => "/links/#{link[:uuid]}"}) 
222           gr += generate_provenance_edges(link[:tail_uuid])
223         end
224       end
225
226       #puts "finished #{uuid}"
227
228       gr
229     end
230
231     def describe_jobs
232       gr = ""
233       @jobs.each do |k, v|
234         gr += "\"#{k}\" [href=\"/jobs?"
235         
236         n = 0
237         v.each do |u|
238           gr += "uuid%5b%5d=#{u[:uuid]}&"
239           n |= @opts[:pips][u[:uuid].intern] if @opts[:pips] and @opts[:pips][u[:uuid].intern]
240         end
241
242         gr += "\",label=\""
243         
244         if @opts[:combine_jobs] == :script_only
245           gr += uuid = "#{v[0][:script]}"
246         elsif @opts[:combine_jobs] == :script_and_version
247           gr += uuid = "#{v[0][:script]}"
248         else
249           gr += uuid = "#{v[0][:script]}\\n#{v[0][:finished_at]}"
250         end
251         gr += "\",#{determine_fillcolor n}];\n"
252       end
253       gr
254     end
255
256   end
257
258   def self.create_provenance_graph(pdata, svgId, opts={})
259     if pdata.is_a? Array or pdata.is_a? ArvadosResourceList
260       p2 = {}
261       pdata.each do |k|
262         p2[k[:uuid].intern] = k if k[:uuid]
263       end
264       pdata = p2
265     end
266
267     unless pdata.is_a? Hash
268       raise "create_provenance_graph accepts Array or Hash for pdata only, pdata is #{pdata.class}"
269     end
270     
271     gr = """strict digraph {
272 node [fontsize=10,shape=box];
273 edge [fontsize=10];
274 """
275
276     if opts[:direction] == :bottom_up
277       gr += "edge [dir=back];"
278     end
279
280     #puts "@pdata is #{pdata}"
281
282     g = GenerateGraph.new(pdata, opts)
283
284     pdata.each do |k, v|
285       gr += g.generate_provenance_edges(k)
286     end
287
288     gr += g.describe_jobs
289
290     gr += "}"
291     svg = ""
292
293     #puts gr
294
295     require 'open3'
296
297     Open3.popen2("dot", "-Tsvg") do |stdin, stdout, wait_thr|
298       stdin.print(gr)
299       stdin.close
300       svg = stdout.read()
301       wait_thr.value
302       stdout.close()
303     end
304
305     svg = svg.sub(/<\?xml.*?\?>/m, "")
306     svg = svg.sub(/<!DOCTYPE.*?>/m, "")
307     svg = svg.sub(/<svg /, "<svg id=\"#{svgId}\" ")
308   end
309
310   def self.find_collections(sp)
311     c = []
312     if sp and not sp.empty?
313       case sp
314       when Hash
315         sp.each do |k, v|
316           c.concat(find_collections(v))
317         end
318       when Array
319         sp.each do |v|
320           c.concat(find_collections(v))
321         end
322       else
323         m = GenerateGraph::collection_uuid(sp)
324         if m
325           c << m
326         end
327       end
328     end
329     c
330   end
331 end