rails-profiler 0.30.3 → 0.30.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cc97292b89b1be664408c01611a72768b1212467b9149268ab1d16c8d90219e
4
- data.tar.gz: f9b2cc7d021b18d65dd0b5232269c4cf1657071d6b0c9416f90ed5df75f16ed3
3
+ metadata.gz: 04defc47e71c610492ff23fbce13ebf669f459a1cac74c5933200bdf4d52365c
4
+ data.tar.gz: d99cf7c5531fd369aa8c9367d95b0a5314f71f38de6ed64a7c81a9693f69566a
5
5
  SHA512:
6
- metadata.gz: '06952a2a976fbd59b49016384d11150b0a394e4e4c902446740e0d328b4c6fbaf7474277e0341ae87dd479de064aa7d80cbac2d289b66a2b886baa3d08b452b2'
7
- data.tar.gz: 4dc610ff0b4f4aea0f52c76539bb06bfd068f1ebbad87840e2b8448239da77f38fc7ee4e977040f616137bdc06f04e8cfd7936fcf62e80f3d37d36a8016a873a
6
+ metadata.gz: '09ba67258286ddc275d2d1cc36234dd8f0e7d24783034dd3959c666b41d96bb334d5c38d720ceca2254d205449094aa57cac9de6afb1842e3437fbd2d238eac3'
7
+ data.tar.gz: '0936d7ca150be6a80809739a2d9b94f77c51c80fad8796bb01d93fb2ca0047a5da3eb4ae832f9567c381bd3dcaacad3000b0273664362570306060c8b3b3eeec'
data/CHANGELOG.md ADDED
@@ -0,0 +1,402 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ Write your entry under `[Unreleased]`, for a user of the gem. When your branch is
9
+ ready, run `bin/changelog stamp`: it works out the version the merge will publish
10
+ and turns `[Unreleased]` into `[X.Y.Z] - <date>`, reopening an empty one above.
11
+ Run it again after a rebase or a new commit, it corrects the number and folds the
12
+ new entries in. Never edit a section that already carries a tag.
13
+
14
+ A section written by `stamp` carries `<!-- stamped -->`. In the older sections,
15
+ each bullet carries the short sha of the commit it describes, and commits left out
16
+ on purpose are listed in that version's `excluded:` comment with the reason; all of
17
+ it in HTML comments, invisible once rendered. `bin/changelog coverage` checks that
18
+ every commit of every tag interval is accounted for one way or the other.
19
+
20
+ ## [Unreleased]
21
+
22
+ ## [0.30.5] - 2026-10-03
23
+
24
+ <!-- stamped -->
25
+
26
+ ### Fixed
27
+
28
+ - **Instrumentation:** Pass the arguments of `Thread.new` on to its block, and keep keyword
29
+ arguments as keywords. While a profile was being collected the block got none at all, which on
30
+ Ruby 3.4 broke the Happy Eyeballs hostname resolution of `Socket.tcp`: connections by hostname
31
+ timed out, for example as `Redis::CannotConnectError`. Keyword arguments were flattened at all
32
+ times, on every Ruby. Affects v0.22.1 through v0.30.4.
33
+ - **Instrumentation:** Raise `ThreadError` on a `Thread.new` with no block during a profiled
34
+ request, as Ruby does.
35
+
36
+ ## [0.30.4] - 2026-07-01
37
+
38
+ ### Fixed
39
+
40
+ - **Cluster:** Resolve profiles across slaves from the master `ProfilesController` <!-- 9236c4c -->
41
+
42
+ ## [0.30.3] - 2026-06-29
43
+
44
+ ### Fixed
45
+
46
+ - Wrap the profile dashboards and `TestRunnerPage` with `QueryClientProvider` <!-- 420ae2f -->
47
+
48
+ ## [0.30.2] - 2026-06-29
49
+
50
+ ### Fixed
51
+
52
+ - **Middleware:** Duplicate downstream headers before mutating them <!-- 3efb20c -->
53
+
54
+ ## [0.30.1] - 2026-06-29
55
+
56
+ ### Fixed
57
+
58
+ - **Cluster:** Fix sentinel mismatch, profile list pagination, nil safety in MCP tools and HTTPS slave URLs <!-- 4b5fb1c -->
59
+
60
+ ## [0.30.0] - 2026-06-28
61
+
62
+ ### Added
63
+
64
+ - **Cluster:** Add master/slave profiler clustering <!-- 1435f22 -->
65
+
66
+ ## [0.29.0] - 2026-06-28
67
+
68
+ ### Added
69
+
70
+ - **SSE:** Push live profile updates over server-sent events <!-- fa0ae2e -->
71
+
72
+ ## [0.28.0] - 2026-06-06
73
+
74
+ ### Added
75
+
76
+ - **HTTP:** Add the `http_backtrace_depth` configuration option <!-- 25b4f99 -->
77
+
78
+ ## [0.27.1] - 2026-06-06
79
+
80
+ ### Changed
81
+
82
+ - **API:** Replace manual `fetch()` calls with `orval` generated TanStack Query hooks <!-- e33341e -->
83
+
84
+ ### Fixed
85
+
86
+ - **File store:** Prevent `ENOENT` errors on concurrent profile file deletion <!-- 8898f0e -->
87
+
88
+ ## [0.27.0] - 2026-06-02
89
+
90
+ ### Added
91
+
92
+ - **MCP:** Add console profiling, mailer detail, logs/env/i18n sections and full documentation <!-- ca362a1 -->
93
+
94
+ ## [0.26.0] - 2026-06-02
95
+
96
+ ### Added
97
+
98
+ - **Test profiler:** Add the `run_tests` MCP tool, the `track_tests` option, specs and docs <!-- 265ce03 -->
99
+ - **Test profiler:** Enrich reporter, add SSE streaming, MCP tools and env safety <!-- a410c4b -->
100
+ - **Test profiler:** Add test profiling, test runner UI and MCP tool <!-- a116fcb -->
101
+
102
+ ## [0.25.0] - 2026-06-01
103
+
104
+ ### Added
105
+
106
+ - **Console:** Add a console profiling tab with IRB instrumentation <!-- f29937a -->
107
+
108
+ ### Changed
109
+
110
+ - **Docs:** Document the console profiling tab and the env override behaviour <!-- 031cac6 -->
111
+
112
+ <!-- excluded:
113
+ bd55e9b specs only, no visible change
114
+ -->
115
+
116
+ ## [0.24.0] - 2026-06-01
117
+
118
+ ### Added
119
+
120
+ - **MCP:** Add tools to manage `ENV` variables <!-- 9126194 -->
121
+
122
+ ## [0.23.0] - 2026-05-27
123
+
124
+ ### Added
125
+
126
+ - **Profile list:** Add a global clear-all button and auto-refresh on tab switch <!-- 460a110 -->
127
+
128
+ ## [0.22.1] - 2026-05-26
129
+
130
+ ### Fixed
131
+
132
+ - **HTTP:** Propagate collector to child threads, add fire-and-forget support, fix deep backtrace capture <!-- 9d0bd61 -->
133
+
134
+ ## [0.22.0] - 2026-05-04
135
+
136
+ ### Added
137
+
138
+ - **Mailer:** Add a body preview, a variables display, a queued badge and design alignment <!-- b7d608f -->
139
+ - **Collector:** Implement `MailerCollector`, tracking ActionMailer deliveries <!-- e59dd72 -->
140
+
141
+ ### Changed
142
+
143
+ - **Toolbar:** Remove the emoji from the mailer toolbar item and the dashboard tab <!-- 55df9bb -->
144
+
145
+ ### Fixed
146
+
147
+ - **Mailer:** Add the Mailers tab to `JobProfileDashboard` <!-- 4ce145d -->
148
+ - **Mailer:** Fix thread isolation and `deliver_later` mode detection <!-- 6e76a99 -->
149
+ - **Mailer:** Add `MailerCollector` to the job profiler and align the toolbar item design <!-- 915be28 -->
150
+ - **Mailer:** Handle the Rails 7 encoded mail string in the `deliver.action_mailer` payload <!-- 5479dcf -->
151
+ - **Mailer:** Replace `Thread.current.delete` with a read and nil write for fiber local variables <!-- 8787201 -->
152
+ - **Version:** Remove the pre-release suffix, which broke Bundler resolution <!-- 98caecd -->
153
+ - **Mailer:** Address code review findings from MR !32 <!-- b46d3d2 -->
154
+
155
+ ## [0.21.0] - 2026-05-02
156
+
157
+ ### Added
158
+
159
+ - **Toolbar:** Add a collapse/expand toggle <!-- 8f0669a -->
160
+
161
+ ## [0.20.0] - 2026-05-01
162
+
163
+ ### Added
164
+
165
+ - **UI:** Display the gem version and warn on a mismatch <!-- 64ff6eb -->
166
+
167
+ ## [0.19.2] - 2026-04-24
168
+
169
+ ### Fixed
170
+
171
+ - **HTTP:** Capture `RestClient` `body_stream`, remove body size limits, show bodies in MCP tool results <!-- 89fb0e5 -->
172
+
173
+ <!-- excluded:
174
+ 4527e12 release plumbing, version placeholder reset
175
+ -->
176
+
177
+ ## [0.19.1] - 2026-04-17
178
+
179
+ ### Fixed
180
+
181
+ - **Env vars:** Fix revert restoring wrong value, SQLite busy errors, middleware rescue handling and UI refresh <!-- 55c8661 -->
182
+
183
+ <!-- excluded:
184
+ 4637391 pipeline only
185
+ b53b4d7 pipeline only
186
+ 7b33bf6 pipeline only
187
+ -->
188
+
189
+ ## [0.19.0] - 2026-04-15
190
+
191
+ ### Added
192
+
193
+ - **Env:** Propagate env var overrides to Sidekiq workers <!-- e75a883 -->
194
+
195
+ ## [0.18.0] - 2026-04-14
196
+
197
+ ### Changed
198
+
199
+ - **UI:** Extract shared tab utilities and redesign the Request tab <!-- 7aa8adb -->
200
+
201
+ ### Fixed
202
+
203
+ - **HTTP:** Capture outbound request body passed as second argument to `http.request()` <!-- 5a59665 -->
204
+
205
+ ## [0.17.0] - 2026-04-13
206
+
207
+ ### Added
208
+
209
+ - **Dashboard:** Add env tab on the home page and env/job toolbar items <!-- d0277ab -->
210
+
211
+ ## [0.16.0] - 2026-04-13
212
+
213
+ ### Added
214
+
215
+ - **Function profiler:** Add `stackprof` sampling mode with multi-clock support and GC visibility <!-- 5f73512 -->
216
+
217
+ ### Fixed
218
+
219
+ - **Function profiler:** Fix `stackprof` bugs, remove minimal mode, improve performance <!-- 76c6197 -->
220
+
221
+ ## [0.15.0] - 2026-04-11
222
+
223
+ ### Added
224
+
225
+ - **Env:** Add environment variables tab <!-- 0e85f18 -->
226
+
227
+ ## [0.14.0] - 2026-04-09
228
+
229
+ ### Added
230
+
231
+ - **Profiler:** Link jobs to their triggering request or parent job <!-- 74ba5e3 -->
232
+
233
+ ## [0.13.0] - 2026-04-09
234
+
235
+ ### Added
236
+
237
+ - **UI:** Add copy/download buttons on HTTP bodies, waterfall view, date sort, and other UX improvements <!-- 5afd003 -->
238
+ - **UI:** Always show waterfall above the HTTP request list <!-- 32c6b6d -->
239
+ - **UI:** Use `SmartBodyPreview` in RequestTab for copy/download on request and response bodies <!-- aef4c0c -->
240
+
241
+ ### Fixed
242
+
243
+ - **UI:** Derive download file extension from actual MIME type instead of body category <!-- 1dc0bc8 -->
244
+ - **UI:** Move `sortedJobs` after `filteredJobs` to fix TDZ error <!-- d4de7bc -->
245
+
246
+ ## [0.12.0] - 2026-04-09
247
+
248
+ ### Added
249
+
250
+ - **Profiler:** Add function-level profiling with flamegraph and stats table <!-- fcb04a6 -->
251
+
252
+ ## [0.11.1] - 2026-04-09
253
+
254
+ ### Added
255
+
256
+ - **Config:** Add `tmp_path` option to centralize the storage directory <!-- 5e77944 -->
257
+
258
+ ### Fixed
259
+
260
+ - **MCP:** Decompress gzip+base64 bodies before returning to UI and MCP tool results <!-- 8f5ac47 -->
261
+
262
+ ## [0.11.0] - 2026-04-06
263
+
264
+ ### Added
265
+
266
+ - **UI:** Add load-more pagination to the profile list <!-- 9f8458b -->
267
+
268
+ ## [0.10.1] - 2026-04-06
269
+
270
+ ### Changed
271
+
272
+ - **Docs:** Add full user documentation, with a UI guide, an MCP guide and screenshots <!-- 80a9198 -->
273
+
274
+ ## [0.10.0] - 2026-04-06
275
+
276
+ ### Added
277
+
278
+ - **Middleware:** Add configurable CORS origins, CSP nonce support and body compression <!-- 3a42188 -->
279
+
280
+ ## [0.9.1] - 2026-04-06
281
+
282
+ ### Removed
283
+
284
+ - **Collectors:** Remove `PerformanceCollector`, replaced by `FlameGraphCollector` <!-- 212e155 -->
285
+
286
+ ## [0.9.0] - 2026-04-06
287
+
288
+ ### Added
289
+
290
+ - **Profiler:** Add the `Profiler.measure` custom instrumentation API <!-- 0a3044e -->
291
+
292
+ ## [0.8.0] - 2026-04-05
293
+
294
+ ### Added
295
+
296
+ - **UI:** Add sortable columns, quick filter presets and flamegraph search <!-- ffd2e6f -->
297
+
298
+ ## [0.7.0] - 2026-04-05
299
+
300
+ ### Added
301
+
302
+ - **Database:** Add N+1 visual detection and `EXPLAIN ANALYZE` integration <!-- a22902d -->
303
+
304
+ ## [0.6.0] - 2026-04-05
305
+
306
+ ### Added
307
+
308
+ - **MCP:** Reduce token usage with filtering, body file saving and pagination <!-- 834251d -->
309
+
310
+ ## [0.5.0] - 2026-04-04
311
+
312
+ ### Added
313
+
314
+ - **MCP:** Add outbound IDs, a domain filter, datetimes, a latest token, exceptions and copy helpers <!-- 9330545 -->
315
+
316
+ ## [0.4.0] - 2026-04-03
317
+
318
+ ### Added
319
+
320
+ - **Collector:** Add an I18n panel tracking translation lookups per request <!-- 3032a15 -->
321
+
322
+ <!-- excluded:
323
+ b50d1eb pipeline only
324
+ -->
325
+
326
+ ## [0.3.0] - 2026-04-02
327
+
328
+ ### Added
329
+
330
+ - **Collector:** Add a routes panel and enrich the request collector <!-- bdf0e3c -->
331
+
332
+ ## [0.2.0] - 2026-03-29
333
+
334
+ ### Added
335
+
336
+ - **Collector:** Add a Logs panel, capturing `Rails.logger` messages per request <!-- 0e0e7ce -->
337
+ - Publish the gem to RubyGems.org <!-- f292a21 -->
338
+
339
+ <!-- excluded:
340
+ 10c1314 pipeline only, despite the fix: type
341
+ 3f97b1d pipeline only
342
+ a8c2044 pipeline only
343
+ 27cc550 pipeline only
344
+ -->
345
+
346
+ ## [0.1.4] - 2026-03-29
347
+
348
+ ### Added
349
+
350
+ - **Profiler:** Capture the full request and response, and add an MCP history clear <!-- 9922c8a -->
351
+
352
+ ## [0.1.3] - 2026-03-29
353
+
354
+ ### Changed
355
+
356
+ - **Assets:** Migrate Sass `@import` to `@use`, which removes the build deprecation warnings <!-- 1a67e48 -->
357
+
358
+ ## [0.1.2] - 2026-03-25
359
+
360
+ This tag holds nothing but the release commit the pipeline wrote. The interval
361
+ carries no change to the gem, so there is nothing to report for this version.
362
+
363
+ ## [0.1.1] - 2026-03-25
364
+
365
+ ### Added
366
+
367
+ - **HTTP:** Decompress gzip and deflate response bodies before storing them <!-- f71a5c7 -->
368
+ - **Flamegraph:** Add an interactive flame graph tab <!-- 63c4b67 -->
369
+ - **Config:** Add `.well-known` and `favicon.ico` to the default `skip_paths` <!-- 1286447 -->
370
+ - **Storage:** Add an SQLite backend, a blob store and delete/clear APIs <!-- 544015b -->
371
+ - **UI:** Add filters to the profile list action bar and fix the badge colours <!-- 40d4081 -->
372
+ - **HTTP:** Add binary download, inline preview and smart text formatting <!-- b09eab8 -->
373
+ - Rename the HTTP tab, add the outbound HTTP list and extend MCP coverage <!-- 71bc537 -->
374
+ - **Jobs:** Add background job profiling for Sidekiq and ActiveJob <!-- 27b87db -->
375
+ - Add an outbound HTTP request collector <!-- e5f2ac9 -->
376
+ - **MCP:** Migrate to the official Ruby SDK, over HTTP transport <!-- 3fc2d45 -->
377
+ - Implement the dashboard UI, with a tab system and a design system <!-- beb83f7 -->
378
+ - Add the AJAX and dump collectors, and the CORS middleware <!-- 08bad9f -->
379
+ - First implementation of the gem <!-- 360c6fa -->
380
+
381
+ ### Changed
382
+
383
+ - **Docs:** Document installation from the GitLab Package Registry <!-- 527c32b -->
384
+ - **Assets:** Serve every gem asset exclusively under `/_profiler/` <!-- e8ae1ae -->
385
+ - **Config:** Add `manifest.json` to the default `skip_paths` <!-- 350f16c -->
386
+ - **Docs:** Correct the installation instructions in the README and remove the dashboard README <!-- e44c8a2 -->
387
+
388
+ ### Fixed
389
+
390
+ - **Packaging:** Fix the gemspec metadata and file glob, and exclude source assets <!-- 195b609 -->
391
+ - **Packaging:** Switch to the yarn build scripts and update dependencies <!-- b62f2d8 -->
392
+ - **Packaging:** Fix the `http_collector` require, the collectors option override and asset serving in API only applications <!-- 729f20f -->
393
+ - **Packaging:** Exclude `.gem` build artifacts from the file list <!-- 9b97f60 -->
394
+ - **Profiler:** Improve memory tracking and profile deserialisation <!-- a0af47e -->
395
+
396
+ <!-- excluded:
397
+ e27cdae pipeline only
398
+ 02d63c9 pipeline only
399
+ 9f598d9 subject is not conventional, tooling only
400
+ 7d1d823 tests only
401
+ c0d46e6 licence file, not a change to the gem
402
+ -->
@@ -12,6 +12,7 @@ module Profiler
12
12
  # all_types is an opt-in used by the cluster proxy so it can mirror the full
