From a9bdb9b17e2346411a54e31350115fb08eddf4a7 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Sat, 17 Oct 2026 07:49:32 -0400 Subject: [PATCH] perf(napi): cache resolved namespace members Namespace objects were Proxies whose get trap resolved metadata and built a new namespace/ctor/enum object on every access (`Windows.Foundation !== Windows.Foundation`). A namespace is now a plain object whose prototype is the resolving Proxy. A resolved name is defined on the object as a read-only, non-enumerable own property, so later reads never reach the Proxy. - Node: Windows.Foundation 4460 -> 12 ns, Windows.UI.Xaml.Controls.Button 11613 -> 14 ns - QuickJS: Windows.Foundation 4300 -> 80 ns, Button 12070 -> 420 ns --- runtime/src/napi_engine/ns_proxy.rs | 39 +++++++++++++++++++++++++---- windows-napi/test/ns-proxy-test.js | 8 ++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/runtime/src/napi_engine/ns_proxy.rs b/runtime/src/napi_engine/ns_proxy.rs index 85e18e9..0389c6b 100644 --- a/runtime/src/napi_engine/ns_proxy.rs +++ b/runtime/src/napi_engine/ns_proxy.rs @@ -648,8 +648,32 @@ fn create_enum_object(env: &Env, declaration: &Decl) -> napi::Result { Ok(obj) } -/// A lazy namespace proxy: `Windows.Data.Json` resolves children through metadata on access. +/// Defines `name` on `obj` as a read-only, non-enumerable, non-configurable data property. +fn define_readonly(env: &Env, obj: &JsObject, name: &str, value: &JsUnknown) -> bool { + let Ok(name) = std::ffi::CString::new(name) else { + return false; + }; + let desc = napi::sys::napi_property_descriptor { + utf8name: name.as_ptr(), + name: std::ptr::null_mut(), + method: None, + getter: None, + setter: None, + value: unsafe { value.raw() }, + attributes: napi::sys::PropertyAttributes::default, + data: std::ptr::null_mut(), + }; + let status = unsafe { napi::sys::napi_define_properties(env.raw(), obj.raw(), 1, &desc) }; + status == napi::sys::Status::napi_ok +} + +/// A lazy namespace: `Windows.Data.Json` resolves children through metadata on first access. +/// The object's prototype is the resolving Proxy; each resolved name becomes a read-only own +/// property, so later reads never reach the Proxy. pub fn create_namespace_proxy(env: &Env, full_name: &str) -> napi::Result { + let ns = env.create_object()?; + define_readonly(env, &ns, "__typeName__", &as_unknown(env, env.create_string(full_name)?)); + let mut handler = env.create_object()?; let ns_name = full_name.to_string(); @@ -659,10 +683,13 @@ pub fn create_namespace_proxy(env: &Env, full_name: &str) -> napi::Result(2) { + define_readonly(env, &receiver, &prop, &value); + } } - resolve_member(env, &format!("{}.{}", get_ns, prop)) + Ok(value) })?; handler.set_named_property("get", get_fn)?; @@ -678,7 +705,9 @@ pub fn create_namespace_proxy(env: &Env, full_name: &str) -> napi::Result