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}