Skip to content

Commit b165a73

Browse files
feat: replace Key internals with DefaultHasher for allocation-free comparison (#3205) (#4006)
BREAKING CHANGE: Key no longer implements Deref<Target=str>. BREAKING CHANGE: Keys from different types with the same string representation are no longer equal (e.g. Key::from("0") != Key::from(0u64)). BREAKING CHANGE: Display in release mode shows #<hash> instead of the original value.
1 parent 8a222d7 commit b165a73

1 file changed

Lines changed: 148 additions & 28 deletions

File tree

  • packages/yew/src/virtual_dom

packages/yew/src/virtual_dom/key.rs

Lines changed: 148 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,111 @@
11
//! This module contains the implementation yew's virtual nodes' keys.
22
33
use std::fmt::{self, Display, Formatter};
4-
use std::ops::Deref;
4+
use std::hash::{Hash, Hasher};
5+
use std::num::NonZeroU64;
56
use std::rc::Rc;
67

78
use crate::html::ImplicitClone;
89

10+
fn hash_value<H: Hash + ?Sized>(value: &H) -> NonZeroU64 {
11+
use std::hash::DefaultHasher;
12+
13+
let mut hasher = DefaultHasher::new();
14+
value.hash(&mut hasher);
15+
NonZeroU64::new(hasher.finish()).unwrap_or(NonZeroU64::MIN)
16+
}
17+
918
/// Represents the (optional) key of Yew's virtual nodes.
1019
///
11-
/// Keys are cheap to clone.
12-
#[derive(Clone, ImplicitClone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
20+
/// Keys are cheap to clone (a single `u64` copy) and to compare (a single
21+
/// integer comparison). Internally a key stores a hash of the value it was
22+
/// created from, so no heap allocation is required for numeric types in release
23+
/// builds.
24+
///
25+
/// In debug builds the original string representation is kept alongside the
26+
/// hash, enabling better diagnostics.
27+
///
28+
/// # Type-aware hashing
29+
///
30+
/// Keys created from different types are **not** equal even when their string
31+
/// representations coincide. For example `Key::from("1")` and `Key::from(1u64)`
32+
/// are distinct.
33+
#[derive(Clone, ImplicitClone)]
1334
pub struct Key {
14-
key: Rc<str>,
35+
hash: NonZeroU64,
36+
#[cfg(debug_assertions)]
37+
original: Rc<str>,
1538
}
1639

1740
impl Display for Key {
1841
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
19-
self.key.fmt(f)
42+
#[cfg(debug_assertions)]
43+
{
44+
self.original.fmt(f)
45+
}
46+
#[cfg(not(debug_assertions))]
47+
{
48+
write!(f, "#{}", self.hash)
49+
}
50+
}
51+
}
52+
53+
impl fmt::Debug for Key {
54+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
55+
#[cfg(debug_assertions)]
56+
{
57+
write!(f, "Key({:?})", self.original)
58+
}
59+
#[cfg(not(debug_assertions))]
60+
{
61+
write!(f, "Key(#{})", self.hash)
62+
}
63+
}
64+
}
65+
66+
impl PartialEq for Key {
67+
fn eq(&self, other: &Self) -> bool {
68+
self.hash == other.hash
2069
}
2170
}
2271

23-
impl Deref for Key {
24-
type Target = str;
72+
impl Eq for Key {}
73+
74+
impl PartialOrd for Key {
75+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
76+
Some(self.cmp(other))
77+
}
78+
}
2579

26-
fn deref(&self) -> &str {
27-
self.key.as_ref()
80+
impl Ord for Key {
81+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
82+
self.hash.cmp(&other.hash)
83+
}
84+
}
85+
86+
impl Hash for Key {
87+
fn hash<H: Hasher>(&self, state: &mut H) {
88+
self.hash.hash(state);
2889
}
2990
}
3091

3192
impl From<Rc<str>> for Key {
3293
fn from(key: Rc<str>) -> Self {
33-
Self { key }
94+
Self {
95+
hash: hash_value(&*key),
96+
#[cfg(debug_assertions)]
97+
original: key,
98+
}
3499
}
35100
}
36101

