MAC collapse at one radio head's receiver: EDCA contention vs granted access

This is one offered load, seen from a single radio head's receiver. 50 client stations transmit to that one head; the head is the receiver and is not drawn. Everything below is what reaches it: a bundle received, a bundle missed, or nothing. Both strands run the same slot clock (120 ms), the same station count, and literally the same arrivals: one draw per station per slot feeds both, so the same station receives the same burst at the same instant on each and the only difference downstream is how the two systems handle it. Channel capacity is 1000 / SLOT_MS = 8.3 A-MPDU/s per strand, which at a 2.000 ms TXOP limit is about 91 MPDUs an aggregate. The unit of work is the A-MPDU; the packets fill it in, and how many fit is set by the TXOP limit the beacon carries. Each system is drawn as two rows: the packets queued in the clients' stacks, and what reaches the head on the air. Only the air carries aggregates - in a queue a packet is just a packet, and the A-MPDU does not exist until the MAC builds one, so only the air row is bundled and outlined. All three rows count packets on one shared scale — the queue rows per station, the airtime row the aggregate that went — so a packet is the same length of light wherever it is drawn, and blue accumulating into an aggregate is the same quantity that leaves as green. Each system is drawn in the three stages a packet passes through inside the network: blue waiting in the concentrator's 802.11 forwarding plane above the L2 drivers, yellow posted to a radio head's ring by L2 DMA and waiting on EDCA arbitration, green arriving as receiver airtime. The two systems differ in where they keep their packets: contention posts everything down immediately and queues in the MAC, while a grant holds the packets in the forwarding plane until the air is available.

What switching on L4S does and does not do here. Marking shortens the queue and slows the sender together, and W = L/λ, so for a single saturating flow the wait it reports is largely unchanged — which is what the whole-path total shows, and it is arithmetic rather than a defect. L4S earns its keep for other traffic sharing the path: a short latency-sensitive flow sitting behind a shallow queue instead of a deep one. This page runs one traffic class, so it can show the queue getting shorter and cannot show who benefits from that. What it can show is where the shortening stops. Marking reaches everything above the L2 drivers and nothing below them: the frozen population is untouched at any mark rate, and the contention strand's wait stays in the tens of seconds because its binding constraint is not queue depth at all, it is EDCA arbitration collapse. Granted access moves the same wait by more than an order of magnitude, which no marking scheme in the IP queue can do.

Every layer re‑quantizes the copy. The end‑to‑end goal is to copy a message from the memory of one machine to the memory of another, and each layer between implements that copy in its own natural unit: a write on the sender, a packet in the forwarding plane, a descriptor in the driver's ring, a TXOP on the air. Those units are not interchangeable, and the boundaries between them are where the interesting things happen — a packet can be marked and a descriptor cannot, a descriptor is posted one at a time and a TXOP succeeds or fails whole. Each row below draws its unit of work broken out from the queue behind it, because the unit is also what separates work from waiting: the time that unit takes is processing, and everything queued behind it is delay.

The rows are a pipeline, and it only runs one way. No writes, no packets. No packets, no DMAs. No DMAs, no EDCA arbitration. No arbitration, no airtime transmissions. Every row can only be occupied because the row above it was occupied first, and an aggregate on the air is never anything but packets that were written, queued, posted and then arbitrated for — it is consumed out of a DMA, never created by one. At a low enough offered load you can watch the stages light in that order, one packet at a time.

None of those three network stages is on the sender. The sender is upstream, with a socket buffer of its own, and it is the only thing in the whole path that a congestion signal can actually slow. That is why the pipeline below carries it as a separate stage: marks are written in the concentrator's forwarding plane, and the only party able to answer them is somewhere else entirely. TCP's CCA runs as fast as possible, so with a classic sender that socket buffer stays empty and every byte it has is already inside the network. Only ECN slows the sender, and so only ECN moves the waiting back out of the network and into the host, where nothing else queues behind it.

That boundary is where an ECN mark gets bound. A host can schedule a DMA but cannot touch the packet afterwards, so CE has to be written before the post — everything in the blue row can still be marked, and nothing in the yellow row can. Marking is therefore early-bound, and how early depends on how long the packets sit below the decision. Contention posts everything immediately, so its marks are bound far ahead of the airtime they are meant to describe and are stale by the time the aggregate actually goes, after backoff and retries. A grant holds packets in the IP stack until the air is available, so the mark is bound close to the transmission it refers to. The depth of the yellow row is the size of that stale population. Green is in the 802.11 MAC: arbitrating or transmitting. Red is a miss: two bundles went at once and the head received neither, so both are drawn. One light is one packet and a bundle is one A-MPDU, which is the countable thing - exactly one bundle can be received in a slot, and red means two or more tried and none arrived. Yellow is a grant that opened onto nothing, one light because it carries no packets at all. Dark is nothing to send. The contention strand holds nothing back, so it is only ever green or red; blue and yellow are what a scheduler looks like. Those four colours are the whole palette. The contention window is reported as a number below rather than as a shade, so nothing competes with them.
Build guide — bill of materials, wiring and the Arduino sketch for the physical two-strand version.

