require-profiler 0.1.1 → 0.2.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: 4387c62f9714573c234aafbbf2254c009341a3e553677458f3ab633d898ec0a7
4
- data.tar.gz: 7ada2f5d9e5020c0b5a6199165df40346cf5514fc68a245451539895da04b6fc
3
+ metadata.gz: 787aa181e1f018e66c9f8163837af8980a0549a23102361206451ca4c640675e
4
+ data.tar.gz: a7a0a01e40938642a7bd2b505ae18e47e42b3f00fa482c63151c5616d8f746d6
5
5
  SHA512:
6
- metadata.gz: 978f991474241a6b408fa2f9bf8ca4370c55900ed763b34cac82a950c1b5a8ae4e740c1dc9f62a6498cc8295ebea05caf9b1c10a1ea9ee1c69c24a22a4ad0166
7
- data.tar.gz: 75185c6c5569be0bc835c64fabfea8846b50e05e5194a8124eb6d329c5ff56516fd2761cd49563ee92ed610853fb5a494e60ffd4d3d5998fbbab1adccce6f64f
6
+ metadata.gz: f99d748f75f3333f90979bfde8082591bb77e6b56590df2424dc566ff556798a50660d2c125d4b261c37235b0cdc95ff7cf228f146bd1e8f27d88af5498d90a3
7
+ data.tar.gz: b0b8aa77547e9852c91f699944877450aa03d125f4324b2efc5ea59854c293680215ee8bf272eda892a7c90c33ee5422bdecbeb3c6cf4530e2389a0aa1da8e05
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.2.0 (2026-05-25) 🔔
6
+
7
+ - Add ability to run Stackprof for a particular file loading. ([@palkan][])
8
+
9
+ - Add focus support (to include only matching files). ([@palkan][])
10
+
11
+ - Add threshold support (`REQUIRE_PROFILE_THRESHOLD`). ([@palkan][])
12
+
13
+ - Add `-require-prof` support. ([@palkan][])
14
+
15
+ - Support passing format and output path via `REQUIRE_PROFILE_FORMAT` and `REQUIRE_PROFILE_PATH`. ([@palkan][])
16
+
5
17
  ## 0.1.1 (2026-04-29)
6
18
 
7
19
  - Fix JSON printer leaving trailing call-stack bytes in output. ([@ardecvz][])
data/README.md CHANGED
@@ -24,6 +24,48 @@ gem "require-profiler"
24
24
 
25
25
  ## Usage
26
26
 
27
+ ### Using `-require-prof` command switch
28
+
29
+ Given that `require-profiler` is a part of your bundle (or available globally), you can enable it as follows:
30
+
31
+ ```sh
32
+ # Without Bundler
33
+ ruby -require-prof some_ruby_script.rb
34
+
35
+ # With Bundler
36
+ ruby -rbundler/setup -require-prof some_ruby_script.rb
37
+ ```
38
+
39
+ For Rails applications, the command will look like:
40
+
41
+ ```sh
42
+ bundle exec ruby -r./config/boot -require-prof config/environment.rb
43
+ ```
44
+
45
+ We load the `config/boot.rb` file first to set up Bundler and **Bootsnap** (it must be required before require-hooks).
46
+
47
+ You can use environment variables to specify the output format and path. For example:
48
+
49
+ ```sh
50
+ REQUIRE_PROFILE_PATH=tmp/require-prof.json bundle exec ruby -r./config/boot -require-prof config/environment.rb
51
+ ```
52
+
53
+ You can also specify the threshold to display only files taking at least the provided number of milliseconds to load:
54
+
55
+ ```sh
56
+ REQUIRE_PROFILE_THRESHOLD=100 bundle exec ruby -r./config/boot -require-prof config/environment.rb
57
+ ```
58
+
59
+ Or file matching the provided _focus_ pattern:
60
+
61
+ ```sh
62
+ REQUIRE_PROFILE_FOCUS=stripe bundle exec ruby -r./config/boot -require-prof config/environment.rb
63
+ ```
64
+
65
+ ### Programmable usage
66
+
67
+ If you code loading process is more complicated, you can manually start and stop the profiler from your application.
68
+
27
69
  Wrap the code you want to profile with `RequireProfiler.start` and `RequireProfiler.stop`:
28
70
 
29
71
  ```ruby
@@ -49,12 +91,30 @@ RequireProfiler.start(
49
91
  )
50
92
  ```
51
93
 
52
- ### Output
94
+ ### Ruby profilers integration
95
+
96
+ Whenever you identified the files that take a long time to load, it's useful to look at them closer and profile the load process using some generic profiler. Require Profiler simplifies this down to the environment variable usage. This is, for example, how you can profile the loading of an initializer file via StackProf:
97
+
98
+ ```sh
99
+ $ REQUIRE_PROFILE_STACKPROF=config/initializers/fragment.rb REQUIRE_PROFILE_THRESHOLD=100 be ruby -r./config/boot -require-prof config/environment.rb
100
+
101
+ ...
102
+ Stackprof JSON profile for config/initializers/fragment.rb is generated: config-initializers-fragment-stackprof.json
103
+ ...
104
+
105
+ ```
106
+
107
+ Now you can use Speedscope to dig deeper.
108
+
109
+ **NOTE:** The `stackprof` gem must be present in your Gemfile for that.
110
+
111
+ ### Configuration
53
112
 
