Refactor Fantom runner to decouple compilation from execution to speed up test execution (#52758)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52758

Changelog: [internal]

This is a change to how Fantom tests run in Meta infra via Buck.

After this, the biggest opportunity will be optimizing how we generate bundles with Metro.

Reviewed By: christophpurrer

Differential Revision: D78672863

fbshipit-source-id: 1152907f3ba60e7d2e48bcc588f3c07aef7bb393
This commit is contained in:
Rubén Norte authored and Facebook GitHub Bot committed 2025-08-04 05:56:51 -07:00
1 parent f047c9b42b
commit 3ecd48fa91
6 files changed
+259 -81

No files matched your search

@@ -0,0 +1,85 @@
/**
* 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 {HermesVariant, SyncCommandResult} from '../utils';
import {isCI} from '../EnvironmentOptions';
import {
getBuckModesForPlatform,
getBuckOptionsForHermes,
getDebugInfoFromCommandResult,
getHermesCompilerTarget,
runBuck2Sync,
runCommandSync,
} from '../utils';
import {getNativeBuildOutputPath} from './utils';
import fs from 'fs';
import path from 'path';
type TesterOptions = $ReadOnly<{
isOptimizedMode: boolean,
hermesVariant: HermesVariant,
}>;
function getHermesCompilerPath({
isOptimizedMode,
hermesVariant,
}: TesterOptions): string {
return path.join(
getNativeBuildOutputPath(),
`hermesc-${(hermesVariant as string).toLowerCase()}-${isOptimizedMode ? 'opt' : 'dev'}`,
);
}
export function build(options: TesterOptions): void {
const destPath = getHermesCompilerPath(options);
if (fs.existsSync(destPath)) {
return;
}
const tmpPath = destPath + '-' + Date.now();
try {
const result = runBuck2Sync([
'build',
...getBuckModesForPlatform(options.isOptimizedMode),
...getBuckOptionsForHermes(options.hermesVariant),
getHermesCompilerTarget(options.hermesVariant),
'--out',
tmpPath,
]);
if (result.status !== 0) {
throw new Error(getDebugInfoFromCommandResult(result));
}
if (fs.existsSync(destPath)) {
// Another test might have compiled the binary after our initial check.
return;
}
fs.renameSync(tmpPath, destPath);
} finally {
try {
fs.unlinkSync(tmpPath);
} catch {}
}
}
export function run(
args: $ReadOnlyArray<string>,
options: TesterOptions,
): SyncCommandResult {
if (!isCI) {
build(options);
}
return runCommandSync(getHermesCompilerPath(options), args);
}
+108
View File
@@ -0,0 +1,108 @@
/**
* 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 {AsyncCommandResult, HermesVariant} from '../utils';
import {debugCpp, isCI} from '../EnvironmentOptions';
import {
getBuckModesForPlatform,
getBuckOptionsForHermes,
getDebugInfoFromCommandResult,
runBuck2,
runBuck2Sync,
runCommand,
} from '../utils';
import {getNativeBuildOutputPath} from './utils';
import fs from 'fs';
import path from 'path';
const FANTOM_TESTER_BUCK_TARGET =
'fbsource//xplat/js/react-native-github/private/react-native-fantom/tester:tester';
type TesterOptions = $ReadOnly<{
isOptimizedMode: boolean,
hermesVariant: HermesVariant,
}>;
function getFantomTesterPath({
isOptimizedMode,
hermesVariant,
}: TesterOptions): string {
return path.join(
getNativeBuildOutputPath(),
`fantom-tester-${(hermesVariant as string).toLowerCase()}-${isOptimizedMode ? 'opt' : 'dev'}`,
);
}
export function build(options: TesterOptions): void {
const destPath = getFantomTesterPath(options);
if (fs.existsSync(destPath)) {
return;
}
const tmpPath = destPath + '-' + Date.now();
try {
const result = runBuck2Sync([
'build',
...getBuckModesForPlatform(options.isOptimizedMode),
...getBuckOptionsForHermes(options.hermesVariant),
FANTOM_TESTER_BUCK_TARGET,
'--out',
tmpPath,
]);
if (result.status !== 0) {
throw new Error(getDebugInfoFromCommandResult(result));
}
if (fs.existsSync(destPath)) {
// Another test might have compiled the binary after our initial check.
return;
}
fs.renameSync(tmpPath, destPath);
} finally {
try {
fs.unlinkSync(tmpPath);
} catch {}
}
}
export function run(
args: $ReadOnlyArray<string>,
options: TesterOptions,
): AsyncCommandResult {
if (isCI && debugCpp) {
throw new Error('Cannot run Fantom with C++ debugging on CI');
}
if (!isCI && !debugCpp) {
build(options);
}
if (debugCpp) {
return runBuck2(
[
'run',
...getBuckModesForPlatform(options.isOptimizedMode),
...getBuckOptionsForHermes(options.hermesVariant),
FANTOM_TESTER_BUCK_TARGET,
'--',
...args,
],
{
withFDB: true,
},
);
}
return runCommand(getFantomTesterPath(options), args);
}
+23
View File
@@ -0,0 +1,23 @@
/**
* 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 path from 'path';
const BUILD_OUTPUT_ROOT = path.resolve(
__dirname,
'..',
'..',
'build',
'native',
);
export function getNativeBuildOutputPath(): string {
return BUILD_OUTPUT_ROOT;
}
+17 -47
View File
@@ -9,11 +9,10 @@
*/
import {isCI} from '../EnvironmentOptions';
import {
getBuckModesForPlatform,
getDebugInfoFromCommandResult,
runBuck2Sync,
} from '../utils';
import {build as buildHermesCompiler} from '../executables/hermesc';
import {build as buildFantomTester} from '../executables/tester';
import {getNativeBuildOutputPath} from '../executables/utils';
import {HermesVariant} from '../utils';
// $FlowExpectedError[untyped-import]
import fs from 'fs';
import Metro from 'metro';
@@ -39,23 +38,20 @@ async function tryOrLog(
}
export default async function build(): Promise<void> {
try {
fs.rmSync(getNativeBuildOutputPath(), {recursive: true});
} catch {}
fs.mkdirSync(getNativeBuildOutputPath(), {recursive: true});
if (isCI) {
await tryOrLog(
() => warmUpHermesCompiler(false),
'Error warming up Hermes compiler (dev)',
);
await tryOrLog(
() => warmUpHermesCompiler(true),
'Error warming up Hermes compiler (opt)',
);
await tryOrLog(
() => warmUpRNTesterCLI(false),
'Error warming up RN Tester CLI (dev)',
);
await tryOrLog(
() => warmUpRNTesterCLI(true),
'Error warming up RN Tester CLI (opt)',
);
for (const isOptimizedMode of [false, true]) {
for (const hermesVariant of HermesVariant.members()) {
buildFantomTester({isOptimizedMode, hermesVariant});
buildHermesCompiler({isOptimizedMode, hermesVariant});
}
}
await tryOrLog(() => warmUpMetro(false), 'Error warming up Metro (dev)');
await tryOrLog(() => warmUpMetro(true), 'Error warming up Metro (opt)');
}
@@ -91,29 +87,3 @@ async function warmUpMetro(isOptimizedMode: boolean): Promise<void> {
fs.unlinkSync(bundlePath);
} catch {}
}
function warmUpHermesCompiler(isOptimizedMode: boolean): void {
const buildHermesCompilerCommandResult = runBuck2Sync([
'build',
...getBuckModesForPlatform(isOptimizedMode),
'//xplat/hermes/tools/hermesc:hermesc',
]);
if (buildHermesCompilerCommandResult.status !== 0) {
throw new Error(
getDebugInfoFromCommandResult(buildHermesCompilerCommandResult),
);
}
}
function warmUpRNTesterCLI(isOptimizedMode: boolean): void {
const buildRNTesterCommandResult = runBuck2Sync([
'build',
...getBuckModesForPlatform(isOptimizedMode),
'//xplat/js/react-native-github/private/react-native-fantom/tester:tester',
]);
if (buildRNTesterCommandResult.status !== 0) {
throw new Error(getDebugInfoFromCommandResult(buildRNTesterCommandResult));
}
}
+11 -24
View File
@@ -22,6 +22,8 @@ import type {
import entrypointTemplate from './entrypoint-template';
import * as EnvironmentOptions from './EnvironmentOptions';
import {run as runHermesCompiler} from './executables/hermesc';
import {run as runFantomTester} from './executables/tester';
import formatFantomConfig from './formatFantomConfig';
import getFantomTestConfigs from './getFantomTestConfigs';
import {
@@ -30,13 +32,8 @@ import {
} from './snapshotUtils';
import {
HermesVariant as HermesVariantEnum,
getBuckModesForPlatform,
getBuckOptionsForHermes,
getDebugInfoFromCommandResult,
getHermesCompilerTarget,
printConsoleLog,
runBuck2,
runBuck2Sync,
runCommand,
symbolicateStackTrace,
} from './utils';
@@ -163,13 +160,8 @@ function generateBytecodeBundle({
isOptimizedMode: boolean,
hermesVariant: HermesVariant,
}): void {
const hermesCompilerCommandResult = runBuck2Sync(
const hermesCompilerCommandResult = runHermesCompiler(
[
'run',
...getBuckModesForPlatform(isOptimizedMode),
...getBuckOptionsForHermes(hermesVariant),
getHermesCompilerTarget(hermesVariant),
'--',
'-emit-binary',
isOptimizedMode ? '-O' : null,
'-max-diagnostic-width',
@@ -178,6 +170,10 @@ function generateBytecodeBundle({
bytecodePath,
sourcePath,
].filter(Boolean),
{
isOptimizedMode,
hermesVariant,
},
);
if (hermesCompilerCommandResult.status !== 0) {
@@ -342,19 +338,10 @@ module.exports = async function runTest(
path.join(__dirname, '..', 'build', 'tester', 'fantom_tester'),
rnTesterCommandArgs,
)
: runBuck2(
[
'run',
...getBuckModesForPlatform(testConfig.isNativeOptimized),
...getBuckOptionsForHermes(testConfig.hermesVariant),
'//xplat/js/react-native-github/private/react-native-fantom/tester:tester',
'--',
...rnTesterCommandArgs,
],
{
withFDB: EnvironmentOptions.debugCpp,
},
);
: runFantomTester(rnTesterCommandArgs, {
isOptimizedMode: testConfig.isNativeOptimized,
hermesVariant: testConfig.hermesVariant,
});
const processedResult = await processRNTesterCommandResult(
rnTesterCommandResult,
+15 -10
View File
@@ -96,7 +96,7 @@ export type SyncCommandResult = {
stderr: string,
};
function maybeLogCommand(command: string, args: Array<string>): void {
function maybeLogCommand(command: string, args: $ReadOnlyArray<string>): void {
if (EnvironmentOptions.logCommands) {
console.log(`RUNNING \`${command} ${args.join(' ')}\``);
}
@@ -104,17 +104,22 @@ function maybeLogCommand(command: string, args: Array<string>): void {
export function runCommand(
command: string,
args: Array<string>,
args: $ReadOnlyArray<string>,
): AsyncCommandResult {
maybeLogCommand(command, args);
const childProcess = spawn(command, args, {
encoding: 'utf8',
env: {
...process.env,
PATH: `/usr/local/bin:${process.env.PATH ?? ''}`,
const childProcess = spawn(
command,
// spawn is typed with Array instead of with $ReadOnlyArray
[...args],
{
encoding: 'utf8',
env: {
...process.env,
PATH: `/usr/local/bin:${process.env.PATH ?? ''}`,
},
},
});
);
const result: AsyncCommandResult = {
childProcess,
@@ -143,11 +148,11 @@ export function runCommand(
export function runCommandSync(
command: string,
args: Array<string>,
args: $ReadOnlyArray<string>,
): SyncCommandResult {
maybeLogCommand(command, args);
const result = spawnSync(command, args, {
const result = spawnSync(command, [...args], {
encoding: 'utf8',
env: {
...process.env,