vernier 1.5.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a5b535bebdbcc91aa853b53657893c37531e8acffe5f00df1753ca1c52e7508
4
- data.tar.gz: 7aa9509fdfb009956bd2eeab6b74bc8fa832e078d9c8fce92445c82a5699ea27
3
+ metadata.gz: 2419244a8929351d5c81f18b90766f20d7d9e29d4909c876deb06d2a9ad39f2d
4
+ data.tar.gz: 94ead7ad3797a5cca2311cebc0ba0a57de369b7a6d09a07cbe4b6fad1f8826ac
5
5
  SHA512:
6
- metadata.gz: 45584b395431f8135928011d03d427f42aa747dda76c4bca3bc063837571e3cc168b3fbc04cd268353e17299f47c7be3c6c486e0885488c6e7b5aba94bcb4190
7
- data.tar.gz: d8df8d3a0ac7ce752cfb4c8fc0f57e0af1bf7640a3f228f52a8cd4a32211cbe21686338ca6068257d49df0a2a0206773da89d93aa673a2915c5ea5881d1f4be2
6
+ metadata.gz: 25551c8b2d537ca00de410e0bbd2173a2cd0452462f8c8ed5c27fd9b6592fa60f7ff1eceb01b429eba69b1989287a7244acef7448b4335b3e2ef87e731af59df
7
+ data.tar.gz: e510284f293ae3efd17e82c8e643b800ccbce5d84ffca04ef35fb16b5edf796672080465384cb40ea59eeacc7cc3cea57cb25e73805183956359a4e58e464f8c
data/README.md CHANGED
@@ -4,9 +4,11 @@ Next-generation Ruby 3.2.1+ sampling profiler. Tracks multiple threads, GVL acti
4
4
 
5
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
6
 
7
- ## Examples
7
+ ## Demos and Examples
8
8
 
9
- [Livestreamed demo: Pairin' with Aaron (YouTube)](https://www.youtube.com/watch?v=9nvX3OHykGQ#t=27m43)
9
+ [Livestreamed demo: Pairin' with Aaron (YouTube)](https://www.youtube.com/watch?v=9nvX3OHykGQ#t=27m40)
10
+
11
+ [Overview at RubyKaigi 2024 (YouTube)](https://youtu.be/QSjN-H4hGsM)
10
12
 
11
13
  Sidekiq jobs from Mastodon (time, threaded)
12
14
  : https://share.firefox.dev/44jZRf3
@@ -22,7 +24,7 @@ Rails benchmark - lobste.rs (time)
22
24
 
23
25
  ## Installation
24
26
 
25
- Vernier requires Ruby version 3.2.1 or greater.
27
+ Vernier requires Ruby version 3.2.1 or greater and a Unix-like operating system.
26
28
 
27
29
  ```ruby
28
30
  gem "vernier", "~> 1.0"
@@ -30,7 +32,7 @@ gem "vernier", "~> 1.0"
30
32
 
31
33
  ## Usage
32
34
 
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 also compatible with) or by using the `vernier view` command in the CLI.
35
+ 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
36
 
35
37
  - **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
38
  - **Stack Chart**: Shows the stack at each sample with the x-axis representing time and can be read left-to-right.
@@ -41,14 +43,14 @@ The output can be viewed in the web app at https://vernier.prof, locally using t
41
43
 
42
44
  The easiest way to record a program or script is via the CLI:
43
45
 
44
- ```
46
+ ```sh
45
47
  $ vernier run -- ruby -e 'sleep 1'
46
48
  starting profiler with interval 500 and allocation interval 0
47
49
  #<Vernier::Result 1.001589 seconds, 1 threads, 1 samples, 1 unique>
48
50
  written to /tmp/profile20240328-82441-gkzffc.vernier.json.gz
49
51
  ```
50
52
 
51
- ```
53
+ ```sh
52
54
  $ vernier run --interval 100 --allocation-interval 10 -- ruby -e '10.times { Object.new }'
53
55
  starting profiler with interval 100 and allocation interval 10
54
56
  #<Vernier::Result 0.00067 seconds, 1 threads, 1 samples, 1 unique>
@@ -85,13 +87,39 @@ some_other_slow_method
85
87
  Vernier.stop_profile
86
88
  ```
87
89
 
90
+ #### Rack middleware
91
+
92
+ You can also use `Vernier::Middleware` to profile a Rack application:
93
+
94
+ ```ruby
95
+ # config.ru
96
+
97
+ require "vernier"
98
+
99
+ use Vernier::Middleware
100
+
101
+ run ->(env) { [200, { "Content-Type" => "text/plain" }, ["Hello, Profiling World!"]] }
102
+ ```
103
+
104
+ If you're using Rails, you can add the middleware to your `config/application.rb`:
105
+
106
+ ```ruby
107
+ config.middleware.use Vernier::Middleware, permit: ->(env) { env["PATH_INFO"].start_with?("/api") }
108
+ ```
109
+
110
+ You can then enable profiling and configure options with query parameters:
111
+
112
+ ```sh
113
+ curl http://localhost:3000?vernier=true&vernier_interval=100&vernier_allocation_interval=10
114
+ ```
115
+
88
116
  ### Retained memory
89
117
 
90
118
  #### Block of code
91
119
 
92
120
  Record a flamegraph of all **retained** allocations from loading `irb`:
93
121
 
94
- ```
122
+ ```sh
95
123
  ruby -r vernier -e 'Vernier.trace_retained(out: "irb_profile.json") { require "irb" }'
96
124
  ```
97
125
 
@@ -100,13 +128,14 @@ ruby -r vernier -e 'Vernier.trace_retained(out: "irb_profile.json") { require "i
100
128
 
101
129
  ### Options
102
130
 
103
- Option | Description
104
- :- | :-
105
- `mode` | The sampling mode to use. One of `:wall`, `:retained` or `:custom`. Default is `:wall`.
106
- `out` | The file to write the profile to.
107
- `interval` | The sampling interval in microseconds. Default is `500`. Only available in `:wall` mode.
108
- `allocation_interval` | The allocation sampling interval in number of allocations. Default is `0` (disabled). Only available in `:wall` mode.
109
- `gc` | Initiate a full and immediate garbage collection cycle before profiling. Default is `true`. Only available in `:retained` mode.
131
+ | Option | Middleware Param | Description | Default (Middleware Default) |
132
+ |-----------------------|-------------------------------|---------------------------------------------------------------|------------------------------|
133
+ | `mode` | N/A | Sampling mode: `:wall`, `:retained`, or `:custom`. | `:wall` (`:wall`) |
134
+ | `out` | N/A | File to write the profile to. | N/A (Auto-generated) |
135
+ | `interval` | `vernier_interval` | Sampling interval (µs). Only in `:wall` mode. | `500` (`200`) |
136
+ | `allocation_interval` | `vernier_allocation_interval` | Allocation sampling interval. Only in `:wall` mode. | `0`/disabled (`200`) |
137
+ | `gc` | N/A | Run full GC cycle before profiling. Only in `:retained` mode. | `true` (N/A) |
138
+ | `metadata` | N/A | Metadata key-value pairs to include in the profile. | `{}` (N/A) |
110
139
 
111
140
  ## Development
112
141
 
data/exe/vernier CHANGED
@@ -7,6 +7,15 @@ require "vernier/version"
7
7
 
8
8
  module Vernier
9
9
  module CLI
10
+ class Metadata < Array
11
+ require 'json'
12
+ require 'base64'
13
+
14
+ def to_s
15
+ Base64.encode64(to_json)
16
+ end
17
+ end
18
+
10
19
  def self.run(options)
11
20
  banner = <<-END
12
21
  Usage: vernier run [FLAGS] -- COMMAND
@@ -38,6 +47,11 @@ FLAGS:
38
47
  o.on('--hooks [HOOKS]', String, "enable instrumentation hooks, currently supported: rails") do |s|
39
48
  options[:hooks] = s
40
49
  end
50
+ o.on("--metadata KEY=VALUE", String, "Set metadata key-value pairs (can be specified multiple times)") do |kv|
51
+ key, value = kv.split('=')
52
+ options[:metadata] ||= Metadata.new
53
+ options[:metadata] << [key, value]
54
+ end
41
55
  end
42
56
  end
43
57
 
@@ -74,10 +74,12 @@ static const char *gvl_event_name(rb_event_flag_t event) {
74
74
  return "no-event";
75
75
  }
76
76
 
77
- // TODO: Rename FuncInfo
78
- struct FrameInfo {
77
+ struct FuncInfo {
79
78
  static const char *label_cstr(VALUE frame) {
80
79
  VALUE label = rb_profile_frame_full_label(frame);
80
+ // Currently (2025-03-22, Ruby 3.4.2) this occurs when an iseq method
81
+ // entry is replaced with a refinement
82
+ if (NIL_P(label)) return "(nil)";
81
83
  return StringValueCStr(label);
82
84
  }
83
85
 
@@ -86,7 +88,7 @@ struct FrameInfo {
86
88
  if (NIL_P(file))
87
89
  file = rb_profile_frame_path(frame);
88
90
  if (NIL_P(file)) {
89
- return "";
91
+ return "(nil)";
90
92
  } else {
91
93
  return StringValueCStr(file);
92
94
  }
@@ -97,7 +99,7 @@ struct FrameInfo {
97
99
  return NIL_P(first_lineno) ? 0 : FIX2INT(first_lineno);
98
100
  }
99
101
 
100
- FrameInfo(VALUE frame) :
102
+ FuncInfo(VALUE frame) :
101
103
  label(label_cstr(frame)),
102
104
  file(file_cstr(frame)),
103
105
  first_lineno(first_lineno_int(frame)) { }
@@ -107,7 +109,7 @@ struct FrameInfo {
107
109
  int first_lineno;
108
110
  };
109
111
 
110
- bool operator==(const FrameInfo& lhs, const FrameInfo& rhs) noexcept {
112
+ bool operator==(const FuncInfo& lhs, const FuncInfo& rhs) noexcept {
111
113
  return
112
114
  lhs.label == rhs.label &&
113
115
  lhs.file == rhs.file &&
@@ -263,13 +265,13 @@ struct StackTable {
263
265
 
264
266
  struct FrameWithInfo {
265
267
  Frame frame;
266
- FrameInfo info;
268
+ FuncInfo info;
267
269
  };
268
270
 
269
271
  IndexMap<Frame> frame_map;
270
272
 
271
273
  IndexMap<VALUE> func_map;
272
- std::vector<FrameInfo> func_info_list;
274
+ std::vector<FuncInfo> func_info_list;
273
275
 
274
276
  struct StackNode {
275
277
  std::unordered_map<Frame, int> children;
@@ -360,7 +362,7 @@ struct StackTable {
360
362
  for (int i = func_info_list.size(); i < func_map.size(); i++) {
361
363
  const auto &func = func_map[i];
362
364
  // must not hold a mutex here
363
- func_info_list.push_back(FrameInfo(func));
365
+ func_info_list.push_back(FuncInfo(func));
364
366
  }
365
367
  }
366
368
 
@@ -1,5 +1,7 @@
1
1
  require "tempfile"
2
2
  require "vernier"
3
+ require "base64"
4
+ require "json"
3
5
 
4
6
  module Vernier
5
7
  module Autorun
@@ -19,10 +21,15 @@ module Vernier
19
21
  interval = options.fetch(:interval, 500).to_i
20
22
  allocation_interval = options.fetch(:allocation_interval, 0).to_i
21
23
  hooks = options.fetch(:hooks, "").split(",")
24
+ metadata = if options[:metadata]
25
+ JSON.parse(Base64.decode64(@options[:metadata])).to_h { |k, v| [k.to_sym, v] }
26
+ else
27
+ {}
28
+ end
22
29
 
23
30
  STDERR.puts("starting profiler with interval #{interval} and allocation interval #{allocation_interval}")
24
31
 
25
- @collector = Vernier::Collector.new(:wall, interval:, allocation_interval:, hooks:)
32
+ @collector = Vernier::Collector.new(:wall, interval:, allocation_interval:, hooks:, metadata:)
26
33
  @collector.start
27
34
  end
28
35
 
@@ -25,6 +25,8 @@ module Vernier
25
25
  @hooks.each do |hook|
26
26
  hook.enable
27
27
  end
28
+
29
+ @user_metadata = options[:metadata] || {}
28
30
  end
29
31
 
30
32
  private def add_hook(hook)
@@ -79,6 +81,7 @@ module Vernier
79
81
  result.meta[:mode] = @mode
80
82
  result.meta[:out] = @out
81
83
  result.meta[:gc] = @gc
84
+ result.meta[:user_metadata] = @user_metadata
82
85
 
83
86
  result.stack_table = stack_table
84
87
  @thread_names.finish
@@ -1,6 +1,6 @@
1
1
  module Vernier
2
2
  class Middleware
3
- def initialize(app, permit: ->(_) { true })
3
+ def initialize(app, permit: ->(_env) { true })
4
4
  @app = app
5
5
  @permit = permit
6
6
  end
@@ -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 output
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
- relevant_files.keys.sort.each do |filename|
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
- output << "="*80 << "\n"
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
@@ -145,7 +145,18 @@ module Vernier
145
145
  end,
146
146
  sourceCodeIsNotOnSearchfox: true,
147
147
  initialVisibleThreads: threads.each_index.to_a,
148
- initialSelectedThreads: Array(threads.find_index(&:is_start))
148
+ initialSelectedThreads: Array(threads.find_index(&:is_start)),
149
+ vernierUserMetadata: profile.meta[:user_metadata],
150
+ extra: [
151
+ label: "User-Supplied Metadata",
152
+ entries: profile.meta[:user_metadata].map do |k, v|
153
+ {
154
+ label: k,
155
+ format: "string",
156
+ value: v
157
+ }
158
+ end
159
+ ]
149
160
  },
150
161
  counters: counter_data,
151
162
  libs: [],
@@ -487,17 +498,21 @@ module Vernier
487
498
  def frame_table
488
499
  funcs = @stack_table_hash[:frame_table].fetch(:func)
489
500
  lines = @stack_table_hash[:frame_table].fetch(:line)
490
- size = funcs.length
501
+ raise unless lines.size == funcs.size
502
+
503
+ size = funcs.size
491
504
  none = [nil] * size
492
- categories = @frame_categories.map(&:idx)
505
+ default = [0] * size
506
+ unidentified = [-1] * size
493
507
 
494
- raise unless lines.size == funcs.size
508
+ categories = @frame_categories.map(&:idx)
509
+ subcategories = @frame_subcategories
495
510
 
496
511
  {
497
- address: [-1] * size,
498
- inlineDepth: [0] * size,
512
+ address: unidentified,
513
+ inlineDepth: default,
499
514
  category: categories,
500
- subcategory: nil,
515
+ subcategory: subcategories,
501
516
  func: funcs,
502
517
  nativeSymbol: none,
503
518
  innerWindowID: none,
@@ -90,7 +90,7 @@ module Vernier
90
90
  end
91
91
 
92
92
  class Stack < BaseType
93
- def each_frame
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vernier
4
- VERSION = "1.5.0"
4
+ VERSION = "1.7.0"
5
5
  end
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.5.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Hawthorn
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-12-18 00:00:00.000000000 Z
10
+ date: 2025-04-03 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.5.22
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: []