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}