rack-mini-profiler 2.3.2 → 3.0.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: 2984e9dec3cda4d5b86a4ed3b8300725207ba826b6a064402b23dcf04b0d5a51
4
- data.tar.gz: 4a05074c124ac23085c08f4ae5389b71ef7b85687c40c5e5594dee4884d87632
3
+ metadata.gz: cde7281c7a63d3d3ac5bc7605ba2cf81dedf6b598ff17ed568610a54a50b517f
4
+ data.tar.gz: 7af763d5136493c71cc4321fe54db79372e5b9aaac4c10d31bc2f27d22f324b7
5
5
  SHA512:
6
- metadata.gz: b29d4a47e99d03489251752bfe57a2b64235400b5ffb9f867738bfb9d165f63ddeeb559fc4edf9a60c91fe2d579b04ee473db1b2c7846ab823580d8b9b5e88c1
7
- data.tar.gz: 3a0768401c048801bf4adb22792fbdb2643d5b5e0174f92149df96c8ae2ab79d0c504e9e2146d72cc45068df6b88ebc675ef11431d47c17a5b929f375a1e6d74
6
+ metadata.gz: abb415f975552a1256753e15ba9d6623aeb55fe4b058c775701f2b7d3a955f62d2a906a93929f1a22545f8558750d48c78f3b7e185f774c224d12e6a256a69a6
7
+ data.tar.gz: f5ff03170537e5dc17a826909d9b77609bc8d6f2390f42b29be9ff1af538332eaa33c269f7433d87142ba7e05315741b3ea5b4c69fcc76980a56c225338d843c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 3.0.0 - 2022-02-24
4
+
5
+ - PERF: Improve snapshots page performance (#518) (introduces breaking changes to the API of `AbstractStore`, `MemoryStore` and `RedisStore`, and removes the `snapshots_limit` config option.)
6
+
7
+ ## 2.3.4 - 2022-02-23
8
+
9
+ - [FEATURE] Add cookie path support for subfolder sites
10
+ - [FIX] Remove deprecated uses of Redis#pipelined
11
+
12
+ ## 2.3.3 - 2021-08-30
13
+
14
+ - [FEATURE] Introduce `pp=flamegraph_mode`
15
+ - [FEATURE] Richer CSP configuration options
16
+ - [FEATURE] Add support for Hotwire Turbo Drive
17
+
3
18
  ## 2.3.2 - 2021-04-30
4
19
 
5
20
  - [FEATURE] Introduce `pp=async-flamegraph` for asynchronous flamegraphs
data/README.md CHANGED
@@ -166,14 +166,14 @@ export RACK_MINI_PROFILER_PATCH="false"
166
166
 
167
167
  ### Flamegraphs
168
168
 
169
- To generate [flamegraphs](http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler):
169
+ To generate [flamegraphs](http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler), add the [**stackprof**](https://rubygems.org/gems/stackprof) gem to your Gemfile.
170
170
 
171
- * add the [**stackprof**](https://rubygems.org/gems/stackprof) gem to your Gemfile
172
- * visit a page in your app with `?pp=flamegraph`
171
+ Then, to view the flamegraph as a direct HTML response from your request, just visit any page in your app with `?pp=flamegraph` appended to the URL.
173
172
 
174
- To store flamegraph data for later viewing, append the `?pp=async-flamegraph` parameter. The request will return as normal.
175
- Flamegraph data for this request, and all subsequent requests made by this page (based on the `REFERER` header) will be stored.
176
- 'flamegraph' links will appear for these requests in the MiniProfiler UI.
173
+ Conversely, if you want your regular response instead (which is specially useful for JSON and/or XHR requests), just append the `?pp=async-flamegraph` parameter to your request/fetch URL; the request will then return as normal, and the flamegraph data will be stored for later *async* viewing, both for this request and for all subsequent requests made by this page (based on the `REFERER` header). For viewing these async flamegraphs, use the 'flamegraph' link that will appear inside the MiniProfiler UI for these requests.
174
+
175
+ Note: Mini Profiler will not record SQL timings for a request if it asks for a flamegraph. The rationale behind this is to keep
176
+ Mini Profiler's methods that are responsible for generating the timings data out of the flamegraph.
177
177
 
178
178
  ### Memory Profiling
179
179
 
@@ -207,7 +207,7 @@ After enabling snapshots sampling, you can see the snapshots that have been coll
207
207
 
208
208
  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.
209
209
 
210
- 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).
210
+ Mini Profiler will keep a maximum of 50 snapshot groups and a maximum of 15 snapshots per group making the default maximum number of snapshots in the system 750. The default group and per group limits can be changed via the `max_snapshot_groups` and `max_snapshots_per_group` configuration options, see the configurations table below.
211
211
 
212
212
  #### Snapshots Transporter
213
213
 
@@ -347,19 +347,41 @@ Single page applications built using Ember, Angular or other frameworks need som
347
347
  On route transition always call:
348
348
 
349
349
  ```
350
- window.MiniProfiler.pageTransition();
350
+ if (window.MiniProfiler !== undefined) {
351
+ window.MiniProfiler.pageTransition();
352
+ }
351
353
  ```
352
354
 
353
355
  This method will remove profiling information that was related to previous page and clear aggregate statistics.
354
356
 
355
357
  #### MiniProfiler's speed badge on pages that are not generated via Rails
356
- You need to inject the following in your SPA to load MiniProfiler's speed badge ([extra details surrounding this script](https://github.com/MiniProfiler/rack-mini-profiler/issues/139#issuecomment-192880706)):
358
+ You need to inject the following in your SPA to load MiniProfiler's speed badge ([extra details surrounding this script](https://github.com/MiniProfiler/rack-mini-profiler/issues/139#issuecomment-192880706) and [credit for the script tag](https://github.com/MiniProfiler/rack-mini-profiler/issues/479#issue-782488320) to [@ivanyv](https://github.com/ivanyv)):
357
359
 
358
360
  ```html
359
- <script async type="text/javascript" id="mini-profiler" src="/mini-profiler-resources/includes.js?v=12b4b45a3c42e6e15503d7a03810ff33" data-version="12b4b45a3c42e6e15503d7a03810ff33" data-path="/mini-profiler-resources/" data-current-id="redo66j4g1077kto8uh3" data-ids="redo66j4g1077kto8uh3" data-horizontal-position="left" data-vertical-position="top" data-trivial="false" data-children="false" data-max-traces="10" data-controls="false" data-authorized="true" data-toggle-shortcut="Alt+P" data-start-hidden="false" data-collapse-results="true"></script>
360
- ```
361
-
362
- _Note:_ The GUID (`data-version` and the `?v=` parameter on the `src`) will change with each release of `rack_mini_profiler`. The MiniProfiler's speed badge will continue to work, although you will have to change the GUID to expire the script to fetch the most recent version.
361
+ <script type="text/javascript" id="mini-profiler"
362
+ src="/mini-profiler-resources/includes.js?v=12b4b45a3c42e6e15503d7a03810ff33"
363
+ data-css-url="/mini-profiler-resources/includes.css?v=12b4b45a3c42e6e15503d7a03810ff33"
364
+ data-version="12b4b45a3c42e6e15503d7a03810ff33"
365
+ data-path="/mini-profiler-resources/"
366
+ data-horizontal-position="left"
367
+ data-vertical-position="top"
368
+ data-ids=""
369
+ data-trivial="false"
370
+ data-children="false"
371
+ data-max-traces="20"
372
+ data-controls="false"
373
+ data-total-sql-count="false"
374
+ data-authorized="true"
375
+ data-toggle-shortcut="alt+p"
376
+ data-start-hidden="false"
377
+ data-collapse-results="true"
378
+ data-html-container="body"
379
+ data-hidden-custom-fields></script>
380
+ ```
381
+
382
+ See an [example of how to do this in a React useEffect](https://gist.github.com/katelovescode/01cfc2b962c165193b160fd10af6c4d5).
383
+
384
+ _Note:_ The GUID (`data-version` and the `?v=` parameter on the `src` and `data-css-url`) will change with each release of `rack_mini_profiler`. The MiniProfiler's speed badge will continue to work, although you will have to change the GUID to expire the script to fetch the most recent version.
363
385
 
364
386
  #### Using MiniProfiler's built in route for apps without HTML responses
365
387
  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.
@@ -398,27 +420,32 @@ toggle_shortcut|Alt+P|Keyboard shortcut to toggle the mini_profiler's visibility
398
420
  start_hidden|`false`|`false` to make mini_profiler visible on page load.
399
421
  backtrace_threshold_ms|`0`|Minimum SQL query elapsed time before a backtrace is recorded.
400
422
  flamegraph_sample_rate|`0.5`|How often to capture stack traces for flamegraphs in milliseconds.
423
+ flamegraph_mode|`:wall`|The [StackProf mode](https://github.com/tmm1/stackprof#all-options) to pass to `StackProf.run`.
401
424
  base_url_path|`'/mini-profiler-resources/'`|Path for assets; added as a prefix when naming assets and sought when responding to requests.
425
+ cookie_path|`'/'`|Set-Cookie header path for profile cookie
402
426
  collapse_results|`true`|If multiple timing results exist in a single page, collapse them till clicked.
403
427
  max_traces_to_show|20|Maximum number of mini profiler timing blocks to show on one page
404
428
  html_container|`body`|The HTML container (as a jQuery selector) to inject the mini_profiler UI into
405
429
  show_total_sql_count|`false`|Displays the total number of SQL executions.
406
- 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.
430
+ 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. Defaults to `true` in development.
407
431
  assets_url|`nil`|See the "Register MiniProfiler's assets in the Rails assets pipeline" section above.
408
432
  snapshot_every_n_requests|`-1`|Determines how frequently snapshots are taken. See the "Snapshots Sampling" above for more details.
409
- snapshots_limit|`1000`|Determines how many snapshots Mini Profiler is allowed to keep.
433
+ max_snapshot_groups|`50`|Determines how many snapshot groups Mini Profiler is allowed to keep.
434
+ max_snapshots_per_group|`15`|Determines how many snapshots per group Mini Profiler is allowed to keep.
410
435
  snapshot_hidden_custom_fields|`[]`|Each snapshot custom field will have a dedicated column in the UI by default. Use this config to exclude certain custom fields from having their own columns.
411
436
  snapshots_transport_destination_url|`nil`|Set this config to a valid URL to enable snapshots transporter which will `POST` snapshots to the given URL. The transporter requires `snapshots_transport_auth_key` config to be set as well.
412
437
  snapshots_transport_auth_key|`nil`|`POST` requests made by the snapshots transporter to the destination URL will have a `Mini-Profiler-Transport-Auth` header with the value of this config. Make sure you use a secure and random key for this config.
413
438
  snapshots_redact_sql_queries|`true`|When this is true, SQL queries will be redacted from sampling snapshots, but the backtrace and duration of each SQL query will be saved with the snapshot to keep debugging performance issues possible.
414
439
  snapshots_transport_gzip_requests|`false`|Make the snapshots transporter gzip the requests it makes to `snapshots_transport_destination_url`.
440
+ content_security_policy_nonce|Rails: Current nonce<br>Rack: nil|Set the content security policy nonce to use when inserting MiniProfiler's script block.
441
+ enable_hotwire_turbo_drive_support| `false` | Enable support for Hotwire TurboDrive page transitions.
415
442
 
416
443
  ### Using MiniProfiler with `Rack::Deflate` middleware
417
444
 
418
445
  If you are using `Rack::Deflate` with Rails and `rack-mini-profiler` in its default configuration,
419
446
  `Rack::MiniProfiler` will be injected (as always) at position 0 in the middleware stack,
420
447
  which means it will run after `Rack::Deflate` on response processing. To prevent attempting to inject
421
- HTML in already compressed response body MiniProfiler will suppress compression by setting
448
+ HTML in already compressed response body MiniProfiler will suppress compression by setting
422
449
  `identity` encoding in `Accept-Encoding` request header.
423
450
 
424
451
  ## Special query strings
@@ -461,20 +488,23 @@ end
461
488
  If you want to contribute to this project, that's great, thank you! You can run the following rake task:
462
489
 
463
490
  ```
491
+ $ BUNDLE_GEMFILE=website/Gemfile bundle install
464
492
  $ bundle exec rake client_dev
465
493
  ```
466
494
 
467
- which will start a local Sinatra server at `http://localhost:9292` where you'll be able to preview your changes. Refreshing the page should be enough to see any changes you make to files in the `lib/html` directory.
495
+ This will start a local Sinatra server at `http://localhost:9292` where you'll be able to preview your changes. Refreshing the page should be enough to see any changes you make to files in the `lib/html` directory.
496
+
497
+ Make sure to prepend `bundle exec` before any Rake tasks you run.
468
498
 
469
499
  ## Running the Specs
470
500
 
501
+ You need Memcached and Redis services running for the specs.
502
+
471
503
  ```
472
504
  $ rake build
473
505
  $ rake spec
474
506
  ```
475
507
 
476
- Additionally you can also run `autotest` if you like.
477
-
478
508
  ## Licence
479
509
 
480
510
  The MIT License (MIT)
@@ -69,8 +69,16 @@
69
69
  .profiler-result .custom-fields-title,
70
70
  .profiler-queries .custom-fields-title {
71
71
  color: #555;
72
- font: Helvetica, Arial, sans-serif;
72
+ font-family: Helvetica, Arial, sans-serif;
73
73
  font-size: 14px; }
74
+ .mp-snapshots .ta-left,
75
+ .profiler-result .ta-left,
76
+ .profiler-queries .ta-left {
77
+ text-align: left; }
78
+ .mp-snapshots .ta-right,
79
+ .profiler-result .ta-right,
80
+ .profiler-queries .ta-right {
81
+ text-align: right; }
74
82
 
75
83
  .profiler-result {
76
84
  font-family: Helvetica, Arial, sans-serif; }
@@ -233,8 +241,6 @@
233
241
  left: 0px; }
234
242
  .profiler-results.profiler-top.profiler-left.profiler-no-controls .profiler-totals, .profiler-results.profiler-top.profiler-left.profiler-no-controls .profiler-result:last-child .profiler-button,
235
243
  .profiler-results.profiler-top.profiler-left .profiler-controls {
236
- -webkit-border-bottom-right-radius: 10px;
237
- -moz-border-radius-bottomright: 10px;
238
244
  border-bottom-right-radius: 10px; }
239
245
  .profiler-results.profiler-top.profiler-left .profiler-button,
240
246
  .profiler-results.profiler-top.profiler-left .profiler-controls {
@@ -243,8 +249,6 @@
243
249
  right: 0px; }
244
250
  .profiler-results.profiler-top.profiler-right.profiler-no-controls .profiler-totals, .profiler-results.profiler-top.profiler-right.profiler-no-controls .profiler-result:last-child .profiler-button,
245
251
  .profiler-results.profiler-top.profiler-right .profiler-controls {
246
- -webkit-border-bottom-left-radius: 10px;
247
- -moz-border-radius-bottomleft: 10px;
248
252
  border-bottom-left-radius: 10px; }
249
253
  .profiler-results.profiler-top.profiler-right .profiler-button,
250
254
  .profiler-results.profiler-top.profiler-right .profiler-controls {
@@ -255,8 +259,6 @@
255
259
  left: 0px; }
256
260
  .profiler-results.profiler-bottom.profiler-left.profiler-no-controls .profiler-totals, .profiler-results.profiler-bottom.profiler-left.profiler-no-controls .profiler-result:first-child .profiler-button,
257
261
  .profiler-results.profiler-bottom.profiler-left .profiler-controls {
258
- -webkit-border-top-right-radius: 10px;
259
- -moz-border-radius-topright: 10px;
260
262
  border-top-right-radius: 10px; }
261
263
  .profiler-results.profiler-bottom.profiler-left .profiler-button,
262
264
  .profiler-results.profiler-bottom.profiler-left .profiler-controls {
@@ -265,8 +267,6 @@
265
267
  right: 0px; }
266
268
  .profiler-results.profiler-bottom.profiler-right.profiler-no-controls .profiler-totals, .profiler-results.profiler-bottom.profiler-right.profiler-no-controls .profiler-result:first-child .profiler-button,
267
269
  .profiler-results.profiler-bottom.profiler-right .profiler-controls {
268
- -webkit-border-bottom-top-radius: 10px;
269
- -moz-border-radius-topleft: 10px;
270
270
  border-top-left-radius: 10px; }
271
271
  .profiler-results.profiler-bottom.profiler-right .profiler-button,
272
272
  .profiler-results.profiler-bottom.profiler-right .profiler-controls {
@@ -324,8 +324,6 @@
324
324
  text-align: left;
325
325
  line-height: 18px;
326
326
  overflow: auto;
327
- -moz-box-shadow: 0px 1px 15px #555;
328
- -webkit-box-shadow: 0px 1px 15px #555;
329
327
  box-shadow: 0px 1px 15px #555; }
330
328
  .profiler-results .profiler-popup .profiler-info {
331
329
  margin-bottom: 3px;
data/lib/html/includes.js CHANGED
@@ -495,6 +495,19 @@ var _MiniProfiler = (function() {
495
495
  }, 3000);
496
496
  };
497
497
 
498
+ var onTurboBeforeVisit = function onTurboBeforeVisit(e) {
499
+ if(!e.defaultPrevented) {
500
+ window.MiniProfilerContainer = document.querySelector('body > .profiler-results')
501
+ window.MiniProfiler.pageTransition()
502
+ }
503
+ }
504
+
505
+ var onTurboLoad = function onTurboLoad(e) {
506
+ if(window.MiniProfilerContainer) {
507
+ document.body.appendChild(window.MiniProfilerContainer)
508
+ }
509
+ }
510
+
498
511
  var onClickEvents = function onClickEvents(e) {
499
512
  // this happens on every keystroke, and :visible is crazy expensive in IE <9
500
513
  // and in this case, the display:none check is sufficient.
@@ -652,6 +665,11 @@ var _MiniProfiler = (function() {
652
665
  turbolinksSkipResultsFetch
653
666
  );
654
667
  }
668
+
669
+ if (options.hotwireTurboDriveSupport) {
670
+ document.addEventListener("turbo:before-visit", onTurboBeforeVisit)
671
+ document.addEventListener("turbo:load", onTurboLoad)
672
+ }
655
673
  };
656
674
 
657
675
  var unbindDocumentEvents = function unbindDocumentEvents() {
@@ -664,6 +682,8 @@ var _MiniProfiler = (function() {
664
682
  "turbolinks:request-start",
665
683
  turbolinksSkipResultsFetch
666
684
  );
685
+ document.removeEventListener("turbo:before-visit", onTurboBeforeVisit);
686
+ document.removeEventListener("turbo:load", onTurboLoad);
667
687
  };
668
688
 
669
689
  var initFullView = function initFullView() {
@@ -1033,6 +1053,8 @@ var _MiniProfiler = (function() {
1033
1053
  .getAttribute("data-hidden-custom-fields")
1034
1054
  .toLowerCase()
1035
1055
  .split(",");
1056
+ var hotwireTurboDriveSupport = script
1057
+ .getAttribute('data-turbo-permanent') === "true";
1036
1058
  return {
1037
1059
  ids: ids,
1038
1060
  path: path,
@@ -1051,7 +1073,8 @@ var _MiniProfiler = (function() {
1051
1073
  collapseResults: collapseResults,
1052
1074
  htmlContainer: htmlContainer,
1053
1075
  cssUrl: cssUrl,
1054
- hiddenCustomFields: hiddenCustomFields
1076
+ hiddenCustomFields: hiddenCustomFields,
1077
+ hotwireTurboDriveSupport: hotwireTurboDriveSupport
1055
1078
  };
1056
1079
  })();
1057
1080
 
@@ -1,6 +1,4 @@
1
1
  @mixin box-shadow($dx, $dy, $radius, $color) {
2
- -moz-box-shadow: $dx $dy $radius $color;
3
- -webkit-box-shadow: $dx $dy $radius $color;
4
2
  box-shadow: $dx $dy $radius $color;
5
3
  }
6
4
 
@@ -58,9 +56,11 @@ $zindex: 2147483640; // near 32bit max 2147483647
58
56
  }
59
57
  .custom-fields-title {
60
58
  color: $textColor;
61
- font: $normalFonts;
59
+ font-family: $normalFonts;
62
60
  font-size: 14px;
63
61
  }
62
+ .ta-left { text-align: left; }
63
+ .ta-right { text-align: right; }
64
64
  }
65
65
 
66
66
  // styles shared between popup view and full view
@@ -326,8 +326,6 @@ $zindex: 2147483640; // near 32bit max 2147483647
326
326
  &.profiler-no-controls .profiler-totals,
327
327
  &.profiler-no-controls .profiler-result:last-child .profiler-button,
328
328
  .profiler-controls {
329
- -webkit-border-bottom-right-radius: $radius;
330
- -moz-border-radius-bottomright: $radius;
331
329
  border-bottom-right-radius: $radius;
332
330
  }
333
331
 
@@ -343,8 +341,6 @@ $zindex: 2147483640; // near 32bit max 2147483647
343
341
  &.profiler-no-controls .profiler-totals,
344
342
  &.profiler-no-controls .profiler-result:last-child .profiler-button,
345
343
  .profiler-controls {
346
- -webkit-border-bottom-left-radius: $radius;
347
- -moz-border-radius-bottomleft: $radius;
348
344
  border-bottom-left-radius: $radius;
349
345
  }
350
346
 
@@ -364,8 +360,6 @@ $zindex: 2147483640; // near 32bit max 2147483647
364
360
  &.profiler-no-controls .profiler-totals,
365
361
  &.profiler-no-controls .profiler-result:first-child .profiler-button,
366
362
  .profiler-controls {
367
- -webkit-border-top-right-radius: $radius;
368
- -moz-border-radius-topright: $radius;
369
363
  border-top-right-radius: $radius;
370
364
  }
371
365
 
@@ -381,8 +375,6 @@ $zindex: 2147483640; // near 32bit max 2147483647
381
375
  &.profiler-no-controls .profiler-totals,
382
376
  &.profiler-no-controls .profiler-result:first-child .profiler-button,
383
377
  .profiler-controls {
384
- -webkit-border-bottom-top-radius: $radius;
385
- -moz-border-radius-topleft: $radius;
386
378
  border-top-left-radius: $radius;
387
379
  }
388
380
 
@@ -117,8 +117,8 @@
117
117
  <table>
118
118
  <thead>
119
119
  <tr>
120
- <th style="text-align:right">step<br />time from start<br />query type<br />duration</th>
121
- <th style="text-align:left">call stack<br />query</th>
120
+ <th class="ta-right">step<br />time from start<br />query type<br />duration</th>
121
+ <th class="ta-left">call stack<br />query</th>
122
122
  </tr>
123
123
  </thead>
124
124
  <tbody>
@@ -1 +1 @@
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}" data-hidden-custom-fields="{hiddenCustomFields}"></script>
1
+ <script async nonce="{cspNonce}" 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}" data-hidden-custom-fields="{hiddenCustomFields}" data-turbo-permanent="{hotwireTurboDriveSupport}"></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_formatted))+'</span> </div> <div class="profiler-output"> <table class="profiler-timings"> <thead> <tr> <th>event</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;
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_formatted))+'</span> </div> <div class="profiler-output"> <table class="profiler-timings"> <thead> <tr> <th>event</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 class="ta-right">step<br />time from start<br />query type<br />duration</th> <th class="ta-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
  ) {
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Rack
3
3
  class MiniProfiler
4
- ASSET_VERSION = '8cccf9fed3d62814b1d70f252554501b'
4
+ ASSET_VERSION = '35a79b300ab5afa978cb59af0b05e059'
5
5
  end
6
6
  end
@@ -74,7 +74,7 @@ module Rack
74
74
  settings["bt"] = @backtrace_level if @backtrace_level
75
75
  settings["a"] = @allowed_tokens.join("|") if @allowed_tokens && MiniProfiler.request_authorized?
76
76
  settings_string = settings.map { |k, v| "#{k}=#{v}" }.join(",")
77
- cookie = { value: settings_string, path: '/', httponly: true }
77
+ cookie = { value: settings_string, path: MiniProfiler.config.cookie_path, httponly: true }
78
78
  cookie[:secure] = true if @request.ssl?
79
79
  cookie[:same_site] = 'Lax'
80
80
  Rack::Utils.set_cookie_header!(headers, COOKIE_NAME, cookie)
@@ -83,7 +83,7 @@ module Rack
83
83
 
84
84
  def discard_cookie!(headers)
85
85
  if @cookie
86
- Rack::Utils.delete_cookie_header!(headers, COOKIE_NAME, path: '/')
86
+ Rack::Utils.delete_cookie_header!(headers, COOKIE_NAME, path: MiniProfiler.config.cookie_path)
87
87
  end
88
88
  end
89
89
 
@@ -17,6 +17,7 @@ module Rack
17
17
  new.instance_eval {
18
18
  @auto_inject = true # automatically inject on every html page
19
19
  @base_url_path = "/mini-profiler-resources/".dup
20
+ @cookie_path = "/".dup
20
21
  @disable_caching = true
21
22
  # called prior to rack chain, to ensure we are allowed to profile
22
23
  @pre_authorize_cb = lambda { |env| true }
@@ -28,6 +29,7 @@ module Rack
28
29
  @authorization_mode = :allow_all
29
30
  @backtrace_threshold_ms = 0
30
31
  @flamegraph_sample_rate = 0.5
32
+ @flamegraph_mode = :wall
31
33
  @storage_failure = Proc.new do |exception|
32
34
  if @logger
33
35
  @logger.warn("MiniProfiler storage failure: #{exception.message}")
@@ -38,7 +40,8 @@ module Rack
38
40
  @skip_sql_param_names = /password/ # skips parameters with the name password by default
39
41
  @enable_advanced_debugging_tools = false
40
42
  @snapshot_every_n_requests = -1
41
- @snapshots_limit = 1000
43
+ @max_snapshot_groups = 50
44
+ @max_snapshots_per_group = 15
42
45
 
43
46
  # ui parameters
44
47
  @autorized = true
@@ -57,6 +60,7 @@ module Rack
57
60
  @snapshots_transport_auth_key = nil
58
61
  @snapshots_redact_sql_queries = true
59
62
  @snapshots_transport_gzip_requests = false
63
+ @enable_hotwire_turbo_drive_support = false
60
64
 
61
65
  self
62
66
  }
@@ -64,11 +68,13 @@ module Rack
64
68
 
65
69
  attr_accessor :authorization_mode, :auto_inject, :backtrace_ignores,
66
70
  :backtrace_includes, :backtrace_remove, :backtrace_threshold_ms,
67
- :base_url_path, :disable_caching, :enabled,
71
+ :base_url_path, :cookie_path, :disable_caching, :enabled,
68
72
  :flamegraph_sample_rate, :logger, :pre_authorize_cb, :skip_paths,
69
73
  :skip_schema_queries, :storage, :storage_failure, :storage_instance,
70
74
  :storage_options, :user_provider, :enable_advanced_debugging_tools,
71
- :skip_sql_param_names, :suppress_encoding, :max_sql_param_length
75
+ :skip_sql_param_names, :suppress_encoding, :max_sql_param_length,
76
+ :content_security_policy_nonce, :enable_hotwire_turbo_drive_support,
77
+ :flamegraph_mode
72
78
 
73
79
  # ui accessors
74
80
  attr_accessor :collapse_results, :max_traces_to_show, :position,
@@ -76,10 +82,10 @@ module Rack
76
82
  :start_hidden, :toggle_shortcut, :html_container
77
83
 
78
84
  # snapshot related config
79
- attr_accessor :snapshot_every_n_requests, :snapshots_limit,
85
+ attr_accessor :snapshot_every_n_requests, :max_snapshots_per_group,
80
86
  :snapshot_hidden_custom_fields, :snapshots_transport_destination_url,
81
87
  :snapshots_transport_auth_key, :snapshots_redact_sql_queries,
82
- :snapshots_transport_gzip_requests
88
+ :snapshots_transport_gzip_requests, :max_snapshot_groups
83
89
 
84
90
  # Deprecated options
85
91
  attr_accessor :use_existing_jquery
@@ -130,10 +130,10 @@ module Rack
130
130
  def serve_results(env)
131
131
  request = Rack::Request.new(env)
132
132
  id = request.params['id']
133
- is_snapshot = request.params['snapshot']
134
- is_snapshot = [true, "true"].include?(is_snapshot)
133
+ group_name = request.params['group']
134
+ is_snapshot = group_name && group_name.size > 0
135
135
  if is_snapshot
136
- page_struct = @storage.load_snapshot(id)
136
+ page_struct = @storage.load_snapshot(id, group_name)
137
137
  else
138
138
  page_struct = @storage.load(id)
139
139
  end
@@ -362,8 +362,17 @@ module Rack
362
362
  else
363
363
  sample_rate = config.flamegraph_sample_rate
364
364
  end
365
+
366
+ mode_match_data = query_string.match(/flamegraph_mode=([a-zA-Z]+)/)
367
+
368
+ if mode_match_data && [:cpu, :wall, :object, :custom].include?(mode_match_data[1].to_sym)
369
+ mode = mode_match_data[1].to_sym
370
+ else
371
+ mode = config.flamegraph_mode
372
+ end
373
+
365
374
  flamegraph = StackProf.run(
366
- mode: :wall,
375
+ mode: mode,
367
376
  raw: true,
368
377
  aggregate: false,
369
378
  interval: (sample_rate * 1000).to_i
@@ -630,7 +639,7 @@ module Rack
630
639
  end
631
640
 
632
641
  def text_result(body)
633
- headers = { 'Content-Type' => 'text/plain' }
642
+ headers = { 'Content-Type' => 'text/plain; charset=utf-8' }
634
643
  [200, headers, [body]]
635
644
  end
636
645
 
@@ -643,7 +652,7 @@ module Rack
643
652
  headers = { 'Content-Type' => 'text/html' }
644
653
  body = "<html><body>
645
654
  <pre style='line-height: 30px; font-size: 16px;'>
646
- Append the following to your query string:
655
+ This is the help menu of the <a href='#{Rack::MiniProfiler::SOURCE_CODE_URI}'>rack-mini-profiler</a> gem, append the following to your query string for more options:
647
656
 
648
657
  #{make_link "help", env} : display this screen
649
658
  #{make_link "env", env} : display the rack environment
@@ -658,6 +667,7 @@ Append the following to your query string:
658
667
  #{make_link "flamegraph", env} : a graph representing sampled activity (requires the stackprof gem).
659
668
  #{make_link "async-flamegraph", env} : store flamegraph data for this page and all its AJAX requests. Flamegraph links will be available in the mini-profiler UI (requires the stackprof gem).
660
669
  #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
670
+ #{make_link "flamegraph&flamegraph_mode=cpu", env}: creates a flamegraph with the specified mode (one of cpu, wall, object, or custom). Overrides value set in config
661
671
  #{make_link "flamegraph_embed", env} : a graph representing sampled activity (requires the stackprof gem), embedded resources for use on an intranet.
662
672
  #{make_link "trace-exceptions", env} : will return all the spots where your application raises exceptions
663
673
  #{make_link "analyze-memory", env} : will perform basic memory analysis of heap
@@ -728,6 +738,10 @@ Append the following to your query string:
728
738
  url = "#{path}includes.js?v=#{version}" if !url
729
739
  css_url = "#{path}includes.css?v=#{version}" if !css_url
730
740
 
741
+ content_security_policy_nonce = @config.content_security_policy_nonce ||
742
+ env["action_dispatch.content_security_policy_nonce"] ||
743
+ env["secure_headers_content_security_policy_nonce"]
744
+
731
745
  settings = {
732
746
  path: path,
733
747
  url: url,
@@ -745,7 +759,9 @@ Append the following to your query string:
745
759
  startHidden: @config.start_hidden,
746
760
  collapseResults: @config.collapse_results,
747
761
  htmlContainer: @config.html_container,
748
- hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(',')
762
+ hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(','),
763
+ cspNonce: content_security_policy_nonce,
764
+ hotwireTurboDriveSupport: @config.enable_hotwire_turbo_drive_support,
749
765
  }
750
766
 
751
767
  if current && current.page_struct
@@ -757,7 +773,7 @@ Append the following to your query string:
757
773
  end
758
774
 
759
775
  # TODO : cache this snippet
760
- script = IO.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
776
+ script = ::File.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
761
777
  # replace the variables
762
778
  settings.each do |k, v|
763
779
  regex = Regexp.new("\\{#{k.to_s}\\}")
@@ -786,16 +802,16 @@ Append the following to your query string:
786
802
  headers = { 'Content-Type' => 'text/html' }
787
803
  qp = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
788
804
  if group_name = qp["group_name"]
789
- list = @storage.find_snapshots_group(group_name)
805
+ list = @storage.snapshots_group(group_name)
790
806
  list.each do |snapshot|
791
- snapshot[:url] = url_for_snapshot(snapshot[:id])
807
+ snapshot[:url] = url_for_snapshot(snapshot[:id], group_name)
792
808
  end
793
809
  data = {
794
810
  group_name: group_name,
795
811
  list: list
796
812
  }
797
813
  else
798
- list = @storage.snapshot_groups_overview
814
+ list = @storage.snapshots_overview
799
815
  list.each do |group|
800
816
  group[:url] = url_for_snapshots_group(group[:name])
801
817
  end
@@ -848,7 +864,7 @@ Append the following to your query string:
848
864
  if defined?(Rails) && defined?(ActionController::RoutingError)
849
865
  hash = Rails.application.routes.recognize_path(path, method: method)
850
866
  if hash && hash[:controller] && hash[:action]
851
- "#{method} #{hash[:controller]}##{hash[:action]}"
867
+ "#{hash[:controller]}##{hash[:action]}"
852
868
  end
853
869
  end
854
870
  rescue ActionController::RoutingError
@@ -860,8 +876,8 @@ Append the following to your query string:
860
876
  "/#{@config.base_url_path.gsub('/', '')}/snapshots?#{qs}"
861
877
  end
862
878
 
863
- def url_for_snapshot(id)
864
- qs = Rack::Utils.build_query({ id: id, snapshot: true })
879
+ def url_for_snapshot(id, group_name)
880
+ qs = Rack::Utils.build_query({ id: id, group: group_name })
865
881
  "/#{@config.base_url_path.gsub('/', '')}/results?#{qs}"
866
882
  end
867
883
 
@@ -886,8 +902,12 @@ Append the following to your query string:
886
902
  if Rack::MiniProfiler.snapshots_transporter?
887
903
  Rack::MiniProfiler::SnapshotsTransporter.transport(page_struct)
888
904
  else
905
+ group_name = rails_route_from_path(page_struct[:request_path], page_struct[:request_method])
906
+ group_name ||= page_struct[:request_path]
907
+ group_name = "#{page_struct[:request_method]} #{group_name}"
889
908
  @storage.push_snapshot(
890
909
  page_struct,
910
+ group_name,
891
911
  @config
892
912
  )
893
913
  end
@@ -45,80 +45,53 @@ module Rack
45
45
  raise NotImplementedError.new("should_take_snapshot? is not implemented")
46
46
  end
47
47
 
48
- def push_snapshot(page_struct, config)
48
+ def push_snapshot(page_struct, group_name, config)
49
49
  raise NotImplementedError.new("push_snapshot is not implemented")
50
50
  end
51
51
 
52
- def fetch_snapshots(batch_size: 200, &blk)
53
- raise NotImplementedError.new("fetch_snapshots is not implemented")
52
+ # returns a hash where the keys are group names and the values
53
+ # are hashes that contain 3 keys:
54
+ # 1. `:worst_score` => the duration of the worst/slowest snapshot in the group (float)
55
+ # 2. `:best_score` => the duration of the best/fastest snapshot in the group (float)
56
+ # 3. `:snapshots_count` => the number of snapshots in the group (integer)
57
+ def fetch_snapshots_overview
58
+ raise NotImplementedError.new("fetch_snapshots_overview is not implemented")
54
59
  end
55
60
 
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
- hash = groups[group_name] ||= {}
62
- hash[:snapshots_count] ||= 0
63
- hash[:snapshots_count] += 1
64
- if !hash[:worst_score] || hash[:worst_score] < snapshot.duration_ms
65
- groups[group_name][:worst_score] = snapshot.duration_ms
66
- end
67
- if !hash[:best_score] || hash[:best_score] > snapshot.duration_ms
68
- groups[group_name][:best_score] = snapshot.duration_ms
69
- end
70
- end
71
- end
72
- groups = groups.to_a
61
+ # @param group_name [String]
62
+ # @return [Array<Rack::MiniProfiler::TimerStruct::Page>] list of snapshots of the group. Blank array if the group doesn't exist.
63
+ def fetch_snapshots_group(group_name)
64
+ raise NotImplementedError.new("fetch_snapshots_group is not implemented")
65
+ end
66
+
67
+ def load_snapshot(id, group_name)
68
+ raise NotImplementedError.new("load_snapshot is not implemented")
69
+ end
70
+
71
+ def snapshots_overview
72
+ groups = fetch_snapshots_overview.to_a
73
73
  groups.sort_by! { |name, hash| hash[:worst_score] }
74
74
  groups.reverse!
75
75
  groups.map! { |name, hash| hash.merge(name: name) }
76
76
  groups
77
77
  end
78
78
 
79
- def find_snapshots_group(group_name)
79
+ def snapshots_group(group_name)
80
+ snapshots = fetch_snapshots_group(group_name)
80
81
  data = []
81
- fetch_snapshots do |batch|
82
- batch.each do |snapshot|
83
- snapshot_group_name = default_snapshot_grouping(snapshot)
84
- if group_name == snapshot_group_name
85
- data << {
86
- id: snapshot[:id],
87
- duration: snapshot.duration_ms,
88
- sql_count: snapshot[:sql_count],
89
- timestamp: snapshot[:started_at],
90
- custom_fields: snapshot[:custom_fields]
91
- }
92
- end
93
- end
82
+ snapshots.each do |snapshot|
83
+ data << {
84
+ id: snapshot[:id],
85
+ duration: snapshot.duration_ms,
86
+ sql_count: snapshot[:sql_count],
87
+ timestamp: snapshot[:started_at],
88
+ custom_fields: snapshot[:custom_fields]
89
+ }
94
90
  end
95
91
  data.sort_by! { |s| s[:duration] }
96
92
  data.reverse!
97
93
  data
98
94
  end
99
-
100
- def load_snapshot(id)
101
- raise NotImplementedError.new("load_snapshot is not implemented")
102
- end
103
-
104
- private
105
-
106
- def default_snapshot_grouping(snapshot)
107
- group_name = rails_route_from_path(snapshot[:request_path], snapshot[:request_method])
108
- group_name ||= snapshot[:request_path]
109
- "#{snapshot[:request_method]} #{group_name}"
110
- end
111
-
112
- def rails_route_from_path(path, method)
113
- if defined?(Rails) && defined?(ActionController::RoutingError)
114
- hash = Rails.application.routes.recognize_path(path, method: method)
115
- if hash && hash[:controller] && hash[:action]
116
- "#{hash[:controller]}##{hash[:action]}"
117
- end
118
- end
119
- rescue ActionController::RoutingError
120
- nil
121
- end
122
95
  end
123
96
  end
124
97
  end
@@ -17,7 +17,9 @@ module Rack
17
17
  def [](key)
18
18
  begin
19
19
  data = ::File.open(path(key), "rb") { |f| f.read }
20
+ # rubocop:disable Security/MarshalLoad
20
21
  Marshal.load data
22
+ # rubocop:enable Security/MarshalLoad
21
23
  rescue
22
24
  nil
23
25
  end
@@ -24,7 +24,9 @@ module Rack
24
24
 
25
25
  def load(id)
26
26
  raw = @client.get("#{@prefix}#{id}")
27
- Marshal::load(raw) if raw
27
+ # rubocop:disable Security/MarshalLoad
28
+ Marshal.load(raw) if raw
29
+ # rubocop:enable Security/MarshalLoad
28
30
  end
29
31
 
30
32
  def set_unviewed(user, id)
@@ -65,14 +67,16 @@ module Rack
65
67
  key1, key2, cycle_at = nil
66
68
 
67
69
  if token_info
68
- key1, key2, cycle_at = Marshal::load(token_info)
70
+ # rubocop:disable Security/MarshalLoad
71
+ key1, key2, cycle_at = Marshal.load(token_info)
72
+ # rubocop:enable Security/MarshalLoad
69
73
 
70
- key1 = nil unless key1 && key1.length == 32
71
- key2 = nil unless key2 && key2.length == 32
74
+ key1 = nil unless key1 && key1.length == 32
75
+ key2 = nil unless key2 && key2.length == 32
72
76
 
73
- if key1 && cycle_at && (cycle_at > Process.clock_gettime(Process::CLOCK_MONOTONIC))
74
- return [key1, key2].compact
75
- end
77
+ if key1 && cycle_at && (cycle_at > Process.clock_gettime(Process::CLOCK_MONOTONIC))
78
+ return [key1, key2].compact
79
+ end
76
80
  end
77
81
 
78
82
  timeout = Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE
@@ -53,6 +53,7 @@ module Rack
53
53
 
54
54
  @token1, @token2, @cycle_at = nil
55
55
  @snapshots_cycle = 0
56
+ @snapshot_groups = {}
56
57
  @snapshots = []
57
58
 
58
59
  initialize_locks
@@ -152,28 +153,69 @@ module Rack
152
153
  end
153
154
  end
154
155
 
155
- def push_snapshot(page_struct, config)
156
+ def push_snapshot(page_struct, group_name, config)
156
157
  @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)
158
+ group = @snapshot_groups[group_name]
159
+ if !group
160
+ @snapshot_groups[group_name] = {
161
+ worst_score: page_struct.duration_ms,
162
+ best_score: page_struct.duration_ms,
163
+ snapshots: [page_struct]
164
+ }
165
+ if @snapshot_groups.size > config.max_snapshot_groups
166
+ group_keys = @snapshot_groups.keys
167
+ group_keys.sort_by! do |key|
168
+ @snapshot_groups[key][:worst_score]
169
+ end
170
+ group_keys.reverse!
171
+ group_keys.pop(group_keys.size - config.max_snapshot_groups)
172
+ @snapshot_groups = @snapshot_groups.slice(*group_keys)
173
+ end
174
+ else
175
+ snapshots = group[:snapshots]
176
+ snapshots << page_struct
177
+ snapshots.sort_by!(&:duration_ms)
178
+ snapshots.reverse!
179
+ if snapshots.size > config.max_snapshots_per_group
180
+ snapshots.pop(snapshots.size - config.max_snapshots_per_group)
181
+ end
182
+ group[:worst_score] = snapshots[0].duration_ms
183
+ group[:best_score] = snapshots[-1].duration_ms
162
184
  end
163
185
  end
164
186
  end
165
187
 
166
- def fetch_snapshots(batch_size: 200, &blk)
188
+ def fetch_snapshots_overview
167
189
  @snapshots_lock.synchronize do
168
- @snapshots.each_slice(batch_size) do |batch|
169
- blk.call(batch)
190
+ groups = {}
191
+ @snapshot_groups.each do |name, group|
192
+ groups[name] = {
193
+ worst_score: group[:worst_score],
194
+ best_score: group[:best_score],
195
+ snapshots_count: group[:snapshots].size
196
+ }
170
197
  end
198
+ groups
171
199
  end
172
200
  end
173
201
 
174
- def load_snapshot(id)
202
+ def fetch_snapshots_group(group_name)
175
203
  @snapshots_lock.synchronize do
176
- @snapshots.find { |s| s[:id] == id }
204
+ group = @snapshot_groups[group_name]
205
+ if group
206
+ group[:snapshots].dup
207
+ else
208
+ []
209
+ end
210
+ end
211
+ end
212
+
213
+ def load_snapshot(id, group_name)
214
+ @snapshots_lock.synchronize do
215
+ group = @snapshot_groups[group_name]
216
+ if group
217
+ group[:snapshots].find { |s| s[:id] == id }
218
+ end
177
219
  end
178
220
  end
179
221
 
@@ -182,7 +224,7 @@ module Rack
182
224
  # used in tests only
183
225
  def wipe_snapshots_data
184
226
  @snapshots_cycle = 0
185
- @snapshots = []
227
+ @snapshot_groups = {}
186
228
  end
187
229
  end
188
230
  end
@@ -25,7 +25,9 @@ module Rack
25
25
  key = prefixed_id(id)
26
26
  raw = redis.get key
27
27
  begin
28
- Marshal::load(raw) if raw
28
+ # rubocop:disable Security/MarshalLoad
29
+ Marshal.load(raw) if raw
30
+ # rubocop:enable Security/MarshalLoad
29
31
  rescue
30
32
  # bad format, junk old data
31
33
  redis.del key
@@ -131,81 +133,127 @@ unviewed_ids: #{get_unviewed_ids(user)}
131
133
  )
