update: added new qt preloader flow

This commit is contained in:
JOhn 2026-06-27 17:02:05 -04:00
parent 86348ce2e3
commit 28a9787658
7 changed files with 264 additions and 51 deletions

View file

@ -26,8 +26,15 @@ core_logger.propagate = False
from gui.v2.infrastructure.navigator import Navigator
from gui.v2.infrastructure.setup_observers import setup_observers
from gui.v2.infrastructure.connection_manager import ConnectionManager
from gui.v2.infrastructure.page_registry import PRELOAD_ORDER, PRELOAD_SKIP
from gui.v2.infrastructure.page_registry import (
PRELOAD_ORDER,
PRELOAD_SKIP,
ASYNC_PREPARE,
REGULAR_FLOW_ONLY,
PAGE_REGISTRY,
)
from gui.v2.workers.worker_thread import WorkerThread
from gui.v2.workers.page_data_worker import PageDataWorker
from gui.v2.ui.builders.bottom_section import create_bottom_section
from gui.v2.ui.builders.top_section import create_top_section
from gui.v2.ui.builders.system_tray import init_system_tray
@ -48,6 +55,7 @@ from gui.v2.actions.flags import (
has_shown_fast_mode_prompt,
mark_fast_mode_prompt_shown,
set_fast_mode_enabled,
is_fast_registration_enabled,
check_first_launch,
)
from gui.v2.actions.profile_data import (
@ -170,6 +178,10 @@ class CustomWindow(QMainWindow):
self._preload_done = 0
self._preload_failed = 0
self._preload_skipped_runtime = 0
self._preload_dispatched = 0
self._preload_inflight = 0
self._preload_workers = {}
self._preload_summary_emitted = False
self._preload_t_start = 0.0
self._preload_timer = QTimer(self)
self._preload_timer.setSingleShot(True)
@ -202,15 +214,26 @@ class CustomWindow(QMainWindow):
def _start_background_preload(self):
import time
self._preload_queue = [n for n in PRELOAD_ORDER if n not in PRELOAD_SKIP]
fast_mode = is_fast_registration_enabled(self.gui_config_file)
skipped_regular = []
if fast_mode:
skipped_regular = [n for n in self._preload_queue if n in REGULAR_FLOW_ONLY]
self._preload_queue = [n for n in self._preload_queue if n not in REGULAR_FLOW_ONLY]
self._preload_total = len(self._preload_queue)
self._preload_done = 0
self._preload_failed = 0
self._preload_skipped_runtime = 0
self._preload_dispatched = 0
self._preload_inflight = 0
self._preload_summary_emitted = False
self._preload_t_start = time.perf_counter()
print(f"[preload] ============================================================", flush=True)
print(f"[preload] starting background preload: {self._preload_total} pages queued", flush=True)
print(f"[preload] order : {', '.join(self._preload_queue)}", flush=True)
print(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)", flush=True)
if fast_mode:
print(f"[preload] fastmode: ON -> skipping regular-flow pages: "
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True)
print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True)
print(f"[preload] ============================================================", flush=True)
self._preload_timer.start(700)
@ -221,19 +244,73 @@ class CustomWindow(QMainWindow):
self._preload_skipped_runtime += 1
print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True)
if not self._preload_queue:
self._emit_preload_summary()
self._maybe_emit_preload_summary()
return
idx = self._preload_done + self._preload_failed + 1
self._preload_dispatched += 1
idx = self._preload_dispatched
name = self._preload_queue.pop(0)
print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True)
ok = self.navigator.preload(name)
if ok:
self._preload_done += 1
if name in ASYNC_PREPARE:
self._dispatch_async_preload(name)
else:
self._preload_failed += 1
ok = self.navigator.preload(name)
if ok:
self._preload_done += 1
else:
self._preload_failed += 1
if self._preload_queue:
self._preload_timer.start(250)
else:
self._maybe_emit_preload_summary()
def _dispatch_async_preload(self, name):
module_path, class_name = PAGE_REGISTRY[name]
print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True)
worker = PageDataWorker(name, module_path, class_name, self)
worker.data_ready.connect(self._on_preload_data_ready)
worker.failed.connect(self._on_preload_failed)
worker.finished.connect(lambda n=name: self._cleanup_preload_worker(n))
self._preload_workers[name] = worker
self._preload_inflight += 1
worker.start()
def _on_preload_data_ready(self, name, payload):
self._preload_inflight -= 1
if name in self.navigator._instances:
print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True)
else:
try:
page = self.navigator._instantiate(name, prepared=payload)
self.navigator.register_instance(name, page)
self._preload_done += 1
print(f"[preload] {name:30s} OK (async, cached={len(self.navigator._instances)}, "
f"stack_size={self.page_stack.count()})", flush=True)
except Exception as e:
self._preload_failed += 1
print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True)
self._maybe_emit_preload_summary()
def _on_preload_failed(self, name, error):
self._preload_inflight -= 1
print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True)
if name not in self.navigator._instances:
ok = self.navigator.preload(name)
if ok:
self._preload_done += 1
else:
self._preload_failed += 1
self._maybe_emit_preload_summary()
def _cleanup_preload_worker(self, name):
worker = self._preload_workers.pop(name, None)
if worker is not None:
worker.deleteLater()
def _maybe_emit_preload_summary(self):
if self._preload_summary_emitted:
return
if not self._preload_queue and self._preload_inflight == 0:
self._preload_summary_emitted = True
self._emit_preload_summary()
def _emit_preload_summary(self):

