Skip to content

Commit

Permalink
Add a batch-checker.
Browse files Browse the repository at this point in the history
  • Loading branch information
mstange committed Dec 20, 2024
1 parent 7d22187 commit 9a2a96b
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 0 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"build-l10n-prod:quiet": "yarn build:clean && yarn build-photon && cross-env NODE_ENV=production L10N=1 webpack",
"build-l10n-prod": "yarn build-l10n-prod:quiet --progress",
"build-photon": "webpack --config res/photon/webpack.config.js",
"build-batch-checker": "yarn build-batch-checker:quiet --progress",
"build-batch-checker:quiet": "yarn build:clean && cross-env NODE_ENV=production webpack --config src/batch-checker/webpack.config.js",
"build-symbolicator-cli": "yarn build-symbolicator-cli:quiet --progress",
"build-symbolicator-cli:quiet": "yarn build:clean && cross-env NODE_ENV=production webpack --config src/symbolicator-cli/webpack.config.js",
"lint": "node bin/output-fixing-commands.js run-p lint-js lint-css prettier-run",
Expand Down
178 changes: 178 additions & 0 deletions src/batch-checker/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// @flow

/**
* This implements a simple CLI to check existing public profiles for certain
* criteria.
*
* To use it it first needs to be built:
* yarn build-batch-checker
*
* Then it can be run from the `dist` directory:
* node dist/batch-checker.js --hashes-file <path to text file>
*
* For example:
* yarn build-batch-checker && node dist/batch-checker.js --hashes-file ~/Downloads/profile-hashes.txt
*
*/

const fs = require('fs');

import { unserializeProfileOfArbitraryFormat } from '../profile-logic/process-profile';
import { getProfileUrlForHash } from '../actions/receive-profile';
import { getTimeRangeIncludingAllThreads } from '../profile-logic/profile-data';
import { encodeUintSetForUrlComponent } from '../utils/uintarray-encoding';

interface CliOptions {
hashesFile: string;
}

function checkProfileThreadCPUDelta(profile: any, hash: string): Set<string> {
const outcomes = new Set();
const rootRange = getTimeRangeIncludingAllThreads(profile);
const { threads } = profile;
for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) {
const thread = threads[threadIndex];
const threadCPUDelta = thread.samples.threadCPUDelta;
if (!threadCPUDelta) {
outcomes.add('has thread without threadCPUDelta');
continue;
}

outcomes.add('has thread with threadCPUDelta');
const len = thread.samples.length;
if (len < 2) {
outcomes.add('has thread with fewer than two samples');
continue;
}
if (threadCPUDelta[0] === null) {
outcomes.add('has null in first threadCPUDelta');
}
const firstNonNullIndex = threadCPUDelta.findIndex((d) => d !== null);
if (firstNonNullIndex !== -1) {
for (let i = firstNonNullIndex + 1; i < len; i++) {
if (threadCPUDelta[i] === null) {
outcomes.add('has null after first null value in threadCPUDelta');
const sampleTime = thread.samples.time[i];
const relativeSampleTime = sampleTime - rootRange.start;
const url = `https://profiler.firefox.com/public/${hash}/?v=10&thread=${encodeUintSetForUrlComponent(new Set([threadIndex]))}`;
console.log(
`non-null at sample ${i} on thread ${threadIndex} at relative time ${(relativeSampleTime / 1000).toFixed(3)}s: ${url}`
);
break;
}
}
}
}
return outcomes;
}

function checkProfileSchemaMatching(profile: any, _hash: string): Set<string> {
const { meta, threads } = profile;
const { markerSchema } = meta;
const markerSchemaNames = new Set(markerSchema.map((schema) => schema.name));
const tracingCategories = new Set();
const textNames = new Set();

const outcomes = new Set();
for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) {
const thread = threads[threadIndex];
const { markers, stringTable } = thread;
for (let markerIndex = 0; markerIndex < markers.length; markerIndex++) {
const nameIndex = markers.name[markerIndex];
const data = markers.data[markerIndex];
if (
data &&
data.type &&
data.type === 'tracing' &&
data.category &&
markerSchemaNames.has(data.category)
) {
if (!tracingCategories.has(data.category)) {
console.log(
`Found tracing marker whose schema is for category ${data.category}, thread index ${threadIndex}, marker index ${markerIndex}`
);
outcomes.add(
`has tracing marker whose schema is for category ${data.category}`
);
tracingCategories.add(data.category);
}
continue;
}
const name = stringTable.getString(nameIndex);
if (
data &&
data.type &&
data.type === 'Text' &&
markerSchemaNames.has(name)
) {
if (!textNames.has(name)) {
console.log(
`Found Text marker whose schema is for name ${name}, thread index ${threadIndex}, marker index ${markerIndex}`
);
outcomes.add(`has Text marker whose schema is for name ${name}`);
textNames.add(name);
}
continue;
}
}
}

return outcomes;
}

function checkProfile(profile: Profile, hash: string): Set<string> {
return checkProfileSchemaMatching(profile, hash);
}

export async function run(options: CliOptions) {
const hashes = fs.readFileSync(options.hashesFile, 'utf8').split('\n');
console.log(`Have ${hashes.length} hashes.`);

for (let i = 0; i < hashes.length; i++) {
const hash = hashes[i];
console.log(
`Checking profile ${i + 1} of ${hashes.length} with hash ${hash}`
);
try {
const response = await fetch(getProfileUrlForHash(hash));
const serializedProfile = await response.json();
const profile =
await unserializeProfileOfArbitraryFormat(serializedProfile);
if (profile === undefined) {
throw new Error('Unable to parse the profile.');
}
const outcome = checkProfile(profile, hash);
console.log(`Outcome: ${[...outcome].join(', ')}`);
} catch (e) {
console.log(`Failed: ${e}`);
}
}

console.log('Finished.');
}

export function makeOptionsFromArgv(processArgv: string[]): CliOptions {
const argv = require('minimist')(processArgv.slice(2));

if (!('hashes-file' in argv && typeof argv['hashes-file'] === 'string')) {
throw new Error(
'Argument --hashes-file must be supplied with the path to a text file of profile hashes'
);
}

return {
hashesFile: argv['hashes-file'],
};
}

if (!module.parent) {
try {
const options = makeOptionsFromArgv(process.argv);
run(options).catch((err) => {
throw err;
});
} catch (e) {
console.error(e);
process.exit(1);
}
}
32 changes: 32 additions & 0 deletions src/batch-checker/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// @noflow
const path = require('path');
const projectRoot = path.join(__dirname, '../..');
const includes = [path.join(projectRoot, 'src')];

module.exports = {
name: 'batch-checker',
target: 'node',
mode: process.env.NODE_ENV,
output: {
path: path.resolve(projectRoot, 'dist'),
filename: 'batch-checker.js',
},
entry: './src/batch-checker/index.js',
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
include: includes,
},
{
test: /\.svg$/,
type: 'asset/resource',
},
],
},
experiments: {
// Make WebAssembly work just like in webpack v4
syncWebAssembly: true,
},
};

0 comments on commit 9a2a96b

Please sign in to comment.