vernier 0.5.1 → 0.7.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/Gemfile +1 -0
- data/README.md +22 -4
- data/examples/gvl_sleep.rb +62 -0
- data/examples/measure_overhead.rb +39 -0
- data/examples/rails.rb +18 -13
- data/examples/threaded_http_requests.rb +1 -1
- data/exe/vernier +3 -0
- data/ext/vernier/vernier.cc +475 -151
- data/lib/vernier/autorun.rb +2 -1
- data/lib/vernier/collector.rb +46 -1
- data/lib/vernier/hooks/active_support.rb +110 -0
- data/lib/vernier/hooks.rb +7 -0
- data/lib/vernier/middleware.rb +32 -0
- data/lib/vernier/output/firefox.rb +131 -49
- data/lib/vernier/result.rb +18 -1
- data/lib/vernier/stack_table.rb +42 -0
- data/lib/vernier/thread_names.rb +52 -0
- data/lib/vernier/version.rb +1 -1
- data/lib/vernier.rb +31 -17
- data/vernier.gemspec +5 -2
- metadata +42 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4988794520691870cb34178385be791e28fa4806eb1f6934b0d0abf52c8a070c
|
4
|
+
data.tar.gz: b1e63c6f5398f61fb68d634f17f33dc6aa7461344cb8acf446dd75ca52395818
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '082561b2a381f88c540e607d1dd9c0d42d841a0de82fc61471fb2d67754ee14a4fd1e87fa71896b586077664e61fd1db623fd250cd13064d198c9dc91d3f13ed'
|
7
|
+
data.tar.gz: a28c197d405e4f52d999db3ae73187047886e1fcfb7d353d13f26ad5ba4b61f0675daf617119b37d72ad3760bd0dffcae234f2dcb0a0e960a60046dfb87dd784
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
Next-generation Ruby 3.2.1+ sampling profiler. Tracks multiple threads, GVL activity, GC pauses, idle time, and more.
|
4
4
|
|
5
|
+
<img width="500" alt="Screenshot 2024-02-29 at 22 47 43" src="https://github.com/jhawthorn/vernier/assets/131752/aa995a41-d74f-405f-8ada-2522dd72c2c8">
|
6
|
+
|
5
7
|
## Examples
|
6
8
|
|
7
9
|
[Livestreamed demo: Pairin' with Aaron (YouTube)](https://www.youtube.com/watch?v=9nvX3OHykGQ#t=27m43)
|
@@ -28,17 +30,33 @@ gem 'vernier'
|
|
28
30
|
|
29
31
|
## Usage
|
30
32
|
|
33
|
+
The output can be viewed in the web app at https://vernier.prof or locally using the [`profile-viewer` gem](https://github.com/tenderlove/profiler/tree/ruby) (both are lightly customized versions of the firefox profiler frontend, which profiles are also compatible with).
|
34
|
+
|
35
|
+
- **Flame Graph**: Shows proportionally how much time is spent within particular stack frames. Frames are grouped together, which means that x-axis / left-to-right order is not meaningful.
|
36
|
+
- **Stack Chart**: Shows the stack at each sample with the x-axis representing time and can be read left-to-right.
|
37
|
+
|
31
38
|
|
32
39
|
### Time
|
33
40
|
|
41
|
+
|
42
|
+
#### Command line
|
43
|
+
|
44
|
+
The easiest way to record a program or script is via the CLI
|
45
|
+
|
34
46
|
```
|
35
|
-
|
47
|
+
$ vernier run -- ruby -e 'sleep 1'
|
48
|
+
starting profiler with interval 500
|
49
|
+
#<Vernier::Result 1.001589 seconds, 1 threads, 2002 samples, 1 unique>
|
50
|
+
written to /tmp/profile20240328-82441-gkzffc.vernier.json
|
36
51
|
```
|
37
52
|
|
38
|
-
|
53
|
+
### Block of code
|
39
54
|
|
40
|
-
|
41
|
-
|
55
|
+
``` ruby
|
56
|
+
Vernier.run(out: "time_profile.json") do
|
57
|
+
some_slow_method
|
58
|
+
end
|
59
|
+
```
|
42
60
|
|
43
61
|
### Retained memory
|
44
62
|
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Different (bad) ways to sleep
|
2
|
+
|
3
|
+
File.write("#{__dir__}/my_sleep.c", <<~EOF)
|
4
|
+
#include <time.h>
|
5
|
+
#include <sys/errno.h>
|
6
|
+
|
7
|
+
void my_sleep() {
|
8
|
+
struct timespec ts;
|
9
|
+
ts.tv_sec = 1;
|
10
|
+
ts.tv_nsec = 0;
|
11
|
+
|
12
|
+
int rc;
|
13
|
+
do {
|
14
|
+
rc = nanosleep(&ts, &ts);
|
15
|
+
} while (rc < 0 && errno == EINTR);
|
16
|
+
}
|
17
|
+
EOF
|
18
|
+
|
19
|
+
soext = RbConfig::CONFIG["SOEXT"]
|
20
|
+
system("gcc", "-shared", "-fPIC", "#{__dir__}/my_sleep.c", "-o", "#{__dir__}/my_sleep.#{soext}")
|
21
|
+
|
22
|
+
require "fiddle"
|
23
|
+
|
24
|
+
SLEEP_LIB = Fiddle.dlopen("./my_sleep.#{soext}")
|
25
|
+
|
26
|
+
def cfunc_sleep_gvl
|
27
|
+
Fiddle::Function.new(SLEEP_LIB['my_sleep'], [], Fiddle::TYPE_VOID, need_gvl: true).call
|
28
|
+
end
|
29
|
+
|
30
|
+
def cfunc_sleep_idle
|
31
|
+
Fiddle::Function.new(SLEEP_LIB['my_sleep'], [], Fiddle::TYPE_VOID, need_gvl: true).call
|
32
|
+
end
|
33
|
+
|
34
|
+
def ruby_sleep_gvl
|
35
|
+
target = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + 1000
|
36
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) < target
|
37
|
+
i = 0
|
38
|
+
while i < 1000
|
39
|
+
i += 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def sleep_idle
|
45
|
+
sleep 1
|
46
|
+
end
|
47
|
+
|
48
|
+
def run(name)
|
49
|
+
STDOUT.print "#{name}..."
|
50
|
+
STDOUT.flush
|
51
|
+
|
52
|
+
before = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
53
|
+
send(name)
|
54
|
+
after = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
55
|
+
|
56
|
+
STDOUT.puts " %.2fs" % (after - before)
|
57
|
+
end
|
58
|
+
|
59
|
+
run(:cfunc_sleep_gvl)
|
60
|
+
run(:cfunc_sleep_idle)
|
61
|
+
run(:ruby_sleep_gvl)
|
62
|
+
run(:sleep_idle)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "benchmark/ips"
|
2
|
+
require "vernier"
|
3
|
+
|
4
|
+
def compare(**options, &block)
|
5
|
+
block.call
|
6
|
+
|
7
|
+
Benchmark.ips do |x|
|
8
|
+
x.report "no profiler" do |n|
|
9
|
+
n.times do
|
10
|
+
block.call
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
x.report "vernier" do |n|
|
15
|
+
Vernier.profile(**options) do
|
16
|
+
n.times do
|
17
|
+
block.call
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
x.compare!
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
compare do
|
27
|
+
i = 0
|
28
|
+
while i < 10_000
|
29
|
+
i += 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
compare(allocation_sample_rate: 1000) do
|
34
|
+
Object.new
|
35
|
+
end
|
36
|
+
|
37
|
+
compare(allocation_sample_rate: 1) do
|
38
|
+
Object.new
|
39
|
+
end
|
data/examples/rails.rb
CHANGED
@@ -59,12 +59,17 @@ end
|
|
59
59
|
|
60
60
|
silence do
|
61
61
|
ActiveRecord::Schema.define do
|
62
|
+
create_table :users, force: true do |t|
|
63
|
+
t.string :login
|
64
|
+
end
|
62
65
|
create_table :posts, force: true do |t|
|
66
|
+
t.belongs_to :user
|
63
67
|
t.string :title
|
64
68
|
t.text :body
|
65
69
|
t.integer :likes
|
66
70
|
end
|
67
71
|
create_table :comments, force: true do |t|
|
72
|
+
t.belongs_to :user
|
68
73
|
t.belongs_to :post
|
69
74
|
t.string :title
|
70
75
|
t.text :body
|
@@ -73,25 +78,33 @@ silence do
|
|
73
78
|
end
|
74
79
|
end
|
75
80
|
|
81
|
+
class User < ActiveRecord::Base
|
82
|
+
end
|
83
|
+
|
76
84
|
class Post < ActiveRecord::Base
|
77
85
|
has_many :comments
|
86
|
+
belongs_to :user
|
78
87
|
end
|
79
88
|
|
80
89
|
class Comment < ActiveRecord::Base
|
81
90
|
belongs_to :post
|
91
|
+
belongs_to :user
|
82
92
|
end
|
83
93
|
|
94
|
+
users = 0.upto(100).map do |i|
|
95
|
+
User.create!(login: "user#{i}")
|
96
|
+
end
|
84
97
|
0.upto(100) do |i|
|
85
|
-
post = Post.create!(title: "Post number #{i}", body: "blog " * 50, likes: ((i * 1337) % 30))
|
98
|
+
post = Post.create!(title: "Post number #{i}", body: "blog " * 50, likes: ((i * 1337) % 30), user: users.sample)
|
86
99
|
5.times do
|
87
|
-
post.comments.create!(post: post, title: "nice post!", body: "keep it up!", posted_at: Time.now)
|
100
|
+
post.comments.create!(post: post, title: "nice post!", body: "keep it up!", posted_at: Time.now, user: users.sample)
|
88
101
|
end
|
89
102
|
end
|
90
103
|
|
91
104
|
class HomeController < ActionController::Base
|
92
105
|
def show
|
93
|
-
posts = Post.order(likes: :desc).includes(:comments).first(10)
|
94
|
-
render json: posts
|
106
|
+
posts = Post.order(likes: :desc).includes(:user, comments: :user).first(10)
|
107
|
+
render json: posts, include: [:user, comments: { include: :user } ]
|
95
108
|
end
|
96
109
|
end
|
97
110
|
|
@@ -110,15 +123,7 @@ end
|
|
110
123
|
# warm up
|
111
124
|
make_request.call
|
112
125
|
|
113
|
-
Vernier.trace(out: "rails.json") do |collector|
|
114
|
-
ActiveSupport::Notifications.monotonic_subscribe do |name, start, finish, id, payload|
|
115
|
-
collector.add_marker(
|
116
|
-
name:,
|
117
|
-
start: (start * 1_000_000_000).to_i,
|
118
|
-
finish: (finish * 1_000_000_000).to_i,
|
119
|
-
)
|
120
|
-
end
|
121
|
-
|
126
|
+
Vernier.trace(out: "rails.json", hooks: [:rails], allocation_sample_rate: 100) do |collector|
|
122
127
|
1000.times do
|
123
128
|
make_request.call
|
124
129
|
end
|
data/exe/vernier
CHANGED
@@ -16,6 +16,9 @@ parser = OptionParser.new(banner) do |o|
|
|
16
16
|
o.on('--interval [MICROSECONDS]', Integer, "sampling interval (default 500)") do |i|
|
17
17
|
options[:interval] = i
|
18
18
|
end
|
19
|
+
o.on('--allocation_sample_rate [ALLOCATIONS]', Integer, "allocation sampling interval (default 0 disabled)") do |i|
|
20
|
+
options[:allocation_sample_rate] = i
|
21
|
+
end
|
19
22
|
o.on('--signal [NAME]', String, "specify a signal to start and stop the profiler") do |s|
|
20
23
|
options[:signal] = s
|
21
24
|
end
|