Trigger garbage collection before evaluation (#1593)

This commit is contained in:
Jonatan Kłosko 2022-12-17 11:17:37 +01:00 committed by GitHub
parent 978de7990e
commit 59cbba63b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 125 additions and 99 deletions

View file

@ -337,105 +337,11 @@ defmodule Livebook.Runtime.Evaluator do
end
defp handle_cast({:evaluate_code, code, ref, parent_refs, opts}, state) do
Evaluator.ObjectTracker.remove_reference_sync(state.object_tracker, {self(), ref})
context = get_context(state, parent_refs)
file = Keyword.get(opts, :file, "nofile")
context = put_in(context.env.file, file)
Evaluator.IOProxy.configure(state.io_proxy, ref, file)
set_pdict(context, state.ignored_pdict_keys)
if old_context = state.contexts[ref] do
for module <- old_context.env.context_modules do
delete_module!(module)
end
end
start_time = System.monotonic_time()
eval_result = eval(code, context.binding, context.env)
evaluation_time_ms = time_diff_ms(start_time)
tracer_info = Evaluator.IOProxy.get_tracer_info(state.io_proxy)
{new_context, result, code_error, identifiers_used, identifiers_defined} =
case eval_result do
{:ok, value, binding, env} ->
context_id = random_id()
new_context = %{
id: context_id,
binding: binding,
env: prune_env(env, tracer_info),
pdict: current_pdict(state)
}
{identifiers_used, identifiers_defined} =
identifier_dependencies(new_context, tracer_info, context)
result = {:ok, value}
{new_context, result, nil, identifiers_used, identifiers_defined}
{:error, kind, error, stacktrace, code_error} ->
for {module, _} <- tracer_info.modules_defined do
delete_module!(module)
end
result = {:error, kind, error, stacktrace}
identifiers_used = :unknown
identifiers_defined = %{}
# Empty context
new_context = initial_context()
{new_context, result, code_error, identifiers_used, identifiers_defined}
end
if ebin_path() do
Livebook.Runtime.Evaluator.Doctests.run(new_context.env.context_modules)
end
state = put_context(state, ref, new_context)
Evaluator.IOProxy.flush(state.io_proxy)
Evaluator.IOProxy.clear_input_cache(state.io_proxy)
output = state.formatter.format_result(result)
metadata = %{
evaluation_time_ms: evaluation_time_ms,
memory_usage: memory(),
code_error: code_error,
identifiers_used: identifiers_used,
identifiers_defined: identifiers_defined
}
send(state.send_to, {:runtime_evaluation_response, ref, output, metadata})
if on_finish = opts[:on_finish] do
on_finish.(result)
end
:erlang.garbage_collect(self())
{:noreply, state}
do_evaluate_code(code, ref, parent_refs, opts, state)
end
defp handle_cast({:forget_evaluation, ref}, state) do
{context, state} = pop_context(state, ref)
if context do
for module <- context.env.context_modules do
delete_module!(module)
# And we immediately purge the newly deleted code
:code.purge(module)
end
Evaluator.ObjectTracker.remove_reference_sync(state.object_tracker, {self(), ref})
:erlang.garbage_collect(self())
end
{:noreply, state}
do_forget_evaluation(ref, state)
end
defp handle_cast({:peek_context, parent_refs, fun}, state) do
@ -483,6 +389,126 @@ defmodule Livebook.Runtime.Evaluator do
{:reply, result, state}
end
defp do_evaluate_code(code, ref, parent_refs, opts, state) do
{old_context, state} = pop_in(state.contexts[ref])
if old_context do
for module <- old_context.env.context_modules do
delete_module(module)
end
end
# We remove the old context from state and jump to a tail-recursive
# function. This way we are sure there is no reference to the old
# state and we can garbage collect the old context before the evaluation
continue_do_evaluate_code(code, ref, parent_refs, opts, state)
end
defp continue_do_evaluate_code(code, ref, parent_refs, opts, state) do
:erlang.garbage_collect(self())
Evaluator.ObjectTracker.remove_reference_sync(state.object_tracker, {self(), ref})
context = get_context(state, parent_refs)
file = Keyword.get(opts, :file, "nofile")
context = put_in(context.env.file, file)
Evaluator.IOProxy.configure(state.io_proxy, ref, file)
set_pdict(context, state.ignored_pdict_keys)
start_time = System.monotonic_time()
eval_result = eval(code, context.binding, context.env)
evaluation_time_ms = time_diff_ms(start_time)
tracer_info = Evaluator.IOProxy.get_tracer_info(state.io_proxy)
{new_context, result, code_error, identifiers_used, identifiers_defined} =
case eval_result do
{:ok, value, binding, env} ->
context_id = random_id()
new_context = %{
id: context_id,
binding: binding,
env: prune_env(env, tracer_info),
pdict: current_pdict(state)
}
{identifiers_used, identifiers_defined} =
identifier_dependencies(new_context, tracer_info, context)
result = {:ok, value}
{new_context, result, nil, identifiers_used, identifiers_defined}
{:error, kind, error, stacktrace, code_error} ->
for {module, _} <- tracer_info.modules_defined do
delete_module(module)
end
result = {:error, kind, error, stacktrace}
identifiers_used = :unknown
identifiers_defined = %{}
# Empty context
new_context = initial_context()
{new_context, result, code_error, identifiers_used, identifiers_defined}
end
if ebin_path() do
Livebook.Runtime.Evaluator.Doctests.run(new_context.env.context_modules)
end
state = put_context(state, ref, new_context)
Evaluator.IOProxy.flush(state.io_proxy)
Evaluator.IOProxy.clear_input_cache(state.io_proxy)
output = state.formatter.format_result(result)
metadata = %{
evaluation_time_ms: evaluation_time_ms,
memory_usage: memory(),
code_error: code_error,
identifiers_used: identifiers_used,
identifiers_defined: identifiers_defined
}
send(state.send_to, {:runtime_evaluation_response, ref, output, metadata})
if on_finish = opts[:on_finish] do
on_finish.(result)
end
:erlang.garbage_collect(self())
{:noreply, state}
end
defp do_forget_evaluation(ref, state) do
{context, state} = pop_context(state, ref)
if context do
for module <- context.env.context_modules do
delete_module(module)
# And we immediately purge the newly deleted code
:code.purge(module)
end
Evaluator.ObjectTracker.remove_reference_sync(state.object_tracker, {self(), ref})
end
continue_do_forget_evaluation(context != nil, state)
end
defp continue_do_forget_evaluation(context?, state) do
if context? do
:erlang.garbage_collect(self())
end
{:noreply, state}
end
defp put_context(state, ref, context) do
update_evaluator_info(fn info ->
put_in(info.contexts[ref], {context.id, context.env})
@ -781,7 +807,7 @@ defmodule Livebook.Runtime.Evaluator do
end
@doc false
def delete_module!(module, ebin_path \\ ebin_path()) do
def delete_module(module, ebin_path \\ ebin_path()) do
# If there is a deleted code for the module, we purge it first
:code.purge(module)
@ -790,7 +816,7 @@ defmodule Livebook.Runtime.Evaluator do
if ebin_path do
ebin_path
|> Path.join("#{module}.beam")
|> File.rm!()
|> File.rm()
end
end

View file

@ -161,7 +161,7 @@ defmodule Livebook.Runtime.Evaluator.IOProxy do
defp cleanup(state) do
# Remove all modules defined during evaluation
for module <- state.modules_defined, function_exported?(module, :module_info, 1) do
Evaluator.delete_module!(module, state.ebin_path)
Evaluator.delete_module(module, state.ebin_path)
end
end