tuttle 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +61 -4
- data/Rakefile +1 -16
- data/app/controllers/tuttle/active_job_controller.rb +51 -0
- data/app/controllers/tuttle/active_model_serializers_controller.rb +3 -8
- data/app/controllers/tuttle/application_controller.rb +3 -0
- data/app/controllers/tuttle/cancancan_controller.rb +8 -15
- data/app/controllers/tuttle/devise_controller.rb +1 -2
- data/app/controllers/tuttle/gems_controller.rb +4 -4
- data/app/controllers/tuttle/home_controller.rb +0 -1
- data/app/controllers/tuttle/paperclip_controller.rb +1 -2
- data/app/controllers/tuttle/rack_mini_profiler_controller.rb +15 -0
- data/app/controllers/tuttle/rails_controller.rb +45 -7
- data/app/controllers/tuttle/request_controller.rb +0 -4
- data/app/controllers/tuttle/ruby_controller.rb +6 -2
- data/app/helpers/tuttle/application_helper.rb +13 -8
- data/app/models/tuttle/configuration_registry.rb +25 -0
- data/app/models/tuttle/version_detector.rb +9 -0
- data/app/views/layouts/tuttle/application.html.erb +29 -20
- data/app/views/tuttle/active_job/index.html.erb +48 -0
- data/app/views/tuttle/active_model_serializers/index.html.erb +2 -0
- data/app/views/tuttle/gems/get_process_mem.html.erb +13 -13
- data/app/views/tuttle/gems/http_clients.html.erb +9 -10
- data/app/views/tuttle/gems/json.html.erb +3 -3
- data/app/views/tuttle/gems/other.html.erb +41 -11
- data/app/views/tuttle/home/index.html.erb +27 -23
- data/app/views/tuttle/{performance_tuning → rack_mini_profiler}/index.html.erb +6 -6
- data/app/views/tuttle/rails/_cache_dalli_store.html.erb +65 -0
- data/app/views/tuttle/rails/_cache_memory_store.html.erb +43 -0
- data/app/views/tuttle/rails/_cache_monitor.html.erb +63 -0
- data/app/views/tuttle/rails/assets.html.erb +181 -28
- data/app/views/tuttle/rails/cache.html.erb +92 -102
- data/app/views/tuttle/rails/database.html.erb +2 -2
- data/app/views/tuttle/rails/index.html.erb +20 -21
- data/app/views/tuttle/rails/routes.html.erb +88 -36
- data/app/views/tuttle/rails/schema_cache.html.erb +2 -1
- data/app/views/tuttle/ruby/index.html.erb +24 -20
- data/config/rails_config_base.yml +80 -0
- data/config/rails_config_v4.x.yml +14 -0
- data/config/rails_config_v5.x.yml +12 -0
- data/config/routes.rb +7 -1
- data/lib/tuttle.rb +3 -6
- data/lib/tuttle/engine.rb +24 -12
- data/lib/tuttle/instrumenter.rb +7 -7
- data/lib/tuttle/middleware/request_profiler.rb +165 -24
- data/lib/tuttle/presenters/action_dispatch/routing/route_wrapper.rb +7 -3
- data/lib/tuttle/presenters/rack_mini_profiler/client_settings.rb +27 -0
- data/lib/tuttle/ruby_prof/fast_call_stack_printer.rb +26 -53
- data/lib/tuttle/version.rb +1 -1
- metadata +18 -8
- data/app/controllers/tuttle/performance_tuning_controller.rb +0 -14
data/lib/tuttle/engine.rb
CHANGED
@@ -1,17 +1,18 @@
|
|
1
|
+
require 'tuttle'
|
1
2
|
require 'rails/engine'
|
2
3
|
|
3
4
|
module Tuttle
|
4
5
|
class Engine < ::Rails::Engine
|
5
6
|
isolate_namespace Tuttle
|
6
7
|
|
7
|
-
|
8
|
+
attr_accessor :reload_needed, :session_start, :session_id
|
8
9
|
|
9
|
-
|
10
|
+
attr_reader :logger
|
10
11
|
|
11
12
|
config.tuttle = ActiveSupport::OrderedOptions.new
|
12
13
|
|
13
14
|
config.before_configuration do
|
14
|
-
Tuttle::Engine.session_start = Time.
|
15
|
+
Tuttle::Engine.session_start = Time.current
|
15
16
|
Tuttle::Engine.session_id = SecureRandom.uuid
|
16
17
|
end
|
17
18
|
|
@@ -25,27 +26,38 @@ module Tuttle
|
|
25
26
|
|
26
27
|
next unless Tuttle.enabled
|
27
28
|
|
28
|
-
|
29
|
+
@logger = ::Logger.new("#{Rails.root}/log/tuttle.log")
|
29
30
|
Tuttle::Engine.logger.info('Tuttle engine started')
|
30
31
|
|
31
|
-
Tuttle.automount_engine = true if Tuttle.automount_engine
|
32
|
+
Tuttle.automount_engine = true if Tuttle.automount_engine.nil?
|
32
33
|
|
33
|
-
if Tuttle.automount_engine
|
34
|
-
|
35
|
-
Tuttle::Engine.logger.info('Auto-mounting /tuttle routes')
|
36
|
-
mount Tuttle::Engine, at: "tuttle"
|
37
|
-
end
|
38
|
-
end
|
34
|
+
mount_engine! if Tuttle.automount_engine
|
35
|
+
use_profiling_middleware! if Tuttle.enable_profiling
|
39
36
|
|
40
37
|
if Tuttle.track_notifications
|
41
38
|
Tuttle::Instrumenter.initialize_tuttle_instrumenter
|
42
39
|
end
|
43
|
-
|
44
40
|
end
|
45
41
|
|
46
42
|
config.to_prepare do
|
47
43
|
Tuttle::Engine.reload_needed = true
|
48
44
|
end
|
49
45
|
|
46
|
+
private
|
47
|
+
|
48
|
+
def mount_engine!
|
49
|
+
Rails.application.routes.prepend do
|
50
|
+
Tuttle::Engine.logger.info('Auto-mounting /tuttle routes')
|
51
|
+
mount Tuttle::Engine, at: 'tuttle'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def use_profiling_middleware!
|
56
|
+
# Add memory/cpu profiler middleware at the end of the stack
|
57
|
+
Tuttle::Engine.logger.info('Using Tuttle::Middleware::RequestProfiler middleware')
|
58
|
+
require 'tuttle/middleware/request_profiler'
|
59
|
+
Rails.application.config.middleware.use Tuttle::Middleware::RequestProfiler
|
60
|
+
end
|
61
|
+
|
50
62
|
end
|
51
63
|
end
|
data/lib/tuttle/instrumenter.rb
CHANGED
@@ -2,9 +2,9 @@ module Tuttle
|
|
2
2
|
class Instrumenter
|
3
3
|
|
4
4
|
mattr_accessor :events, :event_counts, :cache_events
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
self.events = []
|
6
|
+
self.event_counts = Hash.new(0)
|
7
|
+
self.cache_events = []
|
8
8
|
|
9
9
|
def self.initialize_tuttle_instrumenter
|
10
10
|
# For now, only instrument non-production mode
|
@@ -16,7 +16,8 @@ module Tuttle
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
# Note: For Rails < 4.2 instrumentation is not enabled by default.
|
19
|
+
# Note: For Rails < 4.2 instrumentation is not enabled by default.
|
20
|
+
# Hitting the cache inspector page will enable it for that session.
|
20
21
|
Tuttle::Engine.logger.info('Initializing cache_read subscriber')
|
21
22
|
ActiveSupport::Notifications.subscribe('cache_read.active_support') do |*args|
|
22
23
|
cache_call_location = caller_locations.detect { |cl| cl.path.start_with?("#{Rails.root}/app".freeze) }
|
@@ -24,14 +25,13 @@ module Tuttle
|
|
24
25
|
|
25
26
|
Tuttle::Engine.logger.info("Cache Read called: #{cache_call_location.path} on line #{cache_call_location.lineno} :: #{event.payload.inspect}")
|
26
27
|
|
27
|
-
event.payload.merge!(
|
28
|
+
event.payload.merge!(:call_location_path => cache_call_location.path, :call_location_lineno => cache_call_location.lineno)
|
28
29
|
Tuttle::Instrumenter.cache_events << event
|
29
30
|
end
|
30
31
|
|
31
|
-
ActiveSupport::Notifications.subscribe('cache_generate.active_support') do
|
32
|
+
ActiveSupport::Notifications.subscribe('cache_generate.active_support') do
|
32
33
|
Tuttle::Engine.logger.info('Cache Generate called')
|
33
34
|
end
|
34
|
-
|
35
35
|
end
|
36
36
|
|
37
37
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Tuttle
|
4
4
|
module Middleware
|
@@ -11,17 +11,18 @@ module Tuttle
|
|
11
11
|
def call(env)
|
12
12
|
query_string = env['QUERY_STRING']
|
13
13
|
|
14
|
-
tuttle_profiler_action = /tuttle\-profiler=([\w\-]*)/.match(query_string) {
|
14
|
+
tuttle_profiler_action = /(^|[&?])tuttle\-profiler=([\w\-]*)/.match(query_string) { |m| m[2] }
|
15
15
|
|
16
16
|
case tuttle_profiler_action
|
17
|
-
when
|
17
|
+
when 'memory_profiler', 'memory'
|
18
18
|
profile_memory(env, query_string)
|
19
|
-
when
|
19
|
+
when 'ruby-prof', 'cpu'
|
20
20
|
profile_cpu(env, query_string)
|
21
|
+
when 'busted'
|
22
|
+
profile_busted(env, query_string)
|
21
23
|
else
|
22
24
|
@app.call(env)
|
23
25
|
end
|
24
|
-
|
25
26
|
end
|
26
27
|
|
27
28
|
private
|
@@ -31,24 +32,42 @@ module Tuttle
|
|
31
32
|
|
32
33
|
query_params = Rack::Utils.parse_nested_query(query_string)
|
33
34
|
options = {
|
34
|
-
|
35
|
-
|
35
|
+
:ignore_files => query_params['memory_profiler_ignore_files'],
|
36
|
+
:allow_files => query_params['memory_profiler_allow_files']
|
36
37
|
}
|
37
|
-
options[:top]= Integer(query_params['memory_profiler_top']) if query_params.key?('memory_profiler_top')
|
38
|
+
options[:top] = Integer(query_params['memory_profiler_top']) if query_params.key?('memory_profiler_top')
|
39
|
+
|
40
|
+
status = nil
|
41
|
+
body = nil
|
38
42
|
|
43
|
+
t0 = Time.current
|
39
44
|
report = MemoryProfiler.report(options) do
|
40
|
-
|
41
|
-
body.close if body.respond_to?
|
45
|
+
status, _headers, body = @app.call(env)
|
46
|
+
body.close if body.respond_to?(:close)
|
42
47
|
end
|
48
|
+
response_time = Time.current - t0
|
43
49
|
|
44
50
|
result = StringIO.new
|
45
51
|
report.pretty_print(result)
|
46
52
|
|
47
|
-
|
53
|
+
response = ["Report from Tuttle::Middeware::RequestProfiler\n"]
|
54
|
+
response << "Time of request: #{Time.current}\n"
|
55
|
+
response << "Response status: #{status}\n" unless status == 200
|
56
|
+
response << "Response time: #{response_time}\n"
|
57
|
+
response << "Response body size: #{body.body.length}\n" if body.respond_to?(:body)
|
58
|
+
response << "\n"
|
59
|
+
response << result.string
|
60
|
+
[200, { 'Content-Type' => 'text/plain' }, response]
|
48
61
|
end
|
49
62
|
|
50
63
|
def profile_cpu(env, query_string)
|
51
64
|
require 'ruby-prof'
|
65
|
+
require 'tuttle/ruby_prof/fast_call_stack_printer'
|
66
|
+
|
67
|
+
query_params = Rack::Utils.parse_nested_query(query_string)
|
68
|
+
options = {}
|
69
|
+
options[:threshold] = Float(query_params['ruby-prof_threshold']) if query_params.key?('ruby-prof_threshold')
|
70
|
+
rubyprof_printer = /ruby\-prof_printer=([\w]*)/.match(query_string) { |m| m[1] }
|
52
71
|
|
53
72
|
data = ::RubyProf::Profile.profile do
|
54
73
|
_, _, body = @app.call(env)
|
@@ -56,25 +75,147 @@ module Tuttle
|
|
56
75
|
end
|
57
76
|
|
58
77
|
result = StringIO.new
|
59
|
-
rubyprof_printer = /ruby\-prof_printer=([\w]*)/.match(query_string) { $1.to_sym }
|
60
78
|
content_type = 'text/html'
|
61
79
|
|
62
|
-
case rubyprof_printer
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
80
|
+
profiler = case rubyprof_printer
|
81
|
+
when 'flat'
|
82
|
+
content_type = 'text/plain'
|
83
|
+
::RubyProf::FlatPrinter
|
84
|
+
when 'graph'
|
85
|
+
::RubyProf::GraphHtmlPrinter
|
86
|
+
when 'stack', 'call_stack'
|
87
|
+
::RubyProf::CallStackPrinter
|
88
|
+
else
|
89
|
+
options[:application] = env['REQUEST_URI']
|
90
|
+
::Tuttle::RubyProf::FastCallStackPrinter
|
91
|
+
end
|
92
|
+
|
93
|
+
profiler.new(data).print(result, options)
|
74
94
|
|
75
95
|
[200, { 'Content-Type' => content_type }, [result.string]]
|
76
96
|
end
|
77
97
|
|
98
|
+
# These methods *may* cause the method cache to be invalidated
|
99
|
+
TRACE_METHODS = Set.new([:extend, :include, :const_set, :remove_const, :alias_method, :remove_method,
|
100
|
+
:prepend, :append_features, :prepend_features,
|
101
|
+
:public_constant, :private_constant, :autoload,
|
102
|
+
:define_method, :define_singleton_method])
|
103
|
+
|
104
|
+
def profile_busted(env, _query_string)
|
105
|
+
# Note: Requires Busted (of course) and DTrace so will need much better error handling and information
|
106
|
+
# For DTrace on OS X (post 10.11) you may need to disable SIP as well as be running with root privileges
|
107
|
+
# https://derflounder.wordpress.com/2015/10/01/system-integrity-protection-adding-another-layer-to-apples-security-model/
|
108
|
+
|
109
|
+
require 'busted'
|
110
|
+
require 'busted/profiler/default' # Required so `autoload :Default` does not get included in trace
|
111
|
+
|
112
|
+
# Notes:
|
113
|
+
# Much of this is per https://tenderlovemaking.com/2015/12/23/inline-caching-in-mri.html
|
114
|
+
# and https://github.com/charliesome/charlie.bz/blob/master/posts/things-that-clear-rubys-method-cache.md
|
115
|
+
# RubyVM.stat tracks the :global_method_state, :global_constant_state, and :class_serial
|
116
|
+
# :global_method_state - "global serial number that increments every time certain classes get mutated"
|
117
|
+
# ** Changes to this are BAD because they invalidate all caches
|
118
|
+
# :global_constant_state - counter of constants defined *or redefined(?)*
|
119
|
+
# :class_serial - global serial number that increments every time a method is Class is created/modified (?-verify)
|
120
|
+
# Since Ruby 2.1(2.2?) the method caches are per-Class rather than global.
|
121
|
+
# So clearing the method cache of a single Class is less of a performance hit than blowing away the entire method cache
|
122
|
+
#
|
123
|
+
# Busted Dtraces :method-cache-clear internal events which are when Ruby says the method cache was cleared
|
124
|
+
# The @cache_buster_tracepoint Dtraces Class/Module definitions (:class)
|
125
|
+
# and calls to C method (:c_call) which would likely cause a method cache clear
|
126
|
+
#
|
127
|
+
# From the observed results...
|
128
|
+
# :method-cache-clear may fire more times than RubyVM.stat[]
|
129
|
+
cache_busters = []
|
130
|
+
|
131
|
+
# This is really still incomplete...
|
132
|
+
# Internally, Ruby does not fire :c_call or :class for many events that would increment the :class_serial
|
133
|
+
# For example `Person = Class.new` does not fire a :class event
|
134
|
+
# And it also does create a new constant (:constant_state) but does not fire a :const_set event
|
135
|
+
|
136
|
+
# Trace class definitions (which always(?) invalidate the cache) and c-calls which may invalidate the cache
|
137
|
+
@cache_buster_tracepoint ||= TracePoint.new(:class, :c_call) do |trace|
|
138
|
+
if trace.event == :class
|
139
|
+
cache_busters << {
|
140
|
+
:event => trace.event,
|
141
|
+
:event_description => "Class definition",
|
142
|
+
:location => "#{trace.path}##{trace.lineno}",
|
143
|
+
:target_class => trace.self
|
144
|
+
}
|
145
|
+
elsif TRACE_METHODS.include?(trace.method_id)
|
146
|
+
cache_busters << {
|
147
|
+
:event => trace.event,
|
148
|
+
:event_description => "#{trace.defined_class}##{trace.method_id}",
|
149
|
+
:location => "#{trace.path}##{trace.lineno}",
|
150
|
+
:target_class => trace.self.class,
|
151
|
+
:defined_class => trace.defined_class,
|
152
|
+
:method_id => trace.method_id
|
153
|
+
}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Trace the request and capture the RubyVM.stat before/after
|
158
|
+
vmstat_before = RubyVM.stat
|
159
|
+
results = @cache_buster_tracepoint.enable do
|
160
|
+
Busted.run(trace: true) do
|
161
|
+
_, _, body = @app.call(env)
|
162
|
+
body.close if body.respond_to?(:close)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
vmstat_after = RubyVM.stat
|
166
|
+
|
167
|
+
# Prepare the output
|
168
|
+
output = "\nRubyVM.stat: Before After Change\n".dup
|
169
|
+
[:global_method_state, :global_constant_state, :class_serial].each do |stat|
|
170
|
+
output << format("%-22s %-10d %-10d %+d\n",
|
171
|
+
stat,
|
172
|
+
vmstat_before[stat],
|
173
|
+
vmstat_after[stat],
|
174
|
+
vmstat_after[stat] - vmstat_before[stat])
|
175
|
+
end
|
176
|
+
|
177
|
+
output << "\nCounts:\n"
|
178
|
+
output << "method-cache-clear: #{results[:traces][:method].size}\n"
|
179
|
+
output << "C calls that may cause cache clear: #{cache_busters.size}\n"
|
180
|
+
grouped_traces = cache_busters.group_by do |trace_info|
|
181
|
+
if trace_info[:event] == :c_call
|
182
|
+
"#{trace_info[:defined_class]}##{trace_info[:method_id]}"
|
183
|
+
else
|
184
|
+
"Class Defined"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
grouped_traces.each do |grouping, traces|
|
188
|
+
output << " #{grouping}: #{traces.size}\n"
|
189
|
+
end
|
190
|
+
|
191
|
+
output << "\nTraces (method-cache-clear): (#{results[:traces][:method].size} times)\n"
|
192
|
+
results[:traces][:method].each do |trace|
|
193
|
+
output << "#{trace[:class]} - #{trace[:sourcefile]}##{trace[:lineno]}\n"
|
194
|
+
end
|
195
|
+
|
196
|
+
output << "\nTraces (method cache clearing calls): (#{cache_busters.size} times)\n"
|
197
|
+
cache_busters.each do |trace_info|
|
198
|
+
if trace_info[:event] == :c_call
|
199
|
+
output << format("%s\#%s: %s %s\n",
|
200
|
+
trace_info[:defined_class],
|
201
|
+
trace_info[:method_id],
|
202
|
+
trace_info[:target_class],
|
203
|
+
trace_info[:location])
|
204
|
+
else
|
205
|
+
output << format("Class Definition: %s %s %s\n",
|
206
|
+
trace_info[:target_class],
|
207
|
+
trace_info[:defined_class],
|
208
|
+
trace_info[:location])
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
[200,
|
213
|
+
{ 'Content-Type' => 'text/plain' },
|
214
|
+
["Tuttle - Ruby Method/Constant Caches Request Observer v0.0.1\n",
|
215
|
+
"Ruby Version: #{RUBY_VERSION}\n",
|
216
|
+
output]]
|
217
|
+
end
|
218
|
+
|
78
219
|
end
|
79
220
|
end
|
80
221
|
end
|
@@ -5,7 +5,11 @@ module Tuttle
|
|
5
5
|
class RouteWrapper < ::ActionDispatch::Routing::RouteWrapper
|
6
6
|
|
7
7
|
def endpoint_or_app_name
|
8
|
-
uses_dispatcher?
|
8
|
+
if uses_dispatcher?
|
9
|
+
endpoint
|
10
|
+
else
|
11
|
+
rack_app.is_a?(Class) ? rack_app : rack_app.class
|
12
|
+
end
|
9
13
|
end
|
10
14
|
|
11
15
|
def controller
|
@@ -20,8 +24,8 @@ module Tuttle
|
|
20
24
|
rack_app.respond_to?(:dispatcher?)
|
21
25
|
end
|
22
26
|
|
23
|
-
def
|
24
|
-
|
27
|
+
def internal_to_rails?
|
28
|
+
true == internal?
|
25
29
|
end
|
26
30
|
|
27
31
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Tuttle
|
2
|
+
module Presenters
|
3
|
+
module RackMiniProfiler
|
4
|
+
class ClientSettings < SimpleDelegator
|
5
|
+
|
6
|
+
def initialize(env)
|
7
|
+
rmp_cs_args = [env]
|
8
|
+
rmp_cs_args += [::Rack::MiniProfiler.config.storage_instance, Time.current] if version_10?
|
9
|
+
super(::Rack::MiniProfiler::ClientSettings.new(*rmp_cs_args))
|
10
|
+
end
|
11
|
+
|
12
|
+
def version_10?
|
13
|
+
Rack::MiniProfiler::ClientSettings.instance_method(:initialize).arity > 1
|
14
|
+
end
|
15
|
+
|
16
|
+
def tuttle_check_cookie
|
17
|
+
version_10? ? has_valid_cookie? : has_cookie?
|
18
|
+
end
|
19
|
+
|
20
|
+
def tuttle_check_cookie_method
|
21
|
+
version_10? ? 'has_valid_cookie?' : 'has_cookie?'
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -11,30 +11,7 @@ module Tuttle
|
|
11
11
|
|
12
12
|
# Specify print options.
|
13
13
|
#
|
14
|
-
# options -
|
15
|
-
# :min_percent - Number 0 to 100 that specifes the minimum
|
16
|
-
# %self (the methods self time divided by the
|
17
|
-
# overall total time) that a method must take
|
18
|
-
# for it to be printed out in the report.
|
19
|
-
# Default value is 0.
|
20
|
-
#
|
21
|
-
# :print_file - True or false. Specifies if a method's source
|
22
|
-
# file should be printed. Default value if false.
|
23
|
-
#
|
24
|
-
# :threshold - a float from 0 to 100 that sets the threshold of
|
25
|
-
# results displayed.
|
26
|
-
# Default value is 1.0
|
27
|
-
#
|
28
|
-
# :title - a String to overide the default "ruby-prof call tree"
|
29
|
-
# title of the report.
|
30
|
-
#
|
31
|
-
# :expansion - a float from 0 to 100 that sets the threshold of
|
32
|
-
# results that are expanded, if the percent_total
|
33
|
-
# exceeds it.
|
34
|
-
# Default value is 10.0
|
35
|
-
#
|
36
|
-
# :application - a String to overide the name of the application,
|
37
|
-
# as it appears on the report.
|
14
|
+
# options - See CallStackPrinter for based options
|
38
15
|
#
|
39
16
|
def print(output = STDOUT, options = {})
|
40
17
|
@output = output
|
@@ -43,18 +20,19 @@ module Tuttle
|
|
43
20
|
print_header
|
44
21
|
|
45
22
|
@overall_threads_time = @result.threads.inject(0) do |val, thread|
|
46
|
-
val
|
23
|
+
val + thread.total_time
|
47
24
|
end
|
48
25
|
|
49
|
-
@method_full_name_cache =
|
26
|
+
@method_full_name_cache = {}.compare_by_identity
|
50
27
|
|
51
28
|
@result.threads.each do |thread|
|
52
29
|
@overall_time = thread.total_time
|
53
30
|
thread_info = "Thread: #{thread.id}"
|
54
31
|
thread_info << ", Fiber: #{thread.fiber_id}" unless thread.id == thread.fiber_id
|
55
|
-
thread_info <<
|
32
|
+
thread_info << format(' (%4.2f%% ~ %f)', (@overall_time / @overall_threads_time) * 100, @overall_time)
|
33
|
+
|
56
34
|
@output.print "<div class=\"thread\">#{thread_info}</div>"
|
57
|
-
@output.print
|
35
|
+
@output.print '<ul name="thread">'
|
58
36
|
thread.methods.each do |m|
|
59
37
|
# $stderr.print m.dump
|
60
38
|
next unless m.root?
|
@@ -63,38 +41,35 @@ module Tuttle
|
|
63
41
|
print_stack ci, @overall_time
|
64
42
|
end
|
65
43
|
end
|
66
|
-
@output.print
|
44
|
+
@output.print '</ul>'
|
67
45
|
end
|
68
46
|
|
69
47
|
@method_full_name_cache = nil
|
70
48
|
|
71
49
|
print_footer
|
72
|
-
|
73
50
|
end
|
74
51
|
|
75
52
|
def print_stack(call_info, parent_time)
|
76
53
|
total_time = call_info.total_time
|
77
|
-
percent_total = (total_time
|
54
|
+
percent_total = (total_time / @overall_time) * 100
|
78
55
|
return unless percent_total > min_percent
|
79
56
|
|
80
|
-
percent_parent = (total_time/parent_time)*100
|
57
|
+
percent_parent = (total_time / parent_time) * 100
|
81
58
|
if percent_total < threshold
|
82
|
-
@output.write
|
59
|
+
@output.write '<li style="display:none;">'.freeze
|
83
60
|
else
|
84
|
-
@output.write
|
61
|
+
@output.write '<li>'.freeze
|
85
62
|
end
|
86
63
|
|
87
64
|
expanded = percent_total >= expansion
|
88
65
|
kids = call_info.children
|
89
66
|
|
90
|
-
toggle_href = if kids.empty? || kids.none?{|ci| (ci.total_time
|
91
|
-
|
67
|
+
toggle_href = if kids.empty? || kids.none? {|ci| (ci.total_time / @overall_time) * 100 >= threshold}
|
68
|
+
'<a href="#" class="toggle empty"></a>'.freeze
|
69
|
+
elsif expanded
|
70
|
+
'<a href="#" class="toggle minus"></a>'.freeze
|
92
71
|
else
|
93
|
-
|
94
|
-
"<a href=\"#\" class=\"toggle minus\" ></a>".freeze
|
95
|
-
else
|
96
|
-
"<a href=\"#\" class=\"toggle plus\" ></a>".freeze
|
97
|
-
end
|
72
|
+
'<a href="#" class="toggle plus"></a>'.freeze
|
98
73
|
end
|
99
74
|
@output.write toggle_href
|
100
75
|
|
@@ -104,25 +79,25 @@ module Tuttle
|
|
104
79
|
method_full_name(method), call_info.called, method.called
|
105
80
|
unless kids.empty?
|
106
81
|
if expanded
|
107
|
-
@output.write
|
82
|
+
@output.write '<ul>'.freeze
|
108
83
|
else
|
109
84
|
@output.write '<ul style="display:none">'.freeze
|
110
85
|
end
|
111
86
|
kids.sort_by!(&:total_time).reverse_each do |callinfo|
|
112
87
|
print_stack callinfo, total_time
|
113
88
|
end
|
114
|
-
@output.write
|
89
|
+
@output.write '</ul>'.freeze
|
115
90
|
end
|
116
|
-
@output.write
|
117
|
-
end
|
118
|
-
|
119
|
-
def name(call_info)
|
120
|
-
method = call_info.target
|
121
|
-
method.full_name
|
91
|
+
@output.write '</li>'.freeze
|
122
92
|
end
|
123
93
|
|
124
94
|
def method_full_name(method)
|
125
|
-
@method_full_name_cache[method] ||=
|
95
|
+
@method_full_name_cache[method] ||= begin
|
96
|
+
# Use ruby-prof klass_name only for non-Classes or klasses that do not report a name
|
97
|
+
# This prevents klass.inspect from being used which prints complex names for ActiveRecord classes
|
98
|
+
klass_name = method.klass && method.klass.class == Class && method.klass.name || method.klass_name
|
99
|
+
h("#{klass_name}##{method.method_name}")
|
100
|
+
end
|
126
101
|
end
|
127
102
|
|
128
103
|
def threshold
|
@@ -141,7 +116,7 @@ module Tuttle
|
|
141
116
|
@output.puts <<-"end_title_bar"
|
142
117
|
<div id="titlebar">
|
143
118
|
Call tree for application <b>#{h application} #{h arguments}</b><br/>
|
144
|
-
Generated on #{Time.
|
119
|
+
Generated on #{Time.current} with options #{h @options.inspect}<br/>
|
145
120
|
</div>
|
146
121
|
end_title_bar
|
147
122
|
end
|
@@ -159,6 +134,4 @@ CSS_OVERRIDE
|
|
159
134
|
|
160
135
|
end
|
161
136
|
end
|
162
|
-
|
163
137
|
end
|
164
|
-
|