ruby-prof 1.0.0 → 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.
@@ -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