From 8cd6048d4b7e1bc6ad620d5628e7e7f944b0ca4d Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Mon, 31 Jan 2022 14:27:06 -0500 Subject: [PATCH] Optimize presence and rate limit pings --- assets/js/app.js | 2 +- lib/live_beats_web/channels/presence.ex | 71 ++++++++++++++++--------- lib/live_beats_web/live/live_helpers.ex | 17 +++--- lib/live_beats_web/live/nav.ex | 27 +++++++--- lib/live_beats_web/live/profile_live.ex | 67 +++++++++++++++-------- 5 files changed, 122 insertions(+), 62 deletions(-) diff --git a/assets/js/app.js b/assets/js/app.js index 476cdc1..18adfa1 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -183,7 +183,7 @@ Hooks.Ping = { this.handleEvent("pong", () => { let rtt = Date.now() - this.nowMs this.el.innerText = `ping: ${rtt}ms` - this.timer = setTimeout(() => this.ping(rtt), 1000) + this.timer = setTimeout(() => this.ping(rtt), 100) }) this.ping(null) }, diff --git a/lib/live_beats_web/channels/presence.ex b/lib/live_beats_web/channels/presence.ex index d86e9b2..1510917 100644 --- a/lib/live_beats_web/channels/presence.ex +++ b/lib/live_beats_web/channels/presence.ex @@ -13,6 +13,7 @@ defmodule LiveBeatsWeb.Presence do import LiveBeatsWeb.LiveHelpers alias LiveBeats.{Accounts, MediaLibrary} + alias LiveBeatsWeb.Presence.BadgeComponent def subscribe(%MediaLibrary.Profile{} = profile) do LiveBeats.PresenceClient.subscribe(profile) @@ -29,57 +30,79 @@ defmodule LiveBeatsWeb.Presence do {key, %{metas: metas, user: users[String.to_integer(key)]}} end end -end -defmodule LiveBeatsWeb.Presence.BadgeListComponent do - use LiveBeatsWeb, :live_component + def listening_now(assigns) do + import Phoenix.LiveView + count = Enum.count(assigns.presence_ids) + + assigns = + assigns + |> assign(:count, count) + |> assign_new(:total_count, fn -> count end) - def render(assigns) do ~H"""
-

Listening now

+

Listening now (<%= @count %>)

+ <%= if @total_count > @count do %> +

+ <%= @total_count - @count %> more

+ <% end %>
""" end +end + +defmodule LiveBeatsWeb.Presence.BadgeComponent do + use LiveBeatsWeb, :live_component + + def render(assigns) do + ~H""" +
  • + <.link navigate={profile_path(@presence)} class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate"> + +
    +
    + <%= @presence.username %> + <%= if @ping do %> +

    ping: <%= @ping %>ms

    + <%= if @region do %><% end %> + <% end %> +
    +
    + +
  • + """ + end def mount(socket) do - {:ok, socket, temporary_assigns: [presences: [], pings: %{}, regions: %{}]} + {:ok, socket, temporary_assigns: [presence: nil, ping: nil, region: nil]} end def update(%{action: {:ping, action}}, socket) do %{user: user, ping: ping, region: region} = action - {:ok, - socket - |> assign(:presences, [user]) - |> update(:pings, &Map.put(&1, user.id, ping)) - |> update(:regions, &Map.put(&1, user.id, region))} + {:ok, assign(socket, presence: user, ping: ping, region: region)} end + def update(%{presence: nil}, socket), do: {:ok, socket} + def update(assigns, socket) do {:ok, socket - |> assign(assigns) + |> assign(id: assigns.id, presence: assigns.presence) |> assign_new(:pings, fn -> %{} end) |> assign_new(:regions, fn -> %{} end)} end diff --git a/lib/live_beats_web/live/live_helpers.ex b/lib/live_beats_web/live/live_helpers.ex index 074b2f6..e382db2 100644 --- a/lib/live_beats_web/live/live_helpers.ex +++ b/lib/live_beats_web/live/live_helpers.ex @@ -13,12 +13,16 @@ defmodule LiveBeatsWeb.LiveHelpers do def profile_path(current_user_or_profile, action \\ :show) + def profile_path(username, action) when is_binary(username) do + Routes.profile_path(LiveBeatsWeb.Endpoint, action, username) + end + def profile_path(%Accounts.User{} = current_user, action) do - Routes.profile_path(LiveBeatsWeb.Endpoint, action, current_user.username) + profile_path(current_user.username, action) end def profile_path(%MediaLibrary.Profile{} = profile, action) do - Routes.profile_path(LiveBeatsWeb.Endpoint, action, profile.username) + profile_path(profile.username, action) end def connection_status(assigns) do @@ -134,12 +138,13 @@ defmodule LiveBeatsWeb.LiveHelpers do """ end - def link(%{navigate: to} = assigns) do - opts = assigns |> assigns_to_attributes() |> Keyword.put(:to, to) - assigns = assign(assigns, :opts, opts) + def link(%{navigate: _to} = assigns) do + assigns = assign_new(assigns, :class, fn -> nil end) ~H""" - <%= live_redirect @opts do %><%= render_slot(@inner_block) %><% end %> + + <%= render_slot(@inner_block) %> + """ end diff --git a/lib/live_beats_web/live/nav.ex b/lib/live_beats_web/live/nav.ex index 5509850..bdc2274 100644 --- a/lib/live_beats_web/live/nav.ex +++ b/lib/live_beats_web/live/nav.ex @@ -1,7 +1,7 @@ defmodule LiveBeatsWeb.Nav do import Phoenix.LiveView - alias LiveBeats.MediaLibrary + alias LiveBeats.{Accounts, MediaLibrary} alias LiveBeatsWeb.{ProfileLive, SettingsLive} def on_mount(:default, _params, _session, socket) do @@ -32,15 +32,26 @@ defmodule LiveBeatsWeb.Nav do end defp handle_event("ping", %{"rtt" => rtt}, socket) do - %{current_user: current_user} = socket.assigns - - if rtt && current_user && current_user.active_profile_user_id do - MediaLibrary.broadcast_ping(current_user, rtt, socket.assigns.region) - end - - {:halt, push_event(socket, "pong", %{})} + {:halt, + socket + |> rate_limited_ping_broadcast(socket.assigns.current_user, rtt) + |> push_event("pong", %{})} end + defp rate_limited_ping_broadcast(socket, %Accounts.User{} = user, rtt) when is_integer(rtt) do + now = System.system_time(:millisecond) + last_ping_at = socket.assigns[:last_ping_at] + + if is_nil(last_ping_at) || now - last_ping_at > 1000 do + MediaLibrary.broadcast_ping(user, rtt, socket.assigns.region) + assign(socket, :last_ping_at, now) + else + socket + end + end + + defp rate_limited_ping_broadcast(socket, _user, _rtt), do: socket + defp handle_event(_, _, socket), do: {:cont, socket} defp current_user_profile_username(socket) do diff --git a/lib/live_beats_web/live/profile_live.ex b/lib/live_beats_web/live/profile_live.ex index 621b9c6..181dbe8 100644 --- a/lib/live_beats_web/live/profile_live.ex +++ b/lib/live_beats_web/live/profile_live.ex @@ -5,6 +5,8 @@ defmodule LiveBeatsWeb.ProfileLive do alias LiveBeatsWeb.{LayoutComponent, Presence} alias LiveBeatsWeb.ProfileLive.{SongRowComponent, UploadFormComponent} + @max_presences 20 + def render(assigns) do ~H""" <.title_bar> @@ -39,17 +41,11 @@ defmodule LiveBeatsWeb.ProfileLive do - <.live_component - let={%{user: user, ping: ping, region: region}} - id={:presence_badges} module={Presence.BadgeListComponent} + - <%= user.username %> - <%= if ping do %> -

    ping: <%= ping %>ms

    - <%= if region do %><% end %> - <% end %> - + presence_ids={@presence_ids} + total_count={@presences_count} + />
    <%= for song <- if(@owns_profile?, do: @songs, else: []), id = "delete-modal-#{song.id}" do %> @@ -115,7 +111,7 @@ defmodule LiveBeatsWeb.ProfileLive do |> list_songs() |> assign_presences() - {:ok, socket, temporary_assigns: [songs: [], presences: []]} + {:ok, socket, temporary_assigns: [songs: [], presences: %{}]} end def handle_params(params, _url, socket) do @@ -152,15 +148,14 @@ defmodule LiveBeatsWeb.ProfileLive do end def handle_info({LiveBeats.PresenceClient, %{user_joined: presence}}, socket) do - %{user: user} = presence - {:noreply, update(socket, :presences, &[user | &1])} + {:noreply, assign_presence(socket, presence)} end def handle_info({LiveBeats.PresenceClient, %{user_left: presence}}, socket) do %{user: user} = presence if presence.metas == [] do - {:noreply, push_event(socket, "remove-el", %{id: "presence-#{user.id}"})} + {:noreply, remove_presence(socket, user)} else {:noreply, socket} end @@ -191,8 +186,9 @@ defmodule LiveBeatsWeb.ProfileLive do def handle_info({MediaLibrary, {:ping, ping}}, socket) do %{user: user, rtt: rtt, region: region} = ping - send_update(Presence.BadgeListComponent, - id: :presence_badges, + + send_update(Presence.BadgeComponent, + id: user.id, action: {:ping, %{user: user, ping: rtt, region: region}} ) @@ -276,18 +272,43 @@ defmodule LiveBeatsWeb.ProfileLive do end defp assign_presences(socket) do - if profile = socket.assigns.profile do - presences = - profile - |> LiveBeats.PresenceClient.list() - |> Enum.map(fn {_key, meta} -> meta.user end) + socket = assign(socket, presences_count: 0, presences: %{}, presence_ids: %{}) - assign(socket, presences: presences) + if profile = connected?(socket) && socket.assigns.profile do + profile + |> LiveBeats.PresenceClient.list() + |> Enum.reduce(socket, fn {_, presence}, acc -> assign_presence(acc, presence) end) else - assign(socket, presences: []) + socket end end + defp assign_presence(socket, presence) do + %{user: user} = presence + %{presence_ids: presence_ids} = socket.assigns + + cond do + Map.has_key?(presence_ids, user.id) -> + socket + + Enum.count(presence_ids) < @max_presences -> + socket + |> update(:presences, &Map.put(&1, user.id, user)) + |> update(:presence_ids, &Map.put(&1, user.id, System.system_time())) + |> update(:presences_count, &(&1 + 1)) + + true -> + update(socket, :presences_count, &(&1 + 1)) + end + end + + defp remove_presence(socket, user) do + socket + |> update(:presences, &Map.delete(&1, user.id)) + |> update(:presence_ids, &Map.delete(&1, user.id)) + |> update(:presences_count, &(&1 - 1)) + end + defp url_text(nil), do: "" defp url_text(url_str) do