132
134
  end
133
135
 
134
- def push_snapshot(page_struct, config)
135
- zset_key = snapshot_zset_key()
136
- hash_key = snapshot_hash_key()
136
+ def push_snapshot(page_struct, group_name, config)
137
+ group_zset_key = group_snapshot_zset_key(group_name)
138
+ group_hash_key = group_snapshot_hash_key(group_name)
139
+ overview_zset_key = snapshot_overview_zset_key
137
140
 
138
141
  id = page_struct[:id]
139
- score = page_struct.duration_ms
140
- limit = config.snapshots_limit
142
+ score = page_struct.duration_ms.to_s
143
+
144
+ per_group_limit = config.max_snapshots_per_group.to_s
145
+ groups_limit = config.max_snapshot_groups.to_s
141
146
  bytes = Marshal.dump(page_struct)
142
147
 
143
148
  lua = <<~LUA
144
- local zset_key = KEYS[1]
145
- local hash_key = KEYS[2]
149
+ local group_zset_key = KEYS[1]
150
+ local group_hash_key = KEYS[2]
151
+ local overview_zset_key = KEYS[3]
152
+
146
153
  local id = ARGV[1]
147
154
  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)
155
+ local group_name = ARGV[3]
156
+ local per_group_limit = tonumber(ARGV[4])
157
+ local groups_limit = tonumber(ARGV[5])
158
+ local prefix = ARGV[6]
159
+ local bytes = ARGV[7]
160
+
161
+ local current_group_score = redis.call("ZSCORE", overview_zset_key, group_name)
162
+ if current_group_score == false or score > tonumber(current_group_score) then
163
+ redis.call("ZADD", overview_zset_key, score, group_name)
164
+ end
165
+
166
+ local do_save = true
167
+ local overview_size = redis.call("ZCARD", overview_zset_key)
168
+ while (overview_size > groups_limit) do
169
+ local lowest_group = redis.call("ZRANGE", overview_zset_key, 0, 0)[1]
170
+ redis.call("ZREM", overview_zset_key, lowest_group)
171
+ if lowest_group == group_name then
172
+ do_save = false
173
+ else
174
+ local lowest_group_zset_key = prefix .. "-mp-group-snapshot-zset-key-" .. lowest_group
175
+ local lowest_group_hash_key = prefix .. "-mp-group-snapshot-hash-key-" .. lowest_group
176
+ redis.call("DEL", lowest_group_zset_key, lowest_group_hash_key)
177
+ end
178
+ overview_size = overview_size - 1
179
+ end
180
+
181
+ if do_save then
182
+ redis.call("ZADD", group_zset_key, score, id)
183
+ local group_size = redis.call("ZCARD", group_zset_key)
184
+ while (group_size > per_group_limit) do
185
+ local lowest_snapshot_id = redis.call("ZRANGE", group_zset_key, 0, 0)[1]
186
+ redis.call("ZREM", group_zset_key, lowest_snapshot_id)
187
+ if lowest_snapshot_id == id then
188
+ do_save = false
189
+ else
190
+ redis.call("HDEL", group_hash_key, lowest_snapshot_id)
191
+ end
192
+ group_size = group_size - 1
193
+ end
194
+ if do_save then
195
+ redis.call("HSET", group_hash_key, id, bytes)
196
+ end
156
197
  end
