Skip to content

Commit 09d0f40

Browse files
committed
Add merge-android-profiles script
2 parents b03f519 + 1e771ee commit 09d0f40

File tree

4 files changed

+252
-1
lines changed

4 files changed

+252
-1
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+
}
42+
43+
async function fetchProfileWithHash(hash: string): Promise<Profile> {
44+
const response = await fetch(getProfileUrlForHash(hash));
45+
const serializedProfile = await response.json();
46+
return unserializeProfileOfArbitraryFormat(serializedProfile);
47+
}
48+
49+
export async function run(options: CliOptions) {
50+
const profileWithSamples: Profile = await fetchProfileWithHash(
51+
options.samplesHash
52+
);
53+
const profileWithMarkers: Profile = await fetchProfileWithHash(
54+
options.markersHash
55+
);
56+
57+
// const referenceSampleTime = 169912951.547432; // filteredThread.samples.time[0] after zooming in on samples in mozilla::dom::indexedDB::BackgroundTransactionChild::RecvComplete
58+
// const referenceMarkerTime = 664.370158 ; // selectedMarker.start after selecting the marker for the "complete" DOMEvent
59+
60+
// console.log(profileWithSamples.meta);
61+
// console.log(profileWithMarkers.meta);
62+
63+
let timeDelta =
64+
profileWithMarkers.meta.startTime - profileWithSamples.meta.startTime;
65+
if (
66+
profileWithSamples.meta.startTimeAsClockMonotonicNanosecondsSinceBoot !==
67+
undefined &&
68+
profileWithMarkers.meta.startTimeAsClockMonotonicNanosecondsSinceBoot !==
69+
undefined
70+
) {
71+
timeDelta =
72+
(profileWithMarkers.meta.startTimeAsClockMonotonicNanosecondsSinceBoot -
73+
profileWithSamples.meta.startTimeAsClockMonotonicNanosecondsSinceBoot) /
74+
1000000;
75+
}
76+
77+
// console.log({ timeDelta });
78+
79+
const profile = profileWithSamples;
80+
profile.meta.markerSchema = profileWithMarkers.meta.markerSchema;
81+
profile.pages = profileWithMarkers.pages;
82+
83+
const markerProfileCategoryToCategory = new Map();
84+
const markerProfileCategories = ensureExists(
85+
profileWithMarkers.meta.categories
86+
);
87+
const profileCategories = ensureExists(profile.meta.categories);
88+
for (
89+
let markerCategoryIndex = 0;
90+
markerCategoryIndex < markerProfileCategories.length;
91+
markerCategoryIndex++
92+
) {
93+
const category = markerProfileCategories[markerCategoryIndex];
94+
let categoryIndex = profileCategories.findIndex(
95+
(c) => c.name === category.name
96+
);
97+
if (categoryIndex === -1) {
98+
categoryIndex = profileCategories.length;
99+
profileCategories[categoryIndex] = {
100+
name: category.name,
101+
color: category.color,
102+
subcategories: ['Other'],
103+
};
104+
}
105+
markerProfileCategoryToCategory.set(markerCategoryIndex, categoryIndex);
106+
}
107+
108+
const markerThreadsByTid = new Map(
109+
profileWithMarkers.threads.map((thread) => ['' + thread.tid, thread])
110+
);
111+
// console.log([...markerThreadsByTid.keys()]);
112+
113+
// console.log(profile.threads.map((thread) => thread.tid));
114+
115+
const stringIndexMarkerFieldsByDataType =
116+
computeStringIndexMarkerFieldsByDataType(profile.meta.markerSchema);
117+
118+
const sampleThreadTidsWithoutCorrespondingMarkerThreads = new Set();
119+
120+
for (const thread of profile.threads) {
121+
const tid = thread.tid;
122+
const markerThread = markerThreadsByTid.get(tid);
123+
if (markerThread === undefined) {
124+
sampleThreadTidsWithoutCorrespondingMarkerThreads.add(tid);
125+
continue;
126+
}
127+
markerThreadsByTid.delete(tid);
128+
129+
const stringTable = StringTable.withBackingArray(thread.stringArray);
130+
const markerStringArray = markerThread.stringArray;
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+
};

src/profile-logic/marker-schema.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,12 @@ export function computeStringIndexMarkerFieldsByDataType(
679679
const { name, fields } = schema;
680680
const stringIndexFields = [];
681681
for (const field of fields) {
682-
if (field.format === 'unique-string' && field.key) {
682+
if (
683+
(field.format === 'unique-string' ||
684+
field.format === 'flow-id' ||
685+
field.format === 'terminating-flow-id') &&
686+
field.key
687+
) {
683688
stringIndexFields.push(field.key);
684689
}
685690
}

0 commit comments

Comments
 (0)