Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 2 additions & 47 deletions src/toad/frame_presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,15 @@
from __future__ import annotations

import asyncio
import sys
from abc import abstractmethod
from collections.abc import Callable
from functools import partial
from weakref import ref

from agent_comms.declared_family import DeclaredFamily
from textual.driver import Driver
from textual.widget import Widget
from toad.screens.workspace import WorkspaceScreen


class FrameFlush(DeclaredFamily, affix="Flush"):
driver_type: type[Driver]

def __init__(self, driver: Driver):
self.driver = driver

@classmethod
def for_driver(cls, driver: Driver) -> FrameFlush:
declarations = {kind.driver_type: kind for kind in cls.members_with(cls)}
return next(declarations[ancestor](driver) for ancestor in type(driver).__mro__ if ancestor in declarations)

@abstractmethod
def submit(self, callback: Callable[[], None]) -> None: ...


class SynchronousFrameFlush(FrameFlush):
driver_type = Driver

def submit(self, callback: Callable[[], None]) -> None:
callback()


if sys.platform != "win32":
# The framework's POSIX driver imports termios; its declaration exists only
# at that external platform boundary, like Textual's own driver selection.
from textual.drivers.linux_driver import LinuxDriver

class TerminalFrameFlush(FrameFlush):
driver_type = LinuxDriver

def submit(self, callback: Callable[[], None]) -> None:
loop = asyncio.get_running_loop()

def written():
try:
loop.call_soon_threadsafe(callback)
except RuntimeError:
pass # The application loop closed after its terminal write.

self.driver.call_after_flush(written)


class FrameState(DeclaredFamily, affix="Frame"):
ready = False

Expand All @@ -66,7 +21,7 @@ def scene(self):
def displayed(self, frame, deferred):
receipt = WritingFrame(self.scene, deferred)
frame.state = receipt
FrameFlush.for_driver(frame.screen.app._driver).submit(partial(frame.written, receipt))
frame.screen.app._driver.call_after_flush(partial(frame.written, receipt))

def defer(self, frame, owner, callback):
frame.callbacks[owner, callback] = None
Expand Down Expand Up @@ -178,7 +133,7 @@ def defer(self, owner: Widget, callback: Callable[[], object]) -> None:
def flush_owner(self, owner: Widget, callback: Callable[[], object]) -> None:
"""Native sender admission precedes the original terminal writer join."""
if (owner, callback) in self.callbacks:
FrameFlush.for_driver(self.screen.app._driver).submit(
self.screen.app._driver.call_after_flush(
partial(self.release, owner, callback, self.state.scene))

def release(self, owner: Widget, callback: Callable[[], object], scene: PendingFrame) -> None:
Expand Down
3 changes: 1 addition & 2 deletions tests/native_session_retention_pilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ def _display(self, screen, renderable):
if all(marker in paint for marker in self.observed_expected):
self.observed_frames.append((time.monotonic() - self.observed_started) * 1000)
from functools import partial
from toad.frame_presentation import FrameFlush
FrameFlush.for_driver(self._driver).submit(partial(
self._driver.call_after_flush(partial(
self.record_written_destination, self.observed_started,
self.observed_destination))

Expand Down
4 changes: 1 addition & 3 deletions tools/performance/capture_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ async def acquire_and_capture():
write_json(prefix + ".json", {"pid": expected_pid, "scope": "driver frame trace only"})
return
if wait_history_seconds:
from toad.frame_presentation import FrameFlush

if not wait_history_thread:
raise ValueError("Visible-history waiting requires the intended thread")

Expand Down Expand Up @@ -118,7 +116,7 @@ def acknowledge():
written.set_result(None)

app.call_after_refresh(
lambda: FrameFlush.for_driver(app._driver).submit(acknowledge))
lambda: app._driver.call_after_flush(acknowledge))
await written
if visible_history_ready():
write_json(prefix + "-wait.json", {
Expand Down
11 changes: 9 additions & 2 deletions tools/performance/sidebar_validation_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ def observed_exception(self, error):
(Conversation, Conversation.on_agent_ready),
]
if os.environ.get("TOAD_VALIDATION_OPEN_STAGES"):
from toad.acp.agent_process import AgentProcess
from toad.acp.agent_session import AgentSession
from textual.widget import Widget
from toad.sidebar_observation import SidebarObservation
from toad.sidebar_projection import SidebarProjection
Expand All @@ -206,6 +208,11 @@ def observed_exception(self, error):
(SidebarProjection, SidebarProjection.rebuild),
(SessionsTabs, SessionsTabs._sync_tabs),
(NavigationReader, NavigationReader.read),
(AgentProcess, AgentProcess.start),
(AgentSession, AgentSession.initialize),
(AgentSession, AgentSession.load),
(AgentSession, AgentSession.touch),
(AgentSession, AgentSession.run),
))
constructor = Widget.__init__
preprocess = Widget._pre_process
Expand Down Expand Up @@ -238,6 +245,7 @@ async def measured_navigation(self, *args, _function=original_method, _name=meth
return await _function(self, *args, **kwargs)
finally:
record("navigation_stage", stage=_name, begin_ns=begin,
function=_function.__qualname__, object_id=id(self),
duration_ms=(time.monotonic_ns()-begin)/1e6,
mode=args[0] if _name == "_switch_mode_ready" and args else
self.app.current_mode if hasattr(self, "app") else None,
Expand Down Expand Up @@ -373,7 +381,6 @@ def observe_useful_output(cls, app, screen, update, begin, displayed, cpu):
Scalars alone survive the callback; no widget/render-cache graph is
retained by the observer. Headless output remains source evidence.
"""
from toad.frame_presentation import FrameFlush
from toad.widgets.conversation import Conversation, Window
from toad.widgets.transcript_history import TranscriptHistory
from toad.widgets.viewport_body import MeasuredViewportBody
Expand Down Expand Up @@ -422,7 +429,7 @@ def observe_useful_output(cls, app, screen, update, begin, displayed, cpu):
record("body_output", **values)
if not app.is_headless:
mode = app.current_mode
FrameFlush.for_driver(app._driver).submit(
app._driver.call_after_flush(
lambda: record("body_output_written", display_begin_ns=begin,
mode=mode, current_mode=app.current_mode))

Expand Down
Loading