mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
test: bench inserts
This commit is contained in:
parent
0e8b19e269
commit
65f5daa32c
163
mobile-v2/lib/domain/entities/asset_isar.entity.dart
Normal file
163
mobile-v2/lib/domain/entities/asset_isar.entity.dart
Normal 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);
|
||||||
|
}
|
@ -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);
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
136
mobile-v2/lib/domain/repositories/asset_isar.repository.dart
Normal file
136
mobile-v2/lib/domain/repositories/asset_isar.repository.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
232
mobile-v2/test/benchmark_insert_test.dart
Normal file
232
mobile-v2/test/benchmark_insert_test.dart
Normal 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)),
|
||||||
|
);
|
||||||
|
}
|
119
mobile-v2/test/benchmark_query_test.dart
Normal file
119
mobile-v2/test/benchmark_query_test.dart
Normal 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)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
34
mobile-v2/test/test_utils.dart
Normal file
34
mobile-v2/test/test_utils.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user