Skip to content

Commit

Permalink
Copy/update assets on compile (#4765)
Browse files Browse the repository at this point in the history
* Add path_ops.update_directory_tree:

Copy missing and newer files from src to dest

* add console.timing context

Log debug messages with timing for different processes.

* Update assets tree as app._compile step.

If the assets change between hot reload, then update them before reloading (in
case a CSS file was added or something).

* Add timing for other app._compile events

* Only copy assets if assets exist

* Fix docstring for update_directory_tree
  • Loading branch information
masenf authored Feb 7, 2025
1 parent c17cda3 commit 70920a6
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 21 deletions.
66 changes: 45 additions & 21 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,15 @@
_substate_key,
code_uses_state_contexts,
)
from reflex.utils import codespaces, console, exceptions, format, prerequisites, types
from reflex.utils import (
codespaces,
console,
exceptions,
format,
path_ops,
prerequisites,
types,
)
from reflex.utils.exec import is_prod_mode, is_testing_env
from reflex.utils.imports import ImportVar

Expand Down Expand Up @@ -991,9 +999,10 @@ def get_compilation_time() -> str:
should_compile = self._should_compile()

if not should_compile:
for route in self._unevaluated_pages:
console.debug(f"Evaluating page: {route}")
self._compile_page(route, save_page=should_compile)
with console.timing("Evaluate Pages (Backend)"):
for route in self._unevaluated_pages:
console.debug(f"Evaluating page: {route}")
self._compile_page(route, save_page=should_compile)

# Add the optional endpoints (_upload)
self._add_optional_endpoints()
Expand All @@ -1019,10 +1028,11 @@ def get_compilation_time() -> str:
+ adhoc_steps_without_executor,
)

for route in self._unevaluated_pages:
console.debug(f"Evaluating page: {route}")
self._compile_page(route, save_page=should_compile)
progress.advance(task)
with console.timing("Evaluate Pages (Frontend)"):
for route in self._unevaluated_pages:
console.debug(f"Evaluating page: {route}")
self._compile_page(route, save_page=should_compile)
progress.advance(task)

# Add the optional endpoints (_upload)
self._add_optional_endpoints()
Expand Down Expand Up @@ -1057,13 +1067,13 @@ def get_compilation_time() -> str:
custom_components |= component._get_all_custom_components()

# Perform auto-memoization of stateful components.
(
stateful_components_path,
stateful_components_code,
page_components,
) = compiler.compile_stateful_components(self._pages.values())

progress.advance(task)
with console.timing("Auto-memoize StatefulComponents"):
(
stateful_components_path,
stateful_components_code,
page_components,
) = compiler.compile_stateful_components(self._pages.values())
progress.advance(task)

# Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
if code_uses_state_contexts(stateful_components_code) and self._state is None:
Expand All @@ -1086,6 +1096,17 @@ def get_compilation_time() -> str:

progress.advance(task)

# Copy the assets.
assets_src = Path.cwd() / constants.Dirs.APP_ASSETS
if assets_src.is_dir():
with console.timing("Copy assets"):
path_ops.update_directory_tree(
src=assets_src,
dest=(
Path.cwd() / prerequisites.get_web_dir() / constants.Dirs.PUBLIC
),
)

# Use a forking process pool, if possible. Much faster, especially for large sites.
# Fallback to ThreadPoolExecutor as something that will always work.
executor = None
Expand Down Expand Up @@ -1138,9 +1159,10 @@ def _submit_work(fn: Callable, *args, **kwargs):
_submit_work(compiler.remove_tailwind_from_postcss)

# Wait for all compilation tasks to complete.
for future in concurrent.futures.as_completed(result_futures):
compile_results.append(future.result())
progress.advance(task)
with console.timing("Compile to Javascript"):
for future in concurrent.futures.as_completed(result_futures):
compile_results.append(future.result())
progress.advance(task)

app_root = self._app_root(app_wrappers=app_wrappers)

Expand Down Expand Up @@ -1175,7 +1197,8 @@ def _submit_work(fn: Callable, *args, **kwargs):
progress.stop()

# Install frontend packages.
self._get_frontend_packages(all_imports)
with console.timing("Install Frontend Packages"):
self._get_frontend_packages(all_imports)

# Setup the next.config.js
transpile_packages = [
Expand All @@ -1201,8 +1224,9 @@ def _submit_work(fn: Callable, *args, **kwargs):
# Remove pages that are no longer in the app.
p.unlink()

for output_path, code in compile_results:
compiler_utils.write_page(output_path, code)
with console.timing("Write to Disk"):
for output_path, code in compile_results:
compiler_utils.write_page(output_path, code)

@contextlib.asynccontextmanager
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
Expand Down
19 changes: 19 additions & 0 deletions reflex/utils/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from __future__ import annotations

import contextlib
import inspect
import shutil
import time
from pathlib import Path
from types import FrameType

Expand Down Expand Up @@ -317,3 +319,20 @@ def status(*args, **kwargs):
A new status.
"""
return _console.status(*args, **kwargs)


@contextlib.contextmanager
def timing(msg: str):
"""Create a context manager to time a block of code.
Args:
msg: The message to display.
Yields:
None.
"""
start = time.time()
try:
yield
finally:
debug(f"[white]\\[timing] {msg}: {time.time() - start:.2f}s[/white]")
30 changes: 30 additions & 0 deletions reflex/utils/path_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,33 @@ def find_replace(directory: str | Path, find: str, replace: str):
text = filepath.read_text(encoding="utf-8")
text = re.sub(find, replace, text)
filepath.write_text(text, encoding="utf-8")


def update_directory_tree(src: Path, dest: Path):
"""Recursively copies a directory tree from src to dest.
Only copies files if the destination file is missing or modified earlier than the source file.
Args:
src: Source directory
dest: Destination directory
Raises:
ValueError: If the source is not a directory
"""
if not src.is_dir():
raise ValueError(f"Source {src} is not a directory")

# Ensure the destination directory exists
dest.mkdir(parents=True, exist_ok=True)

for item in src.iterdir():
dest_item = dest / item.name

if item.is_dir():
# Recursively copy subdirectories
update_directory_tree(item, dest_item)
elif item.is_file() and (
not dest_item.exists() or item.stat().st_mtime > dest_item.stat().st_mtime
):
# Copy file if it doesn't exist in the destination or is older than the source
shutil.copy2(item, dest_item)

0 comments on commit 70920a6

Please sign in to comment.