Packages

Elixir MOQ client bindings over Rustler NIFs with split publisher/subscriber sessions and draft-14 subgroup/datagram delivery.

Current section

Files

Jump to
moqx lib moqx demo_debug_stats.ex
Raw

lib/moqx/demo_debug_stats.ex

defmodule MOQX.DemoDebugStats do
@moduledoc false
alias MOQX.Debug
defstruct [
:started_at_ms,
:bytes,
:objects,
:groups,
:latency_count,
:latency_sum_ms,
:latency_min_ms,
:latency_max_ms
]
@type t :: %__MODULE__{
started_at_ms: integer(),
bytes: non_neg_integer(),
objects: non_neg_integer(),
groups: MapSet.t(non_neg_integer()),
latency_count: non_neg_integer(),
latency_sum_ms: non_neg_integer(),
latency_min_ms: non_neg_integer() | nil,
latency_max_ms: non_neg_integer() | nil
}
@spec new(integer()) :: t()
def new(started_at_ms \\ System.system_time(:millisecond)) when is_integer(started_at_ms) do
%__MODULE__{
started_at_ms: started_at_ms,
bytes: 0,
objects: 0,
groups: MapSet.new(),
latency_count: 0,
latency_sum_ms: 0,
latency_min_ms: nil,
latency_max_ms: nil
}
end
@spec add_frame(t(), non_neg_integer(), binary(), integer()) :: t()
def add_frame(
%__MODULE__{} = stats,
group_id,
payload,
now_ms \\ System.system_time(:millisecond)
)
when is_integer(group_id) and group_id >= 0 and is_binary(payload) and is_integer(now_ms) do
stats =
%{
stats
| bytes: stats.bytes + byte_size(payload),
objects: stats.objects + 1,
groups: MapSet.put(stats.groups, group_id)
}
case Debug.publisher_age_ms(payload, now_ms) do
{:ok, latency_ms} ->
%{
stats
| latency_count: stats.latency_count + 1,
latency_sum_ms: stats.latency_sum_ms + latency_ms,
latency_min_ms: min_latency(stats.latency_min_ms, latency_ms),
latency_max_ms: max_latency(stats.latency_max_ms, latency_ms)
}
{:error, _reason} ->
stats
end
end
@spec snapshot(t(), integer()) :: map()
def snapshot(%__MODULE__{} = stats, ended_at_ms) when is_integer(ended_at_ms) do
duration_ms = max(ended_at_ms - stats.started_at_ms, 1)
duration_s = duration_ms / 1_000
%{
duration_ms: duration_ms,
bytes_per_sec: stats.bytes / duration_s,
bitrate_kbps: stats.bytes * 8 / duration_s / 1_000,
groups_per_sec: MapSet.size(stats.groups) / duration_s,
objects_per_sec: stats.objects / duration_s,
latency: latency_snapshot(stats)
}
end
@spec format_snapshot(map()) :: String.t()
def format_snapshot(snapshot) do
latency_text =
case snapshot.latency do
nil ->
"n/a"
latency ->
"avg=#{latency.avg_ms}ms min=#{latency.min_ms}ms max=#{latency.max_ms}ms n=#{latency.samples}"
end
"bw=#{format_rate(snapshot.bytes_per_sec)} B/s (#{format_rate(snapshot.bitrate_kbps)} kbps) " <>
"groups/s=#{format_rate(snapshot.groups_per_sec)} objects/s=#{format_rate(snapshot.objects_per_sec)} " <>
"latency=#{latency_text}"
end
defp latency_snapshot(%__MODULE__{latency_count: 0}), do: nil
defp latency_snapshot(%__MODULE__{} = stats) do
%{
samples: stats.latency_count,
avg_ms: div(stats.latency_sum_ms, stats.latency_count),
min_ms: stats.latency_min_ms,
max_ms: stats.latency_max_ms
}
end
defp format_rate(value), do: :erlang.float_to_binary(value, decimals: 1)
defp min_latency(nil, value), do: value
defp min_latency(current, value), do: min(current, value)
defp max_latency(nil, value), do: value
defp max_latency(current, value), do: max(current, value)
end