diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 94fdebfbf..218780212 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -11,6 +11,7 @@ import info from './info/info'; import config from './config/config'; import init from './init'; import doctor from './doctor'; +import profile from './profile'; export const projectCommands = [ start, @@ -24,6 +25,7 @@ export const projectCommands = [ info, config, doctor, + profile, ] as Command[]; export const detachedCommands = [init, doctor] as DetachedCommand[]; diff --git a/packages/cli/src/commands/profile/downloadProfile.ts b/packages/cli/src/commands/profile/downloadProfile.ts new file mode 100644 index 000000000..c54604ef9 --- /dev/null +++ b/packages/cli/src/commands/profile/downloadProfile.ts @@ -0,0 +1,101 @@ +import {Config} from '@react-native-community/cli-types'; +import {execSync} from 'child_process'; +import {logger, CLIError} from '@react-native-community/cli-tools'; +import chalk from 'chalk'; +import fs from 'fs'; + +/** + * get the last modified hermes profile + */ +function getLatestFile(packageName: string): string { + try { + const file = execSync(`adb shell run-as ${packageName} ls cache/ -tp | grep -v /$ | head -1 + `); + + return file.toString().trim(); + } catch (e) { + throw new Error(e); + } +} +/** + * get the package name of the running React Native app + */ +function getPackageName(config: Config) { + const androidProject = config.project.android; + + if (!androidProject) { + throw new CLIError(` + Android project not found. Are you sure this is a React Native project? + If your Android files are located in a non-standard location (e.g. not inside \'android\' folder), consider setting + \`project.android.sourceDir\` option to point to a new location. +`); + } + const {manifestPath} = androidProject; + const androidManifest = fs.readFileSync(manifestPath, 'utf8'); + + let packageNameMatchArray = androidManifest.match(/package="(.+?)"/); + if (!packageNameMatchArray || packageNameMatchArray.length === 0) { + throw new CLIError( + 'Failed to build the app: No package name found. Found errors in /src/main/AndroidManifest.xml', + ); + } + + let packageName = packageNameMatchArray[1]; + + if (!validatePackageName(packageName)) { + logger.warn( + `Invalid application's package name "${chalk.bgRed( + packageName, + )}" in 'AndroidManifest.xml'. Read guidelines for setting the package name here: ${chalk.underline.dim( + 'https://developer.android.com/studio/build/application-id', + )}`, + ); + } + return packageName; +} +/** Validates that the package name is correct + * + */ + +function validatePackageName(packageName: string) { + return /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(packageName); +} + +/** + * Executes the commands to pull a hermes profile + * Commands: + * adb shell run-as com.rnhermesapp cp cache/sampling-profiler-trace1502707982002849976.cpuprofile /sdcard/latest.cpuprofile + * adb pull /sdcard/latest.cpuprofile + */ +export async function downloadProfile( + ctx: Config, + dstPath?: string, + fileName?: string, +) { + try { + const packageName = getPackageName(ctx); + + const file = fileName || (await getLatestFile(packageName)); + if (!file) { + logger.error( + 'There is no file in the cache/ directory. Did you record a profile from the developer menu?', + ); + process.exit(1); + } + logger.info(`File to be pulled: ${file}`); + execSync(`adb shell run-as ${packageName} cp cache/${file} /sdcard`); + + //if not specify destination path, pull to the current directory + if (dstPath === undefined) { + execSync(`adb pull /sdcard/${file} ${ctx.root}`); + console.log(`Successfully pulled the file to ${ctx.root}/${file}`); + } + //if specified destination path, pull to that directory + else { + execSync(`adb pull /sdcard/${file} ${dstPath}`); + console.log(`Successfully pulled the file to ${dstPath}/${file}`); + } + } catch (e) { + throw new Error(e.message); + } +} diff --git a/packages/cli/src/commands/profile/index.ts b/packages/cli/src/commands/profile/index.ts new file mode 100644 index 000000000..7a3ed7157 --- /dev/null +++ b/packages/cli/src/commands/profile/index.ts @@ -0,0 +1,50 @@ +// @ts-ignore untyped +import {logger} from '@react-native-community/cli-tools'; +import {Config} from '@react-native-community/cli-types'; +import {downloadProfile} from './downloadProfile'; + +type Options = { + fileName?: string; +}; + +async function profile( + [dstPath]: Array, + ctx: Config, + options: Options, +) { + try { + logger.info( + 'Downloading a Hermes Sampling Profiler from your Android device...', + ); + + if (options.fileName) { + await downloadProfile(ctx, dstPath, options.fileName); + } else { + logger.info('No filename is provided, pulling latest file'); + await downloadProfile(ctx, dstPath, undefined); + } + } catch (err) { + logger.error(`Unable to download the Hermes Sampling Profiler.\n${err}`); + } +} + +export default { + name: 'profile-hermes [destinationDir]', + description: + 'Download the Hermes Sampling Profiler to the directory of the local machine', + func: profile, + options: [ + //options: download the latest or fileName + { + name: '--fileName [string]', + description: 'Filename of the profile to be downloaded', + }, + ], + examples: [ + { + desc: + 'Download the Hermes Sampling Profiler to the directory of the local machine', + cmd: 'profile-hermes /Users/phuonganh/Desktop', + }, + ], +};