157
198
  LUA
158
199
  redis.eval(
159
200
  lua,
160
- keys: [zset_key, hash_key],
161
- argv: [id, score, bytes, limit]
201
+ keys: [group_zset_key, group_hash_key, overview_zset_key],
202
+ argv: [id, score, group_name, per_group_limit, groups_limit, @prefix, bytes]
162
203
  )
163
204
  end
164
205
 
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
206
+ def fetch_snapshots_overview
207
+ overview_zset_key = snapshot_overview_zset_key
208
+ groups = redis
209
+ .zrange(overview_zset_key, 0, -1, withscores: true)
210
+ .map { |(name, worst_score)| [name, { worst_score: worst_score }] }
211
+
212
+ prefixed_group_names = groups.map { |(group_name, _)| group_snapshot_zset_key(group_name) }
213
+ metadata = redis.eval(<<~LUA, keys: prefixed_group_names)
214
+ local metadata = {}
215
+ for i, k in ipairs(KEYS) do
216
+ local best = redis.call("ZRANGE", k, 0, 0, "WITHSCORES")[2]
217
+ local count = redis.call("ZCARD", k)
218
+ metadata[i] = {best, count}
185
219
  end
186
- batch.compact!
187
- blk.call(batch) if batch.size != 0
188
- break if ids.size < batch_size
189
- iteration += 1
220
+ return metadata
221
+ LUA
222
+ groups.each.with_index do |(_, hash), index|
223
+ best, count = metadata[index]
224
+ hash[:best_score] = best.to_f
225
+ hash[:snapshots_count] = count.to_i
226
+ end
227
+ groups.to_h
228
+ end
229
+
230
+ def fetch_snapshots_group(group_name)
231
+ group_hash_key = group_snapshot_hash_key(group_name)
232
+ snapshots = []
233
+ corrupt_snapshots = []
234
+ redis.hgetall(group_hash_key).each do |id, bytes|
235
+ # rubocop:disable Security/MarshalLoad
236
+ snapshots << Marshal.load(bytes)
237
+ # rubocop:enable Security/MarshalLoad
238
+ rescue
239
+ corrupt_snapshots << id
190
240
  end
