Skip to content

Commit

Permalink
Performance improvements and bug fixes to TTL::Cache.
Browse files Browse the repository at this point in the history
Added tests and benchmarks for TTL caches.
Added timecop as a development dependency.
Updated readme with TTL benchmarks and changelog.
Updated authors and emails in gemspec.
  • Loading branch information
Seberius committed Mar 29, 2015
1 parent e4f4b6d commit 0a53cea
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 39 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ cache = LruRedux::ThreadSafeCache.new(100)

see: benchmark directory (a million random lookup / store)

#### LRU
```
$ ruby ./bench/bench.rb
Rehearsal ---------------------------------------------------------
Expand All @@ -88,6 +89,22 @@ lru_redux thread safe 2.190000 0.010000 2.200000 ( 2.185512)
```

#### TTL
```
$ ruby ./bench/bench_ttl.rb
Rehearsal -----------------------------------------------------------------------
FastCache 6.240000 0.070000 6.310000 ( 6.302569)
LruRedux::TTL::Cache 4.700000 0.010000 4.710000 ( 4.712858)
LruRedux::TTL::ThreadSafeCache 6.300000 0.010000 6.310000 ( 6.319032)
LruRedux::TTL::Cache (TTL disabled) 2.460000 0.010000 2.470000 ( 2.470629)
------------------------------------------------------------- total: 19.800000sec
user system total real
FastCache 6.470000 0.070000 6.540000 ( 6.536193)
LruRedux::TTL::Cache 4.640000 0.010000 4.650000 ( 4.661793)
LruRedux::TTL::ThreadSafeCache 6.310000 0.020000 6.330000 ( 6.328840)
LruRedux::TTL::Cache (TTL disabled) 2.440000 0.000000 2.440000 ( 2.446269)
```

## Contributing

Expand All @@ -98,6 +115,10 @@ lru_redux thread safe 2.190000 0.010000 2.200000 ( 2.185512)
5. Create new Pull Request

## Changlog
###version NEXT - TBD

- New: TTL cache added. This cache is LRU like with the addition of time-based eviction. Check the TTL section in README.md for details.

###version 1.0.0 - 26-Mar-2015

- Ruby Support: Ruby 1.9+ is now required by LruRedux. If you need to use LruRedux in Ruby 1.8, please specify gem version 0.8.4 in your Gemfile. v0.8.4 is the last 1.8 compatible release and included a number of fixes and performance improvements for the Ruby 1.8 implementation. @Seberius
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ require "bundler/gem_tasks"
require 'rake/testtask'

Rake::TestTask.new do |t|
t.pattern = "test/*_test.rb"
t.pattern = "test/**/*_test.rb"
end
50 changes: 24 additions & 26 deletions bench/bench.rb
Original file line number Diff line number Diff line change
@@ -1,45 +1,43 @@
require 'bundler'
require 'lru'
require 'benchmark'
require 'lru'
require 'lru_cache'
require 'threadsafe-lru'

Bundler.require

lru = Cache::LRU.new(:max_elements => 1_000)
# Lru
lru = Cache::LRU.new(max_elements: 1_000)

# LruCache
lru_cache = LRUCache.new(1_000)

lru_redux = LruRedux::Cache.new(1_000)
lru_redux_thread_safe = LruRedux::ThreadSafeCache.new(1_000)
# ThreadSafeLru
thread_safe_lru = ThreadSafeLru::LruCache.new(1_000)

# LruRedux
redux = LruRedux::Cache.new(1_000)
redux_thread_safe = LruRedux::ThreadSafeCache.new(1_000)

puts "** LRU Benchmarks **"
Benchmark.bmbm do |bm|
bm.report 'ThreadSafeLru' do
1_000_000.times { thread_safe_lru.get(rand(2_000)) { :value } }
end

bm.report 'LRU' do
1_000_000.times { lru[rand(2_000)] ||= :value }
end

