tuttle 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
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
-