Repository: nodejs/node
Pull request: src: improve textEncoder encode performance with simdutf

Ref: https://github.com/cloudflare/workerd/pull/5448

Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1789/

```
                                                                               confidence improvement accuracy (*)   (**)  (***)
util/text-encoder.js op='encode' type='ascii' n=1000000 len=1024                      ***     11.86 %       ±1.25% ±1.66% ±2.17%
util/text-encoder.js op='encode' type='ascii' n=1000000 len=256                       ***     -7.82 %       ±1.84% ±2.46% ±3.22%
util/text-encoder.js op='encode' type='ascii' n=1000000 len=32                          *      1.26 %       ±1.19% ±1.59% ±2.07%
util/text-encoder.js op='encode' type='ascii' n=1000000 len=8192                      ***    146.74 %       ±0.78% ±1.05% ±1.39%
util/text-encoder.js op='encode' type='one-byte-string' n=1000000 len=1024            ***     20.69 %       ±0.79% ±1.05% ±1.37%
util/text-encoder.js op='encode' type='one-byte-string' n=1000000 len=256             ***      7.46 %       ±0.79% ±1.05% ±1.37%
util/text-encoder.js op='encode' type='one-byte-string' n=1000000 len=32                      -0.57 %       ±0.91% ±1.21% ±1.58%
util/text-encoder.js op='encode' type='one-byte-string' n=1000000 len=8192            ***    182.16 %       ±0.22% ±0.29% ±0.39%
util/text-encoder.js op='encode' type='two-byte-string' n=1000000 len=1024            ***     95.53 %       ±1.09% ±1.47% ±1.95%
util/text-encoder.js op='encode' type='two-byte-string' n=1000000 len=256             ***     32.64 %       ±0.90% ±1.20% ±1.58%
util/text-encoder.js op='encode' type='two-byte-string' n=1000000 len=32                      -0.23 %       ±1.04% ±1.38% ±1.80%
util/text-encoder.js op='encode' type='two-byte-string' n=1000000 len=8192            ***    299.06 %       ±0.64% ±0.86% ±1.14%
util/text-encoder.js op='encodeInto' type='ascii' n=1000000 len=1024                           2.14 %       ±3.97% ±5.29% ±6.88%
util/text-encoder.js op='encodeInto' type='ascii' n=1000000 len=256                            1.22 %       ±3.31% ±4.41% ±5.75%
util/text-encoder.js op='encodeInto' type='ascii' n=1000000 len=32                    ***     20.60 %       ±2.77% ±3.69% ±4.80%
util/text-encoder.js op='encodeInto' type='ascii' n=1000000 len=8192                           0.91 %       ±3.13% ±4.16% ±5.42%
util/text-encoder.js op='encodeInto' type='one-byte-string' n=1000000 len=1024                 1.21 %       ±5.16% ±6.88% ±8.99%
util/text-encoder.js op='encodeInto' type='one-byte-string' n=1000000 len=256                 -0.65 %       ±1.12% ±1.49% ±1.94%
util/text-encoder.js op='encodeInto' type='one-byte-string' n=1000000 len=32                  -0.48 %       ±3.29% ±4.38% ±5.70%
util/text-encoder.js op='encodeInto' type='one-byte-string' n=1000000 len=8192                -1.57 %       ±1.69% ±2.25% ±2.94%
util/text-encoder.js op='encodeInto' type='two-byte-string' n=1000000 len=1024                -0.51 %       ±0.69% ±0.92% ±1.20%
util/text-encoder.js op='encodeInto' type='two-byte-string' n=1000000 len=256                 -0.38 %       ±0.67% ±0.89% ±1.16%
util/text-encoder.js op='encodeInto' type='two-byte-string' n=1000000 len=32            *     -2.35 %       ±2.05% ±2.73% ±3.55%
util/text-encoder.js op='encodeInto' type='two-byte-string' n=1000000 len=8192          *     -0.32 %       ±0.31% ±0.41% ±0.54%
```

