rack-mini-profiler 0.1.23 → 0.1.24

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.

@@ -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