test: bench inserts

This commit is contained in:
shenlong-tanwen 2025-02-26 12:56:25 +05:30
parent 0e8b19e269
commit 65f5daa32c
11 changed files with 804 additions and 4 deletions

View File

@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/asset.model.dart';
import 'package:immich_mobile/utils/collection_util.dart';
import 'package:isar/isar.dart';
part 'asset_isar.entity.g.dart';
@Collection()
class Asset {
Id get isarId => id ?? Isar.autoIncrement;
final int? id;
final String name;
final String hash;
final int? height;
final int? width;
@Enumerated(EnumType.ordinal)
final AssetType type;
final DateTime createdTime;
final DateTime modifiedTime;
final int duration;
// local only
final String? localId;
// remote only
final String? remoteId;
final String? livePhotoVideoId;
bool get isRemote => remoteId != null;
bool get isLocal => localId != null;
bool get isMerged => isRemote && isLocal;
bool get isImage => type == AssetType.image;
const Asset({
this.id,
required this.name,
required this.hash,
this.height,
this.width,
required this.type,
required this.createdTime,
required this.modifiedTime,
required this.duration,
this.localId,
this.remoteId,
this.livePhotoVideoId,
});
Asset copyWith({
int? id,
String? name,
String? hash,
int? height,
int? width,
AssetType? type,
DateTime? createdTime,
DateTime? modifiedTime,
int? duration,
ValueGetter<String?>? localId,
ValueGetter<String?>? remoteId,
String? livePhotoVideoId,
}) {
return Asset(
id: id ?? this.id,
name: name ?? this.name,
hash: hash ?? this.hash,
height: height ?? this.height,
width: width ?? this.width,
type: type ?? this.type,
createdTime: createdTime ?? this.createdTime,
modifiedTime: modifiedTime ?? this.modifiedTime,
duration: duration ?? this.duration,
localId: localId == null ? this.localId : localId(),
remoteId: remoteId == null ? this.remoteId : remoteId(),
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
);
}
Asset merge(Asset newAsset) {
final existingAsset = this;
assert(existingAsset.id != null, "Existing asset must be from the db");
final oldestCreationTime =
existingAsset.createdTime.isBefore(newAsset.createdTime)
? existingAsset.createdTime
: newAsset.createdTime;
if (newAsset.modifiedTime.isAfter(existingAsset.modifiedTime)) {
return newAsset.copyWith(
id: newAsset.id ?? existingAsset.id,
height: newAsset.height ?? existingAsset.height,
width: newAsset.width ?? existingAsset.width,
createdTime: oldestCreationTime,
localId: () => existingAsset.localId ?? newAsset.localId,
remoteId: () => existingAsset.remoteId ?? newAsset.remoteId,
);
}
return existingAsset.copyWith(
height: existingAsset.height ?? newAsset.height,
width: existingAsset.width ?? newAsset.width,
createdTime: oldestCreationTime,
localId: () => existingAsset.localId ?? newAsset.localId,
remoteId: () => existingAsset.remoteId ?? newAsset.remoteId,
);
}
@override
String toString() => """
{
"id": "${id ?? "-"}",
"remoteId": "${remoteId ?? "-"}",
"localId": "${localId ?? "-"}",
"name": "$name",
"hash": "$hash",
"height": ${height ?? "-"},
"width": ${width ?? "-"},
"type": "$type",
"createdTime": "$createdTime",
"modifiedTime": "$modifiedTime",
"duration": "$duration",
"livePhotoVideoId": "${livePhotoVideoId ?? "-"}",
}""";
@override
bool operator ==(covariant Asset other) {
if (identical(this, other)) return true;
return other.id == id &&
other.name == name &&
other.hash == hash &&
other.height == height &&
other.width == width &&
other.type == type &&
other.createdTime == createdTime &&
other.modifiedTime == modifiedTime &&
other.duration == duration &&
other.localId == localId &&
other.remoteId == remoteId &&
other.livePhotoVideoId == livePhotoVideoId;
}
@override
int get hashCode {
return id.hashCode ^
name.hashCode ^
hash.hashCode ^
height.hashCode ^
width.hashCode ^
type.hashCode ^
createdTime.hashCode ^
modifiedTime.hashCode ^
duration.hashCode ^
localId.hashCode ^
remoteId.hashCode ^
livePhotoVideoId.hashCode;
}
static int compareByHash(Asset a, Asset b) => a.hash.compareTo(b.hash);
static int compareByLocalId(Asset a, Asset b) =>
CollectionUtil.compareToNullable(a.localId, b.localId);
}

