From 9661374b508f77391605dad12e195dbc6bfac127 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 13 Sep 2026 11:40:03 +0000 Subject: [PATCH] src: ffi: create fast-call metadata Symbols lazily The FFI fast-call wrappers key per-function metadata on raw FFI functions using two per-isolate Symbols (kFastArguments / kFastBufferInvoke) that were declared in src/env_properties.h. Everything in env_properties.h is allocated while the startup snapshot is built, so each Symbol advances the isolate's identity-hash RNG before Object.prototype / Function.prototype receive their snapshot identity hashes. In the snapshot produced for Node 26.4.0+ this shifted those hashes so a function map (a function whose `length` was redefined) and a plain-object map (an object literal with an accessor) collide in V8's 64-slot NormalizedMapCache. Every store into such objects then misses the inline cache, and the repro reported in the linked issue is roughly 7x slower. Create the two Symbols lazily in the FFI binding's Initialize, on the first run of internalBinding('ffi') at runtime, instead of declaring them in env_properties.h. They are therefore not allocated during snapshot serialization and no longer bias the snapshot's prototype identity hashes. Their export, property layout, and the fast-call feature behavior are unchanged. Refs: https://github.com/nodejs/node/issues/66011 Signed-off-by: Matteo Collina Assisted-by: Pi --- src/env-inl.h | 32 ++++++++++++++++++++++++++++++++ src/env.h | 20 ++++++++++++++++++++ src/env_properties.h | 2 -- src/node_ffi.cc | 16 +++++++++++++++- 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/env-inl.h b/src/env-inl.h index 47aa31ca9752..fc2a56647b65 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -866,6 +866,23 @@ void Environment::set_process_exit_handler( #undef V #undef VM +inline v8::Local IsolateData::ffi_fast_arguments_symbol() const { + return ffi_fast_arguments_symbol_.Get(isolate_); +} +inline void IsolateData::set_ffi_fast_arguments_symbol( + v8::Local value) { + CHECK(ffi_fast_arguments_symbol_.IsEmpty()); + ffi_fast_arguments_symbol_.Set(isolate_, value); +} +inline v8::Local IsolateData::ffi_fast_buffer_invoke_symbol() const { + return ffi_fast_buffer_invoke_symbol_.Get(isolate_); +} +inline void IsolateData::set_ffi_fast_buffer_invoke_symbol( + v8::Local value) { + CHECK(ffi_fast_buffer_invoke_symbol_.IsEmpty()); + ffi_fast_buffer_invoke_symbol_.Set(isolate_, value); +} + #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) #define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) @@ -881,6 +898,21 @@ void Environment::set_process_exit_handler( #undef VY #undef VP +inline v8::Local Environment::ffi_fast_arguments_symbol() const { + return isolate_data()->ffi_fast_arguments_symbol(); +} +inline void Environment::set_ffi_fast_arguments_symbol( + v8::Local value) { + isolate_data()->set_ffi_fast_arguments_symbol(value); +} +inline v8::Local Environment::ffi_fast_buffer_invoke_symbol() const { + return isolate_data()->ffi_fast_buffer_invoke_symbol(); +} +inline void Environment::set_ffi_fast_buffer_invoke_symbol( + v8::Local value) { + isolate_data()->set_ffi_fast_buffer_invoke_symbol(value); +} + #define V(Name, label, _, __) \ inline v8::Local Environment::Name##_permission_string() const { \ return isolate_data()->Name##_permission_string(); \ diff --git a/src/env.h b/src/env.h index f96feed36f8c..21a1c31511ce 100644 --- a/src/env.h +++ b/src/env.h @@ -205,6 +205,17 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { inline v8::Local async_wrap_provider(int index) const; + // Symbols used by the FFI fast-call API to key per-function metadata on raw + // FFI functions. Kept out of env_properties.h so they are created lazily at + // runtime, not while the startup snapshot is built (allocating Symbols during + // serialization advances the isolate's identity-hash RNG, which can shift the + // snapshot hashes for Object.prototype/Function.prototype and make a function + // map and a plain-object map collide in V8's NormalizedMapCache). + inline v8::Local ffi_fast_arguments_symbol() const; + inline void set_ffi_fast_arguments_symbol(v8::Local value); + inline v8::Local ffi_fast_buffer_invoke_symbol() const; + inline void set_ffi_fast_buffer_invoke_symbol(v8::Local value); + size_t max_young_gen_size = 1; std::unordered_map> static_str_map; @@ -245,6 +256,9 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { PERMISSIONS(V) #undef V + v8::Eternal ffi_fast_arguments_symbol_; + v8::Eternal ffi_fast_buffer_invoke_symbol_; + // Keep a list of all Persistent strings used for AsyncWrap Provider types. std::array, AsyncWrap::PROVIDERS_LENGTH> async_wrap_providers_; @@ -892,6 +906,12 @@ class Environment final : public MemoryRetainer { #undef VY #undef VP + // Runtime-created FFI fast-call API Symbols (see IsolateData). + inline v8::Local ffi_fast_arguments_symbol() const; + inline void set_ffi_fast_arguments_symbol(v8::Local value); + inline v8::Local ffi_fast_buffer_invoke_symbol() const; + inline void set_ffi_fast_buffer_invoke_symbol(v8::Local value); + #define V(Name, label, _, __) \ inline v8::Local Name##_permission_string() const; PERMISSIONS(V) diff --git a/src/env_properties.h b/src/env_properties.h index eb26d3b6cf05..063eb2f53296 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -50,8 +50,6 @@ V(ffi_sb_invoke_slow_symbol, "ffi_sb_invoke_slow_symbol") \ V(ffi_sb_arguments_symbol, "ffi_sb_arguments_symbol") \ V(ffi_sb_return_symbol, "ffi_sb_return_symbol") \ - V(ffi_fast_arguments_symbol, "ffi_fast_arguments_symbol") \ - V(ffi_fast_buffer_invoke_symbol, "ffi_fast_buffer_invoke_symbol") \ V(constructor_key_symbol, "constructor_key_symbol") \ V(handle_onclose_symbol, "handle_onclose") \ V(no_message_symbol, "no_message_symbol") \ diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 7e3da95b958f..b5014b10c2f2 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -1378,7 +1378,21 @@ static void Initialize(Local target, env->ffi_sb_return_symbol()) .Check(); // Fast API wrappers use separate metadata Symbols so pointer-conversion - // routing does not depend on SharedBuffer internals. + // routing does not depend on SharedBuffer internals. These are created here + // (at runtime, on first `internalBinding('ffi')`) instead of being declared + // in env_properties.h, so they are not allocated while the startup snapshot + // is being built. Allocating Symbols during snapshot serialization advances + // the isolate's identity-hash RNG and shifts the identity hashes baked into + // the snapshot for Object.prototype / Function.prototype, which can make a + // function map and a plain-object map collide in V8's NormalizedMapCache. + if (env->ffi_fast_arguments_symbol().IsEmpty()) { + env->set_ffi_fast_arguments_symbol(v8::Symbol::New( + isolate, FIXED_ONE_BYTE_STRING(isolate, "ffi_fast_arguments_symbol"))); + } + if (env->ffi_fast_buffer_invoke_symbol().IsEmpty()) { + env->set_ffi_fast_buffer_invoke_symbol(v8::Symbol::New( + isolate, FIXED_ONE_BYTE_STRING(isolate, "ffi_fast_buffer_invoke_symbol"))); + } target ->Set(context, FIXED_ONE_BYTE_STRING(isolate, "kFastArguments"),