1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-07 12:26:47 +01:00

perf(web): batch asset store changes (#7974)

* perf(web): batch asset store changes

* update external calls to assetstore
This commit is contained in:
Michel Heusschen 2024-03-15 17:11:29 +01:00 committed by GitHub
parent a3dfa27a53
commit 5a6b71dda3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 118 additions and 66 deletions

View file

@ -169,12 +169,12 @@
case AssetAction.UNARCHIVE: case AssetAction.UNARCHIVE:
case AssetAction.FAVORITE: case AssetAction.FAVORITE:
case AssetAction.UNFAVORITE: { case AssetAction.UNFAVORITE: {
assetStore.updateAsset(asset); assetStore.updateAssets([asset]);
break; break;
} }
case AssetAction.ADD: { case AssetAction.ADD: {
assetStore.addAsset(asset); assetStore.addAssets([asset]);
break; break;
} }
} }

View file

@ -46,22 +46,22 @@ const THUMBNAIL_HEIGHT = 235;
interface AddAsset { interface AddAsset {
type: 'add'; type: 'add';
value: AssetResponseDto; values: AssetResponseDto[];
} }
interface UpdateAsset { interface UpdateAsset {
type: 'update'; type: 'update';
value: AssetResponseDto; values: AssetResponseDto[];
} }
interface DeleteAsset { interface DeleteAsset {
type: 'delete'; type: 'delete';
value: string; values: string[];
} }
interface TrashAssets { interface TrashAssets {
type: 'trash'; type: 'trash';
value: string[]; values: string[];
} }
export const photoViewer = writable<HTMLImageElement | null>(null); export const photoViewer = writable<HTMLImageElement | null>(null);
@ -102,16 +102,16 @@ export class AssetStore {
connect() { connect() {
this.unsubscribers.push( this.unsubscribers.push(
websocketEvents.on('on_upload_success', (asset) => { websocketEvents.on('on_upload_success', (asset) => {
this.addPendingChanges({ type: 'add', value: asset }); this.addPendingChanges({ type: 'add', values: [asset] });
}), }),
websocketEvents.on('on_asset_trash', (ids) => { websocketEvents.on('on_asset_trash', (ids) => {
this.addPendingChanges({ type: 'trash', value: ids }); this.addPendingChanges({ type: 'trash', values: ids });
}), }),
websocketEvents.on('on_asset_update', (asset) => { 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) => { 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(() => { processPendingChanges = throttle(() => {
for (const { type, value } of this.pendingChanges) { for (const { type, values } of this.getPendingChangeBatches()) {
switch (type) { switch (type) {
case 'add': { case 'add': {
this.addAsset(value); this.addAssets(values);
break; break;
} }
case 'update': { case 'update': {
this.updateAsset(value); this.updateAssets(values);
break; break;
} }
case 'trash': { case 'trash': {
if (!this.options.isTrashed) { if (!this.options.isTrashed) {
this.removeAssets(value); this.removeAssets(values);
} }
break; break;
} }
case 'delete': { case 'delete': {
this.removeAssets([value]); this.removeAssets(values);
break; break;
} }
} }
@ -151,7 +178,7 @@ export class AssetStore {
this.pendingChanges = []; this.pendingChanges = [];
this.emit(true); this.emit(true);
}, 10_000); }, 2500);
async init(viewport: Viewport) { async init(viewport: Viewport) {
this.initialized = false; this.initialized = false;
@ -277,7 +304,11 @@ export class AssetStore {
return scrollTimeline ? delta : 0; return scrollTimeline ? delta : 0;
} }
addAsset(asset: AssetResponseDto): void { addAssets(assets: AssetResponseDto[]) {
const assetsToUpdate: AssetResponseDto[] = [];
const assetsToAdd: AssetResponseDto[] = [];
for (const asset of assets) {
if ( if (
this.assetToBucket[asset.id] || this.assetToBucket[asset.id] ||
this.options.userId || this.options.userId ||
@ -288,14 +319,23 @@ export class AssetStore {
) { ) {
// If asset is already in the bucket we don't need to recalculate // If asset is already in the bucket we don't need to recalculate
// asset store containers // asset store containers
this.updateAsset(asset); assetsToUpdate.push(asset);
} else {
assetsToAdd.push(asset);
}
}
this.updateAssets(assetsToUpdate);
this.addAssetsToBuckets(assetsToAdd);
}
private addAssetsToBuckets(assets: AssetResponseDto[]) {
if (assets.length === 0) {
return; return;
} }
const updatedBuckets = new Set<AssetBucket>();
this.addAssetToBucket(asset); for (const asset of assets) {
}
private addAssetToBucket(asset: AssetResponseDto) {
const timeBucket = DateTime.fromISO(asset.fileCreatedAt).toUTC().startOf('month').toString(); const timeBucket = DateTime.fromISO(asset.fileCreatedAt).toUTC().startOf('month').toString();
let bucket = this.getBucketByDate(timeBucket); let bucket = this.getBucketByDate(timeBucket);
@ -310,23 +350,27 @@ export class AssetStore {
}; };
this.buckets.push(bucket); this.buckets.push(bucket);
}
bucket.assets.push(asset);
this.assets.push(asset);
updatedBuckets.add(bucket);
}
this.buckets = this.buckets.sort((a, b) => { this.buckets = this.buckets.sort((a, b) => {
const aDate = DateTime.fromISO(a.bucketDate).toUTC(); const aDate = DateTime.fromISO(a.bucketDate).toUTC();
const bDate = DateTime.fromISO(b.bucketDate).toUTC(); const bDate = DateTime.fromISO(b.bucketDate).toUTC();
return bDate.diff(aDate).milliseconds; return bDate.diff(aDate).milliseconds;
}); });
}
bucket.assets.push(asset); for (const bucket of updatedBuckets) {
bucket.assets.sort((a, b) => { bucket.assets.sort((a, b) => {
const aDate = DateTime.fromISO(a.fileCreatedAt).toUTC(); const aDate = DateTime.fromISO(a.fileCreatedAt).toUTC();
const bDate = DateTime.fromISO(b.fileCreatedAt).toUTC(); const bDate = DateTime.fromISO(b.fileCreatedAt).toUTC();
return bDate.diff(aDate).milliseconds; 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); this.emit(true);
} }
@ -358,21 +402,29 @@ export class AssetStore {
return null; return null;
} }
updateAsset(_asset: AssetResponseDto) { updateAssets(assets: AssetResponseDto[]) {
if (assets.length === 0) {
return;
}
const assetsToReculculate: AssetResponseDto[] = [];
for (const _asset of assets) {
const asset = this.assets.find((asset) => asset.id === _asset.id); const asset = this.assets.find((asset) => asset.id === _asset.id);
if (!asset) { if (!asset) {
return; continue;
} }
const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt; const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt;
Object.assign(asset, _asset);
if (recalculate) { if (recalculate) {
this.removeAssets([asset.id]); assetsToReculculate.push(asset);
this.addAssetToBucket(_asset); }
return;
} }
Object.assign(asset, _asset); this.removeAssets(assetsToReculculate.map((asset) => asset.id));
this.emit(recalculate); this.addAssetsToBuckets(assetsToReculculate);
this.emit(assetsToReculculate.length > 0);
} }
removeAssets(ids: string[]) { removeAssets(ids: string[]) {