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.
- checksums.yaml +7 -0
- data/Ruby/CHANGELOG +55 -35
- data/Ruby/README.md +19 -1
- data/Ruby/lib/html/includes.css +451 -75
- data/Ruby/lib/html/includes.js +59 -12
- data/Ruby/lib/html/includes.less +38 -35
- data/Ruby/lib/html/includes.tmpl +4 -3
- data/Ruby/lib/html/profile_handler.js +1 -1
- data/Ruby/lib/mini_profiler/config.rb +4 -1
- data/Ruby/lib/mini_profiler/context.rb +11 -10
- data/Ruby/lib/mini_profiler/flame_graph.rb +54 -0
- data/Ruby/lib/mini_profiler/gc_profiler.rb +8 -4
- data/Ruby/lib/mini_profiler/profiler.rb +179 -170
- data/Ruby/lib/mini_profiler/profiling_methods.rb +131 -108
- data/Ruby/lib/mini_profiler/storage/abstract_store.rb +31 -27
- data/Ruby/lib/mini_profiler/storage/file_store.rb +111 -109
- data/Ruby/lib/mini_profiler/storage/memcache_store.rb +11 -9
- data/Ruby/lib/mini_profiler/storage/memory_store.rb +65 -63
- data/Ruby/lib/mini_profiler/storage/redis_store.rb +14 -4
- data/Ruby/lib/mini_profiler/version.rb +2 -2
- data/Ruby/lib/patches/sql_patches.rb +70 -48
- data/rack-mini-profiler.gemspec +1 -1
- metadata +40 -55
data/Ruby/lib/html/includes.js
CHANGED
@@ -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
|
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
|
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/
|
data/Ruby/lib/html/includes.less
CHANGED
@@ -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
|
-
|
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
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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
|
-
|
371
|
-
|
372
|
-
|
373
|
+
&.profiler-min .profiler-result {
|
374
|
+
display: none;
|
375
|
+
}
|
373
376
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
+
&.profiler-min .profiler-controls span {
|
378
|
+
display: none;
|
379
|
+
}
|
377
380
|
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
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
|
+
}
|
data/Ruby/lib/html/includes.tmpl
CHANGED
@@ -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 < ${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.
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
113
|
-
|
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
|
-
|
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
|
-
|
125
|
-
|
103
|
+
def serve_results(env)
|
104
|
+
request = Rack::Request.new(env)
|
126
105
|
id = request['id']
|
127
|
-
|
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
|
-
|
111
|
+
unless page_struct['HasUserViewed']
|
133
112
|
page_struct['ClientTimings'] = ClientTimerStruct.init_from_form_data(env, page_struct)
|
134
|
-
|
135
|
-
@storage.save(page_struct)
|
136
|
-
@storage.set_viewed(user(env), id)
|
137
|
-
|
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
|
-
|
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
|
-
|
136
|
+
end
|
158
137
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
355
|
-
|
327
|
+
@storage.save(page_struct)
|
328
|
+
|
356
329
|
# inject headers, script
|
357
|
-
|
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
|
-
|
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
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
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
|
-
|
387
|
-
|
360
|
+
end
|
361
|
+
end
|
388
362
|
|
389
363
|
client_settings.write!(headers)
|
390
|
-
|
364
|
+
[status, headers, body]
|
391
365
|
ensure
|
392
366
|
# Make sure this always happens
|
393
367
|
current = nil
|
394
|
-
|
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.
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
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
|
-
|
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 (
|
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 =
|
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
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
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
|
|