Files
ladybird/AK/MemoryStream.cpp
Andreas Kling a6b9548b93 AK: Switch AllocatingMemoryStream to a singly-linked chunk list
The old implementation stored chunks in a Vector, which meant every
discard() had to call Vector::remove(0, N) to drop the consumed chunks
from the front, shifting every remaining chunk down. For a stream used
as a back-pressure queue, draining it by discarding one chunk at a time
was quadratic in the queued chunk count: in RequestServer that cost
about a second of CPU per large response.

Replace it with a singly-linked list of chunks (head, tail, head read
offset, tail write offset) so push-back and pop-front are both O(1)
and no shifting ever happens. Each chunk now holds its CHUNK_SIZE byte
array inline rather than a separately-allocated ByteBuffer, which also
halves the per-chunk allocations. Teardown unlinks iteratively to avoid
recursive OwnPtr destructors on very long chains.
2026-04-22 13:32:07 +02:00

283 lines
7.6 KiB
C++

/*
* Copyright (c) 2021, kleines Filmröllchen <filmroellchen@serenityos.org>.
* Copyright (c) 2022, Tim Schumacher <timschumi@gmx.de>.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/FixedArray.h>
#include <AK/MemMem.h>
#include <AK/MemoryStream.h>
namespace AK {
FixedMemoryStream::FixedMemoryStream(Bytes bytes, Mode mode)
: m_bytes(bytes)
, m_writing_enabled(mode == Mode::ReadWrite)
{
}
FixedMemoryStream::FixedMemoryStream(ReadonlyBytes bytes)
: m_bytes({ const_cast<u8*>(bytes.data()), bytes.size() })
, m_writing_enabled(false)
{
}
bool FixedMemoryStream::is_eof() const
{
return m_offset >= m_bytes.size();
}
bool FixedMemoryStream::is_open() const
{
return true;
}
void FixedMemoryStream::close()
{
// FIXME: It doesn't make sense to close a memory stream. Therefore, we don't do anything here. Is that fine?
}
ErrorOr<void> FixedMemoryStream::truncate(size_t)
{
return Error::from_errno(EBADF);
}
ErrorOr<void> FixedMemoryStream::read_until_filled(AK::Bytes bytes)
{
if (remaining() < bytes.size())
return Error::from_string_literal("Can't read past the end of the stream memory");
m_bytes.slice(m_offset).copy_trimmed_to(bytes);
m_offset += bytes.size();
return {};
}
ErrorOr<size_t> FixedMemoryStream::seek(i64 offset, SeekMode seek_mode)
{
switch (seek_mode) {
case SeekMode::SetPosition:
if (offset > static_cast<i64>(m_bytes.size()))
return Error::from_string_literal("Offset past the end of the stream memory");
m_offset = offset;
break;
case SeekMode::FromCurrentPosition:
if (offset + static_cast<i64>(m_offset) > static_cast<i64>(m_bytes.size()))
return Error::from_string_literal("Offset past the end of the stream memory");
m_offset += offset;
break;
case SeekMode::FromEndPosition:
if (-offset > static_cast<i64>(m_bytes.size()))
return Error::from_string_literal("Offset past the start of the stream memory");
m_offset = m_bytes.size() + offset;
break;
}
return m_offset;
}
ErrorOr<size_t> FixedMemoryStream::write_some(ReadonlyBytes bytes)
{
// MemoryStream isn't based on file-descriptors, but since most other
// Stream implementations are, the interface specifies EBADF as the
// "we don't support this particular operation" error code.
if (!m_writing_enabled)
return Error::from_errno(EBADF);
// FIXME: Can this not error?
auto const nwritten = bytes.copy_trimmed_to(m_bytes.slice(m_offset));
m_offset += nwritten;
return nwritten;
}
ErrorOr<void> FixedMemoryStream::write_until_depleted(ReadonlyBytes bytes)
{
if (remaining() < bytes.size())
return Error::from_string_literal("Write of entire buffer ends past the memory area");
TRY(write_some(bytes));
return {};
}
size_t FixedMemoryStream::offset() const
{
return m_offset;
}
size_t FixedMemoryStream::remaining() const
{
return m_bytes.size() - m_offset;
}
AllocatingMemoryStream::~AllocatingMemoryStream()
{
// Iterative teardown to avoid blowing the stack on long chunk chains.
while (m_head)
m_head = move(m_head->next);
}
size_t AllocatingMemoryStream::used_buffer_size() const
{
return m_used_buffer_size;
}
bool AllocatingMemoryStream::is_eof() const
{
return m_used_buffer_size == 0;
}
bool AllocatingMemoryStream::is_open() const
{
return true;
}
void AllocatingMemoryStream::close()
{
}
ErrorOr<void> AllocatingMemoryStream::append_new_chunk()
{
auto new_chunk = adopt_own_if_nonnull(new (nothrow) Chunk);
if (!new_chunk)
return Error::from_errno(ENOMEM);
if (m_tail) {
m_tail->next = new_chunk.release_nonnull();
m_tail = m_tail->next.ptr();
} else {
m_head = new_chunk.release_nonnull();
m_tail = m_head.ptr();
}
m_tail_write_offset = 0;
return {};
}
void AllocatingMemoryStream::pop_head_chunk()
{
VERIFY(m_head);
if (m_head.ptr() == m_tail) {
m_head = nullptr;
m_tail = nullptr;
m_tail_write_offset = 0;
} else {
m_head = move(m_head->next);
}
m_head_read_offset = 0;
}
ReadonlyBytes AllocatingMemoryStream::peek_some_contiguous() const
{
if (!m_head)
return {};
auto const end = (m_head.ptr() == m_tail) ? m_tail_write_offset : CHUNK_SIZE;
return ReadonlyBytes { m_head->data + m_head_read_offset, end - m_head_read_offset };
}
void AllocatingMemoryStream::peek_some(Bytes bytes) const
{
size_t read_bytes = 0;
auto const* chunk = m_head.ptr();
auto chunk_offset = m_head_read_offset;
while (chunk && read_bytes < bytes.size()) {
auto const end = (chunk == m_tail) ? m_tail_write_offset : CHUNK_SIZE;
ReadonlyBytes available { chunk->data + chunk_offset, end - chunk_offset };
auto copied = available.copy_trimmed_to(bytes.slice(read_bytes));
read_bytes += copied;
if (copied < available.size())
break;
chunk = chunk->next.ptr();
chunk_offset = 0;
}
}
ErrorOr<Bytes> AllocatingMemoryStream::read_some(Bytes bytes)
{
size_t read_bytes = 0;
while (read_bytes < bytes.size() && m_head) {
auto const end = (m_head.ptr() == m_tail) ? m_tail_write_offset : CHUNK_SIZE;
ReadonlyBytes available { m_head->data + m_head_read_offset, end - m_head_read_offset };
auto copied = available.copy_trimmed_to(bytes.slice(read_bytes));
read_bytes += copied;
m_head_read_offset += copied;
m_used_buffer_size -= copied;
if (m_head_read_offset == end)
pop_head_chunk();
}
return bytes.trim(read_bytes);
}
ErrorOr<size_t> AllocatingMemoryStream::write_some(ReadonlyBytes bytes)
{
size_t written_bytes = 0;
while (written_bytes < bytes.size()) {
if (!m_tail || m_tail_write_offset == CHUNK_SIZE)
TRY(append_new_chunk());
Bytes tail_remaining { m_tail->data + m_tail_write_offset, CHUNK_SIZE - m_tail_write_offset };
auto copied = bytes.slice(written_bytes).copy_trimmed_to(tail_remaining);
m_tail_write_offset += copied;
written_bytes += copied;
m_used_buffer_size += copied;
}
return written_bytes;
}
ErrorOr<void> AllocatingMemoryStream::discard(size_t count)
{
if (count > m_used_buffer_size)
return Error::from_string_literal("Number of discarded bytes is higher than the number of allocated bytes");
m_used_buffer_size -= count;
while (count > 0) {
VERIFY(m_head);
auto const end = (m_head.ptr() == m_tail) ? m_tail_write_offset : CHUNK_SIZE;
auto const available = end - m_head_read_offset;
auto const to_consume = min(available, count);
m_head_read_offset += to_consume;
count -= to_consume;
if (m_head_read_offset == end)
pop_head_chunk();
}
return {};
}
ErrorOr<Optional<size_t>> AllocatingMemoryStream::offset_of(ReadonlyBytes needle) const
{
if (!m_head)
return Optional<size_t> {};
size_t chunk_count = 0;
for (auto const* chunk = m_head.ptr(); chunk; chunk = chunk->next.ptr())
++chunk_count;
auto search_spans = TRY(FixedArray<ReadonlyBytes>::create(chunk_count));
size_t i = 0;
for (auto const* chunk = m_head.ptr(); chunk; chunk = chunk->next.ptr(), ++i) {
auto const start = (chunk == m_head.ptr()) ? m_head_read_offset : 0;
auto const end = (chunk == m_tail) ? m_tail_write_offset : CHUNK_SIZE;
search_spans[i] = ReadonlyBytes { chunk->data + start, end - start };
}
return AK::memmem(search_spans.begin(), search_spans.end(), needle);
}
}