silhouette 1.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,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: []