From 5a6b71dda30caa6366c57e2d63bf556aafb5a0c8 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:11:29 +0100 Subject: [PATCH] perf(web): batch asset store changes (#7974) * perf(web): batch asset store changes * update external calls to assetstore --- .../components/photos-page/asset-grid.svelte | 4 +- web/src/lib/stores/assets.store.ts | 180 +++++++++++------- 2 files changed, 118 insertions(+), 66 deletions(-) diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 82224babea..c21439d24d 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -169,12 +169,12 @@ case AssetAction.UNARCHIVE: case AssetAction.FAVORITE: case AssetAction.UNFAVORITE: { - assetStore.updateAsset(asset); + assetStore.updateAssets([asset]); break; } case AssetAction.ADD: { - assetStore.addAsset(asset); + assetStore.addAssets([asset]); break; } } diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index a61f9ed35a..b35da65c1b 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -46,22 +46,22 @@ const THUMBNAIL_HEIGHT = 235; interface AddAsset { type: 'add'; - value: AssetResponseDto; + values: AssetResponseDto[]; } interface UpdateAsset { type: 'update'; - value: AssetResponseDto; + values: AssetResponseDto[]; } interface DeleteAsset { type: 'delete'; - value: string; + values: string[]; } interface TrashAssets { type: 'trash'; - value: string[]; + values: string[]; } export const photoViewer = writable(null); @@ -102,16 +102,16 @@ export class AssetStore { connect() { this.unsubscribers.push( websocketEvents.on('on_upload_success', (asset) => { - this.addPendingChanges({ type: 'add', value: asset }); + this.addPendingChanges({ type: 'add', values: [asset] }); }), websocketEvents.on('on_asset_trash', (ids) => { - this.addPendingChanges({ type: 'trash', value: ids }); + this.addPendingChanges({ type: 'trash', values: ids }); }), websocketEvents.on('on_asset_update', (asset) => { - this.addPendingChanges({ type: 'update', value: asset }); + this.addPendingChanges({ type: 'update', values: [asset] }); }), websocketEvents.on('on_asset_delete', (id: string) => { - this.addPendingChanges({ type: 'delete', value: id }); + this.addPendingChanges({ type: 'delete', values: [id] }); }), ); } @@ -122,28 +122,55 @@ export class AssetStore { } } + private getPendingChangeBatches() { + const batches: PendingChange[] = []; + let batch: PendingChange | undefined; + + for (const { type, values: _values } of this.pendingChanges) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const values = _values as any[]; + + if (batch && batch.type !== type) { + batches.push(batch); + batch = undefined; + } + + if (batch) { + batch.values.push(...values); + } else { + batch = { type, values }; + } + } + + if (batch) { + batches.push(batch); + } + + return batches; + } + processPendingChanges = throttle(() => { - for (const { type, value } of this.pendingChanges) { + for (const { type, values } of this.getPendingChangeBatches()) { switch (type) { case 'add': { - this.addAsset(value); + this.addAssets(values); break; } case 'update': { - this.updateAsset(value); + this.updateAssets(values); break; } case 'trash': { if (!this.options.isTrashed) { - this.removeAssets(value); + this.removeAssets(values); } break; } case 'delete': { - this.removeAssets([value]); + this.removeAssets(values); break; } } @@ -151,7 +178,7 @@ export class AssetStore { this.pendingChanges = []; this.emit(true); - }, 10_000); + }, 2500); async init(viewport: Viewport) { this.initialized = false; @@ -277,56 +304,73 @@ export class AssetStore { return scrollTimeline ? delta : 0; } - addAsset(asset: AssetResponseDto): void { - if ( - this.assetToBucket[asset.id] || - this.options.userId || - this.options.personId || - this.options.albumId || - isMismatched(this.options.isArchived, asset.isArchived) || - isMismatched(this.options.isFavorite, asset.isFavorite) - ) { - // If asset is already in the bucket we don't need to recalculate - // asset store containers - this.updateAsset(asset); - return; + addAssets(assets: AssetResponseDto[]) { + const assetsToUpdate: AssetResponseDto[] = []; + const assetsToAdd: AssetResponseDto[] = []; + + for (const asset of assets) { + if ( + this.assetToBucket[asset.id] || + this.options.userId || + this.options.personId || + this.options.albumId || + isMismatched(this.options.isArchived, asset.isArchived) || + isMismatched(this.options.isFavorite, asset.isFavorite) + ) { + // If asset is already in the bucket we don't need to recalculate + // asset store containers + assetsToUpdate.push(asset); + } else { + assetsToAdd.push(asset); + } } - this.addAssetToBucket(asset); + this.updateAssets(assetsToUpdate); + this.addAssetsToBuckets(assetsToAdd); } - private addAssetToBucket(asset: AssetResponseDto) { - const timeBucket = DateTime.fromISO(asset.fileCreatedAt).toUTC().startOf('month').toString(); - let bucket = this.getBucketByDate(timeBucket); + private addAssetsToBuckets(assets: AssetResponseDto[]) { + if (assets.length === 0) { + return; + } + const updatedBuckets = new Set(); - if (!bucket) { - bucket = { - bucketDate: timeBucket, - bucketHeight: THUMBNAIL_HEIGHT, - bucketCount: 0, - assets: [], - cancelToken: null, - position: BucketPosition.Unknown, - }; + for (const asset of assets) { + const timeBucket = DateTime.fromISO(asset.fileCreatedAt).toUTC().startOf('month').toString(); + let bucket = this.getBucketByDate(timeBucket); - this.buckets.push(bucket); - this.buckets = this.buckets.sort((a, b) => { - const aDate = DateTime.fromISO(a.bucketDate).toUTC(); - const bDate = DateTime.fromISO(b.bucketDate).toUTC(); + if (!bucket) { + bucket = { + bucketDate: timeBucket, + bucketHeight: THUMBNAIL_HEIGHT, + bucketCount: 0, + assets: [], + cancelToken: null, + position: BucketPosition.Unknown, + }; + + this.buckets.push(bucket); + } + + bucket.assets.push(asset); + this.assets.push(asset); + updatedBuckets.add(bucket); + } + + this.buckets = this.buckets.sort((a, b) => { + const aDate = DateTime.fromISO(a.bucketDate).toUTC(); + const bDate = DateTime.fromISO(b.bucketDate).toUTC(); + return bDate.diff(aDate).milliseconds; + }); + + for (const bucket of updatedBuckets) { + bucket.assets.sort((a, b) => { + const aDate = DateTime.fromISO(a.fileCreatedAt).toUTC(); + const bDate = DateTime.fromISO(b.fileCreatedAt).toUTC(); return bDate.diff(aDate).milliseconds; }); } - bucket.assets.push(asset); - bucket.assets.sort((a, b) => { - const aDate = DateTime.fromISO(a.fileCreatedAt).toUTC(); - const bDate = DateTime.fromISO(b.fileCreatedAt).toUTC(); - return bDate.diff(aDate).milliseconds; - }); - - // If we added an asset to the store, we need to recalculate - // asset store containers - this.assets.push(asset); this.emit(true); } @@ -358,21 +402,29 @@ export class AssetStore { return null; } - updateAsset(_asset: AssetResponseDto) { - const asset = this.assets.find((asset) => asset.id === _asset.id); - if (!asset) { + updateAssets(assets: AssetResponseDto[]) { + if (assets.length === 0) { return; } + const assetsToReculculate: AssetResponseDto[] = []; - const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt; - if (recalculate) { - this.removeAssets([asset.id]); - this.addAssetToBucket(_asset); - return; + for (const _asset of assets) { + const asset = this.assets.find((asset) => asset.id === _asset.id); + if (!asset) { + continue; + } + + const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt; + Object.assign(asset, _asset); + + if (recalculate) { + assetsToReculculate.push(asset); + } } - Object.assign(asset, _asset); - this.emit(recalculate); + this.removeAssets(assetsToReculculate.map((asset) => asset.id)); + this.addAssetsToBuckets(assetsToReculculate); + this.emit(assetsToReculculate.length > 0); } removeAssets(ids: string[]) {