Подмена стиля приложения заново полирует каждый живой виджет и блокировала GUI-поток примерно на 630 мс при каждом старте — это и был StartupQtScrollStyle в логах. Нужна она только там, где скроллбары исчезающие, а штатные Windows и Fusion и без подмены отвечают SH_ScrollBar_Transient = 0. Теперь стиль сначала спрашивают и заменяют, только когда это меняет поведение. Заодно две метрики перестали вводить в заблуждение: - StartupQtScrollStyle пишет, была подмена или пропуск; - StartupSecondarySidebarReady делит wall-clock и собственную работу. Группы ставятся по одной через QTimer, поэтому 1464 мс в логе были ожиданием в очереди событий GUI, занятой стартом, а не построением сайдбара: оптимизировать там нечего, и метрика больше не притворяется, что есть. Наблюдатель отзывчивости получил режим охоты за рывками. Штатный порог в 2 секунды ловит «программа не отвечает», но рывки интерфейса живут в десятках миллисекунд и до сих пор не оставляли следов. ZAPRET_UI_JITTER_MS=60 переводит наблюдателя на короткие задержки кадра со стеком GUI-потока в обычном логе; порог файловых дампов остался прежним, иначе каждый рывок писал бы отчёт в папку крэш-логов. Тесты: test_scrollbar_style_skip — пропуск подмены, установка прокси при исчезающих скроллбарах, работа без стиля; test_ui_jitter_watchdog_mode — разбор переменной, параметры режима, формат сообщений в миллисекундах, неизменность штатного режима. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
64 lines
2 KiB
Python
64 lines
2 KiB
Python
from __future__ import annotations
|
||
|
||
import unittest
|
||
from unittest.mock import Mock
|
||
|
||
from PyQt6.QtWidgets import QApplication, QProxyStyle, QStyle, QStyleFactory
|
||
|
||
from main.qt_runtime import _install_non_transient_scrollbars_style
|
||
|
||
|
||
class _TransientScrollbarStyle(QProxyStyle):
|
||
"""Стиль с исчезающими скроллбарами — как на macOS."""
|
||
|
||
def styleHint(self, hint, option=None, widget=None, returnData=None): # noqa: N802 - Qt API
|
||
if hint == QStyle.StyleHint.SH_ScrollBar_Transient:
|
||
return 1
|
||
return super().styleHint(hint, option, widget, returnData)
|
||
|
||
|
||
class _FakeApp:
|
||
def __init__(self, style) -> None:
|
||
self._style = style
|
||
self.setStyle = Mock()
|
||
|
||
def style(self):
|
||
return self._style
|
||
|
||
|
||
class ScrollbarStyleSkipTests(unittest.TestCase):
|
||
"""Подмена QStyle заново полирует все виджеты — делать её вхолостую нельзя."""
|
||
|
||
@classmethod
|
||
def setUpClass(cls) -> None:
|
||
cls._app = QApplication.instance() or QApplication([])
|
||
|
||
def test_permanent_scrollbars_skip_style_replacement(self) -> None:
|
||
app = _FakeApp(QStyleFactory.create("Fusion"))
|
||
|
||
replaced = _install_non_transient_scrollbars_style(app)
|
||
|
||
self.assertFalse(replaced)
|
||
app.setStyle.assert_not_called()
|
||
|
||
def test_transient_scrollbars_get_the_proxy_style(self) -> None:
|
||
app = _FakeApp(_TransientScrollbarStyle(QStyleFactory.create("Fusion")))
|
||
|
||
replaced = _install_non_transient_scrollbars_style(app)
|
||
|
||
self.assertTrue(replaced)
|
||
app.setStyle.assert_called_once()
|
||
proxy = app.setStyle.call_args.args[0]
|
||
self.assertEqual(proxy.styleHint(QStyle.StyleHint.SH_ScrollBar_Transient), 0)
|
||
|
||
def test_missing_style_still_installs_proxy(self) -> None:
|
||
app = _FakeApp(None)
|
||
|
||
replaced = _install_non_transient_scrollbars_style(app)
|
||
|
||
self.assertTrue(replaced)
|
||
app.setStyle.assert_called_once()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|