tuttle 0.0.5 → 0.0.6
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/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
|
-
|