time_up 0.0.2 → 0.0.6

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: 3e94465307a2737c930d8dbd195c92fe315850abf8c7be4b1c6f88f1eec2d1bd
4
- data.tar.gz: 82cac53d1b5c18acdeb7312da3cc929bed1590e707d8ac8edeb0480fff13af29
3
+ metadata.gz: 74e2d5917cd228f02d8e8b4ae785affd6f2feeeb689db16003a1ba397653496f
4
+ data.tar.gz: 2752fe3c544f39ba6c869e0a58ed628ed18f85c7d938ebdf2b0b2a8dcc879e35
5
5
  SHA512:
6
- metadata.gz: b0a1395d51df9233140f9f7b7dd49d6cdc3bc1c50eea76425da7300a6b165230c17452ecaf7b1721c50a9b87f788775913b93ada281906ab681a19376976a34c
7
- data.tar.gz: 12856340affc8a8a298986c8b56c01198256346c5cd0caf8f7fac2726ea742d43fc05e113c7f93960c34cae08318b5c5ac912b5b5adb716e03748bef56818b1a
6
+ metadata.gz: 74cb8513be353995a49ff1a2a39eaf7247da0c8344d29bd4e6eddc0fdb5a662a22ce0473378dc8f65bf8ae42b5c4284b49f46f587b32995856ea9b12d8da51a2
7
+ data.tar.gz: 5f9d08ad26099efdcf0410108fe7fc8d156e3fd7920e0c0461b0bee4d3a47b30487f4a7eb7b4441b1d64fde68c9c2e3b908cc231e783bd1332667153575a5d21
data/.standard.yml ADDED
@@ -0,0 +1 @@
1
+ ruby_version: 2.4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # 0.0.6
2
+
3
+ - Add `TimeUp.all_timers`. It's weird that it's not a thing.
4
+
5
+ # 0.0.5
6
+
7
+ - Add `median` and `percentile` timer statistics, and added them to
8
+ `print_detailed_summary`
9
+
10
+ # 0.0.4
11
+
12
+ - Add `TimeUp.print_detailed_summary`
13
+
14
+ # 0.0.3
15
+
16
+ - Change the return value of TimeUp.start when passed a block to be the
17
+ evaluated value of the block (for easier insertion into existing code without
18
+ adding a bunch of new assignment and returns)
19
+ - Allow timer instances' `start` method to be called with a block
20
+ - Add `timings`, `count`, `min`, `max`, and `mean` methods for basic stats
21
+ tracking
22
+ - Add `TimeUp.all_stats` to roll up all these
23
+
1
24
  # 0.0.2
2
25
 
3
26
  - Switch from a module method to Thread.current variable
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- time_up (0.0.2)
4
+ time_up (0.0.6)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -11,6 +11,9 @@ process and that you want to measure in aggregate. (For example, to see how
11
11
  much time your test suite spends creating factories, truncating the database, or
12
12
  invoking a critical code path.)
13
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).
16
+
14
17
  ## Install
15
18
 
16
19
  Just run `gem install time_up` or add time_up to your Gemfile:
@@ -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
@@ -5,35 +5,35 @@ module TimeUp
5
5
 
6
6
  Thread.current[:time_up_timers] = {}
7
7
 
8
- def self.start(name, &blk)
9
- raise Error.new("Timer name must be a String or Symbol") unless name.is_a?(Symbol) || name.is_a?(String)
10
- timer = __timers[name] ||= Timer.new(name)
11
- timer.start
12
- if blk
13
- blk.call
14
- timer.stop
15
- end
16
- timer
17
- end
18
-
19
- # Delegate methods
20
8
  def self.timer(name)
21
- __timers[name]
9
+ __timers[name] ||= Timer.new(name)
22
10
  end
23
11
 
24
- def self.stop(name)
25
- __ensure_timer(name)
26
- __timers[name].stop
12
+ # Delegate methods
13
+ def self.start(name, &blk)
14
+ timer(name).start(&blk)
27
15
  end
28
16
 
29
- def self.elapsed(name)
30
- __ensure_timer(name)
31
- __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
32
32
  end
33
33
 
34
- def self.reset(name)
34
+ def self.percentile(name, percentage)
35
35
  __ensure_timer(name)
36
- __timers[name].reset
36
+ __timers[name].percentile(percentage)
37
37
  end
38
38
 
39
39
  # Interrogative methods
@@ -47,6 +47,24 @@ module TimeUp
47
47
  }.to_h
48
48
  end
49
49
 
50
+ def self.all_timers
51
+ __timers.values
52
+ end
53
+
54
+ def self.all_stats
55
+ __timers.values.map { |timer|
56
+ [timer.name, {
57
+ elapsed: timer.elapsed,
58
+ count: timer.count,
59
+ min: timer.min,
60
+ max: timer.max,
61
+ mean: timer.mean,
62
+ median: timer.median,
63
+ "95th": timer.percentile(95)
64
+ }]
65
+ }.to_h
66
+ end
67
+
50
68
  def self.active_timers
51
69
  __timers.values.select(&:active?)
52
70
  end
@@ -59,7 +77,7 @@ module TimeUp
59
77
  }
60
78
  io.puts <<~SUMMARY
61
79
 
62
- TimeUp timers summary
80
+ TimeUp summary
63
81
  ========================
64
82
  #{summaries.join("\n")}
65
83
 
@@ -67,6 +85,56 @@ module TimeUp
67
85
  SUMMARY
