ruby-prof 1.1.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.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES +532 -0
  3. data/LICENSE +25 -0
  4. data/README.rdoc +5 -0
  5. data/Rakefile +110 -0
  6. data/bin/ruby-prof +380 -0
  7. data/bin/ruby-prof-check-trace +45 -0
  8. data/ext/ruby_prof/extconf.rb +36 -0
  9. data/ext/ruby_prof/rp_allocation.c +279 -0
  10. data/ext/ruby_prof/rp_allocation.h +31 -0
  11. data/ext/ruby_prof/rp_call_info.c +271 -0
  12. data/ext/ruby_prof/rp_call_info.h +35 -0
  13. data/ext/ruby_prof/rp_measure_allocations.c +52 -0
  14. data/ext/ruby_prof/rp_measure_memory.c +42 -0
  15. data/ext/ruby_prof/rp_measure_process_time.c +67 -0
  16. data/ext/ruby_prof/rp_measure_wall_time.c +62 -0
  17. data/ext/ruby_prof/rp_measurement.c +230 -0
  18. data/ext/ruby_prof/rp_measurement.h +50 -0
  19. data/ext/ruby_prof/rp_method.c +630 -0
  20. data/ext/ruby_prof/rp_method.h +70 -0
  21. data/ext/ruby_prof/rp_profile.c +895 -0
  22. data/ext/ruby_prof/rp_profile.h +37 -0
  23. data/ext/ruby_prof/rp_stack.c +196 -0
  24. data/ext/ruby_prof/rp_stack.h +56 -0
  25. data/ext/ruby_prof/rp_thread.c +337 -0
  26. data/ext/ruby_prof/rp_thread.h +36 -0
  27. data/ext/ruby_prof/ruby_prof.c +48 -0
  28. data/ext/ruby_prof/ruby_prof.h +17 -0
  29. data/ext/ruby_prof/vc/ruby_prof.sln +31 -0
  30. data/ext/ruby_prof/vc/ruby_prof.vcxproj +143 -0
  31. data/lib/ruby-prof.rb +52 -0
  32. data/lib/ruby-prof/assets/call_stack_printer.html.erb +713 -0
  33. data/lib/ruby-prof/assets/call_stack_printer.png +0 -0
  34. data/lib/ruby-prof/assets/graph_printer.html.erb +356 -0
  35. data/lib/ruby-prof/call_info.rb +57 -0
  36. data/lib/ruby-prof/call_info_visitor.rb +38 -0
  37. data/lib/ruby-prof/compatibility.rb +109 -0
  38. data/lib/ruby-prof/exclude_common_methods.rb +198 -0
  39. data/lib/ruby-prof/measurement.rb +14 -0
  40. data/lib/ruby-prof/method_info.rb +90 -0
  41. data/lib/ruby-prof/printers/abstract_printer.rb +127 -0
  42. data/lib/ruby-prof/printers/call_info_printer.rb +51 -0
  43. data/lib/ruby-prof/printers/call_stack_printer.rb +182 -0
  44. data/lib/ruby-prof/printers/call_tree_printer.rb +151 -0
  45. data/lib/ruby-prof/printers/dot_printer.rb +132 -0
  46. data/lib/ruby-prof/printers/flat_printer.rb +52 -0
  47. data/lib/ruby-prof/printers/graph_html_printer.rb +63 -0
  48. data/lib/ruby-prof/printers/graph_printer.rb +114 -0
  49. data/lib/ruby-prof/printers/multi_printer.rb +127 -0
  50. data/lib/ruby-prof/profile.rb +33 -0
  51. data/lib/ruby-prof/rack.rb +171 -0
  52. data/lib/ruby-prof/task.rb +147 -0
  53. data/lib/ruby-prof/thread.rb +35 -0
  54. data/lib/ruby-prof/version.rb +3 -0
  55. data/lib/unprof.rb +10 -0
  56. data/ruby-prof.gemspec +58 -0
  57. data/test/abstract_printer_test.rb +26 -0
  58. data/test/alias_test.rb +129 -0
  59. data/test/basic_test.rb +129 -0
  60. data/test/call_info_visitor_test.rb +31 -0
  61. data/test/duplicate_names_test.rb +32 -0
  62. data/test/dynamic_method_test.rb +53 -0
  63. data/test/enumerable_test.rb +21 -0
  64. data/test/exceptions_test.rb +24 -0
  65. data/test/exclude_methods_test.rb +146 -0
  66. data/test/exclude_threads_test.rb +53 -0
  67. data/test/fiber_test.rb +73 -0
  68. data/test/gc_test.rb +96 -0
  69. data/test/line_number_test.rb +161 -0
  70. data/test/marshal_test.rb +119 -0
  71. data/test/measure_allocations.rb +30 -0
  72. data/test/measure_allocations_test.rb +385 -0
  73. data/test/measure_allocations_trace_test.rb +385 -0
  74. data/test/measure_memory_trace_test.rb +756 -0
  75. data/test/measure_process_time_test.rb +849 -0
  76. data/test/measure_times.rb +54 -0
  77. data/test/measure_wall_time_test.rb +459 -0
  78. data/test/multi_printer_test.rb +71 -0
  79. data/test/no_method_class_test.rb +15 -0
  80. data/test/parser_timings.rb +24 -0
  81. data/test/pause_resume_test.rb +166 -0
  82. data/test/prime.rb +56 -0
  83. data/test/printer_call_stack_test.rb +28 -0
  84. data/test/printer_call_tree_test.rb +31 -0
  85. data/test/printer_flat_test.rb +68 -0
  86. data/test/printer_graph_html_test.rb +60 -0
  87. data/test/printer_graph_test.rb +41 -0
  88. data/test/printers_test.rb +141 -0
  89. data/test/printing_recursive_graph_test.rb +81 -0
  90. data/test/rack_test.rb +157 -0
  91. data/test/recursive_test.rb +210 -0
  92. data/test/singleton_test.rb +38 -0
  93. data/test/stack_printer_test.rb +64 -0
  94. data/test/start_stop_test.rb +109 -0
  95. data/test/test_helper.rb +24 -0
  96. data/test/thread_test.rb +144 -0
  97. data/test/unique_call_path_test.rb +190 -0
  98. data/test/yarv_test.rb +56 -0
  99. metadata +191 -0
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+
3
+ module RubyProf
4
+ # Prints out the call graph based on CallInfo instances. This
5
+ # is mainly for debugging purposes as it provides access into
6
+ # into RubyProf's internals.
7
+ #
8
+ # To use the printer:
9
+ #
10
+ # result = RubyProf.profile do
11
+ # [code to profile]
12
+ # end
13
+ #
14
+ # printer = RubyProf::CallInfoPrinter.new(result)
15
+ # printer.print(STDOUT)
16
+ class CallInfoPrinter < AbstractPrinter
17
+ TIME_WIDTH = 0
18
+
19
+ private
20
+
21
+ def print_header(thread)
22
+ @output << "Thread ID: #{thread.id}\n"
23
+ @output << "Fiber ID: #{thread.fiber_id}\n"
24
+ @output << "Total Time: #{thread.total_time}\n"
25
+ @output << "Sort by: #{sort_method}\n"
26
+ @output << "\n"
27
+ end
28
+
29
+ def print_methods(thread)
30
+ visitor = CallInfoVisitor.new(thread.root_methods)
31
+
32
+ visitor.visit do |call_info, event|
33
+ if event == :enter
34
+ @output << " " * call_info.depth
35
+ @output << call_info.target.full_name
36
+ @output << " ("
37
+ @output << "tt:#{sprintf("%#{TIME_WIDTH}.2f", call_info.total_time)}, "
38
+ @output << "st:#{sprintf("%#{TIME_WIDTH}.2f", call_info.self_time)}, "
39
+ @output << "wt:#{sprintf("%#{TIME_WIDTH}.2f", call_info.wait_time)}, "
40
+ @output << "ct:#{sprintf("%#{TIME_WIDTH}.2f", call_info.children_time)}, "
41
+ @output << "call:#{call_info.called}, "
42
+ @output << ")"
43
+ @output << "\n"
44
+ end
45
+ end
46
+ end
47
+
48
+ def print_footer(thread)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,182 @@
1
+ # encoding: utf-8
2
+
3
+ require 'erb'
4
+ require 'fileutils'
5
+ require 'base64'
6
+ require 'set'
7
+
8
+ module RubyProf
9
+ # Prints a HTML visualization of the call tree.
10
+ #
11
+ # To use the printer:
12
+ #
13
+ # result = RubyProf.profile do
14
+ # [code to profile]
15
+ # end
16
+ #
17
+ # printer = RubyProf::CallStackPrinter.new(result)
18
+ # printer.print(STDOUT)
19
+
20
+ class CallStackPrinter < AbstractPrinter
21
+ include ERB::Util
22
+
23
+ # Specify print options.
24
+ #
25
+ # options - Hash table
26
+ # :min_percent - Number 0 to 100 that specifes the minimum
27
+ # %self (the methods self time divided by the
28
+ # overall total time) that a method must take
29
+ # for it to be printed out in the report.
30
+ # Default value is 0.
31
+ #
32
+ # :threshold - a float from 0 to 100 that sets the threshold of
33
+ # results displayed.
34
+ # Default value is 1.0
35
+ #
36
+ # :title - a String to overide the default "ruby-prof call tree"
37
+ # title of the report.
38
+ #
39
+ # :expansion - a float from 0 to 100 that sets the threshold of
40
+ # results that are expanded, if the percent_total
41
+ # exceeds it.
42
+ # Default value is 10.0
43
+ #
44
+ # :application - a String to overide the name of the application,
45
+ # as it appears on the report.
46
+ def print(output = STDOUT, options = {})
47
+ setup_options(options)
48
+ output << @erb.result(binding)
49
+ a = 1
50
+ end
51
+
52
+ # :enddoc:
53
+ def setup_options(options)
54
+ super(options)
55
+ @erb = ERB.new(self.template)
56
+ end
57
+
58
+ def print_stack(output, visited, call_info, parent_time)
59
+ total_time = call_info.total_time
60
+ percent_parent = (total_time/parent_time)*100
61
+ percent_total = (total_time/@overall_time)*100
62
+ return unless percent_total > min_percent
63
+ color = self.color(percent_total)
64
+ kids = call_info.target.callees
65
+ visible = percent_total >= threshold
66
+ expanded = percent_total >= expansion
67
+ display = visible ? "block" : "none"
68
+
69
+ output << "<li class=\"color#{color}\" style=\"display:#{display}\">" << "\n"
70
+
71
+ if visited.include?(call_info)
72
+ output << "<a href=\"#\" class=\"toggle empty\" ></a>" << "\n"
73
+ output << "<span>%s %s</span>" % [link(call_info.target, true), graph_link(call_info)] << "\n"
74
+ else
75
+ visited << call_info
76
+
77
+ if kids.empty?
78
+ output << "<a href=\"#\" class=\"toggle empty\" ></a>" << "\n"
79
+ else
80
+ visible_children = kids.any?{|ci| (ci.total_time/@overall_time)*100 >= threshold}
81
+ image = visible_children ? (expanded ? "minus" : "plus") : "empty"
82
+ output << "<a href=\"#\" class=\"toggle #{image}\" ></a>" << "\n"
83
+ end
84
+ output << "<span>%4.2f%% (%4.2f%%) %s %s</span>" % [percent_total, percent_parent,
85
+ link(call_info.target, false), graph_link(call_info)] << "\n"
86
+
87
+ unless kids.empty?
88
+ output << (expanded ? '<ul>' : '<ul style="display:none">') << "\n"
89
+ kids.sort_by{|c| -c.total_time}.each do |child_call_info|
90
+ print_stack(output, visited, child_call_info, total_time)
91
+ end
92
+ output << '</ul>' << "\n"
93
+ end
94
+
95
+ visited.delete(call_info)
96
+ end
97
+ output << '</li>' << "\n"
98
+ end
99
+
100
+ def name(call_info)
101
+ method = call_info.target
102
+ method.full_name
103
+ end
104
+
105
+ def link(method, recursive)
106
+ method_name = "#{recursive ? '*' : ''}#{method.full_name}"
107
+ if method.source_file.nil?
108
+ h method_name
109
+ else
110
+ file = File.expand_path(method.source_file)
111
+ "<a href=\"file://#{file}##{method.line}\">#{h method_name}</a>"
112
+ end
113
+ end
114
+
115
+ def graph_link(call_info)
116
+ total_calls = call_info.target.called
117
+ href = "#{method_href(call_info.target)}"
118
+ totals = total_calls.to_s
119
+ "[#{call_info.called} calls, #{totals} total]"
120
+ end
121
+
122
+ def method_href(method)
123
+ h(method.full_name.gsub(/[><#\.\?=:]/,"_"))
124
+ end
125
+
126
+ def total_time(call_infos)
127
+ sum(call_infos.map{|ci| ci.total_time})
128
+ end
129
+
130
+ def sum(a)
131
+ a.inject(0.0){|s,t| s+=t}
132
+ end
133
+
134
+ def dump(ci)
135
+ $stderr.printf "%s/%d t:%f s:%f w:%f \n", ci, ci.object_id, ci.total_time, ci.self_time, ci.wait_time
136
+ end
137
+
138
+ def color(p)
139
+ case i = p.to_i
140
+ when 0..5
141
+ "01"
142
+ when 5..10
143
+ "05"
144
+ when 100
145
+ "9"
146
+ else
147
+ "#{i/10}"
148
+ end
149
+ end
150
+
151
+ def application
152
+ @options[:application] || $PROGRAM_NAME
153
+ end
154
+
155
+ def arguments
156
+ ARGV.join(' ')
157
+ end
158
+
159
+ def title
160
+ @title ||= @options.delete(:title) || "ruby-prof call tree"
161
+ end
162
+
163
+ def threshold
164
+ @options[:threshold] || 1.0
165
+ end
166
+
167
+ def expansion
168
+ @options[:expansion] || 10.0
169
+ end
170
+
171
+ def base64_image
172
+ @data ||= begin
173
+ file = open_asset('call_stack_printer.png')
174
+ Base64.encode64(file).gsub(/\n/, '')
175
+ end
176
+ end
177
+
178
+ def template
179
+ open_asset('call_stack_printer.html.erb')
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,151 @@
1
+ # encoding: utf-8
2
+
3
+ require 'fiber'
4
+ require 'thread'
5
+ require 'fileutils'
6
+
7
+ module RubyProf
8
+ # Generates profiling information in callgrind format for use by
9
+ # kcachegrind and similar tools.
10
+
11
+ class CallTreePrinter < AbstractPrinter
12
+ def calltree_name(method_info)
13
+ klass_path = method_info.klass_name.gsub("::", '/')
14
+ result = "#{klass_path}::#{method_info.method_name}"
15
+
16
+ case method_info.klass_flags
17
+ when 0x2
18
+ "#{result}^"
19
+ when 0x4
20
+ "#{result}^"
21
+ when 0x8
22
+ "#{result}*"
23
+ else
24
+ result
25
+ end
26
+ end
27
+
28
+ def determine_event_specification_and_value_scale
29
+ @event_specification = "events: "
30
+ case RubyProf.measure_mode
31
+ when RubyProf::PROCESS_TIME
32
+ @value_scale = RubyProf::CLOCKS_PER_SEC
33
+ @event_specification << 'process_time'
34
+ when RubyProf::WALL_TIME
35
+ @value_scale = 1_000_000
36
+ @event_specification << 'wall_time'
37
+ when RubyProf.const_defined?(:ALLOCATIONS) && RubyProf::ALLOCATIONS
38
+ @value_scale = 1
39
+ @event_specification << 'allocations'
40
+ when RubyProf.const_defined?(:MEMORY) && RubyProf::MEMORY
41
+ @value_scale = 1
42
+ @event_specification << 'memory'
43
+ when RubyProf.const_defined?(:GC_RUNS) && RubyProf::GC_RUNS
44
+ @value_scale = 1
45
+ @event_specification << 'gc_runs'
46
+ when RubyProf.const_defined?(:GC_TIME) && RubyProf::GC_TIME
47
+ @value_scale = 1000000
48
+ @event_specification << 'gc_time'
49
+ else
50
+ raise "Unknown measure mode: #{RubyProf.measure_mode}"
51
+ end
52
+ end
53
+
54
+ def print(options = {})
55
+ validate_print_params(options)
56
+ setup_options(options)
57
+ determine_event_specification_and_value_scale
58
+ print_threads
59
+ end
60
+
61
+ def validate_print_params(options)
62
+ if options.is_a?(IO)
63
+ raise ArgumentError, "#{self.class.name}#print cannot print to IO objects"
64
+ elsif !options.is_a?(Hash)
65
+ raise ArgumentError, "#{self.class.name}#print requires an options hash"
66
+ end
67
+ end
68
+
69
+ def print_threads
70
+ remove_subsidiary_files_from_previous_profile_runs
71
+ # TODO: merge fibers of a given thread here, instead of relying
72
+ # on the profiler to merge fibers.
73
+ @result.threads.each do |thread|
74
+ print_thread(thread)
75
+ end
76
+ end
77
+
78
+ def convert(value)
79
+ (value * @value_scale).round
80
+ end
81
+
82
+ def file(method)
83
+ method.source_file ? File.expand_path(method.source_file) : ''
84
+ end
85
+
86
+ def print_thread(thread)
87
+ File.open(file_path_for_thread(thread), "w") do |f|
88
+ print_headers(f, thread)
89
+ thread.methods.reverse_each do |method|
90
+ print_method(f, method)
91
+ end
92
+ end
93
+ end
94
+
95
+ def path
96
+ @options[:path] || "."
97
+ end
98
+
99
+ def self.needs_dir?
100
+ true
101
+ end
102
+
103
+ def base_name
104
+ @options[:profile] || "profile"
105
+ end
106
+
107
+ def remove_subsidiary_files_from_previous_profile_runs
108
+ pattern = [base_name, "callgrind.out", $$, "*"].join(".")
109
+ files = Dir.glob(File.join(path, pattern))
110
+ FileUtils.rm_f(files)
111
+ end
112
+
113
+ def file_name_for_thread(thread)
114
+ if thread.fiber_id == Fiber.current.object_id
115
+ [base_name, "callgrind.out", $$].join(".")
116
+ else
117
+ [base_name, "callgrind.out", $$, thread.fiber_id].join(".")
118
+ end
119
+ end
120
+
121
+ def file_path_for_thread(thread)
122
+ File.join(path, file_name_for_thread(thread))
123
+ end
124
+
125
+ def print_headers(output, thread)
126
+ output << "#{@event_specification}\n\n"
127
+ # this doesn't work. kcachegrind does not fully support the spec.
128
+ # output << "thread: #{thread.id}\n\n"
129
+ end
130
+
131
+ def print_method(output, method)
132
+ # Print out the file and method name
133
+ output << "fl=#{file(method)}\n"
134
+ output << "fn=#{self.calltree_name(method)}\n"
135
+
136
+ # Now print out the function line number and its self time
137
+ output << "#{method.line} #{convert(method.self_time)}\n"
138
+
139
+ # Now print out all the children methods
140
+ method.callees.each do |callee|
141
+ output << "cfl=#{file(callee.target)}\n"
142
+ output << "cfn=#{self.calltree_name(callee.target)}\n"
143
+ output << "calls=#{callee.called} #{callee.line}\n"
144
+
145
+ # Print out total times here!
146
+ output << "#{callee.line} #{convert(callee.total_time)}\n"
147
+ end
148
+ output << "\n"
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,132 @@
1
+ # encoding: utf-8
2
+
3
+ require 'set'
4
+
5
+ module RubyProf
6
+ # Generates a graphviz graph in dot format.
7
+ #
8
+ # To use the dot printer:
9
+ #
10
+ # result = RubyProf.profile do
11
+ # [code to profile]
12
+ # end
13
+ #
14
+ # printer = RubyProf::DotPrinter.new(result)
15
+ # printer.print(STDOUT)
16
+ #
17
+ # You can use either dot viewer such as GraphViz, or the dot command line tool
18
+ # to reformat the output into a wide variety of outputs:
19
+ #
20
+ # dot -Tpng graph.dot > graph.png
21
+ #
22
+ class DotPrinter < RubyProf::AbstractPrinter
23
+ CLASS_COLOR = '"#666666"'
24
+ EDGE_COLOR = '"#666666"'
25
+
26
+ # Creates the DotPrinter using a RubyProf::Proile.
27
+ def initialize(result)
28
+ super(result)
29
+ @seen_methods = Set.new
30
+ end
31
+
32
+ # Print a graph report to the provided output.
33
+ #
34
+ # output - Any IO object, including STDOUT or a file. The default value is
35
+ # STDOUT.
36
+ #
37
+ # options - Hash of print options. See #setup_options
38
+ # for more information.
39
+ #
40
+ # When profiling results that cover a large number of method calls it
41
+ # helps to use the :min_percent option, for example:
42
+ #
43
+ # DotPrinter.new(result).print(STDOUT, :min_percent=>5)
44
+ #
45
+ def print(output = STDOUT, options = {})
46
+ @output = output
47
+ setup_options(options)
48
+
49
+ puts 'digraph "Profile" {'
50
+ #puts "label=\"#{mode_name} >=#{min_percent}%\\nTotal: #{total_time}\";"
51
+ puts "labelloc=t;"
52
+ puts "labeljust=l;"
53
+ print_threads
54
+ puts '}'
55
+ end
56
+
57
+ private
58
+
59
+ # Something of a hack, figure out which constant went with the
60
+ # RubyProf.measure_mode so that we can display it. Otherwise it's easy to
61
+ # forget what measurement was made.
62
+ def mode_name
63
+ RubyProf.constants.find{|c| RubyProf.const_get(c) == RubyProf.measure_mode}
64
+ end
65
+
66
+ def print_threads
67
+ @result.threads.each do |thread|
68
+ puts "subgraph \"Thread #{thread.id}\" {"
69
+
70
+ print_thread(thread)
71
+ puts "}"
72
+
73
+ print_classes(thread)
74
+ end
75
+ end
76
+
77
+ # Determines an ID to use to represent the subject in the Dot file.
78
+ def dot_id(subject)
79
+ subject.object_id
80
+ end
81
+
82
+ def print_thread(thread)
83
+ total_time = thread.total_time
84
+ thread.methods.sort_by(&sort_method).reverse_each do |method|
85
+ total_percentage = (method.total_time/total_time) * 100
86
+
87
+ next if total_percentage < min_percent
88
+ name = method.full_name.split("#").last
89
+ puts "#{dot_id(method)} [label=\"#{name}\\n(#{total_percentage.round}%)\"];"
90
+ @seen_methods << method
91
+ print_edges(total_time, method)
92
+ end
93
+ end
94
+
95
+ def print_classes(thread)
96
+ grouped = {}
97
+ thread.methods.each{|m| grouped[m.klass_name] ||= []; grouped[m.klass_name] << m}
98
+ grouped.each do |cls, methods2|
99
+ # Filter down to just seen methods
100
+ big_methods = methods2.select{|m| @seen_methods.include? m}
101
+
102
+ if !big_methods.empty?
103
+ puts "subgraph cluster_#{cls.object_id} {"
104
+ puts "label = \"#{cls}\";"
105
+ puts "fontcolor = #{CLASS_COLOR};"
106
+ puts "fontsize = 16;"
107
+ puts "color = #{CLASS_COLOR};"
108
+ big_methods.each do |m|
109
+ puts "#{m.object_id};"
110
+ end
111
+ puts "}"
112
+ end
113
+ end
114
+ end
115
+
116
+ def print_edges(total_time, method)
117
+ method.callers.sort_by(&:total_time).reverse.each do |call_info|
118
+ target_percentage = (call_info.target.total_time / total_time) * 100.0
119
+ next if target_percentage < min_percent
120
+
121
+ # Get children method
122
+ puts "#{dot_id(method)} -> #{dot_id(call_info.target)} [label=\"#{call_info.called}/#{call_info.target.called}\" fontsize=10 fontcolor=#{EDGE_COLOR}];"
123
+ end
124
+ end
125
+
126
+ # Silly little helper for printing to the @output
127
+ def puts(str)
128
+ @output.puts(str)
129
+ end
130
+
131
+ end
132
+ end