Support 'restart call frame' debug command
authorpeter.rybin@gmail.com <peter.rybin@gmail.com@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Fri, 15 Jun 2012 16:52:03 +0000 (16:52 +0000)
committerpeter.rybin@gmail.com <peter.rybin@gmail.com@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Fri, 15 Jun 2012 16:52:03 +0000 (16:52 +0000)
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
src/liveedit-debugger.js
src/liveedit.cc
src/liveedit.h
src/mirror-debugger.js
src/runtime.cc
src/runtime.h
test/mjsunit/debug-liveedit-restart-frame.js [new file with mode: 0644]
test/mjsunit/mjsunit.status

index 91838e8..d1bafb4 100644 (file)
@@ -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.
index 4463c93..cfcdb81 100644 (file)
@@ -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 = {
index e670b44..47208f6 100644 (file)
@@ -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<JSArray> shared_info_array, Handle<JSArray> 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<JSArray> shared_info_array,
+      Handle<JSArray> 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<JSArray> m_shared_info_array;
+  Handle<JSArray> m_result;
+};
+
+// Drops all call frame matched by target and all frames above them.
+template<typename TARGET>
+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<StackFrame*> 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<JSArray> shared_info_array, Handle<JSArray> 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<JSArray> 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) {
index 424c24e..5b12854 100644 (file)
@@ -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<JSArray> CheckAndDropActivations(
       Handle<JSArray> 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,
index c7f0dcc..f71483a 100644 (file)
@@ -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();
index 95bc313..5b44374 100644 (file)
@@ -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) {
index f5a4f50..9968b29 100644 (file)
@@ -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 (file)
index 0000000..d978a97
--- /dev/null
@@ -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=");
index 5ae1274..da5026d 100644 (file)
@@ -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