| // Copyright 2012 the V8 project authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/debug/debug.h" |
| |
| #include <memory> |
| |
| #include "src/api.h" |
| #include "src/arguments.h" |
| #include "src/assembler-inl.h" |
| #include "src/bootstrapper.h" |
| #include "src/code-stubs.h" |
| #include "src/compilation-cache.h" |
| #include "src/compiler-dispatcher/optimizing-compile-dispatcher.h" |
| #include "src/compiler.h" |
| #include "src/debug/debug-evaluate.h" |
| #include "src/debug/liveedit.h" |
| #include "src/deoptimizer.h" |
| #include "src/execution.h" |
| #include "src/frames-inl.h" |
| #include "src/global-handles.h" |
| #include "src/globals.h" |
| #include "src/interpreter/interpreter.h" |
| #include "src/isolate-inl.h" |
| #include "src/log.h" |
| #include "src/messages.h" |
| #include "src/objects/debug-objects-inl.h" |
| #include "src/snapshot/natives.h" |
| #include "src/wasm/wasm-objects-inl.h" |
| |
| #include "include/v8-debug.h" |
| |
| namespace v8 { |
| namespace internal { |
| |
| Debug::Debug(Isolate* isolate) |
| : debug_context_(Handle<Context>()), |
| is_active_(false), |
| hook_on_function_call_(false), |
| is_suppressed_(false), |
| live_edit_enabled_(false), |
| break_disabled_(false), |
| break_points_active_(true), |
| break_on_exception_(false), |
| break_on_uncaught_exception_(false), |
| side_effect_check_failed_(false), |
| debug_info_list_(nullptr), |
| feature_tracker_(isolate), |
| isolate_(isolate) { |
| ThreadInit(); |
| } |
| |
| BreakLocation BreakLocation::FromFrame(Handle<DebugInfo> debug_info, |
| JavaScriptFrame* frame) { |
| auto summary = FrameSummary::GetTop(frame).AsJavaScript(); |
| int offset = summary.code_offset(); |
| Handle<AbstractCode> abstract_code = summary.abstract_code(); |
| BreakIterator it(debug_info); |
| it.SkipTo(BreakIndexFromCodeOffset(debug_info, abstract_code, offset)); |
| return it.GetBreakLocation(); |
| } |
| |
| void BreakLocation::AllAtCurrentStatement( |
| Handle<DebugInfo> debug_info, JavaScriptFrame* frame, |
| std::vector<BreakLocation>* result_out) { |
| auto summary = FrameSummary::GetTop(frame).AsJavaScript(); |
| int offset = summary.code_offset(); |
| Handle<AbstractCode> abstract_code = summary.abstract_code(); |
| if (abstract_code->IsCode()) offset = offset - 1; |
| int statement_position; |
| { |
| BreakIterator it(debug_info); |
| it.SkipTo(BreakIndexFromCodeOffset(debug_info, abstract_code, offset)); |
| statement_position = it.statement_position(); |
| } |
| for (BreakIterator it(debug_info); !it.Done(); it.Next()) { |
| if (it.statement_position() == statement_position) { |
| result_out->push_back(it.GetBreakLocation()); |
| } |
| } |
| } |
| |
| int BreakLocation::BreakIndexFromCodeOffset(Handle<DebugInfo> debug_info, |
| Handle<AbstractCode> abstract_code, |
| int offset) { |
| // Run through all break points to locate the one closest to the address. |
| int closest_break = 0; |
| int distance = kMaxInt; |
| DCHECK(0 <= offset && offset < abstract_code->Size()); |
| for (BreakIterator it(debug_info); !it.Done(); it.Next()) { |
| // Check if this break point is closer that what was previously found. |
| if (it.code_offset() <= offset && offset - it.code_offset() < distance) { |
| closest_break = it.break_index(); |
| distance = offset - it.code_offset(); |
| // Check whether we can't get any closer. |
| if (distance == 0) break; |
| } |
| } |
| return closest_break; |
| } |
| |
| bool BreakLocation::HasBreakPoint(Handle<DebugInfo> debug_info) const { |
| // First check whether there is a break point with the same source position. |
| if (!debug_info->HasBreakPoint(position_)) return false; |
| // Then check whether a break point at that source position would have |
| // the same code offset. Otherwise it's just a break location that we can |
| // step to, but not actually a location where we can put a break point. |
| DCHECK(abstract_code_->IsBytecodeArray()); |
| BreakIterator it(debug_info); |
| it.SkipToPosition(position_); |
| return it.code_offset() == code_offset_; |
| } |
| |
| debug::BreakLocationType BreakLocation::type() const { |
| switch (type_) { |
| case DEBUGGER_STATEMENT: |
| return debug::kDebuggerStatementBreakLocation; |
| case DEBUG_BREAK_SLOT_AT_CALL: |
| return debug::kCallBreakLocation; |
| case DEBUG_BREAK_SLOT_AT_RETURN: |
| return debug::kReturnBreakLocation; |
| default: |
| return debug::kCommonBreakLocation; |
| } |
| return debug::kCommonBreakLocation; |
| } |
| |
| BreakIterator::BreakIterator(Handle<DebugInfo> debug_info) |
| : debug_info_(debug_info), |
| break_index_(-1), |
| source_position_iterator_( |
| debug_info->DebugBytecodeArray()->SourcePositionTable()) { |
| position_ = debug_info->shared()->start_position(); |
| statement_position_ = position_; |
| // There is at least one break location. |
| DCHECK(!Done()); |
| Next(); |
| } |
| |
| int BreakIterator::BreakIndexFromPosition(int source_position) { |
| int distance = kMaxInt; |
| int closest_break = break_index(); |
| while (!Done()) { |
| int next_position = position(); |
| if (source_position <= next_position && |
| next_position - source_position < distance) { |
| closest_break = break_index(); |
| distance = next_position - source_position; |
| // Check whether we can't get any closer. |
| if (distance == 0) break; |
| } |
| Next(); |
| } |
| return closest_break; |
| } |
| |
| void BreakIterator::Next() { |
| DisallowHeapAllocation no_gc; |
| DCHECK(!Done()); |
| bool first = break_index_ == -1; |
| while (!Done()) { |
| if (!first) source_position_iterator_.Advance(); |
| first = false; |
| if (Done()) return; |
| position_ = source_position_iterator_.source_position().ScriptOffset(); |
| if (source_position_iterator_.is_statement()) { |
| statement_position_ = position_; |
| } |
| DCHECK_LE(0, position_); |
| DCHECK_LE(0, statement_position_); |
| |
| DebugBreakType type = GetDebugBreakType(); |
| if (type != NOT_DEBUG_BREAK) break; |
| } |
| break_index_++; |
| } |
| |
| DebugBreakType BreakIterator::GetDebugBreakType() { |
| BytecodeArray* bytecode_array = debug_info_->OriginalBytecodeArray(); |
| interpreter::Bytecode bytecode = |
| interpreter::Bytecodes::FromByte(bytecode_array->get(code_offset())); |
| |
| if (bytecode == interpreter::Bytecode::kDebugger) { |
| return DEBUGGER_STATEMENT; |
| } else if (bytecode == interpreter::Bytecode::kReturn) { |
| return DEBUG_BREAK_SLOT_AT_RETURN; |
| } else if (interpreter::Bytecodes::IsCallOrConstruct(bytecode)) { |
| return DEBUG_BREAK_SLOT_AT_CALL; |
| } else if (source_position_iterator_.is_statement()) { |
| return DEBUG_BREAK_SLOT; |
| } else { |
| return NOT_DEBUG_BREAK; |
| } |
| } |
| |
| void BreakIterator::SkipToPosition(int position) { |
| BreakIterator it(debug_info_); |
| SkipTo(it.BreakIndexFromPosition(position)); |
| } |
| |
| void BreakIterator::SetDebugBreak() { |
| DebugBreakType debug_break_type = GetDebugBreakType(); |
| if (debug_break_type == DEBUGGER_STATEMENT) return; |
| DCHECK(debug_break_type >= DEBUG_BREAK_SLOT); |
| BytecodeArray* bytecode_array = debug_info_->DebugBytecodeArray(); |
| interpreter::Bytecode bytecode = |
| interpreter::Bytecodes::FromByte(bytecode_array->get(code_offset())); |
| if (interpreter::Bytecodes::IsDebugBreak(bytecode)) return; |
| interpreter::Bytecode debugbreak = |
| interpreter::Bytecodes::GetDebugBreak(bytecode); |
| bytecode_array->set(code_offset(), |
| interpreter::Bytecodes::ToByte(debugbreak)); |
| } |
| |
| void BreakIterator::ClearDebugBreak() { |
| DebugBreakType debug_break_type = GetDebugBreakType(); |
| if (debug_break_type == DEBUGGER_STATEMENT) return; |
| DCHECK(debug_break_type >= DEBUG_BREAK_SLOT); |
| BytecodeArray* bytecode_array = debug_info_->DebugBytecodeArray(); |
| BytecodeArray* original = debug_info_->OriginalBytecodeArray(); |
| bytecode_array->set(code_offset(), original->get(code_offset())); |
| } |
| |
| BreakLocation BreakIterator::GetBreakLocation() { |
| Handle<AbstractCode> code( |
| AbstractCode::cast(debug_info_->DebugBytecodeArray())); |
| return BreakLocation(code, GetDebugBreakType(), code_offset(), position_); |
| } |
| |
| |
| void DebugFeatureTracker::Track(DebugFeatureTracker::Feature feature) { |
| uint32_t mask = 1 << feature; |
| // Only count one sample per feature and isolate. |
| if (bitfield_ & mask) return; |
| isolate_->counters()->debug_feature_usage()->AddSample(feature); |
| bitfield_ |= mask; |
| } |
| |
| |
| // Threading support. |
| void Debug::ThreadInit() { |
| thread_local_.break_count_ = 0; |
| thread_local_.break_id_ = 0; |
| thread_local_.break_frame_id_ = StackFrame::NO_ID; |
| thread_local_.last_step_action_ = StepNone; |
| thread_local_.last_statement_position_ = kNoSourcePosition; |
| thread_local_.last_frame_count_ = -1; |
| thread_local_.fast_forward_to_return_ = false; |
| thread_local_.ignore_step_into_function_ = Smi::kZero; |
| thread_local_.target_frame_count_ = -1; |
| thread_local_.return_value_ = Smi::kZero; |
| thread_local_.async_task_count_ = 0; |
| thread_local_.last_breakpoint_id_ = 0; |
| clear_suspended_generator(); |
| thread_local_.restart_fp_ = nullptr; |
| base::Relaxed_Store(&thread_local_.current_debug_scope_, |
| static_cast<base::AtomicWord>(0)); |
| UpdateHookOnFunctionCall(); |
| } |
| |
| |
| char* Debug::ArchiveDebug(char* storage) { |
| // Simply reset state. Don't archive anything. |
| ThreadInit(); |
| return storage + ArchiveSpacePerThread(); |
| } |
| |
| |
| char* Debug::RestoreDebug(char* storage) { |
| // Simply reset state. Don't restore anything. |
| ThreadInit(); |
| return storage + ArchiveSpacePerThread(); |
| } |
| |
| int Debug::ArchiveSpacePerThread() { return 0; } |
| |
| void Debug::Iterate(RootVisitor* v) { |
| v->VisitRootPointer(Root::kDebug, &thread_local_.return_value_); |
| v->VisitRootPointer(Root::kDebug, &thread_local_.suspended_generator_); |
| v->VisitRootPointer(Root::kDebug, &thread_local_.ignore_step_into_function_); |
| } |
| |
| DebugInfoListNode::DebugInfoListNode(DebugInfo* debug_info) : next_(nullptr) { |
| // Globalize the request debug info object and make it weak. |
| GlobalHandles* global_handles = debug_info->GetIsolate()->global_handles(); |
| debug_info_ = global_handles->Create(debug_info).location(); |
| } |
| |
| |
| DebugInfoListNode::~DebugInfoListNode() { |
| if (debug_info_ == nullptr) return; |
| GlobalHandles::Destroy(reinterpret_cast<Object**>(debug_info_)); |
| debug_info_ = nullptr; |
| } |
| |
| |
| bool Debug::Load() { |
| // Return if debugger is already loaded. |
| if (is_loaded()) return true; |
| |
| // Bail out if we're already in the process of compiling the native |
| // JavaScript source code for the debugger. |
| if (is_suppressed_) return false; |
| SuppressDebug while_loading(this); |
| |
| // Disable breakpoints and interrupts while compiling and running the |
| // debugger scripts including the context creation code. |
| DisableBreak disable(this); |
| PostponeInterruptsScope postpone(isolate_); |
| |
| // Create the debugger context. |
| HandleScope scope(isolate_); |
| ExtensionConfiguration no_extensions; |
| // TODO(yangguo): we rely on the fact that first context snapshot is usable |
| // as debug context. This dependency is gone once we remove |
| // debug context completely. |
| static const int kFirstContextSnapshotIndex = 0; |
| Handle<Context> context = isolate_->bootstrapper()->CreateEnvironment( |
| MaybeHandle<JSGlobalProxy>(), v8::Local<ObjectTemplate>(), &no_extensions, |
| kFirstContextSnapshotIndex, v8::DeserializeEmbedderFieldsCallback(), |
| DEBUG_CONTEXT); |
| |
| // Fail if no context could be created. |
| if (context.is_null()) return false; |
| |
| debug_context_ = isolate_->global_handles()->Create(*context); |
| |
| feature_tracker()->Track(DebugFeatureTracker::kActive); |
| |
| return true; |
| } |
| |
| |
| void Debug::Unload() { |
| ClearAllBreakPoints(); |
| ClearStepping(); |
| RemoveAllCoverageInfos(); |
| RemoveDebugDelegate(); |
| |
| // Return debugger is not loaded. |
| if (!is_loaded()) return; |
| |
| // Clear debugger context global handle. |
| GlobalHandles::Destroy(Handle<Object>::cast(debug_context_).location()); |
| debug_context_ = Handle<Context>(); |
| } |
| |
| void Debug::Break(JavaScriptFrame* frame) { |
| // Initialize LiveEdit. |
| LiveEdit::InitializeThreadLocal(this); |
| |
| // Just continue if breaks are disabled or debugger cannot be loaded. |
| if (break_disabled()) return; |
| |
| // Enter the debugger. |
| DebugScope debug_scope(this); |
| if (debug_scope.failed()) return; |
| |
| // Postpone interrupt during breakpoint processing. |
| PostponeInterruptsScope postpone(isolate_); |
| DisableBreak no_recursive_break(this); |
| |
| // Return if we fail to retrieve debug info. |
| Handle<JSFunction> function(frame->function()); |
| Handle<SharedFunctionInfo> shared(function->shared()); |
| if (!EnsureBreakInfo(shared)) return; |
| Handle<DebugInfo> debug_info(shared->GetDebugInfo(), isolate_); |
| |
| // Find the break location where execution has stopped. |
| BreakLocation location = BreakLocation::FromFrame(debug_info, frame); |
| |
| // Find actual break points, if any, and trigger debug break event. |
| MaybeHandle<FixedArray> break_points_hit = |
| CheckBreakPoints(debug_info, &location); |
| if (!break_points_hit.is_null()) { |
| // Clear all current stepping setup. |
| ClearStepping(); |
| // Notify the debug event listeners. |
| OnDebugBreak(break_points_hit.ToHandleChecked()); |
| return; |
| } |
| |
| // No break point. Check for stepping. |
| StepAction step_action = last_step_action(); |
| int current_frame_count = CurrentFrameCount(); |
| int target_frame_count = thread_local_.target_frame_count_; |
| int last_frame_count = thread_local_.last_frame_count_; |
| |
| // StepOut at not return position was requested and return break locations |
| // were flooded with one shots. |
| if (thread_local_.fast_forward_to_return_) { |
| DCHECK(location.IsReturn()); |
| // We have to ignore recursive calls to function. |
| if (current_frame_count > target_frame_count) return; |
| ClearStepping(); |
| PrepareStep(StepOut); |
| return; |
| } |
| |
| bool step_break = false; |
| switch (step_action) { |
| case StepNone: |
| return; |
| case StepOut: |
| // Step out should not break in a deeper frame than target frame. |
| if (current_frame_count > target_frame_count) return; |
| step_break = true; |
| break; |
| case StepNext: |
| // Step next should not break in a deeper frame than target frame. |
| if (current_frame_count > target_frame_count) return; |
| // Fall through. |
| case StepIn: { |
| FrameSummary summary = FrameSummary::GetTop(frame); |
| step_break = step_break || location.IsReturn() || |
| current_frame_count != last_frame_count || |
| thread_local_.last_statement_position_ != |
| summary.SourceStatementPosition(); |
| break; |
| } |
| } |
| |
| // Clear all current stepping setup. |
| ClearStepping(); |
| |
| if (step_break) { |
| // Notify the debug event listeners. |
| OnDebugBreak(isolate_->factory()->empty_fixed_array()); |
| } else { |
| // Re-prepare to continue. |
| PrepareStep(step_action); |
| } |
| } |
| |
| |
| // Find break point objects for this location, if any, and evaluate them. |
| // Return an array of break point objects that evaluated true, or an empty |
| // handle if none evaluated true. |
| MaybeHandle<FixedArray> Debug::CheckBreakPoints(Handle<DebugInfo> debug_info, |
| BreakLocation* location, |
| bool* has_break_points) { |
| bool has_break_points_to_check = |
| break_points_active_ && location->HasBreakPoint(debug_info); |
| if (has_break_points) *has_break_points = has_break_points_to_check; |
| if (!has_break_points_to_check) return {}; |
| |
| Handle<Object> break_point_objects = |
| debug_info->GetBreakPointObjects(location->position()); |
| return Debug::GetHitBreakPointObjects(break_point_objects); |
| } |
| |
| |
| bool Debug::IsMutedAtCurrentLocation(JavaScriptFrame* frame) { |
| HandleScope scope(isolate_); |
| // A break location is considered muted if break locations on the current |
| // statement have at least one break point, and all of these break points |
| // evaluate to false. Aside from not triggering a debug break event at the |
| // break location, we also do not trigger one for debugger statements, nor |
| // an exception event on exception at this location. |
| FrameSummary summary = FrameSummary::GetTop(frame); |
| DCHECK(!summary.IsWasm()); |
| Handle<JSFunction> function = summary.AsJavaScript().function(); |
| if (!function->shared()->HasBreakInfo()) return false; |
| Handle<DebugInfo> debug_info(function->shared()->GetDebugInfo()); |
| // Enter the debugger. |
| DebugScope debug_scope(this); |
| if (debug_scope.failed()) return false; |
| std::vector<BreakLocation> break_locations; |
| BreakLocation::AllAtCurrentStatement(debug_info, frame, &break_locations); |
| bool has_break_points_at_all = false; |
| for (size_t i = 0; i < break_locations.size(); i++) { |
| bool has_break_points; |
| MaybeHandle<FixedArray> check_result = |
| CheckBreakPoints(debug_info, &break_locations[i], &has_break_points); |
| has_break_points_at_all |= has_break_points; |
| if (has_break_points && !check_result.is_null()) return false; |
| } |
| return has_break_points_at_all; |
| } |
| |
| MaybeHandle<Object> Debug::CallFunction(const char* name, int argc, |
| Handle<Object> args[], |
| bool catch_exceptions) { |
| AllowJavascriptExecutionDebugOnly allow_script(isolate_); |
| PostponeInterruptsScope no_interrupts(isolate_); |
| AssertDebugContext(); |
| Handle<JSReceiver> holder = |
| Handle<JSReceiver>::cast(isolate_->natives_utils_object()); |
| Handle<JSFunction> fun = Handle<JSFunction>::cast( |
| JSReceiver::GetProperty(isolate_, holder, name).ToHandleChecked()); |
| Handle<Object> undefined = isolate_->factory()->undefined_value(); |
| if (catch_exceptions) { |
| MaybeHandle<Object> maybe_exception; |
| return Execution::TryCall(isolate_, fun, undefined, argc, args, |
| Execution::MessageHandling::kReport, |
| &maybe_exception); |
| } else { |
| return Execution::Call(isolate_, fun, undefined, argc, args); |
| } |
| } |
| |
| |
| // Check whether a single break point object is triggered. |
| bool Debug::CheckBreakPoint(Handle<Object> break_point_object) { |
| Factory* factory = isolate_->factory(); |
| HandleScope scope(isolate_); |
| |
| // TODO(kozyatinskiy): replace this if by DCHEK once the JS debug API has been |
| // removed. |
| if (break_point_object->IsBreakPoint()) { |
| Handle<BreakPoint> break_point = |
| Handle<BreakPoint>::cast(break_point_object); |
| if (!break_point->condition()->length()) return true; |
| Handle<String> condition(break_point->condition()); |
| Handle<Object> result; |
| // Since we call CheckBreakpoint only for deoptimized frame on top of stack, |
| // we can use 0 as index of inlined frame. |
| if (!DebugEvaluate::Local(isolate_, break_frame_id(), |
| /* inlined_jsframe_index */ 0, condition, false) |
| .ToHandle(&result)) { |
| if (isolate_->has_pending_exception()) { |
| isolate_->clear_pending_exception(); |
| } |
| return false; |
| } |
| return result->BooleanValue(); |
| } |
| |
| // Ignore check if break point object is not a JSObject. |
| if (!break_point_object->IsJSObject()) return true; |
| |
| // Get the break id as an object. |
| Handle<Object> break_id = factory->NewNumberFromInt(Debug::break_id()); |
| |
| // Call IsBreakPointTriggered. |
| Handle<Object> argv[] = { break_id, break_point_object }; |
| Handle<Object> result; |
| if (!CallFunction("IsBreakPointTriggered", arraysize(argv), argv) |
| .ToHandle(&result)) { |
| return false; |
| } |
| |
| // Return whether the break point is triggered. |
| return result->IsTrue(isolate_); |
| } |
| |
| |
| bool Debug::SetBreakPoint(Handle<JSFunction> function, |
| Handle<Object> break_point_object, |
| int* source_position) { |
| HandleScope scope(isolate_); |
| |
| // Make sure the function is compiled and has set up the debug info. |
| Handle<SharedFunctionInfo> shared(function->shared()); |
| if (!EnsureBreakInfo(shared)) return true; |
| PrepareFunctionForBreakPoints(shared); |
| Handle<DebugInfo> debug_info(shared->GetDebugInfo()); |
| // Source positions starts with zero. |
| DCHECK_LE(0, *source_position); |
| |
| // Find the break point and change it. |
| *source_position = FindBreakablePosition(debug_info, *source_position); |
| DebugInfo::SetBreakPoint(debug_info, *source_position, break_point_object); |
| // At least one active break point now. |
| DCHECK_LT(0, debug_info->GetBreakPointCount()); |
| |
| ClearBreakPoints(debug_info); |
| ApplyBreakPoints(debug_info); |
| |
| feature_tracker()->Track(DebugFeatureTracker::kBreakPoint); |
| return true; |
| } |
| |
| bool Debug::SetBreakPointForScript(Handle<Script> script, |
| Handle<Object> break_point_object, |
| int* source_position) { |
| if (script->type() == Script::TYPE_WASM) { |
| Handle<WasmCompiledModule> compiled_module( |
| WasmCompiledModule::cast(script->wasm_compiled_module()), isolate_); |
| return WasmCompiledModule::SetBreakPoint(compiled_module, source_position, |
| break_point_object); |
| } |
| |
| HandleScope scope(isolate_); |
| |
| // Obtain shared function info for the function. |
| Handle<Object> result = |
| FindSharedFunctionInfoInScript(script, *source_position); |
| if (result->IsUndefined(isolate_)) return false; |
| |
| // Make sure the function has set up the debug info. |
| Handle<SharedFunctionInfo> shared = Handle<SharedFunctionInfo>::cast(result); |
| if (!EnsureBreakInfo(shared)) return false; |
| PrepareFunctionForBreakPoints(shared); |
| |
| // Find position within function. The script position might be before the |
| // source position of the first function. |
| if (shared->start_position() > *source_position) { |
| *source_position = shared->start_position(); |
| } |
| |
| Handle<DebugInfo> debug_info(shared->GetDebugInfo()); |
| |
| // Find breakable position returns first breakable position after |
| // *source_position, it can return 0 if no break location is found after |
| // *source_position. |
| int breakable_position = FindBreakablePosition(debug_info, *source_position); |
| if (breakable_position < *source_position) return false; |
| *source_position = breakable_position; |
| |
| DebugInfo::SetBreakPoint(debug_info, *source_position, break_point_object); |
| // At least one active break point now. |
| DCHECK_LT(0, debug_info->GetBreakPointCount()); |
| |
| ClearBreakPoints(debug_info); |
| ApplyBreakPoints(debug_info); |
| |
| feature_tracker()->Track(DebugFeatureTracker::kBreakPoint); |
| return true; |
| } |
| |
| int Debug::FindBreakablePosition(Handle<DebugInfo> debug_info, |
| int source_position) { |
| DCHECK(debug_info->HasDebugBytecodeArray()); |
| BreakIterator it(debug_info); |
| it.SkipToPosition(source_position); |
| return it.position(); |
| } |
| |
| void Debug::ApplyBreakPoints(Handle<DebugInfo> debug_info) { |
| DisallowHeapAllocation no_gc; |
| if (debug_info->break_points()->IsUndefined(isolate_)) return; |
| FixedArray* break_points = debug_info->break_points(); |
| for (int i = 0; i < break_points->length(); i++) { |
| if (break_points->get(i)->IsUndefined(isolate_)) continue; |
| BreakPointInfo* info = BreakPointInfo::cast(break_points->get(i)); |
| if (info->GetBreakPointCount() == 0) continue; |
| DCHECK(debug_info->HasDebugBytecodeArray()); |
| BreakIterator it(debug_info); |
| it.SkipToPosition(info->source_position()); |
| it.SetDebugBreak(); |
| } |
| } |
| |
| void Debug::ClearBreakPoints(Handle<DebugInfo> debug_info) { |
| // If we attempt to clear breakpoints but none exist, simply return. This can |
| // happen e.g. CoverageInfos exit but no breakpoints are set. |
| if (!debug_info->HasDebugBytecodeArray()) return; |
| |
| DisallowHeapAllocation no_gc; |
| for (BreakIterator it(debug_info); !it.Done(); it.Next()) { |
| it.ClearDebugBreak(); |
| } |
| } |
| |
| void Debug::ClearBreakPoint(Handle<Object> break_point_object) { |
| HandleScope scope(isolate_); |
| |
| for (DebugInfoListNode* node = debug_info_list_; node != nullptr; |
| node = node->next()) { |
| Handle<Object> result = |
| DebugInfo::FindBreakPointInfo(node->debug_info(), break_point_object); |
| if (result->IsUndefined(isolate_)) continue; |
| Handle<DebugInfo> debug_info = node->debug_info(); |
| if (DebugInfo::ClearBreakPoint(debug_info, break_point_object)) { |
| ClearBreakPoints(debug_info); |
| if (debug_info->GetBreakPointCount() == 0) { |
| RemoveBreakInfoAndMaybeFree(debug_info); |
| } else { |
| ApplyBreakPoints(debug_info); |
| } |
| return; |
| } |
| } |
| } |
| |
| bool Debug::SetBreakpoint(Handle<Script> script, Handle<String> condition, |
| int* offset, int* id) { |
| *id = ++thread_local_.last_breakpoint_id_; |
| Handle<BreakPoint> breakpoint = |
| isolate_->factory()->NewBreakPoint(*id, condition); |
| return SetBreakPointForScript(script, breakpoint, offset); |
| } |
| |
| void Debug::RemoveBreakpoint(int id) { |
| Handle<BreakPoint> breakpoint = isolate_->factory()->NewBreakPoint( |
| id, isolate_->factory()->empty_string()); |
| ClearBreakPoint(breakpoint); |
| } |
| |
| // Clear out all the debug break code. |
| void Debug::ClearAllBreakPoints() { |
| ClearAllDebugInfos([=](Handle<DebugInfo> info) { |
| ClearBreakPoints(info); |
| return info->ClearBreakInfo(); |
| }); |
| } |
| |
| void Debug::FloodWithOneShot(Handle<SharedFunctionInfo> shared, |
| bool returns_only) { |
| if (IsBlackboxed(shared)) return; |
| // Make sure the function is compiled and has set up the debug info. |
| if (!EnsureBreakInfo(shared)) return; |
| PrepareFunctionForBreakPoints(shared); |
| Handle<DebugInfo> debug_info(shared->GetDebugInfo()); |
| // Flood the function with break points. |
| DCHECK(debug_info->HasDebugBytecodeArray()); |
| for (BreakIterator it(debug_info); !it.Done(); it.Next()) { |
| if (returns_only && !it.GetBreakLocation().IsReturn()) continue; |
| it.SetDebugBreak(); |
| } |
| } |
| |
| void Debug::ChangeBreakOnException(ExceptionBreakType type, bool enable) { |
| if (type == BreakUncaughtException) { |
| break_on_uncaught_exception_ = enable; |
| } else { |
| break_on_exception_ = enable; |
| } |
| } |
| |
| |
| bool Debug::IsBreakOnException(ExceptionBreakType type) { |
| if (type == BreakUncaughtException) { |
| return break_on_uncaught_exception_; |
| } else { |
| return break_on_exception_; |
| } |
| } |
| |
| MaybeHandle<FixedArray> Debug::GetHitBreakPointObjects( |
| Handle<Object> break_point_objects) { |
| DCHECK(!break_point_objects->IsUndefined(isolate_)); |
| if (!break_point_objects->IsFixedArray()) { |
| if (!CheckBreakPoint(break_point_objects)) return {}; |
| Handle<FixedArray> break_points_hit = isolate_->factory()->NewFixedArray(1); |
| break_points_hit->set(0, *break_point_objects); |
| return break_points_hit; |
| } |
| |
| Handle<FixedArray> array(FixedArray::cast(*break_point_objects)); |
| int num_objects = array->length(); |
| Handle<FixedArray> break_points_hit = |
| isolate_->factory()->NewFixedArray(num_objects); |
| int break_points_hit_count = 0; |
| for (int i = 0; i < num_objects; ++i) { |
| Handle<Object> break_point_object(array->get(i), isolate_); |
| if (CheckBreakPoint(break_point_object)) { |
| break_points_hit->set(break_points_hit_count++, *break_point_object); |
| } |
| } |
| if (break_points_hit_count == 0) return {}; |
| break_points_hit->Shrink(break_points_hit_count); |
| return break_points_hit; |
| } |
| |
| void Debug::PrepareStepIn(Handle<JSFunction> function) { |
| CHECK(last_step_action() >= StepIn); |
| if (ignore_events()) return; |
| if (in_debug_scope()) return; |
| if (break_disabled()) return; |
| Handle<SharedFunctionInfo> shared(function->shared()); |
| if (IsBlackboxed(shared)) return; |
| if (*function == thread_local_.ignore_step_into_function_) return; |
| thread_local_.ignore_step_into_function_ = Smi::kZero; |
| FloodWithOneShot(Handle<SharedFunctionInfo>(function->shared(), isolate_)); |
| } |
| |
| void Debug::PrepareStepInSuspendedGenerator() { |
| CHECK(has_suspended_generator()); |
| if (ignore_events()) return; |
| if (in_debug_scope()) return; |
| if (break_disabled()) return; |
| thread_local_.last_step_action_ = StepIn; |
| UpdateHookOnFunctionCall(); |
| Handle<JSFunction> function( |
| JSGeneratorObject::cast(thread_local_.suspended_generator_)->function()); |
| FloodWithOneShot(Handle<SharedFunctionInfo>(function->shared(), isolate_)); |
| clear_suspended_generator(); |
| } |
| |
| void Debug::PrepareStepOnThrow() { |
| if (last_step_action() == StepNone) return; |
| if (ignore_events()) return; |
| if (in_debug_scope()) return; |
| if (break_disabled()) return; |
| |
| ClearOneShot(); |
| |
| int current_frame_count = CurrentFrameCount(); |
| |
| // Iterate through the JavaScript stack looking for handlers. |
| JavaScriptFrameIterator it(isolate_); |
| while (!it.done()) { |
| JavaScriptFrame* frame = it.frame(); |
| if (frame->LookupExceptionHandlerInTable(nullptr, nullptr) > 0) break; |
| std::vector<SharedFunctionInfo*> infos; |
| frame->GetFunctions(&infos); |
| current_frame_count -= infos.size(); |
| it.Advance(); |
| } |
| |
| // No handler found. Nothing to instrument. |
| if (it.done()) return; |
| |
| bool found_handler = false; |
| // Iterate frames, including inlined frames. First, find the handler frame. |
| // Then skip to the frame we want to break in, then instrument for stepping. |
| for (; !it.done(); it.Advance()) { |
| JavaScriptFrame* frame = JavaScriptFrame::cast(it.frame()); |
| if (last_step_action() == StepIn) { |
| // Deoptimize frame to ensure calls are checked for step-in. |
| Deoptimizer::DeoptimizeFunction(frame->function()); |
| } |
| std::vector<FrameSummary> summaries; |
| frame->Summarize(&summaries); |
| for (size_t i = summaries.size(); i != 0; i--, current_frame_count--) { |
| const FrameSummary& summary = summaries[i - 1]; |
| if (!found_handler) { |
| // We have yet to find the handler. If the frame inlines multiple |
| // functions, we have to check each one for the handler. |
| // If it only contains one function, we already found the handler. |
| if (summaries.size() > 1) { |
| Handle<AbstractCode> code = summary.AsJavaScript().abstract_code(); |
| CHECK_EQ(AbstractCode::INTERPRETED_FUNCTION, code->kind()); |
| BytecodeArray* bytecode = code->GetBytecodeArray(); |
| HandlerTable* table = HandlerTable::cast(bytecode->handler_table()); |
| int code_offset = summary.code_offset(); |
| HandlerTable::CatchPrediction prediction; |
| int index = table->LookupRange(code_offset, nullptr, &prediction); |
| if (index > 0) found_handler = true; |
| } else { |
| found_handler = true; |
| } |
| } |
| |
| if (found_handler) { |
| // We found the handler. If we are stepping next or out, we need to |
| // iterate until we found the suitable target frame to break in. |
| if ((last_step_action() == StepNext || last_step_action() == StepOut) && |
| current_frame_count > thread_local_.target_frame_count_) { |
| continue; |
| } |
| Handle<SharedFunctionInfo> info( |
| summary.AsJavaScript().function()->shared()); |
| if (IsBlackboxed(info)) continue; |
| FloodWithOneShot(info); |
| return; |
| } |
| } |
| } |
| } |
| |
| |
| void Debug::PrepareStep(StepAction step_action) { |
| HandleScope scope(isolate_); |
| |
| DCHECK(in_debug_scope()); |
| |
| // Get the frame where the execution has stopped and skip the debug frame if |
| // any. The debug frame will only be present if execution was stopped due to |
| // hitting a break point. In other situations (e.g. unhandled exception) the |
| // debug frame is not present. |
| StackFrame::Id frame_id = break_frame_id(); |
| // If there is no JavaScript stack don't do anything. |
| if (frame_id == StackFrame::NO_ID) return; |
| |
| feature_tracker()->Track(DebugFeatureTracker::kStepping); |
| |
| thread_local_.last_step_action_ = step_action; |
| |
| StackTraceFrameIterator frames_it(isolate_, frame_id); |
| StandardFrame* frame = frames_it.frame(); |
| |
| // Handle stepping in wasm functions via the wasm interpreter. |
| if (frame->is_wasm()) { |
| // If the top frame is compiled, we cannot step. |
| if (frame->is_wasm_compiled()) return; |
| WasmInterpreterEntryFrame* wasm_frame = |
| WasmInterpreterEntryFrame::cast(frame); |
| wasm_frame->wasm_instance()->debug_info()->PrepareStep(step_action); |
| return; |
| } |
| |
| JavaScriptFrame* js_frame = JavaScriptFrame::cast(frame); |
| DCHECK(js_frame->function()->IsJSFunction()); |
| |
| // Get the debug info (create it if it does not exist). |
| auto summary = FrameSummary::GetTop(frame).AsJavaScript(); |
| Handle<JSFunction> function(summary.function()); |
| Handle<SharedFunctionInfo> shared(function->shared()); |
| if (!EnsureBreakInfo(shared)) return; |
| Handle<DebugInfo> debug_info(shared->GetDebugInfo()); |
| |
| BreakLocation location = BreakLocation::FromFrame(debug_info, js_frame); |
| |
| // Any step at a return is a step-out and we need to schedule DebugOnFunction |
| // call callback. |
| if (location.IsReturn()) { |
| // On StepOut we'll ignore our further calls to current function in |
| // PrepareStepIn callback. |
| if (last_step_action() == StepOut) { |
| thread_local_.ignore_step_into_function_ = *function; |
| } |
| step_action = StepOut; |
| thread_local_.last_step_action_ = StepIn; |
| } |
| UpdateHookOnFunctionCall(); |
| |
| // A step-next in blackboxed function is a step-out. |
| if (step_action == StepNext && IsBlackboxed(shared)) step_action = StepOut; |
| |
| thread_local_.last_statement_position_ = |
| summary.abstract_code()->SourceStatementPosition(summary.code_offset()); |
| int current_frame_count = CurrentFrameCount(); |
| thread_local_.last_frame_count_ = current_frame_count; |
| // No longer perform the current async step. |
| clear_suspended_generator(); |
| |
| switch (step_action) { |
| case StepNone: |
| UNREACHABLE(); |
| break; |
| case StepOut: { |
| // Clear last position info. For stepping out it does not matter. |
| thread_local_.last_statement_position_ = kNoSourcePosition; |
| thread_local_.last_frame_count_ = -1; |
| if (!location.IsReturn() && !IsBlackboxed(shared)) { |
| // At not return position we flood return positions with one shots and |
| // will repeat StepOut automatically at next break. |
| thread_local_.target_frame_count_ = current_frame_count; |
| thread_local_.fast_forward_to_return_ = true; |
| FloodWithOneShot(shared, true); |
| return; |
| } |
| // Skip the current frame, find the first frame we want to step out to |
| // and deoptimize every frame along the way. |
| bool in_current_frame = true; |
| for (; !frames_it.done(); frames_it.Advance()) { |
| // TODO(clemensh): Implement stepping out from JS to wasm. |
| if (frames_it.frame()->is_wasm()) continue; |
| JavaScriptFrame* frame = JavaScriptFrame::cast(frames_it.frame()); |
| if (last_step_action() == StepIn) { |
| // Deoptimize frame to ensure calls are checked for step-in. |
| Deoptimizer::DeoptimizeFunction(frame->function()); |
| } |
| HandleScope scope(isolate_); |
| std::vector<Handle<SharedFunctionInfo>> infos; |
| frame->GetFunctions(&infos); |
| for (; !infos.empty(); current_frame_count--) { |
| Handle<SharedFunctionInfo> info = infos.back(); |
| infos.pop_back(); |
| if (in_current_frame) { |
| // We want to skip out, so skip the current frame. |
| in_current_frame = false; |
| continue; |
| } |
| if (IsBlackboxed(info)) continue; |
| FloodWithOneShot(info); |
| thread_local_.target_frame_count_ = current_frame_count; |
| return; |
| } |
| } |
| break; |
| } |
| case StepNext: |
| thread_local_.target_frame_count_ = current_frame_count; |
| // Fall through. |
| case StepIn: |
| // TODO(clemensh): Implement stepping from JS into wasm. |
| FloodWithOneShot(shared); |
| break; |
| } |
| } |
| |
| // Simple function for returning the source positions for active break points. |
| Handle<Object> Debug::GetSourceBreakLocations( |
| Handle<SharedFunctionInfo> shared) { |
| Isolate* isolate = shared->GetIsolate(); |
| if (!shared->HasBreakInfo()) { |
| return isolate->factory()->undefined_value(); |
| } |
| Handle<DebugInfo> debug_info(shared->GetDebugInfo()); |
| if (debug_info->GetBreakPointCount() == 0) { |
| return isolate->factory()->undefined_value(); |
| } |
| Handle<FixedArray> locations = |
| isolate->factory()->NewFixedArray(debug_info->GetBreakPointCount()); |
| int count = 0; |
| for (int i = 0; i < debug_info->break_points()->length(); ++i) { |
| if (!debug_info->break_points()->get(i)->IsUndefined(isolate)) { |
| BreakPointInfo* break_point_info = |
| BreakPointInfo::cast(debug_info->break_points()->get(i)); |
| int break_points = break_point_info->GetBreakPointCount(); |
| if (break_points == 0) continue; |
| for (int j = 0; j < break_points; ++j) { |
| locations->set(count++, |
| Smi::FromInt(break_point_info->source_position())); |
| } |
| } |
| } |
| return locations; |
| } |
| |
| void Debug::ClearStepping() { |
| // Clear the various stepping setup. |
| ClearOneShot(); |
| |
| thread_local_.last_step_action_ = StepNone; |
| thread_local_.last_statement_position_ = kNoSourcePosition; |
| thread_local_.ignore_step_into_function_ = Smi::kZero; |
| thread_local_.fast_forward_to_return_ = false; |
| thread_local_.last_frame_count_ = -1; |
| thread_local_.target_frame_count_ = -1; |
| UpdateHookOnFunctionCall(); |
| } |
| |
| |
| // Clears all the one-shot break points that are currently set. Normally this |
| // function is called each time a break point is hit as one shot break points |
| // are used to support stepping. |
| void Debug::ClearOneShot() { |
| // The current implementation just runs through all the breakpoints. When the |
| // last break point for a function is removed that function is automatically |
| // removed from the list. |
| for (DebugInfoListNode* node = debug_info_list_; node != nullptr; |
| node = node->next()) { |
| Handle<DebugInfo> debug_info = node->debug_info(); |
| ClearBreakPoints(debug_info); |
| ApplyBreakPoints(debug_info); |
| } |
| } |
| |
| class RedirectActiveFunctions : public ThreadVisitor { |
| public: |
| explicit RedirectActiveFunctions(SharedFunctionInfo* shared) |
| : shared_(shared) { |
| DCHECK(shared->HasBytecodeArray()); |
| } |
| |
| void VisitThread(Isolate* isolate, ThreadLocalTop* top) { |
| for (JavaScriptFrameIterator it(isolate, top); !it.done(); it.Advance()) { |
| JavaScriptFrame* frame = it.frame(); |
| JSFunction* function = frame->function(); |
| if (!frame->is_interpreted()) continue; |
| if (function->shared() != shared_) continue; |
| InterpretedFrame* interpreted_frame = |
| reinterpret_cast<InterpretedFrame*>(frame); |
| BytecodeArray* debug_copy = shared_->GetDebugInfo()->DebugBytecodeArray(); |
| interpreted_frame->PatchBytecodeArray(debug_copy); |
| } |
| } |
| |
| private: |
| SharedFunctionInfo* shared_; |
| DisallowHeapAllocation no_gc_; |
| }; |
| |
| void Debug::DeoptimizeFunction(Handle<SharedFunctionInfo> shared) { |
| // Deoptimize all code compiled from this shared function info including |
| // inlining. |
| if (isolate_->concurrent_recompilation_enabled()) { |
| isolate_->optimizing_compile_dispatcher()->Flush( |
| OptimizingCompileDispatcher::BlockingBehavior::kBlock); |
| } |
| |
| // Make sure we abort incremental marking. |
| isolate_->heap()->CollectAllGarbage(Heap::kMakeHeapIterableMask, |
| GarbageCollectionReason::kDebugger); |
| |
| bool found_something = false; |
| Code::OptimizedCodeIterator iterator(isolate_); |
| while (Code* code = iterator.Next()) { |
| if (code->Inlines(*shared)) { |
| code->set_marked_for_deoptimization(true); |
| found_something = true; |
| } |
| } |
| |
| if (found_something) { |
| // Only go through with the deoptimization if something was found. |
| Deoptimizer::DeoptimizeMarkedCode(isolate_); |
| } |
| } |
| |
| void Debug::PrepareFunctionForBreakPoints(Handle<SharedFunctionInfo> shared) { |
| // To prepare bytecode for debugging, we already need to have the debug |
| // info (containing the debug copy) upfront, but since we do not recompile, |
| // preparing for break points cannot fail. |
| DCHECK(shared->is_compiled()); |
| DCHECK(shared->HasDebugInfo()); |
| DCHECK(shared->HasBreakInfo()); |
| Handle<DebugInfo> debug_info = GetOrCreateDebugInfo(shared); |
| if (debug_info->IsPreparedForBreakpoints()) return; |
| |
| DeoptimizeFunction(shared); |
| // Update PCs on the stack to point to recompiled code. |
| RedirectActiveFunctions redirect_visitor(*shared); |
| redirect_visitor.VisitThread(isolate_, isolate_->thread_local_top()); |
| isolate_->thread_manager()->IterateArchivedThreads(&redirect_visitor); |
| |
| debug_info->set_flags(debug_info->flags() | |
| DebugInfo::kPreparedForBreakpoints); |
| } |
| |
| namespace { |
| template <typename Iterator> |
| void GetBreakablePositions(Iterator* it, int start_position, int end_position, |
| std::vector<BreakLocation>* locations) { |
| while (!it->Done()) { |
| if (it->position() >= start_position && it->position() < end_position) { |
| locations->push_back(it->GetBreakLocation()); |
| } |
| it->Next(); |
| } |
| } |
| |
| void FindBreakablePositions(Handle<DebugInfo> debug_info, int start_position, |
| int end_position, |
| std::vector<BreakLocation>* locations) { |
| DCHECK(debug_info->HasDebugBytecodeArray()); |
| BreakIterator it(debug_info); |
| GetBreakablePositions(&it, start_position, end_position, locations); |
| } |
| } // namespace |
| |
| bool Debug::GetPossibleBreakpoints(Handle<Script> script, int start_position, |
| int end_position, bool restrict_to_function, |
| std::vector<BreakLocation>* locations) { |
| if (restrict_to_function) { |
| Handle<Object> result = |
| FindSharedFunctionInfoInScript(script, start_position); |
| if (result->IsUndefined(isolate_)) return false; |
| |
| // Make sure the function has set up the debug info. |
| Handle<SharedFunctionInfo> shared = |
| Handle<SharedFunctionInfo>::cast(result); |
| if (!EnsureBreakInfo(shared)) return false; |
| |
| Handle<DebugInfo> debug_info(shared->GetDebugInfo()); |
| FindBreakablePositions(debug_info, start_position, end_position, locations); |
| return true; |
| } |
| |
| while (true) { |
| HandleScope scope(isolate_); |
| std::vector<Handle<SharedFunctionInfo>> candidates; |
| SharedFunctionInfo::ScriptIterator iterator(script); |
| for (SharedFunctionInfo* info = iterator.Next(); info != nullptr; |
| info = iterator.Next()) { |
| if (info->end_position() < start_position || |
| info->start_position() >= end_position) { |
| continue; |
| } |
| if (!info->IsSubjectToDebugging()) continue; |
| if (!info->is_compiled() && !info->allows_lazy_compilation()) continue; |
| candidates.push_back(i::handle(info)); |
| } |
| |
| bool was_compiled = false; |
| for (const auto& candidate : candidates) { |
| // Code that cannot be compiled lazily are internal and not debuggable. |
| DCHECK(candidate->allows_lazy_compilation()); |
| if (!candidate->is_compiled()) { |
| if (!Compiler::Compile(candidate, Compiler::CLEAR_EXCEPTION)) { |
| return false; |
| } else { |
| was_compiled = true; |
| } |
| } |
| if (!EnsureBreakInfo(candidate)) return false; |
| } |
| if (was_compiled) continue; |
| |
| for (const auto& candidate : candidates) { |
| CHECK(candidate->HasBreakInfo()); |
| Handle<DebugInfo> debug_info(candidate->GetDebugInfo()); |
| FindBreakablePositions(debug_info, start_position, end_position, |
| locations); |
| } |
| return true; |
| } |
| UNREACHABLE(); |
| } |
| |
| void Debug::RecordGenerator(Handle<JSGeneratorObject> generator_object) { |
| if (last_step_action() <= StepOut) return; |
| |
| if (last_step_action() == StepNext) { |
| // Only consider this generator a step-next target if not stepping in. |
| if (thread_local_.target_frame_count_ < CurrentFrameCount()) return; |
| } |
| |
| DCHECK(!has_suspended_generator()); |
| thread_local_.suspended_generator_ = *generator_object; |
| ClearStepping(); |
| } |
| |
| class SharedFunctionInfoFinder { |
| public: |
| explicit SharedFunctionInfoFinder(int target_position) |
| : current_candidate_(nullptr), |
| current_candidate_closure_(nullptr), |
| current_start_position_(kNoSourcePosition), |
| target_position_(target_position) {} |
| |
| void NewCandidate(SharedFunctionInfo* shared, JSFunction* closure = nullptr) { |
| if (!shared->IsSubjectToDebugging()) return; |
| int start_position = shared->function_token_position(); |
| if (start_position == kNoSourcePosition) { |
| start_position = shared->start_position(); |
| } |
| |
| if (start_position > target_position_) return; |
| if (target_position_ > shared->end_position()) return; |
| |
| if (current_candidate_ != nullptr) { |
| if (current_start_position_ == start_position && |
| shared->end_position() == current_candidate_->end_position()) { |
| // If we already have a matching closure, do not throw it away. |
| if (current_candidate_closure_ != nullptr && closure == nullptr) return; |
| // If a top-level function contains only one function |
| // declaration the source for the top-level and the function |
| // is the same. In that case prefer the non top-level function. |
| if (!current_candidate_->is_toplevel() && shared->is_toplevel()) return; |
| } else if (start_position < current_start_position_ || |
| current_candidate_->end_position() < shared->end_position()) { |
| return; |
| } |
| } |
| |
| current_start_position_ = start_position; |
| current_candidate_ = shared; |
| current_candidate_closure_ = closure; |
| } |
| |
| SharedFunctionInfo* Result() { return current_candidate_; } |
| |
| JSFunction* ResultClosure() { return current_candidate_closure_; } |
| |
| private: |
| SharedFunctionInfo* current_candidate_; |
| JSFunction* current_candidate_closure_; |
| int current_start_position_; |
| int target_position_; |
| DisallowHeapAllocation no_gc_; |
| }; |
| |
| |
| // We need to find a SFI for a literal that may not yet have been compiled yet, |
| // and there may not be a JSFunction referencing it. Find the SFI closest to |
| // the given position, compile it to reveal possible inner SFIs and repeat. |
| // While we are at this, also ensure code with debug break slots so that we do |
| // not have to compile a SFI without JSFunction, which is paifu for those that |
| // cannot be compiled without context (need to find outer compilable SFI etc.) |
| Handle<Object> Debug::FindSharedFunctionInfoInScript(Handle<Script> script, |
| int position) { |
| for (int iteration = 0;; iteration++) { |
| // Go through all shared function infos associated with this script to |
| // find the inner most function containing this position. |
| // If there is no shared function info for this script at all, there is |
| // no point in looking for it by walking the heap. |
| |
| SharedFunctionInfo* shared; |
| { |
| SharedFunctionInfoFinder finder(position); |
| SharedFunctionInfo::ScriptIterator iterator(script); |
| for (SharedFunctionInfo* info = iterator.Next(); info != nullptr; |
| info = iterator.Next()) { |
| finder.NewCandidate(info); |
| } |
| shared = finder.Result(); |
| if (shared == nullptr) break; |
| // We found it if it's already compiled. |
| if (shared->is_compiled()) { |
| Handle<SharedFunctionInfo> shared_handle(shared); |
| // If the iteration count is larger than 1, we had to compile the outer |
| // function in order to create this shared function info. So there can |
| // be no JSFunction referencing it. We can anticipate creating a debug |
| // info while bypassing PrepareFunctionForBreakpoints. |
| if (iteration > 1) { |
| AllowHeapAllocation allow_before_return; |
| CreateBreakInfo(shared_handle); |
| } |
| return shared_handle; |
| } |
| } |
| // If not, compile to reveal inner functions. |
| HandleScope scope(isolate_); |
| // Code that cannot be compiled lazily are internal and not debuggable. |
| DCHECK(shared->allows_lazy_compilation()); |
| if (!Compiler::Compile(handle(shared), Compiler::CLEAR_EXCEPTION)) break; |
| } |
| return isolate_->factory()->undefined_value(); |
| } |
| |
| |
| // Ensures the debug information is present for shared. |
| bool Debug::EnsureBreakInfo(Handle<SharedFunctionInfo> shared) { |
| // Return if we already have the break info for shared. |
| if (shared->HasBreakInfo()) return true; |
| if (!shared->IsSubjectToDebugging()) return false; |
| if (!shared->is_compiled() && |
| !Compiler::Compile(shared, Compiler::CLEAR_EXCEPTION)) { |
| return false; |
| } |
| CreateBreakInfo(shared); |
| return true; |
| } |
| |
| void Debug::CreateBreakInfo(Handle<SharedFunctionInfo> shared) { |
| HandleScope scope(isolate_); |
| Handle<DebugInfo> debug_info = GetOrCreateDebugInfo(shared); |
| |
| // Initialize with break information. |
| |
| DCHECK(!debug_info->HasBreakInfo()); |
| |
| Factory* factory = isolate_->factory(); |
| Handle<FixedArray> break_points( |
| factory->NewFixedArray(DebugInfo::kEstimatedNofBreakPointsInFunction)); |
| |
| // Make a copy of the bytecode array if available. |
| Handle<Object> maybe_debug_bytecode_array = factory->undefined_value(); |
| if (shared->HasBytecodeArray()) { |
| Handle<BytecodeArray> original(shared->bytecode_array()); |
| maybe_debug_bytecode_array = factory->CopyBytecodeArray(original); |
| } |
| |
| debug_info->set_flags(debug_info->flags() | DebugInfo::kHasBreakInfo); |
| debug_info->set_debug_bytecode_array(*maybe_debug_bytecode_array); |
| debug_info->set_break_points(*break_points); |
| } |
| |
| Handle<DebugInfo> Debug::GetOrCreateDebugInfo( |
| Handle<SharedFunctionInfo> shared) { |
| if (shared->HasDebugInfo()) return handle(shared->GetDebugInfo()); |
| |
| // Create debug info and add it to the list. |
| Handle<DebugInfo> debug_info = isolate_->factory()->NewDebugInfo(shared); |
| DebugInfoListNode* node = new DebugInfoListNode(*debug_info); |
| node->set_next(debug_info_list_); |
| debug_info_list_ = node; |
| |
| return debug_info; |
| } |
| |
| void Debug::InstallCoverageInfo(Handle<SharedFunctionInfo> shared, |
| Handle<CoverageInfo> coverage_info) { |
| DCHECK(!coverage_info.is_null()); |
| |
| Handle<DebugInfo> debug_info = GetOrCreateDebugInfo(shared); |
| |
| DCHECK(!debug_info->HasCoverageInfo()); |
| |
| debug_info->set_flags(debug_info->flags() | DebugInfo::kHasCoverageInfo); |
| debug_info->set_coverage_info(*coverage_info); |
| } |
| |
| void Debug::RemoveAllCoverageInfos() { |
| ClearAllDebugInfos( |
| [=](Handle<DebugInfo> info) { return info->ClearCoverageInfo(); }); |
| } |
| |
| void Debug::FindDebugInfo(Handle<DebugInfo> debug_info, |
| DebugInfoListNode** prev, DebugInfoListNode** curr) { |
| HandleScope scope(isolate_); |
| *prev = nullptr; |
| *curr = debug_info_list_; |
| while (*curr != nullptr) { |
| if ((*curr)->debug_info().is_identical_to(debug_info)) return; |
| *prev = *curr; |
| *curr = (*curr)->next(); |
| } |
| |
| UNREACHABLE(); |
| } |
| |
| void Debug::ClearAllDebugInfos(DebugInfoClearFunction clear_function) { |
| DebugInfoListNode* prev = nullptr; |
| DebugInfoListNode* current = debug_info_list_; |
| while (current != nullptr) { |
| DebugInfoListNode* next = current->next(); |
| Handle<DebugInfo> debug_info = current->debug_info(); |
| if (clear_function(debug_info)) { |
| FreeDebugInfoListNode(prev, current); |
| current = next; |
| } else { |
| prev = current; |
| current = next; |
| } |
| } |
| } |
| |
| void Debug::RemoveBreakInfoAndMaybeFree(Handle<DebugInfo> debug_info) { |
| bool should_unlink = debug_info->ClearBreakInfo(); |
| if (should_unlink) { |
| DebugInfoListNode* prev; |
| DebugInfoListNode* node; |
| FindDebugInfo(debug_info, &prev, &node); |
| FreeDebugInfoListNode(prev, node); |
| } |
| } |
| |
| void Debug::FreeDebugInfoListNode(DebugInfoListNode* prev, |
| DebugInfoListNode* node) { |
| DCHECK(node->debug_info()->IsEmpty()); |
| |
| // Unlink from list. If prev is nullptr we are looking at the first element. |
| if (prev == nullptr) { |
| debug_info_list_ = node->next(); |
| } else { |
| prev->set_next(node->next()); |
| } |
| |
| // Pack debugger hints back into the SFI::debug_info field. |
| Handle<DebugInfo> debug_info(node->debug_info()); |
| debug_info->shared()->set_debug_info( |
| Smi::FromInt(debug_info->debugger_hints())); |
| |
| delete node; |
| } |
| |
| bool Debug::IsBreakAtReturn(JavaScriptFrame* frame) { |
| HandleScope scope(isolate_); |
| |
| // Get the executing function in which the debug break occurred. |
| Handle<SharedFunctionInfo> shared(frame->function()->shared()); |
| |
| // With no debug info there are no break points, so we can't be at a return. |
| if (!shared->HasBreakInfo()) return false; |
| |
| DCHECK(!frame->is_optimized()); |
| Handle<DebugInfo> debug_info(shared->GetDebugInfo()); |
| BreakLocation location = BreakLocation::FromFrame(debug_info, frame); |
| return location.IsReturn(); |
| } |
| |
| void Debug::ScheduleFrameRestart(StackFrame* frame) { |
| // Set a target FP for the FrameDropperTrampoline builtin to drop to once |
| // we return from the debugger. |
| DCHECK(frame->is_java_script()); |
| // Only reschedule to a frame further below a frame we already scheduled for. |
| if (frame->fp() <= thread_local_.restart_fp_) return; |
| // If the frame is optimized, trigger a deopt and jump into the |
| // FrameDropperTrampoline in the deoptimizer. |
| thread_local_.restart_fp_ = frame->fp(); |
| |
| // Reset break frame ID to the frame below the restarted frame. |
| StackTraceFrameIterator it(isolate_); |
| thread_local_.break_frame_id_ = StackFrame::NO_ID; |
| for (StackTraceFrameIterator it(isolate_); !it.done(); it.Advance()) { |
| if (it.frame()->fp() > thread_local_.restart_fp_) { |
| thread_local_.break_frame_id_ = it.frame()->id(); |
| return; |
| } |
| } |
| } |
| |
| |
| bool Debug::IsDebugGlobal(JSGlobalObject* global) { |
| return is_loaded() && global == debug_context()->global_object(); |
| } |
| |
| |
| Handle<FixedArray> Debug::GetLoadedScripts() { |
| isolate_->heap()->CollectAllGarbage(Heap::kFinalizeIncrementalMarkingMask, |
| GarbageCollectionReason::kDebugger); |
| Factory* factory = isolate_->factory(); |
| if (!factory->script_list()->IsWeakFixedArray()) { |
| return factory->empty_fixed_array(); |
| } |
| Handle<WeakFixedArray> array = |
| Handle<WeakFixedArray>::cast(factory->script_list()); |
| Handle<FixedArray> results = factory->NewFixedArray(array->Length()); |
| int length = 0; |
| { |
| Script::Iterator iterator(isolate_); |
| Script* script; |
| while ((script = iterator.Next()) != nullptr) { |
| if (script->HasValidSource()) results->set(length++, script); |
| } |
| } |
| results->Shrink(length); |
| return results; |
| } |
| |
| |
| MaybeHandle<Object> Debug::MakeExecutionState() { |
| // Create the execution state object. |
| Handle<Object> argv[] = { isolate_->factory()->NewNumberFromInt(break_id()) }; |
| return CallFunction("MakeExecutionState", arraysize(argv), argv); |
| } |
| |
| |
| MaybeHandle<Object> Debug::MakeBreakEvent(Handle<Object> break_points_hit) { |
| // Create the new break event object. |
| Handle<Object> argv[] = { isolate_->factory()->NewNumberFromInt(break_id()), |
| break_points_hit }; |
| return CallFunction("MakeBreakEvent", arraysize(argv), argv); |
| } |
| |
| |
| MaybeHandle<Object> Debug::MakeExceptionEvent(Handle<Object> exception, |
| bool uncaught, |
| Handle<Object> promise) { |
| // Create the new exception event object. |
| Handle<Object> argv[] = { isolate_->factory()->NewNumberFromInt(break_id()), |
| exception, |
| isolate_->factory()->ToBoolean(uncaught), |
| promise }; |
| return CallFunction("MakeExceptionEvent", arraysize(argv), argv); |
| } |
| |
| |
| MaybeHandle<Object> Debug::MakeCompileEvent(Handle<Script> script, |
| v8::DebugEvent type) { |
| // Create the compile event object. |
| Handle<Object> script_wrapper = Script::GetWrapper(script); |
| Handle<Object> argv[] = { script_wrapper, |
| isolate_->factory()->NewNumberFromInt(type) }; |
| return CallFunction("MakeCompileEvent", arraysize(argv), argv); |
| } |
| |
| MaybeHandle<Object> Debug::MakeAsyncTaskEvent( |
| v8::debug::PromiseDebugActionType type, int id) { |
| // Create the async task event object. |
| Handle<Object> argv[] = {Handle<Smi>(Smi::FromInt(type), isolate_), |
| Handle<Smi>(Smi::FromInt(id), isolate_)}; |
| return CallFunction("MakeAsyncTaskEvent", arraysize(argv), argv); |
| } |
| |
| |
| void Debug::OnThrow(Handle<Object> exception) { |
| if (in_debug_scope() || ignore_events()) return; |
| // Temporarily clear any scheduled_exception to allow evaluating |
| // JavaScript from the debug event handler. |
| HandleScope scope(isolate_); |
| Handle<Object> scheduled_exception; |
| if (isolate_->has_scheduled_exception()) { |
| scheduled_exception = handle(isolate_->scheduled_exception(), isolate_); |
| isolate_->clear_scheduled_exception(); |
| } |
| OnException(exception, isolate_->GetPromiseOnStackOnThrow()); |
| if (!scheduled_exception.is_null()) { |
| isolate_->thread_local_top()->scheduled_exception_ = *scheduled_exception; |
| } |
| PrepareStepOnThrow(); |
| } |
| |
| void Debug::OnPromiseReject(Handle<Object> promise, Handle<Object> value) { |
| if (in_debug_scope() || ignore_events()) return; |
| HandleScope scope(isolate_); |
| // Check whether the promise has been marked as having triggered a message. |
| Handle<Symbol> key = isolate_->factory()->promise_debug_marker_symbol(); |
| if (!promise->IsJSObject() || |
| JSReceiver::GetDataProperty(Handle<JSObject>::cast(promise), key) |
| ->IsUndefined(isolate_)) { |
| OnException(value, promise); |
| } |
| } |
| |
| namespace { |
| v8::Local<v8::Context> GetDebugEventContext(Isolate* isolate) { |
| Handle<Context> context = isolate->debug()->debugger_entry()->GetContext(); |
| // Isolate::context() may have been nullptr when "script collected" event |
| // occurred. |
| if (context.is_null()) return v8::Local<v8::Context>(); |
| Handle<Context> native_context(context->native_context()); |
| return v8::Utils::ToLocal(native_context); |
| } |
| } // anonymous namespace |
| |
| bool Debug::IsExceptionBlackboxed(bool uncaught) { |
| // Uncaught exception is blackboxed if all current frames are blackboxed, |
| // caught exception if top frame is blackboxed. |
| StackTraceFrameIterator it(isolate_); |
| while (!it.done() && it.is_wasm()) it.Advance(); |
| bool is_top_frame_blackboxed = |
| !it.done() ? IsFrameBlackboxed(it.javascript_frame()) : true; |
| if (!uncaught || !is_top_frame_blackboxed) return is_top_frame_blackboxed; |
| return AllFramesOnStackAreBlackboxed(); |
| } |
| |
| bool Debug::IsFrameBlackboxed(JavaScriptFrame* frame) { |
| HandleScope scope(isolate_); |
| std::vector<Handle<SharedFunctionInfo>> infos; |
| frame->GetFunctions(&infos); |
| for (const auto& info : infos) { |
| if (!IsBlackboxed(info)) return false; |
| } |
| return true; |
| } |
| |
| void Debug::OnException(Handle<Object> exception, Handle<Object> promise) { |
| // We cannot generate debug events when JS execution is disallowed. |
| // TODO(5530): Reenable debug events within DisallowJSScopes once relevant |
| // code (MakeExceptionEvent and ProcessDebugEvent) have been moved to C++. |
| if (!AllowJavascriptExecution::IsAllowed(isolate_)) return; |
| |
| Isolate::CatchType catch_type = isolate_->PredictExceptionCatcher(); |
| |
| // Don't notify listener of exceptions that are internal to a desugaring. |
| if (catch_type == Isolate::CAUGHT_BY_DESUGARING) return; |
| |
| bool uncaught = catch_type == Isolate::NOT_CAUGHT; |
| if (promise->IsJSObject()) { |
| Handle<JSObject> jspromise = Handle<JSObject>::cast(promise); |
| // Mark the promise as already having triggered a message. |
| Handle<Symbol> key = isolate_->factory()->promise_debug_marker_symbol(); |
| JSObject::SetProperty(jspromise, key, key, LanguageMode::kStrict).Assert(); |
| // Check whether the promise reject is considered an uncaught exception. |
| uncaught = !isolate_->PromiseHasUserDefinedRejectHandler(jspromise); |
| } |
| |
| if (!debug_delegate_) return; |
| |
| // Bail out if exception breaks are not active |
| if (uncaught) { |
| // Uncaught exceptions are reported by either flags. |
| if (!(break_on_uncaught_exception_ || break_on_exception_)) return; |
| } else { |
| // Caught exceptions are reported is activated. |
| if (!break_on_exception_) return; |
| } |
| |
| { |
| JavaScriptFrameIterator it(isolate_); |
| // Check whether the top frame is blackboxed or the break location is muted. |
| if (!it.done() && (IsMutedAtCurrentLocation(it.frame()) || |
| IsExceptionBlackboxed(uncaught))) { |
| return; |
| } |
| if (it.done()) return; // Do not trigger an event with an empty stack. |
| } |
| |
| DebugScope debug_scope(this); |
| if (debug_scope.failed()) return; |
| HandleScope scope(isolate_); |
| PostponeInterruptsScope postpone(isolate_); |
| DisableBreak no_recursive_break(this); |
| |
| // Create the execution state. |
| Handle<Object> exec_state; |
| // Bail out and don't call debugger if exception. |
| if (!MakeExecutionState().ToHandle(&exec_state)) return; |
| |
| debug_delegate_->ExceptionThrown( |
| GetDebugEventContext(isolate_), |
| v8::Utils::ToLocal(Handle<JSObject>::cast(exec_state)), |
| v8::Utils::ToLocal(exception), v8::Utils::ToLocal(promise), uncaught); |
| } |
| |
| void Debug::OnDebugBreak(Handle<FixedArray> break_points_hit) { |
| DCHECK(!break_points_hit.is_null()); |
| // The caller provided for DebugScope. |
| AssertDebugContext(); |
| // Bail out if there is no listener for this event |
| if (ignore_events()) return; |
| |
| #ifdef DEBUG |
| PrintBreakLocation(); |
| #endif // DEBUG |
| |
| if (!debug_delegate_) return; |
| HandleScope scope(isolate_); |
| PostponeInterruptsScope no_interrupts(isolate_); |
| DisableBreak no_recursive_break(this); |
| |
| // Create the execution state. |
| Handle<Object> exec_state; |
| // Bail out and don't call debugger if exception. |
| if (!MakeExecutionState().ToHandle(&exec_state)) return; |
| |
| std::vector<int> inspector_break_points_hit; |
| int inspector_break_points_count = 0; |
| // This array contains breakpoints installed using JS debug API. |
| for (int i = 0; i < break_points_hit->length(); ++i) { |
| Object* break_point = break_points_hit->get(i); |
| if (break_point->IsBreakPoint()) { |
| inspector_break_points_hit.push_back(BreakPoint::cast(break_point)->id()); |
| ++inspector_break_points_count; |
| } else { |
| break_points_hit->set(i - inspector_break_points_count, break_point); |
| } |
| } |
| int break_points_length = |
| break_points_hit->length() - inspector_break_points_count; |
| Handle<Object> break_points; |
| if (break_points_length) { |
| break_points_hit->Shrink(break_points_length); |
| break_points = isolate_->factory()->NewJSArrayWithElements( |
| break_points_hit, PACKED_ELEMENTS, break_points_length); |
| } else { |
| break_points = isolate_->factory()->undefined_value(); |
| } |
| |
| debug_delegate_->BreakProgramRequested( |
| GetDebugEventContext(isolate_), |
| v8::Utils::ToLocal(Handle<JSObject>::cast(exec_state)), |
| v8::Utils::ToLocal(break_points), inspector_break_points_hit); |
| } |
| |
| |
| void Debug::OnCompileError(Handle<Script> script) { |
| ProcessCompileEvent(v8::CompileError, script); |
| } |
| |
| |
| // Handle debugger actions when a new script is compiled. |
| void Debug::OnAfterCompile(Handle<Script> script) { |
| ProcessCompileEvent(v8::AfterCompile, script); |
| } |
| |
| namespace { |
| // In an async function, reuse the existing stack related to the outer |
| // Promise. Otherwise, e.g. in a direct call to then, save a new stack. |
| // Promises with multiple reactions with one or more of them being async |
| // functions will not get a good stack trace, as async functions require |
| // different stacks from direct Promise use, but we save and restore a |
| // stack once for all reactions. |
| // |
| // If this isn't a case of async function, we return false, otherwise |
| // we set the correct id and return true. |
| // |
| // TODO(littledan): Improve this case. |
| int GetReferenceAsyncTaskId(Isolate* isolate, Handle<JSPromise> promise) { |
| Handle<Symbol> handled_by_symbol = |
| isolate->factory()->promise_handled_by_symbol(); |
| Handle<Object> handled_by_promise = |
| JSObject::GetDataProperty(promise, handled_by_symbol); |
| if (!handled_by_promise->IsJSPromise()) { |
| return isolate->debug()->NextAsyncTaskId(promise); |
| } |
| Handle<JSPromise> handled_by_promise_js = |
| Handle<JSPromise>::cast(handled_by_promise); |
| Handle<Symbol> async_stack_id_symbol = |
| isolate->factory()->promise_async_stack_id_symbol(); |
| Handle<Object> async_task_id = |
| JSObject::GetDataProperty(handled_by_promise_js, async_stack_id_symbol); |
| if (!async_task_id->IsSmi()) { |
| return isolate->debug()->NextAsyncTaskId(promise); |
| } |
| return Handle<Smi>::cast(async_task_id)->value(); |
| } |
| } // namespace |
| |
| void Debug::RunPromiseHook(PromiseHookType hook_type, Handle<JSPromise> promise, |
| Handle<Object> parent) { |
| if (hook_type == PromiseHookType::kResolve) return; |
| if (in_debug_scope() || ignore_events()) return; |
| if (!debug_delegate_) return; |
| PostponeInterruptsScope no_interrupts(isolate_); |
| |
| int id = GetReferenceAsyncTaskId(isolate_, promise); |
| if (hook_type == PromiseHookType::kBefore) { |
| debug_delegate_->PromiseEventOccurred(debug::kDebugWillHandle, id, false); |
| } else if (hook_type == PromiseHookType::kAfter) { |
| debug_delegate_->PromiseEventOccurred(debug::kDebugDidHandle, id, false); |
| } else { |
| DCHECK(hook_type == PromiseHookType::kInit); |
| debug::PromiseDebugActionType type = debug::kDebugPromiseThen; |
| bool last_frame_was_promise_builtin = false; |
| JavaScriptFrameIterator it(isolate_); |
| while (!it.done()) { |
| std::vector<Handle<SharedFunctionInfo>> infos; |
| it.frame()->GetFunctions(&infos); |
| for (size_t i = 1; i <= infos.size(); ++i) { |
| Handle<SharedFunctionInfo> info = infos[infos.size() - i]; |
| if (info->IsUserJavaScript()) { |
| // We should not report PromiseThen and PromiseCatch which is called |
| // indirectly, e.g. Promise.all calls Promise.then internally. |
| if (type == debug::kDebugAsyncFunctionPromiseCreated || |
| last_frame_was_promise_builtin) { |
| debug_delegate_->PromiseEventOccurred(type, id, IsBlackboxed(info)); |
| } |
| return; |
| } |
| last_frame_was_promise_builtin = false; |
| Handle<Code> code(info->code()); |
| if (*code == *BUILTIN_CODE(isolate_, AsyncFunctionPromiseCreate)) { |
| type = debug::kDebugAsyncFunctionPromiseCreated; |
| last_frame_was_promise_builtin = true; |
| } else if (*code == *BUILTIN_CODE(isolate_, PromisePrototypeThen)) { |
| type = debug::kDebugPromiseThen; |
| last_frame_was_promise_builtin = true; |
| } else if (*code == *BUILTIN_CODE(isolate_, PromisePrototypeCatch)) { |
| type = debug::kDebugPromiseCatch; |
| last_frame_was_promise_builtin = true; |
| } else if (*code == *BUILTIN_CODE(isolate_, PromisePrototypeFinally)) { |
| type = debug::kDebugPromiseFinally; |
| last_frame_was_promise_builtin = true; |
| } |
| } |
| it.Advance(); |
| } |
| } |
| } |
| |
| int Debug::NextAsyncTaskId(Handle<JSObject> promise) { |
| LookupIterator it(promise, isolate_->factory()->promise_async_id_symbol()); |
| Maybe<bool> maybe = JSReceiver::HasProperty(&it); |
| if (maybe.ToChecked()) { |
| MaybeHandle<Object> result = Object::GetProperty(&it); |
| return Handle<Smi>::cast(result.ToHandleChecked())->value(); |
| } |
| Handle<Smi> async_id = |
| handle(Smi::FromInt(++thread_local_.async_task_count_), isolate_); |
| Object::SetProperty(&it, async_id, LanguageMode::kSloppy, |
| Object::MAY_BE_STORE_FROM_KEYED) |
| .ToChecked(); |
| return async_id->value(); |
| } |
| |
| namespace { |
| debug::Location GetDebugLocation(Handle<Script> script, int source_position) { |
| Script::PositionInfo info; |
| Script::GetPositionInfo(script, source_position, &info, Script::WITH_OFFSET); |
| // V8 provides ScriptCompiler::CompileFunctionInContext method which takes |
| // expression and compile it as anonymous function like (function() .. |
| // expression ..). To produce correct locations for stmts inside of this |
| // expression V8 compile this function with negative offset. Instead of stmt |
| // position blackboxing use function start position which is negative in |
| // described case. |
| return debug::Location(std::max(info.line, 0), std::max(info.column, 0)); |
| } |
| } // namespace |
| |
| bool Debug::IsBlackboxed(Handle<SharedFunctionInfo> shared) { |
| if (!debug_delegate_) return !shared->IsSubjectToDebugging(); |
| if (!shared->computed_debug_is_blackboxed()) { |
| bool is_blackboxed = |
| !shared->IsSubjectToDebugging() || !shared->script()->IsScript(); |
| if (!is_blackboxed) { |
| SuppressDebug while_processing(this); |
| HandleScope handle_scope(isolate_); |
| PostponeInterruptsScope no_interrupts(isolate_); |
| DisableBreak no_recursive_break(this); |
| DCHECK(shared->script()->IsScript()); |
| Handle<Script> script(Script::cast(shared->script())); |
| DCHECK(script->IsUserJavaScript()); |
| debug::Location start = |
| GetDebugLocation(script, shared->start_position()); |
| debug::Location end = GetDebugLocation(script, shared->end_position()); |
| is_blackboxed = debug_delegate_->IsFunctionBlackboxed( |
| ToApiHandle<debug::Script>(script), start, end); |
| } |
| shared->set_debug_is_blackboxed(is_blackboxed); |
| shared->set_computed_debug_is_blackboxed(true); |
| } |
| return shared->debug_is_blackboxed(); |
| } |
| |
| bool Debug::AllFramesOnStackAreBlackboxed() { |
| HandleScope scope(isolate_); |
| for (StackTraceFrameIterator it(isolate_); !it.done(); it.Advance()) { |
| if (!IsFrameBlackboxed(it.javascript_frame())) return false; |
| } |
| return true; |
| } |
| |
| bool Debug::SetScriptSource(Handle<Script> script, Handle<String> source, |
| bool preview, bool* stack_changed) { |
| DebugScope debug_scope(this); |
| set_live_edit_enabled(true); |
| Handle<Object> script_wrapper = Script::GetWrapper(script); |
| Handle<Object> argv[] = {script_wrapper, source, |
| isolate_->factory()->ToBoolean(preview), |
| isolate_->factory()->NewJSArray(0)}; |
| Handle<Object> result; |
| if (!CallFunction("SetScriptSource", arraysize(argv), argv, false) |
| .ToHandle(&result)) { |
| isolate_->OptionalRescheduleException(false); |
| set_live_edit_enabled(false); |
| return false; |
| } |
| set_live_edit_enabled(false); |
| Handle<Object> stack_changed_value = |
| JSReceiver::GetProperty(isolate_, Handle<JSObject>::cast(result), |
| "stack_modified") |
| .ToHandleChecked(); |
| *stack_changed = stack_changed_value->IsTrue(isolate_); |
| return true; |
| } |
| |
| void Debug::ProcessCompileEvent(v8::DebugEvent event, Handle<Script> script) { |
| // Attach the correct debug id to the script. The debug id is used by the |
| // inspector to filter scripts by native context. |
| script->set_context_data(isolate_->native_context()->debug_context_id()); |
| if (ignore_events()) return; |
| if (!script->IsUserJavaScript() && script->type() != i::Script::TYPE_WASM) { |
| return; |
| } |
| if (!debug_delegate_) return; |
| SuppressDebug while_processing(this); |
| DebugScope debug_scope(this); |
| if (debug_scope.failed()) return; |
| HandleScope scope(isolate_); |
| PostponeInterruptsScope postpone(isolate_); |
| DisableBreak no_recursive_break(this); |
| AllowJavascriptExecution allow_script(isolate_); |
| debug_delegate_->ScriptCompiled(ToApiHandle<debug::Script>(script), |
| live_edit_enabled(), |
| event != v8::AfterCompile); |
| } |
| |
| |
| Handle<Context> Debug::GetDebugContext() { |
| if (!is_loaded()) return Handle<Context>(); |
| DebugScope debug_scope(this); |
| if (debug_scope.failed()) return Handle<Context>(); |
| // The global handle may be destroyed soon after. Return it reboxed. |
| return handle(*debug_context(), isolate_); |
| } |
| |
| int Debug::CurrentFrameCount() { |
| StackTraceFrameIterator it(isolate_); |
| if (break_frame_id() != StackFrame::NO_ID) { |
| // Skip to break frame. |
| DCHECK(in_debug_scope()); |
| while (!it.done() && it.frame()->id() != break_frame_id()) it.Advance(); |
| } |
| int counter = 0; |
| while (!it.done()) { |
| if (it.frame()->is_optimized()) { |
| std::vector<SharedFunctionInfo*> infos; |
| OptimizedFrame::cast(it.frame())->GetFunctions(&infos); |
| counter += infos.size(); |
| } else { |
| counter++; |
| } |
| it.Advance(); |
| } |
| return counter; |
| } |
| |
| void Debug::SetDebugDelegate(debug::DebugDelegate* delegate, |
| bool pass_ownership) { |
| RemoveDebugDelegate(); |
| debug_delegate_ = delegate; |
| owns_debug_delegate_ = pass_ownership; |
| UpdateState(); |
| } |
| |
| void Debug::RemoveDebugDelegate() { |
| if (debug_delegate_ == nullptr) return; |
| if (owns_debug_delegate_) { |
| owns_debug_delegate_ = false; |
| delete debug_delegate_; |
| } |
| debug_delegate_ = nullptr; |
| } |
| |
| void Debug::UpdateState() { |
| bool is_active = debug_delegate_ != nullptr; |
| if (is_active || in_debug_scope()) { |
| // Note that the debug context could have already been loaded to |
| // bootstrap test cases. |
| isolate_->compilation_cache()->Disable(); |
| is_active = Load(); |
| } else if (is_loaded()) { |
| isolate_->compilation_cache()->Enable(); |
| Unload(); |
| } |
| is_active_ = is_active; |
| isolate_->DebugStateUpdated(); |
| } |
| |
| void Debug::UpdateHookOnFunctionCall() { |
| STATIC_ASSERT(LastStepAction == StepIn); |
| hook_on_function_call_ = thread_local_.last_step_action_ == StepIn || |
| isolate_->needs_side_effect_check(); |
| } |
| |
| MaybeHandle<Object> Debug::Call(Handle<Object> fun, Handle<Object> data) { |
| AllowJavascriptExecutionDebugOnly allow_script(isolate_); |
| DebugScope debug_scope(this); |
| if (debug_scope.failed()) return isolate_->factory()->undefined_value(); |
| |
| // Create the execution state. |
| Handle<Object> exec_state; |
| if (!MakeExecutionState().ToHandle(&exec_state)) { |
| return isolate_->factory()->undefined_value(); |
| } |
| |
| Handle<Object> argv[] = { exec_state, data }; |
| return Execution::Call( |
| isolate_, |
| fun, |
| Handle<Object>(debug_context()->global_proxy(), isolate_), |
| arraysize(argv), |
| argv); |
| } |
| |
| void Debug::HandleDebugBreak(IgnoreBreakMode ignore_break_mode) { |
| // Initialize LiveEdit. |
| LiveEdit::InitializeThreadLocal(this); |
| // Ignore debug break during bootstrapping. |
| if (isolate_->bootstrapper()->IsActive()) return; |
| // Just continue if breaks are disabled. |
| if (break_disabled()) return; |
| // Ignore debug break if debugger is not active. |
| if (!is_active()) return; |
| |
| StackLimitCheck check(isolate_); |
| if (check.HasOverflowed()) return; |
| |
| { JavaScriptFrameIterator it(isolate_); |
| DCHECK(!it.done()); |
| Object* fun = it.frame()->function(); |
| if (fun && fun->IsJSFunction()) { |
| HandleScope scope(isolate_); |
| Handle<JSFunction> function(JSFunction::cast(fun), isolate_); |
| // Don't stop in builtin and blackboxed functions. |
| Handle<SharedFunctionInfo> shared(function->shared(), isolate_); |
| bool ignore_break = ignore_break_mode == kIgnoreIfTopFrameBlackboxed |
| ? IsBlackboxed(shared) |
| : AllFramesOnStackAreBlackboxed(); |
| if (ignore_break) { |
| // Inspector uses pause on next statement for asynchronous breakpoints. |
| // When breakpoint is fired we try to break on first not blackboxed |
| // statement. To achieve this goal we need to deoptimize current |
| // function and don't clear requested DebugBreak even if it's blackboxed |
| // to be able to break on not blackboxed function call. |
| // TODO(yangguo): introduce break_on_function_entry since current |
| // implementation is slow. |
| if (isolate_->stack_guard()->CheckDebugBreak()) { |
| Deoptimizer::DeoptimizeFunction(*function); |
| } |
| return; |
| } |
| JSGlobalObject* global = function->context()->global_object(); |
| // Don't stop in debugger functions. |
| if (IsDebugGlobal(global)) return; |
| // Don't stop if the break location is muted. |
| if (IsMutedAtCurrentLocation(it.frame())) return; |
| } |
| } |
| |
| isolate_->stack_guard()->ClearDebugBreak(); |
| |
| // Clear stepping to avoid duplicate breaks. |
| ClearStepping(); |
| |
| HandleScope scope(isolate_); |
| DebugScope debug_scope(this); |
| if (debug_scope.failed()) return; |
| |
| OnDebugBreak(isolate_->factory()->empty_fixed_array()); |
| } |
| |
| #ifdef DEBUG |
| void Debug::PrintBreakLocation() { |
| if (!FLAG_print_break_location) return; |
| HandleScope scope(isolate_); |
| StackTraceFrameIterator iterator(isolate_); |
| if (iterator.done()) return; |
| StandardFrame* frame = iterator.frame(); |
| FrameSummary summary = FrameSummary::GetTop(frame); |
| int source_position = summary.SourcePosition(); |
| Handle<Object> script_obj = summary.script(); |
| PrintF("[debug] break in function '"); |
| summary.FunctionName()->PrintOn(stdout); |
| PrintF("'.\n"); |
| if (script_obj->IsScript()) { |
| Handle<Script> script = Handle<Script>::cast(script_obj); |
| Handle<String> source(String::cast(script->source())); |
| Script::InitLineEnds(script); |
| int line = |
| Script::GetLineNumber(script, source_position) - script->line_offset(); |
| int column = Script::GetColumnNumber(script, source_position) - |
| (line == 0 ? script->column_offset() : 0); |
| Handle<FixedArray> line_ends(FixedArray::cast(script->line_ends())); |
| int line_start = line == 0 ? 0 : Smi::ToInt(line_ends->get(line - 1)) + 1; |
| int line_end = Smi::ToInt(line_ends->get(line)); |
| DisallowHeapAllocation no_gc; |
| String::FlatContent content = source->GetFlatContent(); |
| if (content.IsOneByte()) { |
| PrintF("[debug] %.*s\n", line_end - line_start, |
| content.ToOneByteVector().start() + line_start); |
| PrintF("[debug] "); |
| for (int i = 0; i < column; i++) PrintF(" "); |
| PrintF("^\n"); |
| } else { |
| PrintF("[debug] at line %d column %d\n", line, column); |
| } |
| } |
| } |
| #endif // DEBUG |
| |
| DebugScope::DebugScope(Debug* debug) |
| : debug_(debug), |
| prev_(debug->debugger_entry()), |
| save_(debug_->isolate_), |
| no_termination_exceptons_(debug_->isolate_, |
| StackGuard::TERMINATE_EXECUTION) { |
| // Link recursive debugger entry. |
| base::Relaxed_Store(&debug_->thread_local_.current_debug_scope_, |
| reinterpret_cast<base::AtomicWord>(this)); |
| |
| // Store the previous break id, frame id and return value. |
| break_id_ = debug_->break_id(); |
| break_frame_id_ = debug_->break_frame_id(); |
| |
| // Create the new break info. If there is no proper frames there is no break |
| // frame id. |
| StackTraceFrameIterator it(isolate()); |
| bool has_frames = !it.done(); |
| debug_->thread_local_.break_frame_id_ = |
| has_frames ? it.frame()->id() : StackFrame::NO_ID; |
| debug_->SetNextBreakId(); |
| |
| debug_->UpdateState(); |
| // Make sure that debugger is loaded and enter the debugger context. |
| // The previous context is kept in save_. |
| failed_ = !debug_->is_loaded(); |
| if (!failed_) isolate()->set_context(*debug->debug_context()); |
| } |
| |
| |
| DebugScope::~DebugScope() { |
| // Leaving this debugger entry. |
| base::Relaxed_Store(&debug_->thread_local_.current_debug_scope_, |
| reinterpret_cast<base::AtomicWord>(prev_)); |
| |
| // Restore to the previous break state. |
| debug_->thread_local_.break_frame_id_ = break_frame_id_; |
| debug_->thread_local_.break_id_ = break_id_; |
| |
| debug_->UpdateState(); |
| } |
| |
| ReturnValueScope::ReturnValueScope(Debug* debug) : debug_(debug) { |
| return_value_ = debug_->return_value_handle(); |
| } |
| |
| ReturnValueScope::~ReturnValueScope() { |
| debug_->set_return_value(*return_value_); |
| } |
| |
| bool Debug::PerformSideEffectCheck(Handle<JSFunction> function) { |
| DCHECK(isolate_->needs_side_effect_check()); |
| DisallowJavascriptExecution no_js(isolate_); |
| if (!function->is_compiled() && |
| !Compiler::Compile(function, Compiler::KEEP_EXCEPTION)) { |
| return false; |
| } |
| Deoptimizer::DeoptimizeFunction(*function); |
| if (!SharedFunctionInfo::HasNoSideEffect(handle(function->shared()))) { |
| if (FLAG_trace_side_effect_free_debug_evaluate) { |
| PrintF("[debug-evaluate] Function %s failed side effect check.\n", |
| function->shared()->DebugName()->ToCString().get()); |
| } |
| side_effect_check_failed_ = true; |
| // Throw an uncatchable termination exception. |
| isolate_->TerminateExecution(); |
| return false; |
| } |
| return true; |
| } |
| |
| bool Debug::PerformSideEffectCheckForCallback(Address function) { |
| DCHECK(isolate_->needs_side_effect_check()); |
| if (DebugEvaluate::CallbackHasNoSideEffect(function)) return true; |
| side_effect_check_failed_ = true; |
| // Throw an uncatchable termination exception. |
| isolate_->TerminateExecution(); |
| isolate_->OptionalRescheduleException(false); |
| return false; |
| } |
| |
| void LegacyDebugDelegate::PromiseEventOccurred( |
| v8::debug::PromiseDebugActionType type, int id, bool is_blackboxed) { |
| DebugScope debug_scope(isolate_->debug()); |
| if (debug_scope.failed()) return; |
| HandleScope scope(isolate_); |
| Handle<Object> event_data; |
| if (isolate_->debug()->MakeAsyncTaskEvent(type, id).ToHandle(&event_data)) { |
| ProcessDebugEvent(v8::AsyncTaskEvent, Handle<JSObject>::cast(event_data)); |
| } |
| } |
| |
| void LegacyDebugDelegate::ScriptCompiled(v8::Local<v8::debug::Script> script, |
| bool is_live_edited, |
| bool is_compile_error) { |
| Handle<Object> event_data; |
| v8::DebugEvent event = is_compile_error ? v8::CompileError : v8::AfterCompile; |
| if (isolate_->debug() |
| ->MakeCompileEvent(v8::Utils::OpenHandle(*script), event) |
| .ToHandle(&event_data)) { |
| ProcessDebugEvent(event, Handle<JSObject>::cast(event_data)); |
| } |
| } |
| |
| void LegacyDebugDelegate::BreakProgramRequested( |
| v8::Local<v8::Context> paused_context, v8::Local<v8::Object> exec_state, |
| v8::Local<v8::Value> break_points_hit, |
| const std::vector<debug::BreakpointId>&) { |
| Handle<Object> event_data; |
| if (isolate_->debug() |
| ->MakeBreakEvent(v8::Utils::OpenHandle(*break_points_hit)) |
| .ToHandle(&event_data)) { |
| ProcessDebugEvent( |
| v8::Break, Handle<JSObject>::cast(event_data), |
| Handle<JSObject>::cast(v8::Utils::OpenHandle(*exec_state))); |
| } |
| } |
| |
| void LegacyDebugDelegate::ExceptionThrown(v8::Local<v8::Context> paused_context, |
| v8::Local<v8::Object> exec_state, |
| v8::Local<v8::Value> exception, |
| v8::Local<v8::Value> promise, |
| bool is_uncaught) { |
| Handle<Object> event_data; |
| if (isolate_->debug() |
| ->MakeExceptionEvent(v8::Utils::OpenHandle(*exception), is_uncaught, |
| v8::Utils::OpenHandle(*promise)) |
| .ToHandle(&event_data)) { |
| ProcessDebugEvent( |
| v8::Exception, Handle<JSObject>::cast(event_data), |
| Handle<JSObject>::cast(v8::Utils::OpenHandle(*exec_state))); |
| } |
| } |
| |
| void LegacyDebugDelegate::ProcessDebugEvent(v8::DebugEvent event, |
| Handle<JSObject> event_data) { |
| Handle<Object> exec_state; |
| if (isolate_->debug()->MakeExecutionState().ToHandle(&exec_state)) { |
| ProcessDebugEvent(event, event_data, Handle<JSObject>::cast(exec_state)); |
| } |
| } |
| |
| JavaScriptDebugDelegate::JavaScriptDebugDelegate(Isolate* isolate, |
| Handle<JSFunction> listener, |
| Handle<Object> data) |
| : LegacyDebugDelegate(isolate) { |
| GlobalHandles* global_handles = isolate->global_handles(); |
| listener_ = global_handles->Create(*listener); |
| data_ = global_handles->Create(*data); |
| } |
| |
| JavaScriptDebugDelegate::~JavaScriptDebugDelegate() { |
| GlobalHandles::Destroy(Handle<Object>::cast(listener_).location()); |
| GlobalHandles::Destroy(data_.location()); |
| } |
| |
| void JavaScriptDebugDelegate::ProcessDebugEvent(v8::DebugEvent event, |
| Handle<JSObject> event_data, |
| Handle<JSObject> exec_state) { |
| AllowJavascriptExecutionDebugOnly allow_script(isolate_); |
| Handle<Object> argv[] = {Handle<Object>(Smi::FromInt(event), isolate_), |
| exec_state, event_data, data_}; |
| Handle<JSReceiver> global = isolate_->global_proxy(); |
| // Listener must not throw. |
| Execution::Call(isolate_, listener_, global, arraysize(argv), argv) |
| .ToHandleChecked(); |
| } |
| |
| NativeDebugDelegate::NativeDebugDelegate(Isolate* isolate, |
| v8::Debug::EventCallback callback, |
| Handle<Object> data) |
| : LegacyDebugDelegate(isolate), callback_(callback) { |
| data_ = isolate->global_handles()->Create(*data); |
| } |
| |
| NativeDebugDelegate::~NativeDebugDelegate() { |
| GlobalHandles::Destroy(data_.location()); |
| } |
| |
| NativeDebugDelegate::EventDetails::EventDetails(DebugEvent event, |
| Handle<JSObject> exec_state, |
| Handle<JSObject> event_data, |
| Handle<Object> callback_data) |
| : event_(event), |
| exec_state_(exec_state), |
| event_data_(event_data), |
| callback_data_(callback_data) {} |
| |
| DebugEvent NativeDebugDelegate::EventDetails::GetEvent() const { |
| return event_; |
| } |
| |
| v8::Local<v8::Object> NativeDebugDelegate::EventDetails::GetExecutionState() |
| const { |
| return v8::Utils::ToLocal(exec_state_); |
| } |
| |
| v8::Local<v8::Object> NativeDebugDelegate::EventDetails::GetEventData() const { |
| return v8::Utils::ToLocal(event_data_); |
| } |
| |
| v8::Local<v8::Context> NativeDebugDelegate::EventDetails::GetEventContext() |
| const { |
| return GetDebugEventContext(exec_state_->GetIsolate()); |
| } |
| |
| v8::Local<v8::Value> NativeDebugDelegate::EventDetails::GetCallbackData() |
| const { |
| return v8::Utils::ToLocal(callback_data_); |
| } |
| |
| v8::Isolate* NativeDebugDelegate::EventDetails::GetIsolate() const { |
| return reinterpret_cast<v8::Isolate*>(exec_state_->GetIsolate()); |
| } |
| |
| void NativeDebugDelegate::ProcessDebugEvent(v8::DebugEvent event, |
| Handle<JSObject> event_data, |
| Handle<JSObject> exec_state) { |
| EventDetails event_details(event, exec_state, event_data, data_); |
| Isolate* isolate = isolate_; |
| callback_(event_details); |
| CHECK(!isolate->has_scheduled_exception()); |
| } |
| |
| NoSideEffectScope::~NoSideEffectScope() { |
| if (isolate_->needs_side_effect_check() && |
| isolate_->debug()->side_effect_check_failed_) { |
| DCHECK(isolate_->has_pending_exception()); |
| DCHECK_EQ(isolate_->heap()->termination_exception(), |
| isolate_->pending_exception()); |
| // Convert the termination exception into a regular exception. |
| isolate_->CancelTerminateExecution(); |
| isolate_->Throw(*isolate_->factory()->NewEvalError( |
| MessageTemplate::kNoSideEffectDebugEvaluate)); |
| } |
| isolate_->set_needs_side_effect_check(old_needs_side_effect_check_); |
| isolate_->debug()->UpdateHookOnFunctionCall(); |
| isolate_->debug()->side_effect_check_failed_ = false; |
| } |
| |
| } // namespace internal |
| } // namespace v8 |