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