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
Draft
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
20 changes: 20 additions & 0 deletions _fast_startup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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)
19 changes: 19 additions & 0 deletions _slow_startup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import time

start = time.time()

from nicegui import app, ui # noqa


def runme():
app.shutdown()


# app.on_startup(runme)
ui.textarea('Hello!')
ui.run(
reload=False,
show=False,
native=True,
)
print(time.time() - start)
97 changes: 11 additions & 86 deletions nicegui/native/native_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,98 +2,18 @@

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

from .. import core, helpers, optional_features
from temp_webview.window import open_window, register

from .. import core, optional_features
from ..logging import log
from ..server import Server
from . import native

try:
with warnings.catch_warnings():
# webview depends on bottle which uses the deprecated CGI function (https://github.com/bottlepy/bottle/issues/1403)
warnings.filterwarnings('ignore', category=DeprecationWarning)
import webview
optional_features.register('webview')
except ModuleNotFoundError:
pass


def _open_window(
host: str, port: int, title: str, width: int, height: int, fullscreen: bool, frameless: bool,
method_queue: mp.Queue, response_queue: mp.Queue,
) -> None:
while not helpers.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,
**core.app.native.window_args,
}
webview.settings.update(**core.app.native.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(), **core.app.native.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:
log.exception(f'error in window.{method.__name__}')

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:
log.warning('shutdown is possibly blocked by opened dialogs like a file picker')
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:
log.error(f'window.{method_name} is not callable')
except queue.Empty:
time.sleep(0.016) # NOTE: avoid issue https://github.com/zauberzeug/nicegui/issues/2482 on Windows
except Exception:
log.exception(f'error in window.{method_name}')

Thread(target=window_method_executor).start()


def activate(host: str, port: int, title: str, width: int, height: int, fullscreen: bool, frameless: bool) -> None:
"""Activate native mode."""
Expand All @@ -105,14 +25,19 @@ def check_shutdown() -> None:
time.sleep(0.1)
_thread.interrupt_main()

webview_registered = register()
if webview_registered:
optional_features.register('webview')

if not optional_features.has('webview'):
log.error('Native mode is not supported in this configuration.\n'
'Please run "pip install pywebview" to use it.')
sys.exit(1)

mp.freeze_support()
args = host, port, title, width, height, fullscreen, frameless, native.method_queue, native.response_queue
process = mp.Process(target=_open_window, args=args, daemon=True)
args = (host, port, title, width, height, fullscreen, frameless, native.method_queue, native.response_queue,
core.app.native.window_args, core.app.native.settings, core.app.native.start_args)
process = mp.Process(target=open_window, args=args, daemon=True)
process.start()

Thread(target=check_shutdown, daemon=True).start()
Expand Down
Empty file added temp_webview/__init__.py
Empty file.
111 changes: 111 additions & 0 deletions temp_webview/window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from __future__ import annotations

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

import webview

log: logging.Logger = logging.getLogger('nicegui')


def register() -> bool:
try:
with warnings.catch_warnings():
# webview depends on bottle which uses the deprecated CGI function (https://github.com/bottlepy/bottle/issues/1403)
warnings.filterwarnings('ignore', category=DeprecationWarning)
import webview # noqa
return True
# optional_features.register('webview')
except ModuleNotFoundError:
return False


def open_window(
host: str, port: int, title: str, width: int, height: int, fullscreen: bool, frameless: bool,
method_queue: mp.Queue, response_queue: mp.Queue, window_args, settings, start_args,
) -> None:
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:
log.exception(f'error in window.{method.__name__}')

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:
log.warning('shutdown is possibly blocked by opened dialogs like a file picker')
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:
log.error(f'window.{method_name} is not callable')
except queue.Empty:
time.sleep(0.016) # NOTE: avoid issue https://github.com/zauberzeug/nicegui/issues/2482 on Windows
except Exception:
log.exception(f'error in window.{method_name}')

Thread(target=window_method_executor).start()


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()