Merge 37e10da4fa into 7e3df43463
This commit is contained in:
commit
39b5b46a8d
|
|
@ -293,6 +293,9 @@
|
|||
|<<prompt.radius,prompt.radius>>|Rounding radius (in pixels) for the edges of prompts.
|
||||
|<<qt.args,qt.args>>|Additional arguments to pass to Qt, without leading `--`.
|
||||
|<<qt.chromium.experimental_web_platform_features,qt.chromium.experimental_web_platform_features>>|Enables Web Platform features that are in development.
|
||||
|<<qt.chromium.lifecycle_state.discard_delay,qt.chromium.lifecycle_state.discard_delay>>|The amount of time (in milliseconds) to wait before transitioning a page to the discarded lifecycle state. This state is an extreme resource-saving state, where the browsing context of the web view is discarded and the renderer process is shut down. Resource usage is therefore reduced to near-zero. The web page is automatically reloaded when needed.
|
||||
|<<qt.chromium.lifecycle_state.enabled,qt.chromium.lifecycle_state.enabled>>|Use recommended page lifecycle state.
|
||||
|<<qt.chromium.lifecycle_state.freeze_delay,qt.chromium.lifecycle_state.freeze_delay>>|The amount of time (in milliseconds) to wait before transitioning a page to the frozen lifecycle state. This is a low-CPU state, where most DOM event processing, JavaScript execution, and other tasks are suspended.
|
||||
|<<qt.chromium.low_end_device_mode,qt.chromium.low_end_device_mode>>|When to use Chromium's low-end device mode.
|
||||
|<<qt.chromium.process_model,qt.chromium.process_model>>|Which Chromium process model to use.
|
||||
|<<qt.chromium.sandboxing,qt.chromium.sandboxing>>|What sandboxing mechanisms in Chromium to use.
|
||||
|
|
@ -3845,6 +3848,42 @@ Valid values:
|
|||
|
||||
Default: +pass:[auto]+
|
||||
|
||||
[[qt.chromium.lifecycle_state.discard_delay]]
|
||||
=== qt.chromium.lifecycle_state.discard_delay
|
||||
The amount of time (in milliseconds) to wait before transitioning a page to the discarded lifecycle state. This state is an extreme resource-saving state, where the browsing context of the web view is discarded and the renderer process is shut down. Resource usage is therefore reduced to near-zero. The web page is automatically reloaded when needed.
|
||||
Set to -1 to disable this state.
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
Type: <<types,Int>>
|
||||
|
||||
Default: +pass:[-1]+
|
||||
|
||||
[[qt.chromium.lifecycle_state.enabled]]
|
||||
=== qt.chromium.lifecycle_state.enabled
|
||||
Use recommended page lifecycle state.
|
||||
This puts webpages into one of three lifecycle states: active, frozen, or discarded. Using the recommended lifecycle state lets the browser use less resources by freezing or discarding web views when it's safe to do so.
|
||||
This results in significant battery life savings.
|
||||
Ongoing page activity is taken into account when determining the recommended lifecycle state, as to not disrupt your browsing.
|
||||
This feature is only available on QtWebEngine 6.5+. On older versions this setting is ignored.
|
||||
See the Qt documentation for more details: https://doc.qt.io/qt-6/qtwebengine-features.html#page-lifecycle-api
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[qt.chromium.lifecycle_state.freeze_delay]]
|
||||
=== qt.chromium.lifecycle_state.freeze_delay
|
||||
The amount of time (in milliseconds) to wait before transitioning a page to the frozen lifecycle state. This is a low-CPU state, where most DOM event processing, JavaScript execution, and other tasks are suspended.
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
Type: <<types,Int>>
|
||||
|
||||
Default: +pass:[30000]+
|
||||
|
||||
[[qt.chromium.low_end_device_mode]]
|
||||
=== qt.chromium.low_end_device_mode
|
||||
When to use Chromium's low-end device mode.
|
||||
|
|
|
|||
|
|
@ -1316,6 +1316,14 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||
self._child_event_filter = None
|
||||
self._saved_zoom = None
|
||||
self._scripts.init()
|
||||
|
||||
self._lifecycle_timer_freeze = usertypes.Timer(self)
|
||||
self._lifecycle_timer_freeze.setSingleShot(True)
|
||||
self._lifecycle_timer_freeze.timeout.connect(functools.partial(self._set_lifecycle_state, QWebEnginePage.LifecycleState.Frozen))
|
||||
self._lifecycle_timer_discard = usertypes.Timer(self)
|
||||
self._lifecycle_timer_discard.setSingleShot(True)
|
||||
self._lifecycle_timer_discard.timeout.connect(functools.partial(self._set_lifecycle_state, QWebEnginePage.LifecycleState.Discarded))
|
||||
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
|
||||
self._needs_qtbug65223_workaround = (
|
||||
version.qtwebengine_versions().webengine < utils.VersionNumber(5, 15, 5))
|
||||
|
|
@ -1424,6 +1432,14 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||
# percent encoded content is 2 megabytes minus 30 bytes.
|
||||
self._widget.setHtml(html, base_url)
|
||||
|
||||
def _set_lifecycle_state(
|
||||
self,
|
||||
new_state: QWebEnginePage.LifecycleState,
|
||||
) -> None:
|
||||
"""Set the lifecycle state of the current tab."""
|
||||
log.webview.debug(f"Setting page lifecycle state of {self} to {new_state}")
|
||||
self._widget.page().setLifecycleState(new_state)
|
||||
|
||||
def _show_error_page(self, url, error):
|
||||
"""Show an error page in the tab."""
|
||||
log.misc.debug("Showing error page for {}".format(error))
|
||||
|
|
@ -1724,6 +1740,62 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||
else:
|
||||
selection.selectNone()
|
||||
|
||||
def _schedule_lifecycle_transition(
|
||||
self,
|
||||
state: Optional[QWebEnginePage.LifecycleState] = None,
|
||||
) -> None:
|
||||
"""Schedule, or cancel, a page lifecycle transition.
|
||||
|
||||
Schedule a lifecycle transition to `state`, according to the user's
|
||||
config.
|
||||
If a transition into `state` is already schedule, do nothing.
|
||||
If `state` is `None`, cancel any scheduled transition.
|
||||
"""
|
||||
timers = {
|
||||
QWebEnginePage.LifecycleState.Frozen: (
|
||||
self._lifecycle_timer_freeze,
|
||||
config.val.qt.chromium.lifecycle_state.freeze_delay,
|
||||
),
|
||||
QWebEnginePage.LifecycleState.Discarded: (
|
||||
self._lifecycle_timer_discard,
|
||||
config.val.qt.chromium.lifecycle_state.discard_delay,
|
||||
),
|
||||
}
|
||||
|
||||
to_start = delay = None
|
||||
if state is not None:
|
||||
try:
|
||||
to_start, delay = timers[state]
|
||||
except KeyError:
|
||||
raise utils.Unreachable(state)
|
||||
|
||||
for timer, _ in timers.values():
|
||||
if timer != to_start:
|
||||
timer.stop()
|
||||
|
||||
if to_start and not to_start.isActive() and delay != -1:
|
||||
log.webview.debug(f"Scheduling recommended lifecycle change {delay=} {state=} tab={self}")
|
||||
to_start.start(delay)
|
||||
|
||||
@pyqtSlot(QWebEnginePage.LifecycleState)
|
||||
def _on_recommended_state_changed(
|
||||
self,
|
||||
recommended_state: QWebEnginePage.LifecycleState,
|
||||
) -> None:
|
||||
if self._widget.page().lifecycleState() == recommended_state:
|
||||
self._schedule_lifecycle_transition(None)
|
||||
return
|
||||
|
||||
disabled = not config.val.qt.chromium.lifecycle_state.enabled
|
||||
|
||||
if recommended_state == QWebEnginePage.LifecycleState.Active:
|
||||
self._schedule_lifecycle_transition(None)
|
||||
self._set_lifecycle_state(recommended_state)
|
||||
elif disabled or self.data.pinned:
|
||||
self._schedule_lifecycle_transition(None)
|
||||
else:
|
||||
self._schedule_lifecycle_transition(recommended_state)
|
||||
|
||||
def _connect_signals(self):
|
||||
view = self._widget
|
||||
page = view.page()
|
||||
|
|
@ -1742,6 +1814,9 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||
page.printRequested.connect(self._on_print_requested)
|
||||
page.selectClientCertificate.connect(self._on_select_client_certificate)
|
||||
|
||||
if version.qtwebengine_versions().webengine >= utils.VersionNumber(6, 5):
|
||||
page.recommendedStateChanged.connect(self._on_recommended_state_changed)
|
||||
|
||||
view.titleChanged.connect(self.title_changed)
|
||||
view.urlChanged.connect(self._on_url_changed)
|
||||
view.renderProcessTerminated.connect(
|
||||
|
|
|
|||
|
|
@ -345,6 +345,59 @@ qt.chromium.experimental_web_platform_features:
|
|||
Chromium. By default, this is enabled with Qt 5 to maximize compatibility
|
||||
despite an aging Chromium base.
|
||||
|
||||
qt.chromium.lifecycle_state.enabled:
|
||||
type: Bool
|
||||
default: true
|
||||
backend: QtWebEngine
|
||||
# yamllint disable rule:line-length
|
||||
desc: >-
|
||||
Use recommended page lifecycle state.
|
||||
|
||||
This puts webpages into one of three lifecycle states: active, frozen, or
|
||||
discarded. Using the recommended lifecycle state lets the browser use less
|
||||
resources by freezing or discarding web views when it's safe to do so.
|
||||
|
||||
This results in significant battery life savings.
|
||||
|
||||
Ongoing page activity is taken into account when determining the
|
||||
recommended lifecycle state, as to not disrupt your browsing.
|
||||
|
||||
This feature is only available on QtWebEngine 6.5+. On older versions
|
||||
this setting is ignored.
|
||||
|
||||
See the Qt documentation for more details: https://doc.qt.io/qt-6/qtwebengine-features.html#page-lifecycle-api
|
||||
# yamllint enable rule:line-length
|
||||
|
||||
qt.chromium.lifecycle_state.freeze_delay:
|
||||
type:
|
||||
name: Int
|
||||
minval: 0
|
||||
maxval: maxint
|
||||
default: 30_000
|
||||
backend: QtWebEngine
|
||||
desc: >-
|
||||
The amount of time (in milliseconds) to wait before transitioning a page to
|
||||
the frozen lifecycle state.
|
||||
This is a low-CPU state, where most DOM event processing, JavaScript
|
||||
execution, and other tasks are suspended.
|
||||
|
||||
qt.chromium.lifecycle_state.discard_delay:
|
||||
type:
|
||||
name: Int
|
||||
minval: -1
|
||||
maxval: maxint
|
||||
default: -1
|
||||
backend: QtWebEngine
|
||||
desc: >-
|
||||
The amount of time (in milliseconds) to wait before transitioning a page to
|
||||
the discarded lifecycle state.
|
||||
This state is an extreme resource-saving state, where the browsing context
|
||||
of the web view is discarded and the renderer process is shut down.
|
||||
Resource usage is therefore reduced to near-zero. The web page is
|
||||
automatically reloaded when needed.
|
||||
|
||||
Set to -1 to disable this state.
|
||||
|
||||
qt.highdpi:
|
||||
type: Bool
|
||||
default: false
|
||||
|
|
|
|||
|
|
@ -1989,3 +1989,14 @@ Feature: Tab management
|
|||
And I open data/numbers/2.txt in a new tab
|
||||
And I run :tab-prev
|
||||
Then "Entering mode KeyMode.insert (reason: mode_override)" should be logged
|
||||
|
||||
@qt>=6.5
|
||||
Scenario: Lifecycle change on tab switch
|
||||
When I set qt.chromium.lifecycle_state.enabled to true
|
||||
And I set qt.chromium.lifecycle_state.freeze_delay to 0
|
||||
And I set qt.chromium.lifecycle_state.discard_delay to 0
|
||||
And I open about:blank?1
|
||||
And I open about:blank?2 in a new tab
|
||||
And I run :tab-prev
|
||||
Then "Setting page lifecycle state of <qutebrowser.browser.webengine.webenginetab.WebEngineTab tab_id=* url='about:blank?2'> to LifecycleState.Frozen" should be logged
|
||||
And "Setting page lifecycle state of <qutebrowser.browser.webengine.webenginetab.WebEngineTab tab_id=* url='about:blank?2'> to LifecycleState.Discarded" should be logged
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@ QWebEngineScriptCollection = QtWebEngineCore.QWebEngineScriptCollection
|
|||
QWebEngineScript = QtWebEngineCore.QWebEngineScript
|
||||
|
||||
from qutebrowser.browser import greasemonkey
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.utils import usertypes, utils, version
|
||||
webenginetab = pytest.importorskip(
|
||||
"qutebrowser.browser.webengine.webenginetab")
|
||||
|
||||
pytestmark = pytest.mark.usefixtures('greasemonkey_manager')
|
||||
|
||||
versions = version.qtwebengine_versions(avoid_init=True)
|
||||
|
||||
|
||||
class ScriptsHelper:
|
||||
|
||||
|
|
@ -244,3 +246,163 @@ class TestWebEnginePermissions:
|
|||
pytest.skip("enum member not available")
|
||||
assert clipboard in permissions_cls._options
|
||||
assert clipboard in permissions_cls._messages
|
||||
|
||||
|
||||
class TestPageLifecycle:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_version(self):
|
||||
# While the lifecycle feature was introduced in 5.14, PyQt seems to
|
||||
# have trouble connecting to the signal we require on 6.4 and prior.
|
||||
# https://github.com/qutebrowser/qutebrowser/pull/8547#issuecomment-2890997662
|
||||
if versions.webengine < utils.VersionNumber(6, 5):
|
||||
pytest.skip("Lifecycle feature requires Webengine 6.5+")
|
||||
|
||||
@pytest.fixture
|
||||
def set_state_mock(
|
||||
self,
|
||||
webengine_tab: webenginetab.WebEngineTab,
|
||||
monkeypatch,
|
||||
mocker,
|
||||
):
|
||||
set_state_mock = mocker.Mock()
|
||||
monkeypatch.setattr(
|
||||
webengine_tab._widget.page(),
|
||||
"setLifecycleState",
|
||||
set_state_mock,
|
||||
)
|
||||
return set_state_mock
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def set_config_defaults(
|
||||
self,
|
||||
config_stub,
|
||||
set_state_mock,
|
||||
):
|
||||
self.set_config(config_stub)
|
||||
|
||||
def set_config(
|
||||
self,
|
||||
config_stub,
|
||||
freeze_delay=0,
|
||||
discard_delay=0,
|
||||
enabled=True,
|
||||
):
|
||||
config_stub.val.qt.chromium.lifecycle_state.freeze_delay = freeze_delay
|
||||
config_stub.val.qt.chromium.lifecycle_state.discard_delay = discard_delay
|
||||
config_stub.val.qt.chromium.lifecycle_state.enabled = enabled
|
||||
|
||||
def timer_for(self, tab, state): # pylint: disable=inconsistent-return-statements
|
||||
if state == QWebEnginePage.LifecycleState.Frozen:
|
||||
return tab._lifecycle_timer_freeze
|
||||
elif state == QWebEnginePage.LifecycleState.Discarded:
|
||||
return tab._lifecycle_timer_discard
|
||||
else:
|
||||
pytest.fail(f"Unknown lifecycle state `{state}`")
|
||||
|
||||
def test_qt_method_is_called(
|
||||
self,
|
||||
webengine_tab: webenginetab.WebEngineTab,
|
||||
set_state_mock,
|
||||
qtbot,
|
||||
):
|
||||
"""Basic test to show that we call QT after going through our code."""
|
||||
state = QWebEnginePage.LifecycleState.Discarded
|
||||
webengine_tab._on_recommended_state_changed(state)
|
||||
with qtbot.wait_signal(self.timer_for(webengine_tab, state).timeout):
|
||||
pass
|
||||
set_state_mock.assert_called_once_with(QWebEnginePage.LifecycleState.Discarded)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"new_state, freeze_delay, discard_delay",
|
||||
[
|
||||
(QWebEnginePage.LifecycleState.Discarded, 2000, 10,),
|
||||
(QWebEnginePage.LifecycleState.Frozen, 10, 2000,),
|
||||
]
|
||||
)
|
||||
def test_per_state_delay(
|
||||
self,
|
||||
webengine_tab: webenginetab.WebEngineTab,
|
||||
monkeypatch,
|
||||
mocker,
|
||||
set_state_mock,
|
||||
config_stub,
|
||||
qtbot,
|
||||
new_state,
|
||||
freeze_delay,
|
||||
discard_delay,
|
||||
):
|
||||
"""Show that a different time delay can get set for each state."""
|
||||
self.set_config(
|
||||
config_stub,
|
||||
freeze_delay=freeze_delay,
|
||||
discard_delay=discard_delay,
|
||||
)
|
||||
|
||||
webengine_tab._on_recommended_state_changed(new_state)
|
||||
|
||||
timer = self.timer_for(webengine_tab, new_state)
|
||||
assert timer.remainingTime() == (
|
||||
freeze_delay
|
||||
if new_state == QWebEnginePage.LifecycleState.Frozen
|
||||
else discard_delay
|
||||
)
|
||||
|
||||
with qtbot.wait_signal(timer.timeout, timeout=100):
|
||||
pass
|
||||
set_state_mock.assert_called_once_with(new_state)
|
||||
|
||||
def test_state_disabled(
|
||||
self,
|
||||
webengine_tab: webenginetab.WebEngineTab,
|
||||
monkeypatch,
|
||||
config_stub,
|
||||
):
|
||||
"""For negative delay values, the timer shouldn't be scheduled."""
|
||||
self.set_config(
|
||||
config_stub,
|
||||
discard_delay=-1,
|
||||
)
|
||||
state = QWebEnginePage.LifecycleState.Discarded
|
||||
webengine_tab._on_recommended_state_changed(state)
|
||||
timer = self.timer_for(webengine_tab, state)
|
||||
assert not timer.isActive()
|
||||
|
||||
def test_pinned_tabs_untouched(
|
||||
self,
|
||||
webengine_tab: webenginetab.WebEngineTab,
|
||||
monkeypatch,
|
||||
config_stub,
|
||||
):
|
||||
"""Don't change lifecycle state for a pinned tab."""
|
||||
webengine_tab.set_pinned(True)
|
||||
state = QWebEnginePage.LifecycleState.Frozen
|
||||
webengine_tab._on_recommended_state_changed(state)
|
||||
timer = self.timer_for(webengine_tab, state)
|
||||
assert not timer.isActive()
|
||||
|
||||
def test_timer_interrupted(
|
||||
self,
|
||||
webengine_tab: webenginetab.WebEngineTab,
|
||||
set_state_mock,
|
||||
config_stub,
|
||||
qtbot,
|
||||
):
|
||||
"""Pending time should be cancelled when a new signal comes in."""
|
||||
self.set_config(
|
||||
config_stub,
|
||||
freeze_delay=1,
|
||||
discard_delay=3,
|
||||
)
|
||||
freeze_timer = webengine_tab._lifecycle_timer_freeze
|
||||
discard_timer = webengine_tab._lifecycle_timer_discard
|
||||
|
||||
webengine_tab._on_recommended_state_changed(QWebEnginePage.LifecycleState.Frozen)
|
||||
assert freeze_timer.remainingTime() == 1
|
||||
|
||||
webengine_tab._on_recommended_state_changed(QWebEnginePage.LifecycleState.Discarded)
|
||||
assert discard_timer.remainingTime() == 3
|
||||
|
||||
with qtbot.wait_signal(discard_timer.timeout):
|
||||
pass
|
||||
set_state_mock.assert_called_once_with(QWebEnginePage.LifecycleState.Discarded)
|
||||
|
|
|
|||
Loading…
Reference in New Issue