1use std::{
2 collections::HashSet,
3 path::PathBuf,
4 time::{Duration, Instant},
5};
6
7use super::{
8 batch::{indexed_failures, indexed_outcomes},
9 wasm_cache::{
10 SharedIncrementalTargetMaintenanceConfig, SharedIncrementalTargetMaintenanceOutcome,
11 SharedIncrementalTargetPrunePolicy, WasmBuildBatchInputMetrics,
12 WasmBuildBatchInputResolver, WasmBuildCacheMode, WasmBuildError, WasmBuildOutcome,
13 WasmBuildProgressConfig, WasmBuildProgressEvent, WasmBuildSpec, WasmBuildTimings,
14 build_wasm_canisters_cached_in_batch, build_wasm_canisters_cached_in_batch_with_progress,
15 },
16};
17
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub struct WasmBuildBatchConfig {
21 shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
22}
23
24#[derive(Debug)]
26pub struct WasmBuildBatchReport {
27 results: Vec<Result<WasmBuildOutcome, WasmBuildError>>,
28 entry_elapsed: Vec<Duration>,
29 input_resolution: WasmBuildBatchInputMetrics,
30 total: Duration,
31}
32
33#[derive(Clone, Copy, Debug)]
35pub struct WasmBuildBatchFailure<'a> {
36 index: usize,
37 error: &'a WasmBuildError,
38 entry_elapsed: Duration,
39}
40
41impl<'a> WasmBuildBatchFailure<'a> {
42 #[must_use]
44 pub const fn index(self) -> usize {
45 self.index
46 }
47
48 #[must_use]
50 pub const fn error(self) -> &'a WasmBuildError {
51 self.error
52 }
53
54 #[must_use]
56 pub const fn entry_elapsed(self) -> Duration {
57 self.entry_elapsed
58 }
59}
60
61#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
63pub struct WasmBuildBatchMetrics {
64 specifications: usize,
65 succeeded: usize,
66 failed: usize,
67 built: usize,
68 reused: usize,
69 input_resolution_runs: usize,
70 input_resolution_reuses: usize,
71 successful_timings: WasmBuildTimings,
72 total: Duration,
73}
74
75#[non_exhaustive]
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum WasmBuildBatchProgressEvent {
79 BuildStarted {
81 index: usize,
83 total: usize,
85 },
86 BuildProgress {
88 index: usize,
90 event: WasmBuildProgressEvent,
92 },
93 BuildFinished {
95 index: usize,
97 },
98 BuildFailed {
100 index: usize,
102 },
103}
104
105impl WasmBuildBatchReport {
106 pub fn results(&self) -> &[Result<WasmBuildOutcome, WasmBuildError>] {
108 &self.results
109 }
110
111 #[must_use]
116 pub fn entry_elapsed(&self) -> &[Duration] {
117 &self.entry_elapsed
118 }
119
120 #[must_use]
122 pub fn into_results(self) -> Vec<Result<WasmBuildOutcome, WasmBuildError>> {
123 self.results
124 }
125
126 pub fn outcomes(&self) -> impl Iterator<Item = (usize, &WasmBuildOutcome)> {
128 indexed_outcomes(&self.results)
129 }
130
131 pub fn failures(&self) -> impl Iterator<Item = WasmBuildBatchFailure<'_>> {
133 indexed_failures(&self.results).map(|(index, error)| WasmBuildBatchFailure {
134 index,
135 error,
136 entry_elapsed: self.entry_elapsed[index],
137 })
138 }
139
140 pub fn shared_incremental_maintenance_outcomes(
145 &self,
146 ) -> impl Iterator<Item = (usize, &SharedIncrementalTargetMaintenanceOutcome)> {
147 self.outcomes().filter_map(|(index, outcome)| {
148 outcome
149 .record()
150 .shared_incremental_maintenance()
151 .map(|maintenance| (index, maintenance))
152 })
153 }
154
155 #[must_use]
157 pub const fn total(&self) -> Duration {
158 self.total
159 }
160
161 #[must_use]
163 pub fn is_success(&self) -> bool {
164 self.results.iter().all(Result::is_ok)
165 }
166
167 #[must_use]
169 pub fn metrics(&self) -> WasmBuildBatchMetrics {
170 let mut metrics = WasmBuildBatchMetrics {
171 specifications: self.results.len(),
172 input_resolution_runs: self.input_resolution.runs,
173 input_resolution_reuses: self.input_resolution.reuses,
174 total: self.total,
175 ..WasmBuildBatchMetrics::default()
176 };
177 for result in &self.results {
178 match result {
179 Ok(outcome) => {
180 metrics.succeeded += 1;
181 if outcome.is_reused() {
182 metrics.reused += 1;
183 } else {
184 metrics.built += 1;
185 }
186 metrics.successful_timings = metrics
187 .successful_timings
188 .saturating_add(outcome.record().timings());
189 }
190 Err(_) => metrics.failed += 1,
191 }
192 }
193 metrics
194 }
195}
196
197impl WasmBuildBatchMetrics {
198 #[must_use]
200 pub const fn specifications(self) -> usize {
201 self.specifications
202 }
203
204 #[must_use]
206 pub const fn succeeded(self) -> usize {
207 self.succeeded
208 }
209
210 #[must_use]
212 pub const fn failed(self) -> usize {
213 self.failed
214 }
215
216 #[must_use]
218 pub const fn built(self) -> usize {
219 self.built
220 }
221
222 #[must_use]
224 pub const fn reused(self) -> usize {
225 self.reused
226 }
227
228 #[must_use]
230 pub const fn input_resolution_runs(self) -> usize {
231 self.input_resolution_runs
232 }
233
234 #[must_use]
236 pub const fn input_resolution_reuses(self) -> usize {
237 self.input_resolution_reuses
238 }
239
240 #[must_use]
242 pub const fn successful_timings(self) -> WasmBuildTimings {
243 self.successful_timings
244 }
245
246 #[must_use]
248 pub const fn total(self) -> Duration {
249 self.total
250 }
251}
252
253impl WasmBuildBatchConfig {
254 #[must_use]
256 pub const fn new() -> Self {
257 Self {
258 shared_incremental_maintenance: None,
259 }
260 }
261
262 #[must_use]
264 pub const fn with_shared_incremental_target_maintenance(
265 mut self,
266 config: SharedIncrementalTargetMaintenanceConfig,
267 ) -> Self {
268 self.shared_incremental_maintenance = Some(config);
269 self
270 }
271
272 #[must_use]
274 pub const fn with_shared_incremental_target_maintenance_at_most_every(
275 self,
276 policy: SharedIncrementalTargetPrunePolicy,
277 minimum_interval: Duration,
278 ) -> Self {
279 self.with_shared_incremental_target_maintenance(
280 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
281 )
282 }
283
284 #[must_use]
286 pub const fn shared_incremental_target_maintenance(
287 self,
288 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
289 self.shared_incremental_maintenance
290 }
291}
292
293impl std::fmt::Display for WasmBuildBatchReport {
294 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 let metrics = self.metrics();
296 write!(
297 formatter,
298 "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} successful_timings=({}) total={:?}",
299 metrics.specifications(),
300 metrics.succeeded(),
301 metrics.failed(),
302 metrics.built(),
303 metrics.reused(),
304 metrics.input_resolution_runs(),
305 metrics.input_resolution_reuses(),
306 metrics.successful_timings(),
307 metrics.total(),
308 )
309 }
310}
311
312#[must_use]
319pub fn build_wasm_canisters_cached_batch(specs: &[WasmBuildSpec]) -> WasmBuildBatchReport {
320 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
321}
322
323#[must_use]
330pub fn build_wasm_canisters_cached_batch_with_config(
331 specs: &[WasmBuildSpec],
332 config: WasmBuildBatchConfig,
333) -> WasmBuildBatchReport {
334 let mut resolver = WasmBuildBatchInputResolver::new(specs);
335 let mut report = build_wasm_batch(specs, config, |spec, index| {
336 build_wasm_canisters_cached_in_batch(spec, index, &mut resolver)
337 });
338 report.input_resolution = resolver.metrics();
339 report
340}
341
342#[must_use]
348pub fn build_wasm_canisters_cached_batch_with_progress<F>(
349 specs: &[WasmBuildSpec],
350 config: WasmBuildProgressConfig,
351 observer: F,
352) -> WasmBuildBatchReport
353where
354 F: FnMut(WasmBuildBatchProgressEvent),
355{
356 build_wasm_canisters_cached_batch_with_config_and_progress(
357 specs,
358 WasmBuildBatchConfig::new(),
359 config,
360 observer,
361 )
362}
363
364#[must_use]
366pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
367 specs: &[WasmBuildSpec],
368 batch_config: WasmBuildBatchConfig,
369 progress_config: WasmBuildProgressConfig,
370 mut observer: F,
371) -> WasmBuildBatchReport
372where
373 F: FnMut(WasmBuildBatchProgressEvent),
374{
375 let count = specs.len();
376 let mut resolver = WasmBuildBatchInputResolver::new(specs);
377 let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
378 observer(WasmBuildBatchProgressEvent::BuildStarted {
379 index,
380 total: count,
381 });
382 let result = build_wasm_canisters_cached_in_batch_with_progress(
383 spec,
384 index,
385 &mut resolver,
386 progress_config,
387 |event| observer(WasmBuildBatchProgressEvent::BuildProgress { index, event }),
388 );
389 observer(match result {
390 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index },
391 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index },
392 });
393 result
394 });
395 report.input_resolution = resolver.metrics();
396 report
397}
398
399fn build_wasm_batch<F>(
400 specs: &[WasmBuildSpec],
401 config: WasmBuildBatchConfig,
402 mut build: F,
403) -> WasmBuildBatchReport
404where
405 F: FnMut(&WasmBuildSpec, usize) -> Result<WasmBuildOutcome, WasmBuildError>,
406{
407 let started = Instant::now();
408 let mut results = Vec::with_capacity(specs.len());
409 let mut entry_elapsed = Vec::with_capacity(specs.len());
410 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
411 for (index, spec) in specs.iter().enumerate() {
412 let entry_started = Instant::now();
413 if config.shared_incremental_maintenance.is_some()
414 && spec.shared_incremental_target_maintenance().is_some()
415 {
416 results.push(Err(batch_maintenance_ownership_error()));
417 entry_elapsed.push(entry_started.elapsed());
418 continue;
419 }
420 let configured = maintenance.prepare_spec(spec);
421 results.push(build(configured.as_ref().unwrap_or(spec), index));
422 entry_elapsed.push(entry_started.elapsed());
423 }
424 WasmBuildBatchReport {
425 results,
426 entry_elapsed,
427 input_resolution: WasmBuildBatchInputMetrics::default(),
428 total: started.elapsed(),
429 }
430}
431
432struct BatchMaintenanceTracker {
433 config: Option<SharedIncrementalTargetMaintenanceConfig>,
434 configured_targets: HashSet<PathBuf>,
435}
436
437impl BatchMaintenanceTracker {
438 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
439 Self {
440 config,
441 configured_targets: HashSet::new(),
442 }
443 }
444
445 fn prepare_spec(&mut self, spec: &WasmBuildSpec) -> Option<WasmBuildSpec> {
446 let config = self.config?;
447 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
448 let WasmBuildCacheMode::SharedIncremental { target_dir } = spec.cache_mode() else {
449 return None;
450 };
451 if !self.configured_targets.insert(target_dir.clone()) {
452 return None;
453 }
454 Some(
455 spec.clone()
456 .with_shared_incremental_target_maintenance(config),
457 )
458 }
459}
460
461fn batch_maintenance_ownership_error() -> WasmBuildError {
462 WasmBuildError::InvalidSpec {
463 message:
464 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
465 .to_owned(),
466 }
467}
468
469#[cfg(test)]
470mod tests;