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
Notes:
sideshowbarker
2024-07-17 03:59:29 +09:00
Author: https://github.com/skyrising Commit: https://github.com/SerenityOS/serenity/commit/68f4d21de2 Pull-request: https://github.com/SerenityOS/serenity/pull/21724 Reviewed-by: https://github.com/alimpfard
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?
|
||||
"//Userland",
|
||||
]
|
||||
cflags_cc = [ "-fno-omit-frame-pointer" ]
|
||||
deps = [
|
||||
"//AK",
|
||||
"//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
|
||||
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
|
||||
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()) {
|
||||
native_executable->run(vm());
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ public:
|
|||
Executable& current_executable() { return *m_current_executable; }
|
||||
Executable const& current_executable() const { return *m_current_executable; }
|
||||
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&);
|
||||
|
||||
|
|
|
@ -271,3 +271,4 @@ target_link_libraries(LibJS PRIVATE LibCore LibCrypto LibFileSystem LibRegex Lib
|
|||
if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64")
|
||||
target_link_libraries(LibJS PRIVATE LibX86)
|
||||
endif()
|
||||
target_compile_options(LibJS PRIVATE -fno-omit-frame-pointer)
|
||||
|
|
|
@ -12,15 +12,6 @@
|
|||
#include <LibX86/Disassembler.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 {
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
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 end = start + m_size;
|
||||
for (auto i = 0; i < count; i++) {
|
||||
auto address = bit_cast<FlatPtr>(buffer[i]);
|
||||
if (address < start || address >= end)
|
||||
continue;
|
||||
// return address points after the call
|
||||
// let's subtract 1 to make sure we don't hit the next bytecode
|
||||
// (in practice that's not necessary, because our native_call() sequence continues)
|
||||
auto offset = address - start - 1;
|
||||
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;
|
||||
}
|
||||
if (address < start || address >= end)
|
||||
return {};
|
||||
auto const& entry = find_mapping_entry(address - start - 1);
|
||||
if (entry.block_index < executable.basic_blocks.size()) {
|
||||
auto const& block = *executable.basic_blocks[entry.block_index];
|
||||
if (entry.bytecode_offset < block.size()) {
|
||||
auto iterator = Bytecode::InstructionStreamIterator { block.instruction_stream(), &executable, entry.bytecode_offset };
|
||||
return iterator.source_range();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ public:
|
|||
void run(VM&) const;
|
||||
void dump_disassembly(Bytecode::Executable const& executable) 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 }; }
|
||||
|
||||
|
|
|
@ -75,13 +75,13 @@ ThrowCompletionOr<void> Error::install_error_cause(Value options)
|
|||
|
||||
void Error::populate_stack()
|
||||
{
|
||||
auto& vm = this->vm();
|
||||
m_traceback.ensure_capacity(vm.execution_context_stack().size());
|
||||
for (ssize_t i = vm.execution_context_stack().size() - 1; i >= 0; i--) {
|
||||
auto context = vm.execution_context_stack()[i];
|
||||
auto stack_trace = vm().stack_trace();
|
||||
m_traceback.ensure_capacity(stack_trace.size());
|
||||
for (auto& element : stack_trace) {
|
||||
auto* context = element.execution_context;
|
||||
UnrealizedSourceRange range = {};
|
||||
if (context->instruction_stream_iterator.has_value())
|
||||
range = context->instruction_stream_iterator->source_range();
|
||||
if (element.source_range.has_value())
|
||||
range = element.source_range.value();
|
||||
TracebackFrame frame {
|
||||
.function_name = context->function_name,
|
||||
.source_range_storage = range,
|
||||
|
|
|
@ -51,9 +51,16 @@ public:
|
|||
MarkedVector<Value> local_variables;
|
||||
bool is_strict_mode { false };
|
||||
|
||||
RefPtr<Bytecode::Executable> executable;
|
||||
|
||||
// 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.
|
||||
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 <LibJS/AST.h>
|
||||
#include <LibJS/Bytecode/Interpreter.h>
|
||||
#include <LibJS/JIT/NativeExecutable.h>
|
||||
#include <LibJS/Runtime/AbstractOperations.h>
|
||||
#include <LibJS/Runtime/Array.h>
|
||||
#include <LibJS/Runtime/BoundFunction.h>
|
||||
|
@ -1135,4 +1136,66 @@ void VM::pop_execution_context()
|
|||
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.
|
||||
ThrowCompletionOr<Value> execute_ast_node(ASTNode const&);
|
||||
|
||||
Vector<StackTraceElement> stack_trace() const;
|
||||
|
||||
private:
|
||||
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); }
|
||||
|
||||
Vector<FlatPtr> get_native_stack_trace() const;
|
||||
|
||||
HashMap<String, GCPtr<PrimitiveString>> m_string_cache;
|
||||
HashMap<DeprecatedString, GCPtr<PrimitiveString>> m_deprecated_string_cache;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const stackSetter = stackDescriptor.set;
|
|||
describe("getter - normal behavior", () => {
|
||||
test("basic functionality", () => {
|
||||
const stackFrames = [
|
||||
/^ at .*Error \(.*\/Error\.prototype\.stack\.js:\d+:\d+\)$/,
|
||||
/^ at .*Error$/,
|
||||
/^ at .+\/Error\/Error\.prototype\.stack\.js:\d+:\d+$/,
|
||||
/^ at test \(.+\/test-common.js:\d+:\d+\)$/,
|
||||
/^ at .+\/Error\/Error\.prototype\.stack\.js:6:9$/,
|
||||
|
|
Loading…
Add table
Reference in a new issue