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:
Andreas Kling 2026-04-13 12:49:41 +02:00 committed by Andreas Kling
parent 2ca7dfa649
commit 9af5508aef
6 changed files with 176 additions and 53 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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 };

View file

@ -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;
}

View file

@ -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;

View file

@ -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())