diff --git a/reflex/app.py b/reflex/app.py index 921a66f7f..d4c831868 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -17,6 +17,7 @@ import sys import traceback from datetime import datetime from pathlib import Path +from timeit import default_timer as timer from types import SimpleNamespace from typing import ( TYPE_CHECKING, @@ -73,7 +74,7 @@ from reflex.components.core.client_side_routing import ( from reflex.components.core.sticky import sticky from reflex.components.core.upload import Upload, get_upload_dir from reflex.components.radix import themes -from reflex.config import environment, get_config +from reflex.config import ExecutorType, environment, get_config from reflex.event import ( _EVENT_FIELDS, Event, @@ -1062,10 +1063,23 @@ class App(MiddlewareMixin, LifespanMixin): app_wrappers[(1, "ToasterProvider")] = toast_provider with console.timing("Evaluate Pages (Frontend)"): + performance_metrics: list[tuple[str, float]] = [] for route in self._unevaluated_pages: console.debug(f"Evaluating page: {route}") + start = timer() self._compile_page(route, save_page=should_compile) + end = timer() + performance_metrics.append((route, end - start)) progress.advance(task) + console.debug( + "Slowest pages:\n" + + "\n".join( + f"{route}: {time * 1000:.1f}ms" + for route, time in sorted( + performance_metrics, key=lambda x: x[1], reverse=True + )[:10] + ) + ) # Add the optional endpoints (_upload) self._add_optional_endpoints() @@ -1157,51 +1171,69 @@ class App(MiddlewareMixin, LifespanMixin): ), ) + executor_type = environment.REFLEX_COMPILE_EXECUTOR.get() + # Use a forking process pool, if possible. Much faster, especially for large sites. # Fallback to ThreadPoolExecutor as something that will always work. - executor = None - if environment.REFLEX_COMPILE_USE_MAIN_THREAD.get(): - T = TypeVar("T") + if executor_type is None: + reflex_compile_processes = environment.REFLEX_COMPILE_PROCESSES.get() + reflex_compile_threads = environment.REFLEX_COMPILE_THREADS.get() + if ( + platform.system() not in ("Linux", "Darwin") + and reflex_compile_processes is not None + ): + console.warn("Multiprocessing is only supported on Linux and MacOS.") - class MainThreadExecutor: - def __enter__(self): - return self + if ( + platform.system() in ("Linux", "Darwin") + and reflex_compile_processes is not None + ): + executor_type = ExecutorType.PROCESS + elif reflex_compile_threads is not None: + executor_type = ExecutorType.THREAD + else: + executor_type = ExecutorType.MAIN_THREAD - def __exit__(self, *args): - pass + match executor_type: + case ExecutorType.PROCESS: + executor = concurrent.futures.ProcessPoolExecutor( + max_workers=environment.REFLEX_COMPILE_PROCESSES.get(), + mp_context=multiprocessing.get_context("fork"), + ) + case ExecutorType.THREAD: + executor = concurrent.futures.ThreadPoolExecutor( + max_workers=environment.REFLEX_COMPILE_THREADS.get() + ) + case ExecutorType.MAIN_THREAD: + T = TypeVar("T") - def submit( - self, fn: Callable[..., T], *args, **kwargs - ) -> concurrent.futures.Future[T]: - future_job = concurrent.futures.Future() - future_job.set_result(fn(*args, **kwargs)) - return future_job + class MainThreadExecutor: + def __enter__(self): + return self - executor = MainThreadExecutor() - elif ( - platform.system() in ("Linux", "Darwin") - and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES.get()) - is not None - ): - executor = concurrent.futures.ProcessPoolExecutor( - max_workers=number_of_processes or None, - mp_context=multiprocessing.get_context("fork"), - ) - else: - executor = concurrent.futures.ThreadPoolExecutor( - max_workers=environment.REFLEX_COMPILE_THREADS.get() or None - ) + def __exit__(self, *args): + pass + + def submit( + self, fn: Callable[..., T], *args, **kwargs + ) -> concurrent.futures.Future[T]: + future_job = concurrent.futures.Future() + future_job.set_result(fn(*args, **kwargs)) + return future_job + + executor = MainThreadExecutor() for route, component in zip(self._pages, page_components, strict=True): ExecutorSafeFunctions.COMPONENTS[route] = component ExecutorSafeFunctions.STATE = self._state - with executor: + with console.timing("Compile to Javascript"), executor as executor: result_futures: list[concurrent.futures.Future[tuple[str, str]]] = [] def _submit_work(fn: Callable[..., tuple[str, str]], *args, **kwargs): f = executor.submit(fn, *args, **kwargs) + f.add_done_callback(lambda _: progress.advance(task)) result_futures.append(f) # Compile the pre-compiled pages. @@ -1227,10 +1259,10 @@ class App(MiddlewareMixin, LifespanMixin): _submit_work(compiler.remove_tailwind_from_postcss) # Wait for all compilation tasks to complete. - with console.timing("Compile to Javascript"): - for future in concurrent.futures.as_completed(result_futures): - compile_results.append(future.result()) - progress.advance(task) + compile_results.extend( + future.result() + for future in concurrent.futures.as_completed(result_futures) + ) app_root = self._app_root(app_wrappers=app_wrappers) diff --git a/reflex/config.py b/reflex/config.py index 63d420c86..2ae19cd72 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -481,6 +481,14 @@ class PerformanceMode(enum.Enum): OFF = "off" +class ExecutorType(enum.Enum): + """Executor for compiling the frontend.""" + + THREAD = "thread" + PROCESS = "process" + MAIN_THREAD = "main_thread" + + class EnvironmentVariables: """Environment variables class to instantiate environment variables.""" @@ -522,8 +530,7 @@ class EnvironmentVariables: Path(constants.Dirs.UPLOADED_FILES) ) - # Whether to use the main thread for compiling the frontend. Takes precedence over other compile options. - REFLEX_COMPILE_USE_MAIN_THREAD: EnvVar[bool] = env_var(False) + REFLEX_COMPILE_EXECUTOR: EnvVar[Optional[ExecutorType]] = env_var(None) # Whether to use separate processes to compile the frontend and how many. If not set, defaults to thread executor. REFLEX_COMPILE_PROCESSES: EnvVar[Optional[int]] = env_var(None)