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 packages/reflex-base/news/6798.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`@var_operation` no longer registers its operands in the never-evicting `_global_vars` dict (a memory leak, worst under dev hot-reload), and skipping the f-string tag round-trip makes var-operation construction ~25% faster for plain vars and ~2.6x faster for state-var operands.
33 changes: 32 additions & 1 deletion packages/reflex-base/src/reflex_base/vars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,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"):

@cubic-dev-ai cubic-dev-ai Bot Jul 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Supported NumberVar format specs inside a var_operation still grow _global_vars: NumberVar.__format__ returns early with a formatted nested operation before this suppression check is reached. Applying the suppression-aware raw return in the number-format branches would avoid retaining one intermediate Var per operation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-base/src/reflex_base/vars/base.py, line 937:

<comment>Supported `NumberVar` format specs inside a `var_operation` still grow `_global_vars`: `NumberVar.__format__` returns early with a formatted nested operation before this suppression check is reached. Applying the suppression-aware raw return in the number-format branches would avoid retaining one intermediate Var per operation.</comment>

<file context>
@@ -930,6 +930,13 @@ def __format__(self, format_spec: str) -> str:
+        # 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 str(self)
+
</file context>
Fix with cubic

@cubic-dev-ai cubic-dev-ai Bot Jul 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Operand f-strings passed through LiteralVar.create inside a custom operation now become literal text instead of expressions containing the operand, because this removes the marker consumed by LiteralStringVar.create. Consider limiting the raw interpolation path to construction of CustomVarOperationReturn.js_expression or preserving a non-global marker that the literal parser can still decode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-base/src/reflex_base/vars/base.py, line 937:

<comment>Operand f-strings passed through `LiteralVar.create` inside a custom operation now become literal text instead of expressions containing the operand, because this removes the marker consumed by `LiteralStringVar.create`. Consider limiting the raw interpolation path to construction of `CustomVarOperationReturn.js_expression` or preserving a non-global marker that the literal parser can still decode.</comment>

<file context>
@@ -930,6 +930,13 @@ def __format__(self, format_spec: str) -> str:
+        # 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 str(self)
+
</file context>
Fix with cubic

return str(self)
Comment on lines +937 to +938

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep format suppression local to the operation context

When the same Var is shared across threads, this instance-dict flag changes __format__ process-wide while an operation body is running. A concurrent f"{var}" therefore returns the raw expression without its tag, so a Var constructed from that string loses the original imports/hooks; overlapping operations can also race the non-atomic reference-count updates and leave the flag set or raise during cleanup. Use thread/task-local suppression rather than mutating the shared immutable operand.

Useful? React with 👍 / 👎.


hashed_var = hash(self)

_global_vars[hashed_var] = self
Expand Down Expand Up @@ -1941,10 +1948,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"] = ( # pyright: ignore[reportIndexIssue]

@cubic-dev-ai cubic-dev-ai Bot Jul 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Formatting a shared Var in another thread can silently lose its tag while any var_operation using that Var is running, and concurrent refcount updates can leave stale suppression state. A context-local suppression set (for example, ContextVar with token reset) would isolate nested operations without mutating otherwise immutable shared Vars.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-base/src/reflex_base/vars/base.py, line 1961:

<comment>Formatting a shared Var in another thread can silently lose its tag while any `var_operation` using that Var is running, and concurrent refcount updates can leave stale suppression state. A context-local suppression set (for example, `ContextVar` with token reset) would isolate nested operations without mutating otherwise immutable shared Vars.</comment>

<file context>
@@ -1941,10 +1948,34 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Var[T]:
+        # normally and keep contributing VarData via the return expression.
+        for operand in operands:
+            operand_dict = operand.__dict__
+            operand_dict["_format_without_tagging"] = (  # pyright: ignore[reportIndexIssue]
+                operand_dict.get("_format_without_tagging", 0) + 1
+            )
</file context>
Fix with cubic

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 # pyright: ignore[reportIndexIssue]
else:
del operand_dict["_format_without_tagging"] # pyright: ignore[reportIndexIssue]
Comment on lines +1959 to +1973

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Increment loop outside try-finally can permanently poison operands

The increment loop runs before the try block, so if it raises mid-iteration (e.g., an operand whose __dict__ is inaccessible), any operands already incremented will have _format_without_tagging stuck at a non-zero value permanently — causing them to silently bypass tagging in all future uses. This can happen today when LiteralVar.create(range_value) returns None (see LiteralVar.create line ~1802), placing None in operands; the first valid-var operand gets incremented, then None.__dict__ raises before the try, so the finally never runs. Moving the increment loop inside try (and using .get(..., 0) in the decrement to tolerate partial increments) would guarantee cleanup regardless of which step fails.


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
Expand Down
69 changes: 68 additions & 1 deletion tests/units/vars/test_base.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -66,6 +75,64 @@ class FancyTestStrVar(Var, python_types=FancyTestStr):
)


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)
Expand Down
Loading