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