Serenity Operating System
at master 275 lines 8.1 kB view raw
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}