blob: 78aeeaa72ef3a2b01085460e1b9d3d9e67f412fc [file] [log] [blame]
/*
* Copyright (C) 2025 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <JavaScriptCore/StopTheWorldCallback.h>
#include <JavaScriptCore/VMThreadContext.h>
#include <wtf/Atomics.h>
#include <wtf/Condition.h>
#include <wtf/DoublyLinkedList.h>
#include <wtf/Lock.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/ScopedLambda.h>
#include <wtf/Seconds.h>
#include <wtf/StdLibExtras.h>
namespace JSC {
class VM;
// Understanding Stop the World (STW)
// ==================================
//
// Intuition on how to think about things?
// =======================================
// The actors in play for a Stop the World story are:
// 1. VMManager
// 2. VM
// 3. a Conductor Agent
//
// Events / actions involved in the Stop the World story are:
// 1. Stop requests (with a given StopReason)
// 2. Stop callback handlers
//
// An intuitive way to think about the Stop the World story is:
//
// 1. VMManager is an abstraction representing the process. There is only 1 singleton
// VMManager instance, and it coordinates the tracking and scheduling of VMs.
//
// 2. VM (and its VMThreadContext) represents a thread. A VM instance may actually
// run on different machine threads at different times (JSC's API allows this).
// However, from VMManager's perspective, each VM is like a thread that can be
// suspended / stopped, and resumed.
//
// FIXME: the current VMManager does NOT yet handle cases where more than one VM
// is run on the same machine thread e.g. one VM1 calls into C++, which in turn
// calls into VM2. VM2 now has control of the CPU, but VMManager does not know
// that VM1 is in a way "deactivated". This scenario cannot manifest in WebKit
// with web workloads though.
//
// 3. The Conductor Agent is something like a Debugger agent that tells VMManager
// to stop or resume VMs / threads.
//
// 4. Stop requests are like interrupts. A stop being requested is analogous to an
// interrupt firing.
//
// 5. Stop callback handlers are like interrupt handlers that masked out all interrupts
// so that no other interrupts can fire while the current one is being handled.
//
// When a stop request occurs, VMManager::notifyVMStop() will dispatch a callback
// to the appropriate handler for that request. Similar to the interrupt
// analogy, only one request can be serviced at one time. All other requests
// regardless of priority will be blocked (and held in pending) until the current
// request is done being serviced.
//
// World Execution Modes
// =====================
// The VMManager has a notion of a world mode (see VMManager::Mode). These modes are:
// 1. RunAll - all threads can run or are running.
// 2. RunOne - only one thread can run, like when a debugger is single stepping.
// 3. Stopping - a stop has been requested, and VMs are in the process of stopping.
// 4. Stopped - all threads have been stopped, and the highest priority stop request
// can now be serviced.
//
// Querying VMManager Info
// =======================
// VMManager::info() provides a view into a few pieces of VMManager state:
// 1. numberOfVMs - the number of VMs that have been constructed and are alive.
// 2. numberOfActiveVMs - the number of VMs that are activated i.e. their threads
// have entered the VM. This numberOfActiveVMs is only available while the
// world is NOT in Mode::RunAll (i.e. must be in some form of stoppage).
// 3. numberOfStoppedVMs - the number of VMs that have reached the stopping point
// in VMManager::notifyVMStop(). The currently executing targetVM is counted
// as stopped when single stepping in Mode::RunOne.
// 3. worldMode - this is the current VM world mode (as described above).
//
// Currently, this info is mainly used for testing purposes only.
//
// Initiating Stop the World
// =========================
// Stop the World begins with some agent calling VMManager::requestStopAll() with a
// StopReason. This agent can be from mutator threads or from a helper thread like
// those employed by debuggers.
//
// More than one agent can request STW at the same time. Hence, there can be multiple
// stop requests queued up while the world is being stopped.
//
// StopReason and their Priority
// =============================
// Current StopReasons are:
// None - no requests
// GC - requesting stop for Global GC
// WasmDebugger - requesting stop for Wasm Debugger (like Ctrl-C in lldb)
// MemoryDebugger - similar to WasmDebugger, but for the Memory Debugger.
//
// The priority of these requests are defined by their order of declaration.
// See definition of FOR_EACH_STOP_THE_WORLD_REASON and enum class StopReason.
// StopReason::None is a special case and has no priority.
// StopReason::GC is the current highest priority request.
// StopReason::MemoryDebugger is the current lowest priority request.
//
// StopReason is synonymous with "StopRequest".
// From the client's perspective, it is the reason for a stop request.
// From the VMManager's perspective, it is the type of stop request.
//
// Servicing Order: one request at a time
// ======================================
// The order the stop requests came in does not matter. Once the world is finally
// stopped, the higher priority request is serviced first.
// See fetchTopPriorityStopReason() and m_currentStopReason.
//
// While this request is being serviced, other requests will be ignored. During
// this time of service, new stop requests can be added to m_pendingStopRequestBits,
// but they will be ignored even if they are higher priority. We will service them
// only after the current request has resumed with RunOne or RunAll mode.
//
// StopTheWorldCallback (i.e. stop request handlers)
// =================================================
// When the world is stopped, VMManager will call back to a request handler
// based on what StopReason is in m_currentStopReason. See VMManager::notifyVMStop().
//
// The handler for GC should be static but is not currently implemented yet.
// The handlers for WasmDebugger and MemoryDebugger may be overridden. They are
// made to be overrideable only to enable testing. See VMManagerStopTheWorldTest.cpp
// for how they are used in testing.
//
// Each handler must be of the shape StopTheWorldCallback. See StopTheWorldCallback.h
// The handler will be called with a StopTheWorldEvent. The StopTheWorldEvent indicates
// where the handler is called from. This may have a use later on, but for now, the
// StopTheWorldEvent is only informational.
//
// After the handler is done, it controls how execution will proceed thereafter by
// returning one of the StopTheWorldCallback return values (see StopTheWorldCallback.h).
// The possible return values are:
//
// 1. STW_CONTINUE()
// - this is only used for testing purposes where we want to loop inside
// VMManager::notifyVMStop() while waiting for more things to handle.
// - VMManager::m_worldMode will remain in Mode::Stopped.
// 2. STW_CONTEXT_SWITCH(targetVM)
// - this is used to switch control of the handler to another VM on a different
// thread without resuming any execution. lldb's "thread select ..." can be
// implemented this way.
// - VMManager::m_worldMode will remain in Mode::Stopped.
// 3. STW_RESUME_ONE()
// - this is used to resume only the current VM thread in RunOne mode. This is
// useful for debuggers that wish to single step in the current VM. It keeps
// other threads paused / stopped while this thread executes. It is up to the
// client to detect potential resource deadlocks (e.g. using a timeout) that
// may arise from only resuming 1 thread.
// - VMManager::m_worldMode will transition from Mode::Stopped to Mode::RunOne.
// 4. STW_RESUME_ALL()
// - forces all threads to resume from a stop.
// - VMManager::m_worldMode will transition from Mode::Stopped to Mode::RunAll.
// 5. STW_RESUME()
// - Return to whatever run mode we were executing with before the current Stop
// the World request. That may be either Mode::RunOne or Mode::RunAll.
// - This allows the GC to run (with its own Stop the World requests) even while
// while we're single stepping in a debugger with Mode::RunOne.
//
// Edge Cases and Special Circumstances
// ====================================
// While in Mode::RunOne, if the VM that is running either exits the VM (aka
// deactivates) or its VM is destructed (aka shutdown), the VMManager will transition
// the world mode back to RunAll (unblocking other VMs and threads) since the current
// VM is no longer viable for continuing execution.
#define FOR_EACH_STOP_THE_WORLD_REASON(v) \
v(GC) \
v(WasmDebugger) \
v(MemoryDebugger) \
class VMManager {
WTF_MAKE_NONCOPYABLE(VMManager);
#define DECLARE_STOP_THE_WORLD_REASON_BIT_SHIFT(reason__) reason__##BitShift,
enum StopReasonBitShift {
FOR_EACH_STOP_THE_WORLD_REASON(DECLARE_STOP_THE_WORLD_REASON_BIT_SHIFT)
};
#undef DECLARE_STOP_THE_WORLD_REASON_BIT_SHIFT
#define COUNT_STOP_REASON(event) + 1
static constexpr unsigned NumberOfStopReasons = FOR_EACH_STOP_THE_WORLD_REASON(COUNT_STOP_REASON);
#undef COUNT_STOP_REASON
public:
using StopRequestBits = uint32_t;
static_assert(NumberOfStopReasons <= (sizeof(StopRequestBits) * CHAR_BIT));
#define DECLARE_STOP_THE_WORLD_REASON(reason__) reason__ = (1 << reason__##BitShift),
enum class StopReason : StopRequestBits {
None = 0,
FOR_EACH_STOP_THE_WORLD_REASON(DECLARE_STOP_THE_WORLD_REASON)
};
#undef DECLARE_STOP_THE_WORLD_REASON
enum class Error : uint8_t {
None,
TimedOut
};
JS_EXPORT_PRIVATE static VMManager& singleton();
ALWAYS_INLINE static bool isValidVM(VM* vm)
{
return vm == s_recentVM ? true : isValidVMSlow(vm);
}
// StopTheWorld APIs ======================================================
enum class Mode : uint8_t {
RunAll, // no threads are stopped.
RunOne, // all threads are stopped except for the 1 thread the debugger wants to run.
Stopping, // still waiting for the right thread to service the stop.
Stopped, // all threads have stopped, and the right thread is now servicing the stop.
};
struct Info {
unsigned numberOfVMs;
unsigned numberOfActiveVMs;
unsigned numberOfStoppedVMs;
Mode worldMode;
};
JS_EXPORT_PRIVATE static Info info();
static unsigned numberOfVMs() { return singleton().m_numberOfVMs; }
JS_EXPORT_PRIVATE static void setWasmDebuggerCallback(StopTheWorldCallback);
JS_EXPORT_PRIVATE static void setMemoryDebuggerCallback(StopTheWorldCallback);
ALWAYS_INLINE CONCURRENT_SAFE static void requestStopAll(StopReason reason)
{
singleton().requestStopAllInternal(reason);
}
ALWAYS_INLINE CONCURRENT_SAFE static void requestResumeAll(StopReason reason)
{
singleton().requestResumeAllInternal(reason);
}
void notifyVMConstruction(VM&);
void notifyVMDestruction(VM&);
void notifyVMActivation(VM&);
void notifyVMDeactivation(VM&);
void notifyVMStop(VM&, StopTheWorldEvent);
void handleVMDestructionWhileWorldStopped(VM&);
// Interation APIs ======================================================
using IteratorCallback = IterationStatus(VM&);
using TestCallback = bool(VM&);
static inline VM* findMatchingVM(const Invocable<TestCallback> auto& test)
{
SUPPRESS_FORWARD_DECL_ARG return singleton().findMatchingVMImpl(scopedLambda<TestCallback>(test));
}
static inline void forEachVM(const Invocable<IteratorCallback> auto& functor)
{
SUPPRESS_FORWARD_DECL_ARG singleton().forEachVMImpl(scopedLambda<IteratorCallback>(functor));
}
static inline Error forEachVMWithTimeout(Seconds timeout, const Invocable<IteratorCallback> auto& functor)
{
SUPPRESS_FORWARD_DECL_ARG return singleton().forEachVMWithTimeoutImpl(timeout, scopedLambda<IteratorCallback>(functor));
}
JS_EXPORT_PRIVATE static void dumpVMs();
private:
VMManager() = default;
bool hasPendingStopRequests() const { return m_pendingStopRequestBits.loadRelaxed(); }
JS_EXPORT_PRIVATE CONCURRENT_SAFE void requestStopAllInternal(StopReason);
JS_EXPORT_PRIVATE CONCURRENT_SAFE void requestResumeAllInternal(StopReason);
void resumeTheWorld() WTF_REQUIRES_LOCK(m_worldLock);
void incrementActiveVMs(VM&) WTF_REQUIRES_LOCK(m_worldLock);
void decrementActiveVMs(VM&) WTF_REQUIRES_LOCK(m_worldLock);
JS_EXPORT_PRIVATE static bool isValidVMSlow(VM*);
JS_EXPORT_PRIVATE VM* findMatchingVMImpl(const ScopedLambda<TestCallback>&);
JS_EXPORT_PRIVATE void forEachVMImpl(const ScopedLambda<IteratorCallback>&);
JS_EXPORT_PRIVATE Error forEachVMWithTimeoutImpl(Seconds timeout, const ScopedLambda<IteratorCallback>&);
void iterateVMs(const Invocable<IterationStatus(VM&)> auto&) WTF_REQUIRES_LOCK(m_worldLock);
DoublyLinkedList<VMThreadContext> m_vmList WTF_GUARDED_BY_LOCK(m_worldLock);
Lock m_worldLock;
Condition m_worldConditionVariable;
// === Variables only relevant for StopTheWorld ===================================
// Indicates if the world is running or stopped (see Modes for details).
// Requires m_worldLock to write to this, but not to read it.
Mode m_worldMode { Mode::RunAll };
// Indicates if the world needs to be in RunOne mode (and if it should resume in RunOne mode
// after stops).
bool m_useRunOneMode { false };
// Indicates if there are pending StopTheWorld requests (analogous to pending interrupts).
// In RunOne mode, all VM threads (except one) will be stopped even when m_pendingStopRequestBits
// is empty. Hence, m_pendingStopRequestBits says nothing about whether threads are / should be
// running or not.
// Can be written and read concurrently without m_worldLock.
Atomic<StopRequestBits> m_pendingStopRequestBits { 0 };
// We need to track a m_currentStopReason because we may need to continue servicing the current
// request after a context switch to a different targetVM. Conceptually, if StopTheWorld requests
// are analogous to interrupts, then when a specific interrupt is being serviced, all other
// interrupts are blocked / disabled though their status remains pending. Similarly, all other
// pending StopTheWorld requests will be blocked, and only serviced after the current one being
// serviced is done.
// Only notifyVMStop() may modify m_currentStopReason.
StopReason m_currentStopReason { StopReason::None };
// Indicates the targetVM that will service the StopTheWorld request, or the targetVM that may
// continue running in RunOne mode.
// Can obly be written to while holding the m_worldLock.
// Can be read without the m_worldLock under some restricted circumstances.
VM* m_targetVM { nullptr };
// We'll set m_numberOfActiveVMs to 99999999 when it's not supposed to hold a valid value.
// 99999999 is just some arbitrary token value that is easy to recognize but we're not likely
// to see in any real world value of m_numberOfActiveVMs. The 99999999 value will easily
// convey the idea that the value is invalid at any given point in time that info() is sampled.
static constexpr unsigned invalidNumberOfActiveVMs = 99999999;
// Indicated the number of VMs that have non-null EntryScopes.
// This value is only valid while a StopTheWorld request is being processed. It is calculated
// when the first requesting VM stops of all VMs. While a StopTheWorld request is being serviced,
// it will be updated using the VM's ConcurrentEntryScopeService.
//
// The choice to not track a valid m_numberOfActiveVMs at all times is just an optimization so
// that we can skip this work when not doing Stop the World.
unsigned m_numberOfActiveVMs { invalidNumberOfActiveVMs };
Atomic<unsigned> m_numberOfStoppedVMs { 0 };
// === End of variables only relevant for StopTheWorld =================================
unsigned m_numberOfVMs { 0 };
JS_EXPORT_PRIVATE static VM* s_recentVM;
friend class LazyNeverDestroyed<VMManager>;
};
#undef FOR_EACH_STOP_THE_WORLD_REASON
} // namespace JSC