From e1bb814c88acedb954d331cf8b7af91c4ff555a3 Mon Sep 17 00:00:00 2001 From: CanadaHonk Date: Thu, 26 Jan 2023 23:08:57 +0000 Subject: [PATCH] v8Cache: initial add --- gluon.d.ts | 52 +++++++++++++++++++++ src/api/v8Cache.js | 102 +++++++++++++++++++++++++++++++++++++++++ src/index.js | 3 +- src/launcher/inject.js | 4 +- 4 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 src/api/v8Cache.js diff --git a/gluon.d.ts b/gluon.d.ts index de90626..96c7615 100644 --- a/gluon.d.ts +++ b/gluon.d.ts @@ -1,3 +1,55 @@ +type V8CacheBuildOptions = { + /** + * Path to save the V8 Cache to. Defaults to v8Cache.json in Gluon's browser data. + */ + path?: string, + + /** + * Use eager compilation. + * @default true + */ + eager?: Boolean, + + /** + * URLs of scripts to cache. Defaults to automatically detecting currently loaded scripts in the page. + */ + urls?: string[], + + /** + * Reload the page to force script compilation. + * @default true + */ + reload?: Boolean, + + /** + * Include preload scripts in automatic detection. + * @default true + */ + includePreload?: Boolean +}; + +type V8CacheApi = { + /** Build a V8 Cache. */ + build( + /** Build options. */ + options?: V8CacheBuildOptions + ), + + /** Load a V8 Cache. */ + load( + /** + * Path to load. Defaults to v8Cache.json in Gluon's browser data. + */ + path?: string + ): Promise + + /** Check if a V8 Cache exists with a given path. */ + exists( + /** Path to check. */ + path: string + ): Promise +}; + type PageApi = { /** * Evaluate a string or function in the web context. diff --git a/src/api/v8Cache.js b/src/api/v8Cache.js new file mode 100644 index 0000000..b13816d --- /dev/null +++ b/src/api/v8Cache.js @@ -0,0 +1,102 @@ +import { join, sep } from 'path'; +import { access, writeFile, readFile } from 'fs/promises'; +import { log } from '../lib/logger.js'; + +export default async (CDP, evaluate, { browserType, dataPath }) => { + if (browserType !== 'chromium') { // current implementation is for chromium-based only + const warning = () => log(`Warning: V8 Cache API is only for Chromium (running on ${browserType})`); + + return { + build: () => {}, + load: () => false, + exists: () => false + }; + } + + await CDP.send('Page.enable'); + + const getDefaultPath = () => join(dataPath, 'v8Cache.json'); + const getScriptUrls = async (includePreload = true) => (await evaluate(`[...document.querySelectorAll('script[src]${includePreload ? ', link[as=script]' : ''}')].map(x => x.src ?? x.href).join(';')`)).split(';'); + + const build = async ({ path = getDefaultPath(), eager = true, urls = [], reload = true, includePreload = true, finishOnLoad = true } = {}) => { + const startTime = performance.now(); + + log('v8Cache: beginning cache build...'); + urls ??= await getScriptUrls(includePreload); + + log(`v8Cache: found ${urls.length} scripts`); + + const cache = await new Promise(async resolve => { + let produced = [], done = false; + + const unhook = CDP.on('Page.compilationCacheProduced', ({ params: { url, data }}) => { + // console.log('produced', url); + produced.push({ url, data }); + + process.stdout.write(`v8Cache: caching... (${produced.length}/${urls.length})\r`); + + if (produced.length >= urls.length) { + done = true; + unhook(); + resolve(produced); + } + }); + + await CDP.send('Page.produceCompilationCache', { + scripts: urls.map(url => ({ url, eager })) + }); + + if (reload) { + if (finishOnLoad) CDP.on('Page.frameStoppedLoading', async () => { + // console.log('loaded'); + await new Promise(res => setTimeout(res, 2000)); + if (done) return; + + log('v8Cache: forcing caching to end early as loading is done'); + + unhook(); + resolve(produced); + }, true); + + log('v8Cache: reloading to force script loads...'); + await CDP.send('Page.reload'); + } + + process.stdout.write(`v8Cache: starting cache...\r`); + }); + + log(`v8Cache: cached ${cache.length}/${urls.length} scripts in ${(performance.now() - startTime).toFixed(2)}ms`); + + const raw = JSON.stringify(cache, null, 2); + await writeFile(path, raw); + + log(`v8Cache: saved to .../${path.split(sep).slice(-3).join('/')} (${(Buffer.byteLength(raw, 'utf8') / 1024 / 1024).toFixed(2)}MB)`); + }; + + const exists = (path = getDefaultPath()) => access(path).then(() => true).catch(() => false); + + const load = async (path = getDefaultPath()) => { + if (!await exists(path)) return false; + const startTime = performance.now(); + + const cache = JSON.parse(await readFile(path, 'utf8')); + + for (const entry of cache) { + // console.log('loaded', entry); + CDP.send('Page.addCompilationCache', entry); + } + + log(`v8Cache: loaded ${cache.length} scripts in ${(performance.now() - startTime).toFixed(2)}ms`); + + return true; + }; + + // try to load default ASAP + load(); + + return { + build, + load, + exists + }; +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js index c57fc26..cad70a9 100644 --- a/src/index.js +++ b/src/index.js @@ -206,7 +206,8 @@ const startBrowser = async (url, { windowSize, forceBrowser, forceEngine }) => { localUrl, openingLocal, closeHandlers, - browserType + browserType, + dataPath }); return Window; diff --git a/src/launcher/inject.js b/src/launcher/inject.js index c27975e..208af8d 100644 --- a/src/launcher/inject.js +++ b/src/launcher/inject.js @@ -5,6 +5,7 @@ import LocalCDP from '../lib/local/cdp.js'; import IdleApi from '../api/idle.js'; import ControlsApi from '../api/controls.js'; +import V8CacheApi from '../api/v8Cache.js'; const acquireTarget = async (CDP, filter = () => true) => { let target; @@ -25,7 +26,7 @@ const acquireTarget = async (CDP, filter = () => true) => { })).sessionId; }; -export default async (CDP, proc, injectionType = 'browser', { browserName, browserType, openingLocal, localUrl, url, closeHandlers }) => { +export default async (CDP, proc, injectionType = 'browser', { dataPath, browserName, browserType, openingLocal, localUrl, url, closeHandlers }) => { let pageLoadCallback, pageLoadPromise = new Promise(res => pageLoadCallback = res); let frameLoadCallback = () => {}, onWindowMessage = () => {}; CDP.onMessage(msg => { @@ -125,6 +126,7 @@ export default async (CDP, proc, injectionType = 'browser', { browserName, brows Window.idle = await IdleApi(Window.cdp, { browserType, closeHandlers }); Window.controls = await ControlsApi(Window.cdp); + Window.v8Cache = await V8CacheApi(Window.cdp, evalInWindow, { browserType, dataPath }); return Window; }; \ No newline at end of file