rails-profiler 0.31.0 → 0.31.2
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 +139 -0
- data/app/assets/builds/profiler-toolbar.js +13 -6
- data/app/assets/builds/profiler.js +53 -33
- data/app/controllers/profiler/api/ajax_controller.rb +6 -0
- data/app/controllers/profiler/api/env_vars_controller.rb +10 -0
- data/lib/profiler/allocation_counter.rb +21 -0
- data/lib/profiler/cluster/slave_proxy.rb +6 -0
- data/lib/profiler/collectors/base_collector.rb +31 -0
- data/lib/profiler/collectors/cache_collector.rb +5 -0
- data/lib/profiler/collectors/database_collector.rb +5 -0
- data/lib/profiler/collectors/exception_collector.rb +5 -0
- data/lib/profiler/collectors/flamegraph_collector.rb +5 -0
- data/lib/profiler/collectors/http_collector.rb +6 -1
- data/lib/profiler/collectors/log_collector.rb +5 -0
- data/lib/profiler/collectors/request_collector.rb +14 -11
- data/lib/profiler/collectors/view_collector.rb +5 -0
- data/lib/profiler/configuration.rb +33 -7
- data/lib/profiler/console_profiler.rb +3 -18
- data/lib/profiler/env_override_store.rb +54 -43
- data/lib/profiler/job_profiler.rb +3 -16
- data/lib/profiler/mcp/body_formatter.rb +4 -4
- data/lib/profiler/mcp/file_cache.rb +19 -7
- data/lib/profiler/mcp/resources/recent_requests.rb +2 -0
- data/lib/profiler/mcp/tools/delete_env_var.rb +3 -1
- data/lib/profiler/mcp/tools/get_profile_detail.rb +13 -1
- data/lib/profiler/mcp/tools/get_test_profile_detail.rb +1 -1
- data/lib/profiler/mcp/tools/list_env_vars.rb +3 -0
- data/lib/profiler/mcp/tools/reset_all_env_vars.rb +3 -0
- data/lib/profiler/mcp/tools/reset_env_var.rb +3 -0
- data/lib/profiler/mcp/tools/set_env_var.rb +2 -0
- data/lib/profiler/middleware/capturing_body.rb +112 -0
- data/lib/profiler/middleware/profiler_middleware.rb +261 -53
- data/lib/profiler/middleware/streamed_profile.rb +135 -0
- data/lib/profiler/middleware/toolbar_injector.rb +3 -10
- data/lib/profiler/models/profile.rb +69 -5
- data/lib/profiler/redaction.rb +10 -0
- data/lib/profiler/storage/base_store.rb +5 -1
- data/lib/profiler/storage/blob_store.rb +28 -4
- data/lib/profiler/storage/file_store.rb +21 -15
- data/lib/profiler/storage/memory_store.rb +6 -0
- data/lib/profiler/storage/private_files.rb +135 -0
- data/lib/profiler/storage/redis_store.rb +6 -0
- data/lib/profiler/storage/sqlite_store.rb +26 -8
- data/lib/profiler/storage/token.rb +24 -0
- data/lib/profiler/test_profiler.rb +3 -18
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +12 -0
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e53e69cbdde6c7e775444a738e0069cef86994f505f77d86673a15e25adc768d
|
|
4
|
+
data.tar.gz: c134819ed6bff52aa1d5ad7309b752e1ed6c6a6e1803510d4387462f1607b73a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a277a3d46f4967dc8d875957e95b4d2df118c5a7a96dfcdac83e4a932c4aa4fbd73e1498ef5250bf714e96b5842d99814b617e7f530befcef2eac07984e571c
|
|
7
|
+
data.tar.gz: 978b1b85623419ca8fd8afe3c9550f24be03dbd5e1a8ef02f8ded99270cff218f2e2d68622b66b9edc7def30a46cb574c8b06bdb7b3cdb6f3502b91a919d7941
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,145 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.31.2] - 2026-10-05
|
|
23
|
+
|
|
24
|
+
<!-- stamped -->
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Middleware:** Stream responses through instead of buffering them. The profiler read every
|
|
29
|
+
response body to the end before handing it to the server, so an `ActionController::Live`,
|
|
30
|
+
`response.stream` or enumerator response, or a server-sent events stream, left only once it was
|
|
31
|
+
over (a stream of 5 chunks 200 ms apart sent its first chunk after 1 s instead of 0.2 s, and an
|
|
32
|
+
endless event stream never answered), and a file sent with `send_file` lost `to_path`, so the
|
|
33
|
+
server could no longer send it by its path. A body that does not answer `to_ary`, a
|
|
34
|
+
`text/event-stream` or a body sent by its path now goes to the server chunk by chunk, keeps
|
|
35
|
+
`to_path` and `close`, and its profile is saved when the server closes it, also when the client
|
|
36
|
+
went away, with the body seen so far and the full duration. The collectors that only gather
|
|
37
|
+
what notifications and the logger hand them (SQL, views, cache, exceptions, timeline, logs,
|
|
38
|
+
outbound HTTP) stay subscribed until then, so the queries and views of a `render stream: true`
|
|
39
|
+
page or of an enumerator are still recorded; dumps, mailers, I18n and function profiling are
|
|
40
|
+
read when the application returns. While the server iterates the body, the log lines, outbound
|
|
41
|
+
HTTP calls and `Profiler.measure` blocks it runs are recorded as well. Until it is saved, a
|
|
42
|
+
streamed profile is not listed and its token answers 404: an endless event stream is never
|
|
43
|
+
listed. A body the server never closes is finished when the fiber that started it starts its
|
|
44
|
+
next profiled request, never cutting a stream that a fiber-based server such as Falcon runs
|
|
45
|
+
alongside, and its subscriptions are dropped after 5 minutes at the latest, which the profile
|
|
46
|
+
and the Request tab then say; the server still closes the application's body. Rails 7.0 pages, whose body has no `to_ary`, and pages a `Live`
|
|
47
|
+
controller renders whole still get the toolbar. The toolbar is only injected in pages returned
|
|
48
|
+
whole, so a `render stream: true` page has none.
|
|
49
|
+
- **Middleware:** Stop turning an error raised while a response body is iterated (a stream that
|
|
50
|
+
fails part way, a file sent with `send_file` that disappeared) into an empty `200`. The error now
|
|
51
|
+
reaches the server, as it does without the profiler, and the profile keeps it.
|
|
52
|
+
- **Middleware:** Keep the request and response bodies of a profile to the first
|
|
53
|
+
`config.max_captured_body_bytes` (256 KB by default) instead of copying them whole: a 2 MB upload
|
|
54
|
+
took 2.8 MB in its profile, plus transient copies. `rack.input` is read only up to that size, then
|
|
55
|
+
rewound for the application, and an input that cannot be rewound is no longer read at all. The
|
|
56
|
+
Request tab and the MCP profile detail say when a body was cut, with its whole size, or "at
|
|
57
|
+
least" that size when it is not known; the profile carries `request_body_truncated`,
|
|
58
|
+
`request_body_size`, `response_body_truncated`, `response_body_size` and their
|
|
59
|
+
`*_size_is_minimum`. Only the raw bodies are capped: the parsed `params` and the
|
|
60
|
+
`Parameters:` log line keep their full size, so a 5 MB JSON request still takes about 15 MB in
|
|
61
|
+
its profile. To keep whole bodies, as before, set `config.max_captured_body_bytes = nil`.
|
|
62
|
+
- **Profiles:** Report the number of objects allocated (`allocated_objects`) instead of a figure
|
|
63
|
+
shown as memory. `GC.stat` has no `:total_allocated_size` on the supported Rubies, so `memory`
|
|
64
|
+
was always that number times 40, never a measured byte count. The toolbar, the dashboards, the
|
|
65
|
+
profile list and the MCP tools show "Allocations". The counter belongs to the process: on a
|
|
66
|
+
multi-threaded server it includes what other threads allocated meanwhile. Nothing to change on
|
|
67
|
+
upgrade: the API and the stored profiles still carry `memory` (deprecated, the count times 40),
|
|
68
|
+
and profiles saved by earlier versions are read back with their count. The option
|
|
69
|
+
`memory_warning_threshold`, which nothing ever compared with, is deprecated for
|
|
70
|
+
`allocated_objects_warning_threshold`, a number of objects, still without effect; setting the
|
|
71
|
+
old one warns and converts it.
|
|
72
|
+
- **Toolbar:** Leave a response already framed with `Transfer-Encoding` untouched. Under Rails
|
|
73
|
+
7.0, a `render stream: true` page arrives chunked, and the toolbar was put among the chunks,
|
|
74
|
+
which broke the framing: the browser got an empty or cut page.
|
|
75
|
+
- **Toolbar:** Correct the `Content-Length` of a page the toolbar is injected into. It kept the
|
|
76
|
+
length of the page without the toolbar, so a server that honours it cut the page short.
|
|
77
|
+
- **Toolbar:** Inject the toolbar into a page whose headers are a plain `Hash` with a lower-case
|
|
78
|
+
`content-type`, the form Rack 3 requires; only `Content-Type` was looked up. Every response
|
|
79
|
+
header the profiler reads or writes now ignores the case.
|
|
80
|
+
|
|
81
|
+
## [0.31.1] - 2026-10-05
|
|
82
|
+
|
|
83
|
+
<!-- stamped -->
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **Storage:** Keep the file store's profiles in `tmp_path/profiles` by default, and take only the
|
|
88
|
+
files named after a profile token for profiles. Profiles shared `tmp_path` with
|
|
89
|
+
`env_overrides.json`, which the store listed as a profile with no token and deleted when the
|
|
90
|
+
profiles were cleared. An application that sets `storage_options[:path]` keeps its directory,
|
|
91
|
+
used as given (no subdirectory is added).
|
|
92
|
+
- **MCP:** Keep the bodies saved by the MCP tools in `tmp_path/mcp-cache`, and let their hourly
|
|
93
|
+
cleanup remove only the token directories it wrote there. It removed every directory of
|
|
94
|
+
`tmp_path` older than an hour, the SQLite store's blobs included, at random (one save in
|
|
95
|
+
twenty), which lost the stored HTTP response bodies.
|
|
96
|
+
- **Configuration:** `config.tmp_path` is always a `Pathname`: it was a `String` outside Rails, and
|
|
97
|
+
kept a `String` assigned to it, which made every env override operation fail with only a
|
|
98
|
+
warning. A failure to read or write the env overrides now reaches the Env tab (`500` with the
|
|
99
|
+
error, `ENV` left unchanged) and the MCP env tools (an error answer) instead of reporting a
|
|
100
|
+
success; at boot, before a Sidekiq job and before a console evaluation it is still a warning,
|
|
101
|
+
so an override never makes the application fail.
|
|
102
|
+
- **Jobs:** A storage error while saving a profile no longer makes the application fail: a job,
|
|
103
|
+
a console command, a test, or an outbound HTTP call that ends after its profile was collected
|
|
104
|
+
now goes on, with its own result or its own exception, and the error is printed as a warning.
|
|
105
|
+
The save of a job, console or test profile ran with nothing to catch it, so a full disk, a
|
|
106
|
+
Redis that went away or, from this version, a symbolic link refused in place of the SQLite
|
|
107
|
+
database made the job fail, or replaced the job's own exception. The HTTP middleware already
|
|
108
|
+
kept requests going.
|
|
109
|
+
- **Upgrading:** for the default `tmp_path`; adapt the paths if you set one.
|
|
110
|
+
- Profiles saved by the file store in `tmp/rails-profiler` itself are no longer listed, and are
|
|
111
|
+
not moved. To remove them, with the old MCP body cache, run from the application root:
|
|
112
|
+
```sh
|
|
113
|
+
LC_ALL=C find tmp/rails-profiler -maxdepth 1 -type f -name '[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].json' -delete
|
|
114
|
+
LC_ALL=C find tmp/rails-profiler -mindepth 1 -maxdepth 1 -type d -name '[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' -exec rm -rf {} +
|
|
115
|
+
```
|
|
116
|
+
Both only match names made of 32 lower-case hexadecimal digits, a profile token (`LC_ALL=C`
|
|
117
|
+
keeps `[0-9a-f]` from matching upper-case letters in some locales): `env_overrides.json`, the
|
|
118
|
+
SQLite database, `blobs`, `profiles` and `mcp-cache` are left alone.
|
|
119
|
+
- To keep reading the old profiles instead, set
|
|
120
|
+
`config.storage_options = { path: Rails.root.join("tmp", "rails-profiler") }`, and then do
|
|
121
|
+
**not** run the first command: it would delete the current profiles.
|
|
122
|
+
- The directory `tmp/rails-profiler` created by an earlier version keeps its mode: run
|
|
123
|
+
`chmod 700 tmp/rails-profiler` to restrict it.
|
|
124
|
+
- If the web process and the workers reading the env overrides or a shared file or SQLite store
|
|
125
|
+
run as two different users, set `config.restrict_storage_permissions = false` to create new
|
|
126
|
+
files with the modes of the umask, as before. It does not reopen what was already created
|
|
127
|
+
`0700` and `0600`: run `chmod -R u=rwX,go=rX tmp/rails-profiler` for the modes earlier
|
|
128
|
+
versions created with the usual umask (`0755` and `0644`). A worker under another user that
|
|
129
|
+
also writes there (it opens `env_overrides.json.lock` for writing) needs the
|
|
130
|
+
files belonging to a group of both users (the same primary group for both, or
|
|
131
|
+
`chgrp -R <group> tmp/rails-profiler` then `find tmp/rails-profiler -type d -exec chmod g+s {} +`
|
|
132
|
+
so that new files inherit it), a umask of `002` for both processes, and
|
|
133
|
+
`chmod -R ug=rwX,o=rX tmp/rails-profiler`. The option also follows a symbolic link in place
|
|
134
|
+
of the SQLite database, its `-wal` or `-shm` file or the lock of the env overrides, and drops
|
|
135
|
+
the warning about a shared `tmp_path`, as before.
|
|
136
|
+
|
|
137
|
+
### Security
|
|
138
|
+
|
|
139
|
+
- **Storage:** Check every profile token against the format the profiler issues (32 hexadecimal
|
|
140
|
+
characters) in every storage backend (file, memory, Redis, SQLite and its blobs), before it
|
|
141
|
+
becomes a file name, a directory or a key. A token such as `../../config/important` reached the
|
|
142
|
+
file store as a path: `DELETE /_profiler/api/profiles/:id` deleted, and
|
|
143
|
+
`POST /_profiler/api/ajax/link` overwrote, a `.json` file outside the storage directory. A
|
|
144
|
+
malformed token is now not found (`404` from the API, `nil` from the storage, no exception),
|
|
145
|
+
whoever asks: the controllers, the cluster proxy, which no longer forwards it to a slave, or the
|
|
146
|
+
MCP tools. Saving under one raises an `ArgumentError`. `POST /_profiler/api/ajax/link` answers
|
|
147
|
+
`400` for a malformed `parent_token` instead of storing it. The MCP tools no longer save a body
|
|
148
|
+
(`save_bodies`) under a token sent by a slave that is not a profile token. There is no option to
|
|
149
|
+
accept other tokens: the profiler never issues them, and accepting them would reopen the path.
|
|
150
|
+
- **Storage:** Create the profiler's directories `0700` and its files `0600`, whatever the umask:
|
|
151
|
+
the file store's profiles, the SQLite database and its `-wal` and `-shm` files, the blobs, the
|
|
152
|
+
env overrides with their lock and temporary files, and the MCP body cache. They were created
|
|
153
|
+
with the process defaults, usually `0755` and `0644`, readable by every local user although
|
|
154
|
+
profiles hold cookies, tokens and environment values. A file the profiler writes again (a
|
|
155
|
+
profile, the SQLite database at startup) is brought back to `0600`; a directory that already
|
|
156
|
+
exists keeps its mode. Writing a profile now goes through a temporary file renamed over it. A
|
|
157
|
+
symbolic link placed where the profiler keeps its SQLite database, its `-wal` or `-shm` file or
|
|
158
|
+
the lock of the env overrides is refused instead of followed, and an existing `tmp_path` that
|
|
159
|
+
belongs to another user or that group or others can write to is reported once with a warning.
|
|
160
|
+
|
|
22
161
|
## [0.31.0] - 2026-10-05
|
|
23
162
|
|
|
24
163
|
<!-- stamped -->
|
|
@@ -3831,6 +3831,16 @@
|
|
|
3831
3831
|
] });
|
|
3832
3832
|
}
|
|
3833
3833
|
|
|
3834
|
+
// app/assets/typescript/profiler/dashboard/utils.ts
|
|
3835
|
+
function allocatedObjects(profile) {
|
|
3836
|
+
if (profile.allocated_objects != null) return profile.allocated_objects;
|
|
3837
|
+
return profile.memory != null ? Math.round(profile.memory / 40) : null;
|
|
3838
|
+
}
|
|
3839
|
+
function formatAllocations(count) {
|
|
3840
|
+
return count == null ? "-" : `${count.toLocaleString("en")} objects`;
|
|
3841
|
+
}
|
|
3842
|
+
var ALLOCATIONS_HINT = "Objects allocated by the whole process while this ran: on a multi-threaded server, it includes what other threads allocated meanwhile.";
|
|
3843
|
+
|
|
3834
3844
|
// app/assets/typescript/profiler/components/toolbar/ToolbarApp.tsx
|
|
3835
3845
|
function statusClass2(status) {
|
|
3836
3846
|
if (status >= 200 && status < 300) return "profiler-text--success";
|
|
@@ -3903,12 +3913,9 @@
|
|
|
3903
3913
|
" ms"
|
|
3904
3914
|
] })
|
|
3905
3915
|
] }),
|
|
3906
|
-
profile
|
|
3907
|
-
/* @__PURE__ */ u3("span", { children: "
|
|
3908
|
-
/* @__PURE__ */ u3("strong", { children:
|
|
3909
|
-
(profile.memory / 1024 / 1024).toFixed(2),
|
|
3910
|
-
" MB"
|
|
3911
|
-
] })
|
|
3916
|
+
allocatedObjects(profile) != null && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", title: ALLOCATIONS_HINT, children: [
|
|
3917
|
+
/* @__PURE__ */ u3("span", { children: "Allocations" }),
|
|
3918
|
+
/* @__PURE__ */ u3("strong", { children: formatAllocations(allocatedObjects(profile)) })
|
|
3912
3919
|
] }),
|
|
3913
3920
|
/* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
|
|
3914
3921
|
/* @__PURE__ */ u3("span", { children: "Started" }),
|
|
@@ -3994,6 +3994,14 @@
|
|
|
3994
3994
|
function getGemVersion() {
|
|
3995
3995
|
return document.querySelector('meta[name="profiler-version"]')?.content ?? "";
|
|
3996
3996
|
}
|
|
3997
|
+
function allocatedObjects(profile) {
|
|
3998
|
+
if (profile.allocated_objects != null) return profile.allocated_objects;
|
|
3999
|
+
return profile.memory != null ? Math.round(profile.memory / 40) : null;
|
|
4000
|
+
}
|
|
4001
|
+
function formatAllocations(count) {
|
|
4002
|
+
return count == null ? "-" : `${count.toLocaleString("en")} objects`;
|
|
4003
|
+
}
|
|
4004
|
+
var ALLOCATIONS_HINT = "Objects allocated by the whole process while this ran: on a multi-threaded server, it includes what other threads allocated meanwhile.";
|
|
3997
4005
|
|
|
3998
4006
|
// app/assets/typescript/profiler/components/dashboard/tabs/shared/utils.ts
|
|
3999
4007
|
function methodBadge(method) {
|
|
@@ -5556,10 +5564,6 @@
|
|
|
5556
5564
|
function formatTime(iso) {
|
|
5557
5565
|
return new Date(iso).toLocaleTimeString("en", { hour12: false });
|
|
5558
5566
|
}
|
|
5559
|
-
function formatMemory(bytes) {
|
|
5560
|
-
if (!bytes) return "-";
|
|
5561
|
-
return (bytes / 1024 / 1024).toFixed(2) + " MB";
|
|
5562
|
-
}
|
|
5563
5567
|
var PRESETS = [
|
|
5564
5568
|
{ key: "slow", label: "Slow" },
|
|
5565
5569
|
{ key: "many_queries", label: "Many queries" },
|
|
@@ -5797,8 +5801,8 @@
|
|
|
5797
5801
|
bv = b.duration;
|
|
5798
5802
|
break;
|
|
5799
5803
|
case "memory":
|
|
5800
|
-
av = a3
|
|
5801
|
-
bv = b
|
|
5804
|
+
av = allocatedObjects(a3) ?? 0;
|
|
5805
|
+
bv = allocatedObjects(b) ?? 0;
|
|
5802
5806
|
break;
|
|
5803
5807
|
case "status":
|
|
5804
5808
|
av = a3.status;
|
|
@@ -6043,8 +6047,8 @@
|
|
|
6043
6047
|
"Queries ",
|
|
6044
6048
|
sortIcon(httpSort.col, httpSort.dir, "queries")
|
|
6045
6049
|
] }),
|
|
6046
|
-
/* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "memory" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("memory"), children: [
|
|
6047
|
-
"
|
|
6050
|
+
/* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "memory" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("memory"), title: ALLOCATIONS_HINT, children: [
|
|
6051
|
+
"Allocations ",
|
|
6048
6052
|
sortIcon(httpSort.col, httpSort.dir, "memory")
|
|
6049
6053
|
] }),
|
|
6050
6054
|
/* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "status" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("status"), children: [
|
|
@@ -6066,7 +6070,7 @@
|
|
|
6066
6070
|
" ms"
|
|
6067
6071
|
] }) }),
|
|
6068
6072
|
/* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "\u2014" }),
|
|
6069
|
-
/* @__PURE__ */ u3("td", { children:
|
|
6073
|
+
/* @__PURE__ */ u3("td", { children: formatAllocations(allocatedObjects(p3)) }),
|
|
6070
6074
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: statusClass(p3.status), children: p3.status }) }),
|
|
6071
6075
|
/* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
|
|
6072
6076
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteProfile2(p3.token), title: "Delete", children: "\xD7" }) })
|
|
@@ -6458,9 +6462,36 @@
|
|
|
6458
6462
|
parts.push(` '${url}'`);
|
|
6459
6463
|
return parts.join(" \\\n");
|
|
6460
6464
|
}
|
|
6465
|
+
function truncationNotes(profile, routeData) {
|
|
6466
|
+
const notes = [];
|
|
6467
|
+
const describe = (what, truncated, size, minimum) => {
|
|
6468
|
+
if (truncated) {
|
|
6469
|
+
const total = size != null ? `${minimum ? "at least " : ""}${size.toLocaleString("en")} bytes, ` : "";
|
|
6470
|
+
notes.push(`${what} body truncated: ${total}only the beginning was kept (max_captured_body_bytes).`);
|
|
6471
|
+
}
|
|
6472
|
+
};
|
|
6473
|
+
describe(
|
|
6474
|
+
"Request",
|
|
6475
|
+
profile.request_body_truncated ?? routeData.request_body_truncated,
|
|
6476
|
+
profile.request_body_size ?? routeData.request_body_size,
|
|
6477
|
+
profile.request_body_size_is_minimum ?? routeData.request_body_size_is_minimum
|
|
6478
|
+
);
|
|
6479
|
+
describe(
|
|
6480
|
+
"Response",
|
|
6481
|
+
profile.response_body_truncated ?? routeData.response_body_truncated,
|
|
6482
|
+
profile.response_body_size ?? routeData.response_body_size,
|
|
6483
|
+
profile.response_body_size_is_minimum ?? routeData.response_body_size_is_minimum
|
|
6484
|
+
);
|
|
6485
|
+
const releasedAfter = profile.collectors_released_after_seconds ?? routeData.collectors_released_after_seconds;
|
|
6486
|
+
if (releasedAfter != null) {
|
|
6487
|
+
notes.push(`Incomplete profile: the collectors of this stream were released after ${releasedAfter} s, before the server closed it; what it did later is missing.`);
|
|
6488
|
+
}
|
|
6489
|
+
return notes;
|
|
6490
|
+
}
|
|
6461
6491
|
function RequestTab({ profile }) {
|
|
6462
6492
|
const routeData = profile.collectors_data?.request ?? {};
|
|
6463
6493
|
const hasParams = profile.params && Object.keys(profile.params).length > 0;
|
|
6494
|
+
const notes = truncationNotes(profile, routeData);
|
|
6464
6495
|
return /* @__PURE__ */ u3("div", { class: "profiler-ajax-card profiler-ajax-card--success", style: "margin-bottom:8px;background:var(--profiler-bg-elevated);transform:translateX(3px);box-shadow:var(--profiler-shadow-sm);transition:none", children: [
|
|
6465
6496
|
/* @__PURE__ */ u3(
|
|
6466
6497
|
HttpCardHeader,
|
|
@@ -6480,6 +6511,7 @@
|
|
|
6480
6511
|
routeData.route_pattern
|
|
6481
6512
|
] })
|
|
6482
6513
|
] }),
|
|
6514
|
+
notes.map((note) => /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row profiler-text--xs profiler-text--warning", children: note }, note)),
|
|
6483
6515
|
/* @__PURE__ */ u3(
|
|
6484
6516
|
HttpReqRespDetail,
|
|
6485
6517
|
{
|
|
@@ -8878,12 +8910,9 @@
|
|
|
8878
8910
|
"Status: ",
|
|
8879
8911
|
/* @__PURE__ */ u3("strong", { children: profile.status })
|
|
8880
8912
|
] }),
|
|
8881
|
-
profile
|
|
8882
|
-
"
|
|
8883
|
-
/* @__PURE__ */ u3("strong", { children:
|
|
8884
|
-
(profile.memory / 1024 / 1024).toFixed(2),
|
|
8885
|
-
" MB"
|
|
8886
|
-
] })
|
|
8913
|
+
allocatedObjects(profile) != null && /* @__PURE__ */ u3("span", { title: ALLOCATIONS_HINT, children: [
|
|
8914
|
+
"Allocations: ",
|
|
8915
|
+
/* @__PURE__ */ u3("strong", { children: formatAllocations(allocatedObjects(profile)) })
|
|
8887
8916
|
] }),
|
|
8888
8917
|
/* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: new Date(profile.started_at).toLocaleString("en", { hour12: false, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }) }),
|
|
8889
8918
|
profile.gem_version && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
|
|
@@ -9038,12 +9067,9 @@
|
|
|
9038
9067
|
"Status: ",
|
|
9039
9068
|
/* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : "success"}`, children: isFailed ? "Failed" : "Completed" }) })
|
|
9040
9069
|
] }),
|
|
9041
|
-
profile
|
|
9042
|
-
"
|
|
9043
|
-
/* @__PURE__ */ u3("strong", { children:
|
|
9044
|
-
(profile.memory / 1024 / 1024).toFixed(2),
|
|
9045
|
-
" MB"
|
|
9046
|
-
] })
|
|
9070
|
+
allocatedObjects(profile) != null && /* @__PURE__ */ u3("span", { title: ALLOCATIONS_HINT, children: [
|
|
9071
|
+
"Allocations: ",
|
|
9072
|
+
/* @__PURE__ */ u3("strong", { children: formatAllocations(allocatedObjects(profile)) })
|
|
9047
9073
|
] }),
|
|
9048
9074
|
profile.gem_version && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
|
|
9049
9075
|
"v",
|
|
@@ -9170,12 +9196,9 @@
|
|
|
9170
9196
|
"Status: ",
|
|
9171
9197
|
/* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : "success"}`, children: isFailed ? "Error" : "OK" }) })
|
|
9172
9198
|
] }),
|
|
9173
|
-
profile
|
|
9174
|
-
"
|
|
9175
|
-
/* @__PURE__ */ u3("strong", { children:
|
|
9176
|
-
(profile.memory / 1024 / 1024).toFixed(2),
|
|
9177
|
-
" MB"
|
|
9178
|
-
] })
|
|
9199
|
+
allocatedObjects(profile) != null && /* @__PURE__ */ u3("span", { title: ALLOCATIONS_HINT, children: [
|
|
9200
|
+
"Allocations: ",
|
|
9201
|
+
/* @__PURE__ */ u3("strong", { children: formatAllocations(allocatedObjects(profile)) })
|
|
9179
9202
|
] }),
|
|
9180
9203
|
profile.gem_version && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
|
|
9181
9204
|
"v",
|
|
@@ -9300,12 +9323,9 @@
|
|
|
9300
9323
|
"Status: ",
|
|
9301
9324
|
/* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : testStatus === "pending" ? "warning" : "success"}`, children: testStatus === "failed" ? "\u2717 Failed" : testStatus === "pending" ? "\u23F8 Pending" : "\u2713 Passed" }) })
|
|
9302
9325
|
] }),
|
|
9303
|
-
profile
|
|
9304
|
-
"
|
|
9305
|
-
/* @__PURE__ */ u3("strong", { children:
|
|
9306
|
-
(profile.memory / 1024 / 1024).toFixed(2),
|
|
9307
|
-
" MB"
|
|
9308
|
-
] })
|
|
9326
|
+
allocatedObjects(profile) != null && /* @__PURE__ */ u3("span", { title: ALLOCATIONS_HINT, children: [
|
|
9327
|
+
"Allocations: ",
|
|
9328
|
+
/* @__PURE__ */ u3("strong", { children: formatAllocations(allocatedObjects(profile)) })
|
|
9309
9329
|
] })
|
|
9310
9330
|
] }),
|
|
9311
9331
|
testData?.test_file && /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted profiler-mt-2", children: [
|
|
@@ -12,6 +12,12 @@ module Profiler
|
|
|
12
12
|
return render json: { error: "Missing parent_token or child_token" }, status: :bad_request
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# The parent token is written into the child profile: a value the gem never issues is
|
|
16
|
+
# refused rather than stored. A malformed child token is not found by the storage.
|
|
17
|
+
unless Profiler::Storage::Token.valid?(parent_token)
|
|
18
|
+
return render json: { error: "Invalid parent_token" }, status: :bad_request
|
|
19
|
+
end
|
|
20
|
+
|
|
15
21
|
# Load child profile
|
|
16
22
|
child_profile = Profiler.storage.load(child_token)
|
|
17
23
|
unless child_profile
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
module Profiler
|
|
4
4
|
module Api
|
|
5
5
|
class EnvVarsController < ApplicationController
|
|
6
|
+
# The overrides file could not be read or written: nothing was changed, ENV included.
|
|
7
|
+
rescue_from Profiler::EnvOverrideStore::Error do |error|
|
|
8
|
+
key = params[:key].to_s.strip
|
|
9
|
+
message = key.empty? ? "#{error.message}; nothing was changed" : "#{error.message}; #{key} was left unchanged"
|
|
10
|
+
render json: { error: message }, status: :internal_server_error
|
|
11
|
+
end
|
|
12
|
+
|
|
6
13
|
def show
|
|
7
14
|
variables = Profiler::Redaction.env_snapshot
|
|
8
15
|
overrides = Profiler::Redaction.env_overrides(Profiler.env_override_store.all_overrides)
|
|
@@ -72,8 +79,11 @@ module Profiler
|
|
|
72
79
|
ENV[key].nil? ? nil : Profiler::Redaction.env_value(key, ENV[key])
|
|
73
80
|
end
|
|
74
81
|
|
|
82
|
+
# Read back after a change that succeeded: a failure here must not answer that nothing changed.
|
|
75
83
|
def redacted_override(key)
|
|
76
84
|
Profiler::Redaction.env_overrides(Profiler.env_override_store.all_overrides)[key]
|
|
85
|
+
rescue Profiler::EnvOverrideStore::Error
|
|
86
|
+
nil
|
|
77
87
|
end
|
|
78
88
|
end
|
|
79
89
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
# What every profile reports as allocated_objects: the number of objects Ruby allocated
|
|
5
|
+
# between two readings. GC.stat has no byte count of allocations (no :total_allocated_size on
|
|
6
|
+
# the supported Rubies), so none is reported.
|
|
7
|
+
#
|
|
8
|
+
# The counter belongs to the process, not to the thread: on a multi-threaded server, or with
|
|
9
|
+
# jobs running alongside, the figure includes what the other threads allocated meanwhile.
|
|
10
|
+
module AllocationCounter
|
|
11
|
+
# The bytes per object that profiles saved by earlier versions multiplied the count by, as
|
|
12
|
+
# `memory`: kept to read them back, and to keep writing that field for older readers.
|
|
13
|
+
LEGACY_BYTES_PER_OBJECT = 40
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def current
|
|
18
|
+
GC.stat(:total_allocated_objects)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -4,6 +4,7 @@ require "net/http"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "uri"
|
|
6
6
|
require_relative "security"
|
|
7
|
+
require_relative "../storage/token"
|
|
7
8
|
|
|
8
9
|
module Profiler
|
|
9
10
|
module Cluster
|
|
@@ -39,7 +40,10 @@ module Profiler
|
|
|
39
40
|
Array(data["profiles"]).map { |h| profile_from_api(h) }
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
# A token that is not one the gem issues is not found, without asking the slave.
|
|
42
44
|
def load(token)
|
|
45
|
+
return nil unless Storage::Token.valid?(token)
|
|
46
|
+
|
|
43
47
|
data = get_json("/_profiler/api/profiles/#{Security.escape_segment(token)}")
|
|
44
48
|
return nil unless data["token"] || data["profile"]
|
|
45
49
|
|
|
@@ -48,6 +52,8 @@ module Profiler
|
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
def find_by_parent(parent_token)
|
|
55
|
+
return [] unless Storage::Token.valid?(parent_token)
|
|
56
|
+
|
|
51
57
|
data = get_json("/_profiler/api/profiles", parent_token: parent_token, all_types: true)
|
|
52
58
|
Array(data["profiles"]).map { |h| profile_from_api(h) }
|
|
53
59
|
end
|
|
@@ -71,6 +71,36 @@ module Profiler
|
|
|
71
71
|
# Override in subclasses to collect data
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# Whether collect reads nothing from the request's thread (thread-local slots, the
|
|
75
|
+
# locale): only then can a streamed response keep the collector subscribed until the
|
|
76
|
+
# server closes its body, and collect it from whichever thread closes it. The default
|
|
77
|
+
# is the safe answer: collected on the request's thread when the application returns.
|
|
78
|
+
def collect_from_any_thread?
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Gives back the thread-local slots subscribe took over, and nothing else: a streamed
|
|
83
|
+
# response does this on the request's thread when the application returns, and keeps the
|
|
84
|
+
# notification subscriptions until its body is closed. Idempotent, like unsubscribe.
|
|
85
|
+
def release_thread_slots
|
|
86
|
+
restore_thread_slots
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Puts the slots subscribe claimed back, for a while, on the thread (or fiber) that
|
|
90
|
+
# iterates a streamed body: the logs, outbound HTTP calls and measures it makes belong to
|
|
91
|
+
# this profile. Returns what the slots held there, for return_thread_slots.
|
|
92
|
+
def lend_thread_slots
|
|
93
|
+
(@thread_slot_values || {}).to_h do |key, value|
|
|
94
|
+
previous = Thread.current[key]
|
|
95
|
+
Thread.current[key] = value
|
|
96
|
+
[key, previous]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def return_thread_slots(previous)
|
|
101
|
+
previous&.each { |key, value| Thread.current[key] = value }
|
|
102
|
+
end
|
|
103
|
+
|
|
74
104
|
def toolbar_summary
|
|
75
105
|
# Override in subclasses to provide summary for toolbar
|
|
76
106
|
""
|
|
@@ -104,6 +134,7 @@ module Profiler
|
|
|
104
134
|
def claim_thread_slot(key, value)
|
|
105
135
|
@claimed_thread_slots ||= {}
|
|
106
136
|
@claimed_thread_slots[key] = Thread.current[key] unless @claimed_thread_slots.key?(key)
|
|
137
|
+
(@thread_slot_values ||= {})[key] = value
|
|
107
138
|
Thread.current[key] = value
|
|
108
139
|
end
|
|
109
140
|
|
|
@@ -39,6 +39,11 @@ module Profiler
|
|
|
39
39
|
claim_thread_slot(:profiler_http_collector, self)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Collect reads only what the collector gathered itself.
|
|
43
|
+
def collect_from_any_thread?
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
42
47
|
# The Net::HTTP patch installed by subscribe stays: it is installed once per process and
|
|
43
48
|
# records nothing when no collector holds the thread-local slot.
|
|
44
49
|
def unsubscribe
|
|
@@ -125,7 +130,7 @@ module Profiler
|
|
|
125
130
|
build_data(@requests)
|
|
126
131
|
end
|
|
127
132
|
store_data(data)
|
|
128
|
-
Profiler.
|
|
133
|
+
Profiler.save_profile(@profile, from: "HttpCollector")
|
|
129
134
|
end
|
|
130
135
|
|
|
131
136
|
def group_by_host(requests)
|
|
@@ -93,6 +93,11 @@ module Profiler
|
|
|
93
93
|
warn "LogCollector#subscribe failed: #{e.message}"
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# Collect reads only what the collector gathered itself.
|
|
97
|
+
def collect_from_any_thread?
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
96
101
|
def unsubscribe
|
|
97
102
|
if @capture_logger
|
|
98
103
|
@broadcaster.stop_broadcasting_to(@capture_logger) if @broadcaster.respond_to?(:stop_broadcasting_to)
|
|
@@ -30,7 +30,8 @@ module Profiler
|
|
|
30
30
|
method: @profile.method,
|
|
31
31
|
status: @profile.status,
|
|
32
32
|
duration: @profile.duration,
|
|
33
|
-
|
|
33
|
+
allocated_objects: @profile.allocated_objects,
|
|
34
|
+
memory: @profile.memory, # deprecated, see Profile#memory
|
|
34
35
|
params: @profile.params,
|
|
35
36
|
headers: @profile.headers,
|
|
36
37
|
response_headers: @profile.response_headers,
|
|
@@ -38,6 +39,14 @@ module Profiler
|
|
|
38
39
|
request_body_encoding: @profile.request_body_encoding,
|
|
39
40
|
response_body: @profile.response_body,
|
|
40
41
|
response_body_encoding: @profile.response_body_encoding,
|
|
42
|
+
request_body_size: @profile.request_body_size,
|
|
43
|
+
request_body_truncated: @profile.request_body_truncated || false,
|
|
44
|
+
request_body_size_is_minimum: @profile.request_body_size_is_minimum || false,
|
|
45
|
+
response_body_size: @profile.response_body_size,
|
|
46
|
+
response_body_truncated: @profile.response_body_truncated || false,
|
|
47
|
+
response_body_size_is_minimum: @profile.response_body_size_is_minimum || false,
|
|
48
|
+
# A streamed response whose collectors were released before its body was closed.
|
|
49
|
+
collectors_released_after_seconds: @profile.collectors_released_after_seconds,
|
|
41
50
|
started_at: @profile.started_at&.iso8601,
|
|
42
51
|
finished_at: @profile.finished_at&.iso8601
|
|
43
52
|
}
|
|
@@ -59,7 +68,7 @@ module Profiler
|
|
|
59
68
|
text: "#{@profile.method} #{@profile.status}",
|
|
60
69
|
color: status_color,
|
|
61
70
|
duration: @profile.duration,
|
|
62
|
-
|
|
71
|
+
allocated_objects: format_allocations(@profile.allocated_objects)
|
|
63
72
|
}
|
|
64
73
|
end
|
|
65
74
|
|
|
@@ -98,16 +107,10 @@ module Profiler
|
|
|
98
107
|
{}
|
|
99
108
|
end
|
|
100
109
|
|
|
101
|
-
def
|
|
102
|
-
return "0
|
|
110
|
+
def format_allocations(count)
|
|
111
|
+
return "0 objects" unless count
|
|
103
112
|
|
|
104
|
-
|
|
105
|
-
"#{bytes} B"
|
|
106
|
-
elsif bytes < 1024 * 1024
|
|
107
|
-
"#{(bytes / 1024.0).round(2)} KB"
|
|
108
|
-
else
|
|
109
|
-
"#{(bytes / 1024.0 / 1024.0).round(2)} MB"
|
|
110
|
-
end
|
|
113
|
+
"#{count.to_s.reverse.scan(/\d{1,3}/).join(",").reverse} objects"
|
|
111
114
|
end
|
|
112
115
|
end
|
|
113
116
|
end
|