Merge remote-tracking branch 'origin/master' into 2049-run-job-from-workbench
[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 = CollectionsHelper.match(uuid)
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       case sp
115       when Hash
116         sp.each do |k, v|
117           if prefix.size > 0
118             k = prefix + "::" + k.to_s
119           end
120           gr += script_param_edges(job, k.to_s, v)
121         end
122       when Array
123         i = 0
124         node = ""
125         sp.each do |v|
126           if GenerateGraph::collection_uuid(v)
127             gr += script_param_edges(job, "#{prefix}[#{i}]", v)
128           elsif @opts[:all_script_parameters]
129             node += "', '" unless node == ""
130             node = "['" if node == ""
131             node += "#{v}"
132           end
133           i += 1
134         end
135         unless node == ""
136           node += "']"
137           #puts node
138           #id = "#{job[:uuid]}_#{prefix}"
139           gr += "\"#{node}\" [label=\"#{node}\"];\n"
140           gr += edge(job_uuid(job), node, {:label => prefix})        
141         end
142       when String
143         return '' if sp.empty?
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       gr
156     end
157
158     def generate_provenance_edges(uuid)
159       gr = ""
160       m = GenerateGraph::collection_uuid(uuid)
161       uuid = m if m
162
163       uuid = uuid.intern if uuid
164
165       if (not uuid) or uuid.empty? or @visited[uuid]
166
167         #puts "already @visited #{uuid}"
168         return ""
169       end
170
171       if not @pdata[uuid] then 
172         return describe_node(uuid)
173       else
174         @visited[uuid] = true
175       end
176
177       #puts "visiting #{uuid}"
178
179       if m  
180         # uuid is a collection
181         gr += describe_node(uuid)
182
183         if m == :"d41d8cd98f00b204e9800998ecf8427e+0"
184           # empty collection, don't follow any further
185           return gr
186         end
187
188         @pdata.each do |k, job|
189           if job[:output] == uuid.to_s
190             gr += edge(uuid, job_uuid(job), {:label => "output"})
191             gr += generate_provenance_edges(job[:uuid])
192           end
193           if job[:log] == uuid.to_s
194             gr += edge(uuid, job_uuid(job), {:label => "log"})
195             gr += generate_provenance_edges(job[:uuid])
196           end
197         end
198       else
199         # uuid is something else
200         rsc = ArvadosBase::resource_class_for_uuid uuid.to_s
201
202         if rsc == Job
203           job = @pdata[uuid]
204           if job
205             gr += script_param_edges(job, "", job[:script_parameters])
206
207             if @opts[:script_version_nodes]
208               gr += describe_node(job[:script_version])
209               gr += edge(job_uuid(job), job[:script_version], {:label => "script_version"})
210             end
211           end
212         else
213           gr += describe_node(uuid)
214         end
215       end
216
217       @pdata.each do |k, link|
218         if link[:head_uuid] == uuid.to_s and link[:link_class] == "provenance"
219           gr += describe_node(link[:tail_uuid])
220           gr += edge(link[:head_uuid], link[:tail_uuid], {:label => link[:name], :href => "/links/#{link[:uuid]}"}) 
221           gr += generate_provenance_edges(link[:tail_uuid])
222         end
223       end
224
225       #puts "finished #{uuid}"
226
227       gr
228     end
229
230     def describe_jobs
231       gr = ""
232       @jobs.each do |k, v|
233         gr += "\"#{k}\" [href=\"/jobs?"
234         
235         n = 0
236         v.each do |u|
237           gr += "uuid%5b%5d=#{u[:uuid]}&"
238           n |= @opts[:pips][u[:uuid].intern] if @opts[:pips] and @opts[:pips][u[:uuid].intern]
239         end
240
241         gr += "\",label=\""
242         
243         if @opts[:combine_jobs] == :script_only
244           gr += uuid = "#{v[0][:script]}"
245         elsif @opts[:combine_jobs] == :script_and_version
246           gr += uuid = "#{v[0][:script]}"
247         else
248           gr += uuid = "#{v[0][:script]}\\n#{v[0][:finished_at]}"
249         end
250         gr += "\",#{determine_fillcolor n}];\n"
251       end
252       gr
253     end
254
255   end
256
257   def self.create_provenance_graph(pdata, svgId, opts={})
258     if pdata.is_a? Array or pdata.is_a? ArvadosResourceList
259       p2 = {}
260       pdata.each do |k|
261         p2[k[:uuid].intern] = k if k[:uuid]
262       end
263       pdata = p2
264     end
265
266     unless pdata.is_a? Hash
267       raise "create_provenance_graph accepts Array or Hash for pdata only, pdata is #{pdata.class}"
268     end
269     
270     gr = """strict digraph {
271 node [fontsize=10,shape=box];
272 edge [fontsize=10];
273 """
274
275     if opts[:direction] == :bottom_up
276       gr += "edge [dir=back];"
277     end
278
279     #puts "@pdata is #{pdata}"
280
281     g = GenerateGraph.new(pdata, opts)
282
283     pdata.each do |k, v|
284       gr += g.generate_provenance_edges(k)
285     end
286
287     gr += g.describe_jobs
288
289     gr += "}"
290     svg = ""
291
292     #puts gr
293
294     require 'open3'
295
296     Open3.popen2("dot", "-Tsvg") do |stdin, stdout, wait_thr|
297       stdin.print(gr)
298       stdin.close
299       svg = stdout.read()
300       wait_thr.value
301       stdout.close()
302     end
303
304     svg = svg.sub(/<\?xml.*?\?>/m, "")
305     svg = svg.sub(/<!DOCTYPE.*?>/m, "")
306     svg = svg.sub(/<svg /, "<svg id=\"#{svgId}\" ")
307   end
308
309   def self.find_collections(sp)
310     c = []
311     case sp
312     when Hash
313       sp.each do |k, v|
314         c.concat(find_collections(v))
315       end
316     when Array
317       sp.each do |v|
318         c.concat(find_collections(v))
319       end
320     when String
321       if !sp.empty?
322         m = GenerateGraph::collection_uuid(sp)
323         if m
324           c << m
325         end
326       end
327     end
328     c
329   end
330 end