From 06672589adde67e083219a855921b516f110147a Mon Sep 17 00:00:00 2001 From: m4ko Date: Fri, 28 Aug 2026 22:53:40 +0200 Subject: [PATCH] Cache and cpu / mem improvements for launcher when idle. --- ptlinux/paths.py | 12 ++++ ptlinux/ui.py | 148 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 121 insertions(+), 39 deletions(-) diff --git a/ptlinux/paths.py b/ptlinux/paths.py index d6059c2..9afb1a5 100644 --- a/ptlinux/paths.py +++ b/ptlinux/paths.py @@ -91,6 +91,12 @@ class AppPaths: def launcher_update_lock(self) -> Path: return self.state / "launcher-update.lock" + @property + def launcher_update_check_cache(self) -> Path: + """Display-only cache of the last authenticated update check.""" + + return self.cache / "launcher-update-check.json" + def launcher_update_archive(self, version: str) -> Path: return self.downloads / f"pristontale-eu-linux-update-v{version}.tar.gz" @@ -118,6 +124,12 @@ class AppPaths: def launcher_operation_lock(self) -> Path: return self.state / "launcher-operation.lock" + @property + def game_runtime_marker(self) -> Path: + """Verified Proton/D3D preparation state for the game prefix.""" + + return self.state / "game-runtime.json" + @property def source_game(self) -> Path: return self.launcher_prefix / GAME_RELATIVE_PATH diff --git a/ptlinux/ui.py b/ptlinux/ui.py index ba2474b..13bd435 100644 --- a/ptlinux/ui.py +++ b/ptlinux/ui.py @@ -28,11 +28,18 @@ from .news import ( fetch_news, fetch_news_image, fetch_official_logo, + read_cached_broadcast, + read_cached_news, ) from .overlay import RuntimeCompatibility, inspect_runtime_compatibility from .paths import AppPaths from .settings import Settings, ensure_settings, save_settings -from .self_update import LauncherUpdateCheck, check_for_launcher_update +from .self_update import ( + LauncherUpdateCheck, + cache_launcher_update_check, + check_for_launcher_update, + read_cached_launcher_update_check, +) MAX_NEWS_CARDS = 3 @@ -43,6 +50,10 @@ NEWS_IMAGE_HEIGHT = 118 LAUNCH_STAGE_TOTAL = 4 EVENT_BATCH_SIZE = 48 POLL_INTERVAL_MILLISECONDS = 33 +ACTIVE_POLL_INTERVAL_MILLISECONDS = 75 +IDLE_POLL_INTERVAL_MILLISECONDS = 500 +MAX_ACTIVITY_LOG_LINES = 5000 +ACTIVITY_LOG_TRIM_LINES = 500 TRANSFER_RATE_WINDOW_SECONDS = 3.0 TRANSFER_RATE_MINIMUM_INTERVAL_SECONDS = 0.2 TRANSFER_RATE_IDLE_SECONDS = 2.0 @@ -565,6 +576,8 @@ def run_dashboard(paths: AppPaths) -> None: preloader_progress: ttk.Progressbar | None = None settings_window: tk.Toplevel | None = None operation_active = False + game_window_active = False + activity_log_lines = 0 operation_started_at: float | None = None operation_stage = 0 operation_stage_total = 0 @@ -586,8 +599,16 @@ def run_dashboard(paths: AppPaths) -> None: ) def append_log(value: str) -> None: + nonlocal activity_log_lines + rendered = value.rstrip() + "\n" log.configure(state="normal") - log.insert("end", value.rstrip() + "\n") + log.insert("end", rendered) + activity_log_lines += rendered.count("\n") + if activity_log_lines > MAX_ACTIVITY_LOG_LINES: + retained_lines = MAX_ACTIVITY_LOG_LINES - ACTIVITY_LOG_TRIM_LINES + excess = activity_log_lines - retained_lines + log.delete("1.0", f"{excess + 1}.0") + activity_log_lines -= excess log.see("end") log.configure(state="disabled") @@ -759,7 +780,7 @@ def run_dashboard(paths: AppPaths) -> None: restart_on_success: bool = False, game_files_may_change: bool = False, ) -> None: - nonlocal operation_active + nonlocal operation_active, game_window_active nonlocal operation_started_at nonlocal operation_stage nonlocal operation_stage_total @@ -767,6 +788,7 @@ def run_dashboard(paths: AppPaths) -> None: nonlocal operation_refreshes_compatibility nonlocal last_elapsed_second operation_active = True + game_window_active = False operation_started_at = time.monotonic() operation_stage = 0 operation_stage_total = 0 @@ -1145,6 +1167,47 @@ def run_dashboard(paths: AppPaths) -> None: daemon=True, ).start() + def show_launcher_update_check( + check: LauncherUpdateCheck, + *, + log_reason: bool = True, + ) -> None: + nonlocal launcher_update_check + launcher_update_check = check + if check.available and check.supported: + launcher_update_button.configure( + text=f"UPDATE TO {check.latest_version}", + state="normal", + ) + elif check.available: + launcher_update_button.configure( + text="MANUAL UPDATE NEEDED", + state="normal", + ) + else: + launcher_update_button.configure( + text=f"LAUNCHER {__version__}", + state="normal", + ) + if log_reason: + append_log(check.reason) + + def refresh_launcher_update_check() -> None: + def worker() -> None: + try: + check = check_for_launcher_update() + except RuntimeError as update_error: + events.put(("launcher_update_error", str(update_error))) + else: + cache_launcher_update_check(paths.launcher_update_check_cache, check) + events.put(("launcher_update_check", check)) + + threading.Thread( + target=worker, + name="ptlinux-self-update", + daemon=True, + ).start() + def refresh_remote_content() -> None: def broadcast_worker() -> None: events.put(("broadcast", fetch_latest_broadcast(paths.broadcast_cache))) @@ -1160,27 +1223,17 @@ def run_dashboard(paths: AppPaths) -> None: else: events.put(("version", snapshot)) - def launcher_update_worker() -> None: - try: - check = check_for_launcher_update() - except RuntimeError as update_error: - events.put(("launcher_update_error", str(update_error))) - else: - events.put(("launcher_update_check", check)) - threading.Thread(target=broadcast_worker, name="ptlinux-broadcast", daemon=True).start() threading.Thread(target=news_worker, name="ptlinux-news", daemon=True).start() threading.Thread(target=version_worker, name="ptlinux-version", daemon=True).start() - threading.Thread( - target=launcher_update_worker, - name="ptlinux-self-update", - daemon=True, - ).start() + refresh_launcher_update_check() def finish_operation(returncode: int) -> None: - nonlocal operation_active, operation_refreshes_compatibility, operation_started_at + nonlocal operation_active, game_window_active + nonlocal operation_refreshes_compatibility, operation_started_at refresh_compatibility = operation_refreshes_compatibility operation_active = False + game_window_active = False operation_refreshes_compatibility = False operation_started_at = None progress_updates.clear() @@ -1200,7 +1253,7 @@ def run_dashboard(paths: AppPaths) -> None: progress_text.set("See the activity log for details.") def poll_events() -> None: - nonlocal launcher_update_check, runtime_compatibility + nonlocal game_window_active, launcher_update_check, runtime_compatibility processed = 0 try: while processed < EVENT_BATCH_SIZE: @@ -1236,6 +1289,7 @@ def run_dashboard(paths: AppPaths) -> None: if step > 0 and total > 0 and isinstance(message, str) and message: set_operation_stage(step, total, message) elif kind in {"game_started", "game_window_ready"}: + game_window_active = True progress_bar.stop() status.set("Game client started.") close_game_preloader() @@ -1285,26 +1339,13 @@ def run_dashboard(paths: AppPaths) -> None: value, LauncherUpdateCheck, ): - launcher_update_check = value - if value.available and value.supported: - launcher_update_button.configure( - text=f"UPDATE TO {value.latest_version}", - state="normal", - ) - elif value.available: - launcher_update_button.configure( - text="MANUAL UPDATE NEEDED", - state="normal", - ) - else: - launcher_update_button.configure( - text=f"LAUNCHER {__version__}", - state="normal", - ) - append_log(value.reason) + show_launcher_update_check(value) elif kind == "launcher_update_error" and isinstance(value, str): - launcher_update_check = None - launcher_update_button.configure(text="CHECK LAUNCHER", state="normal") + if launcher_update_check is None: + launcher_update_button.configure( + text="CHECK LAUNCHER", + state="normal", + ) append_log(f"Launcher update check unavailable: {value}") except queue.Empty: pass @@ -1315,7 +1356,14 @@ def run_dashboard(paths: AppPaths) -> None: if speed_text.get() != "0 B/s" and transfer_rate.is_idle(): speed_text.set("0 B/s") update_elapsed_indicator() - root.after(POLL_INTERVAL_MILLISECONDS, poll_events) + root.after( + dashboard_poll_interval( + operation_active=operation_active, + game_window_active=game_window_active, + pending_events=not events.empty(), + ), + poll_events, + ) def command_for(*arguments: str) -> list[str]: return [sys.executable, "-m", "ptlinux", *arguments] @@ -1327,7 +1375,7 @@ def run_dashboard(paths: AppPaths) -> None: if required_reason is not None: status.set("Checking for a compatible launcher update…") progress_text.set("Play remains disabled until this release is supported.") - refresh_remote_content() + refresh_launcher_update_check() return if not check.available: if required_reason is None: @@ -1729,6 +1777,13 @@ def run_dashboard(paths: AppPaths) -> None: news_content.bind("", update_news_viewport) news_canvas.bind("", resize_news_content) + show_broadcast(read_cached_broadcast(paths.broadcast_cache)) + show_news(read_cached_news(paths.news_cache)) + cached_launcher_update = read_cached_launcher_update_check( + paths.launcher_update_check_cache + ) + if cached_launcher_update is not None: + show_launcher_update_check(cached_launcher_update, log_reason=False) refresh_primary_action() refresh_runtime_compatibility() poll_events() @@ -1747,6 +1802,21 @@ def _as_int(value: object) -> int: return value if isinstance(value, int) and value >= 0 else 0 +def dashboard_poll_interval( + *, + operation_active: bool, + game_window_active: bool, + pending_events: bool, +) -> int: + """Choose a responsive poll rate while avoiding busy idle/game polling.""" + + if pending_events: + return POLL_INTERVAL_MILLISECONDS + if operation_active and not game_window_active: + return ACTIVE_POLL_INTERVAL_MILLISECONDS + return IDLE_POLL_INTERVAL_MILLISECONDS + + def _format_bytes(value: int) -> str: units = ("B", "KiB", "MiB", "GiB") amount = float(value)