Skip to content

Commit ee925a1

Browse files
authored
feat: replace HashMap with HashTable for WeakMap (#49)
1 parent 67e941d commit ee925a1

File tree

4 files changed

+312
-42
lines changed

4 files changed

+312
-42
lines changed

oscars/src/collectors/mark_sweep/pointers/weak_map.rs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
use rustc_hash::FxHashMap;
1+
use hashbrown::HashTable;
2+
use rustc_hash::FxHasher;
23

34
use crate::{
45
alloc::mempool3::PoolPointer,
56
collectors::collector::Collector,
67
collectors::mark_sweep::{Finalize, TraceColor, internals::Ephemeron, trace::Trace},
78
};
8-
use core::ptr::NonNull;
9+
use core::{hash::Hasher, ptr::NonNull};
910

1011
use super::Gc;
1112

13+
#[inline]
14+
fn hash_addr(addr: usize) -> u64 {
15+
let mut h = FxHasher::default();
16+
h.write_usize(addr);
17+
h.finish()
18+
}
19+
1220
// type erased trait so the collector can prune any WeakMap without knowing K/V
1321
#[doc(hidden)]
1422
pub trait ErasedWeakMap {
@@ -17,23 +25,27 @@ pub trait ErasedWeakMap {
1725
}
1826

1927
// the actual weak map store, managed by the collector
20-
//
21-
// TODO: a HashTable might be a better approach here
2228
struct WeakMapInner<K: Trace + 'static, V: Trace + 'static> {
23-
entries: FxHashMap<usize, PoolPointer<'static, Ephemeron<K, V>>>,
29+
// keyed by the raw pointer address of the GC object; stored inline as
30+
// `(addr, ptr)` so HashTable needs no separate key allocation
31+
entries: HashTable<(usize, PoolPointer<'static, Ephemeron<K, V>>)>,
2432
is_alive: core::cell::Cell<bool>,
2533
}
2634

2735
impl<K: Trace, V: Trace> WeakMapInner<K, V> {
2836
fn new() -> Self {
2937
Self {
30-
entries: FxHashMap::default(),
38+
entries: HashTable::new(),
3139
is_alive: core::cell::Cell::new(true),
3240
}
3341
}
3442

3543
fn remove_and_invalidate(&mut self, key_addr: usize) {
36-
if let Some(old_ephemeron) = self.entries.remove(&key_addr) {
44+
if let Ok(entry) = self
45+
.entries
46+
.find_entry(hash_addr(key_addr), |e| e.0 == key_addr)
47+
{
48+
let ((_, old_ephemeron), _) = entry.remove();
3749
old_ephemeron.as_inner_ref().invalidate();
3850
}
3951
}
@@ -43,40 +55,48 @@ impl<K: Trace, V: Trace> WeakMapInner<K, V> {
4355
key_addr: usize,
4456
ephemeron_ptr: PoolPointer<'static, Ephemeron<K, V>>,
4557
) {
46-
self.entries.insert(key_addr, ephemeron_ptr);
58+
// caller guarantees no duplicate exists since remove_and_invalidate was called first
59+
self.entries
60+
.insert_unique(hash_addr(key_addr), (key_addr, ephemeron_ptr), |e| {
61+
hash_addr(e.0)
62+
});
4763
}
4864

4965
fn get(&self, key: &Gc<K>) -> Option<&V> {
5066
let key_addr = key.inner_ptr.as_non_null().as_ptr() as usize;
5167
self.entries
52-
.get(&key_addr)
53-
.map(|p| p.as_inner_ref().value())
68+
.find(hash_addr(key_addr), |e| e.0 == key_addr)
69+
.map(|(_, p)| p.as_inner_ref().value())
5470
}
5571

5672
fn is_key_alive(&self, key: &Gc<K>) -> bool {
5773
let key_addr = key.inner_ptr.as_non_null().as_ptr() as usize;
58-
self.entries.contains_key(&key_addr)
74+
self.entries
75+
.find(hash_addr(key_addr), |e| e.0 == key_addr)
76+
.is_some()
5977
}
6078

6179
fn remove(&mut self, key: &Gc<K>) -> bool {
6280
let key_addr = key.inner_ptr.as_non_null().as_ptr() as usize;
6381
// the backing ephemeron stays in the collector queue and gets swept
6482
// when the key is collected
65-
self.entries
66-
.remove(&key_addr)
67-
.map(|p| {
68-
p.as_inner_ref().invalidate();
69-
})
70-
.is_some()
83+
if let Ok(entry) = self
84+
.entries
85+
.find_entry(hash_addr(key_addr), |e| e.0 == key_addr)
86+
{
87+
let ((_, ptr), _) = entry.remove();
88+
ptr.as_inner_ref().invalidate();
89+
true
90+
} else {
91+
false
92+
}
7193
}
7294
}
7395

7496
impl<K: Trace, V: Trace> ErasedWeakMap for WeakMapInner<K, V> {
7597
fn prune_dead_entries(&mut self, color: TraceColor) {
76-
self.entries.retain(|_, ephemeron_ptr| {
77-
let ephemeron = ephemeron_ptr.as_inner_ref();
78-
ephemeron.is_reachable(color)
79-
});
98+
self.entries
99+
.retain(|(_, ephemeron_ptr)| ephemeron_ptr.as_inner_ref().is_reachable(color));
80100
}
81101

82102
fn is_alive(&self) -> bool {

oscars/src/collectors/mark_sweep/tests.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,118 @@ fn alive_wm() {
422422
);
423423
}
424424

425+
#[test]
426+
fn two_distinct_keys_wm() {
427+
// Ensure two distinct Gc keys produce unique addresses and don't collide
428+
// in the HashTable or corrupt each other's entries
429+
let collector = &mut MarkSweepGarbageCollector::default()
430+
.with_page_size(256)
431+
.with_heap_threshold(512);
432+
433+
let mut map = WeakMap::new(collector);
434+
let key1 = Gc::new_in(1u64, collector);
435+
let key2 = Gc::new_in(2u64, collector);
436+
437+
map.insert(&key1, 10u64, collector);
438+
map.insert(&key2, 20u64, collector);
439+
440+
// Verify both entries are independent and accessible
441+
assert_eq!(
442+
map.get(&key1),
443+
Some(&10u64),
444+
"key1 lookup returned wrong value"
445+
);
446+
assert_eq!(
447+
map.get(&key2),
448+
Some(&20u64),
449+
"key2 lookup returned wrong value"
450+
);
451+
assert!(map.is_key_alive(&key1), "key1 should be alive");
452+
assert!(map.is_key_alive(&key2), "key2 should be alive");
453+
454+
// Dropping one key must prune only its entry, leaving the other intact
455+
drop(key1);
456+
collector.collect();
457+
458+
assert_eq!(
459+
map.get(&key2),
460+
Some(&20u64),
461+
"key2 incorrectly pruned after key1 was collected",
462+
);
463+
assert!(
464+
map.is_key_alive(&key2),
465+
"key2 reported dead while still rooted",
466+
);
467+
468+
// Both keys dead and collected means no leaks
469+
drop(key2);
470+
collector.collect();
471+
472+
assert_eq!(
473+
collector.allocator.borrow().pools_len(),
474+
0,
475+
"ephemerons leaked after both keys collected",
476+
);
477+
}
478+
479+
#[test]
480+
fn two_maps_same_key_wm() {
481+
// Same Gc key registered in two independent WeakMaps, each must hold its
482+
// own value and neither map's operations must bleed into the other
483+
let collector = &mut MarkSweepGarbageCollector::default()
484+
.with_page_size(256)
485+
.with_heap_threshold(512);
486+
487+
let key = Gc::new_in(1u64, collector);
488+
let mut map1 = WeakMap::new(collector);
489+
let mut map2 = WeakMap::new(collector);
490+
491+
map1.insert(&key, 10u64, collector);
492+
map2.insert(&key, 20u64, collector);
493+
494+
assert_eq!(map1.get(&key), Some(&10u64));
495+
assert_eq!(map2.get(&key), Some(&20u64));
496+
497+
drop(key);
498+
collector.collect();
499+
500+
assert_eq!(
501+
collector.allocator.borrow().pools_len(),
502+
0,
503+
"ephemerons leaked after key collected"
504+
);
505+
}
506+
507+
#[test]
508+
fn drop_map_with_live_key_wm() {
509+
// Dropping a WeakMap while its key is still alive must not corrupt the
510+
// collector, it skips dead maps during prune_dead_entries
511+
let collector = &mut MarkSweepGarbageCollector::default()
512+
.with_page_size(256)
513+
.with_heap_threshold(512);
514+
515+
let key = Gc::new_in(42u64, collector);
516+
517+
{
518+
let mut map = WeakMap::new(collector);
519+
map.insert(&key, 100u64, collector);
520+
// Map dropped here, WeakMap::drop sets is_alive = false
521+
}
522+
523+
// Collector must handle the dead map entry without panic
524+
collector.collect();
525+
526+
// Key still live, drop it then collect to verify no leak
527+
drop(key);
528+
collector.collect();
529+
530+
assert_eq!(
531+
collector.allocator.borrow().pools_len(),
532+
0,
533+
"ephemerons leaked after map dropped with live key"
534+
);
535+
}
536+
425537
/// Edge-case stability tests for the mark-sweep garbage collector.
426538
///
427539
/// These tests exercise corner cases that could cause crashes, stack overflows,

oscars/src/collectors/mark_sweep_arena2/pointers/weak_map.rs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
use rustc_hash::FxHashMap;
1+
use hashbrown::HashTable;
2+
use rustc_hash::FxHasher;
23

34
use crate::{
45
alloc::arena2::ArenaPointer,
56
collectors::mark_sweep_arena2::{Finalize, TraceColor, internals::Ephemeron, trace::Trace},
67
};
7-
use core::ptr::NonNull;
8+
use core::{hash::Hasher, ptr::NonNull};
89

910
use super::Gc;
1011

12+
#[inline]
13+
fn hash_addr(addr: usize) -> u64 {
14+
let mut h = FxHasher::default();
15+
h.write_usize(addr);
16+
h.finish()
17+
}
18+
1119
// type erased trait so the collector can prune any WeakMap without knowing K/V
1220
#[doc(hidden)]
1321
pub trait ErasedWeakMap {
@@ -16,23 +24,27 @@ pub trait ErasedWeakMap {
1624
}
1725

1826
// the actual weak map store, managed by the collector
19-
//
20-
// TODO: a HashTable might be a better approach here
2127
struct WeakMapInner<K: Trace + 'static, V: Trace + 'static> {
22-
entries: FxHashMap<usize, ArenaPointer<'static, Ephemeron<K, V>>>,
28+
// keyed by the raw pointer address of the GC object, stored inline as
29+
// `(addr, ptr)` so HashTable needs no separate key allocation
30+
entries: HashTable<(usize, ArenaPointer<'static, Ephemeron<K, V>>)>,
2331
is_alive: core::cell::Cell<bool>,
2432
}
2533

2634
impl<K: Trace, V: Trace> WeakMapInner<K, V> {
2735
fn new() -> Self {
2836
Self {
29-
entries: FxHashMap::default(),
37+
entries: HashTable::new(),
3038
is_alive: core::cell::Cell::new(true),
3139
}
3240
}
3341

3442
fn remove_and_invalidate(&mut self, key_addr: usize) {
35-
if let Some(old_ephemeron) = self.entries.remove(&key_addr) {
43+
if let Ok(entry) = self
44+
.entries
45+
.find_entry(hash_addr(key_addr), |e| e.0 == key_addr)
46+
{
47+
let ((_, old_ephemeron), _) = entry.remove();
3648
old_ephemeron.as_inner_ref().invalidate();
3749
}
3850
}
@@ -42,40 +54,48 @@ impl<K: Trace, V: Trace> WeakMapInner<K, V> {
4254
key_addr: usize,
4355
ephemeron_ptr: ArenaPointer<'static, Ephemeron<K, V>>,
4456
) {
45-
self.entries.insert(key_addr, ephemeron_ptr);
57+
// caller guarantees no duplicate exists since remove_and_invalidate was called first
58+
self.entries
59+
.insert_unique(hash_addr(key_addr), (key_addr, ephemeron_ptr), |e| {
60+
hash_addr(e.0)
61+
});
4662
}
4763

4864
fn get(&self, key: &Gc<K>) -> Option<&V> {
4965
let key_addr = key.inner_ptr.as_non_null().as_ptr() as usize;
5066
self.entries
51-
.get(&key_addr)
52-
.map(|p| p.as_inner_ref().value())
67+
.find(hash_addr(key_addr), |e| e.0 == key_addr)
68+
.map(|(_, p)| p.as_inner_ref().value())
5369
}
5470

5571
fn is_key_alive(&self, key: &Gc<K>) -> bool {
5672
let key_addr = key.inner_ptr.as_non_null().as_ptr() as usize;
57-
self.entries.contains_key(&key_addr)
73+
self.entries
74+
.find(hash_addr(key_addr), |e| e.0 == key_addr)
75+
.is_some()
5876
}
5977

6078
fn remove(&mut self, key: &Gc<K>) -> bool {
6179
let key_addr = key.inner_ptr.as_non_null().as_ptr() as usize;
6280
// the backing ephemeron stays in the collector queue and gets swept
6381
// when the key is collected
64-
self.entries
65-
.remove(&key_addr)
66-
.map(|p| {
67-
p.as_inner_ref().invalidate();
68-
})
69-
.is_some()
82+
if let Ok(entry) = self
83+
.entries
84+
.find_entry(hash_addr(key_addr), |e| e.0 == key_addr)
85+
{
86+
let ((_, ptr), _) = entry.remove();
87+
ptr.as_inner_ref().invalidate();
88+
true
89+
} else {
90+
false
91+
}
7092
}
7193
}
7294

7395
impl<K: Trace, V: Trace> ErasedWeakMap for WeakMapInner<K, V> {
7496
fn prune_dead_entries(&mut self, color: TraceColor) {
75-
self.entries.retain(|_, ephemeron_ptr| {
76-
let ephemeron = ephemeron_ptr.as_inner_ref();
77-
ephemeron.is_reachable(color)
78-
});
97+
self.entries
98+
.retain(|(_, ephemeron_ptr)| ephemeron_ptr.as_inner_ref().is_reachable(color));
7999
}
80100

81101
fn is_alive(&self) -> bool {

0 commit comments

Comments
 (0)