Skip to content

Commit 1a7c76c

Browse files
authored
More additions to profiler-edit, for sp3 profiles (#6009)
This makes it so that you can run profiler-edit with `--insert-label-frames browser_labels.toml --only-keep-threads-with-markers-matching='-async,-sync' --merge-non-overlapping-threads-by-name --set-name 'Sp3 5x (with labels, combined main threads)'` to turn https://share.firefox.dev/4cXQFED into https://share.firefox.dev/4ezEcsa
2 parents cfd6aef + 93787a0 commit 1a7c76c

4 files changed

Lines changed: 474 additions & 29 deletions

File tree

src/node-tools/profiler-edit.ts

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44
import fs from 'fs';
5-
import { Command, CommanderError, Option } from 'commander';
5+
import {
6+
Command,
7+
CommanderError,
8+
InvalidArgumentError,
9+
Option,
10+
} from 'commander';
611
import { parse as parseToml } from 'smol-toml';
712

813
import {
@@ -25,13 +30,22 @@ import {
2530
applyWasmSymbolication,
2631
type WasmSymbolicationSpec,
2732
} from 'firefox-profiler/profile-logic/wasm-symbolication';
28-
import type { Profile } from 'firefox-profiler/types/profile';
33+
import { getThreadsWithMarkersMatchingSearchFilter } from 'firefox-profiler/profile-logic/marker-data';
34+
import type {
35+
Profile,
36+
RawThread,
37+
ThreadIndex,
38+
} from 'firefox-profiler/types/profile';
2939
import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';
3040
import {
3141
type AutoLabel,
3242
type LabelDescription,
3343
resolveAllLabels,
3444
} from 'firefox-profiler/utils/label-templates';
45+
import {
46+
mergeNonOverlappingThreadsByName,
47+
remapCountersAndProfilerOverhead,
48+
} from 'firefox-profiler/profile-logic/merge-compare';
3549

3650
/**
3751
* A CLI tool for editing profiles.
@@ -52,9 +66,13 @@ import {
5266
*
5367
* node node-tools-dist/profiler-edit.js --from-hash w1spyw917hg... -o out.json.gz \
5468
* --insert-label-frames known-functions.toml
69+
*
70+
* node node-tools-dist/profiler-edit.js -i big.json.gz -o small.json.gz \
71+
* --only-keep-threads-with-markers-matching '-async,-sync' \
72+
* --merge-non-overlapping-threads-by-name
5573
*/
5674

57-
type ProfileSource =
75+
export type ProfileSource =
5876
| { type: 'FILE'; path: string }
5977
| { type: 'URL'; url: string }
6078
| { type: 'HASH'; hash: string };
@@ -63,7 +81,7 @@ type ProfileSource =
6381
// supplies symbol names, plus (optionally) the URL of the stripped wasm in the
6482
// profile to which those names should be applied. If `strippedWasmUrl` is
6583
// omitted, the profile must contain exactly one .wasm source, which is used.
66-
interface WasmSymbolicationCliSpec {
84+
export interface WasmSymbolicationCliSpec {
6785
// Path to the local unstripped .wasm file (with a "name" custom section).
6886
unstrippedWasmPath: string;
6987
// URL of the matching stripped wasm as it appears in the profile.
@@ -76,9 +94,12 @@ export interface CliOptions {
7694
symbolicateWithServer?: string;
7795
symbolicateWasm: WasmSymbolicationCliSpec[];
7896
insertLabelFrames?: string;
97+
onlyKeepThreadsWithMarkersMatching?: string;
98+
mergeNonOverlappingThreadsByName?: boolean;
99+
setName?: string;
79100
}
80101

81-
function loadWasmSymbolicationSpecs(
102+
export function loadWasmSymbolicationSpecs(
82103
cliSpecs: WasmSymbolicationCliSpec[]
83104
): WasmSymbolicationSpec[] {
84105
return cliSpecs.map((spec) => {
@@ -97,7 +118,7 @@ function loadWasmSymbolicationSpecs(
97118
* (mirrors getLabelIndexForFunc in insert-stack-labels.ts), so auto-discovery
98119
* sees the same strings the labeler will compare against.
99120
*/
100-
function collectFuncNames(profile: Profile): string[] {
121+
export function collectFuncNames(profile: Profile): string[] {
101122
const { funcTable, sources, stringArray } = profile.shared;
102123
const result: string[] = [];
103124
for (let i = 0; i < funcTable.length; i++) {
@@ -265,6 +286,41 @@ export async function run(options: CliOptions) {
265286
profile = insertStackLabels(profile, labels);
266287
}
267288

289+
if (
290+
options.onlyKeepThreadsWithMarkersMatching !== undefined &&
291+
options.onlyKeepThreadsWithMarkersMatching !== ''
292+
) {
293+
const before = profile.threads.length;
294+
const matchingThreadIndexes = getThreadsWithMarkersMatchingSearchFilter(
295+
profile,
296+
options.onlyKeepThreadsWithMarkersMatching
297+
);
298+
const oldThreadIndexToNew = new Map<ThreadIndex, ThreadIndex>();
299+
const matchingThreads: RawThread[] = [];
300+
profile.threads.forEach((thread, oldIndex) => {
301+
if (matchingThreadIndexes.has(oldIndex)) {
302+
oldThreadIndexToNew.set(oldIndex, matchingThreads.length);
303+
matchingThreads.push(thread);
304+
}
305+
});
306+
profile = {
307+
...profile,
308+
threads: matchingThreads,
309+
...remapCountersAndProfilerOverhead(profile, oldThreadIndexToNew),
310+
};
311+
console.log(
312+
`Kept ${profile.threads.length} of ${before} threads with markers matching ${JSON.stringify(options.onlyKeepThreadsWithMarkersMatching)}.`
313+
);
314+
}
315+
316+
if (options.mergeNonOverlappingThreadsByName) {
317+
profile = mergeNonOverlappingThreadsByName(profile);
318+
}
319+
320+
if (options.setName !== undefined) {
321+
profile.meta.product = options.setName;
322+
}
323+
268324
const { profile: compactedProfile } = computeCompactedProfile(profile);
269325

270326
const outputFilename = options.output;
@@ -298,6 +354,15 @@ function collectWasm(
298354
return [...previous, { unstrippedWasmPath: value }];
299355
}
300356

357+
function requireNonEmpty(flagName: string): (value: string) => string {
358+
return (value: string) => {
359+
if (value === '') {
360+
throw new InvalidArgumentError(`${flagName} requires a non-empty value`);
361+
}
362+
return value;
363+
};
364+
}
365+
301366
export function makeOptionsFromArgv(processArgv: string[]): CliOptions {
302367
const program = new Command();
303368
program
@@ -324,7 +389,20 @@ export function makeOptionsFromArgv(processArgv: string[]): CliOptions {
324389
.argParser(collectWasm)
325390
.default([] as WasmSymbolicationCliSpec[])
326391
)
327-
.option('--insert-label-frames <path>', 'TOML file with label definitions');
392+
.option('--insert-label-frames <path>', 'TOML file with label definitions')
393+
.option(
394+
'--only-keep-threads-with-markers-matching <search>',
395+
'Keep only threads with markers matching the given search string'
396+
)
397+
.option(
398+
'--merge-non-overlapping-threads-by-name',
399+
'Merge same-named threads across non-overlapping process runs'
400+
)
401+
.option(
402+
'--set-name <name>',
403+
'Override the profile product name',
404+
requireNonEmpty('--set-name')
405+
);
328406

329407
program.parse(processArgv);
330408
const opts = program.opts();
@@ -376,6 +454,14 @@ export function makeOptionsFromArgv(processArgv: string[]): CliOptions {
376454
opts.insertLabelFrames !== ''
377455
? opts.insertLabelFrames
378456
: undefined,
457+
onlyKeepThreadsWithMarkersMatching:
458+
typeof opts.onlyKeepThreadsWithMarkersMatching === 'string' &&
459+
opts.onlyKeepThreadsWithMarkersMatching !== ''
460+
? opts.onlyKeepThreadsWithMarkersMatching
461+
: undefined,
462+
mergeNonOverlappingThreadsByName:
463+
opts.mergeNonOverlappingThreadsByName === true,
464+
setName: typeof opts.setName === 'string' ? opts.setName : undefined,
379465
};
380466
}
381467

src/profile-logic/marker-data.ts

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
/* This Source Code Form is subject to the terms of the Mozilla Public
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4-
import { getEmptyRawMarkerTable } from './data-structures';
5-
import { getFriendlyThreadName } from './profile-data';
6-
import { removeFilePath, removeURLs, stringsToRegExp } from '../utils/string';
4+
import {
5+
getDefaultCategories,
6+
getEmptyRawMarkerTable,
7+
} from './data-structures';
8+
import { getFriendlyThreadName, getTimeRangeForThread } from './profile-data';
9+
import {
10+
removeFilePath,
11+
removeURLs,
12+
stringsToRegExp,
13+
splitSearchString,
14+
} from '../utils/string';
715
import { StringTable } from '../utils/string-table';
816
import { ensureExists, assertExhaustiveCheck } from '../utils/types';
917
import {
@@ -15,6 +23,7 @@ import {
1523
import {
1624
getSchemaFromMarker,
1725
markerPayloadMatchesSearch,
26+
markerSchemaFrontEndOnly,
1827
} from './marker-schema';
1928

2029
import type {
@@ -42,6 +51,8 @@ import type {
4251
MarkerDisplayLocation,
4352
Tid,
4453
LogMarkerPayload,
54+
ThreadIndex,
55+
Profile,
4556
} from 'firefox-profiler/types';
4657

4758
/**
@@ -998,6 +1009,99 @@ export function deriveMarkersFromRawMarkerTable(
9981009
return { markers, markerIndexToRawMarkerIndexes };
9991010
}
10001011

1012+
/**
1013+
* Return the merged list of marker schemas which contains both the schemas
1014+
* from the profile, and the front-end only schemas ("Jank" etc).
1015+
*/
1016+
export function computeCombinedMarkerSchemaList(
1017+
markerSchemaFromProfile: MarkerSchema[]
1018+
): MarkerSchema[] {
1019+
const frontEndSchemaNames = new Set(
1020+
markerSchemaFrontEndOnly.map((schema) => schema.name)
1021+
);
1022+
return [
1023+
...markerSchemaFromProfile.filter(
1024+
(schema) => !frontEndSchemaNames.has(schema.name)
1025+
),
1026+
...markerSchemaFrontEndOnly,
1027+
];
1028+
}
1029+
1030+
/**
1031+
* Create a Set from the list, with the key being schema.name.
1032+
*/
1033+
export function computeMarkerSchemaByName(
1034+
schemaList: MarkerSchema[]
1035+
): MarkerSchemaByName {
1036+
const markerSchemaByName: MarkerSchemaByName = Object.create(null);
1037+
for (const schema of schemaList) {
1038+
markerSchemaByName[schema.name] = schema;
1039+
}
1040+
return markerSchemaByName;
1041+
}
1042+
1043+
/**
1044+
* Return the set of threads that have at least one marker matching the given
1045+
* marker search string, using the same regular marker search syntax: comma-
1046+
* separated terms, optional `field:value` and `-field:value` qualifiers.
1047+
*
1048+
* This is a somewhat expensive operation because we call deriveMarkersFromRawMarkerTable
1049+
* for every thread.
1050+
*/
1051+
export function getThreadsWithMarkersMatchingSearchFilter(
1052+
profile: Profile,
1053+
markerSearch: string
1054+
): Set<ThreadIndex> {
1055+
const searchRegExps = stringsToMarkerRegExps(splitSearchString(markerSearch));
1056+
if (searchRegExps === null) {
1057+
return new Set();
1058+
}
1059+
1060+
const stringTable = StringTable.withBackingArray(profile.shared.stringArray);
1061+
const categoryList = profile.meta.categories ?? getDefaultCategories();
1062+
1063+
const schemaList = computeCombinedMarkerSchemaList(
1064+
profile.meta.markerSchema ?? []
1065+
);
1066+
const markerSchemaByName = computeMarkerSchemaByName(schemaList);
1067+
1068+
const ipcCorrelations = correlateIPCMarkers(profile.threads, profile.shared);
1069+
1070+
const matchingThreads = new Set<ThreadIndex>();
1071+
1072+
for (
1073+
let threadIndex = 0;
1074+
threadIndex < profile.threads.length;
1075+
threadIndex++
1076+
) {
1077+
const thread = profile.threads[threadIndex];
1078+
const { markers } = deriveMarkersFromRawMarkerTable(
1079+
thread.markers,
1080+
profile.shared.stringArray,
1081+
thread.tid,
1082+
getTimeRangeForThread(thread, profile.meta.interval),
1083+
ipcCorrelations
1084+
);
1085+
if (markers.length === 0) {
1086+
continue;
1087+
}
1088+
const markerIndexes = markers.map((_, i) => i);
1089+
const filtered = getSearchFilteredMarkerIndexes(
1090+
(i) => markers[i],
1091+
markerIndexes,
1092+
markerSchemaByName,
1093+
searchRegExps,
1094+
stringTable,
1095+
categoryList
1096+
);
1097+
if (filtered.length > 0) {
1098+
matchingThreads.add(threadIndex);
1099+
}
1100+
}
1101+
1102+
return matchingThreads;
1103+
}
1104+
10011105
/**
10021106
* This function filters markers from a thread's raw marker table using the
10031107
* range specified as parameter. It's not used by the normal marker filtering

0 commit comments

Comments
 (0)