Cache and cpu / mem improvements for launcher when idle.

This commit is contained in:
2026-08-28 22:53:40 +02:00
parent d20eea6877
commit 06672589ad
2 changed files with 121 additions and 39 deletions

View File

@@ -91,6 +91,12 @@ class AppPaths:
def launcher_update_lock(self) -> Path: def launcher_update_lock(self) -> Path:
return self.state / "launcher-update.lock" 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: def launcher_update_archive(self, version: str) -> Path:
return self.downloads / f"pristontale-eu-linux-update-v{version}.tar.gz" return self.downloads / f"pristontale-eu-linux-update-v{version}.tar.gz"
@@ -118,6 +124,12 @@ class AppPaths:
def launcher_operation_lock(self) -> Path: def launcher_operation_lock(self) -> Path:
return self.state / "launcher-operation.lock" 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 @property
def source_game(self) -> Path: def source_game(self) -> Path:
return self.launcher_prefix / GAME_RELATIVE_PATH return self.launcher_prefix / GAME_RELATIVE_PATH

View File

@@ -28,11 +28,18 @@ from .news import (
fetch_news, fetch_news,
fetch_news_image, fetch_news_image,
fetch_official_logo, fetch_official_logo,
read_cached_broadcast,
read_cached_news,
) )
from .overlay import RuntimeCompatibility, inspect_runtime_compatibility from .overlay import RuntimeCompatibility, inspect_runtime_compatibility
from .paths import AppPaths from .paths import AppPaths
from .settings import Settings, ensure_settings, save_settings 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 MAX_NEWS_CARDS = 3
@@ -43,6 +50,10 @@ NEWS_IMAGE_HEIGHT = 118
LAUNCH_STAGE_TOTAL = 4 LAUNCH_STAGE_TOTAL = 4
EVENT_BATCH_SIZE = 48 EVENT_BATCH_SIZE = 48
POLL_INTERVAL_MILLISECONDS = 33 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_WINDOW_SECONDS = 3.0
TRANSFER_RATE_MINIMUM_INTERVAL_SECONDS = 0.2 TRANSFER_RATE_MINIMUM_INTERVAL_SECONDS = 0.2
TRANSFER_RATE_IDLE_SECONDS = 2.0 TRANSFER_RATE_IDLE_SECONDS = 2.0
@@ -565,6 +576,8 @@ def run_dashboard(paths: AppPaths) -> None:
preloader_progress: ttk.Progressbar | None = None preloader_progress: ttk.Progressbar | None = None
settings_window: tk.Toplevel | None = None settings_window: tk.Toplevel | None = None
operation_active = False operation_active = False
game_window_active = False
activity_log_lines = 0
operation_started_at: float | None = None operation_started_at: float | None = None
operation_stage = 0 operation_stage = 0
operation_stage_total = 0 operation_stage_total = 0
@@ -586,8 +599,16 @@ def run_dashboard(paths: AppPaths) -> None:
) )
def append_log(value: str) -> None: def append_log(value: str) -> None:
nonlocal activity_log_lines
rendered = value.rstrip() + "\n"
log.configure(state="normal") 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.see("end")
log.configure(state="disabled") log.configure(state="disabled")
@@ -759,7 +780,7 @@ def run_dashboard(paths: AppPaths) -> None:
restart_on_success: bool = False, restart_on_success: bool = False,
game_files_may_change: bool = False, game_files_may_change: bool = False,
) -> None: ) -> None:
nonlocal operation_active nonlocal operation_active, game_window_active
nonlocal operation_started_at nonlocal operation_started_at
nonlocal operation_stage nonlocal operation_stage
nonlocal operation_stage_total nonlocal operation_stage_total
@@ -767,6 +788,7 @@ def run_dashboard(paths: AppPaths) -> None:
nonlocal operation_refreshes_compatibility nonlocal operation_refreshes_compatibility
nonlocal last_elapsed_second nonlocal last_elapsed_second
operation_active = True operation_active = True
game_window_active = False
operation_started_at = time.monotonic() operation_started_at = time.monotonic()
operation_stage = 0 operation_stage = 0
operation_stage_total = 0 operation_stage_total = 0
@@ -1145,6 +1167,47 @@ def run_dashboard(paths: AppPaths) -> None:
daemon=True, daemon=True,
).start() ).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 refresh_remote_content() -> None:
def broadcast_worker() -> None: def broadcast_worker() -> None:
events.put(("broadcast", fetch_latest_broadcast(paths.broadcast_cache))) events.put(("broadcast", fetch_latest_broadcast(paths.broadcast_cache)))
@@ -1160,27 +1223,17 @@ def run_dashboard(paths: AppPaths) -> None:
else: else:
events.put(("version", snapshot)) 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=broadcast_worker, name="ptlinux-broadcast", daemon=True).start()
threading.Thread(target=news_worker, name="ptlinux-news", 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=version_worker, name="ptlinux-version", daemon=True).start()
threading.Thread( refresh_launcher_update_check()
target=launcher_update_worker,
name="ptlinux-self-update",
daemon=True,
).start()
def finish_operation(returncode: int) -> None: 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 refresh_compatibility = operation_refreshes_compatibility
operation_active = False operation_active = False
game_window_active = False
operation_refreshes_compatibility = False operation_refreshes_compatibility = False
operation_started_at = None operation_started_at = None
progress_updates.clear() progress_updates.clear()
@@ -1200,7 +1253,7 @@ def run_dashboard(paths: AppPaths) -> None:
progress_text.set("See the activity log for details.") progress_text.set("See the activity log for details.")
def poll_events() -> None: def poll_events() -> None:
nonlocal launcher_update_check, runtime_compatibility nonlocal game_window_active, launcher_update_check, runtime_compatibility
processed = 0 processed = 0
try: try:
while processed < EVENT_BATCH_SIZE: 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: if step > 0 and total > 0 and isinstance(message, str) and message:
set_operation_stage(step, total, message) set_operation_stage(step, total, message)
elif kind in {"game_started", "game_window_ready"}: elif kind in {"game_started", "game_window_ready"}:
game_window_active = True
progress_bar.stop() progress_bar.stop()
status.set("Game client started.") status.set("Game client started.")
close_game_preloader() close_game_preloader()
@@ -1285,26 +1339,13 @@ def run_dashboard(paths: AppPaths) -> None:
value, value,
LauncherUpdateCheck, LauncherUpdateCheck,
): ):
launcher_update_check = value show_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)
elif kind == "launcher_update_error" and isinstance(value, str): elif kind == "launcher_update_error" and isinstance(value, str):
launcher_update_check = None if launcher_update_check is None:
launcher_update_button.configure(text="CHECK LAUNCHER", state="normal") launcher_update_button.configure(
text="CHECK LAUNCHER",
state="normal",
)
append_log(f"Launcher update check unavailable: {value}") append_log(f"Launcher update check unavailable: {value}")
except queue.Empty: except queue.Empty:
pass pass
@@ -1315,7 +1356,14 @@ def run_dashboard(paths: AppPaths) -> None:
if speed_text.get() != "0 B/s" and transfer_rate.is_idle(): if speed_text.get() != "0 B/s" and transfer_rate.is_idle():
speed_text.set("0 B/s") speed_text.set("0 B/s")
update_elapsed_indicator() 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]: def command_for(*arguments: str) -> list[str]:
return [sys.executable, "-m", "ptlinux", *arguments] return [sys.executable, "-m", "ptlinux", *arguments]
@@ -1327,7 +1375,7 @@ def run_dashboard(paths: AppPaths) -> None:
if required_reason is not None: if required_reason is not None:
status.set("Checking for a compatible launcher update…") status.set("Checking for a compatible launcher update…")
progress_text.set("Play remains disabled until this release is supported.") progress_text.set("Play remains disabled until this release is supported.")
refresh_remote_content() refresh_launcher_update_check()
return return
if not check.available: if not check.available:
if required_reason is None: if required_reason is None:
@@ -1729,6 +1777,13 @@ def run_dashboard(paths: AppPaths) -> None:
news_content.bind("<Configure>", update_news_viewport) news_content.bind("<Configure>", update_news_viewport)
news_canvas.bind("<Configure>", resize_news_content) news_canvas.bind("<Configure>", 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_primary_action()
refresh_runtime_compatibility() refresh_runtime_compatibility()
poll_events() poll_events()
@@ -1747,6 +1802,21 @@ def _as_int(value: object) -> int:
return value if isinstance(value, int) and value >= 0 else 0 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: def _format_bytes(value: int) -> str:
units = ("B", "KiB", "MiB", "GiB") units = ("B", "KiB", "MiB", "GiB")
amount = float(value) amount = float(value)