use std::panic::{self, AssertUnwindSafe};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, PoisonError};
use pragma_jobs::{Priority, ThreadPool};
use pragma_sync::__private::ArrivalCounter;
use crate::GraphError;
use crate::context::{TaskContext, TaskResult};
use crate::plan::{self, Access, Dependency, TaskSpec};
use crate::resources::{ResourceKey, Resources};
type System = Box<dyn Fn(&TaskContext<'_>) -> Result<(), GraphError> + Send + Sync>;
#[derive(Debug, Default)]
#[must_use]
pub struct TaskConfig {
access: Vec<Access>,
deps: Vec<Dependency>,
}
impl TaskConfig {
pub fn reads<T: Send + Sync + 'static>(self) -> Self {
self.declare(ResourceKey::of::<T>(), false)
}
pub fn writes<T: Send + Sync + 'static>(self) -> Self {
self.declare(ResourceKey::of::<T>(), true)
}
pub fn before(mut self, task: impl Into<String>) -> Self {
self.deps.push(Dependency::Before(task.into()));
self
}
pub fn after(mut self, task: impl Into<String>) -> Self {
self.deps.push(Dependency::After(task.into()));
self
}
fn declare(mut self, key: ResourceKey, write: bool) -> Self {
match self.access.iter_mut().find(|a| a.key.id == key.id) {
Some(existing) => existing.write |= write,
None => self.access.push(Access { key, write }),
}
self
}
}
struct TaskDecl {
name: String,
config: TaskConfig,
system: System,
}
#[derive(Default)]
#[must_use]
pub struct FrameGraphBuilder {
tasks: Vec<TaskDecl>,
ordered: bool,
}
impl std::fmt::Debug for FrameGraphBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FrameGraphBuilder")
.field(
"tasks",
&self.tasks.iter().map(|t| &t.name).collect::<Vec<_>>(),
)
.field("ordered", &self.ordered)
.finish()
}
}
impl FrameGraphBuilder {
pub fn ordered(mut self) -> Self {
self.ordered = true;
self
}
pub fn task<C, F, R>(&mut self, name: impl Into<String>, configure: C, system: F) -> &mut Self
where
C: FnOnce(TaskConfig) -> TaskConfig,
F: Fn(&TaskContext<'_>) -> R + Send + Sync + 'static,
R: TaskResult,
{
let name = name.into();
let config = configure(TaskConfig::default());
let system: System = Box::new(move |ctx| system(ctx).into_result(ctx.name()));
self.tasks.push(TaskDecl {
name,
config,
system,
});
self
}
pub fn build(self) -> Result<FrameGraph, GraphError> {
if self.tasks.len() > plan::MAX_TASKS {
return Err(GraphError::TooLarge {
tasks: self.tasks.len(),
reason: plan::TOO_MANY_TASKS,
});
}
let specs: Vec<TaskSpec<'_>> = self
.tasks
.iter()
.map(|t| TaskSpec {
name: &t.name,
access: &t.config.access,
deps: &t.config.deps,
})
.collect();
let plan = plan::plan(&specs, self.ordered)?;
drop(specs);
let mut indegree = vec![0_usize; self.tasks.len()];
for &v in plan.successors.iter().flatten() {
if let Some(d) = indegree.get_mut(v) {
*d = d.saturating_add(1);
}
}
let mut required: Vec<(ResourceKey, usize)> = Vec::new();
for (i, task) in self.tasks.iter().enumerate() {
for access in &task.config.access {
if !required.iter().any(|(k, _)| k.id == access.key.id) {
required.push((access.key, i));
}
}
}
let nodes: Vec<Node> = self
.tasks
.into_iter()
.zip(plan.successors)
.zip(&indegree)
.map(|((task, successors), &indegree)| Node {
ready: successors.iter().map(|_| AtomicUsize::new(0)).collect(),
name: task.name,
access: task.config.access,
system: task.system,
successors,
indegree,
})
.collect();
let roots = nodes
.iter()
.enumerate()
.filter(|(_, n)| n.indegree == 0)
.map(|(i, _)| AtomicUsize::new(i))
.collect();
let remaining = nodes.iter().map(|_| ArrivalCounter::new(0)).collect();
Ok(FrameGraph {
nodes,
order: plan.order,
roots,
remaining,
required,
})
}
}
pub(crate) struct Node {
pub(crate) name: String,
pub(crate) access: Vec<Access>,
system: System,
pub(crate) successors: Vec<usize>,
ready: Box<[AtomicUsize]>,
indegree: usize,
}
pub struct FrameGraph {
pub(crate) nodes: Vec<Node>,
order: Vec<usize>,
roots: Box<[AtomicUsize]>,
remaining: Box<[ArrivalCounter]>,
required: Vec<(ResourceKey, usize)>,
}
impl std::fmt::Debug for FrameGraph {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FrameGraph")
.field("order", &self.order().collect::<Vec<_>>())
.field("edges", &self.edges().count())
.finish()
}
}
impl FrameGraph {
pub fn builder() -> FrameGraphBuilder {
FrameGraphBuilder::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.nodes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn order(&self) -> impl Iterator<Item = &str> {
self.order
.iter()
.filter_map(|&i| self.nodes.get(i).map(|n| n.name.as_str()))
}
pub fn edges(&self) -> impl Iterator<Item = (&str, &str)> {
self.order
.iter()
.filter_map(|&i| self.nodes.get(i))
.flat_map(move |node| {
node.successors.iter().filter_map(move |&s| {
self.nodes
.get(s)
.map(|succ| (node.name.as_str(), succ.name.as_str()))
})
})
}
pub fn run(&mut self, pool: &ThreadPool, resources: &mut Resources) -> Result<(), GraphError> {
let _span = tracing::debug_span!("frame_graph", tasks = self.nodes.len()).entered();
#[cfg(feature = "puffin")]
puffin::profile_scope!("frame_graph");
for &(key, task) in &self.required {
if !resources.contains_key(key) {
return Err(GraphError::MissingResource {
task: self
.nodes
.get(task)
.map(|n| n.name.clone())
.unwrap_or_default(),
resource: key.name.to_owned(),
});
}
}
for (counter, node) in self.remaining.iter_mut().zip(&self.nodes) {
counter.reset(node.indegree);
}
let first_error = Mutex::new(None);
let run = Run {
nodes: &self.nodes,
remaining: &self.remaining,
pool,
resources,
first_error: &first_error,
};
let roots = &self.roots;
if let Err(e) = pool.scope(|_| run.fork(roots)) {
run.record(usize::MAX, e.into());
}
match first_error
.into_inner()
.unwrap_or_else(PoisonError::into_inner)
{
Some((_, error)) => Err(error),
None => Ok(()),
}
}
}
struct Run<'a> {
nodes: &'a [Node],
remaining: &'a [ArrivalCounter],
pool: &'a ThreadPool,
resources: &'a Resources,
first_error: &'a Mutex<Option<(usize, GraphError)>>,
}
impl Run<'_> {
fn record(&self, index: usize, error: GraphError) {
let mut slot = self
.first_error
.lock()
.unwrap_or_else(PoisonError::into_inner);
if slot.as_ref().is_none_or(|(prev, _)| index < *prev) {
*slot = Some((index, error));
}
}
fn fork(&self, ready: &[AtomicUsize]) {
match ready {
[] => {}
[one] => self.run_from(one.load(Ordering::Relaxed)),
_ => {
let (left, right) = ready.split_at(ready.len() / 2);
if let Err(e) = self.pool.join(|| self.fork(left), || self.fork(right)) {
self.record(usize::MAX, e.into());
}
}
}
}
fn run_from(&self, mut current: usize) {
loop {
let Some(node) = self.nodes.get(current) else {
return;
};
if !self.execute(current, node) {
return;
}
let mut ready = 0_usize;
for &succ in &node.successors {
let Some(counter) = self.remaining.get(succ) else {
continue;
};
if counter.arrive()
&& let Some(slot) = node.ready.get(ready)
{
slot.store(succ, Ordering::Relaxed);
ready = ready.saturating_add(1);
}
}
match node.ready.get(..ready) {
Some([one]) => current = one.load(Ordering::Relaxed),
Some(many) if !many.is_empty() => return self.fork(many),
_ => return,
}
}
}
fn execute(&self, index: usize, node: &Node) -> bool {
let ctx = TaskContext {
name: &node.name,
access: &node.access,
pool: self.pool,
resources: self.resources,
};
let _span = tracing::debug_span!(
"graph_task",
task = node.name.as_str(),
priority = pragma_jobs::current_priority().map(Priority::as_str),
worker = self.pool.current_worker_index(),
)
.entered();
#[cfg(feature = "puffin")]
puffin::profile_scope!("graph_task", node.name.as_str());
let system = || pragma_jobs::labeled("graph", &node.name, || (node.system)(&ctx));
match panic::catch_unwind(AssertUnwindSafe(system)) {
Ok(Ok(())) => true,
Ok(Err(error)) => {
self.record(index, error);
false
}
Err(payload) => {
self.record(index, GraphError::panicked(&node.name, &*payload));
false
}
}
}
}