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);
+});