bm.report "thread safe lru" do
1_000_000.times do
thread_safe_lru.get(rand(2_000)){ :value }
end
bm.report 'LRUCache' do
1_000_000.times { lru_cache[rand(2_000)] ||= :value }
end

[
[lru, "lru gem"],
[lru_cache, "lru_cache gem"],
].each do |cache, name|
bm.report name do
1_000_000.times do
cache[rand(2_000)] ||= :value
end
end
bm.report 'LruRedux::Cache' do
1_000_000.times { redux.getset(rand(2_000)) { :value } }
end

[
[lru_redux, "lru_redux gem"],
[lru_redux_thread_safe, "lru_redux thread safe"]
].each do |cache, name|
bm.report name do
1_000_000.times do
cache.getset(rand(2_000)) { :value }
end
end
bm.report 'LruRedux::ThreadSafeCache' do
1_000_000.times { redux_thread_safe.getset(rand(2_000)) { :value } }
end
end
33 changes: 33 additions & 0 deletions bench/bench_ttl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'bundler'
require 'benchmark'
require 'fast_cache'

Bundler.require

# FastCache
fast_cache = FastCache::Cache.new(1_000, 5 * 60)

# LruRedux
redux_ttl = LruRedux::TTL::Cache.new(1_000, 5 * 60)
redux_ttl_thread_safe = LruRedux::TTL::ThreadSafeCache.new(1_000, 5 * 60)
redux_ttl_disabled = LruRedux::TTL::Cache.new(1_000, :none)

puts
puts "** TTL Benchmarks **"
Benchmark.bmbm do |bm|
bm.report 'FastCache' do
1_000_000.times { fast_cache.fetch(rand(2_000)) { :value } }
end

bm.report 'LruRedux::TTL::Cache' do
1_000_000.times { redux_ttl.getset(rand(2_000)) { :value } }
end

bm.report 'LruRedux::TTL::ThreadSafeCache' do
1_000_000.times { redux_ttl_thread_safe.getset(rand(2_000)) { :value } }
end

bm.report 'LruRedux::TTL::Cache (TTL disabled)' do
1_000_000.times { redux_ttl_disabled.getset(rand(2_000)) { :value } }
end
end
4 changes: 2 additions & 2 deletions lib/lru_redux/cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class LruRedux::Cache
def initialize(*args)
max_size, _ = args

raise ArgumentError.new(:max_size) if @max_size < 1
raise ArgumentError.new(:max_size) if max_size < 1

@max_size = max_size
@data = {}
Expand All @@ -14,7 +14,7 @@ def initialize(*args)
def max_size=(max_size)
max_size ||= @max_size

raise ArgumentError.new(:max_size) if @max_size < 1
raise ArgumentError.new(:max_size) if max_size < 1

@max_size = max_size

Expand Down
22 changes: 14 additions & 8 deletions lib/lru_redux/ttl/cache.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module LruRedux
module TTL
class Cache
attr_reader :max_size, :ttl

def initialize(*args)
max_size, ttl = args

Expand Down Expand Up @@ -48,10 +50,10 @@ def getset(key)
@data_lru[key] = value
else
result = @data_lru[key] = yield
@data_ttl = Time.now
@data_ttl[key] = Time.now.to_f

if @data_lru.size > @max_size
key, _ = @data_lru.tail
key, _ = @data_lru.first

@data_ttl.delete(key)
@data_lru.delete(key)
Expand Down Expand Up @@ -92,10 +94,10 @@ def []=(key, val)
@data_ttl.delete(key)

@data_lru[key] = val
@data_ttl[key] = Time.now
@data_ttl[key] = Time.now.to_f

if @data_lru.size > @max_size
key, _ = @data_lru.tail
key, _ = @data_lru.first

@data_ttl.delete(key)
@data_lru.delete(key)
Expand Down Expand Up @@ -145,6 +147,10 @@ def clear
@data_ttl.clear
end

