mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 16:52:43 +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
	
	 Simon Wanner
						Simon Wanner