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}