method_timer 0.1.0 → 0.2.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.
@@ -1,69 +1,101 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MethodTimer
4
- # Prints the `run` and `bench` reports to stderr.
4
+ # Renders the last measurement as a table or a detached hash.
5
5
  module Reporter
6
- def report(threshold: 0.001)
7
- all = stats
8
- @name_width = [65, *all.keys.map(&:length)].max
6
+ SORT_KEYS = { self: nil, total: :total_t, sql: :sql_t, gc: :gc_t, calls: :calls,
7
+ sql_calls: :sql_calls, allocs: :allocs }.freeze
8
+
9
+ attr_reader :format
10
+
11
+ def format=(value)
12
+ raise ArgumentError, 'format must be :table or :hash' unless %i[table hash].include?(value)
13
+ @format = value
14
+ end
15
+
16
+ def report(threshold: 0.001, sort: :self, limit: nil)
17
+ raise ArgumentError, "unknown sort: #{sort.inspect}" unless SORT_KEYS.key?(sort)
18
+ valid_limit = limit.nil? || (limit.is_a?(Integer) && limit >= 0)
19
+ raise ArgumentError, 'limit must be a non-negative integer or nil' unless valid_limit
20
+ measurement = @last_measurement || { kind: :run, methods: {}, wall_t: 0.0, overhead: 0.0, rss: nil }
21
+ all = measurement[:methods]
22
+ key = SORT_KEYS.fetch(sort)
23
+ rows = all.reject { |_, values| values[:total_t] < threshold }
24
+ .sort_by { |name, values| [-(key ? values.fetch(key) : self_ruby(values)), name] }
25
+ rows = rows.first([limit, rows.length].min) if limit
26
+ data = { methods: rows.to_h.transform_values(&:dup), totals: measurement_totals(measurement),
27
+ rss: measurement[:rss]&.dup }
28
+ return data if format == :hash
29
+ if measurement[:kind] == :bench
30
+ report_bench(data[:totals], data[:rss])
31
+ else
32
+ report_profile(data, measurement[:overhead], all.keys)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def self_ruby(stats)
39
+ stats[:self_t] - stats[:sql_t] - stats[:gc_t]
40
+ end
41
+
42
+ def measurement_totals(measurement)
43
+ return measurement.slice(:wall_t, :sql_t, :gc_t, :sql_calls, :allocs) if measurement[:kind] == :bench
44
+ values = measurement[:methods].values
45
+ calls = values.sum { |value| value[:calls] }
46
+ {
47
+ wall_t: measurement[:wall_t], ruby_t: values.sum(0.0) { |value| self_ruby(value) },
48
+ sql_t: values.sum(0.0) { |value| value[:sql_t] }, gc_t: values.sum(0.0) { |value| value[:gc_t] },
49
+ calls:, sql_calls: values.sum { |value| value[:sql_calls] }, allocs: values.sum { |value| value[:allocs] },
50
+ overhead_t: calls * measurement[:overhead]
51
+ }
52
+ end
53
+
54
+ def report_profile(data, overhead, names)
55
+ @name_width = [65, *names.map(&:length)].max
9
56
  warn "\n%-*s %10s %10s %10s %10s %10s %10s %12s" %
10
57
  [@name_width, 'method', 'self(ruby)', 'self(sql)', 'self(gc)', 'total', 'calls', 'sql calls', 'allocs']
11
58
  warn '-' * (@name_width + 79)
12
- all.sort_by { |_, v| -self_ruby(v) }.each do |name, v|
13
- next if v[:total_t] < threshold
59
+ data[:methods].each do |name, v|
14
60
  warn '%-*s %9.5fs %9.5fs %9.5fs %9.5fs %10d %10d %12d' %
15
61
  [@name_width, name, self_ruby(v), v[:sql_t], v[:gc_t], v[:total_t], v[:calls], v[:sql_calls], v[:allocs]]
16
62
  end
17
63
  warn '-' * (@name_width + 79)
18
- report_totals(all.values)
19
- report_rss("peak (sampled every #{Memory::RSS_INTERVAL}s)") if @rss_peak
64
+ report_totals(data[:totals], overhead)
65
+ report_rss(data[:rss]) if data[:rss] && data[:rss][:peak]
20
66
  warn '-' * (@name_width + 79)
21
67
  end
22
68
 
23
- private
24
-
25
- def self_ruby(stats)
26
- stats[:self_t] - stats[:sql_t] - stats[:gc_t]
27
- end
28
-
29
- def report_totals(values)
69
+ def report_totals(totals, overhead)
30
70
  w = @name_width
