time_up 0.0.1 → 0.0.5

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: 2814efda7871308dcafe0196407a3bbea424770069d9b998c8728f974a641413
4
- data.tar.gz: 791e65a388e0d62ece0166a747a20cd41f37259d6283e197ff020a05b6eb3e70
3
+ metadata.gz: f6241f3e0e1e66bc75c67a7e7cfc76424ae4e711c5a535b87da3bdf4fa78c153
4
+ data.tar.gz: 33c77eb409cb4d4d4142207ff93a938a1124b87768047f29be0d51057c4eb5fb
5
5
  SHA512:
6
- metadata.gz: a697b5da2199813b882d8a1b3994670dde64c8ce5adb95b321a8bef3c8892cda9110e778c505e0fd4c327f1956f474be07db9f5fc008926093af9c60c4a8accf
7
- data.tar.gz: 1b7089abf119e10a4cff97f880d9a05536a960b112ba59a57f6c6d8642771e566b697db88f6d7c7e2fdca73ff0549803864314de7601d0643e0c018cfbc3731e
6
+ metadata.gz: df9e90669ceef219dcea0bf8b21548252b97abe40b950b922f6c9ee17987494d145fdd130b5c4735a6e57daffaf5ba2d915057044f424ba68656d9b55b5f7820
7
+ data.tar.gz: ad014a9ec0471bfbc3f315b673988d7d5b5115ad29d0cc664ad63eaa81547086bff539de50e311078261c99a9616a0e683c75f9e552bf0a254d26e14d0ef702d
data/.standard.yml ADDED
@@ -0,0 +1 @@
1
+ ruby_version: 2.4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # 0.0.5
2
+
3
+ - Add `median` and `percentile` timer statistics, and added them to
4
+ `print_detailed_summary`
5
+
6
+ # 0.0.4
7
+
8
+ - Add `TimeUp.print_detailed_summary`
9
+
10
+ # 0.0.3
11
+
12
+ - Change the return value of TimeUp.start when passed a block to be the
13
+ evaluated value of the block (for easier insertion into existing code without
14
+ adding a bunch of new assignment and returns)
15
+ - Allow timer instances' `start` method to be called with a block
16
+ - Add `timings`, `count`, `min`, `max`, and `mean` methods for basic stats
17
+ tracking
18
+ - Add `TimeUp.all_stats` to roll up all these
19
+
20
+ # 0.0.2
21
+
22
+ - Switch from a module method to Thread.current variable
23
+
1
24
  # 0.0.1
2
25
 
3
26
  - Make the gem
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- time_up (0.0.1)
4
+ time_up (0.0.5)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -6,10 +6,13 @@ but don't necessarily want to reach for
6
6
  Try `time_up`!
7
7
 
8
8
  This gem is especially useful for long-running processes (like test suites) that
9
- have several time-intensive operations that are repeated over time and that you
10
- want to measure and aggregate. (For example, to see how much time your
11
- test suite spends creating factories, truncating the database, or invoking a
12
- critical code path.)
9
+ have several time-intensive operations that are repeated over the life of the
10
+ process and that you want to measure in aggregate. (For example, to see how
11
+ much time your test suite spends creating factories, truncating the database, or
12
+ invoking a critical code path.)
13
+
14
+ Here's a [blog post about time_up](https://blog.testdouble.com/posts/2021-07-19-benchmarking-your-ruby-with-time_up/) and
15
+ a [great example of when it can be useful](https://gist.github.com/searls/feee0b0eac7c329b390fed90c4714afb).
13
16
 
14
17
  ## Install
15
18
 
@@ -65,10 +68,10 @@ sleep 5
65
68
  puts TimeUp.stop :eggs # => ~5.0
66
69
  ```
67
70
 
68
- `TimeUp.start` also returns an instance of the named timer, which has its own
69
- `start`, `stop`, `elaped`, and `reset` methods. If you want to find that
70
- instance later, you can also call `TimeUp.timer(:some_name)`. So the above
71
- example could be rewritten as:
71
+ When passes without a block, `TimeUp.start` returns an instance of the timer,
72
+ which has its own `start`, `stop`, `elaped`, and `reset` methods. If you want to
73
+ find that instance later, you can also call `TimeUp.timer(:some_name)`. So the
74
+ above example could be rewritten as:
72
75
 
73
76
  ```ruby
74
77
  egg_timer = TimeUp.start :eggs
@@ -107,7 +110,7 @@ TimeUp.print_summary
107
110
  Which will output something like:
108
111
 
109
112
  ```
110
- TimeUp timers summary
113
+ TimeUp summary
111
114
  ========================
112
115
  :roast 0.07267s
113
116
  :veggies 0.03760s
@@ -117,39 +120,87 @@ TimeUp timers summary
117
120
  * Denotes that the timer is still active
118
121
  ```
119
122
 
123
+ And if you're calling the timers multiple times and want to see some basic
124
+ statistics in the print-out, you can call `TimeUp.print_detailed_summary`, which
125
+ will produce this:
126
+
127
+ ```
128
+ =============================================================================
129
+ Name | Elapsed | Count | Min | Max | Mean | Median | 95th %
130
+ -----------------------------------------------------------------------------
131
+ :roast | 0.08454 | 3 | 0.00128 | 0.07280 | 0.02818 | 0.01046 | 0.06657
132
+ :veggies | 0.03779 | 1 | 0.03779 | 0.03779 | 0.03779 | 0.03779 | 0.03779
133
+ :pasta | 0.01260 | 11 | 0.00000 | 0.01258 | 0.00115 | 0.00000 | 0.00630
134
+ :souffle* | 0.00024 | 1 | 0.00024 | 0.00025 | 0.00025 | 0.00025 | 0.00026
135
+
136
+ * Denotes that the timer is still active
137
+ ```
138
+
120
139
  ## API
121
140
 
122
141
  This gem defines a bunch of public methods but they're all pretty short and
123
- straightforward, so I'd encourage you to [read the code](/lib/time_up.rb).
142
+ straightforward, so when in doubt, I'd encourage you to [read the
143
+ code](/lib/time_up.rb).
124
144
 
125
145
  ### `TimeUp` module
126
146
 
