refactor!: Rename sync APIs and ownership spaces (#128)
This commit is contained in:
parent
4331daf661
commit
b89725f6ea
5 changed files with 43650 additions and 9417 deletions
|
|
@ -73,7 +73,7 @@ class TypesTableBenchmark extends AsyncBenchmarkBase {
|
|||
|
||||
late final File _dbFile;
|
||||
late final ClientDatabaseSession _plainSession;
|
||||
CrdtDatabaseSession? _crdtSession;
|
||||
OfflineSyncDatabaseSession? _offlineSyncSession;
|
||||
var _hasOpenSession = false;
|
||||
|
||||
final UuidValue _userId = const Uuid().v7obj();
|
||||
|
|
@ -109,8 +109,8 @@ class TypesTableBenchmark extends AsyncBenchmarkBase {
|
|||
(j) => _createTypesRow(start + j, baseTimestamp),
|
||||
);
|
||||
if (crdtEnabled) {
|
||||
return _crdtSession!.db.transactionForUser(_userId, (tx) async {
|
||||
return Types.db.insert(_crdtSession!, rows, transaction: tx);
|
||||
return _offlineSyncSession!.db.transactionForUser(_userId, (tx) async {
|
||||
return Types.db.insert(_offlineSyncSession!, rows, transaction: tx);
|
||||
});
|
||||
}
|
||||
return Types.db.insert(_plainSession, rows);
|
||||
|
|
@ -121,8 +121,8 @@ class TypesTableBenchmark extends AsyncBenchmarkBase {
|
|||
return;
|
||||
}
|
||||
if (crdtEnabled) {
|
||||
await _crdtSession!.db.transactionForUser(_userId, (tx) async {
|
||||
await Types.db.delete(_crdtSession!, rows, transaction: tx);
|
||||
await _offlineSyncSession!.db.transactionForUser(_userId, (tx) async {
|
||||
await Types.db.delete(_offlineSyncSession!, rows, transaction: tx);
|
||||
});
|
||||
} else {
|
||||
await Types.db.delete(_plainSession, rows);
|
||||
|
|
@ -145,11 +145,11 @@ class TypesTableBenchmark extends AsyncBenchmarkBase {
|
|||
);
|
||||
}).toList();
|
||||
if (crdtEnabled) {
|
||||
_seededRows = await _crdtSession!.db.transactionForUser(
|
||||
_seededRows = await _offlineSyncSession!.db.transactionForUser(
|
||||
_userId,
|
||||
(tx) async {
|
||||
return Types.db.update(
|
||||
_crdtSession!,
|
||||
_offlineSyncSession!,
|
||||
updated,
|
||||
columns: (t) => [
|
||||
t.aBool,
|
||||
|
|
@ -211,11 +211,11 @@ class TypesTableBenchmark extends AsyncBenchmarkBase {
|
|||
];
|
||||
|
||||
if (crdtEnabled) {
|
||||
_seededRows = await _crdtSession!.db.transactionForUser(
|
||||
_seededRows = await _offlineSyncSession!.db.transactionForUser(
|
||||
_userId,
|
||||
(tx) async {
|
||||
return Types.db.upsert(
|
||||
_crdtSession!,
|
||||
_offlineSyncSession!,
|
||||
batch,
|
||||
conflictColumns: (t) => [t.id],
|
||||
updateColumns: (t) => [
|
||||
|
|
@ -252,7 +252,9 @@ class TypesTableBenchmark extends AsyncBenchmarkBase {
|
|||
}
|
||||
|
||||
Future<List<Types>> _selectTypes() async {
|
||||
final rows = await Types.db.find(crdtEnabled ? _crdtSession! : _plainSession);
|
||||
final rows = await Types.db.find(
|
||||
crdtEnabled ? _offlineSyncSession! : _plainSession,
|
||||
);
|
||||
if (rows.length != rowCount) {
|
||||
throw Exception('Rows count is ${rows.length} but expected $rowCount.');
|
||||
}
|
||||
|
|
@ -261,12 +263,12 @@ class TypesTableBenchmark extends AsyncBenchmarkBase {
|
|||
|
||||
Future<void> _bootstrapDatabase() async {
|
||||
await clearUserTables(_plainSession);
|
||||
final wrapped = CrdtDatabaseSession.wraps(
|
||||
final wrapped = OfflineSyncDatabaseSession.wraps(
|
||||
_plainSession,
|
||||
syncTables: benchmarkSyncTables,
|
||||
);
|
||||
await wrapped.db.initialize();
|
||||
_crdtSession = crdtEnabled ? wrapped : null;
|
||||
_offlineSyncSession = crdtEnabled ? wrapped : null;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -339,12 +341,12 @@ class TypesTableBenchmark extends AsyncBenchmarkBase {
|
|||
_seededRows = [];
|
||||
await clearUserTables(_plainSession);
|
||||
await _plainSession.db.unsafeExecute('VACUUM');
|
||||
final wrapped = CrdtDatabaseSession.wraps(
|
||||
final wrapped = OfflineSyncDatabaseSession.wraps(
|
||||
_plainSession,
|
||||
syncTables: benchmarkSyncTables,
|
||||
);
|
||||
await wrapped.db.initialize();
|
||||
_crdtSession = crdtEnabled ? wrapped : null;
|
||||
_offlineSyncSession = crdtEnabled ? wrapped : null;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ typedef MergeMeasurement = ({
|
|||
);
|
||||
}
|
||||
|
||||
/// Base harness for benchmarks of the merge path ([CrdtDatabase.mergeChanges]).
|
||||
/// Base harness for benchmarks of the merge path ([OfflineSyncDatabase.mergeChanges]).
|
||||
///
|
||||
/// Changes are attributed to a single simulated remote node whose HLCs always
|
||||
/// advance, so every merged change is newer than the local state and is
|
||||
|
|
@ -60,7 +60,7 @@ abstract class MergeScenarioBenchmark extends AsyncBenchmarkBase {
|
|||
late final File _dbFile;
|
||||
late final ClientDatabaseSession _plainSession;
|
||||
late final QueryCountingDatabase _countingDb;
|
||||
late CrdtDatabaseSession _crdtSession;
|
||||
late OfflineSyncDatabaseSession _offlineSyncSession;
|
||||
var _hasOpenSession = false;
|
||||
|
||||
final UuidValue _userId = const Uuid().v7obj();
|
||||
|
|
@ -102,7 +102,7 @@ abstract class MergeScenarioBenchmark extends AsyncBenchmarkBase {
|
|||
CrdtMergeInsert insertChangeFor(TableRow<UuidValue?> row) {
|
||||
final hlc = _nextRemoteHlc();
|
||||
return CrdtMergeInsert(
|
||||
uuidScopeId: _userId,
|
||||
uuidSpaceId: _userId,
|
||||
tableName: row.table.tableName,
|
||||
uuidRowId: row.id!,
|
||||
uuidNodeId: hlc.nodeId,
|
||||
|
|
@ -119,7 +119,7 @@ abstract class MergeScenarioBenchmark extends AsyncBenchmarkBase {
|
|||
) {
|
||||
final hlc = _nextRemoteHlc();
|
||||
return CrdtMergeUpdate(
|
||||
uuidScopeId: _userId,
|
||||
uuidSpaceId: _userId,
|
||||
tableName: row.table.tableName,
|
||||
uuidRowId: row.id!,
|
||||
uuidNodeId: hlc.nodeId,
|
||||
|
|
@ -133,7 +133,7 @@ abstract class MergeScenarioBenchmark extends AsyncBenchmarkBase {
|
|||
CrdtMergeDelete deleteChangeFor(TableRow<UuidValue?> row) {
|
||||
final hlc = _nextRemoteHlc();
|
||||
return CrdtMergeDelete(
|
||||
uuidScopeId: _userId,
|
||||
uuidSpaceId: _userId,
|
||||
tableName: row.table.tableName,
|
||||
uuidRowId: row.id!,
|
||||
uuidNodeId: hlc.nodeId,
|
||||
|
|
@ -147,9 +147,9 @@ abstract class MergeScenarioBenchmark extends AsyncBenchmarkBase {
|
|||
/// Seeds [rows] through the merge path so they exist locally with remote
|
||||
/// CRDT metadata, like rows previously received from the remote node.
|
||||
Future<void> mergeSeedRows(List<TableRow<UuidValue?>> rows) {
|
||||
return _crdtSession.db.mergeChanges(
|
||||
return _offlineSyncSession.db.mergeChanges(
|
||||
[for (final row in rows) insertChangeFor(row)],
|
||||
scopeId: _userId,
|
||||
spaceId: _userId,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -176,11 +176,11 @@ abstract class MergeScenarioBenchmark extends AsyncBenchmarkBase {
|
|||
|
||||
await clearUserTables(_plainSession);
|
||||
_countingDb = QueryCountingDatabase(_plainSession.db);
|
||||
_crdtSession = CrdtDatabaseSession(
|
||||
_offlineSyncSession = OfflineSyncDatabaseSession(
|
||||
_countingDb,
|
||||
syncTables: benchmarkSyncTables,
|
||||
);
|
||||
await _crdtSession.db.initialize();
|
||||
await _offlineSyncSession.db.initialize();
|
||||
|
||||
await onSetup();
|
||||
}
|
||||
|
|
@ -223,7 +223,7 @@ abstract class MergeScenarioBenchmark extends AsyncBenchmarkBase {
|
|||
final queriesBefore = _countingDb.queryCount;
|
||||
final rowsBefore = _countingDb.rowsRead;
|
||||
final rowsByTypeBefore = Map.of(_countingDb.rowsReadByType);
|
||||
await _crdtSession.db.mergeChanges(_mergeSet, scopeId: _userId);
|
||||
await _offlineSyncSession.db.mergeChanges(_mergeSet, spaceId: _userId);
|
||||
_timedQueries += _countingDb.queryCount - queriesBefore;
|
||||
_timedRowsRead += _countingDb.rowsRead - rowsBefore;
|
||||
for (final entry in _countingDb.rowsReadByType.entries) {
|
||||
|
|
@ -625,7 +625,7 @@ class SetDefaultMergeBenchmark extends MergeScenarioBenchmark {
|
|||
// HLC advancement alone cannot supersede a higher visibility flag. Advance
|
||||
// both on every delete/restore so no measured cycle becomes a stale no-op.
|
||||
return CrdtMergeDelete(
|
||||
uuidScopeId: _userId,
|
||||
uuidSpaceId: _userId,
|
||||
tableName: Town.t.tableName,
|
||||
uuidRowId: town.id!,
|
||||
uuidNodeId: hlc.nodeId,
|
||||
|
|
@ -640,11 +640,14 @@ class SetDefaultMergeBenchmark extends MergeScenarioBenchmark {
|
|||
|
||||
@override
|
||||
Future<void> validateCycle() async {
|
||||
final company = await Company.db.findById(_crdtSession, _companies.first.id!);
|
||||
final company = await Company.db.findById(
|
||||
_offlineSyncSession,
|
||||
_companies.first.id!,
|
||||
);
|
||||
if (company == null || company.townId != _expectedTownId) {
|
||||
throw StateError('$name cycle $_cycle did not project the company as expected.');
|
||||
}
|
||||
final visibleTown = await Town.db.findById(_crdtSession, _changedTown.id!);
|
||||
final visibleTown = await Town.db.findById(_offlineSyncSession, _changedTown.id!);
|
||||
if ((visibleTown == null) != _alternate) {
|
||||
throw StateError('$name cycle $_cycle did not apply its visibility change.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import 'benchmark.dart';
|
||||
import 'conversion.dart';
|
||||
import 'scope.dart';
|
||||
import 'space.dart';
|
||||
|
||||
void printPerformanceImpact(
|
||||
BenchmarkResults results, {
|
||||
|
|
@ -14,7 +14,7 @@ void printPerformanceImpact(
|
|||
'\n📊 ${results.operation.label.toUpperCase()} performance impact:',
|
||||
);
|
||||
final baselineTime = results.baseline;
|
||||
final crdtTime = results.crdt;
|
||||
final crdtTime = results.offlineSync;
|
||||
|
||||
final runDelayUs = crdtTime - baselineTime;
|
||||
final slowdown = runDelayUs / baselineTime * 100;
|
||||
|
|
@ -41,35 +41,35 @@ void printStorageImpact(
|
|||
_StorageStageComparison(
|
||||
title: 'INSERT only',
|
||||
baselineSize: benchmarkResults.baseline.sizeFor(StorageStage.insert),
|
||||
crdtSize: benchmarkResults.crdt.sizeFor(StorageStage.insert),
|
||||
crdtSize: benchmarkResults.offlineSync.sizeFor(StorageStage.insert),
|
||||
baselineIncrement: benchmarkResults.baseline.sizeFor(StorageStage.insert),
|
||||
crdtIncrement: benchmarkResults.crdt.sizeFor(StorageStage.insert),
|
||||
crdtIncrement: benchmarkResults.offlineSync.sizeFor(StorageStage.insert),
|
||||
unitCount: rowCount,
|
||||
unitLabel: 'inserted row',
|
||||
),
|
||||
_StorageStageComparison(
|
||||
title: 'INSERT + UPDATE',
|
||||
baselineSize: benchmarkResults.baseline.sizeFor(StorageStage.update),
|
||||
crdtSize: benchmarkResults.crdt.sizeFor(StorageStage.update),
|
||||
crdtSize: benchmarkResults.offlineSync.sizeFor(StorageStage.update),
|
||||
baselineIncrement:
|
||||
benchmarkResults.baseline.sizeFor(StorageStage.update) -
|
||||
benchmarkResults.baseline.previousSizeFor(StorageStage.update),
|
||||
crdtIncrement:
|
||||
benchmarkResults.crdt.sizeFor(StorageStage.update) -
|
||||
benchmarkResults.crdt.previousSizeFor(StorageStage.update),
|
||||
benchmarkResults.offlineSync.sizeFor(StorageStage.update) -
|
||||
benchmarkResults.offlineSync.previousSizeFor(StorageStage.update),
|
||||
unitCount: rowCount * TypesTableBenchmark.updatedColumnsPerRow,
|
||||
unitLabel: 'updated column',
|
||||
),
|
||||
_StorageStageComparison(
|
||||
title: 'INSERT + UPDATE + DELETE',
|
||||
baselineSize: benchmarkResults.baseline.sizeFor(StorageStage.delete),
|
||||
crdtSize: benchmarkResults.crdt.sizeFor(StorageStage.delete),
|
||||
crdtSize: benchmarkResults.offlineSync.sizeFor(StorageStage.delete),
|
||||
baselineIncrement:
|
||||
benchmarkResults.baseline.sizeFor(StorageStage.delete) -
|
||||
benchmarkResults.baseline.previousSizeFor(StorageStage.delete),
|
||||
crdtIncrement:
|
||||
benchmarkResults.crdt.sizeFor(StorageStage.delete) -
|
||||
benchmarkResults.crdt.previousSizeFor(StorageStage.delete),
|
||||
benchmarkResults.offlineSync.sizeFor(StorageStage.delete) -
|
||||
benchmarkResults.offlineSync.previousSizeFor(StorageStage.delete),
|
||||
unitCount: rowCount,
|
||||
unitLabel: 'deleted row',
|
||||
),
|
||||
|
|
@ -79,7 +79,7 @@ void printStorageImpact(
|
|||
print(
|
||||
' Base database size: '
|
||||
'${benchmarkResults.baseline.baseDatabaseSize.toFormattedStorageSize()} '
|
||||
'--> ${benchmarkResults.crdt.baseDatabaseSize.toFormattedStorageSize()}',
|
||||
'--> ${benchmarkResults.offlineSync.baseDatabaseSize.toFormattedStorageSize()}',
|
||||
);
|
||||
|
||||
for (final stage in stages) {
|
||||
|
|
@ -139,36 +139,36 @@ void printMergeImpact(
|
|||
if (runningInCI) print('```');
|
||||
}
|
||||
|
||||
void printScopeImpact(
|
||||
ScopeBenchmarkResults results, {
|
||||
void printSpaceImpact(
|
||||
SpaceBenchmarkResults results, {
|
||||
required int rowCount,
|
||||
bool runningInCI = false,
|
||||
}) {
|
||||
String compare(
|
||||
double baseline,
|
||||
double scoped,
|
||||
double spaceScoped,
|
||||
double unscoped,
|
||||
) {
|
||||
String overhead(double value) =>
|
||||
'${formatter2.format((value - baseline) / baseline * 100)}%';
|
||||
return '${baseline.toFormattedDuration()} --> '
|
||||
'${scoped.toFormattedDuration()} scoped (+${overhead(scoped)}) / '
|
||||
'${spaceScoped.toFormattedDuration()} space-scoped (+${overhead(spaceScoped)}) / '
|
||||
'${unscoped.toFormattedDuration()} unscoped (+${overhead(unscoped)})';
|
||||
}
|
||||
|
||||
print(
|
||||
'${runningInCI ? '```' : ''}'
|
||||
'\n🔭 SELECT scope impact '
|
||||
'\n🔭 SELECT space impact '
|
||||
'(${formatter0.format(results.noiseUsers)} extra users, '
|
||||
'${formatter0.format(results.noiseCrdtRows)} CRDT noise rows):',
|
||||
);
|
||||
print(
|
||||
' find all (${formatter0.format(rowCount)} rows): '
|
||||
'${compare(results.baseline.findAllMicros, results.scoped.findAllMicros, results.unscoped.findAllMicros)}',
|
||||
'${compare(results.baseline.findAllMicros, results.spaceScoped.findAllMicros, results.unscoped.findAllMicros)}',
|
||||
);
|
||||
print(
|
||||
' findById (per lookup): '
|
||||
'${compare(results.baseline.findByIdMicros, results.scoped.findByIdMicros, results.unscoped.findByIdMicros)}',
|
||||
'${compare(results.baseline.findByIdMicros, results.spaceScoped.findByIdMicros, results.unscoped.findByIdMicros)}',
|
||||
);
|
||||
if (runningInCI) print('```');
|
||||
}
|
||||
|
|
@ -177,26 +177,26 @@ class BenchmarkResults {
|
|||
const BenchmarkResults({
|
||||
required this.operation,
|
||||
required this.baseline,
|
||||
required this.crdt,
|
||||
required this.offlineSync,
|
||||
});
|
||||
|
||||
final Operation operation;
|
||||
final double baseline;
|
||||
final double crdt;
|
||||
final double offlineSync;
|
||||
}
|
||||
|
||||
class ScopeBenchmarkResults {
|
||||
const ScopeBenchmarkResults({
|
||||
class SpaceBenchmarkResults {
|
||||
const SpaceBenchmarkResults({
|
||||
required this.baseline,
|
||||
required this.scoped,
|
||||
required this.spaceScoped,
|
||||
required this.unscoped,
|
||||
required this.noiseUsers,
|
||||
required this.noiseCrdtRows,
|
||||
});
|
||||
|
||||
final ScopeMeasurement baseline;
|
||||
final ScopeMeasurement scoped;
|
||||
final ScopeMeasurement unscoped;
|
||||
final SpaceMeasurement baseline;
|
||||
final SpaceMeasurement spaceScoped;
|
||||
final SpaceMeasurement unscoped;
|
||||
final int noiseUsers;
|
||||
final int noiseCrdtRows;
|
||||
}
|
||||
|
|
@ -231,11 +231,11 @@ class MergeBenchmarkResults {
|
|||
class StorageBenchmarkResults {
|
||||
const StorageBenchmarkResults({
|
||||
required this.baseline,
|
||||
required this.crdt,
|
||||
required this.offlineSync,
|
||||
});
|
||||
|
||||
final StorageBenchmarkRun baseline;
|
||||
final StorageBenchmarkRun crdt;
|
||||
final StorageBenchmarkRun offlineSync;
|
||||
}
|
||||
|
||||
class _StorageStageComparison {
|
||||
|
|
|
|||
238
benchmark/utils/space.dart
Normal file
238
benchmark/utils/space.dart
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
// Production-shaped tombstone space benchmark: many users and a large
|
||||
// crdt_data_rows table, measuring scoped vs unscoped SELECT cost.
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:serverpod_database/serverpod_database.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:serverpod_offline_sync_test_client/serverpod_offline_sync_test_client.dart';
|
||||
|
||||
import 'benchmark.dart';
|
||||
import 'tables.dart';
|
||||
|
||||
/// Which session executes the measured selects.
|
||||
enum SpaceMode {
|
||||
/// Plain SQLite session without CRDT metadata.
|
||||
baseline,
|
||||
|
||||
/// CRDT session with a persistent user (single-user client pattern).
|
||||
spaceScoped,
|
||||
|
||||
/// CRDT session without a user space (server admin reads).
|
||||
unscoped,
|
||||
}
|
||||
|
||||
/// Timed results for one [SpaceMode], in average microseconds.
|
||||
typedef SpaceMeasurement = ({double findAllMicros, double findByIdMicros});
|
||||
|
||||
/// Measures SELECT cost against a database whose `crdt_data_rows` table holds
|
||||
/// production-shaped metadata instead of a single user's rows.
|
||||
///
|
||||
/// On top of the [rowCount] benchmark rows, the CRDT modes seed [noiseUsers]
|
||||
/// extra spaces tracking [noiseCrdtRows] unrelated rows spread across every
|
||||
/// synced table. Random UUIDs keep the noise ownership-conformant while still
|
||||
/// exercising large CRDT metadata tables.
|
||||
class TombstoneSpaceBenchmark {
|
||||
TombstoneSpaceBenchmark(
|
||||
this.name, {
|
||||
required this.mode,
|
||||
required this.rowCount,
|
||||
required this.noiseUsers,
|
||||
required this.noiseCrdtRows,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final SpaceMode mode;
|
||||
final int rowCount;
|
||||
final int noiseUsers;
|
||||
final int noiseCrdtRows;
|
||||
|
||||
static const _clientUrl = 'http://localhost:8081/';
|
||||
static const _warmupMillis = 100;
|
||||
static const _measurementMillis = 2000;
|
||||
static const _findByIdLookupsPerCycle = 100;
|
||||
|
||||
late final File _dbFile;
|
||||
late final ClientDatabaseSession _plainSession;
|
||||
OfflineSyncDatabaseSession? _offlineSyncSession;
|
||||
var _hasOpenSession = false;
|
||||
|
||||
final UuidValue _userId = const Uuid().v7obj();
|
||||
|
||||
List<Types> _seededRows = [];
|
||||
List<UuidValue> _sampleIds = [];
|
||||
|
||||
DatabaseSession get _activeSession =>
|
||||
mode == SpaceMode.baseline ? _plainSession : _offlineSyncSession!;
|
||||
|
||||
Future<void> _setup() async {
|
||||
final dbPath = p.join(
|
||||
Directory.systemTemp.path,
|
||||
'offline_sync_benchmark_$name.db',
|
||||
);
|
||||
_dbFile = File(dbPath);
|
||||
deleteDatabaseFiles(_dbFile);
|
||||
|
||||
_plainSession = await Client(_clientUrl).createSession(
|
||||
_dbFile.path,
|
||||
isDebugMode: true,
|
||||
);
|
||||
_hasOpenSession = true;
|
||||
|
||||
if (mode != SpaceMode.baseline) {
|
||||
_offlineSyncSession = OfflineSyncDatabaseSession.wraps(
|
||||
_plainSession,
|
||||
syncTables: benchmarkSyncTables,
|
||||
persistentUserId: mode == SpaceMode.spaceScoped ? _userId : null,
|
||||
);
|
||||
await _offlineSyncSession!.db.initialize();
|
||||
}
|
||||
|
||||
await _seedBenchmarkRows();
|
||||
if (mode != SpaceMode.baseline) {
|
||||
await _seedCrdtNoise();
|
||||
}
|
||||
|
||||
final sampleStep = (rowCount / _findByIdLookupsPerCycle).ceil();
|
||||
_sampleIds = [
|
||||
for (var i = 0; i < rowCount; i += sampleStep) _seededRows[i].id!,
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _seedBenchmarkRows() async {
|
||||
final baseTimestamp = DateTime.now();
|
||||
final rows = List.generate(
|
||||
rowCount,
|
||||
(i) => Types(
|
||||
aBool: i.isEven,
|
||||
aDateTime: baseTimestamp.add(Duration(seconds: i)),
|
||||
aText: 'Text $i',
|
||||
anInt: i,
|
||||
anInt64: BigInt.from(i),
|
||||
aReal: i.toDouble(),
|
||||
aBlob: ByteData(0),
|
||||
anEnum: null,
|
||||
optionalText: null,
|
||||
optionalUuid: const Uuid().v7obj(),
|
||||
),
|
||||
);
|
||||
|
||||
if (mode == SpaceMode.baseline) {
|
||||
_seededRows = await Types.db.insert(_plainSession, rows);
|
||||
return;
|
||||
}
|
||||
_seededRows = await _offlineSyncSession!.db.transactionForUser(_userId, (tx) {
|
||||
return Types.db.insert(_offlineSyncSession!, rows, transaction: tx);
|
||||
});
|
||||
}
|
||||
|
||||
/// Injects CRDT metadata noise with raw SQL, bypassing the recorder: the
|
||||
/// noise only needs to be visible to the tombstone predicates, which read
|
||||
/// `crdt_data_rows` alone.
|
||||
Future<void> _seedCrdtNoise() async {
|
||||
final db = _plainSession.db;
|
||||
final hiddenVisibility = CrdtDataRowVisibility.userDelete.index;
|
||||
|
||||
final benchSpace = await OfflineSyncSpace.db.findFirstRow(
|
||||
_plainSession,
|
||||
orderBy: (t) => t.id,
|
||||
);
|
||||
final benchUserId = benchSpace!.id!;
|
||||
await db.unsafeExecute('''
|
||||
WITH RECURSIVE n(i) AS (SELECT 0 UNION ALL SELECT i + 1 FROM n WHERE i < $noiseUsers - 1)
|
||||
INSERT INTO "offline_sync_spaces" ("currentNodeId") SELECT NULL FROM n
|
||||
''');
|
||||
await db.unsafeExecute('''
|
||||
WITH local_node(id) AS (
|
||||
SELECT "currentNodeId" FROM "offline_sync_spaces" WHERE "id" = $benchUserId
|
||||
)
|
||||
INSERT INTO "offline_sync_space_nodes" ("spaceId", "nodeId")
|
||||
SELECT u."id", local_node.id
|
||||
FROM "offline_sync_spaces" u
|
||||
CROSS JOIN local_node
|
||||
WHERE local_node.id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "offline_sync_space_nodes" sn
|
||||
WHERE sn."spaceId" = u."id" AND sn."nodeId" = local_node.id
|
||||
)
|
||||
''');
|
||||
|
||||
// Noise rows for every noise user, spread across all synced tables, with
|
||||
// 10% tombstoned. Random UUIDs keep them unrelated to the domain rows.
|
||||
await db.unsafeExecute('''
|
||||
WITH RECURSIVE n(i) AS (SELECT 0 UNION ALL SELECT i + 1 FROM n WHERE i < $noiseCrdtRows - 1),
|
||||
users(rowidx, uid, nodeId) AS (
|
||||
SELECT ROW_NUMBER() OVER (ORDER BY s."id") - 1, s."id", sn."nodeId"
|
||||
FROM "offline_sync_spaces" s
|
||||
JOIN "offline_sync_space_nodes" sn ON sn."spaceId" = s."id"
|
||||
WHERE s."id" <> $benchUserId
|
||||
),
|
||||
tbls(tblidx, tid) AS (
|
||||
SELECT ROW_NUMBER() OVER (ORDER BY "id") - 1, "id" FROM "crdt_schema_tables"
|
||||
)
|
||||
INSERT INTO "crdt_data_rows"
|
||||
("hlcDatetime", "hlcCounter", "spaceId", "tblId", "uuidRowId", "nodeId", "visibility")
|
||||
SELECT 0, 0, u.uid, t.tid, randomblob(16), u.nodeId,
|
||||
CASE WHEN n.i % 10 = 0 THEN $hiddenVisibility ELSE 0 END
|
||||
FROM n
|
||||
JOIN users u ON u.rowidx = n.i % $noiseUsers
|
||||
JOIN tbls t ON t.tblidx = n.i % (SELECT COUNT(*) FROM "crdt_schema_tables")
|
||||
''');
|
||||
|
||||
await db.unsafeExecute('ANALYZE');
|
||||
}
|
||||
|
||||
Future<void> _runFindAll() async {
|
||||
final rows = await Types.db.find(_activeSession);
|
||||
if (rows.length != rowCount) {
|
||||
throw Exception(
|
||||
'Space benchmark ($name) returned ${rows.length} rows '
|
||||
'but expected $rowCount.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runFindByIdBatch() async {
|
||||
for (final id in _sampleIds) {
|
||||
final row = await Types.db.findById(_activeSession, id);
|
||||
if (row == null) {
|
||||
throw Exception('Space benchmark ($name) could not find row $id.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<double> _measureQuery(Future<void> Function() run) async {
|
||||
Future<void> noPrepare() async {}
|
||||
await measurePreparedCycles(_warmupMillis, prepare: noPrepare, run: run);
|
||||
return measurePreparedCycles(
|
||||
_measurementMillis,
|
||||
prepare: noPrepare,
|
||||
run: run,
|
||||
);
|
||||
}
|
||||
|
||||
/// Seeds the database once and measures both query shapes.
|
||||
Future<SpaceMeasurement> measure() async {
|
||||
await _setup();
|
||||
try {
|
||||
final findAllMicros = await _measureQuery(_runFindAll);
|
||||
final findByIdBatchMicros = await _measureQuery(_runFindByIdBatch);
|
||||
return (
|
||||
findAllMicros: findAllMicros,
|
||||
findByIdMicros: findByIdBatchMicros / _sampleIds.length,
|
||||
);
|
||||
} finally {
|
||||
await _teardown();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _teardown() async {
|
||||
if (!_hasOpenSession) return;
|
||||
await _plainSession.close();
|
||||
_hasOpenSession = false;
|
||||
deleteDatabaseFiles(_dbFile);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// Keeps CRDT scope aligned with integration tests plus the benchmark table.
|
||||
// Keeps CRDT space aligned with integration tests plus the benchmark table.
|
||||
|
||||
import 'package:serverpod_database/serverpod_database.dart';
|
||||
import 'package:serverpod_offline_sync_test_client/serverpod_offline_sync_test_client.dart';
|
||||
|
||||
/// [Types] is benchmark-only; the rest mirrors `client_session.dart` sync scope.
|
||||
/// [Types] is benchmark-only; the rest mirrors `client_session.dart` sync space.
|
||||
List<Table> get benchmarkSyncTables => [
|
||||
Address.t,
|
||||
City.t,
|
||||
|
|
|
|||
Loading…
Reference in a new issue