Optimize presence and rate limit pings

This commit is contained in:
Chris McCord 2022-01-31 14:27:06 -05:00
parent de2f473624
commit 8cd6048d4b
5 changed files with 122 additions and 62 deletions

View file

@ -183,7 +183,7 @@ Hooks.Ping = {
this.handleEvent("pong", () => { this.handleEvent("pong", () => {
let rtt = Date.now() - this.nowMs let rtt = Date.now() - this.nowMs
this.el.innerText = `ping: ${rtt}ms` this.el.innerText = `ping: ${rtt}ms`
this.timer = setTimeout(() => this.ping(rtt), 1000) this.timer = setTimeout(() => this.ping(rtt), 100)
}) })
this.ping(null) this.ping(null)
}, },

View file

@ -13,6 +13,7 @@ defmodule LiveBeatsWeb.Presence do
import LiveBeatsWeb.LiveHelpers import LiveBeatsWeb.LiveHelpers
alias LiveBeats.{Accounts, MediaLibrary} alias LiveBeats.{Accounts, MediaLibrary}
alias LiveBeatsWeb.Presence.BadgeComponent
def subscribe(%MediaLibrary.Profile{} = profile) do def subscribe(%MediaLibrary.Profile{} = profile) do
LiveBeats.PresenceClient.subscribe(profile) LiveBeats.PresenceClient.subscribe(profile)
@ -29,57 +30,79 @@ defmodule LiveBeatsWeb.Presence do
{key, %{metas: metas, user: users[String.to_integer(key)]}} {key, %{metas: metas, user: users[String.to_integer(key)]}}
end end
end end
end
defmodule LiveBeatsWeb.Presence.BadgeListComponent do def listening_now(assigns) do
use LiveBeatsWeb, :live_component 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""" ~H"""
<div class="px-4 mt-6 sm:px-6 lg:px-8"> <!-- users --> <div class="px-4 mt-6 sm:px-6 lg:px-8"> <!-- users -->
<h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Listening now</h2> <h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Listening now (<%= @count %>)</h2>
<ul <ul
id="listening-now" id="listening-now"
phx-update="prepend"
role="list" role="list"
x-max="1" x-max="1"
class="grid grid-cols-1 gap-4 sm:gap-4 sm:grid-cols-2 xl:grid-cols-5 mt-3" class="grid grid-cols-1 gap-4 sm:gap-4 sm:grid-cols-2 xl:grid-cols-5 mt-3"
> >
<%= for presence <- @presences do %> <%= for {id, _time} <- Enum.sort(@presence_ids, fn {_, t1}, {_, t2} -> t1 < t2 end) do %>
<li id={"presence-#{presence.id}"} class="relative col-span-1 flex shadow-sm rounded-md overflow-hidden"> <.live_component
<.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"> id={id}
<img class="w-12 h-12 flex-shrink-0 flex items-center justify-center rounded-l-md bg-purple-600" src={presence.avatar_url} alt=""> module={BadgeComponent}
<div class="flex-1 flex items-center justify-between text-gray-900 text-sm font-medium hover:text-gray-600 pl-3"> presence={@presences[id]}
<div class="flex-1 py-1 text-sm truncate"> />
<%= render_slot(@inner_block, %{user: presence, ping: @pings[presence.id], region: @regions[presence.id]}) %>
</div>
</div>
</.link>
</li>
<% end %> <% end %>
</ul> </ul>
<%= if @total_count > @count do %>
<p>+ <%= @total_count - @count %> more</p>
<% end %>
</div> </div>
""" """
end end
end
defmodule LiveBeatsWeb.Presence.BadgeComponent do
use LiveBeatsWeb, :live_component
def render(assigns) do
~H"""
<li id={"presence-#{@id}"} class="relative col-span-1 flex shadow-sm rounded-md overflow-hidden">
<.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">
<img class="w-12 h-12 flex-shrink-0 flex items-center justify-center rounded-l-md bg-purple-600" src={@presence.avatar_url} alt="">
<div class="flex-1 flex items-center justify-between text-gray-900 text-sm font-medium hover:text-gray-600 pl-3">
<div class="flex-1 py-1 text-sm truncate">
<%= @presence.username %>
<%= if @ping do %>
<p class="text-gray-400 text-xs">ping: <%= @ping %>ms</p>
<%= if @region do %><img class="inline w-7 h-7 absolute right-3 top-3" src={"https://fly.io/ui/images/#{@region}.svg"} /><% end %>
<% end %>
</div>
</div>
</.link>
</li>
"""
end
def mount(socket) do def mount(socket) do
{:ok, socket, temporary_assigns: [presences: [], pings: %{}, regions: %{}]} {:ok, socket, temporary_assigns: [presence: nil, ping: nil, region: nil]}
end end
def update(%{action: {:ping, action}}, socket) do def update(%{action: {:ping, action}}, socket) do
%{user: user, ping: ping, region: region} = action %{user: user, ping: ping, region: region} = action
{:ok, {:ok, assign(socket, presence: user, ping: ping, region: region)}
socket
|> assign(:presences, [user])
|> update(:pings, &Map.put(&1, user.id, ping))
|> update(:regions, &Map.put(&1, user.id, region))}
end end
def update(%{presence: nil}, socket), do: {:ok, socket}
def update(assigns, socket) do def update(assigns, socket) do
{:ok, {:ok,
socket socket
|> assign(assigns) |> assign(id: assigns.id, presence: assigns.presence)
|> assign_new(:pings, fn -> %{} end) |> assign_new(:pings, fn -> %{} end)
|> assign_new(:regions, fn -> %{} end)} |> assign_new(:regions, fn -> %{} end)}
end end

