Skip to content

Commit

Permalink
More efficient implementation for incrementally loading assets.
Browse files Browse the repository at this point in the history
Now using Rx.js style observables/subscriptions to notify the gallery
layout of new items that have been loaded, allowing the layout to be
incrementally built as new assets come in.

This is much faster than how it used to be. Previously the layout
was computed from the entire asset set on each increment and it
grows with each new page of assets loaded.

Eventually the asset set will equal the entire asset library and so as
pages are loaded the layout was getting more and more expensive
to build. Now the layout is computed at each increment only for the
new assets in the increment and not for the entire growing set.
  • Loading branch information
ashleydavis committed Aug 1, 2024
1 parent 2993530 commit a0b3c31
Show file tree
Hide file tree
Showing 11 changed files with 432 additions and 117 deletions.
4 changes: 2 additions & 2 deletions packages/user-interface/src/components/asset-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface IAssetViewProps {
//
export function AssetView({ open, onClose, onNext, onPrev }: IAssetViewProps) {

const { items } = useGallery();
const { getSearchedItems } = useGallery();
const { asset } = useGalleryItem();

//
Expand Down Expand Up @@ -79,7 +79,7 @@ export function AssetView({ open, onClose, onNext, onPrev }: IAssetViewProps) {
</div>
}
<div className="flex-grow" /> {/* Spacer */}
{asset.searchIndex! < items.length - 1
{asset.searchIndex! < getSearchedItems().length - 1
&& <div className="flex flex-col justify-center">
<button
className="mr-4 p-1 px-3 pointer-events-auto rounded border border-solid border-white"
Expand Down
40 changes: 36 additions & 4 deletions packages/user-interface/src/components/gallery-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export function GalleryLayout({
getHeadings,
}: IGalleryLayoutProps) {

const { items } = useGallery();
const { getSearchedItems, onReset, onNewItems, searchText } = useGallery();

const containerRef = useRef<HTMLDivElement>(null);
const [ scrollTop, setScrollTop ] = useState(0);
Expand All @@ -180,11 +180,43 @@ export function GalleryLayout({
const [layout, setLayout] = useState<IGalleryLayout | undefined>(undefined);

//
// Computes the gallery layout.
// Resets the gallery layout as necessary in preparation for incremental loading.
//
useEffect(() => {
setLayout(computePartialLayout(undefined, items, galleryWidth, targetRowHeight, getHeadings));
}, [items, galleryWidth, targetRowHeight]);
const subscription = onReset.subscribe(() => {
setLayout(undefined);
});
return () => {
subscription.unsubscribe();
};
}, []);

//
// Incrementally builds the layout as items are loaded.
//
useEffect(() => {
if (galleryWidth > 0) {
const subscription = onNewItems.subscribe(items => {
//
// Adds new items to the layout.
//
setLayout(prevLayout => computePartialLayout(prevLayout, items, galleryWidth, targetRowHeight, getHeadings));
});
return () => {
subscription.unsubscribe();
};
}
}, [galleryWidth]);

//
// Rebuilds the gallery layout as necessary when important details have changed.
//
useEffect(() => {
if (galleryWidth === 0) {
return;
}
setLayout(computePartialLayout(undefined, getSearchedItems(), galleryWidth, targetRowHeight, getHeadings));
}, [galleryWidth, targetRowHeight, searchText]);

//
// Handles scrolling.
Expand Down
145 changes: 93 additions & 52 deletions packages/user-interface/src/context/asset-database-source.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { ReactNode, createContext, useContext, useEffect, useRef, useState } from "react";
import { IGalleryItem } from "../lib/gallery-item";
import { IAssetData } from "../def/asset-data";
import { GallerySourceContext, IAssetDataLoad, IGalleryItemMap, IGallerySource } from "./gallery-source";
import { GallerySourceContext, IAssetDataLoad, IAssetsUpdated, IGalleryItemMap, IGallerySource } from "./gallery-source";
import { IAsset, IDatabaseOp } from "defs";
import { PersistentQueue } from "../lib/sync/persistent-queue";
import dayjs from "dayjs";
Expand All @@ -16,6 +16,7 @@ import { syncIncoming } from "../lib/sync/sync-incoming";
import { initialSync } from "../lib/sync/initial-sync";
import { IOutgoingUpdate } from "../lib/sync/outgoing-update";
import { uuid } from "../lib/uuid";
import { IObservable, Observable } from "../lib/subscription";

const SYNC_POLL_PERIOD = 60 * 1000; // 1 minute.

Expand Down Expand Up @@ -78,27 +79,59 @@ export function AssetDatabaseProvider({ children }: IAssetDatabaseProviderProps)
//
// Assets that have been loaded.
//
const [ assets, setAssets ] = useState<IGalleryItemMap>({});
const loadedAssets = useRef<IGalleryItemMap>({});

//
// The set currently being viewed.
//
const [ setId, setSetId ] = useState<string | undefined>(undefined);

//
// Assets that have been loaded.
//
function getAssets(): IGalleryItemMap {
return loadedAssets.current;
}

//
// Subscribes to resets of the gallery.
//
const onReset = useRef<IObservable<void>>(new Observable<void>());

//
// Subscribes to new gallery items.
//
const onNewItems = useRef<IObservable<IGalleryItem[]>>(new Observable<IGalleryItem[]>());

//
// Invokes subscriptions for new assets.
//
function _onNewItems(assets: IAsset[]) {
for (const asset of assets) {
loadedAssets.current[asset._id] = asset;
}

onNewItems.current.invoke(assets);
}

const onAssetsUpdated = useRef<IObservable<IAssetsUpdated>>(new Observable<IAssetsUpdated>());

//
// Adds an asset to the default set.
//
function addAsset(asset: IGalleryItem): void {
function addAsset(item: IGalleryItem): void {
if (!setId) {
throw new Error("No set id provided.");
}

setAssets({
...assets,
[asset._id]: asset,
});
const asset: IAsset = {
...item,
setId,
};

_onNewItems([ asset ]);

addAssetToSet(asset, setId)
addAssetToSet(asset, setId)
.catch(err => {
console.error(`Failed to add asset:`);
console.error(err);
Expand Down Expand Up @@ -158,11 +191,10 @@ export function AssetDatabaseProvider({ children }: IAssetDatabaseProviderProps)
//
async function updateAsset(assetId: string, partialAsset: Partial<IGalleryItem>): Promise<void> {

const updatedAsset = { ...assets[assetId], ...partialAsset };
setAssets({
...assets,
[assetId]: updatedAsset,
});
const updatedAsset = { ...loadedAssets.current[assetId], ...partialAsset };
loadedAssets.current[assetId] = updatedAsset;

onAssetsUpdated.current.invoke({ assetIds: [ assetId ] });

const ops: IDatabaseOp[] = [{
collectionName: "metadata",
Expand Down Expand Up @@ -197,13 +229,14 @@ export function AssetDatabaseProvider({ children }: IAssetDatabaseProviderProps)
// Update multiple assets with persisted database changes.
//
async function updateAssets(assetUpdates: { assetId: string, partialAsset: Partial<IGalleryItem>}[]): Promise<void> {
let _assets = {
...assets,
};
for (const { assetId, partialAsset } of assetUpdates) {
_assets[assetId] = { ..._assets[assetId], ...partialAsset };
loadedAssets.current[assetId] = {
...loadedAssets.current[assetId],
...partialAsset,
};
}
setAssets(_assets);

onAssetsUpdated.current.invoke({ assetIds: assetUpdates.map(({ assetId }) => assetId) });

const ops: IDatabaseOp[] = assetUpdates.map(({ assetId, partialAsset }) => ({
collectionName: "metadata",
Expand Down Expand Up @@ -235,17 +268,16 @@ export function AssetDatabaseProvider({ children }: IAssetDatabaseProviderProps)
//
async function addArrayValue(assetId: string, field: string, value: any): Promise<void> {

const updatedAsset: any = { ...assets[assetId] };
const updatedAsset: any = { ...loadedAssets.current[assetId] };
if (updatedAsset[field] === undefined) {
updatedAsset[field] = [];
}
updatedAsset[field] = (updatedAsset[field] as any[]).filter(item => item !== value)
updatedAsset[field].push(value);

setAssets({
...assets,
[assetId]: updatedAsset,
});
loadedAssets.current[assetId] = updatedAsset;

onAssetsUpdated.current.invoke({ assetIds: [ assetId ] });

const ops: IDatabaseOp[] = [{
collectionName: "metadata",
Expand Down Expand Up @@ -282,16 +314,15 @@ export function AssetDatabaseProvider({ children }: IAssetDatabaseProviderProps)
//
async function removeArrayValue(assetId: string, field: string, value: any): Promise<void> {

const updatedAsset: any = { ...assets[assetId] };
const updatedAsset: any = { ...loadedAssets.current[assetId] };
if (updatedAsset[field] === undefined) {
updatedAsset[field] = [];
}
updatedAsset[field] = (updatedAsset[field] as any[]).filter(item => item !== value)

setAssets({
...assets,
[assetId]: updatedAsset,
});

loadedAssets.current[assetId] = updatedAsset;

onAssetsUpdated.current.invoke({ assetIds: [ assetId ] });

const ops: IDatabaseOp[] = [{
collectionName: "metadata",
Expand Down Expand Up @@ -344,16 +375,17 @@ export function AssetDatabaseProvider({ children }: IAssetDatabaseProviderProps)
//
// Initializes the destination set.
//
await initialSync(database, destSetId, api, 0, assets => {
console.log(`Loaded ${assets.length} assets into ${setId}`);
return true;
});
await initialSync(database, destSetId, api, 0,
assets => {
console.log(`Loaded ${assets.length} assets into ${setId}`);
}
);

//
// Saves asset data to other set.
//
for (const assetId of assetIds) {
const asset = assets[assetId];
const asset = loadedAssets.current[assetId];
const newAssetId = uuid();
const assetTypes = ["thumb", "display", "asset"];
for (const assetType of assetTypes) {
Expand Down Expand Up @@ -574,29 +606,35 @@ export function AssetDatabaseProvider({ children }: IAssetDatabaseProviderProps)
// Start with no assets.
// This clears out any existing set of assets.
//
setAssets({});

await initialSync(database, setId, api, loadingId.current, (assets, setIndex) => {
if (setIndex !== loadingId.current) {
// The set we are loading has changed.
// Stop loading assets.
return false;
}
loadedAssets.current = {};

const assetMap: IGalleryItemMap = {};
for (const asset of assets) {
assetMap[asset._id] = asset;
}
//
// Pass a gallery reset down the line.
// This is the starting point for incremental gallery loading.
//
onReset.current.invoke();

await initialSync(database, setId, api, loadingId.current,
//
// As each page of assets are loaded update the asset map in state.
// Sets assets as they are loaded.
//
setAssets(assetMap);
assets => {
//
// As each page of assets are loaded update the asset map in state.
//
_onNewItems(assets);

console.log(`Loaded ${assets.length} assets for set ${setId}`);
console.log(`Loaded ${assets.length} assets for set ${setId}`);
},

return true; // Continue loading assets.
}, assetSortFn);
//
// Continue if the set index matches the current loading index.
// This allows loading to be aborted if the user changes what they are looking at.
//
setIndex => setIndex === loadingId.current,

assetSortFn
);
}
finally {
loadingCount.current -= 1;
Expand Down Expand Up @@ -624,7 +662,10 @@ export function AssetDatabaseProvider({ children }: IAssetDatabaseProviderProps)
isLoading,
isWorking,
isReadOnly: false,
assets,
getAssets,
onReset: onReset.current,
onNewItems: onNewItems.current,
onAssetsUpdated: onAssetsUpdated.current,
addAsset,
updateAsset,
updateAssets,
Expand Down
Loading

0 comments on commit a0b3c31

Please sign in to comment.