Optimise rendering and diff by stripping data into view-specific struct (#119)

* Optimise rendering and diff by stripping data into view-specific struct

* Move data to socket.private
This commit is contained in:
Jonatan Kłosko 2021-03-25 23:29:22 +01:00 committed by GitHub
parent de83020d05
commit 143cd5d80f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 127 additions and 99 deletions

View file

@ -5,16 +5,16 @@ defmodule LivebookWeb.CellComponent do
~L"""
<div class="flex flex-col relative"
data-element="cell"
id="cell-<%= @cell.id %>"
id="cell-<%= @cell_view.id %>"
phx-hook="Cell"
data-cell-id="<%= @cell.id %>"
data-type="<%= @cell.type %>">
data-cell-id="<%= @cell_view.id %>"
data-type="<%= @cell_view.type %>">
<%= render_cell_content(assigns) %>
</div>
"""
end
def render_cell_content(%{cell: %{type: :markdown}} = assigns) do
def render_cell_content(%{cell_view: %{type: :markdown}} = assigns) do
~L"""
<div class="mb-1 flex items-center justify-end">
<div class="relative z-10 flex items-center justify-end space-x-2" data-element="actions">
@ -26,7 +26,7 @@ defmodule LivebookWeb.CellComponent do
<span class="tooltip top" aria-label="Move up">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>"
phx-value-cell_id="<%= @cell_view.id %>"
phx-value-offset="-1">
<%= remix_icon("arrow-up-s-line", class: "text-xl") %>
</button>
@ -34,7 +34,7 @@ defmodule LivebookWeb.CellComponent do
<span class="tooltip top" aria-label="Move down">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>"
phx-value-cell_id="<%= @cell_view.id %>"
phx-value-offset="1">
<%= remix_icon("arrow-down-s-line", class: "text-xl") %>
</button>
@ -42,7 +42,7 @@ defmodule LivebookWeb.CellComponent do
<span class="tooltip top" aria-label="Delete">
<button class="icon-button"
phx-click="delete_cell"
phx-value-cell_id="<%= @cell.id %>">
phx-value-cell_id="<%= @cell_view.id %>">
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
</button>
</span>
@ -57,31 +57,31 @@ defmodule LivebookWeb.CellComponent do
<%= render_editor(assigns) %>
</div>
<div class="markdown" data-element="markdown-container" id="markdown-container-<%= @cell.id %>" phx-update="ignore">
<%= render_markdown_content_placeholder(@cell.source) %>
<div class="markdown" data-element="markdown-container" id="markdown-container-<%= @cell_view.id %>" phx-update="ignore">
<%= render_markdown_content_placeholder(empty: @cell_view.empty?) %>
</div>
</div>
</div>
"""
end
def render_cell_content(%{cell: %{type: :elixir}} = assigns) do
def render_cell_content(%{cell_view: %{type: :elixir}} = assigns) do
~L"""
<div class="mb-1 flex justify-between">
<div class="relative z-10 flex items-center justify-end space-x-2" data-element="actions" data-primary>
<%= if @cell_info.evaluation_status == :ready do %>
<%= if @cell_view.evaluation_status == :ready do %>
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
phx-click="queue_cell_evaluation"
phx-value-cell_id="<%= @cell.id %>">
phx-value-cell_id="<%= @cell_view.id %>">
<%= remix_icon("play-circle-fill", class: "text-xl") %>
<span class="text-sm font-medium">
<%= if(@cell_info.validity_status == :evaluated, do: "Reevaluate", else: "Evaluate") %>
<%= if(@cell_view.validity_status == :evaluated, do: "Reevaluate", else: "Evaluate") %>
</span>
</button>
<% else %>
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
phx-click="cancel_cell_evaluation"
phx-value-cell_id="<%= @cell.id %>">
phx-value-cell_id="<%= @cell_view.id %>">
<%= remix_icon("stop-circle-fill", class: "text-xl") %>
<span class="text-sm font-medium">
Stop
@ -91,14 +91,14 @@ defmodule LivebookWeb.CellComponent do
</div>
<div class="relative z-10 flex items-center justify-end space-x-2" data-element="actions">
<span class="tooltip top" aria-label="Cell settings">
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell.id), class: "icon-button" do %>
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_view.id), class: "icon-button" do %>
<%= remix_icon("list-settings-line", class: "text-xl") %>
<% end %>
</span>
<span class="tooltip top" aria-label="Move up">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>"
phx-value-cell_id="<%= @cell_view.id %>"
phx-value-offset="-1">
<%= remix_icon("arrow-up-s-line", class: "text-xl") %>
</button>
@ -106,7 +106,7 @@ defmodule LivebookWeb.CellComponent do
<span class="tooltip top" aria-label="Move down">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>"
phx-value-cell_id="<%= @cell_view.id %>"
phx-value-offset="1">
<%= remix_icon("arrow-down-s-line", class: "text-xl") %>
</button>
@ -114,7 +114,7 @@ defmodule LivebookWeb.CellComponent do
<span class="tooltip top" aria-label="Delete">
<button class="icon-button"
phx-click="delete_cell"
phx-value-cell_id="<%= @cell.id %>">
phx-value-cell_id="<%= @cell_view.id %>">
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
</button>
</span>
@ -127,7 +127,7 @@ defmodule LivebookWeb.CellComponent do
<div class="w-full">
<%= render_editor(assigns) %>
<%= if @cell.outputs != [] do %>
<%= if @cell_view.outputs != [] do %>
<div class="mt-2">
<%= render_outputs(assigns) %>
</div>
@ -141,19 +141,15 @@ defmodule LivebookWeb.CellComponent do
~L"""
<div class="py-3 rounded-lg overflow-hidden bg-editor relative">
<div
id="editor-container-<%= @cell.id %>"
id="editor-container-<%= @cell_view.id %>"
data-element="editor-container"
phx-update="ignore">
<%= render_editor_content_placeholder(@cell.source) %>
<%= render_editor_content_placeholder(empty: @cell_view.empty?) %>
</div>
<%= if @cell.type == :elixir do %>
<%= if @cell_view.type == :elixir do %>
<div class="absolute bottom-2 right-2">
<%= render_cell_status(
@cell_info.validity_status,
@cell_info.evaluation_status,
@cell_info.digest != @cell_info.evaluation_digest
) %>
<%= render_cell_status(@cell_view.validity_status, @cell_view.evaluation_status, @cell_view.changed?) %>
</div>
<% end %>
</div>
@ -164,7 +160,7 @@ defmodule LivebookWeb.CellComponent do
# There may be a tiny delay before the markdown is rendered
# or and editors are mounted, so show neat placeholders immediately.
defp render_markdown_content_placeholder("" = _content) do
defp render_markdown_content_placeholder(empty: true) do
assigns = %{}
~L"""
@ -172,7 +168,7 @@ defmodule LivebookWeb.CellComponent do
"""
end
defp render_markdown_content_placeholder(_content) do
defp render_markdown_content_placeholder(empty: false) do
assigns = %{}
~L"""
@ -186,7 +182,7 @@ defmodule LivebookWeb.CellComponent do
"""
end
defp render_editor_content_placeholder("" = _content) do
defp render_editor_content_placeholder(empty: true) do
assigns = %{}
~L"""
@ -194,7 +190,7 @@ defmodule LivebookWeb.CellComponent do
"""
end
defp render_editor_content_placeholder(_content) do
defp render_editor_content_placeholder(empty: false) do
assigns = %{}
~L"""
@ -211,9 +207,9 @@ defmodule LivebookWeb.CellComponent do
defp render_outputs(assigns) do
~L"""
<div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200 font-editor">
<%= for {output, index} <- @cell.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
<%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
<div class="p-4">
<%= render_output(output, "#{@cell.id}-output#{index}") %>
<%= render_output(output, "#{@cell_view.id}-output#{index}") %>
</div>
<% end %>
</div>
@ -274,12 +270,7 @@ defmodule LivebookWeb.CellComponent do
end
defp render_cell_status(_, :evaluating, changed) do
render_status_indicator(
"Evaluating",
"bg-blue-500",
"bg-blue-400",
changed
)
render_status_indicator("Evaluating", "bg-blue-500", "bg-blue-400", changed)
end
defp render_cell_status(_, :queued, _) do
@ -287,12 +278,7 @@ defmodule LivebookWeb.CellComponent do
end
defp render_cell_status(:evaluated, _, changed) do
render_status_indicator(
"Evaluated",
"bg-green-400",
nil,
changed
)
render_status_indicator("Evaluated", "bg-green-400", nil, changed)
end
defp render_cell_status(:stale, _, changed) do

View file

@ -3,22 +3,22 @@ defmodule LivebookWeb.SectionComponent do
def render(assigns) do
~L"""
<div data-element="section" data-section-id="<%= @section.id %>">
<div data-element="section" data-section-id="<%= @section_view.id %>">
<div class="flex space-x-4 items-center" data-element="section-headline">
<h2 class="flex-grow text-gray-800 font-semibold text-2xl px-1 -ml-1 rounded-lg border border-transparent hover:border-blue-200 focus:border-blue-300"
data-element="section-name"
id="section-<%= @section.id %>-name"
id="section-<%= @section_view.id %>-name"
contenteditable
spellcheck="false"
phx-blur="set_section_name"
phx-value-section_id="<%= @section.id %>"
phx-value-section_id="<%= @section_view.id %>"
phx-hook="ContentEditable"
data-update-attribute="phx-value-name"><%= @section.name %></h2>
data-update-attribute="phx-value-name"><%= @section_view.name %></h2>
<%# ^ Note it's important there's no space between <h2> and </h2>
because we want the content to exactly match @section.name. %>
because we want the content to exactly match section name. %>
<div class="flex space-x-2 items-center" data-element="section-actions">
<span class="tooltip top" aria-label="Delete">
<button class="icon-button" phx-click="delete_section" phx-value-section_id="<%= @section.id %>" tabindex="-1">
<button class="icon-button" phx-click="delete_section" phx-value-section_id="<%= @section_view.id %>" tabindex="-1">
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
</button>
</span>
@ -26,24 +26,23 @@ defmodule LivebookWeb.SectionComponent do
</div>
<div class="container py-2">
<div class="flex flex-col space-y-1">
<%= for {cell, index} <- Enum.with_index(@section.cells) do %>
<%= for {cell_view, index} <- Enum.with_index(@section_view.cell_views) do %>
<%= live_component @socket, LivebookWeb.InsertButtonsComponent,
id: "#{@section.id}:#{index}",
id: "#{@section_view.id}:#{index}",
persistent: false,
section_id: @section.id,
section_id: @section_view.id,
insert_cell_index: index,
insert_section_index: nil %>
<%= live_component @socket, LivebookWeb.CellComponent,
id: cell.id,
id: cell_view.id,
session_id: @session_id,
cell: cell,
cell_info: @cell_infos[cell.id] %>
cell_view: cell_view %>
<% end %>
<%= live_component @socket, LivebookWeb.InsertButtonsComponent,
id: "#{@section.id}:last",
persistent: @section.cells == [],
section_id: @section.id,
insert_cell_index: length(@section.cells),
id: "#{@section_view.id}:last",
persistent: @section_view.cell_views == [],
section_id: @section_view.id,
insert_cell_index: length(@section_view.cell_views),
insert_section_index: @index + 1 %>
</div>
</div>

View file

@ -18,12 +18,23 @@ defmodule LivebookWeb.SessionLive do
platform = platform_from_socket(socket)
{:ok, assign(socket, initial_assigns(session_id, data, platform))}
{:ok,
socket
|> assign(platform: platform, session_id: session_id, data_view: data_to_view(data))
|> assign_private(data: data)}
else
{:ok, redirect(socket, to: Routes.home_path(socket, :page))}
end
end
# Puts the given assigns in `socket.private`,
# to ensure they are not used for rendering.
defp assign_private(socket, assigns) do
Enum.reduce(assigns, socket, fn {key, value}, socket ->
put_in(socket.private[key], value)
end)
end
defp platform_from_socket(socket) do
with connect_info when connect_info != nil <- get_connect_info(socket),
{:ok, user_agent} <- Map.fetch(connect_info, :user_agent) do
@ -33,14 +44,6 @@ defmodule LivebookWeb.SessionLive do
end
end
defp initial_assigns(session_id, data, platform) do
%{
platform: platform,
session_id: session_id,
data: data
}
end
@impl true
def render(assigns) do
~L"""
@ -50,7 +53,7 @@ defmodule LivebookWeb.SessionLive do
return_to: Routes.session_path(@socket, :page, @session_id),
tab: @tab,
session_id: @session_id,
data: @data %>
data_view: @data_view %>
<% end %>
<%= if @live_action == :shortcuts do %>
@ -101,11 +104,11 @@ defmodule LivebookWeb.SessionLive do
Sections
</h3>
<div class="mt-4 flex flex-col space-y-4" data-element="section-list">
<%= for section <- @data.notebook.sections do %>
<%= for section_item <- @data_view.sections_items do %>
<button class="text-left hover:text-gray-900 text-gray-500"
data-element="section-list-item"
data-section-id="<%= section.id %>">
<%= section.name %>
data-section-id="<%= section_item.id %>">
<%= section_item.name %>
</button>
<% end %>
</div>
@ -125,10 +128,10 @@ defmodule LivebookWeb.SessionLive do
spellcheck="false"
phx-blur="set_notebook_name"
phx-hook="ContentEditable"
data-update-attribute="phx-value-name"><%= @data.notebook.name %></h1>
data-update-attribute="phx-value-name"><%= @data_view.notebook_name %></h1>
</div>
<div class="flex flex-col w-full space-y-16">
<%= if @data.notebook.sections == [] do %>
<%= if @data_view.section_views == [] do %>
<div class="flex justify-center">
<button class="button button-small"
phx-click="insert_section"
@ -136,13 +139,12 @@ defmodule LivebookWeb.SessionLive do
>+ Section</button>
</div>
<% end %>
<%= for {section, index} <- Enum.with_index(@data.notebook.sections) do %>
<%= for {section_view, index} <- Enum.with_index(@data_view.section_views) do %>
<%= live_component @socket, LivebookWeb.SectionComponent,
id: section.id,
id: section_view.id,
index: index,
session_id: @session_id,
section: section,
cell_infos: @data.cell_infos %>
section_view: section_view %>
<% end %>
<div style="height: 80vh"></div>
</div>
@ -158,7 +160,7 @@ defmodule LivebookWeb.SessionLive do
@impl true
def handle_params(%{"cell_id" => cell_id}, _url, socket) do
{:ok, cell, _} = Notebook.fetch_cell_and_section(socket.assigns.data.notebook, cell_id)
{:ok, cell, _} = Notebook.fetch_cell_and_section(socket.private.data.notebook, cell_id)
{:noreply, assign(socket, cell: cell)}
end
@ -172,7 +174,7 @@ defmodule LivebookWeb.SessionLive do
@impl true
def handle_event("cell_init", %{"cell_id" => cell_id}, socket) do
data = socket.assigns.data
data = socket.private.data
case Notebook.fetch_cell_and_section(data.notebook, cell_id) do
{:ok, cell, _section} ->
@ -189,7 +191,7 @@ defmodule LivebookWeb.SessionLive do
end
def handle_event("add_section", _params, socket) do
end_index = length(socket.assigns.data.notebook.sections)
end_index = length(socket.private.data.notebook.sections)
Session.insert_section(socket.assigns.session_id, end_index)
{:noreply, socket}
@ -222,14 +224,14 @@ defmodule LivebookWeb.SessionLive do
def handle_event("insert_cell_below", %{"cell_id" => cell_id, "type" => type}, socket) do
type = String.to_atom(type)
insert_cell_next_to(socket.assigns, cell_id, type, idx_offset: 1)
insert_cell_next_to(socket, cell_id, type, idx_offset: 1)
{:noreply, socket}
end
def handle_event("insert_cell_above", %{"cell_id" => cell_id, "type" => type}, socket) do
type = String.to_atom(type)
insert_cell_next_to(socket.assigns, cell_id, type, idx_offset: 0)
insert_cell_next_to(socket, cell_id, type, idx_offset: 0)
{:noreply, socket}
end
@ -288,7 +290,7 @@ defmodule LivebookWeb.SessionLive do
end
def handle_event("queue_section_cells_evaluation", %{"section_id" => section_id}, socket) do
with {:ok, section} <- Notebook.fetch_section(socket.assigns.data.notebook, section_id) do
with {:ok, section} <- Notebook.fetch_section(socket.private.data.notebook, section_id) do
for cell <- section.cells, cell.type == :elixir do
Session.queue_cell_evaluation(socket.assigns.session_id, cell.id)
end
@ -298,7 +300,7 @@ defmodule LivebookWeb.SessionLive do
end
def handle_event("queue_all_cells_evaluation", _params, socket) do
data = socket.assigns.data
data = socket.private.data
for {cell, _} <- Notebook.elixir_cells_with_section(data.notebook),
data.cell_infos[cell.id].validity_status != :evaluated do
@ -310,8 +312,8 @@ defmodule LivebookWeb.SessionLive do
def handle_event("queue_child_cells_evaluation", %{"cell_id" => cell_id}, socket) do
with {:ok, cell, _section} <-
Notebook.fetch_cell_and_section(socket.assigns.data.notebook, cell_id) do
for {cell, _} <- Notebook.child_cells_with_section(socket.assigns.data.notebook, cell.id) do
Notebook.fetch_cell_and_section(socket.private.data.notebook, cell_id) do
for {cell, _} <- Notebook.child_cells_with_section(socket.private.data.notebook, cell.id) do
Session.queue_cell_evaluation(socket.assigns.session_id, cell.id)
end
end
@ -339,11 +341,12 @@ defmodule LivebookWeb.SessionLive do
@impl true
def handle_info({:operation, operation}, socket) do
case Session.Data.apply_operation(socket.assigns.data, operation) do
case Session.Data.apply_operation(socket.private.data, operation) do
{:ok, data, actions} ->
new_socket =
socket
|> assign(data: data)
|> assign_private(data: data)
|> assign(data_view: data_to_view(data))
|> after_operation(socket, operation)
|> handle_actions(actions)
@ -398,12 +401,12 @@ defmodule LivebookWeb.SessionLive do
defp after_operation(socket, prev_socket, {:delete_cell, _client_pid, cell_id}) do
# Find a sibling cell that the client would focus if the deleted cell has focus.
sibling_cell_id =
case Notebook.fetch_cell_sibling(prev_socket.assigns.data.notebook, cell_id, 1) do
case Notebook.fetch_cell_sibling(prev_socket.private.data.notebook, cell_id, 1) do
{:ok, next_cell} ->
next_cell.id
:error ->
case Notebook.fetch_cell_sibling(prev_socket.assigns.data.notebook, cell_id, -1) do
case Notebook.fetch_cell_sibling(prev_socket.private.data.notebook, cell_id, -1) do
{:ok, previous_cell} -> previous_cell.id
:error -> nil
end
@ -446,12 +449,52 @@ defmodule LivebookWeb.SessionLive do
end
end
defp insert_cell_next_to(assigns, cell_id, type, idx_offset: idx_offset) do
{:ok, cell, section} = Notebook.fetch_cell_and_section(assigns.data.notebook, cell_id)
defp insert_cell_next_to(socket, cell_id, type, idx_offset: idx_offset) do
{:ok, cell, section} = Notebook.fetch_cell_and_section(socket.private.data.notebook, cell_id)
index = Enum.find_index(section.cells, &(&1 == cell))
Session.insert_cell(assigns.session_id, section.id, index + idx_offset, type)
Session.insert_cell(socket.assigns.session_id, section.id, index + idx_offset, type)
end
defp ensure_integer(n) when is_integer(n), do: n
defp ensure_integer(n) when is_binary(n), do: String.to_integer(n)
# Builds view-specific structure of data by cherry-picking
# only the relevant attributes.
# We then use `@data_view` in the templates and consequently
# irrelevant changes to data don't change `@data_view`, so LV doesn't
# have to traverse the whole template tree and no diff is sent to the client.
defp data_to_view(data) do
%{
path: data.path,
runtime: data.runtime,
notebook_name: data.notebook.name,
sections_items:
for section <- data.notebook.sections do
%{id: section.id, name: section.name}
end,
section_views: Enum.map(data.notebook.sections, &section_to_view(&1, data))
}
end
defp section_to_view(section, data) do
%{
id: section.id,
name: section.name,
cell_views: Enum.map(section.cells, &cell_to_view(&1, data))
}
end
defp cell_to_view(cell, data) do
info = data.cell_infos[cell.id]
%{
id: cell.id,
type: cell.type,
empty?: cell.source == "",
outputs: cell.outputs,
validity_status: info.validity_status,
evaluation_status: info.evaluation_status,
changed?: info.evaluation_digest != nil and info.digest != info.evaluation_digest
}
end
end

View file

@ -31,14 +31,14 @@ defmodule LivebookWeb.SessionLive.SettingsComponent do
<%= live_component @socket, LivebookWeb.SessionLive.PersistenceComponent,
id: :persistence,
session_id: @session_id,
current_path: @data.path,
path: @data.path %>
current_path: @data_view.path,
path: @data_view.path %>
<% end %>
<%= if @tab == "runtime" do %>
<%= live_component @socket, LivebookWeb.SessionLive.RuntimeComponent,
id: :runtime,
session_id: @session_id,
runtime: @data.runtime %>
runtime: @data_view.runtime %>
<% end %>
</div>
</div>