diff --git a/private/react-native-fantom/runner/EnvironmentOptions.js b/private/react-native-fantom/runner/EnvironmentOptions.js index 2e089fefd76..53998ff1358 100644 --- a/private/react-native-fantom/runner/EnvironmentOptions.js +++ b/private/react-native-fantom/runner/EnvironmentOptions.js @@ -17,6 +17,7 @@ const VALID_ENVIRONMENT_VARIABLES = [ 'FANTOM_LOG_COMMANDS', 'FANTOM_PRINT_OUTPUT', 'FANTOM_PROFILE_JS', + 'FANTOM_ENABLE_JS_MEMORY_INSTRUMENTATION', ]; /** @@ -63,6 +64,10 @@ export const forceTestModeForBenchmarks: boolean = Boolean( export const profileJS: boolean = Boolean(process.env.FANTOM_PROFILE_JS); +export const enableJSMemoryInstrumentation: boolean = Boolean( + process.env.FANTOM_ENABLE_JS_MEMORY_INSTRUMENTATION, +); + /** * Throws an error if there is an environment variable defined with the FANTOM_ * prefix that is not recognized. @@ -78,4 +83,15 @@ export function validateEnvironmentVariables(): void { ); } } + + // Enabling memory instrumentation is only necessary when taking JS heap + // snapshots in optimized builds (where it is disabled by default). + // This isn't supported in CI or in OSS because that would require adding + // another dimension to the build matrix, duplicating the number of binaries + // we need to build before starting test execution. + if ((isCI || isOSS) && enableJSMemoryInstrumentation) { + throw new Error( + 'Memory instrumentation is not supported in CI or OSS environments, as it requires a custom Hermes build that is not prebuilt in those environments.', + ); + } } diff --git a/private/react-native-fantom/runner/entrypoint-template.js b/private/react-native-fantom/runner/entrypoint-template.js index 2bc3f330f6f..715059ca334 100644 --- a/private/react-native-fantom/runner/entrypoint-template.js +++ b/private/react-native-fantom/runner/entrypoint-template.js @@ -22,12 +22,16 @@ module.exports = function entrypointTemplate({ testConfig, snapshotConfig, jsTraceOutputPath, + jsHeapSnapshotOutputPathTemplate, + jsHeapSnapshotOutputPathTemplateToken, }: { testPath: string, setupModulePath: string, featureFlagsModulePath: string, testConfig: FantomTestConfig, snapshotConfig: SnapshotConfig, + jsHeapSnapshotOutputPathTemplate: string, + jsHeapSnapshotOutputPathTemplateToken: string, jsTraceOutputPath: ?string, }): string { const constants: FantomRuntimeConstants = { @@ -36,6 +40,8 @@ module.exports = function entrypointTemplate({ forceTestModeForBenchmarks: EnvironmentOptions.forceTestModeForBenchmarks, fantomConfigSummary: formatFantomConfig(testConfig), jsTraceOutputPath, + jsHeapSnapshotOutputPathTemplate, + jsHeapSnapshotOutputPathTemplateToken, }; return `/** diff --git a/private/react-native-fantom/runner/paths.js b/private/react-native-fantom/runner/paths.js index e19d0a4dc1a..c764980f6e8 100644 --- a/private/react-native-fantom/runner/paths.js +++ b/private/react-native-fantom/runner/paths.js @@ -23,6 +23,10 @@ export const JS_TRACES_OUTPUT_PATH: string = path.join( OUTPUT_PATH, 'js-traces', ); +export const JS_HEAP_SNAPSHOTS_OUTPUT_PATH: string = path.join( + OUTPUT_PATH, + 'js-heap-snapshots', +); export function getTestBuildOutputPath(): string { const fantomRunID = process.env.__FANTOM_RUN_ID__; @@ -35,14 +39,18 @@ export function getTestBuildOutputPath(): string { return path.join(JS_BUILD_OUTPUT_PATH, fantomRunID); } -export function buildJSTracesOutputPath( +export function buildJSTracesOutputPath({ + testPath, + testConfig, + isMultiConfigTest, +}: { testPath: string, testConfig: FantomTestConfig, - isMultiConfig: boolean, -): string { + isMultiConfigTest: boolean, +}): string { const fileNameParts = [path.basename(testPath)]; - if (isMultiConfig) { + if (isMultiConfigTest) { const configSummary = formatFantomConfig(testConfig, {style: 'short'}); if (configSummary !== '') { fileNameParts.push(configSummary); @@ -55,3 +63,33 @@ export function buildJSTracesOutputPath( return path.join(JS_TRACES_OUTPUT_PATH, fileName); } + +const JS_HEAP_SNAPSHOT_OUTPUT_PATH_TOKEN = '${timestamp}'; + +export function buildJSHeapSnapshotsOutputPathTemplate({ + testPath, + testConfig, + isMultiConfigTest, +}: { + testPath: string, + testConfig: FantomTestConfig, + isMultiConfigTest: boolean, +}): [string, string] { + const fileNameParts = [path.basename(testPath)]; + + if (isMultiConfigTest) { + const configSummary = formatFantomConfig(testConfig, {style: 'short'}); + if (configSummary !== '') { + fileNameParts.push(configSummary); + } + } + + fileNameParts.push(JS_HEAP_SNAPSHOT_OUTPUT_PATH_TOKEN); + + const fileName = fileNameParts.join('-') + '.heapsnapshot'; + + return [ + path.join(JS_HEAP_SNAPSHOTS_OUTPUT_PATH, fileName), + JS_HEAP_SNAPSHOT_OUTPUT_PATH_TOKEN, + ]; +} diff --git a/private/react-native-fantom/runner/runner.js b/private/react-native-fantom/runner/runner.js index e31d0423cab..80a93f8bf5a 100644 --- a/private/react-native-fantom/runner/runner.js +++ b/private/react-native-fantom/runner/runner.js @@ -30,7 +30,9 @@ import {run as runFantomTester} from './executables/tester'; import formatFantomConfig from './formatFantomConfig'; import getFantomTestConfigs from './getFantomTestConfigs'; import { + JS_HEAP_SNAPSHOTS_OUTPUT_PATH, JS_TRACES_OUTPUT_PATH, + buildJSHeapSnapshotsOutputPathTemplate, buildJSTracesOutputPath, getTestBuildOutputPath, } from './paths'; @@ -56,6 +58,7 @@ import readline from 'readline'; const TEST_BUILD_OUTPUT_PATH = getTestBuildOutputPath(); fs.mkdirSync(TEST_BUILD_OUTPUT_PATH, {recursive: true}); +fs.mkdirSync(JS_HEAP_SNAPSHOTS_OUTPUT_PATH, {recursive: true}); if (EnvironmentOptions.profileJS) { fs.mkdirSync(JS_TRACES_OUTPUT_PATH, {recursive: true}); @@ -280,9 +283,22 @@ module.exports = async function runTest( } const jsTraceOutputPath = EnvironmentOptions.profileJS - ? buildJSTracesOutputPath(testPath, testConfig, testConfigs.length > 1) + ? buildJSTracesOutputPath({ + testPath, + testConfig, + isMultiConfigTest: testConfigs.length > 1, + }) : null; + const [ + jsHeapSnapshotOutputPathTemplate, + jsHeapSnapshotOutputPathTemplateToken, + ] = buildJSHeapSnapshotsOutputPathTemplate({ + testPath, + testConfig, + isMultiConfigTest: testConfigs.length > 1, + }); + const entrypointContents = entrypointTemplate({ testPath: `${path.relative(TEST_BUILD_OUTPUT_PATH, testPath)}`, setupModulePath: `${path.relative(TEST_BUILD_OUTPUT_PATH, setupModulePath)}`, @@ -292,6 +308,8 @@ module.exports = async function runTest( updateSnapshot: snapshotState._updateSnapshot, data: getInitialSnapshotData(snapshotState), }, + jsHeapSnapshotOutputPathTemplate, + jsHeapSnapshotOutputPathTemplateToken, jsTraceOutputPath, }); diff --git a/private/react-native-fantom/runner/utils.js b/private/react-native-fantom/runner/utils.js index afc49938125..4dcad8fa8dd 100644 --- a/private/react-native-fantom/runner/utils.js +++ b/private/react-native-fantom/runner/utils.js @@ -27,13 +27,16 @@ export enum HermesVariant { export function getBuckOptionsForHermes( variant: HermesVariant, ): $ReadOnlyArray { + const baseOptions = EnvironmentOptions.enableJSMemoryInstrumentation + ? ['-c hermes.memory_instrumentation=true'] + : []; switch (variant) { case HermesVariant.Hermes: - return []; + return baseOptions; case HermesVariant.StaticHermesStable: - return ['-c hermes.static_hermes=stable']; + return [...baseOptions, '-c hermes.static_hermes=stable']; case HermesVariant.StaticHermesExperimental: - return ['-c hermes.static_hermes=trunk']; + return [...baseOptions, '-c hermes.static_hermes=trunk']; } } diff --git a/private/react-native-fantom/src/Constants.js b/private/react-native-fantom/src/Constants.js index 39f330d1367..172bf7aca74 100644 --- a/private/react-native-fantom/src/Constants.js +++ b/private/react-native-fantom/src/Constants.js @@ -13,6 +13,8 @@ export type FantomRuntimeConstants = $ReadOnly<{ isRunningFromCI: boolean, forceTestModeForBenchmarks: boolean, fantomConfigSummary: string, + jsHeapSnapshotOutputPathTemplate: string, + jsHeapSnapshotOutputPathTemplateToken: string, jsTraceOutputPath: ?string, }>; @@ -21,6 +23,8 @@ let constants: FantomRuntimeConstants = { isRunningFromCI: false, forceTestModeForBenchmarks: false, fantomConfigSummary: '', + jsHeapSnapshotOutputPathTemplate: '', + jsHeapSnapshotOutputPathTemplateToken: '', jsTraceOutputPath: null, }; diff --git a/private/react-native-fantom/src/index.js b/private/react-native-fantom/src/index.js index fcfeec2ea22..936911026d2 100644 --- a/private/react-native-fantom/src/index.js +++ b/private/react-native-fantom/src/index.js @@ -666,20 +666,44 @@ export function createShadowNodeRevisionGetter( /** * Saves a heap snapshot after forcing garbage collection. * - * The heapsnapshot is saved to the filename supplied as an argument. - * If a relative path is supplied, it will be saved relative to where you are invoking the tests. - * - * The supplied filename should end in .heapsnapshot, and it can be opened - * using the "Memory" pane in Chrome DevTools. - * - * @param filepath - File where JS memory heap will be saved. + * It prints the location of the saved snapshot file, which can be opened using + * the "Memory" pane in Chrome DevTools. */ -export function saveJSMemoryHeapSnapshot(filePath: string): void { - if (getConstants().isRunningFromCI) { +export function takeJSMemoryHeapSnapshot(): void { + const constants = getConstants(); + + if (constants.isRunningFromCI) { throw new Error('Unexpected call to `saveJSMemoryHeapSnapshot` from CI'); } - NativeFantom.saveJSMemoryHeapSnapshot(filePath); + const filePath = constants.jsHeapSnapshotOutputPathTemplate.replace( + constants.jsHeapSnapshotOutputPathTemplateToken, + new Date().toISOString(), + ); + + try { + NativeFantom.saveJSMemoryHeapSnapshot(filePath); + } catch (nativeError: mixed) { + let errorMessage = 'Error saving JS heap snapshot.'; + if ( + nativeError instanceof Error && + nativeError.message.includes( + "Cannot create heap snapshots if Hermes isn't built with memory instrumentation.", + ) + ) { + // We would generally use an error with nativeError as cause, but our infra + // doesn't support that yet (it expects a `cause` property on the error with + // a very specific shape). + errorMessage += + ' If you want to take JS heap snapshots in optimized builds, ' + + 'please call Fantom with FANTOM_ENABLE_MEMORY_INSTRUMENTATION=1 ' + + '(only works locally with Buck).'; + } + + throw new Error(errorMessage, {cause: nativeError}); + } + + console.info(`💾 JS heap snapshot saved to ${filePath}\n`); } export * from './HighResTimeStampMock';