Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrieve the page favicons from the browser and display it on the tab selector #5166

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

canova
Copy link
Member

@canova canova commented Oct 16, 2024

This requires changes from Bug 1921778 since it adds the webchannel changes in the backend.

This PR adds the ability to retrieve the favicons for pages as a data url, so we can display them easily in the frontend without needing to fetch them from their url every time we open a profile.

This also changes how we use the Icon component and how we compute the css class names for them. Let me know what you think!

Note that this doesn't change the other icons that we have in places like call tree. I would prefer to do it as a followup as there are some questions to answer there.

Deploy preview with the page data retrieved already

Copy link
Contributor

@julienw julienw 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 patch!
I haven't tried it, but I trust you that you tested it.

I think this should be improved now, because it impacts the web channel protocol:

  1. make the web channel return binary data instead of the data url. I left these comments in the phabricator patch as well. See below for more specific comments about that.

(+ some nits)

Possibly later, but my preference is that it should be done now:

  1. I think the icons from pages don't need to go through all the machinery in actions/icons. This machinery was made so that we wouldn't try to display the favicons that are inexistant, which was unavoidable because we were trying to guess a URL. Without this machinery, some browsers would display the "broken image" icon instead of a blank image.

So I think you could have 2 different paths for these 2 types of icons: the ones where we have a data-url, and the ones where we have a normal URL. We know that the data-url works, so we don't need to check for it.

By having these 2 different paths, you could also batch adding all the icons from pages to the state in just one operation, avoiding the cost of regenerating the map for each icon (like mentioned below).

This way you could also avoid the sha1 operation and instead use a counter like mentioned below.

Tell me what you think!

Please add new commits on top of these ones and do not change the current ones (except for rebase if necessary of cousre).

@@ -1017,6 +1022,36 @@ export async function doSymbolicateProfile(
dispatch(doneSymbolicating());
}

