require-profiler 0.1.0 → 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 +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +62 -2
- data/lib/equire-prof.rb +6 -0
- data/lib/require_profiler/printer/call_stack.rb +2 -0
- data/lib/require_profiler/printer/json.rb +1 -0
- data/lib/require_profiler/printer/text.rb +2 -0
- data/lib/require_profiler/printer.rb +18 -6
- data/lib/require_profiler/reporter.rb +18 -4
- data/lib/require_profiler/ruby_profiling.rb +54 -0
- data/lib/require_profiler/version.rb +1 -1
- data/lib/require_profiler.rb +17 -3
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 787aa181e1f018e66c9f8163837af8980a0549a23102361206451ca4c640675e
|
|
4
|
+
data.tar.gz: a7a0a01e40938642a7bd2b505ae18e47e42b3f00fa482c63151c5616d8f746d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f99d748f75f3333f90979bfde8082591bb77e6b56590df2424dc566ff556798a50660d2c125d4b261c37235b0cdc95ff7cf228f146bd1e8f27d88af5498d90a3
|
|
7
|
+
data.tar.gz: b0b8aa77547e9852c91f699944877450aa03d125f4324b2efc5ea59854c293680215ee8bf272eda892a7c90c33ee5422bdecbeb3c6cf4530e2389a0aa1da8e05
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
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
|
+
|
|
17
|
+
## 0.1.1 (2026-04-29)
|
|
18
|
+
|
|
19
|
+
- Fix JSON printer leaving trailing call-stack bytes in output. ([@ardecvz][])
|
|
20
|
+
|
|
21
|
+
We overwrite the buffered call-stack text with a (shorter) Speedscope JSON document but never truncate it, breaking JSON validity.
|
|
22
|
+
|
|
23
|
+
Truncate the file properly.
|
|
24
|
+
|
|
5
25
|
## 0.1.0 (2026-04-22)
|
|
6
26
|
|
|
7
27
|
- Initial
|
|
28
|
+
|
|
29
|
+
[@palkan]: https://github.com/palkan
|
|
30
|
+
[@ardecvz]: https://github.com/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
|
-
###
|
|
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
|
data/lib/equire-prof.rb
ADDED
|
@@ -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
|
|
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
|
data/lib/require_profiler.rb
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|