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
@@ -633,6 +637,17 @@ def compile_(self):
633
637
TimeElapsedColumn (),
634
638
)
635
639
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
+
636
651
# Get the env mode.
637
652
config = get_config ()
638
653
@@ -641,7 +656,6 @@ def compile_(self):
641
656
642
657
# Compile the pages in parallel.
643
658
custom_components = set ()
644
- # TODO Anecdotally, processes=2 works 10% faster (cpu_count=12)
645
659
all_imports = {}
646
660
app_wrappers : Dict [tuple [int , str ], Component ] = {
647
661
# Default app wrap component renders {children}
@@ -651,116 +665,141 @@ def compile_(self):
651
665
# If a theme component was provided, wrap the app with it
652
666
app_wrappers [(20 , "Theme" )] = self .theme
653
667
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 )
657
669
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 )
660
673
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 )
664
675
665
- component .apply_theme (self .theme )
676
+ # Add component.get_imports() to all_imports.
677
+ all_imports .update (component .get_imports ())
666
678
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 ())
669
681
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 ( )
672
684
673
- # Add the custom components from the page to the set.
674
- custom_components |= component .get_custom_components ()
685
+ progress .advance (task )
675
686
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
687
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
+ )
685
718
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 ()
688
734
689
- The Future will be added to the `result_futures` list.
735
+ with executor :
736
+ result_futures = []
690
737
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 )
698
744
result_futures .append (f )
699
745
700
746
# 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
+
708
750
709
751
# 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
+
712
754
713
755
# Compile the custom components.
714
- submit_work ( compiler . compile_components , custom_components )
756
+ _submit_work ( ExecutorSafeFunctions . compile_custom_components )
715
757
716
758
# 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 )
718
760
719
761
# Compile the root document.
720
- submit_work ( compiler .compile_document_root , self . head_components )
762
+ _submit_work ( ExecutorSafeFunctions .compile_document_root )
721
763
722
764
# Compile the theme.
723
- submit_work ( compiler .compile_theme , style = self . style )
765
+ _submit_work ( ExecutorSafeFunctions .compile_theme )
724
766
725
767
# Compile the contexts.
726
- submit_work ( compiler .compile_contexts , self . state )
768
+ _submit_work ( ExecutorSafeFunctions .compile_contexts )
727
769
728
770
# Compile the Tailwind config.
729
771
if config .tailwind is not None :
730
772
config .tailwind ["content" ] = config .tailwind .get (
731
773
"content" , constants .Tailwind .CONTENT
732
774
)
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 )
741
778
742
779
# Wait for all compilation tasks to complete.
743
780
for future in concurrent .futures .as_completed (result_futures ):
744
781
compile_results .append (future .result ())
745
782
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 () )
748
785
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 ())
751
789
752
- # Install frontend packages.
753
- self .get_frontend_packages (all_imports )
790
+ progress .advance (task )
754
791
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 )
763
800
801
+ for output_path , code in compile_results :
802
+ compiler_utils .write_page (output_path , code )
764
803
@contextlib .asynccontextmanager
765
804
async def modify_state (self , token : str ) -> AsyncIterator [BaseState ]:
766
805
"""Modify the state out of band.
0 commit comments