morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
runtime.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <cstddef>
5#include <cstdint>
6#include <functional>
7#include <memory>
8#include <stdexcept>
9#include <string>
10#include <utility>
11#include <vector>
12
13#include "../attributes.hpp"
14#include "../core/executor.hpp"
15#include "detail/graph.hpp"
16
17/// @file
18/// @brief `morph::reactive::Runtime`: the owner, batching and flush scheduling of a signal graph.
19///
20/// Specified in `docs/spec/reactive/signals.md`.
21
22namespace morph::reactive {
23
24/// @brief How a `Runtime` bounds and reports its flushes.
26 /// @brief How many times one Effect may run in a single flush before the flush is treated as a
27 /// write cycle, reported, and stopped.
29 /// @brief How many flushes in a row are re-posted after an Effect threw. Each re-post runs what is still
30 /// queued, so a defect does not stall it until the next write; past the bound it waits for that
31 /// write, so an Effect that throws on every run cannot loop for ever. A write from outside a flush
32 /// starts a new count.
34 /// @brief Called on the owner after every flush that processed a queued Effect; a frontend
35 /// schedules its redraw here. May be empty. An exception it throws is reported
36 /// (`detail::site::kAfterFlushThrew`) and dropped, so it never reaches the owner executor.
37 std::function<void()> afterFlush;
38};
39
40/// @brief What a `Runtime` has counted since it was made: its flushes, the Effects they ran, its batches and the
41/// misuses it reported. A development tool reads it (spec 6 ยง21); counting costs a few increments per flush.
43 /// @brief Flushes that ran at least one queued Effect.
44 std::uint64_t flushes = 0;
45 /// @brief Queued Effects the flushes processed, in all.
46 std::uint64_t effectsRun = 0;
47 /// @brief Queued Effects the latest flush processed.
48 std::uint64_t lastFlushEffects = 0;
49 /// @brief The most queued Effects one flush processed.
50 std::uint64_t maxFlushEffects = 0;
51 /// @brief Outermost batches opened.
52 std::uint64_t batches = 0;
53 /// @brief Misuses reported (`detail::site`), on the owner or off it.
54 std::uint64_t misuses = 0;
55 /// @brief The sites of the latest misuses, oldest first, at most `detail::kRecentMisuses`.
56 std::vector<std::string> recentMisuses;
57};
58
59/// @brief The owner of a signal graph: every node takes one, and every operation runs on its owner.
60///
61/// Writes are batched. When the outermost batch ends with effects queued, the
62/// runtime posts **one** flush to the owner; no Effect ever runs inside `set()`.
63/// Destroying a Runtime while nodes made from it are alive is reported
64/// (`detail::site::kRuntimeOutlived`); the nodes stay valid and any flush does nothing.
65///
66/// Off the owner, `batch`, `widgetEvent` and `untracked` are reported (`detail::site::kOffOwner`) and
67/// refused: the body still runs, because the caller may need what it returns, but the runtime's state
68/// (batch depth, widget-event depth, tracking frame) is left alone, and each graph operation inside the
69/// body is checked, reported and refused on its own.
70class Runtime {
71public:
72 /// @param owner The executor the graph belongs to (the GUI or loop executor). It must be serial
73 /// (`IExecutor::isSerial`), queue what is posted and run it later, in order, as `Completion`
74 /// requires of its owner: one that runs a post inline would run Effects inside `set()`, and a pool
75 /// would run two flushes at once. Borrowed: it must outlive the Runtime and every node made from it.
76 /// @param options Flush bounds and after-flush hook.
77 /// @throws std::invalid_argument when @p owner is not serial.
78 explicit Runtime(exec::IExecutor& owner MORPH_LIFETIMEBOUND, RuntimeOptions options = {})
79 : _core{makeCore(owner)} {
80 _core->configure(detail::FlushBounds{.maxEffectRunsPerFlush = options.maxEffectRunsPerFlush,
81 .maxThrowReposts = options.maxThrowReposts},
82 std::move(options.afterFlush));
83 }
84
85 /// @brief Reports `detail::site::kRuntimeOutlived` when nodes made from it are still alive, then
86 /// detaches the core, so a flush still posted does nothing.
88 if (_core->liveNodes() != 0) {
89 _core->report(detail::site::kRuntimeOutlived);
90 }
91 _core->detach();
92 }
93
94 Runtime(Runtime const&) = delete;
95 Runtime& operator=(Runtime const&) = delete;
96 Runtime(Runtime&&) = delete;
97 Runtime& operator=(Runtime&&) = delete;
98
99 /// @brief Runs @p fn as one batch: every write inside it is one flush.
100 /// @tparam F A callable taking no arguments.
101 /// @param fn The body. Writes made before it throws still flush.
102 /// @return Whatever @p fn returns.
103 template <typename F>
104 decltype(auto) batch(F&& fn) {
105 if (!_core->checkOwner()) {
106 return std::invoke(std::forward<F>(fn));
107 }
108 detail::BatchScope const scope{*_core};
109 return std::invoke(std::forward<F>(fn));
110 }
111
112 /// @brief Runs a widget callback as one batch. A flush that falls due while it runs, or that a nested
113 /// event loop inside it (a modal dialog) picks up, is deferred: the end of the outermost widget
114 /// event posts it. So a remount never destroys a widget whose native handler is on the stack, and a
115 /// nested loop does not spin on a flush it may not run. The callback is untracked, like
116 /// `untracked`: a widget callback is a transition, so one dispatched from inside an Effect's
117 /// run does not subscribe that Effect to what it reads.
118 /// @tparam F A callable taking no arguments.
119 /// @param fn The callback.
120 /// @return Whatever @p fn returns.
121 template <typename F>
122 decltype(auto) widgetEvent(F&& fn) {
123 if (!_core->checkOwner()) {
124 return std::invoke(std::forward<F>(fn));
125 }
126 detail::WidgetEventScope const event{*_core};
127 detail::TrackingFrame const untrackedFrame{*_core, nullptr};
128 return batch(std::forward<F>(fn));
129 }
130
131 /// @brief Runs @p fn with tracking off: reads inside it subscribe nothing.
132 /// @tparam F A callable taking no arguments.
133 /// @param fn The body.
134 /// @return Whatever @p fn returns.
135 template <typename F>
136 decltype(auto) untracked(F&& fn) const {
137 if (!_core->checkOwner()) {
138 return std::invoke(std::forward<F>(fn));
139 }
140 detail::TrackingFrame const frame{*_core, nullptr};
141 return std::invoke(std::forward<F>(fn));
142 }
143
144 /// @brief The owner executor.
145 /// @return The executor passed at construction.
146 [[nodiscard]] exec::IExecutor& owner() const noexcept { return _core->owner(); }
147
148 /// @brief Whether a flush is posted and has not run yet.
149 /// @return True between the first write that queued an Effect and the flush.
150 [[nodiscard]] bool isFlushRequested() const noexcept { return _core->isFlushRequested(); }
151
152 /// @brief What the runtime has counted so far. Read it on the owner.
153 /// @return The counts.
154 [[nodiscard]] RuntimeStats stats() const {
155 RuntimeStats out{.flushes = _core->flushes(),
156 .effectsRun = _core->effectsRun(),
157 .lastFlushEffects = _core->lastFlushEffects(),
158 .maxFlushEffects = _core->maxFlushEffects(),
159 .batches = _core->batches(),
160 .misuses = _core->misuses(),
161 .recentMisuses = {}};
162 for (char const* const where : _core->recentMisuses()) {
163 out.recentMisuses.emplace_back(where);
164 }
165 return out;
166 }
167
168 /// @brief The shared core every node holds.
169 /// @return The core.
170 [[nodiscard]] std::shared_ptr<detail::RuntimeCore> const& core() const noexcept { return _core; }
171
172private:
173 [[nodiscard]] static std::shared_ptr<detail::RuntimeCore> makeCore(exec::IExecutor& owner) {
174 if (!owner.isSerial()) {
175 throw std::invalid_argument{"morph::reactive::Runtime: the owner executor must be serial"};
176 }
177 return std::make_shared<detail::RuntimeCore>(owner);
178 }
179
180 std::shared_ptr<detail::RuntimeCore> _core;
181};
182
183} // namespace morph::reactive
The owner of a signal graph: every node takes one, and every operation runs on its owner.
Definition runtime.hpp:70
decltype(auto) widgetEvent(F &&fn)
Runs a widget callback as one batch. A flush that falls due while it runs, or that a nested event loo...
Definition runtime.hpp:122
Runtime(exec::IExecutor &owner, RuntimeOptions options={})
Definition runtime.hpp:78
decltype(auto) batch(F &&fn)
Runs fn as one batch: every write inside it is one flush.
Definition runtime.hpp:104
exec::IExecutor & owner() const noexcept
The owner executor.
Definition runtime.hpp:146
decltype(auto) untracked(F &&fn) const
Runs fn with tracking off: reads inside it subscribe nothing.
Definition runtime.hpp:136
~Runtime()
Reports detail::site::kRuntimeOutlived when nodes made from it are still alive, then detaches the cor...
Definition runtime.hpp:87
bool isFlushRequested() const noexcept
Whether a flush is posted and has not run yet.
Definition runtime.hpp:150
RuntimeStats stats() const
What the runtime has counted so far. Read it on the owner.
Definition runtime.hpp:154
std::shared_ptr< detail::RuntimeCore > const & core() const noexcept
The shared core every node holds.
Definition runtime.hpp:170
Opens a batch for its lifetime.
Definition graph.hpp:822
Collects the sources one tracked run reads, and restores the previous frame when it ends.
Definition graph.hpp:367
Marks a widget callback for its lifetime.
Definition graph.hpp:844
The reactive graph's internals: node colours and links, the tracking frame, batching and the flush qu...
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
constexpr char const * kRuntimeOutlived
A Runtime was destroyed while nodes made from it were alive.
Definition graph.hpp:55
Abstract executor interface.
Definition executor.hpp:70
virtual bool isSerial() const noexcept
Whether this executor runs one task at a time, each after the tasks posted before it: a strand,...
Definition executor.hpp:94
How a Runtime bounds and reports its flushes.
Definition runtime.hpp:25
std::function< void()> afterFlush
Called on the owner after every flush that processed a queued Effect; a frontend schedules its redraw...
Definition runtime.hpp:37
std::size_t maxThrowReposts
How many flushes in a row are re-posted after an Effect threw. Each re-post runs what is still queued...
Definition runtime.hpp:33
std::size_t maxEffectRunsPerFlush
How many times one Effect may run in a single flush before the flush is treated as a write cycle,...
Definition runtime.hpp:28
What a Runtime has counted since it was made: its flushes, the Effects they ran, its batches and the ...
Definition runtime.hpp:42
std::uint64_t maxFlushEffects
The most queued Effects one flush processed.
Definition runtime.hpp:50
std::uint64_t misuses
Misuses reported (detail::site), on the owner or off it.
Definition runtime.hpp:54
std::uint64_t lastFlushEffects
Queued Effects the latest flush processed.
Definition runtime.hpp:48
std::vector< std::string > recentMisuses
The sites of the latest misuses, oldest first, at most detail::kRecentMisuses.
Definition runtime.hpp:56
std::uint64_t batches
Outermost batches opened.
Definition runtime.hpp:52
std::uint64_t flushes
Flushes that ran at least one queued Effect.
Definition runtime.hpp:44
std::uint64_t effectsRun
Queued Effects the flushes processed, in all.
Definition runtime.hpp:46
The bounds a flush keeps, as RuntimeOptions sets them.
Definition graph.hpp:113