From d050aea4cd892d71ed7fc78b6c6149a7231db5fc Mon Sep 17 00:00:00 2001 From: Dimitri Staessens Date: Sun, 5 Jul 2026 18:58:37 +0200 Subject: ipcpd: Rework congestion avoidance Congestion avoidance is a property of the layer, orthogonal to ARQ and to flow control: FRCP retransmits and lets the peer pace the sender, per flow, end-to-end; the IPCP paces path aggregates. Each signal means one thing: a loss triggers a retransmission, a mark means congestion, the peer window means a slow receiver. Every flow is paced by the same rate law whatever its QoS, so a greedy raw sender shares a bottleneck fairly with a reliable stream. The unit of control is the (destination address, QoS cube) aggregate: all flows toward that destination share one controller and one rate; a start-time fair-queuing pacer divides the rate across them by deadline instead of blocking the send path, and a new flow rides the aggregate's estimates at its current rate, with no probing of its own. Slow start runs once per aggregate. The congestion signal is a multi-bit magnitude: forwarders mark packets with their standing queue depth, MAX-combined across hops, so a packet carries the deepest queue on its path. The receiver feeds back a time-integral mean over a window that adapts to the flow's byte rate, measuring a slow flow with the same fidelity as a fast one. The sender runs AIMD scaled by elapsed wall-clock time, which makes the steady-state allocation RTT-independent. The PCI gains one byte: the path capacity as a quarter-log2 code. Forwarders estimate their egress rate from busy-period drain and MIN-stamp the byte, the receiver returns the window minimum with its feedback, and the sender scales its rate floor and additive slope to the bottleneck (C / 32). A deep cut implies a backlogged bottleneck and a backlogged bottleneck advertises its capacity, so the scaled floor is live exactly when recovery needs it: the probe heals a halving in seconds at any link rate, and the floor bounds the deepest hole to a factor 32 below the bottleneck. Signed-off-by: Dimitri Staessens Signed-off-by: Sander Vrijders --- src/ipcpd/unicast/CMakeLists.txt | 1 + src/ipcpd/unicast/ca.c | 178 +++++++-- src/ipcpd/unicast/ca.h | 28 +- src/ipcpd/unicast/ca/mb-ecn.c | 826 ++++++++++++++++++++++++++++++++------- src/ipcpd/unicast/ca/mb-ecn.h | 17 +- src/ipcpd/unicast/ca/nop.c | 35 +- src/ipcpd/unicast/ca/nop.h | 17 +- src/ipcpd/unicast/ca/ops.h | 20 +- src/ipcpd/unicast/cap.c | 291 ++++++++++++++ src/ipcpd/unicast/cap.h | 54 +++ src/ipcpd/unicast/dir/dht.c | 2 +- src/ipcpd/unicast/dt.c | 81 +++- src/ipcpd/unicast/dt.h | 3 +- src/ipcpd/unicast/fa.c | 72 +++- src/ipcpd/unicast/fa.h | 1 + src/ipcpd/unicast/psched.c | 199 ++++++++-- src/ipcpd/unicast/psched.h | 6 +- 17 files changed, 1532 insertions(+), 299 deletions(-) create mode 100644 src/ipcpd/unicast/cap.c create mode 100644 src/ipcpd/unicast/cap.h (limited to 'src/ipcpd/unicast') diff --git a/src/ipcpd/unicast/CMakeLists.txt b/src/ipcpd/unicast/CMakeLists.txt index d3388112..d51546a3 100644 --- a/src/ipcpd/unicast/CMakeLists.txt +++ b/src/ipcpd/unicast/CMakeLists.txt @@ -6,6 +6,7 @@ protobuf_generate_c(DHT_PROTO_SRCS DHT_PROTO_HDRS set(UNICAST_SOURCES addr-auth.c ca.c + cap.c connmgr.c dir.c dt.c diff --git a/src/ipcpd/unicast/ca.c b/src/ipcpd/unicast/ca.c index a1751672..d0ee2f73 100644 --- a/src/ipcpd/unicast/ca.c +++ b/src/ipcpd/unicast/ca.c @@ -22,17 +22,47 @@ #define OUROBOROS_PREFIX "ca" +#include "config.h" + +#include #include #include "ca.h" #include "ca/pol.h" +#include +#include + +/* + * A ca_ctx holds congestion state for a (peer address, qos cube) PATH, + * not for a flow. In the default build the façade interns one ctx per + * (addr, qc) and shares it across every flow on that path; the policy + * runs on the shared ctx and cannot tell one flow from many. Per-flow + * ctx (IPCP_CA_PER_FLOW) is a testing build only: it skips interning so + * every flow gets its own ctx. + */ + +struct ca_ctx { + uint64_t addr; + qoscube_t qc; + size_t refs; + void * pol; /* policy ctx (ops->ctx_create result) */ + struct list_head next; +}; + struct { - struct ca_ops * ops; + struct ca_ops * ops; +#ifndef IPCP_CA_PER_FLOW + struct list_head buckets[CA_BUCKETS]; + pthread_mutex_t mtx; +#endif } ca; int ca_init(enum pol_cong_avoid pol) { +#ifndef IPCP_CA_PER_FLOW + size_t i; +#endif switch(pol) { case CA_NONE: log_dbg("Disabling congestion control."); @@ -46,63 +76,167 @@ int ca_init(enum pol_cong_avoid pol) return -1; } +#ifndef IPCP_CA_PER_FLOW + for (i = 0; i < CA_BUCKETS; i++) + list_head_init(&ca.buckets[i]); + + if (pthread_mutex_init(&ca.mtx, NULL) != 0) + return -1; +#endif return 0; } void ca_fini(void) { +#ifndef IPCP_CA_PER_FLOW + size_t i; + + /* Data path is stopped; drain any ctx a flow left interned. */ + for (i = 0; i < CA_BUCKETS; i++) { + struct list_head * p; + struct list_head * h; + + list_for_each_safe(p, h, &ca.buckets[i]) { + struct ca_ctx * ctx; + ctx = list_entry(p, struct ca_ctx, next); + list_del(&ctx->next); + ca.ops->ctx_destroy(ctx->pol); + free(ctx); + } + } + + pthread_mutex_destroy(&ca.mtx); +#endif ca.ops = NULL; } -void * ca_ctx_create(void) +#ifndef IPCP_CA_PER_FLOW +static size_t ca_bucket(uint64_t addr, + qoscube_t qc) { - return ca.ops->ctx_create(); + return (addr ^ (addr >> 32) ^ (uint64_t) qc) & (CA_BUCKETS - 1); +} +#endif + +void * ca_ctx_get(uint64_t addr, + qoscube_t qc) +{ + struct ca_ctx * ctx; +#ifndef IPCP_CA_PER_FLOW + struct list_head * p; + size_t b = ca_bucket(addr, qc); + + pthread_mutex_lock(&ca.mtx); + + list_for_each(p, &ca.buckets[b]) { + ctx = list_entry(p, struct ca_ctx, next); + if (ctx->addr == addr && ctx->qc == qc) { + ctx->refs++; + pthread_mutex_unlock(&ca.mtx); + return ctx; + } + } +#endif + ctx = malloc(sizeof(*ctx)); + if (ctx == NULL) + goto fail_ctx; + + ctx->pol = ca.ops->ctx_create(); + if (ctx->pol == NULL) + goto fail_pol; + + ctx->addr = addr; + ctx->qc = qc; + ctx->refs = 1; + +#ifndef IPCP_CA_PER_FLOW + list_add(&ctx->next, &ca.buckets[b]); + + pthread_mutex_unlock(&ca.mtx); +#endif + return ctx; + fail_pol: + free(ctx); + fail_ctx: +#ifndef IPCP_CA_PER_FLOW + pthread_mutex_unlock(&ca.mtx); +#endif + return NULL; } -void ca_ctx_destroy(void * ctx) +void ca_ctx_put(void * _ctx) { - return ca.ops->ctx_destroy(ctx); + struct ca_ctx * ctx = _ctx; + +#ifndef IPCP_CA_PER_FLOW + pthread_mutex_lock(&ca.mtx); + + if (--ctx->refs > 0) { + pthread_mutex_unlock(&ca.mtx); + return; + } + + list_del(&ctx->next); + + pthread_mutex_unlock(&ca.mtx); +#endif + ca.ops->ctx_destroy(ctx->pol); + + free(ctx); } -ca_wnd_t ca_ctx_update_snd(void * ctx, - size_t len) +time_t ca_ctx_update_snd(void * _ctx, + size_t len, + uint8_t lecn, + uint64_t * ftag) { - return ca.ops->ctx_update_snd(ctx, len); + struct ca_ctx * ctx = _ctx; + + return ca.ops->ctx_update_snd(ctx->pol, len, lecn, ftag); } -bool ca_ctx_update_rcv(void * ctx, +bool ca_ctx_update_rcv(void * _ctx, size_t len, uint8_t ecn, - uint16_t * ece) + uint8_t cap, + uint16_t * ece, + uint8_t * fcap) { - return ca.ops->ctx_update_rcv(ctx, len, ecn, ece); -} + struct ca_ctx * ctx = _ctx; -void ca_ctx_update_ece(void * ctx, - uint16_t ece) -{ - return ca.ops->ctx_update_ece(ctx, ece); + return ca.ops->ctx_update_rcv(ctx->pol, len, ecn, cap, ece, fcap); } -void ca_wnd_wait(ca_wnd_t wnd) +void ca_ctx_update_ece(void * _ctx, + uint16_t ece, + uint8_t cap) { - return ca.ops->wnd_wait(wnd); + struct ca_ctx * ctx = _ctx; + + return ca.ops->ctx_update_ece(ctx->pol, ece, cap); } -int ca_calc_ecn(int fd, +int ca_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, size_t len) { - return ca.ops->calc_ecn(fd, ecn, qc, len); + return ca.ops->calc_ecn(queued, ecn, qc, len); } -ssize_t ca_print_stats(void * ctx, +bool ca_marks_ecn(void) +{ + return ca.ops->marks_ecn; +} + +ssize_t ca_print_stats(void * _ctx, char * buf, size_t len) { + struct ca_ctx * ctx = _ctx; + if (ca.ops->print_stats == NULL) return 0; - return ca.ops->print_stats(ctx, buf, len); + return ca.ops->print_stats(ctx->pol, buf, len); } diff --git a/src/ipcpd/unicast/ca.h b/src/ipcpd/unicast/ca.h index 47ea15a0..d73d35f5 100644 --- a/src/ipcpd/unicast/ca.h +++ b/src/ipcpd/unicast/ca.h @@ -29,38 +29,40 @@ #include #include -typedef union { - time_t wait; -} ca_wnd_t; - int ca_init(enum pol_cong_avoid ca); void ca_fini(void); /* OPS */ -void * ca_ctx_create(void); +void * ca_ctx_get(uint64_t addr, + qoscube_t qc); -void ca_ctx_destroy(void * ctx); +void ca_ctx_put(void * ctx); -ca_wnd_t ca_ctx_update_snd(void * ctx, - size_t len); +time_t ca_ctx_update_snd(void * ctx, + size_t len, + uint8_t lecn, + uint64_t * ftag); bool ca_ctx_update_rcv(void * ctx, size_t len, uint8_t ecn, - uint16_t * ece); + uint8_t cap, + uint16_t * ece, + uint8_t * fcap); void ca_ctx_update_ece(void * ctx, - uint16_t ece); + uint16_t ece, + uint8_t cap); -void ca_wnd_wait(ca_wnd_t wnd); - -int ca_calc_ecn(int fd, +int ca_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, size_t len); +bool ca_marks_ecn(void); + ssize_t ca_print_stats(void * ctx, char * buf, size_t len); diff --git a/src/ipcpd/unicast/ca/mb-ecn.c b/src/ipcpd/unicast/ca/mb-ecn.c index b310c4fc..e59aac88 100644 --- a/src/ipcpd/unicast/ca/mb-ecn.c +++ b/src/ipcpd/unicast/ca/mb-ecn.c @@ -28,9 +28,10 @@ #include "config.h" -#include #include +#include +#include "cap.h" #include "mb-ecn.h" #include @@ -38,31 +39,125 @@ #include #include -/* congestion avoidance constants */ -#define CA_SHFT 5 /* Average over 32 pkts */ -#define CA_WND (1 << CA_SHFT) /* 32 pkts receiver wnd */ -#define CA_UPD (1 << (CA_SHFT - 2)) /* Update snd every 8 pkt */ -#define CA_SLOT 24 /* Initial slot = 16 ms */ -#define CA_INC 1UL << 16 /* ~4MiB/s^2 additive inc */ -#define CA_IWL 1UL << 16 /* Initial limit ~4MiB/s */ -#define CA_MINPS 8 /* Mimimum pkts / slot */ -#define CA_MAXPS 64 /* Maximum pkts / slot */ -#define ECN_Q_SHFT 4 -#define ts_to_ns(ts) ((size_t) ts.tv_sec * BILLION + ts.tv_nsec) +/* + * The sender paces with a token bucket at a rate driven by Δt-scaled + * AIMD: each step changes the rate proportional to elapsed wall-clock + * time, so the per-second dynamics do not depend on how often packets + * arrive. The receiver's averaging window and the sender's feedback + * staleness both stretch with the flow's byte rate, so a slow flow + * is measured and controlled like a fast one; CA_RATE_MIN only + * bounds those horizons (window <= CA_TW_ABSMAX, TTL ~8 s). There + * is no per-flow timer; the control runs on packet sends. + * + * The floor and the AI slope scale with the path: forwarders stamp + * their measured link capacity into the PCI (cap.c), the receiver + * feeds the path MIN back with the ece, and the sender derives + * rate_min = ai_rate = C / 32, clamped to [CA_RATE_MIN, CA_RMIN_MAX], + * falling back to those defaults when the signal goes stale. + */ + +#define CA_SHFT 5 /* ece fixed point: 32 * ecn */ +#define CA_TW_MIN (1ULL << 20) /* min mean window ~1.05 ms */ +#define CA_TW_INIT (1ULL << 26) /* initial mean window ~67ms */ +#define CA_TW_ABSMAX (1ULL << 32) /* window ceiling ~4.3 s */ +/* Quiet horizon, in windows (1 << shift): gap restart and the TTLs. */ +#define CA_TW_GAP_SHFT 2 +#define CA_N_TARGET 16 /* target packets per window */ +#define CA_RX_WBYTES (CA_N_TARGET * 1000ULL) /* target bytes/window */ +#define CA_RX_WCLOSE (2 * CA_RX_WBYTES) /* byte-triggered early close */ +#define CA_TW_SM_SHFT 2 /* window EWMA weight 1/4 */ +#define CA_MARK_Q 4 /* mark quantum (packets) */ + +#define CA_RATE_MIN (1ULL << 13) /* 8 KiB/s rate floor */ +#define CA_RATE_INIT (1ULL << 16) /* slow start seed 64 KiB/s */ +/* Rate cap; also keeps rate * dt and rate * rise below 2^64. */ +#define CA_RATE_MAX (1ULL << 37) +#define CA_INV_SHFT 32 /* reciprocal-rate fixp */ +#define CA_AI_RATE (1ULL << 16) /* 64 KiB/s^2 additive inc */ +#define CA_PROBE_TC (8ULL * BILLION) /* proportional probe TC 8s */ +#define CA_ECE_REF (16 << CA_SHFT) /* full congestion: ecn 16 */ +#define CA_MD_KD_DIV 4 /* one-sided lead gain 1/4 */ +/* Must stay >= 1 ms: the MD term scales by dtc / MILLION (truncates). */ +#define CA_DT_CTRL (BILLION / 1000) +#define CA_DT_CAP (BILLION / 20) /* idle-resume Δt clamp 50ms */ +/* Floor of the rate-relative feedback staleness (ctx->ece_ttl). */ +#define CA_ECE_TTL ((1 << CA_TW_GAP_SHFT) * CA_TW_INIT) +#define CA_SS_TC (BILLION / 50) +#define CA_WASH_BKT (BILLION / 32) /* washout bucket ~31 ms */ +#define CA_WASH_SHFT 2 /* damp 1/4 of bucket change */ + +#define CA_CAP_SHFT 5 /* floor = capacity / 32 */ +#define CA_CAP_SM_SHFT 1 /* capacity EWMA weight 1/2 */ +/* Outlives ece_ttl 16x: onset-fresh fcap re-seeds each episode. */ +#define CA_CAP_TTL_SHFT 4 +#define CA_RMIN_MAX (1ULL << 32) /* derived floor ceiling */ + +#define CA_SND_WIN CA_TW_INIT /* sender util window ~67ms */ +#define CA_USE_NUM 3 /* backlogged: offered >= */ +#define CA_USE_DEN 4 /* 3/4 * window-start rate */ +#define CA_HDRM_MARKS 4 /* ceiling ~2x offered load */ +#define CA_SND_DEC_SHFT 4 /* offered max-filter 1/16 */ +#define CA_SND_DEC_CAP 16 /* bound gapped-close decay */ +#define CA_SND_BYT_MAX (1ULL << 33) /* offered-byte saturation */ + +/* + * Retuning invariants (pinned by the unit tests): + * - (1 << CA_TW_GAP_SHFT) * CA_TW_INIT > S * BILLION / CA_RATE_MIN, or + * a floor-rate flow's onset restart-loops (S ~ one MTU; both ns). + * - CA_RX_WBYTES * BILLION / CA_RATE_MIN < CA_TW_ABSMAX: the + * floor-rate window must clear the ceiling. + * - CA_RATE_MAX * CA_DT_CAP, the folded lead * inv_rate at + * CA_RATE_MIN, and owed * BILLION (owed clamped in mb_ecn_snd) all + * keep the pacer arithmetic below 2^64. + * - CA_DT_CTRL < CA_WASH_BKT < CA_DT_CAP: control cadence under the + * washout bucket under the sparse-step cutoff. + * - CA_RATE_MIN <= CA_RATE_INIT and CA_RMIN_MAX < CA_RATE_MAX. + */ struct mb_ecn_ctx { - uint16_t rx_ece; /* Level of congestion (upstream) */ - size_t rx_ctr; /* Receiver side packet counter */ - - uint16_t tx_ece; /* Level of congestion (downstream) */ - size_t tx_ctr; /* Sender side packet counter */ - size_t tx_wbc; /* Window byte count */ - size_t tx_wpc; /* Window packet count */ - size_t tx_wbl; /* Window byte limit */ - bool tx_cav; /* Congestion avoidance */ - size_t tx_mul; /* Slot size multiplier */ - size_t tx_inc; /* Additive increase */ - size_t tx_slot; + uint16_t rx_ece; /* smoothed congestion echo (32 * ecn) */ + uint64_t rx_acc; /* window integral of ecn * dt */ + uint64_t rx_byt; /* bytes arrived in current window */ + uint64_t rx_ts; /* last packet arrival (ns) */ + uint64_t rx_win; /* window start (ns) */ + uint64_t rx_tw; /* adaptive averaging window (ns) */ + uint8_t rx_cap; /* window bottleneck capacity code */ + + uint16_t tx_ece; /* congestion reported from downstream */ + uint16_t tx_ecp; /* previous tx_ece (rise detection) */ + uint8_t tx_loc; /* local first-hop ecn mark (fallback) */ + uint8_t tx_cap; /* path capacity code fed back to us */ + bool tx_cav; /* past slow start */ + uint64_t rate; /* paced send rate (bytes/s) */ + uint64_t rate_min; /* capacity-derived rate floor (B/s) */ + uint64_t ai_rate; /* additive-increase slope (B/s^2) */ + uint64_t ece_ttl; /* how long feedback stays valid (ns) */ + uint64_t r_bkt; /* rate snapshot at last washout bucket */ + uint64_t wash_acc; /* washout bucket time accumulator (ns) */ + uint64_t inv_rate; /* fixed-point 1/rate for pacing */ + uint64_t vt; /* virtual service clock (bytes) */ + uint64_t lead; /* pacer lead of last send (bytes) */ + uint64_t last_ts; /* last clock advance (ns) */ + uint64_t last_ctrl; /* last rate update (ns) */ + uint64_t last_fb; /* last feedback applied (ns) */ + uint64_t last_loc; /* last local mark seen (ns) */ + uint64_t last_cap; /* last capacity applied (ns) */ + + uint64_t n_ctrl; /* control steps taken */ + uint64_t t_ctrl; /* wall time covered by steps (ns) */ + uint64_t t_bank; /* increase time banked in steps (ns) */ + uint64_t n_fb; /* feedback updates received */ + uint64_t n_ttl; /* feedback aged out (TTL) */ + uint64_t n_cap; /* capacity updates applied */ + uint64_t ss_peak; /* peak rate in slow start (bytes/s) */ + + uint64_t snd_byt; /* bytes offered this window (capped) */ + uint64_t snd_win; /* utilisation window start (ns) */ + uint64_t snd_r0; /* rate at window start */ + uint64_t snd_rate; /* max-filter of offered rate (B/s) */ + bool backlogged; /* offered load keeps the pacer busy */ + bool src_limited; /* rate held at offered-load ceiling */ + bool started; /* a real send has occurred */ }; struct ca_ops mb_ecn_ca_ops = { @@ -71,14 +166,34 @@ struct ca_ops mb_ecn_ca_ops = { .ctx_update_snd = mb_ecn_ctx_update_snd, .ctx_update_rcv = mb_ecn_ctx_update_rcv, .ctx_update_ece = mb_ecn_ctx_update_ece, - .wnd_wait = mb_ecn_wnd_wait, .calc_ecn = mb_ecn_calc_ecn, + .marks_ecn = true, .print_stats = mb_ecn_print_stats }; +static uint64_t mb_ecn_rate_inv(uint64_t rate) +{ + return ((uint64_t) BILLION << CA_INV_SHFT) / rate; +} + +/* + * Feedback arrives once per receiver window, and the window tracks + * the flow's byte rate. Mirror it: age the signal out only past the + * quiet horizon at the current rate, floored for fast flows. + */ +static uint64_t mb_ecn_ece_ttl(uint64_t rate) +{ + uint64_t ttl; + + ttl = (1 << CA_TW_GAP_SHFT) * CA_RX_WBYTES * BILLION / rate; + + return ttl > (uint64_t) CA_ECE_TTL ? ttl : (uint64_t) CA_ECE_TTL; +} + void * mb_ecn_ctx_create(void) { struct timespec now; + uint64_t t; struct mb_ecn_ctx * ctx; ctx = malloc(sizeof(*ctx)); @@ -89,10 +204,27 @@ void * mb_ecn_ctx_create(void) memset(ctx, 0, sizeof(*ctx)); - ctx->tx_mul = CA_SLOT; - ctx->tx_wbl = CA_IWL; - ctx->tx_inc = CA_INC; - ctx->tx_slot = ts_to_ns(now) >> ctx->tx_mul; + t = TS_TO_UINT64(now); + + ctx->rate = CA_RATE_INIT; + ctx->rate_min = CA_RATE_MIN; + ctx->ai_rate = CA_AI_RATE; + ctx->ece_ttl = mb_ecn_ece_ttl(CA_RATE_INIT); + ctx->r_bkt = CA_RATE_INIT; + ctx->inv_rate = mb_ecn_rate_inv(CA_RATE_INIT); + ctx->rx_ts = t; + ctx->rx_win = t; + ctx->rx_tw = CA_TW_INIT; + ctx->last_ts = t; + ctx->last_ctrl = t; + ctx->last_fb = t; + ctx->last_loc = t; + ctx->last_cap = t; + + /* snd_win/last_ts re-seeded lazily on the first real send. */ + ctx->snd_r0 = CA_RATE_INIT; + ctx->snd_rate = CA_RATE_INIT; + ctx->backlogged = true; return (void *) ctx; } @@ -102,158 +234,519 @@ void mb_ecn_ctx_destroy(void * ctx) free(ctx); } -#define _slot_after(new, old) ((int64_t) (old - new) < 0) +/* Local first-hop mark exits slow start and covers dead feedback. */ +static void mb_ecn_loc(struct mb_ecn_ctx * ctx, + uint8_t lecn, + uint64_t t) +{ + if (lecn == 0) + return; + + ctx->tx_loc = lecn; + ctx->tx_cav = true; + ctx->last_loc = t; +} + +/* Slow start: ramp only while backlogged. */ +static void mb_ecn_slow_start(struct mb_ecn_ctx * ctx, + uint64_t dta) +{ + if (ctx->backlogged) + ctx->rate += ctx->rate * dta / CA_SS_TC; + + ctx->r_bkt = ctx->rate; +} + +/* Additive increase plus a rate-independent proportional probe. */ +static void mb_ecn_increase(struct mb_ecn_ctx * ctx, + uint64_t dta) +{ + if (!ctx->backlogged) + return; + + ctx->rate += ctx->ai_rate * dta / BILLION; + ctx->rate += ctx->rate * dta / CA_PROBE_TC; +} + +/* Multiplicative decrease: proportional cut plus a one-sided lead. */ +static void mb_ecn_decrease(struct mb_ecn_ctx * ctx, + uint64_t dtc) +{ + uint64_t dtm; + uint64_t mark; + uint64_t rise; + uint64_t cut; + uint16_t m; + + m = ctx->tx_ece > 0 ? ctx->tx_ece + : (uint16_t) (ctx->tx_loc << CA_SHFT); + if (m == 0) { + ctx->tx_ecp = 0; + return; + } + + mark = MIN(m, CA_ECE_REF); + rise = 0; + if (m > ctx->tx_ecp) + rise = MIN(m - ctx->tx_ecp, CA_ECE_REF); + + cut = ctx->rate * rise / (CA_ECE_REF * CA_MD_KD_DIV); + + /* Honest elapsed ms, so a starved sender still cuts. */ + dtm = dtc / MILLION; + if (mark * dtm >= CA_ECE_REF * 500) + cut += ctx->rate / 2; + else + cut += ctx->rate * mark * dtm / (CA_ECE_REF * 1000); + + if (cut > ctx->rate / 2) + cut = ctx->rate / 2; + + ctx->rate -= cut; + ctx->tx_ecp = m; +} + +/* + * Washout: once per wall-clock bucket, damp a fixed fraction of the + * rate change over that bucket. Bucketed (not per-step) so it stays + * cadence-independent; bounded so it cannot reverse a ramp. A sparse + * step resets it, so a starved sender keeps its cut. + */ +static void mb_ecn_washout(struct mb_ecn_ctx * ctx, + uint64_t dtc, + uint64_t dta) +{ + if (dtc > (uint64_t) CA_DT_CAP) { + ctx->r_bkt = ctx->rate; + ctx->wash_acc = 0; + return; + } + + ctx->wash_acc += dta; + if (ctx->wash_acc < (uint64_t) CA_WASH_BKT) + return; + + if (ctx->rate > ctx->r_bkt) + ctx->rate -= (ctx->rate - ctx->r_bkt) >> CA_WASH_SHFT; + else + ctx->rate += (ctx->r_bkt - ctx->rate) >> CA_WASH_SHFT; + + ctx->r_bkt = ctx->rate; + ctx->wash_acc = 0; +} + +/* Offered-load ceiling backstop while source-limited. */ +static void mb_ecn_ceiling(struct mb_ecn_ctx * ctx) +{ + unsigned code; + uint64_t hi; + + if (ctx->backlogged) { + ctx->src_limited = false; + return; + } + + code = (unsigned) cap_enc(ctx->snd_rate) + CA_HDRM_MARKS; + if (code > UINT8_MAX) /* keep the cast lossless */ + code = UINT8_MAX; + + hi = cap_dec((uint8_t) code); + if (hi > CA_RATE_MAX) + hi = CA_RATE_MAX; + + if (hi < CA_RATE_MIN) + hi = CA_RATE_MIN; + + ctx->src_limited = ctx->rate > hi; + if (ctx->src_limited) { + ctx->rate = hi; + ctx->r_bkt = ctx->rate; + } +} + +static void mb_ecn_ctrl(struct mb_ecn_ctx * ctx, + uint64_t dtc) +{ + uint64_t dta; + uint64_t lo; + + /* AI and slow start bank at most CA_DT_CAP of idle time. */ + dta = MIN(dtc, (uint64_t) CA_DT_CAP); + + ctx->n_ctrl++; + ctx->t_ctrl += dtc; + ctx->t_bank += dta; + + if (ctx->tx_cav) { + mb_ecn_increase(ctx, dta); + mb_ecn_decrease(ctx, dtc); + mb_ecn_washout(ctx, dtc, dta); + } else { + mb_ecn_slow_start(ctx, dta); + } + + mb_ecn_ceiling(ctx); + + /* Capacity floor only while backlogged; else the absolute floor. */ + lo = ctx->backlogged ? ctx->rate_min : (uint64_t) CA_RATE_MIN; + if (ctx->rate < lo) + ctx->rate = lo; + + if (ctx->rate > CA_RATE_MAX) + ctx->rate = CA_RATE_MAX; + + ctx->inv_rate = mb_ecn_rate_inv(ctx->rate); + ctx->ece_ttl = mb_ecn_ece_ttl(ctx->rate); + + if (!ctx->tx_cav && ctx->rate > ctx->ss_peak) + ctx->ss_peak = ctx->rate; +} + +/* Fold offered into the max filter: rise at once, decay 1/16 per window. */ +static void mb_ecn_offered(struct mb_ecn_ctx * ctx, + uint64_t offered, + uint64_t elapsed) +{ + uint64_t n; + + if (offered >= ctx->snd_rate) { + ctx->snd_rate = offered; + return; + } + + n = MIN(elapsed / CA_SND_WIN, CA_SND_DEC_CAP); + while (n-- > 0 && ctx->snd_rate > offered) + ctx->snd_rate -= (ctx->snd_rate - offered) >> CA_SND_DEC_SHFT; +} + +/* + * Close the utilisation window: set backlogged from the level test, + * fold offered into the max filter, then reset the window. + */ +static void mb_ecn_win(struct mb_ecn_ctx * ctx, + uint64_t t) +{ + uint64_t elapsed = t - ctx->snd_win; + uint64_t offered; + + offered = ctx->snd_byt * BILLION / elapsed; + + ctx->backlogged = offered * CA_USE_DEN >= ctx->snd_r0 * CA_USE_NUM; -ca_wnd_t mb_ecn_ctx_update_snd(void * _ctx, - size_t len) + mb_ecn_offered(ctx, offered, elapsed); + + if (ctx->backlogged) + ctx->src_limited = false; + + ctx->snd_win = t; + ctx->snd_byt = 0; + ctx->snd_r0 = ctx->rate; +} + +/* Age out congestion, local-mark and capacity signals once stale. */ +static void mb_ecn_age(struct mb_ecn_ctx * ctx, + uint64_t t) +{ + if (t - ctx->last_fb > ctx->ece_ttl) { + if (ctx->tx_ece > 0) + ctx->n_ttl++; + ctx->tx_ece = 0; + } + + if (t - ctx->last_loc > ctx->ece_ttl) + ctx->tx_loc = 0; + + /* Stale capacity: fall back to the compile-time defaults. */ + if (t - ctx->last_cap > ctx->ece_ttl << CA_CAP_TTL_SHFT) { + ctx->rate_min = CA_RATE_MIN; + ctx->ai_rate = CA_AI_RATE; + ctx->tx_cap = 0; + } +} + +/* Advance the virtual clock; a gap past CA_DT_CAP credits a burst. */ +static void mb_ecn_advance(struct mb_ecn_ctx * ctx, + uint64_t dt, + size_t len, + uint64_t ftag) +{ + uint64_t burst; + uint64_t owed; + + if (dt <= (uint64_t) CA_DT_CAP) { + ctx->vt += ctx->rate * dt / BILLION; + return; + } + + burst = ctx->rate * CA_DT_CAP / BILLION; + if (burst < (uint64_t) len) + burst = len; + + owed = ftag > ctx->vt ? ftag - ctx->vt + burst : burst; + + /* Clamp so owed * BILLION cannot wrap (2^33 B backlog). */ + if (owed > (1ULL << 33)) + owed = 1ULL << 33; + + if (dt >= owed * BILLION / ctx->rate) + ctx->vt += owed; + else + ctx->vt += ctx->rate * dt / BILLION; +} + +static time_t mb_ecn_snd(struct mb_ecn_ctx * ctx, + size_t len, + uint64_t t, + uint64_t * ftag) +{ + uint64_t dt; + uint64_t dtc; + uint64_t s; + + /* Lazy warm-up seed: packet #1 is never an idle resume. */ + if (!ctx->started) { + ctx->started = true; + ctx->last_ts = t; + ctx->snd_win = t; + ctx->snd_r0 = ctx->rate; + } + + dt = t - ctx->last_ts; + ctx->last_ts = t; + + mb_ecn_age(ctx, t); + + /* Offered-load estimator: accumulate, gate growth, size ceiling. */ + ctx->snd_byt += len; + if (ctx->snd_byt > (uint64_t) CA_SND_BYT_MAX) + ctx->snd_byt = CA_SND_BYT_MAX; + + if (dt > (uint64_t) CA_DT_CAP) + ctx->backlogged = false; + + if (t - ctx->snd_win >= (uint64_t) CA_SND_WIN) + mb_ecn_win(ctx, t); + + /* Rate update before the vt advance: burst uses the clamped rate. */ + dtc = t - ctx->last_ctrl; + if (dtc >= (uint64_t) CA_DT_CTRL) { + ctx->last_ctrl = t; + mb_ecn_ctrl(ctx, dtc); + } + + mb_ecn_advance(ctx, dt, len, *ftag); + + /* SFQ start tag: behind the clock starts now, ahead waits. */ + s = *ftag > ctx->vt ? *ftag : ctx->vt; + *ftag = s + len; + + ctx->lead = s - ctx->vt; + + /* Reciprocal pacing; folded so any lead * rate stays in range. */ + if (s > ctx->vt) + return (time_t) ((ctx->lead * (ctx->inv_rate >> 16)) + >> (CA_INV_SHFT - 16)); + + return 0; +} + +time_t mb_ecn_ctx_update_snd(void * _ctx, + size_t len, + uint8_t lecn, + uint64_t * ftag) { struct timespec now; - size_t slot; - ca_wnd_t wnd; + uint64_t t; struct mb_ecn_ctx * ctx = _ctx; clock_gettime(PTHREAD_COND_CLOCK, &now); - slot = ts_to_ns(now) >> ctx->tx_mul; + t = TS_TO_UINT64(now); - ctx->tx_ctr++; - ctx->tx_wpc++; - ctx->tx_wbc += len; + mb_ecn_loc(ctx, lecn, t); - if (ctx->tx_ctr > CA_WND) - ctx->tx_ece = 0; + return mb_ecn_snd(ctx, len, t, ftag); +} - if (_slot_after(slot, ctx->tx_slot)) { - bool carry = false; /* may carry over if window increases */ - - ctx->tx_slot = slot; - - if (!ctx->tx_cav) { /* Slow start */ - if (ctx->tx_wbc > ctx->tx_wbl) - ctx->tx_wbl <<= 1; - } else { - if (ctx->tx_ece) /* Mult. Decrease */ - ctx->tx_wbl -= (ctx->tx_wbl * ctx->tx_ece) - >> (CA_SHFT + 8); - else /* Add. Increase */ - ctx->tx_wbl = ctx->tx_wbc + ctx->tx_inc; - } - - /* Window scaling */ - if (ctx->tx_wpc < CA_MINPS) { - size_t fact = 0; /* factor to scale the window up */ - size_t pkts = ctx->tx_wpc; - while (pkts < CA_MINPS) { - pkts <<= 1; - fact++; - } - ctx->tx_mul += fact; - ctx->tx_slot >>= fact; - if ((ctx->tx_slot & ((1 << fact) - 1)) == 0) { - carry = true; - ctx->tx_slot += 1; - } - ctx->tx_wbl <<= fact; - ctx->tx_inc <<= fact; - } else if (ctx->tx_wpc > CA_MAXPS) { - size_t fact = 0; /* factor to scale the window down */ - size_t pkts = ctx->tx_wpc; - while (pkts > CA_MAXPS) { - pkts >>= 1; - fact++; - } - ctx->tx_mul -= fact; - ctx->tx_slot <<= fact; - ctx->tx_wbl >>= fact; - ctx->tx_inc >>= fact; - } else { - ctx->tx_slot = slot; - } - - if (!carry) { - ctx->tx_wbc = 0; - ctx->tx_wpc = 0; - } - } +/* Estimator idle, or a gap past ~4 current windows: restart fresh. */ +static bool mb_ecn_rcv_fresh(const struct mb_ecn_ctx * ctx, + uint64_t dt) +{ + if (ctx->rx_ece == 0 && ctx->rx_acc == 0) + return true; - if (ctx->tx_wbc > ctx->tx_wbl) - wnd.wait = ((ctx->tx_slot + 1) << ctx->tx_mul) - ts_to_ns(now); + return dt > ctx->rx_tw << CA_TW_GAP_SHFT; +} + +/* Size the next averaging window to ~CA_N_TARGET packets at this rate. */ +static void mb_ecn_resize(struct mb_ecn_ctx * ctx, + uint64_t win) +{ + uint64_t tw = CA_RX_WBYTES * win / ctx->rx_byt; + + if (tw > ctx->rx_tw) + ctx->rx_tw += (tw - ctx->rx_tw) >> CA_TW_SM_SHFT; else - wnd.wait = 0; + ctx->rx_tw -= (ctx->rx_tw - tw) >> CA_TW_SM_SHFT; - return wnd; + if (ctx->rx_tw < CA_TW_MIN) + ctx->rx_tw = CA_TW_MIN; + + if (ctx->rx_tw > CA_TW_ABSMAX) + ctx->rx_tw = CA_TW_ABSMAX; } -void mb_ecn_wnd_wait(ca_wnd_t wnd) +static bool mb_ecn_rcv(struct mb_ecn_ctx * ctx, + size_t len, + uint8_t ecn, + uint8_t cap, + uint16_t * ece, + uint8_t * fcap, + uint64_t t) { - if (wnd.wait > 0) { - struct timespec s = TIMESPEC_INIT_S(0); - if (wnd.wait > BILLION) /* Don't care throttling < 1s */ - s.tv_sec = 1; - else - s.tv_nsec = wnd.wait; + uint64_t dt; + uint64_t win; - nanosleep(&s, NULL); + dt = t - ctx->rx_ts; + ctx->rx_ts = t; + + if (ctx->rx_ece == 0 && ctx->rx_acc == 0 && ecn == 0) + return false; + + /* Onset, or ~4 windows of silence: emit fresh, undiluted. */ + if (mb_ecn_rcv_fresh(ctx, dt)) { + ctx->rx_win = t; + ctx->rx_acc = 0; + ctx->rx_byt = len; + ctx->rx_cap = cap; /* fresh, seeds the new window */ + ctx->rx_ece = (uint16_t) (ecn << CA_SHFT); + *ece = ctx->rx_ece; + *fcap = ctx->rx_cap; + return true; } + + /* Dwell clamp: one packet weighs at most one window of mark. */ + ctx->rx_acc += ecn * MIN(dt, ctx->rx_tw); + ctx->rx_byt += len; + ctx->rx_cap = cap_min(ctx->rx_cap, cap); + + *ece = ctx->rx_ece; + + win = t - ctx->rx_win; + if (win < ctx->rx_tw) { + /* Early close once 2x target bytes arrive (speed-up). */ + if (ctx->rx_byt < CA_RX_WCLOSE) + return false; + + if (win < CA_TW_MIN) + return false; + } + + /* Time-integral mean over the actual window elapsed (never rx_tw). */ + ctx->rx_ece = (uint16_t) ((ctx->rx_acc << CA_SHFT) / win); + + if (ctx->rx_byt > 0) + mb_ecn_resize(ctx, win); + + *fcap = ctx->rx_cap; + + ctx->rx_win = t; + ctx->rx_acc = 0; + ctx->rx_byt = 0; + ctx->rx_cap = 0; /* the next window starts unknown */ + + *ece = ctx->rx_ece; + + return true; } bool mb_ecn_ctx_update_rcv(void * _ctx, size_t len, uint8_t ecn, - uint16_t * ece) + uint8_t cap, + uint16_t * ece, + uint8_t * fcap) { - struct mb_ecn_ctx* ctx = _ctx; - bool update; + struct timespec now; + struct mb_ecn_ctx * ctx = _ctx; - (void) len; + clock_gettime(PTHREAD_COND_CLOCK, &now); - if ((ctx->rx_ece | ecn) == 0) - return false; + return mb_ecn_rcv(ctx, len, ecn, cap, ece, fcap, TS_TO_UINT64(now)); +} - if (ecn == 0) { /* End of congestion */ - ctx->rx_ece >>= 2; - update = ctx->rx_ece == 0; - } else { - if (ctx->rx_ece == 0) { /* Start of congestion */ - ctx->rx_ece = ecn; - ctx->rx_ctr = 0; - update = true; - } else { /* Congestion update */ - ctx->rx_ece -= ctx->rx_ece >> CA_SHFT; - ctx->rx_ece += ecn; - update = (ctx->rx_ctr++ & (CA_UPD - 1)) == true; - } +static void mb_ecn_ece(struct mb_ecn_ctx * ctx, + uint16_t ece, + uint8_t cap, + uint64_t t) +{ + uint64_t tgt; + + ctx->tx_ece = ece; + ctx->tx_cav = true; + ctx->last_fb = t; + ctx->n_fb++; + + /* Scale the floor and AI slope to the path bottleneck. */ + if (cap != 0) { + tgt = cap_dec(cap) >> CA_CAP_SHFT; + if (tgt < CA_RATE_MIN) + tgt = CA_RATE_MIN; + + if (tgt > CA_RMIN_MAX) + tgt = CA_RMIN_MAX; + + if (tgt > ctx->rate_min) + ctx->rate_min += (tgt - ctx->rate_min) + >> CA_CAP_SM_SHFT; + else + ctx->rate_min -= (ctx->rate_min - tgt) + >> CA_CAP_SM_SHFT; + + ctx->ai_rate = ctx->rate_min; + ctx->tx_cap = cap; + ctx->last_cap = t; + ctx->n_cap++; } - *ece = ctx->rx_ece; + /* Control from the feedback path: a starved sender recovers. */ + if (t - ctx->last_ctrl < (uint64_t) CA_DT_CTRL) + return; - return update; -} + mb_ecn_ctrl(ctx, t - ctx->last_ctrl); + ctx->last_ctrl = t; +} void mb_ecn_ctx_update_ece(void * _ctx, - uint16_t ece) + uint16_t ece, + uint8_t cap) { - struct mb_ecn_ctx* ctx = _ctx; + struct timespec now; + struct mb_ecn_ctx * ctx = _ctx; - ctx->tx_ece = ece; - ctx->tx_ctr = 0; - ctx->tx_cav = true; + clock_gettime(PTHREAD_COND_CLOCK, &now); + + mb_ecn_ece(ctx, ece, cap, TS_TO_UINT64(now)); } -int mb_ecn_calc_ecn(int fd, +int mb_ecn_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, size_t len) { - size_t q; + size_t q; + uint8_t mark; (void) len; (void) qc; - q = ipcp_flow_queued(fd); + /* Saturate: a queue past 255 quanta must not wrap to a low mark. */ + q = queued / CA_MARK_Q; + mark = q > 255 ? (uint8_t) 255 : (uint8_t) q; - *ecn |= (uint8_t) (q >> ECN_Q_SHFT); + if (mark > *ecn) + *ecn = mark; return 0; } @@ -262,35 +755,64 @@ ssize_t mb_ecn_print_stats(void * _ctx, char * buf, size_t len) { - struct mb_ecn_ctx* ctx = _ctx; - char * regime; + struct mb_ecn_ctx * ctx = _ctx; + char * regime; + uint64_t rate; + uint64_t peak; + int code; + uint16_t m; if (len < 1024) return 0; - if (!ctx->tx_cav) + /* No signal seen: the rate is unconstrained drift, not a target. */ + rate = ctx->tx_cav ? ctx->rate : 0; + peak = ctx->tx_cav ? ctx->ss_peak : 0; + + /* Match the controller: MD fires on m, incl. the local fallback. */ + m = ctx->tx_ece > 0 ? ctx->tx_ece + : (uint16_t) (ctx->tx_loc << CA_SHFT); + + if (!ctx->tx_cav) { regime = "Slow start"; - else if (ctx->tx_ece) - regime = "Multiplicative dec"; - else + code = 0; + } else if (ctx->src_limited) { + regime = "Source limited"; + code = 3; + } else if (m > 0) { + regime = "Proportional dec"; + code = 2; + } else { regime = "Additive inc"; + code = 1; + } sprintf(buf, "Congestion avoidance algorithm: %20s\n" "Upstream congestion level: %20u\n" - "Upstream packet counter: %20zu\n" "Downstream congestion level: %20u\n" - "Downstream packet counter: %20zu\n" - "Congestion window size (ns): %20" PRIu64 "\n" - "Packets in this window: %20zu\n" - "Bytes in this window: %20zu\n" - "Max bytes in this window: %20zu\n" - "Current congestion regime: %20s\n", + "Paced rate (bytes/s): %20" PRIu64 "\n" + "Pacer lead (bytes): %20" PRIu64 "\n" + "Congestion regime (code): %20d\n" + "Current congestion regime: %20s\n" + "Control steps (count): %20" PRIu64 "\n" + "Control time elapsed (ns): %20" PRIu64 "\n" + "Control time banked (ns): %20" PRIu64 "\n" + "Feedback updates (count): %20" PRIu64 "\n" + "Feedback timeouts (count): %20" PRIu64 "\n" + "Path capacity (bytes/s): %20" PRIu64 "\n" + "Capacity rate floor (bytes/s): %20" PRIu64 "\n" + "Capacity updates (count): %20" PRIu64 "\n" + "Slow start peak rate (bytes/s): %20" PRIu64 "\n", "Multi-bit ECN", - ctx->tx_ece, ctx->tx_ctr, - ctx->rx_ece, ctx->rx_ctr, (uint64_t) (1ULL << ctx->tx_mul), - ctx->tx_wpc, ctx->tx_wbc, ctx->tx_wbl, - regime); + ctx->tx_ece, + ctx->rx_ece, + rate, ctx->lead, code, + regime, + ctx->n_ctrl, ctx->t_ctrl, ctx->t_bank, + ctx->n_fb, ctx->n_ttl, + cap_dec(ctx->tx_cap), ctx->rate_min, ctx->n_cap, + peak); return strlen(buf); } diff --git a/src/ipcpd/unicast/ca/mb-ecn.h b/src/ipcpd/unicast/ca/mb-ecn.h index 1be27764..7bf0b29f 100644 --- a/src/ipcpd/unicast/ca/mb-ecn.h +++ b/src/ipcpd/unicast/ca/mb-ecn.h @@ -29,20 +29,23 @@ void * mb_ecn_ctx_create(void); void mb_ecn_ctx_destroy(void * ctx); -ca_wnd_t mb_ecn_ctx_update_snd(void * ctx, - size_t len); +time_t mb_ecn_ctx_update_snd(void * ctx, + size_t len, + uint8_t lecn, + uint64_t * ftag); bool mb_ecn_ctx_update_rcv(void * ctx, size_t len, uint8_t ecn, - uint16_t * ece); + uint8_t cap, + uint16_t * ece, + uint8_t * fcap); void mb_ecn_ctx_update_ece(void * ctx, - uint16_t ece); - -void mb_ecn_wnd_wait(ca_wnd_t wnd); + uint16_t ece, + uint8_t cap); -int mb_ecn_calc_ecn(int fd, +int mb_ecn_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, size_t len); diff --git a/src/ipcpd/unicast/ca/nop.c b/src/ipcpd/unicast/ca/nop.c index e5cacf66..e6965297 100644 --- a/src/ipcpd/unicast/ca/nop.c +++ b/src/ipcpd/unicast/ca/nop.c @@ -30,8 +30,8 @@ struct ca_ops nop_ca_ops = { .ctx_update_snd = nop_ctx_update_snd, .ctx_update_rcv = nop_ctx_update_rcv, .ctx_update_ece = nop_ctx_update_ece, - .wnd_wait = nop_wnd_wait, .calc_ecn = nop_calc_ecn, + .marks_ecn = false, .print_stats = NULL }; @@ -45,51 +45,52 @@ void nop_ctx_destroy(void * ctx) (void) ctx; } -ca_wnd_t nop_ctx_update_snd(void * ctx, - size_t len) +time_t nop_ctx_update_snd(void * ctx, + size_t len, + uint8_t lecn, + uint64_t * ftag) { - ca_wnd_t wnd; - (void) ctx; (void) len; + (void) lecn; + (void) ftag; - memset(&wnd, 0, sizeof(wnd)); - - return wnd; -} - -void nop_wnd_wait(ca_wnd_t wnd) -{ - (void) wnd; + return 0; } bool nop_ctx_update_rcv(void * ctx, size_t len, uint8_t ecn, - uint16_t * ece) + uint8_t cap, + uint16_t * ece, + uint8_t * fcap) { (void) ctx; (void) len; (void) ecn; + (void) cap; (void) ece; + (void) fcap; return false; } void nop_ctx_update_ece(void * ctx, - uint16_t ece) + uint16_t ece, + uint8_t cap) { (void) ctx; (void) ece; + (void) cap; } -int nop_calc_ecn(int fd, +int nop_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, size_t len) { - (void) fd; + (void) queued; (void) len; (void) ecn; (void) qc; diff --git a/src/ipcpd/unicast/ca/nop.h b/src/ipcpd/unicast/ca/nop.h index 8b892e61..6ca206df 100644 --- a/src/ipcpd/unicast/ca/nop.h +++ b/src/ipcpd/unicast/ca/nop.h @@ -29,20 +29,23 @@ void * nop_ctx_create(void); void nop_ctx_destroy(void * ctx); -ca_wnd_t nop_ctx_update_snd(void * ctx, - size_t len); +time_t nop_ctx_update_snd(void * ctx, + size_t len, + uint8_t lecn, + uint64_t * ftag); bool nop_ctx_update_rcv(void * ctx, size_t len, uint8_t ecn, - uint16_t * ece); + uint8_t cap, + uint16_t * ece, + uint8_t * fcap); void nop_ctx_update_ece(void * ctx, - uint16_t ece); - -void nop_wnd_wait(ca_wnd_t wnd); + uint16_t ece, + uint8_t cap); -int nop_calc_ecn(int fd, +int nop_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, size_t len); diff --git a/src/ipcpd/unicast/ca/ops.h b/src/ipcpd/unicast/ca/ops.h index 6d2ddf1d..a86c1b3b 100644 --- a/src/ipcpd/unicast/ca/ops.h +++ b/src/ipcpd/unicast/ca/ops.h @@ -30,24 +30,30 @@ struct ca_ops { void (* ctx_destroy)(void * ctx); - ca_wnd_t (* ctx_update_snd)(void * ctx, - size_t len); + time_t (* ctx_update_snd)(void * ctx, + size_t len, + uint8_t lecn, + uint64_t * ftag); bool (* ctx_update_rcv)(void * ctx, size_t len, uint8_t ecn, - uint16_t * ece); + uint8_t cap, + uint16_t * ece, + uint8_t * fcap); void (* ctx_update_ece)(void * ctx, - uint16_t ece); - - void (* wnd_wait)(ca_wnd_t wnd); + uint16_t ece, + uint8_t cap); - int (* calc_ecn)(int fd, + int (* calc_ecn)(size_t queued, uint8_t * ecn, qoscube_t qc, size_t len); + /* True if calc_ecn inspects the queue; gates the lookup. */ + bool marks_ecn; + /* Optional, can be NULL */ ssize_t (* print_stats)(void * ctx, char * buf, diff --git a/src/ipcpd/unicast/cap.c b/src/ipcpd/unicast/cap.c new file mode 100644 index 00000000..0d823dc6 --- /dev/null +++ b/src/ipcpd/unicast/cap.c @@ -0,0 +1,291 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Link capacity estimation + * + * Dimitri Staessens + * Sander Vrijders + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#if defined(__linux__) || defined(__CYGWIN__) +#define _DEFAULT_SOURCE +#else +#define _POSIX_C_SOURCE 200809L +#endif + +#include "config.h" + +#include +#include + +#include "cap.h" + +#include + +/* + * Link-capacity estimation by watching the egress queue drain. + * + * A saturated link drains its queue at exactly its capacity, so we + * estimate capacity by measuring the drain rate of the ring buffer + * toward an n-1 flow (the flow to the layer below) while that ring + * is backlogged. + * + * Sampling is lock-free and off the fast path: the ring depth is + * read only at enqueue time, concurrently by many sender threads. + * Each enqueue bumps relaxed counters (packets, bytes, empty-ring + * hits). At most once per CAP_T_MIN, one thread wins a try-lock and + * closes a measurement window. + * + * Over a window, packet conservation gives the slots that drained: + * drained = queue at start (q0) + enqueued - queue now (q1) + * A window stays open until CAP_N_MIN slots have drained, so its + * length self-scales with the link rate (~1 ms at 1 Gbit, ~19 ms at + * 10 Mbit). CAP_T_MAX discards a window that spanned a traffic gap. + * + * Only a backlogged link measures its own capacity, so a window + * whose ring ran mostly idle is discarded (a few empty samples, as + * from a token-bucket shaper, are tolerated). The drain rate feeds a + * max filter that jumps up at once but decays slowly, converging on + * the capacity from below. A window that touched an empty ring at + * either edge may have drained into downstream buffers faster than + * the wire, so it may only lower the estimate, never raise it. + * + * The estimate is published as a quarter-log2 code: capacity is only + * ever needed to order-of-magnitude accuracy. + */ + +#define CAP_T_MIN (BILLION / 1000) /* min fold spacing ~1 ms */ +#define CAP_T_MAX (1ULL << 27) /* stale window cap ~134 ms */ +#define CAP_N_MIN 16 /* drained slots to close */ +#define CAP_DEC_SHFT 4 /* max-filter decay 1/16 */ +#define CAP_IDL_SHFT 3 /* idle tolerance 1/8 */ + +/* Try-lock on the busy flag: test-and-set acquire, store release. */ +#define CAP_TRY(p) (__atomic_exchange_n(p, 1, __ATOMIC_ACQUIRE) == 0) +#define CAP_REL(p) (__atomic_store_n(p, 0, __ATOMIC_RELEASE)) + +struct cap_est { + uint64_t c_pkt; /* total packets enqueued (relaxed) */ + uint64_t c_byt; /* total bytes enqueued (relaxed) */ + uint64_t c_idl; /* times ring seen empty (relaxed) */ + + uint64_t t_gate; /* last fold timestamp (ns) */ + uint8_t busy; /* fold in progress (try-lock) */ + + uint64_t t0; /* window start (ns), 0 = no window */ + uint64_t q0; /* ring occupancy at window start */ + uint64_t pkt0; /* c_pkt snapshot at window start */ + uint64_t byt0; /* c_byt snapshot at window start */ + uint64_t idl0; /* c_idl snapshot at window start */ + uint64_t rate; /* filtered drain rate (bytes/s) */ + + uint8_t cap; /* published capacity code (0=none) */ +}; + +struct { + struct cap_est est[PROC_MAX_FLOWS]; +} cap; + +int cap_init(void) +{ + memset(&cap, 0, sizeof(cap)); + + return 0; +} + +void cap_fini(void) +{ +} + +void cap_reset(int fd) +{ + /* A racing update seeds one bogus window; the filter absorbs. */ + memset(&cap.est[fd], 0, sizeof(cap.est[fd])); +} + +/* + * Rate <-> 8-bit code (cap_enc / cap_dec). The code is a tiny float: + * the high 6 bits are a band e = floor(log2 rate), the low 2 bits a + * quarter k that splits each band [2^e, 2^(e+1)) into four, so + * code = 4 * e + k. Each step is ~19% in rate; that coarseness is + * deliberate, capacity only needs order-of-magnitude accuracy. + * + * The quarter cut points are 256 * 2^(k/4) rounded to an integer: + * {256, 304, 362, 431} over the normalized range [256, 512). + */ +uint8_t cap_enc(uint64_t rate) +{ + static const uint16_t thr[3] = {304, 362, 431}; + uint64_t r = rate; /* copy halved to find band */ + unsigned e = 0; /* band: floor log2 rate */ + unsigned k = 0; /* quarter within band 0..3 */ + unsigned c; /* code = 4 * band + quarter */ + uint16_t top; /* rate scaled to [256, 512) */ + + if (rate == 0) + return 0; + + while (r > 1) { + r >>= 1; + e++; + } + + /* Top 9 bits: rate normalized to [256, 512). */ + top = e >= 8 ? (uint16_t) (rate >> (e - 8)) + : (uint16_t) (rate << (8 - e)); + + while (k < 3 && top >= thr[k]) + k++; + + c = 4 * e + k; + if (c == 0) + c = 1; /* 0 means unknown */ + + return (uint8_t) c; +} + +uint64_t cap_dec(uint8_t c) +{ + static const uint16_t m[4] = {256, 304, 362, 431}; + unsigned e = c >> 2; /* band = c >> 2 */ + unsigned k = c & 3; /* quarter = c & 3 */ + + if (c == 0) + return 0; + + if (e >= 8) + return (uint64_t) m[k] << (e - 8); + + return ((uint64_t) m[k] << e) >> 8; +} + +uint8_t cap_min(uint8_t a, + uint8_t b) +{ + if (a == 0) + return b; + + if (b == 0) + return a; + + return a < b ? a : b; +} + +void cap_stamp(uint8_t * pci, + uint8_t own) +{ + if (own == 0) + return; + + if (*pci == 0 || own < *pci) + *pci = own; +} + +/* Fold flag held; q1 is the caller's pre-write ring sample. */ +static void cap_fold(struct cap_est * e, + uint64_t q1, + uint64_t now) +{ + uint64_t pkt; /* current c_pkt snapshot */ + uint64_t byt; /* current c_byt snapshot */ + uint64_t idl; /* current c_idl snapshot */ + uint64_t dt; /* window duration (ns) */ + uint64_t enq; /* packets enqueued in window */ + uint64_t avg; /* mean packet size (bytes) */ + uint64_t r; /* window drain rate (bytes/s) */ + int64_t drained; /* slots drained over window */ + + pkt = LOAD_RELAXED(&e->c_pkt); + byt = LOAD_RELAXED(&e->c_byt); + idl = LOAD_RELAXED(&e->c_idl); + + dt = now - e->t0; + enq = pkt - e->pkt0; + + drained = (int64_t) (e->q0 + enq - q1); + + if (e->t0 == 0 || dt > CAP_T_MAX || enq == 0) + goto reopen; + + if (drained < (int64_t) CAP_N_MIN) + return; /* extend the window until enough drains */ + + if ((idl - e->idl0) << CAP_IDL_SHFT > enq) + goto reopen; /* mostly idle ring: not saturated */ + + avg = (byt - e->byt0) / enq; + r = (uint64_t) drained * avg * MILLION / (dt / 1000); + + if (r >= e->rate) { + /* Empty-edged windows drain into buffers below. */ + if (e->q0 > 0 && q1 > 0) + e->rate = r; + } else { + e->rate -= (e->rate - r) >> CAP_DEC_SHFT; + } + + STORE_RELAXED(&e->cap, cap_enc(e->rate)); + reopen: + e->t0 = now; + e->q0 = q1; + e->pkt0 = pkt; + e->byt0 = byt; + e->idl0 = idl; +} + +/* Internal, timestamped entry point; tests drive this directly. */ +static void cap_update_at(int fd, + size_t qlen, + size_t len, + uint64_t now) +{ + struct cap_est * e = &cap.est[fd]; /* this flow's estimator */ + + FETCH_ADD_RELAXED(&e->c_pkt, 1); + FETCH_ADD_RELAXED(&e->c_byt, len); + + if (qlen == 0) + FETCH_ADD_RELAXED(&e->c_idl, 1); + + if (now - LOAD_RELAXED(&e->t_gate) < CAP_T_MIN) + return; + + if (!CAP_TRY(&e->busy)) + return; + + if (now - e->t_gate >= CAP_T_MIN) { + cap_fold(e, qlen, now); + STORE_RELAXED(&e->t_gate, now); + } + + CAP_REL(&e->busy); +} + +void cap_update(int fd, + size_t qlen, + size_t len) +{ + struct timespec now; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + cap_update_at(fd, qlen, len, TS_TO_UINT64(now)); +} + +uint8_t cap_get(int fd) +{ + return LOAD_RELAXED(&cap.est[fd].cap); +} diff --git a/src/ipcpd/unicast/cap.h b/src/ipcpd/unicast/cap.h new file mode 100644 index 00000000..df8c2ec1 --- /dev/null +++ b/src/ipcpd/unicast/cap.h @@ -0,0 +1,54 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Link capacity estimation + * + * Dimitri Staessens + * Sander Vrijders + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#ifndef OUROBOROS_IPCPD_UNICAST_CAP_H +#define OUROBOROS_IPCPD_UNICAST_CAP_H + +#include +#include + +int cap_init(void); + +void cap_fini(void); + +/* Account an egress packet; qlen is sampled before the write. */ +void cap_update(int fd, + size_t qlen, + size_t len); + +uint8_t cap_get(int fd); + +void cap_reset(int fd); + +/* Quarter-log2 capacity code: ~2^(c / 4) bytes/s, 0 = unknown. */ +uint8_t cap_enc(uint64_t rate); + +uint64_t cap_dec(uint8_t c); + +uint8_t cap_min(uint8_t a, + uint8_t b); + +/* MIN-combine the own link code into the PCI byte. */ +void cap_stamp(uint8_t * pci, + uint8_t own); + +#endif /* OUROBOROS_IPCPD_UNICAST_CAP_H */ diff --git a/src/ipcpd/unicast/dir/dht.c b/src/ipcpd/unicast/dir/dht.c index b17218c4..c2cd33aa 100644 --- a/src/ipcpd/unicast/dir/dht.c +++ b/src/ipcpd/unicast/dir/dht.c @@ -2238,7 +2238,7 @@ static int dht_send_msg(dht_msg_t * msg, dht_msg__pack(msg, ssm_pk_buff_head(spb)); - if (dt_write_packet(addr, QOS_CUBE_BE, dht.eid, spb) < 0) { + if (dt_write_packet(addr, QOS_CUBE_BE, dht.eid, spb, NULL) < 0) { log_warn("%s write failed", DHT_CODE(msg)); goto fail_send; } diff --git a/src/ipcpd/unicast/dt.c b/src/ipcpd/unicast/dt.c index 0acae05e..6afaa1f9 100644 --- a/src/ipcpd/unicast/dt.c +++ b/src/ipcpd/unicast/dt.c @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #ifdef IPCP_FLOW_STATS @@ -46,6 +47,7 @@ #include "common/comp.h" #include "common/connmgr.h" #include "ca.h" +#include "cap.h" #include "ipcp.h" #include "dt.h" #include "pff.h" @@ -78,12 +80,14 @@ struct comp_info { #define TTL_LEN 1 #define QOS_LEN 1 #define ECN_LEN 1 +#define CAP_LEN 1 struct dt_pci { uint64_t dst_addr; qoscube_t qc; uint8_t ttl; uint8_t ecn; + uint8_t cap; uint64_t eid; }; @@ -96,6 +100,7 @@ struct { size_t qc_o; size_t ttl_o; size_t ecn_o; + size_t cap_o; size_t eid_o; /* Initial TTL value */ @@ -115,6 +120,7 @@ static void dt_pci_ser(uint8_t * head, memcpy(head + dt_pci_info.qc_o, &dt_pci->qc, QOS_LEN); memcpy(head + dt_pci_info.ttl_o, &ttl, TTL_LEN); memcpy(head + dt_pci_info.ecn_o, &dt_pci->ecn, ECN_LEN); + memcpy(head + dt_pci_info.cap_o, &dt_pci->cap, CAP_LEN); memcpy(head + dt_pci_info.eid_o, &dt_pci->eid, dt_pci_info.eid_size); } @@ -133,6 +139,7 @@ static void dt_pci_des(uint8_t * head, memcpy(&dt_pci->qc, head + dt_pci_info.qc_o, QOS_LEN); memcpy(&dt_pci->ttl, head + dt_pci_info.ttl_o, TTL_LEN); memcpy(&dt_pci->ecn, head + dt_pci_info.ecn_o, ECN_LEN); + memcpy(&dt_pci->cap, head + dt_pci_info.cap_o, CAP_LEN); memcpy(&dt_pci->eid, head + dt_pci_info.eid_o, dt_pci_info.eid_size); } @@ -301,12 +308,12 @@ static int dt_rib_readdir(char *** buf) if (*buf == NULL) goto fail_entries; - for (i = 0; i < PROC_MAX_FLOWS; ++i) { + for (i = 0; i < PROC_MAX_FLOWS && idx < (int) dt.n_flows; ++i) { pthread_mutex_lock(&dt.stat[i].lock); if (dt.stat[i].stamp == 0) { pthread_mutex_unlock(&dt.stat[i].lock); - break; + continue; /* n-1 flows start at PROC_RES_FDS */ } pthread_mutex_unlock(&dt.stat[i].lock); @@ -328,6 +335,7 @@ static int dt_rib_readdir(char *** buf) fail_entry: while (idx-- > 0) free((*buf)[idx]); + free(*buf); fail_entries: pthread_rwlock_unlock(&dt.lock); @@ -413,6 +421,7 @@ static void handle_event(void * self, #ifdef IPCP_FLOW_STATS stat_used(fd, c->conn_info.addr); #endif + cap_reset(fd); psched_add(dt.psched, fd); log_dbg("Added fd %d to packet scheduler.", fd); break; @@ -429,15 +438,17 @@ static void handle_event(void * self, } } -static void packet_handler(int fd, - qoscube_t qc, - struct ssm_pk_buff * spb) +static time_t packet_handler(int fd, + qoscube_t qc, + struct ssm_pk_buff * spb) { struct dt_pci dt_pci; int ret; int ofd; uint8_t * head; size_t len; + size_t qlen; + bool marks; len = ssm_pk_buff_len(spb); @@ -456,7 +467,7 @@ static void packet_handler(int fd, log_dbg("TTL was zero."); ipcp_spb_release(spb); dt_stat_inc(fd, r_drp, qc, len); - return; + return 0; } /* FIXME: Use qoscube from PCI instead of incoming flow. */ @@ -466,10 +477,16 @@ static void packet_handler(int fd, dt_pci.dst_addr); ipcp_spb_release(spb); dt_stat_inc(fd, f_nhp, qc, len); - return; + return 0; } - (void) ca_calc_ecn(ofd, head + dt_pci_info.ecn_o, qc, len); + marks = ca_marks_ecn(); + qlen = marks ? ipcp_flow_queued(ofd) : 0; + + (void) ca_calc_ecn(qlen, head + dt_pci_info.ecn_o, qc, len); + + if (marks) + cap_stamp(head + dt_pci_info.cap_o, cap_get(ofd)); ret = ipcp_flow_write(ofd, spb); if (ret < 0) { @@ -478,23 +495,27 @@ static void packet_handler(int fd, notifier_event(NOTIFY_DT_FLOW_DOWN, &ofd); ipcp_spb_release(spb); dt_stat_inc(ofd, w_drp, qc, len); - return; + return 0; } dt_stat_inc(ofd, snd, qc, len); + + if (marks) + cap_update(ofd, qlen, len); } else { dt_pci_shrink(spb); if (dt_pci.eid >= PROC_RES_FDS) { uint8_t ecn = *(head + dt_pci_info.ecn_o); - fa_np1_rcv(dt_pci.eid, ecn, spb); - return; + uint8_t cap = *(head + dt_pci_info.cap_o); + fa_np1_rcv(dt_pci.eid, ecn, cap, spb); + return 0; } if (dt.comps[dt_pci.eid].post_packet == NULL) { log_err("No registered component on eid %" PRIu64 ".", dt_pci.eid); ipcp_spb_release(spb); - return; + return 0; } dt_stat_inc(fd, lcl_r, qc, len); dt_stat_inc(dt_pci.eid, snd, qc, len); @@ -502,6 +523,8 @@ static void packet_handler(int fd, dt.comps[dt_pci.eid].post_packet(dt.comps[dt_pci.eid].comp, spb); } + + return 0; } static void * dt_conn_handle(void * o) @@ -560,9 +583,15 @@ int dt_init(struct dt_config cfg) dt_pci_info.qc_o = dt_pci_info.addr_size; dt_pci_info.ttl_o = dt_pci_info.qc_o + QOS_LEN; dt_pci_info.ecn_o = dt_pci_info.ttl_o + TTL_LEN; - dt_pci_info.eid_o = dt_pci_info.ecn_o + ECN_LEN; + dt_pci_info.cap_o = dt_pci_info.ecn_o + ECN_LEN; + dt_pci_info.eid_o = dt_pci_info.cap_o + CAP_LEN; dt_pci_info.head_size = dt_pci_info.eid_o + dt_pci_info.eid_size; + if (cap_init() < 0) { + log_err("Failed to init capacity estimator."); + goto fail_cap; + } + if (connmgr_comp_init(COMPID_DT, &info)) { log_err("Failed to register with connmgr."); goto fail_connmgr_comp_init; @@ -642,6 +671,8 @@ int dt_init(struct dt_config cfg) fail_routing: connmgr_comp_fini(COMPID_DT); fail_connmgr_comp_init: + cap_fini(); + fail_cap: return -1; } @@ -671,6 +702,8 @@ void dt_fini(void) routing_fini(); connmgr_comp_fini(COMPID_DT); + + cap_fini(); } int dt_start(void) @@ -773,13 +806,16 @@ void dt_unreg_comp(int eid) int dt_write_packet(uint64_t dst_addr, qoscube_t qc, uint64_t eid, - struct ssm_pk_buff * spb) + struct ssm_pk_buff * spb, + uint8_t * ecn) { struct dt_pci dt_pci; int fd; int ret; uint8_t * head; size_t len; + size_t qlen; + bool marks; assert(spb); assert(dst_addr != dt.addr); @@ -813,8 +849,18 @@ int dt_write_packet(uint64_t dst_addr, dt_pci.qc = qc; dt_pci.eid = eid; dt_pci.ecn = 0; + dt_pci.cap = 0; + + marks = ca_marks_ecn(); + qlen = marks ? ipcp_flow_queued(fd) : 0; + + (void) ca_calc_ecn(qlen, &dt_pci.ecn, qc, len); + + if (marks) + dt_pci.cap = cap_get(fd); - (void) ca_calc_ecn(fd, &dt_pci.ecn, qc, len); + if (ecn != NULL) + *ecn = dt_pci.ecn; dt_pci_ser(head, &dt_pci); @@ -828,14 +874,19 @@ int dt_write_packet(uint64_t dst_addr, #ifdef IPCP_FLOW_STATS if (dt_pci.eid < PROC_RES_FDS) dt_stat_inc(fd, lcl_w, qc, len); + dt_stat_inc(fd, snd, qc, len); #endif + if (marks) + cap_update(fd, qlen, len); + return 0; fail_write: #ifdef IPCP_FLOW_STATS if (eid < PROC_RES_FDS) dt_stat_inc(fd, lcl_w, qc, len); + dt_stat_inc(fd, w_drp, qc, len); #endif return -1; diff --git a/src/ipcpd/unicast/dt.h b/src/ipcpd/unicast/dt.h index a484377d..a055efea 100644 --- a/src/ipcpd/unicast/dt.h +++ b/src/ipcpd/unicast/dt.h @@ -48,6 +48,7 @@ void dt_unreg_comp(int eid); int dt_write_packet(uint64_t dst_addr, qoscube_t qc, uint64_t eid, - struct ssm_pk_buff * spb); + struct ssm_pk_buff * spb, + uint8_t * ecn); #endif /* OUROBOROS_IPCPD_UNICAST_DT_H */ diff --git a/src/ipcpd/unicast/fa.c b/src/ipcpd/unicast/fa.c index c6eca175..ac2ecaea 100644 --- a/src/ipcpd/unicast/fa.c +++ b/src/ipcpd/unicast/fa.c @@ -31,6 +31,7 @@ #define FA "flow-allocator" #define OUROBOROS_PREFIX FA +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -81,6 +83,7 @@ struct fa_msg { uint32_t max_gap; uint32_t timeout; uint16_t ece; + uint8_t cap; uint8_t code; uint8_t availability; uint8_t service; @@ -109,6 +112,8 @@ struct fa_flow { uint64_t r_eid; /* Remote endpoint id */ uint64_t r_addr; /* Remote address */ void * ctx; /* Congestion avoidance context */ + uint64_t fair; /* SFQ virtual finish tag (bytes) */ + uint8_t l_ecn; /* Local first-hop mark (relaxed) */ }; struct { @@ -245,6 +250,7 @@ static int fa_rib_readdir(char *** buf) fail_entry: while (idx-- > 0) free((*buf)[idx]); + free(*buf); fail_entries: pthread_rwlock_unlock(&fa.flows_lock); @@ -317,18 +323,21 @@ static uint64_t gen_eid(int fd) return ((uint64_t) rnd << 32) + fd; } -static void packet_handler(int fd, - qoscube_t qc, - struct ssm_pk_buff * spb) +static time_t packet_handler(int fd, + qoscube_t qc, + struct ssm_pk_buff * spb) { struct fa_flow * flow; uint64_t r_addr; uint64_t r_eid; - ca_wnd_t wnd; + time_t wait; size_t len; + uint8_t ecn; flow = &fa.flows[fd]; + ecn = 0; + pthread_rwlock_wrlock(&fa.flows_lock); len = ssm_pk_buff_len(spb); @@ -337,16 +346,16 @@ static void packet_handler(int fd, ++flow->p_snd; flow->b_snd += len; #endif - wnd = ca_ctx_update_snd(flow->ctx, len); + wait = ca_ctx_update_snd(flow->ctx, len, + LOAD_RELAXED(&flow->l_ecn), &flow->fair); r_addr = flow->r_addr; r_eid = flow->r_eid; pthread_rwlock_unlock(&fa.flows_lock); - ca_wnd_wait(wnd); - - if (dt_write_packet(r_addr, qc, r_eid, spb)) { + if (dt_write_packet(r_addr, qc, r_eid, spb, &ecn)) { + STORE_RELAXED(&flow->l_ecn, ecn); ipcp_spb_release(spb); log_dbg("Failed to forward packet."); #ifdef IPCP_FLOW_STATS @@ -355,8 +364,12 @@ static void packet_handler(int fd, flow->b_snd_f += len; pthread_rwlock_unlock(&fa.flows_lock); #endif - return; + return wait; } + + STORE_RELAXED(&flow->l_ecn, ecn); + + return wait; } static int fa_flow_init(struct fa_flow * flow) @@ -370,9 +383,7 @@ static int fa_flow_init(struct fa_flow * flow) flow->s_eid = -1; flow->r_addr = INVALID_ADDR; - flow->ctx = ca_ctx_create(); - if (flow->ctx == NULL) - return -1; + /* ctx is acquired once (r_addr, qc) are known (ca_ctx_get). */ #ifdef IPCP_FLOW_STATS clock_gettime(CLOCK_REALTIME_COARSE, &now); @@ -386,7 +397,8 @@ static int fa_flow_init(struct fa_flow * flow) static void fa_flow_fini(struct fa_flow * flow) { - ca_ctx_destroy(flow->ctx); + if (flow->ctx != NULL) + ca_ctx_put(flow->ctx); memset(flow, 0, sizeof(*flow)); @@ -503,6 +515,13 @@ static int fa_handle_flow_req(struct fa_msg * msg, flow->r_eid = ntoh64(msg->s_eid); flow->r_addr = ntoh64(msg->s_addr); + flow->ctx = ca_ctx_get(flow->r_addr, qos_spec_to_cube(qs)); + if (flow->ctx == NULL) { + fa_flow_fini(flow); + pthread_rwlock_unlock(&fa.flows_lock); + return -ENOMEM; + } + pthread_rwlock_unlock(&fa.flows_lock); return fd; @@ -580,7 +599,7 @@ static int fa_handle_flow_update(struct fa_msg * msg, #ifdef IPCP_FLOW_STATS flow->u_rcv++; #endif - ca_ctx_update_ece(flow->ctx, ntoh16(msg->ece)); + ca_ctx_update_ece(flow->ctx, ntoh16(msg->ece), msg->cap); pthread_rwlock_unlock(&fa.flows_lock); @@ -834,7 +853,7 @@ int fa_alloc(int fd, if (data->len > 0) memcpy(ssm_pk_buff_head(spb) + len, data->data, data->len); - if (dt_write_packet(addr, qc, fa.eid, spb)) { + if (dt_write_packet(addr, qc, fa.eid, spb, NULL)) { log_err("Failed to send flow allocation request packet."); ipcp_spb_release(spb); return -1; @@ -848,6 +867,13 @@ int fa_alloc(int fd, flow->r_addr = addr; flow->s_eid = eid; + flow->ctx = ca_ctx_get(addr, qos_spec_to_cube(qs)); + if (flow->ctx == NULL) { + fa_flow_fini(flow); + pthread_rwlock_unlock(&fa.flows_lock); + return -1; + } + pthread_rwlock_unlock(&fa.flows_lock); return 0; @@ -890,7 +916,7 @@ int fa_alloc_resp(int fd, pthread_rwlock_unlock(&fa.flows_lock); - if (dt_write_packet(flow->r_addr, qc, fa.eid, spb)) { + if (dt_write_packet(flow->r_addr, qc, fa.eid, spb, NULL)) { log_err("Failed to send flow allocation response packet."); goto fail_packet; } @@ -944,7 +970,7 @@ int fa_irm_update(int fd, pthread_rwlock_unlock(&fa.flows_lock); - if (dt_write_packet(r_addr, qc, fa.eid, spb)) { + if (dt_write_packet(r_addr, qc, fa.eid, spb, NULL)) { log_err("Failed to send flow update packet."); ipcp_spb_release(spb); return -1; @@ -972,7 +998,8 @@ int fa_dealloc(int fd) } static int fa_update_remote(int fd, - uint16_t ece) + uint16_t ece, + uint8_t cap) { struct fa_msg * msg; struct ssm_pk_buff * spb; @@ -996,6 +1023,7 @@ static int fa_update_remote(int fd, msg->code = FLOW_UPDATE; msg->r_eid = hton64(flow->r_eid); msg->ece = hton16(ece); + msg->cap = cap; r_addr = flow->r_addr; #ifdef IPCP_FLOW_STATS @@ -1004,7 +1032,7 @@ static int fa_update_remote(int fd, pthread_rwlock_unlock(&fa.flows_lock); - if (dt_write_packet(r_addr, qc, fa.eid, spb)) { + if (dt_write_packet(r_addr, qc, fa.eid, spb, NULL)) { log_err("Failed to send flow update packet."); ipcp_spb_release(spb); return -1; @@ -1015,11 +1043,13 @@ static int fa_update_remote(int fd, void fa_np1_rcv(uint64_t eid, uint8_t ecn, + uint8_t cap, struct ssm_pk_buff * spb) { struct fa_flow * flow; bool update; uint16_t ece; + uint8_t fcap; int fd; size_t len; @@ -1041,7 +1071,7 @@ void fa_np1_rcv(uint64_t eid, ++flow->p_rcv; flow->b_rcv += len; #endif - update = ca_ctx_update_rcv(flow->ctx, len, ecn, &ece); + update = ca_ctx_update_rcv(flow->ctx, len, ecn, cap, &ece, &fcap); pthread_rwlock_unlock(&fa.flows_lock); @@ -1057,5 +1087,5 @@ void fa_np1_rcv(uint64_t eid, } if (update) - fa_update_remote(eid, ece); + fa_update_remote(eid, ece, fcap); } diff --git a/src/ipcpd/unicast/fa.h b/src/ipcpd/unicast/fa.h index f31b40e9..1d0012b0 100644 --- a/src/ipcpd/unicast/fa.h +++ b/src/ipcpd/unicast/fa.h @@ -50,6 +50,7 @@ int fa_irm_update(int fd, void fa_np1_rcv(uint64_t eid, uint8_t ecn, + uint8_t cap, struct ssm_pk_buff * spb); #endif /* OUROBOROS_IPCPD_UNICAST_FA_H */ diff --git a/src/ipcpd/unicast/psched.c b/src/ipcpd/unicast/psched.c index 21e23617..2b535d67 100644 --- a/src/ipcpd/unicast/psched.c +++ b/src/ipcpd/unicast/psched.c @@ -30,6 +30,7 @@ #include #include +#include #include "common/connmgr.h" #include "ipcp.h" @@ -50,7 +51,7 @@ static int qos_prio [] = { #endif struct psched { - fset_t * set[QOS_CUBE_MAX]; + fset_t * set[QOS_CUBE_MAX * IPCP_SCHED_THR_MUL]; next_packet_fn_t callback; read_fn_t read; pthread_t readers[QOS_CUBE_MAX * IPCP_SCHED_THR_MUL]; @@ -59,23 +60,139 @@ struct psched { struct sched_info { struct psched * sch; qoscube_t qc; + size_t idx; }; +/* Map an FD to one reader's set: one FD, one thread (no shared FDs). */ +static size_t fd_set_idx(int fd, qoscube_t qc) +{ + return qc + ((size_t) fd % IPCP_SCHED_THR_MUL) * QOS_CUBE_MAX; +} + static void cleanup_reader(void * o) { fqueue_destroy((fqueue_t *) o); } +/* + * Per-reader deadline scheduler: a paced flow is served, then deferred + * to its next-send deadline instead of blocking the thread, so it never + * stalls its thread-mates. + */ +struct dsched { + uint64_t deadline[PROC_MAX_FLOWS]; /* absolute ns, per tracked fd */ + int active[PROC_MAX_FLOWS]; /* compact list of tracked fds */ + int posn[PROC_MAX_FLOWS]; /* fd -> active index, -1 = none */ + size_t n; +}; + +static void cleanup_dsched(void * o) +{ + free(o); +} + +static void dsched_track(struct dsched * d, + int fd, + uint64_t deadline) +{ + if (d->posn[fd] >= 0) + return; + + d->deadline[fd] = deadline; + d->posn[fd] = (int) d->n; + d->active[d->n++] = fd; +} + +static void dsched_untrack(struct dsched * d, + int fd) +{ + int i = d->posn[fd]; + + if (i < 0) + return; + + d->active[i] = d->active[--d->n]; + d->posn[d->active[i]] = i; + d->posn[fd] = -1; +} + +static uint64_t dsched_serve(struct dsched * d, + struct psched * sched, + qoscube_t qc, + uint64_t now) +{ + struct ssm_pk_buff * spb; + uint64_t dmin = 0; + size_t i; + int fd; + time_t wait; + + for (i = 0; i < d->n; ) { + fd = d->active[i]; + + if (d->deadline[fd] > now) { + if (dmin == 0 || d->deadline[fd] < dmin) + dmin = d->deadline[fd]; + ++i; + continue; + } + + if (sched->read(fd, &spb) < 0) { + dsched_untrack(d, fd); + continue; + } + + wait = sched->callback(fd, qc, spb); + if (wait == 0) + continue; + + d->deadline[fd] = now + (uint64_t) wait; + if (dmin == 0 || d->deadline[fd] < dmin) + dmin = d->deadline[fd]; + ++i; + } + + return dmin; +} + +static void dsched_events(struct dsched * d, + fqueue_t * fq, + uint64_t now) +{ + int fd; + + while ((fd = fqueue_next(fq)) >= 0) { + switch (fqueue_type(fq)) { + case FLOW_DEALLOC: + dsched_untrack(d, fd); + notifier_event(NOTIFY_DT_FLOW_DEALLOC, &fd); + break; + case FLOW_DOWN: + notifier_event(NOTIFY_DT_FLOW_DOWN, &fd); + break; + case FLOW_UP: + notifier_event(NOTIFY_DT_FLOW_UP, &fd); + break; + case FLOW_PKT: + dsched_track(d, fd, now); + break; + default: + break; + } + } +} + static void * packet_reader(void * o) { - struct psched * sched; - struct ssm_pk_buff * spb; - int fd; - fqueue_t * fq; - qoscube_t qc; + struct psched * sched; + struct dsched * d; + fqueue_t * fq; + qoscube_t qc; + size_t idx; sched = ((struct sched_info *) o)->sch; qc = ((struct sched_info *) o)->qc; + idx = ((struct sched_info *) o)->idx; ipcp_lock_to_core(); @@ -85,36 +202,50 @@ static void * packet_reader(void * o) if (fq == NULL) return (void *) -1; + d = malloc(sizeof(*d)); + if (d == NULL) { + fqueue_destroy(fq); + return (void *) -1; + } + + memset(d, 0, sizeof(*d)); + memset(d->posn, 0xFF, sizeof(d->posn)); /* -1: nothing tracked yet */ + + pthread_cleanup_push(cleanup_dsched, d); pthread_cleanup_push(cleanup_reader, fq); while (true) { - int ret = fevent(sched->set[qc], fq, NULL); + struct timespec now_ts; + struct timespec to; + struct timespec * timeo; + uint64_t now; + uint64_t dmin; + uint64_t delta; + int ret; + + clock_gettime(PTHREAD_COND_CLOCK, &now_ts); + + now = TS_TO_UINT64(now_ts); + + dmin = dsched_serve(d, sched, qc, now); + + if (dmin == 0) { + timeo = NULL; + } else { + delta = dmin > now ? dmin - now : 1; + to.tv_sec = (time_t) (delta / BILLION); + to.tv_nsec = (long) (delta % BILLION); + timeo = &to; + } + + ret = fevent(sched->set[idx], fq, timeo); if (ret < 0) continue; - while ((fd = fqueue_next(fq)) >= 0) { - switch (fqueue_type(fq)) { - case FLOW_DEALLOC: - notifier_event(NOTIFY_DT_FLOW_DEALLOC, &fd); - break; - case FLOW_DOWN: - notifier_event(NOTIFY_DT_FLOW_DOWN, &fd); - break; - case FLOW_UP: - notifier_event(NOTIFY_DT_FLOW_UP, &fd); - break; - case FLOW_PKT: - if (sched->read(fd, &spb) < 0) - continue; - - sched->callback(fd, qc, spb); - break; - default: - break; - } - } + dsched_events(d, fq, now); } + pthread_cleanup_pop(true); pthread_cleanup_pop(true); return (void *) 0; @@ -137,7 +268,7 @@ struct psched * psched_create(next_packet_fn_t callback, psched->callback = callback; psched->read = read; - for (i = 0; i < QOS_CUBE_MAX; ++i) { + for (i = 0; i < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++i) { psched->set[i] = fset_create(); if (psched->set[i] == NULL) { for (j = 0; j < i; ++j) @@ -155,6 +286,7 @@ struct psched * psched_create(next_packet_fn_t callback, } infos[i]->sch = psched; infos[i]->qc = i % QOS_CUBE_MAX; + infos[i]->idx = i; } for (i = 0; i < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++i) { @@ -196,11 +328,12 @@ struct psched * psched_create(next_packet_fn_t callback, fail_sched: for (j = 0; j < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++j) pthread_cancel(psched->readers[j]); + for (j = 0; j < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++j) pthread_join(psched->readers[j], NULL); #endif fail_infos: - for (j = 0; j < QOS_CUBE_MAX; ++j) + for (j = 0; j < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++j) fset_destroy(psched->set[j]); fail_flow_set: free(psched); @@ -219,7 +352,7 @@ void psched_destroy(struct psched * psched) pthread_join(psched->readers[i], NULL); } - for (i = 0; i < QOS_CUBE_MAX; ++i) + for (i = 0; i < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++i) fset_destroy(psched->set[i]); free(psched); @@ -233,7 +366,7 @@ void psched_add(struct psched * psched, assert(psched); ipcp_flow_get_qoscube(fd, &qc); - fset_add(psched->set[qc], fd); + fset_add(psched->set[fd_set_idx(fd, qc)], fd); } void psched_del(struct psched * psched, @@ -244,5 +377,5 @@ void psched_del(struct psched * psched, assert(psched); ipcp_flow_get_qoscube(fd, &qc); - fset_del(psched->set[qc], fd); + fset_del(psched->set[fd_set_idx(fd, qc)], fd); } diff --git a/src/ipcpd/unicast/psched.h b/src/ipcpd/unicast/psched.h index d83bb793..8c2914b3 100644 --- a/src/ipcpd/unicast/psched.h +++ b/src/ipcpd/unicast/psched.h @@ -26,9 +26,9 @@ #include #include -typedef void (* next_packet_fn_t)(int fd, - qoscube_t qc, - struct ssm_pk_buff * spb); +typedef time_t (* next_packet_fn_t)(int fd, + qoscube_t qc, + struct ssm_pk_buff * spb); typedef int (* read_fn_t)(int fd, struct ssm_pk_buff ** spb); -- cgit v1.2.3