1use std::{
2 cmp,
3 cmp::Ordering,
4 sync::{
5 Arc, LazyLock,
6 atomic::{AtomicU32, Ordering::Relaxed},
7 },
8 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
9};
10
11use futures::future::BoxFuture;
12use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
13use rspack_collections::IdentifierMap;
14use rspack_core::{
15 AsyncModulesArtifact, BoxModule, ChunkByUkey, ChunkNamedIdArtifact, Compilation,
16 CompilationAfterOptimizeModules, CompilationAfterProcessAssets, CompilationBuildModule,
17 CompilationChunkIds, CompilationFinishModules, CompilationId, CompilationModuleIds,
18 CompilationOptimizeChunkModules, CompilationOptimizeChunks, CompilationOptimizeCodeGeneration,
19 CompilationOptimizeDependencies, CompilationOptimizeModules, CompilationOptimizeTree,
20 CompilationParams, CompilationProcessAssets, CompilationSeal, CompilationSucceedModule,
21 CompilerAfterEmit, CompilerClose, CompilerCompilation, CompilerEmit, CompilerFinishMake,
22 CompilerId, CompilerMake, CompilerThisCompilation, ModuleIdentifier, ModuleIdsArtifact, Plugin,
23 SideEffectsOptimizeArtifact, build_module_graph::BuildModuleGraphArtifact,
24};
25use rspack_error::{Diagnostic, Result};
26use rspack_hook::{plugin, plugin_hook};
27use tokio::sync::Mutex;
28
29type HandlerFn =
30 Arc<dyn Fn(f64, String, Vec<String>) -> BoxFuture<'static, Result<()>> + Send + Sync>;
31
32pub enum ProgressPluginOptions {
33 Handler(HandlerFn),
34 Default(ProgressPluginDisplayOptions),
35}
36
37impl std::fmt::Debug for ProgressPluginOptions {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 ProgressPluginOptions::Handler(_handler) => {
41 f.debug_struct("ProgressPluginOptions::Handler").finish()
42 }
43 ProgressPluginOptions::Default(options) => f
44 .debug_struct("ProgressPluginOptions::Default")
45 .field("options", &options)
46 .finish(),
47 }
48 }
49}
50
51static MULTI_PROGRESS: LazyLock<MultiProgress> =
52 LazyLock::new(|| MultiProgress::with_draw_target(ProgressDrawTarget::stdout_with_hz(100)));
53#[derive(Debug, Default)]
54pub struct ProgressPluginDisplayOptions {
55 pub prefix: String,
57 pub profile: bool,
59 pub template: String,
61 pub tick_strings: Option<Vec<String>>,
63 pub progress_chars: String,
65}
66
67#[derive(Debug)]
68pub struct ProgressPluginStateInfo {
69 pub value: String,
70 pub time: Instant,
71 pub duration: Option<Duration>,
72}
73
74#[plugin]
75#[derive(Debug)]
76pub struct ProgressPlugin {
77 pub options: ProgressPluginOptions,
78 pub progress_bar: Option<ProgressBar>,
79 pub modules_count: Arc<AtomicU32>,
80 pub modules_done: Arc<AtomicU32>,
81 pub active_modules: Arc<Mutex<IdentifierMap<Instant>>>,
82 pub last_modules_count: Arc<Mutex<Option<u32>>>,
83 pub last_active_module: Arc<Mutex<Option<ModuleIdentifier>>>,
84 pub last_state_info: Arc<Mutex<Vec<ProgressPluginStateInfo>>>,
85 pub last_updated: Arc<AtomicU32>,
86}
87
88impl ProgressPlugin {
89 pub fn new(options: ProgressPluginOptions) -> Self {
90 let progress_bar = match &options {
91 ProgressPluginOptions::Handler(_fn) => None,
92 ProgressPluginOptions::Default(options) => {
93 let progress_bar = MULTI_PROGRESS.add(ProgressBar::new(100));
95
96 let mut progress_bar_style = ProgressStyle::with_template(&options.template)
97 .expect("TODO:")
98 .progress_chars(&options.progress_chars);
99 if let Some(tick_strings) = &options.tick_strings {
100 progress_bar_style = progress_bar_style.tick_strings(
101 tick_strings
102 .iter()
103 .map(|s| s.as_str())
104 .collect::<Vec<_>>()
105 .as_slice(),
106 );
107 }
108 progress_bar.set_style(progress_bar_style);
109 Some(progress_bar)
110 }
111 };
112 Self::new_inner(
113 options,
114 progress_bar,
115 Default::default(),
116 Default::default(),
117 Default::default(),
118 Default::default(),
119 Default::default(),
120 Default::default(),
121 Default::default(),
122 )
123 }
124
125 async fn update_throttled(&self) -> Result<()> {
126 let current_time = SystemTime::now()
127 .duration_since(UNIX_EPOCH)
128 .expect("failed to get current time")
129 .as_millis() as u32;
130
131 if current_time - self.last_updated.load(Relaxed) > 200 {
132 self.update().await?;
133 self.last_updated.store(current_time, Relaxed);
134 }
135
136 Ok(())
137 }
138
139 async fn update(&self) -> Result<()> {
140 let modules_done = self.modules_done.load(Relaxed);
141 let percent_by_module = (modules_done as f64)
142 / (cmp::max(
143 self.last_modules_count.lock().await.unwrap_or(1),
144 self.modules_count.load(Relaxed),
145 ) as f64);
146
147 let mut items = vec![];
148
149 if let Some(last_active_module) = self.last_active_module.lock().await.as_ref() {
150 items.push(last_active_module.to_string());
151 let duration = self
152 .active_modules
153 .lock()
154 .await
155 .get(last_active_module)
156 .map(|time| Instant::now() - *time);
157 self
158 .handler(
159 0.1 + percent_by_module * 0.55,
160 String::from("building"),
161 items,
162 duration,
163 )
164 .await?;
165 }
166 Ok(())
167 }
168
169 pub async fn handler(
170 &self,
171 percent: f64,
172 msg: String,
173 state_items: Vec<String>,
174 time: Option<Duration>,
175 ) -> Result<()> {
176 match &self.options {
177 ProgressPluginOptions::Handler(handler) => handler(percent, msg, state_items).await?,
178 ProgressPluginOptions::Default(options) => {
179 if options.profile {
180 self.default_handler(percent, msg, state_items, time).await;
181 } else {
182 self.progress_bar_handler(percent, msg, state_items);
183 }
184 }
185 };
186 Ok(())
187 }
188
189 async fn default_handler(
190 &self,
191 _: f64,
192 msg: String,
193 items: Vec<String>,
194 duration: Option<Duration>,
195 ) {
196 let full_state = [vec![msg.clone()], items.clone()].concat();
197 let now = Instant::now();
198 {
199 let mut last_state_info = self.last_state_info.lock().await;
200 let len = full_state.len().max(last_state_info.len());
201 let original_last_state_info_len = last_state_info.len();
202 for i in (0..len).rev() {
203 if i + 1 > original_last_state_info_len {
204 last_state_info.insert(
205 original_last_state_info_len,
206 ProgressPluginStateInfo {
207 value: full_state[i].clone(),
208 time: now,
209 duration: None,
210 },
211 )
212 } else if i + 1 > full_state.len() || !last_state_info[i].value.eq(&full_state[i]) {
213 let diff = match last_state_info[i].duration {
214 Some(duration) => duration,
215 _ => now - last_state_info[i].time,
216 }
217 .as_millis();
218 let report_state = if i > 0 {
219 last_state_info[i - 1].value.clone() + " > " + last_state_info[i].value.clone().as_str()
220 } else {
221 last_state_info[i].value.clone()
222 };
223
224 if diff > 5 {
225 let mut color = "\x1b[32m";
227 if diff > 10000 {
228 color = "\x1b[31m"
229 } else if diff > 1000 {
230 color = "\x1b[33m"
231 }
232 println!(
233 "{}{} {} ms {}\x1B[0m",
234 color,
235 " | ".repeat(i),
236 diff,
237 report_state
238 );
239 }
240 match (i + 1).cmp(&full_state.len()) {
241 Ordering::Greater => last_state_info.truncate(i),
242 Ordering::Equal => {
243 last_state_info[i] = ProgressPluginStateInfo {
244 value: full_state[i].clone(),
245 time: now,
246 duration,
247 }
248 }
249 Ordering::Less => {
250 last_state_info[i] = ProgressPluginStateInfo {
251 value: full_state[i].clone(),
252 time: now,
253 duration: None,
254 };
255 }
256 }
257 }
258 }
259 }
260 }
261
262 fn progress_bar_handler(&self, percent: f64, msg: String, state_items: Vec<String>) {
263 if let Some(progress_bar) = &self.progress_bar {
264 let msg = msg + " " + state_items.join(" ").as_str();
265 if percent == 1.0 {
266 progress_bar.finish_with_message(msg);
267 } else {
268 progress_bar.set_message(msg);
269 progress_bar.set_position((percent * 100.0) as u64);
270 }
271 }
272 }
273
274 async fn sealing_hooks_report(&self, name: &str, index: i32) -> Result<()> {
275 let number_of_sealing_hooks = 38;
276 self
277 .handler(
278 0.7 + 0.25 * (index as f64 / number_of_sealing_hooks as f64),
279 "sealing".to_string(),
280 vec![name.to_string()],
281 None,
282 )
283 .await
284 }
285
286 pub fn is_profile(&self) -> bool {
287 match &self.options {
288 ProgressPluginOptions::Handler(_) => false,
289 ProgressPluginOptions::Default(options) => options.profile,
290 }
291 }
292}
293
294#[plugin_hook(CompilerThisCompilation for ProgressPlugin)]
295async fn this_compilation(
296 &self,
297 _compilation: &mut Compilation,
298 _params: &mut CompilationParams,
299) -> Result<()> {
300 if let ProgressPluginOptions::Default(options) = &self.options {
301 let progress_bar = self.progress_bar.as_ref().unwrap_or_else(|| unreachable!());
302 if !options.profile {
303 progress_bar.reset();
304 progress_bar.set_prefix(options.prefix.clone());
305 }
306 }
307
308 self
309 .handler(
310 0.08,
311 "setup".to_string(),
312 vec!["compilation".to_string()],
313 None,
314 )
315 .await
316}
317
318#[plugin_hook(CompilerCompilation for ProgressPlugin)]
319async fn compilation(
320 &self,
321 _compilation: &mut Compilation,
322 _params: &mut CompilationParams,
323) -> Result<()> {
324 self
325 .handler(
326 0.09,
327 "setup".to_string(),
328 vec!["compilation".to_string()],
329 None,
330 )
331 .await
332}
333
334#[plugin_hook(CompilerMake for ProgressPlugin)]
335async fn make(&self, _compilation: &mut Compilation) -> Result<()> {
336 self
337 .handler(0.1, String::from("make"), vec![], None)
338 .await?;
339 self.modules_count.store(0, Relaxed);
340 self.modules_done.store(0, Relaxed);
341 Ok(())
342}
343
344#[plugin_hook(CompilationBuildModule for ProgressPlugin)]
345async fn build_module(
346 &self,
347 _compiler_id: CompilerId,
348 _compilation_id: CompilationId,
349 module: &mut BoxModule,
350) -> Result<()> {
351 self
352 .active_modules
353 .lock()
354 .await
355 .insert(module.identifier(), Instant::now());
356 self.modules_count.fetch_add(1, Relaxed);
357 self
358 .last_active_module
359 .lock()
360 .await
361 .replace(module.identifier());
362 if let ProgressPluginOptions::Default(options) = &self.options
363 && !options.profile
364 {
365 self.update_throttled().await?;
366 }
367
368 Ok(())
369}
370
371#[plugin_hook(CompilationSucceedModule for ProgressPlugin)]
372async fn succeed_module(
373 &self,
374 _compiler_id: CompilerId,
375 _compilation_id: CompilationId,
376 module: &mut BoxModule,
377) -> Result<()> {
378 self.modules_done.fetch_add(1, Relaxed);
379 self
380 .last_active_module
381 .lock()
382 .await
383 .replace(module.identifier());
384
385 if self.is_profile() {
387 self.update_throttled().await?;
388 }
389 let mut last_active_module = Default::default();
390 {
391 let mut active_modules = self.active_modules.lock().await;
392 active_modules.remove(&module.identifier());
393
394 if !self.is_profile() {
396 active_modules.iter().for_each(|(module, _)| {
397 last_active_module = *module;
398 });
399 }
400 }
401 if !self.is_profile() {
402 self
403 .last_active_module
404 .lock()
405 .await
406 .replace(last_active_module);
407 if !last_active_module.is_empty() {
408 self.update_throttled().await?;
409 }
410 }
411 Ok(())
412}
413
414#[plugin_hook(CompilerFinishMake for ProgressPlugin)]
415async fn finish_make(&self, _compilation: &mut Compilation) -> Result<()> {
416 self
417 .handler(
418 0.69,
419 "building".to_string(),
420 vec!["finish make".to_string()],
421 None,
422 )
423 .await
424}
425
426#[plugin_hook(CompilationFinishModules for ProgressPlugin)]
427async fn finish_modules(
428 &self,
429 _compilation: &mut Compilation,
430 _async_modules_artifact: &mut AsyncModulesArtifact,
431) -> Result<()> {
432 self.sealing_hooks_report("finish modules", 0).await
433}
434
435#[plugin_hook(CompilationSeal for ProgressPlugin)]
436async fn seal(&self, _compilation: &mut Compilation) -> Result<()> {
437 self.sealing_hooks_report("plugins", 1).await
438}
439
440#[plugin_hook(CompilationOptimizeDependencies for ProgressPlugin)]
441async fn optimize_dependencies(
442 &self,
443 _compilation: &Compilation,
444 _side_effects_optimize_artifact: &mut SideEffectsOptimizeArtifact,
445 _build_module_graph_artifact: &mut BuildModuleGraphArtifact,
446 _diagnostics: &mut Vec<Diagnostic>,
447) -> Result<Option<bool>> {
448 self.sealing_hooks_report("dependencies", 2).await?;
449 Ok(None)
450}
451
452#[plugin_hook(CompilationOptimizeModules for ProgressPlugin)]
453async fn optimize_modules(
454 &self,
455 _compilation: &Compilation,
456 _diagnostics: &mut Vec<Diagnostic>,
457) -> Result<Option<bool>> {
458 self.sealing_hooks_report("module optimization", 7).await?;
459 Ok(None)
460}
461
462#[plugin_hook(CompilationAfterOptimizeModules for ProgressPlugin)]
463async fn after_optimize_modules(&self, _compilation: &Compilation) -> Result<()> {
464 self
465 .sealing_hooks_report("after module optimization", 8)
466 .await
467}
468
469#[plugin_hook(CompilationOptimizeChunks for ProgressPlugin)]
470async fn optimize_chunks(&self, _compilation: &mut Compilation) -> Result<Option<bool>> {
471 self.sealing_hooks_report("chunk optimization", 9).await?;
472 Ok(None)
473}
474
475#[plugin_hook(CompilationOptimizeTree for ProgressPlugin)]
476async fn optimize_tree(&self, _compilation: &Compilation) -> Result<()> {
477 self
478 .sealing_hooks_report("module and chunk tree optimization", 11)
479 .await
480}
481
482#[plugin_hook(CompilationOptimizeChunkModules for ProgressPlugin)]
483async fn optimize_chunk_modules(&self, _compilation: &mut Compilation) -> Result<Option<bool>> {
484 self
485 .sealing_hooks_report("chunk modules optimization", 13)
486 .await?;
487 Ok(None)
488}
489
490#[plugin_hook(CompilationModuleIds for ProgressPlugin)]
491async fn module_ids(
492 &self,
493 _compilation: &Compilation,
494 _module_ids: &mut ModuleIdsArtifact,
495 _diagnostics: &mut Vec<Diagnostic>,
496) -> Result<()> {
497 self.sealing_hooks_report("module ids", 16).await
498}
499
500#[plugin_hook(CompilationChunkIds for ProgressPlugin)]
501async fn chunk_ids(
502 &self,
503 _compilation: &Compilation,
504 _chunk_by_ukey: &mut ChunkByUkey,
505 _named_chunk_ids_artifact: &mut ChunkNamedIdArtifact,
506 _diagnostics: &mut Vec<Diagnostic>,
507) -> Result<()> {
508 self.sealing_hooks_report("chunk ids", 21).await
509}
510
511#[plugin_hook(CompilationOptimizeCodeGeneration for ProgressPlugin)]
512async fn optimize_code_generation(
513 &self,
514 _compilation: &Compilation,
515 _build_module_graph_artifact: &mut BuildModuleGraphArtifact,
516 _diagnostics: &mut Vec<Diagnostic>,
517) -> Result<()> {
518 self.sealing_hooks_report("code generation", 26).await
519}
520
521#[plugin_hook(CompilationProcessAssets for ProgressPlugin, stage = Compilation::PROCESS_ASSETS_STAGE_ADDITIONAL)]
522async fn process_assets(&self, _compilation: &mut Compilation) -> Result<()> {
523 self.sealing_hooks_report("asset processing", 35).await
524}
525
526#[plugin_hook(CompilationAfterProcessAssets for ProgressPlugin)]
527async fn after_process_assets(
528 &self,
529 _compilation: &Compilation,
530 _diagnostics: &mut Vec<Diagnostic>,
531) -> Result<()> {
532 self
533 .sealing_hooks_report("after asset optimization", 36)
534 .await
535}
536
537#[plugin_hook(CompilerEmit for ProgressPlugin)]
538async fn emit(&self, _compilation: &mut Compilation) -> Result<()> {
539 self
540 .handler(0.98, "emitting".to_string(), vec!["emit".to_string()], None)
541 .await
542}
543
544#[plugin_hook(CompilerAfterEmit for ProgressPlugin)]
545async fn after_emit(&self, _compilation: &mut Compilation) -> Result<()> {
546 self
547 .handler(
548 1.0,
549 "emitting".to_string(),
550 vec!["after emit".to_string()],
551 None,
552 )
553 .await
554}
555
556#[plugin_hook(CompilerClose for ProgressPlugin)]
557async fn close(&self, _compilation: &Compilation) -> Result<()> {
558 if let Some(progress_bar) = &self.progress_bar {
559 MULTI_PROGRESS.remove(progress_bar);
560 }
561 Ok(())
562}
563
564impl Plugin for ProgressPlugin {
565 fn name(&self) -> &'static str {
566 "progress"
567 }
568
569 fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
570 ctx
571 .compiler_hooks
572 .this_compilation
573 .tap(this_compilation::new(self));
574 ctx.compiler_hooks.compilation.tap(compilation::new(self));
575 ctx.compiler_hooks.make.tap(make::new(self));
576 ctx
577 .compilation_hooks
578 .build_module
579 .tap(build_module::new(self));
580 ctx
581 .compilation_hooks
582 .succeed_module
583 .tap(succeed_module::new(self));
584 ctx.compiler_hooks.finish_make.tap(finish_make::new(self));
585 ctx
586 .compilation_hooks
587 .finish_modules
588 .tap(finish_modules::new(self));
589 ctx.compilation_hooks.seal.tap(seal::new(self));
590 ctx
591 .compilation_hooks
592 .optimize_dependencies
593 .tap(optimize_dependencies::new(self));
594 ctx
595 .compilation_hooks
596 .optimize_modules
597 .tap(optimize_modules::new(self));
598 ctx
599 .compilation_hooks
600 .after_optimize_modules
601 .tap(after_optimize_modules::new(self));
602 ctx
603 .compilation_hooks
604 .optimize_chunks
605 .tap(optimize_chunks::new(self));
606 ctx
607 .compilation_hooks
608 .optimize_tree
609 .tap(optimize_tree::new(self));
610 ctx
611 .compilation_hooks
612 .optimize_chunk_modules
613 .tap(optimize_chunk_modules::new(self));
614 ctx.compilation_hooks.module_ids.tap(module_ids::new(self));
615 ctx.compilation_hooks.chunk_ids.tap(chunk_ids::new(self));
616 ctx
617 .compilation_hooks
618 .optimize_code_generation
619 .tap(optimize_code_generation::new(self));
620 ctx
621 .compilation_hooks
622 .process_assets
623 .tap(process_assets::new(self));
624 ctx
625 .compilation_hooks
626 .after_process_assets
627 .tap(after_process_assets::new(self));
628 ctx.compiler_hooks.emit.tap(emit::new(self));
629 ctx.compiler_hooks.after_emit.tap(after_emit::new(self));
630 ctx.compiler_hooks.close.tap(close::new(self));
631 Ok(())
632 }
633}