rack-mini-profiler 0.9.4 → 0.9.9

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.

Potentially problematic release.


This version of rack-mini-profiler might be problematic. Click here for more details.

@@ -12,10 +12,13 @@ module Rack
12
12
  @attributes
13
13
  end
14
14
 
15
- attr_accessor :authorization_mode, :auto_inject, :backtrace_ignores, :backtrace_includes, :backtrace_remove,
16
- :backtrace_threshold_ms, :base_url_path, :disable_caching, :enabled, :flamegraph_sample_rate, :logger, :position,
17
- :pre_authorize_cb, :skip_paths, :skip_schema_queries, :start_hidden, :storage, :storage_failure,
18
- :storage_instance, :storage_options, :toggle_shortcut, :user_provider
15
+ attr_accessor :authorization_mode, :auto_inject, :backtrace_ignores,
16
+ :backtrace_includes, :backtrace_remove, :backtrace_threshold_ms,
17
+ :base_url_path, :disable_caching, :disable_env_dump, :enabled,
18
+ :flamegraph_sample_rate, :logger, :position, :pre_authorize_cb,
19
+ :skip_paths, :skip_schema_queries, :start_hidden, :storage,
20
+ :storage_failure, :storage_instance, :storage_options, :toggle_shortcut,
21
+ :user_provider, :collapse_results
19
22
 
20
23
  # Deprecated options
21
24
  attr_accessor :use_existing_jquery
@@ -44,6 +47,8 @@ module Rack
44
47
  end
45
48
  end
46
49
  @enabled = true
50
+ @disable_env_dump = false
51
+ @collapse_results = true
47
52
  self
48
53
  }
49
54
  end
@@ -6,18 +6,15 @@ class Rack::MiniProfiler::GCProfiler
6
6
  end
7
7
 
8
8
  def object_space_stats
9
- stats = {}
10
- ids = {}
9
+ stats = Hash.new(0).compare_by_identity
10
+ ids = Hash.new.compare_by_identity
11
11
 
12
12
  @ignore << stats.__id__
13
13
  @ignore << ids.__id__
14
14
 
