Skip to content

Add profiler-cli for querying profiles - #5963

Merged
canova merged 8 commits into
firefox-devtools:mainfrom
canova:pq-cli
May 4, 2026
Merged

Add profiler-cli for querying profiles#5963
canova merged 8 commits into
firefox-devtools:mainfrom
canova:pq-cli

Conversation

@canova

@canovacanova commented Apr 23, 2026

Copy link
Copy Markdown
Member

Fixes#5953 (we can move follow-ups that we have there once this lands).

This PR is the rebased, squashed and split version of #5663. I also did some reviews on top and updated the code further to fix some issues.

(This PR currently depends on #5968. Please review that one first)

It adds a new command-line tool, profiler-cli, for querying Firefox Profiler profiles from a terminal. The CLI is published as a separate npm package (@firefox-devtools/profiler-cli, alpha) and is the intended entry point for most users

What's in here:

New src/profile-query/ library: Reusable ProfileQuerier class that loads a profile (file, profiler.firefox.com URL, or share link) and exposes methods for profile/thread info, samples (top-down / bottom-up / hot functions), markers, functions, zoom (view range) stack, and per-thread filter stack. All query methods return structured result objects; formatting lives in the CLI layer.

New profiler-cli/ package: Node CLI (profiler-cli, short alias pq) with a persistent daemon architecture so subsequent commands against the same profile are fast. Sessions are stored under ~/.profiler-cli/.

How to do manual testing locally:

  • Pull the branch
  • Run yarn install and yarn build-profiler-cli
  • Add a temporary alias (so it doesn't affect the existing installation that you might have): alias profiler-cli="node $(pwd)/profiler-cli/dist/profiler-cli.js" && alias pq="node $(pwd)/profiler-cli/dist/profiler-cli.js"
  • Then you can use pq and profiler-cli in this terminal session.

@canovacanova added the cli Issues related to the profiler CLI label Apr 23, 2026
@canovacanova self-assigned this Apr 23, 2026
@codecov

codecovBot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.05880% with 1106 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.81%. Comparing base (b868d0e) to head (dfd364d).
⚠️ Report is 13 commits behind head on main.

Files with missing linesPatch %Lines
src/profile-query/index.ts38.56%223 Missing ⚠️
src/profile-query/formatters/page-load.ts0.46%216 Missing ⚠️
src/profile-query/formatters/marker-info.ts75.04%133 Missing ⚠️
src/profile-query/filter-stack.ts0.00%103 Missing ⚠️
src/profile-query/loader.ts0.00%92 Missing ⚠️
src/profile-query/formatters/thread-info.ts51.97%73 Missing ⚠️
src/profile-query/formatters/profile-info.ts0.00%58 Missing ⚠️
src/profile-logic/transforms.ts58.46%54 Missing ⚠️
src/profile-query/function-annotate.ts62.99%47 Missing ⚠️
src/selectors/profile.ts46.34%22 Missing ⚠️
... and 12 more
Additional details and impacted files
@@ Coverage Diff @@## main #5963 +/- ##
==========================================
- Coverage 85.34% 83.81% -1.53% 
==========================================
Files 318 328 +10 Lines 31922 34253 +2331 Branches 8834 9580 +746 ==========================================
+ Hits 27244 28710 +1466 - Misses 4246 5115 +869 + Partials 432 428 -4 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@canova
canova marked this pull request as ready for review April 23, 2026 10:38
@canova
canova requested review from a team and fatadel as code ownersApril 23, 2026 10:38
@canova
canova requested review from mstange and removed request for a teamApril 23, 2026 10:38
@canova

Copy link
Copy Markdown
MemberAuthor

This is ready for review!
(Removing the l10n review request as this ftl change is reviewed already in #5943 which this PR depends)

@flodolo

Copy link
Copy Markdown
Contributor

(Removing the l10n review request as this ftl change is reviewed already in #5943 which this PR depends)

Was too fast 😓

@canova
canovaforce-pushed the pq-cli branch 3 times, most recently from 90b59aa to 11d68caCompareApril 24, 2026 09:25
'function-include filter requires a non-empty filter string.'
);
}
const funcIndexes = new Set(filter.split(',').map(Number));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

surprised we have a comma-separated string of numbers here - we have proper arrays for things like merge-call-node, why not here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mainly because the filter-samples transform accepts a string as a filter, and the idea was to make it more generic so it can accept a bunch of filterType as an argument. But it kinda restricts us in this case:

/**
* Filter the samples in the thread by the filter.
* Currently it only supports filtering by the marker name but can be extended
* to support more filters in the future.
*/
'filter-samples': {
readonlytype: 'filter-samples';
// Expand this type when you need to support more than just the marker.
readonlyfilterType: FilterSamplesType;
readonlyfilter: string;
};

But that's not necessarily a restriction that we have to follow, we can always change it something like this instead:

'filter-samples':
|{readonlytype: 'filter-samples';readonly filterType: 'marker-search'|'outside-marker';readonly filter: string}|{ readonly type: 'filter-samples';readonly filterType: 'function-include'|'stack-prefix';readonly funcIndexes: number[]}|{ readonly type: 'filter-samples';readonly filterType: 'stack-suffix';readonly funcIndex: number}

I don't know if we should do it here in a follow-up as it wouldn't need any upgrader, let me know what you think.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it even need to be a filter-samples transform? All transforms can filter samples.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well yeah, technically they can all filter samples, but currently we only have two transforms that touch the samples table: drop-function and filter-samples. The others usually update the stack table, frame table etc. That's why I think we had this distinction initially. When we were adding the filter-samples transform for the "drop samples outside of marker xyz" functionality, we wanted to make this a bit more generic so we can have more filters to be able to filter the samples. I don't think it makes a huge difference though.

}
}
}
// Memoize bottom-up: does this stack contain any frame for funcIndex?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function is computing a bunch of things that we already have code to compute. Haven't reviewed in detail yet.

