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

How to cut startup time in half while native=True. #3356

Open
EmberLightVFX opened this issue Jul 15, 2024 · 2 comments · May be fixed by #3365
Open

How to cut startup time in half while native=True. #3356

EmberLightVFX opened this issue Jul 15, 2024 · 2 comments · May be fixed by #3365
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@EmberLightVFX
Copy link

EmberLightVFX commented Jul 15, 2024

Description

I noticed while running my NiceGUI script with native=True it seemed all code ran twice even tho reload=False.
I have seen this information: #794 (comment)
but non of those fixed the problem. NiceGUI is still re-initializing twice.
After a lot of digging I found what caused this problem.

Webview must run in a main thread, thus it's executed using multiprocessing so it gets its own thread.
The problem here is that a new multiprocessing thread inherits a copy of the parent process's memory, including all global variables and imported modules.
There is a new initialization of NiceGUI for some reason as it now lives in a new thread.

I tried encapsulating the NiceGUI import and the ui.run code under if __name__ == "__main__": and then move all Webview functions into a complete separate package.
If I simply moved all Webview functions into a new file inside the NiceGUI package all NiceGUI stuff gets re-imported because of everything inside the init.py

With these changes I finally get a window popping up directly after the first ui.run execution, making it as fast as native=False and cuts my startup time in half!

I'm not super familiar with NiceGUI's source code but these are my thoughts on how to implement this fix.

The easiest way is to move all Webview code into a separate package, maybe NiceGUI_native_view or something like that.

The harder way (but the one I personally like more) is to make sure nothing gets initialized on NiceGUI's import. This would require quite a refactor of the code but would give a lot of benefits.
More than fixing my problem above this would also make sure the GUI is only initialized when it's needed. The app I'm currently building works both in the terminal and with a GUI. Right now NiceGUI gets initialized even if I'm simply running it in the terminal. You wouldn't need to add if __name__ == "__main__": at a bunch of places to make sure NiceGUI isn't imported and used.

There would ether be a bunch of checks everywhere in the source code to see if NiceGUI has bin initialized. If not, initialize it. Functions would be in as good as all functions within __init__.py

The other way would be to add a app.initialize() function that would do this for us. The problem here is that all existing usages of NiceGUI would break and would need to be updated to add app.initialize() if they want to use the latest version of NiceGUI.

My fix takes my execution time from 6 seconds down to 3 seconds!

I would love to hear thoughts about this and if there is any interest for a PR with any of these fixes.


These are my test-scripts:
Slow script:

import time

start = time.time()

from nicegui import app, ui

def runme():
    app.shutdown()

app.on_startup(runme)
ui.textarea("Hello!")
ui.run(
    reload=False,
    show=False,
    native=True,
)
print(time.time() - start)

Fast script:

import time

start = time.time()
if __name__ == "__main__":
    from nicegui import app, ui


def runme():
    app.shutdown()


if __name__ == "__main__":
    app.on_startup(runme)
    ui.textarea("Hello!")
    ui.run(
        reload=False,
        show=False,
        native=True,
    )
print(time.time() - start)

window.py
Create a new package called testtest with this file within (+ an empty __init__.py)

from __future__ import annotations

import multiprocessing as mp
import queue
import socket
import tempfile
import time
from threading import Event, Thread
from typing import Any, Callable, Dict, List, Tuple

import webview

def is_port_open(host: str, port: int) -> bool:
    """Check if the port is open by checking if a TCP connection can be established."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        sock.connect((host, port))
    except (ConnectionRefusedError, TimeoutError):
        return False
    except Exception:
        return False
    else:
        return True
    finally:
        sock.close()

def _open_window(
    host: str, port: int, title: str, width: int, height: int, fullscreen: bool, frameless: bool, window_args, settings, start_args,
    method_queue: mp.Queue, response_queue: mp.Queue,
) -> None:
    print("hejhejehej")
    while not is_port_open(host, port):
        time.sleep(0.1)

    window_kwargs = {
        'url': f'http://{host}:{port}',
        'title': title,
        'width': width,
        'height': height,
        'fullscreen': fullscreen,
        'frameless': frameless,
        **window_args,
    }
    webview.settings.update(**settings)
    window = webview.create_window(**window_kwargs)
    closed = Event()
    window.events.closed += closed.set
    _start_window_method_executor(window, method_queue, response_queue, closed)
    webview.start(storage_path=tempfile.mkdtemp(), **start_args)


def _start_window_method_executor(window: webview.Window,
                                  method_queue: mp.Queue,
                                  response_queue: mp.Queue,
                                  closed: Event) -> None:
    def execute(method: Callable, args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> None:
        try:
            response = method(*args, **kwargs)
            if response is not None or 'dialog' in method.__name__:
                response_queue.put(response)
        except Exception:
            pass

    def window_method_executor() -> None:
        pending_executions: List[Thread] = []
        while not closed.is_set():
            try:
                method_name, args, kwargs = method_queue.get(block=False)
                if method_name == 'signal_server_shutdown':
                    if pending_executions:
                        while pending_executions:
                            pending_executions.pop().join()
                elif method_name == 'get_always_on_top':
                    response_queue.put(window.on_top)
                elif method_name == 'set_always_on_top':
                    window.on_top = args[0]
                elif method_name == 'get_position':
                    response_queue.put((int(window.x), int(window.y)))
                elif method_name == 'get_size':
                    response_queue.put((int(window.width), int(window.height)))
                else:
                    method = getattr(window, method_name)
                    if callable(method):
                        pending_executions.append(Thread(target=execute, args=(method, args, kwargs)))
                        pending_executions[-1].start()
                    else:
                        pass
            except queue.Empty:
                time.sleep(0.016)  # NOTE: avoid issue https://github.com/zauberzeug/nicegui/issues/2482 on Windows
            except Exception:
                pass

    Thread(target=window_method_executor).start()

Modify native_module.py
Delete the _open_window function and add from testtest import _open_window in the top

@EmberLightVFX EmberLightVFX changed the title Cut startup time in half while native=True. How to cut startup time in half while native=True. Jul 15, 2024
@falkoschindler
Copy link
Contributor

Thanks for bringing this up, @EmberLightVFX!
Cutting the startup time for native apps in half would indeed be a great improvement.

We're struggling to understand how your testtest module works. Maybe you can create a pull request so we play around with the code? Even though publishing a separate PyPI package wouldn't be desirable, it might be a starting point for a less drastic solution. Thanks!

@EmberLightVFX EmberLightVFX linked a pull request Jul 17, 2024 that will close this issue
@EmberLightVFX
Copy link
Author

EmberLightVFX commented Jul 17, 2024

@falkoschindler I made a PR with the test environment: #3365
I hope it works for you.
I added two tests. One slow (how it currently works) and one fast for how you need to write it with my separarte-module fix.
I named the separate module temp_webview that lives in the root dir.

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

@falkoschindler falkoschindler added the enhancement New feature or request label Jul 19, 2024
@falkoschindler falkoschindler linked a pull request Jul 19, 2024 that will close this issue
@falkoschindler falkoschindler added the help wanted Extra attention is needed label Jul 31, 2024
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 a pull request may close this issue.

2 participants