-
Notifications
You must be signed in to change notification settings - Fork 1.8k
perf(compile): read only set props and cache literal Var dispatch #7121
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |||||
| from reflex_base.components.dynamic import load_dynamic_serializer | ||||||
| from reflex_base.components.field import BaseField, FieldBasedMeta | ||||||
| from reflex_base.components.tags import Tag | ||||||
| from reflex_base.components.tags.tag import render_prop | ||||||
| from reflex_base.constants import Dirs, EventTriggers, Hooks, Imports, MemoizationMode | ||||||
| from reflex_base.constants.compiler import SpecialAttributes | ||||||
| from reflex_base.event import ( | ||||||
|
|
@@ -1161,7 +1162,7 @@ def _render(self, props: dict[str, Any] | None = None) -> Tag: | |||||
| if props is None: | ||||||
| # Add component props to the tag. | ||||||
| props = { | ||||||
| attr.removesuffix("_"): getattr(self, attr) for attr in self.get_props() | ||||||
| prop.removesuffix("_"): value for prop, value in self._iter_set_props() | ||||||
| } | ||||||
|
|
||||||
| # Add ref to element if `ref` is None and `id` is not None. | ||||||
|
|
@@ -1201,6 +1202,39 @@ def get_props(cls) -> Iterable[str]: | |||||
| """ | ||||||
| return cls.get_js_fields() | ||||||
|
|
||||||
| @classmethod | ||||||
| @functools.cache | ||||||
| def _get_defaulted_props(cls) -> frozenset[str]: | ||||||
| """Get the props whose field supplies a value when unset. | ||||||
|
|
||||||
| Returns: | ||||||
| The props with a default other than ``None`` or a default factory. | ||||||
| """ | ||||||
| return frozenset( | ||||||
| prop | ||||||
| for prop, field_ in cls.get_js_fields().items() | ||||||
| if field_.default_factory is not None | ||||||
| or (field_.default is not MISSING and field_.default is not None) | ||||||
| ) | ||||||
|
|
||||||
| def _iter_set_props(self) -> Iterator[tuple[str, Any]]: | ||||||
| """Walk the props that carry a value, in declaration order. | ||||||
|
|
||||||
| An unset prop resolves to ``None`` through its field descriptor and | ||||||
| every consumer drops ``None``, so only props present on the instance | ||||||
| or backed by a class default are read. | ||||||
|
|
||||||
| Yields: | ||||||
| Each prop name with its value. | ||||||
| """ | ||||||
| values = self.__dict__ | ||||||
| defaulted = self._get_defaulted_props() | ||||||
| for prop in self.get_props(): | ||||||
| if prop in values: | ||||||
| yield prop, values[prop] | ||||||
| elif prop in defaulted: | ||||||
| yield prop, getattr(self, prop) | ||||||
|
|
||||||
| @classmethod | ||||||
| @functools.cache | ||||||
| def get_initial_props(cls) -> set[str]: | ||||||
|
|
@@ -1215,9 +1249,8 @@ def get_initial_props(cls) -> set[str]: | |||||
| def _get_component_prop_property(self) -> Sequence[BaseComponent]: | ||||||
| return [ | ||||||
| component | ||||||
| for prop in self.get_props() | ||||||
| if (value := getattr(self, prop)) is not None | ||||||
| and isinstance(value, (BaseComponent, Var)) | ||||||
| for _, value in self._iter_set_props() | ||||||
| if isinstance(value, (BaseComponent, Var)) | ||||||
| for component in _components_from(value) | ||||||
| ] | ||||||
|
|
||||||
|
|
@@ -1438,11 +1471,15 @@ def render(self) -> dict: | |||||
| except AttributeError: | ||||||
| pass | ||||||
| tag = self._render() | ||||||
| rendered_dict = dict( | ||||||
| tag.set( | ||||||
| children=[child.render() for child in self.children], | ||||||
| ) | ||||||
| ) | ||||||
| children = [child.render() for child in self.children] | ||||||
| if type(tag) is Tag: | ||||||
| rendered_dict = {} | ||||||
| if (name := render_prop(tag.name)) is not None: | ||||||
| rendered_dict["name"] = name | ||||||
| rendered_dict["props"] = tag.format_props() | ||||||
| rendered_dict["children"] = children | ||||||
|
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 a child renderer returns a Prompt for AI agents
Suggested change
|
||||||
| else: | ||||||
| rendered_dict = dict(tag.set(children=children)) | ||||||
| self._replace_prop_names(rendered_dict) | ||||||
| self._cached_render_result = rendered_dict | ||||||
| return rendered_dict | ||||||
|
|
@@ -1581,8 +1618,7 @@ def _get_vars( | |||||
| vars.extend(event_vars) | ||||||
|
|
||||||
| # Get Vars associated with component props. | ||||||
| for prop in self.get_props(): | ||||||
| prop_var = getattr(self, prop) | ||||||
| for _, prop_var in self._iter_set_props(): | ||||||
| if isinstance(prop_var, Var): | ||||||
| vars.append(prop_var) | ||||||
|
|
||||||
|
|
||||||
| 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] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return literal_subclass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+131
to
+146
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 a literal-supported value has an unhashable metaclass, Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @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 | ||
| 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 source in the root Context Used: CLAUDE.md (source) Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
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 modifies the main AGENTS.md reference: AGENTS.md:L96-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.
When a custom
Componentoverrides an inherited JavaScript field with a property or data descriptor whose setter stores the raw value under the same name in__dict__, this branch returns that raw value and bypasses the descriptor getter. The previous unconditionalgetattr(self, prop)returned the getter's transformed value, so rendering, Var collection, and component-in-prop discovery can now all use the wrong value; retain descriptor-aware access for such class attributes while keeping the direct-dict fast path for ordinary fields.AGENTS.md reference: AGENTS.md:L43-L43
Useful? React with 👍 / 👎.