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