rails-profiler 0.31.3 → 0.31.4
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 +56 -0
- data/app/controllers/profiler/api/console_controller.rb +1 -11
- data/app/controllers/profiler/api/jobs_controller.rb +1 -11
- data/app/controllers/profiler/api/profiles_controller.rb +18 -28
- data/app/controllers/profiler/api/tests_controller.rb +1 -11
- data/app/controllers/profiler/api/toolbar_controller.rb +0 -20
- data/app/controllers/profiler/application_controller.rb +55 -15
- data/app/controllers/profiler/profiles_controller.rb +0 -16
- data/lib/profiler/ajax_data.rb +40 -0
- data/lib/profiler/cluster/slave_proxy.rb +7 -5
- data/lib/profiler/configuration.rb +15 -4
- data/lib/profiler/mcp/resources/failing_tests.rb +1 -1
- data/lib/profiler/mcp/resources/recent_console.rb +2 -2
- data/lib/profiler/mcp/resources/recent_jobs.rb +2 -2
- data/lib/profiler/mcp/resources/slow_tests.rb +1 -1
- data/lib/profiler/mcp/server.rb +8 -2
- data/lib/profiler/mcp/tools/get_profile_ajax.rb +2 -0
- data/lib/profiler/mcp/tools/get_test_profile_detail.rb +1 -1
- data/lib/profiler/mcp/tools/query_console_profiles.rb +1 -1
- data/lib/profiler/mcp/tools/query_jobs.rb +1 -1
- data/lib/profiler/mcp/tools/query_profiles.rb +1 -1
- data/lib/profiler/mcp/tools/query_test_profiles.rb +1 -1
- data/lib/profiler/mcp/tools/run_tests.rb +1 -1
- data/lib/profiler/middleware/profiler_middleware.rb +4 -1
- data/lib/profiler/railtie.rb +3 -0
- data/lib/profiler/storage/file_store.rb +332 -58
- data/lib/profiler/storage/memory_store.rb +60 -31
- data/lib/profiler/storage/private_files.rb +13 -0
- data/lib/profiler/storage/redis_store.rb +251 -32
- data/lib/profiler/storage/sqlite_store.rb +57 -9
- data/lib/profiler/storage/summary.rb +50 -0
- data/lib/profiler/storage/unavailable.rb +58 -0
- data/lib/profiler/test_helpers/reporter.rb +1 -1
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +12 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ca8756c9efed6c4619db0cbc1226f104deb5f7287f972b137f9a8bf291a40c4
|
|
4
|
+
data.tar.gz: dcb61c33b3262c081493beb1ee783923b3c7b3ac8d4cd26ef9195ea4e3ff182d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2576d031d1b4d3e6e462392ed5ca00bc83c12eaed23b9ec07ea842bd3365e373d7eff1b4c53ae73e779366fc947ad70f01f15e8b1108c8fb36e85409986ebd22
|
|
7
|
+
data.tar.gz: 4b4d5914c82c06e58fd1d8e14157a28b2d0a9bbc00d15f574a5a5bdb0e782c0e9957042f25c38ba2d6cbc93c159b0b4d2f921667d552e397f37fed896bcc2245
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,62 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.31.4] - 2026-10-05
|
|
23
|
+
|
|
24
|
+
<!-- stamped -->
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Storage:** A save into the file store costs the same whatever the number of profiles on disk:
|
|
29
|
+
it listed the directory and read the size of every file on each save, about 5 ms with 500
|
|
30
|
+
profiles and 100 ms with 10,000, now under 1 ms. The store keeps an index next to the profiles
|
|
31
|
+
(`.profiles-index.jsonl`, with its lock `.profiles-index.lock`), which the processes writing to
|
|
32
|
+
the directory share; a missing or damaged one is rebuilt from the profile files, so the profiles
|
|
33
|
+
already saved are found without any migration. Temporary files left by a killed process are now
|
|
34
|
+
removed. The index relies on `flock`: a file store shared between hosts over NFS is not
|
|
35
|
+
supported, use Redis there.
|
|
36
|
+
- **Storage:** `config.max_profiles` (100) is honoured by every backend, the first saved profiles
|
|
37
|
+
evicted first (a job saved when it ends counts as new, whenever it started): only the memory
|
|
38
|
+
store applied it, so the file store grew to `max_size` (100 MB,
|
|
39
|
+
tens of thousands of profiles), SQLite without limit and Redis up to its TTL. In the test
|
|
40
|
+
environment, where every example saves a profile, there is no cap on the count unless the
|
|
41
|
+
application sets one.
|
|
42
|
+
- **Storage:** Finding the children of a page (its AJAX sub-requests and its jobs) reads only
|
|
43
|
+
them, through an index of their parent, instead of reading and parsing every stored profile
|
|
44
|
+
(about 260 ms with 1,500 profiles of 17 KB in the file store, now under 1 ms). Every profile page
|
|
45
|
+
and toolbar paid it, once for the child jobs and once more for the AJAX tab; they now read the
|
|
46
|
+
children once.
|
|
47
|
+
- **Dashboard:** The HTTP, job, console and test lists are filtered and paged by the storage, and
|
|
48
|
+
list summaries of the profiles (without the bodies, params, headers and nested collector data):
|
|
49
|
+
they loaded the 1,000 newest profiles in full on every page (about 200 ms with 1,500 profiles of
|
|
50
|
+
17 KB, now under 10 ms), showed nothing past the thousandth, said there was no next page there,
|
|
51
|
+
and missed the profiles of one type behind 1,000 newer ones of another. The MCP tools and
|
|
52
|
+
resources that list one type of profile (jobs, console, tests) and the test report ask the
|
|
53
|
+
storage for that type too, and found nothing behind 500 newer profiles of another type.
|
|
54
|
+
- **AJAX:** The AJAX tab shows the sub-requests of a page whatever `config.collectors` holds: it
|
|
55
|
+
only appeared with `AjaxCollector` in the list, which the default list does not have. The jobs a
|
|
56
|
+
page enqueued are no longer counted as AJAX requests: they stay under its child jobs. The MCP
|
|
57
|
+
tool `get_profile_ajax` finds the sub-requests too: it read the AJAX data saved with the page,
|
|
58
|
+
before any sub-request.
|
|
59
|
+
- **Storage:** The storage is created once, under a lock: the first requests of a threaded server
|
|
60
|
+
could each create their own store (16 for 16 threads in the spec), and with the memory store the
|
|
61
|
+
profiles saved in all but one were lost. Several file, SQLite or Redis stores share their data
|
|
62
|
+
and lost nothing.
|
|
63
|
+
- **Storage:** A store that cannot be created (its index or lock replaced by a symbolic link, say)
|
|
64
|
+
is reported once per process instead of with a backtrace on every request: the profiles are not
|
|
65
|
+
saved and no toolbar is added, the dashboard and its API answer `503` with the cause in one line
|
|
66
|
+
(no path) instead of a `500`, the MCP tools an error with it, and the store is created again
|
|
67
|
+
once the cause is gone.
|
|
68
|
+
- **Upgrading:** the file, SQLite and Redis stores now keep 100 profiles outside the test
|
|
69
|
+
environment, and **the first save after the upgrade removes the profiles past the cap**, the
|
|
70
|
+
first saved first: of 3,000 profiles written by 0.31.1 in the file store, about 80 remain, and
|
|
71
|
+
that first save takes about a second, during which the other processes writing to the store
|
|
72
|
+
wait. To keep them, set `config.max_profiles` (or `storage_options[:max_profiles]`) **before**
|
|
73
|
+
upgrading: to a higher number, or to `nil` for the earlier behaviour, no cap on the count (the
|
|
74
|
+
file store then keeps up to `max_size`, Redis up to its TTL). The memory store keeps 100 with
|
|
75
|
+
`nil`, as before. A Redis store written by an earlier version is indexed once, by one process,
|
|
76
|
+
on first use, which also removes the profiles past the cap then (about 0.4 s for 20,000).
|
|
77
|
+
|
|
22
78
|
## [0.31.3] - 2026-10-05
|
|
23
79
|
|
|
24
80
|
<!-- stamped -->
|
|
@@ -4,17 +4,7 @@ module Profiler
|
|
|
4
4
|
module Api
|
|
5
5
|
class ConsoleController < ApplicationController
|
|
6
6
|
def index
|
|
7
|
-
|
|
8
|
-
offset = (params[:offset] || 0).to_i
|
|
9
|
-
all = Profiler.storage.list(limit: 1000, offset: 0)
|
|
10
|
-
console = all.select { |p| p.profile_type == "console" }
|
|
11
|
-
page = console.drop(offset).first(limit + 1)
|
|
12
|
-
render json: {
|
|
13
|
-
profiles: page.first(limit).map(&:to_h),
|
|
14
|
-
limit: limit,
|
|
15
|
-
offset: offset,
|
|
16
|
-
has_more: page.size > limit
|
|
17
|
-
}
|
|
7
|
+
render_profile_page(type: "console")
|
|
18
8
|
end
|
|
19
9
|
|
|
20
10
|
def show
|
|
@@ -4,17 +4,7 @@ module Profiler
|
|
|
4
4
|
module Api
|
|
5
5
|
class JobsController < ApplicationController
|
|
6
6
|
def index
|
|
7
|
-
|
|
8
|
-
offset = (params[:offset] || 0).to_i
|
|
9
|
-
all = Profiler.storage.list(limit: 1000, offset: 0)
|
|
10
|
-
jobs = all.select { |p| p.profile_type == "job" }
|
|
11
|
-
page = jobs.drop(offset).first(limit + 1)
|
|
12
|
-
render json: {
|
|
13
|
-
profiles: page.first(limit).map(&:to_h),
|
|
14
|
-
limit: limit,
|
|
15
|
-
offset: offset,
|
|
16
|
-
has_more: page.size > limit
|
|
17
|
-
}
|
|
7
|
+
render_profile_page(type: "job")
|
|
18
8
|
end
|
|
19
9
|
|
|
20
10
|
def show
|
|
@@ -4,20 +4,16 @@ module Profiler
|
|
|
4
4
|
module Api
|
|
5
5
|
class ProfilesController < ApplicationController
|
|
6
6
|
def index
|
|
7
|
-
limit = (params[:limit] || 50).to_i
|
|
8
|
-
offset = (params[:offset] || 0).to_i
|
|
9
|
-
all = Profiler.storage.list(limit: 1000, offset: 0)
|
|
10
7
|
# all_types is an opt-in used by the cluster proxy so it can mirror the full
|
|
11
|
-
# storage.list contract
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
8
|
+
# storage.list contract (every type, or the one given as type, full profiles); the
|
|
9
|
+
# dashboard relies on the default: http profiles, as summaries.
|
|
10
|
+
if params[:parent_token].present?
|
|
11
|
+
render_children_page
|
|
12
|
+
elsif all_types?
|
|
13
|
+
render_profile_page(type: profile_type_param, summary: false)
|
|
14
|
+
else
|
|
15
|
+
render_profile_page(type: "http")
|
|
16
|
+
end
|
|
21
17
|
end
|
|
22
18
|
|
|
23
19
|
def show
|
|
@@ -57,22 +53,16 @@ module Profiler
|
|
|
57
53
|
%w[1 true].include?(params[:all_types].to_s)
|
|
58
54
|
end
|
|
59
55
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if Profiler.configuration.collectors.include?(ajax_collector_class)
|
|
65
|
-
collector = ajax_collector_class.new(profile)
|
|
66
|
-
collector.collect
|
|
56
|
+
def profile_type_param
|
|
57
|
+
params[:type].to_s.match?(/\A[a-z_]{1,32}\z/) ? params[:type].to_s : nil
|
|
58
|
+
end
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
end
|
|
75
|
-
end
|
|
60
|
+
# The children of a page, oldest last, read through the store's parent index.
|
|
61
|
+
def render_children_page
|
|
62
|
+
limit, offset = page_params
|
|
63
|
+
children = Profiler.storage.find_by_parent(params[:parent_token].to_s).reverse
|
|
64
|
+
children = children.select { |p| p.profile_type == "http" } unless all_types?
|
|
65
|
+
render_page(children.drop(offset).first(limit + 1), limit, offset)
|
|
76
66
|
end
|
|
77
67
|
end
|
|
78
68
|
end
|
|
@@ -4,17 +4,7 @@ module Profiler
|
|
|
4
4
|
module Api
|
|
5
5
|
class TestsController < ApplicationController
|
|
6
6
|
def index
|
|
7
|
-
|
|
8
|
-
offset = (params[:offset] || 0).to_i
|
|
9
|
-
all = Profiler.storage.list(limit: 1000, offset: 0)
|
|
10
|
-
tests = all.select { |p| p.profile_type == "test" }
|
|
11
|
-
page = tests.drop(offset).first(limit + 1)
|
|
12
|
-
render json: {
|
|
13
|
-
profiles: page.first(limit).map(&:to_h),
|
|
14
|
-
limit: limit,
|
|
15
|
-
offset: offset,
|
|
16
|
-
has_more: page.size > limit
|
|
17
|
-
}
|
|
7
|
+
render_profile_page(type: "test")
|
|
18
8
|
end
|
|
19
9
|
|
|
20
10
|
def show
|
|
@@ -24,26 +24,6 @@ module Profiler
|
|
|
24
24
|
events_cursor: events_cursor
|
|
25
25
|
}
|
|
26
26
|
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def recalculate_ajax_data(profile)
|
|
31
|
-
# Find AJAX collector in the configured collectors
|
|
32
|
-
ajax_collector_class = Profiler::Collectors::AjaxCollector
|
|
33
|
-
|
|
34
|
-
if Profiler.configuration.collectors.include?(ajax_collector_class)
|
|
35
|
-
collector = ajax_collector_class.new(profile)
|
|
36
|
-
collector.collect
|
|
37
|
-
|
|
38
|
-
# Update tab metadata to reflect has_data status
|
|
39
|
-
if profile.instance_variable_get(:@collectors_metadata)
|
|
40
|
-
ajax_tab = profile.instance_variable_get(:@collectors_metadata).find { |tab| tab[:key] == 'ajax' }
|
|
41
|
-
if ajax_tab
|
|
42
|
-
ajax_tab[:has_data] = collector.has_data?
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
27
|
end
|
|
48
28
|
end
|
|
49
29
|
end
|
|
@@ -6,6 +6,9 @@ module Profiler
|
|
|
6
6
|
class ApplicationController < ActionController::Base
|
|
7
7
|
layout "profiler/application"
|
|
8
8
|
|
|
9
|
+
# A store that cannot be created: the cause in one line, as a 503, for every page and API.
|
|
10
|
+
rescue_from Profiler::Storage::Unavailable::Error, with: :storage_unavailable
|
|
11
|
+
|
|
9
12
|
before_action :check_authorization
|
|
10
13
|
before_action :authorize_request
|
|
11
14
|
# Declared after the access checks, so that an unauthorized request gets its 403 first.
|
|
@@ -48,6 +51,14 @@ module Profiler
|
|
|
48
51
|
deny("Missing #{Profiler::FORGERY_PROTECTION_HEADER} header or CSRF token")
|
|
49
52
|
end
|
|
50
53
|
|
|
54
|
+
def storage_unavailable(error)
|
|
55
|
+
if controller_path.start_with?("profiler/api/")
|
|
56
|
+
render json: { error: error.message }, status: :service_unavailable
|
|
57
|
+
else
|
|
58
|
+
render plain: error.message, status: :service_unavailable
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
51
62
|
def deny(message)
|
|
52
63
|
if controller_path.start_with?("profiler/api/")
|
|
53
64
|
render json: { error: message }, status: :forbidden
|
|
@@ -56,22 +67,51 @@ module Profiler
|
|
|
56
67
|
end
|
|
57
68
|
end
|
|
58
69
|
|
|
70
|
+
# A page of one type of profile, filtered, ordered and paged by the store: one profile more
|
|
71
|
+
# than the limit tells whether there is a next page, at any offset.
|
|
72
|
+
def render_profile_page(type:, summary: true)
|
|
73
|
+
limit, offset = page_params
|
|
74
|
+
render_page(Profiler.storage.list(limit: limit + 1, offset: offset, type: type, summary: summary), limit, offset)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_page(profiles, limit, offset)
|
|
78
|
+
render json: {
|
|
79
|
+
profiles: profiles.first(limit).map(&:to_h),
|
|
80
|
+
limit: limit,
|
|
81
|
+
offset: offset,
|
|
82
|
+
has_more: profiles.size > limit
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def page_params
|
|
87
|
+
[[(params[:limit] || 50).to_i, 0].max, [(params[:offset] || 0).to_i, 0].max]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The children of a page, read once per request: the child jobs and the AJAX tab share them.
|
|
91
|
+
def child_profiles(profile)
|
|
92
|
+
@child_profiles ||= {}
|
|
93
|
+
@child_profiles[profile.token] ||= (@resolved_storage || Profiler.storage).find_by_parent(profile.token)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The AJAX sub-requests are saved after the page: their tab is computed here, whatever the
|
|
97
|
+
# configured collector list (Profiler::AjaxData).
|
|
98
|
+
def recalculate_ajax_data(profile)
|
|
99
|
+
Profiler::AjaxData.attach(profile, child_profiles(profile))
|
|
100
|
+
end
|
|
101
|
+
|
|
59
102
|
def build_child_jobs(profile)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
started_at: j.started_at&.iso8601
|
|
73
|
-
}
|
|
74
|
-
end
|
|
103
|
+
child_profiles(profile).select { |p| p.profile_type == "job" }.map do |j|
|
|
104
|
+
job_data = j.collector_data("job") || {}
|
|
105
|
+
{
|
|
106
|
+
token: j.token,
|
|
107
|
+
job_class: j.path,
|
|
108
|
+
job_id: job_data["job_id"],
|
|
109
|
+
queue: job_data["queue"],
|
|
110
|
+
status: job_data["status"],
|
|
111
|
+
duration: j.duration,
|
|
112
|
+
started_at: j.started_at&.iso8601
|
|
113
|
+
}
|
|
114
|
+
end
|
|
75
115
|
end
|
|
76
116
|
|
|
77
117
|
def build_parent_summary(profile)
|
|
@@ -144,22 +144,6 @@ module Profiler
|
|
|
144
144
|
end
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
-
def recalculate_ajax_data(profile)
|
|
148
|
-
ajax_collector_class = Profiler::Collectors::AjaxCollector
|
|
149
|
-
|
|
150
|
-
if Profiler.configuration.collectors.include?(ajax_collector_class)
|
|
151
|
-
collector = ajax_collector_class.new(profile, storage: @resolved_storage || Profiler.storage)
|
|
152
|
-
collector.collect
|
|
153
|
-
|
|
154
|
-
if profile.instance_variable_get(:@collectors_metadata)
|
|
155
|
-
ajax_tab = profile.instance_variable_get(:@collectors_metadata).find { |tab| tab[:key] == 'ajax' }
|
|
156
|
-
if ajax_tab
|
|
157
|
-
ajax_tab[:has_data] = collector.has_data?
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
147
|
def filter_profiles(profiles)
|
|
164
148
|
filtered = profiles
|
|
165
149
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "collectors/ajax_collector"
|
|
4
|
+
|
|
5
|
+
module Profiler
|
|
6
|
+
# The AJAX tab of a page: its sub-requests are saved after the page itself, so the tab is
|
|
7
|
+
# computed when the page is shown, from its children, whatever the configured collector list.
|
|
8
|
+
# The children are the ones the caller already read (the same find_by_parent gives the child
|
|
9
|
+
# jobs): only the http ones are sub-requests, the jobs stay in child_jobs.
|
|
10
|
+
module AjaxData
|
|
11
|
+
# Hands the children already read to the AjaxCollector in place of the storage.
|
|
12
|
+
Children = Struct.new(:profiles) do
|
|
13
|
+
def find_by_parent(_parent_token)
|
|
14
|
+
profiles
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Adds the ajax collector data to profile, and its tab when it has none: a page without
|
|
21
|
+
# sub-requests and without the tab is left as it is.
|
|
22
|
+
def attach(profile, children)
|
|
23
|
+
requests = children.select { |child| child.profile_type == "http" }
|
|
24
|
+
# A copy: the memory store hands out the very tabs it keeps.
|
|
25
|
+
tabs = profile.collectors_metadata = (profile.collectors_metadata || []).map(&:dup)
|
|
26
|
+
tab = tabs.find { |entry| (entry[:key] || entry["key"]).to_s == "ajax" }
|
|
27
|
+
return if requests.empty? && tab.nil?
|
|
28
|
+
|
|
29
|
+
collector = Collectors::AjaxCollector.new(profile, storage: Children.new(requests))
|
|
30
|
+
collector.collect
|
|
31
|
+
if tab.nil?
|
|
32
|
+
profile.add_collector_metadata(collector)
|
|
33
|
+
elsif tab.key?("key")
|
|
34
|
+
tab["has_data"] = collector.has_data?
|
|
35
|
+
else
|
|
36
|
+
tab[:has_data] = collector.has_data?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -33,11 +33,13 @@ module Profiler
|
|
|
33
33
|
|
|
34
34
|
# Storage-compatible interface for MCP query tools
|
|
35
35
|
|
|
36
|
-
def list(limit: 50, offset: 0, **)
|
|
37
|
-
# all_types mirrors Profiler.storage.list, which returns every profile type
|
|
38
|
-
# without it the proxy would only ever see http profiles (see
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
def list(limit: 50, offset: 0, type: nil, **)
|
|
37
|
+
# all_types mirrors Profiler.storage.list, which returns every profile type, or the one
|
|
38
|
+
# given as type; without it the proxy would only ever see http profiles (see
|
|
39
|
+
# ProfilesController#index). A slave of an earlier version ignores type: filtered here too.
|
|
40
|
+
data = get_json("/_profiler/api/profiles", limit: limit, offset: offset, all_types: true, type: type)
|
|
41
|
+
profiles = Array(data["profiles"]).map { |h| profile_from_api(h) }
|
|
42
|
+
type ? profiles.select { |p| p.profile_type == type.to_s } : profiles
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
# A token that is not one the gem issues is not found, without asking the slave.
|
|
@@ -29,13 +29,15 @@ module Profiler
|
|
|
29
29
|
|
|
30
30
|
# Options whose default depends on the Rails environment: the railtie sets them only when
|
|
31
31
|
# the application has not assigned them itself (see #default).
|
|
32
|
-
RAILS_DEFAULTED = %i[enabled storage track_tests].freeze
|
|
32
|
+
RAILS_DEFAULTED = %i[enabled storage track_tests max_profiles].freeze
|
|
33
|
+
|
|
34
|
+
BACKEND_LOCK = Mutex.new
|
|
33
35
|
|
|
34
36
|
attr_accessor :storage_options, :collectors,
|
|
35
37
|
:skip_paths, :slow_query_threshold, :max_queries_warning, :sql_backtrace,
|
|
36
38
|
:track_memory, :allocated_objects_warning_threshold,
|
|
37
39
|
:mcp_enabled, :mcp_transport, :mcp_port,
|
|
38
|
-
:authorization_mode, :
|
|
40
|
+
:authorization_mode, :extension_cors_enabled,
|
|
39
41
|
:cors_allowed_origins, :api_forgery_protection, :frame_ancestors,
|
|
40
42
|
:track_ajax, :ajax_skip_paths,
|
|
41
43
|
:track_http, :slow_http_threshold, :http_skip_hosts, :http_backtrace_depth,
|
|
@@ -51,7 +53,7 @@ module Profiler
|
|
|
51
53
|
:cluster_master, :cluster_secret, :cluster_require_secret,
|
|
52
54
|
:cluster_allow_insecure_http
|
|
53
55
|
|
|
54
|
-
attr_reader :authorize_block, :enabled, :track_tests
|
|
56
|
+
attr_reader :authorize_block, :enabled, :track_tests, :max_profiles
|
|
55
57
|
|
|
56
58
|
def initialize
|
|
57
59
|
@assigned = []
|
|
@@ -181,6 +183,13 @@ module Profiler
|
|
|
181
183
|
@track_tests = value
|
|
182
184
|
end
|
|
183
185
|
|
|
186
|
+
# How many profiles a store keeps, the oldest evicted first; nil for no cap on the count (the
|
|
187
|
+
# memory store still keeps 100). The railtie leaves it nil in the test environment.
|
|
188
|
+
def max_profiles=(value)
|
|
189
|
+
@assigned |= [:max_profiles]
|
|
190
|
+
@max_profiles = value
|
|
191
|
+
end
|
|
192
|
+
|
|
184
193
|
# Sets one of RAILS_DEFAULTED to value unless the application assigned it, in
|
|
185
194
|
# config/application.rb for instance, which runs before the railtie's initializers.
|
|
186
195
|
def default(option, value)
|
|
@@ -246,8 +255,10 @@ module Profiler
|
|
|
246
255
|
@storage_backend = nil
|
|
247
256
|
end
|
|
248
257
|
|
|
258
|
+
# Under a lock of its own, for a caller that asks the configuration directly; Profiler.storage
|
|
259
|
+
# takes it inside its own, always in that order.
|
|
249
260
|
def storage_backend
|
|
250
|
-
@storage_backend ||= build_storage_backend
|
|
261
|
+
@storage_backend || BACKEND_LOCK.synchronize { @storage_backend ||= build_storage_backend }
|
|
251
262
|
end
|
|
252
263
|
|
|
253
264
|
private
|
|
@@ -5,7 +5,7 @@ module Profiler
|
|
|
5
5
|
module Resources
|
|
6
6
|
class FailingTests
|
|
7
7
|
def self.call
|
|
8
|
-
profiles = Profiler.storage.list(limit: 500)
|
|
8
|
+
profiles = Profiler.storage.list(limit: 500, type: "test")
|
|
9
9
|
failing = profiles.select do |p|
|
|
10
10
|
next false unless p.profile_type == "test"
|
|
11
11
|
test_data = p.collector_data("test") || {}
|
|
@@ -5,8 +5,8 @@ module Profiler
|
|
|
5
5
|
module Resources
|
|
6
6
|
class RecentConsole
|
|
7
7
|
def self.call
|
|
8
|
-
profiles = Profiler.storage.list(limit:
|
|
9
|
-
consoles = profiles.select { |p| p.profile_type == "console" }
|
|
8
|
+
profiles = Profiler.storage.list(limit: 50, type: "console")
|
|
9
|
+
consoles = profiles.select { |p| p.profile_type == "console" }
|
|
10
10
|
|
|
11
11
|
data = consoles.map do |profile|
|
|
12
12
|
console_data = profile.collector_data("console") || {}
|
|
@@ -5,8 +5,8 @@ module Profiler
|
|
|
5
5
|
module Resources
|
|
6
6
|
class RecentJobs
|
|
7
7
|
def self.call
|
|
8
|
-
profiles = Profiler.storage.list(limit:
|
|
9
|
-
jobs = profiles.select { |p| p.profile_type == "job" }
|
|
8
|
+
profiles = Profiler.storage.list(limit: 50, type: "job")
|
|
9
|
+
jobs = profiles.select { |p| p.profile_type == "job" }
|
|
10
10
|
|
|
11
11
|
data = jobs.map do |profile|
|
|
12
12
|
job_data = profile.collector_data("job") || {}
|
|
@@ -5,7 +5,7 @@ module Profiler
|
|
|
5
5
|
module Resources
|
|
6
6
|
class SlowTests
|
|
7
7
|
def self.call
|
|
8
|
-
profiles = Profiler.storage.list(limit: 500)
|
|
8
|
+
profiles = Profiler.storage.list(limit: 500, type: "test")
|
|
9
9
|
tests = profiles.select { |p| p.profile_type == "test" }
|
|
10
10
|
.sort_by { |p| -p.duration }
|
|
11
11
|
.first(10)
|
data/lib/profiler/mcp/server.rb
CHANGED
|
@@ -35,8 +35,12 @@ module Profiler
|
|
|
35
35
|
handler = @resource_handlers[uri]
|
|
36
36
|
next [{ uri: uri, mimeType: "application/json", text: "Resource not found: #{uri}" }] unless handler
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
begin
|
|
39
|
+
result = handler.call
|
|
40
|
+
[{ uri: result[:uri], mimeType: result[:mimeType], text: result[:text] }]
|
|
41
|
+
rescue Profiler::Storage::Unavailable::Error => e
|
|
42
|
+
[{ uri: uri, mimeType: "text/plain", text: e.message }]
|
|
43
|
+
end
|
|
40
44
|
end
|
|
41
45
|
end
|
|
42
46
|
|
|
@@ -393,6 +397,8 @@ module Profiler
|
|
|
393
397
|
::MCP::Tool.define(name: name, description: description, input_schema: input_schema) do |server_context: nil, **args|
|
|
394
398
|
result = handler.call(args.transform_keys(&:to_s))
|
|
395
399
|
result.is_a?(::MCP::Tool::Response) ? result : ::MCP::Tool::Response.new(result)
|
|
400
|
+
rescue Profiler::Storage::Unavailable::Error => e
|
|
401
|
+
::MCP::Tool::Response.new([{ type: "text", text: e.message }], error: true)
|
|
396
402
|
end
|
|
397
403
|
end
|
|
398
404
|
|
|
@@ -22,6 +22,8 @@ module Profiler
|
|
|
22
22
|
return [{ type: "text", text: "Profile not found: #{token}" }]
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# The sub-requests are saved after the page: computed from its children, as the page does.
|
|
26
|
+
Profiler::AjaxData.attach(profile, storage.find_by_parent(profile.token))
|
|
25
27
|
ajax_data = profile.collector_data("ajax")
|
|
26
28
|
unless ajax_data && ajax_data["total_requests"].to_i > 0
|
|
27
29
|
return [{ type: "text", text: "No AJAX requests found in this profile" }]
|
|
@@ -14,7 +14,7 @@ module Profiler
|
|
|
14
14
|
|
|
15
15
|
storage = MCP::SlaveSupport.resolve_storage(params)
|
|
16
16
|
profile = if token == "latest"
|
|
17
|
-
profiles = storage.list(limit:
|
|
17
|
+
profiles = storage.list(limit: 1, type: "test")
|
|
18
18
|
profiles.find { |p| p.profile_type == "test" }
|
|
19
19
|
else
|
|
20
20
|
storage.load(token)
|
|
@@ -12,7 +12,7 @@ module Profiler
|
|
|
12
12
|
limit = params["limit"]&.to_i || 20
|
|
13
13
|
fetch_size = [limit * 5, 500].min
|
|
14
14
|
storage = MCP::SlaveSupport.resolve_storage(params)
|
|
15
|
-
profiles = storage.list(limit: fetch_size)
|
|
15
|
+
profiles = storage.list(limit: fetch_size, type: "console")
|
|
16
16
|
|
|
17
17
|
consoles = profiles.select { |p| p.profile_type == "console" }
|
|
18
18
|
|
|
@@ -12,7 +12,7 @@ module Profiler
|
|
|
12
12
|
limit = params["limit"]&.to_i || 20
|
|
13
13
|
fetch_size = [limit * 5, 500].min
|
|
14
14
|
storage = MCP::SlaveSupport.resolve_storage(params)
|
|
15
|
-
profiles = storage.list(limit: fetch_size)
|
|
15
|
+
profiles = storage.list(limit: fetch_size, type: "job")
|
|
16
16
|
|
|
17
17
|
jobs = profiles.select { |p| p.profile_type == "job" }
|
|
18
18
|
|
|
@@ -12,7 +12,7 @@ module Profiler
|
|
|
12
12
|
storage = MCP::SlaveSupport.resolve_storage(params)
|
|
13
13
|
limit = params["limit"]&.to_i || 20
|
|
14
14
|
fetch_size = [limit * 5, 500].min
|
|
15
|
-
profiles = storage.list(limit: fetch_size)
|
|
15
|
+
profiles = storage.list(limit: fetch_size, type: params["profile_type"])
|
|
16
16
|
|
|
17
17
|
profiles = profiles.select { |p| p.path&.include?(params["path"]) } if params["path"]
|
|
18
18
|
profiles = profiles.select { |p| p.method == params["method"]&.upcase } if params["method"]
|
|
@@ -12,7 +12,7 @@ module Profiler
|
|
|
12
12
|
limit = params["limit"]&.to_i || 20
|
|
13
13
|
fetch_size = [limit * 5, 500].min
|
|
14
14
|
storage = MCP::SlaveSupport.resolve_storage(params)
|
|
15
|
-
profiles = storage.list(limit: fetch_size)
|
|
15
|
+
profiles = storage.list(limit: fetch_size, type: "test")
|
|
16
16
|
|
|
17
17
|
tests = profiles.select { |p| p.profile_type == "test" }
|
|
18
18
|
|
|
@@ -114,7 +114,7 @@ module Profiler
|
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
def self.collect_run_profiles(since)
|
|
117
|
-
Profiler.storage.list(limit: 500).select do |p|
|
|
117
|
+
Profiler.storage.list(limit: 500, type: "test").select do |p|
|
|
118
118
|
p.profile_type == "test" && p.started_at && p.started_at >= since
|
|
119
119
|
end.map(&:token)
|
|
120
120
|
rescue
|
|
@@ -104,7 +104,10 @@ module Profiler
|
|
|
104
104
|
record_response(env, profile, request_body, status, headers, content, content.bytesize, true)
|
|
105
105
|
collect_all(profile, collectors)
|
|
106
106
|
|
|
107
|
-
Profiler.storage
|
|
107
|
+
storage = Profiler.storage
|
|
108
|
+
storage.save(profile.token, profile)
|
|
109
|
+
# Not saved: no token to send, no toolbar to inject for it.
|
|
110
|
+
return [status, headers, body] if storage.is_a?(Profiler::Storage::Unavailable)
|
|
108
111
|
|
|
109
112
|
set_header(headers, TOKEN_HEADER, profile.token)
|
|
110
113
|
|
data/lib/profiler/railtie.rb
CHANGED
|
@@ -15,6 +15,9 @@ module Profiler
|
|
|
15
15
|
config.default(:enabled, development_or_test)
|
|
16
16
|
config.default(:storage, development_or_test ? :file : :memory)
|
|
17
17
|
config.default(:track_tests, Rails.env.test?)
|
|
18
|
+
# One test profile per example: no cap on the count in the test environment, so that the
|
|
19
|
+
# report at the end of a suite sees all of them.
|
|
20
|
+
config.default(:max_profiles, Rails.env.test? ? nil : 100)
|
|
18
21
|
|
|
19
22
|
app.config.profiler.each do |key, value|
|
|
20
23
|
if config.respond_to?("#{key}=")
|