Nageru 1.6.1 is on its way, and what was intended
to only be a release centered around monitoring improvements (more
specifically a full set of native
Prometheus]
metrics) actually ended up getting a fairly substantial change to how Nageru
manages its frame queues.
To understand what's changing and why, it's useful to first understand the
history of Nageru's queue management. Nageru 1.0.0 started out with a fairly
simple scheme, but with some basics that are still relevant today: One of the
input cards was deemed the master card, and whenever it delivers a frame,
the master clock ticks and an output frame is produced. (There are some
subtleties about dropped frames and/or the master card changing frame rates,
but I'm going to ignore them, since they're not important to the
discussion.)
To this end, every card keeps a preallocated frame queue; when a card
delivers a frame, it's put into the queue, and when the master clock ticks,
it tries picking out one frame from each of the other card's queues to mix
together. Note that mix here could be as simple as picking one input and
throwing all the other ones away; the queueing algorithm doesn't care,
it just feeds all of them to the theme and lets that run whatever GPU code
it needs to match the user's preferences.
The only thing that really keeps the queues bounded is that the frames in
them are preallocated (in GPU memory), so if one queue gets longer than 16
frames, Nageru starts dropping it. But is 16 the right number? There are
two conflicting demands here, ignoring memory usage:
- You want to keep the latency down.
- You don't want to run out of frames in the queue if you can avoid it;
if you drop too aggressively, you could find yourself at the next frame
with nothing in the queue, because the input card hasn't delivered it yet
when the master card ticks. (You could argue one should delay the output
in this case, but for how long? And if you're using HDMI/SDI output,
you have no such luxury.)
The 1.0.0 scheme does about as well as one could possibly hope in never dropping
frames, but unfortunately, it can be pretty poor at latency. For instance,
if your master card runs at 50 Hz and you have a 60 Hz card, the latter will
eventually build up a delay of 16 * 16.7 ms = 266.7 ms clearly unacceptable,
and rather unneeded.
You could ask the user to specify a queue length, but the user probably
doesn't know, and also shouldn't really have to care more knobs to twiddle
are a bad thing, and even more so knobs the user is
expected to twiddle.
Thus, Nageru 1.2.0 introduced queue autotuning; it keeps a running estimate
on how big the queue needs to be to avoid underruns, simply based on
experience. If we've been dropping frames on a queue and then there's an
underrun, the safe queue length is increased by one, and if the queue has
been having excess frames for more than a thousand successive master clock
ticks, we reduce it by one again. Whenever the queue has more than this
safe number, we drop frames.
This was simple, effective and largely fixed the problem. However, when
adding metrics, I noticed a peculiar effect: Not all of my devices have
equally good clocks. In particular, when setting up for 1080p50, my output
card's internal clock (which assumes the role of the master clock when using
HDMI/SDI output) seems to tick at about 49.9998 Hz, and my simple home
camcorder delivers frames at about 49.9995 Hz. Over the course of an hour,
this means it produces one more frame than you should have which should
of course be dropped. Having an SDI setup with synchronized clocks
(blackburst/tri-level) would of course fix this problem, but most people are
not so lucky with their cameras, not to mention the price of PC graphics
cards with SDI outputs!
However, this happens very slowly, which means that for a significant amount
of time, the two clocks will
very nearly be in sync, and thus racing.
Who ticks first is determined largely by luck in the jitter (normal is
maybe 1ms, but occasionally, you'll see delayed delivery of as much as 10 ms),
and this means that the 1000 frames estimate is likely to be thrown off,
and the result is hundreds of dropped frames and underruns in that period.
Once the clocks have diverged enough again, you're off the hook, but again,
this isn't a good place to be.
Thus, Nageru 1.6.1 change the algorithm around yet again, by incorporating
more data to build an explicit jitter model. 1.5.0 was already timestamping
each frame to be able to measure end-to-end latency precisely (now also
exposed in Prometheus metrics), but from 1.6.1, they are actually used in
the queueing algorithm. I ran several eight- to twelve-hour tests and simply
stored all the event arrivals to a file, and then simulated a few different
algorithms (including the old algorithm) to see how they fared in measures
such as latency and number of drops/underruns.
I won't go into the full details of the new queueing algorithm (see
the commit
if you're interested), but the gist
is: Based on the last 5000 frames, it tries to estimate the maximum possible
jitter for each input (ie., how late the frame could possibly be). Based on
this as well as clock offsets, it determines whether it's really sure that
there will be an input frame available on the next master tick even if it
drops the queue, and then trims the queue to fit.
The result is pretty satisfying; here's the end-to-end latency of my camera
being sent through to the SDI output:

As you can see, the latency goes up, up, up until Nageru figures it's now
safe to drop a frame, and then does it in one clean drop event; no more
hundreds on drops involved. There
are very late frame arrivals involved in
this run two extra frame drops, to be precise but the algorithm simply
determines immediately that they are outliers, and drops them without letting
them linger in the queue. (Immediate dropping is usually preferred to
sticking around for a bit and then dropping it later, as it means you only
get one disturbance event in your stream as opposed to two. Of course, you
can only do it if you're reasonably sure it won't lead to more underruns
later.)
Nageru 1.6.1 will ship before
Solskogen, as I
intend to run it there :-) And there will probably be lovely premade
Grafana dashboards from the Prometheus data.
Although it would have been a lot nicer if Grafana were
more packaging-friendly, so I could pick it up from stock Debian and run it on
armhf. Hrmf. :-)