Skip to content

URLCache sketches. Mainly memory cache. #2312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 275 additions & 11 deletions Foundation/URLCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ open class CachedURLResponse : NSObject, NSSecureCoding, NSCopying {
open class URLCache : NSObject {

/*!
private static let _sharedCacheLock: NSLock = NSLock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was moving commits between branches. The documentation comment should go below the two private variables. I didn’t review the code before pushing, sorry about that.

private static var _sharedCache: URLCache!

@method sharedURLCache
@abstract Returns the shared URLCache instance.
@discussion Unless set explicitly, this method returns an URLCache
Expand All @@ -147,14 +150,27 @@ open class URLCache : NSObject {
*/
open class var shared: URLCache {
get {
NSUnimplemented()
_sharedCacheLock.lock()
defer { _sharedCacheLock.unlock() }
if _sharedCache == nil {
// TODO: diskPath?
_sharedCache = URLCache(memoryCapacity: 4 * 1024 * 1024, diskCapacity: 20 * 1024 * 1024, diskPath: nil)
}

return _sharedCache
}
set {
NSUnimplemented()
_sharedCacheLock.lock()
defer { _sharedCacheLock.unlock() }
_sharedCache = newValue
}
}

/*!
private let _memoryCache: _URLMemoryCache<_URLCacheKey, CachedURLResponse>
private let _diskPath: String
private let _diskCache: _URLDiskCache<_URLCacheKey, CachedURLResponse>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also happens here. The variables should be outside the comment, not inside.


@method initWithMemoryCapacity:diskCapacity:diskPath:
@abstract Initializes an URLCache with the given capacity and
path.
Expand All @@ -167,7 +183,11 @@ open class URLCache : NSObject {
@result an initialized URLCache, with the given capacity, backed
by disk.
*/
public init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String?) { NSUnimplemented() }
public init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String?) {
self._diskPath = path ?? "/" // TODO?
self._memoryCache = _URLMemoryCache(capacity: memoryCapacity)
self._diskCache = _URLDiskCache(capacity: diskCapacity, path: self._diskPath)
}

/*!
@method cachedResponseForRequest:
Expand All @@ -180,7 +200,13 @@ open class URLCache : NSObject {
request, or nil if there is no NSCachedURLResponse stored with the
given request.
*/
open func cachedResponse(for request: URLRequest) -> CachedURLResponse? { NSUnimplemented() }
open func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
guard let key = _URLCacheKey(request: request) else {
return nil
}

return _memoryCache[key] ?? _diskCache[key]
}

/*!
@method storeCachedResponse:forRequest:
Expand All @@ -189,7 +215,25 @@ open class URLCache : NSObject {
@param cachedResponse The cached response to store.
@param request the NSURLRequest to use as a key for the storage.
*/
open func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) { NSUnimplemented() }
open func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) {
guard cachedResponse.storagePolicy != .notAllowed else {
// Don't store. But don't remove - may be cache reuse case.
return
}

guard let key = _URLCacheKey(request: request) else {
return
}

_memoryCache[key] = cachedResponse
if cachedResponse.storagePolicy != .allowedInMemoryOnly {
_diskCache[key] = cachedResponse
} else {
// If we are not supposed to write to disk, ensure any existing
// entry is removed.
_diskCache[key] = nil
}
}

/*!
@method removeCachedResponseForRequest:
Expand All @@ -199,14 +243,24 @@ open class URLCache : NSObject {
stored with the given request.
@param request the NSURLRequest to use as a key for the lookup.
*/
open func removeCachedResponse(for request: URLRequest) { NSUnimplemented() }
open func removeCachedResponse(for request: URLRequest) {
guard let key = _URLCacheKey(request: request) else {
return
}

_memoryCache[key] = nil
_diskCache[key] = nil
}

/*!
@method removeAllCachedResponses
@abstract Clears the given cache, removing all NSCachedURLResponse
objects that it stores.
*/
open func removeAllCachedResponses() { NSUnimplemented() }
open func removeAllCachedResponses() {
_memoryCache.removeAll()
_diskCache.removeAll()
}

/*!
@method removeCachedResponsesSince:
Expand All @@ -220,15 +274,21 @@ open class URLCache : NSObject {
@discussion At the time this call is made, the in-memory cache will truncate its contents to the size given, if necessary.
@result The in-memory capacity, measured in bytes, for the receiver.
*/
open var memoryCapacity: Int
open var memoryCapacity: Int {
get { return _memoryCache.capacity }
set { _memoryCache.capacity = newValue }
}

/*!
@method diskCapacity
@abstract The on-disk capacity of the receiver.
@discussion At the time this call is made, the on-disk cache will truncate its contents to the size given, if necessary.
@param diskCapacity the new on-disk capacity, measured in bytes, for the receiver.
*/
open var diskCapacity: Int
open var diskCapacity: Int {
get { return _diskCache.capacity }
set { _diskCache.capacity = newValue }
}

