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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69e8587a6c8a55a3a4e5b50ed6b433c4557f067e867cc9c30520bac3f184a48c
4
- data.tar.gz: d7c228db15571b67db8a59adc334aa5611717a61939662bc5abde5464cdfb3e4
3
+ metadata.gz: 5fda50a609f167feffba18ed772f70c528fe8ae883605520a8b51283ea9e45af
4
+ data.tar.gz: 30bb76d54747594b6fe1b2b9432759a53f201b2ed8b6e5852a78fe7775f654a1
5
5
  SHA512:
6
- metadata.gz: a82811c70c3ee43ed58dce8f0e8a169d9b7008f2a72303c67e0b654b5b7b653e25c05be27542fc9c168846516299566a763851ac3b98c8a0051908cf813e7fda
7
- data.tar.gz: e7f878a2dfffe6c9e4f2ab276c5447889af056be98380e54e94a8783ff099fa8b51a94c63a32986add73eaa3b9417bd6a7c606bbf3d21c967f8fc9ddec641f56
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 viewing statistics
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
- # Clear all data
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
- @stats = MemHealth::Tracker.stats
11
- @top_urls = MemHealth::Tracker.top_memory_urls
12
- @max_memory_url = MemHealth::Tracker.max_memory_url
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
- @top_urls = []
16
- @max_memory_url = nil
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
- <!-- Highest Memory Usage URL - Prominent display -->
10
- <% if @max_memory_url %>
11
- <div class="bg-red-50 border border-red-200 rounded-lg px-6 py-6 mb-6">
12
- <h3 class="text-lg font-semibold text-red-800 mb-4">🚨 Highest Memory Usage URL</h3>
13
-
14
- <div class="bg-white rounded-md px-6 py-4 border border-red-100">
15
- <!-- URL Section -->
16
- <div class="mb-4">
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
- <!-- Top URLs table -->
100
- <% if @top_urls.any? %>
101
- <div class="bg-white rounded-lg shadow overflow-hidden">
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
- <div class="bg-white rounded-lg shadow px-6 py-4 text-center">
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
- # Collect additional metadata
36
- metadata = {
37
- puma_thread_index: Thread.current[:puma_thread_index],
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 && before.round(2) > config.ram_before_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
- track_memory_usage(memory_diff, request_url, before.round(2), after.round(2),
49
- account_info.merge(metadata))
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
 
@@ -1,41 +1,71 @@
1
1
  module MemHealth
2
2
  class Tracker
3
3
  class << self
4
- # Get the maximum memory difference recorded
5
- def max_memory_diff
6
- redis.get(redis_max_diff_key)&.to_f || 0.0
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
- # Get all stored URLs, ordered by memory usage (highest first)
10
- def top_memory_urls(limit: config.max_stored_urls)
11
- redis.zrevrange(redis_high_usage_urls_key, 0, limit - 1, with_scores: true).map do |json_data, score|
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
- # Alias for backward compatibility
18
- alias_method :high_usage_urls, :top_memory_urls
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
- # Get the URL with the highest memory usage (the one that set the max diff)
29
- def max_memory_url
30
- json_data = redis.get(redis_max_diff_url_key)
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
@@ -1,3 +1,3 @@
1
1
  module MemHealth
2
- VERSION = '0.1.1'
2
+ VERSION = '0.1.2'
3
3
  end
data/lib/memhealth.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "mem_health/version"
2
2
  require "mem_health/configuration"
3
+ require "mem_health/tracking_concern"
3
4
  require "mem_health/middleware"
5
+ require "mem_health/job_tracking_middleware"
4
6
  require "mem_health/tracker"
5
7
  require "mem_health/engine"
6
8
 
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.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-09-15 00:00:00.000000000 Z
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