mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Refactor runner to use a single instance of Metro for each run (#52777)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/52777 Changelog: [internal] This significantly speeds up test execution in Fantom (around 2x in OSS and 6x at Meta) by starting a Metro server before all tests runs and reusing it across all tests to build test bundles, instead of spinning up a new Metro instance every time we run each test. The architecture change (also considering the previous change in buck prebuilds) looks like this: {F1980689532} This is how is impacts execution times (compared to the baseline): * OSS * Before: 62s {F1980564286} * After: 30s (**2x faster**) {F1980564265} Reviewed By: andrewdacenko Differential Revision: D78741903 fbshipit-source-id: b209f88925e49cc2a2067e8df9b7fa9a29b4c8d2
This commit is contained in:
committed by
Facebook GitHub Bot
parent
3ecd48fa91
commit
5c2b9eda69
@@ -35,4 +35,6 @@ module.exports = {
|
||||
watchPathIgnorePatterns: ['<rootDir>/private/react-native-fantom/build/'],
|
||||
globalSetup:
|
||||
'<rootDir>/private/react-native-fantom/runner/global-setup/setup.js',
|
||||
globalTeardown:
|
||||
'<rootDir>/private/react-native-fantom/runner/global-setup/teardown.js',
|
||||
};
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {RunBuildOptions} from 'metro';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
type CreateBundleOptions = {
|
||||
...RunBuildOptions,
|
||||
out: $NonMaybeType<RunBuildOptions['out']>,
|
||||
testPath: string,
|
||||
};
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||
|
||||
export default async function createBundle({
|
||||
testPath,
|
||||
entry,
|
||||
out,
|
||||
platform,
|
||||
minify,
|
||||
dev,
|
||||
sourceMap,
|
||||
sourceMapUrl,
|
||||
}: CreateBundleOptions): Promise<void> {
|
||||
if (process.env.__FANTOM_METRO_PORT__ == null) {
|
||||
throw new Error(
|
||||
'Could not find Metro server port (process.env.__FANTOM_METRO_PORT__ not set by Fantom)',
|
||||
);
|
||||
}
|
||||
|
||||
const port = Number(process.env.__FANTOM_METRO_PORT__);
|
||||
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
||||
throw new Error(`Invalid port for Metro server: ${port}`);
|
||||
}
|
||||
|
||||
const requestPath = path
|
||||
.relative(PROJECT_ROOT, entry)
|
||||
.replace(/\.js$/, '.bundle');
|
||||
|
||||
const bundleURL = new URL(`http://localhost:${port}/${requestPath}`);
|
||||
|
||||
if (platform != null) {
|
||||
bundleURL.searchParams.append('platform', platform);
|
||||
}
|
||||
|
||||
if (minify != null) {
|
||||
bundleURL.searchParams.append('minify', minify ? 'true' : 'false');
|
||||
}
|
||||
|
||||
if (dev != null) {
|
||||
bundleURL.searchParams.append('dev', dev ? 'true' : 'false');
|
||||
}
|
||||
|
||||
if (sourceMap != null) {
|
||||
bundleURL.searchParams.append('sourceMap', sourceMap ? 'true' : 'false');
|
||||
}
|
||||
|
||||
if (sourceMapUrl != null) {
|
||||
bundleURL.searchParams.append('sourceMapUrl', sourceMapUrl);
|
||||
}
|
||||
|
||||
let bundleResult;
|
||||
let bundleError;
|
||||
|
||||
// Retry in case Metro hasn't seen the changes in the filesystem yet.
|
||||
// TODO(T231910841): Remove this when Metro fixes consistency issues when resolving HTTP requests.
|
||||
let attemps = 0;
|
||||
do {
|
||||
if (attemps > 0) {
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
try {
|
||||
bundleResult = await fetch(bundleURL);
|
||||
} catch (e) {
|
||||
bundleError = e;
|
||||
}
|
||||
|
||||
attemps++;
|
||||
} while (attemps < 3 && (bundleError || bundleResult?.status === 404));
|
||||
|
||||
if (bundleError || bundleResult?.ok !== true) {
|
||||
throw new Error(
|
||||
`Failed to request bundle from Metro: ${bundleError?.message ?? (await bundleResult?.text()) ?? ''}`,
|
||||
);
|
||||
}
|
||||
await fs.promises.writeFile(out, await bundleResult.text(), 'utf8');
|
||||
|
||||
try {
|
||||
const sourceMapURL = new URL(bundleURL.toString());
|
||||
sourceMapURL.pathname = sourceMapURL.pathname.replace(/\.bundle$/, '.map');
|
||||
const sourceMapResult = await fetch(sourceMapURL);
|
||||
if (!sourceMapResult.ok) {
|
||||
throw new Error();
|
||||
}
|
||||
await fs.promises.writeFile(
|
||||
out.replace(/\.js$/, '.map'),
|
||||
await sourceMapResult.text(),
|
||||
'utf8',
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(`Could not fetch source map from Metro for test ${testPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import createBundle from '../createBundle';
|
||||
import {isCI} from '../EnvironmentOptions';
|
||||
import {build as buildHermesCompiler} from '../executables/hermesc';
|
||||
import {build as buildFantomTester} from '../executables/tester';
|
||||
@@ -15,7 +16,6 @@ import {getNativeBuildOutputPath} from '../executables/utils';
|
||||
import {HermesVariant} from '../utils';
|
||||
// $FlowExpectedError[untyped-import]
|
||||
import fs from 'fs';
|
||||
import Metro from 'metro';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
@@ -58,10 +58,6 @@ export default async function build(): Promise<void> {
|
||||
}
|
||||
|
||||
async function warmUpMetro(isOptimizedMode: boolean): Promise<void> {
|
||||
const metroConfig = await Metro.loadConfig({
|
||||
config: path.resolve(__dirname, '..', '..', 'config', 'metro.config.js'),
|
||||
});
|
||||
|
||||
const entrypointPath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
@@ -75,7 +71,8 @@ async function warmUpMetro(isOptimizedMode: boolean): Promise<void> {
|
||||
`fantom-warmup-bundle-${Date.now()}.js`,
|
||||
);
|
||||
|
||||
await Metro.runBuild(metroConfig, {
|
||||
await createBundle({
|
||||
testPath: '(warmup bundle - no test path)',
|
||||
entry: entrypointPath,
|
||||
out: bundlePath,
|
||||
platform: 'android',
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
import {isOSS, validateEnvironmentVariables} from '../EnvironmentOptions';
|
||||
import build from './build';
|
||||
import Metro from 'metro';
|
||||
import path from 'path';
|
||||
|
||||
export default async function globalSetup(
|
||||
globalConfig: {...},
|
||||
@@ -19,7 +21,38 @@ export default async function globalSetup(
|
||||
|
||||
validateEnvironmentVariables();
|
||||
|
||||
await startMetroServer();
|
||||
|
||||
if (!isOSS) {
|
||||
await build();
|
||||
}
|
||||
}
|
||||
|
||||
async function startMetroServer() {
|
||||
const metroConfig = await Metro.loadConfig({
|
||||
config: path.resolve(__dirname, '..', '..', 'config', 'metro.config.js'),
|
||||
});
|
||||
|
||||
// We need to reuse the same port across runs because can only set environment
|
||||
// variables for workers in the first one.
|
||||
// $FlowExpectedError[cannot-write]
|
||||
metroConfig.server.port =
|
||||
process.env.__FANTOM_METRO_PORT__ != null
|
||||
? Number(process.env.__FANTOM_METRO_PORT__)
|
||||
: // Any available port
|
||||
0;
|
||||
|
||||
const server = await Metro.runServer(metroConfig, {
|
||||
waitForBundler: true,
|
||||
watch: true,
|
||||
});
|
||||
|
||||
if (process.env.__FANTOM_METRO_PORT__ == null) {
|
||||
process.env.__FANTOM_METRO_PORT__ = String(
|
||||
server.httpServer.address().port,
|
||||
);
|
||||
}
|
||||
|
||||
// $FlowExpectedError[prop-missing]
|
||||
globalThis.__METRO_SERVER__ = server;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {RunServerResult} from 'metro';
|
||||
|
||||
type MetroServer = $NonMaybeType<RunServerResult?.['httpServer']>;
|
||||
|
||||
declare var __METRO_SERVER__: ?RunServerResult;
|
||||
|
||||
function getMetroServer(): ?MetroServer {
|
||||
return typeof __METRO_SERVER__ !== 'undefined' && __METRO_SERVER__ != null
|
||||
? __METRO_SERVER__.httpServer
|
||||
: null;
|
||||
}
|
||||
|
||||
export default async function globalTeardown(
|
||||
globalConfig: {...},
|
||||
projectConfig: {...},
|
||||
): Promise<void> {
|
||||
const metroServer = getMetroServer();
|
||||
if (metroServer) {
|
||||
await stopMetroServer(metroServer);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopMetroServer(metroServer: MetroServer): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
metroServer.close(error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
require('../../../../scripts/shared/babelRegister').registerForMonorepo();
|
||||
|
||||
module.exports = require('./globalTeardown');
|
||||
+3
-6
@@ -20,6 +20,7 @@ import type {
|
||||
HermesVariant,
|
||||
} from './utils';
|
||||
|
||||
import createBundle from './createBundle';
|
||||
import entrypointTemplate from './entrypoint-template';
|
||||
import * as EnvironmentOptions from './EnvironmentOptions';
|
||||
import {run as runHermesCompiler} from './executables/hermesc';
|
||||
@@ -41,7 +42,6 @@ import fs from 'fs';
|
||||
// $FlowExpectedError[untyped-import]
|
||||
import {formatResultsErrors} from 'jest-message-util';
|
||||
import {SnapshotState, buildSnapshotResolver} from 'jest-snapshot';
|
||||
import Metro from 'metro';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
@@ -210,10 +210,6 @@ module.exports = async function runTest(
|
||||
const testContents = fs.readFileSync(testPath, 'utf8');
|
||||
const testConfigs = getFantomTestConfigs(testPath, testContents);
|
||||
|
||||
const metroConfig = await Metro.loadConfig({
|
||||
config: path.resolve(__dirname, '..', 'config', 'metro.config.js'),
|
||||
});
|
||||
|
||||
const setupModulePath = path.resolve(__dirname, '../runtime/setup.js');
|
||||
const featureFlagsModulePath = path.resolve(
|
||||
__dirname,
|
||||
@@ -305,7 +301,8 @@ module.exports = async function runTest(
|
||||
path.basename(testJSBundlePath, '.js') + '.map',
|
||||
);
|
||||
|
||||
await Metro.runBuild(metroConfig, {
|
||||
await createBundle({
|
||||
testPath,
|
||||
entry: entrypointPath,
|
||||
out: testJSBundlePath,
|
||||
platform: 'android',
|
||||
|
||||
+7
-2
@@ -6,9 +6,14 @@
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
if [[ -f "BUCK" && -z "$FANTOM_FORCE_OSS_BUILD" ]]; then
|
||||
JS_DIR='..' yarn jest --config private/react-native-fantom/config/jest.config.js "$@"
|
||||
export JS_DIR='..'
|
||||
else
|
||||
yarn workspace @react-native/fantom build
|
||||
FANTOM_FORCE_OSS_BUILD=1 yarn jest --config private/react-native-fantom/config/jest.config.js "$@"
|
||||
export FANTOM_FORCE_OSS_BUILD=1
|
||||
fi
|
||||
|
||||
export NODE_OPTIONS='--max-old-space-size=8192'
|
||||
|
||||
yarn jest --config private/react-native-fantom/config/jest.config.js "$@"
|
||||
|
||||
Reference in New Issue
Block a user