Skip to content
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

Test environment for faster startup #3365

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

EmberLightVFX
Copy link

This is a PR for a test environment for this issue: #3356

I couldn't get a correct NiceGUI dev-environment up and running but I hope this PR works for you or at least shows you the setup.

@falkoschindler
Copy link
Contributor

Thanks for the pull request, @EmberLightVFX!

When I run _slow_startup.py or _fast_startup.py, I get this exception (after commenting out the if not optional_features.has('webview') block in native_mode.py):

NiceGUI ready to go on http://localhost:8000
ERROR:    Traceback (most recent call last):
  File "/Users/falko/Library/Caches/pypoetry/virtualenvs/nicegui-85pGnwEl-py3.11/lib/python3.11/site-packages/starlette/routing.py", line 743, in lifespan
    await receive()
  File "/Users/falko/Library/Caches/pypoetry/virtualenvs/nicegui-85pGnwEl-py3.11/lib/python3.11/site-packages/uvicorn/lifespan/on.py", line 137, in receive
    return await self.receive_queue.get()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/[email protected]/3.11.7/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/queues.py", line 158, in get
    await getter
asyncio.exceptions.CancelledError

0.9242219924926758

And when I run a simple script like

from nicegui import ui

ui.label('Hello, world!')

ui.run(native=True)

I get

Process Process-1:
Traceback (most recent call last):
  File "/opt/homebrew/Cellar/[email protected]/3.11.7/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/process.py", line 314, in _bootstrap
    self.run()
  File "/opt/homebrew/Cellar/[email protected]/3.11.7/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
