Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Do not create a new <webview> element or <Frame /> component for each Tab (single-webview) #13783

Merged
merged 62 commits into from
May 11, 2018

Conversation

petemill
Copy link
Member

@petemill petemill commented Apr 9, 2018

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 /> Component

This 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 a componentDidUpdate 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 the WebContents 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 to rendererTabEvents.js and rendererShortcutHandler.js with the exception of items that relied on internal component instance state, including prevProps. These should be especially tested:

  • When navigating to a different origin, expire temporary site-settings for widevine, flash and noscript - 17c1814
  • When changing full-screen via in-content control, keyboard shortcut (esc), or switching tab, ensure that content area and window go in and out of full screen
  • Whenever frame state for a tab was changed after 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 IPC

  • This action communicates to a window that it has a new frame, and adds it that window's frams state. Moved from store action to IPC because:
    • Events are received much more immediately by the renderer now due to:
      • Tabs load immediately, not just when webview is attached
      • Event handlers are not only setup when component is created, there is a single event handler for all window tab events
    • These events must have the frame created in state
    • We can't simply listen for 'tab-inserted-at' as we need the frame data
    • We can't simply pass it in on window creation with shared memory as window creation is not at all the only way a frame can be added to a window (e.g. moving tabs to existing window, using buffer window)

Creates (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 and did-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 and tab-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:

  • Not changing the state.tabs Immutable.List when deleting a tab (setting to null instead)
  • Not forwarding frame objects for each tab back to every window in appState updates, since the window is where the frame state came from in the first place
  • Setting the New Tab theme color immediately, rather than waiting for it to be computed from DOM content

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

  • Each event from each Tab in the window
  • A console group for each attempt to attach a Tab to a <webview> in WebviewDisplay, 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 <webview>, or the reason why it is not):
image

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:

  • Submitted a ticket for my issue if one did not already exist.
  • Used Github auto-closing keywords in the commit message.
  • Added/updated tests for this change (for new code or code which already has tests).
  • Ran git rebase -i to squash commits (if needed).
  • Tagged reviewers and labelled the pull request as needed.
  • Request a security/privacy review as needed. (Ask a Brave employee to help if you cannot access this document.)

Test Plan:

  • Full pass
  • Tests have been ongoing with the beta channel for a few weeks

Reviewer Checklist:

  • Request a security/privacy review as needed if one was not already requested.

Tests

  • Adequate test coverage exists to prevent regressions
  • Tests should be independent and work correctly when run individually or as a suite ref
  • New files have MPL2 license header

@petemill petemill added this to the 0.22.x Release 3 milestone Apr 9, 2018
@petemill petemill self-assigned this Apr 9, 2018
@@ -736,31 +744,23 @@ class Main extends React.Component {
: null
}
</div>
<div className='mainContainer'>
<TransitionGroup className={cx({
Copy link
Member

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?

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 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?

Copy link
Member

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

if (shouldDebug) {
console.debug('%cNo frame for event yet, queueing until later', 'color: red', tabId, eventType)
}
windowStore.on(`new-frame-${tabId}`, (newFrame) => {
Copy link
Member

Choose a reason for hiding this comment

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

should be .once(

@petemill petemill force-pushed the single-webview branch 3 times, most recently from 71f2f78 to 7c03b6f Compare April 10, 2018 19:38
@@ -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')
Copy link
Contributor

Choose a reason for hiding this comment

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

Not needed?

Copy link
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 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.


// Components
const ReduxComponent = require('../reduxComponent')
// comment out to not use pooled webview (2x webviews):
Copy link
Member

@bsclifton bsclifton Apr 11, 2018

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

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

) => {
const tabId = webContents.getId()
if (shouldDebugTabEvents) {
console.log(`Tab [${tabId}] window-confirm`)
Copy link
Member

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)
Copy link
Member

@bsclifton bsclifton Apr 11, 2018

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?)

Copy link
Member

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()) {
Copy link
Member

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

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 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?


// compare frames when state changes debounced, and only when thread is idle
const onWindowStoreChanged = debounce(() => {
idleCallbackId = idleCallbackId || window.requestIdleCallback(relayFrameChanges, { timeout: 1000 })
Copy link
Member

@bsclifton bsclifton Apr 11, 2018

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 }))

const frame = frames[frameKey]
const lastFrame = lastFrames[frameKey]
if (!lastFrame || !lastFrame.equals(frame)) {
changedFrames.push(Immutable.Map().set('frame', frame))
Copy link
Member

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)
Copy link
Member

@bsclifton bsclifton Apr 11, 2018

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)
Copy link
Member

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

case appConstants.APP_TAB_SET_FULL_SCREEN: {
const isFullscreen = action.get('isFullScreen')
const tabId = action.get('tabId')
if (isFullscreen === true || isFullscreen === false) {
Copy link
Contributor

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?

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 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')
Copy link
Member

Choose a reason for hiding this comment

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

extra console.log

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'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)
Copy link
Member

Choose a reason for hiding this comment

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

extra console.log?

Copy link
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 spot. I actually haven't done a pass of cleanup or lint fixing as we're still debugging a couple display issues.

// 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')
Copy link
Member

Choose a reason for hiding this comment

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

extra console.log?

bsclifton and others added 20 commits May 10, 2018 21:34
…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
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.
Copy link
Member

@bsclifton bsclifton left a 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! 😄

@petemill petemill merged commit 73886ff into master May 11, 2018
petemill added a commit that referenced this pull request May 11, 2018
Do not create a new <webview> element or <Frame /> component for each Tab (single-webview)
petemill added a commit that referenced this pull request May 11, 2018
Do not create a new <webview> element or <Frame /> component for each Tab (single-webview)
@petemill
Copy link
Member Author

0.23.x b088714
0.22.x-release3 / 0.22.x 2371c55

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants