Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions execution_graph/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,8 +677,7 @@ impl<H: Host> ExecutionGraph<H> {
/// Builds a report-capable plan from all currently affected dirty work.
#[inline]
fn plan_all_report(&mut self, detail_mask: ReportDetailMask) -> RunPlan {
let collect_because = detail_mask.contains(ReportDetailMask::BECAUSE_OF)
|| detail_mask.contains(ReportDetailMask::WHY_PATH);
let collect_because = detail_mask.contains(ReportDetailMask::BECAUSE_OF);
let collect_why = detail_mask.contains(ReportDetailMask::WHY_PATH);

self.scratch.start_drain(self.nodes.len());
Expand Down Expand Up @@ -797,8 +796,7 @@ impl<H: Host> ExecutionGraph<H> {
return Err(GraphError::BadNodeId);
};
let output_count = n.output_ids.len();
let collect_because = detail_mask.contains(ReportDetailMask::BECAUSE_OF)
|| detail_mask.contains(ReportDetailMask::WHY_PATH);
let collect_because = detail_mask.contains(ReportDetailMask::BECAUSE_OF);
let collect_why = detail_mask.contains(ReportDetailMask::WHY_PATH);

self.scratch.start_drain(self.nodes.len());
Expand Down Expand Up @@ -1606,6 +1604,33 @@ mod tests {
assert!(e.because_of.is_some());
assert!(e.why_path.is_none());
}

g.set_input_value(na, "a", Value::I64(4)).unwrap();
g.invalidate_input("a");

let why_only = g
.run_node_with_report(nb, ReportDetailMask::WHY_PATH)
.unwrap();
assert_eq!(why_only.executed.len(), 2);
for e in &why_only.executed {
assert!(e.because_of.is_none());
assert!(e.why_path.is_some());
}

g.set_input_value(na, "a", Value::I64(5)).unwrap();
g.invalidate_input("a");

let full = g
.run_node_with_report(
nb,
ReportDetailMask::BECAUSE_OF | ReportDetailMask::WHY_PATH,
)
.unwrap();
assert_eq!(full.executed.len(), 2);
for e in &full.executed {
assert!(e.because_of.is_some());
assert!(e.why_path.is_some());
}
}

#[test]
Expand Down
96 changes: 96 additions & 0 deletions execution_graph/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
//! instrumentation. Formatting and UI are left to embedders.

use alloc::vec::Vec;
use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign};

use crate::{NodeId, ResourceKey};

/// Cheap run summary for incremental execution.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RunSummary {
/// Number of nodes executed during the run.
pub executed_nodes: usize,
Expand All @@ -27,20 +29,84 @@ impl ReportDetailMask {
/// Include the immediate dirty key that scheduled the node.
pub const BECAUSE_OF: Self = Self(1 << 0);
/// Include one plausible cause path from dirty root to output key.
///
/// This does not imply [`ReportDetailMask::BECAUSE_OF`]. Use
/// [`ReportDetailMask::FULL`] or combine masks when both fields are needed.
pub const WHY_PATH: Self = Self(1 << 1);
/// Include all optional fields.
pub const FULL: Self = Self(Self::BECAUSE_OF.0 | Self::WHY_PATH.0);

/// Returns a mask with the bits from `self` and `other`.
#[must_use]
#[inline]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}

/// Returns a mask with only the bits common to `self` and `other`.
#[must_use]
#[inline]
pub const fn intersection(self, other: Self) -> Self {
Self(self.0 & other.0)
}

/// Returns `true` if this mask contains every bit in `other`.
#[must_use]
#[inline]
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}

/// Returns `true` if this mask does not request any detail fields.
#[must_use]
#[inline]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
}

impl Default for ReportDetailMask {
#[inline]
fn default() -> Self {
Self::NONE
}
}

impl BitOr for ReportDetailMask {
type Output = Self;

#[inline]
fn bitor(self, rhs: Self) -> Self::Output {
self.union(rhs)
}
}

impl BitOrAssign for ReportDetailMask {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
*self = self.union(rhs);
}
}

impl BitAnd for ReportDetailMask {
type Output = Self;

#[inline]
fn bitand(self, rhs: Self) -> Self::Output {
self.intersection(rhs)
}
}

impl BitAndAssign for ReportDetailMask {
#[inline]
fn bitand_assign(&mut self, rhs: Self) {
*self = self.intersection(rhs);
}
}

/// Per-node detail record with optional payloads controlled by [`ReportDetailMask`].
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct NodeRunDetail {
/// The node that executed.
pub node: NodeId,
Expand All @@ -54,7 +120,37 @@ pub struct NodeRunDetail {

/// Detail report for a graph run.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RunDetailReport {
/// Per-node detail records in execution order.
pub executed: Vec<NodeRunDetail>,
}

#[cfg(test)]
mod tests {
use super::ReportDetailMask;

#[test]
fn report_detail_mask_composes_with_methods_and_operators() {
const FULL_FROM_UNION: ReportDetailMask =
ReportDetailMask::BECAUSE_OF.union(ReportDetailMask::WHY_PATH);

assert_eq!(ReportDetailMask::default(), ReportDetailMask::NONE);
assert!(ReportDetailMask::NONE.is_empty());
assert_eq!(FULL_FROM_UNION, ReportDetailMask::FULL);
assert_eq!(
ReportDetailMask::BECAUSE_OF | ReportDetailMask::WHY_PATH,
ReportDetailMask::FULL
);
assert_eq!(
ReportDetailMask::FULL & ReportDetailMask::BECAUSE_OF,
ReportDetailMask::BECAUSE_OF
);

let mut mask = ReportDetailMask::BECAUSE_OF;
mask |= ReportDetailMask::WHY_PATH;
assert_eq!(mask, ReportDetailMask::FULL);
mask &= ReportDetailMask::WHY_PATH;
assert_eq!(mask, ReportDetailMask::WHY_PATH);
}
}
Loading