Skip to content

Commit 4fdf44b

Browse files
ronagclaude
andcommitted
deps: V8: add CopyArrayBufferViewBytes API
Add v8::ArrayBufferView::CopyArrayBufferViewBytes, which copies a byte range from one ArrayBufferView to another. Unlike the existing v8::ArrayBuffer::CopyArrayBufferBytes, it operates on the *views*: it resolves each view's data pointer directly (JSTypedArray::DataPtr / DataView::data_pointer) and reads the backing buffer's shared/immutable/ detached flags as plain field loads, without ever materializing or fetching the views' ArrayBuffers. This exists because materializing the ArrayBuffer is expensive. Profiling Buffer.prototype.copy (perf, AMD EPYC 9135, x86-64) showed that for small copies the dominant native cost is ArrayBufferView::Buffer() -> JSTypedArray::GetBuffer() -- ~25% of total runtime, paid on every call (not just first materialization) and incurred twice (source and target). Add ByteOffset() (~7%) and IsSharedArrayBuffer() (~6%) and roughly 38% of a small copy is spent turning two views into ArrayBuffers and querying them piecemeal, while the actual memmove is ~4%. Routing the node binding through CopyArrayBufferBytes forces all of that onto the embedder side; a view-level entry point folds it into a single call of cheap field reads. Byte ranges are clamped to both views' byte lengths. Nothing is copied when the source is detached/out-of-bounds or the target is detached/ out-of-bounds or backed by an immutable ArrayBuffer; the number of bytes actually copied is returned. When both views are backed by a SharedArrayBuffer a relaxed-atomic memmove is used, honoring the SharedArrayBuffer memory model; any other combination performs a plain memmove on the backing store (matching CopyArrayBufferBytes). Carried as a floating patch; v8_embedder_string is bumped to -node.22 accordingly. It is the natural sibling of the CopyArrayBufferBytes API added in the preceding floating patch and touches nothing but the public ArrayBuffer/ArrayBufferView API. Refs: #55422 Signed-off-by: Robert Nagy <ronagy@icloud.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4383f67 commit 4fdf44b

3 files changed

Lines changed: 92 additions & 1 deletion

File tree

common.gypi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141
# Reset this number to 0 on major V8 upgrades.
4242
# Increment by one for each non-official patch applied to deps/v8.
43-
'v8_embedder_string': '-node.21',
43+
'v8_embedder_string': '-node.22',
4444

4545
##### V8 defaults for Node.js #####
4646

deps/v8/include/v8-array-buffer.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,26 @@ class V8_EXPORT ArrayBufferView : public Object {
477477
*/
478478
bool HasBuffer() const;
479479

480+
/**
481+
* Copy up to |bytes_to_copy| bytes from |source| (starting |source_start|
482+
* bytes into the view) to |target| (starting |target_start| bytes into the
483+
* view). The byte range is clamped to both views' byte lengths. The views'
484+
* data pointers are resolved directly, without materializing their
485+
* ArrayBuffers, avoiding the overhead of ArrayBufferView::Buffer /
486+
* JSTypedArray::GetBuffer.
487+
*
488+
* Nothing is copied if |source| is detached or out of bounds, or if |target|
489+
* is detached, out of bounds, or backed by an immutable ArrayBuffer. When
490+
* both views are backed by a SharedArrayBuffer the copy uses a relaxed-atomic
491+
* memmove that honors the SharedArrayBuffer memory model. Returns the number
492+
* of bytes actually copied.
493+
*/
494+
static size_t CopyArrayBufferViewBytes(Local<ArrayBufferView> source,
495+
size_t source_start,
496+
Local<ArrayBufferView> target,
497+
size_t target_start,
498+
size_t bytes_to_copy);
499+
480500
V8_INLINE static ArrayBufferView* Cast(Value* value) {
481501
#ifdef V8_ENABLE_CHECKS
482502
CheckCast(value);

deps/v8/src/api/api.cc

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4246,6 +4246,43 @@ static size_t CopyArrayBufferBytesImpl(const void* source_buffer,
42464246
return bytes_to_copy;
42474247
}
42484248

4249+
struct ArrayBufferViewBytes {
4250+
char* data;
4251+
size_t length;
4252+
bool is_shared;
4253+
bool is_immutable;
4254+
};
4255+
4256+
// Resolves a view's data pointer, byte length and backing-buffer flags without
4257+
// materializing its ArrayBuffer (i.e. without JSTypedArray::GetBuffer, which is
4258+
// comparatively expensive). A detached or out-of-bounds view resolves to an
4259+
// empty {nullptr, 0} range.
4260+
ArrayBufferViewBytes GetArrayBufferViewBytes(
4261+
i::Tagged<i::JSArrayBufferView> view) {
4262+
if (view->IsDetachedOrOutOfBounds()) return {nullptr, 0, false, false};
4263+
if (i::IsJSTypedArray(view)) {
4264+
i::Tagged<i::JSTypedArray> array = i::Cast<i::JSTypedArray>(view);
4265+
i::Tagged<i::JSArrayBuffer> buffer = array->buffer();
4266+
return {reinterpret_cast<char*>(array->DataPtr()), array->GetByteLength(),
4267+
buffer->is_shared(), buffer->is_immutable()};
4268+
}
4269+
if (i::IsJSDataView(view)) {
4270+
i::Tagged<i::JSDataView> data_view = i::Cast<i::JSDataView>(view);
4271+
i::Tagged<i::JSArrayBuffer> buffer =
4272+
i::Cast<i::JSArrayBuffer>(data_view->buffer());
4273+
return {reinterpret_cast<char*>(data_view->data_pointer()),
4274+
data_view->byte_length(), buffer->is_shared(),
4275+
buffer->is_immutable()};
4276+
}
4277+
i::Tagged<i::JSRabGsabDataView> data_view =
4278+
i::Cast<i::JSRabGsabDataView>(view);
4279+
i::Tagged<i::JSArrayBuffer> buffer =
4280+
i::Cast<i::JSArrayBuffer>(data_view->buffer());
4281+
return {reinterpret_cast<char*>(data_view->data_pointer()),
4282+
data_view->GetByteLength(), buffer->is_shared(),
4283+
buffer->is_immutable()};
4284+
}
4285+
42494286
size_t v8::SharedArrayBuffer::CopyArrayBufferBytes(
42504287
size_t source_start, size_t bytes_to_copy, Local<SharedArrayBuffer> target,
42514288
size_t target_start) const {
@@ -8960,6 +8997,40 @@ size_t v8::ArrayBuffer::CopyArrayBufferBytes(size_t source_start,
89608997
that->GetByteLength(), bytes_to_copy);
89618998
}
89628999

9000+
size_t v8::ArrayBufferView::CopyArrayBufferViewBytes(
9001+
Local<ArrayBufferView> source, size_t source_start,
9002+
Local<ArrayBufferView> target, size_t target_start, size_t bytes_to_copy) {
9003+
i::DisallowGarbageCollection no_gc;
9004+
ArrayBufferViewBytes src =
9005+
GetArrayBufferViewBytes(*Utils::OpenDirectHandle(*source));
9006+
ArrayBufferViewBytes dst =
9007+
GetArrayBufferViewBytes(*Utils::OpenDirectHandle(*target));
9008+
9009+
// Never write to an immutable target. Detached/out-of-bounds views resolve to
9010+
// a zero length, so they fall out through the clamping below.
9011+
if (dst.is_immutable) return 0;
9012+
9013+
source_start = std::min(source_start, src.length);
9014+
target_start = std::min(target_start, dst.length);
9015+
bytes_to_copy = std::min(
9016+
{bytes_to_copy, src.length - source_start, dst.length - target_start});
9017+
if (bytes_to_copy == 0) return 0;
9018+
9019+
char* source_data = src.data + source_start;
9020+
char* target_data = dst.data + target_start;
9021+
// A relaxed-atomic memmove is only required when both views are backed by a
9022+
// SharedArrayBuffer; any other combination performs a plain memmove on the
9023+
// backing store, matching v8::ArrayBuffer::CopyArrayBufferBytes.
9024+
if (src.is_shared && dst.is_shared) {
9025+
base::Relaxed_Memmove(
9026+
reinterpret_cast<base::Atomic8*>(target_data),
9027+
reinterpret_cast<const base::Atomic8*>(source_data), bytes_to_copy);
9028+
} else {
9029+
std::memmove(target_data, source_data, bytes_to_copy);
9030+
}
9031+
return bytes_to_copy;
9032+
}
9033+
89639034
namespace {
89649035
std::shared_ptr<i::BackingStore> ToInternal(
89659036
std::shared_ptr<i::BackingStoreBase> backing_store) {

0 commit comments

Comments
 (0)