improved startup first frame boot
This commit is contained in:
+146
-48
@@ -161,6 +161,75 @@ def truncate_entries(entries: list[dict[str, Any]], max_pairs: int) -> list[dict
|
||||
if count == target: return entries[i:]
|
||||
return entries
|
||||
|
||||
def _detect_refresh_rate_win32() -> float:
|
||||
"""Return the primary display's current refresh rate in Hz, or 0.0 on failure.
|
||||
|
||||
Uses user32.EnumDisplaySettingsW (ENUM_CURRENT_SETTINGS) which reads the value
|
||||
directly from the display driver in microseconds. The previous implementation
|
||||
shelled out to PowerShell + WMI (Get-CimInstance Win32_VideoController), which
|
||||
cost ~350ms on every startup and blocked the first frame.
|
||||
"""
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
class _DEVMODE(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dmDeviceName", wintypes.WCHAR * 32), ("dmSpecVersion", wintypes.WORD),
|
||||
("dmDriverVersion", wintypes.WORD), ("dmSize", wintypes.WORD),
|
||||
("dmDriverExtra", wintypes.WORD), ("dmFields", wintypes.DWORD),
|
||||
("dmStuff", ctypes.c_byte * 16), ("dmColor", wintypes.SHORT),
|
||||
("dmDuplex", wintypes.SHORT), ("dmYResolution", wintypes.SHORT),
|
||||
("dmTTOption", wintypes.SHORT), ("dmCollate", wintypes.SHORT),
|
||||
("dmFormName", wintypes.WCHAR * 32), ("dmLogPixels", wintypes.WORD),
|
||||
("dmBitsPerPel", wintypes.DWORD), ("dmPelsWidth", wintypes.DWORD),
|
||||
("dmPelsHeight", wintypes.DWORD), ("dmDisplayFlags", wintypes.DWORD),
|
||||
("dmDisplayFrequency", wintypes.DWORD), ("dmICMMethod", wintypes.DWORD),
|
||||
("dmICMIntent", wintypes.DWORD), ("dmMediaType", wintypes.DWORD),
|
||||
("dmDitherType", wintypes.DWORD), ("dmReserved1", wintypes.DWORD),
|
||||
("dmReserved2", wintypes.DWORD), ("dmPanningWidth", wintypes.DWORD),
|
||||
("dmPanningHeight", wintypes.DWORD),
|
||||
]
|
||||
dm = _DEVMODE()
|
||||
dm.dmSize = ctypes.sizeof(_DEVMODE)
|
||||
if ctypes.windll.user32.EnumDisplaySettingsW(None, -1, ctypes.byref(dm)):
|
||||
# dmDisplayFrequency is 0 or 1 for "default/hardware" on some drivers.
|
||||
if dm.dmDisplayFrequency > 1:
|
||||
return float(dm.dmDisplayFrequency)
|
||||
except Exception:
|
||||
pass
|
||||
return 0.0
|
||||
|
||||
def _resolve_font_path(font_path: str, assets_dir: Path) -> str:
|
||||
"""Normalize a configured font path to something hello_imgui can load.
|
||||
|
||||
hello_imgui resolves relative paths against the assets folder. A config may
|
||||
carry a stale ABSOLUTE path from a different project checkout (e.g.
|
||||
C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf after the repo
|
||||
moved to C:/projects/sloppy). In that case the absolute file does not exist
|
||||
and the load fails. This recovers by:
|
||||
|
||||
1. If the absolute path lives under the current assets folder -> relativize.
|
||||
2. If the absolute path exists on disk as-is -> keep it.
|
||||
3. Otherwise recover the basename under assets/fonts or assets.
|
||||
4. Final fallback: the bundled default Inter font.
|
||||
"""
|
||||
p = Path(font_path)
|
||||
if not p.is_absolute():
|
||||
return font_path # already relative; hello_imgui searches the assets folder
|
||||
try:
|
||||
if p.is_relative_to(assets_dir):
|
||||
return str(p.relative_to(assets_dir)).replace("\\", "/")
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
if p.exists():
|
||||
return font_path # absolute path to a real file elsewhere — load directly
|
||||
# Stale absolute path: recover by basename relative to the assets folder.
|
||||
name = p.name
|
||||
for rel in (f"fonts/{name}", name):
|
||||
if (assets_dir / rel).exists():
|
||||
return rel
|
||||
return "fonts/Inter-Regular.ttf"
|
||||
|
||||
class App:
|
||||
"""The main ImGui interface orchestrator for Manual Slop."""
|
||||
|
||||
@@ -172,7 +241,7 @@ class App:
|
||||
# --- Core Dependencies & State ---
|
||||
from src.startup_profiler import startup_profiler
|
||||
with startup_profiler.phase("app_init_AppController"):
|
||||
self.controller = app_controller.AppController()
|
||||
self.controller = app_controller.AppController(defer_warmup=True)
|
||||
self.controller._app = self
|
||||
with startup_profiler.phase("app_init_history_perfmon"):
|
||||
from src import history, performance_monitor
|
||||
@@ -187,7 +256,8 @@ class App:
|
||||
# --- Command Palette ---
|
||||
self.show_command_palette: bool = False
|
||||
# --- Initialization ---
|
||||
self.controller.init_state()
|
||||
with startup_profiler.phase("app_init_state"):
|
||||
self.controller.init_state()
|
||||
from src.hot_reloader import HotReloader, HotModule
|
||||
if 'src.gui_2' not in HotReloader.HOT_MODULES:
|
||||
HotReloader.register(HotModule(
|
||||
@@ -196,11 +266,13 @@ class App:
|
||||
state_keys=['active_discussion', 'show_windows', 'ui_file_paths', 'ui_screenshot_paths', 'disc_entries', 'disc_roles'],
|
||||
delegation_targets=['_render_main_interface', '_render_discussion_hub', '_render_files_and_media', '_render_ai_settings_hub', '_render_operations_hub', '_render_mma_dashboard']
|
||||
))
|
||||
self.workspace_manager = workspace_manager.WorkspaceManager(project_root=self.controller.active_project_root)
|
||||
self.disc_entries = self.controller.disc_entries
|
||||
self.disc_roles = self.controller.disc_roles
|
||||
self.workspace_profiles = self.workspace_manager.load_all_profiles()
|
||||
self.controller.start_services(self)
|
||||
with startup_profiler.phase("app_init_workspace"):
|
||||
self.workspace_manager = workspace_manager.WorkspaceManager(project_root=self.controller.active_project_root)
|
||||
self.disc_entries = self.controller.disc_entries
|
||||
self.disc_roles = self.controller.disc_roles
|
||||
self.workspace_profiles = self.workspace_manager.load_all_profiles()
|
||||
with startup_profiler.phase("app_init_start_services"):
|
||||
self.controller.start_services(self)
|
||||
# --- Controller Callbacks & Actions ---
|
||||
self.controller._predefined_callbacks['save_context_preset'] = self.save_context_preset
|
||||
self.controller._predefined_callbacks['load_context_preset'] = self.load_context_preset
|
||||
@@ -457,16 +529,13 @@ class App:
|
||||
user_scale = theme.get_current_scale()
|
||||
self.runner_params.dpi_aware_params.dpi_window_size_factor = user_scale
|
||||
|
||||
# Detect Monitor Refresh Rate for capping (Win32 only)
|
||||
# Detect Monitor Refresh Rate for capping (Win32 only).
|
||||
# Uses the native EnumDisplaySettings call (~0.3ms) instead of spawning a
|
||||
# PowerShell/WMI subprocess (~350ms) so the first frame is not blocked.
|
||||
fps_cap = 60.0
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
# Use PowerShell to get max refresh rate across all controllers
|
||||
cmd = "powershell -NoProfile -Command \"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty CurrentRefreshRate\""
|
||||
out = subprocess.check_output(cmd, shell=True).decode().splitlines()
|
||||
rates = [float(r.strip()) for r in out if r.strip().isdigit()]
|
||||
if rates: fps_cap = max(rates)
|
||||
except Exception: pass
|
||||
rate = _detect_refresh_rate_win32()
|
||||
if rate: fps_cap = rate
|
||||
|
||||
# Enable idling with monitor refresh rate to effectively cap FPS
|
||||
self.runner_params.fps_idling.enable_idling = True
|
||||
@@ -476,11 +545,17 @@ class App:
|
||||
self.runner_params.imgui_window_params.show_menu_view_themes = True
|
||||
self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder
|
||||
self.runner_params.ini_filename = "manualslop_layout.ini"
|
||||
def _profiled_setup_style() -> None:
|
||||
with startup_profiler.phase("setup_imgui_style"):
|
||||
theme.apply_current()
|
||||
def _profiled_post_init() -> None:
|
||||
with startup_profiler.phase("post_init"):
|
||||
self._post_init()
|
||||
self.runner_params.callbacks.show_gui = self._gui_func
|
||||
self.runner_params.callbacks.show_menus = self._show_menus
|
||||
self.runner_params.callbacks.load_additional_fonts = self._load_fonts
|
||||
self.runner_params.callbacks.setup_imgui_style = theme.apply_current
|
||||
self.runner_params.callbacks.post_init = self._post_init
|
||||
self.runner_params.callbacks.setup_imgui_style = _profiled_setup_style
|
||||
self.runner_params.callbacks.post_init = _profiled_post_init
|
||||
self._fetch_models(self.current_provider)
|
||||
md_options = markdown_helper.get_renderer().options
|
||||
immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options))
|
||||
@@ -489,42 +564,39 @@ class App:
|
||||
session_logger.close_session()
|
||||
|
||||
def _load_fonts(self) -> None:
|
||||
# Set hello_imgui assets folder to the actual absolute path
|
||||
assets_dir = Path(__file__).parent.parent / "assets"
|
||||
if assets_dir.exists():
|
||||
hello_imgui.set_assets_folder(str(assets_dir.absolute()))
|
||||
from src.startup_profiler import startup_profiler
|
||||
with startup_profiler.phase("load_fonts"):
|
||||
# Set hello_imgui assets folder to the actual absolute path
|
||||
assets_dir = Path(__file__).parent.parent / "assets"
|
||||
if assets_dir.exists():
|
||||
hello_imgui.set_assets_folder(str(assets_dir.absolute()))
|
||||
|
||||
# Improved font rendering with oversampling
|
||||
config = imgui.ImFontConfig()
|
||||
config.oversample_h = 3
|
||||
config.oversample_v = 3
|
||||
# Improved font rendering with oversampling
|
||||
config = imgui.ImFontConfig()
|
||||
config.oversample_h = 3
|
||||
config.oversample_v = 3
|
||||
|
||||
font_path, font_size = theme.get_font_loading_params()
|
||||
|
||||
if font_path:
|
||||
p = Path(font_path)
|
||||
if p.is_absolute():
|
||||
font_path, font_size = theme.get_font_loading_params()
|
||||
|
||||
if font_path:
|
||||
font_path = _resolve_font_path(font_path, assets_dir)
|
||||
# Just try loading it directly; hello_imgui will look in the assets folder
|
||||
try:
|
||||
if p.is_relative_to(assets_dir):
|
||||
font_path = str(p.relative_to(assets_dir)).replace("\\", "/")
|
||||
except (ValueError, AttributeError):
|
||||
pass # Fallback to original font_path if relative_to fails or on old Python
|
||||
|
||||
# Just try loading it directly; hello_imgui will look in the assets folder
|
||||
try:
|
||||
self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config)
|
||||
except Exception as e:
|
||||
print(f"Failed to load main font {font_path}: {e}")
|
||||
with startup_profiler.phase("load_fonts.main_with_fontawesome"):
|
||||
self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config)
|
||||
except Exception as e:
|
||||
print(f"Failed to load main font {font_path}: {e}")
|
||||
self.main_font = None
|
||||
else:
|
||||
self.main_font = None
|
||||
else:
|
||||
self.main_font = None
|
||||
|
||||
try:
|
||||
params = hello_imgui.FontLoadingParams(font_config=config)
|
||||
self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params)
|
||||
except Exception as e:
|
||||
print(f"Failed to load mono font: {e}")
|
||||
self.mono_font = None
|
||||
try:
|
||||
with startup_profiler.phase("load_fonts.mono"):
|
||||
params = hello_imgui.FontLoadingParams(font_config=config)
|
||||
self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params)
|
||||
except Exception as e:
|
||||
print(f"Failed to load mono font: {e}")
|
||||
self.mono_font = None
|
||||
|
||||
def _handle_approve_mma_step(self, user_data=None) -> None:
|
||||
"""UI-level wrapper for approving a pending MMA step."""
|
||||
@@ -817,6 +889,32 @@ class App:
|
||||
# ---------------------------------------------------------------- gui
|
||||
|
||||
def _gui_func(self) -> None:
|
||||
# One-shot: log when immapp first hands control to our render callback. The
|
||||
# span init -> here is window/GL/context creation + the font/style/post_init
|
||||
# callbacks (all opaque C++); the span here -> mark_first_frame_rendered is
|
||||
# the cost of the first full-UI render. Splitting them attributes the gap.
|
||||
if not getattr(self, "_gui_func_entered", False):
|
||||
self._gui_func_entered = True
|
||||
try:
|
||||
init_ts = getattr(self.controller, "_init_start_ts", None)
|
||||
if init_ts is not None:
|
||||
sys.stderr.write(f"[startup] first _gui_func entry at {(time.time() - init_ts) * 1000:.1f}ms after init (window/GL + font/style/post_init callbacks done)\n")
|
||||
sys.stderr.flush()
|
||||
except Exception: pass
|
||||
|
||||
# One-shot: kick off the controller's heavy-module warmup on the shared
|
||||
# io_pool once the FIRST frame has actually been painted. Waiting one frame
|
||||
# keeps the ~2s of SDK C-extension imports from holding the GIL during
|
||||
# window creation and font-atlas building, so the window appears at full
|
||||
# speed; the SDKs are then warmed by the time the user sends their first
|
||||
# message. start_warmup() is idempotent.
|
||||
if not getattr(self, "_preload_started", False):
|
||||
if getattr(self, "_first_frame_painted", False):
|
||||
self.controller.start_warmup()
|
||||
self._preload_started = True
|
||||
else:
|
||||
self._first_frame_painted = True
|
||||
|
||||
io = imgui.get_io()
|
||||
if io.key_ctrl and io.key_alt and imgui.is_key_down(imgui.Key.r):
|
||||
self._trigger_hot_reload()
|
||||
|
||||
Reference in New Issue
Block a user