13
13
  # storage.list contract; the dashboard relies on the default http-only filter.
14
14
  scope = all_types? ? all : all.select { |p| p.profile_type == "http" }
15
+ scope = scope.select { |p| p.parent_token == params[:parent_token] } if params[:parent_token].present?
15
16
  page = scope.drop(offset).first(limit + 1)
16
17
  render json: {
17
18
  profiles: page.first(limit).map(&:to_h),
@@ -17,26 +17,30 @@ module Profiler
17
17
  end
18
18
 
19
19
  def build_child_jobs(profile)
20
- Profiler.storage.find_by_parent(profile.token)
21
- .select { |p| p.profile_type == "job" }
22
- .map do |j|
23
- job_data = j.collector_data("job") || {}
24
- {
25
- token: j.token,
26
- job_class: j.path,
27
- job_id: job_data["job_id"],
28
- queue: job_data["queue"],
29
- status: job_data["status"],
30
- duration: j.duration,
31
- started_at: j.started_at&.iso8601
32
- }
33
- end
20
+ storage = @resolved_storage || Profiler.storage
21
+ storage.find_by_parent(profile.token)
22
+ .select { |p| p.profile_type == "job" }
23
+ .map do |j|
24
+ job_data = j.collector_data("job") || {}
25
+ {
26
+ token: j.token,
27
+ job_class: j.path,
28
+ job_id: job_data["job_id"],
29
+ queue: job_data["queue"],
30
+ status: job_data["status"],
31
+ duration: j.duration,
32
+ started_at: j.started_at&.iso8601
33
+ }
34
+ end
34
35
  end
35
36
 
36
37
  def build_parent_summary(profile)
37
38
  return nil unless profile.parent_token
38
39
 
39
- parent = Profiler.storage.load(profile.parent_token)
40
+ storage = @resolved_storage || Profiler.storage
41
+ parent = storage.load(profile.parent_token)
42
+ # Cross-node fallback: parent may live on the master when the profile is on a slave
43
+ parent ||= Profiler.storage.load(profile.parent_token) if storage != Profiler.storage
40
44
  return nil unless parent
41
45
 
42
46
  if parent.profile_type == "job"
@@ -13,7 +13,7 @@ module Profiler
13
13
  end
14
14
 
15
15
  def show
16
- @profile = Profiler.storage.load(params[:id])
16
+ @profile = resolve_profile(params[:id])
17
17
 
18
18
  unless @profile
19
19
  render plain: "Profile not found", status: :not_found
@@ -34,46 +34,121 @@ module Profiler
34
34
  end
35
35
 
36
36
  def timeline
37
- @profile = Profiler.storage.load(params[:id])
37
+ @profile = resolve_profile(params[:id])
38
+ return render plain: "Profile not found", status: :not_found unless @profile
39
+
38
40
  render json: @profile.collector_data("performance")
39
41
  end
40
42
 
41
43
  def database
42
- @profile = Profiler.storage.load(params[:id])
44
+ @profile = resolve_profile(params[:id])
45
+ return render plain: "Profile not found", status: :not_found unless @profile
46
+
43
47
  render json: @profile.collector_data("database")
44
48
  end
45
49
 
46
50
  def views
47
- @profile = Profiler.storage.load(params[:id])
51
+ @profile = resolve_profile(params[:id])
52
+ return render plain: "Profile not found", status: :not_found unless @profile
53
+
48
54
  render json: @profile.collector_data("view")
49
55
  end
50
56
 
51
57
  def cache
52
- @profile = Profiler.storage.load(params[:id])
58
+ @profile = resolve_profile(params[:id])
59
+ return render plain: "Profile not found", status: :not_found unless @profile
60
+
53
61
  render json: @profile.collector_data("cache")
54
62
  end
55
63
 
56
64
  def performance
57
- @profile = Profiler.storage.load(params[:id])
65
+ @profile = resolve_profile(params[:id])
66
+ return render plain: "Profile not found", status: :not_found unless @profile
67
+
58
68
  render json: @profile.collector_data("performance")
59
69
  end
60
70
 
61
71
  def flamegraph
62
- @profile = Profiler.storage.load(params[:id])
72
+ @profile = resolve_profile(params[:id])
73
+ return render plain: "Profile not found", status: :not_found unless @profile
74
+
63
75
  render json: @profile.collector_data("flamegraph")
64
76
  end
65
77
 
66
78
  private
67
79
 
80
+ # Resolves a profile token against local storage first, then online slaves.
81
+ # Sets @resolved_storage to the storage that owns the profile so that
82
+ # secondary lookups (child jobs, parent, AJAX) go to the same node.
83
+ def resolve_profile(token)
84
+ # Local storage — fast path, covers master-owned profiles
85
+ if (profile = Profiler.storage.load(token))
86
+ @resolved_storage = Profiler.storage
87
+ return profile
88
+ end
89
+
90
+ # Cache hit — skip fan-out if we already know which slave has this token
91
+ if (cached_slave = Profiler.token_cache.fetch(token))
92
+ entry = Profiler.slave_registry.all.find { |e| e[:name] == cached_slave }
93
+ if entry && entry[:status] == "online"
94
+ begin
95
+ proxy = Cluster::SlaveProxy.new(cached_slave, open_timeout: 2, read_timeout: 3)
96
+ if (profile = proxy.load(token))
97
+ @resolved_storage = proxy
98
+ return profile
99
+ end
100
+ rescue => e
101
+ Rails.logger.warn("[Profiler] Cached slave #{cached_slave} failed for #{token}: #{e.message}")
102
+ end
103
+ end
104
+ Profiler.token_cache.invalidate(token)
105
+ end
106
+
107
+ # Parallel fan-out across all online slaves
108
+ online_slaves = Profiler.slave_registry.online_names
109
+ return nil if online_slaves.empty?
110
+
111
+ found_profile = nil
112
+ found_proxy = nil
113
+ found_slave = nil
114
+ result_mutex = Mutex.new
115
+
116
+ threads = online_slaves.map do |slave_name|
117
+ Thread.new do
118
+ begin
119
+ proxy = Cluster::SlaveProxy.new(slave_name, open_timeout: 2, read_timeout: 3)
120
+ profile = proxy.load(token)
121
+ if profile
122
+ result_mutex.synchronize do
123
+ unless found_profile
124
+ found_profile = profile
125
+ found_proxy = proxy
126
+ found_slave = slave_name
127
+ end
128
+ end
129
+ end
130
+ rescue => e
131
+ Rails.logger.warn("[Profiler] Fan-out error for slave #{slave_name} (#{token}): #{e.message}")
132
+ end
133
+ end
134
+ end
135
+
136
+ threads.each(&:join)
137
+
138
+ if found_profile
139
+ Profiler.token_cache.store(token, found_slave)
140
+ @resolved_storage = found_proxy
141
+ found_profile
142
+ end
143
+ end
144
+
68
145
  def recalculate_ajax_data(profile)
69
- # Find AJAX collector in the configured collectors
70
146
  ajax_collector_class = Profiler::Collectors::AjaxCollector
71
147
 
72
148
  if Profiler.configuration.collectors.include?(ajax_collector_class)
73
- collector = ajax_collector_class.new(profile)
149
+ collector = ajax_collector_class.new(profile, storage: @resolved_storage || Profiler.storage)
74
150
  collector.collect
75
151
 
76
- # Update tab metadata to reflect has_data status
77
152
  if profile.instance_variable_get(:@collectors_metadata)
78
153
  ajax_tab = profile.instance_variable_get(:@collectors_metadata).find { |tab| tab[:key] == 'ajax' }
79
154
  if ajax_tab
@@ -11,11 +11,13 @@ module Profiler
11
11
  OPEN_TIMEOUT = 5 # seconds
12
12
  READ_TIMEOUT = 10 # seconds
13
13
 
14
- def initialize(slave_name)
14
+ def initialize(slave_name, open_timeout: nil, read_timeout: nil)
15
15
  entry = Profiler.slave_registry.find!(slave_name)
16
16
  raise Profiler::Error, "Slave profiler '#{slave_name}' is offline" if entry.status == "offline"
17
17
 
18
18
  @base_url = entry.url.to_s.chomp("/")
19
+ @open_timeout = open_timeout || OPEN_TIMEOUT
20
+ @read_timeout = read_timeout || READ_TIMEOUT
19
21
  end
20
22
 
21
23
  # Storage-compatible interface for MCP query tools
@@ -35,6 +37,11 @@ module Profiler
35
37
  profile_from_api(raw)
36
38
  end
37
39
 
40
+ def find_by_parent(parent_token)
41
+ data = get_json("/_profiler/api/profiles", parent_token: parent_token, all_types: true)
42
+ Array(data["profiles"]).map { |h| profile_from_api(h) }
43
+ end
44
+
38
45
  def clear(type: nil)
39
46
  params = type ? "?type=#{URI.encode_www_form_component(type)}" : ""
40
47
  delete_json("/_profiler/api/profiles/clear#{params}")
@@ -80,7 +87,7 @@ module Profiler
80
87
 
81
88
  def request(uri, req)
82
89
  resp = Net::HTTP.start(uri.hostname, uri.port,
83
- open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT,
90
+ open_timeout: @open_timeout, read_timeout: @read_timeout,
84
91
  use_ssl: uri.scheme == "https") do |http|
85
92
  http.request(req)
86
93
  end
@@ -45,6 +45,10 @@ module Profiler
45
45
  def all
46
46
  @slaves.values.map(&:to_h)
47
47
  end
48
+
49
+ def online_names
50
+ @slaves.select { |_, entry| entry.status == "online" }.keys
51
+ end
48
52
  end
49
53
  end
50
54
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent-ruby"
4
+
5
+ module Profiler
6
+ module Cluster
7
+ class TokenCache
8
+ TTL = 600 # 10 minutes
9
+
10
+ def initialize
11
+ @cache = Concurrent::Map.new
12
+ end
13
+
14
+ def fetch(token)
15
+ entry = @cache[token]
16
+ return nil if entry.nil? || Time.now > entry[:expires_at]
17
+
18
+ entry[:slave_name]
19
+ end
20
+
21
+ def store(token, slave_name)
22
+ @cache[token] = { slave_name: slave_name, expires_at: Time.now + TTL }
23
+ end
24
+
25
+ def invalidate(token)
26
+ @cache.delete(token)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -5,6 +5,11 @@ require_relative "base_collector"
5
5
  module Profiler
6
6
  module Collectors
7
7
  class AjaxCollector < BaseCollector
8
+ def initialize(profile, storage: Profiler.storage)
9
+ super(profile)
10
+ @storage = storage
11
+ end
12
+
8
13
  def icon
9
14
  "🌐"
10
15
  end
@@ -29,8 +34,7 @@ module Profiler
29
34
  end
30
35
 
31
36
  def collect
32
- # Query storage for child AJAX profiles
33
- ajax_profiles = Profiler.storage.find_by_parent(@profile.token)
37
+ ajax_profiles = @storage.find_by_parent(@profile.token)
34
38
 
35
39
  return store_data({}) if ajax_profiles.empty?
36
40
 
@@ -9,24 +9,39 @@ module Profiler
9
9
  ].freeze
