require-profiler 0.2.2 → 0.3.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: d9031019da202ea4bf4ba530de358c6759bb8417de3f96047a138a9d262c0ff0
4
- data.tar.gz: e51c7eb92cc7419bcdd396103a0565db26e9cf2fd97013577ffcc8b5b127a337
3
+ metadata.gz: 9f7525e2f089705293ecb70a35d28837fb45c144b9c58861c9904d8b58a9e49a
4
+ data.tar.gz: 8a5e2acb8665b0bb7d665d91d0598d16c61d7488f7d40bed23cc1afe03559e7a
5
5
  SHA512:
6
- metadata.gz: cc3d71a84a0faac6f801a9836dc0e0b3c3c6c3d694275f709f8a399229c8a1ee5cb8fe3b9ea4f1c311e04f93e0ffb3c28f3462ce6c582888e155da0a64b469d6
7
- data.tar.gz: d16347ab1249f15e34f325f11f0505f17985c877bf2808b3e1727bff31c4369d95791032546f5f987034f1061ec95a774f1788646afc5f8c8b010374b26da1cc
6
+ metadata.gz: ef22d5d6c1a8d98bc7418802be6b95f9a84e82336e4f1de1681cddfba631ca187c504ccf343d2ea25832b46ca1a6c00bd411996bb8c77315a6f0311a4c5895dd
7
+ data.tar.gz: ae0f1a579693c2242f78eab1f4898e94ed54701ff8d3cc4d080b534791f9043206da1d89ef2da58cf5dcd67fc27404bcf0e2b0aae15d14e67d9f2a3a8c3a0812
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.3.0 (2026-07-23)
6
+
7
+ - Add Rails initialization tracking: railtie initializers, `to_prepare` callbacks, and lazy load hooks. ([@ardecvz][])
8
+
9
+ - Prefix HTTP nodes with `http:`. ([@ardecvz][])
10
+
11
+ ## 0.2.3 (2026-06-05)
12
+
13
+ - Add SKILL.md for rails-hyperdrive. ([@palkan][])
14
+
5
15
  ## 0.2.2 (2026-06-05)
6
16
 
7
17
  - Fixed YAML tracking when Pathname is used. ([@palkan][])
data/README.md CHANGED
@@ -7,8 +7,9 @@ Require Profiler is a tool for profiling Ruby's code loading—`Kernel#require`,
7
7
 
8
8
  It's built on top of [Require Hooks][require-hooks], so it works anywhere Require Hooks does (MRI/JRuby/TruffleRuby).
9
9
 
10
- <a href="https://evilmartians.com/">
11
- <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
10
+ <br/>
11
+
12
+ <img src="https://cdn.evilmartians.com/badges/logo-no-label.svg" alt="Evil Martians logo" width="22" height="16" /> <b>Require Profiler</b> is built by <b><a href="https://evilmartians.com/">Evil Martians</a></b>, an American design and engineering consultancy for <b>developer tools, AI, and cybersecurity startups</b>.
12
13
 
13
14
  ## Installation
14
15
 
@@ -112,6 +113,14 @@ Now you can use Speedscope to dig deeper.
112
113
 
113
114
  Require Profiler also captures HTTP requests and YAML file loading and add them to the profile, so you can find which Ruby files trigger the corresponding actions on load. NOTE: For HTTP requests tracking, you MUST add [sniffer][] gem to your Gemfile.
114
115
 
116
+ Disable with `REQUIRE_PROFILER_YAML=false` or `REQUIRE_PROFILER_HTTP=false`.
117
+
118
+ ### Rails integration
119
+
120
+ When Rails is loaded, Require Profiler automatically captures its initialization - railtie initializers, `to_prepare` callbacks, and lazy load hooks - and adds them to the profile as `initializer:`, `to_prepare:`, and `load_hook:` nodes, so you can find the slow-running blocks, which are usually invisible when profiling code loading alone.
121
+
122
+ Disable with `REQUIRE_PROFILER_RAILS=false` (or set a threshold if the extra nodes add too much noise).
123
+
115
124
  ### Configuration
