Skip to content
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

implement performance mode for existing state size check #4392

Merged
merged 2 commits into from
Nov 22, 2024
Merged
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
14 changes: 14 additions & 0 deletions reflex/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,14 @@ class PathExistsFlag:
ExistingPath = Annotated[Path, PathExistsFlag]


class PerformanceMode(enum.Enum):
"""Performance mode for the app."""

WARN = "warn"
RAISE = "raise"
OFF = "off"


class EnvironmentVariables:
"""Environment variables class to instantiate environment variables."""

Expand Down Expand Up @@ -550,6 +558,12 @@ class EnvironmentVariables:
# Whether to check for outdated package versions.
REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True)

# In which performance mode to run the app.
REFLEX_PERF_MODE: EnvVar[Optional[PerformanceMode]] = env_var(PerformanceMode.WARN)

# The maximum size of the reflex state in kilobytes.
REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000)
Copy link
Collaborator

@Lendemor Lendemor Nov 19, 2024

Choose a reason for hiding this comment

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

Should we set the default state size limit at the same level as the one on the WS ?

So if the State instance goes above this limit it can't be sent over the WS, would make sense to warn/raise there 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't even know about a ws size limit. I will take a look at this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Lendemor where do I find the ws limit?

Copy link
Collaborator

Choose a reason for hiding this comment

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

it's 1mb from socketio https://python-socketio.readthedocs.io/en/stable/api.html

max_http_buffer_size – The maximum size that is accepted for incoming messages. The default is 1,000,000 bytes. In spite of its name, the value set in this argument is enforced for HTTP long-polling and WebSocket connections.



environment = EnvironmentVariables()

Expand Down
28 changes: 19 additions & 9 deletions reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from typing_extensions import Self

from reflex import event
from reflex.config import get_config
from reflex.config import PerformanceMode, get_config
from reflex.istate.data import RouterData
from reflex.istate.storage import ClientStorageBase
from reflex.model import Model
Expand Down Expand Up @@ -90,6 +90,7 @@
ReflexRuntimeError,
SetUndefinedStateVarError,
StateSchemaMismatchError,
StateTooLargeError,
)
from reflex.utils.exec import is_testing_env
from reflex.utils.serializers import serializer
Expand All @@ -110,10 +111,11 @@
var = computed_var


# If the state is this large, it's considered a performance issue.
TOO_LARGE_SERIALIZED_STATE = 100 * 1024 # 100kb
# Only warn about each state class size once.
_WARNED_ABOUT_STATE_SIZE: Set[str] = set()
if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
# If the state is this large, it's considered a performance issue.
TOO_LARGE_SERIALIZED_STATE = environment.REFLEX_STATE_SIZE_LIMIT.get() * 1024
# Only warn about each state class size once.
_WARNED_ABOUT_STATE_SIZE: Set[str] = set()

# Errors caught during pickling of state
HANDLED_PICKLE_ERRORS = (
Expand Down Expand Up @@ -2097,25 +2099,32 @@ def __getstate__(self):
state["__dict__"].pop(inherited_var_name, None)
return state

def _warn_if_too_large(
def _check_state_size(
self,
pickle_state_size: int,
):
"""Print a warning when the state is too large.

Args:
pickle_state_size: The size of the pickled state.

Raises:
StateTooLargeError: If the state is too large.
"""
state_full_name = self.get_full_name()
if (
state_full_name not in _WARNED_ABOUT_STATE_SIZE
and pickle_state_size > TOO_LARGE_SERIALIZED_STATE
and self.substates
):
console.warn(
msg = (
f"State {state_full_name} serializes to {pickle_state_size} bytes "
"which may present performance issues. Consider reducing the size of this state."
+ "which may present performance issues. Consider reducing the size of this state."
)
if environment.REFLEX_PERF_MODE.get() == PerformanceMode.WARN:
console.warn(msg)
elif environment.REFLEX_PERF_MODE.get() == PerformanceMode.RAISE:
raise StateTooLargeError(msg)
_WARNED_ABOUT_STATE_SIZE.add(state_full_name)

@classmethod
Expand Down Expand Up @@ -2157,7 +2166,8 @@ def _serialize(self) -> bytes:
"""
try:
pickle_state = pickle.dumps((self._to_schema(), self))
self._warn_if_too_large(len(pickle_state))
if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
self._check_state_size(len(pickle_state))
return pickle_state
except HANDLED_PICKLE_ERRORS as og_pickle_error:
error = (
Expand Down
4 changes: 4 additions & 0 deletions reflex/utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ class InvalidPropValueError(ReflexError):
"""Raised when a prop value is invalid."""


class StateTooLargeError(ReflexError):
"""Raised when the state is too large to be serialized."""


class SystemPackageMissingError(ReflexError):
"""Raised when a system package is missing."""

Expand Down
Loading