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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +61 -4
  4. data/Rakefile +1 -16
  5. data/app/controllers/tuttle/active_job_controller.rb +51 -0
  6. data/app/controllers/tuttle/active_model_serializers_controller.rb +3 -8
  7. data/app/controllers/tuttle/application_controller.rb +3 -0
  8. data/app/controllers/tuttle/cancancan_controller.rb +8 -15
  9. data/app/controllers/tuttle/devise_controller.rb +1 -2
  10. data/app/controllers/tuttle/gems_controller.rb +4 -4
  11. data/app/controllers/tuttle/home_controller.rb +0 -1
  12. data/app/controllers/tuttle/paperclip_controller.rb +1 -2
  13. data/app/controllers/tuttle/rack_mini_profiler_controller.rb +15 -0
  14. data/app/controllers/tuttle/rails_controller.rb +45 -7
  15. data/app/controllers/tuttle/request_controller.rb +0 -4
  16. data/app/controllers/tuttle/ruby_controller.rb +6 -2
  17. data/app/helpers/tuttle/application_helper.rb +13 -8
  18. data/app/models/tuttle/configuration_registry.rb +25 -0
  19. data/app/models/tuttle/version_detector.rb +9 -0
  20. data/app/views/layouts/tuttle/application.html.erb +29 -20
  21. data/app/views/tuttle/active_job/index.html.erb +48 -0
  22. data/app/views/tuttle/active_model_serializers/index.html.erb +2 -0
  23. data/app/views/tuttle/gems/get_process_mem.html.erb +13 -13
  24. data/app/views/tuttle/gems/http_clients.html.erb +9 -10
  25. data/app/views/tuttle/gems/json.html.erb +3 -3
  26. data/app/views/tuttle/gems/other.html.erb +41 -11
  27. data/app/views/tuttle/home/index.html.erb +27 -23
  28. data/app/views/tuttle/{performance_tuning → rack_mini_profiler}/index.html.erb +6 -6
  29. data/app/views/tuttle/rails/_cache_dalli_store.html.erb +65 -0
  30. data/app/views/tuttle/rails/_cache_memory_store.html.erb +43 -0
  31. data/app/views/tuttle/rails/_cache_monitor.html.erb +63 -0
  32. data/app/views/tuttle/rails/assets.html.erb +181 -28
  33. data/app/views/tuttle/rails/cache.html.erb +92 -102
  34. data/app/views/tuttle/rails/database.html.erb +2 -2
  35. data/app/views/tuttle/rails/index.html.erb +20 -21
  36. data/app/views/tuttle/rails/routes.html.erb +88 -36
  37. data/app/views/tuttle/rails/schema_cache.html.erb +2 -1
  38. data/app/views/tuttle/ruby/index.html.erb +24 -20
  39. data/config/rails_config_base.yml +80 -0
  40. data/config/rails_config_v4.x.yml +14 -0
  41. data/config/rails_config_v5.x.yml +12 -0
  42. data/config/routes.rb +7 -1
  43. data/lib/tuttle.rb +3 -6
  44. data/lib/tuttle/engine.rb +24 -12
  45. data/lib/tuttle/instrumenter.rb +7 -7
  46. data/lib/tuttle/middleware/request_profiler.rb +165 -24
  47. data/lib/tuttle/presenters/action_dispatch/routing/route_wrapper.rb +7 -3
  48. data/lib/tuttle/presenters/rack_mini_profiler/client_settings.rb +27 -0
  49. data/lib/tuttle/ruby_prof/fast_call_stack_printer.rb +26 -53
  50. data/lib/tuttle/version.rb +1 -1
  51. metadata +18 -8
  52. data/app/controllers/tuttle/performance_tuning_controller.rb +0 -14
@@ -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
- mattr_accessor :reload_needed, :session_start, :session_id
8
+ attr_accessor :reload_needed, :session_start, :session_id
8
9
 
9
- mattr_reader :logger
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.now
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
- @@logger = ::Logger.new("#{Rails.root}/log/tuttle.log")
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 == nil
32
+ Tuttle.automount_engine = true if Tuttle.automount_engine.nil?
32
33
 
33
- if Tuttle.automount_engine
34
- Rails.application.routes.prepend do
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
@@ -2,9 +2,9 @@ module Tuttle
2
2
  class Instrumenter
3
3
 
4
4
  mattr_accessor :events, :event_counts, :cache_events
5
- @@events = []
6
- @@event_counts = Hash.new(0)
7
- @@cache_events = []
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. Hitting the cache inspector page will enable it for that session.
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!({:call_location_path => cache_call_location.path, :call_location_lineno => cache_call_location.lineno })
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 |*args|
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
- # frozen-string-literal: true
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) { $1.to_sym }
14
+ tuttle_profiler_action = /(^|[&?])tuttle\-profiler=([\w\-]*)/.match(query_string) { |m| m[2] }
15
15
 
16
16
  case tuttle_profiler_action
17
- when :'memory_profiler', :'memory'
17
+ when 'memory_profiler', 'memory'
18
18
  profile_memory(env, query_string)
19
- when :'ruby-prof', :'cpu'
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
- :ignore_files => query_params['memory_profiler_ignore_files'],
35
- :allow_files => query_params['memory_profiler_allow_files'],
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
- _,_,body = @app.call(env)
41
- body.close if body.respond_to? :close
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
- [200, { 'Content-Type' => 'text/plain' }, ["Report from Tuttle::Middeware::RequestProfiler\n", result.string]]
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
- when :flat
64
- ::RubyProf::FlatPrinter.new(data).print(result)
65
- content_type = 'text/plain'
66
- when :graph
67
- ::RubyProf::GraphHtmlPrinter.new(data).print(result)
68
- when :fast_stack
69
- require 'tuttle/ruby_prof/fast_call_stack_printer'
70
- ::Tuttle::RubyProf::FastCallStackPrinter.new(data).print(result, { :application => env['REQUEST_URI']})
71
- else
72
- ::RubyProf::CallStackPrinter.new(data).print(result)
73
- end
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? ? endpoint : (rack_app.is_a?(Class) ? rack_app : rack_app.class)
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 is_internal_to_rails?
24
- !!internal?
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 - Hash table
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 += thread.total_time
23
+ val + thread.total_time
47
24
  end
48
25
 
49
- @method_full_name_cache = Hash.new.compare_by_identity
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 << " (#{"%4.2f%%" % ((@overall_time/@overall_threads_time)*100)} ~ #{@overall_time})"
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 "<ul name=\"thread\">"
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 "</ul>"
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/@overall_time)*100
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 "<li style=\"display:none;\">".freeze
59
+ @output.write '<li style="display:none;">'.freeze
83
60
  else
84
- @output.write "<li>".freeze
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/@overall_time)*100 >= threshold}
91
- "<a href=\"#\" class=\"toggle empty\" ></a>".freeze
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
- if expanded
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 "<ul>".freeze
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 "</ul>".freeze
89
+ @output.write '</ul>'.freeze
115
90
  end
116
- @output.write "</li>".freeze
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] ||= h(method.full_name)
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.now} with options #{h @options.inspect}<br/>
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
-