vernier 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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