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