mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 03:08:11 +00:00
LibJS: Lazily collect stack trace information
The previous implementation was calling `backtrace()` for every function call, which is quite slow. Instead, this implementation provides VM::stack_trace() which unwinds the native stack, maps it through NativeExecutable::get_source_range and combines it with source ranges from interpreted call frames.
This commit is contained in:
parent
77dc7c4d36
commit
68f4d21de2
11 changed files with 96 additions and 46 deletions
|
@ -6,6 +6,7 @@ shared_library("LibJS") {
|
||||||
# FIXME: Why does LibSyntax need to depend on WindowServer headers?
|
# FIXME: Why does LibSyntax need to depend on WindowServer headers?
|
||||||
"//Userland",
|
"//Userland",
|
||||||
]
|
]
|
||||||
|
cflags_cc = [ "-fno-omit-frame-pointer" ]
|
||||||
deps = [
|
deps = [
|
||||||
"//AK",
|
"//AK",
|
||||||
"//Userland/Libraries/LibCore",
|
"//Userland/Libraries/LibCore",
|
||||||
|
|
|
@ -56,13 +56,6 @@ void Interpreter::visit_edges(Cell::Visitor& visitor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<InstructionStreamIterator const&> Interpreter::instruction_stream_iterator() const
|
|
||||||
{
|
|
||||||
if (m_current_executable && m_current_executable->native_executable())
|
|
||||||
return m_current_executable->native_executable()->instruction_stream_iterator(*m_current_executable);
|
|
||||||
return m_pc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 16.1.6 ScriptEvaluation ( scriptRecord ), https://tc39.es/ecma262/#sec-runtime-semantics-scriptevaluation
|
// 16.1.6 ScriptEvaluation ( scriptRecord ), https://tc39.es/ecma262/#sec-runtime-semantics-scriptevaluation
|
||||||
ThrowCompletionOr<Value> Interpreter::run(Script& script_record, JS::GCPtr<Environment> lexical_environment_override)
|
ThrowCompletionOr<Value> Interpreter::run(Script& script_record, JS::GCPtr<Environment> lexical_environment_override)
|
||||||
{
|
{
|
||||||
|
@ -371,6 +364,8 @@ Interpreter::ValueAndFrame Interpreter::run_and_return_frame(Executable& executa
|
||||||
else
|
else
|
||||||
push_call_frame(make<CallFrame>(), executable.number_of_registers);
|
push_call_frame(make<CallFrame>(), executable.number_of_registers);
|
||||||
|
|
||||||
|
vm().execution_context_stack().last()->executable = &executable;
|
||||||
|
|
||||||
if (auto native_executable = executable.get_or_create_native_executable()) {
|
if (auto native_executable = executable.get_or_create_native_executable()) {
|
||||||
native_executable->run(vm());
|
native_executable->run(vm());
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ public:
|
||||||
Executable& current_executable() { return *m_current_executable; }
|
Executable& current_executable() { return *m_current_executable; }
|
||||||
Executable const& current_executable() const { return *m_current_executable; }
|
Executable const& current_executable() const { return *m_current_executable; }
|
||||||
BasicBlock const& current_block() const { return *m_current_block; }
|
BasicBlock const& current_block() const { return *m_current_block; }
|
||||||
Optional<InstructionStreamIterator const&> instruction_stream_iterator() const;
|
Optional<InstructionStreamIterator const&> instruction_stream_iterator() const { return m_pc; }
|
||||||
|
|
||||||
void visit_edges(Cell::Visitor&);
|
void visit_edges(Cell::Visitor&);
|
||||||
|
|
||||||
|
|
|
@ -271,3 +271,4 @@ target_link_libraries(LibJS PRIVATE LibCore LibCrypto LibFileSystem LibRegex Lib
|
||||||
if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64")
|
if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64")
|
||||||
target_link_libraries(LibJS PRIVATE LibX86)
|
target_link_libraries(LibJS PRIVATE LibX86)
|
||||||
endif()
|
endif()
|
||||||
|
target_compile_options(LibJS PRIVATE -fno-omit-frame-pointer)
|
||||||
|
|
|
@ -12,15 +12,6 @@
|
||||||
#include <LibX86/Disassembler.h>
|
#include <LibX86/Disassembler.h>
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
|
|
||||||
#if __has_include(<execinfo.h>)
|
|
||||||
# include <execinfo.h>
|
|
||||||
# define EXECINFO_BACKTRACE
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(AK_OS_ANDROID) && (__ANDROID_API__ < 33)
|
|
||||||
# undef EXECINFO_BACKTRACE
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace JS::JIT {
|
namespace JS::JIT {
|
||||||
|
|
||||||
NativeExecutable::NativeExecutable(void* code, size_t size, Vector<BytecodeMapping> mapping)
|
NativeExecutable::NativeExecutable(void* code, size_t size, Vector<BytecodeMapping> mapping)
|
||||||
|
@ -159,32 +150,20 @@ BytecodeMapping const& NativeExecutable::find_mapping_entry(size_t native_offset
|
||||||
return m_mapping[nearby_index];
|
return m_mapping[nearby_index];
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Bytecode::InstructionStreamIterator const&> NativeExecutable::instruction_stream_iterator([[maybe_unused]] Bytecode::Executable const& executable) const
|
Optional<UnrealizedSourceRange> NativeExecutable::get_source_range(Bytecode::Executable const& executable, FlatPtr address) const
|
||||||
{
|
{
|
||||||
#ifdef EXECINFO_BACKTRACE
|
|
||||||
void* buffer[10];
|
|
||||||
auto count = backtrace(buffer, 10);
|
|
||||||
auto start = bit_cast<FlatPtr>(m_code);
|
auto start = bit_cast<FlatPtr>(m_code);
|
||||||
auto end = start + m_size;
|
auto end = start + m_size;
|
||||||
for (auto i = 0; i < count; i++) {
|
if (address < start || address >= end)
|
||||||
auto address = bit_cast<FlatPtr>(buffer[i]);
|
return {};
|
||||||
if (address < start || address >= end)
|
auto const& entry = find_mapping_entry(address - start - 1);
|
||||||
continue;
|
if (entry.block_index < executable.basic_blocks.size()) {
|
||||||
// return address points after the call
|
auto const& block = *executable.basic_blocks[entry.block_index];
|
||||||
// let's subtract 1 to make sure we don't hit the next bytecode
|
if (entry.bytecode_offset < block.size()) {
|
||||||
// (in practice that's not necessary, because our native_call() sequence continues)
|
auto iterator = Bytecode::InstructionStreamIterator { block.instruction_stream(), &executable, entry.bytecode_offset };
|
||||||
auto offset = address - start - 1;
|
return iterator.source_range();
|
||||||
auto& entry = find_mapping_entry(offset);
|
|
||||||
if (entry.block_index < executable.basic_blocks.size()) {
|
|
||||||
auto const& block = *executable.basic_blocks[entry.block_index];
|
|
||||||
if (entry.bytecode_offset < block.size()) {
|
|
||||||
// This is rather clunky, but Interpreter::instruction_stream_iterator() gives out references, so we need to keep it alive.
|
|
||||||
m_instruction_stream_iterator = make<Bytecode::InstructionStreamIterator>(block.instruction_stream(), &executable, entry.bytecode_offset);
|
|
||||||
return *m_instruction_stream_iterator;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ public:
|
||||||
void run(VM&) const;
|
void run(VM&) const;
|
||||||
void dump_disassembly(Bytecode::Executable const& executable) const;
|
void dump_disassembly(Bytecode::Executable const& executable) const;
|
||||||
BytecodeMapping const& find_mapping_entry(size_t native_offset) const;
|
BytecodeMapping const& find_mapping_entry(size_t native_offset) const;
|
||||||
Optional<Bytecode::InstructionStreamIterator const&> instruction_stream_iterator(Bytecode::Executable const& executable) const;
|
Optional<UnrealizedSourceRange> get_source_range(Bytecode::Executable const& executable, FlatPtr address) const;
|
||||||
|
|
||||||
ReadonlyBytes code_bytes() const { return { m_code, m_size }; }
|
ReadonlyBytes code_bytes() const { return { m_code, m_size }; }
|
||||||
|
|
||||||
|
|
|
@ -75,13 +75,13 @@ ThrowCompletionOr<void> Error::install_error_cause(Value options)
|
||||||
|
|
||||||
void Error::populate_stack()
|
void Error::populate_stack()
|
||||||
{
|
{
|
||||||
auto& vm = this->vm();
|
auto stack_trace = vm().stack_trace();
|
||||||
m_traceback.ensure_capacity(vm.execution_context_stack().size());
|
m_traceback.ensure_capacity(stack_trace.size());
|
||||||
for (ssize_t i = vm.execution_context_stack().size() - 1; i >= 0; i--) {
|
for (auto& element : stack_trace) {
|
||||||
auto context = vm.execution_context_stack()[i];
|
auto* context = element.execution_context;
|
||||||
UnrealizedSourceRange range = {};
|
UnrealizedSourceRange range = {};
|
||||||
if (context->instruction_stream_iterator.has_value())
|
if (element.source_range.has_value())
|
||||||
range = context->instruction_stream_iterator->source_range();
|
range = element.source_range.value();
|
||||||
TracebackFrame frame {
|
TracebackFrame frame {
|
||||||
.function_name = context->function_name,
|
.function_name = context->function_name,
|
||||||
.source_range_storage = range,
|
.source_range_storage = range,
|
||||||
|
|
|
@ -51,9 +51,16 @@ public:
|
||||||
MarkedVector<Value> local_variables;
|
MarkedVector<Value> local_variables;
|
||||||
bool is_strict_mode { false };
|
bool is_strict_mode { false };
|
||||||
|
|
||||||
|
RefPtr<Bytecode::Executable> executable;
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/webappapis.html#skip-when-determining-incumbent-counter
|
// https://html.spec.whatwg.org/multipage/webappapis.html#skip-when-determining-incumbent-counter
|
||||||
// FIXME: Move this out of LibJS (e.g. by using the CustomData concept), as it's used exclusively by LibWeb.
|
// FIXME: Move this out of LibJS (e.g. by using the CustomData concept), as it's used exclusively by LibWeb.
|
||||||
size_t skip_when_determining_incumbent_counter { 0 };
|
size_t skip_when_determining_incumbent_counter { 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct StackTraceElement {
|
||||||
|
ExecutionContext* execution_context;
|
||||||
|
Optional<UnrealizedSourceRange> source_range;
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
#include <LibFileSystem/FileSystem.h>
|
#include <LibFileSystem/FileSystem.h>
|
||||||
#include <LibJS/AST.h>
|
#include <LibJS/AST.h>
|
||||||
#include <LibJS/Bytecode/Interpreter.h>
|
#include <LibJS/Bytecode/Interpreter.h>
|
||||||
|
#include <LibJS/JIT/NativeExecutable.h>
|
||||||
#include <LibJS/Runtime/AbstractOperations.h>
|
#include <LibJS/Runtime/AbstractOperations.h>
|
||||||
#include <LibJS/Runtime/Array.h>
|
#include <LibJS/Runtime/Array.h>
|
||||||
#include <LibJS/Runtime/BoundFunction.h>
|
#include <LibJS/Runtime/BoundFunction.h>
|
||||||
|
@ -1135,4 +1136,66 @@ void VM::pop_execution_context()
|
||||||
on_call_stack_emptied();
|
on_call_stack_emptied();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if ARCH(X86_64)
|
||||||
|
struct [[gnu::packed]] NativeStackFrame {
|
||||||
|
NativeStackFrame* prev;
|
||||||
|
FlatPtr return_address;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Vector<FlatPtr> VM::get_native_stack_trace() const
|
||||||
|
{
|
||||||
|
Vector<FlatPtr> buffer;
|
||||||
|
#if ARCH(X86_64)
|
||||||
|
// Manually walk the stack, because backtrace() does not traverse through JIT frames.
|
||||||
|
auto* frame = bit_cast<NativeStackFrame*>(__builtin_frame_address(0));
|
||||||
|
while (bit_cast<FlatPtr>(frame) < m_stack_info.top() && bit_cast<FlatPtr>(frame) >= m_stack_info.base()) {
|
||||||
|
buffer.append(frame->return_address);
|
||||||
|
frame = frame->prev;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Optional<UnrealizedSourceRange> get_source_range(ExecutionContext const* context, Vector<FlatPtr> const& native_stack)
|
||||||
|
{
|
||||||
|
// native function
|
||||||
|
if (!context->executable)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
auto const* native_executable = context->executable->native_executable();
|
||||||
|
if (!native_executable) {
|
||||||
|
// Interpreter frame
|
||||||
|
if (context->instruction_stream_iterator.has_value())
|
||||||
|
return context->instruction_stream_iterator->source_range();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// JIT frame
|
||||||
|
for (auto address : native_stack) {
|
||||||
|
auto range = native_executable->get_source_range(*context->executable, address);
|
||||||
|
if (range.has_value()) {
|
||||||
|
auto realized = range->realize();
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<StackTraceElement> VM::stack_trace() const
|
||||||
|
{
|
||||||
|
auto native_stack = get_native_stack_trace();
|
||||||
|
Vector<StackTraceElement> stack_trace;
|
||||||
|
for (ssize_t i = m_execution_context_stack.size() - 1; i >= 0; i--) {
|
||||||
|
auto* context = m_execution_context_stack[i];
|
||||||
|
stack_trace.append({
|
||||||
|
.execution_context = context,
|
||||||
|
.source_range = get_source_range(context, native_stack).value_or({}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack_trace;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,6 +254,8 @@ public:
|
||||||
// NOTE: This is meant as a temporary stopgap until everything is bytecode.
|
// NOTE: This is meant as a temporary stopgap until everything is bytecode.
|
||||||
ThrowCompletionOr<Value> execute_ast_node(ASTNode const&);
|
ThrowCompletionOr<Value> execute_ast_node(ASTNode const&);
|
||||||
|
|
||||||
|
Vector<StackTraceElement> stack_trace() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using ErrorMessages = AK::Array<String, to_underlying(ErrorMessage::__Count)>;
|
using ErrorMessages = AK::Array<String, to_underlying(ErrorMessage::__Count)>;
|
||||||
|
|
||||||
|
@ -277,6 +279,8 @@ private:
|
||||||
|
|
||||||
void set_well_known_symbols(WellKnownSymbols well_known_symbols) { m_well_known_symbols = move(well_known_symbols); }
|
void set_well_known_symbols(WellKnownSymbols well_known_symbols) { m_well_known_symbols = move(well_known_symbols); }
|
||||||
|
|
||||||
|
Vector<FlatPtr> get_native_stack_trace() const;
|
||||||
|
|
||||||
HashMap<String, GCPtr<PrimitiveString>> m_string_cache;
|
HashMap<String, GCPtr<PrimitiveString>> m_string_cache;
|
||||||
HashMap<DeprecatedString, GCPtr<PrimitiveString>> m_deprecated_string_cache;
|
HashMap<DeprecatedString, GCPtr<PrimitiveString>> m_deprecated_string_cache;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ const stackSetter = stackDescriptor.set;
|
||||||
describe("getter - normal behavior", () => {
|
describe("getter - normal behavior", () => {
|
||||||
test("basic functionality", () => {
|
test("basic functionality", () => {
|
||||||
const stackFrames = [
|
const stackFrames = [
|
||||||
/^ at .*Error \(.*\/Error\.prototype\.stack\.js:\d+:\d+\)$/,
|
/^ at .*Error$/,
|
||||||
/^ at .+\/Error\/Error\.prototype\.stack\.js:\d+:\d+$/,
|
/^ at .+\/Error\/Error\.prototype\.stack\.js:\d+:\d+$/,
|
||||||
/^ at test \(.+\/test-common.js:\d+:\d+\)$/,
|
/^ at test \(.+\/test-common.js:\d+:\d+\)$/,
|
||||||
/^ at .+\/Error\/Error\.prototype\.stack\.js:6:9$/,
|
/^ at .+\/Error\/Error\.prototype\.stack\.js:6:9$/,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue