rack-mini-profiler 0.1 → 0.1.21

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.
@@ -6,86 +6,111 @@ require 'mini_profiler/page_timer_struct'
6
6
  require 'mini_profiler/sql_timer_struct'
7
7
  require 'mini_profiler/client_timer_struct'
8
8
  require 'mini_profiler/request_timer_struct'
9
- require 'mini_profiler/body_add_proxy'
10
9
  require 'mini_profiler/storage/abstract_store'
10
+ require 'mini_profiler/storage/memcache_store'
11
11
  require 'mini_profiler/storage/memory_store'
12
12
  require 'mini_profiler/storage/redis_store'
13
13
  require 'mini_profiler/storage/file_store'
14
+ require 'mini_profiler/config'
15
+ require 'mini_profiler/profiling_methods'
16
+ require 'mini_profiler/context'
17
+ require 'mini_profiler/client_settings'
18
+ require 'mini_profiler/gc_profiler'
14
19
 
15
20
  module Rack
16
21
 
17
22
  class MiniProfiler
18
23
 
19
- VERSION = 'rZlycOOTnzxZvxTmFuOEV0dSmu4P5m5bLrCtwJHVXPA='.freeze
20
- @@instance = nil
24
+ VERSION = '107'.freeze
21
25
 
22
- def self.instance
23
- @@instance
24
- end
26
+ class << self
27
+
28
+ include Rack::MiniProfiler::ProfilingMethods
25
29
 
26
- def self.generate_id
27
- rand(36**20).to_s(36)
28
- end
30
+ def generate_id
31
+ rand(36**20).to_s(36)
32
+ end
29
33
 
30
- # Defaults for MiniProfiler's configuration
31
- def self.configuration_defaults
32
- {
33
- :auto_inject => true, # automatically inject on every html page
34
- :base_url_path => "/mini-profiler-resources/",
35
- :authorize_cb => lambda {|env| true}, # callback returns true if this request is authorized to profile
36
- :position => 'left', # Where it is displayed
37
- :backtrace_remove => nil,
38
- :backtrace_filter => nil,
39
- :skip_schema_queries => true,
40
- :storage => MiniProfiler::MemoryStore,
41
- :user_provider => Proc.new{|env| "TODO" }
42
- }
43
- end
34
+ def reset_config
35
+ @config = Config.default
36
+ end
44
37
 
45
- def self.reset_configuration
46
- @configuration = configuration_defaults
47
- end
38
+ # So we can change the configuration if we want
39
+ def config
40
+ @config ||= Config.default
41
+ end
48
42
 
49
- # So we can change the configuration if we want
50
- def self.configuration
51
- @configuration ||= configuration_defaults.dup
52
- end
43
+ def share_template
44
+ return @share_template unless @share_template.nil?
45
+ @share_template = ::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__)))
46
+ end
47
+
48
+ def current
49
+ Thread.current[:mini_profiler_private]
50
+ end
53
51
 
54
- def self.share_template
55
- return @share_template unless @share_template.nil?
56
- @share_template = ::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__)))
52
+ def current=(c)
53
+ # we use TLS cause we need access to this from sql blocks and code blocks that have no access to env
54
+ Thread.current[:mini_profiler_private]= c
55
+ end
56
+
57
+ # discard existing results, don't track this request
58
+ def discard_results
59
+ self.current.discard = true if current
60
+ end
61
+
62
+ def create_current(env={}, options={})
63
+ # profiling the request
64
+ self.current = Context.new
65
+ self.current.inject_js = config.auto_inject && (!env['HTTP_X_REQUESTED_WITH'].eql? 'XMLHttpRequest')
66
+ self.current.page_struct = PageTimerStruct.new(env)
67
+ self.current.current_timer = current.page_struct['Root']
68
+ end
69
+
70
+ def authorize_request
71
+ Thread.current[:mp_authorized] = true
72
+ end
73
+
74
+ def deauthorize_request
75
+ Thread.current[:mp_authorized] = nil
76
+ end
77
+
78
+ def request_authorized?
79
+ Thread.current[:mp_authorized]
80
+ end
57
81
  end
