rubinius-profiler 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,441 @@
1
+ module Rubinius
2
+ module Profiler
3
+
4
+ ##
5
+ # Interface to VM's instrumenting profiler.
6
+
7
+ class Instrumenter
8
+ include Stats::Units
9
+
10
+ attr_reader :info, :options
11
+
12
+ def self.available?
13
+ Rubinius::Tooling.available?
14
+ end
15
+
16
+ def self.active?
17
+ Rubinius::Tooling.active?
18
+ end
19
+
20
+ @loaded = false
21
+ def self.load
22
+ return if @loaded
23
+ Rubinius::Tooling.load File.expand_path("../profiler", __FILE__)
24
+ @loaded = true
25
+ end
26
+
27
+ def initialize(options = {})
28
+ Instrumenter.load
29
+
30
+ @options = { :sort => :percent }
31
+ set_options options
32
+ set_options :full_report => true if Config["profiler.full_report"]
33
+ set_options :graph => true if Config["profiler.graph"]
34
+
35
+ if path = Config["profiler.json"]
36
+ set_options :json => path
37
+ end
38
+
39
+ if name = Config["profiler.output"]
40
+ set_options :output => "#{File.expand_path(name)}-#{$$}"
41
+ end
42
+
43
+ if Config['profiler.cumulative_percentage']
44
+ set_options :cumulative_percentage => true
45
+ end
46
+
47
+ if Config['profiler.classes']
48
+ set_options :classes => true
49
+ end
50
+ end
51
+
52
+ # Set options for profiler output. Presently, the only option
53
+ # is :sort. It takes a single symbol or an array of symbols to
54
+ # specify the column(s) to sort by. The recognized symbols are:
55
+ #
56
+ # Symbol Profiler heading
57
+ # :percent % time
58
+ # :total_seconds cumulative seconds
59
+ # :self_seconds self seconds
60
+ # :calls calls
61
+ # :self_ms self ms/call
62
+ # :total_ms total ms/call
63
+ # :name name
64
+ #
65
+ # @todo Add options for GC allocation counts
66
+ def set_options(options)
67
+ @options.merge!(options)
68
+ end
69
+
70
+ def start
71
+ Rubinius::Tooling.enable
72
+ end
73
+
74
+ def __stop__
75
+ Rubinius::Tooling.disable
76
+ end
77
+
78
+ def stop
79
+ @info = __stop__
80
+ end
81
+
82
+ # Convenience method to profile snippets of code in a larger script or
83
+ # program. Enables the profiler and yields to the given block.
84
+ #
85
+ # pr = Rubinius::Profiler::Instrumenter.new
86
+ # pr.profile { # do some work here }
87
+ def profile(display = true)
88
+ start
89
+ yield if block_given?
90
+ stop
91
+ show if display
92
+ @info
93
+ end
94
+
95
+ SHORT_LINES = 45
96
+
97
+ def show(out=$stdout)
98
+ unless self.class.available? and @info
99
+ out.puts "No profiling data was available"
100
+ return
101
+ end
102
+
103
+ begin
104
+ if name = options[:output]
105
+ out = File.open name, "w"
106
+ end
107
+
108
+ if options[:json]
109
+ json options[:json]
110
+ elsif options[:graph]
111
+ graph out
112
+ else
113
+ flat out
114
+ end
115
+ ensure
116
+ out.close if options[:output]
117
+ end
118
+
119
+ nil
120
+ end
121
+
122
+ def epilogue(out, size, calls)
123
+ unless options[:full_report] or size < SHORT_LINES
124
+ out.puts "\n#{comma(size-SHORT_LINES)} methods omitted"
125
+ end
126
+ out.puts "\n#{comma(size)} methods called a total of #{comma(calls)} times"
127
+ end
128
+
129
+ def flat(out)
130
+ keys = @info.keys.sort
131
+
132
+ keys.each do |t_id|
133
+ thread_flat out, t_id, @info[t_id]
134
+ puts
135
+ end
136
+ end
137
+
138
+ def thread_flat(out, t_id, info)
139
+ total_calls = 0
140
+ total = 0.0
141
+
142
+ all_selves = 0.0
143
+
144
+ # Because the info is actually in a tree form and we to just see it
145
+ # flat for each method, we need to go through and collect all the stats
146
+ # for each unique method.
147
+
148
+ info[:nodes].each do |n_id, data|
149
+ sub = data[4].inject(0) { |a,n| a + info[:nodes][n][1] }
150
+
151
+ meth = info[:methods][data[0]]
152
+ if cur = meth[:edge_total]
153
+ meth[:edge_total] = cur + sub
154
+ else
155
+ meth[:edge_total] = sub
156
+ end
157
+ end
158
+
159
+ data = info[:methods].values.map do |m|
160
+ cumulative = m[:cumulative]
161
+ method_total = m[:total]
162
+ edges_total = m[:edge_total]
163
+ self_total = method_total - edges_total
164
+ called = m[:called]
165
+ total += method_total
166
+ total_calls += called
167
+
168
+ all_selves += self_total
169
+
170
+ name = m[:name]
171
+ name = "#toplevel" if name == "<metaclass>#__script__ {}"
172
+ [ 0,
173
+ sec(cumulative),
174
+ sec(self_total),
175
+ called,
176
+ msec(self_total) / called,
177
+ msec(cumulative) / called,
178
+ name ]
179
+ end
180
+
181
+ all_selves = sec(all_selves)
182
+
183
+ if options[:cumulative_percentage]
184
+ data.each do |d|
185
+ d[0] = (d[1] / sec(info[:runtime])) * 100
186
+ end
187
+ else
188
+ data.each do |d|
189
+ d[0] = (d[2] / all_selves) * 100
190
+ end
191
+ end
192
+
193
+ columns = sort_order
194
+ data = data.sort_by do |row|
195
+ columns.map {|col| row[col] }
196
+ end.reverse
197
+
198
+ out.puts "Thread #{t_id}: total running time: #{sec(info[:runtime])}s"
199
+ out.puts ""
200
+ out.puts " % cumulative self self total"
201
+ out.puts " time seconds seconds calls ms/call ms/call name"
202
+ out.puts "------------------------------------------------------------"
203
+
204
+ report = options[:full_report] ? data : data.first(SHORT_LINES)
205
+ report.each do |d|
206
+ out.printf " %6s", ("%.2f" % d[0])
207
+ out.printf "%8.2f %8.2f %10d %8.2f %8.2f %s\n", *d.last(6)
208
+ end
209
+
210
+ epilogue out, data.size, total_calls
211
+
212
+ if options[:classes]
213
+ classes = Hash.new { |h,k| h[k] = 0.0 }
214
+
215
+ data.each do |row|
216
+ if m = /(.*)[#\.]/.match(row.last)
217
+ classes[m[1]] += ((row[2] / all_selves) * 100)
218
+ end
219
+ end
220
+
221
+ out.puts "\nUsage percentage by class:"
222
+ sorted = classes.to_a.sort_by { |row| row[1] }.reverse
223
+ sorted.each do |row|
224
+ out.printf "%6s: %s\n", ("%.2f" % row[1]), row[0]
225
+ end
226
+ end
227
+ end
228
+
229
+ def json(path)
230
+ File.open path, "w" do |f|
231
+ t_final = @info.size - 1
232
+ t_idx = 0
233
+
234
+ f.puts "["
235
+
236
+ @info.each do |t_id, info|
237
+ f.puts "{"
238
+ f.puts " \"thread_id\": #{t_id},"
239
+ f.puts " \"runtime\": #{info[:runtime]},"
240
+ f.puts " \"total_nodes\": #{info[:total_nodes]},"
241
+ roots = info[:roots].map { |x| x.to_s.dump }.join(',')
242
+ f.puts " \"roots\": [ #{roots} ],"
243
+ f.puts " \"nodes\": {"
244
+ idx = 0
245
+ final = info[:nodes].size - 1
246
+
247
+ info[:nodes].each do |n_id, data|
248
+ f.puts " \"#{n_id}\": {"
249
+ f.puts " \"method\": #{data[0]}, \"total\": #{data[1]}, \"called\": #{data[2]},"
250
+ f.puts " \"total_nodes\": #{data[3]}, \"sub_nodes\": [ #{data[4].join(', ')} ]"
251
+ if idx == final
252
+ f.puts " }"
253
+ else
254
+ f.puts " },"
255
+ end
256
+ idx += 1
257
+ end
258
+
259
+ f.puts " },"
260
+ f.puts " \"methods\": {"
261
+
262
+ idx = 0
263
+ final = info[:methods].size - 1
264
+ info[:methods].each do |m_id, m|
265
+ f.puts " \"#{m_id}\": {"
266
+ f.puts " \"name\": \"#{m[:name]}\", \"file\": \"#{m[:file]}\", \"line\": #{m[:line] || 0},"
267
+ f.puts " \"cumulative\": #{m[:cumulative]}, \"total\": #{m[:total]}, \"called\": #{m[:called]}"
268
+ if idx == final
269
+ f.puts " }"
270
+ else
271
+ f.puts " },"
272
+ end
273
+ idx += 1
274
+ end
275
+
276
+ f.puts " }"
277
+
278
+ if t_idx == t_final
279
+ f.puts "}"
280
+ else
281
+ f.puts "},"
282
+ end
283
+
284
+ t_idx += 1
285
+ end
286
+ f.puts "]"
287
+ end
288
+
289
+ puts "Wrote JSON to: #{path}"
290
+ end
291
+
292
+ def graph(out)
293
+ keys = @info.keys.sort
294
+
295
+ keys.each do |t_id|
296
+ thread_graph out, t_id, @info[t_id]
297
+ out.puts
298
+ end
299
+ end
300
+
301
+ # Prints an entry for each method, along with the method's callers and
302
+ # the methods called. The entry is delimited by the dashed lines. The
303
+ # line for the method itself is called the "primary" line. The callers
304
+ # are printed above the primary line and the methods called are printed
305
+ # below.
306
+ def thread_graph(out, t_id, info)
307
+ total_calls = 0
308
+ run_total = 0.0
309
+
310
+ data = info[:nodes]
311
+
312
+ methods = info[:methods]
313
+
314
+ run_total = info[:runtime].to_f
315
+
316
+ all_callers = Hash.new { |h,k| h[k] = [] }
317
+
318
+ data.each do |n_id, n_data|
319
+ n_data[4].each do |sub|
320
+ all_callers[sub] << n_id
321
+ end
322
+ end
323
+
324
+ indexes = data.keys.sort do |a, b|
325
+ data[b][1] <=> data[a][1]
326
+ end
327
+
328
+ indexes = indexes.first(SHORT_LINES) unless options[:full_report]
329
+
330
+ shown_indexes = {}
331
+
332
+ indexes.each_with_index do |id, index|
333
+ shown_indexes[id] = index + 1
334
+ end
335
+
336
+ out.puts "===== Thread #{t_id} ====="
337
+ out.puts "Total running time: #{sec(info[:runtime])}s"
338
+ out.puts "index % time self children called name"
339
+ out.puts "----------------------------------------------------------"
340
+
341
+ primary = "%-7s%6s %8.2f %9.2f %8d %s [%d]\n"
342
+ secondary = " %8.2f %9.2f %8d %s%s\n"
343
+
344
+ indexes.each do |id|
345
+ m_id, total, called, tn, sub_nodes = data[id]
346
+
347
+ # The idea is to report information about caller as a ratio of the
348
+ # time it called method.
349
+ #
350
+
351
+ callers = all_callers[id].sort_by do |c_id|
352
+ clr = data[c_id]
353
+
354
+ clr[total]
355
+ end
356
+
357
+ callers = callers.first(10) unless options[:full_report]
358
+
359
+ callers.each do |c_id|
360
+ clr_m_id, clr_total, clr_called, clr_tn, clr_sub = data[c_id]
361
+
362
+ sub_total = clr_sub.inject(0) { |a,s| a + data[s][1] }
363
+
364
+ self_total = clr_total - sub_total
365
+ out.printf(secondary, sec(self_total),
366
+ sec(sub_total),
367
+ clr_called,
368
+ methods[clr_m_id][:name],
369
+ graph_method_index(shown_indexes[c_id]))
370
+ end
371
+
372
+ # Now the primary line.
373
+
374
+ children = sub_nodes.inject(0) { |a,s| a + data[s][1] }
375
+ # children = method[:cumulative] * (method[:edges_total].to_f / method[:total])
376
+
377
+ self_total = total - children
378
+ out.printf primary, ("[%d]" % shown_indexes[id]),
379
+ percentage(total, run_total, 1, nil),
380
+ sec(self_total),
381
+ sec(children),
382
+ called,
383
+ methods[m_id][:name],
384
+ shown_indexes[id]
385
+
386
+ # Same as caller, the idea is to report information about callee methods
387
+ # as a ratio of the time it was called from method.
388
+ #
389
+
390
+ edges = sub_nodes.sort_by do |e_id|
391
+ if edge = data[e_id]
392
+ edge[1]
393
+ else
394
+ 0.0
395
+ end
396
+ end
397
+
398
+ edges = edges.last(10) unless options[:full_report]
399
+ # method[:edges] = method[:edges].first(10) unless options[:full_report]
400
+
401
+ edges.reverse_each do |e_id|
402
+ c_m_id, c_total, c_called, c_tn, c_sub_nodes = data[e_id]
403
+
404
+ grandchildren = c_sub_nodes.inject(0) { |a,s| a + data[s][1] }
405
+ grandchildren = 0 if grandchildren < 0
406
+
407
+ self_total = c_total - grandchildren
408
+ out.printf secondary, sec(self_total),
409
+ sec(grandchildren),
410
+ c_called,
411
+ methods[c_m_id][:name],
412
+ graph_method_index(shown_indexes[e_id])
413
+ end
414
+
415
+ out.puts "-------------------------------------------------------"
416
+ end
417
+
418
+ epilogue out, data.size, total_calls
419
+ end
420
+
421
+ def graph_method_index(index)
422
+ index ? " [#{index}]" : ""
423
+ end
424
+
425
+ HEADER_INDEX = {
426
+ :percent => 0,
427
+ :total_seconds => 1,
428
+ :self_seconds => 2,
429
+ :calls => 3,
430
+ :self_ms => 4,
431
+ :total_ms => 5,
432
+ :name => 6
433
+ }
434
+
435
+ def sort_order
436
+ # call to_i so if unrecognized symbol is passed, column will be percent
437
+ Array(@options[:sort]).map { |header| HEADER_INDEX[header].to_i }
438
+ end
439
+ end
440
+ end
441
+ end
@@ -0,0 +1,5 @@
1
+ module Rubinius
2
+ module Profiler
3
+ VERSION = "2.0.0"
4
+ end
5
+ end
@@ -0,0 +1,2 @@
1
+ require "rubinius/profiler/profiler"
2
+ require "rubinius/profiler/version"
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ require './lib/rubinius/profiler/version'
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "rubinius-profiler"
6
+ spec.version = Rubinius::Profiler::VERSION
7
+ spec.authors = ["Brian Shirai"]
8
+ spec.email = ["brixen@gmail.com"]
9
+ spec.description = %q{Rubinius profiler.}
10
+ spec.summary = %q{Rubinius profiler.}
11
+ spec.homepage = "https://github.com/rubinius/rubinius-profiler"
12
+ spec.license = "BSD"
13
+
14
+ spec.files = `git ls-files`.split($/)
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.extensions = ["ext/rubinius/profiler/extconf.rb"]
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.3"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubinius-profiler
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Shirai
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-09-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: Rubinius profiler.
42
+ email:
43
+ - brixen@gmail.com
44
+ executables: []
45
+ extensions:
46
+ - ext/rubinius/profiler/extconf.rb
47
+ extra_rdoc_files: []
48
+ files:
49
+ - .gitignore
50
+ - Gemfile
51
+ - LICENSE
52
+ - README.md
53
+ - Rakefile
54
+ - ext/rubinius/profiler/extconf.rb
55
+ - ext/rubinius/profiler/profiler.cpp
56
+ - lib/rubinius/profiler.rb
57
+ - lib/rubinius/profiler/profiler.rb
58
+ - lib/rubinius/profiler/version.rb
59
+ - rubinius-profiler.gemspec
60
+ homepage: https://github.com/rubinius/rubinius-profiler
61
+ licenses:
62
+ - BSD
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.0.7
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Rubinius profiler.
84
+ test_files: []
85
+ has_rdoc: