7#include <condition_variable>
22enum class JobStatus { Pending, Scheduled,
Running, Done,
Failed };
23enum class JobScope { Heap, Frame };
27thread_local const void *tlsCurrentState =
nullptr;
28thread_local JobImpl *tlsCurrentJob =
nullptr;
30bool isDoneStatus(JobStatus
status) {
31 return status == JobStatus::Done ||
status == JobStatus::Failed;
45 FrameArena() =
default;
46 ~FrameArena() { reset(); }
54 void *alloc(
size_t size,
size_t align) {
55 if (size > kArenaBlockSize)
56 throw eve::Exception(
"JobSystem arena allocation exceeds block size");
58 blocks_.emplace_back(kArenaBlockSize);
59 size_t aligned = (offset_ + align - 1) & ~(align - 1);
60 if (aligned + size > blocks_.back().size()) {
61 blocks_.emplace_back(kArenaBlockSize);
64 void *
ptr = blocks_.back().data() + aligned;
65 offset_ = aligned + size;
74 void track(
void *
ptr,
void (*
destroy)(
void *)) {
80 for (
auto &entry : tracked_)
87 static constexpr size_t kArenaBlockSize = 64 * 1024;
94 std::vector<std::vector<std::byte>> blocks_;
95 std::vector<Tracked> tracked_;
102 mutable std::mutex
mu;
103 std::condition_variable
cv;
114 void runJob(JobImpl *job);
117 void recordError(JobImpl *job,
const std::string &message);
128struct JobImpl final :
public Job {
132 ~JobImpl()
override {
140 void wait()
override;
141 bool isDone()
const override;
142 bool hasFailed()
const override;
143 std::string getError()
const override;
144 int getPendingDependencyCount()
const override;
145 void addDependency(Job *predecessor)
override;
146 void setCompletionCallback(
JobFunc callback)
override;
161class TaskGroupImpl final :
public TaskGroup {
163 TaskGroupImpl(JobSystemThreadPool::State *owner, JobScope scope_)
165 ~TaskGroupImpl()
override =
default;
172 JobImpl *job =
nullptr;
174 std::lock_guard<std::mutex> lock(
state->mu);
178 state->scheduleLocked(job);
181 state->cv.notify_one();
185 void wait()
override {
186 std::vector<JobImpl *> snapshot;
188 std::lock_guard<std::mutex> lock(
state->mu);
191 for (JobImpl *child : snapshot)
193 std::lock_guard<std::mutex> lock(
state->mu);
197 int getPendingCount()
const override {
198 std::lock_guard<std::mutex> lock(
state->mu);
201 if (!isDoneStatus(
child->status))
207 JobSystemThreadPool::State *
state;
218 if (
scope == JobScope::Frame) {
219 void *mem =
arena.alloc(
sizeof(JobImpl),
alignof(JobImpl));
221 arena.track(job, [](
void *
ptr) {
static_cast<JobImpl *
>(
ptr)->~JobImpl(); });
228 job->scheduled =
true;
230 if (job->scope == JobScope::Frame)
232 if (job->depCount == 0)
239 job->enqueued =
true;
240 ready.push_back(job);
245 std::lock_guard<std::mutex> lock(mu);
246 job->status = JobStatus::Running;
252 const void *previousState = tlsCurrentState;
253 if (previousState !=
this)
254 tlsCurrentState =
this;
256 JobImpl *previous = tlsCurrentJob;
264 recordError(job, e.
what());
265 }
catch (
const std::exception &e) {
267 recordError(job, e.
what());
270 recordError(job,
"unknown exception");
273 tlsCurrentJob = previous;
275 completeJob(job,
ok);
276 tlsCurrentState = previousState;
281 std::lock_guard<std::mutex> lock(mu);
282 job->status =
ok ? JobStatus::Done : JobStatus::Failed;
289 releaseDependents(job);
299 std::lock_guard<std::mutex> lock(mu);
301 if (job->scope == JobScope::Frame)
308 if (!job->onComplete)
313 recordError(job, std::string(
"completion callback: ") + e.
what());
314 }
catch (
const std::exception &e) {
315 recordError(job, std::string(
"completion callback: ") + e.
what());
317 recordError(job,
"completion callback: unknown exception");
322 std::lock_guard<std::mutex> lock(mu);
323 if (!job->error.empty())
325 job->error += message;
331 std::lock_guard<std::mutex> lock(mu);
332 for (JobImpl *dep : job->dependents) {
333 if (dep->depCount > 0)
335 if (dep->depCount == 0 && dep->scheduled && !dep->enqueued) {
340 job->dependents.clear();
347 std::unique_lock<std::mutex> lock(mu);
348 if (isDoneStatus(job->status))
350 if (tlsCurrentJob == job)
354 if (isDoneStatus(job->status))
356 if (!ready.empty()) {
357 JobImpl *next = ready.front();
371 std::unique_lock<std::mutex> lock(mu);
372 while (outstandingFrame > 0) {
373 if (!ready.empty()) {
374 JobImpl *next = ready.front();
389 const int target = std::max(1, workerCount * 4);
390 return std::max(1, (count + target - 1) / target);
394 tlsCurrentState =
state;
396 JobImpl *job =
nullptr;
398 std::unique_lock<std::mutex> lock(
state->mu);
402 job =
state->ready.front();
403 state->ready.pop_front();
408 tlsCurrentState =
nullptr;
413void JobImpl::wait() {
414 state->waitJob(
this);
417bool JobImpl::isDone()
const {
418 std::lock_guard<std::mutex> lock(
state->mu);
419 return isDoneStatus(
status);
422bool JobImpl::hasFailed()
const {
423 std::lock_guard<std::mutex> lock(
state->mu);
424 return status == JobStatus::Failed;
427std::string JobImpl::getError()
const {
428 std::lock_guard<std::mutex> lock(
state->mu);
432int JobImpl::getPendingDependencyCount()
const {
433 std::lock_guard<std::mutex> lock(
state->mu);
437void JobImpl::addDependency(Job *predecessor) {
438 auto *pred =
dynamic_cast<JobImpl *
>(predecessor);
439 if (!pred || pred->state !=
state)
440 throw eve::Exception(
"JobSystem::addDependency: job does not belong to this system");
442 throw eve::Exception(
"JobSystem::addDependency: a job cannot depend on itself");
444 std::lock_guard<std::mutex> lock(
state->mu);
446 throw eve::Exception(
"JobSystem::addDependency: job already scheduled");
447 if (isDoneStatus(pred->status))
450 pred->dependents.push_back(
this);
453void JobImpl::setCompletionCallback(
JobFunc callback) {
454 bool alreadyDone =
false;
456 std::lock_guard<std::mutex> lock(
state->mu);
457 if (isDoneStatus(
status)) {
464 if (alreadyDone && callback) {
468 state->recordError(
this, std::string(
"completion callback: ") + e.
what());
469 }
catch (
const std::exception &e) {
470 state->recordError(
this, std::string(
"completion callback: ") + e.what());
472 state->recordError(
this,
"completion callback: unknown exception");
481JobImpl *castJob(JobSystemThreadPool::State *
state, Job *job) {
484 auto *
impl =
dynamic_cast<JobImpl *
>(job);
486 throw eve::Exception(
"JobSystem: job does not belong to this system");
493 if (workerCount <= 0) {
494 workerCount =
static_cast<int>(std::thread::hardware_concurrency());
495 if (workerCount <= 0)
498 state_ = std::make_shared<State>();
499 pool_ = std::make_unique<ThreadPool>(workerCount);
500 state_->workerCount = pool_->getWorkerCount();
501 poolTasks_.reserve(
static_cast<size_t>(state_->workerCount));
503 for (
int i = 0; i < state_->workerCount; ++i) {
505 poolTasks_.push_back(pool_->submit([
state] { State::workerLoop(state.get()); }));
509 std::lock_guard<std::mutex> lock(state_->mu);
510 state_->stopping =
true;
512 state_->cv.notify_all();
515 for (
Task *task : poolTasks_)
524 std::lock_guard<std::mutex> lifecycleLock(lifecycleMu_);
525 const bool fromWorker = tlsCurrentState == state_.get();
527 std::lock_guard<std::mutex> lock(state_->mu);
528 state_->stopping =
true;
530 state_->cv.notify_all();
537 for (
Task *task : poolTasks_)
545 for (
Task *task : poolTasks_)
553 return state_->workerCount;
557 std::lock_guard<std::mutex> lock(state_->mu);
558 return !state_->stopping;
562 std::lock_guard<std::mutex> lock(state_->mu);
563 return static_cast<int>(state_->ready.size());
567 std::lock_guard<std::mutex> lock(state_->mu);
568 return state_->outstanding;
572 JobImpl *job =
nullptr;
574 std::lock_guard<std::mutex> lock(state_->mu);
575 if (state_->stopping)
577 job = state_->createJobLocked(JobScope::Heap, std::move(
body),
JobFunc{});
578 state_->scheduleLocked(job);
580 state_->cv.notify_one();
585 std::lock_guard<std::mutex> lock(state_->mu);
586 return state_->createJobLocked(JobScope::Heap, std::move(
body),
JobFunc{});
590 JobImpl *
impl = castJob(state_.get(), job);
592 std::lock_guard<std::mutex> lock(state_->mu);
593 if (state_->stopping)
596 throw eve::Exception(
"JobSystem::schedule: job already scheduled");
597 state_->scheduleLocked(
impl);
599 state_->cv.notify_one();
603 return parallelForImpl(first, last, std::move(
body), chunk,
false);
607 return new TaskGroupImpl(state_.get(), JobScope::Heap);
611 JobImpl *job =
nullptr;
613 std::lock_guard<std::mutex> lock(state_->mu);
614 if (state_->stopping)
616 job = state_->createJobLocked(JobScope::Frame, std::move(
body),
JobFunc{});
617 state_->scheduleLocked(job);
619 state_->cv.notify_one();
624 std::lock_guard<std::mutex> lock(state_->mu);
625 return state_->createJobLocked(JobScope::Frame, std::move(
body),
JobFunc{});
629 return parallelForImpl(first, last, std::move(
body), chunk,
true);
633 std::lock_guard<std::mutex> lock(state_->mu);
634 void *mem = state_->arena.alloc(
sizeof(TaskGroupImpl),
alignof(TaskGroupImpl));
635 auto *
group =
new (mem) TaskGroupImpl(state_.get(), JobScope::Frame);
636 state_->arena.track(
group, [](
void *
ptr) {
static_cast<TaskGroupImpl *
>(
ptr)->~TaskGroupImpl(); });
641 state_->waitFrameJobs();
645 state_->waitFrameJobs();
649 if (tlsCurrentState == state_.get())
650 throw eve::Exception(
"JobSystem::waitAll cannot be called from its worker");
652 std::unique_lock<std::mutex> lock(
state->mu);
653 while (
state->outstanding > 0) {
654 if (!
state->ready.empty()) {
655 JobImpl *next =
state->ready.front();
656 state->ready.pop_front();
664 state->cv.wait(lock);
669 std::lock_guard<std::mutex> lifecycleLock(lifecycleMu_);
670 if (tlsCurrentState == state_.get())
671 throw eve::Exception(
"JobSystem::stop cannot be called from its worker");
673 std::lock_guard<std::mutex> lock(state_->mu);
674 state_->stopping =
true;
676 state_->cv.notify_all();
679 for (
Task *task : poolTasks_)
691 const JobScope
scope = frameScope ? JobScope::Frame : JobScope::Heap;
692 const int count = last - first;
694 chunk = state_->autoChunk(count);
695 const int taskCount = (count + chunk - 1) / chunk;
697 std::lock_guard<std::mutex> lock(state_->mu);
698 if (state_->stopping)
702 join->depCount = taskCount;
703 if (
scope == JobScope::Heap)
704 join->ownedChildren.reserve(
static_cast<size_t>(taskCount));
707 children.reserve(
static_cast<size_t>(taskCount));
708 for (
int t = 0; t < taskCount; ++t) {
709 const int begin = first + t * chunk;
710 const int end = std::min(last, begin + chunk);
713 child->dependents.push_back(join);
715 if (
scope == JobScope::Heap)
716 join->ownedChildren.push_back(child);
721 state_->scheduleLocked(join);
723 state_->scheduleLocked(
child);
724 state_->cv.notify_all();
std::vector< JobImpl * > dependents
std::vector< JobImpl * > ownedChildren
JobSystemThreadPool::State * state
SettlementPipeline::Stage fn
virtual const char * what() const
Returns a string containing reason for the exception.
Job * createJob(JobFunc body) override
Create a paused job (dependencies can be wired, then schedule()).
void endFrame() override
End the frame: join outstanding frame jobs and recycle the arena.
Job * submitFrame(JobFunc body) override
Create a frame-scoped job and schedule it immediately.
Job * parallelFor(int first, int last, ParallelForBody body, int chunk=1) override
Run body over [first, last) in parallel.
Job * parallelForFrame(int first, int last, ParallelForBody body, int chunk=1) override
Frame-scoped parallel_for; see parallelFor().
TaskGroup * createTaskGroup() override
Create a fork/join task group.
int getPendingCount() const override
Number of ready jobs waiting for a worker (approximate).
JobSystemThreadPool(int workerCount)
void waitAll() override
Block until every scheduled job has finished.
int getOutstandingCount() const override
Number of scheduled jobs that have not finished (approximate).
int getWorkerCount() const override
Number of scheduler worker threads.
Job * submit(JobFunc body) override
Create a job and schedule it immediately.
void stop() override
Stop accepting work and join workers. Idempotent.
void schedule(Job *job) override
Kick a paused job so it can run.
TaskGroup * createFrameTaskGroup() override
Create a fork/join task group whose children are frame-scoped.
bool isRunning() const override
Whether the system is still accepting new jobs.
~JobSystemThreadPool() override
Job * createFrameJob(JobFunc body) override
Create a paused frame-scoped job.
void beginFrame() override
Start a new frame: join leftover frame jobs and recycle the arena.
Handle to a single job inside a JobSystem.
Fork/join group: fork() spawns scheduled children, wait() joins them.
A job executed by a ThreadPool worker. Status strings (no enums): "pending" | "running" | "done" | "f...
std::function< void(int first, int last)> ParallelForBody
Body of a parallel_for subrange.
std::function< void()> JobFunc
A unit of work executed by a JobSystem worker.
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
JobImpl * createJobLocked(JobScope scope, JobFunc body, JobFunc onComplete)
void enqueueLocked(JobImpl *job)
void scheduleLocked(JobImpl *job)
void waitJob(JobImpl *job)
void recordError(JobImpl *job, const std::string &message)
static void workerLoop(State *state)
std::condition_variable cv
void releaseDependents(JobImpl *job)
void runJob(JobImpl *job)
int autoChunk(int count) const
std::deque< JobImpl * > ready
void completeJob(JobImpl *job, bool ok)
void fireCompletion(JobImpl *job)