58
82
 
59
83
  #
60
84
  # options:
61
85
  # :auto_inject - should script be automatically injected on every html page (not xhr)
62
- def initialize(app, opts={})
63
- @@instance = self
64
- MiniProfiler.configuration.merge!(opts)
65
- @options = MiniProfiler.configuration
86
+ def initialize(app, config = nil)
87
+ MiniProfiler.config.merge!(config)
88
+ @config = MiniProfiler.config
66
89
  @app = app
67
- @options[:base_url_path] << "/" unless @options[:base_url_path].end_with? "/"
68
- unless @options[:storage_instance]
69
- @storage = @options[:storage_instance] = @options[:storage].new(@options[:storage_options])
90
+ @config.base_url_path << "/" unless @config.base_url_path.end_with? "/"
91
+ unless @config.storage_instance
92
+ @config.storage_instance = @config.storage.new(@config.storage_options)
70
93
  end
94
+ @storage = @config.storage_instance
71
95
  end
72
96
 
73
97
  def user(env)
74
- options[:user_provider].call(env)
98
+ @config.user_provider.call(env)
75
99
  end
76
100
 
77
101
  def serve_results(env)
78
102
  request = Rack::Request.new(env)
79
- page_struct = @storage.load(request['id'])
103
+ id = request['id']
104
+ page_struct = @storage.load(id)
80
105
  unless page_struct
81
- @storage.set_viewed(user(env), request['Id'])
82
- return [404, {}, ["No such result #{request['id']}"]]
106
+ @storage.set_viewed(user(env), id)
107
+ return [404, {}, ["Request not found: #{request['id']} - user #{user(env)}"]]
83
108
  end
84
109
  unless page_struct['HasUserViewed']
85
- page_struct['ClientTimings'].init_from_form_data(env, page_struct)
110
+ page_struct['ClientTimings'] = ClientTimerStruct.init_from_form_data(env, page_struct)
86
111
  page_struct['HasUserViewed'] = true
87
112
  @storage.save(page_struct)
88
- @storage.set_viewed(user(env), page_struct['Id'])
113
+ @storage.set_viewed(user(env), id)
89
114
  end
90
115
 
91
116
  result_json = page_struct.to_json
@@ -96,7 +121,7 @@ module Rack
96
121
 
97
122
  # Otherwise give the HTML back
98
123
  html = MiniProfiler.share_template.dup
99
- html.gsub!(/\{path\}/, @options[:base_url_path])
124
+ html.gsub!(/\{path\}/, @config.base_url_path)
100
125
  html.gsub!(/\{version\}/, MiniProfiler::VERSION)
101
126
  html.gsub!(/\{json\}/, result_json)
102
127
  html.gsub!(/\{includes\}/, get_profile_script(env))
@@ -109,25 +134,26 @@ module Rack
109
134
  end
110
135
 
111
136
  def serve_html(env)
112
- file_name = env['PATH_INFO'][(@options[:base_url_path].length)..1000]
137
+ file_name = env['PATH_INFO'][(@config.base_url_path.length)..1000]
113
138
  return serve_results(env) if file_name.eql?('results')
114
139
  full_path = ::File.expand_path("../html/#{file_name}", ::File.dirname(__FILE__))
115
140
  return [404, {}, ["Not found"]] unless ::File.exists? full_path
116
141
  f = Rack::File.new nil
117
142
  f.path = full_path
118
- f.cache_control = "max-age:86400"
119
- f.serving env
120
- end
121
143
 
122
- def self.current
123
- Thread.current['profiler.mini.private']
124
- end
144
+ begin
145
+ f.cache_control = "max-age:86400"
146
+ f.serving env
147
+ rescue
148
+ # old versions of rack have a different api
149
+ status, headers, body = f.serving
150
+ headers.merge! 'Cache-Control' => "max-age:86400"
151
+ [status, headers, body]
152
+ end
125
153
 
