Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added papers/c4-alone-creeping-up.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added papers/c4-alone-stable-rtt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added papers/c4-good-estimator-no-creep.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added papers/c4-vs-c4-badf-2025-12-11.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions papers/refining-rate-measurement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Refining Time-Based congestion

Our November 2025 design of C4 focused on two big ideas:
monitor the "max RTT", which deals with competition and with Wi-Fi jitters;
and avoid the "delay creep" inherent to loosening the Congestion Window
and the max RTT by pacing transmission at or below the "nominal rate".
Problem is, we were not entirely successful, as shown in the graph
below:

![qvis graph showing RTT creeping up](./c4-alone-creeping-up.png "C4 alone with RTT creeping up")

This is an example of the RTT spiraling up. A delay increase is
misinterpreted as coming from external source, the nominal max
RTT increases, the CWND gets larger, and by the next cycle the
RTT increases again. As we can see on the graph, this only stops
when the bottleneck's queue is full, which is a case of bufferbloat.
We have to do better.

## Trying to react and correct

Our first approach was to "react and correct". We tried to detect
occurences of "delay creep". The idea was to find variables
that could be monitored easily and used to detect conditions
like "the pacing rate is too high" before queues become too large,
build excessive delays, or cause packet losses.

The "number of bytes in transit" could be an example of such variable.
In the graph above, we see that number creep up with each cycle
of cruising, pushing and recovery. If we saw continous increase
over a cycle, we could trigger a congestion signal
and reduced the nominal rate by a small amount by 1/32. That worked, "almost".
It fixed the "buffer bloat" issue, as seen in the graph below, but there
were side effects detected on other tests.

![qvis graph showing RTT staying stable](./c4-alone-stable-rtt.png "C4 alone with stable RTT")

Some tests showed that we might have over corrected.
First, the test of "media transmission over bad Wi-Fi" showed a slight
worsening of the average media frame transmission delay.
The test of two connections competing on a bad Wi-Fi channel also
shows slightly degraded results. The graph above points to the reason.
The first connection quickly adapts to the Wi-Fi condition, stamping
out the second connection.

![Competition between two C4 connections over bad Wi-Fi](./c4-vs-c4-badf-2025-12-11.png "Two C4 connections over bad Wi-Fi")

The chart of bytes in flight shows that
the first connection is building big queues, and that the RTT increase
to 300 to 400ms. This is clearly too much, as we know that the
simulation does not create more than 250ms in jitter. This point to
misinterpreting delays as jitter instead of congestion, and thus a
need to improve the disambiguation test, to distinguish between
"external causes" such competition or jitter and "internal causes" such
as excessive bandwidth.

We tried, but it didn't work. We instrumented the code to track
many variables, and try to select some that were correlated with our
conditions, but did not find them. We collected lots of simulation
traces covering the span of scenarios in which C4 is expected to
operate, designing the simulations so we could easily detect
the "data rate too large" condition. In the end, the best
observable variables were only weakly correlated with that condition.
Too bad.

## Fixing the bandwidth measurements

The measurement campaign did not succeed in isolating a couple
of neat variables correlated with excess bandwidth, but it brought
an interesting observation. Our data rate assessment code had a
tendency to overestimate the available bandwidth, with errors
of 5% or more being quite common. In our traces, we also
plotted the result of the data rate estimator built in
picoquic and already used in the picoquic implementation
of BBR, and that one appeared much closer to the ground
truth, and actually always lower than the known maximum value.
This pointed to a simple fix: just use the built-in code,
instead of trying to replicate it. And it certainly improved
the result, as seen in this trace of a simple C4 connection:

![C4 trace using built-in rate estimator](./c4-good-estimator-no-creep.png "C4 trace using built-in rate estimator")

That worked! Of course, it does not mean that we are finished with
this issue. We still see excessive delays when a C4 flow competes against
another C4 flow, and it would be nice to fix that. Just relying
on the correctness of the rate estimator is a kind of "open loop" control,
and it would also be nice to fix that. But with a good rate estimator, the
results are already pretty good.








86 changes: 86 additions & 0 deletions papers/revisiting-c4-initial-phase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Revisiting C4's Initial phase

Our November 2025 design of C4 included a "rate based"
initial phase, during which C4 will send at twice the "nominal rate",
monitor acknowledgments and increase the nominal rate if measurements
increase, and exit if congestion is detected or if the measurements
do not increase for 3 consecutive RTT. That algorithm works
well in most scenario, but we were observing early exits in
"high delay jitter" scenarios, such as Wi-Fi networks with lots of
packet collisions.

After observing that phenomenon, we realized that the
rate based algorithm was failing in case of high delay jitter
because it was setting the CWND to the product of pacing rate
and the "nominal" max RTT. The nominal Max RTT was set to a fixed
value, observed either before the initial phase or on the first
roundtrip in that phase. It would work if the initial phase
started during a high jitter event and the initial RTT was large
enough, but in many case it was not and became a limiting
factor.

## Why not increasing Max RTT during Initial phase?


In the initial phase, the algorithm tries to discover the bandwidth
and does not yet have a good estimate of delay jitter, which typically
requires a series of measurements. In these conditions, it is
easy to underestimate the max RTT. On the other hand, the flow is
deliberately probing at a high data rate. If the algorithm
allows updates of max RTT during that phase, the risks of
spiraling into buffer boat are very high, but if the CWND
remains too low, the risk of exiting startup with a severely
underestimated data rate is also very high.

We tried to develop simple rules to classify the delay measurements
between caused by jitter, and caused by congestion. If we could do that,
we would be able to increase the max RTT safely, when appropriate.
However, we could not find variables that were both easy to monitor
and well correlated with the actual cause of the delay.


## Building a robust initial estimator

The "rate based" initial estimator requires estimating both the
data rate and the max RTT simultaneously. In contrast, the "CWND based"
initial estimator use in algorithms like Reno or Cubic
only requires estimating the CWND, plus a possibly
loose estimate of the data rate. The Reno algorithm is remarkably
simple: just increase the CWND by the number of bytes acknowledged,
without any explicit dependency on the measured latency.

The Reno algorithm terminates when packet losses are observed,
leading to bufferbloat. Hystart improves that by terminating when
the measured delays start increasing, but this can lead to early
exit in case of delay jitter. The rate based algorithm terminate when
the measured bandwidth stops growing, which provides good
results. Our proposal is to combine a Reno like growth of the
CWND with a rate-control like exit condition.

Of course, things are not that simple. The "rate" test only stops the
growth of the CWND after the third "non growing" round. If CWND doubles
after each round it becomes excessive, buffers fill up, and lots
of packets are lost. We dealt with that problem by essentially
freezing the increases of after the first "non growing" round.
If a larger measurement happens before 3 RTT, the increases
resume, otherwise, C4 exits the initial phase.

When the initial phase completes, we retain as estimate of the
data rate the highest value measured so far.
We also want to obtain a reasonable estimate of the "max RTT".
In the Reno logic, the "ssthresh" is set to half the CWND
value before congestion is detected. C4 will not use the
ssthresh variable after exiting the Initial phase, but it
can compute set the max RTT to the quotient of ssthresh by the
final rate estimate.

## Further work

The CWND based algorithm improves on the rate based algorithm
because it does not requires estimating the RTT of the connection.
It can absorb jitter events, and keep growing in the following RTT.





Loading