Skip to main content

ic_testkit/artifacts/
wasm_batch.rs

1use super::cache_fs::canonicalize_allow_missing;
2use crate::batch::{BatchLabelError, validate_labels};
3
4use std::{
5    collections::HashSet,
6    marker::PhantomData,
7    path::PathBuf,
8    time::{Duration, Instant},
9};
10
11use super::wasm_cache::{
12    SharedIncrementalTargetMaintenanceConfig, SharedIncrementalTargetMaintenanceOutcome,
13    SharedIncrementalTargetPrunePolicy, WasmBuildBatchAttempt, WasmBuildBatchInputMetrics,
14    WasmBuildBatchInputResolver, WasmBuildError, WasmBuildFailurePhase, WasmBuildFailureTimings,
15    WasmBuildInputReuse, WasmBuildInputSnapshotState, WasmBuildOutcome, WasmBuildProgressConfig,
16    WasmBuildProgressEvent, WasmBuildSessionState, WasmBuildSpec, WasmBuildTimings,
17    WasmInputResolutionTimings, build_wasm_canisters_cached_in_batch,
18    build_wasm_canisters_cached_in_batch_with_progress, shared_incremental_target,
19};
20
21/// Orchestration shared by every entry in one independent Wasm build batch.
22#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
23pub struct WasmBuildBatchConfig {
24    shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
25}
26
27/// Caller-labeled specification for one exact Wasm batch entry.
28///
29/// The label is report and progress identity only; it does not alter the
30/// underlying exact Wasm fingerprint or cache key.
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct LabeledWasmBuildSpec {
33    label: String,
34    spec: WasmBuildSpec,
35}
36
37/// Ordered outcomes and failures from a collect-all Wasm build batch.
38#[derive(Debug)]
39pub struct WasmBuildBatchReport {
40    entries: Vec<WasmBuildBatchEntry>,
41    input_resolution: WasmBuildBatchInputMetrics,
42    total: Duration,
43}
44
45/// Explicit cross-call input snapshot scoped to a caller-held source lease.
46///
47/// The session contains no global state. It may reuse successful Cargo/rustc
48/// identity, metadata, input-discovery, and content-digest work while the
49/// caller keeps the supplied write-exclusion guard alive and unchanged.
50pub struct WasmBuildSession<'guard> {
51    state: WasmBuildSessionState,
52    _source_guard: PhantomData<&'guard ()>,
53}
54
55/// Immutable prepared Cargo input resolution shared by concurrent readers.
56///
57/// Preparation resolves the complete declared specification set while the
58/// caller holds a genuine source write-exclusion guard. Reader batches may run
59/// concurrently through `&self`, but cannot introduce specifications that
60/// were not declared during preparation.
61pub struct WasmBuildInputSnapshot<'guard> {
62    state: WasmBuildInputSnapshotState,
63    _source_guard: PhantomData<&'guard ()>,
64}
65
66/// Aggregate state retained by one explicit Wasm build session.
67#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
68pub struct WasmBuildSessionMetrics {
69    snapshots: usize,
70    snapshot_reuses: usize,
71    invalidated: bool,
72}
73
74/// Preparation and reader-reuse counters for one immutable input snapshot.
75#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76pub struct WasmBuildInputSnapshotMetrics {
77    specifications: usize,
78    input_resolution_runs: usize,
79    input_resolution_reuses: usize,
80    input_resolution_timings: WasmInputResolutionTimings,
81    reader_reuses: usize,
82    invalidated: bool,
83}
84
85/// One ordered caller-labeled result from a Wasm build batch.
86#[derive(Debug)]
87pub struct WasmBuildBatchEntry {
88    index: usize,
89    label: String,
90    result: Result<WasmBuildOutcome, WasmBuildError>,
91    failure: Option<WasmBuildFailureDetails>,
92    entry_elapsed: Duration,
93}
94
95/// Structured phase and partial timings for one failed Wasm entry.
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub struct WasmBuildFailureDetails {
98    phase: WasmBuildFailurePhase,
99    timings: WasmBuildFailureTimings,
100}
101
102/// One successful Wasm batch entry.
103#[derive(Clone, Copy, Debug)]
104pub struct WasmBuildBatchOutcomeEntry<'a> {
105    index: usize,
106    label: &'a str,
107    outcome: &'a WasmBuildOutcome,
108    entry_elapsed: Duration,
109}
110
111/// One failed Wasm batch entry with its retained wall-clock time.
112#[derive(Clone, Copy, Debug)]
113pub struct WasmBuildBatchFailure<'a> {
114    index: usize,
115    label: &'a str,
116    error: &'a WasmBuildError,
117    details: WasmBuildFailureDetails,
118    entry_elapsed: Duration,
119}
120
121/// One integrated shared-target maintenance outcome from a Wasm batch.
122#[derive(Clone, Copy, Debug)]
123pub struct WasmBuildBatchMaintenanceEntry<'a> {
124    index: usize,
125    label: &'a str,
126    outcome: &'a SharedIncrementalTargetMaintenanceOutcome,
127}
128
129/// Structural error that prevents a labeled Wasm batch from starting.
130#[non_exhaustive]
131#[derive(Clone, Debug, Eq, PartialEq)]
132pub enum WasmBuildBatchContractError {
133    /// An entry label was empty.
134    EmptyLabel {
135        /// Zero-based position of the invalid entry.
136        index: usize,
137    },
138    /// Two entries used the same label.
139    DuplicateLabel {
140        /// Duplicated caller label.
141        label: String,
142        /// Position where the label first appeared.
143        first_index: usize,
144        /// Position where the label was repeated.
145        duplicate_index: usize,
146    },
147    /// A source mutation invalidated the caller's immutable-source lease.
148    SourceLeaseInvalidated,
149    /// A prepared snapshot reader requested a specification absent at preparation.
150    SpecificationNotPrepared {
151        /// Zero-based position of the undeclared entry.
152        index: usize,
153        /// Caller-owned label of the undeclared entry.
154        label: String,
155    },
156}
157
158impl LabeledWasmBuildSpec {
159    /// Attach a caller-owned stable label to one Wasm build specification.
160    #[must_use]
161    pub fn new(label: impl Into<String>, spec: WasmBuildSpec) -> Self {
162        Self {
163            label: label.into(),
164            spec,
165        }
166    }
167
168    /// Caller-owned report and progress label.
169    #[must_use]
170    pub fn label(&self) -> &str {
171        &self.label
172    }
173
174    /// Underlying exact Wasm build specification.
175    #[must_use]
176    pub const fn spec(&self) -> &WasmBuildSpec {
177        &self.spec
178    }
179
180    /// Consume the entry into its label and Wasm build specification.
181    #[must_use]
182    pub fn into_parts(self) -> (String, WasmBuildSpec) {
183        (self.label, self.spec)
184    }
185}
186
187impl<'guard> WasmBuildSession<'guard> {
188    /// Assert source immutability and bind reuse to the supplied guard's lifetime.
189    ///
190    /// The guard must prevent mutation of every Cargo/rustc executable,
191    /// manifest, configuration file, discovered source, declared additional
192    /// input, and relevant environment value used by every specification sent
193    /// through this session. The guard must remain held until the session is
194    /// dropped. This method cannot verify the guard's provenance; supplying an
195    /// unrelated value can permit stale cache reuse.
196    #[must_use]
197    pub fn assume_sources_immutable<Guard: ?Sized>(_source_write_guard: &'guard Guard) -> Self {
198        Self {
199            state: WasmBuildSessionState::new(),
200            _source_guard: PhantomData,
201        }
202    }
203
204    /// Build one sequential collect-all batch using retained immutable inputs.
205    pub fn build_batch(
206        &mut self,
207        specs: &[LabeledWasmBuildSpec],
208        config: WasmBuildBatchConfig,
209    ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
210        run_wasm_batch(
211            specs,
212            config,
213            Some(WasmBuildInputReuse::Session(&mut self.state)),
214            None,
215        )
216    }
217
218    /// Build one observed sequential batch using retained immutable inputs.
219    pub fn build_batch_with_progress<F>(
220        &mut self,
221        specs: &[LabeledWasmBuildSpec],
222        batch_config: WasmBuildBatchConfig,
223        progress_config: WasmBuildProgressConfig,
224        mut observer: F,
225    ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
226    where
227        F: FnMut(WasmBuildBatchProgressEvent),
228    {
229        run_wasm_batch(
230            specs,
231            batch_config,
232            Some(WasmBuildInputReuse::Session(&mut self.state)),
233            Some((progress_config, &mut observer)),
234        )
235    }
236
237    /// Current retained snapshot, reuse, and invalidation counters.
238    #[must_use]
239    pub const fn metrics(&self) -> WasmBuildSessionMetrics {
240        WasmBuildSessionMetrics {
241            snapshots: self.state.snapshot_count(),
242            snapshot_reuses: self.state.snapshot_reuses(),
243            invalidated: self.state.is_invalidated(),
244        }
245    }
246}
247
248impl WasmBuildSessionMetrics {
249    /// Number of successful exact specification snapshots currently retained.
250    #[must_use]
251    pub const fn snapshots(self) -> usize {
252        self.snapshots
253    }
254
255    /// Number of later entries resolved from a retained snapshot.
256    #[must_use]
257    pub const fn snapshot_reuses(self) -> usize {
258        self.snapshot_reuses
259    }
260
261    /// Whether a detected source race permanently invalidated this session.
262    #[must_use]
263    pub const fn is_invalidated(self) -> bool {
264        self.invalidated
265    }
266}
267
268impl<'guard> WasmBuildInputSnapshot<'guard> {
269    /// Resolve and freeze the complete specification set under a source lease.
270    ///
271    /// The guard must prevent mutation of every Cargo/rustc executable,
272    /// manifest, configuration file, discovered source, declared additional
273    /// input, and relevant environment value used by the supplied
274    /// specifications. The type system cannot verify guard provenance.
275    pub fn prepare_assuming_sources_immutable<Guard: ?Sized>(
276        _source_write_guard: &'guard Guard,
277        specs: &[WasmBuildSpec],
278    ) -> Result<Self, WasmBuildError> {
279        Ok(Self {
280            state: WasmBuildInputSnapshotState::prepare(specs)?,
281            _source_guard: PhantomData,
282        })
283    }
284
285    /// Build one sequential collect-all batch from prepared inputs.
286    ///
287    /// Separate calls may run concurrently. Every exact specification must
288    /// have been supplied to [`Self::prepare_assuming_sources_immutable`].
289    pub fn build_batch(
290        &self,
291        specs: &[LabeledWasmBuildSpec],
292        config: WasmBuildBatchConfig,
293    ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
294        run_wasm_batch(
295            specs,
296            config,
297            Some(WasmBuildInputReuse::Snapshot(&self.state)),
298            None,
299        )
300    }
301
302    /// Build one observed sequential batch from prepared inputs.
303    ///
304    /// Separate calls may run concurrently and use independent observers.
305    pub fn build_batch_with_progress<F>(
306        &self,
307        specs: &[LabeledWasmBuildSpec],
308        batch_config: WasmBuildBatchConfig,
309        progress_config: WasmBuildProgressConfig,
310        mut observer: F,
311    ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
312    where
313        F: FnMut(WasmBuildBatchProgressEvent),
314    {
315        run_wasm_batch(
316            specs,
317            batch_config,
318            Some(WasmBuildInputReuse::Snapshot(&self.state)),
319            Some((progress_config, &mut observer)),
320        )
321    }
322
323    /// Current preparation, reader-reuse, and invalidation metrics.
324    #[must_use]
325    pub fn metrics(&self) -> WasmBuildInputSnapshotMetrics {
326        let preparation = self.state.preparation_metrics();
327        WasmBuildInputSnapshotMetrics {
328            specifications: self.state.specification_count(),
329            input_resolution_runs: preparation.runs,
330            input_resolution_reuses: preparation.reuses,
331            input_resolution_timings: self.state.preparation_timings(),
332            reader_reuses: self.state.reader_reuses(),
333            invalidated: self.state.is_invalidated(),
334        }
335    }
336}
337
338impl WasmBuildInputSnapshotMetrics {
339    /// Number of exact specifications captured during preparation.
340    #[must_use]
341    pub const fn specifications(self) -> usize {
342        self.specifications
343    }
344
345    /// Number of workspace/toolchain resolution snapshots prepared.
346    #[must_use]
347    pub const fn input_resolution_runs(self) -> usize {
348        self.input_resolution_runs
349    }
350
351    /// Number of prepared specifications sharing another resolution run.
352    #[must_use]
353    pub const fn input_resolution_reuses(self) -> usize {
354        self.input_resolution_reuses
355    }
356
357    /// Complete tool, metadata, discovery, and hashing preparation timings.
358    #[must_use]
359    pub const fn input_resolution_timings(self) -> WasmInputResolutionTimings {
360        self.input_resolution_timings
361    }
362
363    /// Cumulative exact specification resolutions served to readers.
364    #[must_use]
365    pub const fn reader_reuses(self) -> usize {
366        self.reader_reuses
367    }
368
369    /// Whether any reader detected a violation of the source lease.
370    #[must_use]
371    pub const fn is_invalidated(self) -> bool {
372        self.invalidated
373    }
374}
375
376impl WasmBuildBatchEntry {
377    /// Zero-based position in the supplied labeled specification slice.
378    #[must_use]
379    pub const fn index(&self) -> usize {
380        self.index
381    }
382
383    /// Caller-owned stable label.
384    #[must_use]
385    pub fn label(&self) -> &str {
386        &self.label
387    }
388
389    /// Structured success or failure for this entry.
390    pub const fn result(&self) -> Result<&WasmBuildOutcome, &WasmBuildError> {
391        self.result.as_ref()
392    }
393
394    /// Successful Wasm outcome, when this entry succeeded.
395    #[must_use]
396    pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
397        self.result.as_ref().ok()
398    }
399
400    /// Structured build failure, when this entry failed.
401    #[must_use]
402    pub fn error(&self) -> Option<&WasmBuildError> {
403        self.result.as_ref().err()
404    }
405
406    /// Structured phase and partial timings when this entry failed.
407    #[must_use]
408    pub const fn failure_details(&self) -> Option<WasmBuildFailureDetails> {
409        self.failure
410    }
411
412    /// Complete wall-clock time retained for this entry.
413    #[must_use]
414    pub const fn entry_elapsed(&self) -> Duration {
415        self.entry_elapsed
416    }
417
418    /// Whether this entry completed successfully.
419    #[must_use]
420    pub const fn is_success(&self) -> bool {
421        self.result.is_ok()
422    }
423
424    /// Consume the entry into its identity, result, optional failure details, and wall time.
425    pub fn into_parts(
426        self,
427    ) -> (
428        usize,
429        String,
430        Result<WasmBuildOutcome, WasmBuildError>,
431        Option<WasmBuildFailureDetails>,
432        Duration,
433    ) {
434        (
435            self.index,
436            self.label,
437            self.result,
438            self.failure,
439            self.entry_elapsed,
440        )
441    }
442}
443
444impl WasmBuildFailureDetails {
445    /// Primary acquisition phase that returned the failure.
446    #[must_use]
447    pub const fn phase(self) -> WasmBuildFailurePhase {
448        self.phase
449    }
450
451    /// Partial phase timings retained before the failure returned.
452    #[must_use]
453    pub const fn timings(self) -> WasmBuildFailureTimings {
454        self.timings
455    }
456}
457
458impl<'a> WasmBuildBatchOutcomeEntry<'a> {
459    /// Zero-based position in the supplied labeled specification slice.
460    #[must_use]
461    pub const fn index(self) -> usize {
462        self.index
463    }
464
465    /// Caller-owned stable label.
466    #[must_use]
467    pub const fn label(self) -> &'a str {
468        self.label
469    }
470
471    /// Successful Wasm build outcome.
472    #[must_use]
473    pub const fn outcome(self) -> &'a WasmBuildOutcome {
474        self.outcome
475    }
476
477    /// Complete wall-clock time retained for this successful entry.
478    #[must_use]
479    pub const fn entry_elapsed(self) -> Duration {
480        self.entry_elapsed
481    }
482}
483
484impl<'a> WasmBuildBatchFailure<'a> {
485    /// Zero-based position in the supplied specification slice.
486    #[must_use]
487    pub const fn index(self) -> usize {
488        self.index
489    }
490
491    /// Caller-owned stable label.
492    #[must_use]
493    pub const fn label(self) -> &'a str {
494        self.label
495    }
496
497    /// Structured acquisition failure.
498    #[must_use]
499    pub const fn error(self) -> &'a WasmBuildError {
500        self.error
501    }
502
503    /// Primary acquisition phase that returned the failure.
504    #[must_use]
505    pub const fn phase(self) -> WasmBuildFailurePhase {
506        self.details.phase
507    }
508
509    /// Partial phase timings retained before the failure returned.
510    #[must_use]
511    pub const fn timings(self) -> WasmBuildFailureTimings {
512        self.details.timings
513    }
514
515    /// Complete wall-clock time retained for this failed entry.
516    #[must_use]
517    pub const fn entry_elapsed(self) -> Duration {
518        self.entry_elapsed
519    }
520}
521
522impl<'a> WasmBuildBatchMaintenanceEntry<'a> {
523    /// Zero-based position in the supplied labeled specification slice.
524    #[must_use]
525    pub const fn index(self) -> usize {
526        self.index
527    }
528
529    /// Caller-owned stable label.
530    #[must_use]
531    pub const fn label(self) -> &'a str {
532        self.label
533    }
534
535    /// Structured shared-target maintenance outcome.
536    #[must_use]
537    pub const fn outcome(self) -> &'a SharedIncrementalTargetMaintenanceOutcome {
538        self.outcome
539    }
540}
541
542/// Aggregate counters and successful-acquisition timings for a Wasm build batch.
543#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
544pub struct WasmBuildBatchMetrics {
545    specifications: usize,
546    succeeded: usize,
547    failed: usize,
548    built: usize,
549    reused: usize,
550    input_resolution_runs: usize,
551    input_resolution_reuses: usize,
552    input_resolution_session_reuses: usize,
553    input_resolution_prepared_reuses: usize,
554    successful_timings: WasmBuildTimings,
555    total: Duration,
556}
557
558/// Structured progress for an independent sequence of exact Wasm builds.
559#[non_exhaustive]
560#[derive(Clone, Debug, Eq, PartialEq)]
561pub enum WasmBuildBatchProgressEvent {
562    /// One independently resolved build specification is about to start.
563    BuildStarted {
564        /// Zero-based position in the supplied specification slice.
565        index: usize,
566        /// Caller-owned stable label.
567        label: String,
568        /// Total number of supplied specifications.
569        total: usize,
570    },
571    /// Progress forwarded from one independent build.
572    BuildProgress {
573        /// Zero-based position in the supplied specification slice.
574        index: usize,
575        /// Caller-owned stable label.
576        label: String,
577        /// Event emitted by that build.
578        event: WasmBuildProgressEvent,
579    },
580    /// One independent build completed successfully.
581    BuildFinished {
582        /// Zero-based position in the supplied specification slice.
583        index: usize,
584        /// Caller-owned stable label.
585        label: String,
586    },
587    /// One independent build failed.
588    BuildFailed {
589        /// Zero-based position in the supplied specification slice.
590        index: usize,
591        /// Caller-owned stable label.
592        label: String,
593    },
594}
595
596impl WasmBuildBatchReport {
597    /// Ordered labeled entries.
598    #[must_use]
599    pub fn entries(&self) -> &[WasmBuildBatchEntry] {
600        &self.entries
601    }
602
603    /// Consume the report into its ordered labeled entries.
604    #[must_use]
605    pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
606        self.entries
607    }
608
609    /// Structured successful entries with labels and wall-clock times.
610    pub fn outcomes(&self) -> impl Iterator<Item = WasmBuildBatchOutcomeEntry<'_>> {
611        self.entries.iter().filter_map(|entry| {
612            entry.outcome().map(|outcome| WasmBuildBatchOutcomeEntry {
613                index: entry.index,
614                label: &entry.label,
615                outcome,
616                entry_elapsed: entry.entry_elapsed,
617            })
618        })
619    }
620
621    /// Structured failed entries with labels and wall-clock times.
622    pub fn failures(&self) -> impl Iterator<Item = WasmBuildBatchFailure<'_>> {
623        self.entries.iter().filter_map(|entry| {
624            entry.error().map(|error| WasmBuildBatchFailure {
625                index: entry.index,
626                label: &entry.label,
627                error,
628                details: entry
629                    .failure
630                    .expect("failed Wasm batch entry must retain failure details"),
631                entry_elapsed: entry.entry_elapsed,
632            })
633        })
634    }
635
636    /// Labeled integrated shared-target maintenance outcomes.
637    ///
638    /// Batch-owned maintenance contributes at most one outcome for each
639    /// distinct configured shared-target path.
640    pub fn shared_incremental_maintenance_outcomes(
641        &self,
642    ) -> impl Iterator<Item = WasmBuildBatchMaintenanceEntry<'_>> {
643        self.outcomes().filter_map(|entry| {
644            entry
645                .outcome
646                .record()
647                .shared_incremental_maintenance()
648                .map(|outcome| WasmBuildBatchMaintenanceEntry {
649                    index: entry.index,
650                    label: entry.label,
651                    outcome,
652                })
653        })
654    }
655
656    /// Complete wall-clock time for the sequential collect-all batch.
657    #[must_use]
658    pub const fn total(&self) -> Duration {
659        self.total
660    }
661
662    /// Whether every specification completed successfully.
663    #[must_use]
664    pub fn is_success(&self) -> bool {
665        self.entries.iter().all(WasmBuildBatchEntry::is_success)
666    }
667
668    /// Aggregate outcome, input-resolution reuse, and timing counters.
669    #[must_use]
670    pub fn metrics(&self) -> WasmBuildBatchMetrics {
671        let mut metrics = WasmBuildBatchMetrics {
672            specifications: self.entries.len(),
673            input_resolution_runs: self.input_resolution.runs,
674            input_resolution_reuses: self.input_resolution.reuses,
675            input_resolution_session_reuses: self.input_resolution.session_reuses,
676            input_resolution_prepared_reuses: self.input_resolution.prepared_reuses,
677            total: self.total,
678            ..WasmBuildBatchMetrics::default()
679        };
680        for entry in &self.entries {
681            match &entry.result {
682                Ok(outcome) => {
683                    metrics.succeeded += 1;
684                    if outcome.is_reused() {
685                        metrics.reused += 1;
686                    } else {
687                        metrics.built += 1;
688                    }
689                    metrics.successful_timings = metrics
690                        .successful_timings
691                        .saturating_add(outcome.record().timings());
692                }
693                Err(_) => metrics.failed += 1,
694            }
695        }
696        metrics
697    }
698}
699
700impl WasmBuildBatchMetrics {
701    /// Number of supplied specifications.
702    #[must_use]
703    pub const fn specifications(self) -> usize {
704        self.specifications
705    }
706
707    /// Number of successful specifications.
708    #[must_use]
709    pub const fn succeeded(self) -> usize {
710        self.succeeded
711    }
712
713    /// Number of failed specifications.
714    #[must_use]
715    pub const fn failed(self) -> usize {
716        self.failed
717    }
718
719    /// Number of newly built Wasm artifact sets.
720    #[must_use]
721    pub const fn built(self) -> usize {
722        self.built
723    }
724
725    /// Number of Wasm artifact sets reused from the exact cache.
726    #[must_use]
727    pub const fn reused(self) -> usize {
728        self.reused
729    }
730
731    /// Number of workspace/toolchain input-resolution snapshots performed.
732    #[must_use]
733    pub const fn input_resolution_runs(self) -> usize {
734        self.input_resolution_runs
735    }
736
737    /// Number of specifications resolved by reusing another batch snapshot.
738    #[must_use]
739    pub const fn input_resolution_reuses(self) -> usize {
740        self.input_resolution_reuses
741    }
742
743    /// Number of specifications resolved from an explicit session snapshot.
744    #[must_use]
745    pub const fn input_resolution_session_reuses(self) -> usize {
746        self.input_resolution_session_reuses
747    }
748
749    /// Number of specifications resolved from a prepared concurrent snapshot.
750    #[must_use]
751    pub const fn input_resolution_prepared_reuses(self) -> usize {
752        self.input_resolution_prepared_reuses
753    }
754
755    /// Sum of timings from successful acquisitions.
756    #[must_use]
757    pub const fn successful_timings(self) -> WasmBuildTimings {
758        self.successful_timings
759    }
760
761    /// Complete wall-clock time for the sequential batch.
762    #[must_use]
763    pub const fn total(self) -> Duration {
764        self.total
765    }
766}
767
768impl WasmBuildBatchConfig {
769    /// Create batch orchestration without batch-owned target maintenance.
770    #[must_use]
771    pub const fn new() -> Self {
772        Self {
773            shared_incremental_maintenance: None,
774        }
775    }
776
777    /// Maintain each distinct resolved shared target once through its first batch entry.
778    #[must_use]
779    pub const fn with_shared_incremental_target_maintenance(
780        mut self,
781        config: SharedIncrementalTargetMaintenanceConfig,
782    ) -> Self {
783        self.shared_incremental_maintenance = Some(config);
784        self
785    }
786
787    /// Strictly maintain each distinct shared target at most once per interval.
788    #[must_use]
789    pub const fn with_shared_incremental_target_maintenance_at_most_every(
790        self,
791        policy: SharedIncrementalTargetPrunePolicy,
792        minimum_interval: Duration,
793    ) -> Self {
794        self.with_shared_incremental_target_maintenance(
795            SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
796        )
797    }
798
799    /// Batch-owned shared-target maintenance, when configured.
800    #[must_use]
801    pub const fn shared_incremental_target_maintenance(
802        self,
803    ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
804        self.shared_incremental_maintenance
805    }
806}
807
808impl std::fmt::Display for WasmBuildBatchReport {
809    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
810        let metrics = self.metrics();
811        write!(
812            formatter,
813            "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} input_resolution_session_reuses={} input_resolution_prepared_reuses={} successful_timings=({}) total={:?}",
814            metrics.specifications(),
815            metrics.succeeded(),
816            metrics.failed(),
817            metrics.built(),
818            metrics.reused(),
819            metrics.input_resolution_runs(),
820            metrics.input_resolution_reuses(),
821            metrics.input_resolution_session_reuses(),
822            metrics.input_resolution_prepared_reuses(),
823            metrics.successful_timings(),
824            metrics.total(),
825        )
826    }
827}
828
829/// Build every Wasm specification as an independent Cargo invocation.
830///
831/// Specifications run sequentially and every result is retained. Each entry
832/// keeps its own package set, profile arguments, feature resolution,
833/// fingerprint, locks, and cache policy. Packages are never combined into one
834/// Cargo command because doing so can unify shared dependency features.
835pub fn build_wasm_canisters_cached_batch(
836    specs: &[LabeledWasmBuildSpec],
837) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
838    build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
839}
840
841/// Build an independent Wasm batch with shared batch orchestration.
842///
843/// Batch-owned maintenance is attached only to the first specification for
844/// each distinct configured shared-target path. Isolated specifications are
845/// unaffected. An entry mixing batch-owned and per-spec integrated maintenance
846/// reports an indexed error without preventing later entries from running.
847pub fn build_wasm_canisters_cached_batch_with_config(
848    specs: &[LabeledWasmBuildSpec],
849    config: WasmBuildBatchConfig,
850) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
851    run_wasm_batch(specs, config, None, None)
852}
853
854/// Build an independent Wasm batch while forwarding structured progress.
855///
856/// The same observation configuration is applied to every entry. Batch events
857/// identify the originating specification without altering the standalone
858/// build semantics.
859pub fn build_wasm_canisters_cached_batch_with_progress<F>(
860    specs: &[LabeledWasmBuildSpec],
861    config: WasmBuildProgressConfig,
862    observer: F,
863) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
864where
865    F: FnMut(WasmBuildBatchProgressEvent),
866{
867    build_wasm_canisters_cached_batch_with_config_and_progress(
868        specs,
869        WasmBuildBatchConfig::new(),
870        config,
871        observer,
872    )
873}
874
875/// Build a configured independent Wasm batch while forwarding structured progress.
876pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
877    specs: &[LabeledWasmBuildSpec],
878    batch_config: WasmBuildBatchConfig,
879    progress_config: WasmBuildProgressConfig,
880    mut observer: F,
881) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
882where
883    F: FnMut(WasmBuildBatchProgressEvent),
884{
885    run_wasm_batch(
886        specs,
887        batch_config,
888        None,
889        Some((progress_config, &mut observer)),
890    )
891}
892
893fn run_wasm_batch(
894    specs: &[LabeledWasmBuildSpec],
895    batch_config: WasmBuildBatchConfig,
896    reuse: Option<WasmBuildInputReuse<'_>>,
897    mut observation: Option<(
898        WasmBuildProgressConfig,
899        &mut dyn FnMut(WasmBuildBatchProgressEvent),
900    )>,
901) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
902    validate_batch_labels(specs)?;
903    validate_input_reuse(specs, reuse.as_ref())?;
904    let count = specs.len();
905    let build_specs = specs
906        .iter()
907        .map(|labeled| labeled.spec.clone())
908        .collect::<Vec<_>>();
909    let mut resolver = WasmBuildBatchInputResolver::new(&build_specs, reuse);
910    let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
911        let Some((progress_config, observer)) = observation.as_mut() else {
912            return build_wasm_canisters_cached_in_batch(spec, index, &mut resolver);
913        };
914        let label = specs[index].label.clone();
915        observer(WasmBuildBatchProgressEvent::BuildStarted {
916            index,
917            label: label.clone(),
918            total: count,
919        });
920        let attempt = build_wasm_canisters_cached_in_batch_with_progress(
921            spec,
922            index,
923            &mut resolver,
924            *progress_config,
925            |event| {
926                observer(WasmBuildBatchProgressEvent::BuildProgress {
927                    index,
928                    label: label.clone(),
929                    event,
930                });
931            },
932        );
933        observer(match &attempt.result {
934            Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
935            Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
936        });
937        attempt
938    });
939    report.input_resolution = resolver.metrics();
940    Ok(report)
941}
942
943fn build_wasm_batch<F>(
944    specs: &[LabeledWasmBuildSpec],
945    config: WasmBuildBatchConfig,
946    mut build: F,
947) -> WasmBuildBatchReport
948where
949    F: FnMut(&WasmBuildSpec, usize) -> WasmBuildBatchAttempt,
950{
951    let started = Instant::now();
952    let mut entries = Vec::with_capacity(specs.len());
953    let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
954    for (index, labeled) in specs.iter().enumerate() {
955        let entry_started = Instant::now();
956        let spec = &labeled.spec;
957        let attempt = if config.shared_incremental_maintenance.is_some()
958            && spec.shared_incremental_target_maintenance().is_some()
959        {
960            WasmBuildBatchAttempt::invalid_spec(
961                batch_maintenance_ownership_error(),
962                entry_started.elapsed(),
963            )
964        } else {
965            match maintenance.prepare_spec(spec) {
966                Ok(configured) => build(configured.as_ref().unwrap_or(spec), index),
967                Err(error) => WasmBuildBatchAttempt::invalid_spec(error, entry_started.elapsed()),
968            }
969        };
970        let failure = attempt
971            .failure
972            .map(|(phase, timings)| WasmBuildFailureDetails { phase, timings });
973        entries.push(WasmBuildBatchEntry {
974            index,
975            label: labeled.label.clone(),
976            result: attempt.result,
977            failure,
978            entry_elapsed: entry_started.elapsed(),
979        });
980    }
981    WasmBuildBatchReport {
982        entries,
983        input_resolution: WasmBuildBatchInputMetrics::default(),
984        total: started.elapsed(),
985    }
986}
987
988fn validate_batch_labels(
989    specs: &[LabeledWasmBuildSpec],
990) -> Result<(), WasmBuildBatchContractError> {
991    validate_labels(specs.iter().map(|labeled| labeled.label.as_str())).map_err(|error| match error
992    {
993        BatchLabelError::Empty { index } => WasmBuildBatchContractError::EmptyLabel { index },
994        BatchLabelError::Duplicate {
995            label,
996            first_index,
997            duplicate_index,
998        } => WasmBuildBatchContractError::DuplicateLabel {
999            label,
1000            first_index,
1001            duplicate_index,
1002        },
1003    })
1004}
1005
1006fn validate_input_reuse(
1007    specs: &[LabeledWasmBuildSpec],
1008    reuse: Option<&WasmBuildInputReuse<'_>>,
1009) -> Result<(), WasmBuildBatchContractError> {
1010    match reuse {
1011        Some(WasmBuildInputReuse::Session(session)) if session.is_invalidated() => {
1012            Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
1013        }
1014        Some(WasmBuildInputReuse::Snapshot(snapshot)) if snapshot.is_invalidated() => {
1015            Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
1016        }
1017        Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
1018            for (index, labeled) in specs.iter().enumerate() {
1019                if !snapshot.contains(&labeled.spec) {
1020                    return Err(WasmBuildBatchContractError::SpecificationNotPrepared {
1021                        index,
1022                        label: labeled.label.clone(),
1023                    });
1024                }
1025            }
1026            Ok(())
1027        }
1028        _ => Ok(()),
1029    }
1030}
1031
1032struct BatchMaintenanceTracker {
1033    config: Option<SharedIncrementalTargetMaintenanceConfig>,
1034    configured_targets: HashSet<PathBuf>,
1035}
1036
1037impl BatchMaintenanceTracker {
1038    fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
1039        Self {
1040            config,
1041            configured_targets: HashSet::new(),
1042        }
1043    }
1044
1045    fn prepare_spec(
1046        &mut self,
1047        spec: &WasmBuildSpec,
1048    ) -> Result<Option<WasmBuildSpec>, WasmBuildError> {
1049        let Some(config) = self.config else {
1050            return Ok(None);
1051        };
1052        debug_assert!(spec.shared_incremental_target_maintenance().is_none());
1053        let Some(target_dir) = shared_incremental_target(spec) else {
1054            return Ok(None);
1055        };
1056        let canonical =
1057            canonicalize_allow_missing(&target_dir).map_err(|source| WasmBuildError::Io {
1058                operation: "resolve batch shared incremental target",
1059                path: target_dir,
1060                source,
1061            })?;
1062        if !self.configured_targets.insert(canonical) {
1063            return Ok(None);
1064        }
1065        Ok(Some(
1066            spec.clone()
1067                .with_shared_incremental_target_maintenance(config),
1068        ))
1069    }
1070}
1071
1072fn batch_maintenance_ownership_error() -> WasmBuildError {
1073    WasmBuildError::InvalidSpec {
1074        message:
1075            "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
1076                .to_owned(),
1077    }
1078}
1079
1080impl std::fmt::Display for WasmBuildBatchContractError {
1081    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1082        match self {
1083            Self::EmptyLabel { index } => {
1084                write!(formatter, "Wasm batch label at index {index} is empty")
1085            }
1086            Self::DuplicateLabel {
1087                label,
1088                first_index,
1089                duplicate_index,
1090            } => write!(
1091                formatter,
1092                "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
1093            ),
1094            Self::SourceLeaseInvalidated => formatter
1095                .write_str("Wasm build source lease was invalidated by a detected input mutation"),
1096            Self::SpecificationNotPrepared { index, label } => write!(
1097                formatter,
1098                "Wasm batch entry {label:?} at index {index} was not declared when the input snapshot was prepared",
1099            ),
1100        }
1101    }
1102}
1103
1104impl std::error::Error for WasmBuildBatchContractError {}
1105
1106#[cfg(test)]
1107mod tests;