rack-mini-profiler 0.1.23 → 0.1.24

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

@@ -61,11 +61,10 @@ var MiniProfiler = (function () {
61
61
  }
62
62
  };
63
63
 
64
- var getClientPerformance = function () {
64
+ var getClientPerformance = function() {
65
65
  return window.performance == null ? null : window.performance;
66
- }
66
+ };
67
67
 
68
- var waitedForEnd = 0;
69
68
  var fetchResults = function (ids) {
70
69
  var clientPerformance, clientProbes, i, j, p, id, idx;
71
70
 
@@ -365,6 +364,9 @@ var MiniProfiler = (function () {
365
364
  popupHide(button, popup);
366
365
  }
367
366
  });
367
+ $(document).bind('keydown', options.toggleShortcut, function(e) {
368
+ $('.profiler-results').toggle();
369
+ });
368
370
  };
369
371
 
370
372
  var initFullView = function () {
@@ -433,6 +435,7 @@ var MiniProfiler = (function () {
433
435
  // get master page profiler results
434
436
  fetchResults(options.ids);
435
437
  });
438
+ if (options.startHidden) container.hide();
436
439
  }
437
440
  else {
438
441
  fetchResults(options.ids);
@@ -508,6 +511,41 @@ var MiniProfiler = (function () {
508
511
  });
509
512
  }
510
513
 
514
+ if (typeof (MooTools) != 'undefined' && typeof (Request) != 'undefined') {
515
+ Request.prototype.addEvents({
516
+ onComplete: function() {
517
+ var stringIds = this.xhr.getResponseHeader('X-MiniProfiler-Ids');
518
+ if (stringIds) {
519
+ var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds);
520
+ fetchResults(ids);
521
+ }
522
+ }
523
+ });
524
+ }
525
+
526
+ // add support for AngularJS, which use the basic XMLHttpRequest object.
527
+ if (window.angular && typeof (XMLHttpRequest) != 'undefined') {
528
+ var _send = XMLHttpRequest.prototype.send;
529
+
530
+ XMLHttpRequest.prototype.send = function sendReplacement(data) {
531
+ this._onreadystatechange = this.onreadystatechange;
532
+
533
+ this.onreadystatechange = function onReadyStateChangeReplacement() {
534
+ if (this.readyState == 4) {
535
+ var stringIds = this.getResponseHeader('X-MiniProfiler-Ids');
536
+ if (stringIds) {
537
+ var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds);
538
+ fetchResults(ids);
539
+ }
540
+ }
541
+
542
+ return this._onreadystatechange.apply(this, arguments);
543
+ }
544
+
545
+ return _send.apply(this, arguments);
546
+ }
547
+ }
548
+
511
549
  // some elements want to be hidden on certain doc events
512
550
  bindDocumentEvents();
513
551
  };
