Skip to content

Commit c967c63

Browse files
committed
Add merge-android-profiles script
1 parent 0c2562c commit c967c63

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"build-l10n-prod:quiet": "yarn build:clean && yarn build-photon && cross-env NODE_ENV=production L10N=1 webpack",
2020
"build-l10n-prod": "yarn build-l10n-prod:quiet --progress",
2121
"build-photon": "webpack --config res/photon/webpack.config.js",
22+
"build-merge-android-profiles": "yarn build-merge-android-profiles:quiet --progress",
23+
"build-merge-android-profiles:quiet": "yarn build:clean && cross-env NODE_ENV=production webpack --config src/merge-android-profiles/webpack.config.js",
2224
"build-symbolicator-cli": "yarn build-symbolicator-cli:quiet --progress",
2325
"build-symbolicator-cli:quiet": "yarn build:clean && cross-env NODE_ENV=production webpack --config src/symbolicator-cli/webpack.config.js",
2426
"lint": "node bin/output-fixing-commands.js run-p lint-js lint-css prettier-run",
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// @flow
2+
3+
/**
4+
* Merge two existing profiles, taking the samples from the first profile and
5+
* the markers from the second profile.
6+
*
7+
* This was useful during early 2025 when the Mozilla Performance team was
8+
* doing a lot of Android startup profiling:
9+
*
10+
* - The "samples" profile would be collected using simpleperf and converted
11+
* with samply import.
12+
* - The "markers" profile would be collected using the Gecko profiler.
13+
*
14+
* To use this script, it first needs to be built:
15+
* yarn build-merge-android-profiles
16+
*
17+
* Then it can be run from the `dist` directory:
18+
* node dist/merge-android-profiles.js --samples-hash warg8azfac0z5b5sy92h4a69bfrj2fqsjc6ty58 --markers-hash mb6220c2rx3mmhegv82d84tsvgn6a5p8r7g4je8 --output-file ~/Downloads/merged-profile.json
19+
*
20+
* For example:
21+
* yarn build-merge-android-profiles && node dist/merge-android-profiles.js --samples-hash warg8azfac0z5b5sy92h4a69bfrj2fqsjc6ty58 --markers-hash mb6220c2rx3mmhegv82d84tsvgn6a5p8r7g4je8 --output-file ~/Downloads/merged-profile.json
22+
*
23+
*/
24+
25+
const fs = require('fs');
26+
27+
import {
28+
unserializeProfileOfArbitraryFormat,
29+
adjustMarkerTimestamps,
30+
} from '../profile-logic/process-profile';
31+
import { getProfileUrlForHash } from '../actions/receive-profile';
32+
import { computeStringIndexMarkerFieldsByDataType } from '../profile-logic/marker-schema';
33+
import { ensureExists } from '../utils/flow';
34+
import { StringTable } from '../utils/string-table';
35+
36+
import type { Profile } from '../types/profile';
37+
38+
interface CliOptions {
39+
samplesHash: string;
40+
markersHash: string;
41+
outputFile: string;
42+
}
43+
44+
async function fetchProfileWithHash(hash: string): Promise<Profile> {
45+
const response = await fetch(getProfileUrlForHash(hash));
46+
const serializedProfile = await response.json();
47+
return unserializeProfileOfArbitraryFormat(serializedProfile);
48+
}
49+
50+
export async function run(options: CliOptions) {
51+
const profileWithSamples: Profile = await fetchProfileWithHash(
52+
options.samplesHash
53+
);
54+
const profileWithMarkers: Profile = await fetchProfileWithHash(
55+
options.markersHash
56+
);
57+
58+
// const referenceSampleTime = 169912951.547432; // filteredThread.samples.time[0] after zooming in on samples in mozilla::dom::indexedDB::BackgroundTransactionChild::RecvComplete
59+
// const referenceMarkerTime = 664.370158 ; // selectedMarker.start after selecting the marker for the "complete" DOMEvent
60+
61+
// console.log(profileWithSamples.meta);
62+
// console.log(profileWithMarkers.meta);
63+
64+
let timeDelta =
65+
profileWithMarkers.meta.startTime - profileWithSamples.meta.startTime;
66+
if (
67+
profileWithSamples.meta.startTimeAsClockMonotonicNanosecondsSinceBoot !==
68+
undefined &&
69+
profileWithMarkers.meta.startTimeAsClockMonotonicNanosecondsSinceBoot !==
70+
undefined
71+
) {
72+
timeDelta =
73+
(profileWithMarkers.meta.startTimeAsClockMonotonicNanosecondsSinceBoot -
74+
profileWithSamples.meta.startTimeAsClockMonotonicNanosecondsSinceBoot) /
75+
1000000;
76+
}
77+
78+
// console.log({ timeDelta });
79+
80+
const profile = profileWithSamples;
81+
profile.meta.markerSchema = profileWithMarkers.meta.markerSchema;
82+
profile.pages = profileWithMarkers.pages;
83+
84+
const markerProfileCategoryToCategory = new Map();
85+
const markerProfileCategories = ensureExists(
86+
profileWithMarkers.meta.categories
87+
);
88+
const profileCategories = ensureExists(profile.meta.categories);
89+
for (
90+
let markerCategoryIndex = 0;
91+
markerCategoryIndex < markerProfileCategories.length;
92+
markerCategoryIndex++
93+
) {
94+
const category = markerProfileCategories[markerCategoryIndex];
95+
let categoryIndex = profileCategories.findIndex(
96+
(c) => c.name === category.name
97+
);
98+
if (categoryIndex === -1) {
99+
categoryIndex = profileCategories.length;
100+
profileCategories[categoryIndex] = {
101+
name: category.name,
102+
color: category.color,
103+
subcategories: ['Other'],
104+
};
105+
}
106+
markerProfileCategoryToCategory.set(markerCategoryIndex, categoryIndex);
107+
}
108+
109+
const markerThreadsByTid = new Map(
110+
profileWithMarkers.threads.map((thread) => ['' + thread.tid, thread])
111+
);
112+
// console.log([...markerThreadsByTid.keys()]);
113+
114+
// console.log(profile.threads.map((thread) => thread.tid));
115+
116+
const stringIndexMarkerFieldsByDataType =
117+
computeStringIndexMarkerFieldsByDataType(profile.meta.markerSchema);
118+
119+
const sampleThreadTidsWithoutCorrespondingMarkerThreads = new Set();
120+
121+
const stringTable = StringTable.withBackingArray(profile.shared.stringArray);
122+
const markerStringArray = profileWithMarkers.shared.stringArray;
123+
for (const thread of profile.threads) {
124+
const tid = thread.tid;
125+
const markerThread = markerThreadsByTid.get(tid);
126+
if (markerThread === undefined) {
127+
sampleThreadTidsWithoutCorrespondingMarkerThreads.add(tid);
128+
continue;
129+
}
130+
markerThreadsByTid.delete(tid);
131+
132+
thread.markers = adjustMarkerTimestamps(markerThread.markers, timeDelta);
133+
for (let i = 0; i < thread.markers.length; i++) {
134+
thread.markers.category[i] = ensureExists(
135+
markerProfileCategoryToCategory.get(thread.markers.category[i])
136+
);
137+
thread.markers.name[i] = stringTable.indexForString(
138+
markerStringArray[thread.markers.name[i]]
139+
);
140+
const data = thread.markers.data[i];
141+
if (data !== null && data.type) {
142+
const markerType = data.type;
143+
const stringIndexMarkerFields =
144+
stringIndexMarkerFieldsByDataType.get(markerType);
145+
if (stringIndexMarkerFields !== undefined) {
146+
for (const fieldKey of stringIndexMarkerFields) {
147+
const stringIndex = data[fieldKey];
148+
if (typeof stringIndex === 'number') {
149+
const newStringIndex = stringTable.indexForString(
150+
markerStringArray[stringIndex]
151+
);
152+
data[fieldKey] = newStringIndex;
153+
}
154+
}
155+
}
156+
}
157+
}
158+
}
159+
160+
// console.log(
161+
// `Have ${markerThreadsByTid.size} marker threads left over which weren't slurped up by sample threads:`,
162+
// [...markerThreadsByTid.keys()]
163+
// );
164+
// if (markerThreadsByTid.size !== 0) {
165+
// console.log(
166+
// `Have ${sampleThreadTidsWithoutCorrespondingMarkerThreads.size} sample threads which didn't find corresponding marker threads:`,
167+
// [...sampleThreadTidsWithoutCorrespondingMarkerThreads]
168+
// );
169+
// }
170+
171+
fs.writeFileSync(options.outputFile, JSON.stringify(profile));
172+
}
173+
174+
export function makeOptionsFromArgv(processArgv: string[]): CliOptions {
175+
const argv = require('minimist')(processArgv.slice(2));
176+
177+
if (!('samples-hash' in argv && typeof argv['samples-hash'] === 'string')) {
178+
throw new Error(
179+
'Argument --samples-hash must be supplied with the path to a text file of profile hashes'
180+
);
181+
}
182+
183+
if (!('markers-hash' in argv && typeof argv['markers-hash'] === 'string')) {
184+
throw new Error(
185+
'Argument --markers-hash must be supplied with the path to a text file of profile hashes'
186+
);
187+
}
188+
189+
if (!('output-file' in argv && typeof argv['output-file'] === 'string')) {
190+
throw new Error(
191+
'Argument --output-file must be supplied with the path to a text file of profile hashes'
192+
);
193+
}
194+
195+
return {
196+
samplesHash: argv['samples-hash'],
197+
markersHash: argv['markers-hash'],
198+
outputFile: argv['output-file'],
199+
};
200+
}
201+
202+
if (!module.parent) {
203+
try {
204+
const options = makeOptionsFromArgv(process.argv);
205+
run(options).catch((err) => {
206+
throw err;
207+
});
208+
} catch (e) {
209+
console.error(e);
210+
process.exit(1);
211+
}
212+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// @noflow
2+
const path = require('path');
3+
const projectRoot = path.join(__dirname, '../..');
4+
const includes = [path.join(projectRoot, 'src')];
5+
6+
module.exports = {
7+
name: 'merge-android-profiles',
8+
target: 'node',
9+
mode: process.env.NODE_ENV,
10+
output: {
11+
path: path.resolve(projectRoot, 'dist'),
12+
filename: 'merge-android-profiles.js',
13+
},
14+
entry: './src/merge-android-profiles/index.js',
15+
module: {
16+
rules: [
17+
{
18+
test: /\.js$/,
19+
use: ['babel-loader'],
20+
include: includes,
21+
},
22+
{
23+
test: /\.svg$/,
24+
type: 'asset/resource',
25+
},
26+
],
27+
},
28+
experiments: {
29+
// Make WebAssembly work just like in webpack v4
30+
syncWebAssembly: true,
31+
},
32+
};

0 commit comments

Comments
 (0)