1use diagnostics_data::{DiagnosticsHierarchy, ExtendedMoniker, InspectData};
8use std::collections::BTreeMap;
9
10const CHILD_PROPORTION_DISPLAY_THRESHOLD: f64 = 0.005;
12
13#[derive(Debug, PartialEq)]
14pub struct SelfProfilesReport {
15 name: String,
16 root_summary: DurationSummary,
17 custom_rollups: Vec<CustomRollup>,
18}
19
20impl SelfProfilesReport {
21 pub fn from_snapshot(data: &[InspectData]) -> Result<Vec<Self>, AnalysisError> {
22 let mut summaries = vec![];
23 for d in data {
24 if let Some(s) = Self::from_single_snapshot(d) {
25 summaries.push(s?);
26 }
27 }
28 Ok(summaries)
29 }
30
31 pub fn from_single_snapshot(data: &InspectData) -> Option<Result<Self, AnalysisError>> {
32 if let Some(payload) = data.payload.as_ref() {
33 for child_node in &payload.children {
34 if child_node.get_property("__profile_durations_root").and_then(|p| p.boolean())
35 == Some(true)
36 {
37 return Some(Self::from_node(&data.moniker, child_node));
38 }
39 }
40 }
41 None
42 }
43
44 fn from_node(
45 name: &ExtendedMoniker,
46 node: &DiagnosticsHierarchy,
47 ) -> Result<Self, AnalysisError> {
48 let root_summary = DurationSummaryBuilder::from_inspect(node)?.build();
49 Ok(Self { name: name.to_string(), root_summary, custom_rollups: vec![] })
50 }
51
52 pub fn name(&self) -> &str {
53 &self.name
54 }
55
56 pub fn root_summary(&self) -> &DurationSummary {
57 &self.root_summary
58 }
59
60 pub fn leaf_durations(&self) -> Vec<(String, DurationSummary)> {
61 let mut leaves = BTreeMap::new();
62 self.root_summary.summarize_leaves(&self.name, &mut leaves);
63
64 let mut leaves = leaves.into_iter().collect::<Vec<_>>();
65 leaves.sort_by_key(|(_, duration)| duration.runtime.cpu_time);
66 leaves.reverse();
67
68 leaves
69 }
70
71 pub fn matching_durations(&self, match_prefixes: &[String]) -> Vec<(String, DurationSummary)> {
72 let mut matches = BTreeMap::new();
73 self.root_summary.summarize_matches(&self.name, match_prefixes, &mut matches);
74
75 let mut matches = matches.into_iter().collect::<Vec<_>>();
76 matches.sort_by_key(|(_, duration)| duration.runtime.cpu_time);
77 matches.reverse();
78
79 matches
80 }
81
82 pub fn delta_from(&self, baseline: &Self) -> Result<Self, ComparisonError> {
83 if self.name != baseline.name {
84 return Err(ComparisonError::MismatchedNames {
85 lhs: self.name.clone(),
86 rhs: baseline.name.clone(),
87 });
88 }
89 Ok(Self {
90 name: self.name.clone(),
91 root_summary: self.root_summary.delta_from(&baseline.root_summary)?,
92 custom_rollups: vec![],
93 })
94 }
95
96 pub fn add_rollup(
97 &mut self,
98 title: impl Into<String>,
99 prefixes: impl IntoIterator<Item = impl Into<String>>,
100 ) {
101 self.custom_rollups.push(CustomRollup {
102 display_title: title.into(),
103 match_prefixes: prefixes.into_iter().map(|s| s.into()).collect(),
104 });
105 }
106}
107
108impl std::fmt::Display for SelfProfilesReport {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 writeln!(f, "Profile duration summary for `{}`:\n\n{}\n", self.name, self.root_summary)?;
111
112 let root_runtime = self.root_summary.runtime;
113
114 writeln!(f, "Rolled up leaf durations:\n")?;
115 for (name, duration) in &self.leaf_durations() {
116 let proportion_of_total =
117 duration.runtime.cpu_time as f64 / root_runtime.cpu_time as f64;
118 if proportion_of_total >= CHILD_PROPORTION_DISPLAY_THRESHOLD {
119 write!(f, "{}", duration.display_tree(name, root_runtime))?;
120 }
121 }
122
123 for rollup in &self.custom_rollups {
124 writeln!(f, "Custom rollup: {}\n", rollup.display_title)?;
125
126 for (name, duration) in &self.matching_durations(&rollup.match_prefixes) {
127 write!(f, "{}", duration.display_tree(name, root_runtime))?;
128 }
129 }
130
131 Ok(())
132 }
133}
134
135#[derive(Debug, Default)]
136struct DurationSummaryBuilder {
137 count: u64,
138 runtime: TaskRuntimeInfo,
139 location: String,
140 children: BTreeMap<String, Self>,
141}
142
143impl DurationSummaryBuilder {
144 fn from_inspect(node: &DiagnosticsHierarchy) -> Result<Self, AnalysisError> {
145 let mut children = BTreeMap::new();
146 for child_node in &node.children {
147 let (name, child) = Self::from_inspect_recursive(child_node)?;
148 children.insert(name, child);
149 }
150 let location = node.get_property("location").unwrap().string().unwrap().to_owned();
151
152 let runtime = children.values().map(|c| c.runtime).sum();
154
155 Ok(Self { runtime, count: 0, children, location })
156 }
157
158 fn from_inspect_recursive(
159 node: &DiagnosticsHierarchy,
160 ) -> Result<(String, Self), AnalysisError> {
161 let count = node.get_property("count").unwrap().uint().unwrap();
162 let runtime = TaskRuntimeInfo {
163 cpu_time: get_time_property(node, "cpu_time")?,
164 queue_time: get_time_property(node, "queue_time")?,
165 page_fault_time: get_time_property(node, "page_fault_time")?,
166 lock_contention_time: get_time_property(node, "lock_contention_time")?,
167 wall_time: get_time_property(node, "wall_time")?,
168 };
169 let location = node.get_property("location").unwrap().string().unwrap().to_owned();
170
171 let mut children = BTreeMap::new();
172 for child_node in &node.children {
173 let (name, child) = Self::from_inspect_recursive(child_node)?;
174 children.insert(name, child);
175 }
176
177 Ok((node.name.clone(), Self { count, runtime, children, location }))
178 }
179
180 fn build(&self) -> DurationSummary {
181 let mut children = vec![];
182 for (name, child) in &self.children {
183 children.push((name.clone(), child.build()));
184 }
185
186 children.sort_by_key(|(_, analysis)| analysis.runtime.cpu_time);
188 children.reverse();
189
190 DurationSummary {
191 count: self.count,
192 runtime: self.runtime,
193 location: self.location.clone(),
194 children,
195 }
196 }
197}
198
199#[derive(Debug, Clone, PartialEq)]
200pub struct DurationSummary {
201 count: u64,
202 runtime: TaskRuntimeInfo,
203 location: String,
204 children: Vec<(String, Self)>,
205}
206
207impl DurationSummary {
208 pub fn location(&self) -> &str {
210 self.location.as_str()
211 }
212
213 pub fn count(&self) -> u64 {
215 self.count
216 }
217
218 pub fn cpu_time(&self) -> i64 {
220 self.runtime.cpu_time
221 }
222
223 pub fn queue_time(&self) -> i64 {
226 self.runtime.queue_time
227 }
228
229 pub fn page_fault_time(&self) -> i64 {
231 self.runtime.page_fault_time
232 }
233
234 pub fn lock_contention_time(&self) -> i64 {
236 self.runtime.lock_contention_time
237 }
238
239 pub fn wall_time(&self) -> i64 {
241 self.runtime.wall_time
242 }
243
244 pub fn children(&self) -> impl Iterator<Item = (&str, &Self)> {
246 self.children.iter().map(|(name, summary)| (name.as_str(), summary))
247 }
248
249 fn summarize_leaves(&self, own_name: &str, leaves: &mut BTreeMap<String, Self>) {
250 if self.children.is_empty() {
251 match leaves.entry(own_name.to_string()) {
252 std::collections::btree_map::Entry::Vacant(v) => {
253 v.insert(DurationSummary {
254 count: self.count,
255 runtime: self.runtime,
256 location: self.location.clone(),
257 children: vec![],
258 });
259 }
260 std::collections::btree_map::Entry::Occupied(mut o) => {
261 let leaf = o.get_mut();
262 leaf.runtime += self.runtime;
263 leaf.count += self.count;
264 }
265 }
266 } else {
267 for (name, child) in &self.children {
268 child.summarize_leaves(name, leaves);
269 }
270 }
271 }
272
273 fn summarize_matches(
274 &self,
275 own_name: &str,
276 match_prefixes: &[String],
277 out: &mut BTreeMap<String, Self>,
278 ) {
279 if match_prefixes.iter().any(|prefix| own_name.starts_with(prefix.as_str())) {
280 match out.entry(own_name.to_string()) {
281 std::collections::btree_map::Entry::Vacant(v) => {
282 v.insert(DurationSummary {
283 count: self.count,
284 runtime: self.runtime,
285 location: self.location.clone(),
286 children: vec![],
287 });
288 }
289 std::collections::btree_map::Entry::Occupied(mut o) => {
290 let leaf = o.get_mut();
291 leaf.runtime += self.runtime;
292 leaf.count += self.count;
293 }
294 }
295 }
296
297 for (name, child) in &self.children {
298 child.summarize_matches(name, match_prefixes, out);
299 }
300 }
301
302 fn display_leaf_no_location(
303 &self,
304 name: &str,
305 count: u64,
306 runtime: TaskRuntimeInfo,
307 ) -> termtree::Tree<DurationRuntimeWithPercentage> {
308 let mut leaf = termtree::Tree::new(DurationRuntimeWithPercentage {
309 name: name.to_owned(),
310 count,
311 runtime,
312 location: None,
313 portion_of_parent_cpu_time: runtime.cpu_time as f64 / self.runtime.cpu_time as f64,
314 });
315 leaf.set_multiline(true);
316 leaf
317 }
318
319 fn display_tree(
320 &self,
321 name: &str,
322 parent_total_runtime: TaskRuntimeInfo,
323 ) -> termtree::Tree<DurationRuntimeWithPercentage> {
324 let mut tree = termtree::Tree::new(DurationRuntimeWithPercentage {
325 name: name.to_owned(),
326 count: self.count,
327 runtime: self.runtime,
328 location: Some(self.location.clone()),
329 portion_of_parent_cpu_time: self.runtime.cpu_time as f64
330 / parent_total_runtime.cpu_time as f64,
331 });
332 tree.set_multiline(true);
333
334 let mut etc_time = TaskRuntimeInfo::default();
335 let mut etc_count = 0;
336 for (name, child) in &self.children {
337 let portion_of_self = child.runtime.cpu_time as f64 / self.runtime.cpu_time as f64;
338 if portion_of_self > CHILD_PROPORTION_DISPLAY_THRESHOLD {
339 tree.push(child.display_tree(name, self.runtime));
340 } else {
341 etc_count += child.count;
342 etc_time += child.runtime;
343 }
344 }
345 if !self.children.is_empty() {
346 let unaccounted_runtime =
348 self.runtime - self.children.iter().map(|(_, c)| c.runtime).sum();
349 let portion_unaccounted =
350 unaccounted_runtime.cpu_time as f64 / self.runtime.cpu_time as f64;
351 if portion_unaccounted > CHILD_PROPORTION_DISPLAY_THRESHOLD {
352 tree.push(self.display_leaf_no_location("UNACCOUNTED", 0, unaccounted_runtime));
353 } else {
354 etc_time += unaccounted_runtime;
355 }
356 }
357 if etc_time.cpu_time > 0 {
358 tree.push(self.display_leaf_no_location("BELOW_THRESHOLD", etc_count, etc_time));
359 }
360
361 tree
362 }
363
364 fn delta_from(&self, baseline: &Self) -> Result<Self, ComparisonError> {
365 let count = self.count - baseline.count;
366 let runtime = self.runtime - baseline.runtime;
367 if self.location != baseline.location {
368 return Err(ComparisonError::MismatchedLocations {
369 lhs: self.location.clone(),
370 rhs: baseline.location.clone(),
371 });
372 }
373
374 let mut delta_children = BTreeMap::<String, Self>::new();
375 let baseline_children = baseline
376 .children
377 .iter()
378 .map(|(n, s)| (n, s))
379 .collect::<BTreeMap<_, _>>();
380 for (name, summary) in &self.children {
381 if let Some(baseline_summary) = baseline_children.get(name) {
382 delta_children.insert(name.clone(), summary.delta_from(baseline_summary)?);
383 } else {
384 delta_children.insert(name.clone(), summary.clone());
385 }
386 }
387
388 let mut children = delta_children.into_iter().collect::<Vec<_>>();
390 children.sort_by_key(|(_, analysis)| analysis.runtime.cpu_time);
391 children.reverse();
392
393 Ok(Self { count, runtime, children, location: self.location.clone() })
394 }
395}
396
397impl std::fmt::Display for DurationSummary {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 let total_child_runtime = self.children().map(|(_, c)| c.runtime).sum();
400 write!(f, "{}", self.display_tree("overall", total_child_runtime))
401 }
402}
403
404fn get_time_property(
405 node: &DiagnosticsHierarchy,
406 name: &'static str,
407) -> Result<i64, AnalysisError> {
408 let property = node.get_property(name).ok_or(AnalysisError::MissingTime { name })?;
409 property
410 .number_as_int()
411 .ok_or_else(|| AnalysisError::WrongType { name, property: property.name().to_string() })
412}
413
414#[derive(Debug)]
415pub struct DurationRuntimeWithPercentage {
416 name: String,
417 location: Option<String>,
418 portion_of_parent_cpu_time: f64,
419 count: u64,
420 runtime: TaskRuntimeInfo,
421}
422
423impl std::fmt::Display for DurationRuntimeWithPercentage {
424 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
425 write!(f, "{:^5.3}%: {}", self.portion_of_parent_cpu_time * 100.0, self.name)?;
426 if self.count > 0 {
427 write!(f, ", {}x", self.count)?;
428 }
429 writeln!(
430 f,
431 "\n cpu: {}, queue: {}, faulting: {}, contended: {}, wall: {}",
432 ns_to_ms(self.runtime.cpu_time),
433 ns_to_ms(self.runtime.queue_time),
434 ns_to_ms(self.runtime.page_fault_time),
435 ns_to_ms(self.runtime.lock_contention_time),
436 ns_to_ms(self.runtime.wall_time),
437 )?;
438 if let Some(location) = &self.location {
439 writeln!(f, " source: {location}")?;
440 }
441 Ok(())
442 }
443}
444
445fn ns_to_ms(ns: i64) -> String {
446 format!("{:^5.3}ms", ns as f64 / 1_000_000.0)
447}
448
449#[derive(Debug, thiserror::Error)]
450pub enum ComparisonError {
451 #[error("Can't compare profiles from two different sources `{lhs}` and `{rhs}`.")]
452 MismatchedNames { lhs: String, rhs: String },
453
454 #[error("Can't compare profiles which disagree on the source location for a duration: `{lhs}` vs `{rhs}`")]
455 MismatchedLocations { lhs: String, rhs: String },
456}
457
458#[derive(Debug, thiserror::Error)]
460pub enum AnalysisError {
461 #[error("Profile duration inspect node without `{name}` property.")]
462 MissingTime { name: &'static str },
463
464 #[error(
465 "Profile duration inspect node's `{name}` property had a non-integer type: {property:?}"
466 )]
467 WrongType { name: &'static str, property: String },
468}
469
470#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
471struct TaskRuntimeInfo {
472 cpu_time: i64,
473 queue_time: i64,
474 page_fault_time: i64,
475 lock_contention_time: i64,
476 wall_time: i64,
477}
478
479impl std::ops::Add for TaskRuntimeInfo {
480 type Output = Self;
481 fn add(self, rhs: Self) -> Self::Output {
482 Self {
483 cpu_time: self.cpu_time + rhs.cpu_time,
484 queue_time: self.queue_time + rhs.queue_time,
485 page_fault_time: self.page_fault_time + rhs.page_fault_time,
486 lock_contention_time: self.lock_contention_time + rhs.lock_contention_time,
487 wall_time: self.wall_time + rhs.wall_time,
488 }
489 }
490}
491
492impl std::ops::AddAssign for TaskRuntimeInfo {
493 fn add_assign(&mut self, rhs: Self) {
494 self.cpu_time += rhs.cpu_time;
495 self.queue_time += rhs.queue_time;
496 self.page_fault_time += rhs.page_fault_time;
497 self.lock_contention_time += rhs.lock_contention_time;
498 self.wall_time += rhs.wall_time;
499 }
500}
501
502impl std::ops::Sub for TaskRuntimeInfo {
503 type Output = Self;
504 fn sub(self, rhs: Self) -> Self::Output {
505 Self {
506 cpu_time: self.cpu_time - rhs.cpu_time,
507 queue_time: self.queue_time - rhs.queue_time,
508 page_fault_time: self.page_fault_time - rhs.page_fault_time,
509 lock_contention_time: self.lock_contention_time - rhs.lock_contention_time,
510 wall_time: self.wall_time - rhs.wall_time,
511 }
512 }
513}
514
515impl std::ops::SubAssign for TaskRuntimeInfo {
516 fn sub_assign(&mut self, rhs: Self) {
517 self.cpu_time -= rhs.cpu_time;
518 self.queue_time -= rhs.queue_time;
519 self.page_fault_time -= rhs.page_fault_time;
520 self.lock_contention_time -= rhs.lock_contention_time;
521 self.wall_time -= rhs.wall_time;
522 }
523}
524
525impl std::iter::Sum for TaskRuntimeInfo {
526 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
527 iter.fold(Self::default(), |l, r| l + r)
528 }
529}
530
531#[derive(Debug, PartialEq)]
532struct CustomRollup {
533 display_title: String,
534 match_prefixes: Vec<String>,
535}