191
241
  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
242
+ cleanup_corrupt_snapshots(corrupt_snapshots, group_name)
196
243
  end
244
+ snapshots
197
245
  end
198
246
 
199
- def load_snapshot(id)
200
- hash_key = snapshot_hash_key()
201
- bytes = redis.hget(hash_key, id)
247
+ def load_snapshot(id, group_name)
248
+ group_hash_key = group_snapshot_hash_key(group_name)
249
+ bytes = redis.hget(group_hash_key, id)
250
+ return if !bytes
202
251
  begin
252
+ # rubocop:disable Security/MarshalLoad
203
253
  Marshal.load(bytes)
254
+ # rubocop:enable Security/MarshalLoad
204
255
  rescue
205
- redis.pipelined do
206
- redis.zrem(snapshot_zset_key(), id)
207
- redis.hdel(hash_key, id)
208
- end
256
+ cleanup_corrupt_snapshots([id], group_name)
209
257
  nil
210
258
  end
211
259
  end
@@ -231,12 +279,20 @@ unviewed_ids: #{get_unviewed_ids(user)}
231
279
  @snapshot_counter_key ||= "#{@prefix}-mini-profiler-snapshots-counter"
232
280
  end
233
281
 
234
- def snapshot_zset_key
235
- @snapshot_zset_key ||= "#{@prefix}-mini-profiler-snapshots-zset"
282
+ def group_snapshot_zset_key(group_name)
283
+ # if you change this key, remember to change it in the LUA script in
284
+ # the push_snapshot method as well
285
+ "#{@prefix}-mp-group-snapshot-zset-key-#{group_name}"
236
286
  end
