blob: 813708eddeafbd0761dea1521d007af1d52df6c8 [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.
#ifndef NET_SOCKET_BOTTLENECK_BUFFER_H_
#define NET_SOCKET_BOTTLENECK_BUFFER_H_
#include <stddef.h>
#include <stdint.h>
#include <vector>
#include "base/containers/circular_deque.h"
#include "base/containers/span.h"
#include "base/functional/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker_impl.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "net/base/net_export.h"
namespace net {
// An intermediate per-socket bottleneck buffer that models the propagation
// delay (one-way latency) of a network link for one stream direction.
//
// Data enters the buffer instantly (as fast as the producer can push it).
// Each chunk is tagged with a "time_available" timestamp =
// arrival_time + one_way_latency, modelling propagation delay. The
// consumer can pull a chunk once that timestamp has elapsed.
//
// Bandwidth is shaped by a separate, shareable BandwidthThrottle outside
// this buffer; per-buffer throughput is intentionally NOT modelled here.
//
// Callers are meant to own separate download and upload buffers.
// The buffer has a fixed capacity (default 2MB per direction). When
// full, Push() returns 0 (or a partial count) so the producer naturally
// backs off; once Pull() frees space, `space_available_cb_` fires so the
// producer can resume. This backpressure propagates to the OS socket's
// receive window for reads, or blocks application writes for uploads. A
// separate `data_ready_cb_` fires as an "invitation to drain" whenever
// the buffer has consumable data and the consumer hasn't been re-notified
// by an intervening Pull(). It may fire more than once for the same
// front chunk (e.g., when a fresh Push() arrives while ready data is
// still buffered, the drain timer re-arms and the callback fires again).
// Treat each fire as a hint to call Pull(); don't rely on it as a strict
// empty-to-ready edge.
//
// Stream- vs datagram-oriented consumers select how chunk boundaries and
// capacity are handled via the Mode argument to Push()/Pull(). Mode::
// kDatagram never splits a message: Push() accepts a datagram whole or not
// at all, and Pull() delivers one whole chunk or returns an error rather
// than truncating it.
//
// Sizes and capacities are tracked as size_t (matching span/std::vector
// arithmetic, which keeps the implementation cast-free). Push() and Pull()
// return int to match net/'s Socket read/write convention: a non-negative
// byte count, or a negative net::Error (e.g. net::ERR_MSG_TOO_BIG).
//
// Thread-unsafe: all calls must happen on the same sequence.
class NET_EXPORT BottleneckBuffer {
public:
// Minimum buffer capacity.
static constexpr size_t kMinCapacity = 16 * 1024; // 16KB
// Fallback when BDP cannot be computed. For latency-only streams this
// is a finite memory/backpressure cap, not a network bandwidth model.
// Keep it large enough to avoid throttling common high-throughput
// links; 2MB can absorb well over 50Mbps of in-flight data at 100ms
// latency.
static constexpr size_t kDefaultCapacity = 2 * 1024 * 1024; // 2MB
// Computes a capacity based on the bandwidth-delay product (BDP):
// the amount of data that can be "in the pipe" at any given time.
// Returns 2× BDP so the producer can fill the next window while the
// consumer drains the current one. Falls back to kDefaultCapacity when
// `throughput_bytes_per_sec` is 0 or `one_way_latency` is non-positive.
// `throughput_bytes_per_sec` is only used as a sizing hint here; the
// buffer itself does not enforce throughput at runtime.
static size_t BdpCapacity(uint64_t throughput_bytes_per_sec,
base::TimeDelta one_way_latency);
// Invitation to drain: fires when ready data is available. See the
// class doc for level-ish-vs-edge-triggered semantics.
using DataReadyCallback = base::RepeatingClosure;
// Fires on the full→not-full transition.
using SpaceAvailableCallback = base::RepeatingClosure;
// Whether the consumer treats the byte flow as a continuous stream or as
// discrete datagrams. The same choice applies to both directions, so a
// single mode controls chunk-boundary and capacity handling in Push()
// and Pull().
enum class Mode {
// Byte-stream (e.g., TCP): message boundaries don't matter.
// * Push() accepts as many bytes as fit and returns that count
// (0 when full); a partial count is backpressure.
// * Pull() drains ready chunks into `dest` across chunk boundaries
// until `dest` is full or no more chunks are ready, concatenating
// bytes from consecutive ready chunks.
kStream,
// Datagram (e.g., UDP): a message is never split.
// * Push() accepts the whole span atomically or nothing: it returns
// the full size on success, 0 if it doesn't fit right now (retry
// after `space_available_cb_`), or net::ERR_MSG_TOO_BIG if it can
// never fit even in an empty buffer (larger than capacity).
// * Pull() delivers exactly one chunk, preserving the producer's
// message boundary. If the ready front chunk does not fit entirely
// in `dest`, Pull() returns net::ERR_MSG_TOO_BIG and consumes
// nothing, rather than truncating the message.
kDatagram,
};
// `one_way_latency` is the propagation delay added to each chunk.
// Must be non-negative.
// `capacity` is the buffer size in bytes; must be strictly positive.
explicit BottleneckBuffer(base::TimeDelta one_way_latency,
size_t capacity = kDefaultCapacity);
~BottleneckBuffer();
BottleneckBuffer(const BottleneckBuffer&) = delete;
BottleneckBuffer& operator=(const BottleneckBuffer&) = delete;
// Pushes data into the buffer. `mode` controls the behaviour when `data`
// doesn't fit in the remaining capacity; see Mode for details.
// Returns the number of bytes accepted (>= 0), or a negative net::Error
// (net::ERR_MSG_TOO_BIG in kDatagram mode when `data` is larger than the
// total capacity).
int Push(base::span<const uint8_t> data, Mode mode = Mode::kStream);
// Pulls "ready" data (latency elapsed) into `dest`. Returns the number
// of bytes copied (>= 0), 0 if no data is ready yet, or a negative
// net::Error (net::ERR_MSG_TOO_BIG in kDatagram mode when the ready front
// chunk is larger than `dest`). `mode` controls whether the pull crosses
// chunk boundaries; see Mode for details.
int Pull(base::span<uint8_t> dest, Mode mode = Mode::kStream);
// Returns the number of bytes currently buffered (including
// not-yet-ready chunks).
size_t buffered_bytes() const {
CheckValidSequence();
return buffered_bytes_;
}
// Returns true if the buffer has no data.
bool empty() const {
CheckValidSequence();
return buffered_bytes_ == 0;
}
// Returns true if the buffer cannot accept more data.
bool full() const {
CheckValidSequence();
return buffered_bytes_ >= capacity_;
}
// Returns the number of bytes of free space.
size_t free_space() const {
CheckValidSequence();
return buffered_bytes_ < capacity_ ? capacity_ - buffered_bytes_ : 0;
}
// Returns true if there is data ready to be pulled (latency elapsed).
bool has_ready_data() const;
// Returns how many bytes can be pulled from the front chunk, capped at
// `max_bytes`. Returns 0 if the front chunk is not ready yet, or if
// `max_bytes` is 0.
size_t GetReadyBytesAtFront(size_t max_bytes) const;
// Installs the async callbacks. Must be called before the buffer is
// driven (Push()/Pull()), since the drain timer may otherwise fire with
// no callback to run. May be called more than once; each call replaces
// both previously-installed callbacks.
// Neither callback may be null — pass base::DoNothing() to suppress a
// signal. base::DoNothing() overwrites with a no-op; it does NOT
// preserve the previously-installed callback.
//
// Delivery contract for both callbacks:
// * Invoked on a posted task, not synchronously from inside a
// buffer method. The callback is therefore free to destroy the
// owner of this buffer (and the buffer with it) without UAF in
// any buffer method still on the stack.
// * BottleneckBuffer::Reset() invalidates any callback that has been
// posted but not yet dispatched.
//
// `data_ready_cb` is an invitation to drain: it fires whenever the
// buffer has ready data the consumer may want to pull. It can fire
// more than once for the same buffered data (see the class doc).
// If data is *already* ready at the moment SetCallbacks() is
// invoked, `data_ready_cb` is posted immediately to avoid a
// lost-wakeup for late-binding consumers.
//
// `space_available_cb` fires only on the full→not-full transition;
// it is not invoked at registration time even if the buffer is
// currently not-full (a freshly-constructed empty buffer would
// otherwise generate a confusing false positive).
void SetCallbacks(DataReadyCallback data_ready_cb,
SpaceAvailableCallback space_available_cb);
// Discards buffered data and stops the drain timer.
// Does NOT clear callbacks or rebind the sequence checker, so the buffer can
// be reused on the same sequence with the same callback wiring. Any
// callback posted before this reset but not yet dispatched is silently
// dropped. Does NOT fire `space_available_cb_` even though the buffer
// transitions to empty.
void Reset();
size_t capacity() const {
CheckValidSequence();
return capacity_;
}
private:
struct Chunk {
Chunk(std::vector<uint8_t> data, base::TimeTicks time_available);
Chunk(Chunk&&);
Chunk& operator=(Chunk&&);
~Chunk();
std::vector<uint8_t> data;
// Offset into `data` for partially-consumed chunks.
size_t offset = 0;
// Earliest time this chunk can be drained.
base::TimeTicks time_available;
size_t remaining() const { return data.size() - offset; }
};
void CheckValidSequence() const;
// Schedules the drain timer for the next available chunk.
void ScheduleDrainTimer();
void OnDrainTimer();
void PostDataReady();
void PostSpaceAvailable();
void DispatchDataReady(uint64_t generation);
void DispatchSpaceAvailable(uint64_t generation);
const base::TimeDelta one_way_latency_;
const size_t capacity_;
// FIFO queue of buffered chunks.
base::circular_deque<Chunk> chunks_;
size_t buffered_bytes_ = 0;
// Installed by SetCallbacks() before the buffer is driven. SetCallbacks()
// rejects null, so once installed these are always callable.
DataReadyCallback data_ready_cb_;
SpaceAvailableCallback space_available_cb_;
// Generation counter for posted callbacks. Reset() increments
// `callback_generation_`, invalidating any in-flight dispatch* tasks
// tagged with an earlier generation.
uint64_t callback_generation_ = 0;
base::OneShotTimer drain_timer_;
// Binds on first use and CHECKs that all later calls use the same sequence.
// Used directly rather than via SEQUENCE_CHECKER() so the check stays live in
// release: a cross-sequence call on this lock-free state is a memory-safety
// bug, and the macro compiles to a no-op outside DCHECK builds.
mutable base::SequenceCheckerImpl sequence_checker_;
base::WeakPtrFactory<BottleneckBuffer> weak_factory_{this};
};
} // namespace net
#endif // NET_SOCKET_BOTTLENECK_BUFFER_H_