export async function retrievePageFaviconsFromBrowser(
dispatch: Dispatch,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: instead of passing dispatch as a parameter, I'd make this a thunk action, so that dispatch is injected by the thunk action middleware.

Then in the caller, you'd do await dispatch(retrievePageFaviconsFromBrowser(...))

I think it's better because it makes it clearer in the caller that this will ultimately dispatch an action.
Currently it feels like await retrievePageFaviconsFromBrowser ought to return something.

Copy link
Member Author

Choose a reason for hiding this comment

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

I wanted to do similarly to what we already do for doSymbolicateProfile above:

export async function doSymbolicateProfile(
dispatch: Dispatch,
profile: Profile,
symbolStore: SymbolStore
) {
dispatch(startSymbolicating());
const completionPromises = [];
await symbolicateProfile(
profile,
symbolStore,
(
threadIndex: ThreadIndex,
symbolicationStepInfo: SymbolicationStepInfo
) => {
completionPromises.push(
new Promise((resolve) => {
_symbolicationStepQueueSingleton.enqueueSingleSymbolicationStep(
dispatch,
threadIndex,
symbolicationStepInfo,
resolve
);
})
);
}
);
await Promise.all(completionPromises);
dispatch(doneSymbolicating());
}

Both of them are called right after each other. So I would prefer to keep their implementation similar to keep them uniform in the function where we call them:

await doSymbolicateProfile(dispatch, profile, symbolStore);
}
}
if (browserConnection && pages && pages.length > 0) {
await retrievePageFaviconsFromBrowser(dispatch, pages, browserConnection);

Copy link
Contributor

Choose a reason for hiding this comment

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

mmm I see (and I was the one who implemented doSymbolicateProfile initially so I can't blame anyone else :-) )

The difference I see though, is that doSymbolicateProfile starts the symbolication proces which can take long, while retrievePageFaviconsFromBrowser is a more direct process. But I admit the difference is thin.

I guess seeing that we pass dispatch is good enough to know that actions may be dispatched.

Copy link
Contributor

Choose a reason for hiding this comment

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

This makes me think that we might want to run doSymbolicateProfile and retrievePageFaviconsFromBrowser in parallel. Or at least do retrievePageFaviconsFromBrowser first, because it's likely much faster than the symbolication, and it may happen that users share their profile before the symbolication ends.

src/actions/receive-profile.js Outdated Show resolved Hide resolved
Comment on lines 1036 to 1037
// It appears that an error occurred since the pages and favicons arrays
// have different lengths. Return early without doing anything.
Copy link
Contributor

Choose a reason for hiding this comment

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

if that's not normal, it would be good to output a warning or an error to the console so that it's not silently swallowed.

Copy link
Member Author

Choose a reason for hiding this comment

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

This could also happen in the case where the backend doesn't support the new webchannel request yet and we fallback to an empty array in the webchannel logic. I would prefer to not pollute the console for now. But we might want to add for later.

src/reducers/profile-view.js Outdated Show resolved Hide resolved
src/reducers/icons.js Outdated Show resolved Hide resolved
src/profile-logic/profile-data.js Outdated Show resolved Hide resolved
src/test/components/TabSelectorMenu.test.js Outdated Show resolved Hide resolved
Comment on lines 75 to 79
async function _classNameFromUrl(url): Promise<string> {
return url.startsWith('data:image/')
? 'dataUrl' + (await sha1(url))
: url.replace(/[/:.+>< ~()#,]/g, '_');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

(optional)
The goal of this function was to provide always the same class name for the same URL, so that we wouldn't try to insert a lot of identical CSS classes. The other goal was an idempotent operation, so that calling it with the same parameter in 2 different places would generate the same result.

But now that you have this Map, we have a way to ensure this unicity without this trick.

So I wonder if we could do a simple counter with a common prefix instead of a possibly expensive sha1 (expensive depending on the size of the input).

return `favicon-${++faviconCounter}`

What do you think?

The problem with a static counter is for testing, you need to provide a function to reset it for the tests only. Also it might need to change more than just this because of how the class is used currently (but I'm not sure).

(not optional) IMO you can do the sha1 operation for both data urls and and normal urls. Also as a comment you could check for data: only (but you don't need this check anyway).

Copy link
Contributor

Choose a reason for hiding this comment

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

Also I believe you moved how the classname was generated here just becase of the fact you made it async. But I don't mind, I feel like it's better to generate the classnames once rather than regenerating them always. And it makes things easier with the counter.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm yeah, you are right about the favicon class name. We don't really need to sha1 the whole url and just can have a counter. I changed it to that one, thanks!

Comment on lines 1041 to 1045
for (let index = 0; index < favicons.length; index++) {
newPages[index] = {
...newPages[index],
favicon: favicons[index],
};
Copy link
Contributor

Choose a reason for hiding this comment

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

(optional)

Suggested change
for (let index = 0; index < favicons.length; index++) {
newPages[index] = {
...newPages[index],
favicon: favicons[index],
};
for (let index = 0; index < favicons.length; index++) {
if (favicons[index]){
newPages[index] = {
...newPages[index],
favicon: favicons[index],
};
}

so that we don't add unneeded content to the profile.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you follow my suggestion from https://phabricator.services.mozilla.com/D225197, that is the WebChannel should return the binary data + the mimetype, then here is the location where you'll generate base64 data using the FIleReader.

I was also wondering it we should store the data url, or a pair (base64 + mimetype), in the profile JSON. But I think the data url makes sense as it contains both these information.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah it think it's easier/simpler to keep a single data url string that contains the both information.

src/selectors/icons.js Outdated Show resolved Hide resolved
Copy link
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 @julienw! You can find the new commits after the [DO NOT LAND] Enable the tab selector commit.

I changed the frontend to handle the binary data and convert that into the data url once we get them. Also batched the icon/classname logic so we don't have to update the map one by one.

Let me know what you think!

@@ -1017,6 +1022,36 @@ export async function doSymbolicateProfile(
dispatch(doneSymbolicating());
}

export async function retrievePageFaviconsFromBrowser(
dispatch: Dispatch,
Copy link
Member Author

Choose a reason for hiding this comment

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

I wanted to do similarly to what we already do for doSymbolicateProfile above:

export async function doSymbolicateProfile(
dispatch: Dispatch,
profile: Profile,
symbolStore: SymbolStore
) {
dispatch(startSymbolicating());
const completionPromises = [];
await symbolicateProfile(
profile,
symbolStore,
(
threadIndex: ThreadIndex,
symbolicationStepInfo: SymbolicationStepInfo
) => {
completionPromises.push(
new Promise((resolve) => {
_symbolicationStepQueueSingleton.enqueueSingleSymbolicationStep(
dispatch,
threadIndex,
symbolicationStepInfo,
resolve
);
})
);
}
);
await Promise.all(completionPromises);
dispatch(doneSymbolicating());
}

Both of them are called right after each other. So I would prefer to keep their implementation similar to keep them uniform in the function where we call them:

await doSymbolicateProfile(dispatch, profile, symbolStore);
}
}
if (browserConnection && pages && pages.length > 0) {
await retrievePageFaviconsFromBrowser(dispatch, pages, browserConnection);

Comment on lines 1036 to 1037
// It appears that an error occurred since the pages and favicons arrays
// have different lengths. Return early without doing anything.
Copy link
Member Author

Choose a reason for hiding this comment

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

This could also happen in the case where the backend doesn't support the new webchannel request yet and we fallback to an empty array in the webchannel logic. I would prefer to not pollute the console for now. But we might want to add for later.

@@ -71,6 +76,7 @@ class TabSelectorMenuImpl extends React.PureComponent<Props> {
'aria-checked': tabFilter === tabID ? 'false' : 'true',
}}
>
{hasSomeIcons ? <Icon iconUrl={pageData.favicon} /> : null}
Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I would like to have a placeholder as well. I can work on it as a followup.

Do you have any ideas for possible icons that we can put it here?
Maybe "Globe" for the websites: https://icons.design.firefox.com/viewer/#globe
and "Extensions" icon for the webextensions: https://icons.design.firefox.com/viewer/#extensions
What do you think?

src/profile-logic/profile-data.js Outdated Show resolved Hide resolved
src/types/state.js Outdated Show resolved Hide resolved
src/reducers/icons.js Outdated Show resolved Hide resolved
src/reducers/icons.js Outdated Show resolved Hide resolved
src/test/components/TabSelectorMenu.test.js Outdated Show resolved Hide resolved
src/test/store/icons.test.js Outdated Show resolved Hide resolved
Comment on lines 1041 to 1045
for (let index = 0; index < favicons.length; index++) {
newPages[index] = {
...newPages[index],
favicon: favicons[index],
};
Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah it think it's easier/simpler to keep a single data url string that contains the both information.

@canova canova requested a review from julienw October 22, 2024 09:03
Copy link

codecov bot commented Oct 22, 2024

Codecov Report

Attention: Patch coverage is 47.05882% with 45 lines in your changes missing coverage. Please review.

Project coverage is 88.45%. Comparing base (1170042) to head (269d3ff).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/actions/icons.js 50.00% 10 Missing and 1 partial ⚠️
src/actions/receive-profile.js 42.10% 9 Missing and 2 partials ⚠️
src/utils/base64.js 0.00% 10 Missing ⚠️
src/reducers/icons.js 44.44% 5 Missing ⚠️
src/reducers/profile-view.js 0.00% 4 Missing and 1 partial ⚠️
src/app-logic/web-channel.js 0.00% 2 Missing ⚠️
src/app-logic/browser-connection.js 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5166      +/-   ##
==========================================
- Coverage   88.56%   88.45%   -0.11%     
==========================================
  Files         307      308       +1     
  Lines       27936    27997      +61     
  Branches     7560     7572      +12     
==========================================
+ Hits        24741    24766      +25     
- Misses       2978     3013      +35     
- Partials      217      218       +1     

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

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.

2 participants