15
- i=0
16
15
  ObjectSpace.each_object { |o|
17
16
  begin
18
- i = stats[o.class] || 0
19
- i += 1
20
- stats[o.class] = i
17
+ stats[o.class] += 1
21
18
  ids[o.__id__] = o if Integer === o.__id__
22
19
  rescue NoMethodError
23
20
  # protect against BasicObject
@@ -38,12 +35,12 @@ class Rack::MiniProfiler::GCProfiler
38
35
  end
39
36
 
40
37
  def diff_object_stats(before, after)
41
- diff = {}
38
+ diff = {}.compare_by_identity
42
39
  after.each do |k,v|
43
- diff[k] = v - (before[k] || 0)
40
+ diff[k] = v - before[k]
44
41
  end
45
42
  before.each do |k,v|
46
- diff[k] = 0 - v unless after[k]
43
+ diff[k] = 0 - v unless after.has_key?(k)
47
44
  end
48
45
 
49
46
  diff
@@ -92,36 +89,11 @@ class Rack::MiniProfiler::GCProfiler
92
89
  [objects,memory_allocated]
93
90
  end
94
91
 
95
- def profile_gc_time(app, env)
96
- body = []
97
-
98
- begin
99
- GC::Profiler.clear
100
- prev_profiler_state = GC::Profiler.enabled?
101
- prev_gc_state = GC.enable
102
- GC::Profiler.enable
103
- b = app.call(env)[2]
104
- b.close if b.respond_to? :close
105
- body << "GC Profiler ran during this request, if it fired you will see the cost below:\n\n"
106
- body << GC::Profiler.result
107
- ensure
108
- prev_gc_state ? GC.disable : GC.enable
109
- GC::Profiler.disable unless prev_profiler_state
110
- end
111
-
112
- return [200, {'Content-Type' => 'text/plain'}, body]
113
- end
114
-
115
92
  def profile_gc(app, env)
116
93
 
117
94
  # for memsize_of
118
95
  require 'objspace'
119
96
 
120
- body = [];
121
-
122
- stat_before,stat_after,diff,string_analysis,
123
- new_objects, memory_allocated, stat, memory_before, objects_before = nil
124
-
125
97
  # clean up before
126
98
  GC.start
127
99
  stat = GC.stat
@@ -138,13 +110,17 @@ class Rack::MiniProfiler::GCProfiler
138
110
  new_objects, memory_allocated = analyze_growth(stat_before[:ids], stat_after[:ids])
139
111
  objects_before, memory_before = analyze_initial_state(stat_before[:ids])
140
112
 
113
+ body = []
141
114
 
142
115
  body << "
143
116
  Overview
144
- ------------------------------------
145
- Initial state: object count - #{objects_before} , memory allocated outside heap (bytes) #{memory_before}
117
+ --------
118
+ Initial state: object count: #{objects_before}
119
+ Memory allocated outside heap (bytes): #{memory_before}
146
120
 
147
- GC Stats: #{stat.map{|k,v| "#{k} : #{v}" }.join(", ")}
121
+ GC Stats:
122
+ --------
123
+ #{stat.map{|k,v| "#{k} : #{v}" }.sort!.join("\n")}
148
124
 
149
125
  New bytes allocated outside of Ruby heaps: #{memory_allocated}
150
126
  New objects: #{new_objects}
@@ -152,16 +128,16 @@ New objects: #{new_objects}
152
128
 
153
129
  body << "
154
130
  ObjectSpace delta caused by request:
155
- --------------------------------------------\n"
156
- diff.to_a.reject{|k,v| v == 0}.sort{|x,y| y[1] <=> x[1]}.each do |k,v|
157
- body << "#{k} : #{v}\n" if v != 0
131
+ -----------------------------------\n"
132
+ diff.to_a.delete_if{|_k, v| v == 0}.sort_by! { |_k, v| v }.reverse_each do |k,v|
133
+ body << "#{k} : #{v}\n"
158
134
  end
159
135
 
160
136
  body << "\n
161
137
  ObjectSpace stats:
162
138
  -----------------\n"
163
139
 
164
- stat_after[:stats].to_a.sort{|x,y| y[1] <=> x[1]}.each do |k,v|
140
+ stat_after[:stats].to_a.sort_by!{ |_k, v| v }.reverse_each do |k,v|
165
141
  body << "#{k} : #{v}\n"
166
142
  end
167
143
 
@@ -170,7 +146,7 @@ ObjectSpace stats:
170
146
  String stats:
171
147
  ------------\n"
172
148
 
173
- string_analysis.to_a.sort{|x,y| y[1] <=> x[1] }.take(1000).each do |string,count|
149
+ string_analysis.to_a.sort_by!{ |_k, v| -v }.take(1000).each do |string,count|
174
150
  body << "#{count} : #{string}\n"
175
151
  end
176
152
 
@@ -17,6 +17,10 @@ module Rack
17
17
  @config ||= Config.default
18
18
  end
19
19
 
20
+ def resources_root
21
+ @resources_root ||= ::File.expand_path("../../html", __FILE__)
22
+ end
23
+
20
24
  def share_template
21
25
  @share_template ||= ::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__)))
22
26
  end
@@ -37,10 +41,11 @@ module Rack
37
41
 
38
42
  def create_current(env={}, options={})
39
43
  # profiling the request
40
- self.current = Context.new
41
- self.current.inject_js = config.auto_inject && (!env['HTTP_X_REQUESTED_WITH'].eql? 'XMLHttpRequest')
42
- self.current.page_struct = TimerStruct::Page.new(env)
43
- self.current.current_timer = current.page_struct[:root]
44
+ context = Context.new
45
+ context.inject_js = config.auto_inject && (!env['HTTP_X_REQUESTED_WITH'].eql? 'XMLHttpRequest')
46
+ context.page_struct = TimerStruct::Page.new(env)
47
+ context.current_timer = context.page_struct[:root]
48
+ self.current = context
44
49
  end
45
50
 
46
51
  def authorize_request
@@ -92,24 +97,26 @@ module Rack
92
97
  @storage.set_viewed(user(env), id)
93
98
  end
94
99
 
95
- result_json = page_struct.to_json
96
100
  # If we're an XMLHttpRequest, serve up the contents as JSON
97
101
  if request.xhr?
102
+ result_json = page_struct.to_json
98
103
  [200, { 'Content-Type' => 'application/json'}, [result_json]]
99
104
  else
100
-
101
105
  # Otherwise give the HTML back
102
- html = MiniProfiler.share_template.dup
103
- html.gsub!(/\{path\}/, "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}")
104
- html.gsub!(/\{version\}/, MiniProfiler::ASSET_VERSION)
105
- html.gsub!(/\{json\}/, result_json)
106
- html.gsub!(/\{includes\}/, get_profile_script(env))
107
- html.gsub!(/\{name\}/, page_struct[:name])
108
- html.gsub!(/\{duration\}/, "%.1f" % page_struct.duration_ms)
109
-
106
+ html = generate_html(page_struct, env)
110
107
  [200, {'Content-Type' => 'text/html'}, [html]]
111
108
  end
109
+ end
112
110
 
111
+ def generate_html(page_struct, env, result_json = page_struct.to_json)
112
+ html = MiniProfiler.share_template.dup
113
+ html.sub!('{path}', "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}")
114
+ html.sub!('{version}', MiniProfiler::ASSET_VERSION)
115
+ html.sub!('{json}', result_json)
116
+ html.sub!('{includes}', get_profile_script(env))
117
+ html.sub!('{name}', page_struct[:name])
118
+ html.sub!('{duration}', page_struct.duration_ms.round(1).to_s)
119
+ html
113
120
  end
114
121
 
115
122
  def serve_html(env)
@@ -117,21 +124,11 @@ module Rack
117
124
 
118
125
  return serve_results(env) if file_name.eql?('results')
119
126
 
120
- full_path = ::File.expand_path("../html/#{file_name}", ::File.dirname(__FILE__))
121
- return [404, {}, ["Not found"]] unless ::File.exists? full_path
122
- f = Rack::File.new nil
123
- f.path = full_path
124
-
125
- begin
126
- f.cache_control = "max-age:86400"
127
- f.serving env
128
- rescue
129
- # old versions of rack have a different api
130
- status, headers, body = f.serving
131
- headers.merge! 'Cache-Control' => "max-age:86400"
132
- [status, headers, body]
133
- end
127
+ resources_env = env.dup
128
+ resources_env['PATH_INFO'] = file_name
134
129
 
130
+ rack_file = Rack::File.new(MiniProfiler.resources_root, {'Cache-Control' => 'max-age:86400'})
131
+ rack_file.call(resources_env)
135
132
  end
136
133
 
137
134
 
@@ -197,22 +194,27 @@ module Rack
197
194
  client_settings.disable_profiling = false
198
195
  end
199
196
 
197
+ # profile gc
200
198
  if query_string =~ /pp=profile-gc/
201
199
  current.measure = false if current
202
-
203
- if query_string =~ /pp=profile-gc-time/
204
- return Rack::MiniProfiler::GCProfiler.new.profile_gc_time(@app, env)
205
- elsif query_string =~ /pp=profile-gc-ruby-head/
206
- result = StringIO.new
207
- report = MemoryProfiler.report do
208
- _,_,body = @app.call(env)
209
- body.close if body.respond_to? :close
210
- end
211
- report.pretty_print(result)
212
- return text_result(result.string)
213
- else
214
- return Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env)
200
+ return Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env)
201
+ end
202
+
203
+ # profile memory
204
+ if query_string =~ /pp=profile-memory/
205
+ query_params = Rack::Utils.parse_nested_query(query_string)
206
+ options = {
207
+ :ignore_files => query_params['memory_profiler_ignore_files'],
208
+ :allow_files => query_params['memory_profiler_allow_files'],
209
+ }
210
+ options[:top]= Integer(query_params['memory_profiler_top']) if query_params.key?('memory_profiler_top')
211
+ result = StringIO.new
212
+ report = MemoryProfiler.report(options) do
213
+ _,_,body = @app.call(env)
214
+ body.close if body.respond_to? :close
215
215
  end
216
+ report.pretty_print(result)
217
+ return text_result(result.string)
216
218
  end
217
219
 
218
220
  MiniProfiler.create_current(env, @config)
@@ -288,7 +290,7 @@ module Rack
288
290
  if (config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
289
291
  # this is non-obvious, don't kill the profiling cookie on errors or short requests
290
292
  # this ensures that stuff that never reaches the rails stack does not kill profiling
291
- if status == 200 && ((Time.now - start) > 0.1)
293
+ if status.to_i >= 200 && status.to_i < 300 && ((Time.now - start) > 0.1)
292
294
  client_settings.discard_cookie!(headers)
293
295
  end
294
296
  skip_it = true
@@ -302,7 +304,7 @@ module Rack
302
304
  return dump_exceptions exceptions
303
305
  end
304
306
 
305
- if query_string =~ /pp=env/
307
+ if query_string =~ /pp=env/ && !config.disable_env_dump
306
308
  body.close if body.respond_to? :close
307
309
  return dump_env env
308
310
  end
@@ -333,7 +335,7 @@ module Rack
333
335
  @storage.save(page_struct)
334
336
 
335
337
  # inject headers, script
336
- if headers['Content-Type'] && status == 200
338
+ if status >= 200 && status < 300
337
339
  client_settings.write!(headers)
338
340
  result = inject_profiler(env,status,headers,body)
339
341
  return result if result
@@ -386,39 +388,17 @@ module Rack
386
388
  end
387
389
 
388
390
  def inject(fragment, script)
389
- if fragment.match(/<\/body>/i)
390
- # explicit </body>
391
-
392
- regex = /<\/body>/i
393
- close_tag = '</body>'
394
- elsif fragment.match(/<\/html>/i)
395
- # implicit </body>
396
-
397
- regex = /<\/html>/i
398
- close_tag = '</html>'
399
- else
400
- # implicit </body> and </html>. Don't do anything.
401
-
402
- return fragment
403
- end
404
-
405
- matches = fragment.scan(regex).length
406
- index = 1
407
- fragment.gsub(regex) do
408
- # though malformed there is an edge case where /body exists earlier in the html, work around
409
- if index < matches
410
- index += 1
411
- close_tag
412
- else
413
-
414
- # if for whatever crazy reason we dont get a utf string,
415
- # just force the encoding, no utf in the mp scripts anyway
416
- if script.respond_to?(:encoding) && script.respond_to?(:force_encoding)
417
- (script + close_tag).force_encoding(fragment.encoding)
418
- else
419
- script + close_tag
420
- end
391
+ # find explicit or implicit body
392
+ index = fragment.rindex(/<\/body>/i) || fragment.rindex(/<\/html>/i)
393
+ if index
394
+ # if for whatever crazy reason we dont get a utf string,
395
+ # just force the encoding, no utf in the mp scripts anyway
396
+ if script.respond_to?(:encoding) && script.respond_to?(:force_encoding)
397
+ script = script.force_encoding(fragment.encoding)
421
398
  end
399
+ fragment.insert(index, script)
400
+ else
401
+ fragment
422
402
  end
423
403
  end
424
404
 
@@ -463,23 +443,50 @@ module Rack
463
443
  def analyze_memory
464
444
  require 'objspace'
465
445
 
446
+ utf8 = "utf-8"
447
+
448
+ GC.start
449
+
450
+ trunc = lambda do |str|
451
+ str = str.length > 200 ? str : str[0..200]
452
+
453
+ if str.encoding != Encoding::UTF_8
454
+ str = str.dup
455
+ str.force_encoding(utf8)
456
+
457
+ unless str.valid_encoding?
458
+ # work around bust string with a double conversion
459
+ str.encode!("utf-16","utf-8",:invalid => :replace)
460
+ str.encode!("utf-8","utf-16")
461
+ end
462
+ end
463
+
464
+ str
465
+ end
466
+
466
467
  body = "ObjectSpace stats:\n\n"
467
468
 
468
- body << ObjectSpace.count_objects
469
+ counts = ObjectSpace.count_objects
470
+ total_strings = counts[:T_STRING]
471
+
472
+ body << counts
469
473
  .sort{|a,b| b[1] <=> a[1]}
470
474
  .map{|k,v| "#{k}: #{v}"}
471
475
  .join("\n")
472
476
 
473
- body << "\n\n\n1000 Largest strings:\n\n"
474
-
475
477
  strings = []
476
- max_size = 1000
478
+ string_counts = Hash.new(0)
479
+ sample_strings = []
477
480
 
478
- GC.start
479
- GC.start
481
+ max_size = 1000
482
+ sample_every = total_strings / max_size
480
483
 
484
+ i = 0
481
485
  ObjectSpace.each_object(String) do |str|
482
- strings << [str[0..200], str.length]
486
+ i += 1
487
+ string_counts[str] += 1
488
+ strings << [trunc.call(str), str.length]
489
+ sample_strings << [trunc.call(str), str.length] if i % sample_every == 0
483
490
  if strings.length > max_size * 2
484
491
  trim_strings(strings, max_size)
485
492
  end
@@ -487,7 +494,14 @@ module Rack
487
494
 
488
495
  trim_strings(strings, max_size)
489
496
 
490
- body << strings.map{|s,len| "#{s}\n(#{len})\n\n"}.join("\n")
497
+ body << "\n\n\n1000 Largest strings:\n\n"
498
+ body << strings.map{|s,len| "#{s[0..1000]}\n(len: #{len})\n\n"}.join("\n")
499
+
500
+ body << "\n\n\n1000 Sample strings:\n\n"
501
+ body << sample_strings.map{|s,len| "#{s[0..1000]}\n(len: #{len})\n\n"}.join("\n")
502
+
503
+ body << "\n\n\n1000 Most common strings:\n\n"
504
+ body << string_counts.sort{|a,b| b[1] <=> a[1]}[0..max_size].map{|s,len| "#{trunc.call(s)}\n(x #{len})\n\n"}.join("\n")
491
505
 
492
506
  text_result(body)
493
507
  end
@@ -517,12 +531,11 @@ Append the following to your query string:
517
531
  #{make_link "disable", env} : disable profiling for this session
518
532
  #{make_link "enable", env} : enable profiling for this session (if previously disabled)
519
533
  #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request (ruby 1.9.3 only)
520
- #{make_link "profile-gc-time", env} : perform built-in gc profiling on this request (ruby 1.9.3 only)
521
- #{make_link "profile-gc-ruby-head", env} : requires the memory_profiler gem, new location based report
534
+ #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
522
535
  #{make_link "flamegraph", env} : works best on Ruby 2.0, a graph representing sampled activity (requires the flamegraph gem).
523
536
  #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
524
537
  #{make_link "flamegraph_embed", env} : works best on Ruby 2.0, a graph representing sampled activity (requires the flamegraph gem), embedded resources for use on an intranet.
525
- #{make_link "trace-exceptions", env} : requires Ruby 2.0, will return all the spots where your application raises execptions
538
+ #{make_link "trace-exceptions", env} : requires Ruby 2.0, will return all the spots where your application raises exceptions
526
539
  #{make_link "analyze-memory", env} : requires Ruby 2.0, will perform basic memory analysis of heap
527
540
  </pre>
528
541
  </body>
@@ -558,7 +571,17 @@ Append the following to your query string:
558
571
  # * you have disabled auto append behaviour throught :auto_inject => false flag
559
572
  # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
560
573
  def get_profile_script(env)
561
- path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
574
+ path = if ENV["PASSENGER_BASE_URI"] then
575
+ # added because the SCRIPT_NAME workaround below then
576
+ # breaks running under a prefix as permitted by Passenger.
577
+ "#{ENV['PASSENGER_BASE_URI']}#{@config.base_url_path}"
578
+ elsif env["action_controller.instance"]
579
+ # Rails engines break SCRIPT_NAME; the following appears to discard SCRIPT_NAME
580
+ # since url_for appears documented to return any String argument unmodified
581
+ env["action_controller.instance"].url_for("#{@config.base_url_path}")
582
+ else
583
+ "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
584
+ end
562
585
 
563
586
  settings = {
564
587
  :path => path,
@@ -570,7 +593,8 @@ Append the following to your query string:
570
593
  :showControls => false,
571
594
  :authorized => true,
572
595
  :toggleShortcut => @config.toggle_shortcut,
573
- :startHidden => @config.start_hidden
596
+ :startHidden => @config.start_hidden,
597
+ :collapseResults => @config.collapse_results
574
598
  }
575
599
 
576
600
  if current && current.page_struct