LibJS: Split inline frames from execution context stack
Keep JS-to-JS inline calls out of m_execution_context_stack and walk the active stack from the running execution context instead. Base pushes now record the previous running context so duplicate TemporaryExecutionContext pushes and host re-entry still restore correctly. This keeps the fast JS-to-JS path off the vector without losing GC root collection, stack traces, or helpers that need to inspect the active execution context chain.
This commit is contained in:
parent
2ca7dfa649
commit
9af5508aef
6 changed files with 176 additions and 53 deletions
|
|
@ -225,7 +225,6 @@ VM::HandleExceptionResponse VM::handle_exception(u32 program_counter, Value exce
|
|||
auto* caller_frame = callee_frame->caller_frame;
|
||||
auto caller_pc = callee_frame->caller_return_pc;
|
||||
|
||||
vm().pop_execution_context();
|
||||
vm().interpreter_stack().deallocate(callee_frame);
|
||||
|
||||
m_running_execution_context = caller_frame;
|
||||
|
|
@ -293,12 +292,8 @@ ExecutionContext* VM::push_inline_frame(
|
|||
}
|
||||
callee_context->private_environment = callee_function.m_private_environment;
|
||||
|
||||
// Fast-path push onto execution context stack (avoids Vector::append growth check preventing inlining).
|
||||
auto& ec_stack = vm().execution_context_stack();
|
||||
if (ec_stack.size() < ec_stack.capacity()) [[likely]]
|
||||
ec_stack.unchecked_append(callee_context);
|
||||
else
|
||||
ec_stack.append(callee_context);
|
||||
// Inline JS-to-JS frames stay out of the VM execution context stack and
|
||||
// are tracked through caller_frame instead.
|
||||
m_running_execution_context = callee_context;
|
||||
|
||||
// Bind this if the function uses it.
|
||||
|
|
@ -384,7 +379,7 @@ NEVER_INLINE bool VM::try_inline_call_construct(Instruction const& insn, u32 cur
|
|||
// InitializeInstanceElements (can throw).
|
||||
auto init_result = this_argument->initialize_instance_elements(callee_function);
|
||||
if (init_result.is_throw_completion()) [[unlikely]] {
|
||||
vm().pop_execution_context();
|
||||
m_running_execution_context = callee_context->caller_frame;
|
||||
vm().interpreter_stack().deallocate(callee_context);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -403,7 +398,6 @@ NEVER_INLINE void VM::pop_inline_frame(Value return_value)
|
|||
if (callee_frame->caller_is_construct && !return_value.is_object())
|
||||
return_value = callee_frame->this_value.value();
|
||||
|
||||
vm().pop_execution_context();
|
||||
vm().interpreter_stack().deallocate(callee_frame);
|
||||
|
||||
m_running_execution_context = caller_frame;
|
||||
|
|
|
|||
|
|
@ -218,11 +218,10 @@ void AsyncGenerator::execute(VM& vm, Completion completion)
|
|||
auto yield_completion = normal_completion(value);
|
||||
|
||||
// 6. Assert: The execution context stack has at least two elements.
|
||||
VERIFY(vm.execution_context_stack().size() >= 2);
|
||||
auto* previous_context = vm.previous_execution_context();
|
||||
VERIFY(previous_context);
|
||||
|
||||
// 7. Let previousContext be the second to top element of the execution context stack.
|
||||
auto& previous_context = vm.execution_context_stack().at(vm.execution_context_stack().size() - 2);
|
||||
|
||||
// 8. Let previousRealm be previousContext's Realm.
|
||||
auto previous_realm = previous_context->realm;
|
||||
|
||||
|
|
|
|||
|
|
@ -115,8 +115,7 @@ public:
|
|||
}
|
||||
|
||||
// Non-standard: Inline frame linkage for the bytecode interpreter.
|
||||
// When a JS-to-JS call is inlined in the dispatch loop, these fields
|
||||
// allow the Return handler to restore the caller's frame.
|
||||
// Only inline JS-to-JS calls use these fields.
|
||||
ExecutionContext* caller_frame { nullptr };
|
||||
u32 passed_argument_count { 0 };
|
||||
u32 caller_return_pc { 0 };
|
||||
|
|
|
|||
|
|
@ -304,17 +304,18 @@ void VM::gather_roots(HashMap<GC::Cell*, GC::HeapRoot>& roots)
|
|||
for (auto finalization_registry : m_finalization_registry_cleanup_jobs)
|
||||
roots.set(finalization_registry, GC::HeapRoot { .type = GC::HeapRoot::Type::VM });
|
||||
|
||||
auto gather_roots_from_execution_context_stack = [&roots](Vector<ExecutionContext*> const& stack) {
|
||||
for (auto const& execution_context : stack) {
|
||||
auto gather_roots_from_execution_context_stack = [&roots](Vector<ExecutionContext*> const& stack, Vector<ExecutionContext*> const& previous_running_contexts, ExecutionContext* running_execution_context) {
|
||||
for_each_execution_context_top_to_bottom(stack, previous_running_contexts, running_execution_context, [&](ExecutionContext& execution_context) {
|
||||
ExecutionContextRootsCollector visitor;
|
||||
execution_context->visit_edges(visitor);
|
||||
execution_context.visit_edges(visitor);
|
||||
for (auto cell : visitor.roots)
|
||||
roots.set(cell, GC::HeapRoot { .type = GC::HeapRoot::Type::VM });
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
gather_roots_from_execution_context_stack(m_execution_context_stack);
|
||||
for (auto& saved_stack : m_saved_execution_context_stacks)
|
||||
gather_roots_from_execution_context_stack(saved_stack);
|
||||
gather_roots_from_execution_context_stack(m_execution_context_stack, m_execution_context_stack_previous_running_contexts, m_running_execution_context);
|
||||
for (auto const& saved_stack : m_saved_execution_context_stacks)
|
||||
gather_roots_from_execution_context_stack(saved_stack.stack, saved_stack.previous_running_contexts, saved_stack.running_execution_context);
|
||||
|
||||
for (auto& job : m_promise_jobs)
|
||||
roots.set(job, GC::HeapRoot { .type = GC::HeapRoot::Type::VM });
|
||||
|
|
@ -515,53 +516,75 @@ void VM::promise_rejection_tracker(Promise& promise, Promise::RejectionOperation
|
|||
|
||||
void VM::dump_backtrace() const
|
||||
{
|
||||
for (ssize_t i = m_execution_context_stack.size() - 1; i >= 0; --i) {
|
||||
auto& frame = m_execution_context_stack[i];
|
||||
|
||||
if (frame->executable) {
|
||||
auto source_range = frame->executable->source_range_at(frame->program_counter).realize();
|
||||
dbgln("-> {} @ {}:{},{}", frame->function ? frame->function->name_for_call_stack() : ""_utf16, source_range.filename(), source_range.start.line, source_range.start.column);
|
||||
for_each_execution_context_top_to_bottom([&](ExecutionContext const& frame) {
|
||||
if (frame.executable) {
|
||||
auto source_range = frame.executable->source_range_at(frame.program_counter).realize();
|
||||
dbgln("-> {} @ {}:{},{}", frame.function ? frame.function->name_for_call_stack() : ""_utf16, source_range.filename(), source_range.start.line, source_range.start.column);
|
||||
} else {
|
||||
dbgln("-> {}", frame->function ? frame->function->name_for_call_stack() : ""_utf16);
|
||||
dbgln("-> {}", frame.function ? frame.function->name_for_call_stack() : ""_utf16);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void VM::save_execution_context_stack()
|
||||
{
|
||||
m_saved_execution_context_stacks.append(move(m_execution_context_stack));
|
||||
m_saved_execution_context_stacks.append({
|
||||
.stack = move(m_execution_context_stack),
|
||||
.previous_running_contexts = move(m_execution_context_stack_previous_running_contexts),
|
||||
.running_execution_context = m_running_execution_context,
|
||||
});
|
||||
m_running_execution_context = nullptr;
|
||||
}
|
||||
|
||||
void VM::clear_execution_context_stack()
|
||||
{
|
||||
m_execution_context_stack.clear_with_capacity();
|
||||
m_execution_context_stack_previous_running_contexts.clear_with_capacity();
|
||||
m_running_execution_context = nullptr;
|
||||
}
|
||||
|
||||
void VM::restore_execution_context_stack()
|
||||
{
|
||||
m_execution_context_stack = m_saved_execution_context_stacks.take_last();
|
||||
m_running_execution_context = m_execution_context_stack.is_empty() ? nullptr : m_execution_context_stack.last();
|
||||
auto saved_stack = m_saved_execution_context_stacks.take_last();
|
||||
m_execution_context_stack = move(saved_stack.stack);
|
||||
m_execution_context_stack_previous_running_contexts = move(saved_stack.previous_running_contexts);
|
||||
m_running_execution_context = saved_stack.running_execution_context;
|
||||
}
|
||||
|
||||
ExecutionContext* VM::previous_execution_context() const
|
||||
{
|
||||
ExecutionContext* previous_execution_context = nullptr;
|
||||
bool found_running_execution_context = false;
|
||||
for_each_execution_context_top_to_bottom([&](ExecutionContext const& execution_context) {
|
||||
if (!found_running_execution_context) {
|
||||
found_running_execution_context = true;
|
||||
return true;
|
||||
}
|
||||
previous_execution_context = const_cast<ExecutionContext*>(&execution_context);
|
||||
return false;
|
||||
});
|
||||
return previous_execution_context;
|
||||
}
|
||||
|
||||
// 9.4.1 GetActiveScriptOrModule ( ), https://tc39.es/ecma262/#sec-getactivescriptormodule
|
||||
ScriptOrModule VM::get_active_script_or_module() const
|
||||
{
|
||||
// 1. If the execution context stack is empty, return null.
|
||||
if (m_execution_context_stack.is_empty())
|
||||
if (!m_running_execution_context)
|
||||
return Empty {};
|
||||
|
||||
// 2. Let ec be the topmost execution context on the execution context stack whose ScriptOrModule component is not null.
|
||||
for (auto i = m_execution_context_stack.size() - 1; i > 0; i--) {
|
||||
if (!m_execution_context_stack[i]->script_or_module.has<Empty>())
|
||||
return m_execution_context_stack[i]->script_or_module;
|
||||
}
|
||||
ScriptOrModule script_or_module = Empty {};
|
||||
for_each_execution_context_top_to_bottom([&](ExecutionContext const& execution_context) {
|
||||
if (execution_context.script_or_module.has<Empty>())
|
||||
return true;
|
||||
script_or_module = execution_context.script_or_module;
|
||||
return false;
|
||||
});
|
||||
|
||||
// 3. If no such execution context exists, return null. Otherwise, return ec's ScriptOrModule.
|
||||
// Note: Since it is not empty we have 0 and since we got here all the
|
||||
// above contexts don't have a non-null ScriptOrModule
|
||||
return m_execution_context_stack[0]->script_or_module;
|
||||
return script_or_module;
|
||||
}
|
||||
|
||||
VM::StoredModule* VM::get_stored_module(ImportedModuleReferrer const&, ByteString const& filename, Utf16String const&)
|
||||
|
|
@ -793,16 +816,16 @@ void VM::load_imported_module(ImportedModuleReferrer referrer, ModuleRequest con
|
|||
Vector<StackTraceElement> VM::stack_trace() const
|
||||
{
|
||||
Vector<StackTraceElement> stack_trace;
|
||||
stack_trace.ensure_capacity(m_execution_context_stack.size());
|
||||
for (auto* context : m_execution_context_stack.in_reverse()) {
|
||||
for_each_execution_context_top_to_bottom([&](ExecutionContext const& context) {
|
||||
Optional<SourceRange> source_range;
|
||||
if (context->executable)
|
||||
source_range = context->executable->get_source_range(context->program_counter);
|
||||
if (context.executable)
|
||||
source_range = context.executable->get_source_range(context.program_counter);
|
||||
stack_trace.append({
|
||||
.execution_context = context,
|
||||
.execution_context = const_cast<ExecutionContext*>(&context),
|
||||
.source_range = move(source_range),
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return stack_trace;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,21 +207,37 @@ public:
|
|||
if (did_reach_stack_space_limit()) [[unlikely]] {
|
||||
return throw_completion<InternalError>(ErrorType::CallStackSizeExceeded);
|
||||
}
|
||||
context.caller_frame = nullptr;
|
||||
context.caller_return_pc = 0;
|
||||
context.caller_dst_raw = 0;
|
||||
context.caller_is_construct = false;
|
||||
m_execution_context_stack.append(&context);
|
||||
m_execution_context_stack_previous_running_contexts.append(m_running_execution_context);
|
||||
m_running_execution_context = &context;
|
||||
return {};
|
||||
}
|
||||
|
||||
void push_execution_context(ExecutionContext& context)
|
||||
{
|
||||
context.caller_frame = nullptr;
|
||||
context.caller_return_pc = 0;
|
||||
context.caller_dst_raw = 0;
|
||||
context.caller_is_construct = false;
|
||||
m_execution_context_stack.append(&context);
|
||||
m_execution_context_stack_previous_running_contexts.append(m_running_execution_context);
|
||||
m_running_execution_context = &context;
|
||||
}
|
||||
|
||||
void pop_execution_context()
|
||||
ExecutionContext* pop_execution_context()
|
||||
{
|
||||
m_execution_context_stack.take_last();
|
||||
m_running_execution_context = m_execution_context_stack.is_empty() ? nullptr : m_execution_context_stack.last();
|
||||
VERIFY(!m_execution_context_stack.is_empty());
|
||||
auto* context = m_execution_context_stack.take_last();
|
||||
context->caller_frame = nullptr;
|
||||
context->caller_return_pc = 0;
|
||||
context->caller_dst_raw = 0;
|
||||
context->caller_is_construct = false;
|
||||
m_running_execution_context = m_execution_context_stack_previous_running_contexts.take_last();
|
||||
return context;
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/#running-execution-context
|
||||
|
|
@ -238,10 +254,53 @@ public:
|
|||
return *m_running_execution_context;
|
||||
}
|
||||
|
||||
bool has_running_execution_context() const { return m_running_execution_context != nullptr; }
|
||||
|
||||
// https://tc39.es/ecma262/#execution-context-stack
|
||||
// The execution context stack is used to track execution contexts.
|
||||
// The execution context stack tracks base execution contexts. Inline JS-to-JS
|
||||
// frames are threaded through ExecutionContext::caller_frame starting at the
|
||||
// running execution context.
|
||||
Vector<ExecutionContext*> const& execution_context_stack() const { return m_execution_context_stack; }
|
||||
Vector<ExecutionContext*>& execution_context_stack() { return m_execution_context_stack; }
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_execution_context_top_to_bottom(Callback callback)
|
||||
{
|
||||
for_each_execution_context_top_to_bottom(m_execution_context_stack, m_execution_context_stack_previous_running_contexts, m_running_execution_context, callback);
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_execution_context_top_to_bottom(Callback callback) const
|
||||
{
|
||||
for_each_execution_context_top_to_bottom(m_execution_context_stack, m_execution_context_stack_previous_running_contexts, m_running_execution_context, callback);
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
Optional<ExecutionContext*> last_execution_context_matching(Callback callback)
|
||||
{
|
||||
Optional<ExecutionContext*> matching_execution_context;
|
||||
for_each_execution_context_top_to_bottom([&](ExecutionContext& execution_context) {
|
||||
if (!callback(&execution_context))
|
||||
return true;
|
||||
matching_execution_context = &execution_context;
|
||||
return false;
|
||||
});
|
||||
return matching_execution_context;
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
Optional<ExecutionContext const*> last_execution_context_matching(Callback callback) const
|
||||
{
|
||||
Optional<ExecutionContext const*> matching_execution_context;
|
||||
for_each_execution_context_top_to_bottom([&](ExecutionContext const& execution_context) {
|
||||
if (!callback(&execution_context))
|
||||
return true;
|
||||
matching_execution_context = &execution_context;
|
||||
return false;
|
||||
});
|
||||
return matching_execution_context;
|
||||
}
|
||||
|
||||
ExecutionContext* previous_execution_context() const;
|
||||
|
||||
Environment const* lexical_environment() const { return running_execution_context().lexical_environment; }
|
||||
Environment* lexical_environment() { return running_execution_context().lexical_environment; }
|
||||
|
|
@ -396,6 +455,50 @@ private:
|
|||
|
||||
explicit VM(ErrorMessages);
|
||||
|
||||
template<typename Callback>
|
||||
static void for_each_execution_context_top_to_bottom(Vector<ExecutionContext*> const& execution_context_stack, Vector<ExecutionContext*> const& execution_context_stack_previous_running_contexts, ExecutionContext* running_execution_context, Callback callback)
|
||||
{
|
||||
VERIFY(execution_context_stack.size() == execution_context_stack_previous_running_contexts.size());
|
||||
|
||||
if (!running_execution_context) {
|
||||
for (size_t i = execution_context_stack.size(); i-- > 0;) {
|
||||
if (!callback(*execution_context_stack[i]))
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (execution_context_stack.is_empty()) {
|
||||
for (auto* execution_context = running_execution_context; execution_context; execution_context = execution_context->caller_frame) {
|
||||
if (!callback(*execution_context))
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto stack_index = execution_context_stack.size();
|
||||
auto* execution_context = running_execution_context;
|
||||
while (execution_context) {
|
||||
if (!callback(*execution_context))
|
||||
return;
|
||||
if (stack_index > 0 && execution_context == execution_context_stack[stack_index - 1]) {
|
||||
execution_context = execution_context_stack_previous_running_contexts[stack_index - 1];
|
||||
--stack_index;
|
||||
continue;
|
||||
}
|
||||
|
||||
execution_context = execution_context->caller_frame;
|
||||
}
|
||||
|
||||
VERIFY(stack_index == 0);
|
||||
}
|
||||
|
||||
struct SavedExecutionContextStack {
|
||||
Vector<ExecutionContext*> stack;
|
||||
Vector<ExecutionContext*> previous_running_contexts;
|
||||
ExecutionContext* running_execution_context { nullptr };
|
||||
};
|
||||
|
||||
void load_imported_module(ImportedModuleReferrer, ModuleRequest const&, GC::Ptr<GraphLoadingState::HostDefined>, ImportedModulePayload);
|
||||
ThrowCompletionOr<void> link_and_eval_module(CyclicModule&);
|
||||
ThrowCompletionOr<void> link_and_eval_module(SourceTextModule&);
|
||||
|
|
@ -419,9 +522,14 @@ private:
|
|||
GC::Heap m_heap;
|
||||
|
||||
Vector<ExecutionContext*> m_execution_context_stack;
|
||||
// Base pushes may happen while an inline JS-to-JS frame is running, and
|
||||
// TemporaryExecutionContext can push the same context multiple times. Keep
|
||||
// the previous running context for each base push so we can restore and
|
||||
// walk the full active stack without relying on caller_frame there.
|
||||
Vector<ExecutionContext*> m_execution_context_stack_previous_running_contexts;
|
||||
ExecutionContext* m_running_execution_context { nullptr };
|
||||
|
||||
Vector<Vector<ExecutionContext*>> m_saved_execution_context_stacks;
|
||||
Vector<SavedExecutionContextStack> m_saved_execution_context_stacks;
|
||||
|
||||
StackInfo m_stack_info;
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ TESTJS_GLOBAL_FUNCTION(mark_as_garbage, markAsGarbage)
|
|||
auto& variable_name = argument.as_string();
|
||||
|
||||
// In native functions we don't have a lexical environment so get the outer via the execution stack.
|
||||
auto outer_environment = vm.execution_context_stack().last_matching([&](auto& execution_context) {
|
||||
auto outer_environment = vm.last_execution_context_matching([&](auto* execution_context) {
|
||||
return execution_context->lexical_environment != nullptr;
|
||||
});
|
||||
if (!outer_environment.has_value())
|
||||
|
|
|
|||
Loading…
Reference in a new issue