Oolite 1.91.0.7646-241128-10e222e
Loading...
Searching...
No Matches
OOCacheManager.m
Go to the documentation of this file.
1/*
2
3OOCacheManager.m
4
5Oolite
6Copyright (C) 2004-2013 Giles C Williams and contributors
7
8This program is free software; you can redistribute it and/or
9modify it under the terms of the GNU General Public License
10as published by the Free Software Foundation; either version 2
11of the License, or (at your option) any later version.
12
13This program is distributed in the hope that it will be useful,
14but WITHOUT ANY WARRANTY; without even the implied warranty of
15MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16GNU General Public License for more details.
17
18You should have received a copy of the GNU General Public License
19along with this program; if not, write to the Free Software
20Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21MA 02110-1301, USA.
22
23*/
24
25#import "OOCacheManager.h"
26#import "OOPListParsing.h"
27#import "OODeepCopy.h"
31
32
33#define WRITE_ASYNC 1
34#define PROFILE_WRITES 0
35
36
37// Use the (presumed) most efficient plist format for each platform.
38#if OOLITE_MAC_OS_X
39#define CACHE_PLIST_FORMAT NSPropertyListBinaryFormat_v1_0
40#else
41#define CACHE_PLIST_FORMAT NSPropertyListGNUstepBinaryFormat
42#endif
43
44
45#if WRITE_ASYNC
47#endif
48#if PROFILE_WRITES
50#endif
51
52
53static NSString * const kOOLogDataCacheFound = @"dataCache.found";
54static NSString * const kOOLogDataCacheNotFound = @"dataCache.notFound";
55static NSString * const kOOLogDataCacheRebuild = @"dataCache.rebuild";
56static NSString * const kOOLogDataCacheWriteSuccess = @"dataCache.write.success";
57static NSString * const kOOLogDataCacheWriteFailed = @"dataCache.write.failed";
58static NSString * const kOOLogDataCacheRetrieveSuccess = @"dataCache.retrieve.success";
59static NSString * const kOOLogDataCacheRetrieveFailed = @"dataCache.retrieve.failed";
60static NSString * const kOOLogDataCacheSetSuccess = @"dataCache.set.success";
61static NSString * const kOOLogDataCacheSetFailed = @"dataCache.set.failed";
62static NSString * const kOOLogDataCacheRemoveSuccess = @"dataCache.remove.success";
63static NSString * const kOOLogDataCacheClearSuccess = @"dataCache.clear.success";
64static NSString * const kOOLogDataCacheBuildPathError = @"dataCache.write.buildPath.failed";
65static NSString * const kOOLogDataCacheSerializationError = @"dataCache.write.serialize.failed";
66
67static NSString * const kCacheKeyVersion = @"version";
68static NSString * const kCacheKeyEndianTag = @"endian tag";
69static NSString * const kCacheKeyFormatVersion = @"format version";
70static NSString * const kCacheKeyCaches = @"caches";
71
72
73enum
74{
75 kEndianTagValue = 0x0123456789ABCDEFULL,
77};
78
79
81
82
83@interface OOCacheManager (Private)
84
85- (void)loadCache;
86- (void)write;
87- (void)clear;
88- (BOOL)dirty;
89- (void)markClean;
90
91- (NSDictionary *)loadDict;
92- (BOOL)writeDict:(NSDictionary *)inDict;
93
94- (void)buildCachesFromDictionary:(NSDictionary *)inDict;
95- (NSDictionary *)dictionaryOfCaches;
96
97- (BOOL)directoryExists:(NSString *)inPath create:(BOOL)inCreate;
98
99@end
100
101
102@interface OOCacheManager (PlatformSpecific)
103
104- (NSString *)cachePathCreatingIfNecessary:(BOOL)inCreate;
105
106@end
107
108
109#if WRITE_ASYNC
111{
112@private
113 NSDictionary *_cacheContents;
115
116- (id) initWithCacheContents:(NSDictionary *)cacheContents;
117
118@end
119#endif
120
121
122@implementation OOCacheManager
123
124- (id)init
125{
126 self = [super init];
127 if (self != nil)
128 {
129 _permitWrites = YES;
130 [self loadCache];
131 }
132 return self;
133}
134
135
136- (void)dealloc
137{
138 [self clear];
139
140 [super dealloc];
141}
142
143
144- (NSString *)description
145{
146 return [NSString stringWithFormat:@"<%@ %p>{dirty=%s}", [self class], self, [self dirty] ? "yes" : "no"];
147}
148
149
150+ (OOCacheManager *) sharedCache
151{
152 // NOTE: assumes single-threaded access.
153 if (sSingleton == nil)
154 {
155 sSingleton = [[self alloc] init];
156 }
157
158 return sSingleton;
159}
160
161
162- (id)objectForKey:(NSString *)inKey inCache:(NSString *)inCacheKey
163{
164 NSMutableDictionary *cache = nil;
165 id result = nil;
166
167 NSParameterAssert(inKey != nil && inCacheKey != nil);
168
169 cache = [_caches objectForKey:inCacheKey];
170 if (cache != nil)
171 {
172 result = [cache objectForKey:inKey];
173 if (result != nil)
174 {
175 OODebugLog(kOOLogDataCacheRetrieveSuccess, @"Retrieved \"%@\" cache object %@.", inCacheKey, inKey);
176 }
177 else
178 {
179 OODebugLog(kOOLogDataCacheRetrieveFailed, @"Failed to retrieve \"%@\" cache object %@ -- no such entry.", inCacheKey, inKey);
180 }
181 }
182 else
183 {
184 OODebugLog(kOOLogDataCacheRetrieveFailed, @"Failed to retrieve \"%@\" cache object %@ -- no such cache.", inCacheKey, inKey);
185 }
186
187 return result;
188}
189
190
191
192- (void)setObject:(id)inObject forKey:(NSString *)inKey inCache:(NSString *)inCacheKey
193{
194 NSMutableDictionary *cache = nil;
195
196 NSParameterAssert(inObject != nil && inKey != nil && inCacheKey != nil);
197
198 if (EXPECT_NOT(_caches == nil)) return;
199
200 cache = [_caches objectForKey:inCacheKey];
201 if (cache == nil)
202 {
203 cache = [NSMutableDictionary dictionary];
204 if (cache == nil)
205 {
206 OODebugLog(kOOLogDataCacheSetFailed, @"Failed to create cache for key \"%@\".", inCacheKey);
207 return;
208 }
209 [_caches setObject:cache forKey:inCacheKey];
210 }
211
212 [cache setObject:inObject forKey:inKey];
213 _dirty = YES;
214 OODebugLog(kOOLogDataCacheSetSuccess, @"Updated entry %@ in cache \"%@\".", inKey, inCacheKey);
215}
216
217
218- (void)removeObjectForKey:(NSString *)inKey inCache:(NSString *)inCacheKey
219{
220 NSMutableDictionary *cache = nil;
221
222 NSParameterAssert(inKey != nil && inCacheKey != nil);
223
224 cache = [_caches objectForKey:inCacheKey];
225 if (cache != nil)
226 {
227 if (nil != [cache objectForKey:inKey])
228 {
229 [cache removeObjectForKey:inKey];
230 _dirty = YES;
231 OODebugLog(kOOLogDataCacheRemoveSuccess, @"Removed entry keyed %@ from cache \"%@\".", inKey, inCacheKey);
232 }
233 else
234 {
235 OODebugLog(kOOLogDataCacheRemoveSuccess, @"No need to remove non-existent entry keyed %@ from cache \"%@\".", inKey, inCacheKey);
236 }
237 }
238 else
239 {
240 OODebugLog(kOOLogDataCacheRemoveSuccess, @"No need to remove entry keyed %@ from non-existent cache \"%@\".", inKey, inCacheKey);
241 }
242}
243
244
245- (void)clearCache:(NSString *)inCacheKey
246{
247 NSParameterAssert(inCacheKey != nil);
248
249 if (nil != [_caches objectForKey:inCacheKey])
250 {
251 [_caches removeObjectForKey:inCacheKey];
252 _dirty = YES;
253 OODebugLog(kOOLogDataCacheClearSuccess, @"Cleared cache \"%@\".", inCacheKey);
254 }
255 else
256 {
257 OODebugLog(kOOLogDataCacheClearSuccess, @"No need to clear non-existent cache \"%@\".", inCacheKey);
258 }
259}
260
261
262- (void)clearAllCaches
263{
264 [self clear];
265 _caches = [[NSMutableDictionary alloc] init];
266 _dirty = YES;
267}
268
269
270- (void) reloadAllCaches
271{
272 [self clear];
273 [self loadCache];
274}
275
276
277- (void)flush
278{
279 if (_permitWrites && [self dirty] && _scheduledWrite == nil)
280 {
281 [self write];
282 [self markClean];
283 }
284}
285
286
287- (void)finishOngoingFlush
288{
289#if WRITE_ASYNC
291#endif
292}
293
294
295- (void)setAllowCacheWrites:(BOOL)flag
296{
297 _permitWrites = (flag != NO);
298}
299
300
301- (NSString *)cacheDirectoryPathCreatingIfNecessary:(BOOL)create
302{
303 /* Construct the path to the directory for cache files, which is:
304 ~/Library/Caches/org.aegidian.oolite/
305 or
306 ~/GNUStep/Library/Caches/org.aegidian.oolite/
307 In addition to generally being the right place to put caches,
308 ~/Library/Caches has the particular advantage of not being indexed by
309 Spotlight or backed up by Time Machine.
310 */
311 NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0];
312 if (![self directoryExists:cachePath create:create]) return nil;
313
314#if !OOLITE_MAC_OS_X
315 // the old cache file on GNUstep was one level up, so remove it if it exists
316 [[NSFileManager defaultManager] removeFileAtPath:[cachePath stringByAppendingPathComponent:@"Oolite-cache.plist"] handler:nil];
317#endif
318
319 cachePath = [cachePath stringByAppendingPathComponent:@"org.aegidian.oolite"];
320 if (![self directoryExists:cachePath create:create]) return nil;
321 return cachePath;
322}
323
324@end
325
326
327@implementation OOCacheManager (Private)
328
329- (void)loadCache
330{
331 NSDictionary *cache = nil;
332 NSString *cacheVersion = nil;
333 NSString *ooliteVersion = nil;
334 NSData *endianTag = nil;
335 NSNumber *formatVersion = nil;
336 BOOL accept = YES;
337 uint64_t endianTagValue = 0;
338
339 ooliteVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
340
341 [self clear];
342
343 cache = [self loadDict];
344 if (cache != nil)
345 {
346 // We have a cache
347 OOLog(kOOLogDataCacheFound, @"%@", @"Found data cache.");
349
350 cacheVersion = [cache objectForKey:kCacheKeyVersion];
351 if (![cacheVersion isEqual:ooliteVersion])
352 {
353 OOLog(kOOLogDataCacheRebuild, @"Data cache version (%@) does not match Oolite version (%@), rebuilding cache.", cacheVersion, ooliteVersion);
354 accept = NO;
355 }
356
357 formatVersion = [cache objectForKey:kCacheKeyFormatVersion];
358 if (accept && [formatVersion unsignedIntValue] != kFormatVersionValue)
359 {
360 OOLog(kOOLogDataCacheRebuild, @"Data cache format (%@) is not supported format (%u), rebuilding cache.", formatVersion, kFormatVersionValue);
361 accept = NO;
362 }
363
364 if (accept)
365 {
366 endianTag = [cache objectForKey:kCacheKeyEndianTag];
367 if (![endianTag isKindOfClass:[NSData class]] || [endianTag length] != sizeof endianTagValue)
368 {
369 OOLog(kOOLogDataCacheRebuild, @"%@", @"Data cache endian tag is invalid, rebuilding cache.");
370 accept = NO;
371 }
372 else
373 {
374 endianTagValue = *(const uint64_t *)[endianTag bytes];
375 if (endianTagValue != kEndianTagValue)
376 {
377 OOLog(kOOLogDataCacheRebuild, @"%@", @"Data cache endianness is inappropriate for this system, rebuilding cache.");
378 accept = NO;
379 }
380 }
381 }
382
383 if (accept)
384 {
385 // We have a cache, and it's the right format.
386 [self buildCachesFromDictionary:[cache objectForKey:kCacheKeyCaches]];
387 }
388
390 }
391 else
392 {
393 // No cache
394 OOLog(kOOLogDataCacheNotFound, @"%@", @"No data cache found, starting from scratch.");
395 }
396
397 // If loading failed, or there was a version or endianness conflict
398 if (_caches == nil) _caches = [[NSMutableDictionary alloc] init];
399 [self markClean];
400}
401
402
403- (void)write
404{
405 NSMutableDictionary *newCache = nil;
406 NSString *ooliteVersion = nil;
407 NSData *endianTag = nil;
408 NSNumber *formatVersion = nil;
409 NSDictionary *pListRep = nil;
410 uint64_t endianTagValue = kEndianTagValue;
411
412 if (_caches == nil) return;
413 if (_scheduledWrite != nil) return;
414
415#if PROFILE_WRITES
417#endif
418
419#if WRITE_ASYNC
420 OOLog(@"dataCache.willWrite", @"%@", @"Scheduling data cache write.");
421#else
422 OOLog(@"dataCache.willWrite", @"%@", @"About to write cache.");
423#endif
424
425 ooliteVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
426 endianTag = [NSData dataWithBytes:&endianTagValue length:sizeof endianTagValue];
427 formatVersion = [NSNumber numberWithUnsignedInt:kFormatVersionValue];
428
429 pListRep = [self dictionaryOfCaches];
430 if (ooliteVersion == nil || endianTag == nil || formatVersion == nil || pListRep == nil)
431 {
432 OOLog(@"dataCache.cantWrite", @"%@", @"Failed to write data cache -- prerequisites not fulfilled. This is an internal error, please report it.");
433 return;
434 }
435
436 newCache = [NSMutableDictionary dictionaryWithCapacity:4];
437 [newCache setObject:ooliteVersion forKey:kCacheKeyVersion];
438 [newCache setObject:formatVersion forKey:kCacheKeyFormatVersion];
439 [newCache setObject:endianTag forKey:kCacheKeyEndianTag];
440 [newCache setObject:pListRep forKey:kCacheKeyCaches];
441
442#if PROFILE_WRITES && !WRITE_ASYNC
443 OOTimeDelta prepareT = [stopwatch reset];
444#endif
445
446#if WRITE_ASYNC
447 NSDictionary *cacheData = newCache;
448 _scheduledWrite = [[OOAsyncCacheWriter alloc] initWithCacheContents:cacheData];
449
450#if PROFILE_WRITES
451 OOTimeDelta endT = [stopwatch reset];
452 OOLog(@"dataCache.profile", @"Time to prepare cache data: %g seconds.", endT);
453#endif
454
455 [[OOAsyncWorkManager sharedAsyncWorkManager] addTask:_scheduledWrite priority:kOOAsyncPriorityLow];
456#else
457#if PROFILE_WRITES
458 OOLog(@"dataCache.profile", @"Time to prepare cache data: %g seconds.", prepareT);
459#endif
460
461 if ([self writeDict:newCache])
462 {
463 [self markClean];
464 OOLog(kOOLogDataCacheWriteSuccess, @"%@", @"Wrote data cache.");
465 }
466 else
467 {
468 OOLog(kOOLogDataCacheWriteFailed, @"%@", @"Failed to write data cache.");
469 }
470#endif
471}
472
473
474- (void)clear
475{
476 [_caches release];
477 _caches = nil;
478}
479
480
481- (BOOL)dirty
482{
483 return _dirty;
484}
485
486
487- (void)markClean
488{
489 _dirty = NO;
490}
491
492
493- (NSDictionary *)loadDict
494{
495 NSString *path = nil;
496 NSData *data = nil;
497 NSString *errorString = nil;
498 id contents = nil;
499
500 path = [self cachePathCreatingIfNecessary:NO];
501 if (path == nil) return nil;
502
503 @try
504 {
505 data = [NSData dataWithContentsOfFile:path];
506 if (data == nil) return nil;
507
508 contents = [NSPropertyListSerialization propertyListFromData:data
509 mutabilityOption:NSPropertyListImmutable
510 format:NULL
511 errorDescription:&errorString];
512 }
513 @catch (NSException *exception)
514 {
515 errorString = [exception reason];
516 contents = nil;
517 }
518
519 if (errorString != nil)
520 {
521 OOLog(@"dataCache.badData", @"Could not read data cache: %@", errorString);
522#if OOLITE_RELEASE_PLIST_ERROR_STRINGS
523 [errorString release];
524#endif
525 return nil;
526 }
527 if (![contents isKindOfClass:[NSDictionary class]]) return nil;
528
529 return contents;
530}
531
532
533- (BOOL)writeDict:(NSDictionary *)inDict
534{
535 NSString *path = nil;
536 NSData *plist = nil;
537 NSString *errorDesc = nil;
538
539 path = [self cachePathCreatingIfNecessary:YES];
540 if (path == nil) return NO;
541
542#if PROFILE_WRITES
544#endif
545
546 plist = [NSPropertyListSerialization dataFromPropertyList:inDict format:CACHE_PLIST_FORMAT errorDescription:&errorDesc];
547 if (plist == nil)
548 {
549#if OOLITE_RELEASE_PLIST_ERROR_STRINGS
550 [errorDesc autorelease];
551#endif
552 OOLog(kOOLogDataCacheSerializationError, @"Could not convert data cache to property list data: %@", errorDesc);
553 return NO;
554 }
555
556#if PROFILE_WRITES
557 OOTimeDelta serializeT = [stopwatch reset];
558#endif
559
560 BOOL result = [plist writeToFile:path atomically:NO];
561
562#if PROFILE_WRITES
563 OOTimeDelta writeT = [stopwatch reset];
564
565 OOLog(@"dataCache.profile", @"Time to serialize cache: %g seconds. Time to write data: %g seconds.", serializeT, writeT);
566#endif
567
568#if WRITE_ASYNC
569 DESTROY(_scheduledWrite);
570#endif
571 return result;
572}
573
574
575- (void)buildCachesFromDictionary:(NSDictionary *)inDict
576{
577 NSEnumerator *keyEnum = nil;
578 id key = nil;
579 id value = nil;
580 NSMutableDictionary *cache = nil;
581
582 if (inDict == nil ) return;
583
584 [_caches release];
585 _caches = [[NSMutableDictionary alloc] initWithCapacity:[inDict count]];
586
587 for (keyEnum = [inDict keyEnumerator]; (key = [keyEnum nextObject]); )
588 {
589 value = [inDict oo_dictionaryForKey:key];
590 if (value != nil)
591 {
592 cache = [NSMutableDictionary dictionaryWithDictionary:value];
593 if (cache != nil)
594 {
595 [_caches setObject:cache forKey:key];
596 }
597 }
598 }
599}
600
601
602- (NSDictionary *)dictionaryOfCaches
603{
604 return [OODeepCopy(_caches) autorelease];
605}
606
607
608- (BOOL)directoryExists:(NSString *)inPath create:(BOOL)inCreate
609{
610 BOOL exists, directory;
611 NSFileManager *fmgr = [NSFileManager defaultManager];
612
613 exists = [fmgr fileExistsAtPath:inPath isDirectory:&directory];
614
615 if (exists && !directory)
616 {
617 OOLog(kOOLogDataCacheBuildPathError, @"Expected %@ to be a folder, but it is a file.", inPath);
618 return NO;
619 }
620 if (!exists)
621 {
622 if (!inCreate) return NO;
623 if (![fmgr oo_createDirectoryAtPath:inPath attributes:nil])
624 {
625 OOLog(kOOLogDataCacheBuildPathError, @"Could not create folder %@.", inPath);
626 return NO;
627 }
628 }
629
630 return YES;
631}
632
633
634#if OOLITE_MAC_OS_X
635
636- (NSString *)cachePathCreatingIfNecessary:(BOOL)create
637{
638 NSString *cachePath = [self cacheDirectoryPathCreatingIfNecessary:create];
639 return [cachePath stringByAppendingPathComponent:@"Data Cache.plist"];
640}
641
642#else
643
644- (NSString *)cachePathCreatingIfNecessary:(BOOL)create
645{
646 NSString *cachePath = [self cacheDirectoryPathCreatingIfNecessary:create];
647 return [cachePath stringByAppendingPathComponent:@"Oolite-cache.plist"];
648}
649
650#endif
651
652@end
653
654
655@implementation OOCacheManager (Singleton)
657/* Canonical singleton boilerplate.
658 See Cocoa Fundamentals Guide: Creating a Singleton Instance.
659 See also +sharedCache above.
660
661 NOTE: assumes single-threaded access.
662*/
663
664+ (id)allocWithZone:(NSZone *)inZone
665{
666 if (sSingleton == nil)
667 {
668 sSingleton = [super allocWithZone:inZone];
669 return sSingleton;
670 }
671 return nil;
672}
673
674
675- (id)copyWithZone:(NSZone *)inZone
676{
677 return self;
678}
679
680
681- (id)retain
682{
683 return self;
684}
685
686
687- (NSUInteger)retainCount
688{
689 return UINT_MAX;
690}
691
692
693- (void)release
694{}
695
696
697- (id)autorelease
698{
699 return self;
700}
701
702@end
703
704
705#if WRITE_ASYNC
706@implementation OOAsyncCacheWriter
707
708- (id) initWithCacheContents:(NSDictionary *)cacheContents
709{
710 if ((self = [super init]))
711 {
712 _cacheContents = [cacheContents copy];
713 if (_cacheContents == nil)
714 {
715 [self release];
716 self = nil;
717 }
718 }
719
720 return self;
721}
722
723
724- (void) dealloc
725{
726 DESTROY(_cacheContents);
727
728 [super dealloc];
729}
730
731
732- (void) performAsyncTask
733{
734 if ([[OOCacheManager sharedCache] writeDict:_cacheContents])
735 {
736 OOLog(kOOLogDataCacheWriteSuccess, @"%@", @"Wrote data cache.");
737 }
738 else
739 {
740 OOLog(kOOLogDataCacheWriteFailed, @"%@", @"Failed to write data cache.");
741 }
742 DESTROY(_cacheContents);
743}
744
745
746- (void) completeAsyncTask
747{
748 // Don't need to do anything, but this needs to be here so we can wait on it.
749}
750
751@end
752#endif // WRITE_ASYNC
static NSString *const kCacheKeyEndianTag
static NSString *const kOOLogDataCacheWriteSuccess
static NSString *const kOOLogDataCacheBuildPathError
static NSString *const kOOLogDataCacheRetrieveSuccess
static NSString *const kOOLogDataCacheClearSuccess
static NSString *const kOOLogDataCacheWriteFailed
static NSString *const kOOLogDataCacheRetrieveFailed
static NSString *const kOOLogDataCacheSetFailed
@ kFormatVersionValue
@ kEndianTagValue
static NSString *const kCacheKeyCaches
static OOCacheManager * sSingleton
static NSString *const kOOLogDataCacheRebuild
static NSString *const kOOLogDataCacheSerializationError
static NSString *const kCacheKeyVersion
static NSString *const kOOLogDataCacheRemoveSuccess
static NSString *const kOOLogDataCacheNotFound
static NSString *const kCacheKeyFormatVersion
static NSString *const kOOLogDataCacheSetSuccess
static NSString *const kOOLogDataCacheFound
#define DESTROY(x)
Definition OOCocoa.h:77
static OODebugMonitor * sSingleton
#define EXPECT_NOT(x)
#define OODebugLog(class, format,...)
Definition OOLogging.h:146
#define OOLogOutdentIf(class)
Definition OOLogging.h:102
#define OOLog(class, format,...)
Definition OOLogging.h:88
#define OOLogIndentIf(class)
Definition OOLogging.h:101
return nil
double OOTimeDelta
Definition OOTypes.h:224
NSDictionary * dictionaryOfCaches()
BOOL addTask:priority:(id< OOAsyncWorkTask > task,[priority] OOAsyncWorkPriority priority)
void waitForTaskToComplete:(id< OOAsyncWorkTask > task)
OOAsyncWorkManager * sharedAsyncWorkManager()