From be5bb26e3827aad0fd072c5fb857a8ea854fa1f0 Mon Sep 17 00:00:00 2001 From: "peter.rybin@gmail.com" Date: Tue, 6 Apr 2010 17:58:28 +0000 Subject: [PATCH] LiveEdit: implement frame dropping Review URL: http://codereview.chromium.org/1118007 git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@4351 ce2b1a6d-e550-0410-aec6-3dcde31c8c00 --- src/arm/debug-arm.cc | 15 ++ src/builtins.cc | 8 + src/builtins.h | 4 +- src/debug-debugger.js | 7 +- src/debug.cc | 17 +- src/debug.h | 11 ++ src/frames.cc | 53 ++++++ src/frames.h | 20 ++- src/ia32/debug-ia32.cc | 50 ++++++ src/liveedit-debugger.js | 40 ++++- src/liveedit.cc | 267 +++++++++++++++++++++++++++++ src/liveedit.h | 15 +- src/mips/debug-mips.cc | 16 ++ src/runtime.cc | 60 ++++--- src/runtime.h | 5 +- src/x64/debug-x64.cc | 15 ++ test/mjsunit/debug-liveedit-check-stack.js | 131 ++++++++++---- test/mjsunit/mjsunit.status | 4 + 18 files changed, 658 insertions(+), 80 deletions(-) diff --git a/src/arm/debug-arm.cc b/src/arm/debug-arm.cc index bc81b19..1aca7ce 100644 --- a/src/arm/debug-arm.cc +++ b/src/arm/debug-arm.cc @@ -216,8 +216,23 @@ void Debug::GenerateStubNoRegistersDebugBreak(MacroAssembler* masm) { } +void Debug::GeneratePlainReturnLiveEdit(MacroAssembler* masm) { + masm->Abort("LiveEdit frame dropping is not supported on arm"); +} + +void Debug::GenerateFrameDropperLiveEdit(MacroAssembler* masm) { + masm->Abort("LiveEdit frame dropping is not supported on arm"); +} + #undef __ + +void Debug::SetUpFrameDropperFrame(StackFrame* bottom_js_frame, + Handle code) { + UNREACHABLE(); +} +const int Debug::kFrameDropperFrameSize = -1; + #endif // ENABLE_DEBUGGER_SUPPORT } } // namespace v8::internal diff --git a/src/builtins.cc b/src/builtins.cc index 69f6413..767820a 100644 --- a/src/builtins.cc +++ b/src/builtins.cc @@ -1392,6 +1392,14 @@ static void Generate_Return_DebugBreak(MacroAssembler* masm) { static void Generate_StubNoRegisters_DebugBreak(MacroAssembler* masm) { Debug::GenerateStubNoRegistersDebugBreak(masm); } + +static void Generate_PlainReturn_LiveEdit(MacroAssembler* masm) { + Debug::GeneratePlainReturnLiveEdit(masm); +} + +static void Generate_FrameDropper_LiveEdit(MacroAssembler* masm) { + Debug::GenerateFrameDropperLiveEdit(masm); +} #endif Object* Builtins::builtins_[builtin_count] = { NULL, }; diff --git a/src/builtins.h b/src/builtins.h index 1378c54..ccb6c0c 100644 --- a/src/builtins.h +++ b/src/builtins.h @@ -126,7 +126,9 @@ enum BuiltinExtraArguments { V(LoadIC_DebugBreak, LOAD_IC, DEBUG_BREAK) \ V(KeyedLoadIC_DebugBreak, KEYED_LOAD_IC, DEBUG_BREAK) \ V(StoreIC_DebugBreak, STORE_IC, DEBUG_BREAK) \ - V(KeyedStoreIC_DebugBreak, KEYED_STORE_IC, DEBUG_BREAK) + V(KeyedStoreIC_DebugBreak, KEYED_STORE_IC, DEBUG_BREAK) \ + V(PlainReturn_LiveEdit, BUILTIN, DEBUG_BREAK) \ + V(FrameDropper_LiveEdit, BUILTIN, DEBUG_BREAK) #else #define BUILTIN_LIST_DEBUG_A(V) #endif diff --git a/src/debug-debugger.js b/src/debug-debugger.js index cf949fc..d9e0ecd 100644 --- a/src/debug-debugger.js +++ b/src/debug-debugger.js @@ -474,6 +474,11 @@ Debug.disassembleConstructor = function(f) { return %DebugDisassembleConstructor(f); }; +Debug.ExecuteInDebugContext = function(f, without_debugger) { + if (!IS_FUNCTION(f)) throw new Error('Parameters have wrong types.'); + return %ExecuteInDebugContext(f, !!without_debugger); +}; + Debug.sourcePosition = function(f) { if (!IS_FUNCTION(f)) throw new Error('Parameters have wrong types.'); return %FunctionGetScriptSourcePosition(f); @@ -2010,7 +2015,7 @@ DebugCommandProcessor.prototype.changeLiveRequest_ = function(request, response) if (e instanceof Debug.LiveEditChangeScript.Failure) { // Let's treat it as a "success" so that body with change_log will be // sent back. "change_log" will have "failure" field set. - change_log.push( { failure: true } ); + change_log.push( { failure: true, message: e.toString() } ); } else { throw e; } diff --git a/src/debug.cc b/src/debug.cc index 4dce4cf..bac05a0 100644 --- a/src/debug.cc +++ b/src/debug.cc @@ -814,6 +814,8 @@ Object* Debug::Break(Arguments args) { HandleScope scope; ASSERT(args.length() == 0); + thread_local_.frames_are_dropped_ = false; + // Get the top-most JavaScript frame. JavaScriptFrameIterator it; JavaScriptFrame* frame = it.frame(); @@ -890,8 +892,13 @@ Object* Debug::Break(Arguments args) { PrepareStep(step_action, step_count); } - // Install jump to the call address which was overwritten. - SetAfterBreakTarget(frame); + if (thread_local_.frames_are_dropped_) { + // We must have been calling IC stub. Do not return there anymore. + Code* plain_return = Builtins::builtin(Builtins::PlainReturn_LiveEdit); + thread_local_.after_break_target_ = plain_return->entry(); + } else { + SetAfterBreakTarget(frame); + } return Heap::undefined_value(); } @@ -1655,6 +1662,12 @@ void Debug::SetAfterBreakTarget(JavaScriptFrame* frame) { } +void Debug::FramesHaveBeenDropped(StackFrame::Id new_break_frame_id) { + thread_local_.frames_are_dropped_ = true; + thread_local_.break_frame_id_ = new_break_frame_id; +} + + bool Debug::IsDebugGlobal(GlobalObject* global) { return IsLoaded() && global == Debug::debug_context()->global(); } diff --git a/src/debug.h b/src/debug.h index 546512b..a0e8b81 100644 --- a/src/debug.h +++ b/src/debug.h @@ -377,10 +377,18 @@ class Debug { static void GenerateConstructCallDebugBreak(MacroAssembler* masm); static void GenerateReturnDebugBreak(MacroAssembler* masm); static void GenerateStubNoRegistersDebugBreak(MacroAssembler* masm); + static void GeneratePlainReturnLiveEdit(MacroAssembler* masm); + static void GenerateFrameDropperLiveEdit(MacroAssembler* masm); // Called from stub-cache.cc. static void GenerateCallICDebugBreak(MacroAssembler* masm); + static void FramesHaveBeenDropped(StackFrame::Id new_break_frame_id); + + static void SetUpFrameDropperFrame(StackFrame* bottom_js_frame, + Handle code); + static const int kFrameDropperFrameSize; + private: static bool CompileDebuggerScript(int index); static void ClearOneShot(); @@ -446,6 +454,9 @@ class Debug { // Storage location for jump when exiting debug break calls. Address after_break_target_; + // Indicates that LiveEdit has patched the stack. + bool frames_are_dropped_; + // Top debugger entry. EnterDebugger* debugger_entry_; diff --git a/src/frames.cc b/src/frames.cc index 5e81a54..9cf83c9 100644 --- a/src/frames.cc +++ b/src/frames.cc @@ -382,6 +382,12 @@ void EntryFrame::ComputeCallerState(State* state) const { } +void EntryFrame::SetCallerFp(Address caller_fp) { + const int offset = EntryFrameConstants::kCallerFPOffset; + Memory::Address_at(this->fp() + offset) = caller_fp; +} + + StackFrame::Type EntryFrame::GetCallerState(State* state) const { const int offset = EntryFrameConstants::kCallerFPOffset; Address fp = Memory::Address_at(this->fp() + offset); @@ -414,6 +420,11 @@ void ExitFrame::ComputeCallerState(State* state) const { } +void ExitFrame::SetCallerFp(Address caller_fp) { + Memory::Address_at(fp() + ExitFrameConstants::kCallerFPOffset) = caller_fp; +} + + Address ExitFrame::GetCallerStackPointer() const { return fp() + ExitFrameConstants::kCallerSPDisplacement; } @@ -443,6 +454,12 @@ void StandardFrame::ComputeCallerState(State* state) const { } +void StandardFrame::SetCallerFp(Address caller_fp) { + Memory::Address_at(fp() + StandardFrameConstants::kCallerFPOffset) = + caller_fp; +} + + bool StandardFrame::IsExpressionInsideHandler(int n) const { Address address = GetExpressionAddress(n); for (StackHandlerIterator it(this, top_handler()); !it.done(); it.Advance()) { @@ -767,4 +784,40 @@ int JSCallerSavedCode(int n) { } +#define DEFINE_WRAPPER(type, field) \ +class field##_Wrapper : public ZoneObject { \ + public: /* NOLINT */ \ + field##_Wrapper(const field& original) : frame_(original) { \ + } \ + field frame_; \ +}; +STACK_FRAME_TYPE_LIST(DEFINE_WRAPPER) +#undef DEFINE_WRAPPER + +static StackFrame* AllocateFrameCopy(StackFrame* frame) { +#define FRAME_TYPE_CASE(type, field) \ + case StackFrame::type: { \ + field##_Wrapper* wrapper = \ + new field##_Wrapper(*(reinterpret_cast(frame))); \ + return &wrapper->frame_; \ + } + + switch (frame->type()) { + STACK_FRAME_TYPE_LIST(FRAME_TYPE_CASE) + default: UNREACHABLE(); + } +#undef FRAME_TYPE_CASE + return NULL; +} + +Vector CreateStackMap() { + ZoneList list(10); + for (StackFrameIterator it; !it.done(); it.Advance()) { + StackFrame* frame = AllocateFrameCopy(it.frame()); + list.Add(frame); + } + return list.ToVector(); +} + + } } // namespace v8::internal diff --git a/src/frames.h b/src/frames.h index 8cbbc62..98aaead 100644 --- a/src/frames.h +++ b/src/frames.h @@ -114,6 +114,12 @@ class StackFrame BASE_EMBEDDED { // by the debugger. enum Id { NO_ID = 0 }; + // Copy constructor; it breaks the connection to host iterator. + StackFrame(const StackFrame& original) { + this->state_ = original.state_; + this->iterator_ = NULL; + } + // Type testers. bool is_entry() const { return type() == ENTRY; } bool is_entry_construct() const { return type() == ENTRY_CONSTRUCT; } @@ -132,6 +138,8 @@ class StackFrame BASE_EMBEDDED { Address pc() const { return *pc_address(); } void set_pc(Address pc) { *pc_address() = pc; } + virtual void SetCallerFp(Address caller_fp) = 0; + Address* pc_address() const { return state_.pc_address; } // Get the id of this stack frame. @@ -200,7 +208,8 @@ class StackFrame BASE_EMBEDDED { friend class StackHandlerIterator; friend class SafeStackFrameIterator; - DISALLOW_IMPLICIT_CONSTRUCTORS(StackFrame); + private: + void operator=(const StackFrame& original); }; @@ -218,6 +227,7 @@ class EntryFrame: public StackFrame { ASSERT(frame->is_entry()); return static_cast(frame); } + virtual void SetCallerFp(Address caller_fp); protected: explicit EntryFrame(StackFrameIterator* iterator) : StackFrame(iterator) { } @@ -268,6 +278,8 @@ class ExitFrame: public StackFrame { // Garbage collection support. virtual void Iterate(ObjectVisitor* v) const; + virtual void SetCallerFp(Address caller_fp); + static ExitFrame* cast(StackFrame* frame) { ASSERT(frame->is_exit()); return static_cast(frame); @@ -303,6 +315,8 @@ class StandardFrame: public StackFrame { inline void SetExpression(int index, Object* value); int ComputeExpressionsCount() const; + virtual void SetCallerFp(Address caller_fp); + static StandardFrame* cast(StackFrame* frame) { ASSERT(frame->is_standard()); return static_cast(frame); @@ -658,6 +672,10 @@ class StackFrameLocator BASE_EMBEDDED { }; +// Reads all frames on the current stack and copies them into the current +// zone memory. +Vector CreateStackMap(); + } } // namespace v8::internal #endif // V8_FRAMES_H_ diff --git a/src/ia32/debug-ia32.cc b/src/ia32/debug-ia32.cc index 5d18a03..d142b11 100644 --- a/src/ia32/debug-ia32.cc +++ b/src/ia32/debug-ia32.cc @@ -206,8 +206,58 @@ void Debug::GenerateStubNoRegistersDebugBreak(MacroAssembler* masm) { } +void Debug::GeneratePlainReturnLiveEdit(MacroAssembler* masm) { + masm->ret(0); +} + +// FrameDropper is a code replacement for a JavaScript frame with possibly +// several frames above. +// There is no calling conventions here, because it never actually gets called, +// it only gets returned to. +// Frame structure (conforms InternalFrame structure): +// -- JSFunction +// -- code +// -- SMI maker +// -- context +// -- frame base +void Debug::GenerateFrameDropperLiveEdit(MacroAssembler* masm) { + // We do not know our frame height, but set esp based on ebp. + __ lea(esp, Operand(ebp, -4 * kPointerSize)); + + __ pop(edi); // function + + // Skip code self-reference and marker. + __ add(Operand(esp), Immediate(2 * kPointerSize)); + + __ pop(esi); // Context. + __ pop(ebp); + + // Get function code. + __ mov(edx, FieldOperand(edi, JSFunction::kSharedFunctionInfoOffset)); + __ mov(edx, FieldOperand(edx, SharedFunctionInfo::kCodeOffset)); + __ lea(edx, FieldOperand(edx, Code::kHeaderSize)); + + // Re-run JSFunction, edi is function, esi is context. + __ jmp(Operand(edx)); +} + #undef __ + +void Debug::SetUpFrameDropperFrame(StackFrame* bottom_js_frame, + Handle code) { + ASSERT(bottom_js_frame->is_java_script()); + + Address fp = bottom_js_frame->fp(); + Memory::Object_at(fp - 4 * kPointerSize) = + Memory::Object_at(fp - 2 * kPointerSize); // Move edi (function). + + Memory::Object_at(fp - 3 * kPointerSize) = *code; + Memory::Object_at(fp - 2 * kPointerSize) = Smi::FromInt(StackFrame::INTERNAL); +} +const int Debug::kFrameDropperFrameSize = 5; + + #endif // ENABLE_DEBUGGER_SUPPORT } } // namespace v8::internal diff --git a/src/liveedit-debugger.js b/src/liveedit-debugger.js index 2cbf0d5..75effb2 100644 --- a/src/liveedit-debugger.js +++ b/src/liveedit-debugger.js @@ -389,19 +389,32 @@ Debug.LiveEditChangeScript.CheckStackActivations = function(shared_wrapper_list, for (var i = 0; i < shared_wrapper_list.length; i++) { shared_list[i] = shared_wrapper_list[i].info; } - var result = %LiveEditCheckStackActivations(shared_list); + var result = %LiveEditCheckAndDropActivations(shared_list, true); + if (result[shared_list.length]) { + // Extra array element may contain error message. + throw new liveedit.Failure(result[shared_list.length]); + } + var problems = new Array(); + var dropped = new Array(); for (var i = 0; i < shared_list.length; i++) { - if (result[i] == liveedit.FunctionPatchabilityStatus.FUNCTION_BLOCKED_ON_STACK) { - var shared = shared_list[i]; + var shared = shared_wrapper_list[i]; + if (result[i] == liveedit.FunctionPatchabilityStatus.REPLACED_ON_ACTIVE_STACK) { + dropped.push({ name: shared.function_name } ); + } else if (result[i] != liveedit.FunctionPatchabilityStatus.AVAILABLE_FOR_PATCH) { var description = { name: shared.function_name, - start_pos: shared.start_position, - end_pos: shared.end_position + start_pos: shared.start_position, + end_pos: shared.end_position, + replace_problem: + liveedit.FunctionPatchabilityStatus.SymbolName(result[i]) }; problems.push(description); } } + if (dropped.length > 0) { + change_log.push({ dropped_from_stack: dropped }); + } if (problems.length > 0) { change_log.push( { functions_on_stack: problems } ); throw new liveedit.Failure("Blocked by functions on stack"); @@ -410,8 +423,21 @@ Debug.LiveEditChangeScript.CheckStackActivations = function(shared_wrapper_list, // A copy of the FunctionPatchabilityStatus enum from liveedit.h Debug.LiveEditChangeScript.FunctionPatchabilityStatus = { - FUNCTION_AVAILABLE_FOR_PATCH: 0, - FUNCTION_BLOCKED_ON_STACK: 1 + AVAILABLE_FOR_PATCH: 1, + BLOCKED_ON_ACTIVE_STACK: 2, + BLOCKED_ON_OTHER_STACK: 3, + BLOCKED_UNDER_NATIVE_CODE: 4, + REPLACED_ON_ACTIVE_STACK: 5 +} + +Debug.LiveEditChangeScript.FunctionPatchabilityStatus.SymbolName = + function(code) { + var enum = Debug.LiveEditChangeScript.FunctionPatchabilityStatus; + for (name in enum) { + if (enum[name] == code) { + return name; + } + } } diff --git a/src/liveedit.cc b/src/liveedit.cc index 55308ab..0e836e5 100644 --- a/src/liveedit.cc +++ b/src/liveedit.cc @@ -34,6 +34,7 @@ #include "scopes.h" #include "global-handles.h" #include "debug.h" +#include "memory.h" namespace v8 { namespace internal { @@ -673,6 +674,272 @@ void LiveEdit::PatchFunctionPositions(Handle shared_info_array, } +// Check an activation against list of functions. If there is a function +// that matches, its status in result array is changed to status argument value. +static bool CheckActivation(Handle shared_info_array, + Handle result, StackFrame* frame, + LiveEdit::FunctionPatchabilityStatus status) { + if (!frame->is_java_script()) { + return false; + } + int len = Smi::cast(shared_info_array->length())->value(); + for (int i = 0; i < len; i++) { + JSValue* wrapper = JSValue::cast(shared_info_array->GetElement(i)); + Handle shared( + SharedFunctionInfo::cast(wrapper->value())); + + if (frame->code() == shared->code()) { + SetElement(result, i, Handle(Smi::FromInt(status))); + return true; + } + } + return false; +} + + +// Iterates over handler chain and removes all elements that are inside +// frames being dropped. +static bool FixTryCatchHandler(StackFrame* top_frame, + StackFrame* bottom_frame) { + Address* pointer_address = + &Memory::Address_at(Top::get_address_from_id(Top::k_handler_address)); + + while (*pointer_address < top_frame->sp()) { + pointer_address = &Memory::Address_at(*pointer_address); + } + Address* above_frame_address = pointer_address; + while (*pointer_address < bottom_frame->fp()) { + pointer_address = &Memory::Address_at(*pointer_address); + } + bool change = *above_frame_address != *pointer_address; + *above_frame_address = *pointer_address; + return change; +} + + +// Removes specified range of frames from stack. There may be 1 or more +// frames in range. Anyway the bottom frame is restarted rather than dropped, +// and therefore has to be a JavaScript frame. +// Returns error message or NULL. +static const char* DropFrames(Vector frames, + int top_frame_index, + int bottom_js_frame_index) { + StackFrame* pre_top_frame = frames[top_frame_index - 1]; + StackFrame* top_frame = frames[top_frame_index]; + StackFrame* bottom_js_frame = frames[bottom_js_frame_index]; + + ASSERT(bottom_js_frame->is_java_script()); + + // Check the nature of the top frame. + if (pre_top_frame->code()->is_inline_cache_stub() && + pre_top_frame->code()->ic_state() == DEBUG_BREAK) { + // OK, we can drop inline cache calls. + } else if (pre_top_frame->code() == + Builtins::builtin(Builtins::FrameDropper_LiveEdit)) { + // OK, we can drop our own code. + } else if (pre_top_frame->code()->kind() == Code::STUB && + pre_top_frame->code()->major_key()) { + // Unit Test entry, it's fine, we support this case. + } else { + return "Unknown structure of stack above changing function"; + } + + Address unused_stack_top = top_frame->sp(); + Address unused_stack_bottom = bottom_js_frame->fp() + - Debug::kFrameDropperFrameSize * kPointerSize // Size of the new frame. + + kPointerSize; // Bigger address end is exclusive. + + if (unused_stack_top > unused_stack_bottom) { + return "Not enough space for frame dropper frame"; + } + + // Committing now. After this point we should return only NULL value. + + FixTryCatchHandler(pre_top_frame, bottom_js_frame); + // Make sure FixTryCatchHandler is idempotent. + ASSERT(!FixTryCatchHandler(pre_top_frame, bottom_js_frame)); + + Handle code(Builtins::builtin(Builtins::FrameDropper_LiveEdit)); + top_frame->set_pc(code->entry()); + pre_top_frame->SetCallerFp(bottom_js_frame->fp()); + + Debug::SetUpFrameDropperFrame(bottom_js_frame, code); + + for (Address a = unused_stack_top; + a < unused_stack_bottom; + a += kPointerSize) { + Memory::Object_at(a) = Smi::FromInt(0); + } + + return NULL; +} + + +static bool IsDropableFrame(StackFrame* frame) { + return !frame->is_exit(); +} + +// Fills result array with statuses of functions. Modifies the stack +// removing all listed function if possible and if do_drop is true. +static const char* DropActivationsInActiveThread( + Handle shared_info_array, Handle result, bool do_drop) { + + ZoneScope scope(DELETE_ON_EXIT); + Vector frames = CreateStackMap(); + + int array_len = Smi::cast(shared_info_array->length())->value(); + + int top_frame_index = -1; + int frame_index = 0; + for (; frame_index < frames.length(); frame_index++) { + StackFrame* frame = frames[frame_index]; + if (frame->id() == Debug::break_frame_id()) { + top_frame_index = frame_index; + break; + } + if (CheckActivation(shared_info_array, result, frame, + LiveEdit::FUNCTION_BLOCKED_UNDER_NATIVE_CODE)) { + // We are still above break_frame. It is not a target frame, + // it is a problem. + return "Debugger mark-up on stack is not found"; + } + } + + if (top_frame_index == -1) { + // We haven't found break frame, but no function is blocking us anyway. + return NULL; + } + + bool target_frame_found = false; + int bottom_js_frame_index = top_frame_index; + bool c_code_found = false; + + for (; frame_index < frames.length(); frame_index++) { + StackFrame* frame = frames[frame_index]; + if (!IsDropableFrame(frame)) { + c_code_found = true; + break; + } + if (CheckActivation(shared_info_array, result, frame, + LiveEdit::FUNCTION_BLOCKED_ON_ACTIVE_STACK)) { + target_frame_found = true; + bottom_js_frame_index = frame_index; + } + } + + if (c_code_found) { + // There is a C frames on stack. Check that there are no target frames + // below them. + for (; frame_index < frames.length(); frame_index++) { + StackFrame* frame = frames[frame_index]; + if (frame->is_java_script()) { + if (CheckActivation(shared_info_array, result, frame, + LiveEdit::FUNCTION_BLOCKED_UNDER_NATIVE_CODE)) { + // Cannot drop frame under C frames. + return NULL; + } + } + } + } + + if (!do_drop) { + // We are in check-only mode. + return NULL; + } + + if (!target_frame_found) { + // Nothing to drop. + return NULL; + } + + const char* error_message = DropFrames(frames, top_frame_index, + bottom_js_frame_index); + + if (error_message != NULL) { + return error_message; + } + + // Adjust break_frame after some frames has been dropped. + StackFrame::Id new_id = StackFrame::NO_ID; + for (int i = bottom_js_frame_index + 1; i < frames.length(); i++) { + if (frames[i]->type() == StackFrame::JAVA_SCRIPT) { + new_id = frames[i]->id(); + break; + } + } + Debug::FramesHaveBeenDropped(new_id); + + // Replace "blocked on active" with "replaced on active" status. + for (int i = 0; i < array_len; i++) { + if (result->GetElement(i) == + Smi::FromInt(LiveEdit::FUNCTION_BLOCKED_ON_ACTIVE_STACK)) { + result->SetElement(i, Smi::FromInt( + LiveEdit::FUNCTION_REPLACED_ON_ACTIVE_STACK)); + } + } + return NULL; +} + + +class InactiveThreadActivationsChecker : public ThreadVisitor { + public: + InactiveThreadActivationsChecker(Handle shared_info_array, + Handle result) + : shared_info_array_(shared_info_array), result_(result), + has_blocked_functions_(false) { + } + void VisitThread(ThreadLocalTop* top) { + for (StackFrameIterator it(top); !it.done(); it.Advance()) { + has_blocked_functions_ |= CheckActivation( + shared_info_array_, result_, it.frame(), + LiveEdit::FUNCTION_BLOCKED_ON_OTHER_STACK); + } + } + bool HasBlockedFunctions() { + return has_blocked_functions_; + } + + private: + Handle shared_info_array_; + Handle result_; + bool has_blocked_functions_; +}; + + +Handle LiveEdit::CheckAndDropActivations( + Handle shared_info_array, bool do_drop) { + int len = Smi::cast(shared_info_array->length())->value(); + + Handle result = Factory::NewJSArray(len); + + // Fill the default values. + for (int i = 0; i < len; i++) { + SetElement(result, i, + Handle(Smi::FromInt(FUNCTION_AVAILABLE_FOR_PATCH))); + } + + + // First check inactive threads. Fail if some functions are blocked there. + InactiveThreadActivationsChecker inactive_threads_checker(shared_info_array, + result); + ThreadManager::IterateThreads(&inactive_threads_checker); + if (inactive_threads_checker.HasBlockedFunctions()) { + return result; + } + + // Try to drop activations from the current stack. + const char* error_message = + DropActivationsInActiveThread(shared_info_array, result, do_drop); + if (error_message != NULL) { + // Add error message as an array extra element. + Vector vector_message(error_message, strlen(error_message)); + Handle str = Factory::NewStringFromAscii(vector_message); + SetElement(result, len, str); + } + return result; +} + + LiveEditFunctionTracker::LiveEditFunctionTracker(FunctionLiteral* fun) { if (active_function_info_listener != NULL) { active_function_info_listener->FunctionStarted(fun); diff --git a/src/liveedit.h b/src/liveedit.h index 2a9cc62..bd4c1ee 100644 --- a/src/liveedit.h +++ b/src/liveedit.h @@ -91,10 +91,21 @@ class LiveEdit : AllStatic { static void PatchFunctionPositions(Handle shared_info_array, Handle position_change_array); + // Checks listed functions on stack and return array with corresponding + // FunctionPatchabilityStatus statuses; extra array element may + // contain general error message. Modifies the current stack and + // has restart the lowest found frames and drops all other frames above + // if possible and if do_drop is true. + static Handle CheckAndDropActivations( + Handle shared_info_array, bool do_drop); + // A copy of this is in liveedit-debugger.js. enum FunctionPatchabilityStatus { - FUNCTION_AVAILABLE_FOR_PATCH = 0, - FUNCTION_BLOCKED_ON_STACK = 1 + FUNCTION_AVAILABLE_FOR_PATCH = 1, + FUNCTION_BLOCKED_ON_ACTIVE_STACK = 2, + FUNCTION_BLOCKED_ON_OTHER_STACK = 3, + FUNCTION_BLOCKED_UNDER_NATIVE_CODE = 4, + FUNCTION_REPLACED_ON_ACTIVE_STACK = 5 }; }; diff --git a/src/mips/debug-mips.cc b/src/mips/debug-mips.cc index 772bcc0..cdb35ae 100644 --- a/src/mips/debug-mips.cc +++ b/src/mips/debug-mips.cc @@ -104,8 +104,24 @@ void Debug::GenerateStubNoRegistersDebugBreak(MacroAssembler* masm) { } +void Debug::GeneratePlainReturnLiveEdit(MacroAssembler* masm) { + masm->Abort("LiveEdit frame dropping is not supported on mips"); +} + +void Debug::GenerateFrameDropperLiveEdit(MacroAssembler* masm) { + masm->Abort("LiveEdit frame dropping is not supported on mips"); +} + #undef __ + +void Debug::SetUpFrameDropperFrame(StackFrame* bottom_js_frame, + Handle code) { + UNREACHABLE(); +} +const int Debug::kFrameDropperFrameSize = -1; + + #endif // ENABLE_DEBUGGER_SUPPORT } } // namespace v8::internal diff --git a/src/runtime.cc b/src/runtime.cc index 40b37d9..fe4ad95 100644 --- a/src/runtime.cc +++ b/src/runtime.cc @@ -9693,41 +9693,18 @@ static Object* Runtime_LiveEditPatchFunctionPositions(Arguments args) { } -static LiveEdit::FunctionPatchabilityStatus FindFunctionCodeOnStacks( - Handle shared) { - // TODO(635): check all threads, not only the current one. - for (StackFrameIterator it; !it.done(); it.Advance()) { - StackFrame* frame = it.frame(); - if (frame->code() == shared->code()) { - return LiveEdit::FUNCTION_BLOCKED_ON_STACK; - } - } - return LiveEdit::FUNCTION_AVAILABLE_FOR_PATCH; -} - // For array of SharedFunctionInfo's (each wrapped in JSValue) // checks that none of them have activations on stacks (of any thread). // Returns array of the same length with corresponding results of // LiveEdit::FunctionPatchabilityStatus type. -static Object* Runtime_LiveEditCheckStackActivations(Arguments args) { - ASSERT(args.length() == 1); +static Object* Runtime_LiveEditCheckAndDropActivations(Arguments args) { + ASSERT(args.length() == 2); HandleScope scope; CONVERT_ARG_CHECKED(JSArray, shared_array, 0); + CONVERT_BOOLEAN_CHECKED(do_drop, args[1]); - int len = Smi::cast(shared_array->length())->value(); - Handle result = Factory::NewJSArray(len); - - for (int i = 0; i < len; i++) { - JSValue* wrapper = JSValue::cast(shared_array->GetElement(i)); - Handle shared( - SharedFunctionInfo::cast(wrapper->value())); - LiveEdit::FunctionPatchabilityStatus check_res = - FindFunctionCodeOnStacks(shared); - SetElement(result, i, Handle(Smi::FromInt(check_res))); - } - - return *result; + return *LiveEdit::CheckAndDropActivations(shared_array, do_drop); } @@ -9761,6 +9738,35 @@ static Object* Runtime_GetFunctionCodePositionFromSource(Arguments args) { } +// Calls specified function with or without entering the debugger. +// This is used in unit tests to run code as if debugger is entered or simply +// to have a stack with C++ frame in the middle. +static Object* Runtime_ExecuteInDebugContext(Arguments args) { + ASSERT(args.length() == 2); + HandleScope scope; + CONVERT_ARG_CHECKED(JSFunction, function, 0); + CONVERT_BOOLEAN_CHECKED(without_debugger, args[1]); + + Handle result; + bool pending_exception; + { + if (without_debugger) { + result = Execution::Call(function, Top::global(), 0, NULL, + &pending_exception); + } else { + EnterDebugger enter_debugger; + result = Execution::Call(function, Top::global(), 0, NULL, + &pending_exception); + } + } + if (!pending_exception) { + return *result; + } else { + return Failure::Exception(); + } +} + + #endif // ENABLE_DEBUGGER_SUPPORT #ifdef ENABLE_LOGGING_AND_PROFILING diff --git a/src/runtime.h b/src/runtime.h index c079345..896a8f7 100644 --- a/src/runtime.h +++ b/src/runtime.h @@ -334,8 +334,9 @@ namespace internal { F(LiveEditReplaceFunctionCode, 2, 1) \ F(LiveEditRelinkFunctionToScript, 2, 1) \ F(LiveEditPatchFunctionPositions, 2, 1) \ - F(LiveEditCheckStackActivations, 1, 1) \ - F(GetFunctionCodePositionFromSource, 2, 1) + F(LiveEditCheckAndDropActivations, 2, 1) \ + F(GetFunctionCodePositionFromSource, 2, 1) \ + F(ExecuteInDebugContext, 2, 1) #else #define RUNTIME_FUNCTION_LIST_DEBUGGER_SUPPORT(F) #endif diff --git a/src/x64/debug-x64.cc b/src/x64/debug-x64.cc index 261b16c..5470912 100644 --- a/src/x64/debug-x64.cc +++ b/src/x64/debug-x64.cc @@ -177,9 +177,24 @@ void Debug::GenerateStubNoRegistersDebugBreak(MacroAssembler* masm) { } +void Debug::GeneratePlainReturnLiveEdit(MacroAssembler* masm) { + masm->Abort("LiveEdit frame dropping is not supported on x64"); +} + +void Debug::GenerateFrameDropperLiveEdit(MacroAssembler* masm) { + masm->Abort("LiveEdit frame dropping is not supported on x64"); +} + #undef __ +void Debug::SetUpFrameDropperFrame(StackFrame* bottom_js_frame, + Handle code) { + UNREACHABLE(); +} +const int Debug::kFrameDropperFrameSize = -1; + + void BreakLocationIterator::ClearDebugBreakAtReturn() { rinfo()->PatchCode(original_rinfo()->pc(), Assembler::kJSReturnSequenceLength); diff --git a/test/mjsunit/debug-liveedit-check-stack.js b/test/mjsunit/debug-liveedit-check-stack.js index 1d788be..6b16490 100644 --- a/test/mjsunit/debug-liveedit-check-stack.js +++ b/test/mjsunit/debug-liveedit-check-stack.js @@ -30,55 +30,112 @@ Debug = debug.Debug -eval( - "function ChooseAnimal(callback) {\n " + - " callback();\n" + - " return 'Cat';\n" + - "}\n" -); +unique_id = 1; -function Noop() {} -var res = ChooseAnimal(Noop); +function TestBase(name) { + print("TestBase constructor: " + name); -assertEquals("Cat", res); + this.ChooseAnimal = eval( + "/* " + unique_id + "*/\n" + + "(function ChooseAnimal(callback) {\n " + + " callback();\n" + + " return 'Cat';\n" + + "})\n" + ); + // Prevents eval script caching. + unique_id++; -var script = Debug.findScript(ChooseAnimal); + var script = Debug.findScript(this.ChooseAnimal); -var orig_animal = "'Cat'"; -var patch_pos = script.source.indexOf(orig_animal); -var new_animal_patch = "'Capybara'"; + var orig_animal = "'Cat'"; + var patch_pos = script.source.indexOf(orig_animal); + var new_animal_patch = "'Capybara'"; -var got_exception = false; -var successfully_changed = false; + var got_exception = false; + var successfully_changed = false; -function Changer() { - // Never try the same patch again. - assertEquals(false, successfully_changed); - var change_log = new Array(); - try { - Debug.LiveEditChangeScript(script, patch_pos, orig_animal.length, new_animal_patch, change_log); + // Should be called from Debug context. + this.ScriptChanger = function() { + assertEquals(false, successfully_changed, "applying patch second time"); + // Runs in debugger context. + var change_log = new Array(); + try { + Debug.LiveEditChangeScript(script, patch_pos, orig_animal.length, new_animal_patch, change_log); + } finally { + print("Change log: " + JSON.stringify(change_log) + "\n"); + } successfully_changed = true; - } catch (e) { - if (e instanceof Debug.LiveEditChangeScript.Failure) { - got_exception = true; - print(e); - } else { - throw e; + }; +} + +function Noop() {} + +function WrapInCatcher(f, holder) { + return function() { + delete holder[0]; + try { + f(); + } catch (e) { + if (e instanceof Debug.LiveEditChangeScript.Failure) { + holder[0] = e; + } else { + throw e; + } + } + }; +} + +function WrapInNativeCall(f) { + return function() { + return Debug.ExecuteInDebugContext(f, true); + }; +} + +function WrapInDebuggerCall(f) { + return function() { + return Debug.ExecuteInDebugContext(f, false); + }; +} + +function WrapInRestartProof(f) { + var already_called = false; + return function() { + if (already_called) { + return; } + already_called = true; + f(); + } +} + +function WrapInConstructor(f) { + return function() { + return new function() { + f(); + }; } - print("Change log: " + JSON.stringify(change_log) + "\n"); } -var new_res = ChooseAnimal(Changer); -// Function must be not pached. -assertEquals("Cat", new_res); -assertEquals(true, got_exception); +// A series of tests. In each test we call ChooseAnimal function that calls +// a callback that attempts to modify the function on the fly. + +test = new TestBase("First test ChooseAnimal without edit"); +assertEquals("Cat", test.ChooseAnimal(Noop)); + +test = new TestBase("Test without function on stack"); +test.ScriptChanger(); +assertEquals("Capybara", test.ChooseAnimal(Noop)); + +test = new TestBase("Test with function on stack"); +assertEquals("Capybara", test.ChooseAnimal(WrapInDebuggerCall(WrapInRestartProof(test.ScriptChanger)))); + -// This time it should succeed. -Changer(); +test = new TestBase("Test with function on stack and with constructor frame"); +assertEquals("Capybara", test.ChooseAnimal(WrapInConstructor(WrapInDebuggerCall(WrapInRestartProof(test.ScriptChanger))))); -new_res = ChooseAnimal(Noop); -// Function must be not pached. -assertEquals("Capybara", new_res); +test = new TestBase("Test with C++ frame above ChooseAnimal frame"); +exception_holder = {}; +assertEquals("Cat", test.ChooseAnimal(WrapInNativeCall(WrapInDebuggerCall(WrapInCatcher(test.ScriptChanger, exception_holder))))); +assertTrue(!!exception_holder[0]); diff --git a/test/mjsunit/mjsunit.status b/test/mjsunit/mjsunit.status index 7cb2416..47963fe 100644 --- a/test/mjsunit/mjsunit.status +++ b/test/mjsunit/mjsunit.status @@ -48,6 +48,10 @@ unicode-case-overoptimization: PASS, TIMEOUT if ($arch == arm) # Skip long running test in debug and allow it to timeout in release mode. regress/regress-524: (PASS || TIMEOUT), SKIP if $mode == debug +# Skip experimental liveedit drop frame on non-ia32 architectures. +# debug-liveedit-check-stack: SKIP if $arch != ia32 +debug-liveedit-check-stack: SKIP + [ $arch == arm ] # Slow tests which times out in debug mode. -- 2.7.4