View File

@ -3,6 +3,9 @@ import 'dart:async';
import 'package:immich_mobile/domain/models/asset.model.dart'; import 'package:immich_mobile/domain/models/asset.model.dart';
abstract interface class IAssetRepository { abstract interface class IAssetRepository {
/// Batch upsert asset
Future<bool> upsert(Asset assets);
/// Batch upsert asset /// Batch upsert asset
Future<bool> upsertAll(Iterable<Asset> assets); Future<bool> upsertAll(Iterable<Asset> assets);

View File

@ -3,4 +3,6 @@ import 'package:immich_mobile/domain/models/render_list.model.dart';
abstract interface class IRenderListRepository { abstract interface class IRenderListRepository {
/// Streams the [RenderList] for the main timeline /// Streams the [RenderList] for the main timeline
Stream<RenderList> watchAll(); Stream<RenderList> watchAll();
Future<RenderList> getAll();
} }

View File

@ -88,6 +88,16 @@ class AssetRepository with LogMixin implements IAssetRepository {
Future<void> deleteIds(Iterable<int> ids) async { Future<void> deleteIds(Iterable<int> ids) async {
await _db.asset.deleteWhere((row) => row.id.isIn(ids)); await _db.asset.deleteWhere((row) => row.id.isIn(ids));
} }
@override
Future<bool> upsert(Asset asset) async {
final row = _toEntity(asset);
await _db.asset.insertOne(
row,
onConflict: DoUpdate((_) => row, target: [_db.asset.hash]),
);
return true;
}
} }
AssetCompanion _toEntity(Asset asset) { AssetCompanion _toEntity(Asset asset) {
@ -98,8 +108,8 @@ AssetCompanion _toEntity(Asset asset) {
height: Value(asset.height), height: Value(asset.height),
width: Value(asset.width), width: Value(asset.width),
type: asset.type, type: asset.type,
createdTime: asset.createdTime, createdTime: asset.createdTime.toUtc(),
modifiedTime: Value(asset.modifiedTime), modifiedTime: Value(asset.modifiedTime.toUtc()),
duration: Value(asset.duration), duration: Value(asset.duration),
localId: Value(asset.localId), localId: Value(asset.localId),
remoteId: Value(asset.remoteId), remoteId: Value(asset.remoteId),

View File

@ -0,0 +1,136 @@
import 'package:immich_mobile/domain/entities/asset_isar.entity.dart' as entity;
import 'package:immich_mobile/domain/interfaces/asset.interface.dart';
import 'package:immich_mobile/domain/models/asset.model.dart';
import 'package:isar/isar.dart';
class AssetIsarRepository implements IAssetRepository {
final Isar db;
const AssetIsarRepository({required this.db});
@override
Future<bool> deleteAll() async {
await db.writeTxn(() async {
await db.assets.clear();
});
return true;
}
@override
Future<void> deleteIds(Iterable<int> ids) async {
await db.writeTxn(() async {
await db.assets.deleteAll(ids.toList());
});
}
@override
Future<List<Asset>> getAll({int? offset, int? limit}) async {
return await db.assets
.where()
.offset(offset ?? 0)
.limit(limit ?? 100)
.findAll()
.then((value) => value.map(_toModel).toList());
}
@override
Future<List<Asset>> getForHashes(Iterable<String> hashes) async {
return await db.assets
.where()
.filter()
.anyOf(hashes, (asset, hash) => asset.hashEqualTo(hash))
.findAll()
.then((value) => value.map(_toModel).toList());
}
@override
Future<List<Asset>> getForLocalIds(Iterable<String> localIds) async {
return await db.assets
.where()
.filter()
.anyOf(localIds, (asset, localId) => asset.localIdEqualTo(localId))
.findAll()
.then((value) => value.map(_toModel).toList());
}
@override
Future<List<Asset>> getForRemoteIds(Iterable<String> remoteIds) async {
return await db.assets
.where()
.filter()
.anyOf(remoteIds, (asset, remoteId) => asset.remoteIdEqualTo(remoteId))
.findAll()
.then((value) => value.map(_toModel).toList());
}
@override
Future<bool> upsertAll(Iterable<Asset> assets) async {
await db.writeTxn(() async {
await db.assets.putAll(assets.toEntity());
});
return true;
}
@override
Future<bool> upsert(Asset assets) async {
await db.writeTxn(() async {
await db.assets.put(assets.toEntity());
});
return true;
}
}
Asset _toModel(entity.Asset entity) {
return Asset(
id: entity.id,
name: entity.name,
hash: entity.hash,
height: entity.height,
width: entity.width,
type: entity.type,
createdTime: entity.createdTime,
modifiedTime: entity.modifiedTime,
duration: entity.duration,
localId: entity.localId,
remoteId: entity.remoteId,
livePhotoVideoId: entity.livePhotoVideoId,
);
}
extension on Asset {
entity.Asset toEntity() {
return entity.Asset(
id: id,
name: name,
hash: hash,
height: height,
width: width,
type: type,
createdTime: createdTime,
modifiedTime: modifiedTime,
duration: duration,
localId: localId,
remoteId: remoteId,
livePhotoVideoId: livePhotoVideoId,
);
}
}
extension on Iterable<Asset> {
List<entity.Asset> toEntity() {
return map((asset) => entity.Asset(
id: asset.id,
name: asset.name,
hash: asset.hash,
height: asset.height,
width: asset.width,
type: asset.type,
createdTime: asset.createdTime,
modifiedTime: asset.modifiedTime,
duration: asset.duration,
localId: asset.localId,
remoteId: asset.remoteId,
livePhotoVideoId: asset.livePhotoVideoId,
)).toList();
}
}

View File

@ -57,4 +57,44 @@ class RenderListRepository with LogMixin implements IRenderListRepository {
return RenderList(elements: elements, modifiedTime: modified); return RenderList(elements: elements, modifiedTime: modified);
}); });
} }
@override
Future<RenderList> getAll() async {
final assetCountExp = _db.asset.id.count();
final createdTimeExp = _db.asset.createdTime;
final modifiedTimeExp = _db.asset.modifiedTime.max();
final monthYearExp = createdTimeExp.strftime('%m-%Y');
final query = _db.asset.selectOnly()
..addColumns([assetCountExp, createdTimeExp, modifiedTimeExp])
..groupBy([monthYearExp])
..orderBy([OrderingTerm.desc(createdTimeExp)]);
int lastAssetOffset = 0;
DateTime recentModifiedTime = DateTime(1);
final elements = await query.expand((row) {
final createdTime = row.read<DateTime>(createdTimeExp)!;
final assetCount = row.read(assetCountExp)!;
final modifiedTime = row.read(modifiedTimeExp)!;
final assetOffset = lastAssetOffset;
lastAssetOffset += assetCount;
// Get the recent modifed time. This is used to prevent unnecessary grid updates
if (modifiedTime.isAfter(recentModifiedTime)) {
recentModifiedTime = modifiedTime;
}
return [
RenderListMonthHeaderElement(date: createdTime),
RenderListAssetElement(
date: createdTime,
assetCount: assetCount,
assetOffset: assetOffset,
),
];
}).get();
return RenderList(elements: elements, modifiedTime: recentModifiedTime);
}
} }

