rails_memory_profiler 0.1.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +107 -0
- data/Rakefile +16 -0
- data/app/assets/stylesheets/rails_memory_profiler/_01_base.css +24 -0
- data/app/assets/stylesheets/rails_memory_profiler/_02_layout.css +49 -0
- data/app/assets/stylesheets/rails_memory_profiler/_03_table.css +39 -0
- data/app/assets/stylesheets/rails_memory_profiler/_04_badges.css +12 -0
- data/app/assets/stylesheets/rails_memory_profiler/_05_filters.css +58 -0
- data/app/controllers/rails_memory_profiler/application_controller.rb +4 -0
- data/app/controllers/rails_memory_profiler/reports_controller.rb +50 -0
- data/app/helpers/rails_memory_profiler/application_helper.rb +35 -0
- data/app/javascript/rails_memory_profiler/application.js +6 -0
- data/app/javascript/rails_memory_profiler/filter_controller.js +30 -0
- data/app/jobs/rails_memory_profiler/application_job.rb +4 -0
- data/app/mailers/rails_memory_profiler/application_mailer.rb +6 -0
- data/app/models/rails_memory_profiler/application_record.rb +5 -0
- data/app/views/layouts/rails_memory_profiler/application.html.erb +14 -0
- data/app/views/rails_memory_profiler/reports/index.html.erb +56 -0
- data/app/views/rails_memory_profiler/reports/show.html.erb +35 -0
- data/config/importmap.rb +4 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rails_memory_profiler/install/install_generator.rb +25 -0
- data/lib/generators/rails_memory_profiler/install/templates/initializer.rb +28 -0
- data/lib/rails_memory_profiler/configuration.rb +16 -0
- data/lib/rails_memory_profiler/engine.rb +26 -0
- data/lib/rails_memory_profiler/middleware.rb +76 -0
- data/lib/rails_memory_profiler/report_store.rb +63 -0
- data/lib/rails_memory_profiler/request_context.rb +22 -0
- data/lib/rails_memory_profiler/version.rb +3 -0
- data/lib/rails_memory_profiler.rb +22 -0
- data/lib/tasks/rails_memory_profiler_tasks.rake +4 -0
- metadata +119 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 358d4a7e75c7fbdc553c28acc6350da8906ea834308410f32020168bc6e31985
|
|
4
|
+
data.tar.gz: 910ce5bc705376412f44904d6d3640ec4ad48aa5396f330dd45e8101ca77cb5c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ae42d9f95d0e6b01b82be8359de89a4b95379dbbfa5acb66a8ee35a3579dd98870d95b9015ee1041a2a1e43c70978c066509cead89a96c70fd5b7da15dce9fa5
|
|
7
|
+
data.tar.gz: dab8d94f6fbd06e9d96844b1e3c1dc5d010b0aa5b6257cba15f6dbef6e8a4202fe2df0d2b3a763803509ec5d4c08443d60adb146f9f9a86ea977716db4ee1a7e
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Chuck Smith
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# RailsMemoryProfiler
|
|
2
|
+
|
|
3
|
+
[](https://github.com/eclectic-coding/rails_memory_profiler/actions/workflows/main.yml)
|
|
4
|
+
[](https://rubygems.org/gems/rails_memory_profiler)
|
|
5
|
+
[](https://rubygems.org/gems/rails_memory_profiler)
|
|
6
|
+
[](https://www.ruby-lang.org)
|
|
7
|
+
[](https://rubyonrails.org)
|
|
8
|
+
[](https://codecov.io/gh/eclectic-coding/rails_memory_profiler)
|
|
9
|
+
|
|
10
|
+
Per-request memory allocation reports with a mountable dashboard UI. Fills the gap between the [`memory_profiler`](https://github.com/SamSaffron/memory_profiler) gem (terminal-only output, manual block wrapping) and having nowhere useful to browse results in a Rails app.
|
|
11
|
+
|
|
12
|
+
A Rack middleware captures object allocations for every request using `GC.stat` diffs. Results are stored in a thread-safe ring buffer and served through a mountable engine with a sortable, filterable dashboard.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Dashboard](#dashboard)
|
|
20
|
+
- [Configuration](#configuration)
|
|
21
|
+
- [Contributing](#contributing)
|
|
22
|
+
- [License](#license)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Add to your Gemfile:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
gem "rails_memory_profiler", group: :development
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Run the install generator:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bundle exec rails generate rails_memory_profiler:install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This creates `config/initializers/rails_memory_profiler.rb` with all options documented and prints mount instructions.
|
|
41
|
+
|
|
42
|
+
Mount the dashboard in `config/routes.rb`:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
mount RailsMemoryProfiler::Engine, at: "/rails/memory"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then visit `/rails/memory/reports` to see per-request allocation data.
|
|
49
|
+
|
|
50
|
+
[↑ Back to top](#railsmemoryprofiler)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Dashboard
|
|
55
|
+
|
|
56
|
+
The index view shows a sortable table of captured requests. Click any row to open the detail view for that request.
|
|
57
|
+
|
|
58
|
+
Columns: **Path**, **Controller#Action**, **Allocated Objects** (colour-coded), **Retained Objects**, **Duration (ms)**, **Recorded At**.
|
|
59
|
+
|
|
60
|
+
Use the controller filter to narrow the table down to a specific controller without a page reload.
|
|
61
|
+
|
|
62
|
+
[↑ Back to top](#railsmemoryprofiler)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
All options and their defaults:
|
|
69
|
+
|
|
70
|
+
| Option | Default | Description |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `enabled` | `true` in development | Enable/disable request profiling |
|
|
73
|
+
| `sample_rate` | `1` | Profile every Nth request (`1` = every request) |
|
|
74
|
+
| `store_size` | `100` | Max reports in the ring buffer; oldest are evicted when full |
|
|
75
|
+
| `dashboard_enabled` | `true` in development | Enable the dashboard endpoint |
|
|
76
|
+
| `min_allocated_objects` | `0` | Skip requests that allocate fewer objects than this |
|
|
77
|
+
| `ignore_paths` | `[]` | Paths to skip — strings (prefix) or regexes |
|
|
78
|
+
| `ignore_controllers` | `[]` | Controller names to skip (e.g. `"rails/health"`) |
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
RailsMemoryProfiler.configure do |config|
|
|
84
|
+
config.sample_rate = 5
|
|
85
|
+
config.min_allocated_objects = 1_000
|
|
86
|
+
config.ignore_paths = ["/rails/memory", "/up"]
|
|
87
|
+
config.ignore_controllers = ["rails/health"]
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
[↑ Back to top](#railsmemoryprofiler)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Contributing
|
|
96
|
+
|
|
97
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/rails_memory_profiler).
|
|
98
|
+
|
|
99
|
+
[↑ Back to top](#railsmemoryprofiler)
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
106
|
+
|
|
107
|
+
[↑ Back to top](#railsmemoryprofiler)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
|
|
3
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
|
4
|
+
load "rails/tasks/engine.rake"
|
|
5
|
+
|
|
6
|
+
require "bundler/gem_tasks"
|
|
7
|
+
|
|
8
|
+
require 'rubocop/rake_task'
|
|
9
|
+
require 'bundler/audit/task'
|
|
10
|
+
require 'rspec/core/rake_task'
|
|
11
|
+
|
|
12
|
+
RuboCop::RakeTask.new(:lint)
|
|
13
|
+
Bundler::Audit::Task.new
|
|
14
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
15
|
+
|
|
16
|
+
task default: [:lint, :'bundle:audit:update', 'bundle:audit:check', :spec]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--bg: #f8f9fa;
|
|
5
|
+
--surface: #ffffff;
|
|
6
|
+
--border: #dee2e6;
|
|
7
|
+
--text: #212529;
|
|
8
|
+
--muted: #6c757d;
|
|
9
|
+
--accent: #4f46e5;
|
|
10
|
+
--radius: 6px;
|
|
11
|
+
--shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
16
|
+
font-size: 14px;
|
|
17
|
+
line-height: 1.5;
|
|
18
|
+
color: var(--text);
|
|
19
|
+
background: var(--bg);
|
|
20
|
+
padding: 2rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.rmp-monospace { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 12px; }
|
|
24
|
+
.rmp-muted { color: var(--muted); font-size: 12px; }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.rmp-header { margin-bottom: 1.5rem; }
|
|
2
|
+
.rmp-header h1 { font-size: 1.4rem; font-weight: 600; }
|
|
3
|
+
.rmp-header p { color: var(--muted); margin-top: 0.2rem; }
|
|
4
|
+
|
|
5
|
+
.rmp-empty { color: var(--muted); font-style: italic; margin-top: 1rem; }
|
|
6
|
+
.rmp-summary { margin-bottom: 0.5rem; }
|
|
7
|
+
|
|
8
|
+
.rmp-back {
|
|
9
|
+
display: inline-block;
|
|
10
|
+
margin-bottom: 1rem;
|
|
11
|
+
color: var(--accent);
|
|
12
|
+
text-decoration: none;
|
|
13
|
+
font-size: 13px;
|
|
14
|
+
}
|
|
15
|
+
.rmp-back:hover { text-decoration: underline; }
|
|
16
|
+
|
|
17
|
+
.rmp-detail {
|
|
18
|
+
background: var(--surface);
|
|
19
|
+
border-radius: var(--radius);
|
|
20
|
+
box-shadow: var(--shadow);
|
|
21
|
+
padding: 1.5rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.rmp-detail-grid {
|
|
25
|
+
display: grid;
|
|
26
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
27
|
+
gap: 1rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.rmp-detail-item {
|
|
31
|
+
padding: 0.75rem 1rem;
|
|
32
|
+
background: var(--bg);
|
|
33
|
+
border-radius: var(--radius);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.rmp-detail-label {
|
|
37
|
+
font-size: 11px;
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
color: var(--muted);
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
letter-spacing: .04em;
|
|
42
|
+
margin-bottom: 0.25rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.rmp-detail-value {
|
|
46
|
+
font-size: 1.25rem;
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
color: var(--text);
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.rmp-table {
|
|
2
|
+
width: 100%;
|
|
3
|
+
border-collapse: collapse;
|
|
4
|
+
background: var(--surface);
|
|
5
|
+
border-radius: var(--radius);
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
box-shadow: var(--shadow);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.rmp-table th {
|
|
11
|
+
text-align: left;
|
|
12
|
+
padding: 0.6rem 1rem;
|
|
13
|
+
background: #f0f0f0;
|
|
14
|
+
font-weight: 600;
|
|
15
|
+
font-size: 11px;
|
|
16
|
+
text-transform: uppercase;
|
|
17
|
+
letter-spacing: .04em;
|
|
18
|
+
color: var(--muted);
|
|
19
|
+
border-bottom: 1px solid var(--border);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.rmp-table td {
|
|
23
|
+
padding: 0.6rem 1rem;
|
|
24
|
+
border-bottom: 1px solid #eee;
|
|
25
|
+
vertical-align: middle;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.rmp-table tr:last-child td { border-bottom: none; }
|
|
29
|
+
.rmp-table tr:hover td { background: #fafafa; }
|
|
30
|
+
|
|
31
|
+
.rmp-sort-link {
|
|
32
|
+
color: inherit;
|
|
33
|
+
text-decoration: none;
|
|
34
|
+
display: block;
|
|
35
|
+
}
|
|
36
|
+
.rmp-sort-link:hover { color: var(--accent); }
|
|
37
|
+
.rmp-sort-active { color: var(--accent); }
|
|
38
|
+
|
|
39
|
+
.rmp-path { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 11px; opacity: .7; }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
.rmp-badge {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
padding: 2px 8px;
|
|
4
|
+
border-radius: 3px;
|
|
5
|
+
font-size: 11px;
|
|
6
|
+
font-weight: 600;
|
|
7
|
+
white-space: nowrap;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.rmp-badge--low { background: #d1fae5; color: #065f46; }
|
|
11
|
+
.rmp-badge--mid { background: #fef3c7; color: #92400e; }
|
|
12
|
+
.rmp-badge--high { background: #fee2e2; color: #991b1b; }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
.rmp-filters {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 0.75rem;
|
|
5
|
+
margin-bottom: 1rem;
|
|
6
|
+
padding: 0.6rem 1rem;
|
|
7
|
+
background: var(--surface);
|
|
8
|
+
border-radius: var(--radius);
|
|
9
|
+
box-shadow: var(--shadow);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.rmp-filters label {
|
|
13
|
+
font-size: 11px;
|
|
14
|
+
font-weight: 600;
|
|
15
|
+
text-transform: uppercase;
|
|
16
|
+
letter-spacing: .04em;
|
|
17
|
+
color: var(--muted);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.rmp-input {
|
|
21
|
+
font-family: inherit;
|
|
22
|
+
font-size: 13px;
|
|
23
|
+
padding: 0.3rem 0.6rem;
|
|
24
|
+
border: 1px solid var(--border);
|
|
25
|
+
border-radius: var(--radius);
|
|
26
|
+
background: var(--bg);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
min-width: 200px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.rmp-input:focus {
|
|
32
|
+
outline: none;
|
|
33
|
+
border-color: #86b7fe;
|
|
34
|
+
box-shadow: 0 0 0 2px rgba(13,110,253,.15);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.rmp-input-wrapper {
|
|
38
|
+
position: relative;
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.rmp-input-wrapper .rmp-input { padding-right: 1.6rem; }
|
|
44
|
+
|
|
45
|
+
.rmp-input-clear {
|
|
46
|
+
position: absolute;
|
|
47
|
+
right: 0.4rem;
|
|
48
|
+
background: none;
|
|
49
|
+
border: none;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
color: var(--muted);
|
|
52
|
+
font-size: 11px;
|
|
53
|
+
line-height: 1;
|
|
54
|
+
padding: 0.15rem 0.2rem;
|
|
55
|
+
border-radius: 2px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.rmp-input-clear:hover { color: var(--text); background: var(--border); }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module RailsMemoryProfiler
|
|
2
|
+
class ReportsController < ActionController::Base
|
|
3
|
+
protect_from_forgery with: :null_session
|
|
4
|
+
layout "rails_memory_profiler/application"
|
|
5
|
+
helper RailsMemoryProfiler::ApplicationHelper
|
|
6
|
+
|
|
7
|
+
before_action :check_dashboard_enabled
|
|
8
|
+
|
|
9
|
+
SORTABLE_COLUMNS = %w[path controller allocated_objects retained_objects duration_ms recorded_at].freeze
|
|
10
|
+
|
|
11
|
+
def index
|
|
12
|
+
reports = ReportStore.all
|
|
13
|
+
|
|
14
|
+
respond_to do |format|
|
|
15
|
+
format.json { render json: reports }
|
|
16
|
+
format.html do
|
|
17
|
+
@sort = SORTABLE_COLUMNS.include?(params[:sort]) ? params[:sort] : "recorded_at"
|
|
18
|
+
@direction = params[:direction] == "asc" ? "asc" : "desc"
|
|
19
|
+
@reports = sorted(reports, @sort, @direction)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def show
|
|
25
|
+
@report = ReportStore.find(params[:id])
|
|
26
|
+
|
|
27
|
+
respond_to do |format|
|
|
28
|
+
format.html
|
|
29
|
+
format.json do
|
|
30
|
+
if @report
|
|
31
|
+
render json: @report
|
|
32
|
+
else
|
|
33
|
+
render json: { error: "Report not found" }, status: :not_found
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def sorted(reports, sort, direction)
|
|
42
|
+
sorted = reports.sort_by { |r| r[sort.to_sym] || 0 }
|
|
43
|
+
direction == "asc" ? sorted : sorted.reverse
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check_dashboard_enabled
|
|
47
|
+
head :forbidden unless RailsMemoryProfiler.config.dashboard_enabled
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module RailsMemoryProfiler
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
def inline_styles
|
|
4
|
+
dir = RailsMemoryProfiler::Engine.root.join("app/assets/stylesheets/rails_memory_profiler")
|
|
5
|
+
css = dir.glob("_*.css").sort.map(&:read).join("\n")
|
|
6
|
+
content_tag(:style, css.html_safe)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def sort_th(label, column, current_sort, current_dir)
|
|
10
|
+
active = current_sort == column.to_s
|
|
11
|
+
next_dir = (active && current_dir == "asc") ? "desc" : "asc"
|
|
12
|
+
indicator = active ? (current_dir == "asc" ? " ▲" : " ▼") : ""
|
|
13
|
+
params = request.query_parameters.merge("sort" => column, "direction" => next_dir)
|
|
14
|
+
href = "?" + params.to_query
|
|
15
|
+
content_tag(:th) do
|
|
16
|
+
link_to(
|
|
17
|
+
"#{label}#{indicator}".html_safe,
|
|
18
|
+
href,
|
|
19
|
+
class: ["rmp-sort-link", ("rmp-sort-active" if active)].compact.join(" ")
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def allocation_badge(count)
|
|
25
|
+
css_class = if count < 5_000
|
|
26
|
+
"rmp-badge--low"
|
|
27
|
+
elsif count < 20_000
|
|
28
|
+
"rmp-badge--mid"
|
|
29
|
+
else
|
|
30
|
+
"rmp-badge--high"
|
|
31
|
+
end
|
|
32
|
+
content_tag(:span, number_with_delimiter(count), class: "rmp-badge #{css_class}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["input", "row", "clearButton"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this._updateClear()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
filter() {
|
|
11
|
+
const query = this.inputTarget.value.toLowerCase()
|
|
12
|
+
this.rowTargets.forEach(row => {
|
|
13
|
+
const name = (row.dataset.controllerName || "").toLowerCase()
|
|
14
|
+
row.hidden = query.length > 0 && !name.includes(query)
|
|
15
|
+
})
|
|
16
|
+
this._updateClear()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
clear() {
|
|
20
|
+
this.inputTarget.value = ""
|
|
21
|
+
this.rowTargets.forEach(row => { row.hidden = false })
|
|
22
|
+
this._updateClear()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_updateClear() {
|
|
26
|
+
if (this.hasClearButtonTarget) {
|
|
27
|
+
this.clearButtonTarget.hidden = this.inputTarget.value.length === 0
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>RailsMemoryProfiler</title>
|
|
7
|
+
<link rel="icon" href="data:,">
|
|
8
|
+
<%= inline_styles %>
|
|
9
|
+
<%= javascript_importmap_tags "rails_memory_profiler" %>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<%= yield %>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<div class="rmp-header">
|
|
2
|
+
<h1>Memory Profiler</h1>
|
|
3
|
+
<p><%= @reports.size %> request<%= "s" unless @reports.size == 1 %> captured</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="rmp-filters" data-controller="filter">
|
|
7
|
+
<label for="rmp-controller-filter">Controller</label>
|
|
8
|
+
<div class="rmp-input-wrapper">
|
|
9
|
+
<input id="rmp-controller-filter"
|
|
10
|
+
type="text"
|
|
11
|
+
class="rmp-input"
|
|
12
|
+
placeholder="e.g. posts"
|
|
13
|
+
data-filter-target="input"
|
|
14
|
+
data-action="input->filter#filter">
|
|
15
|
+
<button type="button"
|
|
16
|
+
class="rmp-input-clear"
|
|
17
|
+
aria-label="Clear filter"
|
|
18
|
+
hidden
|
|
19
|
+
data-filter-target="clearButton"
|
|
20
|
+
data-action="click->filter#clear">✕</button>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<% if @reports.empty? %>
|
|
24
|
+
<% else %>
|
|
25
|
+
<span class="rmp-muted">Filtering is client-side — no page reload needed.</span>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<% if @reports.empty? %>
|
|
30
|
+
<p class="rmp-empty">No requests recorded yet. Browse your app and refresh.</p>
|
|
31
|
+
<% else %>
|
|
32
|
+
<table class="rmp-table">
|
|
33
|
+
<thead>
|
|
34
|
+
<tr>
|
|
35
|
+
<%= sort_th("Path", "path", @sort, @direction) %>
|
|
36
|
+
<%= sort_th("Controller#Action", "controller", @sort, @direction) %>
|
|
37
|
+
<%= sort_th("Allocated", "allocated_objects", @sort, @direction) %>
|
|
38
|
+
<%= sort_th("Retained", "retained_objects", @sort, @direction) %>
|
|
39
|
+
<%= sort_th("Duration (ms)", "duration_ms", @sort, @direction) %>
|
|
40
|
+
<%= sort_th("Recorded", "recorded_at", @sort, @direction) %>
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
<% @reports.each do |report| %>
|
|
45
|
+
<tr data-filter-target="row" data-controller-name="<%= report[:controller] %>">
|
|
46
|
+
<td><%= link_to report[:path], report_path(report[:id]), class: "rmp-path" %></td>
|
|
47
|
+
<td><%= [report[:controller], report[:action]].compact.join("#") %></td>
|
|
48
|
+
<td><%= allocation_badge(report[:allocated_objects]) %></td>
|
|
49
|
+
<td class="rmp-muted"><%= number_with_delimiter(report[:retained_objects]) %></td>
|
|
50
|
+
<td class="rmp-muted"><%= report[:duration_ms] %></td>
|
|
51
|
+
<td class="rmp-muted"><%= report[:recorded_at]&.strftime("%H:%M:%S") %></td>
|
|
52
|
+
</tr>
|
|
53
|
+
<% end %>
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
<% end %>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<%= link_to "← All Requests", reports_path, class: "rmp-back" %>
|
|
2
|
+
|
|
3
|
+
<% if @report %>
|
|
4
|
+
<div class="rmp-header">
|
|
5
|
+
<h1><%= @report[:method] %> <%= @report[:path] %></h1>
|
|
6
|
+
<p>
|
|
7
|
+
<%= [@report[:controller], @report[:action]].compact.join("#") %>
|
|
8
|
+
·
|
|
9
|
+
<%= @report[:recorded_at]&.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
10
|
+
</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="rmp-detail">
|
|
14
|
+
<div class="rmp-detail-grid">
|
|
15
|
+
<div class="rmp-detail-item">
|
|
16
|
+
<div class="rmp-detail-label">Allocated Objects</div>
|
|
17
|
+
<div class="rmp-detail-value"><%= number_with_delimiter(@report[:allocated_objects]) %></div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="rmp-detail-item">
|
|
20
|
+
<div class="rmp-detail-label">Retained Objects</div>
|
|
21
|
+
<div class="rmp-detail-value"><%= number_with_delimiter(@report[:retained_objects]) %></div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="rmp-detail-item">
|
|
24
|
+
<div class="rmp-detail-label">Duration</div>
|
|
25
|
+
<div class="rmp-detail-value"><%= @report[:duration_ms] %> ms</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="rmp-detail-item">
|
|
28
|
+
<div class="rmp-detail-label">HTTP Method</div>
|
|
29
|
+
<div class="rmp-detail-value"><%= @report[:method] %></div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<% else %>
|
|
34
|
+
<p class="rmp-empty">Report not found — it may have been evicted from the store.</p>
|
|
35
|
+
<% end %>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"
|
|
2
|
+
pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
|
|
3
|
+
pin "rails_memory_profiler", to: "rails_memory_profiler/application.js"
|
|
4
|
+
pin "rails_memory_profiler/filter_controller", to: "rails_memory_profiler/filter_controller.js"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module RailsMemoryProfiler
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Creates a RailsMemoryProfiler initializer in config/initializers."
|
|
9
|
+
|
|
10
|
+
def copy_initializer
|
|
11
|
+
template "initializer.rb", "config/initializers/rails_memory_profiler.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show_readme
|
|
15
|
+
say ""
|
|
16
|
+
say " Mount the dashboard in config/routes.rb:", :green
|
|
17
|
+
say ""
|
|
18
|
+
say ' mount RailsMemoryProfiler::Engine, at: "/rails/memory"'
|
|
19
|
+
say ""
|
|
20
|
+
say " Then visit /rails/memory/reports to see per-request allocation data.", :green
|
|
21
|
+
say ""
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
RailsMemoryProfiler.configure do |config|
|
|
2
|
+
# Enable or disable per-request memory profiling.
|
|
3
|
+
# Defaults to true in development, false elsewhere.
|
|
4
|
+
# config.enabled = Rails.env.development?
|
|
5
|
+
|
|
6
|
+
# Profile every Nth request. 1 = every request, 10 = every 10th, etc.
|
|
7
|
+
# Increase for busier apps where profiling every request adds too much overhead.
|
|
8
|
+
# config.sample_rate = 1
|
|
9
|
+
|
|
10
|
+
# Maximum number of reports kept in the in-memory ring buffer.
|
|
11
|
+
# Oldest reports are evicted when the buffer is full.
|
|
12
|
+
# config.store_size = 100
|
|
13
|
+
|
|
14
|
+
# Enable the HTML/JSON dashboard at /reports (relative to the engine mount point).
|
|
15
|
+
# Defaults to true in development, false elsewhere.
|
|
16
|
+
# config.dashboard_enabled = Rails.env.development?
|
|
17
|
+
|
|
18
|
+
# Skip recording requests that allocate fewer objects than this threshold.
|
|
19
|
+
# Useful for filtering out trivial requests (health checks, asset hits, etc.).
|
|
20
|
+
# config.min_allocated_objects = 0
|
|
21
|
+
|
|
22
|
+
# Paths to skip entirely — accepts strings (prefix match) or regexes.
|
|
23
|
+
# The engine's own mount path is a good candidate to add here.
|
|
24
|
+
# config.ignore_paths = ["/rails/memory", "/up", %r{^/assets/}]
|
|
25
|
+
|
|
26
|
+
# Controllers to skip — matched against the Rails controller name.
|
|
27
|
+
# config.ignore_controllers = ["rails/health", "rails_memory_profiler/reports"]
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module RailsMemoryProfiler
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :enabled, :sample_rate, :store_size, :dashboard_enabled,
|
|
4
|
+
:min_allocated_objects, :ignore_paths, :ignore_controllers
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@enabled = Rails.env.development?
|
|
8
|
+
@sample_rate = 1
|
|
9
|
+
@store_size = 100
|
|
10
|
+
@dashboard_enabled = Rails.env.development?
|
|
11
|
+
@min_allocated_objects = 0
|
|
12
|
+
@ignore_paths = []
|
|
13
|
+
@ignore_controllers = []
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "turbo-rails"
|
|
2
|
+
require "importmap-rails"
|
|
3
|
+
|
|
4
|
+
module RailsMemoryProfiler
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
isolate_namespace RailsMemoryProfiler
|
|
7
|
+
config.generators.api_only = true
|
|
8
|
+
|
|
9
|
+
initializer "rails_memory_profiler.assets" do |app|
|
|
10
|
+
if app.config.respond_to?(:assets)
|
|
11
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "rails_memory_profiler.importmap", before: "importmap" do |app|
|
|
16
|
+
if app.config.respond_to?(:importmap)
|
|
17
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
18
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
initializer "rails_memory_profiler.middleware" do |app|
|
|
23
|
+
app.middleware.use(Middleware)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module RailsMemoryProfiler
|
|
2
|
+
class Middleware
|
|
3
|
+
def initialize(app)
|
|
4
|
+
@app = app
|
|
5
|
+
@request_count = 0
|
|
6
|
+
@mutex = Mutex.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
return @app.call(env) unless RailsMemoryProfiler.config.enabled
|
|
11
|
+
return @app.call(env) if ignored_path?(env["PATH_INFO"])
|
|
12
|
+
return @app.call(env) unless sample?
|
|
13
|
+
|
|
14
|
+
profile(env)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def profile(env)
|
|
20
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
21
|
+
before_alloc = GC.stat[:total_allocated_objects]
|
|
22
|
+
before_freed = GC.stat[:total_freed_objects]
|
|
23
|
+
|
|
24
|
+
status, headers, body = @app.call(env)
|
|
25
|
+
|
|
26
|
+
after_alloc = GC.stat[:total_allocated_objects]
|
|
27
|
+
after_freed = GC.stat[:total_freed_objects]
|
|
28
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
|
|
29
|
+
|
|
30
|
+
allocated_objects = after_alloc - before_alloc
|
|
31
|
+
retained_objects = (after_alloc - after_freed) - (before_alloc - before_freed)
|
|
32
|
+
|
|
33
|
+
if allocated_objects >= RailsMemoryProfiler.config.min_allocated_objects
|
|
34
|
+
params = env["action_dispatch.request.path_parameters"] || {}
|
|
35
|
+
controller = params[:controller]
|
|
36
|
+
|
|
37
|
+
unless ignored_controller?(controller)
|
|
38
|
+
ReportStore.push(
|
|
39
|
+
controller: controller,
|
|
40
|
+
action: params[:action],
|
|
41
|
+
path: env["PATH_INFO"],
|
|
42
|
+
method: env["REQUEST_METHOD"],
|
|
43
|
+
duration_ms: duration_ms,
|
|
44
|
+
allocated_objects: allocated_objects,
|
|
45
|
+
retained_objects: [retained_objects, 0].max,
|
|
46
|
+
recorded_at: Time.current
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
[status, headers, body]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def sample?
|
|
55
|
+
rate = RailsMemoryProfiler.config.sample_rate
|
|
56
|
+
return true if rate <= 1
|
|
57
|
+
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
@request_count = (@request_count + 1) % rate
|
|
60
|
+
@request_count.zero?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def ignored_path?(path)
|
|
65
|
+
RailsMemoryProfiler.config.ignore_paths.any? do |pattern|
|
|
66
|
+
pattern.is_a?(Regexp) ? pattern.match?(path) : path.start_with?(pattern.to_s)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ignored_controller?(controller)
|
|
71
|
+
return false unless controller
|
|
72
|
+
|
|
73
|
+
RailsMemoryProfiler.config.ignore_controllers.any? { |name| name.to_s == controller }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module RailsMemoryProfiler
|
|
2
|
+
module ReportStore
|
|
3
|
+
class << self
|
|
4
|
+
def push(report)
|
|
5
|
+
mutex.synchronize do
|
|
6
|
+
ensure_buffer_size
|
|
7
|
+
buffer[@write_pos] = report.merge(id: SecureRandom.hex(6))
|
|
8
|
+
@write_pos = (@write_pos + 1) % capacity
|
|
9
|
+
@stored = [@stored + 1, capacity].min
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def find(id)
|
|
14
|
+
all.find { |r| r[:id] == id }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def all
|
|
18
|
+
mutex.synchronize do
|
|
19
|
+
stored = @stored || 0
|
|
20
|
+
return [] if stored.zero?
|
|
21
|
+
|
|
22
|
+
if stored < capacity
|
|
23
|
+
buffer.first(stored)
|
|
24
|
+
else
|
|
25
|
+
buffer[@write_pos..] + buffer[0...@write_pos]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def clear
|
|
31
|
+
mutex.synchronize { reset! }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def size
|
|
35
|
+
mutex.synchronize { @stored || 0 }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def mutex
|
|
41
|
+
@mutex ||= Mutex.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def capacity
|
|
45
|
+
RailsMemoryProfiler.config.store_size
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ensure_buffer_size
|
|
49
|
+
reset! if @write_pos.nil? || @buffer.nil? || @buffer.size != capacity
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def buffer
|
|
53
|
+
@buffer ||= Array.new(capacity)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def reset!
|
|
57
|
+
@buffer = Array.new(capacity)
|
|
58
|
+
@write_pos = 0
|
|
59
|
+
@stored = 0
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module RailsMemoryProfiler
|
|
2
|
+
module RequestContext
|
|
3
|
+
class << self
|
|
4
|
+
def set(controller:, action:, path:, method:)
|
|
5
|
+
Thread.current[:rails_memory_profiler_context] = {
|
|
6
|
+
controller: controller,
|
|
7
|
+
action: action,
|
|
8
|
+
path: path,
|
|
9
|
+
method: method
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def current
|
|
14
|
+
Thread.current[:rails_memory_profiler_context] || {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def clear
|
|
18
|
+
Thread.current[:rails_memory_profiler_context] = nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "rails_memory_profiler/version"
|
|
2
|
+
require "rails_memory_profiler/configuration"
|
|
3
|
+
require "rails_memory_profiler/request_context"
|
|
4
|
+
require "rails_memory_profiler/report_store"
|
|
5
|
+
require "rails_memory_profiler/middleware"
|
|
6
|
+
require "rails_memory_profiler/engine"
|
|
7
|
+
|
|
8
|
+
module RailsMemoryProfiler
|
|
9
|
+
class << self
|
|
10
|
+
def configure
|
|
11
|
+
yield config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def config
|
|
15
|
+
@config ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def reset_config!
|
|
19
|
+
@config = Configuration.new
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails_memory_profiler
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Chuck Smith
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: importmap-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: turbo-rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
description: A Rack middleware captures object allocations per request using GC.stat
|
|
55
|
+
diffs and stores them in a thread-safe ring buffer. Results are served through a
|
|
56
|
+
mountable engine with a sortable, filterable dashboard — filling the gap between
|
|
57
|
+
the memory_profiler gem and having nowhere useful to view results in a Rails app.
|
|
58
|
+
email:
|
|
59
|
+
- eclectic-coding@users.noreply.github.com
|
|
60
|
+
executables: []
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- MIT-LICENSE
|
|
65
|
+
- README.md
|
|
66
|
+
- Rakefile
|
|
67
|
+
- app/assets/stylesheets/rails_memory_profiler/_01_base.css
|
|
68
|
+
- app/assets/stylesheets/rails_memory_profiler/_02_layout.css
|
|
69
|
+
- app/assets/stylesheets/rails_memory_profiler/_03_table.css
|
|
70
|
+
- app/assets/stylesheets/rails_memory_profiler/_04_badges.css
|
|
71
|
+
- app/assets/stylesheets/rails_memory_profiler/_05_filters.css
|
|
72
|
+
- app/controllers/rails_memory_profiler/application_controller.rb
|
|
73
|
+
- app/controllers/rails_memory_profiler/reports_controller.rb
|
|
74
|
+
- app/helpers/rails_memory_profiler/application_helper.rb
|
|
75
|
+
- app/javascript/rails_memory_profiler/application.js
|
|
76
|
+
- app/javascript/rails_memory_profiler/filter_controller.js
|
|
77
|
+
- app/jobs/rails_memory_profiler/application_job.rb
|
|
78
|
+
- app/mailers/rails_memory_profiler/application_mailer.rb
|
|
79
|
+
- app/models/rails_memory_profiler/application_record.rb
|
|
80
|
+
- app/views/layouts/rails_memory_profiler/application.html.erb
|
|
81
|
+
- app/views/rails_memory_profiler/reports/index.html.erb
|
|
82
|
+
- app/views/rails_memory_profiler/reports/show.html.erb
|
|
83
|
+
- config/importmap.rb
|
|
84
|
+
- config/routes.rb
|
|
85
|
+
- lib/generators/rails_memory_profiler/install/install_generator.rb
|
|
86
|
+
- lib/generators/rails_memory_profiler/install/templates/initializer.rb
|
|
87
|
+
- lib/rails_memory_profiler.rb
|
|
88
|
+
- lib/rails_memory_profiler/configuration.rb
|
|
89
|
+
- lib/rails_memory_profiler/engine.rb
|
|
90
|
+
- lib/rails_memory_profiler/middleware.rb
|
|
91
|
+
- lib/rails_memory_profiler/report_store.rb
|
|
92
|
+
- lib/rails_memory_profiler/request_context.rb
|
|
93
|
+
- lib/rails_memory_profiler/version.rb
|
|
94
|
+
- lib/tasks/rails_memory_profiler_tasks.rake
|
|
95
|
+
homepage: https://github.com/eclectic-coding/rails_memory_profiler
|
|
96
|
+
licenses:
|
|
97
|
+
- MIT
|
|
98
|
+
metadata:
|
|
99
|
+
homepage_uri: https://github.com/eclectic-coding/rails_memory_profiler
|
|
100
|
+
source_code_uri: https://github.com/eclectic-coding/rails_memory_profiler
|
|
101
|
+
changelog_uri: https://github.com/eclectic-coding/rails_memory_profiler/blob/main/CHANGELOG.md
|
|
102
|
+
rdoc_options: []
|
|
103
|
+
require_paths:
|
|
104
|
+
- lib
|
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.3'
|
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '0'
|
|
115
|
+
requirements: []
|
|
116
|
+
rubygems_version: 4.0.10
|
|
117
|
+
specification_version: 4
|
|
118
|
+
summary: Per-request memory allocation reports with a mountable dashboard UI for Rails.
|
|
119
|
+
test_files: []
|