functiontrace_server/
profile_generator.rs

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