diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index 9df7d0a..3117967 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -23,7 +23,9 @@ pub mod bind_typed_array; use std::sync::Arc; use oxide_kernel::builtin::BuiltinWorld; -use oxide_kernel::kernel::{KernelCore, KernelSession}; +use oxide_kernel::kernel::{BuiltinDirtySet, KernelCore, KernelSession}; +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::mem::P; use oxide_types::object::{JsObject, NativeFnPtr}; use oxide_types::value::JsValue; @@ -64,6 +66,12 @@ pub(crate) fn configure_native_constructor(ctor: &mut JsObject, native_fn: *cons ctor.set_native_arg_count(arg_count); } +fn configure_existing_ctor(ctor: &P, native_fn: *const (), arg_count: u8) { + let ctor_ptr = ctor.as_ptr() as *mut JsObject; + let ctor = unsafe { &mut *ctor_ptr }; + configure_native_constructor(ctor, native_fn, arg_count); +} + pub(crate) fn apply_binding_table( world: &BuiltinWorld, target: &mut JsObject, core: &Arc, bindings: &[(&'static str, *const (), u8)], ) { @@ -84,6 +92,357 @@ pub(crate) fn bind_global_value(core: &Arc, global: &mut JsObject, n global.bump_generation(); } +fn bind_existing_global(core: &Arc, global: &mut JsObject, name: &str, value: JsValue) { + bind_global_value(core, global, name, value); +} + +fn bind_error_subtype_global( + core: &Arc, session: &KernelSession, global: &mut JsObject, name: &str, proto: &P, + ctor_fn: *const (), +) { + let constructor_si = core.string_forge().intern("constructor").0; + if let Some(pos) = core.shape_forge().lookup_position(proto.shape_id(), constructor_si) { + let existing_ctor = proto.get_prop_at(pos); + if existing_ctor.is_object() { + bind_existing_global(core, global, name, existing_ctor); + return; + } + } + + let function_proto_ptr = session.builtin_world().function_proto.as_ptr() as *mut JsObject; + let mut ctor = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(function_proto_ptr))); + ctor.set_function(true); + configure_native_constructor(&mut ctor, ctor_fn, 1); + + let sf = core.string_forge().as_ref(); + let sh = core.shape_forge().as_ref(); + let si_prototype = sf.intern("prototype").0; + let si_name = sf.intern("name").0; + let name_si = sf.intern(name).0; + let ctor_shape1 = sh.make_shape(EMPTY_SHAPE_ID, si_prototype); + let ctor_shape2 = sh.make_shape(ctor_shape1, si_name); + ctor.set_shape_id(ctor_shape2); + ctor.ensure_hash_props() + .push(JsValue::from_js_object(proto.as_ptr() as *mut JsObject)); + ctor.ensure_hash_props().push(JsValue::string(name_si, 0)); + + let ctor_ptr = Box::into_raw(ctor); + bind_existing_global(core, global, name, JsValue::from_js_object(ctor_ptr)); +} + +fn bind_global_functions(core: &Arc, session: &KernelSession, global: &mut JsObject) { + apply_binding_table( + session.builtin_world(), + global, + core, + &[ + ("parseInt", crate::builtins::number::number_parse_int as *const (), 1), + ("parseFloat", crate::builtins::number::number_parse_float as *const (), 1), + ("escape", crate::builtins::global::js_escape as *const (), 1), + ("unescape", crate::builtins::global::js_unescape as *const (), 1), + ("encodeURI", crate::builtins::global::encode_uri as *const (), 1), + ("decodeURI", crate::builtins::global::decode_uri as *const (), 1), + ("encodeURIComponent", crate::builtins::global::encode_uri_component as *const (), 1), + ("decodeURIComponent", crate::builtins::global::decode_uri_component as *const (), 1), + ], + ); +} + +fn bind_reflect_global(core: &Arc, session: &KernelSession, global: &mut JsObject) { + let mut reflect = Box::new(JsObject::new_empty( + EMPTY_SHAPE_ID, + JsValue::from_js_object(session.builtin_world().object_proto.as_ptr() as *mut JsObject), + )); + apply_binding_table( + session.builtin_world(), + &mut reflect, + core, + &[ + ("apply", crate::builtins::reflect::reflect_apply as *const (), 3), + ("construct", crate::builtins::reflect::reflect_construct as *const (), 2), + ("defineProperty", crate::builtins::reflect::reflect_define_property as *const (), 3), + ("deleteProperty", crate::builtins::reflect::reflect_delete_property as *const (), 2), + ("get", crate::builtins::reflect::reflect_get as *const (), 2), + ( + "getOwnPropertyDescriptor", + crate::builtins::reflect::reflect_get_own_property_descriptor as *const (), + 2, + ), + ("getPrototypeOf", crate::builtins::reflect::reflect_get_prototype_of as *const (), 1), + ("has", crate::builtins::reflect::reflect_has as *const (), 2), + ("isExtensible", crate::builtins::reflect::reflect_is_extensible as *const (), 1), + ("ownKeys", crate::builtins::reflect::reflect_own_keys as *const (), 1), + ("preventExtensions", crate::builtins::reflect::reflect_prevent_extensions as *const (), 1), + ("set", crate::builtins::reflect::reflect_set as *const (), 3), + ("setPrototypeOf", crate::builtins::reflect::reflect_set_prototype_of as *const (), 2), + ], + ); + bind_existing_global(core, global, "Reflect", JsValue::from_js_object(Box::into_raw(reflect))); +} + +fn bind_iterator_global(core: &Arc, session: &KernelSession, global: &mut JsObject) { + let function_proto = session.builtin_world().function_proto.as_ptr() as *mut JsObject; + let mut iterator = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(function_proto))); + iterator.set_function(true); + configure_native_constructor(&mut iterator, crate::builtins::iterator::iterator_constructor as *const (), 0); + apply_binding_table( + session.builtin_world(), + &mut iterator, + core, + &[("from", crate::builtins::iterator::iterator_from as *const (), 1)], + ); + bind_existing_global(core, global, "Iterator", JsValue::from_js_object(Box::into_raw(iterator))); +} + +fn bind_stub_globals(core: &Arc, session: &KernelSession, global: &mut JsObject) { + for (index, (name, native_fn, arg_count)) in [ + ("Proxy", crate::builtins::stubs::proxy_stub as *const (), 2), + ("BigInt", crate::builtins::stubs::bigint_stub as *const (), 1), + ("WeakMap", crate::builtins::stubs::weakmap_stub as *const (), 0), + ("WeakSet", crate::builtins::stubs::weakset_stub as *const (), 0), + ("WeakRef", crate::builtins::stubs::weakref_stub as *const (), 1), + ("FinalizationRegistry", crate::builtins::stubs::finalization_registry_stub as *const (), 1), + ("SharedArrayBuffer", crate::builtins::stubs::shared_array_buffer_stub as *const (), 1), + ("Atomics", crate::builtins::stubs::atomics_stub as *const (), 0), + ] + .into_iter() + .enumerate() + { + if let Some(stub) = session.builtin_world().stub_objects.get(index) { + configure_existing_ctor(stub, native_fn, arg_count); + bind_existing_global(core, global, name, JsValue::from_js_object(stub.as_ptr() as *mut JsObject)); + } + } +} + +pub fn bind_global_builtin_slots(core: &Arc, session: &KernelSession, global: &mut JsObject) { + let world = session.builtin_world(); + + configure_existing_ctor(&world.object_constructor, crate::builtins::object::object_constructor as *const (), 1); + configure_existing_ctor(&world.array_constructor, crate::builtins::array::array_constructor as *const (), 1); + configure_existing_ctor( + &world.array_buffer_constructor, + crate::builtins::array_buffer::array_buffer_constructor as *const (), + 1, + ); + configure_existing_ctor( + &world.data_view_constructor, + crate::builtins::data_view::data_view_constructor as *const (), + 3, + ); + configure_existing_ctor(&world.error_constructor, crate::builtins::error::error_constructor as *const (), 1); + configure_existing_ctor(&world.number_constructor, crate::builtins::number::number_constructor as *const (), 1); + configure_existing_ctor(&world.date_constructor, crate::builtins::date::date_constructor as *const (), 7); + configure_existing_ctor(&world.set_constructor, crate::builtins::set::set_constructor as *const (), 1); + configure_existing_ctor(&world.map_constructor, crate::builtins::map::map_constructor as *const (), 1); + configure_existing_ctor(&world.boolean_constructor, crate::builtins::boolean::boolean_constructor as *const (), 1); + configure_existing_ctor(&world.regexp_constructor, crate::builtins::regexp::regexp_constructor as *const (), 2); + configure_existing_ctor(&world.symbol_constructor, crate::builtins::symbol::symbol_constructor as *const (), 1); + + for (name, value) in [ + ("Object", JsValue::from_js_object(world.object_constructor.as_ptr() as *mut JsObject)), + ("Array", JsValue::from_js_object(world.array_constructor.as_ptr() as *mut JsObject)), + ( + "ArrayBuffer", + JsValue::from_js_object(world.array_buffer_constructor.as_ptr() as *mut JsObject), + ), + ("DataView", JsValue::from_js_object(world.data_view_constructor.as_ptr() as *mut JsObject)), + ("Error", JsValue::from_js_object(world.error_constructor.as_ptr() as *mut JsObject)), + ("String", JsValue::from_js_object(world.string_constructor.as_ptr() as *mut JsObject)), + ("Number", JsValue::from_js_object(world.number_constructor.as_ptr() as *mut JsObject)), + ("Date", JsValue::from_js_object(world.date_constructor.as_ptr() as *mut JsObject)), + ("Set", JsValue::from_js_object(world.set_constructor.as_ptr() as *mut JsObject)), + ("Map", JsValue::from_js_object(world.map_constructor.as_ptr() as *mut JsObject)), + ("Boolean", JsValue::from_js_object(world.boolean_constructor.as_ptr() as *mut JsObject)), + ("Function", JsValue::from_js_object(world.function_constructor.as_ptr() as *mut JsObject)), + ("RegExp", JsValue::from_js_object(world.regexp_constructor.as_ptr() as *mut JsObject)), + ("Symbol", JsValue::from_js_object(world.symbol_constructor.as_ptr() as *mut JsObject)), + ("Math", JsValue::from_js_object(world.math_object.as_ptr() as *mut JsObject)), + ("JSON", JsValue::from_js_object(world.json_object.as_ptr() as *mut JsObject)), + ] { + bind_existing_global(core, global, name, value); + } + + for (name, ctor, native_fn) in [ + ( + "Int8Array", + &world.int8array_constructor, + crate::builtins::typed_array::int8array_constructor as *const (), + ), + ( + "Uint8Array", + &world.uint8array_constructor, + crate::builtins::typed_array::uint8array_constructor as *const (), + ), + ( + "Uint8ClampedArray", + &world.uint8clampedarray_constructor, + crate::builtins::typed_array::uint8clampedarray_constructor as *const (), + ), + ( + "Int16Array", + &world.int16array_constructor, + crate::builtins::typed_array::int16array_constructor as *const (), + ), + ( + "Uint16Array", + &world.uint16array_constructor, + crate::builtins::typed_array::uint16array_constructor as *const (), + ), + ( + "Int32Array", + &world.int32array_constructor, + crate::builtins::typed_array::int32array_constructor as *const (), + ), + ( + "Uint32Array", + &world.uint32array_constructor, + crate::builtins::typed_array::uint32array_constructor as *const (), + ), + ( + "Float32Array", + &world.float32array_constructor, + crate::builtins::typed_array::float32array_constructor as *const (), + ), + ( + "Float64Array", + &world.float64array_constructor, + crate::builtins::typed_array::float64array_constructor as *const (), + ), + ( + "BigInt64Array", + &world.bigint64array_constructor, + crate::builtins::typed_array::bigint64array_constructor as *const (), + ), + ( + "BigUint64Array", + &world.biguint64array_constructor, + crate::builtins::typed_array::biguint64array_constructor as *const (), + ), + ] { + configure_existing_ctor(ctor, native_fn, 1); + bind_existing_global(core, global, name, JsValue::from_js_object(ctor.as_ptr() as *mut JsObject)); + } + + bind_error_subtype_global( + core, + session, + global, + "TypeError", + &world.type_error_proto, + crate::builtins::error::type_error_constructor as *const (), + ); + bind_error_subtype_global( + core, + session, + global, + "ReferenceError", + &world.reference_error_proto, + crate::builtins::error::reference_error_constructor as *const (), + ); + bind_error_subtype_global( + core, + session, + global, + "RangeError", + &world.range_error_proto, + crate::builtins::error::range_error_constructor as *const (), + ); + bind_error_subtype_global( + core, + session, + global, + "SyntaxError", + &world.syntax_error_proto, + crate::builtins::error::syntax_error_constructor as *const (), + ); + bind_error_subtype_global( + core, + session, + global, + "URIError", + &world.uri_error_proto, + crate::builtins::error::uri_error_constructor as *const (), + ); + bind_error_subtype_global( + core, + session, + global, + "EvalError", + &world.eval_error_proto, + crate::builtins::error::eval_error_constructor as *const (), + ); + + bind_reflect_global(core, session, global); + bind_iterator_global(core, session, global); + bind_stub_globals(core, session, global); + bind_global_functions(core, session, global); + let global_this = JsValue::from_js_object(global as *mut JsObject); + bind_existing_global(core, global, "globalThis", global_this); +} + +/// Maintenance: when adding a `BuiltinDirtySet` group, update this rebind map, +/// `BuiltinSnapshot`, and `BuiltinWorld::rebuild_with_dirty()` together. +pub fn rebind_dirty_builtins(core: &Arc, session: &mut KernelSession, dirty: &BuiltinDirtySet) { + let global_ptr = session.global_object().as_ptr() as *mut JsObject; + let global = unsafe { &mut *global_ptr }; + + if dirty.object { + bind_object::bind_object(core, session, global); + } + if dirty.array { + bind_array::bind_array(core, session, global); + } + if dirty.array_buffer { + bind_array_buffer::bind_array_buffer(core, session, global); + } + if dirty.data_view { + bind_data_view::bind_data_view(core, session, global); + } + if dirty.typed_array_family { + bind_typed_array::bind_typed_array(core, session, global); + } + if dirty.error_family { + bind_error::bind_error(core, session, global); + } + if dirty.string { + bind_string::bind_string(core, session, global); + } + if dirty.number { + bind_number::bind_number(core, session, global); + } + if dirty.math { + bind_math::bind_math(core, session, global); + } + if dirty.json { + bind_json::bind_json(core, session, global); + } + if dirty.date { + bind_date::bind_date(core, session, global); + } + if dirty.set { + bind_set::bind_set(core, session, global); + } + if dirty.map { + bind_map::bind_map(core, session, global); + } + if dirty.boolean { + bind_boolean::bind_boolean(core, session, global); + } + if dirty.function { + bind_function::bind_function(core, session, global); + } + if dirty.regexp { + bind_regexp::bind_regexp(core, session, global); + } + if dirty.symbol_family { + bind_symbol::bind_symbol(core, session, global); + } + if dirty.stubs { + bind_stubs::bind_stubs(core, session, global); + } +} + pub fn init_kernel_builtins(core: &Arc, session: &mut KernelSession) { let global_ptr = session.global_object().as_ptr() as *mut oxide_types::object::JsObject; let global = unsafe { &mut *global_ptr }; @@ -111,4 +470,5 @@ pub fn init_kernel_builtins(core: &Arc, session: &mut KernelSession) bind_stubs::bind_stubs(core, session, global); bind_global_value(core, global, "globalThis", JsValue::from_js_object(global_ptr)); + session.record_snapshot(); } diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 3549538..e31efc5 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -90,10 +90,29 @@ impl Vm { } pub fn full_reset(&mut self) { - let mut new_session = KernelSession::new(&self.kernel_core); - bindings::init_kernel_builtins(&self.kernel_core, &mut new_session); - self.object_prototype = P::clone(&new_session.builtin_world().object_proto); - self.session = new_session; + let dirty = self.session.selective_reset(&self.kernel_core); + if dirty.any_builtin_dirty() { + bindings::rebind_dirty_builtins(&self.kernel_core, &mut self.session, &dirty); + } + if dirty.global { + let global_ptr = self.session.global_object().as_ptr() as *mut JsObject; + let global = unsafe { &mut *global_ptr }; + bindings::bind_global_builtin_slots(&self.kernel_core, &self.session, global); + } + self.session.record_snapshot(); + self.object_prototype = P::clone(&self.session.builtin_world().object_proto); + self.clear_full_reset_state(); + } + + #[doc(hidden)] + pub fn full_reset_legacy_for_bench(&mut self) { + self.session = KernelSession::new(&self.kernel_core); + bindings::init_kernel_builtins(&self.kernel_core, &mut self.session); + self.object_prototype = P::clone(&self.session.builtin_world().object_proto); + self.clear_full_reset_state(); + } + + fn clear_full_reset_state(&mut self) { self.clear_execution_state(); self.bytecode.clear(); self.constants.clear(); @@ -278,3 +297,126 @@ impl Vm { Ok(values) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn global_prop(vm: &Vm, name: &str) -> JsValue { + global_prop_opt(vm, name).expect("global slot should exist") + } + + fn global_prop_opt(vm: &Vm, name: &str) -> Option { + let global = vm.session.global_object(); + let si = vm.kernel_core.string_forge().intern(name).0; + vm.kernel_core + .shape_forge() + .lookup_position(global.shape_id(), si) + .map(|pos| global.get_prop_at(pos)) + } + + fn run_source(vm: &mut Vm, source: &str) -> JsValue { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse failed"); + let module = oxide_compiler::compiler::Compiler::new() + .compile(&program) + .expect("compile failed"); + vm.run(&module).expect("vm run failed") + } + + #[test] + fn full_reset_clean_keeps_session_objects() { + let mut vm = Vm::new(); + let world_ptr = Arc::as_ptr(&vm.session.builtin_world); + let global_ptr = vm.session.global_object.as_ptr(); + let object_proto_ptr = vm.session.builtin_world().object_proto.as_ptr(); + + vm.full_reset(); + + assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.session.builtin_world))); + assert!(std::ptr::eq(global_ptr, vm.session.global_object.as_ptr())); + assert!(std::ptr::eq(object_proto_ptr, vm.session.builtin_world().object_proto.as_ptr())); + assert!(!vm.session.is_dirty_since_snapshot()); + } + + #[test] + fn full_reset_global_dirty_rebuilds_global_and_restores_slots() { + let mut vm = Vm::new(); + let world_ptr = Arc::as_ptr(&vm.session.builtin_world); + let global_ptr = vm.session.global_object.as_ptr(); + let global = unsafe { &mut *(vm.session.global_object.as_ptr() as *mut JsObject) }; + bindings::bind_global_value(&vm.kernel_core, global, "userGlobal", JsValue::int(99)); + unsafe { &mut *(vm.session.global_object.as_ptr() as *mut JsObject) }.bump_generation(); + + vm.full_reset(); + + assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.session.builtin_world))); + assert!(!std::ptr::eq(global_ptr, vm.session.global_object.as_ptr())); + assert!(global_prop_opt(&vm, "userGlobal").is_none()); + assert!(std::ptr::eq( + global_prop(&vm, "Array").as_js_object_ptr(), + vm.session.builtin_world().array_constructor.as_ptr() as *mut JsObject + )); + assert!(std::ptr::eq( + global_prop(&vm, "globalThis").as_js_object_ptr(), + vm.session.global_object.as_ptr() as *mut JsObject + )); + assert!(!vm.session.is_dirty_since_snapshot()); + } + + #[test] + fn full_reset_dirty_builtin_rebinds_global_slot() { + let mut vm = Vm::new(); + let old_object_proto = vm.session.builtin_world().object_proto.as_ptr(); + let old_array_proto = vm.session.builtin_world().array_proto.as_ptr(); + unsafe { &mut *(old_array_proto as *mut JsObject) }.bump_generation(); + + vm.full_reset(); + + assert!(std::ptr::eq(old_object_proto, vm.session.builtin_world().object_proto.as_ptr())); + assert!(!std::ptr::eq(old_array_proto, vm.session.builtin_world().array_proto.as_ptr())); + assert!(std::ptr::eq( + global_prop(&vm, "Array").as_js_object_ptr(), + vm.session.builtin_world().array_constructor.as_ptr() as *mut JsObject + )); + let constructor_si = vm.kernel_core.string_forge().intern("constructor").0; + let array_proto = &*vm.session.builtin_world().array_proto; + let constructor = vm + .resolve_property(array_proto, constructor_si) + .expect("Array.prototype.constructor"); + assert!(std::ptr::eq( + constructor.as_js_object_ptr(), + vm.session.builtin_world().array_constructor.as_ptr() as *mut JsObject + )); + assert!(!vm.session.is_dirty_since_snapshot()); + } + + #[test] + fn full_reset_dirty_function_keeps_call_working() { + let mut vm = Vm::new(); + let function_proto = vm.session.builtin_world().function_proto.as_ptr(); + unsafe { &mut *(function_proto as *mut JsObject) }.bump_generation(); + + vm.full_reset(); + + let result = run_source(&mut vm, "Array.prototype.push.call([1], 2)"); + assert_eq!(result, JsValue::int(2)); + assert!(!vm.session.is_dirty_since_snapshot()); + } + + #[test] + fn full_reset_refreshes_object_prototype_after_object_dirty() { + let mut vm = Vm::new(); + let old_object_proto = vm.session.builtin_world().object_proto.as_ptr(); + unsafe { &mut *(old_object_proto as *mut JsObject) }.bump_generation(); + + vm.full_reset(); + + assert!(!std::ptr::eq(old_object_proto, vm.session.builtin_world().object_proto.as_ptr())); + assert!(std::ptr::eq( + vm.object_prototype.as_ptr(), + vm.session.builtin_world().object_proto.as_ptr() + )); + assert!(!vm.session.is_dirty_since_snapshot()); + } +} diff --git a/crates/oxide_vm/tests/bench_tests.rs b/crates/oxide_vm/tests/bench_tests.rs index d61cbba..fca0dfb 100644 --- a/crates/oxide_vm/tests/bench_tests.rs +++ b/crates/oxide_vm/tests/bench_tests.rs @@ -1,15 +1,20 @@ use oxide_compiler::compiler::Compiler; +use oxide_compiler::module::CompiledModule; use oxide_parser::Allocator; use oxide_vm::vm::Vm; +fn compile(source: &str) -> CompiledModule { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse failed"); + Compiler::new().compile(&program).expect("compile failed") +} + #[test] fn bench_1m_property_reads() { use std::time::Instant; - let allocator = Allocator::default(); let source = "({a: 1, b: 2, c: 3, d: 4, e: 5}).e"; - let program = oxide_parser::parse(&allocator, source).expect("parse failed"); - let module = Compiler::new().compile(&program).expect("compile failed"); + let module = compile(source); let mut vm = Vm::new(); vm.run(&module).expect("vm run failed"); @@ -27,7 +32,72 @@ fn bench_1m_property_reads() { println!("1M IC property reads: {:.2}ms ({:.0} ns/read)", elapsed.as_secs_f64() * 1000.0, ns_per); if cfg!(debug_assertions) { println!(" (debug build -- skipping timing assertion)"); + } else if cfg!(windows) { + println!(" (windows release build -- skipping absolute timing assertion)"); } else { assert!(ns_per < 500.0, "{} ns/read exceeds 500ns target", ns_per); } } + +#[test] +fn bench_clean_reset_cost() { + use std::time::Instant; + + const N: usize = 1_000; + + let mut selective_vm = Vm::new(); + selective_vm.full_reset(); + let selective_start = Instant::now(); + for _ in 0..N { + selective_vm.full_reset(); + } + let selective_elapsed = selective_start.elapsed(); + + let mut legacy_vm = Vm::new(); + legacy_vm.full_reset_legacy_for_bench(); + let legacy_start = Instant::now(); + for _ in 0..N { + legacy_vm.full_reset_legacy_for_bench(); + } + let legacy_elapsed = legacy_start.elapsed(); + + let selective_ns = selective_elapsed.as_nanos() as f64 / N as f64; + let legacy_ns = legacy_elapsed.as_nanos() as f64 / N as f64; + println!( + "clean full_reset: selective {:.2}ms ({:.0} ns/reset), legacy {:.2}ms ({:.0} ns/reset)", + selective_elapsed.as_secs_f64() * 1000.0, + selective_ns, + legacy_elapsed.as_secs_f64() * 1000.0, + legacy_ns + ); + + if cfg!(debug_assertions) { + println!(" (debug build -- skipping timing assertion)"); + } else { + assert!( + selective_elapsed.as_nanos() * 5 <= legacy_elapsed.as_nanos(), + "selective reset must be >=80% faster; selective={selective_ns:.0}ns legacy={legacy_ns:.0}ns" + ); + } +} + +#[test] +fn bench_dirty_reset_cost() { + use std::time::{Duration, Instant}; + + const N: usize = 1_000; + + let dirty_array_proto = compile("Array.prototype.__resetBench = 1"); + let mut vm = Vm::new(); + let mut elapsed = Duration::ZERO; + + for _ in 0..N { + vm.run(&dirty_array_proto).expect("dirty Array.prototype"); + let reset_start = Instant::now(); + vm.full_reset(); + elapsed += reset_start.elapsed(); + } + + let ns_per = elapsed.as_nanos() as f64 / N as f64; + println!("dirty array full_reset: {:.2}ms ({:.0} ns/reset)", elapsed.as_secs_f64() * 1000.0, ns_per); +}