diff --git a/packages/reflex-base/news/6747.performance.md b/packages/reflex-base/news/6747.performance.md new file mode 100644 index 00000000000..f74ea9384d8 --- /dev/null +++ b/packages/reflex-base/news/6747.performance.md @@ -0,0 +1 @@ +`@var_operation` operands no longer pay the f-string tag round-trip (hash + permanent `_global_vars` registration + regex decode), cutting ~30-40% of var-operation construction cost and stopping internal operations from growing the never-evicting `_global_vars` dict. diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index f7212bef821..2f57c2e4d06 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -840,6 +840,13 @@ def __format__(self, format_spec: str) -> str: Returns: The formatted var. """ + # Operands of a running ``var_operation`` body interpolate as their + # raw JS expression: their VarData flows through the operation's + # ``_args``, so the tag round-trip (and its permanent ``_global_vars`` + # entry) is pure overhead there. See ``var_operation``. + if self.__dict__.get("_format_without_tagging"): + return self._js_expr + hashed_var = hash(self) _global_vars[hashed_var] = self @@ -1856,10 +1863,34 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Var[T]: for key, value in kwargs.items() } + operands = [*args_vars.values(), *kwargs_vars.values()] + # Suppress f-string tagging for the operands while the body runs: + # their VarData reaches the operation through ``_args`` below, so the + # tag round-trip (hash + permanent ``_global_vars`` entry + regex + # decode of the return expression) is pure overhead. The suppression + # is ref-counted so a nested operation on the same var cannot clear + # an outer suppression early; vars created inside the body still tag + # normally and keep contributing VarData via the return expression. + for operand in operands: + operand_dict = operand.__dict__ + operand_dict["_format_without_tagging"] = ( + operand_dict.get("_format_without_tagging", 0) + 1 + ) + try: + return_var = func(*args_vars.values(), **kwargs_vars) # pyright: ignore [reportCallIssue] + finally: + for operand in operands: + operand_dict = operand.__dict__ + remaining = operand_dict["_format_without_tagging"] - 1 + if remaining: + operand_dict["_format_without_tagging"] = remaining + else: + del operand_dict["_format_without_tagging"] + return CustomVarOperation.create( name=func.__name__, args=tuple(list(args_vars.items()) + list(kwargs_vars.items())), - return_var=func(*args_vars.values(), **kwargs_vars), # pyright: ignore [reportCallIssue, reportReturnType] + return_var=return_var, # pyright: ignore [reportArgumentType] ).guess_type() return wrapper diff --git a/tests/units/vars/test_base.py b/tests/units/vars/test_base.py index a8075357fa7..a39274ee283 100644 --- a/tests/units/vars/test_base.py +++ b/tests/units/vars/test_base.py @@ -1,7 +1,16 @@ from collections.abc import Mapping, Sequence import pytest -from reflex_base.vars.base import computed_var, figure_out_type +from reflex_base.utils.imports import ImportVar +from reflex_base.vars.base import ( + Var, + VarData, + _global_vars, + computed_var, + figure_out_type, + var_operation, + var_operation_return, +) from reflex.state import State @@ -42,6 +51,64 @@ def test_figure_out_type(value, expected): assert figure_out_type(value) == expected +def test_var_operation_does_not_register_global_vars() -> None: + """Internal var operations bypass the f-string tag round-trip. + + Regression: each operand interpolation hashed the var and permanently + registered it in the module-global ``_global_vars`` (a memory leak, + worst under hot-reload), then the return expression regex-decoded the + tag back out. Operand VarData already flows through + ``CustomVarOperation._args``. + """ + lhs = Var( + _js_expr="tag_bypass_lhs", + _var_data=VarData(imports={"op-lib": [ImportVar(tag="thing")]}), + ).to(int) + + before = len(_global_vars) + result = lhs + 1 + assert len(_global_vars) == before + + # The suppression flag does not persist on the operand after the op. + assert "_format_without_tagging" not in lhs.__dict__ + + # Operand VarData still reaches the merged operation VarData via _args. + var_data = result._get_all_var_data() + assert var_data is not None + assert dict(var_data.imports)["op-lib"] == (ImportVar(tag="thing"),) + assert str(result) == "(tag_bypass_lhs + 1)" + + # Formatting outside an operation still registers (and tags) as before. + formatted = f"{lhs}" + assert len(_global_vars) == before + 1 + assert formatted != str(lhs) + + +def test_var_operation_body_created_vars_keep_var_data() -> None: + """Vars created inside an operation body still contribute their VarData. + + Only the operands bypass tagging; a var constructed inside the body is + not carried by ``_args``, so it must keep flowing through the tagged + return expression. + """ + from reflex_base.vars.number import NumberVar + + @var_operation + def op_with_derived(value: NumberVar): + derived = Var( + _js_expr="derivedHelper", + _var_data=VarData(imports={"derived-lib": [ImportVar(tag="helper")]}), + ) + return var_operation_return(f"({value} + {derived})", var_type=int) + + result = op_with_derived(Var(_js_expr="a").to(int)) + + var_data = result._get_all_var_data() + assert var_data is not None + assert dict(var_data.imports)["derived-lib"] == (ImportVar(tag="helper"),) + assert str(result) == "(a + derivedHelper)" + + def test_computed_var_replace() -> None: class StateTest(State): @computed_var(cache=True)