tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
maintenance.h
1#pragma once
2
3#include <tess/core/assert.h>
4#include <tess/core/config.h>
5#include <tess/core/fail_fast.h>
6#include <tess/diagnostics/diagnostics.h>
7
8#include <atomic>
9#include <bit>
10#include <cstddef>
11#include <cstdint>
12#include <limits>
13#include <mutex>
14#include <thread>
15#include <vector>
16
17namespace tess::experimental::maintenance {
18
20class MaintenanceBudget {
21 public:
22 explicit constexpr MaintenanceBudget(
23 std::uint64_t units = std::numeric_limits<std::uint64_t>::max()) noexcept
24 : remaining_(units) {}
25
26 [[nodiscard]] constexpr auto consume(std::uint64_t units = 1) noexcept
27 -> bool {
28 if (units > remaining_) {
29 return false;
30 }
31 remaining_ -= units;
32 return true;
33 }
34
35 [[nodiscard]] constexpr auto remaining() const noexcept -> std::uint64_t {
36 return remaining_;
37 }
38
39 private:
40 std::uint64_t remaining_;
41};
42
44class MaintenanceTask {
45 public:
46 MaintenanceTask() = default;
47 MaintenanceTask(const MaintenanceTask&) = delete;
48 auto operator=(const MaintenanceTask&) -> MaintenanceTask& = delete;
49 MaintenanceTask(MaintenanceTask&&) = delete;
50 auto operator=(MaintenanceTask&&) -> MaintenanceTask& = delete;
51
52 virtual ~MaintenanceTask() {
53 if (registration_epoch_.load(std::memory_order_relaxed) != 0) {
54 ::tess::detail::fail_fast(
55 "MaintenanceTask destroyed while registered; release it or "
56 "destroy its registered scheduler first");
57 }
58 }
59 virtual void run(MaintenanceBudget& budget) = 0;
60
61 private:
62 template <typename Backend>
63 friend class RegisteredScheduler;
64
65 std::atomic<std::uint64_t> registration_epoch_ = 0;
66};
67
70 std::uint64_t schedule_calls = 0;
71 std::uint64_t coalesced_calls = 0;
72 std::uint64_t executions = 0;
73 std::uint64_t capacity_failures = 0;
74};
75
78 public:
79 virtual ~MaintenanceScheduler() = default;
80
89 [[nodiscard]] virtual auto schedule(MaintenanceTask& task) -> bool = 0;
90
98 [[nodiscard]] virtual auto run_some(MaintenanceBudget budget) -> bool = 0;
99
101 [[nodiscard]] virtual auto flush() -> bool = 0;
102
103 [[nodiscard]] virtual auto metrics() const noexcept -> MaintenanceMetrics = 0;
104
106 [[nodiscard]] virtual auto has_pending() const noexcept -> bool = 0;
107};
108
109namespace detail {
110
111class MetricsStore {
112 public:
113 void record_schedule() noexcept {
114 schedule_calls_.fetch_add(1, std::memory_order_relaxed);
115 }
116 void record_coalesced() noexcept {
117 coalesced_calls_.fetch_add(1, std::memory_order_relaxed);
118 }
119 void record_execution() noexcept {
120 executions_.fetch_add(1, std::memory_order_relaxed);
121 }
122 void record_capacity_failure() noexcept {
123 capacity_failures_.fetch_add(1, std::memory_order_relaxed);
124 }
125
126 [[nodiscard]] auto snapshot() const noexcept -> MaintenanceMetrics {
127 return MaintenanceMetrics{
128 schedule_calls_.load(std::memory_order_relaxed),
129 coalesced_calls_.load(std::memory_order_relaxed),
130 executions_.load(std::memory_order_relaxed),
131 capacity_failures_.load(std::memory_order_relaxed)};
132 }
133
134 private:
135 std::atomic<std::uint64_t> schedule_calls_ = 0;
136 std::atomic<std::uint64_t> coalesced_calls_ = 0;
137 std::atomic<std::uint64_t> executions_ = 0;
138 std::atomic<std::uint64_t> capacity_failures_ = 0;
139};
140
142struct QueuedMaintenanceEntry {
143 MaintenanceTask* task = nullptr;
144 std::uint64_t admitted_tick = 0;
145 std::size_t queue_slot = std::numeric_limits<std::size_t>::max();
146};
147
148class BoundedTaskQueue {
149 public:
150 explicit BoundedTaskQueue(std::size_t capacity) : entries_(capacity) {}
151
152 [[nodiscard]] auto push(MaintenanceTask& task, std::uint64_t admitted_tick,
153 std::size_t* queue_slot = nullptr) noexcept -> bool {
154 if (size_ == entries_.size()) {
155 return false;
156 }
157 const auto tail = (head_ + size_) % entries_.size();
158 entries_[tail] = QueuedMaintenanceEntry{&task, admitted_tick, tail};
159 if (queue_slot != nullptr) {
160 *queue_slot = tail;
161 }
162 ++size_;
163 return true;
164 }
165
166 [[nodiscard]] auto pop() noexcept -> QueuedMaintenanceEntry {
167 if (size_ == 0) {
168 return {};
169 }
170 auto entry = entries_[head_];
171 entries_[head_] = {};
172 head_ = (head_ + 1) % entries_.size();
173 --size_;
174 return entry;
175 }
176
177 [[nodiscard]] auto empty() const noexcept -> bool { return size_ == 0; }
178
179 [[nodiscard]] auto size() const noexcept -> std::size_t { return size_; }
180
181 [[nodiscard]] auto oldest_admitted_tick() const noexcept -> std::uint64_t {
182 return size_ == 0 ? 0 : entries_[head_].admitted_tick;
183 }
184
185 private:
186 std::vector<QueuedMaintenanceEntry> entries_;
187 std::size_t head_ = 0;
188 std::size_t size_ = 0;
189};
190
192class PendingTaskIndex {
193 public:
194 explicit PendingTaskIndex(std::size_t capacity)
195 : buckets_(bucket_count(capacity), npos), nodes_(capacity) {}
196
197 [[nodiscard]] auto contains(const MaintenanceTask& task) const noexcept
198 -> bool {
199 if (buckets_.empty()) {
200 return false;
201 }
202 for (auto index = buckets_[home(task)]; index != npos;
203 index = nodes_[index].next) {
204 if (nodes_[index].task == &task) {
205 return true;
206 }
207 }
208 return false;
209 }
210
211 [[nodiscard]] auto insert(MaintenanceTask& task,
212 std::size_t queue_slot) noexcept -> bool {
213 if (buckets_.empty() || queue_slot >= nodes_.size() ||
214 nodes_[queue_slot].task != nullptr) {
215 return false;
216 }
217 const auto bucket = home(task);
218 nodes_[queue_slot] = Node{&task, buckets_[bucket]};
219 buckets_[bucket] = queue_slot;
220 return true;
221 }
222
223 [[nodiscard]] auto erase(const MaintenanceTask& task,
224 std::size_t queue_slot) noexcept -> bool {
225 if (buckets_.empty() || queue_slot >= nodes_.size()) {
226 return false;
227 }
228 auto* link = &buckets_[home(task)];
229 while (*link != npos) {
230 if (*link == queue_slot) {
231 *link = nodes_[queue_slot].next;
232 nodes_[queue_slot] = {};
233 return true;
234 }
235 link = &nodes_[*link].next;
236 }
237 return false;
238 }
239
240 private:
241 static constexpr auto npos = std::numeric_limits<std::size_t>::max();
242
243 struct Node {
244 MaintenanceTask* task = nullptr;
245 std::size_t next = npos;
246 };
247
248 [[nodiscard]] static auto bucket_count(std::size_t capacity) noexcept
249 -> std::size_t {
250 if (capacity == 0) {
251 return 0;
252 }
253 const auto maximum = std::numeric_limits<std::size_t>::max();
254 return capacity > maximum - capacity ? capacity : capacity * 2;
255 }
256
257 [[nodiscard]] auto home(const MaintenanceTask& task) const noexcept
258 -> std::size_t {
259 auto value = reinterpret_cast<std::uintptr_t>(&task);
260 value >>= 3u;
261 value ^= value >> 17u;
262 if constexpr (sizeof(value) >= sizeof(std::uint64_t)) {
263 value *= std::uintptr_t{0x9e3779b97f4a7c15ULL};
264 } else {
265 value *= std::uintptr_t{0x9e3779b9U};
266 }
267 return static_cast<std::size_t>(value) % buckets_.size();
268 }
269
270 std::vector<std::size_t> buckets_;
271 std::vector<Node> nodes_;
272};
273
274template <bool Coalescing>
275class QueuedScheduler : public MaintenanceScheduler {
276 public:
277 explicit QueuedScheduler(std::size_t capacity)
278 : queue_(capacity), pending_(Coalescing ? capacity : 0) {}
279
280 [[nodiscard]] auto schedule(MaintenanceTask& task) -> bool override {
281 metrics_.record_schedule();
282 const auto lock = std::scoped_lock{queue_mutex_};
283 // Only synchronous calls from task.run() establish a follow-up. A
284 // concurrent producer must not make a completed task look stalled.
285 const auto called_from_task = running_thread_ == std::this_thread::get_id();
286 if constexpr (Coalescing) {
287 if (pending_.contains(task)) {
288 if (called_from_task) {
289 running_task_scheduled_ = true;
290 }
291 metrics_.record_coalesced();
292 if (accounting_ != nullptr) {
293 ++accounting_->counters.offered;
294 ++accounting_->counters.coalesced_into_pending;
295 }
296 return true;
297 }
298 }
299 const auto admitted_tick =
300 accounting_ != nullptr ? accounting_->last_observed_tick : 0;
301 auto queue_slot = std::size_t{0};
302 if (!queue_.push(task, admitted_tick, &queue_slot)) {
303 metrics_.record_capacity_failure();
304 if (accounting_ != nullptr) {
305 ++accounting_->counters.offered;
306 ++accounting_->counters.rejected;
307 }
308 return false;
309 }
310 if constexpr (Coalescing) {
311 const auto inserted = pending_.insert(task, queue_slot);
312 TESS_ASSERT(inserted);
313 static_cast<void>(inserted);
314 }
315 if (called_from_task) {
316 running_task_scheduled_ = true;
317 }
318 if (accounting_ != nullptr) {
319 ++accounting_->counters.offered;
320 accounting_->record_admitted();
321 }
322 return true;
323 }
324
325 [[nodiscard]] auto run_some(MaintenanceBudget budget) -> bool override {
326 const auto run_lock = std::scoped_lock{run_mutex_};
327 if (accounting_ != nullptr &&
328 budget.remaining() != std::numeric_limits<std::uint64_t>::max()) {
329 const auto lock = std::scoped_lock{queue_mutex_};
330 accounting_->counters.offered_work_units += budget.remaining();
331 }
332 while (budget.remaining() != 0) {
333 auto entry = detail::QueuedMaintenanceEntry{};
334 {
335 const auto queue_lock = std::scoped_lock{queue_mutex_};
336 entry = pop_pending();
337 }
338 if (entry.task == nullptr) {
339 return true;
340 }
341 if (!run_task(entry, budget)) {
342 return false;
343 }
344 }
345 return true;
346 }
347
348 [[nodiscard]] auto flush() -> bool override {
349 const auto run_lock = std::scoped_lock{run_mutex_};
350 // The unbounded flush budget is deliberately not offered work: it
351 // is not a meaningful workload measurement.
352 auto budget = MaintenanceBudget{};
353 for (;;) {
354 auto entry = detail::QueuedMaintenanceEntry{};
355 {
356 const auto queue_lock = std::scoped_lock{queue_mutex_};
357 entry = pop_pending();
358 }
359 if (entry.task == nullptr) {
360 return true;
361 }
362 if (!run_task(entry, budget)) {
363 return false;
364 }
365 }
366 }
367
368 [[nodiscard]] auto metrics() const noexcept -> MaintenanceMetrics override {
369 return metrics_.snapshot();
370 }
371
372 [[nodiscard]] auto has_pending() const noexcept -> bool override {
373 const auto lock = std::scoped_lock{queue_mutex_};
374 return !queue_.empty() || running_active_;
375 }
376
384 void set_flow_accounting(diagnostics::FlowAccounting* accounting) {
385 const auto run_lock = std::scoped_lock{run_mutex_};
386 const auto lock = std::scoped_lock{queue_mutex_};
387 TESS_ASSERT(queue_.empty());
388 accounting_ = accounting;
389 }
390
393 void observe_flow_tick(std::uint64_t tick) {
394 const auto lock = std::scoped_lock{queue_mutex_};
395 if (accounting_ == nullptr) {
396 return;
397 }
398 accounting_->observe_tick(tick);
399 const auto now = accounting_->last_observed_tick;
400 auto any = false;
401 auto oldest = now;
402 if (!queue_.empty()) {
403 any = true;
404 oldest = queue_.oldest_admitted_tick();
405 }
406 if (running_active_ && running_admitted_tick_ < oldest) {
407 any = true;
408 oldest = running_admitted_tick_;
409 }
410 if (running_active_ && !any) {
411 any = true;
412 }
413 accounting_->counters.oldest_outstanding_age_ticks = any ? now - oldest : 0;
414 }
415
416 private:
417 [[nodiscard]] auto run_task(detail::QueuedMaintenanceEntry entry,
418 MaintenanceBudget& budget) -> bool {
419 auto& task = *entry.task;
420 metrics_.record_execution();
421 const auto before = budget.remaining();
422 {
423 const auto lock = std::scoped_lock{queue_mutex_};
424 running_thread_ = std::this_thread::get_id();
425 running_task_scheduled_ = false;
426 running_active_ = true;
427 running_admitted_tick_ = entry.admitted_tick;
428 }
429#if TESS_HAS_EXCEPTIONS
430 try {
431 task.run(budget);
432 } catch (...) {
433 // The queue entry was consumed before invocation and is not restored.
434 // Tasks own the authoritative dirty-mask and content-version state,
435 // which must remain set
436 // on failure; the caller decides whether explicitly scheduling a retry
437 // is safe after observing the exception.
438 const auto lock = std::scoped_lock{queue_mutex_};
439 running_thread_ = {};
440 running_task_scheduled_ = false;
441 running_active_ = false;
442 account_terminal(entry, before, budget.remaining(), false);
443 throw;
444 }
445#else
446 task.run(budget);
447#endif
448 auto scheduled_follow_up = false;
449 {
450 const auto lock = std::scoped_lock{queue_mutex_};
451 scheduled_follow_up = running_task_scheduled_;
452 running_thread_ = {};
453 running_task_scheduled_ = false;
454 running_active_ = false;
455 account_terminal(entry, before, budget.remaining(), true);
456 }
457 // A no-op task may finish without consuming budget. A task that queues
458 // follow-up work has not finished, however, so continuing could spin
459 // through A -> B -> A forever. Stop this drain and leave the follow-up
460 // queued for explicit caller intervention.
461 return budget.remaining() != before || !scheduled_follow_up;
462 }
463
466 [[nodiscard]] auto pop_pending() noexcept -> QueuedMaintenanceEntry {
467 auto entry = queue_.pop();
468 if constexpr (Coalescing) {
469 if (entry.task != nullptr) {
470 const auto erased = pending_.erase(*entry.task, entry.queue_slot);
471 TESS_ASSERT(erased);
472 static_cast<void>(erased);
473 }
474 }
475 return entry;
476 }
477
481 void account_terminal(const detail::QueuedMaintenanceEntry& entry,
482 std::uint64_t budget_before, std::uint64_t budget_after,
483 bool completed) noexcept {
484 if (accounting_ == nullptr) {
485 return;
486 }
487 auto& counters = accounting_->counters;
488 counters.consumed_work_units += budget_before - budget_after;
489 ++(completed ? counters.completed : counters.failed);
490 accounting_->record_left_outstanding();
491 counters.residence_ticks_accumulated +=
492 accounting_->last_observed_tick - entry.admitted_tick;
493 }
494
495 mutable std::mutex queue_mutex_;
496 std::mutex run_mutex_;
497 BoundedTaskQueue queue_;
498 PendingTaskIndex pending_;
499
500 public:
503 ~QueuedScheduler() override {
504 if (accounting_ == nullptr) {
505 return;
506 }
507 for (;;) {
508 const auto entry = pop_pending();
509 if (entry.task == nullptr) {
510 break;
511 }
512 ++accounting_->counters.dropped_after_admission;
513 accounting_->record_left_outstanding();
514 accounting_->counters.residence_ticks_accumulated +=
515 accounting_->last_observed_tick - entry.admitted_tick;
516 }
517 }
518
519 private:
520 MetricsStore metrics_;
521 std::thread::id running_thread_;
522 bool running_task_scheduled_ = false;
523 bool running_active_ = false;
524 std::uint64_t running_admitted_tick_ = 0;
525 diagnostics::FlowAccounting* accounting_ = nullptr;
526};
527
528} // namespace detail
529
531class ImmediateScheduler final : public MaintenanceScheduler {
532 public:
533 explicit ImmediateScheduler(std::size_t = 0) {}
534
535 [[nodiscard]] auto schedule(MaintenanceTask& task) -> bool override {
536 // A task may call schedule() while it runs, so this must be recursive.
537 // The same lock also preserves synchronous return semantics for concurrent
538 // callers and prevents active_run_ from borrowing another thread's frame.
539 const auto run_lock = std::scoped_lock{run_mutex_};
540 metrics_.record_schedule();
541 for (auto* active = active_run_; active != nullptr;
542 active = active->parent) {
543 if (active->task != &task) {
544 continue;
545 }
546 if (active->pending == std::numeric_limits<std::uint64_t>::max()) {
547 metrics_.record_capacity_failure();
548 account_offer(Offer::Rejected);
549 return false;
550 }
551 ++active->pending;
552 account_offer(Offer::Coalesced);
553 return true;
554 }
555
556 // An intrusive stack of call-local frames makes A -> B -> A and direct
557 // self-scheduling iterative without allocating. A count, rather than a
558 // bool, preserves ImmediateScheduler's one-execution-per-request baseline.
559 auto active = ActiveRun{&task, 1, active_run_};
560 struct ActiveRunGuard {
561 ActiveRun*& current;
562 ActiveRun* previous;
563 ~ActiveRunGuard() { current = previous; }
564 };
565 active_run_ = &active;
566 // active_run_ borrows this frame only until guard restores its parent
567 // during the same schedule() call; task.run() cannot retain the frame.
568 // cppcheck-suppress danglingLifetime
569 const auto guard = ActiveRunGuard{active_run_, active.parent};
570 account_offer(Offer::Admitted);
571 auto budget = MaintenanceBudget{};
572 auto completed_ok = true;
573 auto consumed_total = std::uint64_t{0};
574 while (active.pending != 0) {
575 --active.pending;
576 const auto before = budget.remaining();
577 metrics_.record_execution();
578#if TESS_HAS_EXCEPTIONS
579 try {
580 task.run(budget);
581 } catch (...) {
582 consumed_total += before - budget.remaining();
583 account_run_terminal(consumed_total, true);
584 throw;
585 }
586#else
587 task.run(budget);
588#endif
589 consumed_total += before - budget.remaining();
590 if (active.pending != 0 && budget.remaining() == before) {
591 completed_ok = false;
592 break;
593 }
594 }
595 // A zero-progress abandonment still ran the admitted request; its
596 // residual repeats were coalesced offers, never admissions.
597 account_run_terminal(consumed_total, false);
598 return completed_ok;
599 }
600
601 [[nodiscard]] auto run_some(MaintenanceBudget) -> bool override {
602 return true;
603 }
604 [[nodiscard]] auto flush() -> bool override { return true; }
605 [[nodiscard]] auto metrics() const noexcept -> MaintenanceMetrics override {
606 return metrics_.snapshot();
607 }
608
609 [[nodiscard]] auto has_pending() const noexcept -> bool override {
610 const auto run_lock = std::scoped_lock{run_mutex_};
611 return active_run_ != nullptr;
612 }
613
621 const auto run_lock = std::scoped_lock{run_mutex_};
622 TESS_ASSERT(active_run_ == nullptr);
623 accounting_ = accounting;
624 }
625
628 void observe_flow_tick(std::uint64_t tick) {
629 const auto run_lock = std::scoped_lock{run_mutex_};
630 if (accounting_ != nullptr) {
631 accounting_->observe_tick(tick);
632 accounting_->counters.oldest_outstanding_age_ticks = 0;
633 }
634 }
635
636 private:
637 struct ActiveRun {
638 MaintenanceTask* task = nullptr;
639 std::uint64_t pending = 0;
640 ActiveRun* parent = nullptr;
641 };
642
643 enum class Offer : std::uint8_t { Admitted, Rejected, Coalesced };
644
645 void account_offer(Offer offer) noexcept {
646 if (accounting_ == nullptr) {
647 return;
648 }
649 ++accounting_->counters.offered;
650 switch (offer) {
651 case Offer::Admitted:
652 accounting_->record_admitted();
653 break;
654 case Offer::Rejected:
655 ++accounting_->counters.rejected;
656 break;
657 case Offer::Coalesced:
658 ++accounting_->counters.coalesced_into_pending;
659 break;
660 }
661 }
662
663 void account_run_terminal(std::uint64_t consumed, bool failed) noexcept {
664 if (accounting_ == nullptr) {
665 return;
666 }
667 accounting_->counters.consumed_work_units += consumed;
668 ++(failed ? accounting_->counters.failed : accounting_->counters.completed);
669 accounting_->record_left_outstanding();
670 }
671
672 detail::MetricsStore metrics_;
673 mutable std::recursive_mutex run_mutex_;
674 ActiveRun* active_run_ = nullptr;
675 diagnostics::FlowAccounting* accounting_ = nullptr;
676};
677
691class DirtyBitScheduler final : public MaintenanceScheduler {
692 public:
693 explicit DirtyBitScheduler(std::size_t capacity)
694 : tasks_(capacity, nullptr),
695 registrations_(index_capacity(capacity)),
696 pending_(word_count(capacity)) {
697 for (auto& word : pending_) {
698 word.store(0, std::memory_order_relaxed);
699 }
700 }
701
703 [[nodiscard]] auto register_task(MaintenanceTask& task) -> bool {
704 const auto setup_lock = std::scoped_lock{setup_mutex_};
705 if (sealed_.load(std::memory_order_relaxed)) {
706 return false;
707 }
708 if (find_index(task) != npos) {
709 return true;
710 }
711 if (registered_ == tasks_.size()) {
712 metrics_.record_capacity_failure();
713 return false;
714 }
715 const auto index = registered_++;
716 tasks_[index] = &task;
717 auto slot = home(task);
718 while (registrations_[slot].task != nullptr) {
719 slot = next_registration(slot);
720 }
721 registrations_[slot] = Registration{&task, index};
722 return true;
723 }
724
726 void seal() {
727 const auto setup_lock = std::scoped_lock{setup_mutex_};
728 sealed_.store(true, std::memory_order_release);
729 }
730
731 [[nodiscard]] auto schedule(MaintenanceTask& task) -> bool override {
732 metrics_.record_schedule();
733 if (!sealed_.load(std::memory_order_acquire)) {
734 return false;
735 }
736 const auto index = find_index(task);
737 if (index == npos) {
738 return false;
739 }
740 const auto mask = std::uint64_t{1} << (index % 64u);
741 const auto previous =
742 pending_[index / 64u].fetch_or(mask, std::memory_order_release);
743 if ((previous & mask) != 0) {
744 metrics_.record_coalesced();
745 }
746 if (active_scheduler_ == this) {
747 running_task_scheduled_ = true;
748 }
749 return true;
750 }
751
752 [[nodiscard]] auto run_some(MaintenanceBudget budget) -> bool override {
753 if (!sealed_.load(std::memory_order_acquire)) {
754 return false;
755 }
756 const auto run_lock = std::scoped_lock{run_mutex_};
757 return drain(budget);
758 }
759
760 [[nodiscard]] auto flush() -> bool override {
761 if (!sealed_.load(std::memory_order_acquire)) {
762 return false;
763 }
764 const auto run_lock = std::scoped_lock{run_mutex_};
765 auto budget = MaintenanceBudget{};
766 return drain(budget);
767 }
768
769 [[nodiscard]] auto metrics() const noexcept -> MaintenanceMetrics override {
770 return metrics_.snapshot();
771 }
772
773 [[nodiscard]] auto has_pending() const noexcept -> bool override {
774 for (const auto& word : pending_) {
775 if (word.load(std::memory_order_acquire) != 0) {
776 return true;
777 }
778 }
779 return false;
780 }
781
782 private:
783 struct Registration {
784 MaintenanceTask* task = nullptr;
785 std::size_t index = 0;
786 };
787
788 static constexpr auto npos = std::numeric_limits<std::size_t>::max();
789
790 [[nodiscard]] static auto index_capacity(std::size_t capacity) noexcept
791 -> std::size_t {
792 if (capacity == 0) {
793 return 0;
794 }
795 const auto maximum = std::numeric_limits<std::size_t>::max();
796 return capacity > maximum - capacity ? capacity : capacity * 2;
797 }
798
799 [[nodiscard]] static auto word_count(std::size_t capacity) noexcept
800 -> std::size_t {
801 return capacity / 64u + (capacity % 64u == 0 ? 0u : 1u);
802 }
803
804 [[nodiscard]] auto home(const MaintenanceTask& task) const noexcept
805 -> std::size_t {
806 auto value = reinterpret_cast<std::uintptr_t>(&task);
807 value >>= 3u;
808 value ^= value >> 17u;
809 if constexpr (sizeof(value) >= sizeof(std::uint64_t)) {
810 value *= std::uintptr_t{0x9e3779b97f4a7c15ULL};
811 } else {
812 value *= std::uintptr_t{0x9e3779b9U};
813 }
814 return static_cast<std::size_t>(value) % registrations_.size();
815 }
816
817 [[nodiscard]] auto next_registration(std::size_t slot) const noexcept
818 -> std::size_t {
819 return slot + 1 == registrations_.size() ? 0 : slot + 1;
820 }
821
822 [[nodiscard]] auto find_index(const MaintenanceTask& task) const noexcept
823 -> std::size_t {
824 if (registrations_.empty()) {
825 return npos;
826 }
827 auto slot = home(task);
828 for (std::size_t probed = 0; probed < registrations_.size(); ++probed) {
829 if (registrations_[slot].task == nullptr) {
830 return npos;
831 }
832 if (registrations_[slot].task == &task) {
833 return registrations_[slot].index;
834 }
835 slot = next_registration(slot);
836 }
837 return npos;
838 }
839
840 [[nodiscard]] auto drain(MaintenanceBudget& budget) -> bool {
841 for (;;) {
842 auto executed = false;
843 for (std::size_t word_index = 0; word_index < pending_.size();
844 ++word_index) {
845 auto word = pending_[word_index].exchange(0, std::memory_order_acquire);
846 while (word != 0) {
847 if (budget.remaining() == 0) {
848 pending_[word_index].fetch_or(word, std::memory_order_release);
849 return true;
850 }
851 const auto bit = static_cast<std::size_t>(std::countr_zero(word));
852 const auto index = word_index * 64u + bit;
853 const auto mask = std::uint64_t{1} << bit;
854 word &= ~mask;
855 executed = true;
856#if TESS_HAS_EXCEPTIONS
857 try {
858 if (!run_task(*tasks_[index], budget)) {
859 if (word != 0) {
860 pending_[word_index].fetch_or(word, std::memory_order_release);
861 }
862 return false;
863 }
864 } catch (...) {
865 if (word != 0) {
866 pending_[word_index].fetch_or(word, std::memory_order_release);
867 }
868 throw;
869 }
870#else
871 if (!run_task(*tasks_[index], budget)) {
872 if (word != 0) {
873 pending_[word_index].fetch_or(word, std::memory_order_release);
874 }
875 return false;
876 }
877#endif
878 }
879 }
880 if (!executed) {
881 return true;
882 }
883 }
884 }
885
886 [[nodiscard]] auto run_task(MaintenanceTask& task, MaintenanceBudget& budget)
887 -> bool {
888 metrics_.record_execution();
889 const auto before = budget.remaining();
890 struct ActiveRunGuard {
891 DirtyBitScheduler*& active;
892 DirtyBitScheduler* previous;
893 ~ActiveRunGuard() { active = previous; }
894 };
895 const auto previous = active_scheduler_;
896 active_scheduler_ = this;
897 const auto guard = ActiveRunGuard{active_scheduler_, previous};
898 running_task_scheduled_ = false;
899 task.run(budget);
900 return budget.remaining() != before || !running_task_scheduled_;
901 }
902
903 detail::MetricsStore metrics_;
904 std::mutex setup_mutex_;
905 std::mutex run_mutex_;
906 std::vector<MaintenanceTask*> tasks_;
907 std::vector<Registration> registrations_;
908 std::vector<std::atomic<std::uint64_t>> pending_;
909 std::size_t registered_ = 0;
910 std::atomic<bool> sealed_ = false;
911 bool running_task_scheduled_ = false;
912 inline static thread_local DirtyBitScheduler* active_scheduler_ = nullptr;
913};
914
916using FifoScheduler = detail::QueuedScheduler<false>;
917
919using CoalescingScheduler = detail::QueuedScheduler<true>;
920
921} // namespace tess::experimental::maintenance
auto flush() -> bool override
Completes all reachable work; see the non-reentrant drain contract above.
Definition maintenance.h:760
void seal()
Definition maintenance.h:726
auto schedule(MaintenanceTask &task) -> bool override
Definition maintenance.h:731
auto run_some(MaintenanceBudget budget) -> bool override
Definition maintenance.h:752
auto has_pending() const noexcept -> bool override
Definition maintenance.h:773
auto register_task(MaintenanceTask &task) -> bool
Definition maintenance.h:703
auto schedule(MaintenanceTask &task) -> bool override
Definition maintenance.h:535
auto run_some(MaintenanceBudget) -> bool override
Definition maintenance.h:601
auto flush() -> bool override
Completes all reachable work; see the non-reentrant drain contract above.
Definition maintenance.h:604
void observe_flow_tick(std::uint64_t tick)
Definition maintenance.h:628
auto has_pending() const noexcept -> bool override
Definition maintenance.h:609
void set_flow_accounting(diagnostics::FlowAccounting *accounting)
Definition maintenance.h:620
Shared unit budget passed through one maintenance drain.
Definition maintenance.h:20
Backend-neutral experimental maintenance scheduler interface.
Definition maintenance.h:77
virtual auto has_pending() const noexcept -> bool=0
virtual auto flush() -> bool=0
Completes all reachable work; see the non-reentrant drain contract above.
virtual auto run_some(MaintenanceBudget budget) -> bool=0
virtual auto schedule(MaintenanceTask &task) -> bool=0
Long-lived derived-state maintenance operation.
Definition maintenance.h:44
friend class RegisteredScheduler
Definition maintenance.h:63
Definition diagnostics.h:505
void observe_tick(std::uint64_t tick) noexcept
Definition diagnostics.h:513
Scheduler observations used by experiments and diagnostics.
Definition maintenance.h:69