fxprof_processed_profile/
profile.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
5use serde_json::json;
6
7use crate::category::{Category, CategoryHandle, CategoryPairHandle};
8use crate::category_color::CategoryColor;
9use crate::counters::{Counter, CounterHandle};
10use crate::cpu_delta::CpuDelta;
11use crate::fast_hash_map::FastHashMap;
12use crate::frame::{Frame, FrameInfo};
13use crate::frame_table::{InternalFrame, InternalFrameLocation};
14use crate::global_lib_table::{GlobalLibTable, LibraryHandle};
15use crate::lib_mappings::LibMappings;
16use crate::library_info::LibraryInfo;
17use crate::process::{Process, ThreadHandle};
18use crate::reference_timestamp::ReferenceTimestamp;
19use crate::string_table::{GlobalStringIndex, GlobalStringTable};
20use crate::thread::{ProcessHandle, Thread};
21use crate::{MarkerSchema, MarkerTiming, ProfilerMarker, SymbolTable, Timestamp};
22
23/// The sampling interval used during profile recording.
24///
25/// This doesn't have to match the actual delta between sample timestamps.
26/// It just describes the intended interval.
27///
28/// For profiles without sampling data, this can be set to a meaningless
29/// dummy value.
30#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
31pub struct SamplingInterval {
32    nanos: u64,
33}
34
35impl SamplingInterval {
36    /// Create a sampling interval from a sampling frequency in Hz.
37    ///
38    /// Panics on zero or negative values.
39    pub fn from_hz(samples_per_second: f32) -> Self {
40        assert!(samples_per_second > 0.0);
41        let nanos = (1_000_000_000.0 / samples_per_second) as u64;
42        Self::from_nanos(nanos)
43    }
44
45    /// Create a sampling interval from a value in milliseconds.
46    pub fn from_millis(millis: u64) -> Self {
47        Self::from_nanos(millis * 1_000_000)
48    }
49
50    /// Create a sampling interval from a value in nanoseconds
51    pub fn from_nanos(nanos: u64) -> Self {
52        Self { nanos }
53    }
54
55    /// Convert the interval to nanoseconds.
56    pub fn nanos(&self) -> u64 {
57        self.nanos
58    }
59
60    /// Convert the interval to float seconds.
61    pub fn as_secs_f64(&self) -> f64 {
62        self.nanos as f64 / 1_000_000_000.0
63    }
64}
65
66impl From<Duration> for SamplingInterval {
67    fn from(duration: Duration) -> Self {
68        Self::from_nanos(duration.as_nanos() as u64)
69    }
70}
71
72/// A handle for an interned string, returned from [`Profile::intern_string`].
73#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
74pub struct StringHandle(GlobalStringIndex);
75
76/// Stores the profile data and can be serialized as JSON, via [`serde::Serialize`].
77///
78/// The profile data is organized into a list of processes with threads.
79/// Each thread has its own samples and markers.
80///
81/// ```
82/// use fxprof_processed_profile::{Profile, CategoryHandle, CpuDelta, Frame, FrameInfo, FrameFlags, SamplingInterval, Timestamp};
83/// use std::time::SystemTime;
84///
85/// # fn write_profile(output_file: std::fs::File) -> Result<(), Box<dyn std::error::Error>> {
86/// let mut profile = Profile::new("My app", SystemTime::now().into(), SamplingInterval::from_millis(1));
87/// let process = profile.add_process("App process", 54132, Timestamp::from_millis_since_reference(0.0));
88/// let thread = profile.add_thread(process, 54132000, Timestamp::from_millis_since_reference(0.0), true);
89/// profile.set_thread_name(thread, "Main thread");
90/// let stack = vec![
91///     FrameInfo { frame: Frame::Label(profile.intern_string("Root node")), category_pair: CategoryHandle::OTHER.into(), flags: FrameFlags::empty() },
92///     FrameInfo { frame: Frame::Label(profile.intern_string("First callee")), category_pair: CategoryHandle::OTHER.into(), flags: FrameFlags::empty() }
93/// ];
94/// profile.add_sample(thread, Timestamp::from_millis_since_reference(0.0), stack.into_iter(), CpuDelta::ZERO, 1);
95///
96/// let writer = std::io::BufWriter::new(output_file);
97/// serde_json::to_writer(writer, &profile)?;
98/// # Ok(())
99/// # }
100/// ```
101#[derive(Debug)]
102pub struct Profile {
103    pub(crate) product: String,
104    pub(crate) interval: SamplingInterval,
105    pub(crate) global_libs: GlobalLibTable,
106    pub(crate) kernel_libs: LibMappings<LibraryHandle>,
107    pub(crate) categories: Vec<Category>, // append-only for stable CategoryHandles
108    pub(crate) processes: Vec<Process>,   // append-only for stable ProcessHandles
109    pub(crate) counters: Vec<Counter>,
110    pub(crate) threads: Vec<Thread>, // append-only for stable ThreadHandles
111    pub(crate) reference_timestamp: ReferenceTimestamp,
112    pub(crate) string_table: GlobalStringTable,
113    pub(crate) marker_schemas: FastHashMap<&'static str, MarkerSchema>,
114    used_pids: FastHashMap<u32, u32>,
115    used_tids: FastHashMap<u32, u32>,
116}
117
118impl Profile {
119    /// Create a new profile.
120    ///
121    /// The `product` is the name of the main application which was profiled.
122    /// The `reference_timestamp` is some arbitrary absolute timestamp which all
123    /// other timestamps in the profile data are relative to. The `interval` is the intended
124    /// time delta between samples.
125    pub fn new(
126        product: &str,
127        reference_timestamp: ReferenceTimestamp,
128        interval: SamplingInterval,
129    ) -> Self {
130        Profile {
131            interval,
132            product: product.to_string(),
133            threads: Vec::new(),
134            global_libs: GlobalLibTable::new(),
135            kernel_libs: LibMappings::new(),
136            reference_timestamp,
137            processes: Vec::new(),
138            string_table: GlobalStringTable::new(),
139            marker_schemas: FastHashMap::default(),
140            categories: vec![Category {
141                name: "Other".to_string(),
142                color: CategoryColor::Gray,
143                subcategories: Vec::new(),
144            }],
145            used_pids: FastHashMap::default(),
146            used_tids: FastHashMap::default(),
147            counters: Vec::new(),
148        }
149    }
150
151    /// Change the declared sampling interval.
152    pub fn set_interval(&mut self, interval: SamplingInterval) {
153        self.interval = interval;
154    }
155
156    /// Change the reference timestamp.
157    pub fn set_reference_timestamp(&mut self, reference_timestamp: ReferenceTimestamp) {
158        self.reference_timestamp = reference_timestamp;
159    }
160
161    /// Change the product name.
162    pub fn set_product(&mut self, product: &str) {
163        self.product = product.to_string();
164    }
165
166    /// Add a category and return its handle.
167    ///
168    /// Categories are used for stack frames and markers, as part of a "category pair".
169    pub fn add_category(&mut self, name: &str, color: CategoryColor) -> CategoryHandle {
170        let handle = CategoryHandle(self.categories.len() as u16);
171        self.categories.push(Category {
172            name: name.to_string(),
173            color,
174            subcategories: Vec::new(),
175        });
176        handle
177    }
178
179    /// Add a subcategory for a category, and return the "category pair" handle.
180    pub fn add_subcategory(&mut self, category: CategoryHandle, name: &str) -> CategoryPairHandle {
181        let subcategory = self.categories[category.0 as usize].add_subcategory(name.into());
182        CategoryPairHandle(category, Some(subcategory))
183    }
184
185    /// Add an empty process. The name, pid and start time can be changed afterwards,
186    /// but they are required here because they have to be present in the profile JSON.
187    pub fn add_process(&mut self, name: &str, pid: u32, start_time: Timestamp) -> ProcessHandle {
188        let pid = self.make_unique_pid(pid);
189        let handle = ProcessHandle(self.processes.len());
190        self.processes.push(Process::new(name, pid, start_time));
191        handle
192    }
193
194    fn make_unique_pid(&mut self, pid: u32) -> String {
195        Self::make_unique_pid_or_tid(&mut self.used_pids, pid)
196    }
197
198    fn make_unique_tid(&mut self, tid: u32) -> String {
199        Self::make_unique_pid_or_tid(&mut self.used_tids, tid)
200    }
201
202    /// Appends ".1" / ".2" etc. to the pid or tid if needed.
203    ///
204    /// The map contains the next suffix for each pid/tid, or no entry if the pid/tid
205    /// hasn't been used before and needs no suffix.
206    fn make_unique_pid_or_tid(map: &mut FastHashMap<u32, u32>, id: u32) -> String {
207        match map.entry(id) {
208            std::collections::hash_map::Entry::Occupied(mut entry) => {
209                let suffix = *entry.get();
210                *entry.get_mut() += 1;
211                format!("{id}.{suffix}")
212            }
213            std::collections::hash_map::Entry::Vacant(entry) => {
214                entry.insert(1);
215                format!("{id}")
216            }
217        }
218    }
219
220    /// Create a counter. Counters let you make graphs with a time axis and a Y axis. One example of a
221    /// counter is memory usage.
222    ///
223    /// # Example
224    ///
225    /// ```
226    /// use fxprof_processed_profile::{Profile, CategoryHandle, CpuDelta, Frame, SamplingInterval, Timestamp};
227    /// use std::time::SystemTime;
228    ///
229    /// let mut profile = Profile::new("My app", SystemTime::now().into(), SamplingInterval::from_millis(1));
230    /// let process = profile.add_process("App process", 54132, Timestamp::from_millis_since_reference(0.0));
231    /// let memory_counter = profile.add_counter(process, "malloc", "Memory", "Amount of allocated memory");
232    /// profile.add_counter_sample(memory_counter, Timestamp::from_millis_since_reference(0.0), 0.0, 0);
233    /// profile.add_counter_sample(memory_counter, Timestamp::from_millis_since_reference(1.0), 1000.0, 2);
234    /// profile.add_counter_sample(memory_counter, Timestamp::from_millis_since_reference(2.0), 800.0, 1);
235    /// ```
236    pub fn add_counter(
237        &mut self,
238        process: ProcessHandle,
239        name: &str,
240        category: &str,
241        description: &str,
242    ) -> CounterHandle {
243        let handle = CounterHandle(self.counters.len());
244        self.counters.push(Counter::new(
245            name,
246            category,
247            description,
248            process,
249            self.processes[process.0].pid(),
250        ));
251        handle
252    }
253
254    /// Change the start time of a process.
255    pub fn set_process_start_time(&mut self, process: ProcessHandle, start_time: Timestamp) {
256        self.processes[process.0].set_start_time(start_time);
257    }
258
259    /// Set the end time of a process.
260    pub fn set_process_end_time(&mut self, process: ProcessHandle, end_time: Timestamp) {
261        self.processes[process.0].set_end_time(end_time);
262    }
263
264    /// Change the name of a process.
265    pub fn set_process_name(&mut self, process: ProcessHandle, name: &str) {
266        self.processes[process.0].set_name(name);
267    }
268
269    /// Get the `LibraryHandle` for a library. This handle is used in [`Profile::add_lib_mapping`]
270    /// and in the pre-resolved [`Frame`] variants.
271    ///
272    /// Knowing the library information allows symbolication of native stacks once the
273    /// profile is opened in the Firefox Profiler.
274    pub fn add_lib(&mut self, library: LibraryInfo) -> LibraryHandle {
275        self.global_libs.handle_for_lib(library)
276    }
277
278    /// Set the symbol table for a library.
279    ///
280    /// This symbol table can also be specified in the [`LibraryInfo`] which is given to
281    /// [`Profile::add_lib`]. However, sometimes you may want to have the [`LibraryHandle`]
282    /// for a library before you know about all its symbols. In those cases, you can call
283    /// [`Profile::add_lib`] with `symbol_table` set to `None`, and then supply the symbol
284    /// table afterwards.
285    ///
286    /// Symbol tables are optional.
287    pub fn set_lib_symbol_table(&mut self, library: LibraryHandle, symbol_table: Arc<SymbolTable>) {
288        self.global_libs.set_lib_symbol_table(library, symbol_table);
289    }
290
291    /// For a given process, define where in the virtual memory of this process the given library
292    /// is mapped.
293    ///
294    /// Existing mappings which overlap with the range `start_avma..end_avma` will be removed.
295    ///
296    /// A single library can have multiple mappings in the same process.
297    ///
298    /// The new mapping will be respected by future [`Profile::add_sample`] calls, when resolving
299    /// absolute frame addresses to library-relative addresses.
300    pub fn add_lib_mapping(
301        &mut self,
302        process: ProcessHandle,
303        lib: LibraryHandle,
304        start_avma: u64,
305        end_avma: u64,
306        relative_address_at_start: u32,
307    ) {
308        self.processes[process.0].add_lib_mapping(
309            lib,
310            start_avma,
311            end_avma,
312            relative_address_at_start,
313        );
314    }
315
316    /// Mark the library mapping at the specified start address in the specified process as
317    /// unloaded, so that future calls to [`Profile::add_sample`] know about the removal.
318    pub fn remove_lib_mapping(&mut self, process: ProcessHandle, start_avma: u64) {
319        self.processes[process.0].remove_lib_mapping(start_avma);
320    }
321
322    /// Clear all library mappings in the specified process.
323    pub fn clear_process_lib_mappings(&mut self, process: ProcessHandle) {
324        self.processes[process.0].remove_all_lib_mappings();
325    }
326
327    /// Add a kernel library mapping. This allows symbolication of kernel stacks once the profile is
328    /// opened in the Firefox Profiler. Kernel libraries are global and not tied to a process.
329    ///
330    /// Each kernel library covers an address range in the kernel address space, which is
331    /// global across all processes. Future calls to [`Profile::add_sample`] with native
332    /// frames resolve the frame's code address with respect to the currently loaded kernel
333    /// and process libraries.
334    pub fn add_kernel_lib_mapping(
335        &mut self,
336        lib: LibraryHandle,
337        start_avma: u64,
338        end_avma: u64,
339        relative_address_at_start: u32,
340    ) {
341        self.kernel_libs
342            .add_mapping(start_avma, end_avma, relative_address_at_start, lib);
343    }
344
345    /// Mark the kernel library at the specified start address as
346    /// unloaded, so that future calls to [`Profile::add_sample`] know about the unloading.
347    pub fn remove_kernel_lib_mapping(&mut self, start_avma: u64) {
348        self.kernel_libs.remove_mapping(start_avma);
349    }
350
351    /// Add an empty thread to the specified process.
352    pub fn add_thread(
353        &mut self,
354        process: ProcessHandle,
355        tid: u32,
356        start_time: Timestamp,
357        is_main: bool,
358    ) -> ThreadHandle {
359        let tid = self.make_unique_tid(tid);
360        let handle = ThreadHandle(self.threads.len());
361        self.threads
362            .push(Thread::new(process, tid, start_time, is_main));
363        self.processes[process.0].add_thread(handle);
364        handle
365    }
366
367    /// Change the name of a thread.
368    pub fn set_thread_name(&mut self, thread: ThreadHandle, name: &str) {
369        self.threads[thread.0].set_name(name);
370    }
371
372    /// Change the start time of a thread.
373    pub fn set_thread_start_time(&mut self, thread: ThreadHandle, start_time: Timestamp) {
374        self.threads[thread.0].set_start_time(start_time);
375    }
376
377    /// Set the end time of a thread.
378    pub fn set_thread_end_time(&mut self, thread: ThreadHandle, end_time: Timestamp) {
379        self.threads[thread.0].set_end_time(end_time);
380    }
381
382    /// Turn the string into in a [`StringHandle`], for use in [`Frame::Label`].
383    pub fn intern_string(&mut self, s: &str) -> StringHandle {
384        StringHandle(self.string_table.index_for_string(s))
385    }
386
387    /// Get the string for a string handle. This is sometimes useful when writing tests.
388    ///
389    /// Panics if the handle wasn't found, which can happen if you pass a handle
390    /// from a different Profile instance.
391    pub fn get_string(&self, handle: StringHandle) -> &str {
392        self.string_table.get_string(handle.0).unwrap()
393    }
394
395    /// Add a sample to the given thread.
396    ///
397    /// The sample has a timestamp, a stack, a CPU delta, and a weight.
398    ///
399    /// The stack frames are supplied as an iterator. Every frame has an associated
400    /// category pair.
401    ///
402    /// The CPU delta is the amount of CPU time that the CPU was busy with work for this
403    /// thread since the previous sample. It should always be less than or equal the
404    /// time delta between the sample timestamps.
405    ///
406    /// The weight affects the sample's stack's score in the call tree. You usually set
407    /// this to 1. You can use weights greater than one if you want to combine multiple
408    /// adjacent samples with the same stack into one sample, to save space. However,
409    /// this discards any CPU deltas between the adjacent samples, so it's only really
410    /// useful if no CPU time has occurred between the samples, and for that use case the
411    /// [`Profile::add_sample_same_stack_zero_cpu`] method should be preferred.
412    ///
413    /// You can can also set the weight to something negative, such as -1, to create a
414    /// "diff profile". For example, if you have partitioned your samples into "before"
415    /// and "after" groups, you can use -1 for all "before" samples and 1 for all "after"
416    /// samples, and the call tree will show you which stacks occur more frequently in
417    /// the "after" part of the profile, by sorting those stacks to the top.
418    pub fn add_sample(
419        &mut self,
420        thread: ThreadHandle,
421        timestamp: Timestamp,
422        frames: impl Iterator<Item = FrameInfo>,
423        cpu_delta: CpuDelta,
424        weight: i32,
425    ) {
426        let stack_index = self.stack_index_for_frames(thread, frames);
427        self.threads[thread.0].add_sample(timestamp, stack_index, cpu_delta, weight);
428    }
429
430    /// Add a sample with a CPU delta of zero. Internally, multiple consecutive
431    /// samples with a delta of zero will be combined into one sample with an accumulated
432    /// weight.
433    pub fn add_sample_same_stack_zero_cpu(
434        &mut self,
435        thread: ThreadHandle,
436        timestamp: Timestamp,
437        weight: i32,
438    ) {
439        self.threads[thread.0].add_sample_same_stack_zero_cpu(timestamp, weight);
440    }
441
442    /// Add a marker to the given thread.
443    pub fn add_marker<T: ProfilerMarker>(
444        &mut self,
445        thread: ThreadHandle,
446        name: &str,
447        marker: T,
448        timing: MarkerTiming,
449    ) {
450        self.marker_schemas
451            .entry(T::MARKER_TYPE_NAME)
452            .or_insert_with(T::schema);
453        self.threads[thread.0].add_marker(name, marker, timing, None);
454    }
455
456    /// Add a marker to the given thread, with a stack.
457    pub fn add_marker_with_stack<T: ProfilerMarker>(
458        &mut self,
459        thread: ThreadHandle,
460        name: &str,
461        marker: T,
462        timing: MarkerTiming,
463        stack_frames: impl Iterator<Item = FrameInfo>,
464    ) {
465        self.marker_schemas
466            .entry(T::MARKER_TYPE_NAME)
467            .or_insert_with(T::schema);
468        let stack_index = self.stack_index_for_frames(thread, stack_frames);
469        self.threads[thread.0].add_marker(name, marker, timing, stack_index);
470    }
471
472    /// Add a data point to a counter. For a memory counter, `value_delta` is the number
473    /// of bytes that have been allocated / deallocated since the previous counter sample, and
474    /// `number_of_operations` is the number of `malloc` / `free` calls since the previous
475    /// counter sample. Both numbers are deltas.
476    ///
477    /// The graph in the profiler UI will connect subsequent data points with diagonal lines.
478    /// Counters are intended for values that are measured at a certain sample rate. You can
479    /// also use them for instrumented events and emit a new data point at every discrete change,
480    /// but in that case you probably want to emit two values per change: one right before (with
481    /// the old value) and one right at the timestamp of change (with the new value). This way
482    /// you'll get more horizontal lines, and the diagonal line will be very short.
483    pub fn add_counter_sample(
484        &mut self,
485        counter: CounterHandle,
486        timestamp: Timestamp,
487        value_delta: f64,
488        number_of_operations_delta: u32,
489    ) {
490        self.counters[counter.0].add_sample(timestamp, value_delta, number_of_operations_delta)
491    }
492
493    // frames is ordered from caller to callee, i.e. root function first, pc last
494    fn stack_index_for_frames(
495        &mut self,
496        thread: ThreadHandle,
497        frames: impl Iterator<Item = FrameInfo>,
498    ) -> Option<usize> {
499        let thread = &mut self.threads[thread.0];
500        let process = &mut self.processes[thread.process().0];
501        let mut prefix = None;
502        for frame_info in frames {
503            let location = match frame_info.frame {
504                Frame::InstructionPointer(ip) => {
505                    process.convert_address(&mut self.global_libs, &mut self.kernel_libs, ip)
506                }
507                Frame::ReturnAddress(ra) => process.convert_address(
508                    &mut self.global_libs,
509                    &mut self.kernel_libs,
510                    ra.saturating_sub(1),
511                ),
512                Frame::RelativeAddressFromInstructionPointer(lib_handle, relative_address) => {
513                    let global_lib_index = self.global_libs.index_for_used_lib(lib_handle);
514                    InternalFrameLocation::AddressInLib(relative_address, global_lib_index)
515                }
516                Frame::RelativeAddressFromReturnAddress(lib_handle, relative_address) => {
517                    let global_lib_index = self.global_libs.index_for_used_lib(lib_handle);
518                    let nudged_relative_address = relative_address.saturating_sub(1);
519                    InternalFrameLocation::AddressInLib(nudged_relative_address, global_lib_index)
520                }
521                Frame::Label(string_index) => {
522                    let thread_string_index =
523                        thread.convert_string_index(&self.string_table, string_index.0);
524                    InternalFrameLocation::Label(thread_string_index)
525                }
526            };
527            let internal_frame = InternalFrame {
528                location,
529                flags: frame_info.flags,
530                category_pair: frame_info.category_pair,
531            };
532            let frame_index = thread.frame_index_for_frame(internal_frame, &self.global_libs);
533            prefix =
534                Some(thread.stack_index_for_stack(prefix, frame_index, frame_info.category_pair));
535        }
536        prefix
537    }
538
539    /// Returns a flattened list of `ThreadHandle`s in the right order.
540    ///
541    // The processed profile format has all threads from all processes in a flattened threads list.
542    // Each thread duplicates some information about its process, which allows the Firefox Profiler
543    // UI to group threads from the same process.
544    fn sorted_threads(&self) -> (Vec<ThreadHandle>, Vec<usize>) {
545        let mut sorted_threads = Vec::with_capacity(self.threads.len());
546        let mut first_thread_index_per_process = Vec::with_capacity(self.processes.len());
547
548        let mut sorted_processes: Vec<_> = (0..self.processes.len()).map(ProcessHandle).collect();
549        sorted_processes.sort_by(|a_handle, b_handle| {
550            let a = &self.processes[a_handle.0];
551            let b = &self.processes[b_handle.0];
552            a.cmp_for_json_order(b)
553        });
554
555        for process in sorted_processes {
556            let prev_len = sorted_threads.len();
557            first_thread_index_per_process.push(prev_len);
558            sorted_threads.extend_from_slice(self.processes[process.0].threads());
559
560            let sorted_threads_for_this_process = &mut sorted_threads[prev_len..];
561            sorted_threads_for_this_process.sort_by(|a_handle, b_handle| {
562                let a = &self.threads[a_handle.0];
563                let b = &self.threads[b_handle.0];
564                a.cmp_for_json_order(b)
565            });
566        }
567
568        (sorted_threads, first_thread_index_per_process)
569    }
570
571    fn serializable_threads<'a>(
572        &'a self,
573        sorted_threads: &'a [ThreadHandle],
574    ) -> SerializableProfileThreadsProperty<'a> {
575        SerializableProfileThreadsProperty {
576            threads: &self.threads,
577            processes: &self.processes,
578            categories: &self.categories,
579            sorted_threads,
580        }
581    }
582
583    fn serializable_counters<'a>(
584        &'a self,
585        first_thread_index_per_process: &'a [usize],
586    ) -> SerializableProfileCountersProperty<'a> {
587        SerializableProfileCountersProperty {
588            counters: &self.counters,
589            first_thread_index_per_process,
590        }
591    }
592
593    fn contains_js_function(&self) -> bool {
594        self.threads.iter().any(|t| t.contains_js_function())
595    }
596}
597
598impl Serialize for Profile {
599    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
600        let (sorted_threads, first_thread_index_per_process) = self.sorted_threads();
601        let mut map = serializer.serialize_map(None)?;
602        map.serialize_entry("meta", &SerializableProfileMeta(self))?;
603        map.serialize_entry("libs", &self.global_libs)?;
604        map.serialize_entry("threads", &self.serializable_threads(&sorted_threads))?;
605        map.serialize_entry("pages", &[] as &[()])?;
606        map.serialize_entry("profilerOverhead", &[] as &[()])?;
607        map.serialize_entry(
608            "counters",
609            &self.serializable_counters(&first_thread_index_per_process),
610        )?;
611        map.end()
612    }
613}
614
615struct SerializableProfileMeta<'a>(&'a Profile);
616
617impl<'a> Serialize for SerializableProfileMeta<'a> {
618    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
619        let mut map = serializer.serialize_map(None)?;
620        map.serialize_entry("categories", &self.0.categories)?;
621        map.serialize_entry("debug", &false)?;
622        map.serialize_entry(
623            "extensions",
624            &json!({
625                "length": 0,
626                "baseURL": [],
627                "id": [],
628                "name": [],
629            }),
630        )?;
631        map.serialize_entry("interval", &(self.0.interval.as_secs_f64() * 1000.0))?;
632        map.serialize_entry("preprocessedProfileVersion", &46)?;
633        map.serialize_entry("processType", &0)?;
634        map.serialize_entry("product", &self.0.product)?;
635        map.serialize_entry(
636            "sampleUnits",
637            &json!({
638                "time": "ms",
639                "eventDelay": "ms",
640                "threadCPUDelta": "µs",
641            }),
642        )?;
643        map.serialize_entry("startTime", &self.0.reference_timestamp)?;
644        map.serialize_entry("symbolicated", &false)?;
645        map.serialize_entry("pausedRanges", &[] as &[()])?;
646        map.serialize_entry("version", &24)?;
647        map.serialize_entry("usesOnlyOneStackType", &(!self.0.contains_js_function()))?;
648        map.serialize_entry("doesNotUseFrameImplementation", &true)?;
649        map.serialize_entry("sourceCodeIsNotOnSearchfox", &true)?;
650
651        let mut marker_schemas: Vec<MarkerSchema> =
652            self.0.marker_schemas.values().cloned().collect();
653        marker_schemas.sort_by_key(|schema| schema.type_name);
654        map.serialize_entry("markerSchema", &marker_schemas)?;
655
656        map.end()
657    }
658}
659
660struct SerializableProfileThreadsProperty<'a> {
661    threads: &'a [Thread],
662    processes: &'a [Process],
663    categories: &'a [Category],
664    sorted_threads: &'a [ThreadHandle],
665}
666
667impl<'a> Serialize for SerializableProfileThreadsProperty<'a> {
668    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
669        let mut seq = serializer.serialize_seq(Some(self.threads.len()))?;
670
671        for thread in self.sorted_threads {
672            let categories = &self.categories;
673            let thread = &self.threads[thread.0];
674            let process = &self.processes[thread.process().0];
675            seq.serialize_element(&SerializableProfileThread(process, thread, categories))?;
676        }
677
678        seq.end()
679    }
680}
681
682struct SerializableProfileCountersProperty<'a> {
683    counters: &'a [Counter],
684    first_thread_index_per_process: &'a [usize],
685}
686
687impl<'a> Serialize for SerializableProfileCountersProperty<'a> {
688    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
689        let mut seq = serializer.serialize_seq(Some(self.counters.len()))?;
690
691        for counter in self.counters {
692            let main_thread_index = self.first_thread_index_per_process[counter.process().0];
693            seq.serialize_element(&counter.as_serializable(main_thread_index))?;
694        }
695
696        seq.end()
697    }
698}
699
700struct SerializableProfileThread<'a>(&'a Process, &'a Thread, &'a [Category]);
701
702impl<'a> Serialize for SerializableProfileThread<'a> {
703    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
704        let SerializableProfileThread(process, thread, categories) = self;
705        let process_start_time = process.start_time();
706        let process_end_time = process.end_time();
707        let process_name = process.name();
708        let pid = process.pid();
709        thread.serialize_with(
710            serializer,
711            categories,
712            process_start_time,
713            process_end_time,
714            process_name,
715            pid,
716        )
717    }
718}