morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
graph.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <array>
6#include <atomic>
7#include <concepts>
8#include <cstddef>
9#include <cstdint>
10#include <functional>
11#include <iterator>
12#include <memory>
13#include <optional>
14#include <ranges>
15#include <span>
16#include <tuple>
17#include <type_traits>
18#include <utility>
19#include <variant>
20#include <vector>
21
22#include "../../attributes.hpp"
25#include "../../core/executor.hpp"
26
27/// @file
28/// @brief The reactive graph's internals: node colours and links, the tracking
29/// frame, batching and the flush queue.
30///
31/// Specified in `docs/spec/reactive/signals.md`.
32
33namespace morph::reactive::detail {
34
35/// @brief The site names a misuse is reported under through `exec::detail::noteOwner`.
36namespace site {
37/// @brief An operation ran off the Runtime's owner; it is dropped.
38inline constexpr char const* kOffOwner = "morph::reactive: off the Runtime's owner";
39/// @brief An exception escaped an Effect; the flush stops.
40inline constexpr char const* kEffectThrew = "morph::reactive: an exception escaped an Effect";
41/// @brief An Effect re-ran more than `maxEffectRunsPerFlush` times in one flush; the flush stops.
42inline constexpr char const* kWriteCycle = "morph::reactive: an Effect re-ran past maxEffectRunsPerFlush";
43/// @brief A signal was written while a Computed was computing; the write is dropped.
44inline constexpr char const* kSetInComputed = "morph::reactive: set() inside a Computed";
45/// @brief A Computed read itself; the read throws `std::logic_error`.
46inline constexpr char const* kComputedReadsItself = "morph::reactive: a Computed read itself";
47/// @brief `Store::send` was called from inside an update; the message is dropped.
48inline constexpr char const* kSendInUpdate = "morph::reactive: send() inside an update";
49/// @brief `Query::refetch` or `Mutation::run` was called while a Computed was computing; nothing is issued.
50inline constexpr char const* kIssueInComputed =
51 "morph::reactive: Query::refetch() or Mutation::run() inside a Computed";
52/// @brief An exception escaped `RuntimeOptions::afterFlush`; it is dropped.
53inline constexpr char const* kAfterFlushThrew = "morph::reactive: an exception escaped afterFlush";
54/// @brief A Runtime was destroyed while nodes made from it were alive.
55inline constexpr char const* kRuntimeOutlived = "morph::reactive: Runtime destroyed while nodes are alive";
56/// @brief A control node was built over a handler whose callbacks are not delivered on the Runtime's owner.
57inline constexpr char const* kHandlerExecutor = "morph::reactive: a handler delivers off the Runtime's owner";
58} // namespace site
59
60/// @brief How many times one Effect may run in a single flush unless `RuntimeOptions` says otherwise.
61inline constexpr std::size_t kDefaultMaxEffectRunsPerFlush = 100;
62
63/// @brief How many flushes in a row are re-posted after an Effect threw, unless `RuntimeOptions` says otherwise.
64inline constexpr std::size_t kDefaultMaxThrowReposts = 3;
65
66/// @brief How many of the latest misuses a runtime keeps the site of.
67inline constexpr std::size_t kRecentMisuses = 16;
68
69/// @brief Whether `==` is usable for @p T: declared, and its element types usable too.
70///
71/// `std::equality_comparable` alone is not enough. The standard containers declare an unconstrained
72/// `operator==`, so it holds for a container whose element has no `==`, and comparing two of them then fails
73/// to compile. So a range is checked through its element type, and `std::optional`, `std::pair`, `std::tuple`
74/// and `std::variant` through theirs.
75/// @tparam T The value type, without cv-qualifiers.
76template <typename T>
77struct EqualityUsable : std::bool_constant<std::equality_comparable<T>> {};
78
79/// @brief A range is usable when it declares `==` and its element type is usable. A range whose element is
80/// itself (`std::filesystem::path`) is checked as a plain type.
81/// @tparam T The range type.
82template <typename T>
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> {};
87
88/// @brief An optional is usable when its value type is.
89/// @tparam T The value type.
90template <typename T>
91struct EqualityUsable<std::optional<T>> : EqualityUsable<std::remove_cv_t<T>> {};
92
93/// @brief A pair is usable when both its types are.
94/// @tparam First The first type.
95/// @tparam Second The second type.
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> {
99};
100
101/// @brief A tuple is usable when every element type is.
102/// @tparam Ts The element types.
103template <typename... Ts>
104struct EqualityUsable<std::tuple<Ts...>> : std::bool_constant<(EqualityUsable<std::remove_cv_t<Ts>>::value && ...)> {};
105
106/// @brief A variant is usable when every alternative is.
107/// @tparam Ts The alternatives.
108template <typename... Ts>
109struct EqualityUsable<std::variant<Ts...>> : std::bool_constant<(EqualityUsable<std::remove_cv_t<Ts>>::value && ...)> {
110};
111
112/// @brief The bounds a flush keeps, as `RuntimeOptions` sets them.
114 /// @brief How many times one Effect may run in a single flush before the flush is a write cycle.
116 /// @brief How many flushes in a row are re-posted after an Effect threw.
118};
119
120/// @brief A node's freshness. Ordered: a later enumerator is staler.
121enum class Colour : std::uint8_t {
122 Clean, ///< Up to date.
123 Check, ///< A transitive source changed: pull the sources before deciding to recompute.
124 Dirty, ///< A direct source changed: recompute.
125};
126
127class Node;
128class TrackingFrame;
129
130/// @brief State shared by one `Runtime` and every node made from it.
131///
132/// Nodes hold it by `shared_ptr`, so a node that outlives its `Runtime` (a
133/// reported misuse) still has a valid core. The posted flush holds it weakly
134/// and checks `detach()`, so a flush after the `Runtime` is gone does nothing.
135class RuntimeCore : public std::enable_shared_from_this<RuntimeCore> {
136public:
137 /// @param owner The executor every operation must run on, and the one flushes are posted to. It must
138 /// queue what is posted and run it later, in order: an executor that runs a post inline would
139 /// run Effects inside `set()`. Borrowed: it must outlive the core.
140 explicit RuntimeCore(exec::IExecutor& owner MORPH_LIFETIMEBOUND) noexcept : _affinity{owner} {}
141
142 ~RuntimeCore() = default;
143 RuntimeCore(RuntimeCore const&) = delete;
144 RuntimeCore& operator=(RuntimeCore const&) = delete;
145 RuntimeCore(RuntimeCore&&) = delete;
146 RuntimeCore& operator=(RuntimeCore&&) = delete;
147
148 /// @brief Whether the calling thread is on the owner, without reporting anything.
149 /// @return What `checkOwner()` returns, minus the report.
150 [[nodiscard]] bool onOwner() const noexcept { return _affinity.here(); }
151
152 /// @brief Whether the calling thread is on the owner, reporting `site::kOffOwner` when it is not.
153 /// @return True on the owner: inside one of its tasks, or anywhere on the thread that built the Runtime
154 /// when that thread was running no executor's task at the time (a Qt slot, `main()`, a test
155 /// body). That second half is a per-thread check: on that thread a task of another executor
156 /// passes too, so running a different executor's tasks there is not detected.
157 [[nodiscard]] bool checkOwner() const noexcept {
158 if (_affinity.here()) {
159 return true;
160 }
162 return false;
163 }
164
165 /// @brief Reports a misuse: asserts in a debug build, or hands it to an installed owner probe.
166 /// @param where One of the `site::` constants.
167 void report(char const* where) const noexcept {
168 // Counted with atomics: a misuse off the owner is reported from the thread that made it.
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);
172 }
173
174 /// @brief How many misuses have been reported.
175 /// @return The count.
176 [[nodiscard]] std::uint64_t misuses() const noexcept { return _misuses.load(std::memory_order_relaxed); }
177
178 /// @brief The sites of the latest misuses, oldest first, at most `kRecentMisuses`.
179 /// @return The `site::` constants.
180 [[nodiscard]] std::vector<char const*> recentMisuses() const {
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);
188 }
189 }
190 return sites;
191 }
192
193 /// @brief The owner executor.
194 /// @return The executor passed at construction.
195 [[nodiscard]] exec::IExecutor& owner() const noexcept { return _affinity.owner(); }
196
197 /// @brief Sets the flush bounds and the after-flush hook.
198 /// @param bounds How often one Effect may run in a flush, and how many flushes in a row are re-posted after
199 /// an Effect threw.
200 /// @param afterFlush Called after every flush that processed a queued Effect; may be empty.
201 void configure(FlushBounds bounds, std::function<void()> afterFlush) {
202 _bounds = bounds;
203 _afterFlush = std::move(afterFlush);
204 }
205
206 /// @brief Opens a batch; batches nest. Opening the outermost one numbers it.
207 void beginBatch() noexcept {
208 if (_batchDepth++ == 0) {
209 ++_batches;
210 }
211 }
212
213 /// @brief The number of the batch open now: how many outermost batches had been opened when it was.
214 /// @return It, or 0 outside every batch.
215 [[nodiscard]] std::uint64_t currentBatch() const noexcept { return _batchDepth == 0 ? 0 : _batches; }
216
217 /// @brief How many outermost batches have been opened.
218 /// @return The count.
219 [[nodiscard]] std::uint64_t batches() const noexcept { return _batches; }
220
221 /// @brief How many flushes ran at least one queued Effect.
222 /// @return The count.
223 [[nodiscard]] std::uint64_t flushes() const noexcept { return _flushes; }
224
225 /// @brief How many queued Effects the flushes processed, in all.
226 /// @return The count.
227 [[nodiscard]] std::uint64_t effectsRun() const noexcept { return _effectsRun; }
228
229 /// @brief How many queued Effects the latest flush processed.
230 /// @return The count.
231 [[nodiscard]] std::uint64_t lastFlushEffects() const noexcept { return _lastFlushEffects; }
232
233 /// @brief The most queued Effects one flush has processed.
234 /// @return The count.
235 [[nodiscard]] std::uint64_t maxFlushEffects() const noexcept { return _maxFlushEffects; }
236
237 /// @brief Closes a batch; closing the outermost one requests a flush when effects are queued.
238 ///
239 /// The outermost batch closing outside a flush is a write from outside the graph, which starts a new count
240 /// of the re-posts after a throwing Effect.
241 void endBatch() {
242 if (--_batchDepth == 0) {
243 if (!_flushing) {
244 _throwReposts = 0;
245 }
246 requestFlush();
247 }
248 }
249
250 /// @brief Queues an Effect that turned stale. Defined below `Node`.
251 /// @param effect The Effect node.
252 void enqueue(Node& effect);
253
254 /// @brief Posts one flush to the owner, unless one is already requested or running, nothing is
255 /// queued, or the Runtime is gone.
257 if (_flushing || _isFlushRequested || _queue.empty() || _detached) {
258 return;
259 }
260 // Set before posting, so the flag is already down again if the flush runs inside post().
261 _isFlushRequested = true;
262 try {
263 _affinity.owner().post([weak = weak_from_this()] {
264 if (auto const core = weak.lock()) {
265 core->flush();
266 }
267 });
268 } catch (...) {
269 // Nothing was posted: the queue is kept, and the next write posts again.
270 _isFlushRequested = false;
271 throw;
272 }
273 }
274
275 /// @brief Runs every queued Effect that a source change really affects. Defined below `Node`.
276 void flush();
277
278 /// @brief Removes every reference the core holds to a node being destroyed. Defined below `Node`.
279 /// @param node The node.
280 void forget(Node& node) noexcept;
281
282 /// @brief The innermost tracking frame, or null outside every tracked run.
283 /// @return The frame reads register with.
284 [[nodiscard]] TrackingFrame* tracking() const noexcept { return _tracking; }
285
286 /// @brief Installs the innermost tracking frame.
287 /// @param frame The frame, or null.
288 void setTracking(TrackingFrame* frame) noexcept { _tracking = frame; }
289
290 /// @brief Whether a Computed is computing on this runtime.
291 /// @return True between `enterComputed()` and the matching `leaveComputed()`.
292 [[nodiscard]] bool isComputing() const noexcept { return _computing != 0; }
293
294 /// @brief Marks a Computed as computing.
295 void enterComputed() noexcept { ++_computing; }
296
297 /// @brief Marks a Computed as done computing.
298 void leaveComputed() noexcept { --_computing; }
299
300 /// @brief Marks the start of a widget callback.
301 void enterWidgetEvent() noexcept { ++_widgetEventDepth; }
302
303 /// @brief Marks the end of a widget callback.
304 void leaveWidgetEvent() noexcept { --_widgetEventDepth; }
305
306 /// @brief How deeply batches are nested on the owner.
307 /// @return Zero outside every batch.
308 [[nodiscard]] std::size_t batchDepth() const noexcept { return _batchDepth; }
309
310 /// @brief Whether a widget callback is running on the owner.
311 /// @return True between `enterWidgetEvent()` and the matching `leaveWidgetEvent()`.
312 [[nodiscard]] bool isInWidgetEvent() const noexcept { return _widgetEventDepth != 0; }
313
314 /// @brief Whether a flush is posted and has not run yet.
315 /// @return The coalescing flag.
316 [[nodiscard]] bool isFlushRequested() const noexcept { return _isFlushRequested; }
317
318 /// @brief Marks the Runtime as gone: flushes do nothing from now on.
319 void detach() noexcept { _detached = true; }
320
321 /// @brief Counts a node made from this runtime.
322 /// @return The node's creation order: greater than that of every node made from this runtime before it.
323 [[nodiscard]] std::uint64_t nodeCreated() noexcept {
324 ++_liveNodes;
325 return ++_createdNodes;
326 }
327
328 /// @brief Counts a node destroyed.
329 void nodeDestroyed() noexcept { --_liveNodes; }
330
331 /// @brief How many nodes made from this runtime are alive.
332 /// @return The live-node count.
333 [[nodiscard]] std::size_t liveNodes() const noexcept { return _liveNodes; }
334
335private:
336 void dropQueue() noexcept;
337
338 exec::detail::OwnerAffinity _affinity;
339 // A binary heap ordered by depth, then creation, the shallowest and oldest on top: see `flush()`.
340 std::vector<Node*> _queue;
341 std::function<void()> _afterFlush;
342 TrackingFrame* _tracking = nullptr;
343 FlushBounds _bounds;
344 // Flushes re-posted in a row after an Effect threw.
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;
362};
363
364/// @brief Collects the sources one tracked run reads, and restores the previous frame when it ends.
365///
366/// A frame whose observer is null is an untracked region: reads inside it subscribe nothing.
368public:
369 /// @param core The runtime core.
370 /// @param observer The node whose run this is, or null for an untracked region.
371 TrackingFrame(RuntimeCore& core, Node* observer) noexcept
372 : _core{&core}, _previous{core.tracking()}, _observer{observer} {
373 core.setTracking(this);
374 }
375
376 /// @brief Reinstalls the enclosing frame.
377 ~TrackingFrame() { _core->setTracking(_previous); }
378 TrackingFrame(TrackingFrame const&) = delete;
379 TrackingFrame& operator=(TrackingFrame const&) = delete;
380 TrackingFrame(TrackingFrame&&) = delete;
381 TrackingFrame& operator=(TrackingFrame&&) = delete;
382
383 /// @brief Records that the run read @p source; a no-op in an untracked region.
384 /// @param source The node read.
385 void add(Node& source) {
386 if (_observer != nullptr && std::ranges::find(_sources, &source) == _sources.end()) {
387 _sources.push_back(&source);
388 }
389 }
390
391 /// @brief Drops @p node, which is being destroyed, from the recorded sources. When @p node is this
392 /// frame's observer, the frame stops recording and remembers that its observer is gone.
393 /// @param node The node.
394 void forget(Node& node) noexcept {
395 std::erase(_sources, &node);
396 if (&node == _observer) {
397 _observer = nullptr;
398 _observerDestroyed = true;
399 }
400 }
401
402 /// @brief Whether the observer was destroyed during the run: a body may destroy its own node (a
403 /// remount inside an Effect), and the node's run must not touch it afterwards.
404 /// @return True once the observer's destructor has run.
405 [[nodiscard]] bool observerDestroyed() const noexcept { return _observerDestroyed; }
406
407 /// @brief Hands the recorded sources to the observer.
408 /// @return The sources, deduplicated, in first-read order.
409 [[nodiscard]] std::vector<Node*> takeSources() noexcept { return std::move(_sources); }
410
411 /// @brief The enclosing frame.
412 /// @return The frame that was innermost when this one was opened, or null.
413 [[nodiscard]] TrackingFrame* previous() const noexcept { return _previous; }
414
415private:
416 RuntimeCore* _core;
417 TrackingFrame* _previous;
418 Node* _observer;
419 std::vector<Node*> _sources;
420 bool _observerDestroyed = false;
421};
422
423/// @brief A graph node: a source (Signal), an observer (Effect), or both (Computed).
424///
425/// Non-copyable and non-movable: other nodes hold raw pointers to it, and its
426/// destructor unlinks it in both directions. A derived node supplies `recompute()`
427/// and `isEffect()` as private overrides; only `Node` calls them.
428///
429/// A node may be destroyed while it is being brought up to date (an Effect body
430/// that remounts the view owning it). The destructor tells the running pull and
431/// the running tracking frame, and neither touches the node again.
432class Node {
433public:
434 /// @brief Constructs a node. Off the owner this is reported (`site::kOffOwner`) and the node is
435 /// not counted towards the core's live nodes, whose counter belongs to the owner.
436 /// @param core The runtime core this node belongs to.
437 /// @param depth How deeply the scope owning this node is nested; queued Effects run shallowest first.
438 explicit Node(std::shared_ptr<RuntimeCore> core, std::size_t depth = 0)
439 : _core{std::move(core)}, _depth{depth}, _bornOnOwner{_core->checkOwner()} {
440 if (_bornOnOwner) {
441 _creationOrder = _core->nodeCreated();
442 }
443 }
444
445 /// @brief Unlinks this node from its sources and observers, drops every reference the core holds
446 /// to it, and tells a pull running on it that it is gone.
447 ///
448 /// Destroying a node off the owner is a misuse. It is reported (`site::kOffOwner`) but cannot be
449 /// refused: the destructor still edits the owner's graph, queue and counters, and what that does
450 /// beyond the report is undefined.
451 virtual ~Node() {
452 static_cast<void>(_core->checkOwner());
453 if (_destroyedWatch != nullptr) {
454 *_destroyedWatch = true;
455 }
456 for (Node* const source : _sources) {
457 std::erase(source->_observers, this);
458 }
459 for (Node* const observer : _observers) {
460 std::erase(observer->_sources, this);
461 }
462 _core->forget(*this);
463 if (_bornOnOwner) {
464 _core->nodeDestroyed();
465 }
466 }
467
468 Node(Node const&) = delete;
469 Node& operator=(Node const&) = delete;
470 Node(Node&&) = delete;
471 Node& operator=(Node&&) = delete;
472
473 /// @brief Push phase: raises this node to @p colour and marks its observers Check.
474 ///
475 /// An Effect is queued when it leaves Clean. A node already at least as stale stops the walk, because
476 /// its observers were marked when it turned stale, unless an observer has since ended Clean above it
477 /// (`exposeStaleSources()`); then the walk passes through it once.
478 /// @param colour Dirty for a direct observer of a change, Check for a transitive one.
479 // NOLINTNEXTLINE(misc-no-recursion) -- the recursion depth is the graph's depth
480 void markStale(Colour colour) {
481 if (_colour >= colour && !_staleAboveClean) {
482 return;
483 }
484 if (_colour < colour) {
485 if (_colour == Colour::Clean && isEffect()) {
486 // Queued before the colour changes: an enqueue that fails to allocate leaves the node
487 // Clean, so a later walk that reaches it queues it again.
488 _core->enqueue(*this);
489 }
490 _colour = colour;
491 }
492 // Cleared before the walk: two flagged nodes can observe each other (a cycle a refused self-read
493 // leaves linked), and a revisit must stop here. Restored if the walk throws, so the next write
494 // passes through again; a node this walk raised from Clean is not flagged, and an Effect behind
495 // it whose enqueue failed is not reached again.
496 bool const flagged = std::exchange(_staleAboveClean, false);
497 try {
498 for (Node* const observer : _observers) {
499 observer->markStale(Colour::Check);
500 }
501 } catch (...) {
502 _staleAboveClean = flagged;
503 throw;
504 }
505 }
506
507 /// @brief Pull phase: brings this node up to date, recomputing only if a source really changed.
508 ///
509 /// A pull that throws leaves the node Clean (so later changes still reach it) and rethrows. A pull
510 /// during which the node is destroyed returns without touching it.
511 /// @return False when the node was destroyed during the pull: the caller must not touch it either.
512 // GCC sees `_destroyedWatch` keep the address of the local `destroyed` on the path where the
513 // node is destroyed during the pull. On that path `this` is gone too and nothing reads the
514 // member again; on every other path `endPull` restores the enclosing watch before returning.
515#if defined(__GNUC__) && !defined(__clang__)
516#pragma GCC diagnostic push
517#pragma GCC diagnostic ignored "-Wdangling-pointer"
518#endif
519 // NOLINTNEXTLINE(misc-no-recursion) -- the recursion depth is the graph's depth
521 bool destroyed = false;
522 bool* const enclosing = std::exchange(_destroyedWatch, &destroyed);
523 // Once the pull has destroyed this node, `this` is gone: not even a member call may follow.
524 auto const finish = [&] {
525 if (destroyed) {
526 if (enclosing != nullptr) {
527 *enclosing = true;
528 }
529 return;
530 }
531 endPull(enclosing);
532 };
533 try {
534 pull(destroyed);
535 } catch (...) {
536 finish();
537 throw;
538 }
539 finish();
540 return !destroyed;
541 }
542#if defined(__GNUC__) && !defined(__clang__)
543#pragma GCC diagnostic pop
544#endif
545
546 /// @brief Counts one run of this Effect in the flush numbered @p epoch.
547 /// @param epoch The current flush's number.
548 /// @return How many times it has run in that flush, this one included.
549 [[nodiscard]] std::size_t countRun(std::uint64_t epoch) noexcept {
550 if (_epoch != epoch) {
551 _epoch = epoch;
552 _runs = 0;
553 }
554 return ++_runs;
555 }
556
557 /// @brief How deeply the scope owning this node is nested; the flush runs shallower Effects first.
558 /// @return Zero for a node no scope owns.
559 [[nodiscard]] std::size_t depth() const noexcept { return _depth; }
560
561 /// @brief Whether this node runs after @p other when both are queued: it is deeper, or as deep and newer.
562 /// @param other The other node.
563 /// @return The flush queue's ordering.
564 [[nodiscard]] bool runsAfter(Node const& other) const noexcept {
565 return std::pair{_depth, _creationOrder} > std::pair{other._depth, other._creationOrder};
566 }
567
568 /// @brief Where this node stands in its runtime's creation order; among Effects of one depth, the flush
569 /// runs older ones first.
570 /// @return Greater than that of every node made from the same runtime before it; zero for a node
571 /// constructed off the owner, which is never queued.
572 [[nodiscard]] std::uint64_t creationOrder() const noexcept { return _creationOrder; }
573
574 /// @brief Marks this node Clean without running it (a dropped queue entry).
575 void settle() noexcept {
576 _colour = Colour::Clean;
577 exposeStaleSources();
578 }
579
580protected:
581 /// @brief The runtime core.
582 /// @return The core this node belongs to.
583 [[nodiscard]] RuntimeCore& core() const noexcept { return *_core; }
584
585 /// @brief Whether this node was constructed on the owner; one that was not is refused its work.
586 /// @return The owner check the constructor made.
587 [[nodiscard]] bool bornOnOwner() const noexcept { return _bornOnOwner; }
588
589 /// @brief Registers this node as a source of the innermost tracked run, if any.
590 void trackRead() const {
591 if (TrackingFrame* const frame = _core->tracking(); frame != nullptr) {
592 // Reading is logically const; the dependency link is bookkeeping.
593 frame->add(const_cast<Node&>(*this)); // NOLINT(cppcoreguidelines-pro-type-const-cast)
594 }
595 }
596
597 /// @brief Marks this node Dirty, so the next pull recomputes it.
598 void forceDirty() noexcept { _colour = Colour::Dirty; }
599
600 /// @brief Whether a pull is bringing this node up to date right now. A read of the node from inside
601 /// that pull can only come from its own run or from a source's run that reads it: a cycle.
602 /// @return True between the start and the end of `updateIfNecessary()` on this node.
603 [[nodiscard]] bool isPulling() const noexcept { return _destroyedWatch != nullptr; }
604
605 /// @brief Marks every observer of this node @p colour.
606 /// @param colour The colour to raise them to.
608 for (Node* const observer : _observers) {
609 observer->markStale(colour);
610 }
611 }
612
613 /// @brief Replaces this node's sources with @p fresh, unlinking the ones no longer read.
614 ///
615 /// Strong guarantee: linking the new sources is the only step that allocates, and it runs first;
616 /// if it throws, the links it made are undone, so no source keeps a link this node does not list.
617 /// @param fresh The sources the latest run read.
618 void adoptSources(std::vector<Node*> fresh) {
619 std::size_t linked = 0;
620 try {
621 for (Node* const source : fresh) {
622 if (std::ranges::find(_sources, source) == _sources.end()) {
623 source->_observers.push_back(this);
624 }
625 ++linked;
626 }
627 } catch (...) {
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);
631 }
632 }
633 throw;
634 }
635 for (Node* const old : _sources) {
636 if (std::ranges::find(fresh, old) == fresh.end()) {
637 std::erase(old->_observers, this);
638 }
639 }
640 _sources = std::move(fresh);
641 }
642
643 /// @brief Adds @p fresh to this node's sources without dropping any: what a failed run keeps.
644 /// @param fresh The sources the failed run read before it threw.
645 void mergeSources(std::vector<Node*> 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());
650 adoptSources(std::move(fresh));
651 }
652
653private:
654 /// @brief Brings the node's value up to date.
655 /// @return True when observers must treat the value as changed.
656 virtual bool recompute() = 0;
657
658 /// @brief Whether this node is an Effect, which the flush queue runs.
659 /// @return False unless overridden.
660 [[nodiscard]] virtual bool isEffect() const noexcept { return false; }
661
662 // Returns as soon as `destroyed` is set: from then on `this` is gone.
663 // NOLINTNEXTLINE(misc-no-recursion) -- the recursion depth is the graph's depth
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();
669 if (destroyed) {
670 return;
671 }
672 if (_colour == Colour::Dirty) {
673 break;
674 }
675 // A source destroyed by its own pull has unlinked itself, and the next one now sits at i.
676 if (i < _sources.size() && _sources.at(i) == source) {
677 ++i;
678 }
679 }
680 }
681 if (_colour == Colour::Dirty) {
682 bool const changed = recompute();
683 if (destroyed) {
684 return;
685 }
686 if (changed) {
687 markObserversStale(Colour::Dirty);
688 }
689 }
690 }
691
692 void endPull(bool* enclosing) noexcept {
693 _destroyedWatch = enclosing;
694 _colour = Colour::Clean;
695 _staleAboveClean = false;
696 exposeStaleSources();
697 }
698
699 // This node is Clean while a source may still be stale: an Effect settled by a write cycle without
700 // running, an Effect whose run wrote a source of a Computed it had read (the write cannot mark the
701 // running Effect), or a run that threw before reading every source. A stale source stops the next
702 // write's walk, so this node would never hear of it. Each stale source is flagged, so that walk
703 // passes through it once, and its own sources are visited the same way; the visit stops at a Clean
704 // source, whose walk passes anyway, and at one already flagged, whose sources were visited when it
705 // was flagged. Colours are kept, so the next pull still recomputes exactly what changed.
706 // NOLINTNEXTLINE(misc-no-recursion) -- the recursion depth is the graph's depth
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();
712 }
713 }
714 }
715
716 std::shared_ptr<RuntimeCore> _core;
717 std::vector<Node*> _sources;
718 std::vector<Node*> _observers;
719 // Points at the running pull's flag while one runs on this node.
720 bool* _destroyedWatch = nullptr;
721 std::uint64_t _epoch = 0;
722 std::uint64_t _creationOrder = 0;
723 std::size_t _depth;
724 std::size_t _runs = 0;
725 Colour _colour = Colour::Clean;
726 // Stale, with an observer that may be Clean: the next markStale walks through instead of stopping.
727 bool _staleAboveClean = false;
728 bool _bornOnOwner;
729};
730
731/// @brief The flush queue's heap order: the node that runs first is on top.
732/// @param lhs A queued node.
733/// @param rhs Another queued node.
734/// @return Whether @p lhs runs after @p rhs.
735inline bool runsLater(Node const* lhs, Node const* rhs) noexcept { return lhs->runsAfter(*rhs); }
736
737inline void RuntimeCore::enqueue(Node& effect) {
738 _queue.push_back(&effect);
739 std::ranges::push_heap(_queue, runsLater);
740}
741
742// Queued Effects run shallowest scope first, then oldest first. An owner's Effects therefore run before those
743// of the scopes it owns, and an Effect a run creates is newer than the run's own Effect: either way a parent
744// runs before every child it mounted, and unmounts a child before that child can see the state the parent
745// is leaving, whatever order the writes came in. Each run is pulled, so the order changes nothing a run
746// reads: only which Effects still exist to run.
747inline void RuntimeCore::flush() {
748 _isFlushRequested = false;
749 if (_detached || _queue.empty()) {
750 return;
751 }
752 if (_widgetEventDepth != 0) {
753 // A nested event loop inside a widget handler (a modal dialog) ran this flush. A remount must not
754 // destroy the widget whose handler is on the stack, so the queue waits: the widget event's batch is
755 // still open, and closing it, at the end of the outermost widget event, posts the flush again. Not
756 // re-posting here keeps the nested loop from spinning on it.
757 return;
758 }
759 _flushing = true;
760 ++_flushEpoch;
761 ++_flushes;
762 _lastFlushEffects = 0;
763 bool stopped = false;
764 while (!_queue.empty()) {
765 std::ranges::pop_heap(_queue, runsLater);
766 Node* const effect = _queue.back();
767 _queue.pop_back();
768 ++_effectsRun;
769 _maxFlushEffects = std::max(_maxFlushEffects, ++_lastFlushEffects);
770 if (effect->countRun(_flushEpoch) > _bounds.maxEffectRunsPerFlush) {
772 effect->settle();
773 dropQueue();
774 break;
775 }
776 try {
777 effect->updateIfNecessary();
778 } catch (...) {
780 stopped = true;
781 break;
782 }
783 }
784 _flushing = false;
785 if (!stopped) {
786 _throwReposts = 0;
787 } else if (_throwReposts < _bounds.maxThrowReposts) {
788 // What is still queued runs in a flush of its own, so a defect does not stall it until the next write;
789 // a bounded number of times in a row, so an Effect that throws on every run cannot loop for ever. Past
790 // the bound the queue waits for the next write.
791 ++_throwReposts;
792 requestFlush();
793 }
794 if (_afterFlush) {
795 // The hook runs as a task of the owner executor, which has no use for the exception; the flush is
796 // already complete, so reporting it loses nothing.
797 try {
798 _afterFlush();
799 } catch (...) {
801 }
802 }
803}
804
805inline void RuntimeCore::forget(Node& node) noexcept {
806 if (std::erase(_queue, &node) != 0) {
807 std::ranges::make_heap(_queue, runsLater);
808 }
809 for (TrackingFrame* frame = _tracking; frame != nullptr; frame = frame->previous()) {
810 frame->forget(node);
811 }
812}
813
814inline void RuntimeCore::dropQueue() noexcept {
815 for (Node* const node : _queue) {
816 node->settle();
817 }
818 _queue.clear();
819}
820
821/// @brief Opens a batch for its lifetime.
823public:
824 /// @param core The runtime core.
825 explicit BatchScope(RuntimeCore& core) noexcept : _core{&core} { core.beginBatch(); }
826 /// @brief Closes the batch; closing the outermost one requests a flush when effects are queued.
828 try {
829 _core->endBatch();
830 } catch (...) { // NOLINT(bugprone-empty-catch)
831 // Posting the flush failed to allocate. The queue is kept and the next write posts again.
832 }
833 }
834 BatchScope(BatchScope const&) = delete;
835 BatchScope& operator=(BatchScope const&) = delete;
836 BatchScope(BatchScope&&) = delete;
837 BatchScope& operator=(BatchScope&&) = delete;
838
839private:
840 RuntimeCore* _core;
841};
842
843/// @brief Marks a widget callback for its lifetime.
845public:
846 /// @param core The runtime core.
847 explicit WidgetEventScope(RuntimeCore& core) noexcept : _core{&core} { core.enterWidgetEvent(); }
848 /// @brief Marks the end of the widget callback.
849 ~WidgetEventScope() { _core->leaveWidgetEvent(); }
850 WidgetEventScope(WidgetEventScope const&) = delete;
851 WidgetEventScope& operator=(WidgetEventScope const&) = delete;
853 WidgetEventScope& operator=(WidgetEventScope&&) = delete;
854
855private:
856 RuntimeCore* _core;
857};
858
859} // namespace morph::reactive::detail
Opens a batch for its lifetime.
Definition graph.hpp:822
~BatchScope()
Closes the batch; closing the outermost one requests a flush when effects are queued.
Definition graph.hpp:827
BatchScope(RuntimeCore &core) noexcept
Definition graph.hpp:825
A graph node: a source (Signal), an observer (Effect), or both (Computed).
Definition graph.hpp:432
void markStale(Colour colour)
Push phase: raises this node to colour and marks its observers Check.
Definition graph.hpp:480
std::size_t depth() const noexcept
How deeply the scope owning this node is nested; the flush runs shallower Effects first.
Definition graph.hpp:559
std::size_t countRun(std::uint64_t epoch) noexcept
Counts one run of this Effect in the flush numbered epoch.
Definition graph.hpp:549
void trackRead() const
Registers this node as a source of the innermost tracked run, if any.
Definition graph.hpp:590
bool bornOnOwner() const noexcept
Whether this node was constructed on the owner; one that was not is refused its work.
Definition graph.hpp:587
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.
Definition graph.hpp:564
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...
Definition graph.hpp:438
std::uint64_t creationOrder() const noexcept
Where this node stands in its runtime's creation order; among Effects of one depth,...
Definition graph.hpp:572
void forceDirty() noexcept
Marks this node Dirty, so the next pull recomputes it.
Definition graph.hpp:598
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...
Definition graph.hpp:603
void mergeSources(std::vector< Node * > fresh)
Adds fresh to this node's sources without dropping any: what a failed run keeps.
Definition graph.hpp:645
void markObserversStale(Colour colour)
Marks every observer of this node colour.
Definition graph.hpp:607
bool updateIfNecessary()
Pull phase: brings this node up to date, recomputing only if a source really changed.
Definition graph.hpp:520
void settle() noexcept
Marks this node Clean without running it (a dropped queue entry).
Definition graph.hpp:575
RuntimeCore & core() const noexcept
The runtime core.
Definition graph.hpp:583
void adoptSources(std::vector< Node * > fresh)
Replaces this node's sources with fresh, unlinking the ones no longer read.
Definition graph.hpp:618
virtual ~Node()
Unlinks this node from its sources and observers, drops every reference the core holds to it,...
Definition graph.hpp:451
State shared by one Runtime and every node made from it.
Definition graph.hpp:135
void endBatch()
Closes a batch; closing the outermost one requests a flush when effects are queued.
Definition graph.hpp:241
std::size_t liveNodes() const noexcept
How many nodes made from this runtime are alive.
Definition graph.hpp:333
std::uint64_t nodeCreated() noexcept
Counts a node made from this runtime.
Definition graph.hpp:323
void configure(FlushBounds bounds, std::function< void()> afterFlush)
Sets the flush bounds and the after-flush hook.
Definition graph.hpp:201
bool isInWidgetEvent() const noexcept
Whether a widget callback is running on the owner.
Definition graph.hpp:312
void leaveWidgetEvent() noexcept
Marks the end of a widget callback.
Definition graph.hpp:304
TrackingFrame * tracking() const noexcept
The innermost tracking frame, or null outside every tracked run.
Definition graph.hpp:284
void detach() noexcept
Marks the Runtime as gone: flushes do nothing from now on.
Definition graph.hpp:319
std::vector< char const * > recentMisuses() const
The sites of the latest misuses, oldest first, at most kRecentMisuses.
Definition graph.hpp:180
std::uint64_t maxFlushEffects() const noexcept
The most queued Effects one flush has processed.
Definition graph.hpp:235
void enqueue(Node &effect)
Queues an Effect that turned stale. Defined below Node.
Definition graph.hpp:737
bool onOwner() const noexcept
Whether the calling thread is on the owner, without reporting anything.
Definition graph.hpp:150
void beginBatch() noexcept
Opens a batch; batches nest. Opening the outermost one numbers it.
Definition graph.hpp:207
void nodeDestroyed() noexcept
Counts a node destroyed.
Definition graph.hpp:329
std::size_t batchDepth() const noexcept
How deeply batches are nested on the owner.
Definition graph.hpp:308
void forget(Node &node) noexcept
Removes every reference the core holds to a node being destroyed. Defined below Node.
Definition graph.hpp:805
void requestFlush()
Posts one flush to the owner, unless one is already requested or running, nothing is queued,...
Definition graph.hpp:256
std::uint64_t misuses() const noexcept
How many misuses have been reported.
Definition graph.hpp:176
bool checkOwner() const noexcept
Whether the calling thread is on the owner, reporting site::kOffOwner when it is not.
Definition graph.hpp:157
bool isComputing() const noexcept
Whether a Computed is computing on this runtime.
Definition graph.hpp:292
void enterComputed() noexcept
Marks a Computed as computing.
Definition graph.hpp:295
std::uint64_t effectsRun() const noexcept
How many queued Effects the flushes processed, in all.
Definition graph.hpp:227
RuntimeCore(exec::IExecutor &owner) noexcept
Definition graph.hpp:140
std::uint64_t batches() const noexcept
How many outermost batches have been opened.
Definition graph.hpp:219
void enterWidgetEvent() noexcept
Marks the start of a widget callback.
Definition graph.hpp:301
void leaveComputed() noexcept
Marks a Computed as done computing.
Definition graph.hpp:298
std::uint64_t flushes() const noexcept
How many flushes ran at least one queued Effect.
Definition graph.hpp:223
void flush()
Runs every queued Effect that a source change really affects. Defined below Node.
Definition graph.hpp:747
void setTracking(TrackingFrame *frame) noexcept
Installs the innermost tracking frame.
Definition graph.hpp:288
bool isFlushRequested() const noexcept
Whether a flush is posted and has not run yet.
Definition graph.hpp:316
std::uint64_t lastFlushEffects() const noexcept
How many queued Effects the latest flush processed.
Definition graph.hpp:231
void report(char const *where) const noexcept
Reports a misuse: asserts in a debug build, or hands it to an installed owner probe.
Definition graph.hpp:167
std::uint64_t currentBatch() const noexcept
The number of the batch open now: how many outermost batches had been opened when it was.
Definition graph.hpp:215
exec::IExecutor & owner() const noexcept
The owner executor.
Definition graph.hpp:195
Collects the sources one tracked run reads, and restores the previous frame when it ends.
Definition graph.hpp:367
TrackingFrame * previous() const noexcept
The enclosing frame.
Definition graph.hpp:413
void add(Node &source)
Records that the run read source; a no-op in an untracked region.
Definition graph.hpp:385
void forget(Node &node) noexcept
Drops node, which is being destroyed, from the recorded sources. When node is this frame's observer,...
Definition graph.hpp:394
std::vector< Node * > takeSources() noexcept
Hands the recorded sources to the observer.
Definition graph.hpp:409
~TrackingFrame()
Reinstalls the enclosing frame.
Definition graph.hpp:377
TrackingFrame(RuntimeCore &core, Node *observer) noexcept
Definition graph.hpp:371
bool observerDestroyed() const noexcept
Whether the observer was destroyed during the run: a body may destroy its own node (a remount inside ...
Definition graph.hpp:405
Marks a widget callback for its lifetime.
Definition graph.hpp:844
~WidgetEventScope()
Marks the end of the widget callback.
Definition graph.hpp:849
WidgetEventScope(RuntimeCore &core) noexcept
Definition graph.hpp:847
constexpr std::size_t kRecentMisuses
How many of the latest misuses a runtime keeps the site of.
Definition graph.hpp:67
constexpr std::size_t kDefaultMaxEffectRunsPerFlush
How many times one Effect may run in a single flush unless RuntimeOptions says otherwise.
Definition graph.hpp:61
constexpr std::size_t kDefaultMaxThrowReposts
How many flushes in a row are re-posted after an Effect threw, unless RuntimeOptions says otherwise.
Definition graph.hpp:64
Colour
A node's freshness. Ordered: a later enumerator is staler.
Definition graph.hpp:121
@ Check
A transitive source changed: pull the sources before deciding to recompute.
Definition graph.hpp:123
@ Dirty
A direct source changed: recompute.
Definition graph.hpp:124
bool runsLater(Node const *lhs, Node const *rhs) noexcept
The flush queue's heap order: the node that runs first is on top.
Definition graph.hpp:735
The site names a misuse is reported under through exec::detail::noteOwner.
Definition graph.hpp:36
constexpr char const * kComputedReadsItself
A Computed read itself; the read throws std::logic_error.
Definition graph.hpp:46
constexpr char const * kRuntimeOutlived
A Runtime was destroyed while nodes made from it were alive.
Definition graph.hpp:55
constexpr char const * kOffOwner
An operation ran off the Runtime's owner; it is dropped.
Definition graph.hpp:38
constexpr char const * kWriteCycle
An Effect re-ran more than maxEffectRunsPerFlush times in one flush; the flush stops.
Definition graph.hpp:42
constexpr char const * kIssueInComputed
Query::refetch or Mutation::run was called while a Computed was computing; nothing is issued.
Definition graph.hpp:50
constexpr char const * kHandlerExecutor
A control node was built over a handler whose callbacks are not delivered on the Runtime's owner.
Definition graph.hpp:57
constexpr char const * kEffectThrew
An exception escaped an Effect; the flush stops.
Definition graph.hpp:40
constexpr char const * kAfterFlushThrew
An exception escaped RuntimeOptions::afterFlush; it is dropped.
Definition graph.hpp:53
constexpr char const * kSendInUpdate
Store::send was called from inside an update; the message is dropped.
Definition graph.hpp:48
constexpr char const * kSetInComputed
A signal was written while a Computed was computing; the write is dropped.
Definition graph.hpp:44
"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.
Definition executor.hpp:70
Whether == is usable for T: declared, and its element types usable too.
Definition graph.hpp:77
The bounds a flush keeps, as RuntimeOptions sets them.
Definition graph.hpp:113
std::size_t maxThrowReposts
How many flushes in a row are re-posted after an Effect threw.
Definition graph.hpp:117
std::size_t maxEffectRunsPerFlush
How many times one Effect may run in a single flush before the flush is a write cycle.
Definition graph.hpp:115