116
125
 
117
126
  `RequireProfiler.start` accepts the following keyword arguments:
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RequireProfiler
4
+ # Use TracePoint to run a callback when a particular class is defined
5
+ module Patcher
6
+ class << self
7
+ def on_load(name, &callback)
8
+ return callback.call if Object.const_defined?(name)
9
+
10
+ callbacks[name] = callback
11
+
12
+ tracer.enable
13
+ end
14
+
15
+ private
16
+
17
+ def callbacks = @callbacks ||= {}
18
+
19
+ def tracer = @tracer ||= TracePoint.new(:end, &method(:on_class))
20
+
21
+ def name_method = @name_method ||= Module.instance_method(:name)
22
+
23
+ def on_class(event)
24
+ return if event.self.singleton_class?
25
+
26
+ class_name = name_method.bind_call(event.self)
27
+ return unless callbacks[class_name]
28
+
29
+ callback = callbacks.delete(class_name)
30
+ tracer.disable if callbacks.empty?
31
+
32
+ callback.call
33
+ end
34
+ end
35
+ end
36
+ end
@@ -19,7 +19,7 @@ module RequireProfiler
19
19
 
20
20
  Sniffer::DataItem::Request.include(Module.new do
21
21
  def require_path
22
- @url ||= "#{method.to_s.upcase}:#{(port == 443) ? "https" : "http"}://#{host}#{query}"
22
+ @url ||= "http:#{method.to_s.upcase}:#{(port == 443) ? "https" : "http"}://#{host}#{query}"
23
23
  end
24
24
  end)
25
25
 
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RequireProfiler
4
+ module Plugins
5
+ # Track Rails initialization: railtie initializers, to_prepare callbacks, and load hooks
6
+ class RailsPlugin < Base
7
+ module InitializerPatch
8
+ def run(...)
9
+ RailsPlugin.track(:initializer, name, block) { super }
10
+ end
11
+ end
12
+
13
+ module ToPreparePatch
14
+ def to_prepare(*args, &block)
15
+ return super unless block
16
+
17
+ super do |*prepare_args|
18
+ RailsPlugin.track(:to_prepare, nil, block) do
19
+ instance_exec(*prepare_args, &block)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ module LoadHookPatch
26
+ private
27
+
28
+ def execute_hook(name, base, options, block)
29
+ RailsPlugin.track(:load_hook, name, block) { super }
30
+ end
31
+ end
32
+
33
+ class << self
34
+ attr_accessor :reporter
35
+
36
+ def track(kind, name, block)
37
+ path = label(kind, name, block)
38
+ reporter.handle_event(Reporter::Event.new(type: :start, kind: :rails, path:))
39
+ start = Time.now
40
+ yield
41
+ ensure
42
+ time = Time.now - start
43
+ reporter.handle_event(Reporter::Event.new(type: :end, path:, time:))
44
+ end
45
+
46
+ def label(kind, name, block)
47
+ ["rails:#{kind}", name, block&.source_location&.join(":")].compact.join(":")
48
+ end
49
+ end
50
+
51
+ def activate!
52
+ RailsPlugin.reporter = reporter
53
+
54
+ Patcher.on_load("Rails::Initializable::Initializer") do
55
+ ::Rails::Initializable::Initializer.prepend(InitializerPatch)
56
+ end
57
+ Patcher.on_load("ActiveSupport::Reloader") do
58
+ ::ActiveSupport::Reloader.singleton_class.prepend(ToPreparePatch)
59
+ end
60
+ Patcher.on_load("ActiveSupport::LazyLoadHooks") do
61
+ ::ActiveSupport.singleton_class.prepend(LoadHookPatch)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -6,6 +6,7 @@ module RequireProfiler
6
6
  def register_reporter(reporter)
7
7
  HTTPPlugin.new(reporter).activate! unless ENV["REQUIRE_PROFILER_HTTP"] == "false"
