From 495feb5941c56c4a134110f07cc6f2a60a1117de Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Wed, 18 Feb 2026 02:41:18 +0500 Subject: [PATCH 1/6] buffer: improve performance of multiple Buffer operations - copyBytesFrom: calculate byte offsets directly instead of slicing into an intermediate typed array - toString('hex'): use V8 Uint8Array.prototype.toHex() builtin - fill: add single-char ASCII fast path - indexOf: use indexOfString directly for ASCII encoding - swap16/32/64: add V8 Fast API functions --- benchmark/buffers/buffer-bytelength-string.js | 2 +- benchmark/buffers/buffer-copy-bytes-from.js | 31 +++++++ benchmark/buffers/buffer-fill.js | 1 + benchmark/buffers/buffer-indexof.js | 2 +- benchmark/buffers/buffer-tostring.js | 2 +- lib/buffer.js | 88 +++++++++++++------ src/node_buffer.cc | 40 ++++++++- test/parallel/test-buffer-from.js | 39 ++++++++ test/parallel/test-buffer-tostring-range.js | 40 +++++++++ 9 files changed, 212 insertions(+), 33 deletions(-) create mode 100644 benchmark/buffers/buffer-copy-bytes-from.js diff --git a/benchmark/buffers/buffer-bytelength-string.js b/benchmark/buffers/buffer-bytelength-string.js index 557bc8d6fe38d6..65d92cb6f1cbba 100644 --- a/benchmark/buffers/buffer-bytelength-string.js +++ b/benchmark/buffers/buffer-bytelength-string.js @@ -4,7 +4,7 @@ const common = require('../common'); const bench = common.createBenchmark(main, { type: ['one_byte', 'two_bytes', 'three_bytes', 'four_bytes', 'latin1'], - encoding: ['utf8', 'base64'], + encoding: ['utf8', 'base64', 'latin1', 'hex'], repeat: [1, 2, 16, 256], // x16 n: [4e6], }); diff --git a/benchmark/buffers/buffer-copy-bytes-from.js b/benchmark/buffers/buffer-copy-bytes-from.js new file mode 100644 index 00000000000000..508092264f3a43 --- /dev/null +++ b/benchmark/buffers/buffer-copy-bytes-from.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + type: ['Uint8Array', 'Uint16Array', 'Uint32Array', 'Float64Array'], + len: [64, 256, 2048], + partial: ['none', 'offset', 'offset-length'], + n: [6e5], +}); + +function main({ n, len, type, partial }) { + const TypedArrayCtor = globalThis[type]; + const src = new TypedArrayCtor(len); + for (let i = 0; i < len; i++) src[i] = i; + + let offset; + let length; + if (partial === 'offset') { + offset = len >>> 2; + } else if (partial === 'offset-length') { + offset = len >>> 2; + length = len >>> 1; + } + + bench.start(); + for (let i = 0; i < n; i++) { + Buffer.copyBytesFrom(src, offset, length); + } + bench.end(n); +} diff --git a/benchmark/buffers/buffer-fill.js b/benchmark/buffers/buffer-fill.js index f1ae896843eee0..95e584e246af73 100644 --- a/benchmark/buffers/buffer-fill.js +++ b/benchmark/buffers/buffer-fill.js @@ -10,6 +10,7 @@ const bench = common.createBenchmark(main, { 'fill("t")', 'fill("test")', 'fill("t", "utf8")', + 'fill("t", "ascii")', 'fill("t", 0, "utf8")', 'fill("t", 0)', 'fill(Buffer.alloc(1), 0)', diff --git a/benchmark/buffers/buffer-indexof.js b/benchmark/buffers/buffer-indexof.js index 52cc95ccb9cf7e..ac5dffdd6d9103 100644 --- a/benchmark/buffers/buffer-indexof.js +++ b/benchmark/buffers/buffer-indexof.js @@ -19,7 +19,7 @@ const searchStrings = [ const bench = common.createBenchmark(main, { search: searchStrings, - encoding: ['undefined', 'utf8', 'ucs2'], + encoding: ['undefined', 'utf8', 'ascii', 'latin1', 'ucs2'], type: ['buffer', 'string'], n: [5e4], }, { diff --git a/benchmark/buffers/buffer-tostring.js b/benchmark/buffers/buffer-tostring.js index 0638dc996b39ac..64d2373d49980d 100644 --- a/benchmark/buffers/buffer-tostring.js +++ b/benchmark/buffers/buffer-tostring.js @@ -3,7 +3,7 @@ const common = require('../common.js'); const bench = common.createBenchmark(main, { - encoding: ['', 'utf8', 'ascii', 'latin1', 'hex', 'UCS-2'], + encoding: ['', 'utf8', 'ascii', 'latin1', 'hex', 'base64', 'base64url', 'UCS-2'], args: [0, 1, 3], len: [1, 64, 1024], n: [1e6], diff --git a/lib/buffer.js b/lib/buffer.js index 93cf26387cc762..507d68b6664957 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -50,10 +50,21 @@ const { TypedArrayPrototypeGetByteOffset, TypedArrayPrototypeGetLength, TypedArrayPrototypeSet, - TypedArrayPrototypeSlice, Uint8Array, + Uint8ArrayPrototype, + uncurryThis, } = primordials; +// V8 shipping feature (toHex) is installed by +// InitializeExperimentalGlobal(), which is skipped during snapshot creation +// TODO: Remove this once V8 shipping feature (toHex) is available. +let Uint8ArrayPrototypeToHex; +function ensureUint8ArrayToHex() { + if (Uint8ArrayPrototypeToHex === undefined) { + Uint8ArrayPrototypeToHex = uncurryThis(Uint8ArrayPrototype.toHex); + } +} + const { byteLengthUtf8, compare: _compare, @@ -382,6 +393,8 @@ Buffer.copyBytesFrom = function copyBytesFrom(view, offset, length) { return new FastBuffer(); } + const byteLength = TypedArrayPrototypeGetByteLength(view); + if (offset !== undefined || length !== undefined) { if (offset !== undefined) { validateInteger(offset, 'offset', 0); @@ -389,21 +402,32 @@ Buffer.copyBytesFrom = function copyBytesFrom(view, offset, length) { } else { offset = 0; } + let end; if (length !== undefined) { validateInteger(length, 'length', 0); - end = offset + length; + end = MathMin(offset + length, viewLength); } else { end = viewLength; } - view = TypedArrayPrototypeSlice(view, offset, end); + if (end <= offset) return new FastBuffer(); + + const elementSize = byteLength / viewLength; + const srcByteOffset = TypedArrayPrototypeGetByteOffset(view) + + offset * elementSize; + const srcByteLength = (end - offset) * elementSize; + + return fromArrayLike(new Uint8Array( + TypedArrayPrototypeGetBuffer(view), + srcByteOffset, + srcByteLength)); } return fromArrayLike(new Uint8Array( TypedArrayPrototypeGetBuffer(view), TypedArrayPrototypeGetByteOffset(view), - TypedArrayPrototypeGetByteLength(view))); + byteLength)); }; // Identical to the built-in %TypedArray%.of(), but avoids using the deprecated @@ -550,14 +574,15 @@ function fromArrayBuffer(obj, byteOffset, length) { } function fromArrayLike(obj) { - if (obj.length <= 0) + const len = obj.length; + if (len <= 0) return new FastBuffer(); - if (obj.length < (Buffer.poolSize >>> 1)) { - if (obj.length > (poolSize - poolOffset)) + if (len < (Buffer.poolSize >>> 1)) { + if (len > (poolSize - poolOffset)) createPool(); - const b = new FastBuffer(allocPool, poolOffset, obj.length); + const b = new FastBuffer(allocPool, poolOffset, len); TypedArrayPrototypeSet(b, obj, 0); - poolOffset += obj.length; + poolOffset += len; alignPool(); return b; } @@ -657,6 +682,14 @@ function base64ByteLength(str, bytes) { return (bytes * 3) >>> 2; } +function hexSliceToHex(buf, start, end) { + ensureUint8ArrayToHex(); + return Uint8ArrayPrototypeToHex( + new Uint8Array(TypedArrayPrototypeGetBuffer(buf), + TypedArrayPrototypeGetByteOffset(buf) + start, + end - start)); +} + const encodingOps = { utf8: { encoding: 'utf8', @@ -701,11 +734,7 @@ const encodingOps = { write: asciiWrite, slice: asciiSlice, indexOf: (buf, val, byteOffset, dir) => - indexOfBuffer(buf, - fromStringFast(val, encodingOps.ascii), - byteOffset, - encodingsMap.ascii, - dir), + indexOfString(buf, val, byteOffset, encodingsMap.latin1, dir), }, base64: { encoding: 'base64', @@ -738,7 +767,7 @@ const encodingOps = { encodingVal: encodingsMap.hex, byteLength: (string) => string.length >>> 1, write: hexWrite, - slice: hexSlice, + slice: hexSliceToHex, indexOf: (buf, val, byteOffset, dir) => indexOfBuffer(buf, fromStringFast(val, encodingOps.hex), @@ -1087,7 +1116,7 @@ function _fill(buf, value, offset, end, encoding) { value = 0; } else if (value.length === 1) { // Fast path: If `value` fits into a single byte, use that numeric value. - if (normalizedEncoding === 'utf8') { + if (normalizedEncoding === 'utf8' || normalizedEncoding === 'ascii') { const code = StringPrototypeCharCodeAt(value, 0); if (code < 128) { value = code; @@ -1137,21 +1166,22 @@ function _fill(buf, value, offset, end, encoding) { } Buffer.prototype.write = function write(string, offset, length, encoding) { + const len = this.length; // Buffer#write(string); if (offset === undefined) { - return utf8Write(this, string, 0, this.length); + return utf8Write(this, string, 0, len); } // Buffer#write(string, encoding) if (length === undefined && typeof offset === 'string') { encoding = offset; - length = this.length; + length = len; offset = 0; // Buffer#write(string, offset[, length][, encoding]) } else { - validateOffset(offset, 'offset', 0, this.length); + validateOffset(offset, 'offset', 0, len); - const remaining = this.length - offset; + const remaining = len - offset; if (length === undefined) { length = remaining; @@ -1159,7 +1189,7 @@ Buffer.prototype.write = function write(string, offset, length, encoding) { encoding = length; length = remaining; } else { - validateOffset(length, 'length', 0, this.length); + validateOffset(length, 'length', 0, len); if (length > remaining) length = remaining; } @@ -1177,9 +1207,10 @@ Buffer.prototype.write = function write(string, offset, length, encoding) { }; Buffer.prototype.toJSON = function toJSON() { - if (this.length > 0) { - const data = new Array(this.length); - for (let i = 0; i < this.length; ++i) + const len = this.length; + if (len > 0) { + const data = new Array(len); + for (let i = 0; i < len; ++i) data[i] = this[i]; return { type: 'Buffer', data }; } @@ -1233,7 +1264,8 @@ Buffer.prototype.swap16 = function swap16() { swap(this, i, i + 1); return this; } - return _swap16(this); + _swap16(this); + return this; }; Buffer.prototype.swap32 = function swap32() { @@ -1250,7 +1282,8 @@ Buffer.prototype.swap32 = function swap32() { } return this; } - return _swap32(this); + _swap32(this); + return this; }; Buffer.prototype.swap64 = function swap64() { @@ -1269,7 +1302,8 @@ Buffer.prototype.swap64 = function swap64() { } return this; } - return _swap64(this); + _swap64(this); + return this; }; Buffer.prototype.toLocaleString = Buffer.prototype.toString; diff --git a/src/node_buffer.cc b/src/node_buffer.cc index d4a63cf610ca7f..bdac6a5a9dbe5c 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1207,6 +1207,16 @@ void Swap16(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(args[0]); } +void FastSwap16(Local receiver, + Local buffer_obj, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& options) { + HandleScope scope(options.isolate); + ArrayBufferViewContents buffer(buffer_obj); + CHECK(nbytes::SwapBytes16(const_cast(buffer.data()), buffer.length())); +} + +static CFunction fast_swap16(CFunction::Make(FastSwap16)); void Swap32(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -1216,6 +1226,16 @@ void Swap32(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(args[0]); } +void FastSwap32(Local receiver, + Local buffer_obj, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& options) { + HandleScope scope(options.isolate); + ArrayBufferViewContents buffer(buffer_obj); + CHECK(nbytes::SwapBytes32(const_cast(buffer.data()), buffer.length())); +} + +static CFunction fast_swap32(CFunction::Make(FastSwap32)); void Swap64(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -1225,6 +1245,17 @@ void Swap64(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(args[0]); } +void FastSwap64(Local receiver, + Local buffer_obj, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& options) { + HandleScope scope(options.isolate); + ArrayBufferViewContents buffer(buffer_obj); + CHECK(nbytes::SwapBytes64(const_cast(buffer.data()), buffer.length())); +} + +static CFunction fast_swap64(CFunction::Make(FastSwap64)); + static void IsUtf8(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK_EQ(args.Length(), 1); @@ -1622,9 +1653,9 @@ void Initialize(Local target, SetMethodNoSideEffect( context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer); - SetMethod(context, target, "swap16", Swap16); - SetMethod(context, target, "swap32", Swap32); - SetMethod(context, target, "swap64", Swap64); + SetFastMethod(context, target, "swap16", Swap16, &fast_swap16); + SetFastMethod(context, target, "swap32", Swap32, &fast_swap32); + SetFastMethod(context, target, "swap64", Swap64, &fast_swap64); SetMethodNoSideEffect(context, target, "isUtf8", IsUtf8); SetMethodNoSideEffect(context, target, "isAscii", IsAscii); @@ -1693,8 +1724,11 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(IndexOfString); registry->Register(Swap16); + registry->Register(fast_swap16); registry->Register(Swap32); + registry->Register(fast_swap32); registry->Register(Swap64); + registry->Register(fast_swap64); registry->Register(IsUtf8); registry->Register(IsAscii); diff --git a/test/parallel/test-buffer-from.js b/test/parallel/test-buffer-from.js index 6c08b973e6ab41..b8f4a5d2a2a67b 100644 --- a/test/parallel/test-buffer-from.js +++ b/test/parallel/test-buffer-from.js @@ -140,5 +140,44 @@ assert.throws(() => { }) ); +// copyBytesFrom: length exceeds view (should clamp, not throw) +{ + const u8 = new Uint8Array([1, 2, 3, 4, 5]); + const b = Buffer.copyBytesFrom(u8, 2, 100); + assert.strictEqual(b.length, 3); + assert.deepStrictEqual([...b], [3, 4, 5]); +} + +// copyBytesFrom: length 0 returns empty buffer +{ + const u8 = new Uint8Array([1, 2, 3]); + const b = Buffer.copyBytesFrom(u8, 1, 0); + assert.strictEqual(b.length, 0); +} + +// copyBytesFrom: offset past end returns empty buffer +{ + const u8 = new Uint8Array([1, 2, 3]); + const b = Buffer.copyBytesFrom(u8, 10); + assert.strictEqual(b.length, 0); +} + +// copyBytesFrom: Float64Array with offset and length +{ + const f64 = new Float64Array([1.0, 2.0, 3.0, 4.0]); + const b = Buffer.copyBytesFrom(f64, 1, 2); + assert.strictEqual(b.length, 16); + const view = new Float64Array(b.buffer, b.byteOffset, 2); + assert.strictEqual(view[0], 2.0); + assert.strictEqual(view[1], 3.0); +} + +// copyBytesFrom: empty typed array returns empty buffer +{ + const empty = new Uint8Array(0); + const b = Buffer.copyBytesFrom(empty); + assert.strictEqual(b.length, 0); +} + // Invalid encoding is allowed Buffer.from('asd', 1); diff --git a/test/parallel/test-buffer-tostring-range.js b/test/parallel/test-buffer-tostring-range.js index f4adf64c8d9129..00803bc35d7f4e 100644 --- a/test/parallel/test-buffer-tostring-range.js +++ b/test/parallel/test-buffer-tostring-range.js @@ -78,6 +78,46 @@ assert.strictEqual(rangeBuffer.toString('ascii', 0, '-1.99'), ''); assert.strictEqual(rangeBuffer.toString('ascii', 0, 1.99), 'a'); assert.strictEqual(rangeBuffer.toString('ascii', 0, true), 'a'); +// Test hex/base64/base64url partial range encoding (exercises V8 builtin path) +{ + const buf = Buffer.from([0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe]); + assert.strictEqual(buf.toString('hex'), 'deadbeefcafe'); + assert.strictEqual(buf.toString('hex', 0, 3), 'deadbe'); + assert.strictEqual(buf.toString('hex', 2, 5), 'beefca'); + assert.strictEqual(buf.toString('hex', 4), 'cafe'); + assert.strictEqual(buf.toString('hex', 6), ''); + assert.strictEqual(buf.toString('hex', 0, 0), ''); +} + +{ + const buf = Buffer.from('Hello, World!'); + assert.strictEqual(buf.toString('base64'), 'SGVsbG8sIFdvcmxkIQ=='); + assert.strictEqual(buf.toString('base64', 0, 5), 'SGVsbG8='); + assert.strictEqual(buf.toString('base64', 7), 'V29ybGQh'); + assert.strictEqual(buf.toString('base64', 0, 0), ''); +} + +{ + const buf = Buffer.from('Hello, World!'); + assert.strictEqual(buf.toString('base64url'), 'SGVsbG8sIFdvcmxkIQ'); + assert.strictEqual(buf.toString('base64url', 0, 5), 'SGVsbG8'); + assert.strictEqual(buf.toString('base64url', 7), 'V29ybGQh'); + assert.strictEqual(buf.toString('base64url', 0, 0), ''); +} + +// Test with pool-allocated buffer (has non-zero byteOffset) +{ + const poolBuf = Buffer.from('test data for hex encoding'); + assert.strictEqual( + poolBuf.toString('hex'), + Buffer.from('test data for hex encoding').toString('hex') + ); + assert.strictEqual( + poolBuf.toString('hex', 5, 9), + Buffer.from('data').toString('hex') + ); +} + // Try toString() with an object as an encoding assert.strictEqual(rangeBuffer.toString({ toString: function() { return 'ascii'; From 01ba74f3ed1d21b86f0bf104889cc350fb73d60a Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Wed, 18 Feb 2026 04:39:17 +0500 Subject: [PATCH 2/6] buffer: address PR feedback - Guard ensureUint8ArrayToHex against --no-js-base-64 flag by falling back to C++ hexSlice when toHex is unavailable - Remove THROW_AND_RETURN_UNLESS_BUFFER and return value from slow Swap16/32/64 to match fast path conventions (JS validates) - Add TRACK_V8_FAST_API_CALL to FastSwap16/32/64 - Add test/parallel/test-buffer-swap-fast.js for fast API verification --- lib/buffer.js | 12 ++++--- src/node_buffer.cc | 12 ++----- test/parallel/test-buffer-swap-fast.js | 43 ++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 test/parallel/test-buffer-swap-fast.js diff --git a/lib/buffer.js b/lib/buffer.js index 507d68b6664957..b99b3c2ba1258c 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -55,13 +55,13 @@ const { uncurryThis, } = primordials; -// V8 shipping feature (toHex) is installed by -// InitializeExperimentalGlobal(), which is skipped during snapshot creation -// TODO: Remove this once V8 shipping feature (toHex) is available. +// Lazily initialized: toHex is installed by InitializeExperimentalGlobal() +// (skipped during snapshot creation) and may be disabled with --no-js-base-64. let Uint8ArrayPrototypeToHex; function ensureUint8ArrayToHex() { if (Uint8ArrayPrototypeToHex === undefined) { - Uint8ArrayPrototypeToHex = uncurryThis(Uint8ArrayPrototype.toHex); + Uint8ArrayPrototypeToHex = Uint8ArrayPrototype.toHex ? + uncurryThis(Uint8ArrayPrototype.toHex) : null; } } @@ -684,6 +684,10 @@ function base64ByteLength(str, bytes) { function hexSliceToHex(buf, start, end) { ensureUint8ArrayToHex(); + // Fall back to C++ when toHex is unavailable or the result would exceed + // kStringMaxLength (so the correct ERR_STRING_TOO_LONG error is thrown). + if (Uint8ArrayPrototypeToHex === null || (end - start) * 2 > kStringMaxLength) + return hexSlice(buf, start, end); return Uint8ArrayPrototypeToHex( new Uint8Array(TypedArrayPrototypeGetBuffer(buf), TypedArrayPrototypeGetByteOffset(buf) + start, diff --git a/src/node_buffer.cc b/src/node_buffer.cc index bdac6a5a9dbe5c..e040f052bf9cdc 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1200,17 +1200,15 @@ int32_t FastIndexOfNumber(Local, static CFunction fast_index_of_number(CFunction::Make(FastIndexOfNumber)); void Swap16(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]); SPREAD_BUFFER_ARG(args[0], ts_obj); CHECK(nbytes::SwapBytes16(ts_obj_data, ts_obj_length)); - args.GetReturnValue().Set(args[0]); } void FastSwap16(Local receiver, Local buffer_obj, // NOLINTNEXTLINE(runtime/references) FastApiCallbackOptions& options) { + TRACK_V8_FAST_API_CALL("buffer.swap16"); HandleScope scope(options.isolate); ArrayBufferViewContents buffer(buffer_obj); CHECK(nbytes::SwapBytes16(const_cast(buffer.data()), buffer.length())); @@ -1219,17 +1217,15 @@ void FastSwap16(Local receiver, static CFunction fast_swap16(CFunction::Make(FastSwap16)); void Swap32(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]); SPREAD_BUFFER_ARG(args[0], ts_obj); CHECK(nbytes::SwapBytes32(ts_obj_data, ts_obj_length)); - args.GetReturnValue().Set(args[0]); } void FastSwap32(Local receiver, Local buffer_obj, // NOLINTNEXTLINE(runtime/references) FastApiCallbackOptions& options) { + TRACK_V8_FAST_API_CALL("buffer.swap32"); HandleScope scope(options.isolate); ArrayBufferViewContents buffer(buffer_obj); CHECK(nbytes::SwapBytes32(const_cast(buffer.data()), buffer.length())); @@ -1238,17 +1234,15 @@ void FastSwap32(Local receiver, static CFunction fast_swap32(CFunction::Make(FastSwap32)); void Swap64(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]); SPREAD_BUFFER_ARG(args[0], ts_obj); CHECK(nbytes::SwapBytes64(ts_obj_data, ts_obj_length)); - args.GetReturnValue().Set(args[0]); } void FastSwap64(Local receiver, Local buffer_obj, // NOLINTNEXTLINE(runtime/references) FastApiCallbackOptions& options) { + TRACK_V8_FAST_API_CALL("buffer.swap64"); HandleScope scope(options.isolate); ArrayBufferViewContents buffer(buffer_obj); CHECK(nbytes::SwapBytes64(const_cast(buffer.data()), buffer.length())); diff --git a/test/parallel/test-buffer-swap-fast.js b/test/parallel/test-buffer-swap-fast.js new file mode 100644 index 00000000000000..2b4c60567cb5ac --- /dev/null +++ b/test/parallel/test-buffer-swap-fast.js @@ -0,0 +1,43 @@ +// Flags: --expose-internals --no-warnings --allow-natives-syntax +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +function testFastSwap16() { + const buf = Buffer.alloc(256); + buf.swap16(); +} + +function testFastSwap32() { + const buf = Buffer.alloc(256); + buf.swap32(); +} + +function testFastSwap64() { + const buf = Buffer.alloc(256); + buf.swap64(); +} + +eval('%PrepareFunctionForOptimization(Buffer.prototype.swap16)'); +testFastSwap16(); +eval('%OptimizeFunctionOnNextCall(Buffer.prototype.swap16)'); +testFastSwap16(); + +eval('%PrepareFunctionForOptimization(Buffer.prototype.swap32)'); +testFastSwap32(); +eval('%OptimizeFunctionOnNextCall(Buffer.prototype.swap32)'); +testFastSwap32(); + +eval('%PrepareFunctionForOptimization(Buffer.prototype.swap64)'); +testFastSwap64(); +eval('%OptimizeFunctionOnNextCall(Buffer.prototype.swap64)'); +testFastSwap64(); + +if (common.isDebug) { + const { internalBinding } = require('internal/test/binding'); + const { getV8FastApiCallCount } = internalBinding('debug'); + assert.strictEqual(getV8FastApiCallCount('buffer.swap16'), 1); + assert.strictEqual(getV8FastApiCallCount('buffer.swap32'), 1); + assert.strictEqual(getV8FastApiCallCount('buffer.swap64'), 1); +} From c705a5d57043da9bdb479bad1fccdb8f2f77a515 Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Wed, 18 Feb 2026 13:07:31 +0500 Subject: [PATCH 3/6] buffer: revert toHex in favour of nbytes HexEncode update Remove V8 Uint8Array.prototype.toHex() path for Buffer.toString('hex') in favour of the upcoming nbytes HexEncode improvement (nodejs/nbytes#12) which is ~3x faster through the existing C++ hexSlice path. Refs: https://github.com/nodejs/nbytes/pull/12 --- lib/buffer.js | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/lib/buffer.js b/lib/buffer.js index b99b3c2ba1258c..5c7fa7d7f6b57c 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -51,20 +51,8 @@ const { TypedArrayPrototypeGetLength, TypedArrayPrototypeSet, Uint8Array, - Uint8ArrayPrototype, - uncurryThis, } = primordials; -// Lazily initialized: toHex is installed by InitializeExperimentalGlobal() -// (skipped during snapshot creation) and may be disabled with --no-js-base-64. -let Uint8ArrayPrototypeToHex; -function ensureUint8ArrayToHex() { - if (Uint8ArrayPrototypeToHex === undefined) { - Uint8ArrayPrototypeToHex = Uint8ArrayPrototype.toHex ? - uncurryThis(Uint8ArrayPrototype.toHex) : null; - } -} - const { byteLengthUtf8, compare: _compare, @@ -682,18 +670,6 @@ function base64ByteLength(str, bytes) { return (bytes * 3) >>> 2; } -function hexSliceToHex(buf, start, end) { - ensureUint8ArrayToHex(); - // Fall back to C++ when toHex is unavailable or the result would exceed - // kStringMaxLength (so the correct ERR_STRING_TOO_LONG error is thrown). - if (Uint8ArrayPrototypeToHex === null || (end - start) * 2 > kStringMaxLength) - return hexSlice(buf, start, end); - return Uint8ArrayPrototypeToHex( - new Uint8Array(TypedArrayPrototypeGetBuffer(buf), - TypedArrayPrototypeGetByteOffset(buf) + start, - end - start)); -} - const encodingOps = { utf8: { encoding: 'utf8', @@ -771,7 +747,7 @@ const encodingOps = { encodingVal: encodingsMap.hex, byteLength: (string) => string.length >>> 1, write: hexWrite, - slice: hexSliceToHex, + slice: hexSlice, indexOf: (buf, val, byteOffset, dir) => indexOfBuffer(buf, fromStringFast(val, encodingOps.hex), From 6b3cf031be1b93989500983b19caffc45ac14275 Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Wed, 18 Feb 2026 13:14:50 +0500 Subject: [PATCH 4/6] resolve feedback --- lib/buffer.js | 2 +- src/node_buffer.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/buffer.js b/lib/buffer.js index 5c7fa7d7f6b57c..0802c08fa9d71d 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -714,7 +714,7 @@ const encodingOps = { write: asciiWrite, slice: asciiSlice, indexOf: (buf, val, byteOffset, dir) => - indexOfString(buf, val, byteOffset, encodingsMap.latin1, dir), + indexOfString(buf, val, byteOffset, encodingsMap.ascii, dir), }, base64: { encoding: 'base64', diff --git a/src/node_buffer.cc b/src/node_buffer.cc index e040f052bf9cdc..d7fbd3116f1262 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1050,7 +1050,7 @@ void IndexOfString(const FunctionCallbackInfo& args) { needle_length, offset, is_forward); - } else if (enc == LATIN1) { + } else if (enc == ASCII || enc == LATIN1) { uint8_t* needle_data = node::UncheckedMalloc(needle_length); if (needle_data == nullptr) { return args.GetReturnValue().Set(-1); From f5c875bd570e847095828c5868eb87530d6a41aa Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Wed, 18 Feb 2026 13:18:54 +0500 Subject: [PATCH 5/6] resolve feedback on fast callbacks --- src/node_buffer.cc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/node_buffer.cc b/src/node_buffer.cc index d7fbd3116f1262..6e06a29bb46ace 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1210,8 +1210,8 @@ void FastSwap16(Local receiver, FastApiCallbackOptions& options) { TRACK_V8_FAST_API_CALL("buffer.swap16"); HandleScope scope(options.isolate); - ArrayBufferViewContents buffer(buffer_obj); - CHECK(nbytes::SwapBytes16(const_cast(buffer.data()), buffer.length())); + SPREAD_BUFFER_ARG(buffer_obj, ts_obj); + CHECK(nbytes::SwapBytes16(ts_obj_data, ts_obj_length)); } static CFunction fast_swap16(CFunction::Make(FastSwap16)); @@ -1227,8 +1227,8 @@ void FastSwap32(Local receiver, FastApiCallbackOptions& options) { TRACK_V8_FAST_API_CALL("buffer.swap32"); HandleScope scope(options.isolate); - ArrayBufferViewContents buffer(buffer_obj); - CHECK(nbytes::SwapBytes32(const_cast(buffer.data()), buffer.length())); + SPREAD_BUFFER_ARG(buffer_obj, ts_obj); + CHECK(nbytes::SwapBytes32(ts_obj_data, ts_obj_length)); } static CFunction fast_swap32(CFunction::Make(FastSwap32)); @@ -1244,8 +1244,8 @@ void FastSwap64(Local receiver, FastApiCallbackOptions& options) { TRACK_V8_FAST_API_CALL("buffer.swap64"); HandleScope scope(options.isolate); - ArrayBufferViewContents buffer(buffer_obj); - CHECK(nbytes::SwapBytes64(const_cast(buffer.data()), buffer.length())); + SPREAD_BUFFER_ARG(buffer_obj, ts_obj); + CHECK(nbytes::SwapBytes64(ts_obj_data, ts_obj_length)); } static CFunction fast_swap64(CFunction::Make(FastSwap64)); From 59f5d09deda15c202dcfd70b8a20857a6cda976f Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Thu, 19 Feb 2026 02:41:13 +0500 Subject: [PATCH 6/6] added comments and updated tests --- lib/buffer.js | 3 +++ test/parallel/test-buffer-swap-fast.js | 24 ++++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/buffer.js b/lib/buffer.js index 0802c08fa9d71d..be2bdd4334e93d 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -394,6 +394,7 @@ Buffer.copyBytesFrom = function copyBytesFrom(view, offset, length) { let end; if (length !== undefined) { validateInteger(length, 'length', 0); + // The old code used TypedArrayPrototypeSlice which clamps internally. end = MathMin(offset + length, viewLength); } else { end = viewLength; @@ -1096,6 +1097,8 @@ function _fill(buf, value, offset, end, encoding) { value = 0; } else if (value.length === 1) { // Fast path: If `value` fits into a single byte, use that numeric value. + // ASCII shares this branch with utf8 since code < 128 covers the full + // ASCII range; anything outside falls through to C++ bindingFill. if (normalizedEncoding === 'utf8' || normalizedEncoding === 'ascii') { const code = StringPrototypeCharCodeAt(value, 0); if (code < 128) { diff --git a/test/parallel/test-buffer-swap-fast.js b/test/parallel/test-buffer-swap-fast.js index 2b4c60567cb5ac..755edb6d598892 100644 --- a/test/parallel/test-buffer-swap-fast.js +++ b/test/parallel/test-buffer-swap-fast.js @@ -5,18 +5,30 @@ const common = require('../common'); const assert = require('assert'); function testFastSwap16() { - const buf = Buffer.alloc(256); - buf.swap16(); + const buf = Buffer.from([0x01, 0x02, 0x03, 0x04]); + const expected = Buffer.from([0x02, 0x01, 0x04, 0x03]); + const padded = Buffer.alloc(256); + buf.copy(padded); + padded.swap16(); + assert.deepStrictEqual(padded.subarray(0, 4), expected); } function testFastSwap32() { - const buf = Buffer.alloc(256); - buf.swap32(); + const buf = Buffer.from([0x01, 0x02, 0x03, 0x04]); + const expected = Buffer.from([0x04, 0x03, 0x02, 0x01]); + const padded = Buffer.alloc(256); + buf.copy(padded); + padded.swap32(); + assert.deepStrictEqual(padded.subarray(0, 4), expected); } function testFastSwap64() { - const buf = Buffer.alloc(256); - buf.swap64(); + const buf = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); + const expected = Buffer.from([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]); + const padded = Buffer.alloc(256); + buf.copy(padded); + padded.swap64(); + assert.deepStrictEqual(padded.subarray(0, 8), expected); } eval('%PrepareFunctionForOptimization(Buffer.prototype.swap16)');