@@ -529,6 +567,8 @@ var MiniProfiler = (function () {
529
567
 
530
568
  var position = script.getAttribute('data-position');
531
569
 
570
+ var toggleShortcut = script.getAttribute('data-toggle-shortcut');
571
+
532
572
  if (script.getAttribute('data-max-traces'))
533
573
  var maxTraces = parseInt(script.getAttribute('data-max-traces'));
534
574
 
@@ -536,6 +576,7 @@ var MiniProfiler = (function () {
536
576
  if (script.getAttribute('data-children') == 'true') var children = true;
537
577
  if (script.getAttribute('data-controls') == 'true') var controls = true;
538
578
  if (script.getAttribute('data-authorized') == 'true') var authorized = true;
579
+ if (script.getAttribute('data-start-hidden') == 'true') var startHidden = true;
539
580
 
540
581
  return {
541
582
  ids: ids,
@@ -547,14 +588,15 @@ var MiniProfiler = (function () {
547
588
  maxTracesToShow: maxTraces,
548
589
  showControls: controls,
549
590
  currentId: currentId,
550
- authorized: authorized
591
+ authorized: authorized,
592
+ toggleShortcut: toggleShortcut,
593
+ startHidden: startHidden
551
594
  }
552
595
  })();
553
596
 
554
597
  var doInit = function () {
555
598
  // when rendering a shared, full page, this div will exist
556
599
  container = $('.profiler-result-full');
557
-
558
600
  if (container.length) {
559
601
  if (window.location.href.indexOf("&trivial=1") > 0) {
560
602
  options.showTrivial = true
@@ -607,11 +649,10 @@ var MiniProfiler = (function () {
607
649
  } else {
608
650
  doInit();
609
651
  }
610
- }
611
- else {
652
+ } else {
612
653
  doInit();
613
654
  }
614
- }
655
+ };
615
656
 
616
657
  if (typeof(jQuery) == 'function') {
617
658
  var jQueryVersion = jQuery.fn.jquery.split('.');
@@ -711,7 +752,7 @@ var MiniProfiler = (function () {
711
752
  // start adding at the root and recurse down
712
753
  addToResults(root);
713
754
 
714
- var removeDuration = function (list, duration) {
755
+ var removeDuration = function(list, duration) {
715
756
 
716
757
  var newList = [];
717
758
  for (var i = 0; i < list.length; i++) {
@@ -735,7 +776,7 @@ var MiniProfiler = (function () {
735
776
  }
736
777
 
737
778
  return newList;
738
- }
779
+ };
739
780
 
740
781
  var processTimes = function (elem, parent) {
741
782
  var duration = { start: elem.StartMilliseconds, finish: (elem.StartMilliseconds + elem.DurationMilliseconds) };
@@ -757,7 +798,7 @@ var MiniProfiler = (function () {
757
798
  // sort results by time
758
799
  result.sort(function (a, b) { return a.StartMilliseconds - b.StartMilliseconds; });
759
800
 
760
- var determineOverlap = function (gap, node) {
801
+ var determineOverlap = function(gap, node) {
761
802
  var overlap = 0;
762
803
  for (var i = 0; i < node.richTiming.length; i++) {
763
804
  var current = node.richTiming[i];
@@ -771,7 +812,7 @@ var MiniProfiler = (function () {
771
812
  overlap += Math.min(gap.finish, current.finish) - Math.max(gap.start, current.start);
772
813
  }
773
814
  return overlap;
774
- }
815
+ };
775
816
 
776
817
  var determineGap = function (gap, node, match) {
777
818
  var overlap = determineOverlap(gap, node);
@@ -848,6 +889,12 @@ var MiniProfiler = (function () {
848
889
 
849
890
  MiniProfiler.init();
850
891
 
892
+ // jquery.hotkeys.js
893
+ // https://github.com/jeresig/jquery.hotkeys/blob/master/jquery.hotkeys.js
894
+
895
+ (function(d){function h(g){if("string"===typeof g.data){var h=g.handler,j=g.data.toLowerCase().split(" ");g.handler=function(b){if(!(this!==b.target&&(/textarea|select/i.test(b.target.nodeName)||"text"===b.target.type))){var c="keypress"!==b.type&&d.hotkeys.specialKeys[b.which],e=String.fromCharCode(b.which).toLowerCase(),a="",f={};b.altKey&&"alt"!==c&&(a+="alt+");b.ctrlKey&&"ctrl"!==c&&(a+="ctrl+");b.metaKey&&(!b.ctrlKey&&"meta"!==c)&&(a+="meta+");b.shiftKey&&"shift"!==c&&(a+="shift+");c?f[a+c]=
896
+ !0:(f[a+e]=!0,f[a+d.hotkeys.shiftNums[e]]=!0,"shift+"===a&&(f[d.hotkeys.shiftNums[e]]=!0));c=0;for(e=j.length;c<e;c++)if(f[j[c]])return h.apply(this,arguments)}}}}d.hotkeys={version:"0.8",specialKeys:{8:"backspace",9:"tab",13:"return",16:"shift",17:"ctrl",18:"alt",19:"pause",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"insert",46:"del",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9",106:"*",107:"+",
897
+ 109:"-",110:".",111:"/",112:"f1",113:"f2",114:"f3",115:"f4",116:"f5",117:"f6",118:"f7",119:"f8",120:"f9",121:"f10",122:"f11",123:"f12",144:"numlock",145:"scroll",191:"/",224:"meta"},shiftNums:{"`":"~",1:"!",2:"@",3:"#",4:"$",5:"%",6:"^",7:"&",8:"*",9:"(","0":")","-":"_","=":"+",";":": ","'":'"',",":"<",".":">","/":"?","\\":"|"}};d.each(["keydown","keyup","keypress"],function(){d.event.special[this]={add:h}})})(jQuery);
851
898
 
852
899
  // prettify.js
853
900
  // http://code.google.com/p/google-code-prettify/
@@ -49,7 +49,7 @@
49
49
  // styles shared between popup view and full view
50
50
  .profiler-result
51
51
  {
52
-
52
+
53
53
  .profiler-toggle-duration-with-children
54
54
  {
55
55
  float: right;
@@ -143,6 +143,9 @@
143
143
  float:left;
144
144
  margin-left:0px;
145
145
  }
146
+ &.profiler-custom-link {
147
+ float:left;
148
+ }
146
149
  }
147
150
  }
148
151
  }
@@ -189,15 +192,15 @@
189
192
  text-align:right;
190
193
  margin-bottom:5px;
191
194
  }
192
-
195
+
193
196
  .profiler-gap-info, .profiler-gap-info td { background-color: #ccc;}
194
197
  .profiler-gap-info {
195
198
  .profiler-unit {color: #777;}
196
199
  .profiler-info {text-align: right}
197
200
  &.profiler-trivial-gaps {display: none}
198
- }
199
-
200
- .profiler-trivial-gap-container { text-align: center;}
201
+ }
202
+
203
+ .profiler-trivial-gap-container { text-align: center;}
201
204
 
202
205
  // prettify colors
203
206
  .str{color:maroon}
@@ -287,24 +290,24 @@
287
290
  }
288
291
  }
289
292
 
290
- .profiler-controls {
291
- display: block;
292
- font-size:12px;
293
- font-family: @codeFonts;
294
- cursor:default;
295
- text-align: center;
296
-
297
- span {
298
- border-right: 1px solid @mutedColor;
299
- padding-right: 5px;
300
- margin-right: 5px;
301
- cursor:pointer;
302
- }
303
-
304
- span:last-child {
305
- border-right: none;
306
- }
307
- }
293
+ .profiler-controls {
294
+ display: block;
295
+ font-size:12px;
296
+ font-family: @codeFonts;
297
+ cursor:default;
298
+ text-align: center;
299
+
300
+ span {
301
+ border-right: 1px solid @mutedColor;
302
+ padding-right: 5px;
303
+ margin-right: 5px;
304
+ cursor:pointer;
305
+ }
306
+
307
+ span:last-child {
308
+ border-right: none;
309
+ }
310
+ }
308
311
 
309
312
  .profiler-popup {
310
313
  display:none;
@@ -367,19 +370,19 @@
367
370
  }
368
371
  }
369
372
 
370
- &.profiler-min .profiler-result {
371
- display: none;
372
- }
373
+ &.profiler-min .profiler-result {
374
+ display: none;
375
+ }
373
376
 
374
- &.profiler-min .profiler-controls span {
375
- display: none;
376
- }
377
+ &.profiler-min .profiler-controls span {
378
+ display: none;
379
+ }
377
380
 
378
- &.profiler-min .profiler-controls .profiler-min-max {
379
- border-right: none;
380
- padding: 0px;
381
- margin: 0px;
382
- }
381
+ &.profiler-min .profiler-controls .profiler-min-max {
382
+ border-right: none;
383
+ padding: 0px;
384
+ margin: 0px;
385
+ }
383
386
  }
384
387
 
385
388
  // popup results' queries will be displayed in front of this
@@ -465,4 +468,4 @@
465
468
  }
466
469
  }
467
470
  }
468
- }
471
+ }
@@ -123,6 +123,9 @@
123
123
 
124
124
  <script id="linksTemplate" type="text/x-jquery-tmpl">
125
125
  <a href="${MiniProfiler.shareUrl(Id)}" class="profiler-share-profiler-results" target="_blank">share</a>
126
+ {{if CustomLink}}
127
+ <a href="${CustomLink}" class="profiler-custom-link" target="_blank">${CustomLinkName}</a>
128
+ {{/if}}
126
129
  {{if HasTrivialTimings}}
127
130
  <a class="profiler-toggle-trivial" data-show-on-load="${HasAllTrivialTimings}" title="toggles any rows with &lt; ${TrivialDurationThresholdMilliseconds} ms">
128
131
  show trivial
@@ -156,12 +159,10 @@
156
159
  <td class="profiler-duration" title="aggregate duration of all queries in this step (excludes children)">
157
160
  ${MiniProfiler.formatDuration(timing.SqlTimingsDurationMilliseconds)}
158
161
  </td>
159
- {{else}}
160
- <td colspan='2'></td>
161
162
  {{/if}}
162
163
 
163
164
  {{each page.CustomTimingNames}}
164
- {{if timing.CustomTimings[$value]}}
165
+ {{if timing.CustomTimings && timing.CustomTimings[$value]}}
165
166
  <td class="profiler-duration" title="aggregate number of all ${$value.toLowerCase()} invocations in this step (excludes children)">
166
167
  ${timing.CustomTimings[$value].length} ${$value.toLowerCase()}
167
168
  </td>
@@ -1 +1 @@
1
- <script async type="text/javascript" id="mini-profiler" src="{path}includes.js?v={version}" data-version="{version}" data-path="{path}" data-current-id="{currentId}" data-ids="{ids}" data-position="{position}" data-trivial="{showTrivial}" data-children="{showChildren}" data-max-traces="{maxTracesToShow}" data-controls="{showControls}" data-authorized="{authorized}"></script>
1
+ <script async type="text/javascript" id="mini-profiler" src="{path}includes.js?v={version}" data-version="{version}" data-path="{path}" data-current-id="{currentId}" data-ids="{ids}" data-position="{position}" data-trivial="{showTrivial}" data-children="{showChildren}" data-max-traces="{maxTracesToShow}" data-controls="{showControls}" data-authorized="{authorized}" data-toggle-shortcut="{toggleShortcut}" data-start-hidden="{startHidden}"></script>
@@ -14,7 +14,8 @@ module Rack
14
14
 
15
15
  attr_accessor :auto_inject, :base_url_path, :pre_authorize_cb, :position,
16
16
  :backtrace_remove, :backtrace_includes, :backtrace_ignores, :skip_schema_queries,
17
- :storage, :user_provider, :storage_instance, :storage_options, :skip_paths, :authorization_mode
17
+ :storage, :user_provider, :storage_instance, :storage_options, :skip_paths, :authorization_mode,
18
+ :toggle_shortcut, :start_hidden
18
19
 
19
20
  # Deprecated options
20
21
  attr_accessor :use_existing_jquery
@@ -33,6 +34,8 @@ module Rack
33
34
  @storage = MiniProfiler::MemoryStore
34
35
  @user_provider = Proc.new{|env| Rack::Request.new(env).ip}
35
36
  @authorization_mode = :allow_all
37
+ @toggle_shortcut = 'Alt+P'
38
+ @start_hidden = false
36
39
  self
37
40
  }
38
41
  end
@@ -1,10 +1,11 @@
1
- class Rack::MiniProfiler::Context
2
- attr_accessor :inject_js,:current_timer,:page_struct,:skip_backtrace,:full_backtrace,:discard, :mpt_init
3
-
4
- def initialize(opts = {})
5
- opts.each do |k,v|
6
- self.instance_variable_set('@' + k, v)
7
- end
8
- end
9
-
10
- end
1
+ class Rack::MiniProfiler::Context
2
+ attr_accessor :inject_js,:current_timer,:page_struct,:skip_backtrace,:full_backtrace,:discard, :mpt_init, :measure
3
+
4
+ def initialize(opts = {})
5
+ opts["measure"] = true unless opts.key? "measure"
6
+ opts.each do |k,v|
7
+ self.instance_variable_set('@' + k, v)
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,54 @@
1
+ # inspired by https://github.com/brendangregg/FlameGraph
2
+
3
+ class Rack::MiniProfiler::FlameGraph
4
+ def initialize(stacks)
5
+ @stacks = stacks
6
+ end
7
+
8
+ def graph_data
9
+ height = 0
10
+
11
+ table = []
12
+ prev = []
13
+
14
+ # a 2d array makes collapsing easy
15
+ @stacks.each_with_index do |stack, pos|
16
+ col = []
17
+
18
+ stack.reverse.map{|r| r.to_s}.each_with_index do |frame, i|
19
+
20
+ if !prev[i].nil?
21
+ last_col = prev[i]
22
+ if last_col[0] == frame
23
+ last_col[1] += 1
24
+ col << nil
25
+ next
26
+ end
27
+ end
28
+
29
+ prev[i] = [frame, 1]
30
+ col << prev[i]
31
+ end
32
+ prev = prev[0..col.length-1].to_a
33
+ table << col
34
+ end
35
+
36
+ data = []
37
+
38
+ # a 1d array makes rendering easy
39
+ table.each_with_index do |col, col_num|
40
+ col.each_with_index do |row, row_num|
41
+ next unless row && row.length == 2
42
+ data << {
43
+ :x => col_num + 1,
44
+ :y => row_num + 1,
45
+ :width => row[1],
46
+ :frame => row[0]
47
+ }
48
+ end
49
+ end
50
+
51
+ data
52
+ end
53
+
54
+ end
@@ -5,10 +5,14 @@ class Rack::MiniProfiler::GCProfiler
5
5
  ids = Set.new
6
6
  i=0
7
7
  ObjectSpace.each_object { |o|
8
- i = stats[o.class] || 0
9
- i += 1
10
- stats[o.class] = i
11
- ids << o.object_id if Integer === o.object_id
8
+ begin
9
+ i = stats[o.class] || 0
10
+ i += 1
11
+ stats[o.class] = i
12
+ ids << o.object_id if Integer === o.object_id
13
+ rescue NoMethodError
14
+ # Redis::Future undefines .class and .object_id super weird
15
+ end
12
16
  }
13
17
  {:stats => stats, :ids => ids}
14
18
  end
@@ -18,13 +18,14 @@ require 'mini_profiler/profiling_methods'
18
18
  require 'mini_profiler/context'
19
19
  require 'mini_profiler/client_settings'
20
20
  require 'mini_profiler/gc_profiler'
21
+ require 'mini_profiler/flame_graph'
21
22
 
22
23
  module Rack
23
24
 
24
25
  class MiniProfiler
25
-
26
- class << self
27
-
26
+
27
+ class << self
28
+
28
29
  include Rack::MiniProfiler::ProfilingMethods
29
30
 
30
31
  def generate_id
@@ -44,7 +45,7 @@ module Rack
44
45
  return @share_template unless @share_template.nil?
45
46
  @share_template = ::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__)))
46
47
  end
47
-
48
+
48
49
  def current
49
50
  Thread.current[:mini_profiler_private]
50
51
  end
@@ -79,104 +80,82 @@ module Rack
79
80
  Thread.current[:mp_authorized]
80
81
  end
81
82
 
82
- # Add a custom timing. These are displayed similar to SQL/query time in
83
- # columns expanding to the right.
84
- #
85
- # type - String counter type. Each distinct type gets its own column.
86
- # duration_ms - Duration of the call in ms. Either this or a block must be
87
- # given but not both.
88
- #
89
- # When a block is given, calculate the duration by yielding to the block
90
- # and keeping a record of its run time.
91
- #
92
- # Returns the result of the block, or nil when no block is given.
93
- def counter(type, duration_ms=nil)
94
- result = nil
95
- if block_given?
96
- start = Time.now
97
- result = yield
98
- duration_ms = (Time.now - start).to_f * 1000
99
- end
100
- return result if current.nil? || !request_authorized?
101
- current.current_timer.add_custom(type, duration_ms, current.page_struct)
102
- result
103
- end
104
83
  end
105
84
 
106
- #
107
- # options:
108
- # :auto_inject - should script be automatically injected on every html page (not xhr)
109
- def initialize(app, config = nil)
85
+ #
86
+ # options:
87
+ # :auto_inject - should script be automatically injected on every html page (not xhr)
88
+ def initialize(app, config = nil)
110
89
  MiniProfiler.config.merge!(config)
111
- @config = MiniProfiler.config
112
- @app = app
113
- @config.base_url_path << "/" unless @config.base_url_path.end_with? "/"
90
+ @config = MiniProfiler.config
91
+ @app = app
92
+ @config.base_url_path << "/" unless @config.base_url_path.end_with? "/"
114
93
  unless @config.storage_instance
115
94
  @config.storage_instance = @config.storage.new(@config.storage_options)
116
95
  end
117
- @storage = @config.storage_instance
118
- end
119
-
96
+ @storage = @config.storage_instance
97
+ end
98
+
120
99
  def user(env)
121
100
  @config.user_provider.call(env)
122
101
  end
123
102
 
124
- def serve_results(env)
125
- request = Rack::Request.new(env)
103
+ def serve_results(env)
104
+ request = Rack::Request.new(env)
126
105
  id = request['id']
127
- page_struct = @storage.load(id)
106
+ page_struct = @storage.load(id)
128
107
  unless page_struct
129
- @storage.set_viewed(user(env), id)
130
- return [404, {}, ["Request not found: #{request['id']} - user #{user(env)}"]]
108
+ @storage.set_viewed(user(env), id)
109
+ return [404, {}, ["Request not found: #{request['id']} - user #{user(env)}"]]
131
110
  end
132
- unless page_struct['HasUserViewed']
111
+ unless page_struct['HasUserViewed']
133
112
  page_struct['ClientTimings'] = ClientTimerStruct.init_from_form_data(env, page_struct)
134
- page_struct['HasUserViewed'] = true
135
- @storage.save(page_struct)
136
- @storage.set_viewed(user(env), id)
137
- end
113
+ page_struct['HasUserViewed'] = true
114
+ @storage.save(page_struct)
115
+ @storage.set_viewed(user(env), id)
116
+ end
138
117
 
139
118
  result_json = page_struct.to_json
140
119
  # If we're an XMLHttpRequest, serve up the contents as JSON
141
120
  if request.xhr?
142
- [200, { 'Content-Type' => 'application/json'}, [result_json]]
121
+ [200, { 'Content-Type' => 'application/json'}, [result_json]]
143
122
  else
144
123
 
145
124
  # Otherwise give the HTML back
146
- html = MiniProfiler.share_template.dup
147
- html.gsub!(/\{path\}/, @config.base_url_path)
148
- html.gsub!(/\{version\}/, MiniProfiler::VERSION)
125
+ html = MiniProfiler.share_template.dup
126
+ html.gsub!(/\{path\}/, @config.base_url_path)
127
+ html.gsub!(/\{version\}/, MiniProfiler::VERSION)
149
128
  html.gsub!(/\{json\}/, result_json)
150
129
  html.gsub!(/\{includes\}/, get_profile_script(env))
151
130
  html.gsub!(/\{name\}/, page_struct['Name'])
152
131
  html.gsub!(/\{duration\}/, "%.1f" % page_struct.duration_ms)
153
-
132
+
154
133
  [200, {'Content-Type' => 'text/html'}, [html]]
155
134
  end
156
135
 
157
- end
136
+ end
158
137
 
159
- def serve_html(env)
160
- file_name = env['PATH_INFO'][(@config.base_url_path.length)..1000]
161
- return serve_results(env) if file_name.eql?('results')
162
- full_path = ::File.expand_path("../html/#{file_name}", ::File.dirname(__FILE__))
163
- return [404, {}, ["Not found"]] unless ::File.exists? full_path
164
- f = Rack::File.new nil
165
- f.path = full_path
138
+ def serve_html(env)
139
+ file_name = env['PATH_INFO'][(@config.base_url_path.length)..1000]
140
+ return serve_results(env) if file_name.eql?('results')
141
+ full_path = ::File.expand_path("../html/#{file_name}", ::File.dirname(__FILE__))
142
+ return [404, {}, ["Not found"]] unless ::File.exists? full_path
143
+ f = Rack::File.new nil
144
+ f.path = full_path
166
145
 
167
- begin
146
+ begin
168
147
  f.cache_control = "max-age:86400"
169
148
  f.serving env
170
149
  rescue
171
- # old versions of rack have a different api
150
+ # old versions of rack have a different api
172
151
  status, headers, body = f.serving
173
152
  headers.merge! 'Cache-Control' => "max-age:86400"
174
153
  [status, headers, body]
175
154
  end
176
155
 
177
- end
156
+ end
157
+
178
158
 
179
-
180
159
  def current
181
160
  MiniProfiler.current
182
161
  end
@@ -191,7 +170,7 @@ module Rack
191
170
  end
192
171
 
193
172
 
194
- def call(env)
173
+ def call(env)
195
174
 
196
175
  client_settings = ClientSettings.new(env)
197
176
 
@@ -201,20 +180,20 @@ module Rack
201
180
 
202
181
  skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env)) ||
203
182
  (@config.skip_paths && @config.skip_paths.any?{ |p| path[0,p.length] == p}) ||
204
- query_string =~ /pp=skip/
205
-
183
+ query_string =~ /pp=skip/
184
+
206
185
  has_profiling_cookie = client_settings.has_cookie?
207
-
186
+
208
187
  if skip_it || (@config.authorization_mode == :whitelist && !has_profiling_cookie)
209
188
  status,headers,body = @app.call(env)
210
- if !skip_it && @config.authorization_mode == :whitelist && !has_profiling_cookie && MiniProfiler.request_authorized?
211
- client_settings.write!(headers)
189
+ if !skip_it && @config.authorization_mode == :whitelist && !has_profiling_cookie && MiniProfiler.request_authorized?
190
+ client_settings.write!(headers)
212
191
  end
213
192
  return [status,headers,body]
214
193
  end
215
194
 
216
195
  # handle all /mini-profiler requests here
217
- return serve_html(env) if path.start_with? @config.base_url_path
196
+ return serve_html(env) if path.start_with? @config.base_url_path
218
197
 
219
198
  has_disable_cookie = client_settings.disable_profiling?
220
199
  # manual session disable / enable
@@ -236,19 +215,13 @@ module Rack
236
215
  end
237
216
 
238
217
  if query_string =~ /pp=profile-gc/
239
- # begin
240
- if query_string =~ /pp=profile-gc-time/
241
- return Rack::MiniProfiler::GCProfiler.new.profile_gc_time(@app, env)
242
- else
243
- return Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env)
244
- end
245
- # rescue => e
246
- # p e
247
- # e.backtrace.each do |s|
248
- # puts s
249
- # end
250
- # end
218
+ if query_string =~ /pp=profile-gc-time/
219
+ return Rack::MiniProfiler::GCProfiler.new.profile_gc_time(@app, env)
220
+ else
221
+ return Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env)
222
+ end
251
223
  end