View file

@ -13,12 +13,16 @@ defmodule LiveBeatsWeb.LiveHelpers do
def profile_path(current_user_or_profile, action \\ :show) 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 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 end
def profile_path(%MediaLibrary.Profile{} = profile, action) do def profile_path(%MediaLibrary.Profile{} = profile, action) do
Routes.profile_path(LiveBeatsWeb.Endpoint, action, profile.username) profile_path(profile.username, action)
end end
def connection_status(assigns) do def connection_status(assigns) do
@ -134,12 +138,13 @@ defmodule LiveBeatsWeb.LiveHelpers do
""" """
end end
def link(%{navigate: to} = assigns) do def link(%{navigate: _to} = assigns) do
opts = assigns |> assigns_to_attributes() |> Keyword.put(:to, to) assigns = assign_new(assigns, :class, fn -> nil end)
assigns = assign(assigns, :opts, opts)
~H""" ~H"""
<%= live_redirect @opts do %><%= render_slot(@inner_block) %><% end %> <a href={@navigate} data-phx-link="redirect" data-phx-link-state="push" class={@class}>
<%= render_slot(@inner_block) %>
</a>
""" """
end end

View file

@ -1,7 +1,7 @@
defmodule LiveBeatsWeb.Nav do defmodule LiveBeatsWeb.Nav do
import Phoenix.LiveView import Phoenix.LiveView
alias LiveBeats.MediaLibrary alias LiveBeats.{Accounts, MediaLibrary}
alias LiveBeatsWeb.{ProfileLive, SettingsLive} alias LiveBeatsWeb.{ProfileLive, SettingsLive}
def on_mount(:default, _params, _session, socket) do def on_mount(:default, _params, _session, socket) do
@ -32,15 +32,26 @@ defmodule LiveBeatsWeb.Nav do
end end
defp handle_event("ping", %{"rtt" => rtt}, socket) do defp handle_event("ping", %{"rtt" => rtt}, socket) do
%{current_user: current_user} = socket.assigns {:halt,
socket
if rtt && current_user && current_user.active_profile_user_id do |> rate_limited_ping_broadcast(socket.assigns.current_user, rtt)
MediaLibrary.broadcast_ping(current_user, rtt, socket.assigns.region) |> push_event("pong", %{})}
end
{:halt, push_event(socket, "pong", %{})}
end 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 handle_event(_, _, socket), do: {:cont, socket}
defp current_user_profile_username(socket) do defp current_user_profile_username(socket) do

