Line data Source code
1 0 : /*
2 :
3 : OOCache.m
4 : By Jens Ayton
5 :
6 : Oolite
7 : Copyright (C) 2004-2013 Giles C Williams and contributors
8 :
9 : This program is free software; you can redistribute it and/or
10 : modify it under the terms of the GNU General Public License
11 : as published by the Free Software Foundation; either version 2
12 : of the License, or (at your option) any later version.
13 :
14 : This program is distributed in the hope that it will be useful,
15 : but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : GNU General Public License for more details.
18 :
19 : You should have received a copy of the GNU General Public License
20 : along with this program; if not, write to the Free Software
21 : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
22 : MA 02110-1301, USA.
23 :
24 : */
25 :
26 : /* IMPLEMENTATION NOTES
27 : A cache needs to be able to implement three types of operation
28 : efficiently:
29 : * Retrieving: looking up an element by key.
30 : * Inserting: setting the element associated with a key.
31 : * Deleting: removing a single element.
32 : * Pruning: removing one or more least-recently used elements.
33 :
34 : An NSMutableDictionary performs the first three operations efficiently but
35 : has no support for pruning - specifically no support for finding the
36 : least-recently-accessed element. Using standard Foundation containers, it
37 : would be necessary to use several dictionaries and arrays, which would be
38 : quite inefficient since small NSArrays aren’t very good at head insertion
39 : or deletion. Alternatively, a standard dictionary whose value objects
40 : maintain an age-sorted list could be used.
41 :
42 : I chose instead to implement a custom scheme from scratch. It uses two
43 : parallel data structures: a doubly-linked list sorted by age, and a splay
44 : tree to implement insertion/deletion. The implementation is largely
45 : procedural C. Deserialization, pruning and modification tracking is done
46 : in the ObjC class; everything else is done in C functions.
47 :
48 : A SPLAY TREE is a type of semi-balanced binary search tree with certain
49 : useful properties:
50 : * Simplicity. All look-up and restructuring operations are based on a
51 : single operation, splaying, which brings the node with the desired key
52 : (or the node whose key is "left" of the desired key, if there is no
53 : exact match) to the root, while maintaining the binary search tree
54 : invariant. Splaying itself is sufficient for look-up; insertion and
55 : deletion work by splaying and then manipulating at the root.
56 : * Self-optimization. Because each look-up brings the sought element to
57 : the root, often-used elements tend to stay near the top. (Oolite often
58 : performs sequences of identical look-ups, for instance when creating
59 : an asteroid field, or the racing ring set-up which uses lots of
60 : identical ring segments; during combat, missiles, canisters and hull
61 : plates will be commonly used.) Also, this means that for a retrieve-
62 : attempt/insert sequence, the retrieve attempt will optimize the tree
63 : for the insertion.
64 : * Efficiency. In addition to the self-optimization, splay trees have a
65 : small code size and no storage overhead for flags.
66 : The amortized worst-case cost of splaying (cost averaged over a
67 : worst-case sequence of operations) is O(log n); a single worst-case
68 : splay is O(n), but that worst-case also improves the balance of the
69 : tree, so you can't have two worst cases in a row. Insertion and
70 : deletion are also O(log n), consisting of a splay plus an O(1)
71 : operation.
72 : References for splay trees:
73 : * http://www.cs.cmu.edu/~sleator/papers/self-adjusting.pdf
74 : Original research paper.
75 : * http://www.ibr.cs.tu-bs.de/courses/ss98/audii/applets/BST/SplayTree-Example.html
76 : Java applet demonstrating splaying.
77 : * http://www.link.cs.cmu.edu/link/ftp-site/splaying/top-down-splay.c
78 : Sample implementation by one of the inventors. The TreeSplay(),
79 : TreeInsert() and CacheRemove() functions are based on this.
80 :
81 : The AGE LIST is a doubly-linked list, ordered from oldest to youngest.
82 : Whenever an element is retrieved or inserted, it is promoted to the
83 : youngest end of the age list. Pruning proceeds from the oldest end of the
84 : age list.
85 :
86 : if (autoPrune)
87 : {
88 : PRUNING is batched, handling 20% of the cache at once. This is primarily
89 : because deletion somewhat pessimizes the tree (see "Self-optimization"
90 : below). It also provides a bit of code coherency. To reduce pruning
91 : batches while in flight, pruning is also performed before serialization
92 : (which in turn is done, if the cache has changed, whenever the user
93 : docks). This has the effect that the number of items in the cache on disk
94 : never exceeds 80% of the prune threshold. This is probably not actually
95 : poinful, since pruning should be a very small portion of the per-frame run
96 : time in any case. Premature optimization and all that jazz.
97 : Pruning performs at most 0.2n deletions, and is thus O(n log n).
98 : }
99 : else
100 : {
101 : PRUNING is performed manually by calling -prune.
102 : }
103 :
104 : If the macro OOCACHE_PERFORM_INTEGRITY_CHECKS is set to a non-zero value,
105 : the integrity of the tree and the age list will be checked before and
106 : after each high-level operation. This is an inherently O(n) operation.
107 : */
108 :
109 :
110 : #import "OOCache.h"
111 : #import "OOStringParsing.h"
112 :
113 :
114 : #ifndef OOCACHE_PERFORM_INTEGRITY_CHECKS
115 0 : #define OOCACHE_PERFORM_INTEGRITY_CHECKS 0
116 : #endif
117 :
118 :
119 : // Protocol used internally to squash idiotic warnings in gnu-gcc.
120 : @protocol OOCacheComparable <NSObject, NSCopying>
121 0 : - (NSComparisonResult) compare:(id<OOCacheComparable>)other;
122 0 : - (id) copy;
123 : @end
124 :
125 :
126 0 : typedef struct OOCacheImpl OOCacheImpl;
127 0 : typedef struct OOCacheNode OOCacheNode;
128 :
129 :
130 0 : enum { kCountUnknown = -1U };
131 :
132 :
133 0 : static NSString * const kSerializedEntryKeyKey = @"key";
134 0 : static NSString * const kSerializedEntryKeyValue = @"value";
135 :
136 :
137 : static OOCacheImpl *CacheAllocate(void);
138 : static void CacheFree(OOCacheImpl *cache);
139 :
140 : static BOOL CacheInsert(OOCacheImpl *cache, id key, id value);
141 : static BOOL CacheRemove(OOCacheImpl *cache, id key);
142 : static BOOL CacheRemoveOldest(OOCacheImpl *cache, NSString *logKey);
143 : static id CacheRetrieve(OOCacheImpl *cache, id key);
144 : static unsigned CacheGetCount(OOCacheImpl *cache);
145 : static NSArray *CacheArrayOfContentsByAge(OOCacheImpl *cache);
146 : static NSArray *CacheArrayOfNodesByAge(OOCacheImpl *cache);
147 : static NSString *CacheGetName(OOCacheImpl *cache);
148 : static void CacheSetName(OOCacheImpl *cache, NSString *name);
149 :
150 : #if OOCACHE_PERFORM_INTEGRITY_CHECKS
151 : static NSString * const kOOLogCacheIntegrityCheck = @"dataCache.integrityCheck";
152 : static void CacheCheckIntegrity(OOCacheImpl *cache, NSString *context);
153 :
154 : #define CHECK_INTEGRITY(context) CacheCheckIntegrity(cache, (context))
155 : #else
156 0 : #define CHECK_INTEGRITY(context) do {} while (0)
157 : #endif
158 :
159 :
160 : @interface OOCache (Private)
161 :
162 0 : - (void)loadFromArray:(NSArray *)inArray;
163 :
164 : @end
165 :
166 :
167 : @implementation OOCache
168 :
169 :
170 0 : - (void)dealloc
171 : {
172 : CHECK_INTEGRITY(@"dealloc");
173 : CacheFree(cache);
174 :
175 : [super dealloc];
176 : }
177 :
178 :
179 0 : - (NSString *)description
180 : {
181 : return [NSString stringWithFormat:@"<%@ %p>{\"%@\", %u elements, prune threshold=%u, auto-prune=%s dirty=%s}", [self class], self, [self name], CacheGetCount(cache), pruneThreshold, autoPrune ? "yes" : "no", dirty ? "yes" : "no"];
182 : }
183 :
184 :
185 : - (id)init
186 : {
187 : return [self initWithPList:nil];
188 : }
189 :
190 :
191 : - (id)initWithPList:(id)pList
192 : {
193 : BOOL OK = YES;
194 :
195 : self = [super init];
196 : OK = self != nil;
197 :
198 : if (OK)
199 : {
200 : cache = CacheAllocate();
201 : if (cache == NULL) OK = NO;
202 : }
203 :
204 : if (pList != nil)
205 : {
206 : if (OK) OK = [pList isKindOfClass:[NSArray class]];
207 : if (OK) [self loadFromArray:pList];
208 : }
209 : if (OK)
210 : {
211 : pruneThreshold = kOOCacheDefaultPruneThreshold;
212 : autoPrune = YES;
213 : }
214 :
215 : if (!OK)
216 : {
217 : [self release];
218 : self = nil;
219 : }
220 :
221 : return self;
222 : }
223 :
224 :
225 : - (id)pListRepresentation
226 : {
227 : return CacheArrayOfNodesByAge(cache);
228 :
229 : return nil;
230 : }
231 :
232 :
233 : - (id)objectForKey:(id)key
234 : {
235 : id result = nil;
236 :
237 : CHECK_INTEGRITY(@"objectForKey: before");
238 :
239 : result = CacheRetrieve(cache, key);
240 : // Note: while reordering the age list technically makes the cache dirty, it's not worth rewriting it just for that, so we don't flag it.
241 :
242 : CHECK_INTEGRITY(@"objectForKey: after");
243 :
244 : return [[result retain] autorelease];
245 : }
246 :
247 :
248 : - (void)setObject:inObject forKey:(id)key
249 : {
250 : CHECK_INTEGRITY(@"setObject:forKey: before");
251 :
252 : if (CacheInsert(cache, key, inObject))
253 : {
254 : dirty = YES;
255 : if (autoPrune) [self prune];
256 : }
257 :
258 : CHECK_INTEGRITY(@"setObject:forKey: after");
259 : }
260 :
261 :
262 : - (void)removeObjectForKey:(id)key
263 : {
264 : CHECK_INTEGRITY(@"removeObjectForKey: before");
265 :
266 : if (CacheRemove(cache, key)) dirty = YES;
267 :
268 : CHECK_INTEGRITY(@"removeObjectForKey: after");
269 : }
270 :
271 :
272 : - (void)setPruneThreshold:(unsigned)threshold
273 : {
274 : threshold = MAX(threshold, (unsigned)kOOCacheMinimumPruneThreshold);
275 : if (threshold != pruneThreshold)
276 : {
277 : pruneThreshold = threshold;
278 : if (autoPrune) [self prune];
279 : }
280 : }
281 :
282 :
283 : - (unsigned)pruneThreshold
284 : {
285 : return pruneThreshold;
286 : }
287 :
288 :
289 : - (void)setAutoPrune:(BOOL)flag
290 : {
291 : BOOL prune = (flag != NO);
292 : if (prune != autoPrune)
293 : {
294 : autoPrune = prune;
295 : [self prune];
296 : }
297 : }
298 :
299 :
300 : - (BOOL)autoPrune
301 : {
302 : return autoPrune;
303 : }
304 :
305 :
306 : - (void)prune
307 : {
308 : unsigned pruneCount;
309 : unsigned desiredCount;
310 : unsigned count;
311 :
312 : // Order of operations is to ensure rounding down.
313 : if (autoPrune) desiredCount = (pruneThreshold * 4) / 5;
314 : else desiredCount = pruneThreshold;
315 :
316 : if (pruneThreshold == kOOCacheNoPrune || (count = CacheGetCount(cache)) <= pruneThreshold) return;
317 :
318 : pruneCount = count - desiredCount;
319 :
320 : NSString *logKey = [NSString stringWithFormat:@"dataCache.prune.%@", CacheGetName(cache)];
321 : OOLog(logKey, @"Pruning cache \"%@\" - removing %u entries", CacheGetName(cache), pruneCount);
322 : OOLogIndentIf(logKey);
323 :
324 : while (pruneCount--) CacheRemoveOldest(cache, logKey);
325 :
326 : OOLogOutdentIf(logKey);
327 : }
328 :
329 :
330 : - (BOOL)dirty
331 : {
332 : return dirty;
333 : }
334 :
335 :
336 : - (void)markClean
337 : {
338 : dirty = NO;
339 : }
340 :
341 :
342 : - (NSString *)name
343 : {
344 : return CacheGetName(cache);
345 : }
346 :
347 :
348 : - (void)setName:(NSString *)name
349 : {
350 : CacheSetName(cache, name);
351 : }
352 :
353 :
354 : - (NSArray *) objectsByAge
355 : {
356 : return CacheArrayOfContentsByAge(cache);
357 : }
358 :
359 : @end
360 :
361 :
362 : @implementation OOCache (Private)
363 :
364 : - (void)loadFromArray:(NSArray *)array
365 : {
366 : NSEnumerator *entryEnum = nil;
367 : NSDictionary *entry = nil;
368 : NSString *key = nil;
369 : id value = nil;
370 :
371 : if (array == nil) return;
372 :
373 : for (entryEnum = [array objectEnumerator]; (entry = [entryEnum nextObject]); )
374 : {
375 : if ([entry isKindOfClass:[NSDictionary class]])
376 : {
377 : key = [entry objectForKey:kSerializedEntryKeyKey];
378 : value = [entry objectForKey:kSerializedEntryKeyValue];
379 : if ([key isKindOfClass:[NSString class]] && value != nil)
380 : {
381 : [self setObject:value forKey:key];
382 : }
383 : }
384 : }
385 : }
386 :
387 : @end
388 :
389 :
390 : /***** Most of the implementation. In C. Because I'm inconsistent and slightly m. *****/
391 :
392 0 : struct OOCacheImpl
393 : {
394 : // Splay tree root
395 0 : OOCacheNode *root;
396 :
397 : // Ends of age list
398 0 : OOCacheNode *oldest, *youngest;
399 :
400 0 : unsigned count;
401 0 : NSString *name;
402 : };
403 :
404 :
405 0 : struct OOCacheNode
406 : {
407 : // Payload
408 0 : id<OOCacheComparable> key;
409 0 : id value;
410 :
411 : // Splay tree
412 0 : OOCacheNode *leftChild, *rightChild;
413 :
414 : // Age list
415 0 : OOCacheNode *younger, *older;
416 : };
417 :
418 : static OOCacheNode *CacheNodeAllocate(id<OOCacheComparable> key, id value);
419 : static void CacheNodeFree(OOCacheImpl *cache, OOCacheNode *node);
420 : static id CacheNodeGetValue(OOCacheNode *node);
421 : static void CacheNodeSetValue(OOCacheNode *node, id value);
422 :
423 : #if OOCACHE_PERFORM_INTEGRITY_CHECKS
424 : static NSString *CacheNodeGetDescription(OOCacheNode *node);
425 : #endif
426 :
427 : static OOCacheNode *TreeSplay(OOCacheNode **root, id<OOCacheComparable> key);
428 : static OOCacheNode *TreeInsert(OOCacheImpl *cache, id<OOCacheComparable> key, id value);
429 :
430 : #if OOCACHE_PERFORM_INTEGRITY_CHECKS
431 : static unsigned TreeCountNodes(OOCacheNode *node);
432 : static OOCacheNode *TreeCheckIntegrity(OOCacheImpl *cache, OOCacheNode *node, OOCacheNode *expectedParent, NSString *context);
433 : #endif
434 :
435 : static void AgeListMakeYoungest(OOCacheImpl *cache, OOCacheNode *node);
436 : static void AgeListRemove(OOCacheImpl *cache, OOCacheNode *node);
437 :
438 : #if OOCACHE_PERFORM_INTEGRITY_CHECKS
439 : static void AgeListCheckIntegrity(OOCacheImpl *cache, NSString *context);
440 : #endif
441 :
442 :
443 : /***** CacheImpl functions *****/
444 :
445 0 : static OOCacheImpl *CacheAllocate(void)
446 : {
447 : return calloc(sizeof (OOCacheImpl), 1);
448 : }
449 :
450 :
451 0 : static void CacheFree(OOCacheImpl *cache)
452 : {
453 : if (cache == NULL) return;
454 :
455 : CacheNodeFree(cache, cache->root);
456 : [cache->name autorelease];
457 : free(cache);
458 : }
459 :
460 :
461 0 : static BOOL CacheInsert(OOCacheImpl *cache, id key, id value)
462 : {
463 : OOCacheNode *node = NULL;
464 :
465 : if (cache == NULL || key == nil || value == nil) return NO;
466 :
467 : node = TreeInsert(cache, key, value);
468 : if (node != NULL)
469 : {
470 : AgeListMakeYoungest(cache, node);
471 : return YES;
472 : }
473 : else return NO;
474 : }
475 :
476 :
477 0 : static BOOL CacheRemove(OOCacheImpl *cache, id key)
478 : {
479 : OOCacheNode *node = NULL, *newRoot = NULL;
480 :
481 : node = TreeSplay(&cache->root, key);
482 : if (node != NULL)
483 : {
484 : if (node->leftChild == NULL) newRoot = node->rightChild;
485 : else
486 : {
487 : newRoot = node->leftChild;
488 : TreeSplay(&newRoot, key);
489 : newRoot->rightChild = node->rightChild;
490 : }
491 : node->leftChild = NULL;
492 : node->rightChild = NULL;
493 :
494 : cache->root = newRoot;
495 : --cache->count;
496 :
497 : AgeListRemove(cache, node);
498 : CacheNodeFree(cache, node);
499 :
500 : return YES;
501 : }
502 : else return NO;
503 : }
504 :
505 :
506 0 : static BOOL CacheRemoveOldest(OOCacheImpl *cache, NSString *logKey)
507 : {
508 : // This could be more efficient, but does it need to be?
509 : if (cache == NULL || cache->oldest == NULL) return NO;
510 :
511 : OOLog(logKey, @"Pruning cache \"%@\": removing %@", cache->name, cache->oldest->key);
512 : return CacheRemove(cache, cache->oldest->key);
513 : }
514 :
515 :
516 0 : static id CacheRetrieve(OOCacheImpl *cache, id key)
517 : {
518 : OOCacheNode *node = NULL;
519 : id result = nil;
520 :
521 : if (cache == NULL || key == NULL) return nil;
522 :
523 : node = TreeSplay(&cache->root, key);
524 : if (node != NULL)
525 : {
526 : result = CacheNodeGetValue(node);
527 : AgeListMakeYoungest(cache, node);
528 : }
529 : return result;
530 : }
531 :
532 :
533 0 : static NSArray *CacheArrayOfContentsByAge(OOCacheImpl *cache)
534 : {
535 : OOCacheNode *node = NULL;
536 : NSMutableArray *result = nil;
537 :
538 : if (cache == NULL || cache->count == 0) return nil;
539 :
540 : result = [NSMutableArray arrayWithCapacity:cache->count];
541 :
542 : for (node = cache->youngest; node != NULL; node = node->older)
543 : {
544 : [result addObject:node->value];
545 : }
546 : return result;
547 : }
548 :
549 :
550 0 : static NSArray *CacheArrayOfNodesByAge(OOCacheImpl *cache)
551 : {
552 : OOCacheNode *node = NULL;
553 : NSMutableArray *result = nil;
554 :
555 : if (cache == NULL || cache->count == 0) return nil;
556 :
557 : result = [NSMutableArray arrayWithCapacity:cache->count];
558 :
559 : for (node = cache->oldest; node != NULL; node = node->younger)
560 : {
561 : [result addObject:[NSDictionary dictionaryWithObjectsAndKeys:node->key, kSerializedEntryKeyKey, node->value, kSerializedEntryKeyValue, nil]];
562 : }
563 : return result;
564 : }
565 :
566 :
567 0 : static NSString *CacheGetName(OOCacheImpl *cache)
568 : {
569 : return cache->name;
570 : }
571 :
572 :
573 0 : static void CacheSetName(OOCacheImpl *cache, NSString *name)
574 : {
575 : [cache->name autorelease];
576 : cache->name = [name copy];
577 : }
578 :
579 :
580 0 : static unsigned CacheGetCount(OOCacheImpl *cache)
581 : {
582 : return cache->count;
583 : }
584 :
585 : #if OOCACHE_PERFORM_INTEGRITY_CHECKS
586 :
587 : static void CacheCheckIntegrity(OOCacheImpl *cache, NSString *context)
588 : {
589 : unsigned trueCount;
590 :
591 : cache->root = TreeCheckIntegrity(cache, cache->root, NULL, context);
592 :
593 : trueCount = TreeCountNodes(cache->root);
594 : if (kCountUnknown == cache->count) cache->count = trueCount;
595 : else if (cache->count != trueCount)
596 : {
597 : OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): count is %u, but should be %u.", context, cache->name, cache->count, trueCount);
598 : cache->count = trueCount;
599 : }
600 :
601 : AgeListCheckIntegrity(cache, context);
602 : }
603 :
604 : #endif // OOCACHE_PERFORM_INTEGRITY_CHECKS
605 :
606 :
607 : /***** CacheNode functions *****/
608 :
609 : // CacheNodeAllocate(): create a cache node for a key, value pair, without inserting it in the structures.
610 0 : static OOCacheNode *CacheNodeAllocate(id<OOCacheComparable> key, id value)
611 : {
612 : OOCacheNode *result = NULL;
613 :
614 : if (key == nil || value == nil) return NULL;
615 :
616 : result = calloc(sizeof *result, 1);
617 : if (result != NULL)
618 : {
619 : result->key = [key copy];
620 : result->value = [value retain];
621 : }
622 :
623 : return result;
624 : }
625 :
626 :
627 : // CacheNodeFree(): recursively delete a cache node and its children in the splay tree. To delete an individual node, first clear its child pointers.
628 0 : static void CacheNodeFree(OOCacheImpl *cache, OOCacheNode *node)
629 : {
630 : id key, value;
631 :
632 : if (node == NULL) return;
633 :
634 : AgeListRemove(cache, node);
635 :
636 : key = node->key;
637 : node->key = nil;
638 : [key release];
639 :
640 : value = node->value;
641 : node->value = nil;
642 : [value release];
643 :
644 : CacheNodeFree(cache, node->leftChild);
645 : CacheNodeFree(cache, node->rightChild);
646 :
647 : free(node);
648 : }
649 :
650 :
651 : // CacheNodeGetValue(): retrieve the value of a cache node
652 0 : static id CacheNodeGetValue(OOCacheNode *node)
653 : {
654 : if (node == NULL) return nil;
655 :
656 : return node->value;
657 : }
658 :
659 :
660 : // CacheNodeSetValue(): change the value of a cache node (as when setObject:forKey: is called for an existing key).
661 0 : static void CacheNodeSetValue(OOCacheNode *node, id value)
662 : {
663 : if (node == NULL) return;
664 :
665 : id tmp = node->value;
666 : node->value = [value retain];
667 : [tmp release];
668 : }
669 :
670 :
671 : #if OOCACHE_PERFORM_INTEGRITY_CHECKS
672 : // CacheNodeGetDescription(): get a description of a cache node for debugging purposes.
673 : static NSString *CacheNodeGetDescription(OOCacheNode *node)
674 : {
675 : if (node == NULL) return @"0[null]";
676 :
677 : return [NSString stringWithFormat:@"%p[\"%@\"]", node, node->key];
678 : }
679 : #endif // OOCACHE_PERFORM_INTEGRITY_CHECKS
680 :
681 :
682 : /***** Tree functions *****/
683 :
684 : /* TreeSplay()
685 : This is the fundamental operation of a splay tree. It searches for a node
686 : with a given key, and rebalances the tree so that the found node becomes
687 : the root. If no match is found, the node moved to the root is the one that
688 : would have been found before the target, and will thus be a neighbour of
689 : the target if the key is subsequently inserted.
690 : */
691 0 : static OOCacheNode *TreeSplay(OOCacheNode **root, id<OOCacheComparable> key)
692 : {
693 : NSComparisonResult order;
694 : OOCacheNode N = { .leftChild = NULL, .rightChild = NULL };
695 : OOCacheNode *node = NULL, *temp = NULL, *l = &N, *r = &N;
696 : BOOL exact = NO;
697 :
698 : if (root == NULL || *root == NULL || key == nil) return NULL;
699 :
700 : node = *root;
701 :
702 : for (;;)
703 : {
704 : #ifndef NDEBUG
705 : if (node == NULL)
706 : {
707 : OOLog(@"node.error", @"%@", @"node is NULL");
708 : }
709 : else if (node->key == NULL)
710 : {
711 : OOLog(@"node.error", @"%@", @"node->key is NULL");
712 : }
713 : #endif
714 : order = [key compare:node->key];
715 : if (order == NSOrderedAscending)
716 : {
717 : // Closest match is in left subtree
718 : if (node->leftChild == NULL) break;
719 : if ([key compare:node->leftChild->key] == NSOrderedAscending)
720 : {
721 : // Rotate right
722 : temp = node->leftChild;
723 : node->leftChild = temp->rightChild;
724 : temp->rightChild = node;
725 : node = temp;
726 : if (node->leftChild == NULL) break;
727 : }
728 : // Link right
729 : r->leftChild = node;
730 : r = node;
731 : node = node->leftChild;
732 : }
733 : else if (order == NSOrderedDescending)
734 : {
735 : // Closest match is in right subtree
736 : if (node->rightChild == NULL) break;
737 : if ([key compare:node->rightChild->key] == NSOrderedDescending)
738 : {
739 : // Rotate left
740 : temp = node->rightChild;
741 : node->rightChild = temp->leftChild;
742 : temp->leftChild = node;
743 : node = temp;
744 : if (node->rightChild == NULL) break;
745 : }
746 : // Link left
747 : l->rightChild = node;
748 : l = node;
749 : node = node->rightChild;
750 : }
751 : else
752 : {
753 : // Found exact match
754 : exact = YES;
755 : break;
756 : }
757 : }
758 :
759 : // Assemble
760 : l->rightChild = node->leftChild;
761 : r->leftChild = node->rightChild;
762 : node->leftChild = N.rightChild;
763 : node->rightChild = N.leftChild;
764 :
765 : *root = node;
766 : return exact ? node : NULL;
767 : }
768 :
769 :
770 0 : static OOCacheNode *TreeInsert(OOCacheImpl *cache, id<OOCacheComparable> key, id value)
771 : {
772 : OOCacheNode *closest = NULL,
773 : *node = NULL;
774 : NSComparisonResult order;
775 :
776 : if (cache == NULL || key == nil || value == nil) return NULL;
777 :
778 : if (cache->root == NULL)
779 : {
780 : node = CacheNodeAllocate(key, value);
781 : cache->root = node;
782 : cache->count = 1;
783 : }
784 : else
785 : {
786 : node = TreeSplay(&cache->root, key);
787 : if (node != NULL)
788 : {
789 : // Exact match: key already exists, reuse its node
790 : CacheNodeSetValue(node, value);
791 : }
792 : else
793 : {
794 : closest = cache->root;
795 : node = CacheNodeAllocate(key, value);
796 : if (EXPECT_NOT(node == NULL)) return NULL;
797 :
798 : order = [key compare:closest->key];
799 :
800 : if (order == NSOrderedAscending)
801 : {
802 : // Insert to left
803 : node->leftChild = closest->leftChild;
804 : node->rightChild = closest;
805 : closest->leftChild = NULL;
806 : cache->root = node;
807 : ++cache->count;
808 : }
809 : else if (order == NSOrderedDescending)
810 : {
811 : // Insert to right
812 : node->rightChild = closest->rightChild;
813 : node->leftChild = closest;
814 : closest->rightChild = NULL;
815 : cache->root = node;
816 : ++cache->count;
817 : }
818 : else
819 : {
820 : // Key already exists, which we should have caught above
821 : OOLog(@"dataCache.inconsistency", @"%s() internal inconsistency for cache \"%@\", insertion failed.", __PRETTY_FUNCTION__, cache->name);
822 : CacheNodeFree(cache, node);
823 : return NULL;
824 : }
825 : }
826 : }
827 :
828 : return node;
829 : }
830 :
831 :
832 : #if OOCACHE_PERFORM_INTEGRITY_CHECKS
833 : static unsigned TreeCountNodes(OOCacheNode *node)
834 : {
835 : if (node == NULL) return 0;
836 : return 1 + TreeCountNodes(node->leftChild) + TreeCountNodes(node->rightChild);
837 : }
838 :
839 :
840 : // TreeCheckIntegrity(): verify the links and contents of a (sub-)tree. If successful, returns the root of the subtree (which could theoretically be changed), otherwise returns NULL.
841 : static OOCacheNode *TreeCheckIntegrity(OOCacheImpl *cache, OOCacheNode *node, OOCacheNode *expectedParent, NSString *context)
842 : {
843 : NSComparisonResult order;
844 : BOOL OK = YES;
845 :
846 : if (node == NULL) return NULL;
847 :
848 : if (OK && node->key == nil)
849 : {
850 : OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): node \"%@\" has nil key; deleting subtree.", context, cache->name, CacheNodeGetDescription(node));
851 : OK = NO;
852 : }
853 :
854 : if (OK && node->value == nil)
855 : {
856 : OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): node \"%@\" has nil value, deleting.", context, cache->name, CacheNodeGetDescription(node));
857 : OK = NO;
858 : }
859 : if (OK && node->leftChild != NULL)
860 : {
861 : order = [node->key compare:node->leftChild->key];
862 : if (order != NSOrderedDescending)
863 : {
864 : OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): node %@'s left child %@ is not correctly ordered. Deleting subtree.", context, cache->name, CacheNodeGetDescription(node), CacheNodeGetDescription(node->leftChild));
865 : CacheNodeFree(cache, node->leftChild);
866 : node->leftChild = NULL;
867 : cache->count = kCountUnknown;
868 : }
869 : else
870 : {
871 : node->leftChild = TreeCheckIntegrity(cache, node->leftChild, node, context);
872 : }
873 : }
874 : if (node->rightChild != NULL)
875 : {
876 : order = [node->key compare:node->rightChild->key];
877 : if (order != NSOrderedAscending)
878 : {
879 : OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): node \"%@\"'s right child \"%@\" is not correctly ordered. Deleting subtree.", context, cache->name, CacheNodeGetDescription(node), CacheNodeGetDescription(node->rightChild));
880 : CacheNodeFree(cache, node->rightChild);
881 : node->rightChild = NULL;
882 : cache->count = kCountUnknown;
883 : }
884 : else
885 : {
886 : node->rightChild = TreeCheckIntegrity(cache, node->rightChild, node, context);
887 : }
888 : }
889 :
890 : if (OK) return node;
891 : else
892 : {
893 : cache->count = kCountUnknown;
894 : CacheNodeFree(cache, node);
895 : return NULL;
896 : }
897 : }
898 : #endif // OOCACHE_PERFORM_INTEGRITY_CHECKS
899 :
900 :
901 : /***** Age list functions *****/
902 :
903 : // AgeListMakeYoungest(): place a given cache node at the youngest end of the age list.
904 0 : static void AgeListMakeYoungest(OOCacheImpl *cache, OOCacheNode *node)
905 : {
906 : if (cache == NULL || node == NULL) return;
907 :
908 : AgeListRemove(cache, node);
909 : node->older = cache->youngest;
910 : if (NULL != cache->youngest) cache->youngest->younger = node;
911 : cache->youngest = node;
912 : if (cache->oldest == NULL) cache->oldest = node;
913 : }
914 :
915 :
916 : // AgeListRemove(): remove a cache node from the age-sorted tree. Does not affect its position in the splay tree.
917 0 : static void AgeListRemove(OOCacheImpl *cache, OOCacheNode *node)
918 : {
919 : OOCacheNode *younger = NULL;
920 : OOCacheNode *older = NULL;
921 :
922 : if (node == NULL) return;
923 :
924 : younger = node->younger;
925 : older = node->older;
926 :
927 : if (cache->youngest == node) cache->youngest = older;
928 : if (cache->oldest == node) cache->oldest = younger;
929 :
930 : node->younger = NULL;
931 : node->older = NULL;
932 :
933 : if (younger != NULL) younger->older = older;
934 : if (older != NULL) older->younger = younger;
935 : }
936 :
937 :
938 : #if OOCACHE_PERFORM_INTEGRITY_CHECKS
939 :
940 : static void AgeListCheckIntegrity(OOCacheImpl *cache, NSString *context)
941 : {
942 : OOCacheNode *node = NULL, *next = NULL;
943 : unsigned seenCount = 0;
944 :
945 : if (cache == NULL || context == NULL) return;
946 :
947 : node = cache->youngest;
948 :
949 : if (node) for (;;)
950 : {
951 : next = node->older;
952 : ++seenCount;
953 : if (next == NULL) break;
954 :
955 : if (next->younger != node)
956 : {
957 : OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): node \"%@\" has invalid older link (should be \"%@\", is \"%@\"); repairing.", context, cache->name, CacheNodeGetDescription(next), CacheNodeGetDescription(node), CacheNodeGetDescription(next->older));
958 : next->older = node;
959 : }
960 : node = next;
961 : }
962 :
963 : if (seenCount != cache->count)
964 : {
965 : // This is especially bad since this function is called just after verifying that the count field reflects the number of objects in the tree.
966 : OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): expected %u nodes, found %u. Cannot repair; clearing cache.", context, cache->name, cache->count, seenCount);
967 :
968 : /* Start of temporary extra logging */
969 : node = cache->youngest;
970 :
971 : if (node)
972 : {
973 : for (;;)
974 : {
975 : next = node->older;
976 : ++seenCount;
977 : if (next == NULL) break;
978 :
979 : if (node->key != NULL)
980 : {
981 : OOLog(kOOLogCacheIntegrityCheck,@"Key is: %@",node->key);
982 : }
983 : else
984 : {
985 : OOLog(kOOLogCacheIntegrityCheck,@"Key is: NULL");
986 : }
987 :
988 : if (node->value != NULL)
989 : {
990 : OOLog(kOOLogCacheIntegrityCheck,@"Value is: %@",node->value);
991 : }
992 : else
993 : {
994 : OOLog(kOOLogCacheIntegrityCheck,@"Value is: NULL");
995 : }
996 :
997 : node = next;
998 : }
999 : }
1000 : /* End of temporary extra logging */
1001 :
1002 : cache->count = 0;
1003 : CacheNodeFree(cache, cache->root);
1004 : cache->root = NULL;
1005 : cache->youngest = NULL;
1006 : cache->oldest = NULL;
1007 : return;
1008 : }
1009 :
1010 : if (node != cache->oldest)
1011 : {
1012 : OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): oldest pointer in cache is wrong (should be \"%@\", is \"%@\"); repairing.", context, cache->name, CacheNodeGetDescription(node), CacheNodeGetDescription(cache->oldest));
1013 : cache->oldest = node;
1014 : }
1015 : }
1016 :
1017 : #endif // OOCACHE_PERFORM_INTEGRITY_CHECKS
1018 :
1019 :
1020 : #if DEBUG_GRAPHVIZ
1021 :
1022 : /* NOTE: enabling AGE_LIST can result in graph rendering times of many hours,
1023 : because determining paths for non-constraint arcs is NP-hard. In particular,
1024 : I gave up on rendering a dump of a fairly minimal cache manager after
1025 : three and a half hours. Individual caches were fine.
1026 : */
1027 : #define AGE_LIST 0
1028 :
1029 : @implementation OOCache (DebugGraphViz)
1030 :
1031 : - (void) appendNodesFromSubTree:(OOCacheNode *)subTree toString:(NSMutableString *)ioString
1032 : {
1033 : [ioString appendFormat:@"\tn%p [label=\"<f0> | <f1> %@ | <f2>\"];\n", subTree, EscapedGraphVizString([subTree->key description])];
1034 :
1035 : if (subTree->leftChild != NULL)
1036 : {
1037 : [self appendNodesFromSubTree:subTree->leftChild toString:ioString];
1038 : [ioString appendFormat:@"\tn%p:f0 -> n%p:f1;\n", subTree, subTree->leftChild];
1039 : }
1040 : if (subTree->rightChild != NULL)
1041 : {
1042 : [self appendNodesFromSubTree:subTree->rightChild toString:ioString];
1043 : [ioString appendFormat:@"\tn%p:f2 -> n%p:f1;\n", subTree, subTree->rightChild];
1044 : }
1045 : }
1046 :
1047 :
1048 : - (NSString *) generateGraphVizBodyWithRootNamed:(NSString *)rootName
1049 : {
1050 : NSMutableString *result = nil;
1051 :
1052 : result = [NSMutableString string];
1053 :
1054 : // Root node representing cache
1055 : [result appendFormat:@"\t%@ [label=\"Cache \\\"%@\\\"\" shape=box];\n"
1056 : "\tnode [shape=record];\n\t\n", rootName, EscapedGraphVizString([self name])];
1057 :
1058 : if (cache == NULL) return result;
1059 :
1060 : // Cache
1061 : [self appendNodesFromSubTree:cache->root toString:result];
1062 :
1063 : // Arc from cache object to root node
1064 : [result appendString:@"\tedge [color=black constraint=true];\n"];
1065 : [result appendFormat:@"\t%@ -> n%p:f1;\n", rootName, cache->root];
1066 :
1067 : #if AGE_LIST
1068 : OOCacheNode *node = NULL;
1069 : // Arcs representing age list
1070 : [result appendString:@"\t\n\t// Age-sorted list in blue\n\tedge [color=blue constraint=false];\n"];
1071 : node = cache->oldest;
1072 : while (node->younger != NULL)
1073 : {
1074 : [result appendFormat:@"\tn%p:f2 -> n%p:f0;\n", node, node->younger];
1075 : node = node->younger;
1076 : }
1077 : #endif
1078 :
1079 : return result;
1080 : }
1081 :
1082 :
1083 : - (NSString *) generateGraphViz
1084 : {
1085 : NSMutableString *result = nil;
1086 :
1087 : result = [NSMutableString string];
1088 :
1089 : // Header
1090 : [result appendFormat:
1091 : @"// OOCache dump\n\n"
1092 : "digraph cache\n"
1093 : "{\n"
1094 : "\tgraph [charset=\"UTF-8\", label=\"OOCache \"%@\" debug dump\", labelloc=t, labeljust=l];\n\t\n", [self name]];
1095 :
1096 : [result appendString:[self generateGraphVizBodyWithRootNamed:@"cache"]];
1097 :
1098 : [result appendString:@"}\n"];
1099 :
1100 : return result;
1101 : }
1102 :
1103 :
1104 : - (void) writeGraphVizToURL:(NSURL *)url
1105 : {
1106 : NSString *graphViz = nil;
1107 : NSData *data = nil;
1108 :
1109 : graphViz = [self generateGraphViz];
1110 : data = [graphViz dataUsingEncoding:NSUTF8StringEncoding];
1111 :
1112 : if (data != nil)
1113 : {
1114 : [data writeToURL:url atomically:YES];
1115 : }
1116 : }
1117 :
1118 :
1119 : - (void) writeGraphVizToPath:(NSString *)path
1120 : {
1121 : [self writeGraphVizToURL:[NSURL fileURLWithPath:path]];
1122 : }
1123 :
1124 : @end
1125 : #endif
1126 :
|