-
Notifications
You must be signed in to change notification settings - Fork 2k
Running taipy in notebooks is slow #2704
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
base: develop
Are you sure you want to change the base?
Changes from 6 commits
c4c073f
ed9be87
82298f3
647bfad
a372ab0
e1746b4
4f3fcce
2b95d41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -255,10 +255,13 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover | |
| elif key == "port" and str(value).strip() == "auto": | ||
| config["port"] = "auto" | ||
| else: | ||
| config[key] = value if config.get(key) is None else type(config.get(key))(value) # type: ignore[reportCallIssue] | ||
| config[key] = value if config.get(key) is None else type(config.get(key))( | ||
| value) # type: ignore[reportCallIssue] | ||
| except Exception as e: | ||
| _warn( | ||
| f"Invalid keyword arguments value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", # noqa: E501 | ||
| f"Invalid keyword arguments value in Gui.run(): {key} - {value}. " | ||
| f"Unable to parse value to the correct type", | ||
|
||
| # noqa: E501 | ||
|
||
| e, | ||
| ) | ||
| # Load config from env file | ||
|
|
@@ -273,10 +276,13 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover | |
| if isinstance(config[key], bool): | ||
| config[key] = _is_true(value) | ||
| else: | ||
| config[key] = value if config[key] is None else type(config[key])(value) # type: ignore[reportCallIssue] | ||
| config[key] = value if config[key] is None else type(config[key])( | ||
| value) # type: ignore[reportCallIssue] | ||
| except Exception as e: | ||
| _warn( | ||
| f"Invalid env value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", # noqa: E501 | ||
| f"Invalid env value in Gui.run(): {key} - {value}. " | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. idem
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. resolved
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. idem |
||
| f"Unable to parse value to the correct type", | ||
| # noqa: E501 | ||
| e, | ||
| ) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,6 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import contextlib | ||
| import logging | ||
| import os | ||
| import pathlib | ||
|
|
@@ -142,6 +141,7 @@ def _get_default_handler( | |
| base_url: str, | ||
| ) -> Blueprint: | ||
| taipy_bp = Blueprint("Taipy", __name__, static_folder=static_folder, template_folder=template_folder) | ||
|
|
||
| # Serve static react build | ||
|
|
||
| @taipy_bp.route("/", defaults={"path": ""}) | ||
|
|
@@ -171,11 +171,14 @@ def my_index(path): | |
| ) | ||
| except Exception: | ||
| raise RuntimeError( | ||
| "Something is wrong with the taipy-gui front-end installation. Check that the js bundle has been properly built (is Node.js installed?)." # noqa: E501 | ||
| "Something is wrong with the taipy-gui front-end installation. " | ||
| "Check that the js bundle has been properly built (is Node.js installed?)." | ||
| # noqa: E501 | ||
|
||
| ) from None | ||
|
|
||
| if path == "taipy.status.json": | ||
| return self.direct_render_json(self._gui._serve_status(pathlib.Path(template_folder) / path)) # type: ignore[attr-defined] | ||
| return self.direct_render_json( | ||
| self._gui._serve_status(pathlib.Path(template_folder) / path)) # type: ignore[attr-defined] | ||
| if (file_path := str(os.path.normpath((base_path := static_folder + os.path.sep) + path))).startswith( | ||
| base_path | ||
| ) and os.path.isfile(file_path): | ||
|
|
@@ -185,25 +188,26 @@ def my_index(path): | |
| if ( | ||
| path.startswith(f"{k}/") | ||
| and ( | ||
| file_path := str(os.path.normpath((base_path := v + os.path.sep) + path[len(k) + 1 :])) | ||
| ).startswith(base_path) | ||
| file_path := str(os.path.normpath((base_path := v + os.path.sep) + path[len(k) + 1:])) | ||
| ).startswith(base_path) | ||
| and os.path.isfile(file_path) | ||
| ): | ||
| return send_from_directory(base_path, path[len(k) + 1 :]) | ||
| return send_from_directory(base_path, path[len(k) + 1:]) | ||
| if ( | ||
| hasattr(__main__, "__file__") | ||
| and ( | ||
| file_path := str( | ||
| os.path.normpath((base_path := os.path.dirname(__main__.__file__) + os.path.sep) + path) | ||
| ) | ||
| ).startswith(base_path) | ||
| file_path := str( | ||
| os.path.normpath((base_path := os.path.dirname(__main__.__file__) + os.path.sep) + path) | ||
| ) | ||
| ).startswith(base_path) | ||
| and os.path.isfile(file_path) | ||
| and not self._is_ignored(file_path) | ||
| ): | ||
| return send_from_directory(base_path, path) | ||
| if ( | ||
| ( | ||
| file_path := str(os.path.normpath((base_path := self._gui._root_dir + os.path.sep) + path)) # type: ignore[attr-defined] | ||
| file_path := str(os.path.normpath((base_path := self._gui._root_dir + os.path.sep) + path)) | ||
| # type: ignore[attr-defined] | ||
| ).startswith(base_path) | ||
| and os.path.isfile(file_path) | ||
| and not self._is_ignored(file_path) | ||
|
|
@@ -237,7 +241,15 @@ def test_request_context(self, path, data=None): | |
|
|
||
| def _run_notebook(self): | ||
| self._is_running = True | ||
| self._ws.run(self._server, host=self._host, port=self._port, debug=False, use_reloader=False) | ||
| self._ws.run( | ||
| self._server, | ||
| host=self._host, | ||
| port=self._port, | ||
| debug=False, | ||
| use_reloader=False, | ||
| allow_unsafe_werkzeug=True, | ||
| log_output=True | ||
| ) | ||
|
|
||
| def _get_async_mode(self) -> str: | ||
| return self._ws.async_mode # type: ignore[attr-defined] | ||
|
|
@@ -380,9 +392,14 @@ def run( | |
| if not self.is_running_from_reloader() and self._gui._get_config("run_browser", False): # type: ignore[attr-defined] | ||
| webbrowser.open(client_url or server_url, new=2) | ||
| if _is_in_notebook() or run_in_thread: | ||
| self._thread = KThread(target=self._run_notebook) | ||
| self._thread = KThread( | ||
| target=self._run_notebook, | ||
| daemon=True, | ||
| name=f"TaipyGUI-{port}" | ||
| ) | ||
| self._thread.start() | ||
| return | ||
|
|
||
| self._is_running = True | ||
| run_config = { | ||
| "app": self._server, | ||
|
|
@@ -407,17 +424,44 @@ def is_running(self): | |
| def stop_thread(self): | ||
| if hasattr(self, "_thread") and self._thread.is_alive() and self._is_running: | ||
| self._is_running = False | ||
| with contextlib.suppress(Exception): | ||
|
|
||
| try: | ||
| if self._get_async_mode() == "gevent": | ||
| if self._ws.wsgi_server is not None: # type: ignore[attr-defined] | ||
| self._ws.wsgi_server.stop() # type: ignore[attr-defined] | ||
| if hasattr(self._ws, 'wsgi_server') and self._ws.wsgi_server is not None: | ||
| self._ws.wsgi_server.stop() | ||
| else: | ||
| self._thread.kill() | ||
| else: | ||
| self._thread.kill() | ||
| except Exception as e: | ||
| _TaipyLogger._get_logger().warning(f"Error stopping thread: {e}") | ||
|
|
||
| timeout_start = time.time() | ||
| timeout_duration = 5.0 # 5 seconds timeout | ||
|
|
||
| while _is_port_open(self._host, self._port): | ||
| if time.time() - timeout_start > timeout_duration: | ||
| _TaipyLogger._get_logger().warning( | ||
| f"Port {self._port} still occupied after {timeout_duration}s timeout" | ||
| ) | ||
| break | ||
| time.sleep(0.1) | ||
|
|
||
| def __del__(self): | ||
| try: | ||
| if hasattr(self, '_thread') and self._thread and self._thread.is_alive(): | ||
| self.stop_thread() | ||
| if hasattr(self, '_proxy'): | ||
| self.stop_proxy() | ||
| except Exception: | ||
| pass | ||
|
|
||
| def stop_proxy(self): | ||
| if hasattr(self, "_proxy"): | ||
| self._proxy.stop() | ||
| try: | ||
| self._proxy.stop() | ||
| except Exception as e: | ||
| _TaipyLogger._get_logger().warning(f"Error stopping proxy: {e}") | ||
| finally: | ||
| if hasattr(self, "_proxy"): | ||
| delattr(self, "_proxy") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,9 +10,10 @@ | |
| # specific language governing permissions and limitations under the License. | ||
|
|
||
| import contextlib | ||
| import threading | ||
| import typing as t | ||
| import warnings | ||
| from threading import Thread | ||
| from threading import Event, Thread | ||
| from urllib.parse import quote as urlquote | ||
| from urllib.parse import urlparse | ||
|
|
||
|
|
@@ -21,6 +22,7 @@ | |
| from twisted.web.resource import Resource | ||
| from twisted.web.server import NOT_DONE_YET, Site | ||
|
|
||
| from .._warnings import _warn | ||
| from .is_port_open import _is_port_open | ||
|
|
||
| # flake8: noqa: E402 | ||
|
|
@@ -29,10 +31,11 @@ | |
| warnings.filterwarnings( | ||
| "ignore", | ||
| category=UserWarning, | ||
| message="You do not have a working installation of the service_identity module: 'No module named 'service_identity''.*", # noqa: E501 | ||
| message="You don't have a working installation of the service_identity module: " | ||
| "'No module named 'service_identity''.*", | ||
| # noqa: E501 | ||
|
||
| ) | ||
|
|
||
|
|
||
| if t.TYPE_CHECKING: | ||
| from ..gui import Gui | ||
|
|
||
|
|
@@ -93,23 +96,80 @@ def __init__(self, gui: "Gui", listening_port: int) -> None: | |
| self._listening_port = listening_port | ||
| self._gui = gui | ||
| self._is_running = False | ||
| self._thread: t.Optional[Thread] = None | ||
| self._stop_event = Event() | ||
| self._reactor_thread_id: t.Optional[int] = None | ||
|
|
||
| def run(self): | ||
| if self._is_running: | ||
| if self._is_running and self._thread and self._thread.is_alive(): | ||
| return | ||
|
|
||
| host = self._gui._get_config("host", "127.0.0.1") | ||
| port = self._listening_port | ||
|
|
||
| if _is_port_open(host, port): | ||
| raise ConnectionError( | ||
| f"Port {port} is already opened on {host}. You have another server application running on the same port." # noqa: E501 | ||
| f"Port {port} is already opened on {host}. " | ||
| f"You have another server application running on the same port." | ||
| ) | ||
| site = Site(_TaipyReverseProxyResource(host, b"", self._gui)) | ||
| reactor.listenTCP(port, site) | ||
| Thread(target=reactor.run, args=(False,)).start() | ||
|
|
||
| self._thread = Thread( | ||
| target=self._run_reactor, | ||
| args=(host, port), | ||
| daemon=True, | ||
| name=f"TaipyNotebookProxy-{port}" | ||
| ) | ||
|
|
||
| self._stop_event.clear() | ||
| self._thread.start() | ||
| self._is_running = True | ||
|
|
||
| import time | ||
| time.sleep(0.1) | ||
|
|
||
| def _run_reactor(self, host: str, port: int): | ||
| try: | ||
| ident = threading.current_thread().ident | ||
| self._reactor_thread_id = ident if ident is not None else 0 | ||
| site = Site(_TaipyReverseProxyResource(host, b"", self._gui)) | ||
| reactor.listenTCP(port, site) | ||
|
|
||
| reactor.run(installSignalHandlers=False) | ||
|
|
||
| except Exception as e: | ||
| _warn(f"Reactor error: {e}") | ||
| finally: | ||
| self._is_running = False | ||
| self._reactor_thread_id = None | ||
|
|
||
| def stop(self): | ||
| if not self._is_running: | ||
| return | ||
|
|
||
| self._stop_event.set() | ||
| self._is_running = False | ||
| reactor.stop() | ||
|
|
||
| if (self._reactor_thread_id and | ||
| threading.current_thread().ident == self._reactor_thread_id): | ||
| reactor.stop() | ||
| else: | ||
|
|
||
| reactor.callFromThread(reactor.stop) | ||
|
|
||
| if self._thread and self._thread.is_alive(): | ||
| self._thread.join(timeout=2.0) | ||
|
|
||
| if self._thread.is_alive(): | ||
| _warn(f"Warning: Proxy thread {self._thread.name} did not terminate cleanly") | ||
|
|
||
| self._thread = None | ||
| self._reactor_thread_id = None | ||
|
|
||
| def is_alive(self) -> bool: | ||
| return (self._is_running and | ||
| self._thread is not None and | ||
| self._thread.is_alive()) | ||
|
|
||
| def __del__(self): | ||
| with contextlib.suppress(Exception): | ||
| self.stop() | ||
Uh oh!
There was an error while loading. Please reload this page.