1use std::{
2 collections::{HashMap, HashSet},
3 path::PathBuf,
4 time::{Duration, Instant},
5};
6
7use super::wasm_cache::{
8 SharedIncrementalTargetMaintenanceConfig, SharedIncrementalTargetMaintenanceOutcome,
9 SharedIncrementalTargetPrunePolicy, WasmBuildBatchInputMetrics, WasmBuildBatchInputResolver,
10 WasmBuildCacheMode, WasmBuildError, WasmBuildOutcome, WasmBuildProgressConfig,
11 WasmBuildProgressEvent, WasmBuildSpec, WasmBuildTimings, build_wasm_canisters_cached_in_batch,
12 build_wasm_canisters_cached_in_batch_with_progress,
13};
14
15#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17pub struct WasmBuildBatchConfig {
18 shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
19}
20
21#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct LabeledWasmBuildSpec {
27 label: String,
28 spec: WasmBuildSpec,
29}
30
31#[derive(Debug)]
33pub struct WasmBuildBatchReport {
34 entries: Vec<WasmBuildBatchEntry>,
35 input_resolution: WasmBuildBatchInputMetrics,
36 total: Duration,
37}
38
39#[derive(Debug)]
41pub struct WasmBuildBatchEntry {
42 index: usize,
43 label: String,
44 result: Result<WasmBuildOutcome, WasmBuildError>,
45 entry_elapsed: Duration,
46}
47
48#[derive(Clone, Copy, Debug)]
50pub struct WasmBuildBatchOutcomeEntry<'a> {
51 index: usize,
52 label: &'a str,
53 outcome: &'a WasmBuildOutcome,
54 entry_elapsed: Duration,
55}
56
57#[derive(Clone, Copy, Debug)]
59pub struct WasmBuildBatchFailure<'a> {
60 index: usize,
61 label: &'a str,
62 error: &'a WasmBuildError,
63 entry_elapsed: Duration,
64}
65
66#[derive(Clone, Copy, Debug)]
68pub struct WasmBuildBatchMaintenanceEntry<'a> {
69 index: usize,
70 label: &'a str,
71 outcome: &'a SharedIncrementalTargetMaintenanceOutcome,
72}
73
74#[non_exhaustive]
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub enum WasmBuildBatchContractError {
78 EmptyLabel {
80 index: usize,
82 },
83 DuplicateLabel {
85 label: String,
87 first_index: usize,
89 duplicate_index: usize,
91 },
92}
93
94impl LabeledWasmBuildSpec {
95 #[must_use]
97 pub fn new(label: impl Into<String>, spec: WasmBuildSpec) -> Self {
98 Self {
99 label: label.into(),
100 spec,
101 }
102 }
103
104 #[must_use]
106 pub fn label(&self) -> &str {
107 &self.label
108 }
109
110 #[must_use]
112 pub const fn spec(&self) -> &WasmBuildSpec {
113 &self.spec
114 }
115
116 #[must_use]
118 pub fn into_parts(self) -> (String, WasmBuildSpec) {
119 (self.label, self.spec)
120 }
121}
122
123impl WasmBuildBatchEntry {
124 #[must_use]
126 pub const fn index(&self) -> usize {
127 self.index
128 }
129
130 #[must_use]
132 pub fn label(&self) -> &str {
133 &self.label
134 }
135
136 pub const fn result(&self) -> Result<&WasmBuildOutcome, &WasmBuildError> {
138 self.result.as_ref()
139 }
140
141 #[must_use]
143 pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
144 self.result.as_ref().ok()
145 }
146
147 #[must_use]
149 pub fn error(&self) -> Option<&WasmBuildError> {
150 self.result.as_ref().err()
151 }
152
153 #[must_use]
155 pub const fn entry_elapsed(&self) -> Duration {
156 self.entry_elapsed
157 }
158
159 #[must_use]
161 pub const fn is_success(&self) -> bool {
162 self.result.is_ok()
163 }
164
165 pub fn into_parts(
167 self,
168 ) -> (
169 usize,
170 String,
171 Result<WasmBuildOutcome, WasmBuildError>,
172 Duration,
173 ) {
174 (self.index, self.label, self.result, self.entry_elapsed)
175 }
176}
177
178impl<'a> WasmBuildBatchOutcomeEntry<'a> {
179 #[must_use]
181 pub const fn index(self) -> usize {
182 self.index
183 }
184
185 #[must_use]
187 pub const fn label(self) -> &'a str {
188 self.label
189 }
190
191 #[must_use]
193 pub const fn outcome(self) -> &'a WasmBuildOutcome {
194 self.outcome
195 }
196
197 #[must_use]
199 pub const fn entry_elapsed(self) -> Duration {
200 self.entry_elapsed
201 }
202}
203
204impl<'a> WasmBuildBatchFailure<'a> {
205 #[must_use]
207 pub const fn index(self) -> usize {
208 self.index
209 }
210
211 #[must_use]
213 pub const fn label(self) -> &'a str {
214 self.label
215 }
216
217 #[must_use]
219 pub const fn error(self) -> &'a WasmBuildError {
220 self.error
221 }
222
223 #[must_use]
225 pub const fn entry_elapsed(self) -> Duration {
226 self.entry_elapsed
227 }
228}
229
230impl<'a> WasmBuildBatchMaintenanceEntry<'a> {
231 #[must_use]
233 pub const fn index(self) -> usize {
234 self.index
235 }
236
237 #[must_use]
239 pub const fn label(self) -> &'a str {
240 self.label
241 }
242
243 #[must_use]
245 pub const fn outcome(self) -> &'a SharedIncrementalTargetMaintenanceOutcome {
246 self.outcome
247 }
248}
249
250#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
252pub struct WasmBuildBatchMetrics {
253 specifications: usize,
254 succeeded: usize,
255 failed: usize,
256 built: usize,
257 reused: usize,
258 input_resolution_runs: usize,
259 input_resolution_reuses: usize,
260 successful_timings: WasmBuildTimings,
261 total: Duration,
262}
263
264#[non_exhaustive]
266#[derive(Clone, Debug, Eq, PartialEq)]
267pub enum WasmBuildBatchProgressEvent {
268 BuildStarted {
270 index: usize,
272 label: String,
274 total: usize,
276 },
277 BuildProgress {
279 index: usize,
281 label: String,
283 event: WasmBuildProgressEvent,
285 },
286 BuildFinished {
288 index: usize,
290 label: String,
292 },
293 BuildFailed {
295 index: usize,
297 label: String,
299 },
300}
301
302impl WasmBuildBatchReport {
303 #[must_use]
305 pub fn entries(&self) -> &[WasmBuildBatchEntry] {
306 &self.entries
307 }
308
309 #[must_use]
311 pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
312 self.entries
313 }
314
315 pub fn outcomes(&self) -> impl Iterator<Item = WasmBuildBatchOutcomeEntry<'_>> {
317 self.entries.iter().filter_map(|entry| {
318 entry.outcome().map(|outcome| WasmBuildBatchOutcomeEntry {
319 index: entry.index,
320 label: &entry.label,
321 outcome,
322 entry_elapsed: entry.entry_elapsed,
323 })
324 })
325 }
326
327 pub fn failures(&self) -> impl Iterator<Item = WasmBuildBatchFailure<'_>> {
329 self.entries.iter().filter_map(|entry| {
330 entry.error().map(|error| WasmBuildBatchFailure {
331 index: entry.index,
332 label: &entry.label,
333 error,
334 entry_elapsed: entry.entry_elapsed,
335 })
336 })
337 }
338
339 pub fn shared_incremental_maintenance_outcomes(
344 &self,
345 ) -> impl Iterator<Item = WasmBuildBatchMaintenanceEntry<'_>> {
346 self.outcomes().filter_map(|entry| {
347 entry
348 .outcome
349 .record()
350 .shared_incremental_maintenance()
351 .map(|outcome| WasmBuildBatchMaintenanceEntry {
352 index: entry.index,
353 label: entry.label,
354 outcome,
355 })
356 })
357 }
358
359 #[must_use]
361 pub const fn total(&self) -> Duration {
362 self.total
363 }
364
365 #[must_use]
367 pub fn is_success(&self) -> bool {
368 self.entries.iter().all(WasmBuildBatchEntry::is_success)
369 }
370
371 #[must_use]
373 pub fn metrics(&self) -> WasmBuildBatchMetrics {
374 let mut metrics = WasmBuildBatchMetrics {
375 specifications: self.entries.len(),
376 input_resolution_runs: self.input_resolution.runs,
377 input_resolution_reuses: self.input_resolution.reuses,
378 total: self.total,
379 ..WasmBuildBatchMetrics::default()
380 };
381 for entry in &self.entries {
382 match &entry.result {
383 Ok(outcome) => {
384 metrics.succeeded += 1;
385 if outcome.is_reused() {
386 metrics.reused += 1;
387 } else {
388 metrics.built += 1;
389 }
390 metrics.successful_timings = metrics
391 .successful_timings
392 .saturating_add(outcome.record().timings());
393 }
394 Err(_) => metrics.failed += 1,
395 }
396 }
397 metrics
398 }
399}
400
401impl WasmBuildBatchMetrics {
402 #[must_use]
404 pub const fn specifications(self) -> usize {
405 self.specifications
406 }
407
408 #[must_use]
410 pub const fn succeeded(self) -> usize {
411 self.succeeded
412 }
413
414 #[must_use]
416 pub const fn failed(self) -> usize {
417 self.failed
418 }
419
420 #[must_use]
422 pub const fn built(self) -> usize {
423 self.built
424 }
425
426 #[must_use]
428 pub const fn reused(self) -> usize {
429 self.reused
430 }
431
432 #[must_use]
434 pub const fn input_resolution_runs(self) -> usize {
435 self.input_resolution_runs
436 }
437
438 #[must_use]
440 pub const fn input_resolution_reuses(self) -> usize {
441 self.input_resolution_reuses
442 }
443
444 #[must_use]
446 pub const fn successful_timings(self) -> WasmBuildTimings {
447 self.successful_timings
448 }
449
450 #[must_use]
452 pub const fn total(self) -> Duration {
453 self.total
454 }
455}
456
457impl WasmBuildBatchConfig {
458 #[must_use]
460 pub const fn new() -> Self {
461 Self {
462 shared_incremental_maintenance: None,
463 }
464 }
465
466 #[must_use]
468 pub const fn with_shared_incremental_target_maintenance(
469 mut self,
470 config: SharedIncrementalTargetMaintenanceConfig,
471 ) -> Self {
472 self.shared_incremental_maintenance = Some(config);
473 self
474 }
475
476 #[must_use]
478 pub const fn with_shared_incremental_target_maintenance_at_most_every(
479 self,
480 policy: SharedIncrementalTargetPrunePolicy,
481 minimum_interval: Duration,
482 ) -> Self {
483 self.with_shared_incremental_target_maintenance(
484 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
485 )
486 }
487
488 #[must_use]
490 pub const fn shared_incremental_target_maintenance(
491 self,
492 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
493 self.shared_incremental_maintenance
494 }
495}
496
497impl std::fmt::Display for WasmBuildBatchReport {
498 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499 let metrics = self.metrics();
500 write!(
501 formatter,
502 "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} successful_timings=({}) total={:?}",
503 metrics.specifications(),
504 metrics.succeeded(),
505 metrics.failed(),
506 metrics.built(),
507 metrics.reused(),
508 metrics.input_resolution_runs(),
509 metrics.input_resolution_reuses(),
510 metrics.successful_timings(),
511 metrics.total(),
512 )
513 }
514}
515
516pub fn build_wasm_canisters_cached_batch(
523 specs: &[LabeledWasmBuildSpec],
524) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
525 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
526}
527
528pub fn build_wasm_canisters_cached_batch_with_config(
535 specs: &[LabeledWasmBuildSpec],
536 config: WasmBuildBatchConfig,
537) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
538 validate_batch_labels(specs)?;
539 let build_specs = specs
540 .iter()
541 .map(|labeled| labeled.spec.clone())
542 .collect::<Vec<_>>();
543 let mut resolver = WasmBuildBatchInputResolver::new(&build_specs);
544 let mut report = build_wasm_batch(specs, config, |spec, index| {
545 build_wasm_canisters_cached_in_batch(spec, index, &mut resolver)
546 });
547 report.input_resolution = resolver.metrics();
548 Ok(report)
549}
550
551pub fn build_wasm_canisters_cached_batch_with_progress<F>(
557 specs: &[LabeledWasmBuildSpec],
558 config: WasmBuildProgressConfig,
559 observer: F,
560) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
561where
562 F: FnMut(WasmBuildBatchProgressEvent),
563{
564 build_wasm_canisters_cached_batch_with_config_and_progress(
565 specs,
566 WasmBuildBatchConfig::new(),
567 config,
568 observer,
569 )
570}
571
572pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
574 specs: &[LabeledWasmBuildSpec],
575 batch_config: WasmBuildBatchConfig,
576 progress_config: WasmBuildProgressConfig,
577 mut observer: F,
578) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
579where
580 F: FnMut(WasmBuildBatchProgressEvent),
581{
582 validate_batch_labels(specs)?;
583 let count = specs.len();
584 let build_specs = specs
585 .iter()
586 .map(|labeled| labeled.spec.clone())
587 .collect::<Vec<_>>();
588 let mut resolver = WasmBuildBatchInputResolver::new(&build_specs);
589 let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
590 let label = specs[index].label.clone();
591 observer(WasmBuildBatchProgressEvent::BuildStarted {
592 index,
593 label: label.clone(),
594 total: count,
595 });
596 let result = build_wasm_canisters_cached_in_batch_with_progress(
597 spec,
598 index,
599 &mut resolver,
600 progress_config,
601 |event| {
602 observer(WasmBuildBatchProgressEvent::BuildProgress {
603 index,
604 label: label.clone(),
605 event,
606 });
607 },
608 );
609 observer(match result {
610 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
611 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
612 });
613 result
614 });
615 report.input_resolution = resolver.metrics();
616 Ok(report)
617}
618
619fn build_wasm_batch<F>(
620 specs: &[LabeledWasmBuildSpec],
621 config: WasmBuildBatchConfig,
622 mut build: F,
623) -> WasmBuildBatchReport
624where
625 F: FnMut(&WasmBuildSpec, usize) -> Result<WasmBuildOutcome, WasmBuildError>,
626{
627 let started = Instant::now();
628 let mut entries = Vec::with_capacity(specs.len());
629 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
630 for (index, labeled) in specs.iter().enumerate() {
631 let entry_started = Instant::now();
632 let spec = &labeled.spec;
633 if config.shared_incremental_maintenance.is_some()
634 && spec.shared_incremental_target_maintenance().is_some()
635 {
636 entries.push(WasmBuildBatchEntry {
637 index,
638 label: labeled.label.clone(),
639 result: Err(batch_maintenance_ownership_error()),
640 entry_elapsed: entry_started.elapsed(),
641 });
642 continue;
643 }
644 let configured = maintenance.prepare_spec(spec);
645 let result = build(configured.as_ref().unwrap_or(spec), index);
646 entries.push(WasmBuildBatchEntry {
647 index,
648 label: labeled.label.clone(),
649 result,
650 entry_elapsed: entry_started.elapsed(),
651 });
652 }
653 WasmBuildBatchReport {
654 entries,
655 input_resolution: WasmBuildBatchInputMetrics::default(),
656 total: started.elapsed(),
657 }
658}
659
660fn validate_batch_labels(
661 specs: &[LabeledWasmBuildSpec],
662) -> Result<(), WasmBuildBatchContractError> {
663 let mut labels = HashMap::with_capacity(specs.len());
664 for (index, labeled) in specs.iter().enumerate() {
665 if labeled.label.is_empty() {
666 return Err(WasmBuildBatchContractError::EmptyLabel { index });
667 }
668 if let Some(first_index) = labels.get(labeled.label.as_str()) {
669 return Err(WasmBuildBatchContractError::DuplicateLabel {
670 label: labeled.label.clone(),
671 first_index: *first_index,
672 duplicate_index: index,
673 });
674 }
675 labels.insert(labeled.label.as_str(), index);
676 }
677 Ok(())
678}
679
680struct BatchMaintenanceTracker {
681 config: Option<SharedIncrementalTargetMaintenanceConfig>,
682 configured_targets: HashSet<PathBuf>,
683}
684
685impl BatchMaintenanceTracker {
686 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
687 Self {
688 config,
689 configured_targets: HashSet::new(),
690 }
691 }
692
693 fn prepare_spec(&mut self, spec: &WasmBuildSpec) -> Option<WasmBuildSpec> {
694 let config = self.config?;
695 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
696 let WasmBuildCacheMode::SharedIncremental { target_dir } = spec.cache_mode() else {
697 return None;
698 };
699 if !self.configured_targets.insert(target_dir.clone()) {
700 return None;
701 }
702 Some(
703 spec.clone()
704 .with_shared_incremental_target_maintenance(config),
705 )
706 }
707}
708
709fn batch_maintenance_ownership_error() -> WasmBuildError {
710 WasmBuildError::InvalidSpec {
711 message:
712 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
713 .to_owned(),
714 }
715}
716
717impl std::fmt::Display for WasmBuildBatchContractError {
718 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
719 match self {
720 Self::EmptyLabel { index } => {
721 write!(formatter, "Wasm batch label at index {index} is empty")
722 }
723 Self::DuplicateLabel {
724 label,
725 first_index,
726 duplicate_index,
727 } => write!(
728 formatter,
729 "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
730 ),
731 }
732 }
733}
734
735impl std::error::Error for WasmBuildBatchContractError {}
736
737#[cfg(test)]
738mod tests;