View File

@ -39,7 +39,7 @@ packages:
source: hosted source: hosted
version: "2.5.0" version: "2.5.0"
async: async:
dependency: transitive dependency: "direct main"
description: description:
name: async name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
@ -62,6 +62,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.0.0" version: "9.0.0"
benchmarking:
dependency: "direct dev"
description:
name: benchmarking
sha256: "6b7f6955e8b0b6ce0dd4c3bc5611430aac017a74923669ad3687b718e5085c6a"
url: "https://pub.dev"
source: hosted
version: "0.6.1"
bloc: bloc:
dependency: transitive dependency: transitive
description: description:
@ -366,6 +374,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
faker:
dependency: "direct dev"
description:
name: faker
sha256: "544c34e9e1d322824156d5a8d451bc1bb778263b892aded24ec7ba77b0706624"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@ -593,6 +609,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
isar:
dependency: "direct main"
description:
name: isar
sha256: e17a9555bc7f22ff26568b8c64d019b4ffa2dc6bd4cb1c8d9b269aefd32e53ad
url: "https://pub.isar-community.dev"
source: hosted
version: "3.1.8"
isar_flutter_libs:
dependency: "direct main"
description:
name: isar_flutter_libs
sha256: "78710781e658ce4bff59b3f38c5b2735e899e627f4e926e1221934e77b95231a"
url: "https://pub.isar-community.dev"
source: hosted
version: "3.1.8"
isar_generator:
dependency: "direct dev"
description:
name: isar_generator
sha256: "484e73d3b7e81dbd816852fe0b9497333118a9aeb646fd2d349a62cc8980ffe1"
url: "https://pub.isar-community.dev"
source: hosted
version: "3.1.8"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -1333,6 +1373,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.5.0" version: "6.5.0"
xxh3:
dependency: transitive
description:
name: xxh3
sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:

