Optimize presence.

Avoid fetching each user by passing in full pre-fetched
presences from Presence.fetch/2 callback.
Use temporary assigns in ProfileLive to avoid duping
presences in memeory.
Handle removes by a small hook event
This commit is contained in:
Chris McCord 2022-01-11 14:57:06 -05:00
parent a65c789748
commit 9998e06caa
6 changed files with 56 additions and 41 deletions

View file

@ -195,6 +195,7 @@ window.addEventListener("phx:page-loading-stop", info => topbar.hide())
window.addEventListener("phx:page-loading-stop", routeUpdated) window.addEventListener("phx:page-loading-stop", routeUpdated)
window.addEventListener("js:exec", e => e.target[e.detail.call](...e.detail.args)) window.addEventListener("js:exec", e => e.target[e.detail.call](...e.detail.args))
window.addEventListener("phx:remove-el", e => document.getElementById(e.detail.id).remove())
// connect if there are any LiveViews on the page // connect if there are any LiveViews on the page
liveSocket.getSocket().onOpen(() => execJS("#connection-status", "js-hide")) liveSocket.getSocket().onOpen(() => execJS("#connection-status", "js-hide"))

View file

@ -2,8 +2,10 @@ defmodule Phoenix.Presence.Client do
use GenServer use GenServer
@callback init(state :: term) :: {:ok, new_state :: term} @callback init(state :: term) :: {:ok, new_state :: term}
@callback handle_join(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) :: {:ok, term} @callback handle_join(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) ::
@callback handle_leave(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) :: {:ok, term} {:ok, term}
@callback handle_leave(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) ::
{:ok, term}
@doc """ @doc """
TODO TODO
@ -19,7 +21,8 @@ defmodule Phoenix.Presence.Client do
{:ok, name} -> {:ok, name} ->
GenServer.start_link(__MODULE__, opts, name: name) GenServer.start_link(__MODULE__, opts, name: name)
:error -> GenServer.start_link(__MODULE__, opts) :error ->
GenServer.start_link(__MODULE__, opts)
end end
end end
@ -113,7 +116,9 @@ defmodule Phoenix.Presence.Client do
updated_state = updated_state =
update_topics_state(:add_new_presence_or_metas, state, topic, joined_key, joined_meta) update_topics_state(:add_new_presence_or_metas, state, topic, joined_key, joined_meta)
{:ok, updated_client_state} = state.client.handle_join(topic, joined_key, joined_meta, state.client_state) {:ok, updated_client_state} =
state.client.handle_join(topic, joined_key, meta, state.client_state)
updated_state = Map.put(updated_state, :client_state, updated_client_state) updated_state = Map.put(updated_state, :client_state, updated_client_state)
{updated_state, topic} {updated_state, topic}
@ -122,7 +127,9 @@ defmodule Phoenix.Presence.Client do
defp handle_leave({left_key, meta}, {state, topic}) do defp handle_leave({left_key, meta}, {state, topic}) do
updated_state = update_topics_state(:remove_presence_or_metas, state, topic, left_key, meta) updated_state = update_topics_state(:remove_presence_or_metas, state, topic, left_key, meta)
{:ok, updated_client_state} = state.client.handle_leave(topic, left_key, meta, state.client_state) {:ok, updated_client_state} =
state.client.handle_leave(topic, left_key, meta, state.client_state)
updated_state = Map.put(updated_state, :client_state, updated_client_state) updated_state = Map.put(updated_state, :client_state, updated_client_state)
{updated_state, topic} {updated_state, topic}

View file

@ -15,25 +15,34 @@ defmodule LiveBeats.PresenceClient do
end end
@impl Phoenix.Presence.Client @impl Phoenix.Presence.Client
def handle_join(topic, key, _meta, state) do def handle_join(topic, _key, presence, state) do
active_users_topic = active_users_topic =
topic topic
|> profile_identifier() |> profile_identifier()
|> active_users_topic() |> active_users_topic()
Phoenix.PubSub.local_broadcast(@pubsub, active_users_topic, {__MODULE__, %{user_joined: key}}) Phoenix.PubSub.local_broadcast(
@pubsub,
active_users_topic,
{__MODULE__, %{user_joined: presence}}
)
{:ok, state} {:ok, state}
end end
@impl Phoenix.Presence.Client @impl Phoenix.Presence.Client
def handle_leave(topic, key, _meta, state) do def handle_leave(topic, _key, presence, state) do
active_users_topic = active_users_topic =
topic topic
|> profile_identifier() |> profile_identifier()
|> active_users_topic() |> active_users_topic()
Phoenix.PubSub.local_broadcast(@pubsub, active_users_topic, {__MODULE__, %{user_left: key}}) Phoenix.PubSub.local_broadcast(
@pubsub,
active_users_topic,
{__MODULE__, %{user_left: presence}}
)
{:ok, state} {:ok, state}
end end

View file

