From 5a57d9bb643c529a4533cfe6b01c7a2ae719cf89 Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Tue, 3 Dec 2024 15:53:27 -0500 Subject: [PATCH 1/7] intermediate cache --- .../core/src/components/Grid/Grid.tsx | 3 +- .../core/src/components/Grid/useRefreshers.ts | 30 ++++++++++++++++--- .../core/src/components/Grid/useSelect.ts | 4 +-- app/packages/looker/src/lookers/abstract.ts | 8 +++++ app/packages/looker/src/lookers/video.ts | 9 ++++++ 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/app/packages/core/src/components/Grid/Grid.tsx b/app/packages/core/src/components/Grid/Grid.tsx index 0525bcda96..4a5e39d67a 100644 --- a/app/packages/core/src/components/Grid/Grid.tsx +++ b/app/packages/core/src/components/Grid/Grid.tsx @@ -71,7 +71,8 @@ function Grid() { rowAspectRatioThreshold: threshold, get: (next) => page(next), render: (id, element, dimensions, soft, hide) => { - if (lookerStore.has(id.description)) { + const cached = lookerStore.get(id.description); + if (cached) { const looker = lookerStore.get(id.description); hide ? looker?.disable() : looker?.attach(element, dimensions); diff --git a/app/packages/core/src/components/Grid/useRefreshers.ts b/app/packages/core/src/components/Grid/useRefreshers.ts index 2b8d51f25f..0db1fdf475 100644 --- a/app/packages/core/src/components/Grid/useRefreshers.ts +++ b/app/packages/core/src/components/Grid/useRefreshers.ts @@ -83,13 +83,35 @@ export default function useRefreshers() { reset; /** LOOKER STORE REFRESHER */ - return new LRUCache({ - dispose: (looker) => { - looker.destroy(); - }, + const loaded = new LRUCache({ + dispose: (looker) => looker.destroy(), max: MAX_LRU_CACHE_ITEMS, + maxSize: MAX_LRU_CACHE_SIZE, noDisposeOnSet: true, + sizeCalculation: (looker) => looker.getSizeBytesEstimate(), }); + + const loading = new Map(); + + return { + delete: (key: string) => { + loading.delete(key); + loaded.delete(key); + }, + set: (key: string, looker: fos.Lookers) => { + const onReady = () => { + loaded.set(key, looker); + loading.delete(key); + looker.removeEventListener("error", onReady); + looker.removeEventListener("load", onReady); + }; + + looker.addEventListener("error", onReady); + looker.addEventListener("load", onReady); + loading.set(key, looker); + }, + get: (key: string) => loaded.get(key) ?? loading.get(key), + }; }, [reset]); return { diff --git a/app/packages/core/src/components/Grid/useSelect.ts b/app/packages/core/src/components/Grid/useSelect.ts index 9664373992..bc72fe4abd 100644 --- a/app/packages/core/src/components/Grid/useSelect.ts +++ b/app/packages/core/src/components/Grid/useSelect.ts @@ -1,13 +1,13 @@ import type Spotlight from "@fiftyone/spotlight"; import * as fos from "@fiftyone/state"; -import type { LRUCache } from "lru-cache"; import { useEffect } from "react"; import { useRecoilValue } from "recoil"; +import type useRefreshers from "./useRefreshers"; export default function useSelect( getFontSize: () => number, options: ReturnType, - store: LRUCache, + store: ReturnType["lookerStore"], spotlight?: Spotlight ) { const { init, deferred } = fos.useDeferrer(); diff --git a/app/packages/looker/src/lookers/abstract.ts b/app/packages/looker/src/lookers/abstract.ts index 3eca774de8..810ea3645e 100644 --- a/app/packages/looker/src/lookers/abstract.ts +++ b/app/packages/looker/src/lookers/abstract.ts @@ -191,6 +191,14 @@ export abstract class AbstractLooker< return this.sampleOverlays; } + getSizeBytesEstimate() { + let size = 1; + for (let index = 0; index < this.sampleOverlays.length; index++) { + size += this.sampleOverlays[index].getSizeBytes(); + } + return size; + } + protected dispatchEvent(eventType: string, detail: any): void { if (detail instanceof ErrorEvent) { this.updater({ error: detail.error }); diff --git a/app/packages/looker/src/lookers/video.ts b/app/packages/looker/src/lookers/video.ts index 24ab04feb0..f4a524c8d6 100644 --- a/app/packages/looker/src/lookers/video.ts +++ b/app/packages/looker/src/lookers/video.ts @@ -158,6 +158,15 @@ export class VideoLooker extends AbstractLooker { }; } + getSizeBytesEstimate() { + let size = super.getSizeBytesEstimate(); + for (let index = 0; index < this.firstFrame.overlays.length; index++) { + size += this.firstFrame.overlays[index].getSizeBytes(); + } + + return size; + } + hasDefaultZoom(state: VideoState, overlays: Overlay[]): boolean { const pan = [0, 0]; const scale = 1; From df0d6823a87f84751b454a9672adfcaf7803e75e Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Tue, 3 Dec 2024 18:57:49 -0500 Subject: [PATCH 2/7] tweaks --- .../core/src/components/Grid/useRefreshers.ts | 18 ++++++++++++++---- app/packages/looker/src/lookers/abstract.ts | 10 ++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/packages/core/src/components/Grid/useRefreshers.ts b/app/packages/core/src/components/Grid/useRefreshers.ts index 2f3633046a..3523207dd0 100644 --- a/app/packages/core/src/components/Grid/useRefreshers.ts +++ b/app/packages/core/src/components/Grid/useRefreshers.ts @@ -7,6 +7,7 @@ import { useRecoilValue } from "recoil"; import { gridAt, gridOffset, gridPage } from "./recoil"; const MAX_LRU_CACHE_ITEMS = 510; +const MAX_LRU_CACHE_SIZE = 1e9; export default function useRefreshers() { const cropToContent = useRecoilValue(fos.cropToContent(false)); @@ -87,7 +88,10 @@ export default function useRefreshers() { max: MAX_LRU_CACHE_ITEMS, maxSize: MAX_LRU_CACHE_SIZE, noDisposeOnSet: true, - sizeCalculation: (looker) => looker.getSizeBytesEstimate(), + sizeCalculation: (looker) => { + return looker.getSizeBytesEstimate(); + }, + updateAgeOnGet: true, }); const loading = new Map(); @@ -97,19 +101,25 @@ export default function useRefreshers() { loading.delete(key); loaded.delete(key); }, + get: (key: string) => loaded.get(key) ?? loading.get(key), + keys: function* () { + for (const it of loading.keys()) { + yield* it; + } + for (const it of loaded.keys()) { + yield* it; + } + }, set: (key: string, looker: fos.Lookers) => { const onReady = () => { loaded.set(key, looker); loading.delete(key); - looker.removeEventListener("error", onReady); looker.removeEventListener("load", onReady); }; - looker.addEventListener("error", onReady); looker.addEventListener("load", onReady); loading.set(key, looker); }, - get: (key: string) => loaded.get(key) ?? loading.get(key), }; }, [reset]); diff --git a/app/packages/looker/src/lookers/abstract.ts b/app/packages/looker/src/lookers/abstract.ts index 810ea3645e..906fd45b95 100644 --- a/app/packages/looker/src/lookers/abstract.ts +++ b/app/packages/looker/src/lookers/abstract.ts @@ -193,6 +193,10 @@ export abstract class AbstractLooker< getSizeBytesEstimate() { let size = 1; + if (this.state.dimensions) { + const [w, h] = this.state.dimensions; + size += w * h * 4; + } for (let index = 0; index < this.sampleOverlays.length; index++) { size += this.sampleOverlays[index].getSizeBytes(); } @@ -257,6 +261,7 @@ export abstract class AbstractLooker< } private makeUpdate(): StateUpdate { + let ready = false; return (stateOrUpdater, postUpdate) => { try { const updates = @@ -306,6 +311,11 @@ export abstract class AbstractLooker< this.pluckedOverlays.forEach((overlay) => overlay.cleanup?.()); } + if (!ready && this.state.dimensions && this.state.overlaysPrepared) { + ready = true; + this.dispatchEvent("load", undefined); + } + if ( !this.state.windowBBox || this.state.destroyed || From d5a75cf0de7652d9a7228cb7357aa5aa79718fda Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Wed, 4 Dec 2024 12:58:10 -0500 Subject: [PATCH 3/7] cleanup --- .../src/components/Grid/useLookerCache.ts | 56 +++++++++++++++++++ .../core/src/components/Grid/useRefreshers.ts | 52 +---------------- app/packages/looker/src/lookers/abstract.ts | 27 +++++---- app/packages/looker/src/lookers/video.ts | 4 ++ 4 files changed, 79 insertions(+), 60 deletions(-) create mode 100644 app/packages/core/src/components/Grid/useLookerCache.ts diff --git a/app/packages/core/src/components/Grid/useLookerCache.ts b/app/packages/core/src/components/Grid/useLookerCache.ts new file mode 100644 index 0000000000..edd38f8704 --- /dev/null +++ b/app/packages/core/src/components/Grid/useLookerCache.ts @@ -0,0 +1,56 @@ +import { Lookers } from "@fiftyone/state"; +import { LRUCache } from "lru-cache"; +import { useMemo } from "react"; + +const MAX_LRU_CACHE_ITEMS = 510; +const MAX_LRU_CACHE_SIZE = 1e9; + +export default function useLookerCache(reset: string) { + return useMemo(() => { + /** CLEAR CACHE WHEN reset CHANGES */ + reset; + /** CLEAR CACHE WHEN reset CHANGES */ + + const loaded = new LRUCache({ + dispose: (looker) => looker.destroy(), + max: MAX_LRU_CACHE_ITEMS, + maxSize: MAX_LRU_CACHE_SIZE, + noDisposeOnSet: true, + sizeCalculation: (looker) => { + console.log(looker.getSizeBytesEstimate()); + return looker.getSizeBytesEstimate(); + }, + updateAgeOnGet: true, + }); + + // an intermediate mapping while until the "load" event + // "load" must occur before requesting the size bytes estimate + const loading = new Map(); + + return { + delete: (key: string) => { + loading.delete(key); + loaded.delete(key); + }, + get: (key: string) => loaded.get(key) ?? loading.get(key), + keys: function* () { + for (const it of loading.keys()) { + yield* it; + } + for (const it of loaded.keys()) { + yield* it; + } + }, + set: (key: string, looker: Lookers) => { + const onReady = () => { + loaded.set(key, looker); + loading.delete(key); + looker.removeEventListener("load", onReady); + }; + + looker.addEventListener("load", onReady); + loading.set(key, looker); + }, + }; + }, [reset]); +} diff --git a/app/packages/core/src/components/Grid/useRefreshers.ts b/app/packages/core/src/components/Grid/useRefreshers.ts index 3523207dd0..9f028f6699 100644 --- a/app/packages/core/src/components/Grid/useRefreshers.ts +++ b/app/packages/core/src/components/Grid/useRefreshers.ts @@ -1,13 +1,10 @@ import { subscribe } from "@fiftyone/relay"; import * as fos from "@fiftyone/state"; -import { LRUCache } from "lru-cache"; import { useEffect, useMemo } from "react"; import uuid from "react-uuid"; import { useRecoilValue } from "recoil"; import { gridAt, gridOffset, gridPage } from "./recoil"; - -const MAX_LRU_CACHE_ITEMS = 510; -const MAX_LRU_CACHE_SIZE = 1e9; +import useLookerCache from "./useLookerCache"; export default function useRefreshers() { const cropToContent = useRecoilValue(fos.cropToContent(false)); @@ -78,53 +75,8 @@ export default function useRefreshers() { [] ); - const lookerStore = useMemo(() => { - /** LOOKER STORE REFRESHER */ - reset; - /** LOOKER STORE REFRESHER */ - - const loaded = new LRUCache({ - dispose: (looker) => looker.destroy(), - max: MAX_LRU_CACHE_ITEMS, - maxSize: MAX_LRU_CACHE_SIZE, - noDisposeOnSet: true, - sizeCalculation: (looker) => { - return looker.getSizeBytesEstimate(); - }, - updateAgeOnGet: true, - }); - - const loading = new Map(); - - return { - delete: (key: string) => { - loading.delete(key); - loaded.delete(key); - }, - get: (key: string) => loaded.get(key) ?? loading.get(key), - keys: function* () { - for (const it of loading.keys()) { - yield* it; - } - for (const it of loaded.keys()) { - yield* it; - } - }, - set: (key: string, looker: fos.Lookers) => { - const onReady = () => { - loaded.set(key, looker); - loading.delete(key); - looker.removeEventListener("load", onReady); - }; - - looker.addEventListener("load", onReady); - loading.set(key, looker); - }, - }; - }, [reset]); - return { - lookerStore, + lookerStore: useLookerCache(reset), pageReset, reset, }; diff --git a/app/packages/looker/src/lookers/abstract.ts b/app/packages/looker/src/lookers/abstract.ts index 906fd45b95..4e686e3b99 100644 --- a/app/packages/looker/src/lookers/abstract.ts +++ b/app/packages/looker/src/lookers/abstract.ts @@ -193,10 +193,15 @@ export abstract class AbstractLooker< getSizeBytesEstimate() { let size = 1; - if (this.state.dimensions) { + if (this.state.dimensions && !this.state.error) { const [w, h] = this.state.dimensions; size += w * h * 4; } + + if (!this.sampleOverlays?.length) { + return size; + } + for (let index = 0; index < this.sampleOverlays.length; index++) { size += this.sampleOverlays[index].getSizeBytes(); } @@ -220,11 +225,18 @@ export abstract class AbstractLooker< } protected dispatchImpliedEvents( - { options: prevOtions }: Readonly, - { options }: Readonly + previous: Readonly, + next: Readonly ): void { - if (options.showJSON !== prevOtions.showJSON) { - this.dispatchEvent("options", { showJSON: options.showJSON }); + if (previous.options.showJSON !== next.options.showJSON) { + this.dispatchEvent("options", { showJSON: next.options.showJSON }); + } + + const wasLoaded = previous.overlaysPrepared && previous.dimensions; + const isLoaded = next.overlaysPrepared && next.dimensions; + + if (!wasLoaded && isLoaded) { + this.dispatchEvent("load", undefined); } } @@ -311,11 +323,6 @@ export abstract class AbstractLooker< this.pluckedOverlays.forEach((overlay) => overlay.cleanup?.()); } - if (!ready && this.state.dimensions && this.state.overlaysPrepared) { - ready = true; - this.dispatchEvent("load", undefined); - } - if ( !this.state.windowBBox || this.state.destroyed || diff --git a/app/packages/looker/src/lookers/video.ts b/app/packages/looker/src/lookers/video.ts index f4a524c8d6..adcd3c107e 100644 --- a/app/packages/looker/src/lookers/video.ts +++ b/app/packages/looker/src/lookers/video.ts @@ -160,6 +160,10 @@ export class VideoLooker extends AbstractLooker { getSizeBytesEstimate() { let size = super.getSizeBytesEstimate(); + if (!this.firstFrame.overlays?.length) { + return size; + } + for (let index = 0; index < this.firstFrame.overlays.length; index++) { size += this.firstFrame.overlays[index].getSizeBytes(); } From 6e86c742080ba9a91489b226ff4a574c12deb7e3 Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Wed, 4 Dec 2024 14:28:29 -0500 Subject: [PATCH 4/7] add hook test --- .../components/Grid/useLookerCache.test.ts | 47 +++++++++++++++++++ .../src/components/Grid/useLookerCache.ts | 20 ++++++-- app/packages/looker/src/lookers/abstract.ts | 3 +- 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 app/packages/core/src/components/Grid/useLookerCache.test.ts diff --git a/app/packages/core/src/components/Grid/useLookerCache.test.ts b/app/packages/core/src/components/Grid/useLookerCache.test.ts new file mode 100644 index 0000000000..f55b4951d3 --- /dev/null +++ b/app/packages/core/src/components/Grid/useLookerCache.test.ts @@ -0,0 +1,47 @@ +import { act, renderHook } from "@testing-library/react-hooks"; +import { describe, expect, it } from "vitest"; +import useLookerCache from "./useLookerCache"; + +class Looker extends EventTarget { + destroy = () => undefined; + getSizeBytesEstimate = () => 1; +} + +describe("useLookerCache", () => { + it("assert intermediate loading", () => { + const { result, rerender } = renderHook( + (reset) => useLookerCache(reset), + { + initialProps: "one", + } + ); + expect(result.current.loadedSize()).toBe(0); + expect(result.current.loadingSize()).toBe(0); + + act(() => { + const looker = new Looker(); + result.current.set("one", looker); + }); + + expect(result.current.loadingSize()).toBe(1); + expect(result.current.loadedSize()).toBe(0); + expect(result.current.sizeEstimate()).toBe(0); + + act(() => { + const looker = result.current.get("one"); + if (!looker) { + throw new Error("looker is missing"); + } + + looker.dispatchEvent(new Event("load")); + }); + expect(result.current.loadingSize()).toBe(0); + expect(result.current.loadedSize()).toBe(1); + expect(result.current.sizeEstimate()).toBe(1); + + rerender("two"); + expect(result.current.loadedSize()).toBe(0); + expect(result.current.loadingSize()).toBe(0); + expect(result.current.sizeEstimate()).toBe(0); + }); +}); diff --git a/app/packages/core/src/components/Grid/useLookerCache.ts b/app/packages/core/src/components/Grid/useLookerCache.ts index edd38f8704..b6d96aea49 100644 --- a/app/packages/core/src/components/Grid/useLookerCache.ts +++ b/app/packages/core/src/components/Grid/useLookerCache.ts @@ -1,17 +1,24 @@ -import { Lookers } from "@fiftyone/state"; +import * as fos from "@fiftyone/state"; import { LRUCache } from "lru-cache"; import { useMemo } from "react"; const MAX_LRU_CACHE_ITEMS = 510; const MAX_LRU_CACHE_SIZE = 1e9; -export default function useLookerCache(reset: string) { +interface Lookers extends EventTarget { + destroy: () => void; + getSizeBytesEstimate: () => number; +} + +export default function useLookerCache< + T extends Lookers | fos.Lookers = fos.Lookers +>(reset: string) { return useMemo(() => { /** CLEAR CACHE WHEN reset CHANGES */ reset; /** CLEAR CACHE WHEN reset CHANGES */ - const loaded = new LRUCache({ + const loaded = new LRUCache({ dispose: (looker) => looker.destroy(), max: MAX_LRU_CACHE_ITEMS, maxSize: MAX_LRU_CACHE_SIZE, @@ -25,7 +32,7 @@ export default function useLookerCache(reset: string) { // an intermediate mapping while until the "load" event // "load" must occur before requesting the size bytes estimate - const loading = new Map(); + const loading = new Map(); return { delete: (key: string) => { @@ -41,7 +48,9 @@ export default function useLookerCache(reset: string) { yield* it; } }, - set: (key: string, looker: Lookers) => { + loadingSize: () => loading.size, + loadedSize: () => loaded.size, + set: (key: string, looker: T) => { const onReady = () => { loaded.set(key, looker); loading.delete(key); @@ -51,6 +60,7 @@ export default function useLookerCache(reset: string) { looker.addEventListener("load", onReady); loading.set(key, looker); }, + sizeEstimate: () => loaded.calculatedSize, }; }, [reset]); } diff --git a/app/packages/looker/src/lookers/abstract.ts b/app/packages/looker/src/lookers/abstract.ts index 4e686e3b99..bd79dc5000 100644 --- a/app/packages/looker/src/lookers/abstract.ts +++ b/app/packages/looker/src/lookers/abstract.ts @@ -208,7 +208,7 @@ export abstract class AbstractLooker< return size; } - protected dispatchEvent(eventType: string, detail: any): void { + dispatchEvent(eventType: string, detail: any): void { if (detail instanceof ErrorEvent) { this.updater({ error: detail.error }); return; @@ -273,7 +273,6 @@ export abstract class AbstractLooker< } private makeUpdate(): StateUpdate { - let ready = false; return (stateOrUpdater, postUpdate) => { try { const updates = From 839a923d377fb1b3937e5b4f708659115c26f7a6 Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Wed, 4 Dec 2024 14:52:49 -0500 Subject: [PATCH 5/7] fix yield --- app/packages/core/src/components/Grid/useLookerCache.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/packages/core/src/components/Grid/useLookerCache.ts b/app/packages/core/src/components/Grid/useLookerCache.ts index b6d96aea49..c36e7fb098 100644 --- a/app/packages/core/src/components/Grid/useLookerCache.ts +++ b/app/packages/core/src/components/Grid/useLookerCache.ts @@ -42,10 +42,10 @@ export default function useLookerCache< get: (key: string) => loaded.get(key) ?? loading.get(key), keys: function* () { for (const it of loading.keys()) { - yield* it; + yield it; } for (const it of loaded.keys()) { - yield* it; + yield it; } }, loadingSize: () => loading.size, From 84042673d8feaabc41c69f95c37a18b76829e7ab Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Wed, 4 Dec 2024 14:54:02 -0500 Subject: [PATCH 6/7] rm log --- app/packages/core/src/components/Grid/useLookerCache.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/packages/core/src/components/Grid/useLookerCache.ts b/app/packages/core/src/components/Grid/useLookerCache.ts index c36e7fb098..1d852f0898 100644 --- a/app/packages/core/src/components/Grid/useLookerCache.ts +++ b/app/packages/core/src/components/Grid/useLookerCache.ts @@ -23,10 +23,7 @@ export default function useLookerCache< max: MAX_LRU_CACHE_ITEMS, maxSize: MAX_LRU_CACHE_SIZE, noDisposeOnSet: true, - sizeCalculation: (looker) => { - console.log(looker.getSizeBytesEstimate()); - return looker.getSizeBytesEstimate(); - }, + sizeCalculation: (looker) => looker.getSizeBytesEstimate(), updateAgeOnGet: true, }); From 9a06cd24b4f15b4932a886a772a7eee6f9f7ddbb Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Wed, 4 Dec 2024 14:57:25 -0500 Subject: [PATCH 7/7] typo --- app/packages/core/src/components/Grid/useLookerCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/packages/core/src/components/Grid/useLookerCache.ts b/app/packages/core/src/components/Grid/useLookerCache.ts index 1d852f0898..2f101da626 100644 --- a/app/packages/core/src/components/Grid/useLookerCache.ts +++ b/app/packages/core/src/components/Grid/useLookerCache.ts @@ -27,7 +27,7 @@ export default function useLookerCache< updateAgeOnGet: true, }); - // an intermediate mapping while until the "load" event + // an intermediate mapping until the "load" event // "load" must occur before requesting the size bytes estimate const loading = new Map();