From 47721e39651e9c960738562013bbdf290d08eeb4 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Tue, 28 Jul 2015 13:04:56 -0700 Subject: [PATCH] Bug 1028418 - Part 5: Minimize stack walking when capturing SavedFrame stacks with a cache; r=shu --- js/public/RootingAPI.h | 4 + js/src/gc/Rooting.h | 4 + js/src/jsgc.cpp | 1 + js/src/vm/SavedFrame.h | 124 ++++++++++++++++++++++++++ js/src/vm/SavedStacks.cpp | 183 ++++++++++++++++++++++++++++++++------ js/src/vm/SavedStacks.h | 134 +++------------------------- js/src/vm/Stack-inl.h | 38 ++++++++ js/src/vm/Stack.cpp | 9 -- js/src/vm/Stack.h | 161 ++++++++++++++++++++++++++++++++- 9 files changed, 497 insertions(+), 161 deletions(-) create mode 100644 js/src/vm/SavedFrame.h diff --git a/js/public/RootingAPI.h b/js/public/RootingAPI.h index 917b8928d51..cdaeb56430f 100644 --- a/js/public/RootingAPI.h +++ b/js/public/RootingAPI.h @@ -609,6 +609,10 @@ class DispatchWrapper public: // Mimic a pointer type, so that we can drop into Rooted. MOZ_IMPLICIT DispatchWrapper(const T& initial) : tracer(&T::trace), storage(initial) {} + MOZ_IMPLICIT DispatchWrapper(T&& initial) + : tracer(&T::trace), + storage(mozilla::Forward(initial)) + { } T* operator &() { return &storage; } const T* operator &() const { return &storage; } operator T&() { return storage; } diff --git a/js/src/gc/Rooting.h b/js/src/gc/Rooting.h index cc8679b2b88..f6cc72c2422 100644 --- a/js/src/gc/Rooting.h +++ b/js/src/gc/Rooting.h @@ -20,6 +20,7 @@ class ArrayObject; class GlobalObject; class PlainObject; class ScriptSourceObject; +class SavedFrame; class Shape; class ObjectGroup; @@ -33,12 +34,14 @@ typedef JS::Handle HandleLinearString; typedef JS::Handle HandlePropertyName; typedef JS::Handle HandleArrayObject; typedef JS::Handle HandlePlainObject; +typedef JS::Handle HandleSavedFrame; typedef JS::Handle HandleScriptSource; typedef JS::MutableHandle MutableHandleShape; typedef JS::MutableHandle MutableHandleAtom; typedef JS::MutableHandle MutableHandleNativeObject; typedef JS::MutableHandle MutableHandlePlainObject; +typedef JS::MutableHandle MutableHandleSavedFrame; typedef JS::Rooted RootedNativeObject; typedef JS::Rooted RootedShape; @@ -49,6 +52,7 @@ typedef JS::Rooted RootedPropertyName; typedef JS::Rooted RootedArrayObject; typedef JS::Rooted RootedGlobalObject; typedef JS::Rooted RootedPlainObject; +typedef JS::Rooted RootedSavedFrame; typedef JS::Rooted RootedScriptSource; } /* namespace js */ diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index 00ea567edd7..bb4ed6d8f79 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -2609,6 +2609,7 @@ GCRuntime::updatePointersToRelocatedCells(Zone* zone) Debugger::markIncomingCrossCompartmentEdges(&trc); for (CompartmentsInZoneIter c(zone); !c.done(); c.next()) { + c->trace(&trc); WeakMapBase::markAll(c, &trc); if (c->watchpointMap) c->watchpointMap->markAll(&trc); diff --git a/js/src/vm/SavedFrame.h b/js/src/vm/SavedFrame.h new file mode 100644 index 00000000000..9143648ac49 --- /dev/null +++ b/js/src/vm/SavedFrame.h @@ -0,0 +1,124 @@ +/* -*- 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_SavedFrame_h +#define vm_SavedFrame_h + +namespace js { + +class SavedFrame : public NativeObject { + friend class SavedStacks; + + public: + static const Class class_; + static const JSPropertySpec protoAccessors[]; + static const JSFunctionSpec protoFunctions[]; + static const JSFunctionSpec staticFunctions[]; + + // Prototype methods and properties to be exposed to JS. + 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 asyncCauseProperty(JSContext* cx, unsigned argc, Value* vp); + static bool asyncParentProperty(JSContext* cx, unsigned argc, Value* vp); + static bool parentProperty(JSContext* cx, unsigned argc, Value* vp); + static bool toStringMethod(JSContext* cx, unsigned argc, Value* vp); + + static void finalize(FreeOp* fop, JSObject* obj); + + // Convenient getters for SavedFrame's reserved slots for use from C++. + JSAtom* getSource(); + uint32_t getLine(); + uint32_t getColumn(); + JSAtom* getFunctionDisplayName(); + JSAtom* getAsyncCause(); + SavedFrame* getParent(); + JSPrincipals* getPrincipals(); + + bool isSelfHosted(); + + static bool isSavedFrameAndNotProto(JSObject& obj) { + return obj.is() && + !obj.as().getReservedSlot(JSSLOT_SOURCE).isNull(); + } + + struct Lookup; + struct HashPolicy; + + typedef HashSet, + HashPolicy, + SystemAllocPolicy> Set; + + class AutoLookupVector; + + class MOZ_STACK_CLASS HandleLookup { + friend class AutoLookupVector; + + Lookup& lookup; + + explicit HandleLookup(Lookup& lookup) : lookup(lookup) { } + + public: + inline Lookup& get() { return lookup; } + inline Lookup* operator->() { return &lookup; } + }; + + private: + static bool finishSavedFrameInit(JSContext* cx, HandleObject ctor, HandleObject proto); + void initFromLookup(HandleLookup lookup); + + enum { + // The reserved slots in the SavedFrame class. + JSSLOT_SOURCE, + JSSLOT_LINE, + JSSLOT_COLUMN, + JSSLOT_FUNCTIONDISPLAYNAME, + JSSLOT_ASYNCCAUSE, + 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 bool checkThis(JSContext* cx, CallArgs& args, const char* fnName, + MutableHandleObject frame); +}; + +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 ReadBarriered Key; + static void rekey(Key& key, const Key& newKey); +}; + +// Assert that if the given object is not null, that it must be either a +// SavedFrame object or wrapper (Xray or CCW) around a SavedFrame object. +inline void AssertObjectIsSavedFrameOrWrapper(JSContext* cx, HandleObject stack); + +} // namespace js + +#endif // vm_SavedFrame_h diff --git a/js/src/vm/SavedStacks.cpp b/js/src/vm/SavedStacks.cpp index 481f92fae75..45976d93c29 100644 --- a/js/src/vm/SavedStacks.cpp +++ b/js/src/vm/SavedStacks.cpp @@ -8,13 +8,12 @@ #include "mozilla/Attributes.h" #include "mozilla/DebugOnly.h" -#include "mozilla/Maybe.h" +#include "mozilla/Move.h" #include #include #include "jsapi.h" -#include "jscntxt.h" #include "jscompartment.h" #include "jsfriendapi.h" #include "jshashutil.h" @@ -33,11 +32,15 @@ #include "jscntxtinlines.h" #include "vm/NativeObject-inl.h" +#include "vm/Stack-inl.h" using mozilla::AddToHash; using mozilla::DebugOnly; using mozilla::HashString; using mozilla::Maybe; +using mozilla::Move; +using mozilla::Nothing; +using mozilla::Some; namespace js { @@ -46,19 +49,110 @@ namespace js { */ const unsigned ASYNC_STACK_MAX_FRAME_COUNT = 60; +/* static */ Maybe +LiveSavedFrameCache::getFramePtr(FrameIter& iter) +{ + if (iter.hasUsableAbstractFramePtr()) + return Some(FramePtr(iter.abstractFramePtr())); + + if (iter.isPhysicalIonFrame()) + return Some(FramePtr(iter.physicalIonFrame())); + + return Nothing(); +} + +/* static */ void +LiveSavedFrameCache::trace(LiveSavedFrameCache* cache, JSTracer* trc) +{ + if (!cache->initialized()) + return; + + for (auto* entry = cache->frames->begin(); entry < cache->frames->end(); entry++) { + TraceEdge(trc, + &entry->savedFrame, + "LiveSavedFrameCache::frames SavedFrame"); + } +} + +bool +LiveSavedFrameCache::insert(JSContext* cx, FramePtr& framePtr, jsbytecode* pc, + HandleSavedFrame savedFrame) +{ + MOZ_ASSERT(initialized()); + + if (!frames->emplaceBack(framePtr, pc, savedFrame)) { + ReportOutOfMemory(cx); + return false; + } + + // Safe to dereference the cache key because the stack frames are still + // live. After this point, they should never be dereferenced again. + if (framePtr.is()) + framePtr.as().setHasCachedSavedFrame(); + else + framePtr.as()->setHasCachedSavedFrame(); + + return true; +} + +void +LiveSavedFrameCache::find(JSContext* cx, FrameIter& frameIter, MutableHandleSavedFrame frame) const +{ + MOZ_ASSERT(initialized()); + MOZ_ASSERT(!frameIter.done()); + MOZ_ASSERT(frameIter.hasCachedSavedFrame()); + + Maybe maybeFramePtr = getFramePtr(frameIter); + MOZ_ASSERT(maybeFramePtr.isSome()); + + FramePtr framePtr(*maybeFramePtr); + jsbytecode* pc = frameIter.pc(); + size_t numberStillValid = 0; + + frame.set(nullptr); + for (auto* p = frames->begin(); p < frames->end(); p++) { + numberStillValid++; + if (framePtr == p->framePtr && pc == p->pc) { + frame.set(p->savedFrame); + break; + } + } + + if (!frame) { + frames->clear(); + return; + } + + MOZ_ASSERT(0 < numberStillValid && numberStillValid <= frames->length()); + + if (frame->compartment() != cx->compartment()) { + frame.set(nullptr); + numberStillValid--; + } + + // Everything after the cached SavedFrame are stale younger frames we have + // since popped. + frames->shrinkBy(frames->length() - numberStillValid); +} + struct SavedFrame::Lookup { Lookup(JSAtom* source, uint32_t line, uint32_t column, JSAtom* functionDisplayName, JSAtom* asyncCause, SavedFrame* parent, - JSPrincipals* principals) + JSPrincipals* principals, Maybe framePtr, jsbytecode* pc, + Activation* activation) : source(source), line(line), column(column), functionDisplayName(functionDisplayName), asyncCause(asyncCause), parent(parent), - principals(principals) + principals(principals), + framePtr(framePtr), + pc(pc), + activation(activation) { MOZ_ASSERT(source); + MOZ_ASSERT(activation); } explicit Lookup(SavedFrame& savedFrame) @@ -68,19 +162,28 @@ struct SavedFrame::Lookup { functionDisplayName(savedFrame.getFunctionDisplayName()), asyncCause(savedFrame.getAsyncCause()), parent(savedFrame.getParent()), - principals(savedFrame.getPrincipals()) + principals(savedFrame.getPrincipals()), + framePtr(Nothing()), + pc(nullptr), + activation(nullptr) { MOZ_ASSERT(source); } - JSAtom* source; - uint32_t line; - uint32_t column; - JSAtom* functionDisplayName; - JSAtom* asyncCause; - SavedFrame* parent; + JSAtom* source; + uint32_t line; + uint32_t column; + JSAtom* functionDisplayName; + JSAtom* asyncCause; + SavedFrame* parent; JSPrincipals* principals; + // These are used only by the LiveSavedFrameCache and not used for identity or + // hashing. + Maybe framePtr; + jsbytecode* pc; + Activation* activation; + void trace(JSTracer* trc) { TraceManuallyBarrieredEdge(trc, &source, "SavedFrame::Lookup::source"); if (functionDisplayName) { @@ -399,8 +502,7 @@ SavedFrame::checkThis(JSContext* cx, CallArgs& args, const char* fnName, const Value& thisValue = args.thisv(); if (!thisValue.isObject()) { - JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT, - InformalValueTypeName(thisValue)); + JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT, InformalValueTypeName(thisValue)); return false; } @@ -814,7 +916,7 @@ SavedStacks::saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame, unsi MOZ_ASSERT(initialized()); assertSameCompartment(cx, this); - if (creatingSavedFrame) { + if (creatingSavedFrame || cx->isExceptionPending()) { frame.set(nullptr); return true; } @@ -855,13 +957,12 @@ SavedStacks::sweep(JSRuntime* rt) void SavedStacks::trace(JSTracer* trc) { - if (!pcLocationMap.initialized()) - return; - - // Mark each of the source strings in our pc to location cache. - for (PCLocationMap::Enum e(pcLocationMap); !e.empty(); e.popFront()) { - LocationValue& loc = e.front().value(); - TraceEdge(trc, &loc.source, "SavedStacks::PCLocationMap's memoized script source name"); + if (pcLocationMap.initialized()) { + // Mark each of the source strings in our pc to location cache. + for (PCLocationMap::Enum e(pcLocationMap); !e.empty(); e.popFront()) { + LocationValue& loc = e.front().value(); + TraceEdge(trc, &loc.source, "SavedStacks::PCLocationMap's memoized script source name"); + } } } @@ -906,6 +1007,8 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram Activation* asyncActivation = nullptr; RootedSavedFrame asyncStack(cx, nullptr); RootedString asyncCause(cx, nullptr); + bool parentIsInCache = false; + RootedSavedFrame cachedFrame(cx, nullptr); // Accumulate the vector of Lookup objects in |stackChain|. SavedFrame::AutoLookupVector stackChain(cx); @@ -943,6 +1046,11 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram return false; } + // The bit set means that the next older parent (frame, pc) pair *must* + // be in the cache. + if (maxFrameCount == 0) + parentIsInCache = iter.hasCachedSavedFrame(); + auto displayAtom = iter.isNonEvalFunctionFrame() ? iter.functionDisplayAtom() : nullptr; if (!stackChain->emplaceBack(location->source, location->line, @@ -950,7 +1058,10 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram displayAtom, nullptr, nullptr, - iter.compartment()->principals())) + iter.compartment()->principals(), + LiveSavedFrameCache::getFramePtr(iter), + iter.pc(), + &activation)) { ReportOutOfMemory(cx); return false; @@ -958,10 +1069,6 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram ++iter; - // If maxFrameCount is zero there's no limit on the number of frames. - if (maxFrameCount == 0) - continue; - if (maxFrameCount == 1) { // The frame we just saved was the last one we were asked to save. // If we had an async stack, ensure we don't use any of its frames. @@ -969,13 +1076,29 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram break; } + if (parentIsInCache && + !iter.done() && + iter.hasCachedSavedFrame()) + { + auto* cache = activation.getLiveSavedFrameCache(cx); + if (!cache) + return false; + cache->find(cx, iter, &cachedFrame); + if (cachedFrame) + break; + } + + // If maxFrameCount is zero there's no limit on the number of frames. + if (maxFrameCount == 0) + continue; + maxFrameCount--; } // Limit the depth of the async stack, if any, and ensure that the // SavedFrame instances we use are stored in the same compartment as the // rest of the synchronous stack chain. - RootedSavedFrame parentFrame(cx, nullptr); + RootedSavedFrame parentFrame(cx, cachedFrame); if (asyncStack && !adoptAsyncStack(cx, asyncStack, asyncCause, &parentFrame, maxFrameCount)) return false; @@ -987,6 +1110,12 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram parentFrame.set(getOrCreateSavedFrame(cx, lookup)); if (!parentFrame) return false; + + if (maxFrameCount == 0 && lookup->framePtr && parentFrame != cachedFrame) { + auto* cache = lookup->activation->getLiveSavedFrameCache(cx); + if (!cache || !cache->insert(cx, *lookup->framePtr, lookup->pc, parentFrame)) + return false; + } } frame.set(parentFrame); diff --git a/js/src/vm/SavedStacks.h b/js/src/vm/SavedStacks.h index 61857965c11..6d6ea4e1c9b 100644 --- a/js/src/vm/SavedStacks.h +++ b/js/src/vm/SavedStacks.h @@ -11,8 +11,11 @@ #include "jsmath.h" #include "jswrapper.h" #include "js/HashTable.h" +#include "vm/SavedFrame.h" #include "vm/Stack.h" +namespace js { + // # Saved Stacks // // The `SavedStacks` class provides a compact way to capture and save JS stacks @@ -142,123 +145,6 @@ // because the cx's current compartment's principals do not subsume A's captured // principals. - -namespace js { - -class SavedFrame; -typedef JS::Handle HandleSavedFrame; -typedef JS::MutableHandle MutableHandleSavedFrame; -typedef JS::Rooted RootedSavedFrame; - -class SavedFrame : public NativeObject { - friend class SavedStacks; - - public: - static const Class class_; - static void finalize(FreeOp* fop, JSObject* obj); - static const JSPropertySpec protoAccessors[]; - static const JSFunctionSpec protoFunctions[]; - static const JSFunctionSpec staticFunctions[]; - - // Prototype methods and properties to be exposed to JS. - 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 asyncCauseProperty(JSContext* cx, unsigned argc, Value* vp); - static bool asyncParentProperty(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(); - uint32_t getLine(); - uint32_t getColumn(); - JSAtom* getFunctionDisplayName(); - JSAtom* getAsyncCause(); - SavedFrame* getParent(); - JSPrincipals* getPrincipals(); - - bool isSelfHosted(); - - static bool isSavedFrameAndNotProto(JSObject& obj) { - return obj.is() && - !obj.as().getReservedSlot(JSSLOT_SOURCE).isNull(); - } - - struct Lookup; - struct HashPolicy; - - typedef HashSet, - HashPolicy, - SystemAllocPolicy> Set; - - class AutoLookupVector; - - class MOZ_STACK_CLASS HandleLookup { - friend class AutoLookupVector; - - Lookup& lookup; - - explicit HandleLookup(Lookup& lookup) : lookup(lookup) { } - - public: - inline Lookup& get() { return lookup; } - inline Lookup* operator->() { return &lookup; } - }; - - private: - static bool finishSavedFrameInit(JSContext* cx, HandleObject ctor, HandleObject proto); - void initFromLookup(HandleLookup lookup); - - enum { - // The reserved slots in the SavedFrame class. - JSSLOT_SOURCE, - JSSLOT_LINE, - JSSLOT_COLUMN, - JSSLOT_FUNCTIONDISPLAYNAME, - JSSLOT_ASYNCCAUSE, - 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 bool checkThis(JSContext* cx, CallArgs& args, const char* fnName, - MutableHandleObject frame); -}; - -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 ReadBarriered Key; - static void rekey(Key& key, const Key& newKey); -}; - -// Assert that if the given object is not null, that it must be either a -// SavedFrame object or wrapper (Xray or CCW) around a SavedFrame object. -inline void AssertObjectIsSavedFrameOrWrapper(JSContext* cx, HandleObject stack); - class SavedStacks { friend JSObject* SavedStacksMetadataCallback(JSContext* cx, JSObject* target); @@ -309,15 +195,15 @@ class SavedStacks { } }; - bool insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFrame frame, - unsigned maxFrameCount = 0); - bool adoptAsyncStack(JSContext* cx, HandleSavedFrame asyncStack, - HandleString asyncCause, - MutableHandleSavedFrame adoptedStack, - unsigned maxFrameCount); + bool insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFrame frame, + unsigned maxFrameCount = 0); + bool adoptAsyncStack(JSContext* cx, HandleSavedFrame asyncStack, + HandleString asyncCause, + MutableHandleSavedFrame adoptedStack, + unsigned maxFrameCount); SavedFrame* getOrCreateSavedFrame(JSContext* cx, SavedFrame::HandleLookup lookup); SavedFrame* createFrameFromLookup(JSContext* cx, SavedFrame::HandleLookup lookup); - void chooseSamplingProbability(JSContext* cx); + void chooseSamplingProbability(JSContext* cx); // Cache for memoizing PCToLineNumber lookups. diff --git a/js/src/vm/Stack-inl.h b/js/src/vm/Stack-inl.h index e72a1c61b85..bc53fd97741 100644 --- a/js/src/vm/Stack-inl.h +++ b/js/src/vm/Stack-inl.h @@ -887,6 +887,7 @@ Activation::Activation(JSContext* cx, Kind kind) prevProfiling_(prev_ ? prev_->mostRecentProfiling() : nullptr), savedFrameChain_(0), hideScriptedCallerCount_(0), + frameCache_(cx), asyncStack_(cx, cx->runtime_->asyncStackForNewActivations), asyncCause_(cx, cx->runtime_->asyncCauseForNewActivations), asyncCallIsExplicit_(cx->runtime_->asyncCallIsExplicit), @@ -933,6 +934,13 @@ Activation::mostRecentProfiling() return prevProfiling_; } +inline LiveSavedFrameCache* +Activation::getLiveSavedFrameCache(JSContext* cx) { + if (!frameCache_.get().initialized() && !frameCache_.get().init(cx)) + return nullptr; + return frameCache_.address(); +} + InterpreterActivation::InterpreterActivation(RunState& state, JSContext* cx, InterpreterFrame* entryFrame) : Activation(cx, Interpreter), @@ -1010,6 +1018,36 @@ AsmJSActivation::cx() return cx_->asJSContext(); } +inline bool +FrameIter::hasCachedSavedFrame() const +{ + if (isAsmJS()) + return false; + + if (hasUsableAbstractFramePtr()) + return abstractFramePtr().hasCachedSavedFrame(); + + MOZ_ASSERT(data_.jitFrames_.isIonScripted()); + // SavedFrame caching is done at the physical frame granularity (rather than + // for each inlined frame) for ion. Therefore, it is impossible to have a + // cached SavedFrame if this frame is not a physical frame. + return isPhysicalIonFrame() && data_.jitFrames_.current()->hasCachedSavedFrame(); +} + +inline void +FrameIter::setHasCachedSavedFrame() +{ + MOZ_ASSERT(!isAsmJS()); + + if (hasUsableAbstractFramePtr()) { + abstractFramePtr().setHasCachedSavedFrame(); + return; + } + + MOZ_ASSERT(isPhysicalIonFrame()); + data_.jitFrames_.current()->setHasCachedSavedFrame(); +} + } /* namespace js */ #endif /* vm_Stack_inl_h */ diff --git a/js/src/vm/Stack.cpp b/js/src/vm/Stack.cpp index fc78c2f331d..334a7ec6752 100644 --- a/js/src/vm/Stack.cpp +++ b/js/src/vm/Stack.cpp @@ -471,15 +471,6 @@ InterpreterStack::pushExecuteFrame(JSContext* cx, HandleScript script, const Val /*****************************************************************************/ -bool -FrameIter::hasCachedSavedFrame(JSContext* cx, bool* hasCachedSavedFramep) -{ - if (isIon() && !ensureHasRematerializedFrame(cx)) - return false; - *hasCachedSavedFramep = abstractFramePtr().hasCachedSavedFrame(); - return true; -} - void FrameIter::popActivation() { diff --git a/js/src/vm/Stack.h b/js/src/vm/Stack.h index 18f774a4e18..cc17cb13665 100644 --- a/js/src/vm/Stack.h +++ b/js/src/vm/Stack.h @@ -8,17 +8,22 @@ #define vm_Stack_h #include "mozilla/Atomics.h" +#include "mozilla/Maybe.h" #include "mozilla/MemoryReporting.h" +#include "mozilla/Variant.h" #include "jsfun.h" #include "jsscript.h" #include "jsutil.h" #include "asmjs/AsmJSFrameIterator.h" +#include "gc/Rooting.h" #include "jit/JitFrameIterator.h" #ifdef CHECK_OSIPOINT_REGISTERS #include "jit/Registers.h" // for RegisterDump #endif +#include "js/RootingAPI.h" +#include "vm/SavedFrame.h" struct JSCompartment; @@ -34,6 +39,7 @@ class ArgumentsObject; class AsmJSModule; class InterpreterRegs; class CallObject; +class FrameIter; class ScopeObject; class ScriptFrameIter; class SPSProfiler; @@ -44,6 +50,10 @@ class ScopeCoordinate; class SavedFrame; +namespace jit { +class CommonFrameLayout; +} + // VM stack layout // // A JSRuntime's stack consists of a linked list of activations. Every activation @@ -1108,6 +1118,128 @@ struct DefaultHasher { /*****************************************************************************/ +// SavedFrame caching to minimize stack walking. +// +// SavedFrames are hash consed to minimize expensive (with regards to both space +// and time) allocations in the face of many stack frames that tend to share the +// same older tail frames. Despite that, in scenarios where we are frequently +// saving the same or similar stacks, such as when the Debugger's allocation +// site tracking is enabled, these older stack frames still get walked +// repeatedly just to create the lookup structs to find their corresponding +// SavedFrames in the hash table. This stack walking is slow, and we would like +// to minimize it. +// +// We have reserved a bit on most of SpiderMonkey's various frame +// representations (the exceptions being asm and inlined ion frames). As we +// create SavedFrame objects for live stack frames in SavedStacks::insertFrames, +// we set this bit and append the SavedFrame object to the cache. As we walk the +// stack, if we encounter a frame that has this bit set, that indicates that we +// have already captured a SavedFrame object for the given stack frame (but not +// necessarily the current pc) during a previous call to insertFrames. We know +// that the frame's parent was also captured and has its bit set as well, but +// additionally we know the parent was captured at its current pc. For the +// parent, rather than continuing the expensive stack walk, we do a quick and +// cache-friendly linear search through the frame cache. Upon finishing search +// through the frame cache, stale entries are removed. +// +// The frame cache maintains the invariant that its first E[0] .. E[j-1] +// entries are live and sorted from oldest to younger frames, where 0 < j < n +// and n = the length of the cache. When searching the cache, we require +// that we are considering the youngest live frame whose bit is set. Every +// cache entry E[i] where i >= j is a stale entry. Consider the following +// scenario: +// +// P > Q > R > S Initial stack, bits not set. +// P* > Q* > R* > S* Capture a SavedFrame stack, set bits. +// P* > Q* > R* Return from S. +// P* > Q* Return from R. +// P* > Q* > T Call T, its bit is not set. +// +// The frame cache was populated with [P, Q, R, S] when we captured a +// SavedFrame stack, but because we returned from frames R and S, their +// entries in the frame cache are now stale. This fact is unbeknownst to us +// because we do not observe frame pops. Upon capturing a second stack, we +// start stack walking at the youngest frame T, which does not have its bit +// set and must take the hash table lookup slow path rather than the frame +// cache short circuit. Next we proceed to Q and find that it has its bit +// set, and it is therefore the youngest live frame with its bit set. We +// search through the frame cache from oldest to youngest and find the cache +// entry matching Q. We know that T is the next younger live frame from Q +// and that T does not have an entry in the frame cache because its bit was +// not set. Therefore, we have found entry E[j-1] and the subsequent entries +// are stale and should be purged from the frame cache. +// +// We have a LiveSavedFrameCache for each activation to minimize the number of +// entries that must be scanned through, and to avoid the headaches of +// maintaining a cache for each compartment and invalidating stale cache entries +// in the presence of cross-compartment calls. +class LiveSavedFrameCache : public JS::StaticTraceable +{ + public: + using FramePtr = mozilla::Variant; + + private: + struct Entry + { + FramePtr framePtr; + jsbytecode* pc; + RelocatablePtr savedFrame; + + Entry(FramePtr& framePtr, jsbytecode* pc, SavedFrame* savedFrame) + : framePtr(framePtr) + , pc(pc) + , savedFrame(savedFrame) + { } + }; + + using EntryVector = Vector; + EntryVector* frames; + + LiveSavedFrameCache(const LiveSavedFrameCache&) = delete; + LiveSavedFrameCache& operator=(const LiveSavedFrameCache&) = delete; + + public: + explicit LiveSavedFrameCache() : frames(nullptr) { } + + LiveSavedFrameCache(LiveSavedFrameCache&& rhs) + : frames(rhs.frames) + { + MOZ_ASSERT(this != &rhs, "self-move disallowed"); + rhs.frames = nullptr; + } + + ~LiveSavedFrameCache() { + if (frames) { + js_delete(frames); + frames = nullptr; + } + } + + bool initialized() const { return !!frames; } + bool init(JSContext* cx) { + frames = js_new(); + if (!frames) { + JS_ReportOutOfMemory(cx); + return false; + } + return true; + } + + static mozilla::Maybe getFramePtr(FrameIter& iter); + static void trace(LiveSavedFrameCache* cache, JSTracer* trc); + + void find(JSContext* cx, FrameIter& frameIter, MutableHandleSavedFrame frame) const; + bool insert(JSContext* cx, FramePtr& framePtr, jsbytecode* pc, HandleSavedFrame savedFrame); +}; + +static_assert(sizeof(LiveSavedFrameCache) == sizeof(uintptr_t), + "Every js::Activation has a LiveSavedFrameCache, so we need to be pretty careful " + "about avoiding bloat. If you're adding members to LiveSavedFrameCache, maybe you " + "should consider figuring out a way to make js::Activation have a " + "LiveSavedFrameCache* instead of a Rooted."); + +/*****************************************************************************/ + class InterpreterActivation; class AsmJSActivation; @@ -1136,6 +1268,10 @@ class Activation // data structures instead. size_t hideScriptedCallerCount_; + // The cache of SavedFrame objects we have already captured when walking + // this activation's stack. + Rooted frameCache_; + // Youngest saved frame of an async stack that will be iterated during stack // capture in place of the actual stack of previous activations. Note that // the stack of this activation is captured entirely before this is used. @@ -1240,6 +1376,8 @@ class Activation return asyncCallIsExplicit_; } + inline LiveSavedFrameCache* getLiveSavedFrameCache(JSContext* cx); + private: Activation(const Activation& other) = delete; void operator=(const Activation& other) = delete; @@ -1730,6 +1868,7 @@ class FrameIter bool isAsmJS() const { MOZ_ASSERT(!done()); return data_.state_ == ASMJS; } inline bool isIon() const; inline bool isBaseline() const; + inline bool isPhysicalIonFrame() const; bool isFunctionFrame() const; bool isGlobalFrame() const; @@ -1737,7 +1876,9 @@ class FrameIter bool isNonEvalFunctionFrame() const; bool hasArgs() const { return isNonEvalFunctionFrame(); } - bool hasCachedSavedFrame(JSContext* cx, bool* hasCachedSavedFramep); + // These two methods may not be called with asm frames. + inline bool hasCachedSavedFrame() const; + inline void setHasCachedSavedFrame(); ScriptSource* scriptSource() const; const char* scriptFilename() const; @@ -1826,6 +1967,9 @@ class FrameIter // This can only be called when isInterp(): inline InterpreterFrame* interpFrame() const; + // This can only be called when isPhysicalIonFrame(): + inline jit::CommonFrameLayout* physicalIonFrame() const; + private: Data data_; jit::InlineFrameIterator ionInlineFrames_; @@ -2034,5 +2178,20 @@ FrameIter::interpFrame() const return data_.interpFrames_.frame(); } +inline bool +FrameIter::isPhysicalIonFrame() const +{ + return isJit() && + data_.jitFrames_.isIonScripted() && + ionInlineFrames_.frameNo() == 0; +} + +inline jit::CommonFrameLayout* +FrameIter::physicalIonFrame() const +{ + MOZ_ASSERT(isPhysicalIonFrame()); + return data_.jitFrames_.current(); +} + } /* namespace js */ #endif /* vm_Stack_h */