Skip to content

Commit

Permalink
Add Least Frequently Used eviction strategy (#328)
Browse files Browse the repository at this point in the history
* Add Least Frequently Used eviction strategy

## Summary

Currently PINCache only offers LRU (least recently used) as an eviction strategy. However, there are some special workloads where LFU (least frequently used) could offer better performance. This PR introduces LFU alongside the existing LRU eviction strategy. The default is still LRU.

There is also some minor renaming to the `trimToSizeByDateAsync`, `trimToSizeByDate`, `trimToCostByDate`, and `trimToCostByDateAsync` methods, since those now follow the explicit eviction strategy. Old methods remain and work as expected, but are marked deprecated.

## Testing

Added some unit tests for both memory and disk caches to verify objects are evicted based on access count when LFU is selected. Ran tests on iOS, tvOS, macOS.

* Add deprecated messages to clarify what should be used instead

* DRY up a couple of constants

* Remove doc for parameter that doesn't exist

* Nudge github actions
  • Loading branch information
andyfinnell authored May 10, 2024
1 parent 9f3977e commit 803c069
Show file tree
Hide file tree
Showing 8 changed files with 443 additions and 44 deletions.
28 changes: 27 additions & 1 deletion Source/PINCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,33 @@ PIN_SUBCLASSING_RESTRICTED
deserializer:(nullable PINDiskCacheDeserializerBlock)deserializer
keyEncoder:(nullable PINDiskCacheKeyEncoderBlock)keyEncoder
keyDecoder:(nullable PINDiskCacheKeyDecoderBlock)keyDecoder
ttlCache:(BOOL)ttlCache NS_DESIGNATED_INITIALIZER;
ttlCache:(BOOL)ttlCache;

/**
Multiple instances with the same name are *not* allowed and can *not* safely
access the same data on disk. Also used to create the <diskCache>.
Initializer allows you to override default NSKeyedArchiver/NSKeyedUnarchiver serialization for <diskCache>.
You must provide both serializer and deserializer, or opt-out to default implementation providing nil values.
@see name
@param name The name of the cache.
@param rootPath The path of the cache on disk.
@param serializer A block used to serialize object before writing to disk. If nil provided, default NSKeyedArchiver serialized will be used.
@param deserializer A block used to deserialize object read from disk. If nil provided, default NSKeyedUnarchiver serialized will be used.
@param keyEncoder A block used to encode key(filename). If nil provided, default url encoder will be used
@param keyDecoder A block used to decode key(filename). If nil provided, default url decoder will be used
@param ttlCache Whether or not the cache should behave as a TTL cache.
@param evictionStrategy How the cache decide to evict objects when over cost.
@result A new cache with the specified name.
*/
- (instancetype)initWithName:(nonnull NSString *)name
rootPath:(nonnull NSString *)rootPath
serializer:(nullable PINDiskCacheSerializerBlock)serializer
deserializer:(nullable PINDiskCacheDeserializerBlock)deserializer
keyEncoder:(nullable PINDiskCacheKeyEncoderBlock)keyEncoder
keyDecoder:(nullable PINDiskCacheKeyDecoderBlock)keyDecoder
ttlCache:(BOOL)ttlCache
evictionStrategy:(PINCacheEvictionStrategy)evictionStrategy NS_DESIGNATED_INITIALIZER;

@end

Expand Down
19 changes: 17 additions & 2 deletions Source/PINCache.m
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,25 @@ - (instancetype)initWithName:(NSString *)name
return [self initWithName:name rootPath:rootPath serializer:serializer deserializer:deserializer keyEncoder:keyEncoder keyDecoder:keyDecoder ttlCache:NO];
}

- (instancetype)initWithName:(nonnull NSString *)name
rootPath:(nonnull NSString *)rootPath
serializer:(nullable PINDiskCacheSerializerBlock)serializer
deserializer:(nullable PINDiskCacheDeserializerBlock)deserializer
keyEncoder:(nullable PINDiskCacheKeyEncoderBlock)keyEncoder
keyDecoder:(nullable PINDiskCacheKeyDecoderBlock)keyDecoder
ttlCache:(BOOL)ttlCache
{
return [self initWithName:name rootPath:rootPath serializer:serializer deserializer:deserializer keyEncoder:keyEncoder keyDecoder:keyDecoder ttlCache:ttlCache evictionStrategy:PINCacheEvictionStrategyLeastRecentlyUsed];
}

