fxprof_processed_profile/
profile.rs

1use std::collections::hash_map::Entry;
2use std::sync::Arc;
3use std::time::Duration;
4
5use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
6use serde_json::json;
7
8use crate::category::{CategoryHandle, CategoryPairHandle, InternalCategory};
9use crate::category_color::CategoryColor;
10use crate::counters::{Counter, CounterHandle};
11use crate::cpu_delta::CpuDelta;
12use crate::fast_hash_map::FastHashMap;
13use crate::frame::{Frame, FrameInfo};
14use crate::frame_table::{InternalFrame, InternalFrameLocation};
15use crate::global_lib_table::{GlobalLibTable, LibraryHandle, UsedLibraryAddressesIterator};
16use crate::lib_mappings::LibMappings;
17use crate::library_info::{LibraryInfo, SymbolTable};
18use crate::markers::{
19    GraphColor, InternalMarkerSchema, Marker, MarkerHandle, MarkerTiming, MarkerTypeHandle,
20    RuntimeSchemaMarkerSchema, StaticSchemaMarker,
21};
22use crate::process::{Process, ThreadHandle};
23use crate::reference_timestamp::ReferenceTimestamp;
24use crate::sample_table::WeightType;
25use crate::string_table::{GlobalStringIndex, GlobalStringTable};
26use crate::thread::{ProcessHandle, Thread};
27use crate::timestamp::Timestamp;
28
29/// The sampling interval used during profile recording.
30///
31/// This doesn't have to match the actual delta between sample timestamps.
32/// It just describes the intended interval.
33///
34/// For profiles without sampling data, this can be set to a meaningless
35/// dummy value.
36#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
37pub struct SamplingInterval {
38    nanos: u64,
39}
40
41impl SamplingInterval {
42    /// Create a sampling interval from a sampling frequency in Hz.
43    ///
44    /// Panics on zero or negative values.
45    pub fn from_hz(samples_per_second: f32) -> Self {
46        assert!(samples_per_second > 0.0);
47        let nanos = (1_000_000_000.0 / samples_per_second) as u64;
48        Self::from_nanos(nanos)
49    }
50
51    /// Create a sampling interval from a value in milliseconds.
52    pub fn from_millis(millis: u64) -> Self {
53        Self::from_nanos(millis * 1_000_000)
54    }
55
56    /// Create a sampling interval from a value in nanoseconds
57    pub fn from_nanos(nanos: u64) -> Self {
58        Self { nanos }
59    }
60
61    /// Convert the interval to nanoseconds.
62    pub fn nanos(&self) -> u64 {
63        self.nanos
64    }
65
66    /// Convert the interval to float seconds.
67    pub fn as_secs_f64(&self) -> f64 {
68        self.nanos as f64 / 1_000_000_000.0
69    }
70}
71
72impl From<Duration> for SamplingInterval {
73    fn from(duration: Duration) -> Self {
74        Self::from_nanos(duration.as_nanos() as u64)
75    }
76}
77
78/// A handle for an interned string, returned from [`Profile::intern_string`].
79#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
80pub struct StringHandle(pub(crate) GlobalStringIndex);
81
82/// A handle to a frame, specific to a thread. Can be created with [`Profile::intern_frame`](crate::Profile::intern_frame).
83#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
84pub struct FrameHandle(ThreadHandle, usize);
85
86/// A handle to a stack, specific to a thread. Can be created with [`Profile::intern_stack`](crate::Profile::intern_stack).
87#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
88pub struct StackHandle(ThreadHandle, usize);
89
90/// Stores the profile data and can be serialized as JSON, via [`serde::Serialize`].
91///
92/// The profile data is organized into a list of processes with threads.
93/// Each thread has its own samples and markers.
94///
95/// ```
96/// use fxprof_processed_profile::{Profile, CategoryHandle, CpuDelta, Frame, FrameInfo, FrameFlags, SamplingInterval, Timestamp};
97/// use std::time::SystemTime;
98///
99/// # fn write_profile(output_file: std::fs::File) -> Result<(), Box<dyn std::error::Error>> {
100/// let mut profile = Profile::new("My app", SystemTime::now().into(), SamplingInterval::from_millis(1));
101/// let process = profile.add_process("App process", 54132, Timestamp::from_millis_since_reference(0.0));
102/// let thread = profile.add_thread(process, 54132000, Timestamp::from_millis_since_reference(0.0), true);
103/// profile.set_thread_name(thread, "Main thread");
104/// let stack_frames = vec![
105///     FrameInfo { frame: Frame::Label(profile.intern_string("Root node")), category_pair: CategoryHandle::OTHER.into(), flags: FrameFlags::empty() },
106///     FrameInfo { frame: Frame::Label(profile.intern_string("First callee")), category_pair: CategoryHandle::OTHER.into(), flags: FrameFlags::empty() }
107/// ];
108/// let stack = profile.intern_stack_frames(thread, stack_frames.into_iter());
109/// profile.add_sample(thread, Timestamp::from_millis_since_reference(0.0), stack, CpuDelta::ZERO, 1);
110///
111/// let writer = std::io::BufWriter::new(output_file);
112/// serde_json::to_writer(writer, &profile)?;
113/// # Ok(())
114/// # }
115/// ```
116#[derive(Debug)]
117pub struct Profile {
118    pub(crate) product: String,
119    pub(crate) os_name: Option<String>,
120    pub(crate) interval: SamplingInterval,
121    pub(crate) global_libs: GlobalLibTable,
122    pub(crate) kernel_libs: LibMappings<LibraryHandle>,
123    pub(crate) categories: Vec<InternalCategory>, // append-only for stable CategoryHandles
124    pub(crate) processes: Vec<Process>,           // append-only for stable ProcessHandles
125    pub(crate) counters: Vec<Counter>,
126    pub(crate) threads: Vec<Thread>, // append-only for stable ThreadHandles
127    pub(crate) initial_visible_threads: Vec<ThreadHandle>,
128    pub(crate) initial_selected_threads: Vec<ThreadHandle>,
129    pub(crate) reference_timestamp: ReferenceTimestamp,
130    pub(crate) string_table: GlobalStringTable,
131    pub(crate) marker_schemas: Vec<InternalMarkerSchema>,
132    static_schema_marker_types: FastHashMap<&'static str, MarkerTypeHandle>,
133    pub(crate) symbolicated: bool,
134    used_pids: FastHashMap<u32, u32>,
135    used_tids: FastHashMap<u32, u32>,
136}
137
138impl Profile {
139    /// Create a new profile.
140    ///
141    /// The `product` is the name of the main application which was profiled.
142    /// The `reference_timestamp` is some arbitrary absolute timestamp which all
143    /// other timestamps in the profile data are relative to. The `interval` is the intended
144    /// time delta between samples.
145    pub fn new(
146        product: &str,
147        reference_timestamp: ReferenceTimestamp,
148        interval: SamplingInterval,
149    ) -> Self {
150        Profile {
151            interval,
152            product: product.to_string(),
153            os_name: None,
154            threads: Vec::new(),
155            initial_visible_threads: Vec::new(),
156            initial_selected_threads: Vec::new(),
157            global_libs: GlobalLibTable::new(),
158            kernel_libs: LibMappings::new(),
159            reference_timestamp,
160            processes: Vec::new(),
161            string_table: GlobalStringTable::new(),
162            marker_schemas: Vec::new(),
163            categories: vec![InternalCategory::new(
164                "Other".to_string(),
165                CategoryColor::Gray,
166            )],
167            static_schema_marker_types: FastHashMap::default(),
168            symbolicated: false,
169            used_pids: FastHashMap::default(),
170            used_tids: FastHashMap::default(),
171            counters: Vec::new(),
172        }
173    }
174
175    /// Change the declared sampling interval.
176    pub fn set_interval(&mut self, interval: SamplingInterval) {
177        self.interval = interval;
178    }
179
180    /// Change the reference timestamp.
181    pub fn set_reference_timestamp(&mut self, reference_timestamp: ReferenceTimestamp) {
182        self.reference_timestamp = reference_timestamp;
183    }
184
185    /// Change the product name.
186    pub fn set_product(&mut self, product: &str) {
187        self.product = product.to_string();
188    }
189
190    /// Set the name of the operating system.
191    pub fn set_os_name(&mut self, os_name: &str) {
192        self.os_name = Some(os_name.to_string());
193    }
194
195    /// Add a category and return its handle.
196    ///
197    /// Categories are used for stack frames and markers, as part of a "category pair".
198    pub fn add_category(&mut self, name: &str, color: CategoryColor) -> CategoryHandle {
199        let handle = CategoryHandle(self.categories.len() as u16);
200        self.categories
201            .push(InternalCategory::new(name.to_string(), color));
202        handle
203    }
204
205    /// Add a subcategory for a category, and return the "category pair" handle.
206    ///
207    /// Every category has a default subcategory; you can convert a `Category` into
208    /// its corresponding `CategoryPairHandle` for the default category using `category.into()`.
209    pub fn add_subcategory(&mut self, category: CategoryHandle, name: &str) -> CategoryPairHandle {
210        let subcategory = self.categories[category.0 as usize].add_subcategory(name.into());
211        CategoryPairHandle(category, subcategory)
212    }
213
214    /// Add an empty process. The name, pid and start time can be changed afterwards,
215    /// but they are required here because they have to be present in the profile JSON.
216    pub fn add_process(&mut self, name: &str, pid: u32, start_time: Timestamp) -> ProcessHandle {
217        let pid = self.make_unique_pid(pid);
218        let handle = ProcessHandle(self.processes.len());
219        self.processes.push(Process::new(name, pid, start_time));
220        handle
221    }
222
223    fn make_unique_pid(&mut self, pid: u32) -> String {
224        Self::make_unique_pid_or_tid(&mut self.used_pids, pid)
225    }
226
227    fn make_unique_tid(&mut self, tid: u32) -> String {
228        Self::make_unique_pid_or_tid(&mut self.used_tids, tid)
229    }
230
231    /// Appends ".1" / ".2" etc. to the pid or tid if needed.
232    ///
233    /// The map contains the next suffix for each pid/tid, or no entry if the pid/tid
234    /// hasn't been used before and needs no suffix.
235    fn make_unique_pid_or_tid(map: &mut FastHashMap<u32, u32>, id: u32) -> String {
236        match map.entry(id) {
237            std::collections::hash_map::Entry::Occupied(mut entry) => {
238                let suffix = *entry.get();
239                *entry.get_mut() += 1;
240                format!("{id}.{suffix}")
241            }
242            std::collections::hash_map::Entry::Vacant(entry) => {
243                entry.insert(1);
244                format!("{id}")
245            }
246        }
247    }
248
249    /// Create a counter. Counters let you make graphs with a time axis and a Y axis. One example of a
250    /// counter is memory usage.
251    ///
252    /// # Example
253    ///
254    /// ```
255    /// use fxprof_processed_profile::{Profile, CategoryHandle, CpuDelta, Frame, SamplingInterval, Timestamp};
256    /// use std::time::SystemTime;
257    ///
258    /// let mut profile = Profile::new("My app", SystemTime::now().into(), SamplingInterval::from_millis(1));
259    /// let process = profile.add_process("App process", 54132, Timestamp::from_millis_since_reference(0.0));
260    /// let memory_counter = profile.add_counter(process, "malloc", "Memory", "Amount of allocated memory");
261    /// profile.add_counter_sample(memory_counter, Timestamp::from_millis_since_reference(0.0), 0.0, 0);
262    /// profile.add_counter_sample(memory_counter, Timestamp::from_millis_since_reference(1.0), 1000.0, 2);
263    /// profile.add_counter_sample(memory_counter, Timestamp::from_millis_since_reference(2.0), 800.0, 1);
264    /// ```
265    pub fn add_counter(
266        &mut self,
267        process: ProcessHandle,
268        name: &str,
269        category: &str,
270        description: &str,
271    ) -> CounterHandle {
272        let handle = CounterHandle(self.counters.len());
273        self.counters.push(Counter::new(
274            name,
275            category,
276            description,
277            process,
278            self.processes[process.0].pid(),
279        ));
280        handle
281    }
282
283    /// Set the color to use when rendering the counter.
284    pub fn set_counter_color(&mut self, counter: CounterHandle, color: GraphColor) {
285        self.counters[counter.0].set_color(color);
286    }
287
288    /// Change the start time of a process.
289    pub fn set_process_start_time(&mut self, process: ProcessHandle, start_time: Timestamp) {
290        self.processes[process.0].set_start_time(start_time);
291    }
292
293    /// Set the end time of a process.
294    pub fn set_process_end_time(&mut self, process: ProcessHandle, end_time: Timestamp) {
295        self.processes[process.0].set_end_time(end_time);
296    }
297
298    /// Change the name of a process.
299    pub fn set_process_name(&mut self, process: ProcessHandle, name: &str) {
300        self.processes[process.0].set_name(name);
301    }
302
303    /// Get the [`LibraryHandle`] for a library. This handle is used in [`Profile::add_lib_mapping`]
304    /// and in the pre-resolved [`Frame`] variants.
305    ///
306    /// Knowing the library information allows symbolication of native stacks once the
307    /// profile is opened in the Firefox Profiler.
308    pub fn add_lib(&mut self, library: LibraryInfo) -> LibraryHandle {
309        self.global_libs.handle_for_lib(library)
310    }
311
312    /// Set the symbol table for a library.
313    ///
314    /// This symbol table can also be specified in the [`LibraryInfo`] which is given to
315    /// [`Profile::add_lib`]. However, sometimes you may want to have the [`LibraryHandle`]
316    /// for a library before you know about all its symbols. In those cases, you can call
317    /// [`Profile::add_lib`] with `symbol_table` set to `None`, and then supply the symbol
318    /// table afterwards.
319    ///
320    /// Symbol tables are optional.
321    pub fn set_lib_symbol_table(&mut self, library: LibraryHandle, symbol_table: Arc<SymbolTable>) {
322        self.global_libs.set_lib_symbol_table(library, symbol_table);
323    }
324
325    /// For a given process, define where in the virtual memory of this process the given library
326    /// is mapped.
327    ///
328    /// Existing mappings which overlap with the range `start_avma..end_avma` will be removed.
329    ///
330    /// A single library can have multiple mappings in the same process.
331    ///
332    /// The new mapping will be respected by future [`Profile::add_sample`] calls, when resolving
333    /// absolute frame addresses to library-relative addresses.
334    pub fn add_lib_mapping(
335        &mut self,
336        process: ProcessHandle,
337        lib: LibraryHandle,
338        start_avma: u64,
339        end_avma: u64,
340        relative_address_at_start: u32,
341    ) {
342        self.processes[process.0].add_lib_mapping(
343            lib,
344            start_avma,
345            end_avma,
346            relative_address_at_start,
347        );
348    }
349
350    /// Mark the library mapping at the specified start address in the specified process as
351    /// unloaded, so that future calls to [`Profile::add_sample`] know about the removal.
352    pub fn remove_lib_mapping(&mut self, process: ProcessHandle, start_avma: u64) {
353        self.processes[process.0].remove_lib_mapping(start_avma);
354    }
355
356    /// Clear all library mappings in the specified process.
357    pub fn clear_process_lib_mappings(&mut self, process: ProcessHandle) {
358        self.processes[process.0].remove_all_lib_mappings();
359    }
360
361    /// Add a kernel library mapping. This allows symbolication of kernel stacks once the profile is
362    /// opened in the Firefox Profiler. Kernel libraries are global and not tied to a process.
363    ///
364    /// Each kernel library covers an address range in the kernel address space, which is
365    /// global across all processes. Future calls to [`Profile::add_sample`] with native
366    /// frames resolve the frame's code address with respect to the currently loaded kernel
367    /// and process libraries.
368    pub fn add_kernel_lib_mapping(
369        &mut self,
370        lib: LibraryHandle,
371        start_avma: u64,
372        end_avma: u64,
373        relative_address_at_start: u32,
374    ) {
375        self.kernel_libs
376            .add_mapping(start_avma, end_avma, relative_address_at_start, lib);
377    }
378
379    /// Mark the kernel library at the specified start address as
380    /// unloaded, so that future calls to [`Profile::add_sample`] know about the unloading.
381    pub fn remove_kernel_lib_mapping(&mut self, start_avma: u64) {
382        self.kernel_libs.remove_mapping(start_avma);
383    }
384
385    /// Add an empty thread to the specified process.
386    pub fn add_thread(
387        &mut self,
388        process: ProcessHandle,
389        tid: u32,
390        start_time: Timestamp,
391        is_main: bool,
392    ) -> ThreadHandle {
393        let tid = self.make_unique_tid(tid);
394        let handle = ThreadHandle(self.threads.len());
395        self.threads
396            .push(Thread::new(process, tid, start_time, is_main));
397        self.processes[process.0].add_thread(handle);
398        handle
399    }
400
401    /// Change the name of a thread.
402    pub fn set_thread_name(&mut self, thread: ThreadHandle, name: &str) {
403        self.threads[thread.0].set_name(name);
404    }
405
406    /// Change the start time of a thread.
407    pub fn set_thread_start_time(&mut self, thread: ThreadHandle, start_time: Timestamp) {
408        self.threads[thread.0].set_start_time(start_time);
409    }
410
411    /// Set the end time of a thread.
412    pub fn set_thread_end_time(&mut self, thread: ThreadHandle, end_time: Timestamp) {
413        self.threads[thread.0].set_end_time(end_time);
414    }
415
416    /// Set the tid (thread ID) of a thread.
417    pub fn set_thread_tid(&mut self, thread: ThreadHandle, tid: u32) {
418        let tid = self.make_unique_tid(tid);
419        self.threads[thread.0].set_tid(tid);
420    }
421
422    /// Set whether to show a timeline which displays [`MarkerLocations::TIMELINE_OVERVIEW`](crate::MarkerLocations::TIMELINE_OVERVIEW)
423    /// markers for this thread.
424    ///
425    /// Main threads always have such a timeline view and always display such markers,
426    /// but non-main threads only do so when specified using this method.
427    pub fn set_thread_show_markers_in_timeline(&mut self, thread: ThreadHandle, v: bool) {
428        self.threads[thread.0].set_show_markers_in_timeline(v);
429    }
430
431    /// Set the weighting type of samples of a thread.
432    ///
433    /// Default is [WeightType::Samples].
434    pub fn set_thread_samples_weight_type(&mut self, thread: ThreadHandle, t: WeightType) {
435        self.threads[thread.0].set_samples_weight_type(t);
436    }
437
438    /// Add a thread as initially visible in the UI.
439    ///
440    /// If not called, the UI uses its own ranking heuristic to choose which
441    /// threads are visible.
442    pub fn add_initial_visible_thread(&mut self, thread: ThreadHandle) {
443        self.initial_visible_threads.push(thread);
444    }
445
446    /// Clear the list of threads marked as initially visible in the UI.
447    pub fn clear_initial_visible_threads(&mut self) {
448        self.initial_visible_threads.clear();
449    }
450
451    /// Add a thread as initially selected in the UI.
452    ///
453    /// If not called, the UI uses its own heuristic to choose which threads
454    /// are initially selected.
455    pub fn add_initial_selected_thread(&mut self, thread: ThreadHandle) {
456        self.initial_selected_threads.push(thread);
457    }
458
459    /// Clear the list of threads marked as initially selected in the UI.
460    pub fn clear_initial_selected_threads(&mut self) {
461        self.initial_selected_threads.clear();
462    }
463
464    /// Turn the string into in a [`StringHandle`], for use in [`Frame::Label`].
465    pub fn intern_string(&mut self, s: &str) -> StringHandle {
466        StringHandle(self.string_table.index_for_string(s))
467    }
468
469    /// Get the string for a string handle. This is sometimes useful when writing tests.
470    ///
471    /// Panics if the handle wasn't found, which can happen if you pass a handle
472    /// from a different Profile instance.
473    pub fn get_string(&self, handle: StringHandle) -> &str {
474        self.string_table.get_string(handle.0).unwrap()
475    }
476
477    /// Get the frame handle for a stack frame.
478    ///
479    /// The returned handle can only be used with this thread.
480    pub fn intern_frame(&mut self, thread: ThreadHandle, frame_info: FrameInfo) -> FrameHandle {
481        let thread_handle = thread;
482        let thread = &mut self.threads[thread.0];
483        let process = &mut self.processes[thread.process().0];
484        let frame_index = Self::intern_frame_internal(
485            thread,
486            process,
487            frame_info,
488            &mut self.global_libs,
489            &mut self.kernel_libs,
490            &self.string_table,
491        );
492        FrameHandle(thread_handle, frame_index)
493    }
494
495    /// Get the stack handle for a stack with the given `frame` and `parent`,
496    /// for the given thread.
497    ///
498    /// The returned stack handle can be used with [`Profile::add_sample`] and
499    /// [`Profile::set_marker_stack`], but only for samples / markers of the same
500    /// thread.
501    ///
502    /// If `parent` is `None`, this creates a root stack node. Otherwise, `parent`
503    /// is the caller of the returned stack node.
504    pub fn intern_stack(
505        &mut self,
506        thread: ThreadHandle,
507        parent: Option<StackHandle>,
508        frame: FrameHandle,
509    ) -> StackHandle {
510        let thread_handle = thread;
511        let prefix = match parent {
512            Some(StackHandle(parent_thread_handle, prefix_stack_index)) => {
513                assert_eq!(
514                    parent_thread_handle, thread_handle,
515                    "StackHandle from different thread passed to Profile::intern_stack"
516                );
517                Some(prefix_stack_index)
518            }
519            None => None,
520        };
521        let FrameHandle(frame_thread_handle, frame_index) = frame;
522        assert_eq!(
523            frame_thread_handle, thread_handle,
524            "FrameHandle from different thread passed to Profile::intern_stack"
525        );
526        let thread = &mut self.threads[thread.0];
527        let stack_index = thread.stack_index_for_stack(prefix, frame_index);
528        StackHandle(thread_handle, stack_index)
529    }
530
531    /// Get the stack handle for a stack whose frames are given by an iterator.
532    ///
533    /// The stack frames yielded by the iterator need to be ordered from caller-most
534    /// to callee-most.
535    ///
536    /// Returns `None` if the stack has zero frames.
537    pub fn intern_stack_frames(
538        &mut self,
539        thread: ThreadHandle,
540        frames: impl Iterator<Item = FrameInfo>,
541    ) -> Option<StackHandle> {
542        let stack_index = self.stack_index_for_frames(thread, frames)?;
543        Some(StackHandle(thread, stack_index))
544    }
545
546    /// Add a sample to the given thread.
547    ///
548    /// The sample has a timestamp, a stack, a CPU delta, and a weight.
549    ///
550    /// To get the stack handle, you can use [`Profile::intern_stack`] or
551    /// [`Profile::intern_stack_frames`].
552    ///
553    /// The CPU delta is the amount of CPU time that the CPU was busy with work for this
554    /// thread since the previous sample. It should always be less than or equal the
555    /// time delta between the sample timestamps.
556    ///
557    /// The weight affects the sample's stack's score in the call tree. You usually set
558    /// this to 1. You can use weights greater than one if you want to combine multiple
559    /// adjacent samples with the same stack into one sample, to save space. However,
560    /// this discards any CPU deltas between the adjacent samples, so it's only really
561    /// useful if no CPU time has occurred between the samples, and for that use case the
562    /// [`Profile::add_sample_same_stack_zero_cpu`] method should be preferred.
563    ///
564    /// You can can also set the weight to something negative, such as -1, to create a
565    /// "diff profile". For example, if you have partitioned your samples into "before"
566    /// and "after" groups, you can use -1 for all "before" samples and 1 for all "after"
567    /// samples, and the call tree will show you which stacks occur more frequently in
568    /// the "after" part of the profile, by sorting those stacks to the top.
569    pub fn add_sample(
570        &mut self,
571        thread: ThreadHandle,
572        timestamp: Timestamp,
573        stack: Option<StackHandle>,
574        cpu_delta: CpuDelta,
575        weight: i32,
576    ) {
577        let stack_index = match stack {
578            Some(StackHandle(stack_thread_handle, stack_index)) => {
579                assert_eq!(
580                    stack_thread_handle, thread,
581                    "StackHandle from different thread passed to Profile::add_sample"
582                );
583                Some(stack_index)
584            }
585            None => None,
586        };
587        self.threads[thread.0].add_sample(timestamp, stack_index, cpu_delta, weight);
588    }
589
590    /// Add a sample with a CPU delta of zero. Internally, multiple consecutive
591    /// samples with a delta of zero will be combined into one sample with an accumulated
592    /// weight.
593    pub fn add_sample_same_stack_zero_cpu(
594        &mut self,
595        thread: ThreadHandle,
596        timestamp: Timestamp,
597        weight: i32,
598    ) {
599        self.threads[thread.0].add_sample_same_stack_zero_cpu(timestamp, weight);
600    }
601
602    /// Add an allocation or deallocation sample to the given thread. This is used
603    /// to collect stacks showing where allocations and deallocations happened.
604    ///
605    /// When loading profiles with allocation samples in the Firefox Profiler, the
606    /// UI will display a dropdown above the call tree to switch between regular
607    /// samples and allocation samples.
608    ///
609    /// An allocation sample has a timestamp, a stack, a memory address, and an allocation size.
610    ///
611    /// The size should be in bytes, with positive values for allocations and negative
612    /// values for deallocations.
613    ///
614    /// The memory address allows correlating the allocation and deallocation stacks of the
615    /// same object. This lets the UI display just the stacks for objects which haven't
616    /// been deallocated yet ("Retained memory").
617    ///
618    /// To avoid having to capture stacks for every single allocation, you can sample just
619    /// a subset of allocations. The sampling should be done based on the allocation size
620    /// ("probability per byte"). The decision whether to sample should be done at
621    /// allocation time and remembered for the lifetime of the allocation, so that for
622    /// each allocated object you either sample both its allocation and deallocation, or
623    /// neither.
624    ///
625    /// To get the stack handle, you can use [`Profile::intern_stack`] or
626    /// [`Profile::intern_stack_frames`].
627    pub fn add_allocation_sample(
628        &mut self,
629        thread: ThreadHandle,
630        timestamp: Timestamp,
631        stack: Option<StackHandle>,
632        allocation_address: u64,
633        allocation_size: i64,
634    ) {
635        // The profile format strictly separates sample data from different threads.
636        // For allocation samples, this separation is a bit unfortunate, especially
637        // when it comes to the "Retained Memory" panel which shows allocation stacks
638        // for just objects that haven't been deallocated yet. This panel is per-thread,
639        // and it needs to know about deallocations even if they happened on a different
640        // thread from the allocation.
641        // To resolve this conundrum, for now, we will put all allocation and deallocation
642        // samples on a single thread per process, regardless of what thread they actually
643        // happened on.
644        // The Gecko profiler puts all allocation samples on the main thread, for example.
645        // Here in fxprof-processed-profile, we just deem the first thread of each process
646        // as the processes "allocation thread".
647        let process_handle = self.threads[thread.0].process();
648        let process = &self.processes[process_handle.0];
649        let allocation_thread_handle = process.thread_handle_for_allocations().unwrap();
650        let stack_index = match stack {
651            Some(StackHandle(stack_thread_handle, stack_index)) => {
652                assert_eq!(
653                    stack_thread_handle, thread,
654                    "StackHandle from different thread passed to Profile::add_sample"
655                );
656                Some(stack_index)
657            }
658            None => None,
659        };
660        self.threads[allocation_thread_handle.0].add_allocation_sample(
661            timestamp,
662            stack_index,
663            allocation_address,
664            allocation_size,
665        );
666    }
667
668    /// Registers a marker type for a [`RuntimeSchemaMarkerSchema`]. You only need to call this for
669    /// marker types whose schema is dynamically created at runtime.
670    ///
671    /// After you register the marker type, you'll save its [`MarkerTypeHandle`] somewhere, and then
672    /// store it in every marker you create of this type. The marker then needs to return the
673    /// handle from its implementation of [`Marker::marker_type`].
674    ///
675    /// For marker types whose schema is known at compile time, you'll want to implement
676    /// [`StaticSchemaMarker`] instead, and you don't need to call this method.
677    pub fn register_marker_type(&mut self, schema: RuntimeSchemaMarkerSchema) -> MarkerTypeHandle {
678        let handle = MarkerTypeHandle(self.marker_schemas.len());
679        self.marker_schemas.push(schema.into());
680        handle
681    }
682
683    /// Returns the marker type handle for a type that implements [`StaticSchemaMarker`].
684    ///
685    /// You usually don't need to call this, ever. It is called by the blanket impl
686    /// of [`Marker::marker_type`] for all types which implement [`StaticSchemaMarker`].
687    pub fn static_schema_marker_type<T: StaticSchemaMarker>(&mut self) -> MarkerTypeHandle {
688        match self
689            .static_schema_marker_types
690            .entry(T::UNIQUE_MARKER_TYPE_NAME)
691        {
692            Entry::Occupied(entry) => *entry.get(),
693            Entry::Vacant(entry) => {
694                let handle = MarkerTypeHandle(self.marker_schemas.len());
695                let schema = InternalMarkerSchema::from_static_schema::<T>();
696                self.marker_schemas.push(schema);
697                entry.insert(handle);
698                handle
699            }
700        }
701    }
702
703    /// Add a marker to the given thread.
704    ///
705    /// The marker handle that's returned by this method can be used in [`Profile::set_marker_stack`].
706    ///
707    /// ```
708    /// use fxprof_processed_profile::{
709    ///     Profile, CategoryHandle, Marker, MarkerFieldFlags, MarkerFieldFormat, MarkerTiming,
710    ///     StaticSchemaMarker, StaticSchemaMarkerField, StringHandle, ThreadHandle, Timestamp,
711    /// };
712    ///
713    /// # fn fun() {
714    /// # let profile: Profile = panic!();
715    /// # let thread: ThreadHandle = panic!();
716    /// # let start_time: Timestamp = panic!();
717    /// # let end_time: Timestamp = panic!();
718    /// let name = profile.intern_string("Marker name");
719    /// let text = profile.intern_string("Marker text");
720    /// let my_marker = TextMarker { name, text };
721    /// profile.add_marker(thread, MarkerTiming::Interval(start_time, end_time), my_marker);
722    /// # }
723    ///
724    /// #[derive(Debug, Clone)]
725    /// pub struct TextMarker {
726    ///   pub name: StringHandle,
727    ///   pub text: StringHandle,
728    /// }
729    ///
730    /// impl StaticSchemaMarker for TextMarker {
731    ///     const UNIQUE_MARKER_TYPE_NAME: &'static str = "Text";
732    ///
733    ///     const CHART_LABEL: Option<&'static str> = Some("{marker.data.text}");
734    ///     const TABLE_LABEL: Option<&'static str> = Some("{marker.name} - {marker.data.text}");
735    ///
736    ///     const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
737    ///         key: "text",
738    ///         label: "Contents",
739    ///         format: MarkerFieldFormat::String,
740    ///         flags: MarkerFieldFlags::SEARCHABLE,
741    ///     }];
742    ///
743    ///     fn name(&self, _profile: &mut Profile) -> StringHandle {
744    ///         self.name
745    ///     }
746    ///
747    ///     fn category(&self, _profile: &mut Profile) -> CategoryHandle {
748    ///         CategoryHandle::OTHER
749    ///     }
750    ///
751    ///     fn string_field_value(&self, _field_index: u32) -> StringHandle {
752    ///         self.text
753    ///     }
754    ///
755    ///     fn number_field_value(&self, _field_index: u32) -> f64 {
756    ///         unreachable!()
757    ///     }
758    /// }
759    /// ```
760    pub fn add_marker<T: Marker>(
761        &mut self,
762        thread: ThreadHandle,
763        timing: MarkerTiming,
764        marker: T,
765    ) -> MarkerHandle {
766        let marker_type = marker.marker_type(self);
767        let name = marker.name(self);
768        let category = marker.category(self);
769        let thread = &mut self.threads[thread.0];
770        let name_thread_string_index = thread.convert_string_index(&self.string_table, name.0);
771        let schema = &self.marker_schemas[marker_type.0];
772        thread.add_marker(
773            name_thread_string_index,
774            marker_type,
775            schema,
776            marker,
777            timing,
778            category,
779            &mut self.string_table,
780        )
781    }
782
783    /// Sets a marker's stack. Every marker can have an optional stack, regardless
784    /// of its marker type.
785    ///
786    /// A marker's stack is shown in its tooltip, and in the sidebar in the marker table
787    /// panel if a marker with a stack is selected.
788    ///
789    /// To get the stack handle, you can use [`Profile::intern_stack`] or
790    /// [`Profile::intern_stack_frames`].
791    pub fn set_marker_stack(
792        &mut self,
793        thread: ThreadHandle,
794        marker: MarkerHandle,
795        stack: Option<StackHandle>,
796    ) {
797        let stack_index = match stack {
798            Some(StackHandle(stack_thread_handle, stack_index)) => {
799                assert_eq!(
800                    stack_thread_handle, thread,
801                    "StackHandle from different thread passed to Profile::add_sample"
802                );
803                Some(stack_index)
804            }
805            None => None,
806        };
807        self.threads[thread.0].set_marker_stack(marker, stack_index);
808    }
809
810    /// Add a data point to a counter. For a memory counter, `value_delta` is the number
811    /// of bytes that have been allocated / deallocated since the previous counter sample, and
812    /// `number_of_operations` is the number of `malloc` / `free` calls since the previous
813    /// counter sample. Both numbers are deltas.
814    ///
815    /// The graph in the profiler UI will connect subsequent data points with diagonal lines.
816    /// Counters are intended for values that are measured at a time-based sample rate; for example,
817    /// you could add a counter sample once every millisecond with the current memory usage.
818    ///
819    /// Alternatively, you can emit a new data point only whenever the value changes.
820    /// In that case you probably want to emit two values per change: one right before (with
821    /// the old value) and one right at the timestamp of change (with the new value). This way
822    /// you'll get more horizontal lines, and the diagonal line will be very short.
823    pub fn add_counter_sample(
824        &mut self,
825        counter: CounterHandle,
826        timestamp: Timestamp,
827        value_delta: f64,
828        number_of_operations_delta: u32,
829    ) {
830        self.counters[counter.0].add_sample(timestamp, value_delta, number_of_operations_delta)
831    }
832
833    fn intern_frame_internal(
834        thread: &mut Thread,
835        process: &mut Process,
836        frame_info: FrameInfo,
837        global_libs: &mut GlobalLibTable,
838        kernel_libs: &mut LibMappings<LibraryHandle>,
839        string_table: &GlobalStringTable,
840    ) -> usize {
841        let location = match frame_info.frame {
842            Frame::InstructionPointer(ip) => process.convert_address(global_libs, kernel_libs, ip),
843            Frame::ReturnAddress(ra) => {
844                process.convert_address(global_libs, kernel_libs, ra.saturating_sub(1))
845            }
846            Frame::AdjustedReturnAddress(ara) => {
847                process.convert_address(global_libs, kernel_libs, ara)
848            }
849            Frame::RelativeAddressFromInstructionPointer(lib_handle, relative_address) => {
850                let global_lib_index = global_libs.index_for_used_lib(lib_handle);
851                InternalFrameLocation::AddressInLib(relative_address, global_lib_index)
852            }
853            Frame::RelativeAddressFromReturnAddress(lib_handle, relative_address) => {
854                let global_lib_index = global_libs.index_for_used_lib(lib_handle);
855                let adjusted_relative_address = relative_address.saturating_sub(1);
856                InternalFrameLocation::AddressInLib(adjusted_relative_address, global_lib_index)
857            }
858            Frame::RelativeAddressFromAdjustedReturnAddress(
859                lib_handle,
860                adjusted_relative_address,
861            ) => {
862                let global_lib_index = global_libs.index_for_used_lib(lib_handle);
863                InternalFrameLocation::AddressInLib(adjusted_relative_address, global_lib_index)
864            }
865            Frame::Label(string_index) => {
866                let thread_string_index = thread.convert_string_index(string_table, string_index.0);
867                InternalFrameLocation::Label(thread_string_index)
868            }
869        };
870        let internal_frame = InternalFrame {
871            location,
872            flags: frame_info.flags,
873            category_pair: frame_info.category_pair,
874        };
875        thread.frame_index_for_frame(internal_frame, global_libs)
876    }
877
878    /// Set whether the profile is already symbolicated.
879    ///
880    /// Read: whether symbols are resolved.
881    ///
882    /// If your samples refer to labels instead of addresses, it is safe
883    /// to set to true.
884    ///
885    /// Setting to true prevents the Firefox Profiler from attempting to
886    /// resolve symbols.
887    ///
888    /// By default, this is set to false. This causes the Firefox Profiler
889    /// to look up symbols for any address-based [`Frame`], i.e. any frame
890    /// which is not a [`Frame::Label`].
891    ///
892    /// If you use address-based frames and supply your own symbols using
893    /// [`Profile::add_lib`] or [`Profile::set_lib_symbol_table`], you can
894    /// choose to set this to true and avoid another symbol lookup, or you
895    /// can leave it set to false if there is a way to obtain richer symbol
896    /// information than the information supplied in those symbol tables.
897    ///
898    /// For example, when samply creates a profile which includes JIT frames,
899    /// and there is a Jitdump file with symbol information about those JIT
900    /// frames, samply uses [`Profile::set_lib_symbol_table`] to provide
901    /// the function names for the JIT functions. But it does not call
902    /// [`Profile::set_symbolicated`] with true, because the Jitdump files may
903    /// include additional information that's not in the [`SymbolTable`],
904    /// specifically the Jitdump file may have file name and line number information.
905    /// This information is only added into the profile by the Firefox Profiler's
906    /// resolution of symbols: The Firefox Profiler requests symbol information
907    /// for the JIT frame addresses from samply's symbol server, at which point
908    /// samply obtains the richer information from the Jitdump file and returns
909    /// it via the symbol server response.
910    pub fn set_symbolicated(&mut self, v: bool) {
911        self.symbolicated = v;
912    }
913
914    // frames is ordered from caller to callee, i.e. root function first, pc last
915    fn stack_index_for_frames(
916        &mut self,
917        thread: ThreadHandle,
918        frames: impl Iterator<Item = FrameInfo>,
919    ) -> Option<usize> {
920        let thread = &mut self.threads[thread.0];
921        let process = &mut self.processes[thread.process().0];
922        let mut prefix = None;
923        for frame_info in frames {
924            let frame_index = Self::intern_frame_internal(
925                thread,
926                process,
927                frame_info,
928                &mut self.global_libs,
929                &mut self.kernel_libs,
930                &self.string_table,
931            );
932            prefix = Some(thread.stack_index_for_stack(prefix, frame_index));
933        }
934        prefix
935    }
936
937    /// Returns a flattened list of `ThreadHandle`s in the right order.
938    ///
939    // The processed profile format has all threads from all processes in a flattened threads list.
940    // Each thread duplicates some information about its process, which allows the Firefox Profiler
941    // UI to group threads from the same process.
942    fn sorted_threads(&self) -> (Vec<ThreadHandle>, Vec<usize>, Vec<usize>) {
943        let mut sorted_threads = Vec::with_capacity(self.threads.len());
944        let mut first_thread_index_per_process = vec![0; self.processes.len()];
945        let mut new_thread_indices = vec![0; self.threads.len()];
946
947        let mut sorted_processes: Vec<_> = (0..self.processes.len()).map(ProcessHandle).collect();
948        sorted_processes.sort_by(|a_handle, b_handle| {
949            let a = &self.processes[a_handle.0];
950            let b = &self.processes[b_handle.0];
951            a.cmp_for_json_order(b)
952        });
953
954        for process in sorted_processes {
955            let prev_len = sorted_threads.len();
956            first_thread_index_per_process[process.0] = prev_len;
957            sorted_threads.extend_from_slice(self.processes[process.0].threads());
958
959            let sorted_threads_for_this_process = &mut sorted_threads[prev_len..];
960            sorted_threads_for_this_process.sort_by(|a_handle, b_handle| {
961                let a = &self.threads[a_handle.0];
962                let b = &self.threads[b_handle.0];
963                a.cmp_for_json_order(b)
964            });
965
966            for (i, v) in sorted_threads_for_this_process.iter().enumerate() {
967                new_thread_indices[v.0] = prev_len + i;
968            }
969        }
970
971        (
972            sorted_threads,
973            first_thread_index_per_process,
974            new_thread_indices,
975        )
976    }
977
978    fn serializable_threads<'a>(
979        &'a self,
980        sorted_threads: &'a [ThreadHandle],
981    ) -> SerializableProfileThreadsProperty<'a> {
982        SerializableProfileThreadsProperty {
983            threads: &self.threads,
984            processes: &self.processes,
985            sorted_threads,
986            marker_schemas: &self.marker_schemas,
987            global_string_table: &self.string_table,
988        }
989    }
990
991    fn serializable_counters<'a>(
992        &'a self,
993        first_thread_index_per_process: &'a [usize],
994    ) -> SerializableProfileCountersProperty<'a> {
995        SerializableProfileCountersProperty {
996            counters: &self.counters,
997            first_thread_index_per_process,
998        }
999    }
1000
1001    fn contains_js_function(&self) -> bool {
1002        self.threads.iter().any(|t| t.contains_js_function())
1003    }
1004
1005    pub fn lib_used_rva_iter(&self) -> UsedLibraryAddressesIterator {
1006        self.global_libs.lib_used_rva_iter()
1007    }
1008}
1009
1010impl Serialize for Profile {
1011    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1012        let (sorted_threads, first_thread_index_per_process, new_thread_indices) =
1013            self.sorted_threads();
1014        let mut map = serializer.serialize_map(None)?;
1015        map.serialize_entry("meta", &SerializableProfileMeta(self, &new_thread_indices))?;
1016        map.serialize_entry("libs", &self.global_libs)?;
1017        map.serialize_entry("threads", &self.serializable_threads(&sorted_threads))?;
1018        map.serialize_entry("pages", &[] as &[()])?;
1019        map.serialize_entry("profilerOverhead", &[] as &[()])?;
1020        map.serialize_entry(
1021            "counters",
1022            &self.serializable_counters(&first_thread_index_per_process),
1023        )?;
1024        map.end()
1025    }
1026}
1027
1028struct SerializableProfileMeta<'a>(&'a Profile, &'a [usize]);
1029
1030impl Serialize for SerializableProfileMeta<'_> {
1031    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1032        let mut map = serializer.serialize_map(None)?;
1033        map.serialize_entry("categories", &self.0.categories)?;
1034        map.serialize_entry("debug", &false)?;
1035        map.serialize_entry(
1036            "extensions",
1037            &json!({
1038                "length": 0,
1039                "baseURL": [],
1040                "id": [],
1041                "name": [],
1042            }),
1043        )?;
1044        map.serialize_entry("interval", &(self.0.interval.as_secs_f64() * 1000.0))?;
1045        map.serialize_entry("preprocessedProfileVersion", &55)?;
1046        map.serialize_entry("processType", &0)?;
1047        map.serialize_entry("product", &self.0.product)?;
1048        if let Some(os_name) = &self.0.os_name {
1049            map.serialize_entry("oscpu", os_name)?;
1050        }
1051        map.serialize_entry(
1052            "sampleUnits",
1053            &json!({
1054                "time": "ms",
1055                "eventDelay": "ms",
1056                "threadCPUDelta": "µs",
1057            }),
1058        )?;
1059        map.serialize_entry("startTime", &self.0.reference_timestamp)?;
1060        map.serialize_entry("symbolicated", &self.0.symbolicated)?;
1061        map.serialize_entry("pausedRanges", &[] as &[()])?;
1062        map.serialize_entry("version", &24)?; // this version is ignored, only "preprocessedProfileVersion" is used
1063        map.serialize_entry("usesOnlyOneStackType", &(!self.0.contains_js_function()))?;
1064        map.serialize_entry("sourceCodeIsNotOnSearchfox", &true)?;
1065
1066        let mut marker_schemas: Vec<InternalMarkerSchema> = self.0.marker_schemas.clone();
1067        marker_schemas.sort_by(|a, b| a.type_name().cmp(b.type_name()));
1068        map.serialize_entry("markerSchema", &marker_schemas)?;
1069
1070        if !self.0.initial_visible_threads.is_empty() {
1071            map.serialize_entry(
1072                "initialVisibleThreads",
1073                &self
1074                    .0
1075                    .initial_visible_threads
1076                    .iter()
1077                    .map(|x| self.1[x.0])
1078                    .collect::<Vec<_>>(),
1079            )?;
1080        }
1081
1082        if !self.0.initial_selected_threads.is_empty() {
1083            map.serialize_entry(
1084                "initialSelectedThreads",
1085                &self
1086                    .0
1087                    .initial_selected_threads
1088                    .iter()
1089                    .map(|x| self.1[x.0])
1090                    .collect::<Vec<_>>(),
1091            )?;
1092        };
1093
1094        map.end()
1095    }
1096}
1097
1098struct SerializableProfileThreadsProperty<'a> {
1099    threads: &'a [Thread],
1100    processes: &'a [Process],
1101    sorted_threads: &'a [ThreadHandle],
1102    marker_schemas: &'a [InternalMarkerSchema],
1103    global_string_table: &'a GlobalStringTable,
1104}
1105
1106impl Serialize for SerializableProfileThreadsProperty<'_> {
1107    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1108        let mut seq = serializer.serialize_seq(Some(self.threads.len()))?;
1109
1110        for thread in self.sorted_threads {
1111            let thread = &self.threads[thread.0];
1112            let process = &self.processes[thread.process().0];
1113            let marker_schemas = self.marker_schemas;
1114            let global_string_table = self.global_string_table;
1115            seq.serialize_element(&SerializableProfileThread(
1116                process,
1117                thread,
1118                marker_schemas,
1119                global_string_table,
1120            ))?;
1121        }
1122
1123        seq.end()
1124    }
1125}
1126
1127struct SerializableProfileCountersProperty<'a> {
1128    counters: &'a [Counter],
1129    first_thread_index_per_process: &'a [usize],
1130}
1131
1132impl Serialize for SerializableProfileCountersProperty<'_> {
1133    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1134        let mut seq = serializer.serialize_seq(Some(self.counters.len()))?;
1135
1136        for counter in self.counters {
1137            let main_thread_index = self.first_thread_index_per_process[counter.process().0];
1138            seq.serialize_element(&counter.as_serializable(main_thread_index))?;
1139        }
1140
1141        seq.end()
1142    }
1143}
1144
1145struct SerializableProfileThread<'a>(
1146    &'a Process,
1147    &'a Thread,
1148    &'a [InternalMarkerSchema],
1149    &'a GlobalStringTable,
1150);
1151
1152impl Serialize for SerializableProfileThread<'_> {
1153    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1154        let SerializableProfileThread(process, thread, marker_schemas, global_string_table) = self;
1155        let process_start_time = process.start_time();
1156        let process_end_time = process.end_time();
1157        let process_name = process.name();
1158        let pid = process.pid();
1159        thread.serialize_with(
1160            serializer,
1161            process_start_time,
1162            process_end_time,
1163            process_name,
1164            pid,
1165            marker_schemas,
1166            global_string_table,
1167        )
1168    }
1169}