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::{
8    batch::{indexed_failures, indexed_outcomes},
9    wasm_cache::{
10        SharedIncrementalTargetMaintenanceConfig, SharedIncrementalTargetMaintenanceOutcome,
11        SharedIncrementalTargetPrunePolicy, WasmBuildBatchInputMetrics,
12        WasmBuildBatchInputResolver, WasmBuildCacheMode, WasmBuildError, WasmBuildOutcome,
13        WasmBuildProgressConfig, WasmBuildProgressEvent, WasmBuildSpec, WasmBuildTimings,
14        build_wasm_canisters_cached_in_batch, build_wasm_canisters_cached_in_batch_with_progress,
15    },
16};
17
18/// Orchestration shared by every entry in one independent Wasm build batch.
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub struct WasmBuildBatchConfig {
21    shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
22}
23
24/// Ordered outcomes and failures from a collect-all Wasm build batch.
25#[derive(Debug)]
26pub struct WasmBuildBatchReport {
27    results: Vec<Result<WasmBuildOutcome, WasmBuildError>>,
28    entry_elapsed: Vec<Duration>,
29    input_resolution: WasmBuildBatchInputMetrics,
30    total: Duration,
31}
32
33/// Aggregate counters and successful-acquisition timings for a Wasm build batch.
34#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
35pub struct WasmBuildBatchMetrics {
36    specifications: usize,
37    succeeded: usize,
38    failed: usize,
39    built: usize,
40    reused: usize,
41    input_resolution_runs: usize,
42    input_resolution_reuses: usize,
43    successful_timings: WasmBuildTimings,
44    total: Duration,
45}
46
47/// Structured progress for an independent sequence of exact Wasm builds.
48#[non_exhaustive]
49#[derive(Clone, Debug, Eq, PartialEq)]
50pub enum WasmBuildBatchProgressEvent {
51    /// One independently resolved build specification is about to start.
52    BuildStarted {
53        /// Zero-based position in the supplied specification slice.
54        index: usize,
55        /// Total number of supplied specifications.
56        total: usize,
57    },
58    /// Progress forwarded from one independent build.
59    BuildProgress {
60        /// Zero-based position in the supplied specification slice.
61        index: usize,
62        /// Event emitted by that build.
63        event: WasmBuildProgressEvent,
64    },
65    /// One independent build completed successfully.
66    BuildFinished {
67        /// Zero-based position in the supplied specification slice.
68        index: usize,
69    },
70    /// One independent build failed.
71    BuildFailed {
72        /// Zero-based position in the supplied specification slice.
73        index: usize,
74    },
75}
76
77impl WasmBuildBatchReport {
78    /// Per-specification results in the supplied order.
79    pub fn results(&self) -> &[Result<WasmBuildOutcome, WasmBuildError>] {
80        &self.results
81    }
82
83    /// Wall-clock time retained for every entry, including failed entries.
84    ///
85    /// Values use the supplied specification order and include batch-owned
86    /// validation plus the complete acquisition attempt for that entry.
87    #[must_use]
88    pub fn entry_elapsed(&self) -> &[Duration] {
89        &self.entry_elapsed
90    }
91
92    /// Consume the report and return its ordered per-specification results.
93    #[must_use]
94    pub fn into_results(self) -> Vec<Result<WasmBuildOutcome, WasmBuildError>> {
95        self.results
96    }
97
98    /// Successful outcomes with their specification indexes.
99    pub fn outcomes(&self) -> impl Iterator<Item = (usize, &WasmBuildOutcome)> {
100        indexed_outcomes(&self.results)
101    }
102
103    /// Failures with their specification indexes.
104    pub fn failures(&self) -> impl Iterator<Item = (usize, &WasmBuildError)> {
105        indexed_failures(&self.results)
106    }
107
108    /// Integrated shared-target maintenance outcomes with their build indexes.
109    ///
110    /// Batch-owned maintenance contributes at most one outcome for each
111    /// distinct configured shared-target path.
112    pub fn shared_incremental_maintenance_outcomes(
113        &self,
114    ) -> impl Iterator<Item = (usize, &SharedIncrementalTargetMaintenanceOutcome)> {
115        self.outcomes().filter_map(|(index, outcome)| {
116            outcome
117                .record()
118                .shared_incremental_maintenance()
119                .map(|maintenance| (index, maintenance))
120        })
121    }
122
123    /// Complete wall-clock time for the sequential collect-all batch.
124    #[must_use]
125    pub const fn total(&self) -> Duration {
126        self.total
127    }
128
129    /// Whether every specification completed successfully.
130    #[must_use]
131    pub fn is_success(&self) -> bool {
132        self.results.iter().all(Result::is_ok)
133    }
134
135    /// Aggregate outcome, input-resolution reuse, and timing counters.
136    #[must_use]
137    pub fn metrics(&self) -> WasmBuildBatchMetrics {
138        let mut metrics = WasmBuildBatchMetrics {
139            specifications: self.results.len(),
140            input_resolution_runs: self.input_resolution.runs,
141            input_resolution_reuses: self.input_resolution.reuses,
142            total: self.total,
143            ..WasmBuildBatchMetrics::default()
144        };
145        for result in &self.results {
146            match result {
147                Ok(outcome) => {
148                    metrics.succeeded += 1;
149                    if outcome.is_reused() {
150                        metrics.reused += 1;
151                    } else {
152                        metrics.built += 1;
153                    }
154                    metrics.successful_timings = metrics
155                        .successful_timings
156                        .saturating_add(outcome.record().timings());
157                }
158                Err(_) => metrics.failed += 1,
159            }
160        }
161        metrics
162    }
163}
164
165impl WasmBuildBatchMetrics {
166    /// Number of supplied specifications.
167    #[must_use]
168    pub const fn specifications(self) -> usize {
169        self.specifications
170    }
171
172    /// Number of successful specifications.
173    #[must_use]
174    pub const fn succeeded(self) -> usize {
175        self.succeeded
176    }
177
178    /// Number of failed specifications.
179    #[must_use]
180    pub const fn failed(self) -> usize {
181        self.failed
182    }
183
184    /// Number of newly built Wasm artifact sets.
185    #[must_use]
186    pub const fn built(self) -> usize {
187        self.built
188    }
189
190    /// Number of Wasm artifact sets reused from the exact cache.
191    #[must_use]
192    pub const fn reused(self) -> usize {
193        self.reused
194    }
195
196    /// Number of workspace/toolchain input-resolution snapshots performed.
197    #[must_use]
198    pub const fn input_resolution_runs(self) -> usize {
199        self.input_resolution_runs
200    }
201
202    /// Number of specifications resolved by reusing another batch snapshot.
203    #[must_use]
204    pub const fn input_resolution_reuses(self) -> usize {
205        self.input_resolution_reuses
206    }
207
208    /// Sum of timings from successful acquisitions.
209    #[must_use]
210    pub const fn successful_timings(self) -> WasmBuildTimings {
211        self.successful_timings
212    }
213
214    /// Complete wall-clock time for the sequential batch.
215    #[must_use]
216    pub const fn total(self) -> Duration {
217        self.total
218    }
219}
220
221impl WasmBuildBatchConfig {
222    /// Create batch orchestration without batch-owned target maintenance.
223    #[must_use]
224    pub const fn new() -> Self {
225        Self {
226            shared_incremental_maintenance: None,
227        }
228    }
229
230    /// Maintain each distinct shared target once through its first batch entry.
231    #[must_use]
232    pub const fn with_shared_incremental_target_maintenance(
233        mut self,
234        config: SharedIncrementalTargetMaintenanceConfig,
235    ) -> Self {
236        self.shared_incremental_maintenance = Some(config);
237        self
238    }
239
240    /// Strictly maintain each distinct shared target at most once per interval.
241    #[must_use]
242    pub const fn with_shared_incremental_target_maintenance_at_most_every(
243        self,
244        policy: SharedIncrementalTargetPrunePolicy,
245        minimum_interval: Duration,
246    ) -> Self {
247        self.with_shared_incremental_target_maintenance(
248            SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
249        )
250    }
251
252    /// Batch-owned shared-target maintenance, when configured.
253    #[must_use]
254    pub const fn shared_incremental_target_maintenance(
255        self,
256    ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
257        self.shared_incremental_maintenance
258    }
259}
260
261impl std::fmt::Display for WasmBuildBatchReport {
262    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263        let metrics = self.metrics();
264        write!(
265            formatter,
266            "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} successful_timings=({}) total={:?}",
267            metrics.specifications(),
268            metrics.succeeded(),
269            metrics.failed(),
270            metrics.built(),
271            metrics.reused(),
272            metrics.input_resolution_runs(),
273            metrics.input_resolution_reuses(),
274            metrics.successful_timings(),
275            metrics.total(),
276        )
277    }
278}
279
280/// Build every Wasm specification as an independent Cargo invocation.
281///
282/// Specifications run sequentially and every result is retained. Each entry
283/// keeps its own package set, profile arguments, feature resolution,
284/// fingerprint, locks, and cache policy. Packages are never combined into one
285/// Cargo command because doing so can unify shared dependency features.
286#[must_use]
287pub fn build_wasm_canisters_cached_batch(specs: &[WasmBuildSpec]) -> WasmBuildBatchReport {
288    build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
289}
290
291/// Build an independent Wasm batch with shared batch orchestration.
292///
293/// Batch-owned maintenance is attached only to the first specification for
294/// each distinct configured shared-target path. Isolated specifications are
295/// unaffected. An entry mixing batch-owned and per-spec integrated maintenance
296/// reports an indexed error without preventing later entries from running.
297#[must_use]
298pub fn build_wasm_canisters_cached_batch_with_config(
299    specs: &[WasmBuildSpec],
300    config: WasmBuildBatchConfig,
301) -> WasmBuildBatchReport {
302    let mut resolver = WasmBuildBatchInputResolver::new(specs);
303    let mut report = build_wasm_batch(specs, config, |spec, index| {
304        build_wasm_canisters_cached_in_batch(spec, index, &mut resolver)
305    });
306    report.input_resolution = resolver.metrics();
307    report
308}
309
310/// Build an independent Wasm batch while forwarding structured progress.
311///
312/// The same observation configuration is applied to every entry. Batch events
313/// identify the originating specification without altering the standalone
314/// build semantics.
315#[must_use]
316pub fn build_wasm_canisters_cached_batch_with_progress<F>(
317    specs: &[WasmBuildSpec],
318    config: WasmBuildProgressConfig,
319    observer: F,
320) -> WasmBuildBatchReport
321where
322    F: FnMut(WasmBuildBatchProgressEvent),
323{
324    build_wasm_canisters_cached_batch_with_config_and_progress(
325        specs,
326        WasmBuildBatchConfig::new(),
327        config,
328        observer,
329    )
330}
331
332/// Build a configured independent Wasm batch while forwarding structured progress.
333#[must_use]
334pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
335    specs: &[WasmBuildSpec],
336    batch_config: WasmBuildBatchConfig,
337    progress_config: WasmBuildProgressConfig,
338    mut observer: F,
339) -> WasmBuildBatchReport
340where
341    F: FnMut(WasmBuildBatchProgressEvent),
342{
343    let count = specs.len();
344    let mut resolver = WasmBuildBatchInputResolver::new(specs);
345    let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
346        observer(WasmBuildBatchProgressEvent::BuildStarted {
347            index,
348            total: count,
349        });
350        let result = build_wasm_canisters_cached_in_batch_with_progress(
351            spec,
352            index,
353            &mut resolver,
354            progress_config,
355            |event| observer(WasmBuildBatchProgressEvent::BuildProgress { index, event }),
356        );
357        observer(match result {
358            Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index },
359            Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index },
360        });
361        result
362    });
363    report.input_resolution = resolver.metrics();
364    report
365}
366
367fn build_wasm_batch<F>(
368    specs: &[WasmBuildSpec],
369    config: WasmBuildBatchConfig,
370    mut build: F,
371) -> WasmBuildBatchReport
372where
373    F: FnMut(&WasmBuildSpec, usize) -> Result<WasmBuildOutcome, WasmBuildError>,
374{
375    let started = Instant::now();
376    let mut results = Vec::with_capacity(specs.len());
377    let mut entry_elapsed = Vec::with_capacity(specs.len());
378    let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
379    for (index, spec) in specs.iter().enumerate() {
380        let entry_started = Instant::now();
381        if config.shared_incremental_maintenance.is_some()
382            && spec.shared_incremental_target_maintenance().is_some()
383        {
384            results.push(Err(batch_maintenance_ownership_error()));
385            entry_elapsed.push(entry_started.elapsed());
386            continue;
387        }
388        let configured = maintenance.prepare_spec(spec);
389        results.push(build(configured.as_ref().unwrap_or(spec), index));
390        entry_elapsed.push(entry_started.elapsed());
391    }
392    WasmBuildBatchReport {
393        results,
394        entry_elapsed,
395        input_resolution: WasmBuildBatchInputMetrics::default(),
396        total: started.elapsed(),
397    }
398}
399
400struct BatchMaintenanceTracker {
401    config: Option<SharedIncrementalTargetMaintenanceConfig>,
402    configured_targets: HashSet<PathBuf>,
403}
404
405impl BatchMaintenanceTracker {
406    fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
407        Self {
408            config,
409            configured_targets: HashSet::new(),
410        }
411    }
412
413    fn prepare_spec(&mut self, spec: &WasmBuildSpec) -> Option<WasmBuildSpec> {
414        let config = self.config?;
415        debug_assert!(spec.shared_incremental_target_maintenance().is_none());
416        let WasmBuildCacheMode::SharedIncremental { target_dir } = spec.cache_mode() else {
417            return None;
418        };
419        if !self.configured_targets.insert(target_dir.clone()) {
420            return None;
421        }
422        Some(
423            spec.clone()
424                .with_shared_incremental_target_maintenance(config),
425        )
426    }
427}
428
429fn batch_maintenance_ownership_error() -> WasmBuildError {
430    WasmBuildError::InvalidSpec {
431        message:
432            "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
433                .to_owned(),
434    }
435}
436
437#[cfg(test)]
438mod tests;