From f0eb93f6551928eb35a3ab40e1e8b49d1a5e907f Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 8 Apr 2024 13:23:56 -0400 Subject: [PATCH 1/2] buffer: improve `btoa` performance --- benchmark/buffers/buffer-btoa.js | 20 +++++++++++ lib/buffer.js | 9 ++--- src/node_buffer.cc | 59 ++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 benchmark/buffers/buffer-btoa.js diff --git a/benchmark/buffers/buffer-btoa.js b/benchmark/buffers/buffer-btoa.js new file mode 100644 index 00000000000000..3867d5890b1d79 --- /dev/null +++ b/benchmark/buffers/buffer-btoa.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common.js'); +const assert = require('node:assert'); + +const bench = common.createBenchmark(main, { + size: [16, 32, 64, 128, 256, 1024], + n: [1e6], +}); + +function main({ n, size }) { + const input = 'A'.repeat(size); + let out = 0; + + bench.start(); + for (let i = 0; i < n; i++) { + out += btoa(input).length; + } + bench.end(n); + assert(out > 0); +} diff --git a/lib/buffer.js b/lib/buffer.js index ea94ebf24192f9..d4e9830bacd82f 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -69,6 +69,7 @@ const { kMaxLength, kStringMaxLength, atob: _atob, + btoa: _btoa, } = internalBinding('buffer'); const { constants: { @@ -1249,13 +1250,7 @@ function btoa(input) { if (arguments.length === 0) { throw new ERR_MISSING_ARGS('input'); } - input = `${input}`; - for (let n = 0; n < input.length; n++) { - if (input[n].charCodeAt(0) > 0xff) - throw lazyDOMException('Invalid character', 'InvalidCharacterError'); - } - const buf = Buffer.from(input, 'latin1'); - return buf.toString('base64'); + return _btoa(`${input}`); } function atob(input) { diff --git a/src/node_buffer.cc b/src/node_buffer.cc index b31beada451bc8..0f5948545be266 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1211,6 +1211,63 @@ void DetachArrayBuffer(const FunctionCallbackInfo& args) { } } +static void Btoa(const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 1); + Environment* env = Environment::GetCurrent(args); + THROW_AND_RETURN_IF_NOT_STRING(env, args[0], "argument"); + + Local input = args[0].As(); + MaybeStackBuffer buffer; + size_t written; + + if (input->IsExternalOneByte()) { // 8-bit case + auto ext = input->GetExternalOneByteStringResource(); + size_t expected_length = simdutf::base64_length_from_binary(ext->length()); + buffer.AllocateSufficientStorage(expected_length + 1); + buffer.SetLengthAndZeroTerminate(expected_length); + written = + simdutf::binary_to_base64(ext->data(), ext->length(), buffer.out()); + } else if (input->IsOneByte()) { + MaybeStackBuffer stack_buf(input->Length()); + input->WriteOneByte(env->isolate(), + stack_buf.out(), + 0, + input->Length(), + String::NO_NULL_TERMINATION); + + size_t expected_length = + simdutf::base64_length_from_binary(input->Length()); + buffer.AllocateSufficientStorage(expected_length + 1); + buffer.SetLengthAndZeroTerminate(expected_length); + written = + simdutf::binary_to_base64(reinterpret_cast(*stack_buf), + input->Length(), + buffer.out()); + } else { + String::Value value(env->isolate(), input); + MaybeStackBuffer stack_buf(value.length()); + size_t out_len = simdutf::convert_utf16_to_latin1( + reinterpret_cast(*value), + value.length(), + stack_buf.out()); + if (out_len == 0) { // error + return args.GetReturnValue().SetEmptyString(); + } + size_t expected_length = simdutf::base64_length_from_binary(out_len); + buffer.AllocateSufficientStorage(expected_length + 1); + buffer.SetLengthAndZeroTerminate(expected_length); + written = simdutf::binary_to_base64(*stack_buf, out_len, buffer.out()); + } + + auto value = + String::NewFromOneByte(env->isolate(), + reinterpret_cast(buffer.out()), + NewStringType::kNormal, + written) + .ToLocalChecked(); + return args.GetReturnValue().Set(value); +} + // In case of success, the decoded string is returned. // In case of error, a negative value is returned: // * -1 indicates a single character remained, @@ -1329,6 +1386,7 @@ void Initialize(Local target, Isolate* isolate = env->isolate(); SetMethodNoSideEffect(context, target, "atob", Atob); + SetMethodNoSideEffect(context, target, "btoa", Btoa); SetMethod(context, target, "setBufferPrototype", SetBufferPrototype); SetMethodNoSideEffect(context, target, "createFromString", CreateFromString); @@ -1433,6 +1491,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(CopyArrayBuffer); registry->Register(Atob); + registry->Register(Btoa); } } // namespace Buffer From 6900b0dce7cf6f9caaa852fa7a4d574cd0462d78 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Wed, 10 Apr 2024 09:44:11 -0400 Subject: [PATCH 2/2] fix: return exception --- lib/buffer.js | 6 +++++- src/node_buffer.cc | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/buffer.js b/lib/buffer.js index d4e9830bacd82f..d66d5650f30e73 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -1250,7 +1250,11 @@ function btoa(input) { if (arguments.length === 0) { throw new ERR_MISSING_ARGS('input'); } - return _btoa(`${input}`); + const result = _btoa(`${input}`); + if (result === -1) { + throw lazyDOMException('Invalid character', 'InvalidCharacterError'); + } + return result; } function atob(input) { diff --git a/src/node_buffer.cc b/src/node_buffer.cc index 0f5948545be266..e63318b65b2e61 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1251,7 +1251,7 @@ static void Btoa(const FunctionCallbackInfo& args) { value.length(), stack_buf.out()); if (out_len == 0) { // error - return args.GetReturnValue().SetEmptyString(); + return args.GetReturnValue().Set(-1); } size_t expected_length = simdutf::base64_length_from_binary(out_len); buffer.AllocateSufficientStorage(expected_length + 1);