Skip to content

Commit 9590c13

Browse files
fix: minor memory tweaks (#337)
1 parent f03e918 commit 9590c13

4 files changed

Lines changed: 80 additions & 60 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,37 @@
33
## 1.11.2 (unreleased)
44

55
- Don't attempt to create WebSocket connections on watchOS.
6+
- Update default SQLite cache size to 50MB, this was previously erroneously set to 200MB
7+
- Move dispatching responsibility into `SQLiteConnectionPool` implementations after obtaining a
8+
connection lease.
9+
Implementers of `SQLiteConnectionPool` should dispatch blocking SQLite callbacks to an
10+
appropriate dispatcher such as `Dispatchers.IO`. This should prevent the worker pool from
11+
expanding when large numbers of concurrent operations are requested.
612

713
## 1.11.1
814

915
- Fix RSocket connection bugs on iOS (and other platforms using the RSocket sync transport):
10-
- Fix false `connected: true` status when using an invalid token. `ConnectionEstablished` is now
11-
only emitted when the first data frame arrives from the server, matching the HTTP path which
12-
waits for a `200 OK`.
13-
- Fix sync loop terminating permanently when the server rejects the connection with an RSocket
14-
ERROR frame (e.g. invalid JWT). `RSocketError` extends `Throwable` not `Exception`, so it was
15-
not caught by the retry loop.
16-
- Fix sync loop stalling indefinitely after a transport-layer failure (dead socket, network
17-
dropout).
16+
- Fix false `connected: true` status when using an invalid token. `ConnectionEstablished` is now
17+
only emitted when the first data frame arrives from the server, matching the HTTP path which
18+
waits for a `200 OK`.
19+
- Fix sync loop terminating permanently when the server rejects the connection with an RSocket
20+
ERROR frame (e.g. invalid JWT). `RSocketError` extends `Throwable` not `Exception`, so it was
21+
not caught by the retry loop.
22+
- Fix sync loop stalling indefinitely after a transport-layer failure (dead socket, network
23+
dropout).
1824

1925
## 1.11.0
2026

2127
- __Breaking__: On tables, the `localOnly`, `insertOnly`, `trackMetadata`, `trackPreviousValues` and
2228
`ignoreEmptyUpdates` options are now stored in a `TableOptions` class. Existing constructors
2329
continue to work, but calling `copy` with any of these parameters no longer works.
2430
- Make raw tables easier to use:
25-
- Introduce the `RawTableSchema` class storing the name of a raw table in the database. When set,
26-
`put` and `delete` statements can be inferred automatically.
27-
- Add `RawTable.jsonDescription`, which can be passed to the `powersync_create_raw_table_crud_trigger`
28-
SQL function to auto-create triggers forwarding writes to `ps_crud`.
31+
- Introduce the `RawTableSchema` class storing the name of a raw table in the database. When
32+
set,
33+
`put` and `delete` statements can be inferred automatically.
34+
- Add `RawTable.jsonDescription`, which can be passed to the
35+
`powersync_create_raw_table_crud_trigger`
36+
SQL function to auto-create triggers forwarding writes to `ps_crud`.
2937
- Update PowerSync core extension to version 0.4.11.
3038
- Remove the experimental label from Sync Stream APIs.
3139
- Compose: add `composeSyncStream` helper method to subscribe to Sync Streams in a composition.
@@ -59,7 +67,8 @@
5967

6068
## 1.10.2
6169

62-
- Exceptions that occur while initializing a PowerSync database are now rethrown when the database is used.
70+
- Exceptions that occur while initializing a PowerSync database are now rethrown when the database
71+
is used.
6372
- [Internal] Updated PowerSyncKotlin build to use SKIEE's `produceDistributableFramework`.
6473

6574
## 1.10.1

common/src/commonMain/kotlin/com/powersync/db/driver/InternalConnectionPool.kt

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import com.powersync.ExperimentalPowerSyncAPI
66
import com.powersync.PersistentConnectionFactory
77
import com.powersync.utils.JsonUtil
88
import kotlinx.coroutines.CoroutineScope
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.IO
911
import kotlinx.coroutines.flow.MutableSharedFlow
1012
import kotlinx.coroutines.flow.SharedFlow
1113
import kotlinx.coroutines.launch
1214
import kotlinx.coroutines.sync.Mutex
1315
import kotlinx.coroutines.sync.withLock
16+
import kotlinx.coroutines.withContext
1417

1518
@OptIn(ExperimentalPowerSyncAPI::class)
1619
internal class InternalConnectionPool(
@@ -26,6 +29,11 @@ internal class InternalConnectionPool(
2629
// MutableSharedFlow to emit batched table updates
2730
private val tableUpdatesFlow = MutableSharedFlow<Set<String>>(replay = 0)
2831

32+
// Database calls are synchronous/blocking, so we always run them on Dispatchers.IO instead of
33+
// inheriting the caller-provided scope context. The provided scope is still used for
34+
// lifecycle-bound pool coroutines like read workers and update emission.
35+
private val dispatcher = Dispatchers.IO
36+
2937
private fun newConnection(readOnly: Boolean): SQLiteConnection {
3038
val connection =
3139
factory.openConnection(
@@ -38,19 +46,26 @@ internal class InternalConnectionPool(
3846
return connection
3947
}
4048

41-
override suspend fun <T> read(callback: suspend (SQLiteConnectionLease) -> T): T = readPool.read(callback)
49+
override suspend fun <T> read(callback: suspend (SQLiteConnectionLease) -> T): T =
50+
readPool.read { connection ->
51+
withContext(dispatcher) {
52+
callback(connection)
53+
}
54+
}
4255

4356
override suspend fun <T> write(callback: suspend (SQLiteConnectionLease) -> T): T =
4457
writeLockMutex.withLock {
45-
try {
46-
callback(RawConnectionLease(writeConnection))
47-
} finally {
48-
// When we've leased a write connection, we may have to update table update flows
49-
// after users ran their custom statements.
50-
val updatedTables = writeConnection.readPendingUpdates()
51-
if (updatedTables.isNotEmpty()) {
52-
scope.launch {
53-
tableUpdatesFlow.emit(updatedTables)
58+
withContext(dispatcher) {
59+
try {
60+
callback(RawConnectionLease(writeConnection))
61+
} finally {
62+
// When we've leased a write connection, we may have to update table update flows
63+
// after users ran their custom statements.
64+
val updatedTables = writeConnection.readPendingUpdates()
65+
if (updatedTables.isNotEmpty()) {
66+
scope.launch {
67+
tableUpdatesFlow.emit(updatedTables)
68+
}
5469
}
5570
}
5671
}
@@ -61,6 +76,7 @@ internal class InternalConnectionPool(
6176
readPool.withAllConnections { rawReadConnections ->
6277
val readers = rawReadConnections.map { RawConnectionLease(it) }
6378
// Then get access to the write connection
79+
// The write call will dispatch to the provided dispatcher
6480
write { writer ->
6581
action(writer, readers)
6682
}
@@ -85,7 +101,7 @@ internal fun SQLiteConnection.setupDefaultPragmas(readOnly: Boolean) {
85101

86102
execSQL("pragma journal_size_limit = ${6 * 1024 * 1024}")
87103
execSQL("pragma busy_timeout = 30000")
88-
execSQL("pragma cache_size = ${50 * 1024}")
104+
execSQL("pragma cache_size = -${50 * 1024}")
89105

90106
// Older versions of the SDK used to set up an empty schema and raise the user version to 1.
91107
// Keep doing that for consistency.

common/src/commonMain/kotlin/com/powersync/db/driver/SingleConnectionPool.kt

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package com.powersync.db.driver
22

33
import androidx.sqlite.SQLiteConnection
44
import com.powersync.ExperimentalPowerSyncAPI
5+
import kotlinx.coroutines.Dispatchers
6+
import kotlinx.coroutines.IO
57
import kotlinx.coroutines.flow.MutableSharedFlow
68
import kotlinx.coroutines.flow.SharedFlow
79
import kotlinx.coroutines.sync.Mutex
810
import kotlinx.coroutines.sync.withLock
11+
import kotlinx.coroutines.withContext
912

1013
/**
1114
* A [SQLiteConnectionPool] backed by a single database connection.
@@ -20,6 +23,8 @@ public class SingleConnectionPool(
2023
private var closed = false
2124
private val tableUpdatesFlow = MutableSharedFlow<Set<String>>(replay = 0)
2225

26+
private val dispatcher = Dispatchers.IO
27+
2328
init {
2429
conn.setupDefaultPragmas(false)
2530
}
@@ -28,14 +33,16 @@ public class SingleConnectionPool(
2833

2934
override suspend fun <T> write(callback: suspend (SQLiteConnectionLease) -> T): T =
3035
mutex.withLock {
31-
check(!closed) { "Connection closed" }
32-
33-
try {
34-
callback(RawConnectionLease(conn))
35-
} finally {
36-
val updates = conn.readPendingUpdates()
37-
if (updates.isNotEmpty()) {
38-
tableUpdatesFlow.emit(updates)
36+
withContext(dispatcher) {
37+
check(!closed) { "Connection closed" }
38+
39+
try {
40+
callback(RawConnectionLease(conn))
41+
} finally {
42+
val updates = conn.readPendingUpdates()
43+
if (updates.isNotEmpty()) {
44+
tableUpdatesFlow.emit(updates)
45+
}
3946
}
4047
}
4148
}

common/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabaseImpl.kt

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,20 @@ import com.powersync.db.runWrapped
1111
import com.powersync.utils.AtomicMutableSet
1212
import com.powersync.utils.JsonUtil
1313
import com.powersync.utils.throttle
14-
import kotlinx.coroutines.Dispatchers
15-
import kotlinx.coroutines.IO
1614
import kotlinx.coroutines.flow.Flow
1715
import kotlinx.coroutines.flow.SharedFlow
1816
import kotlinx.coroutines.flow.emitAll
1917
import kotlinx.coroutines.flow.flow
2018
import kotlinx.coroutines.flow.map
2119
import kotlinx.coroutines.flow.onSubscription
2220
import kotlinx.coroutines.flow.transform
23-
import kotlinx.coroutines.withContext
2421
import kotlin.time.Duration.Companion.milliseconds
2522

2623
@OptIn(ExperimentalPowerSyncAPI::class)
2724
internal class InternalDatabaseImpl(
2825
private val pool: SQLiteConnectionPool,
2926
private val logger: Logger,
3027
) : InternalDatabase {
31-
// Could be scope.coroutineContext, but the default is GlobalScope, which seems like a bad idea. To discuss.
32-
private val dbContext = Dispatchers.IO
33-
3428
override suspend fun execute(
3529
sql: String,
3630
parameters: List<Any?>?,
@@ -40,20 +34,18 @@ internal class InternalDatabaseImpl(
4034
}
4135

4236
override suspend fun updateSchema(schemaJson: String) {
43-
withContext(dbContext) {
44-
runWrapped {
45-
pool.withAllConnections { writer, readers ->
46-
writer.runTransaction { tx ->
47-
tx.getOptional(
48-
"SELECT powersync_replace_schema(?);",
49-
listOf(schemaJson),
50-
) {}
51-
}
37+
runWrapped {
38+
pool.withAllConnections { writer, readers ->
39+
writer.runTransaction { tx ->
40+
tx.getOptional(
41+
"SELECT powersync_replace_schema(?);",
42+
listOf(schemaJson),
43+
) {}
44+
}
5245

53-
// Update the schema on all read connections
54-
for (readConnection in readers) {
55-
readConnection.execSQL("pragma table_info('sqlite_master')")
56-
}
46+
// Update the schema on all read connections
47+
for (readConnection in readers) {
48+
readConnection.execSQL("pragma table_info('sqlite_master')")
5749
}
5850
}
5951
}
@@ -167,11 +159,9 @@ internal class InternalDatabaseImpl(
167159
*/
168160
@OptIn(ExperimentalPowerSyncAPI::class)
169161
private suspend fun <R> internalReadLock(callback: suspend (SQLiteConnectionLease) -> R): R =
170-
withContext(dbContext) {
171-
runWrapped {
172-
useConnection(true) { connection ->
173-
callback(connection)
174-
}
162+
runWrapped {
163+
useConnection(true) { connection ->
164+
callback(connection)
175165
}
176166
}
177167

@@ -189,11 +179,9 @@ internal class InternalDatabaseImpl(
189179

190180
@OptIn(ExperimentalPowerSyncAPI::class)
191181
private suspend fun <R> internalWriteLock(callback: suspend (SQLiteConnectionLease) -> R): R =
192-
withContext(dbContext) {
182+
runWrapped {
193183
pool.write { writer ->
194-
runWrapped {
195-
callback(writer)
196-
}
184+
callback(writer)
197185
}
198186
}
199187

0 commit comments

Comments
 (0)