@canovacanovaApr 27, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I reviewed that one more time and improved it. Put it as a separate commit so it's easier to review: d1ebfbb

}
}

// Assembly annotation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't really have enough information to make Assembly annotation work properly. We only have the mapping between source line and assembly instruction for instructions that were sampled. The symbolication API doesn't give us this information for the rest of the instructions yet.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but I wanted to do exactly what assembly view in the browser does, and I believe this behavior matches it. When I look at the assembly view in the browser I only see a single line in there with all the sample information summed up. And I use the same selectors to fetch this information. So I think it makes sense to keep them in sync. WDYT?

* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

export type Slice = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole file could use some comments. I wrote this code but I don't like it very much - and that includes the name! It's supposed to make a tree representation of the information you'd get from looking at a visual CPU usage graph, by applying a threshold operation to the entire time series, and then gradually increasing the threshold and seeing where "above water level" things split into separate islands. I suppose we can land it without comments and then I'll make a PR to add them

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, that's a good point. I kept it as is for now.

@canova
canovaforce-pushed the pq-cli branch 2 times, most recently from aa25d72 to d1ebfbbCompareApril 27, 2026 11:45
@canova

Copy link
Copy Markdown
MemberAuthor

Thanks for the first look @mstange! I merged the "idle filtering" PR and rebased this PR on top of the recent main, so you'll see 2 less commits here.

