chore: Move the drift_offline_first to legacy in favor of Serverpod implementation (#32)

This commit is contained in:
Marcelo Mendonça Soares 2026-04-05 23:20:21 -03:00 • committed by Marcelo Soares
commit cbc472523b
No known key found for this signature in database
GPG key ID: CB2F09CE66E99EAD
6 changed files with 13575 additions and 284 deletions

View file

@ -0,0 +1,99 @@
import 'dart:io';
import 'package:benchmark_harness/benchmark_harness.dart';
import 'package:drift_offline_sync/src/database/triggers.dart';
import 'package:path/path.dart' as p;
import 'database.dart';
export '../../lib/src/database/triggers.dart' show Operation;
class CrdtBenchmark extends AsyncBenchmarkBase {
CrdtBenchmark(
super.name, {
required this.crdtEnabled,
required this.operation,
required this.rowCount,
});
final bool crdtEnabled;
final Operation operation;
final int rowCount;
late final CrdtBenchmarkDatabase _db;
late final File _dbFile;
int _lastDatabaseSize = 0;
int _lastRowsCount = 0;
@override
Future<void> setup() async {
final dbPath = p.join(Directory.systemTemp.path, 'benchmark_$name.db');
_dbFile = File(dbPath);
_db = CrdtBenchmarkDatabase.createDatabase(
_dbFile,
crdtEnabled: crdtEnabled,
);
// Force the migration to be applied during setup.
await _db.batchInsertTestRows(1);
await _db.deleteTestRows(1);
await _db.managers.tableWithEveryColumnType.delete();
if (operation != Operation.insert) {
await _db.batchInsertTestRows(rowCount);
}
}
/// The default warmup is to invoke the [run] method once, but this pollutes the
/// results by persisting data before the run.
@override
Future<void> warmup() async {}
@override
Future<void> run() async {
_lastDatabaseSize = 0;
_lastRowsCount = 0;
switch (operation) {
case Operation.insert:
case Operation.update:
await _db.upsertTestRows(rowCount);
case Operation.delete:
await _db.deleteTestRows(rowCount);
}
}
@override
Future<(double, int)> report() async {
final averageMicroseconds = await measure();
return (averageMicroseconds, measureStorage());
}
@override
Future<void> teardown() async {
_lastDatabaseSize = _dbFile.lengthSync();
_lastRowsCount = await _db.testRowsCount();
await _db.close();
if (_dbFile.existsSync()) {
_dbFile.deleteSync();
}
}
int measureStorage() {
if (operation == Operation.delete) {
return _lastDatabaseSize;
}
if (_lastDatabaseSize == 0) {
throw Exception('Last database size is not set.');
}
if (_lastRowsCount < rowCount) {
throw Exception(
'Rows count is $_lastRowsCount but expected at least $rowCount.',
);
}
// If the benchmark ran for more than one loop, it can contain a greater number
// of rows than the expected. We need to normalize it to the expected total.
return _lastDatabaseSize ~/ (_lastRowsCount / rowCount);
}
}

View file

@ -0,0 +1,48 @@
import 'package:intl/intl.dart';
final formatter0 = NumberFormat.decimalPatternDigits(decimalDigits: 0);
final formatter2 = NumberFormat.decimalPatternDigits(decimalDigits: 2);
final formatter3 = NumberFormat.decimalPatternDigits(decimalDigits: 3);
extension FormattedStorageSize on num {
String toFormattedDuration() {
var time = this;
var unit = 1;
while (time.abs() > 1000 && unit < Duration.microsecondsPerSecond) {
time /= 1000;
unit *= 1000;
}
while (unit > Duration.microsecondsPerMinute && time.abs() > 60) {
time /= 60;
unit *= 60;
}
return '${formatter2.format(time)} ${switch (unit) {
1 => 'μs',
Duration.millisecondsPerSecond => 'ms',
Duration.microsecondsPerSecond => 's',
Duration.microsecondsPerMinute => 'min',
Duration.microsecondsPerHour => 'h',
_ => throw Exception('Unexpected time unit: $unit'),
}}';
}
String toFormattedStorageSize() {
var storageIncrease = toDouble();
var storageUnit = 1;
while (storageIncrease > 1024) {
storageIncrease /= 1024;
storageUnit *= 1024;
}
final storageUnitString = switch (storageUnit) {
1 => 'B',
1024 => 'KB',
const (1024 * 1024) => 'MB',
_ => throw Exception('Unexpected storage unit: $storageUnit'),
};
return '${formatter2.format(storageIncrease)} $storageUnitString';
}
}

View file