/*!
@method currentMemoryUsage
Expand All @@ -238,7 +298,9 @@ open class URLCache : NSObject {
usage of the in-memory cache.
@result the current usage of the in-memory cache of the receiver.
*/
open var currentMemoryUsage: Int { NSUnimplemented() }
open var currentMemoryUsage: Int {
return _memoryCache.currentCost
}

/*!
@method currentDiskUsage
Expand All @@ -248,11 +310,213 @@ open class URLCache : NSObject {
usage of the on-disk cache.
@result the current usage of the on-disk cache of the receiver.
*/
open var currentDiskUsage: Int { NSUnimplemented() }
open var currentDiskUsage: Int {
return _diskCache.currentCost
}
}

extension URLCache {
public func storeCachedResponse(_ cachedResponse: CachedURLResponse, for dataTask: URLSessionDataTask) { NSUnimplemented() }
public func getCachedResponse(for dataTask: URLSessionDataTask, completionHandler: (CachedURLResponse?) -> Void) { NSUnimplemented() }
public func removeCachedResponse(for dataTask: URLSessionDataTask) { NSUnimplemented() }
}


private struct _URLCacheKey: Hashable {
private let value: Int

init?(request: URLRequest) {
// TODO: find out the protocol, find out if there's a handler, find out
// if it is cacheable (data, file, ... are not)


var hasher = Hasher()
hasher.combine(request) // TODO: we should not hash everything
value = hasher.finalize()
}

func hash(into hasher: inout Hasher) {
hasher.combine(value)
}
}


private protocol _URLCacheEntry {
var cost: Int { get }
}


extension CachedURLResponse: _URLCacheEntry {
var cost: Int {
return data.count
}
}


private class _URLMemoryCache<Key: Hashable, Value: _URLCacheEntry> {
private class Entry<Value> {
let key: Key
let value: Value
let cost: Int
var index: Array<Entry<Value>>.Index!
// var prev: Entry<Value>? = nil
// var next: Entry<Value>? = nil

init(key: Key, value: Value, cost: Int) {
self.key = key
self.value = value
self.cost = cost
}
}

private var _entries: [Key: Entry<Value>]
private var _lruList: [Entry<Value>]
private let _lock: NSLock

init(capacity: Int) {
self._capacity = capacity
self._entries = [:]
self._lruList = []
self._currentCost = 0
self._lock = NSLock()
}

private var _capacity: Int
var capacity: Int {
get {
_lock.lock()
defer { _lock.unlock() }

return _capacity
}
set {
_lock.lock()
defer { _lock.unlock() }

_capacity = newValue
_performGarbageCollectionWhileLocked()
}
}

private var _currentCost: Int
var currentCost: Int {
get {
_lock.lock()
defer { _lock.unlock() }

return _currentCost
}
}

subscript(_ key: Key) -> Value? {
get {
_lock.lock()
defer { _lock.unlock() }

let entry = _findEntryWhileLocked(key)
return entry?.value;
}
set {
// TODO
// Check if the size is OK for the memory cache, otherwise remove

_lock.lock()
defer { _lock.unlock() }

if let existing = _findEntryWhileLocked(key) {
_removeWhileLocked(existing)
}

guard let newValue = newValue else {
return
}

let entry = Entry(key: key, value: newValue, cost: 1) // TODO: cost
_entries[key] = entry
entry.index = _lruList.endIndex
_lruList.insert(entry, at: entry.index)
_currentCost += 1 // TODO: cost
_performGarbageCollectionWhileLocked()
}
}

func removeAll() {
_lock.lock()
defer { _lock.unlock() }

while let entry = _lruList.first {
_removeWhileLocked(entry)
}
}

private func _performGarbageCollectionWhileLocked() {
while _currentCost > _capacity, let entry = _lruList.first {
_removeWhileLocked(entry)
}
}

private func _removeWhileLocked(_ entry: Entry<Value>) {
_entries[entry.key] = nil
_lruList.remove(at: entry.index)
_currentCost -= entry.cost
}

private func _findEntryWhileLocked(_ key: Key) -> Entry<Value>? {
guard let entry = _entries[key] else {
return nil
}

_lruList.remove(at: entry.index)
entry.index = _lruList.endIndex
_lruList.insert(entry, at: entry.index)

return entry
}
}


private class _URLDiskCache<Key: Hashable, Value: NSCoding> {
private let _path: String

init(capacity: Int, path: String) {
self._capacity = capacity
self._path = path
self._currentCost = 0 // TODO: read from disk

// TODO: run capacity setter code?
}

private var _capacity: Int
var capacity: Int {
get {
// TODO
return _capacity
}
set {
// TODO
_capacity = newValue
}
}

private var _currentCost: Int
var currentCost: Int {
get {
// TODO
return _currentCost
}
}

subscript(_ key: Key) -> Value? {
get {
// TODO
return nil
}
set {
// TODO
}
}

func removeAll() {
// TODO
}
}