22#include "../../attributes.hpp"
25#include "../../core/executor.hpp"
33namespace morph::reactive::detail {
38inline constexpr char const*
kOffOwner =
"morph::reactive: off the Runtime's owner";
40inline constexpr char const*
kEffectThrew =
"morph::reactive: an exception escaped an Effect";
42inline constexpr char const*
kWriteCycle =
"morph::reactive: an Effect re-ran past maxEffectRunsPerFlush";
44inline constexpr char const*
kSetInComputed =
"morph::reactive: set() inside a Computed";
48inline constexpr char const*
kSendInUpdate =
"morph::reactive: send() inside an update";
51 "morph::reactive: Query::refetch() or Mutation::run() inside a Computed";
53inline constexpr char const*
kAfterFlushThrew =
"morph::reactive: an exception escaped afterFlush";
55inline constexpr char const*
kRuntimeOutlived =
"morph::reactive: Runtime destroyed while nodes are alive";
57inline constexpr char const*
kHandlerExecutor =
"morph::reactive: a handler delivers off the Runtime's owner";
83 requires std::ranges::range<T const> && (!std::same_as<std::remove_cv_t<std::ranges::range_value_t<T const>>, T>)
85 : std::bool_constant<std::equality_comparable<T> &&
86 EqualityUsable<std::remove_cv_t<std::ranges::range_value_t<T const>>>::value> {};
96template <
typename First,
typename Second>
97struct EqualityUsable<std::pair<First, Second>> : std::bool_constant<EqualityUsable<std::remove_cv_t<First>>::value &&
98 EqualityUsable<std::remove_cv_t<Second>>::value> {
103template <
typename... Ts>
104struct EqualityUsable<std::tuple<Ts...>> : std::bool_constant<(EqualityUsable<std::remove_cv_t<Ts>>::value && ...)> {};
108template <
typename... Ts>
109struct EqualityUsable<std::variant<Ts...>> : std::bool_constant<(EqualityUsable<std::remove_cv_t<Ts>>::value && ...)> {
135class RuntimeCore :
public std::enable_shared_from_this<RuntimeCore> {
142 ~RuntimeCore() =
default;
150 [[nodiscard]]
bool onOwner() const noexcept {
return _affinity.here(); }
158 if (_affinity.here()) {
167 void report(
char const* where)
const noexcept {
169 std::uint64_t
const index = _misuses.fetch_add(1, std::memory_order_relaxed);
170 _recentMisuses.at(index %
kRecentMisuses).store(where, std::memory_order_relaxed);
171 exec::detail::noteOwner(where, _affinity.owner().coreExecutor(),
false);
176 [[nodiscard]] std::uint64_t
misuses() const noexcept {
return _misuses.load(std::memory_order_relaxed); }
181 std::uint64_t
const total =
misuses();
182 std::uint64_t
const kept = std::min<std::uint64_t>(total,
kRecentMisuses);
183 std::vector<char const*> sites;
184 sites.reserve(
static_cast<std::size_t
>(kept));
185 for (std::uint64_t index = total - kept; index < total; ++index) {
186 if (
char const*
const where = _recentMisuses.at(index %
kRecentMisuses).load(std::memory_order_relaxed)) {
187 sites.push_back(where);
203 _afterFlush = std::move(afterFlush);
208 if (_batchDepth++ == 0) {
215 [[nodiscard]] std::uint64_t
currentBatch() const noexcept {
return _batchDepth == 0 ? 0 : _batches; }
219 [[nodiscard]] std::uint64_t
batches() const noexcept {
return _batches; }
223 [[nodiscard]] std::uint64_t
flushes() const noexcept {
return _flushes; }
227 [[nodiscard]] std::uint64_t
effectsRun() const noexcept {
return _effectsRun; }
231 [[nodiscard]] std::uint64_t
lastFlushEffects() const noexcept {
return _lastFlushEffects; }
235 [[nodiscard]] std::uint64_t
maxFlushEffects() const noexcept {
return _maxFlushEffects; }
242 if (--_batchDepth == 0) {
257 if (_flushing || _isFlushRequested || _queue.empty() || _detached) {
261 _isFlushRequested =
true;
263 _affinity.owner().post([weak = weak_from_this()] {
264 if (
auto const core = weak.lock()) {
270 _isFlushRequested =
false;
292 [[nodiscard]]
bool isComputing() const noexcept {
return _computing != 0; }
308 [[nodiscard]] std::size_t
batchDepth() const noexcept {
return _batchDepth; }
312 [[nodiscard]]
bool isInWidgetEvent() const noexcept {
return _widgetEventDepth != 0; }
319 void detach() noexcept { _detached =
true; }
325 return ++_createdNodes;
333 [[nodiscard]] std::size_t
liveNodes() const noexcept {
return _liveNodes; }
336 void dropQueue() noexcept;
338 exec::detail::OwnerAffinity _affinity;
340 std::vector<
Node*> _queue;
341 std::function<
void()> _afterFlush;
345 std::
size_t _throwReposts = 0;
346 std::
size_t _batchDepth = 0;
347 std::
size_t _computing = 0;
348 std::
size_t _widgetEventDepth = 0;
349 std::
size_t _liveNodes = 0;
350 std::uint64_t _createdNodes = 0;
351 std::uint64_t _flushEpoch = 0;
352 std::uint64_t _batches = 0;
353 std::uint64_t _flushes = 0;
354 std::uint64_t _effectsRun = 0;
355 std::uint64_t _lastFlushEffects = 0;
356 std::uint64_t _maxFlushEffects = 0;
357 mutable std::atomic<std::uint64_t> _misuses{0};
358 mutable std::array<std::atomic<char const*>, kRecentMisuses> _recentMisuses{};
359 bool _isFlushRequested =
false;
360 bool _flushing =
false;
361 bool _detached =
false;
372 : _core{&core}, _previous{core.tracking()}, _observer{observer} {
373 core.setTracking(
this);
386 if (_observer !=
nullptr && std::ranges::find(_sources, &source) == _sources.end()) {
387 _sources.push_back(&source);
395 std::erase(_sources, &node);
396 if (&node == _observer) {
398 _observerDestroyed =
true;
409 [[nodiscard]] std::vector<Node*>
takeSources() noexcept {
return std::move(_sources); }
419 std::vector<Node*> _sources;
420 bool _observerDestroyed =
false;
439 : _core{std::move(
core)}, _depth{
depth}, _bornOnOwner{_core->checkOwner()} {
441 _creationOrder = _core->nodeCreated();
452 static_cast<void>(_core->checkOwner());
453 if (_destroyedWatch !=
nullptr) {
454 *_destroyedWatch =
true;
456 for (
Node*
const source : _sources) {
457 std::erase(source->_observers,
this);
459 for (
Node*
const observer : _observers) {
460 std::erase(observer->_sources,
this);
462 _core->forget(*
this);
464 _core->nodeDestroyed();
469 Node& operator=(
Node const&) =
delete;
481 if (_colour >= colour && !_staleAboveClean) {
484 if (_colour < colour) {
488 _core->enqueue(*
this);
496 bool const flagged = std::exchange(_staleAboveClean,
false);
498 for (
Node*
const observer : _observers) {
502 _staleAboveClean = flagged;
515#if defined(__GNUC__) && !defined(__clang__)
516#pragma GCC diagnostic push
517#pragma GCC diagnostic ignored "-Wdangling-pointer"
521 bool destroyed =
false;
522 bool*
const enclosing = std::exchange(_destroyedWatch, &destroyed);
524 auto const finish = [&] {
526 if (enclosing !=
nullptr) {
542#if defined(__GNUC__) && !defined(__clang__)
543#pragma GCC diagnostic pop
549 [[nodiscard]] std::size_t
countRun(std::uint64_t epoch)
noexcept {
550 if (_epoch != epoch) {
559 [[nodiscard]] std::size_t
depth() const noexcept {
return _depth; }
565 return std::pair{_depth, _creationOrder} > std::pair{other._depth, other._creationOrder};
572 [[nodiscard]] std::uint64_t
creationOrder() const noexcept {
return _creationOrder; }
577 exposeStaleSources();
587 [[nodiscard]]
bool bornOnOwner() const noexcept {
return _bornOnOwner; }
591 if (
TrackingFrame*
const frame = _core->tracking(); frame !=
nullptr) {
593 frame->add(
const_cast<Node&
>(*
this));
603 [[nodiscard]]
bool isPulling() const noexcept {
return _destroyedWatch !=
nullptr; }
608 for (
Node*
const observer : _observers) {
609 observer->markStale(colour);
619 std::size_t linked = 0;
621 for (
Node*
const source : fresh) {
622 if (std::ranges::find(_sources, source) == _sources.end()) {
623 source->_observers.push_back(
this);
628 for (
Node*
const source : std::span{fresh}.first(linked)) {
629 if (std::ranges::find(_sources, source) == _sources.end()) {
630 std::erase(source->_observers,
this);
635 for (
Node*
const old : _sources) {
636 if (std::ranges::find(fresh, old) == fresh.end()) {
637 std::erase(old->_observers,
this);
640 _sources = std::move(fresh);
646 std::vector<Node*> kept;
647 std::ranges::copy_if(_sources, std::back_inserter(kept),
648 [&fresh](
Node const* old) {
return std::ranges::find(fresh, old) == fresh.end(); });
649 fresh.insert(fresh.end(), kept.begin(), kept.end());
656 virtual bool recompute() = 0;
660 [[nodiscard]]
virtual bool isEffect() const noexcept {
return false; }
664 void pull(
bool const& destroyed) {
665 if (_colour == Colour::Check) {
666 for (std::size_t i = 0; i < _sources.size();) {
667 Node*
const source = _sources.at(i);
668 source->updateIfNecessary();
672 if (_colour == Colour::Dirty) {
676 if (i < _sources.size() && _sources.at(i) == source) {
681 if (_colour == Colour::Dirty) {
682 bool const changed = recompute();
692 void endPull(
bool* enclosing)
noexcept {
693 _destroyedWatch = enclosing;
694 _colour = Colour::Clean;
695 _staleAboveClean =
false;
696 exposeStaleSources();
707 void exposeStaleSources() noexcept {
708 for (
Node*
const source : _sources) {
709 if (source->_colour != Colour::Clean && !source->_staleAboveClean) {
710 source->_staleAboveClean =
true;
711 source->exposeStaleSources();
716 std::shared_ptr<RuntimeCore> _core;
717 std::vector<Node*> _sources;
718 std::vector<Node*> _observers;
720 bool* _destroyedWatch =
nullptr;
721 std::uint64_t _epoch = 0;
722 std::uint64_t _creationOrder = 0;
724 std::size_t _runs = 0;
725 Colour _colour = Colour::Clean;
727 bool _staleAboveClean =
false;
735inline bool runsLater(
Node const* lhs,
Node const* rhs)
noexcept {
return lhs->runsAfter(*rhs); }
738 _queue.push_back(&effect);
739 std::ranges::push_heap(_queue,
runsLater);
748 _isFlushRequested =
false;
749 if (_detached || _queue.empty()) {
752 if (_widgetEventDepth != 0) {
762 _lastFlushEffects = 0;
763 bool stopped =
false;
764 while (!_queue.empty()) {
765 std::ranges::pop_heap(_queue,
runsLater);
766 Node*
const effect = _queue.back();
769 _maxFlushEffects = std::max(_maxFlushEffects, ++_lastFlushEffects);
770 if (effect->
countRun(_flushEpoch) > _bounds.maxEffectRunsPerFlush) {
787 }
else if (_throwReposts < _bounds.maxThrowReposts) {
806 if (std::erase(_queue, &node) != 0) {
807 std::ranges::make_heap(_queue,
runsLater);
814inline void RuntimeCore::dropQueue() noexcept {
815 for (
Node*
const node : _queue) {
Opens a batch for its lifetime.
~BatchScope()
Closes the batch; closing the outermost one requests a flush when effects are queued.
BatchScope(RuntimeCore &core) noexcept
A graph node: a source (Signal), an observer (Effect), or both (Computed).
void markStale(Colour colour)
Push phase: raises this node to colour and marks its observers Check.
std::size_t depth() const noexcept
How deeply the scope owning this node is nested; the flush runs shallower Effects first.
std::size_t countRun(std::uint64_t epoch) noexcept
Counts one run of this Effect in the flush numbered epoch.
void trackRead() const
Registers this node as a source of the innermost tracked run, if any.
bool bornOnOwner() const noexcept
Whether this node was constructed on the owner; one that was not is refused its work.
bool runsAfter(Node const &other) const noexcept
Whether this node runs after other when both are queued: it is deeper, or as deep and newer.
Node(std::shared_ptr< RuntimeCore > core, std::size_t depth=0)
Constructs a node. Off the owner this is reported (site::kOffOwner) and the node is not counted towar...
std::uint64_t creationOrder() const noexcept
Where this node stands in its runtime's creation order; among Effects of one depth,...
void forceDirty() noexcept
Marks this node Dirty, so the next pull recomputes it.
bool isPulling() const noexcept
Whether a pull is bringing this node up to date right now. A read of the node from inside that pull c...
void mergeSources(std::vector< Node * > fresh)
Adds fresh to this node's sources without dropping any: what a failed run keeps.
void markObserversStale(Colour colour)
Marks every observer of this node colour.
bool updateIfNecessary()
Pull phase: brings this node up to date, recomputing only if a source really changed.
void settle() noexcept
Marks this node Clean without running it (a dropped queue entry).
RuntimeCore & core() const noexcept
The runtime core.
void adoptSources(std::vector< Node * > fresh)
Replaces this node's sources with fresh, unlinking the ones no longer read.
virtual ~Node()
Unlinks this node from its sources and observers, drops every reference the core holds to it,...
State shared by one Runtime and every node made from it.
void endBatch()
Closes a batch; closing the outermost one requests a flush when effects are queued.
std::size_t liveNodes() const noexcept
How many nodes made from this runtime are alive.
std::uint64_t nodeCreated() noexcept
Counts a node made from this runtime.
void configure(FlushBounds bounds, std::function< void()> afterFlush)
Sets the flush bounds and the after-flush hook.
bool isInWidgetEvent() const noexcept
Whether a widget callback is running on the owner.
void leaveWidgetEvent() noexcept
Marks the end of a widget callback.
TrackingFrame * tracking() const noexcept
The innermost tracking frame, or null outside every tracked run.
void detach() noexcept
Marks the Runtime as gone: flushes do nothing from now on.
std::vector< char const * > recentMisuses() const
The sites of the latest misuses, oldest first, at most kRecentMisuses.
std::uint64_t maxFlushEffects() const noexcept
The most queued Effects one flush has processed.
void enqueue(Node &effect)
Queues an Effect that turned stale. Defined below Node.
bool onOwner() const noexcept
Whether the calling thread is on the owner, without reporting anything.
void beginBatch() noexcept
Opens a batch; batches nest. Opening the outermost one numbers it.
void nodeDestroyed() noexcept
Counts a node destroyed.
std::size_t batchDepth() const noexcept
How deeply batches are nested on the owner.
void forget(Node &node) noexcept
Removes every reference the core holds to a node being destroyed. Defined below Node.
void requestFlush()
Posts one flush to the owner, unless one is already requested or running, nothing is queued,...
std::uint64_t misuses() const noexcept
How many misuses have been reported.
bool checkOwner() const noexcept
Whether the calling thread is on the owner, reporting site::kOffOwner when it is not.
bool isComputing() const noexcept
Whether a Computed is computing on this runtime.
void enterComputed() noexcept
Marks a Computed as computing.
std::uint64_t effectsRun() const noexcept
How many queued Effects the flushes processed, in all.
RuntimeCore(exec::IExecutor &owner) noexcept
std::uint64_t batches() const noexcept
How many outermost batches have been opened.
void enterWidgetEvent() noexcept
Marks the start of a widget callback.
void leaveComputed() noexcept
Marks a Computed as done computing.
std::uint64_t flushes() const noexcept
How many flushes ran at least one queued Effect.
void flush()
Runs every queued Effect that a source change really affects. Defined below Node.
void setTracking(TrackingFrame *frame) noexcept
Installs the innermost tracking frame.
bool isFlushRequested() const noexcept
Whether a flush is posted and has not run yet.
std::uint64_t lastFlushEffects() const noexcept
How many queued Effects the latest flush processed.
void report(char const *where) const noexcept
Reports a misuse: asserts in a debug build, or hands it to an installed owner probe.
std::uint64_t currentBatch() const noexcept
The number of the batch open now: how many outermost batches had been opened when it was.
exec::IExecutor & owner() const noexcept
The owner executor.
Collects the sources one tracked run reads, and restores the previous frame when it ends.
TrackingFrame * previous() const noexcept
The enclosing frame.
void add(Node &source)
Records that the run read source; a no-op in an untracked region.
void forget(Node &node) noexcept
Drops node, which is being destroyed, from the recorded sources. When node is this frame's observer,...
std::vector< Node * > takeSources() noexcept
Hands the recorded sources to the observer.
~TrackingFrame()
Reinstalls the enclosing frame.
TrackingFrame(RuntimeCore &core, Node *observer) noexcept
bool observerDestroyed() const noexcept
Whether the observer was destroyed during the run: a body may destroy its own node (a remount inside ...
constexpr std::size_t kRecentMisuses
How many of the latest misuses a runtime keeps the site of.
constexpr std::size_t kDefaultMaxEffectRunsPerFlush
How many times one Effect may run in a single flush unless RuntimeOptions says otherwise.
constexpr std::size_t kDefaultMaxThrowReposts
How many flushes in a row are re-posted after an Effect threw, unless RuntimeOptions says otherwise.
Colour
A node's freshness. Ordered: a later enumerator is staler.
@ Check
A transitive source changed: pull the sources before deciding to recompute.
@ Dirty
A direct source changed: recompute.
bool runsLater(Node const *lhs, Node const *rhs) noexcept
The flush queue's heap order: the node that runs first is on top.
The site names a misuse is reported under through exec::detail::noteOwner.
constexpr char const * kComputedReadsItself
A Computed read itself; the read throws std::logic_error.
constexpr char const * kRuntimeOutlived
A Runtime was destroyed while nodes made from it were alive.
constexpr char const * kOffOwner
An operation ran off the Runtime's owner; it is dropped.
constexpr char const * kWriteCycle
An Effect re-ran more than maxEffectRunsPerFlush times in one flush; the flush stops.
constexpr char const * kIssueInComputed
Query::refetch or Mutation::run was called while a Computed was computing; nothing is issued.
constexpr char const * kHandlerExecutor
A control node was built over a handler whose callbacks are not delivered on the Runtime's owner.
constexpr char const * kEffectThrew
An exception escaped an Effect; the flush stops.
constexpr char const * kAfterFlushThrew
An exception escaped RuntimeOptions::afterFlush; it is dropped.
constexpr char const * kSendInUpdate
Store::send was called from inside an update; the message is dropped.
constexpr char const * kSetInComputed
A signal was written while a Computed was computing; the write is dropped.
"Am I on my owner?" for a component that belongs to one executor.
The check every loop-owned body makes on entry: "am I on my owner?".
Abstract executor interface.
Whether == is usable for T: declared, and its element types usable too.
The bounds a flush keeps, as RuntimeOptions sets them.
std::size_t maxThrowReposts
How many flushes in a row are re-posted after an Effect threw.
std::size_t maxEffectRunsPerFlush
How many times one Effect may run in a single flush before the flush is a write cycle.