@ -0,0 +1,95 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:drift_offline_sync/drift_offline_sync.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:uuid/uuid.dart';
import '../../test/utils/tables.dart';
part 'database.g.dart';
@DriftDatabase(tables: [TableWithEveryColumnType])
class CrdtBenchmarkDatabase extends _$CrdtBenchmarkDatabase {
CrdtBenchmarkDatabase(super.e, {required this.crdtEnabled}) {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
}
static CrdtBenchmarkDatabase createDatabase(
File file, {
required bool crdtEnabled,
}) {
if (file.existsSync()) {
file.deleteSync();
}
return CrdtBenchmarkDatabase(
NativeDatabase.opened(sqlite3.open(file.path)),
crdtEnabled: crdtEnabled,
);
}
final bool crdtEnabled;
@override
int get schemaVersion => 1;
@override
Migrator createMigrator() => !crdtEnabled
? super.createMigrator()
: OfflineSyncMigrator(
this,
userId: const Uuid().v7(),
nodeId: const Uuid().v7(),
synchronizedTables: [tableWithEveryColumnType],
);
Future<void> batchInsertTestRows(int count) async {
final baseTimestamp = DateTime.now();
await managers.tableWithEveryColumnType.bulkCreate(
(_) => List.generate(count, (i) => _createTestRow(i, baseTimestamp)),
);
}
Future<void> upsertTestRows(int count) async {
final baseTimestamp = DateTime.now();
for (var i = 0; i < count; i++) {
await managers.tableWithEveryColumnType.create(
(_) => _createTestRow(i, baseTimestamp),
mode: InsertMode.insertOrReplace,
);
}
}
Future<void> deleteTestRows(int count) async {
for (var i = 0; i < count; i++) {
await managers.tableWithEveryColumnType
.filter((t) => t.id.equals(RowId(i)))
.delete();
}
}
Future<int> testRowsCount() async {
return managers.tableWithEveryColumnType.count();
}
TableWithEveryColumnTypeCompanion _createTestRow(
int i,
DateTime baseTimestamp,
) {
return TableWithEveryColumnTypeCompanion(
id: Value(RowId(i)),
aBool: Value(i.isEven),
aDateTime: Value(baseTimestamp.add(Duration(seconds: i))),
aText: Value('Text $i'),
anInt: Value(i),
anInt64: Value(BigInt.from(i)),
aReal: Value(i.toDouble()),
aBlob: Value(Uint8List.fromList([i % 256])),
anIntEnum: const Value(null),
aTextWithConverter: const Value(null),
aUuid: const Value(null),
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,64 @@
import 'benchmark.dart';
import 'conversion.dart';
void printPerformanceImpact(
BenchmarkResults benchmarkResults, {
required int rowCount,
bool runningInCI = false,
}) {
print(
'${runningInCI ? '```' : ''}'
'\n📊 ${benchmarkResults.operation.name.toUpperCase()} performance impact:',
);
final (baselineTime, _) = benchmarkResults.baseline;
final (crdtTime, _) = benchmarkResults.crdt;
final runDelayUs = crdtTime - baselineTime;
final slowdown = runDelayUs / baselineTime * 100;
final baselineDelay = baselineTime.toFormattedDuration();
final crdtDelay = crdtTime.toFormattedDuration();
final runDelay = runDelayUs.toFormattedDuration();
final runDelayPerOperation = (runDelayUs / rowCount).toFormattedDuration();
print(' Time: $baselineDelay --> $crdtDelay ($runDelay)');
print(' CRDT overhead: ${formatter2.format(slowdown)}% slower');
print(' Delay per ${benchmarkResults.operation.name}: $runDelayPerOperation');
if (runningInCI) print('```');
}
void printStorageImpact(
BenchmarkResults benchmarkResults, {
required int rowCount,
bool runningInCI = false,
}) {
print('${runningInCI ? '```' : ''}\n💽 Storage impact:');
final (_, baselineSize) = benchmarkResults.baseline;
final (_, crdtSize) = benchmarkResults.crdt;
final storageIncrease = crdtSize - baselineSize;
final storageIncPercent = storageIncrease / baselineSize * 100;
final extraStoragePerRow = storageIncrease / rowCount;
final storageIncreaseVolume = storageIncPercent > 100
? '(x${formatter2.format(storageIncPercent / 100)})'
: '(+${(crdtSize - baselineSize).toFormattedStorageSize()})';
print(
' Storage size: ${baselineSize.toFormattedStorageSize()} '
'--> ${crdtSize.toFormattedStorageSize()} $storageIncreaseVolume',
);
print(' CRDT overhead: ${formatter2.format(storageIncPercent)}% increase');
print(' Extra storage per row: ${extraStoragePerRow.toFormattedStorageSize()}');
if (runningInCI) print('```');
}
class BenchmarkResults {
const BenchmarkResults({
required this.operation,
required this.baseline,
required this.crdt,
});
final Operation operation;
final (double, int) baseline;
final (double, int) crdt;
}

View file

@ -0,0 +1,39 @@
import 'dart:io';
import 'dart:isolate';
import 'package:cli_tools/src/logger/helpers/progress.dart';
Progress? _trackedAnimationInProgress;
Future<T> runWithProgress<T>(
String message,
Future<T> Function() runner, {
bool Function(T result)? validator,
bool skipProgress = false,
}) async {
if (skipProgress) {
return Isolate.run(runner);
}
_stopAnimationInProgress();
final progress = Progress(message, stdout);
_trackedAnimationInProgress = progress;
final result = await Isolate.run(runner);
_trackedAnimationInProgress = null;
if (validator?.call(result) ?? true) {
progress.complete();
} else {
progress.fail();
}
return result;
}
void _stopAnimationInProgress() {
if (_trackedAnimationInProgress != null) {
_trackedAnimationInProgress?.stopAnimation();
// Since animation modifies the current line we add a new line so that
// the next print doesn't end up on the same line.
stdout.write('\n');
}
_trackedAnimationInProgress = null;
}