126
- def self.current=(c)
127
- # we use TLS cause we need access to this from sql blocks and code blocks that have no access to env
128
- Thread.current['profiler.mini.private'] = c
129
- end
130
-
154
+ end
155
+
156
+
131
157
  def current
132
158
  MiniProfiler.current
133
159
  end
@@ -136,54 +162,118 @@ module Rack
136
162
  MiniProfiler.current=c
137
163
  end
138
164
 
139
- def options
140
- @options
141
- end
142
165
 
143
- def self.create_current(env={}, options={})
144
- # profiling the request
145
- self.current = {}
146
- self.current['inject_js'] = options[:auto_inject] && (!env['HTTP_X_REQUESTED_WITH'].eql? 'XMLHttpRequest')
147
- self.current['page_struct'] = PageTimerStruct.new(env)
148
- self.current['current_timer'] = current['page_struct']['Root']
166
+ def config
167
+ @config
149
168
  end
150
169
 
170
+
151
171
  def call(env)
152
- status = headers = body = nil
153
172
 
154
- # only profile if authorized
155
- return @app.call(env) unless @options[:authorize_cb].call(env)
173
+ client_settings = ClientSettings.new(env)
156
174
 
157
- # handle all /mini-profiler requests here
158
- return serve_html(env) if env['PATH_INFO'].start_with? @options[:base_url_path]
175
+ status = headers = body = nil
176
+ query_string = env['QUERY_STRING']
177
+ path = env['PATH_INFO']
159
178
 
160
- MiniProfiler.create_current(env, @options)
161
- if env["QUERY_STRING"] =~ /pp=skip-backtrace/
162
- current['skip-backtrace'] = true
179
+ skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env)) ||
180
+ (@config.skip_paths && @config.skip_paths.any?{ |p| path[0,p.length] == p}) ||
181
+ query_string =~ /pp=skip/
182
+
183
+ has_profiling_cookie = client_settings.has_cookie?
184
+
185
+ if skip_it || (@config.authorization_mode == :whitelist && !has_profiling_cookie)
186
+ status,headers,body = @app.call(env)
187
+ if !skip_it && @config.authorization_mode == :whitelist && !has_profiling_cookie && MiniProfiler.request_authorized?
188
+ client_settings.write!(headers)
189
+ end
190
+ return [status,headers,body]
163
191
  end
164
192
 
165
- start = Time.now
193
+ # handle all /mini-profiler requests here
194
+ return serve_html(env) if path.start_with? @config.base_url_path
195
+
196
+ has_disable_cookie = client_settings.disable_profiling?
197
+ # manual session disable / enable
198
+ if query_string =~ /pp=disable/ || has_disable_cookie
199
+ skip_it = true
200
+ end
201
+
202
+ if query_string =~ /pp=enable/
203
+ skip_it = false
204
+ end
205
+
206
+ if skip_it
207
+ status,headers,body = @app.call(env)
208
+ client_settings.disable_profiling = true
209
+ client_settings.write!(headers)
210
+ return [status,headers,body]
211
+ end
212
+
213
+ if query_string =~ /pp=profile-gc/
214
+ return Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env)
215
+ end
216
+
217
+ MiniProfiler.create_current(env, @config)
218
+ MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
219
+ if query_string =~ /pp=normal-backtrace/
220
+ client_settings.backtrace_level = ClientSettings::BACKTRACE_DEFAULT
221
+ elsif query_string =~ /pp=no-backtrace/
222
+ current.skip_backtrace = true
223
+ client_settings.backtrace_level = ClientSettings::BACKTRACE_NONE
224
+ elsif query_string =~ /pp=full-backtrace/ || client_settings.backtrace_full?
225
+ current.full_backtrace = true
226
+ client_settings.backtrace_level = ClientSettings::BACKTRACE_FULL
227
+ elsif client_settings.backtrace_none?
228
+ current.skip_backtrace = true
229
+ end
166
230
 