224
+
252
225
  MiniProfiler.create_current(env, @config)
253
226
  MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
254
227
  if query_string =~ /pp=normal-backtrace/
@@ -266,31 +239,26 @@ module Rack
266
239
  done_sampling = false
267
240
  quit_sampler = false
268
241
  backtraces = nil
269
- stacktrace_installed = true
270
- if query_string =~ /pp=sample/
242
+
243
+ if query_string =~ /pp=sample/ || query_string =~ /pp=flamegraph/
244
+ current.measure = false
271
245
  skip_frames = 0
272
246
  backtraces = []
273
247
  t = Thread.current
274
-
275
- begin
276
- require 'stacktrace'
277
- skip_frames = stacktrace.length
278
- rescue LoadError
279
- stacktrace_installed = false
280
- end
281
248
 
282
249
  Thread.new {
250
+ # new in Ruby 2.0
251
+ has_backtrace_locations = t.respond_to?(:backtrace_locations)
283
252
  begin
284
- i = 10000 # for sanity never grab more than 10k samples
253
+ i = 10000 # for sanity never grab more than 10k samples
285
254
  while i > 0
286
255
  break if done_sampling
287
256
  i -= 1
288
- if stacktrace_installed
289
- backtraces << t.stacktrace(0,-(1+skip_frames), StackFrame::Flags::METHOD | StackFrame::Flags::KLASS)
290
- else
291
- backtraces << t.backtrace
292
- end
293
- sleep 0.001
257
+ backtraces << (has_backtrace_locations ? t.backtrace_locations : t.backtrace)
258
+
259
+ # On my machine using Ruby 2.0 this give me excellent fidelity of stack trace per 1.2ms
260
+ # with this fidelity analysis becomes very powerful
261
+ sleep 0.0005
294
262
  end
295
263
  ensure
296
264
  quit_sampler = true
@@ -298,11 +266,11 @@ module Rack
298
266
  }
