node/test/parallel/test-buffer-copy.js
Robert Nagy 4383f67279 buffer: optimize Buffer.prototype.copy
Route the native backing of Buffer.prototype.copy (CopyImpl, the `_copy`
binding) through V8's new v8::ArrayBuffer::CopyArrayBufferBytes API
(added in the preceding commit) instead of materializing an
ArrayBufferViewContents and doing a manual memmove. This speeds up
partial copies (sourceStart > 0). All copies now go through this
binding: the previous %TypedArray%.prototype.set fast-path for
whole-buffer copies is dropped, since it would throw on a detached or
immutable target rather than report a 0-byte no-op, and the benchmarks
below show the native path is comparable for that case.

When both sides are backed by a SharedArrayBuffer the relaxed-atomic
overload is used, which honors the SharedArrayBuffer memory model.
Mixed copies (one SharedArrayBuffer, one ArrayBuffer) use the regular
overload.

copy() now reports the number of bytes actually copied, as returned by
V8: 0 when the target is backed by a detached or immutable ArrayBuffer.
The copy is then a no-op rather than a write to read-only memory.

The native binding now plumbs byte offsets and the copied-byte count
through as size_t, passed across the fast/slow API boundary as doubles
(exact for integer values below 2^53), so copies that cross the 4 GiB
boundary are no longer truncated to 32 bits.

buffer-copy.js vs node v26.3.0 (x64, 30 runs):
  partial=false bytes=1024:  +7.91%  (***)
  partial=false bytes=128:   +0.17%
  partial=false bytes=8:     -0.89%
  partial=true  bytes=1024: +22.33%  (***)
  partial=true  bytes=128:  +19.58%  (***)
  partial=true  bytes=8:    +18.70%  (***)

This supersedes the prototype in
https://github.com/nodejs/node/pull/62491, which added a bespoke
ArrayBufferView::FastCopy instead of using the upstream-friendly
CopyArrayBufferBytes API.

Adds SharedArrayBuffer and immutable-ArrayBuffer coverage to the buffer
copy tests, plus a pummel regression test for copies larger than 2^32
bytes.

Refs: https://github.com/nodejs/node/issues/55422
Signed-off-by: Robert Nagy <ronagy@icloud.com>
Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR-URL: https://github.com/nodejs/node/pull/63828
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
2026-06-12 23:26:19 +00:00

304 lines
8 KiB
JavaScript

'use strict';
require('../common');
const assert = require('assert');
const b = Buffer.allocUnsafe(1024);
const c = Buffer.allocUnsafe(512);
let cntr = 0;
{
// copy 512 bytes, from 0 to 512.
b.fill(++cntr);
c.fill(++cntr);
const copied = b.copy(c, 0, 0, 512);
assert.strictEqual(copied, 512);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(c[i], b[i]);
}
}
{
// Current behavior is to coerce values to integers.
b.fill(++cntr);
c.fill(++cntr);
const copied = b.copy(c, '0', '0', '512');
assert.strictEqual(copied, 512);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(c[i], b[i]);
}
}
{
// Floats will be converted to integers via `Math.floor`
b.fill(++cntr);
c.fill(++cntr);
const copied = b.copy(c, 0, 0, 512.5);
assert.strictEqual(copied, 512);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(c[i], b[i]);
}
}
{
// Copy c into b, without specifying sourceEnd
b.fill(++cntr);
c.fill(++cntr);
const copied = c.copy(b, 0, 0);
assert.strictEqual(copied, c.length);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(b[i], c[i]);
}
}
{
// Copy c into b, without specifying sourceStart
b.fill(++cntr);
c.fill(++cntr);
const copied = c.copy(b, 0);
assert.strictEqual(copied, c.length);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(b[i], c[i]);
}
}
{
// Copied source range greater than source length
b.fill(++cntr);
c.fill(++cntr);
const copied = c.copy(b, 0, 0, c.length + 1);
assert.strictEqual(copied, c.length);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(b[i], c[i]);
}
}
{
// Copy longer buffer b to shorter c without targetStart
b.fill(++cntr);
c.fill(++cntr);
const copied = b.copy(c);
assert.strictEqual(copied, c.length);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(c[i], b[i]);
}
}
{
// Copy starting near end of b to c
b.fill(++cntr);
c.fill(++cntr);
const copied = b.copy(c, 0, b.length - Math.floor(c.length / 2));
assert.strictEqual(copied, Math.floor(c.length / 2));
for (let i = 0; i < Math.floor(c.length / 2); i++) {
assert.strictEqual(c[i], b[b.length - Math.floor(c.length / 2) + i]);
}
for (let i = Math.floor(c.length / 2) + 1; i < c.length; i++) {
assert.strictEqual(c[c.length - 1], c[i]);
}
}
{
// Try to copy 513 bytes, and check we don't overrun c
b.fill(++cntr);
c.fill(++cntr);
const copied = b.copy(c, 0, 0, 513);
assert.strictEqual(copied, c.length);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(c[i], b[i]);
}
}
{
// copy 768 bytes from b into b
b.fill(++cntr);
b.fill(++cntr, 256);
const copied = b.copy(b, 0, 256, 1024);
assert.strictEqual(copied, 768);
for (let i = 0; i < b.length; i++) {
assert.strictEqual(b[i], cntr);
}
}
// Copy string longer than buffer length (failure will segfault)
const bb = Buffer.allocUnsafe(10);
bb.fill('hello crazy world');
// Try to copy from before the beginning of b. Should not throw.
b.copy(c, 0, 100, 10);
// Throw with invalid source type
assert.throws(
() => Buffer.prototype.copy.call(0),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
}
);
// Copy throws at negative targetStart
assert.throws(
() => Buffer.allocUnsafe(5).copy(Buffer.allocUnsafe(5), -1, 0),
{
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
message: 'The value of "targetStart" is out of range. ' +
'It must be >= 0. Received -1'
}
);
// Copy throws at negative sourceStart
assert.throws(
() => Buffer.allocUnsafe(5).copy(Buffer.allocUnsafe(5), 0, -1),
{
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
}
);
// Copy throws if sourceStart is greater than length of source
assert.throws(
() => Buffer.allocUnsafe(5).copy(Buffer.allocUnsafe(5), 0, 100),
{
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
}
);
{
// Check sourceEnd resets to targetEnd if former is greater than the latter
b.fill(++cntr);
c.fill(++cntr);
b.copy(c, 0, 0, 1025);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(c[i], b[i]);
}
}
// Throw with negative sourceEnd
assert.throws(
() => b.copy(c, 0, 0, -1),
{
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
message: 'The value of "sourceEnd" is out of range. ' +
'It must be >= 0. Received -1'
}
);
// When sourceStart is greater than sourceEnd, zero copied
assert.strictEqual(b.copy(c, 0, 100, 10), 0);
// When targetStart > targetLength, zero copied
assert.strictEqual(b.copy(c, 512, 0, 10), 0);
// Test that the `target` can be a Uint8Array.
{
const d = new Uint8Array(c);
// copy 512 bytes, from 0 to 512.
b.fill(++cntr);
d.fill(++cntr);
const copied = b.copy(d, 0, 0, 512);
assert.strictEqual(copied, 512);
for (let i = 0; i < d.length; i++) {
assert.strictEqual(d[i], b[i]);
}
}
// Test that the source can be a Uint8Array, too.
{
const e = new Uint8Array(b);
// copy 512 bytes, from 0 to 512.
e.fill(++cntr);
c.fill(++cntr);
const copied = Buffer.prototype.copy.call(e, c, 0, 0, 512);
assert.strictEqual(copied, 512);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(c[i], e[i]);
}
}
// https://github.com/nodejs/node/issues/23668: Do not crash for invalid input.
c.fill('c');
b.copy(c, 'not a valid offset');
// Make sure this acted like a regular copy with `0` offset.
assert.deepStrictEqual(c, b.slice(0, c.length));
// Copy into a Uint16Array target; bytes are packed into 16-bit elements.
{
const x = new Uint16Array(4);
const buf = Buffer.of(1, 2, 3, 4);
const copied = buf.copy(x);
assert.strictEqual(copied, 4);
assert.ok(x instanceof Uint16Array);
const bytes = new Uint8Array(x.buffer, x.byteOffset, 4);
assert.deepStrictEqual(Array.from(bytes), [1, 2, 3, 4]);
const remaining = new Uint8Array(
x.buffer,
x.byteOffset + 4,
x.byteLength - 4
);
assert.ok(remaining.every((b) => b === 0));
}
{
c.fill('C');
assert.throws(() => {
b.copy(c, { [Symbol.toPrimitive]() { throw new Error('foo'); } });
}, /foo/);
// No copying took place:
assert.deepStrictEqual(c.toString(), 'C'.repeat(c.length));
}
// Copying to/from SharedArrayBuffer-backed buffers. The relaxed-atomic copy
// path is used only when both sides are backed by a SharedArrayBuffer; mixed
// copies (one shared, one not) go through the regular non-atomic overload.
{
// SharedArrayBuffer -> SharedArrayBuffer.
const src = Buffer.from(new SharedArrayBuffer(512)).fill(0x61);
const dst = Buffer.from(new SharedArrayBuffer(512)).fill(0x62);
const copied = src.copy(dst, 0, 0, 512);
assert.strictEqual(copied, 512);
assert.deepStrictEqual(Buffer.from(dst), Buffer.from(src));
}
{
// SharedArrayBuffer source -> regular Buffer target (mixed).
const src = Buffer.from(new SharedArrayBuffer(256)).fill(0x63);
const dst = Buffer.allocUnsafe(256).fill(0x64);
assert.strictEqual(src.copy(dst), 256);
assert.deepStrictEqual(dst, Buffer.from(src));
}
{
// Regular Buffer source -> SharedArrayBuffer target (mixed).
const src = Buffer.allocUnsafe(256).fill(0x65);
const dst = Buffer.from(new SharedArrayBuffer(256)).fill(0x66);
assert.strictEqual(src.copy(dst), 256);
assert.deepStrictEqual(Buffer.from(dst), src);
}
{
// Views with a non-zero byteOffset over a SharedArrayBuffer. The native copy
// is relative to the underlying ArrayBuffer, so the view's byteOffset must be
// accounted for.
const sab = new SharedArrayBuffer(16);
const whole = Buffer.from(sab);
for (let i = 0; i < 16; i++) whole[i] = i;
const src = Buffer.from(sab, 4, 8); // sab bytes 4..11
const dst = Buffer.from(sab, 12, 4); // sab bytes 12..15
assert.strictEqual(src.copy(dst, 0, 0, 4), 4);
assert.deepStrictEqual(
[...whole],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 4, 5, 6, 7]);
}
{
// Overlapping copy within a single SharedArrayBuffer uses memmove semantics.
const buf = Buffer.from(new SharedArrayBuffer(8));
for (let i = 0; i < 8; i++) buf[i] = i + 1; // [1,2,3,4,5,6,7,8]
assert.strictEqual(buf.copy(buf, 2, 0, 6), 6);
assert.deepStrictEqual([...buf], [1, 2, 1, 2, 3, 4, 5, 6]);
}