View file

@ -35,6 +35,13 @@ def mark_fast_mode_prompt_shown(gui_config_file):
save_gui_config(gui_config_file, config)
def is_fast_registration_enabled(gui_config_file):
config = load_gui_config(gui_config_file)
if not config:
return False
return config.get("registrations", {}).get("fast_registration_enabled", False)
def set_fast_mode_enabled(gui_config_file, enabled):
config = load_gui_config(gui_config_file)
if config is None:

View file

@ -2,7 +2,7 @@ import importlib
import time
import traceback
from gui.v2.infrastructure.page_registry import PAGE_REGISTRY
from gui.v2.infrastructure.page_registry import PAGE_REGISTRY, ASYNC_PREPARE
class Navigator:
@ -11,18 +11,29 @@ class Navigator:
self.custom_window = custom_window
self._instances = {}
def get_class(self, name):
module_path, class_name = PAGE_REGISTRY[name]
module = importlib.import_module(module_path)
return getattr(module, class_name)
def _instantiate(self, name, prepared=None):
cls = self.get_class(name)
if name in ASYNC_PREPARE:
return cls(self.page_stack, self.custom_window, prepared=prepared)
return cls(self.page_stack, self.custom_window)
def register_instance(self, name, page):
self.page_stack.addWidget(page)
self._instances[name] = page
def navigate(self, name):
page = self._instances.get(name)
if page is None:
if name not in PAGE_REGISTRY:
self._warn_not_migrated(name)
return
module_path, class_name = PAGE_REGISTRY[name]
module = importlib.import_module(module_path)
cls = getattr(module, class_name)
page = cls(self.page_stack, self.custom_window)
self.page_stack.addWidget(page)
self._instances[name] = page
page = self._instantiate(name)
self.register_instance(name, page)
self.page_stack.setCurrentIndex(self.page_stack.indexOf(page))
def preload(self, name):
@ -36,11 +47,8 @@ class Navigator:
print(f"[preload] {name:30s} LOAD {module_path}.{class_name}", flush=True)
t0 = time.perf_counter()
try:
module = importlib.import_module(module_path)
cls = getattr(module, class_name)
page = cls(self.page_stack, self.custom_window)
self.page_stack.addWidget(page)
self._instances[name] = page
page = self._instantiate(name)
self.register_instance(name, page)
dt_ms = (time.perf_counter() - t0) * 1000.0
print(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, "
f"cached={len(self._instances)}, stack_size={self.page_stack.count()})",

View file

@ -38,6 +38,23 @@ PRELOAD_SKIP = {
"ticket_prep",
}
ASYNC_PREPARE = {
"settings",
}
REGULAR_FLOW_ONLY = {
"protocol",
"location",
"hidetor",
"residential",
"tor",
"connection",
"screen",
"browser",
"wireguard",
"sync_screen",
}
PRELOAD_ORDER = [
"settings",
"editor",

View file

@ -19,6 +19,10 @@ class Page(QWidget):
self.selected_residential = []
self.buttons = []
@staticmethod
def prepare_data(custom_window):
return None
def add_selected_profile(self, profile):
self.selected_profiles.clear()
self.selected_wireguard.clear()

View file

@ -36,6 +36,7 @@ from core.errors.logger import logger as core_logger
from gui.v2.actions.key_interpretation import interpret_key_results
from gui.v2.actions.profile_order import normalize_profile_order
from gui.v2.infrastructure.page_registry import PAGE_REGISTRY
from gui.v2.infrastructure.setup_observers import ticket_observer
from gui.v2.ui.pages.Page import Page
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
@ -46,17 +47,21 @@ from gui.v2.ui.styles.styles import (
)
from gui.v2.ui.widgets.clickable_label import ClickableValueLabel
from gui.v2.ui.widgets.terminal_widget import TerminalWidget
from gui.v2.workers.page_data_worker import PageDataWorker
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
from gui.v2.workers.worker_thread import WorkerThread
class Settings(Page):
def __init__(self, page_stack, main_window, parent=None):
def __init__(self, page_stack, main_window, parent=None, prepared=None):
super().__init__("Settings", page_stack, main_window, parent)
self.font_style = f"font-family: '{self.custom_window.open_sans_family}';"
self.update_status = main_window
self.update_logging = main_window
self._prepared = prepared if prepared is not None else self.prepare_data(main_window)
self._settings_refresh_seq = 0
self._settings_workers = set()
self.button_reverse.setVisible(True)
self.button_reverse.setEnabled(True)
self.button_reverse.clicked.connect(self.reverse)
@ -64,6 +69,52 @@ class Settings(Page):
self.title.setText("Settings")
self.setup_ui()
@staticmethod
def prepare_data(custom_window):
profiles = ProfileController.get_all()
profile_order = normalize_profile_order(
getattr(custom_window, 'gui_config_file', None), profiles.keys())
current_connection = custom_window.get_current_connection()
try:
endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled()
except Exception:
endpoint_verification_enabled = False
return {
"profiles": profiles,
"profile_order": profile_order,
"current_connection": current_connection,
"endpoint_verification_enabled": endpoint_verification_enabled,
"systemwide_enabled": Settings._read_systemwide_enabled(),
"bwrap_enabled": Settings._read_bwrap_enabled(),
"gui_config": custom_window._load_gui_config(),
"ticket_failure": custom_window.get_ticket_verification_failure(),
}
def _prepared_value(self, key, loader):
if self._prepared is not None and key in self._prepared:
return self._prepared[key]
return loader()
@staticmethod
def _read_systemwide_enabled():
try:
privilege_policy = PolicyController.get('privilege')
if privilege_policy is not None:
return PolicyController.is_instated(privilege_policy)
except Exception:
pass
return False
@staticmethod
def _read_bwrap_enabled():
try:
capability_policy = PolicyController.get('capability')
if capability_policy is not None:
return PolicyController.is_instated(capability_policy)
except Exception:
pass
return False
def setup_ui(self):
main_container = QWidget(self)
main_container.setGeometry(0, 0, 800, 520)
@ -198,11 +249,13 @@ class Settings(Page):
return page
def create_delete_profile_buttons(self):
profiles = ProfileController.get_all()
profiles = self._prepared_value("profiles", ProfileController.get_all)
profile_ids = normalize_profile_order(
getattr(self.update_status, 'gui_config_file', None),
profiles.keys())
profile_ids = self._prepared_value(
"profile_order",
lambda: normalize_profile_order(
getattr(self.update_status, 'gui_config_file', None),
profiles.keys()))
for index, profile_id in enumerate(profile_ids):
profile = profiles[profile_id]
@ -600,7 +653,7 @@ class Settings(Page):
def update_debug_profile_list(self):
self.debug_profile_selector.clear()
profiles = ProfileController.get_all()
profiles = self._prepared_value("profiles", ProfileController.get_all)
system_profiles = {pid: profile for pid, profile in profiles.items(
) if isinstance(profile, SystemProfile)}
@ -904,7 +957,7 @@ class Settings(Page):
self.profile_selector = QComboBox()
self.profile_selector.setStyleSheet(self.get_combobox_style())
profiles = ProfileController.get_all()
profiles = self._prepared_value("profiles", ProfileController.get_all)
if profiles:
for profile_id, profile in profiles.items():
self.profile_selector.addItem(
@ -1039,7 +1092,7 @@ class Settings(Page):
self.verification_profile_selector = QComboBox()
self.verification_profile_selector.setStyleSheet(
self.get_combobox_style())
profiles = ProfileController.get_all()
profiles = self._prepared_value("profiles", ProfileController.get_all)
if profiles:
for profile_id, profile in profiles.items():
self.verification_profile_selector.addItem(
@ -1134,7 +1187,9 @@ class Settings(Page):
self.endpoint_verification_checkbox = QCheckBox(page)
self.endpoint_verification_checkbox.setGeometry(180, 415, 30, 30)
self.endpoint_verification_checkbox.setChecked(
ConfigurationController.get_endpoint_verification_enabled())
self._prepared_value(
"endpoint_verification_enabled",
ConfigurationController.get_endpoint_verification_enabled))
self.endpoint_verification_checkbox.setStyleSheet(
self.get_checkbox_style())
self.endpoint_verification_checkbox.show()
@ -1295,6 +1350,31 @@ class Settings(Page):
def showEvent(self, event):
super().showEvent(event)
self._settings_refresh_seq += 1
seq = self._settings_refresh_seq
module_path, class_name = PAGE_REGISTRY["settings"]
worker = PageDataWorker(
"settings", module_path, class_name, self.custom_window)
worker.data_ready.connect(
lambda name, payload, s=seq: self._on_settings_data_ready(payload, s))
worker.failed.connect(
lambda name, error: self._on_settings_data_failed(error))
worker.finished.connect(
lambda w=worker: self._cleanup_settings_worker(w))
self._settings_workers.add(worker)
worker.start()
def _on_settings_data_failed(self, error):
core_logger.warning(f"Settings data refresh failed: {error}")
def _cleanup_settings_worker(self, worker):
self._settings_workers.discard(worker)
worker.deleteLater()
def _on_settings_data_ready(self, payload, seq):
if seq != self._settings_refresh_seq:
return
self._prepared = payload
current_index = self.content_layout.currentIndex()
@ -1407,7 +1487,7 @@ class Settings(Page):
layout = QGridLayout(widget)
layout.setSpacing(10)
profiles = ProfileController.get_all()
profiles = self._prepared_value("profiles", ProfileController.get_all)
total_profiles = len(profiles)
session_profiles = sum(1 for p in profiles.values()
if isinstance(p, SessionProfile))
@ -1439,7 +1519,8 @@ class Settings(Page):
config_path = Constants.HV_CONFIG_HOME
current_connection = self.update_status.get_current_connection()
current_connection = self._prepared_value(
"current_connection", self.update_status.get_current_connection)
if current_connection is not None:
current_connection = current_connection.capitalize()
@ -1697,7 +1778,8 @@ class Settings(Page):
inventory_layout.addWidget(self.refresh_tickets_button)
layout.addWidget(inventory_group)
saved_failure = self.update_status.get_ticket_verification_failure()
saved_failure = self._prepared_value(
"ticket_failure", self.update_status.get_ticket_verification_failure)
has_failure = saved_failure is not None
recovery_group = QGroupBox("Verification Failure Recovery")
@ -1954,7 +2036,8 @@ class Settings(Page):
def load_logs_settings(self) -> None:
try:
config = self.update_status._load_gui_config()
config = self._prepared_value(
"gui_config", self.update_status._load_gui_config)
if config and "logging" in config:
self.enable_gui_logging.setChecked(
config["logging"]["gui_logging_enabled"])
@ -2103,7 +2186,8 @@ class Settings(Page):
layout.addLayout(button_layout)
layout.addStretch()
self.load_systemwide_settings()
self.load_systemwide_settings(
self._prepared_value("systemwide_enabled", self._read_systemwide_enabled))
return page
def create_bwrap_page(self):
@ -2155,17 +2239,13 @@ class Settings(Page):
layout.addLayout(button_layout)
layout.addStretch()
self.load_bwrap_settings()
self.load_bwrap_settings(
self._prepared_value("bwrap_enabled", self._read_bwrap_enabled))
return page
def load_systemwide_settings(self):
enabled = False
try:
privilege_policy = PolicyController.get('privilege')
if privilege_policy is not None:
enabled = PolicyController.is_instated(privilege_policy)
except Exception:
enabled = False
def load_systemwide_settings(self, enabled=None):
if enabled is None:
enabled = self._read_systemwide_enabled()
self.systemwide_toggle.setChecked(enabled)
if enabled:
self.systemwide_status_value.setText("Enabled")
@ -2202,14 +2282,9 @@ class Settings(Page):
self.systemwide_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
def load_bwrap_settings(self):
enabled = False
try:
capability_policy = PolicyController.get('capability')
if capability_policy is not None:
enabled = PolicyController.is_instated(capability_policy)
except Exception:
enabled = False
def load_bwrap_settings(self, enabled=None):
if enabled is None:
enabled = self._read_bwrap_enabled()
self.bwrap_toggle.setChecked(enabled)
if enabled:
self.bwrap_status_value.setText("Enabled")
@ -2248,7 +2323,8 @@ class Settings(Page):
def load_registrations_settings(self) -> None:
try:
config = self.update_status._load_gui_config()
config = self._prepared_value(
"gui_config", self.update_status._load_gui_config)
if config and "registrations" in config:
registrations = config["registrations"]
auto_sync = registrations.get("auto_sync_enabled", False)

View file

@ -0,0 +1,24 @@
import importlib
from PyQt6.QtCore import QThread, pyqtSignal
class PageDataWorker(QThread):
data_ready = pyqtSignal(str, object)
failed = pyqtSignal(str, str)
def __init__(self, name, module_path, class_name, custom_window):
super().__init__()
self.name = name
self.module_path = module_path
self.class_name = class_name
self.custom_window = custom_window
def run(self):
try:
module = importlib.import_module(self.module_path)
cls = getattr(module, self.class_name)
payload = cls.prepare_data(self.custom_window)
self.data_ready.emit(self.name, payload)
except Exception as error:
self.failed.emit(self.name, f"{type(error).__name__}: {error}")