299
267
  end
300
268
 
301
- status, headers, body = nil
302
- start = Time.now
303
- begin
269
+ status, headers, body = nil
270
+ start = Time.now
271
+ begin
304
272
 
305
- # Strip all the caching headers so we don't get 304s back
273
+ # Strip all the caching headers so we don't get 304s back
306
274
  # This solves a very annoying bug where rack mini profiler never shows up
307
275
  env['HTTP_IF_MODIFIED_SINCE'] = nil
308
276
  env['HTTP_IF_NONE_MATCH'] = nil
@@ -310,17 +278,18 @@ module Rack
310
278
  status,headers,body = @app.call(env)
311
279
  client_settings.write!(headers)
312
280
  ensure
313
- if backtraces
281
+ if backtraces
314
282
  done_sampling = true
315
283
  sleep 0.001 until quit_sampler
316
284
  end
317
285
  end
318
286
 
319
287
  skip_it = current.discard
288
+
320
289
  if (config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
321
290
  # this is non-obvious, don't kill the profiling cookie on errors or short requests
322
291
  # this ensures that stuff that never reaches the rails stack does not kill profiling
323
- if status == 200 && ((Time.now - start) > 0.1)
292
+ if status == 200 && ((Time.now - start) > 0.1)
324
293
  client_settings.discard_cookie!(headers)
325
294
  end
326
295
  skip_it = true
@@ -338,44 +307,49 @@ module Rack
338
307
  body.close if body.respond_to? :close
339
308
  return help(client_settings)
340
309
  end
341
-
310
+
342
311
  page_struct = current.page_struct
343
312
  page_struct['User'] = user(env)
344
- page_struct['Root'].record_time((Time.now - start) * 1000)
313
+ page_struct['Root'].record_time((Time.now - start) * 1000)
345
314
 
346
315
  if backtraces
347
316
  body.close if body.respond_to? :close
348
- return analyze(backtraces, page_struct)
317
+ if query_string =~ /pp=sample/
318
+ return analyze(backtraces, page_struct)
319
+ else
320
+ return flame_graph(backtraces, page_struct)
321
+ end
349
322
  end
350
-
323
+
351
324
 
352
325
  # no matter what it is, it should be unviewed, otherwise we will miss POST
353
326
  @storage.set_unviewed(page_struct['User'], page_struct['Id'])
354
- @storage.save(page_struct)
355
-
327
+ @storage.save(page_struct)
328
+
356
329
  # inject headers, script
357
- if status == 200
330
+ if status == 200
358
331
 
359
332
  client_settings.write!(headers)
360
-
333
+
361
334
  # mini profiler is meddling with stuff, we can not cache cause we will get incorrect data
362
335
  # Rack::ETag has already inserted some nonesense in the chain
363
336
  headers.delete('ETag')
364
337
  headers.delete('Date')
365
338
  headers['Cache-Control'] = 'must-revalidate, private, max-age=0'
366
339
 
367
- # inject header
340
+ # inject header
368
341
  if headers.is_a? Hash
369
342
  headers['X-MiniProfiler-Ids'] = ids_json(env)
370
343
  end
371
344
 
372
- # inject script
373
- if current.inject_js \
374
- && headers.has_key?('Content-Type') \
375
- && !headers['Content-Type'].match(/text\/html/).nil? then
376
-
345
+ # inject script
346
+ if current.inject_js \
347
+ && headers.has_key?('Content-Type') \
348
+ && !headers['Content-Type'].match(/text\/html/).nil? then
349
+
377
350
  response = Rack::Response.new([], status, headers)
378
351
  script = self.get_profile_script(env)
352
+
379
353
  if String === body
380
354
  response.write inject(body,script)
381
355
  else
@@ -383,15 +357,15 @@ module Rack
383
357
  end
384
358
  body.close if body.respond_to? :close
385
359
  return response.finish
386
- end
387
- end
360
+ end
361
+ end
388
362
 
389
363
  client_settings.write!(headers)
390
- [status, headers, body]
364
+ [status, headers, body]
391
365
  ensure
392
366
  # Make sure this always happens
393
367
  current = nil
394
- end
368
+ end
395
369
 
396
370
  def inject(fragment, script)
397
371
  if fragment.match(/<\/body>/i)
@@ -410,23 +384,43 @@ module Rack
410
384
  return fragment + script
411
385
  end
412
386
 
413
- fragment.sub(regex) do
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)
387
+ matches = fragment.scan(regex).length
388
+ index = 1
389
+ fragment.gsub(regex) do
390
+ # though malformed there is an edge case where /body exists earlier in the html, work around
391
+ if index < matches
392
+ index += 1
393
+ close_tag
418
394
  else
