renderdag/
render.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8use std::collections::BTreeMap;
9use std::ops::Range;
10
11use bitflags::bitflags;
12#[cfg(feature = "serialize")]
13use serde::Serialize;
14
15use super::column::Column;
16use super::column::ColumnsExt;
17use super::output::OutputRendererBuilder;
18
19pub trait Renderer<N> {
20    type Output;
21
22    // Returns the width of the graph line, possibly including another node.
23    fn width(&self, new_node: Option<&N>, new_parents: Option<&Vec<Ancestor<N>>>) -> u64;
24
25    // Reserve a column for the given node.
26    fn reserve(&mut self, node: N);
27
28    // Render the next row.
29    fn next_row(
30        &mut self,
31        node: N,
32        parents: Vec<Ancestor<N>>,
33        glyph: String,
34        message: String,
35    ) -> Self::Output;
36}
37
38/// Renderer for a DAG.
39///
40/// Converts a sequence of DAG node descriptions into rendered graph rows.
41pub struct GraphRowRenderer<N> {
42    columns: Vec<Column<N>>,
43}
44
45/// Ancestor type indication for an ancestor or parent node.
46pub enum Ancestor<N> {
47    /// The node is an eventual ancestor.
48    Ancestor(N),
49
50    /// The node is an immediate parent.
51    Parent(N),
52
53    /// The node is an anonymous ancestor.
54    Anonymous,
55}
56
57impl<N> Ancestor<N> {
58    fn to_column(&self) -> Column<N>
59    where
60        N: Clone,
61    {
62        match self {
63            Ancestor::Ancestor(n) => Column::Ancestor(n.clone()),
64            Ancestor::Parent(n) => Column::Parent(n.clone()),
65            Ancestor::Anonymous => Column::Blocked,
66        }
67    }
68
69    fn id(&self) -> Option<&N> {
70        match self {
71            Ancestor::Ancestor(n) => Some(n),
72            Ancestor::Parent(n) => Some(n),
73            Ancestor::Anonymous => None,
74        }
75    }
76
77    fn is_direct(&self) -> bool {
78        match self {
79            Ancestor::Ancestor(_) => false,
80            Ancestor::Parent(_) => true,
81            Ancestor::Anonymous => true,
82        }
83    }
84
85    fn to_link_line(&self, direct: LinkLine, indirect: LinkLine) -> LinkLine {
86        if self.is_direct() { direct } else { indirect }
87    }
88}
89
90struct AncestorColumnBounds {
91    target: usize,
92    min_ancestor: usize,
93    min_parent: usize,
94    max_parent: usize,
95    max_ancestor: usize,
96}
97
98impl AncestorColumnBounds {
99    fn new<N>(columns: &BTreeMap<usize, &Ancestor<N>>, target: usize) -> Option<Self> {
100        if columns.is_empty() {
101            return None;
102        }
103        let min_ancestor = columns
104            .iter()
105            .next()
106            .map_or(target, |(index, _)| *index)
107            .min(target);
108        let max_ancestor = columns
109            .iter()
110            .next_back()
111            .map_or(target, |(index, _)| *index)
112            .max(target);
113        let min_parent = columns
114            .iter()
115            .find(|(_, ancestor)| ancestor.is_direct())
116            .map_or(target, |(index, _)| *index)
117            .min(target);
118        let max_parent = columns
119            .iter()
120            .rev()
121            .find(|(_, ancestor)| ancestor.is_direct())
122            .map_or(target, |(index, _)| *index)
123            .max(target);
124        Some(Self {
125            target,
126            min_ancestor,
127            min_parent,
128            max_parent,
129            max_ancestor,
130        })
131    }
132
133    fn range(&self) -> Range<usize> {
134        if self.min_ancestor < self.max_ancestor {
135            self.min_ancestor + 1..self.max_ancestor
136        } else {
137            Default::default()
138        }
139    }
140
141    fn horizontal_line(&self, index: usize) -> LinkLine {
142        if index == self.target {
143            LinkLine::empty()
144        } else if index > self.min_parent && index < self.max_parent {
145            LinkLine::HORIZ_PARENT
146        } else if index > self.min_ancestor && index < self.max_ancestor {
147            LinkLine::HORIZ_ANCESTOR
148        } else {
149            LinkLine::empty()
150        }
151    }
152}
153
154impl<N> Column<N> {
155    fn to_node_line(&self) -> NodeLine {
156        match self {
157            Column::Ancestor(_) => NodeLine::Ancestor,
158            Column::Parent(_) => NodeLine::Parent,
159            _ => NodeLine::Blank,
160        }
161    }
162
163    fn to_link_line(&self) -> LinkLine {
164        match self {
165            Column::Ancestor(_) => LinkLine::VERT_ANCESTOR,
166            Column::Parent(_) => LinkLine::VERT_PARENT,
167            _ => LinkLine::empty(),
168        }
169    }
170
171    fn to_pad_line(&self) -> PadLine {
172        match self {
173            Column::Ancestor(_) => PadLine::Ancestor,
174            Column::Parent(_) => PadLine::Parent,
175            _ => PadLine::Blank,
176        }
177    }
178}
179
180/// A column in the node row.
181#[derive(Debug, Copy, Clone, PartialEq, Eq)]
182#[cfg_attr(feature = "serialize", derive(Serialize))]
183pub enum NodeLine {
184    /// Blank.
185    Blank,
186
187    /// Vertical line indicating an ancestor.
188    Ancestor,
189
190    /// Vertical line indicating a parent.
191    Parent,
192
193    /// The node for this row.
194    Node,
195}
196
197/// A column in a padding row.
198#[derive(Debug, Copy, Clone, PartialEq, Eq)]
199#[cfg_attr(feature = "serialize", derive(Serialize))]
200pub enum PadLine {
201    /// Blank.
202    Blank,
203
204    /// Vertical line indicating an ancestor.
205    Ancestor,
206
207    /// Vertical line indicating a parent.
208    Parent,
209}
210
211bitflags! {
212    /// A column in a linking row.
213    #[cfg_attr(feature = "serialize", derive(Serialize))]
214    #[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
215    pub struct LinkLine: u16 {
216        /// This cell contains a horizontal line that connects to a parent.
217        const HORIZ_PARENT = 0b0_0000_0000_0001;
218
219        /// This cell contains a horizontal line that connects to an ancestor.
220        const HORIZ_ANCESTOR = 0b0_0000_0000_0010;
221
222        /// The descendent of this cell is connected to the parent.
223        const VERT_PARENT = 0b0_0000_0000_0100;
224
225        /// The descendent of this cell is connected to an ancestor.
226        const VERT_ANCESTOR = 0b0_0000_0000_1000;
227
228        /// The parent of this cell is linked in this link row and the child
229        /// is to the left.
230        const LEFT_FORK_PARENT = 0b0_0000_0001_0000;
231
232        /// The ancestor of this cell is linked in this link row and the child
233        /// is to the left.
234        const LEFT_FORK_ANCESTOR = 0b0_0000_0010_0000;
235
236        /// The parent of this cell is linked in this link row and the child
237        /// is to the right.
238        const RIGHT_FORK_PARENT = 0b0_0000_0100_0000;
239
240        /// The ancestor of this cell is linked in this link row and the child
241        /// is to the right.
242        const RIGHT_FORK_ANCESTOR = 0b0_0000_1000_0000;
243
244        /// The child of this cell is linked to parents on the left.
245        const LEFT_MERGE_PARENT = 0b0_0001_0000_0000;
246
247        /// The child of this cell is linked to ancestors on the left.
248        const LEFT_MERGE_ANCESTOR = 0b0_0010_0000_0000;
249
250        /// The child of this cell is linked to parents on the right.
251        const RIGHT_MERGE_PARENT = 0b0_0100_0000_0000;
252
253        /// The child of this cell is linked to ancestors on the right.
254        const RIGHT_MERGE_ANCESTOR = 0b0_1000_0000_0000;
255
256        /// The target node of this link line is the child of this column.
257        /// This disambiguates between the node that is connected in this link
258        /// line, and other nodes that are also connected vertically.
259        const CHILD = 0b1_0000_0000_0000;
260
261        const HORIZONTAL = Self::HORIZ_PARENT.bits() | Self::HORIZ_ANCESTOR.bits();
262        const VERTICAL = Self::VERT_PARENT.bits() | Self::VERT_ANCESTOR.bits();
263        const LEFT_FORK = Self::LEFT_FORK_PARENT.bits() | Self::LEFT_FORK_ANCESTOR.bits();
264        const RIGHT_FORK = Self::RIGHT_FORK_PARENT.bits() | Self::RIGHT_FORK_ANCESTOR.bits();
265        const LEFT_MERGE = Self::LEFT_MERGE_PARENT.bits() | Self::LEFT_MERGE_ANCESTOR.bits();
266        const RIGHT_MERGE = Self::RIGHT_MERGE_PARENT.bits() | Self::RIGHT_MERGE_ANCESTOR.bits();
267        const ANY_MERGE = Self::LEFT_MERGE.bits() | Self::RIGHT_MERGE.bits();
268        const ANY_FORK = Self::LEFT_FORK.bits() | Self::RIGHT_FORK.bits();
269        const ANY_FORK_OR_MERGE = Self::ANY_MERGE.bits() | Self::ANY_FORK.bits();
270    }
271}
272
273/// An output graph row.
274#[derive(Debug)]
275#[cfg_attr(feature = "serialize", derive(Serialize))]
276pub struct GraphRow<N> {
277    /// The name of the node for this row.
278    pub node: N,
279
280    /// The glyph for this node.
281    pub glyph: String,
282
283    /// The message for this row.
284    pub message: String,
285
286    /// True if this row is for a merge commit.
287    pub merge: bool,
288
289    /// The node columns for this row.
290    pub node_line: Vec<NodeLine>,
291
292    /// The link columns for this row, if a link row is necessary.
293    pub link_line: Option<Vec<LinkLine>>,
294
295    /// The location of any terminators, if necessary.  Other columns should be
296    /// filled in with pad lines.
297    pub term_line: Option<Vec<bool>>,
298
299    /// The pad columns for this row.
300    pub pad_lines: Vec<PadLine>,
301}
302
303impl<N> GraphRowRenderer<N>
304where
305    N: Clone + Eq,
306{
307    /// Create a new renderer.
308    pub fn new() -> Self {
309        GraphRowRenderer {
310            columns: Vec::new(),
311        }
312    }
313
314    /// Build an output renderer from this renderer.
315    pub fn output(self) -> OutputRendererBuilder<N, Self> {
316        OutputRendererBuilder::new(self)
317    }
318}
319
320impl<N> Renderer<N> for GraphRowRenderer<N>
321where
322    N: Clone + Eq,
323{
324    type Output = GraphRow<N>;
325
326    fn width(&self, node: Option<&N>, parents: Option<&Vec<Ancestor<N>>>) -> u64 {
327        let mut width = self.columns.len();
328        let mut empty_columns = self
329            .columns
330            .iter()
331            .filter(|&column| column == &Column::Empty)
332            .count();
333        if let Some(node) = node {
334            // If the node is not already allocated, and there is no
335            // space for the node, then adding the new node would create
336            // a new column.
337            if self.columns.find(node).is_none() {
338                if empty_columns == 0 {
339                    width += 1;
340                } else {
341                    empty_columns = empty_columns.saturating_sub(1);
342                }
343            }
344        }
345        if let Some(parents) = parents {
346            // Non-allocated parents will also need a new column (except
347            // for one, which can take the place of the node, and any that could be allocated to
348            // empty columns).
349            let unallocated_parents = parents
350                .iter()
351                .filter(|parent| {
352                    parent
353                        .id()
354                        .map_or(true, |parent| self.columns.find(parent).is_none())
355                })
356                .count()
357                .saturating_sub(empty_columns);
358            width += unallocated_parents.saturating_sub(1);
359        }
360        width as u64
361    }
362
363    fn reserve(&mut self, node: N) {
364        if self.columns.find(&node).is_none() {
365            if let Some(index) = self.columns.first_empty() {
366                self.columns[index] = Column::Reserved(node);
367            } else {
368                self.columns.push(Column::Reserved(node));
369            }
370        }
371    }
372
373    fn next_row(
374        &mut self,
375        node: N,
376        parents: Vec<Ancestor<N>>,
377        glyph: String,
378        message: String,
379    ) -> GraphRow<N> {
380        // Find a column for this node.
381        let column = self.columns.find(&node).unwrap_or_else(|| {
382            self.columns
383                .first_empty()
384                .unwrap_or_else(|| self.columns.new_empty())
385        });
386        self.columns[column] = Column::Empty;
387
388        // This row is for a merge if there are multiple parents.
389        let merge = parents.len() > 1;
390
391        // Build the initial node line.
392        let mut node_line: Vec<_> = self.columns.iter().map(|c| c.to_node_line()).collect();
393        node_line[column] = NodeLine::Node;
394
395        // Build the initial link line.
396        let mut link_line: Vec<_> = self.columns.iter().map(|c| c.to_link_line()).collect();
397        let mut need_link_line = false;
398
399        // Build the initial term line.
400        let mut term_line: Vec<_> = self.columns.iter().map(|_| false).collect();
401        let mut need_term_line = false;
402
403        // Build the initial pad line.
404        let mut pad_lines: Vec<_> = self.columns.iter().map(|c| c.to_pad_line()).collect();
405
406        // Assign each parent to a column.
407        let mut parent_columns = BTreeMap::new();
408        for p in parents.iter() {
409            // Check if the parent already has a column.
410            if let Some(parent_id) = p.id() {
411                if let Some(index) = self.columns.find(parent_id) {
412                    self.columns[index].merge(&p.to_column());
413                    parent_columns.insert(index, p);
414                    continue;
415                }
416            }
417            // Assign the parent to an empty column, preferring the column
418            // the current node is going in, to maintain linearity.
419            if let Some(index) = self.columns.find_empty(column) {
420                self.columns[index].merge(&p.to_column());
421                parent_columns.insert(index, p);
422                continue;
423            }
424            // There are no empty columns left.  Make a new column.
425            parent_columns.insert(self.columns.len(), p);
426            node_line.push(NodeLine::Blank);
427            pad_lines.push(PadLine::Blank);
428            link_line.push(LinkLine::default());
429            term_line.push(false);
430            self.columns.push(p.to_column());
431        }
432
433        // Mark parent columns with anonymous parents as terminating.
434        for (i, p) in parent_columns.iter() {
435            if p.id().is_none() {
436                term_line[*i] = true;
437                need_term_line = true;
438            }
439        }
440
441        // Check if we can move the parent to the current column.
442        if parents.len() == 1 {
443            if let Some((&parent_column, _)) = parent_columns.iter().next() {
444                if parent_column > column {
445                    // This node has a single parent which was already
446                    // assigned to a column to the right of this one.
447                    // Move the parent to this column.
448                    self.columns.swap(column, parent_column);
449                    let parent = parent_columns
450                        .remove(&parent_column)
451                        .expect("parent should exist");
452                    parent_columns.insert(column, parent);
453
454                    // Generate a line from this column to the old
455                    // parent column.   We need to continue with the style
456                    // that was being used for the parent column.
457                    let was_direct = link_line[parent_column].contains(LinkLine::VERT_PARENT);
458                    link_line[column] |= if was_direct {
459                        LinkLine::RIGHT_FORK_PARENT
460                    } else {
461                        LinkLine::RIGHT_FORK_ANCESTOR
462                    };
463                    #[allow(clippy::needless_range_loop)]
464                    for i in column + 1..parent_column {
465                        link_line[i] |= if was_direct {
466                            LinkLine::HORIZ_PARENT
467                        } else {
468                            LinkLine::HORIZ_ANCESTOR
469                        };
470                    }
471                    link_line[parent_column] = if was_direct {
472                        LinkLine::LEFT_MERGE_PARENT
473                    } else {
474                        LinkLine::LEFT_MERGE_ANCESTOR
475                    };
476                    need_link_line = true;
477                    // The pad line for the old parent column is now blank.
478                    pad_lines[parent_column] = PadLine::Blank;
479                }
480            }
481        }
482
483        // Connect the node column to all the parent columns.
484        if let Some(bounds) = AncestorColumnBounds::new(&parent_columns, column) {
485            // If the parents extend beyond the columns adjacent to the node, draw a horizontal
486            // ancestor line between the two outermost ancestors.
487            for i in bounds.range() {
488                link_line[i] |= bounds.horizontal_line(i);
489                need_link_line = true;
490            }
491
492            // If there is a parent or ancestor to the right of the node
493            // column, the node merges from the right.
494            if bounds.max_parent > column {
495                link_line[column] |= LinkLine::RIGHT_MERGE_PARENT;
496                need_link_line = true;
497            } else if bounds.max_ancestor > column {
498                link_line[column] |= LinkLine::RIGHT_MERGE_ANCESTOR;
499                need_link_line = true;
500            }
501
502            // If there is a parent or ancestor to the left of the node column, the node merges from the left.
503            if bounds.min_parent < column {
504                link_line[column] |= LinkLine::LEFT_MERGE_PARENT;
505                need_link_line = true;
506            } else if bounds.min_ancestor < column {
507                link_line[column] |= LinkLine::LEFT_MERGE_ANCESTOR;
508                need_link_line = true;
509            }
510
511            // Each parent or ancestor forks towards the node column.
512            #[allow(clippy::comparison_chain)]
513            for (&i, p) in parent_columns.iter() {
514                pad_lines[i] = self.columns[i].to_pad_line();
515                if i < column {
516                    link_line[i] |=
517                        p.to_link_line(LinkLine::RIGHT_FORK_PARENT, LinkLine::RIGHT_FORK_ANCESTOR);
518                } else if i == column {
519                    link_line[i] |= LinkLine::CHILD
520                        | p.to_link_line(LinkLine::VERT_PARENT, LinkLine::VERT_ANCESTOR);
521                } else {
522                    link_line[i] |=
523                        p.to_link_line(LinkLine::LEFT_FORK_PARENT, LinkLine::LEFT_FORK_ANCESTOR);
524                }
525            }
526        }
527
528        // Now that we have assigned all the columns, reset their state.
529        self.columns.reset();
530
531        // Filter out the link line or term line if they are not needed.
532        let link_line = Some(link_line).filter(|_| need_link_line);
533        let term_line = Some(term_line).filter(|_| need_term_line);
534
535        GraphRow {
536            node,
537            glyph,
538            message,
539            merge,
540            node_line,
541            link_line,
542            term_line,
543            pad_lines,
544        }
545    }
546}