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