9
9
import contextlib
10
10
import copy
11
11
import functools
12
+ import multiprocessing
12
13
import os
14
+ import platform
13
15
from typing import (
14
16
Any ,
15
17
AsyncIterator ,
49
51
State ,
50
52
StateManager ,
51
53
StateUpdate ,
54
+ code_uses_state_contexts ,
52
55
)
53
56
from nextpy .base import Base
54
57
from nextpy .build import prerequisites
55
58
from nextpy .build .compiler import compiler
56
59
from nextpy .build .compiler import utils as compiler_utils
60
+ from nextpy .build .compiler .compiler import ExecutorSafeFunctions
57
61
from nextpy .build .config import get_config
58
62
from nextpy .data .model import Model
59
63
from nextpy .frontend .components import connection_modal
@@ -554,6 +558,8 @@ def get_frontend_packages(self, imports: Dict[str, set[ReactImportVar]]):
554
558
Example:
555
559
>>> get_frontend_packages({"react": "16.14.0", "react-dom": "16.14.0"})
556
560
"""
561
+ if getattr (self , '_has_installed_frontend_packages' , False ):
562
+ return
557
563
page_imports = {
558
564
i
559
565
for i , tags in imports .items ()
@@ -582,6 +588,7 @@ def get_frontend_packages(self, imports: Dict[str, set[ReactImportVar]]):
582
588
_frontend_packages .append (package )
583
589
page_imports .update (_frontend_packages )
584
590
prerequisites .install_frontend_packages (page_imports )
591
+ self ._has_installed_frontend_packages = True
585
592
586
593
def _app_root (self , app_wrappers : dict [tuple [int , str ], Component ]) -> Component :
587
594
for component in tuple (app_wrappers .values ()):
@@ -633,6 +640,17 @@ def compile_(self):
633
640
TimeElapsedColumn (),
634
641
)
635
642
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
+
636
654
# Get the env mode.
637
655
config = get_config ()
638
656
@@ -641,7 +659,6 @@ def compile_(self):
641
659
642
660
# Compile the pages in parallel.
643
661
custom_components = set ()
644
- # TODO Anecdotally, processes=2 works 10% faster (cpu_count=12)
645
662
all_imports = {}
646
663
app_wrappers : Dict [tuple [int , str ], Component ] = {
647
664
# Default app wrap component renders {children}
@@ -651,116 +668,141 @@ def compile_(self):
651
668
# If a theme component was provided, wrap the app with it
652
669
app_wrappers [(20 , "Theme" )] = self .theme
653
670
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 )
657
672
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 )
660
676
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 )
664
678
665
- component .apply_theme (self .theme )
679
+ # Add component.get_imports() to all_imports.
680
+ all_imports .update (component .get_imports ())
666
681
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 ())
669
684
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 ( )
672
687
673
- # Add the custom components from the page to the set.
674
- custom_components |= component .get_custom_components ()
688
+ progress .advance (task )
675
689
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 ))
683
690
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 ())
685
697
686
- def submit_work (fn , * args , ** kwargs ):
687
- """Submit work to the thread pool and add a callback to mark the task as complete.
688
698
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 ))
690
708
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 )
698
747
result_futures .append (f )
699
748
700
749
# 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
+
708
753
709
754
# 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
+
712
757
713
758
# Compile the custom components.
714
- submit_work ( compiler . compile_components , custom_components )
759
+ _submit_work ( ExecutorSafeFunctions . compile_custom_components )
715
760
716
761
# 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 )
718
763
719
764
# Compile the root document.
720
- submit_work ( compiler .compile_document_root , self . head_components )
765
+ _submit_work ( ExecutorSafeFunctions .compile_document_root )
721
766
722
767
# Compile the theme.
723
- submit_work ( compiler .compile_theme , style = self . style )
768
+ _submit_work ( ExecutorSafeFunctions .compile_theme )
724
769
725
770
# Compile the contexts.
726
- submit_work ( compiler .compile_contexts , self . state )
771
+ _submit_work ( ExecutorSafeFunctions .compile_contexts )
727
772
728
773
# Compile the Tailwind config.
729
774
if config .tailwind is not None :
730
775
config .tailwind ["content" ] = config .tailwind .get (
731
776
"content" , constants .Tailwind .CONTENT
732
777
)
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 )
741
781
742
782
# Wait for all compilation tasks to complete.
743
783
for future in concurrent .futures .as_completed (result_futures ):
744
784
compile_results .append (future .result ())
745
785
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 () )
748
788
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 ())
751
792
752
- # Install frontend packages.
753
- self .get_frontend_packages (all_imports )
793
+ progress .advance (task )
754
794
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 )
763
803
804
+ for output_path , code in compile_results :
805
+ compiler_utils .write_page (output_path , code )
764
806
@contextlib .asynccontextmanager
765
807
async def modify_state (self , token : str ) -> AsyncIterator [BaseState ]:
766
808
"""Modify the state out of band.
0 commit comments