ketupia

ketupia

PubSub instead of polling in liveview?

The LiveView in the tutorial runs a message loop every 50ms to load the player info and update the view to the current state. Has anyone tried using PubSub when the components are updated (thus relieving the need to poll)?

Most Liked

RomanKotov

RomanKotov

Hi,

Sorry, I did not look at the LiveView tutorial.
I have worked with polling model for music player a couple of years ago: local_assistant/lib/local_assistant/player/player.ex at f02e6779d011a612a317d41a97aad4c330a7034d · RomanKotov/local_assistant · GitHub

This was a local player. We used it in the office to play tracks.
Pros:

  • Easy to implement.
    Cons:
  • The player I used to play track (mopidy) could be blocked on long operations. For example, loading very large playlist could crash the whole application. Or the UI was unstable.

I took the issues into account and created another application to play music, and some other stuff (it will be Elixir-based Smart Home). Now it uses PubSub (indirectly) and the other player (mpv).

New examples:

I like how newer approach. It works better :slight_smile:

Have streamed the implementation process some time. Here is a thread about the Exshome: Exshome - DIY Elixir Smart Home

Hope this helps :slight_smile:

APB9785

APB9785

Creator of ECSx

Some good insights from @RomanKotov - particularly

Just want to emphasize, that it is better not to overload your systems and keep them as lightweight as possible.

is very important. Remember that reading from ETS (which we use for component storage) is extremely fast and lightweight - so if your goal is to reduce the number of times you are fetching the component data, it might not be a worthwhile goal. Better to save that type of optimization for later if you notice performance becoming a problem (and even then, it’s likely there are other things you could optimize that would make a greater impact)

RomanKotov

RomanKotov

I have prepared some examples of how it is possible to use PubSub. I have just created a simple .exs file (for example pub_sub.exs) and then ran it as iex pub_sub.exs.
It does not use real LiveView, or ECSx, but shows some concepts:

Mix.install([:phoenix_pubsub])

defmodule HPComponent do
  def get(_id), do: Enum.random(1..100)
end

defmodule ManaComponent do
  def get(_id), do: Enum.random(1..100)
end

defmodule Entity do
  @callback get_value(id :: String.t()) :: map()

  def get_value(entity, id), do: entity.get_value(id)
  
  def subscribe(entity, id) do
    pub_sub_key = generate_pub_sub_key(entity, id)
    :ok = Phoenix.PubSub.subscribe(MyApp.PubSub, pub_sub_key)
    get_value(entity, id)
  end

  def broadcast_value(entity, id, value) do
    pub_sub_key = generate_pub_sub_key(entity, id)
    Phoenix.PubSub.broadcast!(MyApp.PubSub, pub_sub_key, {__MODULE__, entity, id, value})
  end

  defp generate_pub_sub_key(entity, id), do: inspect({__MODULE__, entity, id})
end

defmodule PlayerEntity do
  @behaviour Entity

  @impl Entity
  def get_value(player_id) do
    %{
      hp: HPComponent.get(player_id),
      mana: ManaComponent.get(player_id)
    }
  end
end

defmodule EntityServer do
  use GenServer

  def start_link(params) do
    GenServer.start_link(__MODULE__, params)
  end
  
  @impl GenServer
  def init(params) do
    id = Keyword.fetch!(params, :id)
    entity = Keyword.fetch!(params, :entity)
    refresh_interval = Keyword.get(params, :refresh_interval, 10)
    
    current_value = Entity.get_value(entity, id)
    
    state = %{
      id: id,
      entity: entity,
      value: current_value,
      refresh_interval: refresh_interval
    }
    schedule_refresh(state)
    
    {:ok, state}
  end

  @impl GenServer
  def handle_info(:refresh, state) do
    new_value = Entity.get_value(state.entity, state.id)

    if new_value != state.value do
      Entity.broadcast_value(state.entity, state.id, new_value)
    end

    schedule_refresh(state)
    
    {:noreply, Map.put(state, :value, new_value)}
  end
  
  defp schedule_refresh(%{refresh_interval: interval}) do
    Process.send_after(self(), :refresh, interval)
  end
end

defmodule LiveViewPlayersView do
  use GenServer

  def start_link(params), do: GenServer.start_link(__MODULE__, params)
  
  @impl GenServer
  def init(_) do
    state = [1, 2]
    |> Enum.map(fn id -> {id, Entity.subscribe(PlayerEntity, id)} end)
    |> Enum.into(%{})

    IO.inspect("Players on start: #{inspect(state)}")
    
    {:ok, state}
  end

  @impl GenServer
  def handle_info({Entity, PlayerEntity, id, value}, state) do
    state = Map.put(state, id, value)
    IO.inspect("At least one player changed: #{inspect(state)}")
    {:noreply, state}
  end
end

children = [
  {Phoenix.PubSub, name: MyApp.PubSub},
  Supervisor.child_spec({EntityServer, entity: PlayerEntity, id: 1}, id: :player_1),
  Supervisor.child_spec({EntityServer, entity: PlayerEntity, id: 2}, id: :player_2),
  LiveViewPlayersView,
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
{:ok, _pid} = Supervisor.start_link(children, opts)

Process.sleep(:infinity)

Let’s break it down:

  • Mix.install(...) part installs all necessary dependencies.
  • Modules HPComponent and ManaComponent emulate components from ECS.
  • Entity module creates a behaviour for entities. As of 0.5.1 - there is no thing in ECSx related to the entities. Entity is only represented as id. So just thought it can be fine to gather every part related to this id from relevant components here. Behaviour needs get_value/1 function. You can extract behaviour itself to other module, so it will not influence the compilation graph. I could add some __using__ macros, but thought behaviour is enough here.
  • Entity module also supports extra functions. get_value/2 returns the current value for the entity. It is up to you whether to keep it. subscribe/2 subscribes to the changes in Entity and returns its current value. broadcast_value/3 incapsulates PubSub details. I find this approach useful in tests - you don’t need to start the whole system, you can just broadcast relevant data.
  • PlayerEntity - implements Entity behaviour. Example of the entity. It takes the data directly from components (ETS in case of ECSx) and has no internal state. If you need multiple parts of player data in one place, possibly it is fine to call get_value from such entity. If you need only a couple of components - it is better to call them directly. You may also want to use structs instead of map here.
  • EntityServer module is a simple GenServer, that periodically polls the state of the entity and broadcasts changes (if there were any).
  • LiveViewPlayersView module is also GenServer that emulates subscription to the Entity changes. IO.inspect/1 parts from it may be replaced with assign/2 or assign/3 in case of single objects. It also can be replaced with stream/4 and related API (if you want to render lists of items). There are some other techniques, but they are LiveView-specific.
  • List children starts all necessary dependencies for this demo to work. They are PubSub itself (with all necessary Registries), a couple of Player entities (needed to use this syntax to launch multiple instances) and LiveView emulation process. Starting EntityServer instances may be delegated to own supervisor instead.
  • Then I start all necessary dependencies for the demo.
  • Process.sleep(:infinity) line just for demo purposes here. It allows script to keep working (and printing changes in player state).

I have also thought about the architecture of manager. Since all logic runs in one process, you may want to give higher priority for this process too (so less relevant ones will not interfere with critical computations). You can do this by updating priority in startup/0 function in your manager, like Process.flag(:priority, :high). Possibly there some other options to do this.

Where Next?

Popular in Questions Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52341 488
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement