Skip to content

Commit befefe0

Browse files
authored
Merge pull request #1521 from fitzgen/anyref-heap-live-count
Utility for counting the number of live JsValues in the wasm-bindgen heap
2 parents 8ef820a + 3348ece commit befefe0

File tree

5 files changed

+168
-38
lines changed

5 files changed

+168
-38
lines changed

crates/cli-support/src/js/mod.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,37 @@ impl<'a> Context<'a> {
636636
))
637637
})?;
638638

639+
self.bind("__wbindgen_anyref_heap_live_count", &|me| {
640+
if me.config.anyref {
641+
// Eventually we should add support to the anyref-xform to
642+
// re-write calls to the imported
643+
// `__wbindgen_anyref_heap_live_count` function into calls to
644+
// the exported `__wbindgen_anyref_heap_live_count_impl`
645+
// function, and to un-export that function.
646+
//
647+
// But for now, we just bounce wasm -> js -> wasm because it is
648+
// easy.
649+
Ok("function() {{ return wasm.__wbindgen_anyref_heap_live_count_impl(); }}".into())
650+
} else {
651+
me.expose_global_heap();
652+
Ok(format!(
653+
"
654+
function() {{
655+
let free_count = 0;
656+
let next = heap_next;
657+
while (next < heap.length) {{
658+
free_count += 1;
659+
next = heap[next];
660+
}}
661+
return heap.length - free_count - {} - {};
662+
}}
663+
",
664+
INITIAL_HEAP_OFFSET,
665+
INITIAL_HEAP_VALUES.len(),
666+
))
667+
}
668+
})?;
669+
639670
self.bind("__wbindgen_debug_string", &|me| {
640671
me.expose_pass_string_to_wasm()?;
641672
me.expose_uint32_memory();

src/anyref.rs

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use std::slice;
2-
use std::vec::Vec;
3-
use std::ptr;
41
use std::alloc::{self, Layout};
52
use std::mem;
3+
use std::ptr;
4+
use std::slice;
5+
use std::vec::Vec;
66

77
use crate::JsValue;
88

@@ -34,9 +34,7 @@ impl Slab {
3434
if ret == self.data.len() {
3535
if self.data.len() == self.data.capacity() {
3636
let extra = 128;
37-
let r = unsafe {
38-
__wbindgen_anyref_table_grow(extra)
39-
};
37+
let r = unsafe { __wbindgen_anyref_table_grow(extra) };
4038
if r == -1 {
4139
internal_error("table grow failure")
4240
}
@@ -59,16 +57,8 @@ impl Slab {
5957
if ptr.is_null() {
6058
internal_error("allocation failure");
6159
}
62-
ptr::copy_nonoverlapping(
63-
self.data.as_ptr(),
64-
ptr,
65-
self.data.len(),
66-
);
67-
let new_vec = Vec::from_raw_parts(
68-
ptr,
69-
self.data.len(),
70-
new_cap,
71-
);
60+
ptr::copy_nonoverlapping(self.data.as_ptr(), ptr, self.data.len());
61+
let new_vec = Vec::from_raw_parts(ptr, self.data.len(), new_cap);
7262
let mut old = mem::replace(&mut self.data, new_vec);
7363
old.set_len(0);
7464
}
@@ -107,6 +97,20 @@ impl Slab {
10797
None => internal_error("slot out of bounds"),
10898
}
10999
}
100+
101+
fn live_count(&self) -> u32 {
102+
let mut free_count = 0;
103+
let mut next = self.head;
104+
while next < self.data.len() {
105+
debug_assert!((free_count as usize) < self.data.len());
106+
free_count += 1;
107+
match self.data.get(next) {
108+
Some(n) => next = *n,
109+
None => internal_error("slot out of bounds"),
110+
};
111+
}
112+
self.data.len() as u32 - free_count - super::JSIDX_RESERVED
113+
}
110114
}
111115

112116
fn internal_error(msg: &str) -> ! {
@@ -135,19 +139,19 @@ fn internal_error(msg: &str) -> ! {
135139
// implementation that will be replaced once #55518 lands on stable.
136140
#[cfg(target_feature = "atomics")]
137141
mod tl {
138-
use std::*; // hack to get `thread_local!` to work
139142
use super::Slab;
140143
use std::cell::Cell;
144+
use std::*; // hack to get `thread_local!` to work
141145

142146
thread_local!(pub static HEAP_SLAB: Cell<Slab> = Cell::new(Slab::new()));
143147
}
144148

145149
#[cfg(not(target_feature = "atomics"))]
146150
mod tl {
151+
use super::Slab;
147152
use std::alloc::{self, Layout};
148153
use std::cell::Cell;
149154
use std::ptr;
150-
use super::Slab;
151155

152156
pub struct HeapSlab;
153157
pub static HEAP_SLAB: HeapSlab = HeapSlab;
@@ -172,40 +176,57 @@ mod tl {
172176
}
173177

174178
#[no_mangle]
175-
pub extern fn __wbindgen_anyref_table_alloc() -> usize {
176-
tl::HEAP_SLAB.try_with(|slot| {
177-
let mut slab = slot.replace(Slab::new());
178-
let ret = slab.alloc();
179-
slot.replace(slab);
180-
ret
181-
}).unwrap_or_else(|_| internal_error("tls access failure"))
179+
pub extern "C" fn __wbindgen_anyref_table_alloc() -> usize {
180+
tl::HEAP_SLAB
181+
.try_with(|slot| {
182+
let mut slab = slot.replace(Slab::new());
183+
let ret = slab.alloc();
184+
slot.replace(slab);
185+
ret
186+
})
187+
.unwrap_or_else(|_| internal_error("tls access failure"))
182188
}
183189

184190
#[no_mangle]
185-
pub extern fn __wbindgen_anyref_table_dealloc(idx: usize) {
191+
pub extern "C" fn __wbindgen_anyref_table_dealloc(idx: usize) {
186192
if idx < super::JSIDX_RESERVED as usize {
187-
return
193+
return;
188194
}
189195
// clear this value from the table so while the table slot is un-allocated
190196
// we don't keep around a strong reference to a potentially large object
191197
unsafe {
192198
__wbindgen_anyref_table_set_null(idx);
193199
}
194-
tl::HEAP_SLAB.try_with(|slot| {
195-
let mut slab = slot.replace(Slab::new());
196-
slab.dealloc(idx);
197-
slot.replace(slab);
198-
}).unwrap_or_else(|_| internal_error("tls access failure"))
200+
tl::HEAP_SLAB
201+
.try_with(|slot| {
202+
let mut slab = slot.replace(Slab::new());
203+
slab.dealloc(idx);
204+
slot.replace(slab);
205+
})
206+
.unwrap_or_else(|_| internal_error("tls access failure"))
199207
}
200208

201209
#[no_mangle]
202-
pub unsafe extern fn __wbindgen_drop_anyref_slice(ptr: *mut JsValue, len: usize) {
210+
pub unsafe extern "C" fn __wbindgen_drop_anyref_slice(ptr: *mut JsValue, len: usize) {
203211
for slot in slice::from_raw_parts_mut(ptr, len) {
204212
__wbindgen_anyref_table_dealloc(slot.idx as usize);
205213
}
206214
}
207215

216+
// Implementation of `__wbindgen_anyref_heap_live_count` for when we are using
217+
// `anyref` instead of the JS `heap`.
218+
#[no_mangle]
219+
pub unsafe extern "C" fn __wbindgen_anyref_heap_live_count_impl() -> u32 {
220+
tl::HEAP_SLAB
221+
.try_with(|slot| {
222+
let slab = slot.replace(Slab::new());
223+
let count = slab.live_count();
224+
slot.replace(slab);
225+
count
226+
})
227+
.unwrap_or_else(|_| internal_error("tls access failure"))
228+
}
229+
208230
// see comment in module above this in `link_mem_intrinsics`
209231
#[inline(never)]
210-
pub fn link_intrinsics() {
211-
}
232+
pub fn link_intrinsics() {}

src/lib.rs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -489,19 +489,26 @@ externs! {
489489
extern "C" {
490490
fn __wbindgen_object_clone_ref(idx: u32) -> u32;
491491
fn __wbindgen_object_drop_ref(idx: u32) -> ();
492+
492493
fn __wbindgen_string_new(ptr: *const u8, len: usize) -> u32;
493494
fn __wbindgen_number_new(f: f64) -> u32;
494-
fn __wbindgen_number_get(idx: u32, invalid: *mut u8) -> f64;
495+
fn __wbindgen_symbol_new(ptr: *const u8, len: usize) -> u32;
496+
497+
fn __wbindgen_anyref_heap_live_count() -> u32;
498+
495499
fn __wbindgen_is_null(idx: u32) -> u32;
496500
fn __wbindgen_is_undefined(idx: u32) -> u32;
497-
fn __wbindgen_boolean_get(idx: u32) -> u32;
498-
fn __wbindgen_symbol_new(ptr: *const u8, len: usize) -> u32;
499501
fn __wbindgen_is_symbol(idx: u32) -> u32;
500502
fn __wbindgen_is_object(idx: u32) -> u32;
501503
fn __wbindgen_is_function(idx: u32) -> u32;
502504
fn __wbindgen_is_string(idx: u32) -> u32;
505+
506+
fn __wbindgen_number_get(idx: u32, invalid: *mut u8) -> f64;
507+
fn __wbindgen_boolean_get(idx: u32) -> u32;
503508
fn __wbindgen_string_get(idx: u32, len: *mut usize) -> *mut u8;
509+
504510
fn __wbindgen_debug_string(idx: u32, len: *mut usize) -> *mut u8;
511+
505512
fn __wbindgen_throw(a: *const u8, b: usize) -> !;
506513
fn __wbindgen_rethrow(a: u32) -> !;
507514

@@ -650,6 +657,56 @@ pub fn throw_val(s: JsValue) -> ! {
650657
}
651658
}
652659

660+
/// Get the count of live `anyref`s / `JsValue`s in `wasm-bindgen`'s heap.
661+
///
662+
/// ## Usage
663+
///
664+
/// This is intended for debugging and writing tests.
665+
///
666+
/// To write a test that asserts against unnecessarily keeping `anref`s /
667+
/// `JsValue`s alive:
668+
///
669+
/// * get an initial live count,
670+
///
671+
/// * perform some series of operations or function calls that should clean up
672+
/// after themselves, and should not keep holding onto `anyref`s / `JsValue`s
673+
/// after completion,
674+
///
675+
/// * get the final live count,
676+
///
677+
/// * and assert that the initial and final counts are the same.
678+
///
679+
/// ## What is Counted
680+
///
681+
/// Note that this only counts the *owned* `anyref`s / `JsValue`s that end up in
682+
/// `wasm-bindgen`'s heap. It does not count borrowed `anyref`s / `JsValue`s
683+
/// that are on its stack.
684+
///
685+
/// For example, these `JsValue`s are accounted for:
686+
///
687+
/// ```ignore
688+
/// #[wasm_bindgen]
689+
/// pub fn my_function(this_is_counted: JsValue) {
690+
/// let also_counted = JsValue::from_str("hi");
691+
/// assert!(wasm_bindgen::anyref_heap_live_count() >= 2);
692+
/// }
693+
/// ```
694+
///
695+
/// While this borrowed `JsValue` ends up on the stack, not the heap, and
696+
/// therefore is not accounted for:
697+
///
698+
/// ```ignore
699+
/// #[wasm_bindgen]
700+
/// pub fn my_other_function(this_is_not_counted: &JsValue) {
701+
/// // ...
702+
/// }
703+
/// ```
704+
pub fn anyref_heap_live_count() -> u32 {
705+
unsafe {
706+
__wbindgen_anyref_heap_live_count()
707+
}
708+
}
709+
653710
/// An extension trait for `Option<T>` and `Result<T, E>` for unwraping the `T`
654711
/// value, or throwing a JS error if it is not available.
655712
///
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use wasm_bindgen::prelude::*;
2+
use wasm_bindgen_test::*;
3+
4+
// This test is in the headless suite so that we can test the `anyref` table
5+
// implementation of `anyref_heap_live_count` (as opposed to the JS `heap`
6+
// implementation) in Firefox.
7+
#[wasm_bindgen_test]
8+
fn test_anyref_heap_live_count() {
9+
let initial = wasm_bindgen::anyref_heap_live_count();
10+
11+
let after_alloc = {
12+
let _vals: Vec<_> = (0..10).map(JsValue::from).collect();
13+
wasm_bindgen::anyref_heap_live_count()
14+
};
15+
16+
let after_dealloc = wasm_bindgen::anyref_heap_live_count();
17+
18+
assert_eq!(initial, after_dealloc);
19+
assert_eq!(initial + 10, after_alloc);
20+
}

tests/headless/main.rs

100644100755
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ pub fn import_export_same_name() {
4949

5050
pub mod snippets;
5151
pub mod modules;
52+
pub mod anyref_heap_live_count;

0 commit comments

Comments
 (0)