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

@@ -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("<Configure>", update_news_viewport)
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_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)