functiontrace_server/profile_generator.rs
1use crate::function_trace::*;
2use ahash::{HashMap, HashSet, RandomState};
3use bytesize::ByteSize;
4use color_eyre::eyre::{Result, WrapErr};
5use indexmap::set::IndexSet;
6use owo_colors::OwoColorize;
7use serde::Serialize;
8use serde_repr::Serialize_repr;
9
10use std::borrow::Cow;
11use std::fs::File;
12use std::io::BufWriter;
13use std::num::NonZeroU32;
14use std::path::PathBuf;
15use std::sync::atomic::Ordering;
16use std::time::{Duration, SystemTime};
17
18/// Number of ms since `meta.start_duration`.
19/// We have two variants, with [`HighResFirefoxTime`] being preferred for accuracy, but
20/// [`FirefoxTime`] being used when the overhead of the larger types is too much.
21///
22/// NOTE: For correctness purposes we should always be using [`f64`], since f32s lose significant
23/// accuracy by ~2^24 ms. That's more than 4hrs long though - it's unlikely that people will be
24/// using FunctionTrace for that long, and if they were both capturing for that long and emitting
25/// enough events for sub-ms accuracy to matter, they would've run out of memory long ago. Using
26/// [`f32`] instead shaves ~40% off of profile sizes, allowing substantially larger programs to be
27/// profiled with FunctionTrace.
28type HighResFirefoxTime = f64;
29type FirefoxTime = f32;
30
31// TODO: Should we be creating these?
32#[derive(Serialize, Debug)]
33enum Unused {}
34
35type StringIndex = usize;
36
37/// A unique id for threads, with no correlation to Unix thread ids.
38type ThreadId = u32;
39
40/// Represents an opaque promise that a thread can be registered with the given id, including some
41/// information internally necessary for doing so.
42pub struct FirefoxProfileThreadId {
43 /// A unique id representing this thread. It has no correlation to Unix thread ids.
44 pub tid: ThreadId,
45
46 /// The initial system time for all tracing in this profile.
47 /// This allows threads to emit offset times based on a single master time for the profile.
48 start_duration: Duration,
49}
50
51/// The top-level structure representing a profile that can be consumed by the Firefox profiler.
52/// This will be exported mostly as-is to JSON at the end of a tracing session.
53///
54/// Based off of <https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js>.
55#[derive(Serialize, Debug)]
56pub struct FirefoxProfile {
57 meta: Metadata,
58 counters: Vec<Unused>,
59 threads: Vec<FirefoxThread>,
60 libs: Vec<Unused>,
61
62 ///////////////////////////////////////////////////////////////////////////
63 // Internal
64 ///////////////////////////////////////////////////////////////////////////
65 // The number of registered threads.
66 #[serde(skip_serializing)]
67 thread_count: u32,
68}
69
70/// Represent a function call in a way that can either be cheaply created (for [`HashMap::get`]) or
71/// allocated on the heap (for [`HashMap::insert`]).
72#[derive(Hash, Debug, PartialEq, Eq)]
73enum FunctionCall<'a> {
74 Native {
75 function_name: Cow<'a, str>,
76 module: Cow<'a, str>,
77 },
78 Python {
79 function_name: Cow<'a, str>,
80 module: Cow<'a, str>,
81 line: NonZeroU32,
82 },
83}
84
85///////////////////////////////////////////////////////////////////////////////
86// Event Categories
87///////////////////////////////////////////////////////////////////////////////
88// https://github.com/firefox-devtools/profiler/blob/78070314c3f0d5f4d51d9dd1e44d11a5e242d921/src/utils/colors.js
89#[derive(Serialize, Debug)]
90#[serde(rename_all = "lowercase")]
91#[allow(dead_code)]
92enum Color {
93 Transparent,
94 Purple,
95 Green,
96 Orange,
97 Yellow,
98 LightBlue,
99 Grey,
100 Blue,
101 Brown,
102}
103
104#[derive(Serialize, Debug)]
105struct Category {
106 name: &'static str,
107 color: Color,
108 subcategories: [&'static str; 1],
109}
110#[derive(Serialize_repr, Debug, Clone, Copy)]
111#[repr(u32)]
112enum CategoryIndex {
113 Logs,
114 Python,
115 Native,
116 Exceptions,
117 Imports,
118 GarbageCollection,
119 Length,
120}
121
122const CATEGORIES: [Category; CategoryIndex::Length as usize] = [
123 Category {
124 name: "Logs",
125 color: Color::Grey,
126 subcategories: ["Unused"],
127 },
128 Category {
129 name: "Python",
130 color: Color::Orange,
131 subcategories: ["Code"],
132 },
133 Category {
134 name: "Native",
135 color: Color::LightBlue,
136 subcategories: ["Code"],
137 },
138 Category {
139 name: "Exceptions",
140 color: Color::Brown,
141 subcategories: ["Unused"],
142 },
143 Category {
144 name: "Imports",
145 color: Color::Grey,
146 subcategories: ["Unused"],
147 },
148 Category {
149 name: "GarbageCollection",
150 color: Color::Purple,
151 subcategories: ["Unused"],
152 },
153];
154
155///////////////////////////////////////////////////////////////////////////////
156// Tables
157///////////////////////////////////////////////////////////////////////////////
158// There are many table structures where the profile is expected to output struct-of-lists rather
159// than list-of-structs in order to save on memory allocation overhead in JS.
160// Rather than dealing with all of these manually, this macro allows us to quickly define the
161// structs as well as useful helper methods.
162macro_rules! struct_table {
163 (struct $struct_name: ident {
164 $($element: ident: $ty: ty),+
165 } $($additional_field:ident: $additional_type: ty),*) => {
166 paste::item! {
167 #[derive(Serialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
168 // NOTE: Ideally this would be a usize, since we should be able to handle quite a lot
169 // of entries with the resources of a modern machine. However, since we're limited to
170 // <2GB profile sizes by the Firefox Profiler, we know than we'll never need more than
171 // a u32.
172 struct [<$struct_name Index>] (u32);
173
174 #[derive(Serialize, Debug, Default)]
175 #[serde(rename_all = "camelCase")]
176 struct [<$struct_name Table>] {
177 $($element: Vec<$ty>,)*
178 $($additional_field: $additional_type,)*
179 length: usize
180 }
181
182 struct $struct_name {
183 $($element: $ty),*,
184 }
185
186 impl [<$struct_name Table>] {
187 #[allow(dead_code)]
188 fn new($([<$additional_field _arg>]: $additional_type,)*) -> Self {
189 Self {
190 $($element: Vec::new()),*,
191 $($additional_field: [<$additional_field _arg>],)*
192 length: 0
193 }
194 }
195
196 #[allow(dead_code)]
197 fn add(&mut self, arg: $struct_name) -> [<$struct_name Index>] {
198 let length = self.length;
199 $(self.$element.push(arg.$element);)*
200 self.length += 1;
201
202 [<$struct_name Index>](length as u32)
203 }
204
205 // Sometimes we want to skip serializing if the table is empty.
206 #[allow(dead_code)]
207 fn is_empty(&self) -> bool {
208 self.length == 0
209 }
210
211 $(
212 // Support typesafe-ish accessors for elements.
213 #[allow(dead_code)]
214 fn [<get_ $element>](&self, index: [<$struct_name Index>]) -> &$ty {
215 &self.$element[index.0 as usize]
216 })*
217 }
218 }
219 }
220}
221
222struct_table! {
223 struct Sample {
224 stack: StackIndex,
225 time: FirefoxTime,
226 weight: Option<FirefoxTime>
227 // TODO: We can draw CPU usage graphs with this, but we don't currently have enough
228 // information to do that.
229 // threadCPUDelta: f32
230 }
231 weight_type: &'static str
232}
233
234struct_table! {
235 struct Stack {
236 frame: FrameIndex,
237 category: CategoryIndex,
238 subcategory: u8,
239 prefix: Option<StackIndex>
240 }
241}
242struct_table! {
243 struct Frame {
244 func: FunctionIndex,
245 inlineDepth: u8,
246 category: CategoryIndex,
247 subcategory: u8,
248 address: Option<Unused>,
249 nativeSymbol: Option<Unused>,
250 innerWindowID: Option<Unused>,
251 implementation: Option<Unused>,
252 // Line/column don't appear to be displayed anywhere for frames.
253 line: Option<Unused>,
254 column: Option<Unused>
255 }
256}
257struct_table! {
258 struct Function {
259 isJS: bool,
260 name: StringIndex,
261 resource: i32,
262 relevantForJS: bool,
263 file_name: StringIndex, // Or module-name for native functions
264 line_number: Option<NonZeroU32>,
265 column_number: Option<Unused>
266 }
267}
268struct_table! {
269 struct NativeAllocation {
270 time: FirefoxTime,
271 weight: isize, // This is actually bytes
272 stack: Option<StackIndex>,
273 memory_address: usize,
274 thread_id: ThreadId
275 }
276 weight_type: &'static str
277}
278struct_table! {
279 struct NativeSymbol {
280 libIndex: Option<Unused>,
281 address: Option<Unused>,
282 name: StringIndex,
283 functionSize: Option<u32>
284 }
285}
286struct_table! {
287 struct Marker {
288 data: serde_json::Value,
289 name: StringIndex,
290 startTime: FirefoxTime,
291 endTime: Option<FirefoxTime>,
292 phase: u64, // TODO: This seems interesting - how can we use it?
293 category: CategoryIndex
294 }
295}
296struct_table! {
297 struct Resource {
298 lib: Unused,
299 name: Unused,
300 host: Unused,
301 r#type: Unused
302 }
303}
304
305///////////////////////////////////////////////////////////////////////////////
306// Markers
307///////////////////////////////////////////////////////////////////////////////
308// Based off of https://github.com/firefox-devtools/profiler/blob/main/src/types/markers.js
309#[derive(Serialize, Debug)]
310#[serde(rename_all = "camelCase")]
311struct MarkerSchema {
312 name: &'static str,
313 tooltip_label: &'static str,
314 table_label: &'static str,
315 chart_label: &'static str,
316 display: &'static [&'static str],
317 data: &'static [MarkerDataSchema],
318}
319
320#[derive(Serialize, Debug)]
321struct MarkerDataSchema {
322 key: &'static str,
323 label: &'static str,
324 format: &'static str,
325 searchable: bool,
326}
327
328const MARKER_DISPLAYS_ALL: &[&str] = &[
329 "marker-chart",
330 "marker-table",
331 "timeline-overview",
332 "stack-chart",
333];
334
335const MARKER_SCHEMAS_SUPPORTED: &[MarkerSchema] = &[
336 MarkerSchema {
337 name: "Log",
338 tooltip_label: "Log event from {marker.data.origin}",
339 chart_label: "{marker.data.origin}",
340 table_label: "{marker.data.origin}: {marker.data.value}",
341 display: MARKER_DISPLAYS_ALL,
342 data: &[
343 MarkerDataSchema {
344 key: "origin",
345 label: "Log Origin",
346 format: "string",
347 searchable: true,
348 },
349 MarkerDataSchema {
350 key: "value",
351 label: "Value",
352 format: "string",
353 searchable: true,
354 },
355 ],
356 },
357 MarkerSchema {
358 name: "Import",
359 tooltip_label: "Imported {marker.data.module}",
360 chart_label: "{marker.data.module}",
361 table_label: "imported {marker.data.module}",
362 display: MARKER_DISPLAYS_ALL,
363 data: &[MarkerDataSchema {
364 key: "module",
365 label: "Imported Module",
366 format: "string",
367 searchable: true,
368 }],
369 },
370 MarkerSchema {
371 name: "Exception",
372 tooltip_label: "Exception {marker.data.exception} in {marker.data.module}",
373 chart_label: "{marker.data.exception}",
374 table_label: "Exception {marker.data.exception}: {marker.data.value}",
375 display: MARKER_DISPLAYS_ALL,
376 data: &[
377 MarkerDataSchema {
378 key: "exception",
379 label: "Exception Type",
380 format: "string",
381 searchable: true,
382 },
383 MarkerDataSchema {
384 key: "value",
385 label: "Exception Data",
386 format: "string",
387 searchable: true,
388 },
389 MarkerDataSchema {
390 key: "module",
391 label: "Module",
392 format: "string",
393 searchable: true,
394 },
395 ],
396 },
397];
398
399///////////////////////////////////////////////////////////////////////////////
400// Top-Level Structures
401///////////////////////////////////////////////////////////////////////////////
402#[derive(Serialize, Debug)]
403#[serde(rename_all = "camelCase")]
404struct Metadata {
405 ///////////////////////////////////////////////////////////////////////////
406 // External API
407 ///////////////////////////////////////////////////////////////////////////
408 interval: f32,
409 /// Number of ms since Unix epoch time
410 start_time: f64,
411 /// Should be 0
412 process_type: u32,
413 categories: &'static [Category],
414 /// Should be 0
415 stackwalk: u32,
416 debug: bool,
417 version: u32,
418 preprocessed_profile_version: u32,
419 symbolicated: bool,
420
421 product: String,
422 #[serde(rename = "appBuildID")]
423 app_build_id: String,
424 abi: String,
425 platform: String,
426 misc: String,
427
428 #[serde(rename = "physicalCPUs")]
429 physical_cpus: usize,
430 #[serde(rename = "logicalCPUs")]
431 logical_cpus: usize,
432
433 marker_schema: &'static [MarkerSchema],
434
435 ///////////////////////////////////////////////////////////////////////////
436 // Internal API
437 ///////////////////////////////////////////////////////////////////////////
438 /// The duration corresponding to `self.start_time`.
439 #[serde(skip_serializing)]
440 start_duration: Duration,
441}
442
443/// The `FirefoxProfile` representation of a thread, with additional internal augmentations to
444/// store data necessary for efficiently parsing the thread's trace log.
445///
446/// NOTE: This is designed to be usable entirely standalone from a `FirefoxProfile` during parsing.
447#[derive(Serialize, Debug, Default)]
448#[serde(rename_all = "camelCase")]
449pub struct FirefoxThread {
450 ///////////////////////////////////////////////////////////////////////////
451 // External API
452 ///////////////////////////////////////////////////////////////////////////
453 process_type: &'static str,
454 process_startup_time: HighResFirefoxTime, // Always the same as `register_time`
455 // TODO: We should use HighResFirefoxTime internally and only truncate when writing out to
456 // functions.
457 process_shutdown_time: HighResFirefoxTime,
458 register_time: HighResFirefoxTime,
459 unregister_time: Option<Unused>, // Always None
460 paused_ranges: Vec<Unused>,
461 name: &'static str,
462 is_main_thread: bool,
463 process_name: String,
464 is_js_tracer: bool,
465 pid: String,
466 tid: ThreadId,
467
468 samples: SampleTable,
469 markers: MarkerTable,
470 stack_table: StackTable,
471 frame_table: FrameTable,
472 func_table: FunctionTable,
473 string_array: IndexSet<String, RandomState>,
474
475 // If an empty allocation table is emitted, the profiler silently fails.
476 #[serde(skip_serializing_if = "NativeAllocationTable::is_empty")]
477 native_allocations: NativeAllocationTable,
478
479 ///////////////////////////////////////////////////////////////////////////
480 // External (Unused)
481 ///////////////////////////////////////////////////////////////////////////
482 resource_table: ResourceTable,
483 native_symbols: NativeSymbolTable,
484
485 ///////////////////////////////////////////////////////////////////////////
486 // Internal
487 ///////////////////////////////////////////////////////////////////////////
488 /// A stack of sample indices, allowing calls/returns to know what their
489 /// parent was.
490 #[serde(skip_serializing)]
491 calls: Vec<SampleIndex>,
492
493 /// A mapping of function UIDs to the de-duplicated store in `func_table`.
494 #[serde(skip_serializing)]
495 functions: HashMap<FunctionCall<'static>, FunctionIndex>,
496
497 /// The starting time for this thread, which all other times are supposed to be relative to.
498 /// This is a copy of meta.start_duration.
499 #[serde(skip_serializing)]
500 start_duration: Duration,
501
502 /// Track allocation address -> bytes.
503 // TODO: This should probably be COW when we do multiprocess support,
504 // and probably can't live on a Thread once we do multithread support.
505 #[serde(skip_serializing)]
506 allocations: HashMap<usize, usize>,
507
508 /// Track the set of Stacks (information about a (function, caller) pair) that exist,
509 /// preventing us from emitting duplicates.
510 #[serde(skip_serializing)]
511 existing_stacks: HashMap<(FunctionIndex, Option<StackIndex>), StackIndex>,
512}
513
514impl FirefoxProfile {
515 /// Create a new `FirefoxProfile` representing a given trace.
516 #[must_use]
517 pub fn new(info: TraceInitialization) -> FirefoxProfile {
518 FirefoxProfile {
519 meta: Metadata {
520 // Mark the time we received this message as the time the program started.
521 // Additionally, the process being traced sent us a Duration that we should record.
522 start_time: SystemTime::now()
523 .duration_since(SystemTime::UNIX_EPOCH)
524 .map_or(0.0, |n| n.as_secs_f64() * 1000.0),
525 start_duration: info.time,
526 interval: 0.000_001, // 1ns
527 process_type: 0,
528 categories: &CATEGORIES,
529 stackwalk: 0,
530
531 // Latest versions as of Aug 2023.
532 // Follow the migration code at
533 // https://github.com/firefox-devtools/profiler/blob/main/src/profile-logic/processed-profile-versioning.js
534 // to update these.
535 version: 27,
536 preprocessed_profile_version: 47,
537 debug: false,
538 symbolicated: true,
539 physical_cpus: num_cpus::get_physical(),
540 logical_cpus: num_cpus::get(),
541
542 product: info.program_name,
543 app_build_id: info.program_version,
544 abi: info.lang_version,
545 platform: info.platform,
546 misc: "<git revision>".to_string(),
547
548 marker_schema: MARKER_SCHEMAS_SUPPORTED,
549 },
550 counters: Vec::new(),
551 threads: Vec::new(),
552 libs: Vec::new(),
553
554 thread_count: 0,
555 }
556 }
557
558 /// Register a new thread, allowing us to parse traces for it without needing ownership of the
559 /// `FirefoxProfile` object.
560 pub fn register_thread(&mut self) -> FirefoxProfileThreadId {
561 self.thread_count += 1;
562
563 FirefoxProfileThreadId {
564 tid: self.thread_count,
565 start_duration: self.meta.start_duration,
566 }
567 }
568
569 /// Finalize this thread, attaching it to the profile.
570 pub fn finalize_thread(&mut self, thread: FirefoxThread) {
571 // Add this thread to the profile.
572 self.threads.push(thread);
573 }
574
575 /// Export the recorded profile to the given output directory.
576 pub fn export(&mut self, output_dir: PathBuf) -> Result<()> {
577 // All threads have exited and we're the last one recording.
578 // Sort threads by the time they started, to ensure earlier threads show higher in the
579 // profile.
580 self.threads.sort_by(|x, y| {
581 x.register_time
582 .partial_cmp(&y.register_time)
583 .expect("Registration time is reasonable")
584 });
585
586 // Determine when the last thread exited, in case we need to update other threads to point
587 // to that.
588 let end_time = self
589 .threads
590 .iter()
591 .map(|x| x.process_shutdown_time)
592 .fold(0.0, HighResFirefoxTime::max);
593
594 // The set of pids we've seen so far. This tells us whether a thread that is registering
595 // should be the main thread for its pid.
596 // NOTE: This assumes pids aren't reused, as otherwise top-level processes will be attached as
597 // threads under a dead process.
598 let mut registered_pids = HashSet::default();
599
600 // Fixup some metadata since we have better ordering information now.
601 for thread in &mut self.threads {
602 thread.name = if registered_pids.contains(&thread.pid) {
603 "thread"
604 } else {
605 thread.is_main_thread = true;
606 "GeckoMain"
607 };
608
609 registered_pids.insert(thread.pid.clone());
610
611 if thread.functions.is_empty() {
612 // If we didn't log any calls, attach a long call to ensure the user will know this
613 // occurred.
614 //
615 // TODO: This happens way too often, which seems likely due to the Unix socket
616 // approach of explicitly sending data across.
617 // If we rewrote `py-functiontrace` into Rust, we could easily use shared memory
618 // and have all data be sent ~instantly to the server. This would both simplify
619 // the server-communication (no more IO thread or `TraceStreamer` for
620 // dealing with partial reads, and would also mean we'd never lose data when
621 // clients exit too early.
622 let message = "[WARNING] FunctionTrace didn't log any data for this thread. This may be because the thread crashed or otherwise exited before flushing its trace buffer.";
623 thread.emit_call(message, thread.register_time, "FunctionTrace", None);
624 thread.emit_return(message, end_time);
625
626 // For some reason, we need to emit multiple functions for the Firefox Profiler to
627 // realize that this should display with a real width. Emit a 0-width call to do
628 // this.
629 thread.emit_call(message, end_time, "FunctionTrace", None);
630 thread.emit_return(message, end_time);
631
632 thread.process_shutdown_time = end_time;
633 }
634 }
635
636 // TODO: If we find empty threads with a parent process that has data, we should delete the
637 // threads and add a marker about them to the parent process.
638
639 // We gzip profiles to save some disk space when transferring files. However, this can
640 // take a significant amount of time, which may not be worth it, so it can be disabled with
641 // this environment variable.
642 let compressed = std::env::var("FUNCTIONTRACE_COMPRESSION")
643 .map(|x| !matches!(x.as_str(), "false" | "no" | "disable"))
644 .unwrap_or(true);
645
646 // Pick a reasonably unique filename in the output directory to write the profile to.
647 let output_path = {
648 let now = chrono::Local::now();
649
650 output_dir.join(format!(
651 "functiontrace.{}.json{}",
652 now.format("%F-%T.%3f"),
653 if compressed { ".gz" } else { "" }
654 ))
655 };
656
657 // Emit the current profile, rendering a spinner while waiting for it to compress.
658 {
659 use spinoff::spinners::{Arrow3, SpinnerFrames};
660
661 // Track the number of bytes we've written.
662 let bytes = std::sync::atomic::AtomicUsize::new(0);
663
664 let mut spinner = spinoff::Spinner::new(
665 Arrow3,
666 "Exporting FunctionTrace profile...",
667 spinoff::Color::Blue,
668 );
669 let gzipped_bytes = std::thread::scope(|s| {
670 // Spawn a background thread to keep the spinner updated, then kick off the JSON
671 // encoding.
672 s.spawn(|| {
673 let spin_interval = {
674 let spinner_frame = SpinnerFrames::from(Arrow3);
675
676 Duration::from_millis(
677 spinner_frame.frames.len() as u64 * spinner_frame.interval as u64,
678 )
679 };
680
681 loop {
682 std::thread::sleep(spin_interval);
683 let written = bytes.load(Ordering::Relaxed);
684
685 if written == usize::MAX {
686 break;
687 }
688
689 spinner.update_text(format!(
690 "Exporting FunctionTrace profile... {}",
691 ByteSize(written as u64).display().iec_short()
692 ));
693 }
694 });
695
696 // Note: Ordering on these streams matters - if we put the progress tracker in
697 // front of [`BufWriter`], we'll end up getting notified every few bytes.
698 let progress_writer = BufWriter::new(progress_streams::ProgressWriter::new(
699 File::create(&output_path)?,
700 |progress: usize| {
701 bytes.fetch_add(progress, Ordering::Relaxed);
702 },
703 ));
704
705 if compressed {
706 // NOTE: We buffer writes before doing compression, since that allows flate2 to
707 // work with much larger chunks and provides a ~10x speedup over unbuffered.
708 let gzip = BufWriter::new(flate2::write::GzEncoder::new(
709 progress_writer,
710 // Level 4 appears to be significantly faster than the default (6) on this
711 // format, while also only using a few percent more disk space.
712 flate2::Compression::new(4),
713 ));
714
715 serde_json::to_writer(gzip, &self)?;
716 } else {
717 serde_json::to_writer(progress_writer, &self)?;
718 }
719
720 // Notfiy the spinner thread that we're done by using [`usize::MAX`] as a flag
721 // value.
722 Ok::<_, std::io::Error>(bytes.swap(usize::MAX, Ordering::SeqCst))
723 })?;
724
725 spinner.success(&format!(
726 "Exported FunctionTrace profile to {} ({})",
727 output_path.display().bold().italic(),
728 ByteSize(gzipped_bytes as u64).display().iec_short()
729 ));
730 };
731
732 // Link `functiontrace.latest.json.gz` to this file. We need to remove any existing symlink
733 // before doing so.
734 let symlink = output_dir.join(format!(
735 "functiontrace.latest.json{}",
736 if compressed { ".gz" } else { "" }
737 ));
738 let _ = std::fs::remove_file(&symlink);
739 std::os::unix::fs::symlink(&output_path, &symlink)
740 .wrap_err("Symlinking functiontrace.latest.json.gz failed")?;
741
742 Ok(())
743 }
744}
745
746impl FirefoxThread {
747 /// Create a new `FirefoxThread`, given some information about it and a
748 /// `FirefoxProfileThreadId` granting permission to do so.
749 #[must_use]
750 pub fn new(registration: ThreadRegistration, thread: &FirefoxProfileThreadId) -> FirefoxThread {
751 let startup_time = (registration.time - thread.start_duration).as_secs_f64() * 1000.0;
752
753 FirefoxThread {
754 process_type: "default",
755 process_startup_time: startup_time,
756 process_shutdown_time: 0.0, // Updated on each event
757 register_time: startup_time,
758 start_duration: thread.start_duration,
759 unregister_time: None,
760 name: "<filled in at export>",
761 process_name: registration.program_name,
762 is_js_tracer: true,
763 pid: registration.pid.to_string(),
764 tid: thread.tid,
765 samples: SampleTable::new("tracing-ms"),
766 native_allocations: NativeAllocationTable::new("bytes"),
767 ..Default::default()
768 }
769 }
770
771 /// Times come in as seconds but need to be converted to milliseconds and adjusted to an offset
772 /// from `FirefoxThread::start_time`.
773 fn offset_time(&self, time: Duration) -> HighResFirefoxTime {
774 (time - self.start_duration).as_secs_f64() * 1000.0
775 }
776
777 /// We use an `IndexSet` to store our strings, but this makes for slightly more complicated code
778 /// if we're trying to avoid unnecessary allocations. This is a small wrapper for those.
779 fn unique_string(&mut self, string: &str) -> StringIndex {
780 match self.string_array.get_full(string) {
781 Some((index, _)) => index,
782 None => self.string_array.insert_full(string.to_string()).0,
783 }
784 }
785
786 fn emit_call(
787 &mut self,
788 name: &str,
789 time: HighResFirefoxTime,
790 file_or_module: &str,
791 line_number: Option<NonZeroU32>,
792 ) -> HighResFirefoxTime {
793 // Create a UID for this function to avoid creating multiple entries in the output for it.
794 let (func_uid, category) = match line_number {
795 // Only native functions don't have known line numbers.
796 None => (
797 FunctionCall::Native {
798 function_name: std::borrow::Cow::Borrowed(name),
799 module: std::borrow::Cow::Borrowed(file_or_module),
800 },
801 CategoryIndex::Native,
802 ),
803 Some(line) => (
804 FunctionCall::Python {
805 function_name: std::borrow::Cow::Borrowed(name),
806 module: std::borrow::Cow::Borrowed(file_or_module),
807 line,
808 },
809 CategoryIndex::Python,
810 ),
811 };
812
813 // Ensure we have a FunctionTable and FrameTable entry for this function.
814 let function_id = match self.functions.get(&func_uid) {
815 Some(&x) => x,
816 None => {
817 // We haven't seen this function before. Reserve an index in the
818 // FunctionTable for it.
819 let funcname_id = self.unique_string(name);
820 let filename_id = self.unique_string(file_or_module);
821
822 // Create a function and frame entry for this function.
823 // There's currently a one-to-one mapping of these.
824 let function_id = self.func_table.add(Function {
825 isJS: false,
826 name: funcname_id,
827 resource: -1,
828 relevantForJS: false,
829 file_name: filename_id,
830 line_number,
831 column_number: None,
832 });
833 self.frame_table.add(Frame {
834 func: function_id,
835 category,
836 subcategory: 0,
837 address: None,
838 inlineDepth: 0,
839 nativeSymbol: None,
840 innerWindowID: None,
841 implementation: None,
842 line: None,
843 column: None,
844 });
845
846 // We need to allocate to put this in the function table.
847 let func_uid = match func_uid {
848 FunctionCall::Native { .. } => FunctionCall::Native {
849 function_name: std::borrow::Cow::Owned(name.to_string()),
850 module: std::borrow::Cow::Owned(file_or_module.to_string()),
851 },
852 FunctionCall::Python { line, .. } => FunctionCall::Python {
853 function_name: std::borrow::Cow::Owned(name.to_string()),
854 module: std::borrow::Cow::Owned(file_or_module.to_string()),
855 line,
856 },
857 };
858
859 self.functions.insert(func_uid, function_id);
860 function_id
861 }
862 };
863 // Frames and functions must be equivalent.
864 let frame_id = FrameIndex(function_id.0);
865
866 // This stack belongs under the previous caller. To find it, we look at the last caller
867 // sample to find the stack attached to that sample.
868 let prefix = self.calls.last().map(|&x| *self.samples.get_stack(x));
869
870 // Ensure we only emit one StackTable entry per stack, as there can be duplicates caused by
871 // things like loops.
872 // This will increase profile generation memory usage, but decreases the emitted profile
873 // size by a substantial amount (>25% on some testcases).
874 let stack_id = *self
875 .existing_stacks
876 .entry((function_id, prefix))
877 .or_insert_with(|| {
878 self.stack_table.add(Stack {
879 frame: frame_id,
880 subcategory: 0,
881 category,
882 prefix,
883 })
884 });
885
886 // Emit a sample and a stacktrace.
887 let samples_id = self.samples.add(Sample {
888 stack: stack_id,
889 time: time as FirefoxTime,
890 weight: None, // We'll overwrite this during the return.
891 });
892 self.calls.push(samples_id);
893
894 time
895 }
896
897 fn emit_return(&mut self, func_name: &str, time: HighResFirefoxTime) -> HighResFirefoxTime {
898 // Find the call that this return corresponds to and update its duration. During startuo,
899 // we'll observe returns with an empty callstack - we ignore this.
900 while let Some(call_id) = self.calls.pop() {
901 // Given the call on top of the stack, verify that it was the one we expected. If it's
902 // wrong (this should be exceptional assuming the client is properly handling
903 // exceptions), we'll keep unwinding the stack until we find the correct call, marking
904 // all unwound functions as ending at the same time.
905
906 // Track the return of functions but not the call.
907 let caller_id = *self.func_table.get_name(
908 *self
909 .frame_table
910 .get_func(*self.stack_table.get_frame(*self.samples.get_stack(call_id))),
911 );
912
913 // Given our potential caller's id, we're most likely a match if the
914 // StringIndex for the function we're returning matches the caller id.
915 let matches = self
916 .string_array
917 .get_full(func_name)
918 .is_some_and(|(id, _)| id == caller_id);
919
920 // Mark the parent as having returned.
921 self.samples.weight[call_id.0 as usize] =
922 Some((time as FirefoxTime) - self.samples.get_time(call_id));
923
924 if matches {
925 // We've found the function we were supposed to be returning from.
926 break;
927 } else if let Some(returned_name) = self.string_array.get_index(caller_id) {
928 log::warn!(
929 "|{}| Returning from `{returned_name}` rather than `{func_name}` as expected",
930 self.tid
931 );
932 } else {
933 // We shouldn't ever return from a function that we don't have the name of.
934 log::error!(
935 "|{}| Returning from from unknown function rather than `{func_name}` as expected",
936 self.tid
937 );
938 }
939 }
940
941 time
942 }
943
944 /// Convert the given `FunctionTrace` into a meaningful format and attach it to the current
945 /// thread.
946 pub fn add_trace(&mut self, trace: FunctionTrace) {
947 let event_time = match trace {
948 FunctionTrace::Call {
949 time,
950 func_name,
951 filename,
952 linenumber,
953 } => self.emit_call(
954 &func_name,
955 self.offset_time(time),
956 &filename,
957 NonZeroU32::new(linenumber),
958 ),
959 FunctionTrace::NativeCall {
960 time,
961 func_name,
962 module_name,
963 } => self.emit_call(&func_name, self.offset_time(time), &module_name, None),
964 FunctionTrace::Return { time, func_name }
965 | FunctionTrace::NativeReturn { time, func_name } => {
966 self.emit_return(&func_name, self.offset_time(time))
967 }
968 FunctionTrace::Exception {
969 time,
970 exception_type,
971 exception_value,
972 filename,
973 linenumber,
974 } => {
975 // TODO: We could report a large amount of information here such as
976 // values of locals. For inspiration, see a django exception report.
977 let unique_type = self.unique_string(&exception_type);
978 let time = self.offset_time(time);
979
980 self.markers.add(Marker {
981 data: serde_json::json!({
982 "type": "Exception",
983 "exeption": exception_type,
984 "value": exception_value,
985 "module": format!("{}:{}", filename, linenumber)
986 }),
987 name: unique_type,
988 startTime: time as FirefoxTime,
989 endTime: None,
990 phase: 0,
991 category: CategoryIndex::Exceptions,
992 });
993
994 time
995 }
996 FunctionTrace::Log {
997 time,
998 log_type,
999 log_value,
1000 } => {
1001 let time = self.offset_time(time);
1002 let unique_type = self.unique_string(&log_type);
1003
1004 self.markers.add(Marker {
1005 data: serde_json::json!({
1006 "type": "Log",
1007 "origin": log_type,
1008 "value": log_value,
1009 }),
1010 name: unique_type,
1011 startTime: time as FirefoxTime,
1012 endTime: None,
1013 phase: 0,
1014 category: CategoryIndex::Logs,
1015 });
1016
1017 time
1018 }
1019 FunctionTrace::Import { time, module_name } => {
1020 let time = self.offset_time(time);
1021 let unique_type = self.unique_string("Import");
1022
1023 self.markers.add(Marker {
1024 data: serde_json::json!({
1025 "type": "Import",
1026 "module": module_name,
1027 }),
1028 name: unique_type,
1029 startTime: time as FirefoxTime,
1030 endTime: None,
1031 phase: 0,
1032 category: CategoryIndex::Imports,
1033 });
1034
1035 time
1036 }
1037 FunctionTrace::Allocation { time, details } => {
1038 let time = self.offset_time(time);
1039 let stack = self.calls.last().map(|&x| *self.samples.get_stack(x));
1040
1041 match details {
1042 AllocationDetails::Alloc { bytes, addr } => {
1043 self.allocations.insert(addr, bytes);
1044
1045 self.native_allocations.add(NativeAllocation {
1046 time: time as FirefoxTime,
1047 weight: bytes as isize,
1048 stack,
1049 memory_address: addr,
1050 thread_id: self.tid,
1051 });
1052 }
1053 AllocationDetails::Realloc {
1054 bytes,
1055 old_addr,
1056 new_addr,
1057 } => {
1058 // Remove the old allocation first.
1059 // If we don't find the allocation to free, it's either a bug or the
1060 // allocation occurred before we started tracing. Assume the latter.
1061 let old_bytes = self.allocations.remove(&old_addr).unwrap_or(0);
1062 self.native_allocations.add(NativeAllocation {
1063 time: time as FirefoxTime,
1064 weight: -(old_bytes as isize),
1065 stack,
1066 memory_address: old_addr,
1067 thread_id: self.tid,
1068 });
1069
1070 // Now add the new one.
1071 self.allocations.insert(new_addr, bytes);
1072 self.native_allocations.add(NativeAllocation {
1073 time: time as FirefoxTime,
1074 weight: bytes as isize,
1075 stack,
1076 memory_address: new_addr,
1077 thread_id: self.tid,
1078 });
1079 }
1080 AllocationDetails::Free { old_addr } => {
1081 // If we don't find the allocation to free, it's either a bug or the
1082 // allocation occurred before we started tracing. Assume the latter.
1083 let bytes = self.allocations.remove(&old_addr).unwrap_or(0);
1084
1085 self.native_allocations.add(NativeAllocation {
1086 time: time as FirefoxTime,
1087 weight: -(bytes as isize),
1088 stack,
1089 memory_address: old_addr,
1090 thread_id: self.tid,
1091 });
1092 }
1093 }
1094
1095 time
1096 }
1097 FunctionTrace::RegisterThread(_) => unreachable!(),
1098 };
1099
1100 // Track what the last event that occurred on our thread was.
1101 self.process_shutdown_time = self.process_shutdown_time.max(event_time);
1102 }
1103}