View file

@ -5,6 +5,8 @@ defmodule LiveBeatsWeb.ProfileLive do
alias LiveBeatsWeb.{LayoutComponent, Presence} alias LiveBeatsWeb.{LayoutComponent, Presence}
alias LiveBeatsWeb.ProfileLive.{SongRowComponent, UploadFormComponent} alias LiveBeatsWeb.ProfileLive.{SongRowComponent, UploadFormComponent}
@max_presences 20
def render(assigns) do def render(assigns) do
~H""" ~H"""
<.title_bar> <.title_bar>
@ -39,17 +41,11 @@ defmodule LiveBeatsWeb.ProfileLive do
</:actions> </:actions>
</.title_bar> </.title_bar>
<.live_component <Presence.listening_now
let={%{user: user, ping: ping, region: region}}
id={:presence_badges} module={Presence.BadgeListComponent}
presences={@presences} presences={@presences}
> presence_ids={@presence_ids}
<%= user.username %> total_count={@presences_count}
<%= if ping do %> />
<p class="text-gray-400 text-xs">ping: <%= ping %>ms</p>
<%= if region do %><img class="inline w-7 h-7 absolute right-3 top-3" src={"https://fly.io/ui/images/#{region}.svg"} /><% end %>
<% end %>
</.live_component>
<div id="dialogs" phx-update="append"> <div id="dialogs" phx-update="append">
<%= for song <- if(@owns_profile?, do: @songs, else: []), id = "delete-modal-#{song.id}" do %> <%= for song <- if(@owns_profile?, do: @songs, else: []), id = "delete-modal-#{song.id}" do %>
@ -115,7 +111,7 @@ defmodule LiveBeatsWeb.ProfileLive do
|> list_songs() |> list_songs()
|> assign_presences() |> assign_presences()
{:ok, socket, temporary_assigns: [songs: [], presences: []]} {:ok, socket, temporary_assigns: [songs: [], presences: %{}]}
end end
def handle_params(params, _url, socket) do def handle_params(params, _url, socket) do
@ -152,15 +148,14 @@ defmodule LiveBeatsWeb.ProfileLive do
end end
def handle_info({LiveBeats.PresenceClient, %{user_joined: presence}}, socket) do def handle_info({LiveBeats.PresenceClient, %{user_joined: presence}}, socket) do
%{user: user} = presence {:noreply, assign_presence(socket, presence)}
{:noreply, update(socket, :presences, &[user | &1])}
end end
def handle_info({LiveBeats.PresenceClient, %{user_left: presence}}, socket) do def handle_info({LiveBeats.PresenceClient, %{user_left: presence}}, socket) do
%{user: user} = presence %{user: user} = presence
if presence.metas == [] do if presence.metas == [] do
{:noreply, push_event(socket, "remove-el", %{id: "presence-#{user.id}"})} {:noreply, remove_presence(socket, user)}
else else
{:noreply, socket} {:noreply, socket}
end end
@ -191,8 +186,9 @@ defmodule LiveBeatsWeb.ProfileLive do
def handle_info({MediaLibrary, {:ping, ping}}, socket) do def handle_info({MediaLibrary, {:ping, ping}}, socket) do
%{user: user, rtt: rtt, region: region} = ping %{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}} action: {:ping, %{user: user, ping: rtt, region: region}}
) )
@ -276,18 +272,43 @@ defmodule LiveBeatsWeb.ProfileLive do
end end
defp assign_presences(socket) do defp assign_presences(socket) do
if profile = socket.assigns.profile do socket = assign(socket, presences_count: 0, presences: %{}, presence_ids: %{})
presences =
profile
|> LiveBeats.PresenceClient.list()
|> Enum.map(fn {_key, meta} -> meta.user end)
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 else
assign(socket, presences: []) socket
end end
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(nil), do: ""
defp url_text(url_str) do defp url_text(url_str) do