From aac61c69c2c2fdcb37f3cc67cab92623386c239f Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 15:40:01 -0800 Subject: [PATCH 1/5] actually get rid of callable var fr fr (#4821) --- reflex/components/core/upload.py | 4 +- reflex/components/core/upload.pyi | 4 +- reflex/components/radix/themes/color_mode.py | 2 +- reflex/style.py | 3 +- reflex/vars/base.py | 55 ------------------- tests/integration/test_upload.py | 2 +- .../tests_playwright/test_appearance.py | 2 +- 7 files changed, 6 insertions(+), 66 deletions(-) diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index 897b89608..6c86d3c44 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -29,7 +29,7 @@ from reflex.event import ( from reflex.utils import format from reflex.utils.imports import ImportVar from reflex.vars import VarData -from reflex.vars.base import CallableVar, Var, get_unique_variable_name +from reflex.vars.base import Var, get_unique_variable_name from reflex.vars.sequence import LiteralStringVar DEFAULT_UPLOAD_ID: str = "default" @@ -45,7 +45,6 @@ upload_files_context_var_data: VarData = VarData( ) -@CallableVar def upload_file(id_: str = DEFAULT_UPLOAD_ID) -> Var: """Get the file upload drop trigger. @@ -75,7 +74,6 @@ def upload_file(id_: str = DEFAULT_UPLOAD_ID) -> Var: ) -@CallableVar def selected_files(id_: str = DEFAULT_UPLOAD_ID) -> Var: """Get the list of selected files. diff --git a/reflex/components/core/upload.pyi b/reflex/components/core/upload.pyi index 6ed96a15e..d1ddceb4d 100644 --- a/reflex/components/core/upload.pyi +++ b/reflex/components/core/upload.pyi @@ -13,14 +13,12 @@ from reflex.event import CallableEventSpec, EventSpec, EventType from reflex.style import Style from reflex.utils.imports import ImportVar from reflex.vars import VarData -from reflex.vars.base import CallableVar, Var +from reflex.vars.base import Var DEFAULT_UPLOAD_ID: str upload_files_context_var_data: VarData -@CallableVar def upload_file(id_: str = DEFAULT_UPLOAD_ID) -> Var: ... -@CallableVar def selected_files(id_: str = DEFAULT_UPLOAD_ID) -> Var: ... @CallableEventSpec def clear_selected_files(id_: str = DEFAULT_UPLOAD_ID) -> EventSpec: ... diff --git a/reflex/components/radix/themes/color_mode.py b/reflex/components/radix/themes/color_mode.py index d9b7c0b02..0718aaac9 100644 --- a/reflex/components/radix/themes/color_mode.py +++ b/reflex/components/radix/themes/color_mode.py @@ -144,7 +144,7 @@ class ColorModeIconButton(IconButton): if allow_system: - def color_mode_item(_color_mode: str): + def color_mode_item(_color_mode: Literal["light", "dark", "system"]): return dropdown_menu.item( _color_mode.title(), on_click=set_color_mode(_color_mode) ) diff --git a/reflex/style.py b/reflex/style.py index 192835ca3..1d818ed06 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -12,7 +12,7 @@ from reflex.utils.exceptions import ReflexError from reflex.utils.imports import ImportVar from reflex.utils.types import get_origin from reflex.vars import VarData -from reflex.vars.base import CallableVar, LiteralVar, Var +from reflex.vars.base import LiteralVar, Var from reflex.vars.function import FunctionVar from reflex.vars.object import ObjectVar @@ -48,7 +48,6 @@ def _color_mode_var(_js_expr: str, _var_type: Type = str) -> Var: ).guess_type() -@CallableVar def set_color_mode( new_color_mode: LiteralColorMode | Var[LiteralColorMode] | None = None, ) -> Var[EventChain]: diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 593c60f3e..a6786b18a 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -1903,61 +1903,6 @@ def _or_operation(a: Var, b: Var): ) -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class CallableVar(Var): - """Decorate a Var-returning function to act as both a Var and a function. - - This is used as a compatibility shim for replacing Var objects in the - API with functions that return a family of Var. - """ - - fn: Callable[..., Var] = dataclasses.field( - default_factory=lambda: lambda: Var(_js_expr="undefined") - ) - original_var: Var = dataclasses.field( - default_factory=lambda: Var(_js_expr="undefined") - ) - - def __init__(self, fn: Callable[..., Var]): - """Initialize a CallableVar. - - Args: - fn: The function to decorate (must return Var) - """ - original_var = fn() - super(CallableVar, self).__init__( - _js_expr=original_var._js_expr, - _var_type=original_var._var_type, - _var_data=VarData.merge(original_var._get_all_var_data()), - ) - object.__setattr__(self, "fn", fn) - object.__setattr__(self, "original_var", original_var) - - def __call__(self, *args: Any, **kwargs: Any) -> Var: - """Call the decorated function. - - Args: - *args: The args to pass to the function. - **kwargs: The kwargs to pass to the function. - - Returns: - The Var returned from calling the function. - """ - return self.fn(*args, **kwargs) - - def __hash__(self) -> int: - """Calculate the hash of the object. - - Returns: - The hash of the object. - """ - return hash((type(self).__name__, self.original_var)) - - RETURN_TYPE = TypeVar("RETURN_TYPE") DICT_KEY = TypeVar("DICT_KEY") diff --git a/tests/integration/test_upload.py b/tests/integration/test_upload.py index e20b1cd6d..471382570 100644 --- a/tests/integration/test_upload.py +++ b/tests/integration/test_upload.py @@ -87,7 +87,7 @@ def UploadFile(): ), rx.box( rx.foreach( - rx.selected_files, + rx.selected_files(), lambda f: rx.text(f, as_="p"), ), id="selected_files", diff --git a/tests/integration/tests_playwright/test_appearance.py b/tests/integration/tests_playwright/test_appearance.py index d325b183f..0b1440ed1 100644 --- a/tests/integration/tests_playwright/test_appearance.py +++ b/tests/integration/tests_playwright/test_appearance.py @@ -61,7 +61,7 @@ def ColorToggleApp(): rx.icon(tag="moon", size=20), value="dark", ), - on_change=set_color_mode, + on_change=set_color_mode(), variant="classic", radius="large", value=color_mode, From b44bbc81a0a707ded71c9f0d0a9d4afb0c308bdd Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 15:54:10 -0800 Subject: [PATCH 2/5] import var perf improvements (#4813) * import var perf improvements * use tuples over iterator * the only thing that matters * maybe tuple map is faster than tuple list comprehension * do it in one list comprehension --- reflex/components/component.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index d27bddf78..005f7791d 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -51,13 +51,7 @@ from reflex.event import ( ) from reflex.style import Style, format_as_emotion from reflex.utils import format, imports, types -from reflex.utils.imports import ( - ImmutableParsedImportDict, - ImportDict, - ImportVar, - ParsedImportDict, - parse_imports, -) +from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports from reflex.vars import VarData from reflex.vars.base import ( CachedVarOperation, @@ -1208,7 +1202,7 @@ class Component(BaseComponent, ABC): Returns: True if the dependency should be transpiled. """ - return ( + return bool(self.transpile_packages) and ( dep in self.transpile_packages or format.format_library_name(dep or "") in self.transpile_packages ) @@ -1291,9 +1285,10 @@ class Component(BaseComponent, ABC): event_imports = Imports.EVENTS if self.event_triggers else {} # Collect imports from Vars used directly by this component. - var_datas = [var._get_all_var_data() for var in self._get_vars()] - var_imports: List[ImmutableParsedImportDict] = [ - var_data.imports for var_data in var_datas if var_data is not None + var_imports = [ + var_data.imports + for var in self._get_vars() + if (var_data := var._get_all_var_data()) is not None ] added_import_dicts: list[ParsedImportDict] = [] From 8e579efe47e4e1a7ba6e61c00166a55ed36eff5f Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 16:17:06 -0800 Subject: [PATCH 3/5] remove some benchmarks from CI (#4812) --- .github/workflows/benchmarks.yml | 50 -- .github/workflows/integration_tests.yml | 32 - .../test_benchmark_compile_components.py | 376 ----------- benchmarks/test_benchmark_compile_pages.py | 595 ------------------ 4 files changed, 1053 deletions(-) delete mode 100644 benchmarks/test_benchmark_compile_components.py delete mode 100644 benchmarks/test_benchmark_compile_pages.py diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 6da40ef6f..b794106e1 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -70,56 +70,6 @@ jobs: env: GITHUB_SHA: ${{ github.sha }} - simple-apps-benchmarks: # This app tests the compile times of various compoonents and pages - if: github.event.pull_request.merged == true - env: - OUTPUT_FILE: benchmarks.json - timeout-minutes: 50 - strategy: - # Prioritize getting more information out of the workflow (even if something fails) - fail-fast: false - matrix: - # Show OS combos first in GUI - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.10.16", "3.11.11", "3.12.8"] - exclude: - - os: windows-latest - python-version: "3.10.16" - - os: windows-latest - python-version: "3.11.11" - # keep only one python version for MacOS - - os: macos-latest - python-version: "3.10.16" - - os: macos-latest - python-version: "3.11.11" - include: - - os: windows-latest - python-version: "3.10.11" - - os: windows-latest - python-version: "3.11.9" - - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup_build_env - with: - python-version: ${{ matrix.python-version }} - run-poetry-install: true - create-venv-at-path: .venv - - name: Run benchmark tests - env: - APP_HARNESS_HEADLESS: 1 - PYTHONUNBUFFERED: 1 - run: | - poetry run pytest -v benchmarks/ --benchmark-json=${{ env.OUTPUT_FILE }} -s - - name: Upload benchmark results - # Only run if the database creds are available in this context. - run: - poetry run python benchmarks/benchmark_compile_times.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --benchmark-json "${{ env.OUTPUT_FILE }}" --branch-name "${{ github.head_ref || github.ref_name }}" - --event-type "${{ github.event_name }}" --pr-id "${{ github.event.pull_request.id }}" - reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file) if: github.event.pull_request.merged == true timeout-minutes: 30 diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index b02604fd6..dc5a14d88 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -94,26 +94,6 @@ jobs: # Check that npm is home npm -v poetry run bash scripts/integration.sh ./reflex-examples/counter dev - - name: Measure and upload .web size - run: - poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --pr-id "${{ github.event.pull_request.id }}" - --branch-name "${{ github.head_ref || github.ref_name }}" - --path ./reflex-examples/counter/.web - --app-name "counter" - - name: Install hyperfine - run: cargo install hyperfine - - name: Benchmark imports - working-directory: ./reflex-examples/counter - run: hyperfine --warmup 3 "export POETRY_VIRTUALENVS_PATH=../../.venv; poetry run python counter/counter.py" --show-output --export-json "${{ env.OUTPUT_FILE }}" --shell bash - - name: Upload Benchmarks - run: - poetry run python benchmarks/benchmark_imports.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --benchmark-json "./reflex-examples/counter/${{ env.OUTPUT_FILE }}" - --branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}" - --app-name "counter" - name: Install requirements for nba proxy example working-directory: ./reflex-examples/nba-proxy run: | @@ -174,12 +154,6 @@ jobs: # Check that npm is home npm -v poetry run bash scripts/integration.sh ./reflex-web prod - - name: Measure and upload .web size - run: - poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" - --app-name "reflex-web" --path ./reflex-web/.web rx-shout-from-template: strategy: @@ -243,9 +217,3 @@ jobs: # Check that npm is home npm -v poetry run bash scripts/integration.sh ./reflex-web prod - - name: Measure and upload .web size - run: - poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" - --app-name "reflex-web" --path ./reflex-web/.web diff --git a/benchmarks/test_benchmark_compile_components.py b/benchmarks/test_benchmark_compile_components.py deleted file mode 100644 index 9bcfbf85b..000000000 --- a/benchmarks/test_benchmark_compile_components.py +++ /dev/null @@ -1,376 +0,0 @@ -"""Benchmark tests for apps with varying component numbers.""" - -from __future__ import annotations - -import functools -import time -from typing import Generator - -import pytest - -from benchmarks import WINDOWS_SKIP_REASON -from reflex import constants -from reflex.compiler import utils -from reflex.testing import AppHarness, chdir -from reflex.utils import build -from reflex.utils.prerequisites import get_web_dir - -web_pages = get_web_dir() / constants.Dirs.PAGES - - -def render_component(num: int): - """Generate a number of components based on num. - - Args: - num: number of components to produce. - - Returns: - The rendered number of components. - """ - import reflex as rx - - return [ - rx.fragment( - rx.box( - rx.accordion.root( - rx.accordion.item( - header="Full Ingredients", - content="Yes. It's built with accessibility in mind.", - font_size="3em", - ), - rx.accordion.item( - header="Applications", - content="Yes. It's unstyled by default, giving you freedom over the look and feel.", - ), - collapsible=True, - variant="ghost", - width="25rem", - ), - padding_top="20px", - ), - rx.box( - rx.drawer.root( - rx.drawer.trigger( - rx.button("Open Drawer with snap points"), as_child=True - ), - rx.drawer.overlay(), - rx.drawer.portal( - rx.drawer.content( - rx.flex( - rx.drawer.title("Drawer Content"), - rx.drawer.description("Drawer description"), - rx.drawer.close( - rx.button("Close Button"), - as_child=True, - ), - direction="column", - margin="5em", - align_items="center", - ), - top="auto", - height="100%", - flex_direction="column", - background_color="var(--green-3)", - ), - ), - snap_points=["148px", "355px", 1], - ), - ), - rx.box( - rx.callout( - "You will need admin privileges to install and access this application.", - icon="info", - size="3", - ), - ), - rx.box( - rx.table.root( - rx.table.header( - rx.table.row( - rx.table.column_header_cell("Full name"), - rx.table.column_header_cell("Email"), - rx.table.column_header_cell("Group"), - ), - ), - rx.table.body( - rx.table.row( - rx.table.row_header_cell("Danilo Sousa"), - rx.table.cell("danilo@example.com"), - rx.table.cell("Developer"), - ), - rx.table.row( - rx.table.row_header_cell("Zahra Ambessa"), - rx.table.cell("zahra@example.com"), - rx.table.cell("Admin"), - ), - rx.table.row( - rx.table.row_header_cell("Jasper Eriksson"), - rx.table.cell("jasper@example.com"), - rx.table.cell("Developer"), - ), - ), - ) - ), - ) - ] * num - - -def AppWithTenComponentsOnePage(): - """A reflex app with roughly 10 components on one page.""" - import reflex as rx - - def index() -> rx.Component: - return rx.center(rx.vstack(*render_component(1))) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -def AppWithHundredComponentOnePage(): - """A reflex app with roughly 100 components on one page.""" - import reflex as rx - - def index() -> rx.Component: - return rx.center(rx.vstack(*render_component(100))) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -def AppWithThousandComponentsOnePage(): - """A reflex app with roughly 1000 components on one page.""" - import reflex as rx - - def index() -> rx.Component: - return rx.center(rx.vstack(*render_component(1000))) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -@pytest.fixture(scope="session") -def app_with_10_components( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Start Blank Template app at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - running AppHarness instance - """ - root = tmp_path_factory.mktemp("app10components") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithTenComponentsOnePage, - render_component=render_component, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_100_components( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Start Blank Template app at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - running AppHarness instance - """ - root = tmp_path_factory.mktemp("app100components") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithHundredComponentOnePage, - render_component=render_component, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_1000_components( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 1000 components at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app1000components") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithThousandComponentsOnePage, - render_component=render_component, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_cold(benchmark, app_with_10_components): - """Test the compile time on a cold start for an app with roughly 10 components. - - Args: - benchmark: The benchmark fixture. - app_with_10_components: The app harness. - """ - - def setup(): - with chdir(app_with_10_components.app_path): - utils.empty_dir(web_pages, ["_app.js"]) - app_with_10_components._initialize_app() - build.setup_frontend(app_with_10_components.app_path) - - def benchmark_fn(): - with chdir(app_with_10_components.app_path): - app_with_10_components.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=10) - - -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_warm(benchmark, app_with_10_components): - """Test the compile time on a warm start for an app with roughly 10 components. - - Args: - benchmark: The benchmark fixture. - app_with_10_components: The app harness. - """ - with chdir(app_with_10_components.app_path): - app_with_10_components._initialize_app() - build.setup_frontend(app_with_10_components.app_path) - - def benchmark_fn(): - with chdir(app_with_10_components.app_path): - app_with_10_components.app_instance._compile() - - benchmark(benchmark_fn) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_cold(benchmark, app_with_100_components): - """Test the compile time on a cold start for an app with roughly 100 components. - - Args: - benchmark: The benchmark fixture. - app_with_100_components: The app harness. - """ - - def setup(): - with chdir(app_with_100_components.app_path): - utils.empty_dir(web_pages, ["_app.js"]) - app_with_100_components._initialize_app() - build.setup_frontend(app_with_100_components.app_path) - - def benchmark_fn(): - with chdir(app_with_100_components.app_path): - app_with_100_components.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - - -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_warm(benchmark, app_with_100_components): - """Test the compile time on a warm start for an app with roughly 100 components. - - Args: - benchmark: The benchmark fixture. - app_with_100_components: The app harness. - """ - with chdir(app_with_100_components.app_path): - app_with_100_components._initialize_app() - build.setup_frontend(app_with_100_components.app_path) - - def benchmark_fn(): - with chdir(app_with_100_components.app_path): - app_with_100_components.app_instance._compile() - - benchmark(benchmark_fn) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_cold(benchmark, app_with_1000_components): - """Test the compile time on a cold start for an app with roughly 1000 components. - - Args: - benchmark: The benchmark fixture. - app_with_1000_components: The app harness. - """ - - def setup(): - with chdir(app_with_1000_components.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_1000_components._initialize_app() - build.setup_frontend(app_with_1000_components.app_path) - - def benchmark_fn(): - with chdir(app_with_1000_components.app_path): - app_with_1000_components.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - - -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_warm(benchmark, app_with_1000_components): - """Test the compile time on a warm start for an app with roughly 1000 components. - - Args: - benchmark: The benchmark fixture. - app_with_1000_components: The app harness. - """ - with chdir(app_with_1000_components.app_path): - app_with_1000_components._initialize_app() - build.setup_frontend(app_with_1000_components.app_path) - - def benchmark_fn(): - with chdir(app_with_1000_components.app_path): - app_with_1000_components.app_instance._compile() - - benchmark(benchmark_fn) diff --git a/benchmarks/test_benchmark_compile_pages.py b/benchmarks/test_benchmark_compile_pages.py deleted file mode 100644 index 6cf39f60c..000000000 --- a/benchmarks/test_benchmark_compile_pages.py +++ /dev/null @@ -1,595 +0,0 @@ -"""Benchmark tests for apps with varying page numbers.""" - -from __future__ import annotations - -import functools -import time -from typing import Generator - -import pytest - -from benchmarks import WINDOWS_SKIP_REASON -from reflex import constants -from reflex.compiler import utils -from reflex.testing import AppHarness, chdir -from reflex.utils import build -from reflex.utils.prerequisites import get_web_dir - -web_pages = get_web_dir() / constants.Dirs.PAGES - - -def render_multiple_pages(app, num: int): - """Add multiple pages based on num. - - Args: - app: The App object. - num: number of pages to render. - - """ - from typing import Tuple - - from rxconfig import config # pyright: ignore [reportMissingImports] - - import reflex as rx - - docs_url = "https://reflex.dev/docs/getting-started/introduction/" - filename = f"{config.app_name}/{config.app_name}.py" - college = [ - "Stanford University", - "Arizona", - "Arizona state", - "Baylor", - "Boston College", - "Boston University", - ] - - class State(rx.State): - """The app state.""" - - position: rx.Field[str] - college: rx.Field[str] - age: rx.Field[Tuple[int, int]] = rx.field((18, 50)) - salary: rx.Field[Tuple[int, int]] = rx.field((0, 25000000)) - - @rx.event - def set_position(self, value: str): - self.position = value - - @rx.event - def set_college(self, value: str): - self.college = value - - @rx.event - def set_age(self, value: list[int]): - self.age = (value[0], value[1]) - - @rx.event - def set_salary(self, value: list[int]): - self.salary = (value[0], value[1]) - - comp1 = rx.center( - rx.theme_panel(), - rx.vstack( - rx.heading("Welcome to Reflex!", size="9"), - rx.text("Get started by editing ", rx.code(filename)), - rx.button( - "Check out our docs!", - on_click=lambda: rx.redirect(docs_url), - size="4", - ), - align="center", - spacing="7", - font_size="2em", - ), - height="100vh", - ) - - comp2 = rx.vstack( - rx.hstack( - rx.vstack( - rx.select( - ["C", "PF", "SF", "PG", "SG"], - placeholder="Select a position. (All)", - on_change=State.set_position, - size="3", - ), - rx.select( - college, - placeholder="Select a college. (All)", - on_change=State.set_college, - size="3", - ), - ), - rx.vstack( - rx.vstack( - rx.hstack( - rx.badge("Min Age: ", State.age[0]), - rx.divider(orientation="vertical"), - rx.badge("Max Age: ", State.age[1]), - ), - rx.slider( - default_value=[18, 50], - min=18, - max=50, - on_value_commit=State.set_age, - ), - align_items="left", - width="100%", - ), - rx.vstack( - rx.hstack( - rx.badge("Min Sal: ", State.salary[0] // 1000000, "M"), - rx.divider(orientation="vertical"), - rx.badge("Max Sal: ", State.salary[1] // 1000000, "M"), - ), - rx.slider( - default_value=[0, 25000000], - min=0, - max=25000000, - on_value_commit=State.set_salary, - ), - align_items="left", - width="100%", - ), - ), - spacing="4", - ), - width="100%", - ) - - for i in range(1, num + 1): - if i % 2 == 1: - app.add_page(comp1, route=f"page{i}") - else: - app.add_page(comp2, route=f"page{i}") - - -def AppWithOnePage(): - """A reflex app with one page.""" - from rxconfig import config # pyright: ignore [reportMissingImports] - - import reflex as rx - - docs_url = "https://reflex.dev/docs/getting-started/introduction/" - filename = f"{config.app_name}/{config.app_name}.py" - - class State(rx.State): - """The app state.""" - - pass - - def index() -> rx.Component: - return rx.center( - rx.input( - id="token", value=State.router.session.client_token, is_read_only=True - ), - rx.vstack( - rx.heading("Welcome to Reflex!", size="9"), - rx.text("Get started by editing ", rx.code(filename)), - rx.button( - "Check out our docs!", - on_click=lambda: rx.redirect(docs_url), - size="4", - ), - align="center", - spacing="7", - font_size="2em", - ), - height="100vh", - ) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -def AppWithTenPages(): - """A reflex app with 10 pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 10) - - -def AppWithHundredPages(): - """A reflex app with 100 pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 100) - - -def AppWithThousandPages(): - """A reflex app with Thousand pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 1000) - - -def AppWithTenThousandPages(): - """A reflex app with ten thousand pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 10000) - - -@pytest.fixture(scope="session") -def app_with_one_page( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 10000 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app1") - - yield AppHarness.create(root=root, app_source=AppWithOnePage) - - -@pytest.fixture(scope="session") -def app_with_ten_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 10 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app10") - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithTenPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_hundred_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 100 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app100") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithHundredPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_thousand_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 1000 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app1000") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithThousandPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_ten_thousand_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 10000 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - running AppHarness instance - """ - root = tmp_path_factory.mktemp("app10000") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithTenThousandPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1_compile_time_cold(benchmark, app_with_one_page): - """Test the compile time on a cold start for an app with 1 page. - - Args: - benchmark: The benchmark fixture. - app_with_one_page: The app harness. - """ - - def setup(): - with chdir(app_with_one_page.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_one_page._initialize_app() - build.setup_frontend(app_with_one_page.app_path) - - def benchmark_fn(): - with chdir(app_with_one_page.app_path): - app_with_one_page.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_one_page._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1_compile_time_warm(benchmark, app_with_one_page): - """Test the compile time on a warm start for an app with 1 page. - - Args: - benchmark: The benchmark fixture. - app_with_one_page: The app harness. - """ - with chdir(app_with_one_page.app_path): - app_with_one_page._initialize_app() - build.setup_frontend(app_with_one_page.app_path) - - def benchmark_fn(): - with chdir(app_with_one_page.app_path): - app_with_one_page.app_instance._compile() - - benchmark(benchmark_fn) - app_with_one_page._reload_state_module() - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_cold(benchmark, app_with_ten_pages): - """Test the compile time on a cold start for an app with 10 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_pages: The app harness. - """ - - def setup(): - with chdir(app_with_ten_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_ten_pages._initialize_app() - build.setup_frontend(app_with_ten_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_ten_pages.app_path): - app_with_ten_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_ten_pages._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_warm(benchmark, app_with_ten_pages): - """Test the compile time on a warm start for an app with 10 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_pages: The app harness. - """ - with chdir(app_with_ten_pages.app_path): - app_with_ten_pages._initialize_app() - build.setup_frontend(app_with_ten_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_ten_pages.app_path): - app_with_ten_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_ten_pages._reload_state_module() - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_cold(benchmark, app_with_hundred_pages): - """Test the compile time on a cold start for an app with 100 page. - - Args: - benchmark: The benchmark fixture. - app_with_hundred_pages: The app harness. - """ - - def setup(): - with chdir(app_with_hundred_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_hundred_pages._initialize_app() - build.setup_frontend(app_with_hundred_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_hundred_pages.app_path): - app_with_hundred_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_hundred_pages._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_warm(benchmark, app_with_hundred_pages): - """Test the compile time on a warm start for an app with 100 page. - - Args: - benchmark: The benchmark fixture. - app_with_hundred_pages: The app harness. - """ - with chdir(app_with_hundred_pages.app_path): - app_with_hundred_pages._initialize_app() - build.setup_frontend(app_with_hundred_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_hundred_pages.app_path): - app_with_hundred_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_hundred_pages._reload_state_module() - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_cold(benchmark, app_with_thousand_pages): - """Test the compile time on a cold start for an app with 1000 page. - - Args: - benchmark: The benchmark fixture. - app_with_thousand_pages: The app harness. - """ - - def setup(): - with chdir(app_with_thousand_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_thousand_pages._initialize_app() - build.setup_frontend(app_with_thousand_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_thousand_pages.app_path): - app_with_thousand_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_thousand_pages._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_warm(benchmark, app_with_thousand_pages): - """Test the compile time on a warm start for an app with 1000 page. - - Args: - benchmark: The benchmark fixture. - app_with_thousand_pages: The app harness. - """ - with chdir(app_with_thousand_pages.app_path): - app_with_thousand_pages._initialize_app() - build.setup_frontend(app_with_thousand_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_thousand_pages.app_path): - app_with_thousand_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_thousand_pages._reload_state_module() - - -@pytest.mark.skip -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10000_compile_time_cold(benchmark, app_with_ten_thousand_pages): - """Test the compile time on a cold start for an app with 10000 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_thousand_pages: The app harness. - """ - - def setup(): - with chdir(app_with_ten_thousand_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_ten_thousand_pages._initialize_app() - build.setup_frontend(app_with_ten_thousand_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_ten_thousand_pages.app_path): - app_with_ten_thousand_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_ten_thousand_pages._reload_state_module() - - -@pytest.mark.skip -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10000_compile_time_warm(benchmark, app_with_ten_thousand_pages): - """Test the compile time on a warm start for an app with 10000 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_thousand_pages: The app harness. - """ - - def benchmark_fn(): - with chdir(app_with_ten_thousand_pages.app_path): - app_with_ten_thousand_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_ten_thousand_pages._reload_state_module() From 7c4257a222bcda6401f834b267948b2a953d1587 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 18:36:30 -0800 Subject: [PATCH 4/5] give option to only use main thread (#4809) * give option to only use main thread * change default to main thread * fix comment * default to None, as 0 would raise a ValueError Co-authored-by: Masen Furer * add warning about passing 0 * move executor to config --------- Co-authored-by: Masen Furer --- reflex/app.py | 62 ++++++++++++------------ reflex/compiler/compiler.py | 2 +- reflex/config.py | 95 +++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 32 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 2c8e889fc..d0ee06ae9 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -11,12 +11,11 @@ import functools import inspect import io import json -import multiprocessing -import platform 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, @@ -76,7 +75,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, @@ -1114,10 +1113,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() @@ -1130,7 +1142,7 @@ class App(MiddlewareMixin, LifespanMixin): progress.advance(task) # Store the compile results. - compile_results = [] + compile_results: list[tuple[str, str]] = [] progress.advance(task) @@ -1209,33 +1221,19 @@ class App(MiddlewareMixin, LifespanMixin): ), ) - # 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 ( - 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 - ) + executor = ExecutorType.get_executor_from_environment() for route, component in zip(self._pages, page_components, strict=True): ExecutorSafeFunctions.COMPONENTS[route] = component ExecutorSafeFunctions.STATE = self._state - with executor: - result_futures = [] + with console.timing("Compile to Javascript"), executor as executor: + result_futures: list[concurrent.futures.Future[tuple[str, str]]] = [] - def _submit_work(fn: Callable, *args, **kwargs): + 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. @@ -1261,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) @@ -1289,10 +1287,12 @@ class App(MiddlewareMixin, LifespanMixin): progress.advance(task) # Compile custom components. - *custom_components_result, custom_components_imports = ( - compiler.compile_components(custom_components) - ) - compile_results.append(custom_components_result) + ( + custom_components_output, + custom_components_result, + custom_components_imports, + ) = compiler.compile_components(custom_components) + compile_results.append((custom_components_output, custom_components_result)) all_imports.update(custom_components_imports) progress.advance(task) diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 667a477e8..81de50182 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -508,7 +508,7 @@ def compile_tailwind( The compiled Tailwind config. """ # Get the path for the output file. - output_path = get_web_dir() / constants.Tailwind.CONFIG + output_path = str((get_web_dir() / constants.Tailwind.CONFIG).absolute()) # Compile the config. code = _compile_tailwind(config) diff --git a/reflex/config.py b/reflex/config.py index 33009b3bc..d0829e627 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -2,11 +2,14 @@ from __future__ import annotations +import concurrent.futures import dataclasses import enum import importlib import inspect +import multiprocessing import os +import platform import sys import threading import urllib.parse @@ -17,6 +20,7 @@ from types import ModuleType from typing import ( TYPE_CHECKING, Any, + Callable, Dict, Generic, List, @@ -497,6 +501,95 @@ class PerformanceMode(enum.Enum): OFF = "off" +class ExecutorType(enum.Enum): + """Executor for compiling the frontend.""" + + THREAD = "thread" + PROCESS = "process" + MAIN_THREAD = "main_thread" + + @classmethod + def get_executor_from_environment(cls): + """Get the executor based on the environment variables. + + Returns: + The executor. + """ + executor_type = environment.REFLEX_COMPILE_EXECUTOR.get() + + reflex_compile_processes = environment.REFLEX_COMPILE_PROCESSES.get() + reflex_compile_threads = environment.REFLEX_COMPILE_THREADS.get() + # By default, use the main thread. Unless the user has specified a different executor. + # Using a process pool is much faster, but not supported on all platforms. It's gated behind a flag. + if executor_type is None: + if ( + platform.system() not in ("Linux", "Darwin") + and reflex_compile_processes is not None + ): + console.warn("Multiprocessing is only supported on Linux and MacOS.") + + if ( + platform.system() in ("Linux", "Darwin") + and reflex_compile_processes is not None + ): + if reflex_compile_processes == 0: + console.warn( + "Number of processes must be greater than 0. If you want to use the default number of processes, set REFLEX_COMPILE_EXECUTOR to 'process'. Defaulting to None." + ) + reflex_compile_processes = None + elif reflex_compile_processes < 0: + console.warn( + "Number of processes must be greater than 0. Defaulting to None." + ) + reflex_compile_processes = None + executor_type = ExecutorType.PROCESS + elif reflex_compile_threads is not None: + if reflex_compile_threads == 0: + console.warn( + "Number of threads must be greater than 0. If you want to use the default number of threads, set REFLEX_COMPILE_EXECUTOR to 'thread'. Defaulting to None." + ) + reflex_compile_threads = None + elif reflex_compile_threads < 0: + console.warn( + "Number of threads must be greater than 0. Defaulting to None." + ) + reflex_compile_threads = None + executor_type = ExecutorType.THREAD + else: + executor_type = ExecutorType.MAIN_THREAD + + match executor_type: + case ExecutorType.PROCESS: + executor = concurrent.futures.ProcessPoolExecutor( + max_workers=reflex_compile_processes, + mp_context=multiprocessing.get_context("fork"), + ) + case ExecutorType.THREAD: + executor = concurrent.futures.ThreadPoolExecutor( + max_workers=reflex_compile_threads + ) + case ExecutorType.MAIN_THREAD: + FUTURE_RESULT_TYPE = TypeVar("FUTURE_RESULT_TYPE") + + class MainThreadExecutor: + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def submit( + self, fn: Callable[..., FUTURE_RESULT_TYPE], *args, **kwargs + ) -> concurrent.futures.Future[FUTURE_RESULT_TYPE]: + future_job = concurrent.futures.Future() + future_job.set_result(fn(*args, **kwargs)) + return future_job + + executor = MainThreadExecutor() + + return executor + + class EnvironmentVariables: """Environment variables class to instantiate environment variables.""" @@ -538,6 +631,8 @@ class EnvironmentVariables: Path(constants.Dirs.UPLOADED_FILES) ) + 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) From 10bae9577c0f898f7ae9b2d540336058bda53837 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 22:49:27 -0800 Subject: [PATCH 5/5] only write if file changed (#4822) * only write if file changed * preface it on it existing --- reflex/compiler/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 91ee18b86..c66dfe304 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -523,6 +523,8 @@ def write_page(path: str | Path, code: str): """ path = Path(path) path_ops.mkdir(path.parent) + if path.exists() and path.read_text(encoding="utf-8") == code: + return path.write_text(code, encoding="utf-8")