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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a5b535bebdbcc91aa853b53657893c37531e8acffe5f00df1753ca1c52e7508
4
- data.tar.gz: 7aa9509fdfb009956bd2eeab6b74bc8fa832e078d9c8fce92445c82a5699ea27
3
+ metadata.gz: b3847eff8849b364e1cd777058a720d4960e7058c4895c86ded9c5f98d053e76
4
+ data.tar.gz: 306275958737222981ec6fd45846b210b757bc858feee84533dbc45b3997d471
5
5
  SHA512:
6
- metadata.gz: 45584b395431f8135928011d03d427f42aa747dda76c4bca3bc063837571e3cc168b3fbc04cd268353e17299f47c7be3c6c486e0885488c6e7b5aba94bcb4190
7
- data.tar.gz: d8df8d3a0ac7ce752cfb4c8fc0f57e0af1bf7640a3f228f52a8cd4a32211cbe21686338ca6068257d49df0a2a0206773da89d93aa673a2915c5ea5881d1f4be2
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, which profiles are also compatible with) or by using the `vernier view` command in the CLI.
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` | 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.
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
 
@@ -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
  }
@@ -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
@@ -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 = funcs.length
490
+ raise unless lines.size == funcs.size
491
+
492
+ size = funcs.size
491
493
  none = [nil] * size
492
- categories = @frame_categories.map(&:idx)
494
+ default = [0] * size
495
+ unidentified = [-1] * size
493
496
 
494
- raise unless lines.size == funcs.size
497
+ categories = @frame_categories.map(&:idx)
498
+ subcategories = @frame_subcategories
495
499
 
496
500
  {
497
- address: [-1] * size,
498
- inlineDepth: [0] * size,
501
+ address: unidentified,
502
+ inlineDepth: default,
499
503
  category: categories,
500
- subcategory: nil,
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 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.6.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.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: 2024-12-18 00:00:00.000000000 Z
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.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: []