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