@@ -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+
674747def _deterministic_hash (value : object ) -> str :
675748 """Hash a rendered dictionary.
676749
0 commit comments