LibJS: Account ArrayBuffer storage as external memory

Report owned ByteBuffer outline capacity from ArrayBuffer as external
memory. Update the heap accounting when backing storage is attached,
resized, detached, or transferred.

Borrowed WebAssembly memory remains uncounted because ArrayBuffer does
not own that storage.
This commit is contained in:
Andreas Kling 2026-05-05 23:57:11 +02:00 committed by Andreas Kling
parent 8fc7dfad26
commit f6a45ea9a6
3 changed files with 55 additions and 3 deletions

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGC/Heap.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/ArrayBuffer.h>
#include <LibJS/Runtime/ArrayBufferConstructor.h>
@ -32,7 +33,9 @@ ThrowCompletionOr<GC::Ref<ArrayBuffer>> ArrayBuffer::create(Realm& realm, size_t
GC::Ref<ArrayBuffer> ArrayBuffer::create(Realm& realm, ByteBuffer buffer, DataBlock::Shared is_shared)
{
return realm.create<ArrayBuffer>(move(buffer), is_shared, prototype_for_shared_state(realm, is_shared));
auto array_buffer = realm.create<ArrayBuffer>(move(buffer), is_shared, prototype_for_shared_state(realm, is_shared));
realm.vm().heap().did_allocate_external_memory(array_buffer->external_memory_size());
return array_buffer;
}
GC::Ref<ArrayBuffer> ArrayBuffer::create(Realm& realm, ByteBuffer* buffer, DataBlock::Shared is_shared)
@ -54,6 +57,28 @@ ArrayBuffer::ArrayBuffer(ByteBuffer* buffer, DataBlock::Shared is_shared, Object
{
}
void ArrayBuffer::account_external_memory_change(size_t old_external_memory_size, size_t new_external_memory_size)
{
if (new_external_memory_size > old_external_memory_size) {
heap().did_allocate_external_memory(new_external_memory_size - old_external_memory_size);
return;
}
heap().did_free_external_memory(old_external_memory_size - new_external_memory_size);
}
void ArrayBuffer::set_data_block(DataBlock block)
{
auto old_external_memory_size = external_memory_size();
m_data_block = move(block);
account_external_memory_change(old_external_memory_size, external_memory_size());
}
void ArrayBuffer::did_change_data_block_capacity(size_t old_external_memory_size)
{
account_external_memory_change(old_external_memory_size, external_memory_size());
}
void ArrayBuffer::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
@ -184,8 +209,10 @@ ThrowCompletionOr<ArrayBuffer*> allocate_array_buffer(VM& vm, FunctionObject& co
if (allocating_resizable_buffer) {
// a. If it is not possible to create a Data Block block consisting of maxByteLength bytes, throw a RangeError exception.
// b. NOTE: Resizable ArrayBuffers are designed to be implementable with in-place growth. Implementations may throw if, for example, virtual memory cannot be reserved up front.
auto old_external_memory_size = obj->external_memory_size();
if (auto result = obj->buffer().try_ensure_capacity(*max_byte_length); result.is_error())
return vm.throw_completion<RangeError>(ErrorType::NotEnoughMemoryToAllocate, *max_byte_length);
obj->did_change_data_block_capacity(old_external_memory_size);
// c. Set obj.[[ArrayBufferMaxByteLength]] to maxByteLength.
obj->set_max_byte_length(*max_byte_length);
@ -253,6 +280,7 @@ ThrowCompletionOr<ArrayBuffer*> array_buffer_copy_and_detach(VM& vm, ArrayBuffer
void ArrayBuffer::detach_buffer()
{
auto old_external_memory_size = external_memory_size();
for (auto& cached_view : m_cached_views) {
auto& view = static_cast<TypedArrayBase&>(cached_view);
if (view.viewed_array_buffer() == this)
@ -260,6 +288,7 @@ void ArrayBuffer::detach_buffer()
}
m_cached_views.clear();
m_data_block.byte_buffer = Empty {};
account_external_memory_change(old_external_memory_size, 0);
}
ThrowCompletionOr<ByteBuffer> ArrayBuffer::detach_and_take_bytes(VM& vm)
@ -269,11 +298,19 @@ ThrowCompletionOr<ByteBuffer> ArrayBuffer::detach_and_take_bytes(VM& vm)
if (!same_value(detach_key(), js_undefined()))
return vm.throw_completion<TypeError>(ErrorType::DetachKeyMismatch, js_undefined(), detach_key());
auto old_external_memory_size = external_memory_size();
auto bytes = m_data_block.byte_buffer.visit(
[](Empty) -> ByteBuffer { VERIFY_NOT_REACHED(); },
[](ByteBuffer& value) -> ByteBuffer { return move(value); },
[](DataBlock::UnownedFixedLengthByteBuffer& value) -> ByteBuffer { return MUST(ByteBuffer::copy(value.buffer->span())); });
detach_buffer();
for (auto& cached_view : m_cached_views) {
auto& view = static_cast<TypedArrayBase&>(cached_view);
if (view.viewed_array_buffer() == this)
view.set_cached_data_ptr(nullptr);
}
m_cached_views.clear();
m_data_block.byte_buffer = Empty {};
account_external_memory_change(old_external_memory_size, 0);
return bytes;
}

View file

@ -80,6 +80,14 @@ struct DataBlock {
[](UnownedFixedLengthByteBuffer const& value) { return value.size; });
}
size_t external_memory_size() const
{
return byte_buffer.visit(
[](Empty) -> size_t { return 0; },
[](ByteBuffer const& buffer) { return buffer.is_inline() ? 0 : buffer.capacity(); },
[](UnownedFixedLengthByteBuffer const&) -> size_t { return 0; });
}
Variant<Empty, ByteBuffer, UnownedFixedLengthByteBuffer> byte_buffer;
Shared is_shared = { Shared::No };
};
@ -96,6 +104,7 @@ public:
virtual ~ArrayBuffer() override = default;
size_t byte_length() const { return m_data_block.size(); }
virtual size_t external_memory_size() const override { return m_data_block.external_memory_size(); }
// [[ArrayBufferData]]
ByteBuffer& buffer() { return m_data_block.buffer(); }
@ -111,7 +120,8 @@ public:
void set_max_byte_length(size_t max_byte_length) { m_max_byte_length = max_byte_length; }
// Used by allocate_array_buffer() to attach the data block after construction
void set_data_block(DataBlock block) { m_data_block = move(block); }
void set_data_block(DataBlock);
void did_change_data_block_capacity(size_t old_external_memory_size);
Value detach_key() const { return m_detach_key; }
void set_detach_key(Value detach_key) { m_detach_key = detach_key; }
@ -180,6 +190,8 @@ private:
virtual void visit_edges(Visitor&) override;
void account_external_memory_change(size_t old_external_memory_size, size_t new_external_memory_size);
DataBlock m_data_block;
Optional<size_t> m_max_byte_length;
IntrusiveList<&CachedTypedArrayView::m_cached_view_list_node> m_cached_views;

View file

@ -14,6 +14,7 @@
#include <AK/StringBuilder.h>
#include <AK/Time.h>
#include <LibFileSystem/FileSystem.h>
#include <LibGC/Heap.h>
#include <LibJS/Bytecode/Executable.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Array.h>
@ -192,8 +193,10 @@ VM::VM(ErrorMessages error_messages)
// The default implementation of HostResizeArrayBuffer is to return NormalCompletion(unhandled).
auto old_external_memory_size = buffer.external_memory_size();
if (auto result = buffer.buffer().try_resize(new_byte_length, ByteBuffer::ZeroFillNewElements::Yes); result.is_error())
return throw_completion<RangeError>(ErrorType::NotEnoughMemoryToAllocate, new_byte_length);
buffer.did_change_data_block_capacity(old_external_memory_size);
return HandledByHost::Handled;
};