Skip to content

Commit 480d8ae

Browse files
improve compile perf
1 parent e54bc83 commit 480d8ae

1 file changed

Lines changed: 103 additions & 30 deletions

File tree

packages/reflex-base/src/reflex_base/components/component.py

Lines changed: 103 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -610,59 +610,116 @@ def _hash_str(value: str) -> str:
610610
return md5(f'"{value}"'.encode(), usedforsecurity=False).hexdigest()
611611

612612

613-
def _update_deterministic_hash(hasher: Any, value: object) -> None:
614-
"""Feed ``value`` into ``hasher`` using a self-delimiting, type-tagged encoding.
613+
@functools.cache
614+
def _deterministic_hash_dataclass_fields(cls: type) -> tuple[tuple[str, bytes], ...]:
615+
"""Per-class cache of dataclass field names and their encoded bytes.
616+
617+
``dataclasses.fields`` rebuilds its result tuple on every call; hashing a
618+
large app calls it millions of times (mostly for ``VarData``), so cache
619+
the derived (name, encoded name) pairs per class.
620+
621+
Args:
622+
cls: The dataclass type to introspect.
623+
624+
Returns:
625+
The (field name, encoded field name) pairs in definition order.
626+
"""
627+
return tuple((f.name, f.name.encode()) for f in dataclasses.fields(cls))
628+
629+
630+
def _encode_deterministic(buf: bytearray, value: object) -> None:
631+
"""Append ``value`` to ``buf`` using a self-delimiting, type-tagged encoding.
615632
616633
Each branch writes a distinct type tag plus length-prefixed payload, which
617634
keeps the encoding injective without building intermediate strings — the
618635
nested ``str([...])`` approach this replaces was the dominant cost of
619636
``_deterministic_hash`` (~4x speedup on synthetic, ~2x on real renders).
620637
638+
Exact-type checks front-run the isinstance ladder: auto-memoization hashes
639+
hundreds of millions of values per compile, nearly all of them plain
640+
``str``/``dict``/``list``/``tuple`` nodes from rendered component dicts,
641+
and the ladder's isinstance calls dominated the compile profile. Subclasses
642+
fall through to the ladder, which keeps the original branch order so the
643+
encoding is byte-identical to the pre-dispatch implementation.
644+
621645
Args:
622-
hasher: A ``hashlib`` hasher (must accept ``.update(bytes)``).
623-
value: The value to fold into the hasher.
646+
buf: The output buffer to append to.
647+
value: The value to fold into the buffer.
624648
625649
Raises:
626650
TypeError: If the value is not hashable.
627651
"""
652+
if type(value) is str:
653+
encoded = value.encode()
654+
buf += b"s"
655+
buf += len(encoded).to_bytes(8, "little")
656+
buf += encoded
657+
return
658+
if type(value) is dict:
659+
items = sorted(value.items(), key=operator.itemgetter(0))
660+
buf += b"d"
661+
buf += len(items).to_bytes(8, "little")
662+
for k, v in items:
663+
_encode_deterministic(buf, k)
664+
_encode_deterministic(buf, v)
665+
return
666+
if type(value) is list or type(value) is tuple:
667+
buf += b"l"
668+
buf += len(value).to_bytes(8, "little")
669+
for item in value:
670+
_encode_deterministic(buf, item)
671+
return
628672
if value is None:
629-
hasher.update(b"N")
630-
elif isinstance(value, bool):
631-
hasher.update(b"T" if value else b"F")
673+
buf += b"N"
674+
return
675+
if type(value) is bool:
676+
buf += b"T" if value else b"F"
677+
return
678+
if type(value) is int or type(value) is float:
679+
buf += b"n"
680+
buf += str(value).encode()
681+
return
682+
# Slow path for subclasses and structured types, in the original ladder
683+
# order so subclass encodings stay identical (e.g. ``IntEnum`` must hit
684+
# the numeric branch before the dataclass branch would see it).
685+
if isinstance(value, bool):
686+
buf += b"T" if value else b"F"
632687
elif isinstance(value, (int, float, enum.Enum)):
633-
hasher.update(b"n")
634-
hasher.update(str(value).encode())
688+
buf += b"n"
689+
buf += str(value).encode()
635690
elif isinstance(value, str):
636691
encoded = value.encode()
637-
hasher.update(b"s")
638-
hasher.update(len(encoded).to_bytes(8, "little"))
639-
hasher.update(encoded)
692+
buf += b"s"
693+
buf += len(encoded).to_bytes(8, "little")
694+
buf += encoded
640695
elif isinstance(value, dict):
641696
items = sorted(value.items(), key=operator.itemgetter(0))
642-
hasher.update(b"d")
643-
hasher.update(len(items).to_bytes(8, "little"))
697+
buf += b"d"
698+
buf += len(items).to_bytes(8, "little")
644699
for k, v in items:
645-
_update_deterministic_hash(hasher, k)
646-
_update_deterministic_hash(hasher, v)
700+
_encode_deterministic(buf, k)
701+
_encode_deterministic(buf, v)
647702
elif isinstance(value, (tuple, list)):
648-
hasher.update(b"l")
649-
hasher.update(len(value).to_bytes(8, "little"))
703+
buf += b"l"
704+
buf += len(value).to_bytes(8, "little")
650705
for item in value:
651-
_update_deterministic_hash(hasher, item)
706+
_encode_deterministic(buf, item)
652707
elif isinstance(value, Var):
653-
hasher.update(b"v")
654-
_update_deterministic_hash(hasher, value._js_expr)
655-
_update_deterministic_hash(hasher, value._get_all_var_data())
708+
buf += b"v"
709+
_encode_deterministic(buf, value._js_expr)
710+
_encode_deterministic(buf, value._get_all_var_data())
656711
elif dataclasses.is_dataclass(value):
657-
fields = dataclasses.fields(value)
658-
hasher.update(b"D")
659-
hasher.update(len(fields).to_bytes(8, "little"))
660-
for field in fields:
661-
hasher.update(field.name.encode())
662-
_update_deterministic_hash(hasher, getattr(value, field.name))
712+
fields = _deterministic_hash_dataclass_fields(
713+
value if isinstance(value, type) else type(value)
714+
)
715+
buf += b"D"
716+
buf += len(fields).to_bytes(8, "little")
717+
for field_name, encoded_field_name in fields:
718+
buf += encoded_field_name
719+
_encode_deterministic(buf, getattr(value, field_name))
663720
elif isinstance(value, BaseComponent):
664-
hasher.update(b"C")
665-
_update_deterministic_hash(hasher, value.render())
721+
buf += b"C"
722+
_encode_deterministic(buf, value.render())
666723
else:
667724
msg = (
668725
f"Cannot hash value `{value}` of type `{type(value).__name__}`. "
@@ -671,6 +728,22 @@ def _update_deterministic_hash(hasher: Any, value: object) -> None:
671728
raise TypeError(msg)
672729

673730

731+
def _update_deterministic_hash(hasher: Any, value: object) -> None:
732+
"""Feed ``value`` into ``hasher`` via :func:`_encode_deterministic`.
733+
734+
Buffering the whole encoding and updating the hasher once replaces the
735+
per-node ``hasher.update`` calls (hundreds of millions per compile) with
736+
cheap bytearray appends.
737+
738+
Args:
739+
hasher: A ``hashlib`` hasher (must accept ``.update(bytes)``).
740+
value: The value to fold into the hasher.
741+
"""
742+
buf = bytearray()
743+
_encode_deterministic(buf, value)
744+
hasher.update(buf)
745+
746+
674747
def _deterministic_hash(value: object) -> str:
675748
"""Hash a rendered dictionary.
676749

0 commit comments

Comments
 (0)