Skip to main content

ic_testkit/artifacts/
wasm_batch.rs

1use std::{
2    collections::HashSet,
3    path::PathBuf,
4    time::{Duration, Instant},
5};
6
7use super::wasm_cache::{
8    SharedIncrementalTargetMaintenanceConfig, SharedIncrementalTargetMaintenanceOutcome,
9    SharedIncrementalTargetPrunePolicy, WasmBuildBatchInputResolver, WasmBuildCacheMode,
10    WasmBuildError, WasmBuildOutcome, WasmBuildProgressConfig, WasmBuildProgressEvent,
11    WasmBuildSpec, build_wasm_canisters_cached_in_batch,
12    build_wasm_canisters_cached_in_batch_with_progress,
13};
14
15/// Orchestration shared by every entry in one independent Wasm build batch.
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17pub struct WasmBuildBatchConfig {
18    shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
19}
20
21/// Ordered outcomes and failures from a collect-all Wasm build batch.
22#[derive(Debug)]
23pub struct WasmBuildBatchReport {
24    results: Vec<Result<WasmBuildOutcome, WasmBuildError>>,
25    total: Duration,
26}
27
28/// Structured progress for an independent sequence of exact Wasm builds.
29#[non_exhaustive]
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum WasmBuildBatchProgressEvent {
32    /// One independently resolved build specification is about to start.
33    BuildStarted {
34        /// Zero-based position in the supplied specification slice.
35        index: usize,
36        /// Total number of supplied specifications.
37        total: usize,
38    },
39    /// Progress forwarded from one independent build.
40    BuildProgress {
41        /// Zero-based position in the supplied specification slice.
42        index: usize,
43        /// Event emitted by that build.
44        event: WasmBuildProgressEvent,
45    },
46    /// One independent build completed successfully.
47    BuildFinished {
48        /// Zero-based position in the supplied specification slice.
49        index: usize,
50    },
51    /// One independent build failed.
52    BuildFailed {
53        /// Zero-based position in the supplied specification slice.
54        index: usize,
55    },
56}
57
58impl WasmBuildBatchReport {
59    /// Per-specification results in the supplied order.
60    pub fn results(&self) -> &[Result<WasmBuildOutcome, WasmBuildError>] {
61        &self.results
62    }
63
64    /// Consume the report and return its ordered per-specification results.
65    #[must_use]
66    pub fn into_results(self) -> Vec<Result<WasmBuildOutcome, WasmBuildError>> {
67        self.results
68    }
69
70    /// Successful outcomes with their specification indexes.
71    pub fn outcomes(&self) -> impl Iterator<Item = (usize, &WasmBuildOutcome)> {
72        self.results
73            .iter()
74            .enumerate()
75            .filter_map(|(index, result)| result.as_ref().ok().map(|outcome| (index, outcome)))
76    }
77
78    /// Failures with their specification indexes.
79    pub fn failures(&self) -> impl Iterator<Item = (usize, &WasmBuildError)> {
80        self.results
81            .iter()
82            .enumerate()
83            .filter_map(|(index, result)| result.as_ref().err().map(|error| (index, error)))
84    }
85
86    /// Integrated shared-target maintenance outcomes with their build indexes.
87    ///
88    /// Batch-owned maintenance contributes at most one outcome for each
89    /// distinct configured shared-target path.
90    pub fn shared_incremental_maintenance_outcomes(
91        &self,
92    ) -> impl Iterator<Item = (usize, &SharedIncrementalTargetMaintenanceOutcome)> {
93        self.outcomes().filter_map(|(index, outcome)| {
94            outcome
95                .record()
96                .shared_incremental_maintenance()
97                .map(|maintenance| (index, maintenance))
98        })
99    }
100
101    /// Complete wall-clock time for the sequential collect-all batch.
102    #[must_use]
103    pub const fn total(&self) -> Duration {
104        self.total
105    }
106
107    /// Whether every specification completed successfully.
108    #[must_use]
109    pub fn is_success(&self) -> bool {
110        self.results.iter().all(Result::is_ok)
111    }
112}
113
114impl WasmBuildBatchConfig {
115    /// Create batch orchestration without batch-owned target maintenance.
116    #[must_use]
117    pub const fn new() -> Self {
118        Self {
119            shared_incremental_maintenance: None,
120        }
121    }
122
123    /// Maintain each distinct shared target once through its first batch entry.
124    #[must_use]
125    pub const fn with_shared_incremental_target_maintenance(
126        mut self,
127        config: SharedIncrementalTargetMaintenanceConfig,
128    ) -> Self {
129        self.shared_incremental_maintenance = Some(config);
130        self
131    }
132
133    /// Strictly maintain each distinct shared target at most once per interval.
134    #[must_use]
135    pub const fn with_shared_incremental_target_maintenance_at_most_every(
136        self,
137        policy: SharedIncrementalTargetPrunePolicy,
138        minimum_interval: Duration,
139    ) -> Self {
140        self.with_shared_incremental_target_maintenance(
141            SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
142        )
143    }
144
145    /// Batch-owned shared-target maintenance, when configured.
146    #[must_use]
147    pub const fn shared_incremental_target_maintenance(
148        self,
149    ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
150        self.shared_incremental_maintenance
151    }
152}
153
154impl std::fmt::Display for WasmBuildBatchReport {
155    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        let succeeded = self.results.iter().filter(|result| result.is_ok()).count();
157        let reused = self
158            .results
159            .iter()
160            .filter_map(|result| result.as_ref().ok())
161            .filter(|outcome| outcome.is_reused())
162            .count();
163        write!(
164            formatter,
165            "builds={} succeeded={} failed={} built={} reused={} total={:?}",
166            self.results.len(),
167            succeeded,
168            self.results.len().saturating_sub(succeeded),
169            succeeded.saturating_sub(reused),
170            reused,
171            self.total,
172        )
173    }
174}
175
176/// Build every Wasm specification as an independent Cargo invocation.
177///
178/// Specifications run sequentially and every result is retained. Each entry
179/// keeps its own package set, profile arguments, feature resolution,
180/// fingerprint, locks, and cache policy. Packages are never combined into one
181/// Cargo command because doing so can unify shared dependency features.
182#[must_use]
183pub fn build_wasm_canisters_cached_batch(specs: &[WasmBuildSpec]) -> WasmBuildBatchReport {
184    build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
185}
186
187/// Build an independent Wasm batch with shared batch orchestration.
188///
189/// Batch-owned maintenance is attached only to the first specification for
190/// each distinct configured shared-target path. Isolated specifications are
191/// unaffected. An entry mixing batch-owned and per-spec integrated maintenance
192/// reports an indexed error without preventing later entries from running.
193#[must_use]
194pub fn build_wasm_canisters_cached_batch_with_config(
195    specs: &[WasmBuildSpec],
196    config: WasmBuildBatchConfig,
197) -> WasmBuildBatchReport {
198    let mut resolver = WasmBuildBatchInputResolver::new(specs);
199    build_wasm_batch(specs, config, |spec, index| {
200        build_wasm_canisters_cached_in_batch(spec, index, &mut resolver)
201    })
202}
203
204/// Build an independent Wasm batch while forwarding structured progress.
205///
206/// The same observation configuration is applied to every entry. Batch events
207/// identify the originating specification without altering the standalone
208/// build semantics.
209pub fn build_wasm_canisters_cached_batch_with_progress<F>(
210    specs: &[WasmBuildSpec],
211    config: WasmBuildProgressConfig,
212    observer: F,
213) -> WasmBuildBatchReport
214where
215    F: FnMut(WasmBuildBatchProgressEvent),
216{
217    build_wasm_canisters_cached_batch_with_config_and_progress(
218        specs,
219        WasmBuildBatchConfig::new(),
220        config,
221        observer,
222    )
223}
224
225/// Build a configured independent Wasm batch while forwarding structured progress.
226pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
227    specs: &[WasmBuildSpec],
228    batch_config: WasmBuildBatchConfig,
229    progress_config: WasmBuildProgressConfig,
230    mut observer: F,
231) -> WasmBuildBatchReport
232where
233    F: FnMut(WasmBuildBatchProgressEvent),
234{
235    let count = specs.len();
236    let mut resolver = WasmBuildBatchInputResolver::new(specs);
237    build_wasm_batch(specs, batch_config, |spec, index| {
238        observer(WasmBuildBatchProgressEvent::BuildStarted {
239            index,
240            total: count,
241        });
242        let result = build_wasm_canisters_cached_in_batch_with_progress(
243            spec,
244            index,
245            &mut resolver,
246            progress_config,
247            |event| observer(WasmBuildBatchProgressEvent::BuildProgress { index, event }),
248        );
249        observer(match result {
250            Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index },
251            Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index },
252        });
253        result
254    })
255}
256
257fn build_wasm_batch<F>(
258    specs: &[WasmBuildSpec],
259    config: WasmBuildBatchConfig,
260    mut build: F,
261) -> WasmBuildBatchReport
262where
263    F: FnMut(&WasmBuildSpec, usize) -> Result<WasmBuildOutcome, WasmBuildError>,
264{
265    let started = Instant::now();
266    let mut results = Vec::with_capacity(specs.len());
267    let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
268    for (index, spec) in specs.iter().enumerate() {
269        if config.shared_incremental_maintenance.is_some()
270            && spec.shared_incremental_target_maintenance().is_some()
271        {
272            results.push(Err(batch_maintenance_ownership_error()));
273            continue;
274        }
275        let configured = maintenance.prepare_spec(spec);
276        results.push(build(configured.as_ref().unwrap_or(spec), index));
277    }
278    WasmBuildBatchReport {
279        results,
280        total: started.elapsed(),
281    }
282}
283
284struct BatchMaintenanceTracker {
285    config: Option<SharedIncrementalTargetMaintenanceConfig>,
286    configured_targets: HashSet<PathBuf>,
287}
288
289impl BatchMaintenanceTracker {
290    fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
291        Self {
292            config,
293            configured_targets: HashSet::new(),
294        }
295    }
296
297    fn prepare_spec(&mut self, spec: &WasmBuildSpec) -> Option<WasmBuildSpec> {
298        let config = self.config?;
299        debug_assert!(spec.shared_incremental_target_maintenance().is_none());
300        let WasmBuildCacheMode::SharedIncremental { target_dir } = spec.cache_mode() else {
301            return None;
302        };
303        if !self.configured_targets.insert(target_dir.clone()) {
304            return None;
305        }
306        Some(
307            spec.clone()
308                .with_shared_incremental_target_maintenance(config),
309        )
310    }
311}
312
313fn batch_maintenance_ownership_error() -> WasmBuildError {
314    WasmBuildError::InvalidSpec {
315        message:
316            "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
317                .to_owned(),
318    }
319}
320
321#[cfg(test)]
322mod tests;