vernier 1.2.0 → 1.3.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: 56faced9b5a5fe99a124ab7743609eb23a981ca27888a5a8957e509d28f508ef
4
- data.tar.gz: 0b258dc2f1f4c143568527213a3f39055a44aed3c983c84a14f71d6af68c900a
3
+ metadata.gz: 73ae72f3ced0043f71cc0fd1f8b5c829b57d6d4c7c39536a6564c8bcb654dfbd
4
+ data.tar.gz: c392fff41de6efe80ed825087ed92760493345aa04a18b288bae8d5afa166b88
5
5
  SHA512:
6
- metadata.gz: '096133a98cfc38236c89025c51f70a0c4def9c6208031775d11640291d08b17d53fb2817d6f9b7fea1e7d0551ac0e58471d757fd5568c732969d73134ca20fc3'
7
- data.tar.gz: 063ce46032770d8d2cd161071587dea386af0497a9437d8bc19148c54263e32c2d4d00b164c926ff19f1236f704f1c76aabff8be63bb2b66cd7409f69742004b
6
+ metadata.gz: e8c95c63b4836d7a160003a7ad2be68c54d135ba34ad663246b563f4e2f5954351a0e8c1e47ee4392b27a980cba8d395b607b1093487305abbcb21dd5467d5f1
7
+ data.tar.gz: 3a516ab9a0f52dfeaeecf8332c92cd63e160c9e51478ac6e8f30b846e1319d16b5132a8e7a6007ca582c46f3d6bdd8d96a88b9c686fb208de97779646f070a38
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.
26
26
 
27
27
  ```ruby
28
28
  gem "vernier", "~> 1.0"
@@ -30,24 +30,29 @@ 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 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).
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 `verrnier 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.
37
37
 
38
-
39
- ### Time
40
-
38
+ ### Time and Allocations
41
39
 
42
40
  #### Command line
43
41
 
44
- The easiest way to record a program or script is via the CLI
42
+ The easiest way to record a program or script is via the CLI:
45
43
 
46
44
  ```
47
45
  $ 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
46
+ starting profiler with interval 500 and allocation interval 0
47
+ #<Vernier::Result 1.001589 seconds, 1 threads, 1 samples, 1 unique>
48
+ written to /tmp/profile20240328-82441-gkzffc.vernier.json.gz
49
+ ```
50
+
51
+ ```
52
+ $ vernier run --interval 100 --allocation-interval 10 -- ruby -e '10.times { Object.new }'
53
+ starting profiler with interval 100 and allocation interval 10
54
+ #<Vernier::Result 0.00067 seconds, 1 threads, 1 samples, 1 unique>
55
+ written to /tmp/profile20241029-26525-dalmym.vernier.json.gz
51
56
  ```
52
57
 
53
58
  #### Block of code
@@ -58,12 +63,18 @@ Vernier.profile(out: "time_profile.json") do
58
63
  end
59
64
  ```
60
65
 
66
+ ``` ruby
67
+ Vernier.profile(out: "time_profile.json", interval: 100, allocation_interval: 10) do
68
+ some_slow_method
69
+ end
70
+ ```
71
+
61
72
  Alternatively you can use the aliases `Vernier.run` and `Vernier.trace`.
62
73
 
63
74
  #### Start and stop
64
75
 
65
76
  ```ruby
66
- Vernier.start_profile(out: "time_profile.json")
77
+ Vernier.start_profile(out: "time_profile.json", interval: 10_000, allocation_interval: 100_000)
67
78
 
68
79
  some_slow_method
69
80
 
@@ -78,13 +89,14 @@ Vernier.stop_profile
78
89
 
79
90
  #### Block of code
80
91
 
81
- Record a flamegraph of all **retained** allocations from loading `irb`.
92
+ Record a flamegraph of all **retained** allocations from loading `irb`:
82
93
 
83
94
  ```
84
95
  ruby -r vernier -e 'Vernier.trace_retained(out: "irb_profile.json") { require "irb" }'
85
96
  ```
86
97
 
87
- Retained-memory flamegraphs must be interpreted a little differently than a typical profiling flamegraph. In a retained-memory flamegraph, the x-axis represents a proportion of memory in bytes, _not time or samples_ The topmost boxes on the y-axis represent the retained objects, with their stacktrace below; their width represents the percentage of overall retained memory each object occupies.
98
+ > [!NOTE]
99
+ > Retained-memory flamegraphs must be interpreted a little differently than a typical profiling flamegraph. In a retained-memory flamegraph, the x-axis represents a proportion of memory in bytes, _not time or samples_ The topmost boxes on the y-axis represent the retained objects, with their stacktrace below; their width represents the percentage of overall retained memory each object occupies.
88
100
 
89
101
  ### Options
90
102
 
@@ -93,7 +105,7 @@ Option | Description
93
105
  `mode` | The sampling mode to use. One of `:wall`, `:retained` or `:custom`. Default is `:wall`.
94
106
  `out` | The file to write the profile to.
95
107
  `interval` | The sampling interval in microseconds. Default is `500`. Only available in `:wall` mode.
96
- `allocation_sample_rate` | The allocation sampling interval in number of allocations. Default is `0` (disabled). 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.
97
109
  `gc` | Initiate a full and immediate garbage collection cycle before profiling. Default is `true`. Only available in `:retained` mode.
98
110
 
99
111
  ## Development
@@ -30,10 +30,10 @@ compare do
30
30
  end
31
31
  end
32
32
 
33
- compare(allocation_sample_rate: 1000) do
33
+ compare(allocation_interval: 1000) do
34
34
  Object.new
35
35
  end
36
36
 
37
- compare(allocation_sample_rate: 1) do
37
+ compare(allocation_interval: 1) do
38
38
  Object.new
39
39
  end
data/examples/rails.rb CHANGED
@@ -123,7 +123,7 @@ end
123
123
  # warm up
124
124
  make_request.call
125
125
 
126
- Vernier.trace(out: "rails.json", hooks: [:rails], allocation_sample_rate: 100) do |collector|
126
+ Vernier.trace(out: "rails.json", hooks: [:rails], allocation_interval: 100) do |collector|
127
127
  1000.times do
128
128
  make_request.call
129
129
  end
@@ -1,6 +1,6 @@
1
1
  require "vernier"
2
2
 
3
- Vernier.trace(out: "http_requests.json", allocation_sample_rate: 100) do
3
+ Vernier.trace(out: "http_requests.json", allocation_interval: 100) do
4
4
 
5
5
  require "net/http"
6
6
  require "uri"
data/exe/vernier CHANGED
@@ -21,8 +21,8 @@ FLAGS:
21
21
  o.on('--interval [MICROSECONDS]', Integer, "sampling interval (default 500)") do |i|
22
22
  options[:interval] = i
23
23
  end
24
- o.on('--allocation_sample_rate [ALLOCATIONS]', Integer, "allocation sampling interval (default 0 disabled)") do |i|
25
- options[:allocation_sample_rate] = i
24
+ o.on('--allocation-interval [ALLOCATIONS]', Integer, "allocation sampling interval (default 0 disabled)") do |i|
25
+ options[:allocation_interval] = i
26
26
  end
27
27
  o.on('--signal [NAME]', String, "specify a signal to start and stop the profiler") do |s|
28
28
  options[:signal] = s
@@ -30,7 +30,7 @@ FLAGS:
30
30
  o.on('--start-paused', "don't automatically start the profiler") do
31
31
  options[:start_paused] = true
32
32
  end
33
- o.on('--hooks [HOOKS]', String, "Enable instrumentation hooks. Currently supported: rails") do |s|
33
+ o.on('--hooks [HOOKS]', String, "enable instrumentation hooks, currently supported: rails") do |s|
34
34
  options[:hooks] = s
35
35
  end
36
36
  end
@@ -38,7 +38,7 @@ FLAGS:
38
38
 
39
39
  def self.view(options)
40
40
  banner = <<-END
41
- Usage: vernier view [FLAGS] FILENAME
41
+ Usage: vernier view [FLAGS] -- FILENAME
42
42
 
43
43
  FLAGS:
44
44
  END
@@ -1114,7 +1114,11 @@ class Thread {
1114
1114
  sample.sample();
1115
1115
 
1116
1116
  int stack_idx = translator.translate(frame_list, sample);
1117
- allocation_samples.record_sample(stack_idx, TimeStamp::Now(), 1);
1117
+ if (stack_idx >= 0) {
1118
+ allocation_samples.record_sample(stack_idx, TimeStamp::Now(), 1);
1119
+ } else {
1120
+ // TODO: should we log an empty frame?
1121
+ }
1118
1122
  }
1119
1123
 