def expire
ttl_evict
end

def count
@data_lru.size
end
Expand All @@ -159,22 +165,22 @@ def valid?
def ttl_evict
return if @ttl == :none

ttl_horizon = Time.now - @ttl
key, time = @data_ttl.tail
ttl_horizon = Time.now.to_f - @ttl
key, time = @data_ttl.first

until time.nil? || time > ttl_horizon
@data_ttl.delete(key)
@data_lru.delete(key)

key, time = @data_ttl.tail
key, time = @data_ttl.first
end
end

def resize
ttl_evict

while @data_lru.size > @max_size
key, _ = @data_lru.tail
key, _ = @data_lru.first

@data_ttl.delete(key)
@data_lru.delete(key)
Expand Down
5 changes: 3 additions & 2 deletions lru_redux.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ require 'lru_redux/version'
Gem::Specification.new do |spec|
spec.name = "lru_redux"
spec.version = LruRedux::VERSION
spec.authors = ["Sam Saffron"]
spec.email = ["[email protected]"]
spec.authors = ["Sam Saffron", "Kaijah Hougham"]
spec.email = ["[email protected]", "[email protected]"]
spec.description = %q{An efficient implementation of an lru cache}
spec.summary = %q{An efficient implementation of an lru cache}
spec.homepage = "https://github.com/SamSaffron/lru_redux"
Expand All @@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "guard-minitest"
spec.add_development_dependency "guard"
spec.add_development_dependency "rb-inotify"
spec.add_development_dependency "timecop", "~> 0.7"
end
90 changes: 90 additions & 0 deletions test/ttl/cache_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
require 'timecop'

class TTLCacheTest < CacheTest
def setup
Timecop.freeze(Time.now)
@c = LruRedux::TTL::Cache.new 3, 5 * 60
end

def teardown
Timecop.return
end

def test_ttl
assert_equal 300, @c.ttl

@c.ttl = 10 * 60

assert_equal 600, @c.ttl
end

# TTL tests using Timecop
def test_ttl_eviction_on_access
@c[:a] = 1
@c[:b] = 2

Timecop.freeze(Time.now + 330)

@c[:c] = 3

assert_equal([[:c, 3]], @c.to_a)
end

def test_ttl_eviction_on_expire
@c[:a] = 1
@c[:b] = 2

Timecop.freeze(Time.now + 330)

@c.expire

assert_equal([], @c.to_a)
end

def test_ttl_eviction_on_new_max_size
@c[:a] = 1
@c[:b] = 2

Timecop.freeze(Time.now + 330)

@c.max_size = 10

assert_equal([], @c.to_a)
end

def test_ttl_eviction_on_new_ttl
@c[:a] = 1
@c[:b] = 2

Timecop.freeze(Time.now + 330)

@c.ttl = 10 * 60

assert_equal([[:b, 2], [:a, 1]], @c.to_a)

@c.ttl = 2 * 60

assert_equal([], @c.to_a)
end

def test_ttl_precedence_over_lru
@c[:a] = 1

Timecop.freeze(Time.now + 60)

@c[:b] = 2
@c[:c] = 3

@c[:a]

assert_equal [[:a, 1], [:c, 3], [:b, 2]],
@c.to_a

Timecop.freeze(Time.now + 270)

@c[:d] = 4

assert_equal [[:d, 4], [:c, 3], [:b, 2]],
@c.to_a
end
end
20 changes: 20 additions & 0 deletions test/ttl/thread_safe_cache_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class TTLThreadSafeCacheTest < TTLCacheTest
def setup
Timecop.freeze(Time.now)
@c = LruRedux::TTL::ThreadSafeCache.new 3, 5 * 60
end

def teardown
Timecop.return
end

def test_recursion
@c[:a] = 1
@c[:b] = 2

# should not blow up
@c.each do |k, _|
@c[k]
end
end
end

0 comments on commit 0a53cea

Please sign in to comment.