Skip to content

Commit 3a61a8a

Browse files
authored
Merge pull request groue#1253 from george-signal/george-signal/optional-memory-management
Make memory management optional.
2 parents 84bee19 + e076644 commit 3a61a8a

File tree

6 files changed

+174
-29
lines changed

6 files changed

+174
-29
lines changed

GRDB/Core/Configuration.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,15 @@ public struct Configuration {
275275
///
276276
/// Default: nil
277277
public var writeTargetQueue: DispatchQueue? = nil
278-
278+
279+
#if os(iOS)
280+
/// Sets whether GRDB will release memory when entering the background or
281+
/// upon receiving a memory warning in iOS.
282+
///
283+
/// Default: true
284+
public var automaticMemoryManagement = true
285+
#endif
286+
279287
// MARK: - Factory Configuration
280288

281289
/// Creates a factory configuration

GRDB/Core/Database.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1184,7 +1184,8 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
11841184

11851185
// MARK: - Memory Management
11861186

1187-
func releaseMemory() {
1187+
public func releaseMemory() {
1188+
SchedulingWatchdog.preconditionValidQueue(self)
11881189
sqlite3_db_release_memory(sqliteConnection)
11891190
schemaCache.clear()
11901191
internalStatementCache.clear()

GRDB/Core/DatabasePool.swift

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ public final class DatabasePool: DatabaseWriter {
103103
// Be a nice iOS citizen, and don't consume too much memory
104104
// See https://github.com/groue/GRDB.swift/#memory-management
105105
#if os(iOS)
106-
setupMemoryManagement()
106+
if configuration.automaticMemoryManagement {
107+
setupMemoryManagement()
108+
}
107109
#endif
108110
}
109111

@@ -154,13 +156,6 @@ public final class DatabasePool: DatabaseWriter {
154156

155157
return configuration
156158
}
157-
158-
/// Blocks the current thread until all database connections have
159-
/// executed the *body* block.
160-
fileprivate func forEachConnection(_ body: (Database) -> Void) {
161-
writer.sync(body)
162-
readerPool?.forEach { $0.sync(body) }
163-
}
164159
}
165160

166161
#if swift(>=5.6) && canImport(_Concurrency)
@@ -172,19 +167,49 @@ extension DatabasePool {
172167

173168
// MARK: - Memory management
174169

175-
/// Free as much memory as possible.
170+
/// Frees as much memory as possible, by disposing non-essential memory from
171+
/// the writer connection, and closing all reader connections.
172+
///
173+
/// This method is synchronous, and blocks the current thread until all
174+
/// database accesses are completed.
176175
///
177-
/// This method blocks the current thread until all database accesses
178-
/// are completed.
176+
/// - warning: This method can prevent concurrent reads from executing,
177+
/// until it returns. Prefer ``releaseMemoryEventually()`` if you intend
178+
/// to keep on using the database while releasing memory.
179179
public func releaseMemory() {
180180
// Release writer memory
181181
writer.sync { $0.releaseMemory() }
182-
// Release readers memory by closing all connections
182+
183+
// Release readers memory by closing all connections.
184+
//
185+
// We must use a barrier in order to guarantee that memory has been
186+
// freed (reader connections closed) when the method exits, as
187+
// documented.
188+
//
189+
// Without the barrier, connections would only close _eventually_ (after
190+
// their eventual concurrent jobs have completed).
183191
readerPool?.barrier {
184192
readerPool?.removeAll()
185193
}
186194
}
187195

196+
/// Eventually frees as much memory as possible, by disposing non-essential
197+
/// memory from the writer connection, and closing all reader connections.
198+
///
199+
/// Unlike ``releaseMemory()``, this method does not prevent concurrent
200+
/// database accesses when it is executing. But it does not notify when
201+
/// non-essential memory has been freed.
202+
public func releaseMemoryEventually() {
203+
// Release readers memory by eventually closing all reader connections
204+
// (they will close after their current jobs have completed).
205+
readerPool?.removeAll()
206+
207+
// Release writer memory eventually.
208+
writer.async { db in
209+
db.releaseMemory()
210+
}
211+
}
212+
188213
#if os(iOS)
189214
/// Listens to UIApplicationDidEnterBackgroundNotification and
190215
/// UIApplicationDidReceiveMemoryWarningNotification in order to release
@@ -216,22 +241,28 @@ extension DatabasePool {
216241

217242
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
218243
if task == .invalid {
219-
// Perform releaseMemory() synchronously.
244+
// Release memory synchronously
220245
releaseMemory()
221246
} else {
222-
// Perform releaseMemory() asynchronously.
223-
DispatchQueue.global().async {
224-
self.releaseMemory()
247+
// Release memory eventually.
248+
//
249+
// We don't know when reader connections will be closed (because
250+
// they may be currently in use), so we don't quite know when
251+
// reader memory will be freed (which would be the ideal timing for
252+
// ending our background task).
253+
//
254+
// So let's just end the background task after the writer connection
255+
// has freed its memory. That's better than nothing.
256+
releaseMemoryEventually()
257+
writer.async { _ in
225258
application.endBackgroundTask(task)
226259
}
227260
}
228261
}
229262

230263
@objc
231264
private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) {
232-
DispatchQueue.global().async {
233-
self.releaseMemory()
234-
}
265+
releaseMemoryEventually()
235266
}
236267
#endif
237268
}

GRDB/Core/DatabaseQueue.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ public final class DatabaseQueue: DatabaseWriter {
4343
// Be a nice iOS citizen, and don't consume too much memory
4444
// See https://github.com/groue/GRDB.swift/#memory-management
4545
#if os(iOS)
46-
setupMemoryManagement()
46+
if configuration.automaticMemoryManagement {
47+
setupMemoryManagement()
48+
}
4749
#endif
4850
}
4951

@@ -117,21 +119,21 @@ extension DatabaseQueue {
117119

118120
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
119121
if task == .invalid {
120-
// Perform releaseMemory() synchronously.
122+
// Release memory synchronously
121123
releaseMemory()
122124
} else {
123-
// Perform releaseMemory() asynchronously.
124-
DispatchQueue.global().async {
125-
self.releaseMemory()
125+
// Release memory asynchronously
126+
writer.async { db in
127+
db.releaseMemory()
126128
application.endBackgroundTask(task)
127129
}
128130
}
129131
}
130132

131133
@objc
132134
private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) {
133-
DispatchQueue.global().async {
134-
self.releaseMemory()
135+
writer.async { db in
136+
db.releaseMemory()
135137
}
136138
}
137139
#endif

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7718,12 +7718,36 @@ dbPool.releaseMemory()
77187718
77197719
This method blocks the current thread until all current database accesses are completed, and the memory collected.
77207720
7721+
> :warning: **Warning**: If `DatabasePool.releaseMemory()` is called while a long read is performed concurrently, then no other read access will be possible until this long read has completed, and the memory has been released. If this does not suit your application needs, look for the asynchronous options below:
7722+
7723+
You can release memory in an asynchronous way as well:
7724+
7725+
```swift
7726+
// On a DatabaseQueue
7727+
dbQueue.asyncWriteWithoutTransaction { db in
7728+
db.releaseMemory()
7729+
}
7730+
7731+
// On a DatabasePool
7732+
dbPool.releaseMemoryEventually()
7733+
```
7734+
7735+
`DatabasePool.releaseMemoryEventually()` does not block the current thread, and does not prevent concurrent database accesses. In exchange for this convenience, you don't know when memory has been freed.
7736+
77217737
77227738
### Memory Management on iOS
77237739
77247740
**The iOS operating system likes applications that do not consume much memory.**
77257741
7726-
[Database queues](#database-queues) and [pools](#database-pools) automatically call the `releaseMemory` method when the application receives a memory warning, and when the application enters background.
7742+
[Database queues](#database-queues) and [pools](#database-pools) automatically free non-essential memory when the application receives a memory warning, and when the application enters background.
7743+
7744+
You can opt out of this automatic memory management:
7745+
7746+
```swift
7747+
var config = Configuration()
7748+
config.automaticMemoryManagement = false
7749+
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config) // or DatabasePool
7750+
```
77277751
77287752
77297753
## Data Protection

Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,85 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase {
4141
XCTAssertEqual(openConnectionCount, 0)
4242
}
4343

44+
#if os(iOS)
45+
func testDatabasePoolReleasesMemoryOnPressureEvent() throws {
46+
// Create a database pool, and expect a reader connection to be closed
47+
let expectation = self.expectation(description: "Reader connection closed")
48+
49+
var configuration = Configuration()
50+
configuration.SQLiteConnectionWillClose = { conn in
51+
if sqlite3_db_readonly(conn, nil) != 0 {
52+
expectation.fulfill()
53+
}
54+
}
55+
let dbPool = try makeDatabasePool(configuration: configuration)
56+
57+
// Precondition: there is one reader.
58+
try dbPool.read { _ in }
59+
60+
// Simulate memory warning.
61+
NotificationCenter.default.post(
62+
name: NSNotification.Name(rawValue: "UIApplicationDidReceiveMemoryWarningNotification"),
63+
object: nil)
64+
65+
// Postcondition: reader connection was closed
66+
withExtendedLifetime(dbPool) { _ in
67+
waitForExpectations(timeout: 0.5)
68+
}
69+
}
70+
71+
func testDatabasePoolDoesNotReleaseMemoryOnPressureEventIfDisabled() throws {
72+
// Create a database pool, and do not expect any reader connection to be closed
73+
let expectation = self.expectation(description: "Reader connection closed")
74+
expectation.isInverted = true
75+
76+
var configuration = Configuration()
77+
configuration.automaticMemoryManagement = false
78+
configuration.SQLiteConnectionWillClose = { conn in
79+
if sqlite3_db_readonly(conn, nil) != 0 {
80+
expectation.fulfill()
81+
}
82+
}
83+
let dbPool = try makeDatabasePool(configuration: configuration)
84+
85+
// Precondition: there is one reader.
86+
try dbPool.read { _ in }
87+
88+
// Simulate memory warning.
89+
NotificationCenter.default.post(
90+
name: NSNotification.Name(rawValue: "UIApplicationDidReceiveMemoryWarningNotification"),
91+
object: nil)
92+
93+
// Postcondition: no reader connection was closed
94+
withExtendedLifetime(dbPool) { _ in
95+
waitForExpectations(timeout: 0.5)
96+
}
97+
}
98+
99+
// Regression test for <https://github.com/groue/GRDB.swift/pull/1253#issuecomment-1177166630>
100+
func testDatabasePoolDoesNotPreventConcurrentReadsOnPressureEvent() throws {
101+
let dbPool = try makeDatabasePool()
102+
103+
// Start a read that blocks
104+
let semaphore = DispatchSemaphore(value: 0)
105+
dbPool.asyncRead { _ in
106+
semaphore.wait()
107+
}
108+
109+
// Simulate memory warning.
110+
NotificationCenter.default.post(
111+
name: NSNotification.Name(rawValue: "UIApplicationDidReceiveMemoryWarningNotification"),
112+
object: nil)
113+
114+
// Make sure we can read
115+
try dbPool.read { _ in }
116+
117+
// Cleanup
118+
semaphore.signal()
119+
}
120+
121+
#endif
122+
44123
// TODO: fix flaky test
45124
// func testDatabasePoolReleaseMemoryClosesReaderConnections() throws {
46125
// let countQueue = DispatchQueue(label: "GRDB")

0 commit comments

Comments
 (0)