1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-06 03:46: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,56 +304,73 @@ export class AssetStore {
return scrollTimeline ? delta : 0; return scrollTimeline ? delta : 0;
} }
addAsset(asset: AssetResponseDto): void { addAssets(assets: AssetResponseDto[]) {
if ( const assetsToUpdate: AssetResponseDto[] = [];
this.assetToBucket[asset.id] || const assetsToAdd: AssetResponseDto[] = [];
this.options.userId ||
this.options.personId || for (const asset of assets) {
this.options.albumId || if (
isMismatched(this.options.isArchived, asset.isArchived) || this.assetToBucket[asset.id] ||
isMismatched(this.options.isFavorite, asset.isFavorite) this.options.userId ||
) { this.options.personId ||
// If asset is already in the bucket we don't need to recalculate this.options.albumId ||
// asset store containers isMismatched(this.options.isArchived, asset.isArchived) ||
this.updateAsset(asset); isMismatched(this.options.isFavorite, asset.isFavorite)
return; ) {
// 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) { private addAssetsToBuckets(assets: AssetResponseDto[]) {
const timeBucket = DateTime.fromISO(asset.fileCreatedAt).toUTC().startOf('month').toString(); if (assets.length === 0) {
let bucket = this.getBucketByDate(timeBucket); return;
}
const updatedBuckets = new Set<AssetBucket>();
if (!bucket) { for (const asset of assets) {
bucket = { const timeBucket = DateTime.fromISO(asset.fileCreatedAt).toUTC().startOf('month').toString();
bucketDate: timeBucket, let bucket = this.getBucketByDate(timeBucket);
bucketHeight: THUMBNAIL_HEIGHT,
bucketCount: 0,
assets: [],
cancelToken: null,
position: BucketPosition.Unknown,
};
this.buckets.push(bucket); if (!bucket) {
this.buckets = this.buckets.sort((a, b) => { bucket = {
const aDate = DateTime.fromISO(a.bucketDate).toUTC(); bucketDate: timeBucket,
const bDate = DateTime.fromISO(b.bucketDate).toUTC(); 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; 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); this.emit(true);
} }
@ -358,21 +402,29 @@ export class AssetStore {
return null; return null;
} }
updateAsset(_asset: AssetResponseDto) { updateAssets(assets: AssetResponseDto[]) {
const asset = this.assets.find((asset) => asset.id === _asset.id); if (assets.length === 0) {
if (!asset) {
return; return;
} }
const assetsToReculculate: AssetResponseDto[] = [];
const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt; for (const _asset of assets) {
if (recalculate) { const asset = this.assets.find((asset) => asset.id === _asset.id);
this.removeAssets([asset.id]); if (!asset) {
this.addAssetToBucket(_asset); continue;
return; }
const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt;
Object.assign(asset, _asset);
if (recalculate) {
assetsToReculculate.push(asset);
}
} }
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[]) {