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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e54608bc4885796e11d6e4fac9cc56fe478f60c5cd43d53637451ef3524f086
4
- data.tar.gz: 6a87a58c7821a279ce08b66432de9a199a6f775f897610e03e12d497eb7ccc46
3
+ metadata.gz: 1603a0c0246f9b132f81e74331425a1217156d62f71f20a06ecd3edca91aef51
4
+ data.tar.gz: a341e35cf639c11cc61c9ef5fd1ecd25e89ff12da299d3a6bfe8cb479c5dd7d8
5
5
  SHA512:
6
- metadata.gz: 3730c9e4610fd90cc0fac86c1cddcf6181e15cef5932ba86f8f8cbca1df77c02ba7847c2e1868b48e51299b6c51224fe533818c3cc245dd02a2da11b2298569c
7
- data.tar.gz: 98f54eed006f7f13840481b777dbe4add5f9983c5ed367f8046a942a584a5ad3eb4ea5ae6e3aef7e4aa0760b37c78ac5dd6ee5bcd45e42091a943dfabdb343b1
6
+ metadata.gz: 4244e2738847e0e34b33cebda96f45d79ad82201ce969310bf45f1ad7431561af0d41610c2e8c60a0c64d428032a1e4bea6cc5b02aa71b5462a918474228844a
7
+ data.tar.gz: b1c0d88134e780bbbb91f711366400585d51d98107d1f35af85ca7c38498f0e23de019f2eb05259e833d8deabf485dc70f1dd581c28e132aba68bfa785967774
@@ -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|`[]`|Paths that skip profiling.
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
 
@@ -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; }
@@ -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.path + "includes.css?v=" + options.version;
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();
@@ -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: #555;
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 #555;
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, #555);
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: #555;
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
+ }
@@ -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>
@@ -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-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>
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>
@@ -0,0 +1,3 @@
1
+ /*
2
+ *= require ./includes
3
+ */
@@ -0,0 +1,2 @@
1
+ //= require ./includes
2
+ //= require ./vendor
@@ -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 arr4=MiniProfiler.getSqlTimings(it.root);if(arr4){var value,index=-1,l4=arr4.length-1;while(index<l4){value=arr4[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;
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 )+' &mdash; '+( 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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Rack
3
3
  class MiniProfiler
4
- ASSET_VERSION = '015eebd28435014a417b0b8cf057054d'
4
+ ASSET_VERSION = 'b5b2bc8cce501b6f38c294cea2f0d2c2'
5
5
  end
6
6
  end
@@ -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
- page_struct = @storage.load(id)
110
- unless page_struct
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(request.params['id'])
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
- unless page_struct[:has_user_viewed]
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:#{cache_control_value}")
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 = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env)) ||
193
- (@config.skip_paths && @config.skip_paths.any? { |p| path.start_with?(p) }) ||
194
- query_string =~ /pp=skip/
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
- return client_settings.handle_cookie(@app.call(env))
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
- version: MiniProfiler::ASSET_VERSION,
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 = Mutex.new
62
- @timer_struct_lock = Mutex.new
63
- @user_view_lock = Mutex.new
64
- @timer_struct_cache = {}
65
- @user_view_cache = {}
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class MiniProfiler
5
- VERSION = '2.0.4'
5
+ VERSION = '2.1.0'
6
6
  end
7
7
  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
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-08-03 00:00:00.000000000 Z
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