From 71215fe59912d933202f5d77ee38e6d58395834e Mon Sep 17 00:00:00 2001 From: Matchu Date: Fri, 16 Apr 2021 20:16:56 -0700 Subject: [PATCH] LRU caches to speed up outfit layer preload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The layer preloader already takes advantage of, and primes, the HTTP cache. But we still do duplicate work, on every OutfitPreview render, to re-execute movie clip libraries, and create a movie clip to test for animations. The former is nontrivial cost, and the latter is often large cost. This can make even basic outfit changes slow, when there's no change to the movie clip layers and the player is paused! Here, we add an LRU cache for movie clip libraries, and for the question of "is it animated?". This should speed up a number of places where we would reload the movie (including between toggling the item), and various changes that were triggering full movie clip rebuilds unnecessarily. We _aren't_ solving here for the fact that toggling an animated item requires rebuilding the movie clip, which could conceivably be cached—but with some state management trickiness, because ideally it should be a separate clip for each context where it's being shown. Imo not yet worth the effort! (esp because I think users understand that toggling an animated item can be slow, whereas this was affecting _other_ actions way too much) --- package.json | 1 + src/app/components/OutfitMovieLayer.js | 12 +++++++ src/app/components/OutfitPreview.js | 50 +++++++++++++++++--------- 3 files changed, 47 insertions(+), 16 deletions(-) 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.