Skip to content

Commit 858b72c

Browse files
Updated Disk Persistence to use a cache instead of keeping all objects in memory indefinitely.
1 parent 97f8364 commit 858b72c

File tree

7 files changed

+122
-16
lines changed

7 files changed

+122
-16
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ targets: [
5353
As this project matures towards release, the project will focus on the functionality and work listed below:
5454
- Force migration methods
5555
- Composite indexes (via macros?)
56-
- Cleaning up old resources in memory
5756
- Cleaning up old resources on disk
5857
- Reversed ranged reads
5958
- Controls for the edit history
@@ -63,6 +62,7 @@ As this project matures towards release, the project will focus on the functiona
6362
- An example app
6463
- A memory persistence useful for testing apps with
6564
- A pre-configured data store tuned to storing pure Data, useful for types like Images
65+
- Cleaning up memory leaks
6666

6767
The above list will be kept up to date during development and will likely see additions during that process.
6868

Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndex.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ typealias DatastoreIndexIdentifier = TypedIdentifier<DiskPersistence<ReadOnly>.D
1313

1414
extension DiskPersistence.Datastore {
1515
actor Index: Identifiable {
16-
unowned let datastore: DiskPersistence<AccessMode>.Datastore
16+
let datastore: DiskPersistence<AccessMode>.Datastore
1717

1818
let id: ID
1919

@@ -34,6 +34,12 @@ extension DiskPersistence.Datastore {
3434
self._manifest = manifest
3535
self.isPersisted = manifest == nil
3636
}
37+
38+
deinit {
39+
Task { [id, datastore] in
40+
await datastore.invalidate(id)
41+
}
42+
}
3743
}
3844
}
3945

Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ typealias DatastorePageIdentifier = DatedIdentifier<DiskPersistence<ReadOnly>.Da
1414

1515
extension DiskPersistence.Datastore {
1616
actor Page: Identifiable {
17-
unowned let datastore: DiskPersistence<AccessMode>.Datastore
17+
let datastore: DiskPersistence<AccessMode>.Datastore
1818

1919
let id: ID
2020

@@ -36,6 +36,12 @@ extension DiskPersistence.Datastore {
3636
}
3737
self.isPersisted = blocks == nil
3838
}
39+
40+
deinit {
41+
Task { [id, datastore] in
42+
await datastore.invalidate(id)
43+
}
44+
}
3945
}
4046
}
4147

Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRoot.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ typealias DatastoreRootIdentifier = DatedIdentifier<DiskPersistence<ReadOnly>.Da
1212

1313
extension DiskPersistence.Datastore {
1414
actor RootObject: Identifiable {
15-
unowned let datastore: DiskPersistence<AccessMode>.Datastore
15+
let datastore: DiskPersistence<AccessMode>.Datastore
1616

1717
let id: DatastoreRootIdentifier
1818

@@ -30,6 +30,12 @@ extension DiskPersistence.Datastore {
3030
self._rootObject = rootObject
3131
self.isPersisted = rootObject == nil
3232
}
33+
34+
deinit {
35+
Task { [id, datastore] in
36+
await datastore.invalidate(id)
37+
}
38+
}
3339
}
3440
}
3541

Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/PersistenceDatastore.swift

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import Foundation
1010

1111
typealias DatastoreIdentifier = TypedIdentifier<DiskPersistence<ReadOnly>.Datastore>
1212

