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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
22pub struct WasmBuildBatchConfig {
23 shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct LabeledWasmBuildSpec {
32 label: String,
33 spec: WasmBuildSpec,
34}
35
36#[derive(Debug)]
38pub struct WasmBuildBatchReport {
39 entries: Vec<WasmBuildBatchEntry>,
40 input_resolution: WasmBuildBatchInputMetrics,
41 total: Duration,
42}
43
44pub struct WasmBuildSession<'guard> {
50 state: WasmBuildSessionState,
51 _source_guard: PhantomData<&'guard ()>,
52}
53
54pub struct WasmBuildInputSnapshot<'guard> {
61 state: WasmBuildInputSnapshotState,
62 _source_guard: PhantomData<&'guard ()>,
63}
64
65#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
67pub struct WasmBuildSessionMetrics {
68 snapshots: usize,
69 snapshot_reuses: usize,
70 invalidated: bool,
71}
72
73#[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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub struct WasmBuildFailureDetails {
97 phase: WasmBuildFailurePhase,
98 timings: WasmBuildFailureTimings,
99}
100
101#[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#[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#[derive(Clone, Copy, Debug)]
122pub struct WasmBuildBatchMaintenanceEntry<'a> {
123 index: usize,
124 label: &'a str,
125 outcome: &'a SharedIncrementalTargetMaintenanceOutcome,
126}
127
128#[non_exhaustive]
130#[derive(Clone, Debug, Eq, PartialEq)]
131pub enum WasmBuildBatchContractError {
132 EmptyLabel {
134 index: usize,
136 },
137 DuplicateLabel {
139 label: String,
141 first_index: usize,
143 duplicate_index: usize,
145 },
146 SourceLeaseInvalidated,
148 SpecificationNotPrepared {
150 index: usize,
152 label: String,
154 },
155}
156
157impl LabeledWasmBuildSpec {
158 #[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 #[must_use]
169 pub fn label(&self) -> &str {
170 &self.label
171 }
172
173 #[must_use]
175 pub const fn spec(&self) -> &WasmBuildSpec {
176 &self.spec
177 }
178
179 #[must_use]
181 pub fn into_parts(self) -> (String, WasmBuildSpec) {
182 (self.label, self.spec)
183 }
184}
185
186impl<'guard> WasmBuildSession<'guard> {
187 #[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 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 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 #[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 #[must_use]
246 pub const fn snapshots(self) -> usize {
247 self.snapshots
248 }
249
250 #[must_use]
252 pub const fn snapshot_reuses(self) -> usize {
253 self.snapshot_reuses
254 }
255
256 #[must_use]
258 pub const fn is_invalidated(self) -> bool {
259 self.invalidated
260 }
261}
262
263impl<'guard> WasmBuildInputSnapshot<'guard> {
264 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 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 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 #[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 #[must_use]
332 pub const fn specifications(self) -> usize {
333 self.specifications
334 }
335
336 #[must_use]
338 pub const fn input_resolution_runs(self) -> usize {
339 self.input_resolution_runs
340 }
341
342 #[must_use]
344 pub const fn input_resolution_reuses(self) -> usize {
345 self.input_resolution_reuses
346 }
347
348 #[must_use]
350 pub const fn input_resolution_timings(self) -> WasmInputResolutionTimings {
351 self.input_resolution_timings
352 }
353
354 #[must_use]
356 pub const fn reader_reuses(self) -> usize {
357 self.reader_reuses
358 }
359
360 #[must_use]
362 pub const fn is_invalidated(self) -> bool {
363 self.invalidated
364 }
365}
366
367impl WasmBuildBatchEntry {
368 #[must_use]
370 pub const fn index(&self) -> usize {
371 self.index
372 }
373
374 #[must_use]
376 pub fn label(&self) -> &str {
377 &self.label
378 }
379
380 pub const fn result(&self) -> Result<&WasmBuildOutcome, &WasmBuildError> {
382 self.result.as_ref()
383 }
384
385 #[must_use]
387 pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
388 self.result.as_ref().ok()
389 }
390
391 #[must_use]
393 pub fn error(&self) -> Option<&WasmBuildError> {
394 self.result.as_ref().err()
395 }
396
397 #[must_use]
399 pub const fn failure_details(&self) -> Option<WasmBuildFailureDetails> {
400 self.failure
401 }
402
403 #[must_use]
405 pub const fn entry_elapsed(&self) -> Duration {
406 self.entry_elapsed
407 }
408
409 #[must_use]
411 pub const fn is_success(&self) -> bool {
412 self.result.is_ok()
413 }
414
415 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 #[must_use]
438 pub const fn phase(self) -> WasmBuildFailurePhase {
439 self.phase
440 }
441
442 #[must_use]
444 pub const fn timings(self) -> WasmBuildFailureTimings {
445 self.timings
446 }
447}
448
449impl<'a> WasmBuildBatchOutcomeEntry<'a> {
450 #[must_use]
452 pub const fn index(self) -> usize {
453 self.index
454 }
455
456 #[must_use]
458 pub const fn label(self) -> &'a str {
459 self.label
460 }
461
462 #[must_use]
464 pub const fn outcome(self) -> &'a WasmBuildOutcome {
465 self.outcome
466 }
467
468 #[must_use]
470 pub const fn entry_elapsed(self) -> Duration {
471 self.entry_elapsed
472 }
473}
474
475impl<'a> WasmBuildBatchFailure<'a> {
476 #[must_use]
478 pub const fn index(self) -> usize {
479 self.index
480 }
481
482 #[must_use]
484 pub const fn label(self) -> &'a str {
485 self.label
486 }
487
488 #[must_use]
490 pub const fn error(self) -> &'a WasmBuildError {
491 self.error
492 }
493
494 #[must_use]
496 pub const fn phase(self) -> WasmBuildFailurePhase {
497 self.details.phase
498 }
499
500 #[must_use]
502 pub const fn timings(self) -> WasmBuildFailureTimings {
503 self.details.timings
504 }
505
506 #[must_use]
508 pub const fn entry_elapsed(self) -> Duration {
509 self.entry_elapsed
510 }
511}
512
513impl<'a> WasmBuildBatchMaintenanceEntry<'a> {
514 #[must_use]
516 pub const fn index(self) -> usize {
517 self.index
518 }
519
520 #[must_use]
522 pub const fn label(self) -> &'a str {
523 self.label
524 }
525
526 #[must_use]
528 pub const fn outcome(self) -> &'a SharedIncrementalTargetMaintenanceOutcome {
529 self.outcome
530 }
531}
532
533#[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#[non_exhaustive]
551#[derive(Clone, Debug, Eq, PartialEq)]
552pub enum WasmBuildBatchProgressEvent {
553 BuildStarted {
555 index: usize,
557 label: String,
559 total: usize,
561 },
562 BuildProgress {
564 index: usize,
566 label: String,
568 event: WasmBuildProgressEvent,
570 },
571 BuildFinished {
573 index: usize,
575 label: String,
577 },
578 BuildFailed {
580 index: usize,
582 label: String,
584 },
585}
586
587impl WasmBuildBatchReport {
588 #[must_use]
590 pub fn entries(&self) -> &[WasmBuildBatchEntry] {
591 &self.entries
592 }
593
594 #[must_use]
596 pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
597 self.entries
598 }
599
600 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 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 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 #[must_use]
649 pub const fn total(&self) -> Duration {
650 self.total
651 }
652
653 #[must_use]
655 pub fn is_success(&self) -> bool {
656 self.entries.iter().all(WasmBuildBatchEntry::is_success)
657 }
658
659 #[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 #[must_use]
694 pub const fn specifications(self) -> usize {
695 self.specifications
696 }
697
698 #[must_use]
700 pub const fn succeeded(self) -> usize {
701 self.succeeded
702 }
703
704 #[must_use]
706 pub const fn failed(self) -> usize {
707 self.failed
708 }
709
710 #[must_use]
712 pub const fn built(self) -> usize {
713 self.built
714 }
715
716 #[must_use]
718 pub const fn reused(self) -> usize {
719 self.reused
720 }
721
722 #[must_use]
724 pub const fn input_resolution_runs(self) -> usize {
725 self.input_resolution_runs
726 }
727
728 #[must_use]
730 pub const fn input_resolution_reuses(self) -> usize {
731 self.input_resolution_reuses
732 }
733
734 #[must_use]
736 pub const fn input_resolution_session_reuses(self) -> usize {
737 self.input_resolution_session_reuses
738 }
739
740 #[must_use]
742 pub const fn input_resolution_prepared_reuses(self) -> usize {
743 self.input_resolution_prepared_reuses
744 }
745
746 #[must_use]
748 pub const fn successful_timings(self) -> WasmBuildTimings {
749 self.successful_timings
750 }
751
752 #[must_use]
754 pub const fn total(self) -> Duration {
755 self.total
756 }
757}
758
759impl WasmBuildBatchConfig {
760 #[must_use]
762 pub const fn new() -> Self {
763 Self {
764 shared_incremental_maintenance: None,
765 }
766 }
767
768 #[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 #[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 #[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
820pub 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
832pub 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
882pub 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
903pub 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;