From bdfc48a3fd4ad059ad532b78a34fdb86450bd9bd Mon Sep 17 00:00:00 2001 From: "peter.rybin@gmail.com" Date: Fri, 15 Jun 2012 16:52:03 +0000 Subject: [PATCH] Support 'restart call frame' debug command Review URL: https://chromiumcodereview.appspot.com/10544151 git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@11836 ce2b1a6d-e550-0410-aec6-3dcde31c8c00 --- src/debug-debugger.js | 36 ++++++- src/liveedit-debugger.js | 18 +++- src/liveedit.cc | 99 ++++++++++++++--- src/liveedit.h | 6 +- src/mirror-debugger.js | 11 +- src/runtime.cc | 39 +++++++ src/runtime.h | 1 + test/mjsunit/debug-liveedit-restart-frame.js | 153 +++++++++++++++++++++++++++ test/mjsunit/mjsunit.status | 1 + 9 files changed, 343 insertions(+), 21 deletions(-) create mode 100644 test/mjsunit/debug-liveedit-restart-frame.js diff --git a/src/debug-debugger.js b/src/debug-debugger.js index 91838e8..d1bafb4 100644 --- a/src/debug-debugger.js +++ b/src/debug-debugger.js @@ -1449,6 +1449,8 @@ DebugCommandProcessor.prototype.processDebugJSONRequest = function( this.profileRequest_(request, response); } else if (request.command == 'changelive') { this.changeLiveRequest_(request, response); + } else if (request.command == 'restartframe') { + this.restartFrameRequest_(request, response); } else if (request.command == 'flags') { this.debuggerFlagsRequest_(request, response); } else if (request.command == 'v8flags') { @@ -2358,9 +2360,6 @@ DebugCommandProcessor.prototype.profileRequest_ = function(request, response) { DebugCommandProcessor.prototype.changeLiveRequest_ = function( request, response) { - if (!Debug.LiveEdit) { - return response.failed('LiveEdit feature is not supported'); - } if (!request.arguments) { return response.failed('Missing arguments'); } @@ -2398,6 +2397,37 @@ DebugCommandProcessor.prototype.changeLiveRequest_ = function( }; +DebugCommandProcessor.prototype.restartFrameRequest_ = function( + request, response) { + if (!request.arguments) { + return response.failed('Missing arguments'); + } + var frame = request.arguments.frame; + + // No frames to evaluate in frame. + if (this.exec_state_.frameCount() == 0) { + return response.failed('No frames'); + } + + var frame_mirror; + // Check whether a frame was specified. + if (!IS_UNDEFINED(frame)) { + var frame_number = %ToNumber(frame); + if (frame_number < 0 || frame_number >= this.exec_state_.frameCount()) { + return response.failed('Invalid frame "' + frame + '"'); + } + // Restart specified frame. + frame_mirror = this.exec_state_.frame(frame_number); + } else { + // Restart selected frame. + frame_mirror = this.exec_state_.frame(); + } + + var result_description = Debug.LiveEdit.RestartFrame(frame_mirror); + response.body = {result: result_description}; +}; + + DebugCommandProcessor.prototype.debuggerFlagsRequest_ = function(request, response) { // Check for legal request. diff --git a/src/liveedit-debugger.js b/src/liveedit-debugger.js index 4463c93..cfcdb81 100644 --- a/src/liveedit-debugger.js +++ b/src/liveedit-debugger.js @@ -1,4 +1,4 @@ -// Copyright 2010 the V8 project authors. All rights reserved. +// Copyright 2012 the V8 project authors. All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: @@ -47,6 +47,8 @@ Debug.LiveEdit = new function() { // Forward declaration for minifier. var FunctionStatus; + var NEEDS_STEP_IN_PROPERTY_NAME = "stack_update_needs_step_in"; + // Applies the change to the script. // The change is in form of list of chunks encoded in a single array as // a series of triplets (pos1_start, pos1_end, pos2_end) @@ -161,7 +163,7 @@ Debug.LiveEdit = new function() { // Our current implementation requires client to manually issue "step in" // command for correct stack state. - preview_description.stack_update_needs_step_in = + preview_description[NEEDS_STEP_IN_PROPERTY_NAME] = preview_description.stack_modified; // Start with breakpoints. Convert their line/column positions and @@ -1078,6 +1080,18 @@ Debug.LiveEdit = new function() { return ProcessOldNode(old_code_tree); } + // Restarts call frame and returns value similar to what LiveEdit returns. + function RestartFrame(frame_mirror) { + var result = frame_mirror.restart(); + if (IS_STRING(result)) { + throw new Failure("Failed to restart frame: " + result); + } + var result = {}; + result[NEEDS_STEP_IN_PROPERTY_NAME] = true; + return result; + } + // Function is public. + this.RestartFrame = RestartFrame; // Functions are public for tests. this.TestApi = { diff --git a/src/liveedit.cc b/src/liveedit.cc index e670b44..47208f6 100644 --- a/src/liveedit.cc +++ b/src/liveedit.cc @@ -1595,17 +1595,36 @@ 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, - Zone* zone) { + +// Describes a set of call frames that execute any of listed functions. +// Finding no such frames does not mean error. +class MultipleFunctionTarget { + public: + MultipleFunctionTarget(Handle shared_info_array, + Handle result) + : m_shared_info_array(shared_info_array), + m_result(result) {} + bool MatchActivation(StackFrame* frame, + LiveEdit::FunctionPatchabilityStatus status) { + return CheckActivation(m_shared_info_array, m_result, frame, status); + } + const char* GetNotFoundMessage() { + return NULL; + } + private: + Handle m_shared_info_array; + Handle m_result; +}; + +// Drops all call frame matched by target and all frames above them. +template +static const char* DropActivationsInActiveThreadImpl( + TARGET& target, bool do_drop, Zone* zone) { Isolate* isolate = Isolate::Current(); Debug* debug = isolate->debug(); ZoneScope scope(isolate, DELETE_ON_EXIT); Vector frames = CreateStackMap(zone); - int array_len = Smi::cast(shared_info_array->length())->value(); int top_frame_index = -1; int frame_index = 0; @@ -1615,8 +1634,8 @@ static const char* DropActivationsInActiveThread( top_frame_index = frame_index; break; } - if (CheckActivation(shared_info_array, result, frame, - LiveEdit::FUNCTION_BLOCKED_UNDER_NATIVE_CODE)) { + if (target.MatchActivation( + 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"; @@ -1625,7 +1644,7 @@ static const char* DropActivationsInActiveThread( if (top_frame_index == -1) { // We haven't found break frame, but no function is blocking us anyway. - return NULL; + return target.GetNotFoundMessage(); } bool target_frame_found = false; @@ -1638,8 +1657,8 @@ static const char* DropActivationsInActiveThread( c_code_found = true; break; } - if (CheckActivation(shared_info_array, result, frame, - LiveEdit::FUNCTION_BLOCKED_ON_ACTIVE_STACK)) { + if (target.MatchActivation( + frame, LiveEdit::FUNCTION_BLOCKED_ON_ACTIVE_STACK)) { target_frame_found = true; bottom_js_frame_index = frame_index; } @@ -1651,8 +1670,8 @@ static const char* DropActivationsInActiveThread( 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)) { + if (target.MatchActivation( + frame, LiveEdit::FUNCTION_BLOCKED_UNDER_NATIVE_CODE)) { // Cannot drop frame under C frames. return NULL; } @@ -1667,7 +1686,7 @@ static const char* DropActivationsInActiveThread( if (!target_frame_found) { // Nothing to drop. - return NULL; + return target.GetNotFoundMessage(); } Debug::FrameDropMode drop_mode = Debug::FRAMES_UNTOUCHED; @@ -1690,6 +1709,23 @@ static const char* DropActivationsInActiveThread( } debug->FramesHaveBeenDropped(new_id, drop_mode, restarter_frame_function_pointer); + return NULL; +} + +// 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, + Zone* zone) { + MultipleFunctionTarget target(shared_info_array, result); + + const char* message = + DropActivationsInActiveThreadImpl(target, do_drop, zone); + if (message) { + return message; + } + + int array_len = Smi::cast(shared_info_array->length())->value(); // Replace "blocked on active" with "replaced on active" status. for (int i = 0; i < array_len; i++) { @@ -1766,6 +1802,41 @@ Handle LiveEdit::CheckAndDropActivations( } +// Describes a single callframe a target. Not finding this frame +// means an error. +class SingleFrameTarget { + public: + explicit SingleFrameTarget(JavaScriptFrame* frame) : m_frame(frame) {} + + bool MatchActivation(StackFrame* frame, + LiveEdit::FunctionPatchabilityStatus status) { + if (frame->fp() == m_frame->fp()) { + m_saved_status = status; + return true; + } + return false; + } + const char* GetNotFoundMessage() { + return "Failed to found requested frame"; + } + LiveEdit::FunctionPatchabilityStatus saved_status() { + return m_saved_status; + } + private: + JavaScriptFrame* m_frame; + LiveEdit::FunctionPatchabilityStatus m_saved_status; +}; + + +// Finds a drops required frame and all frames above. +// Returns error message or NULL. +const char* LiveEdit::RestartFrame(JavaScriptFrame* frame, Zone* zone) { + SingleFrameTarget target(frame); + + return DropActivationsInActiveThreadImpl(target, true, zone); +} + + LiveEditFunctionTracker::LiveEditFunctionTracker(Isolate* isolate, FunctionLiteral* fun) : isolate_(isolate) { diff --git a/src/liveedit.h b/src/liveedit.h index 424c24e..5b12854 100644 --- a/src/liveedit.h +++ b/src/liveedit.h @@ -1,4 +1,4 @@ -// Copyright 2010 the V8 project authors. All rights reserved. +// Copyright 2012 the V8 project authors. All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: @@ -123,6 +123,10 @@ class LiveEdit : AllStatic { static Handle CheckAndDropActivations( Handle shared_info_array, bool do_drop, Zone* zone); + // Restarts the call frame and completely drops all frames above it. + // Return error message or NULL. + static const char* RestartFrame(JavaScriptFrame* frame, Zone* zone); + // A copy of this is in liveedit-debugger.js. enum FunctionPatchabilityStatus { FUNCTION_AVAILABLE_FOR_PATCH = 1, diff --git a/src/mirror-debugger.js b/src/mirror-debugger.js index c7f0dcc..f71483a 100644 --- a/src/mirror-debugger.js +++ b/src/mirror-debugger.js @@ -1,4 +1,4 @@ -// Copyright 2006-2008 the V8 project authors. All rights reserved. +// Copyright 2006-2012 the V8 project authors. All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: @@ -1750,6 +1750,15 @@ FrameMirror.prototype.localsText = function() { }; +FrameMirror.prototype.restart = function() { + var result = %LiveEditRestartFrame(this.break_id_, this.index_); + if (IS_UNDEFINED(result)) { + result = "Failed to find requested frame"; + } + return result; +}; + + FrameMirror.prototype.toText = function(opt_locals) { var result = ''; result += '#' + (this.index() <= 9 ? '0' : '') + this.index(); diff --git a/src/runtime.cc b/src/runtime.cc index 95bc313..5b44374 100644 --- a/src/runtime.cc +++ b/src/runtime.cc @@ -12789,6 +12789,45 @@ RUNTIME_FUNCTION(MaybeObject*, Runtime_LiveEditCompareStrings) { } +// Restarts a call frame and completely drops all frames above. +// Returns true if successful. Otherwise returns undefined or an error message. +RUNTIME_FUNCTION(MaybeObject*, Runtime_LiveEditRestartFrame) { + HandleScope scope(isolate); + ASSERT(args.length() == 2); + + // Check arguments. + Object* check; + { MaybeObject* maybe_check = Runtime_CheckExecutionState( + RUNTIME_ARGUMENTS(isolate, args)); + if (!maybe_check->ToObject(&check)) return maybe_check; + } + CONVERT_NUMBER_CHECKED(int, index, Int32, args[1]); + Heap* heap = isolate->heap(); + + // Find the relevant frame with the requested index. + StackFrame::Id id = isolate->debug()->break_frame_id(); + if (id == StackFrame::NO_ID) { + // If there are no JavaScript stack frames return undefined. + return heap->undefined_value(); + } + + int count = 0; + JavaScriptFrameIterator it(isolate, id); + for (; !it.done(); it.Advance()) { + if (index < count + it.frame()->GetInlineCount()) break; + count += it.frame()->GetInlineCount(); + } + if (it.done()) return heap->undefined_value(); + + const char* error_message = + LiveEdit::RestartFrame(it.frame(), isolate->zone()); + if (error_message) { + return *(isolate->factory()->LookupAsciiSymbol(error_message)); + } + return heap->true_value(); +} + + // A testing entry. Returns statement position which is the closest to // source_position. RUNTIME_FUNCTION(MaybeObject*, Runtime_GetFunctionCodePositionFromSource) { diff --git a/src/runtime.h b/src/runtime.h index f5a4f50..9968b29 100644 --- a/src/runtime.h +++ b/src/runtime.h @@ -442,6 +442,7 @@ namespace internal { F(LiveEditPatchFunctionPositions, 2, 1) \ F(LiveEditCheckAndDropActivations, 2, 1) \ F(LiveEditCompareStrings, 2, 1) \ + F(LiveEditRestartFrame, 2, 1) \ F(GetFunctionCodePositionFromSource, 2, 1) \ F(ExecuteInDebugContext, 2, 1) \ \ diff --git a/test/mjsunit/debug-liveedit-restart-frame.js b/test/mjsunit/debug-liveedit-restart-frame.js new file mode 100644 index 0000000..d978a97 --- /dev/null +++ b/test/mjsunit/debug-liveedit-restart-frame.js @@ -0,0 +1,153 @@ +// Copyright 2012 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Flags: --expose-debug-as debug +// Get the Debug object exposed from the debug context global object. + +Debug = debug.Debug + +function FindCallFrame(exec_state, frame_code) { + var number = Number(frame_code); + if (number >= 0) { + return exec_state.frame(number); + } else { + for (var i = 0; i < exec_state.frameCount(); i++) { + var frame = exec_state.frame(i); + var func_mirror = frame.func(); + if (frame_code == func_mirror.name()) { + return frame; + } + } + } + throw new Error("Failed to find function name " + function_name); +} + +function TestCase(test_scenario, expected_output) { + // Global variable, accessed from eval'd script. + test_output = ""; + + function TestCode() { + function A() { + // Extra stack variable. To make function not slim. + // Restarter doesn't work on slim function when stopped on 'debugger' + // statement. (There is no padding for 'debugger' statement). + var o = {}; + test_output += 'A'; + test_output += '='; + debugger; + return 'Capybara'; + } + function B(p1, p2) { + test_output += 'B'; + return A(); + } + function C() { + test_output += 'C'; + // Function call with argument adaptor is intentional. + return B(); + } + function D() { + test_output += 'D'; + // Function call with argument adaptor is intentional. + return C(1, 2); + } + function E() { + test_output += 'E'; + return D(); + } + function F() { + test_output += 'F'; + return E(); + } + return F(); + } + + var scenario_pos = 0; + + function DebuggerStatementHandler(exec_state) { + while (true) { + assertTrue(scenario_pos < test_scenario.length); + var change_code = test_scenario[scenario_pos++]; + if (change_code == '=') { + // Continue. + return; + } + var frame = FindCallFrame(exec_state, change_code); + // Throws if fails. + Debug.LiveEdit.RestartFrame(frame); + } + } + + var saved_exception = null; + + function listener(event, exec_state, event_data, data) { + if (saved_exception != null) { + return; + } + if (event == Debug.DebugEvent.Break) { + try { + DebuggerStatementHandler(exec_state); + } catch (e) { + saved_exception = e; + } + } else { + print("Other: " + event); + } + } + + Debug.setListener(listener); + assertEquals("Capybara", TestCode()); + Debug.setListener(null); + + if (saved_exception) { + print("Exception: " + saved_exception); + print("Stack: " + saved_exception.stack); + assertUnreachable(); + } + + print(test_output); + + assertEquals(expected_output, test_output); +} + +TestCase('0==', "FEDCBA=A="); +TestCase('1==', "FEDCBA=BA="); +TestCase('2==', "FEDCBA=CBA="); +TestCase('3==', "FEDCBA=DCBA="); +TestCase('4==', "FEDCBA=EDCBA="); +TestCase('5==', "FEDCBA=FEDCBA="); + +TestCase('=', "FEDCBA="); + +TestCase('C==', "FEDCBA=CBA="); + +TestCase('B=C=A=D==', "FEDCBA=BA=CBA=A=DCBA="); + +// Successive restarts don't work now and require additional fix. +//TestCase('BCDE==', "FEDCBA=EDCBA="); +//TestCase('BC=BCDE==', "FEDCBA=CBA=EDCBA="); +//TestCase('EF==', "FEDCBA=FEDCBA="); diff --git a/test/mjsunit/mjsunit.status b/test/mjsunit/mjsunit.status index 5ae1274..da5026d 100644 --- a/test/mjsunit/mjsunit.status +++ b/test/mjsunit/mjsunit.status @@ -63,6 +63,7 @@ unicode-case-overoptimization: PASS, TIMEOUT if ($arch == arm || $arch == mips) debug-liveedit-check-stack: SKIP debug-liveedit-patch-positions-replace: SKIP debug-liveedit-stack-padding: SKIP +debug-liveedit-restart-frame: SKIP # Test Crankshaft compilation time. Expected to take too long in debug mode. regress/regress-1969: PASS, SKIP if $mode == debug -- 2.7.4