Skip to main content

sva_engine/render/
through.rs

1// Concern: renders from the root down through a persistent store, staging what it computes | Non-concern: the store's medium and commit, a render with no store | IO: (&Graph, target, Store) -> Render
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use sva_ast::Graph;
6use sva_formula::{Hash, Held as Representation, NodeId};
7
8use super::table::spill::Spill;
9use super::table::{self, Table};
10use super::{Render, RenderConfig, closed, drive, driving, drove, frontier, planned_over};
11use crate::cache::{Backend, CacheStats, Outcome, Recording, Store, Stored};
12use crate::error::{Diagnostic, EngineError, Located};
13use crate::instantiate;
14use crate::schedule;
15use crate::typing::{self, Typing};
16
17/// `render` through `store`, from the root down: a node the store answers stands as its
18/// samples, and nothing under it is typed, planned or looked up. What the rest computes is
19/// staged beside the store as the render drops it; only `persist` writes the store.
20pub async fn render_through<B: Backend>(
21    graph: &Graph,
22    target: &str,
23    config: RenderConfig,
24    store: &Store<B>,
25) -> Result<Render, EngineError> {
26    let instances = instantiate::instantiate(graph, target, config.rate)?;
27    let root = instances.instance_of(target)?;
28    let order = schedule::schedule_from(&instances, std::slice::from_ref(&root))?;
29    let keys: BTreeMap<String, Hash> = crate::source::identities(graph, &instances, &order)
30        .into_iter()
31        .map(|(path, identity)| {
32            let key = crate::cache::node_key(identity, config.rate, &config.profile);
33            (path, key)
34        })
35        .collect();
36    let mut found = frontier::Frontier::from((&instances, &order), &keys, &root, &config);
37    let (mut held, typed) = loop {
38        found.walk(store).await;
39        let tys = typing::infer_over(&instances, &order.within(&found.visited), &found.stored)?;
40        let id = tys
41            .id(&root)
42            .ok_or_else(|| EngineError::UnknownNode(root.clone()))?;
43        let bounds: BTreeSet<NodeId> = keys
44            .keys()
45            .filter_map(|path| tys.id(path))
46            .filter(|id| readable(&tys, *id))
47            .collect();
48        let typed = tys.lowered().to_vec();
49        let held = planned_over(&instances, (tys, id), config.clone(), &bounds)?;
50        let short = short(&held);
51        if short.is_empty() {
52            break (held, typed);
53        }
54        for path in short {
55            found.reopen(&path);
56        }
57    };
58    let staging = staging(&held, &keys, &found);
59    let mut kept = BTreeSet::new();
60    if let Some(mut driver) = driving(&mut held, Recording::over(None, None))? {
61        driver.spill = Some(Spill::over(staging));
62        while spilled(&mut driver, store, &mut kept).await? {}
63        let rest = driver.spill.take().map(|mut s| s.rest(&driver.table));
64        for (key, meta) in rest.into_iter().flatten() {
65            store.stage_meta(key, &meta).await.map_err(unstaged)?;
66            kept.insert(key);
67        }
68        drove(&mut held, driver);
69    }
70    let computed = held.table.as_ref().map_or(Vec::new(), |table| {
71        let computed = table.values.iter();
72        let computed = computed.filter(|v| !matches!(v.kind, table::Kind::Stored { .. }));
73        computed.map(|v| v.name.clone()).collect()
74    });
75    let mut lookups = found.lookups;
76    for lookup in &mut lookups {
77        if lookup.outcome == Outcome::ComputedNotStored && kept.contains(&lookup.key) {
78            lookup.outcome = Outcome::ComputedStored;
79        }
80    }
81    let reached = held.cache_stats.take().map_or(Vec::new(), |stats| {
82        let walked = lookups.len();
83        stats.reached.iter().map(|(at, _)| (*at, walked)).collect()
84    });
85    held.cache_stats = Some(CacheStats {
86        lookups,
87        reached,
88        typed,
89        planned: computed,
90        ..CacheStats::default()
91    });
92    closed(&mut held)?;
93    Ok(held)
94}
95
96/// Each stored node whose samples miss some its readers ask.
97fn short(held: &Render) -> Vec<String> {
98    let (Some(table), Some(range)) = (&held.table, held.range) else {
99        return Vec::new();
100    };
101    let needs = table.demand(range);
102    table
103        .values
104        .iter()
105        .zip(&needs)
106        .filter(|(value, need)| {
107            matches!(value.kind, table::Kind::Stored { .. }) && !need.compute.is_empty()
108        })
109        .map(|(value, _)| value.name.clone())
110        .collect()
111}
112
113/// One block pulled, and what it handed out moved into the staging area.
114async fn spilled<B: Backend>(
115    driver: &mut drive::Driver,
116    store: &Store<B>,
117    kept: &mut BTreeSet<Hash>,
118) -> Result<bool, EngineError> {
119    let more = driver.pull()?;
120    let spill = driver
121        .spill
122        .as_mut()
123        .expect("a render through a store spills");
124    for (key, samples) in std::mem::take(&mut spill.samples) {
125        store.stage(key, &samples).await.map_err(unstaged)?;
126    }
127    for (key, meta) in spill.whole(&driver.table) {
128        store.stage_meta(key, &meta).await.map_err(unstaged)?;
129        kept.insert(key);
130    }
131    Ok(more)
132}
133
134fn unstaged(why: String) -> EngineError {
135    EngineError::refused(Diagnostic {
136        code: "store.unwritable".to_string(),
137        message: format!("what the render computed could not be staged beside the store: {why}"),
138        location: Located::default(),
139        help: "free the store's disk, move it with `--cache <path>`, or pass `--cache none`"
140            .to_string(),
141    })
142}
143
144/// A node another may read as its samples alone: samples, and nothing that reads it between
145/// them.
146fn readable(tys: &Typing, id: NodeId) -> bool {
147    tys.ty(id).held == Representation::Sampled && !schedule::anywhere(tys, id)
148}
149
150/// Each value a missed node computes, at its own node's key: the root whatever it is, any
151/// other where a reader may take its samples.
152fn staging(
153    held: &Render,
154    keys: &BTreeMap<String, Hash>,
155    found: &frontier::Frontier<'_>,
156) -> Vec<(usize, Hash, Stored)> {
157    let Some(table) = &held.table else {
158        return Vec::new();
159    };
160    let tys = &held.tys;
161    let mut out = Vec::new();
162    for (path, key) in keys {
163        if found.stored.contains_key(path) || !found.visited.contains(path) {
164            continue;
165        }
166        let Some((id, at)) = tys.id(path).and_then(|id| Some((id, table.of(id)?))) else {
167            continue;
168        };
169        let value = &table.values[at];
170        let own = tys.name(id) == path;
171        let readable = readable(tys, id) && value.alias().is_none();
172        let kept = value.pure
173            && value.period.is_none()
174            && !matches!(value.kind, table::Kind::Frames { .. });
175        if !own || !kept || !(at == table.root || readable) {
176            continue;
177        }
178        let (priced, moved) = under(table, at);
179        let ty = tys.ty(id);
180        let meta = Stored {
181            key: *key,
182            segments: Vec::new(),
183            label: table.label(at),
184            width: u8::try_from(value.width).expect("a width the typing held"),
185            codomain: ty.codomain,
186            rate: ty.rate,
187            grid: tys.grid(id),
188            support: value.support,
189            priced,
190            moved,
191            readable,
192        };
193        out.push((at, *key, meta));
194    }
195    out
196}
197
198/// What a value and every value under it cost over the range, and the most any moved a read.
199fn under(table: &Table, at: usize) -> (u128, f64) {
200    let (mut seen, mut open) = (BTreeSet::from([at]), vec![at]);
201    let (mut priced, mut moved) = (0u128, 0.0f64);
202    while let Some(at) = open.pop() {
203        priced += table.planned[at];
204        moved = moved.max(table.values[at].moved);
205        open.extend(
206            table.values[at]
207                .reads
208                .iter()
209                .filter(|read| seen.insert(**read)),
210        );
211    }
212    (priced, moved)
213}