forked from Expensify/App
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsymbolicate-profile.ts
executable file
·174 lines (148 loc) · 6.8 KB
/
symbolicate-profile.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#!/usr/bin/env ts-node
/* eslint-disable @typescript-eslint/naming-convention */
/**
* This script helps to symbolicate a .cpuprofile file that was obtained from a specific (staging) app version (usually provided by a user using the app).
*
* @abstract
*
* 1. When creating a new deployment in our github actions, we upload the source map for android and iOS as artifacts.
* 2. The profiles created by the app on the user's device have the app version encoded in the filename.
* 3. This script takes in a .cpuprofile file, reads the app version from the filename, and downloads the corresponding source map from the artifacts using github's API.
* 4. It then uses the source map to symbolicate the .cpuprofile file using the `react-native-release-profiler` cli.
*
* @note For downloading an artifact a github token is required.
*/
import {execSync} from 'child_process';
import fs from 'fs';
import https from 'https';
import path from 'path';
import GithubUtils from '@github/libs/GithubUtils';
import * as Logger from './utils/Logger';
import parseCommandLineArguments from './utils/parseCommandLineArguments';
const argsMap = parseCommandLineArguments();
/* ============== INPUT VALIDATION ============== */
if (Object.keys(argsMap).length === 0 || argsMap.help !== undefined) {
Logger.log('Symbolicates a .cpuprofile file obtained from a specific app version by downloading the source map from the github action runs.');
Logger.log('Usage: npm run symbolicate-profile -- --profile=<filename> --platform=<ios|android>');
Logger.log('Options:');
Logger.log(' --profile=<filename> The .cpuprofile file to symbolicate');
Logger.log(' --platform=<ios|android> The platform for which the source map was uploaded');
Logger.log(' --ghToken Token to use for requests send to the GitHub API. By default tries to pick up from the environment variable GITHUB_TOKEN');
Logger.log(' --help Display this help message');
process.exit(0);
}
if (argsMap.profile === undefined) {
Logger.error('Please specify the .cpuprofile file to symbolicate using --profile=<filename>');
process.exit(1);
}
if (!fs.existsSync(argsMap.profile)) {
Logger.error(`File ${argsMap.profile} does not exist.`);
process.exit(1);
}
if (argsMap.platform === undefined) {
Logger.error('Please specify the platform using --platform=ios or --platform=android');
process.exit(1);
}
const githubToken = argsMap.ghToken ?? process.env.GITHUB_TOKEN;
if (githubToken === undefined) {
Logger.error('No GitHub token provided. Either set a GITHUB_TOKEN environment variable or pass it using --ghToken');
process.exit(1);
}
GithubUtils.initOctokitWithToken(githubToken);
/* ============= EXTRACT APP VERSION ============= */
// Formatted as "Profile_trace_for_1.4.81-9.cpuprofile"
const appVersionRegex = /\d+\.\d+\.\d+(-\d+)?/;
const appVersion = argsMap.profile.match(appVersionRegex)?.[0];
if (appVersion === undefined) {
Logger.error('Could not extract the app version from the profile filename.');
process.exit(1);
}
Logger.info(`Found app version ${appVersion} in the profile filename`);
/* ============== UTILITY FUNCTIONS ============== */
async function getWorkflowRunArtifact() {
const artifactName = `${argsMap.platform}-sourcemap-${appVersion}`;
Logger.info(`Fetching sourcemap artifact with name "${artifactName}"`);
const artifact = await GithubUtils.getArtifactByName(artifactName);
if (artifact === undefined) {
throw new Error(`Could not find the artifact ${artifactName}! Are you sure the deploy step succeeded?`);
}
return artifact.id;
}
const sourcemapDir = path.resolve(__dirname, '../.sourcemaps');
function downloadFile(url: string) {
Logger.log(`Downloading file from URL: ${url}`);
if (!fs.existsSync(sourcemapDir)) {
Logger.info(`Creating download directory ${sourcemapDir}`);
fs.mkdirSync(sourcemapDir);
}
const destination = path.join(sourcemapDir, `${argsMap.platform}-sourcemap-${appVersion}.zip`);
const file = fs.createWriteStream(destination);
return new Promise<string>((resolve, reject) => {
https
.get(url, (response) => {
response.pipe(file);
file.on('finish', () => {
file.close();
Logger.success(`Downloaded file to ${destination}`);
resolve(destination);
});
})
.on('error', (error) => {
fs.unlink(destination, () => {
reject(error);
});
});
});
}
function unpackZipFile(zipPath: string) {
Logger.info(`Unpacking file ${zipPath}`);
const command = `unzip -o ${zipPath} -d ${sourcemapDir}`;
execSync(command, {stdio: 'inherit'});
Logger.info(`Deleting zip file ${zipPath}`);
return new Promise<void>((resolve, reject) => {
fs.unlink(zipPath, (error) => (error ? reject(error) : resolve()));
});
}
const localSourceMapPath = path.join(sourcemapDir, `${appVersion}-${argsMap.platform}.map`);
function renameDownloadedSourcemapFile() {
const androidName = 'index.android.bundle.map';
const iosName = 'main.jsbundle.map';
const downloadSourcemapPath = path.join(sourcemapDir, argsMap.platform === 'ios' ? iosName : androidName);
if (!fs.existsSync(downloadSourcemapPath)) {
Logger.error(`Could not find the sourcemap file ${downloadSourcemapPath}`);
process.exit(1);
}
Logger.info(`Renaming sourcemap file to ${localSourceMapPath}`);
fs.renameSync(downloadSourcemapPath, localSourceMapPath);
}
// Symbolicate using the downloaded source map
function symbolicateProfile() {
const command = `npx react-native-release-profiler --local ${argsMap.profile} --sourcemap-path ${localSourceMapPath}`;
execSync(command, {stdio: 'inherit'});
}
async function fetchAndProcessArtifact() {
const artifactId = await getWorkflowRunArtifact();
const downloadUrl = await GithubUtils.getArtifactDownloadURL(artifactId);
const zipPath = await downloadFile(downloadUrl);
await unpackZipFile(zipPath);
renameDownloadedSourcemapFile();
}
/* ============== MAIN SCRIPT ============== */
async function runAsyncScript() {
// Step: check if source map locally already exists (if so we can skip the download)
if (fs.existsSync(localSourceMapPath)) {
Logger.success(`Found local source map at ${localSourceMapPath}`);
Logger.info('Skipping download step');
} else {
// Step: Download the source map for the app version and then symbolicate the profile:
try {
await fetchAndProcessArtifact();
} catch (error) {
Logger.error(error);
process.exit(1);
}
}
// Finally, symbolicate the profile
symbolicateProfile();
}
runAsyncScript();