-
Notifications
You must be signed in to change notification settings - Fork 1.8k
perf(events): share one chain per handler and trigger across call sites #7122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Speed up compilation by reading only the props a component sets, caching literal Var dispatch by value type, and trimming render and app-wrap bookkeeping. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Share one event chain per handler and trigger across call sites, and reuse memoized event wrappers by chain identity during compilation. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,6 @@ | |
| import logging | ||
| import re | ||
| import string | ||
| import uuid | ||
| import warnings | ||
| from abc import ABCMeta | ||
| from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence | ||
|
|
@@ -114,6 +113,37 @@ class VarSubclassEntry: | |
|
|
||
| _var_subclasses: list[VarSubclassEntry] = [] | ||
| _var_literal_subclasses: list[tuple[type[LiteralVar], VarSubclassEntry]] = [] | ||
| # Exact value type -> the literal class claiming it, or None when no literal | ||
| # class does. Reset whenever a literal subclass registers. | ||
| _literal_var_by_type: dict[type, type[LiteralVar] | None] = {} | ||
|
|
||
|
|
||
| def _literal_var_for(value: Any) -> type[LiteralVar] | None: | ||
| """Find the literal Var class claiming ``value``'s type. | ||
|
|
||
| Args: | ||
| value: The python value to wrap. | ||
|
|
||
| Returns: | ||
| The matching literal class, or None if no registered class claims it. | ||
| """ | ||
| value_type = type(value) | ||
| try: | ||
| return _literal_var_by_type[value_type] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When an ABC-backed Prompt for AI agents |
||
| except KeyError: | ||
| pass | ||
| literal_subclass = next( | ||
| ( | ||
| literal | ||
| for literal, var_subclass in reversed(_var_literal_subclasses) | ||
| if isinstance(value, var_subclass.python_types) | ||
| ), | ||
| None, | ||
| ) | ||
| # A class object's type is its metaclass, which other classes share. | ||
| if not isinstance(value, type): | ||
| _literal_var_by_type[value_type] = literal_subclass | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Each new concrete value type is retained permanently by this global cache, including types with no literal handler. Use weak type keys or another bounded/lifecycle-aware cache so dynamically generated classes do not accumulate during a long-lived compile process. Prompt for AI agents |
||
| return literal_subclass | ||
|
|
||
|
|
||
| @functools.cache | ||
|
|
@@ -235,7 +265,7 @@ def insert_app_wraps( | |
| if seen is None: | ||
| seen = target.get(key) | ||
| if seen is not None: | ||
| if seen != wrapper: | ||
| if seen is not wrapper and seen != wrapper: | ||
| msg = ( | ||
| f"Conflicting app wraps for {key!r}: two different " | ||
| "components claim the same (priority, tag) slot." | ||
|
|
@@ -1650,6 +1680,7 @@ def __init_subclass__(cls, **kwargs): | |
| _var_literal_subclasses.remove(var_literal_subclass) | ||
|
|
||
| _var_literal_subclasses.append((cls, var_subclass)) | ||
| _literal_var_by_type.clear() | ||
|
|
||
| @classmethod | ||
| def _create_literal_var( | ||
|
|
@@ -1677,9 +1708,8 @@ def _create_literal_var( | |
| return value | ||
| return value._replace(merge_var_data=_var_data) | ||
|
|
||
| for literal_subclass, var_subclass in _var_literal_subclasses[::-1]: | ||
| if isinstance(value, var_subclass.python_types): | ||
| return literal_subclass.create(value, _var_data=_var_data) | ||
| if (literal_subclass := _literal_var_for(value)) is not None: | ||
| return literal_subclass.create(value, _var_data=_var_data) | ||
|
|
||
| if ( | ||
| (as_var_method := getattr(value, "_as_var", None)) is not None | ||
|
|
@@ -1759,9 +1789,8 @@ def _get_all_var_data_without_creating_var_dispatch( | |
| if isinstance(value, Var): | ||
| return value._get_all_var_data() | ||
|
|
||
| for literal_subclass, var_subclass in _var_literal_subclasses[::-1]: | ||
| if isinstance(value, var_subclass.python_types): | ||
| return literal_subclass._get_all_var_data_without_creating_var(value) | ||
| if (literal_subclass := _literal_var_for(value)) is not None: | ||
| return literal_subclass._get_all_var_data_without_creating_var(value) | ||
|
|
||
| if ( | ||
| (as_var_method := getattr(value, "_as_var", None)) is not None | ||
|
|
@@ -2019,6 +2048,8 @@ def __set_name__(self, owner: Any, name: str): | |
| """ | ||
| if self._attrname is None: | ||
| self._attrname = name | ||
| self._cached_field_name = "_reflex_cache_" + name | ||
| cached_field_name = self._cached_field_name | ||
|
|
||
| original_del = getattr(owner, "__del__", None) | ||
|
|
||
|
|
@@ -2028,7 +2059,6 @@ def delete_property(this: Any): | |
| Args: | ||
| this: The object to delete the cached property from. | ||
| """ | ||
| cached_field_name = "_reflex_cache_" + name | ||
| try: | ||
| unique_id = object.__getattribute__(this, cached_field_name) | ||
| except AttributeError: | ||
|
|
@@ -2065,11 +2095,11 @@ def __get__(self, instance: Any, owner: type | None = None): | |
| if self._attrname is None: | ||
| msg = "Cannot use cached_property on a class without __set_name__." | ||
| raise TypeError(msg) | ||
| cached_field_name = "_reflex_cache_" + self._attrname | ||
| cached_field_name = self._cached_field_name | ||
| try: | ||
| unique_id = object.__getattribute__(instance, cached_field_name) | ||
| except AttributeError: | ||
| unique_id = uuid.uuid4().int | ||
| unique_id = object() | ||
| object.__setattr__(instance, cached_field_name, unique_id) | ||
| if unique_id not in GLOBAL_CACHE: | ||
| GLOBAL_CACHE[unique_id] = self._func(instance) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,9 @@ | |
| from reflex_base.constants.compiler import MemoizationDisposition | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Add a root Prompt for AI agents |
||
| from reflex_base.plugins import ComponentAndChildren, PageContext | ||
| from reflex_base.plugins.base import Plugin | ||
| from reflex_components_core.base.bare import Bare | ||
| from reflex_components_core.core.cond import Cond | ||
| from reflex_components_core.core.match import Match | ||
|
Comment on lines
+38
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This changes production code in the root Context Used: CLAUDE.md (source)
Comment on lines
+38
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This commit changes packaged source under AGENTS.md reference: AGENTS.md:L94-L100 Useful? React with 👍 / 👎. |
||
|
|
||
| from reflex.compiler.plugins.builtin import ( | ||
| collect_var_app_wraps_for_component, | ||
|
|
@@ -146,10 +149,6 @@ def _should_memoize(component: Component) -> bool: | |
| Returns: | ||
| True if the component should be wrapped in a memo definition. | ||
| """ | ||
| from reflex_components_core.base.bare import Bare | ||
| from reflex_components_core.core.cond import Cond | ||
| from reflex_components_core.core.match import Match | ||
|
|
||
| strategy = get_memoization_strategy(component) | ||
|
|
||
| if component._memoization_mode.disposition == MemoizationDisposition.NEVER: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: The new
_memoized_event_triggerscache grows unboundedly over the lifetime of the process-wide RegistrationContext and is never invalidated. Each compile/recompile memoizes chains under freshid(event)keys and holds strong references to the chain and its memo Var; nothing clears the dict (fork()omits it andclear_hash_caches()doesn't touch it), so dev-server reloads accumulate stale entries and retained VarData/memo vars across the app lifetime. Add an invalidation point (e.g. clear it in the same placeclear_hash_caches()runs on compile, or onfork()), or bound entries that no longer match the live chain.Prompt for AI agents