feat(vf): add JobStats and JobNode
This commit is contained in:
1 parent
d4d3ef7314
commit
a082c1d181
1 file changed
+178
@@ -0,0 +1,178 @@
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace vf {
|
||||
|
||||
// Three service classes. High is drained before Normal, Normal before Low, so a
|
||||
// chunk right under the observer never waits behind the far edge of the view.
|
||||
enum class JobPriority : std::uint8_t {
|
||||
High = 0,
|
||||
Normal = 1,
|
||||
Low = 2,
|
||||
};
|
||||
|
||||
constexpr int jobPriorityCount = 3;
|
||||
|
||||
struct JobStats {
|
||||
std::uint64_t submitted = 0;
|
||||
std::uint64_t executed = 0;
|
||||
std::uint64_t failed = 0;
|
||||
std::uint64_t queuePops = 0;
|
||||
std::uint32_t peakBusyWorkers = 0;
|
||||
std::uint32_t peakQueueDepth = 0;
|
||||
};
|
||||
|
||||
// Completion token for one submitted job. Copyable and safe to wait on from any
|
||||
// thread; an exception thrown by the job body is captured here instead of
|
||||
// escaping into the worker thread.
|
||||
class JobHandle {
|
||||
public:
|
||||
JobHandle() = default;
|
||||
|
||||
bool valid() const { return static_cast<bool>(state); }
|
||||
bool done() const;
|
||||
bool failed() const;
|
||||
void wait() const;
|
||||
std::exception_ptr error() const;
|
||||
std::string errorMessage() const;
|
||||
void rethrowError() const;
|
||||
|
||||
private:
|
||||
friend class JobSystem;
|
||||
|
||||
struct State;
|
||||
|
||||
explicit JobHandle(std::shared_ptr<State> shared) : state(std::move(shared)) {}
|
||||
|
||||
std::shared_ptr<State> state;
|
||||
};
|
||||
|
||||
// A group of handles waited on as a unit. A batch returned for a graph that
|
||||
// could not be scheduled reports valid() == false and holds no handles.
|
||||
class JobBatch {
|
||||
public:
|
||||
JobBatch() = default;
|
||||
|
||||
bool valid() const { return runnable; }
|
||||
std::size_t size() const { return handles.size(); }
|
||||
bool empty() const { return handles.empty(); }
|
||||
const JobHandle& at(std::size_t index) const { return handles[index]; }
|
||||
|
||||
void add(const JobHandle& handle) { handles.push_back(handle); }
|
||||
void wait() const;
|
||||
bool done() const;
|
||||
std::size_t failedCount() const;
|
||||
std::exception_ptr firstError() const;
|
||||
|
||||
private:
|
||||
friend class JobSystem;
|
||||
|
||||
std::vector<JobHandle> handles;
|
||||
bool runnable = true;
|
||||
};
|
||||
|
||||
struct JobNode {
|
||||
std::string name;
|
||||
std::function<void()> work;
|
||||
JobPriority priority = JobPriority::Normal;
|
||||
std::vector<std::size_t> dependencies;
|
||||
};
|
||||
|
||||
// Dependency graph of jobs. A node becomes runnable only once every node it
|
||||
// depends on has finished. The graph is inert on its own: JobSystem::runGraph
|
||||
// turns it into scheduled work.
|
||||
class JobGraph {
|
||||
public:
|
||||
std::size_t addNode(std::string name, std::function<void()> work,
|
||||
JobPriority priority = JobPriority::Normal);
|
||||
void addDependency(std::size_t node, std::size_t dependsOn);
|
||||
|
||||
std::size_t nodeCount() const { return nodes.size(); }
|
||||
const JobNode& nodeAt(std::size_t index) const { return nodes[index]; }
|
||||
|
||||
// Kahn sweep in ascending node order, so the order is a function of the
|
||||
// graph alone. Returns fewer entries than nodeCount() when a cycle exists.
|
||||
std::vector<std::size_t> topologicalOrder() const;
|
||||
bool hasCycle() const;
|
||||
|
||||
void clear() { nodes.clear(); }
|
||||
|
||||
private:
|
||||
std::vector<JobNode> nodes;
|
||||
};
|
||||
|
||||
// Thread pool with priority queues and dependency-aware graph execution.
|
||||
// A thread count of zero means serial mode: every job runs inline on the
|
||||
// calling thread at submit time, which is the reference behaviour the parallel
|
||||
// modes are checked against.
|
||||
class JobSystem {
|
||||
public:
|
||||
explicit JobSystem(unsigned threadCount = 0);
|
||||
~JobSystem();
|
||||
|
||||
JobSystem(const JobSystem&) = delete;
|
||||
JobSystem& operator=(const JobSystem&) = delete;
|
||||
|
||||
unsigned threadCount() const { return static_cast<unsigned>(workers.size()); }
|
||||
bool serialMode() const { return workers.empty(); }
|
||||
|
||||
JobHandle submit(std::function<void()> work, JobPriority priority = JobPriority::Normal);
|
||||
JobBatch submitBatch(std::vector<std::function<void()>> work,
|
||||
JobPriority priority = JobPriority::Normal);
|
||||
|
||||
// Schedules every node of the graph, respecting dependencies. A cyclic
|
||||
// graph is refused: the returned batch is invalid and nothing runs. When a
|
||||
// node throws, its dependents are not executed and complete with an error
|
||||
// of their own, so waiting on the batch can never hang.
|
||||
JobBatch runGraph(const JobGraph& graph);
|
||||
|
||||
void waitAll();
|
||||
|
||||
JobStats stats() const;
|
||||
void resetStats();
|
||||
|
||||
private:
|
||||
struct QueuedJob {
|
||||
std::function<void()> work;
|
||||
std::shared_ptr<JobHandle::State> state;
|
||||
};
|
||||
|
||||
struct GraphRun;
|
||||
|
||||
void workerLoop();
|
||||
void enqueue(std::function<void()> work, std::shared_ptr<JobHandle::State> state,
|
||||
JobPriority priority);
|
||||
void runInline(std::function<void()> work, const std::shared_ptr<JobHandle::State>& state);
|
||||
void scheduleGraphNode(const std::shared_ptr<GraphRun>& run, std::size_t index);
|
||||
bool executeGraphNode(const std::shared_ptr<GraphRun>& run, std::size_t index);
|
||||
void noteInlineStart();
|
||||
void noteInlineFinish(bool failed);
|
||||
void noteFailure();
|
||||
std::size_t queuedLocked() const;
|
||||
QueuedJob popLocked();
|
||||
|
||||
static void completeState(const std::shared_ptr<JobHandle::State>& state,
|
||||
std::exception_ptr error);
|
||||
|
||||
mutable std::mutex mutex;
|
||||
std::condition_variable workCv;
|
||||
std::condition_variable idleCv;
|
||||
std::vector<std::thread> workers;
|
||||
std::deque<QueuedJob> queues[jobPriorityCount];
|
||||
JobStats counters;
|
||||
unsigned busy = 0;
|
||||
bool stopping = false;
|
||||
};
|
||||
|
||||
} // namespace vf
|
||||
Reference in new issue
Block a user