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/. */
44import fs from 'fs' ;
5- import { Command , CommanderError , Option } from 'commander' ;
5+ import {
6+ Command ,
7+ CommanderError ,
8+ InvalidArgumentError ,
9+ Option ,
10+ } from 'commander' ;
611import { parse as parseToml } from 'smol-toml' ;
712
813import {
@@ -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' ;
2939import { assertExhaustiveCheck } from 'firefox-profiler/utils/types' ;
3040import {
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+
301366export 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
0 commit comments