Skip to content

Commit e5d540d

Browse files
committed
Better benchmarking; fix major performance issue.
1 parent 34c6505 commit e5d540d

File tree

7 files changed

+166
-156
lines changed

7 files changed

+166
-156
lines changed

lib/src/sqlite_connection_factory.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,6 @@ class SqliteConnectionFactory {
5555
final db = await openFactory.open(
5656
SqliteOpenOptions(primaryConnection: primary, readOnly: readOnly));
5757

58-
db.updates.listen((event) {
59-
port.send(['update', event]);
60-
});
6158
return db;
6259
}
6360
}

lib/src/sqlite_connection_impl.dart

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class SqliteConnectionImpl with SqliteQueries implements SqliteConnection {
7878
final stopWatch = lockTimeout == null ? null : (Stopwatch()..start());
7979
// Private lock to synchronize this with other statements on the same connection,
8080
// to ensure that transactions aren't interleaved.
81-
return _connectionMutex.lock(() async {
81+
return await _connectionMutex.lock(() async {
8282
Duration? innerTimeout;
8383
if (lockTimeout != null && stopWatch != null) {
8484
innerTimeout = lockTimeout - stopWatch.elapsed;
@@ -186,9 +186,15 @@ class _TransactionContext implements SqliteWriteContext {
186186

187187
void _sqliteConnectionIsolate(_SqliteConnectionParams params) async {
188188
final db = await params.factory.openRawDatabase(readOnly: params.readOnly);
189+
final port = params.factory.port;
189190

190191
final commandPort = ReceivePort();
191192
params.portCompleter.complete(commandPort.sendPort);
193+
Set<String> updatedTables = {};
194+
195+
db.updates.listen((event) {
196+
updatedTables.add(event.tableName);
197+
});
192198

193199
commandPort.listen((data) async {
194200
if (data is List) {
@@ -198,14 +204,24 @@ void _sqliteConnectionIsolate(_SqliteConnectionParams params) async {
198204
await completer.handle(() async {
199205
String query = data[2];
200206
List<Object?> args = data[3];
201-
var results = db.select(query, args);
202-
return results;
207+
final result = db.select(query, args);
208+
if (updatedTables.isNotEmpty) {
209+
port.send(['update', updatedTables]);
210+
updatedTables = {};
211+
}
212+
return result;
203213
}, ignoreStackTrace: true);
204214
} else if (action == 'tx') {
205215
await completer.handle(() async {
206216
TxCallback cb = data[2];
207-
var result = await cb(db);
208-
return result;
217+
try {
218+
return await cb(db);
219+
} finally {
220+
if (updatedTables.isNotEmpty) {
221+
port.send(['update', updatedTables]);
222+
updatedTables = {};
223+
}
224+
}
209225
});
210226
}
211227
}

lib/src/sqlite_database.dart

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'dart:async';
22
import 'dart:isolate';
33

4-
import 'package:sqlite3/sqlite3.dart' as sqlite;
54
import 'package:sqlite_async/src/sqlite_open_factory.dart';
65

76
import 'connection_pool.dart';
@@ -104,18 +103,21 @@ class SqliteDatabase with SqliteQueries implements SqliteConnection {
104103
if (message is List) {
105104
String type = message[0];
106105
if (type == 'update') {
107-
sqlite.SqliteUpdate event = message[1];
106+
Set<String> tables = message[1];
108107
if (updates == null) {
109-
updates = UpdateNotification.single(event.tableName);
108+
updates = UpdateNotification(tables);
109+
// Use the mutex to only send updates after the current transaction.
110+
// Do take care to avoid getting a lock for each individual update -
111+
// that could add massive performance overhead.
112+
mutex.lock(() async {
113+
if (updates != null) {
114+
_updatesController.add(updates!);
115+
updates = null;
116+
}
117+
});
110118
} else {
111-
updates!.tables.add(event.tableName);
119+
updates!.tables.addAll(tables);
112120
}
113-
mutex.lock(() async {
114-
if (updates != null) {
115-
_updatesController.add(updates!);
116-
updates = null;
117-
}
118-
});
119121
} else if (type == 'init-db') {
120122
PortCompleter<void> completer = message[1];
121123
await completer.handle(() async {

pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ environment:
66
sdk: '>=2.19.1 <3.0.0'
77

88
dependencies:
9-
sqlite3: ^1.9.1
9+
sqlite3: ^1.10.1
1010
logging: ^1.1.1
1111
async: ^2.10.0
1212
collection: ^1.17.0
@@ -16,3 +16,4 @@ dev_dependencies:
1616
test: ^1.21.0
1717
test_api: ^0.4.18
1818
glob: ^2.1.1
19+
benchmarking: ^0.6.1

scripts/benchmark.dart

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import 'package:benchmarking/benchmarking.dart';
2+
import 'package:collection/collection.dart';
3+
4+
import 'package:sqlite_async/sqlite_async.dart';
5+
6+
import '../test/util.dart';
7+
8+
typedef BenchmarkFunction = Future<void> Function(
9+
SqliteDatabase, List<List<String>>);
10+
11+
class SqliteBenchmark {
12+
String name;
13+
int maxBatchSize;
14+
BenchmarkFunction fn;
15+
bool enabled;
16+
17+
SqliteBenchmark(this.name, this.fn,
18+
{this.maxBatchSize = 100000, this.enabled = true});
19+
}
20+
21+
List<SqliteBenchmark> benchmarks = [
22+
SqliteBenchmark('Insert: Direct',
23+
(SqliteDatabase db, List<List<String>> parameters) async {
24+
for (var params in parameters) {
25+
await db.execute(
26+
'INSERT INTO customers(name, email) VALUES(?, ?)', params);
27+
}
28+
}, maxBatchSize: 500),
29+
SqliteBenchmark('Insert: writeTransaction',
30+
(SqliteDatabase db, List<List<String>> parameters) async {
31+
await db.writeTransaction((tx) async {
32+
for (var params in parameters) {
33+
await tx.execute(
34+
'INSERT INTO customers(name, email) VALUES(?, ?)', params);
35+
}
36+
});
37+
}, maxBatchSize: 1000),
38+
SqliteBenchmark('Insert: computeWithDatabase',
39+
(SqliteDatabase db, List<List<String>> parameters) async {
40+
await db.computeWithDatabase((db) async {
41+
for (var params in parameters) {
42+
db.execute('INSERT INTO customers(name, email) VALUES(?, ?)', params);
43+
}
44+
});
45+
}),
46+
SqliteBenchmark('Insert: computeWithDatabase, prepared',
47+
(SqliteDatabase db, List<List<String>> parameters) async {
48+
await db.computeWithDatabase((db) async {
49+
var stmt = db.prepare('INSERT INTO customers(name, email) VALUES(?, ?)');
50+
try {
51+
for (var params in parameters) {
52+
stmt.execute(params);
53+
}
54+
} finally {
55+
stmt.dispose();
56+
}
57+
});
58+
}),
59+
SqliteBenchmark('Insert: executeBatch',
60+
(SqliteDatabase db, List<List<String>> parameters) async {
61+
await db.writeTransaction((tx) async {
62+
await tx.executeBatch(
63+
'INSERT INTO customers(name, email) VALUES(?, ?)', parameters);
64+
});
65+
}),
66+
SqliteBenchmark('Insert: computeWithDatabase, prepared x10',
67+
(SqliteDatabase db, List<List<String>> parameters) async {
68+
await db.computeWithDatabase((db) async {
69+
var stmt = db.prepare(
70+
'INSERT INTO customers(name, email) VALUES (?, ?), (?, ?), (?, ?), (?, ?), (?, ?), (?, ?), (?, ?), (?, ?), (?, ?), (?, ?)');
71+
try {
72+
for (var i = 0; i < parameters.length; i += 10) {
73+
var myParams =
74+
parameters.sublist(i, i + 10).flattened.toList(growable: false);
75+
stmt.execute(myParams);
76+
}
77+
} finally {
78+
stmt.dispose();
79+
}
80+
});
81+
}, enabled: false)
82+
];
83+
84+
void main() async {
85+
setupLogger();
86+
87+
var parameters = List.generate(
88+
20000, (index) => ['Test user $index', 'user$index@example.org']);
89+
90+
createTables(SqliteDatabase db) async {
91+
await db.writeTransaction((tx) async {
92+
await tx.execute(
93+
'CREATE TABLE IF NOT EXISTS customers(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)');
94+
await tx.execute('DELETE FROM customers WHERE 1');
95+
});
96+
await db.execute('VACUUM');
97+
await db.execute('PRAGMA wal_checkpoint(TRUNCATE)');
98+
}
99+
100+
final db = await setupDatabase(path: 'test-db/benchmark.db');
101+
await createTables(db);
102+
103+
benchmark(SqliteBenchmark benchmark) async {
104+
if (!benchmark.enabled) {
105+
return;
106+
}
107+
await createTables(db);
108+
109+
var limitedParameters = parameters;
110+
if (limitedParameters.length > benchmark.maxBatchSize) {
111+
limitedParameters = limitedParameters.sublist(0, benchmark.maxBatchSize);
112+
}
113+
114+
final rows1 = await db.execute('SELECT count(*) as count FROM customers');
115+
assert(rows1[0]['count'] == 0);
116+
final results = await asyncBenchmark(benchmark.name, () async {
117+
await benchmark.fn(db, limitedParameters);
118+
// This would make the benchmark fair, but only if each benchmark uses the
119+
// same batch size.
120+
// await db.execute('PRAGMA wal_checkpoint(TRUNCATE)');
121+
});
122+
123+
final row2 = await db.execute('SELECT count(*) as count FROM customers');
124+
assert(row2[0]['count'] == 0);
125+
results.report(units: limitedParameters.length);
126+
}
127+
128+
for (var entry in benchmarks) {
129+
await benchmark(entry);
130+
}
131+
}

test/performance_test.dart

Lines changed: 0 additions & 135 deletions
This file was deleted.

test/watch_test.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import 'dart:async';
22
import 'dart:math';
33

4-
import 'package:glob/glob.dart';
5-
import 'package:glob/list_local_fs.dart';
64
import 'package:sqlite_async/sqlite_async.dart';
75
import 'package:sqlite_async/src/database_utils.dart';
86
import 'package:test/test.dart';

0 commit comments

Comments
 (0)