8
8
  YAMLPlugin.new(reporter).activate! unless ENV["REQUIRE_PROFILER_YAML"] == "false"
9
+ RailsPlugin.new(reporter).activate! unless ENV["REQUIRE_PROFILER_RAILS"] == "false"
9
10
  end
10
11
  end
11
12
 
@@ -19,5 +20,6 @@ module RequireProfiler
19
20
 
20
21
  autoload :HTTPPlugin, "require_profiler/plugins/http_plugin"
21
22
  autoload :YAMLPlugin, "require_profiler/plugins/yaml_plugin"
23
+ autoload :RailsPlugin, "require_profiler/plugins/rails_plugin"
22
24
  end
23
25
  end
@@ -4,13 +4,22 @@ module RequireProfiler
4
4
  module Printer
5
5
  # CallStack formatter prints collapsed stacks (Brendan Gregg's format)
6
6
  class CallStack < Base
7
+ # Rails labels are "rails:#{type}:#{detail}", e.g. "rails:initializer:name:file:line"
8
+ RAILS_LABEL_PARTS = 3
9
+ RAILS_TYPE_INDEX = 1
10
+
7
11
  def flush(node, parts: [])
8
12
  return unless flush?(node)
9
13
 
10
- path = node.path.sub(prefix_stripper, "")
11
- self_parts = (node.kind == :path) ? path.split("/") : [path]
14
+ path = strip_prefix(node.path)
15
+ self_parts =
16
+ case node.kind
17
+ when :rails then rails_frames(path)
18
+ when :path then path_frames(path)
19
+ else [path]
20
+ end
12
21
 
13
- parts += self_parts.size.times.map { self_parts.take(_1 + 1).join("/") }
22
+ parts += self_parts
14
23
  # We only show self-time, so exclude children
15
24
  val = ((node.time - node.children.sum(&:time)) * 1000).round(3)
16
25
 
@@ -18,6 +27,17 @@ module RequireProfiler
18
27
 
19
28
  node.children.each { flush(_1, parts:) }
20
29
  end
30
+
31
+ private
32
+
33
+ def rails_frames(path)
34
+ ["rails:#{path.split(":", RAILS_LABEL_PARTS)[RAILS_TYPE_INDEX]}", path]
35
+ end
36
+
37
+ def path_frames(path)
38
+ segments = path.split("/")
39
+ segments.size.times.map { segments.take(_1 + 1).join("/") }
40
+ end
21
41
  end
22
42
  end
23
43
  end
@@ -8,7 +8,7 @@ module RequireProfiler
8
8
  def flush(node, indent: 0)
9
9
  return unless flush?(node)
10
10
 
11
- path = node.path.sub(prefix_stripper, "")
11
+ path = strip_prefix(node.path)
12
12
  output << "#{PAD * indent}#{path} — #{time_to_duration(node.time)}\n"
13
13
  node.children.each { flush(_1, indent: indent + 1) }
14
14
 
@@ -16,7 +16,7 @@ module RequireProfiler
16
16
  prefixes << ::Gem.dir if defined?(::Gem.dir)
17
17
  prefixes << ::Bundler.bundle_path if defined?(::Bundler.bundle_path)
18
18
 
