mod diagnostics;
#[cfg(feature = "server")]
mod http;
#[cfg(feature = "live")]
mod watch;
use std::collections::{HashMap, HashSet};
use std::sync::mpsc::channel;
use std::time::{Duration, Instant};
use indicatif::ProgressStyle;
use petgraph::graph::NodeIndex;
use tracing::Level;
use tracing_indicatif::span_ext::IndicatifSpanExt;
use petgraph::Graph;
use camino::Utf8PathBuf;
use crate::core::{Dynamic, Store};
use crate::engine::{Map, Task, TrackerState};
use crate::snapshot::Snapshot;
use crate::{Environment, ImportMap, Output, TaskContext, Website};
#[cfg(feature = "live")]
pub(crate) use watch::watch;
pub use diagnostics::Diagnostics;
#[derive(Debug, Clone)]
pub struct TaskExecution {
pub start: Instant,
pub duration: Duration,
}
#[derive(Clone, Debug)]
pub(crate) struct NodeData {
pub output: Dynamic,
pub tracking: Vec<Option<TrackerState>>,
pub importmap: ImportMap,
pub store_paths: Vec<Utf8PathBuf>,
}
pub(crate) fn run_once_parallel<G: Send + Sync>(
website: &mut Website<G>,
globals: &Environment<G>,
) -> anyhow::Result<(HashMap<NodeIndex, NodeData>, Snapshot, Diagnostics)> {
petgraph::algo::toposort(&website.graph, None)
.map_err(|_| anyhow::anyhow!("cycle detected in task graph"))?;
let mut cache = HashMap::new();
let pending = website.graph.node_indices().collect();
let dirty = HashSet::new();
let diagnostics = run_tasks_parallel(website, globals, &mut cache, &pending, &dirty)?;
let manifest = collect_manifest(&cache, &website.graph);
Ok((cache, manifest, diagnostics))
}
pub(crate) fn run_tasks_parallel<G: Send + Sync>(
site: &Website<G>,
globals: &Environment<G>,
cache: &mut HashMap<NodeIndex, NodeData>,
nodes_to_run: &HashSet<NodeIndex>,
explicitly_dirty: &HashSet<NodeIndex>,
) -> anyhow::Result<Diagnostics> {
let mut dependents: HashMap<NodeIndex, Vec<NodeIndex>> = HashMap::new();
for edge in site.graph.raw_edges() {
dependents
.entry(edge.source())
.or_default()
.push(edge.target());
}
let mut dependency_counts: HashMap<NodeIndex, usize> = nodes_to_run
.iter()
.map(|&i| {
(
i,
site.graph
.neighbors_directed(i, petgraph::Direction::Incoming)
.filter(|dep| nodes_to_run.contains(dep))
.count(),
)
})
.collect();
let total_tasks = nodes_to_run.len() as u64;
let mut completed_tasks = 0;
if total_tasks == 0 {
return Ok(Diagnostics::default());
}
let root_span = tracing::span!(Level::INFO, "building_tasks");
root_span.pb_set_length(total_tasks);
#[allow(clippy::unwrap_used)] let pb_style_root = ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}")
.unwrap()
.progress_chars("=>-");
root_span.pb_set_style(&pb_style_root);
root_span.pb_set_message("Building tasks...");
let _enter = root_span.enter();
let mut execution_times = HashMap::new();
let mut updated_nodes = HashSet::new();
let pb_style = crate::utils::get_style_task()?;
rayon::scope(|s| -> anyhow::Result<()> {
let (result_sender, result_receiver) =
channel::<(NodeIndex, anyhow::Result<NodeData>, Instant, Duration, bool)>();
let spawn_task = |cache: &HashMap<NodeIndex, NodeData>,
index: NodeIndex,
updated_nodes: &HashSet<NodeIndex>| {
let mut dependencies = Vec::new();
let mut importmap = ImportMap::new();
for dep_index in site.graph[index].dependencies() {
#[allow(clippy::unwrap_used)] let node_data = cache.get(&dep_index).unwrap();
dependencies.push(node_data.output.clone());
importmap.merge(node_data.importmap.clone());
}
let is_explicitly_dirty = explicitly_dirty.contains(&index);
let mut should_run = true;
let mut old_data = None;
if !is_explicitly_dirty && let Some(data) = cache.get(&index) {
old_data = Some(data.clone());
let task = &site.graph[index];
if task.is_valid(&data.tracking, &dependencies, updated_nodes) {
should_run = false;
}
}
if !should_run {
let sender = result_sender.clone();
#[allow(clippy::unwrap_used)] let output = old_data.unwrap();
#[allow(clippy::unwrap_used)] sender
.send((index, Ok(output), Instant::now(), Duration::ZERO, false))
.unwrap();
return;
}
let task = site.graph[index].clone();
let sender = result_sender.clone();
let pb_style = pb_style.clone();
let old_output = old_data.map(|d| d.output);
let updated_nodes = updated_nodes.clone();
s.spawn(move |_| {
let span = tracing::span!(Level::INFO, "task", name = task.name());
span.pb_set_style(&pb_style);
span.pb_set_message(&format!("Running {}", task.name()));
let _enter = span.enter();
let context = TaskContext {
env: globals,
importmap: &importmap,
span: span.clone(),
};
let start_time = Instant::now();
let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut rt = Store::new();
match task {
Task::C(task) => task.execute(&context, &mut rt, &dependencies).map(
|(tracking, output)| {
let tracking = tracking.unwrap();
let mut imports = importmap.clone();
imports.merge(rt.imports);
NodeData {
output,
tracking,
importmap: imports,
store_paths: rt.store_paths,
}
},
),
Task::F(task) => task
.execute(
&context,
&mut rt,
&dependencies,
old_output.as_ref(),
&updated_nodes,
)
.map(|(tracking, output)| {
let tracking = tracking.unwrap();
let mut imports = importmap.clone();
imports.merge(rt.imports);
NodeData {
output,
tracking,
importmap: imports,
store_paths: rt.store_paths,
}
}),
}
})) {
Ok(result) => result,
Err(panic) => {
let msg = if let Some(s) = panic.downcast_ref::<&str>() {
format!("Task panicked: {s}")
} else if let Some(s) = panic.downcast_ref::<String>() {
format!("Task panicked: {s}")
} else {
String::from("Task panicked with unknown payload")
};
Err(anyhow::anyhow!(msg))
}
};
let elapsed = start_time.elapsed();
let _ = sender.send((index, output, start_time, elapsed, true));
});
};
for &node_index in nodes_to_run {
if dependency_counts.get(&node_index).cloned().unwrap_or(0) == 0 {
spawn_task(cache, node_index, &updated_nodes);
}
}
while completed_tasks < total_tasks {
#[allow(clippy::unwrap_used)] let (completed_index, output, start, duration, executed) =
result_receiver.recv().unwrap();
cache.insert(completed_index, output?);
execution_times.insert(completed_index, TaskExecution { start, duration });
completed_tasks += 1;
root_span.pb_inc(1);
if executed {
updated_nodes.insert(completed_index);
}
if let Some(dependents_of_completed) = dependents.get(&completed_index) {
for &index in dependents_of_completed {
if let Some(count) = dependency_counts.get_mut(&index) {
*count -= 1;
if *count == 0 {
spawn_task(cache, index, &updated_nodes);
}
}
}
}
}
Ok(())
})?;
tracing::info!("Build complete!");
Ok(Diagnostics { execution_times })
}
pub(crate) fn collect_manifest<G: Send + Sync>(
cache: &HashMap<NodeIndex, NodeData>,
graph: &Graph<Task<G>, ()>,
) -> Snapshot {
let mut manifest = Snapshot::new();
for (index, node_data) in cache {
let task_name = graph[*index].name();
let value = &node_data.output;
if let Some(page) = value.downcast_ref::<Output>() {
manifest.insert_page(*index, &task_name, page.clone());
} else if let Some(page_vec) = value.downcast_ref::<Vec<Output>>() {
for page in page_vec {
manifest.insert_page(*index, &task_name, page.clone());
}
} else if let Some(page_map) = value.downcast_ref::<Map<Output>>() {
for (item, _) in page_map.map.values() {
manifest.insert_page(*index, &task_name, item.clone());
}
}
for path in &node_data.store_paths {
manifest.insert_hash_asset(*index, &task_name, path.clone());
}
}
manifest
}