Skip to content

[Tab selector 5] Add a tab selector component and implement tab switching#5093

Merged
canova merged 16 commits into
firefox-devtools:mainfrom
canova:tab-selector-5
Sep 18, 2024
Merged

[Tab selector 5] Add a tab selector component and implement tab switching#5093
canova merged 16 commits into
firefox-devtools:mainfrom
canova:tab-selector-5

Conversation

@canova

@canova canova commented Aug 26, 2024

Copy link
Copy Markdown
Member

This PR implements the basic tab selector component and makes it possible to switch between tabs.

Currently there are 3 main things missing, I will follow-up with these PRs after this one.

  • Sort the tab selector list by their thread activity.
  • Get the real names of web extensions.
  • Add icons of the domains inside the tab selector.

And then, I would like to sort the threads in the timeline differently if it's filtered by tab. Currently the parent process is always at the top. I would like to put the most active content process to the top if a tab is selected.

Deploy preview

@canova canova requested a review from a team as a code owner August 26, 2024 11:43
@canova canova requested review from julienw and removed request for a team August 26, 2024 11:43
@codecov

codecov Bot commented Aug 26, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 86.80556% with 19 lines in your changes missing coverage. Please review.

Project coverage is 88.48%. Comparing base (fb85b0b) to head (b5c0fd4).
Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
src/profile-logic/tracks.js 81.66% 11 Missing ⚠️
src/components/app/ProfileFilterNavigator.js 76.00% 5 Missing and 1 partial ⚠️
src/actions/receive-profile.js 93.93% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5093      +/-   ##
==========================================
- Coverage   88.48%   88.48%   -0.01%     
==========================================
  Files         304      305       +1     
  Lines       27523    27655     +132     
  Branches     7444     7500      +56     
==========================================
+ Hits        24355    24470     +115     
- Misses       2943     2957      +14     
- Partials      225      228       +3     

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

Comment thread locales/en-US/app.ftl Outdated

@julienw julienw 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.

Thanks, this is exciting work!

My main comments are:

  • the selector should look more like a button, for affordance reasons. It should react on hover especially. But there are some reusable classes that you can look at.
  • I think that the algorithms to generate the list of tracks could be made better, unless you have some more things coming. Especially you're usually filtering after generating arrays of everything, while we could avoid adding them in the first place. I guess this comment is wrong if you intend to cache the result of adding all tracks though, so please tell me what your intent is.
  • what's the story for Android where one content process is shared between several tab ids? Or do we expect that Fission will be enabled soon? (Or maybe it already is and I just don't know.
  • (as a follow-up) I find the behavior that we can't hide the list when clicking on the button again very disturbing, do you think we can make this happen? If use a state to keep the information of whether the menu is displayed, we could maybe decide whether to showMenu or hideMenu, and also add some aria and styles to the button. Which makes me think that this "button + menu" behavior could possibly be extracted to another component.
  • a miriad of small things !

Thanks!