19
- @prefix_stripper = %r{^(#{prefixes.join("|")})/}
19
+ @prefix_stripper = %r{(^|:)(#{prefixes.join("|")})/}
20
20
  end
21
21
 
22
22
  def flush(node)
@@ -29,6 +29,10 @@ module RequireProfiler
29
29
 
30
30
  private
31
31
 
32
+ def strip_prefix(path)
33
+ path.sub(prefix_stripper, '\1')
34
+ end
35
+
32
36
  def flush?(node)
33
37
  return false unless (node.time * 1000) >= threshold
34
38
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RequireProfiler # :nodoc:
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -4,6 +4,7 @@ module RequireProfiler
4
4
  autoload :Reporter, "require_profiler/reporter"
5
5
  autoload :Printer, "require_profiler/printer"
6
6
  autoload :Plugins, "require_profiler/plugins"
7
+ autoload :Patcher, "require_profiler/patcher"
7
8
 
8
9
  # Autoload doesn't work here, because we call it from the hooks for the first time
9
10
  require "require_profiler/ruby_profiling"
@@ -167,21 +167,67 @@ This emits one line per stack in Brendan Gregg's collapsed format with per-frame
167
167
  | `REQUIRE_PROFILER_HTTP` | Disable HTTP tracking when set to `false` | `false` |
168
168
  | `REQUIRE_PROFILER_PLUGINS` | Disable all plugins when set to `false` | `false` |
169
169
 
170
+ ## Important: Use the Profiler's Built-in Filtering
171
+
172
+ **Prefer profiler's built-in filtering and searching capabilities over `grep`, `tail`, `head`, `awk`, or `sed` to filter or search results.**:
173
+
174
+ - To find slow files → use `REQUIRE_PROFILE_THRESHOLD`, not `grep` for timing patterns
175
+ - To investigate a specific gem or file → use `REQUIRE_PROFILE_FOCUS`, not `grep` for the name
176
+ - To reduce output size → use `REQUIRE_PROFILE_THRESHOLD` + `REQUIRE_PROFILE_FOCUS`, not `tail -N | head -M`
177
+
178
+ Shell-based filtering breaks the indented tree structure (you lose parent-child relationships), misses context, and requires re-running the full profile each time you change the filter. The built-in env vars preserve the tree, show ancestor chains, and handle edge cases the profiler already knows about.
179
+
180
+ The only acceptable uses of piping are:
181
+ - `| wc -l` to count total loaded files in step 1
182
+ - Piping into a file when `REQUIRE_PROFILE_PATH` is not available
183
+
170
184
  ## Recommended Agent Workflow
171
185
 
172
186
  Follow this sequence when a user asks about slow boot time:
173
187
 
174
188
  1. **Get a baseline.** Run the full profile command and note the total boot time (the top-level entry's duration) and total file count (`| wc -l`).
175
189
 
176
- 2. **Find the top offenders.** Re-run with `REQUIRE_PROFILE_THRESHOLD=100` (or adjust based on total time) to surface only slow files. Report the top 5-10 slowest entries to the user.
190
+ ```sh
191
+ bundle exec ruby -W0 -r./config/boot -require-prof config/environment.rb
192
+ ```
193
+
194
+ 2. **Find the top offenders.** Re-run with `REQUIRE_PROFILE_THRESHOLD` to surface only slow files. Start with a threshold that shows roughly 10-20 entries (e.g., if total boot is ~4s, try 100ms; if ~1s, try 30ms). Report the top slowest entries to the user.
195
+
196
+ ```sh
197
+ REQUIRE_PROFILE_THRESHOLD=100 bundle exec ruby -W0 -r./config/boot -require-prof config/environment.rb
198
+ ```
177
199
 
178
- 3. **Investigate specific areas.** If the user suspects a particular gem or area, use `REQUIRE_PROFILE_FOCUS` to zoom in. Otherwise, focus on the slowest entries from step 2.
200
+ 3. **Investigate specific areas.** Use `REQUIRE_PROFILE_FOCUS` to zoom into a gem, initializer, or file. The focus pattern is a regexp — use it to match file paths, gem names, or directory patterns. Combine with `REQUIRE_PROFILE_THRESHOLD` for precision.
179
201
 
180
- 4. **Check for boot-time side effects.** Look at YAML and HTTP entries in the profile. HTTP calls during boot are almost always worth investigating — they add latency and can fail. YAML loads of large files can also be significant.
202
+ ```sh
203
+ # Investigate a specific gem
204
+ REQUIRE_PROFILE_FOCUS="stripe" bundle exec ruby -W0 -r./config/boot -require-prof config/environment.rb
205
+
206
+ # Investigate all initializers that took > 50ms
207
+ REQUIRE_PROFILE_THRESHOLD=50 REQUIRE_PROFILE_FOCUS="initializers" bundle exec ruby -W0 -r./config/boot -require-prof config/environment.rb
208
+ ```
209
+
210
+ 4. **Check for boot-time side effects.** YAML and HTTP entries appear in the profile tree by default. HTTP calls during boot are almost always worth investigating — they add latency and can fail. Use `REQUIRE_PROFILE_FOCUS` to find them:
211
+
212
+ ```sh
213
+ # Find YAML loading
214
+ REQUIRE_PROFILE_FOCUS="\.yml" bundle exec ruby -W0 -r./config/boot -require-prof config/environment.rb
215
+
216
+ # Find HTTP calls during boot
217
+ REQUIRE_PROFILE_FOCUS="(GET|POST|PATCH|DELETE):" bundle exec ruby -W0 -r./config/boot -require-prof config/environment.rb
218
+ ```
181
219
 
182
220
  5. **Deep-dive when needed.** For files that are unexpectedly slow (the load time seems too high for what the file does), use `REQUIRE_PROFILE_STACKPROF` to generate a Stackprof profile and identify what's happening inside that file.
183
221
 
184
- 6. **Generate a JSON profile for handoff.** If the user wants to explore the data themselves, or if the profile is too large to analyze in text, export to JSON with `REQUIRE_PROFILE_PATH=tmp/require-profile.json` and point them to Speedscope.
222
+ ```sh
223
+ REQUIRE_PROFILE_STACKPROF=config/initializers/stripe.rb bundle exec ruby -W0 -r./config/boot -require-prof config/environment.rb
224
+ ```
225
+
226
+ 6. **Generate a JSON profile for handoff.** If the user wants to explore the data themselves, or if the profile is too large to analyze in text, export to JSON and point them to Speedscope.
227
+
228
+ ```sh
229
+ REQUIRE_PROFILE_PATH=tmp/require-profile.json bundle exec ruby -W0 -r./config/boot -require-prof config/environment.rb
230
+ ```
185
231
 
186
232
  7. **Suggest actionable improvements** based on findings:
187
233
  - Move heavy gem requires behind lazy loading or autoload
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: require-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: railties
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '7.2'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '7.2'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: rake
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -134,9 +148,10 @@ files:
134
148
  - lib/equire-prof.rb
135
149
  - lib/require-profiler.rb
136
150
  - lib/require_profiler.rb
137
- - lib/require_profiler/hyperdrive/skills/rails-boot-profiling/SKILL.md
151
+ - lib/require_profiler/patcher.rb
138
152
  - lib/require_profiler/plugins.rb
139
153
  - lib/require_profiler/plugins/http_plugin.rb
154
+ - lib/require_profiler/plugins/rails_plugin.rb
140
155
  - lib/require_profiler/plugins/yaml_plugin.rb
141
156
  - lib/require_profiler/printer.rb
142
157
  - lib/require_profiler/printer/call_stack.rb
@@ -145,6 +160,7 @@ files:
145
160
  - lib/require_profiler/reporter.rb
146
161
  - lib/require_profiler/ruby_profiling.rb
147
162
  - lib/require_profiler/version.rb
163
+ - skills/rails-boot-profiling/SKILL.md
148
164
  homepage: https://github.com/palkan/require-profiler
149
165
  licenses:
150
166
  - MIT
@@ -156,6 +172,7 @@ metadata:
156
172
  source_code_uri: https://github.com/palkan/require-profiler
157
173
  hyperdrive_targets: railties
158
174
  hyperdrive_artifacts: skill
175
+ hyperdrive_skills_dir: "./skills"
159
176
  rdoc_options: []
160
177
  require_paths:
161
178
  - lib
@@ -170,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
187
  - !ruby/object:Gem::Version
171
188
  version: '0'
172
189
  requirements: []
173
- rubygems_version: 3.6.9
190
+ rubygems_version: 4.0.16
174
191
  specification_version: 4
175
192
  summary: 'Profile Ruby #require/#load/etc calls'
176
193
  test_files: []