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 let gzip = flate2::write::GzEncoder::new(
707 progress_writer,
708 // Level 4 appears to be significantly faster than the default (6) on this
709 // format, while also only using a few percent more disk space.
710 flate2::Compression::new(4),
711 );
712
713 serde_json::to_writer(gzip, &self)?;
714 } else {
715 serde_json::to_writer(progress_writer, &self)?;
716 }
717
718 // Notfiy the spinner thread that we're done by using [`usize::MAX`] as a flag
719 // value.
720 Ok::<_, std::io::Error>(bytes.swap(usize::MAX, Ordering::SeqCst))
721 })?;
722
723 spinner.success(&format!(
724 "Exported FunctionTrace profile to {} ({})",
725 output_path.display().bold().italic(),
726 ByteSize(gzipped_bytes as u64).display().iec_short()
727 ));
728 };
729
730 // Link `functiontrace.latest.json.gz` to this file. We need to remove any existing symlink
731 // before doing so.
732 let symlink = output_dir.join(format!(
733 "functiontrace.latest.json{}",
734 if compressed { ".gz" } else { "" }
735 ));
736 let _ = std::fs::remove_file(&symlink);
737 std::os::unix::fs::symlink(&output_path, &symlink)
738 .wrap_err("Symlinking functiontrace.latest.json.gz failed")?;
739
740 Ok(())
741 }
742}
743
744impl FirefoxThread {
745 /// Create a new `FirefoxThread`, given some information about it and a
746 /// `FirefoxProfileThreadId` granting permission to do so.
747 #[must_use]
748 pub fn new(registration: ThreadRegistration, thread: &FirefoxProfileThreadId) -> FirefoxThread {
749 let startup_time = (registration.time - thread.start_duration).as_secs_f64() * 1000.0;
750
751 FirefoxThread {
752 process_type: "default",
753 process_startup_time: startup_time,
754 process_shutdown_time: 0.0, // Updated on each event
755 register_time: startup_time,
756 start_duration: thread.start_duration,
757 unregister_time: None,
758 name: "<filled in at export>",
759 process_name: registration.program_name,
760 is_js_tracer: true,
761 pid: registration.pid.to_string(),
762 tid: thread.tid,
763 samples: SampleTable::new("tracing-ms"),
764 native_allocations: NativeAllocationTable::new("bytes"),
765 ..Default::default()
766 }
767 }
768
769 /// Times come in as seconds but need to be converted to milliseconds and adjusted to an offset
770 /// from `FirefoxThread::start_time`.
771 fn offset_time(&self, time: Duration) -> HighResFirefoxTime {
772 (time - self.start_duration).as_secs_f64() * 1000.0
773 }
774
775 /// We use an `IndexSet` to store our strings, but this makes for slightly more complicated code
776 /// if we're trying to avoid unnecessary allocations. This is a small wrapper for those.
777 fn unique_string(&mut self, string: &str) -> StringIndex {
778 match self.string_array.get_full(string) {
779 Some((index, _)) => index,
780 None => self.string_array.insert_full(string.to_string()).0,
781 }
782 }
783
784 fn emit_call(
785 &mut self,
786 name: &str,
787 time: HighResFirefoxTime,
788 file_or_module: &str,
789 line_number: Option<NonZeroU32>,
790 ) -> HighResFirefoxTime {
791 // Create a UID for this function to avoid creating multiple entries in the output for it.
792 let (func_uid, category) = match line_number {
793 // Only native functions don't have known line numbers.
794 None => (
795 FunctionCall::Native {
796 function_name: std::borrow::Cow::Borrowed(name),
797 module: std::borrow::Cow::Borrowed(file_or_module),
798 },
799 CategoryIndex::Native,
800 ),
801 Some(line) => (
802 FunctionCall::Python {
803 function_name: std::borrow::Cow::Borrowed(name),
804 module: std::borrow::Cow::Borrowed(file_or_module),
805 line,
806 },
807 CategoryIndex::Python,
808 ),
809 };
810
811 // Ensure we have a FunctionTable and FrameTable entry for this function.
812 let function_id = match self.functions.get(&func_uid) {
813 Some(&x) => x,
814 None => {
815 // We haven't seen this function before. Reserve an index in the
816 // FunctionTable for it.
817 let funcname_id = self.unique_string(name);
818 let filename_id = self.unique_string(file_or_module);
819
820 // Create a function and frame entry for this function.
821 // There's currently a one-to-one mapping of these.
822 let function_id = self.func_table.add(Function {
823 isJS: false,
824 name: funcname_id,
825 resource: -1,
826 relevantForJS: false,
827 file_name: filename_id,
828 line_number,
829 column_number: None,
830 });
831 self.frame_table.add(Frame {
832 func: function_id,
833 category,
834 subcategory: 0,
835 address: None,
836 inlineDepth: 0,
837 nativeSymbol: None,
838 innerWindowID: None,
839 implementation: None,
840 line: None,
841 column: None,
842 });
843
844 // We need to allocate to put this in the function table.
845 let func_uid = match func_uid {
846 FunctionCall::Native { .. } => FunctionCall::Native {
847 function_name: std::borrow::Cow::Owned(name.to_string()),
848 module: std::borrow::Cow::Owned(file_or_module.to_string()),
849 },
850 FunctionCall::Python { line, .. } => FunctionCall::Python {
851 function_name: std::borrow::Cow::Owned(name.to_string()),
852 module: std::borrow::Cow::Owned(file_or_module.to_string()),
853 line,
854 },
855 };
856
857 self.functions.insert(func_uid, function_id);
858 function_id
859 }
860 };
861 // Frames and functions must be equivalent.
862 let frame_id = FrameIndex(function_id.0);
863
864 // This stack belongs under the previous caller. To find it, we look at the last caller
865 // sample to find the stack attached to that sample.
866 let prefix = self.calls.last().map(|&x| *self.samples.get_stack(x));
867
868 // Ensure we only emit one StackTable entry per stack, as there can be duplicates caused by
869 // things like loops.
870 // This will increase profile generation memory usage, but decreases the emitted profile
871 // size by a substantial amount (>25% on some testcases).
872 let stack_id = *self
873 .existing_stacks
874 .entry((function_id, prefix))
875 .or_insert_with(|| {
876 self.stack_table.add(Stack {
877 frame: frame_id,
878 subcategory: 0,
879 category,
880 prefix,
881 })
882 });
883
884 // Emit a sample and a stacktrace.
885 let samples_id = self.samples.add(Sample {
886 stack: stack_id,
887 time: time as FirefoxTime,
888 weight: None, // We'll overwrite this during the return.
889 });
890 self.calls.push(samples_id);
891
892 time
893 }
894
895 fn emit_return(&mut self, func_name: &str, time: HighResFirefoxTime) -> HighResFirefoxTime {
896 // Find the call that this return corresponds to and update its duration. During startuo,
897 // we'll observe returns with an empty callstack - we ignore this.
898 while let Some(call_id) = self.calls.pop() {
899 // Given the call on top of the stack, verify that it was the one we expected. If
900 // it's wrong, we'll keep unwinding the stack until we find the correct call,
901 // marking all unwound functions as ending at the same time.
902 let matches = {
903 // Track the return of functions but not the call.
904 let caller_id = *self.func_table.get_name(
905 *self
906 .frame_table
907 .get_func(*self.stack_table.get_frame(*self.samples.get_stack(call_id))),
908 );
909
910 // Given our potential caller's id, we're most likely a match if the
911 // StringIndex for the function we're returning matches the caller id.
912 self.string_array
913 .get_full(func_name)
914 .map_or(false, |(id, _)| id == caller_id)
915 };
916
917 // Mark the parent as having returned.
918 self.samples.weight[call_id.0 as usize] =
919 Some((time as FirefoxTime) - self.samples.get_time(call_id));
920
921 if matches {
922 // We've found the function we were supposed to be returning from.
923 break;
924 }
925
926 log::trace!(
927 "Returning from `{}` which was not the most recently called function",
928 func_name
929 );
930 }
931
932 time
933 }
934
935 /// Convert the given `FunctionTrace` into a meaningful format and attach it to the current
936 /// thread.
937 pub fn add_trace(&mut self, trace: FunctionTrace) {
938 let event_time = match trace {
939 FunctionTrace::Call {
940 time,
941 func_name,
942 filename,
943 linenumber,
944 } => self.emit_call(
945 &func_name,
946 self.offset_time(time),
947 &filename,
948 Some(linenumber),
949 ),
950 FunctionTrace::NativeCall {
951 time,
952 func_name,
953 module_name,
954 } => self.emit_call(&func_name, self.offset_time(time), &module_name, None),
955 FunctionTrace::Return { time, func_name }
956 | FunctionTrace::NativeReturn { time, func_name } => {
957 self.emit_return(&func_name, self.offset_time(time))
958 }
959 FunctionTrace::Exception {
960 time,
961 exception_type,
962 exception_value,
963 filename,
964 linenumber,
965 } => {
966 // TODO: We could report a large amount of information here such as
967 // values of locals. For inspiration, see a django exception report.
968 let unique_type = self.unique_string(&exception_type);
969 let time = self.offset_time(time);
970
971 self.markers.add(Marker {
972 data: serde_json::json!({
973 "type": "Exception",
974 "exeption": exception_type,
975 "value": exception_value,
976 "module": format!("{}:{}", filename, linenumber)
977 }),
978 name: unique_type,
979 startTime: time as FirefoxTime,
980 endTime: None,
981 phase: 0,
982 category: CategoryIndex::Exceptions,
983 });
984
985 time
986 }
987 FunctionTrace::Log {
988 time,
989 log_type,
990 log_value,
991 } => {
992 let time = self.offset_time(time);
993 let unique_type = self.unique_string(&log_type);
994
995 self.markers.add(Marker {
996 data: serde_json::json!({
997 "type": "Log",
998 "origin": log_type,
999 "value": log_value,
1000 }),
1001 name: unique_type,
1002 startTime: time as FirefoxTime,
1003 endTime: None,
1004 phase: 0,
1005 category: CategoryIndex::Logs,
1006 });
1007
1008 time
1009 }
1010 FunctionTrace::Import { time, module_name } => {
1011 let time = self.offset_time(time);
1012 let unique_type = self.unique_string("Import");
1013
1014 self.markers.add(Marker {
1015 data: serde_json::json!({
1016 "type": "Import",
1017 "module": module_name,
1018 }),
1019 name: unique_type,
1020 startTime: time as FirefoxTime,
1021 endTime: None,
1022 phase: 0,
1023 category: CategoryIndex::Imports,
1024 });
1025
1026 time
1027 }
1028 FunctionTrace::Allocation { time, details } => {
1029 let time = self.offset_time(time);
1030 let stack = self.calls.last().map(|&x| *self.samples.get_stack(x));
1031
1032 match details {
1033 AllocationDetails::Alloc { bytes, addr } => {
1034 self.allocations.insert(addr, bytes);
1035
1036 self.native_allocations.add(NativeAllocation {
1037 time: time as FirefoxTime,
1038 weight: bytes as isize,
1039 stack,
1040 memory_address: addr,
1041 thread_id: self.tid,
1042 });
1043 }
1044 AllocationDetails::Realloc {
1045 bytes,
1046 old_addr,
1047 new_addr,
1048 } => {
1049 // Remove the old allocation first.
1050 // If we don't find the allocation to free, it's either a bug or the
1051 // allocation occurred before we started tracing. Assume the latter.
1052 let old_bytes = self.allocations.remove(&old_addr).unwrap_or(0);
1053 self.native_allocations.add(NativeAllocation {
1054 time: time as FirefoxTime,
1055 weight: -(old_bytes as isize),
1056 stack,
1057 memory_address: old_addr,
1058 thread_id: self.tid,
1059 });
1060
1061 // Now add the new one.
1062 self.allocations.insert(new_addr, bytes);
1063 self.native_allocations.add(NativeAllocation {
1064 time: time as FirefoxTime,
1065 weight: bytes as isize,
1066 stack,
1067 memory_address: new_addr,
1068 thread_id: self.tid,
1069 });
1070 }
1071 AllocationDetails::Free { old_addr } => {
1072 // If we don't find the allocation to free, it's either a bug or the
1073 // allocation occurred before we started tracing. Assume the latter.
1074 let bytes = self.allocations.remove(&old_addr).unwrap_or(0);
1075
1076 self.native_allocations.add(NativeAllocation {
1077 time: time as FirefoxTime,
1078 weight: -(bytes as isize),
1079 stack,
1080 memory_address: old_addr,
1081 thread_id: self.tid,
1082 });
1083 }
1084 }
1085
1086 time
1087 }
1088 FunctionTrace::RegisterThread(_) => unreachable!(),
1089 };
1090
1091 // Track what the last event that occurred on our thread was.
1092 self.process_shutdown_time = self.process_shutdown_time.max(event_time);
1093 }
1094}