Merge branch 'master' of git.curoverse.com:arvados into 3408-production-datamanager
[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           node_value = encode_quotes node
153           gr += "\"#{node_value}\" [label=\"#{node_value}\"];\n"
154           gr += edge(job_uuid(job), node_value, {:label => prefix})
155         end
156       when String
157         return '' if sp.empty?
158         m = GenerateGraph::collection_uuid(sp)
159         if m and (@pdata[m.intern] or (not @opts[:pdata_only]))
160           gr += edge(job_uuid(job), m, {:label => prefix})
161           gr += generate_provenance_edges(m)
162         elsif @opts[:all_script_parameters]
163           sp_value = encode_quotes sp
164           gr += "\"#{sp_value}\" [label=\"#{sp_value}\"];\n"
165           gr += edge(job_uuid(job), sp_value, {:label => prefix})
166         end
167       end
168       gr
169     end
170
171     def generate_provenance_edges(uuid)
172       gr = ""
173       m = GenerateGraph::collection_uuid(uuid)
174       uuid = m if m
175
176       uuid = uuid.intern if uuid
177
178       if (not uuid) or uuid.empty? or @visited[uuid]
179         return ""
180       end
181
182       if not @pdata[uuid] then
183         return describe_node(uuid)
184       else
185         @visited[uuid] = true
186       end
187
188       if m
189         # uuid is a collection
190         if not Collection.is_empty_blob_locator? uuid.to_s
191           @pdata.each do |k, job|
192             if job[:output] == uuid.to_s
193               extra = { label: 'output' }
194               gr += edge(uuid, job_uuid(job), extra)
195               gr += generate_provenance_edges(job[:uuid])
196             end
197             if job[:log] == uuid.to_s
198               gr += edge(uuid, job_uuid(job), {:label => "log"})
199               gr += generate_provenance_edges(job[:uuid])
200             end
201           end
202         end
203         gr += describe_node(uuid)
204       else
205         # uuid is something else
206         rsc = ArvadosBase::resource_class_for_uuid uuid.to_s
207
208         if rsc == Job
209           job = @pdata[uuid]
210           if job
211             gr += script_param_edges(job, "", job[:script_parameters])
212
213             if @opts[:script_version_nodes]
214               gr += describe_node(job[:script_version])
215               gr += edge(job_uuid(job), job[:script_version], {:label => "script_version"})
216             end
217           end
218         elsif rsc == Link
219           # do nothing
220         else
221           gr += describe_node(uuid)
222         end
223       end
224
225       @pdata.each do |k, link|
226         if link[:head_uuid] == uuid.to_s and link[:link_class] == "provenance"
227           href = url_for ({:controller => Link.to_s.tableize,
228                             :action => :show,
229                             :id => link[:uuid] })
230
231           gr += describe_node(link[:tail_uuid])
232           gr += edge(link[:head_uuid], link[:tail_uuid], {:label => link[:name], :href => href})
233           gr += generate_provenance_edges(link[:tail_uuid])
234         end
235       end
236
237       gr
238     end
239
240     def describe_jobs
241       gr = ""
242       @jobs.each do |k, v|
243         href = url_for ({:controller => Job.to_s.tableize,
244                           :action => :index })
245
246         gr += "\"#{k}\" [href=\"#{href}?"
247
248         n = 0
249         v.each do |u|
250           gr += "uuid%5b%5d=#{u[:uuid]}&"
251           n |= @opts[:pips][u[:uuid].intern] if @opts[:pips] and @opts[:pips][u[:uuid].intern]
252         end
253
254         gr += "\",label=\""
255
256         if @opts[:combine_jobs] == :script_only
257           gr += "#{v[0][:script]}"
258         elsif @opts[:combine_jobs] == :script_and_version
259           gr += "#{v[0][:script]}" # Just show the name but the nodes will be distinct
260         else
261           gr += "#{v[0][:script]}\\n#{v[0][:finished_at]}"
262         end
263         gr += "\",#{determine_fillcolor n}];\n"
264       end
265       gr
266     end
267
268     def encode_quotes value
269       value.andand.gsub("\"", "\\\"")
270     end
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