Collect stack trace on stack overflow.

BUG=v8:2394

Review URL: https://chromiumcodereview.appspot.com/11275186

git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@12933 ce2b1a6d-e550-0410-aec6-3dcde31c8c00
This commit is contained in:
yangguo@chromium.org 2012-11-12 14:54:29 +00:00
parent 69846ce69d
commit 4c27298d27
7 changed files with 320 additions and 133 deletions

View File

@ -553,7 +553,97 @@ void Isolate::PushStackTraceAndDie(unsigned int magic,
}
void Isolate::CaptureAndSetCurrentStackTraceFor(Handle<JSObject> error_object) {
// Determines whether the given stack frame should be displayed in
// a stack trace. The caller is the error constructor that asked
// for the stack trace to be collected. The first time a construct
// call to this function is encountered it is skipped. The seen_caller
// in/out parameter is used to remember if the caller has been seen
// yet.
static bool IsVisibleInStackTrace(StackFrame* raw_frame,
Object* caller,
bool* seen_caller) {
// Only display JS frames.
if (!raw_frame->is_java_script()) return false;
JavaScriptFrame* frame = JavaScriptFrame::cast(raw_frame);
Object* raw_fun = frame->function();
// Not sure when this can happen but skip it just in case.
if (!raw_fun->IsJSFunction()) return false;
if ((raw_fun == caller) && !(*seen_caller)) {
*seen_caller = true;
return false;
}
// Skip all frames until we've seen the caller.
if (!(*seen_caller)) return false;
// Also, skip non-visible built-in functions and any call with the builtins
// object as receiver, so as to not reveal either the builtins object or
// an internal function.
// The --builtins-in-stack-traces command line flag allows including
// internal call sites in the stack trace for debugging purposes.
if (!FLAG_builtins_in_stack_traces) {
JSFunction* fun = JSFunction::cast(raw_fun);
if (frame->receiver()->IsJSBuiltinsObject() ||
(fun->IsBuiltin() && !fun->shared()->native())) {
return false;
}
}
return true;
}
Handle<JSArray> Isolate::CaptureSimpleStackTrace(Handle<JSObject> error_object,
Handle<Object> caller,
int limit) {
limit = Max(limit, 0); // Ensure that limit is not negative.
int initial_size = Min(limit, 10);
Handle<FixedArray> elements =
factory()->NewFixedArrayWithHoles(initial_size * 4);
// If the caller parameter is a function we skip frames until we're
// under it before starting to collect.
bool seen_caller = !caller->IsJSFunction();
int cursor = 0;
int frames_seen = 0;
for (StackFrameIterator iter(this);
!iter.done() && frames_seen < limit;
iter.Advance()) {
StackFrame* raw_frame = iter.frame();
if (IsVisibleInStackTrace(raw_frame, *caller, &seen_caller)) {
frames_seen++;
JavaScriptFrame* frame = JavaScriptFrame::cast(raw_frame);
// Set initial size to the maximum inlining level + 1 for the outermost
// function.
List<FrameSummary> frames(Compiler::kMaxInliningLevels + 1);
frame->Summarize(&frames);
for (int i = frames.length() - 1; i >= 0; i--) {
if (cursor + 4 > elements->length()) {
int new_capacity = JSObject::NewElementsCapacity(elements->length());
Handle<FixedArray> new_elements =
factory()->NewFixedArrayWithHoles(new_capacity);
for (int i = 0; i < cursor; i++) {
new_elements->set(i, elements->get(i));
}
elements = new_elements;
}
ASSERT(cursor + 4 <= elements->length());
Handle<Object> recv = frames[i].receiver();
Handle<JSFunction> fun = frames[i].function();
Handle<Code> code = frames[i].code();
Handle<Smi> offset(Smi::FromInt(frames[i].offset()));
elements->set(cursor++, *recv);
elements->set(cursor++, *fun);
elements->set(cursor++, *code);
elements->set(cursor++, *offset);
}
}
}
Handle<JSArray> result = factory()->NewJSArrayWithElements(elements);
result->set_length(Smi::FromInt(cursor));
return result;
}
void Isolate::CaptureAndSetDetailedStackTrace(Handle<JSObject> error_object) {
if (capture_stack_trace_for_uncaught_exceptions_) {
// Capture stack trace for a detailed exception message.
Handle<String> key = factory()->hidden_stack_trace_symbol();
@ -910,15 +1000,28 @@ const char* const Isolate::kStackOverflowMessage =
Failure* Isolate::StackOverflow() {
HandleScope scope;
// At this point we cannot create an Error object using its javascript
// constructor. Instead, we copy the pre-constructed boilerplate and
// attach the stack trace as a hidden property.
Handle<String> key = factory()->stack_overflow_symbol();
Handle<JSObject> boilerplate =
Handle<JSObject>::cast(GetProperty(js_builtins_object(), key));
Handle<Object> exception = Copy(boilerplate);
// TODO(1240995): To avoid having to call JavaScript code to compute
// the message for stack overflow exceptions which is very likely to
// double fault with another stack overflow exception, we use a
// precomputed message.
Handle<JSObject> exception = Copy(boilerplate);
DoThrow(*exception, NULL);
// Get stack trace limit.
Handle<Object> error = GetProperty(js_builtins_object(), "$Error");
if (!error->IsJSObject()) return Failure::Exception();
Handle<Object> stack_trace_limit =
GetProperty(Handle<JSObject>::cast(error), "stackTraceLimit");
if (!stack_trace_limit->IsNumber()) return Failure::Exception();
int limit = static_cast<int>(stack_trace_limit->Number());
Handle<JSArray> stack_trace = CaptureSimpleStackTrace(
exception, factory()->undefined_value(), limit);
JSObject::SetHiddenProperty(exception,
factory()->hidden_stack_trace_symbol(),
stack_trace);
return Failure::Exception();
}

View File

@ -716,7 +716,10 @@ class Isolate {
int frame_limit,
StackTrace::StackTraceOptions options);
void CaptureAndSetCurrentStackTraceFor(Handle<JSObject> error_object);
Handle<JSArray> CaptureSimpleStackTrace(Handle<JSObject> error_object,
Handle<Object> caller,
int limit);
void CaptureAndSetDetailedStackTrace(Handle<JSObject> error_object);
// Returns if the top context may access the given global object. If
// the result is false, the pending exception is guaranteed to be

View File

@ -755,29 +755,6 @@ function GetStackTraceLine(recv, fun, pos, isGlobal) {
// ----------------------------------------------------------------------------
// Error implementation
// Defines accessors for a property that is calculated the first time
// the property is read.
function DefineOneShotAccessor(obj, name, fun) {
// Note that the accessors consistently operate on 'obj', not 'this'.
// Since the object may occur in someone else's prototype chain we
// can't rely on 'this' being the same as 'obj'.
var value;
var value_factory = fun;
var getter = function() {
if (value_factory == null) {
return value;
}
value = value_factory(obj);
value_factory = null;
return value;
};
var setter = function(v) {
value_factory = null;
value = v;
};
%DefineOrRedefineAccessorProperty(obj, name, getter, setter, DONT_ENUM);
}
function CallSite(receiver, fun, pos) {
this.receiver = receiver;
this.fun = fun;
@ -1112,9 +1089,21 @@ function captureStackTrace(obj, cons_opt) {
var raw_stack = %CollectStackTrace(obj,
cons_opt ? cons_opt : captureStackTrace,
stackTraceLimit);
DefineOneShotAccessor(obj, 'stack', function (obj) {
return FormatRawStackTrace(obj, raw_stack);
});
// Note that 'obj' and 'this' maybe different when called on objects that
// have the error object on its prototype chain. The getter replaces itself
// with a data property as soon as the stack trace has been formatted.
var getter = function() {
var value = FormatRawStackTrace(obj, raw_stack);
%DefineOrRedefineDataProperty(obj, 'stack', value, NONE);
return value;
};
// The 'stack' property of the receiver is set as data property. If
// the receiver is the same as holder, this accessor pair is replaced.
var setter = function(v) {
%DefineOrRedefineDataProperty(this, 'stack', v, NONE);
};
%DefineOrRedefineAccessorProperty(obj, 'stack', getter, setter, DONT_ENUM);
}
@ -1249,4 +1238,37 @@ InstallFunctions($Error.prototype, DONT_ENUM, ['toString', ErrorToString]);
// Boilerplate for exceptions for stack overflows. Used from
// Isolate::StackOverflow().
var kStackOverflowBoilerplate = MakeRangeError('stack_overflow', []);
function SetUpStackOverflowBoilerplate() {
var boilerplate = MakeRangeError('stack_overflow', []);
// The raw stack trace is stored as hidden property of the copy of this
// boilerplate error object. Note that the receiver 'this' may not be that
// error object copy, but can be found on the prototype chain of 'this'.
// When the stack trace is formatted, this accessor property is replaced by
// a data property.
function getter() {
var holder = this;
while (!IS_ERROR(holder)) {
holder = %GetPrototype(holder);
if (holder == null) return MakeSyntaxError('illegal_access', []);
}
var raw_stack = %GetOverflowedRawStackTrace(holder);
var result = IS_ARRAY(raw_stack) ? FormatRawStackTrace(holder, raw_stack)
: void 0;
%DefineOrRedefineDataProperty(holder, 'stack', result, NONE);
return result;
}
// The 'stack' property of the receiver is set as data property. If
// the receiver is the same as holder, this accessor pair is replaced.
function setter(v) {
%DefineOrRedefineDataProperty(this, 'stack', v, NONE);
}
%DefineOrRedefineAccessorProperty(
boilerplate, 'stack', getter, setter, DONT_ENUM);
return boilerplate;
}
var kStackOverflowBoilerplate = SetUpStackOverflowBoilerplate();

View File

@ -12903,47 +12903,6 @@ RUNTIME_FUNCTION(MaybeObject*, Runtime_GetScript) {
}
// Determines whether the given stack frame should be displayed in
// a stack trace. The caller is the error constructor that asked
// for the stack trace to be collected. The first time a construct
// call to this function is encountered it is skipped. The seen_caller
// in/out parameter is used to remember if the caller has been seen
// yet.
static bool ShowFrameInStackTrace(StackFrame* raw_frame,
Object* caller,
bool* seen_caller) {
// Only display JS frames.
if (!raw_frame->is_java_script()) {
return false;
}
JavaScriptFrame* frame = JavaScriptFrame::cast(raw_frame);
Object* raw_fun = frame->function();
// Not sure when this can happen but skip it just in case.
if (!raw_fun->IsJSFunction()) {
return false;
}
if ((raw_fun == caller) && !(*seen_caller)) {
*seen_caller = true;
return false;
}
// Skip all frames until we've seen the caller.
if (!(*seen_caller)) return false;
// Also, skip non-visible built-in functions and any call with the builtins
// object as receiver, so as to not reveal either the builtins object or
// an internal function.
// The --builtins-in-stack-traces command line flag allows including
// internal call sites in the stack trace for debugging purposes.
if (!FLAG_builtins_in_stack_traces) {
JSFunction* fun = JSFunction::cast(raw_fun);
if (frame->receiver()->IsJSBuiltinsObject() ||
(fun->IsBuiltin() && !fun->shared()->native())) {
return false;
}
}
return true;
}
// Collect the raw data for a stack trace. Returns an array of 4
// element segments each containing a receiver, function, code and
// native code offset.
@ -12954,57 +12913,23 @@ RUNTIME_FUNCTION(MaybeObject*, Runtime_CollectStackTrace) {
CONVERT_NUMBER_CHECKED(int32_t, limit, Int32, args[2]);
HandleScope scope(isolate);
Factory* factory = isolate->factory();
// Optionally capture a more detailed stack trace for the message.
isolate->CaptureAndSetDetailedStackTrace(error_object);
// Capture a simple stack trace for the stack property.
return *isolate->CaptureSimpleStackTrace(error_object, caller, limit);
}
limit = Max(limit, 0); // Ensure that limit is not negative.
int initial_size = Min(limit, 10);
Handle<FixedArray> elements =
factory->NewFixedArrayWithHoles(initial_size * 4);
StackFrameIterator iter(isolate);
// If the caller parameter is a function we skip frames until we're
// under it before starting to collect.
bool seen_caller = !caller->IsJSFunction();
int cursor = 0;
int frames_seen = 0;
while (!iter.done() && frames_seen < limit) {
StackFrame* raw_frame = iter.frame();
if (ShowFrameInStackTrace(raw_frame, *caller, &seen_caller)) {
frames_seen++;
JavaScriptFrame* frame = JavaScriptFrame::cast(raw_frame);
// Set initial size to the maximum inlining level + 1 for the outermost
// function.
List<FrameSummary> frames(Compiler::kMaxInliningLevels + 1);
frame->Summarize(&frames);
for (int i = frames.length() - 1; i >= 0; i--) {
if (cursor + 4 > elements->length()) {
int new_capacity = JSObject::NewElementsCapacity(elements->length());
Handle<FixedArray> new_elements =
factory->NewFixedArrayWithHoles(new_capacity);
for (int i = 0; i < cursor; i++) {
new_elements->set(i, elements->get(i));
}
elements = new_elements;
}
ASSERT(cursor + 4 <= elements->length());
Handle<Object> recv = frames[i].receiver();
Handle<JSFunction> fun = frames[i].function();
Handle<Code> code = frames[i].code();
Handle<Smi> offset(Smi::FromInt(frames[i].offset()));
elements->set(cursor++, *recv);
elements->set(cursor++, *fun);
elements->set(cursor++, *code);
elements->set(cursor++, *offset);
}
}
iter.Advance();
}
Handle<JSArray> result = factory->NewJSArrayWithElements(elements);
// Capture and attach a more detailed stack trace if necessary.
isolate->CaptureAndSetCurrentStackTraceFor(error_object);
result->set_length(Smi::FromInt(cursor));
return *result;
// Retrieve the raw stack trace collected on stack overflow and delete
// it since it is used only once to avoid keeping it alive.
RUNTIME_FUNCTION(MaybeObject*, Runtime_GetOverflowedRawStackTrace) {
ASSERT_EQ(args.length(), 1);
CONVERT_ARG_CHECKED(JSObject, error_object, 0);
String* key = isolate->heap()->hidden_stack_trace_symbol();
Object* result = error_object->GetHiddenProperty(key);
RUNTIME_ASSERT(result->IsJSArray() || result->IsUndefined());
error_object->DeleteHiddenProperty(key);
return result;
}

View File

@ -233,6 +233,7 @@ namespace internal {
F(FunctionIsBuiltin, 1, 1) \
F(GetScript, 1, 1) \
F(CollectStackTrace, 3, 1) \
F(GetOverflowedRawStackTrace, 1, 1) \
F(GetV8Version, 0, 1) \
\
F(ClassOf, 1, 1) \

View File

@ -2410,19 +2410,13 @@ class SourceResource: public v8::String::ExternalAsciiStringResource {
};
TEST(ReleaseStackTraceData) {
void ReleaseStackTraceDataTest(const char* source) {
// Test that the data retained by the Error.stack accessor is released
// after the first time the accessor is fired. We use external string
// to check whether the data is being released since the external string
// resource's callback is fired when the external string is GC'ed.
InitializeVM();
v8::HandleScope scope;
static const char* source = "var error = 1; "
"try { "
" throw new Error(); "
"} catch (e) { "
" error = e; "
"} ";
SourceResource* resource = new SourceResource(i::StrDup(source));
{
v8::HandleScope scope;
@ -2434,15 +2428,32 @@ TEST(ReleaseStackTraceData) {
// External source is being retained by the stack trace.
CHECK(!resource->IsDisposed());
CompileRun("error.stack; error.stack;");
CompileRun("error.stack;");
HEAP->CollectAllAvailableGarbage();
// External source has been released.
CHECK(resource->IsDisposed());
delete resource;
}
TEST(ReleaseStackTraceData) {
static const char* source1 = "var error = null; "
/* Normal Error */ "try { "
" throw new Error(); "
"} catch (e) { "
" error = e; "
"} ";
static const char* source2 = "var error = null; "
/* Stack overflow */ "try { "
" (function f() { f(); })(); "
"} catch (e) { "
" error = e; "
"} ";
ReleaseStackTraceDataTest(source1);
ReleaseStackTraceDataTest(source2);
}
TEST(Regression144230) {
InitializeVM();
v8::HandleScope scope;

View File

@ -0,0 +1,122 @@
// 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.
function rec1(a) { rec1(a+1); }
function rec2(a) { rec3(a+1); }
function rec3(a) { rec2(a+1); }
// Test stack trace getter and setter.
try {
rec1(0);
} catch (e) {
assertTrue(e.stack.indexOf("rec1") > 0);
e.stack = "123";
assertEquals("123", e.stack);
}
// Test setter w/o calling the getter.
try {
rec2(0);
} catch (e) {
assertTrue(e.stack.indexOf("rec2") > 0);
assertTrue(e.stack.indexOf("rec3") > 0);
e.stack = "123";
assertEquals("123", e.stack);
}
// Test getter to make sure setter does not affect the boilerplate.
try {
rec1(0);
} catch (e) {
assertTrue(e.stack.indexOf("rec1") > 0);
assertInstanceof(e, RangeError);
}
// Check setting/getting stack property on the prototype chain.
function testErrorPrototype(prototype) {
var object = {};
object.__proto__ = prototype;
object.stack = "123";
assertEquals("123", object.stack);
assertTrue("123" != prototype.stack);
}
try {
rec1(0);
} catch (e) {
e.stack;
testErrorPrototype(e);
}
try {
rec1(0);
} catch (e) {
testErrorPrototype(e);
}
try {
throw new Error();
} catch (e) {
testErrorPrototype(e);
}
Error.stackTraceLimit = 3;
try {
rec1(0);
} catch (e) {
assertEquals(4, e.stack.split('\n').length);
}
Error.stackTraceLimit = 25.9;
try {
rec1(0);
} catch (e) {
assertEquals(26, e.stack.split('\n').length);
}
Error.stackTraceLimit = NaN;
try {
rec1(0);
} catch (e) {
assertEquals(1, e.stack.split('\n').length);
}
Error.stackTraceLimit = "not a number";
try {
rec1(0);
} catch (e) {
assertEquals(undefined, e.stack);
}
Error.stackTraceLimit = 3;
Error = ""; // Overwrite Error in the global object.
try {
rec1(0);
} catch (e) {
assertEquals(4, e.stack.split('\n').length);
}