fxprof_processed_profile/
sample_table.rs

1use std::fmt::{Display, Formatter};
2
3use serde::ser::{Serialize, SerializeMap, Serializer};
4
5use crate::cpu_delta::CpuDelta;
6use crate::serialization_helpers::{SerializableSingleValueColumn, SliceWithPermutation};
7use crate::timestamp::{
8    SerializableTimestampSliceAsDeltas, SerializableTimestampSliceAsDeltasWithPermutation,
9    Timestamp,
10};
11
12/// The sample table contains stacks with timestamps and some extra information.
13///
14/// In the most common case, this is used for time-based sampling: At a fixed but
15/// configurable rate, a profiler samples the current stack of each thread and records
16/// it in the profile.
17#[derive(Debug, Clone)]
18pub struct SampleTable {
19    sample_weight_type: WeightType,
20    sample_weights: Vec<i32>,
21    sample_timestamps: Vec<Timestamp>,
22    /// An index into the thread's stack table for each sample. `None` means the empty stack.
23    sample_stack_indexes: Vec<Option<usize>>,
24    /// CPU usage delta since the previous sample for this thread, for each sample.
25    sample_cpu_deltas: Vec<CpuDelta>,
26    is_sorted_by_time: bool,
27    last_sample_timestamp: Timestamp,
28}
29
30/// Specifies the meaning of the "weight" value of a thread's samples.
31#[derive(Debug, Clone)]
32pub enum WeightType {
33    /// The weight is an integer multiplier. For example, "this stack was
34    /// observed n times when sampling at the specified interval."
35    ///
36    /// This affects the total + self score of each call node in the call tree,
37    /// and the order in the tree because the tree is ordered from large "totals"
38    /// to small "totals".
39    /// It also affects the width of the sample's stack's box in the flame graph.
40    Samples,
41    /// The weight is a duration in (fractional) milliseconds.
42    ///
43    /// Note that, since [`Profile::add_sample`](crate::Profile::add_sample) currently
44    /// only accepts integer weight values, the usefulness of `TracingMs` is
45    /// currently limited.
46    TracingMs,
47    /// The weight of each sample is a value in bytes.
48    ///
49    /// This can be used for profiles with allocation stacks. It can also be used
50    /// for "size" profiles which give a bytes breakdown of the contents of a file.
51    Bytes,
52}
53
54impl Display for WeightType {
55    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56        match self {
57            WeightType::Samples => write!(f, "samples"),
58            WeightType::TracingMs => write!(f, "tracing-ms"),
59            WeightType::Bytes => write!(f, "bytes"),
60        }
61    }
62}
63
64impl Serialize for WeightType {
65    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
66        match self {
67            WeightType::Samples => serializer.serialize_str("samples"),
68            WeightType::TracingMs => serializer.serialize_str("tracing-ms"),
69            WeightType::Bytes => serializer.serialize_str("bytes"),
70        }
71    }
72}
73
74impl SampleTable {
75    pub fn new() -> Self {
76        Self {
77            sample_weight_type: WeightType::Samples,
78            sample_weights: Vec::new(),
79            sample_timestamps: Vec::new(),
80            sample_stack_indexes: Vec::new(),
81            sample_cpu_deltas: Vec::new(),
82            is_sorted_by_time: true,
83            last_sample_timestamp: Timestamp::from_nanos_since_reference(0),
84        }
85    }
86
87    pub fn add_sample(
88        &mut self,
89        timestamp: Timestamp,
90        stack_index: Option<usize>,
91        cpu_delta: CpuDelta,
92        weight: i32,
93    ) {
94        self.sample_weights.push(weight);
95        self.sample_timestamps.push(timestamp);
96        self.sample_stack_indexes.push(stack_index);
97        self.sample_cpu_deltas.push(cpu_delta);
98        if timestamp < self.last_sample_timestamp {
99            self.is_sorted_by_time = false;
100        }
101        self.last_sample_timestamp = timestamp;
102    }
103
104    pub fn set_weight_type(&mut self, t: WeightType) {
105        self.sample_weight_type = t;
106    }
107
108    pub fn modify_last_sample(&mut self, timestamp: Timestamp, weight: i32) {
109        *self.sample_weights.last_mut().unwrap() += weight;
110        *self.sample_timestamps.last_mut().unwrap() = timestamp;
111    }
112}
113
114impl Serialize for SampleTable {
115    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
116        let len = self.sample_timestamps.len();
117        let mut map = serializer.serialize_map(None)?;
118        map.serialize_entry("length", &len)?;
119        map.serialize_entry("weightType", &self.sample_weight_type.to_string())?;
120
121        if self.is_sorted_by_time {
122            map.serialize_entry("stack", &self.sample_stack_indexes)?;
123            map.serialize_entry(
124                "timeDeltas",
125                &SerializableTimestampSliceAsDeltas(&self.sample_timestamps),
126            )?;
127            map.serialize_entry("weight", &self.sample_weights)?;
128            map.serialize_entry("threadCPUDelta", &self.sample_cpu_deltas)?;
129        } else {
130            let mut indexes: Vec<usize> = (0..self.sample_timestamps.len()).collect();
131            indexes.sort_unstable_by_key(|index| self.sample_timestamps[*index]);
132            map.serialize_entry(
133                "stack",
134                &SliceWithPermutation(&self.sample_stack_indexes, &indexes),
135            )?;
136            map.serialize_entry(
137                "timeDeltas",
138                &SerializableTimestampSliceAsDeltasWithPermutation(
139                    &self.sample_timestamps,
140                    &indexes,
141                ),
142            )?;
143            map.serialize_entry(
144                "weight",
145                &SliceWithPermutation(&self.sample_weights, &indexes),
146            )?;
147            map.serialize_entry(
148                "threadCPUDelta",
149                &SliceWithPermutation(&self.sample_cpu_deltas, &indexes),
150            )?;
151        }
152        map.end()
153    }
154}
155
156/// JS documentation of the native allocations table:
157///
158/// ```ignore
159/// /**
160///  * This variant is the original version of the table, before the memory address
161///  * and threadId were added.
162///  */
163/// export type UnbalancedNativeAllocationsTable = {|
164///   time: Milliseconds[],
165///   // "weight" is used here rather than "bytes", so that this type will match the
166///   // SamplesLikeTableShape.
167///   weight: Bytes[],
168///   weightType: 'bytes',
169///   stack: Array<IndexIntoStackTable | null>,
170///   length: number,
171/// |};
172///
173/// /**
174///  * The memory address and thread ID were added later.
175///  */
176/// export type BalancedNativeAllocationsTable = {|
177///   ...UnbalancedNativeAllocationsTable,
178///   memoryAddress: number[],
179///   threadId: number[],
180/// |};
181/// ```
182///
183/// In this crate we always create a `BalancedNativeAllocationsTable`. We require
184/// a memory address for each allocation / deallocation sample.
185#[derive(Debug, Clone, Default)]
186pub struct NativeAllocationsTable {
187    /// The timstamps for each sample
188    time: Vec<Timestamp>,
189    /// The stack index for each sample
190    stack: Vec<Option<usize>>,
191    /// The size in bytes (positive for allocations, negative for deallocations) for each sample
192    allocation_size: Vec<i64>,
193    /// The memory address of the allocation for each sample
194    allocation_address: Vec<u64>,
195}
196
197impl NativeAllocationsTable {
198    /// Add a sample to the [`NativeAllocations`] table.
199    pub fn add_sample(
200        &mut self,
201        timestamp: Timestamp,
202        stack_index: Option<usize>,
203        allocation_address: u64,
204        allocation_size: i64,
205    ) {
206        self.time.push(timestamp);
207        self.stack.push(stack_index);
208        self.allocation_address.push(allocation_address);
209        self.allocation_size.push(allocation_size);
210    }
211}
212
213impl Serialize for NativeAllocationsTable {
214    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
215        let len = self.time.len();
216        let mut map = serializer.serialize_map(None)?;
217        map.serialize_entry("time", &self.time)?;
218        map.serialize_entry("weight", &self.allocation_size)?;
219        map.serialize_entry("weightType", &WeightType::Bytes)?;
220        map.serialize_entry("stack", &self.stack)?;
221        map.serialize_entry("memoryAddress", &self.allocation_address)?;
222
223        // The threadId column is currently unused by the Firefox Profiler.
224        // Fill the column with zeros because the type definitions require it to be a number.
225        // A better alternative would be to use thread indexes or the threads' string TIDs.
226        map.serialize_entry("threadId", &SerializableSingleValueColumn(0, len))?;
227
228        map.serialize_entry("length", &len)?;
229        map.end()
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use assert_json_diff::assert_json_eq;
236    use serde_json::json;
237
238    use super::*;
239
240    #[test]
241    fn test_serialize_native_allocations() {
242        // example of `nativeAllocations`:
243        //
244        // "nativeAllocations": {
245        //     "time": [
246        //         274364.1082197344,
247        //         274364.17226073437,
248        //         274364.2063027344,
249        //         274364.2229277344,
250        //         274364.44117773435,
251        //         274366.4713027344,
252        //         274366.48871973436,
253        //         274366.6601777344,
254        //         274366.6705107344
255        //     ],
256        //     "weight": [
257        //         4096,
258        //         -4096,
259        //         4096,
260        //         -4096,
261        //         147456,
262        //         4096,
263        //         -4096,
264        //         96,
265        //         -96
266        //     ],
267        //     "weightType": "bytes",
268        //     "stack": [
269        //         71,
270        //         88,
271        //         119,
272        //         138,
273        //         null,
274        //         171,
275        //         190,
276        //         210,
277        //         214
278        //     ],
279        //     "memoryAddress": [
280        //         4388749312,
281        //         4388749312,
282        //         4388749312,
283        //         4388749312,
284        //         4376330240,
285        //         4388749312,
286        //         4388749312,
287        //         4377576256,
288        //         4377576256
289        //     ],
290        //     "threadId": [
291        //         0,
292        //         0,
293        //         0,
294        //         0,
295        //         0,
296        //         0,
297        //         0,
298        //         0,
299        //         0
300        //     ],
301        //     "length": 9
302        // },
303
304        let mut native_allocations_table = NativeAllocationsTable::default();
305        native_allocations_table.add_sample(
306            Timestamp::from_millis_since_reference(274_363.248_375),
307            None,
308            5969772544,
309            147456,
310        );
311
312        assert_json_eq!(
313            native_allocations_table,
314            json!({
315              "time": [
316                274363.248375
317              ],
318              "weight": [
319                147456
320              ],
321              "weightType": "bytes",
322              "stack": [
323                null
324              ],
325              "memoryAddress": [
326                5969772544u64
327              ],
328              "threadId": [
329                0
330              ],
331              "length": 1
332            })
333        );
334    }
335}