morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
engine.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file table/engine.hpp
6/// @brief The table engine: sort, filter and view mapping, on and off the owner.
7///
8/// An `Engine` owns a table's view: the sort chain, the filter, and the
9/// mapping between source rows and view rows. It is owner-affine — every
10/// member is called on the owner thread — and toolkit-free: a renderer applies
11/// the `ViewChange` it emits as list-model operations.
12///
13/// **Execution.** Every recomputation is a job over an immutable snapshot of
14/// the rows, the key columns it may reuse, and the compiled sort and filter.
15/// A job never touches the engine. A small table (below
16/// `EngineOptions::smallTable` rows), or an engine with no owner executor,
17/// computes in one step inside the call. Otherwise the job runs on the worker
18/// executor, or, with no worker, in frame-budgeted steps posted to the owner;
19/// `pending()` is true until its result is applied on the owner, and the
20/// previous view stays visible meanwhile. One job runs at a time: a request
21/// that makes the running job's result useless (a new sort, filter or
22/// structural change) stops it and its result is dropped; updated rows wait
23/// for it and are repaired after.
24///
25/// **Changes.** Updated rows re-key only themselves and are merged back into
26/// the order, once per owner turn however many updates arrived. Inserted,
27/// removed and reset rows recompute the view. Work a dropped job had taken
28/// (updated rows, rows owed a `Changed`) is kept for the next one. With
29/// `ReorderPolicy::Deferred` an updated row keeps its place until the view has
30/// been quiet for the settle interval, or until its edit ends.
31///
32/// **Callbacks.** Every handler the engine calls (`onViewChange`, `onPending`,
33/// `onError`, the progress sink, the settle scheduler) may call the engine
34/// again or destroy it: the engine calls out last, or checks that it is still
35/// alive before it goes on.
36///
37/// See `docs/spec/table/engine.md`.
38
39#include <algorithm>
40#include <chrono>
41#include <core/async/StopToken.hpp>
42#include <cstddef>
43#include <cstdint>
44#include <exception>
45#include <expected>
46#include <functional>
47#include <limits>
48#include <memory>
49#include <morph/core/callback_scope.hpp>
50#include <morph/core/executor.hpp>
54#include <morph/table/sort.hpp>
56#include <numeric>
57#include <optional>
58#include <span>
59#include <string>
60#include <unordered_map>
61#include <unordered_set>
62#include <utility>
63#include <vector>
64
65namespace morph::table {
66
67// NOLINTBEGIN(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) -- row and column indices are bounded by the snapshot's own counts, and the sort and filter loops index once per comparison, where at() would check every access
68
69/// @brief What an updated row that would move does.
70enum class ReorderPolicy : std::uint8_t {
71 Immediate, ///< It moves at once.
72 Deferred, ///< It keeps its place until the view is quiet for `settle`, or its edit ends.
73};
74
75/// @brief How an engine runs and what it is given.
77 /// @brief Where results are applied. Null: every computation runs inside the
78 /// call that asks for it (what `table::apply` uses on a server).
80 /// @brief Where jobs run off the owner. Null: jobs run in frame-budgeted
81 /// steps posted to `owner`. Must outlive every job the engine starts.
83 /// @brief Tables with fewer rows compute on the owner in one step.
84 std::size_t smallTable = 2000;
85 /// @brief A `ViewChange` with more operations than this is one `Reset`.
86 std::size_t resetThreshold = 5000;
87 /// @brief The longest an owner step runs when there is no worker.
88 std::chrono::microseconds frameBudget{8000};
89 /// @brief Collator, date parser, comparators and progress sink.
91 /// @brief What an updated row that would move does.
93 /// @brief The quiet interval after which deferred rows move.
94 std::chrono::milliseconds settle{500};
95 /// @brief Runs a callback on the owner after a delay; the engine uses it for
96 /// the settle interval. Empty: deferred rows move only on
97 /// `settleNow()` or `endEdit()`.
98 std::function<void(std::chrono::milliseconds, std::function<void()>)> scheduleSettle;
99};
100
101/// @brief Counters for tests, benchmarks and profiler plots.
103 /// @brief Jobs started (one-step runs included).
104 std::size_t jobs = 0;
105 /// @brief Sorts of every source row.
106 std::size_t fullSorts = 0;
107 /// @brief Sorts of a filtered subset.
108 std::size_t subsetSorts = 0;
109 /// @brief Key columns built from scratch.
110 std::size_t keyBuilds = 0;
111 /// @brief Repairs of updated rows.
112 std::size_t repairs = 0;
113 /// @brief Results dropped because a newer request superseded them.
114 std::size_t dropped = 0;
115 /// @brief Steps run on the owner thread (one-step runs included).
116 std::size_t ownerSteps = 0;
117 /// @brief The longest single step on the owner thread, in microseconds.
118 std::size_t maxOwnerStepMicros = 0;
119};
120
121/// @brief Column order and visibility, kept apart from the engine's view.
122///
123/// At least one column stays visible.
125public:
126 /// @brief A layout of @p ids, all visible, in that order.
127 /// @param ids Column ids, the document's default order.
128 explicit ColumnLayout(std::vector<std::string> ids) : _defaults{std::move(ids)} { reset(); }
129
130 /// @brief The columns in display order.
131 /// @return Column ids.
132 [[nodiscard]] std::span<std::string const> order() const noexcept { return _order; }
133
134 /// @brief The visible columns in display order.
135 /// @return Column ids.
136 [[nodiscard]] std::vector<std::string> visible() const {
137 std::vector<std::string> out;
138 for (auto const& columnId : _order) {
139 if (!_hidden.contains(columnId)) {
140 out.push_back(columnId);
141 }
142 }
143 return out;
144 }
145
146 /// @brief Shows or hides a column.
147 /// @param columnId Column columnId.
148 /// @param show `true` to show.
149 /// @return `false` when the column is unknown, or hiding it would leave none visible.
150 bool setVisible(std::string const& columnId, bool show) {
151 if (std::ranges::find(_order, columnId) == _order.end()) {
152 return false;
153 }
154 if (show) {
155 _hidden.erase(columnId);
156 return true;
157 }
158 if (!_hidden.contains(columnId) && visible().size() == 1) {
159 return false;
160 }
161 _hidden.insert(columnId);
162 return true;
163 }
164
165 /// @brief Moves a column to a display position.
166 /// @param columnId Column columnId.
167 /// @param position Position in `order()`, clamped to the end.
168 /// @return `false` when the column is unknown.
169 bool move(std::string const& columnId, std::size_t position) {
170 auto const found = std::ranges::find(_order, columnId);
171 if (found == _order.end()) {
172 return false;
173 }
174 auto moved = std::move(*found);
175 _order.erase(found);
176 _order.insert(_order.begin() + static_cast<std::ptrdiff_t>(std::min(position, _order.size())),
177 std::move(moved));
178 return true;
179 }
180
181 /// @brief Restores the default order with every column visible.
182 void reset() {
183 _order = _defaults;
184 _hidden.clear();
185 }
186
187 /// @brief Adopts a new column list: kept columns keep their place and
188 /// visibility, new ones are appended visible, vanished ones dropped.
189 /// @param ids The new columns, as the new default order.
190 void rebuild(std::vector<std::string> ids) {
191 std::vector<std::string> order;
192 for (auto const& columnId : _order) {
193 if (std::ranges::find(ids, columnId) != ids.end()) {
194 order.push_back(columnId);
195 }
196 }
197 for (auto const& columnId : ids) {
198 if (std::ranges::find(order, columnId) == order.end()) {
199 order.push_back(columnId);
200 }
201 }
202 std::erase_if(_hidden,
203 [&](std::string const& hiddenId) { return std::ranges::find(ids, hiddenId) == ids.end(); });
204 _defaults = std::move(ids);
205 _order = std::move(order);
206 if (!_order.empty() && visible().empty()) {
207 _hidden.erase(_order.front());
208 }
209 }
210
211private:
212 std::vector<std::string> _defaults;
213 std::vector<std::string> _order;
214 std::unordered_set<std::string> _hidden;
215};
216
217namespace detail {
218
219/// @brief Everything one view job reads. Immutable once the job starts.
220struct JobInput {
221 /// @brief The rows.
222 std::shared_ptr<RowSnapshot const> snapshot;
223 /// @brief The columns.
224 std::vector<ColumnInfo> columns;
225 /// @brief Services for this job alone (`Services::forTask`).
227 /// @brief Key columns by column index; null when not built.
228 std::vector<std::shared_ptr<KeyColumn const>> keys;
229 /// @brief Columns whose keys the job needs.
230 std::vector<std::size_t> needed;
231 /// @brief The sort chain.
233 /// @brief The compiled filter, or null.
234 std::shared_ptr<CompiledFilter const> filter;
235 /// @brief The full source's order for `sort` on `snapshot`, or null.
236 std::shared_ptr<std::vector<std::uint32_t> const> cachedSorted;
237 /// @brief The displayed view the change is computed against.
238 std::vector<std::uint32_t> previousView;
239 /// @brief The snapshot `previousView`'s rows index.
240 std::shared_ptr<RowSnapshot const> previousSnapshot;
241 /// @brief `previousView`'s rows index `snapshot` directly.
242 bool indicesStable = true;
243 /// @brief Rows whose cells changed since `keys` were built: their keys are
244 /// rebuilt on copies, a repair merges them back, and the change
245 /// reports those in both views as `Changed`.
246 std::vector<std::uint32_t> dirty;
247 /// @brief Repair: the sorted, filtered order, without holds, that the dirty rows merge into.
248 std::vector<std::uint32_t> previousTrueView;
249 /// @brief The job repairs updated rows rather than recomputing.
250 bool repair = false;
251 /// @brief Every surviving row may have new content (a reset).
252 bool allChanged = false;
253 /// @brief More operations than this make the change a reset.
254 std::size_t resetThreshold = 5000;
255};
256
257/// @brief What a finished job hands back to the owner.
258struct JobResult {
259 /// @brief Key columns by column index, for `JobInput::snapshot`.
260 std::vector<std::shared_ptr<KeyColumn const>> keys;
261 /// @brief A full order for the chain, to cache, or null.
262 std::shared_ptr<std::vector<std::uint32_t> const> sorted;
263 /// @brief The sorted, filtered view.
264 std::vector<std::uint32_t> view;
265 /// @brief From `JobInput::previousView` to `view`.
267 /// @brief Every source row was sorted.
268 bool fullSort = false;
269 /// @brief Only the filter's survivors were sorted.
270 bool subsetSort = false;
271 /// @brief Updated rows were merged into the previous order.
272 bool repaired = false;
273 /// @brief Key columns built from scratch.
274 std::size_t keyBuilds = 0;
275};
276
277/// @brief One recomputation of a view, runnable in steps.
278///
279/// Phases: build missing keys (or patch the keys of dirty rows), then either
280/// repair the previous order, or filter and sort, then diff against the
281/// previous view. `step` returns at a deadline or a stop request and resumes
282/// where it stopped.
283class ViewJob {
284public:
285 /// @brief Prepares a job.
286 /// @param input Everything it reads.
287 explicit ViewJob(JobInput input) : _in{std::move(input)} {
288 _result.keys = _in.keys;
289 _result.keys.resize(_in.columns.size());
290 }
291
292 /// @brief Runs until done, the deadline, or a stop request.
293 ///
294 /// An exception from an injected service (a collator, date parser or
295 /// comparator) ends the job: it is done, with `failure()` set.
296 /// @param deadline When to yield.
297 /// @param stop A stop request ends the step.
298 /// @return `true` when the job is done.
299 bool step(Deadline deadline, ::core::async::StopToken const& stop) {
300 try {
301 return run(deadline, stop);
302 } catch (std::exception const& error) {
303 _failure = error.what();
304 } catch (...) {
305 _failure = "unknown exception";
306 }
307 _phase = Phase::Done;
308 return true;
309 }
310
311 /// @brief Sets a receiver of (phases done, total) after each phase ends,
312 /// on the thread that runs the step.
313 /// @param listener The receiver.
314 void onPhase(std::function<void(std::size_t, std::size_t)> listener) { _onPhase = std::move(listener); }
315
316 /// @brief Phases done and total, for a progress sink.
317 /// @return (done, total).
318 [[nodiscard]] std::pair<std::size_t, std::size_t> progress() const noexcept {
319 return {static_cast<std::size_t>(_phase), static_cast<std::size_t>(Phase::Done)};
320 }
321
322 /// @brief Whether every phase has run.
323 /// @return `true` when done.
324 [[nodiscard]] bool done() const noexcept { return _phase == Phase::Done; }
325
326 /// @brief Why the job failed: the message of the exception that ended it.
327 /// @return The message, or nothing when the job did not fail.
328 [[nodiscard]] std::optional<std::string> const& failure() const noexcept { return _failure; }
329
330 /// @brief What the job reads.
331 /// @return The input.
332 [[nodiscard]] JobInput const& input() const noexcept { return _in; }
333
334 /// @brief Moves the result out. Valid once `done()`; the input stays readable.
335 /// @return The result.
336 [[nodiscard]] JobResult takeResult() { return std::move(_result); }
337
338private:
339 enum class Phase : std::uint8_t { Keys, Order, Diff, Done };
340
341 bool run(Deadline deadline, ::core::async::StopToken const& stop) {
342 while (_phase != Phase::Done) {
343 if (stop.stop_requested()) {
344 return false;
345 }
346 bool finished = false;
347 switch (_phase) {
348 case Phase::Keys:
349 finished = buildKeys(deadline, stop);
350 break;
351 case Phase::Order:
352 finished = order(deadline, stop);
353 break;
354 case Phase::Diff:
355 diff();
356 finished = true;
357 break;
358 case Phase::Done:
359 default:
360 finished = true;
361 break;
362 }
363 if (!finished) {
364 return false;
365 }
366 advance();
367 if (_onPhase) {
368 auto const [doneUnits, total] = progress();
369 _onPhase(doneUnits, total);
370 }
371 if (_phase != Phase::Done && detail::shouldYield(deadline, stop)) {
372 return false;
373 }
374 }
375 return true;
376 }
377
378 void advance() {
379 switch (_phase) {
380 case Phase::Keys:
381 _phase = Phase::Order;
382 break;
383 case Phase::Order:
384 _phase = Phase::Diff;
385 break;
386 case Phase::Diff:
387 case Phase::Done:
388 default:
389 _phase = Phase::Done;
390 break;
391 }
392 }
393
394 // Builds every needed column that has no keys, and patches the dirty rows
395 // of every column that has them (on a copy: the old keys may still be
396 // read by the owner or another job).
397 bool buildKeys(Deadline deadline, ::core::async::StopToken const& stop) {
398 MORPH_ZONE("table.keyBuild");
399 auto const rows = _in.snapshot->rowCount();
400 if (!_patched && !_in.dirty.empty()) {
401 for (std::size_t column = 0; column < _result.keys.size(); ++column) {
402 if (_result.keys[column] == nullptr) {
403 continue;
404 }
405 auto copy = std::make_shared<KeyColumn>(*_result.keys[column]);
406 for (auto const row : _in.dirty) {
407 copy->set(row, _in.snapshot->cell(row, column), _in.columns[column], _in.services);
408 }
409 _result.keys[column] = std::move(copy);
410 }
411 _patched = true;
412 }
413 while (_neededAt < _in.needed.size()) {
414 auto const column = _in.needed[_neededAt];
415 if (_result.keys[column] != nullptr && _building == nullptr) {
416 ++_neededAt;
417 continue;
418 }
419 if (_building == nullptr) {
420 _building = std::make_shared<KeyColumn>(KeyColumn::forColumn(_in.columns[column], _in.services, rows));
421 _rowAt = 0;
422 }
423 while (_rowAt < rows) {
424 auto const end = std::min(_rowAt + detail::kCheckEvery, rows);
425 morph::table::buildKeys(*_in.snapshot, column, _in.columns[column], _in.services, _rowAt, end,
426 *_building);
427 _rowAt = end;
428 if (_rowAt < rows && detail::shouldYield(deadline, stop)) {
429 return false;
430 }
431 }
432 _result.keys[column] = std::move(_building);
433 _building = nullptr;
434 ++_result.keyBuilds;
435 ++_neededAt;
436 }
437 return true;
438 }
439
440 // Zone text for a profiler capture: the sort chain, and the filter's size.
441 [[nodiscard]] std::string describeSort() const {
442 std::string out;
443 for (auto const& key : _in.sort) {
444 out += key.column;
445 out += key.dir == SortDirection::Descending ? " desc " : " asc ";
446 }
447 return out;
448 }
449 [[nodiscard]] std::string describeFilter() const {
450 return _in.filter == nullptr ? std::string{}
451 : std::to_string(_in.filter->columns().size()) + " columns, " +
452 std::to_string(_in.filter->groups().size()) + " groups";
453 }
454
455 [[nodiscard]] RowOrder rowOrder() const {
456 std::vector<RowOrder::Key> keys;
457 for (auto const& key : _in.sort) {
458 if (auto const column = findColumn(_in.columns, key.column)) {
459 keys.push_back({.keys = _result.keys[*column].get(), .dir = key.dir});
460 }
461 }
462 return RowOrder{std::move(keys)};
463 }
464
465 [[nodiscard]] std::vector<KeyColumn const*> keyPointers() const {
466 std::vector<KeyColumn const*> out;
467 out.reserve(_result.keys.size());
468 for (auto const& keys : _result.keys) {
469 out.push_back(keys.get());
470 }
471 return out;
472 }
473
474 [[nodiscard]] bool passesFilter(std::uint32_t row) const {
475 if (_in.filter == nullptr) {
476 return true;
477 }
478 for (auto const& column : _in.filter->columns()) {
479 if (!CompiledFilter::passes(column, *_result.keys[column.column], row)) {
480 return false;
481 }
482 }
483 for (auto const& group : _in.filter->groups()) {
484 bool const any = std::ranges::any_of(group.columns, [&](CompiledFilter::Column const& column) {
485 return CompiledFilter::passes(column, *_result.keys[column.column], row);
486 });
487 if (!any) {
488 return false;
489 }
490 }
491 return true;
492 }
493
494 // Merges the dirty rows back into an order the clean rows still satisfy.
495 [[nodiscard]] std::vector<std::uint32_t> repairOrder(std::span<std::uint32_t const> previous,
496 std::vector<std::uint32_t> dirtyRows,
497 RowOrder const& orderBy) const {
498 std::vector<bool> isDirty(_in.snapshot->rowCount(), false);
499 for (auto const row : _in.dirty) {
500 isDirty[row] = true;
501 }
502 std::vector<std::uint32_t> clean;
503 clean.reserve(previous.size());
504 for (auto const row : previous) {
505 if (!isDirty[row]) {
506 clean.push_back(row);
507 }
508 }
509 auto const sortedDirty = sortRows(std::move(dirtyRows), orderBy);
510 std::vector<std::uint32_t> out(clean.size() + sortedDirty.size());
511 std::ranges::merge(clean, sortedDirty, out.begin(), orderBy);
512 return out;
513 }
514
515 // Merges the re-keyed dirty rows into the previous order (and the cached
516 // full order), without sorting the rest.
517 void repair() {
518 MORPH_ZONE("table.change");
519 auto const orderBy = rowOrder();
520 std::vector<std::uint32_t> passing;
521 for (auto const row : _in.dirty) {
522 if (passesFilter(row)) {
523 passing.push_back(row);
524 }
525 }
526 _result.view = repairOrder(_in.previousTrueView, std::move(passing), orderBy);
527 if (_in.cachedSorted != nullptr) {
528 _result.sorted =
529 std::make_shared<std::vector<std::uint32_t> const>(repairOrder(*_in.cachedSorted, _in.dirty, orderBy));
530 }
531 _result.repaired = true;
532 }
533
534 // Chooses the work: filter the cached sorted order (with the dirty rows
535 // merged back into it); or filter, then sort the survivors; or sort every
536 // row and cache the order.
537 void plan() {
538 auto const rows = _in.snapshot->rowCount();
539 auto const filtering = _in.filter != nullptr && !_in.filter->empty();
540 _started = true;
541 if (_in.cachedSorted != nullptr) {
542 if (_in.dirty.empty()) {
543 _base = *_in.cachedSorted;
544 _result.sorted = _in.cachedSorted;
545 } else {
546 _base = repairOrder(*_in.cachedSorted, _in.dirty, rowOrder());
547 _result.sorted = std::make_shared<std::vector<std::uint32_t> const>(_base);
548 }
549 _sorted = true;
550 } else {
551 _base.resize(rows);
552 // NOLINTNEXTLINE(modernize-use-ranges): std::ranges::iota is missing from Emscripten's libc++.
553 std::iota(_base.begin(), _base.end(), 0U);
554 _sorted = _in.sort.empty();
555 if (!_sorted && !filtering) {
556 _sort.emplace(std::move(_base), rowOrder());
557 _result.fullSort = true;
558 }
559 }
560 if (filtering && (_sorted || _sort == std::nullopt)) {
561 _filterRun.emplace(*_in.filter, keyPointers(), std::move(_base));
562 }
563 }
564
565 bool order(Deadline deadline, ::core::async::StopToken const& stop) {
566 if (_in.repair) {
567 repair();
568 return true;
569 }
570 if (!_started) {
571 plan();
572 }
573 if (_filterRun) {
574 MORPH_ZONE("table.filter");
575 MORPH_ZONE_TEXT(describeFilter());
576 if (!_filterRun->run(deadline, stop)) {
577 return false;
578 }
579 _base = std::move(*_filterRun).take();
580 _filterRun.reset();
581 if (!_sorted) {
582 // Filter first, then sort only the survivors.
583 _sort.emplace(std::move(_base), rowOrder());
584 _result.subsetSort = true;
585 }
586 }
587 if (_sort) {
588 MORPH_ZONE("table.sort");
589 MORPH_ZONE_TEXT(describeSort());
590 if (!_sort->run(deadline, stop)) {
591 return false;
592 }
593 _base = std::move(*_sort).take();
594 _sort.reset();
595 _sorted = true;
596 if (_result.fullSort) {
597 _result.sorted = std::make_shared<std::vector<std::uint32_t> const>(_base);
598 }
599 }
600 _result.view = std::move(_base);
601 return true;
602 }
603
604 void diff() {
605 MORPH_ZONE("table.apply");
606 std::vector<std::uint32_t> changed = _in.dirty;
607 if (_in.indicesStable) {
608 if (_in.allChanged) {
609 changed = _result.view;
610 }
611 _result.change =
612 diffViews(_in.previousView, _result.view, changed, _in.snapshot->rowCount(), _in.resetThreshold);
613 return;
614 }
615 // Rows of the previous view map to new rows by key; vanished rows get
616 // identities past the new snapshot's rows.
617 auto const rows = _in.snapshot->rowCount();
618 std::unordered_map<RowId, std::uint32_t> byKey;
619 byKey.reserve(rows);
620 for (std::size_t row = 0; row < rows; ++row) {
621 byKey.emplace(_in.snapshot->rowId(row), static_cast<std::uint32_t>(row));
622 }
623 std::vector<std::uint32_t> previous;
624 previous.reserve(_in.previousView.size());
625 auto next = static_cast<std::uint32_t>(rows);
626 for (auto const row : _in.previousView) {
627 auto const found = byKey.find(_in.previousSnapshot->rowId(row));
628 previous.push_back(found != byKey.end() ? found->second : next++);
629 }
630 if (_in.allChanged) {
631 changed = _result.view;
632 }
633 _result.change = diffViews(previous, _result.view, changed, next, _in.resetThreshold);
634 }
635
636 JobInput _in;
637 JobResult _result;
638 Phase _phase = Phase::Keys;
639 // key build
640 bool _patched = false;
641 std::size_t _neededAt = 0;
642 std::size_t _rowAt = 0;
643 std::shared_ptr<KeyColumn> _building;
644 // order
645 bool _started = false;
646 bool _sorted = false;
647 std::vector<std::uint32_t> _base;
648 std::optional<FilterRun> _filterRun;
649 std::optional<ChunkedMergeSort> _sort;
650 // outcome
651 std::optional<std::string> _failure;
652 std::function<void(std::size_t, std::size_t)> _onPhase;
653};
654
655} // namespace detail
656
657/// @brief Sorts, filters and maps the view of one table.
658///
659/// Owner-affine; see the file comment for how it executes. Holds a shared
660/// reference to its source and subscribes to its changes for its lifetime.
661class Engine final : private ChangeListener {
662public:
663 /// @brief Builds the engine; the initial view is every row in source order.
664 /// @param source The rows. Must not be null.
665 /// @param options How the engine runs.
666 explicit Engine(std::shared_ptr<DataSource> source, EngineOptions options = {})
667 : _source{std::move(source)},
668 _options{std::move(options)},
669 _columns(_source->columns().begin(), _source->columns().end()),
670 _snapshot{_source->snapshot()},
671 _viewColumns{_columns},
672 _viewSnapshot{_snapshot},
673 _view(identity(_snapshot->rowCount())),
674 _trueView{_view},
675 _keysSnapshot{_snapshot},
676 _keys(_columns.size()) {
677 _source->subscribe(*this);
678 }
679
680 Engine(Engine const&) = delete;
681 Engine& operator=(Engine const&) = delete;
682 Engine(Engine&&) = delete;
683 Engine& operator=(Engine&&) = delete;
684
685 /// @brief Stops a running job and unsubscribes from the source.
686 ~Engine() override {
687 _jobStop.request_stop();
688 _source->unsubscribe(*this);
689 }
690
691 /// @brief Sets the sort chain; an empty chain is source order.
692 ///
693 /// Keys are checked against the source's latest columns, which `columns()`
694 /// shows once the view computed with them is applied.
695 /// @param chain The keys, most significant first.
696 /// @return `UnknownColumn` when a key names no column; `ServiceFailed` when
697 /// the sort ran inside the call and an injected service threw. The
698 /// sort is then unchanged.
699 std::expected<void, TableError> setSort(SortChain chain) {
700 for (auto const& key : chain) {
701 if (!detail::findColumn(_columns, key.column)) {
702 return std::unexpected(detail::unknownColumn(key.column));
703 }
704 }
705 if (chain == _sort) {
706 return {};
707 }
708 _sort = std::move(chain);
709 return schedule();
710 }
711
712 /// @brief Sets the filter.
713 /// @param spec The filter; an empty spec shows every row.
714 /// @return The compile error, naming its column; or `ServiceFailed` when
715 /// the filter ran inside the call and an injected service threw.
716 /// The filter is then unchanged.
717 std::expected<void, TableError> setFilter(FilterSpec spec) {
718 auto compiled = compile(spec);
719 if (!compiled) {
720 return std::unexpected(std::move(compiled.error()));
721 }
722 _filterSpec = std::move(spec);
723 _filter = std::make_shared<CompiledFilter const>(std::move(*compiled));
724 return schedule();
725 }
726
727 /// @brief The current sort chain.
728 /// @return The chain.
729 [[nodiscard]] SortChain const& sort() const noexcept { return _sort; }
730
731 /// @brief The current filter.
732 /// @return The filter spec.
733 [[nodiscard]] FilterSpec const& filter() const noexcept { return _filterSpec; }
734
735 /// @brief The columns of the applied view: what `cellAt` indexes. A reset
736 /// that changes the columns changes them when its view is applied,
737 /// with a `Reset` change.
738 /// @return The columns.
739 [[nodiscard]] std::span<ColumnInfo const> columns() const noexcept { return _viewColumns; }
740
741 /// @brief Rows in the view.
742 /// @return View row count.
743 [[nodiscard]] std::size_t viewRowCount() const noexcept { return _view.size(); }
744
745 /// @brief Rows in the source the view was computed from.
746 /// @return Source row count.
747 [[nodiscard]] std::size_t sourceRowCount() const { return _viewSnapshot->rowCount(); }
748
749 /// @brief The source row shown at a view row.
750 /// @param viewRow View row index.
751 /// @return The source row, or nothing past the end.
752 [[nodiscard]] std::optional<std::size_t> sourceRowOf(std::size_t viewRow) const {
753 if (viewRow >= _view.size()) {
754 return std::nullopt;
755 }
756 return _view[viewRow];
757 }
758
759 /// @brief The view row showing a source row.
760 /// @param sourceRow Source row index.
761 /// @return The view row, or nothing when the row is filtered out or past the end.
762 [[nodiscard]] std::optional<std::size_t> viewRowOf(std::size_t sourceRow) const {
763 indexView();
764 if (sourceRow >= _viewIndex.size() || _viewIndex[sourceRow] == kAbsent) {
765 return std::nullopt;
766 }
767 return _viewIndex[sourceRow];
768 }
769
770 /// @brief The key of the row at a view row.
771 /// @param viewRow View row index, below `viewRowCount()`.
772 /// @return The row's key.
773 [[nodiscard]] RowId rowIdAt(std::size_t viewRow) const { return _viewSnapshot->rowId(_view.at(viewRow)); }
774
775 /// @brief The view row of the row keyed @p id.
776 /// @param key A row key.
777 /// @return The view row, or nothing when no row in the view has that key.
778 [[nodiscard]] std::optional<std::size_t> viewRowOfKey(RowId const& key) const {
779 indexKeys();
780 auto const found = _keyIndex.find(key);
781 return found == _keyIndex.end() ? std::nullopt : std::optional{found->second};
782 }
783
784 /// @brief One cell of the view.
785 /// @param viewRow View row index, below `viewRowCount()`.
786 /// @param column Column index into `columns()`.
787 /// @return The cell; empty for a column past `columns()`.
788 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -- row then column, as every table reads them
789 [[nodiscard]] Cell cellAt(std::size_t viewRow, std::size_t column) const {
790 auto const row = _view.at(viewRow);
791 if (column >= _viewColumns.size()) {
792 return {};
793 }
794 return _viewSnapshot->cell(row, column);
795 }
796
797 /// @brief The view as source rows.
798 /// @return Source row indices in view order.
799 [[nodiscard]] std::span<std::uint32_t const> view() const noexcept { return _view; }
800
801 /// @brief The snapshot the view's rows index.
802 /// @return The snapshot.
803 [[nodiscard]] std::shared_ptr<RowSnapshot const> const& viewSnapshot() const noexcept { return _viewSnapshot; }
804
805 /// @brief Whether a computation is in flight.
806 /// @return `true` from an asynchronous request until its result is applied.
807 [[nodiscard]] bool pending() const noexcept { return _pending; }
808
809 /// @brief Sets the receiver of view changes, called on the owner after the
810 /// view changed.
811 /// @param handler The receiver; empty to stop receiving.
812 void onViewChange(std::function<void(ViewChange const&)> handler) { _onViewChange = std::move(handler); }
813
814 /// @brief Sets the receiver of `pending()` changes.
815 /// @param handler The receiver; empty to stop receiving.
816 void onPending(std::function<void(bool)> handler) { _onPending = std::move(handler); }
817
818 /// @brief Sets the receiver of errors no call could return: an injected
819 /// service that threw in a job that did not run inside the call
820 /// that asked for it. The sort and filter go back to those of the
821 /// view shown.
822 /// @param handler The receiver; empty to stop receiving.
823 void onError(std::function<void(TableError const&)> handler) { _onError = std::move(handler); }
824
825 /// @brief Counters.
826 /// @return The engine's counters so far.
827 [[nodiscard]] EngineStats const& stats() const noexcept { return _stats; }
828
829 /// @brief Marks a row as being edited: with `Deferred` reorder it keeps its
830 /// place through updates until `endEdit`.
831 /// @param key The row's key.
832 void beginEdit(RowId const& key) { _editing.insert(key); }
833
834 /// @brief Ends an edit; a held row moves to its sorted place now.
835 ///
836 /// The commit that ends an edit usually patches the row just before, and
837 /// that update reaches the engine on a later owner turn: the first job
838 /// that includes it does not hold the row either, so it moves once rather
839 /// than waiting for the settle interval.
840 /// @param key The row's key.
841 void endEdit(RowId const& key) {
842 _editing.erase(key);
843 if (_options.reorder == ReorderPolicy::Deferred) {
844 _releaseOnRepair.insert_or_assign(key, _changeSeq);
845 }
846 if (_holds.erase(key) > 0) {
847 relayout();
848 }
849 }
850
851 /// @brief Releases every deferred row not being edited, as the settle
852 /// interval does.
853 void settleNow() {
854 std::erase_if(_holds, [&](RowId const& key) { return !_editing.contains(key); });
855 relayout();
856 }
857
858 /// @brief Rows currently held in place by `Deferred` reorder.
859 /// @return Their keys.
860 [[nodiscard]] std::vector<RowId> heldRows() const { return {_holds.begin(), _holds.end()}; }
861
862private:
863 static constexpr auto kAbsent = std::numeric_limits<std::uint32_t>::max();
864
865 [[nodiscard]] static std::vector<std::uint32_t> identity(std::size_t rows) {
866 std::vector<std::uint32_t> out(rows);
867 // NOLINTNEXTLINE(modernize-use-ranges): std::ranges::iota is missing from Emscripten's libc++.
868 std::iota(out.begin(), out.end(), 0U);
869 return out;
870 }
871
872 enum class Request : std::uint8_t { Full, Repair };
873
874 // What the engine's state was when a job's input was taken, to tell on
875 // the job's return whether a later change made its result useless.
876 struct Ticket {
877 std::uint64_t epoch = 0; // structural changes absorbed
878 std::uint64_t changeSeq = 0; // notifications absorbed
879 std::uint64_t columnsVersion = 0; // column sets adopted
880 };
881
882 // What applying a result calls out with.
883 struct Applied {
884 ViewChange change;
885 bool armSettle = false;
886 };
887
888 // ── changes ─────────────────────────────────────────────────────────────
889
890 void rowsChanged(RowChange const& change) override {
891 switch (change.kind) {
892 case ChangeKind::Updated:
893 for (auto const row : change.rows) {
894 _notifiedRows.push_back(static_cast<std::uint32_t>(row));
895 }
896 break;
897 case ChangeKind::Reset:
898 _notifiedReset = true;
899 _notifiedStructural = true;
900 break;
901 case ChangeKind::Inserted:
902 case ChangeKind::Removed:
903 default:
904 _notifiedStructural = true;
905 break;
906 }
907 _notified = true;
908 ++_changeSeq;
909 if (_options.owner == nullptr) {
910 flushChanges();
911 return;
912 }
913 if (!_flushScheduled) {
914 _flushScheduled = true;
915 _options.owner->post(_alive.guard([this] { flushChanges(); }));
916 }
917 }
918
919 // Takes in every change notified since the last flush, then asks for the
920 // work it makes: one repair for any number of updates, or a recompute
921 // after a structural change.
922 void flushChanges() {
923 MORPH_ZONE("table.snapshot");
924 _flushScheduled = false;
925 if (!absorb()) {
926 return;
927 }
928 // An error comes back only from a job that ran inside this call, which
929 // has then called nothing out.
930 if (auto const done = schedule(); !done) {
931 reportError(done.error());
932 }
933 }
934
935 // Folds the notified changes into the pending work. Updated rows join the
936 // dirty rows; a structural change drops the keys built for the old row
937 // indices and owes every row a `Changed`.
938 bool absorb() {
939 if (!_notified) {
940 return false;
941 }
942 _notified = false;
943 _absorbedSeq = _changeSeq;
944 _snapshot = _source->snapshot();
945 if (_notifiedReset) {
946 adoptColumns();
947 }
948 if (_notifiedStructural) {
949 ++_epoch;
950 _keys.assign(_columns.size(), nullptr);
951 _keysSnapshot = nullptr;
952 _sortCache = {};
953 _dirty.clear();
954 _obligeAll = true;
955 } else {
956 _dirty.insert(_dirty.end(), _notifiedRows.begin(), _notifiedRows.end());
957 }
958 _notifiedRows.clear();
959 _notifiedStructural = false;
960 _notifiedReset = false;
961 return true;
962 }
963
964 // Re-reads the columns after a reset; a changed column set keeps the sort
965 // keys and filter entries whose columns remain.
966 void adoptColumns() {
967 std::vector<ColumnInfo> columns(_source->columns().begin(), _source->columns().end());
968 if (columns == _columns) {
969 return;
970 }
971 _columns = std::move(columns);
972 ++_columnsVersion;
973 fitToColumns();
974 }
975
976 void fitToColumns() {
977 std::erase_if(_sort, [&](SortKey const& key) { return !detail::findColumn(_columns, key.column); });
978 std::erase_if(_filterSpec.columns,
979 [&](auto const& entry) { return !detail::findColumn(_columns, entry.first).has_value(); });
980 for (auto& group : _filterSpec.groups) {
981 std::erase_if(group.columns,
982 [&](std::string const& columnId) { return !detail::findColumn(_columns, columnId); });
983 }
984 std::erase_if(_filterSpec.groups, [](GroupFilter const& group) { return group.columns.empty(); });
985 auto compiled = compile(_filterSpec);
986 if (compiled) {
987 _filter = std::make_shared<CompiledFilter const>(std::move(*compiled));
988 } else {
989 // A remaining entry no longer compiles for its column's new kind.
990 _filterSpec = {};
991 _filter = nullptr;
992 }
993 }
994
995 // compileFilter, with an exception from the collator or date parser as an error.
996 [[nodiscard]] std::expected<CompiledFilter, TableError> compile(FilterSpec const& spec) const {
997 try {
998 return compileFilter(spec, _columns, _options.services);
999 } catch (std::exception const& error) {
1000 return std::unexpected(serviceFailed(error.what()));
1001 } catch (...) {
1002 return std::unexpected(serviceFailed("unknown exception"));
1003 }
1004 }
1005
1006 [[nodiscard]] static TableError serviceFailed(std::string const& what) {
1007 return TableError{.code = TableErrorCode::ServiceFailed, .column = {}, .message = "a service failed: " + what};
1008 }
1009
1010 // ── scheduling ──────────────────────────────────────────────────────────
1011
1012 // Whether the shown view is out of date with the requested state.
1013 [[nodiscard]] bool hasWork() const {
1014 return !_dirty.empty() || _epoch != _viewEpoch || _sort != _appliedSort || _filter != _appliedFilter ||
1015 _columnsVersion != _viewColumnsVersion;
1016 }
1017
1018 // A repair merges the dirty rows into the shown order: only the rows'
1019 // contents changed since that order was computed. (A structural change
1020 // advances the epoch; every row is owed a `Changed` exactly while the
1021 // epoch is ahead of the view's.)
1022 [[nodiscard]] bool canRepair() const {
1023 return !_dirty.empty() && _epoch == _viewEpoch && _sort == _appliedSort && _filter == _appliedFilter &&
1024 _columnsVersion == _viewColumnsVersion;
1025 }
1026
1027 // Whether a change since @p input was taken makes its result useless.
1028 [[nodiscard]] bool supersedes(detail::JobInput const& input, Ticket const& ticket) const {
1029 return _sort != input.sort || _filter != input.filter || _epoch != ticket.epoch ||
1030 _columnsVersion != ticket.columnsVersion;
1031 }
1032
1033 // Starts the work the requested state needs. While a job runs, a request
1034 // that makes its result useless stops it; either way the work follows it.
1035 std::expected<void, TableError> schedule() {
1036 if (_running) {
1037 if (supersedes(*_runningInput, _runningTicket)) {
1038 _jobStop.request_stop();
1039 }
1040 return {};
1041 }
1042 if (!hasWork()) {
1043 return {};
1044 }
1045 auto const kind = canRepair() ? Request::Repair : Request::Full;
1046 if (_options.owner == nullptr || _snapshot->rowCount() < _options.smallTable) {
1047 return runNow(kind);
1048 }
1049 start(kind);
1050 return {};
1051 }
1052
1053 [[nodiscard]] detail::JobInput makeInput(Request kind, Ticket& ticket) {
1054 detail::JobInput input;
1055 input.snapshot = _snapshot;
1056 input.columns = _columns;
1057 input.services = _options.services.forTask();
1058 input.keys = _keys;
1059 input.keys.resize(_columns.size());
1060 for (auto const& key : _sort) {
1061 if (auto const index = detail::findColumn(_columns, key.column)) {
1062 input.needed.push_back(*index);
1063 }
1064 }
1065 if (_filter != nullptr) {
1066 for (auto const column : _filter->columnsUsed()) {
1067 input.needed.push_back(column);
1068 }
1069 }
1070 std::ranges::sort(input.needed);
1071 auto const [first, last] = std::ranges::unique(input.needed);
1072 input.needed.erase(first, last);
1073 input.sort = _sort;
1074 input.filter = _filter;
1075 // The cached order is of the rows the keys were built from (install
1076 // keeps no other); the job merges the dirty rows back into it.
1077 if (!_sort.empty() && _sortCache.order != nullptr && _sortCache.chain == _sort) {
1078 input.cachedSorted = _sortCache.order;
1079 }
1080 input.previousView = _view;
1081 input.previousSnapshot = _viewSnapshot;
1082 input.repair = kind == Request::Repair;
1083 input.indicesStable = _epoch == _viewEpoch;
1084 input.dirty = std::move(_dirty);
1085 _dirty.clear();
1086 std::ranges::sort(input.dirty);
1087 auto const [dupFirst, dupLast] = std::ranges::unique(input.dirty);
1088 input.dirty.erase(dupFirst, dupLast);
1089 if (input.repair) {
1090 input.previousTrueView = _trueView;
1091 }
1092 input.allChanged = _obligeAll;
1093 _obligeAll = false;
1094 input.resetThreshold = _options.resetThreshold;
1095 ticket = Ticket{.epoch = _epoch, .changeSeq = _absorbedSeq, .columnsVersion = _columnsVersion};
1096 return input;
1097 }
1098
1099 // A job that was dropped or failed hands back the work it took.
1100 void giveBack(detail::JobInput const& input, Ticket const& ticket) {
1101 if (ticket.epoch == _epoch) {
1102 _dirty.insert(_dirty.end(), input.dirty.begin(), input.dirty.end());
1103 }
1104 _obligeAll = _obligeAll || input.allChanged;
1105 }
1106
1107 // A failed job's sort and filter go back to those of the view shown.
1108 void revert() {
1109 _sort = _appliedSort;
1110 _filterSpec = _appliedFilterSpec;
1111 _filter = _appliedFilter;
1112 if (_columnsVersion != _viewColumnsVersion) {
1113 fitToColumns();
1114 }
1115 }
1116
1117 std::expected<void, TableError> runNow(Request kind) {
1118 auto const began = std::chrono::steady_clock::now();
1119 ++_stats.jobs;
1120 Ticket ticket;
1121 detail::ViewJob job{makeInput(kind, ticket)};
1122 static_cast<void>(job.step(kNoDeadline, ::core::async::StopToken{}));
1123 if (job.failure()) {
1124 recordOwnerStep(began);
1125 giveBack(job.input(), ticket);
1126 revert();
1127 return std::unexpected(serviceFailed(*job.failure()));
1128 }
1129 auto const applied = install(job.input(), job.takeResult(), ticket);
1130 recordOwnerStep(began);
1131 deliver(applied);
1132 return {};
1133 }
1134
1135 void start(Request kind) {
1136 _running = true;
1137 ++_stats.jobs;
1138 _jobStop = ::core::async::StopSource{};
1139 auto const job = std::make_shared<detail::ViewJob>(makeInput(kind, _runningTicket));
1140 _runningInput = &job->input();
1141 auto* const owner = _options.owner;
1142 if (_options.worker == nullptr) {
1143 owner->post(_alive.guard([this, job] { ownerStep(job); }));
1144 } else {
1145 // The worker task never names the engine: it holds the job and
1146 // callbacks gated on the engine's lifetime, which run on the owner.
1147 if (auto const progress = _options.services.progress) {
1148 auto report =
1149 _alive.guard([progress](std::size_t done, std::size_t total) { progress->progress(done, total); });
1150 job->onPhase([owner, report = std::move(report)](std::size_t done, std::size_t total) {
1151 owner->post([report, done, total] mutable { report(done, total); });
1152 });
1153 }
1154 auto finished = _alive.guard([this, job] { finish(job); });
1155 _options.worker->post([job, stop = _jobStop.get_token(), owner, finished = std::move(finished)] mutable {
1156 static_cast<void>(job->step(kNoDeadline, stop));
1157 owner->post(std::move(finished));
1158 });
1159 }
1160 // Last: the handler may make a request or destroy the engine.
1161 setPending(true);
1162 }
1163
1164 void ownerStep(std::shared_ptr<detail::ViewJob> const& job) {
1165 auto const began = std::chrono::steady_clock::now();
1166 auto const stop = _jobStop.get_token();
1167 bool const done = job->step(began + _options.frameBudget, stop);
1168 recordOwnerStep(began);
1169 if (auto const progress = _options.services.progress) {
1170 auto const alive = _alive.token();
1171 auto const [doneUnits, total] = job->progress();
1172 progress->progress(doneUnits, total);
1173 if (alive.expired()) {
1174 return;
1175 }
1176 }
1177 if (!done && !stop.stop_requested()) {
1178 _options.owner->post(_alive.guard([this, job] { ownerStep(job); }));
1179 return;
1180 }
1181 finish(job);
1182 }
1183
1184 void finish(std::shared_ptr<detail::ViewJob> const& job) {
1185 _running = false;
1186 _runningInput = nullptr;
1187 auto const& input = job->input();
1188 if (!job->done() || supersedes(input, _runningTicket)) {
1189 ++_stats.dropped;
1190 giveBack(input, _runningTicket);
1191 afterJob(std::nullopt);
1192 return;
1193 }
1194 if (job->failure()) {
1195 giveBack(input, _runningTicket);
1196 revert();
1197 afterJob(serviceFailed(*job->failure()));
1198 return;
1199 }
1200 auto const began = std::chrono::steady_clock::now();
1201 auto const applied = install(input, job->takeResult(), _runningTicket);
1202 recordOwnerStep(began);
1203 if (!deliver(applied)) {
1204 return;
1205 }
1206 afterJob(std::nullopt);
1207 }
1208
1209 // After a job returns: starts the work that remains (not after a failure,
1210 // which would only fail again), lays out holds released meanwhile, ends
1211 // `pending()`, and reports the failure. A handler may have started a job
1212 // already; then `pending()` stays true.
1213 void afterJob(std::optional<TableError> error) {
1214 auto const alive = _alive.token();
1215 if (!error && !_running && hasWork()) {
1216 auto const done = schedule();
1217 if (alive.expired()) {
1218 return;
1219 }
1220 if (!done) {
1221 error = done.error();
1222 }
1223 }
1224 if (_relayoutPending && !_running) {
1225 relayout();
1226 if (alive.expired()) {
1227 return;
1228 }
1229 }
1230 if (!_running) {
1231 setPending(false);
1232 if (alive.expired()) {
1233 return;
1234 }
1235 }
1236 if (error) {
1237 reportError(*error);
1238 }
1239 }
1240
1241 // ── applying ────────────────────────────────────────────────────────────
1242
1243 // Makes a finished job's result the shown view. Calls nothing out.
1244 [[nodiscard]] Applied install(detail::JobInput const& input, detail::JobResult result, Ticket const& ticket) {
1245 _stats.keyBuilds += result.keyBuilds;
1246 _stats.fullSorts += result.fullSort ? 1 : 0;
1247 _stats.subsetSorts += result.subsetSort ? 1 : 0;
1248 _stats.repairs += result.repaired ? 1 : 0;
1249 _keys = std::move(result.keys);
1250 _keysSnapshot = input.snapshot;
1251 if (result.sorted != nullptr) {
1252 _sortCache = SortCache{.chain = input.sort, .snapshot = input.snapshot, .order = std::move(result.sorted)};
1253 } else if (_sortCache.snapshot != input.snapshot) {
1254 // An order of older rows: a repair it missed would merge into it
1255 // as if it were current.
1256 _sortCache = {};
1257 }
1258 MORPH_PLOT("table.sourceRows", input.snapshot->rowCount());
1259 MORPH_PLOT("table.viewRows", result.view.size());
1260 MORPH_PLOT("table.keyCacheColumns",
1261 std::ranges::count_if(_keys, [](auto const& keys) { return keys != nullptr; }));
1262 bool const columnsChanged = ticket.columnsVersion != _viewColumnsVersion;
1263 if (columnsChanged) {
1264 _viewColumns = input.columns;
1265 _viewColumnsVersion = ticket.columnsVersion;
1266 }
1267 _viewEpoch = ticket.epoch;
1268 _appliedSort = input.sort;
1269 _appliedFilter = input.filter;
1270 _appliedFilterSpec = _filterSpec;
1271 _trueView = std::move(result.view);
1272 _viewSnapshot = input.snapshot;
1273 _relayoutPending = false;
1274 _viewIndexValid = false;
1275 _keyIndexValid = false;
1276 Applied out{.change = std::move(result.change), .armSettle = false};
1277 if (_options.reorder == ReorderPolicy::Deferred) {
1278 // An ended edit's token stands for the notifications up to its own
1279 // count: one the shown view had already taken in has no use.
1280 std::erase_if(_releaseOnRepair, [&](auto const& entry) { return entry.second <= _viewChangeSeq; });
1281 if (input.repair) {
1282 holdUpdatedRows(input);
1283 } else {
1284 // A sort, filter or structural change releases every held row.
1285 _holds.clear();
1286 }
1287 std::erase_if(_releaseOnRepair, [&](auto const& entry) { return entry.second <= ticket.changeSeq; });
1288 }
1289 _viewChangeSeq = ticket.changeSeq;
1290 if (!_holds.empty()) {
1291 auto displayed = holdLayout(input.previousView);
1292 out.change = diffViews(input.previousView, displayed, input.dirty, input.snapshot->rowCount(),
1293 _options.resetThreshold);
1294 _view = std::move(displayed);
1295 out.armSettle = true;
1296 } else {
1297 _view = _trueView;
1298 }
1299 if (columnsChanged) {
1300 // Row operations cannot say that the columns changed.
1301 out.change.ops.assign(1, ViewOp{.kind = ViewOp::Kind::Reset, .first = 0, .count = 0, .to = 0});
1302 }
1303 return out;
1304 }
1305
1306 // Under `Deferred`, every updated row that was shown, and every row being
1307 // edited, keeps its place; a row whose edit ended before its commit's
1308 // update arrived does not.
1309 void holdUpdatedRows(detail::JobInput const& input) {
1310 std::vector<bool> shown(input.snapshot->rowCount(), false);
1311 for (auto const row : input.previousView) {
1312 shown[row] = true;
1313 }
1314 for (auto const row : input.dirty) {
1315 auto key = input.snapshot->rowId(row);
1316 if (shown[row] && !_releaseOnRepair.contains(key)) {
1317 _holds.insert(std::move(key));
1318 }
1319 }
1320 for (auto const& editing : _editing) {
1321 _holds.insert(editing);
1322 }
1323 }
1324
1325 // The true view with every held row put back at its displayed index.
1326 [[nodiscard]] std::vector<std::uint32_t> holdLayout(std::span<std::uint32_t const> displayed) const {
1327 std::vector<std::pair<std::size_t, std::uint32_t>> held; // (displayed index, row)
1328 std::vector<bool> isHeld(_viewSnapshot->rowCount(), false);
1329 for (std::size_t i = 0; i < displayed.size(); ++i) {
1330 auto const row = displayed[i];
1331 if (_holds.contains(_viewSnapshot->rowId(row))) {
1332 held.emplace_back(i, row);
1333 isHeld[row] = true;
1334 }
1335 }
1336 std::vector<std::uint32_t> out;
1337 out.reserve(_trueView.size() + held.size());
1338 for (auto const row : _trueView) {
1339 if (!isHeld[row]) {
1340 out.push_back(row);
1341 }
1342 }
1343 for (auto const& [index, row] : held) {
1344 out.insert(out.begin() + static_cast<std::ptrdiff_t>(std::min(index, out.size())), row);
1345 }
1346 return out;
1347 }
1348
1349 // Lays the view out again after holds were released. While a job runs its
1350 // result is about to replace the view, and it was diffed against the view
1351 // as it is now: the release waits for it, and its layout takes it in.
1352 void relayout() {
1353 if (_running) {
1354 _relayoutPending = true;
1355 return;
1356 }
1357 _relayoutPending = false;
1358 auto displayed = _holds.empty() ? _trueView : holdLayout(_view);
1359 auto const change = diffViews(_view, displayed, {}, _viewSnapshot->rowCount(), _options.resetThreshold);
1360 _view = std::move(displayed);
1361 _viewIndexValid = false;
1362 _keyIndexValid = false;
1363 deliver(Applied{.change = change, .armSettle = false});
1364 }
1365
1366 // ── calling out ─────────────────────────────────────────────────────────
1367
1368 // Arms the settle timer and publishes the change. Returns `false` when a
1369 // handler destroyed the engine; the caller then returns at once.
1370 bool deliver(Applied const& applied) {
1371 auto const alive = _alive.token();
1372 if (applied.armSettle && _options.scheduleSettle) {
1373 auto const seq = _changeSeq;
1374 auto const scheduler = _options.scheduleSettle;
1375 scheduler(_options.settle, _alive.guard([this, seq] {
1376 if (seq == _changeSeq) {
1377 settleNow();
1378 }
1379 }));
1380 if (alive.expired()) {
1381 return false;
1382 }
1383 }
1384 if (!applied.change.empty() && _onViewChange) {
1385 // A copy: the handler may replace itself or destroy the engine.
1386 auto const handler = _onViewChange;
1387 handler(applied.change);
1388 }
1389 return !alive.expired();
1390 }
1391
1392 void setPending(bool value) {
1393 if (_pending == value) {
1394 return;
1395 }
1396 _pending = value;
1397 if (_onPending) {
1398 auto const handler = _onPending;
1399 handler(value);
1400 }
1401 }
1402
1403 void reportError(TableError const& error) const {
1404 if (_onError) {
1405 auto const handler = _onError;
1406 handler(error);
1407 }
1408 }
1409
1410 void recordOwnerStep(std::chrono::steady_clock::time_point began) {
1411 auto const micros = static_cast<std::size_t>(
1412 std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - began).count());
1413 _stats.maxOwnerStepMicros = std::max(_stats.maxOwnerStepMicros, micros);
1414 ++_stats.ownerSteps;
1415 }
1416
1417 void indexView() const {
1418 if (_viewIndexValid) {
1419 return;
1420 }
1421 _viewIndex.assign(_viewSnapshot->rowCount(), kAbsent);
1422 for (std::size_t i = 0; i < _view.size(); ++i) {
1423 _viewIndex[_view[i]] = static_cast<std::uint32_t>(i);
1424 }
1425 _viewIndexValid = true;
1426 }
1427
1428 void indexKeys() const {
1429 if (_keyIndexValid) {
1430 return;
1431 }
1432 _keyIndex.clear();
1433 _keyIndex.reserve(_view.size());
1434 for (std::size_t i = 0; i < _view.size(); ++i) {
1435 _keyIndex.emplace(_viewSnapshot->rowId(_view[i]), i);
1436 }
1437 _keyIndexValid = true;
1438 }
1439
1440 struct SortCache {
1441 SortChain chain;
1442 std::shared_ptr<RowSnapshot const> snapshot;
1443 std::shared_ptr<std::vector<std::uint32_t> const> order;
1444 };
1445
1446 std::shared_ptr<DataSource> _source;
1447 EngineOptions _options;
1448 // requested state: what the next job computes
1449 std::vector<ColumnInfo> _columns;
1450 std::shared_ptr<RowSnapshot const> _snapshot;
1451 SortChain _sort;
1452 FilterSpec _filterSpec;
1453 std::shared_ptr<CompiledFilter const> _filter;
1454 std::uint64_t _columnsVersion = 0;
1455 std::uint64_t _epoch = 0;
1456 // applied state: what the shown view was computed from
1457 std::vector<ColumnInfo> _viewColumns;
1458 std::shared_ptr<RowSnapshot const> _viewSnapshot;
1459 SortChain _appliedSort;
1460 FilterSpec _appliedFilterSpec;
1461 std::shared_ptr<CompiledFilter const> _appliedFilter;
1462 std::uint64_t _viewColumnsVersion = 0;
1463 std::uint64_t _viewEpoch = 0;
1464 std::uint64_t _viewChangeSeq = 0; // notifications the view took in
1465 std::vector<std::uint32_t> _view; // displayed
1466 std::vector<std::uint32_t> _trueView; // sorted and filtered, without holds
1467 // caches, valid for `_keysSnapshot`
1468 std::shared_ptr<RowSnapshot const> _keysSnapshot;
1469 std::vector<std::shared_ptr<KeyColumn const>> _keys;
1470 SortCache _sortCache;
1471 mutable std::vector<std::uint32_t> _viewIndex;
1472 mutable bool _viewIndexValid = false;
1473 mutable std::unordered_map<RowId, std::size_t> _keyIndex;
1474 mutable bool _keyIndexValid = false;
1475 // pending work
1476 std::vector<std::uint32_t> _notifiedRows; // updated, not yet absorbed
1477 bool _notified = false;
1478 bool _notifiedStructural = false;
1479 bool _notifiedReset = false;
1480 bool _flushScheduled = false;
1481 std::uint64_t _changeSeq = 0; // notifications received
1482 std::uint64_t _absorbedSeq = 0; // notifications absorbed
1483 std::vector<std::uint32_t> _dirty; // rows changed since `_keysSnapshot`
1484 bool _obligeAll = false; // every row is owed a `Changed`
1485 // deferred reorder
1486 std::unordered_set<RowId> _holds;
1487 std::unordered_set<RowId> _editing;
1488 std::unordered_map<RowId, std::uint64_t> _releaseOnRepair; // ended edits, by the notification count then
1489 bool _relayoutPending = false;
1490 // execution
1491 bool _pending = false;
1492 bool _running = false;
1493 detail::JobInput const* _runningInput = nullptr;
1494 Ticket _runningTicket;
1495 ::core::async::StopSource _jobStop;
1496 std::function<void(ViewChange const&)> _onViewChange;
1497 std::function<void(bool)> _onPending;
1498 std::function<void(TableError const&)> _onError;
1499 EngineStats _stats;
1500 async::CallbackScope _alive;
1501};
1502
1503// NOLINTEND(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
1504
1505} // namespace morph::table
Node column(Nodes const &children, ContainerOptions const &options={})
A column node.
Definition author.hpp:3394
Handle< T > row()
The innermost row, {"row": ""}.
Definition author.hpp:1145
Command seq(Commands const &steps)
seq: steps in order; a failed step stops the rest.
Definition author.hpp:2923
Node progress(AnyHandle const &value, AnyHandle const &label={})
A progress node.
Definition author.hpp:4026
Receives a DataSource's change notifications, on the owner thread.
ColumnLayout(std::vector< std::string > ids)
A layout of ids, all visible, in that order.
Definition engine.hpp:128
std::span< std::string const > order() const noexcept
The columns in display order.
Definition engine.hpp:132
void rebuild(std::vector< std::string > ids)
Adopts a new column list: kept columns keep their place and visibility, new ones are appended visible...
Definition engine.hpp:190
bool setVisible(std::string const &columnId, bool show)
Shows or hides a column.
Definition engine.hpp:150
std::vector< std::string > visible() const
The visible columns in display order.
Definition engine.hpp:136
bool move(std::string const &columnId, std::size_t position)
Moves a column to a display position.
Definition engine.hpp:169
void reset()
Restores the default order with every column visible.
Definition engine.hpp:182
static bool passes(Column const &filter, KeyColumn const &keys, std::uint32_t row)
Whether a row passes one column filter.
Definition filter.hpp:206
void onError(std::function< void(TableError const &)> handler)
Sets the receiver of errors no call could return: an injected service that threw in a job that did no...
Definition engine.hpp:823
Engine(std::shared_ptr< DataSource > source, EngineOptions options={})
Builds the engine; the initial view is every row in source order.
Definition engine.hpp:666
FilterSpec const & filter() const noexcept
The current filter.
Definition engine.hpp:733
std::shared_ptr< RowSnapshot const > const & viewSnapshot() const noexcept
The snapshot the view's rows index.
Definition engine.hpp:803
bool pending() const noexcept
Whether a computation is in flight.
Definition engine.hpp:807
void onViewChange(std::function< void(ViewChange const &)> handler)
Sets the receiver of view changes, called on the owner after the view changed.
Definition engine.hpp:812
std::expected< void, TableError > setSort(SortChain chain)
Sets the sort chain; an empty chain is source order.
Definition engine.hpp:699
std::size_t sourceRowCount() const
Rows in the source the view was computed from.
Definition engine.hpp:747
void beginEdit(RowId const &key)
Marks a row as being edited: with Deferred reorder it keeps its place through updates until endEdit.
Definition engine.hpp:832
std::expected< void, TableError > setFilter(FilterSpec spec)
Sets the filter.
Definition engine.hpp:717
EngineStats const & stats() const noexcept
Counters.
Definition engine.hpp:827
~Engine() override
Stops a running job and unsubscribes from the source.
Definition engine.hpp:686
std::optional< std::size_t > viewRowOf(std::size_t sourceRow) const
The view row showing a source row.
Definition engine.hpp:762
std::optional< std::size_t > sourceRowOf(std::size_t viewRow) const
The source row shown at a view row.
Definition engine.hpp:752
std::optional< std::size_t > viewRowOfKey(RowId const &key) const
The view row of the row keyed id.
Definition engine.hpp:778
void settleNow()
Releases every deferred row not being edited, as the settle interval does.
Definition engine.hpp:853
void endEdit(RowId const &key)
Ends an edit; a held row moves to its sorted place now.
Definition engine.hpp:841
SortChain const & sort() const noexcept
The current sort chain.
Definition engine.hpp:729
std::span< std::uint32_t const > view() const noexcept
The view as source rows.
Definition engine.hpp:799
std::vector< RowId > heldRows() const
Rows currently held in place by Deferred reorder.
Definition engine.hpp:860
Cell cellAt(std::size_t viewRow, std::size_t column) const
One cell of the view.
Definition engine.hpp:789
RowId rowIdAt(std::size_t viewRow) const
The key of the row at a view row.
Definition engine.hpp:773
std::span< ColumnInfo const > columns() const noexcept
The columns of the applied view: what cellAt indexes. A reset that changes the columns changes them w...
Definition engine.hpp:739
void onPending(std::function< void(bool)> handler)
Sets the receiver of pending() changes.
Definition engine.hpp:816
std::size_t viewRowCount() const noexcept
Rows in the view.
Definition engine.hpp:743
std::pair< std::size_t, std::size_t > progress() const noexcept
Phases done and total, for a progress sink.
Definition engine.hpp:318
JobInput const & input() const noexcept
What the job reads.
Definition engine.hpp:332
bool step(Deadline deadline, ::core::async::StopToken const &stop)
Runs until done, the deadline, or a stop request.
Definition engine.hpp:299
std::optional< std::string > const & failure() const noexcept
Why the job failed: the message of the exception that ended it.
Definition engine.hpp:328
void onPhase(std::function< void(std::size_t, std::size_t)> listener)
Sets a receiver of (phases done, total) after each phase ends, on the thread that runs the step.
Definition engine.hpp:314
JobResult takeResult()
Moves the result out. Valid once done(); the input stays readable.
Definition engine.hpp:336
ViewJob(JobInput input)
Prepares a job.
Definition engine.hpp:287
bool done() const noexcept
Whether every phase has run.
Definition engine.hpp:324
Handle< Member > at(Member Class::*member) const
A field of the value, by member pointer: a renamed field is a compile error.
Definition author.hpp:703
The rows a table engine reads, and the services it is given.
std::variant< std::monostate, std::int64_t, math::Rational, double, std::string, bool, QuantityCell > Cell
One cell as a data source hands it out.
std::variant< std::int64_t, std::string > RowId
A row's identity: the table's key, stable across reloads.
ReorderPolicy
What an updated row that would move does.
Definition engine.hpp:70
@ Immediate
It moves at once.
Definition engine.hpp:71
@ Deferred
It keeps its place until the view is quiet for settle, or its edit ends.
Definition engine.hpp:72
Filter specs as data, compiled once and evaluated column at a time.
TableError unknownColumn(std::string const &columnId)
An UnknownColumn error.
Definition filter.hpp:511
std::optional< std::size_t > findColumn(std::span< ColumnInfo const > columns, std::string_view columnId)
The index of the column with id columnId.
Definition filter.hpp:498
std::expected< CompiledFilter, TableError > compileFilter(FilterSpec const &spec, std::span< ColumnInfo const > columns, Services const &services)
Compiles spec against columns.
Definition filter.hpp:614
Compile-time profiler instrumentation: Tracy zones, plots, messages and thread names,...
#define MORPH_PLOT(name, value)
Records value on the plot named name.
Definition profiler.hpp:132
#define MORPH_ZONE_TEXT(text)
Attaches text to the enclosing scope's MORPH_ZONE; an empty text attaches nothing.
Definition profiler.hpp:126
#define MORPH_ZONE(name)
Opens a profiler zone named name that ends with the enclosing scope. At most one per scope.
Definition profiler.hpp:118
Typed sort keys and the chunked, stoppable merge sort over them.
std::vector< SortKey > SortChain
A multi-key sort, most significant key first. Empty means source order.
Definition sort.hpp:71
std::chrono::steady_clock::time_point Deadline
A deadline for one step of work; kNoDeadline runs to completion.
Definition sort.hpp:81
std::vector< std::uint32_t > sortRows(std::vector< std::uint32_t > rows, RowOrder order)
Sorts rows by order in one call.
Definition sort.hpp:648
void buildKeys(RowSnapshot const &snapshot, std::size_t column, ColumnInfo const &info, Services const &services, std::size_t first, std::size_t last, KeyColumn &out)
Builds the keys of rows [first, last) of one column into out.
Definition sort.hpp:409
Abstract executor interface.
Definition executor.hpp:70
virtual void post(std::function< void()> task)=0
Schedules task for asynchronous execution.
How an engine runs and what it is given.
Definition engine.hpp:76
ReorderPolicy reorder
What an updated row that would move does.
Definition engine.hpp:92
std::size_t resetThreshold
A ViewChange with more operations than this is one Reset.
Definition engine.hpp:86
exec::IExecutor * owner
Where results are applied. Null: every computation runs inside the call that asks for it (what table:...
Definition engine.hpp:79
std::size_t smallTable
Tables with fewer rows compute on the owner in one step.
Definition engine.hpp:84
Services services
Collator, date parser, comparators and progress sink.
Definition engine.hpp:90
std::chrono::microseconds frameBudget
The longest an owner step runs when there is no worker.
Definition engine.hpp:88
std::function< void(std::chrono::milliseconds, std::function< void()>)> scheduleSettle
Runs a callback on the owner after a delay; the engine uses it for the settle interval....
Definition engine.hpp:98
std::chrono::milliseconds settle
The quiet interval after which deferred rows move.
Definition engine.hpp:94
exec::IExecutor * worker
Where jobs run off the owner. Null: jobs run in frame-budgeted steps posted to owner....
Definition engine.hpp:82
Counters for tests, benchmarks and profiler plots.
Definition engine.hpp:102
std::size_t maxOwnerStepMicros
The longest single step on the owner thread, in microseconds.
Definition engine.hpp:118
std::size_t keyBuilds
Key columns built from scratch.
Definition engine.hpp:110
std::size_t ownerSteps
Steps run on the owner thread (one-step runs included).
Definition engine.hpp:116
std::size_t fullSorts
Sorts of every source row.
Definition engine.hpp:106
std::size_t dropped
Results dropped because a newer request superseded them.
Definition engine.hpp:114
std::size_t repairs
Repairs of updated rows.
Definition engine.hpp:112
std::size_t subsetSorts
Sorts of a filtered subset.
Definition engine.hpp:108
std::size_t jobs
Jobs started (one-step runs included).
Definition engine.hpp:104
A table's whole filter.
Definition filter.hpp:106
static KeyColumn forColumn(ColumnInfo const &info, Services const &services, std::size_t rows)
An empty key column for info, sized for rows rows.
Definition sort.hpp:209
The services an engine sorts and filters with. A null member is replaced by its default.
The error every fallible table call returns.
The operations that turn one view order into another.
@ Reset
The whole view was replaced.
Everything one view job reads. Immutable once the job starts.
Definition engine.hpp:220
bool indicesStable
previousView's rows index snapshot directly.
Definition engine.hpp:242
std::size_t resetThreshold
More operations than this make the change a reset.
Definition engine.hpp:254
bool repair
The job repairs updated rows rather than recomputing.
Definition engine.hpp:250
std::vector< std::uint32_t > previousTrueView
Repair: the sorted, filtered order, without holds, that the dirty rows merge into.
Definition engine.hpp:248
std::shared_ptr< std::vector< std::uint32_t > const > cachedSorted
The full source's order for sort on snapshot, or null.
Definition engine.hpp:236
std::vector< std::uint32_t > dirty
Rows whose cells changed since keys were built: their keys are rebuilt on copies, a repair merges the...
Definition engine.hpp:246
std::vector< std::size_t > needed
Columns whose keys the job needs.
Definition engine.hpp:230
std::vector< ColumnInfo > columns
The columns.
Definition engine.hpp:224
std::shared_ptr< RowSnapshot const > previousSnapshot
The snapshot previousView's rows index.
Definition engine.hpp:240
std::vector< std::uint32_t > previousView
The displayed view the change is computed against.
Definition engine.hpp:238
Services services
Services for this job alone (Services::forTask).
Definition engine.hpp:226
bool allChanged
Every surviving row may have new content (a reset).
Definition engine.hpp:252
std::shared_ptr< RowSnapshot const > snapshot
The rows.
Definition engine.hpp:222
SortChain sort
The sort chain.
Definition engine.hpp:232
std::shared_ptr< CompiledFilter const > filter
The compiled filter, or null.
Definition engine.hpp:234
std::vector< std::shared_ptr< KeyColumn const > > keys
Key columns by column index; null when not built.
Definition engine.hpp:228
What a finished job hands back to the owner.
Definition engine.hpp:258
std::shared_ptr< std::vector< std::uint32_t > const > sorted
A full order for the chain, to cache, or null.
Definition engine.hpp:262
std::size_t keyBuilds
Key columns built from scratch.
Definition engine.hpp:274
std::vector< std::uint32_t > view
The sorted, filtered view.
Definition engine.hpp:264
bool fullSort
Every source row was sorted.
Definition engine.hpp:268
std::vector< std::shared_ptr< KeyColumn const > > keys
Key columns by column index, for JobInput::snapshot.
Definition engine.hpp:260
ViewChange change
From JobInput::previousView to view.
Definition engine.hpp:266
bool subsetSort
Only the filter's survivors were sorted.
Definition engine.hpp:270
bool repaired
Updated rows were merged into the previous order.
Definition engine.hpp:272
What changed between two view orders, as list-model operations.
ViewChange diffViews(std::span< std::uint32_t const > before, std::span< std::uint32_t const > after, std::span< std::uint32_t const > changed, std::size_t identityLimit, std::size_t resetThreshold)
Computes the operations that turn before into after.