Skip to content

Commit

Permalink
Add a dedicated symbolication tool (#5123)
Browse files Browse the repository at this point in the history
In order to avoid the need to launch an entire browser to get a profile symbolicated, this PR adds a small standalone NodeJS-based tool which can be run as a CLI process to symbolicate a profile. The components used by the frontend are reused so that the logic is shared, they're just packaged together into a minimal app with no React/Redux, etc.

Once built, the tool is at `dist/symbolicator.js`.

## Usage

```
node dist/symbolicator.js \
    --input path/to/unsymbolicated/profile.json \
    --output path/to/write/symbolicated/profile.json \
    --server <URI of Mozilla Symbolication API endpoint such as the one exposed by Samply>
```
  • Loading branch information
richard-fine authored Sep 23, 2024
1 parent 5d53dcb commit bf1ac66
Show file tree
Hide file tree
Showing 15 changed files with 2,355 additions and 10 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
steps:
- checkout-and-dependencies
- run: yarn build-prod:quiet
- run: yarn build-symbolicator-cli:quiet

licence-check:
executor: node
Expand Down
3 changes: 3 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-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",
"lint-fix": "run-p lint-fix-js lint-fix-css prettier-fix",
"lint-js": "node bin/output-fixing-commands.js eslint *.js bin src --report-unused-disable-directives --cache --cache-strategy content",
Expand Down Expand Up @@ -78,6 +80,7 @@
"jszip": "^3.10.1",
"memoize-immutable": "^3.0.0",
"memoize-one": "^6.0.0",
"minimist": "^1.2.8",
"mixedtuplemap": "^1.0.0",
"namedtuplemap": "^1.0.0",
"photon-colors": "^3.3.2",
Expand Down
15 changes: 11 additions & 4 deletions src/profile-logic/symbol-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import SymbolStoreDB from './symbol-store-db';
import { SymbolsNotFoundError } from './errors';

import type { RequestedLib } from 'firefox-profiler/types';
import type { RequestedLib, ISymbolStoreDB } from 'firefox-profiler/types';
import type { SymbolTableAsTuple } from './symbol-store-db';
import { ensureExists } from '../utils/flow';

Expand Down Expand Up @@ -226,11 +226,18 @@ async function _getDemangleCallback(): Promise<DemangleFunction> {
*/
export class SymbolStore {
_symbolProvider: SymbolProvider;
_db: SymbolStoreDB;
_db: ISymbolStoreDB;

constructor(dbNamePrefix: string, symbolProvider: SymbolProvider) {
constructor(
dbNamePrefixOrDB: string | ISymbolStoreDB,
symbolProvider: SymbolProvider
) {
this._symbolProvider = symbolProvider;
this._db = new SymbolStoreDB(`${dbNamePrefix}-symbol-tables`);
if (typeof dbNamePrefixOrDB === 'string') {
this._db = new SymbolStoreDB(`${dbNamePrefixOrDB}-symbol-tables`);
} else {
this._db = dbNamePrefixOrDB;
}
}

async closeDb() {
Expand Down
205 changes: 205 additions & 0 deletions src/symbolicator-cli/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// @flow

/*
* This implements a simple CLI to symbolicate profiles captured by the profiler
* or by samply.
*
* To use it it first needs to be built:
* yarn build-symbolicator-cli
*
* Then it can be run from the `dist` directory:
* node dist/symbolicator-cli.js --input <input profile> --output <symbolicated profile> --server <symbol server URL>
*
* For example:
* node dist/symbolicator-cli.js --input samply-profile.json --output profile-symbolicated.json --server http://localhost:3000
*
*/

const fs = require('fs');

import { unserializeProfileOfArbitraryFormat } from '../profile-logic/process-profile';
import { SymbolStore } from '../profile-logic/symbol-store';
import {
symbolicateProfile,
applySymbolicationSteps,
} from '../profile-logic/symbolication';
import type { SymbolicationStepInfo } from '../profile-logic/symbolication';
import type { SymbolTableAsTuple } from '../profile-logic/symbol-store-db';
import * as MozillaSymbolicationAPI from '../profile-logic/mozilla-symbolication-api';
import { SymbolsNotFoundError } from '../profile-logic/errors';
import type { ThreadIndex } from '../types';

/**
* Simple 'in-memory' symbol DB that conforms to the same interface as SymbolStoreDB but
* just stores everything in a simple dictionary instead of IndexedDB. The composite key
* [debugName, breakpadId] is flattened to a string "debugName:breakpadId" to use as the
* map key.
*/
export class InMemorySymbolDB {
_store: Map<string, SymbolTableAsTuple>;

constructor() {
this._store = new Map();
}

_makeKey(debugName: string, breakpadId: string): string {
return `${debugName}:${breakpadId}`;
}

async storeSymbolTable(
debugName: string,
breakpadId: string,
symbolTable: SymbolTableAsTuple
): Promise<void> {
this._store.set(this._makeKey(debugName, breakpadId), symbolTable);
}

async getSymbolTable(
debugName: string,
breakpadId: string
): Promise<SymbolTableAsTuple> {
const key = this._makeKey(debugName, breakpadId);
const value = this._store.get(key);
if (typeof value !== 'undefined') {
return value;
}
throw new SymbolsNotFoundError(
'The requested library does not exist in the database.',
{ debugName, breakpadId }
);
}

async close(): Promise<void> {}
}

interface CliOptions {
input: string;
output: string;
server: string;
}

export async function run(options: CliOptions) {
console.log(`Loading profile from ${options.input}`);
const serializedProfile = JSON.parse(fs.readFileSync(options.input, 'utf8'));
const profile = await unserializeProfileOfArbitraryFormat(serializedProfile);
if (profile === undefined) {
throw new Error('Unable to parse the profile.');
}

const symbolStoreDB = new InMemorySymbolDB();

/**
* SymbolStore implementation which just forwards everything to the symbol server in
* MozillaSymbolicationAPI format. No support for getting symbols from 'the browser' as
* there is no browser in this context.
*/
const symbolStore = new SymbolStore(symbolStoreDB, {
requestSymbolsFromServer: async (requests) => {
for (const { lib } of requests) {
console.log(` Loading symbols for ${lib.debugName}`);
}
try {
return await MozillaSymbolicationAPI.requestSymbols(
'symbol server',
requests,
async (path, json) => {
const response = await fetch(options.server + path, {
body: json,
method: 'POST',
});
return response.json();
}
);
} catch (e) {
throw new Error(
`There was a problem with the symbolication API request to the symbol server: ${e.message}`
);
}
},

requestSymbolsFromBrowser: async () => {
return [];
},

requestSymbolTableFromBrowser: async () => {
throw new Error('Not supported in this context');
},
});

console.log('Symbolicating...');

const symbolicationStepsPerThread: Map<ThreadIndex, SymbolicationStepInfo[]> =
new Map();
await symbolicateProfile(
profile,
symbolStore,
(
threadIndex: ThreadIndex,
symbolicationStepInfo: SymbolicationStepInfo
) => {
let threadSteps = symbolicationStepsPerThread.get(threadIndex);
if (threadSteps === undefined) {
threadSteps = [];
symbolicationStepsPerThread.set(threadIndex, threadSteps);
}
threadSteps.push(symbolicationStepInfo);
}
);

console.log('Applying collected symbolication steps...');

profile.threads = profile.threads.map((oldThread, threadIndex) => {
const symbolicationSteps = symbolicationStepsPerThread.get(threadIndex);
if (symbolicationSteps === undefined) {
return oldThread;
}
const { thread } = applySymbolicationSteps(oldThread, symbolicationSteps);
return thread;
});

profile.meta.symbolicated = true;

console.log(`Saving profile to ${options.output}`);
fs.writeFileSync(options.output, JSON.stringify(profile));
console.log('Finished.');
}

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

if (!('input' in argv && typeof argv.input === 'string')) {
throw new Error(
'Argument --input must be supplied with the path to the input profile'
);
}

if (!('output' in argv && typeof argv.output === 'string')) {
throw new Error(
'Argument --output must be supplied with the path to the output profile'
);
}

if (!('server' in argv && typeof argv.server === 'string')) {
throw new Error(
'Argument --server must be supplied with the URI of the symbol server endpoint'
);
}

return {
input: argv.input,
output: argv.output,
server: argv.server,
};
}

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

module.exports = {
name: 'symbolicator-cli',
target: 'node',
mode: process.env.NODE_ENV,
output: {
path: path.resolve(projectRoot, 'dist'),
filename: 'symbolicator-cli.js',
},
entry: './src/symbolicator-cli/index.js',
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
include: includes,
},
],
},
experiments: {
// Make WebAssembly work just like in webpack v4
syncWebAssembly: true,
},
};
13 changes: 7 additions & 6 deletions src/test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ Flow type tests are a little different, because they do not use Jest. Instead, t

## The tests

| Test type | Description |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [components](./components) | Tests for React components, utilizing Enzyme for full behavioral testing, and snapshot tests to ensure that components output correct markup. |
| [store](./store) | Testing the [Redux](http://redux.js.org/) store using actions and selectors. |
| [types](./types) | Flow type tests. |
| [unit](./unit) | Unit testing |
| Test type | Description |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [components](./components) | Tests for React components, utilizing Enzyme for full behavioral testing, and snapshot tests to ensure that components output correct markup. |
| [store](./store) | Testing the [Redux](http://redux.js.org/) store using actions and selectors. |
| [types](./types) | Flow type tests. |
| [unit](./unit) | Unit testing |
| [integration](./integration) | Integration testing |
Loading

0 comments on commit bf1ac66

Please sign in to comment.