Changed files:
filename: src/encoding_binding.cc
patch: @@ -312,11 +312,12 @@ void BindingData::EncodeUtf8String(const FunctionCallbackInfo<Value>& args) {
   CHECK_GE(args.Length(), 1);
   CHECK(args[0]->IsString());
 
-  Local<String> str = args[0].As<String>();
-  size_t length = str->Utf8LengthV2(isolate);
+  Local<String> source = args[0].As<String>();
 
-  Local<ArrayBuffer> ab;
-  {
+  // For small strings, use the V8 path
+  static constexpr int kSmallStringThreshold = 32;
+  if (source->Length() <= kSmallStringThreshold) {
+    size_t length = source->Utf8LengthV2(isolate);
     std::unique_ptr<BackingStore> bs = ArrayBuffer::NewBackingStore(
         isolate,
         length,
@@ -328,16 +329,88 @@ void BindingData::EncodeUtf8String(const FunctionCallbackInfo<Value>& args) {
       return;
     }
 
-    // We are certain that `data` is sufficiently large
-    str->WriteUtf8V2(isolate,
-                     static_cast<char*>(bs->Data()),
-                     bs->MaxByteLength(),
-                     String::WriteFlags::kReplaceInvalidUtf8);
+    source->WriteUtf8V2(isolate,
+                        static_cast<char*>(bs->Data()),
+                        bs->MaxByteLength(),
+                        String::WriteFlags::kReplaceInvalidUtf8);
+    Local<ArrayBuffer> ab = ArrayBuffer::New(isolate, std::move(bs));
+    args.GetReturnValue().Set(Uint8Array::New(ab, 0, length));
+    return;
+  }
 
-    ab = ArrayBuffer::New(isolate, std::move(bs));
+  size_t length = source->Length();
+  size_t utf8_length = 0;
+  bool is_one_byte = source->IsOneByte();
+
+  if (is_one_byte) {
+    // One-byte string (Latin1) - copy to buffer first, then process
+    MaybeStackBuffer<uint8_t, MAX_SIZE_FOR_STACK_ALLOC> latin1_buffer(length);
+    source->WriteOneByteV2(isolate, 0, length, latin1_buffer.out());
+
+    auto data = reinterpret_cast<const char*>(latin1_buffer.out());
+
+    // Check if it's pure ASCII - if so, we can just copy
+    simdutf::result result = simdutf::validate_ascii_with_errors(data, length);
+    if (result.error == simdutf::SUCCESS) {
+      // Pure ASCII - direct copy
+      std::unique_ptr<BackingStore> bs = ArrayBuffer::NewBackingStore(
+          isolate, length, BackingStoreInitializationMode::kUninitialized);
+      CHECK(bs);
+      memcpy(bs->Data(), data, length);
+      Local<ArrayBuffer> ab = ArrayBuffer::New(isolate, std::move(bs));
+      args.GetReturnValue().Set(Uint8Array::New(ab, 0, length));
+      return;
+    }
+
+    // Latin1 with non-ASCII characters - need conversion
+    utf8_length = simdutf::utf8_length_from_latin1(data, length);
+    std::unique_ptr<BackingStore> bs = ArrayBuffer::NewBackingStore(
+        isolate, utf8_length, BackingStoreInitializationMode::kUninitialized);
+    CHECK(bs);
+    [[maybe_unused]] size_t written = simdutf::convert_latin1_to_utf8(
+        data, length, static_cast<char*>(bs->Data()));
+    DCHECK_EQ(written, utf8_length);
+    Local<ArrayBuffer> ab = ArrayBuffer::New(isolate, std::move(bs));
+    args.GetReturnValue().Set(Uint8Array::New(ab, 0, utf8_length));
+    return;
+  }
+
+  // Two-byte string (UTF-16) - copy to buffer first
+  MaybeStackBuffer<uint16_t, MAX_SIZE_FOR_STACK_ALLOC> utf16_buffer(length);
+  source->WriteV2(isolate, 0, length, utf16_buffer.out());
+
+  auto data = reinterpret_cast<char16_t*>(utf16_buffer.out());
+
+  // Check for unpaired surrogates
+  simdutf::result validation_result =
+      simdutf::validate_utf16_with_errors(data, length);
+
+  if (validation_result.error == simdutf::SUCCESS) {
+    // Valid UTF-16 - use the fast path
+    utf8_length = simdutf::utf8_length_from_utf16(data, length);
+    std::unique_ptr<BackingStore> bs = ArrayBuffer::NewBackingStore(
+        isolate, utf8_length, BackingStoreInitializationMode::kUninitialized);
+    CHECK(bs);
+    [[maybe_unused]] size_t written = simdutf::convert_utf16_to_utf8(
+        data, length, static_cast<char*>(bs->Data()));
+    DCHECK_EQ(written, utf8_length);
+    Local<ArrayBuffer> ab = ArrayBuffer::New(isolate, std::move(bs));
+    args.GetReturnValue().Set(Uint8Array::New(ab, 0, utf8_length));
+    return;
   }
 
-  args.GetReturnValue().Set(Uint8Array::New(ab, 0, length));
+  // Invalid UTF-16 with unpaired surrogates - convert to well-formed in place
+  simdutf::to_well_formed_utf16(data, length, data);
+
+  utf8_length = simdutf::utf8_length_from_utf16(data, length);
+  std::unique_ptr<BackingStore> bs = ArrayBuffer::NewBackingStore(
+      isolate, utf8_length, BackingStoreInitializationMode::kUninitialized);
+  CHECK(bs);
+  [[maybe_unused]] size_t written = simdutf::convert_utf16_to_utf8(
+      data, length, static_cast<char*>(bs->Data()));
+  DCHECK_EQ(written, utf8_length);
+  Local<ArrayBuffer> ab = ArrayBuffer::New(isolate, std::move(bs));
+  args.GetReturnValue().Set(Uint8Array::New(ab, 0, utf8_length));
 }
 
 // Convert the input into an encoded string