View File

@ -7,6 +7,8 @@ version: 1.102.0+132
environment: environment:
sdk: '>=3.3.3 <4.0.0' sdk: '>=3.3.3 <4.0.0'
isar_version: &isar_version 3.1.8 # define the version to be used
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
@ -36,7 +38,7 @@ dependencies:
logging: ^1.3.0 logging: ^1.3.0
# Collection Utils # Collection Utils
collection: ^1.18.0 collection: ^1.18.0
async: ^2.12.0 async: ^2.11.0
# service_locator # service_locator
get_it: ^8.0.0 get_it: ^8.0.0
# Photo Manager # Photo Manager
@ -56,6 +58,12 @@ dependencies:
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
flutter_cache_manager: ^3.4.1 flutter_cache_manager: ^3.4.1
skeletonizer: ^1.4.2 skeletonizer: ^1.4.2
isar:
version: *isar_version
hosted: https://pub.isar-community.dev/
isar_flutter_libs: # contains Isar Core
version: *isar_version
hosted: https://pub.isar-community.dev/
openapi: openapi:
path: openapi path: openapi
@ -82,6 +90,11 @@ dev_dependencies:
custom_lint: ^0.6.4 custom_lint: ^0.6.4
immich_mobile_lint: immich_mobile_lint:
path: './immich_lint' path: './immich_lint'
benchmarking: ^0.6.1
faker: ^2.2.0
isar_generator:
version: *isar_version
hosted: https://pub.isar-community.dev/
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@ -0,0 +1,232 @@
import 'dart:io';
import 'package:collection/collection.dart';
// ignore: import_rule_drift
import 'package:drift/drift.dart';
// ignore: import_rule_drift
import 'package:drift/native.dart';
import 'package:faker/faker.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/asset.model.dart';
import 'package:immich_mobile/domain/repositories/asset.repository.dart';
import 'package:immich_mobile/domain/repositories/asset_isar.repository.dart';
import 'package:immich_mobile/domain/repositories/database.repository.dart';
import 'package:isar/isar.dart';
// import 'package:isar/isar.dart';
import 'package:sqlite3/sqlite3.dart';
import 'test_utils.dart';
List<Asset> _generateAssets(int count) {
final assets = <Asset>[];
final faker = Faker();
for (int i = 0; i < count; i++) {
assets.add(Asset(
id: i,
name: 'Asset $i',
hash: faker.guid.guid(),
height: faker.randomGenerator.integer(1000),
width: faker.randomGenerator.integer(1000),
type: faker.randomGenerator.element(AssetType.values),
createdTime: faker.date.dateTime(),
modifiedTime: faker.date.dateTime(),
duration: faker.randomGenerator.integer(100),
localId: faker.guid.guid(),
remoteId: faker.guid.guid(),
livePhotoVideoId: faker.guid.guid(),
));
}
return assets;
}
Future<void> _benchDriftInsertsBatched({
required String name,
required Iterable<List<Asset>> assets,
required DriftDatabaseRepository db,
}) async {
final repo = AssetRepository(db: db);
final sp = Stopwatch()..start();
for (final chunk in assets) {
await repo.upsertAll(chunk);
}
print('$name - ${sp.elapsed}');
}
Future<void> _benchIsarInsertsBatched({
required String name,
required Iterable<List<Asset>> assets,
required Isar db,
}) async {
final repo = AssetIsarRepository(db: db);
final sp = Stopwatch()..start();
for (final chunk in assets) {
await repo.upsertAll(chunk);
}
print('$name - ${sp.elapsed}');
}
Future<void> _benchDriftInsertsNonBatched({
required String name,
required List<Asset> assets,
required DriftDatabaseRepository db,
}) async {
final repo = AssetRepository(db: db);
final sp = Stopwatch()..start();
for (final chunk in assets) {
await repo.upsert(chunk);
}
print('$name - ${sp.elapsed}');
}
Future<void> _benchIsarInsertsNonBatched({
required String name,
required List<Asset> assets,
required Isar db,
}) async {
final repo = AssetIsarRepository(db: db);
final sp = Stopwatch()..start();
for (final chunk in assets) {
await repo.upsert(chunk);
}
print('$name - ${sp.elapsed}');
}
Future<void> _cleanup(File file) async {
if (await file.exists()) {
await file.delete();
}
}
void main() {
late DriftDatabaseRepository drift;
late Isar isar;
// ignore: avoid-local-functions
Future<void> setup() async {
drift = DriftDatabaseRepository(LazyDatabase(() {
sqlite3.tempDirectory = 'test/';
return NativeDatabase.createInBackground(File('test/test.sqlite'));
}));
isar = await TestUtils.initIsar();
}
// ignore: avoid-local-functions
Future<void> cleanupFiles() async {
await _cleanup(File('test/test.sqlite'));
await _cleanup(File('test/test.sqlite-shm'));
await _cleanup(File('test/test.sqlite-wal'));
await _cleanup(File('test/default.isar'));
}
// ignore: avoid-local-functions
Future<void> closeDb() async {
await drift.close();
if (isar.isOpen) {
await isar.close();
}
}
test('10K assets', () async {
await cleanupFiles();
await setup();
final assets = _generateAssets(10000);
await _benchDriftInsertsBatched(
name: 'Drift 10K assets batched - 1K slice',
assets: assets.slices(1000),
db: drift,
);
await _benchIsarInsertsBatched(
name: 'Isar 10K assets batched - 1K slice',
assets: assets.slices(1000),
db: isar,
);
await closeDb();
await cleanupFiles();
await setup();
await _benchDriftInsertsNonBatched(
name: 'Drift 10K assets non-batched',
assets: assets,
db: drift,
);
await _benchIsarInsertsNonBatched(
name: 'Isar 10K assets non-batched',
assets: assets,
db: isar,
);
});
test(
'100K assets',
() async {
await cleanupFiles();
await setup();
final assets = _generateAssets(100000);
await _benchDriftInsertsBatched(
name: 'Drift 100K assets batched - 10K slice',
assets: assets.slices(10000),
db: drift,
);
await _benchIsarInsertsBatched(
name: 'Isar 100K assets batched - 10K slice',
assets: assets.slices(10000),
db: isar,
);
await closeDb();
await cleanupFiles();
await setup();
await _benchDriftInsertsNonBatched(
name: 'Drift 100K assets non-batched',
assets: assets,
db: drift,
);
await _benchIsarInsertsNonBatched(
name: 'Isar 100K assets non-batched',
assets: assets,
db: isar,
);
},
timeout: Timeout(Duration(minutes: 5)),
);
test(
'1M assets',
() async {
await cleanupFiles();
await setup();
final assets = _generateAssets(1000000);
await _benchDriftInsertsBatched(
name: 'Drift 1M assets batched - 10K slice',
assets: assets.slices(10000),
db: drift,
);
await _benchIsarInsertsBatched(
name: 'Isar 1M assets batched - 10K slice',
assets: assets.slices(10000),
db: isar,
);
await closeDb();
await cleanupFiles();
await setup();
await _benchDriftInsertsNonBatched(
name: 'Drift 1M assets non-batched',
assets: assets,
db: drift,
);
await _benchIsarInsertsNonBatched(
name: 'Isar 1M assets non-batched',
assets: assets,
db: isar,
);
},
timeout: Timeout(Duration(minutes: 25)),
);
}

