/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * 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 "jit/JSJitFrameIter-inl.h" #include "jit/CalleeToken.h" #include "jit/IonScript.h" #include "jit/JitcodeMap.h" #include "jit/JitFrames.h" #include "jit/JitRuntime.h" #include "jit/JitScript.h" #include "jit/MacroAssembler.h" // js::jit::Assembler::GetPointer #include "jit/SafepointIndex.h" #include "jit/Safepoints.h" #include "jit/ScriptFromCalleeToken.h" #include "jit/VMFunctions.h" #include "js/friend/DumpFunctions.h" // js::DumpObject, js::DumpValue #include "vm/JitActivation.h" #include "vm/JSScript-inl.h" using namespace js; using namespace js::jit; JSJitFrameIter::JSJitFrameIter(const JitActivation* activation) : JSJitFrameIter(activation, FrameType::Exit, activation->jsExitFP()) {} JSJitFrameIter::JSJitFrameIter(const JitActivation* activation, FrameType frameType, uint8_t* fp) : current_(fp), type_(frameType), resumePCinCurrentFrame_(nullptr), cachedSafepointIndex_(nullptr), activation_(activation) { MOZ_ASSERT(type_ == FrameType::JSJitToWasm || type_ == FrameType::Exit); if (activation_->bailoutData()) { current_ = activation_->bailoutData()->fp(); type_ = FrameType::Bailout; } else { MOZ_ASSERT(!TlsContext.get()->inUnsafeCallWithABI); } } bool JSJitFrameIter::checkInvalidation() const { IonScript* dummy; return checkInvalidation(&dummy); } bool JSJitFrameIter::checkInvalidation(IonScript** ionScriptOut) const { JSScript* script = this->script(); if (isBailoutJS()) { *ionScriptOut = activation_->bailoutData()->ionScript(); return !script->hasIonScript() || script->ionScript() != *ionScriptOut; } uint8_t* returnAddr = resumePCinCurrentFrame(); // N.B. the current IonScript is not the same as the frame's // IonScript if the frame has since been invalidated. bool invalidated = !script->hasIonScript() || !script->ionScript()->containsReturnAddress(returnAddr); if (!invalidated) { return false; } int32_t invalidationDataOffset = ((int32_t*)returnAddr)[-1]; uint8_t* ionScriptDataOffset = returnAddr + invalidationDataOffset; IonScript* ionScript = (IonScript*)Assembler::GetPointer(ionScriptDataOffset); MOZ_ASSERT(ionScript->containsReturnAddress(returnAddr)); *ionScriptOut = ionScript; return true; } CalleeToken JSJitFrameIter::calleeToken() const { return ((JitFrameLayout*)current_)->calleeToken(); } JSFunction* JSJitFrameIter::callee() const { MOZ_ASSERT(isScripted()); MOZ_ASSERT(isFunctionFrame()); return CalleeTokenToFunction(calleeToken()); } JSFunction* JSJitFrameIter::maybeCallee() const { if (isScripted() && isFunctionFrame()) { return callee(); } return nullptr; } bool JSJitFrameIter::isBareExit() const { if (type_ != FrameType::Exit) { return false; } return exitFrame()->isBareExit(); } bool JSJitFrameIter::isUnwoundJitExit() const { if (type_ != FrameType::Exit) { return false; } return exitFrame()->isUnwoundJitExit(); } bool JSJitFrameIter::isFunctionFrame() const { return CalleeTokenIsFunction(calleeToken()); } JSScript* JSJitFrameIter::script() const { MOZ_ASSERT(isScripted()); JSScript* script = ScriptFromCalleeToken(calleeToken()); MOZ_ASSERT(script); return script; } JSScript* JSJitFrameIter::maybeForwardedScript() const { MOZ_ASSERT(isScripted()); if (isBaselineJS()) { return MaybeForwardedScriptFromCalleeToken(baselineFrame()->calleeToken()); } JSScript* script = MaybeForwardedScriptFromCalleeToken(calleeToken()); MOZ_ASSERT(script); return script; } void JSJitFrameIter::baselineScriptAndPc(JSScript** scriptRes, jsbytecode** pcRes) const { MOZ_ASSERT(isBaselineJS()); JSScript* script = this->script(); if (scriptRes) { *scriptRes = script; } MOZ_ASSERT(pcRes); // The Baseline Interpreter stores the bytecode pc in the frame. if (baselineFrame()->runningInInterpreter()) { MOZ_ASSERT(baselineFrame()->interpreterScript() == script); *pcRes = baselineFrame()->interpreterPC(); return; } // There must be a BaselineScript with a RetAddrEntry for the current return // address. uint8_t* retAddr = resumePCinCurrentFrame(); const RetAddrEntry& entry = script->baselineScript()->retAddrEntryFromReturnAddress(retAddr); *pcRes = entry.pc(script); } Value* JSJitFrameIter::actualArgs() const { return jsFrame()->actualArgs(); } uint8_t* JSJitFrameIter::prevFp() const { return current()->callerFramePtr(); } // Compute the size of a Baseline frame excluding pushed VMFunction arguments or // callee frame headers. This is used to calculate the number of Value slots in // the frame. The caller asserts this matches BaselineFrame::debugFrameSize. static uint32_t ComputeBaselineFrameSize(const JSJitFrameIter& frame) { MOZ_ASSERT(frame.prevType() == FrameType::BaselineJS); uint32_t frameSize = frame.current()->callerFramePtr() - frame.fp(); if (frame.isBaselineStub()) { return frameSize - BaselineStubFrameLayout::Size(); } // Note: an UnwoundJit exit frame is a JitFrameLayout that was turned into an // ExitFrameLayout by EnsureUnwoundJitExitFrame. We have to use the original // header size here because that's what we have on the stack. if (frame.isScripted() || frame.isUnwoundJitExit()) { return frameSize - JitFrameLayout::Size(); } if (frame.isExitFrame()) { frameSize -= ExitFrameLayout::Size(); if (frame.exitFrame()->isWrapperExit()) { const VMFunctionData* data = frame.exitFrame()->footer()->function(); frameSize -= data->explicitStackSlots() * sizeof(void*); } return frameSize; } MOZ_CRASH("Unexpected frame"); } void JSJitFrameIter::operator++() { MOZ_ASSERT(!isEntry()); // Compute BaselineFrame size. In debug builds this is equivalent to // BaselineFrame::debugFrameSize_. This is asserted at the end of this method. if (current()->prevType() == FrameType::BaselineJS) { uint32_t frameSize = ComputeBaselineFrameSize(*this); baselineFrameSize_ = mozilla::Some(frameSize); } else { baselineFrameSize_ = mozilla::Nothing(); } cachedSafepointIndex_ = nullptr; // If the next frame is the entry frame, just exit. Don't update current_, // since the entry and first frames overlap. if (isEntry(current()->prevType())) { type_ = current()->prevType(); return; } type_ = current()->prevType(); resumePCinCurrentFrame_ = current()->returnAddress(); current_ = prevFp(); MOZ_ASSERT_IF(isBaselineJS(), baselineFrame()->debugFrameSize() == *baselineFrameSize_); } uintptr_t* JSJitFrameIter::spillBase() const { MOZ_ASSERT(isIonJS()); // Get the base address to where safepoint registers are spilled. // Out-of-line calls do not unwind the extra padding space used to // aggregate bailout tables, so we use frameSize instead of frameLocals, // which would only account for local stack slots. return reinterpret_cast(fp() - ionScript()->frameSize()); } MachineState JSJitFrameIter::machineState() const { MOZ_ASSERT(isIonScripted()); // The MachineState is used by GCs for tracing call-sites. if (MOZ_UNLIKELY(isBailoutJS())) { return *activation_->bailoutData()->machineState(); } SafepointReader reader(ionScript(), safepoint()); FloatRegisterSet fregs = reader.allFloatSpills().set().reduceSetForPush(); GeneralRegisterSet regs = reader.allGprSpills().set(); uintptr_t* spill = spillBase(); uint8_t* spillAlign = alignDoubleSpill(reinterpret_cast(spill - regs.size())); char* floatSpill = reinterpret_cast(spillAlign); return MachineState::FromSafepoint(fregs, regs, floatSpill, spill); } JitFrameLayout* JSJitFrameIter::jsFrame() const { MOZ_ASSERT(isScripted()); if (isBailoutJS()) { return (JitFrameLayout*)activation_->bailoutData()->fp(); } return (JitFrameLayout*)fp(); } IonScript* JSJitFrameIter::ionScript() const { MOZ_ASSERT(isIonScripted()); if (isBailoutJS()) { return activation_->bailoutData()->ionScript(); } IonScript* ionScript = nullptr; if (checkInvalidation(&ionScript)) { return ionScript; } return ionScriptFromCalleeToken(); } IonScript* JSJitFrameIter::ionScriptFromCalleeToken() const { MOZ_ASSERT(isIonJS()); MOZ_ASSERT(!checkInvalidation()); return script()->ionScript(); } const SafepointIndex* JSJitFrameIter::safepoint() const { MOZ_ASSERT(isIonJS()); if (!cachedSafepointIndex_) { cachedSafepointIndex_ = ionScript()->getSafepointIndex(resumePCinCurrentFrame()); } return cachedSafepointIndex_; } SnapshotOffset JSJitFrameIter::snapshotOffset() const { MOZ_ASSERT(isIonScripted()); if (isBailoutJS()) { return activation_->bailoutData()->snapshotOffset(); } return osiIndex()->snapshotOffset(); } const OsiIndex* JSJitFrameIter::osiIndex() const { MOZ_ASSERT(isIonJS()); SafepointReader reader(ionScript(), safepoint()); return ionScript()->getOsiIndex(reader.osiReturnPointOffset()); } bool JSJitFrameIter::isConstructing() const { return CalleeTokenIsConstructing(calleeToken()); } unsigned JSJitFrameIter::numActualArgs() const { if (isScripted()) { return jsFrame()->numActualArgs(); } MOZ_ASSERT(isExitFrameLayout()); return exitFrame()->as()->argc(); } void JSJitFrameIter::dumpBaseline() const { MOZ_ASSERT(isBaselineJS()); fprintf(stderr, " JS Baseline frame\n"); if (isFunctionFrame()) { fprintf(stderr, " callee fun: "); #if defined(DEBUG) || defined(JS_JITSPEW) DumpObject(callee()); #else fprintf(stderr, "?\n"); #endif } else { fprintf(stderr, " global frame, no callee\n"); } fprintf(stderr, " file %s line %u\n", script()->filename(), script()->lineno()); JSContext* cx = TlsContext.get(); RootedScript script(cx); jsbytecode* pc; baselineScriptAndPc(script.address(), &pc); fprintf(stderr, " script = %p, pc = %p (offset %u)\n", (void*)script, pc, uint32_t(script->pcToOffset(pc))); fprintf(stderr, " current op: %s\n", CodeName(JSOp(*pc))); fprintf(stderr, " actual args: %u\n", numActualArgs()); for (unsigned i = 0; i < baselineFrameNumValueSlots(); i++) { fprintf(stderr, " slot %u: ", i); #if defined(DEBUG) || defined(JS_JITSPEW) Value* v = baselineFrame()->valueSlot(i); DumpValue(*v); #else fprintf(stderr, "?\n"); #endif } } void JSJitFrameIter::dump() const { switch (type_) { case FrameType::CppToJSJit: fprintf(stderr, " Entry frame\n"); fprintf(stderr, " Caller frame ptr: %p\n", current()->callerFramePtr()); break; case FrameType::BaselineJS: dumpBaseline(); break; case FrameType::BaselineStub: fprintf(stderr, " Baseline stub frame\n"); fprintf(stderr, " Caller frame ptr: %p\n", current()->callerFramePtr()); break; case FrameType::Bailout: case FrameType::IonJS: { InlineFrameIterator frames(TlsContext.get(), this); for (;;) { frames.dump(); if (!frames.more()) { break; } ++frames; } break; } case FrameType::BaselineInterpreterEntry: fprintf(stderr, " Baseline Interpreter Entry frame\n"); fprintf(stderr, " Caller frame ptr: %p\n", current()->callerFramePtr()); break; case FrameType::Rectifier: fprintf(stderr, " Rectifier frame\n"); fprintf(stderr, " Caller frame ptr: %p\n", current()->callerFramePtr()); break; case FrameType::IonICCall: fprintf(stderr, " Ion IC call\n"); fprintf(stderr, " Caller frame ptr: %p\n", current()->callerFramePtr()); break; case FrameType::WasmToJSJit: fprintf(stderr, " Fast wasm-to-JS entry frame\n"); fprintf(stderr, " Caller frame ptr: %p\n", current()->callerFramePtr()); break; case FrameType::Exit: fprintf(stderr, " Exit frame\n"); break; case FrameType::JSJitToWasm: fprintf(stderr, " Wasm exit frame\n"); break; }; fputc('\n', stderr); } #ifdef DEBUG bool JSJitFrameIter::verifyReturnAddressUsingNativeToBytecodeMap() { MOZ_ASSERT(resumePCinCurrentFrame_ != nullptr); // Only handle Ion frames for now. if (type_ != FrameType::IonJS && type_ != FrameType::BaselineJS) { return true; } JSRuntime* rt = TlsContext.get()->runtime(); // Don't verify while off thread. if (!CurrentThreadCanAccessRuntime(rt)) { return true; } // Don't verify if sampling is being suppressed. if (!TlsContext.get()->isProfilerSamplingEnabled()) { return true; } if (JS::RuntimeHeapIsMinorCollecting()) { return true; } JitRuntime* jitrt = rt->jitRuntime(); // Look up and print bytecode info for the native address. const JitcodeGlobalEntry* entry = jitrt->getJitcodeGlobalTable()->lookup(resumePCinCurrentFrame_); if (!entry) { return true; } JitSpew(JitSpew_Profiling, "Found nativeToBytecode entry for %p: %p - %p", resumePCinCurrentFrame_, entry->nativeStartAddr(), entry->nativeEndAddr()); BytecodeLocationVector location; uint32_t depth = UINT32_MAX; if (!entry->callStackAtAddr(rt, resumePCinCurrentFrame_, location, &depth)) { return false; } MOZ_ASSERT(depth > 0 && depth != UINT32_MAX); MOZ_ASSERT(location.length() == depth); JitSpew(JitSpew_Profiling, "Found bytecode location of depth %u:", depth); for (size_t i = 0; i < location.length(); i++) { JitSpew(JitSpew_Profiling, " %s:%u - %zu", location[i].getDebugOnlyScript()->filename(), location[i].getDebugOnlyScript()->lineno(), size_t(location[i].toRawBytecode() - location[i].getDebugOnlyScript()->code())); } if (type_ == FrameType::IonJS) { // Create an InlineFrameIterator here and verify the mapped info against the // iterator info. InlineFrameIterator inlineFrames(TlsContext.get(), this); for (size_t idx = 0; idx < location.length(); idx++) { MOZ_ASSERT(idx < location.length()); MOZ_ASSERT_IF(idx < location.length() - 1, inlineFrames.more()); JitSpew(JitSpew_Profiling, "Match %d: ION %s:%u(%zu) vs N2B %s:%u(%zu)", (int)idx, inlineFrames.script()->filename(), inlineFrames.script()->lineno(), size_t(inlineFrames.pc() - inlineFrames.script()->code()), location[idx].getDebugOnlyScript()->filename(), location[idx].getDebugOnlyScript()->lineno(), size_t(location[idx].toRawBytecode() - location[idx].getDebugOnlyScript()->code())); MOZ_ASSERT(inlineFrames.script() == location[idx].getDebugOnlyScript()); if (inlineFrames.more()) { ++inlineFrames; } } } return true; } #endif // DEBUG JSJitProfilingFrameIterator::JSJitProfilingFrameIterator(JSContext* cx, void* pc, void* sp) { // If no profilingActivation is live, initialize directly to // end-of-iteration state. if (!cx->profilingActivation()) { type_ = FrameType::CppToJSJit; fp_ = nullptr; resumePCinCurrentFrame_ = nullptr; return; } MOZ_ASSERT(cx->profilingActivation()->isJit()); JitActivation* act = cx->profilingActivation()->asJit(); // If the top JitActivation has a null lastProfilingFrame, assume that // it's a trivially empty activation, and initialize directly // to end-of-iteration state. if (!act->lastProfilingFrame()) { type_ = FrameType::CppToJSJit; fp_ = nullptr; resumePCinCurrentFrame_ = nullptr; return; } // Get the fp from the current profilingActivation fp_ = (uint8_t*)act->lastProfilingFrame(); // Use fp_ as endStackAddress_. For cases below where we know we're currently // executing JIT code, we use the current stack pointer instead. endStackAddress_ = fp_; // Profiler sampling must NOT be suppressed if we are here. MOZ_ASSERT(cx->isProfilerSamplingEnabled()); // Try initializing with sampler pc if (tryInitWithPC(pc)) { endStackAddress_ = sp; return; } // Try initializing with sampler pc using native=>bytecode table. JitcodeGlobalTable* table = cx->runtime()->jitRuntime()->getJitcodeGlobalTable(); if (tryInitWithTable(table, pc, /* forLastCallSite = */ false)) { endStackAddress_ = sp; return; } // Try initializing with lastProfilingCallSite pc void* lastCallSite = act->lastProfilingCallSite(); if (lastCallSite) { if (tryInitWithPC(lastCallSite)) { return; } // Try initializing with lastProfilingCallSite pc using native=>bytecode // table. if (tryInitWithTable(table, lastCallSite, /* forLastCallSite = */ true)) { return; } } // If nothing matches, for now just assume we are at the start of the last // frame's baseline jit code or interpreter code. type_ = FrameType::BaselineJS; if (frameScript()->hasBaselineScript()) { resumePCinCurrentFrame_ = frameScript()->baselineScript()->method()->raw(); } else { MOZ_ASSERT(IsBaselineInterpreterEnabled()); resumePCinCurrentFrame_ = cx->runtime()->jitRuntime()->baselineInterpreter().codeRaw(); } } template static inline ReturnType GetPreviousRawFrame(CommonFrameLayout* frame) { return ReturnType(frame->callerFramePtr()); } JSJitProfilingFrameIterator::JSJitProfilingFrameIterator( CommonFrameLayout* fp) { endStackAddress_ = fp; moveToNextFrame(fp); } bool JSJitProfilingFrameIterator::tryInitWithPC(void* pc) { JSScript* callee = frameScript(); // Check for Ion first, since it's more likely for hot code. if (callee->hasIonScript() && callee->ionScript()->method()->containsNativePC(pc)) { type_ = FrameType::IonJS; resumePCinCurrentFrame_ = pc; return true; } // Check for containment in Baseline jitcode second. if (callee->hasBaselineScript() && callee->baselineScript()->method()->containsNativePC(pc)) { type_ = FrameType::BaselineJS; resumePCinCurrentFrame_ = pc; return true; } return false; } bool JSJitProfilingFrameIterator::tryInitWithTable(JitcodeGlobalTable* table, void* pc, bool forLastCallSite) { if (!pc) { return false; } const JitcodeGlobalEntry* entry = table->lookup(pc); if (!entry) { return false; } JSScript* callee = frameScript(); MOZ_ASSERT(entry->isIon() || entry->isIonIC() || entry->isBaseline() || entry->isBaselineInterpreter() || entry->isDummy()); // Treat dummy lookups as an empty frame sequence. if (entry->isDummy()) { type_ = FrameType::CppToJSJit; fp_ = nullptr; resumePCinCurrentFrame_ = nullptr; return true; } // For IonICEntry, use the corresponding IonEntry. if (entry->isIonIC()) { entry = table->lookup(entry->asIonIC().rejoinAddr()); MOZ_ASSERT(entry); MOZ_RELEASE_ASSERT(entry->isIon()); } if (entry->isIon()) { // If looked-up callee doesn't match frame callee, don't accept // lastProfilingCallSite if (entry->asIon().getScript(0) != callee) { return false; } type_ = FrameType::IonJS; resumePCinCurrentFrame_ = pc; return true; } if (entry->isBaseline()) { // If looked-up callee doesn't match frame callee, don't accept // lastProfilingCallSite if (forLastCallSite && entry->asBaseline().script() != callee) { return false; } type_ = FrameType::BaselineJS; resumePCinCurrentFrame_ = pc; return true; } if (entry->isBaselineInterpreter()) { type_ = FrameType::BaselineJS; resumePCinCurrentFrame_ = pc; return true; } return false; } const char* JSJitProfilingFrameIterator::baselineInterpreterLabel() const { MOZ_ASSERT(type_ == FrameType::BaselineJS); return frameScript()->jitScript()->profileString(); } void JSJitProfilingFrameIterator::baselineInterpreterScriptPC( JSScript** script, jsbytecode** pc, uint64_t* realmID) const { MOZ_ASSERT(type_ == FrameType::BaselineJS); BaselineFrame* blFrame = (BaselineFrame*)(fp_ - BaselineFrame::Size()); *script = frameScript(); *pc = (*script)->code(); if (blFrame->runningInInterpreter() && blFrame->interpreterScript() == *script) { jsbytecode* interpPC = blFrame->interpreterPC(); if ((*script)->containsPC(interpPC)) { *pc = interpPC; } *realmID = (*script)->realm()->creationOptions().profilerRealmID(); } } void JSJitProfilingFrameIterator::operator++() { JitFrameLayout* frame = framePtr(); moveToNextFrame(frame); } void JSJitProfilingFrameIterator::moveToNextFrame(CommonFrameLayout* frame) { /* * fp_ points to a Baseline or Ion frame. The possible call-stacks * patterns occurring between this frame and a previous Ion, Baseline or Entry * frame are as follows: * * * ^ * | * ^--- Ion (or Baseline JSOp::Resume) * | * ^--- Baseline Stub <---- Baseline * | * ^--- IonICCall <---- Ion * | * ^--- WasmToJSJit <---- (other wasm frames, not handled by this iterator) * | * ^--- Arguments Rectifier * | ^ * | | * | ^--- Ion * | | * | ^--- Baseline Stub <---- Baseline * | | * | ^--- WasmToJSJit <--- (other wasm frames) * | | * | ^--- Entry Frame (CppToJSJit) * | * ^--- Entry Frame (CppToJSJit) * | * ^--- Entry Frame (BaselineInterpreter) * | ^ * | | * | ^--- Ion * | | * | ^--- Baseline Stub <---- Baseline * | | * | ^--- WasmToJSJit <--- (other wasm frames) * | | * | ^--- Entry Frame (CppToJSJit) * | | * | ^--- Arguments Rectifier * * NOTE: Keep this in sync with JitRuntime::generateProfilerExitFrameTailStub! */ // Unwrap baseline interpreter entry frame. if (frame->prevType() == FrameType::BaselineInterpreterEntry) { frame = GetPreviousRawFrame(frame); } // Unwrap rectifier frames. if (frame->prevType() == FrameType::Rectifier) { frame = GetPreviousRawFrame(frame); MOZ_ASSERT(frame->prevType() == FrameType::IonJS || frame->prevType() == FrameType::BaselineStub || frame->prevType() == FrameType::WasmToJSJit || frame->prevType() == FrameType::CppToJSJit); } FrameType prevType = frame->prevType(); switch (prevType) { case FrameType::IonJS: case FrameType::BaselineJS: resumePCinCurrentFrame_ = frame->returnAddress(); fp_ = GetPreviousRawFrame(frame); type_ = prevType; return; case FrameType::BaselineStub: case FrameType::IonICCall: { FrameType stubPrevType = (prevType == FrameType::BaselineStub) ? FrameType::BaselineJS : FrameType::IonJS; auto* stubFrame = GetPreviousRawFrame(frame); MOZ_ASSERT(stubFrame->prevType() == stubPrevType); resumePCinCurrentFrame_ = stubFrame->returnAddress(); fp_ = GetPreviousRawFrame(stubFrame); type_ = stubPrevType; return; } case FrameType::WasmToJSJit: // No previous js jit frame, this is a transition frame, used to // pass a wasm iterator the correct value of FP. resumePCinCurrentFrame_ = nullptr; fp_ = GetPreviousRawFrame(frame); type_ = FrameType::WasmToJSJit; MOZ_ASSERT(!done()); return; case FrameType::CppToJSJit: // No previous frame, set to nullptr to indicate that // JSJitProfilingFrameIterator is done(). resumePCinCurrentFrame_ = nullptr; fp_ = nullptr; type_ = FrameType::CppToJSJit; return; case FrameType::BaselineInterpreterEntry: case FrameType::Rectifier: case FrameType::Exit: case FrameType::Bailout: case FrameType::JSJitToWasm: // Rectifier and Baseline Interpreter entry frames are handled before // this switch. The other frame types can't call JS functions directly. break; } MOZ_CRASH("Bad frame type."); }