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