Skip to content

Commit 416a801

Browse files
authored
Merge pull request #116 from anubrag/improve-compile-process
Improve compile process
2 parents 85af7fc + ad2728e commit 416a801

File tree

10 files changed

+725
-301
lines changed

10 files changed

+725
-301
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@
3535
<br>
3636

3737
<a href="https://discord.gg/asTZktHrcH" target="blank">
38-
<img alt="Discord" src="https://img.shields.io/discord/1111984809821089883?style=social&logo=discord&label=Join%20Discord%20to%20access%20the%20nextpy%20bot%F0%9F%A4%96" width="500px">
38+
<img alt="Discord" src="https://img.shields.io/discord/1111984809821089883?style=for-the-badge&logo=discord&logoColor=white&label=Live%20Support%20%26%20Coding%20Bots%20on%20Discord&labelColor=%23684DFF&link=https%3A%2F%2Fdiscord.gg%2FasTZktHrcH">
39+
3940
</a>
4041

41-
<hr>
42+
![-----------------------------------------------------](https://res.cloudinary.com/dzznkbdrb/image/upload/v1694798498/divider_1_rej288.gif)
4243

4344
<h3><i>Streamlit's simplicity (but 4-10x faster) + FastAPI's full power + (Pydantic & SQL Alchemy)'s robustness</i></h3>
4445

nextpy/app.py

Lines changed: 112 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import contextlib
1010
import copy
1111
import functools
12+
import multiprocessing
1213
import os
14+
import platform
1315
from typing import (
1416
Any,
1517
AsyncIterator,
@@ -49,11 +51,13 @@
4951
State,
5052
StateManager,
5153
StateUpdate,
54+
code_uses_state_contexts,
5255
)
5356
from nextpy.base import Base
5457
from nextpy.build import prerequisites
5558
from nextpy.build.compiler import compiler
5659
from nextpy.build.compiler import utils as compiler_utils
60+
from nextpy.build.compiler.compiler import ExecutorSafeFunctions
5761
from nextpy.build.config import get_config
5862
from nextpy.data.model import Model
5963
from nextpy.frontend.components import connection_modal
@@ -554,6 +558,8 @@ def get_frontend_packages(self, imports: Dict[str, set[ReactImportVar]]):
554558
Example:
555559
>>> get_frontend_packages({"react": "16.14.0", "react-dom": "16.14.0"})
556560
"""
561+
if getattr(self, '_has_installed_frontend_packages', False):
562+
return
557563
page_imports = {
558564
i
559565
for i, tags in imports.items()
@@ -582,6 +588,7 @@ def get_frontend_packages(self, imports: Dict[str, set[ReactImportVar]]):
582588
_frontend_packages.append(package)
583589
page_imports.update(_frontend_packages)
584590
prerequisites.install_frontend_packages(page_imports)
591+
self._has_installed_frontend_packages = True
585592

586593
def _app_root(self, app_wrappers: dict[tuple[int, str], Component]) -> Component:
587594
for component in tuple(app_wrappers.values()):
@@ -633,6 +640,17 @@ def compile_(self):
633640
TimeElapsedColumn(),
634641
)
635642

643+
# try to be somewhat accurate - but still not 100%
644+
adhoc_steps_without_executor = 6
645+
fixed_pages_within_executor = 7
646+
progress.start()
647+
task = progress.add_task(
648+
"Compiling:",
649+
total=len(self.pages)
650+
+ fixed_pages_within_executor
651+
+ adhoc_steps_without_executor,
652+
)
653+
636654
# Get the env mode.
637655
config = get_config()
638656

@@ -641,7 +659,6 @@ def compile_(self):
641659

642660
# Compile the pages in parallel.
643661
custom_components = set()
644-
# TODO Anecdotally, processes=2 works 10% faster (cpu_count=12)
645662
all_imports = {}
646663
app_wrappers: Dict[tuple[int, str], Component] = {
647664
# Default app wrap component renders {children}
@@ -651,116 +668,141 @@ def compile_(self):
651668
# If a theme component was provided, wrap the app with it
652669
app_wrappers[(20, "Theme")] = self.theme
653670

654-
with progress, concurrent.futures.ThreadPoolExecutor() as thread_pool:
655-
fixed_pages = 7
656-
task = progress.add_task("Compiling:", total=len(self.pages) + fixed_pages)
671+
progress.advance(task)
657672

658-
def mark_complete(_=None):
659-
progress.advance(task)
673+
for _route, component in self.pages.items():
674+
# Merge the component style with the app style.
675+
component.add_style(self.style)
660676

661-
for _route, component in self.pages.items():
662-
# Merge the component style with the app style.
663-
component.add_style(self.style)
677+
component.apply_theme(self.theme)
664678

665-
component.apply_theme(self.theme)
679+
# Add component.get_imports() to all_imports.
680+
all_imports.update(component.get_imports())
666681

667-
# Add component.get_imports() to all_imports.
668-
all_imports.update(component.get_imports())
682+
# Add the app wrappers from this component.
683+
app_wrappers.update(component.get_app_wrap_components())
669684

670-
# Add the app wrappers from this component.
671-
app_wrappers.update(component.get_app_wrap_components())
685+
# Add the custom components from the page to the set.
686+
custom_components |= component.get_custom_components()
672687

673-
# Add the custom components from the page to the set.
674-
custom_components |= component.get_custom_components()
688+
progress.advance(task)
675689

676-
# Perform auto-memoization of stateful components.
677-
(
678-
stateful_components_path,
679-
stateful_components_code,
680-
page_components,
681-
) = compiler.compile_stateful_components(self.pages.values())
682-
compile_results.append((stateful_components_path, stateful_components_code))
683690

684-
result_futures = []
691+
# Perform auto-memoization of stateful components.
692+
(
693+
stateful_components_path,
694+
stateful_components_code,
695+
page_components,
696+
) = compiler.compile_stateful_components(self.pages.values())
685697

686-
def submit_work(fn, *args, **kwargs):
687-
"""Submit work to the thread pool and add a callback to mark the task as complete.
688698

689-
The Future will be added to the `result_futures` list.
699+
progress.advance(task)
700+
701+
# Catch "static" apps (that do not define a xt.State subclass) which are trying to access xt.State.
702+
if code_uses_state_contexts(stateful_components_code) and self.state is None:
703+
raise RuntimeError(
704+
"To access xt.State in frontend components, at least one "
705+
"subclass of xt.State must be defined in the app."
706+
)
707+
compile_results.append((stateful_components_path, stateful_components_code))
690708

691-
Args:
692-
fn: The function to submit.
693-
*args: The args to submit.
694-
**kwargs: The kwargs to submit.
695-
"""
696-
f = thread_pool.submit(fn, *args, **kwargs)
697-
f.add_done_callback(mark_complete)
709+
app_root = self._app_root(app_wrappers=app_wrappers)
710+
711+
progress.advance(task)
712+
713+
# Prepopulate the global ExecutorSafeFunctions class with input data required by the compile functions.
714+
# This is required for multiprocessing to work, in presence of non-picklable inputs.
715+
for route, component in zip(self.pages, page_components):
716+
ExecutorSafeFunctions.COMPILE_PAGE_ARGS_BY_ROUTE[route] = (
717+
route,
718+
component,
719+
self.state,
720+
)
721+
722+
ExecutorSafeFunctions.COMPILE_APP_APP_ROOT = app_root
723+
ExecutorSafeFunctions.CUSTOM_COMPONENTS = custom_components
724+
ExecutorSafeFunctions.HEAD_COMPONENTS = self.head_components
725+
ExecutorSafeFunctions.STYLE = self.style
726+
ExecutorSafeFunctions.STATE = self.state
727+
728+
# Use a forking process pool, if possible. Much faster, especially for large sites.
729+
# Fallback to ThreadPoolExecutor as something that will always work.
730+
executor = None
731+
if platform.system() in ("Linux", "Darwin"):
732+
executor = concurrent.futures.ProcessPoolExecutor(
733+
mp_context=multiprocessing.get_context("fork")
734+
)
735+
else:
736+
executor = concurrent.futures.ThreadPoolExecutor()
737+
738+
with executor:
739+
result_futures = []
740+
741+
def _mark_complete(_=None):
742+
progress.advance(task)
743+
744+
def _submit_work(fn, *args, **kwargs):
745+
f = executor.submit(fn, *args, **kwargs)
746+
f.add_done_callback(_mark_complete)
698747
result_futures.append(f)
699748

700749
# Compile all page components.
701-
for route, component in zip(self.pages, page_components):
702-
submit_work(
703-
compiler.compile_page,
704-
route,
705-
component,
706-
self.state,
707-
)
750+
for route in self.pages:
751+
_submit_work(ExecutorSafeFunctions.compile_page, route)
752+
708753

709754
# Compile the app wrapper.
710-
app_root = self._app_root(app_wrappers=app_wrappers)
711-
submit_work(compiler.compile_app, app_root)
755+
_submit_work(ExecutorSafeFunctions.compile_app)
756+
712757

713758
# Compile the custom components.
714-
submit_work(compiler.compile_components, custom_components)
759+
_submit_work(ExecutorSafeFunctions.compile_custom_components)
715760

716761
# Compile the root stylesheet with base styles.
717-
submit_work(compiler.compile_root_stylesheet, self.stylesheets)
762+
_submit_work(compiler.compile_root_stylesheet, self.stylesheets)
718763

719764
# Compile the root document.
720-
submit_work(compiler.compile_document_root, self.head_components)
765+
_submit_work(ExecutorSafeFunctions.compile_document_root)
721766

722767
# Compile the theme.
723-
submit_work(compiler.compile_theme, style=self.style)
768+
_submit_work(ExecutorSafeFunctions.compile_theme)
724769

725770
# Compile the contexts.
726-
submit_work(compiler.compile_contexts, self.state)
771+
_submit_work(ExecutorSafeFunctions.compile_contexts)
727772

728773
# Compile the Tailwind config.
729774
if config.tailwind is not None:
730775
config.tailwind["content"] = config.tailwind.get(
731776
"content", constants.Tailwind.CONTENT
732777
)
733-
submit_work(compiler.compile_tailwind, config.tailwind)
734-
735-
# Get imports from AppWrap components.
736-
all_imports.update(app_root.get_imports())
737-
738-
# Iterate through all the custom components and add their imports to the all_imports.
739-
for component in custom_components:
740-
all_imports.update(component.get_imports())
778+
_submit_work(compiler.compile_tailwind, config.tailwind)
779+
else:
780+
_submit_work(compiler.remove_tailwind_from_postcss)
741781

742782
# Wait for all compilation tasks to complete.
743783
for future in concurrent.futures.as_completed(result_futures):
744784
compile_results.append(future.result())
745785

746-
# Empty the .web pages directory.
747-
compiler.purge_web_pages_dir()
786+
# Get imports from AppWrap components.
787+
all_imports.update(app_root.get_imports())
748788

749-
# Avoid flickering when installing frontend packages
750-
progress.stop()
789+
# Iterate through all the custom components and add their imports to the all_imports.
790+
for component in custom_components:
791+
all_imports.update(component.get_imports())
751792

752-
# Install frontend packages.
753-
self.get_frontend_packages(all_imports)
793+
progress.advance(task)
754794

755-
# Write the pages at the end to trigger the NextJS hot reload only once.
756-
write_page_futures = []
757-
for output_path, code in compile_results:
758-
write_page_futures.append(
759-
thread_pool.submit(compiler_utils.write_page, output_path, code)
760-
)
761-
for future in concurrent.futures.as_completed(write_page_futures):
762-
future.result()
795+
# Empty the .web pages directory.
796+
compiler.purge_web_pages_dir()
797+
798+
progress.advance(task)
799+
progress.stop()
800+
801+
# Install frontend packages.
802+
self.get_frontend_packages(all_imports)
763803

804+
for output_path, code in compile_results:
805+
compiler_utils.write_page(output_path, code)
764806
@contextlib.asynccontextmanager
765807
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
766808
"""Modify the state out of band.

nextpy/backend/state.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,9 @@ def __init__(self, state_instance):
13271327
"""
13281328
super().__init__(state_instance)
13291329
# compile is not relevant to backend logic
1330+
#TODO: We're currently using this weirdo mechanism for installing initial packages
1331+
# gets to load_module -> compile -> get_frontend_packages -> install_frontend_packages
1332+
# We can improve this
13301333
self._self_app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
13311334
self._self_substate_path = state_instance.get_full_name().split(".")
13321335
self._self_actx = None
@@ -2142,3 +2145,14 @@ def _mark_dirty(
21422145
return super()._mark_dirty(
21432146
wrapped=wrapped, instance=instance, args=args, kwargs=kwargs
21442147
)
2148+
2149+
def code_uses_state_contexts(javascript_code: str) -> bool:
2150+
"""Check if the rendered Javascript uses state contexts.
2151+
2152+
Args:
2153+
javascript_code: The Javascript code to check.
2154+
2155+
Returns:
2156+
True if the code attempts to access a member of StateContexts.
2157+
"""
2158+
return bool("useContext(StateContexts" in javascript_code)

0 commit comments

Comments
 (0)