ruby-prof 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -67,6 +67,15 @@ module RubyProf
67
67
  end
68
68
  end
69
69
 
70
+ def method_href(thread, method)
71
+ h(method.full_name.gsub(/[><#\.\?=:]/,"_") + "_" + thread.fiber_id.to_s)
72
+ end
73
+
74
+ def open_asset(file)
75
+ path = File.join(File.expand_path('../../assets', __FILE__), file)
76
+ File.open(path, 'rb').read
77
+ end
78
+
70
79
  def print_threads
71
80
  @result.threads.each do |thread|
72
81
  print_thread(thread)
@@ -17,7 +17,7 @@ module RubyProf
17
17
  # printer = RubyProf::CallStackPrinter.new(result)
18
18
  # printer.print(STDOUT)
19
19
 
20
- class CallStackPrinter < AbstractPrinter
20
+ class CallStackPrinter < AbstractPrinter
21
21
  include ERB::Util
22
22
 
23
23
  # Specify print options.
@@ -44,41 +44,18 @@ class CallStackPrinter < AbstractPrinter
44
44
  # :application - a String to overide the name of the application,
45
45
  # as it appears on the report.
46
46
  def print(output = STDOUT, options = {})
47
- @output = output
48
47
  setup_options(options)
49
- if @graph_html = options.delete(:graph)
50
- @graph_html = "file://" + @graph_html if @graph_html[0]=="/"
51
- end
52
-
53
- print_header
54
-
55
- @overall_threads_time = @result.threads.inject(0) do |val, thread|
56
- val += thread.total_time
57
- end
58
-
59
- @result.threads.each do |thread|
60
- @current_thread_id = thread.fiber_id
61
- @overall_time = thread.total_time
62
- thread_info = String.new("Thread: #{thread.id}")
63
- thread_info << ", Fiber: #{thread.fiber_id}" unless thread.id == thread.fiber_id
64
- thread_info << " (#{"%4.2f%%" % ((@overall_time/@overall_threads_time)*100)} ~ #{@overall_time})"
65
- @output.print "<div class=\"thread\">#{thread_info}</div>"
66
- @output.print "<ul name=\"thread\">"
67
- thread.methods.each do |m|
68
- # $stderr.print m.dump
69
- next unless m.root?
70
- m.callers.each do |call_info|
71
- visited = Set.new
72
- print_stack(visited, call_info, thread.total_time)
73
- end
74
- end
75
- @output.print "</ul>"
76
- end
48
+ output << @erb.result(binding)
49
+ a = 1
50
+ end
77
51
 
78
- print_footer
52
+ # :enddoc:
53
+ def setup_options(options)
54
+ super(options)
55
+ @erb = ERB.new(self.template)
79
56
  end
80
57
 
81
- def print_stack(visited, call_info, parent_time)
58
+ def print_stack(output, visited, call_info, parent_time)
82
59
  total_time = call_info.total_time
83
60
  percent_parent = (total_time/parent_time)*100
84
61
  percent_total = (total_time/@overall_time)*100
@@ -88,35 +65,36 @@ class CallStackPrinter < AbstractPrinter
88
65
  visible = percent_total >= threshold
89
66
  expanded = percent_total >= expansion
90
67
  display = visible ? "block" : "none"
91
- @output.print "<li class=\"color#{color}\" style=\"display:#{display}\">"
92
68
 
93
- if kids.empty?
94
- @output.print "<a href=\"#\" class=\"toggle empty\" ></a>"
95
- else
96
- visible_children = kids.any?{|ci| (ci.total_time/@overall_time)*100 >= threshold}
97
- image = visible_children ? (expanded ? "minus" : "plus") : "empty"
98
- @output.print "<a href=\"#\" class=\"toggle #{image}\" ></a>"
99
- end
100
- @output.printf "<span> %4.2f%% (%4.2f%%) %s %s</span>\n", percent_total, percent_parent, link(call_info), graph_link(call_info)
69
+ output << "<li class=\"color#{color}\" style=\"display:#{display}\">" << "\n"
101
70
 
102
71
  if visited.include?(call_info)
103
- return
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"
104
74
  else
105
75
  visited << call_info
106
- end
107
76
 
108
- unless kids.empty?
109
- if expanded
110
- @output.print "<ul>"
77
+ if kids.empty?
78
+ output << "<a href=\"#\" class=\"toggle empty\" ></a>" << "\n"
111
79
  else
112
- @output.print '<ul style="display:none">'
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"
113
83
  end
114
- kids.sort_by{|c| -c.total_time}.each do |child_call_info|
115
- print_stack(visited, child_call_info, total_time)
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"
116
93
  end
117
- @output.print "</ul>"
94
+
95
+ visited.delete(call_info)
118
96
  end
119
- @output.print "</li>"
97
+ output << '</li>' << "\n"
120
98
  end
121
99
 
122
100
  def name(call_info)
@@ -124,25 +102,25 @@ class CallStackPrinter < AbstractPrinter
124
102
  method.full_name
125
103
  end
126
104
 
127
- def link(call_info)
128
- method = call_info.target
105
+ def link(method, recursive)
106
+ method_name = "#{recursive ? '*' : ''}#{method.full_name}"
129
107
  if method.source_file.nil?
130
- h(name(call_info))
108
+ h method_name
131
109
  else
132
110
  file = File.expand_path(method.source_file)
133
- "<a href=\"file://#{file}##{method.line}\">#{h(name(call_info))}</a>"
111
+ "<a href=\"file://#{file}##{method.line}\">#{h method_name}</a>"
134
112
  end
135
113
  end
136
114
 
137
115
  def graph_link(call_info)
138
- total_calls = call_info.target.callers.inject(0){|t, ci| t += ci.called}
139
- href = "#{@graph_html}##{method_href(call_info.target)}"
140
- totals = @graph_html ? "<a href='#{href}'>#{total_calls}</a>" : total_calls.to_s
116
+ total_calls = call_info.target.called
117
+ href = "#{method_href(call_info.target)}"
118
+ totals = total_calls.to_s
141
119
  "[#{call_info.called} calls, #{totals} total]"
142
120
  end
143
121
 
144
122
  def method_href(method)
145
- h(method.full_name.gsub(/[><#\.\?=:]/,"_") + "_" + @current_thread_id.to_s)
123
+ h(method.full_name.gsub(/[><#\.\?=:]/,"_"))
146
124
  end
147
125
 
148
126
  def total_time(call_infos)
@@ -190,80 +168,15 @@ class CallStackPrinter < AbstractPrinter
190
168
  @options[:expansion] || 10.0
191
169
  end
192
170
 
193
- def print_header
194
- @output.puts "<html><head>"
195
- @output.puts '<meta http-equiv="content-type" content="text/html; charset=utf-8">'
196
- @output.puts "<title>#{h title}</title>"
197
- print_css
198
- print_java_script
199
- @output.puts '</head><body><div style="display: inline-block;">'
200
- print_title_bar
201
- print_commands
202
- print_help
203
- end
204
-
205
- def print_footer
206
- @output.puts '<div id="sentinel"></div></div></body></html>'
207
- end
208
-
209
- def open_asset(file)
210
- path = File.join(File.expand_path('../../assets', __FILE__), file)
211
- File.open(path, 'rb').read
212
- end
213
-
214
- def print_css
215
- html = open_asset('call_stack_printer.css.html')
216
- @output.puts html.gsub('%s', base64_image)
217
- end
218
-
219
171
  def base64_image
220
- file = open_asset('call_stack_printer.png')
221
- Base64.encode64(file).gsub(/\n/, '')
222
- end
223
-
224
- def print_java_script
225
- html = open_asset('call_stack_printer.js.html')
226
- @output.puts html
227
- end
228
-
229
- def print_title_bar
230
- @output.puts <<-"end_title_bar"
231
- <div id="titlebar">
232
- Call tree for application <b>#{h application} #{h arguments}</b><br/>
233
- Generated on #{Time.now} with options #{h @options.inspect}<br/>
234
- </div>
235
- end_title_bar
236
- end
237
-
238
- def print_commands
239
- @output.puts <<-"end_commands"
240
- <div id=\"commands\">
241
- <span style=\"font-size: 11pt; font-weight: bold;\">Threshold:</span>
242
- <input value=\"#{h threshold}\" size=\"3\" id=\"threshold\" type=\"text\">
243
- <input value=\"Apply\" onclick=\"setThreshold();\" type=\"submit\">
244
- <input value=\"Expand All\" onclick=\"expandAll(event);\" type=\"submit\">
245
- <input value=\"Collapse All\" onclick=\"collapseAll(event);\" type=\"submit\">
246
- <input value=\"Show Help\" onclick=\"toggleHelp(this);\" type=\"submit\">
247
- </div>
248
- end_commands
172
+ @data ||= begin
173
+ file = open_asset('call_stack_printer.png')
174
+ Base64.encode64(file).gsub(/\n/, '')
175
+ end
249
176
  end
250
177
 
251
- def print_help
252
- @output.puts <<-'end_help'
253
- <div style="display: none;" id="help">
254
- &#8226; Enter a decimal value <i>d</i> into the threshold field and click "Apply"
255
- to hide all nodes marked with time values lower than <i>d</i>.<br>
256
- &#8226; Click on "Expand All" for full tree expansion.<br>
257
- &#8226; Click on "Collapse All" to show only top level nodes.<br>
258
- &#8226; Use a, s, d, w as in Quake or Urban Terror to navigate the tree.<br>
259
- &#8226; Use f and b to navigate the tree in preorder forward and backwards.<br>
260
- &#8226; Use x to toggle visibility of a subtree.<br>
261
- &#8226; Use * to expand/collapse a whole subtree.<br>
262
- &#8226; Use h to navigate to thread root.<br>
263
- &#8226; Use n and p to navigate between threads.<br>
264
- &#8226; Click on background to move focus to a subtree.<br>
265
- </div>
266
- end_help
178
+ def template
179
+ open_asset('call_stack_printer.html.erb')
267
180
  end
268
181
  end
269
182
  end
@@ -57,8 +57,7 @@ module RubyProf
57
57
  end
58
58
 
59
59
  def template
60
- path = File.expand_path(File.join(__FILE__, '..', '..', 'assets', 'graph_printer.html.erb'))
61
- File.read(path, :mode => 'rb')
60
+ open_asset('graph_printer.html.erb')
62
61
  end
63
62
  end
64
63
  end
@@ -1,3 +1,3 @@
1
1
  module RubyProf
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require File.expand_path('../test_helper', __FILE__)
5
+ require 'fiber'
6
+ require 'timeout'
7
+ require 'set'
8
+
9
+ # -- Tests ----
10
+ class FiberTest < TestCase
11
+ def fiber_test
12
+ @fiber_ids << Fiber.current.object_id
13
+ enum = Enumerator.new do |yielder|
14
+ [1,2].each do |x|
15
+ @fiber_ids << Fiber.current.object_id
16
+ sleep 0.1
17
+ yielder.yield x
18
+ end
19
+ end
20
+ while true
21
+ begin
22
+ enum.next
23
+ rescue StopIteration
24
+ break
25
+ end
26
+ end
27
+ sleep 0.1
28
+ end
29
+
30
+ def setup
31
+ # Need to use wall time for this test due to the sleep calls
32
+ RubyProf::measure_mode = RubyProf::WALL_TIME
33
+ @fiber_ids = Set.new
34
+ @root_fiber = Fiber.current.object_id
35
+ @thread_id = Thread.current.object_id
36
+ end
37
+
38
+ def test_fibers
39
+ result = RubyProf.profile { fiber_test }
40
+ profiled_fiber_ids = result.threads.map(&:fiber_id)
41
+ assert_equal(2, result.threads.length)
42
+ assert_equal([@thread_id], result.threads.map(&:id).uniq)
43
+ assert_equal(@fiber_ids, Set.new(profiled_fiber_ids))
44
+
45
+ assert profiled_fiber_ids.include?(@root_fiber)
46
+ assert(root_fiber_profile = result.threads.detect{|t| t.fiber_id == @root_fiber})
47
+ assert(enum_fiber_profile = result.threads.detect{|t| t.fiber_id != @root_fiber})
48
+
49
+ assert_in_delta(0.3, root_fiber_profile.total_time, 0.05)
50
+ assert_in_delta(0.2, enum_fiber_profile.total_time, 0.05)
51
+
52
+ assert(method_next = root_fiber_profile.methods.detect{|m| m.full_name == "Enumerator#next"})
53
+ assert(method_each = enum_fiber_profile.methods.detect{|m| m.full_name == "Enumerator#each"})
54
+
55
+ assert_in_delta(0.2, method_next.total_time, 0.05)
56
+ assert_in_delta(0.2, method_each.total_time, 0.05)
57
+ end
58
+
59
+ def test_merged_fibers
60
+ result = RubyProf.profile(merge_fibers: true) { fiber_test }
61
+ assert_equal(1, result.threads.length)
62
+
63
+ thread = result.threads.first
64
+ assert_equal(thread.id, thread.fiber_id)
65
+ assert_in_delta(0.3, thread.total_time, 0.05)
66
+
67
+ assert(method_next = thread.methods.detect{|m| m.full_name == "Enumerator#next"})
68
+ assert(method_each = thread.methods.detect{|m| m.full_name == "Enumerator#each"})
69
+
70
+ assert_in_delta(0.2, method_next.total_time, 0.05)
71
+ assert_in_delta(0.2, method_each.total_time, 0.05)
72
+ end
73
+ end
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require File.expand_path('../test_helper', __FILE__)
5
+
6
+ class GcTest < TestCase
7
+ def some_method
8
+ Array.new(3 * 4)
9
+ end
10
+
11
+ def run_profile
12
+ RubyProf.profile do
13
+ self.some_method
14
+ end
15
+ end
16
+
17
+ def test_hold_onto_thread
18
+ threads = 1000.times.reduce(Array.new) do |array, i|
19
+ array.concat(run_profile.threads)
20
+ GC.start
21
+ array
22
+ end
23
+
24
+ threads.each do |thread|
25
+ error = assert_raises(RuntimeError) do
26
+ thread.id
27
+ end
28
+ assert_match(/has already been freed/, error.message)
29
+ end
30
+ assert(true)
31
+ end
32
+
33
+ def test_hold_onto_method
34
+ methods = 1000.times.reduce(Array.new) do |array, i|
35
+ array.concat(run_profile.threads.map(&:methods).flatten)
36
+ GC.start
37
+ array
38
+ end
39
+
40
+ methods.each do |method|
41
+ error = assert_raises(RuntimeError) do
42
+ method.method_name
43
+ end
44
+ assert_match(/has already been freed/, error.message)
45
+ end
46
+ assert(true)
47
+ end
48
+
49
+ def test_hold_onto_parent_callers
50
+ call_infos = 1000.times.reduce(Array.new) do |array, i|
51
+ array.concat(run_profile.threads.map(&:methods).flatten.map(&:callers).flatten)
52
+ GC.start
53
+ array
54
+ end
55
+
56
+ call_infos.each do |call_info|
57
+ error = assert_raises(RuntimeError) do
58
+ call_info.source_file
59
+ end
60
+ assert_match(/has already been freed/, error.message)
61
+ end
62
+ assert(true)
63
+ end
64
+
65
+ def test_hold_onto_parent_callees
66
+ call_infos = 1000.times.reduce(Array.new) do |array, i|
67
+ array.concat(run_profile.threads.map(&:methods).flatten.map(&:callees).flatten)
68
+ GC.start
69
+ array
70
+ end
71
+
72
+ call_infos.each do |call_info|
73
+ error = assert_raises(RuntimeError) do
74
+ call_info.source_file
75
+ end
76
+ assert_match(/has already been freed/, error.message)
77
+ end
78
+ assert(true)
79
+ end
80
+
81
+ def test_hold_onto_measurements
82
+ measurements = 1000.times.reduce(Array.new) do |array, i|
83
+ array.concat(run_profile.threads.map(&:methods).flatten.map(&:callers).flatten.map(&:measurement))
84
+ GC.start
85
+ array
86
+ end
87
+
88
+ measurements.each do |measurement|
89
+ error = assert_raises(RuntimeError) do
90
+ measurement.total_time
91
+ end
92
+ assert_match(/has already been freed/, error.message)
93
+ end
94
+ assert(true)
95
+ end
96
+ end
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require File.expand_path('../test_helper', __FILE__)
5
+ require 'stringio'
6
+ require 'fileutils'
7
+ require 'tmpdir'
8
+ require_relative 'prime'
9
+
10
+ # -- Tests ----
11
+ class PrinterCallStackTest < TestCase
12
+ def setup
13
+ # WALL_TIME so we can use sleep in our test and get same measurements on linux and windows
14
+ RubyProf::measure_mode = RubyProf::WALL_TIME
15
+ @result = RubyProf.profile do
16
+ run_primes(1000, 5000)
17
+ end
18
+ end
19
+
20
+ def test_graph_html_string
21
+ output = ''
22
+ printer = RubyProf::CallStackPrinter.new(@result)
23
+ printer.print(output)
24
+
25
+ assert_match(/<!DOCTYPE html>/i, output)
26
+ assert_match(/Object#run_primes/i, output)
27
+ end
28
+ end