ring_cache 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +88 -0
- data/Rakefile +8 -0
- data/lib/ring_cache/version.rb +3 -0
- data/lib/ring_cache.rb +120 -0
- data/ring_cache.gemspec +23 -0
- data/test/random_data_generator.rb +16 -0
- data/test/test_helper.rb +10 -0
- data/test/test_ring_cache.rb +186 -0
- data/test/test_ring_cache_performance.rb +52 -0
- metadata +95 -0
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Alvaro Redondo
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# RingCache
|
2
|
+
|
3
|
+
RingCache is an in-memory cache that emulates a ring buffer, in which older elements are evicted to make room for new ones. It is mostly useful in situations in which it is not worth it, or possible, to keep all accessed data in memory, and some elements are more frequently accessed than others.
|
4
|
+
|
5
|
+
As a ring buffer, it can work with a fix capacity. In addition, it allows the possibility of specifying a target hit rate, above which it will evict the elements that were accessed last. This should make it easier to optimize the amount of memory used when the hit rate becomes insensitive to the capacity over a given threshold.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
RingCache does not have any dependency apart from Ruby.
|
10
|
+
|
11
|
+
To install with Bundle, add the following line to the Gemfile:
|
12
|
+
|
13
|
+
gem 'ring_cache'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Otherwise, it can be installed with Rubygems as follows:
|
20
|
+
|
21
|
+
$ gem install ring_cache
|
22
|
+
|
23
|
+
## Cache Initialization
|
24
|
+
|
25
|
+
All options accepted by RingCache are set when initializing the cache.
|
26
|
+
|
27
|
+
The options related to the cache size are:
|
28
|
+
|
29
|
+
* `capacity`: The maximum number of elements that the cache will hold. By default, the capacity is unlimited.
|
30
|
+
|
31
|
+
* `target_hit_rate`: The cache will keep on growing in size until this target is achieved. Then, it will evict elements to make room for new ones to maintain its current size—as long as the hit rate is kept over this threshold. If the hit rate falls below the threshold, the cache will increase its size again. Size is always limited by the `capacity` option. By default, there is no target hit rate.
|
32
|
+
|
33
|
+
The following options allow some control over the stored data:
|
34
|
+
|
35
|
+
* `duplicate_on_store` : Store a duplicate of the element obtained with `dup` rather than the element provided to the cache.
|
36
|
+
* `duplicate_on_retrieve`: Return a duplicate of the accessed data rather than the accessed data itself.
|
37
|
+
* `execute_on_retrieve`: Method or array of methods that should be executed on accessed data before returning it. This could be used, for example, to ensure that returned data is fresh.
|
38
|
+
|
39
|
+
Initialization example:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
cache = RingCache.new(
|
43
|
+
capacity: 2_000,
|
44
|
+
target_hit_rate: 0.9,
|
45
|
+
execute_on_retrieve: :reload
|
46
|
+
)
|
47
|
+
```
|
48
|
+
|
49
|
+
## Usage
|
50
|
+
|
51
|
+
To access data, use the `read` and `write` methods:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
cache.write(:example, 'Lorem ipsum')
|
55
|
+
test = cache.read(:example)
|
56
|
+
# => "Lorem ipsum"
|
57
|
+
```
|
58
|
+
|
59
|
+
Both keys and values can be any data type.
|
60
|
+
|
61
|
+
Use `fetch` as a shortcut to provide missing, more-costly-to-load data in a block:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
cache.fetch(:example) do
|
65
|
+
'Lorem ipsum'
|
66
|
+
end
|
67
|
+
# => "Lorem ipsum"
|
68
|
+
```
|
69
|
+
|
70
|
+
Use `evict` to remove an element from the cache:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
cache.evict(:example)
|
74
|
+
# => true
|
75
|
+
```
|
76
|
+
|
77
|
+
And `reset` to completely erase the cache contents—while maintaining initialization options.
|
78
|
+
|
79
|
+
There are other methods that just return information:
|
80
|
+
|
81
|
+
* `has_key?(key)`: Returns true if the cache contains an element indexed by this key. Otherwise, false.
|
82
|
+
* `hit_rate`: Current hit rate of the cache. This is a number between 0 and 1.
|
83
|
+
* `last_access(key)`: Time when the element indexed by this key was last accessed.
|
84
|
+
* `size`: Number of elements currently stored in the cache.
|
85
|
+
|
86
|
+
## Contributing
|
87
|
+
|
88
|
+
Please, fork the repository, make your changes, and submit a pull request. Thanks!
|
data/Rakefile
ADDED
data/lib/ring_cache.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
lib = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'ring_cache/version'
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
class RingCache
|
8
|
+
attr_reader :capacity, :target_hit_rate
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@duplicate_on_store = options.fetch(:duplicate_on_store, false)
|
12
|
+
@duplicate_on_retrieve = options.fetch(:duplicate_on_retrieve, false)
|
13
|
+
|
14
|
+
execute_on_retrieve = options.fetch(:execute_on_retrieve, [])
|
15
|
+
@execute_on_retrieve = execute_on_retrieve.kind_of?(Array) ? execute_on_retrieve : [execute_on_retrieve]
|
16
|
+
|
17
|
+
@capacity = options.fetch(:capacity, nil)
|
18
|
+
@target_hit_rate = options.fetch(:target_hit_rate, nil)
|
19
|
+
unless @target_hit_rate.nil? or (@target_hit_rate > 0.0 and @target_hit_rate < 1.0)
|
20
|
+
raise ArgumentError, 'Invalid target_hit_rate'
|
21
|
+
end
|
22
|
+
|
23
|
+
reset
|
24
|
+
end
|
25
|
+
|
26
|
+
def evict(key)
|
27
|
+
if @cache.has_key?(key)
|
28
|
+
@access_time_index.delete([@cache[key][:last_accessed_at], key])
|
29
|
+
@cache.delete(key)
|
30
|
+
true
|
31
|
+
else
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch(key, &block)
|
37
|
+
unless (data = read(key))
|
38
|
+
data = block.call
|
39
|
+
write(key, data)
|
40
|
+
end
|
41
|
+
data
|
42
|
+
end
|
43
|
+
|
44
|
+
def has_key?(key)
|
45
|
+
@cache.has_key?(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
def hit_rate
|
49
|
+
(@access_count > 0) ? (@hit_count / @access_count.to_f) : 0.0
|
50
|
+
end
|
51
|
+
|
52
|
+
def last_access(key)
|
53
|
+
has_key?(key) ? @cache[key][:last_accessed_at] : nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def read(key)
|
57
|
+
@access_count += 1
|
58
|
+
|
59
|
+
if @cache.has_key?(key)
|
60
|
+
access_time = Time.now
|
61
|
+
@access_time_index.delete([@cache[key][:last_accessed_at], key])
|
62
|
+
@access_time_index << [access_time, key]
|
63
|
+
@cache[key][:last_accessed_at] = access_time
|
64
|
+
|
65
|
+
@hit_count += 1
|
66
|
+
|
67
|
+
data = @cache[key][:data]
|
68
|
+
data = data.dup if @duplicate_on_retrieve and !data.nil?
|
69
|
+
|
70
|
+
unless @execute_on_retrieve.empty? or data.nil?
|
71
|
+
@execute_on_retrieve.each do |method|
|
72
|
+
method = method.to_sym
|
73
|
+
if data.respond_to?(method)
|
74
|
+
data.send(method)
|
75
|
+
elsif data.kind_of?(Enumerable) and data.all? { |d| d.respond_to?(method) }
|
76
|
+
data.each { |d| d.send(method) }
|
77
|
+
else
|
78
|
+
raise RuntimeError, "Retrieved data does not respond to #{method.inspect}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
data
|
84
|
+
else
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def reset
|
90
|
+
@cache = {}
|
91
|
+
@access_time_index = SortedSet.new
|
92
|
+
@access_count = 0
|
93
|
+
@hit_count = 0
|
94
|
+
true
|
95
|
+
end
|
96
|
+
|
97
|
+
def size
|
98
|
+
@cache.size
|
99
|
+
end
|
100
|
+
|
101
|
+
def write(key, data)
|
102
|
+
evict_oldest if must_evict?
|
103
|
+
data = data.dup if @duplicate_on_store and !data.nil?
|
104
|
+
access_time = Time.now
|
105
|
+
@cache[key] = { last_accessed_at: access_time, data: data }
|
106
|
+
@access_time_index << [access_time, key]
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def evict_oldest
|
112
|
+
access_time_index_entry = @access_time_index.first
|
113
|
+
@cache.delete(access_time_index_entry[1])
|
114
|
+
@access_time_index.delete(access_time_index_entry)
|
115
|
+
end
|
116
|
+
|
117
|
+
def must_evict?
|
118
|
+
(capacity and size >= capacity) or (target_hit_rate and hit_rate >= target_hit_rate)
|
119
|
+
end
|
120
|
+
end
|
data/ring_cache.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ring_cache/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'ring_cache'
|
8
|
+
spec.version = RingCache::VERSION
|
9
|
+
spec.authors = ['Alvaro Redondo']
|
10
|
+
spec.email = ['alvaro@redondo.name']
|
11
|
+
spec.summary = %q{In-memory cache that emulates a ring buffer.}
|
12
|
+
spec.description = %q{RingCache is an in-memory cache that emulates a ring buffer, in which older elements are evicted to make room for new ones.}
|
13
|
+
spec.homepage = 'https://github.com/aredondo/ring_cache'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module RandomDataGenerator
|
2
|
+
private
|
3
|
+
|
4
|
+
def random_data(element_count, key_length)
|
5
|
+
data = []
|
6
|
+
letters = ('a' .. 'z').to_a
|
7
|
+
|
8
|
+
while data.size < element_count
|
9
|
+
key = letters.sample(key_length).join
|
10
|
+
content = letters.sample(1)
|
11
|
+
data << { key: key, content: content }
|
12
|
+
end
|
13
|
+
|
14
|
+
data
|
15
|
+
end
|
16
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
lib = File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'ring_cache'
|
5
|
+
require 'minitest'
|
6
|
+
require 'minitest/autorun'
|
7
|
+
require 'minitest/reporters'
|
8
|
+
require File.expand_path('random_data_generator', File.dirname(__FILE__))
|
9
|
+
|
10
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
@@ -0,0 +1,186 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.expand_path('test_helper', File.dirname(__FILE__))
|
4
|
+
|
5
|
+
class TestRingCache < Minitest::Test
|
6
|
+
include RandomDataGenerator
|
7
|
+
|
8
|
+
def test_generic_checks
|
9
|
+
c = RingCache.new(capacity: 3)
|
10
|
+
c.write(:a, 1)
|
11
|
+
assert_equal 0.0, c.hit_rate
|
12
|
+
assert_equal 1, c.read(:a)
|
13
|
+
assert_equal 1.0, c.hit_rate
|
14
|
+
assert c.has_key?(:a)
|
15
|
+
assert_kind_of Time, c.last_access(:a)
|
16
|
+
assert_equal 1, c.size
|
17
|
+
assert_equal 1, c.instance_variable_get(:@access_time_index).size
|
18
|
+
|
19
|
+
c.write(:b, 2)
|
20
|
+
assert_equal 2, c.read(:b)
|
21
|
+
assert_equal 1.0, c.hit_rate
|
22
|
+
assert c.has_key?(:b)
|
23
|
+
assert_kind_of Time, c.last_access(:b)
|
24
|
+
assert_equal 2, c.size
|
25
|
+
assert_equal 2, c.instance_variable_get(:@access_time_index).size
|
26
|
+
|
27
|
+
c.write(:c, 3)
|
28
|
+
assert_equal 3, c.read(:c)
|
29
|
+
assert_equal 1.0, c.hit_rate
|
30
|
+
assert c.has_key?(:c)
|
31
|
+
assert_kind_of Time, c.last_access(:c)
|
32
|
+
assert_equal 3, c.size
|
33
|
+
assert_equal 3, c.instance_variable_get(:@access_time_index).size
|
34
|
+
|
35
|
+
c.write(:d, 4)
|
36
|
+
assert_equal 4, c.read(:d)
|
37
|
+
assert_equal 1.0, c.hit_rate
|
38
|
+
assert c.has_key?(:d)
|
39
|
+
assert_kind_of Time, c.last_access(:d)
|
40
|
+
assert_equal 3, c.size
|
41
|
+
assert_equal 3, c.instance_variable_get(:@access_time_index).size
|
42
|
+
refute c.has_key?(:a)
|
43
|
+
|
44
|
+
value = c.fetch(:b) do
|
45
|
+
4
|
46
|
+
end
|
47
|
+
assert_equal 2, value
|
48
|
+
assert_equal 1.0, c.hit_rate
|
49
|
+
|
50
|
+
value = c.fetch(:e) do
|
51
|
+
5
|
52
|
+
end
|
53
|
+
assert_equal 5, value
|
54
|
+
assert_equal 3, c.size
|
55
|
+
assert_equal 3, c.instance_variable_get(:@access_time_index).size
|
56
|
+
refute c.has_key?(:c)
|
57
|
+
assert_equal 5 / 6.0, c.hit_rate
|
58
|
+
|
59
|
+
assert c.last_access(:b) > c.last_access(:d)
|
60
|
+
assert c.last_access(:d) < c.last_access(:e)
|
61
|
+
|
62
|
+
c.evict(:d)
|
63
|
+
refute c.has_key?(:d)
|
64
|
+
assert_equal 2, c.size
|
65
|
+
assert_equal 2, c.instance_variable_get(:@access_time_index).size
|
66
|
+
|
67
|
+
value = c.read(:another)
|
68
|
+
assert_equal 5 / 7.0, c.hit_rate
|
69
|
+
|
70
|
+
c.reset
|
71
|
+
assert_equal 0, c.size
|
72
|
+
assert_equal 0.0, c.hit_rate
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_execute_method_on_retrieve
|
76
|
+
test_class = Class.new
|
77
|
+
test_class.class_eval do
|
78
|
+
attr_reader :reloaded
|
79
|
+
define_method(:inititalize) { @reloaded = false }
|
80
|
+
define_method(:reload) { @reloaded = true }
|
81
|
+
end
|
82
|
+
|
83
|
+
cache = RingCache.new(execute_on_retrieve: :reload)
|
84
|
+
|
85
|
+
data = test_class.new
|
86
|
+
refute data.reloaded
|
87
|
+
cache.write(:d1, data)
|
88
|
+
retrieved_data = cache.read(:d1)
|
89
|
+
assert retrieved_data.reloaded, retrieved_data.reloaded.inspect
|
90
|
+
|
91
|
+
data = [
|
92
|
+
test_class.new,
|
93
|
+
test_class.new,
|
94
|
+
test_class.new
|
95
|
+
]
|
96
|
+
refute data.any? { |d| d.reloaded }
|
97
|
+
cache.write(:d2, data)
|
98
|
+
retrieved_data = cache.read(:d2)
|
99
|
+
assert retrieved_data.all? { |e| e.reloaded }, retrieved_data.inspect
|
100
|
+
|
101
|
+
data = Object.new
|
102
|
+
refute_respond_to data, :reloaded
|
103
|
+
cache.write(:d3, data)
|
104
|
+
assert_raises RuntimeError do
|
105
|
+
retrieved_data = cache.read(:d3)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_object_duplication_on_retrieve
|
110
|
+
c = RingCache.new(duplicate_on_retrieve: true)
|
111
|
+
data = {a: 1, b: 2, c: 3}
|
112
|
+
c.write(:d, data)
|
113
|
+
assert c.has_key?(:d)
|
114
|
+
retrieved_data = c.read(:d)
|
115
|
+
assert data == retrieved_data
|
116
|
+
refute data.equal?(retrieved_data)
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_object_duplication_on_store
|
120
|
+
c = RingCache.new(duplicate_on_store: true)
|
121
|
+
data = {a: 1, b: 2, c: 3}
|
122
|
+
c.write(:d, data)
|
123
|
+
assert c.has_key?(:d)
|
124
|
+
retrieved_data = c.read(:d)
|
125
|
+
assert data == retrieved_data
|
126
|
+
refute data.equal?(retrieved_data)
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_random_data
|
130
|
+
cache = RingCache.new(capacity: 1_000)
|
131
|
+
data = random_data(2_000, 10)
|
132
|
+
|
133
|
+
data.each do |element|
|
134
|
+
cache.fetch(element[:key]) do
|
135
|
+
element[:content]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
assert_equal 1_000, cache.size
|
140
|
+
access_time_index = cache.instance_variable_get(:@access_time_index)
|
141
|
+
cache_contents = cache.instance_variable_get(:@cache)
|
142
|
+
assert_equal cache_contents.size, access_time_index.size
|
143
|
+
|
144
|
+
access_time_index.each do |access_time_index_entry|
|
145
|
+
assert cache_contents.has_key?(access_time_index_entry[1])
|
146
|
+
assert_equal access_time_index_entry[0],
|
147
|
+
cache_contents[access_time_index_entry[1]][:last_accessed_at]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_target_hit_rate
|
152
|
+
cache = RingCache.new(capacity: 4, target_hit_rate: 0.5)
|
153
|
+
|
154
|
+
cache.fetch(:a) { 1 }
|
155
|
+
cache.fetch(:b) { 2 }
|
156
|
+
cache.read(:a)
|
157
|
+
cache.read(:b)
|
158
|
+
|
159
|
+
assert_equal 0.5, cache.hit_rate
|
160
|
+
assert_equal 2, cache.size
|
161
|
+
|
162
|
+
cache.read(:a)
|
163
|
+
cache.read(:b)
|
164
|
+
cache.fetch(:c) { 3 }
|
165
|
+
|
166
|
+
assert_equal 4 / 7.0, cache.hit_rate
|
167
|
+
assert_equal 2, cache.size
|
168
|
+
assert cache.has_key?(:c)
|
169
|
+
|
170
|
+
cache.fetch(:d) { 4 }
|
171
|
+
assert_equal 4 / 8.0, cache.hit_rate
|
172
|
+
assert_equal 2, cache.size
|
173
|
+
assert cache.has_key?(:d)
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_works_with_nil
|
177
|
+
cache = RingCache.new(
|
178
|
+
duplicate_on_store: true,
|
179
|
+
duplicate_on_retrieve: true,
|
180
|
+
execute_on_retrieve: :reload
|
181
|
+
)
|
182
|
+
data = nil
|
183
|
+
cache.write(:d, data)
|
184
|
+
assert_nil cache.read(:d)
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.expand_path('test_helper', File.dirname(__FILE__))
|
4
|
+
require 'minitest/benchmark'
|
5
|
+
|
6
|
+
class TestRingCachePerformance < Minitest::Benchmark
|
7
|
+
include RandomDataGenerator
|
8
|
+
|
9
|
+
def self.bench_range
|
10
|
+
bench_exp(100, 100_000, 10)
|
11
|
+
end
|
12
|
+
|
13
|
+
def setup
|
14
|
+
@data = random_data(1_000, 10)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Computer-dependant -- but should be a reasonable baseline
|
18
|
+
def bench_performance
|
19
|
+
if ENV['BENCH']
|
20
|
+
validation = lambda { |ranges, times|
|
21
|
+
count_per_second = ranges.last / times.last.to_f
|
22
|
+
assert count_per_second > 100_000, 'Count per second: %.2f' % count_per_second
|
23
|
+
}
|
24
|
+
|
25
|
+
assert_performance validation do |n|
|
26
|
+
run_cache(n)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def bench_performance_linear
|
32
|
+
if ENV['BENCH']
|
33
|
+
assert_performance_linear do |n|
|
34
|
+
run_cache(n)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def run_cache(times, capacity = 1_000)
|
42
|
+
cache = RingCache.new(capacity: capacity)
|
43
|
+
count = 0
|
44
|
+
while count < times
|
45
|
+
element = @data.sample
|
46
|
+
cache.fetch(element[:key]) do
|
47
|
+
element[:content]
|
48
|
+
end
|
49
|
+
count += 1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ring_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.0'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alvaro Redondo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-10-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.6'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.6'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: RingCache is an in-memory cache that emulates a ring buffer, in which
|
47
|
+
older elements are evicted to make room for new ones.
|
48
|
+
email:
|
49
|
+
- alvaro@redondo.name
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- LICENSE.txt
|
57
|
+
- README.md
|
58
|
+
- Rakefile
|
59
|
+
- lib/ring_cache.rb
|
60
|
+
- lib/ring_cache/version.rb
|
61
|
+
- ring_cache.gemspec
|
62
|
+
- test/random_data_generator.rb
|
63
|
+
- test/test_helper.rb
|
64
|
+
- test/test_ring_cache.rb
|
65
|
+
- test/test_ring_cache_performance.rb
|
66
|
+
homepage: https://github.com/aredondo/ring_cache
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 1.8.29
|
88
|
+
signing_key:
|
89
|
+
specification_version: 3
|
90
|
+
summary: In-memory cache that emulates a ring buffer.
|
91
|
+
test_files:
|
92
|
+
- test/random_data_generator.rb
|
93
|
+
- test/test_helper.rb
|
94
|
+
- test/test_ring_cache.rb
|
95
|
+
- test/test_ring_cache_performance.rb
|