Skip to content

Add profiler-cli for querying profiles#5963

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

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

Conversation

@canova
Copy link
Copy Markdown
Member

@canova canova commented Apr 23, 2026

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.

@canova canova added the cli Issues related to the profiler CLI label Apr 23, 2026
@canova canova self-assigned this Apr 23, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

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 lines Patch % Lines
src/profile-query/index.ts 38.56% 223 Missing ⚠️
src/profile-query/formatters/page-load.ts 0.46% 216 Missing ⚠️
src/profile-query/formatters/marker-info.ts 75.04% 133 Missing ⚠️
src/profile-query/filter-stack.ts 0.00% 103 Missing ⚠️
src/profile-query/loader.ts 0.00% 92 Missing ⚠️
src/profile-query/formatters/thread-info.ts 51.97% 73 Missing ⚠️
src/profile-query/formatters/profile-info.ts 0.00% 58 Missing ⚠️
src/profile-logic/transforms.ts 58.46% 54 Missing ⚠️
src/profile-query/function-annotate.ts 62.99% 47 Missing ⚠️
src/selectors/profile.ts 46.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 owners April 23, 2026 10:38
@canova canova requested review from mstange and removed request for a team April 23, 2026 10:38
@canova
Copy link
Copy Markdown
Member Author

canova commented Apr 23, 2026

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

flodolo commented Apr 23, 2026

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

Was too fast 😓

@canova canova force-pushed the pq-cli branch 3 times, most recently from 90b59aa to 11d68ca Compare April 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
Member Author

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': {
readonly type: 'filter-samples';
// Expand this type when you need to support more than just the marker.
readonly filterType: FilterSamplesType;
readonly filter: string;
};

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

'filter-samples':
    | { readonly type: '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
Member Author

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.

Comment thread src/profile-query/function-annotate.ts Outdated
}
}
}
// 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.

Copy link
Copy Markdown
Member Author

@canova canova Apr 27, 2026

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

Comment thread src/profile-query/function-annotate.ts Outdated
}
}

// 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
Member Author

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?

Comment thread src/utils/slice-tree.ts
* 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
Member Author

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 canova force-pushed the pq-cli branch 2 times, most recently from aa25d72 to d1ebfbb Compare April 27, 2026 11:45
@canova
Copy link
Copy Markdown
Member Author

canova commented Apr 27, 2026

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 mstange April 28, 2026 08:22
Comment thread src/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
Member Author

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
Member Author

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
Member Author

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 thread src/profile-query/README.md Outdated
Comment thread src/profile-query/README.md Outdated
Comment thread src/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
Member Author

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
Member Author

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 thread profiler-cli/CONTRIBUTING.md Outdated
Comment thread profiler-cli/src/test/integration/utils.ts Outdated
Comment thread profiler-cli/src/test/unit/client.test.ts Outdated
Copy link
Copy Markdown
Contributor

@mstange mstange left a comment

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.

Copy link
Copy Markdown
Member Author

@canova canova left a comment

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 thread profiler-cli/src/test/integration/utils.ts Outdated
Comment thread profiler-cli/src/test/unit/client.test.ts Outdated
Comment thread src/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
Member Author

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
Member Author

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
Member Author

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 thread src/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
Member Author

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
Member Author

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
Member Author

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:main May 4, 2026
21 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Issues 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