419
- script + close_tag
395
+
396
+ # if for whatever crazy reason we dont get a utf string,
397
+ # just force the encoding, no utf in the mp scripts anyway
398
+ if script.respond_to?(:encoding) && script.respond_to?(:force_encoding)
399
+ (script + close_tag).force_encoding(fragment.encoding)
400
+ else
401
+ script + close_tag
402
+ end
420
403
  end
421
404
  end
422
405
  end
423
406
 
424
407
  def dump_env(env)
425
408
  headers = {'Content-Type' => 'text/plain'}
426
- body = ""
409
+ body = "Rack Environment\n---------------\n"
427
410
  env.each do |k,v|
428
411
  body << "#{k}: #{v}\n"
429
412
  end
413
+
414
+ body << "\n\nEnvironment\n---------------\n"
415
+ ENV.each do |k,v|
416
+ body << "#{k}: #{v}\n"
417
+ end
418
+
419
+ body << "\n\nInternals\n---------------\n"
420
+ body << "Storage Provider #{config.storage_instance}\n"
421
+ body << "User #{user(env)}\n"
422
+ body << config.storage_instance.diagnostics(user(env)) rescue "no diagnostics implemented for storage"
423
+
430
424
  [200, headers, [body]]
431
425
  end
432
426
 
@@ -439,29 +433,42 @@ module Rack
439
433
  pp=skip : skip mini profiler for this request
