LRU caches to speed up outfit layer preload
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)
This commit is contained in:
parent
6f03a13516
commit
71215fe599
3 changed files with 47 additions and 16 deletions
|
@ -36,6 +36,7 @@
|
||||||
"jimp": "^0.14.0",
|
"jimp": "^0.14.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"jwks-rsa": "^1.9.0",
|
"jwks-rsa": "^1.9.0",
|
||||||
|
"lru-cache": "^6.0.0",
|
||||||
"mysql2": "^2.1.0",
|
"mysql2": "^2.1.0",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import LRU from "lru-cache";
|
||||||
import { useToast } from "@chakra-ui/react";
|
import { useToast } from "@chakra-ui/react";
|
||||||
|
|
||||||
import { loadImage, logAndCapture, safeImageUrl } from "../util";
|
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) {
|
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
|
// 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
|
// 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
|
// 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;
|
return library;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, DarkMode, Flex, Text, useColorModeValue } from "@chakra-ui/react";
|
import { Box, DarkMode, Flex, Text, useColorModeValue } from "@chakra-ui/react";
|
||||||
|
import LRU from "lru-cache";
|
||||||
import { WarningIcon } from "@chakra-ui/icons";
|
import { WarningIcon } from "@chakra-ui/icons";
|
||||||
import { ClassNames } from "@emotion/react";
|
import { ClassNames } from "@emotion/react";
|
||||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||||
|
@ -393,29 +394,18 @@ export function usePreloadLayers(layers) {
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
let movieClips;
|
let newLayersHaveAnimations;
|
||||||
try {
|
try {
|
||||||
movieClips = assets
|
newLayersHaveAnimations = assets
|
||||||
.filter((a) => a.type === "movie")
|
.filter((a) => a.type === "movie")
|
||||||
.map((a) => buildMovieClip(a.library, a.libraryUrl));
|
.some(getHasAnimationsForMovieAsset);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error building movie clips", e);
|
console.error("Error testing layers for animations", e);
|
||||||
assetPromises.forEach((p) => {
|
|
||||||
if (p.cancel) {
|
|
||||||
p.cancel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setError(e);
|
setError(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some movie clips require you to tick to the first frame of the movie
|
setLayersHaveAnimations(newLayersHaveAnimations);
|
||||||
// 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));
|
|
||||||
setLoadedLayers(layers);
|
setLoadedLayers(layers);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -429,6 +419,34 @@ export function usePreloadLayers(layers) {
|
||||||
return { loading, error, loadedLayers, layersHaveAnimations };
|
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
|
* FadeInOnLoad attaches an `onLoad` handler to its single child, and fades in
|
||||||
* the container element once it triggers.
|
* the container element once it triggers.
|
||||||
|
|
Loading…
Reference in a new issue