1120
1124
  void set_state(State new_state) {
@@ -1333,7 +1337,7 @@ class BaseCollector {
1333
1337
  virtual void write_meta(VALUE meta, VALUE result) {
1334
1338
  rb_hash_aset(meta, sym("started_at"), ULL2NUM(started_at.nanoseconds()));
1335
1339
  rb_hash_aset(meta, sym("interval"), Qnil);
1336
- rb_hash_aset(meta, sym("allocation_sample_rate"), Qnil);
1340
+ rb_hash_aset(meta, sym("allocation_interval"), Qnil);
1337
1341
 
1338
1342
  }
1339
1343
 
@@ -1356,6 +1360,9 @@ class BaseCollector {
1356
1360
  rb_gc_mark(stack_table_value);
1357
1361
  };
1358
1362
 
1363
+ virtual void compact() {
1364
+ };
1365
+
1359
1366
  virtual VALUE get_markers() {
1360
1367
  return rb_ary_new();
1361
1368
  };
@@ -1543,6 +1550,23 @@ class RetainedCollector : public BaseCollector {
1543
1550
  rb_gc_mark(tp_newobj);
1544
1551
  rb_gc_mark(tp_freeobj);
1545
1552
  }
1553
+
1554
+ void compact() {
1555
+ RetainedCollector *collector = this;
1556
+ for (auto& obj: collector->object_list) {
1557
+ VALUE reloc_obj = rb_gc_location(obj);
1558
+
1559
+ const auto search = collector->object_frames.find(obj);
1560
+ if (search != collector->object_frames.end()) {
1561
+ int stack_index = search->second;
1562
+
1563
+ collector->object_frames.erase(search);
1564
+ collector->object_frames.emplace(reloc_obj, stack_index);
1565
+ }
1566
+
1567
+ obj = reloc_obj;
1568
+ }
1569
+ }
1546
1570
  };
1547
1571
 
1548
1572
  class GlobalSignalHandler {
@@ -1623,8 +1647,8 @@ class TimeCollector : public BaseCollector {
1623
1647
  SignalSafeSemaphore thread_stopped;
1624
1648
 
1625
1649
  TimeStamp interval;
1626
- unsigned int allocation_sample_rate;
1627
- unsigned int allocation_sample_tick = 0;
1650
+ unsigned int allocation_interval;
1651
+ unsigned int allocation_tick = 0;
1628
1652
 
1629
1653
  VALUE tp_newobj = Qnil;
1630
1654
 
@@ -1637,14 +1661,14 @@ class TimeCollector : public BaseCollector {
1637
1661
  }
1638
1662
 
1639
1663
  public:
1640
- TimeCollector(VALUE stack_table, TimeStamp interval, unsigned int allocation_sample_rate) : BaseCollector(stack_table), interval(interval), allocation_sample_rate(allocation_sample_rate), threads(*get_stack_table(stack_table)) {
1664
+ TimeCollector(VALUE stack_table, TimeStamp interval, unsigned int allocation_interval) : BaseCollector(stack_table), interval(interval), allocation_interval(allocation_interval), threads(*get_stack_table(stack_table)) {
1641
1665
  }
1642
1666
 
1643
1667
  void record_newobj(VALUE obj) {
1644
- if (++allocation_sample_tick < allocation_sample_rate) {
1668
+ if (++allocation_tick < allocation_interval) {
1645
1669
  return;
1646
1670
  }
1647
- allocation_sample_tick = 0;
1671
+ allocation_tick = 0;
1648
1672
 
1649
1673
  VALUE current_thread = rb_thread_current();
1650
1674
  threads.mutex.lock();
@@ -1662,7 +1686,7 @@ class TimeCollector : public BaseCollector {
1662
1686
  void write_meta(VALUE meta, VALUE result) {
1663
1687
  BaseCollector::write_meta(meta, result);
1664
1688
  rb_hash_aset(meta, sym("interval"), ULL2NUM(interval.microseconds()));
1665
- rb_hash_aset(meta, sym("allocation_sample_rate"), ULL2NUM(allocation_sample_rate));
1689
+ rb_hash_aset(meta, sym("allocation_interval"), ULL2NUM(allocation_interval));
1666
1690
 
1667
1691
  }
1668
1692
 
@@ -1860,7 +1884,7 @@ class TimeCollector : public BaseCollector {
1860
1884
  this->threads.initial(thread);
1861
1885
  }
1862
1886
 
1863
- if (allocation_sample_rate > 0) {
1887
+ if (allocation_interval > 0) {
1864
1888
  tp_newobj = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, newobj_i, this);
1865
1889
  rb_tracepoint_enable(tp_newobj);
1866
1890
  }
@@ -1965,12 +1989,19 @@ collector_free(void *data) {
1965
1989
  delete collector;
1966
1990
  }
1967
1991
 
1992
+ static void
1993
+ collector_compact(void *data) {
1994
+ BaseCollector *collector = static_cast<BaseCollector *>(data);
1995
+ collector->compact();
1996
+ }
1997
+
1968
1998
  static const rb_data_type_t rb_collector_type = {
1969
1999
  .wrap_struct_name = "vernier/collector",
1970
2000
  .function = {
1971
2001
  //.dmemsize = rb_collector_memsize,
1972
2002
  .dmark = collector_mark,
1973
2003
  .dfree = collector_free,
2004
+ .dcompact = collector_compact,
1974
2005
  },
1975
2006
  };
1976
2007
 
@@ -1985,7 +2016,7 @@ collector_start(VALUE self) {
1985
2016
  auto *collector = get_collector(self);
1986
2017
 
1987
2018
  if (!collector->start()) {
1988
- rb_raise(rb_eRuntimeError, "already running");
2019
+ rb_raise(rb_eRuntimeError, "collector already running");
1989
2020
  }
1990
2021
 
1991
2022
  return Qtrue;
@@ -2038,14 +2069,17 @@ static VALUE collector_new(VALUE self, VALUE mode, VALUE options) {
2038
2069
  interval = TimeStamp::from_microseconds(NUM2UINT(intervalv));
2039
2070
  }
2040
2071
 
2041
- VALUE allocation_sample_ratev = rb_hash_aref(options, sym("allocation_sample_rate"));
2042
- unsigned int allocation_sample_rate;
2043
- if (NIL_P(allocation_sample_ratev)) {
2044
- allocation_sample_rate = 0;
2072
+ VALUE allocation_intervalv = rb_hash_aref(options, sym("allocation_interval"));
2073
+ if (NIL_P(allocation_intervalv))
2074
+ allocation_intervalv = rb_hash_aref(options, sym("allocation_sample_rate"));
2075
+
2076
+ unsigned int allocation_interval;
2077
+ if (NIL_P(allocation_intervalv)) {
2078
+ allocation_interval = 0;
2045
2079
  } else {
2046
- allocation_sample_rate = NUM2UINT(allocation_sample_ratev);
2080
+ allocation_interval = NUM2UINT(allocation_intervalv);
2047
2081
  }
2048
- collector = new TimeCollector(stack_table, interval, allocation_sample_rate);
2082
+ collector = new TimeCollector(stack_table, interval, allocation_interval);
2049
2083
  } else {
2050
2084
  rb_raise(rb_eArgError, "invalid mode");
2051
2085
  }
@@ -17,12 +17,12 @@ module Vernier
17
17
 
18
18
  def self.start
19
19
  interval = options.fetch(:interval, 500).to_i
20
- allocation_sample_rate = options.fetch(:allocation_sample_rate, 0).to_i
20
+ allocation_interval = options.fetch(:allocation_interval, 0).to_i
21
21
  hooks = options.fetch(:hooks, "").split(",")
22
22
 
23
- STDERR.puts("starting profiler with interval #{interval}")
23
+ STDERR.puts("starting profiler with interval #{interval} and allocation interval #{allocation_interval}")
24
24
 
25
- @collector = Vernier::Collector.new(:wall, interval:, allocation_sample_rate:, hooks:)
25
+ @collector = Vernier::Collector.new(:wall, interval:, allocation_interval:, hooks:)
26
26
  @collector.start
27
27
  end
28
28
 
@@ -32,7 +32,7 @@ module Vernier
32
32
  when :rails, :activesupport
33
33
  @hooks << Vernier::Hooks::ActiveSupport.new(self)
34
34
  else
35
- warn "Unknown hook: #{hook.inspect}"
35
+ warn "unknown hook: #{hook.inspect}"
36
36
  end
37
37
  end
38
38
 
@@ -13,9 +13,9 @@ module Vernier
13
13
  return @app.call(env) unless permitted
14
14
 
15
15
  interval = request.GET.fetch("vernier_interval", 200).to_i
16
- allocation_sample_rate = request.GET.fetch("vernier_allocation_sample_rate", 200).to_i
16
+ allocation_interval = request.GET.fetch("vernier_allocation_interval", 200).to_i
17
17
 
18
- result = Vernier.trace(interval:, allocation_sample_rate:, hooks: [:rails]) do
18
+ result = Vernier.trace(interval:, allocation_interval:, hooks: [:rails]) do
19
19
  @app.call(env)
20
20
  end
21
21
  body = result.to_gecko(gzip: true)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vernier
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/vernier.rb CHANGED
@@ -40,7 +40,7 @@ module Vernier
40
40
  @collector.stop
41
41
  @collector = nil
42
42
 
43
- raise "Profile already started, stopping..."
43
+ raise "profile already started, stopping..."
44
44
  end
45
45
 
46
46
  @collector = Vernier::Collector.new(mode, collector_options)
@@ -48,7 +48,7 @@ module Vernier
48
48
  end
49
49
 
50
50
  def self.stop_profile
51
- raise "No profile started" unless @collector
51
+ raise "profile not started" unless @collector
52
52
 
53
53
  result = @collector.stop
54
54
  @collector = nil
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vernier
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Hawthorn
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-11 00:00:00.000000000 Z
11
+ date: 2024-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport