Adds C++ API for retrieving a stack trace without running JavaScript
authorsgjesse@chromium.org <sgjesse@chromium.org@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Thu, 6 May 2010 07:32:44 +0000 (07:32 +0000)
committersgjesse@chromium.org <sgjesse@chromium.org@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Thu, 6 May 2010 07:32:44 +0000 (07:32 +0000)
This API is extensible, and parameterized with flags so that callers can specify what subset of information they want to capture for each stack frame.

Patch by jaimeyap, see http://codereview.chromium.org/1694011 for details.
Review URL: http://codereview.chromium.org/2028001

git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@4597 ce2b1a6d-e550-0410-aec6-3dcde31c8c00

include/v8-profiler.h
include/v8.h
src/api.cc
src/api.h
src/messages.js
src/top.cc
src/top.h
test/cctest/test-api.cc

index eca6548..f1b8ffb 100644 (file)
@@ -109,7 +109,7 @@ class V8EXPORT CpuProfileNode {
   /** Retrieves a child node by index. */
   const CpuProfileNode* GetChild(int index) const;
 
-  static const int kNoLineNumberInfo = 0;
+  static const int kNoLineNumberInfo = Message::kNoLineNumberInfo;
 };
 
 
index c07ba1f..971ae4b 100644 (file)
@@ -126,6 +126,8 @@ template <class T> class Persistent;
 class FunctionTemplate;
 class ObjectTemplate;
 class Data;
+class StackTrace;
+class StackFrame;
 
 namespace internal {
 
@@ -691,6 +693,106 @@ class V8EXPORT Message {
 
   // TODO(1245381): Print to a string instead of on a FILE.
   static void PrintCurrentStackTrace(FILE* out);
+
+  static const int kNoLineNumberInfo = 0;
+  static const int kNoColumnInfo = 0;
+};
+
+
+/**
+ * Representation of a JavaScript stack trace. The information collected is a
+ * snapshot of the execution stack and the information remains valid after
+ * execution continues.
+ */
+class V8EXPORT StackTrace {
+ public:
+  /**
+   * Flags that determine what information is placed captured for each
+   * StackFrame when grabbing the current stack trace.
+   */
+  enum StackTraceOptions {
+    kLineNumber = 1,
+    kColumnOffset = 1 << 1 | kLineNumber,
+    kScriptName = 1 << 2,
+    kFunctionName = 1 << 3,
+    kIsEval = 1 << 4,
+    kIsConstructor = 1 << 5,
+    kOverview = kLineNumber | kColumnOffset | kScriptName | kFunctionName,
+    kDetailed = kOverview | kIsEval | kIsConstructor
+  };
+
+  /**
+   * Returns a StackFrame at a particular index.
+   */
+  Local<StackFrame> GetFrame(uint32_t index) const;
+
+  /**
+   * Returns the number of StackFrames.
+   */
+  int GetFrameCount() const;
+
+  /**
+   * Returns StackTrace as a v8::Array that contains StackFrame objects.
+   */
+  Local<Array> AsArray();
+
+  /**
+   * Grab a snapshot of the the current JavaScript execution stack.
+   *
+   * \param frame_limit The maximum number of stack frames we want to capture.
+   * \param options Enumerates the set of things we will capture for each
+   *   StackFrame.
+   */
+  static Local<StackTrace> CurrentStackTrace(
+      int frame_limit,
+      StackTraceOptions options = kOverview);
+};
+
+
+/**
+ * A single JavaScript stack frame.
+ */
+class V8EXPORT StackFrame {
+ public:
+  /**
+   * Returns the number, 1-based, of the line for the associate function call.
+   * This method will return Message::kNoLineNumberInfo if it is unable to
+   * retrieve the line number, or if kLineNumber was not passed as an option
+   * when capturing the StackTrace.
+   */
+  int GetLineNumber() const;
+
+  /**
+   * Returns the 1-based column offset on the line for the associated function
+   * call.
+   * This method will return Message::kNoColumnInfo if it is unable to retrieve
+   * the column number, or if kColumnOffset was not passed as an option when
+   * capturing the StackTrace.
+   */
+  int GetColumn() const;
+
+  /**
+   * Returns the name of the resource that contains the script for the
+   * function for this StackFrame.
+   */
+  Local<String> GetScriptName() const;
+
+  /**
+   * Returns the name of the function associated with this stack frame.
+   */
+  Local<String> GetFunctionName() const;
+
+  /**
+   * Returns whether or not the associated function is compiled via a call to
+   * eval().
+   */
+  bool IsEval() const;
+
+  /**
+   * Returns whther or not the associated function is called as a
+   * constructor via "new".
+   */
+  bool IsConstructor() const;
 };
 
 
index 0e5db2c..a4c38b7 100644 (file)
@@ -1438,7 +1438,7 @@ static i::Handle<i::Object> CallV8HeapFunction(const char* name,
 
 
 int Message::GetLineNumber() const {
-  ON_BAILOUT("v8::Message::GetLineNumber()", return -1);
+  ON_BAILOUT("v8::Message::GetLineNumber()", return kNoLineNumberInfo);
   ENTER_V8;
   HandleScope scope;
   EXCEPTION_PREAMBLE();
@@ -1470,7 +1470,7 @@ int Message::GetEndPosition() const {
 
 
 int Message::GetStartColumn() const {
-  if (IsDeadCheck("v8::Message::GetStartColumn()")) return 0;
+  if (IsDeadCheck("v8::Message::GetStartColumn()")) return kNoColumnInfo;
   ENTER_V8;
   HandleScope scope;
   i::Handle<i::JSObject> data_obj = Utils::OpenHandle(this);
@@ -1485,7 +1485,7 @@ int Message::GetStartColumn() const {
 
 
 int Message::GetEndColumn() const {
-  if (IsDeadCheck("v8::Message::GetEndColumn()")) return 0;
+  if (IsDeadCheck("v8::Message::GetEndColumn()")) return kNoColumnInfo;
   ENTER_V8;
   HandleScope scope;
   i::Handle<i::JSObject> data_obj = Utils::OpenHandle(this);
@@ -1525,6 +1525,118 @@ void Message::PrintCurrentStackTrace(FILE* out) {
 }
 
 
+// --- S t a c k T r a c e ---
+
+Local<StackFrame> StackTrace::GetFrame(uint32_t index) const {
+  if (IsDeadCheck("v8::StackTrace::GetFrame()")) return Local<StackFrame>();
+  ENTER_V8;
+  HandleScope scope;
+  i::Handle<i::JSArray> self = Utils::OpenHandle(this);
+  i::Handle<i::JSObject> obj(i::JSObject::cast(self->GetElement(index)));
+  return scope.Close(Utils::StackFrameToLocal(obj));
+}
+
+
+int StackTrace::GetFrameCount() const {
+  if (IsDeadCheck("v8::StackTrace::GetFrameCount()")) return -1;
+  ENTER_V8;
+  return i::Smi::cast(Utils::OpenHandle(this)->length())->value();
+}
+
+
+Local<Array> StackTrace::AsArray() {
+  if (IsDeadCheck("v8::StackTrace::AsArray()")) Local<Array>();
+  ENTER_V8;
+  return Utils::ToLocal(Utils::OpenHandle(this));
+}
+
+
+Local<StackTrace> StackTrace::CurrentStackTrace(int frame_limit,
+    StackTraceOptions options) {
+  if (IsDeadCheck("v8::StackTrace::CurrentStackTrace()")) Local<StackTrace>();
+  ENTER_V8;
+  return i::Top::CaptureCurrentStackTrace(frame_limit, options);
+}
+
+
+// --- S t a c k F r a m e ---
+
+int StackFrame::GetLineNumber() const {
+  if (IsDeadCheck("v8::StackFrame::GetLineNumber()")) {
+    return Message::kNoLineNumberInfo;
+  }
+  ENTER_V8;
+  i::HandleScope scope;
+  i::Handle<i::JSObject> self = Utils::OpenHandle(this);
+  i::Handle<i::Object> line = GetProperty(self, "lineNumber");
+  if (!line->IsSmi()) {
+    return Message::kNoLineNumberInfo;
+  }
+  return i::Smi::cast(*line)->value();
+}
+
+
+int StackFrame::GetColumn() const {
+  if (IsDeadCheck("v8::StackFrame::GetColumn()")) {
+    return Message::kNoColumnInfo;
+  }
+  ENTER_V8;
+  i::HandleScope scope;
+  i::Handle<i::JSObject> self = Utils::OpenHandle(this);
+  i::Handle<i::Object> column = GetProperty(self, "column");
+  if (!column->IsSmi()) {
+    return Message::kNoColumnInfo;
+  }
+  return i::Smi::cast(*column)->value();
+}
+
+
+Local<String> StackFrame::GetScriptName() const {
+  if (IsDeadCheck("v8::StackFrame::GetScriptName()")) return Local<String>();
+  ENTER_V8;
+  HandleScope scope;
+  i::Handle<i::JSObject> self = Utils::OpenHandle(this);
+  i::Handle<i::Object> name = GetProperty(self, "scriptName");
+  if (!name->IsString()) {
+    return Local<String>();
+  }
+  return scope.Close(Local<String>::Cast(Utils::ToLocal(name)));
+}
+
+
+Local<String> StackFrame::GetFunctionName() const {
+  if (IsDeadCheck("v8::StackFrame::GetFunctionName()")) return Local<String>();
+  ENTER_V8;
+  HandleScope scope;
+  i::Handle<i::JSObject> self = Utils::OpenHandle(this);
+  i::Handle<i::Object> name = GetProperty(self, "functionName");
+  if (!name->IsString()) {
+    return Local<String>();
+  }
+  return scope.Close(Local<String>::Cast(Utils::ToLocal(name)));
+}
+
+
+bool StackFrame::IsEval() const {
+  if (IsDeadCheck("v8::StackFrame::IsEval()")) return false;
+  ENTER_V8;
+  i::HandleScope scope;
+  i::Handle<i::JSObject> self = Utils::OpenHandle(this);
+  i::Handle<i::Object> is_eval = GetProperty(self, "isEval");
+  return is_eval->IsTrue();
+}
+
+
+bool StackFrame::IsConstructor() const {
+  if (IsDeadCheck("v8::StackFrame::IsConstructor()")) return false;
+  ENTER_V8;
+  i::HandleScope scope;
+  i::Handle<i::JSObject> self = Utils::OpenHandle(this);
+  i::Handle<i::Object> is_constructor = GetProperty(self, "isConstructor");
+  return is_constructor->IsTrue();
+}
+
+
 // --- D a t a ---
 
 bool Value::IsUndefined() const {
index 7b88112..e7b1394 100644 (file)
--- a/src/api.h
+++ b/src/api.h
@@ -192,6 +192,10 @@ class Utils {
       v8::internal::Handle<v8::internal::Proxy> obj);
   static inline Local<Message> MessageToLocal(
       v8::internal::Handle<v8::internal::Object> obj);
+  static inline Local<StackTrace> StackTraceToLocal(
+      v8::internal::Handle<v8::internal::JSArray> obj);
+  static inline Local<StackFrame> StackFrameToLocal(
+      v8::internal::Handle<v8::internal::JSObject> obj);
   static inline Local<Number> NumberToLocal(
       v8::internal::Handle<v8::internal::Object> obj);
   static inline Local<Integer> IntegerToLocal(
@@ -227,6 +231,10 @@ class Utils {
       OpenHandle(const Function* data);
   static inline v8::internal::Handle<v8::internal::JSObject>
       OpenHandle(const Message* message);
+  static inline v8::internal::Handle<v8::internal::JSArray>
+      OpenHandle(const StackTrace* stack_trace);
+  static inline v8::internal::Handle<v8::internal::JSObject>
+      OpenHandle(const StackFrame* stack_frame);
   static inline v8::internal::Handle<v8::internal::Context>
       OpenHandle(const v8::Context* context);
   static inline v8::internal::Handle<v8::internal::SignatureInfo>
@@ -275,6 +283,8 @@ MAKE_TO_LOCAL(ToLocal, ObjectTemplateInfo, ObjectTemplate)
 MAKE_TO_LOCAL(ToLocal, SignatureInfo, Signature)
 MAKE_TO_LOCAL(ToLocal, TypeSwitchInfo, TypeSwitch)
 MAKE_TO_LOCAL(MessageToLocal, Object, Message)
+MAKE_TO_LOCAL(StackTraceToLocal, JSArray, StackTrace)
+MAKE_TO_LOCAL(StackFrameToLocal, JSObject, StackFrame)
 MAKE_TO_LOCAL(NumberToLocal, Object, Number)
 MAKE_TO_LOCAL(IntegerToLocal, Object, Integer)
 MAKE_TO_LOCAL(Uint32ToLocal, Object, Uint32)
@@ -305,6 +315,8 @@ MAKE_OPEN_HANDLE(Function, JSFunction)
 MAKE_OPEN_HANDLE(Message, JSObject)
 MAKE_OPEN_HANDLE(Context, Context)
 MAKE_OPEN_HANDLE(External, Proxy)
+MAKE_OPEN_HANDLE(StackTrace, JSArray)
+MAKE_OPEN_HANDLE(StackFrame, JSObject)
 
 #undef MAKE_OPEN_HANDLE
 
index de6a362..a46af4a 100644 (file)
@@ -42,6 +42,9 @@ var COMPILATION_TYPE_JSON = 2;
 var kVowelSounds = 0;
 var kCapitalVowelSounds = 0;
 
+// Matches Messages::kNoLineNumberInfo from v8.h
+var kNoLineNumberInfo = 0;
+
 // If this object gets passed to an error constructor the error will
 // get an accessor for .message that constructs a descriptive error
 // message on access.
@@ -203,9 +206,9 @@ function FormatMessage(message) {
 
 
 function GetLineNumber(message) {
-  if (message.startPos == -1) return -1;
+  if (message.startPos == -1) return kNoLineNumberInfo;
   var location = message.script.locationFromPosition(message.startPos, true);
-  if (location == null) return -1;
+  if (location == null) return kNoLineNumberInfo;
   return location.line + 1;
 }
 
index 2f75c8f..c071193 100644 (file)
@@ -337,7 +337,7 @@ static int stack_trace_nesting_level = 0;
 static StringStream* incomplete_message = NULL;
 
 
-Handle<String> Top::StackTrace() {
+Handle<String> Top::StackTraceString() {
   if (stack_trace_nesting_level == 0) {
     stack_trace_nesting_level++;
     HeapStringAllocator allocator;
@@ -365,6 +365,90 @@ Handle<String> Top::StackTrace() {
 }
 
 
+Local<StackTrace> Top::CaptureCurrentStackTrace(
+    int frame_limit, StackTrace::StackTraceOptions options) {
+  v8::HandleScope scope;
+  // Ensure no negative values.
+  int limit = Max(frame_limit, 0);
+  Handle<JSArray> stackTrace = Factory::NewJSArray(frame_limit);
+  FixedArray* frames = FixedArray::cast(stackTrace->elements());
+
+  Handle<String> column_key =  Factory::LookupAsciiSymbol("column");
+  Handle<String> line_key =  Factory::LookupAsciiSymbol("lineNumber");
+  Handle<String> script_key =  Factory::LookupAsciiSymbol("scriptName");
+  Handle<String> function_key =  Factory::LookupAsciiSymbol("functionName");
+  Handle<String> eval_key =  Factory::LookupAsciiSymbol("isEval");
+  Handle<String> constructor_key =  Factory::LookupAsciiSymbol("isConstructor");
+
+  StackTraceFrameIterator it;
+  int frames_seen = 0;
+  while (!it.done() && (frames_seen < limit)) {
+    // Create a JSObject to hold the information for the StackFrame.
+    Handle<JSObject> stackFrame = Factory::NewJSObject(object_function());
+
+    JavaScriptFrame* frame = it.frame();
+    JSFunction* fun(JSFunction::cast(frame->function()));
+    Script* script = Script::cast(fun->shared()->script());
+
+    if (options & StackTrace::kLineNumber) {
+      int script_line_offset = script->line_offset()->value();
+      int position = frame->code()->SourcePosition(frame->pc());
+      int line_number = GetScriptLineNumber(Handle<Script>(script), position);
+
+      if (options & StackTrace::kColumnOffset) {
+        Handle<FixedArray> line_ends(FixedArray::cast(script->line_ends()));
+        int start = (line_number == 0) ?
+            0 : Smi::cast(line_ends->get(line_number - 1))->value() + 1;
+        int column_offset = position - start;
+        if (line_number == script_line_offset) {
+          // For the case where the code is on the same line as the script tag.
+          column_offset += script_line_offset;
+        }
+        SetProperty(stackFrame, column_key,
+                    Handle<Smi>(Smi::FromInt(column_offset + 1)), NONE);
+      }
+      // Adjust the line_number by the offset in the parent resource.
+      line_number += script_line_offset;
+      SetProperty(stackFrame, line_key,
+                  Handle<Smi>(Smi::FromInt(line_number + 1)), NONE);
+    }
+
+    if (options & StackTrace::kScriptName) {
+      Handle<Object> script_name(script->name());
+      SetProperty(stackFrame, script_key, script_name, NONE);
+    }
+
+    if (options & StackTrace::kFunctionName) {
+      Handle<Object> fun_name(fun->shared()->name());
+      if (!fun_name->IsString()) {
+        fun_name = Handle<Object>(fun->shared()->inferred_name());
+      }
+      SetProperty(stackFrame, function_key, fun_name, NONE);
+    }
+
+    if (options & StackTrace::kIsEval) {
+      int type = Smi::cast(script->compilation_type())->value();
+      Handle<Object> is_eval = (type == Script::COMPILATION_TYPE_EVAL) ?
+          Factory::true_value() : Factory::false_value();
+      SetProperty(stackFrame, eval_key, is_eval, NONE);
+    }
+
+    if (options & StackTrace::kIsConstructor) {
+      Handle<Object> is_constructor = (frame->IsConstructor()) ?
+          Factory::true_value() : Factory::false_value();
+      SetProperty(stackFrame, constructor_key, is_constructor, NONE);
+    }
+
+    frames->set(frames_seen, *stackFrame);
+    frames_seen++;
+    it.Advance();
+  }
+
+  stackTrace->set_length(Smi::FromInt(frames_seen));
+  return scope.Close(Utils::StackTraceToLocal(stackTrace));
+}
+
+
 void Top::PrintStack() {
   if (stack_trace_nesting_level == 0) {
     stack_trace_nesting_level++;
@@ -786,7 +870,7 @@ void Top::DoThrow(Object* exception,
       // traces while the bootstrapper is active since the infrastructure
       // may not have been properly initialized.
       Handle<String> stack_trace;
-      if (FLAG_trace_exception) stack_trace = StackTrace();
+      if (FLAG_trace_exception) stack_trace = StackTraceString();
       message_obj = MessageHandler::MakeMessageObject("uncaught_exception",
           location, HandleVector<Object>(&exception_handle, 1), stack_trace);
     }
index d263777..4a76a7f 100644 (file)
--- a/src/top.h
+++ b/src/top.h
@@ -265,7 +265,10 @@ class Top {
   static void PrintStackTrace(FILE* out, char* thread_data);
   static void PrintStack(StringStream* accumulator);
   static void PrintStack();
-  static Handle<String> StackTrace();
+  static Handle<String> StackTraceString();
+  static Local<StackTrace> CaptureCurrentStackTrace(
+      int frame_limit,
+      StackTrace::StackTraceOptions options);
 
   // Returns if the top context may access the given global object. If
   // the result is false, the pending exception is guaranteed to be
index dc80391..6ba708b 100644 (file)
@@ -9584,6 +9584,115 @@ THREADED_TEST(StackTrace) {
 }
 
 
+// Checks that a StackFrame has certain expected values.
+void checkStackFrame(const char* expected_script_name,
+    const char* expected_func_name, int expected_line_number,
+    int expected_column, bool is_eval, bool is_constructor,
+    v8::Handle<v8::StackFrame> frame) {
+  v8::HandleScope scope;
+  v8::String::Utf8Value func_name(frame->GetFunctionName());
+  v8::String::Utf8Value script_name(frame->GetScriptName());
+  if (*script_name == NULL) {
+    // The situation where there is no associated script, like for evals.
+    CHECK(expected_script_name == NULL);
+  } else {
+    CHECK(strstr(*script_name, expected_script_name) != NULL);
+  }
+  CHECK(strstr(*func_name, expected_func_name) != NULL);
+  CHECK_EQ(expected_line_number, frame->GetLineNumber());
+  CHECK_EQ(expected_column, frame->GetColumn());
+  CHECK_EQ(is_eval, frame->IsEval());
+  CHECK_EQ(is_constructor, frame->IsConstructor());
+}
+
+
+v8::Handle<Value> AnalyzeStackInNativeCode(const v8::Arguments& args) {
+  v8::HandleScope scope;
+  const char* origin = "capture-stack-trace-test";
+  const int kOverviewTest = 1;
+  const int kDetailedTest = 2;
+
+  ASSERT(args.Length() == 1);
+
+  int testGroup = args[0]->ToNumber()->Value();
+  if (testGroup == kOverviewTest) {
+    v8::Handle<v8::StackTrace> stackTrace =
+        v8::StackTrace::CurrentStackTrace(10, v8::StackTrace::kOverview);
+    CHECK_EQ(4, stackTrace->GetFrameCount());
+    checkStackFrame(origin, "bar", 2, 10, false, false,
+                    stackTrace->GetFrame(0));
+    checkStackFrame(origin, "foo", 6, 3, false, false,
+                    stackTrace->GetFrame(1));
+    checkStackFrame(NULL, "", 1, 1, false, false,
+                    stackTrace->GetFrame(2));
+    // The last frame is an anonymous function that has the initial call.
+    checkStackFrame(origin, "", 8, 7, false, false,
+                    stackTrace->GetFrame(3));
+
+    CHECK(stackTrace->AsArray()->IsArray());
+  } else if (testGroup == kDetailedTest) {
+    v8::Handle<v8::StackTrace> stackTrace =
+        v8::StackTrace::CurrentStackTrace(10, v8::StackTrace::kDetailed);
+    CHECK_EQ(4, stackTrace->GetFrameCount());
+    checkStackFrame(origin, "bat", 2, 1, false, false,
+                    stackTrace->GetFrame(0));
+    checkStackFrame(origin, "baz", 5, 3, false, true,
+                    stackTrace->GetFrame(1));
+    checkStackFrame(NULL, "", 1, 1, true, false,
+                    stackTrace->GetFrame(2));
+    // The last frame is an anonymous function that has the initial call to foo.
+    checkStackFrame(origin, "", 7, 1, false, false,
+                    stackTrace->GetFrame(3));
+
+    CHECK(stackTrace->AsArray()->IsArray());
+  }
+  return v8::Undefined();
+}
+
+
+// Tests the C++ StackTrace API.
+THREADED_TEST(CaptureStackTrace) {
+  v8::HandleScope scope;
+  v8::Handle<v8::String> origin = v8::String::New("capture-stack-trace-test");
+  Local<ObjectTemplate> templ = ObjectTemplate::New();
+  templ->Set(v8_str("AnalyzeStackInNativeCode"),
+             v8::FunctionTemplate::New(AnalyzeStackInNativeCode));
+  LocalContext context(0, templ);
+
+  // Test getting OVERVIEW information. Should ignore information that is not
+  // script name, function name, line number, and column offset.
+  const char *overview_source =
+    "function bar() {\n"
+    "  var y; AnalyzeStackInNativeCode(1);\n"
+    "}\n"
+    "function foo() {\n"
+    "\n"
+    "  bar();\n"
+    "}\n"
+    "var x;eval('new foo();');";
+  v8::Handle<v8::String> overview_src = v8::String::New(overview_source);
+  v8::Handle<Value> overview_result =
+      v8::Script::New(overview_src, origin)->Run();
+  ASSERT(!overview_result.IsEmpty());
+  ASSERT(overview_result->IsObject());
+
+  // Test getting DETAILED information.
+  const char *detailed_source =
+    "function bat() {\n"
+    "AnalyzeStackInNativeCode(2);\n"
+    "}\n"
+    "function baz() {\n"
+    "  bat();\n"
+    "}\n"
+    "eval('new baz();');";
+  v8::Handle<v8::String> detailed_src = v8::String::New(detailed_source);
+  v8::Handle<Value> detailed_result =
+      v8::Script::New(detailed_src, origin)->Run();
+  ASSERT(!detailed_result.IsEmpty());
+  ASSERT(detailed_result->IsObject());
+}
+
+
 // Test that idle notification can be handled and eventually returns true.
 THREADED_TEST(IdleNotification) {
   bool rv = false;