54
113
  `RequireProfiler.start` accepts the following keyword arguments:
55
114
 
56
115
  - `output:` — `$stdout` (default), any IO-like object, or a file path (string).
57
- - `format:` — `:text` (default), `:call_stack`, or `:json`. When `output:` is a file path with a `.json` extension, the JSON format is picked automatically.
116
+ - `format:` — `:text` (default), `:call_stack`, or `:json`. When `output:` is a file path with a `.json` extension, the JSON format is picked automatically. You can also provide format via the `REQUIRE_PROFILE_FORMAT` env var.
117
+ - `threshold:` — the number of **milliseconds** to use as a threshold to ignore files taking less to load, float. Default is 0.0
58
118
  - `patterns:` / `exclude_patterns:` — see above.
59
119
 
60
120
  ## Output formats
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "require_profiler"
4
+
5
+ RequireProfiler.start
6
+ at_exit { RequireProfiler.stop }
@@ -5,6 +5,8 @@ module RequireProfiler
5
5
  # CallStack formatter prints collapsed stacks (Brendan Gregg's format)
6
6
  class CallStack < Base
7
7
  def flush(node, parts: [])
8
+ return unless flush?(node)
9
+
8
10
  path = node.path.sub(prefix_stripper, "")
9
11
  self_parts = path.split("/")
10
12
 
@@ -6,6 +6,8 @@ module RequireProfiler
6
6
  PAD = " "
7
7
 
8
8
  def flush(node, indent: 0)
9
+ return unless flush?(node)
10
+
9
11
  path = node.path.sub(prefix_stripper, "")
10
12
  output << "#{PAD * indent}#{path} — #{time_to_duration(node.time)}\n"
11
13
  node.children.each { flush(_1, indent: indent + 1) }
@@ -3,11 +3,13 @@
3
3
  module RequireProfiler
4
4
  module Printer
5
5
  class Base
6
- private attr_reader :output
6
+ private attr_reader :output, :threshold, :focus
7
7
  private attr_reader :prefix_stripper
8
8
 
9
- def initialize(output)
9
+ def initialize(output, threshold: 0.0, focus: nil)
10
10
  @output = output
11
+ @threshold = threshold
12
+ @focus = focus
11
13
 
12
14
  # Identify prefixes for the project and the gems
13
15
  prefixes = [::Dir.pwd]
@@ -24,6 +26,16 @@ module RequireProfiler
24
26
  def finish
25
27
  output.close if output.respond_to?(:close) && output != $stdout
26
28
  end
29
+
30
+ private
31
+
32
+ def flush?(node)
33
+ return false unless (node.time * 1000) >= threshold
34
+
35
+ return false if focus && !node.focused
36
+
37
+ true
38
+ end
27
39
  end
28
40
 
29
41
  autoload :Text, "require_profiler/printer/text"
@@ -31,14 +43,14 @@ module RequireProfiler
31
43
  autoload :JSON, "require_profiler/printer/json"
32
44
 
33
45
  class << self
34
- def resolve(output, format)
46
+ def resolve(output, format, **opts)
35
47
  format ||= (output.is_a?(String) && File.extname(output) == ".json") ? :json : :text
36
48
  output = File.open(output, "w+") if output.is_a?(String)
37
49
 
38
50
  case format.to_sym
39
- when :json then JSON.new(output)
40
- when :call_stack then CallStack.new(output)
41
- when :text then Text.new(output)
51
+ when :json then JSON.new(output, **opts)
52
+ when :call_stack then CallStack.new(output, **opts)
53
+ when :text then Text.new(output, **opts)
42
54
  else
43
55
  raise ArgumentError, "Unknown format specified: #{format}. Available formats: text, json, call_stack"
44
56
  end
@@ -5,19 +5,26 @@ module RequireProfiler
5
5
  class Event < Struct.new(:type, :path, :time, keyword_init: true)
6
6
  end
7
7
 
8
- class Node < Struct.new(:path, :time, :children, keyword_init: true)
8
+ class Node < Struct.new(:path, :time, :parent, :children, :focused, keyword_init: true)
9
9
  def initialize(...)
10
10
  super
11
11
  self.children ||= []
12
+ self.focused = false
13
+ end
14
+
15
+ def focused!
16
+ self.focused = true
17
+ parent&.focused!
12
18
  end
13
19
  end
14
20
 
15
- private attr_reader :stack, :totals, :printer, :processor, :queue
21
+ private attr_reader :stack, :totals, :printer, :processor, :queue, :focus
16
22
 
17
- def initialize(printer:)
23
+ def initialize(printer:, focus: nil)
18
24
  @stack = []
19
25
  @totals = {count: 0, time: 0.0}
20
26
  @printer = printer
27
+ @focus = focus
21
28
  @processor = nil
22
29
  @queue = Queue.new
