rails-profiler 0.35.2 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/app/assets/builds/profiler-toolbar.js +2 -4454
- data/app/assets/builds/profiler.css +1 -3546
- data/app/assets/builds/profiler.js +14 -9699
- data/app/assets/javascript/profiler-ajax-interceptor.js +9 -0
- data/app/controllers/profiler/assets_controller.rb +14 -5
- data/app/views/layouts/profiler/application.html.erb +2 -2
- data/app/views/layouts/profiler/embedded.html.erb +6 -4
- data/lib/profiler/assets.rb +61 -0
- data/lib/profiler/middleware/toolbar_injector.rb +44 -12
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +1 -0
- metadata +2 -1
|
@@ -33,6 +33,15 @@
|
|
|
33
33
|
return window.__PROFILER_PARENT_TOKEN__;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// A Turbo Drive visit to a page the profiler left alone brings no inline script to set a new
|
|
37
|
+
// token, and the window keeps the one of the previous page: forget it, or every request of
|
|
38
|
+
// this page would be linked to a page the user has left.
|
|
39
|
+
document.addEventListener('turbo:load', function() {
|
|
40
|
+
if (!document.getElementById('profiler-toolbar')) {
|
|
41
|
+
delete window.__PROFILER_PARENT_TOKEN__;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
36
45
|
function isRequest(input) {
|
|
37
46
|
return typeof Request !== 'undefined' && input instanceof Request;
|
|
38
47
|
}
|
|
@@ -10,16 +10,21 @@ module Profiler
|
|
|
10
10
|
|
|
11
11
|
BUILD_MISSING = "Profiler assets are not built (%s is missing). Run `yarn build:full` in the profiler gem."
|
|
12
12
|
|
|
13
|
+
# A year: the URL a page uses carries the digest of the content (?v=), so a new version of
|
|
14
|
+
# the gem is a new URL. Without the digest, or with an old one, the file is only kept an
|
|
15
|
+
# hour, as it may be replaced under the same URL.
|
|
16
|
+
VERSIONED_CACHE_CONTROL = "public, max-age=#{365 * 24 * 3600}, immutable".freeze
|
|
17
|
+
|
|
13
18
|
def toolbar_js
|
|
14
|
-
serve
|
|
19
|
+
serve "profiler-toolbar.js", "application/javascript"
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
def main_js
|
|
18
|
-
serve
|
|
23
|
+
serve "profiler.js", "application/javascript"
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
def main_css
|
|
22
|
-
serve
|
|
27
|
+
serve "profiler.css", "text/css"
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
def icon
|
|
@@ -30,8 +35,12 @@ module Profiler
|
|
|
30
35
|
private
|
|
31
36
|
|
|
32
37
|
def serve(name, content_type)
|
|
33
|
-
body =
|
|
34
|
-
|
|
38
|
+
body = Profiler::Assets.read(name)
|
|
39
|
+
if Profiler::Assets.current?(name, params[:v])
|
|
40
|
+
response.headers["Cache-Control"] = VERSIONED_CACHE_CONTROL
|
|
41
|
+
else
|
|
42
|
+
expires_in 1.hour, public: true
|
|
43
|
+
end
|
|
35
44
|
render plain: body, content_type: content_type
|
|
36
45
|
rescue Errno::ENOENT
|
|
37
46
|
render plain: format(BUILD_MISSING, name), status: :not_found
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
<meta name="profiler-name" content="<%= Profiler.configuration.resolved_name %>">
|
|
11
11
|
<meta name="profiler-is-master" content="<%= Profiler.configuration.master? %>">
|
|
12
12
|
|
|
13
|
-
<link rel="stylesheet" href="
|
|
14
|
-
<script src="
|
|
13
|
+
<link rel="stylesheet" href="<%= Profiler::Assets.url("profiler.css") %>">
|
|
14
|
+
<script src="<%= Profiler::Assets.url("profiler.js") %>" defer></script>
|
|
15
15
|
</head>
|
|
16
16
|
|
|
17
17
|
<body>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<% nonce = content_security_policy_nonce %>
|
|
2
|
+
<% nonce_attr = nonce ? %( nonce="#{ERB::Util.html_escape(nonce)}").html_safe : "" %>
|
|
1
3
|
<!DOCTYPE html>
|
|
2
4
|
<html data-theme="<%= params[:theme].presence&.then { |t| %w[light dark].include?(t) ? t : 'dark' } || 'dark' %>">
|
|
3
5
|
<head>
|
|
@@ -8,16 +10,16 @@
|
|
|
8
10
|
<%= csp_meta_tag %>
|
|
9
11
|
<meta name="profiler-version" content="<%= Profiler.version %>">
|
|
10
12
|
|
|
11
|
-
<link rel="stylesheet" href="
|
|
12
|
-
<script src="
|
|
13
|
+
<link rel="stylesheet" href="<%= Profiler::Assets.url("profiler.css") %>">
|
|
14
|
+
<script src="<%= Profiler::Assets.url("profiler.js") %>" defer></script>
|
|
13
15
|
|
|
14
|
-
<style
|
|
16
|
+
<style<%= nonce_attr %>>
|
|
15
17
|
/* Embedded overrides, tighter layout, no page chrome */
|
|
16
18
|
body { overflow-y: auto; }
|
|
17
19
|
.container { padding: 16px 20px; }
|
|
18
20
|
</style>
|
|
19
21
|
|
|
20
|
-
<script
|
|
22
|
+
<script<%= nonce_attr %>>
|
|
21
23
|
// Receive theme changes from the Chrome extension panel via postMessage.
|
|
22
24
|
// The extension sends: { type: 'profiler:set-theme', theme: 'light' | 'dark' }
|
|
23
25
|
window.addEventListener('message', function(e) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Profiler
|
|
6
|
+
# The gem's built front end files. Each is read once and kept until the file changes on disk
|
|
7
|
+
# (a rebuild in development), and named by a digest of its content so that a browser that
|
|
8
|
+
# cached one never keeps it past a new version of the gem.
|
|
9
|
+
module Assets
|
|
10
|
+
NAMES = %w[profiler.js profiler.css profiler-toolbar.js].freeze
|
|
11
|
+
URL_PREFIX = "/_profiler/assets"
|
|
12
|
+
|
|
13
|
+
Entry = Struct.new(:mtime, :content, :digest)
|
|
14
|
+
|
|
15
|
+
@entries = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Where the build writes the files; the gem's own, never the application's.
|
|
20
|
+
def builds_dir
|
|
21
|
+
File.expand_path("../../app/assets/builds", __dir__)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The file's content, or raises Errno::ENOENT when the assets are not built.
|
|
25
|
+
def read(name)
|
|
26
|
+
entry(name).content
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Short digest of the content; the gem version when the file is not built.
|
|
30
|
+
def digest(name)
|
|
31
|
+
entry(name).digest
|
|
32
|
+
rescue Errno::ENOENT
|
|
33
|
+
Digest::SHA256.hexdigest(Profiler::VERSION)[0, 12]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The URL a page uses for the asset, carrying its digest.
|
|
37
|
+
def url(name)
|
|
38
|
+
"#{URL_PREFIX}/#{name}?v=#{digest(name)}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def current?(name, version)
|
|
42
|
+
version.is_a?(String) && !version.empty? && version == digest(name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def entry(name)
|
|
48
|
+
raise ArgumentError, "unknown asset: #{name}" unless NAMES.include?(name)
|
|
49
|
+
|
|
50
|
+
path = File.join(builds_dir, name)
|
|
51
|
+
mtime = File.mtime(path)
|
|
52
|
+
cached = @mutex.synchronize { @entries[name] }
|
|
53
|
+
return cached if cached && cached.mtime == mtime
|
|
54
|
+
|
|
55
|
+
content = File.read(path)
|
|
56
|
+
fresh = Entry.new(mtime, content, Digest::SHA256.hexdigest(content)[0, 12])
|
|
57
|
+
@mutex.synchronize { @entries[name] = fresh }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -38,6 +38,24 @@ module Profiler
|
|
|
38
38
|
)*+
|
|
39
39
|
}xi
|
|
40
40
|
|
|
41
|
+
INTERCEPTOR_PATH = File.expand_path("../../../app/assets/javascript/profiler-ajax-interceptor.js", __dir__)
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# The script is the same for every request of a version of the gem: read once. A failed
|
|
45
|
+
# read is not kept, so that the next request tries again.
|
|
46
|
+
def ajax_interceptor_code
|
|
47
|
+
@ajax_interceptor_code ||= File.read(INTERCEPTOR_PATH).freeze
|
|
48
|
+
rescue => e
|
|
49
|
+
Profiler.log_error("ToolbarInjector: could not load the AJAX interceptor script", e)
|
|
50
|
+
""
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The toolbar CSS is a constant of the class too, built once.
|
|
54
|
+
def toolbar_styles
|
|
55
|
+
@toolbar_styles ||= build_toolbar_styles
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
41
59
|
def initialize(body, token, nonce = nil)
|
|
42
60
|
@body = body
|
|
43
61
|
@token = token
|
|
@@ -139,13 +157,7 @@ module Profiler
|
|
|
139
157
|
end
|
|
140
158
|
|
|
141
159
|
def ajax_interceptor_code
|
|
142
|
-
|
|
143
|
-
path = File.join(__dir__, "..", "..", "..", "app", "assets", "javascript", "profiler-ajax-interceptor.js")
|
|
144
|
-
File.read(path)
|
|
145
|
-
rescue => e
|
|
146
|
-
Profiler.log_error("ToolbarInjector: could not load the AJAX interceptor script", e)
|
|
147
|
-
""
|
|
148
|
-
end
|
|
160
|
+
self.class.ajax_interceptor_code
|
|
149
161
|
end
|
|
150
162
|
|
|
151
163
|
# The middleware hands over the page it has already read: an Array of parts, or a String.
|
|
@@ -157,11 +169,11 @@ module Profiler
|
|
|
157
169
|
def toolbar_html
|
|
158
170
|
<<~HTML
|
|
159
171
|
#{ajax_interceptor_script}
|
|
160
|
-
<style>#{toolbar_styles}</style>
|
|
172
|
+
<style#{nonce_attr}>#{self.class.toolbar_styles}</style>
|
|
161
173
|
<div id="profiler-toolbar" class="profiler-root" data-token="#{ERB::Util.html_escape(@token)}"></div>
|
|
162
174
|
<button id="profiler-toolbar-toggle" class="profiler-root" title="Toggle profiler (Alt+P)"><span class="profiler-toggle-icon">▶</span></button>
|
|
163
|
-
<script#{nonce_attr}>(function(){
|
|
164
|
-
<script src="
|
|
175
|
+
<script#{nonce_attr}>(function(){function g(k){try{return localStorage.getItem(k)}catch(e){return null}}var c=g('profiler-toolbar-collapsed')==='true',t=g('profiler-theme'),theme=t==='light'?'light':t==='dark'?'dark':(window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'),el=document.getElementById('profiler-toolbar'),tog=document.getElementById('profiler-toolbar-toggle');if(c){el.style.cssText='animation:none!important;transform:translateX(calc(100% + 44px))';tog.dataset.collapsed='true';}el.setAttribute('data-theme',theme);tog.setAttribute('data-theme',theme);})();</script>
|
|
176
|
+
<script src="#{Profiler::Assets.url("profiler-toolbar.js")}" defer#{nonce_attr}></script>
|
|
165
177
|
HTML
|
|
166
178
|
end
|
|
167
179
|
|
|
@@ -176,7 +188,7 @@ module Profiler
|
|
|
176
188
|
|
|
177
189
|
# Thermal design system, self-contained CSS for the injected toolbar.
|
|
178
190
|
# Variables are defined on #profiler-toolbar to avoid polluting the host app.
|
|
179
|
-
def
|
|
191
|
+
def self.build_toolbar_styles
|
|
180
192
|
<<~'CSS'
|
|
181
193
|
.profiler-root {
|
|
182
194
|
--pf-bg: #080b10;
|
|
@@ -324,6 +336,26 @@ module Profiler
|
|
|
324
336
|
}
|
|
325
337
|
#profiler-toolbar .profiler-toolbar-logo:hover { color: var(--pf-amber-h); }
|
|
326
338
|
|
|
339
|
+
/* Error state: the profile could not be loaded, or a component failed. */
|
|
340
|
+
#profiler-toolbar .profiler-toolbar-error {
|
|
341
|
+
display: inline-flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
gap: 10px;
|
|
344
|
+
padding: 0 13px;
|
|
345
|
+
}
|
|
346
|
+
#profiler-toolbar .profiler-toolbar-retry {
|
|
347
|
+
font-family: var(--pf-mono);
|
|
348
|
+
font-size: 11px;
|
|
349
|
+
color: var(--pf-amber);
|
|
350
|
+
background: transparent;
|
|
351
|
+
border: 1px solid var(--pf-border-s);
|
|
352
|
+
border-radius: 3px;
|
|
353
|
+
padding: 2px 8px;
|
|
354
|
+
cursor: pointer;
|
|
355
|
+
}
|
|
356
|
+
#profiler-toolbar .profiler-toolbar-retry:hover:not(:disabled) { color: var(--pf-amber-h); background: var(--pf-amber-bg); }
|
|
357
|
+
#profiler-toolbar .profiler-toolbar-retry:disabled { cursor: default; opacity: .6; }
|
|
358
|
+
|
|
327
359
|
/* ── Toggle button (fixed, always visible) ───────────────────────── */
|
|
328
360
|
#profiler-toolbar-toggle {
|
|
329
361
|
position: fixed;
|
|
@@ -347,7 +379,7 @@ module Profiler
|
|
|
347
379
|
animation: pfPop 380ms ease-out both;
|
|
348
380
|
transform: none;
|
|
349
381
|
}
|
|
350
|
-
.profiler-toggle-icon {
|
|
382
|
+
#profiler-toolbar-toggle .profiler-toggle-icon {
|
|
351
383
|
display: inline-block;
|
|
352
384
|
transition: transform 280ms cubic-bezier(.4,0,.2,1);
|
|
353
385
|
}
|
data/lib/profiler/version.rb
CHANGED
data/lib/profiler.rb
CHANGED
|
@@ -273,6 +273,7 @@ require_relative "profiler/collectors/mailer_collector"
|
|
|
273
273
|
require_relative "profiler/storage/token"
|
|
274
274
|
require_relative "profiler/storage/private_files"
|
|
275
275
|
require_relative "profiler/storage/unavailable"
|
|
276
|
+
require_relative "profiler/assets"
|
|
276
277
|
require_relative "profiler/ajax_data"
|
|
277
278
|
require_relative "profiler/env_override_store"
|
|
278
279
|
require_relative "profiler/instrumentation/thread_context_propagation"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.36.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
@@ -145,6 +145,7 @@ files:
|
|
|
145
145
|
- lib/profiler.rb
|
|
146
146
|
- lib/profiler/ajax_data.rb
|
|
147
147
|
- lib/profiler/allocation_counter.rb
|
|
148
|
+
- lib/profiler/assets.rb
|
|
148
149
|
- lib/profiler/body_content.rb
|
|
149
150
|
- lib/profiler/boot_env.rb
|
|
150
151
|
- lib/profiler/cluster/master_client.rb
|