237
287
 
238
- def snapshot_hash_key
239
- @snapshot_hash_key ||= "#{@prefix}-mini-profiler-snapshots-hash"
288
+ def group_snapshot_hash_key(group_name)
289
+ # if you change this key, remember to change it in the LUA script in
290
+ # the push_snapshot method as well
291
+ "#{@prefix}-mp-group-snapshot-hash-key-#{group_name}"
292
+ end
293
+
294
+ def snapshot_overview_zset_key
295
+ "#{@prefix}-mp-overviewgroup-snapshot-zset-key"
240
296
  end
241
297
 
242
298
  def cached_redis_eval(script, script_sha, reraise: true, argv: [], keys: [])
@@ -251,13 +307,45 @@ unviewed_ids: #{get_unviewed_ids(user)}
251
307
  end
252
308
  end
253
309
 
310
+ def cleanup_corrupt_snapshots(corrupt_snapshots_ids, group_name)
311
+ group_hash_key = group_snapshot_hash_key(group_name)
312
+ group_zset_key = group_snapshot_zset_key(group_name)
313
+ overview_zset_key = snapshot_overview_zset_key
314
+ lua = <<~LUA
315
+ local group_hash_key = KEYS[1]
316
+ local group_zset_key = KEYS[2]
317
+ local overview_zset_key = KEYS[3]
318
+ local group_name = ARGV[1]
319
+ for i, k in ipairs(ARGV) do
320
+ if k ~= group_name then
321
+ redis.call("HDEL", group_hash_key, k)
322
+ redis.call("ZREM", group_zset_key, k)
323
+ end
324
+ end
325
+ if redis.call("ZCARD", group_zset_key) == 0 then
326
+ redis.call("ZREM", overview_zset_key, group_name)
327
+ redis.call("DEL", group_hash_key, group_zset_key)
328
+ else
329
+ local worst_score = tonumber(redis.call("ZRANGE", group_zset_key, -1, -1, "WITHSCORES")[2])
330
+ redis.call("ZADD", overview_zset_key, worst_score, group_name)
331
+ end
332
+ LUA
333
+ redis.eval(
334
+ lua,
335
+ keys: [group_hash_key, group_zset_key, overview_zset_key],
336
+ argv: [group_name, *corrupt_snapshots_ids]
337
+ )
338
+ end
339
+
254
340
  # only used in tests
