From 9e69e8f09659956e62eeabefe53e28ed43df634b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 22 Mar 2021 15:43:31 +0100 Subject: [PATCH] Add changed cell indicator (#105) --- lib/livebook/session/data.ex | 37 ++++++++---- lib/livebook_web/live/cell_component.ex | 80 ++++++++++++------------- test/livebook/session/data_test.exs | 45 ++++++++++++++ 3 files changed, 111 insertions(+), 51 deletions(-) diff --git a/lib/livebook/session/data.ex b/lib/livebook/session/data.ex index f476d4a8b..2883b2d40 100644 --- a/lib/livebook/session/data.ex +++ b/lib/livebook/session/data.ex @@ -52,7 +52,8 @@ defmodule Livebook.Session.Data do revision: cell_revision(), deltas: list(Delta.t()), revision_by_client_pid: %{pid() => cell_revision()}, - evaluated_at: DateTime.t() + digest: String.t(), + evaluation_digest: String.t() | nil } @type cell_revision :: non_neg_integer() @@ -125,7 +126,7 @@ defmodule Livebook.Session.Data do for section <- notebook.sections, cell <- section.cells, into: %{}, - do: {cell.id, new_cell_info([])} + do: {cell.id, new_cell_info(cell, [])} end @doc """ @@ -415,7 +416,7 @@ defmodule Livebook.Session.Data do data_actions |> set!( notebook: Notebook.insert_cell(data.notebook, section_id, index, cell), - cell_infos: Map.put(data.cell_infos, cell.id, new_cell_info(data.client_pids)) + cell_infos: Map.put(data.cell_infos, cell.id, new_cell_info(cell, data.client_pids)) ) end @@ -512,8 +513,7 @@ defmodule Livebook.Session.Data do data_actions |> set_cell_info!(cell.id, validity_status: :evaluated, - evaluation_status: :ready, - evaluated_at: DateTime.utc_now() + evaluation_status: :ready ) |> set_section_info!(section.id, evaluating_cell_id: nil) end @@ -553,7 +553,9 @@ defmodule Livebook.Session.Data do data_actions |> set!(notebook: Notebook.update_cell(data.notebook, id, &%{&1 | outputs: []})) - |> set_cell_info!(id, evaluation_status: :evaluating) + |> update_cell_info!(id, fn info -> + %{info | evaluation_status: :evaluating, evaluation_digest: info.digest} + end) |> set_section_info!(section.id, evaluating_cell_id: id, evaluation_queue: ids) |> add_action({:start_evaluation, cell, section}) @@ -575,7 +577,11 @@ defmodule Livebook.Session.Data do |> set_section_info!(section.id, evaluating_cell_id: nil, evaluation_queue: []) |> reduce( section.cells, - &set_cell_info!(&1, &2.id, validity_status: :fresh, evaluation_status: :ready) + &set_cell_info!(&1, &2.id, + validity_status: :fresh, + evaluation_status: :ready, + evaluation_digest: nil + ) ) end @@ -651,10 +657,18 @@ defmodule Livebook.Session.Data do new_source = JSInterop.apply_delta_to_string(transformed_new_delta, cell.source) + new_digest = compute_digest(new_source) + data_actions |> set!(notebook: Notebook.update_cell(data.notebook, cell.id, &%{&1 | source: new_source})) |> update_cell_info!(cell.id, fn info -> - info = %{info | deltas: info.deltas ++ [transformed_new_delta], revision: info.revision + 1} + info = %{ + info + | deltas: info.deltas ++ [transformed_new_delta], + revision: info.revision + 1, + digest: new_digest + } + # Before receiving acknowledgement, the client receives all the other deltas, # so we can assume they are in sync with the server and have the same revision. info = put_in(info.revision_by_client_pid[client_pid], info.revision) @@ -709,14 +723,15 @@ defmodule Livebook.Session.Data do } end - defp new_cell_info(client_pids) do + defp new_cell_info(cell, client_pids) do %{ revision: 0, deltas: [], revision_by_client_pid: Map.new(client_pids, &{&1, 0}), validity_status: :fresh, evaluation_status: :ready, - evaluated_at: nil + digest: compute_digest(cell.source), + evaluation_digest: nil } end @@ -770,6 +785,8 @@ defmodule Livebook.Session.Data do set!(data_actions, dirty: dirty) end + defp compute_digest(source), do: :erlang.md5(source) + @doc """ Finds the cell that's currently being evaluated in the given section. """ diff --git a/lib/livebook_web/live/cell_component.ex b/lib/livebook_web/live/cell_component.ex index 22a6cdf0e..7b69d0c75 100644 --- a/lib/livebook_web/live/cell_component.ex +++ b/lib/livebook_web/live/cell_component.ex @@ -249,55 +249,53 @@ defmodule LivebookWeb.CellComponent do |> String.split("\n") end - defp render_cell_status(%{evaluation_status: :evaluating}) do - assigns = %{} - - ~L""" -
-
Evaluating
- - - - -
- """ + defp render_cell_status(%{evaluation_status: :evaluating} = info) do + render_status_indicator( + "Evaluating", + "bg-blue-500", + "bg-blue-400", + info.digest != info.evaluation_digest + ) end defp render_cell_status(%{evaluation_status: :queued}) do - assigns = %{} + render_status_indicator("Queued", "bg-gray-500", "bg-gray-400", false) + end + + defp render_cell_status(%{validity_status: :evaluated} = info) do + render_status_indicator( + "Evaluated", + "bg-green-400", + nil, + info.digest != info.evaluation_digest + ) + end + + defp render_cell_status(%{validity_status: :stale} = info) do + render_status_indicator("Stale", "bg-yellow-200", nil, info.digest != info.evaluation_digest) + end + + defp render_cell_status(_), do: nil + + defp render_status_indicator(text, circle_class, animated_circle_class, show_changed) do + assigns = %{ + text: text, + circle_class: circle_class, + animated_circle_class: animated_circle_class, + show_changed: show_changed + } ~L""" -
-
Queued
+
+
+ <%= @text %> + ">* +
- - + +
""" end - - defp render_cell_status(%{validity_status: :evaluated}) do - assigns = %{} - - ~L""" -
-
Evaluated
-
-
- """ - end - - defp render_cell_status(%{validity_status: :stale}) do - assigns = %{} - - ~L""" -
-
Stale
-
-
- """ - end - - defp render_cell_status(_), do: nil end diff --git a/test/livebook/session/data_test.exs b/test/livebook/session/data_test.exs index 2512f5186..c93edf9f6 100644 --- a/test/livebook/session/data_test.exs +++ b/test/livebook/session/data_test.exs @@ -665,6 +665,32 @@ defmodule Livebook.Session.DataTest do }, []} = Data.apply_operation(data, operation) end + test "updates cell evaluation digest" do + data = + data_after_operations!([ + {:insert_section, self(), 0, "s1"}, + {:insert_cell, self(), "s1", 0, :elixir, "c1"}, + {:insert_cell, self(), "s1", 1, :elixir, "c2"}, + {:queue_cell_evaluation, self(), "c1"} + ]) + + %{cell_infos: %{"c1" => %{digest: digest}}} = data + + operation = {:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}} + + assert {:ok, + %{ + cell_infos: %{ + "c1" => %{ + validity_status: :evaluated, + evaluation_status: :ready, + evaluation_digest: ^digest + } + }, + section_infos: %{"s1" => %{evaluating_cell_id: nil, evaluation_queue: []}} + }, []} = Data.apply_operation(data, operation) + end + test "marks next queued cell in this section as evaluating if there is one" do data = data_after_operations!([ @@ -1048,6 +1074,25 @@ defmodule Livebook.Session.DataTest do }, _actions} = Data.apply_operation(data, operation) end + test "updates cell digest based on the new content" do + data = + data_after_operations!([ + {:client_join, self()}, + {:insert_section, self(), 0, "s1"}, + {:insert_cell, self(), "s1", 0, :elixir, "c1"} + ]) + + %{cell_infos: %{"c1" => %{digest: digest}}} = data + + delta = Delta.new() |> Delta.insert("cats") + operation = {:apply_cell_delta, self(), "c1", delta, 1} + + assert {:ok, %{cell_infos: %{"c1" => %{digest: new_digest}}}, _actions} = + Data.apply_operation(data, operation) + + assert digest != new_digest + end + test "transforms the delta if the revision is not the most recent" do client1_pid = IEx.Helpers.pid(0, 0, 0) client2_pid = IEx.Helpers.pid(0, 0, 1)