Skip to main content

ic_testkit/artifacts/
wasm_batch.rs

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