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, Default, Eq, PartialEq)]
35pub struct WasmBuildBatchMetrics {
36 specifications: usize,
37 succeeded: usize,
38 failed: usize,
39 built: usize,
40 reused: usize,
41 input_resolution_runs: usize,
42 input_resolution_reuses: usize,
43 successful_timings: WasmBuildTimings,
44 total: Duration,
45}
46
47#[non_exhaustive]
49#[derive(Clone, Debug, Eq, PartialEq)]
50pub enum WasmBuildBatchProgressEvent {
51 BuildStarted {
53 index: usize,
55 total: usize,
57 },
58 BuildProgress {
60 index: usize,
62 event: WasmBuildProgressEvent,
64 },
65 BuildFinished {
67 index: usize,
69 },
70 BuildFailed {
72 index: usize,
74 },
75}
76
77impl WasmBuildBatchReport {
78 pub fn results(&self) -> &[Result<WasmBuildOutcome, WasmBuildError>] {
80 &self.results
81 }
82
83 #[must_use]
88 pub fn entry_elapsed(&self) -> &[Duration] {
89 &self.entry_elapsed
90 }
91
92 #[must_use]
94 pub fn into_results(self) -> Vec<Result<WasmBuildOutcome, WasmBuildError>> {
95 self.results
96 }
97
98 pub fn outcomes(&self) -> impl Iterator<Item = (usize, &WasmBuildOutcome)> {
100 indexed_outcomes(&self.results)
101 }
102
103 pub fn failures(&self) -> impl Iterator<Item = (usize, &WasmBuildError)> {
105 indexed_failures(&self.results)
106 }
107
108 pub fn shared_incremental_maintenance_outcomes(
113 &self,
114 ) -> impl Iterator<Item = (usize, &SharedIncrementalTargetMaintenanceOutcome)> {
115 self.outcomes().filter_map(|(index, outcome)| {
116 outcome
117 .record()
118 .shared_incremental_maintenance()
119 .map(|maintenance| (index, maintenance))
120 })
121 }
122
123 #[must_use]
125 pub const fn total(&self) -> Duration {
126 self.total
127 }
128
129 #[must_use]
131 pub fn is_success(&self) -> bool {
132 self.results.iter().all(Result::is_ok)
133 }
134
135 #[must_use]
137 pub fn metrics(&self) -> WasmBuildBatchMetrics {
138 let mut metrics = WasmBuildBatchMetrics {
139 specifications: self.results.len(),
140 input_resolution_runs: self.input_resolution.runs,
141 input_resolution_reuses: self.input_resolution.reuses,
142 total: self.total,
143 ..WasmBuildBatchMetrics::default()
144 };
145 for result in &self.results {
146 match result {
147 Ok(outcome) => {
148 metrics.succeeded += 1;
149 if outcome.is_reused() {
150 metrics.reused += 1;
151 } else {
152 metrics.built += 1;
153 }
154 metrics.successful_timings = metrics
155 .successful_timings
156 .saturating_add(outcome.record().timings());
157 }
158 Err(_) => metrics.failed += 1,
159 }
160 }
161 metrics
162 }
163}
164
165impl WasmBuildBatchMetrics {
166 #[must_use]
168 pub const fn specifications(self) -> usize {
169 self.specifications
170 }
171
172 #[must_use]
174 pub const fn succeeded(self) -> usize {
175 self.succeeded
176 }
177
178 #[must_use]
180 pub const fn failed(self) -> usize {
181 self.failed
182 }
183
184 #[must_use]
186 pub const fn built(self) -> usize {
187 self.built
188 }
189
190 #[must_use]
192 pub const fn reused(self) -> usize {
193 self.reused
194 }
195
196 #[must_use]
198 pub const fn input_resolution_runs(self) -> usize {
199 self.input_resolution_runs
200 }
201
202 #[must_use]
204 pub const fn input_resolution_reuses(self) -> usize {
205 self.input_resolution_reuses
206 }
207
208 #[must_use]
210 pub const fn successful_timings(self) -> WasmBuildTimings {
211 self.successful_timings
212 }
213
214 #[must_use]
216 pub const fn total(self) -> Duration {
217 self.total
218 }
219}
220
221impl WasmBuildBatchConfig {
222 #[must_use]
224 pub const fn new() -> Self {
225 Self {
226 shared_incremental_maintenance: None,
227 }
228 }
229
230 #[must_use]
232 pub const fn with_shared_incremental_target_maintenance(
233 mut self,
234 config: SharedIncrementalTargetMaintenanceConfig,
235 ) -> Self {
236 self.shared_incremental_maintenance = Some(config);
237 self
238 }
239
240 #[must_use]
242 pub const fn with_shared_incremental_target_maintenance_at_most_every(
243 self,
244 policy: SharedIncrementalTargetPrunePolicy,
245 minimum_interval: Duration,
246 ) -> Self {
247 self.with_shared_incremental_target_maintenance(
248 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
249 )
250 }
251
252 #[must_use]
254 pub const fn shared_incremental_target_maintenance(
255 self,
256 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
257 self.shared_incremental_maintenance
258 }
259}
260
261impl std::fmt::Display for WasmBuildBatchReport {
262 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 let metrics = self.metrics();
264 write!(
265 formatter,
266 "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} successful_timings=({}) total={:?}",
267 metrics.specifications(),
268 metrics.succeeded(),
269 metrics.failed(),
270 metrics.built(),
271 metrics.reused(),
272 metrics.input_resolution_runs(),
273 metrics.input_resolution_reuses(),
274 metrics.successful_timings(),
275 metrics.total(),
276 )
277 }
278}
279
280#[must_use]
287pub fn build_wasm_canisters_cached_batch(specs: &[WasmBuildSpec]) -> WasmBuildBatchReport {
288 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
289}
290
291#[must_use]
298pub fn build_wasm_canisters_cached_batch_with_config(
299 specs: &[WasmBuildSpec],
300 config: WasmBuildBatchConfig,
301) -> WasmBuildBatchReport {
302 let mut resolver = WasmBuildBatchInputResolver::new(specs);
303 let mut report = build_wasm_batch(specs, config, |spec, index| {
304 build_wasm_canisters_cached_in_batch(spec, index, &mut resolver)
305 });
306 report.input_resolution = resolver.metrics();
307 report
308}
309
310#[must_use]
316pub fn build_wasm_canisters_cached_batch_with_progress<F>(
317 specs: &[WasmBuildSpec],
318 config: WasmBuildProgressConfig,
319 observer: F,
320) -> WasmBuildBatchReport
321where
322 F: FnMut(WasmBuildBatchProgressEvent),
323{
324 build_wasm_canisters_cached_batch_with_config_and_progress(
325 specs,
326 WasmBuildBatchConfig::new(),
327 config,
328 observer,
329 )
330}
331
332#[must_use]
334pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
335 specs: &[WasmBuildSpec],
336 batch_config: WasmBuildBatchConfig,
337 progress_config: WasmBuildProgressConfig,
338 mut observer: F,
339) -> WasmBuildBatchReport
340where
341 F: FnMut(WasmBuildBatchProgressEvent),
342{
343 let count = specs.len();
344 let mut resolver = WasmBuildBatchInputResolver::new(specs);
345 let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
346 observer(WasmBuildBatchProgressEvent::BuildStarted {
347 index,
348 total: count,
349 });
350 let result = build_wasm_canisters_cached_in_batch_with_progress(
351 spec,
352 index,
353 &mut resolver,
354 progress_config,
355 |event| observer(WasmBuildBatchProgressEvent::BuildProgress { index, event }),
356 );
357 observer(match result {
358 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index },
359 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index },
360 });
361 result
362 });
363 report.input_resolution = resolver.metrics();
364 report
365}
366
367fn build_wasm_batch<F>(
368 specs: &[WasmBuildSpec],
369 config: WasmBuildBatchConfig,
370 mut build: F,
371) -> WasmBuildBatchReport
372where
373 F: FnMut(&WasmBuildSpec, usize) -> Result<WasmBuildOutcome, WasmBuildError>,
374{
375 let started = Instant::now();
376 let mut results = Vec::with_capacity(specs.len());
377 let mut entry_elapsed = Vec::with_capacity(specs.len());
378 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
379 for (index, spec) in specs.iter().enumerate() {
380 let entry_started = Instant::now();
381 if config.shared_incremental_maintenance.is_some()
382 && spec.shared_incremental_target_maintenance().is_some()
383 {
384 results.push(Err(batch_maintenance_ownership_error()));
385 entry_elapsed.push(entry_started.elapsed());
386 continue;
387 }
388 let configured = maintenance.prepare_spec(spec);
389 results.push(build(configured.as_ref().unwrap_or(spec), index));
390 entry_elapsed.push(entry_started.elapsed());
391 }
392 WasmBuildBatchReport {
393 results,
394 entry_elapsed,
395 input_resolution: WasmBuildBatchInputMetrics::default(),
396 total: started.elapsed(),
397 }
398}
399
400struct BatchMaintenanceTracker {
401 config: Option<SharedIncrementalTargetMaintenanceConfig>,
402 configured_targets: HashSet<PathBuf>,
403}
404
405impl BatchMaintenanceTracker {
406 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
407 Self {
408 config,
409 configured_targets: HashSet::new(),
410 }
411 }
412
413 fn prepare_spec(&mut self, spec: &WasmBuildSpec) -> Option<WasmBuildSpec> {
414 let config = self.config?;
415 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
416 let WasmBuildCacheMode::SharedIncremental { target_dir } = spec.cache_mode() else {
417 return None;
418 };
419 if !self.configured_targets.insert(target_dir.clone()) {
420 return None;
421 }
422 Some(
423 spec.clone()
424 .with_shared_incremental_target_maintenance(config),
425 )
426 }
427}
428
429fn batch_maintenance_ownership_error() -> WasmBuildError {
430 WasmBuildError::InvalidSpec {
431 message:
432 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
433 .to_owned(),
434 }
435}
436
437#[cfg(test)]
438mod tests;