-
Notifications
You must be signed in to change notification settings - Fork 971
Do not create a new <webview> element or <Frame /> component for each Tab (single-webview) #13783
Conversation
@@ -736,31 +744,23 @@ class Main extends React.Component { | |||
: null | |||
} | |||
</div> | |||
<div className='mainContainer'> | |||
<TransitionGroup className={cx({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is gone now, can we remove the package.json reference to react-transition-group
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just about to do this, but then realised that this may make the merge a bit more of a pain, especially package-lock.json, across branches. Perhaps a separate PR unless you think that it won't be a pain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separate PR is great 😄 👍 Let's leave it alone
app/renderer/rendererTabEvents.js
Outdated
if (shouldDebug) { | ||
console.debug('%cNo frame for event yet, queueing until later', 'color: red', tabId, eventType) | ||
} | ||
windowStore.on(`new-frame-${tabId}`, (newFrame) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be .once(
71f2f78
to
7c03b6f
Compare
@@ -24,6 +24,7 @@ const BrowserWindow = electron.BrowserWindow | |||
const firstDefinedValue = require('../../../js/lib/functional').firstDefinedValue | |||
const settings = require('../../../js/constants/settings') | |||
const getSetting = require('../../../js/settings').getSetting | |||
const { shouldDebugStoreActions, shouldDebugTabEvents } = require('../../cmdLine') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the note @jumde! I haven't satisfied linter and some tests yet. Some of this code was written in an experimental mode with things added, removed, or changed very quickly. But right now we're pretty locked down on architecture and are in clean-up mode, so that's what I'm looking in to over the course of the next day or 2.
7c03b6f
to
03801fa
Compare
|
||
// Components | ||
const ReduxComponent = require('../reduxComponent') | ||
// comment out to not use pooled webview (2x webviews): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If time allows, it would be much better to check for an environment variable instead of having the notes about commenting in here. Something like:
let WebviewDisplay
if (process.env.USE_POOLED_WEBVIEW === false) {
WebviewDisplay = require('../../webviewDisplay')
} else {
WebviewDisplay = require('../../pooledWebviewDisplay')
}
Info could then be added to https://github.com/brave/browser-laptop/wiki/Command-line-flags
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not end up doing this as we don't have access to process.env in the front-end, and whilst I could transfer such a property from browser to environment variable, I felt that this is not something (right now) someone other than a developer looking at the source code needs to experiment with, yet.... It seems like with improvements on C66, the need for the pooledWebview
is less, and possibly we can replace with the simpler webviewDisplay
in the next release, after I have more time to test properly. The purpose of pooledWebview
is to avoid a flash of blankness (usually white) between attaches of different tabs, which does not seem as noticeable with C66 in initial testing. However, I did not feel comfortable making the permanent switch as that would require another couple rounds of testing to reach the same level of confidence we have with pooledWebview
app/browser/tabMessageBox.js
Outdated
) => { | ||
const tabId = webContents.getId() | ||
if (shouldDebugTabEvents) { | ||
console.log(`Tab [${tabId}] window-confirm`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be window-prompt
function expireContentSettings (state, tabId, origin) { | ||
// Expired Flash settings should be deleted when the webview is | ||
// navigated or closed. Same for NoScript's allow-once option. | ||
const tabValue = tabState.getByTabId(state, tabId) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this new functionality (seems like it)? Do you have a suggested way to test this specifically? (should an issue be created to capture this?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's old functionality. you can enable flash in prefs, then go to https://www.onlinemictest.com/webcam-test-in-adobe-flash/, right click on the flash element, click 'allow once', then close the tab and re-visit it. flash should be re-blocked.
case appConstants.APP_FRAME_CHANGED: | ||
state = tabState.updateFrame(state, action, shouldDebugTabEvents) | ||
case appConstants.APP_FRAMES_CHANGED: | ||
for (const frameAction of action.get('frames').valueSeq()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might be good to check if frames
is truthy (ex: action.has('frames')
) before using
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This action does not get called without that property. Should we have a policy of trusting our actions in the way that they are currently being called, or optimize for possible future mistakes?
app/renderer/rendererFrameTracker.js
Outdated
|
||
// compare frames when state changes debounced, and only when thread is idle | ||
const onWindowStoreChanged = debounce(() => { | ||
idleCallbackId = idleCallbackId || window.requestIdleCallback(relayFrameChanges, { timeout: 1000 }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you put parens around this to make the logic obvious?
Without them (maybe because it's such a long line), it looks like the OR may only run if the assignment is falsey (OR has higher precedence of course).
ex:
idleCallbackId = (idleCallbackId || window.requestIdleCallback(relayFrameChanges, { timeout: 1000 }))
app/renderer/rendererFrameTracker.js
Outdated
const frame = frames[frameKey] | ||
const lastFrame = lastFrames[frameKey] | ||
if (!lastFrame || !lastFrame.equals(frame)) { | ||
changedFrames.push(Immutable.Map().set('frame', frame)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to initialize this in one go; such as:
changedFrames.push(Immutable.Map({frame}))
} | ||
|
||
function handleShortcut (frameKey, shortcut, e, args) { | ||
console.log('got shortcut', shortcut, frameKey) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this console.log need to be removed / commented out / wrapped with debug?
} | ||
case 'stop': { | ||
const tabId = frameStateUtil.getTabIdByFrameKey(windowStore.state, frameKey) | ||
console.log('getting webcontets', tabId) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this (and below) - more console.logs possibly needing removal
777bf6b
to
48f6d5c
Compare
case appConstants.APP_TAB_SET_FULL_SCREEN: { | ||
const isFullscreen = action.get('isFullScreen') | ||
const tabId = action.get('tabId') | ||
if (isFullscreen === true || isFullscreen === false) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the tab is already FullScreen
, do we need to set it to full screen again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code was moved in 23f5b71. At this point, I believe that the tab content area is not fullscreen, but other UX may be (e.g. the window). Agreed that if that's the case, perhaps isFullscreen
is not the best variable name here, although it's more of a state description - "This tab is in fullscreen mode or not, so tell the tab's content to go fullscreen too, or not)".
} | ||
|
||
createWebview () { | ||
console.log('creating a webview') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extra console.log
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left these console.logs in as this is an experimental class at the moment, which we may switch to in an upcoming release.
} | ||
|
||
attachActiveTab (tabId) { | ||
console.log('webviewDisplay: attaching tab id', tabId) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extra console.log?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the spot. I actually haven't done a pass of cleanup or lint fixing as we're still debugging a couple display issues.
app/browser/windows.js
Outdated
// TODO(petemill): race condition if multiple different tabs are moved at the same time | ||
// ...tab-strip-empty may fire before all of those tabs are inserted to new window | ||
win.webContents.once('detached-tab-new-window', () => { | ||
console.log('departing tab made it to new window') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extra console.log?
effaee5
to
5db1435
Compare
…emove other rendering workarounds Fix #13798 since "display: none" and then "display: ''" of a webview will cause the guest to (asynchronously) detach and attach again, and by that time we had attached another guest, which causes the original tab to be destroyed since we are attaching a guest to a webview which already has a guest. Luckily, it doesn't look like this workaround is needed in order to paint the first attachGuest of a webview anymore. Remove all composition workarounds from c65 as they are no longer needed for c66
Not pinned tab Fix #13797
guest-ready could be sent to the original window whilst the frame object has already been sent to the browser / new window without the `guestIsReady` property. Instead of moving this to the browser process, I've removed the event handling entirely as it does not seem like we need to wait for guest-ready anymore (we weren't for attaching the Tab for preview mode anyway...).
Ensure 'tab-inserted-at' message is not missed when frame is mid-transit between windows. Ideally we would just use the property set on tab state, and not rely on frame state, but that would require renderer components reading from frame and tab state objects, instead of only frame objects, which would make those components' state selection functions more expensive.
Fix #14046 Avoid a tab being moved to a new window with a frame index higher than it should have. This occurred because when dropping on the blank area of a tab strip, there is no tab being dragged over. The last tab being dragged over could have been in another window, or could be the dragged tab itself, causing a frame to be created in the new window with an index that is too high, corrupting the "frame index <-> tabId" cache. Instead, we insert the tab at the beginning, and rely on muon to provide an updated tab index (if provided with an index of -1, muon will add the tab to the end of the strip).
Fix #14045 Previously this was set on window state, but was not used, possibly because it was not reliable as it did not get the initial value which is set internally in muon based on origin. Instead, when this was working, it was read directly from the webview, which we try to avoid due to the webview's multi-purpose nature. For example, the webview may not be displaying the active tab - it may be displaying a preview, or waiting to display the active tab asynchronously. Since there is not yet a muon Tab event for 'zoom-changed', we must manually update the zoomPercent tabState property every time we knowingly change the zoom level.
Fix #14044 Avoids race-condition where tab does not have window host webcontents in order to IPC messages to store properties on the frame. Note that not all error page data handling was moved. It would be best to move all load complete / fail handling from renderer to browser in one go. However, certificate errors are not provided on the WebContents, but provided as an app event, so this can be moved by itself more easily. Test Plan: - Test certificate error page has data, and the actions (ignore error and show certifiacte) work correctly when occuring in existing or background tab. - Test other load errors (e.g. bad domain or no network) display correctly when opened in existing or background tab.
Fix #14064 with a band-aid. A proper fix would be to insure that the correct frame / tab index is used when removing a tab from a tab strip and adding it to a new one.
…e is now reliable Fix #14080 Reintroduce `tabs.updateTabsStateForWindow` which should be called whenever we believe muon has new tab properties that we wouldn't have otherwise received events for. It seems that the only time this can happen anymore is when the index of tabs has changed because another tab, with a lower index, was moved or closed, so renamed the function `updateTabIndexesForWindow`. Also allows us to remove the call to dispatch the action `UPDATE_TAB` for each tab with modified properties, which was a performance issue. Instead, since we're only concerned with index change, we make sure that this is handled in the renderer on the appropriate events (tab inserted, removed, deleted, moved, pinned (which triggers moved)). Adds performance measuring for that function when using --debug-tab-events flag.
Fix #14081 Avoids having to send large diffs covering every property of every tab with an index higher than the one being removed. The browser diff and window patch operations are expensive.
…rge often-changing tabs[].frame.x properties These frame state objects are sent from the window to the browser, and whilst this could be looked at being removed as all the data should already be accessible on the tab state, they do not need to be sent back to the renderer, where they originated. The performance impact of this is measurable when there are a lot of frames being modified very quickly, such as app startup, tab removal, tab deletion or anything that should change many frame properties at once (such as index). Also do not add on 'bookmarked' property since I cannot find an instance of it being used. Remove double-setting of appStoreRenderer.state. Whilst this does have a check to make sure the argument is different each time it is called, it is not necessary to call it twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had already reviewed before- been involved in recent testing and have looked at the code while @petemill has been fixing bugs. Great work on this! 😄
Do not create a new <webview> element or <Frame /> component for each Tab (single-webview)
Do not create a new <webview> element or <Frame /> component for each Tab (single-webview)
Fix #13216 of which the overall goal is to move towarrds making sure that performance of a renderer window is not affected by the number of tabs that are associated with that window.
Removes
<Frame />
ComponentThis PR not only removes the
<webview>
element for each frame, the problems of which are addressed in the attached issue, but also removes its React wrapper: the<Frame />
component.The main issue with this component for performance is that for each tab in a window, a new instance of that component was being created. Then, each time the store dispatched an action, the Frame's
mergeProps
function would run. Even though this is a very quick function, this could add up to blocking for ~10% of the window's JS thread time, when there are > 150 tabs. In addition, the component had acomponentDidUpdate
function which would then dispatch further store actions based on the `mergeProps result, which would cause every Frame (and every other React component in the window) to perform this cycle again.Previously, this component listened to events on its associated Tab's WebContents via routing through the
<webview>
. Instead of keeping this model, creating a new React element for each tab, and providing direct access to theWebContents
event handler, we create a single event handler function instance that is called for all events on any tab in a window. Ideally these event handlers and dispatched actions should be performed by the browser process, but a change of that scope is too large to be done at once, especially with the already-large changes required by removing the multiple webviews.Most event handler code is transported as-is from
frame.js
torendererTabEvents.js
andrendererShortcutHandler.js
with the exception of items that relied on internal component instance state, includingprevProps
. These should be especially tested:componentDidUpdate
, the frame would dispatch an action to the browser each time, for each frame. This has been moved to a single-purpose function -rendererFrameTracker.js
Converted
app-new-web-contents-added
action to IPCCreates
(Pooled)WebviewDisplay
This is a vanilla JS class which controls the lifecycle of the
<webview>
, including attaching and detaching to different tabs. This is controlled by a React Component:<GuestInstanceRenderer />
which is a direct child of<Main />
.This class contains all the workarounds for quirks of the
<webview />
including issues with not displaying content of an attached tab until the painting has been forced by hiding and showing, and handling fragility in timing when it comes to attaching to destroyed WebContents.In order to prevent a 'white flash' between the detach of a previous tab and the attach of the next tab to display, we actually maintain a pool of 2
<webview>
elements. We first attach the incoming Tab to an unused<webview>
, make sure it is displaying, before we hide and detach the previously displayed tab from its<webview>
. To this component, switching to a Tab, or viewing it as a preview is the same thing, it only cares about which tab should be currently displayed.Should an incoing request to view a different Tab occur whilst the
WebviewDisplay
is still attaching to a new tab, it will either queue up the next Tab to be displayed after the current attachment is complete, or cancel the current attachment and begin attaching the newly-requested Tab instead, depending on the status of the attachment process.Handles tab window attachment differently
Relying on
did-attach
anddid-detach
had to be changed, since these events are about attachment to a webview. Previously, the only time a tab was detached from a webview was when moving window, and the tab would always get a new webview in another window, even if it was a background tab. Now, every time we switch active tab one tab is detached from the webview and another is attached. We made new events in muon that specifically inform about being removed from or added to windows:tab-detached-at
andtab-inserted-at
, as well as a new muon api to tell a tab to move to a different window.Other major muon changes were on the topic of ensuring that the tab content was fetched and loaded even when never having been attached - previously the tab would just be created but not actually do anything until it was made active.
Other optimizations
Plus a few more changes with the same goal of reducing performance impact of many tabs, windows, or items in state, such as:
There's definitely more to be done in this area, especially with the amount of events locking up the browser and window threads on app startup with even a medium-sized profile (#14082), but I had to draw the line somewhere.
Logging
New flag
--debug-store-actions
now outputs each action at the time of dispatch in both the browser console, and each window console (with timing info and fancy colors).Using the flag
--debug-tab-events
will now show specialized console output in the renderer windows (in addition to the previous log output in the main browser terminal / console) which will show<webview>
inWebviewDisplay
, including timings for each step.A new Debug menu option "Display Tab Identifiers" will now display: Tab Id, Index, Guest InstanceId, FrameKey and FrameIndex on each Tab, as well as the the Tab which is currently being displayed (or is in the process of attaching to a
![image](https://user-images.githubusercontent.com/741836/38519050-658c2642-3bf3-11e8-86f0-c14b4fb7ef42.png)
<webview>
, or the reason why it is not):Misc
Addded support for
BRAVE_SHOW_FIRST_RUN_WELCOME=1
env variable in order to debug issues with the 'about:welcome' tab which is created on a blank profile (but disabled by default in dev environment).Submitter Checklist:
git rebase -i
to squash commits (if needed).Test Plan:
Reviewer Checklist:
Tests