tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
auto_exec.h
1#pragma once
2
3#include <tess/core/config.h>
4#include <tess/core/fail_fast.h>
5#include <tess/ops/phase_executor.h>
6#include <tess/ops/queued.h>
7#include <tess/ops/result_channel.h>
8#include <tess/sim/schedule.h>
9
10#include <cstddef>
11#include <cstdint>
12
13// One schedule task runs the whole queued-operations
14// pipeline -- plan -> parallel phase planning -> execute (serial or pool,
15// chosen per phase) -> per-phase dirty apply -> ack drain -- over a
16// caller-owned OperationBatch queue. Enqueue whenever; the pipeline runs on the
17// task's cadence and both the queue and its result channel are cleared
18// together at the end of every run (the paired-clear discipline handles
19// restart at zero).
20namespace tess {
21
23enum class AutoExecStatus : std::uint8_t {
24 // No operations were queued; the run was a no-op.
25 Idle,
26 // Every planned operation executed; rejected operations (if any) were
27 // delivered through the drain with reasons.
28 Executed,
29 // At least one queued operation's write policy differs from the task's
30 // Policy parameter: NOTHING executed (asserted in debug), and the queue
31 // is DROPPED -- keeping it would wedge the task forever in release,
32 // rescanning and refusing the same poisoned frame while new enqueues pile
33 // on. Pre-validating keeps runtime aborts unreachable, so serial and pool
34 // execution can never diverge on partially-applied plans.
35 PolicyMismatch,
36};
37
38// Statistics of the most recent run, readable between ticks.
41 AutoExecStatus status = AutoExecStatus::Idle;
42 std::size_t planned_ops = 0;
43 std::size_t rejected_ops = 0;
44 std::size_t executed_chunks = 0;
45 std::size_t merged_dirty_chunks = 0;
46 std::size_t drained = 0;
47 std::size_t phases = 0;
48 std::size_t pool_phases = 0;
49};
50
51// Auto-exec over a dense (AlwaysResident) world. `Policy` must be ReadOnly
52// or UniquePerChunk (the write policies the parallel phase planner
53// supports), and every enqueued operation must carry exactly that policy.
54// `ChunkFn` is the per-chunk kernel `fn(view, Ack&)` from the
55// result-bearing execute wrappers; `Ack` accumulates op-exclusively on the
56// executing thread, but the kernel object itself is SHARED across workers
57// when a pool is attached -- it must be safe for concurrent invocation
58// (stateless, or synchronizing any mutable state itself). When a worker pool is
59// attached, phases with at least `parallel_threshold` operations run on it;
60// smaller phases and everything else stay serial, and results are
61// byte-identical either way because pre-validation makes runtime aborts
62// unreachable.
63//
64// Planning reuses a task-owned ExecutionReport (its rows, planned ops,
65// and chunk lists are recycled between runs), so steady-state ticks plan
66// allocation-free once capacities warm up.
68template <typename World, WritePolicy Policy, typename Ack, typename ChunkFn>
69class AutoExecTask {
70 static_assert(Policy == WritePolicy::ReadOnly ||
71 Policy == WritePolicy::UniquePerChunk,
72 "auto-exec supports the parallel-phase write policies only");
73
74 public:
75 using ResultHook = void (*)(void* ctx, OpHandle handle,
76 const OpCompletion& completion,
77 const Ack* ack) noexcept;
78
79 AutoExecTask(World& world, OperationBatch& ops, ChunkFn fn)
80 : world_(&world), ops_(&ops), fn_(static_cast<ChunkFn&&>(fn)) {}
81
82 void reserve_operations(std::size_t count) {
83 channel_.reserve_operations(count);
84 scratch_.reserve_operations(count);
85 }
86
87 // Attaches the production pool: phases with at least `threshold`
88 // operations run on it. The pool must outlive the task.
89 void use_pool(WorkerPoolPhaseExecutor& pool,
90 std::size_t threshold = 2) noexcept {
91 pool_ = &pool;
92 parallel_threshold_ = threshold == 0 ? 1 : threshold;
93 }
94
95 void set_result_hook(void* ctx, ResultHook hook) noexcept {
96 hook_ctx_ = ctx;
97 hook_ = hook;
98 }
99
100 [[nodiscard]] auto last_run() const noexcept -> const AutoExecRunStats& {
101 return last_run_;
102 }
103
104 [[nodiscard]] auto operator()(const ScheduleTaskContext&)
106 last_run_ = AutoExecRunStats{};
107 if (ops_->empty()) {
108 return ScheduleTaskResult{};
109 }
110#if TESS_HAS_EXCEPTIONS
111 try {
112 return run_nonempty();
113 } catch (...) {
114 // Planning/execution exceptions preserve the caller-owned queue for
115 // inspection or replacement, but transient completion slots must never
116 // leak into a later run. Partial world writes make blind retry unsafe.
117 channel_.clear();
118 throw;
119 }
120#else
121 return run_nonempty();
122#endif
123 }
124
125 private:
126 [[nodiscard]] auto run_nonempty() -> ScheduleTaskResult {
127 // Pre-validate policy uniformity BEFORE planning so a mismatch executes
128 // nothing at all (deterministic under any executor).
129 for (const auto& operation : ops_->operations()) {
130 if (operation.write_policy != Policy) {
131 TESS_ASSERT_MSG(false,
132 "auto-exec queue contains a mismatched write policy");
133 last_run_.status = AutoExecStatus::PolicyMismatch;
134 ops_->clear();
135 channel_.clear();
136 return ScheduleTaskResult{};
137 }
138 }
139
140 const auto& report = plan_operations(*world_, *ops_, plan_report_);
141 (void)record_plan_completions(report, channel_);
142 last_run_.planned_ops = report.planned_count();
143 last_run_.rejected_ops = report.failed_count();
144
145 auto produced_dirty = DirtyMask{};
146 if (!report.plan().empty()) {
147 const auto phases = plan_parallel_execution_phases(report.plan());
148 // Policy uniformity was pre-validated against the planner-supported
149 // set, so phase planning cannot fail.
150 TESS_ASSERT(phases.ok());
151 last_run_.phases = phases.phases().size();
152 for (const auto& phase : phases.phases()) {
153 const auto use_pool =
154 pool_ != nullptr && phase.operation_count() >= parallel_threshold_;
155 auto result = PlannedExecutionResult{};
156#if TESS_HAS_EXCEPTIONS
157 try {
158#endif
159 if (use_pool) {
160 ++last_run_.pool_phases;
161 result = execute_phase_partitioned_dirty_with_results<Policy>(
162 *pool_, *world_, report.plan(), phase, scratch_, channel_,
163 [this](auto view, Ack& ack) noexcept(
164 std::is_nothrow_invocable_v<ChunkFn&, decltype(view)&,
165 Ack&>) { fn_(view, ack); });
166 } else {
167 const SerialPhaseExecutor serial;
168 result = execute_phase_partitioned_dirty_with_results<Policy>(
169 serial, *world_, report.plan(), phase, scratch_, channel_,
170 [this](auto view, Ack& ack) noexcept(
171 std::is_nothrow_invocable_v<ChunkFn&, decltype(view)&,
172 Ack&>) { fn_(view, ack); });
173 }
174#if TESS_HAS_EXCEPTIONS
175 } catch (...) {
176 // Dirty records are written before each callback. Both concurrent
177 // executors join before rethrowing, and this allocation-free merge
178 // is noexcept, so every started callback is conservatively visible
179 // without replacing the original kernel exception.
180 const auto merged =
181 detail::merge_planned_dirty_after_exception(*world_, scratch_);
182 TESS_ASSERT(merged.status == PlannedDirtyMergeStatus::Merged);
183 last_run_.merged_dirty_chunks += merged.merged_chunk_count;
184 throw;
185 }
186#endif
187 TESS_ASSERT(result.status == PlannedExecutionStatus::Executed);
188 last_run_.executed_chunks += result.chunk_count;
189 // Merge after EACH phase: the partitioned scratch is re-prepared
190 // per phase, so a single post-loop merge would drop every phase's
191 // dirty records but the last.
192 auto merged = PlannedDirtyMergeResult{};
193#if TESS_HAS_EXCEPTIONS
194 try {
195#endif
196 merged = merge_planned_dirty(*world_, scratch_);
197#if TESS_HAS_EXCEPTIONS
198 } catch (...) {
199 // Normal coalescing reserves before consuming partitions. If that
200 // reserve fails, the no-allocation cold path can still publish every
201 // started callback's dirty metadata before preserving the exception.
202 const auto fallback =
203 detail::merge_planned_dirty_after_exception(*world_, scratch_);
204 TESS_ASSERT(fallback.status == PlannedDirtyMergeStatus::Merged);
205 last_run_.merged_dirty_chunks += fallback.merged_chunk_count;
206 throw;
207 }
208#endif
209 if (merged.status == PlannedDirtyMergeStatus::CapacityExceeded) {
210 // Capacity validation leaves every partition intact. The cold
211 // allocation-free merge therefore preserves completed writes in
212 // exception-free builds just as it does while unwinding above.
213 merged =
214 detail::merge_planned_dirty_after_exception(*world_, scratch_);
215 if (!merged.ok()) {
216 detail::fail_fast("AutoExec dirty fallback validation failed");
217 }
218 }
219 TESS_ASSERT(merged.status == PlannedDirtyMergeStatus::Merged);
220 last_run_.merged_dirty_chunks += merged.merged_chunk_count;
221 }
222 for (const auto& operation : report.plan().operations()) {
223 produced_dirty |= operation.field_access.dirty_mask;
224 }
225 last_run_.status = AutoExecStatus::Executed;
226 } else {
227 last_run_.status = report.operations().empty() ? AutoExecStatus::Idle
228 : AutoExecStatus::Executed;
229 }
230
231 // Clear the queue BEFORE draining: the plan already copied everything
232 // execution needed, and a result hook may enqueue follow-up work -- it
233 // lands in the fresh queue for the next run instead of being discarded.
234 ops_->clear();
235 if (hook_ != nullptr) {
236 last_run_.drained += channel_.drain_results(
237 [this](OpHandle handle, const OpCompletion& completion,
238 const Ack* ack) noexcept {
239 hook_(hook_ctx_, handle, completion, ack);
240 });
241 }
242 channel_.clear();
243 return ScheduleTaskResult{produced_dirty, 0, false};
244 }
245
246 World* world_;
247 OperationBatch* ops_;
248 ChunkFn fn_;
249 WorkerPoolPhaseExecutor* pool_ = nullptr;
250 std::size_t parallel_threshold_ = 2;
251 ResultChannel<Ack> channel_;
253 ResultHook hook_ = nullptr;
254 void* hook_ctx_ = nullptr;
255 AutoExecRunStats last_run_{};
256 // Reused across runs; see the planning note above.
257 ExecutionReport plan_report_;
258};
259
260} // namespace tess
Definition queued.h:1521
Definition queued.h:1668
Definition queued.h:1415
Dense per-operation completion and payload channel.
Definition result_channel.h:92
Definition world.h:22
Counts planning, execution, dirty merging, draining, and phase dispatch.
Definition auto_exec.h:40
Definition metadata_types.h:12
Completion metadata spanning planning and execution failure domains.
Definition result_channel.h:50
Definition queued.h:76
Definition queued.h:949
Definition phase_executor.h:65
Supplies a task with the current tick, trigger bits, and work allowance.
Definition schedule.h:122
Returns produced dirty bits and bounded background progress to the schedule.
Definition schedule.h:136
Definition phase_executor.h:97