1use std::{
2 collections::HashSet,
3 path::PathBuf,
4 time::{Duration, Instant},
5};
6
7use super::wasm_cache::{
8 SharedIncrementalTargetMaintenanceConfig, SharedIncrementalTargetMaintenanceOutcome,
9 SharedIncrementalTargetPrunePolicy, WasmBuildBatchInputResolver, WasmBuildCacheMode,
10 WasmBuildError, WasmBuildOutcome, WasmBuildProgressConfig, WasmBuildProgressEvent,
11 WasmBuildSpec, 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 total: Duration,
26}
27
28#[non_exhaustive]
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum WasmBuildBatchProgressEvent {
32 BuildStarted {
34 index: usize,
36 total: usize,
38 },
39 BuildProgress {
41 index: usize,
43 event: WasmBuildProgressEvent,
45 },
46 BuildFinished {
48 index: usize,
50 },
51 BuildFailed {
53 index: usize,
55 },
56}
57
58impl WasmBuildBatchReport {
59 pub fn results(&self) -> &[Result<WasmBuildOutcome, WasmBuildError>] {
61 &self.results
62 }
63
64 #[must_use]
66 pub fn into_results(self) -> Vec<Result<WasmBuildOutcome, WasmBuildError>> {
67 self.results
68 }
69
70 pub fn outcomes(&self) -> impl Iterator<Item = (usize, &WasmBuildOutcome)> {
72 self.results
73 .iter()
74 .enumerate()
75 .filter_map(|(index, result)| result.as_ref().ok().map(|outcome| (index, outcome)))
76 }
77
78 pub fn failures(&self) -> impl Iterator<Item = (usize, &WasmBuildError)> {
80 self.results
81 .iter()
82 .enumerate()
83 .filter_map(|(index, result)| result.as_ref().err().map(|error| (index, error)))
84 }
85
86 pub fn shared_incremental_maintenance_outcomes(
91 &self,
92 ) -> impl Iterator<Item = (usize, &SharedIncrementalTargetMaintenanceOutcome)> {
93 self.outcomes().filter_map(|(index, outcome)| {
94 outcome
95 .record()
96 .shared_incremental_maintenance()
97 .map(|maintenance| (index, maintenance))
98 })
99 }
100
101 #[must_use]
103 pub const fn total(&self) -> Duration {
104 self.total
105 }
106
107 #[must_use]
109 pub fn is_success(&self) -> bool {
110 self.results.iter().all(Result::is_ok)
111 }
112}
113
114impl WasmBuildBatchConfig {
115 #[must_use]
117 pub const fn new() -> Self {
118 Self {
119 shared_incremental_maintenance: None,
120 }
121 }
122
123 #[must_use]
125 pub const fn with_shared_incremental_target_maintenance(
126 mut self,
127 config: SharedIncrementalTargetMaintenanceConfig,
128 ) -> Self {
129 self.shared_incremental_maintenance = Some(config);
130 self
131 }
132
133 #[must_use]
135 pub const fn with_shared_incremental_target_maintenance_at_most_every(
136 self,
137 policy: SharedIncrementalTargetPrunePolicy,
138 minimum_interval: Duration,
139 ) -> Self {
140 self.with_shared_incremental_target_maintenance(
141 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
142 )
143 }
144
145 #[must_use]
147 pub const fn shared_incremental_target_maintenance(
148 self,
149 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
150 self.shared_incremental_maintenance
151 }
152}
153
154impl std::fmt::Display for WasmBuildBatchReport {
155 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 let succeeded = self.results.iter().filter(|result| result.is_ok()).count();
157 let reused = self
158 .results
159 .iter()
160 .filter_map(|result| result.as_ref().ok())
161 .filter(|outcome| outcome.is_reused())
162 .count();
163 write!(
164 formatter,
165 "builds={} succeeded={} failed={} built={} reused={} total={:?}",
166 self.results.len(),
167 succeeded,
168 self.results.len().saturating_sub(succeeded),
169 succeeded.saturating_sub(reused),
170 reused,
171 self.total,
172 )
173 }
174}
175
176#[must_use]
183pub fn build_wasm_canisters_cached_batch(specs: &[WasmBuildSpec]) -> WasmBuildBatchReport {
184 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
185}
186
187#[must_use]
194pub fn build_wasm_canisters_cached_batch_with_config(
195 specs: &[WasmBuildSpec],
196 config: WasmBuildBatchConfig,
197) -> WasmBuildBatchReport {
198 let mut resolver = WasmBuildBatchInputResolver::new(specs);
199 build_wasm_batch(specs, config, |spec, index| {
200 build_wasm_canisters_cached_in_batch(spec, index, &mut resolver)
201 })
202}
203
204pub fn build_wasm_canisters_cached_batch_with_progress<F>(
210 specs: &[WasmBuildSpec],
211 config: WasmBuildProgressConfig,
212 observer: F,
213) -> WasmBuildBatchReport
214where
215 F: FnMut(WasmBuildBatchProgressEvent),
216{
217 build_wasm_canisters_cached_batch_with_config_and_progress(
218 specs,
219 WasmBuildBatchConfig::new(),
220 config,
221 observer,
222 )
223}
224
225pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
227 specs: &[WasmBuildSpec],
228 batch_config: WasmBuildBatchConfig,
229 progress_config: WasmBuildProgressConfig,
230 mut observer: F,
231) -> WasmBuildBatchReport
232where
233 F: FnMut(WasmBuildBatchProgressEvent),
234{
235 let count = specs.len();
236 let mut resolver = WasmBuildBatchInputResolver::new(specs);
237 build_wasm_batch(specs, batch_config, |spec, index| {
238 observer(WasmBuildBatchProgressEvent::BuildStarted {
239 index,
240 total: count,
241 });
242 let result = build_wasm_canisters_cached_in_batch_with_progress(
243 spec,
244 index,
245 &mut resolver,
246 progress_config,
247 |event| observer(WasmBuildBatchProgressEvent::BuildProgress { index, event }),
248 );
249 observer(match result {
250 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index },
251 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index },
252 });
253 result
254 })
255}
256
257fn build_wasm_batch<F>(
258 specs: &[WasmBuildSpec],
259 config: WasmBuildBatchConfig,
260 mut build: F,
261) -> WasmBuildBatchReport
262where
263 F: FnMut(&WasmBuildSpec, usize) -> Result<WasmBuildOutcome, WasmBuildError>,
264{
265 let started = Instant::now();
266 let mut results = Vec::with_capacity(specs.len());
267 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
268 for (index, spec) in specs.iter().enumerate() {
269 if config.shared_incremental_maintenance.is_some()
270 && spec.shared_incremental_target_maintenance().is_some()
271 {
272 results.push(Err(batch_maintenance_ownership_error()));
273 continue;
274 }
275 let configured = maintenance.prepare_spec(spec);
276 results.push(build(configured.as_ref().unwrap_or(spec), index));
277 }
278 WasmBuildBatchReport {
279 results,
280 total: started.elapsed(),
281 }
282}
283
284struct BatchMaintenanceTracker {
285 config: Option<SharedIncrementalTargetMaintenanceConfig>,
286 configured_targets: HashSet<PathBuf>,
287}
288
289impl BatchMaintenanceTracker {
290 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
291 Self {
292 config,
293 configured_targets: HashSet::new(),
294 }
295 }
296
297 fn prepare_spec(&mut self, spec: &WasmBuildSpec) -> Option<WasmBuildSpec> {
298 let config = self.config?;
299 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
300 let WasmBuildCacheMode::SharedIncremental { target_dir } = spec.cache_mode() else {
301 return None;
302 };
303 if !self.configured_targets.insert(target_dir.clone()) {
304 return None;
305 }
306 Some(
307 spec.clone()
308 .with_shared_incremental_target_maintenance(config),
309 )
310 }
311}
312
313fn batch_maintenance_ownership_error() -> WasmBuildError {
314 WasmBuildError::InvalidSpec {
315 message:
316 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
317 .to_owned(),
318 }
319}
320
321#[cfg(test)]
322mod tests;