rack-mini-profiler 2.0.4 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +25 -1
- data/lib/html/includes.css +36 -0
- data/lib/html/includes.js +50 -3
- data/lib/html/includes.scss +32 -4
- data/lib/html/includes.tmpl +72 -0
- data/lib/html/profile_handler.js +1 -1
- data/lib/html/rack-mini-profiler.css +3 -0
- data/lib/html/rack-mini-profiler.js +2 -0
- data/lib/html/vendor.js +9 -1
- data/lib/mini_profiler/asset_version.rb +1 -1
- data/lib/mini_profiler/config.rb +13 -1
- data/lib/mini_profiler/profiler.rb +153 -11
- data/lib/mini_profiler/storage/abstract_store.rb +72 -0
- data/lib/mini_profiler/storage/memory_store.rb +54 -5
- data/lib/mini_profiler/storage/redis_store.rb +134 -0
- data/lib/mini_profiler/timer_struct/page.rb +4 -1
- data/lib/mini_profiler/version.rb +1 -1
- data/lib/mini_profiler_rails/railtie.rb +11 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1603a0c0246f9b132f81e74331425a1217156d62f71f20a06ecd3edca91aef51
|
4
|
+
data.tar.gz: a341e35cf639c11cc61c9ef5fd1ecd25e89ff12da299d3a6bfe8cb479c5dd7d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4244e2738847e0e34b33cebda96f45d79ad82201ce969310bf45f1ad7431561af0d41610c2e8c60a0c64d428032a1e4bea6cc5b02aa71b5462a918474228844a
|
7
|
+
data.tar.gz: b1c0d88134e780bbbb91f711366400585d51d98107d1f35af85ca7c38498f0e23de019f2eb05259e833d8deabf485dc70f1dd581c28e132aba68bfa785967774
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## 2.1.0 - 2020-09-17
|
4
|
+
|
5
|
+
- [FEATURE] Allow assets to be precompiled with Sprockets
|
6
|
+
- [FEATURE] Snapshots sampling (see README in repo)
|
7
|
+
- [FEATURE] Allow `skip_paths` config to contain regular expressions
|
8
|
+
|
3
9
|
## 2.0.4 - 2020-08-04
|
4
10
|
|
5
11
|
- [FIX] webpacker may exist with no config, allow for that
|
data/README.md
CHANGED
@@ -186,6 +186,18 @@ There are two additional `pp` options that can be used to analyze memory which d
|
|
186
186
|
* Use `?pp=profile-gc` to report on Garbage Collection statistics
|
187
187
|
* Use `?pp=analyze-memory` to report on ObjectSpace statistics
|
188
188
|
|
189
|
+
### Snapshots Sampling
|
190
|
+
|
191
|
+
In a complex web application, it's possible for a request to trigger rare conditions that result in poor performance. Mini Profiler ships with a feature to help detect those rare conditions and fix them. It works by enabling invisible profiling on one request every N requests, and saving the performance metrics that are collected during the request (a.k.a snapshot of the request) so that they can be viewed later. To turn this feature on, set the `snapshot_every_n_requests` config to a value larger than 0. The larger the value is, the less frequently requests are profiled.
|
192
|
+
|
193
|
+
Mini Profiler will exclude requests that are made to skippd paths (see `skip_paths` config below) from being sampled. Additionally, if profiling is enabled for a request that later finishes with a non-2xx status code, Mini Profiler will discard the snapshot and not save it (this behavior may change in the future).
|
194
|
+
|
195
|
+
After enabling snapshots sampling, you can see the snapshots that have been collected at `/mini-profiler-resources/snapshots` (or if you changed the `base_url_path` config, substitute `mini-profiler-resources` with your value of the config). You'll see on that page a table where each row represents a group of snapshots with the duration of the worst snapshot in that group. The worst snapshot in a group is defined as the snapshot whose request took longer than all of the snapshots in the same group. Snapshots grouped by HTTP method and path of the request, and if your application is a Rails app, Mini Profiler will try to convert the path to `controller#action` and group by that instead of request path. Clicking on a group will display the snapshots of that group sorted from worst to best. From there, you can click on a snapshot's ID to see the snapshot with all the performance metrics that were collected.
|
196
|
+
|
197
|
+
Access to the snapshots page is restricted to only those who can see the speed badge on their own requests, see the section below this one about access control.
|
198
|
+
|
199
|
+
Mini Profiler will keep a maximum of 1000 snapshots by default, and you can change that via the `snapshots_limit` config. When snapshots reach the configured limit, Mini Profiler will save a new snapshot only if it's worse than at least one of the existing snapshots and delete the best one (i.e. the snapshot whose request took the least time compared to other snapshots).
|
200
|
+
|
189
201
|
## Access control in non-development environments
|
190
202
|
|
191
203
|
rack-mini-profiler is designed with production profiling in mind. To enable that run `Rack::MiniProfiler.authorize_request` once you know a request is allowed to profile.
|
@@ -329,6 +341,15 @@ _Note:_ The GUID (`data-version` and the `?v=` parameter on the `src`) will chan
|
|
329
341
|
#### Using MiniProfiler's built in route for apps without HTML responses
|
330
342
|
MiniProfiler also ships with a `/rack-mini-profiler/requests` route that displays the speed badge on a blank HTML page. This can be useful when profiling an application that does not render HTML.
|
331
343
|
|
344
|
+
#### Register MiniProfiler's assets in the Rails assets pipeline
|
345
|
+
MiniProfiler can be configured so it registers its assets in the assets pipeline. To do that, you'll need to provide a lambda (or proc) to the `assets_url` config (see the below section). The callback will receive 3 arguments which are: `name` represents asset name (currently it's either `rack-mini-profiling.js` or `rack-mini-profiling.css`), `assets_version` is a 32 characters long hash of MiniProfiler's assets, and `env` which is the `env` object of the request. MiniProfiler expects the `assets_url` callback to return a URL from which the asset can be loaded (the return value will be used as a `href`/`src` attribute in the DOM). If the `assets_url` callback is not set (the default) or it returns a non-truthy value, MiniProfiler will fallback to loading assets from its own middleware (`/mini-profiler-resources/*`). The following callback should work for most applications:
|
346
|
+
|
347
|
+
```ruby
|
348
|
+
Rack::MiniProfiler.config.assets_url = ->(name, version, env) {
|
349
|
+
ActionController::Base.helpers.asset_path(name)
|
350
|
+
}
|
351
|
+
```
|
352
|
+
|
332
353
|
### Configuration Options
|
333
354
|
|
334
355
|
You can set configuration options using the configuration accessor on `Rack::MiniProfiler`.
|
@@ -344,7 +365,7 @@ Option|Default|Description
|
|
344
365
|
-------|---|--------
|
345
366
|
pre_authorize_cb|Rails: dev only<br>Rack: always on|A lambda callback that returns true to make mini_profiler visible on a given request.
|
346
367
|
position|`'top-left'`|Display mini_profiler on `'top-right'`, `'top-left'`, `'bottom-right'` or `'bottom-left'`.
|
347
|
-
skip_paths|`[]`|
|
368
|
+
skip_paths|`[]`|An array of paths that skip profiling. Both `String` and `Regexp` are acceptable in the array.
|
348
369
|
skip_schema_queries|Rails dev: `true`<br>Othwerwise: `false`|`true` to skip schema queries.
|
349
370
|
auto_inject|`true`|`true` to inject the miniprofiler script in the page.
|
350
371
|
backtrace_ignores|`[]`|Regexes of lines to be removed from backtraces.
|
@@ -360,6 +381,9 @@ max_traces_to_show|20|Maximum number of mini profiler timing blocks to show on o
|
|
360
381
|
html_container|`body`|The HTML container (as a jQuery selector) to inject the mini_profiler UI into
|
361
382
|
show_total_sql_count|`false`|Displays the total number of SQL executions.
|
362
383
|
enable_advanced_debugging_tools|`false`|Enables sensitive debugging tools that can be used via the UI. In production we recommend keeping this disabled as memory and environment debugging tools can expose contents of memory that may contain passwords.
|
384
|
+
assets_url|`nil`|See the "Register MiniProfiler's assets in the Rails assets pipeline" section above.
|
385
|
+
snapshot_every_n_requests|`-1`|Determines how frequently snapshots are taken. See the "Snapshots Sampling" above for more details.
|
386
|
+
snapshots_limit|`1000`|Determines how many snapshots Mini Profiler is allowed to keep.
|
363
387
|
|
364
388
|
### Using MiniProfiler with `Rack::Deflate` middleware
|
365
389
|
|
data/lib/html/includes.css
CHANGED
@@ -1,9 +1,20 @@
|
|
1
1
|
@charset "UTF-8";
|
2
|
+
.mp-snapshots,
|
2
3
|
.profiler-result,
|
3
4
|
.profiler-queries {
|
4
5
|
color: #555;
|
5
6
|
line-height: 1;
|
6
7
|
font-size: 12px; }
|
8
|
+
.mp-snapshots pre,
|
9
|
+
.mp-snapshots code,
|
10
|
+
.mp-snapshots label,
|
11
|
+
.mp-snapshots table,
|
12
|
+
.mp-snapshots tbody,
|
13
|
+
.mp-snapshots thead,
|
14
|
+
.mp-snapshots tfoot,
|
15
|
+
.mp-snapshots tr,
|
16
|
+
.mp-snapshots th,
|
17
|
+
.mp-snapshots td,
|
7
18
|
.profiler-result pre,
|
8
19
|
.profiler-result code,
|
9
20
|
.profiler-result label,
|
@@ -33,27 +44,40 @@
|
|
33
44
|
background-color: transparent;
|
34
45
|
overflow: visible;
|
35
46
|
max-height: none; }
|
47
|
+
.mp-snapshots table,
|
36
48
|
.profiler-result table,
|
37
49
|
.profiler-queries table {
|
38
50
|
border-collapse: collapse;
|
39
51
|
border-spacing: 0; }
|
52
|
+
.mp-snapshots a,
|
53
|
+
.mp-snapshots a:hover,
|
40
54
|
.profiler-result a,
|
41
55
|
.profiler-result a:hover,
|
42
56
|
.profiler-queries a,
|
43
57
|
.profiler-queries a:hover {
|
44
58
|
cursor: pointer;
|
45
59
|
color: #0077cc; }
|
60
|
+
.mp-snapshots a,
|
46
61
|
.profiler-result a,
|
47
62
|
.profiler-queries a {
|
48
63
|
text-decoration: none; }
|
64
|
+
.mp-snapshots a:hover,
|
49
65
|
.profiler-result a:hover,
|
50
66
|
.profiler-queries a:hover {
|
51
67
|
text-decoration: underline; }
|
68
|
+
.mp-snapshots .custom-fields-title,
|
69
|
+
.profiler-result .custom-fields-title,
|
70
|
+
.profiler-queries .custom-fields-title {
|
71
|
+
color: #555;
|
72
|
+
font: Helvetica, Arial, sans-serif;
|
73
|
+
font-size: 14px; }
|
52
74
|
|
53
75
|
.profiler-result {
|
54
76
|
font-family: Helvetica, Arial, sans-serif; }
|
55
77
|
.profiler-result .profiler-toggle-duration-with-children {
|
56
78
|
float: right; }
|
79
|
+
.profiler-result .profiler-snapshots-page-link {
|
80
|
+
float: left; }
|
57
81
|
.profiler-result table.profiler-client-timings {
|
58
82
|
margin-top: 10px; }
|
59
83
|
.profiler-result .profiler-label {
|
@@ -402,3 +426,15 @@
|
|
402
426
|
background: #ffffbb; }
|
403
427
|
100% {
|
404
428
|
background: #fff; } }
|
429
|
+
|
430
|
+
.mp-snapshots {
|
431
|
+
font-family: Helvetica, Arial, sans-serif;
|
432
|
+
font-size: 16px; }
|
433
|
+
.mp-snapshots .snapshots-table thead {
|
434
|
+
background: #6a737c;
|
435
|
+
color: #ffffff; }
|
436
|
+
.mp-snapshots .snapshots-table th, .mp-snapshots .snapshots-table td {
|
437
|
+
padding: 5px;
|
438
|
+
box-sizing: border-box; }
|
439
|
+
.mp-snapshots .snapshots-table th {
|
440
|
+
border-right: 1px solid #ffffff; }
|
data/lib/html/includes.js
CHANGED
@@ -673,6 +673,19 @@ var MiniProfiler = (function() {
|
|
673
673
|
});
|
674
674
|
};
|
675
675
|
|
676
|
+
var initSnapshots = function initSnapshots(dataElement) {
|
677
|
+
var data = JSON.parse(dataElement.textContent);
|
678
|
+
var temp = document.createElement("DIV");
|
679
|
+
if (data.page === "overview") {
|
680
|
+
temp.innerHTML = MiniProfiler.templates.snapshotsGroupsList(data);
|
681
|
+
} else if (data.group_name) {
|
682
|
+
temp.innerHTML = MiniProfiler.templates.snapshotsList(data);
|
683
|
+
}
|
684
|
+
Array.from(temp.children).forEach(function (child) {
|
685
|
+
document.body.appendChild(child);
|
686
|
+
});
|
687
|
+
};
|
688
|
+
|
676
689
|
var initControls = function initControls(container) {
|
677
690
|
if (options.showControls) {
|
678
691
|
var _controls = document.createElement("div");
|
@@ -952,7 +965,7 @@ var MiniProfiler = (function() {
|
|
952
965
|
var script = document.getElementById("mini-profiler");
|
953
966
|
if (!script || !script.getAttribute) return;
|
954
967
|
|
955
|
-
options = (function() {
|
968
|
+
this.options = options = (function() {
|
956
969
|
var version = script.getAttribute("data-version");
|
957
970
|
var path = script.getAttribute("data-path");
|
958
971
|
var currentId = script.getAttribute("data-current-id");
|
@@ -980,6 +993,7 @@ var MiniProfiler = (function() {
|
|
980
993
|
script.getAttribute("data-start-hidden") === "true" ||
|
981
994
|
sessionStorage["rack-mini-profiler-start-hidden"] === "true";
|
982
995
|
var htmlContainer = script.getAttribute("data-html-container");
|
996
|
+
var cssUrl = script.getAttribute("data-css-url");
|
983
997
|
return {
|
984
998
|
ids: ids,
|
985
999
|
path: path,
|
@@ -996,11 +1010,18 @@ var MiniProfiler = (function() {
|
|
996
1010
|
toggleShortcut: toggleShortcut,
|
997
1011
|
startHidden: startHidden,
|
998
1012
|
collapseResults: collapseResults,
|
999
|
-
htmlContainer: htmlContainer
|
1013
|
+
htmlContainer: htmlContainer,
|
1014
|
+
cssUrl: cssUrl
|
1000
1015
|
};
|
1001
1016
|
})();
|
1002
1017
|
|
1003
1018
|
var doInit = function doInit() {
|
1019
|
+
var snapshotsElement = document.getElementById("snapshots-data");
|
1020
|
+
if (snapshotsElement != null) {
|
1021
|
+
initSnapshots(snapshotsElement);
|
1022
|
+
return;
|
1023
|
+
}
|
1024
|
+
|
1004
1025
|
// when rendering a shared, full page, this div will exist
|
1005
1026
|
container = document.querySelectorAll(".profiler-result-full");
|
1006
1027
|
|
@@ -1056,7 +1077,7 @@ var MiniProfiler = (function() {
|
|
1056
1077
|
|
1057
1078
|
var init = function init() {
|
1058
1079
|
if (options.authorized) {
|
1059
|
-
var url = options.
|
1080
|
+
var url = options.cssUrl;
|
1060
1081
|
|
1061
1082
|
if (document.createStyleSheet) {
|
1062
1083
|
document.createStyleSheet(url);
|
@@ -1394,8 +1415,34 @@ var MiniProfiler = (function() {
|
|
1394
1415
|
},
|
1395
1416
|
showTotalSqlCount: function showTotalSqlCount() {
|
1396
1417
|
return options.showTotalSqlCount;
|
1418
|
+
},
|
1419
|
+
timestampToRelative: function timestampToRelative(timestamp) {
|
1420
|
+
var now = Math.round((new Date()).getTime() / 1000);
|
1421
|
+
timestamp = Math.round(timestamp / 1000);
|
1422
|
+
var diff = now - timestamp;
|
1423
|
+
if (diff < 60) {
|
1424
|
+
return "< 1 minute";
|
1425
|
+
}
|
1426
|
+
var buildDisplayTime = function buildDisplayTime(num, unit) {
|
1427
|
+
var res = num + " " + unit;
|
1428
|
+
if (num !== 1) {
|
1429
|
+
res += "s";
|
1430
|
+
}
|
1431
|
+
return res;
|
1432
|
+
}
|
1433
|
+
diff = Math.round(diff / 60);
|
1434
|
+
if (diff <= 60) {
|
1435
|
+
return buildDisplayTime(diff, "minute");
|
1436
|
+
}
|
1437
|
+
diff = Math.round(diff / 60);
|
1438
|
+
if (diff <= 24) {
|
1439
|
+
return buildDisplayTime(diff, "hour");
|
1440
|
+
}
|
1441
|
+
diff = Math.round(diff / 24);
|
1442
|
+
return buildDisplayTime(diff, "day");
|
1397
1443
|
}
|
1398
1444
|
};
|
1399
1445
|
})();
|
1400
1446
|
|
1447
|
+
window.MiniProfiler = MiniProfiler;
|
1401
1448
|
MiniProfiler.init();
|
data/lib/html/includes.scss
CHANGED
@@ -14,9 +14,10 @@ $codeFonts: Consolas, monospace, serif;
|
|
14
14
|
$zindex: 2147483640; // near 32bit max 2147483647
|
15
15
|
|
16
16
|
// do some resets
|
17
|
+
.mp-snapshots,
|
17
18
|
.profiler-result,
|
18
19
|
.profiler-queries {
|
19
|
-
color:
|
20
|
+
color: $textColor;
|
20
21
|
line-height: 1;
|
21
22
|
font-size: 12px;
|
22
23
|
|
@@ -55,6 +56,11 @@ $zindex: 2147483640; // near 32bit max 2147483647
|
|
55
56
|
text-decoration: underline;
|
56
57
|
}
|
57
58
|
}
|
59
|
+
.custom-fields-title {
|
60
|
+
color: $textColor;
|
61
|
+
font: $normalFonts;
|
62
|
+
font-size: 14px;
|
63
|
+
}
|
58
64
|
}
|
59
65
|
|
60
66
|
// styles shared between popup view and full view
|
@@ -62,6 +68,9 @@ $zindex: 2147483640; // near 32bit max 2147483647
|
|
62
68
|
.profiler-toggle-duration-with-children {
|
63
69
|
float: right;
|
64
70
|
}
|
71
|
+
.profiler-snapshots-page-link {
|
72
|
+
float: left;
|
73
|
+
}
|
65
74
|
table.profiler-client-timings {
|
66
75
|
margin-top: 10px;
|
67
76
|
}
|
@@ -199,7 +208,7 @@ $zindex: 2147483640; // near 32bit max 2147483647
|
|
199
208
|
|
200
209
|
th {
|
201
210
|
background-color: #fff;
|
202
|
-
border-bottom: 1px solid
|
211
|
+
border-bottom: 1px solid $textColor;
|
203
212
|
font-weight: bold;
|
204
213
|
padding: 15px;
|
205
214
|
white-space: nowrap;
|
@@ -452,7 +461,7 @@ $zindex: 2147483640; // near 32bit max 2147483647
|
|
452
461
|
line-height: 18px;
|
453
462
|
overflow: auto;
|
454
463
|
|
455
|
-
@include box-shadow(0px, 1px, 15px,
|
464
|
+
@include box-shadow(0px, 1px, 15px, $textColor);
|
456
465
|
|
457
466
|
.profiler-info {
|
458
467
|
margin-bottom: 3px;
|
@@ -592,7 +601,7 @@ $zindex: 2147483640; // near 32bit max 2147483647
|
|
592
601
|
}
|
593
602
|
th {
|
594
603
|
font-size: 16px;
|
595
|
-
color:
|
604
|
+
color: $textColor;
|
596
605
|
line-height: 20px;
|
597
606
|
}
|
598
607
|
|
@@ -617,3 +626,22 @@ $zindex: 2147483640; // near 32bit max 2147483647
|
|
617
626
|
background: #fff;
|
618
627
|
}
|
619
628
|
}
|
629
|
+
|
630
|
+
.mp-snapshots {
|
631
|
+
font-family: $normalFonts;
|
632
|
+
font-size: 16px;
|
633
|
+
|
634
|
+
.snapshots-table {
|
635
|
+
thead {
|
636
|
+
background: #6a737c;
|
637
|
+
color: #ffffff;
|
638
|
+
}
|
639
|
+
th, td {
|
640
|
+
padding: 5px;
|
641
|
+
box-sizing: border-box;
|
642
|
+
}
|
643
|
+
th {
|
644
|
+
border-right: 1px solid #ffffff;
|
645
|
+
}
|
646
|
+
}
|
647
|
+
}
|
data/lib/html/includes.tmpl
CHANGED
@@ -45,6 +45,10 @@
|
|
45
45
|
{{= MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) }}
|
46
46
|
{{?}}
|
47
47
|
<a class="profiler-toggle-duration-with-children" title="toggles column with aggregate child durations">show time with children</a>
|
48
|
+
<a
|
49
|
+
class="profiler-snapshots-page-link"
|
50
|
+
title="Go to snapshots page"
|
51
|
+
href="{{= MiniProfiler.options.path }}snapshots">snapshots</a>
|
48
52
|
</td>
|
49
53
|
{{? it.has_sql_timings}}
|
50
54
|
<td colspan="2" class="profiler-number profiler-percent-in-sql" title="{{= MiniProfiler.getSqlTimingsCount(it.root) }} queries spent {{= MiniProfiler.formatDuration(it.duration_milliseconds_in_sql) }} ms of total request time">
|
@@ -92,6 +96,19 @@
|
|
92
96
|
</tfoot>
|
93
97
|
</table>
|
94
98
|
{{?}}
|
99
|
+
{{? it.custom_fields && Object.keys(it.custom_fields).length > 0 }}
|
100
|
+
<p class="custom-fields-title">Snapshot custom fields</p>
|
101
|
+
<table class="profiler-timings">
|
102
|
+
<tbody>
|
103
|
+
{{~ Object.keys(it.custom_fields) :key }}
|
104
|
+
<tr>
|
105
|
+
<td class="profiler-label">{{= key }}</td>
|
106
|
+
<td class="profiler-label">{{= it.custom_fields[key] }}</td>
|
107
|
+
</tr>
|
108
|
+
{{~}}
|
109
|
+
</tbody>
|
110
|
+
</table>
|
111
|
+
{{?}}
|
95
112
|
</div>
|
96
113
|
</div>
|
97
114
|
|
@@ -216,3 +233,58 @@
|
|
216
233
|
</td>
|
217
234
|
</tr>
|
218
235
|
</script>
|
236
|
+
|
237
|
+
<script id="snapshotsGroupsList" type="text/x-dot-tmpl">
|
238
|
+
{{? it.list && it.list.length }}
|
239
|
+
<table class="snapshots-table">
|
240
|
+
<thead>
|
241
|
+
<tr>
|
242
|
+
<th>Requests Group</th>
|
243
|
+
<th>Worst Time (ms)</th>
|
244
|
+
</tr>
|
245
|
+
</thead>
|
246
|
+
<tbody>
|
247
|
+
{{~ it.list :row}}
|
248
|
+
<tr>
|
249
|
+
<td><a href="{{= row.url }}">{{= row.name }}</a></td>
|
250
|
+
<td>{{= MiniProfiler.formatDuration(row.worst_score) }}</td>
|
251
|
+
</tr>
|
252
|
+
{{~}}
|
253
|
+
</tbody>
|
254
|
+
</table>
|
255
|
+
{{??}}
|
256
|
+
<h2>No snapshots exist</h2>
|
257
|
+
{{?}}
|
258
|
+
</script>
|
259
|
+
|
260
|
+
<script id="snapshotsList" type="text/x-dot-tmpl">
|
261
|
+
{{? it.list && it.list.length }}
|
262
|
+
<h2>Snapshots for {{= it.group_name }}</h2>
|
263
|
+
<table class="snapshots-table">
|
264
|
+
<thead>
|
265
|
+
<tr>
|
266
|
+
<th>ID</th>
|
267
|
+
<th>Duration (ms)</th>
|
268
|
+
<th>Age</th>
|
269
|
+
</tr>
|
270
|
+
</thead>
|
271
|
+
<tbody>
|
272
|
+
{{~ it.list :row}}
|
273
|
+
<tr>
|
274
|
+
<td><a href="{{= row.url }}">
|
275
|
+
{{= row.id }}
|
276
|
+
</a></td>
|
277
|
+
<td>{{= MiniProfiler.formatDuration(row.duration) }}</td>
|
278
|
+
<td>
|
279
|
+
{{? row.timestamp }}
|
280
|
+
{{= MiniProfiler.timestampToRelative(row.timestamp) }}
|
281
|
+
{{?}}
|
282
|
+
</td>
|
283
|
+
</tr>
|
284
|
+
{{~}}
|
285
|
+
</tbody>
|
286
|
+
</table>
|
287
|
+
{{??}}
|
288
|
+
<h2>No snapshots for {{= it.group_name }}</h2>
|
289
|
+
{{?}}
|
290
|
+
</script>
|
data/lib/html/profile_handler.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
<script async type="text/javascript" id="mini-profiler" src="{
|
1
|
+
<script async type="text/javascript" id="mini-profiler" src="{url}" data-css-url="{cssUrl}" data-version="{version}" data-path="{path}" data-current-id="{currentId}" data-ids="{ids}" data-horizontal-position="{horizontalPosition}" data-vertical-position="{verticalPosition}" data-trivial="{showTrivial}" data-children="{showChildren}" data-max-traces="{maxTracesToShow}" data-controls="{showControls}" data-total-sql-count="{showTotalSqlCount}" data-authorized="{authorized}" data-toggle-shortcut="{toggleShortcut}" data-start-hidden="{startHidden}" data-collapse-results="{collapseResults}" data-html-container="{htmlContainer}"></script>
|
data/lib/html/vendor.js
CHANGED
@@ -7,7 +7,7 @@
|
|
7
7
|
MiniProfiler.templates = {};
|
8
8
|
MiniProfiler.templates["profilerTemplate"] = function anonymous(it
|
9
9
|
) {
|
10
|
-
var out=' <div class="profiler-result"> <div class="profiler-button ';if(it.has_duplicate_sql_timings){out+='profiler-warning';}out+='"> ';if(it.has_duplicate_sql_timings){out+='<span class="profiler-nuclear">!</span>';}out+=' <span class="profiler-number"> '+( MiniProfiler.formatDuration(it.duration_milliseconds))+' <span class="profiler-unit">ms</span> </span> ';if(MiniProfiler.showTotalSqlCount()){out+=' <span class="profiler-number"> '+( it.sql_count)+' <span class="profiler-unit">sql</span> </span> ';}out+=' </div> <div class="profiler-popup"> <div class="profiler-info"> <span class="profiler-name"> '+( it.name)+' <span class="profiler-overall-duration">('+( MiniProfiler.formatDuration(it.duration_milliseconds))+' ms)</span> </span> <span class="profiler-server-time">'+( it.machine_name)+' on '+( MiniProfiler.renderDate(it.started))+'</span> </div> <div class="profiler-output"> <table class="profiler-timings"> <thead> <tr> <th></th> <th>duration (ms)</th> <th class="profiler-duration-with-children">with children (ms)</th> <th class="time-from-start">from start (ms)</th> ';if(it.has_sql_timings){out+=' <th colspan="2">query time (ms)</th> ';}out+=' ';var arr1=it.custom_timing_names;if(arr1){var value,i1=-1,l1=arr1.length-1;while(i1<l1){value=arr1[i1+=1];out+=' <th colspan="2">'+( value.toLowerCase() )+' (ms)</th> ';} } out+=' </tr> </thead> <tbody> '+( MiniProfiler.templates.timingTemplate({timing: it.root, page: it}) )+' </tbody> <tfoot> <tr> <td colspan="3"> ';if(!it.client_timings){out+=' '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' ';}out+=' <a class="profiler-toggle-duration-with-children" title="toggles column with aggregate child durations">show time with children</a> </td> ';if(it.has_sql_timings){out+=' <td colspan="2" class="profiler-number profiler-percent-in-sql" title="'+( MiniProfiler.getSqlTimingsCount(it.root) )+' queries spent '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in sql</span> </td> ';}out+=' ';var arr2=it.custom_timing_names;if(arr2){var value,i2=-1,l2=arr2.length-1;while(i2<l2){value=arr2[i2+=1];out+=' <td colspan="2" class="profiler-number profiler-percentage-in-sql" title="'+( it.custom_timing_stats[value].count )+' '+( value.toLowerCase() )+' invocations spent '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in '+( value.toLowerCase() )+'</span> </td> ';} } out+=' </tr> </tfoot> </table> ';if(it.client_timings){out+=' <table class="profiler-timings profiler-client-timings"> <thead> <tr> <th>client event</th> <th>duration (ms)</th> <th>from start (ms)</th> </tr> </thead> <tbody> ';var arr3=MiniProfiler.getClientTimings(it.client_timings);if(arr3){var value,i3=-1,l3=arr3.length-1;while(i3<l3){value=arr3[i3+=1];out+=' <tr class="';if(value.isTrivial){out+='profiler-trivial';}out+='"> <td class="profiler-label">'+( value.name )+'</td> <td class="profiler-duration"> ';if(value.duration >= 0){out+=' <span class="profiler-unit"></span>'+( MiniProfiler.formatDuration(value.duration) )+' ';}out+=' </td> <td class="profiler-duration time-from-start"> <span class="profiler-unit">+</span>'+( MiniProfiler.formatDuration(value.start) )+' </td> </tr> ';} } out+=' </tbody> <tfoot> <td colspan="3"> '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' </td> </tfoot> </table> ';}out+=' </div> </div> ';if(it.has_sql_timings){out+=' <div class="profiler-queries"> <table> <thead> <tr> <th style="text-align:right">step<br />time from start<br />query type<br />duration</th> <th style="text-align:left">call stack<br />query</th> </tr> </thead> <tbody> ';var
|
10
|
+
var out=' <div class="profiler-result"> <div class="profiler-button ';if(it.has_duplicate_sql_timings){out+='profiler-warning';}out+='"> ';if(it.has_duplicate_sql_timings){out+='<span class="profiler-nuclear">!</span>';}out+=' <span class="profiler-number"> '+( MiniProfiler.formatDuration(it.duration_milliseconds))+' <span class="profiler-unit">ms</span> </span> ';if(MiniProfiler.showTotalSqlCount()){out+=' <span class="profiler-number"> '+( it.sql_count)+' <span class="profiler-unit">sql</span> </span> ';}out+=' </div> <div class="profiler-popup"> <div class="profiler-info"> <span class="profiler-name"> '+( it.name)+' <span class="profiler-overall-duration">('+( MiniProfiler.formatDuration(it.duration_milliseconds))+' ms)</span> </span> <span class="profiler-server-time">'+( it.machine_name)+' on '+( MiniProfiler.renderDate(it.started))+'</span> </div> <div class="profiler-output"> <table class="profiler-timings"> <thead> <tr> <th></th> <th>duration (ms)</th> <th class="profiler-duration-with-children">with children (ms)</th> <th class="time-from-start">from start (ms)</th> ';if(it.has_sql_timings){out+=' <th colspan="2">query time (ms)</th> ';}out+=' ';var arr1=it.custom_timing_names;if(arr1){var value,i1=-1,l1=arr1.length-1;while(i1<l1){value=arr1[i1+=1];out+=' <th colspan="2">'+( value.toLowerCase() )+' (ms)</th> ';} } out+=' </tr> </thead> <tbody> '+( MiniProfiler.templates.timingTemplate({timing: it.root, page: it}) )+' </tbody> <tfoot> <tr> <td colspan="3"> ';if(!it.client_timings){out+=' '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' ';}out+=' <a class="profiler-toggle-duration-with-children" title="toggles column with aggregate child durations">show time with children</a> <a class="profiler-snapshots-page-link" title="Go to snapshots page" href="'+( MiniProfiler.options.path )+'snapshots">snapshots</a> </td> ';if(it.has_sql_timings){out+=' <td colspan="2" class="profiler-number profiler-percent-in-sql" title="'+( MiniProfiler.getSqlTimingsCount(it.root) )+' queries spent '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in sql</span> </td> ';}out+=' ';var arr2=it.custom_timing_names;if(arr2){var value,i2=-1,l2=arr2.length-1;while(i2<l2){value=arr2[i2+=1];out+=' <td colspan="2" class="profiler-number profiler-percentage-in-sql" title="'+( it.custom_timing_stats[value].count )+' '+( value.toLowerCase() )+' invocations spent '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in '+( value.toLowerCase() )+'</span> </td> ';} } out+=' </tr> </tfoot> </table> ';if(it.client_timings){out+=' <table class="profiler-timings profiler-client-timings"> <thead> <tr> <th>client event</th> <th>duration (ms)</th> <th>from start (ms)</th> </tr> </thead> <tbody> ';var arr3=MiniProfiler.getClientTimings(it.client_timings);if(arr3){var value,i3=-1,l3=arr3.length-1;while(i3<l3){value=arr3[i3+=1];out+=' <tr class="';if(value.isTrivial){out+='profiler-trivial';}out+='"> <td class="profiler-label">'+( value.name )+'</td> <td class="profiler-duration"> ';if(value.duration >= 0){out+=' <span class="profiler-unit"></span>'+( MiniProfiler.formatDuration(value.duration) )+' ';}out+=' </td> <td class="profiler-duration time-from-start"> <span class="profiler-unit">+</span>'+( MiniProfiler.formatDuration(value.start) )+' </td> </tr> ';} } out+=' </tbody> <tfoot> <td colspan="3"> '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' </td> </tfoot> </table> ';}out+=' ';if(it.custom_fields && Object.keys(it.custom_fields).length > 0){out+=' <p class="custom-fields-title">Snapshot custom fields</p> <table class="profiler-timings"> <tbody> ';var arr4=Object.keys(it.custom_fields);if(arr4){var key,i4=-1,l4=arr4.length-1;while(i4<l4){key=arr4[i4+=1];out+=' <tr> <td class="profiler-label">'+( key )+'</td> <td class="profiler-label">'+( it.custom_fields[key] )+'</td> </tr> ';} } out+=' </tbody> </table> ';}out+=' </div> </div> ';if(it.has_sql_timings){out+=' <div class="profiler-queries"> <table> <thead> <tr> <th style="text-align:right">step<br />time from start<br />query type<br />duration</th> <th style="text-align:left">call stack<br />query</th> </tr> </thead> <tbody> ';var arr5=MiniProfiler.getSqlTimings(it.root);if(arr5){var value,index=-1,l5=arr5.length-1;while(index<l5){value=arr5[index+=1];out+=' '+( MiniProfiler.templates.sqlGapTemplate({g: value.prevGap}) )+' '+( MiniProfiler.templates.sqlTimingTemplate({i: index, s: value}) )+' ';if(value.nextGap){out+=' '+( MiniProfiler.templates.sqlGapTemplate({g: value.nextGap}) )+' ';}out+=' ';} } out+=' </tbody> </table> <p class="profiler-trivial-gap-container"> <a class="profiler-toggle-trivial-gaps">show trivial gaps</a> </p> </div> ';}out+=' </div>';return out;
|
11
11
|
}
|
12
12
|
MiniProfiler.templates["linksTemplate"] = function anonymous(it
|
13
13
|
) {
|
@@ -25,6 +25,14 @@ MiniProfiler.templates["sqlGapTemplate"] = function anonymous(it
|
|
25
25
|
) {
|
26
26
|
var out=' <tr class="profiler-gap-info';if(it.g.duration < 4){out+=' profiler-trivial-gaps';}out+='"> <td class="profiler-info"> '+( it.g.duration )+' <span class="profiler-unit">ms</span> </td> <td class="query"> <div>'+( it.g.topReason.name )+' — '+( it.g.topReason.duration.toFixed(2) )+' <span class="profiler-unit">ms</span></div> </td> </tr>';return out;
|
27
27
|
}
|
28
|
+
MiniProfiler.templates["snapshotsGroupsList"] = function anonymous(it
|
29
|
+
) {
|
30
|
+
var out=' ';if(it.list && it.list.length){out+=' <table class="snapshots-table"> <thead> <tr> <th>Requests Group</th> <th>Worst Time (ms)</th> </tr> </thead> <tbody> ';var arr1=it.list;if(arr1){var row,i1=-1,l1=arr1.length-1;while(i1<l1){row=arr1[i1+=1];out+=' <tr> <td><a href="'+( row.url )+'">'+( row.name )+'</a></td> <td>'+( MiniProfiler.formatDuration(row.worst_score) )+'</td> </tr> ';} } out+=' </tbody> </table> ';}else{out+=' <h2>No snapshots exist</h2> ';}return out;
|
31
|
+
}
|
32
|
+
MiniProfiler.templates["snapshotsList"] = function anonymous(it
|
33
|
+
) {
|
34
|
+
var out=' ';if(it.list && it.list.length){out+=' <h2>Snapshots for '+( it.group_name )+'</h2> <table class="snapshots-table"> <thead> <tr> <th>ID</th> <th>Duration (ms)</th> <th>Age</th> </tr> </thead> <tbody> ';var arr1=it.list;if(arr1){var row,i1=-1,l1=arr1.length-1;while(i1<l1){row=arr1[i1+=1];out+=' <tr> <td><a href="'+( row.url )+'"> '+( row.id )+' </a></td> <td>'+( MiniProfiler.formatDuration(row.duration) )+'</td> <td> ';if(row.timestamp){out+=' '+( MiniProfiler.timestampToRelative(row.timestamp) )+' ';}out+=' </td> </tr> ';} } out+=' </tbody> </table> ';}else{out+=' <h2>No snapshots for '+( it.group_name )+'</h2> ';}return out;
|
35
|
+
}
|
28
36
|
|
29
37
|
if (typeof prettyPrint === "undefined") {
|
30
38
|
// prettify.js
|
data/lib/mini_profiler/config.rb
CHANGED
@@ -37,6 +37,8 @@ module Rack
|
|
37
37
|
@max_sql_param_length = 0 # disable sql parameter collection by default
|
38
38
|
@skip_sql_param_names = /password/ # skips parameters with the name password by default
|
39
39
|
@enable_advanced_debugging_tools = false
|
40
|
+
@snapshot_every_n_requests = -1
|
41
|
+
@snapshots_limit = 1000
|
40
42
|
|
41
43
|
# ui parameters
|
42
44
|
@autorized = true
|
@@ -60,7 +62,8 @@ module Rack
|
|
60
62
|
:base_url_path, :disable_caching, :enabled,
|
61
63
|
:flamegraph_sample_rate, :logger, :pre_authorize_cb, :skip_paths,
|
62
64
|
:skip_schema_queries, :storage, :storage_failure, :storage_instance,
|
63
|
-
:storage_options, :user_provider, :enable_advanced_debugging_tools
|
65
|
+
:storage_options, :user_provider, :enable_advanced_debugging_tools,
|
66
|
+
:snapshot_every_n_requests, :snapshots_limit
|
64
67
|
attr_accessor :skip_sql_param_names, :suppress_encoding, :max_sql_param_length
|
65
68
|
|
66
69
|
# ui accessors
|
@@ -71,6 +74,15 @@ module Rack
|
|
71
74
|
# Deprecated options
|
72
75
|
attr_accessor :use_existing_jquery
|
73
76
|
|
77
|
+
attr_reader :assets_url
|
78
|
+
|
79
|
+
def assets_url=(lmbda)
|
80
|
+
if defined?(Rack::MiniProfilerRails)
|
81
|
+
Rack::MiniProfilerRails.create_engine
|
82
|
+
end
|
83
|
+
@assets_url = lmbda
|
84
|
+
end
|
85
|
+
|
74
86
|
def vertical_position
|
75
87
|
position.include?('bottom') ? 'bottom' : 'top'
|
76
88
|
end
|
@@ -38,9 +38,20 @@ module Rack
|
|
38
38
|
|
39
39
|
def current=(c)
|
40
40
|
# we use TLS cause we need access to this from sql blocks and code blocks that have no access to env
|
41
|
+
Thread.current[:mini_profiler_snapshot_custom_fields] = nil
|
41
42
|
Thread.current[:mini_profiler_private] = c
|
42
43
|
end
|
43
44
|
|
45
|
+
def add_snapshot_custom_field(key, value)
|
46
|
+
thread_var_key = :mini_profiler_snapshot_custom_fields
|
47
|
+
Thread.current[thread_var_key] ||= {}
|
48
|
+
Thread.current[thread_var_key][key] = value
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_snapshot_custom_fields
|
52
|
+
Thread.current[:mini_profiler_snapshot_custom_fields]
|
53
|
+
end
|
54
|
+
|
44
55
|
# discard existing results, don't track this request
|
45
56
|
def discard_results
|
46
57
|
self.current.discard = true if current
|
@@ -106,14 +117,23 @@ module Rack
|
|
106
117
|
def serve_results(env)
|
107
118
|
request = Rack::Request.new(env)
|
108
119
|
id = request.params['id']
|
109
|
-
|
110
|
-
|
120
|
+
is_snapshot = request.params['snapshot']
|
121
|
+
is_snapshot = [true, "true"].include?(is_snapshot)
|
122
|
+
if is_snapshot
|
123
|
+
page_struct = @storage.load_snapshot(id)
|
124
|
+
else
|
125
|
+
page_struct = @storage.load(id)
|
126
|
+
end
|
127
|
+
if !page_struct && is_snapshot
|
128
|
+
id = ERB::Util.html_escape(id)
|
129
|
+
return [404, {}, ["Snapshot with id '#{id}' not found"]]
|
130
|
+
elsif !page_struct
|
111
131
|
@storage.set_viewed(user(env), id)
|
112
|
-
id = ERB::Util.html_escape(
|
132
|
+
id = ERB::Util.html_escape(id)
|
113
133
|
user_info = ERB::Util.html_escape(user(env))
|
114
134
|
return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
|
115
135
|
end
|
116
|
-
|
136
|
+
if !page_struct[:has_user_viewed] && !is_snapshot
|
117
137
|
page_struct[:client_timings] = TimerStruct::Client.init_from_form_data(env, page_struct)
|
118
138
|
page_struct[:has_user_viewed] = true
|
119
139
|
@storage.save(page_struct)
|
@@ -148,11 +168,12 @@ module Rack
|
|
148
168
|
file_name = path.sub(@config.base_url_path, '')
|
149
169
|
|
150
170
|
return serve_results(env) if file_name.eql?('results')
|
171
|
+
return handle_snapshots_request(env) if file_name.eql?('snapshots')
|
151
172
|
|
152
173
|
resources_env = env.dup
|
153
174
|
resources_env['PATH_INFO'] = file_name
|
154
175
|
|
155
|
-
rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => "max-age
|
176
|
+
rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => "max-age=#{cache_control_value}")
|
156
177
|
rack_file.call(resources_env)
|
157
178
|
end
|
158
179
|
|
@@ -177,7 +198,6 @@ module Rack
|
|
177
198
|
end
|
178
199
|
|
179
200
|
def call(env)
|
180
|
-
|
181
201
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
182
202
|
client_settings = ClientSettings.new(env, @storage, start)
|
183
203
|
MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
|
@@ -189,15 +209,31 @@ module Rack
|
|
189
209
|
# Someone (e.g. Rails engine) could change the SCRIPT_NAME so we save it
|
190
210
|
env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME'] = ENV['PASSENGER_BASE_URI'] || env['SCRIPT_NAME']
|
191
211
|
|
192
|
-
skip_it = (
|
193
|
-
|
194
|
-
|
212
|
+
skip_it = /pp=skip/.match?(query_string) || (
|
213
|
+
@config.skip_paths &&
|
214
|
+
@config.skip_paths.any? do |p|
|
215
|
+
if p.instance_of?(String)
|
216
|
+
path.start_with?(p)
|
217
|
+
elsif p.instance_of?(Regexp)
|
218
|
+
p.match?(path)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
)
|
222
|
+
if skip_it
|
223
|
+
return client_settings.handle_cookie(@app.call(env))
|
224
|
+
end
|
225
|
+
|
226
|
+
skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env))
|
195
227
|
|
196
228
|
if skip_it || (
|
197
229
|
@config.authorization_mode == :whitelist &&
|
198
230
|
!client_settings.has_valid_cookie?
|
199
231
|
)
|
200
|
-
|
232
|
+
if take_snapshot?(path)
|
233
|
+
return client_settings.handle_cookie(take_snapshot(env, start))
|
234
|
+
else
|
235
|
+
return client_settings.handle_cookie(@app.call(env))
|
236
|
+
end
|
201
237
|
end
|
202
238
|
|
203
239
|
# handle all /mini-profiler requests here
|
@@ -628,10 +664,20 @@ Append the following to your query string:
|
|
628
664
|
# * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
|
629
665
|
def get_profile_script(env)
|
630
666
|
path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
|
667
|
+
version = MiniProfiler::ASSET_VERSION
|
668
|
+
if @config.assets_url
|
669
|
+
url = @config.assets_url.call('rack-mini-profiler.js', version, env)
|
670
|
+
css_url = @config.assets_url.call('rack-mini-profiler.css', version, env)
|
671
|
+
end
|
672
|
+
|
673
|
+
url = "#{path}includes.js?v=#{version}" if !url
|
674
|
+
css_url = "#{path}includes.css?v=#{version}" if !css_url
|
631
675
|
|
632
676
|
settings = {
|
633
677
|
path: path,
|
634
|
-
|
678
|
+
url: url,
|
679
|
+
cssUrl: css_url,
|
680
|
+
version: version,
|
635
681
|
verticalPosition: @config.vertical_position,
|
636
682
|
horizontalPosition: @config.horizontal_position,
|
637
683
|
showTrivial: @config.show_trivial,
|
@@ -674,5 +720,101 @@ Append the following to your query string:
|
|
674
720
|
def cache_control_value
|
675
721
|
86400
|
676
722
|
end
|
723
|
+
|
724
|
+
private
|
725
|
+
|
726
|
+
def handle_snapshots_request(env)
|
727
|
+
self.current = nil
|
728
|
+
MiniProfiler.authorize_request
|
729
|
+
status = 200
|
730
|
+
headers = { 'Content-Type' => 'text/html' }
|
731
|
+
qp = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
|
732
|
+
if group_name = qp["group_name"]
|
733
|
+
list = @storage.find_snapshots_group(group_name)
|
734
|
+
list.each do |snapshot|
|
735
|
+
snapshot[:url] = url_for_snapshot(snapshot[:id])
|
736
|
+
end
|
737
|
+
data = {
|
738
|
+
group_name: group_name,
|
739
|
+
list: list
|
740
|
+
}
|
741
|
+
else
|
742
|
+
list = @storage.snapshot_groups_overview
|
743
|
+
list.each do |group|
|
744
|
+
group[:url] = url_for_snapshots_group(group[:name])
|
745
|
+
end
|
746
|
+
data = {
|
747
|
+
page: "overview",
|
748
|
+
list: list
|
749
|
+
}
|
750
|
+
end
|
751
|
+
data_html = <<~HTML
|
752
|
+
<div style="display: none;" id="snapshots-data">
|
753
|
+
#{data.to_json}
|
754
|
+
</div>
|
755
|
+
HTML
|
756
|
+
response = Rack::Response.new([], status, headers)
|
757
|
+
|
758
|
+
response.write <<~HTML
|
759
|
+
<html>
|
760
|
+
<head></head>
|
761
|
+
<body class="mp-snapshots">
|
762
|
+
HTML
|
763
|
+
response.write(data_html)
|
764
|
+
script = self.get_profile_script(env)
|
765
|
+
response.write(script)
|
766
|
+
response.write <<~HTML
|
767
|
+
</body>
|
768
|
+
</html>
|
769
|
+
HTML
|
770
|
+
response.finish
|
771
|
+
end
|
772
|
+
|
773
|
+
def rails_route_from_path(path, method)
|
774
|
+
if defined?(Rails) && defined?(ActionController::RoutingError)
|
775
|
+
hash = Rails.application.routes.recognize_path(path, method: method)
|
776
|
+
if hash && hash[:controller] && hash[:action]
|
777
|
+
"#{method} #{hash[:controller]}##{hash[:action]}"
|
778
|
+
end
|
779
|
+
end
|
780
|
+
rescue ActionController::RoutingError
|
781
|
+
nil
|
782
|
+
end
|
783
|
+
|
784
|
+
def url_for_snapshots_group(group_name)
|
785
|
+
qs = Rack::Utils.build_query({ group_name: group_name })
|
786
|
+
"/#{@config.base_url_path.gsub('/', '')}/snapshots?#{qs}"
|
787
|
+
end
|
788
|
+
|
789
|
+
def url_for_snapshot(id)
|
790
|
+
qs = Rack::Utils.build_query({ id: id, snapshot: true })
|
791
|
+
"/#{@config.base_url_path.gsub('/', '')}/results?#{qs}"
|
792
|
+
end
|
793
|
+
|
794
|
+
def take_snapshot?(path)
|
795
|
+
@config.snapshot_every_n_requests > 0 &&
|
796
|
+
!path.start_with?(@config.base_url_path) &&
|
797
|
+
@storage.should_take_snapshot?(@config.snapshot_every_n_requests)
|
798
|
+
end
|
799
|
+
|
800
|
+
def take_snapshot(env, start)
|
801
|
+
MiniProfiler.create_current(env, @config)
|
802
|
+
results = @app.call(env)
|
803
|
+
status = results[0].to_i
|
804
|
+
if status >= 200 && status < 300
|
805
|
+
page_struct = current.page_struct
|
806
|
+
page_struct[:root].record_time(
|
807
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
|
808
|
+
)
|
809
|
+
custom_fields = MiniProfiler.get_snapshot_custom_fields
|
810
|
+
page_struct[:custom_fields] = custom_fields if custom_fields
|
811
|
+
@storage.push_snapshot(
|
812
|
+
page_struct,
|
813
|
+
@config
|
814
|
+
)
|
815
|
+
end
|
816
|
+
self.current = nil
|
817
|
+
results
|
818
|
+
end
|
677
819
|
end
|
678
820
|
end
|
@@ -41,6 +41,78 @@ module Rack
|
|
41
41
|
raise NotImplementedError.new("allowed_tokens is not implemented")
|
42
42
|
end
|
43
43
|
|
44
|
+
def should_take_snapshot?(period)
|
45
|
+
raise NotImplementedError.new("should_take_snapshot? is not implemented")
|
46
|
+
end
|
47
|
+
|
48
|
+
def push_snapshot(page_struct, config)
|
49
|
+
raise NotImplementedError.new("push_snapshot is not implemented")
|
50
|
+
end
|
51
|
+
|
52
|
+
def fetch_snapshots(batch_size: 200, &blk)
|
53
|
+
raise NotImplementedError.new("fetch_snapshots is not implemented")
|
54
|
+
end
|
55
|
+
|
56
|
+
def snapshot_groups_overview
|
57
|
+
groups = {}
|
58
|
+
fetch_snapshots do |batch|
|
59
|
+
batch.each do |snapshot|
|
60
|
+
group_name = default_snapshot_grouping(snapshot)
|
61
|
+
if !groups[group_name] || groups[group_name] < snapshot.duration_ms
|
62
|
+
groups[group_name] = snapshot.duration_ms
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
groups = groups.to_a
|
67
|
+
groups.sort_by! { |name, score| score }
|
68
|
+
groups.reverse!
|
69
|
+
groups.map! do |name, score|
|
70
|
+
{ name: name, worst_score: score }
|
71
|
+
end
|
72
|
+
groups
|
73
|
+
end
|
74
|
+
|
75
|
+
def find_snapshots_group(group_name)
|
76
|
+
data = []
|
77
|
+
fetch_snapshots do |batch|
|
78
|
+
batch.each do |snapshot|
|
79
|
+
snapshot_group_name = default_snapshot_grouping(snapshot)
|
80
|
+
if group_name == snapshot_group_name
|
81
|
+
data << {
|
82
|
+
id: snapshot[:id],
|
83
|
+
duration: snapshot.duration_ms,
|
84
|
+
timestamp: snapshot[:started_at]
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
data.sort_by! { |s| s[:duration] }
|
90
|
+
data.reverse!
|
91
|
+
data
|
92
|
+
end
|
93
|
+
|
94
|
+
def load_snapshot(id)
|
95
|
+
raise NotImplementedError.new("load_snapshot is not implemented")
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def default_snapshot_grouping(snapshot)
|
101
|
+
group_name = rails_route_from_path(snapshot[:request_path], snapshot[:request_method])
|
102
|
+
group_name ||= snapshot[:request_path]
|
103
|
+
"#{snapshot[:request_method]} #{group_name}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def rails_route_from_path(path, method)
|
107
|
+
if defined?(Rails) && defined?(ActionController::RoutingError)
|
108
|
+
hash = Rails.application.routes.recognize_path(path, method: method)
|
109
|
+
if hash && hash[:controller] && hash[:action]
|
110
|
+
"#{hash[:controller]}##{hash[:action]}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
rescue ActionController::RoutingError
|
114
|
+
nil
|
115
|
+
end
|
44
116
|
end
|
45
117
|
end
|
46
118
|
end
|
@@ -52,17 +52,21 @@ module Rack
|
|
52
52
|
@expires_in_seconds = args.fetch(:expires_in) { EXPIRES_IN_SECONDS }
|
53
53
|
|
54
54
|
@token1, @token2, @cycle_at = nil
|
55
|
+
@snapshots_cycle = 0
|
56
|
+
@snapshots = []
|
55
57
|
|
56
58
|
initialize_locks
|
57
59
|
initialize_cleanup_thread(args)
|
58
60
|
end
|
59
61
|
|
60
62
|
def initialize_locks
|
61
|
-
@token_lock
|
62
|
-
@timer_struct_lock
|
63
|
-
@user_view_lock
|
64
|
-
@
|
65
|
-
@
|
63
|
+
@token_lock = Mutex.new
|
64
|
+
@timer_struct_lock = Mutex.new
|
65
|
+
@user_view_lock = Mutex.new
|
66
|
+
@snapshots_cycle_lock = Mutex.new
|
67
|
+
@snapshots_lock = Mutex.new
|
68
|
+
@timer_struct_cache = {}
|
69
|
+
@user_view_cache = {}
|
66
70
|
end
|
67
71
|
|
68
72
|
#FIXME: use weak ref, trouble it may be broken in 1.9 so need to use the 'ref' gem
|
@@ -135,6 +139,51 @@ module Rack
|
|
135
139
|
|
136
140
|
end
|
137
141
|
end
|
142
|
+
|
143
|
+
def should_take_snapshot?(period)
|
144
|
+
@snapshots_cycle_lock.synchronize do
|
145
|
+
@snapshots_cycle += 1
|
146
|
+
if @snapshots_cycle % period == 0
|
147
|
+
@snapshots_cycle = 0
|
148
|
+
true
|
149
|
+
else
|
150
|
+
false
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def push_snapshot(page_struct, config)
|
156
|
+
@snapshots_lock.synchronize do
|
157
|
+
@snapshots << page_struct
|
158
|
+
@snapshots.sort_by! { |s| s.duration_ms }
|
159
|
+
@snapshots.reverse!
|
160
|
+
if @snapshots.size > config.snapshots_limit
|
161
|
+
@snapshots.slice!(-1)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def fetch_snapshots(batch_size: 200, &blk)
|
167
|
+
@snapshots_lock.synchronize do
|
168
|
+
@snapshots.each_slice(batch_size) do |batch|
|
169
|
+
blk.call(batch)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def load_snapshot(id)
|
175
|
+
@snapshots_lock.synchronize do
|
176
|
+
@snapshots.find { |s| s[:id] == id }
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
# used in tests only
|
183
|
+
def wipe_snapshots_data
|
184
|
+
@snapshots_cycle = 0
|
185
|
+
@snapshots = []
|
186
|
+
end
|
138
187
|
end
|
139
188
|
end
|
140
189
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'digest'
|
4
|
+
|
3
5
|
module Rack
|
4
6
|
class MiniProfiler
|
5
7
|
class RedisStore < AbstractStore
|
@@ -108,6 +110,106 @@ unviewed_ids: #{get_unviewed_ids(user)}
|
|
108
110
|
[key1, key2].compact
|
109
111
|
end
|
110
112
|
|
113
|
+
COUNTER_LUA = <<~LUA
|
114
|
+
if redis.call("INCR", KEYS[1]) % ARGV[1] == 0 then
|
115
|
+
redis.call("DEL", KEYS[1])
|
116
|
+
return 1
|
117
|
+
else
|
118
|
+
return 0
|
119
|
+
end
|
120
|
+
LUA
|
121
|
+
|
122
|
+
COUNTER_LUA_SHA = Digest::SHA1.hexdigest(COUNTER_LUA)
|
123
|
+
|
124
|
+
def should_take_snapshot?(period)
|
125
|
+
1 == cached_redis_eval(
|
126
|
+
COUNTER_LUA,
|
127
|
+
COUNTER_LUA_SHA,
|
128
|
+
reraise: false,
|
129
|
+
keys: [snapshot_counter_key()],
|
130
|
+
argv: [period]
|
131
|
+
)
|
132
|
+
end
|
133
|
+
|
134
|
+
def push_snapshot(page_struct, config)
|
135
|
+
zset_key = snapshot_zset_key()
|
136
|
+
hash_key = snapshot_hash_key()
|
137
|
+
|
138
|
+
id = page_struct[:id]
|
139
|
+
score = page_struct.duration_ms
|
140
|
+
limit = config.snapshots_limit
|
141
|
+
bytes = Marshal.dump(page_struct)
|
142
|
+
|
143
|
+
lua = <<~LUA
|
144
|
+
local zset_key = KEYS[1]
|
145
|
+
local hash_key = KEYS[2]
|
146
|
+
local id = ARGV[1]
|
147
|
+
local score = tonumber(ARGV[2])
|
148
|
+
local bytes = ARGV[3]
|
149
|
+
local limit = tonumber(ARGV[4])
|
150
|
+
redis.call("ZADD", zset_key, score, id)
|
151
|
+
redis.call("HSET", hash_key, id, bytes)
|
152
|
+
if redis.call("ZCARD", zset_key) > limit then
|
153
|
+
local lowest_snapshot_id = redis.call("ZRANGE", zset_key, 0, 0)[1]
|
154
|
+
redis.call("ZREM", zset_key, lowest_snapshot_id)
|
155
|
+
redis.call("HDEL", hash_key, lowest_snapshot_id)
|
156
|
+
end
|
157
|
+
LUA
|
158
|
+
redis.eval(
|
159
|
+
lua,
|
160
|
+
keys: [zset_key, hash_key],
|
161
|
+
argv: [id, score, bytes, limit]
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
def fetch_snapshots(batch_size: 200, &blk)
|
166
|
+
zset_key = snapshot_zset_key()
|
167
|
+
hash_key = snapshot_hash_key()
|
168
|
+
iteration = 0
|
169
|
+
corrupt_snapshots = []
|
170
|
+
while true
|
171
|
+
ids = redis.zrange(
|
172
|
+
zset_key,
|
173
|
+
batch_size * iteration,
|
174
|
+
batch_size * iteration + batch_size - 1
|
175
|
+
)
|
176
|
+
break if ids.size == 0
|
177
|
+
batch = redis.mapped_hmget(hash_key, *ids).to_a
|
178
|
+
batch.map! do |id, bytes|
|
179
|
+
begin
|
180
|
+
Marshal.load(bytes)
|
181
|
+
rescue
|
182
|
+
corrupt_snapshots << id
|
183
|
+
nil
|
184
|
+
end
|
185
|
+
end
|
186
|
+
batch.compact!
|
187
|
+
blk.call(batch) if batch.size != 0
|
188
|
+
break if ids.size < batch_size
|
189
|
+
iteration += 1
|
190
|
+
end
|
191
|
+
if corrupt_snapshots.size > 0
|
192
|
+
redis.pipelined do
|
193
|
+
redis.zrem(zset_key, corrupt_snapshots)
|
194
|
+
redis.hdel(hash_key, corrupt_snapshots)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def load_snapshot(id)
|
200
|
+
hash_key = snapshot_hash_key()
|
201
|
+
bytes = redis.hget(hash_key, id)
|
202
|
+
begin
|
203
|
+
Marshal.load(bytes)
|
204
|
+
rescue
|
205
|
+
redis.pipelined do
|
206
|
+
redis.zrem(snapshot_zset_key(), id)
|
207
|
+
redis.hdel(hash_key, id)
|
208
|
+
end
|
209
|
+
nil
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
111
213
|
private
|
112
214
|
|
113
215
|
def user_key(user)
|
@@ -125,6 +227,38 @@ unviewed_ids: #{get_unviewed_ids(user)}
|
|
125
227
|
end
|
126
228
|
end
|
127
229
|
|
230
|
+
def snapshot_counter_key
|
231
|
+
@snapshot_counter_key ||= "#{@prefix}-mini-profiler-snapshots-counter"
|
232
|
+
end
|
233
|
+
|
234
|
+
def snapshot_zset_key
|
235
|
+
@snapshot_zset_key ||= "#{@prefix}-mini-profiler-snapshots-zset"
|
236
|
+
end
|
237
|
+
|
238
|
+
def snapshot_hash_key
|
239
|
+
@snapshot_hash_key ||= "#{@prefix}-mini-profiler-snapshots-hash"
|
240
|
+
end
|
241
|
+
|
242
|
+
def cached_redis_eval(script, script_sha, reraise: true, argv: [], keys: [])
|
243
|
+
begin
|
244
|
+
redis.evalsha(script_sha, argv: argv, keys: keys)
|
245
|
+
rescue ::Redis::CommandError => e
|
246
|
+
if e.message.start_with?('NOSCRIPT')
|
247
|
+
redis.eval(script, argv: argv, keys: keys)
|
248
|
+
else
|
249
|
+
raise e if reraise
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# only used in tests
|
255
|
+
def wipe_snapshots_data
|
256
|
+
redis.pipelined do
|
257
|
+
redis.del(snapshot_counter_key())
|
258
|
+
redis.del(snapshot_zset_key())
|
259
|
+
redis.del(snapshot_hash_key())
|
260
|
+
end
|
261
|
+
end
|
128
262
|
end
|
129
263
|
end
|
130
264
|
end
|
@@ -39,8 +39,11 @@ module Rack
|
|
39
39
|
executed_scalars: 0,
|
40
40
|
executed_non_queries: 0,
|
41
41
|
custom_timing_names: [],
|
42
|
-
custom_timing_stats: {}
|
42
|
+
custom_timing_stats: {},
|
43
|
+
custom_fields: {}
|
43
44
|
)
|
45
|
+
self[:request_method] = env['REQUEST_METHOD']
|
46
|
+
self[:request_path] = env['PATH_INFO']
|
44
47
|
name = "#{env['REQUEST_METHOD']} http://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
|
45
48
|
self[:root] = TimerStruct::Request.createRoot(name, self)
|
46
49
|
end
|
@@ -118,6 +118,17 @@ module Rack::MiniProfilerRails
|
|
118
118
|
@already_initialized = true
|
119
119
|
end
|
120
120
|
|
121
|
+
def self.create_engine
|
122
|
+
return if defined?(Rack::MiniProfilerRails::Engine)
|
123
|
+
klass = Class.new(::Rails::Engine) do
|
124
|
+
engine_name 'rack-mini-profiler'
|
125
|
+
config.assets.paths << File.expand_path('../../html', __FILE__)
|
126
|
+
config.assets.precompile << 'rack-mini-profiler.js'
|
127
|
+
config.assets.precompile << 'rack-mini-profiler.css'
|
128
|
+
end
|
129
|
+
Rack::MiniProfilerRails.const_set("Engine", klass)
|
130
|
+
end
|
131
|
+
|
121
132
|
def self.subscribe(event, &blk)
|
122
133
|
if ActiveSupport::Notifications.respond_to?(:monotonic_subscribe)
|
123
134
|
ActiveSupport::Notifications.monotonic_subscribe(event) { |*args| blk.call(*args) }
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-mini-profiler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Saffron
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2020-
|
13
|
+
date: 2020-09-17 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rack
|
@@ -244,6 +244,8 @@ files:
|
|
244
244
|
- lib/html/includes.tmpl
|
245
245
|
- lib/html/pretty-print.js
|
246
246
|
- lib/html/profile_handler.js
|
247
|
+
- lib/html/rack-mini-profiler.css
|
248
|
+
- lib/html/rack-mini-profiler.js
|
247
249
|
- lib/html/share.html
|
248
250
|
- lib/html/vendor.js
|
249
251
|
- lib/mini_profiler/asset_version.rb
|