@canova
canova requested a review from mstangeApril 28, 2026 08:22
Comment threadsrc/profile-query/index.ts Outdated
/**
* Per-thread sizes of each filter "push group". One `filter push` adds one
* entry whose value is the number of Redux transforms it dispatched (e.g.
* `--merge f-1,f-2` → 2). `filter pop N` removes N groups, popping the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a MergeSet transform :)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that would be nice to have! I added a FIXME below that comment.

Comment on lines +192 to +201
/**
* Override the symbolServerUrl field of the current URL state. `symbolServerUrl`
* has no dedicated action (the reducer is a read-only pass-through), so the
* only way to change it is to replace the whole UrlState via UPDATE_URL_STATE.
*/
function overrideSymbolServerUrl(store: Store, symbolServerUrl: string): void {
const current = getUrlState(store.getState());
const next: UrlState = { ...current, symbolServerUrl };
store.dispatch(updateUrlState(next));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like something we should fix properly as a follow-up.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, added a FIXME here too.

Comment on lines +22 to +26
* Maps marker handles (like "m-1", "m-2") to (threadIndex, markerIndex) pairs.
* This provides a user-friendly way to reference markers in the CLI.
*
* Since each thread has its own marker list, we need to store both the thread
* index and the marker index to uniquely identify a marker.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be better to just have the thread index in the marker handle, and the actual marker index so that they're stable across multiple loads of the same profile.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is your suggestion something like m-<thread-handle>-<marker-index>?

I guess it would work, but it'll produce longer marker handles instead of nice short ones. But agents don't care too much about those, so maybe that's fine. It would be a bit more work for humans.

Comment threadsrc/profile-query/README.md Outdated
Comment threadsrc/profile-query/README.md Outdated
Comment threadsrc/profile-query/timestamps.ts Outdated
Comment on lines +134 to +142
* TimestampManager creates compact, hierarchical names for timestamps.
*
* Example names for range [1000, 2000]:
* - 1000 → "ts-0" (range start)
* - 2000 → "ts-Z" (range end)
* - 1500 → "ts-K" (middle of range)
* - 1000.1 → "ts-04" (between ts-0 and ts-1, drills into ts-0's subrange)
* - 500 → "ts<0K" (before range start, in first bucket before-range)
* - 2500 → "ts>0K" (after range end, in first bucket after-range)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came up with this scheme but I guess we'll find out whether it's valuable or just cute and overengineered.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hehe, so far I don't necessarily see them being used in some example sessions I had. But let's see.

Comment on lines +359 to +366
export type CallTreeScoringStrategy =
| 'exponential-0.95' // totalPercentage * (0.95 ^ depth) - slow decay
| 'exponential-0.9' // totalPercentage * (0.9 ^ depth) - medium decay
| 'exponential-0.8' // totalPercentage * (0.8 ^ depth) - fast decay
| 'harmonic-0.1' // totalPercentage / (1 + 0.1 * depth) - very slow
| 'harmonic-0.5' // totalPercentage / (1 + 0.5 * depth) - medium
| 'harmonic-1.0' // totalPercentage / (1 + depth) - standard harmonic
| 'percentage-only'; // totalPercentage - no depth penalty

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should look at slimming this down

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Another thing is that I haven't seen them being used at all so far.

Comment threadprofiler-cli/CONTRIBUTING.md Outdated
Comment threadprofiler-cli/src/test/integration/utils.ts Outdated
Comment threadprofiler-cli/src/test/unit/client.test.ts Outdated

@mstangemstange left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine to land in this state. The comments I wrote on this PR are all things we can address in follow ups. And I expect us to keep tweaking things.

@canovacanova left a comment

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! Updated the PR.

Comment threadprofiler-cli/src/test/integration/utils.ts Outdated
Comment threadprofiler-cli/src/test/unit/client.test.ts Outdated
Comment threadsrc/profile-query/index.ts Outdated
/**
* Per-thread sizes of each filter "push group". One `filter push` adds one
* entry whose value is the number of Redux transforms it dispatched (e.g.
* `--merge f-1,f-2` → 2). `filter pop N` removes N groups, popping the

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that would be nice to have! I added a FIXME below that comment.

Comment on lines +192 to +201
/**
* Override the symbolServerUrl field of the current URL state. `symbolServerUrl`
* has no dedicated action (the reducer is a read-only pass-through), so the
* only way to change it is to replace the whole UrlState via UPDATE_URL_STATE.
*/
function overrideSymbolServerUrl(store: Store, symbolServerUrl: string): void {
const current = getUrlState(store.getState());
const next: UrlState = { ...current, symbolServerUrl };
store.dispatch(updateUrlState(next));
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, added a FIXME here too.

Comment on lines +22 to +26
* Maps marker handles (like "m-1", "m-2") to (threadIndex, markerIndex) pairs.
* This provides a user-friendly way to reference markers in the CLI.
*
* Since each thread has its own marker list, we need to store both the thread
* index and the marker index to uniquely identify a marker.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is your suggestion something like m-<thread-handle>-<marker-index>?

I guess it would work, but it'll produce longer marker handles instead of nice short ones. But agents don't care too much about those, so maybe that's fine. It would be a bit more work for humans.

Comment threadsrc/profile-query/timestamps.ts Outdated
Comment on lines +134 to +142
* TimestampManager creates compact, hierarchical names for timestamps.
*
* Example names for range [1000, 2000]:
* - 1000 → "ts-0" (range start)
* - 2000 → "ts-Z" (range end)
* - 1500 → "ts-K" (middle of range)
* - 1000.1 → "ts-04" (between ts-0 and ts-1, drills into ts-0's subrange)
* - 500 → "ts<0K" (before range start, in first bucket before-range)
* - 2500 → "ts>0K" (after range end, in first bucket after-range)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hehe, so far I don't necessarily see them being used in some example sessions I had. But let's see.

Comment on lines +359 to +366
export type CallTreeScoringStrategy =
| 'exponential-0.95' // totalPercentage * (0.95 ^ depth) - slow decay
| 'exponential-0.9' // totalPercentage * (0.9 ^ depth) - medium decay
| 'exponential-0.8' // totalPercentage * (0.8 ^ depth) - fast decay
| 'harmonic-0.1' // totalPercentage / (1 + 0.1 * depth) - very slow
| 'harmonic-0.5' // totalPercentage / (1 + 0.5 * depth) - medium
| 'harmonic-1.0' // totalPercentage / (1 + depth) - standard harmonic
| 'percentage-only'; // totalPercentage - no depth penalty

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Another thing is that I haven't seen them being used at all so far.

'function-include filter requires a non-empty filter string.'
);
}
const funcIndexes = new Set(filter.split(',').map(Number));

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well yeah, technically they can all filter samples, but currently we only have two transforms that touch the samples table: drop-function and filter-samples. The others usually update the stack table, frame table etc. That's why I think we had this distinction initially. When we were adding the filter-samples transform for the "drop samples outside of marker xyz" functionality, we wanted to make this a bit more generic so we can have more filters to be able to filter the samples. I don't think it makes a huge difference though.

@canova
canova merged commit 6b58ea6 into firefox-devtools:mainMay 4, 2026
21 of 23 checks passed
@canovacanova mentioned this pull request May 7, 2026
canova added a commit that referenced this pull request May 7, 2026
Changes:
[Markus Stange] Use custom splitter component (#4606)
[fatadel] Fix Download button text color when clicked (#5985)
[Samuel Glauser] Fix fullscreen icon size in bottom box (#5987)
[Nazım Can Altınova] Add `profiler-cli` for querying profiles (#5963)
[Nazım Can Altınova] Bump profiler cli version to 0.1.0 (#5996)
[Markus Stange] Switch from max-height to maxHeight in JSX
style={{...}}. (#5990)
[carverdamien] Fix comment about how time and duration are stored
(#5997)
[Nazım Can Altınova] Do not show console error when libnames are failed
to parse as a URL (#5993)
[Nazım Can Altınova] Fix the unnecessary stringify of Uint8Array
contents during zip profile extraction (#6004)
And special thanks to our localizers:
en-CA: chutten
en-CA: Saurabh
en-GB: Ian Neal
es-CL: ravmn
fy-NL: Fjoerfoks
ia: Melo46
nl: Mark Heijl
ru: Valery Ledovskoy
sv-SE: Andreas Pettersson
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cliIssues related to the profiler CLI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement and land the MVP of the profiler CLI

3 participants

@canova@flodolo@mstange