memhealth 0.1.0 → 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 +71 -46
- data/app/controllers/mem_health/dashboard_controller.rb +13 -6
- data/app/views/layouts/memhealth/application.html.erb +12 -1
- 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 -142
- data/lib/mem_health/job_tracking_middleware.rb +97 -0
- data/lib/mem_health/middleware.rb +25 -83
- 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 +10 -3
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
|
@@ -2,61 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
A Rails engine for monitoring memory health, detecting growth patterns (leaks & bloats) and memory swap operations. Helps you identify requests that consume high amounts of RAM and is compatible with Heroku.
|
|
4
4
|
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- Real-time memory usage monitoring
|
|
8
|
-
- Track highest memory consuming requests
|
|
9
|
-
- Account-level tracking for multi-tenant apps
|
|
10
|
-
- Redis-based data storage
|
|
11
|
-
- Web dashboard for viewing statistics
|
|
12
|
-
- Configurable thresholds and limits
|
|
13
|
-
|
|
14
|
-
<img width="1139" height="680" alt="s_2" src="https://github.com/user-attachments/assets/5e170097-77cf-4ec5-a7b0-47aeaf92135f" />
|
|
15
|
-
|
|
16
|
-
<img width="1142" height="696" alt="s_1" src="https://github.com/user-attachments/assets/68eeb503-e259-4dc0-b3b1-3438375b42d4" />
|
|
17
|
-
|
|
18
|
-
## Heroku Memory Issues
|
|
19
|
-
|
|
20
|
-
If you're getting **R14 - Memory quota exceeded** errors, it means your application is using swap memory. Swap uses the disk to store memory instead of RAM. Disk speed is significantly slower than RAM, so page access time is greatly increased. This leads to a significant degradation in application performance. An application that is swapping will be much slower than one that is not. No one wants a slow application, so getting rid of R14 Memory quota exceeded errors on your application is very important.
|
|
21
|
-
|
|
22
|
-
<img width="909" height="272" alt="Screenshot 2025-08-30 at 18 46 41" src="https://github.com/user-attachments/assets/65bb2131-4dfd-4974-9647-805e44bc35f7" />
|
|
23
|
-
|
|
24
|
-
MemHealth helps you identify which specific requests are consuming excessive memory, allowing you to pinpoint and fix the root cause of these performance issues.
|
|
25
|
-
|
|
26
5
|
## Installation
|
|
27
6
|
|
|
28
7
|
Add this line to your application's Gemfile:
|
|
29
8
|
|
|
30
9
|
```ruby
|
|
31
|
-
gem "memhealth"
|
|
10
|
+
gem "memhealth"
|
|
32
11
|
```
|
|
33
12
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
$ bundle install
|
|
37
|
-
|
|
38
|
-
## Configuration
|
|
13
|
+
Use initializer `/config/memhealth.rb` to configure Redis connection:
|
|
39
14
|
|
|
40
15
|
```ruby
|
|
41
16
|
MemHealth.configure do |config|
|
|
42
|
-
config.redis_url = ENV.fetch(
|
|
17
|
+
config.redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
|
|
43
18
|
end
|
|
44
19
|
```
|
|
45
20
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
| Environment Variable | Description | Default Value |
|
|
49
|
-
| --------------------------------- | ------------------------------------------------------------------------------ | ------------- |
|
|
50
|
-
| `MEM_HEALTH_ENABLED` | Enable/disable memory tracking | `false` |
|
|
51
|
-
| `MEM_HEALTH_SKIP_REQUESTS` | Number of initial requests to skip (avoids class loading overhead) | `10` |
|
|
52
|
-
| `MEM_HEALTH_MEMORY_THRESHOLD_MB` | Minimum memory difference (MB) to track a request | `1` |
|
|
53
|
-
| `MEM_HEALTH_RAM_BEFORE_THRESHOLD` | Minimum RAM usage (MB) before tracking (prevents tracking low-memory requests) | `0` |
|
|
54
|
-
| `MEM_HEALTH_MAX_STORED_URLS` | Maximum number of URLs to store in Redis | `20` |
|
|
55
|
-
| `MEM_HEALTH_REDIS_KEY` | Name of environment variable containing Redis URL (e.g., `REDISCLOUD_URL`) | `REDIS_URL` |
|
|
21
|
+
Enable memory tracking via ENV variable:
|
|
56
22
|
|
|
57
|
-
|
|
23
|
+
```bash
|
|
24
|
+
MEM_HEALTH_ENABLED=true
|
|
25
|
+
```
|
|
58
26
|
|
|
59
|
-
|
|
27
|
+
### Dashboard setup
|
|
60
28
|
|
|
61
29
|
Mount the engine in your routes within an authenticated section:
|
|
62
30
|
|
|
@@ -72,12 +40,59 @@ Rails.application.routes.draw do
|
|
|
72
40
|
end
|
|
73
41
|
```
|
|
74
42
|
|
|
75
|
-
|
|
43
|
+
### Worker tracking setup (Sidekiq)
|
|
76
44
|
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
79
54
|
```
|
|
80
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
|
+
|
|
60
|
+
# Features
|
|
61
|
+
|
|
62
|
+
- Real-time memory usage monitoring for web and worker servers
|
|
63
|
+
- Track highest memory consuming requests and background jobs
|
|
64
|
+
- Account-level tracking for multi-tenant apps
|
|
65
|
+
- Redis-based data storage
|
|
66
|
+
- Web dashboard with separate views for web and worker metrics
|
|
67
|
+
- Configurable thresholds and limits
|
|
68
|
+
|
|
69
|
+
<img width="1139" height="680" alt="s_2" src="https://github.com/user-attachments/assets/5e170097-77cf-4ec5-a7b0-47aeaf92135f" />
|
|
70
|
+
|
|
71
|
+
<img width="1142" height="696" alt="s_1" src="https://github.com/user-attachments/assets/68eeb503-e259-4dc0-b3b1-3438375b42d4" />
|
|
72
|
+
|
|
73
|
+
## Heroku Memory Issues
|
|
74
|
+
|
|
75
|
+
If you're getting **R14 - Memory quota exceeded** errors, it means your application is using swap memory. Swap uses the disk to store memory instead of RAM. Disk speed is significantly slower than RAM, so page access time is greatly increased. This leads to a significant degradation in application performance. An application that is swapping will be much slower than one that is not. No one wants a slow application, so getting rid of R14 Memory quota exceeded errors on your application is very important.
|
|
76
|
+
|
|
77
|
+
<img width="909" height="272" alt="Screenshot 2025-08-30 at 18 46 41" src="https://github.com/user-attachments/assets/65bb2131-4dfd-4974-9647-805e44bc35f7" />
|
|
78
|
+
|
|
79
|
+
MemHealth helps you identify which specific requests are consuming excessive memory, allowing you to pinpoint and fix the root cause of these performance issues.
|
|
80
|
+
|
|
81
|
+
## ENVIRONMENT VARIABLES
|
|
82
|
+
|
|
83
|
+
Configure Memhealth using environment variables:
|
|
84
|
+
|
|
85
|
+
| Environment Variable | Description | Default Value |
|
|
86
|
+
| --------------------------------- | ------------------------------------------------------------------------------ | ------------- |
|
|
87
|
+
| `MEM_HEALTH_ENABLED` | Enable/disable memory tracking | `false` |
|
|
88
|
+
| `MEM_HEALTH_SKIP_REQUESTS` | Number of initial requests to skip (avoids class loading overhead) | `10` |
|
|
89
|
+
| `MEM_HEALTH_MEMORY_THRESHOLD_MB` | Minimum memory difference (MB) to track a request | `1` |
|
|
90
|
+
| `MEM_HEALTH_RAM_BEFORE_THRESHOLD` | Minimum RAM usage (MB) before tracking (prevents tracking low-memory requests) | `0` |
|
|
91
|
+
| `MEM_HEALTH_MAX_STORED_URLS` | Maximum number of URLs to store in Redis | `20` |
|
|
92
|
+
| `MEM_HEALTH_REDIS_KEY` | Name of environment variable containing Redis URL (e.g., `REDISCLOUD_URL`) | `REDIS_URL` |
|
|
93
|
+
|
|
94
|
+
The gem will read the Redis connection URL from the environment variable specified in `MEM_HEALTH_REDIS_KEY`, falling back to `REDIS_URL` if not specified.
|
|
95
|
+
|
|
81
96
|
### ActiveAdmin Integration
|
|
82
97
|
|
|
83
98
|
To add Memhealth to your ActiveAdmin Operations menu, add this to your ActiveAdmin initializer:
|
|
@@ -99,13 +114,23 @@ end
|
|
|
99
114
|
## Console Usage
|
|
100
115
|
|
|
101
116
|
```ruby
|
|
102
|
-
# View statistics
|
|
117
|
+
# View web statistics
|
|
103
118
|
MemHealth::Tracker.print_stats
|
|
104
119
|
|
|
105
|
-
# Get top memory consuming URLs
|
|
120
|
+
# Get top memory consuming URLs (web)
|
|
106
121
|
MemHealth::Tracker.top_memory_urls
|
|
107
122
|
|
|
108
|
-
#
|
|
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)
|
|
109
134
|
MemHealth::Tracker.clear_all_data
|
|
110
135
|
```
|
|
111
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
|
|
|
@@ -129,6 +129,17 @@
|
|
|
129
129
|
|
|
130
130
|
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
131
131
|
.whitespace-nowrap { white-space: nowrap; }
|
|
132
|
+
|
|
133
|
+
/* URL truncation in table */
|
|
134
|
+
td.url-cell {
|
|
135
|
+
max-width: 500px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
td.url-cell > div {
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
text-overflow: ellipsis;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
}
|
|
132
143
|
.text-center { text-align: center; }
|
|
133
144
|
.text-left { text-align: left; }
|
|
134
145
|
.uppercase { text-transform: uppercase; }
|
|
@@ -158,7 +169,7 @@
|
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
/* Container styles */
|
|
161
|
-
.container { max-width:
|
|
172
|
+
.container { max-width: 1600px; margin: 0 auto; }
|
|
162
173
|
|
|
163
174
|
/* Header styles */
|
|
164
175
|
.header {
|
|
@@ -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,151 +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 truncate ml-0">
|
|
19
|
-
<%= @max_memory_url["url"] %>
|
|
20
|
-
</dd>
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<!-- Four column layout using flex -->
|
|
24
|
-
<div class="flex gap-4">
|
|
25
|
-
<% if @max_memory_url["ram_before"] && @max_memory_url["ram_after"] %>
|
|
26
|
-
<div class="flex-1 text-left">
|
|
27
|
-
<dt class="text-sm font-medium text-gray-500 mb-1">RAM Usage</dt>
|
|
28
|
-
<dd class="text-sm text-gray-600 ml-0">
|
|
29
|
-
<%= @max_memory_url["ram_before"] %> MB → <%= @max_memory_url["ram_after"] %> MB
|
|
30
|
-
</dd>
|
|
31
|
-
</div>
|
|
32
|
-
<% end %>
|
|
33
|
-
|
|
34
|
-
<div class="flex-1 text-left">
|
|
35
|
-
<dt class="text-sm font-medium text-gray-500 mb-1">Recorded At</dt>
|
|
36
|
-
<dd class="text-sm text-gray-600 ml-0"><%= Time.parse(@max_memory_url["recorded_at"]).utc.strftime("%Y-%m-%d %H:%M") %></dd>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<% if @max_memory_url["account_id"].present? %>
|
|
40
|
-
<div class="flex-1 text-left">
|
|
41
|
-
<dt class="text-sm font-medium text-gray-500 mb-1">Account ID</dt>
|
|
42
|
-
<dd class="text-sm text-gray-600 ml-0"><%= @max_memory_url["account_id"] %></dd>
|
|
43
|
-
</div>
|
|
44
|
-
<% end %>
|
|
45
|
-
|
|
46
|
-
<div class="flex-1 text-left">
|
|
47
|
-
<div class="bg-red-100 border border-red-300 rounded-md px-4 py-3">
|
|
48
|
-
<dt class="text-sm font-medium text-red-700 mb-1">Memory Diff</dt>
|
|
49
|
-
<dd class="text-lg font-bold text-red-800 ml-0">
|
|
50
|
-
<%= @max_memory_url["memory_diff"] %> MB
|
|
51
|
-
</dd>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
<% end %>
|
|
58
|
-
|
|
59
|
-
<!-- Stats summary card -->
|
|
60
|
-
<div class="bg-white rounded-lg shadow px-6 py-6 mb-6">
|
|
61
|
-
<h3 class="text-lg font-medium text-gray-900 mb-4">Memory Usage Statistics</h3>
|
|
62
|
-
|
|
63
|
-
<div class="flex gap-4">
|
|
64
|
-
<div class="bg-blue-50 p-4 rounded-md flex-1 text-left">
|
|
65
|
-
<dt class="text-sm font-medium text-blue-600">Max Memory Difference</dt>
|
|
66
|
-
<dd class="mt-1 text-2xl font-semibold text-blue-900 text-left ml-0"><%= @stats[:max_memory_diff] %> MB</dd>
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
<div class="bg-green-50 p-4 rounded-md flex-1 text-left">
|
|
70
|
-
<dt class="text-sm font-medium text-green-600">Stored URLs</dt>
|
|
71
|
-
<dd class="mt-1 text-2xl font-semibold text-green-900 text-left ml-0"><%= @stats[:stored_urls_count] %>/<%= @stats[:max_stored_urls] %></dd>
|
|
72
|
-
</div>
|
|
73
|
-
|
|
74
|
-
<div class="bg-yellow-50 p-4 rounded-md flex-1 text-left">
|
|
75
|
-
<dt class="text-sm font-medium text-yellow-600">Requests Tracked</dt>
|
|
76
|
-
<dd class="mt-1 text-2xl font-semibold text-yellow-900 text-left ml-0"><%= @stats[:tracked_requests_count] %></dd>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
<div class="bg-purple-50 p-4 rounded-md flex-1 text-left">
|
|
80
|
-
<dt class="text-sm font-medium text-purple-600">Total Requests on current dyno/Rails instance</dt>
|
|
81
|
-
<dd class="mt-1 text-2xl font-semibold text-purple-900 text-left ml-0"><%= @stats[:total_requests_count] %></dd>
|
|
82
|
-
<% if @stats[:skipped_requests_count] > 0 %>
|
|
83
|
-
<p class="text-xs text-purple-500 mt-1"><%= @stats[:skipped_requests_count] %> skipped</p>
|
|
84
|
-
<% end %>
|
|
85
|
-
</div>
|
|
86
|
-
</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>
|
|
87
17
|
</div>
|
|
88
18
|
|
|
89
|
-
<!--
|
|
90
|
-
<% if @
|
|
91
|
-
|
|
92
|
-
<div class="px-6 py-4 border-b border-gray-200">
|
|
93
|
-
<h3 class="text-lg font-medium text-gray-900">Top <%= MemHealth.configuration.max_stored_urls %> URLs by Memory Usage</h3>
|
|
94
|
-
<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>
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<table class="min-w-full divide-y divide-gray-200">
|
|
98
|
-
<thead class="bg-gray-50">
|
|
99
|
-
<tr>
|
|
100
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory Diff</th>
|
|
101
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM Before</th>
|
|
102
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">RAM After</th>
|
|
103
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
|
|
104
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account ID</th>
|
|
105
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Recorded At</th>
|
|
106
|
-
</tr>
|
|
107
|
-
</thead>
|
|
108
|
-
|
|
109
|
-
<tbody class="bg-white divide-y divide-gray-200">
|
|
110
|
-
<% @top_urls.each do |url_data| %>
|
|
111
|
-
<tr>
|
|
112
|
-
<td class="px-6 py-4 whitespace-nowrap">
|
|
113
|
-
<%
|
|
114
|
-
memory_color = if url_data["memory_diff"] > 10
|
|
115
|
-
"bg-red-100 text-red-800"
|
|
116
|
-
elsif url_data["memory_diff"] > 5
|
|
117
|
-
"bg-yellow-100 text-yellow-800"
|
|
118
|
-
else
|
|
119
|
-
"bg-green-100 text-green-800"
|
|
120
|
-
end
|
|
121
|
-
%>
|
|
122
|
-
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full <%= memory_color %>">
|
|
123
|
-
<%= url_data["memory_diff"] %> MB
|
|
124
|
-
</span>
|
|
125
|
-
</td>
|
|
126
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
127
|
-
<%= url_data["ram_before"] ? "#{url_data["ram_before"]} MB" : "/" %>
|
|
128
|
-
</td>
|
|
129
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
130
|
-
<%= url_data["ram_after"] ? "#{url_data["ram_after"]} MB" : "/" %>
|
|
131
|
-
</td>
|
|
132
|
-
<td class="px-6 py-4 text-sm text-gray-900">
|
|
133
|
-
<div class="max-w-xs truncate">
|
|
134
|
-
<code class="bg-gray-100 px-2 py-1 rounded text-xs"><%= url_data["url"] %></code>
|
|
135
|
-
</div>
|
|
136
|
-
</td>
|
|
137
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
138
|
-
<%= url_data["account_id"].present? ? url_data["account_id"] : "/" %>
|
|
139
|
-
</td>
|
|
140
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
141
|
-
<%= Time.parse(url_data["recorded_at"]).utc.strftime("%Y-%m-%d %H:%M") %>
|
|
142
|
-
</td>
|
|
143
|
-
</tr>
|
|
144
|
-
<% end %>
|
|
145
|
-
</tbody>
|
|
146
|
-
</table>
|
|
147
|
-
</div>
|
|
19
|
+
<!-- View Content -->
|
|
20
|
+
<% if @view_type == 'worker' %>
|
|
21
|
+
<%= render 'worker_view' %>
|
|
148
22
|
<% else %>
|
|
149
|
-
|
|
150
|
-
<h3 class="text-lg font-medium text-gray-900 mb-2">No URLs Tracked Yet</h3>
|
|
151
|
-
<p class="text-gray-600">No URLs have been recorded yet.</p>
|
|
152
|
-
<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>
|
|
153
|
-
</div>
|
|
23
|
+
<%= render 'web_view' %>
|
|
154
24
|
<% end %>
|
|
155
25
|
|
|
156
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,24 +16,34 @@ module MemHealth
|
|
|
16
16
|
def call(env)
|
|
17
17
|
@@request_count += 1
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
status, headers, response = @app.call(env)
|
|
21
|
-
after = GetProcessMem.new.mb
|
|
19
|
+
request = Rack::Request.new(env)
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
metrics = measure_memory do
|
|
22
|
+
@status, @headers, @response = @app.call(env)
|
|
23
|
+
end
|
|
26
24
|
|
|
27
25
|
# Skip the first few requests as they have large memory jumps due to class loading
|
|
28
26
|
if @@request_count > config.skip_requests
|
|
29
27
|
redis.incr(redis_tracked_requests_key)
|
|
30
|
-
should_track = memory_diff > config.memory_threshold_mb &&
|
|
31
|
-
|
|
28
|
+
should_track = metrics[:memory_diff] > config.memory_threshold_mb &&
|
|
29
|
+
metrics[:before] > config.ram_before_threshold_mb
|
|
30
|
+
|
|
31
|
+
if should_track
|
|
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)
|
|
41
|
+
end
|
|
32
42
|
else
|
|
33
43
|
@@skipped_requests_count += 1
|
|
34
44
|
end
|
|
35
45
|
|
|
36
|
-
[status, headers, response]
|
|
46
|
+
[@status, @headers, @response]
|
|
37
47
|
end
|
|
38
48
|
|
|
39
49
|
def self.reset_data
|
|
@@ -56,78 +66,10 @@ module MemHealth
|
|
|
56
66
|
|
|
57
67
|
private
|
|
58
68
|
|
|
59
|
-
def config
|
|
60
|
-
MemHealth.configuration
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def redis
|
|
64
|
-
config.redis
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def redis_max_diff_key
|
|
68
|
-
"#{config.redis_key_prefix}:max_diff"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def redis_max_diff_url_key
|
|
72
|
-
"#{config.redis_key_prefix}:max_diff_url"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def redis_high_usage_urls_key
|
|
76
|
-
"#{config.redis_key_prefix}:high_usage_urls"
|
|
77
|
-
end
|
|
78
|
-
|
|
79
69
|
def redis_tracked_requests_key
|
|
80
70
|
self.class.redis_tracked_requests_key
|
|
81
71
|
end
|
|
82
72
|
|
|
83
|
-
def track_memory_usage(memory_diff, request_url, ram_before, ram_after, account_info = {})
|
|
84
|
-
# Update max memory diff seen so far
|
|
85
|
-
current_max = redis.get(redis_max_diff_key)&.to_f || 0.0
|
|
86
|
-
if memory_diff > current_max
|
|
87
|
-
redis.set(redis_max_diff_key, memory_diff)
|
|
88
|
-
|
|
89
|
-
# Store the URL data that caused the maximum memory usage
|
|
90
|
-
max_url_data = {
|
|
91
|
-
url: request_url,
|
|
92
|
-
memory_diff: memory_diff,
|
|
93
|
-
ram_before: ram_before,
|
|
94
|
-
ram_after: ram_after,
|
|
95
|
-
timestamp: Time.current.to_i,
|
|
96
|
-
recorded_at: Time.current.iso8601
|
|
97
|
-
}.merge(account_info)
|
|
98
|
-
redis.set(redis_max_diff_url_key, max_url_data.to_json)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Store all URLs
|
|
102
|
-
timestamp = Time.current.to_i
|
|
103
|
-
url_data = {
|
|
104
|
-
url: request_url,
|
|
105
|
-
memory_diff: memory_diff,
|
|
106
|
-
ram_before: ram_before,
|
|
107
|
-
ram_after: ram_after,
|
|
108
|
-
timestamp: timestamp,
|
|
109
|
-
recorded_at: Time.current.iso8601
|
|
110
|
-
}.merge(account_info)
|
|
111
|
-
|
|
112
|
-
# Add URL to sorted set (score = memory_diff for DESC ordering)
|
|
113
|
-
redis.zadd(redis_high_usage_urls_key, memory_diff, url_data.to_json)
|
|
114
|
-
|
|
115
|
-
# Keep only top N URLs by removing lowest scores
|
|
116
|
-
current_count = redis.zcard(redis_high_usage_urls_key)
|
|
117
|
-
if current_count > config.max_stored_urls
|
|
118
|
-
# Remove the lowest scoring URLs (keep top max_stored_urls)
|
|
119
|
-
redis.zremrangebyrank(redis_high_usage_urls_key, 0, current_count - config.max_stored_urls - 1)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def build_request_url(env)
|
|
124
|
-
request = Rack::Request.new(env)
|
|
125
|
-
url = "#{request.request_method} #{request.fullpath}"
|
|
126
|
-
|
|
127
|
-
# Truncate very long URLs
|
|
128
|
-
(url.length > 600) ? "#{url[0..650]}..." : url
|
|
129
|
-
end
|
|
130
|
-
|
|
131
73
|
def extract_account_info(env)
|
|
132
74
|
account_info = {}
|
|
133
75
|
|
|
@@ -136,13 +78,13 @@ module MemHealth
|
|
|
136
78
|
if defined?(ActsAsTenant) && ActsAsTenant.current_tenant
|
|
137
79
|
account = ActsAsTenant.current_tenant
|
|
138
80
|
account_info[:account_id] = account.id
|
|
139
|
-
elsif env[
|
|
81
|
+
elsif env['warden']&.user&.respond_to?(:account)
|
|
140
82
|
# Try to get from authenticated user
|
|
141
|
-
account = env[
|
|
83
|
+
account = env['warden'].user.account
|
|
142
84
|
account_info[:account_id] = account.id
|
|
143
|
-
elsif env[
|
|
85
|
+
elsif env['HTTP_X_ACCOUNT_ID']
|
|
144
86
|
# Fallback to header if available
|
|
145
|
-
account_info[:account_id] = env[
|
|
87
|
+
account_info[:account_id] = env['HTTP_X_ACCOUNT_ID']
|
|
146
88
|
end
|
|
147
89
|
rescue StandardError => _e
|
|
148
90
|
# Silently fail if account extraction fails
|
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,13 +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
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2025-10-01 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rails
|
|
@@ -77,12 +78,16 @@ files:
|
|
|
77
78
|
- README.md
|
|
78
79
|
- app/controllers/mem_health/dashboard_controller.rb
|
|
79
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
|
|
80
83
|
- app/views/mem_health/dashboard/index.html.erb
|
|
81
84
|
- config/routes.rb
|
|
82
85
|
- lib/mem_health/configuration.rb
|
|
83
86
|
- lib/mem_health/engine.rb
|
|
87
|
+
- lib/mem_health/job_tracking_middleware.rb
|
|
84
88
|
- lib/mem_health/middleware.rb
|
|
85
89
|
- lib/mem_health/tracker.rb
|
|
90
|
+
- lib/mem_health/tracking_concern.rb
|
|
86
91
|
- lib/mem_health/version.rb
|
|
87
92
|
- lib/memhealth.rb
|
|
88
93
|
homepage: https://github.com/topkeyhq/memhealth
|
|
@@ -91,6 +96,7 @@ licenses:
|
|
|
91
96
|
metadata:
|
|
92
97
|
homepage_uri: https://github.com/topkeyhq/memhealth
|
|
93
98
|
source_code_uri: https://github.com/topkeyhq/memhealth
|
|
99
|
+
post_install_message:
|
|
94
100
|
rdoc_options: []
|
|
95
101
|
require_paths:
|
|
96
102
|
- lib
|
|
@@ -105,7 +111,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
105
111
|
- !ruby/object:Gem::Version
|
|
106
112
|
version: '0'
|
|
107
113
|
requirements: []
|
|
108
|
-
rubygems_version: 3.
|
|
114
|
+
rubygems_version: 3.5.16
|
|
115
|
+
signing_key:
|
|
109
116
|
specification_version: 4
|
|
110
117
|
summary: Rails memory health monitoring and tracking
|
|
111
118
|
test_files: []
|