31
- calls = values.sum { |v| v[:calls] }
32
- sql_t = values.sum { |v| v[:sql_t] }
33
- sql_calls = values.sum { |v| v[:sql_calls] }
34
- gc_t = values.sum { |v| v[:gc_t] }
35
- allocs = values.sum { |v| v[:allocs] }
36
- warn '%-*s %10s %10s %10s %9.5fs' % [w, 'TOTAL (wall time of run block)', '', '', '', @grand_total]
37
- warn '%-*s %9.5fs' % [w, ' of which captured (sum of self ruby)', values.sum { |v| self_ruby(v) }]
38
- warn '%-*s %10s %9.5fs %10s %10s %10s %10d' % [w, ' of which SQL (sum of self sql / calls)', '', sql_t,
39
- '', '', '', sql_calls]
40
- warn '%-*s %10s %10s %9.5fs' % [w, ' of which GC (sum of self gc)', '', '', gc_t]
71
+ warn '%-*s %10s %10s %10s %9.5fs' % [w, 'TOTAL (wall time of run block)', '', '', '', totals[:wall_t]]
72
+ warn '%-*s %9.5fs' % [w, ' of which captured (sum of self ruby)', totals[:ruby_t]]
73
+ warn '%-*s %10s %9.5fs %10s %10s %10s %10d' %
74
+ [w, ' of which SQL (sum of self sql / calls)', '', totals[:sql_t], '', '', '', totals[:sql_calls]]
75
+ warn '%-*s %10s %10s %9.5fs' % [w, ' of which GC (sum of self gc)', '', '', totals[:gc_t]]
41
76
  warn '%-*s %9.5fs (%d calls x %.2fµs, counted in callers\' self)' %
42
- [w, ' of which instrumentation overhead (estimated)', calls * @overhead, calls, @overhead * 1e6]
43
- warn '%-*s %10s %10s %10s %10s %10s %10s %12d' % [w, ' total allocations (sum of self allocs)', '', '', '', '',
44
- '', '', allocs]
77
+ [w, ' of which instrumentation overhead (estimated)', totals[:overhead_t], totals[:calls], overhead * 1e6]
78
+ warn '%-*s %10s %10s %10s %10s %10s %10s %12d' %
79
+ [w, ' total allocations (sum of self allocs)', '', '', '', '', '', '', totals[:allocs]]
45
80
  end
46
81
 
47
- def report_bench((t0, gc0, alloc0), (sql_t, sql_calls), rss, sampler)
48
- warn "\n%-20s %9.3fs" % ['wall', Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0]
49
- warn '%-20s %9.3fs' % ['GC', (GC.stat(:time) - gc0) / 1000.0]
50
- warn '%-20s %9.3fs (%d queries)' % ['SQL', sql_t, sql_calls]
51
- warn '%-20s %10d' % ['allocations', GC.stat(:total_allocated_objects) - alloc0]
52
- return unless rss
53
-
54
- @rss_end = current_rss
55
- @rss_peak = [@rss_start, stop_rss_sampler(sampler), @rss_end].compact.max
56
- report_rss("peak (sampled every #{Memory::RSS_INTERVAL}s)")
82
+ def report_bench(totals, rss)
83
+ warn "\n%-20s %9.3fs" % ['wall', totals[:wall_t]]
84
+ warn '%-20s %9.3fs' % ['GC', totals[:gc_t]]
85
+ warn '%-20s %9.3fs (%d queries)' % ['SQL', totals[:sql_t], totals[:sql_calls]]
86
+ warn '%-20s %10d' % ['allocations', totals[:allocs]]
87
+ report_rss(rss) if rss
57
88
  end
58
89
 
59
- def report_rss(peak_label = 'peak')
90
+ def report_rss(rss)
60
91
  warn ''
61
- warn '%-*s %10s' % [@name_width, ' RSS start', format_bytes(@rss_start)]
62
- warn '%-*s %10s' % [@name_width, " RSS #{peak_label}", format_bytes(@rss_peak)]
63
- warn '%-*s %10s' % [@name_width, ' RSS end', format_bytes(@rss_end)]
64
- return unless @rss_peak && @rss_start
92
+ warn '%-*s %10s' % [@name_width, ' RSS start', format_bytes(rss[:start])]
93
+ warn '%-*s %10s' %
94
+ [@name_width, " RSS peak (sampled every #{Memory::RSS_INTERVAL}s)", format_bytes(rss[:peak])]
95
+ warn '%-*s %10s' % [@name_width, ' RSS end', format_bytes(rss[:end])]
96
+ return unless rss[:peak] && rss[:start]
65
97
 
66
- warn '%-*s %10s' % [@name_width, ' RSS growth (peak - start)', format_bytes(@rss_peak - @rss_start)]
98
+ warn '%-*s %10s' % [@name_width, ' RSS growth (peak - start)', format_bytes(rss[:peak] - rss[:start])]
67
99
  end
68
100
 
69
101
  def format_bytes(bytes)