- (instancetype)initWithName:(NSString *)name
rootPath:(NSString *)rootPath
serializer:(PINDiskCacheSerializerBlock)serializer
deserializer:(PINDiskCacheDeserializerBlock)deserializer
keyEncoder:(PINDiskCacheKeyEncoderBlock)keyEncoder
keyDecoder:(PINDiskCacheKeyDecoderBlock)keyDecoder
ttlCache:(BOOL)ttlCache
evictionStrategy:(PINCacheEvictionStrategy)evictionStrategy
{
if (!name)
return nil;
Expand All @@ -72,8 +84,11 @@ - (instancetype)initWithName:(NSString *)name
keyEncoder:keyEncoder
keyDecoder:keyDecoder
operationQueue:_operationQueue
ttlCache:ttlCache];
_memoryCache = [[PINMemoryCache alloc] initWithName:_name operationQueue:_operationQueue ttlCache:ttlCache];
ttlCache:ttlCache
byteLimit:PINDiskCacheDefaultByteLimit
ageLimit:PINDiskCacheDefaultAgeLimit
evictionStrategy:evictionStrategy];
_memoryCache = [[PINMemoryCache alloc] initWithName:_name operationQueue:_operationQueue ttlCache:ttlCache evictionStrategy:evictionStrategy];
}
return self;
}
Expand Down
5 changes: 5 additions & 0 deletions Source/PINCaching.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ typedef void (^PINCacheObjectEnumerationBlock)(__kindof id<PINCaching> cache, NS
*/
typedef void (^PINCacheObjectContainmentBlock)(BOOL containsObject);

typedef NS_ENUM(NSInteger, PINCacheEvictionStrategy) {
PINCacheEvictionStrategyLeastRecentlyUsed,
PINCacheEvictionStrategyLeastFrequentlyUsed,
};

@protocol PINCaching <NSObject>

#pragma mark - Core
Expand Down
51 changes: 45 additions & 6 deletions Source/PINDiskCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ extern NSErrorUserInfoKey const PINDiskCacheErrorReadFailureCodeKey;
extern NSErrorUserInfoKey const PINDiskCacheErrorWriteFailureCodeKey;
extern NSString * const PINDiskCachePrefix;

extern NSUInteger PINDiskCacheDefaultByteLimit;
extern NSTimeInterval PINDiskCacheDefaultAgeLimit;

typedef NS_ENUM(NSInteger, PINDiskCacheError) {
PINDiskCacheErrorReadFailure = -1000,
PINDiskCacheErrorWriteFailure = -1001,
Expand Down Expand Up @@ -168,6 +171,11 @@ PIN_SUBCLASSING_RESTRICTED
*/
@property (assign) NSTimeInterval ageLimit;

/**
The eviction strategy when trimming the cache.
*/
@property (atomic, assign) PINCacheEvictionStrategy evictionStrategy;

/**
The writing protection option used when writing a file on disk. This value is used every time an object is set.
NSDataWritingAtomic and NSDataWritingWithoutOverwriting are ignored if set
Expand Down Expand Up @@ -336,6 +344,33 @@ PIN_SUBCLASSING_RESTRICTED
operationQueue:(nonnull PINOperationQueue *)operationQueue
ttlCache:(BOOL)ttlCache;

/**
@see name
@param name The name of the cache.
@param prefix The prefix for the cache name. Defaults to com.pinterest.PINDiskCache
@param rootPath The path of the cache.
@param serializer A block used to serialize object. If nil provided, default NSKeyedArchiver serialized will be used.
@param deserializer A block used to deserialize object. If nil provided, default NSKeyedUnarchiver serialized will be used.
@param keyEncoder A block used to encode key(filename). If nil provided, default url encoder will be used
@param keyDecoder A block used to decode key(filename). If nil provided, default url decoder will be used
@param operationQueue A PINOperationQueue to run asynchronous operations
@param ttlCache Whether or not the cache should behave as a TTL cache.
@param byteLimit The maximum number of bytes allowed on disk. Defaults to 50MB.
@param ageLimit The maximum number of seconds an object is allowed to exist in the cache. Defaults to 30 days.
@result A new cache with the specified name.
*/
- (instancetype)initWithName:(nonnull NSString *)name
prefix:(nonnull NSString *)prefix
rootPath:(nonnull NSString *)rootPath
serializer:(nullable PINDiskCacheSerializerBlock)serializer
deserializer:(nullable PINDiskCacheDeserializerBlock)deserializer
keyEncoder:(nullable PINDiskCacheKeyEncoderBlock)keyEncoder
keyDecoder:(nullable PINDiskCacheKeyDecoderBlock)keyDecoder
operationQueue:(nonnull PINOperationQueue *)operationQueue
ttlCache:(BOOL)ttlCache
byteLimit:(NSUInteger)byteLimit
ageLimit:(NSTimeInterval)ageLimit;

/**
The designated initializer allowing you to override default NSKeyedArchiver/NSKeyedUnarchiver serialization.
Expand All @@ -351,6 +386,7 @@ PIN_SUBCLASSING_RESTRICTED
@param ttlCache Whether or not the cache should behave as a TTL cache.
@param byteLimit The maximum number of bytes allowed on disk. Defaults to 50MB.
@param ageLimit The maximum number of seconds an object is allowed to exist in the cache. Defaults to 30 days.
@param evictionStrategy How the cache decides to evict objects
@result A new cache with the specified name.
*/
- (instancetype)initWithName:(nonnull NSString *)name
Expand All @@ -363,7 +399,8 @@ PIN_SUBCLASSING_RESTRICTED
operationQueue:(nonnull PINOperationQueue *)operationQueue
ttlCache:(BOOL)ttlCache
byteLimit:(NSUInteger)byteLimit
ageLimit:(NSTimeInterval)ageLimit NS_DESIGNATED_INITIALIZER;
ageLimit:(NSTimeInterval)ageLimit
evictionStrategy:(PINCacheEvictionStrategy)evictionStrategy NS_DESIGNATED_INITIALIZER;

#pragma mark - Asynchronous Methods
/// @name Asynchronous Methods
Expand Down Expand Up @@ -471,7 +508,7 @@ PIN_SUBCLASSING_RESTRICTED
- (void)trimToSizeAsync:(NSUInteger)byteCount completion:(nullable PINCacheBlock)block;

/**
Removes objects from the cache, ordered by date (least recently used first), until the cache is equal to or smaller
Removes objects from the cache, using the eviction strategy, until the cache is equal to or smaller
than the specified byteCount. This method returns immediately and executes the passed block as soon as the cache has
been trimmed.
Expand All @@ -480,7 +517,7 @@ PIN_SUBCLASSING_RESTRICTED
@note This will not remove objects that have been added via one of the @c -setObject:forKey:withAgeLimit methods.
*/
- (void)trimToSizeByDateAsync:(NSUInteger)byteCount completion:(nullable PINCacheBlock)block;
- (void)trimToSizeByEvictionStrategyAsync:(NSUInteger)byteCount completion:(nullable PINCacheBlock)block;

/**
Loops through all objects in the cache (reads and writes are suspended during the enumeration). Data is not actually
Expand Down Expand Up @@ -564,15 +601,15 @@ PIN_SUBCLASSING_RESTRICTED
- (void)trimToSize:(NSUInteger)byteCount;

/**
Removes objects from the cache, ordered by date (least recently used first), until the cache is equal to or
Removes objects from the cache, using the defined evictionStrategy, until the cache is equal to or
smaller than the specified byteCount. This method blocks the calling thread until the cache has been trimmed.
@see trimToSizeByDateAsync:
@see trimToSizeByEvictionStrategyAsync:
@param byteCount The cache will be trimmed equal to or smaller than this size.
@note This will not remove objects that have been added via one of the @c -setObject:forKey:withAgeLimit methods.
*/
- (void)trimToSizeByDate:(NSUInteger)byteCount;
- (void)trimToSizeByEvictionStrategy:(NSUInteger)byteCount;

/**
Loops through all objects in the cache (reads and writes are suspended during the enumeration). Data is not actually
Expand Down Expand Up @@ -616,6 +653,8 @@ typedef void (^PINDiskCacheBlock)(PINDiskCache *cache);
- (void)removeAllObjects:(nullable PINDiskCacheBlock)block __attribute__((deprecated));
- (void)enumerateObjectsWithBlock:(PINDiskCacheFileURLBlock)block completionBlock:(nullable PINDiskCacheBlock)completionBlock __attribute__((deprecated));
- (void)setTtlCache:(BOOL)ttlCache DEPRECATED_MSG_ATTRIBUTE("ttlCache is no longer a settable property and must now be set via initializer.");
- (void)trimToSizeByDate:(NSUInteger)byteCount DEPRECATED_MSG_ATTRIBUTE("Use trimToSizeByEvictionStrategy: instead");
- (void)trimToSizeByDateAsync:(NSUInteger)byteCount completion:(nullable PINCacheBlock)block DEPRECATED_MSG_ATTRIBUTE("Use trimToSizeByEvictionStrategyAsync:completion: instead");
@end

NS_ASSUME_NONNULL_END
Loading

0 comments on commit 803c069

Please sign in to comment.