Skip to main content

harn_vm/prepared_module/
generation.rs

1use std::collections::BTreeMap;
2use std::num::NonZeroUsize;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use super::{PreparedModuleCache, PreparedModuleCacheStats, MAX_REMEMBERED_INTERFACES};
7use crate::module_artifact::ModuleProvenance;
8use crate::{ModulePhaseStats, VmError};
9
10/// Complete measurement for one immutable prepared module generation.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12#[non_exhaustive]
13pub struct PreparedModuleGenerationStats {
14    pub phases: ModulePhaseStats,
15    pub cache: PreparedModuleCacheStats,
16    pub source_modules: u64,
17    pub source_bytes: u64,
18    pub source_digest_blake3: [u8; 32],
19}
20
21impl PreparedModuleCache {
22    /// Create a cache large enough to retain one complete immutable module
23    /// generation. This shares the same hard ceiling as remembered graph
24    /// interfaces, so preparation cannot silently publish a generation whose
25    /// earliest artifacts were evicted before its first call.
26    pub fn for_immutable_generation() -> Self {
27        Self::with_capacity(
28            NonZeroUsize::new(MAX_REMEMBERED_INTERFACES)
29                .expect("immutable generation capacity is non-zero"),
30        )
31    }
32
33    pub(crate) fn source_snapshot(&self) -> Arc<BTreeMap<PathBuf, Arc<str>>> {
34        Arc::new(
35            self.sources
36                .lock()
37                .expect("prepared-module source lock poisoned")
38                .clone(),
39        )
40    }
41
42    /// Prepare the complete immutable generation rooted at `roots`, including
43    /// the root modules themselves, and retain its exact source snapshot.
44    pub fn prepare_module_generation(
45        &self,
46        roots: &[PathBuf],
47    ) -> Result<PreparedModuleGenerationStats, VmError> {
48        let phases = self.prepare_graph_with_provenance(roots, ModuleProvenance::User, true)?;
49        Ok(self.generation_stats(phases, roots))
50    }
51
52    /// Trusted-host counterpart to [`Self::prepare_module_generation`].
53    pub fn prepare_trusted_host_dispatch_generation(
54        &self,
55        roots: &[PathBuf],
56    ) -> Result<PreparedModuleGenerationStats, VmError> {
57        let phases =
58            self.prepare_graph_with_provenance(roots, ModuleProvenance::TrustedHostDispatch, true)?;
59        Ok(self.generation_stats(phases, roots))
60    }
61
62    fn generation_stats(
63        &self,
64        phases: ModulePhaseStats,
65        roots: &[PathBuf],
66    ) -> PreparedModuleGenerationStats {
67        let sources = self
68            .sources
69            .lock()
70            .expect("prepared-module source lock poisoned");
71        let root_dir = roots
72            .first()
73            .and_then(|root| root.parent())
74            .map(harn_modules::canonical_path)
75            .unwrap_or_default();
76        let logical_sources = sources
77            .iter()
78            .map(|(path, source)| {
79                let canonical = harn_modules::canonical_path(path);
80                (logical_generation_path(&root_dir, &canonical), source)
81            })
82            .collect::<BTreeMap<_, _>>();
83        PreparedModuleGenerationStats {
84            phases,
85            cache: self.stats(),
86            source_modules: logical_sources.len() as u64,
87            source_bytes: logical_sources
88                .values()
89                .map(|source| source.len() as u64)
90                .sum(),
91            source_digest_blake3: {
92                let mut hasher = blake3::Hasher::new();
93                hasher.update(b"harn-prepared-module-generation-v1\0");
94                for (path, source) in logical_sources {
95                    hasher.update(&(path.len() as u64).to_le_bytes());
96                    hasher.update(path.as_bytes());
97                    hasher.update(&(source.len() as u64).to_le_bytes());
98                    hasher.update(source.as_bytes());
99                }
100                *hasher.finalize().as_bytes()
101            },
102        }
103    }
104}
105
106fn logical_generation_path(root: &Path, path: &Path) -> String {
107    if let Ok(relative) = path.strip_prefix(root) {
108        return relative
109            .components()
110            .map(|part| part.as_os_str().to_string_lossy())
111            .collect::<Vec<_>>()
112            .join("/");
113    }
114
115    let root_parts = root.components().collect::<Vec<_>>();
116    let path_parts = path.components().collect::<Vec<_>>();
117    let shared = root_parts
118        .iter()
119        .zip(&path_parts)
120        .take_while(|(left, right)| left == right)
121        .count();
122    if shared == 0 {
123        return "<external>".to_string();
124    }
125    std::iter::repeat_n("..".to_string(), root_parts.len() - shared)
126        .chain(
127            path_parts[shared..]
128                .iter()
129                .map(|part| part.as_os_str().to_string_lossy().into_owned()),
130        )
131        .collect::<Vec<_>>()
132        .join("/")
133}