mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-08 20:46:16 +08:00
Improve reproducability of module definitions (#1518)
This commit is contained in:
parent
484e47142a
commit
936146e52c
3 changed files with 111 additions and 21 deletions
|
@ -328,17 +328,25 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
|
|
||||||
set_pdict(context, state.ignored_pdict_keys)
|
set_pdict(context, state.ignored_pdict_keys)
|
||||||
|
|
||||||
|
if old_context = state.contexts[ref] do
|
||||||
|
for module <- old_context.env.context_modules do
|
||||||
|
# If there is a deleted code for the module, we purge it first
|
||||||
|
:code.purge(module)
|
||||||
|
:code.delete(module)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
start_time = System.monotonic_time()
|
start_time = System.monotonic_time()
|
||||||
eval_result = eval(code, context.binding, context.env)
|
eval_result = eval(code, context.binding, context.env)
|
||||||
evaluation_time_ms = time_diff_ms(start_time)
|
evaluation_time_ms = time_diff_ms(start_time)
|
||||||
|
|
||||||
{result_context, result, code_error, identifiers_used, identifiers_defined} =
|
{new_context, result, code_error, identifiers_used, identifiers_defined} =
|
||||||
case eval_result do
|
case eval_result do
|
||||||
{:ok, value, binding, env} ->
|
{:ok, value, binding, env} ->
|
||||||
tracer_info = Evaluator.IOProxy.get_tracer_info(state.io_proxy)
|
tracer_info = Evaluator.IOProxy.get_tracer_info(state.io_proxy)
|
||||||
context_id = random_id()
|
context_id = random_id()
|
||||||
|
|
||||||
result_context = %{
|
new_context = %{
|
||||||
id: context_id,
|
id: context_id,
|
||||||
binding: binding,
|
binding: binding,
|
||||||
env: prune_env(env, tracer_info),
|
env: prune_env(env, tracer_info),
|
||||||
|
@ -346,21 +354,21 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
}
|
}
|
||||||
|
|
||||||
{identifiers_used, identifiers_defined} =
|
{identifiers_used, identifiers_defined} =
|
||||||
identifier_dependencies(result_context, tracer_info, context)
|
identifier_dependencies(new_context, tracer_info, context)
|
||||||
|
|
||||||
result = {:ok, value}
|
result = {:ok, value}
|
||||||
{result_context, result, nil, identifiers_used, identifiers_defined}
|
{new_context, result, nil, identifiers_used, identifiers_defined}
|
||||||
|
|
||||||
{:error, kind, error, stacktrace, code_error} ->
|
{:error, kind, error, stacktrace, code_error} ->
|
||||||
result = {:error, kind, error, stacktrace}
|
result = {:error, kind, error, stacktrace}
|
||||||
identifiers_used = :unknown
|
identifiers_used = :unknown
|
||||||
identifiers_defined = %{}
|
identifiers_defined = %{}
|
||||||
# Empty context
|
# Empty context
|
||||||
result_context = initial_context()
|
new_context = initial_context()
|
||||||
{result_context, result, code_error, identifiers_used, identifiers_defined}
|
{new_context, result, code_error, identifiers_used, identifiers_defined}
|
||||||
end
|
end
|
||||||
|
|
||||||
state = put_context(state, ref, result_context)
|
state = put_context(state, ref, new_context)
|
||||||
|
|
||||||
Evaluator.IOProxy.flush(state.io_proxy)
|
Evaluator.IOProxy.flush(state.io_proxy)
|
||||||
Evaluator.IOProxy.clear_input_cache(state.io_proxy)
|
Evaluator.IOProxy.clear_input_cache(state.io_proxy)
|
||||||
|
@ -386,7 +394,16 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_cast({:forget_evaluation, ref}, state) do
|
defp handle_cast({:forget_evaluation, ref}, state) do
|
||||||
state = delete_context(state, ref)
|
{context, state} = pop_context(state, ref)
|
||||||
|
|
||||||
|
for module <- context.env.context_modules do
|
||||||
|
# If there is a deleted code for the module, we purge it first
|
||||||
|
:code.purge(module)
|
||||||
|
:code.delete(module)
|
||||||
|
# And we immediately purge the newly deleted code
|
||||||
|
:code.purge(module)
|
||||||
|
end
|
||||||
|
|
||||||
Evaluator.ObjectTracker.remove_reference_sync(state.object_tracker, {self(), ref})
|
Evaluator.ObjectTracker.remove_reference_sync(state.object_tracker, {self(), ref})
|
||||||
|
|
||||||
:erlang.garbage_collect(self())
|
:erlang.garbage_collect(self())
|
||||||
|
@ -446,14 +463,13 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
put_in(state.contexts[ref], context)
|
put_in(state.contexts[ref], context)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_context(state, ref) do
|
defp pop_context(state, ref) do
|
||||||
update_evaluator_info(fn info ->
|
update_evaluator_info(fn info ->
|
||||||
{_, info} = pop_in(info.contexts[ref])
|
{_, info} = pop_in(info.contexts[ref])
|
||||||
info
|
info
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{_, state} = pop_in(state.contexts[ref])
|
pop_in(state.contexts[ref])
|
||||||
state
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp update_evaluator_info(fun) do
|
defp update_evaluator_info(fun) do
|
||||||
|
@ -494,6 +510,7 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
env
|
env
|
||||||
|> Map.replace!(:aliases, Map.to_list(tracer_info.aliases_defined))
|
|> Map.replace!(:aliases, Map.to_list(tracer_info.aliases_defined))
|
||||||
|> Map.replace!(:requires, MapSet.to_list(tracer_info.requires_defined))
|
|> Map.replace!(:requires, MapSet.to_list(tracer_info.requires_defined))
|
||||||
|
|> Map.replace!(:context_modules, Map.keys(tracer_info.modules_defined))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp merge_context(prev_context, context) do
|
defp merge_context(prev_context, context) do
|
||||||
|
@ -523,7 +540,7 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
end)
|
end)
|
||||||
|> Map.update!(:aliases, &Keyword.merge(prev_env.aliases, &1))
|
|> Map.update!(:aliases, &Keyword.merge(prev_env.aliases, &1))
|
||||||
|> Map.update!(:requires, &:ordsets.union(prev_env.requires, &1))
|
|> Map.update!(:requires, &:ordsets.union(prev_env.requires, &1))
|
||||||
|> Map.replace!(:context_modules, [])
|
|> Map.update!(:context_modules, &(&1 ++ prev_env.context_modules))
|
||||||
end
|
end
|
||||||
|
|
||||||
@compile {:no_warn_undefined, {Code, :eval_quoted_with_env, 4}}
|
@compile {:no_warn_undefined, {Code, :eval_quoted_with_env, 4}}
|
||||||
|
@ -590,7 +607,8 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
into: identifiers_used
|
into: identifiers_used
|
||||||
|
|
||||||
identifiers_defined =
|
identifiers_defined =
|
||||||
for {module, {version, _vars}} <- tracer_info.modules_defined,
|
for {module, _vars} <- tracer_info.modules_defined,
|
||||||
|
version = module.__info__(:md5),
|
||||||
do: {{:module, module}, version},
|
do: {{:module, module}, version},
|
||||||
into: identifiers_defined
|
into: identifiers_defined
|
||||||
|
|
||||||
|
@ -667,7 +685,7 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
# Note that :prune_binding removes variables used by modules
|
# Note that :prune_binding removes variables used by modules
|
||||||
# (unless used outside), so we get those from the tracer
|
# (unless used outside), so we get those from the tracer
|
||||||
module_used_vars =
|
module_used_vars =
|
||||||
for {_module, {_version, vars}} <- tracer_info.modules_defined,
|
for {_module, vars} <- tracer_info.modules_defined,
|
||||||
var <- vars,
|
var <- vars,
|
||||||
into: MapSet.new(),
|
into: MapSet.new(),
|
||||||
do: var
|
do: var
|
||||||
|
@ -686,6 +704,8 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
do: var
|
do: var
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp prune_stacktrace([{Livebook.Runtime.Evaluator.Tracer, _fun, _arity, _meta} | _]), do: []
|
||||||
|
|
||||||
# Adapted from https://github.com/elixir-lang/elixir/blob/1c1654c88adfdbef38ff07fc30f6fbd34a542c07/lib/iex/lib/iex/evaluator.ex#L355-L372
|
# Adapted from https://github.com/elixir-lang/elixir/blob/1c1654c88adfdbef38ff07fc30f6fbd34a542c07/lib/iex/lib/iex/evaluator.ex#L355-L372
|
||||||
# TODO: Remove else branch once we depend on the versions below
|
# TODO: Remove else branch once we depend on the versions below
|
||||||
if System.otp_release() >= "25" do
|
if System.otp_release() >= "25" do
|
||||||
|
|
|
@ -21,7 +21,7 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def trace(event, env) do
|
def trace(event, env) do
|
||||||
case to_updates(event, env) do
|
case event_to_updates(event, env) do
|
||||||
[] ->
|
[] ->
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
|
@ -33,11 +33,21 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
defp to_updates(event, env) do
|
defp event_to_updates(event, env) do
|
||||||
# Note that import/require/alias/defmodule don't trigger `:alias_reference`
|
# Note that import/require/alias/defmodule don't trigger `:alias_reference`
|
||||||
# for the used alias, so we add it explicitly
|
# for the used alias, so we add it explicitly
|
||||||
|
|
||||||
case event do
|
case event do
|
||||||
|
:start ->
|
||||||
|
if Code.ensure_loaded?(env.module) do
|
||||||
|
raise CompileError,
|
||||||
|
line: env.line,
|
||||||
|
file: env.file,
|
||||||
|
description: "module #{inspect(env.module)} is already defined"
|
||||||
|
end
|
||||||
|
|
||||||
|
[]
|
||||||
|
|
||||||
{:import, _meta, module, _opts} ->
|
{:import, _meta, module, _opts} ->
|
||||||
if(env.module, do: [], else: [:import_defined]) ++
|
if(env.module, do: [], else: [:import_defined]) ++
|
||||||
[{:module_used, module}, {:alias_used, module}]
|
[{:module_used, module}, {:alias_used, module}]
|
||||||
|
@ -73,11 +83,10 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
|
||||||
{:remote_macro, _meta, module, _name, _arity} ->
|
{:remote_macro, _meta, module, _name, _arity} ->
|
||||||
[{:module_used, module}, {:require_used, module}]
|
[{:module_used, module}, {:require_used, module}]
|
||||||
|
|
||||||
{:on_module, bytecode, _ignore} ->
|
{:on_module, _bytecode, _ignore} ->
|
||||||
module = env.module
|
module = env.module
|
||||||
version = :erlang.md5(bytecode)
|
|
||||||
vars = Map.keys(env.versioned_vars)
|
vars = Map.keys(env.versioned_vars)
|
||||||
[{:module_defined, module, version, vars}, {:alias_used, module}]
|
[{:module_defined, module, vars}, {:alias_used, module}]
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
[]
|
[]
|
||||||
|
@ -96,8 +105,8 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
|
||||||
update_in(info.modules_used, &MapSet.put(&1, module))
|
update_in(info.modules_used, &MapSet.put(&1, module))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_update(info, {:module_defined, module, version, vars}) do
|
defp apply_update(info, {:module_defined, module, vars}) do
|
||||||
put_in(info.modules_defined[module], {version, vars})
|
put_in(info.modules_defined[module], vars)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_update(info, {:alias_used, alias}) do
|
defp apply_update(info, {:alias_used, alias}) do
|
||||||
|
|
|
@ -277,6 +277,32 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
ref = Process.monitor(widget_pid1)
|
ref = Process.monitor(widget_pid1)
|
||||||
assert_receive {:DOWN, ^ref, :process, ^widget_pid1, _reason}
|
assert_receive {:DOWN, ^ref, :process, ^widget_pid1, _reason}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "raises when redefining a module in a different evaluation", %{evaluator: evaluator} do
|
||||||
|
code = """
|
||||||
|
defmodule Livebook.Runtime.EvaluatorTest.Redefinition do
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
|
||||||
|
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||||
|
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
|
||||||
|
|
||||||
|
# Redefining in the same evaluation works
|
||||||
|
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||||
|
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
|
||||||
|
|
||||||
|
Evaluator.evaluate_code(evaluator, code, :code_2, [], file: "file.ex")
|
||||||
|
|
||||||
|
assert_receive {:runtime_evaluation_response, :code_2,
|
||||||
|
{:error, :error, %CompileError{}, []},
|
||||||
|
%{
|
||||||
|
code_error: %{
|
||||||
|
line: 1,
|
||||||
|
description:
|
||||||
|
"module Livebook.Runtime.EvaluatorTest.Redefinition is already defined"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "evaluate_code/6 identifier tracking" do
|
describe "evaluate_code/6 identifier tracking" do
|
||||||
|
@ -584,6 +610,9 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
Process.put(:x, 1)
|
Process.put(:x, 1)
|
||||||
Process.put(:y, 1)
|
Process.put(:y, 1)
|
||||||
Process.put(:z, 1)
|
Process.put(:z, 1)
|
||||||
|
|
||||||
|
defmodule Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingOne do
|
||||||
|
end
|
||||||
"""
|
"""
|
||||||
|> eval(evaluator, 0)
|
|> eval(evaluator, 0)
|
||||||
|
|
||||||
|
@ -598,6 +627,9 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
|
|
||||||
Process.put(:y, 2)
|
Process.put(:y, 2)
|
||||||
Process.delete(:z)
|
Process.delete(:z)
|
||||||
|
|
||||||
|
defmodule Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingTwo do
|
||||||
|
end
|
||||||
"""
|
"""
|
||||||
|> eval(evaluator, 1)
|
|> eval(evaluator, 1)
|
||||||
|
|
||||||
|
@ -616,6 +648,10 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
|
|
||||||
assert context.env.versioned_vars == %{{:x, nil} => 0, {:y, nil} => 1}
|
assert context.env.versioned_vars == %{{:x, nil} => 0, {:y, nil} => 1}
|
||||||
|
|
||||||
|
assert context.env.context_modules == [
|
||||||
|
Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingOne
|
||||||
|
]
|
||||||
|
|
||||||
assert context.pdict == %{x: 1, y: 1, z: 1}
|
assert context.pdict == %{x: 1, y: 1, z: 1}
|
||||||
|
|
||||||
# Evaluation 1 context
|
# Evaluation 1 context
|
||||||
|
@ -635,6 +671,10 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
|
|
||||||
assert context.env.versioned_vars == %{{:y, nil} => 0}
|
assert context.env.versioned_vars == %{{:y, nil} => 0}
|
||||||
|
|
||||||
|
assert context.env.context_modules == [
|
||||||
|
Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingTwo
|
||||||
|
]
|
||||||
|
|
||||||
# Process dictionary is not diffed
|
# Process dictionary is not diffed
|
||||||
assert context.pdict == %{x: 1, y: 2}
|
assert context.pdict == %{x: 1, y: 2}
|
||||||
|
|
||||||
|
@ -654,6 +694,11 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
|
|
||||||
assert context.env.versioned_vars == %{{:x, nil} => 0, {:y, nil} => 1}
|
assert context.env.versioned_vars == %{{:x, nil} => 0, {:y, nil} => 1}
|
||||||
|
|
||||||
|
assert context.env.context_modules == [
|
||||||
|
Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingTwo,
|
||||||
|
Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingOne
|
||||||
|
]
|
||||||
|
|
||||||
assert context.pdict == %{x: 1, y: 2}
|
assert context.pdict == %{x: 1, y: 2}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -686,6 +731,22 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
|
|
||||||
assert_receive {:DOWN, ^ref, :process, ^widget_pid1, _reason}
|
assert_receive {:DOWN, ^ref, :process, ^widget_pid1, _reason}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "deletes modules defined by the given evaluation", %{evaluator: evaluator} do
|
||||||
|
code = """
|
||||||
|
defmodule Livebook.Runtime.EvaluatorTest.ForgetEvaluation.Redefinition do
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
|
||||||
|
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||||
|
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
|
||||||
|
|
||||||
|
Evaluator.forget_evaluation(evaluator, :code_1)
|
||||||
|
|
||||||
|
# Define the module in a different evaluation
|
||||||
|
Evaluator.evaluate_code(evaluator, code, :code_2, [])
|
||||||
|
assert_receive {:runtime_evaluation_response, :code_2, {:ok, _}, metadata()}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "initialize_from/3" do
|
describe "initialize_from/3" do
|
||||||
|
|
Loading…
Add table
Reference in a new issue