1 /******************************************************************************* 2 3 Contains unit-tests for LRUCache class. 4 5 Copyright: 6 Copyright (c) 2009-2016 dunnhumby Germany GmbH. 7 All rights reserved. 8 9 License: 10 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 11 Alternatively, this file may be distributed under the terms of the Tango 12 3-Clause BSD License (see LICENSE_BSD.txt for details). 13 14 *******************************************************************************/ 15 16 module ocean.util.container.cache.LRUCache_test; 17 18 19 version (unittest) 20 { 21 import ocean.util.container.cache.LRUCache; 22 import ocean.core.Array: shuffle; 23 import ocean.core.Test; 24 25 import core.memory; 26 import ocean.math.random.Random; 27 import ocean.io.Stdout; 28 import core.sys.posix.stdlib: srand48, mrand48, drand48; 29 import core.sys.posix.unistd: getpid; 30 import core.stdc.stdio : printf; 31 import core.stdc.time: time_t, time; 32 import ocean.time.StopWatch; 33 } 34 35 unittest 36 { 37 srand48(time(null)+getpid()); 38 39 static ulong ulrand ( ) 40 { 41 return (cast (ulong) mrand48() << 0x20) | cast (uint) mrand48(); 42 } 43 44 time_t time = 234567; 45 46 // --------------------------------------------------------------------- 47 // Test of static sized cache 48 49 { 50 static immutable n_records = 33, 51 capacity = 22, 52 n_overflow = 7; 53 54 static assert (n_records >= capacity, 55 "Number of records smaller than capacity!"); 56 57 struct Record 58 { 59 hash_t key; // random number 60 size_t val; // counter 61 } 62 63 // Initialise the list of records. 64 65 Record[n_records] records; 66 67 foreach (i, ref record; records) 68 { 69 record = Record(ulrand(), i); 70 } 71 72 // Populate the cache to the limit. 73 74 time_t t = 0; 75 76 scope cache = new class LRUCache!(size_t) 77 { 78 this ( ) {super(capacity);} 79 80 override time_t now ( ) {return ++t;} 81 }; 82 83 test (capacity == cache.max_length, 84 "Max length of cache does not equal configured capacity!"); 85 86 foreach (record; records[0 .. cache.max_length]) 87 { 88 cache.put(record.key, record.val); 89 } 90 91 // Shuffle records and count how many of the first n_overflow of the 92 // shuffled records are in the cache. If either all or none of these are 93 // in the cache, shuffle and try again. 94 95 uint n_existing; 96 97 do 98 { 99 n_existing = 0; 100 foreach (i, record; records.shuffle(drand48)[0 .. n_overflow]) 101 { 102 n_existing += cache.exists(record.key); 103 } 104 } 105 while (!n_existing || n_existing == n_overflow); 106 107 test (n_existing > 0 && n_existing < n_overflow, "n_existing has unexpected value"); 108 109 // Get the shuffled records from the cache and verify them. Record the 110 // keys of the first n_overflow existing records which will get the 111 // least (oldest) access time by cache.getItem() and therefore be the 112 // first records to be removed on a cache overflow. 113 114 hash_t[n_overflow] oldest_keys; 115 116 { 117 uint i = 0; 118 119 foreach (record; records) 120 { 121 auto v = cache.getAndRefresh(record.key); 122 123 if (record.val < cache.max_length) 124 { 125 test (v !is null); 126 test (*v == record.val); 127 128 if (i < n_overflow) 129 { 130 oldest_keys[i++] = record.key; 131 } 132 } 133 else 134 { 135 test (v is null); 136 } 137 } 138 139 test (i == n_overflow); 140 } 141 142 test (t == cache.max_length * 2); 143 144 // Put the first n_overflow shuffled records so that the cache will 145 // overflow. 146 // Existing records should be updated to a new value. To enable 147 // verification of the update, change the values to 4711 + i. 148 149 foreach (size_t i, ref record; records[0 .. n_overflow]) 150 { 151 record.val = 4711 + i; 152 153 cache.put(record.key, record.val); 154 } 155 156 test (t == cache.max_length * 2 + n_overflow); 157 158 // Verify the records. 159 160 foreach (i, record; records[0 .. n_overflow]) 161 { 162 auto v = cache.getAndRefresh(record.key); 163 164 test (v !is null); 165 test (*v == 4711 + i); 166 } 167 168 test (t == cache.max_length * 2 + n_overflow * 2); 169 170 // oldest_keys[n_existing .. $] should have been removed from the 171 // cache due to cache overflow. 172 173 foreach (key; oldest_keys[n_existing .. $]) 174 { 175 auto v = cache.getAndRefresh(key); 176 177 test (v is null); 178 } 179 180 // cache.get should not have evaluated the lazy ++t. 181 182 test (t == cache.max_length * 2 + n_overflow * 2); 183 184 // Verify that all other records still exist in the cache. 185 186 { 187 uint n = 0; 188 189 foreach (record; records[n_overflow .. $]) 190 { 191 auto v = cache.getAndRefresh(record.key); 192 193 if (v !is null) 194 { 195 test (*v == record.val); 196 197 n++; 198 } 199 } 200 201 test (n == cache.max_length - n_overflow); 202 } 203 204 test (t == cache.max_length * 3 + n_overflow); 205 } 206 207 // More tests to the LRUCache 208 { 209 struct Data 210 { 211 int x; 212 } 213 214 time_t t = 500; 215 216 scope static_cache = new class LRUCache!(Data) 217 { 218 this ( ) {super(2);} 219 220 override time_t now ( ) {return t;} 221 }; 222 223 with (static_cache) 224 { 225 test(length == 0); 226 227 { 228 bool replaced = put(1, Data(23)); 229 230 test(!replaced); 231 232 test(length == 1); 233 test(exists(1)); 234 235 Data* item = getAndRefresh(1); 236 test(item); 237 test(item.x == 23); 238 } 239 240 { 241 t += 1; 242 bool replaced = put(2, Data(24)); 243 244 test(!replaced); 245 246 test(length == 2); 247 test(exists(2)); 248 249 Data* item = getAndRefresh(2); 250 test(item); 251 test(item.x == 24); 252 } 253 254 { 255 t += 1; 256 bool replaced = put(2, Data(25)); 257 258 test(replaced); 259 260 test(length == 2); 261 test(exists(2)); 262 263 Data* item = getAndRefresh(2); 264 test(item); 265 test(item.x == 25); 266 } 267 268 { 269 t += 1; 270 bool replaced = put(3, Data(26)); 271 272 test(!replaced); 273 274 test(length == 2); 275 test(!exists(1)); 276 test(exists(2)); 277 test(exists(3)); 278 279 Data* item = getAndRefresh(3); 280 test(item); 281 test(item.x == 26); 282 } 283 284 { 285 t += 1; 286 bool replaced = put(4, Data(27)); 287 288 test(!replaced); 289 290 test(length == 2); 291 test(!exists(1)); 292 test(!exists(2)); 293 test(exists(3)); 294 test(exists(4)); 295 296 Data* item = getAndRefresh(4); 297 test(item); 298 test(item.x == 27); 299 } 300 301 clear(); 302 test(length == 0); 303 304 { 305 bool replaced = put(1, Data(23)); 306 307 test(!replaced); 308 309 test(length == 1); 310 test(exists(1)); 311 312 Data* item = getAndRefresh(1); 313 test(item); 314 test(item.x == 23); 315 } 316 317 { 318 bool replaced = put(2, Data(24)); 319 320 test(!replaced); 321 322 test(length == 2); 323 test(exists(2)); 324 325 Data* item = getAndRefresh(2); 326 test(item); 327 test(item.x == 24); 328 } 329 330 remove(1); 331 test(length == 1); 332 test(!exists(1)); 333 test(exists(2)); 334 } 335 } 336 337 // Test notifier for removing items from Cache 338 { 339 class CacheImpl: LRUCache!(void[]) 340 { 341 private bool* item_dropped; 342 private size_t* index; 343 344 public this (size_t max_items, bool* item_dropped) 345 { 346 super(max_items); 347 this.item_dropped = item_dropped; 348 } 349 350 protected override void itemDropped (hash_t key, ref CacheImpl.Value value) 351 { 352 *item_dropped = true; 353 } 354 } 355 356 // Test if the whenCacheItemDropped is being called 357 static immutable max_items = 10; 358 auto item_dropped = false; 359 size_t index = 0; 360 361 auto cache = new CacheImpl( max_items, &item_dropped ); 362 363 for(int i = 0; i < max_items * 2; i++) 364 { 365 366 auto data = cache.getRefreshOrCreate(i); 367 368 if(i > max_items - 1) 369 { 370 // Test if it is being called 371 test(item_dropped); 372 373 // Test the next case 374 item_dropped = false; 375 } 376 else 377 { 378 test(!item_dropped); 379 } 380 } 381 } 382 383 // --------------------------------------------------------------------- 384 // Test of dynamic sized cache 385 386 { 387 ubyte[] data1 = cast(ubyte[])"hello world"; 388 ubyte[] data2 = cast(ubyte[])"goodbye world"; 389 ubyte[] data3 = cast(ubyte[])"hallo welt"; 390 391 scope dynamic_cache = new class LRUCache!(ubyte[]) 392 { 393 this ( ) {super(2);} 394 395 time_t now_sec ( ) {return ++time;} 396 }; 397 398 test(dynamic_cache.length == 0); 399 400 { 401 *dynamic_cache.getRefreshOrCreate(1) = data1; 402 { 403 auto val = dynamic_cache.getAndRefresh(1); 404 test(val !is null); 405 test((*val)[] == data1); 406 } 407 408 *dynamic_cache.getRefreshOrCreate(2) = data2; 409 { 410 auto val = dynamic_cache.getAndRefresh(1); 411 test(val !is null); 412 test((*val)[] == data1); 413 } 414 { 415 auto val = dynamic_cache.getAndRefresh(2); 416 test(val !is null); 417 test((*val)[] == data2); 418 } 419 420 *dynamic_cache.getRefreshOrCreate(3) = data3; 421 test(dynamic_cache.getAndRefresh(1) is null); 422 { 423 auto val = dynamic_cache.getAndRefresh(2); 424 test(val !is null); 425 test((*val)[] == data2); 426 } 427 { 428 auto val = dynamic_cache.getAndRefresh(3); 429 test(val !is null); 430 test((*val)[] == data3); 431 } 432 433 dynamic_cache.clear; 434 test(dynamic_cache.length == 0); 435 436 *dynamic_cache.getRefreshOrCreate(1) = data1; 437 test(dynamic_cache.length == 1); 438 { 439 auto val = dynamic_cache.getAndRefresh(1); 440 test(val !is null); 441 test((*val)[] == data1); 442 } 443 444 *dynamic_cache.getRefreshOrCreate(2) = data2; 445 test(dynamic_cache.length == 2); 446 { 447 auto val = dynamic_cache.getAndRefresh(2); 448 test(val !is null); 449 test((*val)[] == data2); 450 } 451 452 dynamic_cache.remove(1); 453 test(dynamic_cache.length == 1); 454 test(dynamic_cache.getAndRefresh(1) is null); 455 { 456 auto val = dynamic_cache.getAndRefresh(2); 457 test(val !is null); 458 test((*val)[] == data2); 459 } 460 } 461 462 // More tests for dynamic cache 463 { 464 dynamic_cache.clear(); 465 dynamic_cache.put(1, data1); 466 test(dynamic_cache.exists(1)); 467 test((*dynamic_cache.getAndRefresh(1))[] == data1); 468 469 dynamic_cache.put(2, data2); 470 test(dynamic_cache.exists(1)); 471 test(dynamic_cache.exists(2)); 472 test((*dynamic_cache.getAndRefresh(1))[] == data1); 473 test((*dynamic_cache.getAndRefresh(2))[] == data2); 474 475 dynamic_cache.put(3, data3); 476 test(!dynamic_cache.exists(1)); 477 test(dynamic_cache.exists(2)); 478 test(dynamic_cache.exists(3)); 479 test((*dynamic_cache.getAndRefresh(2))[] == data2); 480 test((*dynamic_cache.getAndRefresh(3))[] == data3); 481 482 dynamic_cache.clear; 483 test(dynamic_cache.length == 0); 484 485 dynamic_cache.put(1, data1); 486 test(dynamic_cache.length == 1); 487 test(dynamic_cache.exists(1)); 488 test((*dynamic_cache.getAndRefresh(1))[] == data1); 489 490 dynamic_cache.put(2, data2); 491 test(dynamic_cache.length == 2); 492 test(dynamic_cache.exists(2)); 493 test((*dynamic_cache.getAndRefresh(2))[] == data2); 494 495 dynamic_cache.remove(1); 496 test(dynamic_cache.length == 1); 497 test(!dynamic_cache.exists(1)); 498 test(dynamic_cache.exists(2)); 499 } 500 } 501 502 void performanceTest() 503 { 504 GC.disable; 505 506 printf("Starting Cache performance test\n".ptr); 507 508 auto random = new Random; 509 510 static immutable cache_size = 100_000; 511 512 static immutable max_item_size = 1024 * 4; 513 514 StopWatch sw; 515 516 time_t time = 1; 517 518 auto cache = new class LRUCache!(ubyte[]) 519 { 520 this ( ) {super(cache_size);} 521 522 override time_t now ( ) {return time;} 523 }; 524 525 ubyte[] value; 526 value.length = max_item_size; 527 528 // Fill cache 529 printf("Filling cache\n".ptr); 530 sw.start; 531 for ( uint i; i < cache_size; i++ ) 532 { 533 cache.put(i, value); 534 ubyte d_time; 535 random(d_time); 536 time += d_time % 16; 537 } 538 printf("%d puts, %f puts/s\n".ptr, cache_size, cast(float)cache_size / (cast(float)sw.microsec / 1_000_000)); 539 540 // Put values into full cache 541 static immutable puts = 1_000_000; 542 printf("Writing to cache:\n".ptr); 543 sw.start; 544 for ( uint i; i < puts; i++ ) 545 { 546 cache.put(i % cache_size, value); 547 ubyte d_time; 548 random(d_time); 549 time += d_time % 16; 550 } 551 printf("%d puts, %f puts/s\n".ptr, puts, cast(float)puts / (cast(float)sw.microsec / 1_000_000)); 552 553 // Get values from cache 554 static immutable gets = 1_000_000; 555 printf("Reading from cache: %d gets, %f gets/s\n".ptr, gets, cast(float)gets / (cast(float)sw.microsec / 1_000_000)); 556 sw.start; 557 for ( uint i; i < gets; i++ ) 558 { 559 cache.getAndRefresh(i % cache_size); 560 ubyte d_time; 561 random(d_time); 562 time += d_time % 16; 563 } 564 printf("Writing to cache: %d gets, %f gets/s\n".ptr, gets, cast(float)gets / (cast(float)sw.microsec / 1_000_000)); 565 566 printf("Cache performance test finished\n".ptr); 567 } 568 569 debug ( OceanPerformanceTest ) performanceTest(); 570 }