Skip to content

Commit df72860

Browse files
authored
feat: impl sync_trace for logger tracing layer (#12687)
* Sync node events for logger tracing * Align sync_trace * Remove unless info * Cargo shear * Use one writer * Filter JavaScript * Handle timestamps * Clippy * Clippy
1 parent a4152c5 commit df72860

File tree

7 files changed

+174
-48
lines changed

7 files changed

+174
-48
lines changed

Cargo.lock

Lines changed: 5 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ bytes = { version = "1.10.0", default-features = false }
3030
camino = { version = "1.2.2", default-features = false }
3131
cargo_toml = { version = "0.21.0", default-features = false }
3232
cfg-if = { version = "1.0.4", default-features = false }
33+
chrono = { version = "0.4.42", default-features = false }
3334
clap = { version = "4.5.54", default-features = false }
3435
color-backtrace = { version = "0.7.2", default-features = false, features = ["use-backtrace-crate"] }
3536
concat-string = { version = "1.0.1", default-features = false }

crates/rspack_binding_api/src/trace_event.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub(super) fn register_global_trace(
6363
let mut state = state.borrow_mut();
6464
if let TraceState::Uninitialized = *state {
6565
let mut tracer: Box<dyn Tracer> = match layer.as_str() {
66-
"logger" => Box::new(StdoutTracer),
66+
"logger" => Box::new(StdoutTracer::default()),
6767
"perfetto" => Box::new(PerfettoTracer::default()),
6868
_ => anyhow::bail!(
6969
"Unexpected layer: {}, supported layers:'logger', 'perfetto' ",

crates/rspack_tracing/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ version.workspace = true
88

99
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1010
[dependencies]
11+
chrono = { workspace = true, features = ["alloc"] }
1112
rspack_tracing_perfetto = { workspace = true }
13+
serde_json = { workspace = true }
1214
tracing-subscriber = { workspace = true, features = ["env-filter", "json"] }

crates/rspack_tracing/src/lib.rs

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,6 @@ mod perfetto;
22
mod stdout;
33
mod tracer;
44

5-
use std::{fs, io, path::PathBuf};
6-
75
pub use perfetto::PerfettoTracer;
86
pub use stdout::StdoutTracer;
97
pub use tracer::{TraceEvent, Tracer};
10-
use tracing_subscriber::fmt::writer::BoxMakeWriter;
11-
pub(crate) enum TraceWriter {
12-
Stdout,
13-
Stderr,
14-
File { path: PathBuf },
15-
}
16-
17-
impl From<String> for TraceWriter {
18-
fn from(s: String) -> Self {
19-
match s.as_str() {
20-
"stdout" => Self::Stdout,
21-
"stderr" => Self::Stderr,
22-
_ => Self::File { path: s.into() },
23-
}
24-
}
25-
}
26-
27-
impl TraceWriter {
28-
pub fn make_writer(&self) -> BoxMakeWriter {
29-
match self {
30-
TraceWriter::Stdout => BoxMakeWriter::new(io::stdout),
31-
TraceWriter::Stderr => BoxMakeWriter::new(io::stderr),
32-
TraceWriter::File { path } => {
33-
BoxMakeWriter::new(fs::File::create(path).expect("Failed to create trace file"))
34-
}
35-
}
36-
}
37-
}
Lines changed: 163 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,185 @@
1-
use tracing_subscriber::fmt::format::FmtSpan;
1+
use std::{
2+
io::Write,
3+
sync::{Arc, Mutex},
4+
};
5+
6+
use tracing_subscriber::fmt::{MakeWriter, format::FmtSpan};
27

38
use crate::{
4-
TraceWriter,
9+
TraceEvent,
510
tracer::{Layered, Tracer},
611
};
712

8-
pub struct StdoutTracer;
13+
// A custom MakeWriter that wraps a shared Arc<Mutex<>> writer
14+
#[derive(Clone)]
15+
struct SharedWriterMaker {
16+
writer: Arc<Mutex<dyn Write + Send>>,
17+
}
18+
19+
// Wrapper to implement Write trait for the MakeWriter
20+
struct SharedWriter {
21+
writer: Arc<Mutex<dyn Write + Send>>,
22+
}
23+
24+
impl Write for SharedWriter {
25+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
26+
self
27+
.writer
28+
.lock()
29+
.expect("Failed to lock writer")
30+
.write(buf)
31+
}
32+
33+
fn flush(&mut self) -> std::io::Result<()> {
34+
self.writer.lock().expect("Failed to lock writer").flush()
35+
}
36+
}
37+
38+
impl<'a> MakeWriter<'a> for SharedWriterMaker {
39+
type Writer = SharedWriter;
40+
41+
fn make_writer(&'a self) -> Self::Writer {
42+
SharedWriter {
43+
writer: self.writer.clone(),
44+
}
45+
}
46+
}
47+
48+
/// Converts a microsecond timestamp to ISO 8601 format with microsecond precision
49+
/// Example: 1704708707916028 -> "2026-01-08T11:31:47.916028Z"
50+
fn format_timestamp_iso8601(micros: u64) -> String {
51+
use chrono::{DateTime, Utc};
52+
53+
let secs = (micros / 1_000_000) as i64;
54+
let subsec_micros = (micros % 1_000_000) as u32;
55+
let nanos = subsec_micros * 1000;
56+
57+
DateTime::<Utc>::from_timestamp(secs, nanos)
58+
.map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true))
59+
.unwrap_or_else(|| "Invalid timestamp".to_string())
60+
}
61+
62+
#[derive(Default)]
63+
pub struct StdoutTracer {
64+
begin_ts: u64,
65+
writer: Option<Arc<Mutex<dyn Write + Send>>>,
66+
}
967

1068
impl Tracer for StdoutTracer {
1169
fn setup(&mut self, output: &str) -> Option<Layered> {
1270
use tracing_subscriber::{fmt, prelude::*};
13-
let trace_writer = TraceWriter::from(output.to_owned());
71+
72+
// Record the start time in microseconds since UNIX epoch
73+
self.begin_ts = std::time::SystemTime::now()
74+
.duration_since(std::time::UNIX_EPOCH)
75+
.expect("System time before UNIX epoch")
76+
.as_micros() as u64;
77+
78+
// Create the shared writer wrapped in Arc<Mutex<>>
79+
let writer: Arc<Mutex<Box<dyn Write + Send>>> = match output {
80+
"stdout" => Arc::new(Mutex::new(Box::new(std::io::stdout()))),
81+
"stderr" => Arc::new(Mutex::new(Box::new(std::io::stderr()))),
82+
path => {
83+
let file = std::fs::File::create(path)
84+
.unwrap_or_else(|e| panic!("Failed to create trace file: {path} due to {e}"));
85+
Arc::new(Mutex::new(Box::new(file)))
86+
}
87+
};
88+
89+
// Store the shared writer for sync_trace
90+
self.writer = Some(writer.clone());
91+
92+
// Create a custom MakeWriter that uses the same shared writer
93+
let make_writer = SharedWriterMaker { writer };
94+
1495
Some(
1596
fmt::layer()
1697
.json() // Use JSON format for structured logging for easier parsing and debugging
1798
.with_file(false)
1899
// To keep track of the closing point of spans
19100
.with_span_events(FmtSpan::CLOSE)
20-
.with_writer(trace_writer.make_writer())
101+
.with_writer(make_writer)
21102
.boxed(),
22103
)
23104
}
24105

106+
fn sync_trace(&mut self, events: Vec<TraceEvent>) {
107+
if let Some(writer) = &self.writer {
108+
use std::collections::HashMap;
109+
110+
// Track begin events by uuid to match with end events
111+
let mut pending_events: HashMap<u32, TraceEvent> = HashMap::new();
112+
113+
for event in events {
114+
match event.ph.as_str() {
115+
"b" => {
116+
// Store begin event
117+
pending_events.insert(event.uuid, event);
118+
}
119+
"e" => {
120+
// Find matching begin event and calculate duration
121+
if let Some(begin_event) = pending_events.remove(&event.uuid) {
122+
let duration_ns = event.ts.saturating_sub(begin_event.ts);
123+
let duration_ms = duration_ns as f64 / 1_000_000.0;
124+
125+
// Build fields object
126+
let mut fields = serde_json::Map::new();
127+
fields.insert("message".to_string(), serde_json::json!("close"));
128+
fields.insert(
129+
"time.busy".to_string(),
130+
serde_json::json!(format!("{:.2}ms", duration_ms)),
131+
);
132+
133+
// Add any args from the event
134+
if let Some(args) = begin_event.args {
135+
for (key, value) in args {
136+
fields.insert(key, serde_json::json!(value));
137+
}
138+
}
139+
140+
// Build span object if we have track_name
141+
let span_obj = begin_event.track_name.map(|track_name| {
142+
serde_json::json!({
143+
"name": track_name,
144+
})
145+
});
146+
147+
// Convert relative microsecond timestamp to absolute ISO 8601 format
148+
let absolute_ts_micros = self.begin_ts + begin_event.ts;
149+
let timestamp_iso = format_timestamp_iso8601(absolute_ts_micros);
150+
151+
// Build JSON in Rust trace format
152+
let json_value = serde_json::json!({
153+
"timestamp": timestamp_iso,
154+
"level": "DEBUG",
155+
"fields": fields,
156+
"target": "javascript",
157+
"span": span_obj,
158+
});
159+
160+
if let Ok(json_str) = serde_json::to_string(&json_value) {
161+
// Lock the mutex to access the writer
162+
let _ = writeln!(
163+
writer.lock().expect("Failed to lock writer"),
164+
"{}",
165+
json_str
166+
);
167+
}
168+
}
169+
}
170+
_ => {}
171+
}
172+
}
173+
174+
// Flush to ensure events are written immediately
175+
let _ = writer.lock().expect("Failed to lock writer").flush();
176+
}
177+
}
178+
25179
fn teardown(&mut self) {
26-
// noop
180+
// Flush any remaining data
181+
if let Some(writer) = &self.writer {
182+
let _ = writer.lock().expect("Failed to lock writer").flush();
183+
}
27184
}
28185
}

packages/rspack-cli/tests/build/profile/profile.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ describe('profile', () => {
7575
.every(
7676
(line) =>
7777
line.target.startsWith('rspack') ||
78-
line.target.startsWith('rspack_resolver'),
78+
line.target.startsWith('rspack_resolver') ||
79+
line.target.startsWith('javascript'),
7980
),
8081
).toBe(true);
8182
});

0 commit comments

Comments
 (0)