Skip to main content

rspack_plugin_progress/
lib.rs

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  // the prefix name of progress bar
64  pub prefix: String,
65  // tells ProgressPlugin to collect profile data for progress steps.
66  pub profile: bool,
67  // the template of progress bar, see [`indicatif::ProgressStyle::with_template`]
68  pub template: String,
69  // the tick string sequence for spinners, see [`indicatif::ProgressStyle::tick_strings`]
70  pub tick_strings: Option<Vec<String>>,
71  // the progress characters, see [`indicatif::ProgressStyle::progress_chars`]
72  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            // TODO: color map
233            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  // only profile mode should update at succeed module
399  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    // get the last active module
408    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}