LoadLazyChunks: Clean up and performance improvements (#4509)
Co-authored-by: V <vendicated@riseup.net> Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
This commit is contained in:
parent
3e03b47b45
commit
d3de91e421
2 changed files with 56 additions and 43 deletions
|
|
@ -14,7 +14,7 @@ import pLimit from "p-limit";
|
|||
|
||||
function getWebpackChunkMap() {
|
||||
const sym = Symbol();
|
||||
let chunksMap: unknown;
|
||||
let chunksMap: unknown = null;
|
||||
|
||||
Object.defineProperty(Object.prototype, sym, {
|
||||
get() {
|
||||
|
|
@ -30,9 +30,38 @@ function getWebpackChunkMap() {
|
|||
return chunksMap as Record<PropertyKey, string> | null;
|
||||
}
|
||||
|
||||
let chunksAlreadyLoaded = false;
|
||||
|
||||
export async function loadLazyChunks() {
|
||||
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
|
||||
|
||||
if (chunksAlreadyLoaded) {
|
||||
LazyChunkLoaderLogger.log("Lazy chunks have already been loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
const queue = pLimit(50);
|
||||
const workerAssetCache = new Map<string, Promise<boolean>>();
|
||||
const WORKER_ASSET_REGEX = /importScripts\(|self\.postMessage/;
|
||||
|
||||
async function isWorkerAsset(url: string, useQueue: boolean = true): Promise<boolean> {
|
||||
if (workerAssetCache.has(url)) {
|
||||
return workerAssetCache.get(url)!;
|
||||
}
|
||||
|
||||
const doFetch = () => {
|
||||
return fetch(url)
|
||||
.then(r => r.text())
|
||||
.then(t => WORKER_ASSET_REGEX.test(t));
|
||||
};
|
||||
|
||||
const res = useQueue
|
||||
? queue(doFetch)
|
||||
: doFetch();
|
||||
|
||||
workerAssetCache.set(url, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
try {
|
||||
LazyChunkLoaderLogger.log("Loading all chunks...");
|
||||
|
|
@ -41,12 +70,12 @@ export async function loadLazyChunks() {
|
|||
const invalidChunks = new Set<PropertyKey>();
|
||||
const deferredRequires = new Set<PropertyKey>();
|
||||
|
||||
const { promise: chunksSearchingDone, resolve: chunksSearchingResolve } = Promise.withResolvers<void>();
|
||||
const { promise: chunkSearchingDone, resolve: chunkSearchingDoneResolve } = Promise.withResolvers<void>();
|
||||
|
||||
// True if resolved, false otherwise
|
||||
const chunksSearchPromises = [] as Array<() => boolean>;
|
||||
// True if searching promise for that chunk is resolved, false otherwise
|
||||
let chunkSearchResolvedGetters = [] as Array<() => boolean>;
|
||||
|
||||
// This regex loads all language packs which makes webpack finds testing extremely slow, so for now, we prioritize using the one which doesnt include those
|
||||
// This regex loads all language packs which makes webpack finds testing extremely slow, so for now, we prioritize using the one which doesn't include those
|
||||
const CompleteLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
|
||||
const PartialLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
|
||||
|
||||
|
|
@ -56,7 +85,7 @@ export async function loadLazyChunks() {
|
|||
// Workaround to avoid loading the CSS debugging chunk which turns the app pink
|
||||
// const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
|
||||
|
||||
// Disabled for now since this causes lots of chunks concatenated into the same module get marked as invalid, and thus not loaded.
|
||||
// Disabled for now since this causes lots of chunks concatenated into the same module to get marked as invalid, and thus not loaded.
|
||||
const hasCssDebuggingLoad = foundCssDebuggingLoad = false;
|
||||
|
||||
const lazyChunks = factoryCode.matchAll(hasCssDebuggingLoad ? CompleteLazyChunkRegex : PartialLazyChunkRegex);
|
||||
|
|
@ -93,13 +122,7 @@ export async function loadLazyChunks() {
|
|||
|
||||
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
||||
|
||||
const isWorkerAsset = await queue(() =>
|
||||
fetch(wreq.p + wreq.u(id))
|
||||
.then(r => r.text())
|
||||
.then(t => /importScripts\(|self\.postMessage/.test(t))
|
||||
);
|
||||
|
||||
if (isWorkerAsset) {
|
||||
if (await isWorkerAsset(wreq.p + wreq.u(id))) {
|
||||
invalidChunks.add(id);
|
||||
invalidChunkGroup = true;
|
||||
continue;
|
||||
|
|
@ -140,35 +163,27 @@ export async function loadLazyChunks() {
|
|||
}
|
||||
}
|
||||
|
||||
// setImmediate to only check if all chunks were loaded after this function resolves
|
||||
// We check if all chunks were loaded every time a factory is loaded
|
||||
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
|
||||
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
|
||||
|
||||
// Filter out resolved chunk search promises. If the array length is 0 that means all the pending searches and loadings are done
|
||||
// and our regex has finished scanning all modules. Once this happens, the artificial "natural" loading of chunks has been completed
|
||||
// and we can continue with the rest of the code.
|
||||
// setImmediate here is needed so the filtering also includes the promises created from searching the modules loaded by the current invokation.
|
||||
// Otherwise, it could resolve before the async code being put at the end of the event loop has a chance to run and add the promise to the array.
|
||||
// The async code mentioned is the invokation of the current function from the factoryListener.
|
||||
setTimeout(() => {
|
||||
let allResolved = true;
|
||||
|
||||
for (let i = 0; i < chunksSearchPromises.length; i++) {
|
||||
const isResolved = chunksSearchPromises[i]();
|
||||
|
||||
if (isResolved) {
|
||||
// Remove finished promises to avoid having to iterate through a huge array everytime
|
||||
chunksSearchPromises.splice(i--, 1);
|
||||
} else {
|
||||
allResolved = false;
|
||||
}
|
||||
chunkSearchResolvedGetters = chunkSearchResolvedGetters.filter(isResolvedGetter => !isResolvedGetter());
|
||||
if (chunkSearchResolvedGetters.length === 0) {
|
||||
chunkSearchingDoneResolve();
|
||||
}
|
||||
|
||||
if (allResolved) chunksSearchingResolve();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
|
||||
let isResolved = false;
|
||||
searchAndLoadLazyChunks(String(factory))
|
||||
.then(() => isResolved = true)
|
||||
.catch(() => isResolved = true);
|
||||
.finally(() => isResolved = true);
|
||||
|
||||
chunksSearchPromises.push(() => isResolved);
|
||||
chunkSearchResolvedGetters.push(() => isResolved);
|
||||
}
|
||||
|
||||
Webpack.factoryListeners.add(factoryListener);
|
||||
|
|
@ -176,7 +191,7 @@ export async function loadLazyChunks() {
|
|||
factoryListener(wreq.m[moduleId]);
|
||||
}
|
||||
|
||||
await chunksSearchingDone;
|
||||
await chunkSearchingDone;
|
||||
Webpack.factoryListeners.delete(factoryListener);
|
||||
|
||||
// Require deferred entry points
|
||||
|
|
@ -198,17 +213,16 @@ export async function loadLazyChunks() {
|
|||
});
|
||||
|
||||
await Promise.all(chunksLeft.map(async id => queue(async () => {
|
||||
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
|
||||
.then(r => r.text())
|
||||
.then(t => /importScripts\(|self\.postMessage/.test(t));
|
||||
// We will deadlock if we use the queue inside a queue func
|
||||
const isWorkerFile = await isWorkerAsset(wreq.p + wreq.u(id), false);
|
||||
|
||||
// Loads the chunk. Currently this only happens with the language packs which are loaded differently
|
||||
if (!isWorkerAsset) {
|
||||
if (!isWorkerFile) {
|
||||
await wreq.e(id);
|
||||
}
|
||||
})));
|
||||
|
||||
LazyChunkLoaderLogger.log("Finished loading all chunks!");
|
||||
chunksAlreadyLoaded = true;
|
||||
} catch (e) {
|
||||
LazyChunkLoaderLogger.log("A fatal error occurred:", e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ async function runReporter() {
|
|||
try {
|
||||
ReporterLogger.log("Starting test...");
|
||||
|
||||
const { promise: loadLazyChunksDone, resolve: loadLazyChunksResolve } = Promise.withResolvers<void>();
|
||||
const { promise: loadLazyChunksDone, resolve: loadLazyChunksDoneResolve } = Promise.withResolvers<void>();
|
||||
|
||||
// The main patch for starting the reporter chunk loading
|
||||
addPatch({
|
||||
|
|
@ -28,11 +28,10 @@ async function runReporter() {
|
|||
}
|
||||
}, "Vencord Reporter");
|
||||
|
||||
// initReporter is called in the patched entry point of Discord
|
||||
// @ts-expect-error
|
||||
Vencord.Webpack._initReporter = function () {
|
||||
// initReporter is called in the patched entry point of Discord
|
||||
// setImmediate to only start searching for lazy chunks after Discord initialized the app
|
||||
setTimeout(() => loadLazyChunks().then(loadLazyChunksResolve), 0);
|
||||
loadLazyChunks().then(loadLazyChunksDoneResolve);
|
||||
};
|
||||
|
||||
await loadLazyChunksDone;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue