Skip to content

Commit 9532f3c

Browse files
committed
Pull changes from parent + poetry lock updated
1 parent 73cbcdd commit 9532f3c

File tree

7 files changed

+713
-296
lines changed

7 files changed

+713
-296
lines changed

nextpy/app.py

Lines changed: 109 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
@@ -633,6 +637,17 @@ def compile_(self):
633637
TimeElapsedColumn(),
634638
)
635639

640+
# try to be somewhat accurate - but still not 100%
641+
adhoc_steps_without_executor = 6
642+
fixed_pages_within_executor = 7
643+
progress.start()
644+
task = progress.add_task(
645+
"Compiling:",
646+
total=len(self.pages)
647+
+ fixed_pages_within_executor
648+
+ adhoc_steps_without_executor,
649+
)
650+
636651
# Get the env mode.
637652
config = get_config()
638653

@@ -641,7 +656,6 @@ def compile_(self):
641656

642657
# Compile the pages in parallel.
643658
custom_components = set()
644-
# TODO Anecdotally, processes=2 works 10% faster (cpu_count=12)
645659
all_imports = {}
646660
app_wrappers: Dict[tuple[int, str], Component] = {
647661
# Default app wrap component renders {children}
@@ -651,116 +665,141 @@ def compile_(self):
651665
# If a theme component was provided, wrap the app with it
652666
app_wrappers[(20, "Theme")] = self.theme
653667

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)
668+
progress.advance(task)
657669

658-
def mark_complete(_=None):
659-
progress.advance(task)
670+
for _route, component in self.pages.items():
671+
# Merge the component style with the app style.
672+
component.add_style(self.style)
660673

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

665-
component.apply_theme(self.theme)
676+
# Add component.get_imports() to all_imports.
677+
all_imports.update(component.get_imports())
666678

667-
# Add component.get_imports() to all_imports.
668-
all_imports.update(component.get_imports())
679+
# Add the app wrappers from this component.
680+
app_wrappers.update(component.get_app_wrap_components())
669681

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

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

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))
683687

684-
result_futures = []
688+
# Perform auto-memoization of stateful components.
689+
(
690+
stateful_components_path,
691+
stateful_components_code,
692+
page_components,
693+
) = compiler.compile_stateful_components(self.pages.values())
694+
695+
696+
progress.advance(task)
697+
698+
# Catch "static" apps (that do not define a xt.State subclass) which are trying to access xt.State.
699+
if code_uses_state_contexts(stateful_components_code) and self.state is None:
700+
raise RuntimeError(
701+
"To access xt.State in frontend components, at least one "
702+
"subclass of xt.State must be defined in the app."
703+
)
704+
compile_results.append((stateful_components_path, stateful_components_code))
705+
706+
app_root = self._app_root(app_wrappers=app_wrappers)
707+
708+
progress.advance(task)
709+
710+
# Prepopulate the global ExecutorSafeFunctions class with input data required by the compile functions.
711+
# This is required for multiprocessing to work, in presence of non-picklable inputs.
712+
for route, component in zip(self.pages, page_components):
713+
ExecutorSafeFunctions.COMPILE_PAGE_ARGS_BY_ROUTE[route] = (
714+
route,
715+
component,
716+
self.state,
717+
)
685718

686-
def submit_work(fn, *args, **kwargs):
687-
"""Submit work to the thread pool and add a callback to mark the task as complete.
719+
ExecutorSafeFunctions.COMPILE_APP_APP_ROOT = app_root
720+
ExecutorSafeFunctions.CUSTOM_COMPONENTS = custom_components
721+
ExecutorSafeFunctions.HEAD_COMPONENTS = self.head_components
722+
ExecutorSafeFunctions.STYLE = self.style
723+
ExecutorSafeFunctions.STATE = self.state
724+
725+
# Use a forking process pool, if possible. Much faster, especially for large sites.
726+
# Fallback to ThreadPoolExecutor as something that will always work.
727+
executor = None
728+
if platform.system() in ("Linux", "Darwin"):
729+
executor = concurrent.futures.ProcessPoolExecutor(
730+
mp_context=multiprocessing.get_context("fork")
731+
)
732+
else:
733+
executor = concurrent.futures.ThreadPoolExecutor()
688734

689-
The Future will be added to the `result_futures` list.
735+
with executor:
736+
result_futures = []
690737

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)
738+
def _mark_complete(_=None):
739+
progress.advance(task)
740+
741+
def _submit_work(fn, *args, **kwargs):
742+
f = executor.submit(fn, *args, **kwargs)
743+
f.add_done_callback(_mark_complete)
698744
result_futures.append(f)
699745

700746
# 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-
)
747+
for route in self.pages:
748+
_submit_work(ExecutorSafeFunctions.compile_page, route)
749+
708750

709751
# Compile the app wrapper.
710-
app_root = self._app_root(app_wrappers=app_wrappers)
711-
submit_work(compiler.compile_app, app_root)
752+
_submit_work(ExecutorSafeFunctions.compile_app)
753+
712754

713755
# Compile the custom components.
714-
submit_work(compiler.compile_components, custom_components)
756+
_submit_work(ExecutorSafeFunctions.compile_custom_components)
715757

716758
# Compile the root stylesheet with base styles.
717-
submit_work(compiler.compile_root_stylesheet, self.stylesheets)
759+
_submit_work(compiler.compile_root_stylesheet, self.stylesheets)
718760

719761
# Compile the root document.
720-
submit_work(compiler.compile_document_root, self.head_components)
762+
_submit_work(ExecutorSafeFunctions.compile_document_root)
721763

722764
# Compile the theme.
723-
submit_work(compiler.compile_theme, style=self.style)
765+
_submit_work(ExecutorSafeFunctions.compile_theme)
724766

725767
# Compile the contexts.
726-
submit_work(compiler.compile_contexts, self.state)
768+
_submit_work(ExecutorSafeFunctions.compile_contexts)
727769

728770
# Compile the Tailwind config.
729771
if config.tailwind is not None:
730772
config.tailwind["content"] = config.tailwind.get(
731773
"content", constants.Tailwind.CONTENT
732774
)
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())
775+
_submit_work(compiler.compile_tailwind, config.tailwind)
776+
else:
777+
_submit_work(compiler.remove_tailwind_from_postcss)
741778

742779
# Wait for all compilation tasks to complete.
743780
for future in concurrent.futures.as_completed(result_futures):
744781
compile_results.append(future.result())
745782

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

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

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

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()
792+
# Empty the .web pages directory.
793+
compiler.purge_web_pages_dir()
794+
795+
progress.advance(task)
796+
progress.stop()
797+
798+
# Install frontend packages.
799+
self.get_frontend_packages(all_imports)
763800

801+
for output_path, code in compile_results:
802+
compiler_utils.write_page(output_path, code)
764803
@contextlib.asynccontextmanager
765804
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
766805
"""Modify the state out of band.

nextpy/backend/state.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2142,3 +2142,14 @@ def _mark_dirty(
21422142
return super()._mark_dirty(
21432143
wrapped=wrapped, instance=instance, args=args, kwargs=kwargs
21442144
)
2145+
2146+
def code_uses_state_contexts(javascript_code: str) -> bool:
2147+
"""Check if the rendered Javascript uses state contexts.
2148+
2149+
Args:
2150+
javascript_code: The Javascript code to check.
2151+
2152+
Returns:
2153+
True if the code attempts to access a member of StateContexts.
2154+
"""
2155+
return bool("useContext(StateContexts" in javascript_code)

nextpy/build/compiler/compiler.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,3 +438,117 @@ def compile_tailwind(
438438
def purge_web_pages_dir():
439439
"""Empty out .web directory."""
440440
utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"])
441+
442+
443+
class ExecutorSafeFunctions:
444+
"""A helper class designed for easier parallel processing during compilation tasks.
445+
446+
Key Points:
447+
- This class is accessible globally, meaning it can be used anywhere in your code.
448+
- It's especially useful when you're working with multiple processes, like with a ProcessPoolExecutor.
449+
- Here's how it works:
450+
1. Before creating a new (child) process, we save any necessary input data in this class.
451+
2. When a new process starts (forks), it gets its own copy of this class, including the saved input data.
452+
3. The child process uses this copied data to know what it needs to work on.
453+
454+
Why is this useful?
455+
- Sometimes, you can't directly send input data to a new process because it's not in a format that can be easily transferred (not 'picklable').
456+
- Our method bypasses this problem by storing the data in the class, so there's no need to transfer it in the traditional way.
457+
458+
Attributes:
459+
COMPILE_PAGE_ARGS_BY_ROUTE (dict): A mapping of routes to their respective page compilation arguments.
460+
COMPILE_APP_APP_ROOT (Component | None): The root component of the app, used in app compilation.
461+
CUSTOM_COMPONENTS (set[CustomComponent] | None): A set of custom components used in the compilation process.
462+
HEAD_COMPONENTS (list[Component] | None): A list of components that form the document head.
463+
STYLE (ComponentStyle | None): The style configuration for components.
464+
STATE (type[BaseState] | None): The state type used in context compilation.
465+
466+
Limitations:
467+
- It can't handle data that can't be transferred (unpicklable) as output.
468+
- Changes made in a child process won't affect the original data in the parent process.
469+
470+
"""
471+
472+
COMPILE_PAGE_ARGS_BY_ROUTE = {}
473+
COMPILE_APP_APP_ROOT: Component | None = None
474+
CUSTOM_COMPONENTS: set[CustomComponent] | None = None
475+
HEAD_COMPONENTS: list[Component] | None = None
476+
STYLE: ComponentStyle | None = None
477+
STATE: type[BaseState] | None = None
478+
479+
@classmethod
480+
def compile_page(cls, route: str):
481+
"""Compile a page.
482+
483+
Args:
484+
route: The route of the page to compile.
485+
486+
Returns:
487+
The path and code of the compiled page.
488+
"""
489+
return compile_page(*cls.COMPILE_PAGE_ARGS_BY_ROUTE[route])
490+
491+
@classmethod
492+
def compile_app(cls):
493+
"""Compile the app.
494+
495+
Returns:
496+
The path and code of the compiled app.
497+
498+
Raises:
499+
ValueError: If the app root is not set.
500+
"""
501+
if cls.COMPILE_APP_APP_ROOT is None:
502+
raise ValueError("COMPILE_APP_APP_ROOT should be set")
503+
return compile_app(cls.COMPILE_APP_APP_ROOT)
504+
505+
@classmethod
506+
def compile_custom_components(cls):
507+
"""Compile the custom components.
508+
509+
Returns:
510+
The path and code of the compiled custom components.
511+
512+
Raises:
513+
ValueError: If the custom components are not set.
514+
"""
515+
if cls.CUSTOM_COMPONENTS is None:
516+
raise ValueError("CUSTOM_COMPONENTS should be set")
517+
return compile_components(cls.CUSTOM_COMPONENTS)
518+
519+
@classmethod
520+
def compile_document_root(cls):
521+
"""Compile the document root.
522+
523+
Returns:
524+
The path and code of the compiled document root.
525+
526+
Raises:
527+
ValueError: If the head components are not set.
528+
"""
529+
if cls.HEAD_COMPONENTS is None:
530+
raise ValueError("HEAD_COMPONENTS should be set")
531+
return compile_document_root(cls.HEAD_COMPONENTS)
532+
533+
@classmethod
534+
def compile_theme(cls):
535+
"""Compile the theme.
536+
537+
Returns:
538+
The path and code of the compiled theme.
539+
540+
Raises:
541+
ValueError: If the style is not set.
542+
"""
543+
if cls.STYLE is None:
544+
raise ValueError("STYLE should be set")
545+
return compile_theme(cls.STYLE)
546+
547+
@classmethod
548+
def compile_contexts(cls):
549+
"""Compile the contexts.
550+
551+
Returns:
552+
The path and code of the compiled contexts.
553+
"""
554+
return compile_contexts(cls.STATE)

0 commit comments

Comments
 (0)