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 }