vernier 1.5.0 → 1.6.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/README.md +38 -12
- data/ext/vernier/vernier.cc +4 -1
- data/lib/vernier/middleware.rb +1 -1
- data/lib/vernier/output/file_listing.rb +53 -14
- data/lib/vernier/output/firefox.rb +10 -6
- data/lib/vernier/stack_table_helpers.rb +12 -1
- data/lib/vernier/version.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3847eff8849b364e1cd777058a720d4960e7058c4895c86ded9c5f98d053e76
|
4
|
+
data.tar.gz: 306275958737222981ec6fd45846b210b757bc858feee84533dbc45b3997d471
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 803e19370d96f59ddf2668fadd2c4f0318c98d60576ad9f34a7a229495c6c9185d5fa5bc461b998722ca83979fb05a9007120efeaa206a734eff05bb0e3b864e
|
7
|
+
data.tar.gz: 3bab21c2bcb0cdaf82287cd7fbe9a619bba3bdf0658d6ec3cdd7d9b7cdae6feec5f43f1cc2056dd86af31fdf19794173e12d651fd26620f5e05304f7aa1ecf5b
|
data/README.md
CHANGED
@@ -22,7 +22,7 @@ Rails benchmark - lobste.rs (time)
|
|
22
22
|
|
23
23
|
## Installation
|
24
24
|
|
25
|
-
Vernier requires Ruby version 3.2.1 or greater.
|
25
|
+
Vernier requires Ruby version 3.2.1 or greater and a Unix-like operating system.
|
26
26
|
|
27
27
|
```ruby
|
28
28
|
gem "vernier", "~> 1.0"
|
@@ -30,7 +30,7 @@ gem "vernier", "~> 1.0"
|
|
30
30
|
|
31
31
|
## Usage
|
32
32
|
|
33
|
-
The output can be viewed in the web app at https://vernier.prof, locally using the [`profile-viewer` gem](https://github.com/tenderlove/profiler/tree/ruby) (both lightly customized versions of the firefox profiler frontend
|
33
|
+
The output can be viewed in the web app at https://vernier.prof, locally using the [`profile-viewer` gem](https://github.com/tenderlove/profiler/tree/ruby) (both lightly customized versions of the firefox profiler frontend which profiles are compatible with), or by using the `vernier view` command in the CLI.
|
34
34
|
|
35
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
36
|
- **Stack Chart**: Shows the stack at each sample with the x-axis representing time and can be read left-to-right.
|
@@ -41,14 +41,14 @@ The output can be viewed in the web app at https://vernier.prof, locally using t
|
|
41
41
|
|
42
42
|
The easiest way to record a program or script is via the CLI:
|
43
43
|
|
44
|
-
```
|
44
|
+
```sh
|
45
45
|
$ vernier run -- ruby -e 'sleep 1'
|
46
46
|
starting profiler with interval 500 and allocation interval 0
|
47
47
|
#<Vernier::Result 1.001589 seconds, 1 threads, 1 samples, 1 unique>
|
48
48
|
written to /tmp/profile20240328-82441-gkzffc.vernier.json.gz
|
49
49
|
```
|
50
50
|
|
51
|
-
```
|
51
|
+
```sh
|
52
52
|
$ vernier run --interval 100 --allocation-interval 10 -- ruby -e '10.times { Object.new }'
|
53
53
|
starting profiler with interval 100 and allocation interval 10
|
54
54
|
#<Vernier::Result 0.00067 seconds, 1 threads, 1 samples, 1 unique>
|
@@ -85,13 +85,39 @@ some_other_slow_method
|
|
85
85
|
Vernier.stop_profile
|
86
86
|
```
|
87
87
|
|
88
|
+
#### Rack middleware
|
89
|
+
|
90
|
+
You can also use `Vernier::Middleware` to profile a Rack application:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# config.ru
|
94
|
+
|
95
|
+
require "vernier"
|
96
|
+
|
97
|
+
use Vernier::Middleware
|
98
|
+
|
99
|
+
run ->(env) { [200, { "Content-Type" => "text/plain" }, ["Hello, Profiling World!"]] }
|
100
|
+
```
|
101
|
+
|
102
|
+
If you're using Rails, you can add the middleware to your `config/application.rb`:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
config.middleware.use Vernier::Middleware, permit: ->(env) { env["PATH_INFO"].start_with?("/api") }
|
106
|
+
```
|
107
|
+
|
108
|
+
You can then enable profiling and configure options with query parameters:
|
109
|
+
|
110
|
+
```sh
|
111
|
+
curl http://localhost:3000?vernier=true&vernier_interval=100&vernier_allocation_interval=10
|
112
|
+
```
|
113
|
+
|
88
114
|
### Retained memory
|
89
115
|
|
90
116
|
#### Block of code
|
91
117
|
|
92
118
|
Record a flamegraph of all **retained** allocations from loading `irb`:
|
93
119
|
|
94
|
-
```
|
120
|
+
```sh
|
95
121
|
ruby -r vernier -e 'Vernier.trace_retained(out: "irb_profile.json") { require "irb" }'
|
96
122
|
```
|
97
123
|
|
@@ -100,13 +126,13 @@ ruby -r vernier -e 'Vernier.trace_retained(out: "irb_profile.json") { require "i
|
|
100
126
|
|
101
127
|
### Options
|
102
128
|
|
103
|
-
Option | Description
|
104
|
-
|
105
|
-
`mode` |
|
106
|
-
`out` |
|
107
|
-
`interval` |
|
108
|
-
`allocation_interval` |
|
109
|
-
`gc` |
|
129
|
+
| Option | Middleware Param | Description | Default (Middleware Default) |
|
130
|
+
|-----------------------|-------------------------------|---------------------------------------------------------------|------------------------------|
|
131
|
+
| `mode` | N/A | Sampling mode: `:wall`, `:retained`, or `:custom`. | `:wall` (`:wall`) |
|
132
|
+
| `out` | N/A | File to write the profile to. | N/A (Auto-generated) |
|
133
|
+
| `interval` | `vernier_interval` | Sampling interval (µs). Only in `:wall` mode. | `500` (`200`) |
|
134
|
+
| `allocation_interval` | `vernier_allocation_interval` | Allocation sampling interval. Only in `:wall` mode. | `0` (disabled) (`200`) |
|
135
|
+
| `gc` | N/A | Run full GC cycle before profiling. Only in `:retained` mode. | `true` (N/A) |
|
110
136
|
|
111
137
|
## Development
|
112
138
|
|
data/ext/vernier/vernier.cc
CHANGED
@@ -78,6 +78,9 @@ static const char *gvl_event_name(rb_event_flag_t event) {
|
|
78
78
|
struct FrameInfo {
|
79
79
|
static const char *label_cstr(VALUE frame) {
|
80
80
|
VALUE label = rb_profile_frame_full_label(frame);
|
81
|
+
// Currently (2025-03-22, Ruby 3.4.2) this occurs when an iseq method
|
82
|
+
// entry is replaced with a refinement
|
83
|
+
if (NIL_P(label)) return "(nil)";
|
81
84
|
return StringValueCStr(label);
|
82
85
|
}
|
83
86
|
|
@@ -86,7 +89,7 @@ struct FrameInfo {
|
|
86
89
|
if (NIL_P(file))
|
87
90
|
file = rb_profile_frame_path(frame);
|
88
91
|
if (NIL_P(file)) {
|
89
|
-
return "";
|
92
|
+
return "(nil)";
|
90
93
|
} else {
|
91
94
|
return StringValueCStr(file);
|
92
95
|
}
|
data/lib/vernier/middleware.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "filename_filter"
|
4
|
+
require "cgi/util"
|
4
5
|
|
5
6
|
module Vernier
|
6
7
|
module Output
|
@@ -23,29 +24,24 @@ module Vernier
|
|
23
24
|
@profile = profile
|
24
25
|
end
|
25
26
|
|
26
|
-
def
|
27
|
-
output = +""
|
28
|
-
|
27
|
+
def samples_by_file
|
29
28
|
thread = @profile.main_thread
|
30
29
|
if Hash === thread
|
31
30
|
# live profile
|
32
31
|
stack_table = @profile._stack_table
|
33
|
-
weights = thread[:weights]
|
34
|
-
samples = thread[:samples]
|
35
32
|
filename_filter = FilenameFilter.new
|
36
33
|
else
|
37
34
|
stack_table = thread.stack_table
|
38
|
-
weights = thread.weights
|
39
|
-
samples = thread.samples
|
40
35
|
filename_filter = ->(x) { x }
|
41
36
|
end
|
42
37
|
|
38
|
+
weights = thread[:weights]
|
39
|
+
samples = thread[:samples]
|
40
|
+
|
43
41
|
self_samples_by_frame = Hash.new do |h, k|
|
44
42
|
h[k] = SamplesByLocation.new
|
45
43
|
end
|
46
44
|
|
47
|
-
total = weights.sum
|
48
|
-
|
49
45
|
samples.zip(weights).each do |stack_idx, weight|
|
50
46
|
# self time
|
51
47
|
top_frame_index = stack_table.stack_frame_idx(stack_idx)
|
@@ -76,6 +72,10 @@ module Vernier
|
|
76
72
|
samples_by_file.transform_keys! do |filename|
|
77
73
|
filename_filter.call(filename)
|
78
74
|
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def output(template: nil)
|
78
|
+
output = +""
|
79
79
|
|
80
80
|
relevant_files = samples_by_file.select do |k, v|
|
81
81
|
next if k.start_with?("gem:")
|
@@ -83,13 +83,23 @@ module Vernier
|
|
83
83
|
next if k.start_with?("<")
|
84
84
|
v.values.map(&:total).sum > total * 0.01
|
85
85
|
end
|
86
|
-
|
86
|
+
|
87
|
+
if template == "html"
|
88
|
+
html_output(output, relevant_files)
|
89
|
+
else
|
90
|
+
relevant_files.keys.sort.each do |filename|
|
91
|
+
output << "="*80 << "\n"
|
92
|
+
output << filename << "\n"
|
93
|
+
output << "-"*80 << "\n"
|
94
|
+
format_file(output, filename, samples_by_file, total: total)
|
95
|
+
end
|
87
96
|
output << "="*80 << "\n"
|
88
|
-
output << filename << "\n"
|
89
|
-
output << "-"*80 << "\n"
|
90
|
-
format_file(output, filename, samples_by_file, total: total)
|
91
97
|
end
|
92
|
-
|
98
|
+
end
|
99
|
+
|
100
|
+
def total
|
101
|
+
thread = @profile.main_thread
|
102
|
+
thread[:weights].sum
|
93
103
|
end
|
94
104
|
|
95
105
|
def format_file(output, filename, all_samples, total:)
|
@@ -108,6 +118,35 @@ module Vernier
|
|
108
118
|
end
|
109
119
|
end
|
110
120
|
end
|
121
|
+
|
122
|
+
def html_output(output, relevant_files)
|
123
|
+
output << "<pre>"
|
124
|
+
output << " SELF FILE\n"
|
125
|
+
relevant_files.sort_by {|k, v| -v.values.map(&:self).sum }.each do |filename, file_contents|
|
126
|
+
tmpl = "<details style=\"display:inline-block;vertical-align:top;\"><summary>%s</summary>"
|
127
|
+
output << sprintf("% 5.1f%% #{tmpl}\n", file_contents.values.map(&:self).sum * 100 / total.to_f, filename)
|
128
|
+
format_file_html(output, filename, relevant_files)
|
129
|
+
output << "</details>\n"
|
130
|
+
end
|
131
|
+
output << "</pre>"
|
132
|
+
end
|
133
|
+
|
134
|
+
def format_file_html(output, filename, relevant_files)
|
135
|
+
samples = relevant_files[filename]
|
136
|
+
|
137
|
+
# file_name, lines, file_wall, file_cpu, file_idle, file_sort
|
138
|
+
output << sprintf(" TOTAL | SELF | LINE SOURCE\n")
|
139
|
+
File.readlines(filename).each_with_index do |line, i|
|
140
|
+
lineno = i + 1
|
141
|
+
calls = samples[lineno]
|
142
|
+
|
143
|
+
if calls && calls.total > 0
|
144
|
+
output << sprintf("%5.1f%% | %5.1f%% | % 4i %s", 100 * calls.total / total.to_f, 100 * calls.self / total.to_f, lineno, CGI::escapeHTML(line))
|
145
|
+
else
|
146
|
+
output << sprintf(" | | % 4i %s", lineno, CGI::escapeHTML(line))
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
111
150
|
end
|
112
151
|
end
|
113
152
|
end
|
@@ -487,17 +487,21 @@ module Vernier
|
|
487
487
|
def frame_table
|
488
488
|
funcs = @stack_table_hash[:frame_table].fetch(:func)
|
489
489
|
lines = @stack_table_hash[:frame_table].fetch(:line)
|
490
|
-
size
|
490
|
+
raise unless lines.size == funcs.size
|
491
|
+
|
492
|
+
size = funcs.size
|
491
493
|
none = [nil] * size
|
492
|
-
|
494
|
+
default = [0] * size
|
495
|
+
unidentified = [-1] * size
|
493
496
|
|
494
|
-
|
497
|
+
categories = @frame_categories.map(&:idx)
|
498
|
+
subcategories = @frame_subcategories
|
495
499
|
|
496
500
|
{
|
497
|
-
address:
|
498
|
-
inlineDepth:
|
501
|
+
address: unidentified,
|
502
|
+
inlineDepth: default,
|
499
503
|
category: categories,
|
500
|
-
subcategory:
|
504
|
+
subcategory: subcategories,
|
501
505
|
func: funcs,
|
502
506
|
nativeSymbol: none,
|
503
507
|
innerWindowID: none,
|
@@ -90,7 +90,7 @@ module Vernier
|
|
90
90
|
end
|
91
91
|
|
92
92
|
class Stack < BaseType
|
93
|
-
def
|
93
|
+
def each
|
94
94
|
return enum_for(__method__) unless block_given?
|
95
95
|
|
96
96
|
stack_idx = idx
|
@@ -100,6 +100,17 @@ module Vernier
|
|
100
100
|
stack_idx = stack_table.stack_parent_idx(stack_idx)
|
101
101
|
end
|
102
102
|
end
|
103
|
+
alias each_frame each
|
104
|
+
|
105
|
+
def [](n)
|
106
|
+
raise RangeError if n < 0
|
107
|
+
stack_idx = idx
|
108
|
+
while n > 0
|
109
|
+
stack_idx = stack_table.stack_parent_idx(stack_idx)
|
110
|
+
n -= 1
|
111
|
+
end
|
112
|
+
Frame.new(stack_table, stack_table.stack_frame_idx(stack_idx))
|
113
|
+
end
|
103
114
|
|
104
115
|
def leaf_frame_idx
|
105
116
|
stack_table.stack_frame_idx(idx)
|
data/lib/vernier/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vernier
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Hawthorn
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-03-22 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: activesupport
|
@@ -112,7 +111,6 @@ metadata:
|
|
112
111
|
homepage_uri: https://github.com/jhawthorn/vernier
|
113
112
|
source_code_uri: https://github.com/jhawthorn/vernier
|
114
113
|
changelog_uri: https://github.com/jhawthorn/vernier
|
115
|
-
post_install_message:
|
116
114
|
rdoc_options: []
|
117
115
|
require_paths:
|
118
116
|
- lib
|
@@ -127,8 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
125
|
- !ruby/object:Gem::Version
|
128
126
|
version: '0'
|
129
127
|
requirements: []
|
130
|
-
rubygems_version: 3.
|
131
|
-
signing_key:
|
128
|
+
rubygems_version: 3.6.2
|
132
129
|
specification_version: 4
|
133
130
|
summary: A next generation CRuby profiler
|
134
131
|
test_files: []
|