167
231
  done_sampling = false
168
232
  quit_sampler = false
169
233
  backtraces = nil
170
- if env["QUERY_STRING"] =~ /pp=sample/
234
+ stacktrace_installed = true
235
+ if query_string =~ /pp=sample/
236
+ skip_frames = 0
171
237
  backtraces = []
172
238
  t = Thread.current
239
+
240
+ begin
241
+ require 'stacktrace'
242
+ skip_frames = stacktrace.length
243
+ rescue LoadError
244
+ stacktrace_installed = false
245
+ end
246
+
173
247
  Thread.new {
174
- i = 10000 # for sanity never grab more than 10k samples
175
- unless done_sampling || i < 0
176
- i -= 1
177
- backtraces << t.backtrace
178
- sleep 0.001
248
+ begin
249
+ i = 10000 # for sanity never grab more than 10k samples
250
+ while i > 0
251
+ break if done_sampling
252
+ i -= 1
253
+ if stacktrace_installed
254
+ backtraces << t.stacktrace(0,-(1+skip_frames), StackFrame::Flags::METHOD | StackFrame::Flags::KLASS)
255
+ else
256
+ backtraces << t.backtrace
257
+ end
258
+ sleep 0.001
259
+ end
260
+ ensure
261
+ quit_sampler = true
179
262
  end
180
- quit_sampler = true
181
263
  }
182
264
  end
183
265
 
184
266
  status, headers, body = nil
267
+ start = Time.now
185
268
  begin
186
- status,headers, body = @app.call(env)
269
+
270
+ # Strip all the caching headers so we don't get 304s back
271
+ # This solves a very annoying bug where rack mini profiler never shows up
272
+ env['HTTP_IF_MODIFIED_SINCE'] = nil
273
+ env['HTTP_IF_NONE_MATCH'] = nil
274
+
275
+ status,headers,body = @app.call(env)
276
+ client_settings.write!(headers)
187
277
  ensure
188
278
  if backtraces
189
279
  done_sampling = true
@@ -191,40 +281,159 @@ module Rack
191
281
  end
192
282
  end
193
283
 
