diff options
Diffstat (limited to 'src/ipcpd/unicast/ca/tests')
| -rw-r--r-- | src/ipcpd/unicast/ca/tests/ca_test.c | 14 | ||||
| -rw-r--r-- | src/ipcpd/unicast/ca/tests/mb_ecn_test.c | 357 |
2 files changed, 353 insertions, 18 deletions
diff --git a/src/ipcpd/unicast/ca/tests/ca_test.c b/src/ipcpd/unicast/ca/tests/ca_test.c index 4c44d29e..1b86eab8 100644 --- a/src/ipcpd/unicast/ca/tests/ca_test.c +++ b/src/ipcpd/unicast/ca/tests/ca_test.c @@ -44,7 +44,7 @@ static int test_ca_init_fini(enum pol_cong_avoid pol, { TEST_START("(%s)", name); - if (ca_init(pol) < 0) { + if (ca_init(pol, 100) < 0) { printf("Failed to init ca for %s.\n", name); goto fail; } @@ -74,7 +74,7 @@ static int test_ca_init_invalid(void) { TEST_START(); - if (ca_init(CA_INVALID) == 0) { + if (ca_init(CA_INVALID, 100) == 0) { printf("Init accepted an invalid policy.\n"); ca_fini(); goto fail; @@ -96,7 +96,7 @@ static int test_ca_ctx_share(enum pol_cong_avoid pol, TEST_START("(%s)", name); - if (ca_init(pol) < 0) { + if (ca_init(pol, 100) < 0) { printf("Failed to init ca for %s.\n", name); goto fail; } @@ -162,7 +162,7 @@ static int test_ca_ctx_distinct(void) TEST_START(); - if (ca_init(CA_NONE) < 0) { + if (ca_init(CA_NONE, 100) < 0) { printf("Failed to init ca.\n"); goto fail; } @@ -226,7 +226,7 @@ static int test_ca_ctx_refcount(void) TEST_START(); - if (ca_init(CA_NONE) < 0) { + if (ca_init(CA_NONE, 100) < 0) { printf("Failed to init ca.\n"); goto fail; } @@ -283,7 +283,7 @@ static int test_ca_ctx_recreate(void) TEST_START(); - if (ca_init(CA_NONE) < 0) { + if (ca_init(CA_NONE, 100) < 0) { printf("Failed to init ca.\n"); goto fail; } @@ -340,7 +340,7 @@ static int test_ca_fini_drains(void) TEST_START(); - if (ca_init(CA_NONE) < 0) { + if (ca_init(CA_NONE, 100) < 0) { printf("Failed to init ca.\n"); goto fail; } diff --git a/src/ipcpd/unicast/ca/tests/mb_ecn_test.c b/src/ipcpd/unicast/ca/tests/mb_ecn_test.c index 8e3a73da..4bbc12aa 100644 --- a/src/ipcpd/unicast/ca/tests/mb_ecn_test.c +++ b/src/ipcpd/unicast/ca/tests/mb_ecn_test.c @@ -21,7 +21,6 @@ */ #include "mb-ecn.c" - #include <test/test.h> #define MS (MILLION) /* one millisecond in ns */ @@ -41,6 +40,7 @@ static struct mb_ecn_ctx * mk_ctx(void) ctx->last_ts = 0; ctx->last_ctrl = 0; ctx->last_fb = 0; + ctx->last_sig = 0; ctx->last_loc = 0; ctx->last_cap = 0; @@ -51,6 +51,7 @@ static struct mb_ecn_ctx * mk_ctx(void) ctx->backlogged = true; ctx->src_limited = false; ctx->started = false; + ctx->ss_tc = 20 * MS; /* fixed slope for deterministic SS */ return ctx; } @@ -815,7 +816,7 @@ static int test_mb_ecn_rcv_no_overflow_highrate(void) /* * The sender holds a mark across the full inter-feedback gap (TTL > - * 2 * CA_TW_INIT) and a repeated mark must not re-fire the one-sided lead. + * 2 * CA_TW_INIT); a repeated mark adds only the proportional cut. */ static int test_mb_ecn_ece_ttl_covers_cadence(void) { @@ -1048,10 +1049,13 @@ static int test_mb_ecn_rate_floor(void) goto fail; } - /* Cut larger than headroom must clamp; stay inside CA_ECE_TTL. */ + /* + * A starved 600 ms at full mark takes the rate/2 branch, far + * below the floor; stays inside the initial ~976 ms mark TTL. + */ ctx->rate = CA_RATE_MIN + 1000; mb_ecn_ece(ctx, CA_ECE_REF, 0, 0); - mb_ecn_snd(ctx, LEN, 30 * MS, &ftag); + mb_ecn_snd(ctx, LEN, 600 * MS, &ftag); if (ctx->rate != CA_RATE_MIN) { printf("rate floor breached: %" PRIu64 ".\n", ctx->rate); @@ -1116,8 +1120,9 @@ static int test_mb_ecn_fixed_point(void) } /* - * One-sided lead: a mark rise cuts rate * rise / (REF * DEN) once; - * a flat or falling mark leaves only the dt-scaled proportional cut. + * A rising ECE mark takes a one-sided lead cut on top of the + * proportional term; a flat or falling mark takes only the small + * proportional cut. */ static int test_mb_ecn_lead_cut(void) { @@ -1137,17 +1142,17 @@ static int test_mb_ecn_lead_cut(void) ctx->rate = (uint64_t) 100 << 20; ctx->tx_cav = true; - /* Rise 0 -> 256: lead cuts ~rise/(REF*DEN) = 1/8 of the rate. */ + /* Rise 0 -> 256: the lead term cuts hard (~rate/8). */ prev = ctx->rate; mb_ecn_ece(ctx, 256, 0, MS); mb_ecn_snd(ctx, LEN, MS, &ftag); drop = prev - ctx->rate; - if (drop < prev / 10) { - printf("lead cut missing: dropped %" PRIu64 ".\n", drop); + if (drop < prev / 16) { + printf("rising mark under-cut: %" PRIu64 ".\n", drop); goto fail_ctx; } - /* Flat mark: rise 0, only the ~0.05%% proportional cut. */ + /* Flat mark: no rise, only the proportional cut. */ prev = ctx->rate; mb_ecn_ece(ctx, 256, 0, 2 * MS); mb_ecn_snd(ctx, LEN, 2 * MS, &ftag); @@ -1157,7 +1162,7 @@ static int test_mb_ecn_lead_cut(void) goto fail_ctx; } - /* Falling mark: one-sided lead must not fire. */ + /* Falling mark: no rise, no lead beyond the proportional term. */ prev = ctx->rate; mb_ecn_ece(ctx, 64, 0, 3 * MS); mb_ecn_snd(ctx, LEN, 3 * MS, &ftag); @@ -1421,6 +1426,69 @@ static int test_mb_ecn_decrease_dt_invariant(void) return TEST_RC_FAIL; } +/* Sub-ms control steps must not lose decrease time to truncation. */ +static int test_mb_ecn_decrease_subms_carry(void) +{ + struct mb_ecn_ctx * a; + struct mb_ecn_ctx * b; + uint64_t cut_a; + uint64_t cut_b; + uint64_t r0; + uint64_t fta = 0; + uint64_t ftb = 0; + size_t i; + + TEST_START(); + + a = mk_ctx(); + b = mk_ctx(); + if (a == NULL || b == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + r0 = (uint64_t) 100 << 20; + a->rate = r0; + b->rate = r0; + + mb_ecn_ece(a, 256, 0, 0); + mb_ecn_ece(b, 256, 0, 0); + + /* Same 30 ms of marked time; b's steps have a 0.5 ms tail. */ + for (i = 1; i <= 10; i++) + mb_ecn_snd(a, LEN, i * 3 * MS, &fta); + + for (i = 1; i <= 20; i++) + mb_ecn_snd(b, LEN, i * 3 * MS / 2, &ftb); + + cut_a = r0 - a->rate; + cut_b = r0 - b->rate; + + if (cut_a == 0 || cut_b == 0) { + printf("no cut: %" PRIu64 " %" PRIu64 ".\n", cut_a, cut_b); + goto fail_ctx; + } + + /* Within 10%: the sub-ms remainder must carry, not vanish. */ + if (cut_a > cut_b + cut_a / 10 || cut_b > cut_a + cut_a / 10) { + printf("sub-ms decrease lost: %" PRIu64 " vs %" PRIu64 + ".\n", cut_a, cut_b); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + TEST_FAIL(); + return TEST_RC_FAIL; +} + /* Resuming after a long idle gap: bounded AI, no cut from stale marks. */ static int test_mb_ecn_idle_resume_bounded(void) { @@ -2528,6 +2596,269 @@ static int test_mb_ecn_first_send_warmup(void) return TEST_RC_FAIL; } +/* + * Couple one or two backlogged flows through a shared bottleneck of + * capacity cap: each step marks the shared queue with the real + * calc_ecn, feeds each flow that mark delayed by its own lag (in + * steps), drives it backlogged for one step, then drains the queue. + * A faithful discrete run of the fluid model on the real control law, + * with the forwarder abstracted to a single shared price. b may be + * NULL for a single-flow run. + */ +#define TF_STEP (5 * MS) /* control step (ns) */ +#define TF_HIST 64 /* mark ring, bounds max lag */ +#define TF_QMAX (8192 * LEN) /* bottleneck buffer (bytes) */ +#define TF_MAXN 32 /* flows per shared-link run */ + +static void shared_link_run(struct mb_ecn_ctx * a, + struct mb_ecn_ctx * b, + uint64_t cap, + size_t lag_a, + size_t lag_b, + size_t steps) +{ + uint16_t hist[TF_HIST]; + uint64_t ta = 0; + uint64_t tb = 0; + uint64_t fta = 0; + uint64_t ftb = 0; + uint64_t q = 0; + uint64_t drain = cap * TF_STEP / BILLION; + uint64_t tgt; + uint64_t arr; + uint8_t cc = cap_enc(cap); + uint8_t ecn; + uint16_t ea; + uint16_t eb; + size_t k; + + memset(hist, 0, sizeof(hist)); + + for (k = 0; k < steps; k++) { + tgt = (k + 1) * TF_STEP; + ecn = 0; + + mb_ecn_calc_ecn(q / LEN, &ecn, QOS_CUBE_BE, LEN); + hist[k % TF_HIST] = (uint16_t) (ecn << CA_SHFT); + + ea = k < lag_a ? 0 : hist[(k - lag_a) % TF_HIST]; + if (ea > 0) /* no feedback until congestion */ + mb_ecn_ece(a, ea, cc, ta); + + /* Active flow: heartbeat/window liveness stays fresh. */ + a->last_sig = ta; + if (tgt > ta) + ta = drive_backlogged(a, &fta, ta, tgt - ta, LEN); + + arr = a->rate * TF_STEP / BILLION; + + if (b != NULL) { + eb = k < lag_b ? 0 : hist[(k - lag_b) % TF_HIST]; + if (eb > 0) + mb_ecn_ece(b, eb, cc, tb); + + b->last_sig = tb; + if (tgt > tb) + tb = drive_backlogged(b, &ftb, tb, + tgt - tb, LEN); + + arr += b->rate * TF_STEP / BILLION; + } + + q += arr; + q = q > drain ? q - drain : 0; + if (q > (uint64_t) TF_QMAX) + q = TF_QMAX; + } +} + +/* + * Two flows sharing one bottleneck, both fed every queue mark, must + * converge from a lopsided start toward an equal split (the fluid + * model's drho/dt -> 0) rather than latch winner-take-all. Isolates + * the rate law from the forwarder: neither flow is starved of marks. + */ +static int test_mb_ecn_two_flow_converge(void) +{ + struct mb_ecn_ctx * a; + struct mb_ecn_ctx * b; + uint64_t cap = 1ULL << 20; + uint64_t fair = (1ULL << 20) / 2; + uint64_t lo; + uint64_t hi; + + TEST_START(); + + a = mk_ctx(); + b = mk_ctx(); + if (a == NULL || b == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + a->rate = cap; /* a hogs, b starts small */ + b->rate = CA_RATE_INIT; + + shared_link_run(a, b, cap, 0, 0, 6000); + + lo = a->rate < b->rate ? a->rate : b->rate; + hi = a->rate > b->rate ? a->rate : b->rate; + + if (lo < fair / 4) { + printf("flow starved: %" PRIu64 " / %" PRIu64 ".\n", + a->rate, b->rate); + goto fail_ctx; + } + + if (hi > 3 * lo) { + printf("did not converge: %" PRIu64 " / %" PRIu64 ".\n", + a->rate, b->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * A remote bottleneck exits slow start only on fed-back congestion, + * so a longer feedback delay lets the exponential ramp overshoot + * further: the slow-start peak grows with RTT. Nothing caps the ramp + * at the path capacity, so the sender latches near the first-hop rate. + */ +static int test_mb_ecn_ramp_overshoot_grows_with_rtt(void) +{ + struct mb_ecn_ctx * lo_rtt; + struct mb_ecn_ctx * hi_rtt; + uint64_t cap = 1ULL << 20; + + TEST_START(); + + lo_rtt = mk_ctx(); + hi_rtt = mk_ctx(); + if (lo_rtt == NULL || hi_rtt == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + /* Same bottleneck; feedback lags 1 vs 8 steps (~5 vs 40 ms). */ + shared_link_run(lo_rtt, NULL, cap, 1, 0, 2000); + shared_link_run(hi_rtt, NULL, cap, 8, 0, 2000); + + if (hi_rtt->ss_peak <= lo_rtt->ss_peak) { + printf("overshoot did not grow with RTT: %" PRIu64 + " vs %" PRIu64 ".\n", + hi_rtt->ss_peak, lo_rtt->ss_peak); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(lo_rtt); + mb_ecn_ctx_destroy(hi_rtt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(lo_rtt); + mb_ecn_ctx_destroy(hi_rtt); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Washout damps a fixed 1/4 of the rate change once per wall-clock + * bucket: a sub-bucket step banks time only, a full bucket removes a + * quarter of the gap either way without crossing the snapshot, and a + * sparse step (> CA_DT_CAP) resets it so a starved sender keeps its cut. + */ +static int test_mb_ecn_washout_bucket(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t r0 = (uint64_t) 100 << 20; + uint64_t gap; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + gap = r0 >> 4; + + /* Sub-bucket: time banks, the rate does not move. */ + ctx->rate = r0; + ctx->r_bkt = r0 - gap; + ctx->wash_acc = 0; + mb_ecn_washout(ctx, MS, MS); + if (ctx->rate != r0 || ctx->wash_acc != MS) { + printf("sub-bucket washout moved the rate: %" PRIu64 ".\n", + ctx->rate); + goto fail_ctx; + } + + /* Full bucket, rate leads: cut a quarter of the gap, no crossing. */ + ctx->rate = r0; + ctx->r_bkt = r0 - gap; + ctx->wash_acc = 0; + mb_ecn_washout(ctx, MS, CA_WASH_BKT); + if (ctx->rate != r0 - (gap >> CA_WASH_SHFT)) { + printf("bucket down: got %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + if (ctx->rate <= r0 - gap) { + printf("washout reversed a ramp: %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + if (ctx->r_bkt != ctx->rate || ctx->wash_acc != 0) { + printf("washout did not reset the bucket.\n"); + goto fail_ctx; + } + + /* Full bucket, rate trails: add a quarter of the gap (symmetric). */ + ctx->rate = r0; + ctx->r_bkt = r0 + gap; + ctx->wash_acc = 0; + mb_ecn_washout(ctx, MS, CA_WASH_BKT); + if (ctx->rate != r0 + (gap >> CA_WASH_SHFT)) { + printf("bucket up: got %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + /* Sparse step: reset to the current rate, keep the cut. */ + ctx->rate = r0; + ctx->r_bkt = r0 - gap; + ctx->wash_acc = CA_WASH_BKT / 2; + mb_ecn_washout(ctx, CA_DT_CAP + 1, MS); + if (ctx->rate != r0 || ctx->r_bkt != r0 || ctx->wash_acc != 0) { + printf("sparse step did not reset the bucket.\n"); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + int mb_ecn_test(int argc, char ** argv) { @@ -2561,6 +2892,7 @@ int mb_ecn_test(int argc, ret |= test_mb_ecn_starved_decrease_escape(); ret |= test_mb_ecn_starved_local_fallback(); ret |= test_mb_ecn_decrease_dt_invariant(); + ret |= test_mb_ecn_decrease_subms_carry(); ret |= test_mb_ecn_idle_resume_bounded(); ret |= test_mb_ecn_rate_floor(); ret |= test_mb_ecn_ece_staleness(); @@ -2583,6 +2915,9 @@ int mb_ecn_test(int argc, ret |= test_mb_ecn_max_filter(); ret |= test_mb_ecn_idle_clears_backlogged(); ret |= test_mb_ecn_first_send_warmup(); + ret |= test_mb_ecn_two_flow_converge(); + ret |= test_mb_ecn_ramp_overshoot_grows_with_rtt(); + ret |= test_mb_ecn_washout_bucket(); return ret; } |