23
30
 
@@ -31,13 +38,20 @@ module RequireProfiler
31
38
  def handle_event_sync(event)
32
39
  if event.type == :start
33
40
  node = Node.new(path: event.path, children: [])
34
- stack.last&.children&.push(node)
41
+ parent = stack.last
42
+
43
+ if parent
44
+ node.parent = parent
45
+ parent.children&.push(node)
46
+ end
35
47
 
36
48
  stack << node
37
49
  elsif event.type == :end
38
50
  last = stack.pop
39
51
  last.time = event.time
40
52
 
53
+ last.focused! if focus && last.path.match?(focus)
54
+
41
55
  printer.flush(last) if stack.empty?
42
56
 
43
57
  totals[:count] += 1
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RequireProfiler
4
+ # This module contains helpers to activate specific files loading
5
+ # profiling using Stackprof or Vernier (so you can dig deeper into why a particular
6
+ # file is slow to require)
7
+ module RubyProfiling
8
+ class << self
9
+ attr_accessor :enabled, :target_path, :profiler
10
+
11
+ def enabled?
12
+ @enabled
13
+ end
14
+
15
+ def capture(path, &)
16
+ return yield unless path.end_with?(target_path)
17
+
18
+ if profiler == :stackprof
19
+ capture_stackprof(path, &)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def capture_stackprof(path)
26
+ require "stackprof"
27
+
28
+ filename = target_path.sub(/\.rb$/, "").tr("/.", "-") + "-stackprof"
29
+ dump_path = filename + ".dump"
30
+
31
+ options = {
32
+ mode: :wall,
33
+ raw: true,
34
+ out: dump_path
35
+ }
36
+
37
+ ::StackProf.run(**options) { yield }.tap do
38
+ report = ::StackProf::Report.new(
39
+ Marshal.load(IO.binread(dump_path))
40
+ )
41
+ json_path = filename + ".json"
42
+ File.write(json_path, JSON.generate(report.data))
43
+ $stdout.puts "Stackprof JSON profile for #{target_path} is generated: #{json_path}"
44
+ end
45
+ end
46
+ end
47
+
48
+ if ENV["REQUIRE_PROFILE_STACKPROF"]
49
+ self.enabled = true
50
+ self.target_path = ENV["REQUIRE_PROFILE_STACKPROF"]
51
+ self.profiler = :stackprof
52
+ end
53
+ end
54
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RequireProfiler # :nodoc:
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -4,13 +4,23 @@ module RequireProfiler
4
4
  autoload :Reporter, "require_profiler/reporter"
5
5
  autoload :Printer, "require_profiler/printer"
6
6
 
7
+ # Autoload doesn't work here, because we call it from the hooks for the first time
8
+ require "require_profiler/ruby_profiling"
9
+
7
10
  class << self
8
11
  attr_reader :reporter
9
12
 
10
- def start(output: $stdout, format: nil, patterns: nil, exclude_patterns: nil)
13
+ def start(
14
+ output: ENV.fetch("REQUIRE_PROFILE_PATH", $stdout),
15
+ format: ENV["REQUIRE_PROFILE_FORMAT"],
16
+ threshold: ENV.fetch("REQUIRE_PROFILE_THRESHOLD", "0.0").to_f,
17
+ focus: ENV["REQUIRE_PROFILE_FOCUS"],
18
+ patterns: nil, exclude_patterns: nil
19
+ )
11
20
  raise ArgumentError, "There is already profiling in progress" if reporter
12
21
 
13
- reporter = @reporter = Reporter.new(printer: Printer.resolve(output, format))
22
+ focus = Regexp.new(focus) if focus.is_a?(String)
23
+ reporter = @reporter = Reporter.new(printer: Printer.resolve(output, format, threshold:, focus:), focus:)
14
24
 
15
25
  require "require-hooks/setup"
16
26
 
@@ -19,7 +29,11 @@ module RequireProfiler
19
29
 
20
30
  reporter.handle_event(Reporter::Event.new(type: :start, path:))
21
31
 
22
- block.call
32
+ if RubyProfiling.enabled?
33
+ RubyProfiling.capture(path) { block.call }
34
+ else
35
+ block.call
36
+ end
23
37
  ensure
24
38
  time = Time.now - start
25
39
  reporter.handle_event(Reporter::Event.new(type: :end, path:, time:))
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.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
@@ -75,6 +75,7 @@ files:
75
75
  - CHANGELOG.md
76
76
  - LICENSE.txt
77
77
  - README.md
78
+ - lib/equire-prof.rb
78
79
  - lib/require-profiler.rb
79
80
  - lib/require_profiler.rb
80
81
  - lib/require_profiler/printer.rb
@@ -82,6 +83,7 @@ files:
82
83
  - lib/require_profiler/printer/json.rb
83
84
  - lib/require_profiler/printer/text.rb
84
85
  - lib/require_profiler/reporter.rb
86
+ - lib/require_profiler/ruby_profiling.rb
85
87
  - lib/require_profiler/version.rb
86
88
  homepage: https://github.com/palkan/require-profiler
87
89
  licenses: