Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6738.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Skip deep element-wise type validation on state var hot paths: computed var return types are only checked on recompute (not on cache hits), and production mode no longer walks every element of assigned containers for the log-only type check.
1 change: 1 addition & 0 deletions news/6740.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Speed up MutableProxy: immutable elements retrieved through a proxy skip the dataclasses frame-walk check, and the proxy for each mutable state var is cached per instance instead of rebuilt on every attribute read.
1 change: 1 addition & 0 deletions news/6743.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dirty propagation now only walks newly-dirty vars per mutation and skips the computed var expiry scan for classes with no interval vars, roughly halving per-mutation overhead in states with computed vars.
1 change: 1 addition & 0 deletions packages/reflex-base/news/6738.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Skip deep element-wise type validation on state var hot paths: computed var return types are only checked on recompute (not on cache hits), and production mode no longer walks every element of assigned containers for the log-only type check.
1 change: 1 addition & 0 deletions packages/reflex-base/news/6740.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reserve the internal `_mutable_proxy_cache` state field name so user vars cannot collide with the per-instance proxy cache.
1 change: 1 addition & 0 deletions packages/reflex-base/news/6743.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dirty propagation now only walks newly-dirty vars per mutation and skips the computed var expiry scan for classes with no interval vars, roughly halving per-mutation overhead in states with computed vars.
42 changes: 41 additions & 1 deletion packages/reflex-base/src/reflex_base/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import dataclasses
import os
import sys
import types
from collections.abc import Callable, Iterable, Mapping, Sequence
Expand Down Expand Up @@ -161,7 +162,15 @@ def __call__(
dict: Dict, # noqa: UP006
}

RESERVED_BACKEND_VAR_NAMES = {"_abc_impl", "_backend_vars", "_was_touched", "_mixin"}
RESERVED_BACKEND_VAR_NAMES = {
"_abc_impl",
"_backend_vars",
"_was_touched",
"_mixin",
"_mutable_proxy_cache",
"_propagated_dirty_vars",
"_propagated_generation",
}


class Unset:
Expand Down Expand Up @@ -633,6 +642,37 @@ def does_obj_satisfy_typed_dict(
return required_keys.issubset(frozenset(obj))


@lru_cache
def _validation_depth_for_mode(raw_mode: str) -> int:
"""Get the validation depth for a raw REFLEX_ENV_MODE value.

Args:
raw_mode: The stripped environment variable value ("" if unset).

Returns:
The `nested` depth to pass to `_isinstance`.
"""
return 0 if raw_mode == constants.Env.PROD.value else 1


def _validation_depth() -> int:
"""Get the container depth for hot-path state var type validation.

The result of these checks only gates a diagnostic log, so production
mode skips the per-element walk of large containers and only validates
the outer type. The environment is re-read on every call so in-process
mode changes take effect immediately.

Returns:
The `nested` depth to pass to `_isinstance`.
"""
# Read the raw env var directly: interpreting it through
# environment.REFLEX_ENV_MODE.get() on this hot path would re-parse the
# enum on every state var assignment (and the import would be circular).
# Strip to match the canonical parser's whitespace tolerance.
return _validation_depth_for_mode(os.environ.get("REFLEX_ENV_MODE", "").strip())


def _isinstance(
obj: Any,
cls: GenericType,
Expand Down
55 changes: 39 additions & 16 deletions packages/reflex-base/src/reflex_base/vars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
GenericType,
Self,
_isinstance,
_validation_depth,
get_origin,
has_args,
safe_issubclass,
Expand Down Expand Up @@ -2255,6 +2256,19 @@ def is_computed_var(obj: Any) -> TypeGuard[ComputedVar]:
return isinstance(obj, FakeComputedVarBaseClass)


# Incremented whenever a cached computed var is recomputed. State dirty
# propagation uses this to know when an already-propagated dependency needs to
# be re-propagated (a recompute re-materializes a cache that a later mutation
# of its dependencies must invalidate again).
_computed_var_recompute_generation: int = 0


def _bump_computed_var_recompute_generation() -> None:
"""Record that a cached computed var was recomputed."""
global _computed_var_recompute_generation
_computed_var_recompute_generation += 1


@dataclasses.dataclass(
eq=False,
frozen=True,
Expand Down Expand Up @@ -2587,23 +2601,29 @@ def __get__(self, instance: BaseState | None, owner: type):

if not self._cache:
value = self.fget(instance)
else:
# handle caching
if not hasattr(instance, self._cache_attr) or self.needs_update(instance):
# Set cache attr on state instance.
setattr(instance, self._cache_attr, self.fget(instance))
# Ensure the computed var gets serialized to redis.
instance._was_touched = True
# Set the last updated timestamp on the state instance.
setattr(instance, self._last_updated_attr, datetime.datetime.now())
value = getattr(instance, self._cache_attr)
self._check_deprecated_return_type(instance, value)
return value

self._check_deprecated_return_type(instance, value)
# handle caching
if not hasattr(instance, self._cache_attr) or self.needs_update(instance):
# Set cache attr on state instance.
setattr(instance, self._cache_attr, self.fget(instance))
# Ensure the computed var gets serialized to redis.
instance._was_touched = True
# Set the last updated timestamp on the state instance.
setattr(instance, self._last_updated_attr, datetime.datetime.now())
_bump_computed_var_recompute_generation()
value = getattr(instance, self._cache_attr)
# Only validate the return type when the value was just computed.
self._check_deprecated_return_type(instance, value)
return value

return value
return getattr(instance, self._cache_attr)

def _check_deprecated_return_type(self, instance: BaseState, value: Any) -> None:
if not _isinstance(value, self._var_type, nested=1, treat_var_as_type=False):
if not _isinstance(
value, self._var_type, nested=_validation_depth(), treat_var_as_type=False
):
console.error(
f"Computed var '{type(instance).__name__}.{self._name}' must return"
f" a value of type '{escape(str(self._var_type))}', got '{value!s}' of type {type(value)}."
Expand Down Expand Up @@ -2858,9 +2878,12 @@ async def _awaitable_result(instance: BaseState = instance) -> RETURN_TYPE:
instance._was_touched = True
# Set the last updated timestamp on the state instance.
setattr(instance, self._last_updated_attr, datetime.datetime.now())
value = getattr(instance, self._cache_attr)
self._check_deprecated_return_type(instance, value)
return value
_bump_computed_var_recompute_generation()
value = getattr(instance, self._cache_attr)
# Only validate the return type when the value was just computed.
self._check_deprecated_return_type(instance, value)
return value
return getattr(instance, self._cache_attr)

return _awaitable_result()

Expand Down
7 changes: 4 additions & 3 deletions reflex/istate/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,13 +664,14 @@ def _wrap_mutable(self, value: Any, path: tuple[_AccessSpec, ...]) -> Any:
Returns:
The wrapped value.
"""
# When called from dataclasses internal code, return the unwrapped value
if self._is_called_from_dataclasses_internal():
return value
# If we already have a proxy, unwrap and rewrap to make sure the state
# reference is up to date.
if isinstance(value, MutableProxy):
value = value.__wrapped__
# When called from dataclasses internal code, return the unwrapped value
if self._is_called_from_dataclasses_internal():
return value
# Recursively wrap mutable types.
return globals()[self.__base_proxy__](
wrapped=value,
state=self._self_state,
Expand Down
Loading
Loading