time_up 0.0.1 → 0.0.5

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: 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