zapret-kvn/xray_fluent/ui/page_warmup.py
loop-uh 5965af36c5
Some checks failed
Windows project source guards / test (push) Failing after 26s
Возврат окна из трея: хватка памяти растёт вместе с программой, страницы строятся заранее; в журнал — причины для зеркал
Сегодняшние правки Zapret 2 GUI по журналу пользователя перенесены в KVN. 1) Пока окно скрыто, раз в минуту проверяется рабочий набор, и при росте от 16 МБ хватка и снимок страниц берутся заново (запуск в трей брал их на пятой секунде, пока программа ещё не закончила стартовые работы). 2) Страницы окна (DeferredPage) строились только при первом открытии, то есть под щелчком; ui/page_warmup.py собирает страницы узлов, подписок, настроек, обновлений и журнала заранее, но только пока окно скрыто или свёрнуто, через 2 минуты после запуска и когда человек минуту не трогал мышь и клавиатуру: по одной с паузой 3 с (решение пользователя 2026-10-20 для GUI, цена — память страниц в трее). 3) Общее ядро обновления (копия из Zapret 2 GUI) пишет в журнал, откуда получен список имён git-сервера и почему не открылось ни одно имя.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 05:55:59 -04:00

133 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Страницы строятся заранее, пока окно скрыто в трее и человек за компьютером ничего не делает.
Страницы окна (``DeferredPage``) строятся только при первом открытии. После запуска в
трей это значит: первое открытие окна строит всё, что человек нажмёт, прямо под щелчком,
в потоке окна, а код, который ни разу не выполнялся, никакое удержание памяти не
сохранит (в журнале Zapret 2 GUI 21.1.7.183 первый показ после трея — 5,6 с замирания).
Прогрев идёт тихо: не раньше чем через ``AFTER_S`` после запуска, только когда окно
скрыто или свёрнуто и человек не трогал мышь и клавиатуру ``IDLE_MS``, по одной странице
с паузой ``GAP_MS``. Окно на экране прогрев не трогает: страницы тогда строит сам человек
своими щелчками. Цена — память нескольких страниц, пока программа сидит в трее
(решение пользователя 2026-10-20 для Zapret 2 GUI, здесь так же).
"""
from __future__ import annotations
import ctypes
import logging
import sys
import time
from collections.abc import Callable, Sequence
from ctypes import wintypes
from PyQt6.QtCore import QObject, QTimer
LOG = logging.getLogger("xray_fluent.page_warmup")
# Не раньше чем через столько после запуска.
AFTER_S = 120.0
# Сколько человек должен не трогать мышь и клавиатуру.
IDLE_MS = 60_000
# Пауза между страницами.
GAP_MS = 3_000
# Как часто проверять, не пора ли.
POLL_MS = 5_000
class _LastInputInfo(ctypes.Structure):
_fields_ = [("cbSize", wintypes.UINT), ("dwTime", wintypes.DWORD)]
def user_idle_ms() -> int | None:
"""Сколько миллисекунд человек не трогал мышь и клавиатуру; не Windows или сбой — None."""
if sys.platform != "win32":
return None
try:
info = _LastInputInfo()
info.cbSize = ctypes.sizeof(info)
user32 = ctypes.WinDLL("user32", use_last_error=True)
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
kernel32.GetTickCount.restype = wintypes.DWORD
if not user32.GetLastInputInfo(ctypes.byref(info)):
return None
return int((kernel32.GetTickCount() - info.dwTime) & 0xFFFFFFFF)
except Exception:
return None
class HiddenPageWarmup(QObject):
"""Строит отложенные страницы по одной, пока окно скрыто и человек отошёл."""
def __init__(
self,
window,
pages: Callable[[], Sequence[object]],
*,
idle_ms: Callable[[], int | None] = user_idle_ms,
clock: Callable[[], float] = time.monotonic,
after_s: float = AFTER_S,
required_idle_ms: int = IDLE_MS,
gap_ms: int = GAP_MS,
poll_ms: int = POLL_MS,
) -> None:
super().__init__(window)
self._window = window
self._pages = pages
self._idle_ms = idle_ms
self._clock = clock
self._started_at = clock()
self._after_s = float(after_s)
self._required_idle_ms = int(required_idle_ms)
self._gap_ms = int(gap_ms)
self._poll_ms = int(poll_ms)
self._finished = False
self._timer = QTimer(self)
self._timer.setSingleShot(True)
self._timer.timeout.connect(self._on_timer)
self._timer.start(self._poll_ms)
@property
def finished(self) -> bool:
return self._finished
def _window_on_screen(self) -> bool:
window = self._window
try:
return bool(window.isVisible()) and not bool(window.isMinimized())
except Exception:
return True
def _pending(self) -> list[object]:
return [page for page in self._pages() if getattr(page, "_page", object()) is None]
def _may_build_now(self) -> bool:
if self._clock() - self._started_at < self._after_s:
return False
if self._window_on_screen():
return False
try:
idle = self._idle_ms()
except Exception:
idle = None
return idle is not None and int(idle) >= self._required_idle_ms
def _on_timer(self) -> None:
pending = self._pending()
if not pending:
self._finished = True
return
delay = self._poll_ms
if self._may_build_now():
started = time.perf_counter()
try:
pending[0].ensure_page()
except Exception as exc: # noqa: BLE001 — прогрев не должен ронять окно
LOG.info("page warmup failed for %r: %s", pending[0], exc)
else:
LOG.info("page warmup: built %s in %.0f ms", getattr(pending[0], "objectName", lambda: "?")(), (time.perf_counter() - started) * 1000)
delay = max(self._gap_ms, self._poll_ms)
self._timer.start(delay)
__all__ = ["AFTER_S", "GAP_MS", "HiddenPageWarmup", "IDLE_MS", "POLL_MS", "user_idle_ms"]