| // 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 |