diff --git a/js/public/MemoryMetrics.h b/js/public/MemoryMetrics.h index fd2775e78ef7..423760b2306c 100644 --- a/js/public/MemoryMetrics.h +++ b/js/public/MemoryMetrics.h @@ -512,7 +512,8 @@ struct CompartmentStats macro(Other, NotLiveGCThing, compartmentObject) \ macro(Other, NotLiveGCThing, crossCompartmentWrappersTable) \ macro(Other, NotLiveGCThing, regexpCompartment) \ - macro(Other, NotLiveGCThing, debuggeesSet) + macro(Other, NotLiveGCThing, debuggeesSet) \ + macro(Other, NotLiveGCThing, savedStacksSet) CompartmentStats() : FOR_EACH_SIZE(ZERO_SIZE) diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index 180640c68deb..72a26a77a8a2 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -23,6 +23,7 @@ #include "vm/GlobalObject.h" #include "vm/Interpreter.h" #include "vm/ProxyObject.h" +#include "vm/SavedStacks.h" #include "vm/TraceLogging.h" #include "jscntxtinlines.h" @@ -850,6 +851,25 @@ CountHeap(JSContext *cx, unsigned argc, jsval *vp) return true; } +static bool +GetSavedFrameCount(JSContext *cx, unsigned argc, jsval *vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setNumber(cx->compartment()->savedStacks().count()); + return true; +} + +static bool +SaveStack(JSContext *cx, unsigned argc, jsval *vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + Rooted frame(cx); + if (!cx->compartment()->savedStacks().saveCurrentStack(cx, &frame)) + return false; + args.rval().setObject(*frame.get()); + return true; +} + #if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) static bool OOMAfterAllocations(JSContext *cx, unsigned argc, jsval *vp) @@ -1571,6 +1591,15 @@ static const JSFunctionSpecWithHelp TestingFunctions[] = { " then you can provide an extra argument with some specific traceable\n" " thing to count.\n"), + JS_FN_HELP("getSavedFrameCount", GetSavedFrameCount, 0, 0, +"getSavedFrameCount()", +" Return the number of SavedFrame instances stored in this compartment's\n" +" SavedStacks cache."), + + JS_FN_HELP("saveStack", SaveStack, 0, 0, +"saveStack()", +" Capture a stack.\n"), + #if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) JS_FN_HELP("oomAfterAllocations", OOMAfterAllocations, 1, 0, "oomAfterAllocations(count)", diff --git a/js/src/jit-test/tests/saved-stacks/SavedFrame-constructor.js b/js/src/jit-test/tests/saved-stacks/SavedFrame-constructor.js new file mode 100644 index 000000000000..4a3ed4d6a4c6 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/SavedFrame-constructor.js @@ -0,0 +1,3 @@ +// The SavedFrame constructor shouldn't have been exposed to JS on the global. + +assertEq(typeof SavedFrame, "undefined"); diff --git a/js/src/jit-test/tests/saved-stacks/evals.js b/js/src/jit-test/tests/saved-stacks/evals.js new file mode 100644 index 000000000000..93f6dca057b8 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/evals.js @@ -0,0 +1,38 @@ +// Test that we can save stacks with direct and indirect eval calls. + +const directEval = (function iife() { + return eval("(" + function evalFrame() { + return saveStack(); + } + "())"); +}()); + +assertEq(directEval.source.contains("> eval"), true); +assertEq(directEval.functionDisplayName, "evalFrame"); + +assertEq(directEval.parent.source.contains("> eval"), true); + +assertEq(directEval.parent.parent.source.contains("> eval"), false); +assertEq(directEval.parent.parent.functionDisplayName, "iife"); + +assertEq(directEval.parent.parent.parent.source.contains("> eval"), false); + +assertEq(directEval.parent.parent.parent.parent, null); + +const lave = eval; +const indirectEval = (function iife() { + return lave("(" + function evalFrame() { + return saveStack(); + } + "())"); +}()); + +assertEq(indirectEval.source.contains("> eval"), true); +assertEq(indirectEval.functionDisplayName, "evalFrame"); + +assertEq(indirectEval.parent.source.contains("> eval"), true); + +assertEq(indirectEval.parent.parent.source.contains("> eval"), false); +assertEq(indirectEval.parent.parent.functionDisplayName, "iife"); + +assertEq(indirectEval.parent.parent.parent.source.contains("> eval"), false); + +assertEq(indirectEval.parent.parent.parent.parent, null); diff --git a/js/src/jit-test/tests/saved-stacks/function-display-name.js b/js/src/jit-test/tests/saved-stacks/function-display-name.js new file mode 100644 index 000000000000..cfe175758c87 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/function-display-name.js @@ -0,0 +1,17 @@ +// Test the functionDisplayName of SavedFrame instances. + +function uno() { return dos(); } +const dos = () => tres.quattro(); +const tres = { + quattro: () => saveStack() +}; + +const frame = uno(); + +assertEq(frame.functionDisplayName, "tres.quattro"); +assertEq(frame.parent.functionDisplayName, "dos"); +assertEq(frame.parent.parent.functionDisplayName, "uno"); +assertEq(frame.parent.parent.parent.functionDisplayName, null); + +assertEq(frame.parent.parent.parent.parent, null); + diff --git a/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js b/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js new file mode 100644 index 000000000000..35f56fbcb083 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js @@ -0,0 +1,76 @@ +// Test that SavedFrame instances get removed from the SavedStacks frames cache +// after a GC. + +const FUZZ_FACTOR = 3; + +function assertAboutEq(actual, expected) { + if (Math.abs(actual - expected) > FUZZ_FACTOR) + throw new Error("Assertion failed: expected about " + expected + ", got " + actual + + ". FUZZ_FACTOR = " + FUZZ_FACTOR); +} + +const stacks = []; + +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); +stacks.push(saveStack()); + +assertAboutEq(getSavedFrameCount(), 50); + +while (stacks.length) { + stacks.pop(); +} +gc(); + +stacks = null; +gc(); + +assertAboutEq(getSavedFrameCount(), 0); diff --git a/js/src/jit-test/tests/saved-stacks/generators.js b/js/src/jit-test/tests/saved-stacks/generators.js new file mode 100644 index 000000000000..69cd1e4adf38 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/generators.js @@ -0,0 +1,15 @@ +// Test that we can save stacks which have generator frames. + +const { value: frame } = (function iife1() { + return (function* generator() { + yield (function iife2() { + return saveStack(); + }()); + }()).next(); +}()); + +assertEq(frame.functionDisplayName, "iife2"); +assertEq(frame.parent.functionDisplayName, "generator"); +assertEq(frame.parent.parent.functionDisplayName, "iife1"); +assertEq(frame.parent.parent.parent.functionDisplayName, null); +assertEq(frame.parent.parent.parent.parent, null); diff --git a/js/src/jit-test/tests/saved-stacks/get-set.js b/js/src/jit-test/tests/saved-stacks/get-set.js new file mode 100644 index 000000000000..be2e20739937 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/get-set.js @@ -0,0 +1,25 @@ +// Test that we can save stacks with getter and setter function frames. + +function assertStackLengthEq(stack, expectedLength) { + let actual = 0; + while (stack) { + actual++; + stack = stack.parent; + } + assertEq(actual, expectedLength); +} + +const get = { get s() { return saveStack(); } }.s; +assertStackLengthEq(get, 2); + +let set; +try { + ({ + set s(v) { + throw saveStack(); + } + }).s = 1; +} catch (s) { + set = s; +} +assertStackLengthEq(set, 2); diff --git a/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js b/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js new file mode 100644 index 000000000000..ff009bc5b0d2 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js @@ -0,0 +1,21 @@ +// Test that you can't call the SavedFrame constructor and can only use +// SavedFrame's getters on SavedFrame instances. + +load(libdir + "asserts.js"); + +let proto = Object.getPrototypeOf(saveStack()); + +// Can't create new SavedFrame instances by hand. +assertThrowsInstanceOf(() => { + new proto.constructor(); +}, TypeError); + +for (let p of ["source", "line", "column", "functionDisplayName", "parent"]) { + // The getters shouldn't work on the prototype. + assertThrowsInstanceOf(() => proto[p], TypeError); + + // Nor should they work on random objects. + let o = {}; + Object.defineProperty(o, p, Object.getOwnPropertyDescriptor(proto, p)); + assertThrowsInstanceOf(() => o[p], TypeError); +} diff --git a/js/src/jit-test/tests/saved-stacks/native-calls.js b/js/src/jit-test/tests/saved-stacks/native-calls.js new file mode 100644 index 000000000000..9c6a273253db --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/native-calls.js @@ -0,0 +1,12 @@ +// Test that we can save stacks with native code on the stack. + +// Unlike Array.prototype.map, Array.prototype.filter is not self-hosted. +const filter = (function iife() { + try { + [3].filter(n => { throw saveStack() }); + } catch (s) { + return s; + } +}()); + +assertEq(filter.parent.functionDisplayName, "iife"); diff --git a/js/src/jit-test/tests/saved-stacks/principals-01.js b/js/src/jit-test/tests/saved-stacks/principals-01.js new file mode 100644 index 000000000000..1c67956b2df7 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/principals-01.js @@ -0,0 +1,70 @@ +// Test that SavedFrame.prototype.parent gives the next older frame whose +// principals are subsumed by the caller's principals. + +// Given a string of letters |expected|, say "abc", assert that the stack +// contains calls to a series of functions named by the next letter from +// the string, say a, b, and then c. Younger frames appear earlier in +// |expected| than older frames. +function check(expected, stack) { + print("check(" + uneval(expected) + ") against:\n" + stack); + count++; + + while (stack.length && expected.length) { + assertEq(stack.shift(), expected[0]); + expected = expected.slice(1); + } + + if (expected.length > 0) { + throw new Error("Missing frames for: " + expected); + } + if (stack.length > 0 && !stack.every(s => s === null)) { + throw new Error("Unexpected extra frame(s):\n" + stack); + } +} + +// Go from a SavedFrame linked list to an array of function display names. +function extract(stack) { + const results = []; + while (stack) { + results.push(stack.functionDisplayName); + stack = stack.parent; + } + return results; +} + +const low = newGlobal({ principal: 0 }); +const mid = newGlobal({ principal: 0xffff }); +const high = newGlobal({ principal: 0xfffff }); + +var count = 0; + + eval('function a() { check("a", extract(saveStack())); b(); }'); +low .eval('function b() { check("b", extract(saveStack())); c(); }'); +mid .eval('function c() { check("cba", extract(saveStack())); d(); }'); +high.eval('function d() { check("dcba", extract(saveStack())); e(); }'); + eval('function e() { check("edcba", extract(saveStack())); f(); }'); // no principal, so checks skipped +low .eval('function f() { check("fb", extract(saveStack())); g(); }'); +mid .eval('function g() { check("gfecba", extract(saveStack())); h(); }'); +high.eval('function h() { check("hgfedcba", extract(saveStack())); }'); + +// Make everyone's functions visible to each other, as needed. + b = low .b; +low .c = mid .c; +mid .d = high.d; +high.e = e; + f = low .f; +low .g = mid .g; +mid .h = high.h; + +low.check = mid.check = high.check = check; + +// They each must have their own extract so that CCWs don't mess up the +// principals when we ask for the parent property. +low. eval("" + extract); +mid. eval("" + extract); +high.eval("" + extract); + +// Kick the whole process off. +a(); + +assertEq(count, 8); diff --git a/js/src/jit-test/tests/saved-stacks/principals-02.js b/js/src/jit-test/tests/saved-stacks/principals-02.js new file mode 100644 index 000000000000..796c48b06944 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/principals-02.js @@ -0,0 +1,56 @@ +// Test that SavedFrame.prototype.toString only shows frames whose principal is +// subsumed by the caller's principal. + +var count = 0; + +// Given a string of letters |expected|, say "abc", assert that the stack +// contains calls to a series of functions named by the next letter from +// the string, say a, b, and then c. Younger frames appear earlier in +// |expected| than older frames. +function check(expected, stack) { + print("check(" + uneval(expected) + ") against:\n" + stack); + count++; + + // Extract only the function names from the stack trace. Omit the frames + // for the top-level evaluation, if it is present. + const frames = stack + .split("\n") + .filter(f => f.match(/^.@/)) + .map(f => f.replace(/@.*$/g, "")); + + // Check the function names against the expected sequence. + assertEq(frames.length, expected.length); + for (var i = 0; i < expected.length; i++) { + assertEq(frames[i], expected[i]); + } +} + +var low = newGlobal({ principal: 0 }); +var mid = newGlobal({ principal: 0xffff }); +var high = newGlobal({ principal: 0xfffff }); + + eval('function a() { check("a", saveStack().toString()); b(); }'); +low .eval('function b() { check("b", saveStack().toString()); c(); }'); +mid .eval('function c() { check("cba", saveStack().toString()); d(); }'); +high.eval('function d() { check("dcba", saveStack().toString()); e(); }'); + eval('function e() { check("edcba", saveStack().toString()); f(); }'); // no principal, so checks skipped +low .eval('function f() { check("fb", saveStack().toString()); g(); }'); +mid .eval('function g() { check("gfecba", saveStack().toString()); h(); }'); +high.eval('function h() { check("hgfedcba", saveStack().toString()); }'); + +// Make everyone's functions visible to each other, as needed. + b = low .b; +low .c = mid .c; +mid .d = high.d; +high.e = e; + f = low .f; +low .g = mid .g; +mid .h = high.h; + +low.check = mid.check = high.check = check; + +// Kick the whole process off. +a(); + +assertEq(count, 8); + diff --git a/js/src/jit-test/tests/saved-stacks/proxy-handlers.js b/js/src/jit-test/tests/saved-stacks/proxy-handlers.js new file mode 100644 index 000000000000..7ad1f6dc6821 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/proxy-handlers.js @@ -0,0 +1,10 @@ +// Test that we can save stacks with proxy handler frames. + +const stack = (function iife() { + return (new Proxy({}, { + get: function get(t, n, r) { return saveStack(); } + })).stack; +}()); + +assertEq(stack.functionDisplayName, "get"); +assertEq(stack.parent.functionDisplayName, "iife"); diff --git a/js/src/jit-test/tests/saved-stacks/same-stack.js b/js/src/jit-test/tests/saved-stacks/same-stack.js new file mode 100644 index 000000000000..b82ba1c04ab9 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/same-stack.js @@ -0,0 +1,12 @@ +// Test that the same saved stack is only ever allocated once. + +const stacks = []; + +for (let i = 0; i < 10; i++) { + stacks.push(saveStack()); +} + +const s = stacks.pop(); +for (let stack of stacks) { + assertEq(s, stack); +} diff --git a/js/src/jit-test/tests/saved-stacks/self-hosted.js b/js/src/jit-test/tests/saved-stacks/self-hosted.js new file mode 100644 index 000000000000..88f8ce20070c --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/self-hosted.js @@ -0,0 +1,26 @@ +// Test that we can save stacks with self-hosted function frames in them. + +const map = (function () { + return [3].map(n => saveStack()).pop(); +}()); + +assertEq(map.parent.functionDisplayName, "map"); +assertEq(map.parent.source, "self-hosted"); + +const reduce = (function () { + return [3].reduce(() => saveStack(), 3); +}()); + +assertEq(reduce.parent.functionDisplayName, "reduce"); +assertEq(reduce.parent.source, "self-hosted"); + +const forEach = (function () { + try { + [3].forEach(n => { throw saveStack() }); + } catch (s) { + return s; + } +}()); + +assertEq(forEach.parent.functionDisplayName, "forEach"); +assertEq(forEach.parent.source, "self-hosted"); diff --git a/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js b/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js new file mode 100644 index 000000000000..c6b4332dd977 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js @@ -0,0 +1,19 @@ +// Test that parent frames are shared when the older portions of two stacks are +// the same. + +let f1, f2; + +function dos() { + f1 = saveStack(); + f2 = saveStack(); +} + +(function uno() { + dos(); +}()); + + +// Different youngest frames. +assertEq(f1 == f2, false); +// However the parents should be the same. +assertEq(f1.parent, f2.parent); diff --git a/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js b/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js new file mode 100644 index 000000000000..7e55ca31b3d1 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js @@ -0,0 +1,8 @@ +// Test that stringify'ing a saved frame with self-hosted parent frames doesn't +// include the self-hosted parent frame in the output. + +const map = (function () { + return [3].map(n => saveStack()).pop(); +}()); + +assertEq(map.toString().contains("@self-hosted:"), false); diff --git a/js/src/jscompartment.cpp b/js/src/jscompartment.cpp index 65cce0e3f32c..5f1c33d0b69d 100644 --- a/js/src/jscompartment.cpp +++ b/js/src/jscompartment.cpp @@ -113,6 +113,9 @@ JSCompartment::init(JSContext *cx) if (!enumerators) return false; + if (!savedStacks_.init()) + return false; + return debuggees.init(0); } @@ -563,6 +566,7 @@ JSCompartment::sweep(FreeOp *fop, bool releaseTypes) sweepNewTypeObjectTable(newTypeObjects); sweepNewTypeObjectTable(lazyTypeObjects); sweepCallsiteClones(); + savedStacks_.sweep(rt); if (global_ && IsObjectAboutToBeFinalized(global_.unsafeGet())) global_ = nullptr; @@ -665,6 +669,8 @@ JSCompartment::clearTables() newTypeObjects.clear(); if (lazyTypeObjects.initialized()) lazyTypeObjects.clear(); + if (savedStacks_.initialized()) + savedStacks_.clear(); } void @@ -915,7 +921,8 @@ JSCompartment::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf, size_t *shapesCompartmentTables, size_t *crossCompartmentWrappersArg, size_t *regexpCompartment, - size_t *debuggeesSet) + size_t *debuggeesSet, + size_t *savedStacksSet) { *compartmentObject += mallocSizeOf(this); types.addSizeOfExcludingThis(mallocSizeOf, tiAllocationSiteTables, @@ -927,6 +934,7 @@ JSCompartment::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf, *crossCompartmentWrappersArg += crossCompartmentWrappers.sizeOfExcludingThis(mallocSizeOf); *regexpCompartment += regExps.sizeOfExcludingThis(mallocSizeOf); *debuggeesSet += debuggees.sizeOfExcludingThis(mallocSizeOf); + *savedStacksSet += savedStacks_.sizeOfExcludingThis(mallocSizeOf); } void diff --git a/js/src/jscompartment.h b/js/src/jscompartment.h index ed766e6b456c..f1d26d7b1790 100644 --- a/js/src/jscompartment.h +++ b/js/src/jscompartment.h @@ -13,6 +13,7 @@ #include "gc/Zone.h" #include "vm/GlobalObject.h" #include "vm/PIC.h" +#include "vm/SavedStacks.h" namespace js { @@ -198,6 +199,8 @@ struct JSCompartment private: js::ObjectMetadataCallback objectMetadataCallback; + js::SavedStacks savedStacks_; + js::WrapperMap crossCompartmentWrappers; public: @@ -224,7 +227,8 @@ struct JSCompartment size_t *shapesCompartmentTables, size_t *crossCompartmentWrappers, size_t *regexpCompartment, - size_t *debuggeesSet); + size_t *debuggeesSet, + size_t *savedStacksSet); /* * Shared scope property tree, and arena-pool for allocating its nodes. @@ -342,6 +346,8 @@ struct JSCompartment return objectMetadataCallback(cx, obj); } + js::SavedStacks &savedStacks() { return savedStacks_; } + void findOutgoingEdges(js::gc::ComponentFinder &finder); js::DtoaCache dtoaCache; diff --git a/js/src/moz.build b/js/src/moz.build index f6b2eea022e3..96aefd066616 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -179,6 +179,7 @@ UNIFIED_SOURCES += [ 'vm/RegExpObject.cpp', 'vm/RegExpStatics.cpp', 'vm/Runtime.cpp', + 'vm/SavedStacks.cpp', 'vm/ScopeObject.cpp', 'vm/SelfHosting.cpp', 'vm/Shape.cpp', diff --git a/js/src/vm/MemoryMetrics.cpp b/js/src/vm/MemoryMetrics.cpp index 197f43f5b04d..5862324c46c1 100644 --- a/js/src/vm/MemoryMetrics.cpp +++ b/js/src/vm/MemoryMetrics.cpp @@ -259,7 +259,8 @@ StatsCompartmentCallback(JSRuntime *rt, void *data, JSCompartment *compartment) &cStats.shapesMallocHeapCompartmentTables, &cStats.crossCompartmentWrappersTable, &cStats.regexpCompartment, - &cStats.debuggeesSet); + &cStats.debuggeesSet, + &cStats.savedStacksSet); } static void diff --git a/js/src/vm/SavedStacks.cpp b/js/src/vm/SavedStacks.cpp new file mode 100644 index 000000000000..7a07a9af7ecd --- /dev/null +++ b/js/src/vm/SavedStacks.cpp @@ -0,0 +1,512 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +#include "vm/SavedStacks.h" + +#include "jscompartment.h" +#include "jsnum.h" + +#include "vm/GlobalObject.h" +#include "vm/StringBuffer.h" + +#include "jsobjinlines.h" + +using mozilla::AddToHash; +using mozilla::HashString; + +namespace js { + +/* static */ HashNumber +SavedFrame::HashPolicy::hash(const Lookup &lookup) +{ + return AddToHash(HashString(lookup.source->chars(), lookup.source->length()), + lookup.line, + lookup.column, + lookup.functionDisplayName, + SavedFramePtrHasher::hash(lookup.parent), + JSPrincipalsPtrHasher::hash(lookup.principals)); +} + +/* static */ bool +SavedFrame::HashPolicy::match(SavedFrame *existing, const Lookup &lookup) +{ + if (existing->getLine() != lookup.line) + return false; + + if (existing->getColumn() != lookup.column) + return false; + + if (existing->getParent() != lookup.parent) + return false; + + if (existing->getPrincipals() != lookup.principals) + return false; + + JSAtom *source = existing->getSource(); + if (source->length() != lookup.source->length()) + return false; + if (source != lookup.source) + return false; + + JSAtom *functionDisplayName = existing->getFunctionDisplayName(); + if (functionDisplayName) { + if (!lookup.functionDisplayName) + return false; + if (functionDisplayName->length() != lookup.functionDisplayName->length()) + return false; + if (0 != CompareAtoms(functionDisplayName, lookup.functionDisplayName)) + return false; + } else if (lookup.functionDisplayName) { + return false; + } + + return true; +} + +/* static */ void +SavedFrame::HashPolicy::rekey(Key &key, const Key &newKey) +{ + key = newKey; +} + +/* static */ const Class SavedFrame::class_ = { + "SavedFrame", + JSCLASS_HAS_PRIVATE | JSCLASS_IMPLEMENTS_BARRIERS | + JSCLASS_HAS_RESERVED_SLOTS(SavedFrame::JSSLOT_COUNT), + + JS_PropertyStub, // addProperty + JS_DeletePropertyStub, // delProperty + JS_PropertyStub, // getProperty + JS_StrictPropertyStub, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + + SavedFrame::finalize // finalize +}; + +/* static */ void +SavedFrame::finalize(FreeOp *fop, JSObject *obj) +{ + JSPrincipals *p = obj->as().getPrincipals(); + if (p) { + JSRuntime *rt = obj->runtimeFromMainThread(); + JS_DropPrincipals(rt, p); + } +} + +JSAtom * +SavedFrame::getSource() +{ + const Value &v = getReservedSlot(JSSLOT_SOURCE); + JSString *s = v.toString(); + return &s->asAtom(); +} + +size_t +SavedFrame::getLine() +{ + const Value &v = getReservedSlot(JSSLOT_LINE); + return v.toInt32(); +} + +size_t +SavedFrame::getColumn() +{ + const Value &v = getReservedSlot(JSSLOT_COLUMN); + return v.toInt32(); +} + +JSAtom * +SavedFrame::getFunctionDisplayName() +{ + const Value &v = getReservedSlot(JSSLOT_FUNCTIONDISPLAYNAME); + if (v.isNull()) + return nullptr; + JSString *s = v.toString(); + return &s->asAtom(); +} + +SavedFrame * +SavedFrame::getParent() +{ + const Value &v = getReservedSlot(JSSLOT_PARENT); + return v.isObject() ? &v.toObject().as() : nullptr; +} + +JSPrincipals * +SavedFrame::getPrincipals() +{ + const Value &v = getReservedSlot(JSSLOT_PRINCIPALS); + if (v.isUndefined()) + return nullptr; + return static_cast(v.toPrivate()); +} + +void +SavedFrame::initFromLookup(Lookup &lookup) +{ + JS_ASSERT(lookup.source); + JS_ASSERT(getReservedSlot(JSSLOT_SOURCE).isUndefined()); + setReservedSlot(JSSLOT_SOURCE, StringValue(lookup.source)); + + setReservedSlot(JSSLOT_LINE, NumberValue(lookup.line)); + setReservedSlot(JSSLOT_COLUMN, NumberValue(lookup.column)); + setReservedSlot(JSSLOT_FUNCTIONDISPLAYNAME, + lookup.functionDisplayName + ? StringValue(lookup.functionDisplayName) + : NullValue()); + setReservedSlot(JSSLOT_PARENT, ObjectOrNullValue(lookup.parent)); + setReservedSlot(JSSLOT_PRIVATE_PARENT, PrivateValue(lookup.parent)); + + JS_ASSERT(getReservedSlot(JSSLOT_PRINCIPALS).isUndefined()); + if (lookup.principals) + JS_HoldPrincipals(lookup.principals); + setReservedSlot(JSSLOT_PRINCIPALS, PrivateValue(lookup.principals)); +} + +bool +SavedFrame::parentMoved() +{ + const Value &v = getReservedSlot(JSSLOT_PRIVATE_PARENT); + JSObject *p = static_cast(v.toPrivate()); + return p == getParent(); +} + +void +SavedFrame::updatePrivateParent() +{ + setReservedSlot(JSSLOT_PRIVATE_PARENT, PrivateValue(getParent())); +} + +bool +SavedFrame::isSelfHosted() +{ + JSAtom *source = getSource(); + return StringEqualsAscii(source, "self-hosted"); +} + +/* static */ bool +SavedFrame::construct(JSContext *cx, unsigned argc, Value *vp) +{ + JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "SavedFrame"); + return false; +} + +/* static */ SavedFrame * +SavedFrame::checkThis(JSContext *cx, CallArgs &args, const char *fnName) +{ + const Value &thisValue = args.thisv(); + + if (!thisValue.isObject()) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT); + return nullptr; + } + + JSObject &thisObject = thisValue.toObject(); + if (!thisObject.is()) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + SavedFrame::class_.name, fnName, thisObject.getClass()->name); + return nullptr; + } + + // Check for SavedFrame.prototype, which has the same class as SavedFrame + // instances, however doesn't actually represent a captured stack frame. It + // is the only object that is() but doesn't have a source. + if (thisObject.getReservedSlot(JSSLOT_SOURCE).isNull()) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, + SavedFrame::class_.name, fnName, "prototype object"); + return nullptr; + } + + return &thisObject.as(); +} + +// Get the SavedFrame * from the current this value and handle any errors that +// might occur therein. +// +// These parameters must already exist when calling this macro: +// - JSContext *cx +// - unsigned argc +// - Value *vp +// - const char *fnName +// These parameters will be defined after calling this macro: +// - CallArgs args +// - Rooted frame (will be non-null) +#define THIS_SAVEDFRAME(cx, argc, vp, fnName, args, frame) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + Rooted frame(cx, checkThis(cx, args, fnName)); \ + if (!frame) \ + return false + +/* static */ bool +SavedFrame::sourceProperty(JSContext *cx, unsigned argc, Value *vp) +{ + THIS_SAVEDFRAME(cx, argc, vp, "(get source)", args, frame); + args.rval().setString(frame->getSource()); + return true; +} + +/* static */ bool +SavedFrame::lineProperty(JSContext *cx, unsigned argc, Value *vp) +{ + THIS_SAVEDFRAME(cx, argc, vp, "(get line)", args, frame); + uint32_t line = frame->getLine(); + args.rval().setNumber(line); + return true; +} + +/* static */ bool +SavedFrame::columnProperty(JSContext *cx, unsigned argc, Value *vp) +{ + THIS_SAVEDFRAME(cx, argc, vp, "(get column)", args, frame); + uint32_t column = frame->getColumn(); + args.rval().setNumber(column); + return true; +} + +/* static */ bool +SavedFrame::functionDisplayNameProperty(JSContext *cx, unsigned argc, Value *vp) +{ + THIS_SAVEDFRAME(cx, argc, vp, "(get functionDisplayName)", args, frame); + RootedAtom name(cx, frame->getFunctionDisplayName()); + if (name) + args.rval().setString(name); + else + args.rval().setNull(); + return true; +} + +/* static */ bool +SavedFrame::parentProperty(JSContext *cx, unsigned argc, Value *vp) +{ + THIS_SAVEDFRAME(cx, argc, vp, "(get parent)", args, frame); + JSSubsumesOp subsumes = cx->runtime()->securityCallbacks->subsumes; + JSPrincipals *principals = cx->compartment()->principals; + + do + frame = frame->getParent(); + while (frame && principals && subsumes && + !subsumes(principals, frame->getPrincipals())); + + args.rval().setObjectOrNull(frame); + return true; +} + +/* static */ const JSPropertySpec SavedFrame::properties[] = { + JS_PSG("source", SavedFrame::sourceProperty, 0), + JS_PSG("line", SavedFrame::lineProperty, 0), + JS_PSG("column", SavedFrame::columnProperty, 0), + JS_PSG("functionDisplayName", SavedFrame::functionDisplayNameProperty, 0), + JS_PSG("parent", SavedFrame::parentProperty, 0), + JS_PS_END +}; + +/* static */ bool +SavedFrame::toStringMethod(JSContext *cx, unsigned argc, Value *vp) +{ + THIS_SAVEDFRAME(cx, argc, vp, "toString", args, frame); + StringBuffer sb(cx); + JSSubsumesOp subsumes = cx->runtime()->securityCallbacks->subsumes; + JSPrincipals *principals = cx->compartment()->principals; + + do { + if (principals && subsumes && !subsumes(principals, frame->getPrincipals())) + continue; + if (frame->isSelfHosted()) + continue; + + RootedAtom name(cx, frame->getFunctionDisplayName()); + if ((name && !sb.append(name)) + || !sb.append('@') + || !sb.append(frame->getSource()) + || !sb.append(':') + || !NumberValueToStringBuffer(cx, NumberValue(frame->getLine()), sb) + || !sb.append(':') + || !NumberValueToStringBuffer(cx, NumberValue(frame->getColumn()), sb) + || !sb.append('\n')) { + return false; + } + } while ((frame = frame->getParent())); + + args.rval().setString(sb.finishString()); + return true; +} + +/* static */ const JSFunctionSpec SavedFrame::methods[] = { + JS_FN("toString", SavedFrame::toStringMethod, 0, 0), + JS_FS_END +}; + +bool +SavedStacks::init() +{ + return frames.init(); +} + +bool +SavedStacks::saveCurrentStack(JSContext *cx, MutableHandle frame) +{ + JS_ASSERT(initialized()); + JS_ASSERT(&cx->compartment()->savedStacks() == this); + + ScriptFrameIter iter(cx); + return insertFrames(cx, iter, frame); +} + +void +SavedStacks::sweep(JSRuntime *rt) +{ + if (frames.initialized()) { + for (SavedFrame::Set::Enum e(frames); !e.empty(); e.popFront()) { + JSObject *obj = static_cast(e.front()); + JSObject *temp = obj; + + if (IsObjectAboutToBeFinalized(&obj)) { + e.removeFront(); + } else { + SavedFrame *frame = &obj->as(); + bool parentMoved = frame->parentMoved(); + + if (parentMoved) { + frame->updatePrivateParent(); + } + + if (obj != temp || parentMoved) { + Rooted parent(rt, frame->getParent()); + e.rekeyFront(SavedFrame::Lookup(frame->getSource(), + frame->getLine(), + frame->getColumn(), + frame->getFunctionDisplayName(), + parent, + frame->getPrincipals()), + frame); + } + } + } + } + + if (savedFrameProto && IsObjectAboutToBeFinalized(&savedFrameProto)) { + savedFrameProto = nullptr; + } +} + +uint32_t +SavedStacks::count() +{ + JS_ASSERT(initialized()); + return frames.count(); +} + +void +SavedStacks::clear() +{ + frames.clear(); +} + +size_t +SavedStacks::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) +{ + return frames.sizeOfExcludingThis(mallocSizeOf); +} + +bool +SavedStacks::insertFrames(JSContext *cx, ScriptFrameIter &iter, MutableHandle frame) +{ + if (iter.done()) { + frame.set(nullptr); + return true; + } + + ScriptFrameIter thisFrame(iter); + Rooted parentFrame(cx); + if (!insertFrames(cx, ++iter, &parentFrame)) + return false; + + RootedScript script(cx, thisFrame.script()); + RootedFunction callee(cx, thisFrame.maybeCallee()); + const char *filename = script->filename(); + RootedAtom source(cx, Atomize(cx, filename, strlen(filename))); + if (!source) + return false; + uint32_t column; + uint32_t line = PCToLineNumber(script, thisFrame.pc(), &column); + + SavedFrame::Lookup lookup(source, + line, + column, + callee ? callee->displayAtom() : nullptr, + parentFrame, + thisFrame.compartment()->principals); + + frame.set(getOrCreateSavedFrame(cx, lookup)); + return frame.address() != nullptr; +} + +SavedFrame * +SavedStacks::getOrCreateSavedFrame(JSContext *cx, SavedFrame::Lookup &lookup) +{ + SavedFrame::Set::AddPtr p = frames.lookupForAdd(lookup); + if (p) + return *p; + + Rooted frame(cx, createFrameFromLookup(cx, lookup)); + if (!frame) + return nullptr; + + if (!frames.relookupOrAdd(p, lookup, frame)) + return nullptr; + + return frame; +} + +JSObject * +SavedStacks::getOrCreateSavedFramePrototype(JSContext *cx) +{ + if (savedFrameProto) + return savedFrameProto; + + Rooted global(cx, cx->compartment()->maybeGlobal()); + if (!global) + return nullptr; + + savedFrameProto = js_InitClass(cx, global, global->getOrCreateObjectPrototype(cx), + &SavedFrame::class_, SavedFrame::construct, 0, + SavedFrame::properties, SavedFrame::methods, nullptr, nullptr); + // The only object with the SavedFrame::class_ that doesn't have a source + // should be the prototype. + savedFrameProto->setReservedSlot(SavedFrame::JSSLOT_SOURCE, NullValue()); + return savedFrameProto; +} + +SavedFrame * +SavedStacks::createFrameFromLookup(JSContext *cx, SavedFrame::Lookup &lookup) +{ + RootedObject proto(cx, getOrCreateSavedFramePrototype(cx)); + if (!proto) + return nullptr; + + JS_ASSERT(proto->compartment() == cx->compartment()); + + RootedObject global(cx, cx->compartment()->maybeGlobal()); + if (!global) + return nullptr; + + JS_ASSERT(global->compartment() == cx->compartment()); + + RootedObject frameObj(cx, NewObjectWithGivenProto(cx, &SavedFrame::class_, proto, global)); + if (!frameObj) + return nullptr; + + SavedFrame &f = frameObj->as(); + f.initFromLookup(lookup); + + return &f; +} + +} /* namespace js */ diff --git a/js/src/vm/SavedStacks.h b/js/src/vm/SavedStacks.h new file mode 100644 index 000000000000..dc8b9c02e314 --- /dev/null +++ b/js/src/vm/SavedStacks.h @@ -0,0 +1,144 @@ + +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef vm_SavedStacks_h +#define vm_SavedStacks_h + +#include "jscntxt.h" +#include "js/HashTable.h" +#include "vm/Stack.h" + +namespace js { + +class SavedFrame : public JSObject { + friend class SavedStacks; + + public: + static const Class class_; + static void finalize(FreeOp *fop, JSObject *obj); + + // Prototype methods and properties to be exposed to JS. + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + static bool construct(JSContext *cx, unsigned argc, Value *vp); + static bool sourceProperty(JSContext *cx, unsigned argc, Value *vp); + static bool lineProperty(JSContext *cx, unsigned argc, Value *vp); + static bool columnProperty(JSContext *cx, unsigned argc, Value *vp); + static bool functionDisplayNameProperty(JSContext *cx, unsigned argc, Value *vp); + static bool parentProperty(JSContext *cx, unsigned argc, Value *vp); + static bool toStringMethod(JSContext *cx, unsigned argc, Value *vp); + + // Convenient getters for SavedFrame's reserved slots for use from C++. + JSAtom *getSource(); + size_t getLine(); + size_t getColumn(); + JSAtom *getFunctionDisplayName(); + SavedFrame *getParent(); + JSPrincipals *getPrincipals(); + + bool isSelfHosted(); + + struct Lookup; + struct HashPolicy; + + typedef HashSet Set; + + private: + void initFromLookup(Lookup &lookup); + + enum { + // The reserved slots in the SavedFrame class. + JSSLOT_SOURCE, + JSSLOT_LINE, + JSSLOT_COLUMN, + JSSLOT_FUNCTIONDISPLAYNAME, + JSSLOT_PARENT, + JSSLOT_PRINCIPALS, + JSSLOT_PRIVATE_PARENT, + + // The total number of reserved slots in the SavedFrame class. + JSSLOT_COUNT + }; + + // Because we hash the parent pointer, we need to rekey a saved frame + // whenever its parent was relocated by the GC. However, the GC doesn't + // notify us when this occurs. As a work around, we keep a duplicate copy of + // the parent pointer as a private value in a reserved slot. Whenever the + // private value parent pointer doesn't match the regular parent pointer, we + // know that GC moved the parent and we need to update our private value and + // rekey the saved frame in its hash set. These two methods are helpers for + // this process. + bool parentMoved(); + void updatePrivateParent(); + + static SavedFrame *checkThis(JSContext *cx, CallArgs &args, const char *fnName); +}; + +struct SavedFrame::Lookup { + Lookup(JSAtom *source, size_t line, size_t column, JSAtom *functionDisplayName, + Handle parent, JSPrincipals *principals) + : source(source), + line(line), + column(column), + functionDisplayName(functionDisplayName), + parent(parent), + principals(principals) + { + JS_ASSERT(source); + } + + JSAtom *source; + size_t line; + size_t column; + JSAtom *functionDisplayName; + Handle parent; + JSPrincipals *principals; +}; + +struct SavedFrame::HashPolicy +{ + typedef SavedFrame::Lookup Lookup; + typedef PointerHasher SavedFramePtrHasher; + typedef PointerHasher JSPrincipalsPtrHasher; + + static HashNumber hash(const Lookup &lookup); + static bool match(SavedFrame *existing, const Lookup &lookup); + + typedef SavedFrame* Key; + static void rekey(Key &key, const Key &newKey); +}; + +class SavedStacks { + public: + SavedStacks() : frames(), savedFrameProto(nullptr) { } + + bool init(); + bool initialized() const { return frames.initialized(); } + bool saveCurrentStack(JSContext *cx, MutableHandle frame); + void sweep(JSRuntime *rt); + uint32_t count(); + void clear(); + + size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf); + + private: + SavedFrame::Set frames; + JSObject *savedFrameProto; + + bool insertFrames(JSContext *cx, ScriptFrameIter &iter, MutableHandle frame); + SavedFrame *getOrCreateSavedFrame(JSContext *cx, SavedFrame::Lookup &lookup); + // |SavedFrame.prototype| is created lazily and held weakly. It should only + // be accessed through this method. + JSObject *getOrCreateSavedFramePrototype(JSContext *cx); + SavedFrame *createFrameFromLookup(JSContext *cx, SavedFrame::Lookup &lookup); +}; + +} /* namespace js */ + +#endif /* vm_SavedStacks_h */ diff --git a/js/src/vm/Stack.cpp b/js/src/vm/Stack.cpp index ff75a77864b2..b727dd3c71ed 100644 --- a/js/src/vm/Stack.cpp +++ b/js/src/vm/Stack.cpp @@ -684,7 +684,7 @@ FrameIter::FrameIter(const FrameIter &other) : data_(other.data_) #ifdef JS_ION , ionInlineFrames_(other.data_.cx_, - data_.jitFrames_.isScripted() ? &other.ionInlineFrames_ : nullptr) + data_.jitFrames_.isIonJS() ? &other.ionInlineFrames_ : nullptr) #endif { }