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.
- checksums.yaml +4 -4
- data/CHANGES +9 -0
- data/ext/ruby_prof/rp_allocation.c +5 -18
- data/ext/ruby_prof/rp_call_info.c +7 -19
- data/ext/ruby_prof/rp_measure_process_time.c +10 -6
- data/ext/ruby_prof/rp_measurement.c +12 -18
- data/ext/ruby_prof/rp_measurement.h +1 -0
- data/ext/ruby_prof/rp_method.c +8 -20
- data/ext/ruby_prof/rp_profile.c +18 -4
- data/ext/ruby_prof/rp_profile.h +1 -0
- data/ext/ruby_prof/rp_thread.c +12 -13
- data/ext/ruby_prof/vc/ruby_prof.vcxproj +2 -2
- data/lib/ruby-prof.rb +0 -1
- data/lib/ruby-prof/assets/call_stack_printer.html.erb +713 -0
- data/lib/ruby-prof/assets/call_stack_printer.png +0 -0
- data/lib/ruby-prof/printers/abstract_printer.rb +9 -0
- data/lib/ruby-prof/printers/call_stack_printer.rb +43 -130
- data/lib/ruby-prof/printers/graph_html_printer.rb +1 -2
- data/lib/ruby-prof/version.rb +1 -1
- data/test/fiber_test.rb +73 -0
- data/test/gc_test.rb +96 -0
- data/test/printer_call_stack_test.rb +28 -0
- data/test/printer_graph_html_test.rb +1 -1
- metadata +7 -5
- data/lib/ruby-prof/assets/call_stack_printer.css.html +0 -117
- data/lib/ruby-prof/assets/call_stack_printer.js.html +0 -385
Binary file
|
@@ -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
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
109
|
-
|
110
|
-
@output.print "<ul>"
|
77
|
+
if kids.empty?
|
78
|
+
output << "<a href=\"#\" class=\"toggle empty\" ></a>" << "\n"
|
111
79
|
else
|
112
|
-
|
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
|
-
|
115
|
-
|
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
|
-
|
94
|
+
|
95
|
+
visited.delete(call_info)
|
118
96
|
end
|
119
|
-
|
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(
|
128
|
-
|
105
|
+
def link(method, recursive)
|
106
|
+
method_name = "#{recursive ? '*' : ''}#{method.full_name}"
|
129
107
|
if method.source_file.nil?
|
130
|
-
h
|
108
|
+
h method_name
|
131
109
|
else
|
132
110
|
file = File.expand_path(method.source_file)
|
133
|
-
"<a href=\"file://#{file}##{method.line}\">#{h
|
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.
|
139
|
-
href = "#{
|
140
|
-
totals =
|
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(/[><#\.\?=:]/,"_")
|
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
|
-
|
221
|
-
|
222
|
-
|
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
|
252
|
-
|
253
|
-
<div style="display: none;" id="help">
|
254
|
-
• 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
|
-
• Click on "Expand All" for full tree expansion.<br>
|
257
|
-
• Click on "Collapse All" to show only top level nodes.<br>
|
258
|
-
• Use a, s, d, w as in Quake or Urban Terror to navigate the tree.<br>
|
259
|
-
• Use f and b to navigate the tree in preorder forward and backwards.<br>
|
260
|
-
• Use x to toggle visibility of a subtree.<br>
|
261
|
-
• Use * to expand/collapse a whole subtree.<br>
|
262
|
-
• Use h to navigate to thread root.<br>
|
263
|
-
• Use n and p to navigate between threads.<br>
|
264
|
-
• 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
|
data/lib/ruby-prof/version.rb
CHANGED
data/test/fiber_test.rb
ADDED
@@ -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
|
data/test/gc_test.rb
ADDED
@@ -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
|