Offered 0.0 A-MPDU/s  ·  probability 0.0000 per station per slot  ·  capacity 8.3 A-MPDU/s
0.0OFFERED A-MPDU/s
0.0CONTENTION DELIVERED/s
0.0GRANTED DELIVERED/s
1.00ATTEMPTS PER DELIVERY · CONTENTION
1.00ATTEMPTS PER DELIVERY · GRANTED
0CONTENDERS IN THE MAC · CONTENTION
0CONTENDERS IN THE MAC · GRANTED
15MEAN CW · CONTENTION
-MEAN WAIT L/λ · CONTENTION
-MEAN WAIT L/λ · GRANTED
offL4S MARKING · CONTENTION / GRANTED
0 / 0STATIONS WAITING IP / DMA · CONTENTION
0 / 0STATIONS WAITING IP / DMA · GRANTED
0AGGREGATE FROZEN MARKS · CONTENTION
0AGGREGATE FROZEN MARKS · GRANTED
0AGGREGATE IP PACKETS · CONTENTION
0AGGREGATE IP PACKETS · GRANTED
Strand A: EDCA contention. Uniform backoff in [0, CW], freeze while the medium is busy, transmit at zero; colliders double CW and redraw.
send buffer on the sender, per station · outside the network, so none of it is in any queue in the path · what is held plus what is crossing this slot, so an empty row means nothing was offered — an AFAP sender holds nothing, and sits at the one-light minimum · red line is the socket buffer full, past which the application blocks on write · the bright rule is one write0 held · 0 crossing
▼a write crosses into the forwarding plane
IP stack in the concentrator's 802.11 forwarding plane, per station · not on the sender — already inside the network, still above the L2 drivers and still markable · red line is one station's queue full0 packets
▼descriptors posted — marks freeze here
L2 DMA, the station being served · descriptors posted to its radio head's ring and waiting on EDCA arbitration, each one's mark frozen the moment it was posted · the transfer itself is microseconds, so this wait is arbitration · red line is one ring full — nothing can hold yellow off it0 packets
▼the aggregate wins the medium and goes
receiver airtime · what reaches the head, bundled into A-MPDUsPER 0.0% · 0 packets
Little's law pipeline · one λ for every stage, so L = λW holds at each and the stages add · only the middle two are in the network, and they are what iperf 2 reports as bytes in flightλ - pkt/s leaving
stageatomL, packetsqueueingprocessingshare of the wait
send buffer, on the sender upstreamone write0--
IP stack, in the concentratorone packet0--
EDCA arbitration + L2 DMA, frozenone descriptor0--
airtime, in serviceone TXOP0--
in the network0--what every other flow queues behind
whole path0--plus what the sender is holding
held by the sender · outside the network, so no queue in the path is any deeper for it · red line is the socket buffer full0 / 0 packets
unit of work
one write
▼
depth against bloat, in the network, per station · markable plus frozen · red line is both queues full · the bright rule is one A-MPDU, what leaves per TXOP, so the fill is how many TXOPs of backlog are standing0 / 0 packets
unit of work
one A-MPDU
▼
the whole path's wait, on a log axis because the two strands are decades apart · the bright rule is the processing time, so everything to the right of it is queueing-
unit of work
one TXOP, 2 ms
Delivered 0/s  ·  collision slots 0/s  ·  mean CW of queued stations -
Strand B: granted access. One grant per slot, fixed round-robin over all lights, blind to queue state; CW stays pinned at 15.
send buffer on the sender, per station · outside the network, so none of it is in any queue in the path · what is held plus what is crossing this slot, so an empty row means nothing was offered — an AFAP sender holds nothing, and sits at the one-light minimum · red line is the socket buffer full, past which the application blocks on write · the bright rule is one write0 held · 0 crossing
▼a write crosses into the forwarding plane
IP stack in the concentrator's 802.11 forwarding plane, per station · not on the sender — already inside the network, still above the L2 drivers and still markable · red line is one station's queue full0 packets
▼descriptors posted — marks freeze here
L2 DMA, the station being served · only the granted station is ever posted, and the whole of it goes on its grant — there is no arbitration to wait on0 packets
▼the aggregate wins the medium and goes
receiver airtime · what reaches the head, bundled into A-MPDUsPER 0.0% · 0 packets
Little's law pipeline · one λ for every stage, so L = λW holds at each and the stages add · only the middle two are in the network, and they are what iperf 2 reports as bytes in flightλ - pkt/s leaving
stageatomL, packetsqueueingprocessingshare of the wait
send buffer, on the sender upstreamone write0--
IP stack, in the concentratorone packet0--
grant wait + L2 DMA, frozenone descriptor0--
airtime, in serviceone TXOP0--
in the network0--what every other flow queues behind
whole path0--plus what the sender is holding
held by the sender · outside the network, so no queue in the path is any deeper for it · red line is the socket buffer full0 / 0 packets
unit of work
one write
▼
depth against bloat, in the network, per station · markable plus frozen · red line is both queues full · the bright rule is one A-MPDU, what leaves per TXOP, so the fill is how many TXOPs of backlog are standing0 / 0 packets
unit of work
one A-MPDU
▼
the whole path's wait, on a log axis because the two strands are decades apart · the bright rule is the processing time, so everything to the right of it is queueing-
unit of work
one TXOP, 2 ms
Delivered 0/s  ·  grants to empty queues 0/s
Delivered vs offered load. Measured points fill in as load sweeps. The dashed line is the ideal demand-aware scheduler: delivered equals offered up to capacity, then capacity. Strand A rolls over as collisions double windows; strand B tracks offered on delivery but pays the yellow (unused grants) at low load. The crossover is the argument for demand-aware granting.
nothing to send packets waiting in the concentrator's forwarding plane — marks still mutable a station holding posted packets — marks frozen receiver airtime missed: two bundles collided