diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index bbe931e5f..cc762602b 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -95,11 +95,13 @@ async function start() { const depVizUrl = `${BUILD_LINK_BASE}/build-artifacts/build-viz/index.html`; const depVizLink = `Build System`; const moduleInitStatsBackgroundUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/mv3/initialisation/background/index.html`; - const moduleInitStatsBackgroundLink = `MV3 Background Module Init Stats`; + const moduleInitStatsBackgroundLink = `Background Module Init Stats`; const moduleInitStatsUIUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/mv3/initialisation/ui/index.html`; - const moduleInitStatsUILink = `MV3 UI Init Stats`; + const moduleInitStatsUILink = `UI Init Stats`; const moduleLoadStatsUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/mv3/load_time/index.html`; const moduleLoadStatsLink = `Module Load Stats`; + const bundleSizeStatsUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/mv3/bundle_size.json`; + const bundleSizeStatsLink = `Bundle Size Stats`; // link to artifacts const allArtifactsUrl = `https://circleci.com/gh/MetaMask/metamask-extension/${CIRCLE_BUILD_NUM}#artifacts/containers/0`; @@ -112,6 +114,7 @@ async function start() { `mv3: ${moduleInitStatsBackgroundLink}`, `mv3: ${moduleInitStatsUILink}`, `mv3: ${moduleLoadStatsLink}`, + `mv3: ${bundleSizeStatsLink}`, `code coverage: ${coverageLink}`, `storybook: ${storybookLink}`, `all artifacts`, diff --git a/test/e2e/mv3-perf-stats/bundle-size.js b/test/e2e/mv3-perf-stats/bundle-size.js new file mode 100644 index 000000000..0cb0c13ac --- /dev/null +++ b/test/e2e/mv3-perf-stats/bundle-size.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +/* eslint-disable node/shebang */ +const path = require('path'); +const { promises: fs } = require('fs'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); +const { + isWritable, + getFirstParentDirectoryThatExists, +} = require('../../helpers/file'); + +const { exitWithError } = require('../../../development/lib/exit-with-error'); + +/** + * The e2e test case is used to capture bundle time statistics for extension. + */ + +const backgroundFiles = [ + 'runtime-lavamoat.js', + 'lockdown-more.js', + 'globalthis.js', + 'sentry-install.js', + 'policy-load.js', +]; + +const uiFiles = [ + 'globalthis.js', + 'sentry-install.js', + 'runtime-lavamoat.js', + 'lockdown-more.js', + 'policy-load.js', +]; + +const BackgroundFileRegex = /background-[0-9]*.js/u; +const CommonFileRegex = /common-[0-9]*.js/u; +const UIFileRegex = /ui-[0-9]*.js/u; + +async function main() { + const { argv } = yargs(hideBin(process.argv)).usage( + '$0 [options]', + 'Run a page load benchmark', + (_yargs) => + _yargs.option('out', { + description: + 'Output filename. Output printed to STDOUT of this is omitted.', + type: 'string', + normalize: true, + }), + ); + const { out } = argv; + + const distFolder = 'dist/chrome'; + const backgroundFileList = []; + const uiFileList = []; + + const files = fs.readdir(distFolder); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (CommonFileRegex.test(file)) { + const stats = await fs.stat(`${distFolder}/${file}`); + backgroundFileList.push({ name: file, size: stats.size }); + uiFileList.push({ name: file, size: stats.size }); + } else if ( + backgroundFiles.includes(file) || + BackgroundFileRegex.test(file) + ) { + const stats = await fs.stat(`${distFolder}/${file}`); + backgroundFileList.push({ name: file, size: stats.size }); + } else if (uiFiles.includes(file) || UIFileRegex.test(file)) { + const stats = await fs.stat(`${distFolder}/${file}`); + uiFileList.push({ name: file, size: stats.size }); + } + } + + const backgroundBundleSize = backgroundFileList.reduce( + (result, file) => result + file.size, + 0, + ); + + const uiBundleSize = uiFileList.reduce( + (result, file) => result + file.size, + 0, + ); + + const result = { + background: { + name: 'background', + size: backgroundBundleSize, + fileList: backgroundFileList, + }, + ui: { + name: 'ui', + size: uiBundleSize, + fileList: uiFileList, + }, + }; + + if (out) { + const outPath = `${out}/bundle_size.json`; + const outputDirectory = path.dirname(outPath); + const existingParentDirectory = await getFirstParentDirectoryThatExists( + outputDirectory, + ); + if (!(await isWritable(existingParentDirectory))) { + throw new Error('Specified output file directory is not writable'); + } + if (outputDirectory !== existingParentDirectory) { + await fs.mkdir(outputDirectory, { recursive: true }); + } + await fs.writeFile(outPath, JSON.stringify(result, null, 2)); + } else { + console.log(JSON.stringify(result, null, 2)); + } +} + +main().catch((error) => { + exitWithError(error); +}); diff --git a/test/e2e/mv3-perf-stats/index.js b/test/e2e/mv3-perf-stats/index.js new file mode 100644 index 000000000..4e56a2385 --- /dev/null +++ b/test/e2e/mv3-perf-stats/index.js @@ -0,0 +1,2 @@ +require('./init-load-stats'); +require('./bundle-size'); diff --git a/test/e2e/mv3-perf-stats/init-load-stats.js b/test/e2e/mv3-perf-stats/init-load-stats.js new file mode 100644 index 000000000..de5194031 --- /dev/null +++ b/test/e2e/mv3-perf-stats/init-load-stats.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +/* eslint-disable node/shebang */ +const path = require('path'); +const { promises: fs } = require('fs'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); + +const { exitWithError } = require('../../../development/lib/exit-with-error'); +const { + isWritable, + getFirstParentDirectoryThatExists, +} = require('../../helpers/file'); +const { withFixtures, tinyDelayMs } = require('../helpers'); + +/** + * The e2e test case is used to capture load and initialisation time statistics for extension in MV3 environment. + */ + +async function profilePageLoad() { + const parsedLogs = {}; + try { + await withFixtures({ fixtures: 'imported-account' }, async ({ driver }) => { + await driver.delay(tinyDelayMs); + await driver.navigate(); + await driver.delay(1000); + const logs = await driver.checkBrowserForLavamoatLogs(); + + let logString = ''; + let logType = ''; + + logs.forEach((log) => { + if (log.indexOf('"version": 1') >= 0) { + // log end here + logString += log; + parsedLogs[logType] = JSON.parse(`{${logString}}`); + logString = ''; + logType = ''; + } else if (logType) { + // log string continues + logString += log; + } else if ( + log.search(/"name": ".*app\/scripts\/background.js",/u) >= 0 + ) { + // background log starts + logString += log; + logType = 'background'; + } else if (log.search(/"name": ".*app\/scripts\/ui.js",/u) >= 0) { + // ui log starts + logString += log; + logType = 'ui'; + } else if (log.search(/"name": "Total"/u) >= 0) { + // load time log starts + logString += log; + logType = 'loadTime'; + } + }); + }); + } catch (error) { + console.log('Error in trying to parse logs.'); + } + return parsedLogs; +} + +async function main() { + const { argv } = yargs(hideBin(process.argv)).usage( + '$0 [options]', + 'Run a page load benchmark', + (_yargs) => + _yargs.option('out', { + description: + 'Output filename. Output printed to STDOUT of this is omitted.', + type: 'string', + normalize: true, + }), + ); + + const results = await profilePageLoad(); + const { out } = argv; + + const logCategories = [ + { key: 'background', dirPath: 'initialisation/background/stacks.json' }, + { key: 'ui', dirPath: 'initialisation/ui/stacks.json' }, + { key: 'loadTime', dirPath: 'load_time/stats.json' }, + ]; + + if (out) { + logCategories.forEach(async ({ key, dirPath }) => { + if (results[key]) { + const outPath = `${out}/${dirPath}`; + const outputDirectory = path.dirname(outPath); + const existingParentDirectory = await getFirstParentDirectoryThatExists( + outputDirectory, + ); + if (!(await isWritable(existingParentDirectory))) { + throw new Error('Specified output file directory is not writable'); + } + if (outputDirectory !== existingParentDirectory) { + await fs.mkdir(outputDirectory, { recursive: true }); + } + await fs.writeFile(outPath, JSON.stringify(results[key], null, 2)); + } + }); + } else { + console.log(JSON.stringify(results, null, 2)); + } +} + +main().catch((error) => { + exitWithError(error); +});