127
- `TimeUp.start(name, [&blk])` - Starts (or restarts) and returns a named `Timer`
147
+ `TimeUp.timer(name)` - Returns the `Timer` instance named `name` (creating it,
148
+ if it doesn't exist)
128
149
 
129
- `TimeUp.timer(name)` - Returns any a `Timer` instance named `name` or `nil`
150
+ `TimeUp.start(name, [&blk])` - Starts (or restarts) a named
151
+ [Timer](#timeuptimer-class). If passed a block, will return whatever the block
152
+ evaluates to. If called without a block, it will return the timer object
130
153
 
131
- `TimeUp.stop(name)` - Stops the named timer or raises if it's not defined
154
+ `TimeUp.stop(name)` - Stops the named timer
155
+
156
+ `TimeUp.reset(name)` - Resets the named timer's elapsed time to 0, effectively
157
+ restarting it if it's currently running
132
158
 
133
159
  `TimeUp.elapsed(name)` - Returns a `Float` of the total elapsed seconds that the
134
- named timer has been running (and raises if no timer is defined with the given
135
- `name`)
160
+ named timer has been running
136
161
 
137
- `TimeUp.reset(name)` - Resets the named timer's elapsed time to 0, effectively
138
- restarting it if it's currently running. Raises if the timer isn't defined.
162
+ `TimeUp.timings(name)` - Returns an array of each recorded start-to-stop
163
+ duration (including the current one, if the timer is running) of the named timer
164
+
165
+ `TimeUp.count(name)` - The number of times the timer has been started (including
166
+ the current timing, if the timer is running)
167
+
168
+ `TimeUp.min(name)` - The shortest recording by the timer
169
+
170
+ `TimeUp.max(name)` - The longest recording by the timer
139
171
 
140
- `TimeUp.total_elapsed` - Returns a `Float` of the sum of `elapsed` for all the
141
- timers you've created
172
+ `TimeUp.mean(name)` - The arithmetic mean of all recordings by the timer
142
173
 
143
- `TimeUp.all_elapsed` - Returns a hash of timer name keys mapped to their
144
- `elapsed` values. Handy for grabbing a snapshot of the state of things at a
145
- particular point in time without stopping all your timers
174
+ `TimeUp.median(name)` - The median of all recordings by the timer
146
175
 
147
- `TimeUp.active_timers` - Returns an array of all timers that are currently
148
- running. Useful for detecting cases where you might be counting the same time in
149
- multiple places simultaneously
176
+ `TimeUp.percentile(name, percent)` - The timing for the given
177
+ [percentile](https://en.wikipedia.org/wiki/Percentile) of all recordings by the
178
+ timer
150
179
 
151
- `TimeUp.print_summary([IO])` - Pretty-prints a multi-line summary of all your
152
- timers to STDOUT (or the provided IO)
180
+ `TimeUp.total_elapsed` - Returns a `Float` of the sum of `elapsed` across all
181
+ the timers you've created (note that because you can easily run multiple logical
182
+ timers simultaneously, this figure may exceed the total time spent by the
183
+ computer)
184
+
185
+ `TimeUp.all_elapsed` - Returns a Hash of timer name keys mapped to their
186
+ `elapsed` values. Handy for grabbing a reference to a snapshot of the state of
187
+ things without requiring you to stop your timers
188
+
189
+ `TimeUp.all_stats` - Returns a Hash of timer name keys mapped to another
190
+ hash of their basic statistics (`elapsed`, `count`, `min`, `max`,
191
+ and `mean`)
192
+
193
+ `TimeUp.active_timers` - Returns an Array of all timers that are currently
194
+ running. Useful for detecting cases where you might be keeping time in multiple
195
+ places simultaneously
196
+
197
+ `TimeUp.print_summary([io])` - Pretty-prints a multi-line summary of all your
198
+ timers' total elapsed times to standard output (or the provided
199
+ [IO](https://ruby-doc.org/core-3.0.1/IO.html))
200
+
201
+ `TimeUp.print_detailed_summary([io])` - Pretty-prints a multi-line summary of
202
+ all your timers' elapsed times and basic statistics to standard output (or the
203
+ provided [IO](https://ruby-doc.org/core-3.0.1/IO.html))
153
204
 
154
205
  `TimeUp.stop_all` - Stops all timers
155
206
 
@@ -166,6 +217,23 @@ reference to them
166
217
 
167
218
  `elapsed` - A `Float` of the total elapsed seconds the timer has been running
168
219
 
220
+ `timings` - Returns an Array of each recorded start-to-stop duration of the
221
+ timer (including the current one, if the timer is running)
222
+
223
+ `count` - The number of times the timer has been started and stopped
224
+
225
+ `min` - The shortest recording of the timer
226
+
227
+ `max` - The longest recording of the timer
228
+
229
+ `mean` - The arithmetic mean of all recorded durations of the timer
230
+
231
+ `median(name)` - The median of all recordings by the timer
232
+
233
+ `percentile(name, percent)` - The timing for the given
234
+ [percentile](https://en.wikipedia.org/wiki/Percentile) of all recordings by the
235
+ timer
236
+
169
237
  `active?` - Returns `true` if the timer is running
170
238
 
171
239
  `reset(force: false)` - Resets the timer to 0 elapsed seconds. If `force` is
data/lib/time_up.rb CHANGED
@@ -3,132 +3,265 @@ require_relative "time_up/version"
3
3
  module TimeUp
4
4
  class Error < StandardError; end
5
5
 
6
- @timers = {}
7
- def self.start(name, &blk)
8
- raise Error.new("Timer name must be a String or Symbol") unless name.is_a?(Symbol) || name.is_a?(String)
9
- timer = @timers[name] ||= Timer.new(name)
10
- timer.start
11
- if blk
12
- blk.call
13
- timer.stop
14
- end
15
- timer
16
- end
6
+ Thread.current[:time_up_timers] = {}
17
7
 
18
- # Delegate methods
19
8
  def self.timer(name)
20
- @timers[name]
9
+ __timers[name] ||= Timer.new(name)
21
10
  end
22
11
 
23
- def self.stop(name)
24
- __ensure_timer(name)
25
- @timers[name].stop
12
+ # Delegate methods
13
+ def self.start(name, &blk)
14
+ timer(name).start(&blk)
26
15
  end
27
16
 
28
- def self.elapsed(name)
29
- __ensure_timer(name)
30
- @timers[name].elapsed
17
+ [
18
+ :stop,
19
+ :reset,
20
+ :elapsed,
21
+ :timings,
22
+ :count,
23
+ :min,
24
+ :max,
25
+ :mean,
26
+ :median
27
+ ].each do |method_name|
28
+ define_singleton_method method_name do |name|
29
+ __ensure_timer(name)
30
+ __timers[name].send(method_name)
31
+ end
31
32
  end
32
33
 
33
- def self.reset(name)
34
+ def self.percentile(name, percentage)
34
35
  __ensure_timer(name)
35
- @timers[name].reset
36
+ __timers[name].percentile(percentage)
36
37
  end
37
38
 
38
39
  # Interrogative methods
39
40
  def self.total_elapsed
40
- @timers.values.sum(&:elapsed)
41
+ __timers.values.sum(&:elapsed)
41
42
  end
42
43
 
43
44
  def self.all_elapsed
44
- @timers.values.map { |timer|
45
+ __timers.values.map { |timer|
45
46
  [timer.name, timer.elapsed]
46
47
  }.to_h
47
48
  end
48
49
 
50
+ def self.all_stats
51
+ __timers.values.map { |timer|
52
+ [timer.name, {
53
+ elapsed: timer.elapsed,
54
+ count: timer.count,
55
+ min: timer.min,
56
+ max: timer.max,
57
+ mean: timer.mean,
58
+ median: timer.median,
59
+ "95th": timer.percentile(95)
60
+ }]
61
+ }.to_h
62
+ end
63
+
49
64
  def self.active_timers
50
- @timers.values.select(&:active?)
65
+ __timers.values.select(&:active?)
51
66
  end
52
67
 
53
68
  def self.print_summary(io = $stdout)
54
- longest_name_length = @timers.values.map { |t| t.name.inspect.size }.max
55
- summaries = @timers.values.map { |timer|
69
+ longest_name_length = __timers.values.map { |t| t.name.inspect.size }.max
70
+ summaries = __timers.values.map { |timer|
56
71
  name = "#{timer.name.inspect}#{"*" if timer.active?}".ljust(longest_name_length + 1)
57
72
  "#{name}\t#{"%.5f" % timer.elapsed}s"
58
73
  }
59
74
  io.puts <<~SUMMARY
60
75
 
61
- TimeUp timers summary
76
+ TimeUp summary
62
77
  ========================
63
78
  #{summaries.join("\n")}
64
79
 
65
- #{"* Denotes that the timer is still active\n" if @timers.values.any?(&:active?)}
80
+ #{"* Denotes that the timer is still active\n" if __timers.values.any?(&:active?)}
81
+ SUMMARY
82
+ end
83
+
84
+ def self.print_detailed_summary(io = $stdout)
85
+ cols = {
86
+ names: ["Name"],
87
+ elapsed: ["Elapsed"],
88
+ count: ["Count"],
89
+ min: ["Min"],
90
+ max: ["Max"],
91
+ mean: ["Mean"],
92
+ median: ["Median"],
93
+ "95th": ["95th %"]
94
+ }
95
+ __timers.values.each { |timer|
96
+ cols[:names] << "#{timer.name.inspect}#{"*" if timer.active?}"
97
+ cols[:elapsed] << "%.5f" % timer.elapsed
98
+ cols[:count] << timer.count.to_s
99
+ cols[:min] << "%.5f" % timer.min
100
+ cols[:max] << "%.5f" % timer.max
101
+ cols[:mean] << "%.5f" % timer.mean
102
+ cols[:median] << "%.5f" % timer.median
103
+ cols[:"95th"] << "%.5f" % timer.percentile(95)
104
+ }
105
+
106
+ widths = cols.map { |name, vals|
107
+ [name, vals.map(&:length).max]
108
+ }.to_h
109
+
110
+ rows = cols[:names].size.times.map { |i|
111
+ if i == 0
112
+ cols.keys.map { |name|
113
+ cols[name][i].center(widths[name])
114
+ }
115
+ else
116
+ cols.keys.map { |name|
117
+ cols[name][i].ljust(widths[name])
118
+ }
119
+ end
120
+ }
121
+
122
+ full_width = widths.values.sum + (rows[0].size - 1) * 3
123
+ io.puts <<~SUMMARY
124
+
125
+ #{"=" * full_width}
126
+ #{rows[0].join(" | ")}
127
+ #{"-" * full_width}
128
+ #{rows[1..-1].map { |row| row.join(" | ") }.join("\n")}
129
+
130
+ #{"* Denotes that the timer is still active\n" if __timers.values.any?(&:active?)}
66
131
  SUMMARY
67
132
  end
68
133
 
69
134
  # Iterative methods
70
135
  def self.stop_all
71
- @timers.values.each(&:stop)
136
+ __timers.values.each(&:stop)
72
137
  end
73
138
 
74
139
  def self.reset_all
75
- @timers.values.each(&:reset)
140
+ __timers.values.each(&:reset)
76
141
  end
77
142
 
78
143
  def self.delete_all
79
- @timers.values.each { |t| t.reset(force: true) }
80
- @timers = {}
144
+ __timers.values.each { |t| t.reset(force: true) }
145
+ Thread.current[:time_up_timers] = {}
81
146
  end
82
147
 
83
148
  # Internal methods
149
+ def self.__timers
150
+ Thread.current[:time_up_timers]
151
+ end
152
+
84
153
  def self.__ensure_timer(name)
85
- raise Error.new("No timer named #{name.inspect}") unless @timers[name]
154
+ raise Error.new("No timer named #{name.inspect}") unless __timers[name]
86
155
  end
87
156
 
88
157
  class Timer
89
158
  attr_reader :name
90
159
 
91
160
  def initialize(name)
161
+ validate!(name)
92
162
  @name = name
93
163
  @start_time = nil
94
- @elapsed = 0.0
164
+ @total_elapsed = 0.0
165
+ @past_timings = []
95
166
  end
96
167
 
97
- def start
168
+ def start(&blk)
98
169
  @start_time ||= now
170
+ if blk
171
+ blk.call.tap do
172
+ stop
173
+ end
174
+ else
175
+ self
176
+ end
99
177
  end
100
178
 
101
179
  def stop
102
180
  if @start_time
103
- @elapsed += now - @start_time
181
+ duration = now - @start_time
182
+ @past_timings.push(duration)
183
+ @total_elapsed += duration
184
+
104
185
  @start_time = nil
105
186
  end
106
- @elapsed
187
+ @total_elapsed
107
188
  end
108
189
 
109
190
  def elapsed
110
191
  if active?
111
- @elapsed + (now - @start_time)
192
+ @total_elapsed + (now - @start_time)
112
193
  else
113
- @elapsed
194
+ @total_elapsed
114
195
  end
115
196
  end
116
197
 
117
- def active?
118
- !!@start_time
119
- end
120
-
121
198
  def reset(force: false)
122
199
  if force
123
200
  @start_time = nil
124
201
  elsif !@start_time.nil?
125
202
  @start_time = now
126
203
  end
127
- @elapsed = 0.0
204
+ @total_elapsed = 0.0
205
+ @past_timings = []
206
+ end
207
+
208
+ def count
209
+ timings.size
210
+ end
211
+
212
+ def min
213
+ timings.min
214
+ end
215
+
216
+ def max
217
+ timings.max
218
+ end
219
+
220
+ def mean
221
+ times = timings
222
+ return if times.empty?
223
+ times.sum / times.size
224
+ end
225
+
226
+ def median
227
+ times = timings.sort
228
+ return if times.empty?
229
+ (times[(times.size - 1) / 2] + times[times.size / 2]) / 2.0
230
+ end
231
+
232
+ def percentile(percent)
233
+ times = timings.sort
234
+ return if times.empty?
235
+ return 0 if percent <= 0
236
+ return max if percent >= 100
237
+ return times.first if times.size == 1
238
+ position = (percent / 100.0) * (times.size - 1)
239
+
240
+ partial_ratio = position - position.floor
241
+ whole, partial = times[position.floor, 2]
242
+ whole + (partial - whole) * partial_ratio
243
+ end
244
+
245
+ def timings
246
+ if active?
247
+ @past_timings + [now - @start_time]
248
+ else
249
+ @past_timings
250
+ end
251
+ end
252
+
253
+ def active?
254
+ !!@start_time
128
255
  end
129
256
 
130
257
  private
131
258
 
259
+ def validate!(name)
260
+ unless name.is_a?(Symbol) || name.is_a?(String)
261
+ raise Error.new("Timer name must be a String or Symbol")
262
+ end
263
+ end
264
+
132
265
  def now
133
266
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
134
267
  end
@@ -1,3 +1,3 @@
1
1
  module TimeUp
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.5"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: time_up
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-07-15 00:00:00.000000000 Z
11
+ date: 2021-07-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -19,6 +19,7 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - ".github/workflows/main.yml"
21
21
  - ".gitignore"
22
+ - ".standard.yml"
22
23
  - CHANGELOG.md
23
24
  - Gemfile
24
25
  - Gemfile.lock