From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- js/src/jit/WarpOracle.cpp | 1226 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1226 insertions(+) create mode 100644 js/src/jit/WarpOracle.cpp (limited to 'js/src/jit/WarpOracle.cpp') diff --git a/js/src/jit/WarpOracle.cpp b/js/src/jit/WarpOracle.cpp new file mode 100644 index 0000000000..f4b4228c8f --- /dev/null +++ b/js/src/jit/WarpOracle.cpp @@ -0,0 +1,1226 @@ +/* -*- 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/WarpOracle.h" + +#include "mozilla/ScopeExit.h" + +#include + +#include "jit/CacheIR.h" +#include "jit/CacheIRCompiler.h" +#include "jit/CacheIRReader.h" +#include "jit/CompileInfo.h" +#include "jit/InlineScriptTree.h" +#include "jit/JitRealm.h" +#include "jit/JitScript.h" +#include "jit/JitSpewer.h" +#include "jit/MIRGenerator.h" +#include "jit/TrialInlining.h" +#include "jit/TypeData.h" +#include "jit/WarpBuilder.h" +#include "util/DifferentialTesting.h" +#include "vm/BuiltinObjectKind.h" +#include "vm/BytecodeIterator.h" +#include "vm/BytecodeLocation.h" + +#include "jit/InlineScriptTree-inl.h" +#include "vm/BytecodeIterator-inl.h" +#include "vm/BytecodeLocation-inl.h" +#include "vm/EnvironmentObject-inl.h" +#include "vm/Interpreter-inl.h" + +using namespace js; +using namespace js::jit; + +using mozilla::Maybe; + +// WarpScriptOracle creates a WarpScriptSnapshot for a single JSScript. Note +// that a single WarpOracle can use multiple WarpScriptOracles when scripts are +// inlined. +class MOZ_STACK_CLASS WarpScriptOracle { + JSContext* cx_; + WarpOracle* oracle_; + MIRGenerator& mirGen_; + TempAllocator& alloc_; + HandleScript script_; + const CompileInfo* info_; + ICScript* icScript_; + + // Index of the next ICEntry for getICEntry. This assumes the script's + // bytecode is processed from first to last instruction. + uint32_t icEntryIndex_ = 0; + + template + mozilla::GenericErrorResult abort(Args&&... args) { + return oracle_->abort(script_, args...); + } + + WarpEnvironment createEnvironment(); + AbortReasonOr maybeInlineIC(WarpOpSnapshotList& snapshots, + BytecodeLocation loc); + AbortReasonOr maybeInlineCall(WarpOpSnapshotList& snapshots, + BytecodeLocation loc, ICCacheIRStub* stub, + ICFallbackStub* fallbackStub, + uint8_t* stubDataCopy); + AbortReasonOr maybeInlinePolymorphicTypes(WarpOpSnapshotList& snapshots, + BytecodeLocation loc, + ICCacheIRStub* firstStub, + ICFallbackStub* fallbackStub); + [[nodiscard]] bool replaceNurseryAndAllocSitePointers( + ICCacheIRStub* stub, const CacheIRStubInfo* stubInfo, + uint8_t* stubDataCopy); + + public: + WarpScriptOracle(JSContext* cx, WarpOracle* oracle, HandleScript script, + const CompileInfo* info, ICScript* icScript) + : cx_(cx), + oracle_(oracle), + mirGen_(oracle->mirGen()), + alloc_(mirGen_.alloc()), + script_(script), + info_(info), + icScript_(icScript) {} + + AbortReasonOr createScriptSnapshot(); + + ICEntry& getICEntryAndFallback(BytecodeLocation loc, + ICFallbackStub** fallback); +}; + +WarpOracle::WarpOracle(JSContext* cx, MIRGenerator& mirGen, + HandleScript outerScript) + : cx_(cx), + mirGen_(mirGen), + alloc_(mirGen.alloc()), + outerScript_(outerScript) {} + +mozilla::GenericErrorResult WarpOracle::abort(HandleScript script, + AbortReason r) { + auto res = mirGen_.abort(r); + JitSpew(JitSpew_IonAbort, "aborted @ %s", script->filename()); + return res; +} + +mozilla::GenericErrorResult WarpOracle::abort(HandleScript script, + AbortReason r, + const char* message, + ...) { + va_list ap; + va_start(ap, message); + auto res = mirGen_.abortFmt(r, message, ap); + va_end(ap); + JitSpew(JitSpew_IonAbort, "aborted @ %s", script->filename()); + return res; +} + +void WarpOracle::addScriptSnapshot(WarpScriptSnapshot* scriptSnapshot, + ICScript* icScript, size_t bytecodeLength) { + scriptSnapshots_.insertBack(scriptSnapshot); + accumulatedBytecodeSize_ += bytecodeLength; +#ifdef DEBUG + runningScriptHash_ = mozilla::AddToHash(runningScriptHash_, icScript->hash()); +#endif +} + +AbortReasonOr WarpOracle::createSnapshot() { +#ifdef JS_JITSPEW + const char* mode; + if (outerScript_->hasIonScript()) { + mode = "Recompiling"; + } else { + mode = "Compiling"; + } + JitSpew(JitSpew_IonScripts, + "Warp %s script %s:%u:%u (%p) (warmup-counter=%" PRIu32 ",%s%s)", + mode, outerScript_->filename(), outerScript_->lineno(), + outerScript_->column(), static_cast(outerScript_), + outerScript_->getWarmUpCount(), + outerScript_->isGenerator() ? " isGenerator" : "", + outerScript_->isAsync() ? " isAsync" : ""); +#endif + + accumulatedBytecodeSize_ = outerScript_->length(); + + MOZ_ASSERT(outerScript_->hasJitScript()); + ICScript* icScript = outerScript_->jitScript()->icScript(); + WarpScriptOracle scriptOracle(cx_, this, outerScript_, &mirGen_.outerInfo(), + icScript); + + WarpScriptSnapshot* scriptSnapshot; + MOZ_TRY_VAR(scriptSnapshot, scriptOracle.createScriptSnapshot()); + + // Insert the outermost scriptSnapshot at the front of the list. + scriptSnapshots_.insertFront(scriptSnapshot); + + bool recordFinalWarmUpCount = false; +#ifdef JS_CACHEIR_SPEW + recordFinalWarmUpCount = outerScript_->needsFinalWarmUpCount(); +#endif + + auto* snapshot = new (alloc_.fallible()) + WarpSnapshot(cx_, alloc_, std::move(scriptSnapshots_), bailoutInfo_, + recordFinalWarmUpCount); + if (!snapshot) { + return abort(outerScript_, AbortReason::Alloc); + } + + if (!snapshot->nurseryObjects().appendAll(nurseryObjects_)) { + return abort(outerScript_, AbortReason::Alloc); + } + +#ifdef JS_JITSPEW + if (JitSpewEnabled(JitSpew_WarpSnapshots)) { + Fprinter& out = JitSpewPrinter(); + snapshot->dump(out); + } +#endif + +#ifdef DEBUG + // When transpiled CacheIR bails out, we do not want to recompile + // with the exact same data and get caught in an invalidation loop. + // + // To avoid this, we store a hash of the stub pointers and entry + // counts in this snapshot, save that hash in the JitScript if we + // have a TranspiledCacheIR or MonomorphicInlinedStubFolding bailout, + // and assert that the hash has changed when we recompile. + // + // Note: this assertion catches potential performance issues. + // Failing this assertion is not a correctness/security problem. + // We therefore ignore cases involving resource exhaustion (OOM, + // stack overflow, etc), or stubs purged by GC. + HashNumber hash = mozilla::AddToHash(icScript->hash(), runningScriptHash_); + if (outerScript_->jitScript()->hasFailedICHash()) { + HashNumber oldHash = outerScript_->jitScript()->getFailedICHash(); + MOZ_ASSERT_IF(hash == oldHash && !js::SupportDifferentialTesting(), + cx_->hadResourceExhaustion()); + } + snapshot->setICHash(hash); +#endif + + return snapshot; +} + +template +[[nodiscard]] static bool AddOpSnapshot(TempAllocator& alloc, + WarpOpSnapshotList& snapshots, + uint32_t offset, Args&&... args) { + T* snapshot = new (alloc.fallible()) T(offset, std::forward(args)...); + if (!snapshot) { + return false; + } + + snapshots.insertBack(snapshot); + return true; +} + +[[nodiscard]] static bool AddWarpGetImport(TempAllocator& alloc, + WarpOpSnapshotList& snapshots, + uint32_t offset, JSScript* script, + PropertyName* name) { + ModuleEnvironmentObject* env = GetModuleEnvironmentForScript(script); + MOZ_ASSERT(env); + + mozilla::Maybe prop; + ModuleEnvironmentObject* targetEnv; + MOZ_ALWAYS_TRUE(env->lookupImport(NameToId(name), &targetEnv, &prop)); + + uint32_t numFixedSlots = targetEnv->numFixedSlots(); + uint32_t slot = prop->slot(); + + // In the rare case where this import hasn't been initialized already (we have + // an import cycle where modules reference each other's imports), we need a + // check. + bool needsLexicalCheck = + targetEnv->getSlot(slot).isMagic(JS_UNINITIALIZED_LEXICAL); + + return AddOpSnapshot(alloc, snapshots, offset, targetEnv, + numFixedSlots, slot, needsLexicalCheck); +} + +ICEntry& WarpScriptOracle::getICEntryAndFallback(BytecodeLocation loc, + ICFallbackStub** fallback) { + const uint32_t offset = loc.bytecodeToOffset(script_); + + do { + *fallback = icScript_->fallbackStub(icEntryIndex_); + icEntryIndex_++; + } while ((*fallback)->pcOffset() < offset); + + MOZ_ASSERT((*fallback)->pcOffset() == offset); + return icScript_->icEntry(icEntryIndex_ - 1); +} + +WarpEnvironment WarpScriptOracle::createEnvironment() { + // Don't do anything if the script doesn't use the environment chain. + if (!script_->jitScript()->usesEnvironmentChain()) { + return WarpEnvironment(NoEnvironment()); + } + + if (script_->isModule()) { + ModuleObject* module = script_->module(); + JSObject* obj = &module->initialEnvironment(); + return WarpEnvironment(ConstantObjectEnvironment(obj)); + } + + JSFunction* fun = script_->function(); + if (!fun) { + // For global scripts without a non-syntactic global scope, the environment + // chain is the global lexical environment. + MOZ_ASSERT(!script_->isForEval()); + MOZ_ASSERT(!script_->hasNonSyntacticScope()); + JSObject* obj = &script_->global().lexicalEnvironment(); + return WarpEnvironment(ConstantObjectEnvironment(obj)); + } + + JSObject* templateEnv = script_->jitScript()->templateEnvironment(); + + CallObject* callObjectTemplate = nullptr; + if (fun->needsCallObject()) { + callObjectTemplate = &templateEnv->as(); + } + + NamedLambdaObject* namedLambdaTemplate = nullptr; + if (fun->needsNamedLambdaEnvironment()) { + if (callObjectTemplate) { + templateEnv = templateEnv->enclosingEnvironment(); + } + namedLambdaTemplate = &templateEnv->as(); + } + + return WarpEnvironment( + FunctionEnvironment(callObjectTemplate, namedLambdaTemplate)); +} + +AbortReasonOr WarpScriptOracle::createScriptSnapshot() { + MOZ_ASSERT(script_->hasJitScript()); + + if (!script_->jitScript()->ensureHasCachedIonData(cx_, script_)) { + return abort(AbortReason::Error); + } + + if (script_->failedBoundsCheck()) { + oracle_->bailoutInfo().setFailedBoundsCheck(); + } + if (script_->failedLexicalCheck()) { + oracle_->bailoutInfo().setFailedLexicalCheck(); + } + + WarpEnvironment environment = createEnvironment(); + + // Unfortunately LinkedList<> asserts the list is empty in its destructor. + // Clear the list if we abort compilation. + WarpOpSnapshotList opSnapshots; + auto autoClearOpSnapshots = + mozilla::MakeScopeExit([&] { opSnapshots.clear(); }); + + ModuleObject* moduleObject = nullptr; + + // Analyze the bytecode. Abort compilation for unsupported ops and create + // WarpOpSnapshots. + for (BytecodeLocation loc : AllBytecodesIterable(script_)) { + JSOp op = loc.getOp(); + uint32_t offset = loc.bytecodeToOffset(script_); + switch (op) { + case JSOp::Arguments: { + MOZ_ASSERT(script_->needsArgsObj()); + bool mapped = script_->hasMappedArgsObj(); + ArgumentsObject* templateObj = + script_->global().maybeArgumentsTemplateObject(mapped); + if (!AddOpSnapshot(alloc_, opSnapshots, offset, + templateObj)) { + return abort(AbortReason::Alloc); + } + break; + } + case JSOp::RegExp: { + bool hasShared = loc.getRegExp(script_)->hasShared(); + if (!AddOpSnapshot(alloc_, opSnapshots, offset, + hasShared)) { + return abort(AbortReason::Alloc); + } + break; + } + + case JSOp::FunctionThis: + if (!script_->strict() && script_->hasNonSyntacticScope()) { + // Abort because MBoxNonStrictThis doesn't support non-syntactic + // scopes (a deprecated SpiderMonkey mechanism). If this becomes an + // issue we could support it by refactoring GetFunctionThis to not + // take a frame pointer and then call that. + return abort(AbortReason::Disable, + "JSOp::FunctionThis with non-syntactic scope"); + } + break; + + case JSOp::GlobalThis: + MOZ_ASSERT(!script_->hasNonSyntacticScope()); + break; + + case JSOp::BuiltinObject: { + // If we already resolved this built-in we can bake it in. + auto kind = loc.getBuiltinObjectKind(); + if (JSObject* proto = MaybeGetBuiltinObject(cx_->global(), kind)) { + if (!AddOpSnapshot(alloc_, opSnapshots, offset, + proto)) { + return abort(AbortReason::Alloc); + } + } + break; + } + + case JSOp::GetIntrinsic: { + // If we already cloned this intrinsic we can bake it in. + // NOTE: When the initializer runs in a content global, we also have to + // worry about nursery objects. These quickly tenure and stay that + // way so this is only a temporary problem. + PropertyName* name = loc.getPropertyName(script_); + Value val; + if (cx_->global()->maybeGetIntrinsicValue(name, &val, cx_) && + JS::GCPolicy::isTenured(val)) { + if (!AddOpSnapshot(alloc_, opSnapshots, offset, + val)) { + return abort(AbortReason::Alloc); + } + } + break; + } + + case JSOp::ImportMeta: { + if (!moduleObject) { + moduleObject = GetModuleObjectForScript(script_); + MOZ_ASSERT(moduleObject->isTenured()); + } + break; + } + + case JSOp::GetImport: { + PropertyName* name = loc.getPropertyName(script_); + if (!AddWarpGetImport(alloc_, opSnapshots, offset, script_, name)) { + return abort(AbortReason::Alloc); + } + break; + } + + case JSOp::Lambda: { + JSFunction* fun = loc.getFunction(script_); + if (IsAsmJSModule(fun)) { + return abort(AbortReason::Disable, "asm.js module function lambda"); + } + break; + } + + case JSOp::GetElemSuper: { +#if defined(JS_CODEGEN_X86) + // x86 does not have enough registers. + return abort(AbortReason::Disable, + "GetElemSuper is not supported on x86"); +#else + MOZ_TRY(maybeInlineIC(opSnapshots, loc)); + break; +#endif + } + + case JSOp::Rest: { + if (Shape* shape = + script_->global().maybeArrayShapeWithDefaultProto()) { + if (!AddOpSnapshot(alloc_, opSnapshots, offset, shape)) { + return abort(AbortReason::Alloc); + } + } + break; + } + + case JSOp::BindGName: { + Rooted global(cx_, &script_->global()); + Rooted name(cx_, loc.getPropertyName(script_)); + if (JSObject* env = MaybeOptimizeBindGlobalName(cx_, global, name)) { + MOZ_ASSERT(env->isTenured()); + if (!AddOpSnapshot(alloc_, opSnapshots, offset, env)) { + return abort(AbortReason::Alloc); + } + } else { + MOZ_TRY(maybeInlineIC(opSnapshots, loc)); + } + break; + } + + case JSOp::PushVarEnv: { + Rooted scope(cx_, &loc.getScope(script_)->as()); + + auto* templateObj = + VarEnvironmentObject::createTemplateObject(cx_, scope); + if (!templateObj) { + return abort(AbortReason::Alloc); + } + MOZ_ASSERT(templateObj->isTenured()); + + if (!AddOpSnapshot(alloc_, opSnapshots, offset, + templateObj)) { + return abort(AbortReason::Alloc); + } + break; + } + + case JSOp::PushLexicalEnv: + case JSOp::FreshenLexicalEnv: + case JSOp::RecreateLexicalEnv: { + Rooted scope(cx_, + &loc.getScope(script_)->as()); + + auto* templateObj = + BlockLexicalEnvironmentObject::createTemplateObject(cx_, scope); + if (!templateObj) { + return abort(AbortReason::Alloc); + } + MOZ_ASSERT(templateObj->isTenured()); + + if (!AddOpSnapshot(alloc_, opSnapshots, offset, + templateObj)) { + return abort(AbortReason::Alloc); + } + break; + } + + case JSOp::PushClassBodyEnv: { + Rooted scope( + cx_, &loc.getScope(script_)->as()); + + auto* templateObj = + ClassBodyLexicalEnvironmentObject::createTemplateObject(cx_, scope); + if (!templateObj) { + return abort(AbortReason::Alloc); + } + MOZ_ASSERT(templateObj->isTenured()); + + if (!AddOpSnapshot(alloc_, opSnapshots, + offset, templateObj)) { + return abort(AbortReason::Alloc); + } + break; + } + + case JSOp::GetName: + case JSOp::GetGName: + case JSOp::GetProp: + case JSOp::GetElem: + case JSOp::SetProp: + case JSOp::StrictSetProp: + case JSOp::Call: + case JSOp::CallContent: + case JSOp::CallIgnoresRv: + case JSOp::CallIter: + case JSOp::CallContentIter: + case JSOp::New: + case JSOp::NewContent: + case JSOp::SuperCall: + case JSOp::SpreadCall: + case JSOp::SpreadNew: + case JSOp::SpreadSuperCall: + case JSOp::ToNumeric: + case JSOp::Pos: + case JSOp::Inc: + case JSOp::Dec: + case JSOp::Neg: + case JSOp::BitNot: + case JSOp::Iter: + case JSOp::Eq: + case JSOp::Ne: + case JSOp::Lt: + case JSOp::Le: + case JSOp::Gt: + case JSOp::Ge: + case JSOp::StrictEq: + case JSOp::StrictNe: + case JSOp::BindName: + case JSOp::Add: + case JSOp::Sub: + case JSOp::Mul: + case JSOp::Div: + case JSOp::Mod: + case JSOp::Pow: + case JSOp::BitAnd: + case JSOp::BitOr: + case JSOp::BitXor: + case JSOp::Lsh: + case JSOp::Rsh: + case JSOp::Ursh: + case JSOp::In: + case JSOp::HasOwn: + case JSOp::CheckPrivateField: + case JSOp::Instanceof: + case JSOp::GetPropSuper: + case JSOp::InitProp: + case JSOp::InitLockedProp: + case JSOp::InitHiddenProp: + case JSOp::InitElem: + case JSOp::InitHiddenElem: + case JSOp::InitLockedElem: + case JSOp::InitElemInc: + case JSOp::SetName: + case JSOp::StrictSetName: + case JSOp::SetGName: + case JSOp::StrictSetGName: + case JSOp::InitGLexical: + case JSOp::SetElem: + case JSOp::StrictSetElem: + case JSOp::ToPropertyKey: + case JSOp::OptimizeSpreadCall: + case JSOp::Typeof: + case JSOp::TypeofExpr: + case JSOp::NewObject: + case JSOp::NewInit: + case JSOp::NewArray: + case JSOp::JumpIfFalse: + case JSOp::JumpIfTrue: + case JSOp::And: + case JSOp::Or: + case JSOp::Not: + case JSOp::CloseIter: + MOZ_TRY(maybeInlineIC(opSnapshots, loc)); + break; + + case JSOp::Nop: + case JSOp::NopDestructuring: + case JSOp::TryDestructuring: + case JSOp::Lineno: + case JSOp::DebugLeaveLexicalEnv: + case JSOp::Undefined: + case JSOp::Void: + case JSOp::Null: + case JSOp::Hole: + case JSOp::Uninitialized: + case JSOp::IsConstructing: + case JSOp::False: + case JSOp::True: + case JSOp::Zero: + case JSOp::One: + case JSOp::Int8: + case JSOp::Uint16: + case JSOp::Uint24: + case JSOp::Int32: + case JSOp::Double: + case JSOp::BigInt: + case JSOp::String: + case JSOp::Symbol: + case JSOp::Pop: + case JSOp::PopN: + case JSOp::Dup: + case JSOp::Dup2: + case JSOp::DupAt: + case JSOp::Swap: + case JSOp::Pick: + case JSOp::Unpick: + case JSOp::GetLocal: + case JSOp::SetLocal: + case JSOp::InitLexical: + case JSOp::GetArg: + case JSOp::GetFrameArg: + case JSOp::SetArg: + case JSOp::ArgumentsLength: + case JSOp::GetActualArg: + case JSOp::JumpTarget: + case JSOp::LoopHead: + case JSOp::Case: + case JSOp::Default: + case JSOp::Coalesce: + case JSOp::Goto: + case JSOp::DebugCheckSelfHosted: + case JSOp::DynamicImport: + case JSOp::ToString: + case JSOp::GlobalOrEvalDeclInstantiation: + case JSOp::BindVar: + case JSOp::MutateProto: + case JSOp::Callee: + case JSOp::ToAsyncIter: + case JSOp::ObjWithProto: + case JSOp::GetAliasedVar: + case JSOp::SetAliasedVar: + case JSOp::InitAliasedLexical: + case JSOp::EnvCallee: + case JSOp::MoreIter: + case JSOp::EndIter: + case JSOp::IsNoIter: + case JSOp::IsNullOrUndefined: + case JSOp::DelProp: + case JSOp::StrictDelProp: + case JSOp::DelElem: + case JSOp::StrictDelElem: + case JSOp::SetFunName: + case JSOp::PopLexicalEnv: + case JSOp::ImplicitThis: + case JSOp::CheckClassHeritage: + case JSOp::CheckThis: + case JSOp::CheckThisReinit: + case JSOp::Generator: + case JSOp::AfterYield: + case JSOp::FinalYieldRval: + case JSOp::AsyncResolve: + case JSOp::CheckResumeKind: + case JSOp::CanSkipAwait: + case JSOp::MaybeExtractAwaitValue: + case JSOp::AsyncAwait: + case JSOp::Await: + case JSOp::CheckReturn: + case JSOp::CheckLexical: + case JSOp::CheckAliasedLexical: + case JSOp::InitHomeObject: + case JSOp::SuperBase: + case JSOp::SuperFun: + case JSOp::InitElemArray: + case JSOp::InitPropGetter: + case JSOp::InitPropSetter: + case JSOp::InitHiddenPropGetter: + case JSOp::InitHiddenPropSetter: + case JSOp::InitElemGetter: + case JSOp::InitElemSetter: + case JSOp::InitHiddenElemGetter: + case JSOp::InitHiddenElemSetter: + case JSOp::NewTarget: + case JSOp::Object: + case JSOp::CallSiteObj: + case JSOp::CheckIsObj: + case JSOp::CheckObjCoercible: + case JSOp::FunWithProto: + case JSOp::Debugger: + case JSOp::TableSwitch: + case JSOp::Exception: + case JSOp::Throw: + case JSOp::ThrowSetConst: + case JSOp::SetRval: + case JSOp::GetRval: + case JSOp::Return: + case JSOp::RetRval: + case JSOp::InitialYield: + case JSOp::Yield: + case JSOp::ResumeKind: + case JSOp::ThrowMsg: + case JSOp::Try: + case JSOp::Finally: + case JSOp::NewPrivateName: + // Supported by WarpBuilder. Nothing to do. + break; + + // Unsupported ops. Don't use a 'default' here, we want to trigger a + // compiler warning when adding a new JSOp. +#define DEF_CASE(OP) case JSOp::OP: + WARP_UNSUPPORTED_OPCODE_LIST(DEF_CASE) +#undef DEF_CASE +#ifdef DEBUG + return abort(AbortReason::Disable, "Unsupported opcode: %s", + CodeName(op)); +#else + return abort(AbortReason::Disable, "Unsupported opcode: %u", + uint8_t(op)); +#endif + } + } + + auto* scriptSnapshot = new (alloc_.fallible()) WarpScriptSnapshot( + script_, environment, std::move(opSnapshots), moduleObject); + if (!scriptSnapshot) { + return abort(AbortReason::Alloc); + } + + autoClearOpSnapshots.release(); + return scriptSnapshot; +} + +static void LineNumberAndColumn(HandleScript script, BytecodeLocation loc, + unsigned* line, unsigned* column) { +#ifdef DEBUG + *line = PCToLineNumber(script, loc.toRawBytecode(), column); +#else + *line = script->lineno(); + *column = script->column(); +#endif +} + +AbortReasonOr WarpScriptOracle::maybeInlineIC(WarpOpSnapshotList& snapshots, + BytecodeLocation loc) { + // Do one of the following: + // + // * If the Baseline IC has a single ICStub we can inline, add a WarpCacheIR + // snapshot to transpile it to MIR. + // + // * If that single ICStub is a call IC with a known target, instead add a + // WarpInline snapshot to transpile the guards to MIR and inline the target. + // + // * If the Baseline IC is cold (never executed), add a WarpBailout snapshot + // so that we can collect information in Baseline. + // + // * Else, don't add a snapshot and rely on WarpBuilder adding an Ion IC. + + MOZ_ASSERT(loc.opHasIC()); + + // Don't create snapshots when testing ICs. + if (JitOptions.forceInlineCaches) { + return Ok(); + } + + ICFallbackStub* fallbackStub; + const ICEntry& entry = getICEntryAndFallback(loc, &fallbackStub); + ICStub* firstStub = entry.firstStub(); + + uint32_t offset = loc.bytecodeToOffset(script_); + + // Clear the used-by-transpiler flag on the IC. It can still be set from a + // previous compilation because we don't clear the flag on every IC when + // invalidating. + fallbackStub->clearUsedByTranspiler(); + + if (firstStub == fallbackStub) { + [[maybe_unused]] unsigned line, column; + LineNumberAndColumn(script_, loc, &line, &column); + + // No optimized stubs. + JitSpew(JitSpew_WarpTranspiler, + "fallback stub (entered-count: %" PRIu32 + ") for JSOp::%s @ %s:%u:%u", + fallbackStub->enteredCount(), CodeName(loc.getOp()), + script_->filename(), line, column); + + // If the fallback stub was used but there's no optimized stub, use an IC. + if (fallbackStub->enteredCount() != 0) { + return Ok(); + } + + // Cold IC. Bailout to collect information. + if (!AddOpSnapshot(alloc_, snapshots, offset)) { + return abort(AbortReason::Alloc); + } + return Ok(); + } + + ICCacheIRStub* stub = firstStub->toCacheIRStub(); + + // Don't transpile if there are other stubs with entered-count > 0. Counters + // are reset when a new stub is attached so this means the stub that was added + // most recently didn't handle all cases. + // If this code is changed, ICScript::hash may also need changing. + bool firstStubHandlesAllCases = true; + for (ICStub* next = stub->next(); next; next = next->maybeNext()) { + if (next->enteredCount() != 0) { + firstStubHandlesAllCases = false; + break; + } + } + + if (!firstStubHandlesAllCases) { + // In some polymorphic cases, we can generate better code than the + // default fallback if we know the observed types of the operands + // and their relative frequency. + if (ICSupportsPolymorphicTypeData(loc.getOp()) && + fallbackStub->enteredCount() == 0) { + bool inlinedPolymorphicTypes = false; + MOZ_TRY_VAR( + inlinedPolymorphicTypes, + maybeInlinePolymorphicTypes(snapshots, loc, stub, fallbackStub)); + if (inlinedPolymorphicTypes) { + return Ok(); + } + } + + [[maybe_unused]] unsigned line, column; + LineNumberAndColumn(script_, loc, &line, &column); + + JitSpew(JitSpew_WarpTranspiler, + "multiple active stubs for JSOp::%s @ %s:%u:%u", + CodeName(loc.getOp()), script_->filename(), line, column); + return Ok(); + } + + const CacheIRStubInfo* stubInfo = stub->stubInfo(); + const uint8_t* stubData = stub->stubDataStart(); + + // Only create a snapshot if all opcodes are supported by the transpiler. + CacheIRReader reader(stubInfo); + while (reader.more()) { + CacheOp op = reader.readOp(); + CacheIROpInfo opInfo = CacheIROpInfos[size_t(op)]; + reader.skip(opInfo.argLength); + + if (!opInfo.transpile) { + [[maybe_unused]] unsigned line, column; + LineNumberAndColumn(script_, loc, &line, &column); + + MOZ_ASSERT( + fallbackStub->trialInliningState() != TrialInliningState::Inlined, + "Trial-inlined stub not supported by transpiler"); + + // Unsupported CacheIR opcode. + JitSpew(JitSpew_WarpTranspiler, + "unsupported CacheIR opcode %s for JSOp::%s @ %s:%u:%u", + CacheIROpNames[size_t(op)], CodeName(loc.getOp()), + script_->filename(), line, column); + return Ok(); + } + + // While on the main thread, ensure code stubs exist for ops that require + // them. + switch (op) { + case CacheOp::CallRegExpMatcherResult: + if (!cx_->realm()->jitRealm()->ensureRegExpMatcherStubExists(cx_)) { + return abort(AbortReason::Error); + } + break; + case CacheOp::CallRegExpSearcherResult: + if (!cx_->realm()->jitRealm()->ensureRegExpSearcherStubExists(cx_)) { + return abort(AbortReason::Error); + } + break; + case CacheOp::RegExpBuiltinExecMatchResult: + if (!cx_->realm()->jitRealm()->ensureRegExpExecMatchStubExists(cx_)) { + return abort(AbortReason::Error); + } + break; + case CacheOp::RegExpBuiltinExecTestResult: + if (!cx_->realm()->jitRealm()->ensureRegExpExecTestStubExists(cx_)) { + return abort(AbortReason::Error); + } + break; + default: + break; + } + } + + // Copy the ICStub data to protect against the stub being unlinked or mutated. + // We don't need to copy the CacheIRStubInfo: because we store and trace the + // stub's JitCode*, the baselineCacheIRStubCodes_ map in JitZone will keep it + // alive. + uint8_t* stubDataCopy = nullptr; + size_t bytesNeeded = stubInfo->stubDataSize(); + if (bytesNeeded > 0) { + stubDataCopy = alloc_.allocateArray(bytesNeeded); + if (!stubDataCopy) { + return abort(AbortReason::Alloc); + } + + // Note: nursery pointers are handled below so we don't need to trigger any + // GC barriers and can do a bitwise copy. + std::copy_n(stubData, bytesNeeded, stubDataCopy); + + if (!replaceNurseryAndAllocSitePointers(stub, stubInfo, stubDataCopy)) { + return abort(AbortReason::Alloc); + } + } + + JitCode* jitCode = stub->jitCode(); + + if (fallbackStub->trialInliningState() == TrialInliningState::Inlined || + fallbackStub->trialInliningState() == + TrialInliningState::MonomorphicInlined) { + bool inlinedCall; + MOZ_TRY_VAR(inlinedCall, maybeInlineCall(snapshots, loc, stub, fallbackStub, + stubDataCopy)); + if (inlinedCall) { + return Ok(); + } + } + + if (!AddOpSnapshot(alloc_, snapshots, offset, jitCode, stubInfo, + stubDataCopy)) { + return abort(AbortReason::Alloc); + } + + fallbackStub->setUsedByTranspiler(); + + return Ok(); +} + +AbortReasonOr WarpScriptOracle::maybeInlineCall( + WarpOpSnapshotList& snapshots, BytecodeLocation loc, ICCacheIRStub* stub, + ICFallbackStub* fallbackStub, uint8_t* stubDataCopy) { + Maybe inlineData = FindInlinableOpData(stub, loc); + if (inlineData.isNothing()) { + return false; + } + + RootedFunction targetFunction(cx_, inlineData->target); + if (!TrialInliner::canInline(targetFunction, script_, loc)) { + return false; + } + + bool isTrialInlined = + fallbackStub->trialInliningState() == TrialInliningState::Inlined; + MOZ_ASSERT_IF(!isTrialInlined, fallbackStub->trialInliningState() == + TrialInliningState::MonomorphicInlined); + + RootedScript targetScript(cx_, targetFunction->nonLazyScript()); + ICScript* icScript = nullptr; + if (isTrialInlined) { + icScript = inlineData->icScript; + } else { + JitScript* jitScript = targetScript->jitScript(); + icScript = jitScript->icScript(); + } + + if (!icScript) { + return false; + } + + // This is just a cheap check to limit the damage we can do to ourselves if + // we try to monomorphically inline an indirectly recursive call. + const uint32_t maxInliningDepth = 8; + if (!isTrialInlined && + info_->inlineScriptTree()->depth() > maxInliningDepth) { + return false; + } + + // And this is a second cheap check to ensure monomorphic inlining doesn't + // cause us to blow past our script size budget. + if (oracle_->accumulatedBytecodeSize() + targetScript->length() > + JitOptions.ionMaxScriptSize) { + return false; + } + + // Add the inlined script to the inline script tree. + LifoAlloc* lifoAlloc = alloc_.lifoAlloc(); + InlineScriptTree* inlineScriptTree = info_->inlineScriptTree()->addCallee( + &alloc_, loc.toRawBytecode(), targetScript); + if (!inlineScriptTree) { + return abort(AbortReason::Alloc); + } + + // Create a CompileInfo for the inlined script. + jsbytecode* osrPc = nullptr; + bool needsArgsObj = targetScript->needsArgsObj(); + CompileInfo* info = lifoAlloc->new_( + mirGen_.runtime, targetScript, targetFunction, osrPc, needsArgsObj, + inlineScriptTree); + if (!info) { + return abort(AbortReason::Alloc); + } + + // Take a snapshot of the CacheIR. + uint32_t offset = loc.bytecodeToOffset(script_); + JitCode* jitCode = stub->jitCode(); + const CacheIRStubInfo* stubInfo = stub->stubInfo(); + WarpCacheIR* cacheIRSnapshot = new (alloc_.fallible()) + WarpCacheIR(offset, jitCode, stubInfo, stubDataCopy); + if (!cacheIRSnapshot) { + return abort(AbortReason::Alloc); + } + + // Take a snapshot of the inlined script (which may do more + // inlining recursively). + WarpScriptOracle scriptOracle(cx_, oracle_, targetScript, info, icScript); + + AbortReasonOr maybeScriptSnapshot = + scriptOracle.createScriptSnapshot(); + + if (maybeScriptSnapshot.isErr()) { + JitSpew(JitSpew_WarpTranspiler, "Can't create snapshot for JSOp::%s", + CodeName(loc.getOp())); + + switch (maybeScriptSnapshot.unwrapErr()) { + case AbortReason::Disable: { + // If the target script can't be warp-compiled, mark it as + // uninlineable, clean up, and fall through to the non-inlined path. + ICEntry* entry = icScript_->icEntryForStub(fallbackStub); + if (entry->firstStub() == stub) { + fallbackStub->unlinkStub(cx_->zone(), entry, /*prev=*/nullptr, stub); + } + targetScript->setUninlineable(); + info_->inlineScriptTree()->removeCallee(inlineScriptTree); + if (isTrialInlined) { + icScript_->removeInlinedChild(loc.bytecodeToOffset(script_)); + } + fallbackStub->setTrialInliningState(TrialInliningState::Failure); + return false; + } + case AbortReason::Error: + case AbortReason::Alloc: + return Err(maybeScriptSnapshot.unwrapErr()); + default: + MOZ_CRASH("Unexpected abort reason"); + } + } + + WarpScriptSnapshot* scriptSnapshot = maybeScriptSnapshot.unwrap(); + if (!isTrialInlined) { + scriptSnapshot->markIsMonomorphicInlined(); + } + + oracle_->addScriptSnapshot(scriptSnapshot, icScript, targetScript->length()); + + if (!AddOpSnapshot(alloc_, snapshots, offset, + cacheIRSnapshot, scriptSnapshot, info)) { + return abort(AbortReason::Alloc); + } + fallbackStub->setUsedByTranspiler(); + return true; +} + +struct TypeFrequency { + TypeData typeData_; + uint32_t successCount_; + TypeFrequency(TypeData typeData, uint32_t successCount) + : typeData_(typeData), successCount_(successCount) {} + + // Sort highest frequency first. + bool operator<(const TypeFrequency& other) const { + return other.successCount_ < successCount_; + } +}; + +AbortReasonOr WarpScriptOracle::maybeInlinePolymorphicTypes( + WarpOpSnapshotList& snapshots, BytecodeLocation loc, + ICCacheIRStub* firstStub, ICFallbackStub* fallbackStub) { + MOZ_ASSERT(ICSupportsPolymorphicTypeData(loc.getOp())); + + // We use polymorphic type data if there are multiple active stubs, + // all of which have type data available. + Vector candidates; + for (ICStub* stub = firstStub; !stub->isFallback(); + stub = stub->maybeNext()) { + ICCacheIRStub* cacheIRStub = stub->toCacheIRStub(); + uint32_t successCount = + cacheIRStub->enteredCount() - cacheIRStub->next()->enteredCount(); + if (successCount == 0) { + continue; + } + TypeData types = cacheIRStub->typeData(); + if (!types.hasData()) { + return false; + } + if (!candidates.append(TypeFrequency(types, successCount))) { + return abort(AbortReason::Alloc); + } + } + if (candidates.length() < 2) { + return false; + } + + // Sort candidates by success frequency. + std::sort(candidates.begin(), candidates.end()); + + TypeDataList list; + for (auto& candidate : candidates) { + list.addTypeData(candidate.typeData_); + } + + uint32_t offset = loc.bytecodeToOffset(script_); + if (!AddOpSnapshot(alloc_, snapshots, offset, list)) { + return abort(AbortReason::Alloc); + } + + return true; +} + +bool WarpScriptOracle::replaceNurseryAndAllocSitePointers( + ICCacheIRStub* stub, const CacheIRStubInfo* stubInfo, + uint8_t* stubDataCopy) { + // If the stub data contains nursery object pointers, replace them with the + // corresponding nursery index. See WarpObjectField. + // + // If the stub data contains allocation site pointers replace them with the + // initial heap to use, because the site's state may be mutated by the main + // thread while we are compiling. + // + // Also asserts non-object fields don't contain nursery pointers. + + uint32_t field = 0; + size_t offset = 0; + while (true) { + StubField::Type fieldType = stubInfo->fieldType(field); + switch (fieldType) { + case StubField::Type::RawInt32: + case StubField::Type::RawPointer: + case StubField::Type::RawInt64: + case StubField::Type::Double: + break; + case StubField::Type::Shape: + static_assert(std::is_convertible_v, + "Code assumes shapes are tenured"); + break; + case StubField::Type::GetterSetter: + static_assert(std::is_convertible_v, + "Code assumes GetterSetters are tenured"); + break; + case StubField::Type::Symbol: + static_assert(std::is_convertible_v, + "Code assumes symbols are tenured"); + break; + case StubField::Type::BaseScript: + static_assert(std::is_convertible_v, + "Code assumes scripts are tenured"); + break; + case StubField::Type::JitCode: + static_assert(std::is_convertible_v, + "Code assumes JitCodes are tenured"); + break; + case StubField::Type::JSObject: { + JSObject* obj = + stubInfo->getStubField(stub, offset); + if (IsInsideNursery(obj)) { + uint32_t nurseryIndex; + if (!oracle_->registerNurseryObject(obj, &nurseryIndex)) { + return false; + } + uintptr_t oldWord = WarpObjectField::fromObject(obj).rawData(); + uintptr_t newWord = + WarpObjectField::fromNurseryIndex(nurseryIndex).rawData(); + stubInfo->replaceStubRawWord(stubDataCopy, offset, oldWord, newWord); + } + break; + } + case StubField::Type::String: { +#ifdef DEBUG + JSString* str = + stubInfo->getStubField(stub, offset); + MOZ_ASSERT(!IsInsideNursery(str)); +#endif + break; + } + case StubField::Type::Id: { +#ifdef DEBUG + // jsid never contains nursery-allocated things. + jsid id = stubInfo->getStubField(stub, offset); + MOZ_ASSERT_IF(id.isGCThing(), + !IsInsideNursery(id.toGCCellPtr().asCell())); +#endif + break; + } + case StubField::Type::Value: { +#ifdef DEBUG + Value v = + stubInfo->getStubField(stub, offset); + MOZ_ASSERT_IF(v.isGCThing(), !IsInsideNursery(v.toGCThing())); +#endif + break; + } + case StubField::Type::AllocSite: { + uintptr_t oldWord = stubInfo->getStubRawWord(stub, offset); + auto* site = reinterpret_cast(oldWord); + gc::Heap initialHeap = site->initialHeap(); + uintptr_t newWord = uintptr_t(initialHeap); + stubInfo->replaceStubRawWord(stubDataCopy, offset, oldWord, newWord); + break; + } + case StubField::Type::Limit: + return true; // Done. + } + field++; + offset += StubField::sizeInBytes(fieldType); + } +} + +bool WarpOracle::registerNurseryObject(JSObject* obj, uint32_t* nurseryIndex) { + MOZ_ASSERT(IsInsideNursery(obj)); + + auto p = nurseryObjectsMap_.lookupForAdd(obj); + if (p) { + *nurseryIndex = p->value(); + return true; + } + + if (!nurseryObjects_.append(obj)) { + return false; + } + *nurseryIndex = nurseryObjects_.length() - 1; + return nurseryObjectsMap_.add(p, obj, *nurseryIndex); +} -- cgit v1.2.3