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 }