Skip to main content

artifactize_core/monitor/model/
graph.rs

1//! A Run as a graph of Artifacts: one node per Artifact of the tree, with the Artifacts it is
2//! made of, one edge per Artifact an Artifact's evals depend on, each with one of six lights
3//! taken from the tree's own states.
4use std::collections::{BTreeMap, BTreeSet};
5
6use super::{
7    NodeId, Target,
8    states::{Activity, Busy, Completion, EvalView, NotRun},
9    tree::{Kind, Node},
10};
11use crate::types::ArtifactName;
12
13/// How a graph node or edge is lit. Declared in precedence order: an Artifact takes the first
14/// light any of its evals has (RED before ERROR before in progress before blocked before
15/// waiting before GREEN).
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub enum Light {
18    /// An eval's verdict is RED.
19    Red,
20    /// An eval failed operationally: ERROR, not a verdict.
21    Error,
22    /// Running, or waiting for its Human.
23    Active,
24    /// Cannot proceed: an upstream Artifact is RED, ERROR or itself blocked.
25    Blocked,
26    /// Runnable but still waiting: queued, for upstream Artifacts that are not done yet, or
27    /// not run in a finished Run.
28    Waiting,
29    /// Every eval is GREEN (an Artifact without evals is complete too).
30    Green,
31}
32
33impl From<Completion> for Light {
34    /// An upstream Artifact's light where the graph has not lit it (a cycle peer lit later).
35    fn from(completion: Completion) -> Self {
36        match completion {
37            Completion::Complete => Self::Green,
38            Completion::Failed { verdict: true } => Self::Red,
39            Completion::Failed { verdict: false } => Self::Error,
40            Completion::InProgress(Busy::Running | Busy::Human) => Self::Active,
41            Completion::InProgress(Busy::Queued) => Self::Waiting,
42            Completion::Waiting { blocked: true } => Self::Blocked,
43            Completion::Waiting { blocked: false } => Self::Waiting,
44        }
45    }
46}
47
48impl From<&EvalView> for Light {
49    fn from(view: &EvalView) -> Self {
50        match view {
51            EvalView::Done(_) => Self::Green,
52            EvalView::Failed { verdict: true } => Self::Red,
53            EvalView::Failed { verdict: false } => Self::Error,
54            EvalView::InProgress(Activity::Running | Activity::Human(_)) => Self::Active,
55            EvalView::BlockedBy(_) => Self::Blocked,
56            EvalView::InProgress(Activity::Queued(_))
57            | EvalView::WaitingOn(_)
58            | EvalView::NotRun(
59                NotRun::Dependency(_)
60                | NotRun::Budget
61                | NotRun::Stale
62                | NotRun::Unreviewed
63                | NotRun::Absent,
64            ) => Self::Waiting,
65        }
66    }
67}
68
69/// Gate failures come from the Eval-level schedule, never an Artifact-wide inference.
70fn eval_light(eval: &Node, view: &EvalView) -> Light {
71    if eval.held
72        && matches!(
73            view,
74            EvalView::WaitingOn(_) | EvalView::NotRun(NotRun::Dependency(_))
75        )
76    {
77        Light::Blocked
78    } else {
79        Light::from(view)
80    }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct GraphNode {
85    /// The Artifact row's tree id, `a:<artifact>`.
86    pub id: NodeId,
87    pub name: ArtifactName,
88    pub light: Light,
89    /// The Artifacts of the graph it is made of directly, by name.
90    pub parts: Vec<ArtifactName>,
91}
92
93/// `to`'s evals depend on `from`.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct GraphEdge {
96    pub from: ArtifactName,
97    pub to: ArtifactName,
98    /// The input Artifact's current light.
99    pub light: Light,
100}
101
102#[derive(Debug, Clone, Default, PartialEq, Eq)]
103pub struct Graph {
104    /// In the tree's order: upstream first where the graph allows.
105    pub nodes: Vec<GraphNode>,
106    /// By `to` in node order, then `from` in node order.
107    pub edges: Vec<GraphEdge>,
108}
109
110impl Graph {
111    /// Whether `other` has the same Artifacts, parts and edges in the same order, whatever their
112    /// lights: a view can keep where it placed them and only relight them.
113    pub fn same_shape(&self, other: &Self) -> bool {
114        self.nodes.len() == other.nodes.len()
115            && self.edges.len() == other.edges.len()
116            && self
117                .nodes
118                .iter()
119                .zip(&other.nodes)
120                .all(|(one, other)| one.id == other.id && one.parts == other.parts)
121            && self
122                .edges
123                .iter()
124                .zip(&other.edges)
125                .all(|(one, other)| one.from == other.from && one.to == other.to)
126    }
127}
128
129/// The graph of the Artifact rows of a Run's [`tree`](super::tree), lit upstream first in the
130/// tree's order.
131pub fn graph(tree: &[Node]) -> Graph {
132    let artifacts: Vec<(ArtifactName, &Node, Completion)> = tree
133        .iter()
134        .filter_map(|node| match node.kind {
135            Kind::Artifact { completion, .. } => match Target::from(&node.id) {
136                Target::Artifact(name) => Some((name, node, completion)),
137                _ => None,
138            },
139            Kind::Eval(_) => None,
140        })
141        .collect();
142    let mut lights: BTreeMap<ArtifactName, Light> = BTreeMap::new();
143    for (name, node, completion) in &artifacts {
144        // The first light of its evals in precedence order.
145        let evals = node
146            .children
147            .iter()
148            .filter_map(|eval| match &eval.kind {
149                Kind::Eval(view) => Some(eval_light(eval, view)),
150                Kind::Artifact { .. } => None,
151            })
152            .min()
153            .unwrap_or(Light::Green);
154        // Every eval done, yet graph.rs still holds them on their own gates, or a part
155        // is not done: not complete.
156        let light = match evals {
157            Light::Green => Light::from(*completion),
158            light => light,
159        };
160        lights.insert(name.clone(), light);
161    }
162    let position: BTreeMap<&ArtifactName, usize> = artifacts
163        .iter()
164        .enumerate()
165        .map(|(index, (name, _, _))| (name, index))
166        .collect();
167    let mut edges = Vec::new();
168    for (name, node, _) in &artifacts {
169        let upstream: BTreeSet<_> = node
170            .children
171            .iter()
172            .flat_map(|eval| eval.upstream.iter().map(|dependency| &dependency.artifact))
173            .collect();
174        for from in upstream {
175            let Some(&own) = lights.get(from).filter(|_| from != name) else {
176                continue;
177            };
178            edges.push(GraphEdge {
179                from: from.clone(),
180                to: name.clone(),
181                light: own,
182            });
183        }
184    }
185    edges.sort_by_key(|edge| (position[&edge.to], position[&edge.from]));
186    Graph {
187        nodes: artifacts
188            .into_iter()
189            .map(|(name, node, _)| GraphNode {
190                id: node.id.clone(),
191                light: lights[&name],
192                parts: node
193                    .parts
194                    .iter()
195                    .filter(|part| lights.contains_key(*part))
196                    .cloned()
197                    .collect(),
198                name,
199            })
200            .collect(),
201        edges,
202    }
203}