10
10
 
11
11
  def initialize(*args, &block)
12
+ # Ruby refuses a Thread with no block, and the wrapper below is a block, so
13
+ # without this the error would never come while a profile is being collected.
14
+ return super if block.nil?
15
+
12
16
  parent_context = PROPAGATED_KEYS.filter_map do |key|
13
17
  val = Thread.current[key]
14
18
  [key, val] unless val.nil?
15
19
  end.to_h
16
20
 
17
- if parent_context.empty?
18
- super
19
- else
20
- super(*args) do
21
- parent_context.each { |k, v| Thread.current[k] = v }
22
- begin
23
- block&.call
24
- ensure
25
- PROPAGATED_KEYS.each { |k| Thread.current[k] = nil }
26
- end
21
+ return super if parent_context.empty?
22
+
23
+ # Thread.new hands its arguments to the block, so the wrapper has to take
24
+ # them and pass them on. Ruby 3.4 depends on it in the stdlib: the Happy
25
+ # Eyeballs hostname resolution of Socket.tcp runs
26
+ # Thread.new(*thread_args) { |*thread_args| resolve_hostname(*thread_args) }
27
+ # and a wrapper that swallows the arguments leaves the name unresolved.
28
+ # The wrapper is a named proc rather than a literal block because only a
29
+ # proc can be given the ruby2_keywords flag, which, together with the one
30
+ # on this method, keeps a trailing keyword hash a keyword hash rather than
31
+ # flattening it into a positional argument.
32
+ wrapper = proc do |*block_args|
33
+ parent_context.each { |k, v| Thread.current[k] = v }
34
+ begin
35
+ block&.call(*block_args)
36
+ ensure
37
+ PROPAGATED_KEYS.each { |k| Thread.current[k] = nil }
27
38
  end
