Created
May 19, 2018 19:37
-
-
Save Adlai-Holler/f1e71e94c9fefc27ed87d2b309e98f98 to your computer and use it in GitHub Desktop.
A cache that coalesces operations for the same key. We currently don't do enough concurrent duplicate requests for this to be useful but I think it's neat.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ASCache.m | |
// Texture | |
// | |
// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
#import <AsyncDisplayKit/ASCache.h> | |
#import <AsyncDisplayKit/ASInternalHelpers.h> | |
#import <AsyncDisplayKit/ASLog.h> | |
#import <AsyncDisplayKit/ASThread.h> | |
@interface ASCache () <NSLocking> | |
@end | |
typedef struct { | |
BOOL running; | |
int waiterCount; | |
BOOL condCreated; | |
pthread_cond_t cond; | |
NSUInteger hash; | |
CFTypeRef key; | |
CFTypeRef value; | |
} ASCacheOperation; | |
static NSUInteger const kMaxOperationCount = 32; | |
@implementation ASCache { | |
pthread_mutex_t _lock; | |
ASCacheOperation _ops[kMaxOperationCount]; | |
#if ASCachingLogEnabled | |
NSUInteger _hitCount; | |
NSUInteger _missCount; | |
#endif | |
} | |
- (instancetype)init | |
{ | |
if (self = [super init]) { | |
pthread_mutex_init(&_lock, NULL); | |
} | |
return self; | |
} | |
- (void)dealloc | |
{ | |
for (int i = 0; i < kMaxOperationCount; i++) { | |
if (_ops[i].condCreated) { | |
pthread_cond_destroy(&_ops[i].cond); | |
} | |
} | |
} | |
// If cache logging is on, override this method and track stats. | |
#if ASCachingLogEnabled | |
- (id)objectForKey:(id)key | |
{ | |
id result = [super objectForKey:key]; | |
{ | |
ASLockScopeSelf(); | |
if (result) { | |
_hitCount += 1; | |
} else { | |
_missCount += 1; | |
} | |
NSUInteger totalReads = _hitCount + _missCount; | |
if (totalReads % 10 == 0) { | |
as_log_info(ASCachingLog(), "%@ hit rate: %d/%d (%.2f%%)", self.name.length ? self.name : self.debugDescription, _hitCount, totalReads, 100.0 * (_hitCount / (double)totalReads)); | |
} | |
} | |
return result; | |
} | |
#endif | |
- (id)objectForKey:(id)key constructedWithBlock:(id (^)(id))block | |
{ | |
// First check without any locking. | |
id result = [self objectForKey:key]; | |
if (result) { | |
return result; | |
} | |
// Pick an operation based on the key. | |
NSUInteger keyHash = [key hash]; | |
ASCacheOperation *op = NULL; | |
{ | |
ASLockScopeSelf(); | |
ASCacheOperation *firstUnusedOp = NULL; | |
CFTypeRef cfKey = (__bridge CFTypeRef)key; | |
for (int i = 0; i < kMaxOperationCount; i++) { | |
op = &_ops[i]; | |
if (!op->running) { | |
if (!firstUnusedOp) { | |
firstUnusedOp = op; | |
} | |
} else if (op->hash == keyHash && CFEqual(cfKey, op->key)) { | |
// Already inflight. Just attach to that. | |
op->waiterCount++; | |
CFTypeRef cfValue = NULL; | |
while (!(cfValue = op->value)) { | |
pthread_cond_wait(&op->cond, &_lock); | |
} | |
return (__bridge_transfer id)cfValue; | |
} | |
} | |
// If we got here, there's no inflight operation. | |
// Find a bucket and start one. | |
if (firstUnusedOp == NULL) { | |
NSAssert(NO, @"More than %lu in flight cache operations for the same cache is not supported. What is going on?", (unsigned long)kMaxOperationCount); | |
return nil; | |
} | |
op = firstUnusedOp; | |
if (!op->condCreated) { | |
op->condCreated = YES; | |
pthread_cond_init(&op->cond, NULL); | |
} | |
op->running = YES; | |
op->hash = keyHash; | |
op->key = cfKey; | |
} | |
// Do the work. | |
result = block(key); | |
// Async put the entry in the cache and then close the operation. | |
// NOTE: You could do this synchronously, but it's probably | |
// better to get the result returned as soon as possible. | |
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ | |
[self setObject:result forKey:key]; | |
ASLockScopeSelf(); | |
op->running = NO; | |
if (op->waiterCount > 0) { | |
// Retain the result once for each waiter. They will release. | |
for (int i = 0; i < op->waiterCount; i++) { | |
CFRetain((__bridge CFTypeRef)result); | |
} | |
op->waiterCount = 0; | |
op->value = (__bridge CFTypeRef)result; | |
pthread_cond_broadcast(&op->cond); | |
} | |
}); | |
return result; | |
} | |
- (void)lock | |
{ | |
pthread_mutex_lock(&_lock); | |
} | |
- (void)unlock | |
{ | |
pthread_mutex_unlock(&_lock); | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment