`helper.rb` is a legacy name from Bacon days. Since we use RSpec, the convention
is to use `spec/spec_helper.rb`. Applying the principle of least astonishment
here.
Currently, the Ring class is written with help of Hash as backend store.
According to the comments, the implementation should behave like a circular
buffer, however it doesn't. Upon reaching maximum capacity Ring doesn't replace
old elements but keeps writing to new cells, deleting old cells, so that the
hash contains `nil` entries.
The new implementation is based on Array and seems to be closer to the actual
Ring. Older elemens get overwritten with newer ones.
This class also includes Enumerable, however none of our APIs take advantage of
it, so it seems like an overkill. There was also a problem with with this API
because of the above-mentioned nils. For example, if the ring exceeds its
maximum size, then callin `Enumerable#first` on it returns `nil`.
The new implementation deals with this with removal of Enumerable. The `#[]`
syntax is preserved, and now `ring[0]` returns an actual element instead of
`nil`. In case users need the Enumerable functionality, they can call
`Ring#to_a` to build the array, which supports the wanted methods.
As for the speed, the new implementation is:
* slower overall because it's thread-safe
* faster without mutexes for `#<<`
* slower without mutexes for `#[]`
Benchmark for old implementation:
```
Warming up --------------------------------------
Ring#<< 223.451k i/100ms
Ring#[] 2.837k i/100ms
Calculating -------------------------------------
Ring#<< 223.157B (± 3.4%) i/s - 778.097B
Ring#[] 82.485M (± 9.4%) i/s - 402.602M in 4.957792s
```
Benchmark for this implementation:
```
Warming up --------------------------------------
Ring#<< 211.587k i/100ms
Ring#[] 1.974k i/100ms
Calculating -------------------------------------
Ring#<< 211.385B (± 2.8%) i/s - 698.439B
Ring#[] 40.292M (±17.0%) i/s - 190.069M in 4.971195s
```
The benchmark:
```rb
require './lib/pry'
require 'benchmark/ips'
Benchmark.ips do |x|
empty_ring = Pry::Ring.new(100)
populated_ring = Pry::Ring.new(100)
150.times { |i| populated_ring << i }
x.report("Ring#<<") do |times|
empty_ring << times
end
x.report("Ring#[]") do |times|
populated_ring[0]
populated_ring[1]
populated_ring[2]
populated_ring[-1]
populated_ring[-2]
populated_ring[-3]
populated_ring[1..2]
populated_ring[-2..-1]
populated_ring[-2..3]
populated_ring[0..-1]
populated_ring[2..-1]
populated_ring[-1..10]
populated_ring[-1..0]
populated_ring[-1..1]
end
end
```
`HistoryArray` is a very specific name and it doesn't tell the reader what
it *really* means unless you read its code or the docs of the class.
On the other hand, `Ring` is a [very well-known term][1], which means exactly
what `HistoryArray` does. The alias name for it is circular buffer. I chose
`Ring` because it is shorter and used by Golang, so I expect programmers to
be familiar with `Ring`.
[1]: https://en.wikipedia.org/wiki/Circular_buffer
2018-10-20 00:36:22 +08:00
Renamed from spec/history_array_spec.rb (Browse further)