From abcde705cbcfbc986f573e31f44560caf8c0e201 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 6 Nov 2026 03:03:37 -0400 Subject: [PATCH] perf(runtime): reuse wrappers before resolving runtime classes Interface-typed and Object returns looked up the object's runtime class (GetRuntimeClassName, a metadata lookup) before checking the wrapper cache, so returning an already-wrapped object paid for both on every call, twice for interface-typed returns. The cache is now checked first on both engines; napi records which cached wrappers were built for the object's class so an interface-typed return can reuse them. Runtime class names convert from UTF-16 directly instead of formatting one character at a time, which was about half of the lookup's cost. JsonArray.GetAt on cached elements: 2.0M -> 4.8M calls/s (classic V8). --- runtime/src/lib.rs | 12 +++++--- runtime/src/method_call.rs | 9 ------ runtime/src/napi_engine/ns_hostobject.rs | 2 +- runtime/src/napi_engine/ns_proxy.rs | 38 +++++++++++++++++++++--- runtime/src/ns_proxy.rs | 33 +++++++++++++++----- runtime/src/wrapper_cache.rs | 25 ++++++++++++++++ 6 files changed, 93 insertions(+), 26 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 95b8b3b..f609a31 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1193,6 +1193,10 @@ pub(crate) fn return_value_from_kind<'a>( v8::null(scope).into() } else { let instance = unsafe { IUnknown::from_raw(result) }; + // An object that already has a wrapper skips the runtime-class lookup below. + if let Some(local) = crate::wrapper_cache::get_for(scope, &instance) { + return local.into(); + } // Wrap by the runtime class: an interface such as IPropertySet declares no // members itself and only requires the generic IMap/IIterable instances. match crate::ns_proxy::resolve_runtime_class(&instance, type_name.as_ref()) { @@ -4556,7 +4560,7 @@ fn create_ns_ctor_instance_object<'a>( .cast::() .ok() .and_then(|insp| insp.GetRuntimeClassName().ok()) - .map(|cn| cn.to_string()) + .map(|cn| cn.to_string_lossy()) .and_then(|n| MetadataReader::find_by_name(&n).map(|d| (n, d))) .filter(|(_, d)| !matches!(d.read().kind(), DeclarationKind::Struct)); match resolved { @@ -4875,7 +4879,7 @@ fn create_ns_ctor_instance_object<'a>( .cast::() .ok() .and_then(|insp| insp.GetRuntimeClassName().ok()) - .map(|cn| cn.to_string()) + .map(|cn| cn.to_string_lossy()) .and_then(|n| MetadataReader::find_by_name(&n).map(|d| (n, d))) .filter(|(_, d)| !matches!(d.read().kind(), DeclarationKind::Struct)); match resolved { @@ -5974,7 +5978,7 @@ unsafe fn raw_result_to_local<'s>( let unknown = IUnknown::from_raw(result); if let Ok(inspectable) = unknown.cast::() { if let Ok(class_name) = inspectable.GetRuntimeClassName() { - let name_str = class_name.to_string(); + let name_str = class_name.to_string_lossy(); if let Some(decl) = MetadataReader::find_by_name(&name_str) { let instance = unknown.clone(); return Some( @@ -7986,7 +7990,7 @@ fn js_delegate_run( } let inspectable = owned.cast::().ok()?; let class_name = inspectable.GetRuntimeClassName().ok()?; - let name_str = class_name.to_string(); + let name_str = class_name.to_string_lossy(); let decl = MetadataReader::find_by_name(&name_str)?; Some( create_ns_ctor_instance_object( diff --git a/runtime/src/method_call.rs b/runtime/src/method_call.rs index d6b973d..fd0d305 100644 --- a/runtime/src/method_call.rs +++ b/runtime/src/method_call.rs @@ -1390,16 +1390,7 @@ impl MethodCall { crate::create_struct_object_from_raw(declaration, inner, scope) .into() } else { - // Attempt to inspect runtime identity via IInspectable let instance = unsafe { IUnknown::from_raw(inner) }; - match instance.clone().cast::() { - Ok(ins) => { - let _ = ins.GetRuntimeClassName(); - } - Err(e) => { - let _ = e; - } - } crate::ns_proxy::create_ns_ctor_instance_object( sig.as_str(), None, diff --git a/runtime/src/napi_engine/ns_hostobject.rs b/runtime/src/napi_engine/ns_hostobject.rs index fd892eb..ceb02ec 100644 --- a/runtime/src/napi_engine/ns_hostobject.rs +++ b/runtime/src/napi_engine/ns_hostobject.rs @@ -168,7 +168,7 @@ pub fn is_interface(declaration: &Decl) -> bool { /// The metadata class named by `instance`'s GetRuntimeClassName. pub fn runtime_class(instance: &IUnknown) -> Option<(String, Decl)> { let inspectable = instance.cast::().ok()?; - let name = unsafe { inspectable.GetRuntimeClassName() }.ok()?.to_string(); + let name = unsafe { inspectable.GetRuntimeClassName() }.ok()?.to_string_lossy(); let declaration = MetadataReader::find_by_name(&name)?; if !matches!(declaration.read().kind(), DeclarationKind::Class) { return None; diff --git a/runtime/src/napi_engine/ns_proxy.rs b/runtime/src/napi_engine/ns_proxy.rs index 6b54987..bffeb2e 100644 --- a/runtime/src/napi_engine/ns_proxy.rs +++ b/runtime/src/napi_engine/ns_proxy.rs @@ -207,6 +207,9 @@ struct InstanceCacheEntry { /// The wrapper is the JS object of a JS subclass instance: returned for this identity whatever /// class the caller expects (its runtime class, a managed type, isn't in the metadata). any_type: bool, + /// The wrapper was built for the object's class, so it serves any interface-typed return + /// of the same object. + is_class: bool, /// Serial of the InstanceState that owns this entry. On that state's Drop we remove the entry /// only if the serial still matches — so a newer proxy for a reused COM address is never evicted. serial: u64, @@ -243,9 +246,24 @@ fn cached_instance(env: &Env, identity: usize, class_name: &str) -> Option Option { + let type_name = NAPI_INSTANCE_CACHE.with(|c| { + c.borrow().get(&identity).filter(|e| e.is_class).map(|e| e.type_name.clone()) + })?; + cached_instance(env, identity, &type_name) +} + /// Cache `proxy` under `identity` as a weak ref (refcount 0 — GC may reclaim; the entry is /// re-created on the next wrap if so). -fn cache_instance(env: &Env, identity: usize, class_name: &str, serial: u64, proxy: &JsObject) { +fn cache_instance( + env: &Env, + identity: usize, + class_name: &str, + is_class: bool, + serial: u64, + proxy: &JsObject, +) { flush_ref_graveyard(); let mut proxy_ref: napi::sys::napi_ref = std::ptr::null_mut(); let st = unsafe { napi::sys::napi_create_reference(env.raw(), proxy.raw(), 0, &mut proxy_ref) }; @@ -258,6 +276,7 @@ fn cache_instance(env: &Env, identity: usize, class_name: &str, serial: u64, pro proxy_ref, type_name: class_name.to_string(), any_type: false, + is_class, serial, }, ); @@ -1203,6 +1222,16 @@ pub fn create_instance_proxy( ) -> napi::Result { let identity = com_identity_key(&instance); + // An interface-typed return of an object that already has a class wrapper reuses it + // without asking for the runtime class again. + if let Some(id) = identity { + if crate::napi_engine::ns_hostobject::is_interface(&declaration) { + if let Some(existing) = cached_class_instance(env, id) { + return Ok(existing); + } + } + } + // Hybrid fast path: wrap non-collection *class* instances as host objects (shared prototype, // no Proxy trap). The effective type is the class from the declaration when it is itself a // host-eligible class (`new` path), or — for interface-typed returns like `IJsonValue` whose @@ -1227,7 +1256,7 @@ pub fn create_instance_proxy( env, &eff_name, instance, serial, identity, )?; if let Some(id) = identity { - cache_instance(env, id, &eff_name, serial, &obj); + cache_instance(env, id, &eff_name, true, serial, &obj); } return Ok(obj); } @@ -1253,6 +1282,7 @@ pub fn create_instance_proxy( } } let serial = next_instance_serial(); + let is_class = matches!(declaration.read().kind(), DeclarationKind::Class); let iface = interface_context(&declaration); let state = Rc::new(InstanceState { class_name: class_name.to_string(), @@ -1544,7 +1574,7 @@ pub fn create_instance_proxy( let target = as_unknown(env, env.create_object()?); let proxy = make_proxy(env, target, handler)?; if let Some(id) = identity { - cache_instance(env, id, class_name, serial, &proxy); + cache_instance(env, id, class_name, is_class, serial, &proxy); } Ok(proxy) } @@ -1623,7 +1653,7 @@ pub fn try_wrap_inspectable_pointer(env: &Env, raw: *mut c_void) -> Option().ok()?; let class_name = unsafe { inspectable.GetRuntimeClassName() }.ok()?; - let name_str = class_name.to_string(); + let name_str = class_name.to_string_lossy(); let declaration = MetadataReader::find_by_name(&name_str)?; if declaration.read().kind() != DeclarationKind::Class { return None; diff --git a/runtime/src/ns_proxy.rs b/runtime/src/ns_proxy.rs index c187f01..7b48321 100644 --- a/runtime/src/ns_proxy.rs +++ b/runtime/src/ns_proxy.rs @@ -465,11 +465,14 @@ pub(crate) fn try_wrap_inspectable_pointer<'a>( if let Some(v) = try_unbox_property_value(&instance, scope) { return Some(v); } + if let Some(local) = crate::wrapper_cache::get_for(scope, &instance) { + return Some(local.into()); + } let resolved = instance .cast::() .ok() .and_then(|insp| insp.GetRuntimeClassName().ok()) - .map(|cn| cn.to_string()) + .map(|cn| cn.to_string_lossy()) .and_then(|n| MetadataReader::find_by_name(&n).map(|d| (n, d))) .filter(|(_, d)| !matches!(d.read().kind(), DeclarationKind::Struct)); match resolved { @@ -552,11 +555,15 @@ pub(crate) fn set_ret_val_resolving_object( ) { if return_sig == "Object" && !result.is_null() { let instance = unsafe { IUnknown::from_raw(result) }; + if let Some(local) = crate::wrapper_cache::get_for(scope, &instance) { + retval.set(local.into()); + return; + } let resolved = instance .cast::() .ok() .and_then(|insp| insp.GetRuntimeClassName().ok()) - .map(|cn| cn.to_string()) + .map(|cn| cn.to_string_lossy()) .and_then(|n| MetadataReader::find_by_name(&n).map(|d| (n, d))) .filter(|(_, d)| !matches!(d.read().kind(), DeclarationKind::Struct)); match resolved { @@ -782,13 +789,19 @@ pub(crate) fn handle_named_property_getter( } else { let instance = unsafe { IUnknown::from_raw(result) }; + if let Some(local) = + crate::wrapper_cache::get_for(scope, &instance) + { + retval.set(local.into()); + return; + } let resolved = instance .cast::() .ok() .and_then(|insp| { insp.GetRuntimeClassName().ok() }) - .map(|cn| cn.to_string()) + .map(|cn| cn.to_string_lossy()) .and_then(|n| { MetadataReader::find_by_name(&n) .map(|d| (n, d)) @@ -1098,13 +1111,15 @@ fn instance_method_dispatch( ReturnKind::DynamicObject => { if result.is_null() { None + } else if let Some(local) = crate::wrapper_cache::take_cached(scope, result) { + Some(local.into()) } else { let instance = unsafe { IUnknown::from_raw(result) }; let resolved = instance .cast::() .ok() .and_then(|insp| insp.GetRuntimeClassName().ok()) - .map(|cn| cn.to_string()) + .map(|cn| cn.to_string_lossy()) .and_then(|n| MetadataReader::find_by_name(&n).map(|d| (n, d))) .filter(|(_, d)| !matches!(d.read().kind(), DeclarationKind::Struct)); match resolved { @@ -1173,7 +1188,7 @@ fn find_runtime_class_property( .ok()? .GetRuntimeClassName() .ok() - .map(|s| s.to_string())?; + .map(|s| s.to_string_lossy())?; if runtime_name.is_empty() || runtime_name == static_type_name { return None; @@ -1386,13 +1401,15 @@ pub(crate) fn handle_instance_property_getter( ReturnKind::InterfaceObject { decl, type_name } => { if result.is_null() { Some(v8::null(scope).into()) + } else if let Some(local) = crate::wrapper_cache::take_cached(scope, result) { + Some(local.into()) } else { let instance = unsafe { IUnknown::from_raw(result) }; let (resolved_name, resolved_decl) = instance .cast::() .ok() .and_then(|insp| insp.GetRuntimeClassName().ok()) - .map(|cn| cn.to_string()) + .map(|cn| cn.to_string_lossy()) .and_then(|n| MetadataReader::find_by_name(&n).map(|d| (n, d))) .filter(|(_, d)| !matches!(d.read().kind(), DeclarationKind::Struct)) .unwrap_or_else(|| (type_name.to_string(), decl.clone())); @@ -1867,7 +1884,7 @@ pub(crate) unsafe fn raw_result_to_local<'s>( let unknown = IUnknown::from_raw(result); if let Ok(inspectable) = unknown.cast::() { if let Ok(class_name) = inspectable.GetRuntimeClassName() { - let name_str = class_name.to_string(); + let name_str = class_name.to_string_lossy(); if let Some(decl) = MetadataReader::find_by_name(&name_str) { let instance = unknown.clone(); return Some( @@ -2029,7 +2046,7 @@ pub(crate) fn resolve_runtime_class( .cast::() .ok() .and_then(|insp| insp.GetRuntimeClassName().ok()) - .map(|cn| cn.to_string()) + .map(|cn| cn.to_string_lossy()) .filter(|cn| !cn.is_empty() && cn != declared && !cn.contains('<')) .and_then(|cn| MetadataReader::find_by_name(&cn).map(|d| (cn, d))) .filter(|(_, d)| !matches!(d.read().kind(), DeclarationKind::Struct)) diff --git a/runtime/src/wrapper_cache.rs b/runtime/src/wrapper_cache.rs index f5b9a38..0c50d56 100644 --- a/runtime/src/wrapper_cache.rs +++ b/runtime/src/wrapper_cache.rs @@ -66,6 +66,31 @@ pub(crate) fn get<'s>(scope: &v8::PinScope<'s, '_, ()>, key: usize) -> Option, v8::Local<'s, v8::Object>>(ptr) }) } +/// The live wrapper of the COM object behind `instance`, keyed by its IUnknown identity. +#[inline] +pub(crate) fn get_for<'s>( + scope: &v8::PinScope<'s, '_, ()>, + instance: &windows::core::IUnknown, +) -> Option> { + use windows::core::Interface; + let identity = instance.cast::().ok()?; + get(scope, identity.as_raw() as usize) +} + +/// `get_for` on an owned (+1) return pointer: on a hit the reference is released, on a miss +/// the caller still owns it. +#[inline] +pub(crate) fn take_cached<'s>( + scope: &v8::PinScope<'s, '_, ()>, + owned: *mut c_void, +) -> Option> { + use windows::core::Interface; + let instance = std::mem::ManuallyDrop::new(unsafe { windows::core::IUnknown::from_raw(owned) }); + let local = get_for(scope, &instance)?; + drop(std::mem::ManuallyDrop::into_inner(instance)); + Some(local) +} + /// Cache `object` as the wrapper of COM identity `key` and hand it `ffi`. Returns the address /// of the stored `DeclarationFFI` for internal field 0; it lives until V8 collects `object`. pub(crate) fn insert(