Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce global app-idle state/callback #4899

Closed
kdkavanagh opened this issue Aug 19, 2024 · 3 comments
Closed

Introduce global app-idle state/callback #4899

kdkavanagh opened this issue Aug 19, 2024 · 3 comments

Comments

@kdkavanagh
Copy link

kdkavanagh commented Aug 19, 2024

The current Idle events are helpful for individual widgets but arent great for notifying the application code that the app overall is idle. It would be nice to introduce a callback, possibly at the Screen or the App level which is invoked when all message busses are empty and perhaps have been empty for some small period of time.

This feature would allow apps to initiate heavier tasks while the user isnt not currently interacting with any part of the application (vs just a single widget). An example of such a task would be for the app to manually initiate a garbage collection (#4888).

An example implementation might look something like this, applied just to the app/screen's message_pump. I believe this works to capture all message_pumps being idle, as all child message_pumps are awaited before the root message_pump comes back around to request the next message to dispatch (please correct me if this isnt right!).

    async def _process_messages_loop(self) -> None:
        """Process messages until the queue is closed."""
        _rich_traceback_guard = True
        self._thread_id = threading.get_ident()
        while True:
            try:
                if is_main_app_pump:
                    try:
                        global_idle_timeout_seconds = 3
                        start, message = await asyncio.wait_for(self._get_message(), global_idle_timeout_seconds)
                    except asyncio.TimeoutError:
                        # Emit GlobalIdle event here!
                        start, message = await self._get_message()
Copy link

Thank you for your issue. Give us a little time to review it.

PS. You might want to check the FAQ if you haven't done so already.

This is an automated reply, generated by FAQtory

@willmcgugan
Copy link
Collaborator

It's surprising that GC is noticeable at all. Can you produce an MRE that demonstrates the long GC pauses?

With regards to a global idle, I'm not sure that is something that should go in to the core lib, purely because it is hard to define what "idle" is. A blinking cursor will generate a few messages every second for example.

But it would be reasonably straightforward to add to an app. Possibly by wrapping App._dispatch_message

@kdkavanagh
Copy link
Author

RE: GCs, you could give this script a try. It currently disables GCs before running; would need to comment that line out: https://github.com/tconbeer/textual-fastdatatable/blob/main/src/scripts/benchmark.py

I think the issue arises because python's GCs are triggered by the count of objects created, not the actual heap size used like most other GC-enabled languages. (Concretely, this means that allocating N small objects has the same impact as N large objects). Redrawing an entire data table ends up creating many of those segment/strip types.

Good point RE: blinking cursor, and in any case, I think I was wrong about this statement above: "all child message_pumps are awaited before the root message_pump". Seems each individual message pump is its own asyncio task, and each awaits their own queue indply. This means I'd need a different impl to track all message_pumps rather than just the App's pump. (i.e only mutating the App._dispatch_message impl wouldnt be sufficient.

As a result, I've been toying with a different implementation, where each message pump sets a bool busy=True immediately after receiving a message from its queue (in other words, a bool representing "is dispatching" or "is not awaiting next message") , and setting busy=false just before awaiting the next message. If that bool is bubbled up to the parent pump, the parent can (recursively) track the number of currently-busy children. Once you get up to the root level, you have an indicator if any message pump in the tree is currently busy. Then a custom App._get_message implementation works, utilizing asyncio.wait_for to query if any children are busy after the App's pump has been idle for a period of time

@Textualize Textualize locked and limited conversation to collaborators Aug 21, 2024
@willmcgugan willmcgugan converted this issue into discussion #4910 Aug 21, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants