Optimize SyncFavorite to only update a column

This commit is contained in:
Ming Ming 2022-07-28 03:00:42 +08:00
parent 450f43694a
commit 49b5901149
9 changed files with 224 additions and 75 deletions

View file

@ -113,10 +113,11 @@ class ListFavoriteBloc
emit(ListFavoriteBlocSuccess(ev.account, remote));
if (cache != null) {
CacheFavorite(_c)(ev.account, remote, cache: cache)
CacheFavorite(_c)(ev.account, remote.map((f) => f.fileId!))
.onError((e, stackTrace) {
_log.shout(
"[_onEventQuery] Failed while CacheFavorite", e, stackTrace);
return -1;
});
}
} catch (e, stackTrace) {

View file

@ -291,7 +291,7 @@ class ScanAccountDirBloc
// no data in this bloc, ignore
return;
}
if ((ev.newFavorites + ev.removedFavorites).any(_isFileOfInterest)) {
if (ev.account.compareServerIdentity(account)) {
_refreshThrottler.trigger(
maxResponceTime: const Duration(seconds: 3),
maxPendingCount: 10,

View file

@ -12,6 +12,13 @@ class Favorite with EquatableMixin {
"fileId: '$fileId', "
"}";
Favorite copyWith({
int? fileId,
}) =>
Favorite(
fileId: fileId ?? this.fileId,
);
@override
get props => [
fileId,

View file

@ -110,12 +110,9 @@ class ShareRemovedEvent {
}
class FavoriteResyncedEvent {
const FavoriteResyncedEvent(
this.account, this.newFavorites, this.removedFavorites);
const FavoriteResyncedEvent(this.account);
final Account account;
final List<File> newFavorites;
final List<File> removedFavorites;
}
class ThemeChangedEvent {}

View file

@ -5,81 +5,92 @@ import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/list_util.dart' as list_util;
import 'package:nc_photos/use_case/list_favorite_offline.dart';
import 'package:nc_photos/object_extension.dart';
class CacheFavorite {
CacheFavorite(this._c)
: assert(require(_c)),
assert(ListFavoriteOffline.require(_c));
CacheFavorite(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
/// Cache favorites
Future<void> call(
Account account,
List<File> remote, {
List<File>? cache,
}) async {
cache ??= await ListFavoriteOffline(_c)(account);
final remoteSorted =
remote.sorted((a, b) => a.fileId!.compareTo(b.fileId!));
final cacheSorted = cache.sorted((a, b) => a.fileId!.compareTo(b.fileId!));
final result = list_util.diffWith<File>(
cacheSorted, remoteSorted, (a, b) => a.fileId!.compareTo(b.fileId!));
final newFavorites = result.item1;
final removedFavorites =
result.item2.map((f) => f.copyWith(isFavorite: false)).toList();
final newFileIds = newFavorites.map((f) => f.fileId!).toList();
final removedFileIds = removedFavorites.map((f) => f.fileId!).toList();
if (newFileIds.isEmpty && removedFileIds.isEmpty) {
return;
}
await _c.sqliteDb.use((db) async {
final rowIds = await db.accountFileRowIdsByFileIds(
newFileIds + removedFileIds,
appAccount: account,
);
final rowIdsMap =
Map.fromEntries(rowIds.map((e) => MapEntry(e.fileId, e)));
await db.batch((batch) {
for (final id in newFileIds) {
try {
batch.update(
db.accountFiles,
const sql.AccountFilesCompanion(isFavorite: sql.Value(true)),
where: (sql.$AccountFilesTable t) =>
t.rowId.equals(rowIdsMap[id]!.accountFileRowId),
);
} catch (e, stackTrace) {
_log.shout("[call] File not found in DB: $id", e, stackTrace);
}
}
for (final id in removedFileIds) {
try {
batch.update(
db.accountFiles,
const sql.AccountFilesCompanion(isFavorite: sql.Value(null)),
where: (sql.$AccountFilesTable t) =>
t.rowId.equals(rowIdsMap[id]!.accountFileRowId),
);
} catch (e, stackTrace) {
_log.shout("[call] File not found in DB: $id", e, stackTrace);
}
}
});
/// Cache favorites using results from remote
///
/// Return number of files updated
Future<int> call(Account account, Iterable<int> remoteFileIds) async {
_log.info("[call] Cache favorites");
final remote = remoteFileIds.sorted(Comparable.compare);
final updateCount = await _c.sqliteDb.use((db) async {
final dbAccount = await db.accountOf(account);
final cache = await _getCacheFavorites(db, dbAccount);
final cacheMap =
Map.fromEntries(cache.map((e) => MapEntry(e.fileId, e.rowId)));
final diff =
list_util.diff(cacheMap.keys.sorted(Comparable.compare), remote);
final newFileIds = diff.item1;
_log.info("[call] New favorites: ${newFileIds.toReadableString()}");
final removedFildIds = diff.item2;
_log.info(
"[call] Removed favorites: ${removedFildIds.toReadableString()}");
var updateCount = 0;
if (newFileIds.isNotEmpty) {
final rowIds = await db.accountFileRowIdsByFileIds(newFileIds,
sqlAccount: dbAccount);
final count = await (db.update(db.accountFiles)
..where(
(t) => t.rowId.isIn(rowIds.map((id) => id.accountFileRowId))))
.write(
const sql.AccountFilesCompanion(isFavorite: sql.Value(true)));
_log.info("[call] Updated $count row (new)");
updateCount += count;
}
if (removedFildIds.isNotEmpty) {
final count = await (db.update(db.accountFiles)
..where((t) =>
t.account.equals(dbAccount.rowId) &
t.file.isIn(removedFildIds.map((id) => cacheMap[id]))))
.write(
const sql.AccountFilesCompanion(isFavorite: sql.Value(false)));
_log.info("[call] Updated $count row (remove)");
updateCount += count;
}
return updateCount;
});
KiwiContainer()
.resolve<EventBus>()
.fire(FavoriteResyncedEvent(account, newFavorites, removedFavorites));
if (updateCount > 0) {
KiwiContainer().resolve<EventBus>().fire(FavoriteResyncedEvent(account));
}
return updateCount;
}
Future<List<_FileRowIdWithFileId>> _getCacheFavorites(
sql.SqliteDb db, sql.Account dbAccount) async {
final query = db.queryFiles().run((q) {
q
..setQueryMode(sql.FilesQueryMode.expression,
expressions: [db.files.rowId, db.files.fileId])
..setSqlAccount(dbAccount)
..byFavorite(true);
return q.build();
});
return await query
.map((r) => _FileRowIdWithFileId(
r.read(db.files.rowId)!, r.read(db.files.fileId)!))
.get();
}
final DiContainer _c;
static final _log = Logger("use_case.cache_favorite.CacheFavorite");
}
class _FileRowIdWithFileId {
const _FileRowIdWithFileId(this.rowId, this.fileId);
final int rowId;
final int fileId;
}

View file

@ -1,22 +1,34 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/use_case/cache_favorite.dart';
import 'package:nc_photos/use_case/list_favorite.dart';
class SyncFavorite {
SyncFavorite(this._c)
: assert(require(_c)),
assert(CacheFavorite.require(_c)),
assert(ListFavorite.require(_c));
assert(CacheFavorite.require(_c));
static bool require(DiContainer c) => true;
static bool require(DiContainer c) => DiContainer.has(c, DiType.favoriteRepo);
/// Sync favorites in AppDb with remote server
Future<void> call(Account account) async {
/// Sync favorites in cache db with remote server
///
/// Return number of files updated
Future<int> call(Account account) async {
_log.info("[call] Sync favorites with remote");
final remote = await ListFavorite(_c)(account);
await CacheFavorite(_c)(account, remote);
final remote = await _getRemoteFavoriteFileIds(account);
return await CacheFavorite(_c)(account, remote);
}
Future<List<int>> _getRemoteFavoriteFileIds(Account account) async {
final fileIds = <int>[];
for (final r in account.roots) {
final favorites = await _c.favoriteRepo
.list(account, File(path: file_util.unstripPath(account, r)));
fileIds.addAll(favorites.map((f) => f.fileId));
}
return fileIds;
}
final DiContainer _c;

View file

@ -7,6 +7,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
@ -98,6 +99,29 @@ class MockEventBus implements EventBus {
final _streamController = StreamController.broadcast();
}
class MockFavoriteRepo implements FavoriteRepo {
@override
FavoriteDataSource get dataSrc => throw UnimplementedError();
@override
Future<List<Favorite>> list(Account account, File dir) {
throw UnimplementedError();
}
}
class MockFavoriteMemoryRepo extends MockFavoriteRepo {
MockFavoriteMemoryRepo([
List<Favorite> initialData = const [],
]) : favorite = initialData.map((a) => a.copyWith()).toList();
@override
list(Account account, File dir) async {
return favorite.toList();
}
final List<Favorite> favorite;
}
/// Mock of [FileDataSource] where all methods will throw UnimplementedError
class MockFileDataSource implements FileDataSource {
@override

View file

@ -38,6 +38,7 @@ class FilesBuilder {
DateTime? lastModified,
bool isCollection = false,
bool hasPreview = true,
bool? isFavorite,
String ownerId = "admin",
String? ownerDisplayName,
Metadata? metadata,
@ -52,6 +53,7 @@ class FilesBuilder {
isCollection: isCollection,
hasPreview: hasPreview,
fileId: fileId++,
isFavorite: isFavorite,
ownerId: ownerId.toCi(),
ownerDisplayName: ownerDisplayName ?? ownerId.toString(),
metadata: metadata,
@ -65,6 +67,7 @@ class FilesBuilder {
String? etag,
DateTime? lastModified,
bool hasPreview = true,
bool? isFavorite,
String ownerId = "admin",
String? ownerDisplayName,
}) =>
@ -75,6 +78,7 @@ class FilesBuilder {
etag: etag,
lastModified: lastModified,
hasPreview: hasPreview,
isFavorite: isFavorite,
ownerId: ownerId,
ownerDisplayName: ownerDisplayName,
);
@ -85,6 +89,7 @@ class FilesBuilder {
String? etag,
DateTime? lastModified,
bool hasPreview = true,
bool? isFavorite,
String ownerId = "admin",
String? ownerDisplayName,
OrNull<Metadata>? metadata,
@ -96,6 +101,7 @@ class FilesBuilder {
etag: etag,
lastModified: lastModified,
hasPreview: hasPreview,
isFavorite: isFavorite,
ownerId: ownerId,
ownerDisplayName: ownerDisplayName,
metadata: metadata?.obj ??
@ -111,6 +117,7 @@ class FilesBuilder {
int contentLength = 1024,
String? etag,
DateTime? lastModified,
bool? isFavorite,
String ownerId = "admin",
String? ownerDisplayName,
}) =>
@ -120,6 +127,7 @@ class FilesBuilder {
lastModified: lastModified,
isCollection: true,
hasPreview: false,
isFavorite: isFavorite,
ownerId: ownerId,
ownerDisplayName: ownerDisplayName,
);

View file

@ -0,0 +1,89 @@
import 'package:drift/drift.dart' as sql;
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
import 'package:nc_photos/use_case/sync_favorite.dart';
import 'package:test/test.dart';
import '../mock_type.dart';
import '../test_util.dart' as util;
void main() {
group("SyncFavorite", () {
test("new", _new);
test("remove", _remove);
});
}
Future<void> _new() async {
final account = util.buildAccount();
final files = (util.FilesBuilder(initialFileId: 100)
..addDir("admin")
..addJpeg("admin/test1.jpg", isFavorite: true)
..addJpeg("admin/test2.jpg", isFavorite: true)
..addJpeg("admin/test3.jpg")
..addJpeg("admin/test4.jpg")
..addJpeg("admin/test5.jpg"))
.build();
final c = DiContainer(
favoriteRepo: MockFavoriteMemoryRepo([
const Favorite(fileId: 101),
const Favorite(fileId: 102),
const Favorite(fileId: 103),
const Favorite(fileId: 104),
]),
sqliteDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await util.insertFiles(c.sqliteDb, account, files);
});
await SyncFavorite(c)(account);
expect(
await listSqliteDbFavoriteFileIds(c.sqliteDb),
{101, 102, 103, 104},
);
}
Future<void> _remove() async {
final account = util.buildAccount();
final files = (util.FilesBuilder(initialFileId: 100)
..addDir("admin")
..addJpeg("admin/test1.jpg", isFavorite: true)
..addJpeg("admin/test2.jpg", isFavorite: true)
..addJpeg("admin/test3.jpg", isFavorite: true)
..addJpeg("admin/test4.jpg", isFavorite: true)
..addJpeg("admin/test5.jpg"))
.build();
final c = DiContainer(
favoriteRepo: MockFavoriteMemoryRepo([
const Favorite(fileId: 103),
const Favorite(fileId: 104),
]),
sqliteDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await util.insertFiles(c.sqliteDb, account, files);
});
await SyncFavorite(c)(account);
expect(
await listSqliteDbFavoriteFileIds(c.sqliteDb),
{103, 104},
);
}
Future<Set<int>> listSqliteDbFavoriteFileIds(sql.SqliteDb db) async {
final query = db.selectOnly(db.files).join([
sql.innerJoin(
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
])
..addColumns([db.files.fileId])
..where(db.accountFiles.isFavorite.equals(true));
return (await query.map((r) => r.read(db.files.fileId)!).get()).toSet();
}