rails-profiler 0.30.3 → 0.30.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cc97292b89b1be664408c01611a72768b1212467b9149268ab1d16c8d90219e
4
- data.tar.gz: f9b2cc7d021b18d65dd0b5232269c4cf1657071d6b0c9416f90ed5df75f16ed3
3
+ metadata.gz: 2940fc44efea04a648f7bba562f2fc19be1639d996525e9d08483ef7093baa16
4
+ data.tar.gz: 1b9f6534d20625950aef62f906319942d7327ea609649aef00b70ba88c9b17ce
5
5
  SHA512:
6
- metadata.gz: '06952a2a976fbd59b49016384d11150b0a394e4e4c902446740e0d328b4c6fbaf7474277e0341ae87dd479de064aa7d80cbac2d289b66a2b886baa3d08b452b2'
7
- data.tar.gz: 4dc610ff0b4f4aea0f52c76539bb06bfd068f1ebbad87840e2b8448239da77f38fc7ee4e977040f616137bdc06f04e8cfd7936fcf62e80f3d37d36a8016a873a
6
+ metadata.gz: 6ce1584ab5b3d70a6262d205cfbd0e4876d29e89b3766efde13fe345966734c20f6a73df1fe1e14b71f7b8af5c588b04e02c7e9a8b16a0f00c902ff8f5a55aa6
7
+ data.tar.gz: 6e02c794a279605d81ac11c5f31f099378af2d82c967e757bf571ce2ff27ec7ca3f7c342f7e90cca628c5a406c253b0f0252971a723d0c4970db86ec35bd5c4e
@@ -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
 
@@ -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.4"
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.4
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-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -135,6 +135,7 @@ files:
135
135
  - lib/profiler/cluster/master_client.rb
136
136
  - lib/profiler/cluster/slave_proxy.rb
137
137
  - lib/profiler/cluster/slave_registry.rb
138
+ - lib/profiler/cluster/token_cache.rb
138
139
  - lib/profiler/collectors/ajax_collector.rb
139
140
  - lib/profiler/collectors/base_collector.rb
140
141
  - lib/profiler/collectors/cache_collector.rb