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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +109 -213
- data/docs/examples/benchmarking.md +95 -0
- data/docs/examples/class-and-module-methods.md +126 -0
- data/docs/examples/plain-ruby.md +243 -0
- data/docs/examples/rails-console.md +185 -0
- data/docs/examples/real-world-export.md +40 -0
- data/docs/examples/reports-as-data.md +124 -0
- data/docs/reference.md +272 -0
- data/lib/method_timer/instrumenter.rb +50 -24
- data/lib/method_timer/memory.rb +6 -0
- data/lib/method_timer/recorder.rb +13 -11
- data/lib/method_timer/reporter.rb +76 -44
- data/lib/method_timer/sql.rb +8 -2
- data/lib/method_timer/version.rb +1 -1
- data/lib/method_timer.rb +22 -10
- metadata +10 -2
|
@@ -1,69 +1,101 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MethodTimer
|
|
4
|
-
#
|
|
4
|
+
# Renders the last measurement as a table or a detached hash.
|
|
5
5
|
module Reporter
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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(
|
|
19
|
-
report_rss(
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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)',
|
|
43
|
-
warn '%-*s %10s %10s %10s %10s %10s %10s %12d' %
|
|
44
|
-
|
|
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(
|
|
48
|
-
warn "\n%-20s %9.3fs" % ['wall',
|
|
49
|
-
warn '%-20s %9.3fs' % ['GC',
|
|
50
|
-
warn '%-20s %9.3fs (%d queries)' % ['SQL', sql_t, sql_calls]
|
|
51
|
-
warn '%-20s %10d' % ['allocations',
|
|
52
|
-
|
|
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(
|
|
90
|
+
def report_rss(rss)
|
|
60
91
|
warn ''
|
|
61
|
-
warn '%-*s %10s' % [@name_width, ' RSS start', format_bytes(
|
|
62
|
-
warn '%-*s %10s' %
|
|
63
|
-
|
|
64
|
-
|
|
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(
|
|
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)
|
data/lib/method_timer/sql.rb
CHANGED
|
@@ -8,8 +8,14 @@ module MethodTimer
|
|
|
8
8
|
def subscribe_sql
|
|
9
9
|
return unless defined?(ActiveSupport::Notifications)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
data/lib/method_timer/version.rb
CHANGED
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 { |
|
|
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
|
-
|
|
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 |
|
|
56
|
-
sql[0] +=
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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: []
|