diff --git a/package.json b/package.json index c2a7eb2..e8a37be 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "jimp": "^0.14.0", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^1.9.0", + "lru-cache": "^6.0.0", "mysql2": "^2.1.0", "node-fetch": "^2.6.0", "react": "^17.0.1", diff --git a/src/app/components/OutfitMovieLayer.js b/src/app/components/OutfitMovieLayer.js index 0ac06c2..30482ea 100644 --- a/src/app/components/OutfitMovieLayer.js +++ b/src/app/components/OutfitMovieLayer.js @@ -1,4 +1,5 @@ import React from "react"; +import LRU from "lru-cache"; import { useToast } from "@chakra-ui/react"; import { loadImage, logAndCapture, safeImageUrl } from "../util"; @@ -261,7 +262,16 @@ function loadScriptTag(src) { }); } +const MOVIE_LIBRARY_CACHE = new LRU(10); + export async function loadMovieLibrary(librarySrc) { + // First, check the LRU cache. This will enable us to quickly return movie + // libraries, without re-loading and re-parsing and re-executing. + const cachedLibrary = MOVIE_LIBRARY_CACHE.get(librarySrc); + if (cachedLibrary) { + return cachedLibrary; + } + // These library JS files are interesting in their operation. It seems like // the idea is, it pushes an object to a global array, and you need to snap // it up and see it at the end of the array! And I don't really see a way to @@ -322,6 +332,8 @@ export async function loadMovieLibrary(librarySrc) { }); } + MOVIE_LIBRARY_CACHE.set(librarySrc, library); + return library; } diff --git a/src/app/components/OutfitPreview.js b/src/app/components/OutfitPreview.js index 36db461..b43834d 100644 --- a/src/app/components/OutfitPreview.js +++ b/src/app/components/OutfitPreview.js @@ -1,5 +1,6 @@ import React from "react"; import { Box, DarkMode, Flex, Text, useColorModeValue } from "@chakra-ui/react"; +import LRU from "lru-cache"; import { WarningIcon } from "@chakra-ui/icons"; import { ClassNames } from "@emotion/react"; import { CSSTransition, TransitionGroup } from "react-transition-group"; @@ -393,29 +394,18 @@ export function usePreloadLayers(layers) { if (canceled) return; - let movieClips; + let newLayersHaveAnimations; try { - movieClips = assets + newLayersHaveAnimations = assets .filter((a) => a.type === "movie") - .map((a) => buildMovieClip(a.library, a.libraryUrl)); + .some(getHasAnimationsForMovieAsset); } catch (e) { - console.error("Error building movie clips", e); - assetPromises.forEach((p) => { - if (p.cancel) { - p.cancel(); - } - }); + console.error("Error testing layers for animations", e); setError(e); return; } - // Some movie clips require you to tick to the first frame of the movie - // before the children mount onto the stage. If we detect animations - // without doing this, we'll say no, because we see no children! - // Example: http://images.neopets.com/cp/items/data/000/000/235/235877_6d273e217c/235877.js - movieClips.forEach((mc) => mc.advance()); - - setLayersHaveAnimations(movieClips.some(hasAnimations)); + setLayersHaveAnimations(newLayersHaveAnimations); setLoadedLayers(layers); }; @@ -429,6 +419,34 @@ export function usePreloadLayers(layers) { return { loading, error, loadedLayers, layersHaveAnimations }; } +// This cache is large because it's only storing booleans; mostly just capping +// it to put *some* upper bound on memory growth. +const HAS_ANIMATIONS_FOR_MOVIE_ASSET_CACHE = new LRU(50); + +function getHasAnimationsForMovieAsset({ library, libraryUrl }) { + // This operation can be pretty expensive! We store a cache to only do it + // once per layer per session ish, instead of on each outfit change. + const cachedHasAnimations = HAS_ANIMATIONS_FOR_MOVIE_ASSET_CACHE.get( + libraryUrl + ); + if (cachedHasAnimations) { + return cachedHasAnimations; + } + + const movieClip = buildMovieClip(library, libraryUrl); + + // Some movie clips require you to tick to the first frame of the movie + // before the children mount onto the stage. If we detect animations + // without doing this, we'll incorrectly say no, because we see no children! + // Example: http://images.neopets.com/cp/items/data/000/000/235/235877_6d273e217c/235877.js + movieClip.advance(); + + const movieClipHasAnimations = hasAnimations(movieClip); + + HAS_ANIMATIONS_FOR_MOVIE_ASSET_CACHE.set(libraryUrl, movieClipHasAnimations); + return movieClipHasAnimations; +} + /** * FadeInOnLoad attaches an `onLoad` handler to its single child, and fades in * the container element once it triggers.