From 3ecd48fa911a0c0201041ddceeacf1069a1b9d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 4 Aug 2025 05:56:51 -0700 Subject: [PATCH] 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 --- .../runner/executables/hermesc.js | 85 ++++++++++++++ .../runner/executables/tester.js | 108 ++++++++++++++++++ .../runner/executables/utils.js | 23 ++++ .../runner/global-setup/build.js | 64 +++-------- private/react-native-fantom/runner/runner.js | 35 ++---- private/react-native-fantom/runner/utils.js | 25 ++-- 6 files changed, 259 insertions(+), 81 deletions(-) create mode 100644 private/react-native-fantom/runner/executables/hermesc.js create mode 100644 private/react-native-fantom/runner/executables/tester.js create mode 100644 private/react-native-fantom/runner/executables/utils.js diff --git a/private/react-native-fantom/runner/executables/hermesc.js b/private/react-native-fantom/runner/executables/hermesc.js new file mode 100644 index 00000000000..63b0b2406fd --- /dev/null +++ b/private/react-native-fantom/runner/executables/hermesc.js @@ -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, + options: TesterOptions, +): SyncCommandResult { + if (!isCI) { + build(options); + } + + return runCommandSync(getHermesCompilerPath(options), args); +} diff --git a/private/react-native-fantom/runner/executables/tester.js b/private/react-native-fantom/runner/executables/tester.js new file mode 100644 index 00000000000..82ab82e9e21 --- /dev/null +++ b/private/react-native-fantom/runner/executables/tester.js @@ -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, + 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); +} diff --git a/private/react-native-fantom/runner/executables/utils.js b/private/react-native-fantom/runner/executables/utils.js new file mode 100644 index 00000000000..5f9521391ba --- /dev/null +++ b/private/react-native-fantom/runner/executables/utils.js @@ -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; +} diff --git a/private/react-native-fantom/runner/global-setup/build.js b/private/react-native-fantom/runner/global-setup/build.js index e8ad236b627..54ed59f302e 100644 --- a/private/react-native-fantom/runner/global-setup/build.js +++ b/private/react-native-fantom/runner/global-setup/build.js @@ -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 { + 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 { 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)); - } -} diff --git a/private/react-native-fantom/runner/runner.js b/private/react-native-fantom/runner/runner.js index 2633bbf80f5..36fb632bbc4 100644 --- a/private/react-native-fantom/runner/runner.js +++ b/private/react-native-fantom/runner/runner.js @@ -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, diff --git a/private/react-native-fantom/runner/utils.js b/private/react-native-fantom/runner/utils.js index 6b41733ce0a..248eb6e45b0 100644 --- a/private/react-native-fantom/runner/utils.js +++ b/private/react-native-fantom/runner/utils.js @@ -96,7 +96,7 @@ export type SyncCommandResult = { stderr: string, }; -function maybeLogCommand(command: string, args: Array): void { +function maybeLogCommand(command: string, args: $ReadOnlyArray): void { if (EnvironmentOptions.logCommands) { console.log(`RUNNING \`${command} ${args.join(' ')}\``); } @@ -104,17 +104,22 @@ function maybeLogCommand(command: string, args: Array): void { export function runCommand( command: string, - args: Array, + args: $ReadOnlyArray, ): 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, + args: $ReadOnlyArray, ): SyncCommandResult { maybeLogCommand(command, args); - const result = spawnSync(command, args, { + const result = spawnSync(command, [...args], { encoding: 'utf8', env: { ...process.env,