1use std::{
2 collections::{HashMap, HashSet},
3 marker::PhantomData,
4 path::PathBuf,
5 time::{Duration, Instant},
6};
7
8use super::wasm_cache::{
9 SharedIncrementalTargetMaintenanceConfig, SharedIncrementalTargetMaintenanceOutcome,
10 SharedIncrementalTargetPrunePolicy, WasmBuildBatchAttempt, WasmBuildBatchInputMetrics,
11 WasmBuildBatchInputResolver, WasmBuildCacheMode, WasmBuildError, WasmBuildFailurePhase,
12 WasmBuildFailureTimings, WasmBuildInputSnapshotState, WasmBuildOutcome,
13 WasmBuildProgressConfig, WasmBuildProgressEvent, WasmBuildSessionState, WasmBuildSpec,
14 WasmBuildTimings, WasmInputResolutionTimings, build_wasm_canisters_cached_in_batch,
15 build_wasm_canisters_cached_in_batch_with_progress,
16};
17
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub struct WasmBuildBatchConfig {
21 shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct LabeledWasmBuildSpec {
30 label: String,
31 spec: WasmBuildSpec,
32}
33
34#[derive(Debug)]
36pub struct WasmBuildBatchReport {
37 entries: Vec<WasmBuildBatchEntry>,
38 input_resolution: WasmBuildBatchInputMetrics,
39 total: Duration,
40}
41
42pub struct WasmBuildSession<'guard> {
48 state: WasmBuildSessionState,
49 _source_guard: PhantomData<&'guard ()>,
50}
51
52pub struct WasmBuildInputSnapshot<'guard> {
59 state: WasmBuildInputSnapshotState,
60 _source_guard: PhantomData<&'guard ()>,
61}
62
63#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
65pub struct WasmBuildSessionMetrics {
66 snapshots: usize,
67 snapshot_reuses: usize,
68 invalidated: bool,
69}
70
71#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
73pub struct WasmBuildInputSnapshotMetrics {
74 specifications: usize,
75 input_resolution_runs: usize,
76 input_resolution_reuses: usize,
77 input_resolution_timings: WasmInputResolutionTimings,
78 reader_reuses: usize,
79 invalidated: bool,
80}
81
82#[derive(Debug)]
84pub struct WasmBuildBatchEntry {
85 index: usize,
86 label: String,
87 result: Result<WasmBuildOutcome, WasmBuildError>,
88 failure: Option<WasmBuildFailureDetails>,
89 entry_elapsed: Duration,
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub struct WasmBuildFailureDetails {
95 phase: WasmBuildFailurePhase,
96 timings: WasmBuildFailureTimings,
97}
98
99#[derive(Clone, Copy, Debug)]
101pub struct WasmBuildBatchOutcomeEntry<'a> {
102 index: usize,
103 label: &'a str,
104 outcome: &'a WasmBuildOutcome,
105 entry_elapsed: Duration,
106}
107
108#[derive(Clone, Copy, Debug)]
110pub struct WasmBuildBatchFailure<'a> {
111 index: usize,
112 label: &'a str,
113 error: &'a WasmBuildError,
114 details: WasmBuildFailureDetails,
115 entry_elapsed: Duration,
116}
117
118#[derive(Clone, Copy, Debug)]
120pub struct WasmBuildBatchMaintenanceEntry<'a> {
121 index: usize,
122 label: &'a str,
123 outcome: &'a SharedIncrementalTargetMaintenanceOutcome,
124}
125
126#[non_exhaustive]
128#[derive(Clone, Debug, Eq, PartialEq)]
129pub enum WasmBuildBatchContractError {
130 EmptyLabel {
132 index: usize,
134 },
135 DuplicateLabel {
137 label: String,
139 first_index: usize,
141 duplicate_index: usize,
143 },
144 SourceLeaseInvalidated,
146 SpecificationNotPrepared {
148 index: usize,
150 label: String,
152 },
153}
154
155impl LabeledWasmBuildSpec {
156 #[must_use]
158 pub fn new(label: impl Into<String>, spec: WasmBuildSpec) -> Self {
159 Self {
160 label: label.into(),
161 spec,
162 }
163 }
164
165 #[must_use]
167 pub fn label(&self) -> &str {
168 &self.label
169 }
170
171 #[must_use]
173 pub const fn spec(&self) -> &WasmBuildSpec {
174 &self.spec
175 }
176
177 #[must_use]
179 pub fn into_parts(self) -> (String, WasmBuildSpec) {
180 (self.label, self.spec)
181 }
182}
183
184impl<'guard> WasmBuildSession<'guard> {
185 #[must_use]
194 pub fn assume_sources_immutable<Guard: ?Sized>(_source_write_guard: &'guard Guard) -> Self {
195 Self {
196 state: WasmBuildSessionState::new(),
197 _source_guard: PhantomData,
198 }
199 }
200
201 pub fn build_batch(
203 &mut self,
204 specs: &[LabeledWasmBuildSpec],
205 config: WasmBuildBatchConfig,
206 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
207 build_wasm_canisters_cached_batch_with_session(specs, config, &mut self.state)
208 }
209
210 pub fn build_batch_with_progress<F>(
212 &mut self,
213 specs: &[LabeledWasmBuildSpec],
214 batch_config: WasmBuildBatchConfig,
215 progress_config: WasmBuildProgressConfig,
216 observer: F,
217 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
218 where
219 F: FnMut(WasmBuildBatchProgressEvent),
220 {
221 build_wasm_canisters_cached_batch_with_session_and_progress(
222 specs,
223 batch_config,
224 progress_config,
225 &mut self.state,
226 observer,
227 )
228 }
229
230 #[must_use]
232 pub const fn metrics(&self) -> WasmBuildSessionMetrics {
233 WasmBuildSessionMetrics {
234 snapshots: self.state.snapshot_count(),
235 snapshot_reuses: self.state.snapshot_reuses(),
236 invalidated: self.state.is_invalidated(),
237 }
238 }
239}
240
241impl WasmBuildSessionMetrics {
242 #[must_use]
244 pub const fn snapshots(self) -> usize {
245 self.snapshots
246 }
247
248 #[must_use]
250 pub const fn snapshot_reuses(self) -> usize {
251 self.snapshot_reuses
252 }
253
254 #[must_use]
256 pub const fn is_invalidated(self) -> bool {
257 self.invalidated
258 }
259}
260
261impl<'guard> WasmBuildInputSnapshot<'guard> {
262 pub fn prepare_assuming_sources_immutable<Guard: ?Sized>(
269 _source_write_guard: &'guard Guard,
270 specs: &[WasmBuildSpec],
271 ) -> Result<Self, WasmBuildError> {
272 Ok(Self {
273 state: WasmBuildInputSnapshotState::prepare(specs)?,
274 _source_guard: PhantomData,
275 })
276 }
277
278 pub fn build_batch(
283 &self,
284 specs: &[LabeledWasmBuildSpec],
285 config: WasmBuildBatchConfig,
286 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
287 build_wasm_canisters_cached_batch_with_snapshot(specs, config, &self.state)
288 }
289
290 pub fn build_batch_with_progress<F>(
294 &self,
295 specs: &[LabeledWasmBuildSpec],
296 batch_config: WasmBuildBatchConfig,
297 progress_config: WasmBuildProgressConfig,
298 observer: F,
299 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
300 where
301 F: FnMut(WasmBuildBatchProgressEvent),
302 {
303 build_wasm_canisters_cached_batch_with_snapshot_and_progress(
304 specs,
305 batch_config,
306 progress_config,
307 &self.state,
308 observer,
309 )
310 }
311
312 #[must_use]
314 pub fn metrics(&self) -> WasmBuildInputSnapshotMetrics {
315 let preparation = self.state.preparation_metrics();
316 WasmBuildInputSnapshotMetrics {
317 specifications: self.state.specification_count(),
318 input_resolution_runs: preparation.runs,
319 input_resolution_reuses: preparation.reuses,
320 input_resolution_timings: self.state.preparation_timings(),
321 reader_reuses: self.state.reader_reuses(),
322 invalidated: self.state.is_invalidated(),
323 }
324 }
325}
326
327impl WasmBuildInputSnapshotMetrics {
328 #[must_use]
330 pub const fn specifications(self) -> usize {
331 self.specifications
332 }
333
334 #[must_use]
336 pub const fn input_resolution_runs(self) -> usize {
337 self.input_resolution_runs
338 }
339
340 #[must_use]
342 pub const fn input_resolution_reuses(self) -> usize {
343 self.input_resolution_reuses
344 }
345
346 #[must_use]
348 pub const fn input_resolution_timings(self) -> WasmInputResolutionTimings {
349 self.input_resolution_timings
350 }
351
352 #[must_use]
354 pub const fn reader_reuses(self) -> usize {
355 self.reader_reuses
356 }
357
358 #[must_use]
360 pub const fn is_invalidated(self) -> bool {
361 self.invalidated
362 }
363}
364
365impl WasmBuildBatchEntry {
366 #[must_use]
368 pub const fn index(&self) -> usize {
369 self.index
370 }
371
372 #[must_use]
374 pub fn label(&self) -> &str {
375 &self.label
376 }
377
378 pub const fn result(&self) -> Result<&WasmBuildOutcome, &WasmBuildError> {
380 self.result.as_ref()
381 }
382
383 #[must_use]
385 pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
386 self.result.as_ref().ok()
387 }
388
389 #[must_use]
391 pub fn error(&self) -> Option<&WasmBuildError> {
392 self.result.as_ref().err()
393 }
394
395 #[must_use]
397 pub const fn failure_details(&self) -> Option<WasmBuildFailureDetails> {
398 self.failure
399 }
400
401 #[must_use]
403 pub const fn entry_elapsed(&self) -> Duration {
404 self.entry_elapsed
405 }
406
407 #[must_use]
409 pub const fn is_success(&self) -> bool {
410 self.result.is_ok()
411 }
412
413 pub fn into_parts(
415 self,
416 ) -> (
417 usize,
418 String,
419 Result<WasmBuildOutcome, WasmBuildError>,
420 Option<WasmBuildFailureDetails>,
421 Duration,
422 ) {
423 (
424 self.index,
425 self.label,
426 self.result,
427 self.failure,
428 self.entry_elapsed,
429 )
430 }
431}
432
433impl WasmBuildFailureDetails {
434 #[must_use]
436 pub const fn phase(self) -> WasmBuildFailurePhase {
437 self.phase
438 }
439
440 #[must_use]
442 pub const fn timings(self) -> WasmBuildFailureTimings {
443 self.timings
444 }
445}
446
447impl<'a> WasmBuildBatchOutcomeEntry<'a> {
448 #[must_use]
450 pub const fn index(self) -> usize {
451 self.index
452 }
453
454 #[must_use]
456 pub const fn label(self) -> &'a str {
457 self.label
458 }
459
460 #[must_use]
462 pub const fn outcome(self) -> &'a WasmBuildOutcome {
463 self.outcome
464 }
465
466 #[must_use]
468 pub const fn entry_elapsed(self) -> Duration {
469 self.entry_elapsed
470 }
471}
472
473impl<'a> WasmBuildBatchFailure<'a> {
474 #[must_use]
476 pub const fn index(self) -> usize {
477 self.index
478 }
479
480 #[must_use]
482 pub const fn label(self) -> &'a str {
483 self.label
484 }
485
486 #[must_use]
488 pub const fn error(self) -> &'a WasmBuildError {
489 self.error
490 }
491
492 #[must_use]
494 pub const fn phase(self) -> WasmBuildFailurePhase {
495 self.details.phase
496 }
497
498 #[must_use]
500 pub const fn timings(self) -> WasmBuildFailureTimings {
501 self.details.timings
502 }
503
504 #[must_use]
506 pub const fn entry_elapsed(self) -> Duration {
507 self.entry_elapsed
508 }
509}
510
511impl<'a> WasmBuildBatchMaintenanceEntry<'a> {
512 #[must_use]
514 pub const fn index(self) -> usize {
515 self.index
516 }
517
518 #[must_use]
520 pub const fn label(self) -> &'a str {
521 self.label
522 }
523
524 #[must_use]
526 pub const fn outcome(self) -> &'a SharedIncrementalTargetMaintenanceOutcome {
527 self.outcome
528 }
529}
530
531#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
533pub struct WasmBuildBatchMetrics {
534 specifications: usize,
535 succeeded: usize,
536 failed: usize,
537 built: usize,
538 reused: usize,
539 input_resolution_runs: usize,
540 input_resolution_reuses: usize,
541 input_resolution_session_reuses: usize,
542 input_resolution_prepared_reuses: usize,
543 successful_timings: WasmBuildTimings,
544 total: Duration,
545}
546
547#[non_exhaustive]
549#[derive(Clone, Debug, Eq, PartialEq)]
550pub enum WasmBuildBatchProgressEvent {
551 BuildStarted {
553 index: usize,
555 label: String,
557 total: usize,
559 },
560 BuildProgress {
562 index: usize,
564 label: String,
566 event: WasmBuildProgressEvent,
568 },
569 BuildFinished {
571 index: usize,
573 label: String,
575 },
576 BuildFailed {
578 index: usize,
580 label: String,
582 },
583}
584
585impl WasmBuildBatchReport {
586 #[must_use]
588 pub fn entries(&self) -> &[WasmBuildBatchEntry] {
589 &self.entries
590 }
591
592 #[must_use]
594 pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
595 self.entries
596 }
597
598 pub fn outcomes(&self) -> impl Iterator<Item = WasmBuildBatchOutcomeEntry<'_>> {
600 self.entries.iter().filter_map(|entry| {
601 entry.outcome().map(|outcome| WasmBuildBatchOutcomeEntry {
602 index: entry.index,
603 label: &entry.label,
604 outcome,
605 entry_elapsed: entry.entry_elapsed,
606 })
607 })
608 }
609
610 pub fn failures(&self) -> impl Iterator<Item = WasmBuildBatchFailure<'_>> {
612 self.entries.iter().filter_map(|entry| {
613 entry.error().map(|error| WasmBuildBatchFailure {
614 index: entry.index,
615 label: &entry.label,
616 error,
617 details: entry
618 .failure
619 .expect("failed Wasm batch entry must retain failure details"),
620 entry_elapsed: entry.entry_elapsed,
621 })
622 })
623 }
624
625 pub fn shared_incremental_maintenance_outcomes(
630 &self,
631 ) -> impl Iterator<Item = WasmBuildBatchMaintenanceEntry<'_>> {
632 self.outcomes().filter_map(|entry| {
633 entry
634 .outcome
635 .record()
636 .shared_incremental_maintenance()
637 .map(|outcome| WasmBuildBatchMaintenanceEntry {
638 index: entry.index,
639 label: entry.label,
640 outcome,
641 })
642 })
643 }
644
645 #[must_use]
647 pub const fn total(&self) -> Duration {
648 self.total
649 }
650
651 #[must_use]
653 pub fn is_success(&self) -> bool {
654 self.entries.iter().all(WasmBuildBatchEntry::is_success)
655 }
656
657 #[must_use]
659 pub fn metrics(&self) -> WasmBuildBatchMetrics {
660 let mut metrics = WasmBuildBatchMetrics {
661 specifications: self.entries.len(),
662 input_resolution_runs: self.input_resolution.runs,
663 input_resolution_reuses: self.input_resolution.reuses,
664 input_resolution_session_reuses: self.input_resolution.session_reuses,
665 input_resolution_prepared_reuses: self.input_resolution.prepared_reuses,
666 total: self.total,
667 ..WasmBuildBatchMetrics::default()
668 };
669 for entry in &self.entries {
670 match &entry.result {
671 Ok(outcome) => {
672 metrics.succeeded += 1;
673 if outcome.is_reused() {
674 metrics.reused += 1;
675 } else {
676 metrics.built += 1;
677 }
678 metrics.successful_timings = metrics
679 .successful_timings
680 .saturating_add(outcome.record().timings());
681 }
682 Err(_) => metrics.failed += 1,
683 }
684 }
685 metrics
686 }
687}
688
689impl WasmBuildBatchMetrics {
690 #[must_use]
692 pub const fn specifications(self) -> usize {
693 self.specifications
694 }
695
696 #[must_use]
698 pub const fn succeeded(self) -> usize {
699 self.succeeded
700 }
701
702 #[must_use]
704 pub const fn failed(self) -> usize {
705 self.failed
706 }
707
708 #[must_use]
710 pub const fn built(self) -> usize {
711 self.built
712 }
713
714 #[must_use]
716 pub const fn reused(self) -> usize {
717 self.reused
718 }
719
720 #[must_use]
722 pub const fn input_resolution_runs(self) -> usize {
723 self.input_resolution_runs
724 }
725
726 #[must_use]
728 pub const fn input_resolution_reuses(self) -> usize {
729 self.input_resolution_reuses
730 }
731
732 #[must_use]
734 pub const fn input_resolution_session_reuses(self) -> usize {
735 self.input_resolution_session_reuses
736 }
737
738 #[must_use]
740 pub const fn input_resolution_prepared_reuses(self) -> usize {
741 self.input_resolution_prepared_reuses
742 }
743
744 #[must_use]
746 pub const fn successful_timings(self) -> WasmBuildTimings {
747 self.successful_timings
748 }
749
750 #[must_use]
752 pub const fn total(self) -> Duration {
753 self.total
754 }
755}
756
757impl WasmBuildBatchConfig {
758 #[must_use]
760 pub const fn new() -> Self {
761 Self {
762 shared_incremental_maintenance: None,
763 }
764 }
765
766 #[must_use]
768 pub const fn with_shared_incremental_target_maintenance(
769 mut self,
770 config: SharedIncrementalTargetMaintenanceConfig,
771 ) -> Self {
772 self.shared_incremental_maintenance = Some(config);
773 self
774 }
775
776 #[must_use]
778 pub const fn with_shared_incremental_target_maintenance_at_most_every(
779 self,
780 policy: SharedIncrementalTargetPrunePolicy,
781 minimum_interval: Duration,
782 ) -> Self {
783 self.with_shared_incremental_target_maintenance(
784 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
785 )
786 }
787
788 #[must_use]
790 pub const fn shared_incremental_target_maintenance(
791 self,
792 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
793 self.shared_incremental_maintenance
794 }
795}
796
797impl std::fmt::Display for WasmBuildBatchReport {
798 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
799 let metrics = self.metrics();
800 write!(
801 formatter,
802 "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} input_resolution_session_reuses={} input_resolution_prepared_reuses={} successful_timings=({}) total={:?}",
803 metrics.specifications(),
804 metrics.succeeded(),
805 metrics.failed(),
806 metrics.built(),
807 metrics.reused(),
808 metrics.input_resolution_runs(),
809 metrics.input_resolution_reuses(),
810 metrics.input_resolution_session_reuses(),
811 metrics.input_resolution_prepared_reuses(),
812 metrics.successful_timings(),
813 metrics.total(),
814 )
815 }
816}
817
818pub fn build_wasm_canisters_cached_batch(
825 specs: &[LabeledWasmBuildSpec],
826) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
827 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
828}
829
830pub fn build_wasm_canisters_cached_batch_with_config(
837 specs: &[LabeledWasmBuildSpec],
838 config: WasmBuildBatchConfig,
839) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
840 build_wasm_canisters_cached_batch_internal(specs, config, None)
841}
842
843enum WasmBuildInputReuse<'reuse> {
844 Session(&'reuse mut WasmBuildSessionState),
845 Snapshot(&'reuse WasmBuildInputSnapshotState),
846}
847
848fn build_wasm_canisters_cached_batch_with_session(
849 specs: &[LabeledWasmBuildSpec],
850 config: WasmBuildBatchConfig,
851 session: &mut WasmBuildSessionState,
852) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
853 build_wasm_canisters_cached_batch_internal(
854 specs,
855 config,
856 Some(WasmBuildInputReuse::Session(session)),
857 )
858}
859
860fn build_wasm_canisters_cached_batch_with_snapshot(
861 specs: &[LabeledWasmBuildSpec],
862 config: WasmBuildBatchConfig,
863 snapshot: &WasmBuildInputSnapshotState,
864) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
865 build_wasm_canisters_cached_batch_internal(
866 specs,
867 config,
868 Some(WasmBuildInputReuse::Snapshot(snapshot)),
869 )
870}
871
872fn build_wasm_canisters_cached_batch_internal(
873 specs: &[LabeledWasmBuildSpec],
874 config: WasmBuildBatchConfig,
875 reuse: Option<WasmBuildInputReuse<'_>>,
876) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
877 validate_batch_labels(specs)?;
878 validate_input_reuse(specs, reuse.as_ref())?;
879 let build_specs = specs
880 .iter()
881 .map(|labeled| labeled.spec.clone())
882 .collect::<Vec<_>>();
883 let mut resolver = match reuse {
884 None => WasmBuildBatchInputResolver::new(&build_specs),
885 Some(WasmBuildInputReuse::Session(session)) => {
886 WasmBuildBatchInputResolver::with_session(&build_specs, session)
887 }
888 Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
889 WasmBuildBatchInputResolver::with_snapshot(&build_specs, snapshot)
890 }
891 };
892 let mut report = build_wasm_batch(specs, config, |spec, index| {
893 build_wasm_canisters_cached_in_batch(spec, index, &mut resolver)
894 });
895 report.input_resolution = resolver.metrics();
896 Ok(report)
897}
898
899pub fn build_wasm_canisters_cached_batch_with_progress<F>(
905 specs: &[LabeledWasmBuildSpec],
906 config: WasmBuildProgressConfig,
907 observer: F,
908) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
909where
910 F: FnMut(WasmBuildBatchProgressEvent),
911{
912 build_wasm_canisters_cached_batch_with_config_and_progress(
913 specs,
914 WasmBuildBatchConfig::new(),
915 config,
916 observer,
917 )
918}
919
920pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
922 specs: &[LabeledWasmBuildSpec],
923 batch_config: WasmBuildBatchConfig,
924 progress_config: WasmBuildProgressConfig,
925 observer: F,
926) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
927where
928 F: FnMut(WasmBuildBatchProgressEvent),
929{
930 build_wasm_canisters_cached_batch_with_progress_internal(
931 specs,
932 batch_config,
933 progress_config,
934 None,
935 observer,
936 )
937}
938
939fn build_wasm_canisters_cached_batch_with_session_and_progress<F>(
940 specs: &[LabeledWasmBuildSpec],
941 batch_config: WasmBuildBatchConfig,
942 progress_config: WasmBuildProgressConfig,
943 session: &mut WasmBuildSessionState,
944 observer: F,
945) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
946where
947 F: FnMut(WasmBuildBatchProgressEvent),
948{
949 build_wasm_canisters_cached_batch_with_progress_internal(
950 specs,
951 batch_config,
952 progress_config,
953 Some(WasmBuildInputReuse::Session(session)),
954 observer,
955 )
956}
957
958fn build_wasm_canisters_cached_batch_with_snapshot_and_progress<F>(
959 specs: &[LabeledWasmBuildSpec],
960 batch_config: WasmBuildBatchConfig,
961 progress_config: WasmBuildProgressConfig,
962 snapshot: &WasmBuildInputSnapshotState,
963 observer: F,
964) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
965where
966 F: FnMut(WasmBuildBatchProgressEvent),
967{
968 build_wasm_canisters_cached_batch_with_progress_internal(
969 specs,
970 batch_config,
971 progress_config,
972 Some(WasmBuildInputReuse::Snapshot(snapshot)),
973 observer,
974 )
975}
976
977fn build_wasm_canisters_cached_batch_with_progress_internal<F>(
978 specs: &[LabeledWasmBuildSpec],
979 batch_config: WasmBuildBatchConfig,
980 progress_config: WasmBuildProgressConfig,
981 reuse: Option<WasmBuildInputReuse<'_>>,
982 mut observer: F,
983) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
984where
985 F: FnMut(WasmBuildBatchProgressEvent),
986{
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 label = specs[index].label.clone();
1005 observer(WasmBuildBatchProgressEvent::BuildStarted {
1006 index,
1007 label: label.clone(),
1008 total: count,
1009 });
1010 let attempt = build_wasm_canisters_cached_in_batch_with_progress(
1011 spec,
1012 index,
1013 &mut resolver,
1014 progress_config,
1015 |event| {
1016 observer(WasmBuildBatchProgressEvent::BuildProgress {
1017 index,
1018 label: label.clone(),
1019 event,
1020 });
1021 },
1022 );
1023 observer(match &attempt.result {
1024 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
1025 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
1026 });
1027 attempt
1028 });
1029 report.input_resolution = resolver.metrics();
1030 Ok(report)
1031}
1032
1033fn build_wasm_batch<F>(
1034 specs: &[LabeledWasmBuildSpec],
1035 config: WasmBuildBatchConfig,
1036 mut build: F,
1037) -> WasmBuildBatchReport
1038where
1039 F: FnMut(&WasmBuildSpec, usize) -> WasmBuildBatchAttempt,
1040{
1041 let started = Instant::now();
1042 let mut entries = Vec::with_capacity(specs.len());
1043 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
1044 for (index, labeled) in specs.iter().enumerate() {
1045 let entry_started = Instant::now();
1046 let spec = &labeled.spec;
1047 if config.shared_incremental_maintenance.is_some()
1048 && spec.shared_incremental_target_maintenance().is_some()
1049 {
1050 let elapsed = entry_started.elapsed();
1051 let attempt =
1052 WasmBuildBatchAttempt::invalid_spec(batch_maintenance_ownership_error(), elapsed);
1053 entries.push(WasmBuildBatchEntry {
1054 index,
1055 label: labeled.label.clone(),
1056 result: attempt.result,
1057 failure: Some(WasmBuildFailureDetails {
1058 phase: attempt
1059 .failure_phase
1060 .expect("invalid batch entry must retain its failure phase"),
1061 timings: attempt
1062 .failure_timings
1063 .expect("invalid batch entry must retain its failure timings"),
1064 }),
1065 entry_elapsed: elapsed,
1066 });
1067 continue;
1068 }
1069 let configured = maintenance.prepare_spec(spec);
1070 let attempt = build(configured.as_ref().unwrap_or(spec), index);
1071 let failure = attempt
1072 .failure_phase
1073 .zip(attempt.failure_timings)
1074 .map(|(phase, timings)| WasmBuildFailureDetails { phase, timings });
1075 entries.push(WasmBuildBatchEntry {
1076 index,
1077 label: labeled.label.clone(),
1078 result: attempt.result,
1079 failure,
1080 entry_elapsed: entry_started.elapsed(),
1081 });
1082 }
1083 WasmBuildBatchReport {
1084 entries,
1085 input_resolution: WasmBuildBatchInputMetrics::default(),
1086 total: started.elapsed(),
1087 }
1088}
1089
1090fn validate_batch_labels(
1091 specs: &[LabeledWasmBuildSpec],
1092) -> Result<(), WasmBuildBatchContractError> {
1093 let mut labels = HashMap::with_capacity(specs.len());
1094 for (index, labeled) in specs.iter().enumerate() {
1095 if labeled.label.is_empty() {
1096 return Err(WasmBuildBatchContractError::EmptyLabel { index });
1097 }
1098 if let Some(first_index) = labels.get(labeled.label.as_str()) {
1099 return Err(WasmBuildBatchContractError::DuplicateLabel {
1100 label: labeled.label.clone(),
1101 first_index: *first_index,
1102 duplicate_index: index,
1103 });
1104 }
1105 labels.insert(labeled.label.as_str(), index);
1106 }
1107 Ok(())
1108}
1109
1110fn validate_input_reuse(
1111 specs: &[LabeledWasmBuildSpec],
1112 reuse: Option<&WasmBuildInputReuse<'_>>,
1113) -> Result<(), WasmBuildBatchContractError> {
1114 match reuse {
1115 Some(WasmBuildInputReuse::Session(session)) if session.is_invalidated() => {
1116 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
1117 }
1118 Some(WasmBuildInputReuse::Snapshot(snapshot)) if snapshot.is_invalidated() => {
1119 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
1120 }
1121 Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
1122 for (index, labeled) in specs.iter().enumerate() {
1123 if !snapshot.contains(&labeled.spec) {
1124 return Err(WasmBuildBatchContractError::SpecificationNotPrepared {
1125 index,
1126 label: labeled.label.clone(),
1127 });
1128 }
1129 }
1130 Ok(())
1131 }
1132 _ => Ok(()),
1133 }
1134}
1135
1136struct BatchMaintenanceTracker {
1137 config: Option<SharedIncrementalTargetMaintenanceConfig>,
1138 configured_targets: HashSet<PathBuf>,
1139}
1140
1141impl BatchMaintenanceTracker {
1142 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
1143 Self {
1144 config,
1145 configured_targets: HashSet::new(),
1146 }
1147 }
1148
1149 fn prepare_spec(&mut self, spec: &WasmBuildSpec) -> Option<WasmBuildSpec> {
1150 let config = self.config?;
1151 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
1152 let WasmBuildCacheMode::SharedIncremental { target_dir } = spec.cache_mode() else {
1153 return None;
1154 };
1155 if !self.configured_targets.insert(target_dir.clone()) {
1156 return None;
1157 }
1158 Some(
1159 spec.clone()
1160 .with_shared_incremental_target_maintenance(config),
1161 )
1162 }
1163}
1164
1165fn batch_maintenance_ownership_error() -> WasmBuildError {
1166 WasmBuildError::InvalidSpec {
1167 message:
1168 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
1169 .to_owned(),
1170 }
1171}
1172
1173impl std::fmt::Display for WasmBuildBatchContractError {
1174 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1175 match self {
1176 Self::EmptyLabel { index } => {
1177 write!(formatter, "Wasm batch label at index {index} is empty")
1178 }
1179 Self::DuplicateLabel {
1180 label,
1181 first_index,
1182 duplicate_index,
1183 } => write!(
1184 formatter,
1185 "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
1186 ),
1187 Self::SourceLeaseInvalidated => formatter
1188 .write_str("Wasm build source lease was invalidated by a detected input mutation"),
1189 Self::SpecificationNotPrepared { index, label } => write!(
1190 formatter,
1191 "Wasm batch entry {label:?} at index {index} was not declared when the input snapshot was prepared",
1192 ),
1193 }
1194 }
1195}
1196
1197impl std::error::Error for WasmBuildBatchContractError {}
1198
1199#[cfg(test)]
1200mod tests;