Skip to content

Commit 8e8f865

Browse files
committedNov 26, 2023
make concurrent renders configurable
1 parent b66d987 commit 8e8f865

File tree

5 files changed

+49
-7
lines changed

5 files changed

+49
-7
lines changed
 

‎docs/source/about/changelog.rst

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ Unreleased
2828
- :pull:`1118` - `module_from_template` is broken with a recent release of `requests`
2929
- :pull:`1131` - `module_from_template` did not work when using Flask backend
3030

31+
**Added**
32+
33+
- :pull:`1093` - Better async effects (see :ref:`Async Effects`)
34+
- :pull:`1093` - Support concurrent renders - multiple components are now able to render
35+
simultaneously. This is a significant change to the underlying rendering logic and
36+
should be considered experimental. You can enable this feature by setting
37+
``REACTPY_FEATURE_CONCURRENT_RENDER=1`` when running ReactPy.
38+
3139

3240
v1.0.2
3341
------

‎src/py/reactpy/reactpy/config.py

+8
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,11 @@ def boolean(value: str | bool | int) -> bool:
8888
validator=float,
8989
)
9090
"""The default amount of time to wait for an effect to complete"""
91+
92+
REACTPY_CONCURRENT_RENDERING = Option(
93+
"REACTPY_CONCURRENT_RENDERING",
94+
default=False,
95+
mutable=True,
96+
validator=boolean,
97+
)
98+
"""Whether to render components concurrently. This is currently an experimental feature."""

‎src/py/reactpy/reactpy/core/_life_cycle_hook.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ class LifeCycleHook:
9494
"_effect_funcs",
9595
"_effect_starts",
9696
"_effect_stops",
97-
"_is_rendering",
9897
"_render_access",
9998
"_rendered_atleast_once",
10099
"_schedule_render_callback",
@@ -112,7 +111,6 @@ def __init__(
112111
self._context_providers: dict[Context[Any], ContextProviderType[Any]] = {}
113112
self._schedule_render_callback = schedule_render
114113
self._schedule_render_later = False
115-
self._is_rendering = False
116114
self._rendered_atleast_once = False
117115
self._current_state_index = 0
118116
self._state: tuple[Any, ...] = ()
@@ -121,7 +119,7 @@ def __init__(
121119
self._render_access = Semaphore(1) # ensure only one render at a time
122120

123121
def schedule_render(self) -> None:
124-
if self._is_rendering:
122+
if self._is_rendering():
125123
self._schedule_render_later = True
126124
else:
127125
self._schedule_render()
@@ -153,14 +151,12 @@ async def affect_component_will_render(self, component: ComponentType) -> None:
153151
"""The component is about to render"""
154152
await self._render_access.acquire()
155153
self.component = component
156-
self._is_rendering = True
157154
self.set_current()
158155

159156
async def affect_component_did_render(self) -> None:
160157
"""The component completed a render"""
161158
self.unset_current()
162159
del self.component
163-
self._is_rendering = False
164160
self._rendered_atleast_once = True
165161
self._current_state_index = 0
166162
self._render_access.release()
@@ -202,6 +198,9 @@ def unset_current(self) -> None:
202198
if _HOOK_STATE.get().pop() is not self:
203199
raise RuntimeError("Hook stack is in an invalid state") # nocov
204200

201+
def _is_rendering(self) -> bool:
202+
return self._render_access.value != 0
203+
205204
def _schedule_render(self) -> None:
206205
try:
207206
self._schedule_render_callback()

‎src/py/reactpy/reactpy/core/layout.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
from uuid import uuid4
2828
from weakref import ref as weakref
2929

30-
from reactpy.config import REACTPY_CHECK_VDOM_SPEC, REACTPY_DEBUG_MODE
30+
from reactpy.config import (
31+
REACTPY_CHECK_VDOM_SPEC,
32+
REACTPY_CONCURRENT_RENDERING,
33+
REACTPY_DEBUG_MODE,
34+
)
3135
from reactpy.core._life_cycle_hook import LifeCycleHook
3236
from reactpy.core.types import (
3337
ComponentType,
@@ -112,6 +116,26 @@ async def deliver(self, event: LayoutEventMessage) -> None:
112116
)
113117

114118
async def render(self) -> LayoutUpdateMessage:
119+
if REACTPY_CONCURRENT_RENDERING.current:
120+
return await self._concurrent_render()
121+
else: # nocov
122+
return await self._serial_render()
123+
124+
async def _serial_render(self) -> LayoutUpdateMessage: # nocov
125+
"""Await the next available render. This will block until a component is updated"""
126+
while True:
127+
model_state_id = await self._rendering_queue.get()
128+
try:
129+
model_state = self._model_states_by_life_cycle_state_id[model_state_id]
130+
except KeyError:
131+
logger.debug(
132+
"Did not render component with model state ID "
133+
f"{model_state_id!r} - component already unmounted"
134+
)
135+
else:
136+
return await self._create_layout_update(model_state)
137+
138+
async def _concurrent_render(self) -> LayoutUpdateMessage:
115139
"""Await the next available render. This will block until a component is updated"""
116140
while True:
117141
render_completed = (

‎src/py/reactpy/tests/conftest.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from _pytest.config.argparsing import Parser
99
from playwright.async_api import async_playwright
1010

11-
from reactpy.config import REACTPY_TESTING_DEFAULT_TIMEOUT
11+
from reactpy.config import REACTPY_CONCURRENT_RENDERING, REACTPY_TESTING_DEFAULT_TIMEOUT
1212
from reactpy.testing import (
1313
BackendFixture,
1414
DisplayFixture,
@@ -27,6 +27,9 @@ def pytest_addoption(parser: Parser) -> None:
2727
)
2828

2929

30+
REACTPY_CONCURRENT_RENDERING.current = True
31+
32+
3033
@pytest.fixture
3134
async def display(server, page):
3235
async with DisplayFixture(server, page) as display:

0 commit comments

Comments
 (0)