Description
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