194
- page_struct = current['page_struct']
284
+ skip_it = current.discard
285
+ if (config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
286
+ # this is non-obvious, don't kill the profiling cookie on errors or short requests
287
+ # this ensures that stuff that never reaches the rails stack does not kill profiling
288
+ if status == 200 && ((Time.now - start) > 0.1)
289
+ client_settings.discard_cookie!(headers)
290
+ end
291
+ skip_it = true
292
+ end
293
+
294
+ return [status,headers,body] if skip_it
295
+
296
+ # we must do this here, otherwise current[:discard] is not being properly treated
297
+ if query_string =~ /pp=env/
298
+ body.close if body.respond_to? :close
299
+ return dump_env env
300
+ end
301
+
302
+ if query_string =~ /pp=help/
303
+ body.close if body.respond_to? :close
304
+ return help(client_settings)
305
+ end
306
+
307
+ page_struct = current.page_struct
195
308
  page_struct['Root'].record_time((Time.now - start) * 1000)
196
309
 
197
- # inject headers, script
310
+ if backtraces
311
+ body.close if body.respond_to? :close
312
+ return analyze(backtraces, page_struct)
313
+ end
314
+
315
+
316
+ # no matter what it is, it should be unviewed, otherwise we will miss POST
317
+ @storage.set_unviewed(user(env), page_struct['Id'])
318
+ @storage.save(page_struct)
319
+
320
+ # inject headers, script
198
321
  if status == 200
199
- @storage.save(page_struct)
200
- @storage.set_unviewed(user(env), page_struct['Id'])
322
+
323
+ client_settings.write!(headers)
201
324
 
325
+ # mini profiler is meddling with stuff, we can not cache cause we will get incorrect data
326
+ # Rack::ETag has already inserted some nonesense in the chain
327
+ headers.delete('ETag')
328
+ headers.delete('Date')
329
+ headers['Cache-Control'] = 'must-revalidate, private, max-age=0'
330
+
202
331
  # inject header
203
332
  if headers.is_a? Hash
204
333
  headers['X-MiniProfiler-Ids'] = ids_json(env)
205
334
  end
206
335
 
207
336
  # inject script
208
- if current['inject_js'] \
337
+ if current.inject_js \
209
338
  && headers.has_key?('Content-Type') \
210
339
  && !headers['Content-Type'].match(/text\/html/).nil? then
211
- body = MiniProfiler::BodyAddProxy.new(body, self.get_profile_script(env))
340
+
341
+ response = Rack::Response.new([], status, headers)
342
+ script = self.get_profile_script(env)
343
+ if String === body
344
+ response.write inject(body,script)
345
+ else
346
+ body.each { |fragment| response.write inject(fragment, script) }
347
+ end
348
+ body.close if body.respond_to? :close
349
+ return response.finish
212
350
  end
213
351
  end
214
352
 
215
- # mini profiler is meddling with stuff, we can not cache cause we will get incorrect data
216
- # Rack::ETag has already inserted some nonesense in the chain
217
- headers.delete('ETag')
218
- headers.delete('Date')
219
- headers['Cache-Control'] = 'must-revalidate, private, max-age=0'
353
+ client_settings.write!(headers)
220
354
  [status, headers, body]
221
355
  ensure
222
356
  # Make sure this always happens
223
357
  current = nil
224
358
  end
225
359
 
360
+ def inject(fragment, script)
361
+ fragment.sub(/<\/body>/i) do
362
+ # if for whatever crazy reason we dont get a utf string,
363
+ # just force the encoding, no utf in the mp scripts anyway
364
+ if script.respond_to?(:encoding) && script.respond_to?(:force_encoding)
365
+ (script + "</body>").force_encoding(fragment.encoding)
366
+ else
367
+ script + "</body>"
368
+ end
369
+ end
370
+ end
371
+
372
+ def dump_env(env)
373
+ headers = {'Content-Type' => 'text/plain'}
374
+ body = ""
375
+ env.each do |k,v|
376
+ body << "#{k}: #{v}\n"
377
+ end
378
+ [200, headers, [body]]
379
+ end
380
+
381
+ def help(client_settings)
382
+ headers = {'Content-Type' => 'text/plain'}
383
+ body = "Append the following to your query string:
384
+
385
+ pp=help : display this screen
386
+ pp=env : display the rack environment
387
+ pp=skip : skip mini profiler for this request
388
+ pp=no-backtrace #{"(*) " if client_settings.backtrace_none?}: don't collect stack traces from all the SQL executed (sticky, use pp=normal-backtrace to enable)
389
+ pp=normal-backtrace #{"(*) " if client_settings.backtrace_default?}: collect stack traces from all the SQL executed and filter normally
390
+ pp=full-backtrace #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use pp=normal-backtrace to disable)
391
+ pp=sample : sample stack traces and return a report isolating heavy usage (experimental works best with the stacktrace gem)
392
+ pp=disable : disable profiling for this session
393
+ pp=enable : enable profiling for this session (if previously disabled)
394
+ pp=profile-gc: perform gc profiling on this request (ruby 1.9.3 only)
395
+ "
396
+
397
+ client_settings.write!(headers)
398
+ [200, headers, [body]]
399
+ end
400
+
401
+ def analyze(traces, page_struct)
402
+ headers = {'Content-Type' => 'text/plain'}
403
+ body = "Collected: #{traces.count} stack traces. Duration(ms): #{page_struct.duration_ms}"
404
+
405
+ seen = {}
406
+ fulldump = ""
407
+ traces.each do |trace|
408
+ fulldump << "\n\n"
409
+ distinct = {}
410
+ trace.each do |frame|
411
+ frame = "#{frame.klass}::#{frame.method}" unless String === frame
412
+ unless distinct[frame]
413
+ distinct[frame] = true
414
+ seen[frame] ||= 0
415
+ seen[frame] += 1
416
+ end
417
+ fulldump << frame << "\n"
418
+ end
419
+ end
420
+
421
+ body << "\n\nStack Trace Analysis\n"
422
+ seen.to_a.sort{|x,y| y[1] <=> x[1]}.each do |name, count|
423
+ if count > traces.count / 10
424
+ body << "#{name} x #{count}\n"
425
+ end
426
+ end
427
+
428
+ body << "\n\n\nRaw traces \n"
429
+ body << fulldump
430
+
431
+ [200, headers, [body]]
432
+ end
433
+
226
434
  def ids_json(env)
227
- ids = [current['page_struct']["Id"]] + (@storage.get_unviewed_ids(user(env)) || [])
435
+ # cap at 10 ids, otherwise there is a chance you can blow the header
436
+ ids = [current.page_struct["Id"]] + (@storage.get_unviewed_ids(user(env)) || [])[0..8]
228
437
  ::JSON.generate(ids.uniq)
229
438
  end
230
439
 
@@ -236,16 +445,16 @@ module Rack
236
445
  # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
237
446
  def get_profile_script(env)
238
447
  ids = ids_json(env)
239
- path = @options[:base_url_path]
448
+ path = @config.base_url_path
240
449
  version = MiniProfiler::VERSION
241
- position = @options[:position]
450
+ position = @config.position
242
451
  showTrivial = false
243
452
  showChildren = false
244
453
  maxTracesToShow = 10
245
454
  showControls = false
246
- currentId = current['page_struct']["Id"]
455
+ currentId = current.page_struct["Id"]
247
456
  authorized = true
248
- useExistingjQuery = false
457
+ useExistingjQuery = @config.use_existing_jquery
249
458
  # TODO : cache this snippet
250
459
  script = IO.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
251
460
  # replace the variables
@@ -255,52 +464,13 @@ module Rack
255
464
  end
256
465
  # replace the '{{' and '}}''
257
466
  script.gsub!(/\{\{/, '{').gsub!(/\}\}/, '}')
258
- current['inject_js'] = false
467
+ current.inject_js = false
259
468
  script
260
469
  end
261
470
 
262
471
  # cancels automatic injection of profile script for the current page
263
472
  def cancel_auto_inject(env)
264
- current['inject_js'] = false
265
- end
266
-
267
- # perform a profiling step on given block
268
- def self.step(name)
269
- if current
270
- old_timer = current['current_timer']
271
- new_step = RequestTimerStruct.new(name, current['page_struct'])
272
- current['current_timer'] = new_step
273
- new_step['Name'] = name
274
- start = Time.now
275
- result = yield if block_given?
276
- new_step.record_time((Time.now - start)*1000)
277
- old_timer.add_child(new_step)
278
- current['current_timer'] = old_timer
279
- result
280
- else
281
- yield if block_given?
282
- end
283
- end
284
-
285
- def self.profile_method(klass, method, &blk)
286
- default_name = klass.to_s + " " + method.to_s
287
- with_profiling = (method.to_s + "_with_mini_profiler").intern
288
- without_profiling = (method.to_s + "_without_mini_profiler").intern
289
-
290
- klass.send :alias_method, without_profiling, method
291
- klass.send :define_method, with_profiling do |*args, &orig|
292
- name = default_name
293
- name = blk.bind(self).call(*args) if blk
294
- ::Rack::MiniProfiler.step name do
295
- self.send without_profiling, *args, &orig
296
- end
297
- end
298
- klass.send :alias_method, method, with_profiling
299
- end
300
-
301
- def record_sql(query, elapsed_ms)
302
- c = current
303
- c['current_timer'].add_sql(query, elapsed_ms, c['page_struct'], c['skip-backtrace']) if (c && c['current_timer'])
473
+ current.inject_js = false
304
474
  end
305
475
 
306
476
  end