silhouette 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ bin/silhouette
2
+ extconf.rb
3
+ lib/silhouette/processor.rb
4
+ lib/silhouette.rb
5
+ Manifest.txt
6
+ Rakefile
7
+ README
8
+ silhouette_ext.c
9
+ test/silhouette.out
10
+ test/test.rb
data/README ADDED
@@ -0,0 +1,57 @@
1
+
2
+ Here is an example of a generated profile report:
3
+
4
+ $ silhouette silhouette.out
5
+ Number of threads: 1
6
+ Profiling based on method call.
7
+ Cost of profiler: 0.45 seconds.
8
+
9
+ Flat profile (0.76 total seconds):
10
+ % total self self total
11
+ time seconds seconds calls ms/call ms/call name
12
+ 19.74 0.15 0.15 1228 0.12 0.28 Silhouette::DefaultProfiler#process_return
13
+ 10.53 0.23 0.08 1 80.00 640.00 Silhouette::BinaryEmitter#parse
14
+ 6.58 0.28 0.05 6151 0.01 0.01 Array#[]
15
+ 5.26 0.32 0.04 1229 0.03 0.07 Silhouette::DefaultProfiler#process_call
16
+ 5.26 0.36 0.04 4966 0.01 0.01 IO#read
17
+ 5.26 0.40 0.04 3842 0.01 0.01 Object#===
18
+ 3.95 0.43 0.03 4474 0.01 0.01 Hash#[]
19
+ ...
20
+
21
+ Call Tree Profile:
22
+ index calls ms/ self total
23
+ call sec sec
24
+ 1228/1 - - - Silhouette::BinaryEmitter#parse [243]
25
+ [256] 1228 0.12 0.15 0.34 Silhouette::DefaultProfiler#process_return
26
+ 2 0.01 0.00 0.02 Array#[] [4]
27
+ 2 0.01 0.00 0.02 Hash#[] [162]
28
+ 1 0.05 0.00 0.06 Silhouette::ProfileNode#add_cost [265]
29
+ 1 0.02 0.00 0.02 Silhouette::ProfileNode#inc_call! [263]
30
+ 2 0.01 0.00 0.02 Array#last [260]
31
+ 1 0.01 0.00 0.02 Hash#[]= [37]
32
+ ----------------------------------------------------------------------
33
+ ...
34
+
35
+ parent called/called child - - - Parent method name [parent_id]
36
+ ...
37
+ [method_id] calls 0.03 0.04 0.08 Method name
38
+ called child 0.02 0.00 0.02 Child method name [child_id]
39
+ ...
40
+
41
+
42
+ Explanation of Call Tree Profile:
43
+
44
+ For each method profiled, there is an entry that describes the method, it's parents
45
+ and it's direct children. Above we've shown a real world output from the profiler and
46
+ and explanation of each field.
47
+
48
+ First are listed all the parent methods that called the current method. The only stat
49
+ available for the parent is the number of times the parent was called and the number
50
+ of times the parent called the current method.
51
+
52
+ Next listed is the current method.
53
+
54
+ Finally, all the methods directly called by the current method are listed. The calls
55
+ column is only the number of times the current method called the child, not the
56
+ total number of calls the child received.
57
+
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/gempackagetask'
4
+
5
+ $VERBOSE = nil
6
+
7
+ spec = Gem::Specification.new do |s|
8
+ s.name = 'silhouette'
9
+ s.version = '1.0.0'
10
+ s.summary = 'A 2 stage profiler'
11
+ s.author = 'Evan Webb'
12
+ s.email = 'evan@fallingsnow.net'
13
+
14
+ s.has_rdoc = true
15
+ s.files = File.read('Manifest.txt').split($/)
16
+ s.require_path = 'lib'
17
+ s.executables = ['silhouette']
18
+ s.default_executable = 'silhouette'
19
+ s.extensions = ['extconf.rb']
20
+ end
21
+
22
+ desc 'Build Gem'
23
+ Rake::GemPackageTask.new spec do |pkg|
24
+ pkg.need_tar = true
25
+ end
26
+
27
+ desc 'Clean up'
28
+ task :clean => [ :clobber_package ]
29
+
30
+ desc 'Clean up'
31
+ task :clobber => [ :clean ]
32
+
33
+ # vim: syntax=Ruby
34
+
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'silhouette/processor'
4
+
5
+ output = nil
6
+ max = nil
7
+ entry = nil
8
+ depth = nil
9
+ ascii = nil
10
+ long = false
11
+ callsite = false
12
+ gzip = false
13
+ processed = nil
14
+ load = false
15
+
16
+ STDOUT.sync = true
17
+
18
+ opt = OptionParser.new do |opt|
19
+ =begin
20
+ opt.on("-o FILE", "Where to output data") { |output| }
21
+ opt.on("-c", "--combine", "Combine multiple data files.") do
22
+ data = Hash.new
23
+ ARGV.each do |file|
24
+ print "#{file}: "
25
+ rp = Silhouette.new(file)
26
+ rp.data = data
27
+ rp.parse
28
+ puts "done."
29
+ end
30
+
31
+ File.open(output, "w") do |f|
32
+ f << Marshal.dump(data)
33
+ end
34
+ puts "Data saved to #{output}. #{data.keys.size} data points."
35
+ exit
36
+ end
37
+ =end
38
+ opt.on("-m", "--max MAX", "Only show the top N call sites") do |m|
39
+ max = m.to_i
40
+ end
41
+
42
+ opt.on("-e SIG","Only profile calls made from +SIG+") do |e|
43
+ entry = e
44
+ end
45
+
46
+ opt.on("-d DEPTH", "Only process calls +DEPTH+ levels down") do |o|
47
+ depth = o.to_i
48
+ end
49
+
50
+ opt.on("-s", "--site", "Profile based on method call and call site") do |o|
51
+ callsite = true
52
+ end
53
+
54
+ opt.on("-a OUT", "Convert a binary profile file to an ASCII one") do |a|
55
+ ascii = a
56
+ end
57
+
58
+ opt.on("-l", "Use the long format for the ASCII profile") do |l|
59
+ long = true
60
+ end
61
+
62
+ opt.on("-z", "Use GZIP when reading data") do |z|
63
+ gzip = z
64
+ end
65
+
66
+ opt.on("-p FILE", "Process the profile data and save the processed data") do |o|
67
+ processed = o
68
+ STDERR.puts "Saving processed data to #{processed}"
69
+ end
70
+
71
+ opt.on("-r FILE", "Load data and process directly") do |o|
72
+ load = o
73
+ end
74
+
75
+ opt.on("-h", "--help") do
76
+ puts opt
77
+ exit 1
78
+ end
79
+ end
80
+
81
+ opt.parse!
82
+
83
+ if load
84
+ rp = Marshal.load(File.open(load))
85
+ rp.print(STDOUT, max)
86
+ exit
87
+ end
88
+
89
+
90
+ unless file = ARGV.shift
91
+ STDERR.puts "Please specify a file to process."
92
+ end
93
+
94
+ io = File.open(file)
95
+
96
+ if gzip
97
+ require 'zlib'
98
+ io = Zlib::GzipReader.new(io)
99
+ end
100
+
101
+ emit = Silhouette.find_emitter(io)
102
+
103
+ if ascii
104
+ if long
105
+ ap = Silhouette::ASCIIConverterLong.new(ascii)
106
+ else
107
+ ap = Silhouette::ASCIIConverter.new(ascii)
108
+ end
109
+ puts "Saving ASCII profile to #{ascii} using #{ap.class}"
110
+ emit.processor = ap
111
+ emit.parse
112
+ ap.close
113
+ puts "Saved."
114
+ exit
115
+ end
116
+
117
+ if entry
118
+ rp = Silhouette::EntryPointProfiler.new(file, entry, depth)
119
+ else
120
+ rp = Silhouette::DefaultProfiler.new(callsite)
121
+ end
122
+ emit.processor = rp
123
+ emit.parse
124
+ if processed
125
+ rp.save(processed)
126
+ STDERR.puts "Saved data to #{processed}."
127
+ end
128
+ rp.print(STDOUT,max)
@@ -0,0 +1,18 @@
1
+ require "mkmf"
2
+
3
+ if RUBY_VERSION >= "1.9"
4
+ if RUBY_RELEASE_DATE < "2005-03-17"
5
+ STDERR.print("Ruby version is too old\n")
6
+ exit(1)
7
+ end
8
+ elsif RUBY_VERSION >= "1.8"
9
+ if RUBY_RELEASE_DATE < "2005-03-22"
10
+ STDERR.print("Ruby version is too old #{RUBY_RELEASE_DATE}\n")
11
+ exit(1)
12
+ end
13
+ else
14
+ STDERR.print("Ruby version is too old\n")
15
+ exit(1)
16
+ end
17
+
18
+ create_makefile("silhouette_ext")
@@ -0,0 +1,16 @@
1
+
2
+ require "silhouette_ext"
3
+
4
+ at_exit {
5
+ STDERR.puts "Flushing profile information..."
6
+ Silhouette.stop_profile
7
+ }
8
+
9
+ if ENV["SILHOUETTE_FILE"]
10
+ file = ENV["SILHOUETTE_FILE"]
11
+ else
12
+ file = "silhouette.out"
13
+ end
14
+
15
+ STDERR.puts "Logging profile information to #{file}"
16
+ Silhouette.start_profile file
@@ -0,0 +1,653 @@
1
+ require 'pp'
2
+
3
+ module Silhouette
4
+
5
+ class InvalidFormat < Exception; end
6
+
7
+ def self.emitters
8
+ out = []
9
+ constants.each do |name|
10
+ con = const_get(name)
11
+ if Class === con and con.superclass == Emitter
12
+ out << con
13
+ end
14
+ end
15
+ out
16
+ end
17
+
18
+ def self.find_emitter(io)
19
+ if io.kind_of? String
20
+ raise "Unknown file" unless File.exists?(io)
21
+ io = File.open(io)
22
+ end
23
+
24
+ emitters.each do |em|
25
+ begin
26
+ return em.new(io)
27
+ rescue InvalidFormat
28
+ end
29
+ end
30
+
31
+ raise InvalidFormat, "Unable to find valid emitter"
32
+ end
33
+
34
+ class Emitter
35
+ def initialize(processor=nil)
36
+ @processor = processor
37
+ end
38
+
39
+ attr_accessor :processor
40
+ end
41
+
42
+ class BinaryEmitter < Emitter
43
+ MAGIC = "<>"
44
+
45
+ def initialize(file, processor=nil)
46
+ if file.kind_of? String
47
+ raise "Unknown file" unless File.exists?(file)
48
+ @io = File.open(file)
49
+ else
50
+ @io = file
51
+ end
52
+
53
+ magic = @io.read(2)
54
+ raise InvalidFormat unless magic == MAGIC
55
+
56
+ @method_size = "S"
57
+ @file_size = "S"
58
+ @ret_call_fmt = "i#{@method_size}#{@file_size}ii"
59
+ @ret_call_size = 16
60
+ super(processor)
61
+ end
62
+
63
+ class DoneParsing < Exception; end
64
+
65
+ def parse
66
+ begin
67
+ loop { emit }
68
+ rescue DoneParsing
69
+ end
70
+ end
71
+
72
+ FIXED_SIZE = {
73
+ ?c => 20,
74
+ ?r => 20,
75
+ ?@ => 8
76
+ }
77
+
78
+ def next_cmd
79
+ str = @io.read(1)
80
+ raise DoneParsing unless str
81
+
82
+ cmd = str[0]
83
+ if [?!, ?*, ?&].include? cmd
84
+ size = @io.read(4).unpack("i").first
85
+ else
86
+ size = FIXED_SIZE[cmd]
87
+ end
88
+
89
+ [cmd, size, @io.read(size)]
90
+ end
91
+
92
+ def parse
93
+ begin
94
+ # Use "while true" instead of "loop" because loop is
95
+ # really a method call.
96
+ while true
97
+ data = @io.read(1)
98
+ raise DoneParsing unless data
99
+
100
+ cmd = data[0]
101
+
102
+ # These are hardcoded in here for speed.
103
+ if cmd == ?r or cmd == ?c
104
+ size = @ret_call_size
105
+ elsif cmd == ?@
106
+ size = 8
107
+ else
108
+ size = @io.read(4).unpack("i").first
109
+ end
110
+
111
+ proc = @processor
112
+ data = @io.read(size)
113
+
114
+ case cmd
115
+ when ?r
116
+ parts = data.unpack(@ret_call_fmt)
117
+ @processor.process_return(*parts)
118
+ # return [:return, *parts]
119
+ when ?c
120
+ parts = data.unpack(@ret_call_fmt)
121
+ @processor.process_call(*parts)
122
+ # return [:call, *parts]
123
+ when ?!
124
+ parts = data.unpack("Z#{size - 8}ii")
125
+ @processor.process_start(*parts)
126
+ # return [:start, *parts]
127
+ when ?@
128
+ parts = data.unpack("ii")
129
+ @processor.process_end(*parts)
130
+ # return [:end, *parts]
131
+ when ?&
132
+ parts = data.unpack("ia#{size - 4}")
133
+ parts += parts.pop.split("\0")
134
+ @processor.process_method(*parts)
135
+ # return [:method, *parts]
136
+ when ?*
137
+ parts = data.unpack("iZ#{size - 4}")
138
+ @processor.process_file(*parts)
139
+ # return [:file, *parts]
140
+ when ?(
141
+ @method_size = "I"
142
+ @ret_call_size += 2
143
+ @ret_call_fmt = "i#{@method_size}#{@file_size}ii"
144
+ when ?)
145
+ @file_size = "I"
146
+ @ret_call_size += 2
147
+ @ret_call_fmt = "i#{@method_size}#{@file_size}ii"
148
+ else
149
+ raise "Unknown type '#{cmd.chr}'"
150
+ end
151
+ end
152
+
153
+ # This means we're done.
154
+ rescue DoneParsing
155
+ end
156
+ end
157
+ end
158
+
159
+ class Processor
160
+ def initialize(emitter)
161
+ @emitter = emitter
162
+ @processors = Hash.new
163
+ methods.grep(/^process_(.*)/) do |meth|
164
+ @processors[$1.to_sym] = meth.to_sym
165
+ end
166
+ end
167
+
168
+ def run
169
+ @emitter.parse do |kind, *args|
170
+ if meth = @processors[kind]
171
+ __send__(meth, *args)
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ # NOTE: This uses IO#write instead of IO#puts
178
+ # because IO#write is faster as it does a quick test
179
+ # to see if the argument is already a string and just
180
+ # writes it if it is. IO#puts calls respond_to? on
181
+ # all arguments to see if they are strings, which is
182
+ # a lot slower if you do this 20,000 times.
183
+ class ASCIIConverter < Processor
184
+ def initialize(file)
185
+ @io = File.open(file, "w")
186
+ end
187
+
188
+ def process_start(*args)
189
+ @io.write "! #{args.join(' ')}\n"
190
+ end
191
+
192
+ def process_end(*args)
193
+ @io.write "@ #{args.join(' ')}\n"
194
+ end
195
+
196
+ def process_method(*args)
197
+ @io.write "& #{args.join(' ')}\n"
198
+ end
199
+
200
+ def process_file(*args)
201
+ @io.write "* #{args.join(' ')}\n"
202
+ end
203
+
204
+ def process_call(*args)
205
+ @io.write "c #{args.join(' ')}\n"
206
+ end
207
+
208
+ def process_return(*args)
209
+ @io.write "r #{args.join(' ')}\n"
210
+ end
211
+
212
+ def close
213
+ @io.close
214
+ end
215
+ end
216
+
217
+ class ASCIIConverterLong < ASCIIConverter
218
+
219
+ def initialize(file)
220
+ @methods = Hash.new
221
+ @files = Hash.new
222
+ @last_method = nil
223
+ @last_series = nil
224
+ @skip_return = false
225
+ super(file)
226
+ end
227
+ def process_method(idx, klass, kind, meth)
228
+ @methods[idx] = [klass, kind, meth].to_s
229
+ end
230
+
231
+ def process_file(idx, file)
232
+ @files[idx] = file
233
+ end
234
+
235
+ def process_call(thread, meth, file, line, clock)
236
+ @io.puts "c #{thread} #{@methods[meth]} #{@files[file]} #{line} #{clock}"
237
+ end
238
+
239
+ def process_return(thread, meth, file, line, clock)
240
+ @io.puts "r #{thread} #{@methods[meth]} #{clock}"
241
+ end
242
+
243
+ def process_call_rep(thread, meth, file, line, clock)
244
+ if @last_method == [thread, meth, file, line] and @last_series
245
+ @last_series += 1
246
+ @skip_return = true
247
+ else
248
+ @io.puts "cal #{thread} #{@methods[meth]} #{meth} #{@files[file]} #{line} #{clock}"
249
+ end
250
+
251
+ @last_method = [thread, meth, file, line]
252
+ end
253
+
254
+ def process_return_rep(thread, meth, file, line, clock)
255
+ if @last_method == [thread, meth, file, line]
256
+ @last_series = 1 unless @last_series
257
+ elsif @last_series
258
+ p [thread, meth, @methods[meth]]
259
+ p @last_method
260
+ @io.puts "rep #{@last_series}"
261
+ @last_series = nil
262
+ @skip_return = false
263
+ end
264
+ return if @skip_return
265
+ @io.puts "ret #{thread} #{@methods[meth]} #{clock}"
266
+ end
267
+ end
268
+
269
+ class CallTree
270
+ def initialize
271
+ end
272
+ end
273
+
274
+ class CallNode
275
+ def initialize
276
+ end
277
+ end
278
+
279
+ class ProfileNode
280
+ attr_accessor :total_sec, :self_sec, :calls
281
+ attr_accessor :callers, :children
282
+
283
+ attr_reader :key
284
+ def initialize(key, children=[])
285
+ @key = key
286
+ @total_sec = 0.0
287
+ @self_sec = 0.0
288
+ @calls = 0
289
+ @callers = []
290
+ @children = children
291
+ end
292
+
293
+ def inc_call!
294
+ @calls += 1
295
+ end
296
+
297
+ def add_cost(cost, last)
298
+ @total_sec += cost
299
+ @self_sec += (cost - last)
300
+ end
301
+
302
+ def percentage(total)
303
+ @self_sec / total * 100.0
304
+ end
305
+
306
+ def self_ms_per_call
307
+ (@self_sec * 1000.0 / @calls)
308
+ end
309
+
310
+ def total_ms_per_call
311
+ (@total_sec * 1000.0 / @calls)
312
+ end
313
+
314
+ alias method key
315
+ end
316
+
317
+ class DefaultProfiler < Processor
318
+
319
+ def initialize(per_callsite=false)
320
+ @threads = Hash.new
321
+ @map = Hash.new
322
+ @cs_map = Hash.new
323
+ @timestamps = []
324
+ @clocks = []
325
+ @pid = nil
326
+ @total = 0.0
327
+ @filters = []
328
+ @methods = Hash.new
329
+ @files = Hash.new
330
+ @per_callsite = per_callsite
331
+ end
332
+
333
+ def stack(thread)
334
+ @threads[thread] ||= []
335
+ end
336
+
337
+ attr_reader :directory, :timestamps
338
+
339
+ def data
340
+ @map
341
+ end
342
+
343
+ def data=(d)
344
+ @map = d
345
+ end
346
+
347
+ def run
348
+ @emitter.parse do |kind, *args|
349
+ case kind
350
+ when :start
351
+ @directory, @clock_per_sec, @start_clock = *args
352
+ when :end
353
+ @final_clock, @profiler_cost = *args
354
+ when :method
355
+ idx = args.shift
356
+ @methods[idx] = args
357
+ when :file
358
+ idx = args.shift
359
+ @files[idx] = args
360
+ when :call
361
+ thread, method, file, line, clock = *args
362
+ stack(thread).push [clock, 0.0, method]
363
+ when :return
364
+ thread, method, file, line, clock = *args
365
+ if tick = stack(thread).pop
366
+ if tick.last != method
367
+ STDERR.puts "Unmatched return for #{method} (#{tick.last})"
368
+ return
369
+ end
370
+
371
+ if @per_callsite
372
+ key = [method, loc]
373
+ else
374
+ key = method
375
+ end
376
+
377
+ data = (@map[key] ||= [0, 0.0, 0.0, key])
378
+ data[0] += 1
379
+ cost = (clock.to_f - tick[0]) / @clock_per_sec
380
+ data[2] += cost
381
+ data[1] += cost - tick[1]
382
+ @total += cost
383
+
384
+ # Add to the callers callee cost.
385
+ if last = stack(thread).last
386
+ last[1] += cost
387
+ end
388
+ end
389
+ end
390
+ end
391
+ end
392
+
393
+ def save(file)
394
+ File.open(file, "w") do |f|
395
+ f << Marshal.dump(self)
396
+ end
397
+ end
398
+
399
+ def process_start(*args)
400
+ @directory, @clock_per_sec, @start_clock = *args
401
+ end
402
+
403
+ def process_end(*args)
404
+ @final_clock, @profiler_cost = *args
405
+ end
406
+
407
+ def process_method(*args)
408
+ idx = args.shift
409
+ @methods[idx] = args
410
+ end
411
+
412
+ def process_file(*args)
413
+ idx = args.shift
414
+ @files[idx] = args
415
+ end
416
+
417
+ def process_call(thread, method, file, line, clock)
418
+ st = (@threads[thread] ||= [])
419
+ st.push [clock, 0.0, [method, file, line], []]
420
+ end
421
+
422
+ def process_return(thread, method, file, line, clock)
423
+ st = (@threads[thread] ||= [])
424
+ if tick = st.pop
425
+ =begin
426
+ if tick.last != method
427
+ STDERR.puts "Unmatched return for #{method} (#{tick.last})"
428
+ return
429
+ end
430
+ if @per_callsite
431
+ key = [method, loc]
432
+ else
433
+ key = method
434
+ end
435
+ =end
436
+ cost = (clock.to_f - tick[0]) / @clock_per_sec
437
+
438
+ # Add to the callers callee cost and child list.
439
+ if last = st.last
440
+ last[1] += cost
441
+ last[3] << method
442
+ caller = last[2]
443
+ else
444
+ caller = nil
445
+ end
446
+
447
+ # Record the data for the method.
448
+ key = method
449
+ node = (@map[key] ||= ProfileNode.new(key, tick.last))
450
+ node.inc_call!
451
+ node.add_cost cost, tick[1]
452
+ node.callers << caller.first if caller
453
+
454
+ # data = (@map[key] ||= [0, 0.0, 0.0, key, tick.last, []])
455
+ # data[0] += 1
456
+ # data[2] += cost
457
+ # data[1] += cost - tick[1]
458
+ # data[5] << caller.first if caller # Just the method index
459
+
460
+ # Record the data for the method at callsite
461
+ # key = [method, file, line]
462
+ # data = (@cs_map[key] ||= [0, 0.0, 0.0, key, tick.last, caller])
463
+ # data[0] += 1
464
+ # data[2] += cost
465
+ # data[1] += cost - tick[1]
466
+
467
+ end
468
+ end
469
+
470
+ def print(f=STDERR, max=nil)
471
+ print_flat_profile(f, max)
472
+ print_tree_profile(f, max)
473
+ end
474
+
475
+ def total_seconds
476
+ (@final_clock - @start_clock ).to_f / @clock_per_sec
477
+ end
478
+
479
+ def print_flat_profile(f=STDERR, max=nil)
480
+ f.puts "Number of threads: #{@threads.size}"
481
+ if @per_callsite
482
+ f.puts "Profiling based on method call and call site."
483
+ else
484
+ f.puts "Profiling based on method call."
485
+ end
486
+ f.puts "Cost of profiler: #{@profiler_cost.to_f / @clock_per_sec} seconds."
487
+
488
+ total = total_seconds
489
+ total = 0.01 if total == 0
490
+ f.puts "\nFlat profile (#{total} total seconds):"
491
+ data = @map.values
492
+ data.sort! { |a,b| b.self_sec <=> a.self_sec }
493
+ # data.sort! { |a,b| b[1] <=> a[1] }
494
+ sum = 0
495
+ f.puts " % total self self total"
496
+ f.puts " time seconds seconds calls ms/call ms/call name"
497
+ count = 0
498
+ data.each do |node|
499
+ #data.each do |calls, self_ms, total_ms, sig|
500
+ sum += node.self_sec
501
+
502
+ prec = node.percentage(total)
503
+ next if prec < 0.01
504
+
505
+ f.printf "%6.2f ", prec
506
+ f.printf "%8.2f ", sum
507
+ f.printf "%8.2f ", node.self_sec
508
+ f.printf "%8d ", node.calls
509
+ f.printf "%8.2f ", node.self_ms_per_call
510
+ f.printf "%8.2f ", node.total_ms_per_call
511
+ f.puts get_name(node.method)
512
+
513
+ count += 1
514
+ return if max and count > max
515
+ end
516
+ end
517
+
518
+ def collapse_children(cl, map)
519
+ out = Hash.new { |h,k| h[k] = 0 }
520
+
521
+ # p cl
522
+ cl.each do |c|
523
+ out[c] += 1
524
+ end
525
+
526
+ data = []
527
+ out.each do |meth,times|
528
+ # ch_ms = (map[meth][1] * 1000) / map[meth][0]
529
+ data << [meth, times, map[meth]]
530
+ end
531
+
532
+ data.sort! { |a,b| b[2].self_sec <=> a[2].self_sec }
533
+ data
534
+ end
535
+
536
+ def show_callers(callers, map)
537
+ end
538
+
539
+ def print_tree_profile(f=STDERR, max=nil)
540
+ width = 40
541
+ f.puts
542
+ f.puts "Call Tree Profile: "
543
+ f.puts "index calls ms/ self total"
544
+ f.puts " call sec sec"
545
+ map = @map
546
+
547
+ data = map.values
548
+ data.sort! { |a,b| b.self_sec <=> a.self_sec }
549
+ data.each do |pn|
550
+ next if pn.total_ms_per_call < 0.01
551
+ data = collapse_children(pn.callers, map)
552
+
553
+ data.each do |meth, called_times, cn|
554
+ times = map[meth].children.find_all { |i| i == pn.method }.size
555
+ if times == 0
556
+ times = "?"
557
+ else
558
+ called_times = called_times / times
559
+ end
560
+ vars = ["", "#{times}/#{called_times}", "-", "-",
561
+ "-", get_name(cn.method)]
562
+ f.puts "%-2s %14s %8s %8s %8s %s [#{meth}]" % vars
563
+ end
564
+
565
+ cl = collapse_children(pn.children, map)
566
+ chlines = []
567
+ sum = 0
568
+ cl.each do |meth, times, cn|
569
+ self_ms = cn.total_ms_per_call * times
570
+ sum += self_ms
571
+ self_sec = self_ms.to_f / 1000
572
+ total = self_sec * pn.calls
573
+
574
+ next if total < 0.01
575
+ vars = ["", times, cn.total_ms_per_call,
576
+ self_sec, total, get_name(cn.method)]
577
+ chlines << "%-8s %8d %8.2f %8.2f %8.2f %s [#{meth}]" % vars
578
+ end
579
+ vars = ["[#{pn.method}]", pn.calls,
580
+ pn.self_ms_per_call, pn.self_sec, pn.total_sec,
581
+ get_name(pn.method)]
582
+ f.puts "%-8s %8d %8.2f %8.2f %8.2f %s" % vars
583
+ f.puts *chlines if chlines.size > 0
584
+ f.puts "-" * 70
585
+ end
586
+ end
587
+
588
+ def get_name(info, per_cs=false)
589
+ if per_cs
590
+ @methods[info.first].to_s + " @ " + @files[info[1]].to_s + ":#{info[2]}"
591
+ else
592
+ @methods[info].to_s
593
+ end
594
+ end
595
+ end
596
+
597
+ class EntryPointProfiler < DefaultProfiler
598
+ def initialize(file, sig, depth=nil)
599
+ super(file)
600
+ @entry = sig
601
+ @start = Hash.new { |h,k| h[k] = false }
602
+ @max_depth = depth
603
+ @depth = Hash.new { |h,k| h[k] = 0 }
604
+ end
605
+
606
+ def process_call(thread, klass, kind, method, time)
607
+ #return unless thread == "b7533c3c"
608
+ if @entry == [klass, kind, method].to_s
609
+ @start[thread] = true
610
+ STDERR.puts "entered #{@entry} at #{time} in #{thread}"
611
+ return
612
+ end
613
+
614
+ return unless @start[thread]
615
+
616
+ # puts "#{[klass, kind, method]} (#{@depth[thread]})"
617
+
618
+ @depth[thread] += 1
619
+
620
+ #puts "call"
621
+ #p @depth
622
+ return if @max_depth and @depth[thread] > @max_depth
623
+ # puts "call: #{@depth} #{[klass, kind, method]}"
624
+ super
625
+ end
626
+
627
+ def process_return(thread, klass, kind, method, time)
628
+ #return unless thread == "b7533c3c"
629
+ if @entry == [klass, kind, method].to_s
630
+ @start[thread] = false
631
+ STDERR.puts "exitted #{@entry} at #{time} in #{thread}"
632
+ return
633
+ end
634
+
635
+ return unless @start[thread]
636
+
637
+ if !@max_depth or @depth[thread] <= @max_depth
638
+ super
639
+ end
640
+
641
+ @depth[thread] -= 1
642
+ end
643
+
644
+ def print(f=STDERR,max=nil)
645
+ f.puts "Calls only shown if performed from #{@entry}."
646
+ if @max_depth
647
+ f.puts "Calls only processed #{@max_depth} level(s) deep."
648
+ end
649
+ f.puts
650
+ super
651
+ end
652
+ end
653
+ end
@@ -0,0 +1,254 @@
1
+
2
+ #include <time.h>
3
+ #ifdef HAVE_SYS_TIMES_H
4
+ #include <sys/times.h>
5
+ #endif
6
+ #include <unistd.h>
7
+ #include <ruby.h>
8
+ #include <node.h>
9
+ #include <st.h>
10
+ #include <limits.h>
11
+
12
+ static FILE *pro_file;
13
+ static struct timeval global_tv;
14
+ static int profiling_pid;
15
+ static st_table *method_tbl;
16
+ static st_table *file_tbl;
17
+ static char *time_magic = "@";
18
+ static char *start_magic = "!";
19
+ static char *binary_magic = "<>";
20
+ static char *method_magic = "&";
21
+ static char *file_magic = "*";
22
+ static char *call_magic = "c";
23
+ static char *return_magic = "r";
24
+ static char *null_ptr = "\0";
25
+ static int profiler_cost = 0;
26
+ static char *method_size_magic = "(";
27
+ static char *file_size_magic = ")";
28
+ static int method_idx_size = 0;
29
+ static int file_idx_size = 0;
30
+
31
+ #define PER_TIME 10
32
+ #define HASH_SIZE 128
33
+
34
+ #define CLOCK clock()
35
+ #define STR(x) RSTRING(x)->ptr
36
+
37
+ static void
38
+ extprof_event_hook(rb_event_t event, NODE *node,
39
+ VALUE self, ID mid, VALUE in_klass) {
40
+
41
+ static int profiling = 0;
42
+ static char *method, *s_klass;
43
+ static char *kind, *file;
44
+ static int line;
45
+ static VALUE klass;
46
+ static char method_ent[1024];
47
+ static char file_ent[1024];
48
+ static int method_idx = 1;
49
+ static int file_idx = 1;
50
+ unsigned int current_clock = CLOCK;
51
+ unsigned int i, j, m_idx, f_idx, size;
52
+ unsigned short s_m_idx, s_f_idx;
53
+
54
+ if(!mid) return;
55
+
56
+ method = rb_id2name(mid);
57
+ if(!method) {
58
+ method = "<undefined>";
59
+ }
60
+
61
+ /* If we've forked, dont profile anymore. */
62
+ if(getpid() != profiling_pid) {
63
+ return;
64
+ }
65
+
66
+ if (mid == ID_ALLOCATOR) return;
67
+ if (profiling) return;
68
+ profiling++;
69
+
70
+ if(rb_obj_is_kind_of(self, rb_cModule)) {
71
+ kind = ".";
72
+ klass = rb_class_name(self);
73
+ i = RSTRING(klass)->len;
74
+ s_klass = RSTRING(klass)->ptr;
75
+ memcpy(method_ent, s_klass, i);
76
+ *(method_ent + i) = '.';
77
+ } else {
78
+ kind = "#";
79
+ klass = rb_class_name(in_klass);
80
+ i = RSTRING(klass)->len;
81
+ s_klass = RSTRING(klass)->ptr;
82
+ memcpy(method_ent, s_klass, i);
83
+ *(method_ent + i) = '#';
84
+ }
85
+
86
+ j = strlen(method);
87
+
88
+ memcpy(method_ent + i + 1, method, j);
89
+ *(method_ent + i + 1 + j) = 0;
90
+
91
+ // sprintf(method_ent, "%s%s%s", s_klass, kind, method);
92
+ if(!st_lookup(method_tbl, (st_data_t)method_ent, (st_data_t*)&m_idx)) {
93
+ m_idx = ++method_idx;
94
+ if(method_idx > USHRT_MAX) {
95
+ method_idx_size = 1;
96
+ fwrite(&method_size_magic, 1, 1, pro_file);
97
+ }
98
+ st_insert(method_tbl, (st_data_t)method_ent, method_idx);
99
+ fwrite(method_magic, 1, 1, pro_file);
100
+ size = 4 + RSTRING(klass)->len + 4 + j;
101
+ fwrite(&size, 4, 1, pro_file);
102
+ fwrite(&method_idx, 4, 1, pro_file);
103
+ fwrite(RSTRING(klass)->ptr, RSTRING(klass)->len, 1, pro_file);
104
+ fwrite(null_ptr, 1, 1, pro_file);
105
+ fwrite(kind, 1, 1, pro_file);
106
+ fwrite(null_ptr, 1, 1, pro_file);
107
+ fwrite(method, j, 1, pro_file);
108
+ fwrite(null_ptr, 1, 1, pro_file);
109
+ // fprintf(pro_file, "& %d %s %s %s\n", method_idx, s_klass, kind, method);
110
+ }
111
+
112
+ if(node) {
113
+ file = node->nd_file;
114
+ line = nd_line(node);
115
+ // sprintf(file_ent, "%s:%d", node->nd_file, nd_line(node));
116
+ if(!st_lookup(file_tbl, (st_data_t)(node->nd_file), (st_data_t*)&f_idx)) {
117
+ f_idx = ++file_idx;
118
+ if(file_idx > USHRT_MAX) {
119
+ file_idx_size = 1;
120
+ fwrite(&file_size_magic, 1, 1, pro_file);
121
+ }
122
+ st_insert(file_tbl, (st_data_t)(node->nd_file), file_idx);
123
+ fwrite(file_magic, 1, 1, pro_file);
124
+ j = strlen(node->nd_file);
125
+ size = j + 5;
126
+ fwrite(&size, 4, 1, pro_file);
127
+ fwrite(&file_idx, 4, 1, pro_file);
128
+ fwrite(node->nd_file, j + 1, 1, pro_file);
129
+ //fprintf(pro_file, "* %d %s\n", file_idx, node->nd_file);
130
+ }
131
+ } else {
132
+ file = "<unknown>";
133
+ line = 0;
134
+ f_idx = 0;
135
+ }
136
+ #define PL_S(type) fprintf(pro_file, #type " %x %d %d %d\n", (int)rb_thread_current(), \
137
+ m_idx, f_idx, CLOCK);
138
+ #define PL(type) fprintf(pro_file, #type " %x %s %s %s %d\n", (int)rb_thread_current(), \
139
+ STR(rb_class_name(klass)), kind, method, CLOCK)
140
+ #define PL_EXT(type) fprintf(pro_file, #type " %x %s %x %s %s %s %d %f\n", (int)rb_thread_current(), \
141
+ STR(rb_class_name(klass)), (int)self, kind, method, file, line, CLOCK)
142
+
143
+ switch(event) {
144
+ case RUBY_EVENT_RETURN:
145
+ case RUBY_EVENT_C_RETURN:
146
+ fwrite(return_magic, 1, 1, pro_file);
147
+ goto output;
148
+ case RUBY_EVENT_CALL:
149
+ case RUBY_EVENT_C_CALL:
150
+ fwrite(call_magic, 1, 1, pro_file);
151
+ output:
152
+ j = (int)rb_thread_current();
153
+ fwrite(&j, 4, 1, pro_file);
154
+
155
+ if(method_idx_size) {
156
+ fwrite(&m_idx, sizeof(m_idx), 1, pro_file);
157
+ } else {
158
+ s_m_idx = m_idx;
159
+ fwrite(&s_m_idx, sizeof(s_m_idx), 1, pro_file);
160
+ }
161
+
162
+ if(file_idx_size) {
163
+ fwrite(&f_idx, sizeof(f_idx), 1, pro_file);
164
+ } else {
165
+ s_f_idx = f_idx;
166
+ fwrite(&s_f_idx, sizeof(s_f_idx), 1, pro_file);
167
+ }
168
+
169
+ if(node) {
170
+ j = nd_line(node);
171
+ } else {
172
+ j = 0;
173
+ }
174
+ fwrite(&j, 4, 1, pro_file);
175
+ fwrite(&current_clock, 4, 1, pro_file);
176
+ break;
177
+ }
178
+
179
+ profiler_cost = profiler_cost + (CLOCK - current_clock);
180
+ profiling--;
181
+ }
182
+
183
+ static VALUE extprof_start(int argc, VALUE *argv, VALUE self) {
184
+ struct timeval tv;
185
+ char path[1024];
186
+ int size, i;
187
+
188
+ if(argc == 0) {
189
+ pro_file = fopen("silhouette.out","w");
190
+ } else {
191
+ StringValue(argv[0]);
192
+ pro_file = fopen(STR(argv[0]), "w");
193
+ }
194
+
195
+ profiling_pid = getpid();
196
+
197
+ method_tbl = st_init_strtable_with_size(HASH_SIZE);
198
+ file_tbl = st_init_strtable_with_size(HASH_SIZE);
199
+
200
+ getcwd(path, 1023);
201
+ size = strlen(path);
202
+ path[size] = 0;
203
+
204
+ fwrite(binary_magic, 2 ,1 , pro_file);
205
+ fwrite(start_magic, 1, 1, pro_file);
206
+ i = size + 9;
207
+ fwrite(&i, 4, 1, pro_file);
208
+ fwrite(path, size + 1, 1, pro_file);
209
+ i = CLOCKS_PER_SEC;
210
+ fwrite(&i, 4, 1, pro_file);
211
+ i = CLOCK;
212
+ fwrite(&i, 4, 1, pro_file);
213
+ // fprintf(pro_file, "! %s %d %d\n", getcwd(path, 1023), getpid(),
214
+ // CLOCKS_PER_SEC);
215
+ /*
216
+ gettimeofday(&tv, NULL);
217
+ fprintf(pro_file, "@ %d %d %d\n", (int)tv.tv_sec,
218
+ (int)tv.tv_usec, CLOCK);
219
+ */
220
+ rb_add_event_hook(extprof_event_hook,
221
+ RUBY_EVENT_CALL | RUBY_EVENT_RETURN |
222
+ RUBY_EVENT_C_CALL | RUBY_EVENT_C_RETURN);
223
+
224
+ return Qtrue;
225
+ }
226
+
227
+ static VALUE extprof_end(VALUE self) {
228
+ struct timeval tv;
229
+ int i;
230
+
231
+ rb_remove_event_hook(extprof_event_hook);
232
+
233
+ fwrite(time_magic, 1, 1, pro_file);
234
+ i = CLOCK;
235
+ fwrite(&i, 4, 1, pro_file);
236
+ fwrite(&profiler_cost, 4, 1, pro_file);
237
+ /*
238
+ gettimeofday(&tv, NULL);
239
+ fprintf(pro_file, "@ %d %d %d\n", (int)tv.tv_sec,
240
+ (int)tv.tv_usec, CLOCK);
241
+ */
242
+ fflush(pro_file);
243
+ fclose(pro_file);
244
+ return Qtrue;
245
+ }
246
+
247
+ void Init_silhouette_ext() {
248
+ VALUE extprof;
249
+ extprof = rb_define_module("Silhouette");
250
+ rb_define_singleton_method(extprof, "start_profile", extprof_start, -1);
251
+ rb_define_singleton_method(extprof, "stop_profile", extprof_end, 0);
252
+ }
253
+
254
+ /* vim: set filetype=c ts=4 sw=4 noexpandtab : */
Binary file
@@ -0,0 +1,33 @@
1
+
2
+ def plus
3
+ 1 + 1
4
+ end
5
+
6
+ def blah
7
+ 100.times { plus }
8
+ 100.times { plus }
9
+ bleh
10
+ end
11
+
12
+ def bleh
13
+ bloh
14
+ end
15
+
16
+ def bloh
17
+ end
18
+
19
+
20
+ def one
21
+ blah
22
+ end
23
+
24
+ def two
25
+ blah
26
+ end
27
+
28
+ begin
29
+ one
30
+ two
31
+ two
32
+ rescue
33
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: silhouette
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2005-12-06 00:00:00 -08:00
8
+ summary: A 2 stage profiler
9
+ require_paths:
10
+ - lib
11
+ email: evan@fallingsnow.net
12
+ homepage:
13
+ rubyforge_project:
14
+ description:
15
+ autorequire:
16
+ default_executable: silhouette
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ -
22
+ - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: ruby
27
+ signing_key:
28
+ cert_chain:
29
+ authors:
30
+ - Evan Webb
31
+ files:
32
+ - bin/silhouette
33
+ - extconf.rb
34
+ - lib/silhouette/processor.rb
35
+ - lib/silhouette.rb
36
+ - Manifest.txt
37
+ - Rakefile
38
+ - README
39
+ - silhouette_ext.c
40
+ - test/silhouette.out
41
+ - test/test.rb
42
+ test_files: []
43
+ rdoc_options: []
44
+ extra_rdoc_files: []
45
+ executables:
46
+ - silhouette
47
+ extensions:
48
+ - extconf.rb
49
+ requirements: []
50
+ dependencies: []