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