for (let i = 0; i < thread.frameTable.length; i++) {
const innerWindowID = thread.frameTable.innerWindowID[i];
if (innerWindowID === null) {
if (innerWindowID === null || innerWindowID === 0) {

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.

(idea for future optimization) if we used 0 always instead of null, maybe we could use a TypedArray for innerWindowID

Comment thread src/components/shared/TabSelectorMenu.css Outdated
.react-contextmenu-item.tabSelectorMenuItem.checked:not(
.react-contextmenu-item--disabled
)::before {
/* Move the checkmark to the left instead of right, as it's logically better. */

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 wonder if we should evolve the context menu lib instead of this hack :-)

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.

I'm not so sure about it. It's indeed better to update the context menu, but since it's also published in npm, we need to create a major release just in case someone else is using. Updating the context menu, bumping the version, releasing it, then updating the version on the profiler is a lot more work :) I would prefer to do that as a follow-up.

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.

Note that it would be a minor version probably, not a major, because it wouldn't break anything. I'm fine with a follow-up.

Comment thread src/components/shared/TabSelectorMenu.js Outdated
Comment thread src/test/components/TabSelectorMenu.test.js Outdated
Comment thread src/profile-logic/tracks.js Outdated
Comment on lines +564 to +570
// At the end, we need to filter global tracks by current tab.
return filterGlobalTracksByTab(
globalTracks,
profile,
tabID,
tabToThreadIndexesMap
);

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 find that the way the function idsdone is a bit strange

  1. Add all global tracks to an array
  2. Sort them
  3. Like an afterthought, remove all the ones that shouldn't be there

I think it would be possible to not add them in the first place, in step 1.
The small algorithm change might be where we have The main thread index was found, add it.. And also deal with the case of tabToThreadIndexesMap or the retrieved threadIndexes being unavailable but I'm sure you can deal with that.

I believe the logic could be simpler. At least I'd like to see how this could look.

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.

This was intentional due to how we construct the global tracks array. If you look at above, you'll see that there are 2 places where we construct the global process track: 1, 2

The first place is fine, it's a place where we have the mainThreadIndex. But if you look at the second place, we don't really know the mainThreadIndex of that process yet. So the branch where you pointed comes to play. We continue iterating and if we find the global track, we are adding the main thread index there. But the issue is, we already have that global track inside the globalTracks array. So what we need to do is to splice the array and remove that element after we already added it.

So this fact that we have to remove for some cases anyways, seemed more strange to me and wanted to make it a different operation. But you are right about sorting. I think we can do the sorting at the end instead of doing this at the end. So I changed it with this, but let me know what you think.

Comment on lines +1065 to +1081
// Next, filter the tracks by the tab selector threads.
scores = scores.filter(({ threadIndex }) => allTrackThreads.has(threadIndex));

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.

Instead of filtering as an afterthought, would it be possible to use the global and local tracks information directly and generate the scores only for those? Unless we'd like to cache the scores for a faster switch (if the scoring computation is slow, I don't know).

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.

Yes, it's also intentional for caching. I will follow-up with caching in the following PRs. Because we need to iterate the threads every time ew need to calculate the scores so they are not great right now.

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'm not super convinced but OK.

Comment thread src/reducers/app.js Outdated
expect(getTabFilter(getState())).toBe(null);

// Change the tab filter by clicking on the menu item.
const mozillaTab = getByText('mozilla.org');

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.

once you'll move to a button, you can use screen.getByRole('button', { name: 'mozilla.org' }), this also checks for the accessibility of the element (does it has the right role and the right name?). Same below.
If that doesn't work it means there's an accessibility problem (but sometimes it's not super easy to debug, so feel free to use getByText if you have issues and can't figure them out)

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.

These are the items inside the context menu, which were added with MenuItem component. This component uses div instead of buttons still (but has some aria attributes). We need to change the libarary to implement this. Let's make it a followup.

Comment thread src/components/shared/TabSelectorMenu.css

@canova canova left a comment

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.

Thanks for the reviews. I updated the code and added some comments. Let me know what you think!
I changed the span into a button but it behaves slightly differently due to some limitations of how we use it. See my message below.

Edit: I left the older commits as is to make them easier to review. Last 6 commits are new.

.react-contextmenu-item.tabSelectorMenuItem.checked:not(
.react-contextmenu-item--disabled
)::before {
/* Move the checkmark to the left instead of right, as it's logically better. */

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.

I'm not so sure about it. It's indeed better to update the context menu, but since it's also published in npm, we need to create a major release just in case someone else is using. Updating the context menu, bumping the version, releasing it, then updating the version on the profiler is a lot more work :) I would prefer to do that as a follow-up.


// TODO: Remove this once we ship the tab selector and remove the active tab view.
const filterPageDataForActiveTab = getProfileFilterPageData(state);
const pageDataByTabID = getProfileFilterPageDataByTabID(state);

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.

See the second item in the PR message. And see the deploy preview PR with the correct names. My next PR is fixing that.

const localTracksByPid = new Map();

// Create a new set of avaiable pids, so we can filter out the local tracks
// if their globalTracks are also filtered out by the tab selector.

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.

I initially didn't add this change but it was failing to render without throwing any errors during the timeline component rendering. I think we iterate over these localTracksByPid and try to find their global tracks out of it. If they don't exist, it throws an error. (This was the error we hunted down together during the perftools work week :))

Comment thread src/components/app/ProfileFilterNavigator.css
Comment thread src/components/app/ProfileFilterNavigator.js Outdated
tabID: TabID | null,
tabToThreadIndexesMap: Map<ThreadIndex, Set<TabID>>
): GlobalTrack[] {
if (tabID === null) {

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.

As far as I know it's not possible to get a zero tabID from the backend, it should be always non-zero. But you're right, the get operation will always return nothing.

tabFilter !== null ? pageDataByTabID.get(tabFilter) : null;

firstItem = (
<span

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.

I initially made it a span because there could be a button that wraps it and didn't want to create nested buttons as they are not valid. For example when you commit a range, it will create a button around that first item so you can navigate back. I changed the code to create a button, only when there is no committed range.

expect(getTabFilter(getState())).toBe(null);

// Change the tab filter by clicking on the menu item.
const mozillaTab = getByText('mozilla.org');

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.

These are the items inside the context menu, which were added with MenuItem component. This component uses div instead of buttons still (but has some aria attributes). We need to change the libarary to implement this. Let's make it a followup.

Comment thread src/profile-logic/tracks.js Outdated
Comment on lines +564 to +570
// At the end, we need to filter global tracks by current tab.
return filterGlobalTracksByTab(
globalTracks,
profile,
tabID,
tabToThreadIndexesMap
);

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.

This was intentional due to how we construct the global tracks array. If you look at above, you'll see that there are 2 places where we construct the global process track: 1, 2

The first place is fine, it's a place where we have the mainThreadIndex. But if you look at the second place, we don't really know the mainThreadIndex of that process yet. So the branch where you pointed comes to play. We continue iterating and if we find the global track, we are adding the main thread index there. But the issue is, we already have that global track inside the globalTracks array. So what we need to do is to splice the array and remove that element after we already added it.

So this fact that we have to remove for some cases anyways, seemed more strange to me and wanted to make it a different operation. But you are right about sorting. I think we can do the sorting at the end instead of doing this at the end. So I changed it with this, but let me know what you think.

Comment on lines +1065 to +1081
// Next, filter the tracks by the tab selector threads.
scores = scores.filter(({ threadIndex }) => allTrackThreads.has(threadIndex));

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.

Yes, it's also intentional for caching. I will follow-up with caching in the following PRs. Because we need to iterate the threads every time ew need to calculate the scores so they are not great right now.

@canova

canova commented Aug 30, 2024

Copy link
Copy Markdown
Member Author

Also answering some of the questions you had in the main review comment:

the selector should look more like a button, for affordance reasons. It should react on hover especially. But there are some reusable classes that you can look at.

That's fixed now.

what's the story for Android where one content process is shared between several tab ids? Or do we expect that Fission will be enabled soon? (Or maybe it already is and I just don't know.

Currently, we are going from tabs (from pages array) -> tracks. Not from tracks -> tabs. Because of this direction, it doesn't matter if a thread has multiple origins in it or not. It will always show all the tabs that we have in the profile and filter the tracks accordingly. Let's say that we have Web Content #1 track, and it has both mozilla.org and cnn.com origins. Since the pages array has both of them, we will properly show 2 tabs in the selector list. And selecting both of these items will result in showing Web Content #1 track.

(as a follow-up) I find the behavior that we can't hide the list when clicking on the button again very disturbing, do you think we can make this happen? If use a state to keep the information of whether the menu is displayed, we could maybe decide whether to showMenu or hideMenu, and also add some aria and styles to the button. Which makes me think that this "button + menu" behavior could possibly be extracted to another component.

Yeah we can do that as a follow-up. I didn't do it yet since it was similar on other places, but they are annoying there as well.

@canova canova requested a review from julienw August 30, 2024 13:35
@julienw

julienw commented Sep 3, 2024

Copy link
Copy Markdown
Contributor

what's the story for Android where one content process is shared between several tab ids? Or do we expect that Fission will be enabled soon? (Or maybe it already is and I just don't know.

Currently, we are going from tabs (from pages array) -> tracks. Not from tracks -> tabs. Because of this direction, it doesn't matter if a thread has multiple origins in it or not. It will always show all the tabs that we have in the profile and filter the tracks accordingly. Let's say that we have Web Content #1 track, and it has both mozilla.org and cnn.com origins. Since the pages array has both of them, we will properly show 2 tabs in the selector list. And selecting both of these items will result in showing Web Content #1 track.

If the user selects cnn.com, will they see the data for both cnn.com and mozilla.org origins?

@julienw julienw 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.

thanks!
let's move forward with that
I only have a few comments but you can merge after that!

Comment thread src/components/shared/TabSelectorMenu.css Outdated
Comment thread src/components/app/ProfileFilterNavigator.js
Comment thread src/profile-logic/tracks.js Outdated
Comment thread src/profile-logic/tracks.js Outdated
return new Set(profile.meta.initialVisibleThreads);
}

const allTrackThreads = new Set();

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.

optional super nit: if we're keeping it now, I'd move the computation of allTrackThreads to a separate function, so that this function is more readable (and easily memoizable too).

Comment on lines +1065 to +1081
// Next, filter the tracks by the tab selector threads.
scores = scores.filter(({ threadIndex }) => allTrackThreads.has(threadIndex));

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'm not super convinced but OK.

Comment thread src/components/app/ProfileFilterNavigator.js
const localTracksByPid = new Map();

// Create a new set of avaiable pids, so we can filter out the local tracks
// if their globalTracks are also filtered out by the tab selector.

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 see... I guess this makes this Map more consistent. I would have liked that we don't have to regenerate every object but rather hide or "skip over" some elements instead. But I think this works for now.

Comment thread src/components/app/ProfileFilterNavigator.css

@canova canova left a comment

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.

Thanks for the review. I addressed your comments and disabled the tab selector for now until we land the other PRs. I'll land this soon.

Comment thread src/components/app/ProfileFilterNavigator.js
const localTracksByPid = new Map();

// Create a new set of avaiable pids, so we can filter out the local tracks
// if their globalTracks are also filtered out by the tab selector.

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, we can look at implementing it that way later.

@canova canova merged commit 7562508 into firefox-devtools:main Sep 18, 2024
@julienw julienw mentioned this pull request Sep 23, 2024
julienw added a commit that referenced this pull request Sep 23, 2024
[Julien Wajsberg] Two optimizations for the marker chart (#5121)
[Nazım Can Altınova] [Tab selector 5] Add a tab selector component and implement tab switching (#5093)
[Julien Wajsberg] Support profiling from the toolbox in Thunderbird Release (#5135)
[Richard Fine] Add a dedicated symbolication tool (#5123)
[Julien Wajsberg] Export a tool to extract gecko logs from a profile (#4973)

Shout-out to our localizers:
de: Michael Köhler
el: Jim Spentzos
en-CA: chutten
en-GB: Ian Neal
es-CL: ravmn
fr: Théo Chevalier
fy-NL: Fjoerfoks
ia: Melo46
it: Francesco Lodolo [:flod]
nl: Mark Heijl
pt-BR: Marcelo Ghelman
ru: Valery Ledovskoy
sv-SE: Andreas Pettersson
uk: Lobodzets
zh-CN: Olvcpr423
zh-TW: Pin-guang Chen
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