Skip to content

Instantly share code, notes, and snippets.

@Adlai-Holler
Created May 19, 2018 19:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Adlai-Holler/f1e71e94c9fefc27ed87d2b309e98f98 to your computer and use it in GitHub Desktop.
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.
//
// 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