From 185962b4af1c7008cd7a40c4de1cb993277e2d3c Mon Sep 17 00:00:00 2001 From: Dominik Krenn Date: Wed, 5 Nov 2025 10:42:24 +0100 Subject: [PATCH] thread calulations and ui difretn so hughe numbers dont break ui flow --- collatz_ui/app.py | 150 ++++++++++++++++++++++++++++++++++++---- collatz_ui/dashboard.py | 31 +++++++-- collatz_ui/views.py | 15 +++- 3 files changed, 179 insertions(+), 17 deletions(-) diff --git a/collatz_ui/app.py b/collatz_ui/app.py index a01e77d..4abee8b 100644 --- a/collatz_ui/app.py +++ b/collatz_ui/app.py @@ -1,8 +1,11 @@ from __future__ import annotations import curses +import threading import time +from dataclasses import dataclass from pathlib import Path +from typing import Optional from mathstream import StreamNumber, add, mul, div, is_even, clear_logs @@ -13,19 +16,39 @@ from .dashboard import CollatzDashboard LOG_DIR = Path("instance/log") -def collatz_step(n: StreamNumber, three: StreamNumber, two: StreamNumber, one: StreamNumber) -> StreamNumber: +def collatz_step( + n: StreamNumber, + three: StreamNumber, + two: StreamNumber, + one: StreamNumber, +) -> StreamNumber: """Compute the next Collatz value.""" return div(n, two) if is_even(n) else add(mul(n, three), one) +@dataclass +class SharedState: + step: int = 0 + digits: str = "0" + delta_sign: int = 0 + computing: bool = False + calc_start_time: Optional[float] = None + last_calc_duration: float = 0.0 + reached_one: bool = False + + class CollatzApp: """Main application loop driving the Collatz visualization.""" def __init__(self, stdscr: "curses._CursesWindow") -> None: self.stdscr = stdscr self.dashboard = CollatzDashboard(stdscr, LOG_DIR) - self.step = 0 self.start_time = time.time() + self.state = SharedState() + self._state_lock = threading.Lock() + self._stop_event = threading.Event() + self._worker: Optional[threading.Thread] = None + self._error: Optional[BaseException] = None def run(self) -> None: start_file = Path("start.txt") @@ -40,31 +63,134 @@ class CollatzApp: n = StreamNumber(start_file) one, two, three = (StreamNumber(literal=s) for s in ("1", "2", "3")) + initial_digits = "".join(n.stream()) or "0" + with self._state_lock: + self.state.digits = initial_digits + self.state.delta_sign = 0 + self.state.step = 0 + + self.start_time = time.time() + self._worker = threading.Thread( + target=self._worker_loop, + args=(n, one, two, three), + daemon=True, + ) + self._worker.start() + + try: + self._ui_loop() + finally: + self._stop_event.set() + if self._worker: + self._worker.join() + + def _ui_loop(self) -> None: while True: - even_step = is_even(n) - delta_sign = -1 if even_step else 1 - - self.step += 1 - n = collatz_step(n, three, two, one) - digits = "".join(n.stream()) or "0" - + snapshot = self._snapshot_state() elapsed = time.time() - self.start_time - avg_step = elapsed / self.step if self.step else 0.0 + avg_step = elapsed / snapshot.step if snapshot.step else 0.0 + if snapshot.computing and snapshot.calc_start_time is not None: + current_calc = time.time() - snapshot.calc_start_time + else: + current_calc = 0.0 + last_calc = snapshot.last_calc_duration - self.dashboard.render(self.step, elapsed, avg_step, digits, delta_sign) + digits = snapshot.digits or "0" + delta = snapshot.delta_sign + + self.dashboard.render( + snapshot.step, + elapsed, + avg_step, + digits, + delta, + current_calc, + last_calc, + snapshot.computing, + ) + + if self._error: + self.dashboard.show_message( + f"Error: {self._error}", UIColors.DOWN + ) + break ch = self.stdscr.getch() if ch == ord("q"): + self._stop_event.set() break if ch != -1: self.dashboard.handle_input(ch) - if digits == "1": + if snapshot.reached_one: self.dashboard.show_message( "Reached 1 — press any key to exit.", UIColors.UP ) break + if self._stop_event.is_set(): + break + + time.sleep(0.05) + + def _snapshot_state(self) -> SharedState: + with self._state_lock: + return SharedState( + step=self.state.step, + digits=self.state.digits, + delta_sign=self.state.delta_sign, + computing=self.state.computing, + calc_start_time=self.state.calc_start_time, + last_calc_duration=self.state.last_calc_duration, + reached_one=self.state.reached_one, + ) + + def _worker_loop( + self, + current: StreamNumber, + one: StreamNumber, + two: StreamNumber, + three: StreamNumber, + ) -> None: + while not self._stop_event.is_set(): + with self._state_lock: + self.state.computing = True + self.state.calc_start_time = time.time() + + even_step = is_even(current) + delta_sign = -1 if even_step else 1 + calc_started = time.time() + + try: + next_n = collatz_step(current, three, two, one) + digits = "".join(next_n.stream()) or "0" + except Exception as exc: # pragma: no cover - safeguard for worker errors + self._error = exc + with self._state_lock: + self.state.computing = False + self.state.calc_start_time = None + self._stop_event.set() + return + + duration = time.time() - calc_started + + with self._state_lock: + self.state.step += 1 + self.state.digits = digits + self.state.delta_sign = delta_sign + self.state.last_calc_duration = duration + self.state.computing = False + self.state.calc_start_time = None + if digits == "1": + self.state.reached_one = True + + current.free(delete_file=False) + current = next_n + + if digits == "1": + self._stop_event.set() + return + def run_collatz(stdscr: "curses._CursesWindow") -> None: """Entry point expected by curses.wrapper.""" diff --git a/collatz_ui/dashboard.py b/collatz_ui/dashboard.py index 760e620..d092da5 100644 --- a/collatz_ui/dashboard.py +++ b/collatz_ui/dashboard.py @@ -23,13 +23,14 @@ class CollatzDashboard: UIColors.setup() self.num_scroll = 0 self.log_scroll = 0 + self._last_step = 0 self._build_views() def _build_views(self) -> None: lines = curses.LINES cols = curses.COLS - header_h = 5 + header_h = 6 graph_h = 1 available = max(2, lines - header_h - graph_h - 2) num_h = max(1, (available * 3) // 4) @@ -40,9 +41,31 @@ class CollatzDashboard: self.number = NumberView(num_h, cols, header_h + graph_h + 1, 0) self.log = LogView(log_h, cols, header_h + graph_h + num_h + 2, 0, self.log_dir) - def render(self, step: int, elapsed: float, avg_step: float, digits: str, delta_sign: int) -> None: - self.header.render(step, elapsed, avg_step, len(digits)) - self.graph.add_delta(delta_sign) + def render( + self, + step: int, + elapsed: float, + avg_step: float, + digits: str, + delta_sign: int, + current_calc: float, + last_calc: float, + computing: bool, + ) -> None: + self.header.render( + step, + elapsed, + avg_step, + len(digits), + current_calc, + last_calc, + computing, + ) + if step > self._last_step: + self.graph.add_delta(delta_sign) + self._last_step = step + else: + self.graph.redraw() self.num_scroll = self.number.render(digits, self.num_scroll) self.log_scroll = self.log.render(self.log_scroll) curses.doupdate() diff --git a/collatz_ui/views.py b/collatz_ui/views.py index b13a0f9..024d967 100644 --- a/collatz_ui/views.py +++ b/collatz_ui/views.py @@ -13,7 +13,16 @@ class HeaderView: def __init__(self, height: int, width: int, y: int, x: int) -> None: self.win = curses.newwin(height, width, y, x) - def render(self, step: int, elapsed: float, avg_step: float, digits_len: int) -> None: + def render( + self, + step: int, + elapsed: float, + avg_step: float, + digits_len: int, + current_calc: float, + last_calc: float, + computing: bool, + ) -> None: self.win.erase() _, cols = self.win.getmaxyx() cols = max(1, cols) @@ -23,6 +32,7 @@ class HeaderView: " Collatz Stream Visualizer ", f" Step {step:,} • Digits {digits_len:,}", f" Elapsed {elapsed:8.2f}s • Avg {avg_step:8.5f}s", + f" Current calc {current_calc:6.2f}s {'⏳' if computing else '✓'} • Last {last_calc:6.2f}s", " ▲ green = 3n+1 • ▼ red = /2 • ↑↓ number scroll • PgUp/PgDn logs • q quit", ] @@ -56,6 +66,9 @@ class GraphView: self._trim() self._render() + def redraw(self) -> None: + self._render() + def _trim(self) -> None: _, max_x = self.win.getmaxyx() visible = max(0, max_x - self.padding)