use std::collections::HashMap;
use crate::error::{GraphError, GraphResult};
use crate::graph::ComputeGraph;
use crate::node::{BufferId, NodeId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiveInterval {
pub buf: BufferId,
pub def_pos: Option<usize>,
pub last_use_pos: Option<usize>,
pub size_bytes: usize,
pub external: bool,
}
impl LiveInterval {
#[must_use]
pub fn start(&self) -> usize {
self.def_pos.unwrap_or(0)
}
#[must_use]
pub fn end(&self) -> usize {
self.last_use_pos.unwrap_or(self.start())
}
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {
self.start() <= other.end() && other.start() <= self.end()
}
#[must_use]
pub fn is_dead(&self) -> bool {
self.last_use_pos.is_none() && self.def_pos.is_some()
}
#[must_use]
pub fn length(&self) -> usize {
self.end() - self.start()
}
}
#[derive(Debug, Clone)]
pub struct LivenessAnalysis {
pub order: Vec<NodeId>,
intervals: HashMap<BufferId, LiveInterval>,
}
impl LivenessAnalysis {
#[must_use]
pub fn interval(&self, buf: BufferId) -> Option<&LiveInterval> {
self.intervals.get(&buf)
}
pub fn all_intervals(&self) -> impl Iterator<Item = &LiveInterval> {
self.intervals.values()
}
pub fn sorted_by_start(&self) -> Vec<&LiveInterval> {
let mut ivs: Vec<&LiveInterval> = self.intervals.values().collect();
ivs.sort_by_key(|i| (i.start(), i.buf.0));
ivs
}
pub fn interference_pairs(&self) -> Vec<(BufferId, BufferId)> {
let ivs: Vec<&LiveInterval> = self.intervals.values().collect();
let mut pairs = Vec::new();
for i in 0..ivs.len() {
for j in (i + 1)..ivs.len() {
if ivs[i].overlaps(ivs[j]) {
let a = ivs[i].buf.min(ivs[j].buf);
let b = ivs[i].buf.max(ivs[j].buf);
pairs.push((a, b));
}
}
}
pairs.sort();
pairs.dedup();
pairs
}
pub fn dead_buffers(&self) -> Vec<BufferId> {
self.intervals
.values()
.filter(|i| i.is_dead())
.map(|i| i.buf)
.collect()
}
pub fn max_live_count(&self) -> usize {
if self.order.is_empty() {
return 0;
}
let n_steps = self.order.len();
let mut count_at = vec![0usize; n_steps];
for iv in self.intervals.values() {
let range = iv.start()..=iv.end().min(n_steps - 1);
for cnt in count_at[range].iter_mut() {
*cnt += 1;
}
}
*count_at.iter().max().unwrap_or(&0)
}
pub fn max_live_bytes(&self) -> usize {
if self.order.is_empty() {
return 0;
}
let n_steps = self.order.len();
let mut bytes_at = vec![0usize; n_steps];
for iv in self.intervals.values() {
if iv.external {
continue; }
let range = iv.start()..=iv.end().min(n_steps - 1);
let sz = iv.size_bytes;
for b in bytes_at[range].iter_mut() {
*b = b.saturating_add(sz);
}
}
*bytes_at.iter().max().unwrap_or(&0)
}
}
pub fn analyse(graph: &ComputeGraph) -> GraphResult<LivenessAnalysis> {
if graph.is_empty() {
return Err(GraphError::EmptyGraph);
}
let order = graph.topological_order()?;
let pos_of: HashMap<NodeId, usize> = order.iter().enumerate().map(|(p, &id)| (id, p)).collect();
let mut intervals: HashMap<BufferId, LiveInterval> = HashMap::new();
for buf in graph.buffers() {
intervals.insert(
buf.id,
LiveInterval {
buf: buf.id,
def_pos: None,
last_use_pos: None,
size_bytes: buf.size_bytes,
external: buf.external,
},
);
}
for &node_id in &order {
let node = graph.node(node_id)?;
let p = pos_of[&node_id];
for &buf in &node.outputs {
let iv = intervals.entry(buf).or_insert_with(|| LiveInterval {
buf,
def_pos: None,
last_use_pos: None,
size_bytes: 0,
external: false,
});
if iv.def_pos.is_none() {
iv.def_pos = Some(p);
}
}
for &buf in &node.inputs {
let iv = intervals.entry(buf).or_insert_with(|| LiveInterval {
buf,
def_pos: None,
last_use_pos: None,
size_bytes: 0,
external: false,
});
match iv.last_use_pos {
None => iv.last_use_pos = Some(p),
Some(prev) if p > prev => iv.last_use_pos = Some(p),
_ => {}
}
}
}
Ok(LivenessAnalysis { order, intervals })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::GraphBuilder;
fn build_linear_graph() -> (ComputeGraph, BufferId, NodeId, NodeId) {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let buf = b.alloc_buffer("shared", 1024);
let writer = b.add_barrier("writer");
let reader = b.add_barrier("reader");
b.set_outputs(writer, [buf]);
b.set_inputs(reader, [buf]);
b.dep(writer, reader);
let g = b.build().expect("test graph builds successfully");
(g, buf, writer, reader)
}
#[test]
fn liveness_empty_graph() {
let g = ComputeGraph::new();
assert!(matches!(analyse(&g), Err(GraphError::EmptyGraph)));
}
#[test]
fn liveness_buffer_def_and_use() {
let (g, buf, writer, reader) = build_linear_graph();
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
let iv = la
.interval(buf)
.expect("buffer registered in liveness analysis");
let order = &la.order;
let wpos = order
.iter()
.position(|&x| x == writer)
.expect("writer node present in topological order");
let rpos = order
.iter()
.position(|&x| x == reader)
.expect("reader node present in topological order");
assert_eq!(iv.def_pos, Some(wpos));
assert_eq!(iv.last_use_pos, Some(rpos));
assert!(!iv.is_dead());
}
#[test]
fn liveness_dead_buffer() {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let buf = b.alloc_buffer("dead", 512);
let writer = b.add_barrier("w");
b.set_outputs(writer, [buf]);
let g = b.build().expect("test graph builds successfully");
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
let iv = la
.interval(buf)
.expect("buffer registered in liveness analysis");
assert!(iv.is_dead());
}
#[test]
fn liveness_external_buffer_not_counted_in_bytes() {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let ext = b.alloc_external_buffer("ext", 65536);
let reader = b.add_barrier("r");
b.set_inputs(reader, [ext]);
let g = b.build().expect("test graph builds successfully");
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
assert_eq!(la.max_live_bytes(), 0);
}
#[test]
fn liveness_overlap_detection() {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let buf0 = b.alloc_buffer("b0", 1024);
let buf1 = b.alloc_buffer("b1", 2048);
let a = b.add_barrier("a");
let bnode = b.add_barrier("b");
let c = b.add_barrier("c");
b.set_outputs(a, [buf0]);
b.set_outputs(bnode, [buf1]);
b.set_inputs(c, [buf0, buf1]);
b.dep(a, c).dep(bnode, c);
let g = b.build().expect("test graph builds successfully");
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
let pairs = la.interference_pairs();
assert!(pairs.contains(&(BufferId(0), BufferId(1))));
}
#[test]
fn liveness_non_overlapping_no_interference() {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let buf0 = b.alloc_buffer("b0", 512);
let buf1 = b.alloc_buffer("b1", 512);
let n0 = b.add_barrier("n0");
let n1 = b.add_barrier("n1");
let n2 = b.add_barrier("n2");
let n3 = b.add_barrier("n3");
b.set_outputs(n0, [buf0]);
b.set_inputs(n1, [buf0]);
b.set_outputs(n2, [buf1]);
b.set_inputs(n3, [buf1]);
b.chain(&[n0, n1, n2, n3]);
let g = b.build().expect("test graph builds successfully");
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
let iv0 = la
.interval(buf0)
.expect("buf0 registered in liveness analysis");
let iv1 = la
.interval(buf1)
.expect("buf1 registered in liveness analysis");
assert!(!iv0.overlaps(iv1));
assert!(la.interference_pairs().is_empty());
}
#[test]
fn liveness_max_live_count() {
let (g, _buf, _w, _r) = build_linear_graph();
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
assert_eq!(la.max_live_count(), 1);
}
#[test]
fn liveness_max_live_bytes() {
let (g, _buf, _w, _r) = build_linear_graph();
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
assert_eq!(la.max_live_bytes(), 1024);
}
#[test]
fn liveness_sorted_by_start() {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let buf0 = b.alloc_buffer("b0", 100);
let buf1 = b.alloc_buffer("b1", 200);
let n0 = b.add_barrier("n0");
let n1 = b.add_barrier("n1");
b.set_outputs(n0, [buf0]);
b.set_outputs(n1, [buf1]);
b.dep(n0, n1);
let g = b.build().expect("test graph builds successfully");
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
let sorted = la.sorted_by_start();
if sorted.len() == 2 {
assert!(sorted[0].start() <= sorted[1].start());
}
}
#[test]
fn liveness_interval_length() {
let (g, buf, _w, _r) = build_linear_graph();
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
let iv = la
.interval(buf)
.expect("buffer registered in liveness analysis");
assert_eq!(iv.length(), 1);
}
#[test]
fn liveness_dead_buffers_list() {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let dead = b.alloc_buffer("dead", 1);
let _w = {
let w = b.add_barrier("w");
b.set_outputs(w, [dead]);
w
};
let g = b.build().expect("test graph builds successfully");
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
let dead_list = la.dead_buffers();
assert!(dead_list.contains(&dead));
}
#[test]
fn liveness_all_intervals_count() {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
b.alloc_buffer("a", 1);
b.alloc_buffer("b", 2);
b.alloc_buffer("c", 4);
b.add_barrier("n");
let g = b.build().expect("test graph builds successfully");
let la = analyse(&g).expect("liveness analysis succeeds on valid graph");
assert_eq!(la.all_intervals().count(), 3);
}
}