13+
struct WeakValue<T: AnyObject> {
14+
weak var value: T?
15+
16+
init(_ value: T) {
17+
self.value = value
18+
}
19+
}
20+
1321
extension DiskPersistence {
1422
actor Datastore {
1523
let id: DatastoreIdentifier
@@ -21,9 +29,9 @@ extension DiskPersistence {
2129
var lastUpdateDescriptorTask: Task<Any, Error>?
2230

2331
/// The root objects that are being tracked in memory.
24-
var trackedRootObjects: [RootObject.ID : RootObject] = [:]
25-
var trackedIndexes: [Index.ID : Index] = [:]
26-
var trackedPages: [Page.ID : Page] = [:]
32+
var trackedRootObjects: [RootObject.ID : WeakValue<RootObject>] = [:]
33+
var trackedIndexes: [Index.ID : WeakValue<Index>] = [:]
34+
var trackedPages: [Page.ID : WeakValue<Page>] = [:]
2735

2836
/// The root objects on the file system that are actively loaded in memory.
2937
var loadedRootObjects: Set<RootObject.ID> = []
@@ -101,16 +109,23 @@ extension DiskPersistence.Datastore {
101109

102110
extension DiskPersistence.Datastore {
103111
func rootObject(for identifier: RootObject.ID) -> RootObject {
104-
if let rootObject = trackedRootObjects[identifier] {
112+
if let rootObject = trackedRootObjects[identifier]?.value {
105113
return rootObject
106114
}
115+
// print("🤷 Cache Miss: Root \(identifier)")
107116
let rootObject = RootObject(datastore: self, id: identifier)
108-
trackedRootObjects[identifier] = rootObject
117+
trackedRootObjects[identifier] = WeakValue(rootObject)
118+
Task { await snapshot.persistence.cache(rootObject) }
109119
return rootObject
110120
}
111121

112122
func adopt(rootObject: RootObject) {
113-
trackedRootObjects[rootObject.id] = rootObject
123+
trackedRootObjects[rootObject.id] = WeakValue(rootObject)
124+
Task { await snapshot.persistence.cache(rootObject) }
125+
}
126+
127+
func invalidate(_ identifier: RootObject.ID) {
128+
trackedRootObjects.removeValue(forKey: identifier)
114129
}
115130

116131
func mark(identifier: RootObject.ID, asLoaded: Bool) {
@@ -122,16 +137,23 @@ extension DiskPersistence.Datastore {
122137
}
123138

124139
func index(for identifier: Index.ID) -> Index {
125-
if let index = trackedIndexes[identifier] {
140+
if let index = trackedIndexes[identifier]?.value {
126141
return index
127142
}
143+
// print("🤷 Cache Miss: Index \(identifier)")
128144
let index = Index(datastore: self, id: identifier)
129-
trackedIndexes[identifier] = index
145+
trackedIndexes[identifier] = WeakValue(index)
146+
Task { await snapshot.persistence.cache(index) }
130147
return index
131148
}
132149

133150
func adopt(index: Index) {
134-
trackedIndexes[index.id] = index
151+
trackedIndexes[index.id] = WeakValue(index)
152+
Task { await snapshot.persistence.cache(index) }
153+
}
154+
155+
func invalidate(_ identifier: Index.ID) {
156+
trackedIndexes.removeValue(forKey: identifier)
135157
}
136158

137159
func mark(identifier: Index.ID, asLoaded: Bool) {
@@ -143,16 +165,23 @@ extension DiskPersistence.Datastore {
143165
}
144166

145167
func page(for identifier: Page.ID) -> Page {
146-
if let page = trackedPages[identifier.withoutManifest] {
168+
if let page = trackedPages[identifier.withoutManifest]?.value {
147169
return page
148170
}
171+
// print("🤷 Cache Miss: Page \(identifier.page)")
149172
let page = Page(datastore: self, id: identifier)
150-
trackedPages[identifier.withoutManifest] = page
173+
trackedPages[identifier.withoutManifest] = WeakValue(page)
174+
Task { await snapshot.persistence.cache(page) }
151175
return page
152176
}
153177

154178
func adopt(page: Page) {
155-
trackedPages[page.id.withoutManifest] = page
179+
trackedPages[page.id.withoutManifest] = WeakValue(page)
180+
Task { await snapshot.persistence.cache(page) }
181+
}
182+
183+
func invalidate(_ identifier: Page.ID) {
184+
trackedPages.removeValue(forKey: identifier.withoutManifest)
156185
}
157186

158187
func mark(identifier: Page.ID, asLoaded: Bool) {

Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ public actor DiskPersistence<AccessMode: _AccessMode>: Persistence {
2525

2626
var lastTransaction: Transaction?
2727

28+
/// Shared caches across all snapshots and datastores.
29+
var rollingRootObjectCacheIndex = 0
30+
var rollingRootObjectCache: [Datastore.RootObject] = []
31+
32+
var rollingIndexCacheIndex = 0
33+
var rollingIndexCache: [Datastore.Index] = []
34+
35+
var rollingPageCacheIndex = 0
36+
var rollingPageCache: [Datastore.Page] = []
37+
2838
/// Initialize a ``DiskPersistence`` with a read-write URL.
2939
///
3040
/// Use this initializer when creating a persistence from the main process that will access it, such as your app. To access the same persistence from another process, use ``init(readOnlyURL:)`` instead.
@@ -501,6 +511,43 @@ extension DiskPersistence {
501511
}
502512
}
503513

514+
// MARK: - Persistence-wide Caches
515+
516+
extension DiskPersistence {
517+
func cache(_ rootObject: Datastore.RootObject) {
518+
if rollingRootObjectCache.count <= rollingRootObjectCacheIndex {
519+
rollingRootObjectCache.append(rootObject)
520+
} else {
521+
rollingRootObjectCache[rollingRootObjectCacheIndex] = rootObject
522+
}
523+
/// Limit cache to 16 recent root objects. We only really need one per active datastore.
524+
/// Note more-recently accessed entries may be represented multiple times in the cache, and are more likely to survive.
525+
rollingRootObjectCacheIndex = (rollingRootObjectCacheIndex + 1) % 16
526+
}
527+
528+
func cache(_ index: Datastore.Index) {
529+
if rollingIndexCache.count <= rollingIndexCacheIndex {
530+
rollingIndexCache.append(index)
531+
} else {
532+
rollingIndexCache[rollingIndexCacheIndex] = index
533+
}
534+
/// Limit cache to 128 recent indexes, which is 8 per datastore.
535+
/// Note more-recently accessed entries may be represented multiple times in the cache, and are more likely to survive.
536+
rollingIndexCacheIndex = (rollingIndexCacheIndex + 1) % 128
537+
}
538+
539+
func cache(_ page: Datastore.Page) {
540+
if rollingPageCache.count <= rollingPageCacheIndex {
541+
rollingPageCache.append(page)
542+
} else {
543+
rollingPageCache[rollingPageCacheIndex] = page
544+
}
545+
/// Limit cache to 4096 recent pages, which is up to 16MB.
546+
/// Note more-recently accessed entries may be represented multiple times in the cache, and are more likely to survive.
547+
rollingPageCacheIndex = (rollingPageCacheIndex + 1) % 4096
548+
}
549+
}
550+
504551
// MARK: - Helper Types
505552

506553
class WeakDatastore {

Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,18 @@ extension DiskPersistence {
124124
}
125125

126126
private func persist() async throws {
127+
defer {
128+
rootObjects.removeAll()
129+
entryMutations.removeAll()
130+
createdRootObjects.removeAll()
131+
createdIndexes.removeAll()
132+
createdPages.removeAll()
133+
deletedRootObjects.removeAll()
134+
deletedIndexes.removeAll()
135+
deletedPages.removeAll()
136+
childTransactions.removeAll()
137+
}
138+
127139
if let parent {
128140
try await parent.apply(
129141
rootObjects: rootObjects,

0 commit comments

Comments
 (0)