Skip to content

Add a basic function list panel - #6403

Merged
mstange merged 7 commits into
firefox-devtools:mainfrom
mstange:push-qoptmxwlqtrm
Nov 11, 2026
Merged

mstange merged 7 commits into
firefox-devtools:mainfrom
mstange:push-qoptmxwlqtrm

Conversation

@mstange

@mstange mstange commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Main | Deploy preview

This PR adds a "Function List" panel. In this PR this is only the plain list and doesn't have caller / callee trees or flame graphs yet; see #5233 for a full demo with those included.

@mstange
mstange requested a review from canova October 19, 2026 22:16
@mstange
mstange requested a review from a team as a code owner October 19, 2026 22:16
@codecov

codecov Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.09278% with 82 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.95%. Comparing base (db1f1d3) to head (751f8c6).
⚠️ Report is 22 commits behind head on main.

Files with missing lines Patch % Lines
src/components/shared/FunctionListContextMenu.tsx 78.52% 31 Missing and 1 partial ⚠️
src/components/calltree/FunctionList.tsx 73.13% 18 Missing ⚠️
src/reducers/url-state.ts 73.17% 11 Missing ⚠️
src/components/calltree/columns.ts 71.42% 6 Missing ⚠️
src/actions/profile-view.ts 91.83% 4 Missing ⚠️
src/profile-logic/call-tree.ts 80.95% 4 Missing ⚠️
src/components/timeline/TrackThread.tsx 50.00% 1 Missing and 1 partial ⚠️
src/reducers/profile-view.ts 92.00% 2 Missing ⚠️
src/app-logic/url-handling.ts 95.23% 1 Missing ⚠️
src/profile-logic/symbolication.ts 80.00% 1 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6403      +/-   ##
==========================================
- Coverage   84.95%   84.95%   -0.01%     
==========================================
  Files         359      362       +3     
  Lines       39137    39631     +494     
  Branches    11096    11260     +164     
==========================================
+ Hits        33249    33668     +419     
- Misses       5451     5524      +73     
- Partials      437      439       +2     

☔ View full report in Codecov by Harness.
📢 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 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR!

Do you think this basic version of the function list is still useful when we don't have the other panels in it? I'm leaning towards hiding the panel for now, and maybe make it accessible from the devtools etc. for testing. But if you think it still has some value in this state we can also ship it like this.

Another issue that I noticed is that, whenever I switch to another thread while the function list is selected, we don't select any function in the list by default. But we do that initial selection for the first loaded thread only (I think we run that code only on mount). In the call tree, we always select something. I think it would be good to do the same.

Comment thread src/app-logic/url-handling.ts Outdated
// function list's default", encoded as an absent query parameter.
const VALID_FUNCTION_LIST_SORT_COLUMNS = new Set(['total', 'self']);

function convertFunctionListSortToString(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks very similar to how we do for the marker table. Can we maybe deduplicate these by merging these ..ToString and ..FromString helpers. I think it should be easy to do that.

Comment thread src/app-logic/url-handling.ts Outdated
}

// FunctionList sort URL encoding. Same convention as the marker table:
// internal storage is primary-last, URL is primary-first. `null` means "at the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hmm, I think this "primary-first" comment is not correct actually. Currently we do .reverse() in both directions but ColumnSortState.entries is already primary first. So this ends up making primary last. I think the comment below (deduplicating) would also help with this difference.

