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;
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  // the prefix name of progress bar
56  pub prefix: String,
57  // tells ProgressPlugin to collect profile data for progress steps.
58  pub profile: bool,
59  // the template of progress bar, see [`indicatif::ProgressStyle::with_template`]
60  pub template: String,
61  // the tick string sequence for spinners, see [`indicatif::ProgressStyle::tick_strings`]
62  pub tick_strings: Option<Vec<String>>,
63  // the progress characters, see [`indicatif::ProgressStyle::progress_chars`]
64  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        // default interval is 20, means draw every 1000/20 = 50ms, use 100 to draw every 1000/100 = 10ms
94        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            // TODO: color map
226            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  // only profile mode should update at succeed module
386  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    // get the last active module
395    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}