1require 'rubygems' 2require 'redis' 3 4$runs = []; # Remember the error rate of each run for average purposes. 5$o = {}; # Options set parsing arguments 6 7def testit(filename) 8 r = Redis.new 9 r.config("SET","maxmemory","2000000") 10 if $o[:ttl] 11 r.config("SET","maxmemory-policy","volatile-ttl") 12 else 13 r.config("SET","maxmemory-policy","allkeys-lru") 14 end 15 r.config("SET","maxmemory-samples",5) 16 r.config("RESETSTAT") 17 r.flushall 18 19 html = "" 20 html << <<EOF 21 <html> 22 <body> 23 <style> 24 .box { 25 width:5px; 26 height:5px; 27 float:left; 28 margin: 1px; 29 } 30 31 .old { 32 border: 1px black solid; 33 } 34 35 .new { 36 border: 1px green solid; 37 } 38 39 .otherdb { 40 border: 1px red solid; 41 } 42 43 .ex { 44 background-color: #666; 45 } 46 </style> 47 <pre> 48EOF 49 50 # Fill the DB up to the first eviction. 51 oldsize = r.dbsize 52 id = 0 53 while true 54 id += 1 55 begin 56 r.set(id,"foo") 57 rescue 58 break 59 end 60 newsize = r.dbsize 61 break if newsize == oldsize # A key was evicted? Stop. 62 oldsize = newsize 63 end 64 65 inserted = r.dbsize 66 first_set_max_id = id 67 html << "#{r.dbsize} keys inserted.\n" 68 69 # Access keys sequentially, so that in theory the first part will be expired 70 # and the latter part will not, according to perfect LRU. 71 72 if $o[:ttl] 73 STDERR.puts "Set increasing expire value" 74 (1..first_set_max_id).each{|id| 75 r.expire(id,1000+id) 76 STDERR.print(".") if (id % 150) == 0 77 } 78 else 79 STDERR.puts "Access keys sequentially" 80 (1..first_set_max_id).each{|id| 81 r.get(id) 82 sleep 0.001 83 STDERR.print(".") if (id % 150) == 0 84 } 85 end 86 STDERR.puts 87 88 # Insert more 50% keys. We expect that the new keys will rarely be expired 89 # since their last access time is recent compared to the others. 90 # 91 # Note that we insert the first 100 keys of the new set into DB1 instead 92 # of DB0, so that we can try how cross-DB eviction works. 93 half = inserted/2 94 html << "Insert enough keys to evict half the keys we inserted.\n" 95 add = 0 96 97 otherdb_start_idx = id+1 98 otherdb_end_idx = id+100 99 while true 100 add += 1 101 id += 1 102 if id >= otherdb_start_idx && id <= otherdb_end_idx 103 r.select(1) 104 r.set(id,"foo") 105 r.select(0) 106 else 107 r.set(id,"foo") 108 end 109 break if r.info['evicted_keys'].to_i >= half 110 end 111 112 html << "#{add} additional keys added.\n" 113 html << "#{r.dbsize} keys in DB.\n" 114 115 # Check if evicted keys respect LRU 116 # We consider errors from 1 to N progressively more serious as they violate 117 # more the access pattern. 118 119 errors = 0 120 e = 1 121 error_per_key = 100000.0/first_set_max_id 122 half_set_size = first_set_max_id/2 123 maxerr = 0 124 (1..(first_set_max_id/2)).each{|id| 125 if id >= otherdb_start_idx && id <= otherdb_end_idx 126 r.select(1) 127 exists = r.exists(id) 128 r.select(0) 129 else 130 exists = r.exists(id) 131 end 132 if id < first_set_max_id/2 133 thiserr = error_per_key * ((half_set_size-id).to_f/half_set_size) 134 maxerr += thiserr 135 errors += thiserr if exists 136 elsif id >= first_set_max_id/2 137 thiserr = error_per_key * ((id-half_set_size).to_f/half_set_size) 138 maxerr += thiserr 139 errors += thiserr if !exists 140 end 141 } 142 errors = errors*100/maxerr 143 144 STDERR.puts "Test finished with #{errors}% error! Generating HTML on stdout." 145 146 html << "#{errors}% error!\n" 147 html << "</pre>" 148 $runs << errors 149 150 # Generate the graphical representation 151 (1..id).each{|id| 152 # Mark first set and added items in a different way. 153 c = "box" 154 if id >= otherdb_start_idx && id <= otherdb_end_idx 155 c << " otherdb" 156 elsif id <= first_set_max_id 157 c << " old" 158 else 159 c << " new" 160 end 161 162 # Add class if exists 163 if id >= otherdb_start_idx && id <= otherdb_end_idx 164 r.select(1) 165 exists = r.exists(id) 166 r.select(0) 167 else 168 exists = r.exists(id) 169 end 170 171 c << " ex" if exists 172 html << "<div title=\"#{id}\" class=\"#{c}\"></div>" 173 } 174 175 # Close HTML page 176 177 html << <<EOF 178 </body> 179 </html> 180EOF 181 182 f = File.open(filename,"w") 183 f.write(html) 184 f.close 185end 186 187def print_avg 188 avg = ($runs.reduce {|a,b| a+b}) / $runs.length 189 puts "#{$runs.length} runs, AVG is #{avg}" 190end 191 192if ARGV.length < 1 193 STDERR.puts "Usage: ruby test-lru.rb <html-output-filename> [--runs <count>] [--ttl]" 194 STDERR.puts "Options:" 195 STDERR.puts " --runs <count> Execute the test <count> times." 196 STDERR.puts " --ttl Set keys with increasing TTL values" 197 STDERR.puts " (starting from 1000 seconds) in order to" 198 STDERR.puts " test the volatile-lru policy." 199 exit 1 200end 201 202filename = ARGV[0] 203$o[:numruns] = 1 204 205# Options parsing 206i = 1 207while i < ARGV.length 208 if ARGV[i] == '--runs' 209 $o[:numruns] = ARGV[i+1].to_i 210 i+= 1 211 elsif ARGV[i] == '--ttl' 212 $o[:ttl] = true 213 else 214 STDERR.puts "Unknown option #{ARGV[i]}" 215 exit 1 216 end 217 i+= 1 218end 219 220$o[:numruns].times { 221 testit(filename) 222 print_avg if $o[:numruns] != 1 223} 224