summaryrefslogtreecommitdiffstats
path: root/js/src/jit/BaselineDebugModeOSR.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /js/src/jit/BaselineDebugModeOSR.cpp
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/jit/BaselineDebugModeOSR.cpp')
-rw-r--r--js/src/jit/BaselineDebugModeOSR.cpp561
1 files changed, 561 insertions, 0 deletions
diff --git a/js/src/jit/BaselineDebugModeOSR.cpp b/js/src/jit/BaselineDebugModeOSR.cpp
new file mode 100644
index 0000000000..bbdcccfebc
--- /dev/null
+++ b/js/src/jit/BaselineDebugModeOSR.cpp
@@ -0,0 +1,561 @@
+/* -*- 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/BaselineDebugModeOSR.h"
+
+#include "jit/BaselineFrame.h"
+#include "jit/BaselineIC.h"
+#include "jit/BaselineJIT.h"
+#include "jit/Invalidation.h"
+#include "jit/IonScript.h"
+#include "jit/JitFrames.h"
+#include "jit/JitRuntime.h"
+#include "jit/JSJitFrameIter.h"
+
+#include "jit/JitScript-inl.h"
+#include "jit/JSJitFrameIter-inl.h"
+#include "vm/JSScript-inl.h"
+#include "vm/Realm-inl.h"
+
+using namespace js;
+using namespace js::jit;
+
+struct DebugModeOSREntry {
+ JSScript* script;
+ BaselineScript* oldBaselineScript;
+ uint32_t pcOffset;
+ RetAddrEntry::Kind frameKind;
+
+ explicit DebugModeOSREntry(JSScript* script)
+ : script(script),
+ oldBaselineScript(script->baselineScript()),
+ pcOffset(uint32_t(-1)),
+ frameKind(RetAddrEntry::Kind::Invalid) {}
+
+ DebugModeOSREntry(JSScript* script, const RetAddrEntry& retAddrEntry)
+ : script(script),
+ oldBaselineScript(script->baselineScript()),
+ pcOffset(retAddrEntry.pcOffset()),
+ frameKind(retAddrEntry.kind()) {
+#ifdef DEBUG
+ MOZ_ASSERT(pcOffset == retAddrEntry.pcOffset());
+ MOZ_ASSERT(frameKind == retAddrEntry.kind());
+#endif
+ }
+
+ DebugModeOSREntry(DebugModeOSREntry&& other)
+ : script(other.script),
+ oldBaselineScript(other.oldBaselineScript),
+ pcOffset(other.pcOffset),
+ frameKind(other.frameKind) {}
+
+ bool recompiled() const {
+ return oldBaselineScript != script->baselineScript();
+ }
+};
+
+using DebugModeOSREntryVector = Vector<DebugModeOSREntry>;
+
+class UniqueScriptOSREntryIter {
+ const DebugModeOSREntryVector& entries_;
+ size_t index_;
+
+ public:
+ explicit UniqueScriptOSREntryIter(const DebugModeOSREntryVector& entries)
+ : entries_(entries), index_(0) {}
+
+ bool done() { return index_ == entries_.length(); }
+
+ const DebugModeOSREntry& entry() {
+ MOZ_ASSERT(!done());
+ return entries_[index_];
+ }
+
+ UniqueScriptOSREntryIter& operator++() {
+ MOZ_ASSERT(!done());
+ while (++index_ < entries_.length()) {
+ bool unique = true;
+ for (size_t i = 0; i < index_; i++) {
+ if (entries_[i].script == entries_[index_].script) {
+ unique = false;
+ break;
+ }
+ }
+ if (unique) {
+ break;
+ }
+ }
+ return *this;
+ }
+};
+
+static bool CollectJitStackScripts(JSContext* cx,
+ const DebugAPI::ExecutionObservableSet& obs,
+ const ActivationIterator& activation,
+ DebugModeOSREntryVector& entries) {
+ for (OnlyJSJitFrameIter iter(activation); !iter.done(); ++iter) {
+ const JSJitFrameIter& frame = iter.frame();
+ switch (frame.type()) {
+ case FrameType::BaselineJS: {
+ JSScript* script = frame.script();
+
+ if (!obs.shouldRecompileOrInvalidate(script)) {
+ break;
+ }
+
+ BaselineFrame* baselineFrame = frame.baselineFrame();
+
+ if (baselineFrame->runningInInterpreter()) {
+ // Baseline Interpreter frames for scripts that have a BaselineScript
+ // or IonScript don't need to be patched but they do need to be
+ // invalidated and recompiled. See also CollectInterpreterStackScripts
+ // for C++ interpreter frames.
+ if (!entries.append(DebugModeOSREntry(script))) {
+ return false;
+ }
+ } else {
+ // The frame must be settled on a pc with a RetAddrEntry.
+ uint8_t* retAddr = frame.resumePCinCurrentFrame();
+ const RetAddrEntry& retAddrEntry =
+ script->baselineScript()->retAddrEntryFromReturnAddress(retAddr);
+ if (!entries.append(DebugModeOSREntry(script, retAddrEntry))) {
+ return false;
+ }
+ }
+
+ break;
+ }
+
+ case FrameType::BaselineStub:
+ break;
+
+ case FrameType::IonJS: {
+ InlineFrameIterator inlineIter(cx, &frame);
+ while (true) {
+ if (obs.shouldRecompileOrInvalidate(inlineIter.script())) {
+ if (!entries.append(DebugModeOSREntry(inlineIter.script()))) {
+ return false;
+ }
+ }
+ if (!inlineIter.more()) {
+ break;
+ }
+ ++inlineIter;
+ }
+ break;
+ }
+
+ default:;
+ }
+ }
+
+ return true;
+}
+
+static bool CollectInterpreterStackScripts(
+ JSContext* cx, const DebugAPI::ExecutionObservableSet& obs,
+ const ActivationIterator& activation, DebugModeOSREntryVector& entries) {
+ // Collect interpreter frame stacks with IonScript or BaselineScript as
+ // well. These do not need to be patched, but do need to be invalidated
+ // and recompiled.
+ InterpreterActivation* act = activation.activation()->asInterpreter();
+ for (InterpreterFrameIterator iter(act); !iter.done(); ++iter) {
+ JSScript* script = iter.frame()->script();
+ if (obs.shouldRecompileOrInvalidate(script)) {
+ if (!entries.append(DebugModeOSREntry(iter.frame()->script()))) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+#ifdef JS_JITSPEW
+static const char* RetAddrEntryKindToString(RetAddrEntry::Kind kind) {
+ switch (kind) {
+ case RetAddrEntry::Kind::IC:
+ return "IC";
+ case RetAddrEntry::Kind::CallVM:
+ return "callVM";
+ case RetAddrEntry::Kind::StackCheck:
+ return "stack check";
+ case RetAddrEntry::Kind::InterruptCheck:
+ return "interrupt check";
+ case RetAddrEntry::Kind::DebugTrap:
+ return "debug trap";
+ case RetAddrEntry::Kind::DebugPrologue:
+ return "debug prologue";
+ case RetAddrEntry::Kind::DebugAfterYield:
+ return "debug after yield";
+ case RetAddrEntry::Kind::DebugEpilogue:
+ return "debug epilogue";
+ default:
+ MOZ_CRASH("bad RetAddrEntry kind");
+ }
+}
+#endif // JS_JITSPEW
+
+static void SpewPatchBaselineFrame(const uint8_t* oldReturnAddress,
+ const uint8_t* newReturnAddress,
+ JSScript* script,
+ RetAddrEntry::Kind frameKind,
+ const jsbytecode* pc) {
+ JitSpew(JitSpew_BaselineDebugModeOSR,
+ "Patch return %p -> %p on BaselineJS frame (%s:%u:%u) from %s at %s",
+ oldReturnAddress, newReturnAddress, script->filename(),
+ script->lineno(), script->column(),
+ RetAddrEntryKindToString(frameKind), CodeName(JSOp(*pc)));
+}
+
+static void PatchBaselineFramesForDebugMode(
+ JSContext* cx, const DebugAPI::ExecutionObservableSet& obs,
+ const ActivationIterator& activation, DebugModeOSREntryVector& entries,
+ size_t* start) {
+ //
+ // Recompile Patching Overview
+ //
+ // When toggling debug mode with live baseline scripts on the stack, we
+ // could have entered the VM via the following ways from the baseline
+ // script.
+ //
+ // Off to On:
+ // A. From a non-prologue IC (fallback stub or "can call" stub).
+ // B. From a VM call.
+ // C. From inside the interrupt handler via the prologue stack check.
+ //
+ // On to Off:
+ // - All the ways above.
+ // D. From the debug trap handler.
+ // E. From the debug prologue.
+ // F. From the debug epilogue.
+ // G. From a JSOp::AfterYield instruction.
+ //
+ // In general, we patch the return address from VM calls and ICs to the
+ // corresponding entry in the recompiled BaselineScript. For entries that are
+ // not present in the recompiled script (cases D to G above) we switch the
+ // frame to interpreter mode and resume in the Baseline Interpreter.
+ //
+ // Specifics on what needs to be done are documented below.
+ //
+
+ const BaselineInterpreter& baselineInterp =
+ cx->runtime()->jitRuntime()->baselineInterpreter();
+
+ CommonFrameLayout* prev = nullptr;
+ size_t entryIndex = *start;
+
+ for (OnlyJSJitFrameIter iter(activation); !iter.done(); ++iter) {
+ const JSJitFrameIter& frame = iter.frame();
+ switch (frame.type()) {
+ case FrameType::BaselineJS: {
+ // If the script wasn't recompiled or is not observed, there's
+ // nothing to patch.
+ if (!obs.shouldRecompileOrInvalidate(frame.script())) {
+ break;
+ }
+
+ DebugModeOSREntry& entry = entries[entryIndex];
+
+ if (!entry.recompiled()) {
+ entryIndex++;
+ break;
+ }
+
+ BaselineFrame* baselineFrame = frame.baselineFrame();
+ if (baselineFrame->runningInInterpreter()) {
+ // We recompiled the script's BaselineScript but Baseline Interpreter
+ // frames don't need to be patched.
+ entryIndex++;
+ break;
+ }
+
+ JSScript* script = entry.script;
+ uint32_t pcOffset = entry.pcOffset;
+ jsbytecode* pc = script->offsetToPC(pcOffset);
+
+ MOZ_ASSERT(script == frame.script());
+ MOZ_ASSERT(pcOffset < script->length());
+
+ BaselineScript* bl = script->baselineScript();
+ RetAddrEntry::Kind kind = entry.frameKind;
+ uint8_t* retAddr = nullptr;
+ switch (kind) {
+ case RetAddrEntry::Kind::IC:
+ case RetAddrEntry::Kind::CallVM:
+ case RetAddrEntry::Kind::InterruptCheck:
+ case RetAddrEntry::Kind::StackCheck: {
+ // Cases A, B, C above.
+ //
+ // For the baseline frame here, we resume right after the CallVM or
+ // IC returns.
+ //
+ // For CallVM (case B) the assumption is that all callVMs which can
+ // trigger debug mode OSR are the *only* callVMs generated for their
+ // respective pc locations in the Baseline JIT code.
+ const RetAddrEntry* retAddrEntry = nullptr;
+ switch (kind) {
+ case RetAddrEntry::Kind::IC:
+ case RetAddrEntry::Kind::CallVM:
+ case RetAddrEntry::Kind::InterruptCheck:
+ retAddrEntry = &bl->retAddrEntryFromPCOffset(pcOffset, kind);
+ break;
+ case RetAddrEntry::Kind::StackCheck:
+ retAddrEntry = &bl->prologueRetAddrEntry(kind);
+ break;
+ default:
+ MOZ_CRASH("Unexpected kind");
+ }
+ retAddr = bl->returnAddressForEntry(*retAddrEntry);
+ SpewPatchBaselineFrame(prev->returnAddress(), retAddr, script, kind,
+ pc);
+ break;
+ }
+ case RetAddrEntry::Kind::DebugPrologue:
+ case RetAddrEntry::Kind::DebugEpilogue:
+ case RetAddrEntry::Kind::DebugTrap:
+ case RetAddrEntry::Kind::DebugAfterYield: {
+ // Cases D, E, F, G above.
+ //
+ // Resume in the Baseline Interpreter because these callVMs are not
+ // present in the new BaselineScript if we recompiled without debug
+ // instrumentation.
+ if (kind == RetAddrEntry::Kind::DebugPrologue) {
+ frame.baselineFrame()->switchFromJitToInterpreterAtPrologue(cx);
+ } else {
+ frame.baselineFrame()->switchFromJitToInterpreter(cx, pc);
+ }
+ switch (kind) {
+ case RetAddrEntry::Kind::DebugTrap:
+ // DebugTrap handling is different from the ones below because
+ // it's not a callVM but a trampoline call at the start of the
+ // bytecode op. When we return to the frame we can resume at the
+ // interpretOp label.
+ retAddr = baselineInterp.interpretOpAddr().value;
+ break;
+ case RetAddrEntry::Kind::DebugPrologue:
+ retAddr = baselineInterp.retAddrForDebugPrologueCallVM();
+ break;
+ case RetAddrEntry::Kind::DebugEpilogue:
+ retAddr = baselineInterp.retAddrForDebugEpilogueCallVM();
+ break;
+ case RetAddrEntry::Kind::DebugAfterYield:
+ retAddr = baselineInterp.retAddrForDebugAfterYieldCallVM();
+ break;
+ default:
+ MOZ_CRASH("Unexpected kind");
+ }
+ SpewPatchBaselineFrame(prev->returnAddress(), retAddr, script, kind,
+ pc);
+ break;
+ }
+ case RetAddrEntry::Kind::NonOpCallVM:
+ case RetAddrEntry::Kind::Invalid:
+ // These cannot trigger BaselineDebugModeOSR.
+ MOZ_CRASH("Unexpected RetAddrEntry Kind");
+ }
+
+ prev->setReturnAddress(retAddr);
+ entryIndex++;
+ break;
+ }
+
+ case FrameType::IonJS: {
+ // Nothing to patch.
+ InlineFrameIterator inlineIter(cx, &frame);
+ while (true) {
+ if (obs.shouldRecompileOrInvalidate(inlineIter.script())) {
+ entryIndex++;
+ }
+ if (!inlineIter.more()) {
+ break;
+ }
+ ++inlineIter;
+ }
+ break;
+ }
+
+ default:;
+ }
+
+ prev = frame.current();
+ }
+
+ *start = entryIndex;
+}
+
+static void SkipInterpreterFrameEntries(
+ const DebugAPI::ExecutionObservableSet& obs,
+ const ActivationIterator& activation, size_t* start) {
+ size_t entryIndex = *start;
+
+ // Skip interpreter frames, which do not need patching.
+ InterpreterActivation* act = activation.activation()->asInterpreter();
+ for (InterpreterFrameIterator iter(act); !iter.done(); ++iter) {
+ if (obs.shouldRecompileOrInvalidate(iter.frame()->script())) {
+ entryIndex++;
+ }
+ }
+
+ *start = entryIndex;
+}
+
+static bool RecompileBaselineScriptForDebugMode(
+ JSContext* cx, JSScript* script, DebugAPI::IsObserving observing) {
+ // If a script is on the stack multiple times, it may have already
+ // been recompiled.
+ if (script->baselineScript()->hasDebugInstrumentation() == observing) {
+ return true;
+ }
+
+ JitSpew(JitSpew_BaselineDebugModeOSR, "Recompiling (%s:%u:%u) for %s",
+ script->filename(), script->lineno(), script->column(),
+ observing ? "DEBUGGING" : "NORMAL EXECUTION");
+
+ AutoKeepJitScripts keepJitScripts(cx);
+ BaselineScript* oldBaselineScript =
+ script->jitScript()->clearBaselineScript(cx->gcContext(), script);
+
+ MethodStatus status =
+ BaselineCompile(cx, script, /* forceDebugMode = */ observing);
+ if (status != Method_Compiled) {
+ // We will only fail to recompile for debug mode due to OOM. Restore
+ // the old baseline script in case something doesn't properly
+ // propagate OOM.
+ MOZ_ASSERT(status == Method_Error);
+ script->jitScript()->setBaselineScript(script, oldBaselineScript);
+ return false;
+ }
+
+ // Don't destroy the old baseline script yet, since if we fail any of the
+ // recompiles we need to rollback all the old baseline scripts.
+ MOZ_ASSERT(script->baselineScript()->hasDebugInstrumentation() == observing);
+ return true;
+}
+
+static bool InvalidateScriptsInZone(JSContext* cx, Zone* zone,
+ const Vector<DebugModeOSREntry>& entries) {
+ RecompileInfoVector invalid;
+ for (UniqueScriptOSREntryIter iter(entries); !iter.done(); ++iter) {
+ JSScript* script = iter.entry().script;
+ if (script->zone() != zone) {
+ continue;
+ }
+
+ if (script->hasIonScript()) {
+ if (!invalid.emplaceBack(script, script->ionScript()->compilationId())) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+ }
+
+ // Cancel off-thread Ion compile for anything that has a
+ // BaselineScript. If we relied on the call to Invalidate below to
+ // cancel off-thread Ion compiles, only those with existing IonScripts
+ // would be cancelled.
+ if (script->hasBaselineScript()) {
+ CancelOffThreadIonCompile(script);
+ }
+ }
+
+ // No need to cancel off-thread Ion compiles again, we already did it
+ // above.
+ Invalidate(cx, invalid,
+ /* resetUses = */ true, /* cancelOffThread = */ false);
+ return true;
+}
+
+static void UndoRecompileBaselineScriptsForDebugMode(
+ JSContext* cx, const DebugModeOSREntryVector& entries) {
+ // In case of failure, roll back the entire set of active scripts so that
+ // we don't have to patch return addresses on the stack.
+ for (UniqueScriptOSREntryIter iter(entries); !iter.done(); ++iter) {
+ const DebugModeOSREntry& entry = iter.entry();
+ JSScript* script = entry.script;
+ BaselineScript* baselineScript = script->baselineScript();
+ if (entry.recompiled()) {
+ script->jitScript()->setBaselineScript(script, entry.oldBaselineScript);
+ BaselineScript::Destroy(cx->gcContext(), baselineScript);
+ }
+ }
+}
+
+bool jit::RecompileOnStackBaselineScriptsForDebugMode(
+ JSContext* cx, const DebugAPI::ExecutionObservableSet& obs,
+ DebugAPI::IsObserving observing) {
+ // First recompile the active scripts on the stack and patch the live
+ // frames.
+ Vector<DebugModeOSREntry> entries(cx);
+
+ for (ActivationIterator iter(cx); !iter.done(); ++iter) {
+ if (iter->isJit()) {
+ if (!CollectJitStackScripts(cx, obs, iter, entries)) {
+ return false;
+ }
+ } else if (iter->isInterpreter()) {
+ if (!CollectInterpreterStackScripts(cx, obs, iter, entries)) {
+ return false;
+ }
+ }
+ }
+
+ if (entries.empty()) {
+ return true;
+ }
+
+ // When the profiler is enabled, we need to have suppressed sampling,
+ // since the basline jit scripts are in a state of flux.
+ MOZ_ASSERT(!cx->isProfilerSamplingEnabled());
+
+ // Invalidate all scripts we are recompiling.
+ if (Zone* zone = obs.singleZone()) {
+ if (!InvalidateScriptsInZone(cx, zone, entries)) {
+ return false;
+ }
+ } else {
+ using ZoneRange = DebugAPI::ExecutionObservableSet::ZoneRange;
+ for (ZoneRange r = obs.zones()->all(); !r.empty(); r.popFront()) {
+ if (!InvalidateScriptsInZone(cx, r.front(), entries)) {
+ return false;
+ }
+ }
+ }
+
+ // Try to recompile all the scripts. If we encounter an error, we need to
+ // roll back as if none of the compilations happened, so that we don't
+ // crash.
+ for (size_t i = 0; i < entries.length(); i++) {
+ JSScript* script = entries[i].script;
+ AutoRealm ar(cx, script);
+ if (!RecompileBaselineScriptForDebugMode(cx, script, observing)) {
+ UndoRecompileBaselineScriptsForDebugMode(cx, entries);
+ return false;
+ }
+ }
+
+ // If all recompiles succeeded, destroy the old baseline scripts and patch
+ // the live frames.
+ //
+ // After this point the function must be infallible.
+
+ for (UniqueScriptOSREntryIter iter(entries); !iter.done(); ++iter) {
+ const DebugModeOSREntry& entry = iter.entry();
+ if (entry.recompiled()) {
+ BaselineScript::Destroy(cx->gcContext(), entry.oldBaselineScript);
+ }
+ }
+
+ size_t processed = 0;
+ for (ActivationIterator iter(cx); !iter.done(); ++iter) {
+ if (iter->isJit()) {
+ PatchBaselineFramesForDebugMode(cx, obs, iter, entries, &processed);
+ } else if (iter->isInterpreter()) {
+ SkipInterpreterFrameEntries(obs, iter, &processed);
+ }
+ }
+ MOZ_ASSERT(processed == entries.length());
+
+ return true;
+}