dart-sdk/runtime/vm/message_handler.h
Martin Kustermann 2ea92acba5 [vm] Make reloading of isolate groups use new safepoint-level mechanism
The current hot-reload implementation [0] will perform a reload by
first sending OOB messages to all isolates and waiting until those OOB
messages are being handled. The handler of the OOB message will block
the thread (and unschedule isolate) and notify the thread performing
reload it's ready.

This requires that all isolates within a group can actually run & block.
This is the case for the VM implementation of isolates (as they are
run an unlimited size thread pool).

Though flutter seems to multiplex several engine isolates on the same OS
thread. Reloading can then result in one engine isolate performing
reload waiting for another to act on the OOB message (which it will not
do as it's multiplexed on the same thread as the former).

Now that we have a more flexible safepointing mechanism (introduced in
[1]) we can utilize for hot reloading by introducing a new "reloading"
safepoint level.

Reload safepoints
-----------------------

We introduce a new safepoint level (SafepointLevel::kGCAndDeoptAndReload).

Being at a "reload safepoint" implies being at a "deopt safepoint"
which implies being at a "gc safepoint".

Code has to explicitly opt-into making safepoint checks participate /
check into "reload safepoints" using [ReloadParticipationScope]. We do
that at certain well-defined places where reload is possible (e.g. event
loop boundaries, descheduling of isolates, OOM message processing, ...).

While running under [NoReloadScope] we disable checking into "reload
safepoints".

Initiator of hot-reload
-----------------------

When a mutator initiates a reload operation (e.g. as part of a
`ReloadSources` `vm-service` API call) it will use a
[ReloadSafepointOperationScope] to get all other mutators to a
safepoint.

For mutators that aren't already at a "reload safepoint", we'll
notify them via an OOB message (instead of scheduling kVMInterrupt).

While waiting for all mutators to check into a "reload safepoint", the
thread is itself at a safepoint (as other mutators may perform lower
level safepoint operations - e.g. GC, Deopt, ...)

Once all mutators are at a "reload safepoint" the thread will take
ownership of all safepoint levels.

Other mutators
-----------------------

Mutators can be at a "reload safepoint" already (e.g. isolate is not
scheduled). If they try to exit safepoint they will block until the
reload operation is finished.

Mutators that are not at a "reload safepoint" (e.g. executing Dart or VM
code) will be sent an OOB message indicating it should check into a
"reload safepoint". We assume mutators make progress until they can
process OOB message.

Mutators may run under a [NoReloadScope] when handling the OOM message.
In that case they will not check into the "reload safepoint" and simply
ignore the message. To ensure the thread will eventually check-in,
we'll make the destructor of [~NoReloadScope] check & send itself a new OOB
message indicating reload should happen. Eventually getting the mutator
to process the OOM message (which is a well-defined place where we can
check into the reload safepoint).

Non-isolate mutators such as the background compiler do not react to OOB
messages. This means that either those mutators have to be stopped (e.g.
bg compiler) before initiating a reload safepoint operation, the
threads have to explicitly opt-into participating in reload safepoints
or the threads have to deschedule themselves eventually.

Misc
----

Owning a reload safepoint operation implies also owning the deopt &
gc safepoint operation. Yet some code would like to ensure it actually
runs under a [DeoptSafepointOperatoinScope]/[GCSafepointOperationScope].
=> The `Thread::OwnsGCSafepoint()` handles that.

While performing hot-reload we may exercise common code (e.g. kernel
loader, ...) that acquires safepoint locks. Normally it's disallows to
acquire safepoint locks while holding a safepoint operation (since
mutators may be stopped at places where they hold locks, creating
deadlock scenarios).
=> We explicitly opt code into participating in reload safepointing
requests. Those well-defined places aren't holding safepoint locks.
=> The `Thread::CanAcquireSafepointLocks()` will return `true` despite
owning a reload operation. (But if one also holds deopt/gc safepoint
operation it will return false)

Example where this matters: As part of hot-reload, we load kernel which
may create new symbols. The symbol creation code may acquire the symbol
lock and `InsertNewOrGet()` a symbol. This is safe as other mutators
don't hold the symbol lock at reload safepoints. The same cannot be said
for Deopt/GC safepoint operations - as they can interrupt code at many
more places where there's no guarantee that no locks are held.

[0] https://dart-review.googlesource.com/c/sdk/+/187461
[1] https://dart-review.googlesource.com/c/sdk/+/196927

Issue https://github.com/flutter/flutter/issues/124546

TEST=Newly added Reload_* tests.

Change-Id: I6842d7d2b284d043cc047fd702b7c5c7dd1fa3c5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/296183
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
2023-04-21 13:56:49 +00:00

289 lines
9.1 KiB
C++

// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#ifndef RUNTIME_VM_MESSAGE_HANDLER_H_
#define RUNTIME_VM_MESSAGE_HANDLER_H_
#include <memory>
#include "vm/isolate.h"
#include "vm/lockers.h"
#include "vm/message.h"
#include "vm/os_thread.h"
#include "vm/port_set.h"
#include "vm/thread_pool.h"
namespace dart {
// A MessageHandler is an entity capable of accepting messages.
class MessageHandler {
protected:
MessageHandler();
public:
enum MessageStatus {
kOK, // We successfully handled a message.
kError, // We encountered an error handling a message.
kShutdown, // The VM is shutting down.
};
static const char* MessageStatusString(MessageStatus status);
virtual ~MessageHandler();
// Allow subclasses to provide a handler name.
virtual const char* name() const;
typedef uword CallbackData;
typedef MessageStatus (*StartCallback)(CallbackData data);
typedef void (*EndCallback)(CallbackData data);
// Runs this message handler on the thread pool.
//
// Before processing messages, the optional StartFunction is run.
//
// A message handler will run until it terminates either normally or
// abnormally. Normal termination occurs when the message handler
// no longer has any live ports. Abnormal termination occurs when
// HandleMessage() indicates that an error has occurred during
// message processing.
// Returns false if the handler terminated abnormally, otherwise it
// returns true.
bool Run(ThreadPool* pool,
StartCallback start_callback,
EndCallback end_callback,
CallbackData data);
// Handles the next message for this message handler. Should only
// be used when not running the handler on the thread pool (via Run
// or RunBlocking).
//
// Returns true on success.
MessageStatus HandleNextMessage();
// Handles any OOB messages for this message handler. Can be used
// even if the message handler is running on the thread pool.
//
// Returns true on success.
MessageStatus HandleOOBMessages();
// Blocks the thread on a condition variable until a message arrives, and then
// handles all messages.
MessageStatus PauseAndHandleAllMessages(int64_t timeout_millis);
// Returns true if there are pending OOB messages for this message
// handler.
bool HasOOBMessages();
#if defined(TESTING)
std::unique_ptr<Message> StealOOBMessage();
#endif
// Returns true if there are pending normal messages for this message
// handler.
bool HasMessages();
// A message handler tracks how many live ports it has.
bool HasLivePorts() const { return live_ports_ > 0; }
intptr_t live_ports() const { return live_ports_; }
bool paused() const { return paused_ > 0; }
void increment_paused() { paused_++; }
void decrement_paused() {
ASSERT(paused_ > 0);
paused_--;
}
#if !defined(PRODUCT)
void DebugDump();
bool should_pause_on_start() const { return should_pause_on_start_; }
void set_should_pause_on_start(bool should_pause_on_start) {
should_pause_on_start_ = should_pause_on_start;
}
bool is_paused_on_start() const { return is_paused_on_start_; }
bool should_pause_on_exit() const { return should_pause_on_exit_; }
void set_should_pause_on_exit(bool should_pause_on_exit) {
should_pause_on_exit_ = should_pause_on_exit;
}
bool is_paused_on_exit() const { return is_paused_on_exit_; }
// Timestamp of the paused on start or paused on exit.
int64_t paused_timestamp() const { return paused_timestamp_; }
bool ShouldPauseOnStart(MessageStatus status) const;
bool ShouldPauseOnExit(MessageStatus status) const;
void PausedOnStart(bool paused);
void PausedOnExit(bool paused);
#endif
// Gives temporary ownership of |queue| and |oob_queue|. Using this object
// has the side effect that no OOB messages will be handled if a stack
// overflow interrupt is delivered.
class AcquiredQueues : public ValueObject {
public:
explicit AcquiredQueues(MessageHandler* handler);
~AcquiredQueues();
MessageQueue* queue() {
if (handler_ == nullptr) {
return nullptr;
}
return handler_->queue_;
}
MessageQueue* oob_queue() {
if (handler_ == nullptr) {
return nullptr;
}
return handler_->oob_queue_;
}
private:
MessageHandler* handler_;
SafepointMonitorLocker ml_;
friend class MessageHandler;
};
#if defined(DEBUG)
// Check that it is safe to access this message handler.
//
// For example, if this MessageHandler is an isolate, then it is
// only safe to access it when the MessageHandler is the current
// isolate.
virtual void CheckAccess();
#endif
protected:
// ------------ START PortMap API ------------
// These functions should only be called from the PortMap.
// Does this message handler correspond to the current isolate?
virtual bool IsCurrentIsolate() const { return false; }
// Return Isolate to which this message handler corresponds to.
virtual Isolate* isolate() const { return nullptr; }
// Posts a message on this handler's message queue.
// If before_events is true, then the message is enqueued before any pending
// events, but after any pending isolate library events.
void PostMessage(std::unique_ptr<Message> message,
bool before_events = false);
// Notifies this handler that a port is being closed.
void ClosePort(Dart_Port port);
// Notifies this handler that all ports are being closed.
void CloseAllPorts();
// Returns true if the handler is owned by the PortMap.
//
// This is used to delete handlers when their last live port is closed.
virtual bool OwnedByPortMap() const { return false; }
// Requests deletion of this message handler when the next task
// completes.
void RequestDeletion();
void increment_live_ports();
void decrement_live_ports();
// ------------ END PortMap API ------------
// Custom message notification. Optionally provided by subclass.
virtual void MessageNotify(Message::Priority priority);
// Handles a single message. Provided by subclass.
//
// Returns true on success.
virtual MessageStatus HandleMessage(std::unique_ptr<Message> message) = 0;
virtual void NotifyPauseOnStart() {}
virtual void NotifyPauseOnExit() {}
// TODO(iposva): Set a local field before entering MessageHandler methods.
Thread* thread() const { return Thread::Current(); }
private:
template <typename GCVisitorType>
friend void MournFinalized(GCVisitorType* visitor);
friend class PortMap;
friend class MessageHandlerTestPeer;
friend class MessageHandlerTask;
struct PortSetEntry : public PortSet<PortSetEntry>::Entry {};
// Called by MessageHandlerTask to process our task queue.
void TaskCallback();
// Checks if we have a slot for idle task execution, if we have a slot
// for idle task execution it is scheduled immediately or we wait for
// idle expiration and then attempt to schedule the idle task.
// Returns true if their is scope for idle task execution so that we
// can loop back to handle more messages or false if idle tasks are not
// scheduled.
bool CheckIfIdleLocked(MonitorLocker* ml);
// Triggers a run of the idle task.
void RunIdleTaskLocked(MonitorLocker* ml);
// NOTE: These two functions release and reacquire the monitor, you may
// need to call HandleMessages to ensure all pending messages are handled.
void PausedOnStartLocked(MonitorLocker* ml, bool paused);
void PausedOnExitLocked(MonitorLocker* ml, bool paused);
// Dequeue the next message. Prefer messages from the oob_queue_ to
// messages from the queue_.
std::unique_ptr<Message> DequeueMessage(Message::Priority min_priority);
void ClearOOBQueue();
// Handles any pending messages.
MessageStatus HandleMessages(MonitorLocker* ml,
bool allow_normal_messages,
bool allow_multiple_normal_messages);
Monitor monitor_; // Protects all fields in MessageHandler.
MessageQueue* queue_;
MessageQueue* oob_queue_;
// This flag is not thread safe and can only reliably be accessed on a single
// thread.
bool oob_message_handling_allowed_;
bool paused_for_messages_;
PortSet<PortSetEntry>
ports_; // Only accessed by [PortMap], protected by [PortMap]s lock.
intptr_t live_ports_; // The number of open ports, including control ports.
intptr_t paused_; // The number of pause messages received.
#if !defined(PRODUCT)
bool should_pause_on_start_;
bool should_pause_on_exit_;
bool is_paused_on_start_;
bool is_paused_on_exit_;
// When isolate gets paused, remember the status of the message being
// processed so that we can resume correctly(into potentially not-OK status).
MessageStatus remembered_paused_on_exit_status_;
int64_t paused_timestamp_;
#endif
bool task_running_;
bool delete_me_;
ThreadPool* pool_;
StartCallback start_callback_;
EndCallback end_callback_;
CallbackData callback_data_;
DISALLOW_COPY_AND_ASSIGN(MessageHandler);
};
} // namespace dart
#endif // RUNTIME_VM_MESSAGE_HANDLER_H_