Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Validating a list, set, tuple or dict assigned to a state var is faster: items whose type is exactly the declared item type no longer go through the full type check one by one.
134 changes: 69 additions & 65 deletions packages/reflex-base/src/reflex_base/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,34 @@ def _load_var_classes() -> None:
_Var, _LiteralVar, _Field = Var, LiteralVar, Field


def _all_isinstance(
items: Iterable[Any], cls: GenericType, nested: int, treat_var_as_type: bool
) -> bool:
"""Check that every item of a container is an instance of a class.

An item whose type is exactly ``cls`` passes without the full check, which
spares the common container of plain values a call per item. Var classes
are left out of that: ``_isinstance`` checks a Var by the value it holds.
Must be called from ``_isinstance``, after the Var classes are loaded.

Args:
items: The items to check.
cls: The class every item must be an instance of.
nested: How many levels deep to check each item.
treat_var_as_type: Whether to treat Var as the type it represents.

Returns:
Whether every item is an instance of the class.
"""
exact = None if isinstance(cls, type) and issubclass(cls, _Var) else cls
for item in items:
if type(item) is not exact and not _isinstance(
item, cls, nested=nested, treat_var_as_type=treat_var_as_type
):
return False
return True


def _isinstance(
obj: Any,
cls: GenericType,
Expand Down Expand Up @@ -1399,13 +1427,17 @@ def _isinstance(
# attribute; container checks call back in once per element, so read it once.
origin_attr = getattr(cls, "__origin__", None)

# Loops rather than generator expressions throughout: a generator capturing
# the arguments makes them closure cells, allocated on every call.
if origin_attr is Union or (
origin_attr is None and isinstance(cls, types.UnionType)
):
return any(
_isinstance(obj, arg, nested=nested, treat_var_as_type=treat_var_as_type)
for arg in _get_args_cached(cls)
)
for arg in _get_args_cached(cls):
if _isinstance(
obj, arg, nested=nested, treat_var_as_type=treat_var_as_type
):
return True
return False

if origin_attr is Literal:
return obj in _get_args_cached(cls)
Expand Down Expand Up @@ -1453,20 +1485,20 @@ def _isinstance(
if cls.__bound__ is not None
else cls.__constraints__
)
return (
not bounds
or any(isinstance(bound, (str, ForwardRef)) for bound in bounds)
or any(
_isinstance(
obj,
bound,
nested=nested,
treat_var_as_type=treat_var_as_type,
treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable,
)
for bound in bounds
)
)
if not bounds or any(
isinstance(bound, (str, ForwardRef)) for bound in bounds
):
return True
for bound in bounds:
if _isinstance(
obj,
bound,
nested=nested,
treat_var_as_type=treat_var_as_type,
treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable,
):
return True
return False
if typing_extensions.is_protocol(cls):
# A protocol without @runtime_checkable cannot be checked.
return True
Expand Down Expand Up @@ -1496,67 +1528,39 @@ def _isinstance(
if nested > 0 and args:
if origin is list:
expected_class = Sequence if treat_mutable_obj_as_immutable else list
return isinstance(obj, expected_class) and all(
_isinstance(
item,
args[0],
nested=nested - 1,
treat_var_as_type=treat_var_as_type,
)
for item in obj
return isinstance(obj, expected_class) and _all_isinstance(
obj, args[0], nested - 1, treat_var_as_type
)
if origin is tuple:
if args[-1] is Ellipsis:
return isinstance(obj, tuple) and all(
_isinstance(
item,
args[0],
nested=nested - 1,
treat_var_as_type=treat_var_as_type,
)
for item in obj
return isinstance(obj, tuple) and _all_isinstance(
obj, args[0], nested - 1, treat_var_as_type
)
return (
isinstance(obj, tuple)
and len(obj) == len(args)
and all(
_isinstance(
item,
arg,
nested=nested - 1,
treat_var_as_type=treat_var_as_type,
)
for item, arg in zip(obj, args, strict=True)
)
)
if not isinstance(obj, tuple) or len(obj) != len(args):
return False
for item, arg in zip(obj, args, strict=True):
if not _isinstance(
item, arg, nested=nested - 1, treat_var_as_type=treat_var_as_type
):
return False
return True
if safe_issubclass(origin, Mapping):
expected_class = (
dict
if origin is dict and not treat_mutable_obj_as_immutable
else Mapping
)
return isinstance(obj, expected_class) and all(
_isinstance(
key, args[0], nested=nested - 1, treat_var_as_type=treat_var_as_type
)
and _isinstance(
value,
args[1],
nested=nested - 1,
treat_var_as_type=treat_var_as_type,
return (
isinstance(obj, expected_class)
and _all_isinstance(obj.keys(), args[0], nested - 1, treat_var_as_type)
and _all_isinstance(
obj.values(), args[1], nested - 1, treat_var_as_type
)
for key, value in obj.items()
)
if origin is set:
expected_class = Sequence if treat_mutable_obj_as_immutable else set
return isinstance(obj, expected_class) and all(
_isinstance(
item,
args[0],
nested=nested - 1,
treat_var_as_type=treat_var_as_type,
)
for item in obj
return isinstance(obj, expected_class) and _all_isinstance(
obj, args[0], nested - 1, treat_var_as_type
)

if args and origin is _Field:
Expand Down
12 changes: 10 additions & 2 deletions tests/benchmarks/test_event_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

from .fixtures import BenchmarkState

FROM_EVENT_TYPE_CALLS = 500


def test_console_log(benchmark: BenchmarkFixture):
"""Benchmark creation of an ``rx.console_log`` event spec.
Expand Down Expand Up @@ -96,10 +98,16 @@ def test_from_event_type(event_input: Any, benchmark: BenchmarkFixture):

Covers existing Event, EventSpec (from calling EventHandler), and lambdas
returning an Event, EventSpec, or EventHandler — the common shapes encountered
when normalizing user-returned event values.
when normalizing user-returned event values. One conversion takes a few
microseconds, so each measured call converts ``FROM_EVENT_TYPE_CALLS`` times.

Args:
event_input: The parametrized event-like input.
benchmark: The codspeed benchmark fixture.
"""
benchmark(lambda: Event.from_event_type(event_input))
converted = benchmark(
lambda: [
Event.from_event_type(event_input) for _ in range(FROM_EVENT_TYPE_CALLS)
]
)
assert converted == [Event.from_event_type(event_input)] * FROM_EVENT_TYPE_CALLS
19 changes: 11 additions & 8 deletions tests/benchmarks/test_event_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# Drain budget far above any instrumented runtime so measured shutdowns never
# flip from graceful drain to cancellation under benchmark slowdown.
_DRAIN_BUDGET_SECONDS = 60.0
EMIT_UPDATES = 50


async def _noop_handler() -> None:
Expand Down Expand Up @@ -397,11 +398,12 @@ def run(processor: EventProcessor) -> list[Mapping[str, Any]]:


def test_emit_update(benchmark: BenchmarkFixture):
"""Benchmark emit_update's socket lookup and task-based flush scheduling.
"""Benchmark emit_update's socket lookup, emit and flush tick.

The Socket.IO ``emit`` call itself is mocked to a recording no-op, so
only token-to-socket routing and the emit task scheduling are measured,
not any real socket write.
only token-to-socket routing and the loop tick yielded for the flush are
measured, not any real socket write. One update takes a few microseconds,
so each measured call emits ``EMIT_UPDATES`` of them.

Args:
benchmark: The CodSpeed benchmark fixture.
Expand Down Expand Up @@ -432,12 +434,13 @@ async def emit( # noqa: RUF029
)
update = StateUpdate(delta={"state": {"value": 1}})

async def emit_updates() -> None:
"""Emit the update ``EMIT_UPDATES`` times."""
for _ in range(EMIT_UPDATES):
await EventNamespace.emit_update(namespace, update, "token")

try:
benchmark(
lambda: loop.run_until_complete(
EventNamespace.emit_update(namespace, update, "token")
)
)
benchmark(lambda: loop.run_until_complete(emit_updates()))
assert emitted
finally:
loop.close()
6 changes: 4 additions & 2 deletions tests/benchmarks/test_event_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ def _():
loop.close()


@pytest.mark.parametrize("num_events", [1, 10, 100])
# A single event would repeat test_process_event_warm.
@pytest.mark.parametrize("num_events", [10, 100])
def test_process_event_burst_same_token(
num_events: int,
event_processing_harness,
Expand Down Expand Up @@ -208,7 +209,8 @@ def _():
loop.close()


@pytest.mark.parametrize("num_events", [1, 10, 100])
# A single event would repeat test_process_event_warm.
@pytest.mark.parametrize("num_events", [10, 100])
def test_process_event_burst_independent_tokens(
num_events: int,
event_processing_harness,
Expand Down
Loading
Loading