68
86
  end
69
87
 
88
+ def self.print_detailed_summary(io = $stdout)
89
+ cols = {
90
+ names: ["Name"],
91
+ elapsed: ["Elapsed"],
92
+ count: ["Count"],
93
+ min: ["Min"],
94
+ max: ["Max"],
95
+ mean: ["Mean"],
96
+ median: ["Median"],
97
+ "95th": ["95th %"]
98
+ }
99
+ __timers.values.each { |timer|
100
+ cols[:names] << "#{timer.name.inspect}#{"*" if timer.active?}"
101
+ cols[:elapsed] << "%.5f" % timer.elapsed
102
+ cols[:count] << timer.count.to_s
103
+ cols[:min] << "%.5f" % timer.min
104
+ cols[:max] << "%.5f" % timer.max
105
+ cols[:mean] << "%.5f" % timer.mean
106
+ cols[:median] << "%.5f" % timer.median
107
+ cols[:"95th"] << "%.5f" % timer.percentile(95)
108
+ }
109
+
110
+ widths = cols.map { |name, vals|
111
+ [name, vals.map(&:length).max]
112
+ }.to_h
113
+
114
+ rows = cols[:names].size.times.map { |i|
115
+ if i == 0
116
+ cols.keys.map { |name|
117
+ cols[name][i].center(widths[name])
118
+ }
119
+ else
120
+ cols.keys.map { |name|
121
+ cols[name][i].ljust(widths[name])
122
+ }
123
+ end
124
+ }
125
+
126
+ full_width = widths.values.sum + (rows[0].size - 1) * 3
127
+ io.puts <<~SUMMARY
128
+
129
+ #{"=" * full_width}
130
+ #{rows[0].join(" | ")}
131
+ #{"-" * full_width}
132
+ #{rows[1..-1].map { |row| row.join(" | ") }.join("\n")}
133
+
134
+ #{"* Denotes that the timer is still active\n" if __timers.values.any?(&:active?)}
135
+ SUMMARY
136
+ end
137
+
70
138
  # Iterative methods
71
139
  def self.stop_all
72
140
  __timers.values.each(&:stop)
@@ -94,46 +162,110 @@ module TimeUp
94
162
  attr_reader :name
95
163
 
96
164
  def initialize(name)
165
+ validate!(name)
97
166
  @name = name
98
167
  @start_time = nil
99
- @elapsed = 0.0
168
+ @total_elapsed = 0.0
169
+ @past_timings = []
100
170
  end
101
171
 
102
- def start
172
+ def start(&blk)
103
173
  @start_time ||= now
174
+ if blk
175
+ blk.call.tap do
176
+ stop
177
+ end
178
+ else
179
+ self
180
+ end
104
181
  end
105
182
 
106
183
  def stop
107
184
  if @start_time
108
- @elapsed += now - @start_time
185
+ duration = now - @start_time
186
+ @past_timings.push(duration)
187
+ @total_elapsed += duration
188
+
109
189
  @start_time = nil
110
190
  end
111
- @elapsed
191
+ @total_elapsed
112
192
  end
113
193
 
114
194
  def elapsed
115
195
  if active?
116
- @elapsed + (now - @start_time)
196
+ @total_elapsed + (now - @start_time)
117
197
  else
118
- @elapsed
198
+ @total_elapsed
119
199
  end
120
200
  end
121
201
 
122
- def active?
123
- !!@start_time
124
- end
125
-
126
202
  def reset(force: false)
127
203
  if force
128
204
  @start_time = nil
129
205
  elsif !@start_time.nil?
130
206
  @start_time = now
131
207
  end
132
- @elapsed = 0.0
208
+ @total_elapsed = 0.0
209
+ @past_timings = []
210
+ end
211
+
212
+ def count
213
+ timings.size
214
+ end
215
+
216
+ def min
217
+ timings.min
218
+ end
219
+
220
+ def max
221
+ timings.max
222
+ end
223
+
224
+ def mean
225
+ times = timings
226
+ return if times.empty?
227
+ times.sum / times.size
228
+ end
229
+
230
+ def median
231
+ times = timings.sort
232
+ return if times.empty?
233
+ (times[(times.size - 1) / 2] + times[times.size / 2]) / 2.0
234
+ end
235
+
236
+ def percentile(percent)
237
+ times = timings.sort
238
+ return if times.empty?
239
+ return 0 if percent <= 0
240
+ return max if percent >= 100
241
+ return times.first if times.size == 1
242
+ position = (percent / 100.0) * (times.size - 1)
243
+
244
+ partial_ratio = position - position.floor
245
+ whole, partial = times[position.floor, 2]
246
+ whole + (partial - whole) * partial_ratio
247
+ end
248
+
249
+ def timings
250
+ if active?
251
+ @past_timings + [now - @start_time]
252
+ else
253
+ @past_timings
254
+ end
255
+ end
256
+
257
+ def active?
258
+ !!@start_time
133
259
  end
134
260
 
135
261
  private
136
262
 
263
+ def validate!(name)
264
+ unless name.is_a?(Symbol) || name.is_a?(String)
265
+ raise Error.new("Timer name must be a String or Symbol")
266
+ end
267
+ end
268
+
137
269
  def now
138
270
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
139
271
  end
@@ -1,3 +1,3 @@
1
1
  module TimeUp
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.6"
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.2
4
+ version: 0.0.6
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