View File

@ -0,0 +1,119 @@
import 'dart:io';
// ignore: import_rule_drift
import 'package:benchmarking/benchmarking.dart';
// ignore: import_rule_drift
import 'package:drift/drift.dart';
// ignore: import_rule_drift
import 'package:drift/native.dart';
import 'package:faker/faker.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/interfaces/renderlist.interface.dart';
import 'package:immich_mobile/domain/models/asset.model.dart';
import 'package:immich_mobile/domain/repositories/asset.repository.dart';
import 'package:immich_mobile/domain/repositories/database.repository.dart';
import 'package:immich_mobile/domain/repositories/renderlist.repository.dart';
import 'package:sqlite3/sqlite3.dart';
List<Asset> _generateAssets(int count) {
final assets = <Asset>[];
final faker = Faker();
for (int i = 0; i < count; i++) {
assets.add(Asset(
id: i,
name: 'Asset $i',
hash: faker.guid.guid(),
height: faker.randomGenerator.integer(1000),
width: faker.randomGenerator.integer(1000),
type: faker.randomGenerator.element(AssetType.values),
createdTime: faker.date.dateTime(minYear: 2010, maxYear: 2024),
modifiedTime: faker.date.dateTime(minYear: 2010, maxYear: 2024),
duration: faker.randomGenerator.integer(100),
localId: faker.guid.guid(),
remoteId: faker.guid.guid(),
livePhotoVideoId: faker.guid.guid(),
));
}
return assets;
}
Future<void> _driftInserts({
required List<Asset> assets,
required DriftDatabaseRepository db,
}) async {
final repo = AssetRepository(db: db);
await repo.upsertAll(assets);
}
void main() {
late DriftDatabaseRepository drift;
late File file;
setUp(() {
file = File('test/test.sqlite');
drift = DriftDatabaseRepository(LazyDatabase(() {
sqlite3.tempDirectory = 'test/';
return NativeDatabase.createInBackground(file);
}));
});
tearDown(() async {
for (final table in drift.allTables) {
await drift.delete(table).go();
}
await drift.close();
});
group('Generate RenderList', () {
test('10K assets', () async {
List<Asset> assets = [];
final IRenderListRepository repo = RenderListRepository(db: drift);
final result = await asyncBenchmark(
'Generate RenderList for 10K assets',
() async {
await repo.getAll();
},
setup: () async {
assets = _generateAssets(10000);
await _driftInserts(assets: assets, db: drift);
},
);
result.report(units: 10000);
});
test('Drift 100K assets', () async {
List<Asset> assets = [];
await _driftInserts(assets: assets, db: drift);
final IRenderListRepository repo = RenderListRepository(db: drift);
final result = await asyncBenchmark(
'Generate RenderList for 100K assets',
() async {
await repo.getAll();
},
setup: () async {
assets = _generateAssets(100000);
await _driftInserts(assets: assets, db: drift);
},
);
result.report(units: 100000);
});
test(
'Drift 1M assets',
() async {
List<Asset> assets = [];
final IRenderListRepository repo = RenderListRepository(db: drift);
final result = await asyncBenchmark(
'Generate RenderList for 1M assets',
() async {
await repo.getAll();
},
setup: () async {
assets = _generateAssets(1000000);
await _driftInserts(assets: assets, db: drift);
},
);
result.report(units: 1000000);
},
timeout: Timeout(Duration(minutes: 2)),
);
});
}

View File

@ -0,0 +1,34 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/entities/asset_isar.entity.dart';
import 'package:isar/isar.dart';
abstract final class TestUtils {
const TestUtils._();
/// Downloads Isar binaries (if required) and initializes a new Isar db
static Future<Isar> initIsar() async {
await Isar.initializeIsarCore(download: true);
final instance = Isar.getInstance();
if (instance != null) {
return instance;
}
final db = await Isar.open(
[AssetSchema],
directory: "test/",
maxSizeMiB: 1024,
inspector: false,
);
// Clear and close db on test end
addTearDown(() async {
if (!db.isOpen) {
return;
}
await db.writeTxn(() async => await db.clear());
await db.close();
});
return db;
}
}