Serenity Operating System
1/*
2 * Copyright (c) 2021, Nicholas Hollett <niax@niax.co.uk>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "FlameGraphView.h"
8#include "Profile.h"
9#include <AK/Function.h>
10#include <LibGUI/Painter.h>
11#include <LibGUI/Widget.h>
12#include <LibGfx/Font/FontDatabase.h>
13#include <LibGfx/Forward.h>
14#include <LibGfx/Palette.h>
15
16namespace Profiler {
17
18constexpr int bar_rounding = 2;
19constexpr int bar_margin = 2;
20constexpr int bar_padding = 8;
21constexpr int bar_height = 20;
22constexpr int text_threshold = 30;
23
24Vector<Gfx::Color> s_colors;
25
26static Vector<Gfx::Color> const& get_colors()
27{
28 if (s_colors.is_empty()) {
29 // Start with a nice orange, then make shades of it
30 Gfx::Color midpoint(255, 94, 19);
31 s_colors.extend(midpoint.shades(3, 0.5f));
32 s_colors.append(midpoint);
33 s_colors.extend(midpoint.tints(3, 0.5f));
34 }
35
36 return s_colors;
37}
38
39FlameGraphView::FlameGraphView(GUI::Model& model, int text_column, int width_column)
40 : m_model(model)
41 , m_text_column(text_column)
42 , m_width_column(width_column)
43{
44 set_fill_with_background_color(true);
45 set_background_role(Gfx::ColorRole::Base);
46 set_scrollbars_enabled(true);
47 set_frame_thickness(0);
48 set_should_hide_unnecessary_scrollbars(false);
49 horizontal_scrollbar().set_visible(false);
50
51 m_model.register_client(*this);
52
53 m_colors = get_colors();
54 layout_bars();
55 scroll_to_bottom();
56}
57
58GUI::ModelIndex FlameGraphView::hovered_index() const
59{
60 if (!m_hovered_bar)
61 return GUI::ModelIndex();
62 return m_hovered_bar->index;
63}
64
65void FlameGraphView::model_did_update(unsigned)
66{
67 m_selected_indexes.clear();
68 layout_bars();
69 update();
70}
71
72void FlameGraphView::mousemove_event(GUI::MouseEvent& event)
73{
74 StackBar* hovered_bar = nullptr;
75
76 for (size_t i = 0; i < m_bars.size(); ++i) {
77 auto& bar = m_bars[i];
78 if (to_widget_rect(bar.rect).contains(event.x(), event.y())) {
79 hovered_bar = &bar;
80 break;
81 }
82 }
83
84 if (m_hovered_bar == hovered_bar)
85 return;
86
87 m_hovered_bar = hovered_bar;
88
89 if (on_hover_change)
90 on_hover_change();
91
92 DeprecatedString label = "";
93 if (m_hovered_bar != nullptr && m_hovered_bar->index.is_valid()) {
94 label = bar_label(*m_hovered_bar);
95 }
96 set_tooltip(label);
97 show_or_hide_tooltip();
98
99 update();
100}
101
102void FlameGraphView::mousedown_event(GUI::MouseEvent& event)
103{
104 if (event.button() != GUI::MouseButton::Primary)
105 return;
106
107 if (!m_hovered_bar)
108 return;
109
110 m_selected_indexes.clear();
111 GUI::ModelIndex selected_index = m_hovered_bar->index;
112 while (selected_index.is_valid()) {
113 m_selected_indexes.append(selected_index);
114 selected_index = selected_index.parent();
115 }
116
117 layout_bars();
118 update();
119}
120
121void FlameGraphView::resize_event(GUI::ResizeEvent& event)
122{
123 auto old_scroll = vertical_scrollbar().value();
124
125 AbstractScrollableWidget::resize_event(event);
126
127 // Adjust scroll to keep the bottom of the graph fixed
128 auto available_height_delta = m_old_available_size.height() - available_size().height();
129
130 vertical_scrollbar().set_value(old_scroll + available_height_delta);
131
132 layout_bars();
133
134 m_old_available_size = available_size();
135}
136
137void FlameGraphView::paint_event(GUI::PaintEvent& event)
138{
139 GUI::Painter painter(*this);
140 painter.add_clip_rect(event.rect());
141
142 auto content_clip_rect = to_content_rect(event.rect());
143
144 for (auto const& bar : m_bars) {
145 if (!content_clip_rect.intersects_vertically(bar.rect))
146 continue;
147
148 auto label = bar_label(bar);
149
150 auto color = m_colors[label.hash() % m_colors.size()];
151
152 if (&bar == m_hovered_bar)
153 color = color.lightened(1.2f);
154
155 if (bar.selected)
156 color = color.with_alpha(128);
157
158 auto rect = to_widget_rect(bar.rect);
159
160 // Do rounded corners if the node will draw with enough width
161 if (rect.width() > (bar_rounding * 3))
162 painter.fill_rect_with_rounded_corners(rect.shrunken(0, bar_margin), color, bar_rounding);
163 else
164 painter.fill_rect(rect.shrunken(0, bar_margin), color);
165
166 if (rect.width() > text_threshold) {
167 painter.draw_text(
168 rect.shrunken(bar_padding, 0),
169 label,
170 painter.font(),
171 Gfx::TextAlignment::CenterLeft,
172 Gfx::Color::Black,
173 Gfx::TextElision::Right);
174 }
175 }
176}
177
178DeprecatedString FlameGraphView::bar_label(StackBar const& bar) const
179{
180 auto label_index = bar.index.sibling_at_column(m_text_column);
181 DeprecatedString label = "All";
182 if (label_index.is_valid()) {
183 label = m_model.data(label_index).to_deprecated_string();
184 }
185 return label;
186}
187
188void FlameGraphView::layout_bars()
189{
190 m_bars.clear();
191 m_hovered_bar = nullptr;
192
193 // Explicit copy here so the layout can mutate
194 Vector<GUI::ModelIndex> selected = m_selected_indexes;
195 GUI::ModelIndex null_index;
196 layout_children(null_index, 0, 0, available_size().width(), selected);
197
198 // Translate bars from (-height..0) to (0..height) now that we know the height,
199 // use available height as minimum to keep the graph at the bottom when it's small
200 int height = available_size().height();
201
202 for (auto& bar : m_bars)
203 height = max(height, -bar.rect.top());
204 for (auto& bar : m_bars)
205 bar.rect.translate_by(0, height);
206
207 // Update scrollbars if height changed
208 if (height != content_size().height()) {
209 auto old_content_height = content_size().height();
210 auto old_scroll = vertical_scrollbar().value();
211
212 set_content_size(Gfx::IntSize(available_size().width(), height));
213
214 // Adjust scroll to keep the bottom of the graph fixed, so it doesn't jump
215 // around when double-clicking
216 auto content_height_delta = old_content_height - content_size().height();
217
218 vertical_scrollbar().set_value(old_scroll - content_height_delta);
219 }
220}
221
222void FlameGraphView::layout_children(GUI::ModelIndex& index, int depth, int left, int right, Vector<GUI::ModelIndex>& selected_nodes)
223{
224 auto available_width = right - left;
225 if (available_width < 1)
226 return;
227
228 auto y = -(bar_height * depth) - bar_height;
229
230 u32 node_event_count = 0;
231 if (!index.is_valid()) {
232 // We're at the root, so calculate the event count across all roots
233 for (auto i = 0; i < m_model.row_count(index); ++i) {
234 auto const& root = *static_cast<ProfileNode const*>(m_model.index(i).internal_data());
235 node_event_count += root.event_count();
236 }
237 m_bars.append({ {}, { left, y, available_width, bar_height }, false });
238 } else {
239 auto const* node = static_cast<ProfileNode const*>(index.internal_data());
240
241 bool selected = !selected_nodes.is_empty();
242 if (selected) {
243 VERIFY(selected_nodes.take_last() == index);
244 }
245
246 node_event_count = node->event_count();
247
248 Gfx::IntRect node_rect { left, y, available_width, bar_height };
249 m_bars.append({ index, node_rect, selected });
250 }
251
252 float width_per_sample = static_cast<float>(available_width) / node_event_count;
253 float new_left = static_cast<float>(left);
254
255 for (auto i = 0; i < m_model.row_count(index); ++i) {
256 auto child_index = m_model.index(i, 0, index);
257 if (!child_index.is_valid())
258 continue;
259
260 if (!selected_nodes.is_empty()) {
261 if (selected_nodes.last() != child_index)
262 continue;
263
264 layout_children(child_index, depth + 1, left, right, selected_nodes);
265 return;
266 }
267
268 auto const* child = static_cast<ProfileNode const*>(child_index.internal_data());
269 float child_width = width_per_sample * child->event_count();
270 layout_children(child_index, depth + 1, static_cast<int>(new_left), static_cast<int>(new_left + child_width), selected_nodes);
271 new_left += child_width;
272 }
273}
274
275}