rails_memory_profiler 0.5.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ecc5cf3944827bacd93b4ba2609cf46516c1a2ecf54ff922e1b36d36e7e1592
4
- data.tar.gz: 50de3643fab494e8d03a494d4c776914c234469844b095deb0964efbf94036ce
3
+ metadata.gz: 0cfc3104434c10aa68686413abf2c879661158fde43cbe61c53cf31f45154b6f
4
+ data.tar.gz: 114b4471fca8c8a32c2d0ea00debec64f2e338d3b92268bff205ab1db871e73a
5
5
  SHA512:
6
- metadata.gz: '091da3ac58edfd24e7fb0c8090e782a9af30d08dd2f5d59054b9f8108704c99f2c7ac238def2004730c22a77442ece51375ed221694666b828b76cb589e8dd0c'
7
- data.tar.gz: 8bd3e45989fe1cac7bb15b02304d8e846977fa643652d949f2b4ba683019074334678d364dd81cc7ab939f453c6418f6a6f8556142685cf00fb7c65675912416
6
+ metadata.gz: bd622ed113b4cc6c1cf8bcebc612785bc5a6ee549daa6a83b7d74df236eefe9beffac0a81867ad54053b7a2761e24e7687e8de96d0004e11e94cc9a34050d39c
7
+ data.tar.gz: bfed9ad01ac4d045eb61d01bf4effbfa45b2c89839daa9faf58542afd5dbc2268d8c124f85c6d659e8d2d620ce9548a4c39d5d797fbf32ebe8f00e4c9092adf9
data/README.md CHANGED
@@ -18,6 +18,7 @@ A Rack middleware captures object allocations for every request using `GC.stat`
18
18
  - [Installation](#installation)
19
19
  - [Dashboard](#dashboard)
20
20
  - [Configuration](#configuration)
21
+ - [Compatibility](#compatibility)
21
22
  - [Contributing](#contributing)
22
23
  - [License](#license)
23
24
 
@@ -121,9 +122,20 @@ expect { MyClass.new }.to allocate_fewer_than(500)
121
122
 
122
123
  ---
123
124
 
125
+ ## Compatibility
126
+
127
+ | | Ruby 3.3 | Ruby 3.4 | Ruby 4.0 |
128
+ |----------|:--------:|:--------:|:--------:|
129
+ | Rails 7.1 | ✓ | ✓ | ✓ |
130
+ | Rails 8.x | ✓ | ✓ | ✓ |
131
+
132
+ [↑ Back to top](#railsmemoryprofiler)
133
+
134
+ ---
135
+
124
136
  ## Contributing
125
137
 
126
- Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/rails_memory_profiler).
138
+ Bug reports and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
127
139
 
128
140
  [↑ Back to top](#railsmemoryprofiler)
129
141
 
@@ -15,11 +15,6 @@ module RailsMemoryProfiler
15
15
  end
16
16
  end
17
17
 
18
- def clear
19
- ReportStore.clear
20
- redirect_to reports_path
21
- end
22
-
23
18
  def show
24
19
  @report = ReportStore.find(params[:id])
25
20
 
@@ -0,0 +1,8 @@
1
+ module RailsMemoryProfiler
2
+ class StoreController < BaseController
3
+ def destroy
4
+ ReportStore.clear
5
+ redirect_to reports_path
6
+ end
7
+ end
8
+ end
@@ -5,7 +5,7 @@
5
5
  <p><%= @reports.size %> request<%= "s" unless @reports.size == 1 %> captured</p>
6
6
  </div>
7
7
  <% unless @reports.empty? %>
8
- <%= button_to "Clear All", clear_reports_path, method: :delete,
8
+ <%= button_to "Clear All", store_path, method: :delete,
9
9
  class: "rmp-btn-danger",
10
10
  form: { data: { turbo_confirm: "Clear all reports? This cannot be undone." } } %>
11
11
  <% end %>
data/config/routes.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  RailsMemoryProfiler::Engine.routes.draw do
2
- resources :reports, only: [:index, :show] do
3
- collection do
4
- delete :clear
5
- end
6
- end
7
- resource :comparison, only: [:show]
2
+ resources :reports, only: [:index, :show]
3
+ resource :comparison, only: [:show]
4
+ resource :store, only: [:destroy], controller: "store"
8
5
  end
@@ -1,5 +1,68 @@
1
1
  module RailsMemoryProfiler
2
+ # Holds all user-facing settings. Obtain the shared instance via
3
+ # {RailsMemoryProfiler.config} or set values inside a {RailsMemoryProfiler.configure} block.
4
+ #
5
+ # @example
6
+ # RailsMemoryProfiler.configure do |config|
7
+ # config.sample_rate = 5
8
+ # config.min_allocated_objects = 1_000
9
+ # config.notifiers = [RailsMemoryProfiler::Notifiers::Console.new]
10
+ # end
2
11
  class Configuration
12
+ # @!attribute enabled
13
+ # Enable or disable per-request profiling. Defaults to +true+ in development.
14
+ # @return [Boolean]
15
+
16
+ # @!attribute sample_rate
17
+ # Profile every Nth request. +1+ profiles every request.
18
+ # @return [Integer]
19
+
20
+ # @!attribute store_size
21
+ # Maximum number of reports kept in the ring buffer. Oldest are evicted when full.
22
+ # @return [Integer]
23
+
24
+ # @!attribute dashboard_enabled
25
+ # Enable the HTML/JSON dashboard endpoint. Defaults to +true+ in development.
26
+ # @return [Boolean]
27
+
28
+ # @!attribute min_allocated_objects
29
+ # Skip recording requests that allocate fewer objects than this threshold.
30
+ # @return [Integer]
31
+
32
+ # @!attribute ignore_paths
33
+ # Paths to skip entirely. Accepts strings (prefix match) or Regexps.
34
+ # The engine's own mount path is always ignored regardless of this setting.
35
+ # @return [Array<String, Regexp>]
36
+
37
+ # @!attribute ignore_controllers
38
+ # Controller names to skip (matched against the Rails controller name string).
39
+ # @return [Array<String>]
40
+
41
+ # @!attribute detailed_reports
42
+ # Capture full +MemoryProfiler.report+ breakdowns (by gem, class, file, location).
43
+ # Requires +gem "memory_profiler"+ in the host app's Gemfile.
44
+ # @return [Boolean]
45
+
46
+ # @!attribute detailed_sample_rate
47
+ # When {#detailed_reports} is enabled, capture a full report every Nth profiled request.
48
+ # @return [Integer]
49
+
50
+ # @!attribute raise_on_allocation_spike
51
+ # Raise {RailsMemoryProfiler::AllocationSpikeError} when allocated objects exceed this value.
52
+ # Set to +nil+ to disable. Intended for use in test environments.
53
+ # @return [Integer, nil]
54
+
55
+ # @!attribute notifiers
56
+ # Array of notifier objects, each responding to +#call(report)+. Called after
57
+ # each report is stored. Built-in options: {Notifiers::Logger}, {Notifiers::Stdout},
58
+ # {Notifiers::Console}, {Notifiers::FileLogger}.
59
+ # @return [Array<#call>]
60
+
61
+ # @!attribute log_file
62
+ # Path to a file where reports are appended as JSON lines. Shortcut for
63
+ # configuring {Notifiers::FileLogger} manually.
64
+ # @return [String, nil]
65
+
3
66
  attr_accessor :enabled, :sample_rate, :store_size, :dashboard_enabled,
4
67
  :min_allocated_objects, :ignore_paths, :ignore_controllers,
5
68
  :detailed_reports, :detailed_sample_rate,
@@ -6,18 +6,35 @@ module RailsMemoryProfiler
6
6
  isolate_namespace RailsMemoryProfiler
7
7
  config.generators.api_only = true
8
8
 
9
+ MOUNT_PATH_MUTEX = Mutex.new
10
+ private_constant :MOUNT_PATH_MUTEX
11
+
9
12
  class << self
13
+ # Returns the path at which the engine is mounted in the host application,
14
+ # e.g. +"/rails/memory"+. Detected lazily on first call by scanning
15
+ # +Rails.application.routes+, then cached. Returns +nil+ if the engine is
16
+ # not mounted.
17
+ #
18
+ # @return [String, nil]
10
19
  def mount_path
11
- @mount_path ||= begin
12
- mounted = Rails.application.routes.routes.find do |route|
13
- route.app.app == self rescue false
20
+ return @mount_path if @mount_path
21
+
22
+ MOUNT_PATH_MUTEX.synchronize do
23
+ @mount_path ||= begin
24
+ mounted = Rails.application.routes.routes.find do |route|
25
+ route.app.app == self rescue false
26
+ end
27
+ mounted&.path&.spec&.to_s&.gsub(/\([^)]*\)/, "")
14
28
  end
15
- mounted&.path&.spec&.to_s&.gsub(/\([^)]*\)/, "")
16
29
  end
17
30
  end
18
31
 
32
+ # Clears the cached mount path so it will be re-detected on the next call.
33
+ # Primarily used in tests.
34
+ #
35
+ # @return [void]
19
36
  def reset_mount_path!
20
- @mount_path = nil
37
+ MOUNT_PATH_MUTEX.synchronize { @mount_path = nil }
21
38
  end
22
39
  end
23
40
 
@@ -1,5 +1,14 @@
1
1
  module RailsMemoryProfiler
2
+ # Rack middleware that profiles each request and pushes a report into {ReportStore}.
3
+ # Registered automatically by the {Engine} initializer; you do not need to add it
4
+ # to the middleware stack manually.
5
+ #
6
+ # Profiling is skipped when:
7
+ # - {Configuration#enabled} is +false+
8
+ # - the request path matches {Configuration#ignore_paths} or the engine mount path
9
+ # - the request is not selected by {Configuration#sample_rate}
2
10
  class Middleware
11
+ # @param app [#call] the next Rack application in the stack
3
12
  def initialize(app)
4
13
  @app = app
5
14
  @request_count = 0
@@ -7,6 +16,10 @@ module RailsMemoryProfiler
7
16
  @mutex = Mutex.new
8
17
  end
9
18
 
19
+ # Profiles the request if applicable and delegates to the inner app.
20
+ #
21
+ # @param env [Hash] Rack environment
22
+ # @return [Array] Rack response triplet
10
23
  def call(env)
11
24
  return @app.call(env) unless RailsMemoryProfiler.config.enabled
12
25
  return @app.call(env) if ignored_path?(env["PATH_INFO"])
@@ -1,6 +1,25 @@
1
1
  module RailsMemoryProfiler
2
+ # Thread-safe circular buffer that holds per-request profiling reports.
3
+ # Capacity is controlled by {Configuration#store_size}. When full, the oldest
4
+ # report is evicted to make room for each new one.
5
+ #
6
+ # Each report is a Hash with the keys:
7
+ # - +:id+ — unique hex string assigned on push
8
+ # - +:path+, +:method+, +:controller+, +:action+
9
+ # - +:allocated_objects+, +:retained_objects+
10
+ # - +:duration_ms+
11
+ # - +:recorded_at+ — +Time+ of capture
12
+ # - +:detail+ — optional breakdown Hash (present when {Configuration#detailed_reports} is enabled)
2
13
  module ReportStore
14
+ MUTEX = Mutex.new
15
+ private_constant :MUTEX
16
+
3
17
  class << self
18
+ # Adds a report to the buffer, assigning a unique +:id+ key.
19
+ # Evicts the oldest entry when the buffer is at capacity.
20
+ #
21
+ # @param report [Hash] profiling data without an +:id+ key
22
+ # @return [void]
4
23
  def push(report)
5
24
  mutex.synchronize do
6
25
  ensure_buffer_size
@@ -10,10 +29,17 @@ module RailsMemoryProfiler
10
29
  end
11
30
  end
12
31
 
32
+ # Returns the report with the given id, or +nil+ if not found or evicted.
33
+ #
34
+ # @param id [String]
35
+ # @return [Hash, nil]
13
36
  def find(id)
14
37
  all.find { |r| r[:id] == id }
15
38
  end
16
39
 
40
+ # Returns all stored reports in insertion order (oldest first).
41
+ #
42
+ # @return [Array<Hash>]
17
43
  def all
18
44
  mutex.synchronize do
19
45
  stored = @stored || 0
@@ -27,10 +53,16 @@ module RailsMemoryProfiler
27
53
  end
28
54
  end
29
55
 
56
+ # Removes all stored reports.
57
+ #
58
+ # @return [void]
30
59
  def clear
31
60
  mutex.synchronize { reset! }
32
61
  end
33
62
 
63
+ # Returns the number of reports currently stored.
64
+ #
65
+ # @return [Integer]
34
66
  def size
35
67
  mutex.synchronize { @stored || 0 }
36
68
  end
@@ -38,7 +70,7 @@ module RailsMemoryProfiler
38
70
  private
39
71
 
40
72
  def mutex
41
- @mutex ||= Mutex.new
73
+ MUTEX
42
74
  end
43
75
 
44
76
  def capacity
@@ -1,9 +1,25 @@
1
1
  require "rails_memory_profiler"
2
2
 
3
3
  module RailsMemoryProfiler
4
+ # Utility methods for asserting allocation counts in tests.
5
+ # Not auto-required — opt in with:
6
+ #
7
+ # require "rails_memory_profiler/test_helper"
8
+ #
9
+ # Works as a module-level call or mixed into a test class via +include+/+extend+:
10
+ #
11
+ # include RailsMemoryProfiler::TestHelper
12
+ #
13
+ # For RSpec block expectations see {file:lib/rails_memory_profiler/rspec_matchers.rb}.
14
+ # For Minitest assertions see {file:lib/rails_memory_profiler/minitest_matchers.rb}.
4
15
  module TestHelper
5
16
  extend self
6
17
 
18
+ # Runs the block and returns the number of Ruby objects allocated during it.
19
+ # Triggers a full GC before measuring to reduce noise from prior allocations.
20
+ #
21
+ # @yieldreturn [Object] the block's return value is discarded
22
+ # @return [Integer] number of objects allocated
7
23
  def capture_allocations
8
24
  GC.start
9
25
  before = GC.stat(:total_allocated_objects)
@@ -11,6 +27,15 @@ module RailsMemoryProfiler
11
27
  GC.stat(:total_allocated_objects) - before
12
28
  end
13
29
 
30
+ # Asserts that the block allocates fewer than +threshold+ objects.
31
+ # Raises +Minitest::Assertion+ when Minitest is loaded (so the failure is
32
+ # reported as a test failure rather than an error), otherwise raises +RuntimeError+.
33
+ #
34
+ # @param threshold [Integer] maximum acceptable allocation count
35
+ # @yieldreturn [Object] the block's return value is discarded
36
+ # @raise [Minitest::Assertion] when Minitest is present and the threshold is exceeded
37
+ # @raise [RuntimeError] when Minitest is absent and the threshold is exceeded
38
+ # @return [void]
14
39
  def assert_allocations_below(threshold, &block)
15
40
  count = capture_allocations(&block)
16
41
  return if count < threshold
@@ -1,3 +1,3 @@
1
1
  module RailsMemoryProfiler
2
- VERSION = "0.5.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -5,20 +5,54 @@ require "rails_memory_profiler/notifiers"
5
5
  require "rails_memory_profiler/middleware"
6
6
  require "rails_memory_profiler/engine"
7
7
 
8
+ # RailsMemoryProfiler is a Rails engine that captures per-request object
9
+ # allocations via GC.stat diffs and serves them through a mountable dashboard.
10
+ #
11
+ # @example Mounting the engine
12
+ # # config/routes.rb
13
+ # mount RailsMemoryProfiler::Engine, at: "/rails/memory"
14
+ #
15
+ # @example Configuring the gem
16
+ # RailsMemoryProfiler.configure do |config|
17
+ # config.sample_rate = 5
18
+ # config.min_allocated_objects = 1_000
19
+ # end
8
20
  module RailsMemoryProfiler
21
+ # Raised by the middleware when allocated objects exceed
22
+ # {Configuration#raise_on_allocation_spike}.
9
23
  class AllocationSpikeError < StandardError; end
10
24
 
11
25
  class << self
26
+ # Yields the {Configuration} instance for block-style setup.
27
+ #
28
+ # @yieldparam config [Configuration]
29
+ # @return [void]
12
30
  def configure
13
31
  yield config
14
32
  end
15
33
 
34
+ # Returns the shared {Configuration} instance, creating it on first call.
35
+ #
36
+ # @return [Configuration]
16
37
  def config
17
38
  @config ||= Configuration.new
18
39
  end
19
40
 
41
+ # Replaces the shared {Configuration} instance with a fresh default.
42
+ # Primarily used in tests.
43
+ #
44
+ # @return [Configuration]
20
45
  def reset_config!
21
46
  @config = Configuration.new
22
47
  end
48
+
49
+ # Returns a memoized +ActiveSupport::Deprecation+ instance scoped to this
50
+ # gem. Use it to emit deprecation warnings for any future breaking changes
51
+ # rather than calling +warn+ directly.
52
+ #
53
+ # @return [ActiveSupport::Deprecation]
54
+ def deprecator
55
+ @deprecator ||= ActiveSupport::Deprecation.new("1.0.0", "RailsMemoryProfiler")
56
+ end
23
57
  end
24
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_memory_profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -74,6 +74,7 @@ files:
74
74
  - app/controllers/rails_memory_profiler/base_controller.rb
75
75
  - app/controllers/rails_memory_profiler/comparisons_controller.rb
76
76
  - app/controllers/rails_memory_profiler/reports_controller.rb
77
+ - app/controllers/rails_memory_profiler/store_controller.rb
77
78
  - app/helpers/rails_memory_profiler/application_helper.rb
78
79
  - app/javascript/rails_memory_profiler/application.js
79
80
  - app/javascript/rails_memory_profiler/controllers/application.js