blob: 5f804d4223b1073f185449728451a894057a12e0 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/socket/bottleneck_buffer.h"
#include <algorithm>
#include <ranges>
#include <utility>
#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/numerics/safe_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "net/base/net_errors.h"
namespace net {
BottleneckBuffer::Chunk::Chunk(std::vector<uint8_t> data,
base::TimeTicks time_available)
: data(std::move(data)), time_available(time_available) {}
BottleneckBuffer::Chunk::Chunk(Chunk&&) = default;
BottleneckBuffer::Chunk& BottleneckBuffer::Chunk::operator=(Chunk&&) = default;
BottleneckBuffer::Chunk::~Chunk() = default;
// static
size_t BottleneckBuffer::BdpCapacity(uint64_t throughput_bytes_per_sec,
base::TimeDelta one_way_latency) {
if (throughput_bytes_per_sec == 0 || !one_way_latency.is_positive()) {
return kDefaultCapacity;
}
// BDP = throughput × RTT. `one_way_latency` is half-RTT, so RTT = 2×.
// saturated_cast clamps absurdly large BDPs (e.g. 10 Gbps at 1s latency)
// to SIZE_MAX instead of overflowing.
double bdp = throughput_bytes_per_sec * 2.0 * one_way_latency.InSecondsF();
size_t capacity = base::saturated_cast<size_t>(2 * bdp);
return std::max(kMinCapacity, capacity);
}
BottleneckBuffer::BottleneckBuffer(base::TimeDelta one_way_latency,
size_t capacity)
: one_way_latency_(one_way_latency), capacity_(capacity) {
CHECK_GT(capacity_, 0u);
CHECK_GE(one_way_latency_, base::TimeDelta());
// The buffer may be constructed before being handed to the socket
// sequence that uses it. Bind the checker on first use instead of
// construction.
sequence_checker_.DetachFromSequence();
}
BottleneckBuffer::~BottleneckBuffer() {
CheckValidSequence();
}
int BottleneckBuffer::Push(base::span<const uint8_t> data, Mode mode) {
CheckValidSequence();
if (data.empty()) {
return 0;
}
const size_t data_size = data.size();
const size_t space =
capacity_ > buffered_bytes_ ? capacity_ - buffered_bytes_ : 0;
if (mode == Mode::kDatagram) {
if (data_size > capacity_) {
// The datagram can never fit, even in an empty buffer.
return ERR_MSG_TOO_BIG;
}
if (data_size > space) {
// Doesn't fit right now; the caller should retry once Pull() frees
// space and space_available_cb_ fires.
return 0;
}
}
if (space == 0) {
return 0;
}
const size_t to_copy = std::min(space, data_size);
base::TimeTicks now = base::TimeTicks::Now();
base::TimeTicks available_at =
one_way_latency_.is_positive() ? now + one_way_latency_ : now;
auto copied = data.first(to_copy);
std::vector<uint8_t> chunk_data(std::from_range, copied);
chunks_.emplace_back(std::move(chunk_data), available_at);
buffered_bytes_ += to_copy;
// Ensure the drain timer is scheduled now that there is data to
// deliver. ScheduleDrainTimer() is a no-op if a timer is already pending.
ScheduleDrainTimer();
return base::checked_cast<int>(to_copy);
}
int BottleneckBuffer::Pull(base::span<uint8_t> dest, Mode mode) {
CheckValidSequence();
if (chunks_.empty() || dest.empty()) {
return 0;
}
const base::TimeTicks now = base::TimeTicks::Now();
// Datagram mode: the ready front chunk must be delivered whole. If it
// doesn't fit in `dest`, surface an error instead of splitting it.
if (mode == Mode::kDatagram) {
const Chunk& front = chunks_.front();
if (now >= front.time_available && front.remaining() > dest.size()) {
return ERR_MSG_TOO_BIG;
}
}
const bool was_full = full();
size_t total_pulled = 0;
const size_t dest_size = dest.size();
while (!chunks_.empty() && total_pulled < dest_size) {
Chunk& front = chunks_.front();
if (now < front.time_available) {
break;
}
const size_t want = dest_size - total_pulled;
const size_t can_copy = std::min(want, front.remaining());
base::span<const uint8_t> src_span =
base::span(front.data).subspan(front.offset, can_copy);
dest.subspan(total_pulled, can_copy).copy_from(src_span);
front.offset += can_copy;
total_pulled += can_copy;
buffered_bytes_ -= can_copy;
if (front.remaining() == 0) {
chunks_.pop_front();
}
if (mode == Mode::kDatagram) {
break;
}
}
if (was_full && !full()) {
PostSpaceAvailable();
}
// Re-arm for any remaining chunks. ScheduleDrainTimer() is a no-op if the
// buffer is now empty or a timer is already pending.
ScheduleDrainTimer();
return base::checked_cast<int>(total_pulled);
}
bool BottleneckBuffer::has_ready_data() const {
CheckValidSequence();
return !chunks_.empty() &&
base::TimeTicks::Now() >= chunks_.front().time_available;
}
size_t BottleneckBuffer::GetReadyBytesAtFront(size_t max_bytes) const {
CheckValidSequence();
if (chunks_.empty() || max_bytes == 0) {
return 0;
}
if (base::TimeTicks::Now() < chunks_.front().time_available) {
return 0;
}
return std::min(max_bytes, chunks_.front().remaining());
}
void BottleneckBuffer::SetCallbacks(DataReadyCallback data_ready_cb,
SpaceAvailableCallback space_available_cb) {
CheckValidSequence();
// Callbacks must be callable. Pass base::DoNothing() to suppress a signal
// rather than a null callback.
CHECK(!data_ready_cb.is_null());
CHECK(!space_available_cb.is_null());
data_ready_cb_ = std::move(data_ready_cb);
space_available_cb_ = std::move(space_available_cb);
// If data is already ready at registration time, fire data_ready_cb_
// so a consumer that binds late doesn't miss the signal. We don't do
// the equivalent for space_available_cb_ because every empty buffer
// is trivially "not full" — firing that would always be a false
// positive.
if (has_ready_data()) {
PostDataReady();
}
}
void BottleneckBuffer::Reset() {
CheckValidSequence();
chunks_.clear();
buffered_bytes_ = 0;
drain_timer_.Stop();
// Invalidate any callback tasks posted before this Reset() — the
// consumer asked us to forget; they shouldn't observe spurious
// "data ready" / "space available" signals from the old data.
++callback_generation_;
}
void BottleneckBuffer::CheckValidSequence() const {
CHECK(sequence_checker_.CalledOnValidSequence());
}
void BottleneckBuffer::ScheduleDrainTimer() {
CheckValidSequence();
if (chunks_.empty() || drain_timer_.IsRunning()) {
return;
}
base::TimeTicks now = base::TimeTicks::Now();
const Chunk& front = chunks_.front();
// Wait for latency if chunk isn't ready yet.
if (now < front.time_available) {
base::TimeDelta wait = front.time_available - now;
drain_timer_.Start(FROM_HERE, wait,
base::BindOnce(&BottleneckBuffer::OnDrainTimer,
weak_factory_.GetWeakPtr()));
return;
}
// Front chunk is ready now. Post a zero-delay timer task so
// OnDrainTimer fires on its own task rather than re-entering the
// caller of ScheduleDrainTimer.
drain_timer_.Start(FROM_HERE, base::TimeDelta(),
base::BindOnce(&BottleneckBuffer::OnDrainTimer,
weak_factory_.GetWeakPtr()));
}
void BottleneckBuffer::OnDrainTimer() {
CheckValidSequence();
const bool ready = has_ready_data();
if (ready) {
PostDataReady();
}
// Re-schedule IFF the next chunk is not yet ready. Otherwise, the reader
// consumes it synchronously, or schedules the drain timer itself.
if (!chunks_.empty() && !ready) {
ScheduleDrainTimer();
}
}
void BottleneckBuffer::PostDataReady() {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&BottleneckBuffer::DispatchDataReady,
weak_factory_.GetWeakPtr(), callback_generation_));
}
void BottleneckBuffer::PostSpaceAvailable() {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&BottleneckBuffer::DispatchSpaceAvailable,
weak_factory_.GetWeakPtr(), callback_generation_));
}
void BottleneckBuffer::DispatchDataReady(uint64_t generation) {
CheckValidSequence();
// Drop tasks invalidated by an intervening Reset().
if (generation != callback_generation_) {
return;
}
data_ready_cb_.Run();
}
void BottleneckBuffer::DispatchSpaceAvailable(uint64_t generation) {
CheckValidSequence();
// Drop tasks invalidated by an intervening Reset().
if (generation != callback_generation_) {
return;
}
space_available_cb_.Run();
}
} // namespace net