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("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
liveSocket.getSocket().onOpen(() => execJS("#connection-status", "js-hide"))

View file

@ -2,8 +2,10 @@ defmodule Phoenix.Presence.Client do
use GenServer
@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_leave(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) ::
{:ok, term}
@callback handle_leave(topic :: String.t(), key :: String.t(), meta :: [map()], state :: term) ::
{:ok, term}
@doc """
TODO
@ -19,7 +21,8 @@ defmodule Phoenix.Presence.Client do
{:ok, name} ->
GenServer.start_link(__MODULE__, opts, name: name)
:error -> GenServer.start_link(__MODULE__, opts)
:error ->
GenServer.start_link(__MODULE__, opts)
end
end
@ -113,7 +116,9 @@ defmodule Phoenix.Presence.Client do
updated_state =
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, topic}
@ -122,7 +127,9 @@ defmodule Phoenix.Presence.Client do
defp handle_leave({left_key, meta}, {state, topic}) do
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, topic}

View file

@ -15,25 +15,34 @@ defmodule LiveBeats.PresenceClient do
end
@impl Phoenix.Presence.Client
def handle_join(topic, key, _meta, state) do
def handle_join(topic, _key, presence, state) do
active_users_topic =
topic
|> profile_identifier()
|> 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}
end
@impl Phoenix.Presence.Client
def handle_leave(topic, key, _meta, state) do
def handle_leave(topic, _key, presence, state) do
active_users_topic =
topic
|> profile_identifier()
|> 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}
end

View file

@ -5,8 +5,9 @@ defmodule LiveBeatsWeb.Presence do
See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html)
docs for more details.
"""
use Phoenix.Presence, otp_app: :live_beats,
pubsub_server: LiveBeats.PubSub
use Phoenix.Presence,
otp_app: :live_beats,
pubsub_server: LiveBeats.PubSub
import Phoenix.LiveView.Helpers
import LiveBeatsWeb.LiveHelpers
@ -18,10 +19,16 @@ defmodule LiveBeatsWeb.Presence do
~H"""
<!-- users -->
<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>
<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">
<h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Here now</h2>
<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 %>
<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">
<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">

View file

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

View file

@ -51,18 +51,13 @@ defmodule LiveBeatsWeb.ProfileLiveTest do
}
}) =~ "can&#39;t be blank"
assert {:ok, new_lv, html} =
lv |> form("#song-form") |> render_submit() |> follow_redirect(conn)
assert_redirected(lv, "/#{current_user.username}")
assert html =~ "1 song(s) uploaded"
assert html =~ "silence1s"
assert lv |> form("#song-form") |> render_submit() =~ "silence1s"
assert_patch(lv, "/#{current_user.username}")
# deleting songs
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))
refute render(refreshed_lv) =~ "silence1s"