"""Snapshot and ring-drain helper mixin for the main GUI window.""" from __future__ import annotations from pathlib import Path import time from PyQt6.QtWidgets import QFileDialog class AppWindowSnapshotMixin: """Saves runtime data snapshots and maintains ring-reader freshness.""" def _save_snapshot(self) -> None: """Save runtime snapshot in numpy-directory format.""" self._drain_runtime_rings_for_snapshot() if not self._raw_history and not self._pre_history and not self._result_history: self._show_error("No runtime data is available for save") return try: last_n = int(self._save_count.value()) output_root = Path(self._save_path_input.text().strip()).expanduser() snapshot_name = self._save_name_input.text().strip() snapshot_dir, summary = self._store.save_runtime_snapshot_numpy( output_root, snapshot_name, list(self._raw_history), list(self._pre_history), list(self._result_history), last_n, ) self._log( f"Saved numpy snapshot: {snapshot_dir} " f"(raw={summary.get('raw_count', 0)}, " f"preprocessed={summary.get('preprocessed_count', 0)}, " f"results={summary.get('result_count', 0)}, " f"mode={summary.get('selection_mode', 'unknown')})" ) except Exception as exc: # noqa: BLE001 self._show_error(f"Failed to save snapshot: {exc}") def _browse_save_path(self) -> None: """Open directory picker for snapshot output path.""" selected = QFileDialog.getExistingDirectory( self, "Select Snapshot Directory", self._save_path_input.text().strip() or str(self._project_root), ) if selected: self._save_path_input.setText(selected) def _drain_runtime_rings_for_snapshot(self) -> None: """Drain readers before snapshot to reduce partial-history races.""" if self._raw_reader is None and self._pre_reader is None and self._result_reader is None: return try: start_raw_count = len(self._raw_history) start_pre_count = len(self._pre_history) start_result_count = len(self._result_history) deadline = time.monotonic() + 0.8 while True: progress = False if self._raw_reader is not None: for _ in range(self._max_pop_per_snapshot_drain): collection = self._raw_reader.pop_raw_collection() if collection is None: break self._raw_history.append(collection) progress = True if self._pre_reader is not None: for _ in range(self._max_pop_per_snapshot_drain): collection = self._pre_reader.pop_preprocessed_collection() if collection is None: break self._pre_history.append(collection) progress = True if self._result_reader is not None: for _ in range(self._max_pop_per_snapshot_drain): collection = self._result_reader.pop_result_collection() if collection is None: break self._record_result_history(collection) progress = True if progress: continue missing_raw = self._raw_reader is not None and len(self._raw_history) == start_raw_count missing_pre = self._pre_reader is not None and len(self._pre_history) == start_pre_count got_results = len(self._result_history) > start_result_count if got_results and (missing_raw or missing_pre) and time.monotonic() < deadline: time.sleep(0.01) continue break except Exception as exc: # noqa: BLE001 self._log(f"Snapshot drain warning: {exc}") def _drop_pending_ring_payloads(self, *, include_results: bool = True) -> None: """Drop unread payloads from active readers.""" dropped_raw = self._raw_reader.drop_all() if self._raw_reader is not None else 0 dropped_pre = self._pre_reader.drop_all() if self._pre_reader is not None else 0 dropped_results = 0 if include_results and self._result_reader is not None: dropped_results = self._result_reader.drop_all() if dropped_raw or dropped_pre or dropped_results: self._log( "Single capture ring reset: " f"dropped raw={dropped_raw}, " f"preprocessed={dropped_pre}, " f"results={dropped_results}" )