37102
impl From<&'_ str> for Key {
38103
fn from(key: &'_ str) -> Self {
39-
let key: Rc<str> = Rc::from(key);
40-
Self::from(key)
104+
Self {
105+
hash: hash_value(key),
106+
#[cfg(debug_assertions)]
107+
original: Rc::from(key),
108+
}
41109
}
42110
}
43111

@@ -47,33 +115,85 @@ impl From<String> for Key {
47115
}
48116
}
49117

50-
macro_rules! key_impl_from_to_string {
118+
macro_rules! key_impl_from_numeric {
51119
($type:ty) => {
52120
impl From<$type> for Key {
53121
fn from(key: $type) -> Self {
54-
Self::from(key.to_string().as_str())
122+
Self {
123+
hash: hash_value(&key),
124+
#[cfg(debug_assertions)]
125+
original: Rc::from(key.to_string().as_str()),
126+
}
55127
}
56128
}
57129
};
58130
}
59131

60-
key_impl_from_to_string!(char);
61-
key_impl_from_to_string!(u8);
62-
key_impl_from_to_string!(u16);
63-
key_impl_from_to_string!(u32);
64-
key_impl_from_to_string!(u64);
65-
key_impl_from_to_string!(u128);
66-
key_impl_from_to_string!(usize);
67-
key_impl_from_to_string!(i8);
68-
key_impl_from_to_string!(i16);
69-
key_impl_from_to_string!(i32);
70-
key_impl_from_to_string!(i64);
71-
key_impl_from_to_string!(i128);
72-
key_impl_from_to_string!(isize);
132+
key_impl_from_numeric!(char);
133+
key_impl_from_numeric!(u8);
134+
key_impl_from_numeric!(u16);
135+
key_impl_from_numeric!(u32);
136+
key_impl_from_numeric!(u64);
137+
key_impl_from_numeric!(u128);
138+
key_impl_from_numeric!(usize);
139+
key_impl_from_numeric!(i8);
140+
key_impl_from_numeric!(i16);
141+
key_impl_from_numeric!(i32);
142+
key_impl_from_numeric!(i64);
143+
key_impl_from_numeric!(i128);
144+
key_impl_from_numeric!(isize);
145+
146+
#[cfg(test)]
147+
mod tests {
148+
use super::*;
149+
150+
#[test]
151+
fn same_str_equal() {
152+
assert_eq!(Key::from("hello"), Key::from("hello"));
153+
}
154+
155+
#[test]
156+
fn different_str_not_equal() {
157+
assert_ne!(Key::from("hello"), Key::from("world"));
158+
}
159+
160+
#[test]
161+
fn same_integer_equal() {
162+
assert_eq!(Key::from(42u64), Key::from(42u64));
163+
}
164+
165+
#[test]
166+
fn different_integer_not_equal() {
167+
assert_ne!(Key::from(1u64), Key::from(2u64));
168+
}
169+
170+
#[test]
171+
fn str_and_integer_not_equal() {
172+
assert_ne!(Key::from("0"), Key::from(0u64));
173+
}
174+
175+
#[test]
176+
fn string_and_str_equal() {
177+
assert_eq!(Key::from("abc"), Key::from(String::from("abc")));
178+
}
179+
180+
#[test]
181+
fn rc_str_and_str_equal() {
182+
assert_eq!(Key::from("abc"), Key::from(Rc::<str>::from("abc")));
183+
}
184+
185+
#[test]
186+
fn option_key_niche_optimised() {
187+
assert_eq!(
188+
std::mem::size_of::<Option<Key>>(),
189+
std::mem::size_of::<Key>()
190+
);
191+
}
192+
}
73193

74194
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
75195
#[cfg(test)]
76-
mod test {
196+
mod wasm_tests {
77197
use std::rc::Rc;
78198

79199
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};

0 commit comments

Comments
 (0)