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 +4 -4
- data/app/controllers/profiler/api/profiles_controller.rb +1 -0
- data/app/controllers/profiler/application_controller.rb +19 -15
- data/app/controllers/profiler/profiles_controller.rb +85 -10
- data/lib/profiler/cluster/slave_proxy.rb +9 -2
- data/lib/profiler/cluster/slave_registry.rb +4 -0
- data/lib/profiler/cluster/token_cache.rb +30 -0
- data/lib/profiler/collectors/ajax_collector.rb +6 -2
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +7 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2940fc44efea04a648f7bba562f2fc19be1639d996525e9d08483ef7093baa16
|
|
4
|
+
data.tar.gz: 1b9f6534d20625950aef62f906319942d7327ea609649aef00b70ba88c9b17ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
data/lib/profiler/version.rb
CHANGED
data/lib/profiler.rb
CHANGED
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.
|
|
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-
|
|
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
|