255
341
  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
342
+ keys = redis.keys(group_snapshot_hash_key('*'))
343
+ keys += redis.keys(group_snapshot_zset_key('*'))
344
+ redis.del(
345
+ keys,
346
+ snapshot_overview_zset_key,
347
+ snapshot_counter_key
348
+ )
261
349
  end
262
350
  end
263
351
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Rack
4
4
  class MiniProfiler
5
- VERSION = '2.3.2'
5
+ VERSION = '3.0.0'
6
+ SOURCE_CODE_URI = 'https://github.com/MiniProfiler/rack-mini-profiler'
6
7
  end
7
8
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # riak-client 2.2.2 patches
4
4
  class Riak::Multiget
5
- class <<self
5
+ class << self
6
6
  alias_method :get_all_without_profiling, :get_all
7
7
  def get_all(client, fetch_list)
8
8
  return get_all_without_profiling(client, fetch_list) unless SqlPatches.should_measure?
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
24
24
  s.required_ruby_version = '>= 2.4.0'
25
25
 
26
26
  s.metadata = {
27
- 'source_code_uri' => 'https://github.com/MiniProfiler/rack-mini-profiler',
27
+ 'source_code_uri' => Rack::MiniProfiler::SOURCE_CODE_URI,
28
28
  'changelog_uri' => 'https://github.com/MiniProfiler/rack-mini-profiler/blob/master/CHANGELOG.md'
29
29
  }
30
30
 
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.3.2
4
+ version: 3.0.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: 2021-04-29 00:00:00.000000000 Z
13
+ date: 2022-02-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
@@ -354,7 +354,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
354
354
  - !ruby/object:Gem::Version
355
355
  version: '0'
356
356
  requirements: []
357
- rubygems_version: 3.2.2
357
+ rubygems_version: 3.1.6
358
358
  signing_key:
359
359
  specification_version: 4
360
360
  summary: Profiles loading speed for rack applications.