Skip to content

Commit 4cea59b

Browse files
committed
feat: replace Key internals with DefaultHasher for allocation-free comparison (#3205)
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 1e0e1a8 commit 4cea59b

1 file changed

Lines changed: 148 additions & 30 deletions

File tree

  • packages/yew/src/virtual_dom

packages/yew/src/virtual_dom/key.rs

Lines changed: 148 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,109 @@
1-
//! This module contains the implementation yew's virtual nodes' keys.
2-
31
use std::fmt::{self, Display, Formatter};
4-
use std::ops::Deref;
2+
use std::hash::{Hash, Hasher};
3+
use std::num::NonZeroU64;
54
use std::rc::Rc;
65

76
use crate::html::ImplicitClone;
87

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

1738
impl Display for Key {
1839
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
19-
self.key.fmt(f)
40+
#[cfg(debug_assertions)]
41+
{
42+
self.original.fmt(f)
43+
}
44+
#[cfg(not(debug_assertions))]
45+
{
46+
write!(f, "#{}", self.hash)
47+
}
2048
}
2149
}
2250

23-
impl Deref for Key {
24-
type Target = str;
51+
impl fmt::Debug for Key {
52+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
53+
#[cfg(debug_assertions)]
54+
{
55+
write!(f, "Key({:?})", self.original)
56+
}
57+
#[cfg(not(debug_assertions))]
58+
{
59+
write!(f, "Key(#{})", self.hash)
60+
}
61+
}
62+
}
2563

26-
fn deref(&self) -> &str {
27-
self.key.as_ref()
64+
impl PartialEq for Key {
65+
fn eq(&self, other: &Self) -> bool {
66+
self.hash == other.hash
67+
}
68+
}
69+
70+
impl Eq for Key {}
71+
72+
impl PartialOrd for Key {
73+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
74+
Some(self.cmp(other))
75+
}
76+
}
77+
78+
impl Ord for Key {
79+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
80+
self.hash.cmp(&other.hash)
81+
}
82+
}
83+
84+
impl Hash for Key {
85+
fn hash<H: Hasher>(&self, state: &mut H) {
86+
self.hash.hash(state);
2887
}
2988
}
3089

3190
impl From<Rc<str>> for Key {
3291
fn from(key: Rc<str>) -> Self {
33-
Self { key }
92+
Self {
93+
hash: hash_value(&*key),
94+
#[cfg(debug_assertions)]
95+
original: key,
96+
}
3497
}
3598
}
3699

37100
impl From<&'_ str> for Key {
38101
fn from(key: &'_ str) -> Self {
39-
let key: Rc<str> = Rc::from(key);
40-
Self::from(key)
102+
Self {
103+
hash: hash_value(key),
104+
#[cfg(debug_assertions)]
105+
original: Rc::from(key),
106+
}
41107
}
42108
}
43109

@@ -47,33 +113,85 @@ impl From<String> for Key {
47113
}
48114
}
49115

50-
macro_rules! key_impl_from_to_string {
116+
macro_rules! key_impl_from_numeric {
51117
($type:ty) => {
52118
impl From<$type> for Key {
53119
fn from(key: $type) -> Self {
54-
Self::from(key.to_string().as_str())
120+
Self {
121+
hash: hash_value(&key),
122+
#[cfg(debug_assertions)]
123+
original: Rc::from(key.to_string().as_str()),
124+
}
55125
}
56126
}
57127
};
58128
}
59129

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

74192
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
75193
#[cfg(test)]
76-
mod test {
194+
mod wasm_tests {
77195
use std::rc::Rc;
78196

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

0 commit comments

Comments
 (0)