defmodule Membrane.Core.PullBuffer do @moduledoc """ Buffer that is attached to the `:input` pad when working in a `:pull` mode. It stores `Membrane.Buffer`, `Membrane.Event` and `Membrane.Caps` structs and prevents the situation where the data in a stream contains the discontinuities. It also guarantees that element won't be flooded with the incoming data. """ alias Membrane.Buffer alias Membrane.Core.Message require Message use Bunch use Membrane.Log, tags: :core @qe Qex @non_buf_types [:event, :caps] @type t :: %__MODULE__{ name: Membrane.Element.name_t(), demand_pid: pid(), input_ref: Membrane.Element.Pad.ref_t(), q: @qe.t(), preferred_size: pos_integer(), current_size: non_neg_integer(), demand: non_neg_integer(), min_demand: pos_integer(), metric: module(), toilet: boolean() } defstruct name: :pull_buffer, demand_pid: nil, input_ref: nil, q: nil, preferred_size: 100, current_size: 0, demand: nil, min_demand: nil, metric: nil, toilet: false @typedoc """ Properties that can be passed when creating new PullBuffer """ @type prop_t :: {:preferred_size, pos_integer()} | {:min_demand, pos_integer()} | {:toilet, boolean()} @type props_t :: [prop_t()] @spec new( Membrane.Element.name_t(), demand_pid :: pid, Membrane.Element.Pad.ref_t(), Membrane.Buffer.Metric.unit_t(), props_t ) :: t() def new(name, demand_pid, input_ref, demand_unit, props) do metric = Buffer.Metric.from_unit(demand_unit) preferred_size = props[:preferred_size] || metric.pullbuffer_preferred_size min_demand = props[:min_demand] || preferred_size |> div(4) default_toilet = %{warn: preferred_size * 2, fail: preferred_size * 4} toilet = case props[:toilet] do true -> default_toilet t when t in [nil, false] -> false t -> default_toilet |> Map.merge(t |> Map.new()) end %__MODULE__{ name: name, q: @qe.new, demand_pid: demand_pid, input_ref: input_ref, preferred_size: preferred_size, min_demand: min_demand, demand: preferred_size, metric: metric, toilet: toilet } |> fill() end @spec fill(t()) :: t() defp fill(%__MODULE__{} = pb), do: handle_demand(pb, 0) @spec store(t(), atom(), any()) :: {:ok, t()} | {:error, any()} def store(pb, type \\ :buffers, v) def store( %__MODULE__{current_size: size, preferred_size: pref_size, toilet: false} = pb, :buffers, v ) when is_list(v) do if size >= pref_size do debug(""" PullBuffer #{inspect(pb.name)}: received buffers from input #{inspect(pb.input_ref)}, despite not requesting them. It is probably caused by overestimating demand by previous element. """) end {:ok, do_store_buffers(pb, v)} end def store(%__MODULE__{toilet: %{warn: warn_lvl, fail: fail_lvl}} = pb, :buffers, v) when is_list(v) do %__MODULE__{current_size: size} = pb = do_store_buffers(pb, v) if size >= warn_lvl do above_level = if size < fail_lvl do "warn level" else "fail_level" end warn([ """ PullBuffer #{inspect(pb.name)} (toilet): received #{inspect(size)} buffers, which is above #{above_level}, from input #{inspect(pb.input_ref)} that works in push mode. To have control over amount of buffers being produced, consider using push mode. If this is a normal situation, increase toilet warn/fail level. Buffers: \ """, Buffer.print(v), """ PullBuffer #{inspect(pb)} """ ]) end if size >= fail_lvl do warn_error( "PullBuffer #{inspect(pb.name)} (toilet): failing: too many buffers", {:pull_buffer, toilet: :too_many_buffers} ) else {:ok, pb} end end def store(pb, :buffer, v), do: store(pb, :buffers, [v]) def store(%__MODULE__{q: q} = pb, type, v) when type in @non_buf_types do report("Storing #{type}", pb) {:ok, %__MODULE__{pb | q: q |> @qe.push({:non_buffer, type, v})}} end defp do_store_buffers(%__MODULE__{q: q, current_size: size, metric: metric} = pb, v) do buf_cnt = v |> metric.buffers_size report("Storing #{inspect(buf_cnt)} buffers", pb) %__MODULE__{ pb | q: q |> @qe.push({:buffers, v, buf_cnt}), current_size: size + buf_cnt } end def take(%__MODULE__{current_size: size} = pb, count) when count >= 0 do report("Taking #{inspect(count)} buffers", pb) {out, %__MODULE__{current_size: new_size} = pb} = do_take(pb, count) pb = pb |> handle_demand(size - new_size) {{:ok, out}, pb} end defp do_take(%__MODULE__{q: q, current_size: size, metric: metric} = pb, count) do {out, nq} = q |> q_pop(count, metric) {out, %__MODULE__{pb | q: nq, current_size: max(0, size - count)}} end defp q_pop(q, count, metric, acc \\ []) defp q_pop(q, count, metric, acc) when count > 0 do q |> @qe.pop |> case do {{:value, {:buffers, b, buf_cnt}}, nq} when count >= buf_cnt -> q_pop(nq, count - buf_cnt, metric, [{:buffers, b, buf_cnt} | acc]) {{:value, {:buffers, b, buf_cnt}}, nq} when count < buf_cnt -> {b, back} = b |> metric.split_buffers(count) nq = nq |> @qe.push_front({:buffers, back, buf_cnt - count}) {{:value, [{:buffers, b, count} | acc] |> Enum.reverse()}, nq} {:empty, nq} -> {{:empty, acc |> Enum.reverse()}, nq} {{:value, {:non_buffer, type, e}}, nq} -> q_pop(nq, count, metric, [{type, e} | acc]) end end defp q_pop(q, 0, metric, acc) do q |> @qe.pop |> case do {{:value, {:non_buffer, type, e}}, nq} -> q_pop(nq, 0, metric, [{type, e} | acc]) _ -> {{:value, acc |> Enum.reverse()}, q} end end @spec empty?(t()) :: boolean() def empty?(%__MODULE__{current_size: size}), do: size == 0 defp handle_demand( %__MODULE__{ toilet: false, demand_pid: demand_pid, input_ref: input_ref, current_size: size, preferred_size: pref_size, demand: demand, min_demand: min_demand } = pb, new_demand ) when size < pref_size and demand + new_demand > 0 do to_demand = max(demand + new_demand, min_demand) report( """ Sending demand of size #{inspect(to_demand)} to input #{inspect(pb.input_ref)} """, pb ) Message.send(demand_pid, :demand, [to_demand, input_ref]) %__MODULE__{pb | demand: demand + new_demand - to_demand} end defp handle_demand(%__MODULE__{toilet: false, demand: demand} = pb, new_demand), do: %__MODULE__{pb | demand: demand + new_demand} defp handle_demand(%__MODULE__{toilet: toilet} = pb, _new_demand) when toilet != false do pb end defp report(msg, %__MODULE__{ name: name, current_size: size, preferred_size: pref_size, toilet: toilet }) do name_str = if toilet do "#{inspect(name)} (toilet)" else inspect(name) end debug([ "PullBuffer #{name_str}: ", msg, "\n", "PullBuffer size: #{inspect(size)}, ", if toilet do "toilet limits: #{inspect(toilet)}" else "preferred size: #{inspect(pref_size)}" end ]) end end