functiontrace_server/
profile_generator.rs

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