Packages
livebook
0.3.2
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/evaluator.ex
defmodule Livebook.Evaluator do
@moduledoc false
# A process responsible for evaluating notebook code.
#
# The process receives evaluation request and synchronously
# evaluates the given code within itself (rather than spawning a separate process).
# It stores the resulting binding and env as part of the state.
#
# It's important to store the binding in the same process
# where the evaluation happens, as otherwise we would have to
# send them between processes, effectively copying potentially large data.
#
# Note that this process is intentionally not a GenServer,
# because we during evaluation we may receive arbitrary
# messages and we don't want to consume them from the inbox,
# as GenServer does.
require Logger
alias Livebook.Evaluator
@type t :: %{pid: pid(), ref: reference()}
@type state :: %{
formatter: module(),
io_proxy: pid(),
contexts: %{ref() => context()},
initial_context: context(),
# We track the widgets rendered by every evaluation,
# so that we can kill those no longer needed
widget_pids: %{ref() => MapSet.t(pid())},
widget_counts: %{pid() => non_neg_integer()}
}
@typedoc """
An evaluation context.
"""
@type context :: %{binding: Code.binding(), env: Macro.Env.t(), id: binary()}
@typedoc """
A term used to identify evaluation.
"""
@type ref :: term()
@typedoc """
Either {:ok, result} for successful evaluation
or {:error, kind, error, stacktrace} for a failed one.
"""
@type evaluation_response ::
{:ok, any()} | {:error, Exception.kind(), any(), Exception.stacktrace()}
## API
@doc """
Starts the evaluator.
Options:
* `formatter` - a module implementing the `Livebook.Evaluator.Formatter` behaviour,
used for transforming evaluation response before it's sent to the client
"""
@spec start_link(keyword()) :: {:ok, pid(), t()} | {:error, term()}
def start_link(opts \\ []) do
case :proc_lib.start_link(__MODULE__, :init, [opts]) do
{:error, error} -> {:error, error}
evaluator -> {:ok, evaluator.pid, evaluator}
end
end
@doc """
Asynchronously parses and evaluates the given code.
Any exceptions are captured, in which case this method returns an error.
The evaluator stores the resulting binding and environment under `ref`.
Any subsequent calls may specify `prev_ref` pointing to a previous evaluation,
in which case the corresponding binding and environment are used during evaluation.
Evaluation response is sent to the process identified by `send_to` as `{:evaluation_response, ref, response, metadata}`.
Note that response is transformed with the configured formatter (identity by default).
## Options
* `:file` - file to which the evaluated code belongs. Most importantly,
this has an impact on the value of `__DIR__`.
"""
@spec evaluate_code(t(), pid(), String.t(), ref(), ref() | nil, keyword()) :: :ok
def evaluate_code(evaluator, send_to, code, ref, prev_ref \\ nil, opts \\ []) when ref != nil do
cast(evaluator, {:evaluate_code, send_to, code, ref, prev_ref, opts})
end
@doc """
Fetches evaluation context (binding and environment) by evaluation reference.
## Options
* `cached_id` - id of context that the sender may already have,
if it matches the fetched context the `{:error, :not_modified}`
tuple is returned instead
"""
@spec fetch_evaluation_context(t(), ref(), keyword()) ::
{:ok, context()} | {:error, :not_modified}
def fetch_evaluation_context(evaluator, ref, opts \\ []) do
cached_id = opts[:cached_id]
call(evaluator, {:fetch_evaluation_context, ref, cached_id})
end
@doc """
Fetches an evaluation context from another `Evaluator` process
and configures it as the initial context for this evaluator.
The process dictionary is also copied to match the given evaluator.
"""
@spec initialize_from(t(), t(), ref()) :: :ok
def initialize_from(evaluator, source_evaluator, source_evaluation_ref) do
call(evaluator, {:initialize_from, source_evaluator, source_evaluation_ref})
end
@doc """
Removes the evaluation identified by `ref` from history,
so that further evaluations cannot use it.
"""
@spec forget_evaluation(t(), ref()) :: :ok
def forget_evaluation(evaluator, ref) do
cast(evaluator, {:forget_evaluation, ref})
end
@doc """
Asynchronously handles the given intellisense request.
If `evaluation_ref` is given, its binding and environment are also
used as context for the intellisense. Response is sent to the `send_to`
process as `{:intellisense_response, ref, response}`.
"""
@spec handle_intellisense(
t(),
pid(),
term(),
Livebook.Runtime.intellisense_request(),
ref() | nil
) :: :ok
def handle_intellisense(evaluator, send_to, ref, request, evaluation_ref \\ nil) do
cast(evaluator, {:handle_intellisense, send_to, ref, request, evaluation_ref})
end
defp cast(evaluator, message) do
send(evaluator.pid, {:cast, evaluator.ref, message})
:ok
end
defp call(evaluator, message) do
call_ref = make_ref()
send(evaluator.pid, {:call, evaluator.ref, self(), call_ref, message})
receive do
{^call_ref, reply} -> reply
end
end
## Callbacks
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :temporary
}
end
def init(opts) do
formatter = Keyword.get(opts, :formatter, Evaluator.IdentityFormatter)
{:ok, io_proxy} = Evaluator.IOProxy.start_link()
# Use the dedicated IO device as the group leader,
# so that it handles all :stdio operations.
Process.group_leader(self(), io_proxy)
evaluator_ref = make_ref()
state = initial_state(evaluator_ref, formatter, io_proxy)
evaluator = %{pid: self(), ref: evaluator_ref}
:proc_lib.init_ack(evaluator)
loop(state)
end
defp initial_state(evaluator_ref, formatter, io_proxy) do
%{
evaluator_ref: evaluator_ref,
formatter: formatter,
io_proxy: io_proxy,
contexts: %{},
initial_context: initial_context(),
widget_pids: %{},
widget_counts: %{}
}
end
defp loop(%{evaluator_ref: evaluator_ref} = state) do
receive do
{:call, ^evaluator_ref, pid, ref, message} ->
{:reply, reply, state} = handle_call(message, pid, state)
send(pid, {ref, reply})
loop(state)
{:cast, ^evaluator_ref, message} ->
{:noreply, state} = handle_cast(message, state)
loop(state)
end
end
defp initial_context() do
env = :elixir.env_for_eval([])
%{binding: [], env: env, id: random_id()}
end
defp handle_cast({:evaluate_code, send_to, code, ref, prev_ref, opts}, state) do
Evaluator.IOProxy.configure(state.io_proxy, send_to, ref)
context = get_context(state, prev_ref)
file = Keyword.get(opts, :file, "nofile")
context = put_in(context.env.file, file)
start_time = System.monotonic_time()
{result_context, response} =
case eval(code, context.binding, context.env) do
{:ok, result, binding, env} ->
result_context = %{binding: binding, env: env, id: random_id()}
response = {:ok, result}
{result_context, response}
{:error, kind, error, stacktrace} ->
response = {:error, kind, error, stacktrace}
{context, response}
end
evaluation_time_ms = get_execution_time_delta(start_time)
state = put_in(state.contexts[ref], result_context)
Evaluator.IOProxy.flush(state.io_proxy)
Evaluator.IOProxy.clear_input_buffers(state.io_proxy)
output = state.formatter.format_response(response)
metadata = %{evaluation_time_ms: evaluation_time_ms}
send(send_to, {:evaluation_response, ref, output, metadata})
widget_pids = Evaluator.IOProxy.flush_widgets(state.io_proxy)
state = track_evaluation_widgets(state, ref, widget_pids, output)
{:noreply, state}
end
defp handle_cast({:forget_evaluation, ref}, state) do
state =
state
|> Map.update!(:contexts, &Map.delete(&1, ref))
|> garbage_collect_widgets(ref, [])
{:noreply, state}
end
defp handle_cast({:handle_intellisense, send_to, ref, request, evaluation_ref}, state) do
context = get_context(state, evaluation_ref)
# Safely rescue from intellisense errors
response =
try do
Livebook.Intellisense.handle_request(request, context.binding, context.env)
rescue
error -> Logger.error(Exception.format(:error, error, __STACKTRACE__))
end
send(send_to, {:intellisense_response, ref, request, response})
{:noreply, state}
end
defp handle_call({:fetch_evaluation_context, ref, cached_id}, _from, state) do
context = get_context(state, ref)
reply =
if context.id == cached_id do
{:error, :not_modified}
else
{:ok, context}
end
{:reply, reply, state}
end
defp handle_call({:initialize_from, source_evaluator, source_evaluation_ref}, _from, state) do
state =
case Evaluator.fetch_evaluation_context(
source_evaluator,
source_evaluation_ref,
cached_id: state.initial_context.id
) do
{:ok, context} ->
# If the context changed, mirror the process dictionary again
copy_process_dictionary_from(source_evaluator)
put_in(state.initial_context, context)
{:error, :not_modified} ->
state
end
{:reply, :ok, state}
end
defp get_context(state, ref) do
Map.get_lazy(state.contexts, ref, fn -> state.initial_context end)
end
defp eval(code, binding, env) do
try do
quoted = Code.string_to_quoted!(code)
{result, binding, env} = :elixir.eval_quoted(quoted, binding, env)
{:ok, result, binding, env}
catch
kind, error ->
{kind, error, stacktrace} = prepare_error(kind, error, __STACKTRACE__)
{:error, kind, error, stacktrace}
end
end
defp prepare_error(kind, error, stacktrace) do
{error, stacktrace} = Exception.blame(kind, error, stacktrace)
stacktrace = prune_stacktrace(stacktrace)
{kind, error, stacktrace}
end
# Adapted from https://github.com/elixir-lang/elixir/blob/1c1654c88adfdbef38ff07fc30f6fbd34a542c07/lib/iex/lib/iex/evaluator.ex#L355-L372
@elixir_internals [:elixir, :elixir_expand, :elixir_compiler, :elixir_module] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_pass]
defp prune_stacktrace(stacktrace) do
# The order in which each drop_while is listed is important.
# For example, the user may call Code.eval_string/2 in their code
# and if there is an error we should not remove erl_eval
# and eval_bits information from the user stacktrace.
stacktrace
|> Enum.reverse()
|> Enum.drop_while(&(elem(&1, 0) == :proc_lib))
|> Enum.drop_while(&(elem(&1, 0) == :gen_server))
|> Enum.drop_while(&(elem(&1, 0) == __MODULE__))
|> Enum.drop_while(&(elem(&1, 0) == :elixir))
|> Enum.drop_while(&(elem(&1, 0) in [:erl_eval, :eval_bits]))
|> Enum.reverse()
|> Enum.reject(&(elem(&1, 0) in @elixir_internals))
end
defp random_id() do
:crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower)
end
defp copy_process_dictionary_from(source_evaluator) do
{:dictionary, dictionary} = Process.info(source_evaluator.pid, :dictionary)
for {key, value} <- dictionary, not internal_dictionary_key?(key) do
Process.put(key, value)
end
end
defp internal_dictionary_key?("$" <> _), do: true
defp internal_dictionary_key?(_), do: false
# Widgets
defp track_evaluation_widgets(state, ref, widget_pids, output) do
widget_pids =
case widget_pid_from_output(output) do
{:ok, pid} -> MapSet.put(widget_pids, pid)
:error -> widget_pids
end
garbage_collect_widgets(state, ref, widget_pids)
end
defp garbage_collect_widgets(state, ref, widget_pids) do
prev_widget_pids = state.widget_pids[ref] || []
state = put_in(state.widget_pids[ref], widget_pids)
update_in(state.widget_counts, fn counts ->
counts =
Enum.reduce(prev_widget_pids, counts, fn pid, counts ->
Map.update!(counts, pid, &(&1 - 1))
end)
counts =
Enum.reduce(widget_pids, counts, fn pid, counts ->
Map.update(counts, pid, 1, &(&1 + 1))
end)
{to_remove, to_keep} = Enum.split_with(counts, fn {_pid, count} -> count == 0 end)
for {pid, 0} <- to_remove do
Process.exit(pid, :shutdown)
end
Map.new(to_keep)
end)
end
@doc """
Checks the given output value for widget pid to track.
"""
@spec widget_pid_from_output(term()) :: {:ok, pid()} | :error
def widget_pid_from_output(output)
def widget_pid_from_output({_type, pid}) when is_pid(pid) do
{:ok, pid}
end
def widget_pid_from_output(_output), do: :error
defp get_execution_time_delta(started_at) do
System.monotonic_time()
|> Kernel.-(started_at)
|> System.convert_time_unit(:native, :millisecond)
end
end