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