type DispatchProps = {
readonly changeSelectedFunctionIndex: typeof changeSelectedFunctionIndex;
readonly changeRightClickedFunctionIndex: typeof changeRightClickedFunctionIndex;
readonly addTransformToStack: typeof addTransformToStack;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: we don't seem to dispatch this action so far, but maybe we'll use it later?

Comment thread src/profile-logic/call-tree.ts Outdated
Comment on lines 410 to 411
// The call tree's own order (by total sample count) is the intended order,
// so no columns are user-sortable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like this is not accurate anymore as function list has user-sortable columns

Comment thread src/profile-logic/call-tree.ts Outdated
// The call tree's own order (by total sample count) is the intended order,
// so no columns are user-sortable.
getSortableColumns(): SortableColumn[] {
if (this._internal instanceof CallTreeInternalFunctionList) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hmm these checks are a bit weird. Wouldn't it be better to create a sorting method on CallTreeInternal and implement them inside of them directly? This would simplify both getSortableColumns and getRoots.

Comment thread src/actions/profile-view.ts Outdated
Comment on lines +156 to +159
// Update window.history synchronously instead of waiting for the
// UrlManager's componentDidUpdate, which is deferred by React's render
// scheduling and would otherwise pushState a new entry.
replaceHistoryWithUrlState(getUrlState(getState()));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh welp, calling replaceState from the action directly before UrlManager seems hacky to me. We have the UrlManager to handle these type of things, and I think it should be handled there.

Thinking about it, I think this currently works coincidentally because calling this here skips the checks inside that UrlManager. And it also only works because FunctionList's componentDidMount runs before UrlManager.componentDidUpdate if I understand correctly.

Can we make it so that UrlManager can make this decision instead? We could add isSelectionOnlyUrlChange(prev, next) helper in url-handling.ts and then and then we can use it inside UrlManager.

Also I think we wanted to also keep the selected call node of call tree in the url. I think having something like isSelectionOnlyUrlChange would allow us to extend more easily later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Uh oh I hadn't really looked at this part at all. Thank you for catching this. isSelectionOnlyUrlChange is a great idea.

vars={item.l10nVars ? item.l10nVars(ctx) : undefined}
elems={item.l10nElems}
>
<div className="react-contextmenu-item-content" title={oneLine``}>

@canova canova Nov 7, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: just title=""? I don't also know if we need it because of fluent, we might also just remove the whole attribute if it works.

Add a new "Function List" tab which shows a flat, sortable list of functions
rather than a tree. Selection is per-function instead of per-call-node, so
this adds a per-thread selected function to the URL state, along with the
URL handling, reducers and selectors that go with it.

Selecting a function replaces the current history entry instead of pushing a
new one. Otherwise, holding the down arrow key in the list would flood the
back/forward history and get rate-limited by the browser.
The call tree's context menu operates on a call node path, which the function
list does not have. Add a sibling menu that operates on a function index
instead. The menu items are described by a data table rather than repeated
JSX, because the same set of transforms is about to be reused by more than one
view.
These are the same single-key shortcuts the call tree context menu offers
(m, f, S, c, C, r, R, d), applied to the selected function.
When the function list tab is active, the activity graph should highlight
every sample whose stack contains the selected function, rather than the
samples belonging to a selected call node.
Clicking a sample already selects the corresponding self call node. When the
function list tab is active, also select that call node's function so the
click has an effect there too.
Previously, changeSelectedFunctionIndex called replaceState directly, so
that the UrlManager's later componentDidUpdate found the URL already up to
date and didn't push a new entry. Instead, the UrlManager now compares the
previous and the next URL state, and replaces the current entry if only
the selection changed.
@mstange

mstange commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the PR!

Do you think this basic version of the function list is still useful when we don't have the other panels in it? I'm leaning towards hiding the panel for now, and maybe make it accessible from the devtools etc. for testing. But if you think it still has some value in this state we can also ship it like this.

I don't have a super strong opinion on it but I think it's useful enough in this state to be visible by default, specifically the fact that you can select a function and have the activity graph show you where in the timeline it is present anywhere in the stack.

Another issue that I noticed is that, whenever I switch to another thread while the function list is selected, we don't select any function in the list by default. But we do that initial selection for the first loaded thread only (I think we run that code only on mount). In the call tree, we always select something. I think it would be good to do the same.

True. I added this in componentDidUpdate:

if (this.props.threadsKey !== prevProps.threadsKey) {
  this.maybeProcureInitialSelection();
}

@canova canova left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR!
Do you think this basic version of the function list is still useful when we don't have the other panels in it? I'm leaning towards hiding the panel for now, and maybe make it accessible from the devtools etc. for testing. But if you think it still has some value in this state we can also ship it like this.

I don't have a super strong opinion on it but I think it's useful enough in this state to be visible by default, specifically the fact that you can select a function and have the activity graph show you where in the timeline it is present anywhere in the stack.

Sounds good, let's ship it then!

Another issue I found:

STR:

  • Open the right sidebar if it's not open yet.
  • Select a function in the function list
  • Press "m" to merge the function.

The function is no longer visible but the right sidebar still shows the old function (and no line is selected visibly anymore)

And then I can keep spamming "m" and it applies the merge transform to the same function that's no longer visible.
When I press "m" on the call tree, the selected line gets updated.

I guess it's not a huge bug, but it's worth fixing either here or as a follow-up.

@mstange
mstange added this pull request to the merge queue Nov 11, 2026
Merged via the queue into firefox-devtools:main with commit a4c1399 Nov 11, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants