Skip to content

Commit b2e556e

Browse files
authored
Add ability to sort scopes in table view by fields other than count (#243)
### Checklist * [x] I have read the [Contributor Guide](../CONTRIBUTING.md) * [x] I have read and agree to the [Code of Conduct](../CODE_OF_CONDUCT.md) * [x] I have added a description of my changes and why I'd like them included in the section below ### Description of Changes I wanted to be able to sort the scopes in table views by fields other than count. This PR is nowhere near complete, but it does work and shows one way to implement this feature. If this direction makes sense then I'm happy to put in the work to get it up to snuff. If not, then at least this branch exists for anyone who wants it. Not all header fields are supported right now, but for the numerical fields, clicking on the header will sort the scopes in descending order by that field. Clicking the same header again will sort in ascending order. An arrow indicates which column is sorted and in what direction. To implement this, a new field `sort_order` is added to `ProfilerUI`. For example, when you open, the default remains the same, sorted by Count in descending order: <img width="782" alt="Screenshot 2025-06-24 at 6 02 53 PM" src="https://github.com/user-attachments/assets/327f7489-db1b-49d4-a365-a25c750bc30a" /> Here's the result of clicking on Total Self Time twice: <img width="782" alt="Screenshot 2025-06-24 at 6 02 59 PM" src="https://github.com/user-attachments/assets/2d150ebe-3284-4281-9963-0fc8bf0bf98e" /> ### Related Issues See #126
1 parent 3a27093 commit b2e556e

File tree

2 files changed

+125
-10
lines changed

2 files changed

+125
-10
lines changed

puffin_egui/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,9 @@ pub struct ProfilerUi {
318318
/// When did we last run a pass to pack all the frames?
319319
#[cfg_attr(feature = "serde", serde(skip))]
320320
last_pack_pass: Option<web_time::Instant>,
321+
322+
/// Order to sort scopes in table view
323+
sort_order: stats::SortOrder,
321324
}
322325

323326
impl Default for ProfilerUi {
@@ -330,6 +333,10 @@ impl Default for ProfilerUi {
330333
max_num_latest: 1,
331334
slowest_frame: 0.16,
332335
last_pack_pass: None,
336+
sort_order: stats::SortOrder {
337+
key: stats::SortKey::Count,
338+
rev: true,
339+
},
333340
}
334341
}
335342
}
@@ -573,6 +580,7 @@ impl ProfilerUi {
573580
&mut self.stats_options,
574581
frame_view.scope_collection(),
575582
&frames.frames,
583+
&mut self.sort_order,
576584
),
577585
}
578586
}

puffin_egui/src/stats.rs

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,119 @@ pub struct Options {
88
filter: Filter,
99
}
1010

11+
#[derive(Copy, Clone, PartialEq, Eq)]
12+
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
13+
pub enum SortKey {
14+
Location,
15+
FunctionName,
16+
ScopeName,
17+
Count,
18+
Size,
19+
TotalSelfTime,
20+
MeanSelfTime,
21+
MaxSelfTime,
22+
}
23+
24+
/// Determines the order of scopes in table view.
25+
#[derive(Copy, Clone)]
26+
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
27+
pub struct SortOrder {
28+
/// Which column to sort scopes by
29+
pub key: SortKey,
30+
31+
/// Reverse order, if true sort in descending order.
32+
/// If false sort in ascending order.
33+
pub rev: bool,
34+
}
35+
36+
impl SortOrder {
37+
fn sort_scopes(&self, scopes: &mut [(&Key, ScopeStats)], scope_infos: &ScopeCollection) {
38+
match self.key {
39+
SortKey::Location => {
40+
scopes.sort_by_key(|(key, _scope_stats)| {
41+
if let Some(scope_details) = scope_infos.fetch_by_id(&key.id) {
42+
scope_details.location()
43+
} else {
44+
String::new()
45+
}
46+
});
47+
}
48+
SortKey::FunctionName => {
49+
scopes.sort_by_key(|(key, _scope_stats)| {
50+
if let Some(scope_details) = scope_infos.fetch_by_id(&key.id) {
51+
scope_details.function_name.as_str()
52+
} else {
53+
""
54+
}
55+
});
56+
}
57+
SortKey::ScopeName => {
58+
scopes.sort_by_key(|(key, _scope_stats)| {
59+
if let Some(scope_details) = scope_infos.fetch_by_id(&key.id) {
60+
if let Some(name) = &scope_details.scope_name {
61+
name.as_ref()
62+
} else {
63+
""
64+
}
65+
} else {
66+
""
67+
}
68+
});
69+
}
70+
SortKey::Count => {
71+
scopes.sort_by_key(|(_key, scope_stats)| scope_stats.count);
72+
}
73+
SortKey::Size => {
74+
scopes.sort_by_key(|(_key, scope_stats)| scope_stats.bytes);
75+
}
76+
SortKey::TotalSelfTime => {
77+
scopes.sort_by_key(|(_key, scope_stats)| scope_stats.total_self_ns);
78+
}
79+
SortKey::MeanSelfTime => {
80+
scopes.sort_by_key(|(_key, scope_stats)| {
81+
scope_stats.total_self_ns as usize / scope_stats.count
82+
});
83+
}
84+
SortKey::MaxSelfTime => {
85+
scopes.sort_by_key(|(_key, scope_stats)| scope_stats.max_ns);
86+
}
87+
}
88+
if self.rev {
89+
scopes.reverse();
90+
}
91+
}
92+
93+
fn get_arrow(&self) -> &str {
94+
if self.rev { "⏷" } else { "⏶" }
95+
}
96+
97+
fn toggle(&mut self) {
98+
self.rev = !self.rev;
99+
}
100+
}
101+
102+
fn header_label(ui: &mut egui::Ui, name: &str, sort_key: SortKey, sort_order: &mut SortOrder) {
103+
if sort_order.key == sort_key {
104+
if ui
105+
.strong(format!("{} {}", name, sort_order.get_arrow()))
106+
.clicked()
107+
{
108+
sort_order.toggle();
109+
}
110+
} else if ui.strong(name).clicked() {
111+
*sort_order = SortOrder {
112+
key: sort_key,
113+
rev: true,
114+
}
115+
}
116+
}
117+
11118
pub fn ui(
12119
ui: &mut egui::Ui,
13120
options: &mut Options,
14121
scope_infos: &ScopeCollection,
15122
frames: &[std::sync::Arc<UnpackedFrameData>],
123+
sort_order: &mut SortOrder,
16124
) {
17125
let mut threads = std::collections::HashSet::<&ThreadInfo>::new();
18126
let mut stats = Stats::default();
@@ -50,8 +158,7 @@ pub fn ui(
50158
.map(|(key, value)| (key, *value))
51159
.collect();
52160
scopes.sort_by_key(|(key, _)| *key);
53-
scopes.sort_by_key(|(_key, scope_stats)| scope_stats.count);
54-
scopes.reverse();
161+
sort_order.sort_scopes(&mut scopes, scope_infos);
55162

56163
egui::ScrollArea::horizontal().show(ui, |ui| {
57164
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
@@ -66,28 +173,28 @@ pub fn ui(
66173
.columns(egui_extras::Column::auto().resizable(false), 6)
67174
.header(20.0, |mut header| {
68175
header.col(|ui| {
69-
ui.strong("Location");
176+
header_label(ui, "Location", SortKey::Location, sort_order);
70177
});
71178
header.col(|ui| {
72-
ui.strong("Function Name");
179+
header_label(ui, "Function Name", SortKey::FunctionName, sort_order);
73180
});
74181
header.col(|ui| {
75-
ui.strong("Scope Name");
182+
header_label(ui, "Scope Name", SortKey::ScopeName, sort_order);
76183
});
77184
header.col(|ui| {
78-
ui.strong("Count");
185+
header_label(ui, "Count", SortKey::Count, sort_order);
79186
});
80187
header.col(|ui| {
81-
ui.strong("Size");
188+
header_label(ui, "Size", SortKey::Size, sort_order);
82189
});
83190
header.col(|ui| {
84-
ui.strong("Total self time");
191+
header_label(ui, "Total self time", SortKey::TotalSelfTime, sort_order);
85192
});
86193
header.col(|ui| {
87-
ui.strong("Mean self time");
194+
header_label(ui, "Mean self time", SortKey::MeanSelfTime, sort_order);
88195
});
89196
header.col(|ui| {
90-
ui.strong("Max self time");
197+
header_label(ui, "Max self time", SortKey::MaxSelfTime, sort_order);
91198
});
92199
})
93200
.body(|mut body| {

0 commit comments

Comments
 (0)