Skip to content

Commit 9a2a96b

Browse files
committed
Add a batch-checker.
1 parent 7d22187 commit 9a2a96b

3 files changed

Lines changed: 212 additions & 0 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"build-l10n-prod:quiet": "yarn build:clean && yarn build-photon && cross-env NODE_ENV=production L10N=1 webpack",
1414
"build-l10n-prod": "yarn build-l10n-prod:quiet --progress",
1515
"build-photon": "webpack --config res/photon/webpack.config.js",
16+
"build-batch-checker": "yarn build-batch-checker:quiet --progress",
17+
"build-batch-checker:quiet": "yarn build:clean && cross-env NODE_ENV=production webpack --config src/batch-checker/webpack.config.js",
1618
"build-symbolicator-cli": "yarn build-symbolicator-cli:quiet --progress",
1719
"build-symbolicator-cli:quiet": "yarn build:clean && cross-env NODE_ENV=production webpack --config src/symbolicator-cli/webpack.config.js",
1820
"lint": "node bin/output-fixing-commands.js run-p lint-js lint-css prettier-run",

src/batch-checker/index.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// @flow
2+
3+
/**
4+
* This implements a simple CLI to check existing public profiles for certain
5+
* criteria.
6+
*
7+
* To use it it first needs to be built:
8+
* yarn build-batch-checker
9+
*
10+
* Then it can be run from the `dist` directory:
11+
* node dist/batch-checker.js --hashes-file <path to text file>
12+
*
13+
* For example:
14+
* yarn build-batch-checker && node dist/batch-checker.js --hashes-file ~/Downloads/profile-hashes.txt
15+
*
16+
*/
17+
18+
const fs = require('fs');
19+
20+
import { unserializeProfileOfArbitraryFormat } from '../profile-logic/process-profile';
21+
import { getProfileUrlForHash } from '../actions/receive-profile';
22+
import { getTimeRangeIncludingAllThreads } from '../profile-logic/profile-data';
23+
import { encodeUintSetForUrlComponent } from '../utils/uintarray-encoding';
24+
25+
interface CliOptions {
26+
hashesFile: string;
27+
}
28+
29+
function checkProfileThreadCPUDelta(profile: any, hash: string): Set<string> {
30+
const outcomes = new Set();
31+
const rootRange = getTimeRangeIncludingAllThreads(profile);
32+
const { threads } = profile;
33+
for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) {
34+
const thread = threads[threadIndex];
35+
const threadCPUDelta = thread.samples.threadCPUDelta;
36+
if (!threadCPUDelta) {
37+
outcomes.add('has thread without threadCPUDelta');
38+
continue;
39+
}
40+
41+
outcomes.add('has thread with threadCPUDelta');
42+
const len = thread.samples.length;
43+
if (len < 2) {
44+
outcomes.add('has thread with fewer than two samples');
45+
continue;
46+
}
47+
if (threadCPUDelta[0] === null) {
48+
outcomes.add('has null in first threadCPUDelta');
49+
}
50+
const firstNonNullIndex = threadCPUDelta.findIndex((d) => d !== null);
51+
if (firstNonNullIndex !== -1) {
52+
for (let i = firstNonNullIndex + 1; i < len; i++) {
53+
if (threadCPUDelta[i] === null) {
54+
outcomes.add('has null after first null value in threadCPUDelta');
55+
const sampleTime = thread.samples.time[i];
56+
const relativeSampleTime = sampleTime - rootRange.start;
57+
const url = `https://profiler.firefox.com/public/${hash}/?v=10&thread=${encodeUintSetForUrlComponent(new Set([threadIndex]))}`;
58+
console.log(
59+
`non-null at sample ${i} on thread ${threadIndex} at relative time ${(relativeSampleTime / 1000).toFixed(3)}s: ${url}`
60+
);
61+
break;
62+
}
63+
}
64+
}
65+
}
66+
return outcomes;
67+
}
68+
69+
function checkProfileSchemaMatching(profile: any, _hash: string): Set<string> {
70+
const { meta, threads } = profile;
71+
const { markerSchema } = meta;
72+
const markerSchemaNames = new Set(markerSchema.map((schema) => schema.name));
73+
const tracingCategories = new Set();
74+
const textNames = new Set();
75+
76+
const outcomes = new Set();
77+
for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) {
78+
const thread = threads[threadIndex];
79+
const { markers, stringTable } = thread;
80+
for (let markerIndex = 0; markerIndex < markers.length; markerIndex++) {
81+
const nameIndex = markers.name[markerIndex];
82+
const data = markers.data[markerIndex];
83+
if (
84+
data &&
85+
data.type &&
86+
data.type === 'tracing' &&
87+
data.category &&
88+
markerSchemaNames.has(data.category)
89+
) {
90+
if (!tracingCategories.has(data.category)) {
91+
console.log(
92+
`Found tracing marker whose schema is for category ${data.category}, thread index ${threadIndex}, marker index ${markerIndex}`
93+
);
94+
outcomes.add(
95+
`has tracing marker whose schema is for category ${data.category}`
96+
);
97+
tracingCategories.add(data.category);
98+
}
99+
continue;
100+
}
101+
const name = stringTable.getString(nameIndex);
102+
if (
103+
data &&
104+
data.type &&
105+
data.type === 'Text' &&
106+
markerSchemaNames.has(name)
107+
) {
108+
if (!textNames.has(name)) {
109+
console.log(
110+
`Found Text marker whose schema is for name ${name}, thread index ${threadIndex}, marker index ${markerIndex}`
111+
);
112+
outcomes.add(`has Text marker whose schema is for name ${name}`);
113+
textNames.add(name);
114+
}
115+
continue;
116+
}
117+
}
118+
}
119+
120+
return outcomes;
121+
}
122+
123+
function checkProfile(profile: Profile, hash: string): Set<string> {
124+
return checkProfileSchemaMatching(profile, hash);
125+
}
126+
127+
export async function run(options: CliOptions) {
128+
const hashes = fs.readFileSync(options.hashesFile, 'utf8').split('\n');
129+
console.log(`Have ${hashes.length} hashes.`);
130+
131+
for (let i = 0; i < hashes.length; i++) {
132+
const hash = hashes[i];
133+
console.log(
134+
`Checking profile ${i + 1} of ${hashes.length} with hash ${hash}`
135+
);
136+
try {
137+
const response = await fetch(getProfileUrlForHash(hash));
138+
const serializedProfile = await response.json();
139+
const profile =
140+
await unserializeProfileOfArbitraryFormat(serializedProfile);
141+
if (profile === undefined) {
142+
throw new Error('Unable to parse the profile.');
143+
}
144+
const outcome = checkProfile(profile, hash);
145+
console.log(`Outcome: ${[...outcome].join(', ')}`);
146+
} catch (e) {
147+
console.log(`Failed: ${e}`);
148+
}
149+
}
150+
151+
console.log('Finished.');
152+
}
153+
154+
export function makeOptionsFromArgv(processArgv: string[]): CliOptions {
155+
const argv = require('minimist')(processArgv.slice(2));
156+
157+
if (!('hashes-file' in argv && typeof argv['hashes-file'] === 'string')) {
158+
throw new Error(
159+
'Argument --hashes-file must be supplied with the path to a text file of profile hashes'
160+
);
161+
}
162+
163+
return {
164+
hashesFile: argv['hashes-file'],
165+
};
166+
}
167+
168+
if (!module.parent) {
169+
try {
170+
const options = makeOptionsFromArgv(process.argv);
171+
run(options).catch((err) => {
172+
throw err;
173+
});
174+
} catch (e) {
175+
console.error(e);
176+
process.exit(1);
177+
}
178+
}
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: 'batch-checker',
8+
target: 'node',
9+
mode: process.env.NODE_ENV,
10+
output: {
11+
path: path.resolve(projectRoot, 'dist'),
12+
filename: 'batch-checker.js',
13+
},
14+
entry: './src/batch-checker/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)