rubinius-profiler 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: