Repository: vercel/next.js
Pull request: update(turbopack): Update the messaging UX for timing writing files to disk

## Refactor Turbopack Compilation Events and Timing Reporting

### What?
This PR refactors how compilation events are created, displayed, and formatted in Turbopack. It introduces a dedicated `TimingEvent` type for timing-related messages and improves the formatting of duration displays.

Additionally we now display all compilation events now that it has been shown to be stable with no breakages.

### Why?
To provide more consistent and user-friendly output for compilation events, especially for timing information. The new approach makes duration formatting more readable by automatically selecting appropriate units (ms, s, min) based on the duration length.

### How?
- Added a new `TimingEvent` struct that properly formats durations based on their duration
- Updated the `DiagnosticEvent` constructor to take severity first, then message
- Improved event handling in the build process to properly categorize and display different severity levels
- Enhanced file I/O benchmarking to use the new timing event format
- Fixed the formatting of compilation events in the UI

Closes PACK-4696

Changed files:
filename: crates/napi/src/next_api/project.rs
patch: @@ -31,7 +31,7 @@ use turbo_rcstr::RcStr;
 use turbo_tasks::{
     Completion, Effects, FxIndexSet, OperationVc, ReadRef, ResolvedVc, TransientInstance,
     TryJoinIterExt, UpdateInfo, Vc, get_effects,
-    message_queue::{CompilationEvent, DiagnosticEvent, Severity},
+    message_queue::{CompilationEvent, DiagnosticEvent, Severity, TimingEvent},
 };
 use turbo_tasks_fs::{
     DiskFileSystem, FileContent, FileSystem, FileSystemPath, get_relative_path_to,
@@ -405,8 +405,8 @@ pub async fn project_new(
     )?;
 
     turbo_tasks.send_compilation_event(Arc::new(DiagnosticEvent::new(
-        "Starting the compilation events server...".to_owned(),
-        Severity::Trace,
+        Severity::Info,
+        "Starting the compilation events server ...".to_owned(),
     )));
 
     let stats_path = std::env::var_os("NEXT_TURBOPACK_TASK_STATISTICS");
@@ -850,21 +850,14 @@ pub async fn project_write_all_entrypoints_to_disk(
 
             // Start timing writing the files to disk
             let now = Instant::now();
-            compilation_event_sender.send_compilation_event(Arc::new(DiagnosticEvent::new(
-                "Starting to write all entrypoints to disk...".to_owned(),
-                Severity::Event,
-            )));
 
             // Write the files to disk
             effects.apply().await?;
 
             // Send a compilation event to indicate that the files have been written to disk
-            compilation_event_sender.send_compilation_event(Arc::new(DiagnosticEvent::new(
-                format!(
-                    "Finished writing all entrypoints to disk in {:?}",
-                    now.elapsed()
-                ),
-                Severity::Event,
+            compilation_event_sender.send_compilation_event(Arc::new(TimingEvent::new(
+                "Finished writing all entrypoints to disk".to_owned(),
+                now.elapsed(),
             )));
 
             Ok((entrypoints.clone(), issues.clone(), diagnostics.clone()))

filename: packages/next/src/build/turbopack-build/impl.ts
patch: @@ -91,8 +91,27 @@ export async function turbopackBuild(): Promise<{
   try {
     ;(async function logCompilationEvents() {
       for await (const event of project.compilationEventsSubscribe()) {
-        if (event.severity === 'EVENT') {
-          Log.event(event.message)
+        switch (event.severity) {
+          case 'EVENT':
+            Log.event(event.message)
+            break
+          case 'TRACE':
+            Log.trace(event.message)
+            break
+          case 'INFO':
+            Log.info(event.message)
+            break
+          case 'WARNING':
+            Log.warn(event.message)
+            break
+          case 'ERROR':
+            Log.error(event.message)
+            break
+          case 'FATAL':
+            Log.error(event.message)
+            break
+          default:
+            break
         }
       }
     })()

filename: turbopack/crates/turbo-tasks/src/message_queue.rs
patch: @@ -1,4 +1,4 @@
-use std::{any::Any, collections::VecDeque, fmt::Display, sync::Arc};
+use std::{any::Any, collections::VecDeque, fmt::Display, sync::Arc, time::Duration};
 
 use dashmap::DashMap;
 use tokio::sync::{Mutex, mpsc};
@@ -155,14 +155,64 @@ impl Display for Severity {
     }
 }
 
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+/// Compilation event that is used to log the duration of a task
+pub struct TimingEvent {
+    /// Message of the event without the timing information
+    ///
+    /// Example:
+    /// ```rust
+    /// let event = TimingEvent::new("Compiled successfully".to_string(), Duration::from_millis(100));
+    /// let message = event.message();
+    /// assert_eq!(message, "Compiled successfully in 100ms");
+    /// ```
+    pub message: String,
+    /// Duration in milliseconds
+    pub duration: Duration,
+}
+
+impl TimingEvent {
+    pub fn new(message: String, duration: Duration) -> Self {
+        Self { message, duration }
+    }
+}
+
+impl CompilationEvent for TimingEvent {
+    fn type_name(&self) -> &'static str {
+        "TimingEvent"
+    }
+
+    fn severity(&self) -> Severity {
+        Severity::Event
+    }
+
+    fn message(&self) -> String {
+        let duration_secs = self.duration.as_secs_f64();
+        let duration_string = if duration_secs > 120.0 {
+            format!("{:.1}min", duration_secs / 60.0)
+        } else if duration_secs > 40.0 {
+            format!("{duration_secs:.0}s")
+        } else if duration_secs > 2.0 {
+            format!("{duration_secs:.1}s")
+        } else {
+            format!("{:.0}ms", duration_secs * 1000.0)
+        };
+        format!("{} in {}", self.message, duration_string)
+    }
+
+    fn to_json(&self) -> String {
+        serde_json::to_string(self).unwrap()
+    }
+}
+
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 pub struct DiagnosticEvent {
     pub message: String,
     pub severity: Severity,
 }
 
 impl DiagnosticEvent {
-    pub fn new(message: String, severity: Severity) -> Self {
+    pub fn new(severity: Severity, message: String) -> Self {
         Self { message, severity }
     }
 }
@@ -184,3 +234,31 @@ impl CompilationEvent for DiagnosticEvent {
         serde_json::to_string(self).unwrap()
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_timing_event_string_formatting() {
+        let tests = vec![
+            (Duration::from_nanos(1588), "0ms"),
+            (Duration::from_nanos(1022616), "1ms"),
+            (Duration::from_millis(100), "100ms"),
+            (Duration::from_millis(1000), "1000ms"),
+            (Duration::from_millis(10000), "10.0s"),
+            (Duration::from_millis(20381), "20.4s"),
+            (Duration::from_secs(60), "60s"),
+            (Duration::from_secs(100), "100s"),
+            (Duration::from_secs(125), "2.1min"),
+        ];
+
+        for (duration, expected) in tests {
+            let event = TimingEvent::new("Compiled successfully".to_string(), duration);
+            assert_eq!(
+                event.message(),
+                format!("Compiled successfully in {expected}")
+            );
+        }
+    }
+}