@@ -8,8 +8,14 @@ module MethodTimer
8
8
  def subscribe_sql
9
9
  return unless defined?(ActiveSupport::Notifications)
10
10
 
11
- ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, payload|
12
- yield(start, finish) unless payload[:cached] || payload[:name] == 'SCHEMA'
11
+ thread = Thread.current
12
+ ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
13
+ next unless Thread.current.equal?(thread)
14
+ next if event.payload[:cached] || event.payload[:name] == 'SCHEMA'
15
+
16
+ # shortcut: ActiveSupport < 7.2 lacks event GC timing; upgrade for a disjoint SQL/GC split.
17
+ gc_ms = event.respond_to?(:gc_time) ? event.gc_time : 0.0
18
+ yield((event.duration - gc_ms) / 1000.0)
13
19
  end
14
20
  end
15
21
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MethodTimer
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/method_timer.rb CHANGED
@@ -28,11 +28,12 @@ module MethodTimer
28
28
  @strip_root = true
29
29
  @common_prefix = ''
30
30
  @name_width = 65
31
+ @format = :table
31
32
 
32
33
  class << self
33
34
  def run(rss: false)
34
35
  reset
35
- subscriber = subscribe_sql { |start, finish| record_sql(start, finish) }
36
+ subscriber = subscribe_sql { |duration| record_sql(duration) }
36
37
  GC.start
37
38
  @rss_start = @rss_peak = rss ? current_rss : nil
38
39
  sampler = start_rss_sampler if rss
@@ -42,28 +43,39 @@ module MethodTimer
42
43
  ensure
43
44
  @grand_total = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
44
45
  @thread = nil
45
- if rss
46
- @rss_end = current_rss
47
- @rss_peak = [@rss_start, stop_rss_sampler(sampler), @rss_end].compact.max
48
- end
46
+ memory = finish_rss(sampler) if rss
49
47
  unsubscribe_sql(subscriber)
50
- report
48
+ @last_measurement = { kind: :run, methods: stats, wall_t: @grand_total, overhead: @overhead, rss: memory }
49
+ report if format == :table
51
50
  end
52
51
 
53
52
  def bench(rss: false)
53
+ reset
54
54
  sql = [0.0, 0] # [time, calls]
55
- subscriber = subscribe_sql do |start, finish|
56
- sql[0] += finish - start
55
+ subscriber = subscribe_sql do |duration|
56
+ sql[0] += duration
57
57
  sql[1] += 1
58
58
  end
59
59
  GC.start
60
60
  @rss_start = @rss_peak = rss ? current_rss : nil
61
61
  sampler = start_rss_sampler if rss
62
- start = [Process.clock_gettime(Process::CLOCK_MONOTONIC), GC.stat(:time), GC.stat(:total_allocated_objects)]
62
+ start = [Process.clock_gettime(Process::CLOCK_MONOTONIC), GC.total_time, GC.stat(:total_allocated_objects)]
63
63
  yield
64
64
  ensure
65
+ if start
66
+ elapsed = [
67
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - start[0],
68
+ GC.total_time - start[1],
69
+ GC.stat(:total_allocated_objects) - start[2]
70
+ ]
71
+ memory = finish_rss(sampler) if rss
72
+ @last_measurement = {
73
+ kind: :bench, methods: {}, wall_t: elapsed[0], gc_t: elapsed[1] / 1_000_000_000.0,
74
+ allocs: elapsed[2], sql_t: sql[0], sql_calls: sql[1], rss: memory
75
+ }
76
+ end
65
77
  unsubscribe_sql(subscriber)
66
- report_bench(start, sql, rss, sampler) if start
78
+ report if elapsed && format == :table
67
79
  end
68
80
  end
69
81
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: method_timer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konrad Jurkowski
@@ -18,8 +18,16 @@ executables: []
18
18
  extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
+ - CHANGELOG.md
21
22
  - LICENSE.txt
22
23
  - README.md
24
+ - docs/examples/benchmarking.md
25
+ - docs/examples/class-and-module-methods.md
26
+ - docs/examples/plain-ruby.md
27
+ - docs/examples/rails-console.md
28
+ - docs/examples/real-world-export.md
29
+ - docs/examples/reports-as-data.md
30
+ - docs/reference.md
23
31
  - lib/method_timer.rb
24
32
  - lib/method_timer/instrumenter.rb
25
33
  - lib/method_timer/memory.rb
@@ -47,7 +55,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
55
  - !ruby/object:Gem::Version
48
56
  version: '0'
49
57
  requirements: []
50
- rubygems_version: 3.6.9
58
+ rubygems_version: 4.0.20
51
59
  specification_version: 4
52
60
  summary: Low-overhead per-method timing for Ruby code, made for profiling from a console.
53
61
  test_files: []