Skip to content
2 changes: 1 addition & 1 deletion benchmark/buffers/buffer-bytelength-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
});
Expand Down
31 changes: 31 additions & 0 deletions benchmark/buffers/buffer-copy-bytes-from.js
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions benchmark/buffers/buffer-fill.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down
2 changes: 1 addition & 1 deletion benchmark/buffers/buffer-indexof.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const searchStrings = [

const bench = common.createBenchmark(main, {
search: searchStrings,
encoding: ['undefined', 'utf8', 'ucs2', 'latin1'],
encoding: ['undefined', 'utf8', 'ascii', 'latin1', 'ucs2'],
type: ['buffer', 'string'],
n: [5e4],
}, {
Expand Down
2 changes: 1 addition & 1 deletion benchmark/buffers/buffer-tostring.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
121 changes: 67 additions & 54 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ const {
TypedArrayPrototypeGetByteOffset,
TypedArrayPrototypeGetLength,
TypedArrayPrototypeSet,
TypedArrayPrototypeSlice,
TypedArrayPrototypeSubarray,
Uint8Array,
} = primordials;
Expand Down Expand Up @@ -383,28 +382,33 @@ Buffer.copyBytesFrom = function copyBytesFrom(view, offset, length) {
return new FastBuffer();
}

if (offset !== undefined || length !== undefined) {
if (offset !== undefined) {
validateInteger(offset, 'offset', 0);
if (offset >= viewLength) return new FastBuffer();
} else {
offset = 0;
}
let end;
if (length !== undefined) {
validateInteger(length, 'length', 0);
end = offset + length;
} else {
end = viewLength;
}
let start = 0;
let end = viewLength;

view = TypedArrayPrototypeSlice(view, offset, end);
if (offset !== undefined) {
validateInteger(offset, 'offset', 0);
if (offset >= viewLength) return new FastBuffer();
start = offset;
}

if (length !== undefined) {
validateInteger(length, 'length', 0);
// The old code used TypedArrayPrototypeSlice which clamps internally.
end = MathMin(start + length, viewLength);
}

if (end <= start) return new FastBuffer();

const viewByteLength = TypedArrayPrototypeGetByteLength(view);
const elementSize = viewByteLength / viewLength;
const srcByteOffset = TypedArrayPrototypeGetByteOffset(view) +
start * elementSize;
const srcByteLength = (end - start) * elementSize;

return fromArrayLike(new Uint8Array(
TypedArrayPrototypeGetBuffer(view),
TypedArrayPrototypeGetByteOffset(view),
TypedArrayPrototypeGetByteLength(view)));
srcByteOffset,
srcByteLength));
};

// Identical to the built-in %TypedArray%.of(), but avoids using the deprecated
Expand Down Expand Up @@ -551,14 +555,15 @@ function fromArrayBuffer(obj, byteOffset, length) {
}

function fromArrayLike(obj) {
if (obj.length <= 0)
const { length } = obj;
if (length <= 0)
return new FastBuffer();
if (obj.length < (Buffer.poolSize >>> 1)) {
if (obj.length > (poolSize - poolOffset))
if (length < (Buffer.poolSize >>> 1)) {
if (length > (poolSize - poolOffset))
createPool();
const b = new FastBuffer(allocPool, poolOffset, obj.length);
const b = new FastBuffer(allocPool, poolOffset, length);
TypedArrayPrototypeSet(b, obj, 0);
poolOffset += obj.length;
poolOffset += length;
alignPool();
return b;
}
Expand Down Expand Up @@ -732,11 +737,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.ascii, dir),
},
base64: {
encoding: 'base64',
Expand Down Expand Up @@ -897,17 +898,17 @@ Buffer.prototype.toString = function toString(encoding, start, end) {
return utf8Slice(this, 0, this.length);
}

const len = this.length;
const bufferLength = TypedArrayPrototypeGetLength(this);

if (start <= 0)
start = 0;
else if (start >= len)
else if (start >= bufferLength)
return '';
else
start = MathTrunc(start) || 0;

if (end === undefined || end > len)
end = len;
if (end === undefined || end > bufferLength)
end = bufferLength;
else
end = MathTrunc(end) || 0;

Expand Down Expand Up @@ -1118,7 +1119,9 @@ 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') {
// 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') {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, ascii behaves exactly like latin1
Unsure if by design or accidentally

Copy link
Copy Markdown
Contributor Author

@thisalihassan thisalihassan Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this is safe by design I am just extending the existing single byte numeric optimization to cover ASCII, since the guard already constrains it to the valid ASCII range.

const code = StringPrototypeCharCodeAt(value, 0);
if (code < 128) {
value = code;
Expand Down Expand Up @@ -1168,29 +1171,30 @@ function _fill(buf, value, offset, end, encoding) {
}

Buffer.prototype.write = function write(string, offset, length, encoding) {
const bufferLength = TypedArrayPrototypeGetLength(this);
// Buffer#write(string);
if (offset === undefined) {
return utf8Write(this, string, 0, this.length);
return utf8Write(this, string, 0, bufferLength);
}
// Buffer#write(string, encoding)
if (length === undefined && typeof offset === 'string') {
encoding = offset;
length = this.length;
length = bufferLength;
offset = 0;

// Buffer#write(string, offset[, length][, encoding])
} else {
validateOffset(offset, 'offset', 0, this.length);
validateOffset(offset, 'offset', 0, bufferLength);

const remaining = this.length - offset;
const remaining = bufferLength - offset;

if (length === undefined) {
length = remaining;
} else if (typeof length === 'string') {
encoding = length;
length = remaining;
} else {
validateOffset(length, 'length', 0, this.length);
validateOffset(length, 'length', 0, bufferLength);
if (length > remaining)
length = remaining;
}
Expand All @@ -1208,9 +1212,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 bufferLength = TypedArrayPrototypeGetLength(this);
if (bufferLength > 0) {
const data = new Array(bufferLength);
for (let i = 0; i < bufferLength; ++i)
data[i] = this[i];
return { type: 'Buffer', data };
}
Expand All @@ -1235,7 +1240,7 @@ function adjustOffset(offset, length) {
}

Buffer.prototype.subarray = function subarray(start, end) {
const srcLength = this.length;
const srcLength = TypedArrayPrototypeGetLength(this);
start = adjustOffset(start, srcLength);
end = end !== undefined ? adjustOffset(end, srcLength) : srcLength;
const newLength = end > start ? end - start : 0;
Expand All @@ -1253,45 +1258,52 @@ function swap(b, n, m) {
}

Buffer.prototype.swap16 = function swap16() {
// For Buffer.length < 128, it's generally faster to
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
// For Buffer.length <= 32, it's generally faster to
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a reference link to this pull-request above this line

// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
const len = TypedArrayPrototypeGetLength(this);
if (len % 2 !== 0)
throw new ERR_INVALID_BUFFER_SIZE('16-bits');
if (len < 128) {
if (len <= 32) {
for (let i = 0; i < len; i += 2)
swap(this, i, i + 1);
return this;
}
return _swap16(this);
_swap16(this);
Copy link
Copy Markdown
Member

@ChALkeR ChALkeR Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what would happen if it's called on not a TypeArrayView now?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. Buffer.prototype.swap16.apply(new Array(128))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I will add a JS side guard

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would that be good approach @ChALkeR ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It changes the method to not being callable on a typedarray that is not an uint8array, but the previous behavior was inconsistent in that case anyway as the js part swapped elements and the src part swapped bytes

So I think explicitly blocking non-uint8arr should be fine and not a semver-major?
(perhaps cc @nodejs/lts just in case)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right about that, I was pointing about the inconsistency about the different paths. as JS swapped the elements but alright I will work on the feedback
Thanks

Copy link
Copy Markdown
Contributor

@aduh95 aduh95 Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not against changing the current state of things, but IMO that's a separate concern and should be discussed separately. Currently Buffer.prototype.swap16.call(new Float32Array(128)) does not throw, so if we want to make it throw let's open a separate semver-major PR.
My opinion is we should rely on TypedArrayPrototypeGetByteLength doing the identity check for us in this PR, better for performance and not worse than the status quo for DX/UX.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's pretty valid @aduh95, I have used TypedArrayPrototypeGetByteLength I will open a seperate issue on this.

Copy link
Copy Markdown
Member

@ChALkeR ChALkeR Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently Buffer.prototype.swap16.call(new Float32Array(128)) does not throw,

It does not throw but returns garbage (as in, result is not predictable / does not follow any logic)
So anything can be done with it I think

Making it swap bytes is an option. Making it throw is another option.

The current behavior is length-dependent:

  • 129 (516 bytes) throws and complains about non-even number of bytes
  • 128 (512 bytes) does not throw (as you pointed out) and swaps bytes
  • 127 (508 bytes) throws and complains about non-even number of bytes
  • 126 (504 bytes) does not throw and swaps elements (groups of 4 bytes)

That is a mere oversight and not a proper api, and, more significantly, no one should be using that realistically as it doesn't work.
I don't think that making that a hard error is a semver-major.

Copy link
Copy Markdown
Contributor

@aduh95 aduh95 Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing a non-Buffer this is not documented, so it implicitly is undefined behavior.
Garbage out is IMO a good tradeoff for a more performant happy path, although I didn't run any benchmark.

no one should be using that realistically as it doesn't work.

100% agreed – and I would bet that absolutely no one is passing Float32Arrays to that API. Still, I don't think it's worth changing in this PR, which is about improving performance.

return this;
};

Buffer.prototype.swap32 = function swap32() {
// For Buffer.length < 192, it's generally faster to
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
// For Buffer.length <= 32, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
const len = TypedArrayPrototypeGetLength(this);
if (len % 4 !== 0)
throw new ERR_INVALID_BUFFER_SIZE('32-bits');
if (len < 192) {
if (len <= 32) {
for (let i = 0; i < len; i += 4) {
swap(this, i, i + 3);
swap(this, i + 1, i + 2);
}
return this;
}
return _swap32(this);
_swap32(this);
return this;
};

Buffer.prototype.swap64 = function swap64() {
// For Buffer.length < 192, it's generally faster to
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
// For Buffer.length < 48, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
// Threshold differs from swap16/swap32 (<=32) because swap64's
// crossover is between 40 and 48 (native wins at 48, loses at 40).
const len = TypedArrayPrototypeGetLength(this);
if (len % 8 !== 0)
throw new ERR_INVALID_BUFFER_SIZE('64-bits');
if (len < 192) {
if (len < 48) {
for (let i = 0; i < len; i += 8) {
swap(this, i, i + 7);
swap(this, i + 1, i + 6);
Expand All @@ -1300,7 +1312,8 @@ Buffer.prototype.swap64 = function swap64() {
}
return this;
}
return _swap64(this);
_swap64(this);
return this;
};

Buffer.prototype.toLocaleString = Buffer.prototype.toString;
Expand Down
Loading
Loading