TypeError: _open_window() missing 3 required positional arguments: 'start_args', 'method_queue', and 'response_queue'
NiceGUI ready to go on http://localhost:8000
/opt/homebrew/Cellar/[email protected]/3.11.7/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/resource_tracker.py:254: UserWarning: resource_tracker: There appear to be 6 leaked semaphore objects to clean up at shutdown                  
  warnings.warn('resource_tracker: There appear to be %d '

So I'm a bit lost here. Can you or anyone else help to get this working? Thanks!

@falkoschindler falkoschindler added help wanted Extra attention is needed enhancement New feature or request labels Jul 19, 2024
@falkoschindler falkoschindler linked an issue Jul 19, 2024 that may be closed by this pull request
@EmberLightVFX
Copy link
Author

@falkoschindler I most have accidentally pasted some arguments on the _open_window function. I removed them and it should work for you now.

@EmberLightVFX EmberLightVFX marked this pull request as draft July 21, 2024 09:32
@falkoschindler
Copy link
Contributor

I'm still gettting a CancelledError when running either of both scripts:

ERROR:    Traceback (most recent call last):
  File "/Users/falko/.pyenv/versions/3.11.7/lib/python3.11/site-packages/starlette/routing.py", line 743, in lifespan
    await receive()
  File "/Users/falko/.pyenv/versions/3.11.7/lib/python3.11/site-packages/uvicorn/lifespan/on.py", line 137, in receive
    return await self.receive_queue.get()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/falko/.pyenv/versions/3.11.7/lib/python3.11/asyncio/queues.py", line 158, in get
    await getter
asyncio.exceptions.CancelledError

And there are several undefined symbols like window_args, settings and start_args in _open_window.

@EmberLightVFX
Copy link
Author

I have no idea how my files got so scrambled...
I re-did everything and I finally also got the development environment up and running so I could test it on that environment.

For some reason I had to comment out app.on_startup(runme) from my test scripts. They gave me a asyncio.exceptions.CancelledError error. I get this error on the latest main branch without any of my fixes.
Any way, as soon as the GUI window opens, close it and you will see the total time for the process to open and close everything in the terminal.

For me slow takes 13 seconds and fast takes 7 seconds to process.

@falkoschindler
Copy link
Contributor

@EmberLightVFX You seem to be measuring the time until ui.run terminates, which mainly depends on when the user closes the window.

When I measure the time within a startup handler

app.on_startup(lambda: print(time.time() - start))

there is no significant difference between the slow and the fast script. But I guess the measurement is still flawed, because the start time is set separately in each process. So it's hart to measure the true startup time.

@EmberLightVFX
Copy link
Author

@EmberLightVFX You seem to be measuring the time until ui.run terminates, which mainly depends on when the user closes the window.

That's why I had the "on_startup" command close the UI with app.shutdown() for me as soon as it showed up, but it was broken for me with the lateat main commits.

What you can do is to simply use your phones stopwatch and measure how long it takes from when you start the script till you see the UI. "Slow" will take around double the time to start.

@falkoschindler
Copy link
Contributor

Ok, I can confirm that _fast_startup.py is indeed faster than _slow_startup.py. It takes only 1-2 seconds on my machine, so it's hard to measure. But the difference is clearly noticeable.

I'm still struggling to understand why the location of window.py plays such a great role. What exactly is taking up so much time? Is it the import of webview? And how does it relate to window.py being located in a different package?

@EmberLightVFX
Copy link
Author

EmberLightVFX commented Jul 26, 2024

native_mode.py imports some things with from .. import xxxxx
What python does is check for a __import__.py file in .. (the root nicegui dir in this case) and crawls everything within it if found. One of the items that gets checked in the root __import__.py is app from nicegui.py
When python checks the nicegui.py file it will run every root-code within. That means core.app = app = App(default_response_class=NiceGUIJSONResponse, lifespan=_lifespan) and all other root-code runs resulting in NiceGUI re-initializes itself in the new thread that native_mode creates for WebView.

By moving all code that the new thread needs to run into a new package/library there is no need to import anything from NiceGUI as it doesn't need anything from NiceGUI, thus not re-initializing anything in the new thread, thus cutting the startup time in half.

@EmberLightVFX
Copy link
Author

EmberLightVFX commented Jul 26, 2024

Another solution could be to wrap all that root-code within a def initialize() function that the user needs to add before they start using NiceGUI in their code. This might be something for NiceGUI 2.0 as it would break everyones current code.

@falkoschindler
Copy link
Contributor

Very interesting... Unfortunately, we're currently focusing on some other issues that need to get done for the upcoming 2.0 release. The approach described in this PR doesn't feel quite right. Hopefully we can avoid splitting the NiceGUI package and still avoid re-loading everything. Maybe someone from the community likes to experiment with this branch and can come up with an idea how to speed up native mode more elegantly.

@EmberLightVFX
Copy link
Author

Yeah splitting the package into two packages isn't ideal.
Would requireing a ui.init() in the start of a users script be acceptable? I could whip something up like that

@falkoschindler
Copy link
Contributor

Would requireing a ui.init() in the start of a users script be acceptable?

We're still hoping for a solution without such an init call. And even that's impossible, the need for ui.init() is barely acceptable, since it heavily breaks all existing apps and adds boilerplate code for everyone, just to speed-up the native mode. There has to be a better way... 🤞🏻

@EmberLightVFX
Copy link
Author

Got it! I'll try and figure something out!

@tgbl-mk
Copy link

tgbl-mk commented Nov 4, 2024

I've also been trying to find a workaround for this issue as I have some lengthy startup scripts and logic that I'd rather not run twice. A workaround I've found is to create a lock file after the first pass of the file, which can be used to block code execution on the second pass that occurs when native=True

I'll admit I'm not too familiar with what's happening under the hood in niceui, but with the code snippet below everything appears to be working as intended. (which is a significantly reduced example of the actuall application I'm working on).

This would obviously not work if reload=True, though some additional logic could resolve that.

from nicegui import ui
from pathlib import Path

class test_ui():
    def __init__(self, native: bool) -> None:
        print("Initialising UI")
        self.native = native
        print(f"native = {self.native}")

        self.lock_file = Path(__file__).resolve().parent / "app.lock"
        if self.native:
            if not self.lock_file.is_file():
                print("Lock file not present!")
                with open(self.lock_file, "w") as _:
                    pass
                self.compose()
            else:
                print("lock file present!")
                self.lock_file.unlink()
        else:
            self.compose()

    def compose(self) -> None:
        print("Composing UI")
        ui.label("Hello World!")
        ui.button("Ding!", on_click=self.on_ding)

    def on_ding(self) -> None:
        ui.notify("Dong!")

    def run(self) -> None:
        if (self.native and self.lock_file.is_file()) or not self.native:
            print("Running UI")
            ui.run(port= 1000, native=self.native, reload=False)


test_ui(True).run()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

How to cut startup time in half while native=True.
3 participants