@ -5,7 +5,8 @@ defmodule LiveBeatsWeb.Presence do
See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html) See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html)
docs for more details. docs for more details.
""" """
use Phoenix.Presence, otp_app: :live_beats, use Phoenix.Presence,
otp_app: :live_beats,
pubsub_server: LiveBeats.PubSub pubsub_server: LiveBeats.PubSub
import Phoenix.LiveView.Helpers import Phoenix.LiveView.Helpers
@ -18,10 +19,16 @@ defmodule LiveBeatsWeb.Presence do
~H""" ~H"""
<!-- users --> <!-- users -->
<div class="px-4 mt-6 sm:px-6 lg:px-8"> <div class="px-4 mt-6 sm:px-6 lg:px-8">
<h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Who's Listening</h2> <h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Here now</h2>
<ul role="list" class="grid grid-cols-1 gap-4 sm:gap-4 sm:grid-cols-2 xl:grid-cols-5 mt-3" x-max="1"> <ul
id="listening-now"
phx-update="prepend"
role="list"
x-max="1"
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 presence <- @presences do %>
<li class="relative col-span-1 flex shadow-sm rounded-md overflow-hidden"> <li id={"presence-#{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"> <.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-10 h-10 flex-shrink-0 flex items-center justify-center rounded-l-md bg-purple-600" src={presence.avatar_url} alt=""> <img class="w-10 h-10 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 flex items-center justify-between text-gray-900 text-sm font-medium hover:text-gray-600 pl-3">

View file

@ -88,7 +88,9 @@ defmodule LiveBeatsWeb.ProfileLive do
MediaLibrary.subscribe_to_profile(profile) MediaLibrary.subscribe_to_profile(profile)
Accounts.subscribe(current_user.id) Accounts.subscribe(current_user.id)
LiveBeatsWeb.Presence.subscribe(profile) LiveBeatsWeb.Presence.subscribe(profile)
Phoenix.Presence.Client.track(topic(profile.user_id),
Phoenix.Presence.Client.track(
topic(profile.user_id),
current_user.id, current_user.id,
%{} %{}
) )
@ -111,7 +113,7 @@ defmodule LiveBeatsWeb.ProfileLive do
|> list_songs() |> list_songs()
|> assign_presences() |> assign_presences()
{:ok, socket, temporary_assigns: [songs: []]} {:ok, socket, temporary_assigns: [songs: [], presences: []]}
end end
def handle_params(params, _url, socket) do def handle_params(params, _url, socket) do
@ -147,21 +149,14 @@ defmodule LiveBeatsWeb.ProfileLive do
{:noreply, socket} {:noreply, socket}
end end
def handle_info({LiveBeats.PresenceClient, %{user_joined: user_id}}, socket) do def handle_info({LiveBeats.PresenceClient, %{user_joined: presence}}, socket) do
new_user = Accounts.get_user!(user_id) %{user: user} = presence
updated_presences = {:noreply, update(socket, :presences, &[user | &1])}
if new_user in socket.assigns.presences do
socket.assigns.presences
else
[new_user | socket.assigns.presences]
end
{:noreply, assign(socket, :presences, updated_presences)}
end end
def handle_info({LiveBeats.PresenceClient, %{user_left: user_id}}, socket) do def handle_info({LiveBeats.PresenceClient, %{user_left: presence}}, socket) do
updated_presences = socket.assigns.presences %{user: user} = presence
|> Enum.reject(fn user -> user.id == String.to_integer(user_id) end) {:noreply, push_event(socket, "remove-el", %{id: "presence-#{user.id}"})}
{:noreply, assign(socket, :presences, updated_presences)}
end end
def handle_info({Accounts, %Accounts.Events.ActiveProfileChanged{} = event}, socket) do def handle_info({Accounts, %Accounts.Events.ActiveProfileChanged{} = event}, socket) do
@ -264,7 +259,8 @@ defmodule LiveBeatsWeb.ProfileLive do
end end
defp assign_presences(socket) do defp assign_presences(socket) do
presences = socket.assigns.profile.user_id presences =
socket.assigns.profile.user_id
|> topic() |> topic()
|> LiveBeats.PresenceClient.list() |> LiveBeats.PresenceClient.list()
|> Enum.map(fn {_key, meta} -> meta.user end) |> Enum.map(fn {_key, meta} -> meta.user end)

View file

@ -51,18 +51,13 @@ defmodule LiveBeatsWeb.ProfileLiveTest do
} }
}) =~ "can&#39;t be blank" }) =~ "can&#39;t be blank"
assert {:ok, new_lv, html} = assert lv |> form("#song-form") |> render_submit() =~ "silence1s"
lv |> form("#song-form") |> render_submit() |> follow_redirect(conn) assert_patch(lv, "/#{current_user.username}")
assert_redirected(lv, "/#{current_user.username}")
assert html =~ "1 song(s) uploaded"
assert html =~ "silence1s"
# deleting songs # deleting songs
song = MediaLibrary.get_first_song(profile) song = MediaLibrary.get_first_song(profile)
assert new_lv |> element("#delete-modal-#{song.id}-confirm") |> render_click() assert lv |> element("#delete-modal-#{song.id}-confirm") |> render_click()
{:ok, refreshed_lv, _} = live(conn, LiveHelpers.profile_path(current_user)) {:ok, refreshed_lv, _} = live(conn, LiveHelpers.profile_path(current_user))
refute render(refreshed_lv) =~ "silence1s" refute render(refreshed_lv) =~ "silence1s"