memhealth 0.1.1 → 0.1.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/README.md +33 -6
- data/app/controllers/mem_health/dashboard_controller.rb +13 -6
- data/app/views/mem_health/dashboard/_web_view.html.erb +172 -0
- data/app/views/mem_health/dashboard/_worker_view.html.erb +166 -0
- data/app/views/mem_health/dashboard/index.html.erb +12 -168
- data/lib/mem_health/job_tracking_middleware.rb +97 -0
- data/lib/mem_health/middleware.rb +18 -87
- data/lib/mem_health/tracker.rb +70 -16
- data/lib/mem_health/tracking_concern.rb +77 -0
- data/lib/mem_health/version.rb +1 -1
- data/lib/memhealth.rb +2 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5fda50a609f167feffba18ed772f70c528fe8ae883605520a8b51283ea9e45af
|
|
4
|
+
data.tar.gz: 30bb76d54747594b6fe1b2b9432759a53f201b2ed8b6e5852a78fe7775f654a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d69b6f4a6f3341f1eb02f64301d690479788fbd6de6e8ef65e9da88ce40c709af7743c685d290d42e020003d67bd093318ed6189ebe722fa1a37eb25017a800
|
|
7
|
+
data.tar.gz: 9160e5176c77f8d06e3f6ade37ecc39b0ba171829370eb93f33b332b51c775096c403aba3cd52e977b17912fc33123d8f9ba2b9d47da73aaf47138bab8c98072
|
data/README.md
CHANGED
|
@@ -40,13 +40,30 @@ Rails.application.routes.draw do
|
|
|
40
40
|
end
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
### Worker tracking setup (Sidekiq)
|
|
44
|
+
|
|
45
|
+
To track memory usage in background jobs, add the middleware to your Sidekiq configuration:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# config/initializers/sidekiq.rb
|
|
49
|
+
Sidekiq.configure_server do |config|
|
|
50
|
+
config.server_middleware do |chain|
|
|
51
|
+
chain.add MemHealth::JobTrackingMiddleware
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The dashboard will show two tabs:
|
|
57
|
+
- **Web Servers**: Tracks memory usage for HTTP requests
|
|
58
|
+
- **Worker Servers**: Tracks memory usage for Sidekiq background jobs
|
|
59
|
+
|
|
43
60
|
# Features
|
|
44
61
|
|
|
45
|
-
- Real-time memory usage monitoring
|
|
46
|
-
- Track highest memory consuming requests
|
|
62
|
+
- Real-time memory usage monitoring for web and worker servers
|
|
63
|
+
- Track highest memory consuming requests and background jobs
|
|
47
64
|
- Account-level tracking for multi-tenant apps
|
|
48
65
|
- Redis-based data storage
|
|
49
|
-
- Web dashboard for
|
|
66
|
+
- Web dashboard with separate views for web and worker metrics
|
|
50
67
|
- Configurable thresholds and limits
|
|
51
68
|
|
|
52
69
|
<img width="1139" height="680" alt="s_2" src="https://github.com/user-attachments/assets/5e170097-77cf-4ec5-a7b0-47aeaf92135f" />
|
|
@@ -97,13 +114,23 @@ end
|
|
|
97
114
|
## Console Usage
|
|
98
115
|
|
|
99
116
|
```ruby
|
|
100
|
-
# View statistics
|
|
117
|
+
# View web statistics
|
|
101
118
|
MemHealth::Tracker.print_stats
|
|
102
119
|
|
|
103
|
-
# Get top memory consuming URLs
|
|
120
|
+
# Get top memory consuming URLs (web)
|
|
104
121
|
MemHealth::Tracker.top_memory_urls
|
|
105
122
|
|
|
106
|
-
#
|
|
123
|
+
# Get top memory consuming jobs (worker)
|
|
124
|
+
MemHealth::Tracker.top_memory_jobs
|
|
125
|
+
|
|
126
|
+
# Get worker statistics
|
|
127
|
+
MemHealth::Tracker.worker_stats
|
|
128
|
+
|
|
129
|
+
# Get max memory diff for web or worker
|
|
130
|
+
MemHealth::Tracker.max_memory_diff(type: :web)
|
|
131
|
+
MemHealth::Tracker.max_memory_diff(type: :worker)
|
|
132
|
+
|
|
133
|
+
# Clear all data (web and worker)
|
|
107
134
|
MemHealth::Tracker.clear_all_data
|
|
108
135
|
```
|
|
109
136
|
|
|
@@ -2,18 +2,25 @@ module MemHealth
|
|
|
2
2
|
class DashboardController < ActionController::Base
|
|
3
3
|
protect_from_forgery with: :exception
|
|
4
4
|
layout "memhealth/application"
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
def index
|
|
7
7
|
@memory_hunter_enabled = MemHealth.configuration.enabled?
|
|
8
|
+
@view_type = params[:view] || 'web'
|
|
8
9
|
|
|
9
10
|
if @memory_hunter_enabled
|
|
10
|
-
@
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
if @view_type == 'worker'
|
|
12
|
+
@stats = MemHealth::Tracker.worker_stats
|
|
13
|
+
@top_items = MemHealth::Tracker.top_memory_jobs
|
|
14
|
+
@max_memory_item = MemHealth::Tracker.max_memory_job
|
|
15
|
+
else
|
|
16
|
+
@stats = MemHealth::Tracker.stats
|
|
17
|
+
@top_items = MemHealth::Tracker.top_memory_urls
|
|
18
|
+
@max_memory_item = MemHealth::Tracker.max_memory_url
|
|
19
|
+
end
|
|
13
20
|
else
|
|
14
21
|
@stats = nil
|
|
15
|
-
@
|
|
16
|
-
@
|
|
22
|
+
@top_items = []
|
|
23
|
+
@max_memory_item = nil
|
|
17
24
|
end
|
|
18
25
|
end
|
|
19
26
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<!-- Highest Memory Usage URL - Prominent display -->
|
|
2
|
+
<% if @max_memory_item %>
|
|
3
|
+
<div class="bg-red-50 border border-red-200 rounded-lg px-6 py-6 mb-6">
|
|
4
|
+
<h3 class="text-lg font-semibold text-red-800 mb-4">🚨 Highest Memory Usage URL</h3>
|
|
5
|
+
|
|
6
|
+
<div class="bg-white rounded-md px-6 py-4 border border-red-100">
|
|
7
|
+
<!-- URL Section -->
|
|
8
|
+
<div class="mb-4">
|
|
9
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">URL</dt>
|
|
10
|
+
<dd class="text-sm text-gray-900 font-mono bg-gray-100 px-2 py-1 rounded ml-0 flex items-center gap-2" title="<%= @max_memory_item["url"] %>">
|
|
11
|
+
<% if @max_memory_item["request_method"] %>
|
|
12
|
+
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded flex-shrink-0 <%= @max_memory_item["request_method"] == 'GET' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' %>">
|
|
13
|
+
<%= @max_memory_item["request_method"] %>
|
|
14
|
+
</span>
|
|
15
|
+
<% end %>
|
|
16
|
+
<span class="truncate"><%= @max_memory_item["url"] %></span>
|
|
17
|
+
</dd>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- Single row with all metadata -->
|
|
21
|
+
<div class="flex gap-4">
|
|
22
|
+
<div class="flex-1 text-left">
|
|
23
|
+
<div class="bg-red-100 border border-red-300 rounded-md px-4 py-3">
|
|
24
|
+
<dt class="text-sm font-medium text-red-700 mb-1">Memory Diff</dt>
|
|
25
|
+
<dd class="text-lg font-bold text-red-800 ml-0">
|
|
26
|
+
<%= @max_memory_item["memory_diff"] %> MB
|
|
27
|
+
</dd>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="flex-1 text-left">
|
|
32
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">RAM Usage</dt>
|
|
33
|
+
<dd class="text-sm text-gray-600 ml-0">
|
|
34
|
+
<%= @max_memory_item["ram_before"] %> MB → <%= @max_memory_item["ram_after"] %> MB
|
|
35
|
+
</dd>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="flex-1 text-left">
|
|
39
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">Execution Time</dt>
|
|
40
|
+
<dd class="text-sm text-gray-600 ml-0">
|
|
41
|
+
<%= @max_memory_item["execution_time"] ? "#{@max_memory_item["execution_time"]} s" : "-" %>
|
|
42
|
+
</dd>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="flex-1 text-left">
|
|
46
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">Info</dt>
|
|
47
|
+
<dd class="text-sm text-gray-600 ml-0">
|
|
48
|
+
<% info_parts = [] %>
|
|
49
|
+
<% info_parts << @max_memory_item["dyno"] if @max_memory_item["dyno"] %>
|
|
50
|
+
<% info_parts << "T#{@max_memory_item["puma_thread_index"]}" if @max_memory_item["puma_thread_index"] %>
|
|
51
|
+
<% info_parts << "##{@max_memory_item["account_id"]}" if @max_memory_item["account_id"].present? %>
|
|
52
|
+
<% info_parts << Time.parse(@max_memory_item["recorded_at"]).utc.strftime("%m/%d %H:%M") %>
|
|
53
|
+
<%= info_parts.join(" / ") %>
|
|
54
|
+
</dd>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<% end %>
|
|
60
|
+
|
|
61
|
+
<!-- Stats summary card -->
|
|
62
|
+
<div class="bg-white rounded-lg shadow px-6 py-6 mb-6">
|
|
63
|
+
<h3 class="text-lg font-medium text-gray-900 mb-4">Memory Usage Statistics</h3>
|
|
64
|
+
|
|
65
|
+
<div class="flex gap-4">
|
|
66
|
+
<div class="bg-blue-50 p-4 rounded-md flex-1 text-left">
|
|
67
|
+
<dt class="text-sm font-medium text-blue-600">Max Memory Difference</dt>
|
|
68
|
+
<dd class="mt-1 text-2xl font-semibold text-blue-900 text-left ml-0"><%= @stats[:max_memory_diff] %> MB</dd>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="bg-green-50 p-4 rounded-md flex-1 text-left">
|
|
72
|
+
<dt class="text-sm font-medium text-green-600">Stored URLs</dt>
|
|
73
|
+
<dd class="mt-1 text-2xl font-semibold text-green-900 text-left ml-0"><%= @stats[:stored_urls_count] %>/<%= @stats[:max_stored_urls] %></dd>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="bg-yellow-50 p-4 rounded-md flex-1 text-left">
|
|
77
|
+
<dt class="text-sm font-medium text-yellow-600">Requests Tracked</dt>
|
|
78
|
+
<dd class="mt-1 text-2xl font-semibold text-yellow-900 text-left ml-0"><%= @stats[:tracked_requests_count] %></dd>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="bg-purple-50 p-4 rounded-md flex-1 text-left">
|
|
82
|
+
<dt class="text-sm font-medium text-purple-600">Total Requests on current dyno/Rails instance</dt>
|
|
83
|
+
<dd class="mt-1 text-2xl font-semibold text-purple-900 text-left ml-0"><%= @stats[:total_requests_count] %></dd>
|
|
84
|
+
<% if @stats[:skipped_requests_count] > 0 %>
|
|
85
|
+
<p class="text-xs text-purple-500 mt-1"><%= @stats[:skipped_requests_count] %> skipped</p>
|
|
86
|
+
<% end %>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Top URLs table -->
|
|
92
|
+
<% if @top_items.any? %>
|
|
93
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
94
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
95
|
+
<h3 class="text-lg font-medium text-gray-900">Top <%= MemHealth.configuration.max_stored_urls %> URLs by Memory Usage</h3>
|
|
96
|
+
<p class="text-sm text-gray-600 mt-1">URLs ordered by memory difference (highest first) - maximum <%= MemHealth.configuration.max_stored_urls %> URLs stored</p>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
100
|
+
<thead class="bg-gray-50">
|
|
101
|
+
<tr>
|
|
102
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory Diff</th>
|
|
103
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM Before</th>
|
|
104
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM After</th>
|
|
105
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Exec Time</th>
|
|
106
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
|
|
107
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dyno/Thread</th>
|
|
108
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account ID</th>
|
|
109
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Recorded At</th>
|
|
110
|
+
</tr>
|
|
111
|
+
</thead>
|
|
112
|
+
|
|
113
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
114
|
+
<% @top_items.each do |url_data| %>
|
|
115
|
+
<tr>
|
|
116
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
117
|
+
<%
|
|
118
|
+
memory_color = if url_data["memory_diff"] > 10
|
|
119
|
+
"bg-red-100 text-red-800"
|
|
120
|
+
elsif url_data["memory_diff"] > 5
|
|
121
|
+
"bg-yellow-100 text-yellow-800"
|
|
122
|
+
else
|
|
123
|
+
"bg-green-100 text-green-800"
|
|
124
|
+
end
|
|
125
|
+
%>
|
|
126
|
+
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full <%= memory_color %>">
|
|
127
|
+
<%= url_data["memory_diff"] %> MB
|
|
128
|
+
</span>
|
|
129
|
+
</td>
|
|
130
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
131
|
+
<%= url_data["ram_before"] ? "#{url_data["ram_before"]} MB" : "/" %>
|
|
132
|
+
</td>
|
|
133
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
134
|
+
<%= url_data["ram_after"] ? "#{url_data["ram_after"]} MB" : "/" %>
|
|
135
|
+
</td>
|
|
136
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
137
|
+
<%= url_data["execution_time"] ? "#{url_data["execution_time"]} s" : "/" %>
|
|
138
|
+
</td>
|
|
139
|
+
<td class="px-6 py-4 text-gray-700 url-cell">
|
|
140
|
+
<div title="<%= url_data["url"] %>" style="font-size: 0.75rem; line-height: 1rem;">
|
|
141
|
+
<% if url_data["request_method"] %>
|
|
142
|
+
<span class="inline-flex px-1 py-0.5 font-semibold rounded <%= url_data["request_method"] == 'GET' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' %>" style="font-size: 0.65rem;">
|
|
143
|
+
<%= url_data["request_method"] %>
|
|
144
|
+
</span>
|
|
145
|
+
<% end %>
|
|
146
|
+
<code class="bg-gray-100 px-1 py-0.5 rounded"><%= url_data["url"] %></code>
|
|
147
|
+
</div>
|
|
148
|
+
</td>
|
|
149
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
150
|
+
<% dyno_thread = [] %>
|
|
151
|
+
<% dyno_thread << url_data["dyno"] if url_data["dyno"] %>
|
|
152
|
+
<% dyno_thread << "T#{url_data["puma_thread_index"]}" if url_data["puma_thread_index"] %>
|
|
153
|
+
<%= dyno_thread.any? ? dyno_thread.join("/") : "/" %>
|
|
154
|
+
</td>
|
|
155
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
156
|
+
<%= url_data["account_id"].present? ? url_data["account_id"] : "/" %>
|
|
157
|
+
</td>
|
|
158
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
159
|
+
<%= Time.parse(url_data["recorded_at"]).utc.strftime("%Y-%m-%d %H:%M") %>
|
|
160
|
+
</td>
|
|
161
|
+
</tr>
|
|
162
|
+
<% end %>
|
|
163
|
+
</tbody>
|
|
164
|
+
</table>
|
|
165
|
+
</div>
|
|
166
|
+
<% else %>
|
|
167
|
+
<div class="bg-white rounded-lg shadow px-6 py-4 text-center">
|
|
168
|
+
<h3 class="text-lg font-medium text-gray-900 mb-2">No URLs Tracked Yet</h3>
|
|
169
|
+
<p class="text-gray-600">No URLs have been recorded yet.</p>
|
|
170
|
+
<p class="text-sm text-gray-500 mt-2">Memory tracking will begin after the first <%= MemHealth.configuration.skip_requests %> requests to your application.</p>
|
|
171
|
+
</div>
|
|
172
|
+
<% end %>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<!-- Highest Memory Usage Job - Prominent display -->
|
|
2
|
+
<% if @max_memory_item %>
|
|
3
|
+
<div class="bg-red-50 border border-red-200 rounded-lg px-6 py-6 mb-6">
|
|
4
|
+
<h3 class="text-lg font-semibold text-red-800 mb-4">🚨 Highest Memory Usage Job</h3>
|
|
5
|
+
|
|
6
|
+
<div class="bg-white rounded-md px-6 py-4 border border-red-100">
|
|
7
|
+
<!-- Job Section -->
|
|
8
|
+
<div class="mb-4">
|
|
9
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">Worker Class</dt>
|
|
10
|
+
<dd class="text-sm text-gray-900 font-mono bg-gray-100 px-2 py-1 rounded ml-0">
|
|
11
|
+
<%= @max_memory_item["worker_class"] %>
|
|
12
|
+
</dd>
|
|
13
|
+
<% if @max_memory_item["job_args"].present? %>
|
|
14
|
+
<dt class="text-sm font-medium text-gray-500 mb-1 mt-2">Arguments</dt>
|
|
15
|
+
<dd class="text-xs text-gray-600 font-mono bg-gray-50 px-2 py-1 rounded ml-0 break-all">
|
|
16
|
+
<%= @max_memory_item["job_args"] %>
|
|
17
|
+
</dd>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Single row with all metadata -->
|
|
22
|
+
<div class="flex gap-4">
|
|
23
|
+
<div class="flex-1 text-left">
|
|
24
|
+
<div class="bg-red-100 border border-red-300 rounded-md px-4 py-3">
|
|
25
|
+
<dt class="text-sm font-medium text-red-700 mb-1">Memory Diff</dt>
|
|
26
|
+
<dd class="text-lg font-bold text-red-800 ml-0">
|
|
27
|
+
<%= @max_memory_item["memory_diff"] %> MB
|
|
28
|
+
</dd>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="flex-1 text-left">
|
|
33
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">RAM Usage</dt>
|
|
34
|
+
<dd class="text-sm text-gray-600 ml-0">
|
|
35
|
+
<%= @max_memory_item["ram_before"] %> MB → <%= @max_memory_item["ram_after"] %> MB
|
|
36
|
+
</dd>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="flex-1 text-left">
|
|
40
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">Execution Time</dt>
|
|
41
|
+
<dd class="text-sm text-gray-600 ml-0">
|
|
42
|
+
<%= @max_memory_item["execution_time"] ? "#{@max_memory_item["execution_time"]} s" : "-" %>
|
|
43
|
+
</dd>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="flex-1 text-left">
|
|
47
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">Queue</dt>
|
|
48
|
+
<dd class="text-sm text-gray-600 ml-0">
|
|
49
|
+
<%= @max_memory_item["queue"] || "-" %>
|
|
50
|
+
</dd>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="flex-1 text-left">
|
|
54
|
+
<dt class="text-sm font-medium text-gray-500 mb-1">Info</dt>
|
|
55
|
+
<dd class="text-sm text-gray-600 ml-0">
|
|
56
|
+
<% info_parts = [] %>
|
|
57
|
+
<% info_parts << @max_memory_item["dyno"] if @max_memory_item["dyno"] %>
|
|
58
|
+
<% info_parts << Time.parse(@max_memory_item["recorded_at"]).utc.strftime("%m/%d %H:%M") %>
|
|
59
|
+
<%= info_parts.join(" / ") %>
|
|
60
|
+
</dd>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<% end %>
|
|
66
|
+
|
|
67
|
+
<!-- Stats summary card -->
|
|
68
|
+
<div class="bg-white rounded-lg shadow px-6 py-6 mb-6">
|
|
69
|
+
<h3 class="text-lg font-medium text-gray-900 mb-4">Worker Memory Usage Statistics</h3>
|
|
70
|
+
|
|
71
|
+
<div class="flex gap-4">
|
|
72
|
+
<div class="bg-blue-50 p-4 rounded-md flex-1 text-left">
|
|
73
|
+
<dt class="text-sm font-medium text-blue-600">Max Memory Difference</dt>
|
|
74
|
+
<dd class="mt-1 text-2xl font-semibold text-blue-900 text-left ml-0"><%= @stats[:max_memory_diff] %> MB</dd>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="bg-green-50 p-4 rounded-md flex-1 text-left">
|
|
78
|
+
<dt class="text-sm font-medium text-green-600">Stored Jobs</dt>
|
|
79
|
+
<dd class="mt-1 text-2xl font-semibold text-green-900 text-left ml-0"><%= @stats[:stored_jobs_count] %>/<%= @stats[:max_stored_jobs] %></dd>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Top Jobs table -->
|
|
85
|
+
<% if @top_items.any? %>
|
|
86
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
87
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
88
|
+
<h3 class="text-lg font-medium text-gray-900">Top <%= MemHealth.configuration.max_stored_urls %> Jobs by Memory Usage</h3>
|
|
89
|
+
<p class="text-sm text-gray-600 mt-1">Jobs ordered by memory difference (highest first) - maximum <%= MemHealth.configuration.max_stored_urls %> jobs stored</p>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
93
|
+
<thead class="bg-gray-50">
|
|
94
|
+
<tr>
|
|
95
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory Diff</th>
|
|
96
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM Before</th>
|
|
97
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM After</th>
|
|
98
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Exec Time</th>
|
|
99
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Worker Class / Args</th>
|
|
100
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Queue</th>
|
|
101
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dyno</th>
|
|
102
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Job ID</th>
|
|
103
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Recorded At</th>
|
|
104
|
+
</tr>
|
|
105
|
+
</thead>
|
|
106
|
+
|
|
107
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
108
|
+
<% @top_items.each do |job_data| %>
|
|
109
|
+
<tr>
|
|
110
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
111
|
+
<%
|
|
112
|
+
memory_color = if job_data["memory_diff"] > 10
|
|
113
|
+
"bg-red-100 text-red-800"
|
|
114
|
+
elsif job_data["memory_diff"] > 5
|
|
115
|
+
"bg-yellow-100 text-yellow-800"
|
|
116
|
+
else
|
|
117
|
+
"bg-green-100 text-green-800"
|
|
118
|
+
end
|
|
119
|
+
%>
|
|
120
|
+
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full <%= memory_color %>">
|
|
121
|
+
<%= job_data["memory_diff"] %> MB
|
|
122
|
+
</span>
|
|
123
|
+
</td>
|
|
124
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
125
|
+
<%= job_data["ram_before"] ? "#{job_data["ram_before"]} MB" : "/" %>
|
|
126
|
+
</td>
|
|
127
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
128
|
+
<%= job_data["ram_after"] ? "#{job_data["ram_after"]} MB" : "/" %>
|
|
129
|
+
</td>
|
|
130
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
131
|
+
<%= job_data["execution_time"] ? "#{job_data["execution_time"]} s" : "/" %>
|
|
132
|
+
</td>
|
|
133
|
+
<td class="px-6 py-4 text-gray-700">
|
|
134
|
+
<div>
|
|
135
|
+
<code class="bg-gray-100 px-1 py-0.5 rounded text-xs"><%= job_data["worker_class"] %></code>
|
|
136
|
+
<% if job_data["job_args"].present? %>
|
|
137
|
+
<div class="text-xs text-gray-500 mt-1 font-mono truncate" title="<%= job_data["job_args"] %>">
|
|
138
|
+
<%= job_data["job_args"] %>
|
|
139
|
+
</div>
|
|
140
|
+
<% end %>
|
|
141
|
+
</div>
|
|
142
|
+
</td>
|
|
143
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
144
|
+
<%= job_data["queue"] || "/" %>
|
|
145
|
+
</td>
|
|
146
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
147
|
+
<%= job_data["dyno"] || "/" %>
|
|
148
|
+
</td>
|
|
149
|
+
<td class="px-6 py-4 text-sm text-gray-500 font-mono">
|
|
150
|
+
<%= job_data["job_id"] ? job_data["job_id"][0..7] : "/" %>
|
|
151
|
+
</td>
|
|
152
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
153
|
+
<%= Time.parse(job_data["recorded_at"]).utc.strftime("%Y-%m-%d %H:%M") %>
|
|
154
|
+
</td>
|
|
155
|
+
</tr>
|
|
156
|
+
<% end %>
|
|
157
|
+
</tbody>
|
|
158
|
+
</table>
|
|
159
|
+
</div>
|
|
160
|
+
<% else %>
|
|
161
|
+
<div class="bg-white rounded-lg shadow px-6 py-4 text-center">
|
|
162
|
+
<h3 class="text-lg font-medium text-gray-900 mb-2">No Jobs Tracked Yet</h3>
|
|
163
|
+
<p class="text-gray-600">No background jobs have been recorded yet.</p>
|
|
164
|
+
<p class="text-sm text-gray-500 mt-2">Memory tracking will begin once background jobs start running with the JobTrackingMiddleware enabled.</p>
|
|
165
|
+
</div>
|
|
166
|
+
<% end %>
|
|
@@ -6,177 +6,21 @@
|
|
|
6
6
|
<p class="text-sm text-green-700">Memory profiling is active and tracking requests. To disable, set <code class="bg-green-100 px-1 rounded">ENV["MEM_HEALTH_ENABLED"]=false</code></p>
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
|
-
<!--
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<dt class="text-sm font-medium text-gray-500 mb-1">URL</dt>
|
|
18
|
-
<dd class="text-sm text-gray-900 font-mono bg-gray-100 px-2 py-1 rounded ml-0 flex items-center gap-2" title="<%= @max_memory_url["url"] %>">
|
|
19
|
-
<% if @max_memory_url["request_method"] %>
|
|
20
|
-
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded flex-shrink-0 <%= @max_memory_url["request_method"] == 'GET' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' %>">
|
|
21
|
-
<%= @max_memory_url["request_method"] %>
|
|
22
|
-
</span>
|
|
23
|
-
<% end %>
|
|
24
|
-
<span class="truncate"><%= @max_memory_url["url"] %></span>
|
|
25
|
-
</dd>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<!-- Single row with all metadata -->
|
|
29
|
-
<div class="flex gap-4">
|
|
30
|
-
<div class="flex-1 text-left">
|
|
31
|
-
<div class="bg-red-100 border border-red-300 rounded-md px-4 py-3">
|
|
32
|
-
<dt class="text-sm font-medium text-red-700 mb-1">Memory Diff</dt>
|
|
33
|
-
<dd class="text-lg font-bold text-red-800 ml-0">
|
|
34
|
-
<%= @max_memory_url["memory_diff"] %> MB
|
|
35
|
-
</dd>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<div class="flex-1 text-left">
|
|
40
|
-
<dt class="text-sm font-medium text-gray-500 mb-1">RAM Usage</dt>
|
|
41
|
-
<dd class="text-sm text-gray-600 ml-0">
|
|
42
|
-
<%= @max_memory_url["ram_before"] %> MB → <%= @max_memory_url["ram_after"] %> MB
|
|
43
|
-
</dd>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<div class="flex-1 text-left">
|
|
47
|
-
<dt class="text-sm font-medium text-gray-500 mb-1">Execution Time</dt>
|
|
48
|
-
<dd class="text-sm text-gray-600 ml-0">
|
|
49
|
-
<%= @max_memory_url["execution_time"] ? "#{@max_memory_url["execution_time"]} s" : "-" %>
|
|
50
|
-
</dd>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<div class="flex-1 text-left">
|
|
54
|
-
<dt class="text-sm font-medium text-gray-500 mb-1">Info</dt>
|
|
55
|
-
<dd class="text-sm text-gray-600 ml-0">
|
|
56
|
-
<% info_parts = [] %>
|
|
57
|
-
<% info_parts << @max_memory_url["dyno"] if @max_memory_url["dyno"] %>
|
|
58
|
-
<% info_parts << "T#{@max_memory_url["puma_thread_index"]}" if @max_memory_url["puma_thread_index"] %>
|
|
59
|
-
<% info_parts << "##{@max_memory_url["account_id"]}" if @max_memory_url["account_id"].present? %>
|
|
60
|
-
<% info_parts << Time.parse(@max_memory_url["recorded_at"]).utc.strftime("%m/%d %H:%M") %>
|
|
61
|
-
<%= info_parts.join(" / ") %>
|
|
62
|
-
</dd>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
<% end %>
|
|
68
|
-
|
|
69
|
-
<!-- Stats summary card -->
|
|
70
|
-
<div class="bg-white rounded-lg shadow px-6 py-6 mb-6">
|
|
71
|
-
<h3 class="text-lg font-medium text-gray-900 mb-4">Memory Usage Statistics</h3>
|
|
72
|
-
|
|
73
|
-
<div class="flex gap-4">
|
|
74
|
-
<div class="bg-blue-50 p-4 rounded-md flex-1 text-left">
|
|
75
|
-
<dt class="text-sm font-medium text-blue-600">Max Memory Difference</dt>
|
|
76
|
-
<dd class="mt-1 text-2xl font-semibold text-blue-900 text-left ml-0"><%= @stats[:max_memory_diff] %> MB</dd>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
<div class="bg-green-50 p-4 rounded-md flex-1 text-left">
|
|
80
|
-
<dt class="text-sm font-medium text-green-600">Stored URLs</dt>
|
|
81
|
-
<dd class="mt-1 text-2xl font-semibold text-green-900 text-left ml-0"><%= @stats[:stored_urls_count] %>/<%= @stats[:max_stored_urls] %></dd>
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
<div class="bg-yellow-50 p-4 rounded-md flex-1 text-left">
|
|
85
|
-
<dt class="text-sm font-medium text-yellow-600">Requests Tracked</dt>
|
|
86
|
-
<dd class="mt-1 text-2xl font-semibold text-yellow-900 text-left ml-0"><%= @stats[:tracked_requests_count] %></dd>
|
|
87
|
-
</div>
|
|
88
|
-
|
|
89
|
-
<div class="bg-purple-50 p-4 rounded-md flex-1 text-left">
|
|
90
|
-
<dt class="text-sm font-medium text-purple-600">Total Requests on current dyno/Rails instance</dt>
|
|
91
|
-
<dd class="mt-1 text-2xl font-semibold text-purple-900 text-left ml-0"><%= @stats[:total_requests_count] %></dd>
|
|
92
|
-
<% if @stats[:skipped_requests_count] > 0 %>
|
|
93
|
-
<p class="text-xs text-purple-500 mt-1"><%= @stats[:skipped_requests_count] %> skipped</p>
|
|
94
|
-
<% end %>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
9
|
+
<!-- Tab Navigation -->
|
|
10
|
+
<div class="mb-6">
|
|
11
|
+
<nav class="flex space-x-4 border-b border-gray-200">
|
|
12
|
+
<%= link_to "Web Servers", root_path(view: 'web'),
|
|
13
|
+
class: "px-4 py-2 text-sm font-medium border-b-2 #{@view_type == 'web' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}" %>
|
|
14
|
+
<%= link_to "Worker Servers", root_path(view: 'worker'),
|
|
15
|
+
class: "px-4 py-2 text-sm font-medium border-b-2 #{@view_type == 'worker' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}" %>
|
|
16
|
+
</nav>
|
|
97
17
|
</div>
|
|
98
18
|
|
|
99
|
-
<!--
|
|
100
|
-
<% if @
|
|
101
|
-
|
|
102
|
-
<div class="px-6 py-4 border-b border-gray-200">
|
|
103
|
-
<h3 class="text-lg font-medium text-gray-900">Top <%= MemHealth.configuration.max_stored_urls %> URLs by Memory Usage</h3>
|
|
104
|
-
<p class="text-sm text-gray-600 mt-1">URLs ordered by memory difference (highest first) - maximum <%= MemHealth.configuration.max_stored_urls %> URLs stored</p>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<table class="min-w-full divide-y divide-gray-200">
|
|
108
|
-
<thead class="bg-gray-50">
|
|
109
|
-
<tr>
|
|
110
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory Diff</th>
|
|
111
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM Before</th>
|
|
112
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM After</th>
|
|
113
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Exec Time</th>
|
|
114
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
|
|
115
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dyno/Thread</th>
|
|
116
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account ID</th>
|
|
117
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Recorded At</th>
|
|
118
|
-
</tr>
|
|
119
|
-
</thead>
|
|
120
|
-
|
|
121
|
-
<tbody class="bg-white divide-y divide-gray-200">
|
|
122
|
-
<% @top_urls.each do |url_data| %>
|
|
123
|
-
<tr>
|
|
124
|
-
<td class="px-6 py-4 whitespace-nowrap">
|
|
125
|
-
<%
|
|
126
|
-
memory_color = if url_data["memory_diff"] > 10
|
|
127
|
-
"bg-red-100 text-red-800"
|
|
128
|
-
elsif url_data["memory_diff"] > 5
|
|
129
|
-
"bg-yellow-100 text-yellow-800"
|
|
130
|
-
else
|
|
131
|
-
"bg-green-100 text-green-800"
|
|
132
|
-
end
|
|
133
|
-
%>
|
|
134
|
-
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full <%= memory_color %>">
|
|
135
|
-
<%= url_data["memory_diff"] %> MB
|
|
136
|
-
</span>
|
|
137
|
-
</td>
|
|
138
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
139
|
-
<%= url_data["ram_before"] ? "#{url_data["ram_before"]} MB" : "/" %>
|
|
140
|
-
</td>
|
|
141
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
142
|
-
<%= url_data["ram_after"] ? "#{url_data["ram_after"]} MB" : "/" %>
|
|
143
|
-
</td>
|
|
144
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
145
|
-
<%= url_data["execution_time"] ? "#{url_data["execution_time"]} s" : "/" %>
|
|
146
|
-
</td>
|
|
147
|
-
<td class="px-6 py-4 text-gray-700 url-cell">
|
|
148
|
-
<div title="<%= url_data["url"] %>" style="font-size: 0.75rem; line-height: 1rem;">
|
|
149
|
-
<% if url_data["request_method"] %>
|
|
150
|
-
<span class="inline-flex px-1 py-0.5 font-semibold rounded <%= url_data["request_method"] == 'GET' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' %>" style="font-size: 0.65rem;">
|
|
151
|
-
<%= url_data["request_method"] %>
|
|
152
|
-
</span>
|
|
153
|
-
<% end %>
|
|
154
|
-
<code class="bg-gray-100 px-1 py-0.5 rounded"><%= url_data["url"] %></code>
|
|
155
|
-
</div>
|
|
156
|
-
</td>
|
|
157
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
158
|
-
<% dyno_thread = [] %>
|
|
159
|
-
<% dyno_thread << url_data["dyno"] if url_data["dyno"] %>
|
|
160
|
-
<% dyno_thread << "T#{url_data["puma_thread_index"]}" if url_data["puma_thread_index"] %>
|
|
161
|
-
<%= dyno_thread.any? ? dyno_thread.join("/") : "/" %>
|
|
162
|
-
</td>
|
|
163
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
164
|
-
<%= url_data["account_id"].present? ? url_data["account_id"] : "/" %>
|
|
165
|
-
</td>
|
|
166
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
167
|
-
<%= Time.parse(url_data["recorded_at"]).utc.strftime("%Y-%m-%d %H:%M") %>
|
|
168
|
-
</td>
|
|
169
|
-
</tr>
|
|
170
|
-
<% end %>
|
|
171
|
-
</tbody>
|
|
172
|
-
</table>
|
|
173
|
-
</div>
|
|
19
|
+
<!-- View Content -->
|
|
20
|
+
<% if @view_type == 'worker' %>
|
|
21
|
+
<%= render 'worker_view' %>
|
|
174
22
|
<% else %>
|
|
175
|
-
|
|
176
|
-
<h3 class="text-lg font-medium text-gray-900 mb-2">No URLs Tracked Yet</h3>
|
|
177
|
-
<p class="text-gray-600">No URLs have been recorded yet.</p>
|
|
178
|
-
<p class="text-sm text-gray-500 mt-2">Memory tracking will begin after the first <%= MemHealth.configuration.skip_requests %> requests to your application.</p>
|
|
179
|
-
</div>
|
|
23
|
+
<%= render 'web_view' %>
|
|
180
24
|
<% end %>
|
|
181
25
|
|
|
182
26
|
<!-- Action buttons -->
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module MemHealth
|
|
2
|
+
class JobTrackingMiddleware
|
|
3
|
+
include TrackingConcern
|
|
4
|
+
|
|
5
|
+
def call(worker, job, queue, &block)
|
|
6
|
+
return yield unless config.enabled?
|
|
7
|
+
|
|
8
|
+
metrics = measure_memory(&block)
|
|
9
|
+
|
|
10
|
+
# Track if memory usage is significant
|
|
11
|
+
unless metrics[:memory_diff] > config.memory_threshold_mb && metrics[:before] > config.ram_before_threshold_mb
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
related_model = extract_related_model(job)
|
|
16
|
+
job_class = extract_job_class(worker, job)
|
|
17
|
+
|
|
18
|
+
metadata = {
|
|
19
|
+
worker_class: related_model ? "#{job_class} (#{related_model})" : job_class,
|
|
20
|
+
queue: queue,
|
|
21
|
+
dyno: ENV['DYNO'],
|
|
22
|
+
job_id: job['jid'],
|
|
23
|
+
job_args: extract_job_args(job)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
track_memory_usage(metrics[:memory_diff], metrics[:before], metrics[:after],
|
|
27
|
+
metadata.merge(execution_time: metrics[:execution_time]), type: :worker)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def extract_job_class(worker, job)
|
|
33
|
+
# For ActiveJob jobs, extract the actual job class from the wrapper
|
|
34
|
+
if worker.class.name == 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper'
|
|
35
|
+
job['wrapped'] || job['args']&.first&.dig('job_class') || worker.class.name
|
|
36
|
+
else
|
|
37
|
+
worker.class.name
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_job_args(job)
|
|
42
|
+
# Extract arguments, limiting size to avoid storing huge data
|
|
43
|
+
args = if job['wrapped'] # ActiveJob
|
|
44
|
+
job['args']&.first&.dig('arguments')
|
|
45
|
+
else # Native Sidekiq
|
|
46
|
+
job['args']
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
return nil unless args
|
|
50
|
+
|
|
51
|
+
# Convert to string and truncate if too long
|
|
52
|
+
args_string = args.inspect
|
|
53
|
+
args_string.length > 2000 ? "#{args_string[0..1970]}..." : args_string
|
|
54
|
+
rescue StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_related_model(job)
|
|
59
|
+
# Try to find related model from job arguments
|
|
60
|
+
args = if job['wrapped'] # ActiveJob
|
|
61
|
+
job['args']&.first&.dig('arguments')
|
|
62
|
+
else
|
|
63
|
+
job['args']
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
return nil unless args
|
|
67
|
+
|
|
68
|
+
# Look for GlobalID patterns that indicate the actual model being processed
|
|
69
|
+
global_ids = extract_global_ids(args)
|
|
70
|
+
return nil if global_ids.empty?
|
|
71
|
+
|
|
72
|
+
# Return the first non-ActiveJob related model
|
|
73
|
+
global_ids.first
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def extract_global_ids(obj, results = [])
|
|
79
|
+
case obj
|
|
80
|
+
when Hash
|
|
81
|
+
if obj['_aj_globalid']
|
|
82
|
+
# Extract model class from GlobalID (e.g., "gid://app/ModelName/123" -> "ModelName")
|
|
83
|
+
gid = obj['_aj_globalid']
|
|
84
|
+
if gid =~ %r{gid://[^/]+/([^/]+)/}
|
|
85
|
+
model_name = $1
|
|
86
|
+
results << model_name unless results.include?(model_name)
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
obj.each_value { |v| extract_global_ids(v, results) }
|
|
90
|
+
end
|
|
91
|
+
when Array
|
|
92
|
+
obj.each { |v| extract_global_ids(v, results) }
|
|
93
|
+
end
|
|
94
|
+
results
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
require 'get_process_mem'
|
|
2
|
-
|
|
3
1
|
module MemHealth
|
|
4
2
|
class Middleware
|
|
3
|
+
include TrackingConcern
|
|
4
|
+
|
|
5
5
|
@@request_count = 0
|
|
6
6
|
@@skipped_requests_count = 0
|
|
7
7
|
|
|
@@ -16,43 +16,34 @@ module MemHealth
|
|
|
16
16
|
def call(env)
|
|
17
17
|
@@request_count += 1
|
|
18
18
|
|
|
19
|
-
# Track execution time
|
|
20
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
21
|
-
|
|
22
|
-
before = GetProcessMem.new.mb
|
|
23
|
-
status, headers, response = @app.call(env)
|
|
24
|
-
after = GetProcessMem.new.mb
|
|
25
|
-
|
|
26
|
-
# Calculate execution time in seconds
|
|
27
|
-
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
28
|
-
execution_time = (end_time - start_time).round(2)
|
|
29
|
-
|
|
30
|
-
memory_diff = (after - before).round(2)
|
|
31
19
|
request = Rack::Request.new(env)
|
|
32
|
-
request_url = request.fullpath
|
|
33
|
-
account_info = extract_account_info(env)
|
|
34
20
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
dyno: ENV['DYNO'],
|
|
39
|
-
execution_time: execution_time,
|
|
40
|
-
request_method: request.request_method
|
|
41
|
-
}
|
|
21
|
+
metrics = measure_memory do
|
|
22
|
+
@status, @headers, @response = @app.call(env)
|
|
23
|
+
end
|
|
42
24
|
|
|
43
25
|
# Skip the first few requests as they have large memory jumps due to class loading
|
|
44
26
|
if @@request_count > config.skip_requests
|
|
45
27
|
redis.incr(redis_tracked_requests_key)
|
|
46
|
-
should_track = memory_diff > config.memory_threshold_mb &&
|
|
28
|
+
should_track = metrics[:memory_diff] > config.memory_threshold_mb &&
|
|
29
|
+
metrics[:before] > config.ram_before_threshold_mb
|
|
30
|
+
|
|
47
31
|
if should_track
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
metadata = {
|
|
33
|
+
url: request.fullpath,
|
|
34
|
+
puma_thread_index: Thread.current[:puma_thread_index],
|
|
35
|
+
dyno: ENV['DYNO'],
|
|
36
|
+
request_method: request.request_method
|
|
37
|
+
}.merge(extract_account_info(env))
|
|
38
|
+
|
|
39
|
+
track_memory_usage(metrics[:memory_diff], metrics[:before], metrics[:after],
|
|
40
|
+
metadata.merge(execution_time: metrics[:execution_time]), type: :web)
|
|
50
41
|
end
|
|
51
42
|
else
|
|
52
43
|
@@skipped_requests_count += 1
|
|
53
44
|
end
|
|
54
45
|
|
|
55
|
-
[status, headers, response]
|
|
46
|
+
[@status, @headers, @response]
|
|
56
47
|
end
|
|
57
48
|
|
|
58
49
|
def self.reset_data
|
|
@@ -75,70 +66,10 @@ module MemHealth
|
|
|
75
66
|
|
|
76
67
|
private
|
|
77
68
|
|
|
78
|
-
def config
|
|
79
|
-
MemHealth.configuration
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def redis
|
|
83
|
-
config.redis
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def redis_max_diff_key
|
|
87
|
-
"#{config.redis_key_prefix}:max_diff"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def redis_max_diff_url_key
|
|
91
|
-
"#{config.redis_key_prefix}:max_diff_url"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def redis_high_usage_urls_key
|
|
95
|
-
"#{config.redis_key_prefix}:high_usage_urls"
|
|
96
|
-
end
|
|
97
|
-
|
|
98
69
|
def redis_tracked_requests_key
|
|
99
70
|
self.class.redis_tracked_requests_key
|
|
100
71
|
end
|
|
101
72
|
|
|
102
|
-
def track_memory_usage(memory_diff, request_url, ram_before, ram_after, request_metadata = {})
|
|
103
|
-
# Update max memory diff seen so far
|
|
104
|
-
current_max = redis.get(redis_max_diff_key)&.to_f || 0.0
|
|
105
|
-
if memory_diff > current_max
|
|
106
|
-
redis.set(redis_max_diff_key, memory_diff)
|
|
107
|
-
|
|
108
|
-
# Store the URL data that caused the maximum memory usage
|
|
109
|
-
max_url_data = {
|
|
110
|
-
url: request_url,
|
|
111
|
-
memory_diff: memory_diff,
|
|
112
|
-
ram_before: ram_before,
|
|
113
|
-
ram_after: ram_after,
|
|
114
|
-
timestamp: Time.current.to_i,
|
|
115
|
-
recorded_at: Time.current.iso8601
|
|
116
|
-
}.merge(request_metadata)
|
|
117
|
-
redis.set(redis_max_diff_url_key, max_url_data.to_json)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Store all URLs
|
|
121
|
-
timestamp = Time.current.to_i
|
|
122
|
-
url_data = {
|
|
123
|
-
url: request_url,
|
|
124
|
-
memory_diff: memory_diff,
|
|
125
|
-
ram_before: ram_before,
|
|
126
|
-
ram_after: ram_after,
|
|
127
|
-
timestamp: timestamp,
|
|
128
|
-
recorded_at: Time.current.iso8601
|
|
129
|
-
}.merge(request_metadata)
|
|
130
|
-
|
|
131
|
-
# Add URL to sorted set (score = memory_diff for DESC ordering)
|
|
132
|
-
redis.zadd(redis_high_usage_urls_key, memory_diff, url_data.to_json)
|
|
133
|
-
|
|
134
|
-
# Keep only top N URLs by removing lowest scores
|
|
135
|
-
current_count = redis.zcard(redis_high_usage_urls_key)
|
|
136
|
-
return unless current_count > config.max_stored_urls
|
|
137
|
-
|
|
138
|
-
# Remove the lowest scoring URLs (keep top max_stored_urls)
|
|
139
|
-
redis.zremrangebyrank(redis_high_usage_urls_key, 0, current_count - config.max_stored_urls - 1)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
73
|
def extract_account_info(env)
|
|
143
74
|
account_info = {}
|
|
144
75
|
|
data/lib/mem_health/tracker.rb
CHANGED
|
@@ -1,41 +1,71 @@
|
|
|
1
1
|
module MemHealth
|
|
2
2
|
class Tracker
|
|
3
3
|
class << self
|
|
4
|
-
#
|
|
5
|
-
def max_memory_diff
|
|
6
|
-
|
|
4
|
+
# Generic methods for web and worker tracking
|
|
5
|
+
def max_memory_diff(type: :web)
|
|
6
|
+
key = type == :worker ? redis_max_diff_worker_key : redis_max_diff_key
|
|
7
|
+
redis.get(key)&.to_f || 0.0
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
redis.zrevrange(
|
|
10
|
+
def top_memory_items(type: :web, limit: config.max_stored_urls)
|
|
11
|
+
key = type == :worker ? redis_high_usage_jobs_key : redis_high_usage_urls_key
|
|
12
|
+
redis.zrevrange(key, 0, limit - 1, with_scores: true).map do |json_data, score|
|
|
12
13
|
data = JSON.parse(json_data)
|
|
13
14
|
data.merge("memory_diff" => score)
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# Get URLs that used more than a specific threshold
|
|
21
|
-
def urls_above_threshold(threshold_mb)
|
|
22
|
-
redis.zrangebyscore(redis_high_usage_urls_key, threshold_mb, "+inf", with_scores: true).map do |json_data, score|
|
|
18
|
+
def items_above_threshold(threshold_mb, type: :web)
|
|
19
|
+
key = type == :worker ? redis_high_usage_jobs_key : redis_high_usage_urls_key
|
|
20
|
+
redis.zrangebyscore(key, threshold_mb, "+inf", with_scores: true).map do |json_data, score|
|
|
23
21
|
data = JSON.parse(json_data)
|
|
24
22
|
data.merge("memory_diff" => score)
|
|
25
23
|
end
|
|
26
24
|
end
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
json_data = redis.get(
|
|
26
|
+
def max_memory_item(type: :web)
|
|
27
|
+
key = type == :worker ? redis_max_diff_job_key : redis_max_diff_url_key
|
|
28
|
+
json_data = redis.get(key)
|
|
31
29
|
return nil if json_data.nil?
|
|
32
30
|
|
|
33
31
|
JSON.parse(json_data)
|
|
34
32
|
end
|
|
35
33
|
|
|
34
|
+
# Convenience methods with backward compatibility
|
|
35
|
+
def max_memory_diff_worker
|
|
36
|
+
max_memory_diff(type: :worker)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def top_memory_urls(limit: config.max_stored_urls)
|
|
40
|
+
top_memory_items(type: :web, limit: limit)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
alias_method :high_usage_urls, :top_memory_urls
|
|
44
|
+
|
|
45
|
+
def top_memory_jobs(limit: config.max_stored_urls)
|
|
46
|
+
top_memory_items(type: :worker, limit: limit)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def urls_above_threshold(threshold_mb)
|
|
50
|
+
items_above_threshold(threshold_mb, type: :web)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def jobs_above_threshold(threshold_mb)
|
|
54
|
+
items_above_threshold(threshold_mb, type: :worker)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def max_memory_url
|
|
58
|
+
max_memory_item(type: :web)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def max_memory_job
|
|
62
|
+
max_memory_item(type: :worker)
|
|
63
|
+
end
|
|
64
|
+
|
|
36
65
|
# Clear all memory tracking data
|
|
37
66
|
def clear_all_data
|
|
38
|
-
redis.del(redis_max_diff_key, redis_max_diff_url_key, redis_high_usage_urls_key
|
|
67
|
+
redis.del(redis_max_diff_key, redis_max_diff_url_key, redis_high_usage_urls_key,
|
|
68
|
+
redis_max_diff_worker_key, redis_max_diff_job_key, redis_high_usage_jobs_key)
|
|
39
69
|
MemHealth::Middleware.reset_data
|
|
40
70
|
end
|
|
41
71
|
|
|
@@ -58,6 +88,18 @@ module MemHealth
|
|
|
58
88
|
}
|
|
59
89
|
end
|
|
60
90
|
|
|
91
|
+
# Get worker stats summary
|
|
92
|
+
def worker_stats
|
|
93
|
+
max_diff = max_memory_diff_worker
|
|
94
|
+
stored_jobs_count = redis.zcard(redis_high_usage_jobs_key)
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
max_memory_diff: max_diff,
|
|
98
|
+
stored_jobs_count: stored_jobs_count,
|
|
99
|
+
max_stored_jobs: config.max_stored_urls
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
61
103
|
# Pretty print stats for console use
|
|
62
104
|
def print_stats
|
|
63
105
|
stats_data = stats
|
|
@@ -99,6 +141,18 @@ module MemHealth
|
|
|
99
141
|
def redis_high_usage_urls_key
|
|
100
142
|
"#{config.redis_key_prefix}:high_usage_urls"
|
|
101
143
|
end
|
|
144
|
+
|
|
145
|
+
def redis_max_diff_worker_key
|
|
146
|
+
"#{config.redis_key_prefix}:worker:max_diff"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def redis_max_diff_job_key
|
|
150
|
+
"#{config.redis_key_prefix}:worker:max_diff_job"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def redis_high_usage_jobs_key
|
|
154
|
+
"#{config.redis_key_prefix}:worker:high_usage_jobs"
|
|
155
|
+
end
|
|
102
156
|
end
|
|
103
157
|
end
|
|
104
158
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require 'get_process_mem'
|
|
2
|
+
|
|
3
|
+
module MemHealth
|
|
4
|
+
module TrackingConcern
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def config
|
|
8
|
+
MemHealth.configuration
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def redis
|
|
12
|
+
config.redis
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def track_memory_usage(memory_diff, ram_before, ram_after, metadata, type: :web)
|
|
16
|
+
prefix = type == :worker ? 'worker:' : ''
|
|
17
|
+
|
|
18
|
+
# Update max memory diff seen so far
|
|
19
|
+
max_diff_key = "#{config.redis_key_prefix}:#{prefix}max_diff"
|
|
20
|
+
max_item_key = "#{config.redis_key_prefix}:#{prefix}max_diff_#{type == :worker ? 'job' : 'url'}"
|
|
21
|
+
high_usage_key = "#{config.redis_key_prefix}:#{prefix}high_usage_#{type == :worker ? 'jobs' : 'urls'}"
|
|
22
|
+
|
|
23
|
+
current_max = redis.get(max_diff_key)&.to_f || 0.0
|
|
24
|
+
if memory_diff > current_max
|
|
25
|
+
redis.set(max_diff_key, memory_diff)
|
|
26
|
+
|
|
27
|
+
# Store the item data that caused the maximum memory usage
|
|
28
|
+
max_item_data = {
|
|
29
|
+
memory_diff: memory_diff,
|
|
30
|
+
ram_before: ram_before,
|
|
31
|
+
ram_after: ram_after,
|
|
32
|
+
timestamp: Time.current.to_i,
|
|
33
|
+
recorded_at: Time.current.iso8601
|
|
34
|
+
}.merge(metadata)
|
|
35
|
+
redis.set(max_item_key, max_item_data.to_json)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Store all items
|
|
39
|
+
item_data = {
|
|
40
|
+
memory_diff: memory_diff,
|
|
41
|
+
ram_before: ram_before,
|
|
42
|
+
ram_after: ram_after,
|
|
43
|
+
timestamp: Time.current.to_i,
|
|
44
|
+
recorded_at: Time.current.iso8601
|
|
45
|
+
}.merge(metadata)
|
|
46
|
+
|
|
47
|
+
# Add item to sorted set (score = memory_diff for DESC ordering)
|
|
48
|
+
redis.zadd(high_usage_key, memory_diff, item_data.to_json)
|
|
49
|
+
|
|
50
|
+
# Keep only top N items by removing lowest scores
|
|
51
|
+
current_count = redis.zcard(high_usage_key)
|
|
52
|
+
return unless current_count > config.max_stored_urls
|
|
53
|
+
|
|
54
|
+
# Remove the lowest scoring items (keep top max_stored_urls)
|
|
55
|
+
redis.zremrangebyrank(high_usage_key, 0, current_count - config.max_stored_urls - 1)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def measure_memory
|
|
59
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
60
|
+
before = GetProcessMem.new.mb
|
|
61
|
+
|
|
62
|
+
yield
|
|
63
|
+
|
|
64
|
+
after = GetProcessMem.new.mb
|
|
65
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
66
|
+
execution_time = (end_time - start_time).round(2)
|
|
67
|
+
memory_diff = (after - before).round(2)
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
before: before.round(2),
|
|
71
|
+
after: after.round(2),
|
|
72
|
+
memory_diff: memory_diff,
|
|
73
|
+
execution_time: execution_time
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/mem_health/version.rb
CHANGED
data/lib/memhealth.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: memhealth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Klemen Nagode
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -78,12 +78,16 @@ files:
|
|
|
78
78
|
- README.md
|
|
79
79
|
- app/controllers/mem_health/dashboard_controller.rb
|
|
80
80
|
- app/views/layouts/memhealth/application.html.erb
|
|
81
|
+
- app/views/mem_health/dashboard/_web_view.html.erb
|
|
82
|
+
- app/views/mem_health/dashboard/_worker_view.html.erb
|
|
81
83
|
- app/views/mem_health/dashboard/index.html.erb
|
|
82
84
|
- config/routes.rb
|
|
83
85
|
- lib/mem_health/configuration.rb
|
|
84
86
|
- lib/mem_health/engine.rb
|
|
87
|
+
- lib/mem_health/job_tracking_middleware.rb
|
|
85
88
|
- lib/mem_health/middleware.rb
|
|
86
89
|
- lib/mem_health/tracker.rb
|
|
90
|
+
- lib/mem_health/tracking_concern.rb
|
|
87
91
|
- lib/mem_health/version.rb
|
|
88
92
|
- lib/memhealth.rb
|
|
89
93
|
homepage: https://github.com/topkeyhq/memhealth
|