28
39
  end
40
+ wrapper.ruby2_keywords
41
+
42
+ super(*args, &wrapper)
29
43
  end
44
+ ruby2_keywords :initialize
30
45
  end
31
46
  end
32
47
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.30.3"
4
+ VERSION = "0.30.5"
5
5
  end
data/lib/profiler.rb CHANGED
@@ -36,6 +36,13 @@ module Profiler
36
36
  end
37
37
  end
38
38
 
39
+ def token_cache
40
+ @token_cache ||= begin
41
+ require_relative "profiler/cluster/token_cache"
42
+ Cluster::TokenCache.new
43
+ end
44
+ end
45
+
39
46
  def enabled?
40
47
  configuration.enabled
41
48
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.3
4
+ version: 0.30.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-29 00:00:00.000000000 Z
11
+ date: 2026-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -103,6 +103,7 @@ executables:
103
103
  extensions: []
104
104
  extra_rdoc_files: []
105
105
  files:
106
+ - CHANGELOG.md
106
107
  - app/assets/builds/profiler-toolbar.js
107
108
  - app/assets/builds/profiler.css
108
109
  - app/assets/builds/profiler.js
@@ -135,6 +136,7 @@ files:
135
136
  - lib/profiler/cluster/master_client.rb
136
137
  - lib/profiler/cluster/slave_proxy.rb
137
138
  - lib/profiler/cluster/slave_registry.rb
139
+ - lib/profiler/cluster/token_cache.rb
138
140
  - lib/profiler/collectors/ajax_collector.rb
139
141
  - lib/profiler/collectors/base_collector.rb
140
142
  - lib/profiler/collectors/cache_collector.rb
@@ -230,6 +232,7 @@ licenses:
230
232
  metadata:
231
233
  homepage_uri: https://git.duplessy.eu/sebastien/rails-profiler-gem
232
234
  source_code_uri: https://git.duplessy.eu/sebastien/rails-profiler-gem
235
+ changelog_uri: https://git.duplessy.eu/sebastien/rails-profiler-gem/-/blob/master/CHANGELOG.md
233
236
  post_install_message:
234
237
  rdoc_options: []
235
238
  require_paths: