Merge branch 'master' into 3898-job-state-attr
[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               gr += edge(uuid, job_uuid(job), extra)
193               gr += generate_provenance_edges(job[:uuid])
194             end
195             if job[:log] == uuid.to_s
196               gr += edge(uuid, job_uuid(job), {:label => "log"})
197               gr += generate_provenance_edges(job[:uuid])
198             end
199           end
200         end
201         gr += describe_node(uuid)
202       else
203         # uuid is something else
204         rsc = ArvadosBase::resource_class_for_uuid uuid.to_s
205
206         if rsc == Job
207           job = @pdata[uuid]
208           if job
209             gr += script_param_edges(job, "", job[:script_parameters])
210
211             if @opts[:script_version_nodes]
212               gr += describe_node(job[:script_version])
213               gr += edge(job_uuid(job), job[:script_version], {:label => "script_version"})
214             end
215           end
216         elsif rsc == Link
217           # do nothing
218         else
219           gr += describe_node(uuid)
220         end
221       end
222
223       @pdata.each do |k, link|
224         if link[:head_uuid] == uuid.to_s and link[:link_class] == "provenance"
225           href = url_for ({:controller => Link.to_s.tableize,
226                             :action => :show,
227                             :id => link[:uuid] })
228
229           gr += describe_node(link[:tail_uuid])
230           gr += edge(link[:head_uuid], link[:tail_uuid], {:label => link[:name], :href => href})
231           gr += generate_provenance_edges(link[:tail_uuid])
232         end
233       end
234
235       gr
236     end
237
238     def describe_jobs
239       gr = ""
240       @jobs.each do |k, v|
241         href = url_for ({:controller => Job.to_s.tableize,
242                           :action => :index })
243
244         gr += "\"#{k}\" [href=\"#{href}?"
245
246         n = 0
247         v.each do |u|
248           gr += "uuid%5b%5d=#{u[:uuid]}&"
249           n |= @opts[:pips][u[:uuid].intern] if @opts[:pips] and @opts[:pips][u[:uuid].intern]
250         end
251
252         gr += "\",label=\""
253
254         if @opts[:combine_jobs] == :script_only
255           gr += "#{v[0][:script]}"
256         elsif @opts[:combine_jobs] == :script_and_version
257           gr += "#{v[0][:script]}" # Just show the name but the nodes will be distinct
258         else
259           gr += "#{v[0][:script]}\\n#{v[0][:finished_at]}"
260         end
261         gr += "\",#{determine_fillcolor n}];\n"
262       end
263       gr
264     end
265
266   end
267
268   def self.create_provenance_graph(pdata, svgId, opts={})
269     if pdata.is_a? Array or pdata.is_a? ArvadosResourceList
270       p2 = {}
271       pdata.each do |k|
272         p2[k[:uuid].intern] = k if k[:uuid]
273       end
274       pdata = p2
275     end
276
277     unless pdata.is_a? Hash
278       raise "create_provenance_graph accepts Array or Hash for pdata only, pdata is #{pdata.class}"
279     end
280
281     gr = """strict digraph {
282 node [fontsize=10,shape=box];
283 edge [fontsize=10];
284 """
285
286     if opts[:direction] == :bottom_up
287       gr += "edge [dir=back];"
288     end
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     require 'open3'
302
303     Open3.popen2("dot", "-Tsvg") do |stdin, stdout, wait_thr|
304       stdin.print(gr)
305       stdin.close
306       svg = stdout.read()
307       wait_thr.value
308       stdout.close()
309     end
310
311     svg = svg.sub(/<\?xml.*?\?>/m, "")
312     svg = svg.sub(/<!DOCTYPE.*?>/m, "")
313     svg = svg.sub(/<svg /, "<svg id=\"#{svgId}\" ")
314   end
315
316   def self.find_collections(sp)
317     c = []
318     case sp
319     when Hash
320       sp.each do |k, v|
321         c.concat(find_collections(v))
322       end
323     when Array
324       sp.each do |v|
325         c.concat(find_collections(v))
326       end
327     when String
328       if !sp.empty?
329         m = GenerateGraph::collection_uuid(sp)
330         if m
331           c << m
332         end
333       end
334     end
335     c
336   end
337 end