1 /// 2 module cachetools.cache2q; 3 4 /// Implements Q2 cache 5 /// http://www.vldb.org/conf/1994/P439.PDF 6 7 private import std.experimental.allocator; 8 private import std.experimental.allocator.mallocator : Mallocator; 9 private import core.stdc.time; 10 private import std.typecons; 11 12 private import cachetools.internal; 13 private import cachetools.interfaces; 14 private import cachetools.containers.hashmap; 15 private import cachetools.containers.lists; 16 17 /* Pseudocode from the paper 18 // If there is space, we give it to X. 19 // If there is no space, we free a page slot to 20 // make room for page X. 21 reclaimfor(page X) 22 begin 23 if there are free page slots then 24 put X into a free page slot 25 else if( |Alin| > Kin) 26 page out the tail of Alin, call it Y 27 add identifier of Y to the head of Alout 28 if(]Alout] >Kout) 29 remove identifier of Z from 30 the tail of Alout 31 end if 32 put X into the reclaimed page slot 33 else 34 page out the tail of Am, call it Y 35 // do not put it on Alout; it hasn’t been 36 // accessed for a while 37 put X into the reclaimed page slot 38 end if 39 end 40 41 On accessing a page X : 42 begin 43 if X is in Am then 44 move X to the head of Am 45 else if (X is in Alout) then 46 reclaimfor(Х) 47 add X to the head of Am 48 else if (X is in Alin) // do nothing 49 else // X is in no queue 50 reclaimfor(X) 51 add X to the head of Alin 52 end if 53 end 54 */ 55 /** 56 2Q cache is variant of multi-level LRU cache. Original paper http://www.vldb.org/conf/1994/P439.PDF 57 It is adaptive, scan-resistant and can give more hits than plain LRU. 58 $(P This cache consists from three parts (In, Out and Main) where 'In' receive all new elements, 'Out' receives all 59 overflows from 'In', and 'Main' is LRU cache which hold all long-lived data.) 60 **/ 61 class Cache2Q(K, V, Allocator=Mallocator) 62 { 63 private 64 { 65 struct ListElement { 66 StoredType!K key; // we keep key here so we can remove element from map when we evict with LRU or TTL) 67 } 68 alias ListType = CompressedList!(ListElement, Allocator); 69 alias ListElementPtrType = ListType.NodePointer; 70 alias DListType = DList!(ListElement, Allocator); 71 alias DListElementPtrType = DListType.Node!ListElement*; 72 73 struct MapElement 74 { 75 StoredType!V value; 76 ListElementPtrType list_element_ptr; 77 time_t expired_at; 78 } 79 struct MainMapElement 80 { 81 StoredType!V value; 82 DListElementPtrType list_element_ptr; 83 time_t expired_at; 84 } 85 86 int _kin, _kout, _km; 87 88 CompressedList!(ListElement, Allocator) _InList; 89 CompressedList!(ListElement, Allocator) _OutList; 90 DList!(ListElement, Allocator) _MainList; 91 92 HashMap!(K, MapElement, Allocator) _InMap; 93 HashMap!(K, MapElement, Allocator) _OutMap; 94 HashMap!(K, MainMapElement, Allocator) _MainMap; 95 96 time_t _ttl; // global ttl (if > 0) 97 98 bool __reportCacheEvents; 99 SList!(CacheEvent!(K, V), Allocator) __events; // unbounded list of cache events 100 101 } 102 final this() @safe { 103 _InMap.grow_factor(4); 104 _OutMap.grow_factor(4); 105 _MainMap.grow_factor(4); 106 } 107 final this(int s) @safe { 108 _kin = s/6; 109 _kout = s/6; 110 _km = 2*s/3; 111 _InMap.grow_factor(4); 112 _OutMap.grow_factor(4); 113 _MainMap.grow_factor(4); 114 } 115 /// 116 /// Set total cache size. 'In' and 'Out' gets 1/6 of total size, Main gets 2/3 of size. 117 /// 118 final auto size(uint s) @safe 119 { 120 _kin = 1*s/6; 121 _kout = 1*s/6; 122 _km = 4*s/6; 123 return this; 124 } 125 /// 126 /// Set In queue size 127 /// 128 final auto sizeIn(uint s) @safe 129 { 130 _kin = s; 131 return this; 132 } 133 134 /// 135 /// Set Out queue size 136 /// 137 final auto sizeOut(uint s) @safe 138 { 139 _kout = s; 140 return this; 141 } 142 143 /// 144 /// Set Main queue size 145 /// 146 final auto sizeMain(uint s) @safe 147 { 148 _km = s; 149 return this; 150 } 151 152 /// 153 /// Number of elements in cache. 154 /// 155 final int length() @safe 156 { 157 return _InMap.length + _OutMap.length + _MainMap.length; 158 } 159 /// 160 /// Drop all elements from cache. 161 /// 162 final void clear() @safe 163 { 164 _InList.clear(); 165 _OutList.clear(); 166 _MainList.clear(); 167 if ( __reportCacheEvents ) { 168 foreach(p; _InMap.byPair) { 169 __events.insertBack(CacheEvent!(K, V)(EventType.Removed, p.key, p.value.value)); 170 } 171 foreach(p; _OutMap.byPair){ 172 __events.insertBack(CacheEvent!(K, V)(EventType.Removed, p.key, p.value.value)); 173 } 174 foreach(p; _MainMap.byPair){ 175 __events.insertBack(CacheEvent!(K, V)(EventType.Removed, p.key, p.value.value)); 176 } 177 } 178 _InMap.clear(); 179 _OutMap.clear(); 180 _MainMap.clear(); 181 } 182 /// 183 /// Set default ttl (seconds) 184 /// 185 final void ttl(time_t v) @safe 186 { 187 _ttl = v; 188 } 189 /// 190 auto enableCacheEvents() pure nothrow @safe @nogc 191 { 192 __reportCacheEvents = true; 193 return this; 194 } 195 /// 196 final auto cacheEvents() @safe nothrow 197 { 198 auto r = CacheEventRange!(K, V)(__events); 199 __events.clear; 200 return r; 201 } 202 203 struct CacheEventRange(K, V) 204 { 205 206 private SList!(CacheEvent!(K, V), Allocator) __events; 207 208 void opAssign(CacheEventRange!(K, V) other) @safe 209 { 210 __events.clear(); 211 __events = other.__events; 212 } 213 214 this(ref SList!(CacheEvent!(K, V), Allocator) events) @safe 215 { 216 __events = events; 217 } 218 219 bool empty() @safe const nothrow pure 220 { 221 return __events.empty(); 222 } 223 224 void popFront() @safe nothrow 225 { 226 __events.popFront(); 227 } 228 229 auto front() @safe 230 { 231 return __events.front(); 232 } 233 234 auto length() pure const nothrow @safe 235 { 236 return __events.length; 237 } 238 auto save() @safe { 239 return CacheEventRange!(K, V)(__events); 240 } 241 } 242 /// 243 /// Get element from cache. 244 /// 245 final Nullable!V get(K k) @safe 246 { 247 debug(cachetools) safe_tracef("get %s", k); 248 249 MainMapElement* keyInMain = k in _MainMap; 250 if ( keyInMain ) 251 { 252 debug(cachetools) safe_tracef("%s in main cache: %s", k, *keyInMain); 253 if ( keyInMain.expired_at > 0 && keyInMain.expired_at <= time(null) ) 254 { 255 // expired 256 if (__reportCacheEvents) 257 { 258 __events.insertBack(CacheEvent!(K, V)(EventType.Expired, k, keyInMain.value)); 259 } 260 _MainList.remove(keyInMain.list_element_ptr); 261 _MainMap.remove(k); 262 return Nullable!V(); 263 } 264 _MainList.move_to_head(keyInMain.list_element_ptr); 265 return Nullable!V(keyInMain.value); 266 } 267 debug(cachetools) safe_tracef("%s not in main cache", k); 268 269 auto keyInOut = k in _OutMap; 270 if ( keyInOut ) 271 { 272 debug(cachetools) safe_tracef("%s in A1Out cache: %s", k, *keyInOut); 273 if (keyInOut.expired_at > 0 && keyInOut.expired_at <= time(null)) 274 { 275 // expired 276 if (__reportCacheEvents) 277 { 278 __events.insertBack(CacheEvent!(K, V)(EventType.Expired, k, keyInOut.value)); 279 } 280 () @trusted { 281 _OutList.remove(keyInOut.list_element_ptr); 282 }(); 283 _OutMap.remove(k); 284 return Nullable!V(); 285 } 286 // move from Out to Main 287 auto value = keyInOut.value; 288 auto expired_at = keyInOut.expired_at; 289 290 () @trusted 291 { 292 assert((*keyInOut.list_element_ptr).key == k); 293 _OutList.remove(keyInOut.list_element_ptr); 294 }(); 295 296 bool removed = _OutMap.remove(k); 297 assert(removed); 298 debug(cachetools) safe_tracef("%s removed from A1Out cache", k); 299 300 auto mlp = _MainList.insertFront(ListElement(k)); 301 _MainMap.put(k, MainMapElement(value, mlp, expired_at)); 302 debug(cachetools) safe_tracef("%s placed to Main cache", k); 303 if ( _MainList.length > _km ) 304 { 305 debug(cachetools) safe_tracef("Main cache overflowed, pop %s", _MainList.tail().key); 306 if (__reportCacheEvents) 307 { 308 auto key_to_evict = _MainList.tail().key; 309 auto mptr = key_to_evict in _MainMap; 310 __events.insertBack(CacheEvent!(K, V)(EventType.Evicted, key_to_evict, mptr.value)); 311 } 312 _MainMap.remove(_MainList.tail().key); 313 _MainList.popBack(); 314 } 315 return Nullable!V(value); 316 } 317 debug(cachetools) safe_tracef("%s not in Out cache", k); 318 319 auto keyInIn = k in _InMap; 320 if ( keyInIn ) 321 { 322 debug(cachetools) safe_tracef("%s in In cache", k); 323 if (keyInIn.expired_at > 0 && keyInIn.expired_at <= time(null)) 324 { 325 // expired 326 if (__reportCacheEvents) { 327 __events.insertBack(CacheEvent!(K, V)(EventType.Expired, k, keyInIn.value)); 328 } 329 () @trusted { 330 _InList.remove(keyInIn.list_element_ptr); 331 }(); 332 _InMap.remove(k); 333 return Nullable!V(); 334 } 335 // just return value 336 return Nullable!V(keyInIn.value); 337 } 338 debug(cachetools) safe_tracef("%s not in In cache", k); 339 340 return Nullable!V(); 341 } 342 343 /// 344 /// Put element to cache. 345 /// 346 /// Evict something if we have to. 347 /// 348 final PutResult put(K k, V v, TTL ttl = TTL()) @safe 349 out 350 { 351 assert(__result != PutResult(PutResultFlag.None)); 352 } 353 do 354 { 355 time_t exp_time; 356 357 if ( _ttl > 0 && ttl.useDefault ) { 358 exp_time = time(null) + _ttl; 359 } 360 361 if ( ttl.value > 0 ) { 362 exp_time = time(null) + ttl.value; 363 } 364 365 auto keyInMain = k in _MainMap; 366 if ( keyInMain ) 367 { 368 if ( __reportCacheEvents ) { 369 __events.insertBack(CacheEvent!(K, V)(EventType.Updated, k, keyInMain.value)); 370 } 371 keyInMain.value = v; 372 keyInMain.expired_at = exp_time; 373 debug(cachetools) safe_tracef("%s in Main cache", k); 374 return PutResult(PutResultFlag.Replaced); 375 } 376 debug(cachetools) safe_tracef("%s not in Main cache", k); 377 378 auto keyInOut = k in _OutMap; 379 if ( keyInOut ) 380 { 381 if ( __reportCacheEvents ) { 382 __events.insertBack(CacheEvent!(K, V)(EventType.Updated, k, keyInOut.value)); 383 } 384 keyInOut.value = v; 385 keyInOut.expired_at = exp_time; 386 debug(cachetools) safe_tracef("%s in Out cache", k); 387 return PutResult(PutResultFlag.Replaced); 388 } 389 debug(cachetools) safe_tracef("%s not in Out cache", k); 390 391 auto keyInIn = k in _InMap; 392 if ( keyInIn ) 393 { 394 if ( __reportCacheEvents ) { 395 __events.insertBack(CacheEvent!(K, V)(EventType.Updated, k, keyInIn.value)); 396 } 397 keyInIn.value = v; 398 keyInIn.expired_at = exp_time; 399 debug(cachetools) safe_tracef("%s in In cache", k); 400 return PutResult(PutResultFlag.Replaced); 401 } 402 else 403 { 404 debug(cachetools) safe_tracef("insert %s in A1InFifo", k); 405 auto lp = _InList.insertBack(ListElement(k)); 406 _InMap.put(k, MapElement(v, lp, exp_time)); 407 if ( _InList.length <= _kin ) 408 { 409 return PutResult(PutResultFlag.Inserted); 410 } 411 412 debug(cachetools) safe_tracef("pop %s from InLlist", _InList.front.key); 413 414 auto toOutK = _InList.front.key; 415 _InList.popFront(); 416 417 auto in_ptr = toOutK in _InMap; 418 419 auto toOutV = in_ptr.value; 420 auto toOutE = in_ptr.expired_at; 421 bool removed = _InMap.remove(toOutK); 422 423 assert(removed); 424 assert(_InList.length == _InMap.length); 425 426 if ( toOutE > 0 && toOutE <= time(null) ) 427 { 428 // expired, we done 429 if (__reportCacheEvents) { 430 __events.insertBack(CacheEvent!(K, V)(EventType.Expired, toOutK, toOutV)); 431 } 432 return PutResult(PutResultFlag.Inserted|PutResultFlag.Evicted); 433 } 434 435 // and push to Out 436 lp = _OutList.insertBack(ListElement(toOutK)); 437 _OutMap.put(toOutK, MapElement(toOutV, lp, toOutE)); 438 if ( _OutList.length <= _kout ) 439 { 440 return PutResult(PutResultFlag.Inserted|PutResultFlag.Evicted); 441 } 442 // 443 // Out overflowed - throw away head 444 // 445 debug(cachetools) safe_tracef("pop %s from Out", _OutList.front.key); 446 447 if (__reportCacheEvents) 448 { 449 // store in event list 450 auto evicted_key = _OutList.front.key; 451 auto mptr = evicted_key in _OutMap; 452 __events.insertBack(CacheEvent!(K,V)(EventType.Evicted, evicted_key, mptr.value)); 453 } 454 455 removed = _OutMap.remove(_OutList.front.key); 456 _OutList.popFront(); 457 458 assert(removed); 459 assert(_OutList.length == _OutMap.length); 460 461 return PutResult(PutResultFlag.Inserted|PutResultFlag.Evicted); 462 } 463 } 464 /// 465 /// Remove element from cache. 466 /// 467 final bool remove(K k) @safe 468 { 469 debug(cachetools) safe_tracef("remove from 2qcache key %s", k); 470 auto inIn = k in _InMap; 471 if ( inIn ) 472 { 473 auto lp = inIn.list_element_ptr; 474 475 if (__reportCacheEvents) 476 { 477 __events.insertBack(CacheEvent!(K, V)(EventType.Removed, k, inIn.value)); 478 } 479 480 () @trusted 481 { 482 _InList.remove(lp); 483 }(); 484 _InMap.remove(k); 485 return true; 486 } 487 auto inOut = k in _OutMap; 488 if ( inOut ) 489 { 490 491 if (__reportCacheEvents) 492 { 493 __events.insertBack(CacheEvent!(K, V)(EventType.Removed, k, inOut.value)); 494 } 495 496 auto lp = inOut.list_element_ptr; 497 () @trusted 498 { 499 _OutList.remove(lp); 500 }(); 501 _OutMap.remove(k); 502 return true; 503 } 504 auto inMain = k in _MainMap; 505 if ( inMain ) 506 { 507 508 if (__reportCacheEvents) 509 { 510 __events.insertBack(CacheEvent!(K, V)(EventType.Removed, k, inMain.value)); 511 } 512 513 auto lp = inMain.list_element_ptr; 514 _MainList.remove(lp); 515 _MainMap.remove(k); 516 return true; 517 } 518 return false; 519 } 520 } 521 522 @safe unittest 523 { 524 import std.stdio, std.format; 525 import std.datetime; 526 import core.thread; 527 import std.algorithm; 528 import std.experimental.logger; 529 globalLogLevel = LogLevel.info; 530 info("Testing 2Q"); 531 auto cache = new Cache2Q!(int, int); 532 cache.size = 12; 533 foreach(i;0..11) 534 { 535 cache.put(i,i); 536 cache.get(i-3); 537 } 538 cache.put(11,11); 539 // In: [11, 10] 540 // Out: [8, 9] 541 // Main: [0, 6, 7, 2, 3, 1, 5, 4] 542 assert(cache._InMap.length == 2); 543 assert(cache._OutMap.length == 2); 544 assert(cache._MainMap.length == 8); 545 assert(cache.length==12, "expected 12, got %d".format(cache.length)); 546 foreach(i;0..12) 547 { 548 assert(cache.get(i) == i, "missed %s".format(i)); 549 } 550 cache.clear(); 551 assert(cache.length==0); 552 foreach(i;0..11) 553 { 554 cache.put(i,i); 555 cache.get(i-3); 556 } 557 cache.put(11,11); 558 foreach(i;0..12) 559 { 560 assert(cache.remove(i), "failed to remove %s".format(i)); 561 } 562 assert(cache.length==0); 563 foreach(i;0..11) 564 { 565 cache.put(i,i); 566 cache.get(i-3); 567 } 568 cache.put(11,11); 569 // In: [11, 10] 570 // Out: [8, 9] 571 // Main: [0, 6, 7, 2, 3, 1, 5, 4] 572 cache.put(11,22); 573 cache.put(8, 88); 574 cache.put(5,55); 575 assert(cache.get(5) == 55); 576 assert(cache.get(11) == 22); 577 assert(cache.length==12, "expected 12, got %d".format(cache.length)); 578 assert(cache.get(8) == 88); // 8 moved from Out to Main 579 assert(cache.length==11, "expected 11, got %d".format(cache.length)); 580 cache.put(12,12); // in iverflowed, out filled 581 cache.put(13, 13); // in overflowed, out overflowed to main 582 assert(cache.length==12, "expected 12, got %d".format(cache.length)); 583 globalLogLevel = LogLevel.info; 584 } 585 586 unittest 587 { 588 // testing ttl 589 import std.stdio, std.format; 590 import std.datetime; 591 import std.range; 592 import std.algorithm; 593 import core.thread; 594 import std.experimental.logger; 595 596 globalLogLevel = LogLevel.info; 597 auto cache = new Cache2Q!(int, int); 598 cache.sizeIn = 2; 599 cache.sizeOut = 2; 600 cache.sizeMain = 4; 601 cache.enableCacheEvents; 602 603 cache.put(1, 1, TTL(1)); 604 cache.put(2, 2, TTL(1)); 605 // in: 1, 2 606 cache.put(3,3); 607 cache.put(4,4); 608 // in: 3, 4 609 // out 1, 2 610 cache.get(1); 611 // in: 3, 4 612 // out 2 613 // main: 1 614 cache.put(5,5, TTL(1)); 615 // In: 4(-), 5(1) // 616 // Out: 2(1), 3(-) // TTL in parens 617 // Main: 1(1) // 618 assert(4 in cache._InMap && 5 in cache._InMap); 619 assert(2 in cache._OutMap && 3 in cache._OutMap); 620 assert(1 in cache._MainMap); 621 Thread.sleep(1500.msecs); 622 assert(cache.get(1).isNull); 623 assert(cache.get(2).isNull); 624 assert(cache.get(5).isNull); 625 assert(cache.get(3) == 3); 626 assert(cache.get(4) == 4); 627 cache.clear; 628 auto e = cache.cacheEvents; 629 assert(e.filter!(a => a.event == EventType.Removed).count == 2); 630 assert(e.filter!(a => a.event == EventType.Expired).count == 3); 631 cache.ttl = 1; 632 cache.put(1, 1); // default TTL - this must not survive 1s sleep 633 cache.put(2, 2, ~TTL()); // no TTL, ignore default - this must survive any time 634 cache.put(3, 3, TTL(2)); // set TTL for this item - this must not survive 2s 635 Thread.sleep(1000.msecs); 636 assert(cache.get(1).isNull); // expired 637 assert(cache.get(2) == 2); 638 assert(cache.get(3) == 3); 639 Thread.sleep(1000.msecs); 640 assert(cache.get(2) == 2); 641 assert(cache.get(3).isNull); // expired 642 e = cache.cacheEvents; 643 assert(e.map!(a => a.event).all!(a => a == EventType.Expired)); 644 cache.remove(2); 645 e = cache.cacheEvents; 646 assert(e.length == 1 && e.front.key == 2); 647 // test cache events after clear 648 cache.sizeIn = 10; 649 iota(5).each!(i => cache.put(i,i)); 650 cache.clear; 651 e = cache.cacheEvents; 652 assert(e.length == 5); 653 assert(e.map!(a => a.event).all!(a => a == EventType.Removed)); 654 655 // test for clear from all queues 656 cache.sizeIn = 2; 657 cache.sizeOut = 2; 658 cache.sizeMain = 1; 659 cache.ttl = 0; 660 cache.put(1, 1); 661 cache.put(2, 2); 662 // in: 1, 2 663 cache.put(3, 3); 664 cache.put(4, 4); 665 // in: 3, 4 666 // out 1, 2 667 cache.get(1); 668 // in: 3, 4 669 // out 2 670 // main: 1 671 cache.put(5, 5); 672 // In: 4, 5 673 // Out: 2, 3 674 // Main: 1 675 cache.clear; 676 e = cache.cacheEvents; 677 assert(e.length == 5); 678 // test for eviction events from all queues 679 cache.put(1, 1); 680 cache.put(2, 2); 681 // in: 1, 2 682 cache.put(3, 3); 683 cache.put(4, 4); 684 // in: 3, 4 685 // out 1, 2 686 cache.get(1); 687 // in: 3, 4 688 // out 2 689 // main: 1 690 cache.put(5, 5); 691 // In: 4, 5 692 // Out: 2, 3 693 // Main: 1 694 cache.get(2); // 1 evicted and replaced by 2 695 // In: 4, 5 696 // Out: 3 697 // Main: 2 698 e = cache.cacheEvents; 699 assert(e.length == 1); 700 assert(e.front.key == 1); 701 assert(e.front.event == EventType.Evicted); 702 cache.put(4, 44, TTL(1)); // create 'updated' event in In queue 703 e = cache.cacheEvents; 704 assert(e.length == 1); 705 assert(e.front.key == 4); 706 assert(e.front.event == EventType.Updated); 707 cache.put(3, 33); // create 'updated' event in Out queue 708 e = cache.cacheEvents; 709 assert(e.length == 1); 710 assert(e.front.key == 3); 711 assert(e.front.event == EventType.Updated); 712 cache.put(2, 22); // create 'updated' event in In queue 713 e = cache.cacheEvents; 714 assert(e.length == 1); 715 assert(e.front.key == 2); 716 assert(e.front.event == EventType.Updated); 717 Thread.sleep(1.seconds); 718 // In: 4, 5 719 // Out: 3 720 // Main: 2 721 cache.put(6,6); // now key '4' expired and must be dropped from In queue 722 e = cache.cacheEvents; 723 assert(e.length == 1); 724 assert(e.front.key == 4); 725 assert(e.front.event == EventType.Expired); 726 // In: 5, 6 727 // Out: 3 728 // Main: 2 729 cache.put(7, 7); 730 // In: 6, 7 731 // Out: 3, 5 732 // Main: 2 733 cache.put(8, 8); 734 // In: 7, 8 735 // Out: 5, 6 -> 3 evicted 736 // Main: 2 737 e = cache.cacheEvents; 738 assert(e.length == 1); 739 assert(e.front.key == 3); 740 assert(e.front.event == EventType.Evicted); 741 cache.remove(7); // remove from In 742 cache.remove(5); // remove from Out 743 cache.remove(2); // remove from main 744 cache.remove(0); // remove something that were not in cache 745 e = cache.cacheEvents; 746 assert(e.length == 3); 747 assert(e.all!(a => a.event == EventType.Removed)); 748 assert(e.map!"a.key".array == [7,5,2]); 749 } 750 751 /// 752 /// 753 /// 754 @safe @nogc unittest 755 { 756 757 // create cache with total size 1024 758 auto cache = () @trusted { 759 auto allocator = Mallocator.instance; 760 return allocator.make!(Cache2Q!(int, string))(1024); 761 }(); 762 763 cache.sizeIn = 10; // if you need, later you can set any size for In queue 764 cache.sizeOut = 55; // and for out quque 765 cache.sizeMain = 600; // and for main cache 766 cache.put(1, "one"); 767 assert(cache.get(1) == "one"); // key 1 is in cache 768 assert(cache.get(2).isNull); // key 2 not in cache 769 assert(cache.length == 1); // # of elements in cache 770 cache.clear; // clear cache 771 }