440
434
  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)
441
435
  pp=normal-backtrace #{"(*) " if client_settings.backtrace_default?}: collect stack traces from all the SQL executed and filter normally
442
- pp=full-backtrace #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use pp=normal-backtrace to disable)
443
- pp=sample : sample stack traces and return a report isolating heavy usage (experimental works best with the stacktrace gem)
444
- pp=disable : disable profiling for this session
436
+ pp=full-backtrace #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use pp=normal-backtrace to disable)
437
+ pp=sample : sample stack traces and return a report isolating heavy usage (works best on Ruby 2.0)
438
+ pp=disable : disable profiling for this session
445
439
  pp=enable : enable profiling for this session (if previously disabled)
446
440
  pp=profile-gc: perform gc profiling on this request, analyzes ObjectSpace generated by request (ruby 1.9.3 only)
447
441
  pp=profile-gc-time: perform built-in gc profiling on this request (ruby 1.9.3 only)
442
+ pp=flamegraph: works best on Ruby 2.0, a graph representing sampled activity.
448
443
  "
449
-
444
+
450
445
  client_settings.write!(headers)
451
446
  [200, headers, [body]]
452
447
  end
453
448
 
449
+ def flame_graph(traces, page_struct)
450
+ graph = FlameGraph.new(traces)
451
+ data = graph.graph_data
452
+
453
+ headers = {'Content-Type' => 'text/html'}
454
+
455
+ body = IO.read(::File.expand_path('../html/flamegraph.html', ::File.dirname(__FILE__)))
456
+ body.gsub!("/*DATA*/", ::JSON.generate(data));
457
+
458
+ [200, headers, [body]]
459
+ end
460
+
454
461
  def analyze(traces, page_struct)
455
462
  headers = {'Content-Type' => 'text/plain'}
456
463
  body = "Collected: #{traces.count} stack traces. Duration(ms): #{page_struct.duration_ms}"
457
464
 
458
465
  seen = {}
459
466
  fulldump = ""
460
- traces.each do |trace|
467
+ traces.each do |trace|
461
468
  fulldump << "\n\n"
462
469
  distinct = {}
463
470
  trace.each do |frame|
464
- frame = "#{frame.klass}::#{frame.method}" unless String === frame
471
+ frame = frame.to_s unless String === frame
465
472
  unless distinct[frame]
466
473
  distinct[frame] = true
467
474
  seen[frame] ||= 0
@@ -477,7 +484,7 @@ module Rack
477
484
  body << "#{name} x #{count}\n"
478
485
  end
479
486
  end
480
-
487
+
481
488
  body << "\n\n\nRaw traces \n"
482
489
  body << fulldump
483
490
 
@@ -496,40 +503,42 @@ module Rack
496
503
  ids.join(",")
497
504
  end
498
505
 
499
- # get_profile_script returns script to be injected inside current html page
500
- # By default, profile_script is appended to the end of all html requests automatically.
501
- # Calling get_profile_script cancels automatic append for the current page
502
- # Use it when:
503
- # * you have disabled auto append behaviour throught :auto_inject => false flag
504
- # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
505
- def get_profile_script(env)
506
- ids = ids_comma_separated(env)
507
- path = @config.base_url_path
508
- version = MiniProfiler::VERSION
509
- position = @config.position
510
- showTrivial = false
511
- showChildren = false
512
- maxTracesToShow = 10
513
- showControls = false
514
- currentId = current.page_struct["Id"]
515
- authorized = true
516
- # TODO : cache this snippet
517
- script = IO.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
518
- # replace the variables
519
- [:ids, :path, :version, :position, :showTrivial, :showChildren, :maxTracesToShow, :showControls, :currentId, :authorized].each do |v|
520
- regex = Regexp.new("\\{#{v.to_s}\\}")
521
- script.gsub!(regex, eval(v.to_s).to_s)
522
- end
523
- current.inject_js = false
524
- script
525
- end
526
-
527
- # cancels automatic injection of profile script for the current page
528
- def cancel_auto_inject(env)
529
- current.inject_js = false
530
- end
531
-
532
- end
506
+ # get_profile_script returns script to be injected inside current html page
507
+ # By default, profile_script is appended to the end of all html requests automatically.
508
+ # Calling get_profile_script cancels automatic append for the current page
509
+ # Use it when:
510
+ # * you have disabled auto append behaviour throught :auto_inject => false flag
511
+ # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
512
+ def get_profile_script(env)
513
+ ids = ids_comma_separated(env)
514
+ path = @config.base_url_path
515
+ version = MiniProfiler::VERSION
516
+ position = @config.position
517
+ showTrivial = false
518
+ showChildren = false
519
+ maxTracesToShow = 10
520
+ showControls = false
521
+ currentId = current.page_struct["Id"]
522
+ authorized = true
523
+ toggleShortcut = @config.toggle_shortcut
524
+ startHidden = @config.start_hidden
525
+ # TODO : cache this snippet
526
+ script = IO.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
527
+ # replace the variables
528
+ [:ids, :path, :version, :position, :showTrivial, :showChildren, :maxTracesToShow, :showControls, :currentId, :authorized, :toggleShortcut, :startHidden].each do |v|
529
+ regex = Regexp.new("\\{#{v.to_s}\\}")
530
+ script.gsub!(regex, eval(v.to_s).to_s)
531
+ end
532
+ current.inject_js = false
533
+ script
534
+ end
535
+
536
+ # cancels automatic injection of profile script for the current page
537
+ def cancel_auto_inject(env)
538
+ current.inject_js = false
539
+ end
540
+
541
+ end
533
542
 
534
543
  end
535
544